diff --git a/asyncapi/.gitattributes b/asyncapi/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/asyncapi/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/asyncapi/.github/dependabot.yml b/asyncapi/.github/dependabot.yml new file mode 100644 index 0000000..f8a2d40 --- /dev/null +++ b/asyncapi/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: gomod + directory: / + schedule: + interval: weekly + day: sunday + time: '18:31' diff --git a/asyncapi/.gitignore b/asyncapi/.gitignore new file mode 100644 index 0000000..6f6a3a4 --- /dev/null +++ b/asyncapi/.gitignore @@ -0,0 +1,28 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with 'go test -c' +*.test + +# Output files, e.g. from the go coverage tool +*.out + +# IDE directories +.idea +.vscode +.schemas +.history + +# Build and Environment +cmd/app/app +dist/ + +# Log Files +*.log + +# macOS files +*.DS_Store diff --git a/asyncapi/LICENSE b/asyncapi/LICENSE new file mode 100644 index 0000000..23b34fd --- /dev/null +++ b/asyncapi/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright The Linux Foundation + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/asyncapi/README.md b/asyncapi/README.md new file mode 100644 index 0000000..42f5bd2 --- /dev/null +++ b/asyncapi/README.md @@ -0,0 +1,85 @@ +
+ +[![Go Reference](https://pkg.go.dev/badge/github.com/MarkRosemaker/asyncapi.svg)](https://pkg.go.dev/github.com/MarkRosemaker/asyncapi) +[![Go Report Card](https://goreportcard.com/badge/github.com/MarkRosemaker/asyncapi)](https://goreportcard.com/report/github.com/MarkRosemaker/asyncapi) +![Code Coverage](https://img.shields.io/badge/coverage-86.2%25-brightgreen) +[![License: Apache](https://img.shields.io/badge/License-Apache-yellow.svg)](./LICENSE) + +
+ +

+ Transform and master your event-driven API specs with ease. +

+ +Package asyncapi provides a suite of tools for working with [AsyncAPI](https://www.asyncapi.com) specifications, making it easier to parse, format, manipulate, and generate code from these specs. + +It is the counterpart of [MarkRosemaker/openapi](https://github.com/MarkRosemaker/openapi) for event-driven APIs and follows the same design, so both packages can be used side by side. + +## Introduction + +The primary goals of this package are: + +- **Parsing** AsyncAPI specifications into a structured format. +- **Validating** the specifications strictly against the rules of the specification. +- **Formatting** the parsed specifications, including sorting maps and merging duplicate content. +- **Adding information programmatically** to the specifications. +- **Marshalling** the modified specifications back into their original format. +- **Utilizing** the parsed specification for code generation. + +## Features + +- **Comprehensive parsing** of [AsyncAPI 3.1.0](https://www.asyncapi.com/docs/reference/specification/v3.1.0) specifications, in JSON as well as in YAML. +- **Strict validation** against the rules of the specification: required fields, enumerations, key and address patterns, absolute URLs, [runtime expressions](https://www.asyncapi.com/docs/reference/specification/v3.1.0#runtimeExpression), and the rules that span several objects, e.g. that the messages of an [operation](https://www.asyncapi.com/docs/reference/specification/v3.1.0#operationObject) "MUST contain a subset of the messages defined in the channel referenced in this operation". Every error names the exact location of the problem, e.g. `channels["userSignedup"].messages["userSignedUp"].contentType: mime: expected slash after first token`. +- **Marshalling** back to JSON and to YAML, to a file, to a writer or to a byte slice. +- **Reference resolution** of every referencable object, including references that point to other references, e.g. an operation that refers to a message of a channel which in turn refers to a message of the [components object](https://www.asyncapi.com/docs/reference/specification/v3.1.0#componentsObject). +- **Order preservation**: maps keep the order in which their keys were defined, so writing a specification back doesn't reshuffle it. +- **Multi format schemas**: schemas in other formats (Avro, Protobuf, RAML, ...) are kept as they are, [AsyncAPI schemas](https://www.asyncapi.com/docs/reference/specification/v3.1.0#schemaObject) are parsed, including boolean schemas and multiple types. +- **Bindings** of all [20 protocols](https://www.asyncapi.com/docs/reference/specification/v3.1.0#serverBindingsObject) are preserved as they were given, so nothing is lost when a specification is written back. +- **Documented in line with the specification**: every object, field and rule quotes the [official documentation](https://www.asyncapi.com/docs/reference/specification/v3.1.0) and links to the section it comes from. + +## Usage + +```go +package main + +import ( + "fmt" + + "github.com/MarkRosemaker/asyncapi" +) + +func main() { + doc, err := asyncapi.LoadFromFile("path/to/asyncapi.json") // or asyncapi.yaml + if err != nil { + fmt.Println("Error parsing spec:", err) + return + } + + if err := doc.Validate(); err != nil { + fmt.Println("Error validating spec:", err) + return + } + + // sort the keys of the servers, channels, operations and components in alphabetical order + doc.SortMaps() + + // write an improved version of your spec, as JSON or as YAML + if err := doc.WriteToFile("path/to/asyncapi.json"); err != nil { + fmt.Println("Error writing to file:", err) + return + } +} +``` + +## Additional Information + +- [**Go Reference**](https://pkg.go.dev/github.com/MarkRosemaker/asyncapi): The Go reference documentation for the asyncapi package. +- [**Go Report Card**](https://goreportcard.com/report/github.com/MarkRosemaker/asyncapi): Check the code quality report. + +## Contributing + +If you have any contributions to make, please submit a pull request or open an issue on the [GitHub repository](https://github.com/MarkRosemaker/asyncapi). + +## License + +This project is licensed under the [Apache 2.0 License](./LICENSE). diff --git a/asyncapi/any_schema.go b/asyncapi/any_schema.go new file mode 100644 index 0000000..9f1b68d --- /dev/null +++ b/asyncapi/any_schema.go @@ -0,0 +1,193 @@ +package asyncapi + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "fmt" + + "github.com/MarkRosemaker/errpath" +) + +// AnySchema is a schema definition, wherever the specification allows +// a [Schema] object as well as a [MultiFormatSchema] object. +// +// A Schema Object is equivalent to a Multi Format Schema Object with the default schema format, +// which is why both are represented by this one type: if no schema format is given, the schema +// itself is the AsyncAPI schema, otherwise the schema is wrapped in a Multi Format Schema Object. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#multiFormatSchemaObject +type AnySchema struct { + // The name of the schema format that is used to define the information. + // If it is missing, it defaults to an AsyncAPI schema format, i.e. the schema is a [Schema] object. + SchemaFormat SchemaFormat `json:"schemaFormat,omitempty" yaml:"schemaFormat,omitempty"` + // The schema as an AsyncAPI Schema Object. + // It is set if the schema format is missing or denotes an AsyncAPI schema format. + Schema *Schema `json:"-" yaml:"-"` + // The schema in a format other than the AsyncAPI Schema Object, e.g. Avro or Protobuf. + // Non-JSON-based schemas (e.g. Protobuf or XSD) are inlined as a string. + Raw jsontext.Value `json:"-" yaml:"-"` + // A Multi Format Schema Object MAY be extended with Specification Extensions. + Extensions Extensions `json:",inline" yaml:",inline"` +} + +// MultiFormatSchema represents a schema definition in a specific schema format. +// It is the wire format of an [AnySchema] that has a schema format. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#multiFormatSchemaObject +type MultiFormatSchema struct { + // REQUIRED. A string containing the name of the schema format that is used to define the information. + SchemaFormat SchemaFormat `json:"schemaFormat" yaml:"schemaFormat"` + // REQUIRED. Definition of the message payload. + // It can be of any type but defaults to a Schema Object. + Schema jsontext.Value `json:"schema" yaml:"schema"` + // This object MAY be extended with Specification Extensions. + Extensions Extensions `json:",inline" yaml:",inline"` +} + +// Validate checks the schema for correctness. +func (s *AnySchema) Validate() error { + if s.SchemaFormat == "" { + if s.Schema == nil { + return &errpath.ErrRequired{} + } + + return s.Schema.Validate() + } + + if s.Schema == nil && len(s.Raw) == 0 { + return &errpath.ErrField{Field: "schema", Err: &errpath.ErrRequired{}} + } + + if s.Schema != nil { + if err := s.Schema.Validate(); err != nil { + return &errpath.ErrField{Field: "schema", Err: err} + } + } + + return validateExtensions(s.Extensions) +} + +// SortMaps sorts the maps of the underlying AsyncAPI schema by key. +func (s *AnySchema) SortMaps() { + if s == nil { + return + } + + s.Schema.SortMaps() +} + +var _ json.UnmarshalerFrom = (*AnySchema)(nil) + +// UnmarshalJSONFrom unmarshals either a Schema Object or a Multi Format Schema Object. +func (s *AnySchema) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + val, err := dec.ReadValue() + if err != nil { + return err + } + + // a schema may be a boolean, in which case it is an AsyncAPI schema + switch val.Kind() { + case 't', 'f': + return s.unmarshalSchema(val, dec.Options()) + case '{': // check below whether a schema format is given + default: + return fmt.Errorf("schema must be an object or a boolean, got %s", val.Kind()) + } + + // we don't know whether this is a multi format schema, so we peek at the members + members := map[string]jsontext.Value{} + if err := json.Unmarshal(val, &members); err != nil { + return err + } + + if _, ok := members["schemaFormat"]; !ok { + // without a schema format, the object is an AsyncAPI schema + return s.unmarshalSchema(val, dec.Options()) + } + + mf := &MultiFormatSchema{} + if err := json.Unmarshal(val, mf, dec.Options()); err != nil { + return err + } + + s.SchemaFormat = mf.SchemaFormat + s.Extensions = mf.Extensions + + // only an AsyncAPI schema can be parsed into a Schema object + if !mf.SchemaFormat.IsAsyncAPI() { + s.Raw = mf.Schema + return nil + } + + return s.unmarshalSchema(mf.Schema, dec.Options()) +} + +// unmarshalSchema unmarshals the value as an AsyncAPI schema. +func (s *AnySchema) unmarshalSchema(val jsontext.Value, opts json.Options) error { + if len(val) == 0 { + return &errpath.ErrField{Field: "schema", Err: &errpath.ErrRequired{}} + } + + schema := &Schema{} + if err := json.Unmarshal(val, schema, opts); err != nil { + return err + } + + s.Schema = schema + + return nil +} + +var _ json.MarshalerTo = (*AnySchema)(nil) + +// MarshalJSONTo marshals either a Schema Object or a Multi Format Schema Object. +func (s *AnySchema) MarshalJSONTo(enc *jsontext.Encoder) error { + // the schema of a multi format schema object, either the AsyncAPI schema or the raw schema + schema := s.Raw + + if s.Schema != nil { + var err error + if schema, err = json.Marshal(s.Schema, enc.Options()); err != nil { + return err + } + + // without a schema format, the schema itself is the object + if s.SchemaFormat == "" { + return enc.WriteValue(schema) + } + } + + return json.MarshalEncode(enc, &MultiFormatSchema{ + SchemaFormat: s.SchemaFormat, + Schema: schema, + Extensions: s.Extensions, + }) +} + +func (l *loader) collectAnySchemaRefList(ss AnySchemaRefList, ref ref) { + for i, s := range ss { + l.collectAnySchemaRef(s, append(ref, itoa(i))) + } +} + +func (l *loader) collectAnySchemaRef(s *AnySchemaRef, ref ref) { + if !collectRef(l, s, l.schemas, ref) { + return + } + + if s.Value.Schema != nil { + l.collectSchema(s.Value.Schema, ref) + } +} + +func (l *loader) resolveAnySchemaRef(s *AnySchemaRef) error { + return resolveRef(s, l.schemas, func(s *AnySchema) error { + if s.Schema == nil { + return nil + } + + return l.resolveSchema(s.Schema) + }) +} diff --git a/asyncapi/any_schema_test.go b/asyncapi/any_schema_test.go new file mode 100644 index 0000000..836714e --- /dev/null +++ b/asyncapi/any_schema_test.go @@ -0,0 +1,187 @@ +package asyncapi_test + +import ( + "encoding/json/v2" + "strings" + "testing" + + "github.com/MarkRosemaker/asyncapi" +) + +// loadSchema loads a single schema by loading a document that holds it in its components. +func loadSchema(t *testing.T, schema string) *asyncapi.AnySchema { + t.Helper() + + doc, err := asyncapi.LoadFromDataJSON([]byte( + `{"asyncapi":"3.1.0","info":{"title":"foo","version":"1.0.0"},` + + `"components":{"schemas":{"test":` + schema + `}}}`, + )) + if err != nil { + t.Fatal(err) + } + + if err := doc.Validate(); err != nil { + t.Fatal(err) + } + + return doc.Components.Schemas["test"].Value +} + +func TestAnySchema_Schema(t *testing.T) { + t.Parallel() + + s := loadSchema(t, `{"type":"string","format":"date-time"}`) + + if s.SchemaFormat != "" { + t.Fatalf("got: %v, want no schema format", s.SchemaFormat) + } + + if s.Schema == nil { + t.Fatal("expected an AsyncAPI schema") + } + + if got, want := s.Schema.Type.String(), "string"; got != want { + t.Fatalf("got: %v, want: %v", got, want) + } + + if got, want := s.Schema.Format, asyncapi.FormatDateTime; got != want { + t.Fatalf("got: %v, want: %v", got, want) + } + + if !s.Schema.Format.IsKnown() { + t.Fatalf("%q should be a known format", s.Schema.Format) + } +} + +func TestAnySchema_MultipleTypes(t *testing.T) { + t.Parallel() + + s := loadSchema(t, `{"type":["string","null"]}`) + + if got, want := s.Schema.Type.String(), "string, null"; got != want { + t.Fatalf("got: %v, want: %v", got, want) + } + + if !s.Schema.Type.Contains(asyncapi.TypeNull) { + t.Fatal("expected the schema to allow null") + } +} + +func TestAnySchema_Boolean(t *testing.T) { + t.Parallel() + + s := loadSchema(t, `false`) + + if s.Schema == nil || s.Schema.Boolean == nil { + t.Fatal("expected a boolean schema") + } + + if *s.Schema.Boolean { + t.Fatal("expected the schema to be false") + } + + // a boolean schema is written back as a boolean + got, err := json.Marshal(s) + if err != nil { + t.Fatal(err) + } + + if want := "false"; string(got) != want { + t.Fatalf("got: %s, want: %s", got, want) + } +} + +func TestAnySchema_MultiFormat(t *testing.T) { + t.Parallel() + + t.Run("avro", func(t *testing.T) { + t.Parallel() + + s := loadSchema(t, `{"schemaFormat":"application/vnd.apache.avro;version=1.9.0",`+ + `"schema":{"type":"record","name":"User"}}`) + + if got, want := s.SchemaFormat, asyncapi.SchemaFormatAvro; got != want { + t.Fatalf("got: %v, want: %v", got, want) + } + + if !s.SchemaFormat.IsKnown() { + t.Fatalf("%q should be a known schema format", s.SchemaFormat) + } + + if s.SchemaFormat.IsAsyncAPI() { + t.Fatalf("%q is not an AsyncAPI schema format", s.SchemaFormat) + } + + // a schema in another format is not parsed but kept as it is + if s.Schema != nil { + t.Fatal("expected no AsyncAPI schema") + } + + if got, want := string(s.Raw), `{"type":"record","name":"User"}`; got != want { + t.Fatalf("got: %s, want: %s", got, want) + } + }) + + t.Run("asyncapi", func(t *testing.T) { + t.Parallel() + + s := loadSchema(t, `{"schemaFormat":"application/vnd.aai.asyncapi+json;version=3.1.0",`+ + `"schema":{"type":"string"}}`) + + if !s.SchemaFormat.IsAsyncAPI() { + t.Fatalf("%q is an AsyncAPI schema format", s.SchemaFormat) + } + + // an AsyncAPI schema is parsed even if the format is given explicitly + if s.Schema == nil { + t.Fatal("expected an AsyncAPI schema") + } + + if got, want := s.Schema.Type.String(), "string"; got != want { + t.Fatalf("got: %v, want: %v", got, want) + } + }) +} + +func TestAnySchema_Errors(t *testing.T) { + t.Parallel() + + t.Run("not an object", func(t *testing.T) { + t.Parallel() + + _, err := asyncapi.LoadFromDataJSON([]byte( + `{"asyncapi":"3.1.0","info":{"title":"foo","version":"1.0.0"},` + + `"components":{"schemas":{"test":"string"}}}`, + )) + if err == nil { + t.Fatal("expected error") + } + + want := "schema must be an object or a boolean, got string" + if !strings.Contains(err.Error(), want) { + t.Fatalf("got: %v, want it to contain: %v", err, want) + } + }) + + t.Run("multi format without a schema", func(t *testing.T) { + t.Parallel() + + doc, err := asyncapi.LoadFromDataJSON([]byte( + `{"asyncapi":"3.1.0","info":{"title":"foo","version":"1.0.0"},` + + `"components":{"schemas":{"test":` + + `{"schemaFormat":"application/vnd.apache.avro;version=1.9.0"}}}}`, + )) + if err != nil { + t.Fatal(err) + } + + err = doc.Validate() + if err == nil { + t.Fatal("expected error") + } + + if want := `components.schemas["test"].schema is required`; err.Error() != want { + t.Fatalf("got: %v, want: %v", err, want) + } + }) +} diff --git a/asyncapi/bindings.go b/asyncapi/bindings.go new file mode 100644 index 0000000..50f5d50 --- /dev/null +++ b/asyncapi/bindings.go @@ -0,0 +1,126 @@ +package asyncapi + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "iter" + "slices" + "strings" + + "github.com/MarkRosemaker/errpath" + "github.com/MarkRosemaker/ordmap" +) + +// Bindings is a map describing protocol-specific definitions +// for a server, a channel, an operation or a message. +// +// The keys describe the name of the protocol, the values describe the protocol-specific definitions. +// The definitions themselves are described by the [bindings specification] +// and are therefore kept as raw JSON values. +// ([Specification]) +// +// [bindings specification]: https://github.com/asyncapi/bindings +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#serverBindingsObject +type Bindings map[Protocol]*Binding + +// Validate checks that the protocols are known and that the definitions are valid JSON. +func (bs Bindings) Validate() error { + for protocol, b := range bs.ByIndex() { + // the bindings object MAY be extended with specification extensions + if strings.HasPrefix(string(protocol), "x-") { + continue + } + + if !slices.Contains(allBindingProtocols, protocol) { + return &errpath.ErrKey{ + Key: string(protocol), + Err: &errpath.ErrInvalid[Protocol]{Value: protocol, Enum: allBindingProtocols}, + } + } + + if err := b.Validate(); err != nil { + return &errpath.ErrKey{Key: string(protocol), Err: err} + } + } + + return nil +} + +// ByIndex returns a sequence of key-value pairs ordered by index. +func (bs Bindings) ByIndex() iter.Seq2[Protocol, *Binding] { + return ordmap.ByIndex(bs, getIndexBinding) +} + +// Sort sorts the map by key and sets the indices accordingly. +func (bs Bindings) Sort() { + ordmap.Sort(bs, setIndexBinding) +} + +// Set sets a value in the map, adding it at the end of the order. +func (bs *Bindings) Set(key Protocol, b *Binding) { + ordmap.Set(bs, key, b, getIndexBinding, setIndexBinding) +} + +var _ json.MarshalerTo = (*Bindings)(nil) + +// MarshalJSONTo marshals the key-value pairs in order. +func (bs *Bindings) MarshalJSONTo(enc *jsontext.Encoder) error { + return ordmap.MarshalJSONTo(bs, enc) +} + +var _ json.UnmarshalerFrom = (*Bindings)(nil) + +// UnmarshalJSONFrom unmarshals the key-value pairs in order and sets the indices. +func (bs *Bindings) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + return ordmap.UnmarshalJSONFrom(bs, dec, setIndexBinding) +} + +// Binding holds the protocol-specific definitions of a single protocol. +type Binding struct { + // The protocol-specific definition as defined by the bindings specification. + Value jsontext.Value + + // an index to the original location of this object + idx int +} + +// Validate checks that the binding holds a value. +func (b *Binding) Validate() error { + if len(b.Value) == 0 { + return &errpath.ErrRequired{} + } + + return nil +} + +var _ json.UnmarshalerFrom = (*Binding)(nil) + +// UnmarshalJSONFrom unmarshals the value of the binding. +func (b *Binding) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + val, err := dec.ReadValue() + if err != nil { + return err + } + + b.Value = slices.Clone(val) + + return nil +} + +var _ json.MarshalerTo = (*Binding)(nil) + +// MarshalJSONTo marshals the value of the binding. +func (b *Binding) MarshalJSONTo(enc *jsontext.Encoder) error { + return enc.WriteValue(b.Value) +} + +func getIndexBinding(b *Binding) int { return b.idx } +func setIndexBinding(b *Binding, i int) *Binding { b.idx = i; return b } + +func (l *loader) collectBindingsRef(b *BindingsRef, ref ref) { + collectRef(l, b, l.bindings, ref) +} + +func (l *loader) resolveBindingsRef(b *BindingsRef) error { + return resolveRef(b, l.bindings, nil) +} diff --git a/asyncapi/bindings_by_name.go b/asyncapi/bindings_by_name.go new file mode 100644 index 0000000..52cd503 --- /dev/null +++ b/asyncapi/bindings_by_name.go @@ -0,0 +1,72 @@ +package asyncapi + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "iter" + + "github.com/MarkRosemaker/errpath" + "github.com/MarkRosemaker/ordmap" +) + +// BindingsByName is a map of bindings objects. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#componentsServerBindings +type BindingsByName map[string]*BindingsRef + +// Validate validates each bindings object. +func (bs BindingsByName) Validate() error { + for name, b := range bs.ByIndex() { + if err := b.Validate(); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} + +// ByIndex returns a sequence of key-value pairs ordered by index. +func (bs BindingsByName) ByIndex() iter.Seq2[string, *BindingsRef] { + return ordmap.ByIndex(bs, getIndexRef[Bindings, *Bindings]) +} + +// Sort sorts the map by key and sets the indices accordingly. +func (bs BindingsByName) Sort() { + ordmap.Sort(bs, setIndexRef[Bindings, *Bindings]) +} + +// Set sets a value in the map, adding it at the end of the order. +func (bs *BindingsByName) Set(key string, b *BindingsRef) { + ordmap.Set(bs, key, b, getIndexRef[Bindings, *Bindings], setIndexRef[Bindings, *Bindings]) +} + +var _ json.MarshalerTo = (*BindingsByName)(nil) + +// MarshalJSONTo marshals the key-value pairs in order. +func (bs *BindingsByName) MarshalJSONTo(enc *jsontext.Encoder) error { + return ordmap.MarshalJSONTo(bs, enc) +} + +var _ json.UnmarshalerFrom = (*BindingsByName)(nil) + +// UnmarshalJSONFrom unmarshals the key-value pairs in order and sets the indices. +func (bs *BindingsByName) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + return ordmap.UnmarshalJSONFrom(bs, dec, setIndexRef[Bindings, *Bindings]) +} + +func (l *loader) collectBindingsByName(bs BindingsByName, ref ref) { + for name, b := range bs.ByIndex() { + l.collectBindingsRef(b, append(ref, name)) + } +} + +func (l *loader) resolveBindingsByName(bs BindingsByName) error { + for name, b := range bs.ByIndex() { + if err := l.resolveBindingsRef(b); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} diff --git a/asyncapi/bindings_test.go b/asyncapi/bindings_test.go new file mode 100644 index 0000000..3e2a8e8 --- /dev/null +++ b/asyncapi/bindings_test.go @@ -0,0 +1,76 @@ +package asyncapi_test + +import ( + "testing" + + "github.com/MarkRosemaker/asyncapi" +) + +func TestBindings(t *testing.T) { + t.Parallel() + + doc, err := asyncapi.LoadFromFile("examples/v3.1/rpc-client.yaml") + if err != nil { + t.Fatal(err) + } + + if err := doc.Validate(); err != nil { + t.Fatal(err) + } + + bindings := doc.Channels["queue"].Value.Bindings + if bindings == nil { + t.Fatal("expected bindings") + } + + amqp := (*bindings.Value)[asyncapi.ProtocolAMQP] + if amqp == nil { + t.Fatal("expected an AMQP binding") + } + + // the protocol-specific definitions are kept as they are + want := `{"is":"queue","queue":{"exclusive":true}}` + if got := string(amqp.Value); got != want { + t.Fatalf("got: %s, want: %s", got, want) + } +} + +func TestBindings_Validate(t *testing.T) { + t.Parallel() + + t.Run("unknown protocol", func(t *testing.T) { + t.Parallel() + + doc := minimalDocument() + doc.Channels["userSignedup"].Value.Bindings = &asyncapi.BindingsRef{ + Value: &asyncapi.Bindings{"carrierPigeon": {Value: []byte(`{}`)}}, + } + + err := doc.Validate() + if err == nil { + t.Fatal("expected error") + } + + want := `channels["userSignedup"].bindings["carrierPigeon"] ("carrierPigeon") is invalid, ` + + `must be one of: "http", "ws", "kafka", "anypointmq", "amqp", "amqp1", "mqtt", "mqtt5", ` + + `"nats", "jms", "sns", "solace", "sqs", "stomp", "redis", "mercure", "ibmmq", ` + + `"googlepubsub", "pulsar", "ros2"` + if err.Error() != want { + t.Fatalf("got: %v, want: %v", err, want) + } + }) + + t.Run("extension", func(t *testing.T) { + t.Parallel() + + // the bindings object may be extended with specification extensions + doc := minimalDocument() + doc.Channels["userSignedup"].Value.Bindings = &asyncapi.BindingsRef{ + Value: &asyncapi.Bindings{"x-custom": {Value: []byte(`{}`)}}, + } + + if err := doc.Validate(); err != nil { + t.Fatal(err) + } + }) +} diff --git a/asyncapi/channel.go b/asyncapi/channel.go new file mode 100644 index 0000000..6fc79e9 --- /dev/null +++ b/asyncapi/channel.go @@ -0,0 +1,196 @@ +package asyncapi + +import ( + "regexp" + "strings" + + "github.com/MarkRosemaker/errpath" +) + +// reChannelAddressExpression matches a Channel Address Expression, i.e. a name enclosed in curly braces. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#channelAddressExpressions +var reChannelAddressExpression = regexp.MustCompile(`\{([^{}]+)\}`) + +// reParameterKey is the regular expression the keys of the parameters of a channel must match. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#parametersObject +var reParameterKey = regexp.MustCompile(`^[A-Za-z0-9_\-]+$`) + +// Channel describes a shared communication channel. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#channelObject +type Channel struct { + // An optional string representation of this channel's address. + // The address is typically the "topic name", "routing key", "event type", or "path". + // When null or absent, it MUST be interpreted as unknown. + // This is useful when the address is generated dynamically at runtime or can't be known upfront. + // It MAY contain Channel Address Expressions. + // Query parameters and fragments SHALL NOT be used, instead use bindings to define them. + Address string `json:"address,omitempty" yaml:"address,omitempty"` + // A map of the messages that will be sent to this channel by any application at any time. + // Every message sent to this channel MUST be valid against one, and only one, of the message objects defined in this map. + Messages Messages `json:"messages,omitempty" yaml:"messages,omitempty"` + // A human-friendly title for the channel. + Title string `json:"title,omitempty" yaml:"title,omitempty"` + // A short summary of the channel. + Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` + // An optional description of this channel. CommonMark syntax can be used for rich text representation. + Description string `json:"description,omitempty" yaml:"description,omitempty"` + // An array of $ref pointers to the definition of the servers in which this channel is available. + // If the channel is located in the root Channels Object, it MUST point to a subset of server definitions located in the root Servers Object. + // If `servers` is absent or empty, this channel MUST be available on all the servers defined in the Servers Object. + Servers ServerRefList `json:"servers,omitempty" yaml:"servers,omitempty"` + // A map of the parameters included in the channel address. + // It MUST be present only when the address contains Channel Address Expressions. + Parameters Parameters `json:"parameters,omitempty" yaml:"parameters,omitempty"` + // A list of tags for logical grouping of channels. + Tags Tags `json:"tags,omitempty" yaml:"tags,omitempty"` + // Additional external documentation for this channel. + ExternalDocs *ExternalDocsRef `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + // A map where the keys describe the name of the protocol and the values describe protocol-specific definitions for the channel. + Bindings *BindingsRef `json:"bindings,omitempty" yaml:"bindings,omitempty"` + // This object MAY be extended with Specification Extensions. + Extensions Extensions `json:",inline" yaml:",inline"` +} + +// Validate checks the channel for correctness. +func (c *Channel) Validate() error { + c.Description = strings.TrimSpace(c.Description) + + if err := c.Messages.Validate(); err != nil { + return &errpath.ErrField{Field: "messages", Err: err} + } + + if err := c.Servers.Validate(); err != nil { + return &errpath.ErrField{Field: "servers", Err: err} + } + + // "Query parameters and fragments SHALL NOT be used, + // instead use bindings to define them." + if i := strings.IndexAny(c.Address, "?#"); i >= 0 { + return &errpath.ErrField{Field: "address", Err: &errpath.ErrInvalid[string]{ + Value: c.Address, + Message: "query parameters and fragments must not be used, use bindings instead", + }} + } + + // "The key represents the name of the parameter. + // It MUST match the parameter name used in the parent channel address." + for name := range c.Parameters { + if !reParameterKey.MatchString(name) { + return &errpath.ErrField{Field: "parameters", Err: &errpath.ErrKey{ + Key: name, + Err: &errpath.ErrInvalid[string]{ + Value: name, + Message: `must match the regular expression "` + reParameterKey.String() + `"`, + }, + }} + } + } + + if err := c.Parameters.Validate(); err != nil { + return &errpath.ErrField{Field: "parameters", Err: err} + } + + // "This map MUST contain all the parameters used in the parent channel address." + for _, name := range c.AddressExpressions() { + if _, ok := c.Parameters[name]; !ok { + return &errpath.ErrField{Field: "parameters", Err: &errpath.ErrKey{ + Key: name, Err: &errpath.ErrRequired{}, + }} + } + } + + if err := c.Tags.Validate(); err != nil { + return &errpath.ErrField{Field: "tags", Err: err} + } + + if c.ExternalDocs != nil { + if err := c.ExternalDocs.Validate(); err != nil { + return &errpath.ErrField{Field: "externalDocs", Err: err} + } + } + + if c.Bindings != nil { + if err := c.Bindings.Validate(); err != nil { + return &errpath.ErrField{Field: "bindings", Err: err} + } + } + + return validateExtensions(c.Extensions) +} + +// AddressExpressions returns the names of the Channel Address Expressions used in the address, +// i.e. the names of the parameters that are enclosed in curly braces. +func (c *Channel) AddressExpressions() []string { + matches := reChannelAddressExpression.FindAllStringSubmatch(c.Address, -1) + + names := make([]string, 0, len(matches)) + for _, m := range matches { + names = append(names, m[1]) + } + + return names +} + +func (l *loader) collectChannelRef(c *ChannelRef, ref ref) { + if !collectRef(l, c, l.channels, ref) { + return + } + + l.collectChannel(c.Value, ref) +} + +func (l *loader) collectChannel(c *Channel, ref ref) { + l.collectMessages(c.Messages, append(ref, "messages")) + l.collectParameters(c.Parameters, append(ref, "parameters")) + l.collectTags(c.Tags, append(ref, "tags")) + + if c.ExternalDocs != nil { + l.collectExternalDocsRef(c.ExternalDocs, append(ref, "externalDocs")) + } + + if c.Bindings != nil { + l.collectBindingsRef(c.Bindings, append(ref, "bindings")) + } +} + +func (l *loader) resolveChannelRef(c *ChannelRef) error { + return resolveRef(c, l.channels, l.resolveChannel) +} + +func (l *loader) resolveChannel(c *Channel) error { + if err := l.resolveMessages(c.Messages); err != nil { + return &errpath.ErrField{Field: "messages", Err: err} + } + + if err := l.resolveServerRefList(c.Servers); err != nil { + return &errpath.ErrField{Field: "servers", Err: err} + } + + if err := l.resolveParameters(c.Parameters); err != nil { + return &errpath.ErrField{Field: "parameters", Err: err} + } + + if err := l.resolveTags(c.Tags); err != nil { + return &errpath.ErrField{Field: "tags", Err: err} + } + + if c.ExternalDocs != nil { + if err := l.resolveExternalDocsRef(c.ExternalDocs); err != nil { + return &errpath.ErrField{Field: "externalDocs", Err: err} + } + } + + if c.Bindings != nil { + if err := l.resolveBindingsRef(c.Bindings); err != nil { + return &errpath.ErrField{Field: "bindings", Err: err} + } + } + + return nil +} diff --git a/asyncapi/channel_test.go b/asyncapi/channel_test.go new file mode 100644 index 0000000..f5aa513 --- /dev/null +++ b/asyncapi/channel_test.go @@ -0,0 +1,150 @@ +package asyncapi_test + +import ( + "slices" + "testing" + + "github.com/MarkRosemaker/asyncapi" +) + +func TestChannel_AddressExpressions(t *testing.T) { + t.Parallel() + + for address, want := range map[string][]string{ + "user/signedup": {}, + "user/{userId}/signedup": {"userId"}, + "{env}/user/{userId}/{action}": {"env", "userId", "action"}, + "smartylighting.{streetlightId}.on": {"streetlightId"}, + } { + t.Run(address, func(t *testing.T) { + t.Parallel() + + c := &asyncapi.Channel{Address: address} + if got := c.AddressExpressions(); !slices.Equal(got, want) { + t.Fatalf("got: %v, want: %v", got, want) + } + }) + } +} + +func TestChannel_Validate_MissingParameter(t *testing.T) { + t.Parallel() + + doc := minimalDocument() + doc.Channels["userSignedup"].Value.Address = "user/{userId}/signedup" + + err := doc.Validate() + if err == nil { + t.Fatal("expected error") + } + + want := `channels["userSignedup"].parameters["userId"] is required` + if err.Error() != want { + t.Fatalf("got: %v, want: %v", err, want) + } + + // the parameter is defined, so the channel is valid + doc.Channels["userSignedup"].Value.Parameters = asyncapi.Parameters{ + "userId": {Value: &asyncapi.Parameter{Description: "Id of the user."}}, + } + + if err := doc.Validate(); err != nil { + t.Fatal(err) + } +} + +func TestParameter_Validate(t *testing.T) { + t.Parallel() + + for name, tc := range map[string]struct { + param *asyncapi.Parameter + want string + }{ + "empty enum": { + &asyncapi.Parameter{Enum: []string{}}, + "enum array must not be empty", + }, + "default not in enum": { + &asyncapi.Parameter{Enum: []string{"a", "b"}, Default: "c"}, + `default value "c" must exist in the enum's values`, + }, + "invalid location": { + &asyncapi.Parameter{Location: "$request.header#/foo"}, + `location ("$request.header#/foo") is invalid: ` + + `must be a runtime expression, e.g. "$message.header#/correlationId"`, + }, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + + err := tc.param.Validate() + if err == nil { + t.Fatal("expected error") + } + + if err.Error() != tc.want { + t.Fatalf("got: %v, want: %v", err, tc.want) + } + }) + } + + t.Run("valid", func(t *testing.T) { + t.Parallel() + + p := &asyncapi.Parameter{ + Enum: []string{"a", "b"}, + Default: "a", + Examples: []string{"a"}, + Location: "$message.payload#/user/id", + } + + if err := p.Validate(); err != nil { + t.Fatal(err) + } + }) +} + +func TestOperation_Validate_Errors(t *testing.T) { + t.Parallel() + + channelRef := func() *asyncapi.ChannelRef { + return &asyncapi.ChannelRef{ + Ref: &asyncapi.Reference{Identifier: "#/channels/userSignedup"}, + Value: &asyncapi.Channel{Address: "user/signedup"}, + } + } + + for name, tc := range map[string]struct { + op *asyncapi.Operation + want string + }{ + "no action": { + &asyncapi.Operation{Channel: channelRef()}, + `operations["test"].action is required`, + }, + "unknown action": { + &asyncapi.Operation{Action: "publish", Channel: channelRef()}, + `operations["test"].action ("publish") is invalid, must be one of: "send", "receive"`, + }, + "no channel": { + &asyncapi.Operation{Action: asyncapi.OperationActionSend}, + `operations["test"].channel is required`, + }, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + + doc := minimalDocument() + doc.Operations = asyncapi.Operations{"test": {Value: tc.op}} + + err := doc.Validate() + if err == nil { + t.Fatal("expected error") + } + + if err.Error() != tc.want { + t.Fatalf("got: %v, want: %v", err, tc.want) + } + }) + } +} diff --git a/asyncapi/channels.go b/asyncapi/channels.go new file mode 100644 index 0000000..e0bbca4 --- /dev/null +++ b/asyncapi/channels.go @@ -0,0 +1,75 @@ +package asyncapi + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "iter" + + "github.com/MarkRosemaker/errpath" + "github.com/MarkRosemaker/ordmap" +) + +// Channels is an object containing all the Channel Object definitions the application MUST use during runtime. +// +// The key of each entry is an identifier for the described channel. The channel ID is case-sensitive. +// Tools and libraries MAY use it to uniquely identify a channel, therefore, it is RECOMMENDED to follow common programming naming conventions. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#channelsObject +type Channels map[string]*ChannelRef + +// Validate validates each channel. +func (cs Channels) Validate() error { + for name, c := range cs.ByIndex() { + if err := c.Validate(); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} + +// ByIndex returns a sequence of key-value pairs ordered by index. +func (cs Channels) ByIndex() iter.Seq2[string, *ChannelRef] { + return ordmap.ByIndex(cs, getIndexRef[Channel, *Channel]) +} + +// Sort sorts the map by key and sets the indices accordingly. +func (cs Channels) Sort() { + ordmap.Sort(cs, setIndexRef[Channel, *Channel]) +} + +// Set sets a value in the map, adding it at the end of the order. +func (cs *Channels) Set(key string, c *ChannelRef) { + ordmap.Set(cs, key, c, getIndexRef[Channel, *Channel], setIndexRef[Channel, *Channel]) +} + +var _ json.MarshalerTo = (*Channels)(nil) + +// MarshalJSONTo marshals the key-value pairs in order. +func (cs *Channels) MarshalJSONTo(enc *jsontext.Encoder) error { + return ordmap.MarshalJSONTo(cs, enc) +} + +var _ json.UnmarshalerFrom = (*Channels)(nil) + +// UnmarshalJSONFrom unmarshals the key-value pairs in order and sets the indices. +func (cs *Channels) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + return ordmap.UnmarshalJSONFrom(cs, dec, setIndexRef[Channel, *Channel]) +} + +func (l *loader) collectChannels(cs Channels, ref ref) { + for name, c := range cs.ByIndex() { + l.collectChannelRef(c, append(ref, name)) + } +} + +func (l *loader) resolveChannels(cs Channels) error { + for name, c := range cs.ByIndex() { + if err := l.resolveChannelRef(c); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} diff --git a/asyncapi/components.go b/asyncapi/components.go new file mode 100644 index 0000000..78a3620 --- /dev/null +++ b/asyncapi/components.go @@ -0,0 +1,231 @@ +package asyncapi + +import ( + "fmt" + "regexp" + + "github.com/MarkRosemaker/errpath" +) + +// reKey is the regular expression all keys of the fixed fields of the components object must match. +var reKey = regexp.MustCompile(`^[a-zA-Z0-9\.\-_]+$`) + +// Components holds a set of reusable objects for different aspects of the AsyncAPI specification. +// All objects defined within the components object will have no effect on the API unless they are +// explicitly referenced from properties outside the components object. +// ([Specification]) +// +// All the fixed fields are objects that MUST use keys that match the regular expression: +// +// ^[a-zA-Z0-9\.\-_]+$ +// +// Field name examples: +// +// User +// User_1 +// User_Name +// user-name +// my.org.User +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#componentsObject +type Components struct { + // An object to hold reusable Schema Objects. + Schemas Schemas `json:"schemas,omitempty" yaml:"schemas,omitempty"` + // An object to hold reusable Server Objects. + Servers Servers `json:"servers,omitempty" yaml:"servers,omitempty"` + // An object to hold reusable Channel Objects. + Channels Channels `json:"channels,omitempty" yaml:"channels,omitempty"` + // An object to hold reusable Operation Objects. + Operations Operations `json:"operations,omitempty" yaml:"operations,omitempty"` + // An object to hold reusable Message Objects. + Messages Messages `json:"messages,omitempty" yaml:"messages,omitempty"` + // An object to hold reusable Security Scheme Objects. + SecuritySchemes SecuritySchemes `json:"securitySchemes,omitempty" yaml:"securitySchemes,omitempty"` + // An object to hold reusable Server Variable Objects. + ServerVariables ServerVariables `json:"serverVariables,omitempty" yaml:"serverVariables,omitempty"` + // An object to hold reusable Parameter Objects. + Parameters Parameters `json:"parameters,omitempty" yaml:"parameters,omitempty"` + // An object to hold reusable Correlation ID Objects. + CorrelationIDs CorrelationIDs `json:"correlationIds,omitempty" yaml:"correlationIds,omitempty"` + // An object to hold reusable Operation Reply Objects. + Replies Replies `json:"replies,omitempty" yaml:"replies,omitempty"` + // An object to hold reusable Operation Reply Address Objects. + ReplyAddresses ReplyAddresses `json:"replyAddresses,omitempty" yaml:"replyAddresses,omitempty"` + // An object to hold reusable External Documentation Objects. + ExternalDocs ExternalDocsByName `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + // An object to hold reusable Tag Objects. + Tags TagsByName `json:"tags,omitempty" yaml:"tags,omitempty"` + // An object to hold reusable Operation Trait Objects. + OperationTraits OperationTraits `json:"operationTraits,omitempty" yaml:"operationTraits,omitempty"` + // An object to hold reusable Message Trait Objects. + MessageTraits MessageTraits `json:"messageTraits,omitempty" yaml:"messageTraits,omitempty"` + // An object to hold reusable Server Bindings Objects. + ServerBindings BindingsByName `json:"serverBindings,omitempty" yaml:"serverBindings,omitempty"` + // An object to hold reusable Channel Bindings Objects. + ChannelBindings BindingsByName `json:"channelBindings,omitempty" yaml:"channelBindings,omitempty"` + // An object to hold reusable Operation Bindings Objects. + OperationBindings BindingsByName `json:"operationBindings,omitempty" yaml:"operationBindings,omitempty"` + // An object to hold reusable Message Bindings Objects. + MessageBindings BindingsByName `json:"messageBindings,omitempty" yaml:"messageBindings,omitempty"` + // This object MAY be extended with Specification Extensions. + Extensions Extensions `json:",inline" yaml:",inline"` +} + +// validateKey checks that a key of a fixed field of the components object is well-formed. +func validateKey(key string) error { + if reKey.MatchString(key) { + return nil + } + + return &errpath.ErrKey{Key: key, Err: &errpath.ErrInvalid[string]{ + Value: key, + Message: fmt.Sprintf(`must match the regular expression %q`, reKey), + }} +} + +// component is one of the fixed fields of the components object. +type component struct { + field string + keys func(func(string) bool) + valid func() error + sort func() +} + +// fields returns the fixed fields of the components object in the order they are defined. +func (c *Components) fields() []component { + return []component{ + {"schemas", keys(c.Schemas), c.Schemas.Validate, c.Schemas.Sort}, + {"servers", keys(c.Servers), c.Servers.Validate, c.Servers.Sort}, + {"channels", keys(c.Channels), c.Channels.Validate, c.Channels.Sort}, + {"operations", keys(c.Operations), c.Operations.Validate, c.Operations.Sort}, + {"messages", keys(c.Messages), c.Messages.Validate, c.Messages.Sort}, + {"securitySchemes", keys(c.SecuritySchemes), c.SecuritySchemes.Validate, c.SecuritySchemes.Sort}, + {"serverVariables", keys(c.ServerVariables), c.ServerVariables.Validate, c.ServerVariables.Sort}, + {"parameters", keys(c.Parameters), c.Parameters.Validate, c.Parameters.Sort}, + {"correlationIds", keys(c.CorrelationIDs), c.CorrelationIDs.Validate, c.CorrelationIDs.Sort}, + {"replies", keys(c.Replies), c.Replies.Validate, c.Replies.Sort}, + {"replyAddresses", keys(c.ReplyAddresses), c.ReplyAddresses.Validate, c.ReplyAddresses.Sort}, + {"externalDocs", keys(c.ExternalDocs), c.ExternalDocs.Validate, c.ExternalDocs.Sort}, + {"tags", keys(c.Tags), c.Tags.Validate, c.Tags.Sort}, + {"operationTraits", keys(c.OperationTraits), c.OperationTraits.Validate, c.OperationTraits.Sort}, + {"messageTraits", keys(c.MessageTraits), c.MessageTraits.Validate, c.MessageTraits.Sort}, + {"serverBindings", keys(c.ServerBindings), c.ServerBindings.Validate, c.ServerBindings.Sort}, + {"channelBindings", keys(c.ChannelBindings), c.ChannelBindings.Validate, c.ChannelBindings.Sort}, + {"operationBindings", keys(c.OperationBindings), c.OperationBindings.Validate, c.OperationBindings.Sort}, + {"messageBindings", keys(c.MessageBindings), c.MessageBindings.Validate, c.MessageBindings.Sort}, + } +} + +// resolvers returns the field name and the reference resolver of each fixed field, +// in the order the fields are defined. +func (l *loader) resolvers(c Components) []struct { + field string + resolve func() error +} { + return []struct { + field string + resolve func() error + }{ + {"schemas", func() error { return l.resolveSchemas(c.Schemas) }}, + {"servers", func() error { return l.resolveServers(c.Servers) }}, + {"channels", func() error { return l.resolveChannels(c.Channels) }}, + {"operations", func() error { return l.resolveOperations(c.Operations) }}, + {"messages", func() error { return l.resolveMessages(c.Messages) }}, + {"securitySchemes", func() error { return l.resolveSecuritySchemes(c.SecuritySchemes) }}, + {"serverVariables", func() error { return l.resolveServerVariables(c.ServerVariables) }}, + {"parameters", func() error { return l.resolveParameters(c.Parameters) }}, + {"correlationIds", func() error { return l.resolveCorrelationIDs(c.CorrelationIDs) }}, + {"replies", func() error { return l.resolveReplies(c.Replies) }}, + {"replyAddresses", func() error { return l.resolveReplyAddresses(c.ReplyAddresses) }}, + {"externalDocs", func() error { return l.resolveExternalDocsByName(c.ExternalDocs) }}, + {"tags", func() error { return l.resolveTagsByName(c.Tags) }}, + {"operationTraits", func() error { return l.resolveOperationTraits(c.OperationTraits) }}, + {"messageTraits", func() error { return l.resolveMessageTraits(c.MessageTraits) }}, + {"serverBindings", func() error { return l.resolveBindingsByName(c.ServerBindings) }}, + {"channelBindings", func() error { return l.resolveBindingsByName(c.ChannelBindings) }}, + {"operationBindings", func() error { return l.resolveBindingsByName(c.OperationBindings) }}, + {"messageBindings", func() error { return l.resolveBindingsByName(c.MessageBindings) }}, + } +} + +// keys returns a sequence of the keys of a map, in the order they were defined. +func keys[M ~map[string]V, V any](m M) func(func(string) bool) { + return func(yield func(string) bool) { + for k := range m { + if !yield(k) { + return + } + } + } +} + +// Validate checks the components object for correctness. +func (c *Components) Validate() error { + for _, f := range c.fields() { + for key := range f.keys { + if err := validateKey(key); err != nil { + return &errpath.ErrField{Field: f.field, Err: err} + } + } + + if err := f.valid(); err != nil { + return &errpath.ErrField{Field: f.field, Err: err} + } + } + + return validateExtensions(c.Extensions) +} + +// SortMaps sorts each field that is a map by key. +func (c *Components) SortMaps() { + for _, f := range c.fields() { + f.sort() + } + + for _, s := range c.Schemas { + s.Value.SortMaps() + } +} + +// isEmpty reports whether the components object holds no objects at all. +func (c Components) isEmpty() bool { + for _, f := range c.fields() { + for range f.keys { + return false + } + } + + return len(c.Extensions) == 0 +} + +func (l *loader) collectComponents(c Components, ref ref) { + l.collectSchemas(c.Schemas, append(ref, "schemas")) + l.collectServers(c.Servers, append(ref, "servers")) + l.collectChannels(c.Channels, append(ref, "channels")) + l.collectOperations(c.Operations, append(ref, "operations")) + l.collectMessages(c.Messages, append(ref, "messages")) + l.collectSecuritySchemes(c.SecuritySchemes, append(ref, "securitySchemes")) + l.collectServerVariables(c.ServerVariables, append(ref, "serverVariables")) + l.collectParameters(c.Parameters, append(ref, "parameters")) + l.collectCorrelationIDs(c.CorrelationIDs, append(ref, "correlationIds")) + l.collectReplies(c.Replies, append(ref, "replies")) + l.collectReplyAddresses(c.ReplyAddresses, append(ref, "replyAddresses")) + l.collectExternalDocsByName(c.ExternalDocs, append(ref, "externalDocs")) + l.collectTagsByName(c.Tags, append(ref, "tags")) + l.collectOperationTraits(c.OperationTraits, append(ref, "operationTraits")) + l.collectMessageTraits(c.MessageTraits, append(ref, "messageTraits")) + l.collectBindingsByName(c.ServerBindings, append(ref, "serverBindings")) + l.collectBindingsByName(c.ChannelBindings, append(ref, "channelBindings")) + l.collectBindingsByName(c.OperationBindings, append(ref, "operationBindings")) + l.collectBindingsByName(c.MessageBindings, append(ref, "messageBindings")) +} + +func (l *loader) resolveComponents(c Components) error { + for _, r := range l.resolvers(c) { + if err := r.resolve(); err != nil { + return &errpath.ErrField{Field: r.field, Err: err} + } + } + + return nil +} diff --git a/asyncapi/components_test.go b/asyncapi/components_test.go new file mode 100644 index 0000000..372880c --- /dev/null +++ b/asyncapi/components_test.go @@ -0,0 +1,336 @@ +package asyncapi_test + +import ( + "testing" + + "github.com/MarkRosemaker/asyncapi" +) + +// allComponents is a document that makes use of every field of the components object. +const allComponents = `{ + "asyncapi": "3.1.0", + "info": { + "title": "Account Service", + "version": "1.0.0", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0" + }, + "externalDocs": { + "$ref": "#/components/externalDocs/infoDocs" + } + }, + "servers": { + "production": { + "$ref": "#/components/servers/production" + } + }, + "channels": { + "userSignedup": { + "$ref": "#/components/channels/userSignedup" + } + }, + "operations": { + "sendUserSignedup": { + "$ref": "#/components/operations/sendUserSignedup" + } + }, + "components": { + "schemas": { + "userSignedUpPayload": { + "type": "object", + "properties": { + "displayName": { + "type": "string" + } + } + } + }, + "servers": { + "production": { + "host": "rabbitmq.example.org", + "protocol": "amqp", + "variables": { + "port": { + "$ref": "#/components/serverVariables/port" + } + }, + "security": [ + { + "$ref": "#/components/securitySchemes/user-password" + } + ], + "tags": [ + { + "$ref": "#/components/tags/user" + } + ], + "bindings": { + "$ref": "#/components/serverBindings/amqp" + } + } + }, + "channels": { + "userSignedup": { + "address": "user/{userId}/signedup", + "messages": { + "userSignedUp": { + "$ref": "#/components/messages/userSignedUp" + } + }, + "parameters": { + "userId": { + "$ref": "#/components/parameters/userId" + } + }, + "bindings": { + "$ref": "#/components/channelBindings/amqp" + } + }, + "userSignedupReply": { + "messages": { + "userSignedUp": { + "$ref": "#/components/messages/userSignedUp" + } + } + } + }, + "operations": { + "sendUserSignedup": { + "action": "send", + "channel": { + "$ref": "#/components/channels/userSignedup" + }, + "bindings": { + "$ref": "#/components/operationBindings/amqp" + }, + "traits": [ + { + "$ref": "#/components/operationTraits/kafka" + } + ], + "messages": [ + { + "$ref": "#/components/channels/userSignedup/messages/userSignedUp" + } + ], + "reply": { + "$ref": "#/components/replies/userSignedupReply" + } + } + }, + "messages": { + "userSignedUp": { + "payload": { + "$ref": "#/components/schemas/userSignedUpPayload" + }, + "correlationId": { + "$ref": "#/components/correlationIds/default" + }, + "bindings": { + "$ref": "#/components/messageBindings/amqp" + }, + "traits": [ + { + "$ref": "#/components/messageTraits/commonHeaders" + } + ] + } + }, + "securitySchemes": { + "user-password": { + "type": "userPassword" + } + }, + "serverVariables": { + "port": { + "enum": [ + "5672", + "5673" + ], + "default": "5672" + } + }, + "parameters": { + "userId": { + "description": "Id of the user." + } + }, + "correlationIds": { + "default": { + "description": "Default Correlation ID", + "location": "$message.header#/correlationId" + } + }, + "replies": { + "userSignedupReply": { + "address": { + "$ref": "#/components/replyAddresses/userSignedupReply" + }, + "channel": { + "$ref": "#/components/channels/userSignedupReply" + }, + "messages": [ + { + "$ref": "#/components/channels/userSignedupReply/messages/userSignedUp" + } + ] + } + }, + "replyAddresses": { + "userSignedupReply": { + "description": "Consumer inbox", + "location": "$message.header#/replyTo" + } + }, + "externalDocs": { + "infoDocs": { + "url": "https://example.com/docs" + } + }, + "tags": { + "user": { + "name": "user", + "description": "User-related messages" + } + }, + "operationTraits": { + "kafka": { + "bindings": { + "kafka": { + "clientId": "my-app-id" + } + } + } + }, + "messageTraits": { + "commonHeaders": { + "headers": { + "type": "object", + "properties": { + "my-app-header": { + "type": "integer" + } + } + } + } + }, + "serverBindings": { + "amqp": { + "amqp": { + "vhost": "/" + } + } + }, + "channelBindings": { + "amqp": { + "amqp": { + "is": "queue" + } + } + }, + "operationBindings": { + "amqp": { + "amqp": { + "ack": true + } + } + }, + "messageBindings": { + "amqp": { + "amqp": { + "contentEncoding": "gzip" + } + } + } + } +}` + +func TestComponents(t *testing.T) { + t.Parallel() + + doc, err := asyncapi.LoadFromDataJSON([]byte(allComponents)) + if err != nil { + t.Fatal(err) + } + + if err := doc.Validate(); err != nil { + t.Fatal(err) + } + + // every reference was resolved + op := doc.Operations["sendUserSignedup"] + if op.Value != doc.Components.Operations["sendUserSignedup"].Value { + t.Fatal("the operation was not resolved") + } + + reply := op.Value.Reply + if reply.Value != doc.Components.Replies["userSignedupReply"].Value { + t.Fatal("the reply was not resolved") + } + + if reply.Value.Address.Value != doc.Components.ReplyAddresses["userSignedupReply"].Value { + t.Fatal("the reply address was not resolved") + } + + if got, want := reply.Value.Address.Value.Location, + asyncapi.RuntimeExpression("$message.header#/replyTo"); got != want { + t.Fatalf("got: %v, want: %v", got, want) + } + + msg := doc.Components.Messages["userSignedUp"].Value + if msg.CorrelationID.Value != doc.Components.CorrelationIDs["default"].Value { + t.Fatal("the correlation ID was not resolved") + } + + server := doc.Servers["production"].Value + if server.Bindings.Value != doc.Components.ServerBindings["amqp"].Value { + t.Fatal("the server bindings were not resolved") + } + + if server.Tags[0].Value != doc.Components.Tags["user"].Value { + t.Fatal("the tag was not resolved") + } + + if doc.Info.ExternalDocs.Value != doc.Components.ExternalDocs["infoDocs"].Value { + t.Fatal("the external documentation was not resolved") + } + + // the document is written back as it was read + got, err := doc.ToJSON() + if err != nil { + t.Fatal(err) + } + + if string(got) != allComponents { + t.Fatalf("got:\n%s\nwant:\n%s", got, allComponents) + } +} + +func TestComponents_ReplyWithAddressAndChannelAddress(t *testing.T) { + t.Parallel() + + doc, err := asyncapi.LoadFromDataJSON([]byte(allComponents)) + if err != nil { + t.Fatal(err) + } + + // when the reply defines an address, the channel it refers to must not have one + doc.Components.Channels["userSignedupReply"].Value.Address = "user/signedup/reply" + + err = doc.Validate() + if err == nil { + t.Fatal("expected error") + } + + want := `components.replies["userSignedupReply"].channel.address ("user/signedup/reply") ` + + `is invalid: must be empty when the reply defines an address` + if err.Error() != want { + t.Fatalf("got: %v, want: %v", err, want) + } +} diff --git a/asyncapi/contact.go b/asyncapi/contact.go new file mode 100644 index 0000000..ef0827d --- /dev/null +++ b/asyncapi/contact.go @@ -0,0 +1,38 @@ +package asyncapi + +import ( + "net/url" + + "github.com/MarkRosemaker/errpath" + "github.com/go-api-libs/types" +) + +// Contact information for the exposed API. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#contactObject +type Contact struct { + // The identifying name of the contact person/organization. + Name string `json:"name,omitempty" yaml:"name,omitempty"` + // The URL pointing to the contact information. This MUST be in the form of an absolute URL. + URL *url.URL `json:"url,omitempty" yaml:"url,omitempty"` + // The email address of the contact person/organization. MUST be in the format of an email address. + Email types.Email `json:"email,omitempty" yaml:"email,omitempty"` + // This object MAY be extended with Specification Extensions. + Extensions Extensions `json:",inline" yaml:",inline"` +} + +// Validate checks the contact for consistency. +func (c *Contact) Validate() error { + if err := validateURL(c.URL); err != nil { + return &errpath.ErrField{Field: "url", Err: err} + } + + if c.Email != "" { + if err := c.Email.Validate(); err != nil { + return &errpath.ErrField{Field: "email", Err: err} + } + } + + return validateExtensions(c.Extensions) +} diff --git a/asyncapi/correlation_id.go b/asyncapi/correlation_id.go new file mode 100644 index 0000000..409430e --- /dev/null +++ b/asyncapi/correlation_id.go @@ -0,0 +1,41 @@ +package asyncapi + +import ( + "strings" + + "github.com/MarkRosemaker/errpath" +) + +// CorrelationID is an object that specifies an identifier at design time that can used for message tracing and correlation. +// +// For specifying and computing the location of a Correlation ID, a [RuntimeExpression] is used. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#correlationIdObject +type CorrelationID struct { + // An optional description of the identifier. CommonMark syntax can be used for rich text representation. + Description string `json:"description,omitempty" yaml:"description,omitempty"` + // REQUIRED. A runtime expression that specifies the location of the correlation ID. + Location RuntimeExpression `json:"location" yaml:"location"` + // This object MAY be extended with Specification Extensions. + Extensions Extensions `json:",inline" yaml:",inline"` +} + +// Validate checks the correlation ID for correctness. +func (c *CorrelationID) Validate() error { + c.Description = strings.TrimSpace(c.Description) + + if err := c.Location.Validate(); err != nil { + return &errpath.ErrField{Field: "location", Err: err} + } + + return validateExtensions(c.Extensions) +} + +func (l *loader) collectCorrelationIDRef(c *CorrelationIDRef, ref ref) { + collectRef(l, c, l.correlationIDs, ref) +} + +func (l *loader) resolveCorrelationIDRef(c *CorrelationIDRef) error { + return resolveRef(c, l.correlationIDs, nil) +} diff --git a/asyncapi/correlation_ids.go b/asyncapi/correlation_ids.go new file mode 100644 index 0000000..e634617 --- /dev/null +++ b/asyncapi/correlation_ids.go @@ -0,0 +1,72 @@ +package asyncapi + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "iter" + + "github.com/MarkRosemaker/errpath" + "github.com/MarkRosemaker/ordmap" +) + +// CorrelationIDs is a map of Correlation ID Objects. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#componentsCorrelationIDs +type CorrelationIDs map[string]*CorrelationIDRef + +// Validate validates each correlation ID. +func (cs CorrelationIDs) Validate() error { + for name, c := range cs.ByIndex() { + if err := c.Validate(); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} + +// ByIndex returns a sequence of key-value pairs ordered by index. +func (cs CorrelationIDs) ByIndex() iter.Seq2[string, *CorrelationIDRef] { + return ordmap.ByIndex(cs, getIndexRef[CorrelationID, *CorrelationID]) +} + +// Sort sorts the map by key and sets the indices accordingly. +func (cs CorrelationIDs) Sort() { + ordmap.Sort(cs, setIndexRef[CorrelationID, *CorrelationID]) +} + +// Set sets a value in the map, adding it at the end of the order. +func (cs *CorrelationIDs) Set(key string, c *CorrelationIDRef) { + ordmap.Set(cs, key, c, getIndexRef[CorrelationID, *CorrelationID], setIndexRef[CorrelationID, *CorrelationID]) +} + +var _ json.MarshalerTo = (*CorrelationIDs)(nil) + +// MarshalJSONTo marshals the key-value pairs in order. +func (cs *CorrelationIDs) MarshalJSONTo(enc *jsontext.Encoder) error { + return ordmap.MarshalJSONTo(cs, enc) +} + +var _ json.UnmarshalerFrom = (*CorrelationIDs)(nil) + +// UnmarshalJSONFrom unmarshals the key-value pairs in order and sets the indices. +func (cs *CorrelationIDs) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + return ordmap.UnmarshalJSONFrom(cs, dec, setIndexRef[CorrelationID, *CorrelationID]) +} + +func (l *loader) collectCorrelationIDs(cs CorrelationIDs, ref ref) { + for name, c := range cs.ByIndex() { + l.collectCorrelationIDRef(c, append(ref, name)) + } +} + +func (l *loader) resolveCorrelationIDs(cs CorrelationIDs) error { + for name, c := range cs.ByIndex() { + if err := l.resolveCorrelationIDRef(c); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} diff --git a/asyncapi/data_type.go b/asyncapi/data_type.go new file mode 100644 index 0000000..10af6eb --- /dev/null +++ b/asyncapi/data_type.go @@ -0,0 +1,126 @@ +package asyncapi + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "slices" + + "github.com/MarkRosemaker/errpath" +) + +// DataType is the type of a schema, based on the types supported by the JSON Schema Specification Draft 07. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#dataTypeFormat +type DataType string + +const ( + // TypeInteger is a JSON number without a fraction or exponent part. Format: int32, int64 + TypeInteger DataType = "integer" + // TypeNumber is a JSON number. Format: float, double + TypeNumber DataType = "number" + // TypeString is a JSON string. Format: byte, binary, date, date-time, password + TypeString DataType = "string" + // TypeArray is a JSON array. + TypeArray DataType = "array" + // TypeBoolean is a JSON boolean. + TypeBoolean DataType = "boolean" + // TypeObject is a JSON object. + TypeObject DataType = "object" + // TypeNull is the JSON null value. + TypeNull DataType = "null" +) + +var allDataTypes = []DataType{ + TypeInteger, TypeNumber, TypeString, TypeArray, TypeBoolean, TypeObject, TypeNull, +} + +// Validate validates the data type. +func (d DataType) Validate() error { + if slices.Contains(allDataTypes, d) { + return nil + } + + return &errpath.ErrInvalid[DataType]{ + Value: d, + Enum: allDataTypes, + } +} + +// DataTypes is the value of the `type` keyword of a schema. +// +// JSON Schema allows a single type as well as a list of types, +// e.g. `"type": "string"` and `"type": ["string", "null"]` are both valid. +// A single type is marshalled back as a single type, not as a list. +type DataTypes []DataType + +// Validate validates each data type. +func (ds DataTypes) Validate() error { + for i, d := range ds { + if err := d.Validate(); err != nil { + if len(ds) == 1 { // don't report an index if there is only one type + return err + } + + return &errpath.ErrIndex{Index: i, Err: err} + } + } + + return nil +} + +// Contains reports whether the schema is of the given type. +func (ds DataTypes) Contains(d DataType) bool { return slices.Contains(ds, d) } + +// String returns the types as a comma-separated list. +func (ds DataTypes) String() string { + switch len(ds) { + case 0: + return "" + case 1: + return string(ds[0]) + } + + s := string(ds[0]) + for _, d := range ds[1:] { + s += ", " + string(d) + } + + return s +} + +var _ json.UnmarshalerFrom = (*DataTypes)(nil) + +// UnmarshalJSONFrom unmarshals either a single type or a list of types. +func (ds *DataTypes) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + if dec.PeekKind() == '[' { + var types []DataType + if err := json.UnmarshalDecode(dec, &types); err != nil { + return err + } + + *ds = types + + return nil + } + + var d DataType + if err := json.UnmarshalDecode(dec, &d); err != nil { + return err + } + + *ds = DataTypes{d} + + return nil +} + +var _ json.MarshalerTo = (*DataTypes)(nil) + +// MarshalJSONTo marshals a single type as a string and multiple types as a list. +func (ds *DataTypes) MarshalJSONTo(enc *jsontext.Encoder) error { + if len(*ds) == 1 { + return json.MarshalEncode(enc, (*ds)[0]) + } + + return json.MarshalEncode(enc, []DataType(*ds)) +} diff --git a/asyncapi/data_type_format.go b/asyncapi/data_type_format.go new file mode 100644 index 0000000..9fefdac --- /dev/null +++ b/asyncapi/data_type_format.go @@ -0,0 +1,48 @@ +package asyncapi + +import "slices" + +// Format defines additional formats to provide fine detail for primitive data types. +// +// The format property is an open string-valued property, and can have any value to support +// documentation needs, so an unknown format is not an error. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#dataTypeFormat +type Format string + +const ( + // FormatInt32 represents a signed 32 bits integer. + FormatInt32 Format = "int32" + // FormatInt64 represents a signed 64 bits integer. + FormatInt64 Format = "int64" + // FormatFloat represents a float number. + FormatFloat Format = "float" + // FormatDouble represents a double number. + FormatDouble Format = "double" + // FormatByte represents base64 encoded characters. + FormatByte Format = "byte" + // FormatBinary represents any sequence of octets. + FormatBinary Format = "binary" + // FormatDate represents a date as defined by full-date in RFC3339. + FormatDate Format = "date" + // FormatDateTime represents a date-time as defined by date-time in RFC3339. + FormatDateTime Format = "date-time" + // FormatPassword is a hint to UIs that the input needs to be obscured. + FormatPassword Format = "password" +) + +// allFormats are the formats defined by the AsyncAPI Specification. +var allFormats = []Format{ + FormatInt32, FormatInt64, + FormatFloat, FormatDouble, + FormatByte, FormatBinary, + FormatDate, FormatDateTime, + FormatPassword, +} + +// IsKnown reports whether the format is one of the formats defined by the AsyncAPI Specification. +// +// Formats such as "email" or "uuid" can be used even though they are not defined by the +// specification, so a format that is not known is not necessarily invalid. +func (f Format) IsKnown() bool { return slices.Contains(allFormats, f) } diff --git a/asyncapi/doc.go b/asyncapi/doc.go new file mode 100644 index 0000000..55ae6fc --- /dev/null +++ b/asyncapi/doc.go @@ -0,0 +1,48 @@ +// Package asyncapi parses, validates, formats and writes [AsyncAPI] specifications. +// +// It implements version 3.1.0 of the [AsyncAPI Specification], which describes an +// event-driven API: "The AsyncAPI Specification defines a set of files required to describe +// an application's API. These files can then be used to create utilities, such as +// documentation, code, integration, or testing tools." +// +// # Reading a document +// +// A document is read with [LoadFromFile], [LoadFromData] or [LoadFromReader], each of which +// determines whether the document is JSON or YAML, since "an AsyncAPI document can be JSON +// or YAML format" ([Format]). Use the JSON or YAML variants, e.g. [LoadFromDataJSON], if the +// format is already known. +// +// While reading, every reference is resolved, i.e. the [Reference] and the object it points +// to are both available. References that point to other references are followed, e.g. an +// operation that refers to a message of a channel which in turn refers to a message of the +// [Components] object. +// +// # Validating a document +// +// [Document.Validate] checks the document against the rules of the specification: required +// fields, enumerations, patterns of keys and addresses, absolute URLs, runtime expressions, +// as well as the rules that span several objects, e.g. that the messages of an operation +// "MUST contain a subset of the messages defined in the channel referenced in this +// operation" ([Operation Object]). +// +// An error tells where the problem is, e.g. +// +// channels["userSignedup"].messages["userSignedUp"].contentType: mime: expected slash after first token +// +// Validation also normalizes a document where the specification allows it, e.g. by trimming +// the whitespace around a description or by adding the https scheme to a URL that is missing one. +// +// # Writing a document +// +// [Document.ToJSON], [Document.ToYAML], [Document.WriteJSON], [Document.WriteYAML] and +// [Document.WriteToFile] write the document back. Everything that was read is written back: +// the order of the keys of every map is preserved, specification extensions are kept where +// they were, and the definitions of bindings and of schemas in other formats (Avro, Protobuf, +// ...) are kept as they were given. [Document.SortMaps] sorts the maps of the document by key +// if a canonical order is preferred over the original one. +// +// [AsyncAPI]: https://www.asyncapi.com +// [AsyncAPI Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0 +// [Format]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#format +// [Operation Object]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#operationObject +package asyncapi diff --git a/asyncapi/document.go b/asyncapi/document.go new file mode 100644 index 0000000..ee277b6 --- /dev/null +++ b/asyncapi/document.go @@ -0,0 +1,241 @@ +package asyncapi + +import ( + "errors" + "net/url" + "regexp" + + "github.com/MarkRosemaker/errpath" +) + +// ErrEmptyDocument is thrown if the AsyncAPI document neither defines channels nor operations nor components. +var ErrEmptyDocument = errors.New("document must contain at least a channels field, an operations field or a components field") + +var ( + // ErrServerNotInRoot is returned when a channel of the root Channels Object refers to a + // server that is not defined in the root Servers Object. + ErrServerNotInRoot = errors.New("must point to a server of the root servers object") + // ErrChannelNotInRoot is returned when an operation of the root Operations Object refers to + // a channel that is not defined in the root Channels Object. + ErrChannelNotInRoot = errors.New("must point to a channel of the root channels object") +) + +// reServerKey is the regular expression the keys of the servers object must match. +var reServerKey = regexp.MustCompile(`^[A-Za-z0-9_\-]+$`) + +// Document is an AsyncAPI document, the root object. +// It combines resource listing and API declaration together into one document. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#A2SObject +type Document struct { + // REQUIRED. Specifies the AsyncAPI Specification version being used. + // It can be used by tooling Specifications and clients to interpret the version. + // The structure shall be `major`.`minor`.`patch`, where `patch` versions MUST be compatible + // with the existing `major`.`minor` tooling. + AsyncAPI string `json:"asyncapi" yaml:"asyncapi"` + // Identifier of the application the AsyncAPI document is defining. + // It must conform to the URI format. + // It is RECOMMENDED to use a URN to globally and uniquely identify the application + // during long periods of time, even after it becomes unavailable or ceases to exist. + ID *url.URL `json:"id,omitempty" yaml:"id,omitempty"` + // REQUIRED. Provides metadata about the API. The metadata can be used by the clients if needed. + Info *Info `json:"info,omitempty" yaml:"info,omitempty"` + // Provides connection details of servers. + Servers Servers `json:"servers,omitempty" yaml:"servers,omitempty"` + // Default content type to use when encoding/decoding a message's payload. + // The value MUST be a specific media type (e.g. `application/json`). + // This value MUST be used by schema parsers when the contentType property is omitted. + DefaultContentType MediaType `json:"defaultContentType,omitempty" yaml:"defaultContentType,omitempty"` + // The channels used by this application. + Channels Channels `json:"channels,omitempty" yaml:"channels,omitempty"` + // The operations this application MUST implement. + Operations Operations `json:"operations,omitempty" yaml:"operations,omitempty"` + // An element to hold various reusable objects for the specification. + // Everything that is defined inside this object represents a resource that MAY or MAY NOT be + // used in the rest of the document and MAY or MAY NOT be used by the implemented application. + Components Components `json:"components,omitzero" yaml:"components,omitempty"` + // This object MAY be extended with Specification Extensions. + Extensions Extensions `json:",inline" yaml:",inline"` +} + +// reAsyncAPIVersion is a regular expression that matches the AsyncAPI version. +// +// "The format for this string must be `major`.`minor`.`patch`. The `patch` may be suffixed by a +// hyphen and extra alphanumeric characters." Only major version 3 is supported. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#A2SVersionString +var reAsyncAPIVersion = regexp.MustCompile(`^3\.\d+\.\d+(-[A-Za-z0-9]+)?$`) + +// Validate checks the AsyncAPI document for correctness. +func (d *Document) Validate() error { + if d.AsyncAPI == "" { + return &errpath.ErrField{Field: "asyncapi", Err: &errpath.ErrRequired{}} + } + + if !reAsyncAPIVersion.MatchString(d.AsyncAPI) { + return &errpath.ErrField{ + Field: "asyncapi", + Err: &errpath.ErrInvalid[string]{ + Value: d.AsyncAPI, + Message: "must be a valid version (3.x.y)", + }, + } + } + + // the identifier "must conform to the URI format, according to RFC3986" + if err := validateURI(d.ID); err != nil { + return &errpath.ErrField{Field: "id", Err: err} + } + + if d.Info == nil { + return &errpath.ErrField{Field: "info", Err: &errpath.ErrRequired{}} + } + + if err := d.Info.Validate(); err != nil { + return &errpath.ErrField{Field: "info", Err: err} + } + + // the keys of the servers object must match a certain pattern + for name := range d.Servers { + if !reServerKey.MatchString(name) { + return &errpath.ErrField{Field: "servers", Err: &errpath.ErrKey{ + Key: name, + Err: &errpath.ErrInvalid[string]{ + Value: name, + Message: "must match the regular expression " + `"` + reServerKey.String() + `"`, + }, + }} + } + } + + if err := d.Servers.Validate(); err != nil { + return &errpath.ErrField{Field: "servers", Err: err} + } + + if d.DefaultContentType != "" { + if err := d.DefaultContentType.Validate(); err != nil { + return &errpath.ErrField{Field: "defaultContentType", Err: err} + } + } + + // an AsyncAPI document that neither describes channels nor operations nor + // reusable components doesn't describe anything at all + if len(d.Channels) == 0 && len(d.Operations) == 0 && d.Components.isEmpty() { + return ErrEmptyDocument + } + + if err := d.Channels.Validate(); err != nil { + return &errpath.ErrField{Field: "channels", Err: err} + } + + if err := d.Operations.Validate(); err != nil { + return &errpath.ErrField{Field: "operations", Err: err} + } + + if err := d.Components.Validate(); err != nil { + return &errpath.ErrField{Field: "components", Err: err} + } + + if err := d.validateLocations(); err != nil { + return err + } + + return validateExtensions(d.Extensions) +} + +// validateLocations checks the rules that the specification puts on objects +// that are defined in the root of the document, as opposed to the components object: +// +// - The servers of a channel of the root Channels Object "MUST point to a subset of server +// definitions located in the root Servers Object, and MUST NOT point to a subset of server +// definitions located in the Components Object or anywhere else." +// - The channel of an operation of the root Operations Object "MUST point to a channel +// definition located in the root Channels Object, and MUST NOT point to a channel definition +// located in the Components Object or anywhere else." +// +// A channel or an operation that is given as a reference is defined somewhere else, +// where these rules don't apply, so it is skipped here. +func (d *Document) validateLocations() error { + for name, c := range d.Channels.ByIndex() { + if c.isRef() { + continue + } + + for i, s := range c.Value.Servers { + if contains(d.Servers, s.Value) { + continue + } + + return &errpath.ErrField{Field: "channels", Err: &errpath.ErrKey{ + Key: name, + Err: &errpath.ErrField{Field: "servers", Err: &errpath.ErrIndex{ + Index: i, Err: ErrServerNotInRoot, + }}, + }} + } + } + + for name, o := range d.Operations.ByIndex() { + if o.isRef() || o.Value.Channel == nil { + continue + } + + if contains(d.Channels, o.Value.Channel.Value) { + continue + } + + return &errpath.ErrField{Field: "operations", Err: &errpath.ErrKey{ + Key: name, + Err: &errpath.ErrField{Field: "channel", Err: ErrChannelNotInRoot}, + }} + } + + return nil +} + +// SortMaps sorts the servers, channels, operations and the fields of the components that are maps by key. +func (d *Document) SortMaps() { + d.Servers.Sort() + d.Channels.Sort() + d.Operations.Sort() + d.Components.SortMaps() +} + +func (l *loader) collectDocument(doc *Document, ref ref) { + l.collectInfo(doc.Info, append(ref, "info")) + l.collectServers(doc.Servers, append(ref, "servers")) + l.collectChannels(doc.Channels, append(ref, "channels")) + l.collectOperations(doc.Operations, append(ref, "operations")) + l.collectComponents(doc.Components, append(ref, "components")) +} + +func (l *loader) resolveDocument(doc *Document) error { + // fields that don't need to be resolved: + // - AsyncAPI + // - ID + // - DefaultContentType + + if err := l.resolveInfo(doc.Info); err != nil { + return &errpath.ErrField{Field: "info", Err: err} + } + + if err := l.resolveServers(doc.Servers); err != nil { + return &errpath.ErrField{Field: "servers", Err: err} + } + + if err := l.resolveChannels(doc.Channels); err != nil { + return &errpath.ErrField{Field: "channels", Err: err} + } + + if err := l.resolveOperations(doc.Operations); err != nil { + return &errpath.ErrField{Field: "operations", Err: err} + } + + if err := l.resolveComponents(doc.Components); err != nil { + return &errpath.ErrField{Field: "components", Err: err} + } + + return nil +} diff --git a/asyncapi/document_test.go b/asyncapi/document_test.go new file mode 100644 index 0000000..33f33d3 --- /dev/null +++ b/asyncapi/document_test.go @@ -0,0 +1,151 @@ +package asyncapi_test + +import ( + "testing" + + "github.com/MarkRosemaker/asyncapi" +) + +// minimalDocument returns the smallest document that is valid. +func minimalDocument() *asyncapi.Document { + return &asyncapi.Document{ + AsyncAPI: "3.1.0", + Info: &asyncapi.Info{Title: "Account Service", Version: "1.0.0"}, + Channels: asyncapi.Channels{ + "userSignedup": {Value: &asyncapi.Channel{Address: "user/signedup"}}, + }, + } +} + +func TestDocument_Validate(t *testing.T) { + t.Parallel() + + if err := minimalDocument().Validate(); err != nil { + t.Fatal(err) + } +} + +func TestDocument_Validate_Errors(t *testing.T) { + t.Parallel() + + for name, tc := range map[string]struct { + doc func(*asyncapi.Document) + want string + }{ + "no version": { + func(d *asyncapi.Document) { d.AsyncAPI = "" }, + "asyncapi is required", + }, + "version of another major version": { + func(d *asyncapi.Document) { d.AsyncAPI = "2.6.0" }, + `asyncapi ("2.6.0") is invalid: must be a valid version (3.x.y)`, + }, + "version is not a version": { + func(d *asyncapi.Document) { d.AsyncAPI = "three" }, + `asyncapi ("three") is invalid: must be a valid version (3.x.y)`, + }, + "no info": { + func(d *asyncapi.Document) { d.Info = nil }, + "info is required", + }, + "no title": { + func(d *asyncapi.Document) { d.Info.Title = "" }, + "info.title is required", + }, + "no version of the API": { + func(d *asyncapi.Document) { d.Info.Version = "" }, + "info.version is required", + }, + "nothing is described": { + func(d *asyncapi.Document) { d.Channels = nil }, + asyncapi.ErrEmptyDocument.Error(), + }, + "invalid server name": { + func(d *asyncapi.Document) { + d.Servers = asyncapi.Servers{"my server": {Value: &asyncapi.Server{ + Host: "example.com", Protocol: asyncapi.ProtocolKafka, + }}} + }, + `servers["my server"] ("my server") is invalid: must match the regular expression "^[A-Za-z0-9_\-]+$"`, + }, + "server without a host": { + func(d *asyncapi.Document) { + d.Servers = asyncapi.Servers{"production": {Value: &asyncapi.Server{ + Protocol: asyncapi.ProtocolKafka, + }}} + }, + `servers["production"].host is required`, + }, + "server without a protocol": { + func(d *asyncapi.Document) { + d.Servers = asyncapi.Servers{"production": {Value: &asyncapi.Server{ + Host: "example.com", + }}} + }, + `servers["production"].protocol is required`, + }, + "invalid default content type": { + func(d *asyncapi.Document) { d.DefaultContentType = "not a media type" }, + "defaultContentType: mime: expected slash after first token", + }, + "invalid component name": { + func(d *asyncapi.Document) { + d.Components.Schemas = asyncapi.Schemas{ + "not a valid name": {Value: &asyncapi.AnySchema{Schema: &asyncapi.Schema{}}}, + } + }, + `components.schemas["not a valid name"] ("not a valid name") is invalid: must match the regular expression "^[a-zA-Z0-9\\.\\-_]+$"`, + }, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + + doc := minimalDocument() + tc.doc(doc) + + err := doc.Validate() + if err == nil { + t.Fatal("expected error") + } + + if err.Error() != tc.want { + t.Fatalf("got: %v, want: %v", err, tc.want) + } + }) + } +} + +func TestDocument_SortMaps(t *testing.T) { + t.Parallel() + + doc, err := asyncapi.LoadFromFile("examples/v3.1/streetlights-kafka.yaml") + if err != nil { + t.Fatal(err) + } + + doc.SortMaps() + + want := []string{"lightTurnOff", "lightTurnOn", "lightingMeasured", "lightsDim"} + + i := 0 + for name := range doc.Channels.ByIndex() { + if name != want[i] { + t.Fatalf("got: %v, want: %v", name, want[i]) + } + + i++ + } + + wantSchemas := []string{ + "dimLightPayload", "lightMeasuredPayload", "sentAt", "turnOnOffPayload", + } + + i = 0 + for name := range doc.Components.Schemas.ByIndex() { + if name != wantSchemas[i] { + t.Fatalf("got: %v, want: %v", name, wantSchemas[i]) + } + + i++ + } +} diff --git a/asyncapi/examples/invalid.txt b/asyncapi/examples/invalid.txt new file mode 100644 index 0000000..0c583a1 --- /dev/null +++ b/asyncapi/examples/invalid.txt @@ -0,0 +1 @@ +this is not a spec diff --git a/asyncapi/examples/v3.1/anyof.json b/asyncapi/examples/v3.1/anyof.json new file mode 100644 index 0000000..d88ed5d --- /dev/null +++ b/asyncapi/examples/v3.1/anyof.json @@ -0,0 +1,65 @@ +{ + "asyncapi": "3.1.0", + "info": { + "title": "AnyOf example", + "version": "1.0.0" + }, + "channels": { + "test": { + "address": "test", + "messages": { + "testMessages": { + "$ref": "#/components/messages/testMessages" + } + } + } + }, + "operations": { + "test": { + "action": "receive", + "channel": { + "$ref": "#/channels/test" + }, + "messages": [ + { + "$ref": "#/channels/test/messages/testMessages" + } + ] + } + }, + "components": { + "schemas": { + "objectWithKey": { + "type": "object", + "properties": { + "key": { + "type": "string", + "additionalProperties": false + } + } + }, + "objectWithKey2": { + "type": "object", + "properties": { + "key2": { + "type": "string" + } + } + } + }, + "messages": { + "testMessages": { + "payload": { + "anyOf": [ + { + "$ref": "#/components/schemas/objectWithKey" + }, + { + "$ref": "#/components/schemas/objectWithKey2" + } + ] + } + } + } + } +} diff --git a/asyncapi/examples/v3.1/anyof.yaml b/asyncapi/examples/v3.1/anyof.yaml new file mode 100644 index 0000000..80ddeeb --- /dev/null +++ b/asyncapi/examples/v3.1/anyof.yaml @@ -0,0 +1,36 @@ +asyncapi: 3.1.0 +info: + title: AnyOf example + version: 1.0.0 +channels: + test: + address: test + messages: + testMessages: + $ref: '#/components/messages/testMessages' +operations: + test: + action: receive + channel: + $ref: '#/channels/test' + messages: + - $ref: '#/channels/test/messages/testMessages' +components: + messages: + testMessages: + payload: + anyOf: + - $ref: '#/components/schemas/objectWithKey' + - $ref: '#/components/schemas/objectWithKey2' + schemas: + objectWithKey: + type: object + properties: + key: + type: string + additionalProperties: false + objectWithKey2: + type: object + properties: + key2: + type: string \ No newline at end of file diff --git a/asyncapi/examples/v3.1/correlation-id.json b/asyncapi/examples/v3.1/correlation-id.json new file mode 100644 index 0000000..99b4eb2 --- /dev/null +++ b/asyncapi/examples/v3.1/correlation-id.json @@ -0,0 +1,259 @@ +{ + "asyncapi": "3.1.0", + "info": { + "title": "Correlation ID Example", + "version": "1.0.0", + "description": "A cut of the Streetlights API to test Correlation ID", + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0" + } + }, + "servers": { + "production": { + "host": "test.mosquitto.org:{port}", + "protocol": "mqtt", + "description": "Test broker", + "variables": { + "port": { + "enum": [ + "1883", + "8883" + ], + "default": "1883", + "description": "Secure connection (TLS) is available through port 8883." + } + }, + "security": [ + { + "$ref": "#/components/securitySchemes/apiKey" + }, + { + "type": "oauth2", + "description": "Flows to support OAuth 2.0", + "flows": { + "implicit": { + "authorizationUrl": "https://authserver.example/auth", + "availableScopes": { + "streetlights:on": "Ability to switch lights on", + "streetlights:off": "Ability to switch lights off", + "streetlights:dim": "Ability to dim the lights" + } + }, + "password": { + "tokenUrl": "https://authserver.example/token", + "availableScopes": { + "streetlights:on": "Ability to switch lights on", + "streetlights:off": "Ability to switch lights off", + "streetlights:dim": "Ability to dim the lights" + } + }, + "clientCredentials": { + "tokenUrl": "https://authserver.example/token", + "availableScopes": { + "streetlights:on": "Ability to switch lights on", + "streetlights:off": "Ability to switch lights off", + "streetlights:dim": "Ability to dim the lights" + } + }, + "authorizationCode": { + "authorizationUrl": "https://authserver.example/auth", + "tokenUrl": "https://authserver.example/token", + "refreshUrl": "https://authserver.example/refresh", + "availableScopes": { + "streetlights:on": "Ability to switch lights on", + "streetlights:off": "Ability to switch lights off", + "streetlights:dim": "Ability to dim the lights" + } + } + }, + "scopes": [ + "streetlights:on", + "streetlights:off", + "streetlights:dim" + ] + }, + { + "$ref": "#/components/securitySchemes/openIdConnectWellKnown" + } + ] + } + }, + "defaultContentType": "application/json", + "channels": { + "lightingMeasured": { + "address": "smartylighting/streetlights/1/0/event/{streetlightId}/lighting/measured", + "messages": { + "lightMeasured": { + "$ref": "#/components/messages/lightMeasured" + } + }, + "parameters": { + "streetlightId": { + "$ref": "#/components/parameters/streetlightId" + } + } + }, + "lightsDim": { + "address": "smartylighting/streetlights/1/0/action/{streetlightId}/dim", + "messages": { + "dimLight": { + "$ref": "#/components/messages/dimLight" + } + }, + "parameters": { + "streetlightId": { + "$ref": "#/components/parameters/streetlightId" + } + } + } + }, + "operations": { + "receiveLightMeasurement": { + "action": "receive", + "channel": { + "$ref": "#/channels/lightingMeasured" + }, + "summary": "Inform about environmental lighting conditions of a particular streetlight.", + "messages": [ + { + "$ref": "#/channels/lightingMeasured/messages/lightMeasured" + } + ] + }, + "dimLight": { + "action": "send", + "channel": { + "$ref": "#/channels/lightsDim" + }, + "messages": [ + { + "$ref": "#/channels/lightsDim/messages/dimLight" + } + ] + } + }, + "components": { + "schemas": { + "lightMeasuredPayload": { + "type": "object", + "properties": { + "lumens": { + "description": "Light intensity measured in lumens.", + "type": "integer", + "minimum": 0 + }, + "sentAt": { + "$ref": "#/components/schemas/sentAt" + } + } + }, + "dimLightPayload": { + "type": "object", + "properties": { + "percentage": { + "description": "Percentage to which the light should be dimmed to.", + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "sentAt": { + "$ref": "#/components/schemas/sentAt" + } + } + }, + "sentAt": { + "description": "Date and time when the message was sent.", + "type": "string", + "format": "date-time" + } + }, + "messages": { + "lightMeasured": { + "payload": { + "$ref": "#/components/schemas/lightMeasuredPayload" + }, + "correlationId": { + "location": "$message.header#/MQMD/CorrelId" + }, + "contentType": "application/json", + "name": "lightMeasured", + "title": "Light measured", + "summary": "Inform about environmental lighting conditions of a particular streetlight." + }, + "dimLight": { + "payload": { + "$ref": "#/components/schemas/dimLightPayload" + }, + "correlationId": { + "$ref": "#/components/correlationIds/sentAtCorrelator" + }, + "name": "dimLight", + "title": "Dim light", + "summary": "Command a particular streetlight to dim the lights." + } + }, + "securitySchemes": { + "apiKey": { + "type": "apiKey", + "description": "Provide your API key as the user and leave the password empty.", + "in": "user" + }, + "supportedOauthFlows": { + "type": "oauth2", + "description": "Flows to support OAuth 2.0", + "flows": { + "implicit": { + "authorizationUrl": "https://authserver.example/auth", + "availableScopes": { + "streetlights:on": "Ability to switch lights on", + "streetlights:off": "Ability to switch lights off", + "streetlights:dim": "Ability to dim the lights" + } + }, + "password": { + "tokenUrl": "https://authserver.example/token", + "availableScopes": { + "streetlights:on": "Ability to switch lights on", + "streetlights:off": "Ability to switch lights off", + "streetlights:dim": "Ability to dim the lights" + } + }, + "clientCredentials": { + "tokenUrl": "https://authserver.example/token", + "availableScopes": { + "streetlights:on": "Ability to switch lights on", + "streetlights:off": "Ability to switch lights off", + "streetlights:dim": "Ability to dim the lights" + } + }, + "authorizationCode": { + "authorizationUrl": "https://authserver.example/auth", + "tokenUrl": "https://authserver.example/token", + "refreshUrl": "https://authserver.example/refresh", + "availableScopes": { + "streetlights:on": "Ability to switch lights on", + "streetlights:off": "Ability to switch lights off", + "streetlights:dim": "Ability to dim the lights" + } + } + } + }, + "openIdConnectWellKnown": { + "type": "openIdConnect", + "openIdConnectUrl": "https://authserver.example/.well-known" + } + }, + "parameters": { + "streetlightId": { + "description": "The ID of the streetlight." + } + }, + "correlationIds": { + "sentAtCorrelator": { + "description": "Data from message payload used as correlation ID", + "location": "$message.payload#/sentAt" + } + } + } +} diff --git a/asyncapi/examples/v3.1/correlation-id.yaml b/asyncapi/examples/v3.1/correlation-id.yaml new file mode 100644 index 0000000..12985c0 --- /dev/null +++ b/asyncapi/examples/v3.1/correlation-id.yaml @@ -0,0 +1,180 @@ +asyncapi: 3.1.0 +info: + title: Correlation ID Example + version: 1.0.0 + description: A cut of the Streetlights API to test Correlation ID + license: + name: Apache 2.0 + url: 'https://www.apache.org/licenses/LICENSE-2.0' +defaultContentType: application/json +servers: + production: + host: 'test.mosquitto.org:{port}' + protocol: mqtt + description: Test broker + variables: + port: + description: Secure connection (TLS) is available through port 8883. + default: '1883' + enum: + - '1883' + - '8883' + security: + - $ref: '#/components/securitySchemes/apiKey' + - type: oauth2 + description: Flows to support OAuth 2.0 + flows: + implicit: + authorizationUrl: 'https://authserver.example/auth' + availableScopes: + 'streetlights:on': Ability to switch lights on + 'streetlights:off': Ability to switch lights off + 'streetlights:dim': Ability to dim the lights + password: + tokenUrl: 'https://authserver.example/token' + availableScopes: + 'streetlights:on': Ability to switch lights on + 'streetlights:off': Ability to switch lights off + 'streetlights:dim': Ability to dim the lights + clientCredentials: + tokenUrl: 'https://authserver.example/token' + availableScopes: + 'streetlights:on': Ability to switch lights on + 'streetlights:off': Ability to switch lights off + 'streetlights:dim': Ability to dim the lights + authorizationCode: + authorizationUrl: 'https://authserver.example/auth' + tokenUrl: 'https://authserver.example/token' + refreshUrl: 'https://authserver.example/refresh' + availableScopes: + 'streetlights:on': Ability to switch lights on + 'streetlights:off': Ability to switch lights off + 'streetlights:dim': Ability to dim the lights + scopes: + - 'streetlights:on' + - 'streetlights:off' + - 'streetlights:dim' + - $ref: '#/components/securitySchemes/openIdConnectWellKnown' +channels: + lightingMeasured: + address: 'smartylighting/streetlights/1/0/event/{streetlightId}/lighting/measured' + messages: + lightMeasured: + $ref: '#/components/messages/lightMeasured' + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + lightsDim: + address: 'smartylighting/streetlights/1/0/action/{streetlightId}/dim' + messages: + dimLight: + $ref: '#/components/messages/dimLight' + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' +operations: + receiveLightMeasurement: + action: receive + channel: + $ref: '#/channels/lightingMeasured' + summary: >- + Inform about environmental lighting conditions of a particular + streetlight. + messages: + - $ref: '#/channels/lightingMeasured/messages/lightMeasured' + dimLight: + action: send + channel: + $ref: '#/channels/lightsDim' + messages: + - $ref: '#/channels/lightsDim/messages/dimLight' +components: + messages: + lightMeasured: + name: lightMeasured + title: Light measured + summary: >- + Inform about environmental lighting conditions of a particular + streetlight. + correlationId: + location: $message.header#/MQMD/CorrelId + contentType: application/json + payload: + $ref: '#/components/schemas/lightMeasuredPayload' + dimLight: + name: dimLight + title: Dim light + summary: Command a particular streetlight to dim the lights. + correlationId: + $ref: '#/components/correlationIds/sentAtCorrelator' + payload: + $ref: '#/components/schemas/dimLightPayload' + schemas: + lightMeasuredPayload: + type: object + properties: + lumens: + type: integer + minimum: 0 + description: Light intensity measured in lumens. + sentAt: + $ref: '#/components/schemas/sentAt' + dimLightPayload: + type: object + properties: + percentage: + type: integer + description: Percentage to which the light should be dimmed to. + minimum: 0 + maximum: 100 + sentAt: + $ref: '#/components/schemas/sentAt' + sentAt: + type: string + format: date-time + description: Date and time when the message was sent. + parameters: + streetlightId: + description: The ID of the streetlight. + correlationIds: + sentAtCorrelator: + description: Data from message payload used as correlation ID + location: $message.payload#/sentAt + securitySchemes: + apiKey: + type: apiKey + in: user + description: Provide your API key as the user and leave the password empty. + supportedOauthFlows: + type: oauth2 + description: Flows to support OAuth 2.0 + flows: + implicit: + authorizationUrl: 'https://authserver.example/auth' + availableScopes: + 'streetlights:on': Ability to switch lights on + 'streetlights:off': Ability to switch lights off + 'streetlights:dim': Ability to dim the lights + password: + tokenUrl: 'https://authserver.example/token' + availableScopes: + 'streetlights:on': Ability to switch lights on + 'streetlights:off': Ability to switch lights off + 'streetlights:dim': Ability to dim the lights + clientCredentials: + tokenUrl: 'https://authserver.example/token' + availableScopes: + 'streetlights:on': Ability to switch lights on + 'streetlights:off': Ability to switch lights off + 'streetlights:dim': Ability to dim the lights + authorizationCode: + authorizationUrl: 'https://authserver.example/auth' + tokenUrl: 'https://authserver.example/token' + refreshUrl: 'https://authserver.example/refresh' + availableScopes: + 'streetlights:on': Ability to switch lights on + 'streetlights:off': Ability to switch lights off + 'streetlights:dim': Ability to dim the lights + openIdConnectWellKnown: + type: openIdConnect + openIdConnectUrl: 'https://authserver.example/.well-known' \ No newline at end of file diff --git a/asyncapi/examples/v3.1/operation-security.json b/asyncapi/examples/v3.1/operation-security.json new file mode 100644 index 0000000..1799b1e --- /dev/null +++ b/asyncapi/examples/v3.1/operation-security.json @@ -0,0 +1,175 @@ +{ + "asyncapi": "3.1.0", + "info": { + "title": "Notifications", + "version": "1.0.0", + "description": "This contract defines HTTP Push notification for application authorization revocation topic" + }, + "channels": { + "authRevoke": { + "address": "AUTHORIZATION_REVOCATION", + "messages": { + "message": { + "$ref": "#/components/messages/message" + } + } + } + }, + "operations": { + "sendAuthRevoke": { + "action": "send", + "channel": { + "$ref": "#/channels/authRevoke" + }, + "security": [ + { + "type": "oauth2", + "description": "The oauth security descriptions", + "flows": { + "clientCredentials": { + "tokenUrl": "https://example.com/api/oauth/dialog", + "availableScopes": { + "subscribe:auth_revocations": "Scope required for authorization revocation topic" + } + } + }, + "scopes": [ + "subscribe:auth_revocations" + ] + } + ], + "bindings": { + "http": { + "method": "POST" + } + }, + "messages": [ + { + "$ref": "#/channels/authRevoke/messages/message" + } + ] + } + }, + "components": { + "schemas": { + "MetaData": { + "type": "object", + "properties": { + "topic": { + "description": "Topic subscribed to.", + "type": "string" + }, + "schemaVersion": { + "description": "The schema for this topic.", + "type": "string" + }, + "deprecated": { + "description": "If this is a deprecated schema or topic.", + "type": "boolean", + "default": "false" + } + } + }, + "Notification": { + "type": "object", + "properties": { + "notificationId": { + "description": "The notification Id.", + "type": "string" + }, + "eventDate": { + "description": "The event date associated with this notification in UTC.", + "type": "string" + }, + "publishDate": { + "description": "The message publish date in UTC.", + "type": "string" + }, + "publishAttemptCount": { + "description": "The number of attempts made to publish this message.", + "type": "integer" + }, + "data": { + "$ref": "#/components/schemas/AuthorizationRevocationData" + } + } + }, + "AuthorizationRevocationData": { + "description": "The Authorization Revocation payload.", + "type": "object", + "properties": { + "username": { + "description": "The username for the user.", + "type": "string" + }, + "userId": { + "description": "The immutable public userId for the user", + "type": "string" + }, + "eiasToken": { + "description": "The legacy eiasToken specific to the user", + "type": "string" + }, + "revokeReason": { + "description": "The reason for authorization revocation", + "type": "string", + "enum": [ + "REVOKED_BY_APP", + "REVOKED_BY_USER", + "REVOKED_BY_ADMIN", + "PASSWORD_CHANGE" + ] + }, + "revocationDate": { + "description": "Date and time when the authorization was revoked", + "type": "string" + } + } + } + }, + "messages": { + "message": { + "headers": { + "type": "object", + "properties": { + "X-SIGNATURE": { + "description": "ECC message signature", + "type": "string" + }, + "Content-Type": { + "type": "string", + "enum": [ + "application/json" + ] + } + } + }, + "payload": { + "type": "object", + "properties": { + "metadata": { + "$ref": "#/components/schemas/MetaData" + }, + "notification": { + "$ref": "#/components/schemas/Notification" + } + } + } + } + }, + "securitySchemes": { + "petstore_auth": { + "type": "oauth2", + "description": "The oauth security descriptions", + "flows": { + "clientCredentials": { + "tokenUrl": "https://example.com/api/oauth/dialog", + "availableScopes": { + "subscribe:auth_revocations": "Scope required for authorization revocation topic" + } + } + } + } + } + } +} diff --git a/asyncapi/examples/v3.1/operation-security.yaml b/asyncapi/examples/v3.1/operation-security.yaml new file mode 100644 index 0000000..766235e --- /dev/null +++ b/asyncapi/examples/v3.1/operation-security.yaml @@ -0,0 +1,117 @@ +asyncapi: 3.1.0 +info: + title: Notifications + version: 1.0.0 + description: >- + This contract defines HTTP Push notification for application authorization + revocation topic +channels: + authRevoke: + address: AUTHORIZATION_REVOCATION + messages: + message: + $ref: '#/components/messages/message' +operations: + sendAuthRevoke: + action: send + channel: + $ref: '#/channels/authRevoke' + security: + - type: oauth2 + description: The oauth security descriptions + flows: + clientCredentials: + tokenUrl: 'https://example.com/api/oauth/dialog' + availableScopes: + 'subscribe:auth_revocations': Scope required for authorization revocation topic + scopes: + - 'subscribe:auth_revocations' + bindings: + http: + method: POST + messages: + - $ref: '#/channels/authRevoke/messages/message' +components: + messages: + message: + headers: + type: object + properties: + X-SIGNATURE: + description: ECC message signature + type: string + Content-Type: + type: string + enum: + - application/json + payload: + type: object + properties: + metadata: + $ref: '#/components/schemas/MetaData' + notification: + $ref: '#/components/schemas/Notification' + schemas: + MetaData: + type: object + properties: + topic: + type: string + description: Topic subscribed to. + schemaVersion: + type: string + description: The schema for this topic. + deprecated: + type: boolean + description: If this is a deprecated schema or topic. + default: 'false' + Notification: + type: object + properties: + notificationId: + type: string + description: The notification Id. + eventDate: + type: string + description: The event date associated with this notification in UTC. + publishDate: + type: string + description: The message publish date in UTC. + publishAttemptCount: + type: integer + description: The number of attempts made to publish this message. + data: + $ref: '#/components/schemas/AuthorizationRevocationData' + AuthorizationRevocationData: + type: object + description: The Authorization Revocation payload. + properties: + username: + type: string + description: The username for the user. + userId: + type: string + description: The immutable public userId for the user + eiasToken: + type: string + description: The legacy eiasToken specific to the user + revokeReason: + type: string + enum: + - REVOKED_BY_APP + - REVOKED_BY_USER + - REVOKED_BY_ADMIN + - PASSWORD_CHANGE + description: The reason for authorization revocation + revocationDate: + type: string + description: Date and time when the authorization was revoked + securitySchemes: + petstore_auth: + type: oauth2 + description: The oauth security descriptions + flows: + clientCredentials: + tokenUrl: 'https://example.com/api/oauth/dialog' + availableScopes: + 'subscribe:auth_revocations': Scope required for authorization revocation topic diff --git a/asyncapi/examples/v3.1/request-reply.json b/asyncapi/examples/v3.1/request-reply.json new file mode 100644 index 0000000..b2d42b0 --- /dev/null +++ b/asyncapi/examples/v3.1/request-reply.json @@ -0,0 +1,353 @@ +{ + "asyncapi": "3.1.0", + "info": { + "title": "Adeo AsyncAPI Case Study", + "version": "%REPLACED_BY_MAVEN%", + "description": "This Adeo specification illustrates how ADEO uses AsyncAPI to document some of their exchanges.", + "contact": { + "name": "AsyncAPI Community", + "email": "case-study@asyncapi.com" + }, + "tags": [ + { + "name": "costing", + "description": "Costing channels, used by Costing clients." + } + ] + }, + "servers": { + "production": { + "host": "prod.url:9092", + "protocol": "kafka", + "description": "Kafka PRODUCTION cluster", + "security": [ + { + "$ref": "#/components/securitySchemes/sasl-ssl" + } + ], + "bindings": { + "kafka": { + "schemaRegistryUrl": "https://schema-registry.prod.url/" + } + } + }, + "staging": { + "host": "staging.url:9092", + "protocol": "kafka", + "description": "Kafka STAGING cluster for `uat` and `preprod` environments", + "security": [ + { + "$ref": "#/components/securitySchemes/sasl-ssl" + } + ], + "bindings": { + "kafka": { + "schemaRegistryUrl": "https://schema-registry.prod.url/" + } + } + }, + "dev": { + "host": "dev.url:9092", + "protocol": "kafka", + "description": "Kafka DEV cluster for `dev` and `sit` environments", + "security": [ + { + "$ref": "#/components/securitySchemes/sasl-ssl" + } + ], + "bindings": { + "kafka": { + "schemaRegistryUrl": "https://schema-registry.prod.url/" + } + } + } + }, + "channels": { + "costingRequestChannel": { + "address": "adeo-{env}-case-study-COSTING-REQUEST-{version}", + "messages": { + "CostingRequest": { + "$ref": "#/components/messages/costingRequestV1" + } + }, + "description": "Use this topic to do a Costing Request to Costing product. We use the [**RecordNameStrategy**](https://docs.confluent.io/platform/current/schema-registry/serdes-develop/index.html#subject-name-strategy) to infer the messages schema. You have to define `value.subject.name.strategy` to `io.confluent.kafka.serializers.subject.RecordNameStrategy` in your producer to use the schema we manage. The schema below illustrates how Costing Request messages are handled. ![](https://user-images.githubusercontent.com/5501911/188920831-689cec5f-8dc3-460b-8794-0b54ec8b0ac8.png)", + "parameters": { + "env": { + "$ref": "#/components/parameters/Env" + }, + "version": { + "$ref": "#/components/parameters/Version" + } + }, + "bindings": { + "kafka": { + "replicas": 3, + "partitions": 3, + "topicConfiguration": { + "cleanup.policy": [ + "delete" + ], + "retention.ms": 60000000 + } + } + } + }, + "costingResponseChannel": { + "messages": { + "costingResponse": { + "$ref": "#/components/messages/costingResponse" + } + }, + "description": "This topic is used to REPLY Costing Requests and is targeted by the `REPLY_TOPIC` header. **You must grant PUBLISH access to our `svc-ccr-app` service account.**. We use the [**RecordNameStrategy**](https://docs.confluent.io/platform/current/schema-registry/serdes-develop/index.html#subject-name-strategy) to infer the messages schema. \nTopic should follow pattern \"adeo-{env}-case-study-COSTING-RESPONSE-{version}\"\nYou have to define `key.subject.name.strategy` and `value.subject.name.strategy` to `io.confluent.kafka.serializers.subject.RecordNameStrategy` in your consumer. The schema below illustrates how Costing Response messages are handled.\n ![](https://user-images.githubusercontent.com/5501911/188920831-689cec5f-8dc3-460b-8794-0b54ec8b0ac8.png)", + "tags": [ + { + "name": "costing" + } + ], + "bindings": { + "kafka": { + "x-key.subject.name.strategy": { + "type": "string", + "description": "We use the RecordNameStrategy to infer the messages schema. Use `key.subject.name.strategy=io.confluent.kafka.serializers.subject.RecordNameStrategy` in your consumer configuration.\n" + }, + "x-value.subject.name.strategy": { + "type": "string", + "description": "We use the RecordNameStrategy to infer the messages schema. Use `value.subject.name.strategy=io.confluent.kafka.serializers.subject.RecordNameStrategy` in your consumer configuration.\n" + } + } + } + } + }, + "operations": { + "receiveACostingRequest": { + "action": "receive", + "channel": { + "$ref": "#/channels/costingRequestChannel" + }, + "summary": "[COSTING] Request one or more Costing calculation for any product\n", + "description": "You can try a costing request using our [Conduktor producer template](https://conduktor.url/)", + "tags": [ + { + "name": "costing" + } + ], + "bindings": { + "kafka": { + "groupId": { + "type": "string", + "description": "The groupId must be prefixed by your `svc` account, deliver by the Adeo Kafka team. This `svc` must have the write access to the topic.\n" + }, + "x-value.subject.name.strategy": { + "type": "string", + "description": "We use the RecordNameStrategy to infer the messages schema. Use `value.subject.name.strategy=io.confluent.kafka.serializers.subject.RecordNameStrategy` in your producer configuration.\n" + } + } + }, + "reply": { + "address": { + "location": "$message.header#/REPLY_TOPIC" + }, + "channel": { + "$ref": "#/channels/costingResponseChannel" + } + } + } + }, + "components": { + "schemas": { + "RequesterId": { + "description": "The Costing requester service account used to produce costing request.", + "type": "string", + "examples": [ + "svc-ecollect-app" + ] + }, + "RequesterCode": { + "description": "The Costing requester code (generally the BU Code). The requester code is useful to get the dedicated context (tenant).", + "type": "string", + "examples": [ + 1 + ] + }, + "MessageId": { + "description": "A unique Message ID.", + "type": "string", + "format": "uuid", + "examples": [ + "1fa6ef40-8f47-40a8-8cf6-f8607d0066ef" + ] + }, + "RequestId": { + "description": "A unique Request ID needed to define a `CORRELATION_ID` for exchanges, which will be sent back in the Costing Responses.", + "type": "string", + "format": "uuid", + "examples": [ + "1fa6ef40-8f47-40a8-8cf6-f8607d0066ef" + ] + }, + "CorrelationId": { + "description": "A unique Correlation ID defined from the `REQUEST_ID` or the `MESSAGE_ID` provided in the Costing Request.", + "type": "string", + "format": "uuid", + "examples": [ + "1fa6ef40-8f47-40a8-8cf6-f8607d0066ef" + ] + }, + "BuCode": { + "description": "The Business Unit code for which data are applicable.", + "type": "string", + "examples": [ + 1 + ] + }, + "ReplyTopic": { + "description": "The Kafka topic where to send the Costing Response. This is required for the [Return Address EIP pattern](https://www.enterpriseintegrationpatterns.com/patterns/messaging/ReturnAddress.html). **You must grant WRITE access to our `svc-ccr-app` service account.**", + "type": "string", + "examples": [ + "adeo-case-study-COSTING-RESPONSE-V1" + ] + }, + "ErrorStep": { + "description": "The woker that has thrown the error.", + "type": "string", + "examples": [ + "EXPOSE_RESULT" + ] + }, + "ErrorMessage": { + "description": "The error message describing the error.", + "type": "string", + "examples": [ + "Error message" + ] + }, + "ErrorCode": { + "description": "The error code.", + "type": "string", + "examples": [ + "CURRENCY_NOT_FOUND" + ] + } + }, + "messages": { + "costingRequestV1": { + "headers": { + "type": "object", + "required": [ + "REQUESTER_ID", + "REQUESTER_CODE", + "REQUEST_ID", + "REPLY_TOPIC" + ], + "properties": { + "REQUEST_ID": { + "$ref": "#/components/schemas/RequestId" + }, + "REPLY_TOPIC": { + "$ref": "#/components/schemas/ReplyTopic" + }, + "REQUESTER_ID": { + "$ref": "#/components/schemas/RequesterId" + }, + "REQUESTER_CODE": { + "$ref": "#/components/schemas/RequesterCode" + } + } + }, + "payload": { + "schemaFormat": "application/vnd.apache.avro;version=1.9.0", + "schema": { + "$ref": "https://www.asyncapi.com/resources/casestudies/adeo/CostingRequestPayload.avsc" + } + }, + "correlationId": { + "$ref": "#/components/correlationIds/costingCorrelationId" + }, + "name": "CostingRequestV1", + "title": "Costing Request V1", + "summary": "Costing Request V1 inputs.", + "tags": [ + { + "name": "costing" + } + ] + }, + "costingResponse": { + "headers": { + "type": "object", + "properties": { + "CALCULATION_ID": { + "$ref": "#/components/schemas/MessageId" + }, + "CORRELATION_ID": { + "$ref": "#/components/schemas/CorrelationId" + }, + "REQUEST_TIMESTAMP": { + "description": "Timestamp of the costing request", + "type": "string", + "format": "date-time" + }, + "CALCULATION_TIMESTAMP": { + "description": "Technical timestamp for the costing calculation", + "type": "string", + "format": "date-time" + } + } + }, + "payload": { + "schemaFormat": "application/vnd.apache.avro;version=1.9.0", + "schema": { + "$ref": "https://www.asyncapi.com/resources/casestudies/adeo/CostingResponsePayload.avsc" + } + }, + "correlationId": { + "$ref": "#/components/correlationIds/costingCorrelationId" + }, + "name": "CostingResponse", + "title": "Costing Response", + "summary": "Costing Response ouputs.", + "description": "Please refer to the `CostingResponseKey.avsc` schema, available on [our github project](https://github.url/).", + "tags": [ + { + "name": "costing" + } + ] + } + }, + "securitySchemes": { + "sasl-ssl": { + "type": "plain", + "description": "Use [SASL authentication with SSL encryption](https://docs.confluent.io/platform/current/security/security_tutorial.html#configure-clients) to connect to the ADEO Broker.", + "x-sasl.jaas.config": "org.apache.kafka.common.security.plain.PlainLoginModule required username=\"\" password=\"\";", + "x-security.protocol": "SASL_SSL", + "x-ssl.endpoint.identification.algorithm": "https", + "x-sasl.mechanism": "PLAIN" + } + }, + "parameters": { + "Env": { + "enum": [ + "dev", + "sit", + "uat1", + "preprod", + "prod" + ], + "description": "Adeo Kafka Environement for messages publications." + }, + "Version": { + "default": "V1", + "description": "the topic version you want to use", + "examples": [ + "V1" + ] + } + }, + "correlationIds": { + "costingCorrelationId": { + "description": "This correlation ID is used for message tracing and messages correlation. This correlation ID is generated at runtime based on the `REQUEST_ID` and sent to the RESPONSE message.", + "location": "$message.header#/REQUEST_ID" + } + } + } +} diff --git a/asyncapi/examples/v3.1/request-reply.yaml b/asyncapi/examples/v3.1/request-reply.yaml new file mode 100644 index 0000000..348075e --- /dev/null +++ b/asyncapi/examples/v3.1/request-reply.yaml @@ -0,0 +1,298 @@ +asyncapi: 3.1.0 +info: + title: Adeo AsyncAPI Case Study + version: '%REPLACED_BY_MAVEN%' + description: > + This Adeo specification illustrates how ADEO uses AsyncAPI to document some + of their exchanges. + contact: + name: AsyncAPI Community + email: case-study@asyncapi.com + tags: + - name: costing + description: Costing channels, used by Costing clients. +servers: + production: + host: prod.url:9092 + protocol: kafka + description: Kafka PRODUCTION cluster + security: + - $ref: '#/components/securitySchemes/sasl-ssl' + bindings: + kafka: + schemaRegistryUrl: https://schema-registry.prod.url/ + staging: + host: staging.url:9092 + protocol: kafka + description: Kafka STAGING cluster for `uat` and `preprod` environments + security: + - $ref: '#/components/securitySchemes/sasl-ssl' + bindings: + kafka: + schemaRegistryUrl: https://schema-registry.prod.url/ + dev: + host: dev.url:9092 + protocol: kafka + description: Kafka DEV cluster for `dev` and `sit` environments + security: + - $ref: '#/components/securitySchemes/sasl-ssl' + bindings: + kafka: + schemaRegistryUrl: https://schema-registry.prod.url/ +channels: + costingRequestChannel: + address: adeo-{env}-case-study-COSTING-REQUEST-{version} + description: > + Use this topic to do a Costing Request to Costing product. We use the + [**RecordNameStrategy**](https://docs.confluent.io/platform/current/schema-registry/serdes-develop/index.html#subject-name-strategy) + to infer the messages schema. You have to define + `value.subject.name.strategy` to + `io.confluent.kafka.serializers.subject.RecordNameStrategy` in your + producer to use the schema we manage. The schema below illustrates how + Costing Request messages are handled. + ![](https://user-images.githubusercontent.com/5501911/188920831-689cec5f-8dc3-460b-8794-0b54ec8b0ac8.png) + parameters: + env: + $ref: '#/components/parameters/Env' + version: + $ref: '#/components/parameters/Version' + bindings: + kafka: + replicas: 3 + partitions: 3 + topicConfiguration: + cleanup.policy: + - delete + retention.ms: 60000000 + messages: + CostingRequest: + $ref: '#/components/messages/costingRequestV1' + costingResponseChannel: + address: null + description: > + This topic is used to REPLY Costing Requests and is targeted by the + `REPLY_TOPIC` header. **You must grant PUBLISH access to our `svc-ccr-app` + service account.**. We use the + [**RecordNameStrategy**](https://docs.confluent.io/platform/current/schema-registry/serdes-develop/index.html#subject-name-strategy) + to infer the messages schema. + + Topic should follow pattern "adeo-{env}-case-study-COSTING-RESPONSE-{version}" + + You have to define + `key.subject.name.strategy` and `value.subject.name.strategy` to + `io.confluent.kafka.serializers.subject.RecordNameStrategy` in your + consumer. The schema below illustrates how Costing Response messages are + handled. + ![](https://user-images.githubusercontent.com/5501911/188920831-689cec5f-8dc3-460b-8794-0b54ec8b0ac8.png) + + bindings: + kafka: + x-key.subject.name.strategy: + type: string + description: > + We use the RecordNameStrategy to infer the messages schema. Use + `key.subject.name.strategy=io.confluent.kafka.serializers.subject.RecordNameStrategy` + in your consumer configuration. + x-value.subject.name.strategy: + type: string + description: > + We use the RecordNameStrategy to infer the messages schema. Use + `value.subject.name.strategy=io.confluent.kafka.serializers.subject.RecordNameStrategy` + in your consumer configuration. + tags: + - name: costing + messages: + costingResponse: + $ref: '#/components/messages/costingResponse' +operations: + receiveACostingRequest: + action: receive + channel: + $ref: '#/channels/costingRequestChannel' + reply: + channel: + $ref: '#/channels/costingResponseChannel' + address: + location: $message.header#/REPLY_TOPIC + summary: | + [COSTING] Request one or more Costing calculation for any product + description: > + You can try a costing request using our [Conduktor producer + template](https://conduktor.url/) + tags: + - name: costing + bindings: + kafka: + groupId: + type: string + description: > + The groupId must be prefixed by your `svc` account, deliver by the + Adeo Kafka team. This `svc` must have the write access to the topic. + x-value.subject.name.strategy: + type: string + description: > + We use the RecordNameStrategy to infer the messages schema. Use + `value.subject.name.strategy=io.confluent.kafka.serializers.subject.RecordNameStrategy` + in your producer configuration. + +components: + correlationIds: + costingCorrelationId: + description: > + This correlation ID is used for message tracing and messages + correlation. This correlation ID is generated at runtime based on the + `REQUEST_ID` and sent to the RESPONSE message. + location: $message.header#/REQUEST_ID + messages: + costingRequestV1: + name: CostingRequestV1 + title: Costing Request V1 + summary: Costing Request V1 inputs. + tags: + - name: costing + correlationId: + $ref: '#/components/correlationIds/costingCorrelationId' + headers: + type: object + required: + - REQUESTER_ID + - REQUESTER_CODE + - REQUEST_ID + - REPLY_TOPIC + properties: + REQUEST_ID: + $ref: '#/components/schemas/RequestId' + REPLY_TOPIC: + $ref: '#/components/schemas/ReplyTopic' + REQUESTER_ID: + $ref: '#/components/schemas/RequesterId' + REQUESTER_CODE: + $ref: '#/components/schemas/RequesterCode' + payload: + schemaFormat: application/vnd.apache.avro;version=1.9.0 + schema: + $ref: https://www.asyncapi.com/resources/casestudies/adeo/CostingRequestPayload.avsc + costingResponse: + name: CostingResponse + title: Costing Response + summary: Costing Response ouputs. + tags: + - name: costing + description: > + Please refer to the `CostingResponseKey.avsc` schema, available on [our + github project](https://github.url/). + correlationId: + $ref: '#/components/correlationIds/costingCorrelationId' + headers: + type: object + properties: + CALCULATION_ID: + $ref: '#/components/schemas/MessageId' + CORRELATION_ID: + $ref: '#/components/schemas/CorrelationId' + REQUEST_TIMESTAMP: + type: string + format: date-time + description: Timestamp of the costing request + CALCULATION_TIMESTAMP: + type: string + format: date-time + description: Technical timestamp for the costing calculation + payload: + schemaFormat: application/vnd.apache.avro;version=1.9.0 + schema: + $ref: https://www.asyncapi.com/resources/casestudies/adeo/CostingResponsePayload.avsc + schemas: + RequesterId: + type: string + description: The Costing requester service account used to produce costing request. + examples: + - svc-ecollect-app + RequesterCode: + type: string + description: >- + The Costing requester code (generally the BU Code). The requester code + is useful to get the dedicated context (tenant). + examples: + - 1 + MessageId: + type: string + format: uuid + description: A unique Message ID. + examples: + - 1fa6ef40-8f47-40a8-8cf6-f8607d0066ef + RequestId: + type: string + format: uuid + description: >- + A unique Request ID needed to define a `CORRELATION_ID` for exchanges, + which will be sent back in the Costing Responses. + examples: + - 1fa6ef40-8f47-40a8-8cf6-f8607d0066ef + CorrelationId: + type: string + format: uuid + description: >- + A unique Correlation ID defined from the `REQUEST_ID` or the + `MESSAGE_ID` provided in the Costing Request. + examples: + - 1fa6ef40-8f47-40a8-8cf6-f8607d0066ef + BuCode: + type: string + description: The Business Unit code for which data are applicable. + examples: + - 1 + ReplyTopic: + type: string + description: > + The Kafka topic where to send the Costing Response. This is required for + the [Return Address EIP + pattern](https://www.enterpriseintegrationpatterns.com/patterns/messaging/ReturnAddress.html). + **You must grant WRITE access to our `svc-ccr-app` service account.** + examples: + - adeo-case-study-COSTING-RESPONSE-V1 + ErrorStep: + type: string + description: | + The woker that has thrown the error. + examples: + - EXPOSE_RESULT + ErrorMessage: + type: string + description: | + The error message describing the error. + examples: + - Error message + ErrorCode: + type: string + description: | + The error code. + examples: + - CURRENCY_NOT_FOUND + parameters: + Env: + description: Adeo Kafka Environement for messages publications. + enum: + - dev + - sit + - uat1 + - preprod + - prod + Version: + description: the topic version you want to use + examples: + - V1 + default: V1 + securitySchemes: + sasl-ssl: + type: plain + x-sasl.jaas.config: >- + org.apache.kafka.common.security.plain.PlainLoginModule required + username="" password=""; + x-security.protocol: SASL_SSL + x-ssl.endpoint.identification.algorithm: https + x-sasl.mechanism: PLAIN + description: > + Use [SASL authentication with SSL + encryption](https://docs.confluent.io/platform/current/security/security_tutorial.html#configure-clients) + to connect to the ADEO Broker. \ No newline at end of file diff --git a/asyncapi/examples/v3.1/rpc-client.json b/asyncapi/examples/v3.1/rpc-client.json new file mode 100644 index 0000000..b8cbb5b --- /dev/null +++ b/asyncapi/examples/v3.1/rpc-client.json @@ -0,0 +1,119 @@ +{ + "asyncapi": "3.1.0", + "id": "urn:example:rpcclient", + "info": { + "title": "RPC Client Example", + "version": "1.0.0", + "description": "This example demonstrates how to define an RPC client." + }, + "servers": { + "production": { + "host": "rabbitmq.example.org", + "protocol": "amqp" + } + }, + "defaultContentType": "application/json", + "channels": { + "queue": { + "address": "{queue}", + "messages": { + "receiveSumResult": { + "payload": { + "type": "object", + "properties": { + "result": { + "type": "number", + "examples": [ + 7 + ] + } + } + }, + "correlationId": { + "location": "$message.header#/correlation_id" + } + } + }, + "parameters": { + "queue": {} + }, + "bindings": { + "amqp": { + "is": "queue", + "queue": { + "exclusive": true + } + } + } + }, + "rpc_queue": { + "address": "rpc_queue", + "messages": { + "requestSum": { + "payload": { + "type": "object", + "properties": { + "numbers": { + "type": "array", + "items": { + "type": "number" + }, + "examples": [ + [ + 4, + 3 + ] + ] + } + } + }, + "correlationId": { + "location": "$message.header#/correlation_id" + } + } + }, + "bindings": { + "amqp": { + "is": "queue", + "queue": { + "durable": false + } + } + } + } + }, + "operations": { + "receiveSumResult": { + "action": "receive", + "channel": { + "$ref": "#/channels/queue" + }, + "bindings": { + "amqp": { + "ack": false + } + }, + "messages": [ + { + "$ref": "#/channels/queue/messages/receiveSumResult" + } + ] + }, + "requestSum": { + "action": "send", + "channel": { + "$ref": "#/channels/rpc_queue" + }, + "bindings": { + "amqp": { + "ack": true + } + }, + "messages": [ + { + "$ref": "#/channels/rpc_queue/messages/requestSum" + } + ] + } + } +} diff --git a/asyncapi/examples/v3.1/rpc-client.yaml b/asyncapi/examples/v3.1/rpc-client.yaml new file mode 100644 index 0000000..62624c9 --- /dev/null +++ b/asyncapi/examples/v3.1/rpc-client.yaml @@ -0,0 +1,72 @@ +asyncapi: 3.1.0 +id: 'urn:example:rpcclient' +info: + title: RPC Client Example + version: 1.0.0 + description: This example demonstrates how to define an RPC client. +defaultContentType: application/json +servers: + production: + host: rabbitmq.example.org + protocol: amqp +channels: + queue: + address: '{queue}' + messages: + receiveSumResult: + correlationId: + location: $message.header#/correlation_id + payload: + type: object + properties: + result: + type: number + examples: + - 7 + parameters: + queue: {} + bindings: + amqp: + is: queue + queue: + exclusive: true + rpc_queue: + address: rpc_queue + messages: + requestSum: + correlationId: + location: $message.header#/correlation_id + payload: + type: object + properties: + numbers: + type: array + items: + type: number + examples: + - - 4 + - 3 + bindings: + amqp: + is: queue + queue: + durable: false +operations: + receiveSumResult: + action: receive + channel: + $ref: '#/channels/queue' + bindings: + amqp: + ack: false + messages: + - $ref: '#/channels/queue/messages/receiveSumResult' + requestSum: + action: send + channel: + $ref: '#/channels/rpc_queue' + bindings: + amqp: + ack: true + messages: + - $ref: '#/channels/rpc_queue/messages/requestSum' diff --git a/asyncapi/examples/v3.1/simple.json b/asyncapi/examples/v3.1/simple.json new file mode 100644 index 0000000..a2eef78 --- /dev/null +++ b/asyncapi/examples/v3.1/simple.json @@ -0,0 +1,51 @@ +{ + "asyncapi": "3.1.0", + "info": { + "title": "Account Service", + "version": "1.0.0", + "description": "This service is in charge of processing user signups" + }, + "channels": { + "userSignedup": { + "address": "user/signedup", + "messages": { + "UserSignedUp": { + "$ref": "#/components/messages/UserSignedUp" + } + } + } + }, + "operations": { + "sendUserSignedup": { + "action": "send", + "channel": { + "$ref": "#/channels/userSignedup" + }, + "messages": [ + { + "$ref": "#/channels/userSignedup/messages/UserSignedUp" + } + ] + } + }, + "components": { + "messages": { + "UserSignedUp": { + "payload": { + "type": "object", + "properties": { + "displayName": { + "description": "Name of the user", + "type": "string" + }, + "email": { + "description": "Email of the user", + "type": "string", + "format": "email" + } + } + } + } + } + } +} diff --git a/asyncapi/examples/v3.1/simple.yaml b/asyncapi/examples/v3.1/simple.yaml new file mode 100644 index 0000000..192274b --- /dev/null +++ b/asyncapi/examples/v3.1/simple.yaml @@ -0,0 +1,31 @@ +asyncapi: 3.1.0 +info: + title: Account Service + version: 1.0.0 + description: This service is in charge of processing user signups +channels: + userSignedup: + address: user/signedup + messages: + UserSignedUp: + $ref: '#/components/messages/UserSignedUp' +operations: + sendUserSignedup: + action: send + channel: + $ref: '#/channels/userSignedup' + messages: + - $ref: '#/channels/userSignedup/messages/UserSignedUp' +components: + messages: + UserSignedUp: + payload: + type: object + properties: + displayName: + type: string + description: Name of the user + email: + type: string + format: email + description: Email of the user \ No newline at end of file diff --git a/asyncapi/examples/v3.1/streetlights-kafka.json b/asyncapi/examples/v3.1/streetlights-kafka.json new file mode 100644 index 0000000..9a8e7df --- /dev/null +++ b/asyncapi/examples/v3.1/streetlights-kafka.json @@ -0,0 +1,322 @@ +{ + "asyncapi": "3.1.0", + "info": { + "title": "Streetlights Kafka API", + "version": "1.0.0", + "description": "The Smartylighting Streetlights API allows you to remotely manage the city lights.\n\n### Check out its awesome features:\n\n* Turn a specific streetlight on/off 🌃\n* Dim a specific streetlight 😎\n* Receive real-time information about environmental lighting conditions 📈", + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0" + } + }, + "servers": { + "scram-connections": { + "host": "test.mykafkacluster.org:18092", + "protocol": "kafka-secure", + "description": "Test broker secured with scramSha256", + "security": [ + { + "$ref": "#/components/securitySchemes/saslScram" + } + ], + "tags": [ + { + "name": "env:test-scram", + "description": "This environment is meant for running internal tests through scramSha256" + }, + { + "name": "kind:remote", + "description": "This server is a remote server. Not exposed by the application" + }, + { + "name": "visibility:private", + "description": "This resource is private and only available to certain users" + } + ] + }, + "mtls-connections": { + "host": "test.mykafkacluster.org:28092", + "protocol": "kafka-secure", + "description": "Test broker secured with X509", + "security": [ + { + "$ref": "#/components/securitySchemes/certs" + } + ], + "tags": [ + { + "name": "env:test-mtls", + "description": "This environment is meant for running internal tests through mtls" + }, + { + "name": "kind:remote", + "description": "This server is a remote server. Not exposed by the application" + }, + { + "name": "visibility:private", + "description": "This resource is private and only available to certain users" + } + ] + } + }, + "defaultContentType": "application/json", + "channels": { + "lightingMeasured": { + "address": "smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured", + "messages": { + "lightMeasured": { + "$ref": "#/components/messages/lightMeasured" + } + }, + "description": "The topic on which measured values may be produced and consumed.", + "parameters": { + "streetlightId": { + "$ref": "#/components/parameters/streetlightId" + } + } + }, + "lightTurnOn": { + "address": "smartylighting.streetlights.1.0.action.{streetlightId}.turn.on", + "messages": { + "turnOn": { + "$ref": "#/components/messages/turnOnOff" + } + }, + "parameters": { + "streetlightId": { + "$ref": "#/components/parameters/streetlightId" + } + } + }, + "lightTurnOff": { + "address": "smartylighting.streetlights.1.0.action.{streetlightId}.turn.off", + "messages": { + "turnOff": { + "$ref": "#/components/messages/turnOnOff" + } + }, + "parameters": { + "streetlightId": { + "$ref": "#/components/parameters/streetlightId" + } + } + }, + "lightsDim": { + "address": "smartylighting.streetlights.1.0.action.{streetlightId}.dim", + "messages": { + "dimLight": { + "$ref": "#/components/messages/dimLight" + } + }, + "parameters": { + "streetlightId": { + "$ref": "#/components/parameters/streetlightId" + } + } + } + }, + "operations": { + "receiveLightMeasurement": { + "action": "receive", + "channel": { + "$ref": "#/channels/lightingMeasured" + }, + "summary": "Inform about environmental lighting conditions of a particular streetlight.", + "traits": [ + { + "$ref": "#/components/operationTraits/kafka" + } + ], + "messages": [ + { + "$ref": "#/channels/lightingMeasured/messages/lightMeasured" + } + ] + }, + "turnOn": { + "action": "send", + "channel": { + "$ref": "#/channels/lightTurnOn" + }, + "traits": [ + { + "$ref": "#/components/operationTraits/kafka" + } + ], + "messages": [ + { + "$ref": "#/channels/lightTurnOn/messages/turnOn" + } + ] + }, + "turnOff": { + "action": "send", + "channel": { + "$ref": "#/channels/lightTurnOff" + }, + "traits": [ + { + "$ref": "#/components/operationTraits/kafka" + } + ], + "messages": [ + { + "$ref": "#/channels/lightTurnOff/messages/turnOff" + } + ] + }, + "dimLight": { + "action": "send", + "channel": { + "$ref": "#/channels/lightsDim" + }, + "traits": [ + { + "$ref": "#/components/operationTraits/kafka" + } + ], + "messages": [ + { + "$ref": "#/channels/lightsDim/messages/dimLight" + } + ] + } + }, + "components": { + "schemas": { + "lightMeasuredPayload": { + "type": "object", + "properties": { + "lumens": { + "description": "Light intensity measured in lumens.", + "type": "integer", + "minimum": 0 + }, + "sentAt": { + "$ref": "#/components/schemas/sentAt" + } + } + }, + "turnOnOffPayload": { + "type": "object", + "properties": { + "command": { + "description": "Whether to turn on or off the light.", + "type": "string", + "enum": [ + "on", + "off" + ] + }, + "sentAt": { + "$ref": "#/components/schemas/sentAt" + } + } + }, + "dimLightPayload": { + "type": "object", + "properties": { + "percentage": { + "description": "Percentage to which the light should be dimmed to.", + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "sentAt": { + "$ref": "#/components/schemas/sentAt" + } + } + }, + "sentAt": { + "description": "Date and time when the message was sent.", + "type": "string", + "format": "date-time" + } + }, + "messages": { + "lightMeasured": { + "payload": { + "$ref": "#/components/schemas/lightMeasuredPayload" + }, + "contentType": "application/json", + "name": "lightMeasured", + "title": "Light measured", + "summary": "Inform about environmental lighting conditions of a particular streetlight.", + "traits": [ + { + "$ref": "#/components/messageTraits/commonHeaders" + } + ] + }, + "turnOnOff": { + "payload": { + "$ref": "#/components/schemas/turnOnOffPayload" + }, + "name": "turnOnOff", + "title": "Turn on/off", + "summary": "Command a particular streetlight to turn the lights on or off.", + "traits": [ + { + "$ref": "#/components/messageTraits/commonHeaders" + } + ] + }, + "dimLight": { + "payload": { + "$ref": "#/components/schemas/dimLightPayload" + }, + "name": "dimLight", + "title": "Dim light", + "summary": "Command a particular streetlight to dim the lights.", + "traits": [ + { + "$ref": "#/components/messageTraits/commonHeaders" + } + ] + } + }, + "securitySchemes": { + "saslScram": { + "type": "scramSha256", + "description": "Provide your username and password for SASL/SCRAM authentication" + }, + "certs": { + "type": "X509", + "description": "Download the certificate files from service provider" + } + }, + "parameters": { + "streetlightId": { + "description": "The ID of the streetlight." + } + }, + "operationTraits": { + "kafka": { + "bindings": { + "kafka": { + "clientId": { + "type": "string", + "enum": [ + "my-app-id" + ] + } + } + } + } + }, + "messageTraits": { + "commonHeaders": { + "headers": { + "type": "object", + "properties": { + "my-app-header": { + "type": "integer", + "minimum": 0, + "maximum": 100 + } + } + } + } + } + } +} diff --git a/asyncapi/examples/v3.1/streetlights-kafka.yaml b/asyncapi/examples/v3.1/streetlights-kafka.yaml new file mode 100644 index 0000000..b52651c --- /dev/null +++ b/asyncapi/examples/v3.1/streetlights-kafka.yaml @@ -0,0 +1,199 @@ +asyncapi: 3.1.0 +info: + title: Streetlights Kafka API + version: 1.0.0 + description: "The Smartylighting Streetlights API allows you to remotely manage the city lights.\n\n### Check out its awesome features:\n\n* Turn a specific streetlight on/off \U0001F303\n* Dim a specific streetlight \U0001F60E\n* Receive real-time information about environmental lighting conditions \U0001F4C8\n" + license: + name: Apache 2.0 + url: 'https://www.apache.org/licenses/LICENSE-2.0' +defaultContentType: application/json +servers: + scram-connections: + host: 'test.mykafkacluster.org:18092' + protocol: kafka-secure + description: Test broker secured with scramSha256 + security: + - $ref: '#/components/securitySchemes/saslScram' + tags: + - name: 'env:test-scram' + description: >- + This environment is meant for running internal tests through + scramSha256 + - name: 'kind:remote' + description: This server is a remote server. Not exposed by the application + - name: 'visibility:private' + description: This resource is private and only available to certain users + mtls-connections: + host: 'test.mykafkacluster.org:28092' + protocol: kafka-secure + description: Test broker secured with X509 + security: + - $ref: '#/components/securitySchemes/certs' + tags: + - name: 'env:test-mtls' + description: This environment is meant for running internal tests through mtls + - name: 'kind:remote' + description: This server is a remote server. Not exposed by the application + - name: 'visibility:private' + description: This resource is private and only available to certain users +channels: + lightingMeasured: + address: 'smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured' + messages: + lightMeasured: + $ref: '#/components/messages/lightMeasured' + description: The topic on which measured values may be produced and consumed. + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + lightTurnOn: + address: 'smartylighting.streetlights.1.0.action.{streetlightId}.turn.on' + messages: + turnOn: + $ref: '#/components/messages/turnOnOff' + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + lightTurnOff: + address: 'smartylighting.streetlights.1.0.action.{streetlightId}.turn.off' + messages: + turnOff: + $ref: '#/components/messages/turnOnOff' + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + lightsDim: + address: 'smartylighting.streetlights.1.0.action.{streetlightId}.dim' + messages: + dimLight: + $ref: '#/components/messages/dimLight' + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' +operations: + receiveLightMeasurement: + action: receive + channel: + $ref: '#/channels/lightingMeasured' + summary: >- + Inform about environmental lighting conditions of a particular + streetlight. + traits: + - $ref: '#/components/operationTraits/kafka' + messages: + - $ref: '#/channels/lightingMeasured/messages/lightMeasured' + turnOn: + action: send + channel: + $ref: '#/channels/lightTurnOn' + traits: + - $ref: '#/components/operationTraits/kafka' + messages: + - $ref: '#/channels/lightTurnOn/messages/turnOn' + turnOff: + action: send + channel: + $ref: '#/channels/lightTurnOff' + traits: + - $ref: '#/components/operationTraits/kafka' + messages: + - $ref: '#/channels/lightTurnOff/messages/turnOff' + dimLight: + action: send + channel: + $ref: '#/channels/lightsDim' + traits: + - $ref: '#/components/operationTraits/kafka' + messages: + - $ref: '#/channels/lightsDim/messages/dimLight' +components: + messages: + lightMeasured: + name: lightMeasured + title: Light measured + summary: >- + Inform about environmental lighting conditions of a particular + streetlight. + contentType: application/json + traits: + - $ref: '#/components/messageTraits/commonHeaders' + payload: + $ref: '#/components/schemas/lightMeasuredPayload' + turnOnOff: + name: turnOnOff + title: Turn on/off + summary: Command a particular streetlight to turn the lights on or off. + traits: + - $ref: '#/components/messageTraits/commonHeaders' + payload: + $ref: '#/components/schemas/turnOnOffPayload' + dimLight: + name: dimLight + title: Dim light + summary: Command a particular streetlight to dim the lights. + traits: + - $ref: '#/components/messageTraits/commonHeaders' + payload: + $ref: '#/components/schemas/dimLightPayload' + schemas: + lightMeasuredPayload: + type: object + properties: + lumens: + type: integer + minimum: 0 + description: Light intensity measured in lumens. + sentAt: + $ref: '#/components/schemas/sentAt' + turnOnOffPayload: + type: object + properties: + command: + type: string + enum: + - 'on' + - 'off' + description: Whether to turn on or off the light. + sentAt: + $ref: '#/components/schemas/sentAt' + dimLightPayload: + type: object + properties: + percentage: + type: integer + description: Percentage to which the light should be dimmed to. + minimum: 0 + maximum: 100 + sentAt: + $ref: '#/components/schemas/sentAt' + sentAt: + type: string + format: date-time + description: Date and time when the message was sent. + securitySchemes: + saslScram: + type: scramSha256 + description: Provide your username and password for SASL/SCRAM authentication + certs: + type: X509 + description: Download the certificate files from service provider + parameters: + streetlightId: + description: The ID of the streetlight. + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + operationTraits: + kafka: + bindings: + kafka: + clientId: + type: string + enum: + - my-app-id diff --git a/asyncapi/examples/v3.1/streetlights-mqtt.json b/asyncapi/examples/v3.1/streetlights-mqtt.json new file mode 100644 index 0000000..bd0eccc --- /dev/null +++ b/asyncapi/examples/v3.1/streetlights-mqtt.json @@ -0,0 +1,392 @@ +{ + "asyncapi": "3.1.0", + "info": { + "title": "Streetlights MQTT API", + "version": "1.0.0", + "description": "The Smartylighting Streetlights API allows you to remotely manage the city lights.\n\n### Check out its awesome features:\n\n* Turn a specific streetlight on/off 🌃\n* Dim a specific streetlight 😎\n* Receive real-time information about environmental lighting conditions 📈", + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0" + } + }, + "servers": { + "production": { + "host": "test.mosquitto.org:{port}", + "protocol": "mqtt", + "description": "Test broker", + "variables": { + "port": { + "enum": [ + "1883", + "8883" + ], + "default": "1883", + "description": "Secure connection (TLS) is available through port 8883." + } + }, + "security": [ + { + "$ref": "#/components/securitySchemes/apiKey" + }, + { + "type": "oauth2", + "description": "Flows to support OAuth 2.0", + "flows": { + "implicit": { + "authorizationUrl": "https://authserver.example/auth", + "availableScopes": { + "streetlights:on": "Ability to switch lights on", + "streetlights:off": "Ability to switch lights off", + "streetlights:dim": "Ability to dim the lights" + } + }, + "password": { + "tokenUrl": "https://authserver.example/token", + "availableScopes": { + "streetlights:on": "Ability to switch lights on", + "streetlights:off": "Ability to switch lights off", + "streetlights:dim": "Ability to dim the lights" + } + }, + "clientCredentials": { + "tokenUrl": "https://authserver.example/token", + "availableScopes": { + "streetlights:on": "Ability to switch lights on", + "streetlights:off": "Ability to switch lights off", + "streetlights:dim": "Ability to dim the lights" + } + }, + "authorizationCode": { + "authorizationUrl": "https://authserver.example/auth", + "tokenUrl": "https://authserver.example/token", + "refreshUrl": "https://authserver.example/refresh", + "availableScopes": { + "streetlights:on": "Ability to switch lights on", + "streetlights:off": "Ability to switch lights off", + "streetlights:dim": "Ability to dim the lights" + } + } + }, + "scopes": [ + "streetlights:on", + "streetlights:off", + "streetlights:dim" + ] + }, + { + "$ref": "#/components/securitySchemes/openIdConnectWellKnown" + } + ], + "tags": [ + { + "name": "env:production", + "description": "This environment is meant for production use case" + }, + { + "name": "kind:remote", + "description": "This server is a remote server. Not exposed by the application" + }, + { + "name": "visibility:public", + "description": "This resource is public and available to everyone" + } + ] + } + }, + "defaultContentType": "application/json", + "channels": { + "lightingMeasured": { + "address": "smartylighting/streetlights/1/0/event/{streetlightId}/lighting/measured", + "messages": { + "lightMeasured": { + "$ref": "#/components/messages/lightMeasured" + } + }, + "description": "The topic on which measured values may be produced and consumed.", + "parameters": { + "streetlightId": { + "$ref": "#/components/parameters/streetlightId" + } + } + }, + "lightTurnOn": { + "address": "smartylighting/streetlights/1/0/action/{streetlightId}/turn/on", + "messages": { + "turnOn": { + "$ref": "#/components/messages/turnOnOff" + } + }, + "parameters": { + "streetlightId": { + "$ref": "#/components/parameters/streetlightId" + } + } + }, + "lightTurnOff": { + "address": "smartylighting/streetlights/1/0/action/{streetlightId}/turn/off", + "messages": { + "turnOff": { + "$ref": "#/components/messages/turnOnOff" + } + }, + "parameters": { + "streetlightId": { + "$ref": "#/components/parameters/streetlightId" + } + } + }, + "lightsDim": { + "address": "smartylighting/streetlights/1/0/action/{streetlightId}/dim", + "messages": { + "dimLight": { + "$ref": "#/components/messages/dimLight" + } + }, + "parameters": { + "streetlightId": { + "$ref": "#/components/parameters/streetlightId" + } + } + } + }, + "operations": { + "receiveLightMeasurement": { + "action": "receive", + "channel": { + "$ref": "#/channels/lightingMeasured" + }, + "summary": "Inform about environmental lighting conditions of a particular streetlight.", + "traits": [ + { + "$ref": "#/components/operationTraits/mqtt" + } + ], + "messages": [ + { + "$ref": "#/channels/lightingMeasured/messages/lightMeasured" + } + ] + }, + "turnOn": { + "action": "send", + "channel": { + "$ref": "#/channels/lightTurnOn" + }, + "traits": [ + { + "$ref": "#/components/operationTraits/mqtt" + } + ], + "messages": [ + { + "$ref": "#/channels/lightTurnOn/messages/turnOn" + } + ] + }, + "turnOff": { + "action": "send", + "channel": { + "$ref": "#/channels/lightTurnOff" + }, + "traits": [ + { + "$ref": "#/components/operationTraits/mqtt" + } + ], + "messages": [ + { + "$ref": "#/channels/lightTurnOff/messages/turnOff" + } + ] + }, + "dimLight": { + "action": "send", + "channel": { + "$ref": "#/channels/lightsDim" + }, + "traits": [ + { + "$ref": "#/components/operationTraits/mqtt" + } + ], + "messages": [ + { + "$ref": "#/channels/lightsDim/messages/dimLight" + } + ] + } + }, + "components": { + "schemas": { + "lightMeasuredPayload": { + "type": "object", + "properties": { + "lumens": { + "description": "Light intensity measured in lumens.", + "type": "integer", + "minimum": 0 + }, + "sentAt": { + "$ref": "#/components/schemas/sentAt" + } + } + }, + "turnOnOffPayload": { + "type": "object", + "properties": { + "command": { + "description": "Whether to turn on or off the light.", + "type": "string", + "enum": [ + "on", + "off" + ] + }, + "sentAt": { + "$ref": "#/components/schemas/sentAt" + } + } + }, + "dimLightPayload": { + "type": "object", + "properties": { + "percentage": { + "description": "Percentage to which the light should be dimmed to.", + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "sentAt": { + "$ref": "#/components/schemas/sentAt" + } + } + }, + "sentAt": { + "description": "Date and time when the message was sent.", + "type": "string", + "format": "date-time" + } + }, + "messages": { + "lightMeasured": { + "payload": { + "$ref": "#/components/schemas/lightMeasuredPayload" + }, + "contentType": "application/json", + "name": "lightMeasured", + "title": "Light measured", + "summary": "Inform about environmental lighting conditions of a particular streetlight.", + "traits": [ + { + "$ref": "#/components/messageTraits/commonHeaders" + } + ] + }, + "turnOnOff": { + "payload": { + "$ref": "#/components/schemas/turnOnOffPayload" + }, + "name": "turnOnOff", + "title": "Turn on/off", + "summary": "Command a particular streetlight to turn the lights on or off.", + "traits": [ + { + "$ref": "#/components/messageTraits/commonHeaders" + } + ] + }, + "dimLight": { + "payload": { + "$ref": "#/components/schemas/dimLightPayload" + }, + "name": "dimLight", + "title": "Dim light", + "summary": "Command a particular streetlight to dim the lights.", + "traits": [ + { + "$ref": "#/components/messageTraits/commonHeaders" + } + ] + } + }, + "securitySchemes": { + "apiKey": { + "type": "apiKey", + "description": "Provide your API key as the user and leave the password empty.", + "in": "user" + }, + "supportedOauthFlows": { + "type": "oauth2", + "description": "Flows to support OAuth 2.0", + "flows": { + "implicit": { + "authorizationUrl": "https://authserver.example/auth", + "availableScopes": { + "streetlights:on": "Ability to switch lights on", + "streetlights:off": "Ability to switch lights off", + "streetlights:dim": "Ability to dim the lights" + } + }, + "password": { + "tokenUrl": "https://authserver.example/token", + "availableScopes": { + "streetlights:on": "Ability to switch lights on", + "streetlights:off": "Ability to switch lights off", + "streetlights:dim": "Ability to dim the lights" + } + }, + "clientCredentials": { + "tokenUrl": "https://authserver.example/token", + "availableScopes": { + "streetlights:on": "Ability to switch lights on", + "streetlights:off": "Ability to switch lights off", + "streetlights:dim": "Ability to dim the lights" + } + }, + "authorizationCode": { + "authorizationUrl": "https://authserver.example/auth", + "tokenUrl": "https://authserver.example/token", + "refreshUrl": "https://authserver.example/refresh", + "availableScopes": { + "streetlights:on": "Ability to switch lights on", + "streetlights:off": "Ability to switch lights off", + "streetlights:dim": "Ability to dim the lights" + } + } + } + }, + "openIdConnectWellKnown": { + "type": "openIdConnect", + "openIdConnectUrl": "https://authserver.example/.well-known" + } + }, + "parameters": { + "streetlightId": { + "description": "The ID of the streetlight." + } + }, + "operationTraits": { + "mqtt": { + "bindings": { + "mqtt": { + "qos": 1 + } + } + } + }, + "messageTraits": { + "commonHeaders": { + "headers": { + "type": "object", + "properties": { + "my-app-header": { + "type": "integer", + "minimum": 0, + "maximum": 100 + } + } + } + } + } + } +} diff --git a/asyncapi/examples/v3.1/streetlights-mqtt.yaml b/asyncapi/examples/v3.1/streetlights-mqtt.yaml new file mode 100644 index 0000000..88755b4 --- /dev/null +++ b/asyncapi/examples/v3.1/streetlights-mqtt.yaml @@ -0,0 +1,253 @@ +asyncapi: 3.1.0 +info: + title: Streetlights MQTT API + version: 1.0.0 + description: "The Smartylighting Streetlights API allows you to remotely manage the city lights.\n\n### Check out its awesome features:\n\n* Turn a specific streetlight on/off \U0001F303\n* Dim a specific streetlight \U0001F60E\n* Receive real-time information about environmental lighting conditions \U0001F4C8\n" + license: + name: Apache 2.0 + url: 'https://www.apache.org/licenses/LICENSE-2.0' +defaultContentType: application/json +servers: + production: + host: 'test.mosquitto.org:{port}' + protocol: mqtt + description: Test broker + variables: + port: + description: Secure connection (TLS) is available through port 8883. + default: '1883' + enum: + - '1883' + - '8883' + security: + - $ref: '#/components/securitySchemes/apiKey' + - type: oauth2 + description: Flows to support OAuth 2.0 + flows: + implicit: + authorizationUrl: 'https://authserver.example/auth' + availableScopes: + 'streetlights:on': Ability to switch lights on + 'streetlights:off': Ability to switch lights off + 'streetlights:dim': Ability to dim the lights + password: + tokenUrl: 'https://authserver.example/token' + availableScopes: + 'streetlights:on': Ability to switch lights on + 'streetlights:off': Ability to switch lights off + 'streetlights:dim': Ability to dim the lights + clientCredentials: + tokenUrl: 'https://authserver.example/token' + availableScopes: + 'streetlights:on': Ability to switch lights on + 'streetlights:off': Ability to switch lights off + 'streetlights:dim': Ability to dim the lights + authorizationCode: + authorizationUrl: 'https://authserver.example/auth' + tokenUrl: 'https://authserver.example/token' + refreshUrl: 'https://authserver.example/refresh' + availableScopes: + 'streetlights:on': Ability to switch lights on + 'streetlights:off': Ability to switch lights off + 'streetlights:dim': Ability to dim the lights + scopes: + - 'streetlights:on' + - 'streetlights:off' + - 'streetlights:dim' + - $ref: '#/components/securitySchemes/openIdConnectWellKnown' + tags: + - name: 'env:production' + description: This environment is meant for production use case + - name: 'kind:remote' + description: This server is a remote server. Not exposed by the application + - name: 'visibility:public' + description: This resource is public and available to everyone +channels: + lightingMeasured: + address: 'smartylighting/streetlights/1/0/event/{streetlightId}/lighting/measured' + messages: + lightMeasured: + $ref: '#/components/messages/lightMeasured' + description: The topic on which measured values may be produced and consumed. + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + lightTurnOn: + address: 'smartylighting/streetlights/1/0/action/{streetlightId}/turn/on' + messages: + turnOn: + $ref: '#/components/messages/turnOnOff' + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + lightTurnOff: + address: 'smartylighting/streetlights/1/0/action/{streetlightId}/turn/off' + messages: + turnOff: + $ref: '#/components/messages/turnOnOff' + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + lightsDim: + address: 'smartylighting/streetlights/1/0/action/{streetlightId}/dim' + messages: + dimLight: + $ref: '#/components/messages/dimLight' + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' +operations: + receiveLightMeasurement: + action: receive + channel: + $ref: '#/channels/lightingMeasured' + summary: >- + Inform about environmental lighting conditions of a particular + streetlight. + traits: + - $ref: '#/components/operationTraits/mqtt' + messages: + - $ref: '#/channels/lightingMeasured/messages/lightMeasured' + turnOn: + action: send + channel: + $ref: '#/channels/lightTurnOn' + traits: + - $ref: '#/components/operationTraits/mqtt' + messages: + - $ref: '#/channels/lightTurnOn/messages/turnOn' + turnOff: + action: send + channel: + $ref: '#/channels/lightTurnOff' + traits: + - $ref: '#/components/operationTraits/mqtt' + messages: + - $ref: '#/channels/lightTurnOff/messages/turnOff' + dimLight: + action: send + channel: + $ref: '#/channels/lightsDim' + traits: + - $ref: '#/components/operationTraits/mqtt' + messages: + - $ref: '#/channels/lightsDim/messages/dimLight' +components: + messages: + lightMeasured: + name: lightMeasured + title: Light measured + summary: >- + Inform about environmental lighting conditions of a particular + streetlight. + contentType: application/json + traits: + - $ref: '#/components/messageTraits/commonHeaders' + payload: + $ref: '#/components/schemas/lightMeasuredPayload' + turnOnOff: + name: turnOnOff + title: Turn on/off + summary: Command a particular streetlight to turn the lights on or off. + traits: + - $ref: '#/components/messageTraits/commonHeaders' + payload: + $ref: '#/components/schemas/turnOnOffPayload' + dimLight: + name: dimLight + title: Dim light + summary: Command a particular streetlight to dim the lights. + traits: + - $ref: '#/components/messageTraits/commonHeaders' + payload: + $ref: '#/components/schemas/dimLightPayload' + schemas: + lightMeasuredPayload: + type: object + properties: + lumens: + type: integer + minimum: 0 + description: Light intensity measured in lumens. + sentAt: + $ref: '#/components/schemas/sentAt' + turnOnOffPayload: + type: object + properties: + command: + type: string + enum: + - 'on' + - 'off' + description: Whether to turn on or off the light. + sentAt: + $ref: '#/components/schemas/sentAt' + dimLightPayload: + type: object + properties: + percentage: + type: integer + description: Percentage to which the light should be dimmed to. + minimum: 0 + maximum: 100 + sentAt: + $ref: '#/components/schemas/sentAt' + sentAt: + type: string + format: date-time + description: Date and time when the message was sent. + securitySchemes: + apiKey: + type: apiKey + in: user + description: Provide your API key as the user and leave the password empty. + supportedOauthFlows: + type: oauth2 + description: Flows to support OAuth 2.0 + flows: + implicit: + authorizationUrl: 'https://authserver.example/auth' + availableScopes: + 'streetlights:on': Ability to switch lights on + 'streetlights:off': Ability to switch lights off + 'streetlights:dim': Ability to dim the lights + password: + tokenUrl: 'https://authserver.example/token' + availableScopes: + 'streetlights:on': Ability to switch lights on + 'streetlights:off': Ability to switch lights off + 'streetlights:dim': Ability to dim the lights + clientCredentials: + tokenUrl: 'https://authserver.example/token' + availableScopes: + 'streetlights:on': Ability to switch lights on + 'streetlights:off': Ability to switch lights off + 'streetlights:dim': Ability to dim the lights + authorizationCode: + authorizationUrl: 'https://authserver.example/auth' + tokenUrl: 'https://authserver.example/token' + refreshUrl: 'https://authserver.example/refresh' + availableScopes: + 'streetlights:on': Ability to switch lights on + 'streetlights:off': Ability to switch lights off + 'streetlights:dim': Ability to dim the lights + openIdConnectWellKnown: + type: openIdConnect + openIdConnectUrl: 'https://authserver.example/.well-known' + parameters: + streetlightId: + description: The ID of the streetlight. + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + operationTraits: + mqtt: + bindings: + mqtt: + qos: 1 diff --git a/asyncapi/extensions.go b/asyncapi/extensions.go new file mode 100644 index 0000000..3b54232 --- /dev/null +++ b/asyncapi/extensions.go @@ -0,0 +1,52 @@ +package asyncapi + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "errors" + "strings" + + "github.com/MarkRosemaker/errpath" +) + +// Extensions represents additional fields that can be added to AsyncAPI objects. +// +// While the AsyncAPI Specification tries to accommodate most use cases, additional data can be added to extend the specification at certain points. +// +// The extensions properties are implemented as patterned fields that are always prefixed by `x-`, for example, x-internal-id. The value can be null, a primitive, an array or an object. +// ([Specification]) +// +// It is here an alias of jsontext.Value to allow inlining within structs, enabling +// seamless marshalling and unmarshalling. Using jsontext.Value preserves the order +// of fields, preventing unnecessary changes when parsing and writing AsyncAPI +// specifications. Although a map could be used, it doesn't maintain the order, +// leading to potential inconsistencies in the output. Custom marshalling for an +// inlined object is not possible, which prevents the use of an ordered map. +// +// Note: For convenience, certain common extensions are implemented as fields +// directly within the respective structs. +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#specificationExtensions +type Extensions = jsontext.Value + +// ErrUnknownField is returned when a field is not recognized and also doesn't have a "x-" prefix signifying it is an extension. +var ErrUnknownField = errors.New(`unknown field or extension without "x-" prefix`) + +func validateExtensions(ext Extensions) error { + if len(ext) == 0 { + return nil + } + + m := map[string]any{} + if err := json.Unmarshal(ext, &m); err != nil { + return err + } + + for k := range m { + if !strings.HasPrefix(k, "x-") { + return &errpath.ErrField{Field: k, Err: ErrUnknownField} + } + } + + return nil +} diff --git a/asyncapi/extensions_test.go b/asyncapi/extensions_test.go new file mode 100644 index 0000000..8709b65 --- /dev/null +++ b/asyncapi/extensions_test.go @@ -0,0 +1,65 @@ +package asyncapi_test + +import ( + "testing" + + "github.com/MarkRosemaker/asyncapi" +) + +func TestExtensions(t *testing.T) { + t.Parallel() + + data := []byte(`{ + "asyncapi": "3.1.0", + "info": { + "title": "Account Service", + "version": "1.0.0", + "x-twitter": "@asyncapispec" + }, + "channels": { + "userSignedup": { + "address": "user/signedup" + } + }, + "x-linkedin": "async-api" +}`) + + doc, err := asyncapi.LoadFromDataJSON(data) + if err != nil { + t.Fatal(err) + } + + if err := doc.Validate(); err != nil { + t.Fatal(err) + } + + if got, want := string(doc.Info.Extensions), `{"x-twitter":"@asyncapispec"}`; got != want { + t.Fatalf("got: %s, want: %s", got, want) + } + + // the extensions are written back where they were + got, err := doc.ToJSON() + if err != nil { + t.Fatal(err) + } + + if string(got) != string(data) { + t.Fatalf("got:\n%s\nwant:\n%s", got, data) + } +} + +func TestExtensions_Errors(t *testing.T) { + t.Parallel() + + doc := minimalDocument() + doc.Info.Extensions = []byte(`{"twitter":"@asyncapispec"}`) + + err := doc.Validate() + if err == nil { + t.Fatal("expected error") + } + + if want := "info.twitter: " + asyncapi.ErrUnknownField.Error(); err.Error() != want { + t.Fatalf("got: %v, want: %v", err, want) + } +} diff --git a/asyncapi/external_docs.go b/asyncapi/external_docs.go new file mode 100644 index 0000000..4345bbd --- /dev/null +++ b/asyncapi/external_docs.go @@ -0,0 +1,44 @@ +package asyncapi + +import ( + "net/url" + "strings" + + "github.com/MarkRosemaker/errpath" +) + +// ExternalDocs allows referencing an external resource for extended documentation. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#externalDocumentationObject +type ExternalDocs struct { + // A short description of the target documentation. CommonMark syntax can be used for rich text representation. + Description string `json:"description,omitempty" yaml:"description,omitempty"` + // REQUIRED. The URL for the target documentation. This MUST be in the form of an absolute URL. + URL *url.URL `json:"url,omitempty" yaml:"url,omitempty"` + // This object MAY be extended with Specification Extensions. + Extensions Extensions `json:",inline" yaml:",inline"` +} + +// Validate checks the external documentation for consistency. +func (ed *ExternalDocs) Validate() error { + if ed.URL == nil { + return &errpath.ErrField{Field: "url", Err: &errpath.ErrRequired{}} + } + + if err := validateURL(ed.URL); err != nil { + return &errpath.ErrField{Field: "url", Err: err} + } + + ed.Description = strings.TrimSpace(ed.Description) + + return validateExtensions(ed.Extensions) +} + +func (l *loader) collectExternalDocsRef(d *ExternalDocsRef, ref ref) { + collectRef(l, d, l.externalDocs, ref) +} + +func (l *loader) resolveExternalDocsRef(d *ExternalDocsRef) error { + return resolveRef(d, l.externalDocs, nil) +} diff --git a/asyncapi/external_docs_by_name.go b/asyncapi/external_docs_by_name.go new file mode 100644 index 0000000..0bed507 --- /dev/null +++ b/asyncapi/external_docs_by_name.go @@ -0,0 +1,72 @@ +package asyncapi + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "iter" + + "github.com/MarkRosemaker/errpath" + "github.com/MarkRosemaker/ordmap" +) + +// ExternalDocsByName is a map of External Documentation Objects. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#componentsExternalDocs +type ExternalDocsByName map[string]*ExternalDocsRef + +// Validate validates each external documentation object. +func (ds ExternalDocsByName) Validate() error { + for name, d := range ds.ByIndex() { + if err := d.Validate(); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} + +// ByIndex returns a sequence of key-value pairs ordered by index. +func (ds ExternalDocsByName) ByIndex() iter.Seq2[string, *ExternalDocsRef] { + return ordmap.ByIndex(ds, getIndexRef[ExternalDocs, *ExternalDocs]) +} + +// Sort sorts the map by key and sets the indices accordingly. +func (ds ExternalDocsByName) Sort() { + ordmap.Sort(ds, setIndexRef[ExternalDocs, *ExternalDocs]) +} + +// Set sets a value in the map, adding it at the end of the order. +func (ds *ExternalDocsByName) Set(key string, d *ExternalDocsRef) { + ordmap.Set(ds, key, d, getIndexRef[ExternalDocs, *ExternalDocs], setIndexRef[ExternalDocs, *ExternalDocs]) +} + +var _ json.MarshalerTo = (*ExternalDocsByName)(nil) + +// MarshalJSONTo marshals the key-value pairs in order. +func (ds *ExternalDocsByName) MarshalJSONTo(enc *jsontext.Encoder) error { + return ordmap.MarshalJSONTo(ds, enc) +} + +var _ json.UnmarshalerFrom = (*ExternalDocsByName)(nil) + +// UnmarshalJSONFrom unmarshals the key-value pairs in order and sets the indices. +func (ds *ExternalDocsByName) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + return ordmap.UnmarshalJSONFrom(ds, dec, setIndexRef[ExternalDocs, *ExternalDocs]) +} + +func (l *loader) collectExternalDocsByName(ds ExternalDocsByName, ref ref) { + for name, d := range ds.ByIndex() { + l.collectExternalDocsRef(d, append(ref, name)) + } +} + +func (l *loader) resolveExternalDocsByName(ds ExternalDocsByName) error { + for name, d := range ds.ByIndex() { + if err := l.resolveExternalDocsRef(d); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} diff --git a/asyncapi/full_test.go b/asyncapi/full_test.go new file mode 100644 index 0000000..4ffb416 --- /dev/null +++ b/asyncapi/full_test.go @@ -0,0 +1,461 @@ +package asyncapi_test + +import ( + "testing" + + "github.com/MarkRosemaker/asyncapi" +) + +// fullDocument is a document that makes use of as many fields as possible. +const fullDocument = `{ + "asyncapi": "3.1.0", + "id": "urn:example:com:smartylighting:streetlights:server", + "info": { + "title": "Account Service", + "version": "1.0.0", + "description": "This service is in charge of processing user signups.", + "termsOfService": "https://example.com/terms", + "contact": { + "name": "API Support", + "url": "https://www.example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0" + }, + "tags": [ + { + "name": "user", + "description": "User-related messages", + "externalDocs": { + "description": "Find more info here", + "url": "https://example.com/docs/user" + } + } + ], + "externalDocs": { + "url": "https://example.com/docs" + } + }, + "servers": { + "production": { + "host": "{env}.example.org:{port}", + "protocol": "kafka-secure", + "protocolVersion": "3.5.0", + "pathname": "/events", + "description": "The production broker.", + "title": "Production", + "summary": "Production broker", + "variables": { + "env": { + "enum": [ + "prod", + "staging" + ], + "default": "prod", + "description": "The environment.", + "examples": [ + "prod" + ] + }, + "port": { + "default": "9092" + } + }, + "security": [ + { + "type": "oauth2", + "description": "Sign in with your account.", + "flows": { + "implicit": { + "authorizationUrl": "https://example.com/oauth/authorize", + "refreshUrl": "https://example.com/oauth/refresh", + "availableScopes": { + "user:read": "Read user data" + } + }, + "password": { + "tokenUrl": "https://example.com/oauth/token", + "availableScopes": {} + }, + "clientCredentials": { + "tokenUrl": "https://example.com/oauth/token", + "availableScopes": {} + }, + "authorizationCode": { + "authorizationUrl": "https://example.com/oauth/authorize", + "tokenUrl": "https://example.com/oauth/token", + "refreshUrl": "https://example.com/oauth/refresh", + "availableScopes": {} + } + }, + "scopes": [ + "user:read" + ] + } + ], + "tags": [ + { + "name": "env:production" + } + ], + "externalDocs": { + "url": "https://example.com/docs/production" + }, + "bindings": { + "kafka": { + "schemaRegistryUrl": "https://schema-registry.example.com/" + } + } + } + }, + "defaultContentType": "application/json", + "channels": { + "userSignedup": { + "address": "user/{userId}/signedup", + "messages": { + "userSignedUp": { + "headers": { + "type": "object", + "properties": { + "correlationId": { + "type": "string" + } + } + }, + "payload": { + "$ref": "#/components/schemas/userSignedUpPayload" + }, + "correlationId": { + "location": "$message.header#/correlationId" + }, + "contentType": "application/json", + "name": "userSignedUp", + "title": "User signed up", + "summary": "A user signed up.", + "description": "This message is sent when a user signs up.", + "tags": [ + { + "name": "signup" + } + ], + "externalDocs": { + "url": "https://example.com/docs/user-signed-up" + }, + "bindings": { + "kafka": { + "key": { + "type": "string" + } + } + }, + "examples": [ + { + "headers": { + "correlationId": "my-correlation-id" + }, + "payload": { + "displayName": "Lucas" + }, + "name": "SimpleSignup", + "summary": "A simple example" + } + ], + "traits": [ + { + "$ref": "#/components/messageTraits/commonHeaders" + } + ] + } + }, + "title": "User signed up", + "summary": "The channel a user signup is announced on.", + "description": "This channel is used to announce user signups.", + "servers": [ + { + "$ref": "#/servers/production" + } + ], + "parameters": { + "userId": { + "enum": [ + "1", + "2" + ], + "default": "1", + "description": "Id of the user.", + "examples": [ + "1" + ], + "location": "$message.payload#/user/id" + } + }, + "tags": [ + { + "name": "user" + } + ], + "externalDocs": { + "url": "https://example.com/docs/user-signedup" + }, + "bindings": { + "kafka": { + "topic": "user-signedup" + } + } + } + }, + "operations": { + "sendUserSignedup": { + "action": "send", + "channel": { + "$ref": "#/channels/userSignedup" + }, + "title": "Send a user signup", + "summary": "Announce that a user signed up.", + "description": "This operation announces that a user signed up.", + "security": [ + { + "type": "userPassword" + } + ], + "tags": [ + { + "name": "user" + } + ], + "externalDocs": { + "url": "https://example.com/docs/send-user-signedup" + }, + "bindings": { + "kafka": { + "groupId": { + "type": "string" + } + } + }, + "traits": [ + { + "title": "Kafka operation", + "summary": "An operation on Kafka.", + "description": "This trait is applied to all Kafka operations.", + "security": [ + { + "type": "userPassword" + } + ], + "tags": [ + { + "name": "kafka" + } + ], + "externalDocs": { + "url": "https://example.com/docs/kafka" + }, + "bindings": { + "kafka": { + "clientId": "my-app-id" + } + } + } + ], + "messages": [ + { + "$ref": "#/channels/userSignedup/messages/userSignedUp" + } + ] + } + }, + "components": { + "schemas": { + "userSignedUpPayload": { + "$id": "https://example.com/schemas/userSignedUpPayload", + "$schema": "http://json-schema.org/draft-07/schema", + "$comment": "The payload of a user signup.", + "title": "User signup payload", + "description": "The payload of a user signup.", + "type": "object", + "allOf": [ + { + "type": "object" + } + ], + "oneOf": [ + { + "required": [ + "displayName" + ] + } + ], + "anyOf": [ + { + "required": [ + "email" + ] + } + ], + "not": { + "type": "null" + }, + "if": { + "required": [ + "email" + ] + }, + "then": { + "required": [ + "displayName" + ] + }, + "else": true, + "minProperties": 1, + "maxProperties": 10, + "required": [ + "displayName" + ], + "properties": { + "displayName": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "pattern": "^[a-zA-Z ]+$", + "examples": [ + "Lucas" + ] + }, + "email": { + "type": "string", + "format": "email" + }, + "age": { + "type": "integer", + "multipleOf": 1, + "minimum": 0, + "exclusiveMinimum": -1, + "maximum": 200, + "exclusiveMaximum": 201, + "default": 0 + }, + "roles": { + "type": "array", + "minItems": 1, + "maxItems": 10, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "admin", + "user" + ] + }, + "additionalItems": false, + "contains": { + "const": "user" + } + }, + "avatar": { + "type": "string", + "contentEncoding": "base64", + "contentMediaType": "image/png" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "password": { + "type": "string", + "format": "password", + "writeOnly": true + }, + "legacyId": { + "type": [ + "string", + "null" + ], + "deprecated": true + } + }, + "patternProperties": { + "^x-": { + "type": "string" + } + }, + "additionalProperties": false, + "propertyNames": { + "pattern": "^[a-zA-Z]+$" + }, + "definitions": { + "empty": {} + }, + "externalDocs": { + "url": "https://example.com/docs/user-signed-up-payload" + } + } + }, + "messageTraits": { + "commonHeaders": { + "headers": { + "type": "object", + "properties": { + "my-app-header": { + "type": "integer" + } + } + }, + "correlationId": { + "location": "$message.header#/correlationId" + }, + "contentType": "application/json", + "name": "commonHeaders", + "title": "Common headers", + "summary": "The headers all messages have in common.", + "description": "These headers are applied to all messages.", + "tags": [ + { + "name": "headers" + } + ], + "externalDocs": { + "url": "https://example.com/docs/common-headers" + }, + "bindings": { + "kafka": { + "bindingVersion": "0.5.0" + } + }, + "examples": [ + { + "headers": { + "my-app-header": 12 + } + } + ] + } + } + } +}` + +func TestFullDocument(t *testing.T) { + t.Parallel() + + doc, err := asyncapi.LoadFromDataJSON([]byte(fullDocument)) + if err != nil { + t.Fatal(err) + } + + if err := doc.Validate(); err != nil { + t.Fatal(err) + } + + // the document is written back as it was read + got, err := doc.ToJSON() + if err != nil { + t.Fatal(err) + } + + if string(got) != fullDocument { + t.Fatalf("got:\n%s\nwant:\n%s", got, fullDocument) + } +} diff --git a/asyncapi/go.mod b/asyncapi/go.mod new file mode 100644 index 0000000..7a53a18 --- /dev/null +++ b/asyncapi/go.mod @@ -0,0 +1,19 @@ +module github.com/MarkRosemaker/asyncapi + +go 1.26.3 + +require ( + github.com/MarkRosemaker/errpath v0.0.0-20260425165607-bbd4959d04d9 + github.com/MarkRosemaker/jsonutil v0.0.0-20260718153618-78b5039427a4 + github.com/MarkRosemaker/ordmap v0.0.0-20260727220052-3181ae3b2977 + github.com/MarkRosemaker/yaml v0.0.0-20260508005758-fe21a538b084 + github.com/go-api-libs/types v0.0.0-20251210072721-82754f56609d +) + +require ( + cloud.google.com/go v0.123.0 // indirect + github.com/MarkRosemaker/json2yaml v0.0.0-20260507220148-d6cc0d01bff0 // indirect + github.com/MarkRosemaker/yaml2json v0.0.0-20260507220136-7748efc522b2 // indirect + golang.org/x/exp v0.0.0-20260727155853-b88d891fe743 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/asyncapi/go.sum b/asyncapi/go.sum new file mode 100644 index 0000000..aeb5ec5 --- /dev/null +++ b/asyncapi/go.sum @@ -0,0 +1,31 @@ +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +github.com/MarkRosemaker/errpath v0.0.0-20260425165607-bbd4959d04d9 h1:z9JAM7/nPytzu4NSjcqt+5OXmAfhS2jNxyW/yNGd1yQ= +github.com/MarkRosemaker/errpath v0.0.0-20260425165607-bbd4959d04d9/go.mod h1:VnXpUlsS3n1gSToedcQl2FCARGoA16CyitbCanUIv+I= +github.com/MarkRosemaker/json2yaml v0.0.0-20260507220148-d6cc0d01bff0 h1:9GB1anTC/7DR/G5LEUIQ4Ejn9lGt/YLlSzbycHEUKs0= +github.com/MarkRosemaker/json2yaml v0.0.0-20260507220148-d6cc0d01bff0/go.mod h1:SNnMfdX8qWy8+LhVMwvJU1CxWj6/HWeEh1jFREniPXg= +github.com/MarkRosemaker/jsonutil v0.0.0-20260718153618-78b5039427a4 h1:n2PpiQa2D7xaPuPd0CO+8A9zR0PyZxdrk6NUd/SbuPI= +github.com/MarkRosemaker/jsonutil v0.0.0-20260718153618-78b5039427a4/go.mod h1:1G6zbTPTdPOnBs2iLOpq3WzCFi3Gq3Y5rFOIPWst9C8= +github.com/MarkRosemaker/ordmap v0.0.0-20260727220052-3181ae3b2977 h1:Q4dpK6BRf0IwynoKmBdIDGupx/CZJwIfg2syif2m1z0= +github.com/MarkRosemaker/ordmap v0.0.0-20260727220052-3181ae3b2977/go.mod h1:X6pEr6WhX3CUMKNxQZoRHIlIpoLAlcuyCuzwveOD9yw= +github.com/MarkRosemaker/yaml v0.0.0-20260508005758-fe21a538b084 h1:ANUVXeLrgCDf2HZuqprb4BK2ZJC/HSGENaTo+Pp3zaM= +github.com/MarkRosemaker/yaml v0.0.0-20260508005758-fe21a538b084/go.mod h1:SDtV3o7QG3ZBjgjPe9PLGQEp5pxKgP+lmXvQRrKNhuI= +github.com/MarkRosemaker/yaml2json v0.0.0-20260507220136-7748efc522b2 h1:dEXAUQ4vLk1WNFTMx36HMYSn5cIjtQFReB9+0GX6QFU= +github.com/MarkRosemaker/yaml2json v0.0.0-20260507220136-7748efc522b2/go.mod h1:8s2Ukgy4JjwYPcdig9dl2SbP5L+/wX4MNIvbqLhEWpk= +github.com/go-api-libs/types v0.0.0-20251210072721-82754f56609d h1:OihMUySw53Oy0DTnE1CAfQ6YkX5/NBaS8Vc2Ww4T56Y= +github.com/go-api-libs/types v0.0.0-20251210072721-82754f56609d/go.mod h1:tJx9kzXjCU+mT7BzZvqg149HnZiHmr8JvbFWsvm5vp0= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +golang.org/x/exp v0.0.0-20260727155853-b88d891fe743 h1:ex206bKw+v3K0dm3andkrIF+ijyQKJG1pLgwQ2PYdQM= +golang.org/x/exp v0.0.0-20260727155853-b88d891fe743/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/asyncapi/info.go b/asyncapi/info.go new file mode 100644 index 0000000..a6bc719 --- /dev/null +++ b/asyncapi/info.go @@ -0,0 +1,163 @@ +package asyncapi + +import ( + "net/url" + "strings" + + "github.com/MarkRosemaker/errpath" +) + +// The Info object provides metadata about the API. +// The metadata can be used by the clients if needed. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#infoObject +type Info struct { + // REQUIRED. The title of the application. + Title string `json:"title" yaml:"title"` + // REQUIRED. Provides the version of the application API (not to be confused with the specification version). + Version string `json:"version" yaml:"version"` + // A short description of the application. CommonMark syntax can be used for rich text representation. + Description string `json:"description,omitempty" yaml:"description,omitempty"` + // A URL to the Terms of Service for the API. This MUST be in the form of an absolute URL. + TermsOfService *url.URL `json:"termsOfService,omitempty" yaml:"termsOfService,omitempty"` + // The contact information for the exposed API. + Contact *Contact `json:"contact,omitempty" yaml:"contact,omitempty"` + // The license information for the exposed API. + License *License `json:"license,omitempty" yaml:"license,omitempty"` + // A list of tags for application API documentation control. Tags can be used for logical grouping of applications. + Tags Tags `json:"tags,omitempty" yaml:"tags,omitempty"` + // Additional external documentation of the exposed API. + ExternalDocs *ExternalDocsRef `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + // This object MAY be extended with Specification Extensions. + Extensions Extensions `json:",inline" yaml:",inline"` +} + +// Validate checks the info object for correctness. +func (i *Info) Validate() error { + if i.Title == "" { + return &errpath.ErrField{Field: "title", Err: &errpath.ErrRequired{}} + } + + // NOTE: The version *here* is the version of the application API, + // it can be any string, unlike the version of the specification. + if i.Version == "" { + return &errpath.ErrField{Field: "version", Err: &errpath.ErrRequired{}} + } + + i.Description = strings.TrimSpace(i.Description) + + if err := validateURL(i.TermsOfService); err != nil { + return &errpath.ErrField{Field: "termsOfService", Err: err} + } + + if i.Contact != nil { + if err := i.Contact.Validate(); err != nil { + return &errpath.ErrField{Field: "contact", Err: err} + } + } + + if i.License != nil { + if err := i.License.Validate(); err != nil { + return &errpath.ErrField{Field: "license", Err: err} + } + } + + if err := i.Tags.Validate(); err != nil { + return &errpath.ErrField{Field: "tags", Err: err} + } + + if i.ExternalDocs != nil { + if err := i.ExternalDocs.Validate(); err != nil { + return &errpath.ErrField{Field: "externalDocs", Err: err} + } + } + + return validateExtensions(i.Extensions) +} + +// fixScheme ensures that the URL has a scheme and that it is valid. +// If the URL is nil, it is a no-op. +func fixScheme(u *url.URL) { + if u == nil { + return + } + + if u.Scheme == "" { + u.Scheme = "https" + } +} + +// validateURL checks that the URL is an absolute URL, as the specification demands +// of every URL it defines, e.g. "This MUST be in the form of an absolute URL." +// +// Since a missing scheme is a common mistake that is easy to correct, +// the scheme is assumed to be https and added if it is missing. +// If the URL is nil, it is a no-op. +func validateURL(u *url.URL) error { + if u == nil { + return nil + } + + fixScheme(u) + + // an absolute URL addresses a host, unless it is an opaque URI such as a URN + if u.Host == "" && u.Opaque == "" { + return &errpath.ErrInvalid[string]{ + Value: u.String(), + Message: "must be an absolute URL", + } + } + + return nil +} + +// validateURI checks that the URI conforms to the URI format, according to [RFC3986], +// i.e. that it is absolute. Unlike [validateURL], no scheme is added. +// If the URI is nil, it is a no-op. +// +// [RFC3986]: https://tools.ietf.org/html/rfc3986 +func validateURI(u *url.URL) error { + if u == nil { + return nil + } + + if !u.IsAbs() { + return &errpath.ErrInvalid[string]{ + Value: u.String(), + Message: "must conform to the URI format", + } + } + + return nil +} + +func (l *loader) collectInfo(i *Info, ref ref) { + if i == nil { + return + } + + l.collectTags(i.Tags, append(ref, "tags")) + + if i.ExternalDocs != nil { + l.collectExternalDocsRef(i.ExternalDocs, append(ref, "externalDocs")) + } +} + +func (l *loader) resolveInfo(i *Info) error { + if i == nil { + return nil + } + + if err := l.resolveTags(i.Tags); err != nil { + return &errpath.ErrField{Field: "tags", Err: err} + } + + if i.ExternalDocs != nil { + if err := l.resolveExternalDocsRef(i.ExternalDocs); err != nil { + return &errpath.ErrField{Field: "externalDocs", Err: err} + } + } + + return nil +} diff --git a/asyncapi/json.go b/asyncapi/json.go new file mode 100644 index 0000000..0ff77e1 --- /dev/null +++ b/asyncapi/json.go @@ -0,0 +1,29 @@ +package asyncapi + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + + "github.com/MarkRosemaker/jsonutil" +) + +// jsonOpts are the options used to read and write a document. +// +// "An AsyncAPI document can be JSON or YAML format. All field names in the specification are +// case sensitive. [...] In order to preserve the ability to round-trip between YAML and JSON +// formats, YAML version 1.2 is RECOMMENDED along with some additional constraints." +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#format +var jsonOpts = json.JoinOptions([]json.Options{ + // the AsyncAPI specification doesn't allow unknown fields in most objects + // also protect against deleting unknown fields when overwriting later + json.RejectUnknownMembers(true), + json.WithMarshalers(json.JoinMarshalers( + json.MarshalToFunc(jsonutil.URLMarshal), + )), + json.WithUnmarshalers(json.JoinUnmarshalers( + json.UnmarshalFromFunc(jsonutil.URLUnmarshal), + )), + jsontext.WithIndent(" "), // indent with two spaces +}...) diff --git a/asyncapi/license_struct.go b/asyncapi/license_struct.go new file mode 100644 index 0000000..bf86a7c --- /dev/null +++ b/asyncapi/license_struct.go @@ -0,0 +1,33 @@ +package asyncapi + +import ( + "net/url" + + "github.com/MarkRosemaker/errpath" +) + +// License information for the exposed API. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#licenseObject +type License struct { + // REQUIRED. The license name used for the API. + Name string `json:"name" yaml:"name"` + // A URL to the license used for the API. This MUST be in the form of an absolute URL. + URL *url.URL `json:"url,omitempty" yaml:"url,omitempty"` + // This object MAY be extended with Specification Extensions. + Extensions Extensions `json:",inline" yaml:",inline"` +} + +// Validate checks the license for correctness. +func (l *License) Validate() error { + if l.Name == "" { + return &errpath.ErrField{Field: "name", Err: &errpath.ErrRequired{}} + } + + if err := validateURL(l.URL); err != nil { + return &errpath.ErrField{Field: "url", Err: err} + } + + return validateExtensions(l.Extensions) +} diff --git a/asyncapi/loader.go b/asyncapi/loader.go new file mode 100644 index 0000000..9bf0442 --- /dev/null +++ b/asyncapi/loader.go @@ -0,0 +1,140 @@ +package asyncapi + +import ( + "bytes" + "encoding/json/jsontext" + "fmt" + "io" + "os" + "path/filepath" +) + +// loader helps deserialize an AsyncAPI v3 document. +// +// It remembers where every object that can be referenced is defined, +// so that the references of the document can be resolved once it was read. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#referenceObject +type loader struct { + schemas map[string]*AnySchema + servers map[string]*Server + serverVariables map[string]*ServerVariable + channels map[string]*Channel + operations map[string]*Operation + operationTraits map[string]*OperationTrait + replies map[string]*OperationReply + replyAddresses map[string]*OperationReplyAddress + messages map[string]*Message + messageTraits map[string]*MessageTrait + parameters map[string]*Parameter + correlationIDs map[string]*CorrelationID + securitySchemes map[string]*SecurityScheme + tags map[string]*Tag + externalDocs map[string]*ExternalDocs + bindings map[string]*Bindings + + // aliases maps the path of a reference to the identifier of the object it refers to + aliases map[string]string +} + +func (l *loader) reset() { + l.schemas = map[string]*AnySchema{} + l.servers = map[string]*Server{} + l.serverVariables = map[string]*ServerVariable{} + l.channels = map[string]*Channel{} + l.operations = map[string]*Operation{} + l.operationTraits = map[string]*OperationTrait{} + l.replies = map[string]*OperationReply{} + l.replyAddresses = map[string]*OperationReplyAddress{} + l.messages = map[string]*Message{} + l.messageTraits = map[string]*MessageTrait{} + l.parameters = map[string]*Parameter{} + l.correlationIDs = map[string]*CorrelationID{} + l.securitySchemes = map[string]*SecurityScheme{} + l.tags = map[string]*Tag{} + l.externalDocs = map[string]*ExternalDocs{} + l.bindings = map[string]*Bindings{} + l.aliases = map[string]string{} +} + +// newLoader returns an empty loader. +func newLoader() *loader { + return &loader{} +} + +// LoadFromFile reads an AsyncAPI specification from a file and parses it into a structured format. +func LoadFromFile(location string) (*Document, error) { + return newLoader().LoadFromFile(location) +} + +// LoadFromFile reads an AsyncAPI specification from a file and parses it into a structured format. +func (l *loader) LoadFromFile(location string) (*Document, error) { + f, err := os.Open(location) + if err != nil { + return nil, err + } + + // determine the file type and load accordingly + doc, err := func() (*Document, error) { + switch ext := filepath.Ext(location); ext { + case ".json": + return l.LoadFromReaderJSON(f) + case ".yaml", ".yml": + return l.LoadFromReaderYAML(f) + default: + return nil, fmt.Errorf("unsupported file extension: %s", ext) + } + }() + + return doc, errorsJoin(err, f.Close()) +} + +// LoadFromData reads an AsyncAPI specification from a byte array and parses it into a structured format. +func LoadFromData(data []byte) (*Document, error) { + return newLoader().LoadFromData(data) +} + +// LoadFromData reads an AsyncAPI specification from a byte array and parses it into a structured format. +// It will try to determine the format of the data and load it accordingly. +// If you know the format of the data, use LoadFromDataJSON or LoadFromDataYAML instead. +func (l *loader) LoadFromData(data []byte) (*Document, error) { + if jsontext.Value(data).IsValid() { + return l.LoadFromDataJSON(data) + } + + return l.LoadFromDataYAML(data) +} + +// LoadFromReader reads an AsyncAPI specification from an io.Reader and parses it into a structured format. +// It will try to determine the format of the data and load it accordingly. +// If you know the format of the data, use LoadFromReaderJSON or LoadFromReaderYAML instead. +func LoadFromReader(r io.Reader) (*Document, error) { + return newLoader().LoadFromReader(r) +} + +// LoadFromReader reads an AsyncAPI specification from an io.Reader and parses it into a structured format. +// It will try to determine the format of the data and load it accordingly. +// If you know the format of the data, use LoadFromReaderJSON or LoadFromReaderYAML instead. +func (l *loader) LoadFromReader(r io.Reader) (*Document, error) { + l.reset() + + // by default, assume the data is JSON + load := l.LoadFromReaderJSON + + // check if the data is JSON, save read data to buffer + buff := &bytes.Buffer{} + ok, err := isJSONRead(io.TeeReader(r, buff)) + if err != nil { + return nil, err + } + + // if the data is not JSON, use YAML + if !ok { + load = l.LoadFromReaderYAML + } + + // load the document using appropriate loader + // use multi-reader to combine what was read and the rest of the data + return load(io.MultiReader(buff, r)) // already includes resolving of references +} diff --git a/asyncapi/loader_json.go b/asyncapi/loader_json.go new file mode 100644 index 0000000..cd5e04f --- /dev/null +++ b/asyncapi/loader_json.go @@ -0,0 +1,67 @@ +package asyncapi + +import ( + "encoding/json/v2" + "io" + "unicode" +) + +// LoadFromReaderJSON reads an AsyncAPI specification in JSON format from an io.Reader and +// parses it into a structured format. +// +// "An AsyncAPI document can be JSON or YAML format." ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#format +func (l *loader) LoadFromReaderJSON(r io.Reader) (*Document, error) { + l.reset() + + doc := &Document{} + if err := json.UnmarshalRead(r, doc, jsonOpts); err != nil { + return nil, err + } + + if err := l.collectResolveRefs(doc); err != nil { + return nil, err + } + + return doc, nil +} + +// LoadFromDataJSON reads an AsyncAPI specification from a byte array in JSON format and parses it into a structured format. +func LoadFromDataJSON(data []byte) (*Document, error) { + return newLoader().LoadFromDataJSON(data) +} + +// LoadFromDataJSON reads an AsyncAPI specification from a byte array in JSON format and parses it into a structured format. +func (l *loader) LoadFromDataJSON(data []byte) (*Document, error) { + l.reset() + + doc := &Document{} + if err := json.Unmarshal(data, doc, jsonOpts); err != nil { + return nil, err + } + + if err := l.collectResolveRefs(doc); err != nil { + return nil, err + } + + return doc, nil +} + +// isJSONRead checks if the data in the reader is JSON. +// NOTE: this is a somewhat naive check, but it should work for most cases. +func isJSONRead(r io.Reader) (bool, error) { + for { + var b [1]byte + _, err := r.Read(b[:]) + if err != nil { + return false, err + } + + if unicode.IsSpace(rune(b[0])) { + continue + } + + return b[0] == '{', nil + } +} diff --git a/asyncapi/loader_test.go b/asyncapi/loader_test.go new file mode 100644 index 0000000..9d37690 --- /dev/null +++ b/asyncapi/loader_test.go @@ -0,0 +1,338 @@ +package asyncapi_test + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/MarkRosemaker/asyncapi" +) + +// examplePaths returns the paths of all example documents with the given extension. +func examplePaths(t *testing.T, ext string) []string { + t.Helper() + + paths, err := filepath.Glob(filepath.Join("examples", "v3.1", "*"+ext)) + if err != nil { + t.Fatal(err) + } + + if len(paths) == 0 { + t.Fatalf("no example documents with extension %q", ext) + } + + return paths +} + +func TestLoadFromFile(t *testing.T) { + t.Parallel() + + for _, path := range examplePaths(t, ".yaml") { + t.Run(filepath.Base(path), func(t *testing.T) { + t.Parallel() + + doc, err := asyncapi.LoadFromFile(path) + if err != nil { + t.Fatal(err) + } + + if err := doc.Validate(); err != nil { + t.Fatal(err) + } + + gotJSON, err := doc.ToJSON() + if err != nil { + t.Fatal(err) + } + + // the JSON version of the same document must be identical + wantJSON, err := os.ReadFile(strings.TrimSuffix(path, ".yaml") + ".json") + if err != nil { + t.Fatal(err) + } + + if got, want := string(gotJSON), strings.TrimSpace(string(wantJSON)); got != want { + t.Fatalf("got:\n%s\nwant:\n%s", got, want) + } + }) + } +} + +func TestLoadFromFile_JSON(t *testing.T) { + t.Parallel() + + for _, path := range examplePaths(t, ".json") { + t.Run(filepath.Base(path), func(t *testing.T) { + t.Parallel() + + doc, err := asyncapi.LoadFromFile(path) + if err != nil { + t.Fatal(err) + } + + if err := doc.Validate(); err != nil { + t.Fatal(err) + } + + // writing the document must reproduce the file + buf := &bytes.Buffer{} + if err := doc.WriteJSON(buf); err != nil { + t.Fatal(err) + } + + want, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + + if got, want := buf.String(), strings.TrimSpace(string(want)); got != want { + t.Fatalf("got:\n%s\nwant:\n%s", got, want) + } + }) + } +} + +func TestLoadFromFile_Errors(t *testing.T) { + t.Parallel() + + t.Run("file doesn't exist", func(t *testing.T) { + t.Parallel() + + if _, err := asyncapi.LoadFromFile("examples/does-not-exist.yaml"); err == nil { + t.Fatal("expected error") + } + }) + + t.Run("unsupported file extension", func(t *testing.T) { + t.Parallel() + + _, err := asyncapi.LoadFromFile("examples/invalid.txt") + if err == nil { + t.Fatal("expected error") + } + + if want := "unsupported file extension: .txt"; err.Error() != want { + t.Fatalf("got: %v, want: %v", err, want) + } + }) +} + +func TestLoadFromData(t *testing.T) { + t.Parallel() + + yamlData, err := os.ReadFile("examples/v3.1/simple.yaml") + if err != nil { + t.Fatal(err) + } + + jsonData, err := os.ReadFile("examples/v3.1/simple.json") + if err != nil { + t.Fatal(err) + } + + for name, data := range map[string][]byte{"yaml": yamlData, "json": jsonData} { + t.Run(name, func(t *testing.T) { + t.Parallel() + + // the format is detected automatically + doc, err := asyncapi.LoadFromData(data) + if err != nil { + t.Fatal(err) + } + + if got, want := doc.Info.Title, "Account Service"; got != want { + t.Fatalf("got: %v, want: %v", got, want) + } + + // the same document is loaded from a reader + docFromReader, err := asyncapi.LoadFromReader(bytes.NewReader(data)) + if err != nil { + t.Fatal(err) + } + + gotJSON, err := doc.ToJSON() + if err != nil { + t.Fatal(err) + } + + wantJSON, err := docFromReader.ToJSON() + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(gotJSON, wantJSON) { + t.Fatalf("got:\n%s\nwant:\n%s", gotJSON, wantJSON) + } + }) + } +} + +func TestLoadFromData_Errors(t *testing.T) { + t.Parallel() + + t.Run("unresolved reference", func(t *testing.T) { + t.Parallel() + + _, err := asyncapi.LoadFromDataYAML([]byte(`asyncapi: 3.1.0 +info: + title: Account Service + version: 1.0.0 +channels: + userSignedup: + address: user/signedup + messages: + UserSignedUp: + $ref: '#/components/messages/DoesNotExist' +`)) + if err == nil { + t.Fatal("expected error") + } + + want := `channels["userSignedup"].messages["UserSignedUp"]: couldn't resolve "#/components/messages/DoesNotExist"` + if err.Error() != want { + t.Fatalf("got: %v, want: %v", err, want) + } + }) + + t.Run("unknown field", func(t *testing.T) { + t.Parallel() + + // an unknown field is kept as an extension and only reported when validating + doc, err := asyncapi.LoadFromDataJSON([]byte( + `{"asyncapi":"3.1.0","info":{"title":"foo","version":"1.0.0"},` + + `"channels":{"foo":{}},"doesNotExist":true}`, + )) + if err != nil { + t.Fatal(err) + } + + err = doc.Validate() + if err == nil { + t.Fatal("expected error") + } + + if want := "doesNotExist: " + asyncapi.ErrUnknownField.Error(); err.Error() != want { + t.Fatalf("got: %v, want: %v", err, want) + } + }) + + t.Run("unknown field of a channel", func(t *testing.T) { + t.Parallel() + + doc, err := asyncapi.LoadFromDataJSON([]byte( + `{"asyncapi":"3.1.0","info":{"title":"foo","version":"1.0.0"},` + + `"channels":{"foo":{"doesNotExist":true}}}`, + )) + if err != nil { + t.Fatal(err) + } + + err = doc.Validate() + if err == nil { + t.Fatal("expected error") + } + + want := `channels["foo"].doesNotExist: ` + asyncapi.ErrUnknownField.Error() + if err.Error() != want { + t.Fatalf("got: %v, want: %v", err, want) + } + }) + + t.Run("invalid JSON", func(t *testing.T) { + t.Parallel() + + if _, err := asyncapi.LoadFromDataJSON([]byte(`{`)); err == nil { + t.Fatal("expected error") + } + }) +} + +func TestWriteToFile(t *testing.T) { + t.Parallel() + + doc, err := asyncapi.LoadFromFile("examples/v3.1/simple.yaml") + if err != nil { + t.Fatal(err) + } + + t.Run("json", func(t *testing.T) { + t.Parallel() + + path := filepath.Join(t.TempDir(), "sub", "asyncapi.json") + if err := doc.WriteToFile(path); err != nil { + t.Fatal(err) + } + + got, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + + want, err := doc.ToJSON() + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(got, want) { + t.Fatalf("got:\n%s\nwant:\n%s", got, want) + } + }) + + t.Run("yaml", func(t *testing.T) { + t.Parallel() + + path := filepath.Join(t.TempDir(), "asyncapi.yaml") + if err := doc.WriteToFile(path); err != nil { + t.Fatal(err) + } + + got, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + + want, err := doc.ToYAML() + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(got, want) { + t.Fatalf("got:\n%s\nwant:\n%s", got, want) + } + + // the document that was written can be read again + again, err := asyncapi.LoadFromFile(path) + if err != nil { + t.Fatal(err) + } + + gotJSON, err := again.ToJSON() + if err != nil { + t.Fatal(err) + } + + wantJSON, err := doc.ToJSON() + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(gotJSON, wantJSON) { + t.Fatalf("got:\n%s\nwant:\n%s", gotJSON, wantJSON) + } + }) + + t.Run("unsupported file extension", func(t *testing.T) { + t.Parallel() + + err := doc.WriteToFile(filepath.Join(t.TempDir(), "asyncapi.txt")) + if err == nil { + t.Fatal("expected error") + } + + if want := "unsupported file extension: .txt"; err.Error() != want { + t.Fatalf("got: %v, want: %v", err, want) + } + }) +} diff --git a/asyncapi/loader_yaml.go b/asyncapi/loader_yaml.go new file mode 100644 index 0000000..9114cdb --- /dev/null +++ b/asyncapi/loader_yaml.go @@ -0,0 +1,49 @@ +package asyncapi + +import ( + "io" + + "github.com/MarkRosemaker/yaml" +) + +// LoadFromReaderYAML reads an AsyncAPI specification in YAML format from an io.Reader and +// parses it into a structured format. +// +// "An AsyncAPI document can be JSON or YAML format." ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#format +func (l *loader) LoadFromReaderYAML(r io.Reader) (*Document, error) { + l.reset() + + doc := &Document{} + if err := yaml.UnmarshalRead(r, doc, jsonOpts); err != nil { + return nil, err + } + + if err := l.collectResolveRefs(doc); err != nil { + return nil, err + } + + return doc, nil +} + +// LoadFromDataYAML reads an AsyncAPI specification from a byte array in YAML format and parses it into a structured format. +func LoadFromDataYAML(data []byte) (*Document, error) { + return newLoader().LoadFromDataYAML(data) +} + +// LoadFromDataYAML reads an AsyncAPI specification from a byte array in YAML format and parses it into a structured format. +func (l *loader) LoadFromDataYAML(data []byte) (*Document, error) { + l.reset() + + doc := &Document{} + if err := yaml.Unmarshal(data, doc, jsonOpts); err != nil { + return nil, err + } + + if err := l.collectResolveRefs(doc); err != nil { + return nil, err + } + + return doc, nil +} diff --git a/asyncapi/map_of_strings.go b/asyncapi/map_of_strings.go new file mode 100644 index 0000000..0e2b10e --- /dev/null +++ b/asyncapi/map_of_strings.go @@ -0,0 +1,69 @@ +package asyncapi + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "iter" + + "github.com/MarkRosemaker/ordmap" +) + +// MapOfStrings is an ordered map of strings, e.g. the available scopes of an OAuth flow, +// which are "a map between the scope name and a short description for it". +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#oauthFlowObject +type MapOfStrings map[string]String + +// ByIndex returns a sequence of key-value pairs ordered by index. +func (ss MapOfStrings) ByIndex() iter.Seq2[string, String] { + return ordmap.ByIndex(ss, getIndexString) +} + +// Sort sorts the map by key and sets the indices accordingly. +func (ss MapOfStrings) Sort() { + ordmap.Sort(ss, setIndexString) +} + +// Set sets a value in the map, adding it at the end of the order. +func (ss *MapOfStrings) Set(key string, s String) { + ordmap.Set(ss, key, s, getIndexString, setIndexString) +} + +var _ json.MarshalerTo = (*MapOfStrings)(nil) + +// MarshalJSONTo marshals the key-value pairs in order. +func (ss *MapOfStrings) MarshalJSONTo(enc *jsontext.Encoder) error { + return ordmap.MarshalJSONTo(ss, enc) +} + +var _ json.UnmarshalerFrom = (*MapOfStrings)(nil) + +// UnmarshalJSONFrom unmarshals the key-value pairs in order and sets the indices. +func (ss *MapOfStrings) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + return ordmap.UnmarshalJSONFrom(ss, dec, setIndexString) +} + +// String is a string value that remembers its position in an ordered map. +type String struct { + Value string + + idx int +} + +var _ json.UnmarshalerFrom = (*String)(nil) + +// UnmarshalJSONFrom unmarshals the value of the String. +func (s *String) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + return json.UnmarshalDecode(dec, &s.Value) +} + +var _ json.MarshalerTo = (*String)(nil) + +// MarshalJSONTo marshals the value of the String. +func (s *String) MarshalJSONTo(enc *jsontext.Encoder) error { + return json.MarshalEncode(enc, s.Value) +} + +func getIndexString(s String) int { return s.idx } +func setIndexString(s String, i int) String { s.idx = i; return s } diff --git a/asyncapi/media_type.go b/asyncapi/media_type.go new file mode 100644 index 0000000..ba4a919 --- /dev/null +++ b/asyncapi/media_type.go @@ -0,0 +1,29 @@ +package asyncapi + +import "mime" + +// MediaType is "the content type to use when encoding/decoding a message's payload. +// The value MUST be a specific media type (e.g. `application/json`)." +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#defaultContentTypeString +type MediaType string + +const ( + // MediaTypeJSON is the media type for JSON payloads. + MediaTypeJSON MediaType = "application/json" + // MediaTypeYAML is the media type for YAML payloads. + MediaTypeYAML MediaType = "application/yaml" + // MediaTypeAvro is the media type for Avro payloads. + MediaTypeAvro MediaType = "avro/binary" + // MediaTypeProtobuf is the media type for Protocol Buffers payloads. + MediaTypeProtobuf MediaType = "application/protobuf" + // MediaTypeText is the media type for plain text payloads. + MediaTypeText MediaType = "text/plain" +) + +// Validate checks that the media type is well-formed. +func (mt MediaType) Validate() error { + _, _, err := mime.ParseMediaType(string(mt)) + return err +} diff --git a/asyncapi/message.go b/asyncapi/message.go new file mode 100644 index 0000000..03e10a7 --- /dev/null +++ b/asyncapi/message.go @@ -0,0 +1,181 @@ +package asyncapi + +import ( + "strings" + + "github.com/MarkRosemaker/errpath" +) + +// Message describes a message received on a given channel and operation. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#messageObject +type Message struct { + // Schema definition of the application headers. Schema MUST be a map of key-value pairs. + // It MUST NOT define the protocol headers. + Headers *AnySchemaRef `json:"headers,omitempty" yaml:"headers,omitempty"` + // Definition of the message payload. + Payload *AnySchemaRef `json:"payload,omitempty" yaml:"payload,omitempty"` + // Definition of the correlation ID used for message tracing or matching. + CorrelationID *CorrelationIDRef `json:"correlationId,omitempty" yaml:"correlationId,omitempty"` + // The content type to use when encoding/decoding a message's payload. + // When omitted, the value MUST be the one specified on the defaultContentType field of the document. + ContentType MediaType `json:"contentType,omitempty" yaml:"contentType,omitempty"` + // A machine-friendly name for the message. + Name string `json:"name,omitempty" yaml:"name,omitempty"` + // A human-friendly title for the message. + Title string `json:"title,omitempty" yaml:"title,omitempty"` + // A short summary of what the message is about. + Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` + // A verbose explanation of the message. CommonMark syntax can be used for rich text representation. + Description string `json:"description,omitempty" yaml:"description,omitempty"` + // A list of tags for logical grouping and categorization of messages. + Tags Tags `json:"tags,omitempty" yaml:"tags,omitempty"` + // Additional external documentation for this message. + ExternalDocs *ExternalDocsRef `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + // A map where the keys describe the name of the protocol and the values describe protocol-specific definitions for the message. + Bindings *BindingsRef `json:"bindings,omitempty" yaml:"bindings,omitempty"` + // List of examples. + Examples MessageExamples `json:"examples,omitempty" yaml:"examples,omitempty"` + // A list of traits to apply to the message object. + // Traits MUST be merged using the traits merge mechanism. + // The resulting object MUST be a valid Message Object. + Traits MessageTraitList `json:"traits,omitempty" yaml:"traits,omitempty"` + // This object MAY be extended with Specification Extensions. + Extensions Extensions `json:",inline" yaml:",inline"` +} + +// Validate checks the message for correctness. +func (m *Message) Validate() error { + if m.Headers != nil { + if err := m.Headers.Validate(); err != nil { + return &errpath.ErrField{Field: "headers", Err: err} + } + } + + if m.Payload != nil { + if err := m.Payload.Validate(); err != nil { + return &errpath.ErrField{Field: "payload", Err: err} + } + } + + if m.CorrelationID != nil { + if err := m.CorrelationID.Validate(); err != nil { + return &errpath.ErrField{Field: "correlationId", Err: err} + } + } + + if m.ContentType != "" { + if err := m.ContentType.Validate(); err != nil { + return &errpath.ErrField{Field: "contentType", Err: err} + } + } + + m.Description = strings.TrimSpace(m.Description) + + if err := m.Tags.Validate(); err != nil { + return &errpath.ErrField{Field: "tags", Err: err} + } + + if m.ExternalDocs != nil { + if err := m.ExternalDocs.Validate(); err != nil { + return &errpath.ErrField{Field: "externalDocs", Err: err} + } + } + + if m.Bindings != nil { + if err := m.Bindings.Validate(); err != nil { + return &errpath.ErrField{Field: "bindings", Err: err} + } + } + + if err := m.Examples.Validate(); err != nil { + return &errpath.ErrField{Field: "examples", Err: err} + } + + if err := m.Traits.Validate(); err != nil { + return &errpath.ErrField{Field: "traits", Err: err} + } + + return validateExtensions(m.Extensions) +} + +func (l *loader) collectMessageRef(m *MessageRef, ref ref) { + if !collectRef(l, m, l.messages, ref) { + return + } + + l.collectMessage(m.Value, ref) +} + +func (l *loader) collectMessage(m *Message, ref ref) { + if m.Headers != nil { + l.collectAnySchemaRef(m.Headers, append(ref, "headers")) + } + + if m.Payload != nil { + l.collectAnySchemaRef(m.Payload, append(ref, "payload")) + } + + if m.CorrelationID != nil { + l.collectCorrelationIDRef(m.CorrelationID, append(ref, "correlationId")) + } + + l.collectTags(m.Tags, append(ref, "tags")) + + if m.ExternalDocs != nil { + l.collectExternalDocsRef(m.ExternalDocs, append(ref, "externalDocs")) + } + + if m.Bindings != nil { + l.collectBindingsRef(m.Bindings, append(ref, "bindings")) + } + + l.collectMessageTraitList(m.Traits, append(ref, "traits")) +} + +func (l *loader) resolveMessageRef(m *MessageRef) error { + return resolveRef(m, l.messages, l.resolveMessage) +} + +func (l *loader) resolveMessage(m *Message) error { + if m.Headers != nil { + if err := l.resolveAnySchemaRef(m.Headers); err != nil { + return &errpath.ErrField{Field: "headers", Err: err} + } + } + + if m.Payload != nil { + if err := l.resolveAnySchemaRef(m.Payload); err != nil { + return &errpath.ErrField{Field: "payload", Err: err} + } + } + + if m.CorrelationID != nil { + if err := l.resolveCorrelationIDRef(m.CorrelationID); err != nil { + return &errpath.ErrField{Field: "correlationId", Err: err} + } + } + + if err := l.resolveTags(m.Tags); err != nil { + return &errpath.ErrField{Field: "tags", Err: err} + } + + if m.ExternalDocs != nil { + if err := l.resolveExternalDocsRef(m.ExternalDocs); err != nil { + return &errpath.ErrField{Field: "externalDocs", Err: err} + } + } + + if m.Bindings != nil { + if err := l.resolveBindingsRef(m.Bindings); err != nil { + return &errpath.ErrField{Field: "bindings", Err: err} + } + } + + if err := l.resolveMessageTraitList(m.Traits); err != nil { + return &errpath.ErrField{Field: "traits", Err: err} + } + + return nil +} diff --git a/asyncapi/message_example.go b/asyncapi/message_example.go new file mode 100644 index 0000000..d608315 --- /dev/null +++ b/asyncapi/message_example.go @@ -0,0 +1,52 @@ +package asyncapi + +import ( + "encoding/json/jsontext" + "errors" + + "github.com/MarkRosemaker/errpath" +) + +// ErrEmptyMessageExample is returned when a message example neither has headers nor a payload. +var ErrEmptyMessageExample = errors.New("must contain either headers and/or payload") + +// MessageExample represents an example of a [Message] object +// and MUST contain either headers and/or payload fields. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#messageExampleObject +type MessageExample struct { + // The value of this field MUST validate against the headers of the message. + Headers jsontext.Value `json:"headers,omitempty" yaml:"headers,omitempty"` + // The value of this field MUST validate against the payload of the message. + Payload jsontext.Value `json:"payload,omitempty" yaml:"payload,omitempty"` + // A machine-friendly name. + Name string `json:"name,omitempty" yaml:"name,omitempty"` + // A short summary of what the example is about. + Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` + // This object MAY be extended with Specification Extensions. + Extensions Extensions `json:",inline" yaml:",inline"` +} + +// Validate checks the message example for correctness. +func (ex *MessageExample) Validate() error { + if len(ex.Headers) == 0 && len(ex.Payload) == 0 { + return ErrEmptyMessageExample + } + + return validateExtensions(ex.Extensions) +} + +// MessageExamples is a list of examples of a message. +type MessageExamples []*MessageExample + +// Validate validates each example. +func (exs MessageExamples) Validate() error { + for i, ex := range exs { + if err := ex.Validate(); err != nil { + return &errpath.ErrIndex{Index: i, Err: err} + } + } + + return nil +} diff --git a/asyncapi/message_test.go b/asyncapi/message_test.go new file mode 100644 index 0000000..cb2260f --- /dev/null +++ b/asyncapi/message_test.go @@ -0,0 +1,92 @@ +package asyncapi_test + +import ( + "testing" + + "github.com/MarkRosemaker/asyncapi" +) + +func TestMessage_Traits(t *testing.T) { + t.Parallel() + + doc, err := asyncapi.LoadFromFile("examples/v3.1/streetlights-kafka.yaml") + if err != nil { + t.Fatal(err) + } + + if err := doc.Validate(); err != nil { + t.Fatal(err) + } + + msg := doc.Components.Messages["lightMeasured"].Value + if got, want := len(msg.Traits), 1; got != want { + t.Fatalf("got: %d traits, want: %d", got, want) + } + + // the trait was resolved + if msg.Traits[0].Value != doc.Components.MessageTraits["commonHeaders"].Value { + t.Fatal("the trait of the message was not resolved") + } + + if got, want := msg.ContentType, asyncapi.MediaTypeJSON; got != want { + t.Fatalf("got: %v, want: %v", got, want) + } + + // the payload was resolved + if msg.Payload.Value != doc.Components.Schemas["lightMeasuredPayload"].Value { + t.Fatal("the payload of the message was not resolved") + } +} + +func TestMessage_Validate_Errors(t *testing.T) { + t.Parallel() + + for name, tc := range map[string]struct { + msg *asyncapi.Message + want string + }{ + "invalid content type": { + &asyncapi.Message{ContentType: "not a media type"}, + `channels["userSignedup"].messages["test"].contentType: ` + + "mime: expected slash after first token", + }, + "example without headers and payload": { + &asyncapi.Message{Examples: asyncapi.MessageExamples{{Name: "empty"}}}, + `channels["userSignedup"].messages["test"].examples[0]: ` + + asyncapi.ErrEmptyMessageExample.Error(), + }, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + + doc := minimalDocument() + doc.Channels["userSignedup"].Value.Messages = asyncapi.Messages{ + "test": {Value: tc.msg}, + } + + err := doc.Validate() + if err == nil { + t.Fatal("expected error") + } + + if err.Error() != tc.want { + t.Fatalf("got: %v, want: %v", err, tc.want) + } + }) + } +} + +func TestMessageExample(t *testing.T) { + t.Parallel() + + ex := &asyncapi.MessageExample{ + Name: "SimpleSignup", + Summary: "A simple UserSignup example message", + Headers: []byte(`{"correlationId":"my-correlation-id"}`), + Payload: []byte(`{"user":{"someUserKey":"someUserValue"}}`), + } + + if err := ex.Validate(); err != nil { + t.Fatal(err) + } +} diff --git a/asyncapi/message_trait.go b/asyncapi/message_trait.go new file mode 100644 index 0000000..34a57ca --- /dev/null +++ b/asyncapi/message_trait.go @@ -0,0 +1,158 @@ +package asyncapi + +import ( + "strings" + + "github.com/MarkRosemaker/errpath" +) + +// MessageTrait describes a trait that MAY be applied to a [Message] object. +// This object MAY contain any property from the [Message] object, except `payload` and `traits`. +// +// If you're looking to apply traits to an operation, see the [OperationTrait] object. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#messageTraitObject +type MessageTrait struct { + // Schema definition of the application headers. Schema MUST be a map of key-value pairs. + // It MUST NOT define the protocol headers. + Headers *AnySchemaRef `json:"headers,omitempty" yaml:"headers,omitempty"` + // Definition of the correlation ID used for message tracing or matching. + CorrelationID *CorrelationIDRef `json:"correlationId,omitempty" yaml:"correlationId,omitempty"` + // The content type to use when encoding/decoding a message's payload. + // When omitted, the value MUST be the one specified on the defaultContentType field of the document. + ContentType MediaType `json:"contentType,omitempty" yaml:"contentType,omitempty"` + // A machine-friendly name for the message. + Name string `json:"name,omitempty" yaml:"name,omitempty"` + // A human-friendly title for the message. + Title string `json:"title,omitempty" yaml:"title,omitempty"` + // A short summary of what the message is about. + Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` + // A verbose explanation of the message. CommonMark syntax can be used for rich text representation. + Description string `json:"description,omitempty" yaml:"description,omitempty"` + // A list of tags for logical grouping and categorization of messages. + Tags Tags `json:"tags,omitempty" yaml:"tags,omitempty"` + // Additional external documentation for this message. + ExternalDocs *ExternalDocsRef `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + // A map where the keys describe the name of the protocol and the values describe protocol-specific definitions for the message. + Bindings *BindingsRef `json:"bindings,omitempty" yaml:"bindings,omitempty"` + // List of examples. + Examples MessageExamples `json:"examples,omitempty" yaml:"examples,omitempty"` + // This object MAY be extended with Specification Extensions. + Extensions Extensions `json:",inline" yaml:",inline"` +} + +// Validate checks the message trait for correctness. +func (t *MessageTrait) Validate() error { + if t.Headers != nil { + if err := t.Headers.Validate(); err != nil { + return &errpath.ErrField{Field: "headers", Err: err} + } + } + + if t.CorrelationID != nil { + if err := t.CorrelationID.Validate(); err != nil { + return &errpath.ErrField{Field: "correlationId", Err: err} + } + } + + if t.ContentType != "" { + if err := t.ContentType.Validate(); err != nil { + return &errpath.ErrField{Field: "contentType", Err: err} + } + } + + t.Description = strings.TrimSpace(t.Description) + + if err := t.Tags.Validate(); err != nil { + return &errpath.ErrField{Field: "tags", Err: err} + } + + if t.ExternalDocs != nil { + if err := t.ExternalDocs.Validate(); err != nil { + return &errpath.ErrField{Field: "externalDocs", Err: err} + } + } + + if t.Bindings != nil { + if err := t.Bindings.Validate(); err != nil { + return &errpath.ErrField{Field: "bindings", Err: err} + } + } + + if err := t.Examples.Validate(); err != nil { + return &errpath.ErrField{Field: "examples", Err: err} + } + + return validateExtensions(t.Extensions) +} + +func (l *loader) collectMessageTraitList(ts MessageTraitList, ref ref) { + for i, t := range ts { + l.collectMessageTraitRef(t, append(ref, itoa(i))) + } +} + +func (l *loader) collectMessageTraitRef(t *MessageTraitRef, ref ref) { + if !collectRef(l, t, l.messageTraits, ref) { + return + } + + l.collectMessageTrait(t.Value, ref) +} + +func (l *loader) collectMessageTrait(t *MessageTrait, ref ref) { + if t.Headers != nil { + l.collectAnySchemaRef(t.Headers, append(ref, "headers")) + } + + if t.CorrelationID != nil { + l.collectCorrelationIDRef(t.CorrelationID, append(ref, "correlationId")) + } + + l.collectTags(t.Tags, append(ref, "tags")) + + if t.ExternalDocs != nil { + l.collectExternalDocsRef(t.ExternalDocs, append(ref, "externalDocs")) + } + + if t.Bindings != nil { + l.collectBindingsRef(t.Bindings, append(ref, "bindings")) + } +} + +func (l *loader) resolveMessageTraitRef(t *MessageTraitRef) error { + return resolveRef(t, l.messageTraits, l.resolveMessageTrait) +} + +func (l *loader) resolveMessageTrait(t *MessageTrait) error { + if t.Headers != nil { + if err := l.resolveAnySchemaRef(t.Headers); err != nil { + return &errpath.ErrField{Field: "headers", Err: err} + } + } + + if t.CorrelationID != nil { + if err := l.resolveCorrelationIDRef(t.CorrelationID); err != nil { + return &errpath.ErrField{Field: "correlationId", Err: err} + } + } + + if err := l.resolveTags(t.Tags); err != nil { + return &errpath.ErrField{Field: "tags", Err: err} + } + + if t.ExternalDocs != nil { + if err := l.resolveExternalDocsRef(t.ExternalDocs); err != nil { + return &errpath.ErrField{Field: "externalDocs", Err: err} + } + } + + if t.Bindings != nil { + if err := l.resolveBindingsRef(t.Bindings); err != nil { + return &errpath.ErrField{Field: "bindings", Err: err} + } + } + + return nil +} diff --git a/asyncapi/message_traits.go b/asyncapi/message_traits.go new file mode 100644 index 0000000..8474195 --- /dev/null +++ b/asyncapi/message_traits.go @@ -0,0 +1,72 @@ +package asyncapi + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "iter" + + "github.com/MarkRosemaker/errpath" + "github.com/MarkRosemaker/ordmap" +) + +// MessageTraits is a map of Message Trait Objects. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#componentsMessageTraits +type MessageTraits map[string]*MessageTraitRef + +// Validate validates each message trait. +func (ts MessageTraits) Validate() error { + for name, t := range ts.ByIndex() { + if err := t.Validate(); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} + +// ByIndex returns a sequence of key-value pairs ordered by index. +func (ts MessageTraits) ByIndex() iter.Seq2[string, *MessageTraitRef] { + return ordmap.ByIndex(ts, getIndexRef[MessageTrait, *MessageTrait]) +} + +// Sort sorts the map by key and sets the indices accordingly. +func (ts MessageTraits) Sort() { + ordmap.Sort(ts, setIndexRef[MessageTrait, *MessageTrait]) +} + +// Set sets a value in the map, adding it at the end of the order. +func (ts *MessageTraits) Set(key string, t *MessageTraitRef) { + ordmap.Set(ts, key, t, getIndexRef[MessageTrait, *MessageTrait], setIndexRef[MessageTrait, *MessageTrait]) +} + +var _ json.MarshalerTo = (*MessageTraits)(nil) + +// MarshalJSONTo marshals the key-value pairs in order. +func (ts *MessageTraits) MarshalJSONTo(enc *jsontext.Encoder) error { + return ordmap.MarshalJSONTo(ts, enc) +} + +var _ json.UnmarshalerFrom = (*MessageTraits)(nil) + +// UnmarshalJSONFrom unmarshals the key-value pairs in order and sets the indices. +func (ts *MessageTraits) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + return ordmap.UnmarshalJSONFrom(ts, dec, setIndexRef[MessageTrait, *MessageTrait]) +} + +func (l *loader) collectMessageTraits(ts MessageTraits, ref ref) { + for name, t := range ts.ByIndex() { + l.collectMessageTraitRef(t, append(ref, name)) + } +} + +func (l *loader) resolveMessageTraits(ts MessageTraits) error { + for name, t := range ts.ByIndex() { + if err := l.resolveMessageTraitRef(t); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} diff --git a/asyncapi/messages.go b/asyncapi/messages.go new file mode 100644 index 0000000..6858e01 --- /dev/null +++ b/asyncapi/messages.go @@ -0,0 +1,75 @@ +package asyncapi + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "iter" + + "github.com/MarkRosemaker/errpath" + "github.com/MarkRosemaker/ordmap" +) + +// Messages describes a map of messages included in a channel. +// +// The key of each entry represents the message identifier. It is case-sensitive. +// Tools and libraries MAY use it to uniquely identify a message, therefore, it is RECOMMENDED to follow common programming naming conventions. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#messagesObject +type Messages map[string]*MessageRef + +// Validate validates each message. +func (ms Messages) Validate() error { + for name, m := range ms.ByIndex() { + if err := m.Validate(); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} + +// ByIndex returns a sequence of key-value pairs ordered by index. +func (ms Messages) ByIndex() iter.Seq2[string, *MessageRef] { + return ordmap.ByIndex(ms, getIndexRef[Message, *Message]) +} + +// Sort sorts the map by key and sets the indices accordingly. +func (ms Messages) Sort() { + ordmap.Sort(ms, setIndexRef[Message, *Message]) +} + +// Set sets a value in the map, adding it at the end of the order. +func (ms *Messages) Set(key string, m *MessageRef) { + ordmap.Set(ms, key, m, getIndexRef[Message, *Message], setIndexRef[Message, *Message]) +} + +var _ json.MarshalerTo = (*Messages)(nil) + +// MarshalJSONTo marshals the key-value pairs in order. +func (ms *Messages) MarshalJSONTo(enc *jsontext.Encoder) error { + return ordmap.MarshalJSONTo(ms, enc) +} + +var _ json.UnmarshalerFrom = (*Messages)(nil) + +// UnmarshalJSONFrom unmarshals the key-value pairs in order and sets the indices. +func (ms *Messages) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + return ordmap.UnmarshalJSONFrom(ms, dec, setIndexRef[Message, *Message]) +} + +func (l *loader) collectMessages(ms Messages, ref ref) { + for name, m := range ms.ByIndex() { + l.collectMessageRef(m, append(ref, name)) + } +} + +func (l *loader) resolveMessages(ms Messages) error { + for name, m := range ms.ByIndex() { + if err := l.resolveMessageRef(m); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} diff --git a/asyncapi/oauth_flow.go b/asyncapi/oauth_flow.go new file mode 100644 index 0000000..5e9a37d --- /dev/null +++ b/asyncapi/oauth_flow.go @@ -0,0 +1,131 @@ +package asyncapi + +import ( + "net/url" + + "github.com/MarkRosemaker/errpath" +) + +// OAuthFlowImplicit holds the configuration details for the OAuth Implicit flow. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#oauthFlowObject +type OAuthFlowImplicit struct { + // REQUIRED. The authorization URL to be used for this flow. This MUST be in the form of an absolute URL. + AuthorizationURL *url.URL `json:"authorizationUrl" yaml:"authorizationUrl"` + // The URL to be used for obtaining refresh tokens. This MUST be in the form of an absolute URL. + RefreshURL *url.URL `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` + // REQUIRED. The available scopes for the OAuth2 security scheme. + // A map between the scope name and a short description for it. + AvailableScopes MapOfStrings `json:"availableScopes" yaml:"availableScopes"` + // This object MAY be extended with Specification Extensions. + Extensions Extensions `json:",inline" yaml:",inline"` +} + +// Validate checks the OAuth flow for correctness. +func (f *OAuthFlowImplicit) Validate() error { + if f.AuthorizationURL == nil { + return &errpath.ErrField{Field: "authorizationUrl", Err: &errpath.ErrRequired{}} + } + + if err := validateURL(f.AuthorizationURL); err != nil { + return &errpath.ErrField{Field: "authorizationUrl", Err: err} + } + + if err := validateURL(f.RefreshURL); err != nil { + return &errpath.ErrField{Field: "refreshUrl", Err: err} + } + + if f.AvailableScopes == nil { + return &errpath.ErrField{Field: "availableScopes", Err: &errpath.ErrRequired{}} + } + + return validateExtensions(f.Extensions) +} + +// OAuthFlowPassword holds the configuration details for the OAuth Resource Owner Protected Credentials flow. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#oauthFlowObject +type OAuthFlowPassword struct { + // REQUIRED. The token URL to be used for this flow. This MUST be in the form of an absolute URL. + TokenURL *url.URL `json:"tokenUrl" yaml:"tokenUrl"` + // The URL to be used for obtaining refresh tokens. This MUST be in the form of an absolute URL. + RefreshURL *url.URL `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` + // REQUIRED. The available scopes for the OAuth2 security scheme. + // A map between the scope name and a short description for it. + AvailableScopes MapOfStrings `json:"availableScopes" yaml:"availableScopes"` + // This object MAY be extended with Specification Extensions. + Extensions Extensions `json:",inline" yaml:",inline"` +} + +// Validate checks the OAuth flow for correctness. +func (f *OAuthFlowPassword) Validate() error { + if f.TokenURL == nil { + return &errpath.ErrField{Field: "tokenUrl", Err: &errpath.ErrRequired{}} + } + + if err := validateURL(f.TokenURL); err != nil { + return &errpath.ErrField{Field: "tokenUrl", Err: err} + } + + if err := validateURL(f.RefreshURL); err != nil { + return &errpath.ErrField{Field: "refreshUrl", Err: err} + } + + if f.AvailableScopes == nil { + return &errpath.ErrField{Field: "availableScopes", Err: &errpath.ErrRequired{}} + } + + return validateExtensions(f.Extensions) +} + +// OAuthFlowClientCredentials holds the configuration details for the OAuth Client Credentials flow. +type OAuthFlowClientCredentials = OAuthFlowPassword + +// OAuthFlowAuthorizationCode holds the configuration details for the OAuth Authorization Code flow. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#oauthFlowObject +type OAuthFlowAuthorizationCode struct { + // REQUIRED. The authorization URL to be used for this flow. This MUST be in the form of an absolute URL. + AuthorizationURL *url.URL `json:"authorizationUrl" yaml:"authorizationUrl"` + // REQUIRED. The token URL to be used for this flow. This MUST be in the form of an absolute URL. + TokenURL *url.URL `json:"tokenUrl" yaml:"tokenUrl"` + // The URL to be used for obtaining refresh tokens. This MUST be in the form of an absolute URL. + RefreshURL *url.URL `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` + // REQUIRED. The available scopes for the OAuth2 security scheme. + // A map between the scope name and a short description for it. + AvailableScopes MapOfStrings `json:"availableScopes" yaml:"availableScopes"` + // This object MAY be extended with Specification Extensions. + Extensions Extensions `json:",inline" yaml:",inline"` +} + +// Validate checks the OAuth flow for correctness. +func (f *OAuthFlowAuthorizationCode) Validate() error { + if f.AuthorizationURL == nil { + return &errpath.ErrField{Field: "authorizationUrl", Err: &errpath.ErrRequired{}} + } + + if f.TokenURL == nil { + return &errpath.ErrField{Field: "tokenUrl", Err: &errpath.ErrRequired{}} + } + + if err := validateURL(f.AuthorizationURL); err != nil { + return &errpath.ErrField{Field: "authorizationUrl", Err: err} + } + + if err := validateURL(f.TokenURL); err != nil { + return &errpath.ErrField{Field: "tokenUrl", Err: err} + } + + if err := validateURL(f.RefreshURL); err != nil { + return &errpath.ErrField{Field: "refreshUrl", Err: err} + } + + if f.AvailableScopes == nil { + return &errpath.ErrField{Field: "availableScopes", Err: &errpath.ErrRequired{}} + } + + return validateExtensions(f.Extensions) +} diff --git a/asyncapi/oauth_flows.go b/asyncapi/oauth_flows.go new file mode 100644 index 0000000..1e9535f --- /dev/null +++ b/asyncapi/oauth_flows.go @@ -0,0 +1,49 @@ +package asyncapi + +import "github.com/MarkRosemaker/errpath" + +// OAuthFlows allows configuration of the supported OAuth Flows. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#oauthFlowsObject +type OAuthFlows struct { + // Configuration for the OAuth Implicit flow. + Implicit *OAuthFlowImplicit `json:"implicit,omitempty" yaml:"implicit,omitempty"` + // Configuration for the OAuth Resource Owner Protected Credentials flow. + Password *OAuthFlowPassword `json:"password,omitempty" yaml:"password,omitempty"` + // Configuration for the OAuth Client Credentials flow. + ClientCredentials *OAuthFlowClientCredentials `json:"clientCredentials,omitempty" yaml:"clientCredentials,omitempty"` + // Configuration for the OAuth Authorization Code flow. + AuthorizationCode *OAuthFlowAuthorizationCode `json:"authorizationCode,omitempty" yaml:"authorizationCode,omitempty"` + // This object MAY be extended with Specification Extensions. + Extensions Extensions `json:",inline" yaml:",inline"` +} + +// Validate checks the OAuth flows for correctness. +func (f *OAuthFlows) Validate() error { + if f.Implicit != nil { + if err := f.Implicit.Validate(); err != nil { + return &errpath.ErrField{Field: "implicit", Err: err} + } + } + + if f.Password != nil { + if err := f.Password.Validate(); err != nil { + return &errpath.ErrField{Field: "password", Err: err} + } + } + + if f.ClientCredentials != nil { + if err := f.ClientCredentials.Validate(); err != nil { + return &errpath.ErrField{Field: "clientCredentials", Err: err} + } + } + + if f.AuthorizationCode != nil { + if err := f.AuthorizationCode.Validate(); err != nil { + return &errpath.ErrField{Field: "authorizationCode", Err: err} + } + } + + return validateExtensions(f.Extensions) +} diff --git a/asyncapi/operation.go b/asyncapi/operation.go new file mode 100644 index 0000000..f6d7e8f --- /dev/null +++ b/asyncapi/operation.go @@ -0,0 +1,192 @@ +package asyncapi + +import ( + "strings" + + "github.com/MarkRosemaker/errpath" +) + +// Operation describes a specific operation. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#operationObject +type Operation struct { + // REQUIRED. Use `send` when it's expected that the application will send a message to the given channel, + // and `receive` when the application should expect receiving messages from the given channel. + Action OperationAction `json:"action" yaml:"action"` + // REQUIRED. A $ref pointer to the definition of the channel in which this operation is performed. + // If the operation is located in the root Operations Object, it MUST point to a channel definition located in the root Channels Object. + Channel *ChannelRef `json:"channel" yaml:"channel"` + // A human-friendly title for the operation. + Title string `json:"title,omitempty" yaml:"title,omitempty"` + // A short summary of what the operation is about. + Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` + // A verbose explanation of the operation. CommonMark syntax can be used for rich text representation. + Description string `json:"description,omitempty" yaml:"description,omitempty"` + // A declaration of which security schemes are associated with this operation. + // Only one of the security scheme objects MUST be satisfied to authorize an operation. + // In cases where server security also applies, it MUST also be satisfied. + Security SecuritySchemeRefList `json:"security,omitempty" yaml:"security,omitempty"` + // A list of tags for logical grouping and categorization of operations. + Tags Tags `json:"tags,omitempty" yaml:"tags,omitempty"` + // Additional external documentation for this operation. + ExternalDocs *ExternalDocsRef `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + // A map where the keys describe the name of the protocol and the values describe protocol-specific definitions for the operation. + Bindings *BindingsRef `json:"bindings,omitempty" yaml:"bindings,omitempty"` + // A list of traits to apply to the operation object. + // Traits MUST be merged using the traits merge mechanism. + // The resulting object MUST be a valid Operation Object. + Traits OperationTraitList `json:"traits,omitempty" yaml:"traits,omitempty"` + // A list of $ref pointers pointing to the supported Message Objects that can be processed by this operation. + // It MUST contain a subset of the messages defined in the channel referenced in this operation. + // + // Note: excluding this property from the operation implies that all messages from the channel will be included. + // Explicitly set it to an empty, non-nil list if this operation should contain no messages. + Messages MessageRefList `json:"messages,omitempty" yaml:"messages,omitempty"` + // The definition of the reply in a request-reply operation. + Reply *OperationReplyRef `json:"reply,omitempty" yaml:"reply,omitempty"` + // This object MAY be extended with Specification Extensions. + Extensions Extensions `json:",inline" yaml:",inline"` +} + +// Validate checks the operation for correctness. +func (o *Operation) Validate() error { + if o.Action == "" { + return &errpath.ErrField{Field: "action", Err: &errpath.ErrRequired{}} + } + + if err := o.Action.Validate(); err != nil { + return &errpath.ErrField{Field: "action", Err: err} + } + + if o.Channel == nil { + return &errpath.ErrField{Field: "channel", Err: &errpath.ErrRequired{}} + } + + if !o.Channel.isRef() { + return &errpath.ErrField{Field: "channel", Err: ErrMustBeReference} + } + + if err := o.Channel.Validate(); err != nil { + return &errpath.ErrField{Field: "channel", Err: err} + } + + o.Description = strings.TrimSpace(o.Description) + + if err := o.Security.Validate(); err != nil { + return &errpath.ErrField{Field: "security", Err: err} + } + + if err := o.Tags.Validate(); err != nil { + return &errpath.ErrField{Field: "tags", Err: err} + } + + if o.ExternalDocs != nil { + if err := o.ExternalDocs.Validate(); err != nil { + return &errpath.ErrField{Field: "externalDocs", Err: err} + } + } + + if o.Bindings != nil { + if err := o.Bindings.Validate(); err != nil { + return &errpath.ErrField{Field: "bindings", Err: err} + } + } + + if err := o.Traits.Validate(); err != nil { + return &errpath.ErrField{Field: "traits", Err: err} + } + + if err := o.Messages.Validate(); err != nil { + return &errpath.ErrField{Field: "messages", Err: err} + } + + // "It MUST contain a subset of the messages defined in the channel + // referenced in this operation." + if err := o.Messages.mustBeOfChannel(o.Channel.Value); err != nil { + return &errpath.ErrField{Field: "messages", Err: err} + } + + if o.Reply != nil { + if err := o.Reply.Validate(); err != nil { + return &errpath.ErrField{Field: "reply", Err: err} + } + } + + return validateExtensions(o.Extensions) +} + +func (l *loader) collectOperationRef(o *OperationRef, ref ref) { + if !collectRef(l, o, l.operations, ref) { + return + } + + l.collectOperation(o.Value, ref) +} + +func (l *loader) collectOperation(o *Operation, ref ref) { + l.collectSecuritySchemeRefList(o.Security, append(ref, "security")) + l.collectTags(o.Tags, append(ref, "tags")) + + if o.ExternalDocs != nil { + l.collectExternalDocsRef(o.ExternalDocs, append(ref, "externalDocs")) + } + + if o.Bindings != nil { + l.collectBindingsRef(o.Bindings, append(ref, "bindings")) + } + + l.collectOperationTraitList(o.Traits, append(ref, "traits")) + + if o.Reply != nil { + l.collectOperationReplyRef(o.Reply, append(ref, "reply")) + } +} + +func (l *loader) resolveOperationRef(o *OperationRef) error { + return resolveRef(o, l.operations, l.resolveOperation) +} + +func (l *loader) resolveOperation(o *Operation) error { + if o.Channel != nil { + if err := l.resolveChannelRef(o.Channel); err != nil { + return &errpath.ErrField{Field: "channel", Err: err} + } + } + + if err := l.resolveSecuritySchemeRefList(o.Security); err != nil { + return &errpath.ErrField{Field: "security", Err: err} + } + + if err := l.resolveTags(o.Tags); err != nil { + return &errpath.ErrField{Field: "tags", Err: err} + } + + if o.ExternalDocs != nil { + if err := l.resolveExternalDocsRef(o.ExternalDocs); err != nil { + return &errpath.ErrField{Field: "externalDocs", Err: err} + } + } + + if o.Bindings != nil { + if err := l.resolveBindingsRef(o.Bindings); err != nil { + return &errpath.ErrField{Field: "bindings", Err: err} + } + } + + if err := l.resolveOperationTraitList(o.Traits); err != nil { + return &errpath.ErrField{Field: "traits", Err: err} + } + + if err := l.resolveMessageRefList(o.Messages); err != nil { + return &errpath.ErrField{Field: "messages", Err: err} + } + + if o.Reply != nil { + if err := l.resolveOperationReplyRef(o.Reply); err != nil { + return &errpath.ErrField{Field: "reply", Err: err} + } + } + + return nil +} diff --git a/asyncapi/operation_action.go b/asyncapi/operation_action.go new file mode 100644 index 0000000..0b7c1f9 --- /dev/null +++ b/asyncapi/operation_action.go @@ -0,0 +1,38 @@ +package asyncapi + +import ( + "slices" + + "github.com/MarkRosemaker/errpath" +) + +// OperationAction describes whether the application sends messages to a channel +// or receives messages from it. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#operationObject +type OperationAction string + +const ( + // OperationActionSend is used when it's expected that the application will send a message to the given channel. + OperationActionSend OperationAction = "send" + // OperationActionReceive is used when the application should expect receiving messages from the given channel. + OperationActionReceive OperationAction = "receive" +) + +var allOperationActions = []OperationAction{ + OperationActionSend, + OperationActionReceive, +} + +// Validate validates the operation action. +func (a OperationAction) Validate() error { + if slices.Contains(allOperationActions, a) { + return nil + } + + return &errpath.ErrInvalid[OperationAction]{ + Value: a, + Enum: allOperationActions, + } +} diff --git a/asyncapi/operation_reply.go b/asyncapi/operation_reply.go new file mode 100644 index 0000000..d850bbe --- /dev/null +++ b/asyncapi/operation_reply.go @@ -0,0 +1,99 @@ +package asyncapi + +import "github.com/MarkRosemaker/errpath" + +// OperationReply describes the reply part that MAY be applied to an [Operation] object. +// If an operation implements the request/reply pattern, the reply object represents the response message. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#operationReplyObject +type OperationReply struct { + // Definition of the address that implementations MUST use for the reply. + Address *OperationReplyAddressRef `json:"address,omitempty" yaml:"address,omitempty"` + // A $ref pointer to the definition of the channel in which this operation is performed. + // When address is specified, the address property of the channel referenced by this property MUST be either null or not defined. + Channel *ChannelRef `json:"channel,omitempty" yaml:"channel,omitempty"` + // A list of $ref pointers pointing to the supported Message Objects that can be processed by this operation as reply. + // It MUST contain a subset of the messages defined in the channel referenced in this operation reply. + Messages MessageRefList `json:"messages,omitempty" yaml:"messages,omitempty"` + // This object MAY be extended with Specification Extensions. + Extensions Extensions `json:",inline" yaml:",inline"` +} + +// Validate checks the operation reply for correctness. +func (r *OperationReply) Validate() error { + if r.Address != nil { + if err := r.Address.Validate(); err != nil { + return &errpath.ErrField{Field: "address", Err: err} + } + } + + if r.Channel != nil { + if !r.Channel.isRef() { + return &errpath.ErrField{Field: "channel", Err: ErrMustBeReference} + } + + if err := r.Channel.Validate(); err != nil { + return &errpath.ErrField{Field: "channel", Err: err} + } + + // when an address is specified, the address of the referenced channel MUST be empty + if r.Address != nil && r.Channel.Value != nil && r.Channel.Value.Address != "" { + return &errpath.ErrField{Field: "channel", Err: &errpath.ErrField{ + Field: "address", + Err: &errpath.ErrInvalid[string]{ + Value: r.Channel.Value.Address, + Message: "must be empty when the reply defines an address", + }, + }} + } + } + + if err := r.Messages.Validate(); err != nil { + return &errpath.ErrField{Field: "messages", Err: err} + } + + // "It MUST contain a subset of the messages defined in the channel + // referenced in this operation reply." + if r.Channel != nil { + if err := r.Messages.mustBeOfChannel(r.Channel.Value); err != nil { + return &errpath.ErrField{Field: "messages", Err: err} + } + } + + return validateExtensions(r.Extensions) +} + +func (l *loader) collectOperationReplyRef(r *OperationReplyRef, ref ref) { + if !collectRef(l, r, l.replies, ref) { + return + } + + if r.Value.Address != nil { + l.collectOperationReplyAddressRef(r.Value.Address, append(ref, "address")) + } +} + +func (l *loader) resolveOperationReplyRef(r *OperationReplyRef) error { + return resolveRef(r, l.replies, l.resolveOperationReply) +} + +func (l *loader) resolveOperationReply(r *OperationReply) error { + if r.Address != nil { + if err := l.resolveOperationReplyAddressRef(r.Address); err != nil { + return &errpath.ErrField{Field: "address", Err: err} + } + } + + if r.Channel != nil { + if err := l.resolveChannelRef(r.Channel); err != nil { + return &errpath.ErrField{Field: "channel", Err: err} + } + } + + if err := l.resolveMessageRefList(r.Messages); err != nil { + return &errpath.ErrField{Field: "messages", Err: err} + } + + return nil +} diff --git a/asyncapi/operation_reply_address.go b/asyncapi/operation_reply_address.go new file mode 100644 index 0000000..f340a0a --- /dev/null +++ b/asyncapi/operation_reply_address.go @@ -0,0 +1,41 @@ +package asyncapi + +import ( + "strings" + + "github.com/MarkRosemaker/errpath" +) + +// OperationReplyAddress is an object that specifies where an operation has to send the reply. +// +// For specifying and computing the location of a reply address, a [RuntimeExpression] is used. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#operationReplyAddressObject +type OperationReplyAddress struct { + // An optional description of the address. CommonMark syntax can be used for rich text representation. + Description string `json:"description,omitempty" yaml:"description,omitempty"` + // REQUIRED. A runtime expression that specifies the location of the reply address. + Location RuntimeExpression `json:"location" yaml:"location"` + // This object MAY be extended with Specification Extensions. + Extensions Extensions `json:",inline" yaml:",inline"` +} + +// Validate checks the reply address for correctness. +func (a *OperationReplyAddress) Validate() error { + a.Description = strings.TrimSpace(a.Description) + + if err := a.Location.Validate(); err != nil { + return &errpath.ErrField{Field: "location", Err: err} + } + + return validateExtensions(a.Extensions) +} + +func (l *loader) collectOperationReplyAddressRef(a *OperationReplyAddressRef, ref ref) { + collectRef(l, a, l.replyAddresses, ref) +} + +func (l *loader) resolveOperationReplyAddressRef(a *OperationReplyAddressRef) error { + return resolveRef(a, l.replyAddresses, nil) +} diff --git a/asyncapi/operation_trait.go b/asyncapi/operation_trait.go new file mode 100644 index 0000000..6d576e7 --- /dev/null +++ b/asyncapi/operation_trait.go @@ -0,0 +1,115 @@ +package asyncapi + +import ( + "strings" + + "github.com/MarkRosemaker/errpath" +) + +// OperationTrait describes a trait that MAY be applied to an [Operation] object. +// This object MAY contain any property from the [Operation] object, except the `action`, `channel`, `messages` and `traits` ones. +// +// If you're looking to apply traits to a message, see the [MessageTrait] object. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#operationTraitObject +type OperationTrait struct { + // A human-friendly title for the operation. + Title string `json:"title,omitempty" yaml:"title,omitempty"` + // A short summary of what the operation is about. + Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` + // A verbose explanation of the operation. CommonMark syntax can be used for rich text representation. + Description string `json:"description,omitempty" yaml:"description,omitempty"` + // A declaration of which security schemes are associated with this operation. + Security SecuritySchemeRefList `json:"security,omitempty" yaml:"security,omitempty"` + // A list of tags for logical grouping and categorization of operations. + Tags Tags `json:"tags,omitempty" yaml:"tags,omitempty"` + // Additional external documentation for this operation. + ExternalDocs *ExternalDocsRef `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + // A map where the keys describe the name of the protocol and the values describe protocol-specific definitions for the operation. + Bindings *BindingsRef `json:"bindings,omitempty" yaml:"bindings,omitempty"` + // This object MAY be extended with Specification Extensions. + Extensions Extensions `json:",inline" yaml:",inline"` +} + +// Validate checks the operation trait for correctness. +func (t *OperationTrait) Validate() error { + t.Description = strings.TrimSpace(t.Description) + + if err := t.Security.Validate(); err != nil { + return &errpath.ErrField{Field: "security", Err: err} + } + + if err := t.Tags.Validate(); err != nil { + return &errpath.ErrField{Field: "tags", Err: err} + } + + if t.ExternalDocs != nil { + if err := t.ExternalDocs.Validate(); err != nil { + return &errpath.ErrField{Field: "externalDocs", Err: err} + } + } + + if t.Bindings != nil { + if err := t.Bindings.Validate(); err != nil { + return &errpath.ErrField{Field: "bindings", Err: err} + } + } + + return validateExtensions(t.Extensions) +} + +func (l *loader) collectOperationTraitList(ts OperationTraitList, ref ref) { + for i, t := range ts { + l.collectOperationTraitRef(t, append(ref, itoa(i))) + } +} + +func (l *loader) collectOperationTraitRef(t *OperationTraitRef, ref ref) { + if !collectRef(l, t, l.operationTraits, ref) { + return + } + + l.collectOperationTrait(t.Value, ref) +} + +func (l *loader) collectOperationTrait(t *OperationTrait, ref ref) { + l.collectSecuritySchemeRefList(t.Security, append(ref, "security")) + l.collectTags(t.Tags, append(ref, "tags")) + + if t.ExternalDocs != nil { + l.collectExternalDocsRef(t.ExternalDocs, append(ref, "externalDocs")) + } + + if t.Bindings != nil { + l.collectBindingsRef(t.Bindings, append(ref, "bindings")) + } +} + +func (l *loader) resolveOperationTraitRef(t *OperationTraitRef) error { + return resolveRef(t, l.operationTraits, l.resolveOperationTrait) +} + +func (l *loader) resolveOperationTrait(t *OperationTrait) error { + if err := l.resolveSecuritySchemeRefList(t.Security); err != nil { + return &errpath.ErrField{Field: "security", Err: err} + } + + if err := l.resolveTags(t.Tags); err != nil { + return &errpath.ErrField{Field: "tags", Err: err} + } + + if t.ExternalDocs != nil { + if err := l.resolveExternalDocsRef(t.ExternalDocs); err != nil { + return &errpath.ErrField{Field: "externalDocs", Err: err} + } + } + + if t.Bindings != nil { + if err := l.resolveBindingsRef(t.Bindings); err != nil { + return &errpath.ErrField{Field: "bindings", Err: err} + } + } + + return nil +} diff --git a/asyncapi/operation_traits.go b/asyncapi/operation_traits.go new file mode 100644 index 0000000..d66429e --- /dev/null +++ b/asyncapi/operation_traits.go @@ -0,0 +1,72 @@ +package asyncapi + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "iter" + + "github.com/MarkRosemaker/errpath" + "github.com/MarkRosemaker/ordmap" +) + +// OperationTraits is a map of Operation Trait Objects. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#componentsOperationTraits +type OperationTraits map[string]*OperationTraitRef + +// Validate validates each operation trait. +func (ts OperationTraits) Validate() error { + for name, t := range ts.ByIndex() { + if err := t.Validate(); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} + +// ByIndex returns a sequence of key-value pairs ordered by index. +func (ts OperationTraits) ByIndex() iter.Seq2[string, *OperationTraitRef] { + return ordmap.ByIndex(ts, getIndexRef[OperationTrait, *OperationTrait]) +} + +// Sort sorts the map by key and sets the indices accordingly. +func (ts OperationTraits) Sort() { + ordmap.Sort(ts, setIndexRef[OperationTrait, *OperationTrait]) +} + +// Set sets a value in the map, adding it at the end of the order. +func (ts *OperationTraits) Set(key string, t *OperationTraitRef) { + ordmap.Set(ts, key, t, getIndexRef[OperationTrait, *OperationTrait], setIndexRef[OperationTrait, *OperationTrait]) +} + +var _ json.MarshalerTo = (*OperationTraits)(nil) + +// MarshalJSONTo marshals the key-value pairs in order. +func (ts *OperationTraits) MarshalJSONTo(enc *jsontext.Encoder) error { + return ordmap.MarshalJSONTo(ts, enc) +} + +var _ json.UnmarshalerFrom = (*OperationTraits)(nil) + +// UnmarshalJSONFrom unmarshals the key-value pairs in order and sets the indices. +func (ts *OperationTraits) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + return ordmap.UnmarshalJSONFrom(ts, dec, setIndexRef[OperationTrait, *OperationTrait]) +} + +func (l *loader) collectOperationTraits(ts OperationTraits, ref ref) { + for name, t := range ts.ByIndex() { + l.collectOperationTraitRef(t, append(ref, name)) + } +} + +func (l *loader) resolveOperationTraits(ts OperationTraits) error { + for name, t := range ts.ByIndex() { + if err := l.resolveOperationTraitRef(t); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} diff --git a/asyncapi/operations.go b/asyncapi/operations.go new file mode 100644 index 0000000..e2d02bc --- /dev/null +++ b/asyncapi/operations.go @@ -0,0 +1,75 @@ +package asyncapi + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "iter" + + "github.com/MarkRosemaker/errpath" + "github.com/MarkRosemaker/ordmap" +) + +// Operations holds a dictionary with all the operations this application MUST implement. +// +// The key of each entry MUST be a string used to identify the operation in the document where it is defined, and its value is case-sensitive. +// Tools and libraries MAY use it to uniquely identify an operation, therefore, it is RECOMMENDED to follow common programming naming conventions. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#operationsObject +type Operations map[string]*OperationRef + +// Validate validates each operation. +func (ops Operations) Validate() error { + for name, o := range ops.ByIndex() { + if err := o.Validate(); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} + +// ByIndex returns a sequence of key-value pairs ordered by index. +func (ops Operations) ByIndex() iter.Seq2[string, *OperationRef] { + return ordmap.ByIndex(ops, getIndexRef[Operation, *Operation]) +} + +// Sort sorts the map by key and sets the indices accordingly. +func (ops Operations) Sort() { + ordmap.Sort(ops, setIndexRef[Operation, *Operation]) +} + +// Set sets a value in the map, adding it at the end of the order. +func (ops *Operations) Set(key string, o *OperationRef) { + ordmap.Set(ops, key, o, getIndexRef[Operation, *Operation], setIndexRef[Operation, *Operation]) +} + +var _ json.MarshalerTo = (*Operations)(nil) + +// MarshalJSONTo marshals the key-value pairs in order. +func (ops *Operations) MarshalJSONTo(enc *jsontext.Encoder) error { + return ordmap.MarshalJSONTo(ops, enc) +} + +var _ json.UnmarshalerFrom = (*Operations)(nil) + +// UnmarshalJSONFrom unmarshals the key-value pairs in order and sets the indices. +func (ops *Operations) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + return ordmap.UnmarshalJSONFrom(ops, dec, setIndexRef[Operation, *Operation]) +} + +func (l *loader) collectOperations(ops Operations, ref ref) { + for name, o := range ops.ByIndex() { + l.collectOperationRef(o, append(ref, name)) + } +} + +func (l *loader) resolveOperations(ops Operations) error { + for name, o := range ops.ByIndex() { + if err := l.resolveOperationRef(o); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} diff --git a/asyncapi/ordmap_test.go b/asyncapi/ordmap_test.go new file mode 100644 index 0000000..277ebca --- /dev/null +++ b/asyncapi/ordmap_test.go @@ -0,0 +1,105 @@ +package asyncapi_test + +import ( + "testing" + + "github.com/MarkRosemaker/asyncapi" + "github.com/MarkRosemaker/ordmap" +) + +func TestOrderedMaps(t *testing.T) { + t.Parallel() + + testSort[*asyncapi.Schemas](t) + testSort[*asyncapi.Servers](t) + testSort[*asyncapi.ServerVariables](t) + testSort[*asyncapi.Channels](t) + testSort[*asyncapi.Operations](t) + testSort[*asyncapi.Messages](t) + testSort[*asyncapi.MessageTraits](t) + testSort[*asyncapi.OperationTraits](t) + testSort[*asyncapi.Parameters](t) + testSort[*asyncapi.CorrelationIDs](t) + testSort[*asyncapi.Replies](t) + testSort[*asyncapi.ReplyAddresses](t) + testSort[*asyncapi.ExternalDocsByName](t) + testSort[*asyncapi.TagsByName](t) + testSort[*asyncapi.SecuritySchemes](t) + testSort[*asyncapi.BindingsByName](t) + testSort[*asyncapi.Bindings](t) + testSortValues[*asyncapi.MapOfStrings](t) +} + +// testSort checks that an ordered map of pointers keeps the order in which the keys were set +// and that it can be sorted by key. +func testSort[MP interface { + Set(K, *V) + *M +}, M interface { + Sort() + ordmap.ByIndexer[K, *V] +}, K ~string, V any](t *testing.T, +) { + t.Helper() + + var om M + + om.Sort() // no panic + + // set some values + var a, b, c V + MP(&om).Set("c", &c) + MP(&om).Set("a", &a) + MP(&om).Set("b", &b) + + checkOrder(t, om, []K{"c", "a", "b"}) + + om.Sort() + + checkOrder(t, om, []K{"a", "b", "c"}) +} + +// testSortValues is like testSort for an ordered map that holds values instead of pointers. +func testSortValues[MP interface { + Set(K, V) + *M +}, M interface { + Sort() + ordmap.ByIndexer[K, V] +}, K ~string, V any](t *testing.T, +) { + t.Helper() + + var om M + + om.Sort() // no panic + + // set some values + var a, b, c V + MP(&om).Set("c", c) + MP(&om).Set("a", a) + MP(&om).Set("b", b) + + checkOrder(t, om, []K{"c", "a", "b"}) + + om.Sort() + + checkOrder(t, om, []K{"a", "b", "c"}) +} + +func checkOrder[M ordmap.ByIndexer[K, V], K ~string, V any](t *testing.T, om M, want []K) { + t.Helper() + + i := 0 + for k := range om.ByIndex() { + if k != want[i] { + t.Fatalf("got: %v, want: %v", k, want[i]) + } + + i++ + } + + if i != len(want) { + t.Fatalf("got: %d keys, want: %d", i, len(want)) + } +} diff --git a/asyncapi/parameter.go b/asyncapi/parameter.go new file mode 100644 index 0000000..0f3e36a --- /dev/null +++ b/asyncapi/parameter.go @@ -0,0 +1,60 @@ +package asyncapi + +import ( + "errors" + "fmt" + "slices" + "strings" + + "github.com/MarkRosemaker/errpath" +) + +// Parameter describes a parameter included in a channel address. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#parameterObject +type Parameter struct { + // An enumeration of string values to be used if the substitution options are from a limited set. + Enum []string `json:"enum,omitempty" yaml:"enum,omitempty"` + // The default value to use for substitution, and to send, if an alternate value is not supplied. + Default string `json:"default,omitempty" yaml:"default,omitempty"` + // An optional description for the parameter. CommonMark syntax MAY be used for rich text representation. + Description string `json:"description,omitempty" yaml:"description,omitempty"` + // An array of examples of the parameter value. + Examples []string `json:"examples,omitempty" yaml:"examples,omitempty"` + // A runtime expression that specifies the location of the parameter value. + Location RuntimeExpression `json:"location,omitempty" yaml:"location,omitempty"` + // This object MAY be extended with Specification Extensions. + Extensions Extensions `json:",inline" yaml:",inline"` +} + +// Validate checks the parameter for correctness. +func (p *Parameter) Validate() error { + // either the array has entries or it is not defined + if p.Enum != nil && len(p.Enum) == 0 { + return errors.New("enum array must not be empty") + } + + // if the enum is defined, the default value MUST exist in the enum's values + if len(p.Enum) > 0 && p.Default != "" && !slices.Contains(p.Enum, p.Default) { + return fmt.Errorf("default value %q must exist in the enum's values", p.Default) + } + + p.Description = strings.TrimSpace(p.Description) + + if p.Location != "" { + if err := p.Location.Validate(); err != nil { + return &errpath.ErrField{Field: "location", Err: err} + } + } + + return validateExtensions(p.Extensions) +} + +func (l *loader) collectParameterRef(p *ParameterRef, ref ref) { + collectRef(l, p, l.parameters, ref) +} + +func (l *loader) resolveParameterRef(p *ParameterRef) error { + return resolveRef(p, l.parameters, nil) +} diff --git a/asyncapi/parameters.go b/asyncapi/parameters.go new file mode 100644 index 0000000..7b624d2 --- /dev/null +++ b/asyncapi/parameters.go @@ -0,0 +1,74 @@ +package asyncapi + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "iter" + + "github.com/MarkRosemaker/errpath" + "github.com/MarkRosemaker/ordmap" +) + +// Parameters describes a map of parameters included in a channel address. +// +// This map MUST contain all the parameters used in the parent channel address. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#parametersObject +type Parameters map[string]*ParameterRef + +// Validate validates each parameter. +func (ps Parameters) Validate() error { + for name, p := range ps.ByIndex() { + if err := p.Validate(); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} + +// ByIndex returns a sequence of key-value pairs ordered by index. +func (ps Parameters) ByIndex() iter.Seq2[string, *ParameterRef] { + return ordmap.ByIndex(ps, getIndexRef[Parameter, *Parameter]) +} + +// Sort sorts the map by key and sets the indices accordingly. +func (ps Parameters) Sort() { + ordmap.Sort(ps, setIndexRef[Parameter, *Parameter]) +} + +// Set sets a value in the map, adding it at the end of the order. +func (ps *Parameters) Set(key string, p *ParameterRef) { + ordmap.Set(ps, key, p, getIndexRef[Parameter, *Parameter], setIndexRef[Parameter, *Parameter]) +} + +var _ json.MarshalerTo = (*Parameters)(nil) + +// MarshalJSONTo marshals the key-value pairs in order. +func (ps *Parameters) MarshalJSONTo(enc *jsontext.Encoder) error { + return ordmap.MarshalJSONTo(ps, enc) +} + +var _ json.UnmarshalerFrom = (*Parameters)(nil) + +// UnmarshalJSONFrom unmarshals the key-value pairs in order and sets the indices. +func (ps *Parameters) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + return ordmap.UnmarshalJSONFrom(ps, dec, setIndexRef[Parameter, *Parameter]) +} + +func (l *loader) collectParameters(ps Parameters, ref ref) { + for name, p := range ps.ByIndex() { + l.collectParameterRef(p, append(ref, name)) + } +} + +func (l *loader) resolveParameters(ps Parameters) error { + for name, p := range ps.ByIndex() { + if err := l.resolveParameterRef(p); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} diff --git a/asyncapi/protocol.go b/asyncapi/protocol.go new file mode 100644 index 0000000..f04f867 --- /dev/null +++ b/asyncapi/protocol.go @@ -0,0 +1,75 @@ +package asyncapi + +// Protocol is the protocol a server supports for connection, +// respectively the name of the protocol a binding applies to. +type Protocol string + +const ( + // ProtocolHTTP is the HTTP protocol. + ProtocolHTTP Protocol = "http" + // ProtocolWebSockets is the WebSockets protocol. + ProtocolWebSockets Protocol = "ws" + // ProtocolKafka is the Kafka protocol. + ProtocolKafka Protocol = "kafka" + // ProtocolAnypointMQ is the Anypoint MQ protocol. + ProtocolAnypointMQ Protocol = "anypointmq" + // ProtocolAMQP is the AMQP 0-9-1 protocol. + ProtocolAMQP Protocol = "amqp" + // ProtocolAMQP1 is the AMQP 1.0 protocol. + ProtocolAMQP1 Protocol = "amqp1" + // ProtocolMQTT is the MQTT protocol. + ProtocolMQTT Protocol = "mqtt" + // ProtocolMQTT5 is the MQTT 5 protocol. + ProtocolMQTT5 Protocol = "mqtt5" + // ProtocolNATS is the NATS protocol. + ProtocolNATS Protocol = "nats" + // ProtocolJMS is the JMS protocol. + ProtocolJMS Protocol = "jms" + // ProtocolSNS is the SNS protocol. + ProtocolSNS Protocol = "sns" + // ProtocolSolace is the Solace protocol. + ProtocolSolace Protocol = "solace" + // ProtocolSQS is the SQS protocol. + ProtocolSQS Protocol = "sqs" + // ProtocolSTOMP is the STOMP protocol. + ProtocolSTOMP Protocol = "stomp" + // ProtocolRedis is the Redis protocol. + ProtocolRedis Protocol = "redis" + // ProtocolMercure is the Mercure protocol. + ProtocolMercure Protocol = "mercure" + // ProtocolIBMMQ is the IBM MQ protocol. + ProtocolIBMMQ Protocol = "ibmmq" + // ProtocolGooglePubSub is the Google Cloud Pub/Sub protocol. + ProtocolGooglePubSub Protocol = "googlepubsub" + // ProtocolPulsar is the Pulsar protocol. + ProtocolPulsar Protocol = "pulsar" + // ProtocolROS2 is the ROS 2 protocol. + ProtocolROS2 Protocol = "ros2" +) + +// allBindingProtocols are the protocols a bindings object may describe. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#serverBindingsObject +var allBindingProtocols = []Protocol{ + ProtocolHTTP, + ProtocolWebSockets, + ProtocolKafka, + ProtocolAnypointMQ, + ProtocolAMQP, + ProtocolAMQP1, + ProtocolMQTT, + ProtocolMQTT5, + ProtocolNATS, + ProtocolJMS, + ProtocolSNS, + ProtocolSolace, + ProtocolSQS, + ProtocolSTOMP, + ProtocolRedis, + ProtocolMercure, + ProtocolIBMMQ, + ProtocolGooglePubSub, + ProtocolPulsar, + ProtocolROS2, +} diff --git a/asyncapi/ref.go b/asyncapi/ref.go new file mode 100644 index 0000000..31f16f4 --- /dev/null +++ b/asyncapi/ref.go @@ -0,0 +1,120 @@ +package asyncapi + +import ( + "bytes" + "encoding/json/jsontext" + "encoding/json/v2" + "fmt" + + "github.com/MarkRosemaker/errpath" +) + +// Reference is "a simple object to allow referencing other components in the specification, +// internally and externally." +// +// "The Reference Object is defined by [JSON Reference] and follows the same structure, +// behavior and rules. A JSON Reference SHALL only be used to refer to a schema that is +// formatted in either JSON or YAML. In the case of a YAML-formatted Schema, the JSON Reference +// SHALL be applied to the JSON representation of that schema." +// +// "For this specification, reference resolution is done as defined by the JSON Reference +// specification and not by the JSON Schema specification." +// +// "This object cannot be extended with additional properties and any properties added SHALL be +// ignored." Additional properties are therefore dropped when a document is read, they are not +// written back. +// ([Specification]) +// +// [JSON Reference]: https://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03 +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#referenceObject +type Reference struct { + // REQUIRED. The reference string. + Identifier string `json:"$ref" yaml:"$ref"` +} + +// Validate checks the reference for correctness. +func (r *Reference) Validate() error { + if r.Identifier == "" { + return &errpath.ErrField{Field: "$ref", Err: &errpath.ErrRequired{}} + } + + return nil +} + +type referencable[T any] interface { + Validate() error + *T +} + +// refOrValue is a reference to a component or the component itself. +type refOrValue[T any, O referencable[T]] struct { + // The referenced object. + Value O `json:",inline" yaml:",inline"` + // The reference. + Ref *Reference `json:",inline" yaml:",inline"` + + // an index to the original location of this object + idx int +} + +// Validate checks the reference or the value it points to. +func (r *refOrValue[T, O]) Validate() error { + if r.Ref != nil { + if r.Value == nil { + return fmt.Errorf("%s (%T) was not resolved", r.Ref.Identifier, r.Value) + } + + return r.Ref.Validate() + } + + return r.Value.Validate() +} + +// isRef reports whether the object was given as a reference. +func (r *refOrValue[_, _]) isRef() bool { return r.Ref != nil } + +var _ json.UnmarshalerFrom = (*refOrValue[Tag, *Tag])(nil) + +func (r *refOrValue[T, O]) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + // we don't know if this is a reference or not, so we read the value first + val, err := dec.ReadValue() + if err != nil { + return err + } + + // try to unmarshal as a reference + // NOTE: A reference object cannot be extended with additional properties + // and any properties added SHALL be ignored, so we don't reject them here. + ref := &Reference{} + if err := json.UnmarshalDecode( + jsontext.NewDecoder(bytes.NewBuffer(val), dec.Options()), ref, + json.RejectUnknownMembers(false), + ); err == nil && ref.Identifier != "" { + // we successfully unmarshalled as a reference + r.Ref = ref // set the reference + return nil + } + + // it is not a reference, unmarshal as object + var v O + if err := json.UnmarshalDecode( + jsontext.NewDecoder(bytes.NewBuffer(val), dec.Options()), &v, + ); err != nil { + var t T + return fmt.Errorf("value of %T: %w", t, err) + } + + r.Value = v // set the value + + return nil +} + +var _ json.MarshalerTo = (*refOrValue[Tag, *Tag])(nil) + +func (r *refOrValue[_, _]) MarshalJSONTo(enc *jsontext.Encoder) error { + if r.Ref == nil { + return json.MarshalEncode(enc, r.Value) + } + + return json.MarshalEncode(enc, r.Ref) +} diff --git a/asyncapi/references_resolve.go b/asyncapi/references_resolve.go new file mode 100644 index 0000000..1b28771 --- /dev/null +++ b/asyncapi/references_resolve.go @@ -0,0 +1,129 @@ +package asyncapi + +import ( + "fmt" + "strconv" + "strings" +) + +// ref is the path to an object within the document, i.e. a JSON pointer split into its tokens. +// +// "The Reference Object is defined by JSON Reference and follows the same structure, behavior +// and rules. [...] For this specification, reference resolution is done as defined by the JSON +// Reference specification and not by the JSON Schema specification." ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#referenceObject +type ref []string + +func (r ref) String() string { + return strings.Join(r, "/") +} + +// itoa converts an index of a list to a token of a JSON pointer. +func itoa(i int) string { return strconv.Itoa(i) } + +// collectResolveRefs expands references in a document that was just unmarshaled. +func (l *loader) collectResolveRefs(doc *Document) error { + // collect all the objects that can be referenced + l.collectDocument(doc, []string{"#"}) + + // let the paths that hold a reference point to the object that is ultimately referenced + l.dereference() + l.linkAliases() + + // resolve all the references + return l.resolveDocument(doc) +} + +// linkAliases makes the objects that are referenced available under the paths +// of the references that point to them. +func (l *loader) linkAliases() { + linkAliases(l, l.schemas) + linkAliases(l, l.servers) + linkAliases(l, l.serverVariables) + linkAliases(l, l.channels) + linkAliases(l, l.operations) + linkAliases(l, l.operationTraits) + linkAliases(l, l.replies) + linkAliases(l, l.replyAddresses) + linkAliases(l, l.messages) + linkAliases(l, l.messageTraits) + linkAliases(l, l.parameters) + linkAliases(l, l.correlationIDs) + linkAliases(l, l.securitySchemes) + linkAliases(l, l.tags) + linkAliases(l, l.externalDocs) + linkAliases(l, l.bindings) +} + +func linkAliases[T any](l *loader, values map[string]*T) { + for path, target := range l.aliases { + if v, ok := values[target]; ok { + values[path] = v + } + } +} + +// collectRef remembers where an object is defined so that references to it can be resolved. +// +// It reports whether the object itself was given. If it was given as a reference, the reference +// is remembered as well, because it may be referenced in turn, e.g. an operation refers to a +// message of a channel which in turn refers to a message of the components object. +func collectRef[T any, O referencable[T]]( + l *loader, r *refOrValue[T, O], values map[string]*T, ref ref, +) bool { + if r.Ref != nil { + l.aliases[ref.String()] = r.Ref.Identifier + return false + } + + if r.Value == nil { + return false + } + + values[ref.String()] = (*T)(r.Value) + + return true +} + +// resolveRef resolves a reference to a value or resolves the value itself. +func resolveRef[T any, O referencable[T]]( + r *refOrValue[T, O], values map[string]*T, resolveValue func(*T) error, +) error { + if r.Ref != nil && r.Value == nil { + val, ok := values[r.Ref.Identifier] + if !ok { + return fmt.Errorf("couldn't resolve %q", r.Ref.Identifier) + } + + r.Value = val + + return nil + } + + if resolveValue == nil || r.Value == nil { + return nil + } + + return resolveValue(r.Value) +} + +// dereference follows the references that point to other references +// so that every reference points to the object it ultimately refers to. +func (l *loader) dereference() { + for from, to := range l.aliases { + seen := map[string]bool{from: true} + + for { + next, ok := l.aliases[to] + if !ok || seen[to] { + break + } + + seen[to] = true + to = next + } + + l.aliases[from] = to + } +} diff --git a/asyncapi/refs.go b/asyncapi/refs.go new file mode 100644 index 0000000..5f1b1ec --- /dev/null +++ b/asyncapi/refs.go @@ -0,0 +1,226 @@ +package asyncapi + +import ( + "errors" + + "github.com/MarkRosemaker/errpath" +) + +// ErrMustBeReference is returned when the specification demands a reference object +// but the object itself was given, e.g. for the channel of an operation: +// "Please note the `channel` property value MUST be a Reference Object and, therefore, +// MUST NOT contain a Channel Object." ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#operationObject +var ErrMustBeReference = errors.New("must be a reference object") + +// ErrMessageNotOfChannel is returned when an operation or an operation reply refers to a message +// that is not one of the messages of the channel it operates on. +var ErrMessageNotOfChannel = errors.New("must be a message of the channel of this operation") + +type ( + // AnySchemaRef is a reference to a schema or an actual schema. + AnySchemaRef = refOrValue[AnySchema, *AnySchema] + // ServerRef is a reference to a Server or an actual Server. + ServerRef = refOrValue[Server, *Server] + // ServerVariableRef is a reference to a ServerVariable or an actual ServerVariable. + ServerVariableRef = refOrValue[ServerVariable, *ServerVariable] + // ChannelRef is a reference to a Channel or an actual Channel. + ChannelRef = refOrValue[Channel, *Channel] + // OperationRef is a reference to an Operation or an actual Operation. + OperationRef = refOrValue[Operation, *Operation] + // OperationTraitRef is a reference to an OperationTrait or an actual OperationTrait. + OperationTraitRef = refOrValue[OperationTrait, *OperationTrait] + // OperationReplyRef is a reference to an OperationReply or an actual OperationReply. + OperationReplyRef = refOrValue[OperationReply, *OperationReply] + // OperationReplyAddressRef is a reference to an OperationReplyAddress or an actual OperationReplyAddress. + OperationReplyAddressRef = refOrValue[OperationReplyAddress, *OperationReplyAddress] + // MessageRef is a reference to a Message or an actual Message. + MessageRef = refOrValue[Message, *Message] + // MessageTraitRef is a reference to a MessageTrait or an actual MessageTrait. + MessageTraitRef = refOrValue[MessageTrait, *MessageTrait] + // ParameterRef is a reference to a Parameter or an actual Parameter. + ParameterRef = refOrValue[Parameter, *Parameter] + // CorrelationIDRef is a reference to a CorrelationID or an actual CorrelationID. + CorrelationIDRef = refOrValue[CorrelationID, *CorrelationID] + // SecuritySchemeRef is a reference to a SecurityScheme or an actual SecurityScheme. + SecuritySchemeRef = refOrValue[SecurityScheme, *SecurityScheme] + // TagRef is a reference to a Tag or an actual Tag. + TagRef = refOrValue[Tag, *Tag] + // ExternalDocsRef is a reference to an ExternalDocs or an actual ExternalDocs. + ExternalDocsRef = refOrValue[ExternalDocs, *ExternalDocs] + // BindingsRef is a reference to a Bindings object or an actual Bindings object. + BindingsRef = refOrValue[Bindings, *Bindings] + + // AnySchemaRefList is a slice of AnySchemaRef. + AnySchemaRefList []*AnySchemaRef + // ServerRefList is a slice of ServerRef. + ServerRefList []*ServerRef + // MessageRefList is a slice of MessageRef. + MessageRefList []*MessageRef + // SecuritySchemeRefList is a slice of SecuritySchemeRef. + SecuritySchemeRefList []*SecuritySchemeRef + // OperationTraitList is a slice of OperationTraitRef. + OperationTraitList []*OperationTraitRef + // MessageTraitList is a slice of MessageTraitRef. + MessageTraitList []*MessageTraitRef +) + +func getIndexRef[T any, O referencable[T]](ref *refOrValue[T, O]) int { return ref.idx } +func setIndexRef[T any, O referencable[T]]( + ref *refOrValue[T, O], i int, +) *refOrValue[T, O] { + ref.idx = i + return ref +} + +// contains reports whether the map holds the given object, no matter whether it was +// defined there or whether the entry is a reference that was resolved to it. +func contains[T any, O referencable[T]](m map[string]*refOrValue[T, O], v O) bool { + if v == nil { + return false + } + + for _, r := range m { + if r.Value == v { + return true + } + } + + return false +} + +// validateRefList validates every entry of a list of references or values. +func validateRefList[T any, O referencable[T]](rs []*refOrValue[T, O]) error { + for i, r := range rs { + if err := r.Validate(); err != nil { + return &errpath.ErrIndex{Index: i, Err: err} + } + } + + return nil +} + +// mustBeRefs makes sure that every entry of a list is given as a reference. +// +// The specification demands that some properties, e.g. the servers of a channel, +// hold reference objects and not the objects themselves. +func mustBeRefs[T any, O referencable[T]](rs []*refOrValue[T, O]) error { + for i, r := range rs { + if !r.isRef() { + return &errpath.ErrIndex{Index: i, Err: ErrMustBeReference} + } + } + + return nil +} + +// Validate validates each schema of the list. +func (ss AnySchemaRefList) Validate() error { return validateRefList(ss) } + +// Validate validates each server of the list and makes sure they are references. +func (ss ServerRefList) Validate() error { + if err := mustBeRefs(ss); err != nil { + return err + } + + return validateRefList(ss) +} + +// Validate validates each message of the list and makes sure they are references. +func (ms MessageRefList) Validate() error { + if err := mustBeRefs(ms); err != nil { + return err + } + + return validateRefList(ms) +} + +// mustBeOfChannel makes sure that every message of the list is a message of the given channel, +// as the specification demands of the messages of an operation and of an operation reply. +// +// If the channel is not known, e.g. because it wasn't given, there is nothing to check. +func (ms MessageRefList) mustBeOfChannel(c *Channel) error { + if c == nil { + return nil + } + + for i, m := range ms { + if contains(c.Messages, m.Value) { + continue + } + + return &errpath.ErrIndex{Index: i, Err: ErrMessageNotOfChannel} + } + + return nil +} + +// Validate validates each security scheme of the list. +func (ss SecuritySchemeRefList) Validate() error { return validateRefList(ss) } + +// Validate validates each operation trait of the list. +func (ts OperationTraitList) Validate() error { return validateRefList(ts) } + +// Validate validates each message trait of the list. +func (ts MessageTraitList) Validate() error { return validateRefList(ts) } + +func (l *loader) resolveAnySchemaRefList(ss AnySchemaRefList) error { + for i, s := range ss { + if err := l.resolveAnySchemaRef(s); err != nil { + return &errpath.ErrIndex{Index: i, Err: err} + } + } + + return nil +} + +func (l *loader) resolveServerRefList(ss ServerRefList) error { + for i, s := range ss { + if err := l.resolveServerRef(s); err != nil { + return &errpath.ErrIndex{Index: i, Err: err} + } + } + + return nil +} + +func (l *loader) resolveMessageRefList(ms MessageRefList) error { + for i, m := range ms { + if err := l.resolveMessageRef(m); err != nil { + return &errpath.ErrIndex{Index: i, Err: err} + } + } + + return nil +} + +func (l *loader) resolveSecuritySchemeRefList(ss SecuritySchemeRefList) error { + for i, s := range ss { + if err := l.resolveSecuritySchemeRef(s); err != nil { + return &errpath.ErrIndex{Index: i, Err: err} + } + } + + return nil +} + +func (l *loader) resolveOperationTraitList(ts OperationTraitList) error { + for i, t := range ts { + if err := l.resolveOperationTraitRef(t); err != nil { + return &errpath.ErrIndex{Index: i, Err: err} + } + } + + return nil +} + +func (l *loader) resolveMessageTraitList(ts MessageTraitList) error { + for i, t := range ts { + if err := l.resolveMessageTraitRef(t); err != nil { + return &errpath.ErrIndex{Index: i, Err: err} + } + } + + return nil +} diff --git a/asyncapi/refs_test.go b/asyncapi/refs_test.go new file mode 100644 index 0000000..95ab5dc --- /dev/null +++ b/asyncapi/refs_test.go @@ -0,0 +1,141 @@ +package asyncapi_test + +import ( + "testing" + + "github.com/MarkRosemaker/asyncapi" +) + +func TestReferences_Resolve(t *testing.T) { + t.Parallel() + + doc, err := asyncapi.LoadFromFile("examples/v3.1/simple.yaml") + if err != nil { + t.Fatal(err) + } + + channel := doc.Channels["userSignedup"] + if channel.Value == nil { + t.Fatal("the channel was not resolved") + } + + // the message of the channel is a reference to a message of the components object + msg := channel.Value.Messages["UserSignedUp"] + if msg.Ref == nil { + t.Fatal("expected the message to be given as a reference") + } + + if msg.Value != doc.Components.Messages["UserSignedUp"].Value { + t.Fatal("the message of the channel was not resolved") + } + + // the operation refers to the message of the channel, + // which in turn refers to the message of the components object + op := doc.Operations["sendUserSignedup"] + if got, want := len(op.Value.Messages), 1; got != want { + t.Fatalf("got: %d messages, want: %d", got, want) + } + + if op.Value.Messages[0].Value != msg.Value { + t.Fatal("the message of the operation was not resolved") + } + + // the operation refers to the channel + if op.Value.Channel.Value != channel.Value { + t.Fatal("the channel of the operation was not resolved") + } +} + +func TestReferences_MustBeReference(t *testing.T) { + t.Parallel() + + for name, tc := range map[string]struct { + doc func(*asyncapi.Document) + want string + }{ + "channel of an operation": { + func(d *asyncapi.Document) { + d.Operations = asyncapi.Operations{"sendUserSignedup": {Value: &asyncapi.Operation{ + Action: asyncapi.OperationActionSend, + Channel: &asyncapi.ChannelRef{Value: &asyncapi.Channel{Address: "user/signedup"}}, + }}} + }, + `operations["sendUserSignedup"].channel: ` + asyncapi.ErrMustBeReference.Error(), + }, + "messages of an operation": { + func(d *asyncapi.Document) { + d.Operations = asyncapi.Operations{"sendUserSignedup": {Value: &asyncapi.Operation{ + Action: asyncapi.OperationActionSend, + Channel: &asyncapi.ChannelRef{ + Ref: &asyncapi.Reference{Identifier: "#/channels/userSignedup"}, + // the reference was resolved when the document was loaded + Value: &asyncapi.Channel{Address: "user/signedup"}, + }, + Messages: asyncapi.MessageRefList{{Value: &asyncapi.Message{Name: "userSignedUp"}}}, + }}} + }, + `operations["sendUserSignedup"].messages[0]: ` + asyncapi.ErrMustBeReference.Error(), + }, + "servers of a channel": { + func(d *asyncapi.Document) { + d.Channels["userSignedup"].Value.Servers = asyncapi.ServerRefList{ + {Value: &asyncapi.Server{Host: "example.com", Protocol: asyncapi.ProtocolKafka}}, + } + }, + `channels["userSignedup"].servers[0]: ` + asyncapi.ErrMustBeReference.Error(), + }, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + + doc := minimalDocument() + tc.doc(doc) + + err := doc.Validate() + if err == nil { + t.Fatal("expected error") + } + + if err.Error() != tc.want { + t.Fatalf("got: %v, want: %v", err, tc.want) + } + }) + } +} + +func TestReferences_Unresolved(t *testing.T) { + t.Parallel() + + // a reference that was never resolved is reported when validating + doc := minimalDocument() + doc.Channels["userSignedup"].Value.Messages = asyncapi.Messages{ + "UserSignedUp": {Ref: &asyncapi.Reference{Identifier: "#/components/messages/UserSignedUp"}}, + } + + err := doc.Validate() + if err == nil { + t.Fatal("expected error") + } + + want := `channels["userSignedup"].messages["UserSignedUp"]: ` + + `#/components/messages/UserSignedUp (*asyncapi.Message) was not resolved` + if err.Error() != want { + t.Fatalf("got: %v, want: %v", err, want) + } +} + +func TestReference_Validate(t *testing.T) { + t.Parallel() + + r := &asyncapi.Reference{} + if err := r.Validate(); err == nil { + t.Fatal("expected error") + } else if want := "$ref is required"; err.Error() != want { + t.Fatalf("got: %v, want: %v", err, want) + } + + r.Identifier = "#/components/messages/UserSignedUp" + if err := r.Validate(); err != nil { + t.Fatal(err) + } +} diff --git a/asyncapi/replies.go b/asyncapi/replies.go new file mode 100644 index 0000000..c7094c9 --- /dev/null +++ b/asyncapi/replies.go @@ -0,0 +1,72 @@ +package asyncapi + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "iter" + + "github.com/MarkRosemaker/errpath" + "github.com/MarkRosemaker/ordmap" +) + +// Replies is a map of Operation Reply Objects. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#componentsReplies +type Replies map[string]*OperationReplyRef + +// Validate validates each reply. +func (rs Replies) Validate() error { + for name, r := range rs.ByIndex() { + if err := r.Validate(); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} + +// ByIndex returns a sequence of key-value pairs ordered by index. +func (rs Replies) ByIndex() iter.Seq2[string, *OperationReplyRef] { + return ordmap.ByIndex(rs, getIndexRef[OperationReply, *OperationReply]) +} + +// Sort sorts the map by key and sets the indices accordingly. +func (rs Replies) Sort() { + ordmap.Sort(rs, setIndexRef[OperationReply, *OperationReply]) +} + +// Set sets a value in the map, adding it at the end of the order. +func (rs *Replies) Set(key string, r *OperationReplyRef) { + ordmap.Set(rs, key, r, getIndexRef[OperationReply, *OperationReply], setIndexRef[OperationReply, *OperationReply]) +} + +var _ json.MarshalerTo = (*Replies)(nil) + +// MarshalJSONTo marshals the key-value pairs in order. +func (rs *Replies) MarshalJSONTo(enc *jsontext.Encoder) error { + return ordmap.MarshalJSONTo(rs, enc) +} + +var _ json.UnmarshalerFrom = (*Replies)(nil) + +// UnmarshalJSONFrom unmarshals the key-value pairs in order and sets the indices. +func (rs *Replies) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + return ordmap.UnmarshalJSONFrom(rs, dec, setIndexRef[OperationReply, *OperationReply]) +} + +func (l *loader) collectReplies(rs Replies, ref ref) { + for name, r := range rs.ByIndex() { + l.collectOperationReplyRef(r, append(ref, name)) + } +} + +func (l *loader) resolveReplies(rs Replies) error { + for name, r := range rs.ByIndex() { + if err := l.resolveOperationReplyRef(r); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} diff --git a/asyncapi/reply_addresses.go b/asyncapi/reply_addresses.go new file mode 100644 index 0000000..db428e3 --- /dev/null +++ b/asyncapi/reply_addresses.go @@ -0,0 +1,72 @@ +package asyncapi + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "iter" + + "github.com/MarkRosemaker/errpath" + "github.com/MarkRosemaker/ordmap" +) + +// ReplyAddresses is a map of Operation Reply Address Objects. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#componentsReplyAddresses +type ReplyAddresses map[string]*OperationReplyAddressRef + +// Validate validates each reply address. +func (as ReplyAddresses) Validate() error { + for name, a := range as.ByIndex() { + if err := a.Validate(); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} + +// ByIndex returns a sequence of key-value pairs ordered by index. +func (as ReplyAddresses) ByIndex() iter.Seq2[string, *OperationReplyAddressRef] { + return ordmap.ByIndex(as, getIndexRef[OperationReplyAddress, *OperationReplyAddress]) +} + +// Sort sorts the map by key and sets the indices accordingly. +func (as ReplyAddresses) Sort() { + ordmap.Sort(as, setIndexRef[OperationReplyAddress, *OperationReplyAddress]) +} + +// Set sets a value in the map, adding it at the end of the order. +func (as *ReplyAddresses) Set(key string, a *OperationReplyAddressRef) { + ordmap.Set(as, key, a, getIndexRef[OperationReplyAddress, *OperationReplyAddress], setIndexRef[OperationReplyAddress, *OperationReplyAddress]) +} + +var _ json.MarshalerTo = (*ReplyAddresses)(nil) + +// MarshalJSONTo marshals the key-value pairs in order. +func (as *ReplyAddresses) MarshalJSONTo(enc *jsontext.Encoder) error { + return ordmap.MarshalJSONTo(as, enc) +} + +var _ json.UnmarshalerFrom = (*ReplyAddresses)(nil) + +// UnmarshalJSONFrom unmarshals the key-value pairs in order and sets the indices. +func (as *ReplyAddresses) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + return ordmap.UnmarshalJSONFrom(as, dec, setIndexRef[OperationReplyAddress, *OperationReplyAddress]) +} + +func (l *loader) collectReplyAddresses(as ReplyAddresses, ref ref) { + for name, a := range as.ByIndex() { + l.collectOperationReplyAddressRef(a, append(ref, name)) + } +} + +func (l *loader) resolveReplyAddresses(as ReplyAddresses) error { + for name, a := range as.ByIndex() { + if err := l.resolveOperationReplyAddressRef(a); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} diff --git a/asyncapi/runtime_expression.go b/asyncapi/runtime_expression.go new file mode 100644 index 0000000..d9c60ff --- /dev/null +++ b/asyncapi/runtime_expression.go @@ -0,0 +1,53 @@ +package asyncapi + +import ( + "regexp" + + "github.com/MarkRosemaker/errpath" +) + +// A runtime expression allows values to be defined based on information that will be available within the message. +// This mechanism is used by the [CorrelationID] object and the [OperationReplyAddress] object. +// +// The runtime expression is defined by the following [ABNF] syntax: +// +// expression = ( "$message" "." source ) +// source = ( header-reference | payload-reference ) +// header-reference = "header" ["#" fragment] +// payload-reference = "payload" ["#" fragment] +// fragment = a JSON Pointer [RFC6901] +// +// Examples: +// +// | Source Location | Example expression | +// |-------------------------|----------------------------------| +// | Message Header Property | `$message.header#/MQMD/CorrelId` | +// | Message Payload Property | `$message.payload#/messageId` | +// +// Runtime expressions preserve the type of the referenced value. +// ([Specification]) +// +// [ABNF]: https://tools.ietf.org/html/rfc5234 +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#runtimeExpression +// +// [RFC6901]: https://tools.ietf.org/html/rfc6901 +type RuntimeExpression string + +// reRuntimeExpression matches a runtime expression. +var reRuntimeExpression = regexp.MustCompile(`^\$message\.(header|payload)(#(/[^/]*)*)?$`) + +// Validate checks that the runtime expression is well-formed. +func (expr RuntimeExpression) Validate() error { + if expr == "" { + return &errpath.ErrRequired{} + } + + if !reRuntimeExpression.MatchString(string(expr)) { + return &errpath.ErrInvalid[RuntimeExpression]{ + Value: expr, + Message: `must be a runtime expression, e.g. "$message.header#/correlationId"`, + } + } + + return nil +} diff --git a/asyncapi/runtime_expression_test.go b/asyncapi/runtime_expression_test.go new file mode 100644 index 0000000..9a9360f --- /dev/null +++ b/asyncapi/runtime_expression_test.go @@ -0,0 +1,90 @@ +package asyncapi_test + +import ( + "testing" + + "github.com/MarkRosemaker/asyncapi" +) + +func TestRuntimeExpression_Validate(t *testing.T) { + t.Parallel() + + t.Run("valid", func(t *testing.T) { + t.Parallel() + + for _, expr := range []asyncapi.RuntimeExpression{ + "$message.header#/MQMD/CorrelId", + "$message.payload#/messageId", + "$message.header", + "$message.payload", + } { + if err := expr.Validate(); err != nil { + t.Fatalf("%s: %v", expr, err) + } + } + }) + + t.Run("empty", func(t *testing.T) { + t.Parallel() + + var expr asyncapi.RuntimeExpression + + err := expr.Validate() + if err == nil { + t.Fatal("expected error") + } + + if want := "a value is required"; err.Error() != want { + t.Fatalf("got: %v, want: %v", err, want) + } + }) + + t.Run("invalid", func(t *testing.T) { + t.Parallel() + + for _, expr := range []asyncapi.RuntimeExpression{ + "$request.header#/foo", + "$message.body#/foo", + "message.header", + "$message.header/foo", + } { + if err := expr.Validate(); err == nil { + t.Fatalf("%s: expected error", expr) + } + } + }) +} + +func TestCorrelationID_Validate(t *testing.T) { + t.Parallel() + + doc, err := asyncapi.LoadFromFile("examples/v3.1/rpc-client.yaml") + if err != nil { + t.Fatal(err) + } + + if err := doc.Validate(); err != nil { + t.Fatal(err) + } + + msg := doc.Channels["queue"].Value.Messages["receiveSumResult"].Value + if got, want := msg.CorrelationID.Value.Location, + asyncapi.RuntimeExpression("$message.header#/correlation_id"); got != want { + t.Fatalf("got: %v, want: %v", got, want) + } + + // an invalid location is reported + msg.CorrelationID.Value.Location = "$message.somewhere" + + err = doc.Validate() + if err == nil { + t.Fatal("expected error") + } + + want := `channels["queue"].messages["receiveSumResult"].correlationId.location ` + + `("$message.somewhere") is invalid: ` + + `must be a runtime expression, e.g. "$message.header#/correlationId"` + if err.Error() != want { + t.Fatalf("got: %v, want: %v", err, want) + } +} diff --git a/asyncapi/save_json.go b/asyncapi/save_json.go new file mode 100644 index 0000000..f58bb28 --- /dev/null +++ b/asyncapi/save_json.go @@ -0,0 +1,64 @@ +package asyncapi + +import ( + "encoding/json/v2" + "errors" + "fmt" + "io" + "os" + "path/filepath" +) + +// WriteJSON writes the document in JSON format to the given writer. +// +// "An AsyncAPI document can be JSON or YAML format." ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#format +func (d Document) WriteJSON(w io.Writer) error { + return json.MarshalWrite(w, d, jsonOpts) +} + +// ToJSON marshals the document into JSON. +func (d *Document) ToJSON() ([]byte, error) { + return json.Marshal(d, jsonOpts) +} + +// WriteToFile writes the document to a file, in JSON or in YAML format, +// depending on the extension of the given path. +func (d *Document) WriteToFile(path string) error { + // determine the file type and write accordingly + var write func(io.Writer) error + + switch ext := filepath.Ext(path); ext { + case ".json": + write = d.WriteJSON + case ".yaml", ".yml": + write = d.WriteYAML + default: + return fmt.Errorf("unsupported file extension: %s", ext) + } + + // create the underlying directories if they don't exist + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + + f, err := os.Create(path) + if err != nil { + return err + } + + return errorsJoin(write(f), f.Close()) +} + +func errorsJoin(err1, err2 error) error { + if err1 == nil { + return err2 + } + + if err2 == nil { + return err1 + } + + return errors.Join(err1, err2) +} diff --git a/asyncapi/save_yaml.go b/asyncapi/save_yaml.go new file mode 100644 index 0000000..2d9d870 --- /dev/null +++ b/asyncapi/save_yaml.go @@ -0,0 +1,30 @@ +package asyncapi + +import ( + "io" + + "github.com/MarkRosemaker/yaml" +) + +// WriteYAML writes the document in YAML format to the given writer. +// +// "An AsyncAPI document can be JSON or YAML format. [...] In order to preserve the ability to +// round-trip between YAML and JSON formats, YAML version 1.2 is RECOMMENDED along with some +// additional constraints." ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#format +func (d Document) WriteYAML(w io.Writer) error { + data, err := d.ToYAML() + if err != nil { + return err + } + + _, err = w.Write(data) + + return err +} + +// ToYAML marshals the document into YAML. +func (d *Document) ToYAML() ([]byte, error) { + return yaml.Marshal(d, jsonOpts) +} diff --git a/asyncapi/schema.go b/asyncapi/schema.go new file mode 100644 index 0000000..bf632ef --- /dev/null +++ b/asyncapi/schema.go @@ -0,0 +1,435 @@ +package asyncapi + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "fmt" + "net/url" + "regexp" + "slices" + "strings" + + "github.com/MarkRosemaker/errpath" +) + +// Schema allows the definition of input and output data types. +// These types can be objects, but also primitives and arrays. +// This object is a superset of the [JSON Schema Specification Draft 07]. +// +// The empty schema (which allows any instance to validate) MAY be represented by the boolean +// value `true` and a schema which allows no instance to validate MAY be represented by the +// boolean value `false`. Both are represented by the [Schema.Boolean] field. +// +// For other formats (e.g. Avro, RAML, etc.) see the [MultiFormatSchema] object. +// ([Specification]) +// +// [JSON Schema Specification Draft 07]: https://json-schema.org/specification-links.html#draft-7 +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#schemaObject +type Schema struct { + // A boolean schema. `true` allows any instance to validate, `false` allows none. + // When it is set, all other fields are ignored. + Boolean *bool `json:"-" yaml:"-"` + + // The URI of the schema, used to identify it and to resolve relative references against. + ID *url.URL `json:"$id,omitempty" yaml:"$id,omitempty"` + // The dialect of the schema. + Dialect *url.URL `json:"$schema,omitempty" yaml:"$schema,omitempty"` + // A comment for the schema that is not meant to be displayed to end users. + Comment string `json:"$comment,omitempty" yaml:"$comment,omitempty"` + + // The name of the schema. + Title string `json:"title,omitempty" yaml:"title,omitempty"` + // A short description of the schema. CommonMark syntax can be used for rich text representation. + Description string `json:"description,omitempty" yaml:"description,omitempty"` + // Specifies the data type of the schema. It is either a single type or a list of types. + Type DataTypes `json:"type,omitempty" yaml:"type,omitempty"` + // Further refines the data type. See [Format] for the formats defined by the specification. + Format Format `json:"format,omitempty" yaml:"format,omitempty"` + + // Composition + + // AllOf validates the value against ALL of the given schemas. + AllOf AnySchemaRefList `json:"allOf,omitempty" yaml:"allOf,omitempty"` + // OneOf validates the value against EXACTLY ONE of the given schemas. + OneOf AnySchemaRefList `json:"oneOf,omitempty" yaml:"oneOf,omitempty"` + // AnyOf validates the value against AT LEAST ONE of the given schemas. + AnyOf AnySchemaRefList `json:"anyOf,omitempty" yaml:"anyOf,omitempty"` + // Not validates the value against the negation of the given schema. + Not *AnySchemaRef `json:"not,omitempty" yaml:"not,omitempty"` + // If is the condition of a conditional schema. + If *AnySchemaRef `json:"if,omitempty" yaml:"if,omitempty"` + // Then is applied when the value validates against the schema given in the `if` keyword. + Then *AnySchemaRef `json:"then,omitempty" yaml:"then,omitempty"` + // Else is applied when the value does not validate against the schema given in the `if` keyword. + Else *AnySchemaRef `json:"else,omitempty" yaml:"else,omitempty"` + + // Integer / Number + + // The value must be a multiple of this number. + MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` + // The minimum value of the number. + Min *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"` + // The exclusive minimum value of the number. + ExclusiveMin *float64 `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"` + // The maximum value of the number. + Max *float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"` + // The exclusive maximum value of the number. + ExclusiveMax *float64 `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"` + + // String + + // The minimum length of the string. + MinLength uint `json:"minLength,omitzero" yaml:"minLength,omitempty"` + // The maximum length of the string. + MaxLength *uint `json:"maxLength,omitempty" yaml:"maxLength,omitempty"` + // The pattern is used to validate the string. + // This string SHOULD be a valid regular expression, according to the ECMA 262 regular expression dialect. + // NOTE: We simply use text unmarshalling for this field. This guarantees that the regular expression is valid or we can't unmarshal. + Pattern *regexp.Regexp `json:"pattern,omitempty" yaml:"pattern,omitempty"` + + // Array + + // The minimum number of items in the array. + MinItems uint `json:"minItems,omitzero" yaml:"minItems,omitempty"` + // The maximum number of items in the array. + MaxItems *uint `json:"maxItems,omitempty" yaml:"maxItems,omitempty"` + // Whether the items of the array must be unique. + UniqueItems bool `json:"uniqueItems,omitzero" yaml:"uniqueItems,omitempty"` + // The schema the items of the array must validate against. + Items *AnySchemaRef `json:"items,omitempty" yaml:"items,omitempty"` + // The schema the additional items of the array must validate against. + AdditionalItems *AnySchemaRef `json:"additionalItems,omitempty" yaml:"additionalItems,omitempty"` + // The schema at least one item of the array must validate against. + Contains *AnySchemaRef `json:"contains,omitempty" yaml:"contains,omitempty"` + + // Object + + // The minimum number of properties of the object. + MinProperties uint `json:"minProperties,omitzero" yaml:"minProperties,omitempty"` + // The maximum number of properties of the object. + MaxProperties *uint `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"` + // Which properties are required. + Required []string `json:"required,omitempty" yaml:"required,omitempty"` + // The properties of the object. + Properties Schemas `json:"properties,omitempty" yaml:"properties,omitempty"` + // The properties of the object whose names match a regular expression. + PatternProperties Schemas `json:"patternProperties,omitempty" yaml:"patternProperties,omitempty"` + // The schema the additional properties of the object must validate against. + AdditionalProperties *AnySchemaRef `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"` + // The schema the property names of the object must validate against. + PropertyNames *AnySchemaRef `json:"propertyNames,omitempty" yaml:"propertyNames,omitempty"` + // Reusable schemas that are referenced from within this schema. + Definitions Schemas `json:"definitions,omitempty" yaml:"definitions,omitempty"` + + // Values + + // A list of possible values. + Enum []jsontext.Value `json:"enum,omitempty" yaml:"enum,omitempty"` + // The only possible value. + Const jsontext.Value `json:"const,omitempty" yaml:"const,omitempty"` + // The value that is used if no other value is present. + // Unlike JSON Schema, the value MUST conform to the defined type for the schema defined at the same level. + Default jsontext.Value `json:"default,omitempty" yaml:"default,omitempty"` + // A list of examples of the value. + Examples []jsontext.Value `json:"examples,omitempty" yaml:"examples,omitempty"` + + // special encoding for binary data + ContentEncoding string `json:"contentEncoding,omitempty" yaml:"contentEncoding,omitempty"` + ContentMediaType string `json:"contentMediaType,omitempty" yaml:"contentMediaType,omitempty"` + + // Whether the value is only sent by the server and must not be sent by the client. + ReadOnly bool `json:"readOnly,omitzero" yaml:"readOnly,omitempty"` + // Whether the value is only sent by the client and must not be sent by the server. + WriteOnly bool `json:"writeOnly,omitzero" yaml:"writeOnly,omitempty"` + + // AsyncAPI vocabulary + + // Adds support for polymorphism. + // The discriminator is the schema property name that is used to differentiate between other schemas that inherit this schema. + // The property name used MUST be defined at this schema and it MUST be in the required property list. + Discriminator string `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` + // Additional external documentation for this schema. + ExternalDocs *ExternalDocsRef `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + // Specifies that a schema is deprecated and SHOULD be transitioned out of usage. + Deprecated bool `json:"deprecated,omitzero" yaml:"deprecated,omitempty"` + + // This object MAY be extended with Specification Extensions. + Extensions Extensions `json:",inline" yaml:",inline"` +} + +// schemaValue is a [Schema] without the custom marshalling, used to avoid infinite recursion. +type schemaValue Schema + +var _ json.UnmarshalerFrom = (*Schema)(nil) + +// UnmarshalJSONFrom unmarshals the schema, which may be a boolean schema. +func (s *Schema) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + switch dec.PeekKind() { + case 't', 'f': + var b bool + if err := json.UnmarshalDecode(dec, &b); err != nil { + return err + } + + s.Boolean = &b + + return nil + } + + return json.UnmarshalDecode(dec, (*schemaValue)(s)) +} + +var _ json.MarshalerTo = (*Schema)(nil) + +// MarshalJSONTo marshals the schema, which may be a boolean schema. +func (s *Schema) MarshalJSONTo(enc *jsontext.Encoder) error { + if s.Boolean != nil { + return json.MarshalEncode(enc, *s.Boolean) + } + + return json.MarshalEncode(enc, (*schemaValue)(s)) +} + +// Validate checks the schema for correctness. +func (s *Schema) Validate() error { + if s.Boolean != nil { + return nil // a boolean schema has no other fields + } + + s.Description = strings.TrimSpace(s.Description) + + if err := s.Type.Validate(); err != nil { + return &errpath.ErrField{Field: "type", Err: err} + } + + // NOTE: The format is an open string-valued property, + // so a format that is not defined by the specification is not an error. + + if err := s.validateComposition(); err != nil { + return err + } + + if err := s.validateRanges(); err != nil { + return err + } + + if err := s.validateProperties(); err != nil { + return err + } + + for _, sub := range s.subSchemas() { + if sub.schema == nil { + continue + } + + if err := sub.schema.Validate(); err != nil { + return &errpath.ErrField{Field: sub.field, Err: err} + } + } + + if s.ExternalDocs != nil { + if err := s.ExternalDocs.Validate(); err != nil { + return &errpath.ErrField{Field: "externalDocs", Err: err} + } + } + + return validateExtensions(s.Extensions) +} + +func (s *Schema) validateComposition() error { + if err := s.AllOf.Validate(); err != nil { + return &errpath.ErrField{Field: "allOf", Err: err} + } + + if err := s.OneOf.Validate(); err != nil { + return &errpath.ErrField{Field: "oneOf", Err: err} + } + + if err := s.AnyOf.Validate(); err != nil { + return &errpath.ErrField{Field: "anyOf", Err: err} + } + + return nil +} + +// subSchema pairs a field name with the subschema it holds. +type subSchema struct { + field string + schema *AnySchemaRef +} + +// subSchemas returns the subschemas that are held by a single field, in the order of the fields. +func (s *Schema) subSchemas() []subSchema { + return []subSchema{ + {"not", s.Not}, {"if", s.If}, {"then", s.Then}, {"else", s.Else}, + {"items", s.Items}, {"additionalItems", s.AdditionalItems}, {"contains", s.Contains}, + {"additionalProperties", s.AdditionalProperties}, {"propertyNames", s.PropertyNames}, + } +} + +// validateRanges checks that the lower bounds are not greater than the upper bounds. +func (s *Schema) validateRanges() error { + if s.Min != nil && s.Max != nil && *s.Min > *s.Max { + return &errpath.ErrField{Field: "minimum", Err: &errpath.ErrInvalid[float64]{ + Value: *s.Min, + Message: fmt.Sprintf("minimum is greater than maximum (%v > %v)", *s.Min, *s.Max), + }} + } + + if s.MaxLength != nil && s.MinLength > *s.MaxLength { + return &errpath.ErrField{Field: "minLength", Err: &errpath.ErrInvalid[uint]{ + Value: s.MinLength, + Message: fmt.Sprintf("minLength is greater than maxLength (%d > %d)", s.MinLength, *s.MaxLength), + }} + } + + if s.MaxItems != nil && s.MinItems > *s.MaxItems { + return &errpath.ErrField{Field: "minItems", Err: &errpath.ErrInvalid[uint]{ + Value: s.MinItems, + Message: fmt.Sprintf("minItems is greater than maxItems (%d > %d)", s.MinItems, *s.MaxItems), + }} + } + + if s.MaxProperties != nil && s.MinProperties > *s.MaxProperties { + return &errpath.ErrField{Field: "minProperties", Err: &errpath.ErrInvalid[uint]{ + Value: s.MinProperties, + Message: fmt.Sprintf("minProperties is greater than maxProperties (%d > %d)", + s.MinProperties, *s.MaxProperties), + }} + } + + if s.MultipleOf != nil && *s.MultipleOf <= 0 { + return &errpath.ErrField{Field: "multipleOf", Err: &errpath.ErrInvalid[float64]{ + Value: *s.MultipleOf, + Message: "must be greater than zero", + }} + } + + return nil +} + +func (s *Schema) validateProperties() error { + if err := s.Properties.Validate(); err != nil { + return &errpath.ErrField{Field: "properties", Err: err} + } + + if err := s.PatternProperties.Validate(); err != nil { + return &errpath.ErrField{Field: "patternProperties", Err: err} + } + + if err := s.Definitions.Validate(); err != nil { + return &errpath.ErrField{Field: "definitions", Err: err} + } + + // the discriminator MUST be defined at this schema and MUST be in the required property list + if s.Discriminator != "" { + if _, ok := s.Properties[s.Discriminator]; !ok { + return &errpath.ErrField{Field: "discriminator", Err: &errpath.ErrInvalid[string]{ + Value: s.Discriminator, + Message: "property does not exist", + }} + } + + if !slices.Contains(s.Required, s.Discriminator) { + return &errpath.ErrField{Field: "discriminator", Err: &errpath.ErrInvalid[string]{ + Value: s.Discriminator, + Message: "property must be required", + }} + } + } + + return nil +} + +// SortMaps sorts the properties of the schema and of all of its subschemas by key. +func (s *Schema) SortMaps() { + if s == nil { + return + } + + s.Properties.Sort() + s.PatternProperties.Sort() + s.Definitions.Sort() + + for _, ss := range []Schemas{s.Properties, s.PatternProperties, s.Definitions} { + for _, sub := range ss { + sub.Value.SortMaps() + } + } + + for _, ss := range []AnySchemaRefList{s.AllOf, s.OneOf, s.AnyOf} { + for _, sub := range ss { + sub.Value.SortMaps() + } + } + + for _, sub := range s.subSchemas() { + if sub.schema != nil { + sub.schema.Value.SortMaps() + } + } +} + +func (l *loader) collectSchema(s *Schema, ref ref) { + l.collectAnySchemaRefList(s.AllOf, append(ref, "allOf")) + l.collectAnySchemaRefList(s.OneOf, append(ref, "oneOf")) + l.collectAnySchemaRefList(s.AnyOf, append(ref, "anyOf")) + + for _, sub := range s.subSchemas() { + if sub.schema != nil { + l.collectAnySchemaRef(sub.schema, append(ref, sub.field)) + } + } + + l.collectSchemas(s.Properties, append(ref, "properties")) + l.collectSchemas(s.PatternProperties, append(ref, "patternProperties")) + l.collectSchemas(s.Definitions, append(ref, "definitions")) + + if s.ExternalDocs != nil { + l.collectExternalDocsRef(s.ExternalDocs, append(ref, "externalDocs")) + } +} + +func (l *loader) resolveSchema(s *Schema) error { + if err := l.resolveAnySchemaRefList(s.AllOf); err != nil { + return &errpath.ErrField{Field: "allOf", Err: err} + } + + if err := l.resolveAnySchemaRefList(s.OneOf); err != nil { + return &errpath.ErrField{Field: "oneOf", Err: err} + } + + if err := l.resolveAnySchemaRefList(s.AnyOf); err != nil { + return &errpath.ErrField{Field: "anyOf", Err: err} + } + + for _, sub := range s.subSchemas() { + if sub.schema == nil { + continue + } + + if err := l.resolveAnySchemaRef(sub.schema); err != nil { + return &errpath.ErrField{Field: sub.field, Err: err} + } + } + + if err := l.resolveSchemas(s.Properties); err != nil { + return &errpath.ErrField{Field: "properties", Err: err} + } + + if err := l.resolveSchemas(s.PatternProperties); err != nil { + return &errpath.ErrField{Field: "patternProperties", Err: err} + } + + if err := l.resolveSchemas(s.Definitions); err != nil { + return &errpath.ErrField{Field: "definitions", Err: err} + } + + if s.ExternalDocs != nil { + if err := l.resolveExternalDocsRef(s.ExternalDocs); err != nil { + return &errpath.ErrField{Field: "externalDocs", Err: err} + } + } + + return nil +} diff --git a/asyncapi/schema_format.go b/asyncapi/schema_format.go new file mode 100644 index 0000000..4af1ad4 --- /dev/null +++ b/asyncapi/schema_format.go @@ -0,0 +1,67 @@ +package asyncapi + +import ( + "slices" + "strings" +) + +// SchemaFormat is the name of the schema format that is used to define the information +// of a [MultiFormatSchema]. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#multiFormatSchemaObject +type SchemaFormat string + +const ( + // SchemaFormatAsyncAPI is the AsyncAPI 3.1.0 Schema Object format. + // It is the default when a schema format is not provided. + SchemaFormatAsyncAPI SchemaFormat = "application/vnd.aai.asyncapi;version=3.1.0" + // SchemaFormatAsyncAPIJSON is the AsyncAPI 3.1.0 Schema Object format, given as JSON. + SchemaFormatAsyncAPIJSON SchemaFormat = "application/vnd.aai.asyncapi+json;version=3.1.0" + // SchemaFormatAsyncAPIYAML is the AsyncAPI 3.1.0 Schema Object format, given as YAML. + SchemaFormatAsyncAPIYAML SchemaFormat = "application/vnd.aai.asyncapi+yaml;version=3.1.0" + // SchemaFormatJSONSchema is the JSON Schema Draft 07 format. + SchemaFormatJSONSchema SchemaFormat = "application/schema+json;version=draft-07" + // SchemaFormatJSONSchemaYAML is the JSON Schema Draft 07 format, given as YAML. + SchemaFormatJSONSchemaYAML SchemaFormat = "application/schema+yaml;version=draft-07" + // SchemaFormatAvro is the Avro 1.9.0 schema format. + SchemaFormatAvro SchemaFormat = "application/vnd.apache.avro;version=1.9.0" + // SchemaFormatAvroJSON is the Avro 1.9.0 schema format, given as JSON. + SchemaFormatAvroJSON SchemaFormat = "application/vnd.apache.avro+json;version=1.9.0" + // SchemaFormatAvroYAML is the Avro 1.9.0 schema format, given as YAML. + SchemaFormatAvroYAML SchemaFormat = "application/vnd.apache.avro+yaml;version=1.9.0" + // SchemaFormatOpenAPI is the OpenAPI 3.0.0 Schema Object format. + SchemaFormatOpenAPI SchemaFormat = "application/vnd.oai.openapi;version=3.0.0" + // SchemaFormatOpenAPIJSON is the OpenAPI 3.0.0 Schema Object format, given as JSON. + SchemaFormatOpenAPIJSON SchemaFormat = "application/vnd.oai.openapi+json;version=3.0.0" + // SchemaFormatOpenAPIYAML is the OpenAPI 3.0.0 Schema Object format, given as YAML. + SchemaFormatOpenAPIYAML SchemaFormat = "application/vnd.oai.openapi+yaml;version=3.0.0" + // SchemaFormatRAML is the RAML 1.0 data type format. + SchemaFormatRAML SchemaFormat = "application/raml+yaml;version=1.0" + // SchemaFormatProtobuf2 is the Protocol Buffers version 2 format. + SchemaFormatProtobuf2 SchemaFormat = "application/vnd.google.protobuf;version=2" + // SchemaFormatProtobuf3 is the Protocol Buffers version 3 format. + SchemaFormatProtobuf3 SchemaFormat = "application/vnd.google.protobuf;version=3" +) + +// allSchemaFormats are the schema formats listed in the specification. +// Custom values are allowed but their implementation is optional. +var allSchemaFormats = []SchemaFormat{ + SchemaFormatAsyncAPI, SchemaFormatAsyncAPIJSON, SchemaFormatAsyncAPIYAML, + SchemaFormatJSONSchema, SchemaFormatJSONSchemaYAML, + SchemaFormatAvro, SchemaFormatAvroJSON, SchemaFormatAvroYAML, + SchemaFormatOpenAPI, SchemaFormatOpenAPIJSON, SchemaFormatOpenAPIYAML, + SchemaFormatRAML, + SchemaFormatProtobuf2, SchemaFormatProtobuf3, +} + +// IsKnown reports whether the schema format is one of the formats listed in the specification. +// +// Custom values are allowed, so a format that is not known is not necessarily invalid. +func (f SchemaFormat) IsKnown() bool { return slices.Contains(allSchemaFormats, f) } + +// IsAsyncAPI reports whether the schema format denotes an AsyncAPI Schema Object, +// i.e. whether the schema can be parsed into a [Schema]. +func (f SchemaFormat) IsAsyncAPI() bool { + return strings.HasPrefix(string(f), "application/vnd.aai.asyncapi") +} diff --git a/asyncapi/schema_test.go b/asyncapi/schema_test.go new file mode 100644 index 0000000..0d53d17 --- /dev/null +++ b/asyncapi/schema_test.go @@ -0,0 +1,168 @@ +package asyncapi_test + +import ( + "testing" + + "github.com/MarkRosemaker/asyncapi" +) + +func TestSchema_Validate_Errors(t *testing.T) { + t.Parallel() + + for name, tc := range map[string]struct { + schema *asyncapi.Schema + want string + }{ + "unknown type": { + &asyncapi.Schema{Type: asyncapi.DataTypes{"struct"}}, + `type ("struct") is invalid, must be one of: "integer", "number", "string", ` + + `"array", "boolean", "object", "null"`, + }, + "unknown type among others": { + &asyncapi.Schema{Type: asyncapi.DataTypes{asyncapi.TypeString, "struct"}}, + `type[1] ("struct") is invalid, must be one of: "integer", "number", "string", ` + + `"array", "boolean", "object", "null"`, + }, + "minimum greater than maximum": { + &asyncapi.Schema{ + Type: asyncapi.DataTypes{asyncapi.TypeInteger}, + Min: ptr(10.0), Max: ptr(5.0), + }, + "minimum (10) is invalid: minimum is greater than maximum (10 > 5)", + }, + "minLength greater than maxLength": { + &asyncapi.Schema{ + Type: asyncapi.DataTypes{asyncapi.TypeString}, + MinLength: 10, MaxLength: ptr(uint(5)), + }, + "minLength (10) is invalid: minLength is greater than maxLength (10 > 5)", + }, + "minItems greater than maxItems": { + &asyncapi.Schema{ + Type: asyncapi.DataTypes{asyncapi.TypeArray}, + MinItems: 10, MaxItems: ptr(uint(5)), + }, + "minItems (10) is invalid: minItems is greater than maxItems (10 > 5)", + }, + "minProperties greater than maxProperties": { + &asyncapi.Schema{ + Type: asyncapi.DataTypes{asyncapi.TypeObject}, + MinProperties: 10, MaxProperties: ptr(uint(5)), + }, + "minProperties (10) is invalid: minProperties is greater than maxProperties (10 > 5)", + }, + "multipleOf zero": { + &asyncapi.Schema{ + Type: asyncapi.DataTypes{asyncapi.TypeNumber}, + MultipleOf: ptr(0.0), + }, + "multipleOf (0) is invalid: must be greater than zero", + }, + "discriminator that is not a property": { + &asyncapi.Schema{ + Type: asyncapi.DataTypes{asyncapi.TypeObject}, + Discriminator: "petType", + }, + `discriminator ("petType") is invalid: property does not exist`, + }, + "discriminator that is not required": { + &asyncapi.Schema{ + Type: asyncapi.DataTypes{asyncapi.TypeObject}, + Properties: asyncapi.Schemas{ + "petType": {Value: &asyncapi.AnySchema{Schema: &asyncapi.Schema{ + Type: asyncapi.DataTypes{asyncapi.TypeString}, + }}}, + }, + Discriminator: "petType", + }, + `discriminator ("petType") is invalid: property must be required`, + }, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + + err := tc.schema.Validate() + if err == nil { + t.Fatal("expected error") + } + + if err.Error() != tc.want { + t.Fatalf("got: %v, want: %v", err, tc.want) + } + }) + } +} + +func TestSchema_Validate_Discriminator(t *testing.T) { + t.Parallel() + + s := &asyncapi.Schema{ + Type: asyncapi.DataTypes{asyncapi.TypeObject}, + Properties: asyncapi.Schemas{ + "petType": {Value: &asyncapi.AnySchema{Schema: &asyncapi.Schema{ + Type: asyncapi.DataTypes{asyncapi.TypeString}, + }}}, + }, + Required: []string{"petType"}, + Discriminator: "petType", + } + + if err := s.Validate(); err != nil { + t.Fatal(err) + } +} + +func TestSchema_Composition(t *testing.T) { + t.Parallel() + + doc, err := asyncapi.LoadFromFile("examples/v3.1/anyof.yaml") + if err != nil { + t.Fatal(err) + } + + if err := doc.Validate(); err != nil { + t.Fatal(err) + } + + payload := doc.Components.Messages["testMessages"].Value.Payload.Value.Schema + if got, want := len(payload.AnyOf), 2; got != want { + t.Fatalf("got: %d schemas, want: %d", got, want) + } + + // the references of the composition were resolved + if payload.AnyOf[0].Value != doc.Components.Schemas["objectWithKey"].Value { + t.Fatal("the first schema was not resolved") + } + + if payload.AnyOf[1].Value != doc.Components.Schemas["objectWithKey2"].Value { + t.Fatal("the second schema was not resolved") + } +} + +func TestSchema_SortMaps(t *testing.T) { + t.Parallel() + + s := &asyncapi.Schema{ + Type: asyncapi.DataTypes{asyncapi.TypeObject}, + Properties: asyncapi.Schemas{ + "c": {Value: &asyncapi.AnySchema{Schema: &asyncapi.Schema{}}}, + "a": {Value: &asyncapi.AnySchema{Schema: &asyncapi.Schema{}}}, + "b": {Value: &asyncapi.AnySchema{Schema: &asyncapi.Schema{}}}, + }, + } + + s.SortMaps() + + want := []string{"a", "b", "c"} + + i := 0 + for name := range s.Properties.ByIndex() { + if name != want[i] { + t.Fatalf("got: %v, want: %v", name, want[i]) + } + + i++ + } +} + +func ptr[T any](v T) *T { return &v } diff --git a/asyncapi/schemas.go b/asyncapi/schemas.go new file mode 100644 index 0000000..5957161 --- /dev/null +++ b/asyncapi/schemas.go @@ -0,0 +1,73 @@ +package asyncapi + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "iter" + + "github.com/MarkRosemaker/errpath" + "github.com/MarkRosemaker/ordmap" +) + +// Schemas is a map of schema definitions, each of which is either a schema object, +// a multi format schema object or a reference to one of them. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#componentsSchemas +type Schemas map[string]*AnySchemaRef + +// Validate validates each schema. +func (ss Schemas) Validate() error { + for name, s := range ss.ByIndex() { + if err := s.Validate(); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} + +// ByIndex returns a sequence of key-value pairs ordered by index. +func (ss Schemas) ByIndex() iter.Seq2[string, *AnySchemaRef] { + return ordmap.ByIndex(ss, getIndexRef[AnySchema, *AnySchema]) +} + +// Sort sorts the map by key and sets the indices accordingly. +func (ss Schemas) Sort() { + ordmap.Sort(ss, setIndexRef[AnySchema, *AnySchema]) +} + +// Set sets a value in the map, adding it at the end of the order. +func (ss *Schemas) Set(key string, s *AnySchemaRef) { + ordmap.Set(ss, key, s, getIndexRef[AnySchema, *AnySchema], setIndexRef[AnySchema, *AnySchema]) +} + +var _ json.MarshalerTo = (*Schemas)(nil) + +// MarshalJSONTo marshals the key-value pairs in order. +func (ss *Schemas) MarshalJSONTo(enc *jsontext.Encoder) error { + return ordmap.MarshalJSONTo(ss, enc) +} + +var _ json.UnmarshalerFrom = (*Schemas)(nil) + +// UnmarshalJSONFrom unmarshals the key-value pairs in order and sets the indices. +func (ss *Schemas) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + return ordmap.UnmarshalJSONFrom(ss, dec, setIndexRef[AnySchema, *AnySchema]) +} + +func (l *loader) collectSchemas(ss Schemas, ref ref) { + for name, s := range ss.ByIndex() { + l.collectAnySchemaRef(s, append(ref, name)) + } +} + +func (l *loader) resolveSchemas(ss Schemas) error { + for name, s := range ss.ByIndex() { + if err := l.resolveAnySchemaRef(s); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} diff --git a/asyncapi/security_scheme.go b/asyncapi/security_scheme.go new file mode 100644 index 0000000..954a197 --- /dev/null +++ b/asyncapi/security_scheme.go @@ -0,0 +1,150 @@ +package asyncapi + +import ( + "net/url" + "strings" + + "github.com/MarkRosemaker/errpath" +) + +// SecurityScheme defines a security scheme that can be used by the operations. +// +// Supported schemes are: +// - User/Password. +// - API key (either as user or as password). +// - X.509 certificate. +// - End-to-end encryption (either symmetric or asymmetric). +// - HTTP authentication. +// - HTTP API key. +// - OAuth2's common flows (Implicit, Resource Owner Protected Credentials, Client Credentials and Authorization Code) as defined in [RFC6749]. +// - [OpenID Connect Discovery]. +// - SASL (Simple Authentication and Security Layer) as defined in [RFC4422]. +// +// ([Specification]) +// +// [RFC6749]: https://tools.ietf.org/html/rfc6749 +// [OpenID Connect Discovery]: https://tools.ietf.org/html/draft-ietf-oauth-discovery-06 +// [RFC4422]: https://tools.ietf.org/html/rfc4422 +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#securitySchemeObject +type SecurityScheme struct { + // REQUIRED. The type of the security scheme. + Type SecuritySchemeType `json:"type" yaml:"type"` + // A short description for security scheme. CommonMark syntax MAY be used for rich text representation. + Description string `json:"description,omitempty" yaml:"description,omitempty"` + // REQUIRED for `httpApiKey`. The name of the header, query or cookie parameter to be used. + Name string `json:"name,omitempty" yaml:"name,omitempty"` + // REQUIRED for `apiKey` and `httpApiKey`. The location of the API key. + // Valid values are `user` and `password` for `apiKey` + // and `query`, `header` or `cookie` for `httpApiKey`. + In SecuritySchemeIn `json:"in,omitempty" yaml:"in,omitempty"` + // REQUIRED for `http`. The name of the HTTP Authorization scheme to be used in the Authorization header as defined in RFC7235. + Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"` + // A hint to the client to identify how the bearer token is formatted, e.g. "jwt". + // Bearer tokens are usually generated by an authorization server, so this information is primarily for documentation purposes. + BearerFormat string `json:"bearerFormat,omitempty" yaml:"bearerFormat,omitempty"` + // REQUIRED for `oauth2`. An object containing configuration information for the flow types supported. + Flows *OAuthFlows `json:"flows,omitempty" yaml:"flows,omitempty"` + // REQUIRED for `openIdConnect`. OpenId Connect URL to discover OAuth2 configuration values. + // This MUST be in the form of an absolute URL. + OpenIDConnectURL *url.URL `json:"openIdConnectUrl,omitempty" yaml:"openIdConnectUrl,omitempty"` + // List of the needed scope names. An empty array means no scopes are needed. + // Only valid for `oauth2` and `openIdConnect`. + Scopes []string `json:"scopes,omitempty" yaml:"scopes,omitempty"` + // This object MAY be extended with Specification Extensions. + Extensions Extensions `json:",inline" yaml:",inline"` +} + +const ( + // SecuritySchemeBearer is the value of SecurityScheme.Scheme for bearer tokens. + SecuritySchemeBearer = "bearer" + // SecuritySchemeBasic is the value of SecurityScheme.Scheme for basic authentication. + SecuritySchemeBasic = "basic" +) + +// Validate checks the security scheme for correctness. +func (s *SecurityScheme) Validate() error { + if s.Type == "" { + return &errpath.ErrField{Field: "type", Err: &errpath.ErrRequired{}} + } + + if err := s.Type.Validate(); err != nil { + return &errpath.ErrField{Field: "type", Err: err} + } + + s.Description = strings.TrimSpace(s.Description) + + switch s.Type { + case SecuritySchemeTypeAPIKey: + if s.In == "" { + return &errpath.ErrField{Field: "in", Err: &errpath.ErrRequired{}} + } + + if err := s.In.validate(allSecuritySchemeInAPIKey); err != nil { + return &errpath.ErrField{Field: "in", Err: err} + } + case SecuritySchemeTypeHTTPAPIKey: + if s.Name == "" { + return &errpath.ErrField{Field: "name", Err: &errpath.ErrRequired{}} + } + + if s.In == "" { + return &errpath.ErrField{Field: "in", Err: &errpath.ErrRequired{}} + } + + if err := s.In.validate(allSecuritySchemeInHTTPAPIKey); err != nil { + return &errpath.ErrField{Field: "in", Err: err} + } + case SecuritySchemeTypeHTTP: + if s.Scheme == "" { + return &errpath.ErrField{Field: "scheme", Err: &errpath.ErrRequired{}} + } + + if SecuritySchemeBearer == strings.ToLower(s.Scheme) { + s.Scheme = SecuritySchemeBearer // unify + } + case SecuritySchemeTypeOAuth2: + if s.Flows == nil { + return &errpath.ErrField{Field: "flows", Err: &errpath.ErrRequired{}} + } + + if err := s.Flows.Validate(); err != nil { + return &errpath.ErrField{Field: "flows", Err: err} + } + case SecuritySchemeTypeOpenIDConnect: + if s.OpenIDConnectURL == nil { + return &errpath.ErrField{Field: "openIdConnectUrl", Err: &errpath.ErrRequired{}} + } + + if err := validateURL(s.OpenIDConnectURL); err != nil { + return &errpath.ErrField{Field: "openIdConnectUrl", Err: err} + } + default: // no additional fields are required + } + + // scopes are only valid for oauth2 and openIdConnect + switch s.Type { + case SecuritySchemeTypeOAuth2, SecuritySchemeTypeOpenIDConnect: + default: + if s.Scopes != nil { + return &errpath.ErrField{Field: "scopes", Err: &errpath.ErrInvalid[string]{ + Message: `only valid for the types "oauth2" and "openIdConnect"`, + }} + } + } + + return validateExtensions(s.Extensions) +} + +func (l *loader) collectSecuritySchemeRefList(ss SecuritySchemeRefList, ref ref) { + for i, s := range ss { + l.collectSecuritySchemeRef(s, append(ref, itoa(i))) + } +} + +func (l *loader) collectSecuritySchemeRef(s *SecuritySchemeRef, ref ref) { + collectRef(l, s, l.securitySchemes, ref) +} + +func (l *loader) resolveSecuritySchemeRef(s *SecuritySchemeRef) error { + return resolveRef(s, l.securitySchemes, nil) +} diff --git a/asyncapi/security_scheme_in.go b/asyncapi/security_scheme_in.go new file mode 100644 index 0000000..2aad0eb --- /dev/null +++ b/asyncapi/security_scheme_in.go @@ -0,0 +1,52 @@ +package asyncapi + +import ( + "slices" + + "github.com/MarkRosemaker/errpath" +) + +// SecuritySchemeIn is "the location of the API key. Valid values are `user` and `password` +// for `apiKey` and `query`, `header` or `cookie` for `httpApiKey`." +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#securitySchemeObject +type SecuritySchemeIn string + +const ( + // SecuritySchemeInUser is the location of an API key that is sent as the user of a connection. + SecuritySchemeInUser SecuritySchemeIn = "user" + // SecuritySchemeInPassword is the location of an API key that is sent as the password of a connection. + SecuritySchemeInPassword SecuritySchemeIn = "password" + // SecuritySchemeInQuery is the location of an API key that is sent as a query parameter. + SecuritySchemeInQuery SecuritySchemeIn = "query" + // SecuritySchemeInHeader is the location of an API key that is sent as a header. + SecuritySchemeInHeader SecuritySchemeIn = "header" + // SecuritySchemeInCookie is the location of an API key that is sent as a cookie. + SecuritySchemeInCookie SecuritySchemeIn = "cookie" +) + +// allSecuritySchemeInAPIKey are the valid locations for a security scheme of type apiKey. +var allSecuritySchemeInAPIKey = []SecuritySchemeIn{ + SecuritySchemeInUser, + SecuritySchemeInPassword, +} + +// allSecuritySchemeInHTTPAPIKey are the valid locations for a security scheme of type httpApiKey. +var allSecuritySchemeInHTTPAPIKey = []SecuritySchemeIn{ + SecuritySchemeInQuery, + SecuritySchemeInHeader, + SecuritySchemeInCookie, +} + +// validate checks that the location is one of the given valid locations. +func (in SecuritySchemeIn) validate(valid []SecuritySchemeIn) error { + if slices.Contains(valid, in) { + return nil + } + + return &errpath.ErrInvalid[SecuritySchemeIn]{ + Value: in, + Enum: valid, + } +} diff --git a/asyncapi/security_scheme_test.go b/asyncapi/security_scheme_test.go new file mode 100644 index 0000000..59003ba --- /dev/null +++ b/asyncapi/security_scheme_test.go @@ -0,0 +1,168 @@ +package asyncapi_test + +import ( + "net/url" + "testing" + + "github.com/MarkRosemaker/asyncapi" +) + +func TestSecurityScheme_Validate(t *testing.T) { + t.Parallel() + + for name, s := range map[string]*asyncapi.SecurityScheme{ + "user/password": {Type: asyncapi.SecuritySchemeTypeUserPassword}, + "api key": { + Type: asyncapi.SecuritySchemeTypeAPIKey, + In: asyncapi.SecuritySchemeInUser, + }, + "X.509": {Type: asyncapi.SecuritySchemeTypeX509}, + "http api key": { + Type: asyncapi.SecuritySchemeTypeHTTPAPIKey, + Name: "api_key", + In: asyncapi.SecuritySchemeInHeader, + }, + "http": {Type: asyncapi.SecuritySchemeTypeHTTP, Scheme: "basic"}, + "oauth2": { + Type: asyncapi.SecuritySchemeTypeOAuth2, + Flows: &asyncapi.OAuthFlows{ + ClientCredentials: &asyncapi.OAuthFlowClientCredentials{ + TokenURL: mustParseURL("https://example.com/api/oauth/token"), + AvailableScopes: asyncapi.MapOfStrings{}, + }, + }, + Scopes: []string{}, + }, + "openid connect": { + Type: asyncapi.SecuritySchemeTypeOpenIDConnect, + OpenIDConnectURL: mustParseURL("https://example.com/.well-known/openid-configuration"), + }, + "sasl": {Type: asyncapi.SecuritySchemeTypeScramSha256}, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + + if err := s.Validate(); err != nil { + t.Fatal(err) + } + }) + } +} + +func TestSecurityScheme_Validate_Errors(t *testing.T) { + t.Parallel() + + for name, tc := range map[string]struct { + scheme *asyncapi.SecurityScheme + want string + }{ + "no type": { + &asyncapi.SecurityScheme{}, + "type is required", + }, + "unknown type": { + &asyncapi.SecurityScheme{Type: "carrierPigeon"}, + `type ("carrierPigeon") is invalid, must be one of: "userPassword", "apiKey", ` + + `"X509", "symmetricEncryption", "asymmetricEncryption", "httpApiKey", "http", ` + + `"oauth2", "openIdConnect", "plain", "scramSha256", "scramSha512", "gssapi"`, + }, + "api key without a location": { + &asyncapi.SecurityScheme{Type: asyncapi.SecuritySchemeTypeAPIKey}, + "in is required", + }, + "api key in the wrong location": { + &asyncapi.SecurityScheme{ + Type: asyncapi.SecuritySchemeTypeAPIKey, + In: asyncapi.SecuritySchemeInHeader, + }, + `in ("header") is invalid, must be one of: "user", "password"`, + }, + "http api key without a name": { + &asyncapi.SecurityScheme{ + Type: asyncapi.SecuritySchemeTypeHTTPAPIKey, + In: asyncapi.SecuritySchemeInHeader, + }, + "name is required", + }, + "http api key in the wrong location": { + &asyncapi.SecurityScheme{ + Type: asyncapi.SecuritySchemeTypeHTTPAPIKey, + Name: "api_key", + In: asyncapi.SecuritySchemeInUser, + }, + `in ("user") is invalid, must be one of: "query", "header", "cookie"`, + }, + "http without a scheme": { + &asyncapi.SecurityScheme{Type: asyncapi.SecuritySchemeTypeHTTP}, + "scheme is required", + }, + "oauth2 without flows": { + &asyncapi.SecurityScheme{Type: asyncapi.SecuritySchemeTypeOAuth2}, + "flows is required", + }, + "oauth2 flow without scopes": { + &asyncapi.SecurityScheme{ + Type: asyncapi.SecuritySchemeTypeOAuth2, + Flows: &asyncapi.OAuthFlows{ + Implicit: &asyncapi.OAuthFlowImplicit{ + AuthorizationURL: mustParseURL("https://example.com/api/oauth/dialog"), + }, + }, + }, + "flows.implicit.availableScopes is required", + }, + "openid connect without a URL": { + &asyncapi.SecurityScheme{Type: asyncapi.SecuritySchemeTypeOpenIDConnect}, + "openIdConnectUrl is required", + }, + "scopes of a scheme that has none": { + &asyncapi.SecurityScheme{ + Type: asyncapi.SecuritySchemeTypeX509, + Scopes: []string{"read"}, + }, + `scopes is invalid: only valid for the types "oauth2" and "openIdConnect"`, + }, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + + err := tc.scheme.Validate() + if err == nil { + t.Fatal("expected error") + } + + if err.Error() != tc.want { + t.Fatalf("got: %v, want: %v", err, tc.want) + } + }) + } +} + +func TestSecurityScheme_UnifyBearer(t *testing.T) { + t.Parallel() + + s := &asyncapi.SecurityScheme{Type: asyncapi.SecuritySchemeTypeHTTP, Scheme: "Bearer"} + if err := s.Validate(); err != nil { + t.Fatal(err) + } + + if got, want := s.Scheme, asyncapi.SecuritySchemeBearer; got != want { + t.Fatalf("got: %v, want: %v", got, want) + } +} + +func TestSecurityScheme_FixScheme(t *testing.T) { + t.Parallel() + + s := &asyncapi.SecurityScheme{ + Type: asyncapi.SecuritySchemeTypeOpenIDConnect, + OpenIDConnectURL: &url.URL{Host: "example.com", Path: "/.well-known/openid-configuration"}, + } + if err := s.Validate(); err != nil { + t.Fatal(err) + } + + if got, want := s.OpenIDConnectURL.Scheme, "https"; got != want { + t.Fatalf("got: %v, want: %v", got, want) + } +} diff --git a/asyncapi/security_scheme_type.go b/asyncapi/security_scheme_type.go new file mode 100644 index 0000000..5f0f812 --- /dev/null +++ b/asyncapi/security_scheme_type.go @@ -0,0 +1,70 @@ +package asyncapi + +import ( + "slices" + + "github.com/MarkRosemaker/errpath" +) + +// SecuritySchemeType is the type of a security scheme. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#securitySchemeObject +type SecuritySchemeType string + +const ( + // SecuritySchemeTypeUserPassword is the user/password authentication. + SecuritySchemeTypeUserPassword SecuritySchemeType = "userPassword" + // SecuritySchemeTypeAPIKey is an API key, either as user or as password. + SecuritySchemeTypeAPIKey SecuritySchemeType = "apiKey" + // SecuritySchemeTypeX509 is an X.509 certificate. + SecuritySchemeTypeX509 SecuritySchemeType = "X509" + // SecuritySchemeTypeSymmetricEncryption is a symmetric end-to-end encryption. + SecuritySchemeTypeSymmetricEncryption SecuritySchemeType = "symmetricEncryption" + // SecuritySchemeTypeAsymmetricEncryption is an asymmetric end-to-end encryption. + SecuritySchemeTypeAsymmetricEncryption SecuritySchemeType = "asymmetricEncryption" + // SecuritySchemeTypeHTTPAPIKey is an API key that is sent as an HTTP header, query or cookie parameter. + SecuritySchemeTypeHTTPAPIKey SecuritySchemeType = "httpApiKey" + // SecuritySchemeTypeHTTP is an HTTP authentication. + SecuritySchemeTypeHTTP SecuritySchemeType = "http" + // SecuritySchemeTypeOAuth2 is one of OAuth2's common flows. + SecuritySchemeTypeOAuth2 SecuritySchemeType = "oauth2" + // SecuritySchemeTypeOpenIDConnect is OpenID Connect Discovery. + SecuritySchemeTypeOpenIDConnect SecuritySchemeType = "openIdConnect" + // SecuritySchemeTypePlain is the SASL PLAIN mechanism. + SecuritySchemeTypePlain SecuritySchemeType = "plain" + // SecuritySchemeTypeScramSha256 is the SASL SCRAM-SHA-256 mechanism. + SecuritySchemeTypeScramSha256 SecuritySchemeType = "scramSha256" + // SecuritySchemeTypeScramSha512 is the SASL SCRAM-SHA-512 mechanism. + SecuritySchemeTypeScramSha512 SecuritySchemeType = "scramSha512" + // SecuritySchemeTypeGSSAPI is the SASL GSSAPI mechanism. + SecuritySchemeTypeGSSAPI SecuritySchemeType = "gssapi" +) + +var allSecuritySchemeTypes = []SecuritySchemeType{ + SecuritySchemeTypeUserPassword, + SecuritySchemeTypeAPIKey, + SecuritySchemeTypeX509, + SecuritySchemeTypeSymmetricEncryption, + SecuritySchemeTypeAsymmetricEncryption, + SecuritySchemeTypeHTTPAPIKey, + SecuritySchemeTypeHTTP, + SecuritySchemeTypeOAuth2, + SecuritySchemeTypeOpenIDConnect, + SecuritySchemeTypePlain, + SecuritySchemeTypeScramSha256, + SecuritySchemeTypeScramSha512, + SecuritySchemeTypeGSSAPI, +} + +// Validate validates the security scheme type. +func (tp SecuritySchemeType) Validate() error { + if slices.Contains(allSecuritySchemeTypes, tp) { + return nil + } + + return &errpath.ErrInvalid[SecuritySchemeType]{ + Value: tp, + Enum: allSecuritySchemeTypes, + } +} diff --git a/asyncapi/security_schemes.go b/asyncapi/security_schemes.go new file mode 100644 index 0000000..7b1cb04 --- /dev/null +++ b/asyncapi/security_schemes.go @@ -0,0 +1,72 @@ +package asyncapi + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "iter" + + "github.com/MarkRosemaker/errpath" + "github.com/MarkRosemaker/ordmap" +) + +// SecuritySchemes is a map of Security Scheme Objects. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#componentsSecuritySchemes +type SecuritySchemes map[string]*SecuritySchemeRef + +// Validate validates each security scheme. +func (ss SecuritySchemes) Validate() error { + for name, s := range ss.ByIndex() { + if err := s.Validate(); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} + +// ByIndex returns a sequence of key-value pairs ordered by index. +func (ss SecuritySchemes) ByIndex() iter.Seq2[string, *SecuritySchemeRef] { + return ordmap.ByIndex(ss, getIndexRef[SecurityScheme, *SecurityScheme]) +} + +// Sort sorts the map by key and sets the indices accordingly. +func (ss SecuritySchemes) Sort() { + ordmap.Sort(ss, setIndexRef[SecurityScheme, *SecurityScheme]) +} + +// Set sets a value in the map, adding it at the end of the order. +func (ss *SecuritySchemes) Set(key string, s *SecuritySchemeRef) { + ordmap.Set(ss, key, s, getIndexRef[SecurityScheme, *SecurityScheme], setIndexRef[SecurityScheme, *SecurityScheme]) +} + +var _ json.MarshalerTo = (*SecuritySchemes)(nil) + +// MarshalJSONTo marshals the key-value pairs in order. +func (ss *SecuritySchemes) MarshalJSONTo(enc *jsontext.Encoder) error { + return ordmap.MarshalJSONTo(ss, enc) +} + +var _ json.UnmarshalerFrom = (*SecuritySchemes)(nil) + +// UnmarshalJSONFrom unmarshals the key-value pairs in order and sets the indices. +func (ss *SecuritySchemes) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + return ordmap.UnmarshalJSONFrom(ss, dec, setIndexRef[SecurityScheme, *SecurityScheme]) +} + +func (l *loader) collectSecuritySchemes(ss SecuritySchemes, ref ref) { + for name, s := range ss.ByIndex() { + l.collectSecuritySchemeRef(s, append(ref, name)) + } +} + +func (l *loader) resolveSecuritySchemes(ss SecuritySchemes) error { + for name, s := range ss.ByIndex() { + if err := l.resolveSecuritySchemeRef(s); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} diff --git a/asyncapi/server.go b/asyncapi/server.go new file mode 100644 index 0000000..9ea6c00 --- /dev/null +++ b/asyncapi/server.go @@ -0,0 +1,139 @@ +package asyncapi + +import ( + "strings" + + "github.com/MarkRosemaker/errpath" +) + +// Server is an object representing a message broker, a server or any other kind of computer program capable of sending and/or receiving data. +// This object is used to capture details such as URIs, protocols and security configuration. +// Variable substitution can be used so that some details, for example usernames and passwords, can be injected by code generation tools. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#serverObject +type Server struct { + // REQUIRED. The server host name. It MAY include the port. + // This field supports Server Variables. Variable substitutions will be made when a variable is named in {braces}. + Host string `json:"host" yaml:"host"` + // REQUIRED. The protocol this server supports for connection. + Protocol Protocol `json:"protocol" yaml:"protocol"` + // The version of the protocol used for connection. For instance: AMQP 0.9.1, HTTP 2.0, Kafka 1.0.0, etc. + ProtocolVersion string `json:"protocolVersion,omitempty" yaml:"protocolVersion,omitempty"` + // The path to a resource in the host. + // This field supports Server Variables. Variable substitutions will be made when a variable is named in {braces}. + Pathname string `json:"pathname,omitempty" yaml:"pathname,omitempty"` + // An optional string describing the server. CommonMark syntax MAY be used for rich text representation. + Description string `json:"description,omitempty" yaml:"description,omitempty"` + // A human-friendly title for the server. + Title string `json:"title,omitempty" yaml:"title,omitempty"` + // A short summary of the server. + Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` + // A map between a variable name and its value. The value is used for substitution in the server's host and pathname template. + Variables ServerVariables `json:"variables,omitempty" yaml:"variables,omitempty"` + // A declaration of which security schemes can be used with this server. + // The list of values includes alternative security scheme objects that can be used. + // Only one of the security scheme objects need to be satisfied to authorize a connection or operation. + Security SecuritySchemeRefList `json:"security,omitempty" yaml:"security,omitempty"` + // A list of tags for logical grouping and categorization of servers. + Tags Tags `json:"tags,omitempty" yaml:"tags,omitempty"` + // Additional external documentation for this server. + ExternalDocs *ExternalDocsRef `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + // A map where the keys describe the name of the protocol and the values describe protocol-specific definitions for the server. + Bindings *BindingsRef `json:"bindings,omitempty" yaml:"bindings,omitempty"` + // This object MAY be extended with Specification Extensions. + Extensions Extensions `json:",inline" yaml:",inline"` +} + +// Validate checks the server for correctness. +func (s *Server) Validate() error { + if s.Host == "" { + return &errpath.ErrField{Field: "host", Err: &errpath.ErrRequired{}} + } + + if s.Protocol == "" { + return &errpath.ErrField{Field: "protocol", Err: &errpath.ErrRequired{}} + } + + s.Description = strings.TrimSpace(s.Description) + + if err := s.Variables.Validate(); err != nil { + return &errpath.ErrField{Field: "variables", Err: err} + } + + if err := s.Security.Validate(); err != nil { + return &errpath.ErrField{Field: "security", Err: err} + } + + if err := s.Tags.Validate(); err != nil { + return &errpath.ErrField{Field: "tags", Err: err} + } + + if s.ExternalDocs != nil { + if err := s.ExternalDocs.Validate(); err != nil { + return &errpath.ErrField{Field: "externalDocs", Err: err} + } + } + + if s.Bindings != nil { + if err := s.Bindings.Validate(); err != nil { + return &errpath.ErrField{Field: "bindings", Err: err} + } + } + + return validateExtensions(s.Extensions) +} + +func (l *loader) collectServerRef(s *ServerRef, ref ref) { + if !collectRef(l, s, l.servers, ref) { + return + } + + l.collectServer(s.Value, ref) +} + +func (l *loader) collectServer(s *Server, ref ref) { + l.collectServerVariables(s.Variables, append(ref, "variables")) + l.collectSecuritySchemeRefList(s.Security, append(ref, "security")) + l.collectTags(s.Tags, append(ref, "tags")) + + if s.ExternalDocs != nil { + l.collectExternalDocsRef(s.ExternalDocs, append(ref, "externalDocs")) + } + + if s.Bindings != nil { + l.collectBindingsRef(s.Bindings, append(ref, "bindings")) + } +} + +func (l *loader) resolveServerRef(s *ServerRef) error { + return resolveRef(s, l.servers, l.resolveServer) +} + +func (l *loader) resolveServer(s *Server) error { + if err := l.resolveServerVariables(s.Variables); err != nil { + return &errpath.ErrField{Field: "variables", Err: err} + } + + if err := l.resolveSecuritySchemeRefList(s.Security); err != nil { + return &errpath.ErrField{Field: "security", Err: err} + } + + if err := l.resolveTags(s.Tags); err != nil { + return &errpath.ErrField{Field: "tags", Err: err} + } + + if s.ExternalDocs != nil { + if err := l.resolveExternalDocsRef(s.ExternalDocs); err != nil { + return &errpath.ErrField{Field: "externalDocs", Err: err} + } + } + + if s.Bindings != nil { + if err := l.resolveBindingsRef(s.Bindings); err != nil { + return &errpath.ErrField{Field: "bindings", Err: err} + } + } + + return nil +} diff --git a/asyncapi/server_variable.go b/asyncapi/server_variable.go new file mode 100644 index 0000000..6777c6f --- /dev/null +++ b/asyncapi/server_variable.go @@ -0,0 +1,50 @@ +package asyncapi + +import ( + "errors" + "fmt" + "slices" + "strings" +) + +// ServerVariable is an object representing a Server Variable for server URL template substitution. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#serverVariableObject +type ServerVariable struct { + // An enumeration of string values to be used if the substitution options are from a limited set. + Enum []string `json:"enum,omitempty" yaml:"enum,omitempty"` + // The default value to use for substitution, and to send, if an alternate value is not supplied. + Default string `json:"default,omitempty" yaml:"default,omitempty"` + // An optional description for the server variable. CommonMark syntax MAY be used for rich text representation. + Description string `json:"description,omitempty" yaml:"description,omitempty"` + // An array of examples of the server variable. + Examples []string `json:"examples,omitempty" yaml:"examples,omitempty"` + // This object MAY be extended with Specification Extensions. + Extensions Extensions `json:",inline" yaml:",inline"` +} + +// Validate checks the server variable for correctness. +func (v *ServerVariable) Validate() error { + // either the array has entries or it is not defined + if v.Enum != nil && len(v.Enum) == 0 { + return errors.New("enum array must not be empty") + } + + // if the enum is defined, the default value MUST exist in the enum's values + if len(v.Enum) > 0 && v.Default != "" && !slices.Contains(v.Enum, v.Default) { + return fmt.Errorf("default value %q must exist in the enum's values", v.Default) + } + + v.Description = strings.TrimSpace(v.Description) + + return validateExtensions(v.Extensions) +} + +func (l *loader) collectServerVariableRef(v *ServerVariableRef, ref ref) { + collectRef(l, v, l.serverVariables, ref) +} + +func (l *loader) resolveServerVariableRef(v *ServerVariableRef) error { + return resolveRef(v, l.serverVariables, nil) +} diff --git a/asyncapi/server_variables.go b/asyncapi/server_variables.go new file mode 100644 index 0000000..1d14cdc --- /dev/null +++ b/asyncapi/server_variables.go @@ -0,0 +1,73 @@ +package asyncapi + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "iter" + + "github.com/MarkRosemaker/errpath" + "github.com/MarkRosemaker/ordmap" +) + +// ServerVariables is a map between a variable name and its value. +// The value is used for substitution in the server's host and pathname template. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#serverObjectVariables +type ServerVariables map[string]*ServerVariableRef + +// Validate validates each server variable. +func (vars ServerVariables) Validate() error { + for name, v := range vars.ByIndex() { + if err := v.Validate(); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} + +// ByIndex returns a sequence of key-value pairs ordered by index. +func (vars ServerVariables) ByIndex() iter.Seq2[string, *ServerVariableRef] { + return ordmap.ByIndex(vars, getIndexRef[ServerVariable, *ServerVariable]) +} + +// Sort sorts the map by key and sets the indices accordingly. +func (vars ServerVariables) Sort() { + ordmap.Sort(vars, setIndexRef[ServerVariable, *ServerVariable]) +} + +// Set sets a value in the map, adding it at the end of the order. +func (vars *ServerVariables) Set(key string, v *ServerVariableRef) { + ordmap.Set(vars, key, v, getIndexRef[ServerVariable, *ServerVariable], setIndexRef[ServerVariable, *ServerVariable]) +} + +var _ json.MarshalerTo = (*ServerVariables)(nil) + +// MarshalJSONTo marshals the key-value pairs in order. +func (vars *ServerVariables) MarshalJSONTo(enc *jsontext.Encoder) error { + return ordmap.MarshalJSONTo(vars, enc) +} + +var _ json.UnmarshalerFrom = (*ServerVariables)(nil) + +// UnmarshalJSONFrom unmarshals the key-value pairs in order and sets the indices. +func (vars *ServerVariables) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + return ordmap.UnmarshalJSONFrom(vars, dec, setIndexRef[ServerVariable, *ServerVariable]) +} + +func (l *loader) collectServerVariables(vars ServerVariables, ref ref) { + for name, v := range vars.ByIndex() { + l.collectServerVariableRef(v, append(ref, name)) + } +} + +func (l *loader) resolveServerVariables(vars ServerVariables) error { + for name, v := range vars.ByIndex() { + if err := l.resolveServerVariableRef(v); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} diff --git a/asyncapi/servers.go b/asyncapi/servers.go new file mode 100644 index 0000000..047cc2a --- /dev/null +++ b/asyncapi/servers.go @@ -0,0 +1,72 @@ +package asyncapi + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "iter" + + "github.com/MarkRosemaker/errpath" + "github.com/MarkRosemaker/ordmap" +) + +// Servers is a map of Server Objects. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#serversObject +type Servers map[string]*ServerRef + +// Validate validates each server. +func (ss Servers) Validate() error { + for name, s := range ss.ByIndex() { + if err := s.Validate(); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} + +// ByIndex returns a sequence of key-value pairs ordered by index. +func (ss Servers) ByIndex() iter.Seq2[string, *ServerRef] { + return ordmap.ByIndex(ss, getIndexRef[Server, *Server]) +} + +// Sort sorts the map by key and sets the indices accordingly. +func (ss Servers) Sort() { + ordmap.Sort(ss, setIndexRef[Server, *Server]) +} + +// Set sets a value in the map, adding it at the end of the order. +func (ss *Servers) Set(key string, s *ServerRef) { + ordmap.Set(ss, key, s, getIndexRef[Server, *Server], setIndexRef[Server, *Server]) +} + +var _ json.MarshalerTo = (*Servers)(nil) + +// MarshalJSONTo marshals the key-value pairs in order. +func (ss *Servers) MarshalJSONTo(enc *jsontext.Encoder) error { + return ordmap.MarshalJSONTo(ss, enc) +} + +var _ json.UnmarshalerFrom = (*Servers)(nil) + +// UnmarshalJSONFrom unmarshals the key-value pairs in order and sets the indices. +func (ss *Servers) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + return ordmap.UnmarshalJSONFrom(ss, dec, setIndexRef[Server, *Server]) +} + +func (l *loader) collectServers(ss Servers, ref ref) { + for name, s := range ss.ByIndex() { + l.collectServerRef(s, append(ref, name)) + } +} + +func (l *loader) resolveServers(ss Servers) error { + for name, s := range ss.ByIndex() { + if err := l.resolveServerRef(s); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} diff --git a/asyncapi/tag.go b/asyncapi/tag.go new file mode 100644 index 0000000..269c774 --- /dev/null +++ b/asyncapi/tag.go @@ -0,0 +1,61 @@ +package asyncapi + +import ( + "strings" + + "github.com/MarkRosemaker/errpath" +) + +// Tag allows adding meta data to a single tag. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#tagObject +type Tag struct { + // REQUIRED. The name of the tag. + Name string `json:"name" yaml:"name"` + // A short description for the tag. CommonMark syntax can be used for rich text representation. + Description string `json:"description,omitempty" yaml:"description,omitempty"` + // Additional external documentation for this tag. + ExternalDocs *ExternalDocsRef `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + // This object MAY be extended with Specification Extensions. + Extensions Extensions `json:",inline" yaml:",inline"` +} + +// Validate checks the tag for correctness. +func (t *Tag) Validate() error { + if t.Name == "" { + return &errpath.ErrField{Field: "name", Err: &errpath.ErrRequired{}} + } + + t.Description = strings.TrimSpace(t.Description) + + if t.ExternalDocs != nil { + if err := t.ExternalDocs.Validate(); err != nil { + return &errpath.ErrField{Field: "externalDocs", Err: err} + } + } + + return validateExtensions(t.Extensions) +} + +func (l *loader) collectTagRef(t *TagRef, ref ref) { + if !collectRef(l, t, l.tags, ref) { + return + } + + if t.Value.ExternalDocs != nil { + l.collectExternalDocsRef(t.Value.ExternalDocs, append(ref, "externalDocs")) + } +} + +func (l *loader) resolveTagRef(t *TagRef) error { + return resolveRef(t, l.tags, func(t *Tag) error { + if t.ExternalDocs != nil { + if err := l.resolveExternalDocsRef(t.ExternalDocs); err != nil { + return &errpath.ErrField{Field: "externalDocs", Err: err} + } + } + + return nil + }) +} diff --git a/asyncapi/tags.go b/asyncapi/tags.go new file mode 100644 index 0000000..37c36f4 --- /dev/null +++ b/asyncapi/tags.go @@ -0,0 +1,62 @@ +package asyncapi + +import ( + "errors" + + "github.com/MarkRosemaker/errpath" +) + +// Tags is a list of Tag Objects. A Tag Object in a list can be referenced by a Reference Object. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#tagsObject +type Tags []*TagRef + +// Validate validates each tag and makes sure that the tag names are unique. +func (tags Tags) Validate() error { + names := map[string]error{} + + for i, t := range tags { + if err := t.Validate(); err != nil { + return &errpath.ErrIndex{Index: i, Err: err} + } + + // a tag that was given as a reference is validated where it is defined + if t.isRef() { + continue + } + + errNotUnique := &errpath.ErrIndex{ + Index: i, + Err: &errpath.ErrField{ + Field: "name", + Err: &errpath.ErrInvalid[string]{Value: t.Value.Name, Message: "must be unique"}, + }, + } + + prevInstance := names[t.Value.Name] + if prevInstance == nil { + names[t.Value.Name] = errNotUnique + } else { // output both instances of the name + return errors.Join(prevInstance, errNotUnique) + } + } + + return nil +} + +func (l *loader) collectTags(tags Tags, ref ref) { + for i, t := range tags { + l.collectTagRef(t, append(ref, itoa(i))) + } +} + +func (l *loader) resolveTags(tags Tags) error { + for i, t := range tags { + if err := l.resolveTagRef(t); err != nil { + return &errpath.ErrIndex{Index: i, Err: err} + } + } + + return nil +} diff --git a/asyncapi/tags_by_name.go b/asyncapi/tags_by_name.go new file mode 100644 index 0000000..476deac --- /dev/null +++ b/asyncapi/tags_by_name.go @@ -0,0 +1,72 @@ +package asyncapi + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "iter" + + "github.com/MarkRosemaker/errpath" + "github.com/MarkRosemaker/ordmap" +) + +// TagsByName is a map of Tag Objects. +// ([Specification]) +// +// [Specification]: https://www.asyncapi.com/docs/reference/specification/v3.1.0#componentsTags +type TagsByName map[string]*TagRef + +// Validate validates each tag. +func (ts TagsByName) Validate() error { + for name, t := range ts.ByIndex() { + if err := t.Validate(); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} + +// ByIndex returns a sequence of key-value pairs ordered by index. +func (ts TagsByName) ByIndex() iter.Seq2[string, *TagRef] { + return ordmap.ByIndex(ts, getIndexRef[Tag, *Tag]) +} + +// Sort sorts the map by key and sets the indices accordingly. +func (ts TagsByName) Sort() { + ordmap.Sort(ts, setIndexRef[Tag, *Tag]) +} + +// Set sets a value in the map, adding it at the end of the order. +func (ts *TagsByName) Set(key string, t *TagRef) { + ordmap.Set(ts, key, t, getIndexRef[Tag, *Tag], setIndexRef[Tag, *Tag]) +} + +var _ json.MarshalerTo = (*TagsByName)(nil) + +// MarshalJSONTo marshals the key-value pairs in order. +func (ts *TagsByName) MarshalJSONTo(enc *jsontext.Encoder) error { + return ordmap.MarshalJSONTo(ts, enc) +} + +var _ json.UnmarshalerFrom = (*TagsByName)(nil) + +// UnmarshalJSONFrom unmarshals the key-value pairs in order and sets the indices. +func (ts *TagsByName) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + return ordmap.UnmarshalJSONFrom(ts, dec, setIndexRef[Tag, *Tag]) +} + +func (l *loader) collectTagsByName(ts TagsByName, ref ref) { + for name, t := range ts.ByIndex() { + l.collectTagRef(t, append(ref, name)) + } +} + +func (l *loader) resolveTagsByName(ts TagsByName) error { + for name, t := range ts.ByIndex() { + if err := l.resolveTagRef(t); err != nil { + return &errpath.ErrKey{Key: name, Err: err} + } + } + + return nil +} diff --git a/asyncapi/tags_test.go b/asyncapi/tags_test.go new file mode 100644 index 0000000..de845a5 --- /dev/null +++ b/asyncapi/tags_test.go @@ -0,0 +1,101 @@ +package asyncapi_test + +import ( + "testing" + + "github.com/MarkRosemaker/asyncapi" +) + +func TestTags_Validate(t *testing.T) { + t.Parallel() + + t.Run("no name", func(t *testing.T) { + t.Parallel() + + tags := asyncapi.Tags{{Value: &asyncapi.Tag{}}} + + err := tags.Validate() + if err == nil { + t.Fatal("expected error") + } + + if want := "[0].name is required"; err.Error() != want { + t.Fatalf("got: %v, want: %v", err, want) + } + }) + + t.Run("duplicate name", func(t *testing.T) { + t.Parallel() + + tags := asyncapi.Tags{ + {Value: &asyncapi.Tag{Name: "user"}}, + {Value: &asyncapi.Tag{Name: "user"}}, + } + + err := tags.Validate() + if err == nil { + t.Fatal("expected error") + } + + want := `[0].name ("user") is invalid: must be unique` + "\n" + + `[1].name ("user") is invalid: must be unique` + if err.Error() != want { + t.Fatalf("got: %v, want: %v", err, want) + } + }) + + t.Run("external docs without a URL", func(t *testing.T) { + t.Parallel() + + tags := asyncapi.Tags{{Value: &asyncapi.Tag{ + Name: "user", + ExternalDocs: &asyncapi.ExternalDocsRef{Value: &asyncapi.ExternalDocs{}}, + }}} + + err := tags.Validate() + if err == nil { + t.Fatal("expected error") + } + + if want := "[0].externalDocs.url is required"; err.Error() != want { + t.Fatalf("got: %v, want: %v", err, want) + } + }) + + t.Run("valid", func(t *testing.T) { + t.Parallel() + + tags := asyncapi.Tags{ + {Value: &asyncapi.Tag{Name: "user", Description: " Messages about users. "}}, + {Value: &asyncapi.Tag{ + Name: "signup", + ExternalDocs: &asyncapi.ExternalDocsRef{Value: &asyncapi.ExternalDocs{ + URL: mustParseURL("https://example.com/docs"), + }}, + }}, + } + + if err := tags.Validate(); err != nil { + t.Fatal(err) + } + + // the description is trimmed + if got, want := tags[0].Value.Description, "Messages about users."; got != want { + t.Fatalf("got: %q, want: %q", got, want) + } + }) +} + +func TestExternalDocs_FixScheme(t *testing.T) { + t.Parallel() + + // the scheme is added if it is missing + ed := &asyncapi.ExternalDocs{URL: mustParseURL("//example.com/docs")} + if err := ed.Validate(); err != nil { + t.Fatal(err) + } + + if got, want := ed.URL.String(), "https://example.com/docs"; got != want { + t.Fatalf("got: %v, want: %v", got, want) + } +} diff --git a/asyncapi/util_test.go b/asyncapi/util_test.go new file mode 100644 index 0000000..ac8e692 --- /dev/null +++ b/asyncapi/util_test.go @@ -0,0 +1,12 @@ +package asyncapi_test + +import "net/url" + +func mustParseURL(s string) *url.URL { + u, err := url.Parse(s) + if err != nil { + panic(err) + } + + return u +} diff --git a/asyncapi/validate_test.go b/asyncapi/validate_test.go new file mode 100644 index 0000000..1485d76 --- /dev/null +++ b/asyncapi/validate_test.go @@ -0,0 +1,210 @@ +package asyncapi_test + +import ( + "net/url" + "strings" + "testing" + + "github.com/MarkRosemaker/asyncapi" +) + +// TestValidate_Strict checks the rules that the specification states as a MUST +// but that a document can break without being syntactically wrong. +func TestValidate_Strict(t *testing.T) { + t.Parallel() + + for name, tc := range map[string]struct { + doc func(*asyncapi.Document) + want string + }{ + "identifier that is not a URI": { + func(d *asyncapi.Document) { d.ID = mustParseURL("smartylighting") }, + `id ("smartylighting") is invalid: must conform to the URI format`, + }, + "terms of service that is not an absolute URL": { + func(d *asyncapi.Document) { d.Info.TermsOfService = &url.URL{Path: "terms"} }, + `info.termsOfService ("https://terms") is invalid: must be an absolute URL`, + }, + "contact URL that is not an absolute URL": { + func(d *asyncapi.Document) { + d.Info.Contact = &asyncapi.Contact{URL: &url.URL{Path: "support"}} + }, + `info.contact.url ("https://support") is invalid: must be an absolute URL`, + }, + "license URL that is not an absolute URL": { + func(d *asyncapi.Document) { + d.Info.License = &asyncapi.License{Name: "Apache 2.0", URL: &url.URL{Path: "license"}} + }, + `info.license.url ("https://license") is invalid: must be an absolute URL`, + }, + "external docs URL that is not an absolute URL": { + func(d *asyncapi.Document) { + d.Info.ExternalDocs = &asyncapi.ExternalDocsRef{ + Value: &asyncapi.ExternalDocs{URL: &url.URL{Path: "docs"}}, + } + }, + `info.externalDocs.url ("https://docs") is invalid: must be an absolute URL`, + }, + "channel address with a query": { + func(d *asyncapi.Document) { + d.Channels["userSignedup"].Value.Address = "user/signedup?filter=all" + }, + `channels["userSignedup"].address ("user/signedup?filter=all") is invalid: ` + + "query parameters and fragments must not be used, use bindings instead", + }, + "channel address with a fragment": { + func(d *asyncapi.Document) { + d.Channels["userSignedup"].Value.Address = "user/signedup#now" + }, + `channels["userSignedup"].address ("user/signedup#now") is invalid: ` + + "query parameters and fragments must not be used, use bindings instead", + }, + "parameter name that doesn't match the pattern": { + func(d *asyncapi.Document) { + d.Channels["userSignedup"].Value.Parameters = asyncapi.Parameters{ + "user id": {Value: &asyncapi.Parameter{}}, + } + }, + `channels["userSignedup"].parameters["user id"] ("user id") is invalid: ` + + `must match the regular expression "^[A-Za-z0-9_\-]+$"`, + }, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + + doc := minimalDocument() + tc.doc(doc) + + err := doc.Validate() + if err == nil { + t.Fatal("expected error") + } + + if err.Error() != tc.want { + t.Fatalf("got: %v, want: %v", err, tc.want) + } + }) + } +} + +func TestValidate_Locations(t *testing.T) { + t.Parallel() + + t.Run("server that is not in the root", func(t *testing.T) { + t.Parallel() + + // the channel refers to a server of the components object + doc, err := asyncapi.LoadFromDataJSON([]byte( + `{"asyncapi":"3.1.0","info":{"title":"foo","version":"1.0.0"},` + + `"channels":{"userSignedup":{"servers":[` + + `{"$ref":"#/components/servers/production"}]}},` + + `"components":{"servers":{"production":` + + `{"host":"example.com","protocol":"kafka"}}}}`, + )) + if err != nil { + t.Fatal(err) + } + + validateErr := doc.Validate() + if validateErr == nil { + t.Fatal("expected error") + } + + want := `channels["userSignedup"].servers[0]: ` + asyncapi.ErrServerNotInRoot.Error() + if validateErr.Error() != want { + t.Fatalf("got: %v, want: %v", validateErr, want) + } + }) + + t.Run("channel that is not in the root", func(t *testing.T) { + t.Parallel() + + // the operation refers to a channel of the components object + doc, err := asyncapi.LoadFromDataJSON([]byte( + `{"asyncapi":"3.1.0","info":{"title":"foo","version":"1.0.0"},` + + `"operations":{"sendUserSignedup":{"action":"send",` + + `"channel":{"$ref":"#/components/channels/userSignedup"}}},` + + `"components":{"channels":{"userSignedup":{"address":"user/signedup"}}}}`, + )) + if err != nil { + t.Fatal(err) + } + + validateErr := doc.Validate() + if validateErr == nil { + t.Fatal("expected error") + } + + want := `operations["sendUserSignedup"].channel: ` + asyncapi.ErrChannelNotInRoot.Error() + if validateErr.Error() != want { + t.Fatalf("got: %v, want: %v", validateErr, want) + } + }) + + t.Run("message that is not of the channel", func(t *testing.T) { + t.Parallel() + + // the operation refers to a message of another channel + doc, err := asyncapi.LoadFromDataJSON([]byte( + `{"asyncapi":"3.1.0","info":{"title":"foo","version":"1.0.0"},` + + `"channels":{"userSignedup":{"address":"user/signedup"},` + + `"userLoggedIn":{"address":"user/loggedin","messages":{"userLoggedIn":{}}}},` + + `"operations":{"sendUserSignedup":{"action":"send",` + + `"channel":{"$ref":"#/channels/userSignedup"},` + + `"messages":[{"$ref":"#/channels/userLoggedIn/messages/userLoggedIn"}]}}}`, + )) + if err != nil { + t.Fatal(err) + } + + validateErr := doc.Validate() + if validateErr == nil { + t.Fatal("expected error") + } + + want := `operations["sendUserSignedup"].messages[0]: ` + + asyncapi.ErrMessageNotOfChannel.Error() + if validateErr.Error() != want { + t.Fatalf("got: %v, want: %v", validateErr, want) + } + }) +} + +func TestReference_IgnoresAdditionalProperties(t *testing.T) { + t.Parallel() + + // "This object cannot be extended with additional properties + // and any properties added SHALL be ignored." + doc, err := asyncapi.LoadFromDataJSON([]byte( + `{"asyncapi":"3.1.0","info":{"title":"foo","version":"1.0.0"},` + + `"channels":{"userSignedup":{"address":"user/signedup","messages":{"userSignedUp":` + + `{"$ref":"#/components/messages/userSignedUp","summary":"ignored"}}}},` + + `"components":{"messages":{"userSignedUp":{"name":"userSignedUp"}}}}`, + )) + if err != nil { + t.Fatal(err) + } + + if err := doc.Validate(); err != nil { + t.Fatal(err) + } + + msg := doc.Channels["userSignedup"].Value.Messages["userSignedUp"] + if msg.Ref == nil { + t.Fatal("expected the message to be given as a reference") + } + + if msg.Value != doc.Components.Messages["userSignedUp"].Value { + t.Fatal("the message was not resolved") + } + + // the additional property is not written back + got, err := doc.ToJSON() + if err != nil { + t.Fatal(err) + } + + if want := "ignored"; strings.Contains(string(got), want) { + t.Fatalf("got:\n%s\nwant it to not contain: %s", got, want) + } +} diff --git a/asyncapi/vendor/cloud.google.com/go/LICENSE b/asyncapi/vendor/cloud.google.com/go/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/asyncapi/vendor/cloud.google.com/go/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/asyncapi/vendor/cloud.google.com/go/civil/civil.go b/asyncapi/vendor/cloud.google.com/go/civil/civil.go new file mode 100644 index 0000000..8a7a1fa --- /dev/null +++ b/asyncapi/vendor/cloud.google.com/go/civil/civil.go @@ -0,0 +1,500 @@ +// Copyright 2016 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package civil implements types for civil time, a time-zone-independent +// representation of time that follows the rules of the proleptic +// Gregorian calendar with exactly 24-hour days, 60-minute hours, and 60-second +// minutes. +// +// Because they lack location information, these types do not represent unique +// moments or intervals of time. Use time.Time for that purpose. +package civil + +import ( + "database/sql/driver" + "fmt" + "time" +) + +// A Date represents a date (year, month, day). +// +// This type does not include location information, and therefore does not +// describe a unique 24-hour timespan. +type Date struct { + Year int // Year (e.g., 2014). + Month time.Month // Month of the year (January = 1, ...). + Day int // Day of the month, starting at 1. +} + +// DateOf returns the Date in which a time occurs in that time's location. +func DateOf(t time.Time) Date { + var d Date + d.Year, d.Month, d.Day = t.Date() + return d +} + +// ParseDate parses a string in RFC3339 full-date format and returns the date value it represents. +func ParseDate(s string) (Date, error) { + t, err := time.Parse("2006-01-02", s) + if err != nil { + return Date{}, err + } + return DateOf(t), nil +} + +// String returns the date in RFC3339 full-date format. +func (d Date) String() string { + return fmt.Sprintf("%04d-%02d-%02d", d.Year, d.Month, d.Day) +} + +// IsValid reports whether the date is valid. +func (d Date) IsValid() bool { + return DateOf(d.In(time.UTC)) == d +} + +// In returns the time corresponding to time 00:00:00 of the date in the location. +// +// In is always consistent with time.Date, even when time.Date returns a time +// on a different day. For example, if loc is America/Indiana/Vincennes, then both +// +// time.Date(1955, time.May, 1, 0, 0, 0, 0, loc) +// +// and +// +// civil.Date{Year: 1955, Month: time.May, Day: 1}.In(loc) +// +// return 23:00:00 on April 30, 1955. +// +// In panics if loc is nil. +func (d Date) In(loc *time.Location) time.Time { + return time.Date(d.Year, d.Month, d.Day, 0, 0, 0, 0, loc) +} + +// AddDays returns the date that is n days in the future. +// n can also be negative to go into the past. +func (d Date) AddDays(n int) Date { + return DateOf(d.In(time.UTC).AddDate(0, 0, n)) +} + +// AddMonths returns the date that is n months in the future. +// n can also be negative to go into the past. +func (d Date) AddMonths(n int) Date { + return DateOf(d.In(time.UTC).AddDate(0, n, 0)) +} + +// AddYears returns the date that is n years in the future. +// n can also be negative to go into the past. +func (d Date) AddYears(n int) Date { + return DateOf(d.In(time.UTC).AddDate(n, 0, 0)) +} + +// DaysSince returns the signed number of days between the date and s, not including the end day. +// This is the inverse operation to AddDays. +func (d Date) DaysSince(s Date) (days int) { + // We convert to Unix time so we do not have to worry about leap seconds: + // Unix time increases by exactly 86400 seconds per day. + deltaUnix := d.In(time.UTC).Unix() - s.In(time.UTC).Unix() + return int(deltaUnix / 86400) +} + +// Before reports whether d occurs before d2. +func (d Date) Before(d2 Date) bool { + if d.Year != d2.Year { + return d.Year < d2.Year + } + if d.Month != d2.Month { + return d.Month < d2.Month + } + return d.Day < d2.Day +} + +// After reports whether d occurs after d2. +func (d Date) After(d2 Date) bool { + return d2.Before(d) +} + +// Compare compares d and d2. If d is before d2, it returns -1; +// if d is after d2, it returns +1; otherwise it returns 0. +func (d Date) Compare(d2 Date) int { + if d.Before(d2) { + return -1 + } else if d.After(d2) { + return +1 + } + return 0 +} + +// IsZero reports whether date fields are set to their default value. +func (d Date) IsZero() bool { + return (d.Year == 0) && (int(d.Month) == 0) && (d.Day == 0) +} + +// Weekday returns the day of the week for the date. +func (d Date) Weekday() time.Weekday { + return d.In(time.UTC).Weekday() +} + +// MarshalText implements the encoding.TextMarshaler interface. +// The output is the result of d.String(). +func (d Date) MarshalText() ([]byte, error) { + return []byte(d.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface. +// The date is expected to be a string in a format accepted by ParseDate. +func (d *Date) UnmarshalText(data []byte) error { + var err error + *d, err = ParseDate(string(data)) + return err +} + +// Value implements the database/sql/driver Valuer interface. +func (d Date) Value() (driver.Value, error) { + return d.String(), nil +} + +// Scan implements the database/sql Scanner interface. +func (d *Date) Scan(v any) error { + switch vt := v.(type) { + case time.Time: + *d = DateOf(vt) + case *time.Time: + if vt != nil { + *d = DateOf(*vt) + } + case string: + var err error + *d, err = ParseDate(vt) + return err + case *string: + var err error + if vt != nil { + *d, err = ParseDate(*vt) + } + return err + case []byte: + var err error + *d, err = ParseDate(string(vt)) + return err + case *[]byte: + var err error + if vt != nil { + *d, err = ParseDate(string(*vt)) + } + return err + case Date: + *d = vt + case *Date: + *d = *vt + default: + return fmt.Errorf("unsupported scan type for Date: %T", v) + } + return nil +} + +// A Time represents a time with nanosecond precision. +// +// This type does not include location information, and therefore does not +// describe a unique moment in time. +// +// This type exists to represent the TIME type in storage-based APIs like BigQuery. +// Most operations on Times are unlikely to be meaningful. Prefer the DateTime type. +type Time struct { + Hour int // The hour of the day in 24-hour format; range [0-23] + Minute int // The minute of the hour; range [0-59] + Second int // The second of the minute; range [0-59] + Nanosecond int // The nanosecond of the second; range [0-999999999] +} + +// TimeOf returns the Time representing the time of day in which a time occurs +// in that time's location. It ignores the date. +func TimeOf(t time.Time) Time { + var tm Time + tm.Hour, tm.Minute, tm.Second = t.Clock() + tm.Nanosecond = t.Nanosecond() + return tm +} + +// ParseTime parses a string and returns the time value it represents. +// ParseTime accepts an extended form of the RFC3339 partial-time format. After +// the HH:MM:SS part of the string, an optional fractional part may appear, +// consisting of a decimal point followed by one to nine decimal digits. +// (RFC3339 admits only one digit after the decimal point). +func ParseTime(s string) (Time, error) { + t, err := time.Parse("15:04:05.999999999", s) + if err != nil { + return Time{}, err + } + return TimeOf(t), nil +} + +// String returns the date in the format described in ParseTime. If Nanoseconds +// is zero, no fractional part will be generated. Otherwise, the result will +// end with a fractional part consisting of a decimal point and nine digits. +func (t Time) String() string { + s := fmt.Sprintf("%02d:%02d:%02d", t.Hour, t.Minute, t.Second) + if t.Nanosecond == 0 { + return s + } + return s + fmt.Sprintf(".%09d", t.Nanosecond) +} + +// IsValid reports whether the time is valid. +func (t Time) IsValid() bool { + // Construct a non-zero time. + tm := time.Date(2, 2, 2, t.Hour, t.Minute, t.Second, t.Nanosecond, time.UTC) + return TimeOf(tm) == t +} + +// IsZero reports whether time fields are set to their default value. +func (t Time) IsZero() bool { + return (t.Hour == 0) && (t.Minute == 0) && (t.Second == 0) && (t.Nanosecond == 0) +} + +// Before reports whether t occurs before t2. +func (t Time) Before(t2 Time) bool { + if t.Hour != t2.Hour { + return t.Hour < t2.Hour + } + if t.Minute != t2.Minute { + return t.Minute < t2.Minute + } + if t.Second != t2.Second { + return t.Second < t2.Second + } + + return t.Nanosecond < t2.Nanosecond +} + +// After reports whether t occurs after t2. +func (t Time) After(t2 Time) bool { + return t2.Before(t) +} + +// Compare compares t and t2. If t is before t2, it returns -1; +// if t is after t2, it returns +1; otherwise it returns 0. +func (t Time) Compare(t2 Time) int { + if t.Before(t2) { + return -1 + } else if t.After(t2) { + return +1 + } + return 0 +} + +// MarshalText implements the encoding.TextMarshaler interface. +// The output is the result of t.String(). +func (t Time) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface. +// The time is expected to be a string in a format accepted by ParseTime. +func (t *Time) UnmarshalText(data []byte) error { + var err error + *t, err = ParseTime(string(data)) + return err +} + +// Value implements the database/sql/driver Valuer interface. +func (t Time) Value() (driver.Value, error) { + return t.String(), nil +} + +// Scan implements the database/sql Scanner interface. +func (t *Time) Scan(v any) error { + switch vt := v.(type) { + case time.Time: + *t = TimeOf(vt) + case *time.Time: + if vt != nil { + *t = TimeOf(*vt) + } + case string: + var err error + *t, err = ParseTime(vt) + return err + case *string: + var err error + if vt != nil { + *t, err = ParseTime(*vt) + } + return err + case []byte: + var err error + *t, err = ParseTime(string(vt)) + return err + case *[]byte: + var err error + if vt != nil { + *t, err = ParseTime(string(*vt)) + } + return err + case Time: + *t = vt + case *Time: + *t = *vt + default: + return fmt.Errorf("unsupported scan type for Time: %T", v) + } + return nil +} + +// A DateTime represents a date and time. +// +// This type does not include location information, and therefore does not +// describe a unique moment in time. +type DateTime struct { + Date Date + Time Time +} + +// Note: We deliberately do not embed Date into DateTime, to avoid promoting AddDays and Sub. + +// DateTimeOf returns the DateTime in which a time occurs in that time's location. +func DateTimeOf(t time.Time) DateTime { + return DateTime{ + Date: DateOf(t), + Time: TimeOf(t), + } +} + +// ParseDateTime parses a string and returns the DateTime it represents. +// ParseDateTime accepts a variant of the RFC3339 date-time format that omits +// the time offset but includes an optional fractional time, as described in +// ParseTime. Informally, the accepted format is +// +// YYYY-MM-DDTHH:MM:SS[.FFFFFFFFF] +// +// where the 'T' may be a lower-case 't'. +func ParseDateTime(s string) (DateTime, error) { + t, err := time.Parse("2006-01-02T15:04:05.999999999", s) + if err != nil { + t, err = time.Parse("2006-01-02t15:04:05.999999999", s) + if err != nil { + return DateTime{}, err + } + } + return DateTimeOf(t), nil +} + +// String returns the date in the format described in ParseDate. +func (dt DateTime) String() string { + return dt.Date.String() + "T" + dt.Time.String() +} + +// IsValid reports whether the datetime is valid. +func (dt DateTime) IsValid() bool { + return dt.Date.IsValid() && dt.Time.IsValid() +} + +// In returns the time corresponding to the DateTime in the given location. +// +// If the time is missing or ambigous at the location, In returns the same +// result as time.Date. For example, if loc is America/Indiana/Vincennes, then +// both +// +// time.Date(1955, time.May, 1, 0, 30, 0, 0, loc) +// +// and +// +// civil.DateTime{ +// civil.Date{Year: 1955, Month: time.May, Day: 1}}, +// civil.Time{Minute: 30}}.In(loc) +// +// return 23:30:00 on April 30, 1955. +// +// In panics if loc is nil. +func (dt DateTime) In(loc *time.Location) time.Time { + return time.Date(dt.Date.Year, dt.Date.Month, dt.Date.Day, dt.Time.Hour, dt.Time.Minute, dt.Time.Second, dt.Time.Nanosecond, loc) +} + +// Before reports whether dt occurs before dt2. +func (dt DateTime) Before(dt2 DateTime) bool { + return dt.In(time.UTC).Before(dt2.In(time.UTC)) +} + +// After reports whether dt occurs after dt2. +func (dt DateTime) After(dt2 DateTime) bool { + return dt2.Before(dt) +} + +// Compare compares dt and dt2. If dt is before dt2, it returns -1; +// if dt is after dt2, it returns +1; otherwise it returns 0. +func (dt DateTime) Compare(dt2 DateTime) int { + return dt.In(time.UTC).Compare(dt2.In(time.UTC)) +} + +// IsZero reports whether datetime fields are set to their default value. +func (dt DateTime) IsZero() bool { + return dt.Date.IsZero() && dt.Time.IsZero() +} + +// MarshalText implements the encoding.TextMarshaler interface. +// The output is the result of dt.String(). +func (dt DateTime) MarshalText() ([]byte, error) { + return []byte(dt.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface. +// The datetime is expected to be a string in a format accepted by ParseDateTime +func (dt *DateTime) UnmarshalText(data []byte) error { + var err error + *dt, err = ParseDateTime(string(data)) + return err +} + +// Value implements the database/sql/driver Valuer interface. +func (dt DateTime) Value() (driver.Value, error) { + return dt.String(), nil +} + +// Scan implements the database/sql Scanner interface. +func (dt *DateTime) Scan(v any) error { + switch vt := v.(type) { + case time.Time: + *dt = DateTimeOf(vt) + case *time.Time: + if vt != nil { + *dt = DateTimeOf(*vt) + } + case string: + var err error + *dt, err = ParseDateTime(vt) + return err + case *string: + var err error + if vt != nil { + *dt, err = ParseDateTime(*vt) + } + return err + case []byte: + var err error + *dt, err = ParseDateTime(string(vt)) + return err + case *[]byte: + var err error + if vt != nil { + *dt, err = ParseDateTime(string(*vt)) + } + return err + case DateTime: + *dt = vt + case *DateTime: + *dt = *vt + default: + return fmt.Errorf("unsupported scan type for DateTime: %T", v) + } + return nil +} diff --git a/asyncapi/vendor/github.com/MarkRosemaker/errpath/.gitattributes b/asyncapi/vendor/github.com/MarkRosemaker/errpath/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/errpath/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/asyncapi/vendor/github.com/MarkRosemaker/errpath/.gitignore b/asyncapi/vendor/github.com/MarkRosemaker/errpath/.gitignore new file mode 100644 index 0000000..6f6a3a4 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/errpath/.gitignore @@ -0,0 +1,28 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with 'go test -c' +*.test + +# Output files, e.g. from the go coverage tool +*.out + +# IDE directories +.idea +.vscode +.schemas +.history + +# Build and Environment +cmd/app/app +dist/ + +# Log Files +*.log + +# macOS files +*.DS_Store diff --git a/asyncapi/vendor/github.com/MarkRosemaker/errpath/LICENSE b/asyncapi/vendor/github.com/MarkRosemaker/errpath/LICENSE new file mode 100644 index 0000000..4531175 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/errpath/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Mark Rosemaker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/asyncapi/vendor/github.com/MarkRosemaker/errpath/README.md b/asyncapi/vendor/github.com/MarkRosemaker/errpath/README.md new file mode 100644 index 0000000..e199b91 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/errpath/README.md @@ -0,0 +1,132 @@ +# Error Path +[![Go Reference](https://pkg.go.dev/badge/github.com/MarkRosemaker/errpath.svg)](https://pkg.go.dev/github.com/MarkRosemaker/errpath) +[![Go Report Card](https://goreportcard.com/badge/github.com/MarkRosemaker/errpath)](https://goreportcard.com/report/github.com/MarkRosemaker/errpath) +![Code Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE) +

+ errpath logo: golang gopher determinedly walking through a blue and red maze +

+ +Package errpath provides utilities for creating and managing detailed error paths. +It allows users to construct error messages that include the full path to the error, +which can be particularly useful when traversing complex data structures such as JSON +or YAML files. + +Example for an error in an OpenAPI: `components.schemas["Pet"].allOf[0]: invalid schema` + +The package defines several error types that can be used to represent different kinds +of errors, such as missing required values, invalid values, and errors occurring at +specific fields, indices, or keys within a data structure. These error types implement +a chaining mechanism that builds a detailed error path. + +## Installation + +To use this package, import it as follows: + +```go +import "github.com/MarkRosemaker/errpath" +``` + +## Creating Errors + +There are several types of errors you can create with this package: + +### ErrRequired + +Signals that a required value is missing. + +```go + err := &errpath.ErrRequired{} +``` + +### ErrInvalid + +Signals that a value is invalid. You can optionally provide valid values and an explanatory message. + +```go + err := &errpath.ErrInvalid[string]{ + Value: "invalid_value", + Enum: []string{"valid1", "valid2"}, + Message: "must be one of the valid values", + } +``` + +### ErrField + +Represents an error that occurred in a specific field. + +```go + err := &errpath.ErrField{ + Field: "fieldName", + Err: &errpath.ErrRequired{}, + } +``` + +### ErrIndex + +Represents an error that occurred at a specific index in a slice. + +```go + err := &errpath.ErrIndex{ + Index: 3, + Err: &errpath.ErrInvalid[int]{Value: 42}, + } +``` + +### ErrKey + +Represents an error that occurred at a specific key in a map. + +```go + err := &errpath.ErrKey{ + Key: "keyName", + Err: &errpath.ErrRequired{}, + } +``` + +## Error Chaining + +Errors can be nested to form detailed error paths. For example: + +```go + err := &errpath.ErrField{ + Field: "foo", + Err: &errpath.ErrField{ + Field: "bar", + Err: &errpath.ErrKey{ + Key: "baz", + Err: &errpath.ErrField{ + Field: "qux", + Err: &errpath.ErrIndex{ + Index: 3, + Err: &errpath.ErrField{ + Field: "quux", + Err: &errpath.ErrInvalid[string]{ + Value: "corge", + }, + }, + }, + }, + }, + }, + } +``` + +This will produce an error message like: + +``` + foo.bar["baz"].qux[3].quux ("corge") is invalid +``` + +## Additional Information + +- [**Go Reference**](https://pkg.go.dev/github.com/MarkRosemaker/errpath): The Go reference documentation for the errpath package. +- [**Go Report Card**](https://goreportcard.com/report/github.com/MarkRosemaker/errpath): Check the code quality report. + +## Contributing + +If you have any contributions to make, please submit a pull request or open an issue on the [GitHub repository](https://github.com/MarkRosemaker/errpath). + +## License + +This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details. diff --git a/asyncapi/vendor/github.com/MarkRosemaker/errpath/doc.go b/asyncapi/vendor/github.com/MarkRosemaker/errpath/doc.go new file mode 100644 index 0000000..4eadfbd --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/errpath/doc.go @@ -0,0 +1,85 @@ +/* +Package errpath provides utilities for creating and managing detailed error paths. +It allows users to construct error messages that include the full path to the error, +which can be particularly useful when traversing complex data structures such as JSON +or YAML files. + +The package defines several error types that can be used to represent different kinds +of errors, such as missing required values, invalid values, and errors occurring at +specific fields, indices, or keys within a data structure. These error types implement +a chaining mechanism that builds a detailed error path. + +Usage: + +To use this package, import it as follows: + + import "github.com/MarkRosemaker/errpath" + +Creating Errors: + +There are several types of errors you can create with this package: + +1. ErrRequired: Signals that a required value is missing. + + err := &errpath.ErrRequired{} + +2. ErrInvalid: Signals that a value is invalid. You can optionally provide valid values and an explanatory message. + + err := &errpath.ErrInvalid[string]{ + Value: "invalid_value", + Enum: []string{"valid1", "valid2"}, + Message: "must be one of the valid values", + } + +3. ErrField: Represents an error that occurred in a specific field. + + err := &errpath.ErrField{ + Field: "fieldName", + Err: &errpath.ErrRequired{}, + } + +4. ErrIndex: Represents an error that occurred at a specific index in a slice. + + err := &errpath.ErrIndex{ + Index: 3, + Err: &errpath.ErrInvalid[int]{Value: 42}, + } + +5. ErrKey: Represents an error that occurred at a specific key in a map. + + err := &errpath.ErrKey{ + Key: "keyName", + Err: &errpath.ErrRequired{}, + } + +Error Chaining: + +Errors can be nested to form detailed error paths. For example: + + err := &errpath.ErrField{ + Field: "foo", + Err: &errpath.ErrField{ + Field: "bar", + Err: &errpath.ErrKey{ + Key: "baz", + Err: &errpath.ErrField{ + Field: "qux", + Err: &errpath.ErrIndex{ + Index: 3, + Err: &errpath.ErrField{ + Field: "quux", + Err: &errpath.ErrInvalid[string]{ + Value: "corge", + }, + }, + }, + }, + }, + }, + } + +This will produce an error message like: + + foo.bar["baz"].qux[3].quux ("corge") is invalid +*/ +package errpath diff --git a/asyncapi/vendor/github.com/MarkRosemaker/errpath/field.go b/asyncapi/vendor/github.com/MarkRosemaker/errpath/field.go new file mode 100644 index 0000000..50142d9 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/errpath/field.go @@ -0,0 +1,29 @@ +package errpath + +import "strings" + +var _ pathWriter = (*ErrField)(nil) + +// ErrField is an error that occurred in a field. +type ErrField struct { + // The name of the field where the error occurred. + Field string + // The underlying error. + Err error +} + +// Error returns the whole path to the field and the error message. +func (e *ErrField) Error() string { + b := &strings.Builder{} + b.WriteString(e.Field) + return writePath(b, e.Err) +} + +// writePath appends the field to the path by separating it with a dot from the previous path +func (e *ErrField) writePath(b *strings.Builder) { + b.WriteString(".") + b.WriteString(e.Field) +} + +// Unwrap returns the wrapped error. +func (e *ErrField) Unwrap() error { return e.Err } diff --git a/asyncapi/vendor/github.com/MarkRosemaker/errpath/index.go b/asyncapi/vendor/github.com/MarkRosemaker/errpath/index.go new file mode 100644 index 0000000..df341bb --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/errpath/index.go @@ -0,0 +1,32 @@ +package errpath + +import ( + "fmt" + "strings" +) + +var _ pathWriter = (*ErrIndex)(nil) + +// ErrIndex is an error that occurred in a slice. It contains the index of the element. +type ErrIndex struct { + // The index of the slice where the error occurred. + Index int + // The underlying error. + Err error +} + +// Error returns the index and the error message. +// However, it makes sense to wrap `ErrIndex` in an `ErrField` so this method is rarely called. +func (e *ErrIndex) Error() string { + b := &strings.Builder{} + e.writePath(b) + return writePath(b, e.Err) +} + +// writePath appends the index to the path by writing it in brackets after the previous path +func (e *ErrIndex) writePath(b *strings.Builder) { + fmt.Fprintf(b, "[%d]", e.Index) +} + +// Unwrap returns the wrapped error. +func (e *ErrIndex) Unwrap() error { return e.Err } diff --git a/asyncapi/vendor/github.com/MarkRosemaker/errpath/invalid.go b/asyncapi/vendor/github.com/MarkRosemaker/errpath/invalid.go new file mode 100644 index 0000000..b004b98 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/errpath/invalid.go @@ -0,0 +1,74 @@ +package errpath + +import ( + "fmt" + "reflect" + "strings" +) + +var _ pathWriter = (*ErrInvalid[string])(nil) + +// ErrInvalid signals that a value is invalid. +type ErrInvalid[T any] struct { + // The value that is invalid. + Value T + // An optional list of valid values. + Enum []T + // An optional message that explains the error. + Message string +} + +// Error returns helpful information about the invalid field and how to fix it. +// Without a previous error path, it simply calls it "a value". +// It makes sense to wrap `ErrInvalid` in another error such as `ErrField` so the user knows that the field is invalid. +func (e *ErrInvalid[_]) Error() string { + b := &strings.Builder{} + b.WriteString("a value") + e.writePath(b) + return b.String() +} + +// writePath appends information to the path so that the user knows why the value is invalid +func (e *ErrInvalid[T]) writePath(b *strings.Builder) { + if s := stringify(e.Value); s != "" { + fmt.Fprintf(b, " (%s)", s) + } + + b.WriteString(" is invalid") + if e.Message != "" { + b.WriteString(": ") + b.WriteString(e.Message) + } + + if len(e.Enum) == 0 { + return + } + + b.WriteString(", must be one of: ") + enums := make([]string, len(e.Enum)) + for i, v := range e.Enum { + enums[i] = stringify(v) + } + + b.WriteString(strings.Join(enums, ", ")) +} + +func stringify(val any) string { + v := reflect.ValueOf(val) + switch v.Kind() { + case reflect.Bool, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, + reflect.Float32, reflect.Float64, + reflect.Complex64, reflect.Complex128: + // write "false" and 0 even though it is the zero value + return fmt.Sprint(v.Interface()) + default: + // do not stringify zero values + if v.IsZero() { + return "" + } + + return fmt.Sprintf("%#v", v.Interface()) + } +} diff --git a/asyncapi/vendor/github.com/MarkRosemaker/errpath/key.go b/asyncapi/vendor/github.com/MarkRosemaker/errpath/key.go new file mode 100644 index 0000000..4365f28 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/errpath/key.go @@ -0,0 +1,32 @@ +package errpath + +import ( + "fmt" + "strings" +) + +var _ pathWriter = (*ErrKey)(nil) + +// ErrKey is an error that occurred in a map. It contains the key of the element. +type ErrKey struct { + // The key of the map where the error occurred. + Key string + // The underlying error. + Err error +} + +// Error returns the key and the error message. +// However, it makes sense to wrap `ErrKey` in an `ErrField` so this method is rarely called. +func (e *ErrKey) Error() string { + b := &strings.Builder{} + e.writePath(b) + return writePath(b, e.Err) +} + +// writePath appends the key to the path by writing it in brackets and in quotes after the previous path +func (e *ErrKey) writePath(b *strings.Builder) { + fmt.Fprintf(b, "[%q]", e.Key) +} + +// Unwrap returns the wrapped error. +func (e *ErrKey) Unwrap() error { return e.Err } diff --git a/asyncapi/vendor/github.com/MarkRosemaker/errpath/logo.jpg b/asyncapi/vendor/github.com/MarkRosemaker/errpath/logo.jpg new file mode 100644 index 0000000..1b646e8 Binary files /dev/null and b/asyncapi/vendor/github.com/MarkRosemaker/errpath/logo.jpg differ diff --git a/asyncapi/vendor/github.com/MarkRosemaker/errpath/required.go b/asyncapi/vendor/github.com/MarkRosemaker/errpath/required.go new file mode 100644 index 0000000..30b4864 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/errpath/required.go @@ -0,0 +1,23 @@ +package errpath + +import "strings" + +var _ pathWriter = (*ErrRequired)(nil) + +// ErrRequired signals that a required value is missing. +type ErrRequired struct{} + +// Error fulfills the error interface. +// Without a previous error path, it simply says "a value is required". +// Naturally, this is not very helpful, so it makes sense to wrap `ErrRequired` in another error such as `ErrField` so the user knows that the field was required. +func (e *ErrRequired) Error() string { + b := &strings.Builder{} + b.WriteString("a value") + e.writePath(b) + return b.String() +} + +// writePath appends `" is required"` to the path so that the user knows the given path was required +func (e *ErrRequired) writePath(b *strings.Builder) { + b.WriteString(" is required") +} diff --git a/asyncapi/vendor/github.com/MarkRosemaker/errpath/write_path.go b/asyncapi/vendor/github.com/MarkRosemaker/errpath/write_path.go new file mode 100644 index 0000000..f4cd9ea --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/errpath/write_path.go @@ -0,0 +1,48 @@ +package errpath + +import ( + "errors" + "fmt" + "strings" +) + +// A pathWriter allows us to continue a chain of errors. +// Instead of "info: name is required", we can have "info.name is required". +type pathWriter interface{ writePath(*strings.Builder) } + +// a joined error is the result of calling `errors.Join()` or similar error joiner +type joinedErr interface{ Unwrap() []error } + +func writePath(b *strings.Builder, err error) string { + // check if the error is a pathWriter + if w, ok := err.(pathWriter); ok { + // write the element of the path concerning this error + w.writePath(b) + // continue writing the path of the underlying error + return writePath(b, errors.Unwrap(err)) + } + + // check if we have a collection of errors + if joined, ok := err.(joinedErr); ok { + // multiple errors, print them as separate lines + // but with the same prefix up until here + prefix := b.String() + for i, e := range joined.Unwrap() { + if i != 0 { + b.WriteByte('\n') + b.WriteString(prefix) + } + + writePath(b, e) + } + + return b.String() // end of the path + } + + // we end the path and write the error that we don't know yet + if err != nil { + fmt.Fprintf(b, ": %v", err) + } + + return b.String() // end of the path +} diff --git a/asyncapi/vendor/github.com/MarkRosemaker/json2yaml/.gitattributes b/asyncapi/vendor/github.com/MarkRosemaker/json2yaml/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/json2yaml/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/asyncapi/vendor/github.com/MarkRosemaker/json2yaml/.gitignore b/asyncapi/vendor/github.com/MarkRosemaker/json2yaml/.gitignore new file mode 100644 index 0000000..6f6a3a4 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/json2yaml/.gitignore @@ -0,0 +1,28 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with 'go test -c' +*.test + +# Output files, e.g. from the go coverage tool +*.out + +# IDE directories +.idea +.vscode +.schemas +.history + +# Build and Environment +cmd/app/app +dist/ + +# Log Files +*.log + +# macOS files +*.DS_Store diff --git a/asyncapi/vendor/github.com/MarkRosemaker/json2yaml/convert.go b/asyncapi/vendor/github.com/MarkRosemaker/json2yaml/convert.go new file mode 100644 index 0000000..547b53d --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/json2yaml/convert.go @@ -0,0 +1,90 @@ +package json2yaml + +import ( + "bytes" + "encoding/json/jsontext" + "fmt" + "io" + + "gopkg.in/yaml.v3" +) + +// Convert converts a JSON value to a YAML node. +func Convert(b jsontext.Value) (*yaml.Node, error) { + dec := jsontext.NewDecoder(bytes.NewReader(b)) + + n := &yaml.Node{} + if err := decodeFromJSON(dec, n); err != nil { + return nil, err + } + + // check if we reached the end + if dec.PeekKind() != 0 { + return nil, fmt.Errorf("expected EOF, got %v", dec.PeekKind()) + } + + return n, nil +} + +func decodeFromJSON(dec *jsontext.Decoder, n *yaml.Node) error { + tkn, err := dec.ReadToken() + if err != nil { + return err + } + + switch tkn.Kind() { + case '"', 't', 'f', 'n', '0': + n.Kind = yaml.ScalarNode + n.Value = tkn.String() + case '{': + n.Kind = yaml.MappingNode + return decodeMapFromJSON(dec, n) + case '[': + n.Kind = yaml.SequenceNode + + for { + if dec.PeekKind() == ']' { + _, err := dec.ReadToken() // read the ']' we peeked at + return err + } + + el := &yaml.Node{} + if err := decodeFromJSON(dec, el); err != nil { + return err + } + + n.Content = append(n.Content, el) + } + default: + return fmt.Errorf("unsupported token kind: %v", tkn.Kind()) + } + + return nil +} + +func decodeMapFromJSON(dec *jsontext.Decoder, n *yaml.Node) error { + for { + switch k := dec.PeekKind(); k { + case '"': // string + key := &yaml.Node{} + // ignore error, we know it's a string + _ = decodeFromJSON(dec, key) + n.Content = append(n.Content, key) + case '}': + _, err := dec.ReadToken() // read the '}' we peeked at + return err // done + case 0: + return io.EOF + default: + return fmt.Errorf("unexpected kind for mapping key: %s", k) + } + + // write the value + val := &yaml.Node{} + if err := decodeFromJSON(dec, val); err != nil { + return err + } + + n.Content = append(n.Content, val) + } +} diff --git a/asyncapi/vendor/github.com/MarkRosemaker/json2yaml/example.json b/asyncapi/vendor/github.com/MarkRosemaker/json2yaml/example.json new file mode 100644 index 0000000..4693f4a --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/json2yaml/example.json @@ -0,0 +1,46 @@ +{ + "string": "Hello, World!", + "int": 42, + "float": 3.14, + "bool": true, + "bool2": false, + "null_value": null, + "list": [ + "item1", + "item2", + "item3" + ], + "dictionary": { + "key1": "value1", + "key2": "value2" + }, + "nested": { + "list_of_dicts": [ + { + "name": "item1", + "value": 1 + }, + { + "name": "item2", + "value": 2 + } + ], + "dict_of_lists": { + "key1": [ + "item1", + "item2" + ], + "key2": [ + "item3", + "item4" + ] + } + }, + "multi": "This text\nhas multiple\nlines.", + "block1": "\nThis block\nhas new line before and after\n", + "block2": "\nThis block\nonly has a new line before", + "block3": "This block\nonly has a new line after", + "text": "\n\n\nThis is text that \nstarts only after 3 new lines.\n\n\n", + "folded": "This is a folded style multiline string.\n", + "quoted": "This string contains: special characters, colons: and dashes -" + } \ No newline at end of file diff --git a/asyncapi/vendor/github.com/MarkRosemaker/json2yaml/example.yaml b/asyncapi/vendor/github.com/MarkRosemaker/json2yaml/example.yaml new file mode 100644 index 0000000..f3aa0df --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/json2yaml/example.yaml @@ -0,0 +1,43 @@ +string: Hello, World! +int: 42 +float: 3.14 +bool: true +bool2: false +null_value: null +list: + - item1 + - item2 + - item3 +dictionary: + key1: value1 + key2: value2 +nested: + list_of_dicts: + - name: item1 + value: 1 + - name: item2 + value: 2 + dict_of_lists: + key1: + - item1 + - item2 + key2: + - item3 + - item4 +multi: |- + This text + has multiple + lines. +block1: |4 + This block + has new line before and after +block2: |4- + This block + only has a new line before +block3: |- + This block + only has a new line after +text: "\n\n\nThis is text that \nstarts only after 3 new lines.\n\n\n" +folded: | + This is a folded style multiline string. +quoted: 'This string contains: special characters, colons: and dashes -' diff --git a/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/.gitattributes b/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/.gitignore b/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/.gitignore new file mode 100644 index 0000000..6f6a3a4 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/.gitignore @@ -0,0 +1,28 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with 'go test -c' +*.test + +# Output files, e.g. from the go coverage tool +*.out + +# IDE directories +.idea +.vscode +.schemas +.history + +# Build and Environment +cmd/app/app +dist/ + +# Log Files +*.log + +# macOS files +*.DS_Store diff --git a/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/LICENSE b/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/LICENSE new file mode 100644 index 0000000..4531175 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Mark Rosemaker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/README.md b/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/README.md new file mode 100644 index 0000000..41b6096 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/README.md @@ -0,0 +1,213 @@ +# JSON v2 Utilities +[![Go Reference](https://pkg.go.dev/badge/github.com/MarkRosemaker/jsonutil.svg)](https://pkg.go.dev/github.com/MarkRosemaker/jsonutil) +[![Go Report Card](https://goreportcard.com/badge/github.com/MarkRosemaker/jsonutil)](https://goreportcard.com/report/github.com/MarkRosemaker/jsonutil) +![Code Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE) +

+ jsonutil logo: cute golang gopher in JSON brackets with gear in the background and text saying v2 +

+ +`jsonutil` is a Go package that provides custom [JSON v2](https://pkg.go.dev/encoding/json/v2) marshaling and unmarshaling functions for specific data types. + +This package is particularly useful when you need to handle JSON encoding and decoding for types like `time.Duration` and `url.URL` in a customized manner. + +## Features + +* Custom marshaler and unmarshaler for `url.URL`: + * `URLMarshal` marshals `url.URL` as a string. + * `URLUnmarshal` unmarshals `url.URL` from a string. +* Custom marshaler and unmarshaler for `time.Duration`: + * `DurationMarshalIntSeconds` marshals `time.Duration` as an integer representing seconds. + * `DurationUnmarshalIntSeconds` unmarshals `time.Duration` from an integer assuming it represents seconds. +* Custom marshaler for maps with ordered keys: + * `OrderedMapMarshal[M ~map[K]V, K cmp.Ordered, V any]` marshals `M` so that the keys are sorted. +* Custom marshaler for `http.Header`: + * `HTTPHeaderMarshal` marshals the values of `http.Header` as single strings. + * `HTTPHeaderUnmarshal` unmarshals the values of `http.Header` from single strings. + +## Installation + +To install the library, use the following command: + +```shell +go get github.com/MarkRosemaker/jsonutil +``` + +## Usage + +### Custom Marshaling and Unmarshaling for `url.URL` + +To use the custom marshaler and unmarshaler for `url.URL`, you can import the package and use the provided functions: + +```go +package main + +import ( + "encoding/json/v2" + "fmt" + "net/url" + + "github.com/MarkRosemaker/jsonutil" +) + +var jsonOpts = json.JoinOptions( + json.WithMarshalers(json.MarshalToFunc(jsonutil.URLMarshal)), + json.WithUnmarshalers(json.UnmarshalFromFunc(jsonutil.URLUnmarshal)), +) + +type MyStruct struct { + Foo string `json:"foo"` + Link url.URL `json:"link"` +} + +func main() { + out := &MyStruct{} + if err := json.Unmarshal([]byte(`{"foo":"bar","link":"https://example.com/"}`), out, jsonOpts); err != nil { + panic(err) + } + + if out.Link.String() != "https://example.com/" { + panic("something went wrong") + } + + res, err := json.Marshal(MyStruct{ + Foo: "baz", + Link: url.URL{Scheme: "http", Host: "example.com", Path: "/some-path"}, + }, jsonOpts) + if err != nil { + panic(err) + } + + fmt.Println(string(res)) + // Output: {"foo":"baz","link":"http://example.com/some-path"} +} +``` + +### Custom Marshaling and Unmarshaling for `time.Duration` + +To use the custom marshaler and unmarshaler for `time.Duration`, you can import the package and use the provided functions: + +```go +package main + +import ( + "encoding/json/v2" + "fmt" + "time" + + "github.com/MarkRosemaker/jsonutil" +) + +var jsonOpts = json.JoinOptions( + json.WithMarshalers(json.MarshalToFunc(jsonutil.DurationMarshalIntSeconds)), + json.WithUnmarshalers(json.UnmarshalFromFunc(jsonutil.DurationUnmarshalIntSeconds)), +) + +type MyStruct struct { + Foo string `json:"foo"` + Duration time.Duration `json:"duration"` +} + +func main() { + out := &MyStruct{} + if err := json.Unmarshal([]byte(`{"foo":"bar","duration":3}`), out, jsonOpts); err != nil { + panic(err) + } + + if out.Duration != 3*time.Second { + panic("something went wrong") + } + + res, err := json.Marshal(MyStruct{ + Foo: "baz", + Duration: time.Minute, + }, jsonOpts) + if err != nil { + panic(err) + } + + fmt.Println(string(res)) + // Output: {"foo":"baz","duration":60} +} +``` + +### Custom Marshaling for Maps with Ordered Keys + +To use the custom marshaler for `M` where `[M ~map[K]V, K cmp.Ordered, V any]`, you can import the package and use the provided functions: + +```go +package main + +import ( + "encoding/json/v2" + "fmt" + + "github.com/MarkRosemaker/jsonutil" +) + +type myMap map[string]int + +var jsonOpts = json.JoinOptions( + json.WithMarshalers(json.MarshalToFunc(jsonutil.OrderedMapMarshal[myMap])), +) + +func main() { + res, err := json.Marshal(myMap{ + "foo": 1, + "bar": 2, + }, jsonOpts) + if err != nil { + panic(err) + } + + fmt.Println(string(res)) + // Output: {"bar":2,"bar":1} +} +``` + +### Custom Marshaling and Unmarshaling for `http.Header` + +To use the custom marshaler and unmarshaler for `http.Header`, you can import the package and use the provided functions: + +```go +package main + +import ( + "encoding/json/v2" + "fmt" + "net/http" + + "github.com/MarkRosemaker/jsonutil" +) + +var jsonOpts = json.JoinOptions( + json.WithMarshalers(json.MarshalToFunc(jsonutil.HTTPHeaderMarshal)), + json.WithUnmarshalers(json.UnmarshalFromFunc(jsonutil.HTTPHeaderUnmarshal)), +) + +func main() { + out := &http.Header{} + if err := json.Unmarshal([]byte(`{"foo":"bar","baz":"quux"}`), out, jsonOpts); err != nil { + panic(err) + } + + res, err := json.Marshal(http.Header{ + "foo": []string{"bar"}, + "baz": []string{"quux"}, + }, jsonOpts) + if err != nil { + panic(err) + } + + fmt.Println(string(res)) + // Output: {"Foo":"bar","Baz":"quux"} +} +``` + +## Contributing + +If you have any contributions to make, please submit a pull request or open an issue on the [GitHub repository](https://github.com/MarkRosemaker/jsonutil). + +## License + +This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details. diff --git a/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/date.go b/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/date.go new file mode 100644 index 0000000..52ad616 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/date.go @@ -0,0 +1,36 @@ +package jsonutil + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "time" + + "cloud.google.com/go/civil" +) + +// DateMarshalIntUnix is a custom marshaler for civil.Date, marshaling them as integers representing unix time. +func DateMarshalIntUnix(enc *jsontext.Encoder, d civil.Date) error { + if d.IsZero() { + return enc.WriteToken(jsontext.Int(0)) + } + + return enc.WriteToken(jsontext.Int(int64( + time.Date(d.Year, d.Month, d.Day, 0, 0, 0, 0, time.UTC).Unix(), + ))) +} + +// DateUnmarshalIntUnix is a custom unmarshaler for civil.Date, unmarshaling them from integers and assuming they represent unix time. +func DateUnmarshalIntUnix(dec *jsontext.Decoder, d *civil.Date) error { + var seconds int64 + if err := json.UnmarshalDecode(dec, &seconds); err != nil { + return err + } + + if seconds == 0 { + *d = civil.Date{} + } else { + *d = civil.DateOf(time.Unix(seconds, 0)) + } + + return nil +} diff --git a/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/duration.go b/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/duration.go new file mode 100644 index 0000000..ff5c56c --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/duration.go @@ -0,0 +1,24 @@ +package jsonutil + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "time" +) + +// DurationMarshalIntSeconds is a custom marshaler for time.Duration, marshaling them as integers representing seconds. +func DurationMarshalIntSeconds(enc *jsontext.Encoder, d time.Duration) error { + return enc.WriteToken(jsontext.Int(int64(d / time.Second))) +} + +// DurationUnmarshalIntSeconds is a custom unmarshaler for time.Duration, unmarshaling them from integers and assuming they represent seconds. +func DurationUnmarshalIntSeconds(dec *jsontext.Decoder, d *time.Duration) error { + var seconds int64 + if err := json.UnmarshalDecode(dec, &seconds); err != nil { + return err + } + + *d = time.Duration(seconds) * time.Second + + return nil +} diff --git a/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/http_header.go b/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/http_header.go new file mode 100644 index 0000000..8f499ed --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/http_header.go @@ -0,0 +1,85 @@ +package jsonutil + +import ( + "encoding/json/jsontext" + "fmt" + "maps" + "net/http" + "net/textproto" + "slices" +) + +// HTTPHeaderMarshal is a custom marshaler for http.Header, marshaling values as a single strings. +// It also marshals the keys in their canonical form. +// Note that we omit keys that don't have a value. +func HTTPHeaderMarshal(enc *jsontext.Encoder, m http.Header) error { + if m == nil { + return enc.WriteToken(jsontext.Null) + } + + if err := enc.WriteToken(jsontext.BeginObject); err != nil { + return err + } + + for _, key := range slices.Sorted(maps.Keys(m)) { + v := m[key] + if len(v) == 0 || v[0] == "" { + continue + } + + if err := enc.WriteToken(jsontext.String(textproto.CanonicalMIMEHeaderKey(key))); err != nil { + return err + } + + if err := enc.WriteToken(jsontext.String(v[0])); err != nil { + return err + } + } + + return enc.WriteToken(jsontext.EndObject) +} + +// HTTPHeaderUnmarshal is a custom unmarshaler for http.Header, unmarshaling values as single strings. +func HTTPHeaderUnmarshal(dec *jsontext.Decoder, h *http.Header) error { + tkn, err := dec.ReadToken() + if err != nil { + return err + } + + switch tkn.Kind() { + case jsontext.KindBeginObject: // expected, continue below + *h = http.Header{} + case jsontext.KindNull: + *h = nil + return nil // nil map + default: + return fmt.Errorf("expected begin object, got %s", tkn.Kind()) + } + + for dec.PeekKind() != jsontext.KindEndObject { + keyTkn, err := dec.ReadToken() + if err != nil { + return err + } + + if keyTkn.Kind() != jsontext.KindString { + return fmt.Errorf("expected string key, got %s", keyTkn.Kind()) + } + + key := keyTkn.String() + + val, err := dec.ReadToken() + if err != nil { + return err + } + + if val.Kind() != jsontext.KindString { + return fmt.Errorf("expected string value, got %s", val.Kind()) + } + + h.Set(key, val.String()) + } + + _, err = dec.ReadToken() // consume jsontext.KindEndObject + return err +} diff --git a/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/logo.jpg b/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/logo.jpg new file mode 100644 index 0000000..1a7ff30 Binary files /dev/null and b/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/logo.jpg differ diff --git a/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/ordered_map.go b/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/ordered_map.go new file mode 100644 index 0000000..1736a02 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/ordered_map.go @@ -0,0 +1,32 @@ +package jsonutil + +import ( + "cmp" + "encoding/json/jsontext" + "encoding/json/v2" + "maps" + "slices" +) + +// OrderedMapMarshal is a custom marshaler for maps with ordered keys, marshaling them in an ordered fashion. +func OrderedMapMarshal[M ~map[K]V, K cmp.Ordered, V any](enc *jsontext.Encoder, m M) error { + if m == nil { + return enc.WriteToken(jsontext.Null) + } + + if err := enc.WriteToken(jsontext.BeginObject); err != nil { + return err + } + + for _, key := range slices.Sorted(maps.Keys(m)) { + if err := json.MarshalEncode(enc, key); err != nil { + return err + } + + if err := json.MarshalEncode(enc, m[key]); err != nil { + return err + } + } + + return enc.WriteToken(jsontext.EndObject) +} diff --git a/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/read_file.go b/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/read_file.go new file mode 100644 index 0000000..cbc9d51 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/read_file.go @@ -0,0 +1,27 @@ +package jsonutil + +import ( + "encoding/json/v2" + "errors" + "os" +) + +// ReadFile reads a json file and unmarshals it. +func ReadFile[T any](name string, opts ...json.Options) (T, error) { + var v T + + f, err := os.Open(name) + if err != nil { + return v, err + } + + if err := json.UnmarshalRead(f, &v, opts...); err != nil { + if closeErr := f.Close(); closeErr != nil { + return v, errors.Join(err, closeErr) + } + + return v, err + } + + return v, f.Close() +} diff --git a/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/time.go b/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/time.go new file mode 100644 index 0000000..db0439d --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/time.go @@ -0,0 +1,70 @@ +package jsonutil + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "errors" + "fmt" + "time" +) + +// TimeMarshalIntUnix is a custom marshaler for time.Time, marshaling them as integers representing unix time. +func TimeMarshalIntUnix(enc *jsontext.Encoder, t time.Time) error { + if t.IsZero() { + return enc.WriteToken(jsontext.Int(0)) + } + + return enc.WriteToken(jsontext.Int(int64(t.Unix()))) +} + +// TimeUnmarshalIntUnix is a custom unmarshaler for time.Time, unmarshaling them from integers and assuming they represent unix time. +func TimeUnmarshalIntUnix(dec *jsontext.Decoder, d *time.Time) error { + var seconds int64 + if err := json.UnmarshalDecode(dec, &seconds); err != nil { + return err + } + + if seconds == 0 { + *d = time.Time{} + } else { + *d = time.Unix(seconds, 0) + } + + return nil +} + +// TimeUnmarshalStringOrIntUnix unmarshals a time.Time from either an RFC3339 +// string or an integer representing unix seconds. Nulls decode as the zero time. +func TimeUnmarshalStringOrIntUnix(dec *jsontext.Decoder, d *time.Time) error { + tkn, err := dec.ReadToken() + if err != nil { + return err + } + + switch tkn.Kind() { + case jsontext.KindNumber: + if seconds := tkn.Int(); seconds == 0 { + *d = time.Time{} + } else { + *d = time.Unix(seconds, 0) + } + case jsontext.KindString: + s := tkn.String() + if err := d.UnmarshalText([]byte(s)); err != nil { + const altLayout = "Mon Jan 2 2006 15:04:05 MST-0700" + ts, err2 := time.Parse(altLayout, s) + if err2 != nil { + return errors.Join(err, err2) + } + + *d = ts + } + + return nil + case jsontext.KindNull: // ok, nothing to do + default: + return fmt.Errorf("unknown token kind %s", tkn.Kind()) + } + + return nil +} diff --git a/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/url.go b/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/url.go new file mode 100644 index 0000000..34bd7b2 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/url.go @@ -0,0 +1,36 @@ +package jsonutil + +import ( + "encoding/json/jsontext" + "fmt" + "net/url" +) + +// URLMarshal is a custom marshaler for URL values, marshaling them as strings. +func URLMarshal(enc *jsontext.Encoder, u url.URL) error { + return enc.WriteToken(jsontext.String(u.String())) +} + +// URLUnmarshal is a custom unmarshaler for URL values, unmarshaling them from strings. +func URLUnmarshal(dec *jsontext.Decoder, u *url.URL) error { + tkn, err := dec.ReadToken() + if err != nil { + return err + } + + switch tkn.Kind() { + case jsontext.KindString: + parsed, err := url.Parse(tkn.String()) + if err != nil { + return err + } + + *u = *parsed + + return nil + case jsontext.KindNull: + return nil // no url given + default: + return fmt.Errorf("expected string, got %s", tkn) + } +} diff --git a/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/write_file.go b/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/write_file.go new file mode 100644 index 0000000..607ffb3 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/jsonutil/write_file.go @@ -0,0 +1,29 @@ +package jsonutil + +import ( + "encoding/json/v2" + "errors" + "os" +) + +// WriteFile writes a json file by marshalling it. +func WriteFile[T any](name string, data T, opts ...json.Options) error { + f, err := os.Create(name) + if err != nil { + return err + } + + if err := json.MarshalWrite(f, data, opts...); err != nil { + if closeErr := f.Close(); closeErr != closeErr { + return errors.Join(err, closeErr) + } + + if rmErr := os.Remove(name); rmErr != rmErr { + return errors.Join(err, rmErr) + } + + return err + } + + return f.Close() +} diff --git a/asyncapi/vendor/github.com/MarkRosemaker/ordmap/.gitattributes b/asyncapi/vendor/github.com/MarkRosemaker/ordmap/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/ordmap/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/asyncapi/vendor/github.com/MarkRosemaker/ordmap/.gitignore b/asyncapi/vendor/github.com/MarkRosemaker/ordmap/.gitignore new file mode 100644 index 0000000..6f6a3a4 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/ordmap/.gitignore @@ -0,0 +1,28 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with 'go test -c' +*.test + +# Output files, e.g. from the go coverage tool +*.out + +# IDE directories +.idea +.vscode +.schemas +.history + +# Build and Environment +cmd/app/app +dist/ + +# Log Files +*.log + +# macOS files +*.DS_Store diff --git a/asyncapi/vendor/github.com/MarkRosemaker/ordmap/LICENSE b/asyncapi/vendor/github.com/MarkRosemaker/ordmap/LICENSE new file mode 100644 index 0000000..4531175 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/ordmap/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Mark Rosemaker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/asyncapi/vendor/github.com/MarkRosemaker/ordmap/README.md b/asyncapi/vendor/github.com/MarkRosemaker/ordmap/README.md new file mode 100644 index 0000000..60015bb --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/ordmap/README.md @@ -0,0 +1,127 @@ +# Ordered Map +[![Go Reference](https://pkg.go.dev/badge/github.com/MarkRosemaker/ordmap.svg)](https://pkg.go.dev/github.com/MarkRosemaker/ordmap) +[![Go Report Card](https://goreportcard.com/badge/github.com/MarkRosemaker/ordmap)](https://goreportcard.com/report/github.com/MarkRosemaker/ordmap) +![Code Coverage](https://img.shields.io/badge/coverage-98.5%25-brightgreen) +[![License: **MIT**](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE) +

+ ordmap logo: a gopher holding a map, surrounded by keys +

+ +`ordmap` is a Go package that provides a generic ordered map implementation, primarily designed for [JSON v2](https://pkg.go.dev/encoding/json/v2) marshalling and unmarshalling. + +An ordered map maintains the order of keys based on insertion, allowing you to iterate over the map in the order in which entries were added. This can be particularly useful for applications where the order of elements is important, such as in JSON serialization or when maintaining the sequence of operations. + +## Features + +- **Seamless JSON v2 Integration:** Directly integrates with the [JSON v2](https://pkg.go.dev/encoding/json/v2) library for efficient and order-preserving marshalling and unmarshalling. +- **Custom Ordered Maps:** Provides robust helper functions to easily define your own custom ordered maps with minimal boilerplate code. +- **Pre-Defined Ordered Map Alias:** Simplifies usage by offering a pre-defined ordered map type that can be conveniently aliased for specific key and value types. +- **Efficient Ordered Operations:** Ensures efficient insertion, retrieval, and iteration while maintaining the order of elements, making it ideal for use cases where order matters. +- **Ordered Iteration:** Leverages the `ByIndex` method to iterate over the map in an ordered manner based on the insertion sequence. + +## Installation + +To install the library, use the following command: + +```shell +go get github.com/MarkRosemaker/ordmap +``` + +## Usage + +### Custom Ordered Map + +To create your own custom ordered map, you can utilize helper functions to define its methods: + +```go +package main + +import ( + "encoding/json/v2" + "encoding/json/jsontext" + "iter" + + "github.com/MarkRosemaker/ordmap" +) + +type MyOrderedMap map[string]*ValueWithIndex + +type ValueWithIndex struct { + Foo string `json:"foo"` + Bar int `json:"bar"` + + idx int // to order a map of this type +} + +func getIndex(v *ValueWithIndex) int { return v.idx } +func setIndex(v *ValueWithIndex, i int) *ValueWithIndex { v.idx = i; return v } + +// ByIndex returns a sequence of key-value pairs ordered by index. +func (om MyOrderedMap) ByIndex() iter.Seq2[string, *ValueWithIndex] { + return ordmap.ByIndex(om, getIndex) +} + +// Sort sorts the map by key and sets the indices accordingly. +func (om MyOrderedMap) Sort() { + ordmap.Sort(om, setIndex) +} + +// Set sets a value in the map, adding it at the end of the order. +func (om *MyOrderedMap) Set(key string, v *ValueWithIndex) { + ordmap.Set(om, key, v, getIndex, setIndex) +} + +// MarshalJSONTo marshals the key-value pairs in order. +func (om *MyOrderedMap) MarshalJSONTo(enc *jsontext.Encoder, opts json.Options) error { + return ordmap.MarshalJSONTo(om, enc, opts) +} + +// UnmarshalJSONFrom unmarshals the key-value pairs in order and sets the indices. +func (om *MyOrderedMap) UnmarshalJSONFrom(dec *jsontext.Decoder, opts json.Options) error { + return ordmap.UnmarshalJSONFrom(om, dec, opts, setIndex) +} +``` + +If you prefer the map values to be non-pointer types, you can adjust the implementation as follows: + +```go +type MyOrderedMap map[string]ValueWithIndex + +func getIndex(v ValueWithIndex) int { return v.idx } +func setIndex(v ValueWithIndex, i int) ValueWithIndex { v.idx = i; return v } + +func (om MyOrderedMap) ByIndex() iter.Seq2[string, ValueWithIndex] { + return ordmap.ByIndex(om, getIndex) +} + +func (om *MyOrderedMap) Set(key string, v ValueWithIndex) { + ordmap.Set(om, key, v, getIndex, setIndex) +} +``` + +### Using The Pre-Defined Ordered Map + +For simplicity, an ordered map type is already defined for you. You only need to specify the key and value types: + +```go +package main + +import ( + "github.com/MarkRosemaker/ordmap" +) + +type MyOrderedMap = ordmap.OrderedMap[string, *MyValue] + +type MyValue struct { + Foo string `json:"foo"` + Bar int `json:"bar"` +} +``` + +## Contributing + +If you have any contributions to make, please submit a pull request or open an issue on the [GitHub repository](https://github.com/MarkRosemaker/ordmap). + +## License + +This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details. diff --git a/asyncapi/vendor/github.com/MarkRosemaker/ordmap/by_index.go b/asyncapi/vendor/github.com/MarkRosemaker/ordmap/by_index.go new file mode 100644 index 0000000..db38943 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/ordmap/by_index.go @@ -0,0 +1,63 @@ +package ordmap + +import ( + "iter" + "sort" + + "golang.org/x/exp/maps" +) + +// ByIndexer is an interface for an ordered map that implements an iterator. +type ByIndexer[K comparable, V any] interface { + // ByIndex returns a sequence of key-value pairs sorted by index. + ByIndex() iter.Seq2[K, V] +} + +// ByIndex is a helper function for an ordered map to implement an iterator. +func ByIndex[M ~map[K]V, K comparable, V any](m M, getIndex func(V) int) iter.Seq2[K, V] { + // get the keys and sort them by index + keys := maps.Keys(m) + sort.Slice(keys, func(i, j int) bool { + idxI := getIndex(m[keys[i]]) + idxJ := getIndex(m[keys[j]]) + return idxI != 0 && // if i is not initialized, it should be at the end + (idxJ == 0 || // if j is not initialized, it should be at the end + idxI < idxJ) // otherwise, sort by index + }) + + return func(yield func(K, V) bool) { + for _, k := range keys { + if !yield(k, m[k]) { + return + } + } + } +} + +// ByIndex returns a sequence of key-value pairs sorted by index. +func (om OrderedMap[K, V]) ByIndex() iter.Seq2[K, V] { + // get the keys and sort them by index + keys := maps.Keys(om) + sort.Slice(keys, func(i, j int) bool { + return om[keys[i]].idx != 0 && // if i is not initialized, it should be at the end + (om[keys[j]].idx == 0 || // if j is not initialized, it should be at the end + om[keys[i]].idx < om[keys[j]].idx) // otherwise, sort by index + }) + + return func(yield func(K, V) bool) { + for _, k := range keys { + if !yield(k, om[k].V) { + return + } + } + } + + // NOTE: The above is equivalent to the following: + // return func(yield func(K, V) bool) { + // for k, v := range ByIndex(om, getIndex) { + // if !yield(k, v.V) { + // return + // } + // } + // } +} diff --git a/asyncapi/vendor/github.com/MarkRosemaker/ordmap/json_marshal_v2.go b/asyncapi/vendor/github.com/MarkRosemaker/ordmap/json_marshal_v2.go new file mode 100644 index 0000000..2e2c44c --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/ordmap/json_marshal_v2.go @@ -0,0 +1,37 @@ +package ordmap + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "fmt" + + "github.com/MarkRosemaker/errpath" +) + +var _ json.MarshalerTo = (*OrderedMap[string, any])(nil) + +// MarshalJSONTo marshals the key-value pairs in order. +func (om *OrderedMap[_, _]) MarshalJSONTo(enc *jsontext.Encoder) error { + return MarshalJSONTo(om, enc) +} + +// MarshalJSONTo marshals an ordered map by encoding its key-value pairs in order. +func MarshalJSONTo[M ByIndexer[K, V], K comparable, V any]( + m M, enc *jsontext.Encoder, +) error { + if err := enc.WriteToken(jsontext.BeginObject); err != nil { + return err // should never fail + } + + for k, v := range m.ByIndex() { + if err := json.MarshalEncode(enc, k, enc.Options()); err != nil { + return err + } + + if err := json.MarshalEncode(enc, v, enc.Options()); err != nil { + return &errpath.ErrKey{Key: fmt.Sprint(k), Err: err} + } + } + + return enc.WriteToken(jsontext.EndObject) +} diff --git a/asyncapi/vendor/github.com/MarkRosemaker/ordmap/json_unmarshal_v2.go b/asyncapi/vendor/github.com/MarkRosemaker/ordmap/json_unmarshal_v2.go new file mode 100644 index 0000000..43d2df1 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/ordmap/json_unmarshal_v2.go @@ -0,0 +1,58 @@ +package ordmap + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "fmt" + + "github.com/MarkRosemaker/errpath" +) + +var _ json.UnmarshalerFrom = (*OrderedMap[string, any])(nil) + +// UnmarshalJSONFrom unmarshals the key-value pairs in order and sets the indices. +func (om *OrderedMap[K, V]) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + return UnmarshalJSONFrom(om, dec, setIndex) +} + +// UnmarshalJSONFrom is a helper function to unmarshal an ordered map setting the indices in order. +func UnmarshalJSONFrom[M ~map[K]R, K comparable, R any]( + m *M, dec *jsontext.Decoder, + setIndex func(R, int) R, +) error { + tkn, err := dec.ReadToken() + if err != nil { + return err + } + + if tkn.Kind() != '{' { + return fmt.Errorf("expected {, got %s", tkn.Kind()) + } + + // create the map + *m = M{} + + i := 1 // start at 1 to avoid confusion with zero values + + for { + // check if we reached the end of the object + if dec.PeekKind() == '}' { + _, err := dec.ReadToken() // consume '}', should not fail + return err + } + + var key K + if err := json.UnmarshalDecode(dec, &key, dec.Options()); err != nil { + return err + } + + var v R + if err := json.UnmarshalDecode(dec, &v, dec.Options()); err != nil { + return &errpath.ErrKey{Key: fmt.Sprint(key), Err: err} + } + + // set the variable in the map with the proper index + (*m)[K(key)] = setIndex(v, i) + i++ + } +} diff --git a/asyncapi/vendor/github.com/MarkRosemaker/ordmap/logo.jpg b/asyncapi/vendor/github.com/MarkRosemaker/ordmap/logo.jpg new file mode 100644 index 0000000..651a4ac Binary files /dev/null and b/asyncapi/vendor/github.com/MarkRosemaker/ordmap/logo.jpg differ diff --git a/asyncapi/vendor/github.com/MarkRosemaker/ordmap/ordered_map.go b/asyncapi/vendor/github.com/MarkRosemaker/ordmap/ordered_map.go new file mode 100644 index 0000000..86feb27 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/ordmap/ordered_map.go @@ -0,0 +1,6 @@ +package ordmap + +// OrderedMap is a map that can be ordered. +type OrderedMap[K comparable, V any] map[K]Value[V] + +// NOTE: For the methods of OrderedMap, see the respective files. diff --git a/asyncapi/vendor/github.com/MarkRosemaker/ordmap/set.go b/asyncapi/vendor/github.com/MarkRosemaker/ordmap/set.go new file mode 100644 index 0000000..7dc980c --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/ordmap/set.go @@ -0,0 +1,28 @@ +package ordmap + +// Set sets a value in the map, adding it at the end of the order. +func (om *OrderedMap[K, V]) Set(key K, v V) { + Set(om, key, Value[V]{V: v}, getIndex[V], setIndex[V]) +} + +// Set is a helper function to set a value in the map, adding it at the end of the order. +func Set[M ~map[K]V, K comparable, V any]( + m *M, key K, v V, + getIndex func(V) int, + setIndex func(V, int) V, +) { + // check if the map is nil and create it if it is + if *m == nil { + *m = M{key: setIndex(v, 1)} + return + } + + highestIdx := 0 + for _, v := range *m { + if idx := getIndex(v); idx > highestIdx { + highestIdx = idx + } + } + + (*m)[key] = setIndex(v, highestIdx+1) +} diff --git a/asyncapi/vendor/github.com/MarkRosemaker/ordmap/sort.go b/asyncapi/vendor/github.com/MarkRosemaker/ordmap/sort.go new file mode 100644 index 0000000..ec32a40 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/ordmap/sort.go @@ -0,0 +1,32 @@ +package ordmap + +import ( + "cmp" + "slices" + + "golang.org/x/exp/maps" +) + +// Sort sorts the map by key using a custom comparison function and sets the indices accordingly. +func (om OrderedMap[K, V]) Sort(less func(K, K) int) { + SortFunc(om, setIndex, less) +} + +// Sort is a helper function to sort a map by key and set the indices accordingly. +func Sort[M ~map[K]V, K cmp.Ordered, V any](m M, setIndex func(V, int) V) { + doSort(m, setIndex, slices.Sort) +} + +// SortFunc is a helper function to sort a map by key using a custom comparison function and set the indices accordingly. +func SortFunc[M ~map[K]V, K comparable, V any](m M, setIndex func(V, int) V, less func(K, K) int) { + doSort(m, setIndex, func(keys []K) { slices.SortFunc(keys, less) }) +} + +func doSort[M ~map[K]V, K comparable, V any](m M, setIndex func(V, int) V, sortKeys func([]K)) { + keys := maps.Keys(m) + sortKeys(keys) + + for i, key := range keys { + m[key] = setIndex(m[key], i+1) + } +} diff --git a/asyncapi/vendor/github.com/MarkRosemaker/ordmap/value.go b/asyncapi/vendor/github.com/MarkRosemaker/ordmap/value.go new file mode 100644 index 0000000..e801da1 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/ordmap/value.go @@ -0,0 +1,34 @@ +package ordmap + +import ( + "encoding/json/jsontext" + "encoding/json/v2" +) + +var ( + _ json.UnmarshalerFrom = (*Value[any])(nil) + _ json.MarshalerTo = (*Value[any])(nil) +) + +// Value is a value with an index. +type Value[V any] struct { + V V + idx int +} + +// UnmarshalJSONFrom unmarshals a value by just decoding the value. +// The index is set by the caller. +func (cs *Value[_]) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + return json.UnmarshalDecode(dec, &cs.V, dec.Options()) +} + +// MarshalJSONTo marshals a value by encoding just the value and ignoring the index. +func (v Value[_]) MarshalJSONTo(enc *jsontext.Encoder) error { + return json.MarshalEncode(enc, v.V, enc.Options()) +} + +// getIndex returns the index of a value. +func getIndex[V any](v Value[V]) int { return v.idx } + +// setIndex sets the index of a value. +func setIndex[V any](v Value[V], i int) Value[V] { v.idx = i; return v } diff --git a/asyncapi/vendor/github.com/MarkRosemaker/yaml/.gitattributes b/asyncapi/vendor/github.com/MarkRosemaker/yaml/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/yaml/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/asyncapi/vendor/github.com/MarkRosemaker/yaml/.gitignore b/asyncapi/vendor/github.com/MarkRosemaker/yaml/.gitignore new file mode 100644 index 0000000..6f6a3a4 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/yaml/.gitignore @@ -0,0 +1,28 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with 'go test -c' +*.test + +# Output files, e.g. from the go coverage tool +*.out + +# IDE directories +.idea +.vscode +.schemas +.history + +# Build and Environment +cmd/app/app +dist/ + +# Log Files +*.log + +# macOS files +*.DS_Store diff --git a/asyncapi/vendor/github.com/MarkRosemaker/yaml/README.md b/asyncapi/vendor/github.com/MarkRosemaker/yaml/README.md new file mode 100644 index 0000000..95514d1 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/yaml/README.md @@ -0,0 +1,68 @@ +# YAML Marshalling and Unmarshalling + +This repository provides functionality for YAML marshalling and unmarshalling similar to [JSON v2](https://pkg.go.dev/encoding/json/v2). It allows users to use the same JSON options they would use for JSON v2 to encode and decode YAML. + +## Features + +- Marshal Go structs to YAML +- Unmarshal YAML to Go structs +- Support for JSON v2 options + +## Installation + +To install the package, use the following command: + +```sh +go get github.com/MarkRosemaker/yaml +``` + +## Usage + +Here is a basic example of how to use the package: + +```go +package main + +import ( + "fmt" + "github.com/MarkRosemaker/yaml" +) + +type Example struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + ex := Example{Name: "John Doe", Age: 30} + + // Marshal to YAML + yamlData, err := yaml.Marshal(ex) + if (err != nil) { + fmt.Println("Error marshalling to YAML:", err) + return + } + fmt.Println("YAML Data:", string(yamlData)) + + // Unmarshal from YAML + var ex2 Example + err = yaml.Unmarshal(yamlData, &ex2) + if (err != nil) { + fmt.Println("Error unmarshalling from YAML:", err) + return + } + fmt.Println("Unmarshalled Struct:", ex2) +} +``` + +## JSON v2 Options + +This package supports the same options available in JSON v2 for encoding and decoding. You can use json struct tags to customize the YAML output just as you would with JSON. + +## Contributing + +Contributions are welcome! Please open an issue or submit a pull request. + +## License + +This project is licensed under the MIT License. diff --git a/asyncapi/vendor/github.com/MarkRosemaker/yaml/arshal.go b/asyncapi/vendor/github.com/MarkRosemaker/yaml/arshal.go new file mode 100644 index 0000000..8be4b05 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/yaml/arshal.go @@ -0,0 +1,63 @@ +package yaml + +import ( + "encoding/json/v2" + "io" + + "github.com/MarkRosemaker/json2yaml" + "github.com/MarkRosemaker/yaml2json" + "gopkg.in/yaml.v3" +) + +// Marshal serializes a Go value as a []byte according to the provided +// marshal and encode options (while ignoring unmarshal or decode options). +func Marshal(in any, opts ...json.Options) ([]byte, error) { + // marshal as json with the given options + out, err := json.Marshal(in, opts...) + if err != nil { + return nil, err + } + + // convert to yaml + n, err := json2yaml.Convert(out) + if err != nil { + return nil, err + } + + // marshal the yaml to bytes + return yaml.Marshal(n) +} + +// Unmarshal decodes a []byte input into a Go value according to the provided +// unmarshal and decode options (while ignoring marshal or encode options). +// The output must be a non-nil pointer. +func Unmarshal(in []byte, out any, opts ...json.Options) error { + // parse input into a yaml document node + n := &yaml.Node{} + if err := yaml.Unmarshal(in, n); err != nil { + return err + } + + // convert the yaml to json + val, err := yaml2json.Convert(n) + if err != nil { + return err + } + + return json.Unmarshal(val, out, opts...) +} + +// UnmarshalRead deserializes a Go value from an [io.Reader] according to the +// provided unmarshal and decode options (while ignoring marshal or encode options). +// It consumes the entirety of [io.Reader] until [io.EOF] is encountered, +// without reporting an error for EOF. The output must be a non-nil pointer. +// See [Unmarshal] for details about the conversion of JSON into a Go value. +func UnmarshalRead(in io.Reader, out any, opts ...json.Options) error { + // read the input into a byte slice + b, err := io.ReadAll(in) + if err != nil { + return err + } + + return Unmarshal(b, out, opts...) +} diff --git a/asyncapi/vendor/github.com/MarkRosemaker/yaml/different order.yaml b/asyncapi/vendor/github.com/MarkRosemaker/yaml/different order.yaml new file mode 100644 index 0000000..23172e7 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/yaml/different order.yaml @@ -0,0 +1,29 @@ +string: Hello, World! +int: 42 +float: 3.14 +bool: true +bool2: false +null_value: null +list: + - item1 + - item2 + - item3 +dictionary: + key2: value2 + key1: value1 +nested: + list_of_dicts: + - name: item1 + value: 1 + - name: item2 + value: 2 + dict_of_lists: + key1: + - item1 + - item2 + key2: + - item3 + - item4 +block: |- + This is a block + style multiline string. diff --git a/asyncapi/vendor/github.com/MarkRosemaker/yaml/example.json b/asyncapi/vendor/github.com/MarkRosemaker/yaml/example.json new file mode 100644 index 0000000..4415e13 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/yaml/example.json @@ -0,0 +1,42 @@ +{ + "string": "Hello, World!", + "int": 42, + "float": 3.14, + "bool": true, + "bool2": false, + "null_value": null, + "list": [ + "item1", + "item2", + "item3" + ], + "dictionary": { + "key1": "value1", + "key2": "value2" + }, + "nested": { + "list_of_dicts": [ + { + "name": "item1", + "value": 1 + }, + { + "name": "item2", + "value": 2 + } + ], + "dict_of_lists": { + "key1": [ + "item1", + "item2" + ], + "key2": [ + "item3", + "item4" + ] + } + }, + "block": "This is a block\nstyle multiline string.\n", + "folded": "This is a folded style multiline string.\n", + "quoted": "This string contains: special characters, colons: and dashes -" + } \ No newline at end of file diff --git a/asyncapi/vendor/github.com/MarkRosemaker/yaml/example.yaml b/asyncapi/vendor/github.com/MarkRosemaker/yaml/example.yaml new file mode 100644 index 0000000..52ebe73 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/yaml/example.yaml @@ -0,0 +1,29 @@ +string: Hello, World! +int: 42 +float: 3.14 +bool: true +bool2: false +null_value: null +list: + - item1 + - item2 + - item3 +dictionary: + key1: value1 + key2: value2 +nested: + list_of_dicts: + - name: item1 + value: 1 + - name: item2 + value: 2 + dict_of_lists: + key1: + - item1 + - item2 + key2: + - item3 + - item4 +block: |- + This is a block + style multiline string. diff --git a/asyncapi/vendor/github.com/MarkRosemaker/yaml2json/.gitattributes b/asyncapi/vendor/github.com/MarkRosemaker/yaml2json/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/yaml2json/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/asyncapi/vendor/github.com/MarkRosemaker/yaml2json/.gitignore b/asyncapi/vendor/github.com/MarkRosemaker/yaml2json/.gitignore new file mode 100644 index 0000000..6f6a3a4 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/yaml2json/.gitignore @@ -0,0 +1,28 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with 'go test -c' +*.test + +# Output files, e.g. from the go coverage tool +*.out + +# IDE directories +.idea +.vscode +.schemas +.history + +# Build and Environment +cmd/app/app +dist/ + +# Log Files +*.log + +# macOS files +*.DS_Store diff --git a/asyncapi/vendor/github.com/MarkRosemaker/yaml2json/convert.go b/asyncapi/vendor/github.com/MarkRosemaker/yaml2json/convert.go new file mode 100644 index 0000000..d5d28b4 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/yaml2json/convert.go @@ -0,0 +1,92 @@ +package yaml2json + +import ( + "bytes" + "encoding/json/jsontext" + "fmt" + "io" + "strconv" + + "gopkg.in/yaml.v3" +) + +// Convert converts a YAML node to a JSON. +func Convert(n *yaml.Node) (jsontext.Value, error) { + w := &bytes.Buffer{} + if err := encodeToJSON(jsontext.NewEncoder(w), n); err != nil { + return nil, err + } + + return jsontext.Value(w.Bytes()), nil +} + +func encodeToJSON(enc *jsontext.Encoder, n *yaml.Node) error { + switch n.Kind { + case yaml.DocumentNode: + if len(n.Content) != 1 { + return fmt.Errorf("expected 1 content node, got %d", len(n.Content)) + } + + return encodeToJSON(enc, n.Content[0]) + case yaml.SequenceNode: + if err := enc.WriteToken(jsontext.BeginArray); err != nil { + return err + } + + for _, c := range n.Content { + if err := encodeToJSON(enc, c); err != nil { + return err + } + } + + return enc.WriteToken(jsontext.EndArray) + case yaml.MappingNode: + l := len(n.Content) + if l%2 != 0 { + return fmt.Errorf("unbalanced mapping node") + } + + if err := enc.WriteToken(jsontext.BeginObject); err != nil { + return err + } + + for i := 0; i < l; i += 2 { + if err := encodeToJSON(enc, n.Content[i]); err != nil { + return err + } + + if err := encodeToJSON(enc, n.Content[i+1]); err != nil { + return err + } + } + + return enc.WriteToken(jsontext.EndObject) + case yaml.ScalarNode: + if n.Style == 0 { + switch n.Value { + case "null": + return enc.WriteToken(jsontext.Null) + case "true": + return enc.WriteToken(jsontext.True) + case "false": + return enc.WriteToken(jsontext.False) + } + + if n, err := strconv.ParseInt(n.Value, 10, 64); err == nil { + return enc.WriteToken(jsontext.Int(n)) + } + + if n, err := strconv.ParseFloat(n.Value, 64); err == nil { + return enc.WriteToken(jsontext.Float(n)) + } + } + + return enc.WriteToken(jsontext.String(n.Value)) + case yaml.AliasNode: + return encodeToJSON(enc, n.Alias) + case 0: + return io.EOF + default: + return fmt.Errorf("unsupported node kind: %v", n.Kind) + } +} diff --git a/asyncapi/vendor/github.com/MarkRosemaker/yaml2json/example.json b/asyncapi/vendor/github.com/MarkRosemaker/yaml2json/example.json new file mode 100644 index 0000000..cc0c513 --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/yaml2json/example.json @@ -0,0 +1,55 @@ +{ + "string": "Hello, World!", + "int": 42, + "float": 3.14, + "bool": true, + "bool2": false, + "null_value": null, + "list": [ + "item1", + "item2", + "item3" + ], + "dictionary": { + "key1": "value1", + "key2": "value2" + }, + "nested": { + "list_of_dicts": [ + { + "name": "item1", + "value": 1 + }, + { + "name": "item2", + "value": 2 + } + ], + "dict_of_lists": { + "key1": [ + "item1", + "item2" + ], + "key2": [ + "item3", + "item4" + ] + } + }, + "block": "This is a block\nstyle multiline string.\n", + "folded": "This is a folded style multiline string.\n", + "quoted": "This string contains: special characters, colons: and dashes -", + "foo": { + "bar": { + "qux": "quxqux", + "baz": "bazbaz" + } + }, + "greeting": { + "audience": "world", + "bar": { + "qux": "quxqux", + "baz": "bazbaz" + } + } +} \ No newline at end of file diff --git a/asyncapi/vendor/github.com/MarkRosemaker/yaml2json/example.yaml b/asyncapi/vendor/github.com/MarkRosemaker/yaml2json/example.yaml new file mode 100644 index 0000000..2e7da8e --- /dev/null +++ b/asyncapi/vendor/github.com/MarkRosemaker/yaml2json/example.yaml @@ -0,0 +1,54 @@ +# Scalars +string: "Hello, World!" +int: 42 +float: 3.14 +bool: true +bool2: false +null_value: null + +# Sequences (Lists) +list: + - item1 + - item2 + - item3 + +# Mappings (Dictionaries) +dictionary: + key1: value1 + key2: value2 + +# Nested structures +nested: + list_of_dicts: + - name: item1 + value: 1 + - name: item2 + value: 2 + dict_of_lists: + key1: + - item1 + - item2 + key2: + - item3 + - item4 + +# Multiline strings +block: | + This is a block + style multiline string. +folded: > + This is a folded + style multiline string. + +# Special characters +quoted: "This string contains: special characters, colons: and dashes -" + +# Aliases +foo: + bar: &bar + qux: 'quxqux' + baz: 'bazbaz' +greeting: + audience: 'world' + bar: *bar #greeting.bar has the same values as foo.bar. + #So greeting.bar.baz is 'bazbaz' diff --git a/asyncapi/vendor/github.com/go-api-libs/types/.gitattributes b/asyncapi/vendor/github.com/go-api-libs/types/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/asyncapi/vendor/github.com/go-api-libs/types/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/asyncapi/vendor/github.com/go-api-libs/types/.gitignore b/asyncapi/vendor/github.com/go-api-libs/types/.gitignore new file mode 100644 index 0000000..6f6a3a4 --- /dev/null +++ b/asyncapi/vendor/github.com/go-api-libs/types/.gitignore @@ -0,0 +1,28 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with 'go test -c' +*.test + +# Output files, e.g. from the go coverage tool +*.out + +# IDE directories +.idea +.vscode +.schemas +.history + +# Build and Environment +cmd/app/app +dist/ + +# Log Files +*.log + +# macOS files +*.DS_Store diff --git a/asyncapi/vendor/github.com/go-api-libs/types/LICENSE b/asyncapi/vendor/github.com/go-api-libs/types/LICENSE new file mode 100644 index 0000000..4531175 --- /dev/null +++ b/asyncapi/vendor/github.com/go-api-libs/types/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Mark Rosemaker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/asyncapi/vendor/github.com/go-api-libs/types/README.md b/asyncapi/vendor/github.com/go-api-libs/types/README.md new file mode 100644 index 0000000..185528b --- /dev/null +++ b/asyncapi/vendor/github.com/go-api-libs/types/README.md @@ -0,0 +1,56 @@ +# Types +[![Go Reference](https://pkg.go.dev/badge/github.com/go-api-libs/types.svg)](https://pkg.go.dev/github.com/go-api-libs/types) +[![Go Report Card](https://goreportcard.com/badge/github.com/go-api-libs/types)](https://goreportcard.com/report/github.com/go-api-libs/types) +![Code Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE) + +This library offers a collection of commonly used data types in API contexts, ensuring correct marshalling, unmarshalling, and validation. By standardizing these types across all our API libraries, we aim to reduce redundancy, increase consistency, and enhance type safety. + +## Features + +- **Standardized Types**: Tailored for API data interchange +- **Validation**: Built-in validation rules to ensure data integrity before processing or transmission. +- **JSON Marshalling/Unmarshalling**: Custom JSON handling for each type to handle special cases (e.g., ensuring email addresses are formatted correctly). +- **Used Across go-api-libs**: These types are employed by all API libraries within the [go-api-libs](https://github.com/go-api-libs/) organization for consistency. + +## Installation + +To install the library, use the following command: + +```shell +go get github.com/go-api-libs/types +``` + +## Usage + +When using any of the API libraries from the [go-api-libs](https://github.com/go-api-libs/) organization, you can trust that types in successful API responses are valid. + +Beyond that, here's how you can use some types from this library: + +```go +package main + +import ( + "fmt" + + "github.com/go-api-libs/types" +) + +func main() { + // Using Email type + email := types.Email("user@example.com") + if err := email.Validate(); err != nil { + fmt.Println(err) + } else { + fmt.Println("Valid email address") + } +} +``` + +## Contributing + +If you have any contributions to make, please submit a pull request or open an issue on the [GitHub repository](https://github.com/go-api-libs/types). + +## License + +This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details. diff --git a/asyncapi/vendor/github.com/go-api-libs/types/email.go b/asyncapi/vendor/github.com/go-api-libs/types/email.go new file mode 100644 index 0000000..eb44051 --- /dev/null +++ b/asyncapi/vendor/github.com/go-api-libs/types/email.go @@ -0,0 +1,44 @@ +package types + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "errors" + "fmt" + "regexp" +) + +// regex from https://emailregex.com/ +var reEmail = regexp.MustCompile(`(?m)(?:[a-z0-9!#$%&'*+/=?^_` + "`" + `{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_` + "`" + `{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])`) + +// ErrInvalidEmail is returned when an email is invalid. +var ErrInvalidEmail = errors.New("invalid email") + +// Email is a string that represents an email address. +type Email string + +// Validate checks if the email is valid. +func (e Email) Validate() error { + if !reEmail.MatchString(string(e)) { + return fmt.Errorf("%w: %q", ErrInvalidEmail, e) + } + + return nil +} + +var _ json.UnmarshalerFrom = (*Email)(nil) + +func (e *Email) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + s := "" + if err := json.UnmarshalDecode(dec, &s); err != nil { + return err + } + + if s == "" { + return nil // no email + } + + *e = Email(s) + + return e.Validate() +} diff --git a/asyncapi/vendor/golang.org/x/exp/LICENSE b/asyncapi/vendor/golang.org/x/exp/LICENSE new file mode 100644 index 0000000..2a7cf70 --- /dev/null +++ b/asyncapi/vendor/golang.org/x/exp/LICENSE @@ -0,0 +1,27 @@ +Copyright 2009 The Go Authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google LLC nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/asyncapi/vendor/golang.org/x/exp/PATENTS b/asyncapi/vendor/golang.org/x/exp/PATENTS new file mode 100644 index 0000000..7330990 --- /dev/null +++ b/asyncapi/vendor/golang.org/x/exp/PATENTS @@ -0,0 +1,22 @@ +Additional IP Rights Grant (Patents) + +"This implementation" means the copyrightable works distributed by +Google as part of the Go project. + +Google hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) +patent license to make, have made, use, offer to sell, sell, import, +transfer and otherwise run, modify and propagate the contents of this +implementation of Go, where such license applies only to those patent +claims, both currently owned or controlled by Google and acquired in +the future, licensable by Google that are necessarily infringed by this +implementation of Go. This grant does not include claims that would be +infringed only as a consequence of further modification of this +implementation. If you or your agent or exclusive licensee institute or +order or agree to the institution of patent litigation against any +entity (including a cross-claim or counterclaim in a lawsuit) alleging +that this implementation of Go or any code incorporated within this +implementation of Go constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any patent +rights granted to you under this License for this implementation of Go +shall terminate as of the date such litigation is filed. diff --git a/asyncapi/vendor/golang.org/x/exp/maps/maps.go b/asyncapi/vendor/golang.org/x/exp/maps/maps.go new file mode 100644 index 0000000..4a9747e --- /dev/null +++ b/asyncapi/vendor/golang.org/x/exp/maps/maps.go @@ -0,0 +1,86 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package maps defines various functions useful with maps of any type. +package maps + +import "maps" + +// Keys returns the keys of the map m. +// The keys will be in an indeterminate order. +// +// The simplest true equivalent using the standard library is: +// +// slices.AppendSeq(make([]K, 0, len(m)), maps.Keys(m)) +func Keys[M ~map[K]V, K comparable, V any](m M) []K { + + r := make([]K, 0, len(m)) + for k := range m { + r = append(r, k) + } + return r +} + +// Values returns the values of the map m. +// The values will be in an indeterminate order. +// +// The simplest true equivalent using the standard library is: +// +// slices.AppendSeq(make([]V, 0, len(m)), maps.Values(m)) +func Values[M ~map[K]V, K comparable, V any](m M) []V { + + r := make([]V, 0, len(m)) + for _, v := range m { + r = append(r, v) + } + return r +} + +// Equal reports whether two maps contain the same key/value pairs. +// Values are compared using ==. +// +//go:fix inline +func Equal[M1, M2 ~map[K]V, K, V comparable](m1 M1, m2 M2) bool { + return maps.Equal(m1, m2) +} + +// EqualFunc is like Equal, but compares values using eq. +// Keys are still compared with ==. +// +//go:fix inline +func EqualFunc[M1 ~map[K]V1, M2 ~map[K]V2, K comparable, V1, V2 any](m1 M1, m2 M2, eq func(V1, V2) bool) bool { + return maps.EqualFunc(m1, m2, eq) +} + +// Clear removes all entries from m, leaving it empty. +// +//go:fix inline +func Clear[M ~map[K]V, K comparable, V any](m M) { + clear(m) +} + +// Clone returns a copy of m. This is a shallow clone: +// the new keys and values are set using ordinary assignment. +// +//go:fix inline +func Clone[M ~map[K]V, K comparable, V any](m M) M { + return maps.Clone(m) +} + +// Copy copies all key/value pairs in src adding them to dst. +// When a key in src is already present in dst, +// the value in dst will be overwritten by the value associated +// with the key in src. +// +//go:fix inline +func Copy[M1 ~map[K]V, M2 ~map[K]V, K comparable, V any](dst M1, src M2) { + maps.Copy(dst, src) +} + +// DeleteFunc deletes any key/value pairs from m for which del returns true. +// +//go:fix inline +func DeleteFunc[M ~map[K]V, K comparable, V any](m M, del func(K, V) bool) { + maps.DeleteFunc(m, del) +} diff --git a/asyncapi/vendor/gopkg.in/yaml.v3/LICENSE b/asyncapi/vendor/gopkg.in/yaml.v3/LICENSE new file mode 100644 index 0000000..2683e4b --- /dev/null +++ b/asyncapi/vendor/gopkg.in/yaml.v3/LICENSE @@ -0,0 +1,50 @@ + +This project is covered by two different licenses: MIT and Apache. + +#### MIT License #### + +The following files were ported to Go from C files of libyaml, and thus +are still covered by their original MIT license, with the additional +copyright staring in 2011 when the project was ported over: + + apic.go emitterc.go parserc.go readerc.go scannerc.go + writerc.go yamlh.go yamlprivateh.go + +Copyright (c) 2006-2010 Kirill Simonov +Copyright (c) 2006-2011 Kirill Simonov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +### Apache License ### + +All the remaining project files are covered by the Apache license: + +Copyright (c) 2011-2019 Canonical Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/asyncapi/vendor/gopkg.in/yaml.v3/NOTICE b/asyncapi/vendor/gopkg.in/yaml.v3/NOTICE new file mode 100644 index 0000000..866d74a --- /dev/null +++ b/asyncapi/vendor/gopkg.in/yaml.v3/NOTICE @@ -0,0 +1,13 @@ +Copyright 2011-2016 Canonical Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/asyncapi/vendor/gopkg.in/yaml.v3/README.md b/asyncapi/vendor/gopkg.in/yaml.v3/README.md new file mode 100644 index 0000000..08eb1ba --- /dev/null +++ b/asyncapi/vendor/gopkg.in/yaml.v3/README.md @@ -0,0 +1,150 @@ +# YAML support for the Go language + +Introduction +------------ + +The yaml package enables Go programs to comfortably encode and decode YAML +values. It was developed within [Canonical](https://www.canonical.com) as +part of the [juju](https://juju.ubuntu.com) project, and is based on a +pure Go port of the well-known [libyaml](http://pyyaml.org/wiki/LibYAML) +C library to parse and generate YAML data quickly and reliably. + +Compatibility +------------- + +The yaml package supports most of YAML 1.2, but preserves some behavior +from 1.1 for backwards compatibility. + +Specifically, as of v3 of the yaml package: + + - YAML 1.1 bools (_yes/no, on/off_) are supported as long as they are being + decoded into a typed bool value. Otherwise they behave as a string. Booleans + in YAML 1.2 are _true/false_ only. + - Octals encode and decode as _0777_ per YAML 1.1, rather than _0o777_ + as specified in YAML 1.2, because most parsers still use the old format. + Octals in the _0o777_ format are supported though, so new files work. + - Does not support base-60 floats. These are gone from YAML 1.2, and were + actually never supported by this package as it's clearly a poor choice. + +and offers backwards +compatibility with YAML 1.1 in some cases. +1.2, including support for +anchors, tags, map merging, etc. Multi-document unmarshalling is not yet +implemented, and base-60 floats from YAML 1.1 are purposefully not +supported since they're a poor design and are gone in YAML 1.2. + +Installation and usage +---------------------- + +The import path for the package is *gopkg.in/yaml.v3*. + +To install it, run: + + go get gopkg.in/yaml.v3 + +API documentation +----------------- + +If opened in a browser, the import path itself leads to the API documentation: + + - [https://gopkg.in/yaml.v3](https://gopkg.in/yaml.v3) + +API stability +------------- + +The package API for yaml v3 will remain stable as described in [gopkg.in](https://gopkg.in). + + +License +------- + +The yaml package is licensed under the MIT and Apache License 2.0 licenses. +Please see the LICENSE file for details. + + +Example +------- + +```Go +package main + +import ( + "fmt" + "log" + + "gopkg.in/yaml.v3" +) + +var data = ` +a: Easy! +b: + c: 2 + d: [3, 4] +` + +// Note: struct fields must be public in order for unmarshal to +// correctly populate the data. +type T struct { + A string + B struct { + RenamedC int `yaml:"c"` + D []int `yaml:",flow"` + } +} + +func main() { + t := T{} + + err := yaml.Unmarshal([]byte(data), &t) + if err != nil { + log.Fatalf("error: %v", err) + } + fmt.Printf("--- t:\n%v\n\n", t) + + d, err := yaml.Marshal(&t) + if err != nil { + log.Fatalf("error: %v", err) + } + fmt.Printf("--- t dump:\n%s\n\n", string(d)) + + m := make(map[interface{}]interface{}) + + err = yaml.Unmarshal([]byte(data), &m) + if err != nil { + log.Fatalf("error: %v", err) + } + fmt.Printf("--- m:\n%v\n\n", m) + + d, err = yaml.Marshal(&m) + if err != nil { + log.Fatalf("error: %v", err) + } + fmt.Printf("--- m dump:\n%s\n\n", string(d)) +} +``` + +This example will generate the following output: + +``` +--- t: +{Easy! {2 [3 4]}} + +--- t dump: +a: Easy! +b: + c: 2 + d: [3, 4] + + +--- m: +map[a:Easy! b:map[c:2 d:[3 4]]] + +--- m dump: +a: Easy! +b: + c: 2 + d: + - 3 + - 4 +``` + diff --git a/asyncapi/vendor/gopkg.in/yaml.v3/apic.go b/asyncapi/vendor/gopkg.in/yaml.v3/apic.go new file mode 100644 index 0000000..ae7d049 --- /dev/null +++ b/asyncapi/vendor/gopkg.in/yaml.v3/apic.go @@ -0,0 +1,747 @@ +// +// Copyright (c) 2011-2019 Canonical Ltd +// Copyright (c) 2006-2010 Kirill Simonov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package yaml + +import ( + "io" +) + +func yaml_insert_token(parser *yaml_parser_t, pos int, token *yaml_token_t) { + //fmt.Println("yaml_insert_token", "pos:", pos, "typ:", token.typ, "head:", parser.tokens_head, "len:", len(parser.tokens)) + + // Check if we can move the queue at the beginning of the buffer. + if parser.tokens_head > 0 && len(parser.tokens) == cap(parser.tokens) { + if parser.tokens_head != len(parser.tokens) { + copy(parser.tokens, parser.tokens[parser.tokens_head:]) + } + parser.tokens = parser.tokens[:len(parser.tokens)-parser.tokens_head] + parser.tokens_head = 0 + } + parser.tokens = append(parser.tokens, *token) + if pos < 0 { + return + } + copy(parser.tokens[parser.tokens_head+pos+1:], parser.tokens[parser.tokens_head+pos:]) + parser.tokens[parser.tokens_head+pos] = *token +} + +// Create a new parser object. +func yaml_parser_initialize(parser *yaml_parser_t) bool { + *parser = yaml_parser_t{ + raw_buffer: make([]byte, 0, input_raw_buffer_size), + buffer: make([]byte, 0, input_buffer_size), + } + return true +} + +// Destroy a parser object. +func yaml_parser_delete(parser *yaml_parser_t) { + *parser = yaml_parser_t{} +} + +// String read handler. +func yaml_string_read_handler(parser *yaml_parser_t, buffer []byte) (n int, err error) { + if parser.input_pos == len(parser.input) { + return 0, io.EOF + } + n = copy(buffer, parser.input[parser.input_pos:]) + parser.input_pos += n + return n, nil +} + +// Reader read handler. +func yaml_reader_read_handler(parser *yaml_parser_t, buffer []byte) (n int, err error) { + return parser.input_reader.Read(buffer) +} + +// Set a string input. +func yaml_parser_set_input_string(parser *yaml_parser_t, input []byte) { + if parser.read_handler != nil { + panic("must set the input source only once") + } + parser.read_handler = yaml_string_read_handler + parser.input = input + parser.input_pos = 0 +} + +// Set a file input. +func yaml_parser_set_input_reader(parser *yaml_parser_t, r io.Reader) { + if parser.read_handler != nil { + panic("must set the input source only once") + } + parser.read_handler = yaml_reader_read_handler + parser.input_reader = r +} + +// Set the source encoding. +func yaml_parser_set_encoding(parser *yaml_parser_t, encoding yaml_encoding_t) { + if parser.encoding != yaml_ANY_ENCODING { + panic("must set the encoding only once") + } + parser.encoding = encoding +} + +// Create a new emitter object. +func yaml_emitter_initialize(emitter *yaml_emitter_t) { + *emitter = yaml_emitter_t{ + buffer: make([]byte, output_buffer_size), + raw_buffer: make([]byte, 0, output_raw_buffer_size), + states: make([]yaml_emitter_state_t, 0, initial_stack_size), + events: make([]yaml_event_t, 0, initial_queue_size), + best_width: -1, + } +} + +// Destroy an emitter object. +func yaml_emitter_delete(emitter *yaml_emitter_t) { + *emitter = yaml_emitter_t{} +} + +// String write handler. +func yaml_string_write_handler(emitter *yaml_emitter_t, buffer []byte) error { + *emitter.output_buffer = append(*emitter.output_buffer, buffer...) + return nil +} + +// yaml_writer_write_handler uses emitter.output_writer to write the +// emitted text. +func yaml_writer_write_handler(emitter *yaml_emitter_t, buffer []byte) error { + _, err := emitter.output_writer.Write(buffer) + return err +} + +// Set a string output. +func yaml_emitter_set_output_string(emitter *yaml_emitter_t, output_buffer *[]byte) { + if emitter.write_handler != nil { + panic("must set the output target only once") + } + emitter.write_handler = yaml_string_write_handler + emitter.output_buffer = output_buffer +} + +// Set a file output. +func yaml_emitter_set_output_writer(emitter *yaml_emitter_t, w io.Writer) { + if emitter.write_handler != nil { + panic("must set the output target only once") + } + emitter.write_handler = yaml_writer_write_handler + emitter.output_writer = w +} + +// Set the output encoding. +func yaml_emitter_set_encoding(emitter *yaml_emitter_t, encoding yaml_encoding_t) { + if emitter.encoding != yaml_ANY_ENCODING { + panic("must set the output encoding only once") + } + emitter.encoding = encoding +} + +// Set the canonical output style. +func yaml_emitter_set_canonical(emitter *yaml_emitter_t, canonical bool) { + emitter.canonical = canonical +} + +// Set the indentation increment. +func yaml_emitter_set_indent(emitter *yaml_emitter_t, indent int) { + if indent < 2 || indent > 9 { + indent = 2 + } + emitter.best_indent = indent +} + +// Set the preferred line width. +func yaml_emitter_set_width(emitter *yaml_emitter_t, width int) { + if width < 0 { + width = -1 + } + emitter.best_width = width +} + +// Set if unescaped non-ASCII characters are allowed. +func yaml_emitter_set_unicode(emitter *yaml_emitter_t, unicode bool) { + emitter.unicode = unicode +} + +// Set the preferred line break character. +func yaml_emitter_set_break(emitter *yaml_emitter_t, line_break yaml_break_t) { + emitter.line_break = line_break +} + +///* +// * Destroy a token object. +// */ +// +//YAML_DECLARE(void) +//yaml_token_delete(yaml_token_t *token) +//{ +// assert(token); // Non-NULL token object expected. +// +// switch (token.type) +// { +// case YAML_TAG_DIRECTIVE_TOKEN: +// yaml_free(token.data.tag_directive.handle); +// yaml_free(token.data.tag_directive.prefix); +// break; +// +// case YAML_ALIAS_TOKEN: +// yaml_free(token.data.alias.value); +// break; +// +// case YAML_ANCHOR_TOKEN: +// yaml_free(token.data.anchor.value); +// break; +// +// case YAML_TAG_TOKEN: +// yaml_free(token.data.tag.handle); +// yaml_free(token.data.tag.suffix); +// break; +// +// case YAML_SCALAR_TOKEN: +// yaml_free(token.data.scalar.value); +// break; +// +// default: +// break; +// } +// +// memset(token, 0, sizeof(yaml_token_t)); +//} +// +///* +// * Check if a string is a valid UTF-8 sequence. +// * +// * Check 'reader.c' for more details on UTF-8 encoding. +// */ +// +//static int +//yaml_check_utf8(yaml_char_t *start, size_t length) +//{ +// yaml_char_t *end = start+length; +// yaml_char_t *pointer = start; +// +// while (pointer < end) { +// unsigned char octet; +// unsigned int width; +// unsigned int value; +// size_t k; +// +// octet = pointer[0]; +// width = (octet & 0x80) == 0x00 ? 1 : +// (octet & 0xE0) == 0xC0 ? 2 : +// (octet & 0xF0) == 0xE0 ? 3 : +// (octet & 0xF8) == 0xF0 ? 4 : 0; +// value = (octet & 0x80) == 0x00 ? octet & 0x7F : +// (octet & 0xE0) == 0xC0 ? octet & 0x1F : +// (octet & 0xF0) == 0xE0 ? octet & 0x0F : +// (octet & 0xF8) == 0xF0 ? octet & 0x07 : 0; +// if (!width) return 0; +// if (pointer+width > end) return 0; +// for (k = 1; k < width; k ++) { +// octet = pointer[k]; +// if ((octet & 0xC0) != 0x80) return 0; +// value = (value << 6) + (octet & 0x3F); +// } +// if (!((width == 1) || +// (width == 2 && value >= 0x80) || +// (width == 3 && value >= 0x800) || +// (width == 4 && value >= 0x10000))) return 0; +// +// pointer += width; +// } +// +// return 1; +//} +// + +// Create STREAM-START. +func yaml_stream_start_event_initialize(event *yaml_event_t, encoding yaml_encoding_t) { + *event = yaml_event_t{ + typ: yaml_STREAM_START_EVENT, + encoding: encoding, + } +} + +// Create STREAM-END. +func yaml_stream_end_event_initialize(event *yaml_event_t) { + *event = yaml_event_t{ + typ: yaml_STREAM_END_EVENT, + } +} + +// Create DOCUMENT-START. +func yaml_document_start_event_initialize( + event *yaml_event_t, + version_directive *yaml_version_directive_t, + tag_directives []yaml_tag_directive_t, + implicit bool, +) { + *event = yaml_event_t{ + typ: yaml_DOCUMENT_START_EVENT, + version_directive: version_directive, + tag_directives: tag_directives, + implicit: implicit, + } +} + +// Create DOCUMENT-END. +func yaml_document_end_event_initialize(event *yaml_event_t, implicit bool) { + *event = yaml_event_t{ + typ: yaml_DOCUMENT_END_EVENT, + implicit: implicit, + } +} + +// Create ALIAS. +func yaml_alias_event_initialize(event *yaml_event_t, anchor []byte) bool { + *event = yaml_event_t{ + typ: yaml_ALIAS_EVENT, + anchor: anchor, + } + return true +} + +// Create SCALAR. +func yaml_scalar_event_initialize(event *yaml_event_t, anchor, tag, value []byte, plain_implicit, quoted_implicit bool, style yaml_scalar_style_t) bool { + *event = yaml_event_t{ + typ: yaml_SCALAR_EVENT, + anchor: anchor, + tag: tag, + value: value, + implicit: plain_implicit, + quoted_implicit: quoted_implicit, + style: yaml_style_t(style), + } + return true +} + +// Create SEQUENCE-START. +func yaml_sequence_start_event_initialize(event *yaml_event_t, anchor, tag []byte, implicit bool, style yaml_sequence_style_t) bool { + *event = yaml_event_t{ + typ: yaml_SEQUENCE_START_EVENT, + anchor: anchor, + tag: tag, + implicit: implicit, + style: yaml_style_t(style), + } + return true +} + +// Create SEQUENCE-END. +func yaml_sequence_end_event_initialize(event *yaml_event_t) bool { + *event = yaml_event_t{ + typ: yaml_SEQUENCE_END_EVENT, + } + return true +} + +// Create MAPPING-START. +func yaml_mapping_start_event_initialize(event *yaml_event_t, anchor, tag []byte, implicit bool, style yaml_mapping_style_t) { + *event = yaml_event_t{ + typ: yaml_MAPPING_START_EVENT, + anchor: anchor, + tag: tag, + implicit: implicit, + style: yaml_style_t(style), + } +} + +// Create MAPPING-END. +func yaml_mapping_end_event_initialize(event *yaml_event_t) { + *event = yaml_event_t{ + typ: yaml_MAPPING_END_EVENT, + } +} + +// Destroy an event object. +func yaml_event_delete(event *yaml_event_t) { + *event = yaml_event_t{} +} + +///* +// * Create a document object. +// */ +// +//YAML_DECLARE(int) +//yaml_document_initialize(document *yaml_document_t, +// version_directive *yaml_version_directive_t, +// tag_directives_start *yaml_tag_directive_t, +// tag_directives_end *yaml_tag_directive_t, +// start_implicit int, end_implicit int) +//{ +// struct { +// error yaml_error_type_t +// } context +// struct { +// start *yaml_node_t +// end *yaml_node_t +// top *yaml_node_t +// } nodes = { NULL, NULL, NULL } +// version_directive_copy *yaml_version_directive_t = NULL +// struct { +// start *yaml_tag_directive_t +// end *yaml_tag_directive_t +// top *yaml_tag_directive_t +// } tag_directives_copy = { NULL, NULL, NULL } +// value yaml_tag_directive_t = { NULL, NULL } +// mark yaml_mark_t = { 0, 0, 0 } +// +// assert(document) // Non-NULL document object is expected. +// assert((tag_directives_start && tag_directives_end) || +// (tag_directives_start == tag_directives_end)) +// // Valid tag directives are expected. +// +// if (!STACK_INIT(&context, nodes, INITIAL_STACK_SIZE)) goto error +// +// if (version_directive) { +// version_directive_copy = yaml_malloc(sizeof(yaml_version_directive_t)) +// if (!version_directive_copy) goto error +// version_directive_copy.major = version_directive.major +// version_directive_copy.minor = version_directive.minor +// } +// +// if (tag_directives_start != tag_directives_end) { +// tag_directive *yaml_tag_directive_t +// if (!STACK_INIT(&context, tag_directives_copy, INITIAL_STACK_SIZE)) +// goto error +// for (tag_directive = tag_directives_start +// tag_directive != tag_directives_end; tag_directive ++) { +// assert(tag_directive.handle) +// assert(tag_directive.prefix) +// if (!yaml_check_utf8(tag_directive.handle, +// strlen((char *)tag_directive.handle))) +// goto error +// if (!yaml_check_utf8(tag_directive.prefix, +// strlen((char *)tag_directive.prefix))) +// goto error +// value.handle = yaml_strdup(tag_directive.handle) +// value.prefix = yaml_strdup(tag_directive.prefix) +// if (!value.handle || !value.prefix) goto error +// if (!PUSH(&context, tag_directives_copy, value)) +// goto error +// value.handle = NULL +// value.prefix = NULL +// } +// } +// +// DOCUMENT_INIT(*document, nodes.start, nodes.end, version_directive_copy, +// tag_directives_copy.start, tag_directives_copy.top, +// start_implicit, end_implicit, mark, mark) +// +// return 1 +// +//error: +// STACK_DEL(&context, nodes) +// yaml_free(version_directive_copy) +// while (!STACK_EMPTY(&context, tag_directives_copy)) { +// value yaml_tag_directive_t = POP(&context, tag_directives_copy) +// yaml_free(value.handle) +// yaml_free(value.prefix) +// } +// STACK_DEL(&context, tag_directives_copy) +// yaml_free(value.handle) +// yaml_free(value.prefix) +// +// return 0 +//} +// +///* +// * Destroy a document object. +// */ +// +//YAML_DECLARE(void) +//yaml_document_delete(document *yaml_document_t) +//{ +// struct { +// error yaml_error_type_t +// } context +// tag_directive *yaml_tag_directive_t +// +// context.error = YAML_NO_ERROR // Eliminate a compiler warning. +// +// assert(document) // Non-NULL document object is expected. +// +// while (!STACK_EMPTY(&context, document.nodes)) { +// node yaml_node_t = POP(&context, document.nodes) +// yaml_free(node.tag) +// switch (node.type) { +// case YAML_SCALAR_NODE: +// yaml_free(node.data.scalar.value) +// break +// case YAML_SEQUENCE_NODE: +// STACK_DEL(&context, node.data.sequence.items) +// break +// case YAML_MAPPING_NODE: +// STACK_DEL(&context, node.data.mapping.pairs) +// break +// default: +// assert(0) // Should not happen. +// } +// } +// STACK_DEL(&context, document.nodes) +// +// yaml_free(document.version_directive) +// for (tag_directive = document.tag_directives.start +// tag_directive != document.tag_directives.end +// tag_directive++) { +// yaml_free(tag_directive.handle) +// yaml_free(tag_directive.prefix) +// } +// yaml_free(document.tag_directives.start) +// +// memset(document, 0, sizeof(yaml_document_t)) +//} +// +///** +// * Get a document node. +// */ +// +//YAML_DECLARE(yaml_node_t *) +//yaml_document_get_node(document *yaml_document_t, index int) +//{ +// assert(document) // Non-NULL document object is expected. +// +// if (index > 0 && document.nodes.start + index <= document.nodes.top) { +// return document.nodes.start + index - 1 +// } +// return NULL +//} +// +///** +// * Get the root object. +// */ +// +//YAML_DECLARE(yaml_node_t *) +//yaml_document_get_root_node(document *yaml_document_t) +//{ +// assert(document) // Non-NULL document object is expected. +// +// if (document.nodes.top != document.nodes.start) { +// return document.nodes.start +// } +// return NULL +//} +// +///* +// * Add a scalar node to a document. +// */ +// +//YAML_DECLARE(int) +//yaml_document_add_scalar(document *yaml_document_t, +// tag *yaml_char_t, value *yaml_char_t, length int, +// style yaml_scalar_style_t) +//{ +// struct { +// error yaml_error_type_t +// } context +// mark yaml_mark_t = { 0, 0, 0 } +// tag_copy *yaml_char_t = NULL +// value_copy *yaml_char_t = NULL +// node yaml_node_t +// +// assert(document) // Non-NULL document object is expected. +// assert(value) // Non-NULL value is expected. +// +// if (!tag) { +// tag = (yaml_char_t *)YAML_DEFAULT_SCALAR_TAG +// } +// +// if (!yaml_check_utf8(tag, strlen((char *)tag))) goto error +// tag_copy = yaml_strdup(tag) +// if (!tag_copy) goto error +// +// if (length < 0) { +// length = strlen((char *)value) +// } +// +// if (!yaml_check_utf8(value, length)) goto error +// value_copy = yaml_malloc(length+1) +// if (!value_copy) goto error +// memcpy(value_copy, value, length) +// value_copy[length] = '\0' +// +// SCALAR_NODE_INIT(node, tag_copy, value_copy, length, style, mark, mark) +// if (!PUSH(&context, document.nodes, node)) goto error +// +// return document.nodes.top - document.nodes.start +// +//error: +// yaml_free(tag_copy) +// yaml_free(value_copy) +// +// return 0 +//} +// +///* +// * Add a sequence node to a document. +// */ +// +//YAML_DECLARE(int) +//yaml_document_add_sequence(document *yaml_document_t, +// tag *yaml_char_t, style yaml_sequence_style_t) +//{ +// struct { +// error yaml_error_type_t +// } context +// mark yaml_mark_t = { 0, 0, 0 } +// tag_copy *yaml_char_t = NULL +// struct { +// start *yaml_node_item_t +// end *yaml_node_item_t +// top *yaml_node_item_t +// } items = { NULL, NULL, NULL } +// node yaml_node_t +// +// assert(document) // Non-NULL document object is expected. +// +// if (!tag) { +// tag = (yaml_char_t *)YAML_DEFAULT_SEQUENCE_TAG +// } +// +// if (!yaml_check_utf8(tag, strlen((char *)tag))) goto error +// tag_copy = yaml_strdup(tag) +// if (!tag_copy) goto error +// +// if (!STACK_INIT(&context, items, INITIAL_STACK_SIZE)) goto error +// +// SEQUENCE_NODE_INIT(node, tag_copy, items.start, items.end, +// style, mark, mark) +// if (!PUSH(&context, document.nodes, node)) goto error +// +// return document.nodes.top - document.nodes.start +// +//error: +// STACK_DEL(&context, items) +// yaml_free(tag_copy) +// +// return 0 +//} +// +///* +// * Add a mapping node to a document. +// */ +// +//YAML_DECLARE(int) +//yaml_document_add_mapping(document *yaml_document_t, +// tag *yaml_char_t, style yaml_mapping_style_t) +//{ +// struct { +// error yaml_error_type_t +// } context +// mark yaml_mark_t = { 0, 0, 0 } +// tag_copy *yaml_char_t = NULL +// struct { +// start *yaml_node_pair_t +// end *yaml_node_pair_t +// top *yaml_node_pair_t +// } pairs = { NULL, NULL, NULL } +// node yaml_node_t +// +// assert(document) // Non-NULL document object is expected. +// +// if (!tag) { +// tag = (yaml_char_t *)YAML_DEFAULT_MAPPING_TAG +// } +// +// if (!yaml_check_utf8(tag, strlen((char *)tag))) goto error +// tag_copy = yaml_strdup(tag) +// if (!tag_copy) goto error +// +// if (!STACK_INIT(&context, pairs, INITIAL_STACK_SIZE)) goto error +// +// MAPPING_NODE_INIT(node, tag_copy, pairs.start, pairs.end, +// style, mark, mark) +// if (!PUSH(&context, document.nodes, node)) goto error +// +// return document.nodes.top - document.nodes.start +// +//error: +// STACK_DEL(&context, pairs) +// yaml_free(tag_copy) +// +// return 0 +//} +// +///* +// * Append an item to a sequence node. +// */ +// +//YAML_DECLARE(int) +//yaml_document_append_sequence_item(document *yaml_document_t, +// sequence int, item int) +//{ +// struct { +// error yaml_error_type_t +// } context +// +// assert(document) // Non-NULL document is required. +// assert(sequence > 0 +// && document.nodes.start + sequence <= document.nodes.top) +// // Valid sequence id is required. +// assert(document.nodes.start[sequence-1].type == YAML_SEQUENCE_NODE) +// // A sequence node is required. +// assert(item > 0 && document.nodes.start + item <= document.nodes.top) +// // Valid item id is required. +// +// if (!PUSH(&context, +// document.nodes.start[sequence-1].data.sequence.items, item)) +// return 0 +// +// return 1 +//} +// +///* +// * Append a pair of a key and a value to a mapping node. +// */ +// +//YAML_DECLARE(int) +//yaml_document_append_mapping_pair(document *yaml_document_t, +// mapping int, key int, value int) +//{ +// struct { +// error yaml_error_type_t +// } context +// +// pair yaml_node_pair_t +// +// assert(document) // Non-NULL document is required. +// assert(mapping > 0 +// && document.nodes.start + mapping <= document.nodes.top) +// // Valid mapping id is required. +// assert(document.nodes.start[mapping-1].type == YAML_MAPPING_NODE) +// // A mapping node is required. +// assert(key > 0 && document.nodes.start + key <= document.nodes.top) +// // Valid key id is required. +// assert(value > 0 && document.nodes.start + value <= document.nodes.top) +// // Valid value id is required. +// +// pair.key = key +// pair.value = value +// +// if (!PUSH(&context, +// document.nodes.start[mapping-1].data.mapping.pairs, pair)) +// return 0 +// +// return 1 +//} +// +// diff --git a/asyncapi/vendor/gopkg.in/yaml.v3/decode.go b/asyncapi/vendor/gopkg.in/yaml.v3/decode.go new file mode 100644 index 0000000..0173b69 --- /dev/null +++ b/asyncapi/vendor/gopkg.in/yaml.v3/decode.go @@ -0,0 +1,1000 @@ +// +// Copyright (c) 2011-2019 Canonical Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package yaml + +import ( + "encoding" + "encoding/base64" + "fmt" + "io" + "math" + "reflect" + "strconv" + "time" +) + +// ---------------------------------------------------------------------------- +// Parser, produces a node tree out of a libyaml event stream. + +type parser struct { + parser yaml_parser_t + event yaml_event_t + doc *Node + anchors map[string]*Node + doneInit bool + textless bool +} + +func newParser(b []byte) *parser { + p := parser{} + if !yaml_parser_initialize(&p.parser) { + panic("failed to initialize YAML emitter") + } + if len(b) == 0 { + b = []byte{'\n'} + } + yaml_parser_set_input_string(&p.parser, b) + return &p +} + +func newParserFromReader(r io.Reader) *parser { + p := parser{} + if !yaml_parser_initialize(&p.parser) { + panic("failed to initialize YAML emitter") + } + yaml_parser_set_input_reader(&p.parser, r) + return &p +} + +func (p *parser) init() { + if p.doneInit { + return + } + p.anchors = make(map[string]*Node) + p.expect(yaml_STREAM_START_EVENT) + p.doneInit = true +} + +func (p *parser) destroy() { + if p.event.typ != yaml_NO_EVENT { + yaml_event_delete(&p.event) + } + yaml_parser_delete(&p.parser) +} + +// expect consumes an event from the event stream and +// checks that it's of the expected type. +func (p *parser) expect(e yaml_event_type_t) { + if p.event.typ == yaml_NO_EVENT { + if !yaml_parser_parse(&p.parser, &p.event) { + p.fail() + } + } + if p.event.typ == yaml_STREAM_END_EVENT { + failf("attempted to go past the end of stream; corrupted value?") + } + if p.event.typ != e { + p.parser.problem = fmt.Sprintf("expected %s event but got %s", e, p.event.typ) + p.fail() + } + yaml_event_delete(&p.event) + p.event.typ = yaml_NO_EVENT +} + +// peek peeks at the next event in the event stream, +// puts the results into p.event and returns the event type. +func (p *parser) peek() yaml_event_type_t { + if p.event.typ != yaml_NO_EVENT { + return p.event.typ + } + // It's curious choice from the underlying API to generally return a + // positive result on success, but on this case return true in an error + // scenario. This was the source of bugs in the past (issue #666). + if !yaml_parser_parse(&p.parser, &p.event) || p.parser.error != yaml_NO_ERROR { + p.fail() + } + return p.event.typ +} + +func (p *parser) fail() { + var where string + var line int + if p.parser.context_mark.line != 0 { + line = p.parser.context_mark.line + // Scanner errors don't iterate line before returning error + if p.parser.error == yaml_SCANNER_ERROR { + line++ + } + } else if p.parser.problem_mark.line != 0 { + line = p.parser.problem_mark.line + // Scanner errors don't iterate line before returning error + if p.parser.error == yaml_SCANNER_ERROR { + line++ + } + } + if line != 0 { + where = "line " + strconv.Itoa(line) + ": " + } + var msg string + if len(p.parser.problem) > 0 { + msg = p.parser.problem + } else { + msg = "unknown problem parsing YAML content" + } + failf("%s%s", where, msg) +} + +func (p *parser) anchor(n *Node, anchor []byte) { + if anchor != nil { + n.Anchor = string(anchor) + p.anchors[n.Anchor] = n + } +} + +func (p *parser) parse() *Node { + p.init() + switch p.peek() { + case yaml_SCALAR_EVENT: + return p.scalar() + case yaml_ALIAS_EVENT: + return p.alias() + case yaml_MAPPING_START_EVENT: + return p.mapping() + case yaml_SEQUENCE_START_EVENT: + return p.sequence() + case yaml_DOCUMENT_START_EVENT: + return p.document() + case yaml_STREAM_END_EVENT: + // Happens when attempting to decode an empty buffer. + return nil + case yaml_TAIL_COMMENT_EVENT: + panic("internal error: unexpected tail comment event (please report)") + default: + panic("internal error: attempted to parse unknown event (please report): " + p.event.typ.String()) + } +} + +func (p *parser) node(kind Kind, defaultTag, tag, value string) *Node { + var style Style + if tag != "" && tag != "!" { + tag = shortTag(tag) + style = TaggedStyle + } else if defaultTag != "" { + tag = defaultTag + } else if kind == ScalarNode { + tag, _ = resolve("", value) + } + n := &Node{ + Kind: kind, + Tag: tag, + Value: value, + Style: style, + } + if !p.textless { + n.Line = p.event.start_mark.line + 1 + n.Column = p.event.start_mark.column + 1 + n.HeadComment = string(p.event.head_comment) + n.LineComment = string(p.event.line_comment) + n.FootComment = string(p.event.foot_comment) + } + return n +} + +func (p *parser) parseChild(parent *Node) *Node { + child := p.parse() + parent.Content = append(parent.Content, child) + return child +} + +func (p *parser) document() *Node { + n := p.node(DocumentNode, "", "", "") + p.doc = n + p.expect(yaml_DOCUMENT_START_EVENT) + p.parseChild(n) + if p.peek() == yaml_DOCUMENT_END_EVENT { + n.FootComment = string(p.event.foot_comment) + } + p.expect(yaml_DOCUMENT_END_EVENT) + return n +} + +func (p *parser) alias() *Node { + n := p.node(AliasNode, "", "", string(p.event.anchor)) + n.Alias = p.anchors[n.Value] + if n.Alias == nil { + failf("unknown anchor '%s' referenced", n.Value) + } + p.expect(yaml_ALIAS_EVENT) + return n +} + +func (p *parser) scalar() *Node { + var parsedStyle = p.event.scalar_style() + var nodeStyle Style + switch { + case parsedStyle&yaml_DOUBLE_QUOTED_SCALAR_STYLE != 0: + nodeStyle = DoubleQuotedStyle + case parsedStyle&yaml_SINGLE_QUOTED_SCALAR_STYLE != 0: + nodeStyle = SingleQuotedStyle + case parsedStyle&yaml_LITERAL_SCALAR_STYLE != 0: + nodeStyle = LiteralStyle + case parsedStyle&yaml_FOLDED_SCALAR_STYLE != 0: + nodeStyle = FoldedStyle + } + var nodeValue = string(p.event.value) + var nodeTag = string(p.event.tag) + var defaultTag string + if nodeStyle == 0 { + if nodeValue == "<<" { + defaultTag = mergeTag + } + } else { + defaultTag = strTag + } + n := p.node(ScalarNode, defaultTag, nodeTag, nodeValue) + n.Style |= nodeStyle + p.anchor(n, p.event.anchor) + p.expect(yaml_SCALAR_EVENT) + return n +} + +func (p *parser) sequence() *Node { + n := p.node(SequenceNode, seqTag, string(p.event.tag), "") + if p.event.sequence_style()&yaml_FLOW_SEQUENCE_STYLE != 0 { + n.Style |= FlowStyle + } + p.anchor(n, p.event.anchor) + p.expect(yaml_SEQUENCE_START_EVENT) + for p.peek() != yaml_SEQUENCE_END_EVENT { + p.parseChild(n) + } + n.LineComment = string(p.event.line_comment) + n.FootComment = string(p.event.foot_comment) + p.expect(yaml_SEQUENCE_END_EVENT) + return n +} + +func (p *parser) mapping() *Node { + n := p.node(MappingNode, mapTag, string(p.event.tag), "") + block := true + if p.event.mapping_style()&yaml_FLOW_MAPPING_STYLE != 0 { + block = false + n.Style |= FlowStyle + } + p.anchor(n, p.event.anchor) + p.expect(yaml_MAPPING_START_EVENT) + for p.peek() != yaml_MAPPING_END_EVENT { + k := p.parseChild(n) + if block && k.FootComment != "" { + // Must be a foot comment for the prior value when being dedented. + if len(n.Content) > 2 { + n.Content[len(n.Content)-3].FootComment = k.FootComment + k.FootComment = "" + } + } + v := p.parseChild(n) + if k.FootComment == "" && v.FootComment != "" { + k.FootComment = v.FootComment + v.FootComment = "" + } + if p.peek() == yaml_TAIL_COMMENT_EVENT { + if k.FootComment == "" { + k.FootComment = string(p.event.foot_comment) + } + p.expect(yaml_TAIL_COMMENT_EVENT) + } + } + n.LineComment = string(p.event.line_comment) + n.FootComment = string(p.event.foot_comment) + if n.Style&FlowStyle == 0 && n.FootComment != "" && len(n.Content) > 1 { + n.Content[len(n.Content)-2].FootComment = n.FootComment + n.FootComment = "" + } + p.expect(yaml_MAPPING_END_EVENT) + return n +} + +// ---------------------------------------------------------------------------- +// Decoder, unmarshals a node into a provided value. + +type decoder struct { + doc *Node + aliases map[*Node]bool + terrors []string + + stringMapType reflect.Type + generalMapType reflect.Type + + knownFields bool + uniqueKeys bool + decodeCount int + aliasCount int + aliasDepth int + + mergedFields map[interface{}]bool +} + +var ( + nodeType = reflect.TypeOf(Node{}) + durationType = reflect.TypeOf(time.Duration(0)) + stringMapType = reflect.TypeOf(map[string]interface{}{}) + generalMapType = reflect.TypeOf(map[interface{}]interface{}{}) + ifaceType = generalMapType.Elem() + timeType = reflect.TypeOf(time.Time{}) + ptrTimeType = reflect.TypeOf(&time.Time{}) +) + +func newDecoder() *decoder { + d := &decoder{ + stringMapType: stringMapType, + generalMapType: generalMapType, + uniqueKeys: true, + } + d.aliases = make(map[*Node]bool) + return d +} + +func (d *decoder) terror(n *Node, tag string, out reflect.Value) { + if n.Tag != "" { + tag = n.Tag + } + value := n.Value + if tag != seqTag && tag != mapTag { + if len(value) > 10 { + value = " `" + value[:7] + "...`" + } else { + value = " `" + value + "`" + } + } + d.terrors = append(d.terrors, fmt.Sprintf("line %d: cannot unmarshal %s%s into %s", n.Line, shortTag(tag), value, out.Type())) +} + +func (d *decoder) callUnmarshaler(n *Node, u Unmarshaler) (good bool) { + err := u.UnmarshalYAML(n) + if e, ok := err.(*TypeError); ok { + d.terrors = append(d.terrors, e.Errors...) + return false + } + if err != nil { + fail(err) + } + return true +} + +func (d *decoder) callObsoleteUnmarshaler(n *Node, u obsoleteUnmarshaler) (good bool) { + terrlen := len(d.terrors) + err := u.UnmarshalYAML(func(v interface{}) (err error) { + defer handleErr(&err) + d.unmarshal(n, reflect.ValueOf(v)) + if len(d.terrors) > terrlen { + issues := d.terrors[terrlen:] + d.terrors = d.terrors[:terrlen] + return &TypeError{issues} + } + return nil + }) + if e, ok := err.(*TypeError); ok { + d.terrors = append(d.terrors, e.Errors...) + return false + } + if err != nil { + fail(err) + } + return true +} + +// d.prepare initializes and dereferences pointers and calls UnmarshalYAML +// if a value is found to implement it. +// It returns the initialized and dereferenced out value, whether +// unmarshalling was already done by UnmarshalYAML, and if so whether +// its types unmarshalled appropriately. +// +// If n holds a null value, prepare returns before doing anything. +func (d *decoder) prepare(n *Node, out reflect.Value) (newout reflect.Value, unmarshaled, good bool) { + if n.ShortTag() == nullTag { + return out, false, false + } + again := true + for again { + again = false + if out.Kind() == reflect.Ptr { + if out.IsNil() { + out.Set(reflect.New(out.Type().Elem())) + } + out = out.Elem() + again = true + } + if out.CanAddr() { + outi := out.Addr().Interface() + if u, ok := outi.(Unmarshaler); ok { + good = d.callUnmarshaler(n, u) + return out, true, good + } + if u, ok := outi.(obsoleteUnmarshaler); ok { + good = d.callObsoleteUnmarshaler(n, u) + return out, true, good + } + } + } + return out, false, false +} + +func (d *decoder) fieldByIndex(n *Node, v reflect.Value, index []int) (field reflect.Value) { + if n.ShortTag() == nullTag { + return reflect.Value{} + } + for _, num := range index { + for { + if v.Kind() == reflect.Ptr { + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + v = v.Elem() + continue + } + break + } + v = v.Field(num) + } + return v +} + +const ( + // 400,000 decode operations is ~500kb of dense object declarations, or + // ~5kb of dense object declarations with 10000% alias expansion + alias_ratio_range_low = 400000 + + // 4,000,000 decode operations is ~5MB of dense object declarations, or + // ~4.5MB of dense object declarations with 10% alias expansion + alias_ratio_range_high = 4000000 + + // alias_ratio_range is the range over which we scale allowed alias ratios + alias_ratio_range = float64(alias_ratio_range_high - alias_ratio_range_low) +) + +func allowedAliasRatio(decodeCount int) float64 { + switch { + case decodeCount <= alias_ratio_range_low: + // allow 99% to come from alias expansion for small-to-medium documents + return 0.99 + case decodeCount >= alias_ratio_range_high: + // allow 10% to come from alias expansion for very large documents + return 0.10 + default: + // scale smoothly from 99% down to 10% over the range. + // this maps to 396,000 - 400,000 allowed alias-driven decodes over the range. + // 400,000 decode operations is ~100MB of allocations in worst-case scenarios (single-item maps). + return 0.99 - 0.89*(float64(decodeCount-alias_ratio_range_low)/alias_ratio_range) + } +} + +func (d *decoder) unmarshal(n *Node, out reflect.Value) (good bool) { + d.decodeCount++ + if d.aliasDepth > 0 { + d.aliasCount++ + } + if d.aliasCount > 100 && d.decodeCount > 1000 && float64(d.aliasCount)/float64(d.decodeCount) > allowedAliasRatio(d.decodeCount) { + failf("document contains excessive aliasing") + } + if out.Type() == nodeType { + out.Set(reflect.ValueOf(n).Elem()) + return true + } + switch n.Kind { + case DocumentNode: + return d.document(n, out) + case AliasNode: + return d.alias(n, out) + } + out, unmarshaled, good := d.prepare(n, out) + if unmarshaled { + return good + } + switch n.Kind { + case ScalarNode: + good = d.scalar(n, out) + case MappingNode: + good = d.mapping(n, out) + case SequenceNode: + good = d.sequence(n, out) + case 0: + if n.IsZero() { + return d.null(out) + } + fallthrough + default: + failf("cannot decode node with unknown kind %d", n.Kind) + } + return good +} + +func (d *decoder) document(n *Node, out reflect.Value) (good bool) { + if len(n.Content) == 1 { + d.doc = n + d.unmarshal(n.Content[0], out) + return true + } + return false +} + +func (d *decoder) alias(n *Node, out reflect.Value) (good bool) { + if d.aliases[n] { + // TODO this could actually be allowed in some circumstances. + failf("anchor '%s' value contains itself", n.Value) + } + d.aliases[n] = true + d.aliasDepth++ + good = d.unmarshal(n.Alias, out) + d.aliasDepth-- + delete(d.aliases, n) + return good +} + +var zeroValue reflect.Value + +func resetMap(out reflect.Value) { + for _, k := range out.MapKeys() { + out.SetMapIndex(k, zeroValue) + } +} + +func (d *decoder) null(out reflect.Value) bool { + if out.CanAddr() { + switch out.Kind() { + case reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice: + out.Set(reflect.Zero(out.Type())) + return true + } + } + return false +} + +func (d *decoder) scalar(n *Node, out reflect.Value) bool { + var tag string + var resolved interface{} + if n.indicatedString() { + tag = strTag + resolved = n.Value + } else { + tag, resolved = resolve(n.Tag, n.Value) + if tag == binaryTag { + data, err := base64.StdEncoding.DecodeString(resolved.(string)) + if err != nil { + failf("!!binary value contains invalid base64 data") + } + resolved = string(data) + } + } + if resolved == nil { + return d.null(out) + } + if resolvedv := reflect.ValueOf(resolved); out.Type() == resolvedv.Type() { + // We've resolved to exactly the type we want, so use that. + out.Set(resolvedv) + return true + } + // Perhaps we can use the value as a TextUnmarshaler to + // set its value. + if out.CanAddr() { + u, ok := out.Addr().Interface().(encoding.TextUnmarshaler) + if ok { + var text []byte + if tag == binaryTag { + text = []byte(resolved.(string)) + } else { + // We let any value be unmarshaled into TextUnmarshaler. + // That might be more lax than we'd like, but the + // TextUnmarshaler itself should bowl out any dubious values. + text = []byte(n.Value) + } + err := u.UnmarshalText(text) + if err != nil { + fail(err) + } + return true + } + } + switch out.Kind() { + case reflect.String: + if tag == binaryTag { + out.SetString(resolved.(string)) + return true + } + out.SetString(n.Value) + return true + case reflect.Interface: + out.Set(reflect.ValueOf(resolved)) + return true + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + // This used to work in v2, but it's very unfriendly. + isDuration := out.Type() == durationType + + switch resolved := resolved.(type) { + case int: + if !isDuration && !out.OverflowInt(int64(resolved)) { + out.SetInt(int64(resolved)) + return true + } + case int64: + if !isDuration && !out.OverflowInt(resolved) { + out.SetInt(resolved) + return true + } + case uint64: + if !isDuration && resolved <= math.MaxInt64 && !out.OverflowInt(int64(resolved)) { + out.SetInt(int64(resolved)) + return true + } + case float64: + if !isDuration && resolved <= math.MaxInt64 && !out.OverflowInt(int64(resolved)) { + out.SetInt(int64(resolved)) + return true + } + case string: + if out.Type() == durationType { + d, err := time.ParseDuration(resolved) + if err == nil { + out.SetInt(int64(d)) + return true + } + } + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + switch resolved := resolved.(type) { + case int: + if resolved >= 0 && !out.OverflowUint(uint64(resolved)) { + out.SetUint(uint64(resolved)) + return true + } + case int64: + if resolved >= 0 && !out.OverflowUint(uint64(resolved)) { + out.SetUint(uint64(resolved)) + return true + } + case uint64: + if !out.OverflowUint(uint64(resolved)) { + out.SetUint(uint64(resolved)) + return true + } + case float64: + if resolved <= math.MaxUint64 && !out.OverflowUint(uint64(resolved)) { + out.SetUint(uint64(resolved)) + return true + } + } + case reflect.Bool: + switch resolved := resolved.(type) { + case bool: + out.SetBool(resolved) + return true + case string: + // This offers some compatibility with the 1.1 spec (https://yaml.org/type/bool.html). + // It only works if explicitly attempting to unmarshal into a typed bool value. + switch resolved { + case "y", "Y", "yes", "Yes", "YES", "on", "On", "ON": + out.SetBool(true) + return true + case "n", "N", "no", "No", "NO", "off", "Off", "OFF": + out.SetBool(false) + return true + } + } + case reflect.Float32, reflect.Float64: + switch resolved := resolved.(type) { + case int: + out.SetFloat(float64(resolved)) + return true + case int64: + out.SetFloat(float64(resolved)) + return true + case uint64: + out.SetFloat(float64(resolved)) + return true + case float64: + out.SetFloat(resolved) + return true + } + case reflect.Struct: + if resolvedv := reflect.ValueOf(resolved); out.Type() == resolvedv.Type() { + out.Set(resolvedv) + return true + } + case reflect.Ptr: + panic("yaml internal error: please report the issue") + } + d.terror(n, tag, out) + return false +} + +func settableValueOf(i interface{}) reflect.Value { + v := reflect.ValueOf(i) + sv := reflect.New(v.Type()).Elem() + sv.Set(v) + return sv +} + +func (d *decoder) sequence(n *Node, out reflect.Value) (good bool) { + l := len(n.Content) + + var iface reflect.Value + switch out.Kind() { + case reflect.Slice: + out.Set(reflect.MakeSlice(out.Type(), l, l)) + case reflect.Array: + if l != out.Len() { + failf("invalid array: want %d elements but got %d", out.Len(), l) + } + case reflect.Interface: + // No type hints. Will have to use a generic sequence. + iface = out + out = settableValueOf(make([]interface{}, l)) + default: + d.terror(n, seqTag, out) + return false + } + et := out.Type().Elem() + + j := 0 + for i := 0; i < l; i++ { + e := reflect.New(et).Elem() + if ok := d.unmarshal(n.Content[i], e); ok { + out.Index(j).Set(e) + j++ + } + } + if out.Kind() != reflect.Array { + out.Set(out.Slice(0, j)) + } + if iface.IsValid() { + iface.Set(out) + } + return true +} + +func (d *decoder) mapping(n *Node, out reflect.Value) (good bool) { + l := len(n.Content) + if d.uniqueKeys { + nerrs := len(d.terrors) + for i := 0; i < l; i += 2 { + ni := n.Content[i] + for j := i + 2; j < l; j += 2 { + nj := n.Content[j] + if ni.Kind == nj.Kind && ni.Value == nj.Value { + d.terrors = append(d.terrors, fmt.Sprintf("line %d: mapping key %#v already defined at line %d", nj.Line, nj.Value, ni.Line)) + } + } + } + if len(d.terrors) > nerrs { + return false + } + } + switch out.Kind() { + case reflect.Struct: + return d.mappingStruct(n, out) + case reflect.Map: + // okay + case reflect.Interface: + iface := out + if isStringMap(n) { + out = reflect.MakeMap(d.stringMapType) + } else { + out = reflect.MakeMap(d.generalMapType) + } + iface.Set(out) + default: + d.terror(n, mapTag, out) + return false + } + + outt := out.Type() + kt := outt.Key() + et := outt.Elem() + + stringMapType := d.stringMapType + generalMapType := d.generalMapType + if outt.Elem() == ifaceType { + if outt.Key().Kind() == reflect.String { + d.stringMapType = outt + } else if outt.Key() == ifaceType { + d.generalMapType = outt + } + } + + mergedFields := d.mergedFields + d.mergedFields = nil + + var mergeNode *Node + + mapIsNew := false + if out.IsNil() { + out.Set(reflect.MakeMap(outt)) + mapIsNew = true + } + for i := 0; i < l; i += 2 { + if isMerge(n.Content[i]) { + mergeNode = n.Content[i+1] + continue + } + k := reflect.New(kt).Elem() + if d.unmarshal(n.Content[i], k) { + if mergedFields != nil { + ki := k.Interface() + if mergedFields[ki] { + continue + } + mergedFields[ki] = true + } + kkind := k.Kind() + if kkind == reflect.Interface { + kkind = k.Elem().Kind() + } + if kkind == reflect.Map || kkind == reflect.Slice { + failf("invalid map key: %#v", k.Interface()) + } + e := reflect.New(et).Elem() + if d.unmarshal(n.Content[i+1], e) || n.Content[i+1].ShortTag() == nullTag && (mapIsNew || !out.MapIndex(k).IsValid()) { + out.SetMapIndex(k, e) + } + } + } + + d.mergedFields = mergedFields + if mergeNode != nil { + d.merge(n, mergeNode, out) + } + + d.stringMapType = stringMapType + d.generalMapType = generalMapType + return true +} + +func isStringMap(n *Node) bool { + if n.Kind != MappingNode { + return false + } + l := len(n.Content) + for i := 0; i < l; i += 2 { + shortTag := n.Content[i].ShortTag() + if shortTag != strTag && shortTag != mergeTag { + return false + } + } + return true +} + +func (d *decoder) mappingStruct(n *Node, out reflect.Value) (good bool) { + sinfo, err := getStructInfo(out.Type()) + if err != nil { + panic(err) + } + + var inlineMap reflect.Value + var elemType reflect.Type + if sinfo.InlineMap != -1 { + inlineMap = out.Field(sinfo.InlineMap) + elemType = inlineMap.Type().Elem() + } + + for _, index := range sinfo.InlineUnmarshalers { + field := d.fieldByIndex(n, out, index) + d.prepare(n, field) + } + + mergedFields := d.mergedFields + d.mergedFields = nil + var mergeNode *Node + var doneFields []bool + if d.uniqueKeys { + doneFields = make([]bool, len(sinfo.FieldsList)) + } + name := settableValueOf("") + l := len(n.Content) + for i := 0; i < l; i += 2 { + ni := n.Content[i] + if isMerge(ni) { + mergeNode = n.Content[i+1] + continue + } + if !d.unmarshal(ni, name) { + continue + } + sname := name.String() + if mergedFields != nil { + if mergedFields[sname] { + continue + } + mergedFields[sname] = true + } + if info, ok := sinfo.FieldsMap[sname]; ok { + if d.uniqueKeys { + if doneFields[info.Id] { + d.terrors = append(d.terrors, fmt.Sprintf("line %d: field %s already set in type %s", ni.Line, name.String(), out.Type())) + continue + } + doneFields[info.Id] = true + } + var field reflect.Value + if info.Inline == nil { + field = out.Field(info.Num) + } else { + field = d.fieldByIndex(n, out, info.Inline) + } + d.unmarshal(n.Content[i+1], field) + } else if sinfo.InlineMap != -1 { + if inlineMap.IsNil() { + inlineMap.Set(reflect.MakeMap(inlineMap.Type())) + } + value := reflect.New(elemType).Elem() + d.unmarshal(n.Content[i+1], value) + inlineMap.SetMapIndex(name, value) + } else if d.knownFields { + d.terrors = append(d.terrors, fmt.Sprintf("line %d: field %s not found in type %s", ni.Line, name.String(), out.Type())) + } + } + + d.mergedFields = mergedFields + if mergeNode != nil { + d.merge(n, mergeNode, out) + } + return true +} + +func failWantMap() { + failf("map merge requires map or sequence of maps as the value") +} + +func (d *decoder) merge(parent *Node, merge *Node, out reflect.Value) { + mergedFields := d.mergedFields + if mergedFields == nil { + d.mergedFields = make(map[interface{}]bool) + for i := 0; i < len(parent.Content); i += 2 { + k := reflect.New(ifaceType).Elem() + if d.unmarshal(parent.Content[i], k) { + d.mergedFields[k.Interface()] = true + } + } + } + + switch merge.Kind { + case MappingNode: + d.unmarshal(merge, out) + case AliasNode: + if merge.Alias != nil && merge.Alias.Kind != MappingNode { + failWantMap() + } + d.unmarshal(merge, out) + case SequenceNode: + for i := 0; i < len(merge.Content); i++ { + ni := merge.Content[i] + if ni.Kind == AliasNode { + if ni.Alias != nil && ni.Alias.Kind != MappingNode { + failWantMap() + } + } else if ni.Kind != MappingNode { + failWantMap() + } + d.unmarshal(ni, out) + } + default: + failWantMap() + } + + d.mergedFields = mergedFields +} + +func isMerge(n *Node) bool { + return n.Kind == ScalarNode && n.Value == "<<" && (n.Tag == "" || n.Tag == "!" || shortTag(n.Tag) == mergeTag) +} diff --git a/asyncapi/vendor/gopkg.in/yaml.v3/emitterc.go b/asyncapi/vendor/gopkg.in/yaml.v3/emitterc.go new file mode 100644 index 0000000..0f47c9c --- /dev/null +++ b/asyncapi/vendor/gopkg.in/yaml.v3/emitterc.go @@ -0,0 +1,2020 @@ +// +// Copyright (c) 2011-2019 Canonical Ltd +// Copyright (c) 2006-2010 Kirill Simonov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package yaml + +import ( + "bytes" + "fmt" +) + +// Flush the buffer if needed. +func flush(emitter *yaml_emitter_t) bool { + if emitter.buffer_pos+5 >= len(emitter.buffer) { + return yaml_emitter_flush(emitter) + } + return true +} + +// Put a character to the output buffer. +func put(emitter *yaml_emitter_t, value byte) bool { + if emitter.buffer_pos+5 >= len(emitter.buffer) && !yaml_emitter_flush(emitter) { + return false + } + emitter.buffer[emitter.buffer_pos] = value + emitter.buffer_pos++ + emitter.column++ + return true +} + +// Put a line break to the output buffer. +func put_break(emitter *yaml_emitter_t) bool { + if emitter.buffer_pos+5 >= len(emitter.buffer) && !yaml_emitter_flush(emitter) { + return false + } + switch emitter.line_break { + case yaml_CR_BREAK: + emitter.buffer[emitter.buffer_pos] = '\r' + emitter.buffer_pos += 1 + case yaml_LN_BREAK: + emitter.buffer[emitter.buffer_pos] = '\n' + emitter.buffer_pos += 1 + case yaml_CRLN_BREAK: + emitter.buffer[emitter.buffer_pos+0] = '\r' + emitter.buffer[emitter.buffer_pos+1] = '\n' + emitter.buffer_pos += 2 + default: + panic("unknown line break setting") + } + if emitter.column == 0 { + emitter.space_above = true + } + emitter.column = 0 + emitter.line++ + // [Go] Do this here and below and drop from everywhere else (see commented lines). + emitter.indention = true + return true +} + +// Copy a character from a string into buffer. +func write(emitter *yaml_emitter_t, s []byte, i *int) bool { + if emitter.buffer_pos+5 >= len(emitter.buffer) && !yaml_emitter_flush(emitter) { + return false + } + p := emitter.buffer_pos + w := width(s[*i]) + switch w { + case 4: + emitter.buffer[p+3] = s[*i+3] + fallthrough + case 3: + emitter.buffer[p+2] = s[*i+2] + fallthrough + case 2: + emitter.buffer[p+1] = s[*i+1] + fallthrough + case 1: + emitter.buffer[p+0] = s[*i+0] + default: + panic("unknown character width") + } + emitter.column++ + emitter.buffer_pos += w + *i += w + return true +} + +// Write a whole string into buffer. +func write_all(emitter *yaml_emitter_t, s []byte) bool { + for i := 0; i < len(s); { + if !write(emitter, s, &i) { + return false + } + } + return true +} + +// Copy a line break character from a string into buffer. +func write_break(emitter *yaml_emitter_t, s []byte, i *int) bool { + if s[*i] == '\n' { + if !put_break(emitter) { + return false + } + *i++ + } else { + if !write(emitter, s, i) { + return false + } + if emitter.column == 0 { + emitter.space_above = true + } + emitter.column = 0 + emitter.line++ + // [Go] Do this here and above and drop from everywhere else (see commented lines). + emitter.indention = true + } + return true +} + +// Set an emitter error and return false. +func yaml_emitter_set_emitter_error(emitter *yaml_emitter_t, problem string) bool { + emitter.error = yaml_EMITTER_ERROR + emitter.problem = problem + return false +} + +// Emit an event. +func yaml_emitter_emit(emitter *yaml_emitter_t, event *yaml_event_t) bool { + emitter.events = append(emitter.events, *event) + for !yaml_emitter_need_more_events(emitter) { + event := &emitter.events[emitter.events_head] + if !yaml_emitter_analyze_event(emitter, event) { + return false + } + if !yaml_emitter_state_machine(emitter, event) { + return false + } + yaml_event_delete(event) + emitter.events_head++ + } + return true +} + +// Check if we need to accumulate more events before emitting. +// +// We accumulate extra +// - 1 event for DOCUMENT-START +// - 2 events for SEQUENCE-START +// - 3 events for MAPPING-START +// +func yaml_emitter_need_more_events(emitter *yaml_emitter_t) bool { + if emitter.events_head == len(emitter.events) { + return true + } + var accumulate int + switch emitter.events[emitter.events_head].typ { + case yaml_DOCUMENT_START_EVENT: + accumulate = 1 + break + case yaml_SEQUENCE_START_EVENT: + accumulate = 2 + break + case yaml_MAPPING_START_EVENT: + accumulate = 3 + break + default: + return false + } + if len(emitter.events)-emitter.events_head > accumulate { + return false + } + var level int + for i := emitter.events_head; i < len(emitter.events); i++ { + switch emitter.events[i].typ { + case yaml_STREAM_START_EVENT, yaml_DOCUMENT_START_EVENT, yaml_SEQUENCE_START_EVENT, yaml_MAPPING_START_EVENT: + level++ + case yaml_STREAM_END_EVENT, yaml_DOCUMENT_END_EVENT, yaml_SEQUENCE_END_EVENT, yaml_MAPPING_END_EVENT: + level-- + } + if level == 0 { + return false + } + } + return true +} + +// Append a directive to the directives stack. +func yaml_emitter_append_tag_directive(emitter *yaml_emitter_t, value *yaml_tag_directive_t, allow_duplicates bool) bool { + for i := 0; i < len(emitter.tag_directives); i++ { + if bytes.Equal(value.handle, emitter.tag_directives[i].handle) { + if allow_duplicates { + return true + } + return yaml_emitter_set_emitter_error(emitter, "duplicate %TAG directive") + } + } + + // [Go] Do we actually need to copy this given garbage collection + // and the lack of deallocating destructors? + tag_copy := yaml_tag_directive_t{ + handle: make([]byte, len(value.handle)), + prefix: make([]byte, len(value.prefix)), + } + copy(tag_copy.handle, value.handle) + copy(tag_copy.prefix, value.prefix) + emitter.tag_directives = append(emitter.tag_directives, tag_copy) + return true +} + +// Increase the indentation level. +func yaml_emitter_increase_indent(emitter *yaml_emitter_t, flow, indentless bool) bool { + emitter.indents = append(emitter.indents, emitter.indent) + if emitter.indent < 0 { + if flow { + emitter.indent = emitter.best_indent + } else { + emitter.indent = 0 + } + } else if !indentless { + // [Go] This was changed so that indentations are more regular. + if emitter.states[len(emitter.states)-1] == yaml_EMIT_BLOCK_SEQUENCE_ITEM_STATE { + // The first indent inside a sequence will just skip the "- " indicator. + emitter.indent += 2 + } else { + // Everything else aligns to the chosen indentation. + emitter.indent = emitter.best_indent*((emitter.indent+emitter.best_indent)/emitter.best_indent) + } + } + return true +} + +// State dispatcher. +func yaml_emitter_state_machine(emitter *yaml_emitter_t, event *yaml_event_t) bool { + switch emitter.state { + default: + case yaml_EMIT_STREAM_START_STATE: + return yaml_emitter_emit_stream_start(emitter, event) + + case yaml_EMIT_FIRST_DOCUMENT_START_STATE: + return yaml_emitter_emit_document_start(emitter, event, true) + + case yaml_EMIT_DOCUMENT_START_STATE: + return yaml_emitter_emit_document_start(emitter, event, false) + + case yaml_EMIT_DOCUMENT_CONTENT_STATE: + return yaml_emitter_emit_document_content(emitter, event) + + case yaml_EMIT_DOCUMENT_END_STATE: + return yaml_emitter_emit_document_end(emitter, event) + + case yaml_EMIT_FLOW_SEQUENCE_FIRST_ITEM_STATE: + return yaml_emitter_emit_flow_sequence_item(emitter, event, true, false) + + case yaml_EMIT_FLOW_SEQUENCE_TRAIL_ITEM_STATE: + return yaml_emitter_emit_flow_sequence_item(emitter, event, false, true) + + case yaml_EMIT_FLOW_SEQUENCE_ITEM_STATE: + return yaml_emitter_emit_flow_sequence_item(emitter, event, false, false) + + case yaml_EMIT_FLOW_MAPPING_FIRST_KEY_STATE: + return yaml_emitter_emit_flow_mapping_key(emitter, event, true, false) + + case yaml_EMIT_FLOW_MAPPING_TRAIL_KEY_STATE: + return yaml_emitter_emit_flow_mapping_key(emitter, event, false, true) + + case yaml_EMIT_FLOW_MAPPING_KEY_STATE: + return yaml_emitter_emit_flow_mapping_key(emitter, event, false, false) + + case yaml_EMIT_FLOW_MAPPING_SIMPLE_VALUE_STATE: + return yaml_emitter_emit_flow_mapping_value(emitter, event, true) + + case yaml_EMIT_FLOW_MAPPING_VALUE_STATE: + return yaml_emitter_emit_flow_mapping_value(emitter, event, false) + + case yaml_EMIT_BLOCK_SEQUENCE_FIRST_ITEM_STATE: + return yaml_emitter_emit_block_sequence_item(emitter, event, true) + + case yaml_EMIT_BLOCK_SEQUENCE_ITEM_STATE: + return yaml_emitter_emit_block_sequence_item(emitter, event, false) + + case yaml_EMIT_BLOCK_MAPPING_FIRST_KEY_STATE: + return yaml_emitter_emit_block_mapping_key(emitter, event, true) + + case yaml_EMIT_BLOCK_MAPPING_KEY_STATE: + return yaml_emitter_emit_block_mapping_key(emitter, event, false) + + case yaml_EMIT_BLOCK_MAPPING_SIMPLE_VALUE_STATE: + return yaml_emitter_emit_block_mapping_value(emitter, event, true) + + case yaml_EMIT_BLOCK_MAPPING_VALUE_STATE: + return yaml_emitter_emit_block_mapping_value(emitter, event, false) + + case yaml_EMIT_END_STATE: + return yaml_emitter_set_emitter_error(emitter, "expected nothing after STREAM-END") + } + panic("invalid emitter state") +} + +// Expect STREAM-START. +func yaml_emitter_emit_stream_start(emitter *yaml_emitter_t, event *yaml_event_t) bool { + if event.typ != yaml_STREAM_START_EVENT { + return yaml_emitter_set_emitter_error(emitter, "expected STREAM-START") + } + if emitter.encoding == yaml_ANY_ENCODING { + emitter.encoding = event.encoding + if emitter.encoding == yaml_ANY_ENCODING { + emitter.encoding = yaml_UTF8_ENCODING + } + } + if emitter.best_indent < 2 || emitter.best_indent > 9 { + emitter.best_indent = 2 + } + if emitter.best_width >= 0 && emitter.best_width <= emitter.best_indent*2 { + emitter.best_width = 80 + } + if emitter.best_width < 0 { + emitter.best_width = 1<<31 - 1 + } + if emitter.line_break == yaml_ANY_BREAK { + emitter.line_break = yaml_LN_BREAK + } + + emitter.indent = -1 + emitter.line = 0 + emitter.column = 0 + emitter.whitespace = true + emitter.indention = true + emitter.space_above = true + emitter.foot_indent = -1 + + if emitter.encoding != yaml_UTF8_ENCODING { + if !yaml_emitter_write_bom(emitter) { + return false + } + } + emitter.state = yaml_EMIT_FIRST_DOCUMENT_START_STATE + return true +} + +// Expect DOCUMENT-START or STREAM-END. +func yaml_emitter_emit_document_start(emitter *yaml_emitter_t, event *yaml_event_t, first bool) bool { + + if event.typ == yaml_DOCUMENT_START_EVENT { + + if event.version_directive != nil { + if !yaml_emitter_analyze_version_directive(emitter, event.version_directive) { + return false + } + } + + for i := 0; i < len(event.tag_directives); i++ { + tag_directive := &event.tag_directives[i] + if !yaml_emitter_analyze_tag_directive(emitter, tag_directive) { + return false + } + if !yaml_emitter_append_tag_directive(emitter, tag_directive, false) { + return false + } + } + + for i := 0; i < len(default_tag_directives); i++ { + tag_directive := &default_tag_directives[i] + if !yaml_emitter_append_tag_directive(emitter, tag_directive, true) { + return false + } + } + + implicit := event.implicit + if !first || emitter.canonical { + implicit = false + } + + if emitter.open_ended && (event.version_directive != nil || len(event.tag_directives) > 0) { + if !yaml_emitter_write_indicator(emitter, []byte("..."), true, false, false) { + return false + } + if !yaml_emitter_write_indent(emitter) { + return false + } + } + + if event.version_directive != nil { + implicit = false + if !yaml_emitter_write_indicator(emitter, []byte("%YAML"), true, false, false) { + return false + } + if !yaml_emitter_write_indicator(emitter, []byte("1.1"), true, false, false) { + return false + } + if !yaml_emitter_write_indent(emitter) { + return false + } + } + + if len(event.tag_directives) > 0 { + implicit = false + for i := 0; i < len(event.tag_directives); i++ { + tag_directive := &event.tag_directives[i] + if !yaml_emitter_write_indicator(emitter, []byte("%TAG"), true, false, false) { + return false + } + if !yaml_emitter_write_tag_handle(emitter, tag_directive.handle) { + return false + } + if !yaml_emitter_write_tag_content(emitter, tag_directive.prefix, true) { + return false + } + if !yaml_emitter_write_indent(emitter) { + return false + } + } + } + + if yaml_emitter_check_empty_document(emitter) { + implicit = false + } + if !implicit { + if !yaml_emitter_write_indent(emitter) { + return false + } + if !yaml_emitter_write_indicator(emitter, []byte("---"), true, false, false) { + return false + } + if emitter.canonical || true { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + } + + if len(emitter.head_comment) > 0 { + if !yaml_emitter_process_head_comment(emitter) { + return false + } + if !put_break(emitter) { + return false + } + } + + emitter.state = yaml_EMIT_DOCUMENT_CONTENT_STATE + return true + } + + if event.typ == yaml_STREAM_END_EVENT { + if emitter.open_ended { + if !yaml_emitter_write_indicator(emitter, []byte("..."), true, false, false) { + return false + } + if !yaml_emitter_write_indent(emitter) { + return false + } + } + if !yaml_emitter_flush(emitter) { + return false + } + emitter.state = yaml_EMIT_END_STATE + return true + } + + return yaml_emitter_set_emitter_error(emitter, "expected DOCUMENT-START or STREAM-END") +} + +// Expect the root node. +func yaml_emitter_emit_document_content(emitter *yaml_emitter_t, event *yaml_event_t) bool { + emitter.states = append(emitter.states, yaml_EMIT_DOCUMENT_END_STATE) + + if !yaml_emitter_process_head_comment(emitter) { + return false + } + if !yaml_emitter_emit_node(emitter, event, true, false, false, false) { + return false + } + if !yaml_emitter_process_line_comment(emitter) { + return false + } + if !yaml_emitter_process_foot_comment(emitter) { + return false + } + return true +} + +// Expect DOCUMENT-END. +func yaml_emitter_emit_document_end(emitter *yaml_emitter_t, event *yaml_event_t) bool { + if event.typ != yaml_DOCUMENT_END_EVENT { + return yaml_emitter_set_emitter_error(emitter, "expected DOCUMENT-END") + } + // [Go] Force document foot separation. + emitter.foot_indent = 0 + if !yaml_emitter_process_foot_comment(emitter) { + return false + } + emitter.foot_indent = -1 + if !yaml_emitter_write_indent(emitter) { + return false + } + if !event.implicit { + // [Go] Allocate the slice elsewhere. + if !yaml_emitter_write_indicator(emitter, []byte("..."), true, false, false) { + return false + } + if !yaml_emitter_write_indent(emitter) { + return false + } + } + if !yaml_emitter_flush(emitter) { + return false + } + emitter.state = yaml_EMIT_DOCUMENT_START_STATE + emitter.tag_directives = emitter.tag_directives[:0] + return true +} + +// Expect a flow item node. +func yaml_emitter_emit_flow_sequence_item(emitter *yaml_emitter_t, event *yaml_event_t, first, trail bool) bool { + if first { + if !yaml_emitter_write_indicator(emitter, []byte{'['}, true, true, false) { + return false + } + if !yaml_emitter_increase_indent(emitter, true, false) { + return false + } + emitter.flow_level++ + } + + if event.typ == yaml_SEQUENCE_END_EVENT { + if emitter.canonical && !first && !trail { + if !yaml_emitter_write_indicator(emitter, []byte{','}, false, false, false) { + return false + } + } + emitter.flow_level-- + emitter.indent = emitter.indents[len(emitter.indents)-1] + emitter.indents = emitter.indents[:len(emitter.indents)-1] + if emitter.column == 0 || emitter.canonical && !first { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + if !yaml_emitter_write_indicator(emitter, []byte{']'}, false, false, false) { + return false + } + if !yaml_emitter_process_line_comment(emitter) { + return false + } + if !yaml_emitter_process_foot_comment(emitter) { + return false + } + emitter.state = emitter.states[len(emitter.states)-1] + emitter.states = emitter.states[:len(emitter.states)-1] + + return true + } + + if !first && !trail { + if !yaml_emitter_write_indicator(emitter, []byte{','}, false, false, false) { + return false + } + } + + if !yaml_emitter_process_head_comment(emitter) { + return false + } + if emitter.column == 0 { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + + if emitter.canonical || emitter.column > emitter.best_width { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + if len(emitter.line_comment)+len(emitter.foot_comment)+len(emitter.tail_comment) > 0 { + emitter.states = append(emitter.states, yaml_EMIT_FLOW_SEQUENCE_TRAIL_ITEM_STATE) + } else { + emitter.states = append(emitter.states, yaml_EMIT_FLOW_SEQUENCE_ITEM_STATE) + } + if !yaml_emitter_emit_node(emitter, event, false, true, false, false) { + return false + } + if len(emitter.line_comment)+len(emitter.foot_comment)+len(emitter.tail_comment) > 0 { + if !yaml_emitter_write_indicator(emitter, []byte{','}, false, false, false) { + return false + } + } + if !yaml_emitter_process_line_comment(emitter) { + return false + } + if !yaml_emitter_process_foot_comment(emitter) { + return false + } + return true +} + +// Expect a flow key node. +func yaml_emitter_emit_flow_mapping_key(emitter *yaml_emitter_t, event *yaml_event_t, first, trail bool) bool { + if first { + if !yaml_emitter_write_indicator(emitter, []byte{'{'}, true, true, false) { + return false + } + if !yaml_emitter_increase_indent(emitter, true, false) { + return false + } + emitter.flow_level++ + } + + if event.typ == yaml_MAPPING_END_EVENT { + if (emitter.canonical || len(emitter.head_comment)+len(emitter.foot_comment)+len(emitter.tail_comment) > 0) && !first && !trail { + if !yaml_emitter_write_indicator(emitter, []byte{','}, false, false, false) { + return false + } + } + if !yaml_emitter_process_head_comment(emitter) { + return false + } + emitter.flow_level-- + emitter.indent = emitter.indents[len(emitter.indents)-1] + emitter.indents = emitter.indents[:len(emitter.indents)-1] + if emitter.canonical && !first { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + if !yaml_emitter_write_indicator(emitter, []byte{'}'}, false, false, false) { + return false + } + if !yaml_emitter_process_line_comment(emitter) { + return false + } + if !yaml_emitter_process_foot_comment(emitter) { + return false + } + emitter.state = emitter.states[len(emitter.states)-1] + emitter.states = emitter.states[:len(emitter.states)-1] + return true + } + + if !first && !trail { + if !yaml_emitter_write_indicator(emitter, []byte{','}, false, false, false) { + return false + } + } + + if !yaml_emitter_process_head_comment(emitter) { + return false + } + + if emitter.column == 0 { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + + if emitter.canonical || emitter.column > emitter.best_width { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + + if !emitter.canonical && yaml_emitter_check_simple_key(emitter) { + emitter.states = append(emitter.states, yaml_EMIT_FLOW_MAPPING_SIMPLE_VALUE_STATE) + return yaml_emitter_emit_node(emitter, event, false, false, true, true) + } + if !yaml_emitter_write_indicator(emitter, []byte{'?'}, true, false, false) { + return false + } + emitter.states = append(emitter.states, yaml_EMIT_FLOW_MAPPING_VALUE_STATE) + return yaml_emitter_emit_node(emitter, event, false, false, true, false) +} + +// Expect a flow value node. +func yaml_emitter_emit_flow_mapping_value(emitter *yaml_emitter_t, event *yaml_event_t, simple bool) bool { + if simple { + if !yaml_emitter_write_indicator(emitter, []byte{':'}, false, false, false) { + return false + } + } else { + if emitter.canonical || emitter.column > emitter.best_width { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + if !yaml_emitter_write_indicator(emitter, []byte{':'}, true, false, false) { + return false + } + } + if len(emitter.line_comment)+len(emitter.foot_comment)+len(emitter.tail_comment) > 0 { + emitter.states = append(emitter.states, yaml_EMIT_FLOW_MAPPING_TRAIL_KEY_STATE) + } else { + emitter.states = append(emitter.states, yaml_EMIT_FLOW_MAPPING_KEY_STATE) + } + if !yaml_emitter_emit_node(emitter, event, false, false, true, false) { + return false + } + if len(emitter.line_comment)+len(emitter.foot_comment)+len(emitter.tail_comment) > 0 { + if !yaml_emitter_write_indicator(emitter, []byte{','}, false, false, false) { + return false + } + } + if !yaml_emitter_process_line_comment(emitter) { + return false + } + if !yaml_emitter_process_foot_comment(emitter) { + return false + } + return true +} + +// Expect a block item node. +func yaml_emitter_emit_block_sequence_item(emitter *yaml_emitter_t, event *yaml_event_t, first bool) bool { + if first { + if !yaml_emitter_increase_indent(emitter, false, false) { + return false + } + } + if event.typ == yaml_SEQUENCE_END_EVENT { + emitter.indent = emitter.indents[len(emitter.indents)-1] + emitter.indents = emitter.indents[:len(emitter.indents)-1] + emitter.state = emitter.states[len(emitter.states)-1] + emitter.states = emitter.states[:len(emitter.states)-1] + return true + } + if !yaml_emitter_process_head_comment(emitter) { + return false + } + if !yaml_emitter_write_indent(emitter) { + return false + } + if !yaml_emitter_write_indicator(emitter, []byte{'-'}, true, false, true) { + return false + } + emitter.states = append(emitter.states, yaml_EMIT_BLOCK_SEQUENCE_ITEM_STATE) + if !yaml_emitter_emit_node(emitter, event, false, true, false, false) { + return false + } + if !yaml_emitter_process_line_comment(emitter) { + return false + } + if !yaml_emitter_process_foot_comment(emitter) { + return false + } + return true +} + +// Expect a block key node. +func yaml_emitter_emit_block_mapping_key(emitter *yaml_emitter_t, event *yaml_event_t, first bool) bool { + if first { + if !yaml_emitter_increase_indent(emitter, false, false) { + return false + } + } + if !yaml_emitter_process_head_comment(emitter) { + return false + } + if event.typ == yaml_MAPPING_END_EVENT { + emitter.indent = emitter.indents[len(emitter.indents)-1] + emitter.indents = emitter.indents[:len(emitter.indents)-1] + emitter.state = emitter.states[len(emitter.states)-1] + emitter.states = emitter.states[:len(emitter.states)-1] + return true + } + if !yaml_emitter_write_indent(emitter) { + return false + } + if len(emitter.line_comment) > 0 { + // [Go] A line comment was provided for the key. That's unusual as the + // scanner associates line comments with the value. Either way, + // save the line comment and render it appropriately later. + emitter.key_line_comment = emitter.line_comment + emitter.line_comment = nil + } + if yaml_emitter_check_simple_key(emitter) { + emitter.states = append(emitter.states, yaml_EMIT_BLOCK_MAPPING_SIMPLE_VALUE_STATE) + return yaml_emitter_emit_node(emitter, event, false, false, true, true) + } + if !yaml_emitter_write_indicator(emitter, []byte{'?'}, true, false, true) { + return false + } + emitter.states = append(emitter.states, yaml_EMIT_BLOCK_MAPPING_VALUE_STATE) + return yaml_emitter_emit_node(emitter, event, false, false, true, false) +} + +// Expect a block value node. +func yaml_emitter_emit_block_mapping_value(emitter *yaml_emitter_t, event *yaml_event_t, simple bool) bool { + if simple { + if !yaml_emitter_write_indicator(emitter, []byte{':'}, false, false, false) { + return false + } + } else { + if !yaml_emitter_write_indent(emitter) { + return false + } + if !yaml_emitter_write_indicator(emitter, []byte{':'}, true, false, true) { + return false + } + } + if len(emitter.key_line_comment) > 0 { + // [Go] Line comments are generally associated with the value, but when there's + // no value on the same line as a mapping key they end up attached to the + // key itself. + if event.typ == yaml_SCALAR_EVENT { + if len(emitter.line_comment) == 0 { + // A scalar is coming and it has no line comments by itself yet, + // so just let it handle the line comment as usual. If it has a + // line comment, we can't have both so the one from the key is lost. + emitter.line_comment = emitter.key_line_comment + emitter.key_line_comment = nil + } + } else if event.sequence_style() != yaml_FLOW_SEQUENCE_STYLE && (event.typ == yaml_MAPPING_START_EVENT || event.typ == yaml_SEQUENCE_START_EVENT) { + // An indented block follows, so write the comment right now. + emitter.line_comment, emitter.key_line_comment = emitter.key_line_comment, emitter.line_comment + if !yaml_emitter_process_line_comment(emitter) { + return false + } + emitter.line_comment, emitter.key_line_comment = emitter.key_line_comment, emitter.line_comment + } + } + emitter.states = append(emitter.states, yaml_EMIT_BLOCK_MAPPING_KEY_STATE) + if !yaml_emitter_emit_node(emitter, event, false, false, true, false) { + return false + } + if !yaml_emitter_process_line_comment(emitter) { + return false + } + if !yaml_emitter_process_foot_comment(emitter) { + return false + } + return true +} + +func yaml_emitter_silent_nil_event(emitter *yaml_emitter_t, event *yaml_event_t) bool { + return event.typ == yaml_SCALAR_EVENT && event.implicit && !emitter.canonical && len(emitter.scalar_data.value) == 0 +} + +// Expect a node. +func yaml_emitter_emit_node(emitter *yaml_emitter_t, event *yaml_event_t, + root bool, sequence bool, mapping bool, simple_key bool) bool { + + emitter.root_context = root + emitter.sequence_context = sequence + emitter.mapping_context = mapping + emitter.simple_key_context = simple_key + + switch event.typ { + case yaml_ALIAS_EVENT: + return yaml_emitter_emit_alias(emitter, event) + case yaml_SCALAR_EVENT: + return yaml_emitter_emit_scalar(emitter, event) + case yaml_SEQUENCE_START_EVENT: + return yaml_emitter_emit_sequence_start(emitter, event) + case yaml_MAPPING_START_EVENT: + return yaml_emitter_emit_mapping_start(emitter, event) + default: + return yaml_emitter_set_emitter_error(emitter, + fmt.Sprintf("expected SCALAR, SEQUENCE-START, MAPPING-START, or ALIAS, but got %v", event.typ)) + } +} + +// Expect ALIAS. +func yaml_emitter_emit_alias(emitter *yaml_emitter_t, event *yaml_event_t) bool { + if !yaml_emitter_process_anchor(emitter) { + return false + } + emitter.state = emitter.states[len(emitter.states)-1] + emitter.states = emitter.states[:len(emitter.states)-1] + return true +} + +// Expect SCALAR. +func yaml_emitter_emit_scalar(emitter *yaml_emitter_t, event *yaml_event_t) bool { + if !yaml_emitter_select_scalar_style(emitter, event) { + return false + } + if !yaml_emitter_process_anchor(emitter) { + return false + } + if !yaml_emitter_process_tag(emitter) { + return false + } + if !yaml_emitter_increase_indent(emitter, true, false) { + return false + } + if !yaml_emitter_process_scalar(emitter) { + return false + } + emitter.indent = emitter.indents[len(emitter.indents)-1] + emitter.indents = emitter.indents[:len(emitter.indents)-1] + emitter.state = emitter.states[len(emitter.states)-1] + emitter.states = emitter.states[:len(emitter.states)-1] + return true +} + +// Expect SEQUENCE-START. +func yaml_emitter_emit_sequence_start(emitter *yaml_emitter_t, event *yaml_event_t) bool { + if !yaml_emitter_process_anchor(emitter) { + return false + } + if !yaml_emitter_process_tag(emitter) { + return false + } + if emitter.flow_level > 0 || emitter.canonical || event.sequence_style() == yaml_FLOW_SEQUENCE_STYLE || + yaml_emitter_check_empty_sequence(emitter) { + emitter.state = yaml_EMIT_FLOW_SEQUENCE_FIRST_ITEM_STATE + } else { + emitter.state = yaml_EMIT_BLOCK_SEQUENCE_FIRST_ITEM_STATE + } + return true +} + +// Expect MAPPING-START. +func yaml_emitter_emit_mapping_start(emitter *yaml_emitter_t, event *yaml_event_t) bool { + if !yaml_emitter_process_anchor(emitter) { + return false + } + if !yaml_emitter_process_tag(emitter) { + return false + } + if emitter.flow_level > 0 || emitter.canonical || event.mapping_style() == yaml_FLOW_MAPPING_STYLE || + yaml_emitter_check_empty_mapping(emitter) { + emitter.state = yaml_EMIT_FLOW_MAPPING_FIRST_KEY_STATE + } else { + emitter.state = yaml_EMIT_BLOCK_MAPPING_FIRST_KEY_STATE + } + return true +} + +// Check if the document content is an empty scalar. +func yaml_emitter_check_empty_document(emitter *yaml_emitter_t) bool { + return false // [Go] Huh? +} + +// Check if the next events represent an empty sequence. +func yaml_emitter_check_empty_sequence(emitter *yaml_emitter_t) bool { + if len(emitter.events)-emitter.events_head < 2 { + return false + } + return emitter.events[emitter.events_head].typ == yaml_SEQUENCE_START_EVENT && + emitter.events[emitter.events_head+1].typ == yaml_SEQUENCE_END_EVENT +} + +// Check if the next events represent an empty mapping. +func yaml_emitter_check_empty_mapping(emitter *yaml_emitter_t) bool { + if len(emitter.events)-emitter.events_head < 2 { + return false + } + return emitter.events[emitter.events_head].typ == yaml_MAPPING_START_EVENT && + emitter.events[emitter.events_head+1].typ == yaml_MAPPING_END_EVENT +} + +// Check if the next node can be expressed as a simple key. +func yaml_emitter_check_simple_key(emitter *yaml_emitter_t) bool { + length := 0 + switch emitter.events[emitter.events_head].typ { + case yaml_ALIAS_EVENT: + length += len(emitter.anchor_data.anchor) + case yaml_SCALAR_EVENT: + if emitter.scalar_data.multiline { + return false + } + length += len(emitter.anchor_data.anchor) + + len(emitter.tag_data.handle) + + len(emitter.tag_data.suffix) + + len(emitter.scalar_data.value) + case yaml_SEQUENCE_START_EVENT: + if !yaml_emitter_check_empty_sequence(emitter) { + return false + } + length += len(emitter.anchor_data.anchor) + + len(emitter.tag_data.handle) + + len(emitter.tag_data.suffix) + case yaml_MAPPING_START_EVENT: + if !yaml_emitter_check_empty_mapping(emitter) { + return false + } + length += len(emitter.anchor_data.anchor) + + len(emitter.tag_data.handle) + + len(emitter.tag_data.suffix) + default: + return false + } + return length <= 128 +} + +// Determine an acceptable scalar style. +func yaml_emitter_select_scalar_style(emitter *yaml_emitter_t, event *yaml_event_t) bool { + + no_tag := len(emitter.tag_data.handle) == 0 && len(emitter.tag_data.suffix) == 0 + if no_tag && !event.implicit && !event.quoted_implicit { + return yaml_emitter_set_emitter_error(emitter, "neither tag nor implicit flags are specified") + } + + style := event.scalar_style() + if style == yaml_ANY_SCALAR_STYLE { + style = yaml_PLAIN_SCALAR_STYLE + } + if emitter.canonical { + style = yaml_DOUBLE_QUOTED_SCALAR_STYLE + } + if emitter.simple_key_context && emitter.scalar_data.multiline { + style = yaml_DOUBLE_QUOTED_SCALAR_STYLE + } + + if style == yaml_PLAIN_SCALAR_STYLE { + if emitter.flow_level > 0 && !emitter.scalar_data.flow_plain_allowed || + emitter.flow_level == 0 && !emitter.scalar_data.block_plain_allowed { + style = yaml_SINGLE_QUOTED_SCALAR_STYLE + } + if len(emitter.scalar_data.value) == 0 && (emitter.flow_level > 0 || emitter.simple_key_context) { + style = yaml_SINGLE_QUOTED_SCALAR_STYLE + } + if no_tag && !event.implicit { + style = yaml_SINGLE_QUOTED_SCALAR_STYLE + } + } + if style == yaml_SINGLE_QUOTED_SCALAR_STYLE { + if !emitter.scalar_data.single_quoted_allowed { + style = yaml_DOUBLE_QUOTED_SCALAR_STYLE + } + } + if style == yaml_LITERAL_SCALAR_STYLE || style == yaml_FOLDED_SCALAR_STYLE { + if !emitter.scalar_data.block_allowed || emitter.flow_level > 0 || emitter.simple_key_context { + style = yaml_DOUBLE_QUOTED_SCALAR_STYLE + } + } + + if no_tag && !event.quoted_implicit && style != yaml_PLAIN_SCALAR_STYLE { + emitter.tag_data.handle = []byte{'!'} + } + emitter.scalar_data.style = style + return true +} + +// Write an anchor. +func yaml_emitter_process_anchor(emitter *yaml_emitter_t) bool { + if emitter.anchor_data.anchor == nil { + return true + } + c := []byte{'&'} + if emitter.anchor_data.alias { + c[0] = '*' + } + if !yaml_emitter_write_indicator(emitter, c, true, false, false) { + return false + } + return yaml_emitter_write_anchor(emitter, emitter.anchor_data.anchor) +} + +// Write a tag. +func yaml_emitter_process_tag(emitter *yaml_emitter_t) bool { + if len(emitter.tag_data.handle) == 0 && len(emitter.tag_data.suffix) == 0 { + return true + } + if len(emitter.tag_data.handle) > 0 { + if !yaml_emitter_write_tag_handle(emitter, emitter.tag_data.handle) { + return false + } + if len(emitter.tag_data.suffix) > 0 { + if !yaml_emitter_write_tag_content(emitter, emitter.tag_data.suffix, false) { + return false + } + } + } else { + // [Go] Allocate these slices elsewhere. + if !yaml_emitter_write_indicator(emitter, []byte("!<"), true, false, false) { + return false + } + if !yaml_emitter_write_tag_content(emitter, emitter.tag_data.suffix, false) { + return false + } + if !yaml_emitter_write_indicator(emitter, []byte{'>'}, false, false, false) { + return false + } + } + return true +} + +// Write a scalar. +func yaml_emitter_process_scalar(emitter *yaml_emitter_t) bool { + switch emitter.scalar_data.style { + case yaml_PLAIN_SCALAR_STYLE: + return yaml_emitter_write_plain_scalar(emitter, emitter.scalar_data.value, !emitter.simple_key_context) + + case yaml_SINGLE_QUOTED_SCALAR_STYLE: + return yaml_emitter_write_single_quoted_scalar(emitter, emitter.scalar_data.value, !emitter.simple_key_context) + + case yaml_DOUBLE_QUOTED_SCALAR_STYLE: + return yaml_emitter_write_double_quoted_scalar(emitter, emitter.scalar_data.value, !emitter.simple_key_context) + + case yaml_LITERAL_SCALAR_STYLE: + return yaml_emitter_write_literal_scalar(emitter, emitter.scalar_data.value) + + case yaml_FOLDED_SCALAR_STYLE: + return yaml_emitter_write_folded_scalar(emitter, emitter.scalar_data.value) + } + panic("unknown scalar style") +} + +// Write a head comment. +func yaml_emitter_process_head_comment(emitter *yaml_emitter_t) bool { + if len(emitter.tail_comment) > 0 { + if !yaml_emitter_write_indent(emitter) { + return false + } + if !yaml_emitter_write_comment(emitter, emitter.tail_comment) { + return false + } + emitter.tail_comment = emitter.tail_comment[:0] + emitter.foot_indent = emitter.indent + if emitter.foot_indent < 0 { + emitter.foot_indent = 0 + } + } + + if len(emitter.head_comment) == 0 { + return true + } + if !yaml_emitter_write_indent(emitter) { + return false + } + if !yaml_emitter_write_comment(emitter, emitter.head_comment) { + return false + } + emitter.head_comment = emitter.head_comment[:0] + return true +} + +// Write an line comment. +func yaml_emitter_process_line_comment(emitter *yaml_emitter_t) bool { + if len(emitter.line_comment) == 0 { + return true + } + if !emitter.whitespace { + if !put(emitter, ' ') { + return false + } + } + if !yaml_emitter_write_comment(emitter, emitter.line_comment) { + return false + } + emitter.line_comment = emitter.line_comment[:0] + return true +} + +// Write a foot comment. +func yaml_emitter_process_foot_comment(emitter *yaml_emitter_t) bool { + if len(emitter.foot_comment) == 0 { + return true + } + if !yaml_emitter_write_indent(emitter) { + return false + } + if !yaml_emitter_write_comment(emitter, emitter.foot_comment) { + return false + } + emitter.foot_comment = emitter.foot_comment[:0] + emitter.foot_indent = emitter.indent + if emitter.foot_indent < 0 { + emitter.foot_indent = 0 + } + return true +} + +// Check if a %YAML directive is valid. +func yaml_emitter_analyze_version_directive(emitter *yaml_emitter_t, version_directive *yaml_version_directive_t) bool { + if version_directive.major != 1 || version_directive.minor != 1 { + return yaml_emitter_set_emitter_error(emitter, "incompatible %YAML directive") + } + return true +} + +// Check if a %TAG directive is valid. +func yaml_emitter_analyze_tag_directive(emitter *yaml_emitter_t, tag_directive *yaml_tag_directive_t) bool { + handle := tag_directive.handle + prefix := tag_directive.prefix + if len(handle) == 0 { + return yaml_emitter_set_emitter_error(emitter, "tag handle must not be empty") + } + if handle[0] != '!' { + return yaml_emitter_set_emitter_error(emitter, "tag handle must start with '!'") + } + if handle[len(handle)-1] != '!' { + return yaml_emitter_set_emitter_error(emitter, "tag handle must end with '!'") + } + for i := 1; i < len(handle)-1; i += width(handle[i]) { + if !is_alpha(handle, i) { + return yaml_emitter_set_emitter_error(emitter, "tag handle must contain alphanumerical characters only") + } + } + if len(prefix) == 0 { + return yaml_emitter_set_emitter_error(emitter, "tag prefix must not be empty") + } + return true +} + +// Check if an anchor is valid. +func yaml_emitter_analyze_anchor(emitter *yaml_emitter_t, anchor []byte, alias bool) bool { + if len(anchor) == 0 { + problem := "anchor value must not be empty" + if alias { + problem = "alias value must not be empty" + } + return yaml_emitter_set_emitter_error(emitter, problem) + } + for i := 0; i < len(anchor); i += width(anchor[i]) { + if !is_alpha(anchor, i) { + problem := "anchor value must contain alphanumerical characters only" + if alias { + problem = "alias value must contain alphanumerical characters only" + } + return yaml_emitter_set_emitter_error(emitter, problem) + } + } + emitter.anchor_data.anchor = anchor + emitter.anchor_data.alias = alias + return true +} + +// Check if a tag is valid. +func yaml_emitter_analyze_tag(emitter *yaml_emitter_t, tag []byte) bool { + if len(tag) == 0 { + return yaml_emitter_set_emitter_error(emitter, "tag value must not be empty") + } + for i := 0; i < len(emitter.tag_directives); i++ { + tag_directive := &emitter.tag_directives[i] + if bytes.HasPrefix(tag, tag_directive.prefix) { + emitter.tag_data.handle = tag_directive.handle + emitter.tag_data.suffix = tag[len(tag_directive.prefix):] + return true + } + } + emitter.tag_data.suffix = tag + return true +} + +// Check if a scalar is valid. +func yaml_emitter_analyze_scalar(emitter *yaml_emitter_t, value []byte) bool { + var ( + block_indicators = false + flow_indicators = false + line_breaks = false + special_characters = false + tab_characters = false + + leading_space = false + leading_break = false + trailing_space = false + trailing_break = false + break_space = false + space_break = false + + preceded_by_whitespace = false + followed_by_whitespace = false + previous_space = false + previous_break = false + ) + + emitter.scalar_data.value = value + + if len(value) == 0 { + emitter.scalar_data.multiline = false + emitter.scalar_data.flow_plain_allowed = false + emitter.scalar_data.block_plain_allowed = true + emitter.scalar_data.single_quoted_allowed = true + emitter.scalar_data.block_allowed = false + return true + } + + if len(value) >= 3 && ((value[0] == '-' && value[1] == '-' && value[2] == '-') || (value[0] == '.' && value[1] == '.' && value[2] == '.')) { + block_indicators = true + flow_indicators = true + } + + preceded_by_whitespace = true + for i, w := 0, 0; i < len(value); i += w { + w = width(value[i]) + followed_by_whitespace = i+w >= len(value) || is_blank(value, i+w) + + if i == 0 { + switch value[i] { + case '#', ',', '[', ']', '{', '}', '&', '*', '!', '|', '>', '\'', '"', '%', '@', '`': + flow_indicators = true + block_indicators = true + case '?', ':': + flow_indicators = true + if followed_by_whitespace { + block_indicators = true + } + case '-': + if followed_by_whitespace { + flow_indicators = true + block_indicators = true + } + } + } else { + switch value[i] { + case ',', '?', '[', ']', '{', '}': + flow_indicators = true + case ':': + flow_indicators = true + if followed_by_whitespace { + block_indicators = true + } + case '#': + if preceded_by_whitespace { + flow_indicators = true + block_indicators = true + } + } + } + + if value[i] == '\t' { + tab_characters = true + } else if !is_printable(value, i) || !is_ascii(value, i) && !emitter.unicode { + special_characters = true + } + if is_space(value, i) { + if i == 0 { + leading_space = true + } + if i+width(value[i]) == len(value) { + trailing_space = true + } + if previous_break { + break_space = true + } + previous_space = true + previous_break = false + } else if is_break(value, i) { + line_breaks = true + if i == 0 { + leading_break = true + } + if i+width(value[i]) == len(value) { + trailing_break = true + } + if previous_space { + space_break = true + } + previous_space = false + previous_break = true + } else { + previous_space = false + previous_break = false + } + + // [Go]: Why 'z'? Couldn't be the end of the string as that's the loop condition. + preceded_by_whitespace = is_blankz(value, i) + } + + emitter.scalar_data.multiline = line_breaks + emitter.scalar_data.flow_plain_allowed = true + emitter.scalar_data.block_plain_allowed = true + emitter.scalar_data.single_quoted_allowed = true + emitter.scalar_data.block_allowed = true + + if leading_space || leading_break || trailing_space || trailing_break { + emitter.scalar_data.flow_plain_allowed = false + emitter.scalar_data.block_plain_allowed = false + } + if trailing_space { + emitter.scalar_data.block_allowed = false + } + if break_space { + emitter.scalar_data.flow_plain_allowed = false + emitter.scalar_data.block_plain_allowed = false + emitter.scalar_data.single_quoted_allowed = false + } + if space_break || tab_characters || special_characters { + emitter.scalar_data.flow_plain_allowed = false + emitter.scalar_data.block_plain_allowed = false + emitter.scalar_data.single_quoted_allowed = false + } + if space_break || special_characters { + emitter.scalar_data.block_allowed = false + } + if line_breaks { + emitter.scalar_data.flow_plain_allowed = false + emitter.scalar_data.block_plain_allowed = false + } + if flow_indicators { + emitter.scalar_data.flow_plain_allowed = false + } + if block_indicators { + emitter.scalar_data.block_plain_allowed = false + } + return true +} + +// Check if the event data is valid. +func yaml_emitter_analyze_event(emitter *yaml_emitter_t, event *yaml_event_t) bool { + + emitter.anchor_data.anchor = nil + emitter.tag_data.handle = nil + emitter.tag_data.suffix = nil + emitter.scalar_data.value = nil + + if len(event.head_comment) > 0 { + emitter.head_comment = event.head_comment + } + if len(event.line_comment) > 0 { + emitter.line_comment = event.line_comment + } + if len(event.foot_comment) > 0 { + emitter.foot_comment = event.foot_comment + } + if len(event.tail_comment) > 0 { + emitter.tail_comment = event.tail_comment + } + + switch event.typ { + case yaml_ALIAS_EVENT: + if !yaml_emitter_analyze_anchor(emitter, event.anchor, true) { + return false + } + + case yaml_SCALAR_EVENT: + if len(event.anchor) > 0 { + if !yaml_emitter_analyze_anchor(emitter, event.anchor, false) { + return false + } + } + if len(event.tag) > 0 && (emitter.canonical || (!event.implicit && !event.quoted_implicit)) { + if !yaml_emitter_analyze_tag(emitter, event.tag) { + return false + } + } + if !yaml_emitter_analyze_scalar(emitter, event.value) { + return false + } + + case yaml_SEQUENCE_START_EVENT: + if len(event.anchor) > 0 { + if !yaml_emitter_analyze_anchor(emitter, event.anchor, false) { + return false + } + } + if len(event.tag) > 0 && (emitter.canonical || !event.implicit) { + if !yaml_emitter_analyze_tag(emitter, event.tag) { + return false + } + } + + case yaml_MAPPING_START_EVENT: + if len(event.anchor) > 0 { + if !yaml_emitter_analyze_anchor(emitter, event.anchor, false) { + return false + } + } + if len(event.tag) > 0 && (emitter.canonical || !event.implicit) { + if !yaml_emitter_analyze_tag(emitter, event.tag) { + return false + } + } + } + return true +} + +// Write the BOM character. +func yaml_emitter_write_bom(emitter *yaml_emitter_t) bool { + if !flush(emitter) { + return false + } + pos := emitter.buffer_pos + emitter.buffer[pos+0] = '\xEF' + emitter.buffer[pos+1] = '\xBB' + emitter.buffer[pos+2] = '\xBF' + emitter.buffer_pos += 3 + return true +} + +func yaml_emitter_write_indent(emitter *yaml_emitter_t) bool { + indent := emitter.indent + if indent < 0 { + indent = 0 + } + if !emitter.indention || emitter.column > indent || (emitter.column == indent && !emitter.whitespace) { + if !put_break(emitter) { + return false + } + } + if emitter.foot_indent == indent { + if !put_break(emitter) { + return false + } + } + for emitter.column < indent { + if !put(emitter, ' ') { + return false + } + } + emitter.whitespace = true + //emitter.indention = true + emitter.space_above = false + emitter.foot_indent = -1 + return true +} + +func yaml_emitter_write_indicator(emitter *yaml_emitter_t, indicator []byte, need_whitespace, is_whitespace, is_indention bool) bool { + if need_whitespace && !emitter.whitespace { + if !put(emitter, ' ') { + return false + } + } + if !write_all(emitter, indicator) { + return false + } + emitter.whitespace = is_whitespace + emitter.indention = (emitter.indention && is_indention) + emitter.open_ended = false + return true +} + +func yaml_emitter_write_anchor(emitter *yaml_emitter_t, value []byte) bool { + if !write_all(emitter, value) { + return false + } + emitter.whitespace = false + emitter.indention = false + return true +} + +func yaml_emitter_write_tag_handle(emitter *yaml_emitter_t, value []byte) bool { + if !emitter.whitespace { + if !put(emitter, ' ') { + return false + } + } + if !write_all(emitter, value) { + return false + } + emitter.whitespace = false + emitter.indention = false + return true +} + +func yaml_emitter_write_tag_content(emitter *yaml_emitter_t, value []byte, need_whitespace bool) bool { + if need_whitespace && !emitter.whitespace { + if !put(emitter, ' ') { + return false + } + } + for i := 0; i < len(value); { + var must_write bool + switch value[i] { + case ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '_', '.', '~', '*', '\'', '(', ')', '[', ']': + must_write = true + default: + must_write = is_alpha(value, i) + } + if must_write { + if !write(emitter, value, &i) { + return false + } + } else { + w := width(value[i]) + for k := 0; k < w; k++ { + octet := value[i] + i++ + if !put(emitter, '%') { + return false + } + + c := octet >> 4 + if c < 10 { + c += '0' + } else { + c += 'A' - 10 + } + if !put(emitter, c) { + return false + } + + c = octet & 0x0f + if c < 10 { + c += '0' + } else { + c += 'A' - 10 + } + if !put(emitter, c) { + return false + } + } + } + } + emitter.whitespace = false + emitter.indention = false + return true +} + +func yaml_emitter_write_plain_scalar(emitter *yaml_emitter_t, value []byte, allow_breaks bool) bool { + if len(value) > 0 && !emitter.whitespace { + if !put(emitter, ' ') { + return false + } + } + + spaces := false + breaks := false + for i := 0; i < len(value); { + if is_space(value, i) { + if allow_breaks && !spaces && emitter.column > emitter.best_width && !is_space(value, i+1) { + if !yaml_emitter_write_indent(emitter) { + return false + } + i += width(value[i]) + } else { + if !write(emitter, value, &i) { + return false + } + } + spaces = true + } else if is_break(value, i) { + if !breaks && value[i] == '\n' { + if !put_break(emitter) { + return false + } + } + if !write_break(emitter, value, &i) { + return false + } + //emitter.indention = true + breaks = true + } else { + if breaks { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + if !write(emitter, value, &i) { + return false + } + emitter.indention = false + spaces = false + breaks = false + } + } + + if len(value) > 0 { + emitter.whitespace = false + } + emitter.indention = false + if emitter.root_context { + emitter.open_ended = true + } + + return true +} + +func yaml_emitter_write_single_quoted_scalar(emitter *yaml_emitter_t, value []byte, allow_breaks bool) bool { + + if !yaml_emitter_write_indicator(emitter, []byte{'\''}, true, false, false) { + return false + } + + spaces := false + breaks := false + for i := 0; i < len(value); { + if is_space(value, i) { + if allow_breaks && !spaces && emitter.column > emitter.best_width && i > 0 && i < len(value)-1 && !is_space(value, i+1) { + if !yaml_emitter_write_indent(emitter) { + return false + } + i += width(value[i]) + } else { + if !write(emitter, value, &i) { + return false + } + } + spaces = true + } else if is_break(value, i) { + if !breaks && value[i] == '\n' { + if !put_break(emitter) { + return false + } + } + if !write_break(emitter, value, &i) { + return false + } + //emitter.indention = true + breaks = true + } else { + if breaks { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + if value[i] == '\'' { + if !put(emitter, '\'') { + return false + } + } + if !write(emitter, value, &i) { + return false + } + emitter.indention = false + spaces = false + breaks = false + } + } + if !yaml_emitter_write_indicator(emitter, []byte{'\''}, false, false, false) { + return false + } + emitter.whitespace = false + emitter.indention = false + return true +} + +func yaml_emitter_write_double_quoted_scalar(emitter *yaml_emitter_t, value []byte, allow_breaks bool) bool { + spaces := false + if !yaml_emitter_write_indicator(emitter, []byte{'"'}, true, false, false) { + return false + } + + for i := 0; i < len(value); { + if !is_printable(value, i) || (!emitter.unicode && !is_ascii(value, i)) || + is_bom(value, i) || is_break(value, i) || + value[i] == '"' || value[i] == '\\' { + + octet := value[i] + + var w int + var v rune + switch { + case octet&0x80 == 0x00: + w, v = 1, rune(octet&0x7F) + case octet&0xE0 == 0xC0: + w, v = 2, rune(octet&0x1F) + case octet&0xF0 == 0xE0: + w, v = 3, rune(octet&0x0F) + case octet&0xF8 == 0xF0: + w, v = 4, rune(octet&0x07) + } + for k := 1; k < w; k++ { + octet = value[i+k] + v = (v << 6) + (rune(octet) & 0x3F) + } + i += w + + if !put(emitter, '\\') { + return false + } + + var ok bool + switch v { + case 0x00: + ok = put(emitter, '0') + case 0x07: + ok = put(emitter, 'a') + case 0x08: + ok = put(emitter, 'b') + case 0x09: + ok = put(emitter, 't') + case 0x0A: + ok = put(emitter, 'n') + case 0x0b: + ok = put(emitter, 'v') + case 0x0c: + ok = put(emitter, 'f') + case 0x0d: + ok = put(emitter, 'r') + case 0x1b: + ok = put(emitter, 'e') + case 0x22: + ok = put(emitter, '"') + case 0x5c: + ok = put(emitter, '\\') + case 0x85: + ok = put(emitter, 'N') + case 0xA0: + ok = put(emitter, '_') + case 0x2028: + ok = put(emitter, 'L') + case 0x2029: + ok = put(emitter, 'P') + default: + if v <= 0xFF { + ok = put(emitter, 'x') + w = 2 + } else if v <= 0xFFFF { + ok = put(emitter, 'u') + w = 4 + } else { + ok = put(emitter, 'U') + w = 8 + } + for k := (w - 1) * 4; ok && k >= 0; k -= 4 { + digit := byte((v >> uint(k)) & 0x0F) + if digit < 10 { + ok = put(emitter, digit+'0') + } else { + ok = put(emitter, digit+'A'-10) + } + } + } + if !ok { + return false + } + spaces = false + } else if is_space(value, i) { + if allow_breaks && !spaces && emitter.column > emitter.best_width && i > 0 && i < len(value)-1 { + if !yaml_emitter_write_indent(emitter) { + return false + } + if is_space(value, i+1) { + if !put(emitter, '\\') { + return false + } + } + i += width(value[i]) + } else if !write(emitter, value, &i) { + return false + } + spaces = true + } else { + if !write(emitter, value, &i) { + return false + } + spaces = false + } + } + if !yaml_emitter_write_indicator(emitter, []byte{'"'}, false, false, false) { + return false + } + emitter.whitespace = false + emitter.indention = false + return true +} + +func yaml_emitter_write_block_scalar_hints(emitter *yaml_emitter_t, value []byte) bool { + if is_space(value, 0) || is_break(value, 0) { + indent_hint := []byte{'0' + byte(emitter.best_indent)} + if !yaml_emitter_write_indicator(emitter, indent_hint, false, false, false) { + return false + } + } + + emitter.open_ended = false + + var chomp_hint [1]byte + if len(value) == 0 { + chomp_hint[0] = '-' + } else { + i := len(value) - 1 + for value[i]&0xC0 == 0x80 { + i-- + } + if !is_break(value, i) { + chomp_hint[0] = '-' + } else if i == 0 { + chomp_hint[0] = '+' + emitter.open_ended = true + } else { + i-- + for value[i]&0xC0 == 0x80 { + i-- + } + if is_break(value, i) { + chomp_hint[0] = '+' + emitter.open_ended = true + } + } + } + if chomp_hint[0] != 0 { + if !yaml_emitter_write_indicator(emitter, chomp_hint[:], false, false, false) { + return false + } + } + return true +} + +func yaml_emitter_write_literal_scalar(emitter *yaml_emitter_t, value []byte) bool { + if !yaml_emitter_write_indicator(emitter, []byte{'|'}, true, false, false) { + return false + } + if !yaml_emitter_write_block_scalar_hints(emitter, value) { + return false + } + if !yaml_emitter_process_line_comment(emitter) { + return false + } + //emitter.indention = true + emitter.whitespace = true + breaks := true + for i := 0; i < len(value); { + if is_break(value, i) { + if !write_break(emitter, value, &i) { + return false + } + //emitter.indention = true + breaks = true + } else { + if breaks { + if !yaml_emitter_write_indent(emitter) { + return false + } + } + if !write(emitter, value, &i) { + return false + } + emitter.indention = false + breaks = false + } + } + + return true +} + +func yaml_emitter_write_folded_scalar(emitter *yaml_emitter_t, value []byte) bool { + if !yaml_emitter_write_indicator(emitter, []byte{'>'}, true, false, false) { + return false + } + if !yaml_emitter_write_block_scalar_hints(emitter, value) { + return false + } + if !yaml_emitter_process_line_comment(emitter) { + return false + } + + //emitter.indention = true + emitter.whitespace = true + + breaks := true + leading_spaces := true + for i := 0; i < len(value); { + if is_break(value, i) { + if !breaks && !leading_spaces && value[i] == '\n' { + k := 0 + for is_break(value, k) { + k += width(value[k]) + } + if !is_blankz(value, k) { + if !put_break(emitter) { + return false + } + } + } + if !write_break(emitter, value, &i) { + return false + } + //emitter.indention = true + breaks = true + } else { + if breaks { + if !yaml_emitter_write_indent(emitter) { + return false + } + leading_spaces = is_blank(value, i) + } + if !breaks && is_space(value, i) && !is_space(value, i+1) && emitter.column > emitter.best_width { + if !yaml_emitter_write_indent(emitter) { + return false + } + i += width(value[i]) + } else { + if !write(emitter, value, &i) { + return false + } + } + emitter.indention = false + breaks = false + } + } + return true +} + +func yaml_emitter_write_comment(emitter *yaml_emitter_t, comment []byte) bool { + breaks := false + pound := false + for i := 0; i < len(comment); { + if is_break(comment, i) { + if !write_break(emitter, comment, &i) { + return false + } + //emitter.indention = true + breaks = true + pound = false + } else { + if breaks && !yaml_emitter_write_indent(emitter) { + return false + } + if !pound { + if comment[i] != '#' && (!put(emitter, '#') || !put(emitter, ' ')) { + return false + } + pound = true + } + if !write(emitter, comment, &i) { + return false + } + emitter.indention = false + breaks = false + } + } + if !breaks && !put_break(emitter) { + return false + } + + emitter.whitespace = true + //emitter.indention = true + return true +} diff --git a/asyncapi/vendor/gopkg.in/yaml.v3/encode.go b/asyncapi/vendor/gopkg.in/yaml.v3/encode.go new file mode 100644 index 0000000..de9e72a --- /dev/null +++ b/asyncapi/vendor/gopkg.in/yaml.v3/encode.go @@ -0,0 +1,577 @@ +// +// Copyright (c) 2011-2019 Canonical Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package yaml + +import ( + "encoding" + "fmt" + "io" + "reflect" + "regexp" + "sort" + "strconv" + "strings" + "time" + "unicode/utf8" +) + +type encoder struct { + emitter yaml_emitter_t + event yaml_event_t + out []byte + flow bool + indent int + doneInit bool +} + +func newEncoder() *encoder { + e := &encoder{} + yaml_emitter_initialize(&e.emitter) + yaml_emitter_set_output_string(&e.emitter, &e.out) + yaml_emitter_set_unicode(&e.emitter, true) + return e +} + +func newEncoderWithWriter(w io.Writer) *encoder { + e := &encoder{} + yaml_emitter_initialize(&e.emitter) + yaml_emitter_set_output_writer(&e.emitter, w) + yaml_emitter_set_unicode(&e.emitter, true) + return e +} + +func (e *encoder) init() { + if e.doneInit { + return + } + if e.indent == 0 { + e.indent = 4 + } + e.emitter.best_indent = e.indent + yaml_stream_start_event_initialize(&e.event, yaml_UTF8_ENCODING) + e.emit() + e.doneInit = true +} + +func (e *encoder) finish() { + e.emitter.open_ended = false + yaml_stream_end_event_initialize(&e.event) + e.emit() +} + +func (e *encoder) destroy() { + yaml_emitter_delete(&e.emitter) +} + +func (e *encoder) emit() { + // This will internally delete the e.event value. + e.must(yaml_emitter_emit(&e.emitter, &e.event)) +} + +func (e *encoder) must(ok bool) { + if !ok { + msg := e.emitter.problem + if msg == "" { + msg = "unknown problem generating YAML content" + } + failf("%s", msg) + } +} + +func (e *encoder) marshalDoc(tag string, in reflect.Value) { + e.init() + var node *Node + if in.IsValid() { + node, _ = in.Interface().(*Node) + } + if node != nil && node.Kind == DocumentNode { + e.nodev(in) + } else { + yaml_document_start_event_initialize(&e.event, nil, nil, true) + e.emit() + e.marshal(tag, in) + yaml_document_end_event_initialize(&e.event, true) + e.emit() + } +} + +func (e *encoder) marshal(tag string, in reflect.Value) { + tag = shortTag(tag) + if !in.IsValid() || in.Kind() == reflect.Ptr && in.IsNil() { + e.nilv() + return + } + iface := in.Interface() + switch value := iface.(type) { + case *Node: + e.nodev(in) + return + case Node: + if !in.CanAddr() { + var n = reflect.New(in.Type()).Elem() + n.Set(in) + in = n + } + e.nodev(in.Addr()) + return + case time.Time: + e.timev(tag, in) + return + case *time.Time: + e.timev(tag, in.Elem()) + return + case time.Duration: + e.stringv(tag, reflect.ValueOf(value.String())) + return + case Marshaler: + v, err := value.MarshalYAML() + if err != nil { + fail(err) + } + if v == nil { + e.nilv() + return + } + e.marshal(tag, reflect.ValueOf(v)) + return + case encoding.TextMarshaler: + text, err := value.MarshalText() + if err != nil { + fail(err) + } + in = reflect.ValueOf(string(text)) + case nil: + e.nilv() + return + } + switch in.Kind() { + case reflect.Interface: + e.marshal(tag, in.Elem()) + case reflect.Map: + e.mapv(tag, in) + case reflect.Ptr: + e.marshal(tag, in.Elem()) + case reflect.Struct: + e.structv(tag, in) + case reflect.Slice, reflect.Array: + e.slicev(tag, in) + case reflect.String: + e.stringv(tag, in) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + e.intv(tag, in) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + e.uintv(tag, in) + case reflect.Float32, reflect.Float64: + e.floatv(tag, in) + case reflect.Bool: + e.boolv(tag, in) + default: + panic("cannot marshal type: " + in.Type().String()) + } +} + +func (e *encoder) mapv(tag string, in reflect.Value) { + e.mappingv(tag, func() { + keys := keyList(in.MapKeys()) + sort.Sort(keys) + for _, k := range keys { + e.marshal("", k) + e.marshal("", in.MapIndex(k)) + } + }) +} + +func (e *encoder) fieldByIndex(v reflect.Value, index []int) (field reflect.Value) { + for _, num := range index { + for { + if v.Kind() == reflect.Ptr { + if v.IsNil() { + return reflect.Value{} + } + v = v.Elem() + continue + } + break + } + v = v.Field(num) + } + return v +} + +func (e *encoder) structv(tag string, in reflect.Value) { + sinfo, err := getStructInfo(in.Type()) + if err != nil { + panic(err) + } + e.mappingv(tag, func() { + for _, info := range sinfo.FieldsList { + var value reflect.Value + if info.Inline == nil { + value = in.Field(info.Num) + } else { + value = e.fieldByIndex(in, info.Inline) + if !value.IsValid() { + continue + } + } + if info.OmitEmpty && isZero(value) { + continue + } + e.marshal("", reflect.ValueOf(info.Key)) + e.flow = info.Flow + e.marshal("", value) + } + if sinfo.InlineMap >= 0 { + m := in.Field(sinfo.InlineMap) + if m.Len() > 0 { + e.flow = false + keys := keyList(m.MapKeys()) + sort.Sort(keys) + for _, k := range keys { + if _, found := sinfo.FieldsMap[k.String()]; found { + panic(fmt.Sprintf("cannot have key %q in inlined map: conflicts with struct field", k.String())) + } + e.marshal("", k) + e.flow = false + e.marshal("", m.MapIndex(k)) + } + } + } + }) +} + +func (e *encoder) mappingv(tag string, f func()) { + implicit := tag == "" + style := yaml_BLOCK_MAPPING_STYLE + if e.flow { + e.flow = false + style = yaml_FLOW_MAPPING_STYLE + } + yaml_mapping_start_event_initialize(&e.event, nil, []byte(tag), implicit, style) + e.emit() + f() + yaml_mapping_end_event_initialize(&e.event) + e.emit() +} + +func (e *encoder) slicev(tag string, in reflect.Value) { + implicit := tag == "" + style := yaml_BLOCK_SEQUENCE_STYLE + if e.flow { + e.flow = false + style = yaml_FLOW_SEQUENCE_STYLE + } + e.must(yaml_sequence_start_event_initialize(&e.event, nil, []byte(tag), implicit, style)) + e.emit() + n := in.Len() + for i := 0; i < n; i++ { + e.marshal("", in.Index(i)) + } + e.must(yaml_sequence_end_event_initialize(&e.event)) + e.emit() +} + +// isBase60 returns whether s is in base 60 notation as defined in YAML 1.1. +// +// The base 60 float notation in YAML 1.1 is a terrible idea and is unsupported +// in YAML 1.2 and by this package, but these should be marshalled quoted for +// the time being for compatibility with other parsers. +func isBase60Float(s string) (result bool) { + // Fast path. + if s == "" { + return false + } + c := s[0] + if !(c == '+' || c == '-' || c >= '0' && c <= '9') || strings.IndexByte(s, ':') < 0 { + return false + } + // Do the full match. + return base60float.MatchString(s) +} + +// From http://yaml.org/type/float.html, except the regular expression there +// is bogus. In practice parsers do not enforce the "\.[0-9_]*" suffix. +var base60float = regexp.MustCompile(`^[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+(?:\.[0-9_]*)?$`) + +// isOldBool returns whether s is bool notation as defined in YAML 1.1. +// +// We continue to force strings that YAML 1.1 would interpret as booleans to be +// rendered as quotes strings so that the marshalled output valid for YAML 1.1 +// parsing. +func isOldBool(s string) (result bool) { + switch s { + case "y", "Y", "yes", "Yes", "YES", "on", "On", "ON", + "n", "N", "no", "No", "NO", "off", "Off", "OFF": + return true + default: + return false + } +} + +func (e *encoder) stringv(tag string, in reflect.Value) { + var style yaml_scalar_style_t + s := in.String() + canUsePlain := true + switch { + case !utf8.ValidString(s): + if tag == binaryTag { + failf("explicitly tagged !!binary data must be base64-encoded") + } + if tag != "" { + failf("cannot marshal invalid UTF-8 data as %s", shortTag(tag)) + } + // It can't be encoded directly as YAML so use a binary tag + // and encode it as base64. + tag = binaryTag + s = encodeBase64(s) + case tag == "": + // Check to see if it would resolve to a specific + // tag when encoded unquoted. If it doesn't, + // there's no need to quote it. + rtag, _ := resolve("", s) + canUsePlain = rtag == strTag && !(isBase60Float(s) || isOldBool(s)) + } + // Note: it's possible for user code to emit invalid YAML + // if they explicitly specify a tag and a string containing + // text that's incompatible with that tag. + switch { + case strings.Contains(s, "\n"): + if e.flow { + style = yaml_DOUBLE_QUOTED_SCALAR_STYLE + } else { + style = yaml_LITERAL_SCALAR_STYLE + } + case canUsePlain: + style = yaml_PLAIN_SCALAR_STYLE + default: + style = yaml_DOUBLE_QUOTED_SCALAR_STYLE + } + e.emitScalar(s, "", tag, style, nil, nil, nil, nil) +} + +func (e *encoder) boolv(tag string, in reflect.Value) { + var s string + if in.Bool() { + s = "true" + } else { + s = "false" + } + e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE, nil, nil, nil, nil) +} + +func (e *encoder) intv(tag string, in reflect.Value) { + s := strconv.FormatInt(in.Int(), 10) + e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE, nil, nil, nil, nil) +} + +func (e *encoder) uintv(tag string, in reflect.Value) { + s := strconv.FormatUint(in.Uint(), 10) + e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE, nil, nil, nil, nil) +} + +func (e *encoder) timev(tag string, in reflect.Value) { + t := in.Interface().(time.Time) + s := t.Format(time.RFC3339Nano) + e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE, nil, nil, nil, nil) +} + +func (e *encoder) floatv(tag string, in reflect.Value) { + // Issue #352: When formatting, use the precision of the underlying value + precision := 64 + if in.Kind() == reflect.Float32 { + precision = 32 + } + + s := strconv.FormatFloat(in.Float(), 'g', -1, precision) + switch s { + case "+Inf": + s = ".inf" + case "-Inf": + s = "-.inf" + case "NaN": + s = ".nan" + } + e.emitScalar(s, "", tag, yaml_PLAIN_SCALAR_STYLE, nil, nil, nil, nil) +} + +func (e *encoder) nilv() { + e.emitScalar("null", "", "", yaml_PLAIN_SCALAR_STYLE, nil, nil, nil, nil) +} + +func (e *encoder) emitScalar(value, anchor, tag string, style yaml_scalar_style_t, head, line, foot, tail []byte) { + // TODO Kill this function. Replace all initialize calls by their underlining Go literals. + implicit := tag == "" + if !implicit { + tag = longTag(tag) + } + e.must(yaml_scalar_event_initialize(&e.event, []byte(anchor), []byte(tag), []byte(value), implicit, implicit, style)) + e.event.head_comment = head + e.event.line_comment = line + e.event.foot_comment = foot + e.event.tail_comment = tail + e.emit() +} + +func (e *encoder) nodev(in reflect.Value) { + e.node(in.Interface().(*Node), "") +} + +func (e *encoder) node(node *Node, tail string) { + // Zero nodes behave as nil. + if node.Kind == 0 && node.IsZero() { + e.nilv() + return + } + + // If the tag was not explicitly requested, and dropping it won't change the + // implicit tag of the value, don't include it in the presentation. + var tag = node.Tag + var stag = shortTag(tag) + var forceQuoting bool + if tag != "" && node.Style&TaggedStyle == 0 { + if node.Kind == ScalarNode { + if stag == strTag && node.Style&(SingleQuotedStyle|DoubleQuotedStyle|LiteralStyle|FoldedStyle) != 0 { + tag = "" + } else { + rtag, _ := resolve("", node.Value) + if rtag == stag { + tag = "" + } else if stag == strTag { + tag = "" + forceQuoting = true + } + } + } else { + var rtag string + switch node.Kind { + case MappingNode: + rtag = mapTag + case SequenceNode: + rtag = seqTag + } + if rtag == stag { + tag = "" + } + } + } + + switch node.Kind { + case DocumentNode: + yaml_document_start_event_initialize(&e.event, nil, nil, true) + e.event.head_comment = []byte(node.HeadComment) + e.emit() + for _, node := range node.Content { + e.node(node, "") + } + yaml_document_end_event_initialize(&e.event, true) + e.event.foot_comment = []byte(node.FootComment) + e.emit() + + case SequenceNode: + style := yaml_BLOCK_SEQUENCE_STYLE + if node.Style&FlowStyle != 0 { + style = yaml_FLOW_SEQUENCE_STYLE + } + e.must(yaml_sequence_start_event_initialize(&e.event, []byte(node.Anchor), []byte(longTag(tag)), tag == "", style)) + e.event.head_comment = []byte(node.HeadComment) + e.emit() + for _, node := range node.Content { + e.node(node, "") + } + e.must(yaml_sequence_end_event_initialize(&e.event)) + e.event.line_comment = []byte(node.LineComment) + e.event.foot_comment = []byte(node.FootComment) + e.emit() + + case MappingNode: + style := yaml_BLOCK_MAPPING_STYLE + if node.Style&FlowStyle != 0 { + style = yaml_FLOW_MAPPING_STYLE + } + yaml_mapping_start_event_initialize(&e.event, []byte(node.Anchor), []byte(longTag(tag)), tag == "", style) + e.event.tail_comment = []byte(tail) + e.event.head_comment = []byte(node.HeadComment) + e.emit() + + // The tail logic below moves the foot comment of prior keys to the following key, + // since the value for each key may be a nested structure and the foot needs to be + // processed only the entirety of the value is streamed. The last tail is processed + // with the mapping end event. + var tail string + for i := 0; i+1 < len(node.Content); i += 2 { + k := node.Content[i] + foot := k.FootComment + if foot != "" { + kopy := *k + kopy.FootComment = "" + k = &kopy + } + e.node(k, tail) + tail = foot + + v := node.Content[i+1] + e.node(v, "") + } + + yaml_mapping_end_event_initialize(&e.event) + e.event.tail_comment = []byte(tail) + e.event.line_comment = []byte(node.LineComment) + e.event.foot_comment = []byte(node.FootComment) + e.emit() + + case AliasNode: + yaml_alias_event_initialize(&e.event, []byte(node.Value)) + e.event.head_comment = []byte(node.HeadComment) + e.event.line_comment = []byte(node.LineComment) + e.event.foot_comment = []byte(node.FootComment) + e.emit() + + case ScalarNode: + value := node.Value + if !utf8.ValidString(value) { + if stag == binaryTag { + failf("explicitly tagged !!binary data must be base64-encoded") + } + if stag != "" { + failf("cannot marshal invalid UTF-8 data as %s", stag) + } + // It can't be encoded directly as YAML so use a binary tag + // and encode it as base64. + tag = binaryTag + value = encodeBase64(value) + } + + style := yaml_PLAIN_SCALAR_STYLE + switch { + case node.Style&DoubleQuotedStyle != 0: + style = yaml_DOUBLE_QUOTED_SCALAR_STYLE + case node.Style&SingleQuotedStyle != 0: + style = yaml_SINGLE_QUOTED_SCALAR_STYLE + case node.Style&LiteralStyle != 0: + style = yaml_LITERAL_SCALAR_STYLE + case node.Style&FoldedStyle != 0: + style = yaml_FOLDED_SCALAR_STYLE + case strings.Contains(value, "\n"): + style = yaml_LITERAL_SCALAR_STYLE + case forceQuoting: + style = yaml_DOUBLE_QUOTED_SCALAR_STYLE + } + + e.emitScalar(value, node.Anchor, tag, style, []byte(node.HeadComment), []byte(node.LineComment), []byte(node.FootComment), []byte(tail)) + default: + failf("cannot encode node with unknown kind %d", node.Kind) + } +} diff --git a/asyncapi/vendor/gopkg.in/yaml.v3/parserc.go b/asyncapi/vendor/gopkg.in/yaml.v3/parserc.go new file mode 100644 index 0000000..268558a --- /dev/null +++ b/asyncapi/vendor/gopkg.in/yaml.v3/parserc.go @@ -0,0 +1,1258 @@ +// +// Copyright (c) 2011-2019 Canonical Ltd +// Copyright (c) 2006-2010 Kirill Simonov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package yaml + +import ( + "bytes" +) + +// The parser implements the following grammar: +// +// stream ::= STREAM-START implicit_document? explicit_document* STREAM-END +// implicit_document ::= block_node DOCUMENT-END* +// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* +// block_node_or_indentless_sequence ::= +// ALIAS +// | properties (block_content | indentless_block_sequence)? +// | block_content +// | indentless_block_sequence +// block_node ::= ALIAS +// | properties block_content? +// | block_content +// flow_node ::= ALIAS +// | properties flow_content? +// | flow_content +// properties ::= TAG ANCHOR? | ANCHOR TAG? +// block_content ::= block_collection | flow_collection | SCALAR +// flow_content ::= flow_collection | SCALAR +// block_collection ::= block_sequence | block_mapping +// flow_collection ::= flow_sequence | flow_mapping +// block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END +// indentless_sequence ::= (BLOCK-ENTRY block_node?)+ +// block_mapping ::= BLOCK-MAPPING_START +// ((KEY block_node_or_indentless_sequence?)? +// (VALUE block_node_or_indentless_sequence?)?)* +// BLOCK-END +// flow_sequence ::= FLOW-SEQUENCE-START +// (flow_sequence_entry FLOW-ENTRY)* +// flow_sequence_entry? +// FLOW-SEQUENCE-END +// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +// flow_mapping ::= FLOW-MAPPING-START +// (flow_mapping_entry FLOW-ENTRY)* +// flow_mapping_entry? +// FLOW-MAPPING-END +// flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? + +// Peek the next token in the token queue. +func peek_token(parser *yaml_parser_t) *yaml_token_t { + if parser.token_available || yaml_parser_fetch_more_tokens(parser) { + token := &parser.tokens[parser.tokens_head] + yaml_parser_unfold_comments(parser, token) + return token + } + return nil +} + +// yaml_parser_unfold_comments walks through the comments queue and joins all +// comments behind the position of the provided token into the respective +// top-level comment slices in the parser. +func yaml_parser_unfold_comments(parser *yaml_parser_t, token *yaml_token_t) { + for parser.comments_head < len(parser.comments) && token.start_mark.index >= parser.comments[parser.comments_head].token_mark.index { + comment := &parser.comments[parser.comments_head] + if len(comment.head) > 0 { + if token.typ == yaml_BLOCK_END_TOKEN { + // No heads on ends, so keep comment.head for a follow up token. + break + } + if len(parser.head_comment) > 0 { + parser.head_comment = append(parser.head_comment, '\n') + } + parser.head_comment = append(parser.head_comment, comment.head...) + } + if len(comment.foot) > 0 { + if len(parser.foot_comment) > 0 { + parser.foot_comment = append(parser.foot_comment, '\n') + } + parser.foot_comment = append(parser.foot_comment, comment.foot...) + } + if len(comment.line) > 0 { + if len(parser.line_comment) > 0 { + parser.line_comment = append(parser.line_comment, '\n') + } + parser.line_comment = append(parser.line_comment, comment.line...) + } + *comment = yaml_comment_t{} + parser.comments_head++ + } +} + +// Remove the next token from the queue (must be called after peek_token). +func skip_token(parser *yaml_parser_t) { + parser.token_available = false + parser.tokens_parsed++ + parser.stream_end_produced = parser.tokens[parser.tokens_head].typ == yaml_STREAM_END_TOKEN + parser.tokens_head++ +} + +// Get the next event. +func yaml_parser_parse(parser *yaml_parser_t, event *yaml_event_t) bool { + // Erase the event object. + *event = yaml_event_t{} + + // No events after the end of the stream or error. + if parser.stream_end_produced || parser.error != yaml_NO_ERROR || parser.state == yaml_PARSE_END_STATE { + return true + } + + // Generate the next event. + return yaml_parser_state_machine(parser, event) +} + +// Set parser error. +func yaml_parser_set_parser_error(parser *yaml_parser_t, problem string, problem_mark yaml_mark_t) bool { + parser.error = yaml_PARSER_ERROR + parser.problem = problem + parser.problem_mark = problem_mark + return false +} + +func yaml_parser_set_parser_error_context(parser *yaml_parser_t, context string, context_mark yaml_mark_t, problem string, problem_mark yaml_mark_t) bool { + parser.error = yaml_PARSER_ERROR + parser.context = context + parser.context_mark = context_mark + parser.problem = problem + parser.problem_mark = problem_mark + return false +} + +// State dispatcher. +func yaml_parser_state_machine(parser *yaml_parser_t, event *yaml_event_t) bool { + //trace("yaml_parser_state_machine", "state:", parser.state.String()) + + switch parser.state { + case yaml_PARSE_STREAM_START_STATE: + return yaml_parser_parse_stream_start(parser, event) + + case yaml_PARSE_IMPLICIT_DOCUMENT_START_STATE: + return yaml_parser_parse_document_start(parser, event, true) + + case yaml_PARSE_DOCUMENT_START_STATE: + return yaml_parser_parse_document_start(parser, event, false) + + case yaml_PARSE_DOCUMENT_CONTENT_STATE: + return yaml_parser_parse_document_content(parser, event) + + case yaml_PARSE_DOCUMENT_END_STATE: + return yaml_parser_parse_document_end(parser, event) + + case yaml_PARSE_BLOCK_NODE_STATE: + return yaml_parser_parse_node(parser, event, true, false) + + case yaml_PARSE_BLOCK_NODE_OR_INDENTLESS_SEQUENCE_STATE: + return yaml_parser_parse_node(parser, event, true, true) + + case yaml_PARSE_FLOW_NODE_STATE: + return yaml_parser_parse_node(parser, event, false, false) + + case yaml_PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE: + return yaml_parser_parse_block_sequence_entry(parser, event, true) + + case yaml_PARSE_BLOCK_SEQUENCE_ENTRY_STATE: + return yaml_parser_parse_block_sequence_entry(parser, event, false) + + case yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE: + return yaml_parser_parse_indentless_sequence_entry(parser, event) + + case yaml_PARSE_BLOCK_MAPPING_FIRST_KEY_STATE: + return yaml_parser_parse_block_mapping_key(parser, event, true) + + case yaml_PARSE_BLOCK_MAPPING_KEY_STATE: + return yaml_parser_parse_block_mapping_key(parser, event, false) + + case yaml_PARSE_BLOCK_MAPPING_VALUE_STATE: + return yaml_parser_parse_block_mapping_value(parser, event) + + case yaml_PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE: + return yaml_parser_parse_flow_sequence_entry(parser, event, true) + + case yaml_PARSE_FLOW_SEQUENCE_ENTRY_STATE: + return yaml_parser_parse_flow_sequence_entry(parser, event, false) + + case yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE: + return yaml_parser_parse_flow_sequence_entry_mapping_key(parser, event) + + case yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE: + return yaml_parser_parse_flow_sequence_entry_mapping_value(parser, event) + + case yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE: + return yaml_parser_parse_flow_sequence_entry_mapping_end(parser, event) + + case yaml_PARSE_FLOW_MAPPING_FIRST_KEY_STATE: + return yaml_parser_parse_flow_mapping_key(parser, event, true) + + case yaml_PARSE_FLOW_MAPPING_KEY_STATE: + return yaml_parser_parse_flow_mapping_key(parser, event, false) + + case yaml_PARSE_FLOW_MAPPING_VALUE_STATE: + return yaml_parser_parse_flow_mapping_value(parser, event, false) + + case yaml_PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE: + return yaml_parser_parse_flow_mapping_value(parser, event, true) + + default: + panic("invalid parser state") + } +} + +// Parse the production: +// stream ::= STREAM-START implicit_document? explicit_document* STREAM-END +// ************ +func yaml_parser_parse_stream_start(parser *yaml_parser_t, event *yaml_event_t) bool { + token := peek_token(parser) + if token == nil { + return false + } + if token.typ != yaml_STREAM_START_TOKEN { + return yaml_parser_set_parser_error(parser, "did not find expected ", token.start_mark) + } + parser.state = yaml_PARSE_IMPLICIT_DOCUMENT_START_STATE + *event = yaml_event_t{ + typ: yaml_STREAM_START_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + encoding: token.encoding, + } + skip_token(parser) + return true +} + +// Parse the productions: +// implicit_document ::= block_node DOCUMENT-END* +// * +// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* +// ************************* +func yaml_parser_parse_document_start(parser *yaml_parser_t, event *yaml_event_t, implicit bool) bool { + + token := peek_token(parser) + if token == nil { + return false + } + + // Parse extra document end indicators. + if !implicit { + for token.typ == yaml_DOCUMENT_END_TOKEN { + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + } + } + + if implicit && token.typ != yaml_VERSION_DIRECTIVE_TOKEN && + token.typ != yaml_TAG_DIRECTIVE_TOKEN && + token.typ != yaml_DOCUMENT_START_TOKEN && + token.typ != yaml_STREAM_END_TOKEN { + // Parse an implicit document. + if !yaml_parser_process_directives(parser, nil, nil) { + return false + } + parser.states = append(parser.states, yaml_PARSE_DOCUMENT_END_STATE) + parser.state = yaml_PARSE_BLOCK_NODE_STATE + + var head_comment []byte + if len(parser.head_comment) > 0 { + // [Go] Scan the header comment backwards, and if an empty line is found, break + // the header so the part before the last empty line goes into the + // document header, while the bottom of it goes into a follow up event. + for i := len(parser.head_comment) - 1; i > 0; i-- { + if parser.head_comment[i] == '\n' { + if i == len(parser.head_comment)-1 { + head_comment = parser.head_comment[:i] + parser.head_comment = parser.head_comment[i+1:] + break + } else if parser.head_comment[i-1] == '\n' { + head_comment = parser.head_comment[:i-1] + parser.head_comment = parser.head_comment[i+1:] + break + } + } + } + } + + *event = yaml_event_t{ + typ: yaml_DOCUMENT_START_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + + head_comment: head_comment, + } + + } else if token.typ != yaml_STREAM_END_TOKEN { + // Parse an explicit document. + var version_directive *yaml_version_directive_t + var tag_directives []yaml_tag_directive_t + start_mark := token.start_mark + if !yaml_parser_process_directives(parser, &version_directive, &tag_directives) { + return false + } + token = peek_token(parser) + if token == nil { + return false + } + if token.typ != yaml_DOCUMENT_START_TOKEN { + yaml_parser_set_parser_error(parser, + "did not find expected ", token.start_mark) + return false + } + parser.states = append(parser.states, yaml_PARSE_DOCUMENT_END_STATE) + parser.state = yaml_PARSE_DOCUMENT_CONTENT_STATE + end_mark := token.end_mark + + *event = yaml_event_t{ + typ: yaml_DOCUMENT_START_EVENT, + start_mark: start_mark, + end_mark: end_mark, + version_directive: version_directive, + tag_directives: tag_directives, + implicit: false, + } + skip_token(parser) + + } else { + // Parse the stream end. + parser.state = yaml_PARSE_END_STATE + *event = yaml_event_t{ + typ: yaml_STREAM_END_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + } + skip_token(parser) + } + + return true +} + +// Parse the productions: +// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* +// *********** +// +func yaml_parser_parse_document_content(parser *yaml_parser_t, event *yaml_event_t) bool { + token := peek_token(parser) + if token == nil { + return false + } + + if token.typ == yaml_VERSION_DIRECTIVE_TOKEN || + token.typ == yaml_TAG_DIRECTIVE_TOKEN || + token.typ == yaml_DOCUMENT_START_TOKEN || + token.typ == yaml_DOCUMENT_END_TOKEN || + token.typ == yaml_STREAM_END_TOKEN { + parser.state = parser.states[len(parser.states)-1] + parser.states = parser.states[:len(parser.states)-1] + return yaml_parser_process_empty_scalar(parser, event, + token.start_mark) + } + return yaml_parser_parse_node(parser, event, true, false) +} + +// Parse the productions: +// implicit_document ::= block_node DOCUMENT-END* +// ************* +// explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* +// +func yaml_parser_parse_document_end(parser *yaml_parser_t, event *yaml_event_t) bool { + token := peek_token(parser) + if token == nil { + return false + } + + start_mark := token.start_mark + end_mark := token.start_mark + + implicit := true + if token.typ == yaml_DOCUMENT_END_TOKEN { + end_mark = token.end_mark + skip_token(parser) + implicit = false + } + + parser.tag_directives = parser.tag_directives[:0] + + parser.state = yaml_PARSE_DOCUMENT_START_STATE + *event = yaml_event_t{ + typ: yaml_DOCUMENT_END_EVENT, + start_mark: start_mark, + end_mark: end_mark, + implicit: implicit, + } + yaml_parser_set_event_comments(parser, event) + if len(event.head_comment) > 0 && len(event.foot_comment) == 0 { + event.foot_comment = event.head_comment + event.head_comment = nil + } + return true +} + +func yaml_parser_set_event_comments(parser *yaml_parser_t, event *yaml_event_t) { + event.head_comment = parser.head_comment + event.line_comment = parser.line_comment + event.foot_comment = parser.foot_comment + parser.head_comment = nil + parser.line_comment = nil + parser.foot_comment = nil + parser.tail_comment = nil + parser.stem_comment = nil +} + +// Parse the productions: +// block_node_or_indentless_sequence ::= +// ALIAS +// ***** +// | properties (block_content | indentless_block_sequence)? +// ********** * +// | block_content | indentless_block_sequence +// * +// block_node ::= ALIAS +// ***** +// | properties block_content? +// ********** * +// | block_content +// * +// flow_node ::= ALIAS +// ***** +// | properties flow_content? +// ********** * +// | flow_content +// * +// properties ::= TAG ANCHOR? | ANCHOR TAG? +// ************************* +// block_content ::= block_collection | flow_collection | SCALAR +// ****** +// flow_content ::= flow_collection | SCALAR +// ****** +func yaml_parser_parse_node(parser *yaml_parser_t, event *yaml_event_t, block, indentless_sequence bool) bool { + //defer trace("yaml_parser_parse_node", "block:", block, "indentless_sequence:", indentless_sequence)() + + token := peek_token(parser) + if token == nil { + return false + } + + if token.typ == yaml_ALIAS_TOKEN { + parser.state = parser.states[len(parser.states)-1] + parser.states = parser.states[:len(parser.states)-1] + *event = yaml_event_t{ + typ: yaml_ALIAS_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + anchor: token.value, + } + yaml_parser_set_event_comments(parser, event) + skip_token(parser) + return true + } + + start_mark := token.start_mark + end_mark := token.start_mark + + var tag_token bool + var tag_handle, tag_suffix, anchor []byte + var tag_mark yaml_mark_t + if token.typ == yaml_ANCHOR_TOKEN { + anchor = token.value + start_mark = token.start_mark + end_mark = token.end_mark + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + if token.typ == yaml_TAG_TOKEN { + tag_token = true + tag_handle = token.value + tag_suffix = token.suffix + tag_mark = token.start_mark + end_mark = token.end_mark + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + } + } else if token.typ == yaml_TAG_TOKEN { + tag_token = true + tag_handle = token.value + tag_suffix = token.suffix + start_mark = token.start_mark + tag_mark = token.start_mark + end_mark = token.end_mark + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + if token.typ == yaml_ANCHOR_TOKEN { + anchor = token.value + end_mark = token.end_mark + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + } + } + + var tag []byte + if tag_token { + if len(tag_handle) == 0 { + tag = tag_suffix + tag_suffix = nil + } else { + for i := range parser.tag_directives { + if bytes.Equal(parser.tag_directives[i].handle, tag_handle) { + tag = append([]byte(nil), parser.tag_directives[i].prefix...) + tag = append(tag, tag_suffix...) + break + } + } + if len(tag) == 0 { + yaml_parser_set_parser_error_context(parser, + "while parsing a node", start_mark, + "found undefined tag handle", tag_mark) + return false + } + } + } + + implicit := len(tag) == 0 + if indentless_sequence && token.typ == yaml_BLOCK_ENTRY_TOKEN { + end_mark = token.end_mark + parser.state = yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE + *event = yaml_event_t{ + typ: yaml_SEQUENCE_START_EVENT, + start_mark: start_mark, + end_mark: end_mark, + anchor: anchor, + tag: tag, + implicit: implicit, + style: yaml_style_t(yaml_BLOCK_SEQUENCE_STYLE), + } + return true + } + if token.typ == yaml_SCALAR_TOKEN { + var plain_implicit, quoted_implicit bool + end_mark = token.end_mark + if (len(tag) == 0 && token.style == yaml_PLAIN_SCALAR_STYLE) || (len(tag) == 1 && tag[0] == '!') { + plain_implicit = true + } else if len(tag) == 0 { + quoted_implicit = true + } + parser.state = parser.states[len(parser.states)-1] + parser.states = parser.states[:len(parser.states)-1] + + *event = yaml_event_t{ + typ: yaml_SCALAR_EVENT, + start_mark: start_mark, + end_mark: end_mark, + anchor: anchor, + tag: tag, + value: token.value, + implicit: plain_implicit, + quoted_implicit: quoted_implicit, + style: yaml_style_t(token.style), + } + yaml_parser_set_event_comments(parser, event) + skip_token(parser) + return true + } + if token.typ == yaml_FLOW_SEQUENCE_START_TOKEN { + // [Go] Some of the events below can be merged as they differ only on style. + end_mark = token.end_mark + parser.state = yaml_PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE + *event = yaml_event_t{ + typ: yaml_SEQUENCE_START_EVENT, + start_mark: start_mark, + end_mark: end_mark, + anchor: anchor, + tag: tag, + implicit: implicit, + style: yaml_style_t(yaml_FLOW_SEQUENCE_STYLE), + } + yaml_parser_set_event_comments(parser, event) + return true + } + if token.typ == yaml_FLOW_MAPPING_START_TOKEN { + end_mark = token.end_mark + parser.state = yaml_PARSE_FLOW_MAPPING_FIRST_KEY_STATE + *event = yaml_event_t{ + typ: yaml_MAPPING_START_EVENT, + start_mark: start_mark, + end_mark: end_mark, + anchor: anchor, + tag: tag, + implicit: implicit, + style: yaml_style_t(yaml_FLOW_MAPPING_STYLE), + } + yaml_parser_set_event_comments(parser, event) + return true + } + if block && token.typ == yaml_BLOCK_SEQUENCE_START_TOKEN { + end_mark = token.end_mark + parser.state = yaml_PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE + *event = yaml_event_t{ + typ: yaml_SEQUENCE_START_EVENT, + start_mark: start_mark, + end_mark: end_mark, + anchor: anchor, + tag: tag, + implicit: implicit, + style: yaml_style_t(yaml_BLOCK_SEQUENCE_STYLE), + } + if parser.stem_comment != nil { + event.head_comment = parser.stem_comment + parser.stem_comment = nil + } + return true + } + if block && token.typ == yaml_BLOCK_MAPPING_START_TOKEN { + end_mark = token.end_mark + parser.state = yaml_PARSE_BLOCK_MAPPING_FIRST_KEY_STATE + *event = yaml_event_t{ + typ: yaml_MAPPING_START_EVENT, + start_mark: start_mark, + end_mark: end_mark, + anchor: anchor, + tag: tag, + implicit: implicit, + style: yaml_style_t(yaml_BLOCK_MAPPING_STYLE), + } + if parser.stem_comment != nil { + event.head_comment = parser.stem_comment + parser.stem_comment = nil + } + return true + } + if len(anchor) > 0 || len(tag) > 0 { + parser.state = parser.states[len(parser.states)-1] + parser.states = parser.states[:len(parser.states)-1] + + *event = yaml_event_t{ + typ: yaml_SCALAR_EVENT, + start_mark: start_mark, + end_mark: end_mark, + anchor: anchor, + tag: tag, + implicit: implicit, + quoted_implicit: false, + style: yaml_style_t(yaml_PLAIN_SCALAR_STYLE), + } + return true + } + + context := "while parsing a flow node" + if block { + context = "while parsing a block node" + } + yaml_parser_set_parser_error_context(parser, context, start_mark, + "did not find expected node content", token.start_mark) + return false +} + +// Parse the productions: +// block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END +// ******************** *********** * ********* +// +func yaml_parser_parse_block_sequence_entry(parser *yaml_parser_t, event *yaml_event_t, first bool) bool { + if first { + token := peek_token(parser) + if token == nil { + return false + } + parser.marks = append(parser.marks, token.start_mark) + skip_token(parser) + } + + token := peek_token(parser) + if token == nil { + return false + } + + if token.typ == yaml_BLOCK_ENTRY_TOKEN { + mark := token.end_mark + prior_head_len := len(parser.head_comment) + skip_token(parser) + yaml_parser_split_stem_comment(parser, prior_head_len) + token = peek_token(parser) + if token == nil { + return false + } + if token.typ != yaml_BLOCK_ENTRY_TOKEN && token.typ != yaml_BLOCK_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_BLOCK_SEQUENCE_ENTRY_STATE) + return yaml_parser_parse_node(parser, event, true, false) + } else { + parser.state = yaml_PARSE_BLOCK_SEQUENCE_ENTRY_STATE + return yaml_parser_process_empty_scalar(parser, event, mark) + } + } + if token.typ == yaml_BLOCK_END_TOKEN { + parser.state = parser.states[len(parser.states)-1] + parser.states = parser.states[:len(parser.states)-1] + parser.marks = parser.marks[:len(parser.marks)-1] + + *event = yaml_event_t{ + typ: yaml_SEQUENCE_END_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + } + + skip_token(parser) + return true + } + + context_mark := parser.marks[len(parser.marks)-1] + parser.marks = parser.marks[:len(parser.marks)-1] + return yaml_parser_set_parser_error_context(parser, + "while parsing a block collection", context_mark, + "did not find expected '-' indicator", token.start_mark) +} + +// Parse the productions: +// indentless_sequence ::= (BLOCK-ENTRY block_node?)+ +// *********** * +func yaml_parser_parse_indentless_sequence_entry(parser *yaml_parser_t, event *yaml_event_t) bool { + token := peek_token(parser) + if token == nil { + return false + } + + if token.typ == yaml_BLOCK_ENTRY_TOKEN { + mark := token.end_mark + prior_head_len := len(parser.head_comment) + skip_token(parser) + yaml_parser_split_stem_comment(parser, prior_head_len) + token = peek_token(parser) + if token == nil { + return false + } + if token.typ != yaml_BLOCK_ENTRY_TOKEN && + token.typ != yaml_KEY_TOKEN && + token.typ != yaml_VALUE_TOKEN && + token.typ != yaml_BLOCK_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE) + return yaml_parser_parse_node(parser, event, true, false) + } + parser.state = yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE + return yaml_parser_process_empty_scalar(parser, event, mark) + } + parser.state = parser.states[len(parser.states)-1] + parser.states = parser.states[:len(parser.states)-1] + + *event = yaml_event_t{ + typ: yaml_SEQUENCE_END_EVENT, + start_mark: token.start_mark, + end_mark: token.start_mark, // [Go] Shouldn't this be token.end_mark? + } + return true +} + +// Split stem comment from head comment. +// +// When a sequence or map is found under a sequence entry, the former head comment +// is assigned to the underlying sequence or map as a whole, not the individual +// sequence or map entry as would be expected otherwise. To handle this case the +// previous head comment is moved aside as the stem comment. +func yaml_parser_split_stem_comment(parser *yaml_parser_t, stem_len int) { + if stem_len == 0 { + return + } + + token := peek_token(parser) + if token == nil || token.typ != yaml_BLOCK_SEQUENCE_START_TOKEN && token.typ != yaml_BLOCK_MAPPING_START_TOKEN { + return + } + + parser.stem_comment = parser.head_comment[:stem_len] + if len(parser.head_comment) == stem_len { + parser.head_comment = nil + } else { + // Copy suffix to prevent very strange bugs if someone ever appends + // further bytes to the prefix in the stem_comment slice above. + parser.head_comment = append([]byte(nil), parser.head_comment[stem_len+1:]...) + } +} + +// Parse the productions: +// block_mapping ::= BLOCK-MAPPING_START +// ******************* +// ((KEY block_node_or_indentless_sequence?)? +// *** * +// (VALUE block_node_or_indentless_sequence?)?)* +// +// BLOCK-END +// ********* +// +func yaml_parser_parse_block_mapping_key(parser *yaml_parser_t, event *yaml_event_t, first bool) bool { + if first { + token := peek_token(parser) + if token == nil { + return false + } + parser.marks = append(parser.marks, token.start_mark) + skip_token(parser) + } + + token := peek_token(parser) + if token == nil { + return false + } + + // [Go] A tail comment was left from the prior mapping value processed. Emit an event + // as it needs to be processed with that value and not the following key. + if len(parser.tail_comment) > 0 { + *event = yaml_event_t{ + typ: yaml_TAIL_COMMENT_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + foot_comment: parser.tail_comment, + } + parser.tail_comment = nil + return true + } + + if token.typ == yaml_KEY_TOKEN { + mark := token.end_mark + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + if token.typ != yaml_KEY_TOKEN && + token.typ != yaml_VALUE_TOKEN && + token.typ != yaml_BLOCK_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_BLOCK_MAPPING_VALUE_STATE) + return yaml_parser_parse_node(parser, event, true, true) + } else { + parser.state = yaml_PARSE_BLOCK_MAPPING_VALUE_STATE + return yaml_parser_process_empty_scalar(parser, event, mark) + } + } else if token.typ == yaml_BLOCK_END_TOKEN { + parser.state = parser.states[len(parser.states)-1] + parser.states = parser.states[:len(parser.states)-1] + parser.marks = parser.marks[:len(parser.marks)-1] + *event = yaml_event_t{ + typ: yaml_MAPPING_END_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + } + yaml_parser_set_event_comments(parser, event) + skip_token(parser) + return true + } + + context_mark := parser.marks[len(parser.marks)-1] + parser.marks = parser.marks[:len(parser.marks)-1] + return yaml_parser_set_parser_error_context(parser, + "while parsing a block mapping", context_mark, + "did not find expected key", token.start_mark) +} + +// Parse the productions: +// block_mapping ::= BLOCK-MAPPING_START +// +// ((KEY block_node_or_indentless_sequence?)? +// +// (VALUE block_node_or_indentless_sequence?)?)* +// ***** * +// BLOCK-END +// +// +func yaml_parser_parse_block_mapping_value(parser *yaml_parser_t, event *yaml_event_t) bool { + token := peek_token(parser) + if token == nil { + return false + } + if token.typ == yaml_VALUE_TOKEN { + mark := token.end_mark + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + if token.typ != yaml_KEY_TOKEN && + token.typ != yaml_VALUE_TOKEN && + token.typ != yaml_BLOCK_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_BLOCK_MAPPING_KEY_STATE) + return yaml_parser_parse_node(parser, event, true, true) + } + parser.state = yaml_PARSE_BLOCK_MAPPING_KEY_STATE + return yaml_parser_process_empty_scalar(parser, event, mark) + } + parser.state = yaml_PARSE_BLOCK_MAPPING_KEY_STATE + return yaml_parser_process_empty_scalar(parser, event, token.start_mark) +} + +// Parse the productions: +// flow_sequence ::= FLOW-SEQUENCE-START +// ******************* +// (flow_sequence_entry FLOW-ENTRY)* +// * ********** +// flow_sequence_entry? +// * +// FLOW-SEQUENCE-END +// ***************** +// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +// * +// +func yaml_parser_parse_flow_sequence_entry(parser *yaml_parser_t, event *yaml_event_t, first bool) bool { + if first { + token := peek_token(parser) + if token == nil { + return false + } + parser.marks = append(parser.marks, token.start_mark) + skip_token(parser) + } + token := peek_token(parser) + if token == nil { + return false + } + if token.typ != yaml_FLOW_SEQUENCE_END_TOKEN { + if !first { + if token.typ == yaml_FLOW_ENTRY_TOKEN { + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + } else { + context_mark := parser.marks[len(parser.marks)-1] + parser.marks = parser.marks[:len(parser.marks)-1] + return yaml_parser_set_parser_error_context(parser, + "while parsing a flow sequence", context_mark, + "did not find expected ',' or ']'", token.start_mark) + } + } + + if token.typ == yaml_KEY_TOKEN { + parser.state = yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE + *event = yaml_event_t{ + typ: yaml_MAPPING_START_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + implicit: true, + style: yaml_style_t(yaml_FLOW_MAPPING_STYLE), + } + skip_token(parser) + return true + } else if token.typ != yaml_FLOW_SEQUENCE_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_FLOW_SEQUENCE_ENTRY_STATE) + return yaml_parser_parse_node(parser, event, false, false) + } + } + + parser.state = parser.states[len(parser.states)-1] + parser.states = parser.states[:len(parser.states)-1] + parser.marks = parser.marks[:len(parser.marks)-1] + + *event = yaml_event_t{ + typ: yaml_SEQUENCE_END_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + } + yaml_parser_set_event_comments(parser, event) + + skip_token(parser) + return true +} + +// +// Parse the productions: +// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +// *** * +// +func yaml_parser_parse_flow_sequence_entry_mapping_key(parser *yaml_parser_t, event *yaml_event_t) bool { + token := peek_token(parser) + if token == nil { + return false + } + if token.typ != yaml_VALUE_TOKEN && + token.typ != yaml_FLOW_ENTRY_TOKEN && + token.typ != yaml_FLOW_SEQUENCE_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE) + return yaml_parser_parse_node(parser, event, false, false) + } + mark := token.end_mark + skip_token(parser) + parser.state = yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE + return yaml_parser_process_empty_scalar(parser, event, mark) +} + +// Parse the productions: +// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +// ***** * +// +func yaml_parser_parse_flow_sequence_entry_mapping_value(parser *yaml_parser_t, event *yaml_event_t) bool { + token := peek_token(parser) + if token == nil { + return false + } + if token.typ == yaml_VALUE_TOKEN { + skip_token(parser) + token := peek_token(parser) + if token == nil { + return false + } + if token.typ != yaml_FLOW_ENTRY_TOKEN && token.typ != yaml_FLOW_SEQUENCE_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE) + return yaml_parser_parse_node(parser, event, false, false) + } + } + parser.state = yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE + return yaml_parser_process_empty_scalar(parser, event, token.start_mark) +} + +// Parse the productions: +// flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +// * +// +func yaml_parser_parse_flow_sequence_entry_mapping_end(parser *yaml_parser_t, event *yaml_event_t) bool { + token := peek_token(parser) + if token == nil { + return false + } + parser.state = yaml_PARSE_FLOW_SEQUENCE_ENTRY_STATE + *event = yaml_event_t{ + typ: yaml_MAPPING_END_EVENT, + start_mark: token.start_mark, + end_mark: token.start_mark, // [Go] Shouldn't this be end_mark? + } + return true +} + +// Parse the productions: +// flow_mapping ::= FLOW-MAPPING-START +// ****************** +// (flow_mapping_entry FLOW-ENTRY)* +// * ********** +// flow_mapping_entry? +// ****************** +// FLOW-MAPPING-END +// **************** +// flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +// * *** * +// +func yaml_parser_parse_flow_mapping_key(parser *yaml_parser_t, event *yaml_event_t, first bool) bool { + if first { + token := peek_token(parser) + parser.marks = append(parser.marks, token.start_mark) + skip_token(parser) + } + + token := peek_token(parser) + if token == nil { + return false + } + + if token.typ != yaml_FLOW_MAPPING_END_TOKEN { + if !first { + if token.typ == yaml_FLOW_ENTRY_TOKEN { + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + } else { + context_mark := parser.marks[len(parser.marks)-1] + parser.marks = parser.marks[:len(parser.marks)-1] + return yaml_parser_set_parser_error_context(parser, + "while parsing a flow mapping", context_mark, + "did not find expected ',' or '}'", token.start_mark) + } + } + + if token.typ == yaml_KEY_TOKEN { + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + if token.typ != yaml_VALUE_TOKEN && + token.typ != yaml_FLOW_ENTRY_TOKEN && + token.typ != yaml_FLOW_MAPPING_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_FLOW_MAPPING_VALUE_STATE) + return yaml_parser_parse_node(parser, event, false, false) + } else { + parser.state = yaml_PARSE_FLOW_MAPPING_VALUE_STATE + return yaml_parser_process_empty_scalar(parser, event, token.start_mark) + } + } else if token.typ != yaml_FLOW_MAPPING_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE) + return yaml_parser_parse_node(parser, event, false, false) + } + } + + parser.state = parser.states[len(parser.states)-1] + parser.states = parser.states[:len(parser.states)-1] + parser.marks = parser.marks[:len(parser.marks)-1] + *event = yaml_event_t{ + typ: yaml_MAPPING_END_EVENT, + start_mark: token.start_mark, + end_mark: token.end_mark, + } + yaml_parser_set_event_comments(parser, event) + skip_token(parser) + return true +} + +// Parse the productions: +// flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +// * ***** * +// +func yaml_parser_parse_flow_mapping_value(parser *yaml_parser_t, event *yaml_event_t, empty bool) bool { + token := peek_token(parser) + if token == nil { + return false + } + if empty { + parser.state = yaml_PARSE_FLOW_MAPPING_KEY_STATE + return yaml_parser_process_empty_scalar(parser, event, token.start_mark) + } + if token.typ == yaml_VALUE_TOKEN { + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + if token.typ != yaml_FLOW_ENTRY_TOKEN && token.typ != yaml_FLOW_MAPPING_END_TOKEN { + parser.states = append(parser.states, yaml_PARSE_FLOW_MAPPING_KEY_STATE) + return yaml_parser_parse_node(parser, event, false, false) + } + } + parser.state = yaml_PARSE_FLOW_MAPPING_KEY_STATE + return yaml_parser_process_empty_scalar(parser, event, token.start_mark) +} + +// Generate an empty scalar event. +func yaml_parser_process_empty_scalar(parser *yaml_parser_t, event *yaml_event_t, mark yaml_mark_t) bool { + *event = yaml_event_t{ + typ: yaml_SCALAR_EVENT, + start_mark: mark, + end_mark: mark, + value: nil, // Empty + implicit: true, + style: yaml_style_t(yaml_PLAIN_SCALAR_STYLE), + } + return true +} + +var default_tag_directives = []yaml_tag_directive_t{ + {[]byte("!"), []byte("!")}, + {[]byte("!!"), []byte("tag:yaml.org,2002:")}, +} + +// Parse directives. +func yaml_parser_process_directives(parser *yaml_parser_t, + version_directive_ref **yaml_version_directive_t, + tag_directives_ref *[]yaml_tag_directive_t) bool { + + var version_directive *yaml_version_directive_t + var tag_directives []yaml_tag_directive_t + + token := peek_token(parser) + if token == nil { + return false + } + + for token.typ == yaml_VERSION_DIRECTIVE_TOKEN || token.typ == yaml_TAG_DIRECTIVE_TOKEN { + if token.typ == yaml_VERSION_DIRECTIVE_TOKEN { + if version_directive != nil { + yaml_parser_set_parser_error(parser, + "found duplicate %YAML directive", token.start_mark) + return false + } + if token.major != 1 || token.minor != 1 { + yaml_parser_set_parser_error(parser, + "found incompatible YAML document", token.start_mark) + return false + } + version_directive = &yaml_version_directive_t{ + major: token.major, + minor: token.minor, + } + } else if token.typ == yaml_TAG_DIRECTIVE_TOKEN { + value := yaml_tag_directive_t{ + handle: token.value, + prefix: token.prefix, + } + if !yaml_parser_append_tag_directive(parser, value, false, token.start_mark) { + return false + } + tag_directives = append(tag_directives, value) + } + + skip_token(parser) + token = peek_token(parser) + if token == nil { + return false + } + } + + for i := range default_tag_directives { + if !yaml_parser_append_tag_directive(parser, default_tag_directives[i], true, token.start_mark) { + return false + } + } + + if version_directive_ref != nil { + *version_directive_ref = version_directive + } + if tag_directives_ref != nil { + *tag_directives_ref = tag_directives + } + return true +} + +// Append a tag directive to the directives stack. +func yaml_parser_append_tag_directive(parser *yaml_parser_t, value yaml_tag_directive_t, allow_duplicates bool, mark yaml_mark_t) bool { + for i := range parser.tag_directives { + if bytes.Equal(value.handle, parser.tag_directives[i].handle) { + if allow_duplicates { + return true + } + return yaml_parser_set_parser_error(parser, "found duplicate %TAG directive", mark) + } + } + + // [Go] I suspect the copy is unnecessary. This was likely done + // because there was no way to track ownership of the data. + value_copy := yaml_tag_directive_t{ + handle: make([]byte, len(value.handle)), + prefix: make([]byte, len(value.prefix)), + } + copy(value_copy.handle, value.handle) + copy(value_copy.prefix, value.prefix) + parser.tag_directives = append(parser.tag_directives, value_copy) + return true +} diff --git a/asyncapi/vendor/gopkg.in/yaml.v3/readerc.go b/asyncapi/vendor/gopkg.in/yaml.v3/readerc.go new file mode 100644 index 0000000..b7de0a8 --- /dev/null +++ b/asyncapi/vendor/gopkg.in/yaml.v3/readerc.go @@ -0,0 +1,434 @@ +// +// Copyright (c) 2011-2019 Canonical Ltd +// Copyright (c) 2006-2010 Kirill Simonov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package yaml + +import ( + "io" +) + +// Set the reader error and return 0. +func yaml_parser_set_reader_error(parser *yaml_parser_t, problem string, offset int, value int) bool { + parser.error = yaml_READER_ERROR + parser.problem = problem + parser.problem_offset = offset + parser.problem_value = value + return false +} + +// Byte order marks. +const ( + bom_UTF8 = "\xef\xbb\xbf" + bom_UTF16LE = "\xff\xfe" + bom_UTF16BE = "\xfe\xff" +) + +// Determine the input stream encoding by checking the BOM symbol. If no BOM is +// found, the UTF-8 encoding is assumed. Return 1 on success, 0 on failure. +func yaml_parser_determine_encoding(parser *yaml_parser_t) bool { + // Ensure that we had enough bytes in the raw buffer. + for !parser.eof && len(parser.raw_buffer)-parser.raw_buffer_pos < 3 { + if !yaml_parser_update_raw_buffer(parser) { + return false + } + } + + // Determine the encoding. + buf := parser.raw_buffer + pos := parser.raw_buffer_pos + avail := len(buf) - pos + if avail >= 2 && buf[pos] == bom_UTF16LE[0] && buf[pos+1] == bom_UTF16LE[1] { + parser.encoding = yaml_UTF16LE_ENCODING + parser.raw_buffer_pos += 2 + parser.offset += 2 + } else if avail >= 2 && buf[pos] == bom_UTF16BE[0] && buf[pos+1] == bom_UTF16BE[1] { + parser.encoding = yaml_UTF16BE_ENCODING + parser.raw_buffer_pos += 2 + parser.offset += 2 + } else if avail >= 3 && buf[pos] == bom_UTF8[0] && buf[pos+1] == bom_UTF8[1] && buf[pos+2] == bom_UTF8[2] { + parser.encoding = yaml_UTF8_ENCODING + parser.raw_buffer_pos += 3 + parser.offset += 3 + } else { + parser.encoding = yaml_UTF8_ENCODING + } + return true +} + +// Update the raw buffer. +func yaml_parser_update_raw_buffer(parser *yaml_parser_t) bool { + size_read := 0 + + // Return if the raw buffer is full. + if parser.raw_buffer_pos == 0 && len(parser.raw_buffer) == cap(parser.raw_buffer) { + return true + } + + // Return on EOF. + if parser.eof { + return true + } + + // Move the remaining bytes in the raw buffer to the beginning. + if parser.raw_buffer_pos > 0 && parser.raw_buffer_pos < len(parser.raw_buffer) { + copy(parser.raw_buffer, parser.raw_buffer[parser.raw_buffer_pos:]) + } + parser.raw_buffer = parser.raw_buffer[:len(parser.raw_buffer)-parser.raw_buffer_pos] + parser.raw_buffer_pos = 0 + + // Call the read handler to fill the buffer. + size_read, err := parser.read_handler(parser, parser.raw_buffer[len(parser.raw_buffer):cap(parser.raw_buffer)]) + parser.raw_buffer = parser.raw_buffer[:len(parser.raw_buffer)+size_read] + if err == io.EOF { + parser.eof = true + } else if err != nil { + return yaml_parser_set_reader_error(parser, "input error: "+err.Error(), parser.offset, -1) + } + return true +} + +// Ensure that the buffer contains at least `length` characters. +// Return true on success, false on failure. +// +// The length is supposed to be significantly less that the buffer size. +func yaml_parser_update_buffer(parser *yaml_parser_t, length int) bool { + if parser.read_handler == nil { + panic("read handler must be set") + } + + // [Go] This function was changed to guarantee the requested length size at EOF. + // The fact we need to do this is pretty awful, but the description above implies + // for that to be the case, and there are tests + + // If the EOF flag is set and the raw buffer is empty, do nothing. + if parser.eof && parser.raw_buffer_pos == len(parser.raw_buffer) { + // [Go] ACTUALLY! Read the documentation of this function above. + // This is just broken. To return true, we need to have the + // given length in the buffer. Not doing that means every single + // check that calls this function to make sure the buffer has a + // given length is Go) panicking; or C) accessing invalid memory. + //return true + } + + // Return if the buffer contains enough characters. + if parser.unread >= length { + return true + } + + // Determine the input encoding if it is not known yet. + if parser.encoding == yaml_ANY_ENCODING { + if !yaml_parser_determine_encoding(parser) { + return false + } + } + + // Move the unread characters to the beginning of the buffer. + buffer_len := len(parser.buffer) + if parser.buffer_pos > 0 && parser.buffer_pos < buffer_len { + copy(parser.buffer, parser.buffer[parser.buffer_pos:]) + buffer_len -= parser.buffer_pos + parser.buffer_pos = 0 + } else if parser.buffer_pos == buffer_len { + buffer_len = 0 + parser.buffer_pos = 0 + } + + // Open the whole buffer for writing, and cut it before returning. + parser.buffer = parser.buffer[:cap(parser.buffer)] + + // Fill the buffer until it has enough characters. + first := true + for parser.unread < length { + + // Fill the raw buffer if necessary. + if !first || parser.raw_buffer_pos == len(parser.raw_buffer) { + if !yaml_parser_update_raw_buffer(parser) { + parser.buffer = parser.buffer[:buffer_len] + return false + } + } + first = false + + // Decode the raw buffer. + inner: + for parser.raw_buffer_pos != len(parser.raw_buffer) { + var value rune + var width int + + raw_unread := len(parser.raw_buffer) - parser.raw_buffer_pos + + // Decode the next character. + switch parser.encoding { + case yaml_UTF8_ENCODING: + // Decode a UTF-8 character. Check RFC 3629 + // (http://www.ietf.org/rfc/rfc3629.txt) for more details. + // + // The following table (taken from the RFC) is used for + // decoding. + // + // Char. number range | UTF-8 octet sequence + // (hexadecimal) | (binary) + // --------------------+------------------------------------ + // 0000 0000-0000 007F | 0xxxxxxx + // 0000 0080-0000 07FF | 110xxxxx 10xxxxxx + // 0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx + // 0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + // + // Additionally, the characters in the range 0xD800-0xDFFF + // are prohibited as they are reserved for use with UTF-16 + // surrogate pairs. + + // Determine the length of the UTF-8 sequence. + octet := parser.raw_buffer[parser.raw_buffer_pos] + switch { + case octet&0x80 == 0x00: + width = 1 + case octet&0xE0 == 0xC0: + width = 2 + case octet&0xF0 == 0xE0: + width = 3 + case octet&0xF8 == 0xF0: + width = 4 + default: + // The leading octet is invalid. + return yaml_parser_set_reader_error(parser, + "invalid leading UTF-8 octet", + parser.offset, int(octet)) + } + + // Check if the raw buffer contains an incomplete character. + if width > raw_unread { + if parser.eof { + return yaml_parser_set_reader_error(parser, + "incomplete UTF-8 octet sequence", + parser.offset, -1) + } + break inner + } + + // Decode the leading octet. + switch { + case octet&0x80 == 0x00: + value = rune(octet & 0x7F) + case octet&0xE0 == 0xC0: + value = rune(octet & 0x1F) + case octet&0xF0 == 0xE0: + value = rune(octet & 0x0F) + case octet&0xF8 == 0xF0: + value = rune(octet & 0x07) + default: + value = 0 + } + + // Check and decode the trailing octets. + for k := 1; k < width; k++ { + octet = parser.raw_buffer[parser.raw_buffer_pos+k] + + // Check if the octet is valid. + if (octet & 0xC0) != 0x80 { + return yaml_parser_set_reader_error(parser, + "invalid trailing UTF-8 octet", + parser.offset+k, int(octet)) + } + + // Decode the octet. + value = (value << 6) + rune(octet&0x3F) + } + + // Check the length of the sequence against the value. + switch { + case width == 1: + case width == 2 && value >= 0x80: + case width == 3 && value >= 0x800: + case width == 4 && value >= 0x10000: + default: + return yaml_parser_set_reader_error(parser, + "invalid length of a UTF-8 sequence", + parser.offset, -1) + } + + // Check the range of the value. + if value >= 0xD800 && value <= 0xDFFF || value > 0x10FFFF { + return yaml_parser_set_reader_error(parser, + "invalid Unicode character", + parser.offset, int(value)) + } + + case yaml_UTF16LE_ENCODING, yaml_UTF16BE_ENCODING: + var low, high int + if parser.encoding == yaml_UTF16LE_ENCODING { + low, high = 0, 1 + } else { + low, high = 1, 0 + } + + // The UTF-16 encoding is not as simple as one might + // naively think. Check RFC 2781 + // (http://www.ietf.org/rfc/rfc2781.txt). + // + // Normally, two subsequent bytes describe a Unicode + // character. However a special technique (called a + // surrogate pair) is used for specifying character + // values larger than 0xFFFF. + // + // A surrogate pair consists of two pseudo-characters: + // high surrogate area (0xD800-0xDBFF) + // low surrogate area (0xDC00-0xDFFF) + // + // The following formulas are used for decoding + // and encoding characters using surrogate pairs: + // + // U = U' + 0x10000 (0x01 00 00 <= U <= 0x10 FF FF) + // U' = yyyyyyyyyyxxxxxxxxxx (0 <= U' <= 0x0F FF FF) + // W1 = 110110yyyyyyyyyy + // W2 = 110111xxxxxxxxxx + // + // where U is the character value, W1 is the high surrogate + // area, W2 is the low surrogate area. + + // Check for incomplete UTF-16 character. + if raw_unread < 2 { + if parser.eof { + return yaml_parser_set_reader_error(parser, + "incomplete UTF-16 character", + parser.offset, -1) + } + break inner + } + + // Get the character. + value = rune(parser.raw_buffer[parser.raw_buffer_pos+low]) + + (rune(parser.raw_buffer[parser.raw_buffer_pos+high]) << 8) + + // Check for unexpected low surrogate area. + if value&0xFC00 == 0xDC00 { + return yaml_parser_set_reader_error(parser, + "unexpected low surrogate area", + parser.offset, int(value)) + } + + // Check for a high surrogate area. + if value&0xFC00 == 0xD800 { + width = 4 + + // Check for incomplete surrogate pair. + if raw_unread < 4 { + if parser.eof { + return yaml_parser_set_reader_error(parser, + "incomplete UTF-16 surrogate pair", + parser.offset, -1) + } + break inner + } + + // Get the next character. + value2 := rune(parser.raw_buffer[parser.raw_buffer_pos+low+2]) + + (rune(parser.raw_buffer[parser.raw_buffer_pos+high+2]) << 8) + + // Check for a low surrogate area. + if value2&0xFC00 != 0xDC00 { + return yaml_parser_set_reader_error(parser, + "expected low surrogate area", + parser.offset+2, int(value2)) + } + + // Generate the value of the surrogate pair. + value = 0x10000 + ((value & 0x3FF) << 10) + (value2 & 0x3FF) + } else { + width = 2 + } + + default: + panic("impossible") + } + + // Check if the character is in the allowed range: + // #x9 | #xA | #xD | [#x20-#x7E] (8 bit) + // | #x85 | [#xA0-#xD7FF] | [#xE000-#xFFFD] (16 bit) + // | [#x10000-#x10FFFF] (32 bit) + switch { + case value == 0x09: + case value == 0x0A: + case value == 0x0D: + case value >= 0x20 && value <= 0x7E: + case value == 0x85: + case value >= 0xA0 && value <= 0xD7FF: + case value >= 0xE000 && value <= 0xFFFD: + case value >= 0x10000 && value <= 0x10FFFF: + default: + return yaml_parser_set_reader_error(parser, + "control characters are not allowed", + parser.offset, int(value)) + } + + // Move the raw pointers. + parser.raw_buffer_pos += width + parser.offset += width + + // Finally put the character into the buffer. + if value <= 0x7F { + // 0000 0000-0000 007F . 0xxxxxxx + parser.buffer[buffer_len+0] = byte(value) + buffer_len += 1 + } else if value <= 0x7FF { + // 0000 0080-0000 07FF . 110xxxxx 10xxxxxx + parser.buffer[buffer_len+0] = byte(0xC0 + (value >> 6)) + parser.buffer[buffer_len+1] = byte(0x80 + (value & 0x3F)) + buffer_len += 2 + } else if value <= 0xFFFF { + // 0000 0800-0000 FFFF . 1110xxxx 10xxxxxx 10xxxxxx + parser.buffer[buffer_len+0] = byte(0xE0 + (value >> 12)) + parser.buffer[buffer_len+1] = byte(0x80 + ((value >> 6) & 0x3F)) + parser.buffer[buffer_len+2] = byte(0x80 + (value & 0x3F)) + buffer_len += 3 + } else { + // 0001 0000-0010 FFFF . 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + parser.buffer[buffer_len+0] = byte(0xF0 + (value >> 18)) + parser.buffer[buffer_len+1] = byte(0x80 + ((value >> 12) & 0x3F)) + parser.buffer[buffer_len+2] = byte(0x80 + ((value >> 6) & 0x3F)) + parser.buffer[buffer_len+3] = byte(0x80 + (value & 0x3F)) + buffer_len += 4 + } + + parser.unread++ + } + + // On EOF, put NUL into the buffer and return. + if parser.eof { + parser.buffer[buffer_len] = 0 + buffer_len++ + parser.unread++ + break + } + } + // [Go] Read the documentation of this function above. To return true, + // we need to have the given length in the buffer. Not doing that means + // every single check that calls this function to make sure the buffer + // has a given length is Go) panicking; or C) accessing invalid memory. + // This happens here due to the EOF above breaking early. + for buffer_len < length { + parser.buffer[buffer_len] = 0 + buffer_len++ + } + parser.buffer = parser.buffer[:buffer_len] + return true +} diff --git a/asyncapi/vendor/gopkg.in/yaml.v3/resolve.go b/asyncapi/vendor/gopkg.in/yaml.v3/resolve.go new file mode 100644 index 0000000..64ae888 --- /dev/null +++ b/asyncapi/vendor/gopkg.in/yaml.v3/resolve.go @@ -0,0 +1,326 @@ +// +// Copyright (c) 2011-2019 Canonical Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package yaml + +import ( + "encoding/base64" + "math" + "regexp" + "strconv" + "strings" + "time" +) + +type resolveMapItem struct { + value interface{} + tag string +} + +var resolveTable = make([]byte, 256) +var resolveMap = make(map[string]resolveMapItem) + +func init() { + t := resolveTable + t[int('+')] = 'S' // Sign + t[int('-')] = 'S' + for _, c := range "0123456789" { + t[int(c)] = 'D' // Digit + } + for _, c := range "yYnNtTfFoO~" { + t[int(c)] = 'M' // In map + } + t[int('.')] = '.' // Float (potentially in map) + + var resolveMapList = []struct { + v interface{} + tag string + l []string + }{ + {true, boolTag, []string{"true", "True", "TRUE"}}, + {false, boolTag, []string{"false", "False", "FALSE"}}, + {nil, nullTag, []string{"", "~", "null", "Null", "NULL"}}, + {math.NaN(), floatTag, []string{".nan", ".NaN", ".NAN"}}, + {math.Inf(+1), floatTag, []string{".inf", ".Inf", ".INF"}}, + {math.Inf(+1), floatTag, []string{"+.inf", "+.Inf", "+.INF"}}, + {math.Inf(-1), floatTag, []string{"-.inf", "-.Inf", "-.INF"}}, + {"<<", mergeTag, []string{"<<"}}, + } + + m := resolveMap + for _, item := range resolveMapList { + for _, s := range item.l { + m[s] = resolveMapItem{item.v, item.tag} + } + } +} + +const ( + nullTag = "!!null" + boolTag = "!!bool" + strTag = "!!str" + intTag = "!!int" + floatTag = "!!float" + timestampTag = "!!timestamp" + seqTag = "!!seq" + mapTag = "!!map" + binaryTag = "!!binary" + mergeTag = "!!merge" +) + +var longTags = make(map[string]string) +var shortTags = make(map[string]string) + +func init() { + for _, stag := range []string{nullTag, boolTag, strTag, intTag, floatTag, timestampTag, seqTag, mapTag, binaryTag, mergeTag} { + ltag := longTag(stag) + longTags[stag] = ltag + shortTags[ltag] = stag + } +} + +const longTagPrefix = "tag:yaml.org,2002:" + +func shortTag(tag string) string { + if strings.HasPrefix(tag, longTagPrefix) { + if stag, ok := shortTags[tag]; ok { + return stag + } + return "!!" + tag[len(longTagPrefix):] + } + return tag +} + +func longTag(tag string) string { + if strings.HasPrefix(tag, "!!") { + if ltag, ok := longTags[tag]; ok { + return ltag + } + return longTagPrefix + tag[2:] + } + return tag +} + +func resolvableTag(tag string) bool { + switch tag { + case "", strTag, boolTag, intTag, floatTag, nullTag, timestampTag: + return true + } + return false +} + +var yamlStyleFloat = regexp.MustCompile(`^[-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?$`) + +func resolve(tag string, in string) (rtag string, out interface{}) { + tag = shortTag(tag) + if !resolvableTag(tag) { + return tag, in + } + + defer func() { + switch tag { + case "", rtag, strTag, binaryTag: + return + case floatTag: + if rtag == intTag { + switch v := out.(type) { + case int64: + rtag = floatTag + out = float64(v) + return + case int: + rtag = floatTag + out = float64(v) + return + } + } + } + failf("cannot decode %s `%s` as a %s", shortTag(rtag), in, shortTag(tag)) + }() + + // Any data is accepted as a !!str or !!binary. + // Otherwise, the prefix is enough of a hint about what it might be. + hint := byte('N') + if in != "" { + hint = resolveTable[in[0]] + } + if hint != 0 && tag != strTag && tag != binaryTag { + // Handle things we can lookup in a map. + if item, ok := resolveMap[in]; ok { + return item.tag, item.value + } + + // Base 60 floats are a bad idea, were dropped in YAML 1.2, and + // are purposefully unsupported here. They're still quoted on + // the way out for compatibility with other parser, though. + + switch hint { + case 'M': + // We've already checked the map above. + + case '.': + // Not in the map, so maybe a normal float. + floatv, err := strconv.ParseFloat(in, 64) + if err == nil { + return floatTag, floatv + } + + case 'D', 'S': + // Int, float, or timestamp. + // Only try values as a timestamp if the value is unquoted or there's an explicit + // !!timestamp tag. + if tag == "" || tag == timestampTag { + t, ok := parseTimestamp(in) + if ok { + return timestampTag, t + } + } + + plain := strings.Replace(in, "_", "", -1) + intv, err := strconv.ParseInt(plain, 0, 64) + if err == nil { + if intv == int64(int(intv)) { + return intTag, int(intv) + } else { + return intTag, intv + } + } + uintv, err := strconv.ParseUint(plain, 0, 64) + if err == nil { + return intTag, uintv + } + if yamlStyleFloat.MatchString(plain) { + floatv, err := strconv.ParseFloat(plain, 64) + if err == nil { + return floatTag, floatv + } + } + if strings.HasPrefix(plain, "0b") { + intv, err := strconv.ParseInt(plain[2:], 2, 64) + if err == nil { + if intv == int64(int(intv)) { + return intTag, int(intv) + } else { + return intTag, intv + } + } + uintv, err := strconv.ParseUint(plain[2:], 2, 64) + if err == nil { + return intTag, uintv + } + } else if strings.HasPrefix(plain, "-0b") { + intv, err := strconv.ParseInt("-"+plain[3:], 2, 64) + if err == nil { + if true || intv == int64(int(intv)) { + return intTag, int(intv) + } else { + return intTag, intv + } + } + } + // Octals as introduced in version 1.2 of the spec. + // Octals from the 1.1 spec, spelled as 0777, are still + // decoded by default in v3 as well for compatibility. + // May be dropped in v4 depending on how usage evolves. + if strings.HasPrefix(plain, "0o") { + intv, err := strconv.ParseInt(plain[2:], 8, 64) + if err == nil { + if intv == int64(int(intv)) { + return intTag, int(intv) + } else { + return intTag, intv + } + } + uintv, err := strconv.ParseUint(plain[2:], 8, 64) + if err == nil { + return intTag, uintv + } + } else if strings.HasPrefix(plain, "-0o") { + intv, err := strconv.ParseInt("-"+plain[3:], 8, 64) + if err == nil { + if true || intv == int64(int(intv)) { + return intTag, int(intv) + } else { + return intTag, intv + } + } + } + default: + panic("internal error: missing handler for resolver table: " + string(rune(hint)) + " (with " + in + ")") + } + } + return strTag, in +} + +// encodeBase64 encodes s as base64 that is broken up into multiple lines +// as appropriate for the resulting length. +func encodeBase64(s string) string { + const lineLen = 70 + encLen := base64.StdEncoding.EncodedLen(len(s)) + lines := encLen/lineLen + 1 + buf := make([]byte, encLen*2+lines) + in := buf[0:encLen] + out := buf[encLen:] + base64.StdEncoding.Encode(in, []byte(s)) + k := 0 + for i := 0; i < len(in); i += lineLen { + j := i + lineLen + if j > len(in) { + j = len(in) + } + k += copy(out[k:], in[i:j]) + if lines > 1 { + out[k] = '\n' + k++ + } + } + return string(out[:k]) +} + +// This is a subset of the formats allowed by the regular expression +// defined at http://yaml.org/type/timestamp.html. +var allowedTimestampFormats = []string{ + "2006-1-2T15:4:5.999999999Z07:00", // RCF3339Nano with short date fields. + "2006-1-2t15:4:5.999999999Z07:00", // RFC3339Nano with short date fields and lower-case "t". + "2006-1-2 15:4:5.999999999", // space separated with no time zone + "2006-1-2", // date only + // Notable exception: time.Parse cannot handle: "2001-12-14 21:59:43.10 -5" + // from the set of examples. +} + +// parseTimestamp parses s as a timestamp string and +// returns the timestamp and reports whether it succeeded. +// Timestamp formats are defined at http://yaml.org/type/timestamp.html +func parseTimestamp(s string) (time.Time, bool) { + // TODO write code to check all the formats supported by + // http://yaml.org/type/timestamp.html instead of using time.Parse. + + // Quick check: all date formats start with YYYY-. + i := 0 + for ; i < len(s); i++ { + if c := s[i]; c < '0' || c > '9' { + break + } + } + if i != 4 || i == len(s) || s[i] != '-' { + return time.Time{}, false + } + for _, format := range allowedTimestampFormats { + if t, err := time.Parse(format, s); err == nil { + return t, true + } + } + return time.Time{}, false +} diff --git a/asyncapi/vendor/gopkg.in/yaml.v3/scannerc.go b/asyncapi/vendor/gopkg.in/yaml.v3/scannerc.go new file mode 100644 index 0000000..ca00701 --- /dev/null +++ b/asyncapi/vendor/gopkg.in/yaml.v3/scannerc.go @@ -0,0 +1,3038 @@ +// +// Copyright (c) 2011-2019 Canonical Ltd +// Copyright (c) 2006-2010 Kirill Simonov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package yaml + +import ( + "bytes" + "fmt" +) + +// Introduction +// ************ +// +// The following notes assume that you are familiar with the YAML specification +// (http://yaml.org/spec/1.2/spec.html). We mostly follow it, although in +// some cases we are less restrictive that it requires. +// +// The process of transforming a YAML stream into a sequence of events is +// divided on two steps: Scanning and Parsing. +// +// The Scanner transforms the input stream into a sequence of tokens, while the +// parser transform the sequence of tokens produced by the Scanner into a +// sequence of parsing events. +// +// The Scanner is rather clever and complicated. The Parser, on the contrary, +// is a straightforward implementation of a recursive-descendant parser (or, +// LL(1) parser, as it is usually called). +// +// Actually there are two issues of Scanning that might be called "clever", the +// rest is quite straightforward. The issues are "block collection start" and +// "simple keys". Both issues are explained below in details. +// +// Here the Scanning step is explained and implemented. We start with the list +// of all the tokens produced by the Scanner together with short descriptions. +// +// Now, tokens: +// +// STREAM-START(encoding) # The stream start. +// STREAM-END # The stream end. +// VERSION-DIRECTIVE(major,minor) # The '%YAML' directive. +// TAG-DIRECTIVE(handle,prefix) # The '%TAG' directive. +// DOCUMENT-START # '---' +// DOCUMENT-END # '...' +// BLOCK-SEQUENCE-START # Indentation increase denoting a block +// BLOCK-MAPPING-START # sequence or a block mapping. +// BLOCK-END # Indentation decrease. +// FLOW-SEQUENCE-START # '[' +// FLOW-SEQUENCE-END # ']' +// BLOCK-SEQUENCE-START # '{' +// BLOCK-SEQUENCE-END # '}' +// BLOCK-ENTRY # '-' +// FLOW-ENTRY # ',' +// KEY # '?' or nothing (simple keys). +// VALUE # ':' +// ALIAS(anchor) # '*anchor' +// ANCHOR(anchor) # '&anchor' +// TAG(handle,suffix) # '!handle!suffix' +// SCALAR(value,style) # A scalar. +// +// The following two tokens are "virtual" tokens denoting the beginning and the +// end of the stream: +// +// STREAM-START(encoding) +// STREAM-END +// +// We pass the information about the input stream encoding with the +// STREAM-START token. +// +// The next two tokens are responsible for tags: +// +// VERSION-DIRECTIVE(major,minor) +// TAG-DIRECTIVE(handle,prefix) +// +// Example: +// +// %YAML 1.1 +// %TAG ! !foo +// %TAG !yaml! tag:yaml.org,2002: +// --- +// +// The correspoding sequence of tokens: +// +// STREAM-START(utf-8) +// VERSION-DIRECTIVE(1,1) +// TAG-DIRECTIVE("!","!foo") +// TAG-DIRECTIVE("!yaml","tag:yaml.org,2002:") +// DOCUMENT-START +// STREAM-END +// +// Note that the VERSION-DIRECTIVE and TAG-DIRECTIVE tokens occupy a whole +// line. +// +// The document start and end indicators are represented by: +// +// DOCUMENT-START +// DOCUMENT-END +// +// Note that if a YAML stream contains an implicit document (without '---' +// and '...' indicators), no DOCUMENT-START and DOCUMENT-END tokens will be +// produced. +// +// In the following examples, we present whole documents together with the +// produced tokens. +// +// 1. An implicit document: +// +// 'a scalar' +// +// Tokens: +// +// STREAM-START(utf-8) +// SCALAR("a scalar",single-quoted) +// STREAM-END +// +// 2. An explicit document: +// +// --- +// 'a scalar' +// ... +// +// Tokens: +// +// STREAM-START(utf-8) +// DOCUMENT-START +// SCALAR("a scalar",single-quoted) +// DOCUMENT-END +// STREAM-END +// +// 3. Several documents in a stream: +// +// 'a scalar' +// --- +// 'another scalar' +// --- +// 'yet another scalar' +// +// Tokens: +// +// STREAM-START(utf-8) +// SCALAR("a scalar",single-quoted) +// DOCUMENT-START +// SCALAR("another scalar",single-quoted) +// DOCUMENT-START +// SCALAR("yet another scalar",single-quoted) +// STREAM-END +// +// We have already introduced the SCALAR token above. The following tokens are +// used to describe aliases, anchors, tag, and scalars: +// +// ALIAS(anchor) +// ANCHOR(anchor) +// TAG(handle,suffix) +// SCALAR(value,style) +// +// The following series of examples illustrate the usage of these tokens: +// +// 1. A recursive sequence: +// +// &A [ *A ] +// +// Tokens: +// +// STREAM-START(utf-8) +// ANCHOR("A") +// FLOW-SEQUENCE-START +// ALIAS("A") +// FLOW-SEQUENCE-END +// STREAM-END +// +// 2. A tagged scalar: +// +// !!float "3.14" # A good approximation. +// +// Tokens: +// +// STREAM-START(utf-8) +// TAG("!!","float") +// SCALAR("3.14",double-quoted) +// STREAM-END +// +// 3. Various scalar styles: +// +// --- # Implicit empty plain scalars do not produce tokens. +// --- a plain scalar +// --- 'a single-quoted scalar' +// --- "a double-quoted scalar" +// --- |- +// a literal scalar +// --- >- +// a folded +// scalar +// +// Tokens: +// +// STREAM-START(utf-8) +// DOCUMENT-START +// DOCUMENT-START +// SCALAR("a plain scalar",plain) +// DOCUMENT-START +// SCALAR("a single-quoted scalar",single-quoted) +// DOCUMENT-START +// SCALAR("a double-quoted scalar",double-quoted) +// DOCUMENT-START +// SCALAR("a literal scalar",literal) +// DOCUMENT-START +// SCALAR("a folded scalar",folded) +// STREAM-END +// +// Now it's time to review collection-related tokens. We will start with +// flow collections: +// +// FLOW-SEQUENCE-START +// FLOW-SEQUENCE-END +// FLOW-MAPPING-START +// FLOW-MAPPING-END +// FLOW-ENTRY +// KEY +// VALUE +// +// The tokens FLOW-SEQUENCE-START, FLOW-SEQUENCE-END, FLOW-MAPPING-START, and +// FLOW-MAPPING-END represent the indicators '[', ']', '{', and '}' +// correspondingly. FLOW-ENTRY represent the ',' indicator. Finally the +// indicators '?' and ':', which are used for denoting mapping keys and values, +// are represented by the KEY and VALUE tokens. +// +// The following examples show flow collections: +// +// 1. A flow sequence: +// +// [item 1, item 2, item 3] +// +// Tokens: +// +// STREAM-START(utf-8) +// FLOW-SEQUENCE-START +// SCALAR("item 1",plain) +// FLOW-ENTRY +// SCALAR("item 2",plain) +// FLOW-ENTRY +// SCALAR("item 3",plain) +// FLOW-SEQUENCE-END +// STREAM-END +// +// 2. A flow mapping: +// +// { +// a simple key: a value, # Note that the KEY token is produced. +// ? a complex key: another value, +// } +// +// Tokens: +// +// STREAM-START(utf-8) +// FLOW-MAPPING-START +// KEY +// SCALAR("a simple key",plain) +// VALUE +// SCALAR("a value",plain) +// FLOW-ENTRY +// KEY +// SCALAR("a complex key",plain) +// VALUE +// SCALAR("another value",plain) +// FLOW-ENTRY +// FLOW-MAPPING-END +// STREAM-END +// +// A simple key is a key which is not denoted by the '?' indicator. Note that +// the Scanner still produce the KEY token whenever it encounters a simple key. +// +// For scanning block collections, the following tokens are used (note that we +// repeat KEY and VALUE here): +// +// BLOCK-SEQUENCE-START +// BLOCK-MAPPING-START +// BLOCK-END +// BLOCK-ENTRY +// KEY +// VALUE +// +// The tokens BLOCK-SEQUENCE-START and BLOCK-MAPPING-START denote indentation +// increase that precedes a block collection (cf. the INDENT token in Python). +// The token BLOCK-END denote indentation decrease that ends a block collection +// (cf. the DEDENT token in Python). However YAML has some syntax pecularities +// that makes detections of these tokens more complex. +// +// The tokens BLOCK-ENTRY, KEY, and VALUE are used to represent the indicators +// '-', '?', and ':' correspondingly. +// +// The following examples show how the tokens BLOCK-SEQUENCE-START, +// BLOCK-MAPPING-START, and BLOCK-END are emitted by the Scanner: +// +// 1. Block sequences: +// +// - item 1 +// - item 2 +// - +// - item 3.1 +// - item 3.2 +// - +// key 1: value 1 +// key 2: value 2 +// +// Tokens: +// +// STREAM-START(utf-8) +// BLOCK-SEQUENCE-START +// BLOCK-ENTRY +// SCALAR("item 1",plain) +// BLOCK-ENTRY +// SCALAR("item 2",plain) +// BLOCK-ENTRY +// BLOCK-SEQUENCE-START +// BLOCK-ENTRY +// SCALAR("item 3.1",plain) +// BLOCK-ENTRY +// SCALAR("item 3.2",plain) +// BLOCK-END +// BLOCK-ENTRY +// BLOCK-MAPPING-START +// KEY +// SCALAR("key 1",plain) +// VALUE +// SCALAR("value 1",plain) +// KEY +// SCALAR("key 2",plain) +// VALUE +// SCALAR("value 2",plain) +// BLOCK-END +// BLOCK-END +// STREAM-END +// +// 2. Block mappings: +// +// a simple key: a value # The KEY token is produced here. +// ? a complex key +// : another value +// a mapping: +// key 1: value 1 +// key 2: value 2 +// a sequence: +// - item 1 +// - item 2 +// +// Tokens: +// +// STREAM-START(utf-8) +// BLOCK-MAPPING-START +// KEY +// SCALAR("a simple key",plain) +// VALUE +// SCALAR("a value",plain) +// KEY +// SCALAR("a complex key",plain) +// VALUE +// SCALAR("another value",plain) +// KEY +// SCALAR("a mapping",plain) +// BLOCK-MAPPING-START +// KEY +// SCALAR("key 1",plain) +// VALUE +// SCALAR("value 1",plain) +// KEY +// SCALAR("key 2",plain) +// VALUE +// SCALAR("value 2",plain) +// BLOCK-END +// KEY +// SCALAR("a sequence",plain) +// VALUE +// BLOCK-SEQUENCE-START +// BLOCK-ENTRY +// SCALAR("item 1",plain) +// BLOCK-ENTRY +// SCALAR("item 2",plain) +// BLOCK-END +// BLOCK-END +// STREAM-END +// +// YAML does not always require to start a new block collection from a new +// line. If the current line contains only '-', '?', and ':' indicators, a new +// block collection may start at the current line. The following examples +// illustrate this case: +// +// 1. Collections in a sequence: +// +// - - item 1 +// - item 2 +// - key 1: value 1 +// key 2: value 2 +// - ? complex key +// : complex value +// +// Tokens: +// +// STREAM-START(utf-8) +// BLOCK-SEQUENCE-START +// BLOCK-ENTRY +// BLOCK-SEQUENCE-START +// BLOCK-ENTRY +// SCALAR("item 1",plain) +// BLOCK-ENTRY +// SCALAR("item 2",plain) +// BLOCK-END +// BLOCK-ENTRY +// BLOCK-MAPPING-START +// KEY +// SCALAR("key 1",plain) +// VALUE +// SCALAR("value 1",plain) +// KEY +// SCALAR("key 2",plain) +// VALUE +// SCALAR("value 2",plain) +// BLOCK-END +// BLOCK-ENTRY +// BLOCK-MAPPING-START +// KEY +// SCALAR("complex key") +// VALUE +// SCALAR("complex value") +// BLOCK-END +// BLOCK-END +// STREAM-END +// +// 2. Collections in a mapping: +// +// ? a sequence +// : - item 1 +// - item 2 +// ? a mapping +// : key 1: value 1 +// key 2: value 2 +// +// Tokens: +// +// STREAM-START(utf-8) +// BLOCK-MAPPING-START +// KEY +// SCALAR("a sequence",plain) +// VALUE +// BLOCK-SEQUENCE-START +// BLOCK-ENTRY +// SCALAR("item 1",plain) +// BLOCK-ENTRY +// SCALAR("item 2",plain) +// BLOCK-END +// KEY +// SCALAR("a mapping",plain) +// VALUE +// BLOCK-MAPPING-START +// KEY +// SCALAR("key 1",plain) +// VALUE +// SCALAR("value 1",plain) +// KEY +// SCALAR("key 2",plain) +// VALUE +// SCALAR("value 2",plain) +// BLOCK-END +// BLOCK-END +// STREAM-END +// +// YAML also permits non-indented sequences if they are included into a block +// mapping. In this case, the token BLOCK-SEQUENCE-START is not produced: +// +// key: +// - item 1 # BLOCK-SEQUENCE-START is NOT produced here. +// - item 2 +// +// Tokens: +// +// STREAM-START(utf-8) +// BLOCK-MAPPING-START +// KEY +// SCALAR("key",plain) +// VALUE +// BLOCK-ENTRY +// SCALAR("item 1",plain) +// BLOCK-ENTRY +// SCALAR("item 2",plain) +// BLOCK-END +// + +// Ensure that the buffer contains the required number of characters. +// Return true on success, false on failure (reader error or memory error). +func cache(parser *yaml_parser_t, length int) bool { + // [Go] This was inlined: !cache(A, B) -> unread < B && !update(A, B) + return parser.unread >= length || yaml_parser_update_buffer(parser, length) +} + +// Advance the buffer pointer. +func skip(parser *yaml_parser_t) { + if !is_blank(parser.buffer, parser.buffer_pos) { + parser.newlines = 0 + } + parser.mark.index++ + parser.mark.column++ + parser.unread-- + parser.buffer_pos += width(parser.buffer[parser.buffer_pos]) +} + +func skip_line(parser *yaml_parser_t) { + if is_crlf(parser.buffer, parser.buffer_pos) { + parser.mark.index += 2 + parser.mark.column = 0 + parser.mark.line++ + parser.unread -= 2 + parser.buffer_pos += 2 + parser.newlines++ + } else if is_break(parser.buffer, parser.buffer_pos) { + parser.mark.index++ + parser.mark.column = 0 + parser.mark.line++ + parser.unread-- + parser.buffer_pos += width(parser.buffer[parser.buffer_pos]) + parser.newlines++ + } +} + +// Copy a character to a string buffer and advance pointers. +func read(parser *yaml_parser_t, s []byte) []byte { + if !is_blank(parser.buffer, parser.buffer_pos) { + parser.newlines = 0 + } + w := width(parser.buffer[parser.buffer_pos]) + if w == 0 { + panic("invalid character sequence") + } + if len(s) == 0 { + s = make([]byte, 0, 32) + } + if w == 1 && len(s)+w <= cap(s) { + s = s[:len(s)+1] + s[len(s)-1] = parser.buffer[parser.buffer_pos] + parser.buffer_pos++ + } else { + s = append(s, parser.buffer[parser.buffer_pos:parser.buffer_pos+w]...) + parser.buffer_pos += w + } + parser.mark.index++ + parser.mark.column++ + parser.unread-- + return s +} + +// Copy a line break character to a string buffer and advance pointers. +func read_line(parser *yaml_parser_t, s []byte) []byte { + buf := parser.buffer + pos := parser.buffer_pos + switch { + case buf[pos] == '\r' && buf[pos+1] == '\n': + // CR LF . LF + s = append(s, '\n') + parser.buffer_pos += 2 + parser.mark.index++ + parser.unread-- + case buf[pos] == '\r' || buf[pos] == '\n': + // CR|LF . LF + s = append(s, '\n') + parser.buffer_pos += 1 + case buf[pos] == '\xC2' && buf[pos+1] == '\x85': + // NEL . LF + s = append(s, '\n') + parser.buffer_pos += 2 + case buf[pos] == '\xE2' && buf[pos+1] == '\x80' && (buf[pos+2] == '\xA8' || buf[pos+2] == '\xA9'): + // LS|PS . LS|PS + s = append(s, buf[parser.buffer_pos:pos+3]...) + parser.buffer_pos += 3 + default: + return s + } + parser.mark.index++ + parser.mark.column = 0 + parser.mark.line++ + parser.unread-- + parser.newlines++ + return s +} + +// Get the next token. +func yaml_parser_scan(parser *yaml_parser_t, token *yaml_token_t) bool { + // Erase the token object. + *token = yaml_token_t{} // [Go] Is this necessary? + + // No tokens after STREAM-END or error. + if parser.stream_end_produced || parser.error != yaml_NO_ERROR { + return true + } + + // Ensure that the tokens queue contains enough tokens. + if !parser.token_available { + if !yaml_parser_fetch_more_tokens(parser) { + return false + } + } + + // Fetch the next token from the queue. + *token = parser.tokens[parser.tokens_head] + parser.tokens_head++ + parser.tokens_parsed++ + parser.token_available = false + + if token.typ == yaml_STREAM_END_TOKEN { + parser.stream_end_produced = true + } + return true +} + +// Set the scanner error and return false. +func yaml_parser_set_scanner_error(parser *yaml_parser_t, context string, context_mark yaml_mark_t, problem string) bool { + parser.error = yaml_SCANNER_ERROR + parser.context = context + parser.context_mark = context_mark + parser.problem = problem + parser.problem_mark = parser.mark + return false +} + +func yaml_parser_set_scanner_tag_error(parser *yaml_parser_t, directive bool, context_mark yaml_mark_t, problem string) bool { + context := "while parsing a tag" + if directive { + context = "while parsing a %TAG directive" + } + return yaml_parser_set_scanner_error(parser, context, context_mark, problem) +} + +func trace(args ...interface{}) func() { + pargs := append([]interface{}{"+++"}, args...) + fmt.Println(pargs...) + pargs = append([]interface{}{"---"}, args...) + return func() { fmt.Println(pargs...) } +} + +// Ensure that the tokens queue contains at least one token which can be +// returned to the Parser. +func yaml_parser_fetch_more_tokens(parser *yaml_parser_t) bool { + // While we need more tokens to fetch, do it. + for { + // [Go] The comment parsing logic requires a lookahead of two tokens + // so that foot comments may be parsed in time of associating them + // with the tokens that are parsed before them, and also for line + // comments to be transformed into head comments in some edge cases. + if parser.tokens_head < len(parser.tokens)-2 { + // If a potential simple key is at the head position, we need to fetch + // the next token to disambiguate it. + head_tok_idx, ok := parser.simple_keys_by_tok[parser.tokens_parsed] + if !ok { + break + } else if valid, ok := yaml_simple_key_is_valid(parser, &parser.simple_keys[head_tok_idx]); !ok { + return false + } else if !valid { + break + } + } + // Fetch the next token. + if !yaml_parser_fetch_next_token(parser) { + return false + } + } + + parser.token_available = true + return true +} + +// The dispatcher for token fetchers. +func yaml_parser_fetch_next_token(parser *yaml_parser_t) (ok bool) { + // Ensure that the buffer is initialized. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + + // Check if we just started scanning. Fetch STREAM-START then. + if !parser.stream_start_produced { + return yaml_parser_fetch_stream_start(parser) + } + + scan_mark := parser.mark + + // Eat whitespaces and comments until we reach the next token. + if !yaml_parser_scan_to_next_token(parser) { + return false + } + + // [Go] While unrolling indents, transform the head comments of prior + // indentation levels observed after scan_start into foot comments at + // the respective indexes. + + // Check the indentation level against the current column. + if !yaml_parser_unroll_indent(parser, parser.mark.column, scan_mark) { + return false + } + + // Ensure that the buffer contains at least 4 characters. 4 is the length + // of the longest indicators ('--- ' and '... '). + if parser.unread < 4 && !yaml_parser_update_buffer(parser, 4) { + return false + } + + // Is it the end of the stream? + if is_z(parser.buffer, parser.buffer_pos) { + return yaml_parser_fetch_stream_end(parser) + } + + // Is it a directive? + if parser.mark.column == 0 && parser.buffer[parser.buffer_pos] == '%' { + return yaml_parser_fetch_directive(parser) + } + + buf := parser.buffer + pos := parser.buffer_pos + + // Is it the document start indicator? + if parser.mark.column == 0 && buf[pos] == '-' && buf[pos+1] == '-' && buf[pos+2] == '-' && is_blankz(buf, pos+3) { + return yaml_parser_fetch_document_indicator(parser, yaml_DOCUMENT_START_TOKEN) + } + + // Is it the document end indicator? + if parser.mark.column == 0 && buf[pos] == '.' && buf[pos+1] == '.' && buf[pos+2] == '.' && is_blankz(buf, pos+3) { + return yaml_parser_fetch_document_indicator(parser, yaml_DOCUMENT_END_TOKEN) + } + + comment_mark := parser.mark + if len(parser.tokens) > 0 && (parser.flow_level == 0 && buf[pos] == ':' || parser.flow_level > 0 && buf[pos] == ',') { + // Associate any following comments with the prior token. + comment_mark = parser.tokens[len(parser.tokens)-1].start_mark + } + defer func() { + if !ok { + return + } + if len(parser.tokens) > 0 && parser.tokens[len(parser.tokens)-1].typ == yaml_BLOCK_ENTRY_TOKEN { + // Sequence indicators alone have no line comments. It becomes + // a head comment for whatever follows. + return + } + if !yaml_parser_scan_line_comment(parser, comment_mark) { + ok = false + return + } + }() + + // Is it the flow sequence start indicator? + if buf[pos] == '[' { + return yaml_parser_fetch_flow_collection_start(parser, yaml_FLOW_SEQUENCE_START_TOKEN) + } + + // Is it the flow mapping start indicator? + if parser.buffer[parser.buffer_pos] == '{' { + return yaml_parser_fetch_flow_collection_start(parser, yaml_FLOW_MAPPING_START_TOKEN) + } + + // Is it the flow sequence end indicator? + if parser.buffer[parser.buffer_pos] == ']' { + return yaml_parser_fetch_flow_collection_end(parser, + yaml_FLOW_SEQUENCE_END_TOKEN) + } + + // Is it the flow mapping end indicator? + if parser.buffer[parser.buffer_pos] == '}' { + return yaml_parser_fetch_flow_collection_end(parser, + yaml_FLOW_MAPPING_END_TOKEN) + } + + // Is it the flow entry indicator? + if parser.buffer[parser.buffer_pos] == ',' { + return yaml_parser_fetch_flow_entry(parser) + } + + // Is it the block entry indicator? + if parser.buffer[parser.buffer_pos] == '-' && is_blankz(parser.buffer, parser.buffer_pos+1) { + return yaml_parser_fetch_block_entry(parser) + } + + // Is it the key indicator? + if parser.buffer[parser.buffer_pos] == '?' && (parser.flow_level > 0 || is_blankz(parser.buffer, parser.buffer_pos+1)) { + return yaml_parser_fetch_key(parser) + } + + // Is it the value indicator? + if parser.buffer[parser.buffer_pos] == ':' && (parser.flow_level > 0 || is_blankz(parser.buffer, parser.buffer_pos+1)) { + return yaml_parser_fetch_value(parser) + } + + // Is it an alias? + if parser.buffer[parser.buffer_pos] == '*' { + return yaml_parser_fetch_anchor(parser, yaml_ALIAS_TOKEN) + } + + // Is it an anchor? + if parser.buffer[parser.buffer_pos] == '&' { + return yaml_parser_fetch_anchor(parser, yaml_ANCHOR_TOKEN) + } + + // Is it a tag? + if parser.buffer[parser.buffer_pos] == '!' { + return yaml_parser_fetch_tag(parser) + } + + // Is it a literal scalar? + if parser.buffer[parser.buffer_pos] == '|' && parser.flow_level == 0 { + return yaml_parser_fetch_block_scalar(parser, true) + } + + // Is it a folded scalar? + if parser.buffer[parser.buffer_pos] == '>' && parser.flow_level == 0 { + return yaml_parser_fetch_block_scalar(parser, false) + } + + // Is it a single-quoted scalar? + if parser.buffer[parser.buffer_pos] == '\'' { + return yaml_parser_fetch_flow_scalar(parser, true) + } + + // Is it a double-quoted scalar? + if parser.buffer[parser.buffer_pos] == '"' { + return yaml_parser_fetch_flow_scalar(parser, false) + } + + // Is it a plain scalar? + // + // A plain scalar may start with any non-blank characters except + // + // '-', '?', ':', ',', '[', ']', '{', '}', + // '#', '&', '*', '!', '|', '>', '\'', '\"', + // '%', '@', '`'. + // + // In the block context (and, for the '-' indicator, in the flow context + // too), it may also start with the characters + // + // '-', '?', ':' + // + // if it is followed by a non-space character. + // + // The last rule is more restrictive than the specification requires. + // [Go] TODO Make this logic more reasonable. + //switch parser.buffer[parser.buffer_pos] { + //case '-', '?', ':', ',', '?', '-', ',', ':', ']', '[', '}', '{', '&', '#', '!', '*', '>', '|', '"', '\'', '@', '%', '-', '`': + //} + if !(is_blankz(parser.buffer, parser.buffer_pos) || parser.buffer[parser.buffer_pos] == '-' || + parser.buffer[parser.buffer_pos] == '?' || parser.buffer[parser.buffer_pos] == ':' || + parser.buffer[parser.buffer_pos] == ',' || parser.buffer[parser.buffer_pos] == '[' || + parser.buffer[parser.buffer_pos] == ']' || parser.buffer[parser.buffer_pos] == '{' || + parser.buffer[parser.buffer_pos] == '}' || parser.buffer[parser.buffer_pos] == '#' || + parser.buffer[parser.buffer_pos] == '&' || parser.buffer[parser.buffer_pos] == '*' || + parser.buffer[parser.buffer_pos] == '!' || parser.buffer[parser.buffer_pos] == '|' || + parser.buffer[parser.buffer_pos] == '>' || parser.buffer[parser.buffer_pos] == '\'' || + parser.buffer[parser.buffer_pos] == '"' || parser.buffer[parser.buffer_pos] == '%' || + parser.buffer[parser.buffer_pos] == '@' || parser.buffer[parser.buffer_pos] == '`') || + (parser.buffer[parser.buffer_pos] == '-' && !is_blank(parser.buffer, parser.buffer_pos+1)) || + (parser.flow_level == 0 && + (parser.buffer[parser.buffer_pos] == '?' || parser.buffer[parser.buffer_pos] == ':') && + !is_blankz(parser.buffer, parser.buffer_pos+1)) { + return yaml_parser_fetch_plain_scalar(parser) + } + + // If we don't determine the token type so far, it is an error. + return yaml_parser_set_scanner_error(parser, + "while scanning for the next token", parser.mark, + "found character that cannot start any token") +} + +func yaml_simple_key_is_valid(parser *yaml_parser_t, simple_key *yaml_simple_key_t) (valid, ok bool) { + if !simple_key.possible { + return false, true + } + + // The 1.2 specification says: + // + // "If the ? indicator is omitted, parsing needs to see past the + // implicit key to recognize it as such. To limit the amount of + // lookahead required, the “:” indicator must appear at most 1024 + // Unicode characters beyond the start of the key. In addition, the key + // is restricted to a single line." + // + if simple_key.mark.line < parser.mark.line || simple_key.mark.index+1024 < parser.mark.index { + // Check if the potential simple key to be removed is required. + if simple_key.required { + return false, yaml_parser_set_scanner_error(parser, + "while scanning a simple key", simple_key.mark, + "could not find expected ':'") + } + simple_key.possible = false + return false, true + } + return true, true +} + +// Check if a simple key may start at the current position and add it if +// needed. +func yaml_parser_save_simple_key(parser *yaml_parser_t) bool { + // A simple key is required at the current position if the scanner is in + // the block context and the current column coincides with the indentation + // level. + + required := parser.flow_level == 0 && parser.indent == parser.mark.column + + // + // If the current position may start a simple key, save it. + // + if parser.simple_key_allowed { + simple_key := yaml_simple_key_t{ + possible: true, + required: required, + token_number: parser.tokens_parsed + (len(parser.tokens) - parser.tokens_head), + mark: parser.mark, + } + + if !yaml_parser_remove_simple_key(parser) { + return false + } + parser.simple_keys[len(parser.simple_keys)-1] = simple_key + parser.simple_keys_by_tok[simple_key.token_number] = len(parser.simple_keys) - 1 + } + return true +} + +// Remove a potential simple key at the current flow level. +func yaml_parser_remove_simple_key(parser *yaml_parser_t) bool { + i := len(parser.simple_keys) - 1 + if parser.simple_keys[i].possible { + // If the key is required, it is an error. + if parser.simple_keys[i].required { + return yaml_parser_set_scanner_error(parser, + "while scanning a simple key", parser.simple_keys[i].mark, + "could not find expected ':'") + } + // Remove the key from the stack. + parser.simple_keys[i].possible = false + delete(parser.simple_keys_by_tok, parser.simple_keys[i].token_number) + } + return true +} + +// max_flow_level limits the flow_level +const max_flow_level = 10000 + +// Increase the flow level and resize the simple key list if needed. +func yaml_parser_increase_flow_level(parser *yaml_parser_t) bool { + // Reset the simple key on the next level. + parser.simple_keys = append(parser.simple_keys, yaml_simple_key_t{ + possible: false, + required: false, + token_number: parser.tokens_parsed + (len(parser.tokens) - parser.tokens_head), + mark: parser.mark, + }) + + // Increase the flow level. + parser.flow_level++ + if parser.flow_level > max_flow_level { + return yaml_parser_set_scanner_error(parser, + "while increasing flow level", parser.simple_keys[len(parser.simple_keys)-1].mark, + fmt.Sprintf("exceeded max depth of %d", max_flow_level)) + } + return true +} + +// Decrease the flow level. +func yaml_parser_decrease_flow_level(parser *yaml_parser_t) bool { + if parser.flow_level > 0 { + parser.flow_level-- + last := len(parser.simple_keys) - 1 + delete(parser.simple_keys_by_tok, parser.simple_keys[last].token_number) + parser.simple_keys = parser.simple_keys[:last] + } + return true +} + +// max_indents limits the indents stack size +const max_indents = 10000 + +// Push the current indentation level to the stack and set the new level +// the current column is greater than the indentation level. In this case, +// append or insert the specified token into the token queue. +func yaml_parser_roll_indent(parser *yaml_parser_t, column, number int, typ yaml_token_type_t, mark yaml_mark_t) bool { + // In the flow context, do nothing. + if parser.flow_level > 0 { + return true + } + + if parser.indent < column { + // Push the current indentation level to the stack and set the new + // indentation level. + parser.indents = append(parser.indents, parser.indent) + parser.indent = column + if len(parser.indents) > max_indents { + return yaml_parser_set_scanner_error(parser, + "while increasing indent level", parser.simple_keys[len(parser.simple_keys)-1].mark, + fmt.Sprintf("exceeded max depth of %d", max_indents)) + } + + // Create a token and insert it into the queue. + token := yaml_token_t{ + typ: typ, + start_mark: mark, + end_mark: mark, + } + if number > -1 { + number -= parser.tokens_parsed + } + yaml_insert_token(parser, number, &token) + } + return true +} + +// Pop indentation levels from the indents stack until the current level +// becomes less or equal to the column. For each indentation level, append +// the BLOCK-END token. +func yaml_parser_unroll_indent(parser *yaml_parser_t, column int, scan_mark yaml_mark_t) bool { + // In the flow context, do nothing. + if parser.flow_level > 0 { + return true + } + + block_mark := scan_mark + block_mark.index-- + + // Loop through the indentation levels in the stack. + for parser.indent > column { + + // [Go] Reposition the end token before potential following + // foot comments of parent blocks. For that, search + // backwards for recent comments that were at the same + // indent as the block that is ending now. + stop_index := block_mark.index + for i := len(parser.comments) - 1; i >= 0; i-- { + comment := &parser.comments[i] + + if comment.end_mark.index < stop_index { + // Don't go back beyond the start of the comment/whitespace scan, unless column < 0. + // If requested indent column is < 0, then the document is over and everything else + // is a foot anyway. + break + } + if comment.start_mark.column == parser.indent+1 { + // This is a good match. But maybe there's a former comment + // at that same indent level, so keep searching. + block_mark = comment.start_mark + } + + // While the end of the former comment matches with + // the start of the following one, we know there's + // nothing in between and scanning is still safe. + stop_index = comment.scan_mark.index + } + + // Create a token and append it to the queue. + token := yaml_token_t{ + typ: yaml_BLOCK_END_TOKEN, + start_mark: block_mark, + end_mark: block_mark, + } + yaml_insert_token(parser, -1, &token) + + // Pop the indentation level. + parser.indent = parser.indents[len(parser.indents)-1] + parser.indents = parser.indents[:len(parser.indents)-1] + } + return true +} + +// Initialize the scanner and produce the STREAM-START token. +func yaml_parser_fetch_stream_start(parser *yaml_parser_t) bool { + + // Set the initial indentation. + parser.indent = -1 + + // Initialize the simple key stack. + parser.simple_keys = append(parser.simple_keys, yaml_simple_key_t{}) + + parser.simple_keys_by_tok = make(map[int]int) + + // A simple key is allowed at the beginning of the stream. + parser.simple_key_allowed = true + + // We have started. + parser.stream_start_produced = true + + // Create the STREAM-START token and append it to the queue. + token := yaml_token_t{ + typ: yaml_STREAM_START_TOKEN, + start_mark: parser.mark, + end_mark: parser.mark, + encoding: parser.encoding, + } + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce the STREAM-END token and shut down the scanner. +func yaml_parser_fetch_stream_end(parser *yaml_parser_t) bool { + + // Force new line. + if parser.mark.column != 0 { + parser.mark.column = 0 + parser.mark.line++ + } + + // Reset the indentation level. + if !yaml_parser_unroll_indent(parser, -1, parser.mark) { + return false + } + + // Reset simple keys. + if !yaml_parser_remove_simple_key(parser) { + return false + } + + parser.simple_key_allowed = false + + // Create the STREAM-END token and append it to the queue. + token := yaml_token_t{ + typ: yaml_STREAM_END_TOKEN, + start_mark: parser.mark, + end_mark: parser.mark, + } + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce a VERSION-DIRECTIVE or TAG-DIRECTIVE token. +func yaml_parser_fetch_directive(parser *yaml_parser_t) bool { + // Reset the indentation level. + if !yaml_parser_unroll_indent(parser, -1, parser.mark) { + return false + } + + // Reset simple keys. + if !yaml_parser_remove_simple_key(parser) { + return false + } + + parser.simple_key_allowed = false + + // Create the YAML-DIRECTIVE or TAG-DIRECTIVE token. + token := yaml_token_t{} + if !yaml_parser_scan_directive(parser, &token) { + return false + } + // Append the token to the queue. + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce the DOCUMENT-START or DOCUMENT-END token. +func yaml_parser_fetch_document_indicator(parser *yaml_parser_t, typ yaml_token_type_t) bool { + // Reset the indentation level. + if !yaml_parser_unroll_indent(parser, -1, parser.mark) { + return false + } + + // Reset simple keys. + if !yaml_parser_remove_simple_key(parser) { + return false + } + + parser.simple_key_allowed = false + + // Consume the token. + start_mark := parser.mark + + skip(parser) + skip(parser) + skip(parser) + + end_mark := parser.mark + + // Create the DOCUMENT-START or DOCUMENT-END token. + token := yaml_token_t{ + typ: typ, + start_mark: start_mark, + end_mark: end_mark, + } + // Append the token to the queue. + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce the FLOW-SEQUENCE-START or FLOW-MAPPING-START token. +func yaml_parser_fetch_flow_collection_start(parser *yaml_parser_t, typ yaml_token_type_t) bool { + + // The indicators '[' and '{' may start a simple key. + if !yaml_parser_save_simple_key(parser) { + return false + } + + // Increase the flow level. + if !yaml_parser_increase_flow_level(parser) { + return false + } + + // A simple key may follow the indicators '[' and '{'. + parser.simple_key_allowed = true + + // Consume the token. + start_mark := parser.mark + skip(parser) + end_mark := parser.mark + + // Create the FLOW-SEQUENCE-START of FLOW-MAPPING-START token. + token := yaml_token_t{ + typ: typ, + start_mark: start_mark, + end_mark: end_mark, + } + // Append the token to the queue. + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce the FLOW-SEQUENCE-END or FLOW-MAPPING-END token. +func yaml_parser_fetch_flow_collection_end(parser *yaml_parser_t, typ yaml_token_type_t) bool { + // Reset any potential simple key on the current flow level. + if !yaml_parser_remove_simple_key(parser) { + return false + } + + // Decrease the flow level. + if !yaml_parser_decrease_flow_level(parser) { + return false + } + + // No simple keys after the indicators ']' and '}'. + parser.simple_key_allowed = false + + // Consume the token. + + start_mark := parser.mark + skip(parser) + end_mark := parser.mark + + // Create the FLOW-SEQUENCE-END of FLOW-MAPPING-END token. + token := yaml_token_t{ + typ: typ, + start_mark: start_mark, + end_mark: end_mark, + } + // Append the token to the queue. + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce the FLOW-ENTRY token. +func yaml_parser_fetch_flow_entry(parser *yaml_parser_t) bool { + // Reset any potential simple keys on the current flow level. + if !yaml_parser_remove_simple_key(parser) { + return false + } + + // Simple keys are allowed after ','. + parser.simple_key_allowed = true + + // Consume the token. + start_mark := parser.mark + skip(parser) + end_mark := parser.mark + + // Create the FLOW-ENTRY token and append it to the queue. + token := yaml_token_t{ + typ: yaml_FLOW_ENTRY_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + } + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce the BLOCK-ENTRY token. +func yaml_parser_fetch_block_entry(parser *yaml_parser_t) bool { + // Check if the scanner is in the block context. + if parser.flow_level == 0 { + // Check if we are allowed to start a new entry. + if !parser.simple_key_allowed { + return yaml_parser_set_scanner_error(parser, "", parser.mark, + "block sequence entries are not allowed in this context") + } + // Add the BLOCK-SEQUENCE-START token if needed. + if !yaml_parser_roll_indent(parser, parser.mark.column, -1, yaml_BLOCK_SEQUENCE_START_TOKEN, parser.mark) { + return false + } + } else { + // It is an error for the '-' indicator to occur in the flow context, + // but we let the Parser detect and report about it because the Parser + // is able to point to the context. + } + + // Reset any potential simple keys on the current flow level. + if !yaml_parser_remove_simple_key(parser) { + return false + } + + // Simple keys are allowed after '-'. + parser.simple_key_allowed = true + + // Consume the token. + start_mark := parser.mark + skip(parser) + end_mark := parser.mark + + // Create the BLOCK-ENTRY token and append it to the queue. + token := yaml_token_t{ + typ: yaml_BLOCK_ENTRY_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + } + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce the KEY token. +func yaml_parser_fetch_key(parser *yaml_parser_t) bool { + + // In the block context, additional checks are required. + if parser.flow_level == 0 { + // Check if we are allowed to start a new key (not nessesary simple). + if !parser.simple_key_allowed { + return yaml_parser_set_scanner_error(parser, "", parser.mark, + "mapping keys are not allowed in this context") + } + // Add the BLOCK-MAPPING-START token if needed. + if !yaml_parser_roll_indent(parser, parser.mark.column, -1, yaml_BLOCK_MAPPING_START_TOKEN, parser.mark) { + return false + } + } + + // Reset any potential simple keys on the current flow level. + if !yaml_parser_remove_simple_key(parser) { + return false + } + + // Simple keys are allowed after '?' in the block context. + parser.simple_key_allowed = parser.flow_level == 0 + + // Consume the token. + start_mark := parser.mark + skip(parser) + end_mark := parser.mark + + // Create the KEY token and append it to the queue. + token := yaml_token_t{ + typ: yaml_KEY_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + } + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce the VALUE token. +func yaml_parser_fetch_value(parser *yaml_parser_t) bool { + + simple_key := &parser.simple_keys[len(parser.simple_keys)-1] + + // Have we found a simple key? + if valid, ok := yaml_simple_key_is_valid(parser, simple_key); !ok { + return false + + } else if valid { + + // Create the KEY token and insert it into the queue. + token := yaml_token_t{ + typ: yaml_KEY_TOKEN, + start_mark: simple_key.mark, + end_mark: simple_key.mark, + } + yaml_insert_token(parser, simple_key.token_number-parser.tokens_parsed, &token) + + // In the block context, we may need to add the BLOCK-MAPPING-START token. + if !yaml_parser_roll_indent(parser, simple_key.mark.column, + simple_key.token_number, + yaml_BLOCK_MAPPING_START_TOKEN, simple_key.mark) { + return false + } + + // Remove the simple key. + simple_key.possible = false + delete(parser.simple_keys_by_tok, simple_key.token_number) + + // A simple key cannot follow another simple key. + parser.simple_key_allowed = false + + } else { + // The ':' indicator follows a complex key. + + // In the block context, extra checks are required. + if parser.flow_level == 0 { + + // Check if we are allowed to start a complex value. + if !parser.simple_key_allowed { + return yaml_parser_set_scanner_error(parser, "", parser.mark, + "mapping values are not allowed in this context") + } + + // Add the BLOCK-MAPPING-START token if needed. + if !yaml_parser_roll_indent(parser, parser.mark.column, -1, yaml_BLOCK_MAPPING_START_TOKEN, parser.mark) { + return false + } + } + + // Simple keys after ':' are allowed in the block context. + parser.simple_key_allowed = parser.flow_level == 0 + } + + // Consume the token. + start_mark := parser.mark + skip(parser) + end_mark := parser.mark + + // Create the VALUE token and append it to the queue. + token := yaml_token_t{ + typ: yaml_VALUE_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + } + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce the ALIAS or ANCHOR token. +func yaml_parser_fetch_anchor(parser *yaml_parser_t, typ yaml_token_type_t) bool { + // An anchor or an alias could be a simple key. + if !yaml_parser_save_simple_key(parser) { + return false + } + + // A simple key cannot follow an anchor or an alias. + parser.simple_key_allowed = false + + // Create the ALIAS or ANCHOR token and append it to the queue. + var token yaml_token_t + if !yaml_parser_scan_anchor(parser, &token, typ) { + return false + } + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce the TAG token. +func yaml_parser_fetch_tag(parser *yaml_parser_t) bool { + // A tag could be a simple key. + if !yaml_parser_save_simple_key(parser) { + return false + } + + // A simple key cannot follow a tag. + parser.simple_key_allowed = false + + // Create the TAG token and append it to the queue. + var token yaml_token_t + if !yaml_parser_scan_tag(parser, &token) { + return false + } + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce the SCALAR(...,literal) or SCALAR(...,folded) tokens. +func yaml_parser_fetch_block_scalar(parser *yaml_parser_t, literal bool) bool { + // Remove any potential simple keys. + if !yaml_parser_remove_simple_key(parser) { + return false + } + + // A simple key may follow a block scalar. + parser.simple_key_allowed = true + + // Create the SCALAR token and append it to the queue. + var token yaml_token_t + if !yaml_parser_scan_block_scalar(parser, &token, literal) { + return false + } + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce the SCALAR(...,single-quoted) or SCALAR(...,double-quoted) tokens. +func yaml_parser_fetch_flow_scalar(parser *yaml_parser_t, single bool) bool { + // A plain scalar could be a simple key. + if !yaml_parser_save_simple_key(parser) { + return false + } + + // A simple key cannot follow a flow scalar. + parser.simple_key_allowed = false + + // Create the SCALAR token and append it to the queue. + var token yaml_token_t + if !yaml_parser_scan_flow_scalar(parser, &token, single) { + return false + } + yaml_insert_token(parser, -1, &token) + return true +} + +// Produce the SCALAR(...,plain) token. +func yaml_parser_fetch_plain_scalar(parser *yaml_parser_t) bool { + // A plain scalar could be a simple key. + if !yaml_parser_save_simple_key(parser) { + return false + } + + // A simple key cannot follow a flow scalar. + parser.simple_key_allowed = false + + // Create the SCALAR token and append it to the queue. + var token yaml_token_t + if !yaml_parser_scan_plain_scalar(parser, &token) { + return false + } + yaml_insert_token(parser, -1, &token) + return true +} + +// Eat whitespaces and comments until the next token is found. +func yaml_parser_scan_to_next_token(parser *yaml_parser_t) bool { + + scan_mark := parser.mark + + // Until the next token is not found. + for { + // Allow the BOM mark to start a line. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + if parser.mark.column == 0 && is_bom(parser.buffer, parser.buffer_pos) { + skip(parser) + } + + // Eat whitespaces. + // Tabs are allowed: + // - in the flow context + // - in the block context, but not at the beginning of the line or + // after '-', '?', or ':' (complex value). + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + + for parser.buffer[parser.buffer_pos] == ' ' || ((parser.flow_level > 0 || !parser.simple_key_allowed) && parser.buffer[parser.buffer_pos] == '\t') { + skip(parser) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + + // Check if we just had a line comment under a sequence entry that + // looks more like a header to the following content. Similar to this: + // + // - # The comment + // - Some data + // + // If so, transform the line comment to a head comment and reposition. + if len(parser.comments) > 0 && len(parser.tokens) > 1 { + tokenA := parser.tokens[len(parser.tokens)-2] + tokenB := parser.tokens[len(parser.tokens)-1] + comment := &parser.comments[len(parser.comments)-1] + if tokenA.typ == yaml_BLOCK_SEQUENCE_START_TOKEN && tokenB.typ == yaml_BLOCK_ENTRY_TOKEN && len(comment.line) > 0 && !is_break(parser.buffer, parser.buffer_pos) { + // If it was in the prior line, reposition so it becomes a + // header of the follow up token. Otherwise, keep it in place + // so it becomes a header of the former. + comment.head = comment.line + comment.line = nil + if comment.start_mark.line == parser.mark.line-1 { + comment.token_mark = parser.mark + } + } + } + + // Eat a comment until a line break. + if parser.buffer[parser.buffer_pos] == '#' { + if !yaml_parser_scan_comments(parser, scan_mark) { + return false + } + } + + // If it is a line break, eat it. + if is_break(parser.buffer, parser.buffer_pos) { + if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) { + return false + } + skip_line(parser) + + // In the block context, a new line may start a simple key. + if parser.flow_level == 0 { + parser.simple_key_allowed = true + } + } else { + break // We have found a token. + } + } + + return true +} + +// Scan a YAML-DIRECTIVE or TAG-DIRECTIVE token. +// +// Scope: +// %YAML 1.1 # a comment \n +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +// %TAG !yaml! tag:yaml.org,2002: \n +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +// +func yaml_parser_scan_directive(parser *yaml_parser_t, token *yaml_token_t) bool { + // Eat '%'. + start_mark := parser.mark + skip(parser) + + // Scan the directive name. + var name []byte + if !yaml_parser_scan_directive_name(parser, start_mark, &name) { + return false + } + + // Is it a YAML directive? + if bytes.Equal(name, []byte("YAML")) { + // Scan the VERSION directive value. + var major, minor int8 + if !yaml_parser_scan_version_directive_value(parser, start_mark, &major, &minor) { + return false + } + end_mark := parser.mark + + // Create a VERSION-DIRECTIVE token. + *token = yaml_token_t{ + typ: yaml_VERSION_DIRECTIVE_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + major: major, + minor: minor, + } + + // Is it a TAG directive? + } else if bytes.Equal(name, []byte("TAG")) { + // Scan the TAG directive value. + var handle, prefix []byte + if !yaml_parser_scan_tag_directive_value(parser, start_mark, &handle, &prefix) { + return false + } + end_mark := parser.mark + + // Create a TAG-DIRECTIVE token. + *token = yaml_token_t{ + typ: yaml_TAG_DIRECTIVE_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + value: handle, + prefix: prefix, + } + + // Unknown directive. + } else { + yaml_parser_set_scanner_error(parser, "while scanning a directive", + start_mark, "found unknown directive name") + return false + } + + // Eat the rest of the line including any comments. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + + for is_blank(parser.buffer, parser.buffer_pos) { + skip(parser) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + + if parser.buffer[parser.buffer_pos] == '#' { + // [Go] Discard this inline comment for the time being. + //if !yaml_parser_scan_line_comment(parser, start_mark) { + // return false + //} + for !is_breakz(parser.buffer, parser.buffer_pos) { + skip(parser) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + } + + // Check if we are at the end of the line. + if !is_breakz(parser.buffer, parser.buffer_pos) { + yaml_parser_set_scanner_error(parser, "while scanning a directive", + start_mark, "did not find expected comment or line break") + return false + } + + // Eat a line break. + if is_break(parser.buffer, parser.buffer_pos) { + if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) { + return false + } + skip_line(parser) + } + + return true +} + +// Scan the directive name. +// +// Scope: +// %YAML 1.1 # a comment \n +// ^^^^ +// %TAG !yaml! tag:yaml.org,2002: \n +// ^^^ +// +func yaml_parser_scan_directive_name(parser *yaml_parser_t, start_mark yaml_mark_t, name *[]byte) bool { + // Consume the directive name. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + + var s []byte + for is_alpha(parser.buffer, parser.buffer_pos) { + s = read(parser, s) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + + // Check if the name is empty. + if len(s) == 0 { + yaml_parser_set_scanner_error(parser, "while scanning a directive", + start_mark, "could not find expected directive name") + return false + } + + // Check for an blank character after the name. + if !is_blankz(parser.buffer, parser.buffer_pos) { + yaml_parser_set_scanner_error(parser, "while scanning a directive", + start_mark, "found unexpected non-alphabetical character") + return false + } + *name = s + return true +} + +// Scan the value of VERSION-DIRECTIVE. +// +// Scope: +// %YAML 1.1 # a comment \n +// ^^^^^^ +func yaml_parser_scan_version_directive_value(parser *yaml_parser_t, start_mark yaml_mark_t, major, minor *int8) bool { + // Eat whitespaces. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + for is_blank(parser.buffer, parser.buffer_pos) { + skip(parser) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + + // Consume the major version number. + if !yaml_parser_scan_version_directive_number(parser, start_mark, major) { + return false + } + + // Eat '.'. + if parser.buffer[parser.buffer_pos] != '.' { + return yaml_parser_set_scanner_error(parser, "while scanning a %YAML directive", + start_mark, "did not find expected digit or '.' character") + } + + skip(parser) + + // Consume the minor version number. + if !yaml_parser_scan_version_directive_number(parser, start_mark, minor) { + return false + } + return true +} + +const max_number_length = 2 + +// Scan the version number of VERSION-DIRECTIVE. +// +// Scope: +// %YAML 1.1 # a comment \n +// ^ +// %YAML 1.1 # a comment \n +// ^ +func yaml_parser_scan_version_directive_number(parser *yaml_parser_t, start_mark yaml_mark_t, number *int8) bool { + + // Repeat while the next character is digit. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + var value, length int8 + for is_digit(parser.buffer, parser.buffer_pos) { + // Check if the number is too long. + length++ + if length > max_number_length { + return yaml_parser_set_scanner_error(parser, "while scanning a %YAML directive", + start_mark, "found extremely long version number") + } + value = value*10 + int8(as_digit(parser.buffer, parser.buffer_pos)) + skip(parser) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + + // Check if the number was present. + if length == 0 { + return yaml_parser_set_scanner_error(parser, "while scanning a %YAML directive", + start_mark, "did not find expected version number") + } + *number = value + return true +} + +// Scan the value of a TAG-DIRECTIVE token. +// +// Scope: +// %TAG !yaml! tag:yaml.org,2002: \n +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +// +func yaml_parser_scan_tag_directive_value(parser *yaml_parser_t, start_mark yaml_mark_t, handle, prefix *[]byte) bool { + var handle_value, prefix_value []byte + + // Eat whitespaces. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + + for is_blank(parser.buffer, parser.buffer_pos) { + skip(parser) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + + // Scan a handle. + if !yaml_parser_scan_tag_handle(parser, true, start_mark, &handle_value) { + return false + } + + // Expect a whitespace. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + if !is_blank(parser.buffer, parser.buffer_pos) { + yaml_parser_set_scanner_error(parser, "while scanning a %TAG directive", + start_mark, "did not find expected whitespace") + return false + } + + // Eat whitespaces. + for is_blank(parser.buffer, parser.buffer_pos) { + skip(parser) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + + // Scan a prefix. + if !yaml_parser_scan_tag_uri(parser, true, nil, start_mark, &prefix_value) { + return false + } + + // Expect a whitespace or line break. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + if !is_blankz(parser.buffer, parser.buffer_pos) { + yaml_parser_set_scanner_error(parser, "while scanning a %TAG directive", + start_mark, "did not find expected whitespace or line break") + return false + } + + *handle = handle_value + *prefix = prefix_value + return true +} + +func yaml_parser_scan_anchor(parser *yaml_parser_t, token *yaml_token_t, typ yaml_token_type_t) bool { + var s []byte + + // Eat the indicator character. + start_mark := parser.mark + skip(parser) + + // Consume the value. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + + for is_alpha(parser.buffer, parser.buffer_pos) { + s = read(parser, s) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + + end_mark := parser.mark + + /* + * Check if length of the anchor is greater than 0 and it is followed by + * a whitespace character or one of the indicators: + * + * '?', ':', ',', ']', '}', '%', '@', '`'. + */ + + if len(s) == 0 || + !(is_blankz(parser.buffer, parser.buffer_pos) || parser.buffer[parser.buffer_pos] == '?' || + parser.buffer[parser.buffer_pos] == ':' || parser.buffer[parser.buffer_pos] == ',' || + parser.buffer[parser.buffer_pos] == ']' || parser.buffer[parser.buffer_pos] == '}' || + parser.buffer[parser.buffer_pos] == '%' || parser.buffer[parser.buffer_pos] == '@' || + parser.buffer[parser.buffer_pos] == '`') { + context := "while scanning an alias" + if typ == yaml_ANCHOR_TOKEN { + context = "while scanning an anchor" + } + yaml_parser_set_scanner_error(parser, context, start_mark, + "did not find expected alphabetic or numeric character") + return false + } + + // Create a token. + *token = yaml_token_t{ + typ: typ, + start_mark: start_mark, + end_mark: end_mark, + value: s, + } + + return true +} + +/* + * Scan a TAG token. + */ + +func yaml_parser_scan_tag(parser *yaml_parser_t, token *yaml_token_t) bool { + var handle, suffix []byte + + start_mark := parser.mark + + // Check if the tag is in the canonical form. + if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) { + return false + } + + if parser.buffer[parser.buffer_pos+1] == '<' { + // Keep the handle as '' + + // Eat '!<' + skip(parser) + skip(parser) + + // Consume the tag value. + if !yaml_parser_scan_tag_uri(parser, false, nil, start_mark, &suffix) { + return false + } + + // Check for '>' and eat it. + if parser.buffer[parser.buffer_pos] != '>' { + yaml_parser_set_scanner_error(parser, "while scanning a tag", + start_mark, "did not find the expected '>'") + return false + } + + skip(parser) + } else { + // The tag has either the '!suffix' or the '!handle!suffix' form. + + // First, try to scan a handle. + if !yaml_parser_scan_tag_handle(parser, false, start_mark, &handle) { + return false + } + + // Check if it is, indeed, handle. + if handle[0] == '!' && len(handle) > 1 && handle[len(handle)-1] == '!' { + // Scan the suffix now. + if !yaml_parser_scan_tag_uri(parser, false, nil, start_mark, &suffix) { + return false + } + } else { + // It wasn't a handle after all. Scan the rest of the tag. + if !yaml_parser_scan_tag_uri(parser, false, handle, start_mark, &suffix) { + return false + } + + // Set the handle to '!'. + handle = []byte{'!'} + + // A special case: the '!' tag. Set the handle to '' and the + // suffix to '!'. + if len(suffix) == 0 { + handle, suffix = suffix, handle + } + } + } + + // Check the character which ends the tag. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + if !is_blankz(parser.buffer, parser.buffer_pos) { + yaml_parser_set_scanner_error(parser, "while scanning a tag", + start_mark, "did not find expected whitespace or line break") + return false + } + + end_mark := parser.mark + + // Create a token. + *token = yaml_token_t{ + typ: yaml_TAG_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + value: handle, + suffix: suffix, + } + return true +} + +// Scan a tag handle. +func yaml_parser_scan_tag_handle(parser *yaml_parser_t, directive bool, start_mark yaml_mark_t, handle *[]byte) bool { + // Check the initial '!' character. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + if parser.buffer[parser.buffer_pos] != '!' { + yaml_parser_set_scanner_tag_error(parser, directive, + start_mark, "did not find expected '!'") + return false + } + + var s []byte + + // Copy the '!' character. + s = read(parser, s) + + // Copy all subsequent alphabetical and numerical characters. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + for is_alpha(parser.buffer, parser.buffer_pos) { + s = read(parser, s) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + + // Check if the trailing character is '!' and copy it. + if parser.buffer[parser.buffer_pos] == '!' { + s = read(parser, s) + } else { + // It's either the '!' tag or not really a tag handle. If it's a %TAG + // directive, it's an error. If it's a tag token, it must be a part of URI. + if directive && string(s) != "!" { + yaml_parser_set_scanner_tag_error(parser, directive, + start_mark, "did not find expected '!'") + return false + } + } + + *handle = s + return true +} + +// Scan a tag. +func yaml_parser_scan_tag_uri(parser *yaml_parser_t, directive bool, head []byte, start_mark yaml_mark_t, uri *[]byte) bool { + //size_t length = head ? strlen((char *)head) : 0 + var s []byte + hasTag := len(head) > 0 + + // Copy the head if needed. + // + // Note that we don't copy the leading '!' character. + if len(head) > 1 { + s = append(s, head[1:]...) + } + + // Scan the tag. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + + // The set of characters that may appear in URI is as follows: + // + // '0'-'9', 'A'-'Z', 'a'-'z', '_', '-', ';', '/', '?', ':', '@', '&', + // '=', '+', '$', ',', '.', '!', '~', '*', '\'', '(', ')', '[', ']', + // '%'. + // [Go] TODO Convert this into more reasonable logic. + for is_alpha(parser.buffer, parser.buffer_pos) || parser.buffer[parser.buffer_pos] == ';' || + parser.buffer[parser.buffer_pos] == '/' || parser.buffer[parser.buffer_pos] == '?' || + parser.buffer[parser.buffer_pos] == ':' || parser.buffer[parser.buffer_pos] == '@' || + parser.buffer[parser.buffer_pos] == '&' || parser.buffer[parser.buffer_pos] == '=' || + parser.buffer[parser.buffer_pos] == '+' || parser.buffer[parser.buffer_pos] == '$' || + parser.buffer[parser.buffer_pos] == ',' || parser.buffer[parser.buffer_pos] == '.' || + parser.buffer[parser.buffer_pos] == '!' || parser.buffer[parser.buffer_pos] == '~' || + parser.buffer[parser.buffer_pos] == '*' || parser.buffer[parser.buffer_pos] == '\'' || + parser.buffer[parser.buffer_pos] == '(' || parser.buffer[parser.buffer_pos] == ')' || + parser.buffer[parser.buffer_pos] == '[' || parser.buffer[parser.buffer_pos] == ']' || + parser.buffer[parser.buffer_pos] == '%' { + // Check if it is a URI-escape sequence. + if parser.buffer[parser.buffer_pos] == '%' { + if !yaml_parser_scan_uri_escapes(parser, directive, start_mark, &s) { + return false + } + } else { + s = read(parser, s) + } + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + hasTag = true + } + + if !hasTag { + yaml_parser_set_scanner_tag_error(parser, directive, + start_mark, "did not find expected tag URI") + return false + } + *uri = s + return true +} + +// Decode an URI-escape sequence corresponding to a single UTF-8 character. +func yaml_parser_scan_uri_escapes(parser *yaml_parser_t, directive bool, start_mark yaml_mark_t, s *[]byte) bool { + + // Decode the required number of characters. + w := 1024 + for w > 0 { + // Check for a URI-escaped octet. + if parser.unread < 3 && !yaml_parser_update_buffer(parser, 3) { + return false + } + + if !(parser.buffer[parser.buffer_pos] == '%' && + is_hex(parser.buffer, parser.buffer_pos+1) && + is_hex(parser.buffer, parser.buffer_pos+2)) { + return yaml_parser_set_scanner_tag_error(parser, directive, + start_mark, "did not find URI escaped octet") + } + + // Get the octet. + octet := byte((as_hex(parser.buffer, parser.buffer_pos+1) << 4) + as_hex(parser.buffer, parser.buffer_pos+2)) + + // If it is the leading octet, determine the length of the UTF-8 sequence. + if w == 1024 { + w = width(octet) + if w == 0 { + return yaml_parser_set_scanner_tag_error(parser, directive, + start_mark, "found an incorrect leading UTF-8 octet") + } + } else { + // Check if the trailing octet is correct. + if octet&0xC0 != 0x80 { + return yaml_parser_set_scanner_tag_error(parser, directive, + start_mark, "found an incorrect trailing UTF-8 octet") + } + } + + // Copy the octet and move the pointers. + *s = append(*s, octet) + skip(parser) + skip(parser) + skip(parser) + w-- + } + return true +} + +// Scan a block scalar. +func yaml_parser_scan_block_scalar(parser *yaml_parser_t, token *yaml_token_t, literal bool) bool { + // Eat the indicator '|' or '>'. + start_mark := parser.mark + skip(parser) + + // Scan the additional block scalar indicators. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + + // Check for a chomping indicator. + var chomping, increment int + if parser.buffer[parser.buffer_pos] == '+' || parser.buffer[parser.buffer_pos] == '-' { + // Set the chomping method and eat the indicator. + if parser.buffer[parser.buffer_pos] == '+' { + chomping = +1 + } else { + chomping = -1 + } + skip(parser) + + // Check for an indentation indicator. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + if is_digit(parser.buffer, parser.buffer_pos) { + // Check that the indentation is greater than 0. + if parser.buffer[parser.buffer_pos] == '0' { + yaml_parser_set_scanner_error(parser, "while scanning a block scalar", + start_mark, "found an indentation indicator equal to 0") + return false + } + + // Get the indentation level and eat the indicator. + increment = as_digit(parser.buffer, parser.buffer_pos) + skip(parser) + } + + } else if is_digit(parser.buffer, parser.buffer_pos) { + // Do the same as above, but in the opposite order. + + if parser.buffer[parser.buffer_pos] == '0' { + yaml_parser_set_scanner_error(parser, "while scanning a block scalar", + start_mark, "found an indentation indicator equal to 0") + return false + } + increment = as_digit(parser.buffer, parser.buffer_pos) + skip(parser) + + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + if parser.buffer[parser.buffer_pos] == '+' || parser.buffer[parser.buffer_pos] == '-' { + if parser.buffer[parser.buffer_pos] == '+' { + chomping = +1 + } else { + chomping = -1 + } + skip(parser) + } + } + + // Eat whitespaces and comments to the end of the line. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + for is_blank(parser.buffer, parser.buffer_pos) { + skip(parser) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + if parser.buffer[parser.buffer_pos] == '#' { + if !yaml_parser_scan_line_comment(parser, start_mark) { + return false + } + for !is_breakz(parser.buffer, parser.buffer_pos) { + skip(parser) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + } + + // Check if we are at the end of the line. + if !is_breakz(parser.buffer, parser.buffer_pos) { + yaml_parser_set_scanner_error(parser, "while scanning a block scalar", + start_mark, "did not find expected comment or line break") + return false + } + + // Eat a line break. + if is_break(parser.buffer, parser.buffer_pos) { + if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) { + return false + } + skip_line(parser) + } + + end_mark := parser.mark + + // Set the indentation level if it was specified. + var indent int + if increment > 0 { + if parser.indent >= 0 { + indent = parser.indent + increment + } else { + indent = increment + } + } + + // Scan the leading line breaks and determine the indentation level if needed. + var s, leading_break, trailing_breaks []byte + if !yaml_parser_scan_block_scalar_breaks(parser, &indent, &trailing_breaks, start_mark, &end_mark) { + return false + } + + // Scan the block scalar content. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + var leading_blank, trailing_blank bool + for parser.mark.column == indent && !is_z(parser.buffer, parser.buffer_pos) { + // We are at the beginning of a non-empty line. + + // Is it a trailing whitespace? + trailing_blank = is_blank(parser.buffer, parser.buffer_pos) + + // Check if we need to fold the leading line break. + if !literal && !leading_blank && !trailing_blank && len(leading_break) > 0 && leading_break[0] == '\n' { + // Do we need to join the lines by space? + if len(trailing_breaks) == 0 { + s = append(s, ' ') + } + } else { + s = append(s, leading_break...) + } + leading_break = leading_break[:0] + + // Append the remaining line breaks. + s = append(s, trailing_breaks...) + trailing_breaks = trailing_breaks[:0] + + // Is it a leading whitespace? + leading_blank = is_blank(parser.buffer, parser.buffer_pos) + + // Consume the current line. + for !is_breakz(parser.buffer, parser.buffer_pos) { + s = read(parser, s) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + + // Consume the line break. + if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) { + return false + } + + leading_break = read_line(parser, leading_break) + + // Eat the following indentation spaces and line breaks. + if !yaml_parser_scan_block_scalar_breaks(parser, &indent, &trailing_breaks, start_mark, &end_mark) { + return false + } + } + + // Chomp the tail. + if chomping != -1 { + s = append(s, leading_break...) + } + if chomping == 1 { + s = append(s, trailing_breaks...) + } + + // Create a token. + *token = yaml_token_t{ + typ: yaml_SCALAR_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + value: s, + style: yaml_LITERAL_SCALAR_STYLE, + } + if !literal { + token.style = yaml_FOLDED_SCALAR_STYLE + } + return true +} + +// Scan indentation spaces and line breaks for a block scalar. Determine the +// indentation level if needed. +func yaml_parser_scan_block_scalar_breaks(parser *yaml_parser_t, indent *int, breaks *[]byte, start_mark yaml_mark_t, end_mark *yaml_mark_t) bool { + *end_mark = parser.mark + + // Eat the indentation spaces and line breaks. + max_indent := 0 + for { + // Eat the indentation spaces. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + for (*indent == 0 || parser.mark.column < *indent) && is_space(parser.buffer, parser.buffer_pos) { + skip(parser) + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + if parser.mark.column > max_indent { + max_indent = parser.mark.column + } + + // Check for a tab character messing the indentation. + if (*indent == 0 || parser.mark.column < *indent) && is_tab(parser.buffer, parser.buffer_pos) { + return yaml_parser_set_scanner_error(parser, "while scanning a block scalar", + start_mark, "found a tab character where an indentation space is expected") + } + + // Have we found a non-empty line? + if !is_break(parser.buffer, parser.buffer_pos) { + break + } + + // Consume the line break. + if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) { + return false + } + // [Go] Should really be returning breaks instead. + *breaks = read_line(parser, *breaks) + *end_mark = parser.mark + } + + // Determine the indentation level if needed. + if *indent == 0 { + *indent = max_indent + if *indent < parser.indent+1 { + *indent = parser.indent + 1 + } + if *indent < 1 { + *indent = 1 + } + } + return true +} + +// Scan a quoted scalar. +func yaml_parser_scan_flow_scalar(parser *yaml_parser_t, token *yaml_token_t, single bool) bool { + // Eat the left quote. + start_mark := parser.mark + skip(parser) + + // Consume the content of the quoted scalar. + var s, leading_break, trailing_breaks, whitespaces []byte + for { + // Check that there are no document indicators at the beginning of the line. + if parser.unread < 4 && !yaml_parser_update_buffer(parser, 4) { + return false + } + + if parser.mark.column == 0 && + ((parser.buffer[parser.buffer_pos+0] == '-' && + parser.buffer[parser.buffer_pos+1] == '-' && + parser.buffer[parser.buffer_pos+2] == '-') || + (parser.buffer[parser.buffer_pos+0] == '.' && + parser.buffer[parser.buffer_pos+1] == '.' && + parser.buffer[parser.buffer_pos+2] == '.')) && + is_blankz(parser.buffer, parser.buffer_pos+3) { + yaml_parser_set_scanner_error(parser, "while scanning a quoted scalar", + start_mark, "found unexpected document indicator") + return false + } + + // Check for EOF. + if is_z(parser.buffer, parser.buffer_pos) { + yaml_parser_set_scanner_error(parser, "while scanning a quoted scalar", + start_mark, "found unexpected end of stream") + return false + } + + // Consume non-blank characters. + leading_blanks := false + for !is_blankz(parser.buffer, parser.buffer_pos) { + if single && parser.buffer[parser.buffer_pos] == '\'' && parser.buffer[parser.buffer_pos+1] == '\'' { + // Is is an escaped single quote. + s = append(s, '\'') + skip(parser) + skip(parser) + + } else if single && parser.buffer[parser.buffer_pos] == '\'' { + // It is a right single quote. + break + } else if !single && parser.buffer[parser.buffer_pos] == '"' { + // It is a right double quote. + break + + } else if !single && parser.buffer[parser.buffer_pos] == '\\' && is_break(parser.buffer, parser.buffer_pos+1) { + // It is an escaped line break. + if parser.unread < 3 && !yaml_parser_update_buffer(parser, 3) { + return false + } + skip(parser) + skip_line(parser) + leading_blanks = true + break + + } else if !single && parser.buffer[parser.buffer_pos] == '\\' { + // It is an escape sequence. + code_length := 0 + + // Check the escape character. + switch parser.buffer[parser.buffer_pos+1] { + case '0': + s = append(s, 0) + case 'a': + s = append(s, '\x07') + case 'b': + s = append(s, '\x08') + case 't', '\t': + s = append(s, '\x09') + case 'n': + s = append(s, '\x0A') + case 'v': + s = append(s, '\x0B') + case 'f': + s = append(s, '\x0C') + case 'r': + s = append(s, '\x0D') + case 'e': + s = append(s, '\x1B') + case ' ': + s = append(s, '\x20') + case '"': + s = append(s, '"') + case '\'': + s = append(s, '\'') + case '\\': + s = append(s, '\\') + case 'N': // NEL (#x85) + s = append(s, '\xC2') + s = append(s, '\x85') + case '_': // #xA0 + s = append(s, '\xC2') + s = append(s, '\xA0') + case 'L': // LS (#x2028) + s = append(s, '\xE2') + s = append(s, '\x80') + s = append(s, '\xA8') + case 'P': // PS (#x2029) + s = append(s, '\xE2') + s = append(s, '\x80') + s = append(s, '\xA9') + case 'x': + code_length = 2 + case 'u': + code_length = 4 + case 'U': + code_length = 8 + default: + yaml_parser_set_scanner_error(parser, "while parsing a quoted scalar", + start_mark, "found unknown escape character") + return false + } + + skip(parser) + skip(parser) + + // Consume an arbitrary escape code. + if code_length > 0 { + var value int + + // Scan the character value. + if parser.unread < code_length && !yaml_parser_update_buffer(parser, code_length) { + return false + } + for k := 0; k < code_length; k++ { + if !is_hex(parser.buffer, parser.buffer_pos+k) { + yaml_parser_set_scanner_error(parser, "while parsing a quoted scalar", + start_mark, "did not find expected hexdecimal number") + return false + } + value = (value << 4) + as_hex(parser.buffer, parser.buffer_pos+k) + } + + // Check the value and write the character. + if (value >= 0xD800 && value <= 0xDFFF) || value > 0x10FFFF { + yaml_parser_set_scanner_error(parser, "while parsing a quoted scalar", + start_mark, "found invalid Unicode character escape code") + return false + } + if value <= 0x7F { + s = append(s, byte(value)) + } else if value <= 0x7FF { + s = append(s, byte(0xC0+(value>>6))) + s = append(s, byte(0x80+(value&0x3F))) + } else if value <= 0xFFFF { + s = append(s, byte(0xE0+(value>>12))) + s = append(s, byte(0x80+((value>>6)&0x3F))) + s = append(s, byte(0x80+(value&0x3F))) + } else { + s = append(s, byte(0xF0+(value>>18))) + s = append(s, byte(0x80+((value>>12)&0x3F))) + s = append(s, byte(0x80+((value>>6)&0x3F))) + s = append(s, byte(0x80+(value&0x3F))) + } + + // Advance the pointer. + for k := 0; k < code_length; k++ { + skip(parser) + } + } + } else { + // It is a non-escaped non-blank character. + s = read(parser, s) + } + if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) { + return false + } + } + + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + + // Check if we are at the end of the scalar. + if single { + if parser.buffer[parser.buffer_pos] == '\'' { + break + } + } else { + if parser.buffer[parser.buffer_pos] == '"' { + break + } + } + + // Consume blank characters. + for is_blank(parser.buffer, parser.buffer_pos) || is_break(parser.buffer, parser.buffer_pos) { + if is_blank(parser.buffer, parser.buffer_pos) { + // Consume a space or a tab character. + if !leading_blanks { + whitespaces = read(parser, whitespaces) + } else { + skip(parser) + } + } else { + if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) { + return false + } + + // Check if it is a first line break. + if !leading_blanks { + whitespaces = whitespaces[:0] + leading_break = read_line(parser, leading_break) + leading_blanks = true + } else { + trailing_breaks = read_line(parser, trailing_breaks) + } + } + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + + // Join the whitespaces or fold line breaks. + if leading_blanks { + // Do we need to fold line breaks? + if len(leading_break) > 0 && leading_break[0] == '\n' { + if len(trailing_breaks) == 0 { + s = append(s, ' ') + } else { + s = append(s, trailing_breaks...) + } + } else { + s = append(s, leading_break...) + s = append(s, trailing_breaks...) + } + trailing_breaks = trailing_breaks[:0] + leading_break = leading_break[:0] + } else { + s = append(s, whitespaces...) + whitespaces = whitespaces[:0] + } + } + + // Eat the right quote. + skip(parser) + end_mark := parser.mark + + // Create a token. + *token = yaml_token_t{ + typ: yaml_SCALAR_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + value: s, + style: yaml_SINGLE_QUOTED_SCALAR_STYLE, + } + if !single { + token.style = yaml_DOUBLE_QUOTED_SCALAR_STYLE + } + return true +} + +// Scan a plain scalar. +func yaml_parser_scan_plain_scalar(parser *yaml_parser_t, token *yaml_token_t) bool { + + var s, leading_break, trailing_breaks, whitespaces []byte + var leading_blanks bool + var indent = parser.indent + 1 + + start_mark := parser.mark + end_mark := parser.mark + + // Consume the content of the plain scalar. + for { + // Check for a document indicator. + if parser.unread < 4 && !yaml_parser_update_buffer(parser, 4) { + return false + } + if parser.mark.column == 0 && + ((parser.buffer[parser.buffer_pos+0] == '-' && + parser.buffer[parser.buffer_pos+1] == '-' && + parser.buffer[parser.buffer_pos+2] == '-') || + (parser.buffer[parser.buffer_pos+0] == '.' && + parser.buffer[parser.buffer_pos+1] == '.' && + parser.buffer[parser.buffer_pos+2] == '.')) && + is_blankz(parser.buffer, parser.buffer_pos+3) { + break + } + + // Check for a comment. + if parser.buffer[parser.buffer_pos] == '#' { + break + } + + // Consume non-blank characters. + for !is_blankz(parser.buffer, parser.buffer_pos) { + + // Check for indicators that may end a plain scalar. + if (parser.buffer[parser.buffer_pos] == ':' && is_blankz(parser.buffer, parser.buffer_pos+1)) || + (parser.flow_level > 0 && + (parser.buffer[parser.buffer_pos] == ',' || + parser.buffer[parser.buffer_pos] == '?' || parser.buffer[parser.buffer_pos] == '[' || + parser.buffer[parser.buffer_pos] == ']' || parser.buffer[parser.buffer_pos] == '{' || + parser.buffer[parser.buffer_pos] == '}')) { + break + } + + // Check if we need to join whitespaces and breaks. + if leading_blanks || len(whitespaces) > 0 { + if leading_blanks { + // Do we need to fold line breaks? + if leading_break[0] == '\n' { + if len(trailing_breaks) == 0 { + s = append(s, ' ') + } else { + s = append(s, trailing_breaks...) + } + } else { + s = append(s, leading_break...) + s = append(s, trailing_breaks...) + } + trailing_breaks = trailing_breaks[:0] + leading_break = leading_break[:0] + leading_blanks = false + } else { + s = append(s, whitespaces...) + whitespaces = whitespaces[:0] + } + } + + // Copy the character. + s = read(parser, s) + + end_mark = parser.mark + if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) { + return false + } + } + + // Is it the end? + if !(is_blank(parser.buffer, parser.buffer_pos) || is_break(parser.buffer, parser.buffer_pos)) { + break + } + + // Consume blank characters. + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + + for is_blank(parser.buffer, parser.buffer_pos) || is_break(parser.buffer, parser.buffer_pos) { + if is_blank(parser.buffer, parser.buffer_pos) { + + // Check for tab characters that abuse indentation. + if leading_blanks && parser.mark.column < indent && is_tab(parser.buffer, parser.buffer_pos) { + yaml_parser_set_scanner_error(parser, "while scanning a plain scalar", + start_mark, "found a tab character that violates indentation") + return false + } + + // Consume a space or a tab character. + if !leading_blanks { + whitespaces = read(parser, whitespaces) + } else { + skip(parser) + } + } else { + if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) { + return false + } + + // Check if it is a first line break. + if !leading_blanks { + whitespaces = whitespaces[:0] + leading_break = read_line(parser, leading_break) + leading_blanks = true + } else { + trailing_breaks = read_line(parser, trailing_breaks) + } + } + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + } + + // Check indentation level. + if parser.flow_level == 0 && parser.mark.column < indent { + break + } + } + + // Create a token. + *token = yaml_token_t{ + typ: yaml_SCALAR_TOKEN, + start_mark: start_mark, + end_mark: end_mark, + value: s, + style: yaml_PLAIN_SCALAR_STYLE, + } + + // Note that we change the 'simple_key_allowed' flag. + if leading_blanks { + parser.simple_key_allowed = true + } + return true +} + +func yaml_parser_scan_line_comment(parser *yaml_parser_t, token_mark yaml_mark_t) bool { + if parser.newlines > 0 { + return true + } + + var start_mark yaml_mark_t + var text []byte + + for peek := 0; peek < 512; peek++ { + if parser.unread < peek+1 && !yaml_parser_update_buffer(parser, peek+1) { + break + } + if is_blank(parser.buffer, parser.buffer_pos+peek) { + continue + } + if parser.buffer[parser.buffer_pos+peek] == '#' { + seen := parser.mark.index+peek + for { + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + if is_breakz(parser.buffer, parser.buffer_pos) { + if parser.mark.index >= seen { + break + } + if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) { + return false + } + skip_line(parser) + } else if parser.mark.index >= seen { + if len(text) == 0 { + start_mark = parser.mark + } + text = read(parser, text) + } else { + skip(parser) + } + } + } + break + } + if len(text) > 0 { + parser.comments = append(parser.comments, yaml_comment_t{ + token_mark: token_mark, + start_mark: start_mark, + line: text, + }) + } + return true +} + +func yaml_parser_scan_comments(parser *yaml_parser_t, scan_mark yaml_mark_t) bool { + token := parser.tokens[len(parser.tokens)-1] + + if token.typ == yaml_FLOW_ENTRY_TOKEN && len(parser.tokens) > 1 { + token = parser.tokens[len(parser.tokens)-2] + } + + var token_mark = token.start_mark + var start_mark yaml_mark_t + var next_indent = parser.indent + if next_indent < 0 { + next_indent = 0 + } + + var recent_empty = false + var first_empty = parser.newlines <= 1 + + var line = parser.mark.line + var column = parser.mark.column + + var text []byte + + // The foot line is the place where a comment must start to + // still be considered as a foot of the prior content. + // If there's some content in the currently parsed line, then + // the foot is the line below it. + var foot_line = -1 + if scan_mark.line > 0 { + foot_line = parser.mark.line-parser.newlines+1 + if parser.newlines == 0 && parser.mark.column > 1 { + foot_line++ + } + } + + var peek = 0 + for ; peek < 512; peek++ { + if parser.unread < peek+1 && !yaml_parser_update_buffer(parser, peek+1) { + break + } + column++ + if is_blank(parser.buffer, parser.buffer_pos+peek) { + continue + } + c := parser.buffer[parser.buffer_pos+peek] + var close_flow = parser.flow_level > 0 && (c == ']' || c == '}') + if close_flow || is_breakz(parser.buffer, parser.buffer_pos+peek) { + // Got line break or terminator. + if close_flow || !recent_empty { + if close_flow || first_empty && (start_mark.line == foot_line && token.typ != yaml_VALUE_TOKEN || start_mark.column-1 < next_indent) { + // This is the first empty line and there were no empty lines before, + // so this initial part of the comment is a foot of the prior token + // instead of being a head for the following one. Split it up. + // Alternatively, this might also be the last comment inside a flow + // scope, so it must be a footer. + if len(text) > 0 { + if start_mark.column-1 < next_indent { + // If dedented it's unrelated to the prior token. + token_mark = start_mark + } + parser.comments = append(parser.comments, yaml_comment_t{ + scan_mark: scan_mark, + token_mark: token_mark, + start_mark: start_mark, + end_mark: yaml_mark_t{parser.mark.index + peek, line, column}, + foot: text, + }) + scan_mark = yaml_mark_t{parser.mark.index + peek, line, column} + token_mark = scan_mark + text = nil + } + } else { + if len(text) > 0 && parser.buffer[parser.buffer_pos+peek] != 0 { + text = append(text, '\n') + } + } + } + if !is_break(parser.buffer, parser.buffer_pos+peek) { + break + } + first_empty = false + recent_empty = true + column = 0 + line++ + continue + } + + if len(text) > 0 && (close_flow || column-1 < next_indent && column != start_mark.column) { + // The comment at the different indentation is a foot of the + // preceding data rather than a head of the upcoming one. + parser.comments = append(parser.comments, yaml_comment_t{ + scan_mark: scan_mark, + token_mark: token_mark, + start_mark: start_mark, + end_mark: yaml_mark_t{parser.mark.index + peek, line, column}, + foot: text, + }) + scan_mark = yaml_mark_t{parser.mark.index + peek, line, column} + token_mark = scan_mark + text = nil + } + + if parser.buffer[parser.buffer_pos+peek] != '#' { + break + } + + if len(text) == 0 { + start_mark = yaml_mark_t{parser.mark.index + peek, line, column} + } else { + text = append(text, '\n') + } + + recent_empty = false + + // Consume until after the consumed comment line. + seen := parser.mark.index+peek + for { + if parser.unread < 1 && !yaml_parser_update_buffer(parser, 1) { + return false + } + if is_breakz(parser.buffer, parser.buffer_pos) { + if parser.mark.index >= seen { + break + } + if parser.unread < 2 && !yaml_parser_update_buffer(parser, 2) { + return false + } + skip_line(parser) + } else if parser.mark.index >= seen { + text = read(parser, text) + } else { + skip(parser) + } + } + + peek = 0 + column = 0 + line = parser.mark.line + next_indent = parser.indent + if next_indent < 0 { + next_indent = 0 + } + } + + if len(text) > 0 { + parser.comments = append(parser.comments, yaml_comment_t{ + scan_mark: scan_mark, + token_mark: start_mark, + start_mark: start_mark, + end_mark: yaml_mark_t{parser.mark.index + peek - 1, line, column}, + head: text, + }) + } + return true +} diff --git a/asyncapi/vendor/gopkg.in/yaml.v3/sorter.go b/asyncapi/vendor/gopkg.in/yaml.v3/sorter.go new file mode 100644 index 0000000..9210ece --- /dev/null +++ b/asyncapi/vendor/gopkg.in/yaml.v3/sorter.go @@ -0,0 +1,134 @@ +// +// Copyright (c) 2011-2019 Canonical Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package yaml + +import ( + "reflect" + "unicode" +) + +type keyList []reflect.Value + +func (l keyList) Len() int { return len(l) } +func (l keyList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } +func (l keyList) Less(i, j int) bool { + a := l[i] + b := l[j] + ak := a.Kind() + bk := b.Kind() + for (ak == reflect.Interface || ak == reflect.Ptr) && !a.IsNil() { + a = a.Elem() + ak = a.Kind() + } + for (bk == reflect.Interface || bk == reflect.Ptr) && !b.IsNil() { + b = b.Elem() + bk = b.Kind() + } + af, aok := keyFloat(a) + bf, bok := keyFloat(b) + if aok && bok { + if af != bf { + return af < bf + } + if ak != bk { + return ak < bk + } + return numLess(a, b) + } + if ak != reflect.String || bk != reflect.String { + return ak < bk + } + ar, br := []rune(a.String()), []rune(b.String()) + digits := false + for i := 0; i < len(ar) && i < len(br); i++ { + if ar[i] == br[i] { + digits = unicode.IsDigit(ar[i]) + continue + } + al := unicode.IsLetter(ar[i]) + bl := unicode.IsLetter(br[i]) + if al && bl { + return ar[i] < br[i] + } + if al || bl { + if digits { + return al + } else { + return bl + } + } + var ai, bi int + var an, bn int64 + if ar[i] == '0' || br[i] == '0' { + for j := i - 1; j >= 0 && unicode.IsDigit(ar[j]); j-- { + if ar[j] != '0' { + an = 1 + bn = 1 + break + } + } + } + for ai = i; ai < len(ar) && unicode.IsDigit(ar[ai]); ai++ { + an = an*10 + int64(ar[ai]-'0') + } + for bi = i; bi < len(br) && unicode.IsDigit(br[bi]); bi++ { + bn = bn*10 + int64(br[bi]-'0') + } + if an != bn { + return an < bn + } + if ai != bi { + return ai < bi + } + return ar[i] < br[i] + } + return len(ar) < len(br) +} + +// keyFloat returns a float value for v if it is a number/bool +// and whether it is a number/bool or not. +func keyFloat(v reflect.Value) (f float64, ok bool) { + switch v.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return float64(v.Int()), true + case reflect.Float32, reflect.Float64: + return v.Float(), true + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return float64(v.Uint()), true + case reflect.Bool: + if v.Bool() { + return 1, true + } + return 0, true + } + return 0, false +} + +// numLess returns whether a < b. +// a and b must necessarily have the same kind. +func numLess(a, b reflect.Value) bool { + switch a.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return a.Int() < b.Int() + case reflect.Float32, reflect.Float64: + return a.Float() < b.Float() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return a.Uint() < b.Uint() + case reflect.Bool: + return !a.Bool() && b.Bool() + } + panic("not a number") +} diff --git a/asyncapi/vendor/gopkg.in/yaml.v3/writerc.go b/asyncapi/vendor/gopkg.in/yaml.v3/writerc.go new file mode 100644 index 0000000..b8a116b --- /dev/null +++ b/asyncapi/vendor/gopkg.in/yaml.v3/writerc.go @@ -0,0 +1,48 @@ +// +// Copyright (c) 2011-2019 Canonical Ltd +// Copyright (c) 2006-2010 Kirill Simonov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package yaml + +// Set the writer error and return false. +func yaml_emitter_set_writer_error(emitter *yaml_emitter_t, problem string) bool { + emitter.error = yaml_WRITER_ERROR + emitter.problem = problem + return false +} + +// Flush the output buffer. +func yaml_emitter_flush(emitter *yaml_emitter_t) bool { + if emitter.write_handler == nil { + panic("write handler not set") + } + + // Check if the buffer is empty. + if emitter.buffer_pos == 0 { + return true + } + + if err := emitter.write_handler(emitter, emitter.buffer[:emitter.buffer_pos]); err != nil { + return yaml_emitter_set_writer_error(emitter, "write error: "+err.Error()) + } + emitter.buffer_pos = 0 + return true +} diff --git a/asyncapi/vendor/gopkg.in/yaml.v3/yaml.go b/asyncapi/vendor/gopkg.in/yaml.v3/yaml.go new file mode 100644 index 0000000..8cec6da --- /dev/null +++ b/asyncapi/vendor/gopkg.in/yaml.v3/yaml.go @@ -0,0 +1,698 @@ +// +// Copyright (c) 2011-2019 Canonical Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package yaml implements YAML support for the Go language. +// +// Source code and other details for the project are available at GitHub: +// +// https://github.com/go-yaml/yaml +// +package yaml + +import ( + "errors" + "fmt" + "io" + "reflect" + "strings" + "sync" + "unicode/utf8" +) + +// The Unmarshaler interface may be implemented by types to customize their +// behavior when being unmarshaled from a YAML document. +type Unmarshaler interface { + UnmarshalYAML(value *Node) error +} + +type obsoleteUnmarshaler interface { + UnmarshalYAML(unmarshal func(interface{}) error) error +} + +// The Marshaler interface may be implemented by types to customize their +// behavior when being marshaled into a YAML document. The returned value +// is marshaled in place of the original value implementing Marshaler. +// +// If an error is returned by MarshalYAML, the marshaling procedure stops +// and returns with the provided error. +type Marshaler interface { + MarshalYAML() (interface{}, error) +} + +// Unmarshal decodes the first document found within the in byte slice +// and assigns decoded values into the out value. +// +// Maps and pointers (to a struct, string, int, etc) are accepted as out +// values. If an internal pointer within a struct is not initialized, +// the yaml package will initialize it if necessary for unmarshalling +// the provided data. The out parameter must not be nil. +// +// The type of the decoded values should be compatible with the respective +// values in out. If one or more values cannot be decoded due to a type +// mismatches, decoding continues partially until the end of the YAML +// content, and a *yaml.TypeError is returned with details for all +// missed values. +// +// Struct fields are only unmarshalled if they are exported (have an +// upper case first letter), and are unmarshalled using the field name +// lowercased as the default key. Custom keys may be defined via the +// "yaml" name in the field tag: the content preceding the first comma +// is used as the key, and the following comma-separated options are +// used to tweak the marshalling process (see Marshal). +// Conflicting names result in a runtime error. +// +// For example: +// +// type T struct { +// F int `yaml:"a,omitempty"` +// B int +// } +// var t T +// yaml.Unmarshal([]byte("a: 1\nb: 2"), &t) +// +// See the documentation of Marshal for the format of tags and a list of +// supported tag options. +// +func Unmarshal(in []byte, out interface{}) (err error) { + return unmarshal(in, out, false) +} + +// A Decoder reads and decodes YAML values from an input stream. +type Decoder struct { + parser *parser + knownFields bool +} + +// NewDecoder returns a new decoder that reads from r. +// +// The decoder introduces its own buffering and may read +// data from r beyond the YAML values requested. +func NewDecoder(r io.Reader) *Decoder { + return &Decoder{ + parser: newParserFromReader(r), + } +} + +// KnownFields ensures that the keys in decoded mappings to +// exist as fields in the struct being decoded into. +func (dec *Decoder) KnownFields(enable bool) { + dec.knownFields = enable +} + +// Decode reads the next YAML-encoded value from its input +// and stores it in the value pointed to by v. +// +// See the documentation for Unmarshal for details about the +// conversion of YAML into a Go value. +func (dec *Decoder) Decode(v interface{}) (err error) { + d := newDecoder() + d.knownFields = dec.knownFields + defer handleErr(&err) + node := dec.parser.parse() + if node == nil { + return io.EOF + } + out := reflect.ValueOf(v) + if out.Kind() == reflect.Ptr && !out.IsNil() { + out = out.Elem() + } + d.unmarshal(node, out) + if len(d.terrors) > 0 { + return &TypeError{d.terrors} + } + return nil +} + +// Decode decodes the node and stores its data into the value pointed to by v. +// +// See the documentation for Unmarshal for details about the +// conversion of YAML into a Go value. +func (n *Node) Decode(v interface{}) (err error) { + d := newDecoder() + defer handleErr(&err) + out := reflect.ValueOf(v) + if out.Kind() == reflect.Ptr && !out.IsNil() { + out = out.Elem() + } + d.unmarshal(n, out) + if len(d.terrors) > 0 { + return &TypeError{d.terrors} + } + return nil +} + +func unmarshal(in []byte, out interface{}, strict bool) (err error) { + defer handleErr(&err) + d := newDecoder() + p := newParser(in) + defer p.destroy() + node := p.parse() + if node != nil { + v := reflect.ValueOf(out) + if v.Kind() == reflect.Ptr && !v.IsNil() { + v = v.Elem() + } + d.unmarshal(node, v) + } + if len(d.terrors) > 0 { + return &TypeError{d.terrors} + } + return nil +} + +// Marshal serializes the value provided into a YAML document. The structure +// of the generated document will reflect the structure of the value itself. +// Maps and pointers (to struct, string, int, etc) are accepted as the in value. +// +// Struct fields are only marshalled if they are exported (have an upper case +// first letter), and are marshalled using the field name lowercased as the +// default key. Custom keys may be defined via the "yaml" name in the field +// tag: the content preceding the first comma is used as the key, and the +// following comma-separated options are used to tweak the marshalling process. +// Conflicting names result in a runtime error. +// +// The field tag format accepted is: +// +// `(...) yaml:"[][,[,]]" (...)` +// +// The following flags are currently supported: +// +// omitempty Only include the field if it's not set to the zero +// value for the type or to empty slices or maps. +// Zero valued structs will be omitted if all their public +// fields are zero, unless they implement an IsZero +// method (see the IsZeroer interface type), in which +// case the field will be excluded if IsZero returns true. +// +// flow Marshal using a flow style (useful for structs, +// sequences and maps). +// +// inline Inline the field, which must be a struct or a map, +// causing all of its fields or keys to be processed as if +// they were part of the outer struct. For maps, keys must +// not conflict with the yaml keys of other struct fields. +// +// In addition, if the key is "-", the field is ignored. +// +// For example: +// +// type T struct { +// F int `yaml:"a,omitempty"` +// B int +// } +// yaml.Marshal(&T{B: 2}) // Returns "b: 2\n" +// yaml.Marshal(&T{F: 1}} // Returns "a: 1\nb: 0\n" +// +func Marshal(in interface{}) (out []byte, err error) { + defer handleErr(&err) + e := newEncoder() + defer e.destroy() + e.marshalDoc("", reflect.ValueOf(in)) + e.finish() + out = e.out + return +} + +// An Encoder writes YAML values to an output stream. +type Encoder struct { + encoder *encoder +} + +// NewEncoder returns a new encoder that writes to w. +// The Encoder should be closed after use to flush all data +// to w. +func NewEncoder(w io.Writer) *Encoder { + return &Encoder{ + encoder: newEncoderWithWriter(w), + } +} + +// Encode writes the YAML encoding of v to the stream. +// If multiple items are encoded to the stream, the +// second and subsequent document will be preceded +// with a "---" document separator, but the first will not. +// +// See the documentation for Marshal for details about the conversion of Go +// values to YAML. +func (e *Encoder) Encode(v interface{}) (err error) { + defer handleErr(&err) + e.encoder.marshalDoc("", reflect.ValueOf(v)) + return nil +} + +// Encode encodes value v and stores its representation in n. +// +// See the documentation for Marshal for details about the +// conversion of Go values into YAML. +func (n *Node) Encode(v interface{}) (err error) { + defer handleErr(&err) + e := newEncoder() + defer e.destroy() + e.marshalDoc("", reflect.ValueOf(v)) + e.finish() + p := newParser(e.out) + p.textless = true + defer p.destroy() + doc := p.parse() + *n = *doc.Content[0] + return nil +} + +// SetIndent changes the used indentation used when encoding. +func (e *Encoder) SetIndent(spaces int) { + if spaces < 0 { + panic("yaml: cannot indent to a negative number of spaces") + } + e.encoder.indent = spaces +} + +// Close closes the encoder by writing any remaining data. +// It does not write a stream terminating string "...". +func (e *Encoder) Close() (err error) { + defer handleErr(&err) + e.encoder.finish() + return nil +} + +func handleErr(err *error) { + if v := recover(); v != nil { + if e, ok := v.(yamlError); ok { + *err = e.err + } else { + panic(v) + } + } +} + +type yamlError struct { + err error +} + +func fail(err error) { + panic(yamlError{err}) +} + +func failf(format string, args ...interface{}) { + panic(yamlError{fmt.Errorf("yaml: "+format, args...)}) +} + +// A TypeError is returned by Unmarshal when one or more fields in +// the YAML document cannot be properly decoded into the requested +// types. When this error is returned, the value is still +// unmarshaled partially. +type TypeError struct { + Errors []string +} + +func (e *TypeError) Error() string { + return fmt.Sprintf("yaml: unmarshal errors:\n %s", strings.Join(e.Errors, "\n ")) +} + +type Kind uint32 + +const ( + DocumentNode Kind = 1 << iota + SequenceNode + MappingNode + ScalarNode + AliasNode +) + +type Style uint32 + +const ( + TaggedStyle Style = 1 << iota + DoubleQuotedStyle + SingleQuotedStyle + LiteralStyle + FoldedStyle + FlowStyle +) + +// Node represents an element in the YAML document hierarchy. While documents +// are typically encoded and decoded into higher level types, such as structs +// and maps, Node is an intermediate representation that allows detailed +// control over the content being decoded or encoded. +// +// It's worth noting that although Node offers access into details such as +// line numbers, colums, and comments, the content when re-encoded will not +// have its original textual representation preserved. An effort is made to +// render the data plesantly, and to preserve comments near the data they +// describe, though. +// +// Values that make use of the Node type interact with the yaml package in the +// same way any other type would do, by encoding and decoding yaml data +// directly or indirectly into them. +// +// For example: +// +// var person struct { +// Name string +// Address yaml.Node +// } +// err := yaml.Unmarshal(data, &person) +// +// Or by itself: +// +// var person Node +// err := yaml.Unmarshal(data, &person) +// +type Node struct { + // Kind defines whether the node is a document, a mapping, a sequence, + // a scalar value, or an alias to another node. The specific data type of + // scalar nodes may be obtained via the ShortTag and LongTag methods. + Kind Kind + + // Style allows customizing the apperance of the node in the tree. + Style Style + + // Tag holds the YAML tag defining the data type for the value. + // When decoding, this field will always be set to the resolved tag, + // even when it wasn't explicitly provided in the YAML content. + // When encoding, if this field is unset the value type will be + // implied from the node properties, and if it is set, it will only + // be serialized into the representation if TaggedStyle is used or + // the implicit tag diverges from the provided one. + Tag string + + // Value holds the unescaped and unquoted represenation of the value. + Value string + + // Anchor holds the anchor name for this node, which allows aliases to point to it. + Anchor string + + // Alias holds the node that this alias points to. Only valid when Kind is AliasNode. + Alias *Node + + // Content holds contained nodes for documents, mappings, and sequences. + Content []*Node + + // HeadComment holds any comments in the lines preceding the node and + // not separated by an empty line. + HeadComment string + + // LineComment holds any comments at the end of the line where the node is in. + LineComment string + + // FootComment holds any comments following the node and before empty lines. + FootComment string + + // Line and Column hold the node position in the decoded YAML text. + // These fields are not respected when encoding the node. + Line int + Column int +} + +// IsZero returns whether the node has all of its fields unset. +func (n *Node) IsZero() bool { + return n.Kind == 0 && n.Style == 0 && n.Tag == "" && n.Value == "" && n.Anchor == "" && n.Alias == nil && n.Content == nil && + n.HeadComment == "" && n.LineComment == "" && n.FootComment == "" && n.Line == 0 && n.Column == 0 +} + + +// LongTag returns the long form of the tag that indicates the data type for +// the node. If the Tag field isn't explicitly defined, one will be computed +// based on the node properties. +func (n *Node) LongTag() string { + return longTag(n.ShortTag()) +} + +// ShortTag returns the short form of the YAML tag that indicates data type for +// the node. If the Tag field isn't explicitly defined, one will be computed +// based on the node properties. +func (n *Node) ShortTag() string { + if n.indicatedString() { + return strTag + } + if n.Tag == "" || n.Tag == "!" { + switch n.Kind { + case MappingNode: + return mapTag + case SequenceNode: + return seqTag + case AliasNode: + if n.Alias != nil { + return n.Alias.ShortTag() + } + case ScalarNode: + tag, _ := resolve("", n.Value) + return tag + case 0: + // Special case to make the zero value convenient. + if n.IsZero() { + return nullTag + } + } + return "" + } + return shortTag(n.Tag) +} + +func (n *Node) indicatedString() bool { + return n.Kind == ScalarNode && + (shortTag(n.Tag) == strTag || + (n.Tag == "" || n.Tag == "!") && n.Style&(SingleQuotedStyle|DoubleQuotedStyle|LiteralStyle|FoldedStyle) != 0) +} + +// SetString is a convenience function that sets the node to a string value +// and defines its style in a pleasant way depending on its content. +func (n *Node) SetString(s string) { + n.Kind = ScalarNode + if utf8.ValidString(s) { + n.Value = s + n.Tag = strTag + } else { + n.Value = encodeBase64(s) + n.Tag = binaryTag + } + if strings.Contains(n.Value, "\n") { + n.Style = LiteralStyle + } +} + +// -------------------------------------------------------------------------- +// Maintain a mapping of keys to structure field indexes + +// The code in this section was copied from mgo/bson. + +// structInfo holds details for the serialization of fields of +// a given struct. +type structInfo struct { + FieldsMap map[string]fieldInfo + FieldsList []fieldInfo + + // InlineMap is the number of the field in the struct that + // contains an ,inline map, or -1 if there's none. + InlineMap int + + // InlineUnmarshalers holds indexes to inlined fields that + // contain unmarshaler values. + InlineUnmarshalers [][]int +} + +type fieldInfo struct { + Key string + Num int + OmitEmpty bool + Flow bool + // Id holds the unique field identifier, so we can cheaply + // check for field duplicates without maintaining an extra map. + Id int + + // Inline holds the field index if the field is part of an inlined struct. + Inline []int +} + +var structMap = make(map[reflect.Type]*structInfo) +var fieldMapMutex sync.RWMutex +var unmarshalerType reflect.Type + +func init() { + var v Unmarshaler + unmarshalerType = reflect.ValueOf(&v).Elem().Type() +} + +func getStructInfo(st reflect.Type) (*structInfo, error) { + fieldMapMutex.RLock() + sinfo, found := structMap[st] + fieldMapMutex.RUnlock() + if found { + return sinfo, nil + } + + n := st.NumField() + fieldsMap := make(map[string]fieldInfo) + fieldsList := make([]fieldInfo, 0, n) + inlineMap := -1 + inlineUnmarshalers := [][]int(nil) + for i := 0; i != n; i++ { + field := st.Field(i) + if field.PkgPath != "" && !field.Anonymous { + continue // Private field + } + + info := fieldInfo{Num: i} + + tag := field.Tag.Get("yaml") + if tag == "" && strings.Index(string(field.Tag), ":") < 0 { + tag = string(field.Tag) + } + if tag == "-" { + continue + } + + inline := false + fields := strings.Split(tag, ",") + if len(fields) > 1 { + for _, flag := range fields[1:] { + switch flag { + case "omitempty": + info.OmitEmpty = true + case "flow": + info.Flow = true + case "inline": + inline = true + default: + return nil, errors.New(fmt.Sprintf("unsupported flag %q in tag %q of type %s", flag, tag, st)) + } + } + tag = fields[0] + } + + if inline { + switch field.Type.Kind() { + case reflect.Map: + if inlineMap >= 0 { + return nil, errors.New("multiple ,inline maps in struct " + st.String()) + } + if field.Type.Key() != reflect.TypeOf("") { + return nil, errors.New("option ,inline needs a map with string keys in struct " + st.String()) + } + inlineMap = info.Num + case reflect.Struct, reflect.Ptr: + ftype := field.Type + for ftype.Kind() == reflect.Ptr { + ftype = ftype.Elem() + } + if ftype.Kind() != reflect.Struct { + return nil, errors.New("option ,inline may only be used on a struct or map field") + } + if reflect.PtrTo(ftype).Implements(unmarshalerType) { + inlineUnmarshalers = append(inlineUnmarshalers, []int{i}) + } else { + sinfo, err := getStructInfo(ftype) + if err != nil { + return nil, err + } + for _, index := range sinfo.InlineUnmarshalers { + inlineUnmarshalers = append(inlineUnmarshalers, append([]int{i}, index...)) + } + for _, finfo := range sinfo.FieldsList { + if _, found := fieldsMap[finfo.Key]; found { + msg := "duplicated key '" + finfo.Key + "' in struct " + st.String() + return nil, errors.New(msg) + } + if finfo.Inline == nil { + finfo.Inline = []int{i, finfo.Num} + } else { + finfo.Inline = append([]int{i}, finfo.Inline...) + } + finfo.Id = len(fieldsList) + fieldsMap[finfo.Key] = finfo + fieldsList = append(fieldsList, finfo) + } + } + default: + return nil, errors.New("option ,inline may only be used on a struct or map field") + } + continue + } + + if tag != "" { + info.Key = tag + } else { + info.Key = strings.ToLower(field.Name) + } + + if _, found = fieldsMap[info.Key]; found { + msg := "duplicated key '" + info.Key + "' in struct " + st.String() + return nil, errors.New(msg) + } + + info.Id = len(fieldsList) + fieldsList = append(fieldsList, info) + fieldsMap[info.Key] = info + } + + sinfo = &structInfo{ + FieldsMap: fieldsMap, + FieldsList: fieldsList, + InlineMap: inlineMap, + InlineUnmarshalers: inlineUnmarshalers, + } + + fieldMapMutex.Lock() + structMap[st] = sinfo + fieldMapMutex.Unlock() + return sinfo, nil +} + +// IsZeroer is used to check whether an object is zero to +// determine whether it should be omitted when marshaling +// with the omitempty flag. One notable implementation +// is time.Time. +type IsZeroer interface { + IsZero() bool +} + +func isZero(v reflect.Value) bool { + kind := v.Kind() + if z, ok := v.Interface().(IsZeroer); ok { + if (kind == reflect.Ptr || kind == reflect.Interface) && v.IsNil() { + return true + } + return z.IsZero() + } + switch kind { + case reflect.String: + return len(v.String()) == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + case reflect.Slice: + return v.Len() == 0 + case reflect.Map: + return v.Len() == 0 + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Struct: + vt := v.Type() + for i := v.NumField() - 1; i >= 0; i-- { + if vt.Field(i).PkgPath != "" { + continue // Private field + } + if !isZero(v.Field(i)) { + return false + } + } + return true + } + return false +} diff --git a/asyncapi/vendor/gopkg.in/yaml.v3/yamlh.go b/asyncapi/vendor/gopkg.in/yaml.v3/yamlh.go new file mode 100644 index 0000000..7c6d007 --- /dev/null +++ b/asyncapi/vendor/gopkg.in/yaml.v3/yamlh.go @@ -0,0 +1,807 @@ +// +// Copyright (c) 2011-2019 Canonical Ltd +// Copyright (c) 2006-2010 Kirill Simonov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package yaml + +import ( + "fmt" + "io" +) + +// The version directive data. +type yaml_version_directive_t struct { + major int8 // The major version number. + minor int8 // The minor version number. +} + +// The tag directive data. +type yaml_tag_directive_t struct { + handle []byte // The tag handle. + prefix []byte // The tag prefix. +} + +type yaml_encoding_t int + +// The stream encoding. +const ( + // Let the parser choose the encoding. + yaml_ANY_ENCODING yaml_encoding_t = iota + + yaml_UTF8_ENCODING // The default UTF-8 encoding. + yaml_UTF16LE_ENCODING // The UTF-16-LE encoding with BOM. + yaml_UTF16BE_ENCODING // The UTF-16-BE encoding with BOM. +) + +type yaml_break_t int + +// Line break types. +const ( + // Let the parser choose the break type. + yaml_ANY_BREAK yaml_break_t = iota + + yaml_CR_BREAK // Use CR for line breaks (Mac style). + yaml_LN_BREAK // Use LN for line breaks (Unix style). + yaml_CRLN_BREAK // Use CR LN for line breaks (DOS style). +) + +type yaml_error_type_t int + +// Many bad things could happen with the parser and emitter. +const ( + // No error is produced. + yaml_NO_ERROR yaml_error_type_t = iota + + yaml_MEMORY_ERROR // Cannot allocate or reallocate a block of memory. + yaml_READER_ERROR // Cannot read or decode the input stream. + yaml_SCANNER_ERROR // Cannot scan the input stream. + yaml_PARSER_ERROR // Cannot parse the input stream. + yaml_COMPOSER_ERROR // Cannot compose a YAML document. + yaml_WRITER_ERROR // Cannot write to the output stream. + yaml_EMITTER_ERROR // Cannot emit a YAML stream. +) + +// The pointer position. +type yaml_mark_t struct { + index int // The position index. + line int // The position line. + column int // The position column. +} + +// Node Styles + +type yaml_style_t int8 + +type yaml_scalar_style_t yaml_style_t + +// Scalar styles. +const ( + // Let the emitter choose the style. + yaml_ANY_SCALAR_STYLE yaml_scalar_style_t = 0 + + yaml_PLAIN_SCALAR_STYLE yaml_scalar_style_t = 1 << iota // The plain scalar style. + yaml_SINGLE_QUOTED_SCALAR_STYLE // The single-quoted scalar style. + yaml_DOUBLE_QUOTED_SCALAR_STYLE // The double-quoted scalar style. + yaml_LITERAL_SCALAR_STYLE // The literal scalar style. + yaml_FOLDED_SCALAR_STYLE // The folded scalar style. +) + +type yaml_sequence_style_t yaml_style_t + +// Sequence styles. +const ( + // Let the emitter choose the style. + yaml_ANY_SEQUENCE_STYLE yaml_sequence_style_t = iota + + yaml_BLOCK_SEQUENCE_STYLE // The block sequence style. + yaml_FLOW_SEQUENCE_STYLE // The flow sequence style. +) + +type yaml_mapping_style_t yaml_style_t + +// Mapping styles. +const ( + // Let the emitter choose the style. + yaml_ANY_MAPPING_STYLE yaml_mapping_style_t = iota + + yaml_BLOCK_MAPPING_STYLE // The block mapping style. + yaml_FLOW_MAPPING_STYLE // The flow mapping style. +) + +// Tokens + +type yaml_token_type_t int + +// Token types. +const ( + // An empty token. + yaml_NO_TOKEN yaml_token_type_t = iota + + yaml_STREAM_START_TOKEN // A STREAM-START token. + yaml_STREAM_END_TOKEN // A STREAM-END token. + + yaml_VERSION_DIRECTIVE_TOKEN // A VERSION-DIRECTIVE token. + yaml_TAG_DIRECTIVE_TOKEN // A TAG-DIRECTIVE token. + yaml_DOCUMENT_START_TOKEN // A DOCUMENT-START token. + yaml_DOCUMENT_END_TOKEN // A DOCUMENT-END token. + + yaml_BLOCK_SEQUENCE_START_TOKEN // A BLOCK-SEQUENCE-START token. + yaml_BLOCK_MAPPING_START_TOKEN // A BLOCK-SEQUENCE-END token. + yaml_BLOCK_END_TOKEN // A BLOCK-END token. + + yaml_FLOW_SEQUENCE_START_TOKEN // A FLOW-SEQUENCE-START token. + yaml_FLOW_SEQUENCE_END_TOKEN // A FLOW-SEQUENCE-END token. + yaml_FLOW_MAPPING_START_TOKEN // A FLOW-MAPPING-START token. + yaml_FLOW_MAPPING_END_TOKEN // A FLOW-MAPPING-END token. + + yaml_BLOCK_ENTRY_TOKEN // A BLOCK-ENTRY token. + yaml_FLOW_ENTRY_TOKEN // A FLOW-ENTRY token. + yaml_KEY_TOKEN // A KEY token. + yaml_VALUE_TOKEN // A VALUE token. + + yaml_ALIAS_TOKEN // An ALIAS token. + yaml_ANCHOR_TOKEN // An ANCHOR token. + yaml_TAG_TOKEN // A TAG token. + yaml_SCALAR_TOKEN // A SCALAR token. +) + +func (tt yaml_token_type_t) String() string { + switch tt { + case yaml_NO_TOKEN: + return "yaml_NO_TOKEN" + case yaml_STREAM_START_TOKEN: + return "yaml_STREAM_START_TOKEN" + case yaml_STREAM_END_TOKEN: + return "yaml_STREAM_END_TOKEN" + case yaml_VERSION_DIRECTIVE_TOKEN: + return "yaml_VERSION_DIRECTIVE_TOKEN" + case yaml_TAG_DIRECTIVE_TOKEN: + return "yaml_TAG_DIRECTIVE_TOKEN" + case yaml_DOCUMENT_START_TOKEN: + return "yaml_DOCUMENT_START_TOKEN" + case yaml_DOCUMENT_END_TOKEN: + return "yaml_DOCUMENT_END_TOKEN" + case yaml_BLOCK_SEQUENCE_START_TOKEN: + return "yaml_BLOCK_SEQUENCE_START_TOKEN" + case yaml_BLOCK_MAPPING_START_TOKEN: + return "yaml_BLOCK_MAPPING_START_TOKEN" + case yaml_BLOCK_END_TOKEN: + return "yaml_BLOCK_END_TOKEN" + case yaml_FLOW_SEQUENCE_START_TOKEN: + return "yaml_FLOW_SEQUENCE_START_TOKEN" + case yaml_FLOW_SEQUENCE_END_TOKEN: + return "yaml_FLOW_SEQUENCE_END_TOKEN" + case yaml_FLOW_MAPPING_START_TOKEN: + return "yaml_FLOW_MAPPING_START_TOKEN" + case yaml_FLOW_MAPPING_END_TOKEN: + return "yaml_FLOW_MAPPING_END_TOKEN" + case yaml_BLOCK_ENTRY_TOKEN: + return "yaml_BLOCK_ENTRY_TOKEN" + case yaml_FLOW_ENTRY_TOKEN: + return "yaml_FLOW_ENTRY_TOKEN" + case yaml_KEY_TOKEN: + return "yaml_KEY_TOKEN" + case yaml_VALUE_TOKEN: + return "yaml_VALUE_TOKEN" + case yaml_ALIAS_TOKEN: + return "yaml_ALIAS_TOKEN" + case yaml_ANCHOR_TOKEN: + return "yaml_ANCHOR_TOKEN" + case yaml_TAG_TOKEN: + return "yaml_TAG_TOKEN" + case yaml_SCALAR_TOKEN: + return "yaml_SCALAR_TOKEN" + } + return "" +} + +// The token structure. +type yaml_token_t struct { + // The token type. + typ yaml_token_type_t + + // The start/end of the token. + start_mark, end_mark yaml_mark_t + + // The stream encoding (for yaml_STREAM_START_TOKEN). + encoding yaml_encoding_t + + // The alias/anchor/scalar value or tag/tag directive handle + // (for yaml_ALIAS_TOKEN, yaml_ANCHOR_TOKEN, yaml_SCALAR_TOKEN, yaml_TAG_TOKEN, yaml_TAG_DIRECTIVE_TOKEN). + value []byte + + // The tag suffix (for yaml_TAG_TOKEN). + suffix []byte + + // The tag directive prefix (for yaml_TAG_DIRECTIVE_TOKEN). + prefix []byte + + // The scalar style (for yaml_SCALAR_TOKEN). + style yaml_scalar_style_t + + // The version directive major/minor (for yaml_VERSION_DIRECTIVE_TOKEN). + major, minor int8 +} + +// Events + +type yaml_event_type_t int8 + +// Event types. +const ( + // An empty event. + yaml_NO_EVENT yaml_event_type_t = iota + + yaml_STREAM_START_EVENT // A STREAM-START event. + yaml_STREAM_END_EVENT // A STREAM-END event. + yaml_DOCUMENT_START_EVENT // A DOCUMENT-START event. + yaml_DOCUMENT_END_EVENT // A DOCUMENT-END event. + yaml_ALIAS_EVENT // An ALIAS event. + yaml_SCALAR_EVENT // A SCALAR event. + yaml_SEQUENCE_START_EVENT // A SEQUENCE-START event. + yaml_SEQUENCE_END_EVENT // A SEQUENCE-END event. + yaml_MAPPING_START_EVENT // A MAPPING-START event. + yaml_MAPPING_END_EVENT // A MAPPING-END event. + yaml_TAIL_COMMENT_EVENT +) + +var eventStrings = []string{ + yaml_NO_EVENT: "none", + yaml_STREAM_START_EVENT: "stream start", + yaml_STREAM_END_EVENT: "stream end", + yaml_DOCUMENT_START_EVENT: "document start", + yaml_DOCUMENT_END_EVENT: "document end", + yaml_ALIAS_EVENT: "alias", + yaml_SCALAR_EVENT: "scalar", + yaml_SEQUENCE_START_EVENT: "sequence start", + yaml_SEQUENCE_END_EVENT: "sequence end", + yaml_MAPPING_START_EVENT: "mapping start", + yaml_MAPPING_END_EVENT: "mapping end", + yaml_TAIL_COMMENT_EVENT: "tail comment", +} + +func (e yaml_event_type_t) String() string { + if e < 0 || int(e) >= len(eventStrings) { + return fmt.Sprintf("unknown event %d", e) + } + return eventStrings[e] +} + +// The event structure. +type yaml_event_t struct { + + // The event type. + typ yaml_event_type_t + + // The start and end of the event. + start_mark, end_mark yaml_mark_t + + // The document encoding (for yaml_STREAM_START_EVENT). + encoding yaml_encoding_t + + // The version directive (for yaml_DOCUMENT_START_EVENT). + version_directive *yaml_version_directive_t + + // The list of tag directives (for yaml_DOCUMENT_START_EVENT). + tag_directives []yaml_tag_directive_t + + // The comments + head_comment []byte + line_comment []byte + foot_comment []byte + tail_comment []byte + + // The anchor (for yaml_SCALAR_EVENT, yaml_SEQUENCE_START_EVENT, yaml_MAPPING_START_EVENT, yaml_ALIAS_EVENT). + anchor []byte + + // The tag (for yaml_SCALAR_EVENT, yaml_SEQUENCE_START_EVENT, yaml_MAPPING_START_EVENT). + tag []byte + + // The scalar value (for yaml_SCALAR_EVENT). + value []byte + + // Is the document start/end indicator implicit, or the tag optional? + // (for yaml_DOCUMENT_START_EVENT, yaml_DOCUMENT_END_EVENT, yaml_SEQUENCE_START_EVENT, yaml_MAPPING_START_EVENT, yaml_SCALAR_EVENT). + implicit bool + + // Is the tag optional for any non-plain style? (for yaml_SCALAR_EVENT). + quoted_implicit bool + + // The style (for yaml_SCALAR_EVENT, yaml_SEQUENCE_START_EVENT, yaml_MAPPING_START_EVENT). + style yaml_style_t +} + +func (e *yaml_event_t) scalar_style() yaml_scalar_style_t { return yaml_scalar_style_t(e.style) } +func (e *yaml_event_t) sequence_style() yaml_sequence_style_t { return yaml_sequence_style_t(e.style) } +func (e *yaml_event_t) mapping_style() yaml_mapping_style_t { return yaml_mapping_style_t(e.style) } + +// Nodes + +const ( + yaml_NULL_TAG = "tag:yaml.org,2002:null" // The tag !!null with the only possible value: null. + yaml_BOOL_TAG = "tag:yaml.org,2002:bool" // The tag !!bool with the values: true and false. + yaml_STR_TAG = "tag:yaml.org,2002:str" // The tag !!str for string values. + yaml_INT_TAG = "tag:yaml.org,2002:int" // The tag !!int for integer values. + yaml_FLOAT_TAG = "tag:yaml.org,2002:float" // The tag !!float for float values. + yaml_TIMESTAMP_TAG = "tag:yaml.org,2002:timestamp" // The tag !!timestamp for date and time values. + + yaml_SEQ_TAG = "tag:yaml.org,2002:seq" // The tag !!seq is used to denote sequences. + yaml_MAP_TAG = "tag:yaml.org,2002:map" // The tag !!map is used to denote mapping. + + // Not in original libyaml. + yaml_BINARY_TAG = "tag:yaml.org,2002:binary" + yaml_MERGE_TAG = "tag:yaml.org,2002:merge" + + yaml_DEFAULT_SCALAR_TAG = yaml_STR_TAG // The default scalar tag is !!str. + yaml_DEFAULT_SEQUENCE_TAG = yaml_SEQ_TAG // The default sequence tag is !!seq. + yaml_DEFAULT_MAPPING_TAG = yaml_MAP_TAG // The default mapping tag is !!map. +) + +type yaml_node_type_t int + +// Node types. +const ( + // An empty node. + yaml_NO_NODE yaml_node_type_t = iota + + yaml_SCALAR_NODE // A scalar node. + yaml_SEQUENCE_NODE // A sequence node. + yaml_MAPPING_NODE // A mapping node. +) + +// An element of a sequence node. +type yaml_node_item_t int + +// An element of a mapping node. +type yaml_node_pair_t struct { + key int // The key of the element. + value int // The value of the element. +} + +// The node structure. +type yaml_node_t struct { + typ yaml_node_type_t // The node type. + tag []byte // The node tag. + + // The node data. + + // The scalar parameters (for yaml_SCALAR_NODE). + scalar struct { + value []byte // The scalar value. + length int // The length of the scalar value. + style yaml_scalar_style_t // The scalar style. + } + + // The sequence parameters (for YAML_SEQUENCE_NODE). + sequence struct { + items_data []yaml_node_item_t // The stack of sequence items. + style yaml_sequence_style_t // The sequence style. + } + + // The mapping parameters (for yaml_MAPPING_NODE). + mapping struct { + pairs_data []yaml_node_pair_t // The stack of mapping pairs (key, value). + pairs_start *yaml_node_pair_t // The beginning of the stack. + pairs_end *yaml_node_pair_t // The end of the stack. + pairs_top *yaml_node_pair_t // The top of the stack. + style yaml_mapping_style_t // The mapping style. + } + + start_mark yaml_mark_t // The beginning of the node. + end_mark yaml_mark_t // The end of the node. + +} + +// The document structure. +type yaml_document_t struct { + + // The document nodes. + nodes []yaml_node_t + + // The version directive. + version_directive *yaml_version_directive_t + + // The list of tag directives. + tag_directives_data []yaml_tag_directive_t + tag_directives_start int // The beginning of the tag directives list. + tag_directives_end int // The end of the tag directives list. + + start_implicit int // Is the document start indicator implicit? + end_implicit int // Is the document end indicator implicit? + + // The start/end of the document. + start_mark, end_mark yaml_mark_t +} + +// The prototype of a read handler. +// +// The read handler is called when the parser needs to read more bytes from the +// source. The handler should write not more than size bytes to the buffer. +// The number of written bytes should be set to the size_read variable. +// +// [in,out] data A pointer to an application data specified by +// yaml_parser_set_input(). +// [out] buffer The buffer to write the data from the source. +// [in] size The size of the buffer. +// [out] size_read The actual number of bytes read from the source. +// +// On success, the handler should return 1. If the handler failed, +// the returned value should be 0. On EOF, the handler should set the +// size_read to 0 and return 1. +type yaml_read_handler_t func(parser *yaml_parser_t, buffer []byte) (n int, err error) + +// This structure holds information about a potential simple key. +type yaml_simple_key_t struct { + possible bool // Is a simple key possible? + required bool // Is a simple key required? + token_number int // The number of the token. + mark yaml_mark_t // The position mark. +} + +// The states of the parser. +type yaml_parser_state_t int + +const ( + yaml_PARSE_STREAM_START_STATE yaml_parser_state_t = iota + + yaml_PARSE_IMPLICIT_DOCUMENT_START_STATE // Expect the beginning of an implicit document. + yaml_PARSE_DOCUMENT_START_STATE // Expect DOCUMENT-START. + yaml_PARSE_DOCUMENT_CONTENT_STATE // Expect the content of a document. + yaml_PARSE_DOCUMENT_END_STATE // Expect DOCUMENT-END. + yaml_PARSE_BLOCK_NODE_STATE // Expect a block node. + yaml_PARSE_BLOCK_NODE_OR_INDENTLESS_SEQUENCE_STATE // Expect a block node or indentless sequence. + yaml_PARSE_FLOW_NODE_STATE // Expect a flow node. + yaml_PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE // Expect the first entry of a block sequence. + yaml_PARSE_BLOCK_SEQUENCE_ENTRY_STATE // Expect an entry of a block sequence. + yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE // Expect an entry of an indentless sequence. + yaml_PARSE_BLOCK_MAPPING_FIRST_KEY_STATE // Expect the first key of a block mapping. + yaml_PARSE_BLOCK_MAPPING_KEY_STATE // Expect a block mapping key. + yaml_PARSE_BLOCK_MAPPING_VALUE_STATE // Expect a block mapping value. + yaml_PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE // Expect the first entry of a flow sequence. + yaml_PARSE_FLOW_SEQUENCE_ENTRY_STATE // Expect an entry of a flow sequence. + yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE // Expect a key of an ordered mapping. + yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE // Expect a value of an ordered mapping. + yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE // Expect the and of an ordered mapping entry. + yaml_PARSE_FLOW_MAPPING_FIRST_KEY_STATE // Expect the first key of a flow mapping. + yaml_PARSE_FLOW_MAPPING_KEY_STATE // Expect a key of a flow mapping. + yaml_PARSE_FLOW_MAPPING_VALUE_STATE // Expect a value of a flow mapping. + yaml_PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE // Expect an empty value of a flow mapping. + yaml_PARSE_END_STATE // Expect nothing. +) + +func (ps yaml_parser_state_t) String() string { + switch ps { + case yaml_PARSE_STREAM_START_STATE: + return "yaml_PARSE_STREAM_START_STATE" + case yaml_PARSE_IMPLICIT_DOCUMENT_START_STATE: + return "yaml_PARSE_IMPLICIT_DOCUMENT_START_STATE" + case yaml_PARSE_DOCUMENT_START_STATE: + return "yaml_PARSE_DOCUMENT_START_STATE" + case yaml_PARSE_DOCUMENT_CONTENT_STATE: + return "yaml_PARSE_DOCUMENT_CONTENT_STATE" + case yaml_PARSE_DOCUMENT_END_STATE: + return "yaml_PARSE_DOCUMENT_END_STATE" + case yaml_PARSE_BLOCK_NODE_STATE: + return "yaml_PARSE_BLOCK_NODE_STATE" + case yaml_PARSE_BLOCK_NODE_OR_INDENTLESS_SEQUENCE_STATE: + return "yaml_PARSE_BLOCK_NODE_OR_INDENTLESS_SEQUENCE_STATE" + case yaml_PARSE_FLOW_NODE_STATE: + return "yaml_PARSE_FLOW_NODE_STATE" + case yaml_PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE: + return "yaml_PARSE_BLOCK_SEQUENCE_FIRST_ENTRY_STATE" + case yaml_PARSE_BLOCK_SEQUENCE_ENTRY_STATE: + return "yaml_PARSE_BLOCK_SEQUENCE_ENTRY_STATE" + case yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE: + return "yaml_PARSE_INDENTLESS_SEQUENCE_ENTRY_STATE" + case yaml_PARSE_BLOCK_MAPPING_FIRST_KEY_STATE: + return "yaml_PARSE_BLOCK_MAPPING_FIRST_KEY_STATE" + case yaml_PARSE_BLOCK_MAPPING_KEY_STATE: + return "yaml_PARSE_BLOCK_MAPPING_KEY_STATE" + case yaml_PARSE_BLOCK_MAPPING_VALUE_STATE: + return "yaml_PARSE_BLOCK_MAPPING_VALUE_STATE" + case yaml_PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE: + return "yaml_PARSE_FLOW_SEQUENCE_FIRST_ENTRY_STATE" + case yaml_PARSE_FLOW_SEQUENCE_ENTRY_STATE: + return "yaml_PARSE_FLOW_SEQUENCE_ENTRY_STATE" + case yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE: + return "yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_KEY_STATE" + case yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE: + return "yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_VALUE_STATE" + case yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE: + return "yaml_PARSE_FLOW_SEQUENCE_ENTRY_MAPPING_END_STATE" + case yaml_PARSE_FLOW_MAPPING_FIRST_KEY_STATE: + return "yaml_PARSE_FLOW_MAPPING_FIRST_KEY_STATE" + case yaml_PARSE_FLOW_MAPPING_KEY_STATE: + return "yaml_PARSE_FLOW_MAPPING_KEY_STATE" + case yaml_PARSE_FLOW_MAPPING_VALUE_STATE: + return "yaml_PARSE_FLOW_MAPPING_VALUE_STATE" + case yaml_PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE: + return "yaml_PARSE_FLOW_MAPPING_EMPTY_VALUE_STATE" + case yaml_PARSE_END_STATE: + return "yaml_PARSE_END_STATE" + } + return "" +} + +// This structure holds aliases data. +type yaml_alias_data_t struct { + anchor []byte // The anchor. + index int // The node id. + mark yaml_mark_t // The anchor mark. +} + +// The parser structure. +// +// All members are internal. Manage the structure using the +// yaml_parser_ family of functions. +type yaml_parser_t struct { + + // Error handling + + error yaml_error_type_t // Error type. + + problem string // Error description. + + // The byte about which the problem occurred. + problem_offset int + problem_value int + problem_mark yaml_mark_t + + // The error context. + context string + context_mark yaml_mark_t + + // Reader stuff + + read_handler yaml_read_handler_t // Read handler. + + input_reader io.Reader // File input data. + input []byte // String input data. + input_pos int + + eof bool // EOF flag + + buffer []byte // The working buffer. + buffer_pos int // The current position of the buffer. + + unread int // The number of unread characters in the buffer. + + newlines int // The number of line breaks since last non-break/non-blank character + + raw_buffer []byte // The raw buffer. + raw_buffer_pos int // The current position of the buffer. + + encoding yaml_encoding_t // The input encoding. + + offset int // The offset of the current position (in bytes). + mark yaml_mark_t // The mark of the current position. + + // Comments + + head_comment []byte // The current head comments + line_comment []byte // The current line comments + foot_comment []byte // The current foot comments + tail_comment []byte // Foot comment that happens at the end of a block. + stem_comment []byte // Comment in item preceding a nested structure (list inside list item, etc) + + comments []yaml_comment_t // The folded comments for all parsed tokens + comments_head int + + // Scanner stuff + + stream_start_produced bool // Have we started to scan the input stream? + stream_end_produced bool // Have we reached the end of the input stream? + + flow_level int // The number of unclosed '[' and '{' indicators. + + tokens []yaml_token_t // The tokens queue. + tokens_head int // The head of the tokens queue. + tokens_parsed int // The number of tokens fetched from the queue. + token_available bool // Does the tokens queue contain a token ready for dequeueing. + + indent int // The current indentation level. + indents []int // The indentation levels stack. + + simple_key_allowed bool // May a simple key occur at the current position? + simple_keys []yaml_simple_key_t // The stack of simple keys. + simple_keys_by_tok map[int]int // possible simple_key indexes indexed by token_number + + // Parser stuff + + state yaml_parser_state_t // The current parser state. + states []yaml_parser_state_t // The parser states stack. + marks []yaml_mark_t // The stack of marks. + tag_directives []yaml_tag_directive_t // The list of TAG directives. + + // Dumper stuff + + aliases []yaml_alias_data_t // The alias data. + + document *yaml_document_t // The currently parsed document. +} + +type yaml_comment_t struct { + + scan_mark yaml_mark_t // Position where scanning for comments started + token_mark yaml_mark_t // Position after which tokens will be associated with this comment + start_mark yaml_mark_t // Position of '#' comment mark + end_mark yaml_mark_t // Position where comment terminated + + head []byte + line []byte + foot []byte +} + +// Emitter Definitions + +// The prototype of a write handler. +// +// The write handler is called when the emitter needs to flush the accumulated +// characters to the output. The handler should write @a size bytes of the +// @a buffer to the output. +// +// @param[in,out] data A pointer to an application data specified by +// yaml_emitter_set_output(). +// @param[in] buffer The buffer with bytes to be written. +// @param[in] size The size of the buffer. +// +// @returns On success, the handler should return @c 1. If the handler failed, +// the returned value should be @c 0. +// +type yaml_write_handler_t func(emitter *yaml_emitter_t, buffer []byte) error + +type yaml_emitter_state_t int + +// The emitter states. +const ( + // Expect STREAM-START. + yaml_EMIT_STREAM_START_STATE yaml_emitter_state_t = iota + + yaml_EMIT_FIRST_DOCUMENT_START_STATE // Expect the first DOCUMENT-START or STREAM-END. + yaml_EMIT_DOCUMENT_START_STATE // Expect DOCUMENT-START or STREAM-END. + yaml_EMIT_DOCUMENT_CONTENT_STATE // Expect the content of a document. + yaml_EMIT_DOCUMENT_END_STATE // Expect DOCUMENT-END. + yaml_EMIT_FLOW_SEQUENCE_FIRST_ITEM_STATE // Expect the first item of a flow sequence. + yaml_EMIT_FLOW_SEQUENCE_TRAIL_ITEM_STATE // Expect the next item of a flow sequence, with the comma already written out + yaml_EMIT_FLOW_SEQUENCE_ITEM_STATE // Expect an item of a flow sequence. + yaml_EMIT_FLOW_MAPPING_FIRST_KEY_STATE // Expect the first key of a flow mapping. + yaml_EMIT_FLOW_MAPPING_TRAIL_KEY_STATE // Expect the next key of a flow mapping, with the comma already written out + yaml_EMIT_FLOW_MAPPING_KEY_STATE // Expect a key of a flow mapping. + yaml_EMIT_FLOW_MAPPING_SIMPLE_VALUE_STATE // Expect a value for a simple key of a flow mapping. + yaml_EMIT_FLOW_MAPPING_VALUE_STATE // Expect a value of a flow mapping. + yaml_EMIT_BLOCK_SEQUENCE_FIRST_ITEM_STATE // Expect the first item of a block sequence. + yaml_EMIT_BLOCK_SEQUENCE_ITEM_STATE // Expect an item of a block sequence. + yaml_EMIT_BLOCK_MAPPING_FIRST_KEY_STATE // Expect the first key of a block mapping. + yaml_EMIT_BLOCK_MAPPING_KEY_STATE // Expect the key of a block mapping. + yaml_EMIT_BLOCK_MAPPING_SIMPLE_VALUE_STATE // Expect a value for a simple key of a block mapping. + yaml_EMIT_BLOCK_MAPPING_VALUE_STATE // Expect a value of a block mapping. + yaml_EMIT_END_STATE // Expect nothing. +) + +// The emitter structure. +// +// All members are internal. Manage the structure using the @c yaml_emitter_ +// family of functions. +type yaml_emitter_t struct { + + // Error handling + + error yaml_error_type_t // Error type. + problem string // Error description. + + // Writer stuff + + write_handler yaml_write_handler_t // Write handler. + + output_buffer *[]byte // String output data. + output_writer io.Writer // File output data. + + buffer []byte // The working buffer. + buffer_pos int // The current position of the buffer. + + raw_buffer []byte // The raw buffer. + raw_buffer_pos int // The current position of the buffer. + + encoding yaml_encoding_t // The stream encoding. + + // Emitter stuff + + canonical bool // If the output is in the canonical style? + best_indent int // The number of indentation spaces. + best_width int // The preferred width of the output lines. + unicode bool // Allow unescaped non-ASCII characters? + line_break yaml_break_t // The preferred line break. + + state yaml_emitter_state_t // The current emitter state. + states []yaml_emitter_state_t // The stack of states. + + events []yaml_event_t // The event queue. + events_head int // The head of the event queue. + + indents []int // The stack of indentation levels. + + tag_directives []yaml_tag_directive_t // The list of tag directives. + + indent int // The current indentation level. + + flow_level int // The current flow level. + + root_context bool // Is it the document root context? + sequence_context bool // Is it a sequence context? + mapping_context bool // Is it a mapping context? + simple_key_context bool // Is it a simple mapping key context? + + line int // The current line. + column int // The current column. + whitespace bool // If the last character was a whitespace? + indention bool // If the last character was an indentation character (' ', '-', '?', ':')? + open_ended bool // If an explicit document end is required? + + space_above bool // Is there's an empty line above? + foot_indent int // The indent used to write the foot comment above, or -1 if none. + + // Anchor analysis. + anchor_data struct { + anchor []byte // The anchor value. + alias bool // Is it an alias? + } + + // Tag analysis. + tag_data struct { + handle []byte // The tag handle. + suffix []byte // The tag suffix. + } + + // Scalar analysis. + scalar_data struct { + value []byte // The scalar value. + multiline bool // Does the scalar contain line breaks? + flow_plain_allowed bool // Can the scalar be expessed in the flow plain style? + block_plain_allowed bool // Can the scalar be expressed in the block plain style? + single_quoted_allowed bool // Can the scalar be expressed in the single quoted style? + block_allowed bool // Can the scalar be expressed in the literal or folded styles? + style yaml_scalar_style_t // The output style. + } + + // Comments + head_comment []byte + line_comment []byte + foot_comment []byte + tail_comment []byte + + key_line_comment []byte + + // Dumper stuff + + opened bool // If the stream was already opened? + closed bool // If the stream was already closed? + + // The information associated with the document nodes. + anchors *struct { + references int // The number of references. + anchor int // The anchor id. + serialized bool // If the node has been emitted? + } + + last_anchor_id int // The last assigned anchor id. + + document *yaml_document_t // The currently emitted document. +} diff --git a/asyncapi/vendor/gopkg.in/yaml.v3/yamlprivateh.go b/asyncapi/vendor/gopkg.in/yaml.v3/yamlprivateh.go new file mode 100644 index 0000000..e88f9c5 --- /dev/null +++ b/asyncapi/vendor/gopkg.in/yaml.v3/yamlprivateh.go @@ -0,0 +1,198 @@ +// +// Copyright (c) 2011-2019 Canonical Ltd +// Copyright (c) 2006-2010 Kirill Simonov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package yaml + +const ( + // The size of the input raw buffer. + input_raw_buffer_size = 512 + + // The size of the input buffer. + // It should be possible to decode the whole raw buffer. + input_buffer_size = input_raw_buffer_size * 3 + + // The size of the output buffer. + output_buffer_size = 128 + + // The size of the output raw buffer. + // It should be possible to encode the whole output buffer. + output_raw_buffer_size = (output_buffer_size*2 + 2) + + // The size of other stacks and queues. + initial_stack_size = 16 + initial_queue_size = 16 + initial_string_size = 16 +) + +// Check if the character at the specified position is an alphabetical +// character, a digit, '_', or '-'. +func is_alpha(b []byte, i int) bool { + return b[i] >= '0' && b[i] <= '9' || b[i] >= 'A' && b[i] <= 'Z' || b[i] >= 'a' && b[i] <= 'z' || b[i] == '_' || b[i] == '-' +} + +// Check if the character at the specified position is a digit. +func is_digit(b []byte, i int) bool { + return b[i] >= '0' && b[i] <= '9' +} + +// Get the value of a digit. +func as_digit(b []byte, i int) int { + return int(b[i]) - '0' +} + +// Check if the character at the specified position is a hex-digit. +func is_hex(b []byte, i int) bool { + return b[i] >= '0' && b[i] <= '9' || b[i] >= 'A' && b[i] <= 'F' || b[i] >= 'a' && b[i] <= 'f' +} + +// Get the value of a hex-digit. +func as_hex(b []byte, i int) int { + bi := b[i] + if bi >= 'A' && bi <= 'F' { + return int(bi) - 'A' + 10 + } + if bi >= 'a' && bi <= 'f' { + return int(bi) - 'a' + 10 + } + return int(bi) - '0' +} + +// Check if the character is ASCII. +func is_ascii(b []byte, i int) bool { + return b[i] <= 0x7F +} + +// Check if the character at the start of the buffer can be printed unescaped. +func is_printable(b []byte, i int) bool { + return ((b[i] == 0x0A) || // . == #x0A + (b[i] >= 0x20 && b[i] <= 0x7E) || // #x20 <= . <= #x7E + (b[i] == 0xC2 && b[i+1] >= 0xA0) || // #0xA0 <= . <= #xD7FF + (b[i] > 0xC2 && b[i] < 0xED) || + (b[i] == 0xED && b[i+1] < 0xA0) || + (b[i] == 0xEE) || + (b[i] == 0xEF && // #xE000 <= . <= #xFFFD + !(b[i+1] == 0xBB && b[i+2] == 0xBF) && // && . != #xFEFF + !(b[i+1] == 0xBF && (b[i+2] == 0xBE || b[i+2] == 0xBF)))) +} + +// Check if the character at the specified position is NUL. +func is_z(b []byte, i int) bool { + return b[i] == 0x00 +} + +// Check if the beginning of the buffer is a BOM. +func is_bom(b []byte, i int) bool { + return b[0] == 0xEF && b[1] == 0xBB && b[2] == 0xBF +} + +// Check if the character at the specified position is space. +func is_space(b []byte, i int) bool { + return b[i] == ' ' +} + +// Check if the character at the specified position is tab. +func is_tab(b []byte, i int) bool { + return b[i] == '\t' +} + +// Check if the character at the specified position is blank (space or tab). +func is_blank(b []byte, i int) bool { + //return is_space(b, i) || is_tab(b, i) + return b[i] == ' ' || b[i] == '\t' +} + +// Check if the character at the specified position is a line break. +func is_break(b []byte, i int) bool { + return (b[i] == '\r' || // CR (#xD) + b[i] == '\n' || // LF (#xA) + b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) + b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) + b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9) // PS (#x2029) +} + +func is_crlf(b []byte, i int) bool { + return b[i] == '\r' && b[i+1] == '\n' +} + +// Check if the character is a line break or NUL. +func is_breakz(b []byte, i int) bool { + //return is_break(b, i) || is_z(b, i) + return ( + // is_break: + b[i] == '\r' || // CR (#xD) + b[i] == '\n' || // LF (#xA) + b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) + b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) + b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9 || // PS (#x2029) + // is_z: + b[i] == 0) +} + +// Check if the character is a line break, space, or NUL. +func is_spacez(b []byte, i int) bool { + //return is_space(b, i) || is_breakz(b, i) + return ( + // is_space: + b[i] == ' ' || + // is_breakz: + b[i] == '\r' || // CR (#xD) + b[i] == '\n' || // LF (#xA) + b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) + b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) + b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9 || // PS (#x2029) + b[i] == 0) +} + +// Check if the character is a line break, space, tab, or NUL. +func is_blankz(b []byte, i int) bool { + //return is_blank(b, i) || is_breakz(b, i) + return ( + // is_blank: + b[i] == ' ' || b[i] == '\t' || + // is_breakz: + b[i] == '\r' || // CR (#xD) + b[i] == '\n' || // LF (#xA) + b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) + b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) + b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9 || // PS (#x2029) + b[i] == 0) +} + +// Determine the width of the character. +func width(b byte) int { + // Don't replace these by a switch without first + // confirming that it is being inlined. + if b&0x80 == 0x00 { + return 1 + } + if b&0xE0 == 0xC0 { + return 2 + } + if b&0xF0 == 0xE0 { + return 3 + } + if b&0xF8 == 0xF0 { + return 4 + } + return 0 + +} diff --git a/asyncapi/vendor/modules.txt b/asyncapi/vendor/modules.txt new file mode 100644 index 0000000..673664e --- /dev/null +++ b/asyncapi/vendor/modules.txt @@ -0,0 +1,30 @@ +# cloud.google.com/go v0.123.0 +## explicit; go 1.24.0 +cloud.google.com/go/civil +# github.com/MarkRosemaker/errpath v0.0.0-20260425165607-bbd4959d04d9 +## explicit; go 1.23.2 +github.com/MarkRosemaker/errpath +# github.com/MarkRosemaker/json2yaml v0.0.0-20260507220148-d6cc0d01bff0 +## explicit; go 1.23.4 +github.com/MarkRosemaker/json2yaml +# github.com/MarkRosemaker/jsonutil v0.0.0-20260718153618-78b5039427a4 +## explicit; go 1.26.3 +github.com/MarkRosemaker/jsonutil +# github.com/MarkRosemaker/ordmap v0.0.0-20260727220052-3181ae3b2977 +## explicit; go 1.25.0 +github.com/MarkRosemaker/ordmap +# github.com/MarkRosemaker/yaml v0.0.0-20260508005758-fe21a538b084 +## explicit; go 1.23.4 +github.com/MarkRosemaker/yaml +# github.com/MarkRosemaker/yaml2json v0.0.0-20260507220136-7748efc522b2 +## explicit; go 1.23.4 +github.com/MarkRosemaker/yaml2json +# github.com/go-api-libs/types v0.0.0-20251210072721-82754f56609d +## explicit; go 1.23.2 +github.com/go-api-libs/types +# golang.org/x/exp v0.0.0-20260727155853-b88d891fe743 +## explicit; go 1.25.0 +golang.org/x/exp/maps +# gopkg.in/yaml.v3 v3.0.1 +## explicit +gopkg.in/yaml.v3