diff --git a/.github/workflows/release-beta.yml b/.github/workflows/release-beta.yml index 9cbebb05..7a5e69c6 100644 --- a/.github/workflows/release-beta.yml +++ b/.github/workflows/release-beta.yml @@ -38,9 +38,15 @@ jobs: - name: Setup .NET Core @ Latest uses: actions/setup-dotnet@v1 - + + - name: run number with offset + env: + NUM: ${{ github.run_number }} + run: | + echo GITHUB_RUN_NUMBER_WITH_OFFSET=$(($NUM+100)) >> "$GITHUB_ENV" + - run: echo ${{env.GITHUB_RUN_NUMBER_WITH_OFFSET}} - name: Build ${{ matrix.package-name }} project and pack NuGet package - run: dotnet pack src/${{ matrix.package-name }}/${{ matrix.package-name }}.csproj -c Release -o out-${{ matrix.package-name }} -p:PackageVersion=${{ needs.check.outputs.version }}-beta.${{github.run_number}} + run: dotnet pack src/${{ matrix.package-name }}/${{ matrix.package-name }}.csproj -c Release -o out-${{ matrix.package-name }} -p:PackageVersion=${{ needs.check.outputs.version }}-beta.${{env.GITHUB_RUN_NUMBER_WITH_OFFSET}} - name: Push generated package to GitHub Packages registry run: dotnet nuget push out-${{ matrix.package-name }}/*.nupkg -s https://api.nuget.org/v3/index.json --skip-duplicate -n --api-key ${{secrets.NUGET}} diff --git a/.github/workflows/release-manual.yml b/.github/workflows/release-manual.yml new file mode 100644 index 00000000..0eeca64b --- /dev/null +++ b/.github/workflows/release-manual.yml @@ -0,0 +1,28 @@ +name: Publish custom NuGet package version +on: + workflow_dispatch: + inputs: + package_version: + description: 'NuGet package version (e.g. 6.0.0-beta.1041)' + required: true + +jobs: + pre-release: + runs-on: ubuntu-latest + name: Publish NuGet packages + environment: AsyncAPI + strategy: + matrix: + package-name: [ "LEGO.AsyncAPI", "LEGO.AsyncAPI.Readers", "LEGO.AsyncAPI.Bindings" ] + steps: + - name: Checkout repository + uses: actions/checkout@v1 + + - name: Setup .NET Core @ Latest + uses: actions/setup-dotnet@v1 + + - name: Build ${{ matrix.package-name }} project and pack NuGet package + run: dotnet pack src/${{ matrix.package-name }}/${{ matrix.package-name }}.csproj -c Release -o out-${{ matrix.package-name }} -p:PackageVersion=${{ github.event.inputs.package_version }} + + - name: Push generated package to NuGet + run: dotnet nuget push out-${{ matrix.package-name }}/*.nupkg -s https://api.nuget.org/v3/index.json --skip-duplicate -n --api-key ${{secrets.NUGET}} diff --git a/README.md b/README.md index ce65fd33..9f882a1a 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,48 @@ var stream = await httpClient.GetStreamAsync("master/examples/streetlights-kafka var asyncApiDocument = new AsyncApiStreamReader().Read(stream, out var diagnostic); ``` +#### Reading External $ref + +You can read externally referenced AsyncAPI documents by setting the `ReferenceResolution` property of the `AsyncApiReaderSettings` object to `ReferenceResolutionSetting.ResolveAllReferences` and providing an implementation for the `IAsyncApiExternalReferenceReader` interface. This interface contains a single method to which the built AsyncAPI.NET reader library will pass the location content contained in a `$ref` property (usually some form of path) and interface will return the content which is retrieved from wherever the `$ref` points to as a `string`. The AsyncAPI.NET reader will then automatically infer the `T` type of the content and recursively parse the external content into an AsyncAPI document as a child of the original document that contained the `$ref`. This means that you can have externally referenced documents that themselves contain external references. + +This interface allows users to load the content of their external reference however and from whereever is required. A new instance of the implementor of `IAsyncApiExternalReferenceReader` should be registered with the `ExternalReferenceReader` property of the `AsyncApiReaderSettings` when creating the reader from which the `GetExternalResource` method will be called when resolving external references. + +Below is a very simple example of implementation for `IAsyncApiExternalReferenceReader` that simply loads a file and returns it as a string found at the reference endpoint. +```csharp +using System.IO; + +public class AsyncApiExternalFileSystemReader : IAsyncApiExternalReferenceReader +{ + public string Load(string reference) + { + return File.ReadAllText(reference); + } +} +``` + +This can then be configured in the reader as follows: +```csharp +var settings = new AsyncApiReaderSettings +{ + ReferenceResolution = ReferenceResolutionSetting.ResolveAllReferences, + ExternalReferenceReader = new AsyncApiExternalFileSystemReader(), +}; +var reader = new AsyncApiStringReader(settings); +``` + +This would function for a AsyncAPI document with following reference to load the content of `message.yaml` as a `AsyncApiMessage` object inline with the document object. +```yaml +asyncapi: 2.3.0 +info: + title: test + version: 1.0.0 +channels: + workspace: + publish: + message: + $ref: "../../../message.yaml" +``` + ### Bindings To add support for reading bindings, simply add the bindings you wish to support, to the `Bindings` collection of `AsyncApiReaderSettings`. There is a nifty helper to add different types of bindings, or like in the example `All` of them. @@ -99,4 +141,4 @@ This project welcomes contributions and suggestions. Do you want to contribute to the project? Find out how [here](CONTRIBUTING.md). ## License -[Modified Apache 2.0 (Section 6)](https://github.com/LEGO/AsyncAPI.NET/blob/main/LICENSE) +[Modified Apache 2.0 (Section 6)](https://github.com/LEGO/AsyncAPI.NET/blob/main/LICENSE.txt) diff --git a/src/LEGO.AsyncAPI.Bindings/AMQP/AMQPOperationBinding.cs b/src/LEGO.AsyncAPI.Bindings/AMQP/AMQPOperationBinding.cs index 86dc74ef..3a2e6a35 100644 --- a/src/LEGO.AsyncAPI.Bindings/AMQP/AMQPOperationBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/AMQP/AMQPOperationBinding.cs @@ -5,7 +5,6 @@ namespace LEGO.AsyncAPI.Bindings.AMQP using System; using System.Collections.Generic; using LEGO.AsyncAPI.Models; - using LEGO.AsyncAPI.Readers; using LEGO.AsyncAPI.Readers.ParseNodes; using LEGO.AsyncAPI.Writers; diff --git a/src/LEGO.AsyncAPI.Bindings/BindingsCollection.cs b/src/LEGO.AsyncAPI.Bindings/BindingsCollection.cs index 4fd5560f..83faf695 100644 --- a/src/LEGO.AsyncAPI.Bindings/BindingsCollection.cs +++ b/src/LEGO.AsyncAPI.Bindings/BindingsCollection.cs @@ -45,7 +45,7 @@ public static TCollection Add( return destination; } - public static IEnumerable> All => new List> + public static ICollection> All => new List> { Pulsar, Kafka, @@ -57,18 +57,18 @@ public static TCollection Add( MQTT, }; - public static IEnumerable> Http => new List> + public static ICollection> Http => new List> { new HttpOperationBinding(), new HttpMessageBinding(), }; - public static IEnumerable> Websockets => new List> + public static ICollection> Websockets => new List> { new WebSocketsChannelBinding(), }; - public static IEnumerable> Kafka => new List> + public static ICollection> Kafka => new List> { new KafkaServerBinding(), new KafkaChannelBinding(), @@ -76,32 +76,32 @@ public static TCollection Add( new KafkaMessageBinding(), }; - public static IEnumerable> Pulsar => new List> + public static ICollection> Pulsar => new List> { new PulsarServerBinding(), new PulsarChannelBinding(), }; - public static IEnumerable> Sqs => new List> + public static ICollection> Sqs => new List> { new SqsChannelBinding(), new SqsOperationBinding(), }; - public static IEnumerable> Sns => new List> + public static ICollection> Sns => new List> { new SnsChannelBinding(), new SnsOperationBinding(), }; - public static IEnumerable> AMQP => new List> + public static ICollection> AMQP => new List> { new AMQPChannelBinding(), new AMQPOperationBinding(), new AMQPMessageBinding(), }; - public static IEnumerable> MQTT => new List> + public static ICollection> MQTT => new List> { new MQTTServerBinding(), new MQTTOperationBinding(), diff --git a/src/LEGO.AsyncAPI.Bindings/Http/HttpMessageBinding.cs b/src/LEGO.AsyncAPI.Bindings/Http/HttpMessageBinding.cs index 7a5731ef..cbf65bc4 100644 --- a/src/LEGO.AsyncAPI.Bindings/Http/HttpMessageBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/Http/HttpMessageBinding.cs @@ -42,7 +42,7 @@ public override void SerializeProperties(IAsyncApiWriter writer) protected override FixedFieldMap FixedFieldMap => new() { { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, - { "headers", (a, n) => { a.Headers = JsonSchemaDeserializer.LoadSchema(n); } }, + { "headers", (a, n) => { a.Headers = AsyncApiSchemaDeserializer.LoadSchema(n); } }, }; } } diff --git a/src/LEGO.AsyncAPI.Bindings/Http/HttpOperationBinding.cs b/src/LEGO.AsyncAPI.Bindings/Http/HttpOperationBinding.cs index f70858c2..7b88d31b 100644 --- a/src/LEGO.AsyncAPI.Bindings/Http/HttpOperationBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/Http/HttpOperationBinding.cs @@ -63,7 +63,7 @@ public override void SerializeProperties(IAsyncApiWriter writer) { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, { "type", (a, n) => { a.Type = n.GetScalarValue().GetEnumFromDisplayName(); } }, { "method", (a, n) => { a.Method = n.GetScalarValue(); } }, - { "query", (a, n) => { a.Query = JsonSchemaDeserializer.LoadSchema(n); } }, + { "query", (a, n) => { a.Query = AsyncApiSchemaDeserializer.LoadSchema(n); } }, }; public override string BindingKey => "http"; diff --git a/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaMessageBinding.cs b/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaMessageBinding.cs index 2f665560..2e57b50a 100644 --- a/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaMessageBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaMessageBinding.cs @@ -67,7 +67,7 @@ public override void SerializeProperties(IAsyncApiWriter writer) protected override FixedFieldMap FixedFieldMap => new() { { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, - { "key", (a, n) => { a.Key = JsonSchemaDeserializer.LoadSchema(n); } }, + { "key", (a, n) => { a.Key = AsyncApiSchemaDeserializer.LoadSchema(n); } }, { "schemaIdLocation", (a, n) => { a.SchemaIdLocation = n.GetScalarValue(); } }, { "schemaIdPayloadEncoding", (a, n) => { a.SchemaIdPayloadEncoding = n.GetScalarValue(); } }, { "schemaLookupStrategy", (a, n) => { a.SchemaLookupStrategy = n.GetScalarValue(); } }, diff --git a/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaOperationBinding.cs b/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaOperationBinding.cs index 53db7ae0..b6c6e046 100644 --- a/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaOperationBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaOperationBinding.cs @@ -28,8 +28,8 @@ public class KafkaOperationBinding : OperationBinding protected override FixedFieldMap FixedFieldMap => new() { { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, - { "groupId", (a, n) => { a.GroupId = JsonSchemaDeserializer.LoadSchema(n); } }, - { "clientId", (a, n) => { a.ClientId = JsonSchemaDeserializer.LoadSchema(n); } }, + { "groupId", (a, n) => { a.GroupId = AsyncApiSchemaDeserializer.LoadSchema(n); } }, + { "clientId", (a, n) => { a.ClientId = AsyncApiSchemaDeserializer.LoadSchema(n); } }, }; /// diff --git a/src/LEGO.AsyncAPI.Bindings/MQTT/LastWill.cs b/src/LEGO.AsyncAPI.Bindings/MQTT/LastWill.cs index 23118bfa..955ad90f 100644 --- a/src/LEGO.AsyncAPI.Bindings/MQTT/LastWill.cs +++ b/src/LEGO.AsyncAPI.Bindings/MQTT/LastWill.cs @@ -2,9 +2,9 @@ namespace LEGO.AsyncAPI.Bindings.MQTT { + using System; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Writers; - using System; public class LastWill : IAsyncApiElement { diff --git a/src/LEGO.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs b/src/LEGO.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs index b48e5ae9..597cc4c7 100644 --- a/src/LEGO.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs @@ -57,7 +57,7 @@ public override void SerializeProperties(IAsyncApiWriter writer) protected override FixedFieldMap FixedFieldMap => new() { { "payloadFormatIndicator", (a, n) => { a.PayloadFormatIndicator = n.GetIntegerValueOrDefault(); } }, - { "correlationData", (a, n) => { a.CorrelationData = JsonSchemaDeserializer.LoadSchema(n); } }, + { "correlationData", (a, n) => { a.CorrelationData = AsyncApiSchemaDeserializer.LoadSchema(n); } }, { "contentType", (a, n) => { a.ContentType = n.GetScalarValue(); } }, { "responseTopic", (a, n) => { a.ResponseTopic = n.GetScalarValue(); } }, }; diff --git a/src/LEGO.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs b/src/LEGO.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs index d3155ecd..f58eede9 100644 --- a/src/LEGO.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs @@ -3,9 +3,6 @@ namespace LEGO.AsyncAPI.Bindings.MQTT { using System; - using System.Collections.Generic; - using LEGO.AsyncAPI.Models; - using LEGO.AsyncAPI.Readers; using LEGO.AsyncAPI.Readers.ParseNodes; using LEGO.AsyncAPI.Writers; diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/Ordering.cs b/src/LEGO.AsyncAPI.Bindings/Sns/Ordering.cs index 23f69f52..d44a0c9e 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sns/Ordering.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sns/Ordering.cs @@ -11,7 +11,7 @@ namespace LEGO.AsyncAPI.Bindings.Sns public class Ordering : IAsyncApiExtensible { /// - /// What type of SNS Topic is this? + /// What type of SNS Topic is this?. /// public OrderingType Type { get; set; } diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/Queue.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/Queue.cs index 33166af5..eb3d256a 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sqs/Queue.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/Queue.cs @@ -16,7 +16,7 @@ public class Queue : IAsyncApiExtensible public string Name { get; set; } /// - /// Is this a FIFO queue? + /// Is this a FIFO queue?. /// public bool FifoQueue { get; set; } @@ -56,7 +56,7 @@ public class Queue : IAsyncApiExtensible public RedrivePolicy RedrivePolicy { get; set; } /// - /// The security policy for the SQS Queue + /// The security policy for the SQS Queue. /// public Policy Policy { get; set; } diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/RedrivePolicy.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/RedrivePolicy.cs index 923d668c..538b1edf 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sqs/RedrivePolicy.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/RedrivePolicy.cs @@ -3,9 +3,9 @@ namespace LEGO.AsyncAPI.Bindings.Sqs { using System; + using System.Collections.Generic; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Writers; - using System.Collections.Generic; public class RedrivePolicy : IAsyncApiExtensible { diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/SqsOperationBinding.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/SqsOperationBinding.cs index 0beb89b8..66f15314 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sqs/SqsOperationBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/SqsOperationBinding.cs @@ -10,7 +10,7 @@ namespace LEGO.AsyncAPI.Bindings.Sqs public class SqsOperationBinding : OperationBinding { /// - /// Queue objects that are either the endpoint for an SNS Operation Binding Object, or the deadLetterQueue of the SQS Operation Binding Object + /// Queue objects that are either the endpoint for an SNS Operation Binding Object, or the deadLetterQueue of the SQS Operation Binding Object. /// public List Queues { get; set; } diff --git a/src/LEGO.AsyncAPI.Bindings/WebSockets/WebSocketsChannelBinding.cs b/src/LEGO.AsyncAPI.Bindings/WebSockets/WebSocketsChannelBinding.cs index c87393bb..8827bea4 100644 --- a/src/LEGO.AsyncAPI.Bindings/WebSockets/WebSocketsChannelBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/WebSockets/WebSocketsChannelBinding.cs @@ -31,8 +31,8 @@ public class WebSocketsChannelBinding : ChannelBinding { { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, { "method", (a, n) => { a.Method = n.GetScalarValue(); } }, - { "query", (a, n) => { a.Query = JsonSchemaDeserializer.LoadSchema(n); } }, - { "headers", (a, n) => { a.Headers = JsonSchemaDeserializer.LoadSchema(n); } }, + { "query", (a, n) => { a.Query = AsyncApiSchemaDeserializer.LoadSchema(n); } }, + { "headers", (a, n) => { a.Headers = AsyncApiSchemaDeserializer.LoadSchema(n); } }, }; public override void SerializeProperties(IAsyncApiWriter writer) diff --git a/src/LEGO.AsyncAPI.Readers/AsyncApiExternalReferenceResolver.cs b/src/LEGO.AsyncAPI.Readers/AsyncApiExternalReferenceResolver.cs new file mode 100644 index 00000000..0f2044a1 --- /dev/null +++ b/src/LEGO.AsyncAPI.Readers/AsyncApiExternalReferenceResolver.cs @@ -0,0 +1,256 @@ +using LEGO.AsyncAPI.Readers.Exceptions; +using LEGO.AsyncAPI.Services; + +namespace LEGO.AsyncAPI.Readers +{ + using System; + using System.Collections.Generic; + using System.Linq; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Models.Interfaces; + + /// + /// This class is used to walk an AsyncApiDocument and convert unresolved references to references to populated objects. + /// + internal class AsyncApiExternalReferenceResolver : AsyncApiVisitorBase + { + private AsyncApiDocument currentDocument; + private List errors = new List(); + private AsyncApiReaderSettings readerSettings; + + public AsyncApiExternalReferenceResolver( + AsyncApiDocument currentDocument, + AsyncApiReaderSettings readerSettings) + { + this.currentDocument = currentDocument; + this.readerSettings = readerSettings; + } + + public IEnumerable Errors + { + get + { + return this.errors; + } + } + + public override void Visit(IAsyncApiReferenceable referenceable) + { + if (referenceable.Reference != null) + { + referenceable.Reference.HostDocument = this.currentDocument; + } + } + + public override void Visit(AsyncApiComponents components) + { + this.ResolveMap(components.Parameters); + this.ResolveMap(components.Channels); + this.ResolveMap(components.Schemas); + this.ResolveMap(components.Servers); + this.ResolveMap(components.CorrelationIds); + this.ResolveMap(components.MessageTraits); + this.ResolveMap(components.OperationTraits); + this.ResolveMap(components.SecuritySchemes); + this.ResolveMap(components.ChannelBindings); + this.ResolveMap(components.MessageBindings); + this.ResolveMap(components.OperationBindings); + this.ResolveMap(components.ServerBindings); + this.ResolveMap(components.Messages); + } + + public override void Visit(AsyncApiDocument doc) + { + this.ResolveMap(doc.Servers); + this.ResolveMap(doc.Channels); + } + + public override void Visit(AsyncApiChannel channel) + { + this.ResolveMap(channel.Parameters); + this.ResolveObject(channel.Bindings, r => channel.Bindings = r); + } + + public override void Visit(AsyncApiMessageTrait trait) + { + this.ResolveObject(trait.CorrelationId, r => trait.CorrelationId = r); + this.ResolveObject(trait.Headers, r => trait.Headers = r); + } + + /// + /// Resolve all references used in an operation. + /// + public override void Visit(AsyncApiOperation operation) + { + this.ResolveList(operation.Message); + this.ResolveList(operation.Traits); + this.ResolveObject(operation.Bindings, r => operation.Bindings = r); + } + + public override void Visit(AsyncApiMessage message) + { + this.ResolveObject(message.Headers, r => message.Headers = r); + switch (message.Payload) + { + case AsyncApiJsonSchemaPayload json: + this.ResolveObject(message.Payload as AsyncApiJsonSchemaPayload, r => message.Payload = new AsyncApiJsonSchemaPayload(r)); + break; + case AsyncApiAvroSchemaPayload avro: + // ToFix: this might not resolve correctly. + this.ResolveObject(message.Payload as AsyncApiAvroSchemaPayload, r => message.Payload = r); + break; + default: + break; + } + + this.ResolveList(message.Traits); + this.ResolveObject(message.CorrelationId, r => message.CorrelationId = r); + this.ResolveObject(message.Bindings, r => message.Bindings = r); + } + + public override void Visit(AsyncApiServer server) + { + this.ResolveObject(server.Bindings, r => server.Bindings = r); + } + + /// + /// Resolve all references to SecuritySchemes. + /// + public override void Visit(AsyncApiSecurityRequirement securityRequirement) + { + foreach (var scheme in securityRequirement.Keys.ToList()) + { + this.ResolveObject(scheme, (resolvedScheme) => + { + if (resolvedScheme != null) + { + // If scheme was unresolved + // copy Scopes and remove old unresolved scheme + var scopes = securityRequirement[scheme]; + securityRequirement.Remove(scheme); + securityRequirement.Add(resolvedScheme, scopes); + } + }); + } + } + + /// + /// Resolve all references to parameters. + /// + public override void Visit(IList parameters) + { + this.ResolveList(parameters); + } + + /// + /// Resolve all references used in a parameter. + /// + public override void Visit(AsyncApiParameter parameter) + { + this.ResolveObject(parameter.Schema, r => parameter.Schema = r); + } + + /// + /// Resolve all references used in a schema. + /// + public override void Visit(AsyncApiSchema schema) + { + this.ResolveObject(schema.Items, r => schema.Items = r); + this.ResolveList(schema.OneOf); + this.ResolveList(schema.AllOf); + this.ResolveList(schema.AnyOf); + this.ResolveObject(schema.Contains, r => schema.Contains = r); + this.ResolveObject(schema.Else, r => schema.Else = r); + this.ResolveObject(schema.If, r => schema.If = r); + this.ResolveObject(schema.Items, r => schema.Items = r); + this.ResolveObject(schema.Not, r => schema.Not = r); + this.ResolveObject(schema.Then, r => schema.Then = r); + this.ResolveObject(schema.PropertyNames, r => schema.PropertyNames = r); + this.ResolveObject(schema.AdditionalProperties, r => schema.AdditionalProperties = r); + this.ResolveMap(schema.Properties); + } + + private void ResolveObject(T entity, Action assign) + where T : class, IAsyncApiReferenceable, new() + { + if (entity == null) + { + return; + } + + if (this.IsUnresolvedReference(entity)) + { + assign(this.ResolveReference(entity.Reference)); + } + } + + private void ResolveList(IList list) + where T : class, IAsyncApiReferenceable, new() + { + if (list == null) + { + return; + } + + for (int i = 0; i < list.Count; i++) + { + var entity = list[i]; + if (this.IsUnresolvedReference(entity)) + { + list[i] = this.ResolveReference(entity.Reference); + } + } + } + + private void ResolveMap(IDictionary map) + where T : class, IAsyncApiReferenceable, new() + { + if (map == null) + { + return; + } + + foreach (var key in map.Keys.ToList()) + { + var entity = map[key]; + if (this.IsUnresolvedReference(entity)) + { + map[key] = this.ResolveReference(entity.Reference); + } + } + } + + private T ResolveReference(AsyncApiReference reference) + where T : class, IAsyncApiReferenceable, new() + { + if (reference.IsExternal) + { + if (this.readerSettings.ExternalReferenceReader is null) + { + throw new AsyncApiReaderException( + "External reference configured in AsyncApi document but no implementation provided for ExternalReferenceReader."); + } + + // read external content + var externalContent = this.readerSettings.ExternalReferenceReader.Load(reference.Reference); + + // read external object content + var reader = new AsyncApiStringReader(this.readerSettings); + var externalAsyncApiContent = reader.ReadFragment(externalContent, AsyncApiVersion.AsyncApi2_0, out var diagnostic); + foreach (var error in diagnostic.Errors) + { + this.errors.Add(error); + } + + return externalAsyncApiContent; + } + + return null; + } + + private bool IsUnresolvedReference(IAsyncApiReferenceable possibleReference) + { + return (possibleReference != null && possibleReference.UnresolvedReference); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs b/src/LEGO.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs index 3a973654..6b340651 100644 --- a/src/LEGO.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs +++ b/src/LEGO.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs @@ -12,6 +12,7 @@ namespace LEGO.AsyncAPI.Readers using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Readers.Interface; + using LEGO.AsyncAPI.Services; using LEGO.AsyncAPI.Validations; /// @@ -165,23 +166,48 @@ public T ReadFragment(JsonNode input, AsyncApiVersion version, out AsyncApiDi private void ResolveReferences(AsyncApiDiagnostic diagnostic, AsyncApiDocument document) { - var errors = new List(); - - // Resolve References if requested switch (this.settings.ReferenceResolution) { - case ReferenceResolutionSetting.ResolveReferences: - errors.AddRange(document.ResolveReferences()); + case ReferenceResolutionSetting.ResolveAllReferences: + this.ResolveAllReferences(diagnostic, document); + break; + case ReferenceResolutionSetting.ResolveInternalReferences: + this.ResolveInternalReferences(diagnostic, document); break; - case ReferenceResolutionSetting.DoNotResolveReferences: break; } + } + + private void ResolveAllReferences(AsyncApiDiagnostic diagnostic, AsyncApiDocument document) + { + this.ResolveInternalReferences(diagnostic, document); + this.ResolveExternalReferences(diagnostic, document); + } + + private void ResolveInternalReferences(AsyncApiDiagnostic diagnostic, AsyncApiDocument document) + { + var errors = new List(); + + var reader = new AsyncApiStringReader(this.settings); + errors.AddRange(document.ResolveReferences()); foreach (var item in errors) { diagnostic.Errors.Add(item); } } + + private void ResolveExternalReferences(AsyncApiDiagnostic diagnostic, AsyncApiDocument document) + { + var resolver = new AsyncApiExternalReferenceResolver(document, this.settings); + var walker = new AsyncApiWalker(resolver); + walker.Walk(document); + + foreach (var error in resolver.Errors) + { + diagnostic.Errors.Add(error); + } + } } } \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Readers/AsyncApiReaderSettings.cs b/src/LEGO.AsyncAPI.Readers/AsyncApiReaderSettings.cs index 6ca2e129..341a743c 100644 --- a/src/LEGO.AsyncAPI.Readers/AsyncApiReaderSettings.cs +++ b/src/LEGO.AsyncAPI.Readers/AsyncApiReaderSettings.cs @@ -18,9 +18,14 @@ public enum ReferenceResolutionSetting DoNotResolveReferences, /// - /// Resolve internal component references and inline them. + /// Resolve all references and inline them. /// - ResolveReferences, + ResolveInternalReferences, + + /// + /// Resolve internal component references and inline them while leaving external references as placeholder objects with an AsyncApiReference instance and UnresolvedReference set to true. + /// + ResolveAllReferences, } /// @@ -32,7 +37,13 @@ public class AsyncApiReaderSettings : AsyncApiSettings /// Indicates how references in the source document should be handled. /// public ReferenceResolutionSetting ReferenceResolution { get; set; } = - ReferenceResolutionSetting.ResolveReferences; + ReferenceResolutionSetting.ResolveInternalReferences; + + /// + /// Indicates what should happen when unmapped members are encountered during deserialization. + /// Error and Warning will add an error or warning to the diagnostics object. + /// + public UnmappedMemberHandling UnmappedMemberHandling { get; set; } = UnmappedMemberHandling.Error; /// /// Dictionary of parsers for converting extensions into strongly typed classes. @@ -42,7 +53,7 @@ public Dictionary> { get; set; } = new Dictionary>(); - public IEnumerable> + public ICollection> Bindings { get; set; } = new List>(); @@ -57,5 +68,10 @@ public IEnumerable> /// from an object. /// public bool LeaveStreamOpen { get; set; } + + /// + /// External reference reader implementation provided by users for reading external resources. + /// + public IAsyncApiExternalReferenceReader ExternalReferenceReader { get; set; } } } \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Readers/AsyncApiTextReader.cs b/src/LEGO.AsyncAPI.Readers/AsyncApiTextReader.cs index eeece6a7..8d1eafc6 100644 --- a/src/LEGO.AsyncAPI.Readers/AsyncApiTextReader.cs +++ b/src/LEGO.AsyncAPI.Readers/AsyncApiTextReader.cs @@ -4,7 +4,6 @@ namespace LEGO.AsyncAPI.Readers { using System.IO; using System.Linq; - using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Nodes; using System.Threading; diff --git a/src/LEGO.AsyncAPI.Readers/Exceptions/AsyncApiUnsupportedSpecVersionException.cs b/src/LEGO.AsyncAPI.Readers/Exceptions/AsyncApiUnsupportedSpecVersionException.cs index d68da4bf..590b3d73 100644 --- a/src/LEGO.AsyncAPI.Readers/Exceptions/AsyncApiUnsupportedSpecVersionException.cs +++ b/src/LEGO.AsyncAPI.Readers/Exceptions/AsyncApiUnsupportedSpecVersionException.cs @@ -29,7 +29,7 @@ public AsyncApiUnsupportedSpecVersionException(string specificationVersion) /// inner exception. /// /// Version that caused this exception to be thrown. - /// The setting used for reading and writing + /// The setting used for reading and writing. /// Inner exception that caused this exception to be thrown. public AsyncApiUnsupportedSpecVersionException(string specificationVersion, Exception innerException) : base(string.Format(CultureInfo.InvariantCulture, MessagePattern, specificationVersion), innerException) diff --git a/src/LEGO.AsyncAPI.Readers/Interface/IAsyncApiExternalReferenceReader.cs b/src/LEGO.AsyncAPI.Readers/Interface/IAsyncApiExternalReferenceReader.cs new file mode 100644 index 00000000..6eef5608 --- /dev/null +++ b/src/LEGO.AsyncAPI.Readers/Interface/IAsyncApiExternalReferenceReader.cs @@ -0,0 +1,14 @@ +namespace LEGO.AsyncAPI.Readers; + +/// +/// Interface that provides method for reading external references.å. +/// +public interface IAsyncApiExternalReferenceReader +{ + /// + /// Method that returns the AsyncAPI content that the external reference from the $ref points to. + /// + /// The content address of the $ref. + /// The content of the reference as a string. + public string Load(string reference); +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Readers/ParseNodes/MapNode.cs b/src/LEGO.AsyncAPI.Readers/ParseNodes/MapNode.cs index b087c875..63bea6fc 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/MapNode.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/MapNode.cs @@ -214,6 +214,14 @@ public override AsyncApiAny CreateAny() return new AsyncApiAny(this.node); } + public void ParseFields(ref T parentInstance, IDictionary> fixedFields, IDictionary, Action> patternFields) + { + foreach (var propertyNode in this) + { + propertyNode.ParseField(parentInstance, fixedFields, patternFields); + } + } + private string ToScalarValue(JsonNode node) { var scalarNode = node is JsonValue value ? value : throw new AsyncApiException($"Expected scalar value"); diff --git a/src/LEGO.AsyncAPI.Readers/ParseNodes/PropertyNode.cs b/src/LEGO.AsyncAPI.Readers/ParseNodes/PropertyNode.cs index e0359659..2313f30d 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/PropertyNode.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/PropertyNode.cs @@ -29,8 +29,7 @@ public void ParseField( IDictionary> fixedFields, IDictionary, Action> patternFields) { - var found = fixedFields.TryGetValue(this.Name, out var fixedFieldMap); - + var _ = fixedFields.TryGetValue(this.Name, out var fixedFieldMap); if (fixedFieldMap != null) { try @@ -78,8 +77,12 @@ public void ParseField( } else { - this.Context.Diagnostic.Errors.Add( - new AsyncApiError("", $"{this.Name} is not a valid property at {this.Context.GetLocation()}")); + switch (this.Context.Settings.UnmappedMemberHandling) + { + case UnmappedMemberHandling.Error: + this.Context.Diagnostic.Errors.Add(new AsyncApiError(string.Empty, $"{this.Name} is not a valid property at {this.Context.GetLocation()}")); + break; + } } } } diff --git a/src/LEGO.AsyncAPI.Readers/ParseNodes/ValueNode.cs b/src/LEGO.AsyncAPI.Readers/ParseNodes/ValueNode.cs index 201ab6f7..76add0b2 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/ValueNode.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/ValueNode.cs @@ -2,11 +2,11 @@ namespace LEGO.AsyncAPI.Readers.ParseNodes { + using System; + using System.Text.Json.Nodes; using LEGO.AsyncAPI.Exceptions; using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Readers.Exceptions; - using System; - using System.Text.Json.Nodes; public class ValueNode : ParseNode { diff --git a/src/LEGO.AsyncAPI.Readers/UnmappedMemberHandling.cs b/src/LEGO.AsyncAPI.Readers/UnmappedMemberHandling.cs new file mode 100644 index 00000000..b58c34c4 --- /dev/null +++ b/src/LEGO.AsyncAPI.Readers/UnmappedMemberHandling.cs @@ -0,0 +1,20 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Readers +{ + /// + /// Unmapped member handling. + /// + public enum UnmappedMemberHandling + { + /// + /// Add error to diagnostics for unmapped members. + /// + Error, + + /// + /// Ignore unmapped members. + /// + Ignore, + } +} diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiAvroSchemaDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiAvroSchemaDeserializer.cs new file mode 100644 index 00000000..baae8873 --- /dev/null +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiAvroSchemaDeserializer.cs @@ -0,0 +1,342 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Readers +{ + using System; + using System.Threading; + using LEGO.AsyncAPI.Exceptions; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Models.Avro.LogicalTypes; + using LEGO.AsyncAPI.Readers.Exceptions; + using LEGO.AsyncAPI.Readers.ParseNodes; + using LEGO.AsyncAPI.Writers; + + public class AsyncApiAvroSchemaDeserializer + { + private static readonly FixedFieldMap FieldFixedFields = new() + { + { "name", (a, n) => a.Name = n.GetScalarValue() }, + { "type", (a, n) => a.Type = LoadSchema(n) }, + { "doc", (a, n) => a.Doc = n.GetScalarValue() }, + { "default", (a, n) => a.Default = n.CreateAny() }, + { "aliases", (a, n) => a.Aliases = n.CreateSimpleList(n2 => n2.GetScalarValue()) }, + { "order", (a, n) => a.Order = n.GetScalarValue().GetEnumFromDisplayName() }, + }; + + private static readonly FixedFieldMap RecordFixedFields = new() + { + { "type", (a, n) => { } }, + { "name", (a, n) => a.Name = n.GetScalarValue() }, + { "doc", (a, n) => a.Doc = n.GetScalarValue() }, + { "namespace", (a, n) => a.Namespace = n.GetScalarValue() }, + { "aliases", (a, n) => a.Aliases = n.CreateSimpleList(n2 => n2.GetScalarValue()) }, + { "fields", (a, n) => a.Fields = n.CreateList(LoadField) }, + }; + + private static readonly FixedFieldMap EnumFixedFields = new() + { + { "type", (a, n) => { } }, + { "name", (a, n) => a.Name = n.GetScalarValue() }, + { "doc", (a, n) => a.Doc = n.GetScalarValue() }, + { "namespace", (a, n) => a.Namespace = n.GetScalarValue() }, + { "aliases", (a, n) => a.Aliases = n.CreateSimpleList(n2 => n2.GetScalarValue()) }, + { "symbols", (a, n) => a.Symbols = n.CreateSimpleList(n2 => n2.GetScalarValue()) }, + { "default", (a, n) => a.Default = n.GetScalarValue() }, + }; + + private static readonly FixedFieldMap FixedFixedFields = new() + { + { "type", (a, n) => { } }, + { "name", (a, n) => a.Name = n.GetScalarValue() }, + { "namespace", (a, n) => a.Namespace = n.GetScalarValue() }, + { "aliases", (a, n) => a.Aliases = n.CreateSimpleList(n2 => n2.GetScalarValue()) }, + { "size", (a, n) => a.Size = int.Parse(n.GetScalarValue(), n.Context.Settings.CultureInfo) }, + }; + + private static readonly FixedFieldMap ArrayFixedFields = new() + { + { "type", (a, n) => { } }, + { "items", (a, n) => a.Items = LoadSchema(n) }, + }; + + private static readonly FixedFieldMap MapFixedFields = new() + { + { "type", (a, n) => { } }, + { "values", (a, n) => a.Values = n.GetScalarValue().GetEnumFromDisplayName() }, + }; + + private static readonly FixedFieldMap UnionFixedFields = new() + { + { "types", (a, n) => a.Types = n.CreateList(LoadSchema) }, + }; + + private static readonly FixedFieldMap DecimalFixedFields = new() + { + { "type", (a, n) => { } }, + { "logicalType", (a, n) => { } }, + { "precision", (a, n) => a.Precision = int.Parse(n.GetScalarValue()) }, + { "scale", (a, n) => a.Scale = int.Parse(n.GetScalarValue()) }, + }; + + private static readonly FixedFieldMap UUIDFixedFields = new() + { + { "type", (a, n) => { } }, + { "logicalType", (a, n) => { } }, + }; + + private static readonly FixedFieldMap DateFixedFields = new() + { + { "type", (a, n) => { } }, + { "logicalType", (a, n) => { } }, + }; + + private static readonly FixedFieldMap TimeMillisFixedFields = new() + { + { "type", (a, n) => { } }, + { "logicalType", (a, n) => { } }, + }; + + private static readonly FixedFieldMap TimeMicrosFixedFields = new() + { + { "type", (a, n) => { } }, + { "logicalType", (a, n) => { } }, + }; + + private static readonly FixedFieldMap TimestampMillisFixedFields = new() + { + { "type", (a, n) => { } }, + { "logicalType", (a, n) => { } }, + }; + + private static readonly FixedFieldMap TimestampMicrosFixedFields = new() + { + { "type", (a, n) => { } }, + { "logicalType", (a, n) => { } }, + }; + + private static readonly FixedFieldMap DurationFixedFields = new() + { + { "type", (a, n) => { } }, + { "logicalType", (a, n) => { } }, + { "name", (a, n) => a.Name = n.GetScalarValue() }, + { "namespace", (a, n) => a.Namespace = n.GetScalarValue() }, + { "aliases", (a, n) => a.Aliases = n.CreateSimpleList(n2 => n2.GetScalarValue()) }, + { "size", (a, n) => { } }, + }; + + private static readonly PatternFieldMap RecordMetadataPatternFields = + new() + { + { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, + }; + + private static readonly PatternFieldMap FieldMetadataPatternFields = + new() + { + { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, + }; + + private static readonly PatternFieldMap EnumMetadataPatternFields = + new() + { + { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, + }; + + private static readonly PatternFieldMap FixedMetadataPatternFields = + new() + { + { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, + }; + + private static readonly PatternFieldMap ArrayMetadataPatternFields = + new() + { + { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, + }; + + private static readonly PatternFieldMap MapMetadataPatternFields = + new() + { + { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, + }; + + private static readonly PatternFieldMap UnionMetadataPatternFields = + new() + { + { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, + }; + + private static readonly PatternFieldMap DecimalMetadataPatternFields = + new() + { + { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, + }; + + private static readonly PatternFieldMap UUIDMetadataPatternFields = + new() + { + { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, + }; + + private static readonly PatternFieldMap DateMetadataPatternFields = + new() + { + { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, + }; + + private static readonly PatternFieldMap TimeMillisMetadataPatternFields = + new() + { + { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, + }; + + private static readonly PatternFieldMap TimeMicrosMetadataPatternFields = + new() + { + { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, + }; + + private static readonly PatternFieldMap TimestampMillisMetadataPatternFields = + new() + { + { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, + }; + + private static readonly PatternFieldMap TimestampMicrosMetadataPatternFields = + new() + { + { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, + }; + + private static readonly PatternFieldMap DurationMetadataPatternFields = + new() + { + { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, + }; + + public static AvroSchema LoadSchema(ParseNode node) + { + if (node is ValueNode valueNode) + { + return new AvroPrimitive(valueNode.GetScalarValue().GetEnumFromDisplayName()); + } + + if (node is ListNode) + { + var union = new AvroUnion(); + foreach (var item in node as ListNode) + { + union.Types.Add(LoadSchema(item)); + } + + return union; + } + + if (node is MapNode mapNode) + { + var pointer = mapNode.GetReferencePointer(); + + if (pointer != null) + { + return new AvroRecord + { + UnresolvedReference = true, + Reference = node.Context.VersionService.ConvertToAsyncApiReference(pointer, ReferenceType.Schema), + }; + } + + var isLogicalType = mapNode["logicalType"] != null; + if (isLogicalType) + { + return LoadLogicalType(mapNode); + } + + var type = mapNode["type"]?.Value.GetScalarValue(); + switch (type) + { + case "record": + var record = new AvroRecord(); + mapNode.ParseFields(ref record, RecordFixedFields, RecordMetadataPatternFields); + return record; + case "enum": + var @enum = new AvroEnum(); + mapNode.ParseFields(ref @enum, EnumFixedFields, EnumMetadataPatternFields); + return @enum; + case "fixed": + var @fixed = new AvroFixed(); + mapNode.ParseFields(ref @fixed, FixedFixedFields, FixedMetadataPatternFields); + return @fixed; + case "array": + var array = new AvroArray(); + mapNode.ParseFields(ref array, ArrayFixedFields, ArrayMetadataPatternFields); + return array; + case "map": + var map = new AvroMap(); + mapNode.ParseFields(ref map, MapFixedFields, MapMetadataPatternFields); + return map; + case "union": + var union = new AvroUnion(); + mapNode.ParseFields(ref union, UnionFixedFields, UnionMetadataPatternFields); + return union; + default: + throw new AsyncApiException($"Unsupported type: {type}"); + } + } + + throw new AsyncApiReaderException("Invalid node type"); + } + + private static AvroSchema LoadLogicalType(MapNode mapNode) + { + var type = mapNode["logicalType"]?.Value.GetScalarValue(); + switch (type) + { + case "decimal": + var @decimal = new AvroDecimal(); + mapNode.ParseFields(ref @decimal, DecimalFixedFields, DecimalMetadataPatternFields); + return @decimal; + case "uuid": + var uuid = new AvroUUID(); + mapNode.ParseFields(ref uuid, UUIDFixedFields, UUIDMetadataPatternFields); + return uuid; + case "date": + var date = new AvroDate(); + mapNode.ParseFields(ref date, DateFixedFields, DateMetadataPatternFields); + return date; + case "time-millis": + var timeMillis = new AvroTimeMillis(); + mapNode.ParseFields(ref timeMillis, TimeMillisFixedFields, TimeMillisMetadataPatternFields); + return timeMillis; + case "time-micros": + var timeMicros = new AvroTimeMicros(); + mapNode.ParseFields(ref timeMicros, TimeMicrosFixedFields, TimeMicrosMetadataPatternFields); + return timeMicros; + case "timestamp-millis": + var timestampMillis = new AvroTimestampMillis(); + mapNode.ParseFields(ref timestampMillis, TimestampMillisFixedFields, TimestampMillisMetadataPatternFields); + return timestampMillis; + case "timestamp-micros": + var timestampMicros = new AvroTimestampMicros(); + mapNode.ParseFields(ref timestampMicros, TimestampMicrosFixedFields, TimestampMicrosMetadataPatternFields); + return timestampMicros; + case "duration": + var duration = new AvroDuration(); + mapNode.ParseFields(ref duration, DurationFixedFields, DurationMetadataPatternFields); + return duration; + default: + throw new AsyncApiException($"Unsupported type: {type}"); + } + } + + private static AvroField LoadField(ParseNode node) + { + var mapNode = node.CheckMapNode("field"); + var field = new AvroField(); + + mapNode.ParseFields(ref field, FieldFixedFields, FieldMetadataPatternFields); + + return field; + + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiComponentsDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiComponentsDeserializer.cs index 3b63db28..df13308a 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiComponentsDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiComponentsDeserializer.cs @@ -10,7 +10,7 @@ internal static partial class AsyncApiV2Deserializer { private static FixedFieldMap componentsFixedFields = new() { - { "schemas", (a, n) => a.Schemas = n.CreateMapWithReference(ReferenceType.Schema, JsonSchemaDeserializer.LoadSchema) }, + { "schemas", (a, n) => a.Schemas = n.CreateMapWithReference(ReferenceType.Schema, AsyncApiSchemaDeserializer.LoadSchema) }, { "servers", (a, n) => a.Servers = n.CreateMapWithReference(ReferenceType.Server, LoadServer) }, { "channels", (a, n) => a.Channels = n.CreateMapWithReference(ReferenceType.Channel, LoadChannel) }, { "messages", (a, n) => a.Messages = n.CreateMapWithReference(ReferenceType.Message, LoadMessage) }, diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageDeserializer.cs index 4c16bf22..2670d88a 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageDeserializer.cs @@ -2,12 +2,13 @@ namespace LEGO.AsyncAPI.Readers { + using System.Collections.Generic; + using System.Linq; using LEGO.AsyncAPI.Exceptions; using LEGO.AsyncAPI.Extensions; using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Readers.ParseNodes; - using System.Collections.Generic; - using System.Linq; /// /// Class containing logic to deserialize AsyncApi document into @@ -21,10 +22,10 @@ internal static partial class AsyncApiV2Deserializer "messageId", (a, n) => { a.MessageId = n.GetScalarValue(); } }, { - "headers", (a, n) => { a.Headers = JsonSchemaDeserializer.LoadSchema(n); } + "headers", (a, n) => { a.Headers = AsyncApiSchemaDeserializer.LoadSchema(n); } }, { - "payload", (a, n) => { a.Payload = JsonSchemaDeserializer.LoadSchema(n); } + "payload", (a, n) => { a.Payload = null; /* resolved after the initial run */ } }, { "correlationId", (a, n) => { a.CorrelationId = LoadCorrelationId(n); } @@ -64,7 +65,39 @@ internal static partial class AsyncApiV2Deserializer }, }; - static readonly IEnumerable SupportedSchemaFormats = new List + public static IAsyncApiMessagePayload LoadJsonSchemaPayload(ParseNode n) + { + return LoadPayload(n, null); + } + + public static IAsyncApiMessagePayload LoadAvroPayload(ParseNode n) + { + return LoadPayload(n, "application/vnd.apache.avro"); + } + + private static IAsyncApiMessagePayload LoadPayload(ParseNode n, string format) + { + + if (n == null) + { + return null; + } + + switch (format) + { + case null: + case "": + case var _ when SupportedJsonSchemaFormats.Where(s => format.StartsWith(s)).Any(): + return new AsyncApiJsonSchemaPayload(AsyncApiSchemaDeserializer.LoadSchema(n)); + case var _ when SupportedAvroSchemaFormats.Where(s => format.StartsWith(s)).Any(): + return new AsyncApiAvroSchemaPayload(AsyncApiAvroSchemaDeserializer.LoadSchema(n)); + default: + var supportedFormats = SupportedJsonSchemaFormats.Concat(SupportedAvroSchemaFormats); + throw new AsyncApiException($"'Could not deserialize Payload. Supported formats are {string.Join(", ", supportedFormats)}"); + } + } + + static readonly IEnumerable SupportedJsonSchemaFormats = new List { "application/vnd.aai.asyncapi+json", "application/vnd.aai.asyncapi+yaml", @@ -73,11 +106,21 @@ internal static partial class AsyncApiV2Deserializer "application/schema+yaml;version=draft-07", }; + static readonly IEnumerable SupportedAvroSchemaFormats = new List + { + "application/vnd.apache.avro", + "application/vnd.apache.avro+json", + "application/vnd.apache.avro+yaml", + "application/vnd.apache.avro+json;version=1.9.0", + "application/vnd.apache.avro+yaml;version=1.9.0", + }; + private static string LoadSchemaFormat(string schemaFormat) { - if (!SupportedSchemaFormats.Where(s => schemaFormat.StartsWith(s)).Any()) + var supportedFormats = SupportedJsonSchemaFormats.Concat(SupportedAvroSchemaFormats); + if (!supportedFormats.Where(s => schemaFormat.StartsWith(s)).Any()) { - throw new AsyncApiException($"'{schemaFormat}' is not a supported format. Supported formats are {string.Join(", ", SupportedSchemaFormats)}"); + throw new AsyncApiException($"'{schemaFormat}' is not a supported format. Supported formats are {string.Join(", ", supportedFormats)}"); } return schemaFormat; @@ -100,6 +143,7 @@ public static AsyncApiMessage LoadMessage(ParseNode node) var message = new AsyncApiMessage(); ParseMap(mapNode, message, messageFixedFields, messagePatternFields); + message.Payload = LoadPayload(mapNode["payload"]?.Value, message.SchemaFormat); return message; } diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageTraitDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageTraitDeserializer.cs index eca8af64..0d229e92 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageTraitDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageTraitDeserializer.cs @@ -11,7 +11,7 @@ internal static partial class AsyncApiV2Deserializer private static FixedFieldMap messageTraitFixedFields = new() { { "messageId", (a, n) => { a.MessageId = n.GetScalarValue(); } }, - { "headers", (a, n) => { a.Headers = JsonSchemaDeserializer.LoadSchema(n); } }, + { "headers", (a, n) => { a.Headers = AsyncApiSchemaDeserializer.LoadSchema(n); } }, { "correlationId", (a, n) => { a.CorrelationId = LoadCorrelationId(n); } }, { "schemaFormat", (a, n) => { a.SchemaFormat = n.GetScalarValue(); } }, { "contentType", (a, n) => { a.ContentType = n.GetScalarValue(); } }, diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiParameterDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiParameterDeserializer.cs index bff810f1..c77f48f4 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiParameterDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiParameterDeserializer.cs @@ -11,7 +11,7 @@ internal static partial class AsyncApiV2Deserializer private static FixedFieldMap parameterFixedFields = new() { { "description", (a, n) => { a.Description = n.GetScalarValue(); } }, - { "schema", (a, n) => { a.Schema = JsonSchemaDeserializer.LoadSchema(n); } }, + { "schema", (a, n) => { a.Schema = AsyncApiSchemaDeserializer.LoadSchema(n); } }, { "location", (a, n) => { a.Location = n.GetScalarValue(); } }, }; diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiSchemaDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiSchemaDeserializer.cs index 1934cb0d..b8e790f2 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiSchemaDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiSchemaDeserializer.cs @@ -9,7 +9,7 @@ namespace LEGO.AsyncAPI.Readers using LEGO.AsyncAPI.Readers.ParseNodes; using LEGO.AsyncAPI.Writers; - public class JsonSchemaDeserializer + public class AsyncApiSchemaDeserializer { private static readonly FixedFieldMap schemaFixedFields = new() { @@ -221,28 +221,6 @@ public class JsonSchemaDeserializer { s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, AsyncApiV2Deserializer.LoadExtension(p, n)) }, }; - private static readonly AnyFieldMap schemaAnyFields = new() - { - { - AsyncApiConstants.Default, - new AnyFieldMapParameter( - s => s.Default, - (s, v) => s.Default = v, - s => s) - }, - }; - - private static readonly AnyListFieldMap schemaAnyListFields = new() - { - { - AsyncApiConstants.Enum, - new AnyListFieldMapParameter( - s => s.Enum, - (s, v) => s.Enum = v, - s => s) - }, - }; - public static AsyncApiSchema LoadSchema(ParseNode node) { var mapNode = node.CheckMapNode(AsyncApiConstants.Schema); @@ -265,9 +243,6 @@ public static AsyncApiSchema LoadSchema(ParseNode node) propertyNode.ParseField(schema, schemaFixedFields, schemaPatternFields); } - AsyncApiV2Deserializer.ProcessAnyFields(mapNode, schema, schemaAnyFields); - AsyncApiV2Deserializer.ProcessAnyListFields(mapNode, schema, schemaAnyListFields); - return schema; } } diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs index 10edd3ba..4acc3395 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs @@ -35,7 +35,10 @@ public AsyncApiV2VersionService(AsyncApiDiagnostic diagnostic) [typeof(AsyncApiOAuthFlows)] = AsyncApiV2Deserializer.LoadOAuthFlows, [typeof(AsyncApiOperation)] = AsyncApiV2Deserializer.LoadOperation, [typeof(AsyncApiParameter)] = AsyncApiV2Deserializer.LoadParameter, - [typeof(AsyncApiSchema)] = JsonSchemaDeserializer.LoadSchema, + [typeof(AsyncApiSchema)] = AsyncApiSchemaDeserializer.LoadSchema, + [typeof(AvroSchema)] = AsyncApiAvroSchemaDeserializer.LoadSchema, + [typeof(AsyncApiJsonSchemaPayload)] = AsyncApiV2Deserializer.LoadJsonSchemaPayload, + [typeof(AsyncApiAvroSchemaPayload)] = AsyncApiV2Deserializer.LoadAvroPayload, [typeof(AsyncApiSecurityRequirement)] = AsyncApiV2Deserializer.LoadSecurityRequirement, [typeof(AsyncApiSecurityScheme)] = AsyncApiV2Deserializer.LoadSecurityScheme, [typeof(AsyncApiServer)] = AsyncApiV2Deserializer.LoadServer, diff --git a/src/LEGO.AsyncAPI/AsyncApiVersion.cs b/src/LEGO.AsyncAPI/AsyncApiVersion.cs index c5be2cc8..dbac63de 100644 --- a/src/LEGO.AsyncAPI/AsyncApiVersion.cs +++ b/src/LEGO.AsyncAPI/AsyncApiVersion.cs @@ -5,7 +5,7 @@ namespace LEGO.AsyncAPI public enum AsyncApiVersion { /// - /// Represents AsyncAPI V2 spec + /// Represents AsyncAPI V2 spec. /// AsyncApi2_0, } diff --git a/src/LEGO.AsyncAPI/Models/Any/AsyncApiAny.cs b/src/LEGO.AsyncAPI/Models/Any/AsyncApiAny.cs index 5c5f2b31..45b764b3 100644 --- a/src/LEGO.AsyncAPI/Models/Any/AsyncApiAny.cs +++ b/src/LEGO.AsyncAPI/Models/Any/AsyncApiAny.cs @@ -2,7 +2,6 @@ namespace LEGO.AsyncAPI.Models { - using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Nodes; using LEGO.AsyncAPI.Models.Interfaces; diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiAvroSchemaPayload.cs b/src/LEGO.AsyncAPI/Models/AsyncApiAvroSchemaPayload.cs new file mode 100644 index 00000000..0e9d1cb9 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/AsyncApiAvroSchemaPayload.cs @@ -0,0 +1,66 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + using System; + + public class AsyncApiAvroSchemaPayload : IAsyncApiMessagePayload + { + private readonly AvroSchema schema; + + public AsyncApiAvroSchemaPayload(AvroSchema schema) + { + this.schema = schema; + } + + public AsyncApiAvroSchemaPayload() + { + this.schema = new AvroRecord(); + } + + public bool TryGetAs(out T schema) + where T : AvroSchema + { + schema = this.schema as T; + return schema != null; + } + + public bool Is() + where T : AvroSchema + { + return this.schema is T; + } + + public Type GetSchemaType() + { + return this.schema.GetType(); + } + + public bool UnresolvedReference { get => this.schema.UnresolvedReference; set => this.schema.UnresolvedReference = value; } + + public AsyncApiReference Reference { get => this.schema.Reference; set => this.schema.Reference = value; } + + public void SerializeV2(IAsyncApiWriter writer) + { + var settings = writer.GetSettings(); + + if (this.Reference != null) + { + if (!settings.ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV2(writer); + return; + } + } + + this.SerializeV2WithoutReference(writer); + } + + public void SerializeV2WithoutReference(IAsyncApiWriter writer) + { + this.schema.SerializeV2(writer); + } + } +} diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiDocument.cs b/src/LEGO.AsyncAPI/Models/AsyncApiDocument.cs index 5be55202..7b725a6c 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiDocument.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiDocument.cs @@ -46,7 +46,7 @@ public class AsyncApiDocument : IAsyncApiExtensible, IAsyncApiSerializable /// /// REQUIRED. The available channels and messages for the API. /// - public IDictionary Channels { get; set; } = new Dictionary(); + public IDictionary Channels { get; set; } /// /// an element to hold various schemas for the specification. diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiMessage.cs b/src/LEGO.AsyncAPI/Models/AsyncApiMessage.cs index 2c627541..b4848bf6 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiMessage.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiMessage.cs @@ -25,7 +25,7 @@ public class AsyncApiMessage : IAsyncApiExtensible, IAsyncApiReferenceable, IAsy /// /// definition of the message payload. It can be of any type but defaults to Schema object. It must match the schema format, including encoding type - e.g Avro should be inlined as either a YAML or JSON object NOT a string to be parsed as YAML or JSON. /// - public AsyncApiSchema Payload { get; set; } + public IAsyncApiMessagePayload Payload { get; set; } /// /// definition of the correlation ID used for message tracing or matching. diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiReference.cs b/src/LEGO.AsyncAPI/Models/AsyncApiReference.cs index 4f9660d1..9475310e 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiReference.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiReference.cs @@ -15,7 +15,7 @@ public class AsyncApiReference : IAsyncApiSerializable /// External resource in the reference. /// It maybe: /// 1. a absolute/relative file path, for example: ../commons/pet.json - /// 2. a Url, for example: http://localhost/pet.json + /// 2. a Url, for example: http://localhost/pet.json. /// public string ExternalResource { get; set; } @@ -35,7 +35,7 @@ public class AsyncApiReference : IAsyncApiSerializable public AsyncApiDocument HostDocument { get; set; } = null; /// - /// Gets a flag indicating whether a file is a valid OpenAPI document or a fragment + /// Gets a flag indicating whether a file is a valid OpenAPI document or a fragment. /// public bool IsFragment { get; set; } = false; @@ -71,7 +71,7 @@ public string Reference } /// - /// Serialize to Async Api v2.4. + /// Serialize to Async Api. /// public void SerializeV2(IAsyncApiWriter writer) { diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiSchemaPayload.cs b/src/LEGO.AsyncAPI/Models/AsyncApiSchemaPayload.cs new file mode 100644 index 00000000..9e182265 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/AsyncApiSchemaPayload.cs @@ -0,0 +1,127 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + using System.Collections.Generic; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + + public class AsyncApiJsonSchemaPayload : IAsyncApiMessagePayload + { + private readonly AsyncApiSchema schema; + + public AsyncApiJsonSchemaPayload() + { + this.schema = new AsyncApiSchema(); + } + + public AsyncApiJsonSchemaPayload(AsyncApiSchema schema) + { + this.schema = schema; + } + + public string Title { get => this.schema.Title; set => this.schema.Title = value; } + + public SchemaType? Type { get => this.schema.Type; set => this.schema.Type = value; } + + public string Format { get => this.schema.Format; set => this.schema.Format = value; } + + public string Description { get => this.schema.Description; set => this.schema.Description = value; } + + public double? Maximum { get => this.schema.Maximum; set => this.schema.Maximum = value; } + + public double? ExclusiveMaximum { get => this.schema.ExclusiveMaximum; set => this.schema.ExclusiveMaximum = value; } + + public double? Minimum { get => this.schema.Minimum; set => this.schema.Minimum = value; } + + public double? ExclusiveMinimum { get => this.schema.ExclusiveMinimum; set => this.schema.ExclusiveMinimum = value; } + + public int? MaxLength { get => this.schema.MaxLength; set => this.schema.MaxLength = value; } + + public int? MinLength { get => this.schema.MinLength; set => this.schema.MinLength = value; } + + public string Pattern { get => this.schema.Pattern; set => this.schema.Pattern = value; } + + public double? MultipleOf { get => this.schema.MultipleOf; set => this.schema.MultipleOf = value; } + + public AsyncApiAny Default { get => this.schema.Default; set => this.schema.Default = value; } + + public bool ReadOnly { get => this.schema.ReadOnly; set => this.schema.ReadOnly = value; } + + public bool WriteOnly { get => this.schema.WriteOnly; set => this.schema.WriteOnly = value; } + + public IList AllOf { get => this.schema.AllOf; set => this.schema.AllOf = value; } + + public IList OneOf { get => this.schema.OneOf; set => this.schema.OneOf = value; } + + public IList AnyOf { get => this.schema.AnyOf; set => this.schema.AnyOf = value; } + + public AsyncApiSchema Not { get => this.schema.Not; set => this.schema.Not = value; } + + public AsyncApiSchema Contains { get => this.schema.Contains; set => this.schema.Contains = value; } + + public AsyncApiSchema If { get => this.schema.If; set => this.schema.If = value; } + + public AsyncApiSchema Then { get => this.schema.Then; set => this.schema.Then = value; } + + public AsyncApiSchema Else { get => this.schema.Else; set => this.schema.Else = value; } + + public ISet Required { get => this.schema.Required; set => this.schema.Required = value; } + + public AsyncApiSchema Items { get => this.schema.Items; set => this.schema.Items = value; } + + public AsyncApiSchema AdditionalItems { get => this.schema.AdditionalItems; set => this.schema.AdditionalItems = value; } + + public int? MaxItems { get => this.schema.MaxItems; set => this.schema.MaxItems = value; } + + public int? MinItems { get => this.schema.MinItems; set => this.schema.MinItems = value; } + + public bool? UniqueItems { get => this.schema.UniqueItems; set => this.schema.UniqueItems = value; } + + public IDictionary Properties { get => this.schema.Properties; set => this.schema.Properties = value; } + + public int? MaxProperties { get => this.schema.MaxProperties; set => this.schema.MaxProperties = value; } + + public int? MinProperties { get => this.schema.MinProperties; set => this.schema.MinProperties = value; } + + public IDictionary PatternProperties { get => this.schema.PatternProperties; set => this.schema.PatternProperties = value; } + + public AsyncApiSchema PropertyNames { get => this.schema.PropertyNames; set => this.schema.PropertyNames = value; } + + public string Discriminator { get => this.schema.Discriminator; set => this.schema.Discriminator = value; } + + public IList Enum { get => this.schema.Enum; set => this.schema.Enum = value; } + + public IList Examples { get => this.schema.Examples; set => this.schema.Examples = value; } + + public AsyncApiAny Const { get => this.schema.Const; set => this.schema.Const = value; } + + public bool Nullable { get => this.schema.Nullable; set => this.schema.Nullable = value; } + + public AsyncApiExternalDocumentation ExternalDocs { get => this.schema.ExternalDocs; set => this.schema.ExternalDocs = value; } + + public bool Deprecated { get => this.schema.Deprecated; set => this.schema.Deprecated = value; } + + public bool UnresolvedReference { get => this.schema.UnresolvedReference; set => this.schema.UnresolvedReference = value; } + + public AsyncApiReference Reference { get => this.schema.Reference; set => this.schema.Reference = value; } + + public IDictionary Extensions { get => this.schema.Extensions; set => this.schema.Extensions = value; } + + public AsyncApiSchema AdditionalProperties { get => this.schema.AdditionalProperties; set => this.schema.AdditionalProperties = value; } + + public static implicit operator AsyncApiSchema(AsyncApiJsonSchemaPayload payload) => payload.schema; + + public static implicit operator AsyncApiJsonSchemaPayload(AsyncApiSchema schema) => new AsyncApiJsonSchemaPayload(schema); + + public void SerializeV2(IAsyncApiWriter writer) + { + this.schema.SerializeV2(writer); + } + + public void SerializeV2WithoutReference(IAsyncApiWriter writer) + { + this.schema.SerializeV2WithoutReference(writer); + } + } +} diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiSerializableExtensions.cs b/src/LEGO.AsyncAPI/Models/AsyncApiSerializableExtensions.cs index 1d1bd35a..91167e19 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiSerializableExtensions.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiSerializableExtensions.cs @@ -33,7 +33,7 @@ public static void SerializeAsJson(this T element, Stream stream, AsyncApiVer /// The AsyncApi element. /// The output stream. /// The AsyncApi specification version. - /// The settings used for writing + /// The settings used for writing. public static void SerializeAsJson(this T element, Stream stream, AsyncApiVersion specificationVersion, AsyncApiWriterSettings settings) where T : IAsyncApiSerializable { @@ -60,7 +60,7 @@ public static void SerializeAsYaml(this T element, Stream stream, AsyncApiVer /// The AsyncApi element. /// The output stream. /// The AsyncApi specification version. - /// The settings used for writing + /// The settings used for writing. public static void SerializeAsYaml(this T element, Stream stream, AsyncApiVersion specificationVersion, AsyncApiWriterSettings settings) where T : IAsyncApiSerializable { diff --git a/src/LEGO.AsyncAPI/Models/Avro/AvroArray.cs b/src/LEGO.AsyncAPI/Models/Avro/AvroArray.cs new file mode 100644 index 00000000..0958fa3d --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/AvroArray.cs @@ -0,0 +1,47 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + using System.Collections.Generic; + using System.Linq; + using LEGO.AsyncAPI.Writers; + + public class AvroArray : AvroSchema + { + public override string Type { get; } = "array"; + + /// + /// The schema of the array's items. + /// + public AvroSchema Items { get; set; } + + /// + /// A map of properties not in the schema, but added as additional metadata. + /// + public override IDictionary Metadata { get; set; } = new Dictionary(); + + public override void SerializeV2WithoutReference(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteOptionalProperty("type", this.Type); + writer.WriteRequiredObject("items", this.Items, (w, f) => f.SerializeV2(w)); + if (this.Metadata.Any()) + { + foreach (var item in this.Metadata) + { + writer.WritePropertyName(item.Key); + if (item.Value == null) + { + writer.WriteNull(); + } + else + { + writer.WriteAny(item.Value); + } + } + } + + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/Avro/AvroEnum.cs b/src/LEGO.AsyncAPI/Models/Avro/AvroEnum.cs new file mode 100644 index 00000000..dbfbb627 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/AvroEnum.cs @@ -0,0 +1,77 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + using System.Collections.Generic; + using System.Linq; + using LEGO.AsyncAPI.Writers; + + public class AvroEnum : AvroSchema + { + public override string Type { get; } = "enum"; + + /// + /// The name of the schema. Required for named types. See Avro Names. + /// + public string Name { get; set; } + + /// + /// The namespace of the schema. Useful for named types to avoid name conflicts. + /// + public string Namespace { get; set; } + + /// + /// Documentation for the schema. + /// + public string Doc { get; set; } + + /// + /// Alternate names for this enum. + /// + public IList Aliases { get; set; } = new List(); + + /// + /// Listing symbols. All symbols in an enum must be unique. + /// + public IList Symbols { get; set; } = new List(); + + /// + /// A default value for this enumeration. + /// + public string Default { get; set; } + + /// + /// A map of properties not in the schema, but added as additional metadata. + /// + public override IDictionary Metadata { get; set; } = new Dictionary(); + + public override void SerializeV2WithoutReference(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteOptionalProperty("type", this.Type); + writer.WriteRequiredProperty("name", this.Name); + writer.WriteOptionalProperty("namespace", this.Namespace); + writer.WriteOptionalCollection("aliases", this.Aliases, (w, s) => w.WriteValue(s)); + writer.WriteOptionalProperty("doc", this.Doc); + writer.WriteRequiredCollection("symbols", this.Symbols, (w, s) => w.WriteValue(s)); + writer.WriteOptionalProperty("default", this.Default); + if (this.Metadata.Any()) + { + foreach (var item in this.Metadata) + { + writer.WritePropertyName(item.Key); + if (item.Value == null) + { + writer.WriteNull(); + } + else + { + writer.WriteAny(item.Value); + } + } + } + + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/Avro/AvroField.cs b/src/LEGO.AsyncAPI/Models/Avro/AvroField.cs new file mode 100644 index 00000000..659f1a4a --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/AvroField.cs @@ -0,0 +1,107 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + using System.Collections.Generic; + using System.Linq; + using LEGO.AsyncAPI.Attributes; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + + public enum AvroFieldOrder + { + None = 0, + + [Display("ascending")] + Ascending, + + [Display("descending")] + Descending, + + [Display("ignore")] + Ignore, + } + /// + /// Represents a field within an Avro record schema. + /// + public class AvroField : IAsyncApiSerializable + { + /// + /// The name of the field. + /// + public string Name { get; set; } + + /// + /// The type of the field. Can be a primitive type, a complex type, or a union. + /// + public AvroSchema Type { get; set; } + + /// + /// The documentation for the field. + /// + public string Doc { get; set; } + + /// + /// The default value for the field. + /// + public AsyncApiAny Default { get; set; } + + /// + /// The order of the field, can be 'ascending', 'descending', or 'ignore'. + /// + public AvroFieldOrder Order { get; set; } + + /// + /// Alternate names for this record (optional). + /// + public IList Aliases { get; set; } = new List(); + + /// + /// A map of properties not in the schema, but added as additional metadata. + /// + public IDictionary Metadata { get; set; } = new Dictionary(); + + public void SerializeV2(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteOptionalProperty("name", this.Name); + writer.WriteOptionalObject("type", this.Type, (w, s) => s.SerializeV2(w)); + writer.WriteOptionalProperty("doc", this.Doc); + writer.WriteOptionalObject("default", this.Default, (w, s) => + { + if (s.TryGetValue(out string value) && value == "null") + { + w.WriteNull(); + } + else + { + w.WriteAny(s); + } + }); + + if (this.Order != AvroFieldOrder.None) + { + writer.WriteOptionalProperty("order", this.Order.GetDisplayName()); + } + + writer.WriteOptionalCollection("aliases", this.Aliases, (w, s) => w.WriteValue(s)); + if (this.Metadata.Any()) + { + foreach (var item in this.Metadata) + { + writer.WritePropertyName(item.Key); + if (item.Value == null) + { + writer.WriteNull(); + } + else + { + writer.WriteAny(item.Value); + } + } + } + + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/Avro/AvroFixed.cs b/src/LEGO.AsyncAPI/Models/Avro/AvroFixed.cs new file mode 100644 index 00000000..241e8c1e --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/AvroFixed.cs @@ -0,0 +1,66 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + using System.Collections.Generic; + using System.Linq; + using LEGO.AsyncAPI.Writers; + + public class AvroFixed : AvroSchema + { + public override string Type { get; } = "fixed"; + + /// + /// The name of the schema. Required for named types. See Avro Names. + /// + public string Name { get; set; } + + /// + /// The namespace of the schema. Useful for named types to avoid name conflicts. + /// + public string Namespace { get; set; } + + + /// + /// Alternate names for this record. + /// + public IList Aliases { get; set; } = new List(); + + /// + /// Number of bytes per value. + /// + public int Size { get; set; } + + /// + /// A map of properties not in the schema, but added as additional metadata. + /// + public override IDictionary Metadata { get; set; } = new Dictionary(); + + public override void SerializeV2WithoutReference(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteOptionalProperty("type", this.Type); + writer.WriteRequiredProperty("name", this.Name); + writer.WriteOptionalProperty("namespace", this.Namespace); + writer.WriteOptionalCollection("aliases", this.Aliases, (w, s) => w.WriteValue(s)); + writer.WriteRequiredProperty("size", this.Size); + if (this.Metadata.Any()) + { + foreach (var item in this.Metadata) + { + writer.WritePropertyName(item.Key); + if (item.Value == null) + { + writer.WriteNull(); + } + else + { + writer.WriteAny(item.Value); + } + } + } + + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/Avro/AvroMap.cs b/src/LEGO.AsyncAPI/Models/Avro/AvroMap.cs new file mode 100644 index 00000000..8b3028c6 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/AvroMap.cs @@ -0,0 +1,44 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + using System.Collections.Generic; + using System.Linq; + using LEGO.AsyncAPI.Writers; + + public class AvroMap : AvroSchema + { + public override string Type { get; } = "map"; + + public AvroPrimitiveType Values { get; set; } + + /// + /// A map of properties not in the schema, but added as additional metadata. + /// + public override IDictionary Metadata { get; set; } = new Dictionary(); + + public override void SerializeV2WithoutReference(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteOptionalProperty("type", this.Type); + writer.WriteRequiredProperty("values", this.Values.GetDisplayName()); + if (this.Metadata.Any()) + { + foreach (var item in this.Metadata) + { + writer.WritePropertyName(item.Key); + if (item.Value == null) + { + writer.WriteNull(); + } + else + { + writer.WriteAny(item.Value); + } + } + } + + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/Avro/AvroPrimitive.cs b/src/LEGO.AsyncAPI/Models/Avro/AvroPrimitive.cs new file mode 100644 index 00000000..4d12c792 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/AvroPrimitive.cs @@ -0,0 +1,43 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + using System.Collections.Generic; + using System.Linq; + using LEGO.AsyncAPI.Writers; + + public class AvroPrimitive : AvroSchema + { + public override string Type { get; } + + /// + /// A map of properties not in the schema, but added as additional metadata. + /// + public override IDictionary Metadata { get; set; } = new Dictionary(); + + public AvroPrimitive(AvroPrimitiveType type) + { + this.Type = type.GetDisplayName(); + } + + public override void SerializeV2WithoutReference(IAsyncApiWriter writer) + { + writer.WriteValue(this.Type); + if (this.Metadata.Any()) + { + foreach (var item in this.Metadata) + { + writer.WritePropertyName(item.Key); + if (item.Value == null) + { + writer.WriteNull(); + } + else + { + writer.WriteAny(item.Value); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/Avro/AvroPrimitiveType.cs b/src/LEGO.AsyncAPI/Models/Avro/AvroPrimitiveType.cs new file mode 100644 index 00000000..b76e7554 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/AvroPrimitiveType.cs @@ -0,0 +1,33 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + using LEGO.AsyncAPI.Attributes; + + public enum AvroPrimitiveType + { + [Display("null")] + Null, + + [Display("boolean")] + Boolean, + + [Display("int")] + Int, + + [Display("long")] + Long, + + [Display("float")] + Float, + + [Display("double")] + Double, + + [Display("bytes")] + Bytes, + + [Display("string")] + String, + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/Avro/AvroRecord.cs b/src/LEGO.AsyncAPI/Models/Avro/AvroRecord.cs new file mode 100644 index 00000000..04642f5c --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/AvroRecord.cs @@ -0,0 +1,71 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + using System.Collections.Generic; + using System.Linq; + using LEGO.AsyncAPI.Writers; + + public class AvroRecord : AvroSchema + { + public override string Type { get; } = "record"; + + /// + /// The name of the schema. Required for named types. See Avro Names. + /// + public string Name { get; set; } + + /// + /// The namespace of the schema. Useful for named types to avoid name conflicts. + /// + public string Namespace { get; set; } + + /// + /// Documentation for the schema. + /// + public string Doc { get; set; } + + /// + /// Alternate names for this record. + /// + public IList Aliases { get; set; } = new List(); + + /// + /// A list of fields contained within this record. + /// + public IList Fields { get; set; } = new List(); + + /// + /// A map of properties not in the schema, but added as additional metadata. + /// + public override IDictionary Metadata { get; set; } = new Dictionary(); + + public override void SerializeV2WithoutReference(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteOptionalProperty("type", this.Type); + writer.WriteRequiredProperty("name", this.Name); + writer.WriteOptionalProperty("namespace", this.Namespace); + writer.WriteOptionalProperty("doc", this.Doc); + writer.WriteOptionalCollection("aliases", this.Aliases, (w, s) => w.WriteValue(s)); + writer.WriteRequiredCollection("fields", this.Fields, (w, s) => s.SerializeV2(w)); + if (this.Metadata.Any()) + { + foreach (var item in this.Metadata) + { + writer.WritePropertyName(item.Key); + if (item.Value == null) + { + writer.WriteNull(); + } + else + { + writer.WriteAny(item.Value); + } + } + } + + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/Avro/AvroSchema.cs b/src/LEGO.AsyncAPI/Models/Avro/AvroSchema.cs new file mode 100644 index 00000000..fce1df2d --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/AvroSchema.cs @@ -0,0 +1,46 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + using System; + using System.Collections.Generic; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + + public abstract class AvroSchema : IAsyncApiSerializable, IAsyncApiReferenceable + { + public abstract string Type { get; } + + /// + /// A map of properties not in the schema, but added as additional metadata. + /// + public abstract IDictionary Metadata { get; set; } + + public bool UnresolvedReference { get; set; } + + public AsyncApiReference Reference { get; set; } + + public static implicit operator AvroSchema(AvroPrimitiveType type) + { + return new AvroPrimitive(type); + } + + public void SerializeV2(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (this.Reference != null && !writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV2(writer); + return; + } + + this.SerializeV2WithoutReference(writer); + } + + public abstract void SerializeV2WithoutReference(IAsyncApiWriter writer); + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/Avro/AvroUnion.cs b/src/LEGO.AsyncAPI/Models/Avro/AvroUnion.cs new file mode 100644 index 00000000..5762935d --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/AvroUnion.cs @@ -0,0 +1,50 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + using System.Collections.Generic; + using System.Linq; + using LEGO.AsyncAPI.Writers; + + public class AvroUnion : AvroSchema + { + public override string Type { get; } = "map"; + + /// + /// The types in this union. + /// + public IList Types { get; set; } = new List(); + + /// + /// A map of properties not in the schema, but added as additional metadata. + /// + public override IDictionary Metadata { get; set; } = new Dictionary(); + + public override void SerializeV2WithoutReference(IAsyncApiWriter writer) + { + writer.WriteStartArray(); + foreach (var type in this.Types) + { + type.SerializeV2(writer); + } + + if (this.Metadata.Any()) + { + foreach (var item in this.Metadata) + { + writer.WritePropertyName(item.Key); + if (item.Value == null) + { + writer.WriteNull(); + } + else + { + writer.WriteAny(item.Value); + } + } + } + + writer.WriteEndArray(); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/AvroDate.cs b/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/AvroDate.cs new file mode 100644 index 00000000..0b01ab0c --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/AvroDate.cs @@ -0,0 +1,14 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models.Avro.LogicalTypes +{ + public class AvroDate : AvroLogicalType + { + public AvroDate() + : base(AvroPrimitiveType.Int) + { + } + + public override LogicalType LogicalType => LogicalType.Date; + } +} diff --git a/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/AvroDecimal.cs b/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/AvroDecimal.cs new file mode 100644 index 00000000..7c042dbb --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/AvroDecimal.cs @@ -0,0 +1,26 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models.Avro.LogicalTypes +{ + using LEGO.AsyncAPI.Writers; + + public class AvroDecimal : AvroLogicalType + { + public AvroDecimal() + : base(AvroPrimitiveType.Bytes) + { + } + + public override LogicalType LogicalType => LogicalType.Decimal; + + public int? Scale { get; set; } + + public int? Precision { get; set; } + + public override void SerializeV2Core(IAsyncApiWriter writer) + { + writer.WriteOptionalProperty("scale", this.Scale); + writer.WriteOptionalProperty("precision", this.Precision); + } + } +} diff --git a/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/AvroDuration.cs b/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/AvroDuration.cs new file mode 100644 index 00000000..01469d0b --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/AvroDuration.cs @@ -0,0 +1,42 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models.Avro.LogicalTypes +{ + using System.Linq; + using LEGO.AsyncAPI.Writers; + + public class AvroDuration : AvroFixed + { + public LogicalType LogicalType { get; } = LogicalType.Duration; + + public new int Size { get; } = 12; + + public override void SerializeV2WithoutReference(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteOptionalProperty("type", this.Type); + writer.WriteOptionalProperty("logicalType", this.LogicalType.GetDisplayName()); + writer.WriteRequiredProperty("name", this.Name); + writer.WriteOptionalProperty("namespace", this.Namespace); + writer.WriteOptionalCollection("aliases", this.Aliases, (w, s) => w.WriteValue(s)); + writer.WriteRequiredProperty("size", this.Size); + if (this.Metadata.Any()) + { + foreach (var item in this.Metadata) + { + writer.WritePropertyName(item.Key); + if (item.Value == null) + { + writer.WriteNull(); + } + else + { + writer.WriteAny(item.Value); + } + } + } + + writer.WriteEndObject(); + } + } +} diff --git a/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/AvroLogicalType.cs b/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/AvroLogicalType.cs new file mode 100644 index 00000000..235899a1 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/AvroLogicalType.cs @@ -0,0 +1,48 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models.Avro.LogicalTypes +{ + using System.Linq; + using LEGO.AsyncAPI.Writers; + + public abstract class AvroLogicalType : AvroPrimitive + { + protected AvroLogicalType(AvroPrimitiveType type) + : base(type) + { + } + + public abstract LogicalType LogicalType { get; } + + public virtual void SerializeV2Core(IAsyncApiWriter writer) + { + } + + public override void SerializeV2WithoutReference(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteOptionalProperty("type", this.Type); + writer.WriteOptionalProperty("logicalType", this.LogicalType.GetDisplayName()); + + this.SerializeV2Core(writer); + + if (this.Metadata.Any()) + { + foreach (var item in this.Metadata) + { + writer.WritePropertyName(item.Key); + if (item.Value == null) + { + writer.WriteNull(); + } + else + { + writer.WriteAny(item.Value); + } + } + } + + writer.WriteEndObject(); + } + } +} diff --git a/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/AvroTimeMicros.cs b/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/AvroTimeMicros.cs new file mode 100644 index 00000000..df7dfa59 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/AvroTimeMicros.cs @@ -0,0 +1,14 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models.Avro.LogicalTypes +{ + public class AvroTimeMicros : AvroLogicalType + { + public AvroTimeMicros() + : base(AvroPrimitiveType.Long) + { + } + + public override LogicalType LogicalType => LogicalType.Time_Micros; + } +} diff --git a/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/AvroTimeMillis.cs b/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/AvroTimeMillis.cs new file mode 100644 index 00000000..71cbb6e2 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/AvroTimeMillis.cs @@ -0,0 +1,14 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models.Avro.LogicalTypes +{ + public class AvroTimeMillis : AvroLogicalType + { + public AvroTimeMillis() + : base(AvroPrimitiveType.Int) + { + } + + public override LogicalType LogicalType => LogicalType.Time_Millis; + } +} diff --git a/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/AvroTimestampMicros.cs b/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/AvroTimestampMicros.cs new file mode 100644 index 00000000..295f2883 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/AvroTimestampMicros.cs @@ -0,0 +1,14 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models.Avro.LogicalTypes +{ + public class AvroTimestampMicros : AvroLogicalType + { + public AvroTimestampMicros() + : base(AvroPrimitiveType.Long) + { + } + + public override LogicalType LogicalType => LogicalType.Timestamp_Micros; + } +} diff --git a/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/AvroTimestampMillis.cs b/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/AvroTimestampMillis.cs new file mode 100644 index 00000000..0f03ca0c --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/AvroTimestampMillis.cs @@ -0,0 +1,14 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models.Avro.LogicalTypes +{ + public class AvroTimestampMillis : AvroLogicalType + { + public AvroTimestampMillis() + : base(AvroPrimitiveType.Long) + { + } + + public override LogicalType LogicalType => LogicalType.Timestamp_Millis; + } +} diff --git a/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/AvroUUID.cs b/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/AvroUUID.cs new file mode 100644 index 00000000..6a7a27a0 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/AvroUUID.cs @@ -0,0 +1,14 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models.Avro.LogicalTypes +{ + public class AvroUUID : AvroLogicalType + { + public AvroUUID() + : base(AvroPrimitiveType.String) + { + } + + public override LogicalType LogicalType => LogicalType.UUID; + } +} diff --git a/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/LogicalType.cs b/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/LogicalType.cs new file mode 100644 index 00000000..7db53258 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/LogicalTypes/LogicalType.cs @@ -0,0 +1,33 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models.Avro.LogicalTypes +{ + using LEGO.AsyncAPI.Attributes; + + public enum LogicalType + { + [Display("decimal")] + Decimal, + + [Display("uuid")] + UUID, + + [Display("date")] + Date, + + [Display("time-millis")] + Time_Millis, + + [Display("time-micros")] + Time_Micros, + + [Display("timestamp-millis")] + Timestamp_Millis, + + [Display("timestamp-micros")] + Timestamp_Micros, + + [Display("duration")] + Duration, + } +} diff --git a/src/LEGO.AsyncAPI/Models/Interfaces/IAsyncApiPayload.cs b/src/LEGO.AsyncAPI/Models/Interfaces/IAsyncApiPayload.cs new file mode 100644 index 00000000..76112af9 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Interfaces/IAsyncApiPayload.cs @@ -0,0 +1,8 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models.Interfaces +{ + public interface IAsyncApiMessagePayload : IAsyncApiSerializable, IAsyncApiReferenceable + { + } +} diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiSchema.cs b/src/LEGO.AsyncAPI/Models/JsonSchema/AsyncApiSchema.cs similarity index 100% rename from src/LEGO.AsyncAPI/Models/AsyncApiSchema.cs rename to src/LEGO.AsyncAPI/Models/JsonSchema/AsyncApiSchema.cs diff --git a/src/LEGO.AsyncAPI/Models/ParameterLocation.cs b/src/LEGO.AsyncAPI/Models/ParameterLocation.cs index 54feb896..bcda720d 100644 --- a/src/LEGO.AsyncAPI/Models/ParameterLocation.cs +++ b/src/LEGO.AsyncAPI/Models/ParameterLocation.cs @@ -7,12 +7,12 @@ namespace LEGO.AsyncAPI.Models public enum ParameterLocation { /// - /// The user + /// The user. /// [Display("user")] User, /// - /// The password + /// The password. /// [Display("password")] Password, diff --git a/src/LEGO.AsyncAPI/Models/ReferenceType.cs b/src/LEGO.AsyncAPI/Models/ReferenceType.cs index 8903dd73..cc7dcaf5 100644 --- a/src/LEGO.AsyncAPI/Models/ReferenceType.cs +++ b/src/LEGO.AsyncAPI/Models/ReferenceType.cs @@ -84,7 +84,7 @@ public enum ReferenceType [Display("headers")] Header, /// - /// The server variable + /// The server variable. /// [Display("serverVariable")] ServerVariable, } diff --git a/src/LEGO.AsyncAPI/Resource.Designer.cs b/src/LEGO.AsyncAPI/Resource.Designer.cs index 983e6ad0..755eda9c 100644 --- a/src/LEGO.AsyncAPI/Resource.Designer.cs +++ b/src/LEGO.AsyncAPI/Resource.Designer.cs @@ -104,5 +104,23 @@ internal static string Validation_MustBeAbsoluteUrl { return ResourceManager.GetString("Validation_MustBeAbsoluteUrl", resourceCulture); } } + + /// + /// Looks up a localized string similar to '{0}' MUST match the regular expression '{1}'.. + /// + internal static string Validation_NameMustMatchRegularExpr { + get { + return ResourceManager.GetString("Validation_NameMustMatchRegularExpr", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Symbols MUST match the regular expression '{1}'.. + /// + internal static string Validation_SymbolsMustMatchRegularExpression { + get { + return ResourceManager.GetString("Validation_SymbolsMustMatchRegularExpression", resourceCulture); + } + } } } diff --git a/src/LEGO.AsyncAPI/Resource.resx b/src/LEGO.AsyncAPI/Resource.resx index fa6a1ecc..9d882464 100644 --- a/src/LEGO.AsyncAPI/Resource.resx +++ b/src/LEGO.AsyncAPI/Resource.resx @@ -132,4 +132,10 @@ The field '{0}' in '{1}' object MUST be an absolute uri. + + '{0}' MUST match the regular expression '{1}'. + + + Symbols MUST match the regular expression '{1}'. + \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Services/AsyncApiReferenceResolver.cs b/src/LEGO.AsyncAPI/Services/AsyncApiReferenceResolver.cs index b3c693e0..3d54dae8 100644 --- a/src/LEGO.AsyncAPI/Services/AsyncApiReferenceResolver.cs +++ b/src/LEGO.AsyncAPI/Services/AsyncApiReferenceResolver.cs @@ -1,5 +1,4 @@ // Copyright (c) The LEGO Group. All rights reserved. - namespace LEGO.AsyncAPI.Services { using System; @@ -17,7 +16,8 @@ internal class AsyncApiReferenceResolver : AsyncApiVisitorBase private AsyncApiDocument currentDocument; private List errors = new List(); - public AsyncApiReferenceResolver(AsyncApiDocument currentDocument) + public AsyncApiReferenceResolver( + AsyncApiDocument currentDocument) { this.currentDocument = currentDocument; } @@ -86,7 +86,13 @@ public override void Visit(AsyncApiOperation operation) public override void Visit(AsyncApiMessage message) { this.ResolveObject(message.Headers, r => message.Headers = r); - this.ResolveObject(message.Payload, r => message.Payload = r); + + // #ToFix Resolve references correctly + if (message.Payload is AsyncApiJsonSchemaPayload) + { + this.ResolveObject(message.Payload as AsyncApiJsonSchemaPayload, r => message.Payload = new AsyncApiJsonSchemaPayload(r)); + } + this.ResolveList(message.Traits); this.ResolveObject(message.CorrelationId, r => message.CorrelationId = r); this.ResolveObject(message.Bindings, r => message.Bindings = r); @@ -207,6 +213,7 @@ private void ResolveMap(IDictionary map) private T ResolveReference(AsyncApiReference reference) where T : class, IAsyncApiReferenceable, new() { + // external references are resolved by the AsyncApiExternalReferenceResolver if (reference.IsExternal) { return new() diff --git a/src/LEGO.AsyncAPI/Services/AsyncApiVisitorBase.cs b/src/LEGO.AsyncAPI/Services/AsyncApiVisitorBase.cs index 899731d0..c048e0f5 100644 --- a/src/LEGO.AsyncAPI/Services/AsyncApiVisitorBase.cs +++ b/src/LEGO.AsyncAPI/Services/AsyncApiVisitorBase.cs @@ -55,6 +55,14 @@ public virtual void Visit(AsyncApiDocument doc) { } + public virtual void Visit(AsyncApiJsonSchemaPayload jsonPayload) + { + } + + public virtual void Visit(AsyncApiAvroSchemaPayload avroPayload) + { + } + public virtual void Visit(IDictionary anys) { } diff --git a/src/LEGO.AsyncAPI/Services/AsyncApiWalker.cs b/src/LEGO.AsyncAPI/Services/AsyncApiWalker.cs index 844ab7e9..9f9c40ee 100644 --- a/src/LEGO.AsyncAPI/Services/AsyncApiWalker.cs +++ b/src/LEGO.AsyncAPI/Services/AsyncApiWalker.cs @@ -326,6 +326,11 @@ internal void Walk(AsyncApiParameter parameter, bool isComponent = false) this.Walk(parameter as IAsyncApiExtensible); } + internal void Walk(AsyncApiAvroSchemaPayload payload) + { + this.visitor.Visit(payload); + } + internal void Walk(AsyncApiSchema schema, bool isComponent = false) { if (schema == null || this.ProcessAsReference(schema, isComponent)) @@ -534,7 +539,16 @@ internal void Walk(AsyncApiMessage message, bool isComponent = false) if (message != null) { this.Walk(AsyncApiConstants.Headers, () => this.Walk(message.Headers)); - this.Walk(AsyncApiConstants.Payload, () => this.Walk(message.Payload)); + if (message.Payload is AsyncApiJsonSchemaPayload payload) + { + this.Walk(AsyncApiConstants.Payload, () => this.Walk((AsyncApiSchema)payload)); + } + + if (message.Payload is AsyncApiAvroSchemaPayload avroPayload) + { + this.Walk(AsyncApiConstants.Payload, () => this.Walk(avroPayload)); + } + this.Walk(AsyncApiConstants.CorrelationId, () => this.Walk(message.CorrelationId)); this.Walk(AsyncApiConstants.Tags, () => this.Walk(message.Tags)); this.Walk(AsyncApiConstants.Examples, () => this.Walk(message.Examples)); diff --git a/src/LEGO.AsyncAPI/Validation/AsyncApiValidator.cs b/src/LEGO.AsyncAPI/Validation/AsyncApiValidator.cs index 3f8bf392..32b36ca6 100644 --- a/src/LEGO.AsyncAPI/Validation/AsyncApiValidator.cs +++ b/src/LEGO.AsyncAPI/Validation/AsyncApiValidator.cs @@ -144,6 +144,10 @@ public void AddWarning(AsyncApiValidatorWarning warning) public override void Visit(IMessageBinding item) => this.Validate(item); + public override void Visit(AsyncApiAvroSchemaPayload item) => this.Validate(item); + + public override void Visit(AsyncApiJsonSchemaPayload item) => this.Validate(item); + /// /// Execute validation rules against an . /// diff --git a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiAvroRules.cs b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiAvroRules.cs new file mode 100644 index 00000000..b12c88b2 --- /dev/null +++ b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiAvroRules.cs @@ -0,0 +1,60 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Validation.Rules +{ + using System.Linq; + using System.Text.RegularExpressions; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Validations; + + [AsyncApiRule] + public static class AsyncApiAvroRules + { + /// + /// The key regex. + /// + public static Regex NameRegex = new Regex(@"^[A-Za-z_][A-Za-z0-9_]*$"); + + public static ValidationRule NameRegularExpression => + new ValidationRule( + (context, avroPayload) => + { + string name = null; + context.Enter("name"); + if (avroPayload.TryGetAs(out var record)) + { + name = record.Name; + } + + if (avroPayload.TryGetAs(out var @enum)) + { + name = @enum.Name; + if (@enum.Symbols.Any(symbol => !NameRegex.IsMatch(symbol))) + { + context.CreateError( + "SymbolsRegularExpression", + string.Format(Resource.Validation_SymbolsMustMatchRegularExpression, NameRegex.ToString())); + } + } + + if (avroPayload.TryGetAs(out var @fixed)) + { + name = @fixed.Name; + } + + if (name == null) + { + return; + } + + if (!NameRegex.IsMatch(record.Name)) + { + context.CreateError( + nameof(NameRegex), + string.Format(Resource.Validation_NameMustMatchRegularExpr, name, NameRegex.ToString())); + } + + context.Exit(); + }); + } +} diff --git a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs index 27076369..264c1611 100644 --- a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs +++ b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs @@ -30,7 +30,7 @@ public static class AsyncApiDocumentRules context.Exit(); context.Enter("channels"); - if (document.Channels == null || !document.Channels.Keys.Any()) + if (document.Channels == null) { context.CreateError( nameof(DocumentRequiredFields), diff --git a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiOAuthFlowRules.cs b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiOAuthFlowRules.cs index 457a03ec..66090074 100644 --- a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiOAuthFlowRules.cs +++ b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiOAuthFlowRules.cs @@ -2,9 +2,9 @@ namespace LEGO.AsyncAPI.Validation.Rules { + using System.Linq; using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Validations; - using System.Linq; [AsyncApiRule] public static class AsyncApiOAuthFlowRules diff --git a/src/LEGO.AsyncAPI/Writers/AsyncApiWriterException.cs b/src/LEGO.AsyncAPI/Writers/AsyncApiWriterException.cs index 9d89bba3..17229818 100644 --- a/src/LEGO.AsyncAPI/Writers/AsyncApiWriterException.cs +++ b/src/LEGO.AsyncAPI/Writers/AsyncApiWriterException.cs @@ -2,8 +2,8 @@ namespace LEGO.AsyncAPI.Writers { - using LEGO.AsyncAPI.Exceptions; using System; + using LEGO.AsyncAPI.Exceptions; public class AsyncApiWriterException : AsyncApiException { diff --git a/src/LEGO.AsyncAPI/Writers/AsyncApiWriterExtensions.cs b/src/LEGO.AsyncAPI/Writers/AsyncApiWriterExtensions.cs index 67c4737a..6547a359 100644 --- a/src/LEGO.AsyncAPI/Writers/AsyncApiWriterExtensions.cs +++ b/src/LEGO.AsyncAPI/Writers/AsyncApiWriterExtensions.cs @@ -204,6 +204,22 @@ public static void WriteOptionalCollection( } } + /// + /// Write the required of collection string. + /// + /// The AsyncApi writer. + /// The property name. + /// The collection values. + /// The collection element writer action. + public static void WriteRequiredCollection( + this IAsyncApiWriter writer, + string name, + IEnumerable elements, + Action action) + { + writer.WriteCollectionInternal(name, elements, action); + } + /// /// Write the optional AsyncApi object/element collection. /// @@ -285,10 +301,7 @@ public static void WriteRequiredMap( Action action) where T : IAsyncApiElement { - if (elements != null && elements.Any()) - { - writer.WriteMapInternal(name, elements, action); - } + writer.WriteMapInternal(name, elements, action); } /// diff --git a/src/LEGO.AsyncAPI/Writers/AsyncApiYamlWriter.cs b/src/LEGO.AsyncAPI/Writers/AsyncApiYamlWriter.cs index d0fab76e..a278ebcd 100644 --- a/src/LEGO.AsyncAPI/Writers/AsyncApiYamlWriter.cs +++ b/src/LEGO.AsyncAPI/Writers/AsyncApiYamlWriter.cs @@ -1,13 +1,13 @@ // Copyright (c) The LEGO Group. All rights reserved. -using System; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; - namespace LEGO.AsyncAPI.Writers { + using System; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Text.RegularExpressions; + /// /// Used to conver an AsyncApi schema into a yaml document. /// @@ -42,7 +42,7 @@ public AsyncApiYamlWriter(TextWriter textWriter) /// Initializes a new instance of the class. /// /// The text writer. - /// The settings used to read and write yaml + /// The settings used to read and write yaml. public AsyncApiYamlWriter(TextWriter textWriter, AsyncApiWriterSettings settings) : base(textWriter, settings) { diff --git a/src/LEGO.AsyncAPI/Writers/SpecialCharacterStringExtensions.cs b/src/LEGO.AsyncAPI/Writers/SpecialCharacterStringExtensions.cs index f2f6fbe2..9bce6aa2 100644 --- a/src/LEGO.AsyncAPI/Writers/SpecialCharacterStringExtensions.cs +++ b/src/LEGO.AsyncAPI/Writers/SpecialCharacterStringExtensions.cs @@ -2,11 +2,6 @@ namespace LEGO.AsyncAPI.Writers { - using System; - using System.Globalization; - using System.Linq; - using System.Text.RegularExpressions; - public static class SpecialCharacterStringExtensions { /// diff --git a/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentBuilder.cs b/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentBuilder.cs index 3f2ee429..315ace21 100644 --- a/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentBuilder.cs +++ b/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentBuilder.cs @@ -2,9 +2,10 @@ namespace LEGO.AsyncAPI.Tests { + using System; + using System.Collections.Generic; using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Models.Interfaces; - using System; internal class AsyncApiDocumentBuilder { @@ -42,6 +43,11 @@ public AsyncApiDocumentBuilder WithDefaultContentType(string contentType = "appl public AsyncApiDocumentBuilder WithChannel(string key, AsyncApiChannel channel) { + if (this.document.Channels == null) + { + this.document.Channels = new Dictionary(); + } + this.document.Channels.Add(key, channel); return this; } diff --git a/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs b/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs index 63e818b4..dbcb8026 100644 --- a/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs +++ b/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs @@ -4,7 +4,6 @@ namespace LEGO.AsyncAPI.Tests { using System; using System.Collections.Generic; - using System.Globalization; using System.IO; using System.Linq; using FluentAssertions; @@ -493,7 +492,7 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() }, }, }, - Payload = new AsyncApiSchema() + Payload = new AsyncApiJsonSchemaPayload { Reference = new AsyncApiReference() { @@ -518,7 +517,7 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() }, }, }, - Payload = new AsyncApiSchema() + Payload = new AsyncApiJsonSchemaPayload() { Reference = new AsyncApiReference() { @@ -543,7 +542,7 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() }, }, }, - Payload = new AsyncApiSchema() + Payload = new AsyncApiJsonSchemaPayload() { Reference = new AsyncApiReference() { @@ -1132,6 +1131,108 @@ public void SerializeV2_WithFullSpec_Serializes() .BePlatformAgnosticEquivalentTo(expected); } + [Test] + public void Read_WithAvroSchemaPayload_NoErrors() + { + // Arrange + var yaml = + """ + asyncapi: '2.6.0' + info: + title: schema-validation-test + version: '1.0.0' + description: Async API for schema validation tests + contact: + name: Test + url: https://test.test/ + + channels: + schema-validation-topic: + description: A topic to publish messages for testing Pulsar schema validation + publish: + message: + $ref: '#/components/messages/schema-validation-message' + + components: + messages: + schema-validation-message: + name: schema-validation-message + title: Message for schema validation testing that is a json object + summary: A test message is used for testing Pulsar schema validation + payload: + type: record + name: UserSignedUp + namespace: esp + doc: ESP Schema validation test + fields: + - name: userId + type: int + - name: userEmail + type: string + schemaFormat: 'application/vnd.apache.avro;version=1.9.0' + """; + + // Act + var result = new AsyncApiStringReader().Read(yaml, out var diagnostics); + + // Assert + diagnostics.Errors.Should().HaveCount(0); + result.Channels.First().Value.Publish.Message.First().Payload.As().TryGetAs(out var record).Should().BeTrue(); + record.Name.Should().Be("UserSignedUp"); + } + + [Test] + public void Read_WithJsonSchemaReference_NoErrors() + { + // Arrange + var yaml = + """ + asyncapi: '2.6.0' + info: + title: schema-validation-test + version: '1.0.0' + description: Async API for schema validation tests + contact: + name: Test + url: https://test.test/ + + channels: + schema-validation-topic: + description: A topic to publish messages for testing Pulsar schema validation + publish: + message: + $ref: '#/components/messages/schema-validation-message' + subscribe: + message: + $ref: '#/components/messages/schema-validation-message' + + components: + schemas: + schema-validation-message-payload: + type: object + properties: + content: + type: string + description: Content of the message + messages: + schema-validation-message: + name: schema-validation-message + title: Message for schema validation testing that is a json object + summary: A test message is used for testing Pulsar schema validation + payload: + $ref: '#/components/schemas/schema-validation-message-payload' + contentType: application/json + """; + + // Act + var result = new AsyncApiStringReader().Read(yaml, out var diagnostics); + + // Assert + diagnostics.Errors.Should().HaveCount(0); + result.Channels.First().Value.Publish.Message.First().Title.Should().Be("Message for schema validation testing that is a json object"); + result.Channels.First().Value.Publish.Message.First().Payload.As().Properties.Should().HaveCount(1); + } + [Test] public void Serialize_WithBindingReferences_SerializesDeserializes() { @@ -1202,6 +1303,10 @@ public void Serialize_WithBindingReferences_SerializesDeserializes() }, }, }; + if (doc.Channels == null) + { + doc.Channels = new Dictionary(); + } doc.Channels.Add( "testChannel", new AsyncApiChannel @@ -1260,6 +1365,10 @@ public void Serializev2_WithBindings_Serializes() Protocol = "pulsar+ssl", Url = "example.com", }); + if (doc.Channels == null) + { + doc.Channels = new Dictionary(); + } doc.Channels.Add( "testChannel", new AsyncApiChannel diff --git a/test/LEGO.AsyncAPI.Tests/AsyncApiReaderTests.cs b/test/LEGO.AsyncAPI.Tests/AsyncApiReaderTests.cs index 8953f2cd..036fae56 100644 --- a/test/LEGO.AsyncAPI.Tests/AsyncApiReaderTests.cs +++ b/test/LEGO.AsyncAPI.Tests/AsyncApiReaderTests.cs @@ -32,6 +32,7 @@ public void Read_WithExtensionParser_Parses() info: title: test version: 1.0.0 + test: 1234 contact: name: API Support url: https://www.example.com/support @@ -59,6 +60,7 @@ public void Read_WithExtensionParser_Parses() { { extensionName, valueExtensionParser }, }, + UnmappedMemberHandling = UnmappedMemberHandling.Ignore, }; var reader = new AsyncApiStringReader(settings); @@ -66,6 +68,96 @@ public void Read_WithExtensionParser_Parses() Assert.AreEqual((doc.Channels["workspace"].Extensions[extensionName] as AsyncApiAny).GetValue(), 1234); } + [Test] + public void Read_WithUnmappedMemberHandlingError_AddsError() + { + var extensionName = "x-someValue"; + var yaml = $""" + asyncapi: 2.3.0 + info: + title: test + version: 1.0.0 + test: 1234 + contact: + name: API Support + url: https://www.example.com/support + email: support@example.com + channels: + workspace: + {extensionName}: onetwothreefour + """; + Func valueExtensionParser = (any) => + { + if (any.TryGetValue(out var value)) + { + if (value == "onetwothreefour") + { + return new AsyncApiAny(1234); + } + } + + return new AsyncApiAny("No value provided"); + }; + + var settings = new AsyncApiReaderSettings + { + ExtensionParsers = new Dictionary> + { + { extensionName, valueExtensionParser }, + }, + UnmappedMemberHandling = UnmappedMemberHandling.Error, + }; + + var reader = new AsyncApiStringReader(settings); + var doc = reader.Read(yaml, out var diagnostic); + diagnostic.Errors.Should().HaveCount(1); + } + + [Test] + public void Read_WithUnmappedMemberHandlingIgnore_NoErrors() + { + var extensionName = "x-someValue"; + var yaml = $""" + asyncapi: 2.3.0 + info: + title: test + version: 1.0.0 + test: 1234 + contact: + name: API Support + url: https://www.example.com/support + email: support@example.com + channels: + workspace: + {extensionName}: onetwothreefour + """; + Func valueExtensionParser = (any) => + { + if (any.TryGetValue(out var value)) + { + if (value == "onetwothreefour") + { + return new AsyncApiAny(1234); + } + } + + return new AsyncApiAny("No value provided"); + }; + + var settings = new AsyncApiReaderSettings + { + ExtensionParsers = new Dictionary> + { + { extensionName, valueExtensionParser }, + }, + UnmappedMemberHandling = UnmappedMemberHandling.Ignore, + }; + + var reader = new AsyncApiStringReader(settings); + var doc = reader.Read(yaml, out var diagnostic); + diagnostic.Errors.Should().HaveCount(0); + } + [Test] public void Read_WithThrowingExtensionParser_AddsToDiagnostics() { diff --git a/test/LEGO.AsyncAPI.Tests/Bindings/StringOrStringList_Should.cs b/test/LEGO.AsyncAPI.Tests/Bindings/StringOrStringList_Should.cs index c437f15c..1a7c8681 100644 --- a/test/LEGO.AsyncAPI.Tests/Bindings/StringOrStringList_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Bindings/StringOrStringList_Should.cs @@ -4,7 +4,6 @@ namespace LEGO.AsyncAPI.Tests.Bindings { using System; using System.Collections.Generic; - using System.Linq; using FluentAssertions; using LEGO.AsyncAPI.Bindings; using LEGO.AsyncAPI.Models; diff --git a/test/LEGO.AsyncAPI.Tests/FluentAssertionExtensions.cs b/test/LEGO.AsyncAPI.Tests/FluentAssertionExtensions.cs index 31fec68a..a9a771ab 100644 --- a/test/LEGO.AsyncAPI.Tests/FluentAssertionExtensions.cs +++ b/test/LEGO.AsyncAPI.Tests/FluentAssertionExtensions.cs @@ -3,7 +3,6 @@ namespace LEGO.AsyncAPI.Tests { using System; - using System.IO; using FluentAssertions; using FluentAssertions.Primitives; using NUnit.Framework; diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiAnyTests.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiAnyTests.cs index 6c93e483..1dfdc59a 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiAnyTests.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiAnyTests.cs @@ -1,10 +1,9 @@ // Copyright (c) The LEGO Group. All rights reserved. -using LEGO.AsyncAPI.Models; -using NUnit.Framework; -using System; using System.Collections.Generic; using System.Linq; +using LEGO.AsyncAPI.Models; +using NUnit.Framework; namespace LEGO.AsyncAPI.Tests { diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiChannel_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiChannel_Should.cs index 50c6768b..b19cd383 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiChannel_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiChannel_Should.cs @@ -12,7 +12,7 @@ namespace LEGO.AsyncAPI.Tests.Models using LEGO.AsyncAPI.Readers; using NUnit.Framework; - internal class AsyncApiChannel_Should : TestBase + public class AsyncApiChannel_Should : TestBase { [Test] public void AsyncApiChannel_WithInlineParameter_DoesNotCreateReference() diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs index a37f1503..e37aa0d0 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs @@ -41,7 +41,7 @@ public void AsyncApiMessage_WithNoType_DeserializesToDefault() // Assert diagnostic.Errors.Should().BeEmpty(); - message.Payload.Properties.First().Value.Enum.Should().HaveCount(2); + message.Payload.As().Properties.First().Value.Enum.Should().HaveCount(2); } [Test] @@ -78,7 +78,7 @@ public void AsyncApiMessage_WithUnsupportedSchemaFormat_DeserializesWithError() type: - 'null' - string - schemaFormat: application/vnd.apache.avro;version=1.9.0 + schemaFormat: whatever """; // Act @@ -86,7 +86,7 @@ public void AsyncApiMessage_WithUnsupportedSchemaFormat_DeserializesWithError() // Assert diagnostic.Errors.Should().HaveCount(1); - diagnostic.Errors.First().Message.Should().StartWith("'application/vnd.apache.avro;version=1.9.0' is not a supported format"); + diagnostic.Errors.First().Message.Should().StartWith("'whatever' is not a supported format"); } [Test] @@ -104,7 +104,7 @@ public void AsyncApiMessage_WithNoSchemaFormat_DoesNotSerializeSchemaFormat() """; var message = new AsyncApiMessage(); - message.Payload = new AsyncApiSchema() + message.Payload = new AsyncApiJsonSchemaPayload() { Properties = new Dictionary() { @@ -120,7 +120,6 @@ public void AsyncApiMessage_WithNoSchemaFormat_DoesNotSerializeSchemaFormat() // Act var actual = message.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); - var deserializedMessage = new AsyncApiStringReader().ReadFragment(expected, AsyncApiVersion.AsyncApi2_0, out _); // Assert @@ -130,7 +129,7 @@ public void AsyncApiMessage_WithNoSchemaFormat_DoesNotSerializeSchemaFormat() } [Test] - public void AsyncApiMessage_WithSchemaFormat_Serializes() + public void AsyncApiMessage_WithJsonSchemaFormat_Serializes() { // Arrange var expected = @@ -146,7 +145,7 @@ public void AsyncApiMessage_WithSchemaFormat_Serializes() var message = new AsyncApiMessage(); message.SchemaFormat = "application/vnd.aai.asyncapi+json;version=2.6.0"; - message.Payload = new AsyncApiSchema() + message.Payload = new AsyncApiJsonSchemaPayload() { Properties = new Dictionary() { @@ -169,6 +168,76 @@ public void AsyncApiMessage_WithSchemaFormat_Serializes() message.Should().BeEquivalentTo(deserializedMessage); } + [Test] + public void AsyncApiMessage_WithAvroSchemaFormat_Serializes() + { + // Arrange + var expected = + """ + payload: + type: record + name: User + namespace: com.example + fields: + - name: username + type: string + doc: The username of the user. + default: guest + order: ascending + schemaFormat: application/vnd.apache.avro + """; + + var message = new AsyncApiMessage(); + message.SchemaFormat = "application/vnd.apache.avro"; + var schema = new AvroRecord() + { + Name = "User", + Namespace = "com.example", + Fields = new List + { + new AvroField() + { + Name = "username", + Type = AvroPrimitiveType.String, + Doc = "The username of the user.", + Default = new AsyncApiAny("guest"), + Order = AvroFieldOrder.Ascending, + }, + }, + }; + message.Payload = new AsyncApiAvroSchemaPayload(schema); + + // Act + var actual = message.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + var deserializedMessage = new AsyncApiStringReader().ReadFragment(expected, AsyncApiVersion.AsyncApi2_0, out _); + + // Assert + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + message.Should().BeEquivalentTo(deserializedMessage); + } + + [Test] + public void AsyncApiMessage_WithAvroAsReference_Deserializes() + { + // Arrange + var input = + """ + schemaFormat: 'application/vnd.apache.avro+yaml;version=1.9.0' + payload: + $ref: 'path/to/user-create.avsc/#UserCreate' + """; + + // Act + var deserializedMessage = new AsyncApiStringReader().ReadFragment(input, AsyncApiVersion.AsyncApi2_0, out _); + + // Assert + deserializedMessage.Payload.Reference.Should().NotBeNull(); + deserializedMessage.Payload.Reference.IsExternal.Should().BeTrue(); + deserializedMessage.Payload.Reference.IsFragment.Should().BeTrue(); + + } + [Test] public void AsyncApiMessage_WithFilledObject_Serializes() { @@ -257,7 +326,7 @@ public void AsyncApiMessage_WithFilledObject_Serializes() }), }, }, - Payload = new AsyncApiSchema() + Payload = new AsyncApiJsonSchemaPayload() { Properties = new Dictionary { diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiOperation_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiOperation_Should.cs index 5597b211..847da7be 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiOperation_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiOperation_Should.cs @@ -3,7 +3,6 @@ namespace LEGO.AsyncAPI.Tests.Models { using System; - using System.Globalization; using System.IO; using FluentAssertions; using LEGO.AsyncAPI.Bindings.Http; diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs index c4491e40..16d58b4c 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs @@ -2,12 +2,12 @@ namespace LEGO.AsyncAPI.Tests { + using System.Linq; using FluentAssertions; - using FluentAssertions.Primitives; + using LEGO.AsyncAPI.Extensions; using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Readers; using NUnit.Framework; - using System.Linq; public class AsyncApiReference_Should : TestBase { @@ -26,9 +26,10 @@ public void AsyncApiReference_WithExternalFragmentUriReference_AllowReference() // Assert diagnostic.Errors.Should().BeEmpty(); - deserialized.Payload.UnresolvedReference.Should().BeTrue(); + var payload = deserialized.Payload.As(); + payload.UnresolvedReference.Should().BeTrue(); - var reference = deserialized.Payload.Reference; + var reference = payload.Reference; reference.ExternalResource.Should().Be("http://example.com/some-resource"); reference.Id.Should().Be("/path/to/external/fragment"); reference.IsFragment.Should().BeTrue(); @@ -54,9 +55,10 @@ public void AsyncApiReference_WithFragmentReference_AllowReference() // Assert diagnostic.Errors.Should().BeEmpty(); - deserialized.Payload.UnresolvedReference.Should().BeTrue(); + var payload = deserialized.Payload.As(); + payload.UnresolvedReference.Should().BeTrue(); - var reference = deserialized.Payload.Reference; + var reference = payload.Reference; reference.Type.Should().Be(ReferenceType.Schema); reference.ExternalResource.Should().Be("/fragments/myFragment"); reference.Id.Should().BeNull(); @@ -82,7 +84,8 @@ public void AsyncApiReference_WithInternalComponentReference_AllowReference() // Assert diagnostic.Errors.Should().BeEmpty(); - var reference = deserialized.Payload.Reference; + var payload = deserialized.Payload.As(); + var reference = payload.Reference; reference.ExternalResource.Should().BeNull(); reference.Type.Should().Be(ReferenceType.Schema); reference.Id.Should().Be("test"); @@ -109,7 +112,8 @@ public void AsyncApiReference_WithExternalFragmentReference_AllowReference() // Assert diagnostic.Errors.Should().BeEmpty(); - var reference = deserialized.Payload.Reference; + var payload = deserialized.Payload.As(); + var reference = payload.Reference; reference.ExternalResource.Should().Be("./myjsonfile.json"); reference.Id.Should().Be("/fragment"); reference.IsFragment.Should().BeTrue(); @@ -135,7 +139,8 @@ public void AsyncApiReference_WithExternalComponentReference_AllowReference() // Assert diagnostic.Errors.Should().BeEmpty(); - var reference = deserialized.Payload.Reference; + var payload = deserialized.Payload.As(); + var reference = payload.Reference; reference.ExternalResource.Should().Be("./someotherdocument.json"); reference.Type.Should().Be(ReferenceType.Schema); reference.Id.Should().Be("test"); @@ -167,7 +172,7 @@ public void AsyncApiDocument_WithInternalComponentReference_ResolvesReference() var settings = new AsyncApiReaderSettings() { - ReferenceResolution = ReferenceResolutionSetting.ResolveReferences, + ReferenceResolution = ReferenceResolutionSetting.ResolveInternalReferences, }; var reader = new AsyncApiStringReader(settings); @@ -187,7 +192,7 @@ public void AsyncApiDocument_WithInternalComponentReference_ResolvesReference() } [Test] - public void AsyncApiDocument_WithExternalReference_DoesNotResolve() + public void AsyncApiDocument_WithNoConfiguredExternalReferenceReader_ThrowsError() { // Arrange var actual = """ @@ -202,7 +207,38 @@ public void AsyncApiDocument_WithExternalReference_DoesNotResolve() var settings = new AsyncApiReaderSettings() { - ReferenceResolution = ReferenceResolutionSetting.ResolveReferences, + ReferenceResolution = ReferenceResolutionSetting.ResolveAllReferences, + }; + var reader = new AsyncApiStringReader(settings); + + // Act + reader.Read(actual, out var diagnostic); + + // Assert + diagnostic.Errors.Count.Should().Be(1); + var error = diagnostic.Errors.First(); + error.Message.Should() + .Be( + "External reference configured in AsyncApi document but no implementation provided for ExternalReferenceReader."); + } + + [Test] + public void AsyncApiDocument_WithExternalReferenceOnlySetToResolveInternalReferences_DoesNotResolve() + { + // Arrange + var actual = """ + asyncapi: 2.6.0 + info: + title: My AsyncAPI Document + version: 1.0.0 + channels: + myChannel: + $ref: http://example.com/channel.json + """; + + var settings = new AsyncApiReaderSettings() + { + ReferenceResolution = ReferenceResolutionSetting.ResolveInternalReferences, }; var reader = new AsyncApiStringReader(settings); @@ -237,7 +273,8 @@ public void AsyncApiReference_WithExternalReference_AllowsReferenceDoesNotResolv // Assert diagnostic.Errors.Should().BeEmpty(); - var reference = deserialized.Payload.Reference; + var payload = deserialized.Payload.As(); + var reference = payload.Reference; reference.ExternalResource.Should().Be("http://example.com/json.json"); reference.Id.Should().BeNull(); reference.IsExternal.Should().BeTrue(); @@ -250,5 +287,154 @@ public void AsyncApiReference_WithExternalReference_AllowsReferenceDoesNotResolv .Should() .BePlatformAgnosticEquivalentTo(actual); } + + [Test] + public void AsyncApiReference_WithExternalResourcesInterface_DeserializesCorrectly() + { + var yaml = """ + asyncapi: 2.3.0 + info: + title: test + version: 1.0.0 + channels: + workspace: + publish: + message: + $ref: "./some/path/to/external/message.yaml" + """; + var settings = new AsyncApiReaderSettings + { + ReferenceResolution = ReferenceResolutionSetting.ResolveAllReferences, + ExternalReferenceReader = new MockExternalReferenceReader(), + }; + var reader = new AsyncApiStringReader(settings); + var doc = reader.Read(yaml, out var diagnostic); + var message = doc.Channels["workspace"].Publish.Message.First(); + message.Name.Should().Be("Test"); + var payload = message.Payload.As(); + payload.Properties.Count.Should().Be(3); + } + + [Test] + public void AvroReference_WithExternalResourcesInterface_DeserializesCorrectly() + { + var yaml = """ + asyncapi: 2.3.0 + info: + title: test + version: 1.0.0 + channels: + workspace: + publish: + message: + schemaFormat: 'application/vnd.apache.avro+yaml;version=1.9.0' + payload: + $ref: 'path/to/user-create.avsc/#UserCreate' + """; + var settings = new AsyncApiReaderSettings + { + ReferenceResolution = ReferenceResolutionSetting.ResolveAllReferences, + ExternalReferenceReader = new MockExternalAvroReferenceReader(), + }; + var reader = new AsyncApiStringReader(settings); + var doc = reader.Read(yaml, out var diagnostic); + var payload = doc.Channels["workspace"].Publish.Message.First().Payload; + payload.Should().BeAssignableTo(typeof(AsyncApiAvroSchemaPayload)); + var avro = payload as AsyncApiAvroSchemaPayload; + avro.TryGetAs(out var record); + record.Name.Should().Be("SomeEvent"); + } + } + + public class MockExternalAvroReferenceReader : IAsyncApiExternalReferenceReader + { + public string Load(string reference) + { + return + """ + { + "type": "record", + "name": "SomeEvent", + "namespace": "my.namspace.for.event", + "fields": [ + { + "name": "countryCode", + "type": "string", + "doc": "Country of the partner, (e.g. DE)" + }, + { + "name": "occurredOn", + "type": "string", + "doc": "Timestamp of when action occurred." + }, + { + "name": "partnerId", + "type": "string", + "doc": "Id of the partner" + }, + { + "name": "platformSource", + "type": "string", + "doc": "Platform source" + } + ], + "example": { + "occurredOn": "2023-11-03T09:56.582+00:00", + "partnerId": "1", + "platformSource": "Brecht", + "countryCode": "DE" + } + } + """; + } + } + public class MockExternalReferenceReader : IAsyncApiExternalReferenceReader + { + public string Load(string reference) + { + if (reference == "./some/path/to/external/message.yaml") + { + return """ + name: Test + title: Test message + summary: Test. + schemaFormat: application/schema+yaml;version=draft-07 + contentType: application/cloudevents+json + payload: + $ref: "./some/path/to/schema.yaml" + """; + } + + return """ + type: object + properties: + orderId: + description: The ID of the order. + type: string + format: uuid + name: + description: Name of order. + type: string + orderDetails: + description: User details. + type: object + properties: + userId: + description: User Id. + type: string + format: uuid + userName: + description: User name. + type: string + required: + - orderId + example: + orderId: 8f9189f8-653b-4849-a1ec-c838c030bd67 + handler: SomeName + orderDetails: + userId: Admin + userName: Admin + """; + } } } \ No newline at end of file diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiSchema_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiSchema_Should.cs index ec7c023f..61cc49ae 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiSchema_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiSchema_Should.cs @@ -4,7 +4,6 @@ namespace LEGO.AsyncAPI.Tests.Models { using System; using System.Collections.Generic; - using System.Globalization; using System.IO; using FluentAssertions; using LEGO.AsyncAPI.Models; @@ -421,7 +420,7 @@ public void Serialize_WithInliningOptions_ShouldInlineAccordingly(bool shouldInl { new AsyncApiMessage { - Payload = new AsyncApiSchema + Payload = new AsyncApiJsonSchemaPayload { Type = SchemaType.Object, Required = new HashSet { "testB" }, @@ -510,6 +509,28 @@ public void Serialize_WithAnyOf_DoesNotWriteIf() Assert.True(!yaml.Contains("if:")); } + [Test] + public void Deserialize_BasicExample() + { + var input = + """ + title: title1 + type: integer + maximum: 42 + minimum: 10 + exclusiveMinimum: 42.0 + multipleOf: 3 + default: 15 + nullable: true + externalDocs: + url: http://example.com/externalDocs + """; + + var schema = new AsyncApiStringReader().ReadFragment(input, AsyncApiVersion.AsyncApi2_0, out var diag); + + diag.Errors.Should().BeEmpty(); + schema.Should().BeEquivalentTo(AdvancedSchemaNumber); + } /// /// Regression test. /// Bug: Serializing properties multiple times - specifically Schema.Not was serialized into Not and Else. diff --git a/test/LEGO.AsyncAPI.Tests/Models/AvroSchema_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AvroSchema_Should.cs new file mode 100644 index 00000000..4dcce166 --- /dev/null +++ b/test/LEGO.AsyncAPI.Tests/Models/AvroSchema_Should.cs @@ -0,0 +1,468 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Tests.Models +{ + using System.Collections.Generic; + using FluentAssertions; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Readers; + using NUnit.Framework; + + public class AvroSchema_Should + { + [Test] + public void Serialize_WithDefaultNull_SetJsonNull() + { + var input = """ + type: record + name: User + namespace: Producer + doc: ESP Schema validation test + fields: + - name: userId + type: int + - name: userEmail + type: + - null + - string + default: null + """; + + var model = new AsyncApiStringReader().ReadFragment(input, AsyncApiVersion.AsyncApi2_0, out var diag); + var reserialized = model.SerializeAsJson(AsyncApiVersion.AsyncApi2_0); + reserialized.Should().Contain("default\": null"); + } + + [Test] + public void Deserialize_WithMetadata_CreatesMetadata() + { + var input = + """ + { + "type": "record", + "name": "SomeEvent", + "namespace": "my.namspace.for.event", + "fields": [ + { + "name": "countryCode", + "type": "string", + "doc": "Country of the partner, (e.g. DE)" + }, + { + "name": "occurredOn", + "type": "string", + "doc": "Timestamp of when action occurred." + }, + { + "name": "partnerId", + "type": "string", + "doc": "Id of the partner" + }, + { + "name": "platformSource", + "type": "string", + "doc": "Platform source" + } + ], + "example": { + "occurredOn": "2023-11-03T09:56.582+00:00", + "partnerId": "1", + "platformSource": "Brecht", + "countryCode": "DE" + } + } + """; + var model = new AsyncApiStringReader().ReadFragment(input, AsyncApiVersion.AsyncApi2_0, out var diag); + model.Metadata.Should().HaveCount(1); + var reserialized = model.SerializeAsJson(AsyncApiVersion.AsyncApi2_0); + + // Assert + input.Should() + .BePlatformAgnosticEquivalentTo(reserialized); + + } + + [Test] + public void SerializeV2_SerializesCorrectly() + { + // Arrange + var expected = """ + type: record + name: User + namespace: com.example + fields: + - name: username + type: string + doc: The username of the user. + default: guest + order: ascending + - name: status + type: + type: enum + name: Status + symbols: + - ACTIVE + - INACTIVE + - BANNED + doc: The status of the user. + - name: emails + type: + type: array + items: string + doc: A list of email addresses. + - name: metadata + type: + type: map + values: string + doc: Metadata associated with the user. + - name: address + type: + type: record + name: Address + fields: + - name: street + type: string + - name: city + type: string + - name: zipcode + type: string + doc: The address of the user. + - name: profilePicture + type: + type: fixed + name: ProfilePicture + size: 256 + doc: A fixed-size profile picture. + - name: contact + type: + - 'null' + - type: record + name: PhoneNumber + fields: + - name: countryCode + type: int + - name: number + type: string + doc: 'The contact information of the user, which can be either null or a phone number.' + """; + + var schema = new AvroRecord + { + Name = "User", + Namespace = "com.example", + Fields = new List + { + new AvroField + { + Name = "username", + Type = AvroPrimitiveType.String, + Doc = "The username of the user.", + Default = new AsyncApiAny("guest"), + Order = AvroFieldOrder.Ascending, + }, + new AvroField + { + Name = "status", + Type = new AvroEnum + { + Name = "Status", + Symbols = new List { "ACTIVE", "INACTIVE", "BANNED" }, + }, + Doc = "The status of the user.", + }, + new AvroField + { + Name = "emails", + Type = new AvroArray + { + Items = AvroPrimitiveType.String, + }, + Doc = "A list of email addresses.", + }, + new AvroField + { + Name = "metadata", + Type = new AvroMap + { + Values = AvroPrimitiveType.String, + }, + Doc = "Metadata associated with the user.", + }, + new AvroField + { + Name = "address", + Type = new AvroRecord + { + Name = "Address", + Fields = new List + { + new AvroField { Name = "street", Type = AvroPrimitiveType.String }, + new AvroField { Name = "city", Type = AvroPrimitiveType.String }, + new AvroField { Name = "zipcode", Type = AvroPrimitiveType.String }, + }, + }, + Doc = "The address of the user.", + }, + new AvroField + { + Name = "profilePicture", + Type = new AvroFixed + { + Name = "ProfilePicture", + Size = 256, + }, + Doc = "A fixed-size profile picture.", + }, + new AvroField + { + Name = "contact", + Type = new AvroUnion + { + Types = new List + { + AvroPrimitiveType.Null, + new AvroRecord + { + Name = "PhoneNumber", + Fields = new List + { + new AvroField { Name = "countryCode", Type = AvroPrimitiveType.Int }, + new AvroField { Name = "number", Type = AvroPrimitiveType.String }, + }, + }, + }, + }, + Doc = "The contact information of the user, which can be either null or a phone number.", + }, + }, + }; + + // Act + var actual = schema.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + + // Assert + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + } + + [Test] + public void SerializeV2_WithLogicalTypes_SerializesCorrectly() + { + // Arrange + var input = """ + { + "type": "array", + "items": [ + { + "type": "bytes", + "logicalType": "decimal", + "scale": 2, + "precision": 4 + }, + { + "type": "string", + "logicalType": "uuid" + }, + { + "type": "int", + "logicalType": "date" + }, + { + "type": "int", + "logicalType": "time-millis" + }, + { + "type": "long", + "logicalType": "time-micros" + }, + { + "type": "long", + "logicalType": "timestamp-millis" + }, + { + "type": "long", + "logicalType": "timestamp-micros" + }, + { + "type": "fixed", + "logicalType": "duration", + "name": "Duration", + "size": 12 + } + ] + } + """; + + // Act + var model = new AsyncApiStringReader().ReadFragment(input, AsyncApiVersion.AsyncApi2_0, out var diag); + var serialized = model.SerializeAsJson(AsyncApiVersion.AsyncApi2_0); + // Assert + model.As().Items.As().Types.Should().HaveCount(8); + + serialized.Should().BePlatformAgnosticEquivalentTo(input); + } + + [Test] + public void ReadFragment_DeserializesCorrectly() + { + // Arrange + var input = """ + type: record + name: User + namespace: com.example + fields: + - name: username + type: string + doc: The username of the user. + default: guest + order: ascending + - name: status + type: + type: enum + name: Status + symbols: + - ACTIVE + - INACTIVE + - BANNED + doc: The status of the user. + - name: emails + type: + type: array + items: string + doc: A list of email addresses. + - name: metadata + type: + type: map + values: string + doc: Metadata associated with the user. + - name: address + type: + type: record + name: Address + fields: + - name: street + type: string + - name: city + type: string + - name: zipcode + type: string + doc: The address of the user. + - name: profilePicture + type: + type: fixed + name: ProfilePicture + size: 256 + doc: A fixed-size profile picture. + - name: contact + type: + - 'null' + - type: record + name: PhoneNumber + fields: + - name: countryCode + type: int + - name: number + type: string + doc: 'The contact information of the user, which can be either null or a phone number.' + """; + + var expected = new AvroRecord + { + Name = "User", + Namespace = "com.example", + Fields = new List + { + new AvroField + { + Name = "username", + Type = AvroPrimitiveType.String, + Doc = "The username of the user.", + Default = new AsyncApiAny("guest"), + Order = AvroFieldOrder.Ascending, + }, + new AvroField + { + Name = "status", + Type = new AvroEnum + { + Name = "Status", + Symbols = new List { "ACTIVE", "INACTIVE", "BANNED" }, + }, + Doc = "The status of the user.", + }, + new AvroField + { + Name = "emails", + Type = new AvroArray + { + Items = AvroPrimitiveType.String, + }, + Doc = "A list of email addresses.", + }, + new AvroField + { + Name = "metadata", + Type = new AvroMap + { + Values = AvroPrimitiveType.String, + }, + Doc = "Metadata associated with the user.", + }, + new AvroField + { + Name = "address", + Type = new AvroRecord + { + Name = "Address", + Fields = new List + { + new AvroField { Name = "street", Type = AvroPrimitiveType.String }, + new AvroField { Name = "city", Type = AvroPrimitiveType.String }, + new AvroField { Name = "zipcode", Type = AvroPrimitiveType.String }, + }, + }, + Doc = "The address of the user.", + }, + new AvroField + { + Name = "profilePicture", + Type = new AvroFixed + { + Name = "ProfilePicture", + Size = 256, + }, + Doc = "A fixed-size profile picture.", + }, + new AvroField + { + Name = "contact", + Type = new AvroUnion + { + Types = new List + { + AvroPrimitiveType.Null, + new AvroRecord + { + Name = "PhoneNumber", + Fields = new List + { + new AvroField { Name = "countryCode", Type = AvroPrimitiveType.Int }, + new AvroField { Name = "number", Type = AvroPrimitiveType.String }, + }, + }, + }, + }, + Doc = "The contact information of the user, which can be either null or a phone number.", + }, + }, + }; + + // Act + var actual = new AsyncApiStringReader().ReadFragment(input, AsyncApiVersion.AsyncApi2_0, out var diagnostic); + + // Assert + actual.Should() + .BeEquivalentTo(expected); + } + } +} diff --git a/test/LEGO.AsyncAPI.Tests/Serialization/AsyncApiYamlWriterTests.cs b/test/LEGO.AsyncAPI.Tests/Serialization/AsyncApiYamlWriterTests.cs index c364d345..69ae67e3 100644 --- a/test/LEGO.AsyncAPI.Tests/Serialization/AsyncApiYamlWriterTests.cs +++ b/test/LEGO.AsyncAPI.Tests/Serialization/AsyncApiYamlWriterTests.cs @@ -2,10 +2,9 @@ namespace LEGO.AsyncAPI.Tests.Writers { + using System.IO; using LEGO.AsyncAPI.Writers; using NUnit.Framework; - using System; - using System.IO; internal class AsyncApiYamlWriterTests : TestBase { diff --git a/test/LEGO.AsyncAPI.Tests/TestBase.cs b/test/LEGO.AsyncAPI.Tests/TestBase.cs index 17309617..50e313ed 100644 --- a/test/LEGO.AsyncAPI.Tests/TestBase.cs +++ b/test/LEGO.AsyncAPI.Tests/TestBase.cs @@ -43,9 +43,9 @@ public void Log(string message) /// Attempts to find the first file that matches the name of the active unit test /// and returns it as an expected type. /// - /// The type to return + /// The type to return. /// The name of the resource file with an optional extension. - /// The result + /// The result. protected T GetTestData([CallerMemberName] string resourceName = "") { string searchPattern = string.IsNullOrWhiteSpace(Path.GetExtension(resourceName)) diff --git a/test/LEGO.AsyncAPI.Tests/Validation/ValidationRulesetTests.cs b/test/LEGO.AsyncAPI.Tests/Validation/ValidationRulesetTests.cs index 48446738..6dda6f63 100644 --- a/test/LEGO.AsyncAPI.Tests/Validation/ValidationRulesetTests.cs +++ b/test/LEGO.AsyncAPI.Tests/Validation/ValidationRulesetTests.cs @@ -35,7 +35,7 @@ public void DefaultRuleSet_PropertyReturnsTheCorrectRules() Assert.IsNotEmpty(rules); // Update the number if you add new default rule(s). - Assert.AreEqual(17, rules.Count); + Assert.AreEqual(18, rules.Count); } } }