diff --git a/README.md b/README.md index fe9189f5..e15bf00d 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,48 @@ var stream = await httpClient.GetStreamAsync("master/examples/streetlights-kafka var asyncApiDocument = new AsyncApiStreamReader().Read(stream, out var diagnostic); ``` +#### Reading External $ref + +You can read externally referenced AsyncAPI documents by setting the `ReferenceResolution` property of the `AsyncApiReaderSettings` object to `ReferenceResolutionSetting.ResolveAllReferences` and providing an implementation for the `IAsyncApiExternalReferenceReader` interface. This interface contains a single method to which the built AsyncAPI.NET reader library will pass the location content contained in a `$ref` property (usually some form of path) and interface will return the content which is retrieved from wherever the `$ref` points to as a `string`. The AsyncAPI.NET reader will then automatically infer the `T` type of the content and recursively parse the external content into an AsyncAPI document as a child of the original document that contained the `$ref`. This means that you can have externally referenced documents that themselves contain external references. + +This interface allows users to load the content of their external reference however and from whereever is required. A new instance of the implementor of `IAsyncApiExternalReferenceReader` should be registered with the `ExternalReferenceReader` property of the `AsyncApiReaderSettings` when creating the reader from which the `GetExternalResource` method will be called when resolving external references. + +Below is a very simple example of implementation for `IAsyncApiExternalReferenceReader` that simply loads a file and returns it as a string found at the reference endpoint. +```csharp +using System.IO; + +public class AsyncApiExternalFileSystemReader : IAsyncApiExternalReferenceReader +{ + public string GetExternalResource(string reference) + { + return File.ReadAllText(reference); + } +} +``` + +This can then be configured in the reader as follows: +```csharp +var settings = new AsyncApiReaderSettings +{ + ReferenceResolution = ReferenceResolutionSetting.ResolveAllReferences, + ExternalReferenceReader = new AsyncApiExternalFileSystemReader(), +}; +var reader = new AsyncApiStringReader(settings); +``` + +This would function for a AsyncAPI document with following reference to load the content of `message.yaml` as a `AsyncApiMessage` object inline with the document object. +```yaml +asyncapi: 2.3.0 +info: + title: test + version: 1.0.0 +channels: + workspace: + publish: + message: + $ref: "../../../message.yaml" +``` + ### Bindings To add support for reading bindings, simply add the bindings you wish to support, to the `Bindings` collection of `AsyncApiReaderSettings`. There is a nifty helper to add different types of bindings, or like in the example `All` of them. diff --git a/src/LEGO.AsyncAPI.Bindings/AMQP/AMQPOperationBinding.cs b/src/LEGO.AsyncAPI.Bindings/AMQP/AMQPOperationBinding.cs index 86dc74ef..3a2e6a35 100644 --- a/src/LEGO.AsyncAPI.Bindings/AMQP/AMQPOperationBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/AMQP/AMQPOperationBinding.cs @@ -5,7 +5,6 @@ namespace LEGO.AsyncAPI.Bindings.AMQP using System; using System.Collections.Generic; using LEGO.AsyncAPI.Models; - using LEGO.AsyncAPI.Readers; using LEGO.AsyncAPI.Readers.ParseNodes; using LEGO.AsyncAPI.Writers; diff --git a/src/LEGO.AsyncAPI.Bindings/BindingsCollection.cs b/src/LEGO.AsyncAPI.Bindings/BindingsCollection.cs index 4fd5560f..83faf695 100644 --- a/src/LEGO.AsyncAPI.Bindings/BindingsCollection.cs +++ b/src/LEGO.AsyncAPI.Bindings/BindingsCollection.cs @@ -45,7 +45,7 @@ public static TCollection Add( return destination; } - public static IEnumerable> All => new List> + public static ICollection> All => new List> { Pulsar, Kafka, @@ -57,18 +57,18 @@ public static TCollection Add( MQTT, }; - public static IEnumerable> Http => new List> + public static ICollection> Http => new List> { new HttpOperationBinding(), new HttpMessageBinding(), }; - public static IEnumerable> Websockets => new List> + public static ICollection> Websockets => new List> { new WebSocketsChannelBinding(), }; - public static IEnumerable> Kafka => new List> + public static ICollection> Kafka => new List> { new KafkaServerBinding(), new KafkaChannelBinding(), @@ -76,32 +76,32 @@ public static TCollection Add( new KafkaMessageBinding(), }; - public static IEnumerable> Pulsar => new List> + public static ICollection> Pulsar => new List> { new PulsarServerBinding(), new PulsarChannelBinding(), }; - public static IEnumerable> Sqs => new List> + public static ICollection> Sqs => new List> { new SqsChannelBinding(), new SqsOperationBinding(), }; - public static IEnumerable> Sns => new List> + public static ICollection> Sns => new List> { new SnsChannelBinding(), new SnsOperationBinding(), }; - public static IEnumerable> AMQP => new List> + public static ICollection> AMQP => new List> { new AMQPChannelBinding(), new AMQPOperationBinding(), new AMQPMessageBinding(), }; - public static IEnumerable> MQTT => new List> + public static ICollection> MQTT => new List> { new MQTTServerBinding(), new MQTTOperationBinding(), diff --git a/src/LEGO.AsyncAPI.Bindings/Http/HttpMessageBinding.cs b/src/LEGO.AsyncAPI.Bindings/Http/HttpMessageBinding.cs index 7a5731ef..cbf65bc4 100644 --- a/src/LEGO.AsyncAPI.Bindings/Http/HttpMessageBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/Http/HttpMessageBinding.cs @@ -42,7 +42,7 @@ public override void SerializeProperties(IAsyncApiWriter writer) protected override FixedFieldMap FixedFieldMap => new() { { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, - { "headers", (a, n) => { a.Headers = JsonSchemaDeserializer.LoadSchema(n); } }, + { "headers", (a, n) => { a.Headers = AsyncApiSchemaDeserializer.LoadSchema(n); } }, }; } } diff --git a/src/LEGO.AsyncAPI.Bindings/Http/HttpOperationBinding.cs b/src/LEGO.AsyncAPI.Bindings/Http/HttpOperationBinding.cs index f70858c2..7b88d31b 100644 --- a/src/LEGO.AsyncAPI.Bindings/Http/HttpOperationBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/Http/HttpOperationBinding.cs @@ -63,7 +63,7 @@ public override void SerializeProperties(IAsyncApiWriter writer) { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, { "type", (a, n) => { a.Type = n.GetScalarValue().GetEnumFromDisplayName(); } }, { "method", (a, n) => { a.Method = n.GetScalarValue(); } }, - { "query", (a, n) => { a.Query = JsonSchemaDeserializer.LoadSchema(n); } }, + { "query", (a, n) => { a.Query = AsyncApiSchemaDeserializer.LoadSchema(n); } }, }; public override string BindingKey => "http"; diff --git a/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaMessageBinding.cs b/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaMessageBinding.cs index 2f665560..2e57b50a 100644 --- a/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaMessageBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaMessageBinding.cs @@ -67,7 +67,7 @@ public override void SerializeProperties(IAsyncApiWriter writer) protected override FixedFieldMap FixedFieldMap => new() { { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, - { "key", (a, n) => { a.Key = JsonSchemaDeserializer.LoadSchema(n); } }, + { "key", (a, n) => { a.Key = AsyncApiSchemaDeserializer.LoadSchema(n); } }, { "schemaIdLocation", (a, n) => { a.SchemaIdLocation = n.GetScalarValue(); } }, { "schemaIdPayloadEncoding", (a, n) => { a.SchemaIdPayloadEncoding = n.GetScalarValue(); } }, { "schemaLookupStrategy", (a, n) => { a.SchemaLookupStrategy = n.GetScalarValue(); } }, diff --git a/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaOperationBinding.cs b/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaOperationBinding.cs index 53db7ae0..b6c6e046 100644 --- a/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaOperationBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaOperationBinding.cs @@ -28,8 +28,8 @@ public class KafkaOperationBinding : OperationBinding protected override FixedFieldMap FixedFieldMap => new() { { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, - { "groupId", (a, n) => { a.GroupId = JsonSchemaDeserializer.LoadSchema(n); } }, - { "clientId", (a, n) => { a.ClientId = JsonSchemaDeserializer.LoadSchema(n); } }, + { "groupId", (a, n) => { a.GroupId = AsyncApiSchemaDeserializer.LoadSchema(n); } }, + { "clientId", (a, n) => { a.ClientId = AsyncApiSchemaDeserializer.LoadSchema(n); } }, }; /// diff --git a/src/LEGO.AsyncAPI.Bindings/MQTT/LastWill.cs b/src/LEGO.AsyncAPI.Bindings/MQTT/LastWill.cs index 23118bfa..955ad90f 100644 --- a/src/LEGO.AsyncAPI.Bindings/MQTT/LastWill.cs +++ b/src/LEGO.AsyncAPI.Bindings/MQTT/LastWill.cs @@ -2,9 +2,9 @@ namespace LEGO.AsyncAPI.Bindings.MQTT { + using System; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Writers; - using System; public class LastWill : IAsyncApiElement { diff --git a/src/LEGO.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs b/src/LEGO.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs index b48e5ae9..597cc4c7 100644 --- a/src/LEGO.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs @@ -57,7 +57,7 @@ public override void SerializeProperties(IAsyncApiWriter writer) protected override FixedFieldMap FixedFieldMap => new() { { "payloadFormatIndicator", (a, n) => { a.PayloadFormatIndicator = n.GetIntegerValueOrDefault(); } }, - { "correlationData", (a, n) => { a.CorrelationData = JsonSchemaDeserializer.LoadSchema(n); } }, + { "correlationData", (a, n) => { a.CorrelationData = AsyncApiSchemaDeserializer.LoadSchema(n); } }, { "contentType", (a, n) => { a.ContentType = n.GetScalarValue(); } }, { "responseTopic", (a, n) => { a.ResponseTopic = n.GetScalarValue(); } }, }; diff --git a/src/LEGO.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs b/src/LEGO.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs index d3155ecd..f58eede9 100644 --- a/src/LEGO.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs @@ -3,9 +3,6 @@ namespace LEGO.AsyncAPI.Bindings.MQTT { using System; - using System.Collections.Generic; - using LEGO.AsyncAPI.Models; - using LEGO.AsyncAPI.Readers; using LEGO.AsyncAPI.Readers.ParseNodes; using LEGO.AsyncAPI.Writers; diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/Ordering.cs b/src/LEGO.AsyncAPI.Bindings/Sns/Ordering.cs index 23f69f52..d44a0c9e 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sns/Ordering.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sns/Ordering.cs @@ -11,7 +11,7 @@ namespace LEGO.AsyncAPI.Bindings.Sns public class Ordering : IAsyncApiExtensible { /// - /// What type of SNS Topic is this? + /// What type of SNS Topic is this?. /// public OrderingType Type { get; set; } diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/Statement.cs b/src/LEGO.AsyncAPI.Bindings/Sns/Statement.cs index 7f3771f0..c92eaf94 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sns/Statement.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sns/Statement.cs @@ -19,7 +19,7 @@ public class Statement : IAsyncApiExtensible public StringOrStringList Principal { get; set; } /// - /// The SNS permission being allowed or denied e.g. sns:Publish + /// The SNS permission being allowed or denied e.g. sns:Publish. /// public StringOrStringList Action { get; set; } diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/Queue.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/Queue.cs index 33166af5..eb3d256a 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sqs/Queue.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/Queue.cs @@ -16,7 +16,7 @@ public class Queue : IAsyncApiExtensible public string Name { get; set; } /// - /// Is this a FIFO queue? + /// Is this a FIFO queue?. /// public bool FifoQueue { get; set; } @@ -56,7 +56,7 @@ public class Queue : IAsyncApiExtensible public RedrivePolicy RedrivePolicy { get; set; } /// - /// The security policy for the SQS Queue + /// The security policy for the SQS Queue. /// public Policy Policy { get; set; } diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/RedrivePolicy.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/RedrivePolicy.cs index 923d668c..538b1edf 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sqs/RedrivePolicy.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/RedrivePolicy.cs @@ -3,9 +3,9 @@ namespace LEGO.AsyncAPI.Bindings.Sqs { using System; + using System.Collections.Generic; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Writers; - using System.Collections.Generic; public class RedrivePolicy : IAsyncApiExtensible { diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/SqsOperationBinding.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/SqsOperationBinding.cs index d8eb43dd..98110209 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 9518d2d4..0bed331f 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sqs/Statement.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/Statement.cs @@ -19,7 +19,7 @@ public class Statement : IAsyncApiExtensible public StringOrStringList Principal { get; set; } /// - /// The SNS permission being allowed or denied e.g. sns:Publish + /// The SNS permission being allowed or denied e.g. sns:Publish. /// public StringOrStringList Action { get; set; } diff --git a/src/LEGO.AsyncAPI.Bindings/WebSockets/WebSocketsChannelBinding.cs b/src/LEGO.AsyncAPI.Bindings/WebSockets/WebSocketsChannelBinding.cs index c87393bb..8827bea4 100644 --- a/src/LEGO.AsyncAPI.Bindings/WebSockets/WebSocketsChannelBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/WebSockets/WebSocketsChannelBinding.cs @@ -31,8 +31,8 @@ public class WebSocketsChannelBinding : ChannelBinding { { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, { "method", (a, n) => { a.Method = n.GetScalarValue(); } }, - { "query", (a, n) => { a.Query = JsonSchemaDeserializer.LoadSchema(n); } }, - { "headers", (a, n) => { a.Headers = JsonSchemaDeserializer.LoadSchema(n); } }, + { "query", (a, n) => { a.Query = AsyncApiSchemaDeserializer.LoadSchema(n); } }, + { "headers", (a, n) => { a.Headers = AsyncApiSchemaDeserializer.LoadSchema(n); } }, }; public override void SerializeProperties(IAsyncApiWriter writer) diff --git a/src/LEGO.AsyncAPI.Readers/AsyncApiExternalReferenceResolver.cs b/src/LEGO.AsyncAPI.Readers/AsyncApiExternalReferenceResolver.cs new file mode 100644 index 00000000..2c111773 --- /dev/null +++ b/src/LEGO.AsyncAPI.Readers/AsyncApiExternalReferenceResolver.cs @@ -0,0 +1,247 @@ +using LEGO.AsyncAPI.Readers.Exceptions; +using LEGO.AsyncAPI.Services; + +namespace LEGO.AsyncAPI.Readers +{ + using System; + using System.Collections.Generic; + using System.Linq; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Models.Interfaces; + + /// + /// This class is used to walk an AsyncApiDocument and convert unresolved references to references to populated objects. + /// + internal class AsyncApiExternalReferenceResolver : AsyncApiVisitorBase + { + private AsyncApiDocument currentDocument; + private List errors = new List(); + private AsyncApiReaderSettings readerSettings; + + public AsyncApiExternalReferenceResolver( + AsyncApiDocument currentDocument, + AsyncApiReaderSettings readerSettings) + { + this.currentDocument = currentDocument; + this.readerSettings = readerSettings; + } + + public IEnumerable Errors + { + get + { + return this.errors; + } + } + + public override void Visit(IAsyncApiReferenceable referenceable) + { + if (referenceable.Reference != null) + { + referenceable.Reference.HostDocument = this.currentDocument; + } + } + + public override void Visit(AsyncApiComponents components) + { + this.ResolveMap(components.Parameters); + this.ResolveMap(components.Channels); + this.ResolveMap(components.Schemas); + this.ResolveMap(components.Servers); + this.ResolveMap(components.CorrelationIds); + this.ResolveMap(components.MessageTraits); + this.ResolveMap(components.OperationTraits); + this.ResolveMap(components.SecuritySchemes); + this.ResolveMap(components.ChannelBindings); + this.ResolveMap(components.MessageBindings); + this.ResolveMap(components.OperationBindings); + this.ResolveMap(components.ServerBindings); + this.ResolveMap(components.Messages); + } + + public override void Visit(AsyncApiDocument doc) + { + this.ResolveMap(doc.Servers); + this.ResolveMap(doc.Channels); + } + + public override void Visit(AsyncApiChannel channel) + { + this.ResolveMap(channel.Parameters); + this.ResolveObject(channel.Bindings, r => channel.Bindings = r); + } + + public override void Visit(AsyncApiMessageTrait trait) + { + this.ResolveObject(trait.CorrelationId, r => trait.CorrelationId = r); + this.ResolveObject(trait.Headers, r => trait.Headers = r); + } + + /// + /// Resolve all references used in an operation. + /// + public override void Visit(AsyncApiOperation operation) + { + this.ResolveList(operation.Message); + this.ResolveList(operation.Traits); + this.ResolveObject(operation.Bindings, r => operation.Bindings = r); + } + + public override void Visit(AsyncApiMessage message) + { + this.ResolveObject(message.Headers, r => message.Headers = r); + if (message.Payload is AsyncApiJsonSchemaPayload) + { + this.ResolveObject(message.Payload as AsyncApiJsonSchemaPayload, 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) + { + if (this.readerSettings.ExternalReferenceReader is null) + { + throw new AsyncApiReaderException( + "External reference configured in AsyncApi document but no implementation provided for ExternalReferenceReader."); + } + + // read external content + var externalContent = this.readerSettings.ExternalReferenceReader.Load(reference.Reference); + + // read external object content + var reader = new AsyncApiStringReader(this.readerSettings); + var externalAsyncApiContent = reader.ReadFragment(externalContent, AsyncApiVersion.AsyncApi2_0, out var diagnostic); + foreach (var error in diagnostic.Errors) + { + this.errors.Add(error); + } + + return externalAsyncApiContent; + } + + return null; + } + + private bool IsUnresolvedReference(IAsyncApiReferenceable possibleReference) + { + return (possibleReference != null && possibleReference.UnresolvedReference); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs b/src/LEGO.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs index 3a973654..6b340651 100644 --- a/src/LEGO.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs +++ b/src/LEGO.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs @@ -12,6 +12,7 @@ namespace LEGO.AsyncAPI.Readers using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Readers.Interface; + using LEGO.AsyncAPI.Services; using LEGO.AsyncAPI.Validations; /// @@ -165,23 +166,48 @@ public T ReadFragment(JsonNode input, AsyncApiVersion version, out AsyncApiDi private void ResolveReferences(AsyncApiDiagnostic diagnostic, AsyncApiDocument document) { - var errors = new List(); - - // Resolve References if requested switch (this.settings.ReferenceResolution) { - case ReferenceResolutionSetting.ResolveReferences: - errors.AddRange(document.ResolveReferences()); + case ReferenceResolutionSetting.ResolveAllReferences: + this.ResolveAllReferences(diagnostic, document); + break; + case ReferenceResolutionSetting.ResolveInternalReferences: + this.ResolveInternalReferences(diagnostic, document); break; - case ReferenceResolutionSetting.DoNotResolveReferences: break; } + } + + private void ResolveAllReferences(AsyncApiDiagnostic diagnostic, AsyncApiDocument document) + { + this.ResolveInternalReferences(diagnostic, document); + this.ResolveExternalReferences(diagnostic, document); + } + + private void ResolveInternalReferences(AsyncApiDiagnostic diagnostic, AsyncApiDocument document) + { + var errors = new List(); + + var reader = new AsyncApiStringReader(this.settings); + errors.AddRange(document.ResolveReferences()); foreach (var item in errors) { diagnostic.Errors.Add(item); } } + + private void ResolveExternalReferences(AsyncApiDiagnostic diagnostic, AsyncApiDocument document) + { + var resolver = new AsyncApiExternalReferenceResolver(document, this.settings); + var walker = new AsyncApiWalker(resolver); + walker.Walk(document); + + foreach (var error in resolver.Errors) + { + diagnostic.Errors.Add(error); + } + } } } \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Readers/AsyncApiReaderSettings.cs b/src/LEGO.AsyncAPI.Readers/AsyncApiReaderSettings.cs index 6ca2e129..341a743c 100644 --- a/src/LEGO.AsyncAPI.Readers/AsyncApiReaderSettings.cs +++ b/src/LEGO.AsyncAPI.Readers/AsyncApiReaderSettings.cs @@ -18,9 +18,14 @@ public enum ReferenceResolutionSetting DoNotResolveReferences, /// - /// Resolve internal component references and inline them. + /// Resolve all references and inline them. /// - ResolveReferences, + ResolveInternalReferences, + + /// + /// Resolve internal component references and inline them while leaving external references as placeholder objects with an AsyncApiReference instance and UnresolvedReference set to true. + /// + ResolveAllReferences, } /// @@ -32,7 +37,13 @@ public class AsyncApiReaderSettings : AsyncApiSettings /// Indicates how references in the source document should be handled. /// public ReferenceResolutionSetting ReferenceResolution { get; set; } = - ReferenceResolutionSetting.ResolveReferences; + ReferenceResolutionSetting.ResolveInternalReferences; + + /// + /// Indicates what should happen when unmapped members are encountered during deserialization. + /// Error and Warning will add an error or warning to the diagnostics object. + /// + public UnmappedMemberHandling UnmappedMemberHandling { get; set; } = UnmappedMemberHandling.Error; /// /// Dictionary of parsers for converting extensions into strongly typed classes. @@ -42,7 +53,7 @@ public Dictionary> { get; set; } = new Dictionary>(); - public IEnumerable> + public ICollection> Bindings { get; set; } = new List>(); @@ -57,5 +68,10 @@ public IEnumerable> /// from an object. /// public bool LeaveStreamOpen { get; set; } + + /// + /// External reference reader implementation provided by users for reading external resources. + /// + public IAsyncApiExternalReferenceReader ExternalReferenceReader { get; set; } } } \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Readers/AsyncApiTextReader.cs b/src/LEGO.AsyncAPI.Readers/AsyncApiTextReader.cs index eeece6a7..8d1eafc6 100644 --- a/src/LEGO.AsyncAPI.Readers/AsyncApiTextReader.cs +++ b/src/LEGO.AsyncAPI.Readers/AsyncApiTextReader.cs @@ -4,7 +4,6 @@ namespace LEGO.AsyncAPI.Readers { using System.IO; using System.Linq; - using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Nodes; using System.Threading; diff --git a/src/LEGO.AsyncAPI.Readers/Exceptions/AsyncApiUnsupportedSpecVersionException.cs b/src/LEGO.AsyncAPI.Readers/Exceptions/AsyncApiUnsupportedSpecVersionException.cs index d68da4bf..590b3d73 100644 --- a/src/LEGO.AsyncAPI.Readers/Exceptions/AsyncApiUnsupportedSpecVersionException.cs +++ b/src/LEGO.AsyncAPI.Readers/Exceptions/AsyncApiUnsupportedSpecVersionException.cs @@ -29,7 +29,7 @@ public AsyncApiUnsupportedSpecVersionException(string specificationVersion) /// inner exception. /// /// Version that caused this exception to be thrown. - /// The setting used for reading and writing + /// The setting used for reading and writing. /// Inner exception that caused this exception to be thrown. public AsyncApiUnsupportedSpecVersionException(string specificationVersion, Exception innerException) : base(string.Format(CultureInfo.InvariantCulture, MessagePattern, specificationVersion), innerException) diff --git a/src/LEGO.AsyncAPI.Readers/Interface/IAsyncApiExternalReferenceReader.cs b/src/LEGO.AsyncAPI.Readers/Interface/IAsyncApiExternalReferenceReader.cs new file mode 100644 index 00000000..6eef5608 --- /dev/null +++ b/src/LEGO.AsyncAPI.Readers/Interface/IAsyncApiExternalReferenceReader.cs @@ -0,0 +1,14 @@ +namespace LEGO.AsyncAPI.Readers; + +/// +/// Interface that provides method for reading external references.å. +/// +public interface IAsyncApiExternalReferenceReader +{ + /// + /// Method that returns the AsyncAPI content that the external reference from the $ref points to. + /// + /// The content address of the $ref. + /// The content of the reference as a string. + public string Load(string reference); +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Readers/ParseNodes/MapNode.cs b/src/LEGO.AsyncAPI.Readers/ParseNodes/MapNode.cs index b087c875..63bea6fc 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/MapNode.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/MapNode.cs @@ -214,6 +214,14 @@ public override AsyncApiAny CreateAny() return new AsyncApiAny(this.node); } + public void ParseFields(ref T parentInstance, IDictionary> fixedFields, IDictionary, Action> patternFields) + { + foreach (var propertyNode in this) + { + propertyNode.ParseField(parentInstance, fixedFields, patternFields); + } + } + private string ToScalarValue(JsonNode node) { var scalarNode = node is JsonValue value ? value : throw new AsyncApiException($"Expected scalar value"); diff --git a/src/LEGO.AsyncAPI.Readers/ParseNodes/PropertyNode.cs b/src/LEGO.AsyncAPI.Readers/ParseNodes/PropertyNode.cs index e0359659..2313f30d 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/PropertyNode.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/PropertyNode.cs @@ -29,8 +29,7 @@ public void ParseField( IDictionary> fixedFields, IDictionary, Action> patternFields) { - var found = fixedFields.TryGetValue(this.Name, out var fixedFieldMap); - + var _ = fixedFields.TryGetValue(this.Name, out var fixedFieldMap); if (fixedFieldMap != null) { try @@ -78,8 +77,12 @@ public void ParseField( } else { - this.Context.Diagnostic.Errors.Add( - new AsyncApiError("", $"{this.Name} is not a valid property at {this.Context.GetLocation()}")); + switch (this.Context.Settings.UnmappedMemberHandling) + { + case UnmappedMemberHandling.Error: + this.Context.Diagnostic.Errors.Add(new AsyncApiError(string.Empty, $"{this.Name} is not a valid property at {this.Context.GetLocation()}")); + break; + } } } } diff --git a/src/LEGO.AsyncAPI.Readers/ParseNodes/ValueNode.cs b/src/LEGO.AsyncAPI.Readers/ParseNodes/ValueNode.cs index 201ab6f7..76add0b2 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/ValueNode.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/ValueNode.cs @@ -2,11 +2,11 @@ namespace LEGO.AsyncAPI.Readers.ParseNodes { + using System; + using System.Text.Json.Nodes; using LEGO.AsyncAPI.Exceptions; using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Readers.Exceptions; - using System; - using System.Text.Json.Nodes; public class ValueNode : ParseNode { diff --git a/src/LEGO.AsyncAPI.Readers/UnmappedMemberHandling.cs b/src/LEGO.AsyncAPI.Readers/UnmappedMemberHandling.cs new file mode 100644 index 00000000..b58c34c4 --- /dev/null +++ b/src/LEGO.AsyncAPI.Readers/UnmappedMemberHandling.cs @@ -0,0 +1,20 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Readers +{ + /// + /// Unmapped member handling. + /// + public enum UnmappedMemberHandling + { + /// + /// Add error to diagnostics for unmapped members. + /// + Error, + + /// + /// Ignore unmapped members. + /// + Ignore, + } +} diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiAvroSchemaDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiAvroSchemaDeserializer.cs new file mode 100644 index 00000000..5015cef2 --- /dev/null +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiAvroSchemaDeserializer.cs @@ -0,0 +1,191 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Readers +{ + using System; + using LEGO.AsyncAPI.Exceptions; + using LEGO.AsyncAPI.Models; + 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 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() }, + }; + + public static AvroSchema LoadSchema(ParseNode node) + { + if (node is ValueNode valueNode) + { + return new AvroPrimitive(valueNode.GetScalarValue().GetEnumFromDisplayName()); + } + + if (node is ListNode) + { + var union = new AvroUnion(); + foreach (var item in node as ListNode) + { + union.Types.Add(LoadSchema(item)); + } + + return union; + } + + if (node is MapNode mapNode) + { + var pointer = mapNode.GetReferencePointer(); + + if (pointer != null) + { + return new AvroRecord + { + UnresolvedReference = true, + Reference = node.Context.VersionService.ConvertToAsyncApiReference(pointer, ReferenceType.Schema), + }; + } + + var 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 AvroField LoadField(ParseNode node) + { + var mapNode = node.CheckMapNode("field"); + var field = new AvroField(); + + mapNode.ParseFields(ref field, FieldFixedFields, FieldMetadataPatternFields); + + return field; + + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiComponentsDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiComponentsDeserializer.cs index 3b63db28..df13308a 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiComponentsDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiComponentsDeserializer.cs @@ -10,7 +10,7 @@ internal static partial class AsyncApiV2Deserializer { private static FixedFieldMap componentsFixedFields = new() { - { "schemas", (a, n) => a.Schemas = n.CreateMapWithReference(ReferenceType.Schema, JsonSchemaDeserializer.LoadSchema) }, + { "schemas", (a, n) => a.Schemas = n.CreateMapWithReference(ReferenceType.Schema, AsyncApiSchemaDeserializer.LoadSchema) }, { "servers", (a, n) => a.Servers = n.CreateMapWithReference(ReferenceType.Server, LoadServer) }, { "channels", (a, n) => a.Channels = n.CreateMapWithReference(ReferenceType.Channel, LoadChannel) }, { "messages", (a, n) => a.Messages = n.CreateMapWithReference(ReferenceType.Message, LoadMessage) }, diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageDeserializer.cs index 4c16bf22..2670d88a 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageDeserializer.cs @@ -2,12 +2,13 @@ namespace LEGO.AsyncAPI.Readers { + using System.Collections.Generic; + using System.Linq; using LEGO.AsyncAPI.Exceptions; using LEGO.AsyncAPI.Extensions; using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Readers.ParseNodes; - using System.Collections.Generic; - using System.Linq; /// /// Class containing logic to deserialize AsyncApi document into @@ -21,10 +22,10 @@ internal static partial class AsyncApiV2Deserializer "messageId", (a, n) => { a.MessageId = n.GetScalarValue(); } }, { - "headers", (a, n) => { a.Headers = JsonSchemaDeserializer.LoadSchema(n); } + "headers", (a, n) => { a.Headers = AsyncApiSchemaDeserializer.LoadSchema(n); } }, { - "payload", (a, n) => { a.Payload = JsonSchemaDeserializer.LoadSchema(n); } + "payload", (a, n) => { a.Payload = null; /* resolved after the initial run */ } }, { "correlationId", (a, n) => { a.CorrelationId = LoadCorrelationId(n); } @@ -64,7 +65,39 @@ internal static partial class AsyncApiV2Deserializer }, }; - static readonly IEnumerable SupportedSchemaFormats = new List + public static IAsyncApiMessagePayload LoadJsonSchemaPayload(ParseNode n) + { + return LoadPayload(n, null); + } + + public static IAsyncApiMessagePayload LoadAvroPayload(ParseNode n) + { + return LoadPayload(n, "application/vnd.apache.avro"); + } + + private static IAsyncApiMessagePayload LoadPayload(ParseNode n, string format) + { + + if (n == null) + { + return null; + } + + switch (format) + { + case null: + case "": + case var _ when SupportedJsonSchemaFormats.Where(s => format.StartsWith(s)).Any(): + return new AsyncApiJsonSchemaPayload(AsyncApiSchemaDeserializer.LoadSchema(n)); + case var _ when SupportedAvroSchemaFormats.Where(s => format.StartsWith(s)).Any(): + return new AsyncApiAvroSchemaPayload(AsyncApiAvroSchemaDeserializer.LoadSchema(n)); + default: + var supportedFormats = SupportedJsonSchemaFormats.Concat(SupportedAvroSchemaFormats); + throw new AsyncApiException($"'Could not deserialize Payload. Supported formats are {string.Join(", ", supportedFormats)}"); + } + } + + static readonly IEnumerable SupportedJsonSchemaFormats = new List { "application/vnd.aai.asyncapi+json", "application/vnd.aai.asyncapi+yaml", @@ -73,11 +106,21 @@ internal static partial class AsyncApiV2Deserializer "application/schema+yaml;version=draft-07", }; + static readonly IEnumerable SupportedAvroSchemaFormats = new List + { + "application/vnd.apache.avro", + "application/vnd.apache.avro+json", + "application/vnd.apache.avro+yaml", + "application/vnd.apache.avro+json;version=1.9.0", + "application/vnd.apache.avro+yaml;version=1.9.0", + }; + private static string LoadSchemaFormat(string schemaFormat) { - if (!SupportedSchemaFormats.Where(s => schemaFormat.StartsWith(s)).Any()) + var supportedFormats = SupportedJsonSchemaFormats.Concat(SupportedAvroSchemaFormats); + if (!supportedFormats.Where(s => schemaFormat.StartsWith(s)).Any()) { - throw new AsyncApiException($"'{schemaFormat}' is not a supported format. Supported formats are {string.Join(", ", SupportedSchemaFormats)}"); + throw new AsyncApiException($"'{schemaFormat}' is not a supported format. Supported formats are {string.Join(", ", supportedFormats)}"); } return schemaFormat; @@ -100,6 +143,7 @@ public static AsyncApiMessage LoadMessage(ParseNode node) var message = new AsyncApiMessage(); ParseMap(mapNode, message, messageFixedFields, messagePatternFields); + message.Payload = LoadPayload(mapNode["payload"]?.Value, message.SchemaFormat); return message; } diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageTraitDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageTraitDeserializer.cs index eca8af64..0d229e92 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageTraitDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageTraitDeserializer.cs @@ -11,7 +11,7 @@ internal static partial class AsyncApiV2Deserializer private static FixedFieldMap messageTraitFixedFields = new() { { "messageId", (a, n) => { a.MessageId = n.GetScalarValue(); } }, - { "headers", (a, n) => { a.Headers = JsonSchemaDeserializer.LoadSchema(n); } }, + { "headers", (a, n) => { a.Headers = AsyncApiSchemaDeserializer.LoadSchema(n); } }, { "correlationId", (a, n) => { a.CorrelationId = LoadCorrelationId(n); } }, { "schemaFormat", (a, n) => { a.SchemaFormat = n.GetScalarValue(); } }, { "contentType", (a, n) => { a.ContentType = n.GetScalarValue(); } }, diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiParameterDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiParameterDeserializer.cs index bff810f1..c77f48f4 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiParameterDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiParameterDeserializer.cs @@ -11,7 +11,7 @@ internal static partial class AsyncApiV2Deserializer private static FixedFieldMap parameterFixedFields = new() { { "description", (a, n) => { a.Description = n.GetScalarValue(); } }, - { "schema", (a, n) => { a.Schema = JsonSchemaDeserializer.LoadSchema(n); } }, + { "schema", (a, n) => { a.Schema = AsyncApiSchemaDeserializer.LoadSchema(n); } }, { "location", (a, n) => { a.Location = n.GetScalarValue(); } }, }; diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiSchemaDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiSchemaDeserializer.cs index b99fdef8..b0c10de7 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiSchemaDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiSchemaDeserializer.cs @@ -9,7 +9,7 @@ namespace LEGO.AsyncAPI.Readers using LEGO.AsyncAPI.Readers.ParseNodes; using LEGO.AsyncAPI.Writers; - public class JsonSchemaDeserializer + public class AsyncApiSchemaDeserializer { private static readonly FixedFieldMap schemaFixedFields = new() { @@ -215,28 +215,6 @@ public class JsonSchemaDeserializer { s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, AsyncApiV2Deserializer.LoadExtension(p, n)) }, }; - private static readonly AnyFieldMap schemaAnyFields = new() - { - { - AsyncApiConstants.Default, - new AnyFieldMapParameter( - s => s.Default, - (s, v) => s.Default = v, - s => s) - }, - }; - - private static readonly AnyListFieldMap schemaAnyListFields = new() - { - { - AsyncApiConstants.Enum, - new AnyListFieldMapParameter( - s => s.Enum, - (s, v) => s.Enum = v, - s => s) - }, - }; - public static AsyncApiSchema LoadSchema(ParseNode node) { var mapNode = node.CheckMapNode(AsyncApiConstants.Schema); @@ -259,9 +237,6 @@ public static AsyncApiSchema LoadSchema(ParseNode node) propertyNode.ParseField(schema, schemaFixedFields, schemaPatternFields); } - AsyncApiV2Deserializer.ProcessAnyFields(mapNode, schema, schemaAnyFields); - AsyncApiV2Deserializer.ProcessAnyListFields(mapNode, schema, schemaAnyListFields); - return schema; } } diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs index 10edd3ba..4acc3395 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs @@ -35,7 +35,10 @@ public AsyncApiV2VersionService(AsyncApiDiagnostic diagnostic) [typeof(AsyncApiOAuthFlows)] = AsyncApiV2Deserializer.LoadOAuthFlows, [typeof(AsyncApiOperation)] = AsyncApiV2Deserializer.LoadOperation, [typeof(AsyncApiParameter)] = AsyncApiV2Deserializer.LoadParameter, - [typeof(AsyncApiSchema)] = JsonSchemaDeserializer.LoadSchema, + [typeof(AsyncApiSchema)] = AsyncApiSchemaDeserializer.LoadSchema, + [typeof(AvroSchema)] = AsyncApiAvroSchemaDeserializer.LoadSchema, + [typeof(AsyncApiJsonSchemaPayload)] = AsyncApiV2Deserializer.LoadJsonSchemaPayload, + [typeof(AsyncApiAvroSchemaPayload)] = AsyncApiV2Deserializer.LoadAvroPayload, [typeof(AsyncApiSecurityRequirement)] = AsyncApiV2Deserializer.LoadSecurityRequirement, [typeof(AsyncApiSecurityScheme)] = AsyncApiV2Deserializer.LoadSecurityScheme, [typeof(AsyncApiServer)] = AsyncApiV2Deserializer.LoadServer, diff --git a/src/LEGO.AsyncAPI/AsyncApiVersion.cs b/src/LEGO.AsyncAPI/AsyncApiVersion.cs index c5be2cc8..dbac63de 100644 --- a/src/LEGO.AsyncAPI/AsyncApiVersion.cs +++ b/src/LEGO.AsyncAPI/AsyncApiVersion.cs @@ -5,7 +5,7 @@ namespace LEGO.AsyncAPI public enum AsyncApiVersion { /// - /// Represents AsyncAPI V2 spec + /// Represents AsyncAPI V2 spec. /// AsyncApi2_0, } diff --git a/src/LEGO.AsyncAPI/Models/Any/AsyncApiAny.cs b/src/LEGO.AsyncAPI/Models/Any/AsyncApiAny.cs index 5c5f2b31..45b764b3 100644 --- a/src/LEGO.AsyncAPI/Models/Any/AsyncApiAny.cs +++ b/src/LEGO.AsyncAPI/Models/Any/AsyncApiAny.cs @@ -2,7 +2,6 @@ namespace LEGO.AsyncAPI.Models { - using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Nodes; using LEGO.AsyncAPI.Models.Interfaces; diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiAvroSchemaPayload.cs b/src/LEGO.AsyncAPI/Models/AsyncApiAvroSchemaPayload.cs new file mode 100644 index 00000000..596115ba --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/AsyncApiAvroSchemaPayload.cs @@ -0,0 +1,53 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + + public class AsyncApiAvroSchemaPayload : IAsyncApiMessagePayload + { + public AvroSchema Schema { get; set; } + + public AsyncApiAvroSchemaPayload(AvroSchema schema) + { + this.Schema = schema; + } + + public AsyncApiAvroSchemaPayload() + { + } + + public bool TryGetAs(out T schema) + where T : AvroSchema + { + schema = this.Schema as T; + return schema != null; + } + + public bool UnresolvedReference { get => this.Schema.UnresolvedReference; set => this.Schema.UnresolvedReference = value; } + + public AsyncApiReference Reference { get => this.Schema.Reference; set => this.Schema.Reference = value; } + + public void SerializeV2(IAsyncApiWriter writer) + { + var settings = writer.GetSettings(); + + if (this.Reference != null) + { + if (!settings.ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV2(writer); + return; + } + } + + this.SerializeV2WithoutReference(writer); + } + + public void SerializeV2WithoutReference(IAsyncApiWriter writer) + { + this.Schema.SerializeV2(writer); + } + } +} diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiMessage.cs b/src/LEGO.AsyncAPI/Models/AsyncApiMessage.cs index 2c627541..b4848bf6 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiMessage.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiMessage.cs @@ -25,7 +25,7 @@ public class AsyncApiMessage : IAsyncApiExtensible, IAsyncApiReferenceable, IAsy /// /// definition of the message payload. It can be of any type but defaults to Schema object. It must match the schema format, including encoding type - e.g Avro should be inlined as either a YAML or JSON object NOT a string to be parsed as YAML or JSON. /// - public AsyncApiSchema Payload { get; set; } + public IAsyncApiMessagePayload Payload { get; set; } /// /// definition of the correlation ID used for message tracing or matching. diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiReference.cs b/src/LEGO.AsyncAPI/Models/AsyncApiReference.cs index 4f9660d1..9475310e 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiReference.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiReference.cs @@ -15,7 +15,7 @@ public class AsyncApiReference : IAsyncApiSerializable /// External resource in the reference. /// It maybe: /// 1. a absolute/relative file path, for example: ../commons/pet.json - /// 2. a Url, for example: http://localhost/pet.json + /// 2. a Url, for example: http://localhost/pet.json. /// public string ExternalResource { get; set; } @@ -35,7 +35,7 @@ public class AsyncApiReference : IAsyncApiSerializable public AsyncApiDocument HostDocument { get; set; } = null; /// - /// Gets a flag indicating whether a file is a valid OpenAPI document or a fragment + /// Gets a flag indicating whether a file is a valid OpenAPI document or a fragment. /// public bool IsFragment { get; set; } = false; @@ -71,7 +71,7 @@ public string Reference } /// - /// Serialize to Async Api v2.4. + /// Serialize to Async Api. /// public void SerializeV2(IAsyncApiWriter writer) { diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiSchemaPayload.cs b/src/LEGO.AsyncAPI/Models/AsyncApiSchemaPayload.cs new file mode 100644 index 00000000..19db07b2 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/AsyncApiSchemaPayload.cs @@ -0,0 +1,127 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + using System.Collections.Generic; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + + public class AsyncApiJsonSchemaPayload : IAsyncApiMessagePayload + { + private readonly AsyncApiSchema schema; + + public AsyncApiJsonSchemaPayload() + { + this.schema = new AsyncApiSchema(); + } + + public AsyncApiJsonSchemaPayload(AsyncApiSchema schema) + { + this.schema = schema; + } + + public string Title { get => this.schema.Title; set => this.schema.Title = value; } + + public SchemaType? Type { get => this.schema.Type; set => this.schema.Type = value; } + + public string Format { get => this.schema.Format; set => this.schema.Format = value; } + + public string Description { get => this.schema.Description; set => this.schema.Description = value; } + + public double? Maximum { get => this.schema.Maximum; set => this.schema.Maximum = value; } + + public bool? ExclusiveMaximum { get => this.schema.ExclusiveMaximum; set => this.schema.ExclusiveMaximum = value; } + + public double? Minimum { get => this.schema.Minimum; set => this.schema.Minimum = value; } + + public bool? ExclusiveMinimum { get => this.schema.ExclusiveMinimum; set => this.schema.ExclusiveMinimum = value; } + + public int? MaxLength { get => this.schema.MaxLength; set => this.schema.MaxLength = value; } + + public int? MinLength { get => this.schema.MinLength; set => this.schema.MinLength = value; } + + public string Pattern { get => this.schema.Pattern; set => this.schema.Pattern = value; } + + public double? MultipleOf { get => this.schema.MultipleOf; set => this.schema.MultipleOf = value; } + + public AsyncApiAny Default { get => this.schema.Default; set => this.schema.Default = value; } + + public bool ReadOnly { get => this.schema.ReadOnly; set => this.schema.ReadOnly = value; } + + public bool WriteOnly { get => this.schema.WriteOnly; set => this.schema.WriteOnly = value; } + + public IList AllOf { get => this.schema.AllOf; set => this.schema.AllOf = value; } + + public IList OneOf { get => this.schema.OneOf; set => this.schema.OneOf = value; } + + public IList AnyOf { get => this.schema.AnyOf; set => this.schema.AnyOf = value; } + + public AsyncApiSchema Not { get => this.schema.Not; set => this.schema.Not = value; } + + public AsyncApiSchema Contains { get => this.schema.Contains; set => this.schema.Contains = value; } + + public AsyncApiSchema If { get => this.schema.If; set => this.schema.If = value; } + + public AsyncApiSchema Then { get => this.schema.Then; set => this.schema.Then = value; } + + public AsyncApiSchema Else { get => this.schema.Else; set => this.schema.Else = value; } + + public ISet Required { get => this.schema.Required; set => this.schema.Required = value; } + + public AsyncApiSchema Items { get => this.schema.Items; set => this.schema.Items = value; } + + public AsyncApiSchema AdditionalItems { get => this.schema.AdditionalItems; set => this.schema.AdditionalItems = value; } + + public int? MaxItems { get => this.schema.MaxItems; set => this.schema.MaxItems = value; } + + public int? MinItems { get => this.schema.MinItems; set => this.schema.MinItems = value; } + + public bool? UniqueItems { get => this.schema.UniqueItems; set => this.schema.UniqueItems = value; } + + public IDictionary Properties { get => this.schema.Properties; set => this.schema.Properties = value; } + + public int? MaxProperties { get => this.schema.MaxProperties; set => this.schema.MaxProperties = value; } + + public int? MinProperties { get => this.schema.MinProperties; set => this.schema.MinProperties = value; } + + public IDictionary PatternProperties { get => this.schema.PatternProperties; set => this.schema.PatternProperties = value; } + + public AsyncApiSchema PropertyNames { get => this.schema.PropertyNames; set => this.schema.PropertyNames = value; } + + public string Discriminator { get => this.schema.Discriminator; set => this.schema.Discriminator = value; } + + public IList Enum { get => this.schema.Enum; set => this.schema.Enum = value; } + + public IList Examples { get => this.schema.Examples; set => this.schema.Examples = value; } + + public AsyncApiAny Const { get => this.schema.Const; set => this.schema.Const = value; } + + public bool Nullable { get => this.schema.Nullable; set => this.schema.Nullable = value; } + + public AsyncApiExternalDocumentation ExternalDocs { get => this.schema.ExternalDocs; set => this.schema.ExternalDocs = value; } + + public bool Deprecated { get => this.schema.Deprecated; set => this.schema.Deprecated = value; } + + public bool UnresolvedReference { get => this.schema.UnresolvedReference; set => this.schema.UnresolvedReference = value; } + + public AsyncApiReference Reference { get => this.schema.Reference; set => this.schema.Reference = value; } + + public IDictionary Extensions { get => this.schema.Extensions; set => this.schema.Extensions = value; } + + public AsyncApiSchema AdditionalProperties { get => this.schema.AdditionalProperties; set => this.schema.AdditionalProperties = value; } + + public static implicit operator AsyncApiSchema(AsyncApiJsonSchemaPayload payload) => payload.schema; + + public static implicit operator AsyncApiJsonSchemaPayload(AsyncApiSchema schema) => new AsyncApiJsonSchemaPayload(schema); + + public void SerializeV2(IAsyncApiWriter writer) + { + this.schema.SerializeV2(writer); + } + + public void SerializeV2WithoutReference(IAsyncApiWriter writer) + { + this.schema.SerializeV2WithoutReference(writer); + } + } +} diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiSerializableExtensions.cs b/src/LEGO.AsyncAPI/Models/AsyncApiSerializableExtensions.cs index 1d1bd35a..91167e19 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiSerializableExtensions.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiSerializableExtensions.cs @@ -33,7 +33,7 @@ public static void SerializeAsJson(this T element, Stream stream, AsyncApiVer /// The AsyncApi element. /// The output stream. /// The AsyncApi specification version. - /// The settings used for writing + /// The settings used for writing. public static void SerializeAsJson(this T element, Stream stream, AsyncApiVersion specificationVersion, AsyncApiWriterSettings settings) where T : IAsyncApiSerializable { @@ -60,7 +60,7 @@ public static void SerializeAsYaml(this T element, Stream stream, AsyncApiVer /// The AsyncApi element. /// The output stream. /// The AsyncApi specification version. - /// The settings used for writing + /// The settings used for writing. public static void SerializeAsYaml(this T element, Stream stream, AsyncApiVersion specificationVersion, AsyncApiWriterSettings settings) where T : IAsyncApiSerializable { diff --git a/src/LEGO.AsyncAPI/Models/Avro/AvroArray.cs b/src/LEGO.AsyncAPI/Models/Avro/AvroArray.cs new file mode 100644 index 00000000..0958fa3d --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/AvroArray.cs @@ -0,0 +1,47 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + using System.Collections.Generic; + using System.Linq; + using LEGO.AsyncAPI.Writers; + + public class AvroArray : AvroSchema + { + public override string Type { get; } = "array"; + + /// + /// The schema of the array's items. + /// + public AvroSchema Items { get; set; } + + /// + /// A map of properties not in the schema, but added as additional metadata. + /// + public override IDictionary Metadata { get; set; } = new Dictionary(); + + public override void SerializeV2WithoutReference(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteOptionalProperty("type", this.Type); + writer.WriteRequiredObject("items", this.Items, (w, f) => f.SerializeV2(w)); + if (this.Metadata.Any()) + { + foreach (var item in this.Metadata) + { + writer.WritePropertyName(item.Key); + if (item.Value == null) + { + writer.WriteNull(); + } + else + { + writer.WriteAny(item.Value); + } + } + } + + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/Avro/AvroEnum.cs b/src/LEGO.AsyncAPI/Models/Avro/AvroEnum.cs new file mode 100644 index 00000000..dbfbb627 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/AvroEnum.cs @@ -0,0 +1,77 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + using System.Collections.Generic; + using System.Linq; + using LEGO.AsyncAPI.Writers; + + public class AvroEnum : AvroSchema + { + public override string Type { get; } = "enum"; + + /// + /// The name of the schema. Required for named types. See Avro Names. + /// + public string Name { get; set; } + + /// + /// The namespace of the schema. Useful for named types to avoid name conflicts. + /// + public string Namespace { get; set; } + + /// + /// Documentation for the schema. + /// + public string Doc { get; set; } + + /// + /// Alternate names for this enum. + /// + public IList Aliases { get; set; } = new List(); + + /// + /// Listing symbols. All symbols in an enum must be unique. + /// + public IList Symbols { get; set; } = new List(); + + /// + /// A default value for this enumeration. + /// + public string Default { get; set; } + + /// + /// A map of properties not in the schema, but added as additional metadata. + /// + public override IDictionary Metadata { get; set; } = new Dictionary(); + + public override void SerializeV2WithoutReference(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteOptionalProperty("type", this.Type); + writer.WriteRequiredProperty("name", this.Name); + writer.WriteOptionalProperty("namespace", this.Namespace); + writer.WriteOptionalCollection("aliases", this.Aliases, (w, s) => w.WriteValue(s)); + writer.WriteOptionalProperty("doc", this.Doc); + writer.WriteRequiredCollection("symbols", this.Symbols, (w, s) => w.WriteValue(s)); + writer.WriteOptionalProperty("default", this.Default); + if (this.Metadata.Any()) + { + foreach (var item in this.Metadata) + { + writer.WritePropertyName(item.Key); + if (item.Value == null) + { + writer.WriteNull(); + } + else + { + writer.WriteAny(item.Value); + } + } + } + + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/Avro/AvroField.cs b/src/LEGO.AsyncAPI/Models/Avro/AvroField.cs new file mode 100644 index 00000000..6e8f9ed8 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/AvroField.cs @@ -0,0 +1,96 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + using System.Collections.Generic; + using System.Linq; + using LEGO.AsyncAPI.Attributes; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + + public enum AvroFieldOrder + { + None = 0, + + [Display("ascending")] + Ascending, + + [Display("descending")] + Descending, + + [Display("ignore")] + Ignore, + } + /// + /// Represents a field within an Avro record schema. + /// + public class AvroField : IAsyncApiSerializable + { + /// + /// The name of the field. + /// + public string Name { get; set; } + + /// + /// The type of the field. Can be a primitive type, a complex type, or a union. + /// + public AvroSchema Type { get; set; } + + /// + /// The documentation for the field. + /// + public string Doc { get; set; } + + /// + /// The default value for the field. + /// + public AsyncApiAny Default { get; set; } + + /// + /// The order of the field, can be 'ascending', 'descending', or 'ignore'. + /// + public AvroFieldOrder Order { get; set; } + + /// + /// Alternate names for this record (optional). + /// + public IList Aliases { get; set; } = new List(); + + /// + /// A map of properties not in the schema, but added as additional metadata. + /// + public IDictionary Metadata { get; set; } = new Dictionary(); + + public void SerializeV2(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteOptionalProperty("name", this.Name); + writer.WriteOptionalObject("type", this.Type, (w, s) => s.SerializeV2(w)); + writer.WriteOptionalProperty("doc", this.Doc); + writer.WriteOptionalObject("default", this.Default, (w, s) => w.WriteAny(s)); + if (this.Order != AvroFieldOrder.None) + { + writer.WriteOptionalProperty("order", this.Order.GetDisplayName()); + } + + writer.WriteOptionalCollection("aliases", this.Aliases, (w, s) => w.WriteValue(s)); + if (this.Metadata.Any()) + { + foreach (var item in this.Metadata) + { + writer.WritePropertyName(item.Key); + if (item.Value == null) + { + writer.WriteNull(); + } + else + { + writer.WriteAny(item.Value); + } + } + } + + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/Avro/AvroFixed.cs b/src/LEGO.AsyncAPI/Models/Avro/AvroFixed.cs new file mode 100644 index 00000000..241e8c1e --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/AvroFixed.cs @@ -0,0 +1,66 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + using System.Collections.Generic; + using System.Linq; + using LEGO.AsyncAPI.Writers; + + public class AvroFixed : AvroSchema + { + public override string Type { get; } = "fixed"; + + /// + /// The name of the schema. Required for named types. See Avro Names. + /// + public string Name { get; set; } + + /// + /// The namespace of the schema. Useful for named types to avoid name conflicts. + /// + public string Namespace { get; set; } + + + /// + /// Alternate names for this record. + /// + public IList Aliases { get; set; } = new List(); + + /// + /// Number of bytes per value. + /// + public int Size { get; set; } + + /// + /// A map of properties not in the schema, but added as additional metadata. + /// + public override IDictionary Metadata { get; set; } = new Dictionary(); + + public override void SerializeV2WithoutReference(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteOptionalProperty("type", this.Type); + writer.WriteRequiredProperty("name", this.Name); + writer.WriteOptionalProperty("namespace", this.Namespace); + writer.WriteOptionalCollection("aliases", this.Aliases, (w, s) => w.WriteValue(s)); + writer.WriteRequiredProperty("size", this.Size); + if (this.Metadata.Any()) + { + foreach (var item in this.Metadata) + { + writer.WritePropertyName(item.Key); + if (item.Value == null) + { + writer.WriteNull(); + } + else + { + writer.WriteAny(item.Value); + } + } + } + + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/Avro/AvroMap.cs b/src/LEGO.AsyncAPI/Models/Avro/AvroMap.cs new file mode 100644 index 00000000..c66d476b --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/AvroMap.cs @@ -0,0 +1,45 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + using System; + using System.Collections.Generic; + using System.Linq; + using LEGO.AsyncAPI.Writers; + + public class AvroMap : AvroSchema + { + public override string Type { get; } = "map"; + + public AvroPrimitiveType Values { get; set; } + + /// + /// A map of properties not in the schema, but added as additional metadata. + /// + public override IDictionary Metadata { get; set; } = new Dictionary(); + + public override void SerializeV2WithoutReference(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteOptionalProperty("type", this.Type); + writer.WriteRequiredProperty("values", this.Values.GetDisplayName()); + if (this.Metadata.Any()) + { + foreach (var item in this.Metadata) + { + writer.WritePropertyName(item.Key); + if (item.Value == null) + { + writer.WriteNull(); + } + else + { + writer.WriteAny(item.Value); + } + } + } + + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/Avro/AvroPrimitive.cs b/src/LEGO.AsyncAPI/Models/Avro/AvroPrimitive.cs new file mode 100644 index 00000000..d7526344 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/AvroPrimitive.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 AvroPrimitive : AvroSchema + { + public override string Type { get; } + + /// + /// A map of properties not in the schema, but added as additional metadata. + /// + public override IDictionary Metadata { get; set; } = new Dictionary(); + + public AvroPrimitive(AvroPrimitiveType type) + { + this.Type = type.GetDisplayName(); + } + + public AvroPrimitive() + { + } + + public override void SerializeV2WithoutReference(IAsyncApiWriter writer) + { + writer.WriteValue(this.Type); + if (this.Metadata.Any()) + { + foreach (var item in this.Metadata) + { + writer.WritePropertyName(item.Key); + if (item.Value == null) + { + writer.WriteNull(); + } + else + { + writer.WriteAny(item.Value); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/Avro/AvroPrimitiveType.cs b/src/LEGO.AsyncAPI/Models/Avro/AvroPrimitiveType.cs new file mode 100644 index 00000000..b76e7554 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/AvroPrimitiveType.cs @@ -0,0 +1,33 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + using LEGO.AsyncAPI.Attributes; + + public enum AvroPrimitiveType + { + [Display("null")] + Null, + + [Display("boolean")] + Boolean, + + [Display("int")] + Int, + + [Display("long")] + Long, + + [Display("float")] + Float, + + [Display("double")] + Double, + + [Display("bytes")] + Bytes, + + [Display("string")] + String, + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/Avro/AvroRecord.cs b/src/LEGO.AsyncAPI/Models/Avro/AvroRecord.cs new file mode 100644 index 00000000..04642f5c --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/AvroRecord.cs @@ -0,0 +1,71 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + using System.Collections.Generic; + using System.Linq; + using LEGO.AsyncAPI.Writers; + + public class AvroRecord : AvroSchema + { + public override string Type { get; } = "record"; + + /// + /// The name of the schema. Required for named types. See Avro Names. + /// + public string Name { get; set; } + + /// + /// The namespace of the schema. Useful for named types to avoid name conflicts. + /// + public string Namespace { get; set; } + + /// + /// Documentation for the schema. + /// + public string Doc { get; set; } + + /// + /// Alternate names for this record. + /// + public IList Aliases { get; set; } = new List(); + + /// + /// A list of fields contained within this record. + /// + public IList Fields { get; set; } = new List(); + + /// + /// A map of properties not in the schema, but added as additional metadata. + /// + public override IDictionary Metadata { get; set; } = new Dictionary(); + + public override void SerializeV2WithoutReference(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteOptionalProperty("type", this.Type); + writer.WriteRequiredProperty("name", this.Name); + writer.WriteOptionalProperty("namespace", this.Namespace); + writer.WriteOptionalProperty("doc", this.Doc); + writer.WriteOptionalCollection("aliases", this.Aliases, (w, s) => w.WriteValue(s)); + writer.WriteRequiredCollection("fields", this.Fields, (w, s) => s.SerializeV2(w)); + if (this.Metadata.Any()) + { + foreach (var item in this.Metadata) + { + writer.WritePropertyName(item.Key); + if (item.Value == null) + { + writer.WriteNull(); + } + else + { + writer.WriteAny(item.Value); + } + } + } + + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/Avro/AvroSchema.cs b/src/LEGO.AsyncAPI/Models/Avro/AvroSchema.cs new file mode 100644 index 00000000..fce1df2d --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/AvroSchema.cs @@ -0,0 +1,46 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + using System; + using System.Collections.Generic; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + + public abstract class AvroSchema : IAsyncApiSerializable, IAsyncApiReferenceable + { + public abstract string Type { get; } + + /// + /// A map of properties not in the schema, but added as additional metadata. + /// + public abstract IDictionary Metadata { get; set; } + + public bool UnresolvedReference { get; set; } + + public AsyncApiReference Reference { get; set; } + + public static implicit operator AvroSchema(AvroPrimitiveType type) + { + return new AvroPrimitive(type); + } + + public void SerializeV2(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (this.Reference != null && !writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV2(writer); + return; + } + + this.SerializeV2WithoutReference(writer); + } + + public abstract void SerializeV2WithoutReference(IAsyncApiWriter writer); + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/Avro/AvroUnion.cs b/src/LEGO.AsyncAPI/Models/Avro/AvroUnion.cs new file mode 100644 index 00000000..5762935d --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Avro/AvroUnion.cs @@ -0,0 +1,50 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + using System.Collections.Generic; + using System.Linq; + using LEGO.AsyncAPI.Writers; + + public class AvroUnion : AvroSchema + { + public override string Type { get; } = "map"; + + /// + /// The types in this union. + /// + public IList Types { get; set; } = new List(); + + /// + /// A map of properties not in the schema, but added as additional metadata. + /// + public override IDictionary Metadata { get; set; } = new Dictionary(); + + public override void SerializeV2WithoutReference(IAsyncApiWriter writer) + { + writer.WriteStartArray(); + foreach (var type in this.Types) + { + type.SerializeV2(writer); + } + + if (this.Metadata.Any()) + { + foreach (var item in this.Metadata) + { + writer.WritePropertyName(item.Key); + if (item.Value == null) + { + writer.WriteNull(); + } + else + { + writer.WriteAny(item.Value); + } + } + } + + writer.WriteEndArray(); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/Interfaces/IAsyncApiPayload.cs b/src/LEGO.AsyncAPI/Models/Interfaces/IAsyncApiPayload.cs new file mode 100644 index 00000000..76112af9 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Interfaces/IAsyncApiPayload.cs @@ -0,0 +1,8 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models.Interfaces +{ + public interface IAsyncApiMessagePayload : IAsyncApiSerializable, IAsyncApiReferenceable + { + } +} diff --git a/src/LEGO.AsyncAPI/Models/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/Services/AsyncApiReferenceResolver.cs b/src/LEGO.AsyncAPI/Services/AsyncApiReferenceResolver.cs index da6195c6..c37c2804 100644 --- a/src/LEGO.AsyncAPI/Services/AsyncApiReferenceResolver.cs +++ b/src/LEGO.AsyncAPI/Services/AsyncApiReferenceResolver.cs @@ -1,5 +1,4 @@ // Copyright (c) The LEGO Group. All rights reserved. - namespace LEGO.AsyncAPI.Services { using System; @@ -17,7 +16,8 @@ internal class AsyncApiReferenceResolver : AsyncApiVisitorBase private AsyncApiDocument currentDocument; private List errors = new List(); - public AsyncApiReferenceResolver(AsyncApiDocument currentDocument) + public AsyncApiReferenceResolver( + AsyncApiDocument currentDocument) { this.currentDocument = currentDocument; } @@ -86,7 +86,13 @@ public override void Visit(AsyncApiOperation operation) public override void Visit(AsyncApiMessage message) { this.ResolveObject(message.Headers, r => message.Headers = r); - this.ResolveObject(message.Payload, r => message.Payload = r); + + // #ToFix Resolve references correctly + if (message.Payload is AsyncApiJsonSchemaPayload) + { + this.ResolveObject(message.Payload as AsyncApiJsonSchemaPayload, r => message.Payload = r); + } + this.ResolveList(message.Traits); this.ResolveObject(message.CorrelationId, r => message.CorrelationId = r); this.ResolveObject(message.Bindings, r => message.Bindings = r); @@ -207,6 +213,7 @@ private void ResolveMap(IDictionary map) private T ResolveReference(AsyncApiReference reference) where T : class, IAsyncApiReferenceable, new() { + // external references are resolved by the AsyncApiExternalReferenceResolver if (reference.IsExternal) { return new() diff --git a/src/LEGO.AsyncAPI/Services/AsyncApiWalker.cs b/src/LEGO.AsyncAPI/Services/AsyncApiWalker.cs index fc5c5186..a2108647 100644 --- a/src/LEGO.AsyncAPI/Services/AsyncApiWalker.cs +++ b/src/LEGO.AsyncAPI/Services/AsyncApiWalker.cs @@ -501,7 +501,12 @@ internal void Walk(AsyncApiMessage message, bool isComponent = false) if (message != null) { this.Walk(AsyncApiConstants.Headers, () => this.Walk(message.Headers)); - this.Walk(AsyncApiConstants.Payload, () => this.Walk(message.Payload)); + if (message.Payload is AsyncApiJsonSchemaPayload payload) + { + this.Walk(AsyncApiConstants.Payload, () => this.Walk((AsyncApiSchema)payload)); + } + + // #ToFix Add walking for avro. this.Walk(AsyncApiConstants.CorrelationId, () => this.Walk(message.CorrelationId)); this.Walk(AsyncApiConstants.Tags, () => this.Walk(message.Tags)); this.Walk(AsyncApiConstants.Examples, () => this.Walk(message.Examples)); diff --git a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiOAuthFlowRules.cs b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiOAuthFlowRules.cs index 457a03ec..66090074 100644 --- a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiOAuthFlowRules.cs +++ b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiOAuthFlowRules.cs @@ -2,9 +2,9 @@ namespace LEGO.AsyncAPI.Validation.Rules { + using System.Linq; using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Validations; - using System.Linq; [AsyncApiRule] public static class AsyncApiOAuthFlowRules diff --git a/src/LEGO.AsyncAPI/Writers/AsyncApiWriterException.cs b/src/LEGO.AsyncAPI/Writers/AsyncApiWriterException.cs index 9d89bba3..17229818 100644 --- a/src/LEGO.AsyncAPI/Writers/AsyncApiWriterException.cs +++ b/src/LEGO.AsyncAPI/Writers/AsyncApiWriterException.cs @@ -2,8 +2,8 @@ namespace LEGO.AsyncAPI.Writers { - using LEGO.AsyncAPI.Exceptions; using System; + using LEGO.AsyncAPI.Exceptions; public class AsyncApiWriterException : AsyncApiException { diff --git a/src/LEGO.AsyncAPI/Writers/AsyncApiWriterExtensions.cs b/src/LEGO.AsyncAPI/Writers/AsyncApiWriterExtensions.cs index 67c4737a..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/AsyncApiYamlWriter.cs b/src/LEGO.AsyncAPI/Writers/AsyncApiYamlWriter.cs index d0fab76e..f7b32dd9 100644 --- a/src/LEGO.AsyncAPI/Writers/AsyncApiYamlWriter.cs +++ b/src/LEGO.AsyncAPI/Writers/AsyncApiYamlWriter.cs @@ -42,7 +42,7 @@ public AsyncApiYamlWriter(TextWriter textWriter) /// Initializes a new instance of the class. /// /// The text writer. - /// The settings used to read and write yaml + /// The settings used to read and write yaml. public AsyncApiYamlWriter(TextWriter textWriter, AsyncApiWriterSettings settings) : base(textWriter, settings) { diff --git a/src/LEGO.AsyncAPI/Writers/SpecialCharacterStringExtensions.cs b/src/LEGO.AsyncAPI/Writers/SpecialCharacterStringExtensions.cs index f2f6fbe2..9bce6aa2 100644 --- a/src/LEGO.AsyncAPI/Writers/SpecialCharacterStringExtensions.cs +++ b/src/LEGO.AsyncAPI/Writers/SpecialCharacterStringExtensions.cs @@ -2,11 +2,6 @@ namespace LEGO.AsyncAPI.Writers { - using System; - using System.Globalization; - using System.Linq; - using System.Text.RegularExpressions; - public static class SpecialCharacterStringExtensions { /// diff --git a/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentBuilder.cs b/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentBuilder.cs index 3f2ee429..f800a1f7 100644 --- a/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentBuilder.cs +++ b/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentBuilder.cs @@ -2,9 +2,9 @@ namespace LEGO.AsyncAPI.Tests { + using System; using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Models.Interfaces; - using System; internal class AsyncApiDocumentBuilder { diff --git a/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs b/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs index 63e818b4..4cbf1bd4 100644 --- a/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs +++ b/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs @@ -4,7 +4,6 @@ namespace LEGO.AsyncAPI.Tests { using System; using System.Collections.Generic; - using System.Globalization; using System.IO; using System.Linq; using FluentAssertions; @@ -493,7 +492,7 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() }, }, }, - Payload = new AsyncApiSchema() + Payload = new AsyncApiJsonSchemaPayload { Reference = new AsyncApiReference() { @@ -518,7 +517,7 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() }, }, }, - Payload = new AsyncApiSchema() + Payload = new AsyncApiJsonSchemaPayload() { Reference = new AsyncApiReference() { @@ -543,7 +542,7 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() }, }, }, - Payload = new AsyncApiSchema() + Payload = new AsyncApiJsonSchemaPayload() { Reference = new AsyncApiReference() { diff --git a/test/LEGO.AsyncAPI.Tests/AsyncApiReaderTests.cs b/test/LEGO.AsyncAPI.Tests/AsyncApiReaderTests.cs index bf19a944..20716506 100644 --- a/test/LEGO.AsyncAPI.Tests/AsyncApiReaderTests.cs +++ b/test/LEGO.AsyncAPI.Tests/AsyncApiReaderTests.cs @@ -5,7 +5,7 @@ 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; using LEGO.AsyncAPI.Models.Interfaces; @@ -31,6 +31,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 @@ -58,6 +59,7 @@ public void Read_WithExtensionParser_Parses() { { extensionName, valueExtensionParser }, }, + UnmappedMemberHandling = UnmappedMemberHandling.Ignore, }; var reader = new AsyncApiStringReader(settings); @@ -65,6 +67,96 @@ public void Read_WithExtensionParser_Parses() Assert.AreEqual((doc.Channels["workspace"].Extensions[extensionName] as AsyncApiAny).GetValue(), 1234); } + [Test] + public void Read_WithUnmappedMemberHandlingError_AddsError() + { + var extensionName = "x-someValue"; + var yaml = $""" + asyncapi: 2.3.0 + info: + title: test + version: 1.0.0 + test: 1234 + contact: + name: API Support + url: https://www.example.com/support + email: support@example.com + channels: + workspace: + {extensionName}: onetwothreefour + """; + Func valueExtensionParser = (any) => + { + if (any.TryGetValue(out var value)) + { + if (value == "onetwothreefour") + { + return new AsyncApiAny(1234); + } + } + + return new AsyncApiAny("No value provided"); + }; + + var settings = new AsyncApiReaderSettings + { + ExtensionParsers = new Dictionary> + { + { extensionName, valueExtensionParser }, + }, + UnmappedMemberHandling = UnmappedMemberHandling.Error, + }; + + var reader = new AsyncApiStringReader(settings); + var doc = reader.Read(yaml, out var diagnostic); + diagnostic.Errors.Should().HaveCount(1); + } + + [Test] + public void Read_WithUnmappedMemberHandlingIgnore_NoErrors() + { + var extensionName = "x-someValue"; + var yaml = $""" + asyncapi: 2.3.0 + info: + title: test + version: 1.0.0 + test: 1234 + contact: + name: API Support + url: https://www.example.com/support + email: support@example.com + channels: + workspace: + {extensionName}: onetwothreefour + """; + Func valueExtensionParser = (any) => + { + if (any.TryGetValue(out var value)) + { + if (value == "onetwothreefour") + { + return new AsyncApiAny(1234); + } + } + + return new AsyncApiAny("No value provided"); + }; + + var settings = new AsyncApiReaderSettings + { + ExtensionParsers = new Dictionary> + { + { extensionName, valueExtensionParser }, + }, + UnmappedMemberHandling = UnmappedMemberHandling.Ignore, + }; + + var reader = new AsyncApiStringReader(settings); + var doc = reader.Read(yaml, out var diagnostic); + diagnostic.Errors.Should().HaveCount(0); + } + [Test] public void Read_WithThrowingExtensionParser_AddsToDiagnostics() { diff --git a/test/LEGO.AsyncAPI.Tests/Bindings/StringOrStringList_Should.cs b/test/LEGO.AsyncAPI.Tests/Bindings/StringOrStringList_Should.cs index c437f15c..1a7c8681 100644 --- a/test/LEGO.AsyncAPI.Tests/Bindings/StringOrStringList_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Bindings/StringOrStringList_Should.cs @@ -4,7 +4,6 @@ namespace LEGO.AsyncAPI.Tests.Bindings { using System; using System.Collections.Generic; - using System.Linq; using FluentAssertions; using LEGO.AsyncAPI.Bindings; using LEGO.AsyncAPI.Models; diff --git a/test/LEGO.AsyncAPI.Tests/FluentAssertionExtensions.cs b/test/LEGO.AsyncAPI.Tests/FluentAssertionExtensions.cs index 31fec68a..a9a771ab 100644 --- a/test/LEGO.AsyncAPI.Tests/FluentAssertionExtensions.cs +++ b/test/LEGO.AsyncAPI.Tests/FluentAssertionExtensions.cs @@ -3,7 +3,6 @@ namespace LEGO.AsyncAPI.Tests { using System; - using System.IO; using FluentAssertions; using FluentAssertions.Primitives; using NUnit.Framework; diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiAnyTests.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiAnyTests.cs index 6c93e483..1dfdc59a 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiAnyTests.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiAnyTests.cs @@ -1,10 +1,9 @@ // Copyright (c) The LEGO Group. All rights reserved. -using LEGO.AsyncAPI.Models; -using NUnit.Framework; -using System; using System.Collections.Generic; using System.Linq; +using LEGO.AsyncAPI.Models; +using NUnit.Framework; namespace LEGO.AsyncAPI.Tests { diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiChannel_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiChannel_Should.cs index ef24e07b..19c6be71 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiChannel_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiChannel_Should.cs @@ -10,7 +10,7 @@ namespace LEGO.AsyncAPI.Tests.Models using LEGO.AsyncAPI.Models.Interfaces; using NUnit.Framework; - internal class AsyncApiChannel_Should : TestBase + public class AsyncApiChannel_Should : TestBase { [Test] public void AsyncApiChannel_WithWebSocketsBinding_Serializes() diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs index a37f1503..538664a8 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs @@ -41,7 +41,7 @@ public void AsyncApiMessage_WithNoType_DeserializesToDefault() // Assert diagnostic.Errors.Should().BeEmpty(); - message.Payload.Properties.First().Value.Enum.Should().HaveCount(2); + message.Payload.As().Properties.First().Value.Enum.Should().HaveCount(2); } [Test] @@ -78,7 +78,7 @@ public void AsyncApiMessage_WithUnsupportedSchemaFormat_DeserializesWithError() type: - 'null' - string - schemaFormat: application/vnd.apache.avro;version=1.9.0 + schemaFormat: whatever """; // Act @@ -86,7 +86,7 @@ public void AsyncApiMessage_WithUnsupportedSchemaFormat_DeserializesWithError() // Assert diagnostic.Errors.Should().HaveCount(1); - diagnostic.Errors.First().Message.Should().StartWith("'application/vnd.apache.avro;version=1.9.0' is not a supported format"); + diagnostic.Errors.First().Message.Should().StartWith("'whatever' is not a supported format"); } [Test] @@ -104,7 +104,7 @@ public void AsyncApiMessage_WithNoSchemaFormat_DoesNotSerializeSchemaFormat() """; var message = new AsyncApiMessage(); - message.Payload = new AsyncApiSchema() + message.Payload = new AsyncApiJsonSchemaPayload() { Properties = new Dictionary() { @@ -120,7 +120,6 @@ public void AsyncApiMessage_WithNoSchemaFormat_DoesNotSerializeSchemaFormat() // Act var actual = message.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); - var deserializedMessage = new AsyncApiStringReader().ReadFragment(expected, AsyncApiVersion.AsyncApi2_0, out _); // Assert @@ -130,7 +129,7 @@ public void AsyncApiMessage_WithNoSchemaFormat_DoesNotSerializeSchemaFormat() } [Test] - public void AsyncApiMessage_WithSchemaFormat_Serializes() + public void AsyncApiMessage_WithJsonSchemaFormat_Serializes() { // Arrange var expected = @@ -146,7 +145,7 @@ public void AsyncApiMessage_WithSchemaFormat_Serializes() var message = new AsyncApiMessage(); message.SchemaFormat = "application/vnd.aai.asyncapi+json;version=2.6.0"; - message.Payload = new AsyncApiSchema() + message.Payload = new AsyncApiJsonSchemaPayload() { Properties = new Dictionary() { @@ -169,6 +168,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"; + message.Payload = new AsyncApiAvroSchemaPayload() + { + 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, + }, + }, + }, + }; + + // Act + var actual = message.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + var deserializedMessage = new AsyncApiStringReader().ReadFragment(expected, AsyncApiVersion.AsyncApi2_0, out _); + + // Assert + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + message.Should().BeEquivalentTo(deserializedMessage); + } + + [Test] + public void AsyncApiMessage_WithAvroAsReference_Deserializes() + { + // Arrange + var input = + """ + schemaFormat: 'application/vnd.apache.avro+yaml;version=1.9.0' + payload: + $ref: 'path/to/user-create.avsc/#UserCreate' + """; + + // Act + var deserializedMessage = new AsyncApiStringReader().ReadFragment(input, AsyncApiVersion.AsyncApi2_0, out _); + + // Assert + deserializedMessage.Payload.Reference.Should().NotBeNull(); + deserializedMessage.Payload.Reference.IsExternal.Should().BeTrue(); + deserializedMessage.Payload.Reference.IsFragment.Should().BeTrue(); + + } + [Test] public void AsyncApiMessage_WithFilledObject_Serializes() { @@ -257,7 +328,7 @@ public void AsyncApiMessage_WithFilledObject_Serializes() }), }, }, - Payload = new AsyncApiSchema() + Payload = new AsyncApiJsonSchemaPayload() { Properties = new Dictionary { diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiOperation_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiOperation_Should.cs index 5597b211..847da7be 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiOperation_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiOperation_Should.cs @@ -3,7 +3,6 @@ namespace LEGO.AsyncAPI.Tests.Models { using System; - using System.Globalization; using System.IO; using FluentAssertions; using LEGO.AsyncAPI.Bindings.Http; diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs index c4491e40..41972784 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs @@ -2,12 +2,11 @@ namespace LEGO.AsyncAPI.Tests { + using System.Linq; using FluentAssertions; - using FluentAssertions.Primitives; using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Readers; using NUnit.Framework; - using System.Linq; public class AsyncApiReference_Should : TestBase { @@ -26,9 +25,10 @@ public void AsyncApiReference_WithExternalFragmentUriReference_AllowReference() // Assert diagnostic.Errors.Should().BeEmpty(); - deserialized.Payload.UnresolvedReference.Should().BeTrue(); + var payload = deserialized.Payload.As(); + payload.UnresolvedReference.Should().BeTrue(); - var reference = deserialized.Payload.Reference; + var reference = payload.Reference; reference.ExternalResource.Should().Be("http://example.com/some-resource"); reference.Id.Should().Be("/path/to/external/fragment"); reference.IsFragment.Should().BeTrue(); @@ -54,9 +54,10 @@ public void AsyncApiReference_WithFragmentReference_AllowReference() // Assert diagnostic.Errors.Should().BeEmpty(); - deserialized.Payload.UnresolvedReference.Should().BeTrue(); + var payload = deserialized.Payload.As(); + payload.UnresolvedReference.Should().BeTrue(); - var reference = deserialized.Payload.Reference; + var reference = payload.Reference; reference.Type.Should().Be(ReferenceType.Schema); reference.ExternalResource.Should().Be("/fragments/myFragment"); reference.Id.Should().BeNull(); @@ -82,7 +83,8 @@ public void AsyncApiReference_WithInternalComponentReference_AllowReference() // Assert diagnostic.Errors.Should().BeEmpty(); - var reference = deserialized.Payload.Reference; + var payload = deserialized.Payload.As(); + var reference = payload.Reference; reference.ExternalResource.Should().BeNull(); reference.Type.Should().Be(ReferenceType.Schema); reference.Id.Should().Be("test"); @@ -109,7 +111,8 @@ public void AsyncApiReference_WithExternalFragmentReference_AllowReference() // Assert diagnostic.Errors.Should().BeEmpty(); - var reference = deserialized.Payload.Reference; + var payload = deserialized.Payload.As(); + var reference = payload.Reference; reference.ExternalResource.Should().Be("./myjsonfile.json"); reference.Id.Should().Be("/fragment"); reference.IsFragment.Should().BeTrue(); @@ -135,7 +138,8 @@ public void AsyncApiReference_WithExternalComponentReference_AllowReference() // Assert diagnostic.Errors.Should().BeEmpty(); - var reference = deserialized.Payload.Reference; + var payload = deserialized.Payload.As(); + var reference = payload.Reference; reference.ExternalResource.Should().Be("./someotherdocument.json"); reference.Type.Should().Be(ReferenceType.Schema); reference.Id.Should().Be("test"); @@ -167,7 +171,7 @@ public void AsyncApiDocument_WithInternalComponentReference_ResolvesReference() var settings = new AsyncApiReaderSettings() { - ReferenceResolution = ReferenceResolutionSetting.ResolveReferences, + ReferenceResolution = ReferenceResolutionSetting.ResolveInternalReferences, }; var reader = new AsyncApiStringReader(settings); @@ -187,7 +191,7 @@ public void AsyncApiDocument_WithInternalComponentReference_ResolvesReference() } [Test] - public void AsyncApiDocument_WithExternalReference_DoesNotResolve() + public void AsyncApiDocument_WithNoConfiguredExternalReferenceReader_ThrowsError() { // Arrange var actual = """ @@ -202,7 +206,38 @@ public void AsyncApiDocument_WithExternalReference_DoesNotResolve() var settings = new AsyncApiReaderSettings() { - ReferenceResolution = ReferenceResolutionSetting.ResolveReferences, + ReferenceResolution = ReferenceResolutionSetting.ResolveAllReferences, + }; + var reader = new AsyncApiStringReader(settings); + + // Act + reader.Read(actual, out var diagnostic); + + // Assert + diagnostic.Errors.Count.Should().Be(1); + var error = diagnostic.Errors.First(); + error.Message.Should() + .Be( + "External reference configured in AsyncApi document but no implementation provided for ExternalReferenceReader."); + } + + [Test] + public void AsyncApiDocument_WithExternalReferenceOnlySetToResolveInternalReferences_DoesNotResolve() + { + // Arrange + var actual = """ + asyncapi: 2.6.0 + info: + title: My AsyncAPI Document + version: 1.0.0 + channels: + myChannel: + $ref: http://example.com/channel.json + """; + + var settings = new AsyncApiReaderSettings() + { + ReferenceResolution = ReferenceResolutionSetting.ResolveInternalReferences, }; var reader = new AsyncApiStringReader(settings); @@ -237,7 +272,8 @@ public void AsyncApiReference_WithExternalReference_AllowsReferenceDoesNotResolv // Assert diagnostic.Errors.Should().BeEmpty(); - var reference = deserialized.Payload.Reference; + var payload = deserialized.Payload.As(); + var reference = payload.Reference; reference.ExternalResource.Should().Be("http://example.com/json.json"); reference.Id.Should().BeNull(); reference.IsExternal.Should().BeTrue(); @@ -250,5 +286,82 @@ public void AsyncApiReference_WithExternalReference_AllowsReferenceDoesNotResolv .Should() .BePlatformAgnosticEquivalentTo(actual); } + + [Test] + public void AsyncApiReference_WithExternalResourcesInterface_DeserializesCorrectly() + { + var yaml = """ + asyncapi: 2.3.0 + info: + title: test + version: 1.0.0 + channels: + workspace: + publish: + message: + $ref: "./some/path/to/external/message.yaml" + """; + var settings = new AsyncApiReaderSettings + { + ReferenceResolution = ReferenceResolutionSetting.ResolveAllReferences, + ExternalReferenceReader = new MockExternalReferenceReader(), + }; + var reader = new AsyncApiStringReader(settings); + var doc = reader.Read(yaml, out var diagnostic); + var message = doc.Channels["workspace"].Publish.Message.First(); + message.Name.Should().Be("Test"); + var payload = message.Payload.As(); + payload.Properties.Count.Should().Be(3); + } + } + + public class MockExternalReferenceReader : IAsyncApiExternalReferenceReader + { + public string Load(string reference) + { + if (reference == "./some/path/to/external/message.yaml") + { + return """ + name: Test + title: Test message + summary: Test. + schemaFormat: application/schema+yaml;version=draft-07 + contentType: application/cloudevents+json + payload: + $ref: "./some/path/to/schema.yaml" + """; + } + + return """ + type: object + properties: + orderId: + description: The ID of the order. + type: string + format: uuid + name: + description: Name of order. + type: string + orderDetails: + description: User details. + type: object + properties: + userId: + description: User Id. + type: string + format: uuid + userName: + description: User name. + type: string + required: + - orderId + example: + orderId: 8f9189f8-653b-4849-a1ec-c838c030bd67 + handler: SomeName + orderDetails: + userId: Admin + userName: Admin + """; + } } } \ No newline at end of file diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiSchema_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiSchema_Should.cs index 830a44bc..53fc0350 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiSchema_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiSchema_Should.cs @@ -4,7 +4,6 @@ namespace LEGO.AsyncAPI.Tests.Models { using System; using System.Collections.Generic; - using System.Globalization; using System.IO; using FluentAssertions; using LEGO.AsyncAPI.Models; @@ -421,7 +420,7 @@ public void Serialize_WithInliningOptions_ShouldInlineAccordingly(bool shouldInl { new AsyncApiMessage { - Payload = new AsyncApiSchema + Payload = new AsyncApiJsonSchemaPayload { Type = SchemaType.Object, Required = new HashSet { "testB" }, @@ -510,6 +509,28 @@ public void Serialize_WithAnyOf_DoesNotWriteIf() Assert.True(!yaml.Contains("if:")); } + [Test] + public void Deserialize_BasicExample() + { + var input = + """ + title: title1 + type: integer + maximum: 42 + minimum: 10 + exclusiveMinimum: true + multipleOf: 3 + default: 15 + nullable: true + externalDocs: + url: http://example.com/externalDocs + """; + + var schema = new AsyncApiStringReader().ReadFragment(input, AsyncApiVersion.AsyncApi2_0, out var diag); + + diag.Errors.Should().BeEmpty(); + schema.Should().BeEquivalentTo(AdvancedSchemaNumber); + } /// /// Regression test. /// Bug: Serializing properties multiple times - specifically Schema.Not was serialized into Not and Else. diff --git a/test/LEGO.AsyncAPI.Tests/Models/AvroSchema_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AvroSchema_Should.cs new file mode 100644 index 00000000..2b5b5b0e --- /dev/null +++ b/test/LEGO.AsyncAPI.Tests/Models/AvroSchema_Should.cs @@ -0,0 +1,388 @@ +// 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 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 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))