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/Common.Build.props b/Common.Build.props index ba84a0f8..0f367727 100644 --- a/Common.Build.props +++ b/Common.Build.props @@ -2,7 +2,7 @@ 10 - netstandard2.0;net6 + netstandard2.0;net8 disable The LEGO Group https://github.com/LEGO/AsyncAPI.NET diff --git a/README.md b/README.md index ce65fd33..19f27895 100644 --- a/README.md +++ b/README.md @@ -35,39 +35,72 @@ Main classes to know: ### Writing ```csharp -var myFirstAsyncApi = new AsyncApiDocument -{ - Info = new AsyncApiInfo - { - Title = "my first asyncapi" - }, - Channels = new Dictionary - { - { - "users", new AsyncApiChannel - { - Subscribe = new AsyncApiOperation - { - OperationId = "users", - Description = "my users channel" - } - } - } - } -}; -var yaml = myFirstAsyncApi.SerializeAsYaml(); -//asyncapi: '2.5.0' + var myFirstAsyncApi = new AsyncApiDocument + { + Info = new AsyncApiInfo + { + Title = "my first asyncapi", + }, + Channels = new Dictionary + { + { + "users", new AsyncApiChannel + { + Subscribe = new AsyncApiOperation + { + OperationId = "users", + Description = "my users channel", + Message = new List + { + new AsyncApiMessageReference("#/components/messages/MyMessage"), + }, + }, + } + }, + }, + Components = new AsyncApiComponents + { + Messages = new Dictionary + { + { + "MyMessage", new AsyncApiMessage + { + Name = "Hello!", + } + }, + }, + }, + }; + +var yaml = myFirstAsyncApi.SerializeAsYaml(AsyncApi); + +//asyncapi: 2.6.0 // info: // title: my first asyncapi -// channels: -// users: -// subscribe: -// operationId: users -// description: my users channel +//channels: +// users: +// subscribe: +// operationId: users +// description: my users channel +// message: +// $ref: '#/components/messages/MyMessage' +//components: +// messages: +// MyMessage: +// name: Hello! ``` + ### Reading +There are 3 reader types +1. AsyncApiStringReader +2. AsyncApiTextReader +3. AsyncApiStreamReader + +All 3 supports both json and yaml. + +#### StreamReader ```csharp var httpClient = new HttpClient { @@ -78,6 +111,46 @@ var stream = await httpClient.GetStreamAsync("master/examples/streetlights-kafka var asyncApiDocument = new AsyncApiStreamReader().Read(stream, out var diagnostic); ``` +#### StringReader +```csharp +var yaml = + """ + asyncapi: 2.6.0 + info: + title: my first asyncapi + channels: + users: + subscribe: + operationId: users + description: my users channel + message: + $ref: '#/components/messages/MyMessage' + components: + messages: + MyMessage: + name: Hello! + """; + +var asyncApiDocument = new AsyncApiStringReader().Read(yaml, out var diagnostic); +``` +All readers will write warnings and errors to the diagnostics. + + +### Reference Resolution +Internal references are resolved by default. This includes component and non-component references e.g `#/components/messages/MyMessage` and `#/servers/0`. +External references can be resolved by setting `ReferenceResolution` to `ResolveAllReferences`. +The default implementation will resolve anything prefixed with `file://`, `http://` & `https://`, however a custom implementation can be made, by inhereting from the `IStreamLoader` interface and setting the `ExternalReferenceLoader` in the `AsyncApiReaderSettings`. +External references are always force converted to Json during resolution. This means that both yaml and json is supported - but not other serialization languages. + +```csharp +var settings = new AsyncApiReaderSettings { ReferenceResolution = ReferenceResolution.ResolveAllReferences }; +var document = new AsyncApiStringReader(settings).Read(json, out var diagnostics); +``` + + + +Reference resolution can be disabled by setting `ReferenceResolution` to `DoNotResolveReferences`. + ### 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. @@ -85,7 +158,7 @@ There is a nifty helper to add different types of bindings, or like in the examp ```csharp var settings = new AsyncApiReaderSettings(); settings.Bindings = BindingsCollection.All; -var asyncApiDocument = new AsyncApiStringReader(settings).Read(stream, out var diagnostic); +var asyncApiDocument = new AsyncApiStringReader(settings).Read(yaml, out var diagnostic); ``` ## Attribution @@ -99,4 +172,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..b6c7bf78 100644 --- a/src/LEGO.AsyncAPI.Bindings/Http/HttpMessageBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/Http/HttpMessageBinding.cs @@ -16,7 +16,7 @@ public class HttpMessageBinding : MessageBinding /// /// A Schema object containing the definitions for HTTP-specific headers. This schema MUST be of type object and have a properties key. /// - public AsyncApiSchema Headers { get; set; } + public AsyncApiJsonSchema Headers { get; set; } /// /// Serialize to AsyncAPI V2 document without using reference. @@ -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..b7f1215e 100644 --- a/src/LEGO.AsyncAPI.Bindings/Http/HttpOperationBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/Http/HttpOperationBinding.cs @@ -36,7 +36,7 @@ public enum HttpOperationType /// /// A Schema object containing the definitions for each query parameter. This schema MUST be of type object and have a properties key. /// - public AsyncApiSchema Query { get; set; } + public AsyncApiJsonSchema Query { get; set; } /// /// Serialize to AsyncAPI V2 document without using reference. @@ -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..e63c95e1 100644 --- a/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaMessageBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaMessageBinding.cs @@ -16,7 +16,7 @@ public class KafkaMessageBinding : MessageBinding /// /// The message key. NOTE: You can also use the reference object way. /// - public AsyncApiSchema Key { get; set; } + public AsyncApiJsonSchema Key { get; set; } /// /// If a Schema Registry is used when performing this operation, tells where the id of schema is stored (e.g. header or payload). @@ -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..358a929b 100644 --- a/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaOperationBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaOperationBinding.cs @@ -16,20 +16,20 @@ public class KafkaOperationBinding : OperationBinding /// /// Id of the consumer group. /// - public AsyncApiSchema GroupId { get; set; } + public AsyncApiJsonSchema GroupId { get; set; } /// /// Id of the consumer inside a consumer group. /// - public AsyncApiSchema ClientId { get; set; } + public AsyncApiJsonSchema ClientId { get; set; } public override string BindingKey => "kafka"; 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/LEGO.AsyncAPI.Bindings.csproj b/src/LEGO.AsyncAPI.Bindings/LEGO.AsyncAPI.Bindings.csproj index 9c0b025e..2c678678 100644 --- a/src/LEGO.AsyncAPI.Bindings/LEGO.AsyncAPI.Bindings.csproj +++ b/src/LEGO.AsyncAPI.Bindings/LEGO.AsyncAPI.Bindings.csproj @@ -19,6 +19,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + 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..338230ac 100644 --- a/src/LEGO.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs @@ -22,7 +22,7 @@ public class MQTTMessageBinding : MessageBinding /// /// Correlation Data is used to identify the request the response message is for. /// - public AsyncApiSchema CorrelationData { get; set; } + public AsyncApiJsonSchema CorrelationData { get; set; } /// /// String describing the content type of the message payload. @@ -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/Condition.cs b/src/LEGO.AsyncAPI.Bindings/Sns/Condition.cs index 38b21da9..fca3b30c 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sns/Condition.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sns/Condition.cs @@ -39,25 +39,25 @@ public static Condition Parse(ParseNode node) switch (node) { case MapNode mapNode: - { - var conditionValues = new Dictionary>(); - foreach (var conditionNode in mapNode) { - switch (conditionNode.Value) + var conditionValues = new Dictionary>(); + foreach (var conditionNode in mapNode) { - case MapNode conditionValueNode: - conditionValues.Add(conditionNode.Name, new Dictionary(conditionValueNode.Select(x => - new KeyValuePair(x.Name, StringOrStringList.Parse(x.Value))) - .ToDictionary(x => x.Key, x => x.Value))); - break; - default: - throw new ArgumentException($"An error occured while parsing a {nameof(Condition)} node. " + - $"AWS condition values should be one or more key value pairs."); + switch (conditionNode.Value) + { + case MapNode conditionValueNode: + conditionValues.Add(conditionNode.Name, new Dictionary(conditionValueNode.Select(x => + new KeyValuePair(x.Name, StringOrStringList.Parse(x.Value))) + .ToDictionary(x => x.Key, x => x.Value))); + break; + default: + throw new ArgumentException($"An error occured while parsing a {nameof(Condition)} node. " + + $"AWS condition values should be one or more key value pairs."); + } } - } - return new Condition(conditionValues); - } + return new Condition(conditionValues); + } default: throw new ArgumentException($"An error occured while parsing a {nameof(Condition)} node. " + 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/Sns/Principal.cs b/src/LEGO.AsyncAPI.Bindings/Sns/Principal.cs index a803f6c2..2d630641 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sns/Principal.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sns/Principal.cs @@ -1,3 +1,5 @@ +// Copyright (c) The LEGO Group. All rights reserved. + namespace LEGO.AsyncAPI.Bindings.Sns; using System; @@ -28,20 +30,20 @@ public static Principal Parse(ParseNode node) return new PrincipalStar(); case MapNode mapNode: - { - var propertyNode = mapNode.First(); - if (!IsValidPrincipalProperty(propertyNode.Name)) { - throw new ArgumentException($"An error occured while parsing a {nameof(Principal)} node. " + - $"Node should contain a valid AWS principal property name."); - } + var propertyNode = mapNode.First(); + if (!IsValidPrincipalProperty(propertyNode.Name)) + { + throw new ArgumentException($"An error occured while parsing a {nameof(Principal)} node. " + + $"Node should contain a valid AWS principal property name."); + } - var principalValue = new KeyValuePair( - propertyNode.Name, - StringOrStringList.Parse(propertyNode.Value)); + var principalValue = new KeyValuePair( + propertyNode.Name, + StringOrStringList.Parse(propertyNode.Value)); - return new PrincipalObject(principalValue); - } + return new PrincipalObject(principalValue); + } default: throw new ArgumentException($"An error occured while parsing a {nameof(Principal)} node. " + diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/PrincipalObject.cs b/src/LEGO.AsyncAPI.Bindings/Sns/PrincipalObject.cs index 209be8bf..946fa4d6 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sns/PrincipalObject.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sns/PrincipalObject.cs @@ -1,3 +1,5 @@ +// Copyright (c) The LEGO Group. All rights reserved. + namespace LEGO.AsyncAPI.Bindings.Sns; using System; diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/PrincipalStar.cs b/src/LEGO.AsyncAPI.Bindings/Sns/PrincipalStar.cs index c885d252..bd0e37bf 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sns/PrincipalStar.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sns/PrincipalStar.cs @@ -1,3 +1,5 @@ +// Copyright (c) The LEGO Group. All rights reserved. + namespace LEGO.AsyncAPI.Bindings.Sns; using System; diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/SnsOperationBinding.cs b/src/LEGO.AsyncAPI.Bindings/Sns/SnsOperationBinding.cs index 350b7a59..6f2dbba9 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sns/SnsOperationBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sns/SnsOperationBinding.cs @@ -59,7 +59,7 @@ public class SnsOperationBinding : OperationBinding private FixedFieldMap redrivePolicyFixedFields => new() { - { "deadLetterQueue", (a, n) => { a.DeadLetterQueue = n.ParseMapWithExtensions(identifierFixFields); } }, + { "deadLetterQueue", (a, n) => { a.DeadLetterQueue = n.ParseMapWithExtensions(this.identifierFixFields); } }, { "maxReceiveCount", (a, n) => { a.MaxReceiveCount = n.GetIntegerValue(); } }, }; diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/Statement.cs b/src/LEGO.AsyncAPI.Bindings/Sns/Statement.cs index da93cfbf..5a7a248c 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sns/Statement.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sns/Statement.cs @@ -4,7 +4,6 @@ namespace LEGO.AsyncAPI.Bindings.Sns using System; using System.Collections.Generic; using LEGO.AsyncAPI.Attributes; - using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Writers; diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/Condition.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/Condition.cs index 93bdf733..9172f3e3 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sqs/Condition.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/Condition.cs @@ -39,25 +39,25 @@ public static Condition Parse(ParseNode node) switch (node) { case MapNode mapNode: - { - var conditionValues = new Dictionary>(); - foreach (var conditionNode in mapNode) { - switch (conditionNode.Value) + var conditionValues = new Dictionary>(); + foreach (var conditionNode in mapNode) { - case MapNode conditionValueNode: - conditionValues.Add(conditionNode.Name, new Dictionary(conditionValueNode.Select(x => - new KeyValuePair(x.Name, StringOrStringList.Parse(x.Value))) - .ToDictionary(x => x.Key, x => x.Value))); - break; - default: - throw new ArgumentException($"An error occured while parsing a {nameof(Condition)} node. " + - $"AWS condition values should be one or more key value pairs."); + switch (conditionNode.Value) + { + case MapNode conditionValueNode: + conditionValues.Add(conditionNode.Name, new Dictionary(conditionValueNode.Select(x => + new KeyValuePair(x.Name, StringOrStringList.Parse(x.Value))) + .ToDictionary(x => x.Key, x => x.Value))); + break; + default: + throw new ArgumentException($"An error occured while parsing a {nameof(Condition)} node. " + + $"AWS condition values should be one or more key value pairs."); + } } - } - return new Condition(conditionValues); - } + return new Condition(conditionValues); + } default: throw new ArgumentException($"An error occured while parsing a {nameof(Condition)} node. " + diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/Principal.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/Principal.cs index 2821f952..eee2b4fe 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sqs/Principal.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/Principal.cs @@ -30,20 +30,20 @@ public static Principal Parse(ParseNode node) return new PrincipalStar(); case MapNode mapNode: - { - var propertyNode = mapNode.First(); - if (!IsValidPrincipalProperty(propertyNode.Name)) { - throw new ArgumentException($"An error occured while parsing a {nameof(Principal)} node. " + - $"Node should contain a valid AWS principal property name."); - } + var propertyNode = mapNode.First(); + if (!IsValidPrincipalProperty(propertyNode.Name)) + { + throw new ArgumentException($"An error occured while parsing a {nameof(Principal)} node. " + + $"Node should contain a valid AWS principal property name."); + } - var principalValue = new KeyValuePair( - propertyNode.Name, - StringOrStringList.Parse(propertyNode.Value)); + var principalValue = new KeyValuePair( + propertyNode.Name, + StringOrStringList.Parse(propertyNode.Value)); - return new PrincipalObject(principalValue); - } + return new PrincipalObject(principalValue); + } default: throw new ArgumentException($"An error occured while parsing a {nameof(Principal)} node. " + diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/PrincipalObject.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/PrincipalObject.cs index 61e6e546..a1613247 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sqs/PrincipalObject.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/PrincipalObject.cs @@ -1,3 +1,5 @@ +// Copyright (c) The LEGO Group. All rights reserved. + namespace LEGO.AsyncAPI.Bindings.Sqs; using System; diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/PrincipalStar.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/PrincipalStar.cs index 1705b966..a49b02c6 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sqs/PrincipalStar.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/PrincipalStar.cs @@ -1,3 +1,5 @@ +// Copyright (c) The LEGO Group. All rights reserved. + namespace LEGO.AsyncAPI.Bindings.Sqs; using System; 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/Sqs/Statement.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/Statement.cs index 4a9c5303..0c2873d2 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sqs/Statement.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/Statement.cs @@ -5,7 +5,6 @@ namespace LEGO.AsyncAPI.Bindings.Sqs using System; using System.Collections.Generic; using LEGO.AsyncAPI.Attributes; - using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Writers; diff --git a/src/LEGO.AsyncAPI.Bindings/WebSockets/WebSocketsChannelBinding.cs b/src/LEGO.AsyncAPI.Bindings/WebSockets/WebSocketsChannelBinding.cs index c87393bb..4321c879 100644 --- a/src/LEGO.AsyncAPI.Bindings/WebSockets/WebSocketsChannelBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/WebSockets/WebSocketsChannelBinding.cs @@ -18,12 +18,12 @@ public class WebSocketsChannelBinding : ChannelBinding /// /// A Schema object containing the definitions for each query parameter. This schema MUST be of type 'object' and have a 'properties' key. /// - public AsyncApiSchema Query { get; set; } + public AsyncApiJsonSchema Query { get; set; } /// /// A Schema object containing the definitions of the HTTP headers to use when establishing the connection. This schma MUST be of type 'object' and have a 'properties' key. /// - public AsyncApiSchema Headers { get; set; } + public AsyncApiJsonSchema Headers { get; set; } public override string BindingKey => "websockets"; @@ -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/AsyncApiDiagnostics.cs b/src/LEGO.AsyncAPI.Readers/AsyncApiDiagnostics.cs index faceb39e..f1412e25 100644 --- a/src/LEGO.AsyncAPI.Readers/AsyncApiDiagnostics.cs +++ b/src/LEGO.AsyncAPI.Readers/AsyncApiDiagnostics.cs @@ -13,5 +13,18 @@ public class AsyncApiDiagnostic : IDiagnostic public IList Warnings { get; set; } = new List(); public AsyncApiVersion SpecificationVersion { get; set; } + + public void Append(AsyncApiDiagnostic diagnosticToAdd) + { + foreach (var error in diagnosticToAdd.Errors) + { + this.Errors.Add(new(error.Pointer, error.Message)); + } + + foreach (var warning in diagnosticToAdd.Warnings) + { + this.Warnings.Add(new(warning.Pointer, warning.Message)); + } + } } } diff --git a/src/LEGO.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs b/src/LEGO.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs index 3a973654..4512fb44 100644 --- a/src/LEGO.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs +++ b/src/LEGO.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs @@ -2,17 +2,22 @@ namespace LEGO.AsyncAPI.Readers { - using System.Collections.Generic; + using System; + using System.IO; using System.Linq; using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; + using Json.Pointer; using LEGO.AsyncAPI.Exceptions; using LEGO.AsyncAPI.Extensions; using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Readers.Interface; + using LEGO.AsyncAPI.Readers.Services; + using LEGO.AsyncAPI.Services; using LEGO.AsyncAPI.Validations; + using YamlDotNet.RepresentationModel; /// /// Service class for converting contents of TextReader into AsyncApiDocument instances. @@ -20,6 +25,7 @@ namespace LEGO.AsyncAPI.Readers internal class AsyncApiJsonDocumentReader : IAsyncApiReader { private readonly AsyncApiReaderSettings settings; + private ParsingContext context; /// /// Initializes a new instance of the class. @@ -39,7 +45,7 @@ public AsyncApiJsonDocumentReader(AsyncApiReaderSettings settings = null) public AsyncApiDocument Read(JsonNode input, out AsyncApiDiagnostic diagnostic) { diagnostic = new AsyncApiDiagnostic(); - var context = new ParsingContext(diagnostic, this.settings) + this.context ??= new ParsingContext(diagnostic, this.settings) { ExtensionParsers = this.settings.ExtensionParsers, ServerBindingParsers = this.settings.Bindings.OfType>().ToDictionary(b => b.BindingKey, b => b), @@ -51,8 +57,7 @@ public AsyncApiDocument Read(JsonNode input, out AsyncApiDiagnostic diagnostic) AsyncApiDocument document = null; try { - document = context.Parse(input); - + document = this.context.Parse(input); this.ResolveReferences(diagnostic, document); } catch (AsyncApiException ex) @@ -80,17 +85,21 @@ public AsyncApiDocument Read(JsonNode input, out AsyncApiDiagnostic diagnostic) public async Task ReadAsync(JsonNode input, CancellationToken cancellationToken = default) { var diagnostic = new AsyncApiDiagnostic(); - var context = new ParsingContext(diagnostic, this.settings) + this.context ??= new ParsingContext(diagnostic, this.settings) { ExtensionParsers = this.settings.ExtensionParsers, + ServerBindingParsers = this.settings.Bindings.OfType>().ToDictionary(b => b.BindingKey, b => b), + ChannelBindingParsers = this.settings.Bindings.OfType>().ToDictionary(b => b.BindingKey, b => b), + OperationBindingParsers = this.settings.Bindings.OfType>().ToDictionary(b => b.BindingKey, b => b), + MessageBindingParsers = this.settings.Bindings.OfType>().ToDictionary(b => b.BindingKey, b => b), }; AsyncApiDocument document = null; try { // Parse the AsyncApi Document - document = context.Parse(input); - this.ResolveReferences(diagnostic, document); + document = this.context.Parse(input); + await this.ResolveReferencesAsync(diagnostic, document); } catch (AsyncApiException ex) { @@ -130,7 +139,7 @@ public T ReadFragment(JsonNode input, AsyncApiVersion version, out AsyncApiDi where T : IAsyncApiElement { diagnostic = new AsyncApiDiagnostic(); - var context = new ParsingContext(diagnostic, this.settings) + this.context ??= new ParsingContext(diagnostic, this.settings) { ExtensionParsers = this.settings.ExtensionParsers, ServerBindingParsers = this.settings.Bindings.OfType>().ToDictionary(b => b.BindingKey, b => b), @@ -143,7 +152,7 @@ public T ReadFragment(JsonNode input, AsyncApiVersion version, out AsyncApiDi try { // Parse the AsyncApi element - element = context.ParseFragment(input, version); + element = this.context.ParseFragment(input, version); } catch (AsyncApiException ex) { @@ -163,25 +172,256 @@ public T ReadFragment(JsonNode input, AsyncApiVersion version, out AsyncApiDi return (T)element; } - private void ResolveReferences(AsyncApiDiagnostic diagnostic, AsyncApiDocument document) + private async Task ResolveReferencesAsync(AsyncApiDiagnostic diagnostic, IAsyncApiSerializable serializable) { - var errors = new List(); + if (this.settings.ReferenceResolution == ReferenceResolutionSetting.DoNotResolveReferences) + { + return; + } - // Resolve References if requested - switch (this.settings.ReferenceResolution) + var collector = new AsyncApiReferenceCollector(this.context.Workspace); + var walker = new AsyncApiWalker(collector); + walker.Walk(serializable); + + foreach (var reference in collector.References) { - case ReferenceResolutionSetting.ResolveReferences: - errors.AddRange(document.ResolveReferences()); - break; + if (this.context.Workspace.Contains(reference.Reference.Reference)) + { + continue; + } - case ReferenceResolutionSetting.DoNotResolveReferences: - break; + IAsyncApiSerializable component = null; + if (reference.Reference.IsExternal) + { + if (this.settings.ReferenceResolution != ReferenceResolutionSetting.ResolveAllReferences) + { + continue; + } + + component = await this.ResolveExternalReferenceAsync(diagnostic, reference); + } + else + { + var stream = this.context.Workspace.ResolveReference(string.Empty); // get whole document. + component = this.ResolveStreamReference(stream, reference, diagnostic); + } + + if (component == null) + { + diagnostic.Errors.Add(new AsyncApiError(string.Empty, $"Unable to deserialize reference '{reference.Reference.Reference}'")); + continue; + } + + this.context.Workspace.RegisterComponent(reference.Reference.Reference, component); + await this.ResolveReferencesAsync(diagnostic, component); + } + } + + private void ResolveReferences(AsyncApiDiagnostic diagnostic, IAsyncApiSerializable serializable) + { + if (this.settings.ReferenceResolution == ReferenceResolutionSetting.DoNotResolveReferences) + { + return; + } + + var collector = new AsyncApiReferenceCollector(this.context.Workspace); + var walker = new AsyncApiWalker(collector); + walker.Walk(serializable); + + foreach (var reference in collector.References) + { + if (this.context.Workspace.Contains(reference.Reference.Reference)) + { + continue; + } + + IAsyncApiSerializable component = null; + if (reference.Reference.IsExternal) + { + if (this.settings.ReferenceResolution != ReferenceResolutionSetting.ResolveAllReferences) + { + continue; + } + + component = this.ResolveExternalReference(diagnostic, reference); + } + else + { + var stream = this.context.Workspace.ResolveReference(string.Empty); // get whole document. + component = this.ResolveStreamReference(stream, reference, diagnostic); + } + + if (component == null) + { + diagnostic.Errors.Add(new AsyncApiError(string.Empty, $"Unable to deserialize reference '{reference.Reference.Reference}'")); + continue; + } + + this.context.Workspace.RegisterComponent(reference.Reference.Reference, component); + this.ResolveReferences(diagnostic, component); + } + } + + private IAsyncApiSerializable ResolveExternalReference(AsyncApiDiagnostic diagnostic, IAsyncApiReferenceable reference) + { + if (reference is null) + { + throw new ArgumentNullException(nameof(reference)); + } + + var loader = this.settings.ExternalReferenceLoader ??= new DefaultStreamLoader(); + try + { + Stream stream; + if (this.context.Workspace.Contains(reference.Reference.ExternalResource)) + { + stream = this.context.Workspace.ResolveReference(reference.Reference.ExternalResource); + } + else + { + stream = loader.Load(new Uri(reference.Reference.ExternalResource, UriKind.RelativeOrAbsolute)); + this.context.Workspace.RegisterComponent(reference.Reference.ExternalResource, stream); + } + + return this.ResolveStreamReference(stream, reference, diagnostic); + } + catch (AsyncApiException ex) + { + diagnostic.Errors.Add(new AsyncApiError(ex)); + return null; + } + } + + private async Task ResolveExternalReferenceAsync(AsyncApiDiagnostic diagnostic, IAsyncApiReferenceable reference) + { + if (reference is null) + { + throw new ArgumentNullException(nameof(reference)); + } + + var loader = this.settings.ExternalReferenceLoader ??= new DefaultStreamLoader(); + try + { + Stream stream; + if (this.context.Workspace.Contains(reference.Reference.ExternalResource)) + { + stream = this.context.Workspace.ResolveReference(reference.Reference.ExternalResource); + } + else + { + stream = await loader.LoadAsync(new Uri(reference.Reference.ExternalResource, UriKind.RelativeOrAbsolute)); + this.context.Workspace.RegisterComponent(reference.Reference.ExternalResource, stream); + } + + return this.ResolveStreamReference(stream, reference, diagnostic); + } + catch (AsyncApiException ex) + { + diagnostic.Errors.Add(new AsyncApiError(ex)); + return null; } + } - foreach (var item in errors) + private JsonNode ReadToJson(Stream stream) + { + if (stream != null) { - diagnostic.Errors.Add(item); + var reader = new StreamReader(stream); + var yamlStream = new YamlStream(); + yamlStream.Load(reader); + return yamlStream.Documents.First().ToJsonNode(this.settings.CultureInfo); } + + return default; + } + + private IAsyncApiSerializable ResolveStreamReference(Stream stream, IAsyncApiReferenceable reference, AsyncApiDiagnostic diagnostic) + { + JsonNode json = null; + try + { + json = this.ReadToJson(stream); + } + catch + { + diagnostic.Errors.Add(new AsyncApiError(string.Empty, $"Unable to deserialize reference: '{reference.Reference.Reference}'")); + return null; + } + + if (reference.Reference.IsFragment) + { + var pointer = JsonPointer.Parse(reference.Reference.FragmentId); + if (pointer.TryEvaluate(json, out var pointerResult)) + { + json = pointerResult; + } + else + { + diagnostic.Errors.Add(new AsyncApiError(reference.Reference.Reference, "Could not resolve reference fragment.")); + return null; + } + } + + AsyncApiDiagnostic fragmentDiagnostic = new AsyncApiDiagnostic(); + IAsyncApiSerializable result = null; + switch (reference.Reference.Type) + { + case ReferenceType.Schema: + if (reference is AsyncApiJsonSchemaReference) + { + result = this.ReadFragment(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + } + + if (reference is AsyncApiAvroSchemaReference) + { + result = this.ReadFragment(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + } + + break; + + case ReferenceType.Server: + result = this.ReadFragment(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + break; + case ReferenceType.Channel: + result = this.ReadFragment(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + break; + case ReferenceType.Message: + result = this.ReadFragment(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + break; + case ReferenceType.SecurityScheme: + result = this.ReadFragment(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + break; + case ReferenceType.Parameter: + result = this.ReadFragment(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + break; + case ReferenceType.CorrelationId: + result = this.ReadFragment(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + break; + case ReferenceType.OperationTrait: + result = this.ReadFragment(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + break; + case ReferenceType.MessageTrait: + result = this.ReadFragment(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + break; + case ReferenceType.ServerBindings: + result = this.ReadFragment>(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + break; + case ReferenceType.ChannelBindings: + result = this.ReadFragment>(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + break; + case ReferenceType.OperationBindings: + result = this.ReadFragment>(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + break; + case ReferenceType.MessageBindings: + result = this.ReadFragment>(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + break; + default: + diagnostic.Errors.Add(new AsyncApiError(reference.Reference.Reference, "Could not resolve reference.")); + break; + } + + diagnostic.Append(fragmentDiagnostic); + return result; } } -} \ No newline at end of file +} diff --git a/src/LEGO.AsyncAPI.Readers/AsyncApiReaderSettings.cs b/src/LEGO.AsyncAPI.Readers/AsyncApiReaderSettings.cs index 6ca2e129..890b83e6 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,15 @@ public IEnumerable> /// from an object. /// public bool LeaveStreamOpen { get; set; } + + /// + /// External reference reader implementation provided by users for reading external resources. + /// + public IStreamLoader ExternalReferenceLoader { get; set; } = null; + + /// + /// URL where relative references should be resolved from if. + /// + public Uri BaseUrl { get; set; } } } \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Readers/AsyncApiReferenceHostDocumentResolver.cs b/src/LEGO.AsyncAPI.Readers/AsyncApiReferenceHostDocumentResolver.cs new file mode 100644 index 00000000..362b7011 --- /dev/null +++ b/src/LEGO.AsyncAPI.Readers/AsyncApiReferenceHostDocumentResolver.cs @@ -0,0 +1,28 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Readers +{ + using System.Collections.Generic; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Services; + + internal class AsyncApiReferenceWorkspaceResolver : AsyncApiVisitorBase + { + private AsyncApiWorkspace workspace; + + public AsyncApiReferenceWorkspaceResolver( + AsyncApiWorkspace workspace) + { + this.workspace = workspace; + } + + public override void Visit(IAsyncApiReferenceable referenceable) + { + if (referenceable.Reference != null) + { + referenceable.Reference.Workspace = this.workspace; + } + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Readers/AsyncApiTextReader.cs b/src/LEGO.AsyncAPI.Readers/AsyncApiTextReader.cs index eeece6a7..78b6973a 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; @@ -123,7 +122,7 @@ static JsonNode LoadYamlDocument(TextReader input, AsyncApiReaderSettings settin { var yamlStream = new YamlStream(); yamlStream.Load(input); - return yamlStream.Documents.First().ToJsonNode(settings); + return yamlStream.Documents.First().ToJsonNode(settings.CultureInfo); } } } 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/LEGO.AsyncAPI.Readers.csproj b/src/LEGO.AsyncAPI.Readers/LEGO.AsyncAPI.Readers.csproj index 53071ca0..90a0d82f 100644 --- a/src/LEGO.AsyncAPI.Readers/LEGO.AsyncAPI.Readers.csproj +++ b/src/LEGO.AsyncAPI.Readers/LEGO.AsyncAPI.Readers.csproj @@ -6,6 +6,7 @@ AsyncAPI.NET.Readers LEGO.AsyncAPI.Readers LEGO.AsyncAPI.Readers + netstandard2.0;net8 @@ -17,10 +18,12 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/LEGO.AsyncAPI.Readers/ParseNodes/AnyFieldMapParameter.cs b/src/LEGO.AsyncAPI.Readers/ParseNodes/AnyFieldMapParameter.cs index 76e9008a..98ded2f6 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/AnyFieldMapParameter.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/AnyFieldMapParameter.cs @@ -10,7 +10,7 @@ internal class AnyFieldMapParameter public AnyFieldMapParameter( Func propertyGetter, Action propertySetter, - Func schemaGetter) + Func schemaGetter) { this.PropertyGetter = propertyGetter; this.PropertySetter = propertySetter; @@ -21,6 +21,6 @@ public AnyFieldMapParameter( public Action PropertySetter { get; } - public Func SchemaGetter { get; } + public Func SchemaGetter { get; } } } \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Readers/ParseNodes/AnyListFieldMapParameter{T}.cs b/src/LEGO.AsyncAPI.Readers/ParseNodes/AnyListFieldMapParameter{T}.cs index ee1af993..15aafb4f 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/AnyListFieldMapParameter{T}.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/AnyListFieldMapParameter{T}.cs @@ -11,7 +11,7 @@ internal class AnyListFieldMapParameter public AnyListFieldMapParameter( Func> propertyGetter, Action> propertySetter, - Func schemaGetter) + Func schemaGetter) { this.PropertyGetter = propertyGetter; this.PropertySetter = propertySetter; @@ -22,6 +22,6 @@ public AnyListFieldMapParameter( public Action> PropertySetter { get; } - public Func SchemaGetter { get; } + public Func SchemaGetter { get; } } } \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Readers/ParseNodes/AnyMapFieldMapParameter{T,U}.cs b/src/LEGO.AsyncAPI.Readers/ParseNodes/AnyMapFieldMapParameter{T,U}.cs index 2399fe31..9b36f6fa 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/AnyMapFieldMapParameter{T,U}.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/AnyMapFieldMapParameter{T,U}.cs @@ -12,7 +12,7 @@ public AnyMapFieldMapParameter( Func> propertyMapGetter, Func propertyGetter, Action propertySetter, - Func schemaGetter) + Func schemaGetter) { this.PropertyMapGetter = propertyMapGetter; this.PropertyGetter = propertyGetter; @@ -26,6 +26,6 @@ public AnyMapFieldMapParameter( public Action PropertySetter { get; } - public Func SchemaGetter { get; } + public Func SchemaGetter { get; } } } \ 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..8f392f35 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/MapNode.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/MapNode.cs @@ -116,11 +116,7 @@ public override Dictionary CreateMapWithReference( if (entry.value.Reference == null) { - entry.value.Reference = new AsyncApiReference() - { - Type = referenceType, - Id = entry.key, - }; + entry.value.Reference = new AsyncApiReference(entry.key, referenceType); } } finally @@ -185,7 +181,6 @@ public T GetReferencedObject(ReferenceType referenceType, string referenceId) { return new T() { - UnresolvedReference = true, Reference = this.Context.VersionService.ConvertToAsyncApiReference(referenceId, referenceType), }; } @@ -214,6 +209,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..246018b4 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/ValueNode.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/ValueNode.cs @@ -2,16 +2,17 @@ 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 { private readonly JsonNode node; private string cachedScalarValue; + public ValueNode(ParsingContext context, JsonNode node) : base( context) diff --git a/src/LEGO.AsyncAPI.Readers/ParsingContext.cs b/src/LEGO.AsyncAPI.Readers/ParsingContext.cs index 9bcc051b..5fb0c2b3 100644 --- a/src/LEGO.AsyncAPI.Readers/ParsingContext.cs +++ b/src/LEGO.AsyncAPI.Readers/ParsingContext.cs @@ -4,7 +4,9 @@ namespace LEGO.AsyncAPI.Readers { using System; using System.Collections.Generic; + using System.IO; using System.Linq; + using System.Text.Json; using System.Text.Json.Nodes; using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Models.Interfaces; @@ -44,6 +46,8 @@ internal Dictionary> ExtensionPars /// public AsyncApiReaderSettings Settings { get; } + public AsyncApiWorkspace Workspace { get; } + ///// ///// Initializes a new instance of the class. ///// @@ -63,6 +67,7 @@ public ParsingContext(AsyncApiDiagnostic diagnostic, AsyncApiReaderSettings sett { this.Diagnostic = diagnostic; this.Settings = settings; + this.Workspace = new AsyncApiWorkspace(); } internal AsyncApiDocument Parse(JsonNode jsonNode) @@ -78,6 +83,10 @@ internal AsyncApiDocument Parse(JsonNode jsonNode) case string version when version.StartsWith("2"): this.VersionService = new AsyncApiV2VersionService(this.Diagnostic); doc = this.VersionService.LoadDocument(this.RootNode); + + // Register components + this.Workspace.RegisterComponents(doc); // pre-register components. + this.Workspace.RegisterComponent(string.Empty, this.ParseToStream(jsonNode)); // register root document. this.Diagnostic.SpecificationVersion = AsyncApiVersion.AsyncApi2_0; break; @@ -88,6 +97,18 @@ internal AsyncApiDocument Parse(JsonNode jsonNode) return doc; } + private Stream ParseToStream(JsonNode node) + { + var stream = new MemoryStream(); + using (var writer = new Utf8JsonWriter(stream)) + { + node.WriteTo(writer); + } + + stream.Position = 0; + return stream; + } + internal T ParseFragment(JsonNode jsonNode, AsyncApiVersion version) where T : IAsyncApiElement { diff --git a/src/LEGO.AsyncAPI.Readers/Services/AsyncApiRemoteReferenceCollector.cs b/src/LEGO.AsyncAPI.Readers/Services/AsyncApiRemoteReferenceCollector.cs new file mode 100644 index 00000000..4094fc6a --- /dev/null +++ b/src/LEGO.AsyncAPI.Readers/Services/AsyncApiRemoteReferenceCollector.cs @@ -0,0 +1,47 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Readers.Services +{ + using System.Collections.Generic; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Services; + + internal class AsyncApiReferenceCollector : AsyncApiVisitorBase + { + private readonly List references = new(); + private AsyncApiWorkspace workspace; + + public AsyncApiReferenceCollector( + AsyncApiWorkspace workspace) + { + this.workspace = workspace; + } + /// + /// List of all external references collected from AsyncApiDocument. + /// + public IEnumerable References + { + get + { + return this.references; + } + } + + /// + /// Collect reference for each reference. + /// + /// + public override void Visit(IAsyncApiReferenceable referenceable) + { + if (referenceable.Reference != null) + { + if (referenceable.Reference.Workspace == null) + { + referenceable.Reference.Workspace = this.workspace; + } + + this.references.Add(referenceable); + } + } + } +} diff --git a/src/LEGO.AsyncAPI.Readers/Services/DefaultStreamLoader.cs b/src/LEGO.AsyncAPI.Readers/Services/DefaultStreamLoader.cs index 6e506730..3d949a23 100644 --- a/src/LEGO.AsyncAPI.Readers/Services/DefaultStreamLoader.cs +++ b/src/LEGO.AsyncAPI.Readers/Services/DefaultStreamLoader.cs @@ -1,4 +1,4 @@ -// Copyright (c) The LEGO Group. All rights reserved. +// Copyright (c) The LEGO Group. All rights reserved. namespace LEGO.AsyncAPI.Readers.Services { @@ -6,46 +6,54 @@ namespace LEGO.AsyncAPI.Readers.Services using System.IO; using System.Net.Http; using System.Threading.Tasks; + using LEGO.AsyncAPI.Readers.Exceptions; using LEGO.AsyncAPI.Readers.Interface; internal class DefaultStreamLoader : IStreamLoader { - private readonly Uri baseUrl; - private HttpClient httpClient = new HttpClient(); - - public DefaultStreamLoader(Uri baseUrl) - { - this.baseUrl = baseUrl; - } + private static readonly HttpClient HttpClient = new HttpClient(); public Stream Load(Uri uri) { - var absoluteUri = new Uri(this.baseUrl, uri); - switch (uri.Scheme) + try + { + switch (uri.Scheme) + { + case "file": + return File.OpenRead(uri.AbsolutePath); + case "http": + case "https": + return HttpClient.GetStreamAsync(uri).GetAwaiter().GetResult(); + default: + throw new ArgumentException("Unsupported scheme"); + } + } + catch (Exception ex) { - case "file": - return File.OpenRead(absoluteUri.AbsolutePath); - case "http": - case "https": - return this.httpClient.GetStreamAsync(absoluteUri).GetAwaiter().GetResult(); - default: - throw new ArgumentException("Unsupported scheme"); + + throw new AsyncApiReaderException($"Something went wrong trying to fetch '{uri.OriginalString}. {ex.Message}'", ex); } } public async Task LoadAsync(Uri uri) { - var absoluteUri = new Uri(this.baseUrl, uri); - - switch (absoluteUri.Scheme) + try { - case "file": - return File.OpenRead(absoluteUri.AbsolutePath); - case "http": - case "https": - return await this.httpClient.GetStreamAsync(absoluteUri); - default: - throw new ArgumentException("Unsupported scheme"); + switch (uri.Scheme) + { + case "file": + return File.OpenRead(uri.AbsolutePath); + case "http": + case "https": + return await HttpClient.GetStreamAsync(uri); + default: + throw new ArgumentException("Unsupported scheme"); + } + } + catch (Exception ex) + { + + throw new AsyncApiReaderException($"Something went wrong trying to fetch '{uri.OriginalString}'. {ex.Message}", ex); } } } 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..348005e0 --- /dev/null +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiAvroSchemaDeserializer.cs @@ -0,0 +1,336 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Readers +{ + 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 AsyncApiAvroSchema 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 AsyncApiAvroSchemaReference(pointer); + } + + 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 AsyncApiAvroSchema 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/AsyncApiChannelBindingDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiChannelBindingDeserializer.cs index 1aab9e85..aa91b942 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiChannelBindingDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiChannelBindingDeserializer.cs @@ -15,7 +15,7 @@ internal static AsyncApiBindings LoadChannelBindings(ParseNode var pointer = mapNode.GetReferencePointer(); if (pointer != null) { - return mapNode.GetReferencedObject>(ReferenceType.ChannelBindings, pointer); + return new AsyncApiBindingsReference(pointer); } var channelBindings = new AsyncApiBindings(); diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiChannelDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiChannelDeserializer.cs index 1d6acae6..7b9e3e63 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiChannelDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiChannelDeserializer.cs @@ -30,7 +30,7 @@ public static AsyncApiChannel LoadChannel(ParseNode node) var pointer = mapNode.GetReferencePointer(); if (pointer != null) { - return mapNode.GetReferencedObject(ReferenceType.Channel, pointer); + return new AsyncApiChannelReference(pointer); } var pathItem = new AsyncApiChannel(); diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiComponentsDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiComponentsDeserializer.cs index 3b63db28..75c50873 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiComponentsDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiComponentsDeserializer.cs @@ -10,19 +10,19 @@ internal static partial class AsyncApiV2Deserializer { private static FixedFieldMap componentsFixedFields = new() { - { "schemas", (a, n) => a.Schemas = n.CreateMapWithReference(ReferenceType.Schema, JsonSchemaDeserializer.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) }, - { "securitySchemes", (a, n) => a.SecuritySchemes = n.CreateMapWithReference(ReferenceType.SecurityScheme, LoadSecurityScheme) }, - { "parameters", (a, n) => a.Parameters = n.CreateMapWithReference(ReferenceType.Parameter, LoadParameter) }, - { "correlationIds", (a, n) => a.CorrelationIds = n.CreateMapWithReference(ReferenceType.CorrelationId, LoadCorrelationId) }, - { "operationTraits", (a, n) => a.OperationTraits = n.CreateMapWithReference(ReferenceType.OperationTrait, LoadOperationTrait) }, - { "messageTraits", (a, n) => a.MessageTraits = n.CreateMapWithReference(ReferenceType.MessageTrait, LoadMessageTrait) }, - { "serverBindings", (a, n) => a.ServerBindings = n.CreateMapWithReference(ReferenceType.ServerBindings, LoadServerBindings) }, - { "channelBindings", (a, n) => a.ChannelBindings = n.CreateMapWithReference(ReferenceType.ChannelBindings, LoadChannelBindings) }, - { "operationBindings", (a, n) => a.OperationBindings = n.CreateMapWithReference(ReferenceType.OperationBindings, LoadOperationBindings) }, - { "messageBindings", (a, n) => a.MessageBindings = n.CreateMapWithReference(ReferenceType.MessageBindings, LoadMessageBindings) }, + { "schemas", (a, n) => a.Schemas = n.CreateMap(AsyncApiSchemaDeserializer.LoadSchema) }, + { "servers", (a, n) => a.Servers = n.CreateMap(LoadServer) }, + { "channels", (a, n) => a.Channels = n.CreateMap(LoadChannel) }, + { "messages", (a, n) => a.Messages = n.CreateMap(LoadMessage) }, + { "securitySchemes", (a, n) => a.SecuritySchemes = n.CreateMap(LoadSecurityScheme) }, + { "parameters", (a, n) => a.Parameters = n.CreateMap(LoadParameter) }, + { "correlationIds", (a, n) => a.CorrelationIds = n.CreateMap(LoadCorrelationId) }, + { "operationTraits", (a, n) => a.OperationTraits = n.CreateMap(LoadOperationTrait) }, + { "messageTraits", (a, n) => a.MessageTraits = n.CreateMap(LoadMessageTrait) }, + { "serverBindings", (a, n) => a.ServerBindings = n.CreateMap(LoadServerBindings) }, + { "channelBindings", (a, n) => a.ChannelBindings = n.CreateMap(LoadChannelBindings) }, + { "operationBindings", (a, n) => a.OperationBindings = n.CreateMap(LoadOperationBindings) }, + { "messageBindings", (a, n) => a.MessageBindings = n.CreateMap(LoadMessageBindings) }, }; private static PatternFieldMap componentsPatternFields = diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiCorrelationIdDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiCorrelationIdDeserializer.cs index 0b8e88f0..ee908181 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiCorrelationIdDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiCorrelationIdDeserializer.cs @@ -31,7 +31,7 @@ public static AsyncApiCorrelationId LoadCorrelationId(ParseNode node) var pointer = mapNode.GetReferencePointer(); if (pointer != null) { - return mapNode.GetReferencedObject(ReferenceType.CorrelationId, pointer); + return new AsyncApiCorrelationIdReference(pointer); } var correlationId = new AsyncApiCorrelationId(); diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageBindingDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageBindingDeserializer.cs index 9a180e84..eb58ca1f 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageBindingDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageBindingDeserializer.cs @@ -12,9 +12,13 @@ internal static partial class AsyncApiV2Deserializer internal static AsyncApiBindings LoadMessageBindings(ParseNode node) { var mapNode = node.CheckMapNode("messageBindings"); + var pointer = mapNode.GetReferencePointer(); + if (pointer != null) + { + return new AsyncApiBindingsReference(pointer); + } var messageBindings = new AsyncApiBindings(); - foreach (var property in mapNode) { var messageBinding = LoadMessageBinding(property); diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageDeserializer.cs index 4c16bf22..fe7b8d4a 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 AsyncApiSchemaDeserializer.LoadSchema(n); + case var _ when SupportedAvroSchemaFormats.Where(s => format.StartsWith(s)).Any(): + return 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; @@ -94,12 +137,13 @@ public static AsyncApiMessage LoadMessage(ParseNode node) var pointer = mapNode.GetReferencePointer(); if (pointer != null) { - return mapNode.GetReferencedObject(ReferenceType.Message, pointer); + return new AsyncApiMessageReference(pointer); } 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..40e8a945 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(); } }, @@ -38,7 +38,7 @@ public static AsyncApiMessageTrait LoadMessageTrait(ParseNode node) if (pointer != null) { - return mapNode.GetReferencedObject(ReferenceType.MessageTrait, pointer); + return new AsyncApiMessageTraitReference(pointer); } var messageTrait = new AsyncApiMessageTrait(); diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiOperationBindingDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiOperationBindingDeserializer.cs index 4d4eb85f..0fb5db0a 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiOperationBindingDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiOperationBindingDeserializer.cs @@ -12,9 +12,13 @@ internal static partial class AsyncApiV2Deserializer internal static AsyncApiBindings LoadOperationBindings(ParseNode node) { var mapNode = node.CheckMapNode("operationBindings"); + var pointer = mapNode.GetReferencePointer(); + if (pointer != null) + { + return new AsyncApiBindingsReference(pointer); + } var operationBindings = new AsyncApiBindings(); - foreach (var property in mapNode) { var operationBinding = LoadOperationBinding(property); diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiOperationTraitDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiOperationTraitDeserializer.cs index 561456e9..ee503d91 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiOperationTraitDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiOperationTraitDeserializer.cs @@ -31,7 +31,7 @@ public static AsyncApiOperationTrait LoadOperationTrait(ParseNode node) var pointer = mapNode.GetReferencePointer(); if (pointer != null) { - return mapNode.GetReferencedObject(ReferenceType.OperationTrait, pointer); + return new AsyncApiOperationTraitReference(pointer); } var operationTrait = new AsyncApiOperationTrait(); diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiParameterDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiParameterDeserializer.cs index bff810f1..e7628c12 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(); } }, }; @@ -28,7 +28,7 @@ public static AsyncApiParameter LoadParameter(ParseNode node) var pointer = mapNode.GetReferencePointer(); if (pointer != null) { - return mapNode.GetReferencedObject(ReferenceType.Parameter, pointer); + return new AsyncApiParameterReference(pointer); } var parameter = new AsyncApiParameter(); diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiSchemaDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiSchemaDeserializer.cs index 1934cb0d..45b74674 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiSchemaDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiSchemaDeserializer.cs @@ -9,9 +9,9 @@ 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() + private static readonly FixedFieldMap schemaFixedFields = new() { { "title", (a, n) => { a.Title = n.GetScalarValue(); } @@ -215,35 +215,13 @@ public class JsonSchemaDeserializer }, }; - private static readonly PatternFieldMap schemaPatternFields = + private static readonly PatternFieldMap schemaPatternFields = new() { { 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) + public static AsyncApiJsonSchema LoadSchema(ParseNode node) { var mapNode = node.CheckMapNode(AsyncApiConstants.Schema); @@ -251,23 +229,16 @@ public static AsyncApiSchema LoadSchema(ParseNode node) if (pointer != null) { - return new AsyncApiSchema - { - UnresolvedReference = true, - Reference = node.Context.VersionService.ConvertToAsyncApiReference(pointer, ReferenceType.Schema), - }; + return new AsyncApiJsonSchemaReference(pointer); } - var schema = new AsyncApiSchema(); + var schema = new AsyncApiJsonSchema(); foreach (var propertyNode in mapNode) { 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/AsyncApiSecurityRequirementDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiSecurityRequirementDeserializer.cs index e3574f6a..f9f02907 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiSecurityRequirementDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiSecurityRequirementDeserializer.cs @@ -36,19 +36,11 @@ public static AsyncApiSecurityRequirement LoadSecurityRequirement(ParseNode node return securityRequirement; } - private static AsyncApiSecurityScheme LoadSecuritySchemeByReference( + private static AsyncApiSecuritySchemeReference LoadSecuritySchemeByReference( ParsingContext context, string schemeName) { - var securitySchemeObject = new AsyncApiSecurityScheme - { - UnresolvedReference = true, - Reference = new AsyncApiReference - { - Id = schemeName, - Type = ReferenceType.SecurityScheme, - }, - }; + var securitySchemeObject = new AsyncApiSecuritySchemeReference(schemeName); return securitySchemeObject; } diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiSecuritySchemeDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiSecuritySchemeDeserializer.cs index 92708116..37481197 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiSecuritySchemeDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiSecuritySchemeDeserializer.cs @@ -56,7 +56,7 @@ public static AsyncApiSecurityScheme LoadSecurityScheme(ParseNode node) var pointer = mapNode.GetReferencePointer(); if (pointer != null) { - return mapNode.GetReferencedObject(ReferenceType.SecurityScheme, pointer); + return new AsyncApiSecuritySchemeReference(pointer); } var securityScheme = new AsyncApiSecurityScheme(); diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiServerBindingDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiServerBindingDeserializer.cs index 44c821d3..18a8ea84 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiServerBindingDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiServerBindingDeserializer.cs @@ -15,7 +15,7 @@ internal static AsyncApiBindings LoadServerBindings(ParseNode no var pointer = mapNode.GetReferencePointer(); if (pointer != null) { - return mapNode.GetReferencedObject>(ReferenceType.ServerBindings, pointer); + return new AsyncApiBindingsReference(pointer); } var serverBindings = new AsyncApiBindings(); diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiServerDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiServerDeserializer.cs index 6eae00af..9d72d588 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiServerDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiServerDeserializer.cs @@ -52,7 +52,7 @@ public static AsyncApiServer LoadServer(ParseNode node) var pointer = mapNode.GetReferencePointer(); if (pointer != null) { - return mapNode.GetReferencedObject(ReferenceType.Server, pointer); + return new AsyncApiServerReference(pointer); } var server = new AsyncApiServer(); diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiServerVariableDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiServerVariableDeserializer.cs index b773d255..a47ec6a6 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiServerVariableDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiServerVariableDeserializer.cs @@ -41,7 +41,7 @@ public static AsyncApiServerVariable LoadServerVariable(ParseNode node) var pointer = mapNode.GetReferencePointer(); if (pointer != null) { - return mapNode.GetReferencedObject(ReferenceType.ServerVariable, pointer); + return new AsyncApiServerVariableReference(pointer); } var serverVariable = new AsyncApiServerVariable(); diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs index 10edd3ba..6cbca7ed 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs @@ -9,7 +9,6 @@ namespace LEGO.AsyncAPI.Readers.V2 using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Readers.Interface; using LEGO.AsyncAPI.Readers.ParseNodes; - using LEGO.AsyncAPI.Writers; internal class AsyncApiV2VersionService : IAsyncApiVersionService { @@ -35,14 +34,22 @@ public AsyncApiV2VersionService(AsyncApiDiagnostic diagnostic) [typeof(AsyncApiOAuthFlows)] = AsyncApiV2Deserializer.LoadOAuthFlows, [typeof(AsyncApiOperation)] = AsyncApiV2Deserializer.LoadOperation, [typeof(AsyncApiParameter)] = AsyncApiV2Deserializer.LoadParameter, - [typeof(AsyncApiSchema)] = JsonSchemaDeserializer.LoadSchema, + [typeof(AsyncApiJsonSchema)] = AsyncApiSchemaDeserializer.LoadSchema, + [typeof(AsyncApiAvroSchema)] = AsyncApiAvroSchemaDeserializer.LoadSchema, + [typeof(AsyncApiJsonSchema)] = AsyncApiV2Deserializer.LoadJsonSchemaPayload, + [typeof(AsyncApiAvroSchema)] = AsyncApiV2Deserializer.LoadAvroPayload, [typeof(AsyncApiSecurityRequirement)] = AsyncApiV2Deserializer.LoadSecurityRequirement, [typeof(AsyncApiSecurityScheme)] = AsyncApiV2Deserializer.LoadSecurityScheme, [typeof(AsyncApiServer)] = AsyncApiV2Deserializer.LoadServer, [typeof(AsyncApiServerVariable)] = AsyncApiV2Deserializer.LoadServerVariable, [typeof(AsyncApiTag)] = AsyncApiV2Deserializer.LoadTag, [typeof(AsyncApiMessage)] = AsyncApiV2Deserializer.LoadMessage, + [typeof(AsyncApiMessageTrait)] = AsyncApiV2Deserializer.LoadMessageTrait, [typeof(AsyncApiChannel)] = AsyncApiV2Deserializer.LoadChannel, + [typeof(AsyncApiBindings)] = AsyncApiV2Deserializer.LoadServerBindings, + [typeof(AsyncApiBindings)] = AsyncApiV2Deserializer.LoadChannelBindings, + [typeof(AsyncApiBindings)] = AsyncApiV2Deserializer.LoadMessageBindings, + [typeof(AsyncApiBindings)] = AsyncApiV2Deserializer.LoadOperationBindings, }; /// @@ -59,78 +66,15 @@ public AsyncApiReference ConvertToAsyncApiReference( throw new AsyncApiException($"The reference string '{reference}' has invalid format."); } - var segments = reference.Split('#'); - if (segments.Length == 1) + try { - if (type == ReferenceType.SecurityScheme) - { - return new AsyncApiReference - { - Type = type, - Id = reference, - }; - } - - var asyncApiReference = new AsyncApiReference(); - asyncApiReference.Type = type; - if (reference.StartsWith("/")) - { - asyncApiReference.IsFragment = true; - } - - asyncApiReference.ExternalResource = segments[0]; - - return asyncApiReference; + return new AsyncApiReference(reference, type); } - else if (segments.Length == 2) + catch (AsyncApiException ex) { - // Local reference - if (reference.StartsWith("#")) - { - try - { - return this.ParseReference(segments[1]); - } - catch (AsyncApiException ex) - { - this.Diagnostic.Errors.Add(new AsyncApiError(ex)); - return null; - } - } - - var id = segments[1]; - var asyncApiReference = new AsyncApiReference(); - if (id.StartsWith("/components/")) - { - var localSegments = segments[1].Split('/'); - var referencedType = localSegments[2].GetEnumFromDisplayName(); - if (type == null) - { - type = referencedType; - } - else - { - if (type != referencedType) - { - throw new AsyncApiException("Referenced type mismatch"); - } - } - - id = localSegments[3]; - } - else - { - asyncApiReference.IsFragment = true; - } - - asyncApiReference.ExternalResource = segments[0]; - asyncApiReference.Type = type; - asyncApiReference.Id = id; - - return asyncApiReference; + this.Diagnostic.Errors.Add(new AsyncApiError(ex)); + return null; } - - throw new AsyncApiException($"The reference string '{reference}' has invalid format."); } public AsyncApiDocument LoadDocument(RootNode rootNode) @@ -143,27 +87,5 @@ public T LoadElement(ParseNode node) { return (T)this.loaders[typeof(T)](node); } - - private AsyncApiReference ParseReference(string localReference) - { - if (string.IsNullOrWhiteSpace(localReference)) - { - throw new ArgumentException( - $"The argument '{nameof(localReference)}' is null, empty or consists only of white-space."); - } - - var segments = localReference.Split('/'); - - if (segments.Length == 4) - { - if (segments[1] == "components") - { - var referenceType = segments[2].GetEnumFromDisplayName(); - return new AsyncApiReference { Type = referenceType, Id = segments[3] }; - } - } - - throw new AsyncApiException($"The reference string '{localReference}' has invalid format."); - } } } \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Readers/YamlConverter.cs b/src/LEGO.AsyncAPI.Readers/YamlConverter.cs index 648ee50a..942bc28f 100644 --- a/src/LEGO.AsyncAPI.Readers/YamlConverter.cs +++ b/src/LEGO.AsyncAPI.Readers/YamlConverter.cs @@ -10,55 +10,55 @@ namespace LEGO.AsyncAPI.Readers internal static class YamlConverter { - public static JsonNode ToJsonNode(this YamlDocument yamlDocument, AsyncApiReaderSettings settings) + public static JsonNode ToJsonNode(this YamlDocument yamlDocument, CultureInfo cultureInfo) { - return yamlDocument.RootNode.ToJsonNode(settings); + return yamlDocument.RootNode.ToJsonNode(cultureInfo); } - public static JsonObject ToJsonObject(this YamlMappingNode yamlMappingNode, AsyncApiReaderSettings settings) + public static JsonObject ToJsonObject(this YamlMappingNode yamlMappingNode, CultureInfo cultureInfo) { var node = new JsonObject(); foreach (var keyValuePair in yamlMappingNode) { var key = ((YamlScalarNode)keyValuePair.Key).Value!; - node[key] = keyValuePair.Value.ToJsonNode(settings); + node[key] = keyValuePair.Value.ToJsonNode(cultureInfo); } return node; } - public static JsonArray ToJsonArray(this YamlSequenceNode yaml, AsyncApiReaderSettings settings) + public static JsonArray ToJsonArray(this YamlSequenceNode yaml, CultureInfo cultureInfo) { var node = new JsonArray(); foreach (var value in yaml) { - node.Add(value.ToJsonNode(settings)); + node.Add(value.ToJsonNode(cultureInfo)); } return node; } - public static JsonNode ToJsonNode(this YamlNode yaml, AsyncApiReaderSettings settings) + public static JsonNode ToJsonNode(this YamlNode yaml, CultureInfo cultureInfo) { return yaml switch { - YamlMappingNode map => map.ToJsonObject(settings), - YamlSequenceNode seq => seq.ToJsonArray(settings), - YamlScalarNode scalar => scalar.ToJsonValue(settings), + YamlMappingNode map => map.ToJsonObject(cultureInfo), + YamlSequenceNode seq => seq.ToJsonArray(cultureInfo), + YamlScalarNode scalar => scalar.ToJsonValue(cultureInfo), _ => throw new NotSupportedException("This yaml isn't convertible to JSON"), }; } - private static JsonValue ToJsonValue(this YamlScalarNode yaml, AsyncApiReaderSettings settings) + private static JsonValue ToJsonValue(this YamlScalarNode yaml, CultureInfo cultureInfo) { switch (yaml.Style) { case ScalarStyle.Plain: - return decimal.TryParse(yaml.Value, NumberStyles.Float, settings.CultureInfo, out var d) + return decimal.TryParse(yaml.Value, NumberStyles.Float, cultureInfo, out var d) ? JsonValue.Create(d) : bool.TryParse(yaml.Value, out var b) ? JsonValue.Create(b) - : JsonValue.Create(yaml.Value) !; + : JsonValue.Create(yaml.Value)!; case ScalarStyle.SingleQuoted: case ScalarStyle.DoubleQuoted: case ScalarStyle.Literal: 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/AsyncApiWorkspace.cs b/src/LEGO.AsyncAPI/AsyncApiWorkspace.cs new file mode 100644 index 00000000..5d581e22 --- /dev/null +++ b/src/LEGO.AsyncAPI/AsyncApiWorkspace.cs @@ -0,0 +1,192 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI +{ + using System; + using System.Collections.Generic; + using System.IO; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Models.Interfaces; + + public class AsyncApiWorkspace + { + private readonly Dictionary artifactsRegistry = new(); + private readonly Dictionary resolvedReferenceRegistry = new(); + + public void RegisterComponents(AsyncApiDocument document) + { + if (document?.Components == null) + { + return; + } + + string baseUri = "#/components/"; + string location; + + // Register Schema + foreach (var item in document.Components.Schemas) + { + location = baseUri + ReferenceType.Schema.GetDisplayName() + "/" + item.Key; + this.RegisterComponent(location, item.Value); + } + + // Register Parameters + foreach (var item in document.Components.Parameters) + { + location = baseUri + ReferenceType.Parameter.GetDisplayName() + "/" + item.Key; + this.RegisterComponent(location, item.Value); + } + + // Register Channels + foreach (var item in document.Components.Channels) + { + location = baseUri + ReferenceType.Channel.GetDisplayName() + "/" + item.Key; + this.RegisterComponent(location, item.Value); + } + + // Register Servers + foreach (var item in document.Components.Servers) + { + location = baseUri + ReferenceType.Server.GetDisplayName() + "/" + item.Key; + this.RegisterComponent(location, item.Value); + } + + // Register ServerVariables + foreach (var item in document.Components.ServerVariables) + { + location = baseUri + ReferenceType.ServerVariable.GetDisplayName() + "/" + item.Key; + this.RegisterComponent(location, item.Value); + } + + // Register Messages + foreach (var item in document.Components.Messages) + { + location = baseUri + ReferenceType.Message.GetDisplayName() + "/" + item.Key; + this.RegisterComponent(location, item.Value); + } + + // Register SecuritySchemes + foreach (var item in document.Components.SecuritySchemes) + { + location = baseUri + ReferenceType.SecurityScheme.GetDisplayName() + "/" + item.Key; + this.RegisterComponent(location, item.Value); + this.RegisterComponent(item.Key, item.Value); + } + + // Register Parameters + foreach (var item in document.Components.Parameters) + { + location = baseUri + ReferenceType.Parameter.GetDisplayName() + "/" + item.Key; + this.RegisterComponent(location, item.Value); + } + + // Register CorrelationIds + foreach (var item in document.Components.CorrelationIds) + { + location = baseUri + ReferenceType.CorrelationId.GetDisplayName() + "/" + item.Key; + this.RegisterComponent(location, item.Value); + } + + // Register OperationTraits + foreach (var item in document.Components.OperationTraits) + { + location = baseUri + ReferenceType.OperationTrait.GetDisplayName() + "/" + item.Key; + this.RegisterComponent(location, item.Value); + } + + // Register MessageTraits + foreach (var item in document.Components.MessageTraits) + { + location = baseUri + ReferenceType.MessageTrait.GetDisplayName() + "/" + item.Key; + this.RegisterComponent(location, item.Value); + } + + // Register ServerBindings + foreach (var item in document.Components.ServerBindings) + { + location = baseUri + ReferenceType.ServerBindings.GetDisplayName() + "/" + item.Key; + this.RegisterComponent(location, item.Value); + } + + // Register ChannelBindings + foreach (var item in document.Components.ChannelBindings) + { + location = baseUri + ReferenceType.ChannelBindings.GetDisplayName() + "/" + item.Key; + this.RegisterComponent(location, item.Value); + } + + // Register OperationBindings + foreach (var item in document.Components.OperationBindings) + { + location = baseUri + ReferenceType.OperationBindings.GetDisplayName() + "/" + item.Key; + this.RegisterComponent(location, item.Value); + } + + // Register MessageBindings + foreach (var item in document.Components.MessageBindings) + { + location = baseUri + ReferenceType.MessageBindings.GetDisplayName() + "/" + item.Key; + this.RegisterComponent(location, item.Value); + } + } + + public bool RegisterComponent(string location, T component) + { + var uri = this.ToLocationUrl(location); + if (component is IAsyncApiSerializable referenceable) + { + if (!this.resolvedReferenceRegistry.ContainsKey(uri)) + { + this.resolvedReferenceRegistry[uri] = referenceable; + return true; + } + } + + if (component is Stream stream) + { + if (!this.artifactsRegistry.ContainsKey(uri)) + { + this.artifactsRegistry[uri] = stream; + } + return true; + } + + return false; + } + + public bool Contains(string location) + { + var key = this.ToLocationUrl(location); + return this.resolvedReferenceRegistry.ContainsKey(key) || this.artifactsRegistry.ContainsKey(key); + } + + public T ResolveReference(AsyncApiReference reference) + where T : class + { + return this.ResolveReference(reference.Reference); + } + + public T ResolveReference(string location) + where T : class + { + var uri = this.ToLocationUrl(location); + if (this.resolvedReferenceRegistry.TryGetValue(uri, out var referenceableValue)) + { + return referenceableValue as T; + } + + if (this.artifactsRegistry.TryGetValue(uri, out var stream)) + { + stream.Position = 0; + return (T)(object)stream; + } + + return default; + } + + private Uri ToLocationUrl(string location) + { + return new(location, UriKind.RelativeOrAbsolute); + } + } +} diff --git a/src/LEGO.AsyncAPI/LEGO.AsyncAPI.csproj b/src/LEGO.AsyncAPI/LEGO.AsyncAPI.csproj index 99e69016..c73cc9d0 100644 --- a/src/LEGO.AsyncAPI/LEGO.AsyncAPI.csproj +++ b/src/LEGO.AsyncAPI/LEGO.AsyncAPI.csproj @@ -5,6 +5,7 @@ AsyncAPI.NET LEGO.AsyncAPI LEGO.AsyncAPI + netstandard2.0;net8 @@ -19,7 +20,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + <_Parameter1>$(MSBuildProjectName).Tests 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/AsyncApiBinding.cs b/src/LEGO.AsyncAPI/Models/AsyncApiBinding.cs index 4034b4e9..a2c8303a 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiBinding.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiBinding.cs @@ -4,7 +4,6 @@ namespace LEGO.AsyncAPI.Bindings { using System; using System.Collections.Generic; - using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Writers; @@ -12,10 +11,6 @@ public abstract class AsyncApiBinding : IBinding { public abstract string BindingKey { get; } - public bool UnresolvedReference { get; set; } - - public AsyncApiReference Reference { get; set; } - public IDictionary Extensions { get; set; } = new Dictionary(); public string BindingVersion { get; set; } @@ -27,12 +22,6 @@ public void SerializeV2(IAsyncApiWriter writer) throw new ArgumentNullException(nameof(writer)); } - if (this.Reference != null && !writer.GetSettings().ShouldInlineReference(this.Reference)) - { - this.Reference.SerializeV2(writer); - return; - } - this.SerializeProperties(writer); } diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiBindings{TBinding}.cs b/src/LEGO.AsyncAPI/Models/AsyncApiBindings{TBinding}.cs index f11858aa..260629ae 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiBindings{TBinding}.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiBindings{TBinding}.cs @@ -3,39 +3,17 @@ namespace LEGO.AsyncAPI.Models { using System; + using System.Collections; using System.Collections.Generic; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Writers; - public class AsyncApiBindings : Dictionary, IAsyncApiReferenceable + public class AsyncApiBindings : IDictionary, IAsyncApiSerializable where TBinding : IBinding { - public bool UnresolvedReference { get; set; } + private Dictionary inner = new Dictionary(); - public AsyncApiReference Reference { get; set; } - - public void Add(TBinding binding) - { - this[binding.BindingKey] = binding; - } - - 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 void SerializeV2WithoutReference(IAsyncApiWriter writer) + public virtual void SerializeV2(IAsyncApiWriter writer) { if (writer is null) { @@ -56,5 +34,79 @@ public void SerializeV2WithoutReference(IAsyncApiWriter writer) writer.WriteEndObject(); } + + public virtual void Add(TBinding binding) + { + this[binding.BindingKey] = binding; + } + + public virtual TBinding this[string key] + { + get => inner[key]; + set => inner[key] = value; + } + + public virtual ICollection Keys => inner.Keys; + + public virtual ICollection Values => inner.Values; + + public virtual int Count => inner.Count; + + public virtual bool IsReadOnly => ((IDictionary)inner).IsReadOnly; + + public virtual void Add(string key, TBinding value) + { + inner.Add(key, value); + } + + public virtual bool ContainsKey(string key) + { + return inner.ContainsKey(key); + } + + public virtual bool Remove(string key) + { + return inner.Remove(key); + } + + public virtual bool TryGetValue(string key, out TBinding value) + { + return inner.TryGetValue(key, out value); + } + + public virtual void Add(KeyValuePair item) + { + ((IDictionary)inner).Add(item); + } + + public virtual void Clear() + { + inner.Clear(); + } + + public virtual bool Contains(KeyValuePair item) + { + return ((IDictionary)inner).Contains(item); + } + + public virtual void CopyTo(KeyValuePair[] array, int arrayIndex) + { + ((IDictionary)inner).CopyTo(array, arrayIndex); + } + + public virtual bool Remove(KeyValuePair item) + { + return ((IDictionary)inner).Remove(item); + } + + public virtual IEnumerator> GetEnumerator() + { + return inner.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return inner.GetEnumerator(); + } } } diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiChannel.cs b/src/LEGO.AsyncAPI/Models/AsyncApiChannel.cs index 2ff637f5..a7e25963 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiChannel.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiChannel.cs @@ -10,12 +10,12 @@ namespace LEGO.AsyncAPI.Models /// /// Describes the operations available on a single channel. /// - public class AsyncApiChannel : IAsyncApiReferenceable, IAsyncApiExtensible + public class AsyncApiChannel : IAsyncApiSerializable, IAsyncApiExtensible { /// /// an optional description of this channel item. CommonMark syntax can be used for rich text representation. /// - public string Description { get; set; } + public virtual string Description { get; set; } /// /// the servers on which this channel is available, specified as an optional unordered list of names (string keys) of Server Objects defined in the Servers Object (a map). @@ -23,52 +23,32 @@ public class AsyncApiChannel : IAsyncApiReferenceable, IAsyncApiExtensible /// /// If servers is absent or empty then this channel must be available on all servers defined in the Servers Object. /// - public IList Servers { get; set; } = new List(); + public virtual IList Servers { get; set; } = new List(); /// /// a definition of the SUBSCRIBE operation, which defines the messages produced by the application and sent to the channel. /// - public AsyncApiOperation Subscribe { get; set; } + public virtual AsyncApiOperation Subscribe { get; set; } /// /// a definition of the PUBLISH operation, which defines the messages consumed by the application from the channel. /// - public AsyncApiOperation Publish { get; set; } + public virtual AsyncApiOperation Publish { get; set; } /// /// a map of the parameters included in the channel name. It SHOULD be present only when using channels with expressions (as defined by RFC 6570 section 2.2). /// - public IDictionary Parameters { get; set; } = new Dictionary(); + public virtual IDictionary Parameters { get; set; } = new Dictionary(); /// /// a map where the keys describe the name of the protocol and the values describe protocol-specific definitions for the channel. /// - public AsyncApiBindings Bindings { get; set; } = new AsyncApiBindings(); + public virtual AsyncApiBindings Bindings { get; set; } = new AsyncApiBindings(); /// - public IDictionary Extensions { get; set; } = new Dictionary(); + public virtual IDictionary Extensions { get; set; } = new Dictionary(); - public bool UnresolvedReference { get; set; } - - public AsyncApiReference Reference { get; set; } - - 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 void SerializeV2WithoutReference(IAsyncApiWriter writer) + public virtual void SerializeV2(IAsyncApiWriter writer) { if (writer is null) { @@ -92,11 +72,9 @@ public void SerializeV2WithoutReference(IAsyncApiWriter writer) // parameters writer.WriteOptionalMap(AsyncApiConstants.Parameters, this.Parameters, (writer, key, component) => { - if (component.Reference != null && - component.Reference.Type == ReferenceType.Channel && - component.Reference.Id == key) + if (component is AsyncApiParameterReference reference) { - component.SerializeV2WithoutReference(writer); + reference.SerializeV2(writer); } else { diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiComponents.cs b/src/LEGO.AsyncAPI/Models/AsyncApiComponents.cs index 6c8a2a4c..0d00a9d9 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiComponents.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiComponents.cs @@ -19,7 +19,7 @@ public class AsyncApiComponents : IAsyncApiExtensible, IAsyncApiSerializable /// /// An object to hold reusable Schema Objects. /// - public IDictionary Schemas { get; set; } = new Dictionary(); + public IDictionary Schemas { get; set; } = new Dictionary(); /// /// An object to hold reusable Server Objects. @@ -97,21 +97,21 @@ public void SerializeV2(IAsyncApiWriter writer) // If references have been inlined we don't need the to render the components section // however if they have cycles, then we will need a component rendered - if (writer.GetSettings().InlineReferences) + if (writer.GetSettings().InlineLocalReferences) { var loops = writer.GetSettings().LoopDetector.Loops; writer.WriteStartObject(); - if (loops.TryGetValue(typeof(AsyncApiSchema), out List schemas)) + if (loops.TryGetValue(typeof(AsyncApiJsonSchemaReference), out List schemas)) { - var asyncApiSchemas = schemas.Cast().Distinct().ToList() - .ToDictionary(k => k.Reference.Id); + var asyncApiSchemas = schemas.Cast().Distinct().ToList() + .ToDictionary(k => k.Reference.FragmentId); writer.WriteOptionalMap( AsyncApiConstants.Schemas, this.Schemas, (w, key, component) => { - component.SerializeV2WithoutReference(w); + component.SerializeV2(w); }); } @@ -130,11 +130,9 @@ public void SerializeV2(IAsyncApiWriter writer) this.Schemas, (w, key, component) => { - if (component.Reference != null && - component.Reference.Type == ReferenceType.Schema && - component.Reference.Id == key) + if (component is AsyncApiJsonSchemaReference reference) { - component.SerializeV2WithoutReference(w); + reference.SerializeV2(w); } else { @@ -148,11 +146,9 @@ public void SerializeV2(IAsyncApiWriter writer) this.Servers, (w, key, component) => { - if (component.Reference != null && - component.Reference.Type == ReferenceType.Server && - component.Reference.Id == key) + if (component is AsyncApiServerReference reference) { - component.SerializeV2WithoutReference(w); + reference.SerializeV2(w); } else { @@ -166,11 +162,9 @@ public void SerializeV2(IAsyncApiWriter writer) this.ServerVariables, (w, key, component) => { - if (component.Reference != null && - component.Reference.Type == ReferenceType.ServerVariable && - component.Reference.Id == key) + if (component is AsyncApiServerVariableReference reference) { - component.SerializeV2WithoutReference(w); + reference.SerializeV2(w); } else { @@ -184,11 +178,9 @@ public void SerializeV2(IAsyncApiWriter writer) this.Channels, (w, key, component) => { - if (component.Reference != null && - component.Reference.Type == ReferenceType.Channel && - component.Reference.Id == key) + if (component is AsyncApiChannelReference reference) { - component.SerializeV2WithoutReference(w); + reference.SerializeV2(w); } else { @@ -202,11 +194,9 @@ public void SerializeV2(IAsyncApiWriter writer) this.Messages, (w, key, component) => { - if (component.Reference != null && - component.Reference.Type == ReferenceType.Message && - component.Reference.Id == key) + if (component is AsyncApiMessageReference reference) { - component.SerializeV2WithoutReference(w); + reference.SerializeV2(w); } else { @@ -220,11 +210,9 @@ public void SerializeV2(IAsyncApiWriter writer) this.SecuritySchemes, (w, key, component) => { - if (component.Reference != null && - component.Reference.Type == ReferenceType.SecurityScheme && - component.Reference.Id == key) + if (component is AsyncApiSecuritySchemeReference reference) { - component.SerializeV2WithoutReference(w); + reference.SerializeV2(w); } else { @@ -238,11 +226,9 @@ public void SerializeV2(IAsyncApiWriter writer) this.Parameters, (w, key, component) => { - if (component.Reference != null && - component.Reference.Type == ReferenceType.Parameter && - component.Reference.Id == key) + if (component is AsyncApiParameterReference reference) { - component.SerializeV2WithoutReference(w); + reference.SerializeV2(w); } else { @@ -256,11 +242,9 @@ public void SerializeV2(IAsyncApiWriter writer) this.CorrelationIds, (w, key, component) => { - if (component.Reference != null && - component.Reference.Type == ReferenceType.CorrelationId && - component.Reference.Id == key) + if (component is AsyncApiCorrelationIdReference reference) { - component.SerializeV2WithoutReference(w); + reference.SerializeV2(w); } else { @@ -274,11 +258,9 @@ public void SerializeV2(IAsyncApiWriter writer) this.OperationTraits, (w, key, component) => { - if (component.Reference != null && - component.Reference.Type == ReferenceType.OperationTrait && - component.Reference.Id == key) + if (component is AsyncApiOperationTraitReference reference) { - component.SerializeV2WithoutReference(w); + reference.SerializeV2(w); } else { @@ -292,11 +274,9 @@ public void SerializeV2(IAsyncApiWriter writer) this.MessageTraits, (w, key, component) => { - if (component.Reference != null && - component.Reference.Type == ReferenceType.MessageTrait && - component.Reference.Id == key) + if (component is AsyncApiMessageTraitReference reference) { - component.SerializeV2WithoutReference(w); + reference.SerializeV2(w); } else { @@ -310,11 +290,9 @@ public void SerializeV2(IAsyncApiWriter writer) this.ServerBindings, (w, key, component) => { - if (component.Reference != null && - component.Reference.Type == ReferenceType.ServerBindings && - component.Reference.Id == key) + if (component is AsyncApiBindingsReference reference) { - component.SerializeV2WithoutReference(w); + reference.SerializeV2(w); } else { @@ -328,11 +306,9 @@ public void SerializeV2(IAsyncApiWriter writer) this.ChannelBindings, (w, key, component) => { - if (component.Reference != null && - component.Reference.Type == ReferenceType.ChannelBindings && - component.Reference.Id == key) + if (component is AsyncApiBindingsReference reference) { - component.SerializeV2WithoutReference(w); + reference.SerializeV2(w); } else { @@ -346,11 +322,9 @@ public void SerializeV2(IAsyncApiWriter writer) this.OperationBindings, (w, key, component) => { - if (component.Reference != null && - component.Reference.Type == ReferenceType.OperationBindings && - component.Reference.Id == key) + if (component is AsyncApiBindingsReference reference) { - component.SerializeV2WithoutReference(w); + reference.SerializeV2(w); } else { @@ -364,11 +338,9 @@ public void SerializeV2(IAsyncApiWriter writer) this.MessageBindings, (w, key, component) => { - if (component.Reference != null && - component.Reference.Type == ReferenceType.MessageBindings && - component.Reference.Id == key) + if (component is AsyncApiBindingsReference reference) { - component.SerializeV2WithoutReference(w); + reference.SerializeV2(w); } else { diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiCorrelationId.cs b/src/LEGO.AsyncAPI/Models/AsyncApiCorrelationId.cs index 156e7828..b3342110 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiCorrelationId.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiCorrelationId.cs @@ -10,44 +10,22 @@ namespace LEGO.AsyncAPI.Models /// /// An object that specifies an identifier at design time that can used for message tracing and correlation. /// - public class AsyncApiCorrelationId : IAsyncApiReferenceable, IAsyncApiExtensible, IAsyncApiSerializable + public class AsyncApiCorrelationId : IAsyncApiExtensible, IAsyncApiSerializable { /// /// an optional description of the identifier. CommonMark syntax can be used for rich text representation. /// - public string Description { get; set; } + public virtual string Description { get; set; } /// /// REQUIRED. A runtime expression that specifies the location of the correlation ID. /// - public string Location { get; set; } + public virtual string Location { get; set; } /// - public bool UnresolvedReference { get; set; } + public virtual IDictionary Extensions { get; set; } = new Dictionary(); - /// - public AsyncApiReference Reference { get; set; } - - /// - public IDictionary Extensions { get; set; } = new Dictionary(); - - 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 void SerializeV2WithoutReference(IAsyncApiWriter writer) + public virtual void SerializeV2(IAsyncApiWriter writer) { if (writer is null) { diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiDocument.cs b/src/LEGO.AsyncAPI/Models/AsyncApiDocument.cs index 5be55202..0359d083 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiDocument.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiDocument.cs @@ -4,10 +4,8 @@ namespace LEGO.AsyncAPI.Models { using System; using System.Collections.Generic; - using LEGO.AsyncAPI.Exceptions; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Writers; - using LEGO.AsyncAPI.Services; /// /// This is the root document object for the API specification. It combines resource listing and API declaration together into one document. @@ -46,7 +44,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. @@ -73,10 +71,7 @@ public void SerializeV2(IAsyncApiWriter writer) throw new ArgumentNullException(nameof(writer)); } - if (writer.GetSettings().InlineReferences) - { - this.ResolveReferences(); - } + writer.Workspace.RegisterComponents(this); writer.WriteStartObject(); @@ -90,37 +85,13 @@ public void SerializeV2(IAsyncApiWriter writer) writer.WriteOptionalProperty(AsyncApiConstants.Id, this.Id); // servers - writer.WriteOptionalMap(AsyncApiConstants.Servers, this.Servers, (writer, key, component) => - { - if (component.Reference != null && - component.Reference.Type == ReferenceType.Server && - component.Reference.Id == key) - { - component.SerializeV2WithoutReference(writer); - } - else - { - component.SerializeV2(writer); - } - }); + writer.WriteOptionalMap(AsyncApiConstants.Servers, this.Servers, (writer, key, component) => component.SerializeV2(writer)); // content type writer.WriteOptionalProperty(AsyncApiConstants.DefaultContentType, this.DefaultContentType); // channels - writer.WriteRequiredMap(AsyncApiConstants.Channels, this.Channels, (writer, key, component) => - { - if (component.Reference != null && - component.Reference.Type == ReferenceType.Channel && - component.Reference.Id == key) - { - component.SerializeV2WithoutReference(writer); - } - else - { - component.SerializeV2(writer); - } - }); + writer.WriteRequiredMap(AsyncApiConstants.Channels, this.Channels, (writer, key, component) => component.SerializeV2(writer)); // components writer.WriteOptionalObject(AsyncApiConstants.Components, this.Components, (w, c) => c.SerializeV2(w)); @@ -136,76 +107,5 @@ public void SerializeV2(IAsyncApiWriter writer) writer.WriteEndObject(); } - - public IEnumerable ResolveReferences() - { - var resolver = new AsyncApiReferenceResolver(this); - var walker = new AsyncApiWalker(resolver); - walker.Walk(this); - return resolver.Errors; - } - - internal T ResolveReference(AsyncApiReference reference) - where T : class, IAsyncApiReferenceable - { - return this.ResolveReference(reference) as T; - } - - internal IAsyncApiReferenceable ResolveReference(AsyncApiReference reference) - { - if (reference == null) - { - return null; - } - - if (!reference.Type.HasValue) - { - throw new ArgumentException("Reference must have a type."); - } - - if (this.Components == null) - { - throw new AsyncApiException(string.Format("Invalid reference Id: '{0}'", reference.Id)); - } - - try - { - switch (reference.Type) - { - case ReferenceType.Schema: - return this.Components.Schemas[reference.Id]; - case ReferenceType.Server: - return this.Components.Servers[reference.Id]; - case ReferenceType.Channel: - return this.Components.Channels[reference.Id]; - case ReferenceType.Message: - return this.Components.Messages[reference.Id]; - case ReferenceType.SecurityScheme: - return this.Components.SecuritySchemes[reference.Id]; - case ReferenceType.Parameter: - return this.Components.Parameters[reference.Id]; - case ReferenceType.CorrelationId: - return this.Components.CorrelationIds[reference.Id]; - case ReferenceType.OperationTrait: - return this.Components.OperationTraits[reference.Id]; - case ReferenceType.MessageTrait: - return this.Components.MessageTraits[reference.Id]; - case ReferenceType.ServerBindings: - return this.Components.ServerBindings[reference.Id]; - case ReferenceType.ChannelBindings: - return this.Components.ChannelBindings[reference.Id]; - case ReferenceType.OperationBindings: - return this.Components.OperationBindings[reference.Id]; - case ReferenceType.MessageBindings: - return this.Components.MessageBindings[reference.Id]; - default: - throw new AsyncApiException("Invalid reference type."); - } - } - catch (KeyNotFoundException) - { - throw new AsyncApiException(string.Format("Invalid reference Id: '{0}'", reference.Id)); - } - } } } diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiMessage.cs b/src/LEGO.AsyncAPI/Models/AsyncApiMessage.cs index 2c627541..6853222d 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiMessage.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiMessage.cs @@ -1,4 +1,4 @@ -// Copyright (c) The LEGO Group. All rights reserved. +// Copyright (c) The LEGO Group. All rights reserved. namespace LEGO.AsyncAPI.Models { @@ -10,27 +10,27 @@ namespace LEGO.AsyncAPI.Models /// /// Describes a message received on a given channel and operation. /// - public class AsyncApiMessage : IAsyncApiExtensible, IAsyncApiReferenceable, IAsyncApiSerializable + public class AsyncApiMessage : IAsyncApiExtensible, IAsyncApiSerializable { /// /// Unique string used to identify the message. The id MUST be unique among all messages described in the API. /// - public string MessageId { get; set; } + public virtual string MessageId { get; set; } /// /// schema definition of the application headers. Schema MUST be of type "object". /// - public AsyncApiSchema Headers { get; set; } + public virtual AsyncApiJsonSchema Headers { get; set; } /// /// 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 virtual IAsyncApiMessagePayload Payload { get; set; } /// /// definition of the correlation ID used for message tracing or matching. /// - public AsyncApiCorrelationId CorrelationId { get; set; } + public virtual AsyncApiCorrelationId CorrelationId { get; set; } /// /// a string containing the name of the schema format used to define the message payload. @@ -38,84 +38,62 @@ public class AsyncApiMessage : IAsyncApiExtensible, IAsyncApiReferenceable, IAsy /// /// If omitted, implementations should parse the payload as a Schema object. /// - public string SchemaFormat { get; set; } + public virtual string SchemaFormat { get; set; } /// /// the content type to use when encoding/decoding a message's payload. /// - public string ContentType { get; set; } + public virtual string ContentType { get; set; } /// /// a machine-friendly name for the message. /// - public string Name { get; set; } + public virtual string Name { get; set; } /// /// a human-friendly title for the message. /// - public string Title { get; set; } + public virtual string Title { get; set; } /// /// a short summary of what the message is about. /// - public string Summary { get; set; } + public virtual string Summary { get; set; } /// /// a verbose explanation of the message. CommonMark syntax can be used for rich text representation. /// - public string Description { get; set; } + public virtual string Description { get; set; } /// /// a list of tags for API documentation control. Tags can be used for logical grouping of messages. /// - public IList Tags { get; set; } = new List(); + public virtual IList Tags { get; set; } = new List(); /// /// additional external documentation for this message. /// - public AsyncApiExternalDocumentation ExternalDocs { get; set; } + public virtual AsyncApiExternalDocumentation ExternalDocs { get; set; } /// /// a map where the keys describe the name of the protocol and the values describe protocol-specific definitions for the message. /// - public AsyncApiBindings Bindings { get; set; } = new AsyncApiBindings(); + public virtual AsyncApiBindings Bindings { get; set; } = new AsyncApiBindings(); /// /// list of examples. /// - public IList Examples { get; set; } = new List(); + public virtual IList Examples { get; set; } = new List(); /// /// a list of traits to apply to the message object. Traits MUST be merged into the message object using the JSON Merge Patch algorithm in the same order they are defined here. The resulting object MUST be a valid Message Object. /// - public IList Traits { get; set; } = new List(); + public virtual IList Traits { get; set; } = new List(); /// - public IDictionary Extensions { get; set; } = new Dictionary(); + public virtual IDictionary Extensions { get; set; } = new Dictionary(); - /// - public bool UnresolvedReference { get; set; } - - /// - public AsyncApiReference Reference { get; set; } - - 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 void SerializeV2WithoutReference(IAsyncApiWriter writer) + public virtual void SerializeV2(IAsyncApiWriter writer) { if (writer is null) { diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiMessageTrait.cs b/src/LEGO.AsyncAPI/Models/AsyncApiMessageTrait.cs index 856fb7cd..1e62adc0 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiMessageTrait.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiMessageTrait.cs @@ -10,22 +10,22 @@ namespace LEGO.AsyncAPI.Models /// /// Describes a trait that MAY be applied to a Message Object. /// - public class AsyncApiMessageTrait : IAsyncApiExtensible, IAsyncApiReferenceable, IAsyncApiSerializable + public class AsyncApiMessageTrait : IAsyncApiExtensible, IAsyncApiSerializable { /// /// Unique string used to identify the message. The id MUST be unique among all messages described in the API. /// - public string MessageId { get; set; } + public virtual string MessageId { get; set; } /// /// schema definition of the application headers. Schema MUST be of type "object". /// - public AsyncApiSchema Headers { get; set; } + public virtual AsyncApiJsonSchema Headers { get; set; } /// /// definition of the correlation ID used for message tracing or matching. /// - public AsyncApiCorrelationId CorrelationId { get; set; } + public virtual AsyncApiCorrelationId CorrelationId { get; set; } /// /// a string containing the name of the schema format used to define the message payload. @@ -33,79 +33,56 @@ public class AsyncApiMessageTrait : IAsyncApiExtensible, IAsyncApiReferenceable, /// /// If omitted, implementations should parse the payload as a Schema object. /// - public string SchemaFormat { get; set; } + public virtual string SchemaFormat { get; set; } /// /// the content type to use when encoding/decoding a message's payload. /// - public string ContentType { get; set; } + public virtual string ContentType { get; set; } /// /// a machine-friendly name for the message. /// - public string Name { get; set; } + public virtual string Name { get; set; } /// /// a human-friendly title for the message. /// - public string Title { get; set; } + public virtual string Title { get; set; } /// /// a short summary of what the message is about. /// - public string Summary { get; set; } + public virtual string Summary { get; set; } /// /// a verbose explanation of the message. CommonMark syntax can be used for rich text representation. /// - public string Description { get; set; } + public virtual string Description { get; set; } /// /// a list of tags for API documentation control. Tags can be used for logical grouping of messages. /// - public IList Tags { get; set; } = new List(); + public virtual IList Tags { get; set; } = new List(); /// /// additional external documentation for this message. /// - public AsyncApiExternalDocumentation ExternalDocs { get; set; } - + public virtual AsyncApiExternalDocumentation ExternalDocs { get; set; } /// /// a map where the keys describe the name of the protocol and the values describe protocol-specific definitions for the message. /// - public AsyncApiBindings Bindings { get; set; } = new AsyncApiBindings(); + public virtual AsyncApiBindings Bindings { get; set; } = new AsyncApiBindings(); /// /// list of examples. /// - public IList Examples { get; set; } = new List(); - - /// - public IDictionary Extensions { get; set; } = new Dictionary(); - - /// - public bool UnresolvedReference { get; set; } + public virtual IList Examples { get; set; } = new List(); /// - public AsyncApiReference Reference { get; set; } - - 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 virtual IDictionary Extensions { get; set; } = new Dictionary(); - public void SerializeV2WithoutReference(IAsyncApiWriter writer) + public virtual void SerializeV2(IAsyncApiWriter writer) { if (writer is null) { diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiOperationTrait.cs b/src/LEGO.AsyncAPI/Models/AsyncApiOperationTrait.cs index c7e0d10e..9260808e 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiOperationTrait.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiOperationTrait.cs @@ -10,7 +10,7 @@ namespace LEGO.AsyncAPI.Models /// /// Describes a trait that MAY be applied to an Operation Object. /// - public class AsyncApiOperationTrait : IAsyncApiExtensible, IAsyncApiReferenceable, IAsyncApiSerializable + public class AsyncApiOperationTrait : IAsyncApiExtensible, IAsyncApiSerializable { /// /// unique string used to identify the operation. @@ -18,59 +18,37 @@ public class AsyncApiOperationTrait : IAsyncApiExtensible, IAsyncApiReferenceabl /// /// The id MUST be unique among all operations described in the API. /// - public string OperationId { get; set; } + public virtual string OperationId { get; set; } /// /// a short summary of what the operation is about. /// - public string Summary { get; set; } + public virtual string Summary { get; set; } /// /// a short summary of what the operation is about. /// - public string Description { get; set; } + public virtual string Description { get; set; } /// /// a list of tags for API documentation control. Tags can be used for logical grouping of operations. /// - public IList Tags { get; set; } = new List(); + public virtual IList Tags { get; set; } = new List(); /// /// additional external documentation for this operation. /// - public AsyncApiExternalDocumentation ExternalDocs { get; set; } + public virtual AsyncApiExternalDocumentation ExternalDocs { get; set; } /// /// a map where the keys describe the name of the protocol and the values describe protocol-specific definitions for the operation. /// - public AsyncApiBindings Bindings { get; set; } = new AsyncApiBindings(); + public virtual AsyncApiBindings Bindings { get; set; } = new AsyncApiBindings(); /// - public IDictionary Extensions { get; set; } = new Dictionary(); + public virtual IDictionary Extensions { get; set; } = new Dictionary(); - /// - public bool UnresolvedReference { get; set; } - - /// - public AsyncApiReference Reference { get; set; } - - 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 void SerializeV2WithoutReference(IAsyncApiWriter writer) + public virtual void SerializeV2(IAsyncApiWriter writer) { if (writer is null) { diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiParameter.cs b/src/LEGO.AsyncAPI/Models/AsyncApiParameter.cs index fd925c7b..1db08626 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiParameter.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiParameter.cs @@ -1,4 +1,4 @@ -// Copyright (c) The LEGO Group. All rights reserved. +// Copyright (c) The LEGO Group. All rights reserved. namespace LEGO.AsyncAPI.Models { @@ -10,49 +10,27 @@ namespace LEGO.AsyncAPI.Models /// /// Describes a parameter included in a channel name. /// - public class AsyncApiParameter : IAsyncApiReferenceable, IAsyncApiExtensible, IAsyncApiSerializable + public class AsyncApiParameter : IAsyncApiExtensible, IAsyncApiSerializable { /// /// Gets or sets a verbose explanation of the parameter. CommonMark syntax can be used for rich text representation. /// - public string Description { get; set; } + public virtual string Description { get; set; } /// /// Gets or sets definition of the parameter. /// - public AsyncApiSchema Schema { get; set; } + public virtual AsyncApiJsonSchema Schema { get; set; } /// /// Gets or sets a runtime expression that specifies the location of the parameter value. /// - public string Location { get; set; } + public virtual string Location { get; set; } /// - public IDictionary Extensions { get; set; } = new Dictionary(); + public virtual IDictionary Extensions { get; set; } = new Dictionary(); - /// - public bool UnresolvedReference { get; set; } - - /// - public AsyncApiReference Reference { get; set; } - - 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 void SerializeV2WithoutReference(IAsyncApiWriter writer) + public virtual void SerializeV2(IAsyncApiWriter writer) { if (writer is null) { diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiReference.cs b/src/LEGO.AsyncAPI/Models/AsyncApiReference.cs index 4f9660d1..38386b7b 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiReference.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiReference.cs @@ -3,6 +3,7 @@ namespace LEGO.AsyncAPI.Models { using System; + using LEGO.AsyncAPI.Exceptions; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Writers; @@ -11,13 +12,81 @@ namespace LEGO.AsyncAPI.Models /// public class AsyncApiReference : IAsyncApiSerializable { + private string originalString; + + public AsyncApiReference(string reference, ReferenceType? type) + { + if (string.IsNullOrWhiteSpace(reference)) + { + throw new AsyncApiException($"The reference string '{reference}' has invalid format."); + } + + this.originalString = reference; + var segments = reference.Split('#'); + if (segments.Length == 1) + { + if (type == ReferenceType.SecurityScheme) + { + this.Type = ReferenceType.SecurityScheme; + this.FragmentId = reference; + return; + } + + this.Type = type; + this.ExternalResource = segments[0]; + + return; + } + else if (segments.Length == 2) + { + if (reference.StartsWith("#")) // is local reference + { + var localSegments = reference.Split('/'); + + if (localSegments.Length == 4) + { + if (localSegments[1] == "components") // is local components reference + { + var referencedType = localSegments[2].GetEnumFromDisplayName(); + if (type == null || type == ReferenceType.None) + { + type = referencedType; + } + else + { + if (type != referencedType) + { + throw new AsyncApiException("Referenced type mismatch"); + } + } + } + } + } + else + { + this.ExternalResource = segments[0]; + } + + this.Type = type; + this.FragmentId = segments[1]; + + return; + } + + throw new AsyncApiException($"The reference string '{reference}' has invalid format."); + } + /// /// 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; } + public string ExternalResource + { + get; + set; + } /// /// Gets or sets the element type referenced. @@ -27,17 +96,17 @@ public class AsyncApiReference : IAsyncApiSerializable /// /// Gets or sets the identifier of the reusable component of one particular ReferenceType. /// - public string Id { get; set; } + public string FragmentId { get; set; } /// /// Gets or sets the AsyncApiDocument that is hosting the AsyncApiReference instance. This is used to enable dereferencing the reference. /// - public AsyncApiDocument HostDocument { get; set; } = null; + public AsyncApiWorkspace Workspace { 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; + public bool IsFragment => this.FragmentId != null; /// /// Gets a flag indicating whether this reference is an external reference. @@ -51,27 +120,12 @@ public string Reference { get { - if (this.IsExternal) - { - return this.GetExternalReferenceV2(); - } - - if (!this.Type.HasValue) - { - throw new ArgumentNullException(nameof(this.Type)); - } - - //if (this.Type == ReferenceType.SecurityScheme) - //{ - // return this.Id; - //} - - return "#/components/" + this.Type.GetDisplayName() + "/" + this.Id; + return this.originalString; } } /// - /// Serialize to Async Api v2.4. + /// Serialize to Async Api. /// public void SerializeV2(IAsyncApiWriter writer) { @@ -97,17 +151,7 @@ public void SerializeV2(IAsyncApiWriter writer) private string GetExternalReferenceV2() { - if (this.Id != null) - { - if (this.IsFragment) - { - return this.ExternalResource + "#" + this.Id; - } - - return this.ExternalResource + "#/components/" + this.Type.GetDisplayName() + "/" + this.Id; - } - - return this.ExternalResource; + return this.ExternalResource + (this.FragmentId != null ? "#" + this.FragmentId : string.Empty); } public void Write(IAsyncApiWriter writer) diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiReferenceEqualityComparer.cs b/src/LEGO.AsyncAPI/Models/AsyncApiReferenceEqualityComparer.cs index cbdf812c..da7abd22 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiReferenceEqualityComparer.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiReferenceEqualityComparer.cs @@ -31,7 +31,7 @@ public bool Equals(IAsyncApiReferenceable x, IAsyncApiReferenceable y) return false; } - return x.Reference.Id == y.Reference.Id; + return x.Reference.FragmentId == y.Reference.FragmentId; } /// @@ -39,7 +39,7 @@ public bool Equals(IAsyncApiReferenceable x, IAsyncApiReferenceable y) /// public int GetHashCode(IAsyncApiReferenceable obj) { - return obj?.Reference?.Id == null ? 0 : obj.Reference.Id.GetHashCode(); + return obj?.Reference?.FragmentId == null ? 0 : obj.Reference.FragmentId.GetHashCode(); } } } \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiSecurityRequirement.cs b/src/LEGO.AsyncAPI/Models/AsyncApiSecurityRequirement.cs index 475497d2..95e93a89 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiSecurityRequirement.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiSecurityRequirement.cs @@ -7,7 +7,7 @@ namespace LEGO.AsyncAPI.Models using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Writers; - public class AsyncApiSecurityRequirement : Dictionary>, IAsyncApiSerializable + public class AsyncApiSecurityRequirement : Dictionary>, IAsyncApiSerializable { /// /// Initializes a new instance of the class. @@ -45,7 +45,7 @@ public void SerializeV2(IAsyncApiWriter writer) } // securityScheme.SerializeV2(writer); - writer.WritePropertyName(securityScheme.Reference.Id); + writer.WritePropertyName(securityScheme.Reference.FragmentId); writer.WriteStartArray(); foreach (var scope in scopes) diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiSecurityScheme.cs b/src/LEGO.AsyncAPI/Models/AsyncApiSecurityScheme.cs index 08fd3d67..4c5bb0ff 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiSecurityScheme.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiSecurityScheme.cs @@ -7,89 +7,60 @@ namespace LEGO.AsyncAPI.Models using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Writers; - public class AsyncApiSecurityScheme : IAsyncApiSerializable, IAsyncApiReferenceable, IAsyncApiExtensible + public class AsyncApiSecurityScheme : IAsyncApiSerializable, IAsyncApiExtensible { /// /// REQUIRED. The type of the security scheme. Valid values are "userPassword", "apiKey", "X509", "symmetricEncryption", "asymmetricEncryption", "httpApiKey", "http", "oauth2", "openIdConnect", "plain", "scramSha256", "scramSha512", and "gssapi". /// - public SecuritySchemeType Type { get; set; } + public virtual SecuritySchemeType Type { get; set; } /// /// A short description for security scheme. CommonMark syntax MAY be used for rich text representation. /// - public string Description { get; set; } + public virtual string Description { get; set; } /// /// REQUIRED. The name of the header, query or cookie parameter to be used. /// - public string Name { get; set; } + public virtual string Name { get; set; } /// /// REQUIRED. The location of the API key. Valid values are "user" and "password" for apiKey and "query", "header" or "cookie" for httpApiKey. /// - public ParameterLocation In { get; set; } + public virtual ParameterLocation In { get; set; } /// /// REQUIRED. The name of the HTTP Authorization scheme to be used /// in the Authorization header as defined in RFC7235. /// - public string Scheme { get; set; } + public virtual string Scheme { get; set; } /// /// A hint to the client to identify how the bearer token is formatted. /// Bearer tokens are usually generated by an authorization server, /// so this information is primarily for documentation purposes. /// - public string BearerFormat { get; set; } + public virtual string BearerFormat { get; set; } /// /// REQUIRED. An object containing configuration information for the flow types supported. /// - public AsyncApiOAuthFlows Flows { get; set; } + public virtual AsyncApiOAuthFlows Flows { get; set; } /// /// REQUIRED. OpenId Connect URL to discover OAuth2 configuration values. /// - public Uri OpenIdConnectUrl { get; set; } + public virtual Uri OpenIdConnectUrl { get; set; } /// /// Specification Extensions. /// - public IDictionary Extensions { get; set; } = new Dictionary(); - - /// - /// Indicates if object is populated with data or is just a reference to the data. - /// - public bool UnresolvedReference { get; set; } - - /// - /// Reference object. - /// - public AsyncApiReference Reference { get; set; } - - /// - /// Serialize to Async Api v2.3. - /// - public void SerializeV2(IAsyncApiWriter writer) - { - if (writer is null) - { - throw new ArgumentNullException(nameof(writer)); - } - - if (this.Reference != null) - { - this.Reference.SerializeV2(writer); - return; - } - - this.SerializeV2WithoutReference(writer); - } + public virtual IDictionary Extensions { get; set; } = new Dictionary(); /// /// Serialize to AsyncApi V2 document without using reference. /// - public void SerializeV2WithoutReference(IAsyncApiWriter writer) + public virtual void SerializeV2(IAsyncApiWriter writer) { writer.WriteStartObject(); @@ -143,47 +114,5 @@ public void SerializeV2WithoutReference(IAsyncApiWriter writer) writer.WriteEndObject(); } - - /// - /// Arbitrarily chooses one object from the - /// to populate in V2 security scheme. - /// - private static void WriteOAuthFlowForV2(IAsyncApiWriter writer, AsyncApiOAuthFlows flows) - { - if (flows != null) - { - if (flows.Implicit != null) - { - WriteOAuthFlowForV2(writer, AsyncApiConstants.Implicit, flows.Implicit); - } - else if (flows.Password != null) - { - WriteOAuthFlowForV2(writer, AsyncApiConstants.Password, flows.Password); - } - else if (flows.ClientCredentials != null) - { - WriteOAuthFlowForV2(writer, AsyncApiConstants.Application, flows.ClientCredentials); - } - else if (flows.AuthorizationCode != null) - { - WriteOAuthFlowForV2(writer, AsyncApiConstants.AccessCode, flows.AuthorizationCode); - } - } - } - - private static void WriteOAuthFlowForV2(IAsyncApiWriter writer, string flowValue, AsyncApiOAuthFlow flow) - { - // flow - writer.WriteOptionalProperty(AsyncApiConstants.Flow, flowValue); - - // authorizationUrl - writer.WriteOptionalProperty(AsyncApiConstants.AuthorizationUrl, flow.AuthorizationUrl?.ToString()); - - // tokenUrl - writer.WriteOptionalProperty(AsyncApiConstants.TokenUrl, flow.TokenUrl?.ToString()); - - // scopes - writer.WriteOptionalMap(AsyncApiConstants.Scopes, flow.Scopes, (w, s) => w.WriteValue(s)); - } } } \ No newline at end of file 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/AsyncApiServer.cs b/src/LEGO.AsyncAPI/Models/AsyncApiServer.cs index 5d4fa071..396b65d9 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiServer.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiServer.cs @@ -2,40 +2,35 @@ namespace LEGO.AsyncAPI.Models { - using System; using System.Collections.Generic; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Writers; - - /// - /// The definition of a server this application MAY connect to. - /// - public class AsyncApiServer : IAsyncApiSerializable, IAsyncApiExtensible, IAsyncApiReferenceable + public class AsyncApiServer : IAsyncApiSerializable, IAsyncApiExtensible { /// /// REQUIRED. A URL to the target host. /// - public string Url { get; set; } + public virtual string Url { get; set; } /// /// REQUIRED. The protocol this URL supports for connection. /// - public string Protocol { get; set; } + public virtual string Protocol { get; set; } /// /// the version of the protocol used for connection. /// - public string ProtocolVersion { get; set; } + public virtual string ProtocolVersion { get; set; } /// /// an optional string describing the host designated by the URL. /// - public string Description { get; set; } + public virtual string Description { get; set; } /// /// a map between a variable name and its value. The value is used for substitution in the server's URL template. /// - public IDictionary Variables { get; set; } = new Dictionary(); + public virtual IDictionary Variables { get; set; } = new Dictionary(); /// /// a declaration of which security mechanisms can be used with this server. The list of values includes alternative security requirement objects that can be used. @@ -43,42 +38,22 @@ public class AsyncApiServer : IAsyncApiSerializable, IAsyncApiExtensible, IAsync /// /// The name used for each property MUST correspond to a security scheme declared in the Security Schemes under the Components Object. /// - public IList Security { get; set; } = new List(); + public virtual IList Security { get; set; } = new List(); /// /// A list of tags for logical grouping and categorization of servers. /// - public IList Tags { get; set; } = new List(); + public virtual IList Tags { get; set; } = new List(); /// /// a map where the keys describe the name of the protocol and the values describe protocol-specific definitions for the server. /// - public AsyncApiBindings Bindings { get; set; } = new AsyncApiBindings(); + public virtual AsyncApiBindings Bindings { get; set; } = new AsyncApiBindings(); /// - public IDictionary Extensions { get; set; } = new Dictionary(); - - public bool UnresolvedReference { get; set; } - - public AsyncApiReference Reference { get; set; } - - 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 virtual IDictionary Extensions { get; set; } = new Dictionary(); - public void SerializeV2WithoutReference(IAsyncApiWriter writer) + public virtual void SerializeV2(IAsyncApiWriter writer) { writer.WriteStartObject(); diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiServerVariable.cs b/src/LEGO.AsyncAPI/Models/AsyncApiServerVariable.cs index ed02634a..57069b99 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiServerVariable.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiServerVariable.cs @@ -10,52 +10,32 @@ namespace LEGO.AsyncAPI.Models /// /// An object representing a Server Variable for server URL template substitution. /// - public class AsyncApiServerVariable : IAsyncApiSerializable, IAsyncApiExtensible, IAsyncApiReferenceable + public class AsyncApiServerVariable : IAsyncApiSerializable, IAsyncApiExtensible { /// /// An enumeration of string values to be used if the substitution options are from a limited set. /// - public IList Enum { get; set; } = new List(); + public virtual IList Enum { get; set; } = new List(); /// /// The default value to use for substitution, and to send, if an alternate value is not supplied. /// - public string Default { get; set; } + public virtual string Default { get; set; } /// /// An optional description for the server variable. CommonMark syntax MAY be used for rich text representation. /// - public string Description { get; set; } + public virtual string Description { get; set; } /// /// An array of examples of the server variable. /// - public IList Examples { get; set; } = new List(); + public virtual IList Examples { get; set; } = new List(); /// - public IDictionary Extensions { get; set; } = new Dictionary(); + public virtual IDictionary Extensions { get; set; } = new Dictionary(); - public bool UnresolvedReference { get; set; } - - public AsyncApiReference Reference { get; set; } - - 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 void SerializeV2WithoutReference(IAsyncApiWriter writer) + public virtual void SerializeV2(IAsyncApiWriter writer) { if (writer is null) { diff --git a/src/LEGO.AsyncAPI/Models/Avro/AsyncApiAvroSchema.cs b/src/LEGO.AsyncAPI/Models/Avro/AsyncApiAvroSchema.cs new file mode 100644 index 00000000..3e1fa291 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/AsyncApiAvroSchema.cs @@ -0,0 +1,45 @@ +// 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 AsyncApiAvroSchema : IAsyncApiSerializable, IAsyncApiMessagePayload + { + 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 static implicit operator AsyncApiAvroSchema(AvroPrimitiveType type) + { + return new AvroPrimitive(type); + } + + public abstract void SerializeV2(IAsyncApiWriter writer); + + public virtual bool TryGetAs(out T result) + where T : AsyncApiAvroSchema + { + result = this as T; + return result != null; + } + + public virtual bool Is() + where T : AsyncApiAvroSchema + { + return this is T; + } + + public virtual T As() + where T : AsyncApiAvroSchema + { + return this as T; + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/Avro/AvroArray.cs b/src/LEGO.AsyncAPI/Models/Avro/AvroArray.cs new file mode 100644 index 00000000..d00e6f79 --- /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 : AsyncApiAvroSchema + { + public override string Type { get; } = "array"; + + /// + /// The schema of the array's items. + /// + public AsyncApiAvroSchema 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 SerializeV2(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..ced40502 --- /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 : AsyncApiAvroSchema + { + 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 SerializeV2(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..15763861 --- /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 AsyncApiAvroSchema 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..e50ec714 --- /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 : AsyncApiAvroSchema + { + 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 SerializeV2(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..ba762d69 --- /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 : AsyncApiAvroSchema + { + 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 SerializeV2(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..fe97023a --- /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 : AsyncApiAvroSchema + { + 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 SerializeV2(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..bbb085a9 --- /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 : AsyncApiAvroSchema + { + 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 SerializeV2(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/AvroUnion.cs b/src/LEGO.AsyncAPI/Models/Avro/AvroUnion.cs new file mode 100644 index 00000000..df5832e1 --- /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 : AsyncApiAvroSchema + { + 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 SerializeV2(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..44b5b8c6 --- /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 SerializeV2(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..cb0f5b3c --- /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 SerializeV2(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..1da08177 --- /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 + { + } +} diff --git a/src/LEGO.AsyncAPI/Models/Interfaces/IAsyncApiReferenceable.cs b/src/LEGO.AsyncAPI/Models/Interfaces/IAsyncApiReferenceable.cs index 9f0bc64c..b1de15ae 100644 --- a/src/LEGO.AsyncAPI/Models/Interfaces/IAsyncApiReferenceable.cs +++ b/src/LEGO.AsyncAPI/Models/Interfaces/IAsyncApiReferenceable.cs @@ -2,23 +2,16 @@ namespace LEGO.AsyncAPI.Models.Interfaces { - using LEGO.AsyncAPI.Writers; - public interface IAsyncApiReferenceable : IAsyncApiSerializable { /// /// Indicates if object is populated with data or is just a reference to the data. /// - bool UnresolvedReference { get; set; } + bool UnresolvedReference { get; } /// /// Reference object. /// AsyncApiReference Reference { get; set; } - - /// - /// Serialize to AsyncAPI V2 document without using reference. - /// - void SerializeV2WithoutReference(IAsyncApiWriter writer); } } \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiSchema.cs b/src/LEGO.AsyncAPI/Models/JsonSchema/AsyncApiJsonSchema.cs similarity index 78% rename from src/LEGO.AsyncAPI/Models/AsyncApiSchema.cs rename to src/LEGO.AsyncAPI/Models/JsonSchema/AsyncApiJsonSchema.cs index 0931c953..3d36eaf1 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiSchema.cs +++ b/src/LEGO.AsyncAPI/Models/JsonSchema/AsyncApiJsonSchema.cs @@ -11,69 +11,69 @@ namespace LEGO.AsyncAPI.Models /// /// The Schema Object allows the definition of input and output data types. /// - public class AsyncApiSchema : IAsyncApiReferenceable, IAsyncApiExtensible, IAsyncApiSerializable + public class AsyncApiJsonSchema : IAsyncApiExtensible, IAsyncApiSerializable, IAsyncApiMessagePayload { /// /// follow JSON Schema definition. Short text providing information about the data. /// - public string Title { get; set; } + public virtual string Title { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public SchemaType? Type { get; set; } + public virtual SchemaType? Type { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public string Format { get; set; } + public virtual string Format { get; set; } /// /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 /// CommonMark syntax MAY be used for rich text representation. /// - public string Description { get; set; } + public virtual string Description { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public double? Maximum { get; set; } + public virtual double? Maximum { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public double? ExclusiveMaximum { get; set; } + public virtual double? ExclusiveMaximum { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public double? Minimum { get; set; } + public virtual double? Minimum { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public double? ExclusiveMinimum { get; set; } + public virtual double? ExclusiveMinimum { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public int? MaxLength { get; set; } + public virtual int? MaxLength { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public int? MinLength { get; set; } + public virtual int? MinLength { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html /// This string SHOULD be a valid regular expression, according to the ECMA 262 regular expression dialect. /// - public string Pattern { get; set; } + public virtual string Pattern { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public double? MultipleOf { get; set; } + public virtual double? MultipleOf { get; set; } /// /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 @@ -81,7 +81,7 @@ public class AsyncApiSchema : IAsyncApiReferenceable, IAsyncApiExtensible, IAsyn /// Unlike JSON Schema, the value MUST conform to the defined type for the Schema Object defined at the same level. /// For example, if type is string, then default can be "foo" but cannot be 1. /// - public AsyncApiAny Default { get; set; } + public virtual AsyncApiAny Default { get; set; } /// /// a value indicating whether relevant only for Schema "properties" definitions. Declares the property as "read only". @@ -91,7 +91,7 @@ public class AsyncApiSchema : IAsyncApiReferenceable, IAsyncApiExtensible, IAsyn /// A property MUST NOT be marked as both readOnly and writeOnly being true. /// Default value is false. /// - public bool ReadOnly { get; set; } + public virtual bool ReadOnly { get; set; } /// /// a value indicating whether relevant only for Schema "properties" definitions. Declares the property as "write only". @@ -101,162 +101,156 @@ public class AsyncApiSchema : IAsyncApiReferenceable, IAsyncApiExtensible, IAsyn /// A property MUST NOT be marked as both readOnly and writeOnly being true. /// Default value is false. /// - public bool WriteOnly { get; set; } + public virtual bool WriteOnly { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html /// Inline or referenced schema MUST be of a Schema Object and not a standard JSON Schema. /// - public IList AllOf { get; set; } = new List(); + public virtual IList AllOf { get; set; } = new List(); /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html /// Inline or referenced schema MUST be of a Schema Object and not a standard JSON Schema. /// - public IList OneOf { get; set; } = new List(); + public virtual IList OneOf { get; set; } = new List(); /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html /// Inline or referenced schema MUST be of a Schema Object and not a standard JSON Schema. /// - public IList AnyOf { get; set; } = new List(); + public virtual IList AnyOf { get; set; } = new List(); /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html /// Inline or referenced schema MUST be of a Schema Object and not a standard JSON Schema. /// - public AsyncApiSchema Not { get; set; } + public virtual AsyncApiJsonSchema Not { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public AsyncApiSchema Contains { get; set; } + public virtual AsyncApiJsonSchema Contains { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public AsyncApiSchema If { get; set; } + public virtual AsyncApiJsonSchema If { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public AsyncApiSchema Then { get; set; } + public virtual AsyncApiJsonSchema Then { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public AsyncApiSchema Else { get; set; } + public virtual AsyncApiJsonSchema Else { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public ISet Required { get; set; } = new HashSet(); + public virtual ISet Required { get; set; } = new HashSet(); /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html /// Value MUST be an object and not an array. Inline or referenced schema MUST be of a Schema Object /// and not a standard JSON Schema. items MUST be present if the type is array. /// - public AsyncApiSchema Items { get; set; } + public virtual AsyncApiJsonSchema Items { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html /// Value MUST be an object and not an array. Inline or referenced schema MUST be of a Schema Object /// and not a standard JSON Schema. items MUST be present if the type is array. /// - public AsyncApiSchema AdditionalItems { get; set; } + public virtual AsyncApiJsonSchema AdditionalItems { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public int? MaxItems { get; set; } + public virtual int? MaxItems { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public int? MinItems { get; set; } + public virtual int? MinItems { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public bool? UniqueItems { get; set; } + public virtual bool? UniqueItems { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html /// Property definitions MUST be a Schema Object and not a standard JSON Schema (inline or referenced). /// - public IDictionary Properties { get; set; } = new Dictionary(); + public virtual IDictionary Properties { get; set; } = new Dictionary(); /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public int? MaxProperties { get; set; } + public virtual int? MaxProperties { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public int? MinProperties { get; set; } + public virtual int? MinProperties { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html /// Value can be boolean or object. Inline or referenced schema /// MUST be of a Schema Object and not a standard JSON Schema. /// - public AsyncApiSchema AdditionalProperties { get; set; } + public virtual AsyncApiJsonSchema AdditionalProperties { get; set; } - public IDictionary PatternProperties { get; set; } = new Dictionary(); + public virtual IDictionary PatternProperties { get; set; } = new Dictionary(); /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public AsyncApiSchema PropertyNames { get; set; } + public virtual AsyncApiJsonSchema PropertyNames { get; set; } /// /// adds support for polymorphism. /// The discriminator is the schema property name that is used to differentiate between other schema that inherit this schema. /// - public string Discriminator { get; set; } + public virtual string Discriminator { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public IList Enum { get; set; } = new List(); + public virtual IList Enum { get; set; } = new List(); /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public IList Examples { get; set; } = new List(); + public virtual IList Examples { get; set; } = new List(); /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public AsyncApiAny Const { get; set; } + public virtual AsyncApiAny Const { get; set; } /// /// a value indicating whether allows sending a null value for the defined schema. Default value is false. /// - public bool Nullable { get; set; } + public virtual bool Nullable { get; set; } /// /// additional external documentation for this schema. /// - public AsyncApiExternalDocumentation ExternalDocs { get; set; } + public virtual AsyncApiExternalDocumentation ExternalDocs { get; set; } /// /// a value indicating whether specifies that a schema is deprecated and SHOULD be transitioned out of usage. /// Default value is false. /// - public bool Deprecated { get; set; } + public virtual bool Deprecated { get; set; } - /// - public bool UnresolvedReference { get; set; } + public virtual IDictionary Extensions { get; set; } = new Dictionary(); - /// - public AsyncApiReference Reference { get; set; } - - public IDictionary Extensions { get; set; } = new Dictionary(); - - public void SerializeV2WithoutReference(IAsyncApiWriter writer) + public virtual void SerializeV2(IAsyncApiWriter writer) { writer.WriteStartObject(); @@ -420,55 +414,5 @@ public void SerializeV2WithoutReference(IAsyncApiWriter writer) writer.WriteEndObject(); } - - public void SerializeV2(IAsyncApiWriter writer) - { - if (writer is null) - { - throw new ArgumentNullException(nameof(writer)); - } - - var target = this; - - var settings = writer.GetSettings(); - - if (this.Reference != null) - { - if (!settings.ShouldInlineReference(this.Reference)) - { - this.Reference.SerializeV2(writer); - return; - } - - // If Loop is detected then just Serialize as a reference. - if (!settings.LoopDetector.PushLoop(this)) - { - settings.LoopDetector.SaveLoop(this); - this.Reference.SerializeV2(writer); - return; - } - - target = this.GetReferenced(this.Reference.HostDocument); - } - - target.SerializeV2WithoutReference(writer); - - if (this.Reference != null) - { - settings.LoopDetector.PopLoop(); - } - } - - public AsyncApiSchema GetReferenced(AsyncApiDocument document) - { - if (this.Reference != null && document != null) - { - return document.ResolveReference(this.Reference); - } - else - { - return this; - } - } } } \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/JsonSchema/FalseApiSchema.cs b/src/LEGO.AsyncAPI/Models/JsonSchema/FalseApiSchema.cs index 01f313e5..669d8499 100644 --- a/src/LEGO.AsyncAPI/Models/JsonSchema/FalseApiSchema.cs +++ b/src/LEGO.AsyncAPI/Models/JsonSchema/FalseApiSchema.cs @@ -5,8 +5,8 @@ namespace LEGO.AsyncAPI.Models /// /// An object representing 'false' for properties of AsyncApiSchema that can be false OR a schema. /// - /// - public class FalseApiSchema : AsyncApiSchema + /// + public class FalseApiSchema : AsyncApiJsonSchema { } } diff --git a/src/LEGO.AsyncAPI/Models/MessagePayloadExtensions.cs b/src/LEGO.AsyncAPI/Models/MessagePayloadExtensions.cs new file mode 100644 index 00000000..069869b6 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/MessagePayloadExtensions.cs @@ -0,0 +1,30 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + using System; + + public static class MessagePayloadExtensions + { + public static bool TryGetAs(this IAsyncApiMessagePayload payload, out T result) + where T : class, IAsyncApiMessagePayload + { + result = payload as T; + return result != null; + } + + public static T As(this IAsyncApiMessagePayload payload) + where T : class, IAsyncApiMessagePayload + { + return payload as T; + } + + public static bool Is(this IAsyncApiMessagePayload payload) + where T : class, IAsyncApiMessagePayload + { + return payload is T; + } + } +} 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/Models/References/AsyncApiAvroSchemaReference.cs b/src/LEGO.AsyncAPI/Models/References/AsyncApiAvroSchemaReference.cs new file mode 100644 index 00000000..decfc0c6 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/References/AsyncApiAvroSchemaReference.cs @@ -0,0 +1,80 @@ +// 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 class AsyncApiAvroSchemaReference : AsyncApiAvroSchema, IAsyncApiReferenceable + { + private AsyncApiAvroSchema target; + + private AsyncApiAvroSchema Target + { + get + { + this.target ??= this.Reference.Workspace?.ResolveReference(this.Reference); + return this.target; + } + } + + public AsyncApiAvroSchemaReference(string reference) + { + this.Reference = new AsyncApiReference(reference, ReferenceType.Schema); + } + + public override string Type => this.Target?.Type; + + public override IDictionary Metadata { get => this.Target?.Metadata; set => this.Target.Metadata = value; } + + public bool UnresolvedReference { get { return this.Target == null; } } + + public AsyncApiReference Reference { get; set; } + + public override T As() + { + if (this.Target == null) + { + return null; + } + return this.Target.As(); + } + + public override bool Is() + { + if (this.Target == null) + { + return false; + } + return this.Target.Is(); + } + + public override bool TryGetAs(out T result) + { + if (this.Target == null) + { + result = default; + return false; + } + return this.Target.TryGetAs(out result); + } + + public override 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.Target.SerializeV2(writer); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/References/AsyncApiBindingsReference{TBinding}.cs b/src/LEGO.AsyncAPI/Models/References/AsyncApiBindingsReference{TBinding}.cs new file mode 100644 index 00000000..378d67d0 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/References/AsyncApiBindingsReference{TBinding}.cs @@ -0,0 +1,135 @@ +// 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 class AsyncApiBindingsReference : AsyncApiBindings, IAsyncApiReferenceable + where TBinding : IBinding + { + public bool UnresolvedReference { get => this.target == null; } + + public AsyncApiReference Reference { get; set; } + + private AsyncApiBindings target; + + private AsyncApiBindings Target + { + get + { + this.target ??= this.Reference.Workspace?.ResolveReference>(this.Reference); + return this.target; + } + } + + public override void Add(TBinding binding) + { + this.Target.Add(binding); + } + + public override ICollection Keys => this.Target.Keys; + + public override ICollection Values => this.Target.Values; + + public override int Count => this.Target.Count; + + public override bool IsReadOnly => this.Target.IsReadOnly; + + public AsyncApiBindingsReference(string reference) + { + ReferenceType type = ReferenceType.None; + if (typeof(TBinding) == typeof(IServerBinding)) + { + type = ReferenceType.ServerBindings; + } + if (typeof(TBinding) == typeof(IMessageBinding)) + { + type = ReferenceType.MessageBindings; + } + if (typeof(TBinding) == typeof(IOperationBinding)) + { + type = ReferenceType.OperationBindings; + } + if (typeof(TBinding) == typeof(IChannelBinding)) + { + type = ReferenceType.ChannelBindings; + } + + if (type == ReferenceType.None) + { + throw new NotImplementedException($"Binding type '{typeof(TBinding)}' not supported."); + } + + this.Reference = new AsyncApiReference(reference, type); + } + + public override 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.Target.SerializeV2(writer); + } + + public override void Add(string key, TBinding value) + { + this.Target.Add(key, value); + } + + public override bool ContainsKey(string key) + { + return this.Target.ContainsKey(key); + } + + public override bool Remove(string key) + { + return this.Target.Remove(key); + } + + public override bool TryGetValue(string key, out TBinding value) + { + return this.Target.TryGetValue(key, out value); + } + + public override void Add(KeyValuePair item) + { + this.Target.Add(item); + } + + public override void Clear() + { + this.Target.Clear(); + } + + public override bool Contains(KeyValuePair item) + { + return this.Target.Contains(item); + } + + public override void CopyTo(KeyValuePair[] array, int arrayIndex) + { + this.Target.CopyTo(array, arrayIndex); + } + + public override bool Remove(KeyValuePair item) + { + return this.Target.Remove(item); + } + + public override IEnumerator> GetEnumerator() + { + return this.Target.GetEnumerator(); + } + } +} diff --git a/src/LEGO.AsyncAPI/Models/References/AsyncApiChannelReference.cs b/src/LEGO.AsyncAPI/Models/References/AsyncApiChannelReference.cs new file mode 100644 index 00000000..437c4e50 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/References/AsyncApiChannelReference.cs @@ -0,0 +1,59 @@ +// 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 AsyncApiChannelReference : AsyncApiChannel, IAsyncApiReferenceable + { + private AsyncApiChannel target; + + private AsyncApiChannel Target + { + get + { + this.target ??= this.Reference.Workspace?.ResolveReference(this.Reference.Reference); + return this.target; + } + } + + public AsyncApiChannelReference(string reference) + { + this.Reference = new AsyncApiReference(reference, ReferenceType.Channel); + } + + public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + + public override IList Servers { get => this.Target?.Servers; set => this.Target.Servers = value; } + + public override AsyncApiOperation Subscribe { get => this.Target?.Subscribe; set => this.Target.Subscribe = value; } + + public override AsyncApiOperation Publish { get => this.Target?.Publish; set => this.Target.Publish = value; } + + public override IDictionary Parameters { get => this.Target?.Parameters; set => this.Target.Parameters = value; } + + public override AsyncApiBindings Bindings { get => this.Target?.Bindings; set => this.Target.Bindings = value; } + + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + + public AsyncApiReference Reference { get; set; } + + public bool UnresolvedReference { get { return this.Target == null; } } + + public override void SerializeV2(IAsyncApiWriter writer) + { + if (!writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV2(writer); + return; + } + else + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV2(writer); + } + } + } +} diff --git a/src/LEGO.AsyncAPI/Models/References/AsyncApiCorrelationIdReference.cs b/src/LEGO.AsyncAPI/Models/References/AsyncApiCorrelationIdReference.cs new file mode 100644 index 00000000..89784131 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/References/AsyncApiCorrelationIdReference.cs @@ -0,0 +1,54 @@ +// 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; + + /// + /// The definition of a correlation ID this application MAY use. + /// + public class AsyncApiCorrelationIdReference : AsyncApiCorrelationId, IAsyncApiReferenceable + { + private AsyncApiCorrelationId target; + + private AsyncApiCorrelationId Target + { + get + { + this.target ??= this.Reference.Workspace?.ResolveReference(this.Reference.Reference); + return this.target; + } + } + + public AsyncApiCorrelationIdReference(string reference) + { + this.Reference = new AsyncApiReference(reference, ReferenceType.CorrelationId); + } + + public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + + public override string Location { get => this.Target?.Location; set => this.Target.Location = value; } + + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + + public AsyncApiReference Reference { get; set; } + + public bool UnresolvedReference { get { return this.Target == null; } } + + public override void SerializeV2(IAsyncApiWriter writer) + { + if (!writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV2(writer); + return; + } + else + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV2(writer); + } + } + } +} diff --git a/src/LEGO.AsyncAPI/Models/References/AsyncApiJsonSchemaReference.cs b/src/LEGO.AsyncAPI/Models/References/AsyncApiJsonSchemaReference.cs new file mode 100644 index 00000000..a7211559 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/References/AsyncApiJsonSchemaReference.cs @@ -0,0 +1,319 @@ +// 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 class AsyncApiJsonSchemaReference : AsyncApiJsonSchema, IAsyncApiReferenceable + { + private AsyncApiJsonSchema target; + + private AsyncApiJsonSchema Target + { + get + { + this.target ??= this.Reference.Workspace?.ResolveReference(this.Reference); + return this.target; + } + } + + public AsyncApiJsonSchemaReference(string reference) + { + this.Reference = new AsyncApiReference(reference, ReferenceType.Schema); + } + + public AsyncApiReference Reference { get; set; } + + public bool UnresolvedReference + { + get { return this.Target == null; } + } + + public override string Title + { + get => this.Target?.Title; + set => this.Target.Title = value; + } + + public override SchemaType? Type + { + get => this.Target?.Type; + set => this.Target.Type = value; + } + + public override string Format + { + get => this.Target?.Format; + set => this.Target.Format = value; + } + + public override string Description + { + get => this.Target?.Description; + set => this.Target.Description = value; + } + + public override double? Maximum + { + get => this.Target?.Maximum; + set => this.Target.Maximum = value; + } + + public override double? ExclusiveMaximum + { + get => this.Target?.ExclusiveMaximum; + set => this.Target.ExclusiveMaximum = value; + } + + public override double? Minimum + { + get => this.Target?.Minimum; + set => this.Target.Minimum = value; + } + + public override double? ExclusiveMinimum + { + get => this.Target?.ExclusiveMinimum; + set => this.Target.ExclusiveMinimum = value; + } + + public override int? MaxLength + { + get => this.Target?.MaxLength; + set => this.Target.MaxLength = value; + } + + public override int? MinLength + { + get => this.Target?.MinLength; + set => this.Target.MinLength = value; + } + + public override string Pattern + { + get => this.Target?.Pattern; + set => this.Target.Pattern = value; + } + + public override double? MultipleOf + { + get => this.Target?.MultipleOf; + set => this.Target.MultipleOf = value; + } + + public override AsyncApiAny Default + { + get => this.Target?.Default; + set => this.Target.Default = value; + } + + public override bool ReadOnly + { + get => this.Target.ReadOnly; + set => this.Target.ReadOnly = value; + } + + public override bool WriteOnly + { + get => this.Target.WriteOnly; + set => this.Target.WriteOnly = value; + } + + public override IList AllOf + { + get => this.Target?.AllOf; + set => this.Target.AllOf = value; + } + + public override IList OneOf + { + get => this.Target?.OneOf; + set => this.Target.OneOf = value; + } + + public override IList AnyOf + { + get => this.Target?.AnyOf; + set => this.Target.AnyOf = value; + } + + public override AsyncApiJsonSchema Not + { + get => this.Target?.Not; + set => this.Target.Not = value; + } + + public override AsyncApiJsonSchema Contains + { + get => this.Target?.Contains; + set => this.Target.Contains = value; + } + + public override AsyncApiJsonSchema If + { + get => this.Target?.If; + set => this.Target.If = value; + } + + public override AsyncApiJsonSchema Then + { + get => this.Target?.Then; + set => this.Target.Then = value; + } + + public override AsyncApiJsonSchema Else + { + get => this.Target?.Else; + set => this.Target.Else = value; + } + + public override ISet Required + { + get => this.Target?.Required; + set => this.Target.Required = value; + } + + public override AsyncApiJsonSchema Items + { + get => this.Target?.Items; + set => this.Target.Items = value; + } + + public override AsyncApiJsonSchema AdditionalItems + { + get => this.Target?.AdditionalItems; + set => this.Target.AdditionalItems = value; + } + + public override int? MaxItems + { + get => this.Target?.MaxItems; + set => this.Target.MaxItems = value; + } + + public override int? MinItems + { + get => this.Target?.MinItems; + set => this.Target.MinItems = value; + } + + public override bool? UniqueItems + { + get => this.Target?.UniqueItems; + set => this.Target.UniqueItems = value; + } + + public override IDictionary Properties + { + get => this.Target?.Properties; + set => this.Target.Properties = value; + } + + public override int? MaxProperties + { + get => this.Target?.MaxProperties; + set => this.Target.MaxProperties = value; + } + + public override int? MinProperties + { + get => this.Target?.MinProperties; + set => this.Target.MinProperties = value; + } + + public override AsyncApiJsonSchema AdditionalProperties + { + get => this.Target?.AdditionalProperties; + set => this.Target.AdditionalProperties = value; + } + + public override IDictionary PatternProperties + { + get => this.Target?.PatternProperties; + set => this.Target.PatternProperties = value; + } + + public override AsyncApiJsonSchema PropertyNames + { + get => this.Target?.PropertyNames; + set => this.Target.PropertyNames = value; + } + + public override string Discriminator + { + get => this.Target?.Discriminator; + set => this.Target.Discriminator = value; + } + + public override IList Enum + { + get => this.Target?.Enum; + set => this.Target.Enum = value; + } + + public override IList Examples + { + get => this.Target?.Examples; + set => this.Target.Examples = value; + } + + public override AsyncApiAny Const + { + get => this.Target?.Const; + set => this.Target.Const = value; + } + + public override bool Nullable + { + get => this.Target.Nullable; + set => this.Target.Nullable = value; + } + + public override AsyncApiExternalDocumentation ExternalDocs + { + get => this.Target?.ExternalDocs; + set => this.Target.ExternalDocs = value; + } + + public override bool Deprecated + { + get => this.Target.Deprecated; + set => this.Target.Deprecated = value; + } + + public override IDictionary Extensions + { + get => this.Target?.Extensions; + set => this.Target.Extensions = value; + } + + public override void SerializeV2(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + var settings = writer.GetSettings(); + if (!settings.ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV2(writer); + return; + } + + this.Reference.Workspace = writer.Workspace; + // If Loop is detected then just Serialize as a reference. + if (!settings.LoopDetector.PushLoop(this)) + { + settings.LoopDetector.SaveLoop(this); + this.Reference.SerializeV2(writer); + return; + } + + this.Target.SerializeV2(writer); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/References/AsyncApiMessageReference.cs b/src/LEGO.AsyncAPI/Models/References/AsyncApiMessageReference.cs new file mode 100644 index 00000000..05aa8ed8 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/References/AsyncApiMessageReference.cs @@ -0,0 +1,80 @@ +// 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; + + /// + /// The definition of a message this application MAY use. + /// + public class AsyncApiMessageReference : AsyncApiMessage, IAsyncApiReferenceable + { + private AsyncApiMessage target; + + private AsyncApiMessage Target + { + get + { + this.target ??= this.Reference.Workspace?.ResolveReference(this.Reference.Reference); + return this.target; + } + } + + public AsyncApiMessageReference(string reference) + { + this.Reference = new AsyncApiReference(reference, ReferenceType.Message); + } + + public override string MessageId { get => this.Target?.MessageId; set => this.Target.MessageId = value; } + + public override AsyncApiJsonSchema Headers { get => this.Target?.Headers; set => this.Target.Headers = value; } + + public override IAsyncApiMessagePayload Payload { get => this.Target?.Payload; set => this.Target.Payload = value; } + + public override AsyncApiCorrelationId CorrelationId { get => this.Target?.CorrelationId; set => this.Target.CorrelationId = value; } + + public override string SchemaFormat { get => this.Target?.SchemaFormat; set => this.Target.SchemaFormat = value; } + + public override string ContentType { get => this.Target?.ContentType; set => this.Target.ContentType = value; } + + public override string Name { get => this.Target?.Name; set => this.Target.Name = value; } + + public override string Title { get => this.Target?.Title; set => this.Target.Title = value; } + + public override string Summary { get => this.Target?.Summary; set => this.Target.Summary = value; } + + public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + + public override IList Tags { get => this.Target?.Tags; set => this.Target.Tags = value; } + + public override AsyncApiExternalDocumentation ExternalDocs { get => this.Target?.ExternalDocs; set => this.Target.ExternalDocs = value; } + + public override AsyncApiBindings Bindings { get => this.Target?.Bindings; set => this.Target.Bindings = value; } + + public override IList Examples { get => this.Target?.Examples; set => this.Target.Examples = value; } + + public override IList Traits { get => this.Target?.Traits; set => this.Target.Traits = value; } + + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + + public AsyncApiReference Reference { get; set; } + + public bool UnresolvedReference { get { return this.Target == null; } } + + public override void SerializeV2(IAsyncApiWriter writer) + { + if (!writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV2(writer); + return; + } + else + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV2(writer); + } + } + } +} diff --git a/src/LEGO.AsyncAPI/Models/References/AsyncApiMessageTraitReference.cs b/src/LEGO.AsyncAPI/Models/References/AsyncApiMessageTraitReference.cs new file mode 100644 index 00000000..118ad4b9 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/References/AsyncApiMessageTraitReference.cs @@ -0,0 +1,76 @@ +// 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; + + /// + /// The definition of a message trait this application MAY use. + /// + public class AsyncApiMessageTraitReference : AsyncApiMessageTrait, IAsyncApiReferenceable + { + private AsyncApiMessageTrait target; + + private AsyncApiMessageTrait Target + { + get + { + this.target ??= this.Reference.Workspace?.ResolveReference(this.Reference); + return this.target; + } + } + + public AsyncApiMessageTraitReference(string reference) + { + this.Reference = new AsyncApiReference(reference, ReferenceType.MessageTrait); + } + + public override string MessageId { get => this.Target?.MessageId; set => this.Target.MessageId = value; } + + public override AsyncApiJsonSchema Headers { get => this.Target?.Headers; set => this.Target.Headers = value; } + + public override AsyncApiCorrelationId CorrelationId { get => this.Target?.CorrelationId; set => this.Target.CorrelationId = value; } + + public override string SchemaFormat { get => this.Target?.SchemaFormat; set => this.Target.SchemaFormat = value; } + + public override string ContentType { get => this.Target?.ContentType; set => this.Target.ContentType = value; } + + public override string Name { get => this.Target?.Name; set => this.Target.Name = value; } + + public override string Title { get => this.Target?.Title; set => this.Target.Title = value; } + + public override string Summary { get => this.Target?.Summary; set => this.Target.Summary = value; } + + public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + + public override IList Tags { get => this.Target?.Tags; set => this.Target.Tags = value; } + + public override AsyncApiExternalDocumentation ExternalDocs { get => this.Target?.ExternalDocs; set => this.Target.ExternalDocs = value; } + + public override AsyncApiBindings Bindings { get => this.Target?.Bindings; set => this.Target.Bindings = value; } + + public override IList Examples { get => this.Target?.Examples; set => this.Target.Examples = value; } + + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + + public AsyncApiReference Reference { get; set; } + + public bool UnresolvedReference { get { return this.Target == null; } } + + public override void SerializeV2(IAsyncApiWriter writer) + { + if (!writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV2(writer); + return; + } + else + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV2(writer); + } + } + } +} diff --git a/src/LEGO.AsyncAPI/Models/References/AsyncApiOperationTraitReference.cs b/src/LEGO.AsyncAPI/Models/References/AsyncApiOperationTraitReference.cs new file mode 100644 index 00000000..36bb3bd9 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/References/AsyncApiOperationTraitReference.cs @@ -0,0 +1,62 @@ +// 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; + + /// + /// The definition of an operation trait this application MAY use. + /// + public class AsyncApiOperationTraitReference : AsyncApiOperationTrait, IAsyncApiReferenceable + { + private AsyncApiOperationTrait target; + + private AsyncApiOperationTrait Target + { + get + { + this.target ??= this.Reference.Workspace?.ResolveReference(this.Reference); + return this.target; + } + } + + public AsyncApiOperationTraitReference(string reference) + { + this.Reference = new AsyncApiReference(reference, ReferenceType.OperationTrait); + } + + public override string OperationId { get => this.Target?.OperationId; set => this.Target.OperationId = value; } + + public override string Summary { get => this.Target?.Summary; set => this.Target.Summary = value; } + + public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + + public override IList Tags { get => this.Target?.Tags; set => this.Target.Tags = value; } + + public override AsyncApiExternalDocumentation ExternalDocs { get => this.Target?.ExternalDocs; set => this.Target.ExternalDocs = value; } + + public override AsyncApiBindings Bindings { get => this.Target?.Bindings; set => this.Target.Bindings = value; } + + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + + public AsyncApiReference Reference { get; set; } + + public bool UnresolvedReference { get { return this.Target == null; } } + + public override void SerializeV2(IAsyncApiWriter writer) + { + if (!writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV2(writer); + return; + } + else + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV2(writer); + } + } + } +} diff --git a/src/LEGO.AsyncAPI/Models/References/AsyncApiParameterReference.cs b/src/LEGO.AsyncAPI/Models/References/AsyncApiParameterReference.cs new file mode 100644 index 00000000..1c570ca0 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/References/AsyncApiParameterReference.cs @@ -0,0 +1,56 @@ +// 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; + + /// + /// The definition of a parameter this application MAY use. + /// + public class AsyncApiParameterReference : AsyncApiParameter, IAsyncApiReferenceable + { + private AsyncApiParameter target; + + private AsyncApiParameter Target + { + get + { + this.target ??= this.Reference.Workspace?.ResolveReference(this.Reference); + return this.target; + } + } + + public AsyncApiParameterReference(string reference) + { + this.Reference = new AsyncApiReference(reference, ReferenceType.Parameter); + } + + public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + + public override AsyncApiJsonSchema Schema { get => this.Target?.Schema; set => this.Target.Schema = value; } + + public override string Location { get => this.Target?.Location; set => this.Target.Location = value; } + + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + + public AsyncApiReference Reference { get; set; } + + public bool UnresolvedReference { get { return this.Target == null; } } + + public override void SerializeV2(IAsyncApiWriter writer) + { + if (!writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV2(writer); + return; + } + else + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV2(writer); + } + } + } +} diff --git a/src/LEGO.AsyncAPI/Models/References/AsyncApiSecuritySchemeReference.cs b/src/LEGO.AsyncAPI/Models/References/AsyncApiSecuritySchemeReference.cs new file mode 100644 index 00000000..7e8440ef --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/References/AsyncApiSecuritySchemeReference.cs @@ -0,0 +1,67 @@ +// 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; + + /// + /// The definition of a security scheme this application MAY use. + /// + public class AsyncApiSecuritySchemeReference : AsyncApiSecurityScheme, IAsyncApiReferenceable + { + private AsyncApiSecurityScheme target; + + private AsyncApiSecurityScheme Target + { + get + { + this.target ??= this.Reference.Workspace?.ResolveReference(this.Reference); + return this.target; + } + } + + public AsyncApiSecuritySchemeReference(string reference) + { + this.Reference = new AsyncApiReference(reference, ReferenceType.SecurityScheme); + } + + public override SecuritySchemeType Type { get => this.Target.Type; set => this.Target.Type = value; } + + public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + + public override string Name { get => this.Target?.Name; set => this.Target.Name = value; } + + public override ParameterLocation In { get => this.Target.In; set => this.Target.In = value; } + + public override string Scheme { get => this.Target?.Scheme; set => this.Target.Scheme = value; } + + public override string BearerFormat { get => this.Target?.BearerFormat; set => this.Target.BearerFormat = value; } + + public override AsyncApiOAuthFlows Flows { get => this.Target?.Flows; set => this.Target.Flows = value; } + + public override Uri OpenIdConnectUrl { get => this.Target?.OpenIdConnectUrl; set => this.Target.OpenIdConnectUrl = value; } + + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + + public AsyncApiReference Reference { get; set; } + + public bool UnresolvedReference { get { return this.Target == null; } } + + public override void SerializeV2(IAsyncApiWriter writer) + { + if (!writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV2(writer); + return; + } + else + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV2(writer); + } + } + } +} diff --git a/src/LEGO.AsyncAPI/Models/References/AsyncApiServerReference.cs b/src/LEGO.AsyncAPI/Models/References/AsyncApiServerReference.cs new file mode 100644 index 00000000..1ef0d510 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/References/AsyncApiServerReference.cs @@ -0,0 +1,66 @@ +// 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; + + /// + /// The definition of a server this application MAY connect to. + /// + public class AsyncApiServerReference : AsyncApiServer, IAsyncApiReferenceable + { + private AsyncApiServer target; + + private AsyncApiServer Target + { + get + { + this.target ??= this.Reference.Workspace?.ResolveReference(this.Reference); + return this.target; + } + } + + public AsyncApiServerReference(string reference) + { + this.Reference = new AsyncApiReference(reference, ReferenceType.Server); + } + + public override string Url { get => this.Target?.Url; set => this.Target.Url = value; } + + public override string Protocol { get => this.Target?.Protocol; set => this.Target.Protocol = value; } + + public override string ProtocolVersion { get => this.Target?.ProtocolVersion; set => this.Target.ProtocolVersion = value; } + + public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + + public override IDictionary Variables { get => this.Target?.Variables; set => this.Target.Variables = value; } + + public override IList Security { get => this.Target?.Security; set => this.Target.Security = value; } + + public override IList Tags { get => this.Target?.Tags; set => this.Target.Tags = value; } + + public override AsyncApiBindings Bindings { get => this.Target?.Bindings; set => this.Target.Bindings = value; } + + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + + public AsyncApiReference Reference { get; set; } + + public bool UnresolvedReference { get { return this.Target == null; } } + + public override void SerializeV2(IAsyncApiWriter writer) + { + if (!writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV2(writer); + return; + } + else + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV2(writer); + } + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/References/AsyncApiServerVariableReference.cs b/src/LEGO.AsyncAPI/Models/References/AsyncApiServerVariableReference.cs new file mode 100644 index 00000000..c5bb968d --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/References/AsyncApiServerVariableReference.cs @@ -0,0 +1,57 @@ +// 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; + + /// + /// The definition of a server variable this application MAY use. + /// + public class AsyncApiServerVariableReference : AsyncApiServerVariable, IAsyncApiReferenceable + { + private AsyncApiServerVariable target; + + private AsyncApiServerVariable Target + { + get + { + this.target ??= this.Reference.Workspace?.ResolveReference(this.Reference); + return this.target; + } + } + + public AsyncApiServerVariableReference(string reference) + { + this.Reference = new AsyncApiReference(reference, ReferenceType.ServerVariable); + } + public override IList Enum { get => this.Target?.Enum; set => this.Target.Enum = value; } + + public override string Default { get => this.Target?.Default; set => this.Target.Default = value; } + + public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + + public override IList Examples { get => this.Target?.Examples; set => this.Target.Examples = value; } + + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + + public AsyncApiReference Reference { get; set; } + + public bool UnresolvedReference { get { return this.Target == null; } } + + public override void SerializeV2(IAsyncApiWriter writer) + { + if (!writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV2(writer); + return; + } + else + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV2(writer); + } + } + } +} diff --git a/src/LEGO.AsyncAPI/Resource.Designer.cs b/src/LEGO.AsyncAPI/Resource.Designer.cs index 983e6ad0..b28de91a 100644 --- a/src/LEGO.AsyncAPI/Resource.Designer.cs +++ b/src/LEGO.AsyncAPI/Resource.Designer.cs @@ -60,6 +60,15 @@ internal Resource() { } } + /// + /// Looks up a localized string similar to Channel signature '{0}' MUST be unique.. + /// + internal static string Validation_ChannelsMustBeUnique { + get { + return ResourceManager.GetString("Validation_ChannelsMustBeUnique", resourceCulture); + } + } + /// /// Looks up a localized string similar to The string '{0}' MUST be an email address.. /// @@ -104,5 +113,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..a97d5939 100644 --- a/src/LEGO.AsyncAPI/Resource.resx +++ b/src/LEGO.AsyncAPI/Resource.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Channel signature '{0}' MUST be unique. + The string '{0}' MUST be an email address. @@ -132,4 +135,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 deleted file mode 100644 index b3c693e0..00000000 --- a/src/LEGO.AsyncAPI/Services/AsyncApiReferenceResolver.cs +++ /dev/null @@ -1,241 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Services -{ - using System; - using System.Collections.Generic; - using System.Linq; - using LEGO.AsyncAPI.Exceptions; - 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 AsyncApiReferenceResolver : AsyncApiVisitorBase - { - private AsyncApiDocument currentDocument; - private List errors = new List(); - - public AsyncApiReferenceResolver(AsyncApiDocument currentDocument) - { - this.currentDocument = currentDocument; - } - - 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); - this.ResolveObject(message.Payload, r => message.Payload = r); - 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) - { - return new() - { - UnresolvedReference = true, - Reference = reference, - }; - } - - try - { - var resolvedReference = this.currentDocument.ResolveReference(reference) as T; - if (resolvedReference == null) - { - throw new AsyncApiException($"Cannot resolve reference '{reference.Reference}' to '{typeof(T).Name}'."); - } - - return resolvedReference; - } - catch (AsyncApiException ex) - { - this.errors.Add(new AsyncApiReferenceError(ex)); - return null; - } - } - - private bool IsUnresolvedReference(IAsyncApiReferenceable possibleReference) - { - return (possibleReference != null && possibleReference.UnresolvedReference); - } - } -} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Services/AsyncApiVisitorBase.cs b/src/LEGO.AsyncAPI/Services/AsyncApiVisitorBase.cs index 899731d0..09ffe62e 100644 --- a/src/LEGO.AsyncAPI/Services/AsyncApiVisitorBase.cs +++ b/src/LEGO.AsyncAPI/Services/AsyncApiVisitorBase.cs @@ -55,6 +55,18 @@ public virtual void Visit(AsyncApiDocument doc) { } + public virtual void Visit(IAsyncApiMessagePayload payload) + { + } + + public virtual void Visit(AsyncApiAvroSchema item) + { + } + + public virtual void Visit(AsyncApiJsonSchema item) + { + } + public virtual void Visit(IDictionary anys) { } @@ -144,13 +156,6 @@ public virtual void Visit(AsyncApiExternalDocumentation externalDocs) { } - /// - /// Visits . - /// - public virtual void Visit(AsyncApiSchema schema) - { - } - public virtual void Visit(AsyncApiMessage message) { } diff --git a/src/LEGO.AsyncAPI/Services/AsyncApiWalker.cs b/src/LEGO.AsyncAPI/Services/AsyncApiWalker.cs index 844ab7e9..2ee2226b 100644 --- a/src/LEGO.AsyncAPI/Services/AsyncApiWalker.cs +++ b/src/LEGO.AsyncAPI/Services/AsyncApiWalker.cs @@ -1,4 +1,4 @@ -// Copyright (c) The LEGO Group. All rights reserved. +// Copyright (c) The LEGO Group. All rights reserved. namespace LEGO.AsyncAPI.Services { @@ -10,7 +10,7 @@ namespace LEGO.AsyncAPI.Services public class AsyncApiWalker { private readonly AsyncApiVisitorBase visitor; - private readonly Stack schemaLoop = new(); + private readonly Stack schemaLoop = new(); public AsyncApiWalker(AsyncApiVisitorBase visitor) { @@ -221,8 +221,9 @@ internal void Walk(AsyncApiOAuthFlow oAuthFlow) internal void Walk(AsyncApiSecurityScheme securityScheme, bool isComponent = false) { - if (securityScheme == null || this.ProcessAsReference(securityScheme, isComponent)) + if (securityScheme is AsyncApiSecuritySchemeReference) { + this.Walk(securityScheme as IAsyncApiReferenceable); return; } @@ -270,22 +271,21 @@ internal void Walk(AsyncApiExternalDocumentation externalDocs) internal void Walk(AsyncApiChannel channel, bool isComponent = false) { - if (channel == null || this.ProcessAsReference(channel, isComponent)) + if (channel == null) { return; } - - this.visitor.Visit(channel); - - if (channel != null) + if (channel is AsyncApiChannelReference) { - this.Walk(AsyncApiConstants.Subscribe, () => this.Walk(channel.Subscribe)); - this.Walk(AsyncApiConstants.Publish, () => this.Walk(channel.Publish)); - - this.Walk(AsyncApiConstants.Bindings, () => this.Walk(channel.Bindings)); - this.Walk(AsyncApiConstants.Parameters, () => this.Walk(channel.Parameters)); + this.Walk(channel as IAsyncApiReferenceable); + return; } + this.Walk(AsyncApiConstants.Subscribe, () => this.Walk(channel.Subscribe)); + this.Walk(AsyncApiConstants.Publish, () => this.Walk(channel.Publish)); + + this.Walk(AsyncApiConstants.Bindings, () => this.Walk(channel.Bindings)); + this.Walk(AsyncApiConstants.Parameters, () => this.Walk(channel.Parameters)); this.Walk(channel as IAsyncApiExtensible); } @@ -311,8 +311,9 @@ private void Walk(IDictionary parameters) internal void Walk(AsyncApiParameter parameter, bool isComponent = false) { - if (parameter == null || this.ProcessAsReference(parameter, isComponent)) + if (parameter is AsyncApiParameterReference reference) { + this.Walk(reference as IAsyncApiReferenceable); return; } @@ -326,13 +327,44 @@ internal void Walk(AsyncApiParameter parameter, bool isComponent = false) this.Walk(parameter as IAsyncApiExtensible); } - internal void Walk(AsyncApiSchema schema, bool isComponent = false) + internal void Walk(IAsyncApiMessagePayload payload) + { + this.visitor.Visit(payload); + if (payload is AsyncApiJsonSchema jsonSchema) + { + this.Walk(AsyncApiConstants.Payload, () => this.Walk((AsyncApiJsonSchema)payload)); + } + + if (payload is AsyncApiAvroSchema avroPayload) + { + this.Walk(AsyncApiConstants.Payload, () => this.Walk(avroPayload)); + } + } + + internal void Walk(AsyncApiAvroSchema schema) + { + if (schema is AsyncApiAvroSchemaReference reference) + { + this.Walk(reference as IAsyncApiReferenceable); + return; + } + + this.visitor.Visit(schema); + } + + internal void Walk(AsyncApiJsonSchema schema, bool isComponent = false) { - if (schema == null || this.ProcessAsReference(schema, isComponent)) + if (schema == null) { return; } + if (schema is AsyncApiJsonSchemaReference reference) + { + this.Walk(reference as IAsyncApiReferenceable); + return; + } + if (this.schemaLoop.Contains(schema)) { return; // Loop detected, this schema has already been walked. @@ -505,8 +537,9 @@ private void Walk(IList traits) internal void Walk(AsyncApiOperationTrait trait, bool isComponent = false) { - if (trait == null || this.ProcessAsReference(trait, isComponent)) + if (trait is AsyncApiOperationTraitReference reference) { + this.Walk(reference as IAsyncApiReferenceable); return; } @@ -524,8 +557,9 @@ internal void Walk(AsyncApiOperationTrait trait, bool isComponent = false) internal void Walk(AsyncApiMessage message, bool isComponent = false) { - if (message == null || this.ProcessAsReference(message, isComponent)) + if (message is AsyncApiMessageReference reference) { + this.Walk(reference as IAsyncApiReferenceable); return; } @@ -534,7 +568,7 @@ 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)); + this.Walk(message.Payload); this.Walk(AsyncApiConstants.CorrelationId, () => this.Walk(message.CorrelationId)); this.Walk(AsyncApiConstants.Tags, () => this.Walk(message.Tags)); this.Walk(AsyncApiConstants.Examples, () => this.Walk(message.Examples)); @@ -567,8 +601,9 @@ private void Walk(IList traits) internal void Walk(AsyncApiMessageTrait trait, bool isComponent = false) { - if (trait == null || this.ProcessAsReference(trait, isComponent)) + if (trait is AsyncApiMessageTraitReference reference) { + this.Walk(reference as IAsyncApiReferenceable); return; } @@ -589,8 +624,9 @@ internal void Walk(AsyncApiMessageTrait trait, bool isComponent = false) internal void Walk(AsyncApiBindings serverBindings, bool isComponent = false) { - if (serverBindings == null || this.ProcessAsReference(serverBindings, isComponent)) + if (serverBindings is AsyncApiBindingsReference reference) { + this.Walk(reference as IAsyncApiReferenceable); return; } @@ -618,8 +654,9 @@ internal void Walk(IServerBinding binding) internal void Walk(AsyncApiBindings channelBindings, bool isComponent = false) { - if (channelBindings == null || this.ProcessAsReference(channelBindings, isComponent)) + if (channelBindings is AsyncApiBindingsReference reference) { + this.Walk(reference as IAsyncApiReferenceable); return; } @@ -647,8 +684,9 @@ internal void Walk(IChannelBinding binding) internal void Walk(AsyncApiBindings operationBindings, bool isComponent = false) { - if (operationBindings == null || this.ProcessAsReference(operationBindings, isComponent)) + if (operationBindings is AsyncApiBindingsReference reference) { + this.Walk(reference as IAsyncApiReferenceable); return; } @@ -676,8 +714,9 @@ internal void Walk(IOperationBinding binding) internal void Walk(AsyncApiBindings messageBindings, bool isComponent = false) { - if (messageBindings == null || this.ProcessAsReference(messageBindings, isComponent)) + if (messageBindings is AsyncApiBindingsReference reference) { + this.Walk(reference as IAsyncApiReferenceable); return; } @@ -761,8 +800,9 @@ internal void Walk(IDictionary anys) internal void Walk(AsyncApiCorrelationId correlationId, bool isComponent = false) { - if (correlationId == null || this.ProcessAsReference(correlationId, isComponent)) + if (correlationId is AsyncApiCorrelationIdReference) { + this.Walk(correlationId as IAsyncApiReferenceable); return; } @@ -822,8 +862,9 @@ internal void Walk(AsyncApiInfo info) internal void Walk(AsyncApiServer server, bool isComponent = false) { - if (server == null || this.ProcessAsReference(server, isComponent)) + if (server is AsyncApiServerReference) { + this.Walk(server as IAsyncApiReferenceable); return; } @@ -861,6 +902,11 @@ internal void Walk(AsyncApiSecurityRequirement securityRequirement) } this.visitor.Visit(securityRequirement); + foreach (var item in securityRequirement.Keys) + { + this.Walk(item as IAsyncApiReferenceable); + } + this.Walk(securityRequirement as IAsyncApiExtensible); } @@ -1042,7 +1088,8 @@ public void Walk(IAsyncApiElement element) case AsyncApiOAuthFlow e: this.Walk(e); break; case AsyncApiOperation e: this.Walk(e); break; case AsyncApiParameter e: this.Walk(e); break; - case AsyncApiSchema e: this.Walk(e); break; + case AsyncApiJsonSchema e: this.Walk(e); break; + case AsyncApiAvroSchema e: this.Walk(e); break; case AsyncApiSecurityRequirement e: this.Walk(e); break; case AsyncApiSecurityScheme e: this.Walk(e); break; case AsyncApiServer e: this.Walk(e); break; diff --git a/src/LEGO.AsyncAPI/Validation/AsyncApiValidator.cs b/src/LEGO.AsyncAPI/Validation/AsyncApiValidator.cs index 3f8bf392..95ecf5c5 100644 --- a/src/LEGO.AsyncAPI/Validation/AsyncApiValidator.cs +++ b/src/LEGO.AsyncAPI/Validation/AsyncApiValidator.cs @@ -125,10 +125,14 @@ public void AddWarning(AsyncApiValidatorWarning warning) public override void Visit(AsyncApiParameter item) => this.Validate(item); /// - /// Execute validation rules against an . + /// Execute validation rules against an . /// /// The object to be validated. - public override void Visit(AsyncApiSchema item) => this.Validate(item); + public override void Visit(AsyncApiJsonSchema item) => this.Validate(item); + + public override void Visit(AsyncApiAvroSchema item) => this.Validate(item); + + public override void Visit(IAsyncApiMessagePayload item) => this.Validate(item); /// /// Execute validation rules against an . @@ -182,7 +186,7 @@ private void Validate(object item, Type type) // Validate unresolved references as references var potentialReference = item as IAsyncApiReferenceable; - if (potentialReference != null && potentialReference.UnresolvedReference) + if (potentialReference != null) { type = typeof(IAsyncApiReferenceable); } diff --git a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiAvroRules.cs b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiAvroRules.cs new file mode 100644 index 00000000..364986bf --- /dev/null +++ b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiAvroRules.cs @@ -0,0 +1,64 @@ +// 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.Models.Interfaces; + using LEGO.AsyncAPI.Validations; + + [AsyncApiRule] + public static class AsyncApiMessagePayloadRules + { + /// + /// The key regex. + /// + public static Regex NameRegex = new Regex(@"^[A-Za-z_][A-Za-z0-9_]*$"); + + public static ValidationRule NameRegularExpression => + new ValidationRule( + (context, messagePayload) => + { + string name = null; + context.Enter("name"); + if (messagePayload is AsyncApiAvroSchema avroPayload) + { + 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..2f759905 100644 --- a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs +++ b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs @@ -2,6 +2,8 @@ namespace LEGO.AsyncAPI.Validation.Rules { + using System; + using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using LEGO.AsyncAPI.Models; @@ -10,10 +12,13 @@ namespace LEGO.AsyncAPI.Validation.Rules [AsyncApiRule] public static class AsyncApiDocumentRules { + private static TimeSpan RegexTimeout = TimeSpan.FromSeconds(1); + /// /// The key regex. /// - public static Regex KeyRegex = new Regex(@"^[a-zA-Z0-9\.\-_]+$"); + public static Regex KeyRegex = new Regex(@"^[a-zA-Z0-9\.\-_]+$", RegexOptions.None, RegexTimeout); + public static Regex ChannelKeyUriTemplateRegex = new Regex(@"^(?:(?:[^\x00-\x20""'<>%\\^`{|}]|%[0-9a-f]{2})|\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?)*\})*$", RegexOptions.IgnoreCase, RegexTimeout); public static ValidationRule DocumentRequiredFields => new ValidationRule( @@ -30,16 +35,57 @@ public static class AsyncApiDocumentRules context.Exit(); context.Enter("channels"); - if (document.Channels == null || !document.Channels.Keys.Any()) + try { - context.CreateError( - nameof(DocumentRequiredFields), - string.Format(Resource.Validation_FieldRequired, "channels", "document")); - } + if (document.Channels == null) + { + context.CreateError( + nameof(DocumentRequiredFields), + string.Format(Resource.Validation_FieldRequired, "channels", "document")); + return; + } + var hashSet = new HashSet(); + foreach (var key in document.Channels.Keys) + { + // Uri-template + if (!ChannelKeyUriTemplateRegex.IsMatch(key)) + { + context.CreateError( + "ChannelKeys", + string.Format(Resource.Validation_KeyMustMatchRegularExpr, key, "channels", KeyRegex.ToString())); + } - context.Exit(); + // Unique channel keys + var pathSignature = GetKeySignature(key); + if (!hashSet.Add(pathSignature)) + { + context.CreateError("ChannelKey", string.Format(Resource.Validation_ChannelsMustBeUnique, pathSignature)); + } + } + } + finally + { + context.Exit(); + } }); + private static string GetKeySignature(string path) + { + for (int openBrace = path.IndexOf('{'); openBrace > -1; openBrace = path.IndexOf('{', openBrace + 2)) + { + int closeBrace = path.IndexOf('}', openBrace); + + if (closeBrace < 0) + { + return path; + } + + path = path.Substring(0, openBrace + 1) + path.Substring(closeBrace); + } + + return path; + } + public static ValidationRule KeyMustBeRegularExpression => new ValidationRule( (context, document) => 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/AsyncApiWriterBase.cs b/src/LEGO.AsyncAPI/Writers/AsyncApiWriterBase.cs index 7cc6ee02..d6d6b750 100644 --- a/src/LEGO.AsyncAPI/Writers/AsyncApiWriterBase.cs +++ b/src/LEGO.AsyncAPI/Writers/AsyncApiWriterBase.cs @@ -2,6 +2,7 @@ namespace LEGO.AsyncAPI.Writers { + using LEGO.AsyncAPI.Models; using System; using System.Collections.Generic; using System.IO; @@ -67,6 +68,8 @@ public AsyncApiWriterBase(TextWriter textWriter, AsyncApiWriterSettings settings /// protected TextWriter Writer { get; } + public AsyncApiWorkspace Workspace { get; set; } = new AsyncApiWorkspace(); + /// /// Write start object. /// 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..c909e168 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. /// diff --git a/src/LEGO.AsyncAPI/Writers/AsyncApiWriterSettings.cs b/src/LEGO.AsyncAPI/Writers/AsyncApiWriterSettings.cs index 2b663bb2..de7b2fb0 100644 --- a/src/LEGO.AsyncAPI/Writers/AsyncApiWriterSettings.cs +++ b/src/LEGO.AsyncAPI/Writers/AsyncApiWriterSettings.cs @@ -21,7 +21,7 @@ static AsyncApiWriterSettings() /// public AsyncApiWriterSettings() { - this.InlineReferences = false; + this.InlineLocalReferences = false; this.LoopDetector = new LoopDetector(); } @@ -46,10 +46,10 @@ public ReferenceInlineSetting ReferenceInline switch (this.referenceInline) { case ReferenceInlineSetting.DoNotInlineReferences: - this.InlineReferences = false; + this.InlineLocalReferences = false; break; case ReferenceInlineSetting.InlineReferences: - this.InlineReferences = true; + this.InlineLocalReferences = true; break; } } @@ -58,7 +58,7 @@ public ReferenceInlineSetting ReferenceInline /// /// Gets or sets a value indicating whether indicates if local references should be rendered as an inline object. /// - public bool InlineReferences { get; set; } + public bool InlineLocalReferences { get; set; } /// /// Figures out if a loop exists. @@ -72,7 +72,12 @@ public ReferenceInlineSetting ReferenceInline /// True if it should be inlined otherwise false. public bool ShouldInlineReference(AsyncApiReference reference) { - return this.InlineReferences; + if (reference.IsExternal) + { + return false; + } + + return this.InlineLocalReferences; } } } diff --git a/src/LEGO.AsyncAPI/Writers/AsyncApiYamlWriter.cs b/src/LEGO.AsyncAPI/Writers/AsyncApiYamlWriter.cs index d0fab76e..0118e2b4 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. /// @@ -32,17 +32,7 @@ static AsyncApiYamlWriter() /// Initializes a new instance of the class. /// /// The text writer. - [Obsolete($"Please use overridden constructor that takes in a {nameof(AsyncApiWriterSettings)} instance.")] - public AsyncApiYamlWriter(TextWriter textWriter) - : this(textWriter, new AsyncApiWriterSettings()) - { - } - - /// - /// 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/IAsyncApiWriter.cs b/src/LEGO.AsyncAPI/Writers/IAsyncApiWriter.cs index 7faff47f..e85bb834 100644 --- a/src/LEGO.AsyncAPI/Writers/IAsyncApiWriter.cs +++ b/src/LEGO.AsyncAPI/Writers/IAsyncApiWriter.cs @@ -1,9 +1,13 @@ // Copyright (c) The LEGO Group. All rights reserved. +using LEGO.AsyncAPI.Models; + namespace LEGO.AsyncAPI.Writers { public interface IAsyncApiWriter { + AsyncApiWorkspace Workspace { get; set; } + /// /// Write the start object. /// 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..3a5ad497 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,11 +43,16 @@ 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; } - public AsyncApiDocumentBuilder WithComponent(string key, AsyncApiSchema schema) + public AsyncApiDocumentBuilder WithComponent(string key, AsyncApiJsonSchema schema) { if (this.document.Components == null) { diff --git a/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs b/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs index 63e818b4..94c09dbe 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; @@ -227,14 +226,7 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() new AsyncApiSecurityRequirement { { - new AsyncApiSecurityScheme() - { - Reference = new AsyncApiReference() - { - Id = "saslScram", - Type = ReferenceType.SecurityScheme, - }, - }, new List() + new AsyncApiSecuritySchemeReference("saslScram"), new List() }, }, }, @@ -267,14 +259,7 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() new AsyncApiSecurityRequirement { { - new AsyncApiSecurityScheme() - { - Reference = new AsyncApiReference() - { - Id = "certs", - Type = ReferenceType.SecurityScheme, - }, - }, new List() + new AsyncApiSecuritySchemeReference("certs"), new List() }, }, }, @@ -306,14 +291,7 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Parameters = new Dictionary { { - "streetlightId", new AsyncApiParameter() - { - Reference = new AsyncApiReference() - { - Id = "streetlightId", - Type = ReferenceType.Parameter, - }, - } + "streetlightId", new AsyncApiParameterReference("#/components/parameters/streetlightId") }, }, Publish = new AsyncApiOperation() @@ -322,25 +300,11 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() OperationId = "receiveLightMeasurement", Traits = new List { - new AsyncApiOperationTrait() - { - Reference = new AsyncApiReference() - { - Id = "kafka", - Type = ReferenceType.OperationTrait, - }, - }, + new AsyncApiOperationTraitReference("#/components/operationTraits/kafka"), }, Message = new List { - new AsyncApiMessage() - { - Reference = new AsyncApiReference() - { - Id = "lightMeasured", - Type = ReferenceType.Message, - }, - }, + new AsyncApiMessageReference("#/components/messages/lightMeasured"), }, }, }) @@ -351,14 +315,7 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Parameters = new Dictionary { { - "streetlightId", new AsyncApiParameter() - { - Reference = new AsyncApiReference() - { - Id = "streetlightId", - Type = ReferenceType.Parameter, - }, - } + "streetlightId", new AsyncApiParameterReference("#/components/parameters/streetlightId") }, }, Subscribe = new AsyncApiOperation() @@ -366,25 +323,11 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() OperationId = "turnOn", Traits = new List { - new AsyncApiOperationTrait() - { - Reference = new AsyncApiReference() - { - Id = "kafka", - Type = ReferenceType.OperationTrait, - }, - }, + new AsyncApiOperationTraitReference("#/components/operationTraits/kafka"), }, Message = new List { - new AsyncApiMessage() - { - Reference = new AsyncApiReference() - { - Id = "turnOnOff", - Type = ReferenceType.Message, - }, - }, + new AsyncApiMessageReference("#/components/messages/turnOnOff"), }, }, }) @@ -395,14 +338,7 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Parameters = new Dictionary { { - "streetlightId", new AsyncApiParameter() - { - Reference = new AsyncApiReference() - { - Id = "streetlightId", - Type = ReferenceType.Parameter, - }, - } + "streetlightId", new AsyncApiParameterReference("#/components/parameters/streetlightId") }, }, Subscribe = new AsyncApiOperation() @@ -410,25 +346,11 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() OperationId = "turnOff", Traits = new List { - new AsyncApiOperationTrait() - { - Reference = new AsyncApiReference() - { - Id = "kafka", - Type = ReferenceType.OperationTrait, - }, - }, + new AsyncApiOperationTraitReference("#/components/operationTraits/kafka"), }, Message = new List { - new AsyncApiMessage() - { - Reference = new AsyncApiReference() - { - Id = "turnOnOff", - Type = ReferenceType.Message, - }, - }, + new AsyncApiMessageReference("#/components/messages/turnOnOff"), }, }, }) @@ -439,14 +361,7 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Parameters = new Dictionary { { - "streetlightId", new AsyncApiParameter() - { - Reference = new AsyncApiReference() - { - Id = "streetlightId", - Type = ReferenceType.Parameter, - }, - } + "streetlightId", new AsyncApiParameterReference("#/components/parameters/streetlightId") }, }, Subscribe = new AsyncApiOperation() @@ -454,25 +369,11 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() OperationId = "dimLight", Traits = new List { - new AsyncApiOperationTrait() - { - Reference = new AsyncApiReference() - { - Id = "kafka", - Type = ReferenceType.OperationTrait, - }, - }, + new AsyncApiOperationTraitReference("#/components/operationTraits/kafka"), }, Message = new List { - new AsyncApiMessage() - { - Reference = new AsyncApiReference() - { - Id = "dimLight", - Type = ReferenceType.Message, - }, - }, + new AsyncApiMessageReference("#/components/messages/dimLight"), }, }, }) @@ -484,23 +385,9 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() ContentType = "application/json", Traits = new List() { - new AsyncApiMessageTrait() - { - Reference = new AsyncApiReference() - { - Type = ReferenceType.MessageTrait, - Id = "commonHeaders", - }, - }, - }, - Payload = new AsyncApiSchema() - { - Reference = new AsyncApiReference() - { - Type = ReferenceType.Schema, - Id = "lightMeasuredPayload", - }, + new AsyncApiMessageTraitReference("#/components/messageTraits/commonHeaders"), }, + Payload = new AsyncApiJsonSchemaReference("#/components/schemas/lightMeasuredPayload"), }) .WithComponent("turnOnOff", new AsyncApiMessage() { @@ -509,23 +396,9 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Summary = "Command a particular streetlight to turn the lights on or off.", Traits = new List() { - new AsyncApiMessageTrait() - { - Reference = new AsyncApiReference() - { - Type = ReferenceType.MessageTrait, - Id = "commonHeaders", - }, - }, - }, - Payload = new AsyncApiSchema() - { - Reference = new AsyncApiReference() - { - Type = ReferenceType.Schema, - Id = "turnOnOffPayload", - }, + new AsyncApiMessageTraitReference("#/components/messageTraits/commonHeaders"), }, + Payload = new AsyncApiJsonSchemaReference("#/components/schemas/turnOnOffPayload"), }) .WithComponent("dimLight", new AsyncApiMessage() { @@ -534,31 +407,17 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Summary = "Command a particular streetlight to dim the lights.", Traits = new List() { - new AsyncApiMessageTrait() - { - Reference = new AsyncApiReference() - { - Type = ReferenceType.MessageTrait, - Id = "commonHeaders", - }, - }, - }, - Payload = new AsyncApiSchema() - { - Reference = new AsyncApiReference() - { - Type = ReferenceType.Schema, - Id = "dimLightPayload", - }, + new AsyncApiMessageTraitReference("#/components/messageTraits/commonHeaders"), }, + Payload = new AsyncApiJsonSchemaReference("#/components/schemas/dimLightPayload"), }) - .WithComponent("lightMeasuredPayload", new AsyncApiSchema() + .WithComponent("lightMeasuredPayload", new AsyncApiJsonSchema() { Type = SchemaType.Object, - Properties = new Dictionary() + Properties = new Dictionary() { { - "lumens", new AsyncApiSchema() + "lumens", new AsyncApiJsonSchema() { Type = SchemaType.Integer, Minimum = 0, @@ -566,24 +425,17 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() } }, { - "sentAt", new AsyncApiSchema() - { - Reference = new AsyncApiReference() - { - Type = ReferenceType.Schema, - Id = "sentAt", - }, - } + "sentAt", new AsyncApiJsonSchemaReference("#/components/schemas/sentAt") }, }, }) - .WithComponent("turnOnOffPayload", new AsyncApiSchema() + .WithComponent("turnOnOffPayload", new AsyncApiJsonSchema() { Type = SchemaType.Object, - Properties = new Dictionary() + Properties = new Dictionary() { { - "command", new AsyncApiSchema() + "command", new AsyncApiJsonSchema() { Type = SchemaType.String, Enum = new List @@ -595,24 +447,17 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() } }, { - "sentAt", new AsyncApiSchema() - { - Reference = new AsyncApiReference() - { - Type = ReferenceType.Schema, - Id = "sentAt", - }, - } + "sentAt", new AsyncApiJsonSchemaReference("#/components/schemas/sentAt") }, }, }) - .WithComponent("dimLightPayload", new AsyncApiSchema() + .WithComponent("dimLightPayload", new AsyncApiJsonSchema() { Type = SchemaType.Object, - Properties = new Dictionary() + Properties = new Dictionary() { { - "percentage", new AsyncApiSchema() + "percentage", new AsyncApiJsonSchema() { Type = SchemaType.Integer, Description = "Percentage to which the light should be dimmed to.", @@ -621,18 +466,11 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() } }, { - "sentAt", new AsyncApiSchema() - { - Reference = new AsyncApiReference() - { - Type = ReferenceType.Schema, - Id = "sentAt", - }, - } + "sentAt", new AsyncApiJsonSchemaReference("#/components/schemas/sentAt") }, }, }) - .WithComponent("sentAt", new AsyncApiSchema() + .WithComponent("sentAt", new AsyncApiJsonSchema() { Type = SchemaType.String, Format = "date-time", @@ -651,20 +489,20 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() .WithComponent("streetlightId", new AsyncApiParameter() { Description = "The ID of the streetlight.", - Schema = new AsyncApiSchema() + Schema = new AsyncApiJsonSchema() { Type = SchemaType.String, }, }) .WithComponent("commonHeaders", new AsyncApiMessageTrait() { - Headers = new AsyncApiSchema() + Headers = new AsyncApiJsonSchema() { Type = SchemaType.Object, - Properties = new Dictionary() + Properties = new Dictionary() { { - "my-app-header", new AsyncApiSchema() + "my-app-header", new AsyncApiJsonSchema() { Type = SchemaType.Integer, Minimum = 0, @@ -681,7 +519,7 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() { "kafka", new KafkaOperationBinding() { - ClientId = new AsyncApiSchema() + ClientId = new AsyncApiJsonSchema() { Type = SchemaType.String, Enum = new List @@ -922,14 +760,8 @@ public void SerializeV2_WithFullSpec_Serializes() new AsyncApiSecurityRequirement { { - new AsyncApiSecurityScheme() - { - Reference = new AsyncApiReference() - { - Id = securitySchemeName, - Type = ReferenceType.SecurityScheme, - }, - }, new List + new AsyncApiSecuritySchemeReference(securitySchemeName), + new List { requirementString, } @@ -1017,7 +849,7 @@ public void SerializeV2_WithFullSpec_Serializes() { Name = traitName, Title = traitTitle, - Headers = new AsyncApiSchema + Headers = new AsyncApiJsonSchema { Title = schemaTitle, WriteOnly = true, @@ -1121,7 +953,7 @@ public void SerializeV2_WithFullSpec_Serializes() }; var outputString = new StringWriter(); - var writer = new AsyncApiYamlWriter(outputString); + var writer = new AsyncApiYamlWriter(outputString, AsyncApiWriterSettings.Default); // Act document.SerializeV2(writer); @@ -1132,9 +964,143 @@ 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() { + var expected = + """ + asyncapi: 2.6.0 + info: + description: test description + servers: + production: + url: example.com + protocol: pulsar+ssl + description: test description + bindings: + $ref: '#/components/serverBindings/bindings' + channels: + testChannel: + $ref: '#/components/channels/otherchannel' + components: + channels: + otherchannel: + publish: + description: test + bindings: + $ref: '#/components/channelBindings/bindings' + serverBindings: + bindings: + pulsar: + tenant: staging + channelBindings: + bindings: + pulsar: + namespace: users + persistence: persistent + """; var doc = new AsyncApiDocument(); doc.Info = new AsyncApiInfo() { @@ -1145,14 +1111,7 @@ public void Serialize_WithBindingReferences_SerializesDeserializes() Description = "test description", Protocol = "pulsar+ssl", Url = "example.com", - Bindings = new AsyncApiBindings() - { - Reference = new AsyncApiReference() - { - Type = ReferenceType.ServerBindings, - Id = "bindings", - }, - }, + Bindings = new AsyncApiBindingsReference("#/components/serverBindings/bindings") }); doc.Components = new AsyncApiComponents() { @@ -1165,14 +1124,7 @@ public void Serialize_WithBindingReferences_SerializesDeserializes() { Description = "test", }, - Bindings = new AsyncApiBindings() - { - Reference = new AsyncApiReference() - { - Type = ReferenceType.ChannelBindings, - Id = "bindings", - }, - }, + Bindings = new AsyncApiBindingsReference("#/components/channelBindings/bindings") } }, }, @@ -1202,22 +1154,26 @@ public void Serialize_WithBindingReferences_SerializesDeserializes() }, }, }; + if (doc.Channels == null) + { + doc.Channels = new Dictionary(); + } doc.Channels.Add( "testChannel", - new AsyncApiChannel - { - Reference = new AsyncApiReference() - { - Type = ReferenceType.Channel, - Id = "otherchannel", - }, - }); + new AsyncApiChannelReference("#/components/channels/otherchannel")); var actual = doc.Serialize(AsyncApiVersion.AsyncApi2_0, AsyncApiFormat.Yaml); + actual.Should().BePlatformAgnosticEquivalentTo(expected); var settings = new AsyncApiReaderSettings(); settings.Bindings = BindingsCollection.Pulsar; var reader = new AsyncApiStringReader(settings); var deserialized = reader.Read(actual, out var diagnostic); + var serverBindings = deserialized.Servers.First().Value.Bindings; + serverBindings.TryGetValue(out var binding); + binding.Tenant.Should().Be("staging"); + + var reserialized = deserialized.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + reserialized.Should().BePlatformAgnosticEquivalentTo(expected); } [Test] @@ -1260,6 +1216,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 @@ -1286,7 +1246,7 @@ public void Serializev2_WithBindings_Serializes() { new HttpMessageBinding { - Headers = new AsyncApiSchema + Headers = new AsyncApiJsonSchema { Description = "this mah binding", }, @@ -1295,7 +1255,7 @@ public void Serializev2_WithBindings_Serializes() { new KafkaMessageBinding { - Key = new AsyncApiSchema + Key = new AsyncApiJsonSchema { Description = "this mah other binding", }, diff --git a/test/LEGO.AsyncAPI.Tests/AsyncApiLicenseTests.cs b/test/LEGO.AsyncAPI.Tests/AsyncApiLicenseTests.cs index 1edcbe37..97fd1634 100644 --- a/test/LEGO.AsyncAPI.Tests/AsyncApiLicenseTests.cs +++ b/test/LEGO.AsyncAPI.Tests/AsyncApiLicenseTests.cs @@ -70,7 +70,7 @@ public void LoadLicense_WithJson_Deserializes() var settings = new AsyncApiReaderSettings(); var context = new ParsingContext(diagnostic, settings); - var node = new MapNode(context, JsonNode.Parse(stream)); + var node = new MapNode(context, JsonNode.Parse(stream).ToJsonString()); // Act var actual = AsyncApiV2Deserializer.LoadLicense(node); diff --git a/test/LEGO.AsyncAPI.Tests/AsyncApiReaderTests.cs b/test/LEGO.AsyncAPI.Tests/AsyncApiReaderTests.cs index 8953f2cd..881b7826 100644 --- a/test/LEGO.AsyncAPI.Tests/AsyncApiReaderTests.cs +++ b/test/LEGO.AsyncAPI.Tests/AsyncApiReaderTests.cs @@ -5,7 +5,6 @@ namespace LEGO.AsyncAPI.Tests using System; using System.Collections.Generic; using System.Linq; - using System.Text.Json.Nodes; using FluentAssertions; using LEGO.AsyncAPI.Exceptions; using LEGO.AsyncAPI.Models; @@ -23,6 +22,32 @@ public void Read_WithMissingEverything_DeserializesWithErrors() var doc = reader.Read(yaml, out var diagnostic); } + [Test] + public void Read_WithComponentBindings_Deserializes() + { + var yaml = @" + asyncapi: 2.6.0 + info: + title: test + version: 1.0.0 + channels: + workspace: + publish: + bindings: + http: + type: response + message: + $ref: '#/components/messages/WorkspaceEventPayload' + components: + messages: + WorkspaceEventPayload: + schemaFormat: application/schema+yaml;version=draft-07 + "; + var reader = new AsyncApiStringReader(); + var doc = reader.Read(yaml, out var diagnostic); + Assert.AreEqual("application/schema+yaml;version=draft-07", doc.Components.Messages["WorkspaceEventPayload"].SchemaFormat); + } + [Test] public void Read_WithExtensionParser_Parses() { @@ -32,6 +57,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 +85,7 @@ public void Read_WithExtensionParser_Parses() { { extensionName, valueExtensionParser }, }, + UnmappedMemberHandling = UnmappedMemberHandling.Ignore, }; var reader = new AsyncApiStringReader(settings); @@ -66,6 +93,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() { @@ -340,7 +457,7 @@ public void Read_WithBasicPlusSecuritySchemeDeserializes() [Test] public void Read_WithWrongReference_AddsError() { - var yaml = + var yaml = """ asyncapi: 2.3.0 info: @@ -360,7 +477,7 @@ public void Read_WithWrongReference_AddsError() var reader = new AsyncApiStringReader(); var doc = reader.Read(yaml, out var diagnostic); diagnostic.Errors.Should().NotBeEmpty(); - doc.Channels.Values.First().Publish.Message.First().Should().BeNull(); + doc.Channels.Values.First().Publish.Message.Should().BeEmpty(); } [Test] @@ -608,7 +725,7 @@ public void Read_WithBasicPlusSecurityRequirementsDeserializes() implicit: authorizationUrl: https://example.com/api/oauth/dialog scopes: - write:pets: modify pets in your account + write:pets: modify pets in your account read:pets: read your pets """; var reader = new AsyncApiStringReader(); diff --git a/test/LEGO.AsyncAPI.Tests/Bindings/BindingExtensions_Should.cs b/test/LEGO.AsyncAPI.Tests/Bindings/BindingExtensions_Should.cs index 3806fc03..a0c8cb88 100644 --- a/test/LEGO.AsyncAPI.Tests/Bindings/BindingExtensions_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Bindings/BindingExtensions_Should.cs @@ -19,11 +19,11 @@ public void TryGetValue_WithChannelBinding_ReturnsBinding() channel.Bindings.Add(new WebSocketsChannelBinding { Method = "POST", - Query = new AsyncApiSchema + Query = new AsyncApiJsonSchema { Description = "this mah query", }, - Headers = new AsyncApiSchema + Headers = new AsyncApiJsonSchema { Description = "this mah binding", }, @@ -74,7 +74,7 @@ public void TryGetValue_WithMessageBinding_ReturnsBinding() message.Bindings.Add(new MQTTMessageBinding { PayloadFormatIndicator = 2, - CorrelationData = new AsyncApiSchema + CorrelationData = new AsyncApiJsonSchema { Description = "Test", }, diff --git a/test/LEGO.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs b/test/LEGO.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs index f2c17c6c..08cd1bee 100644 --- a/test/LEGO.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs @@ -27,7 +27,7 @@ public void HttpMessageBinding_FilledObject_SerializesAndDeserializes() message.Bindings.Add(new HttpMessageBinding { - Headers = new AsyncApiSchema + Headers = new AsyncApiJsonSchema { Description = "this mah binding", }, @@ -65,7 +65,7 @@ public void HttpOperationBinding_FilledObject_SerializesAndDeserializes() { Type = HttpOperationBinding.HttpOperationType.Request, Method = "POST", - Query = new AsyncApiSchema + Query = new AsyncApiJsonSchema { Description = "this mah query", }, diff --git a/test/LEGO.AsyncAPI.Tests/Bindings/Kafka/KafkaBindings_Should.cs b/test/LEGO.AsyncAPI.Tests/Bindings/Kafka/KafkaBindings_Should.cs index 2c5c6f3f..828acedc 100644 --- a/test/LEGO.AsyncAPI.Tests/Bindings/Kafka/KafkaBindings_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Bindings/Kafka/KafkaBindings_Should.cs @@ -128,7 +128,7 @@ public void KafkaMessageBinding_WithFilledObject_SerializesAndDeserializes() message.Bindings.Add(new KafkaMessageBinding { - Key = new AsyncApiSchema + Key = new AsyncApiJsonSchema { Description = "this mah other binding", }, @@ -167,11 +167,11 @@ public void KafkaOperationBinding_WithFilledObject_SerializesAndDeserializes() operation.Bindings.Add(new KafkaOperationBinding { - GroupId = new AsyncApiSchema + GroupId = new AsyncApiJsonSchema { Description = "this mah groupId", }, - ClientId = new AsyncApiSchema + ClientId = new AsyncApiJsonSchema { Description = "this mah clientId", }, 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/Bindings/WebSockets/WebSocketBindings_Should.cs b/test/LEGO.AsyncAPI.Tests/Bindings/WebSockets/WebSocketBindings_Should.cs index c37ada58..203177ac 100644 --- a/test/LEGO.AsyncAPI.Tests/Bindings/WebSockets/WebSocketBindings_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Bindings/WebSockets/WebSocketBindings_Should.cs @@ -8,7 +8,7 @@ namespace LEGO.AsyncAPI.Tests.Bindings.WebSockets using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Readers; using NUnit.Framework; - + public class WebSocketBindings_Should : TestBase { [Test] @@ -30,11 +30,11 @@ public void WebSocketChannelBinding_WithFilledObject_SerializesAndDeserializes() channel.Bindings.Add(new WebSocketsChannelBinding { Method = "POST", - Query = new AsyncApiSchema + Query = new AsyncApiJsonSchema { Description = "this mah query", }, - Headers = new AsyncApiSchema + Headers = new AsyncApiJsonSchema { Description = "this mah binding", }, 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/LEGO.AsyncAPI.Tests.csproj b/test/LEGO.AsyncAPI.Tests/LEGO.AsyncAPI.Tests.csproj index bd35d2ae..46589db0 100644 --- a/test/LEGO.AsyncAPI.Tests/LEGO.AsyncAPI.Tests.csproj +++ b/test/LEGO.AsyncAPI.Tests/LEGO.AsyncAPI.Tests.csproj @@ -10,10 +10,9 @@ - + - diff --git a/test/LEGO.AsyncAPI.Tests/MQTT/MQTTBindings_Should.cs b/test/LEGO.AsyncAPI.Tests/MQTT/MQTTBindings_Should.cs index bbccf86e..2424c21c 100644 --- a/test/LEGO.AsyncAPI.Tests/MQTT/MQTTBindings_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/MQTT/MQTTBindings_Should.cs @@ -119,7 +119,7 @@ public void MQTTMessageBinding_WithFilledObject_SerializesAndDeserializes() message.Bindings.Add(new MQTTMessageBinding { ContentType = "application/json", - CorrelationData = new AsyncApiSchema + CorrelationData = new AsyncApiJsonSchema { Type = SchemaType.String, Format = "uuid", diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiAnyTests.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiAnyTests.cs index 6c93e483..c0b233fc 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiAnyTests.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiAnyTests.cs @@ -1,13 +1,12 @@ // Copyright (c) The LEGO Group. All rights reserved. -using LEGO.AsyncAPI.Models; -using NUnit.Framework; -using System; -using System.Collections.Generic; -using System.Linq; - namespace LEGO.AsyncAPI.Tests { + using System.Collections.Generic; + using System.Linq; + using LEGO.AsyncAPI.Models; + using NUnit.Framework; + public class AsyncApiAnyTests { [Test] diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiChannel_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiChannel_Should.cs index 50c6768b..24f37628 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() @@ -29,8 +29,8 @@ public void AsyncApiChannel_WithInlineParameter_DoesNotCreateReference() - 97845c62-329c-4d87-ad24-4f611b909a10 """; - var channel = new AsyncApiStringReader().ReadFragment(input, AsyncApiVersion.AsyncApi2_0, out var _ ); - channel.Parameters.First().Value.Reference.Should().BeNull(); + var channel = new AsyncApiStringReader().ReadFragment(input, AsyncApiVersion.AsyncApi2_0, out var _); + channel.Parameters.First().Value.Should().NotBeOfType(); } [Test] @@ -59,24 +59,24 @@ public void AsyncApiChannel_WithWebSocketsBinding_Serializes() new WebSocketsChannelBinding() { Method = "POST", - Query = new AsyncApiSchema() + Query = new AsyncApiJsonSchema() { - Properties = new Dictionary() + Properties = new Dictionary() { { - "index", new AsyncApiSchema() + "index", new AsyncApiJsonSchema() { Description = "the index", } }, }, }, - Headers = new AsyncApiSchema() + Headers = new AsyncApiJsonSchema() { - Properties = new Dictionary() + Properties = new Dictionary() { { - "x-correlation-id", new AsyncApiSchema() + "x-correlation-id", new AsyncApiJsonSchema() { Description = "the correlationid", } diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs index a37f1503..c2e7142e 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,12 +104,12 @@ public void AsyncApiMessage_WithNoSchemaFormat_DoesNotSerializeSchemaFormat() """; var message = new AsyncApiMessage(); - message.Payload = new AsyncApiSchema() + message.Payload = new AsyncApiJsonSchema() { - Properties = new Dictionary() + Properties = new Dictionary() { { - "propertyA", new AsyncApiSchema() + "propertyA", new AsyncApiJsonSchema() { Type = SchemaType.String | SchemaType.Null, } @@ -120,17 +120,16 @@ public void AsyncApiMessage_WithNoSchemaFormat_DoesNotSerializeSchemaFormat() // 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_WithSchemaFormat_Serializes() + public void AsyncApiMessage_WithJsonSchemaFormat_Serializes() { // Arrange var expected = @@ -146,12 +145,12 @@ 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 AsyncApiJsonSchema() { - Properties = new Dictionary() + Properties = new Dictionary() { { - "propertyA", new AsyncApiSchema() + "propertyA", new AsyncApiJsonSchema() { Type = SchemaType.String | SchemaType.Null, } @@ -169,6 +168,78 @@ 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 = 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 + var payloadReference = deserializedMessage.Payload as AsyncApiAvroSchemaReference; + payloadReference.UnresolvedReference.Should().BeTrue(); + payloadReference.Reference.Should().NotBeNull(); + payloadReference.Reference.IsExternal.Should().BeTrue(); + payloadReference.Reference.IsFragment.Should().BeTrue(); + + } + [Test] public void AsyncApiMessage_WithFilledObject_Serializes() { @@ -244,7 +315,7 @@ public void AsyncApiMessage_WithFilledObject_Serializes() var message = new AsyncApiMessage { - Headers = new AsyncApiSchema + Headers = new AsyncApiJsonSchema { Title = "HeaderTitle", WriteOnly = true, @@ -257,18 +328,18 @@ public void AsyncApiMessage_WithFilledObject_Serializes() }), }, }, - Payload = new AsyncApiSchema() + Payload = new AsyncApiJsonSchema() { - Properties = new Dictionary + Properties = new Dictionary { { - "propA", new AsyncApiSchema() + "propA", new AsyncApiJsonSchema() { Type = SchemaType.String, } }, { - "propB", new AsyncApiSchema() + "propB", new AsyncApiJsonSchema() { Type = SchemaType.String, } @@ -307,7 +378,7 @@ public void AsyncApiMessage_WithFilledObject_Serializes() { "http", new HttpMessageBinding { - Headers = new AsyncApiSchema + Headers = new AsyncApiJsonSchema { Title = "SchemaTitle", WriteOnly = true, @@ -341,7 +412,7 @@ public void AsyncApiMessage_WithFilledObject_Serializes() { Name = "MessageTraitName", Title = "MessageTraitTitle", - Headers = new AsyncApiSchema + Headers = new AsyncApiJsonSchema { Title = "SchemaTitle", WriteOnly = true, diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiOperation_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiOperation_Should.cs index 5597b211..6c53ad3d 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; @@ -106,7 +105,7 @@ public void AsyncApiOperation_WithBindings_Serializes() { Type = HttpOperationBinding.HttpOperationType.Request, Method = "PUT", - Query = new AsyncApiSchema + Query = new AsyncApiJsonSchema { Description = "some query", }, @@ -115,11 +114,11 @@ public void AsyncApiOperation_WithBindings_Serializes() { new KafkaOperationBinding { - GroupId = new AsyncApiSchema + GroupId = new AsyncApiJsonSchema { Description = "some Id", }, - ClientId = new AsyncApiSchema + ClientId = new AsyncApiJsonSchema { Description = "some Id", }, diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs index c4491e40..3509fc1a 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs @@ -1,16 +1,168 @@ -// Copyright (c) The LEGO Group. All rights reserved. +// Copyright (c) The LEGO Group. All rights reserved. namespace LEGO.AsyncAPI.Tests { + using System; + using System.IO; + using System.Linq; + using System.Threading.Tasks; using FluentAssertions; - using FluentAssertions.Primitives; using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Readers; + using LEGO.AsyncAPI.Readers.Interface; + using LEGO.AsyncAPI.Readers.V2; using NUnit.Framework; - using System.Linq; public class AsyncApiReference_Should : TestBase { + [Test] + public void ReferencePointers() + { + var diag = new AsyncApiDiagnostic(); + var versionService = new AsyncApiV2VersionService(diag); + var externalFragment = versionService.ConvertToAsyncApiReference("https://github.com/test/test#whatever", ReferenceType.None); + var internalFragment = versionService.ConvertToAsyncApiReference("#/components/servers/server1", ReferenceType.None); + var localFile = versionService.ConvertToAsyncApiReference("./local/some/folder/whatever.yaml", ReferenceType.None); + var externalFile = versionService.ConvertToAsyncApiReference("https://github.com/test/whatever.yaml", ReferenceType.None); + + externalFragment.ExternalResource.Should().Be("https://github.com/test/test"); + externalFragment.FragmentId.Should().Be("whatever"); + externalFragment.Reference.Should().Be("https://github.com/test/test#whatever"); + externalFragment.IsFragment.Should().BeTrue(); + externalFragment.IsExternal.Should().BeTrue(); + + internalFragment.ExternalResource.Should().BeNull(); + internalFragment.FragmentId.Should().Be("/components/servers/server1"); + internalFragment.Reference.Should().Be("#/components/servers/server1"); + internalFragment.IsFragment.Should().BeTrue(); + internalFragment.IsExternal.Should().BeFalse(); + + localFile.ExternalResource.Should().Be("./local/some/folder/whatever.yaml"); + localFile.Reference.Should().Be("./local/some/folder/whatever.yaml"); + localFile.FragmentId.Should().Be(null); + localFile.IsFragment.Should().BeFalse(); + + externalFile.ExternalResource.Should().Be("https://github.com/test/whatever.yaml"); + externalFile.Reference.Should().Be("https://github.com/test/whatever.yaml"); + externalFile.FragmentId.Should().Be(null); + externalFile.IsFragment.Should().BeFalse(); + } + + [Test] + public void Reference() + { + var json = + """ + { + "asyncapi": "2.6.0", + "info": { }, + "servers": { + "production": { + "$ref": "https://github.com/test/test#whatever" + } + } + } + """; + + var doc = new AsyncApiStringReader().Read(json, out var diag); + var reference = doc.Servers.First().Value as AsyncApiServerReference; + reference.Reference.ExternalResource.Should().Be("https://github.com/test/test"); + reference.Reference.FragmentId.Should().Be("whatever"); + reference.Reference.IsFragment.Should().BeTrue(); + } + + [Test] + public void ExternalFragmentReference_ResolvesFragtment() + { + var externalJson = + """ + { + "servers": [ + { + "url": "wss://production.gigantic-server.com:443", + "protocol": "wss", + "protocolVersion": "1.0.0", + "description": "The production API server", + "variables": { + "username": { + "default": "demo", + "description": "This value is assigned by the service provider" + }, + "password": { + "default": "demo", + "description": "This value is assigned by the service provider" + } + } + } + ] + } + """; + + var json = + """ + { + "asyncapi": "2.6.0", + "info": { }, + "servers": { + "production": { + "$ref": "whatever/whatever.json#/servers/0" + } + } + } + """; + + var doc = new AsyncApiStringReader(new AsyncApiReaderSettings { ReferenceResolution = ReferenceResolutionSetting.ResolveAllReferences, ExternalReferenceLoader = new MockStringLoader(externalJson) }).Read(json, out var diag); + var reference = doc.Servers.First().Value as AsyncApiServerReference; + reference.Reference.FragmentId.Should().Be("/servers/0"); + reference.Reference.IsFragment.Should().BeTrue(); + reference.Url.Should().Be("wss://production.gigantic-server.com:443"); + } + + [Test] + public void ServerReference_WithComponentReference_ResolvesReference() + { + var json = + """ + { + "asyncapi": "2.6.0", + "info": { }, + "servers": { + "production": { + "$ref": "#/components/servers/whatever" + } + }, + "components": { + "servers": { + "whatever": { + "url": "wss://production.gigantic-server.com:443", + "protocol": "wss", + "protocolVersion": "1.0.0", + "description": "The production API server", + "variables": { + "username": { + "default": "demo", + "description": "This value is assigned by the service provider" + }, + "password": { + "default": "demo", + "description": "This value is assigned by the service provider" + } + } + } + } + } + } + """; + + var doc = new AsyncApiStringReader().Read(json, out var diag); + var reference = doc.Servers.First().Value as AsyncApiServerReference; + reference.Reference.ExternalResource.Should().BeNull(); + reference.Reference.FragmentId.Should().Be("/components/servers/whatever"); + reference.Reference.IsFragment.Should().BeTrue(); + reference.Url.Should().Be("wss://production.gigantic-server.com:443"); + + } + [Test] public void AsyncApiReference_WithExternalFragmentUriReference_AllowReference() { @@ -26,11 +178,12 @@ 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.FragmentId.Should().Be("/path/to/external/fragment"); reference.IsFragment.Should().BeTrue(); reference.IsExternal.Should().BeTrue(); reference.Type.Should().Be(ReferenceType.Schema); @@ -54,13 +207,14 @@ 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(); - reference.IsFragment.Should().BeTrue(); + reference.FragmentId.Should().BeNull(); + reference.IsFragment.Should().BeFalse(); reference.IsExternal.Should().BeTrue(); var expected = deserialized.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); actual.Should() @@ -82,11 +236,12 @@ 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"); - reference.IsFragment.Should().BeFalse(); + reference.FragmentId.Should().Be("/components/schemas/test"); + reference.IsFragment.Should().BeTrue(); reference.IsExternal.Should().BeFalse(); var expected = deserialized.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); @@ -109,9 +264,10 @@ 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.FragmentId.Should().Be("/fragment"); reference.IsFragment.Should().BeTrue(); reference.IsExternal.Should().BeTrue(); @@ -135,11 +291,12 @@ 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"); - reference.IsFragment.Should().BeFalse(); + reference.FragmentId.Should().Be("/components/schemas/test"); + reference.IsFragment.Should().BeTrue(); reference.IsExternal.Should().BeTrue(); var expected = deserialized.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); @@ -167,7 +324,7 @@ public void AsyncApiDocument_WithInternalComponentReference_ResolvesReference() var settings = new AsyncApiReaderSettings() { - ReferenceResolution = ReferenceResolutionSetting.ResolveReferences, + ReferenceResolution = ReferenceResolutionSetting.ResolveInternalReferences, }; var reader = new AsyncApiStringReader(settings); @@ -176,33 +333,33 @@ public void AsyncApiDocument_WithInternalComponentReference_ResolvesReference() // Assert diagnostic.Errors.Should().BeEmpty(); - var channel = deserialized.Channels.First().Value; + var channel = deserialized.Channels.First().Value as AsyncApiChannelReference; channel.UnresolvedReference.Should().BeFalse(); channel.Description.Should().Be("customDescription"); channel.Reference.ExternalResource.Should().BeNull(); - channel.Reference.Id.Should().Be("myChannel"); + channel.Reference.FragmentId.Should().Be("/components/channels/myChannel"); channel.Reference.IsExternal.Should().BeFalse(); channel.Reference.Type.Should().Be(ReferenceType.Channel); } [Test] - public void AsyncApiDocument_WithExternalReference_DoesNotResolve() + 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 - """; + 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.ResolveReferences, + ReferenceResolution = ReferenceResolutionSetting.ResolveInternalReferences, }; var reader = new AsyncApiStringReader(settings); @@ -211,13 +368,13 @@ public void AsyncApiDocument_WithExternalReference_DoesNotResolve() // Assert diagnostic.Errors.Should().BeEmpty(); - var channel = deserialized.Channels.First().Value; + var channel = deserialized.Channels.First().Value as AsyncApiChannelReference; channel.UnresolvedReference.Should().BeTrue(); channel.Description.Should().BeNull(); channel.Reference.ExternalResource.Should().Be("http://example.com/channel.json"); channel.Reference.Type.Should().Be(ReferenceType.Channel); - channel.Reference.Id.Should().BeNull(); + channel.Reference.FragmentId.Should().BeNull(); channel.Reference.IsExternal.Should().BeTrue(); channel.Reference.IsFragment.Should().BeFalse(); } @@ -237,9 +394,10 @@ 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.FragmentId.Should().BeNull(); reference.IsExternal.Should().BeTrue(); reference.IsFragment.Should().BeFalse(); diagnostic.Errors.Should().BeEmpty(); @@ -250,5 +408,188 @@ 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, + ExternalReferenceLoader = new MockJsonSchemaLoader(), + }; + 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(1); + } + + [Test] + public void AsyncApiReference_DocumentLevelReferencePointer_DeserializesCorrectly() + { + var yaml = """ + asyncapi: 2.3.0 + info: + title: test + version: 1.0.0 + channels: + workspace: + publish: + message: + title: test message + other: + $ref: "#/channels/workspace" + """; + + var reader = new AsyncApiStringReader(); + var doc = reader.Read(yaml, out var diagnostic); + doc.Channels.Should().HaveCount(2); + doc.Channels["other"].Publish.Message.First().Title.Should().Be("test message"); + } + + [Test] + public void AsyncApiReference_WithExternalAvroResource_DeserializesCorrectly() + { + var avroPayload = + """ + { + "type": "record", + "name": "thecodebuzz_schema", + "namespace": "thecodebuzz.avro", + "fields": [ + { + "name": "username", + "type": "string", + "doc": "Name of the user account on Thecodebuzz.com" + }, + { + "name": "email", + "type": "string", + "doc": "The email of the user logging message on the blog" + }, + { + "name": "timestamp", + "type": "long", + "doc": "time in seconds" + } + ], + "doc:": "A basic schema for storing thecodebuzz blogs messages" + } + """; + + var yaml = """ + asyncapi: 2.6.0 + info: + title: test + version: 1.0.0 + channels: + workspace: + publish: + message: + payload: + $ref: ./some/path/to/external/payload.json + schemaFormat: application/vnd.apache.avro + name: Test + title: Test message + summary: Test. + """; + var settings = new AsyncApiReaderSettings + { + ReferenceResolution = ReferenceResolutionSetting.ResolveAllReferences, + ExternalReferenceLoader = new MockStringLoader(avroPayload), + }; + var reader = new AsyncApiStringReader(settings); + var doc = reader.Read(yaml, out var diagnostic); + var message = doc.Channels["workspace"].Publish.Message.First(); + var payload = message.Payload.As(); + payload.As().Name.Should().Be("thecodebuzz_schema"); + + doc.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0).Should() + .BePlatformAgnosticEquivalentTo(yaml); + + } + } + + public class MockStringLoader : IStreamLoader + { + public MockStringLoader(string input) + { + this.input = input; + } + + private readonly string input; + + public Stream Load(Uri uri) + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(this.input); + writer.Flush(); + stream.Position = 0; + return stream; + } + + public Task LoadAsync(Uri uri) + { + return Task.FromResult(this.Load(uri)); + } + } + + public class MockJsonSchemaLoader : IStreamLoader + { + const string Message = + """ + 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" + """; + + const string Schema = + """ + type: object + properties: + lumens: + type: integer + minimum: 0 + description: Light intensity measured in lumens. + """; + + public Stream Load(Uri uri) + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + if (uri.ToString() == "./some/path/to/external/message.yaml") + { + writer.Write(Message); + } + else + { + writer.Write(Schema); + } + writer.Flush(); + stream.Position = 0; + return stream; + } + + public Task LoadAsync(Uri uri) + { + return Task.FromResult(this.Load(uri)); + } } } \ 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..af2badf8 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiSchema_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiSchema_Should.cs @@ -1,10 +1,9 @@ -// Copyright (c) The LEGO Group. All rights reserved. +// Copyright (c) The LEGO Group. All rights reserved. namespace LEGO.AsyncAPI.Tests.Models { using System; using System.Collections.Generic; - using System.Globalization; using System.IO; using FluentAssertions; using LEGO.AsyncAPI.Models; @@ -14,9 +13,9 @@ namespace LEGO.AsyncAPI.Tests.Models public class AsyncApiSchema_Should : TestBase { - public static AsyncApiSchema BasicSchema = new AsyncApiSchema(); + public static AsyncApiJsonSchema BasicSchema = new AsyncApiJsonSchema(); - public static AsyncApiSchema AdvancedSchemaNumber = new AsyncApiSchema + public static AsyncApiJsonSchema AdvancedSchemaNumber = new AsyncApiJsonSchema { Title = "title1", MultipleOf = 3, @@ -32,7 +31,7 @@ public class AsyncApiSchema_Should : TestBase }, }; - public static AsyncApiSchema AdvancedSchemaBigNumbers = new AsyncApiSchema + public static AsyncApiJsonSchema AdvancedSchemaBigNumbers = new AsyncApiJsonSchema { Title = "title1", MultipleOf = 3, @@ -48,20 +47,20 @@ public class AsyncApiSchema_Should : TestBase }, }; - public static AsyncApiSchema AdvancedSchemaObject = new AsyncApiSchema + public static AsyncApiJsonSchema AdvancedSchemaObject = new AsyncApiJsonSchema { Title = "title1", - Properties = new Dictionary + Properties = new Dictionary { - ["property1"] = new AsyncApiSchema + ["property1"] = new AsyncApiJsonSchema { - Properties = new Dictionary + Properties = new Dictionary { - ["property2"] = new AsyncApiSchema + ["property2"] = new AsyncApiJsonSchema { Type = SchemaType.Integer, }, - ["property3"] = new AsyncApiSchema + ["property3"] = new AsyncApiJsonSchema { Type = SchemaType.String, MaxLength = 15, @@ -71,78 +70,78 @@ public class AsyncApiSchema_Should : TestBase Items = new FalseApiSchema(), AdditionalItems = new FalseApiSchema(), }, - ["property4"] = new AsyncApiSchema + ["property4"] = new AsyncApiJsonSchema { - Properties = new Dictionary + Properties = new Dictionary { - ["property5"] = new AsyncApiSchema + ["property5"] = new AsyncApiJsonSchema { - Properties = new Dictionary + Properties = new Dictionary { - ["property6"] = new AsyncApiSchema + ["property6"] = new AsyncApiJsonSchema { Type = SchemaType.Boolean, }, }, }, - ["property7"] = new AsyncApiSchema + ["property7"] = new AsyncApiJsonSchema { Type = SchemaType.String, MinLength = 2, }, }, - PatternProperties = new Dictionary() + PatternProperties = new Dictionary() { { "^S_", - new AsyncApiSchema() + new AsyncApiJsonSchema() { Type = SchemaType.String, } }, { - "^I_", new AsyncApiSchema() + "^I_", new AsyncApiJsonSchema() { Type = SchemaType.Integer, } }, }, - PropertyNames = new AsyncApiSchema() + PropertyNames = new AsyncApiJsonSchema() { Pattern = "^[A-Za-z_][A-Za-z0-9_]*$", }, - AdditionalProperties = new AsyncApiSchema + AdditionalProperties = new AsyncApiJsonSchema { - Properties = new Dictionary + Properties = new Dictionary { - ["Property8"] = new AsyncApiSchema + ["Property8"] = new AsyncApiJsonSchema { Type = SchemaType.String | SchemaType.Null, }, }, }, - Items = new AsyncApiSchema + Items = new AsyncApiJsonSchema { - Properties = new Dictionary + Properties = new Dictionary { - ["Property9"] = new AsyncApiSchema + ["Property9"] = new AsyncApiJsonSchema { Type = SchemaType.String | SchemaType.Null, }, }, }, - AdditionalItems = new AsyncApiSchema + AdditionalItems = new AsyncApiJsonSchema { - Properties = new Dictionary + Properties = new Dictionary { - ["Property10"] = new AsyncApiSchema + ["Property10"] = new AsyncApiJsonSchema { Type = SchemaType.String | SchemaType.Null, }, }, }, }, - ["property11"] = new AsyncApiSchema + ["property11"] = new AsyncApiJsonSchema { Const = new AsyncApiAny("aSpecialConstant"), }, @@ -154,43 +153,43 @@ public class AsyncApiSchema_Should : TestBase }, }; - public static AsyncApiSchema AdvancedSchemaWithAllOf = new AsyncApiSchema + public static AsyncApiJsonSchema AdvancedSchemaWithAllOf = new AsyncApiJsonSchema { Title = "title1", - AllOf = new List + AllOf = new List { - new AsyncApiSchema + new AsyncApiJsonSchema { Title = "title2", - Properties = new Dictionary + Properties = new Dictionary { - ["property1"] = new AsyncApiSchema + ["property1"] = new AsyncApiJsonSchema { Type = SchemaType.Integer, }, - ["property2"] = new AsyncApiSchema + ["property2"] = new AsyncApiJsonSchema { Type = SchemaType.String, MaxLength = 15, }, }, }, - new AsyncApiSchema + new AsyncApiJsonSchema { Title = "title3", - Properties = new Dictionary + Properties = new Dictionary { - ["property3"] = new AsyncApiSchema + ["property3"] = new AsyncApiJsonSchema { - Properties = new Dictionary + Properties = new Dictionary { - ["property4"] = new AsyncApiSchema + ["property4"] = new AsyncApiJsonSchema { Type = SchemaType.Boolean , }, }, }, - ["property5"] = new AsyncApiSchema + ["property5"] = new AsyncApiJsonSchema { Type = SchemaType.String, MinLength = 2, @@ -206,7 +205,7 @@ public class AsyncApiSchema_Should : TestBase }, }; - public static AsyncApiSchema ReferencedSchema = new AsyncApiSchema + public static AsyncApiJsonSchema ReferencedSchema = new AsyncApiJsonSchema { Title = "title1", MultipleOf = 3, @@ -221,30 +220,24 @@ public class AsyncApiSchema_Should : TestBase { Url = new Uri("http://example.com/externalDocs"), }, - - Reference = new AsyncApiReference - { - Type = ReferenceType.Schema, - Id = "schemaObject1", - }, }; - public static AsyncApiSchema AdvancedSchemaWithRequiredPropertiesObject = new AsyncApiSchema + public static AsyncApiJsonSchema AdvancedSchemaWithRequiredPropertiesObject = new AsyncApiJsonSchema { Title = "title1", Required = new HashSet() { "property1" }, - Properties = new Dictionary + Properties = new Dictionary { - ["property1"] = new AsyncApiSchema + ["property1"] = new AsyncApiJsonSchema { Required = new HashSet() { "property3" }, - Properties = new Dictionary + Properties = new Dictionary { - ["property2"] = new AsyncApiSchema + ["property2"] = new AsyncApiJsonSchema { Type = SchemaType.Integer, }, - ["property3"] = new AsyncApiSchema + ["property3"] = new AsyncApiJsonSchema { Type = SchemaType.String, MaxLength = 15, @@ -253,21 +246,21 @@ public class AsyncApiSchema_Should : TestBase }, ReadOnly = true, }, - ["property4"] = new AsyncApiSchema + ["property4"] = new AsyncApiJsonSchema { - Properties = new Dictionary + Properties = new Dictionary { - ["property5"] = new AsyncApiSchema + ["property5"] = new AsyncApiJsonSchema { - Properties = new Dictionary + Properties = new Dictionary { - ["property6"] = new AsyncApiSchema + ["property6"] = new AsyncApiJsonSchema { Type = SchemaType.Boolean, }, }, }, - ["property7"] = new AsyncApiSchema + ["property7"] = new AsyncApiJsonSchema { Type = SchemaType.String, MinLength = 2, @@ -371,11 +364,11 @@ public void SerializeAsJson_WithAdvancedSchemaObject_V2Works() public void Deserialize_WithAdvancedSchema_Works() { // Arrange - var json = GetTestData(); + var json = this.GetTestData(); var expected = AdvancedSchemaObject; // Act - var actual = new AsyncApiStringReader().ReadFragment(json, AsyncApiVersion.AsyncApi2_0, out var _diagnostics); + var actual = new AsyncApiStringReader().ReadFragment(json, AsyncApiVersion.AsyncApi2_0, out var _diagnostics); // Assert actual.Should().BeEquivalentTo(expected); @@ -421,34 +414,34 @@ public void Serialize_WithInliningOptions_ShouldInlineAccordingly(bool shouldInl { new AsyncApiMessage { - Payload = new AsyncApiSchema + Payload = new AsyncApiJsonSchema { Type = SchemaType.Object, Required = new HashSet { "testB" }, - Properties = new Dictionary + Properties = new Dictionary { - { "testC", new AsyncApiSchema { Reference = new AsyncApiReference { Type = ReferenceType.Schema, Id = "testC" } } }, - { "testB", new AsyncApiSchema { Reference = new AsyncApiReference { Type = ReferenceType.Schema, Id = "testB" } } }, + { "testC", new AsyncApiJsonSchemaReference("#/components/schemas/testC") }, + { "testB", new AsyncApiJsonSchemaReference("#/components/schemas/testB") }, }, }, }, }, }, }) - .WithComponent("testD", new AsyncApiSchema() { Type = SchemaType.String, Format = "uuid" }) - .WithComponent("testC", new AsyncApiSchema() + .WithComponent("testD", new AsyncApiJsonSchema() { Type = SchemaType.String, Format = "uuid" }) + .WithComponent("testC", new AsyncApiJsonSchema() { Type = SchemaType.Object, - Properties = new Dictionary + Properties = new Dictionary { - { "testD", new AsyncApiSchema { Reference = new AsyncApiReference { Type = ReferenceType.Schema, Id = "testD" } } }, + { "testD", new AsyncApiJsonSchemaReference("#/components/schemas/testD") }, }, }) - .WithComponent("testB", new AsyncApiSchema() { Description = "test", Type = SchemaType.Boolean }) + .WithComponent("testB", new AsyncApiJsonSchema() { Description = "test", Type = SchemaType.Boolean }) .Build(); var outputString = new StringWriter(); - var writer = new AsyncApiYamlWriter(outputString, new AsyncApiWriterSettings { InlineReferences = shouldInline }); + var writer = new AsyncApiYamlWriter(outputString, new AsyncApiWriterSettings { InlineLocalReferences = shouldInline }); // Act asyncApiDocument.SerializeV2(writer); @@ -482,10 +475,10 @@ public void SerializeV2_WithNullWriter_Throws() [Test] public void Serialize_WithOneOf_DoesNotWriteThen() { - var mainSchema = new AsyncApiSchema(); - var subSchema = new AsyncApiSchema(); - subSchema.Properties.Add("title", new AsyncApiSchema() { Type = SchemaType.String }); - mainSchema.OneOf = new List() { subSchema }; + var mainSchema = new AsyncApiJsonSchema(); + var subSchema = new AsyncApiJsonSchema(); + subSchema.Properties.Add("title", new AsyncApiJsonSchema() { Type = SchemaType.String }); + mainSchema.OneOf = new List() { subSchema }; var yaml = mainSchema.Serialize(AsyncApiVersion.AsyncApi2_0, AsyncApiFormat.Yaml); @@ -500,16 +493,38 @@ public void Serialize_WithOneOf_DoesNotWriteThen() [Test] public void Serialize_WithAnyOf_DoesNotWriteIf() { - var mainSchema = new AsyncApiSchema(); - var subSchema = new AsyncApiSchema(); - subSchema.Properties.Add("title", new AsyncApiSchema() { Type = SchemaType.String }); - mainSchema.AnyOf = new List() { subSchema }; + var mainSchema = new AsyncApiJsonSchema(); + var subSchema = new AsyncApiJsonSchema(); + subSchema.Properties.Add("title", new AsyncApiJsonSchema() { Type = SchemaType.String }); + mainSchema.AnyOf = new List() { subSchema }; var yaml = mainSchema.Serialize(AsyncApiVersion.AsyncApi2_0, AsyncApiFormat.Yaml); 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. @@ -517,9 +532,9 @@ public void Serialize_WithAnyOf_DoesNotWriteIf() [Test] public void Serialize_WithNot_DoesNotWriteElse() { - var mainSchema = new AsyncApiSchema(); - var subSchema = new AsyncApiSchema(); - subSchema.Properties.Add("title", new AsyncApiSchema() { Type = SchemaType.String }); + var mainSchema = new AsyncApiJsonSchema(); + var subSchema = new AsyncApiJsonSchema(); + subSchema.Properties.Add("title", new AsyncApiJsonSchema() { Type = SchemaType.String }); mainSchema.Not = subSchema; var yaml = mainSchema.Serialize(AsyncApiVersion.AsyncApi2_0, AsyncApiFormat.Yaml); diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiSecurityRequirement_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiSecurityRequirement_Should.cs index b92224df..273e1a7d 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiSecurityRequirement_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiSecurityRequirement_Should.cs @@ -24,7 +24,7 @@ public void SerializeV2_WithNullWriter_Throws() public void SerializeV2_Serializes() { var asyncApiSecurityRequirement = new AsyncApiSecurityRequirement(); - asyncApiSecurityRequirement.Add(new AsyncApiSecurityScheme { Type = SecuritySchemeType.ApiKey }, new List { "string" }); + asyncApiSecurityRequirement.Add(new AsyncApiSecuritySchemeReference("apiKey"), new List { "string" }); var output = asyncApiSecurityRequirement.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); } diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiServer_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiServer_Should.cs index 8b14560b..c306fb73 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiServer_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiServer_Should.cs @@ -48,15 +48,8 @@ public void AsyncApiServer_Serializes() new AsyncApiSecurityRequirement { { - new AsyncApiSecurityScheme() - { - Reference = new AsyncApiReference() - { - Id = "schem1", - Type = ReferenceType.SecurityScheme, - }, - Name = "scheme1", - }, new List + new AsyncApiSecuritySchemeReference("schem1") + , new List { "requirement", } 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..88cb7c87 --- /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/ValidationRuleTests.cs b/test/LEGO.AsyncAPI.Tests/Validation/ValidationRuleTests.cs new file mode 100644 index 00000000..2b1d3ebf --- /dev/null +++ b/test/LEGO.AsyncAPI.Tests/Validation/ValidationRuleTests.cs @@ -0,0 +1,125 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Tests.Validation +{ + using FluentAssertions; + using LEGO.AsyncAPI.Readers; + using NUnit.Framework; + using System.Linq; + + public class ValidationRuleTests + { + [Test] + [TestCase("chat-{person-id}")] + public void ChannelKey_WithInvalidParameter_DiagnosticsError(string channelKey) + { + var input = + $""" + asyncapi: 2.6.0 + info: + title: Chat Application + version: 1.0.0 + servers: + testing: + url: test.mosquitto.org:1883 + protocol: mqtt + description: Test broker + channels: + {channelKey}: + publish: + operationId: onMessageReceieved + message: + name: text + payload: + type: string + subscribe: + operationId: sendMessage + message: + name: text + payload: + type: string + """; + + var document = new AsyncApiStringReader().Read(input, out var diagnostic); + diagnostic.Errors.First().Message.Should().Be($"The key '{channelKey}' in 'channels' MUST match the regular expression '^[a-zA-Z0-9\\.\\-_]+$'."); + diagnostic.Errors.First().Pointer.Should().Be("#/channels"); + } + + [Test] + public void ChannelKey_WithNonUniqueKey_DiagnosticsError() + { + var input = + """ + asyncapi: 2.6.0 + info: + title: Chat Application + version: 1.0.0 + servers: + testing: + url: test.mosquitto.org:1883 + protocol: mqtt + description: Test broker + channels: + chat/{personId}: + publish: + operationId: onMessageReceieved + message: + name: text + payload: + type: string + chat/{personIdentity}: + publish: + operationId: onMessageReceieved + message: + name: text + payload: + type: string + """; + + var document = new AsyncApiStringReader().Read(input, out var diagnostic); + diagnostic.Errors.First().Message.Should().Be("Channel signature 'chat/{}' MUST be unique."); + diagnostic.Errors.First().Pointer.Should().Be("#/channels"); + } + + [Test] + [TestCase("chat")] + [TestCase("/some/chat/{personId}")] + [TestCase("chat-{personId}")] + [TestCase("chat-{person_id}")] + [TestCase("chat-{person%2Did}")] + [TestCase("chat-{personId2}")] + public void ChannelKey_WithValidKey_Success(string channelKey) + { + var input = + $""" + asyncapi: 2.6.0 + info: + title: Chat Application + version: 1.0.0 + servers: + testing: + url: test.mosquitto.org:1883 + protocol: mqtt + description: Test broker + channels: + {channelKey}: + publish: + operationId: onMessageReceieved + message: + name: text + payload: + type: string + subscribe: + operationId: sendMessage + message: + name: text + payload: + type: string + """; + + var document = new AsyncApiStringReader().Read(input, out var diagnostic); + diagnostic.Errors.Should().BeEmpty(); + } + } + +} 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); } } }