From e039c25584e696ccb216657706e901ed9b3953cd Mon Sep 17 00:00:00 2001 From: VisualBean Date: Fri, 28 Mar 2025 13:26:07 +0100 Subject: [PATCH] feat: full v3 support --- Common.Build.props | 2 +- .../AsyncApiReaderSettings.cs | 5 - .../AsyncApiReferenceHostDocumentResolver.cs | 26 - .../ByteBard.AsyncAPI.Readers.csproj | 5 +- .../{V2 => }/ExtensionHelpers.cs | 2 +- .../ParseNodes/ListNode.cs | 11 + .../ParseNodes/MapNode.cs | 47 +- .../ParseNodes/ParseNode.cs | 10 + .../ParseNodes/PropertyNode.cs | 26 + .../ParseNodes/ValueNode.cs | 1 - .../ParsingContext.cs | 76 ++- .../AsyncApiRemoteReferenceCollector.cs | 1 + .../Services/DefaultStreamLoader.cs | 2 - .../StringExtensions.cs | 31 + .../TempStorageKeys.cs | 11 + .../V2/AsyncApiAvroSchemaDeserializer.cs | 36 +- .../V2/AsyncApiChannelDeserializer.cs | 66 +- .../V2/AsyncApiComponentsDeserializer.cs | 14 +- .../V2/AsyncApiDocumentDeserializer.cs | 103 +++- .../V2/AsyncApiMessageDeserializer.cs | 37 +- ... => AsyncApiMessageExampleDeserializer.cs} | 0 .../V2/AsyncApiMessageTraitDeserializer.cs | 5 +- .../V2/AsyncApiOAuthFlowDeserializer.cs | 2 +- .../V2/AsyncApiOperationDeserializer.cs | 65 +- .../V2/AsyncApiOperationTraitDeserializer.cs | 3 +- .../V2/AsyncApiParameterDeserializer.cs | 22 +- .../V2/AsyncApiSchemaDeserializer.cs | 1 - ...AsyncApiSecurityRequirementDeserializer.cs | 12 +- .../V2/AsyncApiSecuritySchemeDeserializer.cs | 1 - .../V2/AsyncApiServerDeserializer.cs | 23 +- .../V2/AsyncApiV2VersionService.cs | 3 +- .../V3/AsyncApiChannelBindingDeserializer.cs | 66 ++ .../V3/AsyncApiChannelDeserializer.cs | 52 ++ .../V3/AsyncApiComponentsDeserializer.cs | 48 ++ .../V3/AsyncApiContactDeserializer.cs | 36 ++ .../V3/AsyncApiCorrelationIdDeserializer.cs | 44 ++ .../V3/AsyncApiDeserializer.cs | 159 +++++ .../V3/AsyncApiDocumentDeserializer.cs | 37 ++ .../V3/AsyncApiExternalDocsDeserializer.cs | 38 ++ .../V3/AsyncApiInfoDeserializer.cs | 38 ++ .../V3/AsyncApiLicenseDeserializer.cs | 32 + .../V3/AsyncApiMessageBindingDeserializer.cs | 66 ++ .../V3/AsyncApiMessageDeserializer.cs | 95 +++ .../V3/AsyncApiMessageExampleDeserializer.cs | 33 + .../V3/AsyncApiMessageTraitDeserializer.cs | 46 ++ .../AsyncApiMultiFormatSchemaDeserializer.cs | 90 +++ .../V3/AsyncApiOAuthFlowDeserializer.cs | 57 ++ .../V3/AsyncApiOAuthFlowsDeserializer.cs | 41 ++ .../AsyncApiOperationBindingDeserializer.cs | 66 ++ .../V3/AsyncApiOperationDeserializer.cs | 67 ++ ...yncApiOperationReplyAddressDeserializer.cs | 37 ++ .../V3/AsyncApiOperationReplyDeserializer.cs | 38 ++ .../V3/AsyncApiOperationTraitDeserializer.cs | 43 ++ .../V3/AsyncApiParameterDeserializer.cs | 61 ++ .../V3/AsyncApiSecuritySchemeDeserializer.cs | 72 +++ .../V3/AsyncApiServerBindingDeserializer.cs | 65 ++ .../V3/AsyncApiServerDeserializer.cs | 82 +++ .../V3/AsyncApiServerVariableDeserializer.cs | 52 ++ .../V3/AsyncApiTagDeserializer.cs | 38 ++ .../V3/AsyncApiV3VersionService.cs | 89 +++ src/ByteBard.AsyncAPI/AsyncApiVersion.cs | 5 + src/ByteBard.AsyncAPI/AsyncApiWorkspace.cs | 102 +++- .../ByteBard.AsyncAPI.csproj | 3 +- .../Extensions/AsyncApiElementExtensions.cs | 8 +- .../Extensions/AsyncApiExtensions.cs | 14 + .../Models/AsyncApiAction.cs | 13 + .../Models/AsyncApiBinding.cs | 10 + .../Models/AsyncApiBindings{TBinding}.cs | 49 +- .../Models/AsyncApiChannel.cs | 87 ++- .../Models/AsyncApiComponents.cs | 408 ++++++++++++- .../Models/AsyncApiConstants.cs | 8 + .../Models/AsyncApiContact.cs | 10 + .../Models/AsyncApiCorrelationId.cs | 10 + .../Models/AsyncApiDocument.cs | 102 +++- .../Models/AsyncApiExternalDocumentation.cs | 18 +- src/ByteBard.AsyncAPI/Models/AsyncApiInfo.cs | 63 +- .../Models/AsyncApiLicense.cs | 10 + .../Models/AsyncApiMessage.cs | 55 +- .../Models/AsyncApiMessageExample.cs | 10 + .../Models/AsyncApiMessageTrait.cs | 44 +- .../Models/AsyncApiMultiFormatSchema.cs | 52 ++ .../Models/AsyncApiOAuthFlow.cs | 31 +- .../Models/AsyncApiOAuthFlows.cs | 33 + .../Models/AsyncApiOperation.cs | 88 ++- .../Models/AsyncApiOperationReply.cs | 38 ++ .../Models/AsyncApiOperationReplyAddress.cs | 41 ++ .../Models/AsyncApiOperationTrait.cs | 35 +- .../Models/AsyncApiParameter.cs | 58 +- .../Models/AsyncApiReference.cs | 50 +- .../Models/AsyncApiSecurityRequirement.cs | 60 -- .../Models/AsyncApiSecurityScheme.cs | 188 +++++- .../Models/AsyncApiSerializableExtensions.cs | 3 + .../Models/AsyncApiServer.cs | 76 ++- .../Models/AsyncApiServerVariable.cs | 10 + src/ByteBard.AsyncAPI/Models/AsyncApiTag.cs | 20 +- .../Models/Avro/AsyncApiAvroSchema.cs | 5 +- .../Models/Avro/AvroArray.cs | 24 + src/ByteBard.AsyncAPI/Models/Avro/AvroEnum.cs | 10 + .../Models/Avro/AvroField.cs | 44 ++ .../Models/Avro/AvroFixed.cs | 11 +- src/ByteBard.AsyncAPI/Models/Avro/AvroMap.cs | 10 + .../Models/Avro/AvroPrimitive.cs | 10 + .../Models/Avro/AvroRecord.cs | 28 + .../Models/Avro/AvroUnion.cs | 27 + .../Models/Interfaces/IAsyncApiPayload.cs | 6 - .../Models/Interfaces/IAsyncApiSchema.cs | 6 + .../Interfaces/IAsyncApiSerializable.cs | 2 + .../Models/JsonSchema/AsyncApiJsonSchema.cs | 14 +- .../Models/MessagePayloadExtensions.cs | 16 +- src/ByteBard.AsyncAPI/Models/ReferenceType.cs | 27 +- .../References/AsyncApiAvroSchemaReference.cs | 21 + .../AsyncApiBindingsReference{TBinding}.cs | 23 + .../References/AsyncApiChannelReference.cs | 77 ++- .../AsyncApiCorrelationIdReference.cs | 68 ++- .../AsyncApiExternalDocumentationReference.cs | 115 ++++ .../References/AsyncApiJsonSchemaReference.cs | 74 ++- .../References/AsyncApiMessageReference.cs | 84 ++- .../AsyncApiMessageTraitReference.cs | 74 ++- .../AsyncApiMultiFormatSchemaReference.cs | 66 ++ .../References/AsyncApiOperationReference.cs | 124 ++++ .../AsyncApiOperationReplyAddressReference.cs | 94 +++ .../AsyncApiOperationReplyReference.cs | 96 +++ .../AsyncApiOperationTraitReference.cs | 72 ++- .../References/AsyncApiParameterReference.cs | 73 ++- .../AsyncApiSecuritySchemeReference.cs | 52 +- .../References/AsyncApiServerReference.cs | 77 ++- .../AsyncApiServerVariableReference.cs | 68 ++- .../Models/References/AsyncApiTagReference.cs | 117 ++++ src/ByteBard.AsyncAPI/Resource.Designer.cs | 18 + src/ByteBard.AsyncAPI/Resource.resx | 6 + .../Services/AsyncApiVisitorBase.cs | 40 +- .../Services/AsyncApiWalker.cs | 271 ++++++--- src/ByteBard.AsyncAPI/Services/CurrentKeys.cs | 1 + .../Validation/AsyncApiValidator.cs | 13 +- .../Validation/IValidationContext.cs | 7 + .../Validation/Rules/AsyncApiAvroRules.cs | 4 +- .../Rules/AsyncApiComponentsRules.cs | 11 +- .../Validation/Rules/AsyncApiDocumentRules.cs | 70 +-- .../Rules/AsyncApiMultiFormatSchemaRules.cs | 24 + .../Rules/AsyncApiOAuthFlowRules.cs | 6 +- .../AsyncApiOperationReplyAdressRules.cs | 24 + .../Rules/AsyncApiOperationRules.cs | 94 +++ .../Rules/AsyncApiSecuritySchemaRules.cs | 106 ++++ .../Validation/Rules/AsyncApiServerRules.cs | 6 +- .../Writers/AsyncApiWriterBase.cs | 1 - .../Writers/AsyncApiWriterExtensions.cs | 38 +- .../Writers/AsyncApiWriterSettings.cs | 4 +- .../Writers/IAsyncApiWriter.cs | 4 +- .../AsyncApiDocumentBuilder.cs | 10 +- .../AsyncApiDocumentV2Tests.cs | 573 +++++++++--------- .../AsyncApiDocumentV3Tests.cs | 234 +++++++ .../AsyncApiLicenseTests.cs | 4 +- .../AsyncApiReaderTests.cs | 106 ++-- .../Bindings/AMQP/AMQPBindings_Should.cs | 8 +- .../Bindings/BindingExtensions_Should.cs | 8 +- .../Bindings/CustomBinding_Should.cs | 2 +- .../Bindings/Http/HttpBindings_Should.cs | 4 +- .../Bindings/Kafka/KafkaBindings_Should.cs | 12 +- .../Bindings/Pulsar/PulsarBindings_Should.cs | 22 +- .../Bindings/Sns/SnsBindings_Should.cs | 4 +- .../Bindings/Sqs/SqsBindings_should.cs | 4 +- .../Bindings/StringOrStringList_Should.cs | 10 +- .../WebSockets/WebSocketBindings_Should.cs | 2 +- .../ByteBard.AsyncAPI.Tests.csproj | 15 +- .../MQTT/MQTTBindings_Should.cs | 10 +- .../Models/AsyncApiAnyTests.cs | 2 +- .../Models/AsyncApiChannel_Should.cs | 79 ++- .../Models/AsyncApiContact_Should.cs | 2 +- .../AsyncApiExternalDocumentation_Should.cs | 2 +- .../Models/AsyncApiInfo_Should.cs | 2 +- .../Models/AsyncApiLicense_Should.cs | 2 +- .../Models/AsyncApiMessageExample_Should.cs | 2 +- .../Models/AsyncApiMessage_Should.cs | 39 +- .../Models/AsyncApiOAuthFlow_Should.cs | 2 +- .../Models/AsyncApiOperation_Should.cs | 179 +++++- .../Models/AsyncApiReference_Should.cs | 164 +++-- .../Models/AsyncApiSchema_Should.cs | 123 +++- .../AsyncApiSecurityRequirement_Should.cs | 30 - .../Models/AsyncApiServer_Should.cs | 109 ++-- .../Models/AvroSchema_Should.cs | 10 +- .../Serialization/AsyncApiYamlWriterTests.cs | 38 +- test/ByteBard.AsyncAPI.Tests/TestBase.cs | 12 +- .../AsyncApiSchema_InlinedReferences.yml | 2 +- .../AsyncApiSchema_NoInlinedReferences.yml | 2 +- .../Deserialize_WithAdvancedSchema_Works.json | 0 ...Json_WithAdvancedSchemaObject_V2Works.json | 0 ...n_WithAdvancedSchemaWithAllOf_V2Works.json | 0 .../AsyncApiSchema_InlinedReferences.yml | 27 + .../AsyncApiSchema_NoInlinedReferences.yml | 34 ++ .../Validation/ValidationRuleTests.cs | 274 +++++---- .../Validation/ValidationRulesetTests.cs | 6 +- 191 files changed, 7367 insertions(+), 1315 deletions(-) delete mode 100644 src/ByteBard.AsyncAPI.Readers/AsyncApiReferenceHostDocumentResolver.cs rename src/ByteBard.AsyncAPI.Readers/{V2 => }/ExtensionHelpers.cs (99%) create mode 100644 src/ByteBard.AsyncAPI.Readers/StringExtensions.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/TempStorageKeys.cs rename src/ByteBard.AsyncAPI.Readers/V2/{AsyncApiExampleDeserializer.cs => AsyncApiMessageExampleDeserializer.cs} (100%) create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiChannelBindingDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiChannelDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiComponentsDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiContactDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiCorrelationIdDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiDocumentDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiExternalDocsDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiInfoDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiLicenseDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMessageBindingDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMessageDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMessageExampleDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMessageTraitDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMultiFormatSchemaDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOAuthFlowDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOAuthFlowsDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOperationBindingDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOperationDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOperationReplyAddressDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOperationReplyDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOperationTraitDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiParameterDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiSecuritySchemeDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiServerBindingDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiServerDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiServerVariableDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiTagDeserializer.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/V3/AsyncApiV3VersionService.cs create mode 100644 src/ByteBard.AsyncAPI/Extensions/AsyncApiExtensions.cs create mode 100644 src/ByteBard.AsyncAPI/Models/AsyncApiAction.cs create mode 100644 src/ByteBard.AsyncAPI/Models/AsyncApiMultiFormatSchema.cs create mode 100644 src/ByteBard.AsyncAPI/Models/AsyncApiOperationReply.cs create mode 100644 src/ByteBard.AsyncAPI/Models/AsyncApiOperationReplyAddress.cs delete mode 100644 src/ByteBard.AsyncAPI/Models/AsyncApiSecurityRequirement.cs delete mode 100644 src/ByteBard.AsyncAPI/Models/Interfaces/IAsyncApiPayload.cs create mode 100644 src/ByteBard.AsyncAPI/Models/Interfaces/IAsyncApiSchema.cs create mode 100644 src/ByteBard.AsyncAPI/Models/References/AsyncApiExternalDocumentationReference.cs create mode 100644 src/ByteBard.AsyncAPI/Models/References/AsyncApiMultiFormatSchemaReference.cs create mode 100644 src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationReference.cs create mode 100644 src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationReplyAddressReference.cs create mode 100644 src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationReplyReference.cs create mode 100644 src/ByteBard.AsyncAPI/Models/References/AsyncApiTagReference.cs create mode 100644 src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiMultiFormatSchemaRules.cs create mode 100644 src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiOperationReplyAdressRules.cs create mode 100644 src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiOperationRules.cs create mode 100644 src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiSecuritySchemaRules.cs create mode 100644 test/ByteBard.AsyncAPI.Tests/AsyncApiDocumentV3Tests.cs delete mode 100644 test/ByteBard.AsyncAPI.Tests/Models/AsyncApiSecurityRequirement_Should.cs rename test/ByteBard.AsyncAPI.Tests/{TestData => V2_TestData}/AsyncApiSchema_InlinedReferences.yml (95%) rename test/ByteBard.AsyncAPI.Tests/{TestData => V2_TestData}/AsyncApiSchema_NoInlinedReferences.yml (96%) rename test/ByteBard.AsyncAPI.Tests/{TestData => V2_TestData}/Deserialize_WithAdvancedSchema_Works.json (100%) rename test/ByteBard.AsyncAPI.Tests/{TestData => V2_TestData}/SerializeAsJson_WithAdvancedSchemaObject_V2Works.json (100%) rename test/ByteBard.AsyncAPI.Tests/{TestData => V2_TestData}/SerializeAsJson_WithAdvancedSchemaWithAllOf_V2Works.json (100%) create mode 100644 test/ByteBard.AsyncAPI.Tests/V3_TestData/AsyncApiSchema_InlinedReferences.yml create mode 100644 test/ByteBard.AsyncAPI.Tests/V3_TestData/AsyncApiSchema_NoInlinedReferences.yml diff --git a/Common.Build.props b/Common.Build.props index d52b3e3..ed1ff79 100644 --- a/Common.Build.props +++ b/Common.Build.props @@ -2,7 +2,7 @@ 10 - netstandard2.0;net8 + netstandard2.1;net8 disable ByteBard https://github.com/ByteBardOrg/AsyncAPI.NET diff --git a/src/ByteBard.AsyncAPI.Readers/AsyncApiReaderSettings.cs b/src/ByteBard.AsyncAPI.Readers/AsyncApiReaderSettings.cs index 6474323..54baa13 100644 --- a/src/ByteBard.AsyncAPI.Readers/AsyncApiReaderSettings.cs +++ b/src/ByteBard.AsyncAPI.Readers/AsyncApiReaderSettings.cs @@ -71,10 +71,5 @@ public ICollection> /// External reference reader implementation provided by users for reading external resources. /// public IStreamLoader ExternalReferenceLoader { get; set; } = null; - - /// - /// URL where relative references should be resolved from if. - /// - public Uri BaseUrl { get; set; } } } \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI.Readers/AsyncApiReferenceHostDocumentResolver.cs b/src/ByteBard.AsyncAPI.Readers/AsyncApiReferenceHostDocumentResolver.cs deleted file mode 100644 index c5c69ea..0000000 --- a/src/ByteBard.AsyncAPI.Readers/AsyncApiReferenceHostDocumentResolver.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace ByteBard.AsyncAPI.Readers -{ - using System.Collections.Generic; - using ByteBard.AsyncAPI.Models; - using ByteBard.AsyncAPI.Models.Interfaces; - using ByteBard.AsyncAPI.Services; - - internal class AsyncApiReferenceWorkspaceResolver : AsyncApiVisitorBase - { - private AsyncApiWorkspace workspace; - - public AsyncApiReferenceWorkspaceResolver( - AsyncApiWorkspace workspace) - { - this.workspace = workspace; - } - - public override void Visit(IAsyncApiReferenceable referenceable) - { - if (referenceable.Reference != null) - { - referenceable.Reference.Workspace = this.workspace; - } - } - } -} \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI.Readers/ByteBard.AsyncAPI.Readers.csproj b/src/ByteBard.AsyncAPI.Readers/ByteBard.AsyncAPI.Readers.csproj index 265ee34..7bda8da 100644 --- a/src/ByteBard.AsyncAPI.Readers/ByteBard.AsyncAPI.Readers.csproj +++ b/src/ByteBard.AsyncAPI.Readers/ByteBard.AsyncAPI.Readers.csproj @@ -6,16 +6,15 @@ ByteBard.AsyncAPI.NET.Readers ByteBard.AsyncAPI.Readers ByteBard.AsyncAPI.Readers - netstandard2.0;net8 - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/ByteBard.AsyncAPI.Readers/V2/ExtensionHelpers.cs b/src/ByteBard.AsyncAPI.Readers/ExtensionHelpers.cs similarity index 99% rename from src/ByteBard.AsyncAPI.Readers/V2/ExtensionHelpers.cs rename to src/ByteBard.AsyncAPI.Readers/ExtensionHelpers.cs index beca3de..4e8da95 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/ExtensionHelpers.cs +++ b/src/ByteBard.AsyncAPI.Readers/ExtensionHelpers.cs @@ -38,4 +38,4 @@ public static IAsyncApiExtension LoadExtension(string name, ParseNode node) return node.CreateAny(); } } -} \ No newline at end of file +} diff --git a/src/ByteBard.AsyncAPI.Readers/ParseNodes/ListNode.cs b/src/ByteBard.AsyncAPI.Readers/ParseNodes/ListNode.cs index 347a4db..bd87d81 100644 --- a/src/ByteBard.AsyncAPI.Readers/ParseNodes/ListNode.cs +++ b/src/ByteBard.AsyncAPI.Readers/ParseNodes/ListNode.cs @@ -50,6 +50,17 @@ public override List CreateSimpleList(Func map) return this.nodeList.Select(n => map(new ValueNode(this.Context, n))).ToList(); } + public override HashSet CreateSimpleSet(Func map) + { + if (this.nodeList == null) + { + throw new AsyncApiReaderException( + $"Expected list while parsing {typeof(T).Name}"); + } + + return this.nodeList.Select(n => map(new ValueNode(this.Context, n))).ToHashSet(); + } + public IEnumerator GetEnumerator() { return this.nodeList.Select(n => Create(this.Context, n)).ToList().GetEnumerator(); diff --git a/src/ByteBard.AsyncAPI.Readers/ParseNodes/MapNode.cs b/src/ByteBard.AsyncAPI.Readers/ParseNodes/MapNode.cs index 05bab3e..c042de2 100644 --- a/src/ByteBard.AsyncAPI.Readers/ParseNodes/MapNode.cs +++ b/src/ByteBard.AsyncAPI.Readers/ParseNodes/MapNode.cs @@ -50,6 +50,42 @@ public PropertyNode this[string key] } } + public override Dictionary CreateMap(Func keySelector, Func map) + { + var jsonMap = this.node; + if (jsonMap == null) + { + throw new AsyncApiReaderException($"Expected map while parsing {typeof(T).Name}", this.Context); + } + + var nodes = jsonMap.Select( + n => + { + var originalKey = n.Key; + var newKey = keySelector(originalKey); + T value; + try + { + this.Context.StartObject(originalKey); + value = n.Value is JsonObject + ? map(new MapNode(this.Context, n.Value), originalKey) + : default(T); + } + finally + { + this.Context.EndObject(); + } + + return new + { + key = newKey, + value, + }; + }); + + return nodes.ToDictionary(k => k.key, v => v.value); + } + public override Dictionary CreateMap(Func map) { var jsonMap = this.node; @@ -207,7 +243,7 @@ public override AsyncApiAny CreateAny() return new AsyncApiAny(this.node); } - public void ParseFields(ref T parentInstance, IDictionary> fixedFields, IDictionary, Action> patternFields) + public void ParseFields(T parentInstance, IDictionary> fixedFields, IDictionary, Action> patternFields) { foreach (var propertyNode in this) { @@ -215,6 +251,15 @@ public void ParseFields(ref T parentInstance, IDictionary(T parentInstance, IDictionary, Action> patternFields) + { + foreach (var propertyNode in this) + { + propertyNode.ParseField(parentInstance, patternFields); + } + } + private string ToScalarValue(JsonNode node) { var scalarNode = node is JsonValue value ? value : throw new AsyncApiException($"Expected scalar value"); diff --git a/src/ByteBard.AsyncAPI.Readers/ParseNodes/ParseNode.cs b/src/ByteBard.AsyncAPI.Readers/ParseNodes/ParseNode.cs index 0ea7fe7..6a75ddc 100644 --- a/src/ByteBard.AsyncAPI.Readers/ParseNodes/ParseNode.cs +++ b/src/ByteBard.AsyncAPI.Readers/ParseNodes/ParseNode.cs @@ -46,6 +46,11 @@ public virtual List CreateList(Func map) throw new AsyncApiReaderException("Cannot create list from this type of node.", this.Context); } + public virtual Dictionary CreateMap(Func keySelector, Func map) + { + throw new AsyncApiReaderException("Cannot create map from this type of node.", this.Context); + } + public virtual Dictionary CreateMap(Func map) { throw new AsyncApiReaderException("Cannot create map from this type of node.", this.Context); @@ -72,6 +77,11 @@ public virtual List CreateSimpleList(Func map) throw new AsyncApiReaderException("Cannot create simple list from this type of node.", this.Context); } + public virtual HashSet CreateSimpleSet(Func map) + { + throw new AsyncApiReaderException("Cannot create simple list from this type of node.", this.Context); + } + public virtual Dictionary CreateSimpleMap(Func map) { throw new AsyncApiReaderException("Cannot create simple map from this type of node.", this.Context); diff --git a/src/ByteBard.AsyncAPI.Readers/ParseNodes/PropertyNode.cs b/src/ByteBard.AsyncAPI.Readers/ParseNodes/PropertyNode.cs index ab12f6a..51d8686 100644 --- a/src/ByteBard.AsyncAPI.Readers/ParseNodes/PropertyNode.cs +++ b/src/ByteBard.AsyncAPI.Readers/ParseNodes/PropertyNode.cs @@ -22,6 +22,32 @@ public PropertyNode(ParsingContext context, string name, JsonNode node) public ParseNode Value { get; set; } + public void ParseField(T parentInstance, IDictionary, Action> patternFields) + { + var map = patternFields.Where(p => p.Key(this.Name)).Select(p => p.Value).FirstOrDefault(); + if (map != null) + { + try + { + this.Context.StartObject(this.Name); + map(parentInstance, this.Name, this.Value); + } + catch (AsyncApiReaderException ex) + { + this.Context.Diagnostic.Errors.Add(new AsyncApiError(ex)); + } + catch (AsyncApiException ex) + { + ex.Pointer = this.Context.GetLocation(); + this.Context.Diagnostic.Errors.Add(new AsyncApiError(ex)); + } + finally + { + this.Context.EndObject(); + } + } + } + public void ParseField( T parentInstance, IDictionary> fixedFields, diff --git a/src/ByteBard.AsyncAPI.Readers/ParseNodes/ValueNode.cs b/src/ByteBard.AsyncAPI.Readers/ParseNodes/ValueNode.cs index e76c19c..e36580b 100644 --- a/src/ByteBard.AsyncAPI.Readers/ParseNodes/ValueNode.cs +++ b/src/ByteBard.AsyncAPI.Readers/ParseNodes/ValueNode.cs @@ -27,7 +27,6 @@ public override string GetScalarValue() { if (this.cachedScalarValue == null) { - // TODO: Update this property to use the .ToString() or JsonReader. var scalarNode = this.node is JsonValue value ? value : throw new AsyncApiException($"Expected scalar value"); this.cachedScalarValue = Convert.ToString(scalarNode.GetValue(), this.Context.Settings.CultureInfo); } diff --git a/src/ByteBard.AsyncAPI.Readers/ParsingContext.cs b/src/ByteBard.AsyncAPI.Readers/ParsingContext.cs index 2507731..e546d64 100644 --- a/src/ByteBard.AsyncAPI.Readers/ParsingContext.cs +++ b/src/ByteBard.AsyncAPI.Readers/ParsingContext.cs @@ -12,10 +12,14 @@ namespace ByteBard.AsyncAPI.Readers using ByteBard.AsyncAPI.Readers.Interface; using ByteBard.AsyncAPI.Readers.ParseNodes; using ByteBard.AsyncAPI.Readers.V2; - + using ByteBard.AsyncAPI.Readers.V3; public class ParsingContext { private readonly Stack currentLocation = new(); + private readonly Dictionary tempStorage = new(); + private readonly Dictionary> scopedTempStorage = new(); + public volatile int MessageCounter = 0; + public volatile int OperationCounter = 0; internal Dictionary> ExtensionParsers { @@ -40,7 +44,7 @@ internal Dictionary> ExtensionPars public AsyncApiDiagnostic Diagnostic { get; } /// - /// Gets the settings used fore reading json. + /// Gets the settings used fore reading json. /// public AsyncApiReaderSettings Settings { get; } @@ -82,12 +86,22 @@ internal AsyncApiDocument Parse(JsonNode jsonNode) this.VersionService = new AsyncApiV2VersionService(this.Diagnostic); doc = this.VersionService.LoadDocument(this.RootNode); - // Register components - this.Workspace.RegisterComponents(doc); // pre-register components. - this.Workspace.RegisterComponent(string.Empty, this.ParseToStream(jsonNode)); // register root document. + this.Workspace.SetRootDocument(doc); + this.Workspace.RegisterComponents(doc); + this.Workspace.RegisterComponent(string.Empty, this.ParseToStream(doc)); + this.Diagnostic.SpecificationVersion = AsyncApiVersion.AsyncApi2_0; break; + case string version when version.StartsWith("3"): + this.VersionService = new AsyncApiV3VersionService(this.Diagnostic); + doc = this.VersionService.LoadDocument(this.RootNode); + this.Workspace.SetRootDocument(doc); + this.Workspace.RegisterComponents(doc); + this.Workspace.RegisterComponent(string.Empty, this.ParseToStream(jsonNode)); + + this.Diagnostic.SpecificationVersion = AsyncApiVersion.AsyncApi3_0; + break; default: throw new AsyncApiUnsupportedSpecVersionException(inputVersion); } @@ -95,6 +109,12 @@ internal AsyncApiDocument Parse(JsonNode jsonNode) return doc; } + private Stream ParseToStream(AsyncApiDocument document) + { + var json = document.SerializeAsJson(AsyncApiVersion.AsyncApi3_0); + return this.ParseToStream(JsonNode.Parse(json)); + } + private Stream ParseToStream(JsonNode node) { var stream = new MemoryStream(); @@ -120,6 +140,10 @@ internal T ParseFragment(JsonNode jsonNode, AsyncApiVersion version) this.VersionService = new AsyncApiV2VersionService(this.Diagnostic); element = this.VersionService.LoadElement(node); break; + case AsyncApiVersion.AsyncApi3_0: + this.VersionService = new AsyncApiV3VersionService(this.Diagnostic); + element = this.VersionService.LoadElement(node); + break; } return element; @@ -133,6 +157,48 @@ private static string GetVersion(RootNode rootNode) internal IAsyncApiVersionService VersionService { get; set; } + public T GetFromTempStorage(string key, object scope = null) + { + Dictionary storage; + + if (scope == null) + { + storage = this.tempStorage; + } + else if (!this.scopedTempStorage.TryGetValue(scope, out storage)) + { + return default; + } + + return storage.TryGetValue(key, out var value) ? (T)value : default; + } + + /// + /// Sets the temporary storage for this key and value. + /// + public void SetTempStorage(string key, object value, object scope = null) + { + Dictionary storage; + + if (scope == null) + { + storage = this.tempStorage; + } + else if (!this.scopedTempStorage.TryGetValue(scope, out storage)) + { + storage = this.scopedTempStorage[scope] = new(); + } + + if (value == null) + { + storage.Remove(key); + } + else + { + storage[key] = value; + } + } + public void EndObject() { this.currentLocation.Pop(); diff --git a/src/ByteBard.AsyncAPI.Readers/Services/AsyncApiRemoteReferenceCollector.cs b/src/ByteBard.AsyncAPI.Readers/Services/AsyncApiRemoteReferenceCollector.cs index 7716ae1..0306a2f 100644 --- a/src/ByteBard.AsyncAPI.Readers/Services/AsyncApiRemoteReferenceCollector.cs +++ b/src/ByteBard.AsyncAPI.Readers/Services/AsyncApiRemoteReferenceCollector.cs @@ -14,6 +14,7 @@ public AsyncApiReferenceCollector( { this.workspace = workspace; } + /// /// List of all external references collected from AsyncApiDocument. /// diff --git a/src/ByteBard.AsyncAPI.Readers/Services/DefaultStreamLoader.cs b/src/ByteBard.AsyncAPI.Readers/Services/DefaultStreamLoader.cs index 600fa7a..6ccd69b 100644 --- a/src/ByteBard.AsyncAPI.Readers/Services/DefaultStreamLoader.cs +++ b/src/ByteBard.AsyncAPI.Readers/Services/DefaultStreamLoader.cs @@ -28,7 +28,6 @@ public Stream Load(Uri uri) } catch (Exception ex) { - throw new AsyncApiReaderException($"Something went wrong trying to fetch '{uri.OriginalString}. {ex.Message}'", ex); } } @@ -50,7 +49,6 @@ public async Task LoadAsync(Uri uri) } catch (Exception ex) { - throw new AsyncApiReaderException($"Something went wrong trying to fetch '{uri.OriginalString}'. {ex.Message}", ex); } } diff --git a/src/ByteBard.AsyncAPI.Readers/StringExtensions.cs b/src/ByteBard.AsyncAPI.Readers/StringExtensions.cs new file mode 100644 index 0000000..f66ff84 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/StringExtensions.cs @@ -0,0 +1,31 @@ +using ByteBard.AsyncAPI.Attributes; +using System; +using System.Reflection; +public static class StringExtensions +{ + /// + /// Gets the enum value based on the given enum type and display name. + /// + /// The display name. + public static T GetEnumFromDisplayName(this string displayName) + { + var type = typeof(T); + if (!type.IsEnum) + { + return default; + } + + foreach (var value in Enum.GetValues(type)) + { + var field = type.GetField(value.ToString()); + + var displayAttribute = (DisplayAttribute)field.GetCustomAttribute(typeof(DisplayAttribute)); + if (displayAttribute != null && displayAttribute.Name == displayName) + { + return (T)value; + } + } + + return default; + } +} \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI.Readers/TempStorageKeys.cs b/src/ByteBard.AsyncAPI.Readers/TempStorageKeys.cs new file mode 100644 index 0000000..be52640 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/TempStorageKeys.cs @@ -0,0 +1,11 @@ +namespace ByteBard.AsyncAPI.Readers +{ + public static class TempStorageKeys + { + public const string SecuritySchemeScopes = "SecuritySchemeScopes"; + public const string Operations = "Operations"; + public const string OperationMessageReferences = "OperationMessageReferences"; + public const string ComponentMessages = "ComponentMessages"; + public const string ChannelAddresses = "ChannelAddresses"; + } +} \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiAvroSchemaDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiAvroSchemaDeserializer.cs index 8c28c48..d67777e 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiAvroSchemaDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiAvroSchemaDeserializer.cs @@ -5,7 +5,6 @@ using ByteBard.AsyncAPI.Models.Avro.LogicalTypes; using ByteBard.AsyncAPI.Readers.Exceptions; using ByteBard.AsyncAPI.Readers.ParseNodes; - using ByteBard.AsyncAPI.Writers; public class AsyncApiAvroSchemaDeserializer { @@ -228,6 +227,11 @@ public static AsyncApiAvroSchema LoadSchema(ParseNode node) return union; } + if (node is PropertyNode propertyNode) + { + node = propertyNode.Value; + } + if (node is MapNode mapNode) { var pointer = mapNode.GetReferencePointer(); @@ -248,27 +252,27 @@ public static AsyncApiAvroSchema LoadSchema(ParseNode node) { case "record": var record = new AvroRecord(); - mapNode.ParseFields(ref record, RecordFixedFields, RecordMetadataPatternFields); + mapNode.ParseFields(record, RecordFixedFields, RecordMetadataPatternFields); return record; case "enum": var @enum = new AvroEnum(); - mapNode.ParseFields(ref @enum, EnumFixedFields, EnumMetadataPatternFields); + mapNode.ParseFields(@enum, EnumFixedFields, EnumMetadataPatternFields); return @enum; case "fixed": var @fixed = new AvroFixed(); - mapNode.ParseFields(ref @fixed, FixedFixedFields, FixedMetadataPatternFields); + mapNode.ParseFields(@fixed, FixedFixedFields, FixedMetadataPatternFields); return @fixed; case "array": var array = new AvroArray(); - mapNode.ParseFields(ref array, ArrayFixedFields, ArrayMetadataPatternFields); + mapNode.ParseFields(array, ArrayFixedFields, ArrayMetadataPatternFields); return array; case "map": var map = new AvroMap(); - mapNode.ParseFields(ref map, MapFixedFields, MapMetadataPatternFields); + mapNode.ParseFields(map, MapFixedFields, MapMetadataPatternFields); return map; case "union": var union = new AvroUnion(); - mapNode.ParseFields(ref union, UnionFixedFields, UnionMetadataPatternFields); + mapNode.ParseFields(union, UnionFixedFields, UnionMetadataPatternFields); return union; default: throw new AsyncApiException($"Unsupported type: {type}"); @@ -285,35 +289,35 @@ private static AsyncApiAvroSchema LoadLogicalType(MapNode mapNode) { case "decimal": var @decimal = new AvroDecimal(); - mapNode.ParseFields(ref @decimal, DecimalFixedFields, DecimalMetadataPatternFields); + mapNode.ParseFields(@decimal, DecimalFixedFields, DecimalMetadataPatternFields); return @decimal; case "uuid": var uuid = new AvroUUID(); - mapNode.ParseFields(ref uuid, UUIDFixedFields, UUIDMetadataPatternFields); + mapNode.ParseFields(uuid, UUIDFixedFields, UUIDMetadataPatternFields); return uuid; case "date": var date = new AvroDate(); - mapNode.ParseFields(ref date, DateFixedFields, DateMetadataPatternFields); + mapNode.ParseFields(date, DateFixedFields, DateMetadataPatternFields); return date; case "time-millis": var timeMillis = new AvroTimeMillis(); - mapNode.ParseFields(ref timeMillis, TimeMillisFixedFields, TimeMillisMetadataPatternFields); + mapNode.ParseFields(timeMillis, TimeMillisFixedFields, TimeMillisMetadataPatternFields); return timeMillis; case "time-micros": var timeMicros = new AvroTimeMicros(); - mapNode.ParseFields(ref timeMicros, TimeMicrosFixedFields, TimeMicrosMetadataPatternFields); + mapNode.ParseFields(timeMicros, TimeMicrosFixedFields, TimeMicrosMetadataPatternFields); return timeMicros; case "timestamp-millis": var timestampMillis = new AvroTimestampMillis(); - mapNode.ParseFields(ref timestampMillis, TimestampMillisFixedFields, TimestampMillisMetadataPatternFields); + mapNode.ParseFields(timestampMillis, TimestampMillisFixedFields, TimestampMillisMetadataPatternFields); return timestampMillis; case "timestamp-micros": var timestampMicros = new AvroTimestampMicros(); - mapNode.ParseFields(ref timestampMicros, TimestampMicrosFixedFields, TimestampMicrosMetadataPatternFields); + mapNode.ParseFields(timestampMicros, TimestampMicrosFixedFields, TimestampMicrosMetadataPatternFields); return timestampMicros; case "duration": var duration = new AvroDuration(); - mapNode.ParseFields(ref duration, DurationFixedFields, DurationMetadataPatternFields); + mapNode.ParseFields(duration, DurationFixedFields, DurationMetadataPatternFields); return duration; default: throw new AsyncApiException($"Unsupported type: {type}"); @@ -325,7 +329,7 @@ private static AvroField LoadField(ParseNode node) var mapNode = node.CheckMapNode("field"); var field = new AvroField(); - mapNode.ParseFields(ref field, FieldFixedFields, FieldMetadataPatternFields); + mapNode.ParseFields(field, FieldFixedFields, FieldMetadataPatternFields); return field; diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiChannelDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiChannelDeserializer.cs index 7d46d97..df26b17 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiChannelDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiChannelDeserializer.cs @@ -3,15 +3,17 @@ namespace ByteBard.AsyncAPI.Readers using ByteBard.AsyncAPI.Extensions; using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Readers.ParseNodes; + using System.Collections.Generic; + using System.Threading; internal static partial class AsyncApiV2Deserializer { private static readonly FixedFieldMap ChannelFixedFields = new() { { "description", (a, n) => { a.Description = n.GetScalarValue(); } }, - { "servers", (a, n) => { a.Servers = n.CreateSimpleList(s => s.GetScalarValue()); } }, - { "subscribe", (a, n) => { a.Subscribe = LoadOperation(n); } }, - { "publish", (a, n) => { a.Publish = LoadOperation(n); } }, + { "servers", (a, n) => { a.Servers = n.CreateSimpleList(s => new AsyncApiServerReference("#/servers/" + s.GetScalarValue())); } }, + { "subscribe", (a, n) => { /* happens after initial reading */ } }, + { "publish", (a, n) => { /* happens after initial reading */ } }, { "parameters", (a, n) => { a.Parameters = n.CreateMap(LoadParameter); } }, { "bindings", (a, n) => { a.Bindings = LoadChannelBindings(n); } }, }; @@ -22,7 +24,7 @@ internal static partial class AsyncApiV2Deserializer { s => s.StartsWith("x-"), (a, p, n) => a.AddExtension(p, LoadExtension(p, n)) }, }; - public static AsyncApiChannel LoadChannel(ParseNode node) + public static AsyncApiChannel LoadChannel(ParseNode node, string channelAddress = null) { var mapNode = node.CheckMapNode("channel"); var pointer = mapNode.GetReferencePointer(); @@ -31,11 +33,61 @@ public static AsyncApiChannel LoadChannel(ParseNode node) return new AsyncApiChannelReference(pointer); } - var pathItem = new AsyncApiChannel(); + var channel = new AsyncApiChannel(); - ParseMap(mapNode, pathItem, ChannelFixedFields, ChannelPatternFields); + ParseMap(mapNode, channel, ChannelFixedFields, ChannelPatternFields); + if (channelAddress != null) + { + channel.Address = channelAddress; + } + + LoadV2Operation(mapNode["subscribe"]?.Value, channel, AsyncApiAction.Send); + LoadV2Operation(mapNode["publish"]?.Value, channel, AsyncApiAction.Receive); + + return channel; + } + + public static string NormalizeChannelKey(string channelKey, ParseNode node = null) + { + string newKey = string.Empty; + foreach (var character in channelKey) + { + if (char.IsLetterOrDigit(character)) + { + newKey += character; + } + } + + if (node != null) + { + var addresses = node.Context.GetFromTempStorage>(TempStorageKeys.ChannelAddresses) ?? new Dictionary(); + addresses.Add(newKey, channelKey); + node.Context.SetTempStorage(TempStorageKeys.ChannelAddresses, addresses); + } + + return newKey; + } + + private static void LoadV2Operation(ParseNode node, AsyncApiChannel instance, AsyncApiAction action) + { + if (node == null) + { + return; + } + + var operation = LoadOperation(node); + var operationKey = node.CheckMapNode("operation")?["operationId"]?.Value.GetScalarValue() ?? "anonymous-operation-" + Interlocked.Increment(ref node.Context.OperationCounter).ToString(); + operation.Action = action; + operation.Channel = new AsyncApiChannelReference("#/channels/" + NormalizeChannelKey(instance.Address)); + + var globalOperations = node.Context.GetFromTempStorage>(TempStorageKeys.Operations) ?? new Dictionary(); + + if (!globalOperations.TryAdd(operationKey, operation)) + { + node.Context.Diagnostic.Errors.Add(new AsyncApiError(node.Context.GetLocation(), $"OperationId: '{operationKey}' is not unique.")); + } - return pathItem; + node.Context.SetTempStorage(TempStorageKeys.Operations, globalOperations); } } } diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiComponentsDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiComponentsDeserializer.cs index d9e7aa3..da5dbfd 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiComponentsDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiComponentsDeserializer.cs @@ -8,9 +8,9 @@ internal static partial class AsyncApiV2Deserializer { private static FixedFieldMap componentsFixedFields = new() { - { "schemas", (a, n) => a.Schemas = n.CreateMap(AsyncApiSchemaDeserializer.LoadSchema) }, + { "schemas", (a, n) => a.Schemas = n.CreateMap(LoadMultiSchemaFormat) }, { "servers", (a, n) => a.Servers = n.CreateMap(LoadServer) }, - { "channels", (a, n) => a.Channels = n.CreateMap(LoadChannel) }, + { "channels", (a, n) => a.Channels = n.CreateMap((key) => NormalizeChannelKey(key), (n, key) => LoadChannel(n, channelAddress: NormalizeChannelKey(key))) }, { "messages", (a, n) => a.Messages = n.CreateMap(LoadMessage) }, { "securitySchemes", (a, n) => a.SecuritySchemes = n.CreateMap(LoadSecurityScheme) }, { "parameters", (a, n) => a.Parameters = n.CreateMap(LoadParameter) }, @@ -38,5 +38,15 @@ public static AsyncApiComponents LoadComponents(ParseNode node) return components; } + + private static AsyncApiMultiFormatSchema LoadMultiSchemaFormat(ParseNode node) + { + var schemas = new AsyncApiMultiFormatSchema + { + Schema = AsyncApiSchemaDeserializer.LoadSchema(node), + }; + + return schemas; + } } } diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiDocumentDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiDocumentDeserializer.cs index 7ee0a16..ba54d28 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiDocumentDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiDocumentDeserializer.cs @@ -3,6 +3,8 @@ namespace ByteBard.AsyncAPI.Readers using ByteBard.AsyncAPI.Extensions; using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Readers.ParseNodes; + using System.Collections.Generic; + using System.Linq; internal static partial class AsyncApiV2Deserializer { @@ -11,12 +13,12 @@ internal static partial class AsyncApiV2Deserializer { "asyncapi", (a, n) => { a.Asyncapi = "2.6.0"; } }, { "id", (a, n) => a.Id = n.GetScalarValue() }, { "info", (a, n) => a.Info = LoadInfo(n) }, + { "components", (a, n) => a.Components = LoadComponents(n) }, // Load before anything else so upgrading can go smoothly. { "servers", (a, n) => a.Servers = n.CreateMap(LoadServer) }, { "defaultContentType", (a, n) => a.DefaultContentType = n.GetScalarValue() }, - { "channels", (a, n) => a.Channels = n.CreateMap(LoadChannel) }, - { "components", (a, n) => a.Components = LoadComponents(n) }, - { "tags", (a, n) => a.Tags = n.CreateList(LoadTag) }, - { "externalDocs", (a, n) => a.ExternalDocs = LoadExternalDocs(n) }, + { "channels", (a, n) => a.Channels = n.CreateMap(key => NormalizeChannelKey(key, n), (n2, originalKey) => LoadChannel(n2, channelAddress: originalKey)) }, + { "tags", (a, n) => a.Info.Tags = n.CreateList(LoadTag) }, + { "externalDocs", (a, n) => a.Info.ExternalDocs = LoadExternalDocs(n) }, }; private static PatternFieldMap asyncApiPatternFields = new() @@ -24,15 +26,106 @@ internal static partial class AsyncApiV2Deserializer { s => s.StartsWith("x-"), (a, p, n) => a.AddExtension(p, LoadExtension(p, n)) }, }; + private static void SetSecuritySchemeScopes(ParsingContext context, AsyncApiDocument document) + { + if (document.Components?.SecuritySchemes == null) + { return; } + foreach (var securityScheme in document.Components?.SecuritySchemes) + { + var scopes = context.GetFromTempStorage>(TempStorageKeys.SecuritySchemeScopes, securityScheme.Key); + if (scopes == null) + { + return; + } + + foreach (var scope in scopes) + { + securityScheme.Value.Scopes.Add(scope); + } + } + } + public static AsyncApiDocument LoadAsyncApi(RootNode rootNode) { var document = new AsyncApiDocument(); var asyncApiNode = rootNode.GetMap(); - ParseMap(asyncApiNode, document, asyncApiFixedFields, asyncApiPatternFields); + asyncApiNode.Context.Workspace.RegisterComponents(document); + + SetSecuritySchemeScopes(asyncApiNode.Context, document); + SetMessages(asyncApiNode.Context, document); + SetOperations(asyncApiNode.Context, document); return document; } + + private static void SetMessages(ParsingContext context, AsyncApiDocument document) + { + var messages = context.GetFromTempStorage>(TempStorageKeys.ComponentMessages); + if (messages == null) + { + return; + } + + foreach (var message in messages) + { + document?.Components?.Messages.Add(message.Key, message.Value); + } + } + + private static void SetOperations(ParsingContext context, AsyncApiDocument document) + { + var operations = context.GetFromTempStorage>(TempStorageKeys.Operations); + if (operations == null) + { + return; + } + + foreach (var operation in operations) + { + document.Operations.Add(operation); + if (operation.Value.Channel != null) + { + var messages = context.GetFromTempStorage>(TempStorageKeys.OperationMessageReferences, operation.Value); + var operationChannelFragmentKey = operation.Value.Channel.Reference.Reference.Split("/")[^1]; + var channel = document.Channels.FirstOrDefault(channel => channel.Key == operationChannelFragmentKey); + if (channel.Value == null) + { + // it most likely came from a components channel, so the reference will be wrong. + // Find the channel that references this operations channel, and move the reference. + var correctChannelReference = document.Channels.FirstOrDefault(channel => channel.Value is AsyncApiChannelReference reference && reference.Reference.Reference.EndsWith(operationChannelFragmentKey)); + if (correctChannelReference.Key != null) + { + operation.Value.Channel = new AsyncApiChannelReference("#/channels/" + correctChannelReference.Key); + channel = correctChannelReference; + } + else + { + continue; + } + } + + if (channel.Value is AsyncApiChannelReference channelReference) + { + channelReference.Reference.Workspace = context.Workspace; + // Set reference address to the key, as key in v2 is the address of v3. + var addresses = context.GetFromTempStorage>(TempStorageKeys.ChannelAddresses) ?? new Dictionary(); + channelReference.Address = addresses.GetValueOrDefault(channel.Key) ?? channel.Key; + } + + if (messages == null) + { + continue; + } + + foreach (var message in messages) + { + channel.Value.Messages.TryAdd(message.Key, message.Value); + operation.Value.Messages.Add(new AsyncApiMessageReference($"#/channels/{channel.Key}/messages/{message.Key}")); + } + } + } + } } } diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageDeserializer.cs index 07c2c11..d995c0f 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageDeserializer.cs @@ -17,19 +17,19 @@ internal static partial class AsyncApiV2Deserializer private static readonly FixedFieldMap messageFixedFields = new() { { - "messageId", (a, n) => { a.MessageId = n.GetScalarValue(); } + "messageId", (a, n) => { } }, { - "headers", (a, n) => { a.Headers = AsyncApiSchemaDeserializer.LoadSchema(n); } + "headers", (a, n) => { /* Loaded later */ } }, { - "payload", (a, n) => { a.Payload = null; /* resolved after the initial run */ } + "payload", (a, n) => { /* a.Payload = new AsyncApiMultiFormatSchema(); */ } }, { "correlationId", (a, n) => { a.CorrelationId = LoadCorrelationId(n); } }, { - "schemaFormat", (a, n) => { a.SchemaFormat = LoadSchemaFormat(n.GetScalarValue()); } + "schemaFormat", (a, n) => { /* a.Payload.SchemaFormat = n.GetScalarValue(); */ } }, { "contentType", (a, n) => { a.ContentType = n.GetScalarValue(); } @@ -63,19 +63,18 @@ internal static partial class AsyncApiV2Deserializer }, }; - public static IAsyncApiMessagePayload LoadJsonSchemaPayload(ParseNode n) + public static IAsyncApiSchema LoadJsonSchemaPayload(ParseNode n) { return LoadPayload(n, null); } - public static IAsyncApiMessagePayload LoadAvroPayload(ParseNode n) + public static IAsyncApiSchema LoadAvroPayload(ParseNode n) { return LoadPayload(n, "application/vnd.apache.avro"); } - private static IAsyncApiMessagePayload LoadPayload(ParseNode n, string format) + private static IAsyncApiSchema LoadPayload(ParseNode n, string format) { - if (n == null) { return null; @@ -91,7 +90,7 @@ private static IAsyncApiMessagePayload LoadPayload(ParseNode n, string format) return AsyncApiAvroSchemaDeserializer.LoadSchema(n); default: var supportedFormats = SupportedJsonSchemaFormats.Concat(SupportedAvroSchemaFormats); - throw new AsyncApiException($"'Could not deserialize Payload. Supported formats are {string.Join(", ", supportedFormats)}"); + throw new AsyncApiException($"Could not deserialize Payload. Supported formats are {string.Join(", ", supportedFormats)}"); } } @@ -141,7 +140,25 @@ public static AsyncApiMessage LoadMessage(ParseNode node) var message = new AsyncApiMessage(); ParseMap(mapNode, message, messageFixedFields, messagePatternFields); - message.Payload = LoadPayload(mapNode["payload"]?.Value, message.SchemaFormat); + + if (mapNode["headers"] != null) + { + message.Headers = new AsyncApiMultiFormatSchema { Schema = AsyncApiSchemaDeserializer.LoadSchema(mapNode["headers"].Value) }; + } + + if (mapNode["payload"] != null) + { + var schema = mapNode["schemaFormat"]?.Value.GetScalarValue(); + var payload = LoadPayload(mapNode["payload"].Value, schema); + if (payload is IAsyncApiReferenceable reference && !reference.Reference.IsExternal) + { + message.Payload = new AsyncApiMultiFormatSchemaReference(reference.Reference.Reference); + } + else + { + message.Payload = new AsyncApiMultiFormatSchema { Schema = payload, SchemaFormat = schema }; + } + } return message; } diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiExampleDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageExampleDeserializer.cs similarity index 100% rename from src/ByteBard.AsyncAPI.Readers/V2/AsyncApiExampleDeserializer.cs rename to src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageExampleDeserializer.cs diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageTraitDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageTraitDeserializer.cs index 9b65008..a013c01 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageTraitDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageTraitDeserializer.cs @@ -8,10 +8,9 @@ internal static partial class AsyncApiV2Deserializer { private static FixedFieldMap messageTraitFixedFields = new() { - { "messageId", (a, n) => { a.MessageId = n.GetScalarValue(); } }, - { "headers", (a, n) => { a.Headers = AsyncApiSchemaDeserializer.LoadSchema(n); } }, + { "headers", (a, n) => { a.Headers = new AsyncApiMultiFormatSchema { Schema = AsyncApiSchemaDeserializer.LoadSchema(n) }; } }, { "correlationId", (a, n) => { a.CorrelationId = LoadCorrelationId(n); } }, - { "schemaFormat", (a, n) => { a.SchemaFormat = n.GetScalarValue(); } }, + { "schemaFormat", (a, n) => { a.Headers.SchemaFormat = n.GetScalarValue(); } }, { "contentType", (a, n) => { a.ContentType = n.GetScalarValue(); } }, { "name", (a, n) => { a.Name = n.GetScalarValue(); } }, { "title", (a, n) => { a.Title = n.GetScalarValue(); } }, diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiOAuthFlowDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiOAuthFlowDeserializer.cs index 09d9ac5..1dd9efe 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiOAuthFlowDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiOAuthFlowDeserializer.cs @@ -32,7 +32,7 @@ internal static partial class AsyncApiV2Deserializer o.RefreshUrl = new Uri(n.GetScalarValue(), UriKind.RelativeOrAbsolute); } }, - { "scopes", (o, n) => o.Scopes = n.CreateSimpleMap(LoadString) }, + { "scopes", (o, n) => o.AvailableScopes = n.CreateSimpleMap(LoadString) }, }; private static readonly PatternFieldMap oAuthFlowPatternFields = diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiOperationDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiOperationDeserializer.cs index 77b0d4b..d1bac9b 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiOperationDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiOperationDeserializer.cs @@ -1,6 +1,7 @@ namespace ByteBard.AsyncAPI.Readers { using System.Collections.Generic; + using System.Threading; using ByteBard.AsyncAPI.Extensions; using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Readers.ParseNodes; @@ -11,7 +12,7 @@ internal static partial class AsyncApiV2Deserializer new() { { - "operationId", (a, n) => { a.OperationId = n.GetScalarValue(); } + "operationId", (a, n) => { } }, { "summary", (a, n) => { a.Summary = n.GetScalarValue(); } @@ -35,20 +36,67 @@ internal static partial class AsyncApiV2Deserializer "traits", (a, n) => { a.Traits = n.CreateList(LoadOperationTrait); } }, { - "message", (a, n) => { a.Message = LoadMessages(n); } + "message", (a, n) => { LoadMessages(n, a); } }, }; - private static IList LoadMessages(ParseNode n) + private static KeyValuePair GetMessage(ParseNode node) { - var mapNode = n.CheckMapNode("message"); + var messageNode = node.CheckMapNode("message"); + string key = string.Empty; + var message = LoadMessage(node); + if (message is AsyncApiMessageReference reference) + { + key = reference.Reference.Reference.Split("/")[^1]; + } + else if (messageNode["messageId"] != null) + { + key = messageNode["messageId"]?.Value.GetScalarValue(); + } + else + { + key = "anonymous-message-" + Interlocked.Increment(ref node.Context.MessageCounter).ToString(); + } + + return new KeyValuePair(key, message); + } + private static void LoadMessages(ParseNode n, AsyncApiOperation instance) + { + var mapNode = n.CheckMapNode("message"); + var messages = new Dictionary(); if (mapNode["oneOf"] != null) { - return mapNode["oneOf"].Value.CreateList(LoadMessage); + foreach (var node in (ListNode)mapNode["oneOf"].Value) + { + var kvp = GetMessage(node); + messages.Add(kvp.Key, kvp.Value); + } } + else + { + var kvp = GetMessage(n); + messages.Add(kvp.Key, kvp.Value); + } + + var componentMessageReferences = n.Context.GetFromTempStorage>(TempStorageKeys.OperationMessageReferences) ?? new Dictionary(); + var componentMessages = n.Context.GetFromTempStorage>(TempStorageKeys.ComponentMessages) ?? new Dictionary(); + + foreach (var message in messages) + { + if (message.Value is not AsyncApiMessageReference messageReference) + { + var componentReference = "#/components/messages/" + message.Key; + componentMessages.Add(message.Key, message.Value); + componentMessageReferences.Add(message.Key, new AsyncApiMessageReference(componentReference)); + continue; + } - return new List { LoadMessage(n) }; + componentMessageReferences.Add(message.Key, messageReference); + } + + n.Context.SetTempStorage(TempStorageKeys.ComponentMessages, componentMessages); + n.Context.SetTempStorage(TempStorageKeys.OperationMessageReferences, componentMessageReferences, instance); } private static readonly PatternFieldMap operationPatternFields = @@ -60,9 +108,12 @@ private static IList LoadMessages(ParseNode n) internal static AsyncApiOperation LoadOperation(ParseNode node) { var mapNode = node.CheckMapNode("operation"); + if (mapNode == null) + { + return null; + } var operation = new AsyncApiOperation(); - ParseMap(mapNode, operation, operationFixedFields, operationPatternFields); return operation; diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiOperationTraitDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiOperationTraitDeserializer.cs index 84aff64..ddabf70 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiOperationTraitDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiOperationTraitDeserializer.cs @@ -8,9 +8,10 @@ internal static partial class AsyncApiV2Deserializer { private static FixedFieldMap operationTraitFixedFields = new() { - { "operationId", (a, n) => { a.OperationId = n.GetScalarValue(); } }, + { "operationId", (a, n) => { a.Title = n.GetScalarValue(); } }, { "summary", (a, n) => { a.Summary = n.GetScalarValue(); } }, { "description", (a, n) => { a.Description = n.GetScalarValue(); } }, + { "security", (a, n) => { a.Security = n.CreateList(LoadSecurityRequirement); } }, { "tags", (a, n) => { a.Tags = n.CreateList(LoadTag); } }, { "externalDocs", (a, n) => { a.Tags = n.CreateList(LoadTag); } }, { "bindings", (a, n) => { a.Bindings = LoadOperationBindings(n); } }, diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiParameterDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiParameterDeserializer.cs index fecc64e..762ce2d 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiParameterDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiParameterDeserializer.cs @@ -3,13 +3,14 @@ namespace ByteBard.AsyncAPI.Readers using ByteBard.AsyncAPI.Extensions; using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Readers.ParseNodes; + using System.Linq; internal static partial class AsyncApiV2Deserializer { private static FixedFieldMap parameterFixedFields = new() { { "description", (a, n) => { a.Description = n.GetScalarValue(); } }, - { "schema", (a, n) => { a.Schema = AsyncApiSchemaDeserializer.LoadSchema(n); } }, + { "schema", (a, n) => { LoadParameterFromSchema(a, n); } }, { "location", (a, n) => { a.Location = n.GetScalarValue(); } }, }; @@ -19,6 +20,25 @@ internal static partial class AsyncApiV2Deserializer { s => s.StartsWith("x-"), (a, p, n) => a.AddExtension(p, LoadExtension(p, n)) }, }; + private static void LoadParameterFromSchema(AsyncApiParameter instance, ParseNode node) + { + var schema = AsyncApiSchemaDeserializer.LoadSchema(node); + if (schema.Enum.Any()) + { + instance.Enum = schema.Enum.Select(e => e.GetValue()).ToList(); + } + + if (schema.Default != null) + { + instance.Default = schema.Default.GetValue(); + } + + if (schema.Examples.Any()) + { + instance.Examples = schema.Examples.Select(e => e.GetValue()).ToList(); + } + } + public static AsyncApiParameter LoadParameter(ParseNode node) { var mapNode = node.CheckMapNode("parameter"); diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiSchemaDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiSchemaDeserializer.cs index 2a02ab5..1cecdf7 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiSchemaDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiSchemaDeserializer.cs @@ -5,7 +5,6 @@ namespace ByteBard.AsyncAPI.Readers using ByteBard.AsyncAPI.Extensions; using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Readers.ParseNodes; - using ByteBard.AsyncAPI.Writers; public class AsyncApiSchemaDeserializer { diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiSecurityRequirementDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiSecurityRequirementDeserializer.cs index 9332230..f9aa8fe 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiSecurityRequirementDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiSecurityRequirementDeserializer.cs @@ -9,20 +9,20 @@ namespace ByteBard.AsyncAPI.Readers /// internal static partial class AsyncApiV2Deserializer { - public static AsyncApiSecurityRequirement LoadSecurityRequirement(ParseNode node) + public static AsyncApiSecurityScheme LoadSecurityRequirement(ParseNode node) { var mapNode = node.CheckMapNode("security"); - var securityRequirement = new AsyncApiSecurityRequirement(); + var securityScheme = new AsyncApiSecurityScheme(); foreach (var property in mapNode) { var scheme = LoadSecuritySchemeByReference(mapNode.Context, property.Name); var scopes = property.Value.CreateSimpleList(value => value.GetScalarValue()); - if (scheme != null) { - securityRequirement.Add(scheme, scopes); + node.Context.SetTempStorage(TempStorageKeys.SecuritySchemeScopes, scopes, property.Name); + return scheme; } else { @@ -31,14 +31,14 @@ public static AsyncApiSecurityRequirement LoadSecurityRequirement(ParseNode node } } - return securityRequirement; + return null; } private static AsyncApiSecuritySchemeReference LoadSecuritySchemeByReference( ParsingContext context, string schemeName) { - var securitySchemeObject = new AsyncApiSecuritySchemeReference(schemeName); + var securitySchemeObject = new AsyncApiSecuritySchemeReference("#/components/securitySchemes/" + schemeName); return securitySchemeObject; } diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiSecuritySchemeDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiSecuritySchemeDeserializer.cs index 20d3377..ef39cda 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiSecuritySchemeDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiSecuritySchemeDeserializer.cs @@ -4,7 +4,6 @@ namespace ByteBard.AsyncAPI.Readers using ByteBard.AsyncAPI.Extensions; using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Readers.ParseNodes; - using ByteBard.AsyncAPI.Writers; /// /// Class containing logic to deserialize AsyncApi document into diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiServerDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiServerDeserializer.cs index 1ea5ccd..9370ef1 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiServerDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiServerDeserializer.cs @@ -3,6 +3,7 @@ namespace ByteBard.AsyncAPI.Readers using ByteBard.AsyncAPI.Extensions; using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Readers.ParseNodes; + using System; /// /// Class containing logic to deserialize AsyncApi document into @@ -13,7 +14,7 @@ internal static partial class AsyncApiV2Deserializer private static readonly FixedFieldMap serverFixedFields = new() { { - "url", (a, n) => { a.Url = n.GetScalarValue(); } + "url", (a, n) => { SetHostAndPathname(a, n); } }, { "description", (a, n) => { a.Description = n.GetScalarValue(); } @@ -38,6 +39,26 @@ internal static partial class AsyncApiV2Deserializer }, }; + private static void SetHostAndPathname(AsyncApiServer a, ParseNode n) + { + var value = n.GetScalarValue(); + if (!value.Contains("://")) + { + // Set arbitrary protocol. + value = "unknown://" + value; + } + + if (Uri.TryCreate(value, UriKind.RelativeOrAbsolute, out var uri)) + { + a.Host = uri.Authority; + a.PathName = uri.LocalPath == "/" ? null : uri.LocalPath; + } + else + { + a.Host = n.GetScalarValue(); + } + } + private static readonly PatternFieldMap serverPatternFields = new() { diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs index 738a5da..ec05781 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs @@ -36,14 +36,13 @@ public AsyncApiV2VersionService(AsyncApiDiagnostic diagnostic) [typeof(AsyncApiAvroSchema)] = AsyncApiAvroSchemaDeserializer.LoadSchema, [typeof(AsyncApiJsonSchema)] = AsyncApiV2Deserializer.LoadJsonSchemaPayload, [typeof(AsyncApiAvroSchema)] = AsyncApiV2Deserializer.LoadAvroPayload, - [typeof(AsyncApiSecurityRequirement)] = AsyncApiV2Deserializer.LoadSecurityRequirement, [typeof(AsyncApiSecurityScheme)] = AsyncApiV2Deserializer.LoadSecurityScheme, [typeof(AsyncApiServer)] = AsyncApiV2Deserializer.LoadServer, [typeof(AsyncApiServerVariable)] = AsyncApiV2Deserializer.LoadServerVariable, [typeof(AsyncApiTag)] = AsyncApiV2Deserializer.LoadTag, [typeof(AsyncApiMessage)] = AsyncApiV2Deserializer.LoadMessage, [typeof(AsyncApiMessageTrait)] = AsyncApiV2Deserializer.LoadMessageTrait, - [typeof(AsyncApiChannel)] = AsyncApiV2Deserializer.LoadChannel, + [typeof(AsyncApiChannel)] = (node) => AsyncApiV2Deserializer.LoadChannel(node), // Support for reading a channel fragment is limited. [typeof(AsyncApiBindings)] = AsyncApiV2Deserializer.LoadServerBindings, [typeof(AsyncApiBindings)] = AsyncApiV2Deserializer.LoadChannelBindings, [typeof(AsyncApiBindings)] = AsyncApiV2Deserializer.LoadMessageBindings, diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiChannelBindingDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiChannelBindingDeserializer.cs new file mode 100644 index 0000000..e533fde --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiChannelBindingDeserializer.cs @@ -0,0 +1,66 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using ByteBard.AsyncAPI.Exceptions; + using ByteBard.AsyncAPI.Extensions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Readers.ParseNodes; + + internal static partial class AsyncApiV3Deserializer + { + internal static AsyncApiBindings LoadChannelBindings(ParseNode node) + { + var mapNode = node.CheckMapNode("channelBindings"); + var pointer = mapNode.GetReferencePointer(); + if (pointer != null) + { + return new AsyncApiBindingsReference(pointer); + } + + var channelBindings = new AsyncApiBindings(); + foreach (var property in mapNode) + { + var channelBinding = LoadChannelBinding(property); + + if (channelBinding != null) + { + channelBindings.Add(channelBinding); + } + else + { + mapNode.Context.Diagnostic.Errors.Add( + new AsyncApiError(node.Context.GetLocation(), $"ChannelBinding {property.Name} is not found")); + } + } + + mapNode.ParseFields(channelBindings, channelBindingPatternFields); + + return channelBindings; + } + + private static readonly PatternFieldMap> channelBindingPatternFields = + new() + { + { s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n)) }, + }; + + private static IChannelBinding LoadChannelBinding(ParseNode node) + { + var property = node as PropertyNode; + try + { + if (node.Context.ChannelBindingParsers.TryGetValue(property.Name, out var parser)) + { + return parser.LoadBinding(property); + } + } + catch (AsyncApiException ex) + { + ex.Pointer = node.Context.GetLocation(); + node.Context.Diagnostic.Errors.Add(new AsyncApiError(ex)); + } + + return null; + } + } +} diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiChannelDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiChannelDeserializer.cs new file mode 100644 index 0000000..a9ecaf2 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiChannelDeserializer.cs @@ -0,0 +1,52 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using ByteBard.AsyncAPI.Extensions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers.ParseNodes; + + internal static partial class AsyncApiV3Deserializer + { + private static readonly FixedFieldMap ChannelFixedFields = new() + { + { "address", (a, n) => { a.Address = n.GetScalarValue(); } }, + { "messages", (a, n) => { a.Messages = n.CreateMap(LoadMessage); } }, + { "title", (a, n) => { a.Title = n.GetScalarValue(); } }, + { "summary", (a, n) => { a.Summary = n.GetScalarValue(); } }, + { "description", (a, n) => { a.Description = n.GetScalarValue(); } }, + { "servers", (a, n) => { a.Servers = n.CreateList(LoadServerReference); } }, + { "parameters", (a, n) => { a.Parameters = n.CreateMap(LoadParameter); } }, + { "tags", (a, n) => { a.Tags = n.CreateList(LoadTag); } }, + { "externalDocs", (a, n) => { a.ExternalDocs = LoadExternalDocs(n); } }, + { "bindings", (a, n) => { a.Bindings = LoadChannelBindings(n); } }, + }; + + private static readonly PatternFieldMap ChannelPatternFields = + new() + { + { s => s.StartsWith("x-"), (a, p, n) => a.AddExtension(p, LoadExtension(p, n)) }, + }; + + public static AsyncApiChannelReference LoadChannelReference(ParseNode node) + { + var mapNode = node.CheckMapNode("channel"); + var pointer = mapNode.GetReferencePointer(); + return new AsyncApiChannelReference(pointer); + } + + public static AsyncApiChannel LoadChannel(ParseNode node) + { + var mapNode = node.CheckMapNode("channel"); + var pointer = mapNode.GetReferencePointer(); + if (pointer != null) + { + return new AsyncApiChannelReference(pointer); + } + + var channel = new AsyncApiChannel(); + + ParseMap(mapNode, channel, ChannelFixedFields, ChannelPatternFields); + + return channel; + } + } +} diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiComponentsDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiComponentsDeserializer.cs new file mode 100644 index 0000000..914a491 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiComponentsDeserializer.cs @@ -0,0 +1,48 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using ByteBard.AsyncAPI.Extensions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers.ParseNodes; + + internal static partial class AsyncApiV3Deserializer + { + private static FixedFieldMap componentsFixedFields = new() + { + { "schemas", (a, n) => a.Schemas = n.CreateMap(LoadMultiFormatSchema) }, + { "servers", (a, n) => a.Servers = n.CreateMap(LoadServer) }, + { "channels", (a, n) => a.Channels = n.CreateMap(LoadChannel) }, + { "operations", (a, n) => a.Operations = n.CreateMap(LoadOperation) }, + { "messages", (a, n) => a.Messages = n.CreateMap(LoadMessage) }, + { "securitySchemes", (a, n) => a.SecuritySchemes = n.CreateMap(LoadSecurityScheme) }, + { "serverVariables", (a, n) => a.ServerVariables = n.CreateMap(LoadServerVariable) }, + { "parameters", (a, n) => a.Parameters = n.CreateMap(LoadParameter) }, + { "correlationIds", (a, n) => a.CorrelationIds = n.CreateMap(LoadCorrelationId) }, + { "replies", (a, n) => a.Replies = n.CreateMap(LoadOperationReply) }, + { "replyAddresses", (a, n) => a.ReplyAddresses = n.CreateMap(LoadOperationReplyAddress) }, + { "externalDocs", (a, n) => a.ExternalDocs = n.CreateMap(LoadExternalDocs) }, + { "tags", (a, n) => a.Tags = n.CreateMap(LoadTag) }, + { "operationTraits", (a, n) => a.OperationTraits = n.CreateMap(LoadOperationTrait) }, + { "messageTraits", (a, n) => a.MessageTraits = n.CreateMap(LoadMessageTrait) }, + { "serverBindings", (a, n) => a.ServerBindings = n.CreateMap(LoadServerBindings) }, + { "channelBindings", (a, n) => a.ChannelBindings = n.CreateMap(LoadChannelBindings) }, + { "operationBindings", (a, n) => a.OperationBindings = n.CreateMap(LoadOperationBindings) }, + { "messageBindings", (a, n) => a.MessageBindings = n.CreateMap(LoadMessageBindings) }, + }; + + private static PatternFieldMap componentsPatternFields = + new() + { + { s => s.StartsWith("x-"), (a, p, n) => a.AddExtension(p, LoadExtension(p, n)) }, + }; + + public static AsyncApiComponents LoadComponents(ParseNode node) + { + var mapNode = node.CheckMapNode("components"); + var components = new AsyncApiComponents(); + + ParseMap(mapNode, components, componentsFixedFields, componentsPatternFields); + + return components; + } + } +} diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiContactDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiContactDeserializer.cs new file mode 100644 index 0000000..915c766 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiContactDeserializer.cs @@ -0,0 +1,36 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using System; + using ByteBard.AsyncAPI.Extensions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers.ParseNodes; + + /// + /// Class containing logic to deserialize AsyncApi document into + /// runtime AsyncApi object model. + /// + internal static partial class AsyncApiV3Deserializer + { + private static FixedFieldMap contactFixedFields = new() + { + { "name", (o, n) => { o.Name = n.GetScalarValue(); } }, + { "email", (o, n) => { o.Email = n.GetScalarValue(); } }, + { "url", (o, n) => { o.Url = new Uri(n.GetScalarValue(), UriKind.RelativeOrAbsolute); } }, + }; + + private static PatternFieldMap contactPatternFields = new() + { + { s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n)) }, + }; + + public static AsyncApiContact LoadContact(ParseNode node) + { + var mapNode = node as MapNode; + var contact = new AsyncApiContact(); + + ParseMap(mapNode, contact, contactFixedFields, contactPatternFields); + + return contact; + } + } +} diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiCorrelationIdDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiCorrelationIdDeserializer.cs new file mode 100644 index 0000000..31e4156 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiCorrelationIdDeserializer.cs @@ -0,0 +1,44 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using ByteBard.AsyncAPI.Extensions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers.ParseNodes; + + /// + /// Class containing logic to deserialize AsyncAPI document into + /// runtime AsyncAPI object model. + /// + internal static partial class AsyncApiV3Deserializer + { + private static readonly FixedFieldMap correlationIdFixedFileds = + new() + { + { "description", (a, n) => { a.Description = n.GetScalarValue(); } }, + { "location", (a, n) => { a.Location = n.GetScalarValue(); } }, + }; + + private static readonly PatternFieldMap correlationIdPatternFields = + new() + { + { s => s.StartsWith("x-"), (a, p, n) => a.AddExtension(p, LoadExtension(p, n)) }, + }; + + public static AsyncApiCorrelationId LoadCorrelationId(ParseNode node) + { + var mapNode = node.CheckMapNode("correlationId"); + var pointer = mapNode.GetReferencePointer(); + if (pointer != null) + { + return new AsyncApiCorrelationIdReference(pointer); + } + + var correlationId = new AsyncApiCorrelationId(); + foreach (var property in mapNode) + { + property.ParseField(correlationId, correlationIdFixedFileds, correlationIdPatternFields); + } + + return correlationId; + } + } +} diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiDeserializer.cs new file mode 100644 index 0000000..f0bffd3 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiDeserializer.cs @@ -0,0 +1,159 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using System.Collections.Generic; + using System.Linq; + using ByteBard.AsyncAPI.Exceptions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Readers.ParseNodes; + + internal static partial class AsyncApiV3Deserializer + { + internal static void ParseMap( + MapNode mapNode, + T domainObject, + FixedFieldMap fixedFieldMap, + PatternFieldMap patternFieldMap) + { + if (mapNode == null) + { + return; + } + + foreach (var propertyNode in mapNode) + { + propertyNode.ParseField(domainObject, fixedFieldMap, patternFieldMap); + } + } + + internal static void ProcessAnyFields( + MapNode mapNode, + T domainObject, + AnyFieldMap anyFieldMap) + { + foreach (var anyFieldName in anyFieldMap.Keys.ToList()) + { + try + { + mapNode.Context.StartObject(anyFieldName); + + var anyFieldValue = anyFieldMap[anyFieldName].PropertyGetter(domainObject); + if (anyFieldValue == null) + { + anyFieldMap[anyFieldName].PropertySetter(domainObject, null); + } + else + { + anyFieldMap[anyFieldName].PropertySetter(domainObject, anyFieldValue); + } + } + catch (AsyncApiException exception) + { + exception.Pointer = mapNode.Context.GetLocation(); + mapNode.Context.Diagnostic.Errors.Add(new AsyncApiError(exception)); + } + finally + { + mapNode.Context.EndObject(); + } + } + } + + internal static void ProcessAnyListFields( + MapNode mapNode, + T domainObject, + AnyListFieldMap anyListFieldMap) + { + foreach (var anyListFieldName in anyListFieldMap.Keys.ToList()) + { + try + { + var newProperty = new List(); + + mapNode.Context.StartObject(anyListFieldName); + + foreach (var propertyElement in anyListFieldMap[anyListFieldName].PropertyGetter(domainObject)) + { + newProperty.Add(propertyElement); + } + + anyListFieldMap[anyListFieldName].PropertySetter(domainObject, newProperty); + } + catch (AsyncApiException exception) + { + exception.Pointer = mapNode.Context.GetLocation(); + mapNode.Context.Diagnostic.Errors.Add(new AsyncApiError(exception)); + } + finally + { + mapNode.Context.EndObject(); + } + } + } + + private static void ProcessAnyMapFields( + MapNode mapNode, + T domainObject, + AnyMapFieldMap anyMapFieldMap) + { + foreach (var anyMapFieldName in anyMapFieldMap.Keys.ToList()) + { + try + { + var newProperty = new List(); + + mapNode.Context.StartObject(anyMapFieldName); + + foreach (var propertyMapElement in anyMapFieldMap[anyMapFieldName].PropertyMapGetter(domainObject)) + { + mapNode.Context.StartObject(propertyMapElement.Key); + + if (propertyMapElement.Value != null) + { + var any = anyMapFieldMap[anyMapFieldName].PropertyGetter(propertyMapElement.Value); + + anyMapFieldMap[anyMapFieldName].PropertySetter(propertyMapElement.Value, any); + } + } + } + catch (AsyncApiException exception) + { + exception.Pointer = mapNode.Context.GetLocation(); + mapNode.Context.Diagnostic.Errors.Add(new AsyncApiError(exception)); + } + finally + { + mapNode.Context.EndObject(); + } + } + } + + public static AsyncApiAny LoadAny(ParseNode node) + { + return node.CreateAny(); + } + + public static IAsyncApiExtension LoadExtension(string name, ParseNode node) + { + try + { + if (node.Context.ExtensionParsers.TryGetValue(name, out var parser)) + { + return parser(node.CreateAny()); + } + } + catch (AsyncApiException ex) + { + ex.Pointer = node.Context.GetLocation(); + node.Context.Diagnostic.Errors.Add(new AsyncApiError(ex)); + } + + return node.CreateAny(); + } + + private static string LoadString(ParseNode node) + { + return node.GetScalarValue(); + } + } +} diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiDocumentDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiDocumentDeserializer.cs new file mode 100644 index 0000000..38890a0 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiDocumentDeserializer.cs @@ -0,0 +1,37 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using ByteBard.AsyncAPI.Extensions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers.ParseNodes; + + internal static partial class AsyncApiV3Deserializer + { + private static FixedFieldMap asyncApiFixedFields = new() + { + { "asyncapi", (a, n) => { a.Asyncapi = "3.0.0"; } }, + { "id", (a, n) => a.Id = n.GetScalarValue() }, + { "info", (a, n) => a.Info = LoadInfo(n) }, + { "servers", (a, n) => a.Servers = n.CreateMap(LoadServer) }, + { "defaultContentType", (a, n) => a.DefaultContentType = n.GetScalarValue() }, + { "channels", (a, n) => a.Channels = n.CreateMap(LoadChannel) }, + { "operations", (a, n) => a.Operations = n.CreateMap(LoadOperation) }, + { "components", (a, n) => a.Components = LoadComponents(n) }, + }; + + private static PatternFieldMap asyncApiPatternFields = new() + { + { s => s.StartsWith("x-"), (a, p, n) => a.AddExtension(p, LoadExtension(p, n)) }, + }; + + public static AsyncApiDocument LoadAsyncApi(RootNode rootNode) + { + var document = new AsyncApiDocument(); + + var asyncApiNode = rootNode.GetMap(); + + ParseMap(asyncApiNode, document, asyncApiFixedFields, asyncApiPatternFields); + + return document; + } + } +} diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiExternalDocsDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiExternalDocsDeserializer.cs new file mode 100644 index 0000000..f6ba1a4 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiExternalDocsDeserializer.cs @@ -0,0 +1,38 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using System; + using ByteBard.AsyncAPI.Extensions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers.ParseNodes; + + internal static partial class AsyncApiV3Deserializer + { + private static FixedFieldMap externalDocumentationFixedFields = new() + { + { "description", (a, n) => { a.Description = n.GetScalarValue(); } }, + { "url", (a, n) => { a.Url = new Uri(n.GetScalarValue()); } }, + }; + + private static PatternFieldMap externalDocumentationPatternFields = + new() + { + { s => s.StartsWith("x-"), (a, p, n) => a.AddExtension(p, LoadExtension(p, n)) }, + }; + + public static AsyncApiExternalDocumentation LoadExternalDocs(ParseNode node) + { + var mapNode = node.CheckMapNode("externalDocs"); + var pointer = mapNode.GetReferencePointer(); + if (pointer != null) + { + return new AsyncApiExternalDocumentationReference(pointer); + } + + var components = new AsyncApiExternalDocumentation(); + + ParseMap(mapNode, components, externalDocumentationFixedFields, externalDocumentationPatternFields); + + return components; + } + } +} diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiInfoDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiInfoDeserializer.cs new file mode 100644 index 0000000..6e169e7 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiInfoDeserializer.cs @@ -0,0 +1,38 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using System; + using ByteBard.AsyncAPI.Extensions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers.ParseNodes; + + internal static partial class AsyncApiV3Deserializer + { + private static FixedFieldMap infoFixedFields = new() + { + { "title", (a, n) => { a.Title = n.GetScalarValue(); } }, + { "version", (a, n) => { a.Version = n.GetScalarValue(); } }, + { "description", (a, n) => { a.Description = n.GetScalarValue(); } }, + { "termsOfService", (a, n) => { a.TermsOfService = new Uri(n.GetScalarValue()); } }, + { "contact", (a, n) => { a.Contact = LoadContact(n); } }, + { "license", (a, n) => { a.License = LoadLicense(n); } }, + { "tags", (a, n) => { a.Tags = n.CreateList(LoadTag); } }, + { "externalDocs", (a, n) => { a.ExternalDocs = LoadExternalDocs(n); } }, + }; + + private static PatternFieldMap infoPatternFields = + new() + { + { s => s.StartsWith("x-"), (a, p, n) => a.AddExtension(p, LoadExtension(p, n)) }, + }; + + public static AsyncApiInfo LoadInfo(ParseNode node) + { + var mapNode = node.CheckMapNode("info"); + var info = new AsyncApiInfo(); + + ParseMap(mapNode, info, infoFixedFields, infoPatternFields); + + return info; + } + } +} diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiLicenseDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiLicenseDeserializer.cs new file mode 100644 index 0000000..8e73da5 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiLicenseDeserializer.cs @@ -0,0 +1,32 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using System; + using ByteBard.AsyncAPI.Extensions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers.ParseNodes; + + internal static partial class AsyncApiV3Deserializer + { + private static FixedFieldMap licenseFixedFields = new() + { + { "name", (a, n) => { a.Name = n.GetScalarValue(); } }, + { "url", (a, n) => { a.Url = new Uri(n.GetScalarValue()); } }, + }; + + private static PatternFieldMap licensePatternFields = + new() + { + { s => s.StartsWith("x-"), (a, p, n) => a.AddExtension(p, LoadExtension(p, n)) }, + }; + + public static AsyncApiLicense LoadLicense(ParseNode node) + { + var mapNode = node.CheckMapNode("license"); + var license = new AsyncApiLicense(); + + ParseMap(mapNode, license, licenseFixedFields, licensePatternFields); + + return license; + } + } +} diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMessageBindingDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMessageBindingDeserializer.cs new file mode 100644 index 0000000..018e6db --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMessageBindingDeserializer.cs @@ -0,0 +1,66 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using ByteBard.AsyncAPI.Exceptions; + using ByteBard.AsyncAPI.Extensions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Readers.ParseNodes; + + internal static partial class AsyncApiV3Deserializer + { + internal static AsyncApiBindings LoadMessageBindings(ParseNode node) + { + var mapNode = node.CheckMapNode("messageBindings"); + var pointer = mapNode.GetReferencePointer(); + if (pointer != null) + { + return new AsyncApiBindingsReference(pointer); + } + + var messageBindings = new AsyncApiBindings(); + foreach (var property in mapNode) + { + var messageBinding = LoadMessageBinding(property); + + if (messageBinding != null) + { + messageBindings.Add(messageBinding); + } + else + { + mapNode.Context.Diagnostic.Errors.Add( + new AsyncApiError(node.Context.GetLocation(), $"MessageBinding {property.Name} is not found")); + } + } + + mapNode.ParseFields(messageBindings, messageBindingPatternFields); + + return messageBindings; + } + + private static readonly PatternFieldMap> messageBindingPatternFields = + new() + { + { s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n)) }, + }; + + internal static IMessageBinding LoadMessageBinding(ParseNode node) + { + var property = node as PropertyNode; + try + { + if (node.Context.MessageBindingParsers.TryGetValue(property.Name, out var parser)) + { + return parser.LoadBinding(property); + } + } + catch (AsyncApiException ex) + { + ex.Pointer = node.Context.GetLocation(); + node.Context.Diagnostic.Errors.Add(new AsyncApiError(ex)); + } + + return null; + } + } +} diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMessageDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMessageDeserializer.cs new file mode 100644 index 0000000..b2c31f6 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMessageDeserializer.cs @@ -0,0 +1,95 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using ByteBard.AsyncAPI.Extensions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers.ParseNodes; + + /// + /// Class containing logic to deserialize AsyncApi document into + /// runtime AsyncApi object model. + /// + internal static partial class AsyncApiV3Deserializer + { + private static readonly FixedFieldMap messageFixedFields = new() + { + { + "messageId", (a, n) => { } + }, + { + "headers", (a, n) => { a.Headers = LoadMultiFormatSchema(n); } + }, + { + "payload", (a, n) => { a.Payload = LoadMultiFormatSchema(n); } + }, + { + "correlationId", (a, n) => { a.CorrelationId = LoadCorrelationId(n); } + }, + { + "schemaFormat", (a, n) => { /* loaded as part of multiformatschema */ } + }, + { + "contentType", (a, n) => { a.ContentType = n.GetScalarValue(); } + }, + { + "name", (a, n) => { a.Name = n.GetScalarValue(); } + }, + { + "title", (a, n) => { a.Title = n.GetScalarValue(); } + }, + { + "summary", (a, n) => { a.Summary = n.GetScalarValue(); } + }, + { + "description", (a, n) => { a.Description = n.GetScalarValue(); } + }, + { + "tags", (a, n) => a.Tags = n.CreateList(LoadTag) + }, + { + "externalDocs", (a, n) => { a.ExternalDocs = LoadExternalDocs(n); } + }, + { + "bindings", (a, n) => { a.Bindings = LoadMessageBindings(n); } + }, + { + "examples", (a, n) => a.Examples = n.CreateList(LoadExample) + }, + { + "traits", (a, n) => a.Traits = n.CreateList(LoadMessageTrait) + }, + }; + + private static readonly PatternFieldMap messagePatternFields = new() + { + { s => s.StartsWith("x-"), (a, p, n) => a.AddExtension(p, LoadExtension(p, n)) }, + }; + + public static AsyncApiMessageReference LoadMessageReference(ParseNode node) + { + var mapNode = node.CheckMapNode("message"); + var pointer = mapNode.GetReferencePointer(); + if (pointer != null) + { + return new AsyncApiMessageReference(pointer); + } + + return null; + } + + public static AsyncApiMessage LoadMessage(ParseNode node) + { + var mapNode = node.CheckMapNode("message"); + var pointer = mapNode.GetReferencePointer(); + if (pointer != null) + { + return new AsyncApiMessageReference(pointer); + } + + var message = new AsyncApiMessage(); + + ParseMap(mapNode, message, messageFixedFields, messagePatternFields); + + return message; + } + } +} \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMessageExampleDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMessageExampleDeserializer.cs new file mode 100644 index 0000000..8b610ba --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMessageExampleDeserializer.cs @@ -0,0 +1,33 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using ByteBard.AsyncAPI.Extensions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers.ParseNodes; + + internal static partial class AsyncApiV3Deserializer + { + private static FixedFieldMap exampleFixedFields = new() + { + { "headers", (a, n) => { a.Headers = n.CreateMap(LoadAny); } }, + { "payload", (a, n) => { a.Payload = n.CreateAny(); } }, + { "name", (a, n) => { a.Name = n.GetScalarValue(); } }, + { "summary", (a, n) => { a.Summary = n.GetScalarValue(); } }, + }; + + private static PatternFieldMap examplePatternFields = + new() + { + { s => s.StartsWith("x-"), (a, p, n) => a.AddExtension(p, LoadExtension(p, n)) }, + }; + + public static AsyncApiMessageExample LoadExample(ParseNode node) + { + var mapNode = node.CheckMapNode("example"); + var example = new AsyncApiMessageExample(); + + ParseMap(mapNode, example, exampleFixedFields, examplePatternFields); + + return example; + } + } +} diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMessageTraitDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMessageTraitDeserializer.cs new file mode 100644 index 0000000..dfa8f31 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMessageTraitDeserializer.cs @@ -0,0 +1,46 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using ByteBard.AsyncAPI.Extensions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers.ParseNodes; + + internal static partial class AsyncApiV3Deserializer + { + private static FixedFieldMap messageTraitFixedFields = new() + { + { "headers", (a, n) => { a.Headers = LoadMultiFormatSchema(n); } }, + { "correlationId", (a, n) => { a.CorrelationId = LoadCorrelationId(n); } }, + { "contentType", (a, n) => { a.ContentType = n.GetScalarValue(); } }, + { "name", (a, n) => { a.Name = n.GetScalarValue(); } }, + { "title", (a, n) => { a.Title = n.GetScalarValue(); } }, + { "summary", (a, n) => { a.Summary = n.GetScalarValue(); } }, + { "description", (a, n) => { a.Description = n.GetScalarValue(); } }, + { "tags", (a, n) => { a.Tags = n.CreateList(LoadTag); } }, + { "externalDocs", (a, n) => { a.ExternalDocs = LoadExternalDocs(n); } }, + { "bindings", (a, n) => { a.Bindings = LoadMessageBindings(n); } }, + { "examples", (a, n) => { a.Examples = n.CreateList(LoadExample); } }, + }; + + private static PatternFieldMap messageTraitPatternFields = + new() + { + { s => s.StartsWith("x-"), (a, p, n) => a.AddExtension(p, LoadExtension(p, n)) }, + }; + + public static AsyncApiMessageTrait LoadMessageTrait(ParseNode node) + { + var mapNode = node.CheckMapNode("traits"); + var pointer = mapNode.GetReferencePointer(); + + if (pointer != null) + { + return new AsyncApiMessageTraitReference(pointer); + } + + var messageTrait = new AsyncApiMessageTrait(); + + ParseMap(mapNode, messageTrait, messageTraitFixedFields, messageTraitPatternFields); + return messageTrait; + } + } +} diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMultiFormatSchemaDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMultiFormatSchemaDeserializer.cs new file mode 100644 index 0000000..5b132ca --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMultiFormatSchemaDeserializer.cs @@ -0,0 +1,90 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using System.Collections.Generic; + using System.Linq; + using ByteBard.AsyncAPI.Exceptions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Readers.ParseNodes; + + internal static partial class AsyncApiV3Deserializer + { + public static AsyncApiMultiFormatSchema LoadMultiFormatSchema(ParseNode node) + { + var mapNode = node.CheckMapNode("MultiFormatSchema"); + var pointer = mapNode.GetReferencePointer(); + + var schemaFormat = new AsyncApiMultiFormatSchema(); + var defaultSchemaFormat = "application/vnd.aai.asyncapi+json;version=3.0.0"; + if (pointer != null) + { + return new AsyncApiMultiFormatSchemaReference(pointer); + } + + // Not a pointer and no schemaFormat means it MUST be a jsonSchema, + if (mapNode["schemaFormat"] == null) + { + schemaFormat.Schema = LoadSchema(node, defaultSchemaFormat); + schemaFormat.SchemaFormat = defaultSchemaFormat; + return schemaFormat; + } + + var format = mapNode["schemaFormat"].Value.GetScalarValue(); + var schema = mapNode["schema"].Value; + schemaFormat.Schema = LoadSchema(schema, LoadSchemaFormat(format)); + schemaFormat.SchemaFormat = format; + return schemaFormat; + + } + + private static IAsyncApiSchema LoadSchema(ParseNode n, string format) + { + if (n == null) + { + return null; + } + + switch (format) + { + case null: + case "": + case var _ when SupportedJsonSchemaFormats.Where(s => format.StartsWith(s)).Any(): + return AsyncApiSchemaDeserializer.LoadSchema(n); + case var _ when SupportedAvroSchemaFormats.Where(s => format.StartsWith(s)).Any(): + return AsyncApiAvroSchemaDeserializer.LoadSchema(n); + default: + var supportedFormats = SupportedJsonSchemaFormats.Concat(SupportedAvroSchemaFormats); + throw new AsyncApiException($"Could not deserialize Schema. Supported formats are {string.Join(", ", supportedFormats)}"); + } + } + + static readonly IEnumerable SupportedJsonSchemaFormats = new List + { + "application/vnd.aai.asyncapi+json", + "application/vnd.aai.asyncapi+yaml", + "application/vnd.aai.asyncapi", + "application/schema+json;version=draft-07", + "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) + { + 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(", ", supportedFormats)}"); + } + + return schemaFormat; + } + } +} \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOAuthFlowDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOAuthFlowDeserializer.cs new file mode 100644 index 0000000..fed2a08 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOAuthFlowDeserializer.cs @@ -0,0 +1,57 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using System; + using ByteBard.AsyncAPI.Extensions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers.ParseNodes; + + /// + /// Class containing logic to deserialize AsyncAPI document into + /// runtime AsyncAPI object model. + /// + internal static partial class AsyncApiV3Deserializer + { + private static readonly FixedFieldMap oAuthFlowFixedFields = + new() + { + { + "authorizationUrl", (o, n) => + { + o.AuthorizationUrl = new Uri(n.GetScalarValue(), UriKind.RelativeOrAbsolute); + } + }, + { + "tokenUrl", (o, n) => + { + o.TokenUrl = new Uri(n.GetScalarValue(), UriKind.RelativeOrAbsolute); + } + }, + { + "refreshUrl", (o, n) => + { + o.RefreshUrl = new Uri(n.GetScalarValue(), UriKind.RelativeOrAbsolute); + } + }, + { "availableScopes", (o, n) => o.AvailableScopes = n.CreateSimpleMap(LoadString) }, + }; + + private static readonly PatternFieldMap oAuthFlowPatternFields = + new() + { + { s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p,n)) }, + }; + + public static AsyncApiOAuthFlow LoadOAuthFlow(ParseNode node) + { + var mapNode = node.CheckMapNode("OAuthFlow"); + + var oauthFlow = new AsyncApiOAuthFlow(); + foreach (var property in mapNode) + { + property.ParseField(oauthFlow, oAuthFlowFixedFields, oAuthFlowPatternFields); + } + + return oauthFlow; + } + } +} diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOAuthFlowsDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOAuthFlowsDeserializer.cs new file mode 100644 index 0000000..9afac0a --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOAuthFlowsDeserializer.cs @@ -0,0 +1,41 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using ByteBard.AsyncAPI.Extensions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers.ParseNodes; + + /// + /// Class containing logic to deserialize AsyncAPI document into + /// runtime AsyncAPI object model. + /// + internal static partial class AsyncApiV3Deserializer + { + private static readonly FixedFieldMap oAuthFlowsFixedFileds = + new() + { + { "implicit", (a, n) => a.Implicit = LoadOAuthFlow(n) }, + { "password", (a, n) => a.Password = LoadOAuthFlow(n) }, + { "clientCredentials", (a, n) => a.ClientCredentials = LoadOAuthFlow(n) }, + { "authorizationCode", (a, n) => a.AuthorizationCode = LoadOAuthFlow(n) }, + }; + + private static readonly PatternFieldMap oAuthFlowsPatternFields = + new() + { + { s => s.StartsWith("x-"), (a, p, n) => a.AddExtension(p, LoadExtension(p, n)) }, + }; + + public static AsyncApiOAuthFlows LoadOAuthFlows(ParseNode node) + { + var mapNode = node.CheckMapNode("OAuthFlows"); + + var oAuthFlows = new AsyncApiOAuthFlows(); + foreach (var property in mapNode) + { + property.ParseField(oAuthFlows, oAuthFlowsFixedFileds, oAuthFlowsPatternFields); + } + + return oAuthFlows; + } + } +} \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOperationBindingDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOperationBindingDeserializer.cs new file mode 100644 index 0000000..435c8e2 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOperationBindingDeserializer.cs @@ -0,0 +1,66 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using ByteBard.AsyncAPI.Exceptions; + using ByteBard.AsyncAPI.Extensions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Readers.ParseNodes; + + internal static partial class AsyncApiV3Deserializer + { + internal static AsyncApiBindings LoadOperationBindings(ParseNode node) + { + var mapNode = node.CheckMapNode("operationBindings"); + var pointer = mapNode.GetReferencePointer(); + if (pointer != null) + { + return new AsyncApiBindingsReference(pointer); + } + + var operationBindings = new AsyncApiBindings(); + foreach (var property in mapNode) + { + var operationBinding = LoadOperationBinding(property); + + if (operationBinding != null) + { + operationBindings.Add(operationBinding); + } + else + { + mapNode.Context.Diagnostic.Errors.Add( + new AsyncApiError(node.Context.GetLocation(), $"OperationBinding '{property.Name}' was not found")); + } + } + + mapNode.ParseFields(operationBindings, operationBindingPatternFields); + + return operationBindings; + } + + private static readonly PatternFieldMap> operationBindingPatternFields = + new() + { + { s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n)) }, + }; + + internal static IOperationBinding LoadOperationBinding(ParseNode node) + { + var property = node as PropertyNode; + try + { + if (node.Context.OperationBindingParsers.TryGetValue(property.Name, out var parser)) + { + return parser.LoadBinding(property); + } + } + catch (AsyncApiException ex) + { + ex.Pointer = node.Context.GetLocation(); + node.Context.Diagnostic.Errors.Add(new AsyncApiError(ex)); + } + + return null; + } + } +} diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOperationDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOperationDeserializer.cs new file mode 100644 index 0000000..0589811 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOperationDeserializer.cs @@ -0,0 +1,67 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using ByteBard.AsyncAPI.Extensions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers.ParseNodes; + + internal static partial class AsyncApiV3Deserializer + { + private static readonly FixedFieldMap operationFixedFields = + new() + { + { + "action", (a, n) => { a.Action = n.GetScalarValue().GetEnumFromDisplayName(); } + }, + { + "channel", (a, n) => { a.Channel = LoadChannelReference(n); } + }, + { + "title", (a, n) => { a.Title = n.GetScalarValue(); } + }, + { + "summary", (a, n) => { a.Summary = n.GetScalarValue(); } + }, + { + "description", (a, n) => { a.Description = n.GetScalarValue(); } + }, + { + "security", (a, n) => { a.Security = n.CreateList(LoadSecurityScheme); } + }, + { + "tags", (a, n) => a.Tags = n.CreateList(LoadTag) + }, + { + "externalDocs", (a, n) => { a.ExternalDocs = LoadExternalDocs(n); } + }, + { + "bindings", (a, n) => { a.Bindings = LoadOperationBindings(n); } + }, + { + "traits", (a, n) => { a.Traits = n.CreateList(LoadOperationTrait); } + }, + { + "messages", (a, n) => { a.Messages = n.CreateList(LoadMessageReference); } + }, + { + "reply", (a, n) => { a.Reply = LoadOperationReply(n); } + }, + }; + + private static readonly PatternFieldMap operationPatternFields = + new() + { + { s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n)) }, + }; + + internal static AsyncApiOperation LoadOperation(ParseNode node) + { + var mapNode = node.CheckMapNode("operation"); + + var operation = new AsyncApiOperation(); + + ParseMap(mapNode, operation, operationFixedFields, operationPatternFields); + + return operation; + } + } +} \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOperationReplyAddressDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOperationReplyAddressDeserializer.cs new file mode 100644 index 0000000..9fe3678 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOperationReplyAddressDeserializer.cs @@ -0,0 +1,37 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using ByteBard.AsyncAPI.Extensions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers.ParseNodes; + + internal static partial class AsyncApiV3Deserializer + { + private static FixedFieldMap replyAddressFixedFields = new() + { + { "description", (a, n) => { a.Description = n.GetScalarValue(); } }, + { "location", (a, n) => { a.Location = n.GetScalarValue(); } }, + }; + + private static PatternFieldMap replyAddressPatternFields = + new() + { + { s => s.StartsWith("x-"), (a, p, n) => a.AddExtension(p, LoadExtension(p, n)) }, + }; + + public static AsyncApiOperationReplyAddress LoadOperationReplyAddress(ParseNode node) + { + var mapNode = node.CheckMapNode("address"); + var pointer = mapNode.GetReferencePointer(); + if (pointer != null) + { + return new AsyncApiOperationReplyAddressReference(pointer); + } + + var reply = new AsyncApiOperationReplyAddress(); + + ParseMap(mapNode, reply, replyAddressFixedFields, replyAddressPatternFields); + + return reply; + } + } +} \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOperationReplyDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOperationReplyDeserializer.cs new file mode 100644 index 0000000..89b201b --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOperationReplyDeserializer.cs @@ -0,0 +1,38 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using ByteBard.AsyncAPI.Extensions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers.ParseNodes; + + internal static partial class AsyncApiV3Deserializer + { + private static FixedFieldMap replyFixedFields = new() + { + { "address", (a, n) => { a.Address = LoadOperationReplyAddress(n); } }, + { "channel", (a, n) => { a.Channel = LoadChannelReference(n); } }, + { "messages", (a, n) => { a.Messages = n.CreateList(LoadMessageReference); } }, + }; + + private static PatternFieldMap replyPatternFields = + new() + { + { s => s.StartsWith("x-"), (a, p, n) => a.AddExtension(p, LoadExtension(p, n)) }, + }; + + public static AsyncApiOperationReply LoadOperationReply(ParseNode node) + { + var mapNode = node.CheckMapNode("reply"); + var pointer = mapNode.GetReferencePointer(); + if (pointer != null) + { + return new AsyncApiOperationReplyReference(pointer); + } + + var reply = new AsyncApiOperationReply(); + + ParseMap(mapNode, reply, replyFixedFields, replyPatternFields); + + return reply; + } + } +} \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOperationTraitDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOperationTraitDeserializer.cs new file mode 100644 index 0000000..10d14ec --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiOperationTraitDeserializer.cs @@ -0,0 +1,43 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using ByteBard.AsyncAPI.Extensions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers.ParseNodes; + + internal static partial class AsyncApiV3Deserializer + { + private static FixedFieldMap operationTraitFixedFields = new() + { + { "title", (a, n) => { a.Title = n.GetScalarValue(); } }, + { "summary", (a, n) => { a.Summary = n.GetScalarValue(); } }, + { "description", (a, n) => { a.Description = n.GetScalarValue(); } }, + { "tags", (a, n) => { a.Tags = n.CreateList(LoadTag); } }, + { "security", (a, n) => { a.Security = n.CreateList(LoadSecurityScheme); } }, + { "externalDocs", (a, n) => { a.Tags = n.CreateList(LoadTag); } }, + { "bindings", (a, n) => { a.Bindings = LoadOperationBindings(n); } }, + }; + + private static PatternFieldMap operationTraitPatternFields = + new() + { + { s => s.StartsWith("x-"), (a, p, n) => a.AddExtension(p, LoadExtension(p, n)) }, + }; + + public static AsyncApiOperationTrait LoadOperationTrait(ParseNode node) + { + var mapNode = node.CheckMapNode("traits"); + + var pointer = mapNode.GetReferencePointer(); + if (pointer != null) + { + return new AsyncApiOperationTraitReference(pointer); + } + + var operationTrait = new AsyncApiOperationTrait(); + + ParseMap(mapNode, operationTrait, operationTraitFixedFields, operationTraitPatternFields); + + return operationTrait; + } + } +} \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiParameterDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiParameterDeserializer.cs new file mode 100644 index 0000000..721fd2c --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiParameterDeserializer.cs @@ -0,0 +1,61 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using ByteBard.AsyncAPI.Extensions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers.ParseNodes; + using System.Linq; + + internal static partial class AsyncApiV3Deserializer + { + private static FixedFieldMap parameterFixedFields = new() + { + { "enum", (a, n) => { a.Enum = n.CreateSimpleList(n2 => n2.GetScalarValue()); } }, + { "default", (a, n) => { a.Default = n.GetScalarValue(); } }, + { "description", (a, n) => { a.Description = n.GetScalarValue(); } }, + { "examples", (a, n) => { a.Examples = n.CreateSimpleList(n2 => n2.GetScalarValue()); } }, + { "location", (a, n) => { a.Location = n.GetScalarValue(); } }, + }; + + private static PatternFieldMap parameterPatternFields = + new() + { + { s => s.StartsWith("x-"), (a, p, n) => a.AddExtension(p, LoadExtension(p, n)) }, + }; + + private static void LoadParameterFromSchema(AsyncApiParameter instance, ParseNode node) + { + var schema = AsyncApiSchemaDeserializer.LoadSchema(node); + if (schema.Enum.Any()) + { + instance.Enum = schema.Enum.Select(e => e.GetValue()).ToList(); + } + + if (schema.Default != null) + { + instance.Default = schema.Default.GetValue(); + } + + if (schema.Examples.Any()) + { + instance.Examples = schema.Examples.Select(e => e.GetValue()).ToList(); + } + } + + public static AsyncApiParameter LoadParameter(ParseNode node) + { + var mapNode = node.CheckMapNode("parameter"); + + var pointer = mapNode.GetReferencePointer(); + if (pointer != null) + { + return new AsyncApiParameterReference(pointer); + } + + var parameter = new AsyncApiParameter(); + + ParseMap(mapNode, parameter, parameterFixedFields, parameterPatternFields); + + return parameter; + } + } +} \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiSecuritySchemeDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiSecuritySchemeDeserializer.cs new file mode 100644 index 0000000..3875fa6 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiSecuritySchemeDeserializer.cs @@ -0,0 +1,72 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using System; + using ByteBard.AsyncAPI.Extensions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers.ParseNodes; + + /// + /// Class containing logic to deserialize AsyncApi document into + /// runtime AsyncApi object model. + /// + internal static partial class AsyncApiV3Deserializer + { + private static readonly FixedFieldMap securitySchemeFixedFields = + new() + { + { + "type", (o, n) => { o.Type = n.GetScalarValue().GetEnumFromDisplayName(); } + }, + { + "description", (o, n) => { o.Description = n.GetScalarValue(); } + }, + { + "name", (o, n) => { o.Name = n.GetScalarValue(); } + }, + { + "in", (o, n) => { o.In = n.GetScalarValue().GetEnumFromDisplayName(); } + }, + { + "scheme", (o, n) => { o.Scheme = n.GetScalarValue(); } + }, + { + "bearerFormat", (o, n) => { o.BearerFormat = n.GetScalarValue(); } + }, + { + "flows", (o, n) => { o.Flows = LoadOAuthFlows(n); } + }, + { + "openIdConnectUrl", + (o, n) => { o.OpenIdConnectUrl = new Uri(n.GetScalarValue(), UriKind.RelativeOrAbsolute); } + }, + { + "scopes", + (o, n) => { o.Scopes = n.CreateSimpleSet(n2 => n2.GetScalarValue()); } + }, + }; + + private static readonly PatternFieldMap securitySchemePatternFields = + new() + { + { s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n)) }, + }; + + public static AsyncApiSecurityScheme LoadSecurityScheme(ParseNode node) + { + var mapNode = node.CheckMapNode("securityScheme"); + var pointer = mapNode.GetReferencePointer(); + if (pointer != null) + { + return new AsyncApiSecuritySchemeReference(pointer); + } + + var securityScheme = new AsyncApiSecurityScheme(); + foreach (var property in mapNode) + { + property.ParseField(securityScheme, securitySchemeFixedFields, securitySchemePatternFields); + } + + return securityScheme; + } + } +} diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiServerBindingDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiServerBindingDeserializer.cs new file mode 100644 index 0000000..e410d4d --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiServerBindingDeserializer.cs @@ -0,0 +1,65 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using ByteBard.AsyncAPI.Exceptions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Readers.ParseNodes; + using ByteBard.AsyncAPI.Extensions; + + internal static partial class AsyncApiV3Deserializer + { + internal static AsyncApiBindings LoadServerBindings(ParseNode node) + { + var mapNode = node.CheckMapNode("serverBindings"); + var pointer = mapNode.GetReferencePointer(); + if (pointer != null) + { + return new AsyncApiBindingsReference(pointer); + } + + var serverBindings = new AsyncApiBindings(); + foreach (var property in mapNode) + { + var serverBinding = LoadServerBinding(property); + + if (serverBinding != null) + { + serverBindings.Add(serverBinding); + } + else + { + mapNode.Context.Diagnostic.Errors.Add( + new AsyncApiError(node.Context.GetLocation(), $"ServerBinding {property.Name} is not found")); + } + } + + mapNode.ParseFields(serverBindings, serverBindingPatternFields); + return serverBindings; + } + + private static readonly PatternFieldMap> serverBindingPatternFields = + new() + { + { s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n)) }, + }; + + internal static IServerBinding LoadServerBinding(ParseNode node) + { + var property = node as PropertyNode; + try + { + if (node.Context.ServerBindingParsers.TryGetValue(property.Name, out var parser)) + { + return parser.LoadBinding(property); + } + } + catch (AsyncApiException ex) + { + ex.Pointer = node.Context.GetLocation(); + node.Context.Diagnostic.Errors.Add(new AsyncApiError(ex)); + } + + return null; + } + } +} diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiServerDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiServerDeserializer.cs new file mode 100644 index 0000000..df2bbd5 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiServerDeserializer.cs @@ -0,0 +1,82 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using ByteBard.AsyncAPI.Extensions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers.ParseNodes; + + /// + /// Class containing logic to deserialize AsyncApi document into + /// runtime AsyncApi object model. + /// + internal static partial class AsyncApiV3Deserializer + { + private static readonly FixedFieldMap serverFixedFields = new() + { + { + "host", (a, n) => { a.Host = n.GetScalarValue(); } + }, + { + "pathname", (a, n) => { a.PathName = n.GetScalarValue(); } + }, + { + "title", (a, n) => { a.Title = n.GetScalarValue(); } + }, + { + "summary", (a, n) => { a.Summary = n.GetScalarValue(); } + }, + { + "externalDocs", (a, n) => { a.ExternalDocs = LoadExternalDocs(n); } + }, + { + "description", (a, n) => { a.Description = n.GetScalarValue(); } + }, + { + "variables", (a, n) => { a.Variables = n.CreateMap(LoadServerVariable); } + }, + { + "security", (a, n) => { a.Security = n.CreateList(LoadSecurityScheme); } + }, + { + "tags", (a, n) => { a.Tags = n.CreateList(LoadTag); } + }, + { + "bindings", (o, n) => { o.Bindings = LoadServerBindings(n); } + }, + { + "protocolVersion", (a, n) => { a.ProtocolVersion = n.GetScalarValue(); } + }, + { + "protocol", (a, n) => { a.Protocol = n.GetScalarValue(); } + }, + }; + + private static readonly PatternFieldMap serverPatternFields = + new() + { + { s => s.StartsWith("x-"), (a, p, n) => a.AddExtension(p, LoadExtension(p, n)) }, + }; + + public static AsyncApiServerReference LoadServerReference(ParseNode node) + { + var mapNode = node.CheckMapNode("server"); + var pointer = mapNode.GetReferencePointer(); + return new AsyncApiServerReference(pointer); + } + + public static AsyncApiServer LoadServer(ParseNode node) + { + var mapNode = node.CheckMapNode("server"); + var pointer = mapNode.GetReferencePointer(); + if (pointer != null) + { + return new AsyncApiServerReference(pointer); + } + + var server = new AsyncApiServer(); + + ParseMap(mapNode, server, serverFixedFields, serverPatternFields); + + return server; + } + } +} diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiServerVariableDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiServerVariableDeserializer.cs new file mode 100644 index 0000000..d8110d1 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiServerVariableDeserializer.cs @@ -0,0 +1,52 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using ByteBard.AsyncAPI.Extensions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers.ParseNodes; + + /// + /// Class containing logic to deserialize AsyncApi document into + /// runtime AsyncApi object model. + /// + internal static partial class AsyncApiV3Deserializer + { + private static readonly FixedFieldMap serverVariableFixedFields = + new() + { + { + "enum", (a, n) => { a.Enum = n.CreateSimpleList(s => s.GetScalarValue()); } + }, + { + "default", (a, n) => { a.Default = n.GetScalarValue(); } + }, + { + "description", (a, n) => { a.Description = n.GetScalarValue(); } + }, + { + "examples", (a, n) => { a.Examples = n.CreateSimpleList(s => s.GetScalarValue()); } + }, + }; + + private static readonly PatternFieldMap serverVariablePatternFields = + new() + { + { s => s.StartsWith("x-"), (a, p, n) => a.AddExtension(p, LoadExtension(p, n)) }, + }; + + public static AsyncApiServerVariable LoadServerVariable(ParseNode node) + { + var mapNode = node.CheckMapNode("serverVariable"); + var pointer = mapNode.GetReferencePointer(); + if (pointer != null) + { + return new AsyncApiServerVariableReference(pointer); + } + + var serverVariable = new AsyncApiServerVariable(); + + ParseMap(mapNode, serverVariable, serverVariableFixedFields, serverVariablePatternFields); + + return serverVariable; + } + } +} diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiTagDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiTagDeserializer.cs new file mode 100644 index 0000000..73a85e1 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiTagDeserializer.cs @@ -0,0 +1,38 @@ +namespace ByteBard.AsyncAPI.Readers +{ + using ByteBard.AsyncAPI.Extensions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers.ParseNodes; + + internal static partial class AsyncApiV3Deserializer + { + private static FixedFieldMap tagsFixedFields = new() + { + { "name", (a, n) => { a.Name = n.GetScalarValue(); } }, + { "description", (a, n) => { a.Description = n.GetScalarValue(); } }, + { "externalDocs", (a, n) => { a.ExternalDocs = LoadExternalDocs(n); } }, + }; + + private static PatternFieldMap tagsPatternFields = + new() + { + { s => s.StartsWith("x-"), (a, p, n) => a.AddExtension(p, LoadExtension(p, n)) }, + }; + + public static AsyncApiTag LoadTag(ParseNode node) + { + var mapNode = node.CheckMapNode("tags"); + var pointer = mapNode.GetReferencePointer(); + if (pointer != null) + { + return new AsyncApiTagReference(pointer); + } + + var tag = new AsyncApiTag(); + + ParseMap(mapNode, tag, tagsFixedFields, tagsPatternFields); + + return tag; + } + } +} diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiV3VersionService.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiV3VersionService.cs new file mode 100644 index 0000000..d644807 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiV3VersionService.cs @@ -0,0 +1,89 @@ +namespace ByteBard.AsyncAPI.Readers.V3 +{ + using System; + using System.Collections.Generic; + using ByteBard.AsyncAPI.Exceptions; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Readers.Interface; + using ByteBard.AsyncAPI.Readers.ParseNodes; + + internal class AsyncApiV3VersionService : IAsyncApiVersionService + { + public AsyncApiDiagnostic Diagnostic { get; } + + /// + /// Create Parsing Context. + /// + /// Provide instance for diagnostic object for collecting and accessing information about the parsing. + public AsyncApiV3VersionService(AsyncApiDiagnostic diagnostic) + { + this.Diagnostic = diagnostic; + } + + private IDictionary> loaders = new Dictionary> + { + [typeof(AsyncApiAny)] = AsyncApiV3Deserializer.LoadAny, + [typeof(AsyncApiComponents)] = AsyncApiV3Deserializer.LoadComponents, + [typeof(AsyncApiExternalDocumentation)] = AsyncApiV3Deserializer.LoadExternalDocs, + [typeof(AsyncApiInfo)] = AsyncApiV3Deserializer.LoadInfo, + [typeof(AsyncApiLicense)] = AsyncApiV3Deserializer.LoadLicense, + [typeof(AsyncApiOAuthFlow)] = AsyncApiV3Deserializer.LoadOAuthFlow, + [typeof(AsyncApiOAuthFlows)] = AsyncApiV3Deserializer.LoadOAuthFlows, + [typeof(AsyncApiOperation)] = AsyncApiV3Deserializer.LoadOperation, + [typeof(AsyncApiOperationReply)] = AsyncApiV3Deserializer.LoadOperationReply, + [typeof(AsyncApiOperationReplyAddress)] = AsyncApiV3Deserializer.LoadOperationReplyAddress, + [typeof(AsyncApiParameter)] = AsyncApiV3Deserializer.LoadParameter, + [typeof(AsyncApiJsonSchema)] = AsyncApiSchemaDeserializer.LoadSchema, + [typeof(AsyncApiAvroSchema)] = AsyncApiAvroSchemaDeserializer.LoadSchema, + [typeof(AsyncApiSecurityScheme)] = AsyncApiV3Deserializer.LoadSecurityScheme, + [typeof(AsyncApiMultiFormatSchema)] = AsyncApiV3Deserializer.LoadMultiFormatSchema, + [typeof(AsyncApiServer)] = AsyncApiV3Deserializer.LoadServer, + [typeof(AsyncApiServerVariable)] = AsyncApiV3Deserializer.LoadServerVariable, + [typeof(AsyncApiTag)] = AsyncApiV3Deserializer.LoadTag, + [typeof(AsyncApiMessage)] = AsyncApiV3Deserializer.LoadMessage, + [typeof(AsyncApiMessageTrait)] = AsyncApiV3Deserializer.LoadMessageTrait, + [typeof(AsyncApiChannel)] = AsyncApiV3Deserializer.LoadChannel, + [typeof(AsyncApiBindings)] = AsyncApiV3Deserializer.LoadServerBindings, + [typeof(AsyncApiBindings)] = AsyncApiV3Deserializer.LoadChannelBindings, + [typeof(AsyncApiBindings)] = AsyncApiV3Deserializer.LoadMessageBindings, + [typeof(AsyncApiBindings)] = AsyncApiV3Deserializer.LoadOperationBindings, + }; + + /// + /// Parse the string to a object. + /// + /// The URL of the reference. + /// The type of object referenced based on the context of the reference. + public AsyncApiReference ConvertToAsyncApiReference( + string reference, + ReferenceType? type) + { + if (string.IsNullOrWhiteSpace(reference)) + { + throw new AsyncApiException($"The reference string '{reference}' has invalid format."); + } + + try + { + return new AsyncApiReference(reference, type); + } + catch (AsyncApiException ex) + { + this.Diagnostic.Errors.Add(new AsyncApiError(ex)); + return null; + } + } + + public AsyncApiDocument LoadDocument(RootNode rootNode) + { + return AsyncApiV3Deserializer.LoadAsyncApi(rootNode); + } + + public T LoadElement(ParseNode node) + where T : IAsyncApiElement + { + return (T)this.loaders[typeof(T)](node); + } + } +} \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/AsyncApiVersion.cs b/src/ByteBard.AsyncAPI/AsyncApiVersion.cs index 446be6e..ce5d275 100644 --- a/src/ByteBard.AsyncAPI/AsyncApiVersion.cs +++ b/src/ByteBard.AsyncAPI/AsyncApiVersion.cs @@ -6,5 +6,10 @@ public enum AsyncApiVersion /// Represents AsyncAPI V2 spec. /// AsyncApi2_0, + + /// + /// Represents AsyncAPI V3 spec. + /// + AsyncApi3_0, } } diff --git a/src/ByteBard.AsyncAPI/AsyncApiWorkspace.cs b/src/ByteBard.AsyncAPI/AsyncApiWorkspace.cs index 9438132..94f4c23 100644 --- a/src/ByteBard.AsyncAPI/AsyncApiWorkspace.cs +++ b/src/ByteBard.AsyncAPI/AsyncApiWorkspace.cs @@ -11,6 +11,8 @@ public class AsyncApiWorkspace private readonly Dictionary artifactsRegistry = new(); private readonly Dictionary resolvedReferenceRegistry = new(); + public AsyncApiDocument RootDocument { get; private set; } + public void RegisterComponents(AsyncApiDocument document) { if (document?.Components == null) @@ -18,114 +20,172 @@ public void RegisterComponents(AsyncApiDocument document) return; } - string baseUri = "#/components/"; + string componentsBaseUri = "#/components/"; string location; // Register Schema foreach (var item in document.Components.Schemas) { - location = baseUri + ReferenceType.Schema.GetDisplayName() + "/" + item.Key; + location = componentsBaseUri + ReferenceType.Schema.GetDisplayName() + "/" + item.Key; this.RegisterComponent(location, item.Value); } // Register Parameters foreach (var item in document.Components.Parameters) { - location = baseUri + ReferenceType.Parameter.GetDisplayName() + "/" + item.Key; + location = componentsBaseUri + ReferenceType.Parameter.GetDisplayName() + "/" + item.Key; this.RegisterComponent(location, item.Value); } // Register Channels foreach (var item in document.Components.Channels) { - location = baseUri + ReferenceType.Channel.GetDisplayName() + "/" + item.Key; + location = componentsBaseUri + ReferenceType.Channel.GetDisplayName() + "/" + item.Key; + this.RegisterComponent(location, item.Value); + } + + // Register Operations + foreach (var item in document.Components.Operations) + { + location = componentsBaseUri + ReferenceType.Operation.GetDisplayName() + "/" + item.Key; this.RegisterComponent(location, item.Value); } // Register Servers foreach (var item in document.Components.Servers) { - location = baseUri + ReferenceType.Server.GetDisplayName() + "/" + item.Key; + location = componentsBaseUri + ReferenceType.Server.GetDisplayName() + "/" + item.Key; this.RegisterComponent(location, item.Value); } // Register ServerVariables foreach (var item in document.Components.ServerVariables) { - location = baseUri + ReferenceType.ServerVariable.GetDisplayName() + "/" + item.Key; + location = componentsBaseUri + ReferenceType.ServerVariable.GetDisplayName() + "/" + item.Key; this.RegisterComponent(location, item.Value); } // Register Messages foreach (var item in document.Components.Messages) { - location = baseUri + ReferenceType.Message.GetDisplayName() + "/" + item.Key; + location = componentsBaseUri + ReferenceType.Message.GetDisplayName() + "/" + item.Key; this.RegisterComponent(location, item.Value); } // Register SecuritySchemes foreach (var item in document.Components.SecuritySchemes) { - location = baseUri + ReferenceType.SecurityScheme.GetDisplayName() + "/" + item.Key; + location = componentsBaseUri + ReferenceType.SecurityScheme.GetDisplayName() + "/" + item.Key; this.RegisterComponent(location, item.Value); this.RegisterComponent(item.Key, item.Value); } - // Register Parameters - foreach (var item in document.Components.Parameters) + // Register Server Variables + foreach (var item in document.Components.ServerVariables) { - location = baseUri + ReferenceType.Parameter.GetDisplayName() + "/" + item.Key; + location = componentsBaseUri + ReferenceType.ServerVariable.GetDisplayName() + "/" + item.Key; this.RegisterComponent(location, item.Value); } // Register CorrelationIds foreach (var item in document.Components.CorrelationIds) { - location = baseUri + ReferenceType.CorrelationId.GetDisplayName() + "/" + item.Key; + location = componentsBaseUri + ReferenceType.CorrelationId.GetDisplayName() + "/" + item.Key; + this.RegisterComponent(location, item.Value); + } + + // Register Replies + foreach (var item in document.Components.Replies) + { + location = componentsBaseUri + ReferenceType.OperationReply.GetDisplayName() + "/" + item.Key; + this.RegisterComponent(location, item.Value); + } + + // Register ReplyAddresses + foreach (var item in document.Components.ReplyAddresses) + { + location = componentsBaseUri + ReferenceType.OperationReplyAddress.GetDisplayName() + "/" + item.Key; + this.RegisterComponent(location, item.Value); + } + + // Register ExternalDocs + foreach (var item in document.Components.ExternalDocs) + { + location = componentsBaseUri + ReferenceType.ExternalDocs.GetDisplayName() + "/" + item.Key; + this.RegisterComponent(location, item.Value); + } + + // Register Tags + foreach (var item in document.Components.Tags) + { + location = componentsBaseUri + ReferenceType.Tag.GetDisplayName() + "/" + item.Key; this.RegisterComponent(location, item.Value); } // Register OperationTraits foreach (var item in document.Components.OperationTraits) { - location = baseUri + ReferenceType.OperationTrait.GetDisplayName() + "/" + item.Key; + location = componentsBaseUri + ReferenceType.OperationTrait.GetDisplayName() + "/" + item.Key; this.RegisterComponent(location, item.Value); } // Register MessageTraits foreach (var item in document.Components.MessageTraits) { - location = baseUri + ReferenceType.MessageTrait.GetDisplayName() + "/" + item.Key; + location = componentsBaseUri + ReferenceType.MessageTrait.GetDisplayName() + "/" + item.Key; this.RegisterComponent(location, item.Value); } // Register ServerBindings foreach (var item in document.Components.ServerBindings) { - location = baseUri + ReferenceType.ServerBindings.GetDisplayName() + "/" + item.Key; + location = componentsBaseUri + ReferenceType.ServerBindings.GetDisplayName() + "/" + item.Key; this.RegisterComponent(location, item.Value); } // Register ChannelBindings foreach (var item in document.Components.ChannelBindings) { - location = baseUri + ReferenceType.ChannelBindings.GetDisplayName() + "/" + item.Key; + location = componentsBaseUri + ReferenceType.ChannelBindings.GetDisplayName() + "/" + item.Key; this.RegisterComponent(location, item.Value); } // Register OperationBindings foreach (var item in document.Components.OperationBindings) { - location = baseUri + ReferenceType.OperationBindings.GetDisplayName() + "/" + item.Key; + location = componentsBaseUri + ReferenceType.OperationBindings.GetDisplayName() + "/" + item.Key; this.RegisterComponent(location, item.Value); } // Register MessageBindings foreach (var item in document.Components.MessageBindings) { - location = baseUri + ReferenceType.MessageBindings.GetDisplayName() + "/" + item.Key; + location = componentsBaseUri + ReferenceType.MessageBindings.GetDisplayName() + "/" + item.Key; this.RegisterComponent(location, item.Value); } + + string channelBaseUri = "#/channels/"; + foreach (var channel in document.Channels) + { + var registerableChannelValue = channel.Value; + if (channel.Value is IAsyncApiReferenceable reference) + { + if (reference.Reference.IsExternal) + { + continue; + } + + registerableChannelValue = this.ResolveReference(reference.Reference); + } + + location = channelBaseUri + channel.Key; + this.RegisterComponent(location, registerableChannelValue); + + foreach (var message in registerableChannelValue.Messages) + { + this.RegisterComponent(location + "/messages/" + message.Key, message.Value); + } + } } public bool RegisterComponent(string location, T component) @@ -146,6 +206,7 @@ public bool RegisterComponent(string location, T component) { this.artifactsRegistry[uri] = stream; } + return true; } @@ -186,5 +247,10 @@ private Uri ToLocationUrl(string location) { return new(location, UriKind.RelativeOrAbsolute); } + + public void SetRootDocument(AsyncApiDocument doc) + { + this.RootDocument = doc; + } } } diff --git a/src/ByteBard.AsyncAPI/ByteBard.AsyncAPI.csproj b/src/ByteBard.AsyncAPI/ByteBard.AsyncAPI.csproj index 32579af..1cb4f01 100644 --- a/src/ByteBard.AsyncAPI/ByteBard.AsyncAPI.csproj +++ b/src/ByteBard.AsyncAPI/ByteBard.AsyncAPI.csproj @@ -5,7 +5,6 @@ ByteBard.AsyncAPI.NET ByteBard.AsyncAPI ByteBard.AsyncAPI - netstandard2.0;net8 @@ -13,7 +12,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + <_Parameter1>$(MSBuildProjectName).Tests diff --git a/src/ByteBard.AsyncAPI/Extensions/AsyncApiElementExtensions.cs b/src/ByteBard.AsyncAPI/Extensions/AsyncApiElementExtensions.cs index f6b1a14..1990d9e 100644 --- a/src/ByteBard.AsyncAPI/Extensions/AsyncApiElementExtensions.cs +++ b/src/ByteBard.AsyncAPI/Extensions/AsyncApiElementExtensions.cs @@ -20,7 +20,13 @@ public static class AsyncApiElementExtensions /// An IEnumerable of errors. This function will never return null. public static IEnumerable Validate(this IAsyncApiElement element, ValidationRuleSet ruleSet) { - var validator = new AsyncApiValidator(ruleSet); + AsyncApiDocument rootDocument = null; + if (element is AsyncApiDocument document) + { + rootDocument = document; + } + + var validator = new AsyncApiValidator(ruleSet, rootDocument); var walker = new AsyncApiWalker(validator); walker.Walk(element); return validator.Errors.Cast().Union(validator.Warnings); diff --git a/src/ByteBard.AsyncAPI/Extensions/AsyncApiExtensions.cs b/src/ByteBard.AsyncAPI/Extensions/AsyncApiExtensions.cs new file mode 100644 index 0000000..622a7b7 --- /dev/null +++ b/src/ByteBard.AsyncAPI/Extensions/AsyncApiExtensions.cs @@ -0,0 +1,14 @@ +namespace ByteBard.AsyncAPI.Extensions +{ + using System.Text.RegularExpressions; + + public static class AsyncApiExtensions + { + private static Regex channelAddressExpressionRegex = new Regex("{[a-zA-Z1-9_-]*}"); + + public static bool IsChannelAddressExpression(this string address) + { + return address != null && channelAddressExpressionRegex.IsMatch(address); + } + } +} diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiAction.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiAction.cs new file mode 100644 index 0000000..68eaa2b --- /dev/null +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiAction.cs @@ -0,0 +1,13 @@ +namespace ByteBard.AsyncAPI.Models +{ + using ByteBard.AsyncAPI.Attributes; + + public enum AsyncApiAction + { + [Display("send")] + Send, + + [Display("receive")] + Receive, + } +} \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiBinding.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiBinding.cs index ef304b2..22730d7 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiBinding.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiBinding.cs @@ -14,6 +14,16 @@ public abstract class AsyncApiBinding : IBinding public string BindingVersion { get; set; } public void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + public void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + private void SerializeCore(IAsyncApiWriter writer) { if (writer is null) { diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiBindings{TBinding}.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiBindings{TBinding}.cs index 66105e6..2bebd67 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiBindings{TBinding}.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiBindings{TBinding}.cs @@ -6,12 +6,22 @@ using ByteBard.AsyncAPI.Models.Interfaces; using ByteBard.AsyncAPI.Writers; - public class AsyncApiBindings : IDictionary, IAsyncApiSerializable + public class AsyncApiBindings : IDictionary, IAsyncApiSerializable, IAsyncApiExtensible where TBinding : IBinding { private Dictionary inner = new Dictionary(); public virtual void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + public virtual void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + private void SerializeCore(IAsyncApiWriter writer) { if (writer is null) { @@ -30,9 +40,12 @@ public virtual void SerializeV2(IAsyncApiWriter writer) bindingValue.SerializeV2(writer); } + writer.WriteExtensions(this.Extensions); writer.WriteEndObject(); } + public virtual IDictionary Extensions { get; set; } = new Dictionary(); + public virtual void Add(TBinding binding) { this[binding.BindingKey] = binding; @@ -40,71 +53,71 @@ public virtual void Add(TBinding binding) public virtual TBinding this[string key] { - get => inner[key]; - set => inner[key] = value; + get => this.inner[key]; + set => this.inner[key] = value; } - public virtual ICollection Keys => inner.Keys; + public virtual ICollection Keys => this.inner.Keys; - public virtual ICollection Values => inner.Values; + public virtual ICollection Values => this.inner.Values; - public virtual int Count => inner.Count; + public virtual int Count => this.inner.Count; - public virtual bool IsReadOnly => ((IDictionary)inner).IsReadOnly; + public virtual bool IsReadOnly => ((IDictionary)this.inner).IsReadOnly; public virtual void Add(string key, TBinding value) { - inner.Add(key, value); + this.inner.Add(key, value); } public virtual bool ContainsKey(string key) { - return inner.ContainsKey(key); + return this.inner.ContainsKey(key); } public virtual bool Remove(string key) { - return inner.Remove(key); + return this.inner.Remove(key); } public virtual bool TryGetValue(string key, out TBinding value) { - return inner.TryGetValue(key, out value); + return this.inner.TryGetValue(key, out value); } public virtual void Add(KeyValuePair item) { - ((IDictionary)inner).Add(item); + ((IDictionary)this.inner).Add(item); } public virtual void Clear() { - inner.Clear(); + this.inner.Clear(); } public virtual bool Contains(KeyValuePair item) { - return ((IDictionary)inner).Contains(item); + return ((IDictionary)this.inner).Contains(item); } public virtual void CopyTo(KeyValuePair[] array, int arrayIndex) { - ((IDictionary)inner).CopyTo(array, arrayIndex); + ((IDictionary)this.inner).CopyTo(array, arrayIndex); } public virtual bool Remove(KeyValuePair item) { - return ((IDictionary)inner).Remove(item); + return ((IDictionary)this.inner).Remove(item); } public virtual IEnumerator> GetEnumerator() { - return inner.GetEnumerator(); + return this.inner.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { - return inner.GetEnumerator(); + return this.inner.GetEnumerator(); } } } diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiChannel.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiChannel.cs index 97b31bd..945be14 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiChannel.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiChannel.cs @@ -2,6 +2,8 @@ { using System; using System.Collections.Generic; + using System.Linq; + using ByteBard.AsyncAPI.Extensions; using ByteBard.AsyncAPI.Models.Interfaces; using ByteBard.AsyncAPI.Writers; @@ -10,6 +12,26 @@ /// public class AsyncApiChannel : IAsyncApiSerializable, IAsyncApiExtensible { + /// + /// An optional string representation of this channel's address. The address is typically the "topic name", "routing key", "event type", or "path". When null or absent, it MUST be interpreted as unknown. This is useful when the address is generated dynamically at runtime or can't be known upfront. It MAY contain Channel Address Expressions. Query parameters and fragments SHALL NOT be used, instead use bindings to define them. + /// + public virtual string? Address { get; set; } + + /// + /// A map of the messages that will be sent to this channel by any application at any time. Every message sent to this channel MUST be valid against one, and only one, of the message objects defined in this map. + /// + public virtual IDictionary Messages { get; set; } = new Dictionary(); + + /// + /// A human-friendly title for the channel. + /// + public virtual string Title { get; set; } + + /// + /// A short summary of the channel. + /// + public virtual string Summary { get; set; } + /// /// an optional description of this channel item. CommonMark syntax can be used for rich text representation. /// @@ -21,22 +43,22 @@ public class AsyncApiChannel : IAsyncApiSerializable, IAsyncApiExtensible /// /// If servers is absent or empty then this channel must be available on all servers defined in the Servers Object. /// - public virtual IList Servers { get; set; } = new List(); + public virtual IList Servers { get; set; } = new List(); /// - /// a definition of the SUBSCRIBE operation, which defines the messages produced by the application and sent to the channel. + /// A map of the parameters included in the channel address. It MUST be present only when the address contains Channel Address Expressions. /// - public virtual AsyncApiOperation Subscribe { get; set; } + public virtual IDictionary Parameters { get; set; } = new Dictionary(); /// - /// a definition of the PUBLISH operation, which defines the messages consumed by the application from the channel. + /// A list of tags for logical grouping of channels. /// - public virtual AsyncApiOperation Publish { get; set; } + public virtual IList Tags { get; set; } = new List(); /// - /// a map of the parameters included in the channel name. It SHOULD be present only when using channels with expressions (as defined by RFC 6570 section 2.2). + /// Additional external documentation for this channel. /// - public virtual IDictionary Parameters { get; set; } = new Dictionary(); + public virtual AsyncApiExternalDocumentation ExternalDocs { get; set; } /// /// a map where the keys describe the name of the protocol and the values describe protocol-specific definitions for the channel. @@ -59,13 +81,15 @@ public virtual void SerializeV2(IAsyncApiWriter writer) writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description); // servers - writer.WriteOptionalCollection(AsyncApiConstants.Servers, this.Servers, (w, s) => w.WriteValue(s)); + writer.WriteOptionalCollection(AsyncApiConstants.Servers, this.Servers.Select(s => s.Reference.FragmentId).ToList(), (w, s) => w.WriteValue(s)); + + var operations = writer.Workspace.RootDocument?.Operations.Values.Where(operation => CheckOperationChannel(operation, writer)).ToList(); - // subscribe - writer.WriteOptionalObject(AsyncApiConstants.Subscribe, this.Subscribe, (w, s) => s.SerializeV2(w)); + // subscribe (Now Send) + writer.WriteOptionalObject(AsyncApiConstants.Subscribe, operations?.FirstOrDefault(o => o.Action == AsyncApiAction.Send), (w, s) => s?.SerializeV2(w)); - // publish - writer.WriteOptionalObject(AsyncApiConstants.Publish, this.Publish, (w, s) => s.SerializeV2(w)); + // publish (Now Receive) + writer.WriteOptionalObject(AsyncApiConstants.Publish, operations?.FirstOrDefault(o => o.Action == AsyncApiAction.Receive), (w, s) => s?.SerializeV2(w)); // parameters writer.WriteOptionalMap(AsyncApiConstants.Parameters, this.Parameters, (writer, key, component) => @@ -87,5 +111,44 @@ public virtual void SerializeV2(IAsyncApiWriter writer) writer.WriteEndObject(); } + + public virtual void SerializeV3(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + + writer.WriteOptionalProperty(AsyncApiConstants.Address, this.Address); + writer.WriteRequiredMap(AsyncApiConstants.Messages, this.Messages, (w, k, m) => m.SerializeV3(w)); + writer.WriteOptionalProperty(AsyncApiConstants.Title, this.Title); + writer.WriteOptionalProperty(AsyncApiConstants.Summary, this.Summary); + writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description); + writer.WriteOptionalCollection(AsyncApiConstants.Servers, this.Servers, (w, s) => s.Reference.SerializeV3(w)); + if (this.Address.IsChannelAddressExpression()) + { + writer.WriteOptionalMap(AsyncApiConstants.Parameters, this.Parameters, (w, key, p) => p.SerializeV3(w)); + } + + writer.WriteOptionalCollection(AsyncApiConstants.Tags, this.Tags, (w, t) => t.SerializeV3(w)); + writer.WriteOptionalObject(AsyncApiConstants.ExternalDocs, this.ExternalDocs, (w, s) => s.SerializeV2(w)); + writer.WriteOptionalObject(AsyncApiConstants.Bindings, this.Bindings, (w, t) => t.SerializeV2(w)); + writer.WriteExtensions(this.Extensions); + + writer.WriteEndObject(); + } + + private bool CheckOperationChannel(AsyncApiOperation operation, IAsyncApiWriter writer) + { + if (operation is AsyncApiOperationReference reference) + { + reference.Reference.Workspace = writer.Workspace; + } + + operation.Channel.Reference.Workspace = writer.Workspace; + return operation.Channel.Equals(this); + } } } \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiComponents.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiComponents.cs index e990f3b..042db5d 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiComponents.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiComponents.cs @@ -15,9 +15,12 @@ namespace ByteBard.AsyncAPI.Models public class AsyncApiComponents : IAsyncApiExtensible, IAsyncApiSerializable { /// - /// An object to hold reusable Schema Objects. + /// An object to hold reusable Schema Object. /// - public IDictionary Schemas { get; set; } = new Dictionary(); + /// + /// If this is a Schema Object, then the schemaFormat will be assumed to be "application/vnd.aai.asyncapi+json;version=asyncapi" where the version is equal to the AsyncAPI Version String. + /// + public IDictionary Schemas { get; set; } = new Dictionary(); /// /// An object to hold reusable Server Objects. @@ -25,14 +28,14 @@ public class AsyncApiComponents : IAsyncApiExtensible, IAsyncApiSerializable public IDictionary Servers { get; set; } = new Dictionary(); /// - /// An object to hold reusable Server Variable Objects. + /// An object to hold reusable Channel Item Objects. /// - public IDictionary ServerVariables { get; set; } = new Dictionary(); + public IDictionary Channels { get; set; } = new Dictionary(); /// - /// An object to hold reusable Channel Item Objects. + /// An object to hold reusable Operation Objects. /// - public IDictionary Channels { get; set; } = new Dictionary(); + public IDictionary Operations { get; set; } = new Dictionary(); /// /// An object to hold reusable Message Objects. @@ -44,6 +47,11 @@ public class AsyncApiComponents : IAsyncApiExtensible, IAsyncApiSerializable /// public IDictionary SecuritySchemes { get; set; } = new Dictionary(); + /// + /// An object to hold reusable Server Variable Objects. + /// + public IDictionary ServerVariables { get; set; } = new Dictionary(); + /// /// An object to hold reusable Parameter Objects. /// @@ -54,6 +62,26 @@ public class AsyncApiComponents : IAsyncApiExtensible, IAsyncApiSerializable /// public IDictionary CorrelationIds { get; set; } = new Dictionary(); + /// + /// An object to hold reusable Operation Reply Objects. + /// + public IDictionary Replies { get; set; } = new Dictionary(); + + /// + /// An object to hold reusable Operation Reply Address Objects. + /// + public IDictionary ReplyAddresses { get; set; } = new Dictionary(); + + /// + /// An object to hold reusable External Documentation Objects. + /// + public IDictionary ExternalDocs { get; set; } = new Dictionary(); + + /// + /// An object to hold reusable Tag Objects. + /// + public IDictionary Tags { get; set; } = new Dictionary(); + /// /// An object to hold reusable Operation Trait Objects. /// @@ -128,13 +156,13 @@ public void SerializeV2(IAsyncApiWriter writer) this.Schemas, (w, key, component) => { - if (component is AsyncApiJsonSchemaReference reference) + if (component is AsyncApiMultiFormatSchemaReference reference) { - reference.SerializeV2(w); + reference.Schema.SerializeV2(w); } else { - component.SerializeV2(w); + component.Schema.SerializeV2(w); } }); @@ -351,5 +379,367 @@ public void SerializeV2(IAsyncApiWriter writer) writer.WriteEndObject(); } + + public void SerializeV3(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + // If references have been inlined we don't need the to render the components section + // however if they have cycles, then we will need a component rendered + if (writer.GetSettings().InlineLocalReferences) + { + var loops = writer.GetSettings().LoopDetector.Loops; + writer.WriteStartObject(); + if (loops.TryGetValue(typeof(AsyncApiMultiFormatSchemaReference), out List schemas)) + { + var asyncApiSchemas = schemas.Cast().Distinct().ToList() + .ToDictionary(k => k.Reference.FragmentId); + + writer.WriteOptionalMap( + AsyncApiConstants.Schemas, + this.Schemas, + (w, key, component) => + { + component.SerializeV3(w); + }); + } + + writer.WriteEndObject(); + return; + } + + writer.WriteStartObject(); + + // Serialize each referenceable object as full object without reference if the reference in the object points to itself. + // If the reference exists but points to other objects, the object is serialized to just that reference. + + // schemas + writer.WriteOptionalMap( + AsyncApiConstants.Schemas, + this.Schemas, + (w, key, component) => + { + if (component is AsyncApiMultiFormatSchemaReference reference) + { + reference.SerializeV3(w); + } + else + { + component.SerializeV3(w); + } + }); + + // servers + writer.WriteOptionalMap( + AsyncApiConstants.Servers, + this.Servers, + (w, key, component) => + { + if (component is AsyncApiServerReference reference) + { + reference.SerializeV3(w); + } + else + { + component.SerializeV3(w); + } + }); + + // servers + writer.WriteOptionalMap( + AsyncApiConstants.ServerVariables, + this.ServerVariables, + (w, key, component) => + { + if (component is AsyncApiServerVariableReference reference) + { + reference.SerializeV3(w); + } + else + { + component.SerializeV3(w); + } + }); + + // channels + writer.WriteOptionalMap( + AsyncApiConstants.Channels, + this.Channels, + (w, key, component) => + { + if (component is AsyncApiChannelReference reference) + { + reference.SerializeV3(w); + } + else + { + component.SerializeV3(w); + } + }); + + // operations + writer.WriteOptionalMap( + AsyncApiConstants.Operations, + this.Operations, + (w, key, component) => + { + if (component is AsyncApiOperationReference reference) + { + reference.SerializeV3(w); + } + else + { + component.SerializeV3(w); + } + }); + + // messages + writer.WriteOptionalMap( + AsyncApiConstants.Messages, + this.Messages, + (w, key, component) => + { + if (component is AsyncApiMessageReference reference) + { + reference.SerializeV3(w); + } + else + { + component.SerializeV3(w); + } + }); + + // securitySchemes + writer.WriteOptionalMap( + AsyncApiConstants.SecuritySchemes, + this.SecuritySchemes, + (w, key, component) => + { + if (component is AsyncApiSecuritySchemeReference reference) + { + reference.SerializeV3(w); + } + else + { + component.SerializeV3(w); + } + }); + + // server variables + writer.WriteOptionalMap( + AsyncApiConstants.ServerVariables, + this.ServerVariables, + (w, key, component) => + { + if (component is AsyncApiServerVariableReference reference) + { + reference.SerializeV3(w); + } + else + { + component.SerializeV3(w); + } + }); + + // parameters + writer.WriteOptionalMap( + AsyncApiConstants.Parameters, + this.Parameters, + (w, key, component) => + { + if (component is AsyncApiParameterReference reference) + { + reference.SerializeV3(w); + } + else + { + component.SerializeV3(w); + } + }); + + // correlationIds + writer.WriteOptionalMap( + AsyncApiConstants.CorrelationIds, + this.CorrelationIds, + (w, key, component) => + { + if (component is AsyncApiCorrelationIdReference reference) + { + reference.SerializeV3(w); + } + else + { + component.SerializeV3(w); + } + }); + + // replies + writer.WriteOptionalMap( + AsyncApiConstants.Reply, + this.Replies, + (w, key, component) => + { + if (component is AsyncApiOperationReplyReference reference) + { + reference.SerializeV3(w); + } + else + { + component.SerializeV3(w); + } + }); + + // replies + writer.WriteOptionalMap( + AsyncApiConstants.Address, + this.ReplyAddresses, + (w, key, component) => + { + if (component is AsyncApiOperationReplyAddressReference reference) + { + reference.SerializeV3(w); + } + else + { + component.SerializeV3(w); + } + }); + + // external docs + writer.WriteOptionalMap( + AsyncApiConstants.ExternalDocs, + this.ExternalDocs, + (w, key, component) => + { + if (component is AsyncApiExternalDocumentationReference reference) + { + reference.SerializeV3(w); + } + else + { + component.SerializeV3(w); + } + }); + + // tags + writer.WriteOptionalMap( + AsyncApiConstants.Tags, + this.Tags, + (w, key, component) => + { + if (component is AsyncApiTagReference reference) + { + reference.SerializeV3(w); + } + else + { + component.SerializeV3(w); + } + }); + + // operationTraits + writer.WriteOptionalMap( + AsyncApiConstants.OperationTraits, + this.OperationTraits, + (w, key, component) => + { + if (component is AsyncApiOperationTraitReference reference) + { + reference.SerializeV3(w); + } + else + { + component.SerializeV3(w); + } + }); + + // messageTraits + writer.WriteOptionalMap( + AsyncApiConstants.MessageTraits, + this.MessageTraits, + (w, key, component) => + { + if (component is AsyncApiMessageTraitReference reference) + { + reference.SerializeV3(w); + } + else + { + component.SerializeV3(w); + } + }); + + //// serverBindings + writer.WriteOptionalMap( + AsyncApiConstants.ServerBindings, + this.ServerBindings, + (w, key, component) => + { + if (component is AsyncApiBindingsReference reference) + { + reference.SerializeV3(w); + } + else + { + component.SerializeV3(w); + } + }); + + //// channelBindings + writer.WriteOptionalMap( + AsyncApiConstants.ChannelBindings, + this.ChannelBindings, + (w, key, component) => + { + if (component is AsyncApiBindingsReference reference) + { + reference.SerializeV3(w); + } + else + { + component.SerializeV3(w); + } + }); + + //// operationBindings + writer.WriteOptionalMap( + AsyncApiConstants.OperationBindings, + this.OperationBindings, + (w, key, component) => + { + if (component is AsyncApiBindingsReference reference) + { + reference.SerializeV3(w); + } + else + { + component.SerializeV3(w); + } + }); + + //// messageBindings + writer.WriteOptionalMap( + AsyncApiConstants.MessageBindings, + this.MessageBindings, + (w, key, component) => + { + if (component is AsyncApiBindingsReference reference) + { + reference.SerializeV3(w); + } + else + { + component.SerializeV3(w); + } + }); + + // extensions + writer.WriteExtensions(this.Extensions); + + writer.WriteEndObject(); + } } } \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiConstants.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiConstants.cs index 60b2408..d50f18d 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiConstants.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiConstants.cs @@ -8,9 +8,13 @@ public static class AsyncApiConstants public const string Info = "info"; public const string Servers = "servers"; public const string Channels = "channels"; + public const string Operations = "operations"; public const string Components = "components"; public const string Tags = "tags"; public const string ExternalDocs = "externalDocs"; + public const string Action = "action"; + public const string Channel = "channel"; + public const string Address = "address"; public const string Title = "title"; public const string Description = "description"; public const string TermsOfService = "termsOfService"; @@ -21,6 +25,8 @@ public static class AsyncApiConstants public const string Url = "url"; public const string Email = "email"; public const string Protocol = "protocol"; + public const string Host = "host"; + public const string PathName = "pathname"; public const string ProtocolVersion = "protocolVersion"; public const string Variables = "variables"; public const string Security = "security"; @@ -40,6 +46,7 @@ public static class AsyncApiConstants public const string AuthorizationUrl = "authorizationUrl"; public const string TokenUrl = "tokenUrl"; public const string Scopes = "scopes"; + public const string AvailableScopes = "availableScopes"; public const string Application = "application"; public const string AccessCode = "accessCode"; public const string Enum = "enum"; @@ -91,6 +98,7 @@ public static class AsyncApiConstants public const string Parameters = "parameters"; public const string Schemas = "schemas"; public const string Messages = "messages"; + public const string Reply = "reply"; public const string SecuritySchemes = "securitySchemes"; public const string CorrelationIds = "correlationIds"; public const string OperationTraits = "operationTraits"; diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiContact.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiContact.cs index 0a13e2e..ca56cd4 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiContact.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiContact.cs @@ -29,6 +29,16 @@ public class AsyncApiContact : IAsyncApiSerializable, IAsyncApiExtensible public IDictionary Extensions { get; set; } = new Dictionary(); public void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + public void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + private void SerializeCore(IAsyncApiWriter writer) { if (writer is null) { diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiCorrelationId.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiCorrelationId.cs index e11e2fd..c0482a4 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiCorrelationId.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiCorrelationId.cs @@ -24,6 +24,16 @@ public class AsyncApiCorrelationId : IAsyncApiExtensible, IAsyncApiSerializable public virtual IDictionary Extensions { get; set; } = new Dictionary(); public virtual void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + public virtual void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + private void SerializeCore(IAsyncApiWriter writer) { if (writer is null) { diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiDocument.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiDocument.cs index 079325e..6af7345 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiDocument.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiDocument.cs @@ -40,24 +40,19 @@ public class AsyncApiDocument : IAsyncApiExtensible, IAsyncApiSerializable public string DefaultContentType { get; set; } /// - /// REQUIRED. The available channels and messages for the API. + /// The channels used by this application. /// public IDictionary Channels { get; set; } = new Dictionary(); /// - /// an element to hold various schemas for the specification. - /// - public AsyncApiComponents Components { get; set; } - - /// - /// a list of tags used by the specification with additional metadata. Each tag name in the list MUST be unique. - /// - public IList Tags { get; set; } = new List(); + /// The operations this application MUST implement. + /// + public IDictionary Operations { get; set; } = new Dictionary(); /// - /// additional external documentation. + /// an element to hold various schemas for the specification. /// - public AsyncApiExternalDocumentation ExternalDocs { get; set; } + public AsyncApiComponents Components { get; set; } = new AsyncApiComponents(); /// public IDictionary Extensions { get; set; } = new Dictionary(); @@ -69,6 +64,7 @@ public void SerializeV2(IAsyncApiWriter writer) throw new ArgumentNullException(nameof(writer)); } + writer.Workspace.SetRootDocument(this); writer.Workspace.RegisterComponents(this); writer.WriteStartObject(); @@ -83,22 +79,96 @@ public void SerializeV2(IAsyncApiWriter writer) writer.WriteOptionalProperty(AsyncApiConstants.Id, this.Id); // servers - writer.WriteOptionalMap(AsyncApiConstants.Servers, this.Servers, (writer, key, component) => component.SerializeV2(writer)); + writer.WriteOptionalMap(AsyncApiConstants.Servers, this.Servers, (writer, component) => component.SerializeV2(writer)); // content type writer.WriteOptionalProperty(AsyncApiConstants.DefaultContentType, this.DefaultContentType); // channels - writer.WriteRequiredMap(AsyncApiConstants.Channels, this.Channels, (writer, key, component) => component.SerializeV2(writer)); + writer.WriteRequiredMap(AsyncApiConstants.Channels, this.Channels, (channel) => GetChannelAddress(channel, writer.Workspace), (writer, key, component) => component.SerializeV2(writer)); // components - writer.WriteOptionalObject(AsyncApiConstants.Components, this.Components, (w, c) => c.SerializeV2(w)); + if (this.Components.Schemas.Count > 0 || + this.Components.Servers.Count > 0 || + this.Components.Channels.Count > 0 || + this.Components.Operations.Count > 0 || + this.Components.Messages.Count > 0 || + this.Components.SecuritySchemes.Count > 0 || + this.Components.ServerVariables.Count > 0 || + this.Components.Parameters.Count > 0 || + this.Components.CorrelationIds.Count > 0 || + this.Components.Replies.Count > 0 || + this.Components.ReplyAddresses.Count > 0 || + this.Components.ExternalDocs.Count > 0 || + this.Components.Tags.Count > 0 || + this.Components.OperationTraits.Count > 0 || + this.Components.MessageTraits.Count > 0 || + this.Components.ServerBindings.Count > 0 || + this.Components.ChannelBindings.Count > 0 || + this.Components.OperationBindings.Count > 0 || + this.Components.MessageBindings.Count > 0 || + this.Components.Extensions.Count > 0) + { + writer.WriteOptionalObject(AsyncApiConstants.Components, this.Components, (w, c) => c.SerializeV2(w)); + } // tags - writer.WriteOptionalCollection(AsyncApiConstants.Tags, this.Tags, (w, t) => t.SerializeV2(w)); + writer.WriteOptionalCollection(AsyncApiConstants.Tags, this.Info.Tags, (w, t) => t.SerializeV2(w)); // external docs - writer.WriteOptionalObject(AsyncApiConstants.ExternalDocs, this.ExternalDocs, (w, e) => e.SerializeV2(w)); + writer.WriteOptionalObject(AsyncApiConstants.ExternalDocs, this.Info.ExternalDocs, (w, e) => e.SerializeV2(w)); + + // extensions + writer.WriteExtensions(this.Extensions); + + writer.WriteEndObject(); + } + + private string GetChannelAddress(AsyncApiChannel channel, AsyncApiWorkspace workspace) + { + if (channel is AsyncApiChannelReference reference) + { + reference.Reference.Workspace = workspace; + } + + return channel.Address; + } + + public void SerializeV3(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.Workspace.SetRootDocument(this); + writer.Workspace.RegisterComponents(this); + + writer.WriteStartObject(); + + // asyncApi + writer.WriteRequiredProperty(AsyncApiConstants.AsyncApi, "3.0.0"); + + // info + writer.WriteRequiredObject(AsyncApiConstants.Info, this.Info, (w, i) => i.SerializeV3(w)); + + // id + writer.WriteOptionalProperty(AsyncApiConstants.Id, this.Id); + + // content type + writer.WriteOptionalProperty(AsyncApiConstants.DefaultContentType, this.DefaultContentType); + + // servers + writer.WriteOptionalMap(AsyncApiConstants.Servers, this.Servers, (writer, key, server) => server.SerializeV3(writer)); + + // channels + writer.WriteOptionalMap(AsyncApiConstants.Channels, this.Channels, (writer, key, channel) => channel.SerializeV3(writer)); + + // operations + writer.WriteOptionalMap(AsyncApiConstants.Operations, this.Operations, (writer, key, operation) => operation.SerializeV3(writer)); + + // components + writer.WriteOptionalObject(AsyncApiConstants.Components, this.Components, (w, component) => component.SerializeV3(w)); // extensions writer.WriteExtensions(this.Extensions); diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiExternalDocumentation.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiExternalDocumentation.cs index 7f9b973..fb81424 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiExternalDocumentation.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiExternalDocumentation.cs @@ -13,17 +13,27 @@ public class AsyncApiExternalDocumentation : IAsyncApiExtensible, IAsyncApiSeria /// /// a short description of the target documentation. CommonMark syntax can be used for rich text representation. /// - public string Description { get; set; } + public virtual string Description { get; set; } /// /// REQUIRED. The URL for the target documentation. Value MUST be in the format of a URL. /// - public Uri Url { get; set; } + public virtual Uri Url { get; set; } /// - public IDictionary Extensions { get; set; } = new Dictionary(); + public virtual IDictionary Extensions { get; set; } = new Dictionary(); - public void SerializeV2(IAsyncApiWriter writer) + public virtual void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + public virtual void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + private void SerializeCore(IAsyncApiWriter writer) { if (writer is null) { diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiInfo.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiInfo.cs index 0b40a86..7f8d77a 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiInfo.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiInfo.cs @@ -11,35 +11,45 @@ public class AsyncApiInfo : IAsyncApiSerializable, IAsyncApiExtensible { /// - /// Gets or sets REQUIRED. The title of the application. + /// REQUIRED. The title of the application. /// public string Title { get; set; } /// - /// Gets or sets REQUIRED. Provides the version of the application API (not to be confused with the specification version). + /// REQUIRED Provides the version of the application API (not to be confused with the specification version). /// public string Version { get; set; } /// - /// Gets or sets a short description of the application. CommonMark syntax can be used for rich text representation. + /// A short description of the application. CommonMark syntax can be used for rich text representation. /// public string Description { get; set; } /// - /// Gets or sets a URL to the Terms of Service for the API. MUST be in the format of a URL. + /// A URL to the Terms of Service for the API. This MUST be in the form of an absolute URL. /// public Uri TermsOfService { get; set; } /// - /// Gets or sets the contact information for the exposed API. + /// The contact information for the exposed API. /// public AsyncApiContact Contact { get; set; } /// - /// Gets or sets the license information for the exposed API. + /// The license information for the exposed API. /// public AsyncApiLicense License { get; set; } + /// + /// a list of tags used by the specification with additional metadata. Each tag name in the list MUST be unique. + /// + public IList Tags { get; set; } = new List(); + + /// + /// additional external documentation. + /// + public AsyncApiExternalDocumentation ExternalDocs { get; set; } + /// public IDictionary Extensions { get; set; } = new Dictionary(); @@ -53,7 +63,7 @@ public void SerializeV2(IAsyncApiWriter writer) writer.WriteStartObject(); // title - writer.WriteOptionalProperty(AsyncApiConstants.Title, this.Title); + writer.WriteRequiredProperty(AsyncApiConstants.Title, this.Title); // version writer.WriteOptionalProperty(AsyncApiConstants.Version, this.Version); @@ -75,5 +85,44 @@ public void SerializeV2(IAsyncApiWriter writer) writer.WriteEndObject(); } + + public void SerializeV3(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + + // title + writer.WriteRequiredProperty(AsyncApiConstants.Title, this.Title); + + // version + writer.WriteOptionalProperty(AsyncApiConstants.Version, this.Version); + + // description + writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description); + + // termsOfService + writer.WriteOptionalProperty(AsyncApiConstants.TermsOfService, this.TermsOfService?.OriginalString); + + // contact object + writer.WriteOptionalObject(AsyncApiConstants.Contact, this.Contact, (w, c) => c.SerializeV3(w)); + + // license object + writer.WriteOptionalObject(AsyncApiConstants.License, this.License, (w, l) => l.SerializeV3(w)); + + // tags + writer.WriteOptionalCollection(AsyncApiConstants.Tags, this.Tags, (w, t) => t.SerializeV3(w)); + + // external docs + writer.WriteOptionalObject(AsyncApiConstants.ExternalDocs, this.ExternalDocs, (w, e) => e.SerializeV3(w)); + + // specification extensions + writer.WriteExtensions(this.Extensions); + + writer.WriteEndObject(); + } } } \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiLicense.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiLicense.cs index be62ab4..e16b2d4 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiLicense.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiLicense.cs @@ -24,6 +24,16 @@ public class AsyncApiLicense : IAsyncApiSerializable, IAsyncApiExtensible public IDictionary Extensions { get; set; } = new Dictionary(); public void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + public void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + private void SerializeCore(IAsyncApiWriter writer) { if (writer is null) { diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiMessage.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiMessage.cs index a778f4e..3f5adf7 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiMessage.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiMessage.cs @@ -1,4 +1,4 @@ -namespace ByteBard.AsyncAPI.Models +namespace ByteBard.AsyncAPI.Models { using System; using System.Collections.Generic; @@ -10,34 +10,24 @@ namespace ByteBard.AsyncAPI.Models /// public class AsyncApiMessage : IAsyncApiExtensible, IAsyncApiSerializable { - /// - /// Unique string used to identify the message. The id MUST be unique among all messages described in the API. - /// - public virtual string MessageId { get; set; } - /// /// schema definition of the application headers. Schema MUST be of type "object". /// - public virtual AsyncApiJsonSchema Headers { get; set; } + public virtual AsyncApiMultiFormatSchema Headers { get; set; } /// - /// definition of the message payload. It can be of any type but defaults to Schema object. It must match the schema format, including encoding type - e.g Avro should be inlined as either a YAML or JSON object NOT a string to be parsed as YAML or JSON. + /// definition of the message payload. /// - public virtual IAsyncApiMessagePayload Payload { get; set; } + /// + /// If this is a Schema Object, then the schemaFormat will be assumed to be "application/vnd.aai.asyncapi+json;version=asyncapi" where the version is equal to the AsyncAPI Version String. + /// + public virtual AsyncApiMultiFormatSchema Payload { get; set; } /// /// definition of the correlation ID used for message tracing or matching. /// public virtual AsyncApiCorrelationId CorrelationId { get; set; } - /// - /// a string containing the name of the schema format used to define the message payload. - /// - /// - /// If omitted, implementations should parse the payload as a Schema object. - /// - public virtual string SchemaFormat { get; set; } - /// /// the content type to use when encoding/decoding a message's payload. /// @@ -99,10 +89,10 @@ public virtual void SerializeV2(IAsyncApiWriter writer) } writer.WriteStartObject(); - writer.WriteOptionalObject(AsyncApiConstants.Headers, this.Headers, (w, h) => h.SerializeV2(w)); + writer.WriteOptionalObject(AsyncApiConstants.Headers, this.Headers, (w, h) => h.Schema.SerializeV2(w)); writer.WriteOptionalObject(AsyncApiConstants.Payload, this.Payload, (w, p) => p.SerializeV2(w)); writer.WriteOptionalObject(AsyncApiConstants.CorrelationId, this.CorrelationId, (w, c) => c.SerializeV2(w)); - writer.WriteOptionalProperty(AsyncApiConstants.SchemaFormat, this.SchemaFormat); + writer.WriteOptionalProperty(AsyncApiConstants.SchemaFormat, this.Payload?.SchemaFormat); writer.WriteOptionalProperty(AsyncApiConstants.ContentType, this.ContentType); writer.WriteOptionalProperty(AsyncApiConstants.Name, this.Name); writer.WriteOptionalProperty(AsyncApiConstants.Title, this.Title); @@ -118,5 +108,32 @@ public virtual void SerializeV2(IAsyncApiWriter writer) writer.WriteExtensions(this.Extensions); writer.WriteEndObject(); } + + public virtual void SerializeV3(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteOptionalObject(AsyncApiConstants.Headers, this.Headers, (w, h) => h.SerializeV3(w)); + writer.WriteOptionalObject(AsyncApiConstants.Payload, this.Payload, (w, p) => p.SerializeV3(w)); + writer.WriteOptionalObject(AsyncApiConstants.CorrelationId, this.CorrelationId, (w, c) => c.SerializeV3(w)); + writer.WriteOptionalProperty(AsyncApiConstants.ContentType, this.ContentType); + writer.WriteOptionalProperty(AsyncApiConstants.Name, this.Name); + writer.WriteOptionalProperty(AsyncApiConstants.Title, this.Title); + writer.WriteOptionalProperty(AsyncApiConstants.Summary, this.Summary); + writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description); + writer.WriteOptionalCollection(AsyncApiConstants.Tags, this.Tags, (w, t) => t.SerializeV3(w)); + writer.WriteOptionalObject(AsyncApiConstants.ExternalDocs, this.ExternalDocs, (w, e) => e.SerializeV3(w)); + + writer.WriteOptionalObject(AsyncApiConstants.Bindings, this.Bindings, (w, t) => t.SerializeV3(w)); + writer.WriteOptionalCollection(AsyncApiConstants.Examples, this.Examples, (w, e) => e.SerializeV3(w)); + + writer.WriteOptionalCollection(AsyncApiConstants.Traits, this.Traits, (w, t) => t.SerializeV3(w)); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } } } \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiMessageExample.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiMessageExample.cs index 8010027..557c6f0 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiMessageExample.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiMessageExample.cs @@ -34,6 +34,16 @@ public class AsyncApiMessageExample : IAsyncApiExtensible, IAsyncApiSerializable public IDictionary Extensions { get; set; } = new Dictionary(); public void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + public void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + private void SerializeCore(IAsyncApiWriter writer) { if (writer is null) { diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiMessageTrait.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiMessageTrait.cs index 06e93ae..82d3867 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiMessageTrait.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiMessageTrait.cs @@ -10,29 +10,16 @@ /// public class AsyncApiMessageTrait : IAsyncApiExtensible, IAsyncApiSerializable { - /// - /// Unique string used to identify the message. The id MUST be unique among all messages described in the API. - /// - public virtual string MessageId { get; set; } - /// /// schema definition of the application headers. Schema MUST be of type "object". /// - public virtual AsyncApiJsonSchema Headers { get; set; } + public virtual AsyncApiMultiFormatSchema Headers { get; set; } /// /// definition of the correlation ID used for message tracing or matching. /// public virtual AsyncApiCorrelationId CorrelationId { get; set; } - /// - /// a string containing the name of the schema format used to define the message payload. - /// - /// - /// If omitted, implementations should parse the payload as a Schema object. - /// - public virtual string SchemaFormat { get; set; } - /// /// the content type to use when encoding/decoding a message's payload. /// @@ -88,10 +75,10 @@ public virtual void SerializeV2(IAsyncApiWriter writer) } writer.WriteStartObject(); - writer.WriteOptionalProperty(AsyncApiConstants.MessageId, this.MessageId); - writer.WriteOptionalObject(AsyncApiConstants.Headers, this.Headers, (w, h) => h.SerializeV2(w)); + //writer.WriteOptionalProperty(AsyncApiConstants.MessageId, this.MessageId); + writer.WriteOptionalObject(AsyncApiConstants.Headers, this.Headers, (w, h) => h.Schema.SerializeV2(w)); writer.WriteOptionalObject(AsyncApiConstants.CorrelationId, this.CorrelationId, (w, c) => c.SerializeV2(w)); - writer.WriteOptionalProperty(AsyncApiConstants.SchemaFormat, this.SchemaFormat); + writer.WriteOptionalProperty(AsyncApiConstants.SchemaFormat, this.Headers.SchemaFormat); writer.WriteOptionalProperty(AsyncApiConstants.ContentType, this.ContentType); writer.WriteOptionalProperty(AsyncApiConstants.Name, this.Name); writer.WriteOptionalProperty(AsyncApiConstants.Title, this.Title); @@ -104,5 +91,28 @@ public virtual void SerializeV2(IAsyncApiWriter writer) writer.WriteExtensions(this.Extensions); writer.WriteEndObject(); } + + public virtual void SerializeV3(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteOptionalObject(AsyncApiConstants.Headers, this.Headers, (w, h) => h.SerializeV3(w)); + writer.WriteOptionalObject(AsyncApiConstants.CorrelationId, this.CorrelationId, (w, c) => c.SerializeV3(w)); + writer.WriteOptionalProperty(AsyncApiConstants.ContentType, this.ContentType); + writer.WriteOptionalProperty(AsyncApiConstants.Name, this.Name); + writer.WriteOptionalProperty(AsyncApiConstants.Title, this.Title); + writer.WriteOptionalProperty(AsyncApiConstants.Summary, this.Summary); + writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description); + writer.WriteOptionalCollection(AsyncApiConstants.Tags, this.Tags, (w, t) => t.SerializeV3(w)); + writer.WriteOptionalObject(AsyncApiConstants.ExternalDocs, this.ExternalDocs, (w, e) => e.SerializeV3(w)); + writer.WriteOptionalObject(AsyncApiConstants.Bindings, this.Bindings, (w, t) => t.SerializeV3(w)); + writer.WriteOptionalCollection(AsyncApiConstants.Examples, this.Examples, (w, e) => e.SerializeV3(w)); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } } } \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiMultiFormatSchema.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiMultiFormatSchema.cs new file mode 100644 index 0000000..14556d7 --- /dev/null +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiMultiFormatSchema.cs @@ -0,0 +1,52 @@ +namespace ByteBard.AsyncAPI.Models +{ + using System; + using System.Collections.Generic; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Writers; + + public class AsyncApiMultiFormatSchema : IAsyncApiSerializable, IAsyncApiExtensible + { + /// + /// Required. A string containing the name of the schema format that is used to define the information. If schemaFormat is missing, it MUST default to application/vnd.aai.asyncapi+json;version={{asyncapi}} where {{asyncapi}} matches the AsyncAPI Version String. In such a case, this would make the Multi Format Schema Object equivalent to the Schema Object. When using Reference Object within the schema, the schemaFormat of the resource being referenced MUST match the schemaFormat of the schema that contains the initial reference. For example, if you reference Avro schema, then schemaFormat of referencing resource and the resource being reference MUST match. + /// + public virtual string SchemaFormat { get; set; } + + /// + /// Required. Definition of the message payload. It can be of any type but defaults to Schema Object. It MUST match the schema format defined in schemaFormat, including the encoding type. E.g., Avro should be inlined as either a YAML or JSON object instead of as a string to be parsed as YAML or JSON. Non-JSON-based schemas (e.g., Protobuf or XSD) MUST be inlined as a string. + /// + public virtual IAsyncApiSchema Schema { get; set; } + + public IDictionary Extensions { get; set; } = new Dictionary(); + + public virtual void SerializeV2(IAsyncApiWriter writer) + { + this.Schema.SerializeV2(writer); + } + + public virtual void SerializeV3(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + // Serialize without including the schema. + if (string.IsNullOrEmpty(this.SchemaFormat) && this.Schema is AsyncApiJsonSchema) + { + this.Schema.SerializeV3(writer); + return; + } + + writer.WriteStartObject(); + + writer.WriteRequiredProperty(AsyncApiConstants.SchemaFormat, this.SchemaFormat); + + writer.WriteRequiredObject(AsyncApiConstants.Schema, this.Schema, (w, s) => s.SerializeV3(w)); + + writer.WriteExtensions(this.Extensions); + + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiOAuthFlow.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiOAuthFlow.cs index 0575044..d0b515f 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiOAuthFlow.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiOAuthFlow.cs @@ -27,7 +27,7 @@ public class AsyncApiOAuthFlow : IAsyncApiSerializable, IAsyncApiExtensible /// /// REQUIRED. A map between the scope name and a short description for it. /// - public IDictionary Scopes { get; set; } = new Dictionary(); + public IDictionary AvailableScopes { get; set; } = new Dictionary(); /// /// Specification Extensions. @@ -56,7 +56,34 @@ public void SerializeV2(IAsyncApiWriter writer) writer.WriteOptionalProperty(AsyncApiConstants.RefreshUrl, this.RefreshUrl?.ToString()); // scopes - writer.WriteRequiredMap(AsyncApiConstants.Scopes, this.Scopes, (w, s) => w.WriteValue(s)); + writer.WriteRequiredMap(AsyncApiConstants.Scopes, this.AvailableScopes, (w, s) => w.WriteValue(s)); + + // extensions + writer.WriteExtensions(this.Extensions); + + writer.WriteEndObject(); + } + + public void SerializeV3(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + + // authorizationUrl + writer.WriteOptionalProperty(AsyncApiConstants.AuthorizationUrl, this.AuthorizationUrl?.ToString()); + + // tokenUrl + writer.WriteOptionalProperty(AsyncApiConstants.TokenUrl, this.TokenUrl?.ToString()); + + // refreshUrl + writer.WriteOptionalProperty(AsyncApiConstants.RefreshUrl, this.RefreshUrl?.ToString()); + + // scopes + writer.WriteRequiredMap(AsyncApiConstants.AvailableScopes, this.AvailableScopes, (w, s) => w.WriteValue(s)); // extensions writer.WriteExtensions(this.Extensions); diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiOAuthFlows.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiOAuthFlows.cs index a80b4f5..aa01b07 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiOAuthFlows.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiOAuthFlows.cs @@ -70,5 +70,38 @@ public void SerializeV2(IAsyncApiWriter writer) writer.WriteEndObject(); } + + public void SerializeV3(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + + // implicit + writer.WriteOptionalObject(AsyncApiConstants.Implicit, this.Implicit, (w, o) => o.SerializeV3(w)); + + // password + writer.WriteOptionalObject(AsyncApiConstants.Password, this.Password, (w, o) => o.SerializeV3(w)); + + // clientCredentials + writer.WriteOptionalObject( + AsyncApiConstants.ClientCredentials, + this.ClientCredentials, + (w, o) => o.SerializeV3(w)); + + // authorizationCode + writer.WriteOptionalObject( + AsyncApiConstants.AuthorizationCode, + this.AuthorizationCode, + (w, o) => o.SerializeV3(w)); + + // extensions + writer.WriteExtensions(this.Extensions); + + writer.WriteEndObject(); + } } } \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiOperation.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiOperation.cs index 3110067..eef4788 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiOperation.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiOperation.cs @@ -5,64 +5,75 @@ using System.Linq; using ByteBard.AsyncAPI.Models.Interfaces; using ByteBard.AsyncAPI.Writers; - /// /// Describes a publish or a subscribe operation. This provides a place to document how and why messages are sent and received. /// public class AsyncApiOperation : IAsyncApiSerializable, IAsyncApiExtensible { + /// + /// Required. Use send when it's expected that the application will send a message to the given channel, and receive when the application should expect receiving messages from the given channel. + /// + public virtual AsyncApiAction Action { get; set; } + + /// + /// Required. A $ref pointer to the definition of the channel in which this operation is performed. If the operation is located in the root Operations Object, it MUST point to a channel definition located in the root Channels Object, and MUST NOT point to a channel definition located in the Components Object or anywhere else. If the operation is located in the Components Object, it MAY point to a Channel Object in any location. Please note the channel property value MUST be a Reference Object and, therefore, MUST NOT contain a Channel Object. However, it is RECOMMENDED that parsers (or other software) dereference this property for a better development experience. + /// + public virtual AsyncApiChannelReference Channel { get; set; } + /// /// unique string used to identify the operation. /// - public string OperationId { get; set; } + public virtual string Title { get; set; } /// /// a short summary of what the operation is about. /// - public string Summary { get; set; } + public virtual string Summary { get; set; } /// /// a verbose explanation of the operation. CommonMark syntax can be used for rich text representation. /// - public string Description { get; set; } + public virtual string Description { get; set; } /// /// A declaration of which security mechanisms can be used with this server. The list of values includes alternative security requirement objects that can be used. Only one of the security requirement objects need to be satisfied to authorize a connection or operation. /// - public IList Security { get; set; } = new List(); + public virtual IList Security { get; set; } = new List(); /// /// a list of tags for API documentation control. Tags can be used for logical grouping of operations. /// - public IList Tags { get; set; } = new List(); + public virtual IList Tags { get; set; } = new List(); /// /// additional external documentation for this operation. /// - public AsyncApiExternalDocumentation ExternalDocs { get; set; } + public virtual AsyncApiExternalDocumentation ExternalDocs { get; set; } /// /// a map where the keys describe the name of the protocol and the values describe protocol-specific definitions for the operation. /// - public AsyncApiBindings Bindings { get; set; } = new AsyncApiBindings(); + public virtual AsyncApiBindings Bindings { get; set; } = new AsyncApiBindings(); /// /// a list of traits to apply to the operation object. /// - public IList Traits { get; set; } = new List(); + public virtual IList Traits { get; set; } = new List(); /// - /// a definition of the message that will be published or received on this channel. + /// A list of $ref pointers pointing to the supported Message Objects that can be processed by this operation. It MUST contain a subset of the messages defined in the channel referenced in this operation, and MUST NOT point to a subset of message definitions located in the Messages Object in the Components Object or anywhere else. Every message processed by this operation MUST be valid against one, and only one, of the message objects referenced in this list. Please note the messages property value MUST be a list of Reference Objects and, therefore, MUST NOT contain Message Objects. However, it is RECOMMENDED that parsers (or other software) dereference this property for a better development experience. /// - /// - /// `oneOf` is allowed here to specify multiple messages, however, a message MUST be valid only against one of the referenced message objects. - /// - public IList Message { get; set; } = new List(); + public virtual IList Messages { get; set; } = new List(); + + /// + /// The definition of the reply in a request-reply operation. + /// + public virtual AsyncApiOperationReply Reply { get; set; } /// - public IDictionary Extensions { get; set; } = new Dictionary(); + public virtual IDictionary Extensions { get; set; } = new Dictionary(); - public void SerializeV2(IAsyncApiWriter writer) + public virtual void SerializeV2(IAsyncApiWriter writer) { if (writer is null) { @@ -70,29 +81,64 @@ public void SerializeV2(IAsyncApiWriter writer) } writer.WriteStartObject(); - writer.WriteOptionalProperty(AsyncApiConstants.OperationId, this.OperationId); + + // writer.WriteOptionalProperty(AsyncApiConstants.OperationId, this.OperationId); writer.WriteOptionalProperty(AsyncApiConstants.Summary, this.Summary); writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description); - writer.WriteOptionalCollection(AsyncApiConstants.Security, this.Security, (w, t) => t.SerializeV2(w)); + writer.WriteOptionalCollection(AsyncApiConstants.Security, this.Security, (w, t) => this.SerializeAsSecurityRequirement(t, w)); writer.WriteOptionalCollection(AsyncApiConstants.Tags, this.Tags, (w, t) => t.SerializeV2(w)); writer.WriteOptionalObject(AsyncApiConstants.ExternalDocs, this.ExternalDocs, (w, e) => e.SerializeV2(w)); writer.WriteOptionalObject(AsyncApiConstants.Bindings, this.Bindings, (w, t) => t.SerializeV2(w)); writer.WriteOptionalCollection(AsyncApiConstants.Traits, this.Traits, (w, t) => t.SerializeV2(w)); - if (this.Message.Count > 1) + if (this.Messages.Count > 1) { writer.WritePropertyName(AsyncApiConstants.Message); writer.WriteStartObject(); - writer.WriteOptionalCollection(AsyncApiConstants.OneOf, this.Message, (w, t) => t.SerializeV2(w)); + writer.WriteOptionalCollection(AsyncApiConstants.OneOf, this.Messages, (w, t) => t.SerializeV2(w)); writer.WriteEndObject(); } else { - writer.WriteOptionalObject(AsyncApiConstants.Message, this.Message.FirstOrDefault(), (w, m) => m.SerializeV2(w)); + writer.WriteOptionalObject(AsyncApiConstants.Message, this.Messages.FirstOrDefault(), (w, m) => m.SerializeV2(w)); + } + + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + + public virtual void SerializeV3(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); } + writer.WriteStartObject(); + writer.WriteRequiredProperty(AsyncApiConstants.Action, this.Action.GetDisplayName()); + writer.WriteRequiredObject(AsyncApiConstants.Channel, this.Channel, (w, c) => c.Reference.SerializeV3(w)); + writer.WriteOptionalProperty(AsyncApiConstants.Title, this.Title); + writer.WriteOptionalProperty(AsyncApiConstants.Summary, this.Summary); + writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description); + writer.WriteOptionalCollection(AsyncApiConstants.Security, this.Security, (w, t) => t.SerializeV3(w)); + writer.WriteOptionalCollection(AsyncApiConstants.Tags, this.Tags, (w, t) => t.SerializeV3(w)); + writer.WriteOptionalObject(AsyncApiConstants.ExternalDocs, this.ExternalDocs, (w, e) => e.SerializeV3(w)); + writer.WriteOptionalObject(AsyncApiConstants.Bindings, this.Bindings, (w, t) => t.SerializeV3(w)); + writer.WriteOptionalCollection(AsyncApiConstants.Traits, this.Traits, (w, t) => t.SerializeV3(w)); + writer.WriteOptionalCollection(AsyncApiConstants.Messages, this.Messages, (w, m) => m.Reference.SerializeV3(w)); + writer.WriteOptionalObject(AsyncApiConstants.Reply, this.Reply, (w, t) => t.SerializeV3(w)); writer.WriteExtensions(this.Extensions); writer.WriteEndObject(); } + + private void SerializeAsSecurityRequirement(AsyncApiSecurityScheme scheme, IAsyncApiWriter w) + { + if (scheme is not AsyncApiSecuritySchemeReference schemeReference) + { + throw new AsyncApiWriterException("Cannot serialize securityScheme as V2 as it is not a Reference."); + } + + schemeReference.SerializeAsSecurityRequirement(w); + } } } \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiOperationReply.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiOperationReply.cs new file mode 100644 index 0000000..98b10a7 --- /dev/null +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiOperationReply.cs @@ -0,0 +1,38 @@ +namespace ByteBard.AsyncAPI.Models +{ + using System; + using System.Collections.Generic; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Writers; + + public class AsyncApiOperationReply : IAsyncApiSerializable, IAsyncApiExtensible + { + public virtual AsyncApiOperationReplyAddress Address { get; set; } + + public virtual AsyncApiChannelReference Channel { get; set; } + + public virtual IList Messages { get; set; } = new List(); + + public virtual IDictionary Extensions { get; set; } = new Dictionary(); + + public void SerializeV2(IAsyncApiWriter writer) + { + throw new NotImplementedException(); + } + + public virtual void SerializeV3(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteOptionalObject(AsyncApiConstants.Address, this.Address, (w, a) => a.SerializeV3(w)); + writer.WriteOptionalObject(AsyncApiConstants.Channel, this.Channel, (w, c) => c.Reference.SerializeV3(w)); + writer.WriteOptionalCollection(AsyncApiConstants.Messages, this.Messages, (w, m) => m.Reference.SerializeV3(w)); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiOperationReplyAddress.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiOperationReplyAddress.cs new file mode 100644 index 0000000..303ee7b --- /dev/null +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiOperationReplyAddress.cs @@ -0,0 +1,41 @@ +namespace ByteBard.AsyncAPI.Models +{ + using System; + using System.Collections.Generic; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Writers; + + public class AsyncApiOperationReplyAddress : IAsyncApiSerializable, IAsyncApiExtensible + { + /// + /// An optional description of the address. CommonMark syntax can be used for rich text representation. + /// + public virtual string Description { get; set; } + + /// + /// REQUIRED. A runtime expression that specifies the location of the reply address. + /// + public virtual string Location { get; set; } + + public virtual IDictionary Extensions { get; set; } = new Dictionary(); + + public void SerializeV2(IAsyncApiWriter writer) + { + throw new NotImplementedException(); + } + + public virtual void SerializeV3(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description); + writer.WriteRequiredProperty(AsyncApiConstants.Location, this.Location); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiOperationTrait.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiOperationTrait.cs index 76766a1..7f34c07 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiOperationTrait.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiOperationTrait.cs @@ -11,12 +11,9 @@ public class AsyncApiOperationTrait : IAsyncApiExtensible, IAsyncApiSerializable { /// - /// unique string used to identify the operation. + /// A human-friendly title for the operation. /// - /// - /// The id MUST be unique among all operations described in the API. - /// - public virtual string OperationId { get; set; } + public virtual string Title { get; set; } /// /// a short summary of what the operation is about. @@ -28,6 +25,11 @@ public class AsyncApiOperationTrait : IAsyncApiExtensible, IAsyncApiSerializable /// public virtual string Description { get; set; } + /// + /// A declaration of which security schemes are associated with this operation. Only one of the security scheme objects MUST be satisfied to authorize an operation. In cases where Server Security also applies, it MUST also be satisfied. + /// + public virtual IList Security { get; set; } = new List(); + /// /// a list of tags for API documentation control. Tags can be used for logical grouping of operations. /// @@ -55,9 +57,10 @@ public virtual void SerializeV2(IAsyncApiWriter writer) writer.WriteStartObject(); - writer.WriteOptionalProperty(AsyncApiConstants.OperationId, this.OperationId); + // writer.WriteOptionalProperty(AsyncApiConstants.OperationId, this.OperationId); writer.WriteOptionalProperty(AsyncApiConstants.Summary, this.Summary); writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description); + writer.WriteOptionalCollection(AsyncApiConstants.Security, this.Security, (w, t) => t.SerializeV2(w)); writer.WriteOptionalCollection(AsyncApiConstants.Tags, this.Tags, (w, t) => t.SerializeV2(w)); writer.WriteOptionalObject(AsyncApiConstants.ExternalDocs, this.ExternalDocs, (w, e) => e.SerializeV2(w)); writer.WriteOptionalObject(AsyncApiConstants.Bindings, this.Bindings, (w, t) => t.SerializeV2(w)); @@ -65,5 +68,25 @@ public virtual void SerializeV2(IAsyncApiWriter writer) writer.WriteEndObject(); } + + public virtual void SerializeV3(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + + writer.WriteOptionalProperty(AsyncApiConstants.Title, this.Title); + writer.WriteOptionalProperty(AsyncApiConstants.Summary, this.Summary); + writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description); + writer.WriteOptionalCollection(AsyncApiConstants.Tags, this.Tags, (w, t) => t.SerializeV3(w)); + writer.WriteOptionalObject(AsyncApiConstants.ExternalDocs, this.ExternalDocs, (w, e) => e.SerializeV3(w)); + writer.WriteOptionalObject(AsyncApiConstants.Bindings, this.Bindings, (w, t) => t.SerializeV3(w)); + writer.WriteExtensions(this.Extensions); + + writer.WriteEndObject(); + } } } \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiParameter.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiParameter.cs index 9a9e1b4..2083158 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiParameter.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiParameter.cs @@ -2,6 +2,7 @@ namespace ByteBard.AsyncAPI.Models { using System; using System.Collections.Generic; + using System.Linq; using ByteBard.AsyncAPI.Models.Interfaces; using ByteBard.AsyncAPI.Writers; @@ -11,17 +12,26 @@ namespace ByteBard.AsyncAPI.Models public class AsyncApiParameter : IAsyncApiExtensible, IAsyncApiSerializable { /// - /// Gets or sets a verbose explanation of the parameter. CommonMark syntax can be used for rich text representation. + /// An enumeration of string values to be used if the substitution options are from a limited set. + /// + public virtual IList Enum { get; set; } = new List(); + + /// + /// The default value to use for substitution, and to send, if an alternate value is not supplied. + /// + public virtual string Default { get; set; } + /// + /// An optional description for the parameter. CommonMark syntax MAY be used for rich text representation. /// public virtual string Description { get; set; } /// - /// Gets or sets definition of the parameter. + /// An array of examples of the parameter value. /// - public virtual AsyncApiJsonSchema Schema { get; set; } + public virtual IList Examples { get; set; } = new List(); /// - /// Gets or sets a runtime expression that specifies the location of the parameter value. + /// A runtime expression that specifies the location of the parameter value. /// public virtual string Location { get; set; } @@ -37,7 +47,45 @@ public virtual void SerializeV2(IAsyncApiWriter writer) writer.WriteStartObject(); writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description); - writer.WriteOptionalObject(AsyncApiConstants.Schema, this.Schema, (w, s) => s.SerializeV2(w)); + + if (this.Enum.Any() || this.Default != null || this.Examples.Any()) + { + var schema = new AsyncApiJsonSchema(); + if (this.Enum.Any()) + { + schema.Enum = this.Enum.Select(e => new AsyncApiAny(e)).ToList(); + } + + if (this.Default != null) + { + schema.Default = new AsyncApiAny(this.Default); + } + + if (this.Examples.Any()) + { + schema.Examples = this.Examples.Select(e => new AsyncApiAny(e)).ToList(); + } + + writer.WriteOptionalObject(AsyncApiConstants.Schema, schema, (w, s) => s.SerializeV2(w)); + } + + writer.WriteOptionalProperty(AsyncApiConstants.Location, this.Location); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + + public virtual void SerializeV3(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteOptionalCollection(AsyncApiConstants.Enum, this.Enum, (w, e) => w.WriteValue(e)); + writer.WriteOptionalProperty(AsyncApiConstants.Default, this.Default); + writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description); + writer.WriteOptionalCollection(AsyncApiConstants.Examples, this.Examples, (w, e) => w.WriteValue(e)); writer.WriteOptionalProperty(AsyncApiConstants.Location, this.Location); writer.WriteExtensions(this.Extensions); writer.WriteEndObject(); diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiReference.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiReference.cs index f611416..8bb7916 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiReference.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiReference.cs @@ -1,6 +1,7 @@ namespace ByteBard.AsyncAPI.Models { using System; + using System.Diagnostics; using ByteBard.AsyncAPI.Exceptions; using ByteBard.AsyncAPI.Models.Interfaces; using ByteBard.AsyncAPI.Writers; @@ -8,7 +9,8 @@ /// /// A simple object to allow referencing other components in the specification, internally and externally. /// - public class AsyncApiReference : IAsyncApiSerializable + [DebuggerDisplay("{Reference}")] + public class AsyncApiReference : IAsyncApiSerializable, IEquatable { private string originalString; @@ -112,7 +114,7 @@ public string ExternalResource public bool IsExternal => this.ExternalResource != null; /// - /// Gets the full reference string for v2. + /// Gets the full reference string; /// public string Reference { @@ -122,6 +124,33 @@ public string Reference } } + public static bool operator !=(AsyncApiReference left, AsyncApiReference right) => !(left == right); + + public static bool operator ==(AsyncApiReference left, AsyncApiReference right) + { + return Equals(left, null) ? Equals(right, null) : left.Equals(right); + } + + public bool Equals(AsyncApiReference reference) + { + if (reference == null) + { + return false; + } + + return this.Reference == reference.Reference; + } + + public override bool Equals(object obj) + { + if (obj is not AsyncApiReference reference) + { + return false; + } + + return this.Equals(reference); + } + /// /// Serialize to Async Api. /// @@ -147,14 +176,19 @@ public void SerializeV2(IAsyncApiWriter writer) writer.WriteEndObject(); } - private string GetExternalReferenceV2() + public void SerializeV3(IAsyncApiWriter writer) { - return this.ExternalResource + (this.FragmentId != null ? "#" + this.FragmentId : string.Empty); - } + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } - public void Write(IAsyncApiWriter writer) - { - this.SerializeV2(writer); + writer.WriteStartObject(); + + // $ref + writer.WriteOptionalProperty(AsyncApiConstants.DollarRef, this.Reference); + + writer.WriteEndObject(); } } } \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiSecurityRequirement.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiSecurityRequirement.cs deleted file mode 100644 index d81ffe3..0000000 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiSecurityRequirement.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace ByteBard.AsyncAPI.Models -{ - using System; - using System.Collections.Generic; - using ByteBard.AsyncAPI.Models.Interfaces; - using ByteBard.AsyncAPI.Writers; - - public class AsyncApiSecurityRequirement : Dictionary>, IAsyncApiSerializable - { - /// - /// Initializes a new instance of the class. - /// This constructor ensures that only Reference.Id is considered when two dictionary keys - /// of type are compared. - /// - public AsyncApiSecurityRequirement() - : base(new AsyncApiReferenceEqualityComparer()) - { - } - - /// - /// Serialize to Async Api v2. - /// - public void SerializeV2(IAsyncApiWriter writer) - { - if (writer is null) - { - throw new ArgumentNullException(nameof(writer)); - } - - writer.WriteStartObject(); - - foreach (var securitySchemeAndScopesValuePair in this) - { - var securityScheme = securitySchemeAndScopesValuePair.Key; - var scopes = securitySchemeAndScopesValuePair.Value; - - if (securityScheme.Reference == null) - { - // Reaching this point means the reference to a specific AsyncApiSecurityScheme fails. - // We are not able to serialize this SecurityScheme/Scopes key value pair since we do not know what - // string to output. - continue; - } - - // securityScheme.SerializeV2(writer); - writer.WritePropertyName(securityScheme.Reference.FragmentId); - writer.WriteStartArray(); - - foreach (var scope in scopes) - { - writer.WriteValue(scope); - } - - writer.WriteEndArray(); - } - - writer.WriteEndObject(); - } - } -} \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiSecurityScheme.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiSecurityScheme.cs index 45b91a3..46f358a 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiSecurityScheme.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiSecurityScheme.cs @@ -2,11 +2,135 @@ { using System; using System.Collections.Generic; + using System.Linq; using ByteBard.AsyncAPI.Models.Interfaces; using ByteBard.AsyncAPI.Writers; public class AsyncApiSecurityScheme : IAsyncApiSerializable, IAsyncApiExtensible { + public static AsyncApiSecurityScheme UserPassword(string description = null) => new() + { + Type = SecuritySchemeType.UserPassword, + Description = description, + }; + + public static AsyncApiSecurityScheme ApiKey(ParameterLocation @in, string description = null) => new() + { + Type = SecuritySchemeType.ApiKey, + Description = description, + In = @in, + }; + + public static AsyncApiSecurityScheme X509(string description = null) => new() + { + Type = SecuritySchemeType.X509, + Description = description, + }; + + public static AsyncApiSecurityScheme SymmetricEncryption(string description = null) => new() + { + Type = SecuritySchemeType.SymmetricEncryption, + Description = description, + }; + + public static AsyncApiSecurityScheme AsymmetricEncryption(string description = null) => new() + { + Type = SecuritySchemeType.AsymmetricEncryption, + Description = description, + }; + + public static AsyncApiSecurityScheme HttpApiKey(ParameterLocation @in, string name, string description = null) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException($"'{nameof(name)}' cannot be null or whitespace."); + } + + return new AsyncApiSecurityScheme + { + Type = SecuritySchemeType.HttpApiKey, + Description = description, + Name = name, + In = @in, + }; + } + + public static AsyncApiSecurityScheme Http(string scheme, string bearerFormat = null, string description = null) + { + if (string.IsNullOrWhiteSpace(scheme)) + { + throw new ArgumentException($"'{nameof(scheme)}' cannot be null or whitespace."); + } + + return new AsyncApiSecurityScheme + { + Type = SecuritySchemeType.Http, + Description = description, + Scheme = scheme, + BearerFormat = bearerFormat, + }; + } + + public static AsyncApiSecurityScheme OAuth2(AsyncApiOAuthFlows flows, string[] scopes = null, string description = null) + { + if (flows is null) + { + throw new ArgumentException($"'{nameof(flows)}' cannot be null."); + } + + return new AsyncApiSecurityScheme + { + Type = SecuritySchemeType.OAuth2, + Description = description, + Flows = flows, + Scopes = scopes?.ToHashSet() ?? new HashSet(), + }; + } + + public static AsyncApiSecurityScheme OpenIdConnect(Uri openIdConnectUrl, string description = null) + { + if (openIdConnectUrl is null) + { + throw new ArgumentException($"'{nameof(openIdConnectUrl)}' cannot be null."); + } + + if (!openIdConnectUrl.IsAbsoluteUri) + { + throw new ArgumentException($"'{nameof(openIdConnectUrl)}' must be an absolute URI."); + } + + return new AsyncApiSecurityScheme + { + Type = SecuritySchemeType.OpenIdConnect, + Description = description, + OpenIdConnectUrl = openIdConnectUrl, + }; + } + + public static AsyncApiSecurityScheme Plain(string description = null) => new() + { + Type = SecuritySchemeType.Plain, + Description = description, + }; + + public static AsyncApiSecurityScheme ScramSha256(string description = null) => new() + { + Type = SecuritySchemeType.ScramSha256, + Description = description, + }; + + public static AsyncApiSecurityScheme ScramSha512(string description = null) => new() + { + Type = SecuritySchemeType.ScramSha512, + Description = description, + }; + + public static AsyncApiSecurityScheme Gssapi(string description = null) => new() + { + Type = SecuritySchemeType.Gssapi, + Description = description, + }; + /// /// REQUIRED. The type of the security scheme. Valid values are "userPassword", "apiKey", "X509", "symmetricEncryption", "asymmetricEncryption", "httpApiKey", "http", "oauth2", "openIdConnect", "plain", "scramSha256", "scramSha512", and "gssapi". /// @@ -25,7 +149,7 @@ public class AsyncApiSecurityScheme : IAsyncApiSerializable, IAsyncApiExtensible /// /// REQUIRED. The location of the API key. Valid values are "user" and "password" for apiKey and "query", "header" or "cookie" for httpApiKey. /// - public virtual ParameterLocation In { get; set; } + public virtual ParameterLocation? In { get; set; } /// /// REQUIRED. The name of the HTTP Authorization scheme to be used @@ -50,6 +174,11 @@ public class AsyncApiSecurityScheme : IAsyncApiSerializable, IAsyncApiExtensible /// public virtual Uri OpenIdConnectUrl { get; set; } + /// + /// List of the needed scope names. An empty array means no scopes are needed. + /// + public virtual ISet Scopes { get; set; } = new HashSet(); + /// /// Specification Extensions. /// @@ -112,5 +241,62 @@ public virtual void SerializeV2(IAsyncApiWriter writer) writer.WriteEndObject(); } + + public virtual void SerializeV3(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + + // type + writer.WriteRequiredProperty(AsyncApiConstants.Type, this.Type.GetDisplayName()); + + // description + writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description); + + switch (this.Type) + { + case SecuritySchemeType.UserPassword: + break; + case SecuritySchemeType.ApiKey: + writer.WriteOptionalProperty(AsyncApiConstants.In, this.In.GetDisplayName()); + break; + case SecuritySchemeType.X509: + break; + case SecuritySchemeType.SymmetricEncryption: + break; + case SecuritySchemeType.AsymmetricEncryption: + break; + case SecuritySchemeType.HttpApiKey: + writer.WriteOptionalProperty(AsyncApiConstants.Name, this.Name); + writer.WriteOptionalProperty(AsyncApiConstants.In, this.In.GetDisplayName()); + break; + case SecuritySchemeType.Http: + writer.WriteOptionalProperty(AsyncApiConstants.Scheme, this.Scheme); + writer.WriteOptionalProperty(AsyncApiConstants.BearerFormat, this.BearerFormat); + break; + case SecuritySchemeType.OAuth2: + writer.WriteRequiredObject(AsyncApiConstants.Flows, this.Flows, (w, o) => o.SerializeV2(w)); + writer.WriteOptionalCollection(AsyncApiConstants.Scopes, this.Scopes, (writer, s) => writer.WriteValue(s)); + break; + case SecuritySchemeType.OpenIdConnect: + writer.WriteOptionalProperty(AsyncApiConstants.OpenIdConnectUrl, this.OpenIdConnectUrl?.ToString()); + writer.WriteOptionalCollection(AsyncApiConstants.Scopes, this.Scopes, (writer, s) => writer.WriteValue(s)); + break; + case SecuritySchemeType.Plain: + break; + case SecuritySchemeType.ScramSha256: + break; + case SecuritySchemeType.ScramSha512: + break; + case SecuritySchemeType.Gssapi: + break; + default: + break; + } + + // extensions + writer.WriteExtensions(this.Extensions); + + writer.WriteEndObject(); + } } } \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiSerializableExtensions.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiSerializableExtensions.cs index e8d0b7d..2d70e04 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiSerializableExtensions.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiSerializableExtensions.cs @@ -154,6 +154,9 @@ public static void Serialize(this T element, IAsyncApiWriter writer, AsyncApi case AsyncApiVersion.AsyncApi2_0: element.SerializeV2(writer); break; + case AsyncApiVersion.AsyncApi3_0: + element.SerializeV3(writer); + break; default: throw new AsyncApiException($"specification version '{specificationVersion}' is not supported."); } diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiServer.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiServer.cs index b95f9ca..afa8952 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiServer.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiServer.cs @@ -3,12 +3,18 @@ using System.Collections.Generic; using ByteBard.AsyncAPI.Models.Interfaces; using ByteBard.AsyncAPI.Writers; + public class AsyncApiServer : IAsyncApiSerializable, IAsyncApiExtensible { /// - /// REQUIRED. A URL to the target host. + /// REQUIRED. The server host name. It MAY include the port. This field supports Server Variables. Variable substitutions will be made when a variable is named in {braces}. /// - public virtual string Url { get; set; } + public virtual string Host { get; set; } + + /// + /// The path to a resource in the host. This field supports Server Variables. Variable substitutions will be made when a variable is named in {braces}. + /// + public virtual string PathName { get; set; } /// /// REQUIRED. The protocol this URL supports for connection. @@ -16,7 +22,7 @@ public class AsyncApiServer : IAsyncApiSerializable, IAsyncApiExtensible public virtual string Protocol { get; set; } /// - /// the version of the protocol used for connection. + /// The version of the protocol used for connection. For instance: AMQP 0.9.1, HTTP 2.0, Kafka 1.0.0, etc. /// public virtual string ProtocolVersion { get; set; } @@ -25,24 +31,39 @@ public class AsyncApiServer : IAsyncApiSerializable, IAsyncApiExtensible /// public virtual string Description { get; set; } + /// + /// A human-friendly title for the server. + /// + public virtual string Title { get; set; } + + /// + /// A short summary of the server. + /// + public virtual string Summary { get; set; } + /// /// a map between a variable name and its value. The value is used for substitution in the server's URL template. /// public virtual IDictionary Variables { get; set; } = new Dictionary(); /// - /// a declaration of which security mechanisms can be used with this server. The list of values includes alternative security requirement objects that can be used. + /// A declaration of which security schemes can be used with this server. The list of values includes alternative security scheme objects that can be used. Only one of the security scheme objects need to be satisfied to authorize a connection or operation. /// /// /// The name used for each property MUST correspond to a security scheme declared in the Security Schemes under the Components Object. /// - public virtual IList Security { get; set; } = new List(); + public virtual IList Security { get; set; } = new List(); /// /// A list of tags for logical grouping and categorization of servers. /// public virtual IList Tags { get; set; } = new List(); + /// + /// Additional external documentation for this server. + /// + public virtual AsyncApiExternalDocumentation ExternalDocs { get; set; } + /// /// a map where the keys describe the name of the protocol and the values describe protocol-specific definitions for the server. /// @@ -55,7 +76,7 @@ public virtual void SerializeV2(IAsyncApiWriter writer) { writer.WriteStartObject(); - writer.WriteRequiredProperty(AsyncApiConstants.Url, this.Url); + writer.WriteRequiredProperty(AsyncApiConstants.Url, this.GenerateServerUrl()); writer.WriteRequiredProperty(AsyncApiConstants.Protocol, this.Protocol); @@ -65,7 +86,7 @@ public virtual void SerializeV2(IAsyncApiWriter writer) writer.WriteOptionalMap(AsyncApiConstants.Variables, this.Variables, (w, v) => v.SerializeV2(w)); - writer.WriteOptionalCollection(AsyncApiConstants.Security, this.Security, (w, s) => s.SerializeV2(w)); + writer.WriteOptionalCollection(AsyncApiConstants.Security, this.Security, (w, s) => this.SerializeAsSecurityRequirement(s, w)); writer.WriteOptionalCollection(AsyncApiConstants.Tags, this.Tags, (w, s) => s.SerializeV2(w)); @@ -74,5 +95,46 @@ public virtual void SerializeV2(IAsyncApiWriter writer) writer.WriteEndObject(); } + + private void SerializeAsSecurityRequirement(AsyncApiSecurityScheme scheme, IAsyncApiWriter w) + { + if (scheme is not AsyncApiSecuritySchemeReference schemeReference) + { + throw new AsyncApiWriterException("Cannot serialize securityScheme as V2 as it is not a Reference."); + } + + schemeReference.SerializeAsSecurityRequirement(w); + } + + public virtual void SerializeV3(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + + writer.WriteRequiredProperty(AsyncApiConstants.Host, this.Host); + + writer.WriteRequiredProperty(AsyncApiConstants.Protocol, this.Protocol); + + writer.WriteOptionalProperty(AsyncApiConstants.ProtocolVersion, this.ProtocolVersion); + + writer.WriteOptionalProperty(AsyncApiConstants.PathName, this.PathName); + + writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description); + + writer.WriteOptionalMap(AsyncApiConstants.Variables, this.Variables, (w, v) => v.SerializeV3(w)); + + writer.WriteOptionalCollection(AsyncApiConstants.Security, this.Security, (w, s) => s.SerializeV3(w)); + + writer.WriteOptionalCollection(AsyncApiConstants.Tags, this.Tags, (w, s) => s.SerializeV3(w)); + + writer.WriteOptionalObject(AsyncApiConstants.Bindings, this.Bindings, (w, t) => t.SerializeV3(w)); + writer.WriteExtensions(this.Extensions); + + writer.WriteEndObject(); + } + + private string GenerateServerUrl() + { + return $"{this.Host}{this.PathName}"; + } } } \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiServerVariable.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiServerVariable.cs index a49ba60..6f0c6f3 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiServerVariable.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiServerVariable.cs @@ -34,6 +34,16 @@ public class AsyncApiServerVariable : IAsyncApiSerializable, IAsyncApiExtensible public virtual IDictionary Extensions { get; set; } = new Dictionary(); public virtual void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + public virtual void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + private void SerializeCore(IAsyncApiWriter writer) { if (writer is null) { diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiTag.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiTag.cs index 52077aa..89ab74f 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiTag.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiTag.cs @@ -13,21 +13,31 @@ public class AsyncApiTag : IAsyncApiExtensible, IAsyncApiSerializable /// /// REQUIRED. The name of the tag. /// - public string Name { get; set; } + public virtual string Name { get; set; } /// /// a short description for the tag. CommonMark syntax can be used for rich text representation. /// - public string Description { get; set; } + public virtual string Description { get; set; } /// /// additional external documentation for this tag. /// - public AsyncApiExternalDocumentation ExternalDocs { get; set; } + public virtual AsyncApiExternalDocumentation ExternalDocs { get; set; } - public IDictionary Extensions { get; set; } = new Dictionary(); + public virtual IDictionary Extensions { get; set; } = new Dictionary(); - public void SerializeV2(IAsyncApiWriter writer) + public virtual void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + public virtual void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + private void SerializeCore(IAsyncApiWriter writer) { if (writer is null) { diff --git a/src/ByteBard.AsyncAPI/Models/Avro/AsyncApiAvroSchema.cs b/src/ByteBard.AsyncAPI/Models/Avro/AsyncApiAvroSchema.cs index 284e5fc..bf847de 100644 --- a/src/ByteBard.AsyncAPI/Models/Avro/AsyncApiAvroSchema.cs +++ b/src/ByteBard.AsyncAPI/Models/Avro/AsyncApiAvroSchema.cs @@ -1,11 +1,10 @@ namespace ByteBard.AsyncAPI.Models { - using System; using System.Collections.Generic; using ByteBard.AsyncAPI.Models.Interfaces; using ByteBard.AsyncAPI.Writers; - public abstract class AsyncApiAvroSchema : IAsyncApiSerializable, IAsyncApiMessagePayload + public abstract class AsyncApiAvroSchema : IAsyncApiSerializable, IAsyncApiSchema { public abstract string Type { get; } @@ -21,6 +20,8 @@ public static implicit operator AsyncApiAvroSchema(AvroPrimitiveType type) public abstract void SerializeV2(IAsyncApiWriter writer); + public abstract void SerializeV3(IAsyncApiWriter writer); + public virtual bool TryGetAs(out T result) where T : AsyncApiAvroSchema { diff --git a/src/ByteBard.AsyncAPI/Models/Avro/AvroArray.cs b/src/ByteBard.AsyncAPI/Models/Avro/AvroArray.cs index df3d14e..98eaa20 100644 --- a/src/ByteBard.AsyncAPI/Models/Avro/AvroArray.cs +++ b/src/ByteBard.AsyncAPI/Models/Avro/AvroArray.cs @@ -41,5 +41,29 @@ public override void SerializeV2(IAsyncApiWriter writer) writer.WriteEndObject(); } + + public override void SerializeV3(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteOptionalProperty("type", this.Type); + writer.WriteRequiredObject("items", this.Items, (w, f) => f.SerializeV3(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/ByteBard.AsyncAPI/Models/Avro/AvroEnum.cs b/src/ByteBard.AsyncAPI/Models/Avro/AvroEnum.cs index 836ccba..26a503a 100644 --- a/src/ByteBard.AsyncAPI/Models/Avro/AvroEnum.cs +++ b/src/ByteBard.AsyncAPI/Models/Avro/AvroEnum.cs @@ -44,6 +44,16 @@ public class AvroEnum : AsyncApiAvroSchema public override IDictionary Metadata { get; set; } = new Dictionary(); public override void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + public void SerializeCore(IAsyncApiWriter writer) { writer.WriteStartObject(); writer.WriteOptionalProperty("type", this.Type); diff --git a/src/ByteBard.AsyncAPI/Models/Avro/AvroField.cs b/src/ByteBard.AsyncAPI/Models/Avro/AvroField.cs index f60290b..31bbbf3 100644 --- a/src/ByteBard.AsyncAPI/Models/Avro/AvroField.cs +++ b/src/ByteBard.AsyncAPI/Models/Avro/AvroField.cs @@ -19,6 +19,7 @@ public enum AvroFieldOrder [Display("ignore")] Ignore, } + /// /// Represents a field within an Avro record schema. /// @@ -101,5 +102,48 @@ public void SerializeV2(IAsyncApiWriter writer) writer.WriteEndObject(); } + + public void SerializeV3(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteOptionalProperty("name", this.Name); + writer.WriteOptionalObject("type", this.Type, (w, s) => s.SerializeV3(w)); + writer.WriteOptionalProperty("doc", this.Doc); + writer.WriteOptionalObject("default", this.Default, (w, s) => + { + if (s.TryGetValue(out string value) && value == "null") + { + w.WriteNull(); + } + else + { + w.WriteAny(s); + } + }); + + if (this.Order != AvroFieldOrder.None) + { + writer.WriteOptionalProperty("order", this.Order.GetDisplayName()); + } + + writer.WriteOptionalCollection("aliases", this.Aliases, (w, s) => w.WriteValue(s)); + if (this.Metadata.Any()) + { + foreach (var item in this.Metadata) + { + writer.WritePropertyName(item.Key); + if (item.Value == null) + { + writer.WriteNull(); + } + else + { + writer.WriteAny(item.Value); + } + } + } + + writer.WriteEndObject(); + } } } \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Models/Avro/AvroFixed.cs b/src/ByteBard.AsyncAPI/Models/Avro/AvroFixed.cs index fad2968..23d0673 100644 --- a/src/ByteBard.AsyncAPI/Models/Avro/AvroFixed.cs +++ b/src/ByteBard.AsyncAPI/Models/Avro/AvroFixed.cs @@ -18,7 +18,6 @@ public class AvroFixed : AsyncApiAvroSchema /// public string Namespace { get; set; } - /// /// Alternate names for this record. /// @@ -35,6 +34,16 @@ public class AvroFixed : AsyncApiAvroSchema public override IDictionary Metadata { get; set; } = new Dictionary(); public override void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + public void SerializeCore(IAsyncApiWriter writer) { writer.WriteStartObject(); writer.WriteOptionalProperty("type", this.Type); diff --git a/src/ByteBard.AsyncAPI/Models/Avro/AvroMap.cs b/src/ByteBard.AsyncAPI/Models/Avro/AvroMap.cs index 4fadd8a..3799c7a 100644 --- a/src/ByteBard.AsyncAPI/Models/Avro/AvroMap.cs +++ b/src/ByteBard.AsyncAPI/Models/Avro/AvroMap.cs @@ -16,6 +16,16 @@ public class AvroMap : AsyncApiAvroSchema public override IDictionary Metadata { get; set; } = new Dictionary(); public override void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + public void SerializeCore(IAsyncApiWriter writer) { writer.WriteStartObject(); writer.WriteOptionalProperty("type", this.Type); diff --git a/src/ByteBard.AsyncAPI/Models/Avro/AvroPrimitive.cs b/src/ByteBard.AsyncAPI/Models/Avro/AvroPrimitive.cs index 3960a9a..1f920f0 100644 --- a/src/ByteBard.AsyncAPI/Models/Avro/AvroPrimitive.cs +++ b/src/ByteBard.AsyncAPI/Models/Avro/AvroPrimitive.cs @@ -19,6 +19,16 @@ public AvroPrimitive(AvroPrimitiveType type) } public override void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + public void SerializeCore(IAsyncApiWriter writer) { writer.WriteValue(this.Type); if (this.Metadata.Any()) diff --git a/src/ByteBard.AsyncAPI/Models/Avro/AvroRecord.cs b/src/ByteBard.AsyncAPI/Models/Avro/AvroRecord.cs index 93a45c4..c2676a1 100644 --- a/src/ByteBard.AsyncAPI/Models/Avro/AvroRecord.cs +++ b/src/ByteBard.AsyncAPI/Models/Avro/AvroRecord.cs @@ -65,5 +65,33 @@ public override void SerializeV2(IAsyncApiWriter writer) writer.WriteEndObject(); } + + public override void SerializeV3(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.SerializeV3(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/ByteBard.AsyncAPI/Models/Avro/AvroUnion.cs b/src/ByteBard.AsyncAPI/Models/Avro/AvroUnion.cs index 530b2b4..3fb5186 100644 --- a/src/ByteBard.AsyncAPI/Models/Avro/AvroUnion.cs +++ b/src/ByteBard.AsyncAPI/Models/Avro/AvroUnion.cs @@ -44,5 +44,32 @@ public override void SerializeV2(IAsyncApiWriter writer) writer.WriteEndArray(); } + + public override void SerializeV3(IAsyncApiWriter writer) + { + writer.WriteStartArray(); + foreach (var type in this.Types) + { + type.SerializeV3(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/ByteBard.AsyncAPI/Models/Interfaces/IAsyncApiPayload.cs b/src/ByteBard.AsyncAPI/Models/Interfaces/IAsyncApiPayload.cs deleted file mode 100644 index 84be54a..0000000 --- a/src/ByteBard.AsyncAPI/Models/Interfaces/IAsyncApiPayload.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace ByteBard.AsyncAPI.Models.Interfaces -{ - public interface IAsyncApiMessagePayload : IAsyncApiSerializable - { - } -} diff --git a/src/ByteBard.AsyncAPI/Models/Interfaces/IAsyncApiSchema.cs b/src/ByteBard.AsyncAPI/Models/Interfaces/IAsyncApiSchema.cs new file mode 100644 index 0000000..46e64a5 --- /dev/null +++ b/src/ByteBard.AsyncAPI/Models/Interfaces/IAsyncApiSchema.cs @@ -0,0 +1,6 @@ +namespace ByteBard.AsyncAPI.Models.Interfaces +{ + public interface IAsyncApiSchema : IAsyncApiSerializable + { + } +} diff --git a/src/ByteBard.AsyncAPI/Models/Interfaces/IAsyncApiSerializable.cs b/src/ByteBard.AsyncAPI/Models/Interfaces/IAsyncApiSerializable.cs index e31af7b..a2fa2f2 100644 --- a/src/ByteBard.AsyncAPI/Models/Interfaces/IAsyncApiSerializable.cs +++ b/src/ByteBard.AsyncAPI/Models/Interfaces/IAsyncApiSerializable.cs @@ -5,5 +5,7 @@ public interface IAsyncApiSerializable : IAsyncApiElement { void SerializeV2(IAsyncApiWriter writer); + + void SerializeV3(IAsyncApiWriter writer); } } diff --git a/src/ByteBard.AsyncAPI/Models/JsonSchema/AsyncApiJsonSchema.cs b/src/ByteBard.AsyncAPI/Models/JsonSchema/AsyncApiJsonSchema.cs index 6a882ff..bdb3dbe 100644 --- a/src/ByteBard.AsyncAPI/Models/JsonSchema/AsyncApiJsonSchema.cs +++ b/src/ByteBard.AsyncAPI/Models/JsonSchema/AsyncApiJsonSchema.cs @@ -9,7 +9,7 @@ /// /// The Schema Object allows the definition of input and output data types. /// - public class AsyncApiJsonSchema : IAsyncApiExtensible, IAsyncApiSerializable, IAsyncApiMessagePayload + public class AsyncApiJsonSchema : IAsyncApiExtensible, IAsyncApiSerializable, IAsyncApiSchema { /// /// follow JSON Schema definition. Short text providing information about the data. @@ -248,7 +248,19 @@ public class AsyncApiJsonSchema : IAsyncApiExtensible, IAsyncApiSerializable, IA public virtual IDictionary Extensions { get; set; } = new Dictionary(); + public static implicit operator AsyncApiMultiFormatSchema(AsyncApiJsonSchema schema) => new AsyncApiMultiFormatSchema { Schema = schema }; + public virtual void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + public virtual void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeCore(writer); + } + + private void SerializeCore(IAsyncApiWriter writer) { writer.WriteStartObject(); diff --git a/src/ByteBard.AsyncAPI/Models/MessagePayloadExtensions.cs b/src/ByteBard.AsyncAPI/Models/MessagePayloadExtensions.cs index bf4916b..3f75153 100644 --- a/src/ByteBard.AsyncAPI/Models/MessagePayloadExtensions.cs +++ b/src/ByteBard.AsyncAPI/Models/MessagePayloadExtensions.cs @@ -1,26 +1,24 @@ namespace ByteBard.AsyncAPI.Models { using ByteBard.AsyncAPI.Models.Interfaces; - using ByteBard.AsyncAPI.Writers; - using System; - public static class MessagePayloadExtensions + public static class MultiFormatSchemaExtensions { - public static bool TryGetAs(this IAsyncApiMessagePayload payload, out T result) - where T : class, IAsyncApiMessagePayload + public static bool TryGetAs(this IAsyncApiSchema payload, out T result) + where T : class, IAsyncApiSchema { result = payload as T; return result != null; } - public static T As(this IAsyncApiMessagePayload payload) - where T : class, IAsyncApiMessagePayload + public static T As(this IAsyncApiSchema payload) + where T : class, IAsyncApiSchema { return payload as T; } - public static bool Is(this IAsyncApiMessagePayload payload) - where T : class, IAsyncApiMessagePayload + public static bool Is(this IAsyncApiSchema payload) + where T : class, IAsyncApiSchema { return payload is T; } diff --git a/src/ByteBard.AsyncAPI/Models/ReferenceType.cs b/src/ByteBard.AsyncAPI/Models/ReferenceType.cs index 42b0f4d..8bf9cd0 100644 --- a/src/ByteBard.AsyncAPI/Models/ReferenceType.cs +++ b/src/ByteBard.AsyncAPI/Models/ReferenceType.cs @@ -84,6 +84,31 @@ public enum ReferenceType /// /// The server variable. /// - [Display("serverVariable")] ServerVariable, + [Display("serverVariables")] ServerVariable, + + /// + /// The tag. + /// + [Display("tags")] Tag, + + /// + /// ReplyAddresses item. + /// + [Display("replyAddresses")] OperationReplyAddress, + + /// + /// ExternalDocs item. + /// + [Display("externalDocs")] ExternalDocs, + + /// + /// Replies item. + /// + [Display("replies")] OperationReply, + + /// + /// Operations item. + /// + [Display("operations")] Operation, } } diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiAvroSchemaReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiAvroSchemaReference.cs index 63ea614..2ed4b33 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiAvroSchemaReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiAvroSchemaReference.cs @@ -2,9 +2,11 @@ { using System; using System.Collections.Generic; + using System.Diagnostics; using ByteBard.AsyncAPI.Models.Interfaces; using ByteBard.AsyncAPI.Writers; + [DebuggerDisplay("{Reference}")] public class AsyncApiAvroSchemaReference : AsyncApiAvroSchema, IAsyncApiReferenceable { private AsyncApiAvroSchema target; @@ -37,6 +39,7 @@ public override T As() { return null; } + return this.Target.As(); } @@ -46,6 +49,7 @@ public override bool Is() { return false; } + return this.Target.Is(); } @@ -56,6 +60,7 @@ public override bool TryGetAs(out T result) result = default; return false; } + return this.Target.TryGetAs(out result); } @@ -74,5 +79,21 @@ public override void SerializeV2(IAsyncApiWriter writer) this.Target.SerializeV2(writer); } + + public override void SerializeV3(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (this.Reference != null && !writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV2(writer); + return; + } + + this.Target.SerializeV3(writer); + } } } \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiBindingsReference{TBinding}.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiBindingsReference{TBinding}.cs index b02ed53..d16fd24 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiBindingsReference{TBinding}.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiBindingsReference{TBinding}.cs @@ -2,9 +2,11 @@ { using System; using System.Collections.Generic; + using System.Diagnostics; using ByteBard.AsyncAPI.Models.Interfaces; using ByteBard.AsyncAPI.Writers; + [DebuggerDisplay("{Reference}")] public class AsyncApiBindingsReference : AsyncApiBindings, IAsyncApiReferenceable where TBinding : IBinding { @@ -32,6 +34,8 @@ public override void Add(TBinding binding) public override ICollection Values => this.Target.Values; + public override IDictionary Extensions => this.target.Extensions; + public override int Count => this.Target.Count; public override bool IsReadOnly => this.Target.IsReadOnly; @@ -43,14 +47,17 @@ public AsyncApiBindingsReference(string reference) { type = ReferenceType.ServerBindings; } + if (typeof(TBinding) == typeof(IMessageBinding)) { type = ReferenceType.MessageBindings; } + if (typeof(TBinding) == typeof(IOperationBinding)) { type = ReferenceType.OperationBindings; } + if (typeof(TBinding) == typeof(IChannelBinding)) { type = ReferenceType.ChannelBindings; @@ -80,6 +87,22 @@ public override void SerializeV2(IAsyncApiWriter writer) this.Target.SerializeV2(writer); } + public override void SerializeV3(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (this.Reference != null && !writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV3(writer); + return; + } + + this.Target.SerializeV3(writer); + } + public override void Add(string key, TBinding value) { this.Target.Add(key, value); diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiChannelReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiChannelReference.cs index 681a4cb..0447d04 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiChannelReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiChannelReference.cs @@ -1,10 +1,13 @@ namespace ByteBard.AsyncAPI.Models { + using System; using System.Collections.Generic; + using System.Diagnostics; using ByteBard.AsyncAPI.Models.Interfaces; using ByteBard.AsyncAPI.Writers; - public class AsyncApiChannelReference : AsyncApiChannel, IAsyncApiReferenceable + [DebuggerDisplay("{Reference}")] + public class AsyncApiChannelReference : AsyncApiChannel, IAsyncApiReferenceable, IEquatable, IEquatable { private AsyncApiChannel target; @@ -22,16 +25,24 @@ public AsyncApiChannelReference(string reference) this.Reference = new AsyncApiReference(reference, ReferenceType.Channel); } - public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + public override string Address { get => this.Target?.Address; set => this.Target.Address = value; } + + public override IDictionary Messages { get => this.Target?.Messages; set => this.Target.Messages = value; } + + public override string Title { get => this.Target?.Title; set => this.Target.Title = value; } - public override IList Servers { get => this.Target?.Servers; set => this.Target.Servers = value; } + public override string Summary { get => this.Target?.Summary; set => this.Target.Summary = value; } - public override AsyncApiOperation Subscribe { get => this.Target?.Subscribe; set => this.Target.Subscribe = value; } + public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } - public override AsyncApiOperation Publish { get => this.Target?.Publish; set => this.Target.Publish = value; } + public override IList Servers { get => this.Target?.Servers; set => this.Target.Servers = value; } public override IDictionary Parameters { get => this.Target?.Parameters; set => this.Target.Parameters = value; } + public override IList Tags { get => this.Target?.Tags; set => this.Target.Tags = value; } + + public override AsyncApiExternalDocumentation ExternalDocs { get => this.Target?.ExternalDocs; set => this.Target.ExternalDocs = value; } + public override AsyncApiBindings Bindings { get => this.Target?.Bindings; set => this.Target.Bindings = value; } public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } @@ -53,5 +64,61 @@ public override void SerializeV2(IAsyncApiWriter writer) this.Target.SerializeV2(writer); } } + + public override void SerializeV3(IAsyncApiWriter writer) + { + if (!writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV3(writer); + return; + } + else + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV3(writer); + } + } + + public static bool operator !=(AsyncApiChannelReference left, AsyncApiChannelReference right) => !(left == right); + + public static bool operator ==(AsyncApiChannelReference left, AsyncApiChannelReference right) + { + return Equals(left, null) ? Equals(right, null) : left.Equals(right); + } + + public bool Equals(AsyncApiChannelReference other) + { + if (other == null) + { + return false; + } + + if (other.Target is AsyncApiChannelReference reference) + { + return this.Equals(reference); + } + + return this.Target == other.Target; + } + + public override bool Equals(object obj) + { + if (obj is AsyncApiChannelReference reference) + { + return this.Equals(reference); + } + + if (obj is AsyncApiChannel channel) + { + return this.Equals(channel); + } + + return false; + } + + public bool Equals(AsyncApiChannel other) + { + return this.Target == other; + } } } diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiCorrelationIdReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiCorrelationIdReference.cs index caa6384..822dfbc 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiCorrelationIdReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiCorrelationIdReference.cs @@ -1,13 +1,17 @@ namespace ByteBard.AsyncAPI.Models { + using System; using System.Collections.Generic; - using ByteBard.AsyncAPI.Models.Interfaces; - using ByteBard.AsyncAPI.Writers; - /// /// The definition of a correlation ID this application MAY use. /// - public class AsyncApiCorrelationIdReference : AsyncApiCorrelationId, IAsyncApiReferenceable + + using System.Diagnostics; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Writers; + + [DebuggerDisplay("{Reference}")] + public class AsyncApiCorrelationIdReference : AsyncApiCorrelationId, IAsyncApiReferenceable, IEquatable, IEquatable { private AsyncApiCorrelationId target; @@ -35,6 +39,48 @@ public AsyncApiCorrelationIdReference(string reference) public bool UnresolvedReference { get { return this.Target == null; } } + public static bool operator !=(AsyncApiCorrelationIdReference left, AsyncApiCorrelationIdReference right) => !(left == right); + + public static bool operator ==(AsyncApiCorrelationIdReference left, AsyncApiCorrelationIdReference right) + { + return Equals(left, null) ? Equals(right, null) : left.Equals(right); + } + + public bool Equals(AsyncApiCorrelationIdReference other) + { + if (other is null) + { + return false; + } + + if (other.Target is AsyncApiCorrelationIdReference reference) + { + return this.Equals(reference); + } + + return this.Target == other.Target; + } + + public override bool Equals(object obj) + { + if (obj is AsyncApiCorrelationIdReference reference) + { + return this.Equals(reference); + } + + if (obj is AsyncApiCorrelationId message) + { + return this.Equals(message); + } + + return false; + } + + public bool Equals(AsyncApiCorrelationId other) + { + return this.Target == other; + } + public override void SerializeV2(IAsyncApiWriter writer) { if (!writer.GetSettings().ShouldInlineReference(this.Reference)) @@ -48,5 +94,19 @@ public override void SerializeV2(IAsyncApiWriter writer) this.Target.SerializeV2(writer); } } + + public override void SerializeV3(IAsyncApiWriter writer) + { + if (!writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV3(writer); + return; + } + else + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV3(writer); + } + } } } diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiExternalDocumentationReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiExternalDocumentationReference.cs new file mode 100644 index 0000000..0fc7680 --- /dev/null +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiExternalDocumentationReference.cs @@ -0,0 +1,115 @@ +namespace ByteBard.AsyncAPI.Models +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Writers; + + [DebuggerDisplay("{Reference}")] + public class AsyncApiExternalDocumentationReference : AsyncApiExternalDocumentation, IAsyncApiReferenceable, IEquatable, IEquatable + { + private AsyncApiExternalDocumentation target; + + private AsyncApiExternalDocumentation Target + { + get + { + this.target ??= this.Reference.Workspace?.ResolveReference(this.Reference.Reference); + return this.target; + } + } + + public AsyncApiExternalDocumentationReference(string reference) + { + this.Reference = new AsyncApiReference(reference, ReferenceType.ExternalDocs); + } + + public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + + public override Uri Url { get => this.Target?.Url; set => this.Target.Url = value; } + + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + + public AsyncApiReference Reference { get; set; } + + public bool UnresolvedReference { get { return this.Target == null; } } + + public static bool operator !=(AsyncApiExternalDocumentationReference left, AsyncApiExternalDocumentationReference right) => !(left == right); + + public static bool operator ==(AsyncApiExternalDocumentationReference left, AsyncApiExternalDocumentationReference right) + { + return Equals(left, null) ? Equals(right, null) : left.Equals(right); + } + + public bool Equals(AsyncApiExternalDocumentationReference other) + { + if (other is null) + { + return false; + } + + if (other.Target is AsyncApiExternalDocumentationReference reference) + { + return this.Equals(reference); + } + + return this.Target == other.Target; + } + + public override bool Equals(object obj) + { + if (obj is AsyncApiExternalDocumentationReference reference) + { + return this.Equals(reference); + } + + if (obj is AsyncApiExternalDocumentation message) + { + return this.Equals(message); + } + + return false; + } + + public bool Equals(AsyncApiExternalDocumentation other) + { + return this.Target == other; + } + + /// + /// Serializes the v2. + /// + /// + /// If serialization of the referenced ExternalDocs will be skipped. + /// + /// The writer. + public override void SerializeV2(IAsyncApiWriter writer) + { + if (!writer.GetSettings().ShouldInlineReference(this.Reference)) + { + // We cannot serialize the ExternalDocs as a reference under V2. + return; + } + else + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV2(writer); + } + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + if (!writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV3(writer); + return; + } + else + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV3(writer); + } + } + } +} diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiJsonSchemaReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiJsonSchemaReference.cs index bf54ae3..1545479 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiJsonSchemaReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiJsonSchemaReference.cs @@ -2,10 +2,12 @@ { using System; using System.Collections.Generic; + using System.Diagnostics; using ByteBard.AsyncAPI.Models.Interfaces; using ByteBard.AsyncAPI.Writers; - public class AsyncApiJsonSchemaReference : AsyncApiJsonSchema, IAsyncApiReferenceable + [DebuggerDisplay("{Reference}")] + public class AsyncApiJsonSchemaReference : AsyncApiJsonSchema, IAsyncApiReferenceable, IEquatable, IEquatable { private AsyncApiJsonSchema target; @@ -13,7 +15,7 @@ private AsyncApiJsonSchema Target { get { - this.target ??= this.Reference.Workspace?.ResolveReference(this.Reference); + this.target ??= this.Reference.Workspace?.ResolveReference(this.Reference) ?? this.Reference.Workspace?.ResolveReference(this.Reference)?.Schema?.As(); return this.target; } } @@ -288,6 +290,48 @@ public override IDictionary Extensions set => this.Target.Extensions = value; } + public static bool operator !=(AsyncApiJsonSchemaReference left, AsyncApiJsonSchemaReference right) => !(left == right); + + public static bool operator ==(AsyncApiJsonSchemaReference left, AsyncApiJsonSchemaReference right) + { + return Equals(left, null) ? Equals(right, null) : left.Equals(right); + } + + public bool Equals(AsyncApiJsonSchemaReference other) + { + if (other is null) + { + return false; + } + + if (other.Target is AsyncApiJsonSchemaReference reference) + { + return this.Equals(reference); + } + + return this.Target == other.Target; + } + + public override bool Equals(object obj) + { + if (obj is AsyncApiJsonSchemaReference reference) + { + return this.Equals(reference); + } + + if (obj is AsyncApiJsonSchema message) + { + return this.Equals(message); + } + + return false; + } + + public bool Equals(AsyncApiJsonSchema other) + { + return this.Target == other; + } + public override void SerializeV2(IAsyncApiWriter writer) { if (writer is null) @@ -313,5 +357,31 @@ public override void SerializeV2(IAsyncApiWriter writer) this.Target.SerializeV2(writer); } + + public override void SerializeV3(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + var settings = writer.GetSettings(); + if (!settings.ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV3(writer); + return; + } + + this.Reference.Workspace = writer.Workspace; + // If Loop is detected then just Serialize as a reference. + if (!settings.LoopDetector.PushLoop(this)) + { + settings.LoopDetector.SaveLoop(this); + this.Reference.SerializeV3(writer); + return; + } + + this.Target.SerializeV3(writer); + } } } \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiMessageReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiMessageReference.cs index 45665a3..fd2099a 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiMessageReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiMessageReference.cs @@ -1,13 +1,16 @@ namespace ByteBard.AsyncAPI.Models { + using System; using System.Collections.Generic; + using System.Diagnostics; using ByteBard.AsyncAPI.Models.Interfaces; using ByteBard.AsyncAPI.Writers; /// /// The definition of a message this application MAY use. /// - public class AsyncApiMessageReference : AsyncApiMessage, IAsyncApiReferenceable + [DebuggerDisplay("{Reference}")] + public class AsyncApiMessageReference : AsyncApiMessage, IAsyncApiReferenceable, IEquatable, IEquatable { private AsyncApiMessage target; @@ -25,16 +28,12 @@ public AsyncApiMessageReference(string reference) this.Reference = new AsyncApiReference(reference, ReferenceType.Message); } - public override string MessageId { get => this.Target?.MessageId; set => this.Target.MessageId = value; } + public override AsyncApiMultiFormatSchema Headers { get => this.Target?.Headers; set => this.Target.Headers = value; } - public override AsyncApiJsonSchema Headers { get => this.Target?.Headers; set => this.Target.Headers = value; } - - public override IAsyncApiMessagePayload Payload { get => this.Target?.Payload; set => this.Target.Payload = value; } + public override AsyncApiMultiFormatSchema Payload { get => this.Target?.Payload; set => this.Target.Payload = value; } public override AsyncApiCorrelationId CorrelationId { get => this.Target?.CorrelationId; set => this.Target.CorrelationId = value; } - public override string SchemaFormat { get => this.Target?.SchemaFormat; set => this.Target.SchemaFormat = value; } - public override string ContentType { get => this.Target?.ContentType; set => this.Target.ContentType = value; } public override string Name { get => this.Target?.Name; set => this.Target.Name = value; } @@ -61,10 +60,67 @@ public AsyncApiMessageReference(string reference) public bool UnresolvedReference { get { return this.Target == null; } } + public static bool operator !=(AsyncApiMessageReference left, AsyncApiMessageReference right) => !(left == right); + + public static bool operator ==(AsyncApiMessageReference left, AsyncApiMessageReference right) + { + return Equals(left, null) ? Equals(right, null) : left.Equals(right); + } + + public bool Equals(AsyncApiMessageReference other) + { + if (other is null) + { + return false; + } + + if (other.Target is AsyncApiMessageReference reference) + { + return this.Equals(reference); + } + + return this.Target == other.Target; + } + + public override bool Equals(object obj) + { + if (obj is AsyncApiMessageReference reference) + { + return this.Equals(reference); + } + + if (obj is AsyncApiMessage message) + { + return this.Equals(message); + } + + return false; + } + + public bool Equals(AsyncApiMessage other) + { + return this.Target == other; + } + public override void SerializeV2(IAsyncApiWriter writer) { if (!writer.GetSettings().ShouldInlineReference(this.Reference)) { + // If messages are from a V2 component, and is proxied through channels its a reference pointing to a reference. + if (this.Target is AsyncApiMessageReference reference) + { + reference.SerializeV2(writer); + return; + } + + if (this.Reference.Reference.StartsWith("#/channels")) + { + // Try force inline anyway. + this.Reference.Workspace = writer.Workspace; + this.Target?.SerializeV2(writer); + return; + } + this.Reference.SerializeV2(writer); return; } @@ -74,5 +130,19 @@ public override void SerializeV2(IAsyncApiWriter writer) this.Target.SerializeV2(writer); } } + + public override void SerializeV3(IAsyncApiWriter writer) + { + if (!writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV3(writer); + return; + } + else + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV3(writer); + } + } } } diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiMessageTraitReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiMessageTraitReference.cs index a88bd00..ba950df 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiMessageTraitReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiMessageTraitReference.cs @@ -1,13 +1,17 @@ namespace ByteBard.AsyncAPI.Models { + using System; using System.Collections.Generic; - using ByteBard.AsyncAPI.Models.Interfaces; - using ByteBard.AsyncAPI.Writers; - /// /// The definition of a message trait this application MAY use. /// - public class AsyncApiMessageTraitReference : AsyncApiMessageTrait, IAsyncApiReferenceable + + using System.Diagnostics; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Writers; + + [DebuggerDisplay("{Reference}")] + public class AsyncApiMessageTraitReference : AsyncApiMessageTrait, IAsyncApiReferenceable, IEquatable, IEquatable { private AsyncApiMessageTrait target; @@ -25,14 +29,10 @@ public AsyncApiMessageTraitReference(string reference) this.Reference = new AsyncApiReference(reference, ReferenceType.MessageTrait); } - public override string MessageId { get => this.Target?.MessageId; set => this.Target.MessageId = value; } - - public override AsyncApiJsonSchema Headers { get => this.Target?.Headers; set => this.Target.Headers = value; } + public override AsyncApiMultiFormatSchema Headers { get => this.Target?.Headers; set => this.Target.Headers = value; } public override AsyncApiCorrelationId CorrelationId { get => this.Target?.CorrelationId; set => this.Target.CorrelationId = value; } - public override string SchemaFormat { get => this.Target?.SchemaFormat; set => this.Target.SchemaFormat = value; } - public override string ContentType { get => this.Target?.ContentType; set => this.Target.ContentType = value; } public override string Name { get => this.Target?.Name; set => this.Target.Name = value; } @@ -57,6 +57,48 @@ public AsyncApiMessageTraitReference(string reference) public bool UnresolvedReference { get { return this.Target == null; } } + public static bool operator !=(AsyncApiMessageTraitReference left, AsyncApiMessageTraitReference right) => !(left == right); + + public static bool operator ==(AsyncApiMessageTraitReference left, AsyncApiMessageTraitReference right) + { + return Equals(left, null) ? Equals(right, null) : left.Equals(right); + } + + public bool Equals(AsyncApiMessageTraitReference other) + { + if (other is null) + { + return false; + } + + if (other.Target is AsyncApiMessageTraitReference reference) + { + return this.Equals(reference); + } + + return this.Target == other.Target; + } + + public override bool Equals(object obj) + { + if (obj is AsyncApiMessageTraitReference reference) + { + return this.Equals(reference); + } + + if (obj is AsyncApiMessageTrait message) + { + return this.Equals(message); + } + + return false; + } + + public bool Equals(AsyncApiMessageTrait other) + { + return this.Target == other; + } + public override void SerializeV2(IAsyncApiWriter writer) { if (!writer.GetSettings().ShouldInlineReference(this.Reference)) @@ -70,5 +112,19 @@ public override void SerializeV2(IAsyncApiWriter writer) this.Target.SerializeV2(writer); } } + + public override void SerializeV3(IAsyncApiWriter writer) + { + if (!writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV3(writer); + return; + } + else + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV3(writer); + } + } } } diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiMultiFormatSchemaReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiMultiFormatSchemaReference.cs new file mode 100644 index 0000000..9e165a4 --- /dev/null +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiMultiFormatSchemaReference.cs @@ -0,0 +1,66 @@ +namespace ByteBard.AsyncAPI.Models +{ + /// + /// The definition of a MultiFormatSchema this application MAY use. + /// + + using System.Diagnostics; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Writers; + + [DebuggerDisplay("{Reference}")] + public class AsyncApiMultiFormatSchemaReference : AsyncApiMultiFormatSchema, IAsyncApiReferenceable + { + private AsyncApiMultiFormatSchema target; + + private AsyncApiMultiFormatSchema Target + { + get + { + this.target ??= this.Reference.Workspace?.ResolveReference(this.Reference.Reference); + return this.target; + } + } + + public AsyncApiMultiFormatSchemaReference(string reference) + { + this.Reference = new AsyncApiReference(reference, ReferenceType.Schema); + } + + public override string SchemaFormat { get => this.Target?.SchemaFormat; set => this.Target.SchemaFormat = value; } + + public override IAsyncApiSchema Schema { get => this.Target?.Schema; set => this.Target.Schema = value; } + + public AsyncApiReference Reference { get; set; } + + public bool UnresolvedReference { get { return this.Target == null; } } + + public override void SerializeV2(IAsyncApiWriter writer) + { + if (!writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV2(writer); + return; + } + else + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV2(writer); + } + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + if (!writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV3(writer); + return; + } + else + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV3(writer); + } + } + } +} diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationReference.cs new file mode 100644 index 0000000..57c7a0c --- /dev/null +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationReference.cs @@ -0,0 +1,124 @@ +namespace ByteBard.AsyncAPI.Models +{ + using System; + using System.Collections.Generic; + /// + /// The definition of an operation trait this application MAY use. + /// + + using System.Diagnostics; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Writers; + + [DebuggerDisplay("{Reference}")] + public class AsyncApiOperationReference : AsyncApiOperation, IAsyncApiReferenceable, IEquatable, IEquatable + { + private AsyncApiOperation target; + + private AsyncApiOperation Target + { + get + { + this.target ??= this.Reference.Workspace?.ResolveReference(this.Reference); + return this.target; + } + } + + public AsyncApiOperationReference(string reference) + { + this.Reference = new AsyncApiReference(reference, ReferenceType.Operation); + } + + public override AsyncApiAction Action { get => this.Target.Action; set => this.Target.Action = value; } + + public override AsyncApiChannelReference Channel { get => this.Target?.Channel; set => this.Target.Channel = value; } + + public override string Title { get => this.Target?.Title; set => this.Target.Title = value; } + + public override string Summary { get => this.Target?.Summary; set => this.Target.Summary = value; } + + public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + + public override IList Security { get => this.Target?.Security; set => this.Target.Security = value; } + + public override IList Tags { get => this.Target?.Tags; set => this.Target.Tags = value; } + + public override AsyncApiExternalDocumentation ExternalDocs { get => this.Target?.ExternalDocs; set => this.Target.ExternalDocs = value; } + + public override AsyncApiBindings Bindings { get => this.Target?.Bindings; set => this.Target.Bindings = value; } + + public override IList Traits { get => this.Target?.Traits; set => this.Target.Traits = value; } + + public override IList Messages { get => this.Target?.Messages; set => this.Target.Messages = value; } + + public override AsyncApiOperationReply Reply { get => this.Target?.Reply; set => this.Target.Reply = value; } + + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + + public AsyncApiReference Reference { get; set; } + + public bool UnresolvedReference { get { return this.Target == null; } } + + public static bool operator !=(AsyncApiOperationReference left, AsyncApiOperationReference right) => !(left == right); + + public static bool operator ==(AsyncApiOperationReference left, AsyncApiOperationReference right) + { + return Equals(left, null) ? Equals(right, null) : left.Equals(right); + } + + public bool Equals(AsyncApiOperationReference other) + { + if (other is null) + { + return false; + } + + if (other.Target is AsyncApiOperationReference reference) + { + return this.Equals(reference); + } + + return this.Target == other.Target; + } + + public override bool Equals(object obj) + { + if (obj is AsyncApiOperationReference reference) + { + return this.Equals(reference); + } + + if (obj is AsyncApiOperation message) + { + return this.Equals(message); + } + + return false; + } + + public bool Equals(AsyncApiOperation other) + { + return this.Target == other; + } + + public override void SerializeV2(IAsyncApiWriter writer) + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV2(writer); + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + if (!writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV3(writer); + return; + } + else + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV3(writer); + } + } + } +} diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationReplyAddressReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationReplyAddressReference.cs new file mode 100644 index 0000000..7c6460c --- /dev/null +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationReplyAddressReference.cs @@ -0,0 +1,94 @@ +namespace ByteBard.AsyncAPI.Models +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Writers; + + [DebuggerDisplay("{Reference}")] + public class AsyncApiOperationReplyAddressReference : AsyncApiOperationReplyAddress, IAsyncApiReferenceable, IEquatable, IEquatable + { + private AsyncApiOperationReplyAddress target; + + private AsyncApiOperationReplyAddress Target + { + get + { + this.target ??= this.Reference.Workspace?.ResolveReference(this.Reference.Reference); + return this.target; + } + } + + public AsyncApiOperationReplyAddressReference(string reference) + { + this.Reference = new AsyncApiReference(reference, ReferenceType.OperationReplyAddress); + } + + public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + + public override string Location { get => this.Target?.Location; set => this.Target.Location = value; } + + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + + public AsyncApiReference Reference { get; set; } + + public bool UnresolvedReference { get { return this.Target == null; } } + + public static bool operator !=(AsyncApiOperationReplyAddressReference left, AsyncApiOperationReplyAddressReference right) => !(left == right); + + public static bool operator ==(AsyncApiOperationReplyAddressReference left, AsyncApiOperationReplyAddressReference right) + { + return Equals(left, null) ? Equals(right, null) : left.Equals(right); + } + + public bool Equals(AsyncApiOperationReplyAddressReference other) + { + if (other is null) + { + return false; + } + + if (other.Target is AsyncApiOperationReplyAddressReference reference) + { + return this.Equals(reference); + } + + return this.Target == other.Target; + } + + public override bool Equals(object obj) + { + if (obj is AsyncApiOperationReplyAddressReference reference) + { + return this.Equals(reference); + } + + if (obj is AsyncApiOperationReplyAddress message) + { + return this.Equals(message); + } + + return false; + } + + public bool Equals(AsyncApiOperationReplyAddress other) + { + return this.Target == other; + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + if (!writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV3(writer); + return; + } + else + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV3(writer); + } + } + } +} diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationReplyReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationReplyReference.cs new file mode 100644 index 0000000..7570277 --- /dev/null +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationReplyReference.cs @@ -0,0 +1,96 @@ +namespace ByteBard.AsyncAPI.Models +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Writers; + + [DebuggerDisplay("{Reference}")] + public class AsyncApiOperationReplyReference : AsyncApiOperationReply, IAsyncApiReferenceable, IEquatable, IEquatable + { + private AsyncApiOperationReply target; + + private AsyncApiOperationReply Target + { + get + { + this.target ??= this.Reference.Workspace?.ResolveReference(this.Reference.Reference); + return this.target; + } + } + + public AsyncApiOperationReplyReference(string reference) + { + this.Reference = new AsyncApiReference(reference, ReferenceType.OperationReply); + } + + public override AsyncApiOperationReplyAddress Address { get => this.Target?.Address; set => this.Target.Address = value; } + + public override AsyncApiChannelReference Channel { get => this.Target?.Channel; set => this.Target.Channel = value; } + + public override IList Messages { get => this.Target?.Messages; set => this.Target.Messages = value; } + + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + + public AsyncApiReference Reference { get; set; } + + public bool UnresolvedReference { get { return this.Target == null; } } + + public static bool operator !=(AsyncApiOperationReplyReference left, AsyncApiOperationReplyReference right) => !(left == right); + + public static bool operator ==(AsyncApiOperationReplyReference left, AsyncApiOperationReplyReference right) + { + return Equals(left, null) ? Equals(right, null) : left.Equals(right); + } + + public bool Equals(AsyncApiOperationReplyReference other) + { + if (other is null) + { + return false; + } + + if (other.Target is AsyncApiOperationReplyReference reference) + { + return this.Equals(reference); + } + + return this.Target == other.Target; + } + + public override bool Equals(object obj) + { + if (obj is AsyncApiOperationReplyReference reference) + { + return this.Equals(reference); + } + + if (obj is AsyncApiOperationReply message) + { + return this.Equals(message); + } + + return false; + } + + public bool Equals(AsyncApiOperationReply other) + { + return this.Target == other; + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + if (!writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV3(writer); + return; + } + else + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV3(writer); + } + } + } +} diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationTraitReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationTraitReference.cs index 3fa077f..aa6c42f 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationTraitReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiOperationTraitReference.cs @@ -1,13 +1,17 @@ namespace ByteBard.AsyncAPI.Models { + using System; using System.Collections.Generic; - using ByteBard.AsyncAPI.Models.Interfaces; - using ByteBard.AsyncAPI.Writers; - /// /// The definition of an operation trait this application MAY use. /// - public class AsyncApiOperationTraitReference : AsyncApiOperationTrait, IAsyncApiReferenceable + + using System.Diagnostics; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Writers; + + [DebuggerDisplay("{Reference}")] + public class AsyncApiOperationTraitReference : AsyncApiOperationTrait, IAsyncApiReferenceable, IEquatable, IEquatable { private AsyncApiOperationTrait target; @@ -25,12 +29,14 @@ public AsyncApiOperationTraitReference(string reference) this.Reference = new AsyncApiReference(reference, ReferenceType.OperationTrait); } - public override string OperationId { get => this.Target?.OperationId; set => this.Target.OperationId = value; } + public override string Title { get => this.Target?.Title; set => this.Target.Title = value; } public override string Summary { get => this.Target?.Summary; set => this.Target.Summary = value; } public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + public override IList Security { get => this.Target?.Security; set => this.Target.Security = value; } + public override IList Tags { get => this.Target?.Tags; set => this.Target.Tags = value; } public override AsyncApiExternalDocumentation ExternalDocs { get => this.Target?.ExternalDocs; set => this.Target.ExternalDocs = value; } @@ -43,6 +49,48 @@ public AsyncApiOperationTraitReference(string reference) public bool UnresolvedReference { get { return this.Target == null; } } + public static bool operator !=(AsyncApiOperationTraitReference left, AsyncApiOperationTraitReference right) => !(left == right); + + public static bool operator ==(AsyncApiOperationTraitReference left, AsyncApiOperationTraitReference right) + { + return Equals(left, null) ? Equals(right, null) : left.Equals(right); + } + + public bool Equals(AsyncApiOperationTraitReference other) + { + if (other is null) + { + return false; + } + + if (other.Target is AsyncApiOperationTraitReference reference) + { + return this.Equals(reference); + } + + return this.Target == other.Target; + } + + public override bool Equals(object obj) + { + if (obj is AsyncApiOperationTraitReference reference) + { + return this.Equals(reference); + } + + if (obj is AsyncApiOperationTrait message) + { + return this.Equals(message); + } + + return false; + } + + public bool Equals(AsyncApiOperationTrait other) + { + return this.Target == other; + } + public override void SerializeV2(IAsyncApiWriter writer) { if (!writer.GetSettings().ShouldInlineReference(this.Reference)) @@ -56,5 +104,19 @@ public override void SerializeV2(IAsyncApiWriter writer) this.Target.SerializeV2(writer); } } + + public override void SerializeV3(IAsyncApiWriter writer) + { + if (!writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV3(writer); + return; + } + else + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV3(writer); + } + } } } diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiParameterReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiParameterReference.cs index e5860d2..9c5562f 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiParameterReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiParameterReference.cs @@ -1,13 +1,16 @@ namespace ByteBard.AsyncAPI.Models { + using System; using System.Collections.Generic; - using ByteBard.AsyncAPI.Models.Interfaces; - using ByteBard.AsyncAPI.Writers; - /// /// The definition of a parameter this application MAY use. /// - public class AsyncApiParameterReference : AsyncApiParameter, IAsyncApiReferenceable + using System.Diagnostics; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Writers; + + [DebuggerDisplay("{Reference}")] + public class AsyncApiParameterReference : AsyncApiParameter, IAsyncApiReferenceable, IEquatable, IEquatable { private AsyncApiParameter target; @@ -25,9 +28,13 @@ public AsyncApiParameterReference(string reference) this.Reference = new AsyncApiReference(reference, ReferenceType.Parameter); } + public override IList Enum { get => this.Target?.Enum; set => this.Target.Enum = value; } + + public override string Default { get => this.Target?.Default; set => this.Target.Default = value; } + public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } - public override AsyncApiJsonSchema Schema { get => this.Target?.Schema; set => this.Target.Schema = value; } + public override IList Examples { get => this.Target?.Examples; set => this.Target.Examples = value; } public override string Location { get => this.Target?.Location; set => this.Target.Location = value; } @@ -37,6 +44,48 @@ public AsyncApiParameterReference(string reference) public bool UnresolvedReference { get { return this.Target == null; } } + public static bool operator !=(AsyncApiParameterReference left, AsyncApiParameterReference right) => !(left == right); + + public static bool operator ==(AsyncApiParameterReference left, AsyncApiParameterReference right) + { + return Equals(left, null) ? Equals(right, null) : left.Equals(right); + } + + public bool Equals(AsyncApiParameterReference other) + { + if (other is null) + { + return false; + } + + if (other.Target is AsyncApiParameterReference reference) + { + return this.Equals(reference); + } + + return this.Target == other.Target; + } + + public override bool Equals(object obj) + { + if (obj is AsyncApiParameterReference reference) + { + return this.Equals(reference); + } + + if (obj is AsyncApiParameter message) + { + return this.Equals(message); + } + + return false; + } + + public bool Equals(AsyncApiParameter other) + { + return this.Target == other; + } + public override void SerializeV2(IAsyncApiWriter writer) { if (!writer.GetSettings().ShouldInlineReference(this.Reference)) @@ -50,5 +99,19 @@ public override void SerializeV2(IAsyncApiWriter writer) this.Target.SerializeV2(writer); } } + + public override void SerializeV3(IAsyncApiWriter writer) + { + if (!writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV3(writer); + return; + } + else + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV3(writer); + } + } } } diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiSecuritySchemeReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiSecuritySchemeReference.cs index ceddbe0..a1b1053 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiSecuritySchemeReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiSecuritySchemeReference.cs @@ -2,12 +2,12 @@ namespace ByteBard.AsyncAPI.Models { using System; using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; using ByteBard.AsyncAPI.Models.Interfaces; using ByteBard.AsyncAPI.Writers; - /// - /// The definition of a security scheme this application MAY use. - /// + [DebuggerDisplay("{Reference}")] public class AsyncApiSecuritySchemeReference : AsyncApiSecurityScheme, IAsyncApiReferenceable { private AsyncApiSecurityScheme target; @@ -32,7 +32,7 @@ public AsyncApiSecuritySchemeReference(string reference) public override string Name { get => this.Target?.Name; set => this.Target.Name = value; } - public override ParameterLocation In { get => this.Target.In; set => this.Target.In = value; } + public override ParameterLocation? In { get => this.Target?.In; set => this.Target.In = value; } public override string Scheme { get => this.Target?.Scheme; set => this.Target.Scheme = value; } @@ -42,6 +42,8 @@ public AsyncApiSecuritySchemeReference(string reference) public override Uri OpenIdConnectUrl { get => this.Target?.OpenIdConnectUrl; set => this.Target.OpenIdConnectUrl = value; } + public override ISet Scopes { get => this.Target?.Scopes; set => this.Target.Scopes = value; } + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } public AsyncApiReference Reference { get; set; } @@ -61,5 +63,47 @@ public override void SerializeV2(IAsyncApiWriter writer) this.Target.SerializeV2(writer); } } + + public override void SerializeV3(IAsyncApiWriter writer) + { + if (!writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV3(writer); + return; + } + else + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV3(writer); + } + } + + public void SerializeAsSecurityRequirement(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + this.Reference.Workspace = writer.Workspace; + + writer.WriteStartObject(); + + writer.WritePropertyName(this.Reference.FragmentId.Split("/")[^1]); + + writer.WriteStartArray(); + + if (this.Scopes.Any()) + { + foreach (var scope in this.Scopes) + { + writer.WriteValue(scope); + } + } + + writer.WriteEndArray(); + + writer.WriteEndObject(); + } } } diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiServerReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiServerReference.cs index e1d9ddd..50d8b8b 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiServerReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiServerReference.cs @@ -1,13 +1,16 @@ namespace ByteBard.AsyncAPI.Models { + using System; using System.Collections.Generic; - using ByteBard.AsyncAPI.Models.Interfaces; - using ByteBard.AsyncAPI.Writers; - /// /// The definition of a server this application MAY connect to. /// - public class AsyncApiServerReference : AsyncApiServer, IAsyncApiReferenceable + using System.Diagnostics; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Writers; + + [DebuggerDisplay("{Reference}")] + public class AsyncApiServerReference : AsyncApiServer, IAsyncApiReferenceable, IEquatable, IEquatable { private AsyncApiServer target; @@ -25,7 +28,9 @@ public AsyncApiServerReference(string reference) this.Reference = new AsyncApiReference(reference, ReferenceType.Server); } - public override string Url { get => this.Target?.Url; set => this.Target.Url = value; } + public override string Host { get => this.Target?.Host; set => this.Target.Host = value; } + + public override string PathName { get => this.Target?.PathName; set => this.Target.PathName = value; } public override string Protocol { get => this.Target?.Protocol; set => this.Target.Protocol = value; } @@ -33,9 +38,13 @@ public AsyncApiServerReference(string reference) public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + public override string Title { get => this.Target?.Title; set => this.Target.Title = value; } + + public override string Summary { get => this.Target?.Summary; set => this.Target.Summary = value; } + public override IDictionary Variables { get => this.Target?.Variables; set => this.Target.Variables = value; } - public override IList Security { get => this.Target?.Security; set => this.Target.Security = value; } + public override IList Security { get => this.Target?.Security; set => this.Target.Security = value; } public override IList Tags { get => this.Target?.Tags; set => this.Target.Tags = value; } @@ -47,6 +56,48 @@ public AsyncApiServerReference(string reference) public bool UnresolvedReference { get { return this.Target == null; } } + public static bool operator !=(AsyncApiServerReference left, AsyncApiServerReference right) => !(left == right); + + public static bool operator ==(AsyncApiServerReference left, AsyncApiServerReference right) + { + return Equals(left, null) ? Equals(right, null) : left.Equals(right); + } + + public bool Equals(AsyncApiServerReference other) + { + if (other is null) + { + return false; + } + + if (other.Target is AsyncApiServerReference reference) + { + return this.Equals(reference); + } + + return this.Target == other.Target; + } + + public override bool Equals(object obj) + { + if (obj is AsyncApiServerReference reference) + { + return this.Equals(reference); + } + + if (obj is AsyncApiServer message) + { + return this.Equals(message); + } + + return false; + } + + public bool Equals(AsyncApiServer other) + { + return this.Target == other; + } + public override void SerializeV2(IAsyncApiWriter writer) { if (!writer.GetSettings().ShouldInlineReference(this.Reference)) @@ -60,5 +111,19 @@ public override void SerializeV2(IAsyncApiWriter writer) this.Target.SerializeV2(writer); } } + + public override void SerializeV3(IAsyncApiWriter writer) + { + if (!writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV3(writer); + return; + } + else + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV3(writer); + } + } } } \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiServerVariableReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiServerVariableReference.cs index 5499c2f..c625ff5 100644 --- a/src/ByteBard.AsyncAPI/Models/References/AsyncApiServerVariableReference.cs +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiServerVariableReference.cs @@ -1,13 +1,16 @@ namespace ByteBard.AsyncAPI.Models { + using System; using System.Collections.Generic; - using ByteBard.AsyncAPI.Models.Interfaces; - using ByteBard.AsyncAPI.Writers; - /// /// The definition of a server variable this application MAY use. /// - public class AsyncApiServerVariableReference : AsyncApiServerVariable, IAsyncApiReferenceable + using System.Diagnostics; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Writers; + + [DebuggerDisplay("{Reference}")] + public class AsyncApiServerVariableReference : AsyncApiServerVariable, IAsyncApiReferenceable, IEquatable, IEquatable { private AsyncApiServerVariable target; @@ -24,6 +27,7 @@ public AsyncApiServerVariableReference(string reference) { this.Reference = new AsyncApiReference(reference, ReferenceType.ServerVariable); } + public override IList Enum { get => this.Target?.Enum; set => this.Target.Enum = value; } public override string Default { get => this.Target?.Default; set => this.Target.Default = value; } @@ -38,6 +42,48 @@ public AsyncApiServerVariableReference(string reference) public bool UnresolvedReference { get { return this.Target == null; } } + public static bool operator !=(AsyncApiServerVariableReference left, AsyncApiServerVariableReference right) => !(left == right); + + public static bool operator ==(AsyncApiServerVariableReference left, AsyncApiServerVariableReference right) + { + return Equals(left, null) ? Equals(right, null) : left.Equals(right); + } + + public bool Equals(AsyncApiServerVariableReference other) + { + if (other is null) + { + return false; + } + + if (other.Target is AsyncApiServerVariableReference reference) + { + return this.Equals(reference); + } + + return this.Target == other.Target; + } + + public override bool Equals(object obj) + { + if (obj is AsyncApiServerVariableReference reference) + { + return this.Equals(reference); + } + + if (obj is AsyncApiServerVariable message) + { + return this.Equals(message); + } + + return false; + } + + public bool Equals(AsyncApiServerVariable other) + { + return this.Target == other; + } + public override void SerializeV2(IAsyncApiWriter writer) { if (!writer.GetSettings().ShouldInlineReference(this.Reference)) @@ -51,5 +97,19 @@ public override void SerializeV2(IAsyncApiWriter writer) this.Target.SerializeV2(writer); } } + + public override void SerializeV3(IAsyncApiWriter writer) + { + if (!writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV3(writer); + return; + } + else + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV3(writer); + } + } } } diff --git a/src/ByteBard.AsyncAPI/Models/References/AsyncApiTagReference.cs b/src/ByteBard.AsyncAPI/Models/References/AsyncApiTagReference.cs new file mode 100644 index 0000000..1b35c04 --- /dev/null +++ b/src/ByteBard.AsyncAPI/Models/References/AsyncApiTagReference.cs @@ -0,0 +1,117 @@ +namespace ByteBard.AsyncAPI.Models +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Writers; + + [DebuggerDisplay("{Reference}")] + public class AsyncApiTagReference : AsyncApiTag, IAsyncApiReferenceable, IEquatable, IEquatable + { + private AsyncApiTag target; + + private AsyncApiTag Target + { + get + { + this.target ??= this.Reference.Workspace?.ResolveReference(this.Reference.Reference); + return this.target; + } + } + + public AsyncApiTagReference(string reference) + { + this.Reference = new AsyncApiReference(reference, ReferenceType.Tag); + } + + public override string Name { get => this.Target?.Name; set => this.Target.Name = value; } + + public override string Description { get => this.Target?.Description; set => this.Target.Description = value; } + + public override AsyncApiExternalDocumentation ExternalDocs { get => this.Target?.ExternalDocs; set => this.Target.ExternalDocs = value; } + + public override IDictionary Extensions { get => this.Target?.Extensions; set => this.Target.Extensions = value; } + + public AsyncApiReference Reference { get; set; } + + public bool UnresolvedReference { get { return this.Target == null; } } + + public static bool operator !=(AsyncApiTagReference left, AsyncApiTagReference right) => !(left == right); + + public static bool operator ==(AsyncApiTagReference left, AsyncApiTagReference right) + { + return Equals(left, null) ? Equals(right, null) : left.Equals(right); + } + + public bool Equals(AsyncApiTagReference other) + { + if (other is null) + { + return false; + } + + if (other.Target is AsyncApiTagReference reference) + { + return this.Equals(reference); + } + + return this.Target == other.Target; + } + + public override bool Equals(object obj) + { + if (obj is AsyncApiTagReference reference) + { + return this.Equals(reference); + } + + if (obj is AsyncApiTag message) + { + return this.Equals(message); + } + + return false; + } + + public bool Equals(AsyncApiTag other) + { + return this.Target == other; + } + + /// + /// Serializes the v2. + /// + /// + /// If serialization of the referenced tag will be skipped. + /// + /// The writer. + public override void SerializeV2(IAsyncApiWriter writer) + { + if (!writer.GetSettings().ShouldInlineReference(this.Reference)) + { + // We cannot serialize the Tag as a reference under V2. + return; + } + else + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV2(writer); + } + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + if (!writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV3(writer); + return; + } + else + { + this.Reference.Workspace = writer.Workspace; + this.Target.SerializeV3(writer); + } + } + } +} diff --git a/src/ByteBard.AsyncAPI/Resource.Designer.cs b/src/ByteBard.AsyncAPI/Resource.Designer.cs index a1de5ee..5b922ad 100644 --- a/src/ByteBard.AsyncAPI/Resource.Designer.cs +++ b/src/ByteBard.AsyncAPI/Resource.Designer.cs @@ -123,6 +123,24 @@ internal static string Validation_NameMustMatchRegularExpr { } } + /// + /// Looks up a localized string similar to The messages of operation '{0}' MUST be a subset of the referenced channels messages.. + /// + internal static string Validation_OperationMessagesMustReferenceOperationChannel { + get { + return ResourceManager.GetString("Validation_OperationMessagesMustReferenceOperationChannel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The operation '{0}' MUST point to a channel definition located in the root Channels Object.. + /// + internal static string Validation_OperationMustReferenceValidChannel { + get { + return ResourceManager.GetString("Validation_OperationMustReferenceValidChannel", resourceCulture); + } + } + /// /// Looks up a localized string similar to Symbols MUST match the regular expression '{1}'.. /// diff --git a/src/ByteBard.AsyncAPI/Resource.resx b/src/ByteBard.AsyncAPI/Resource.resx index a97d593..59ef3ca 100644 --- a/src/ByteBard.AsyncAPI/Resource.resx +++ b/src/ByteBard.AsyncAPI/Resource.resx @@ -126,6 +126,9 @@ The extension name '{0}' in '{1}' object MUST being with 'x-'. + + The operation '{0}' MUST point to a channel definition located in the root Channels Object. + The field '{0}' in '{1}' object is REQUIRED. @@ -141,4 +144,7 @@ Symbols MUST match the regular expression '{1}'. + + The messages of operation '{0}' MUST be a subset of the referenced channels messages. + \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Services/AsyncApiVisitorBase.cs b/src/ByteBard.AsyncAPI/Services/AsyncApiVisitorBase.cs index 348cbf2..d57edc2 100644 --- a/src/ByteBard.AsyncAPI/Services/AsyncApiVisitorBase.cs +++ b/src/ByteBard.AsyncAPI/Services/AsyncApiVisitorBase.cs @@ -53,7 +53,7 @@ public virtual void Visit(AsyncApiDocument doc) { } - public virtual void Visit(IAsyncApiMessagePayload payload) + public virtual void Visit(IAsyncApiSchema payload) { } @@ -69,10 +69,18 @@ public virtual void Visit(IDictionary anys) { } + public virtual void Visit(IDictionary anys) + { + } + public virtual void Visit(IList traits) { } + public virtual void Visit(IList traits) + { + } + /// /// Visits . /// @@ -154,10 +162,22 @@ public virtual void Visit(AsyncApiExternalDocumentation externalDocs) { } + public virtual void Visit(AsyncApiOperationReply reply) + { + } + + public virtual void Visit(AsyncApiOperationReplyAddress replyAddress) + { + } + public virtual void Visit(AsyncApiMessage message) { } + public virtual void Visit(AsyncApiMultiFormatSchema multiFormatSchema) + { + } + public virtual void Visit(IList messages) { } @@ -176,13 +196,6 @@ public virtual void Visit(AsyncApiOAuthFlow asyncApiOAuthFlow) { } - /// - /// Visits . - /// - public virtual void Visit(AsyncApiSecurityRequirement securityRequirement) - { - } - /// /// Visits . /// @@ -197,13 +210,6 @@ public virtual void Visit(IList asyncApiTags) { } - /// - /// Visits list of . - /// - public virtual void Visit(IList asyncApiSecurityRequirements) - { - } - /// /// Visits . /// @@ -300,5 +306,9 @@ public virtual void Visit(AsyncApiMessageExample messageExample) public virtual void Visit(IList messageExamples) { } + + public virtual void Visit(IDictionary messages) + { + } } } diff --git a/src/ByteBard.AsyncAPI/Services/AsyncApiWalker.cs b/src/ByteBard.AsyncAPI/Services/AsyncApiWalker.cs index 3d89eef..fbb7271 100644 --- a/src/ByteBard.AsyncAPI/Services/AsyncApiWalker.cs +++ b/src/ByteBard.AsyncAPI/Services/AsyncApiWalker.cs @@ -29,9 +29,8 @@ public void Walk(AsyncApiDocument doc) this.Walk(AsyncApiConstants.Info, () => this.Walk(doc.Info)); this.Walk(AsyncApiConstants.Servers, () => this.Walk(doc.Servers)); this.Walk(AsyncApiConstants.Channels, () => this.Walk(doc.Channels)); + this.Walk(AsyncApiConstants.Operations, () => this.Walk(doc.Operations)); this.Walk(AsyncApiConstants.Components, () => this.Walk(doc.Components)); - this.Walk(AsyncApiConstants.Tags, () => this.Walk(doc.Tags)); - this.Walk(AsyncApiConstants.ExternalDocs, () => this.Walk(doc.ExternalDocs)); this.Walk(doc as IAsyncApiExtensible); } @@ -55,7 +54,7 @@ internal void Walk(AsyncApiComponents components) { foreach (var item in components.Schemas) { - this.Walk(item.Key, () => this.Walk(item.Value, isComponent: true)); + this.Walk(item.Key, () => this.Walk(item.Value)); } } }); @@ -66,7 +65,7 @@ internal void Walk(AsyncApiComponents components) { foreach (var item in components.Channels) { - this.Walk(item.Key, () => this.Walk(item.Value, isComponent: true)); + this.Walk(item.Key, () => this.Walk(item.Value)); } } }); @@ -77,7 +76,7 @@ internal void Walk(AsyncApiComponents components) { foreach (var item in components.ServerBindings) { - this.Walk(item.Key, () => this.Walk(item.Value, isComponent: true)); + this.Walk(item.Key, () => this.Walk(item.Value)); } } }); @@ -88,7 +87,7 @@ internal void Walk(AsyncApiComponents components) { foreach (var item in components.ChannelBindings) { - this.Walk(item.Key, () => this.Walk(item.Value, isComponent: true)); + this.Walk(item.Key, () => this.Walk(item.Value)); } } }); @@ -99,7 +98,7 @@ internal void Walk(AsyncApiComponents components) { foreach (var item in components.OperationBindings) { - this.Walk(item.Key, () => this.Walk(item.Value, isComponent: true)); + this.Walk(item.Key, () => this.Walk(item.Value)); } } }); @@ -110,7 +109,7 @@ internal void Walk(AsyncApiComponents components) { foreach (var item in components.MessageBindings) { - this.Walk(item.Key, () => this.Walk(item.Value, isComponent: true)); + this.Walk(item.Key, () => this.Walk(item.Value)); } } }); @@ -121,7 +120,7 @@ internal void Walk(AsyncApiComponents components) { foreach (var item in components.Parameters) { - this.Walk(item.Key, () => this.Walk(item.Value, isComponent: true)); + this.Walk(item.Key, () => this.Walk(item.Value)); } } }); @@ -132,7 +131,7 @@ internal void Walk(AsyncApiComponents components) { foreach (var item in components.Messages) { - this.Walk(item.Key, () => this.Walk(item.Value, isComponent: true)); + this.Walk(item.Key, () => this.Walk(item.Value)); } } }); @@ -143,7 +142,7 @@ internal void Walk(AsyncApiComponents components) { foreach (var item in components.Servers) { - this.Walk(item.Key, () => this.Walk(item.Value, isComponent: true)); + this.Walk(item.Key, () => this.Walk(item.Value)); } } }); @@ -154,7 +153,7 @@ internal void Walk(AsyncApiComponents components) { foreach (var item in components.CorrelationIds) { - this.Walk(item.Key, () => this.Walk(item.Value, isComponent: true)); + this.Walk(item.Key, () => this.Walk(item.Value)); } } }); @@ -165,7 +164,7 @@ internal void Walk(AsyncApiComponents components) { foreach (var item in components.MessageTraits) { - this.Walk(item.Key, () => this.Walk(item.Value, isComponent: true)); + this.Walk(item.Key, () => this.Walk(item.Value)); } } }); @@ -176,7 +175,7 @@ internal void Walk(AsyncApiComponents components) { foreach (var item in components.OperationTraits) { - this.Walk(item.Key, () => this.Walk(item.Value, isComponent: true)); + this.Walk(item.Key, () => this.Walk(item.Value)); } } }); @@ -187,7 +186,7 @@ internal void Walk(AsyncApiComponents components) { foreach (var item in components.SecuritySchemes) { - this.Walk(item.Key, () => this.Walk(item.Value, isComponent: true)); + this.Walk(item.Key, () => this.Walk(item.Value)); } } }); @@ -217,8 +216,13 @@ internal void Walk(AsyncApiOAuthFlow oAuthFlow) this.Walk(oAuthFlow as IAsyncApiExtensible); } - internal void Walk(AsyncApiSecurityScheme securityScheme, bool isComponent = false) + internal void Walk(AsyncApiSecurityScheme securityScheme) { + if (securityScheme is null) + { + return; + } + if (securityScheme is AsyncApiSecuritySchemeReference) { this.Walk(securityScheme as IAsyncApiReferenceable); @@ -227,11 +231,7 @@ internal void Walk(AsyncApiSecurityScheme securityScheme, bool isComponent = fal this.visitor.Visit(securityScheme); - if (securityScheme != null) - { - this.Walk(AsyncApiConstants.Flows, () => this.Walk(securityScheme.Flows)); - } - + this.Walk(AsyncApiConstants.Flows, () => this.Walk(securityScheme.Flows)); this.Walk(securityScheme as IAsyncApiExtensible); } @@ -262,31 +262,86 @@ internal void Walk(AsyncApiExternalDocumentation externalDocs) return; } + if (externalDocs is AsyncApiExternalDocumentationReference) + { + this.Walk(externalDocs as IAsyncApiReferenceable); + return; + } + this.visitor.Visit(externalDocs); this.Walk(externalDocs as IAsyncApiExtensible); } - internal void Walk(AsyncApiChannel channel, bool isComponent = false) + internal void Walk(AsyncApiChannel channel) { if (channel == null) { return; } + if (channel is AsyncApiChannelReference) { this.Walk(channel as IAsyncApiReferenceable); return; } - this.Walk(AsyncApiConstants.Subscribe, () => this.Walk(channel.Subscribe)); - this.Walk(AsyncApiConstants.Publish, () => this.Walk(channel.Publish)); - - this.Walk(AsyncApiConstants.Bindings, () => this.Walk(channel.Bindings)); + this.Walk(AsyncApiConstants.Messages, () => this.Walk(channel.Messages)); + this.Walk(AsyncApiConstants.Servers, () => this.Walk(channel.Servers)); this.Walk(AsyncApiConstants.Parameters, () => this.Walk(channel.Parameters)); + this.Walk(AsyncApiConstants.Tags, () => this.Walk(channel.Tags)); + this.Walk(AsyncApiConstants.ExternalDocs, () => this.Walk(channel.ExternalDocs)); + this.Walk(AsyncApiConstants.Bindings, () => this.Walk(channel.Bindings)); this.Walk(channel as IAsyncApiExtensible); } + private void Walk(IList serverReferences) + { + if (serverReferences == null) + { + return; + } + + foreach (var serverReference in serverReferences) + { + this.Walk(serverReference as IAsyncApiReferenceable); + } + } + + private void Walk(IDictionary messages) + { + if (messages == null) + { + return; + } + + this.visitor.Visit(messages); + + foreach (var message in messages) + { + this.visitor.CurrentKeys.Message = message.Key; + this.Walk(message.Key, () => this.Walk(message.Value)); + this.visitor.CurrentKeys.Message = null; + } + } + + private void Walk(IDictionary operations) + { + if (operations == null) + { + return; + } + + this.visitor.Visit(operations); + + foreach (var operation in operations) + { + this.visitor.CurrentKeys.Operation = operation.Key; + this.Walk(operation.Key, () => this.Walk(operation.Value)); + this.visitor.CurrentKeys.Operation = null; + } + } + private void Walk(IDictionary parameters) { if (parameters == null) @@ -307,7 +362,7 @@ private void Walk(IDictionary parameters) } } - internal void Walk(AsyncApiParameter parameter, bool isComponent = false) + internal void Walk(AsyncApiParameter parameter) { if (parameter is AsyncApiParameterReference reference) { @@ -316,18 +371,13 @@ internal void Walk(AsyncApiParameter parameter, bool isComponent = false) } this.visitor.Visit(parameter); - - if (parameter != null) - { - this.Walk(AsyncApiConstants.Schema, () => this.Walk(parameter.Schema)); - } - this.Walk(parameter as IAsyncApiExtensible); } - internal void Walk(IAsyncApiMessagePayload payload) + internal void Walk(IAsyncApiSchema payload) { this.visitor.Visit(payload); + if (payload is AsyncApiJsonSchema jsonSchema) { this.Walk(AsyncApiConstants.Payload, () => this.Walk((AsyncApiJsonSchema)payload)); @@ -350,7 +400,7 @@ internal void Walk(AsyncApiAvroSchema schema) this.visitor.Visit(schema); } - internal void Walk(AsyncApiJsonSchema schema, bool isComponent = false) + internal void Walk(AsyncApiJsonSchema schema) { if (schema == null) { @@ -505,15 +555,66 @@ internal void Walk(AsyncApiOperation operation) if (operation != null) { this.Walk(AsyncApiConstants.Tags, () => this.Walk(operation.Tags)); + this.Walk(AsyncApiConstants.Channel, () => this.Walk(operation.Channel as IAsyncApiReferenceable)); this.Walk(AsyncApiConstants.ExternalDocs, () => this.Walk(operation.ExternalDocs)); this.Walk(AsyncApiConstants.Traits, () => this.Walk(operation.Traits)); - this.Walk(AsyncApiConstants.Message, () => this.Walk(operation.Message)); + foreach (var message in operation.Messages) + { + this.Walk(message as IAsyncApiReferenceable); + } + + this.Walk(operation.Security); + this.Walk(AsyncApiConstants.Bindings, () => this.Walk(operation.Bindings)); + this.Walk(AsyncApiConstants.Reply, () => this.Walk(operation.Reply)); } this.Walk(operation as IAsyncApiExtensible); } + private void Walk(AsyncApiOperationReply reply) + { + if (reply == null) + { + return; + } + + if (reply is AsyncApiOperationReplyReference reference) + { + this.Walk(reference as IAsyncApiReferenceable); + return; + } + + this.visitor.Visit(reply); + + this.Walk(reply.Address); + this.Walk(reply.Channel as IAsyncApiReferenceable); + + foreach (var message in reply.Messages) + { + this.Walk(message as IAsyncApiReferenceable); + } + + this.Walk(reply as IAsyncApiExtensible); + } + + private void Walk(AsyncApiOperationReplyAddress replyAddress) + { + if (replyAddress == null) + { + return; + } + + if (replyAddress is AsyncApiOperationReplyAddressReference reference) + { + this.Walk(reference as IAsyncApiReferenceable); + return; + } + + this.visitor.Visit(replyAddress); + this.Walk(replyAddress as IAsyncApiExtensible); + } + private void Walk(IList traits) { if (traits == null) @@ -533,7 +634,7 @@ private void Walk(IList traits) } } - internal void Walk(AsyncApiOperationTrait trait, bool isComponent = false) + internal void Walk(AsyncApiOperationTrait trait) { if (trait is AsyncApiOperationTraitReference reference) { @@ -553,7 +654,7 @@ internal void Walk(AsyncApiOperationTrait trait, bool isComponent = false) this.Walk(trait as IAsyncApiExtensible); } - internal void Walk(AsyncApiMessage message, bool isComponent = false) + internal void Walk(AsyncApiMessage message) { if (message is AsyncApiMessageReference reference) { @@ -566,7 +667,7 @@ internal void Walk(AsyncApiMessage message, bool isComponent = false) if (message != null) { this.Walk(AsyncApiConstants.Headers, () => this.Walk(message.Headers)); - this.Walk(message.Payload); + this.Walk(AsyncApiConstants.Payload, () => this.Walk(message.Payload)); this.Walk(AsyncApiConstants.CorrelationId, () => this.Walk(message.CorrelationId)); this.Walk(AsyncApiConstants.Tags, () => this.Walk(message.Tags)); this.Walk(AsyncApiConstants.Examples, () => this.Walk(message.Examples)); @@ -597,7 +698,7 @@ private void Walk(IList traits) } } - internal void Walk(AsyncApiMessageTrait trait, bool isComponent = false) + internal void Walk(AsyncApiMessageTrait trait) { if (trait is AsyncApiMessageTraitReference reference) { @@ -620,7 +721,25 @@ internal void Walk(AsyncApiMessageTrait trait, bool isComponent = false) this.Walk(trait as IAsyncApiExtensible); } - internal void Walk(AsyncApiBindings serverBindings, bool isComponent = false) + internal void Walk(AsyncApiMultiFormatSchema multiFormatSchema) + { + if (multiFormatSchema == null) + { + return; + } + + if (multiFormatSchema is AsyncApiMultiFormatSchemaReference reference) + { + this.Walk(multiFormatSchema as IAsyncApiReferenceable); + return; + } + + this.visitor.Visit(multiFormatSchema); + this.Walk(multiFormatSchema.Schema); + this.Walk(multiFormatSchema as IAsyncApiExtensible); + } + + internal void Walk(AsyncApiBindings serverBindings) { if (serverBindings is AsyncApiBindingsReference reference) { @@ -638,6 +757,8 @@ internal void Walk(AsyncApiBindings serverBindings, bool isCompo this.visitor.CurrentKeys.ServerBinding = null; } } + + this.Walk(serverBindings as IAsyncApiExtensible); } internal void Walk(IServerBinding binding) @@ -650,7 +771,7 @@ internal void Walk(IServerBinding binding) this.visitor.Visit(binding); } - internal void Walk(AsyncApiBindings channelBindings, bool isComponent = false) + internal void Walk(AsyncApiBindings channelBindings) { if (channelBindings is AsyncApiBindingsReference reference) { @@ -668,6 +789,8 @@ internal void Walk(AsyncApiBindings channelBindings, bool isCom this.visitor.CurrentKeys.ChannelBinding = null; } } + + this.Walk(channelBindings as IAsyncApiExtensible); } internal void Walk(IChannelBinding binding) @@ -680,7 +803,7 @@ internal void Walk(IChannelBinding binding) this.visitor.Visit(binding); } - internal void Walk(AsyncApiBindings operationBindings, bool isComponent = false) + internal void Walk(AsyncApiBindings operationBindings) { if (operationBindings is AsyncApiBindingsReference reference) { @@ -698,6 +821,8 @@ internal void Walk(AsyncApiBindings operationBindings, bool i this.visitor.CurrentKeys.OperationBinding = null; } } + + this.Walk(operationBindings as IAsyncApiExtensible); } internal void Walk(IOperationBinding binding) @@ -710,7 +835,7 @@ internal void Walk(IOperationBinding binding) this.visitor.Visit(binding); } - internal void Walk(AsyncApiBindings messageBindings, bool isComponent = false) + internal void Walk(AsyncApiBindings messageBindings) { if (messageBindings is AsyncApiBindingsReference reference) { @@ -728,6 +853,8 @@ internal void Walk(AsyncApiBindings messageBindings, bool isCom this.visitor.CurrentKeys.MessageBinding = null; } } + + this.Walk(messageBindings as IAsyncApiExtensible); } internal void Walk(IMessageBinding binding) @@ -796,7 +923,7 @@ internal void Walk(IDictionary anys) } } - internal void Walk(AsyncApiCorrelationId correlationId, bool isComponent = false) + internal void Walk(AsyncApiCorrelationId correlationId) { if (correlationId is AsyncApiCorrelationIdReference) { @@ -816,6 +943,12 @@ internal void Walk(AsyncApiTag tag) return; } + if (tag is AsyncApiTagReference) + { + this.Walk(tag as IAsyncApiReferenceable); + return; + } + this.visitor.Visit(tag); this.visitor.Visit(tag.ExternalDocs); this.visitor.Visit(tag as IAsyncApiExtensible); @@ -853,12 +986,14 @@ internal void Walk(AsyncApiInfo info) { this.Walk(AsyncApiConstants.Contact, () => this.Walk(info.Contact)); this.Walk(AsyncApiConstants.License, () => this.Walk(info.License)); + this.Walk(AsyncApiConstants.Tags, () => this.Walk(info.Tags)); + this.Walk(AsyncApiConstants.ExternalDocs, () => this.Walk(info.ExternalDocs)); } this.Walk(info as IAsyncApiExtensible); } - internal void Walk(AsyncApiServer server, bool isComponent = false) + internal void Walk(AsyncApiServer server) { if (server is AsyncApiServerReference) { @@ -873,41 +1008,25 @@ internal void Walk(AsyncApiServer server, bool isComponent = false) this.visitor.Visit(server as IAsyncApiExtensible); } - internal void Walk(IList securityRequirements) + internal void Walk(IList securitySchemes) { - if (securityRequirements == null) + if (securitySchemes == null) { return; } - this.visitor.Visit(securityRequirements); + this.visitor.Visit(securitySchemes); - // Visit Examples - if (securityRequirements != null) + // Visit traits + if (securitySchemes != null) { - for (int i = 0; i < securityRequirements.Count; i++) + for (int i = 0; i < securitySchemes.Count; i++) { - this.Walk(i.ToString(), () => this.Walk(securityRequirements[i])); + this.Walk(i.ToString(), () => this.Walk(securitySchemes[i])); } } } - internal void Walk(AsyncApiSecurityRequirement securityRequirement) - { - if (securityRequirement is null) - { - return; - } - - this.visitor.Visit(securityRequirement); - foreach (var item in securityRequirement.Keys) - { - this.Walk(item as IAsyncApiReferenceable); - } - - this.Walk(securityRequirement as IAsyncApiExtensible); - } - internal void Walk(IList messages) { if (messages == null) @@ -986,6 +1105,7 @@ internal void Walk(AsyncApiLicense license) } this.visitor.Visit(license); + this.Walk(license as IAsyncApiExtensible); } internal void Walk(AsyncApiContact contact) @@ -996,6 +1116,7 @@ internal void Walk(AsyncApiContact contact) } this.visitor.Visit(contact); + this.Walk(contact as IAsyncApiExtensible); } internal void Walk(AsyncApiAny any) @@ -1045,17 +1166,6 @@ internal void Walk(IAsyncApiExtension extension) this.visitor.Visit(extension); } - private bool ProcessAsReference(IAsyncApiReferenceable referenceable, bool isComponent = false) - { - var isReference = referenceable.Reference != null && !isComponent; - if (isReference) - { - this.Walk(referenceable); - } - - return isReference; - } - internal void Walk(IAsyncApiReferenceable referenceable) { this.visitor.Visit(referenceable); @@ -1088,7 +1198,6 @@ public void Walk(IAsyncApiElement element) case AsyncApiParameter e: this.Walk(e); break; case AsyncApiJsonSchema e: this.Walk(e); break; case AsyncApiAvroSchema e: this.Walk(e); break; - case AsyncApiSecurityRequirement e: this.Walk(e); break; case AsyncApiSecurityScheme e: this.Walk(e); break; case AsyncApiServer e: this.Walk(e); break; case AsyncApiServerVariable e: this.Walk(e); break; diff --git a/src/ByteBard.AsyncAPI/Services/CurrentKeys.cs b/src/ByteBard.AsyncAPI/Services/CurrentKeys.cs index 3c7f4b7..832bca2 100644 --- a/src/ByteBard.AsyncAPI/Services/CurrentKeys.cs +++ b/src/ByteBard.AsyncAPI/Services/CurrentKeys.cs @@ -23,5 +23,6 @@ public class CurrentKeys public string Parameter { get; set; } public string Message { get; set; } + public string Operation { get; set; } } } diff --git a/src/ByteBard.AsyncAPI/Validation/AsyncApiValidator.cs b/src/ByteBard.AsyncAPI/Validation/AsyncApiValidator.cs index 56ad3f3..a34f996 100644 --- a/src/ByteBard.AsyncAPI/Validation/AsyncApiValidator.cs +++ b/src/ByteBard.AsyncAPI/Validation/AsyncApiValidator.cs @@ -19,11 +19,14 @@ public class AsyncApiValidator : AsyncApiVisitorBase, IValidationContext /// Create a vistor that will validate an AsyncApiDocument. /// /// - public AsyncApiValidator(ValidationRuleSet ruleSet) + public AsyncApiValidator(ValidationRuleSet ruleSet, AsyncApiDocument rootDocument = null) { this.ruleSet = ruleSet; + this.RootDocument = rootDocument; } + public AsyncApiDocument RootDocument { get; } + /// /// Gets the validation errors. /// @@ -130,7 +133,7 @@ public void AddWarning(AsyncApiValidatorWarning warning) public override void Visit(AsyncApiAvroSchema item) => this.Validate(item); - public override void Visit(IAsyncApiMessagePayload item) => this.Validate(item); + public override void Visit(IAsyncApiSchema item) => this.Validate(item); /// /// Execute validation rules against an . @@ -138,6 +141,12 @@ public void AddWarning(AsyncApiValidatorWarning warning) /// The object to be validated. public override void Visit(AsyncApiServer item) => this.Validate(item); + /// + /// Execute validation rules against an . + /// + /// The object to be validated. + public override void Visit(AsyncApiOperation item) => this.Validate(item); + public override void Visit(IServerBinding item) => this.Validate(item); public override void Visit(IChannelBinding item) => this.Validate(item); diff --git a/src/ByteBard.AsyncAPI/Validation/IValidationContext.cs b/src/ByteBard.AsyncAPI/Validation/IValidationContext.cs index c610c9c..a9a4f1f 100644 --- a/src/ByteBard.AsyncAPI/Validation/IValidationContext.cs +++ b/src/ByteBard.AsyncAPI/Validation/IValidationContext.cs @@ -1,3 +1,5 @@ +using ByteBard.AsyncAPI.Models; + namespace ByteBard.AsyncAPI.Validations { /// @@ -32,5 +34,10 @@ public interface IValidationContext /// Pointer to source of validation error in document. /// string PathString { get; } + + /// + /// The root document. + /// + AsyncApiDocument RootDocument { get; } } } \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiAvroRules.cs b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiAvroRules.cs index bfc83c4..78cf1c3 100644 --- a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiAvroRules.cs +++ b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiAvroRules.cs @@ -14,8 +14,8 @@ public static class AsyncApiMessagePayloadRules /// public static Regex NameRegex = new Regex(@"^[A-Za-z_][A-Za-z0-9_]*$"); - public static ValidationRule NameRegularExpression => - new ValidationRule( + public static ValidationRule NameRegularExpression => + new ValidationRule( (context, messagePayload) => { string name = null; diff --git a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiComponentsRules.cs b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiComponentsRules.cs index 2ae6ca5..380ef42 100644 --- a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiComponentsRules.cs +++ b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiComponentsRules.cs @@ -11,23 +11,26 @@ public static class AsyncApiComponentsRules /// /// The key regex. /// - public static Regex KeyRegex = new Regex(@"^[a-zA-Z0-9\.\-_]+$"); + public static Regex KeyRegex = new Regex(@"^[\w\d\.\-_]+$"); /// /// All the fixed fields declared above are objects - /// that MUST use keys that match the regular expression: ^[a-zA-Z0-9\.\-_]+$. + /// that MUST use keys that match the regular expression: ^[\w\d\.\-_]+$. /// public static ValidationRule KeyMustBeRegularExpression => new ValidationRule( (context, components) => { ValidateKeys(context, components.Channels?.Keys, "channels"); - ValidateKeys(context, components.ChannelBindings?.Keys, "channelbindings"); + ValidateKeys(context, components.ChannelBindings?.Keys, "channelBindings"); ValidateKeys(context, components.CorrelationIds?.Keys, "correlationIds"); + ValidateKeys(context, components.Replies?.Keys, "replies"); + ValidateKeys(context, components.ReplyAddresses?.Keys, "replyAddresses"); ValidateKeys(context, components.MessageBindings?.Keys, "messageBindings"); ValidateKeys(context, components.Messages?.Keys, "messages"); ValidateKeys(context, components.MessageTraits?.Keys, "messageTraits"); ValidateKeys(context, components.OperationBindings?.Keys, "operationBindings"); + ValidateKeys(context, components.Operations?.Keys, "operations"); ValidateKeys(context, components.OperationTraits?.Keys, "operationTraits"); ValidateKeys(context, components.Parameters?.Keys, "parameters"); ValidateKeys(context, components.Schemas?.Keys, "schemas"); @@ -35,6 +38,8 @@ public static class AsyncApiComponentsRules ValidateKeys(context, components.ServerBindings?.Keys, "serverBindings"); ValidateKeys(context, components.Servers?.Keys, "servers"); ValidateKeys(context, components.ServerVariables?.Keys, "serverVariables"); + ValidateKeys(context, components.ExternalDocs?.Keys, "externalDocs"); + ValidateKeys(context, components.Tags?.Keys, "tags"); }); private static void ValidateKeys(IValidationContext context, IEnumerable keys, string component) diff --git a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs index 24ab2e3..f1f8f4f 100644 --- a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs +++ b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs @@ -2,7 +2,6 @@ { using System; using System.Collections.Generic; - using System.Linq; using System.Text.RegularExpressions; using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Validations; @@ -15,8 +14,7 @@ public static class AsyncApiDocumentRules /// /// The key regex. /// - public static Regex KeyRegex = new Regex(@"^[a-zA-Z0-9\.\-_]+$", RegexOptions.None, RegexTimeout); - public static Regex ChannelKeyUriTemplateRegex = new Regex(@"^(?:(?:[^\x00-\x20""'<>%\\^`{|}]|%[0-9a-f]{2})|\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?)*\})*$", RegexOptions.IgnoreCase, RegexTimeout); + private static Regex keyRegex = new Regex(@"^[a-zA-Z0-9\.\-_]+$", RegexOptions.None, RegexTimeout); public static ValidationRule DocumentRequiredFields => new ValidationRule( @@ -31,61 +29,33 @@ public static class AsyncApiDocumentRules } context.Exit(); + }); + public static ValidationRule ChannelKeyRegex => + new ValidationRule( + (context, document) => + { context.Enter("channels"); - try + var hashSet = new HashSet(); + foreach (var key in document.Channels?.Keys) { - // MUST have at least 1 channel - if (document.Channels == null || !document.Channels.Keys.Any()) + if (!keyRegex.IsMatch(key)) { context.CreateError( - nameof(DocumentRequiredFields), - string.Format(Resource.Validation_FieldRequired, "channels", "document")); - return; + "ChannelKeys", + string.Format(Resource.Validation_KeyMustMatchRegularExpr, key, "channels", keyRegex.ToString())); } - var hashSet = new HashSet(); - foreach (var key in document.Channels.Keys) - { - // Uri-template - if (!ChannelKeyUriTemplateRegex.IsMatch(key)) - { - context.CreateError( - "ChannelKeys", - string.Format(Resource.Validation_KeyMustMatchRegularExpr, key, "channels", KeyRegex.ToString())); - } - // Unique channel keys - var pathSignature = GetKeySignature(key); - if (!hashSet.Add(pathSignature)) - { - context.CreateError("ChannelKey", string.Format(Resource.Validation_ChannelsMustBeUnique, pathSignature)); - } + if (!hashSet.Add(key)) + { + context.CreateError("ChannelKey", string.Format(Resource.Validation_ChannelsMustBeUnique)); } } - finally - { - context.Exit(); - } - }); - - private static string GetKeySignature(string path) - { - for (int openBrace = path.IndexOf('{'); openBrace > -1; openBrace = path.IndexOf('{', openBrace + 2)) - { - int closeBrace = path.IndexOf('}', openBrace); - - if (closeBrace < 0) - { - return path; - } - path = path.Substring(0, openBrace + 1) + path.Substring(closeBrace); - } - - return path; - } + context.Exit(); + }); - public static ValidationRule KeyMustBeRegularExpression => + public static ValidationRule ServerKeyRegex => new ValidationRule( (context, document) => { @@ -97,11 +67,11 @@ private static string GetKeySignature(string path) context.Enter("servers"); foreach (var key in document.Servers?.Keys) { - if (!KeyRegex.IsMatch(key)) + if (!keyRegex.IsMatch(key)) { context.CreateError( - nameof(KeyMustBeRegularExpression), - string.Format(Resource.Validation_KeyMustMatchRegularExpr, key, "servers", KeyRegex.ToString())); + "ServerKeys", + string.Format(Resource.Validation_KeyMustMatchRegularExpr, key, "servers", keyRegex.ToString())); } } diff --git a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiMultiFormatSchemaRules.cs b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiMultiFormatSchemaRules.cs new file mode 100644 index 0000000..814465a --- /dev/null +++ b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiMultiFormatSchemaRules.cs @@ -0,0 +1,24 @@ +namespace ByteBard.AsyncAPI.Validation.Rules +{ + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Validations; + + [AsyncApiRule] + public static class AsyncApiMultiFormatSchemaRules + { + public static ValidationRule MultiFormatSchemaRequiredFields => + new ValidationRule( + (context, multiFormatSchema) => + { + context.Enter("schema"); + if (multiFormatSchema.Schema == null) + { + context.CreateError( + nameof(MultiFormatSchemaRequiredFields), + string.Format(Resource.Validation_FieldRequired, "schema", "multiFormatSchema")); + } + + context.Exit(); + }); + } +} \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiOAuthFlowRules.cs b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiOAuthFlowRules.cs index ba463b9..c68a529 100644 --- a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiOAuthFlowRules.cs +++ b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiOAuthFlowRules.cs @@ -31,12 +31,12 @@ public static class AsyncApiOAuthFlowRules context.Exit(); - context.Enter("scopes"); - if (oauthFlow.Scopes == null || !oauthFlow.Scopes.Keys.Any()) + context.Enter("availableScopes"); + if (oauthFlow.AvailableScopes == null || !oauthFlow.AvailableScopes.Keys.Any()) { context.CreateError( nameof(OAuthFlowRequiredFields), - string.Format(Resource.Validation_FieldRequired, "scopes", "flow")); + string.Format(Resource.Validation_FieldRequired, "availableScopes", "flow")); } context.Exit(); diff --git a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiOperationReplyAdressRules.cs b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiOperationReplyAdressRules.cs new file mode 100644 index 0000000..1f5d4a1 --- /dev/null +++ b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiOperationReplyAdressRules.cs @@ -0,0 +1,24 @@ +namespace ByteBard.AsyncAPI.Validation.Rules +{ + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Validations; + + [AsyncApiRule] + public static class AsyncApiOperationReplyAddressRules + { + public static ValidationRule OperationReplyAddressRequiredFields => + new ValidationRule( + (context, replyAddress) => + { + context.Enter("location"); + if (replyAddress.Location is null) + { + context.CreateError( + nameof(OperationReplyAddressRequiredFields), + string.Format(Resource.Validation_FieldRequired, "location", "replyAddress")); + } + + context.Exit(); + }); + } +} \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiOperationRules.cs b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiOperationRules.cs new file mode 100644 index 0000000..85548b8 --- /dev/null +++ b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiOperationRules.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using System.Linq; + +namespace ByteBard.AsyncAPI.Validation.Rules +{ + using System; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Validations; + + [AsyncApiRule] + public static class AsyncApiOperationRules + { + public static ValidationRule OperationRequiredFields => + new ValidationRule( + (context, operation) => + { + context.Enter("action"); + if (!Enum.IsDefined(typeof(AsyncApiAction), operation.Action)) + { + context.CreateError( + nameof(OperationRequiredFields), + string.Format(Resource.Validation_FieldRequired, "action", "operation")); + } + + context.Exit(); + + context.Enter("channel"); + if (operation.Channel is null) + { + context.CreateError( + nameof(OperationRequiredFields), + string.Format(Resource.Validation_FieldRequired, "channel", "operation")); + } + + context.Exit(); + }); + + public static ValidationRule OperationChannelReference => + new ValidationRule( + (context, operation) => + { + if (context.RootDocument?.Operations.Values.FirstOrDefault(op => op == operation) is null) + { + return; + } + + var channels = + context.RootDocument.Channels.Values.Where(channel => operation.Channel.Equals(channel)); + + var referencedChannel = channels.FirstOrDefault(c => operation.Channel.Equals(c)); + if (referencedChannel == null) + { + context.CreateError( + "OperationChannelRef", + string.Format(Resource.Validation_OperationMustReferenceValidChannel, operation.Title)); + } + }); + + public static ValidationRule OperationMessages => + new ValidationRule( + (context, operation) => + { + if (context.RootDocument?.Operations.Values.FirstOrDefault(op => op == operation) is null) + { + return; + } + + var channels = + context.RootDocument.Channels.Values.Where(channel => operation.Channel.Equals(channel)); + + var referencedChannel = channels.FirstOrDefault(c => operation.Channel.Equals(c)); + + if (referencedChannel == null) + { + return; + } + + if (!AllOperationsMessagesReferencesChannelMessages(operation.Messages, referencedChannel.Messages.Values)) + { + context.CreateError( + "OperationChannelRef", + string.Format(Resource.Validation_OperationMessagesMustReferenceOperationChannel, operation.Title)); + } + }); + + private static bool AllOperationsMessagesReferencesChannelMessages( + IList operationMessages, ICollection channelMessages) => + operationMessages.All(opMessage => OperationMessageReferencesAnyChannelMessage(opMessage, channelMessages)); + + private static bool OperationMessageReferencesAnyChannelMessage( + AsyncApiMessageReference operationMessage, ICollection channelMessages) => + channelMessages.Any(channelMessage => channelMessage.Equals(operationMessage)); + } +} \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiSecuritySchemaRules.cs b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiSecuritySchemaRules.cs new file mode 100644 index 0000000..821178c --- /dev/null +++ b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiSecuritySchemaRules.cs @@ -0,0 +1,106 @@ +using System.Collections.Generic; + +namespace ByteBard.AsyncAPI.Validation.Rules +{ + using System; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Validations; + + [AsyncApiRule] + public static class AsyncApiSecuritySchemeRules + { + public static ValidationRule SecuritySchemeRequiredFields => + new ValidationRule( + (context, securityScheme) => + { + context.Enter("type"); + if (!Enum.IsDefined(typeof(SecuritySchemeType), securityScheme.Type)) + { + context.CreateError( + nameof(SecuritySchemeRequiredFields), + string.Format(Resource.Validation_FieldRequired, "type", "securityScheme")); + } + + context.Exit(); + + context.Enter("name"); + if (string.IsNullOrWhiteSpace(securityScheme.Name) && securityScheme.IsFieldRequired("name")) + { + context.CreateError( + nameof(SecuritySchemeRequiredFields), + string.Format(Resource.Validation_FieldRequired, "name", "securityScheme")); + } + + context.Exit(); + + context.Enter("in"); + if (securityScheme.In is null && securityScheme.IsFieldRequired("in")) + { + context.CreateError( + nameof(SecuritySchemeRequiredFields), + string.Format(Resource.Validation_FieldRequired, "in", "securityScheme")); + } + + context.Exit(); + + context.Enter("scheme"); + if (string.IsNullOrWhiteSpace(securityScheme.Scheme) && securityScheme.IsFieldRequired("scheme")) + { + context.CreateError( + nameof(SecuritySchemeRequiredFields), + string.Format(Resource.Validation_FieldRequired, "scheme", "securityScheme")); + } + + context.Exit(); + + context.Enter("flows"); + if (securityScheme.Flows is null && securityScheme.IsFieldRequired("flows")) + { + context.CreateError( + nameof(SecuritySchemeRequiredFields), + string.Format(Resource.Validation_FieldRequired, "flows", "securityScheme")); + } + + context.Exit(); + + context.Enter("openIdConnectUrl"); + if (securityScheme.OpenIdConnectUrl is null && securityScheme.IsFieldRequired("openIdConnectUrl")) + { + context.CreateError( + nameof(SecuritySchemeRequiredFields), + string.Format(Resource.Validation_FieldRequired, "openIdConnectUrl", "securityScheme")); + } + + context.Exit(); + }); + + public static ValidationRule OpenIdConnectUrlMustBeAbsolute => + new ValidationRule( + (context, securityScheme) => + { + context.Enter("openIdConnectUrl"); + if (securityScheme.OpenIdConnectUrl != null && !securityScheme.OpenIdConnectUrl.IsAbsoluteUri) + { + context.CreateError( + nameof(OpenIdConnectUrlMustBeAbsolute), + string.Format(Resource.Validation_MustBeAbsoluteUrl, "openIdConnectUrl", "securityScheme")); + } + + context.Exit(); + }); + + private static bool IsFieldRequired(this AsyncApiSecurityScheme sc, string fieldName) + { + return RequiredFieldsByType[fieldName](sc); + } + + private static readonly Dictionary> RequiredFieldsByType = new() + { + { "name", sc => sc.Type is SecuritySchemeType.ApiKey }, + { "in", sc => sc.Type is SecuritySchemeType.ApiKey or SecuritySchemeType.HttpApiKey }, + { "scheme", sc => sc.Type is SecuritySchemeType.Http }, + { "flows", sc => sc.Type is SecuritySchemeType.OAuth2 }, + { "openIdConnectUrl", sc => sc.Type is SecuritySchemeType.OpenIdConnect }, + }; + } +} \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiServerRules.cs b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiServerRules.cs index 785864d..50b0213 100644 --- a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiServerRules.cs +++ b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiServerRules.cs @@ -10,12 +10,12 @@ public static class AsyncApiServerRules new ValidationRule( (context, server) => { - context.Enter("url"); - if (server.Url == null) + context.Enter("host"); + if (server.Host == null) { context.CreateError( nameof(ServerRequiredFields), - string.Format(Resource.Validation_FieldRequired, "url", "server")); + string.Format(Resource.Validation_FieldRequired, "host", "server")); } context.Exit(); diff --git a/src/ByteBard.AsyncAPI/Writers/AsyncApiWriterBase.cs b/src/ByteBard.AsyncAPI/Writers/AsyncApiWriterBase.cs index 71804bd..820d54d 100644 --- a/src/ByteBard.AsyncAPI/Writers/AsyncApiWriterBase.cs +++ b/src/ByteBard.AsyncAPI/Writers/AsyncApiWriterBase.cs @@ -1,6 +1,5 @@ namespace ByteBard.AsyncAPI.Writers { - using ByteBard.AsyncAPI.Models; using System; using System.Collections.Generic; using System.IO; diff --git a/src/ByteBard.AsyncAPI/Writers/AsyncApiWriterExtensions.cs b/src/ByteBard.AsyncAPI/Writers/AsyncApiWriterExtensions.cs index e8cf73f..1df47b9 100644 --- a/src/ByteBard.AsyncAPI/Writers/AsyncApiWriterExtensions.cs +++ b/src/ByteBard.AsyncAPI/Writers/AsyncApiWriterExtensions.cs @@ -305,6 +305,20 @@ public static void WriteRequiredMap( } } + public static void WriteRequiredMap( + this IAsyncApiWriter writer, + string name, + IDictionary elements, + Func keySelector, + Action action) + where T : IAsyncApiElement + { + if (elements != null && elements.Any()) + { + writer.WriteMapInternal(name, elements, keySelector, action); + } + } + /// /// Write the optional AsyncApi element map. /// @@ -407,6 +421,16 @@ private static void WriteMapInternal( string name, IDictionary elements, Action action) + { + WriteMapInternal(writer, name, elements, null, action); + } + + private static void WriteMapInternal( + this IAsyncApiWriter writer, + string name, + IDictionary elements, + Func keySelector, + Action action) { CheckArguments(writer, name, action); @@ -417,10 +441,20 @@ private static void WriteMapInternal( { foreach (var item in elements) { - writer.WritePropertyName(item.Key); + string itemKey = item.Key; + if (keySelector != null && item.Value != null) + { + var newKey = keySelector(item.Value); + if (!string.IsNullOrWhiteSpace(newKey)) + { + itemKey = newKey; + } + } + + writer.WritePropertyName(itemKey); if (item.Value != null) { - action(writer, item.Key, item.Value); + action(writer, itemKey, item.Value); } else { diff --git a/src/ByteBard.AsyncAPI/Writers/AsyncApiWriterSettings.cs b/src/ByteBard.AsyncAPI/Writers/AsyncApiWriterSettings.cs index b1dc3a8..7b78bc5 100644 --- a/src/ByteBard.AsyncAPI/Writers/AsyncApiWriterSettings.cs +++ b/src/ByteBard.AsyncAPI/Writers/AsyncApiWriterSettings.cs @@ -56,7 +56,7 @@ public ReferenceInlineSetting ReferenceInline /// /// Gets or sets a value indicating whether indicates if local references should be rendered as an inline object. /// - public bool InlineLocalReferences { get; set; } + public bool InlineLocalReferences { get; private set; } /// /// Figures out if a loop exists. @@ -66,7 +66,7 @@ public ReferenceInlineSetting ReferenceInline /// /// Returns back if the refernece should be inlined or not. /// - /// The refernece. + /// The reference. /// True if it should be inlined otherwise false. public bool ShouldInlineReference(AsyncApiReference reference) { diff --git a/src/ByteBard.AsyncAPI/Writers/IAsyncApiWriter.cs b/src/ByteBard.AsyncAPI/Writers/IAsyncApiWriter.cs index 67f0684..790e521 100644 --- a/src/ByteBard.AsyncAPI/Writers/IAsyncApiWriter.cs +++ b/src/ByteBard.AsyncAPI/Writers/IAsyncApiWriter.cs @@ -1,6 +1,4 @@ -using ByteBard.AsyncAPI.Models; - -namespace ByteBard.AsyncAPI.Writers +namespace ByteBard.AsyncAPI.Writers { public interface IAsyncApiWriter { diff --git a/test/ByteBard.AsyncAPI.Tests/AsyncApiDocumentBuilder.cs b/test/ByteBard.AsyncAPI.Tests/AsyncApiDocumentBuilder.cs index 952a6d4..3ab1229 100644 --- a/test/ByteBard.AsyncAPI.Tests/AsyncApiDocumentBuilder.cs +++ b/test/ByteBard.AsyncAPI.Tests/AsyncApiDocumentBuilder.cs @@ -44,6 +44,12 @@ public AsyncApiDocumentBuilder WithChannel(string key, AsyncApiChannel channel) return this; } + public AsyncApiDocumentBuilder WithOperation(string key, AsyncApiOperation operation) + { + this.document.Operations.Add(key, operation); + return this; + } + public AsyncApiDocumentBuilder WithComponent(string key, AsyncApiJsonSchema schema) { if (this.document.Components == null) @@ -200,13 +206,13 @@ public AsyncApiDocumentBuilder WithComponent(string key, AsyncApiBindings + Security = new List { - new AsyncApiSecurityRequirement - { - { - new AsyncApiSecuritySchemeReference("saslScram"), new List() - }, - }, + new AsyncApiSecuritySchemeReference("#/components/securitySchemes/saslScram"), }, Tags = new List { @@ -249,17 +240,12 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() }) .WithServer("mtls-connections", new AsyncApiServer { - Url = "test.mykafkacluster.org:28092", + Host = "test.mykafkacluster.org:28092", Protocol = "kafka-secure", Description = "Test broker secured with X509", - Security = new List + Security = new List { - new AsyncApiSecurityRequirement - { - { - new AsyncApiSecuritySchemeReference("certs"), new List() - }, - }, + new AsyncApiSecuritySchemeReference("#/components/securitySchemes/certs"), }, Tags = new List { @@ -282,9 +268,10 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() }) .WithDefaultContentType() .WithChannel( - "smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured", + "lighting.measured", new AsyncApiChannel() { + Address = "smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured", Description = "The topic on which measured values may be produced and consumed.", Parameters = new Dictionary { @@ -292,87 +279,94 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() "streetlightId", new AsyncApiParameterReference("#/components/parameters/streetlightId") }, }, - Publish = new AsyncApiOperation() - { - Summary = "Inform about environmental lighting conditions of a particular streetlight.", - OperationId = "receiveLightMeasurement", - Traits = new List - { - new AsyncApiOperationTraitReference("#/components/operationTraits/kafka"), - }, - Message = new List - { - new AsyncApiMessageReference("#/components/messages/lightMeasured"), - }, - }, }) .WithChannel( - "smartylighting.streetlights.1.0.action.{streetlightId}.turn.on", + "turn.on", new AsyncApiChannel() { + Address = "smartylighting.streetlights.1.0.action.{streetlightId}.turn.on", Parameters = new Dictionary { { "streetlightId", new AsyncApiParameterReference("#/components/parameters/streetlightId") }, }, - Subscribe = new AsyncApiOperation() + }) + .WithChannel( + "turn.off", + new AsyncApiChannel() + { + Address = "smartylighting.streetlights.1.0.action.{streetlightId}.turn.off", + Parameters = new Dictionary { - OperationId = "turnOn", - Traits = new List - { - new AsyncApiOperationTraitReference("#/components/operationTraits/kafka"), - }, - Message = new List - { - new AsyncApiMessageReference("#/components/messages/turnOnOff"), - }, + { + "streetlightId", new AsyncApiParameterReference("#/components/parameters/streetlightId") + }, }, }) .WithChannel( - "smartylighting.streetlights.1.0.action.{streetlightId}.turn.off", + "dim", new AsyncApiChannel() { + Address = "smartylighting.streetlights.1.0.action.{streetlightId}.dim", Parameters = new Dictionary { { "streetlightId", new AsyncApiParameterReference("#/components/parameters/streetlightId") }, }, - Subscribe = new AsyncApiOperation() + }) + .WithOperation("receiveLightMeasurement", new AsyncApiOperation() + { + Action = AsyncApiAction.Receive, + Summary = "Inform about environmental lighting conditions of a particular streetlight.", + Channel = new AsyncApiChannelReference("#/channels/lighting.measured"), + Traits = new List { - OperationId = "turnOff", - Traits = new List - { new AsyncApiOperationTraitReference("#/components/operationTraits/kafka"), - }, - Message = new List - { - new AsyncApiMessageReference("#/components/messages/turnOnOff"), - }, + }, + Messages = new List + { + new("#/components/messages/lightMeasured"), }, }) - .WithChannel( - "smartylighting.streetlights.1.0.action.{streetlightId}.dim", - new AsyncApiChannel() + .WithOperation("turnOn", new AsyncApiOperation() { - Parameters = new Dictionary + Action = AsyncApiAction.Send, + Channel = new AsyncApiChannelReference("#/channels/turn.on"), + Traits = new List { + new AsyncApiOperationTraitReference("#/components/operationTraits/kafka"), + }, + Messages = new List { - "streetlightId", new AsyncApiParameterReference("#/components/parameters/streetlightId") + new("#/components/messages/turnOnOff"), }, + }) + .WithOperation("turnOff", new AsyncApiOperation() + { + Action = AsyncApiAction.Send, + Channel = new AsyncApiChannelReference("#/channels/turn.off"), + Traits = new List + { + new AsyncApiOperationTraitReference("#/components/operationTraits/kafka"), }, - Subscribe = new AsyncApiOperation() + Messages = new List + { + new("#/components/messages/turnOnOff"), + }, + }) + .WithOperation("dimLight", new AsyncApiOperation() + { + Action = AsyncApiAction.Send, + Channel = new AsyncApiChannelReference("#/channels/dim"), + Traits = new List { - OperationId = "dimLight", - Traits = new List - { new AsyncApiOperationTraitReference("#/components/operationTraits/kafka"), - }, - Message = new List - { - new AsyncApiMessageReference("#/components/messages/dimLight"), - }, + }, + Messages = new List + { + new("#/components/messages/dimLight"), }, }) .WithComponent("lightMeasured", new AsyncApiMessage() @@ -474,23 +468,12 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Format = "date-time", Description = "Date and time when the message was sent.", }) - .WithComponent("saslScram", new AsyncApiSecurityScheme - { - Type = SecuritySchemeType.ScramSha256, - Description = "Provide your username and password for SASL/SCRAM authentication", - }) - .WithComponent("certs", new AsyncApiSecurityScheme - { - Type = SecuritySchemeType.X509, - Description = "Download the certificate files from service provider", - }) + .WithComponent("saslScram", AsyncApiSecurityScheme.ScramSha256("Provide your username and password for SASL/SCRAM authentication")) + .WithComponent("certs", AsyncApiSecurityScheme.X509("Download the certificate files from service provider")) .WithComponent("streetlightId", new AsyncApiParameter() { Description = "The ID of the streetlight.", - Schema = new AsyncApiJsonSchema() - { - Type = SchemaType.String, - }, + Default = "1", }) .WithComponent("commonHeaders", new AsyncApiMessageTrait() { @@ -539,14 +522,14 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() } [Test] - public void SerializeV2_WithFullSpec_Serializes() + public void V2_SerializeV2_WithFullSpec_Serializes() { var expected = """ asyncapi: 2.6.0 info: title: apiTitle - version: apiVersion + version: 1.0.0 description: description termsOfService: https://example.com/termsOfService contact: @@ -561,18 +544,16 @@ public void SerializeV2_WithFullSpec_Serializes() id: documentId servers: myServer: - url: https://example.com/server + url: example.com/server protocol: KafkaProtocol protocolVersion: protocolVersion description: serverDescription security: - - securitySchemeName: - - requirementItem + - securitySchemeName: [] channels: channel1: description: channelDescription subscribe: - operationId: myOperation summary: operationSummary description: operationDescription tags: @@ -582,8 +563,7 @@ public void SerializeV2_WithFullSpec_Serializes() description: externalDocsDescription url: https://example.com/externalDocs traits: - - operationId: myOperation - summary: traitSummary + - summary: traitSummary description: traitDescription tags: - name: tagName @@ -603,7 +583,6 @@ public void SerializeV2_WithFullSpec_Serializes() description: correlationDescription location: correlationLocation x-extension: value - schemaFormat: schemaFormat contentType: contentType name: messageName title: messageTitle @@ -662,7 +641,7 @@ public void SerializeV2_WithFullSpec_Serializes() string licenseUri = "https://example.com/license"; string extensionKey = "x-extension"; string extensionString = "value"; - string apiVersion = "apiVersion"; + string apiVersion = "1.0.0"; string termsOfServiceUri = "https://example.com/termsOfService"; string channelKey = "channel1"; string channelDescription = "channelDescription"; @@ -675,6 +654,8 @@ public void SerializeV2_WithFullSpec_Serializes() string messageTitle = "messageTitle"; string messageSummary = "messageSummary"; string messageName = "messageName"; + string messageKeyOne = "messageKeyOne"; + string messageKeyTwo = "messageKeyTwo"; string contentType = "contentType"; string schemaFormat = "schemaFormat"; string correlationLocation = "correlationLocation"; @@ -695,7 +676,7 @@ public void SerializeV2_WithFullSpec_Serializes() string serverKey = "myServer"; string serverDescription = "serverDescription"; string protocolVersion = "protocolVersion"; - string serverUrl = "https://example.com/server"; + string serverHost = "example.com/server"; string protocol = "KafkaProtocol"; string securirySchemeDescription = "securitySchemeDescription"; string securitySchemeName = "securitySchemeName"; @@ -727,7 +708,7 @@ public void SerializeV2_WithFullSpec_Serializes() { Implicit = new AsyncApiOAuthFlow { - Scopes = new Dictionary + AvailableScopes = new Dictionary { { scopeKey, scopeValue }, }, @@ -751,20 +732,11 @@ public void SerializeV2_WithFullSpec_Serializes() { Description = serverDescription, ProtocolVersion = protocolVersion, - Url = serverUrl, + Host = serverHost, Protocol = protocol, - Security = new List + Security = new List { - new AsyncApiSecurityRequirement - { - { - new AsyncApiSecuritySchemeReference(securitySchemeName), - new List - { - requirementString, - } - }, - }, + new AsyncApiSecuritySchemeReference($"#/components/securitySchemes/{securitySchemeName}"), }, } }, @@ -801,148 +773,160 @@ public void SerializeV2_WithFullSpec_Serializes() channelKey, new AsyncApiChannel { Description = channelDescription, - Subscribe = new AsyncApiOperation + Messages = new Dictionary() { - Description = operationDescription, - OperationId = operationId, - Summary = operationSummary, - ExternalDocs = new AsyncApiExternalDocumentation { - Url = new Uri(externalDocsUri), - Description = externalDocsDescription, + messageKeyOne, new AsyncApiMessage + { + Description = messageDescription, + Title = messageTitle, + Summary = messageSummary, + Name = messageName, + ContentType = contentType, + } }, - Message = new List { + messageKeyTwo, new AsyncApiMessage { - new AsyncApiMessage + Description = messageDescription, + Title = messageTitle, + Summary = messageSummary, + Name = messageName, + ContentType = contentType, + CorrelationId = new AsyncApiCorrelationId { - Description = messageDescription, - Title = messageTitle, - Summary = messageSummary, - Name = messageName, - ContentType = contentType, - } - }, - { - new AsyncApiMessage - { - Description = messageDescription, - Title = messageTitle, - Summary = messageSummary, - Name = messageName, - ContentType = contentType, - SchemaFormat = schemaFormat, - CorrelationId = new AsyncApiCorrelationId + Location = correlationLocation, + Description = correlationDescription, + Extensions = new Dictionary { - Location = correlationLocation, - Description = correlationDescription, - Extensions = new Dictionary - { - { extensionKey, new AsyncApiAny(extensionString) }, - }, + { extensionKey, new AsyncApiAny(extensionString) }, }, - Traits = new List + }, + Traits = new List + { + new AsyncApiMessageTrait { - new AsyncApiMessageTrait + Name = traitName, + Title = traitTitle, + Headers = new AsyncApiJsonSchema { - Name = traitName, - Title = traitTitle, - Headers = new AsyncApiJsonSchema + Title = schemaTitle, + WriteOnly = true, + Description = schemaDescription, + Examples = new List { - Title = schemaTitle, - WriteOnly = true, - Description = schemaDescription, - Examples = new List + new AsyncApiAny(new ExtensionClass { - new AsyncApiAny(new ExtensionClass - { - Key = anyStringValue, - OtherKey = anyLongValue, - }), - }, + Key = anyStringValue, + OtherKey = anyLongValue, + }), }, - Examples = new List + }, + Examples = new List + { + new AsyncApiMessageExample { - new AsyncApiMessageExample + Summary = exampleSummary, + Name = exampleName, + Payload = new AsyncApiAny(new ExtensionClass { - Summary = exampleSummary, - Name = exampleName, - Payload = new AsyncApiAny(new ExtensionClass - { - Key = anyStringValue, - OtherKey = anyLongValue, - }), - Extensions = new Dictionary - { - { extensionKey, new AsyncApiAny(extensionString) }, - }, - }, - }, - Description = traitDescription, - Summary = traitSummary, - Tags = new List - { - new AsyncApiTag + Key = anyStringValue, + OtherKey = anyLongValue, + }), + Extensions = new Dictionary { - Name = tagName, - Description = tagDescription, + { extensionKey, new AsyncApiAny(extensionString) }, }, }, - ExternalDocs = new AsyncApiExternalDocumentation - { - Url = new Uri(externalDocsUri), - Description = externalDocsDescription, - }, - Extensions = new Dictionary + }, + Description = traitDescription, + Summary = traitSummary, + Tags = new List + { + new AsyncApiTag { - { extensionKey, new AsyncApiAny(extensionString) }, + Name = tagName, + Description = tagDescription, }, }, + ExternalDocs = new AsyncApiExternalDocumentation + { + Url = new Uri(externalDocsUri), + Description = externalDocsDescription, + }, + Extensions = new Dictionary + { + { extensionKey, new AsyncApiAny(extensionString) }, + }, }, - Extensions = new Dictionary - { - { extensionKey, new AsyncApiAny(extensionString) }, - }, - } - }, + }, + Extensions = new Dictionary + { + { extensionKey, new AsyncApiAny(extensionString) }, + }, + } }, - Extensions = new Dictionary + }, + } + }, + }, + Operations = new Dictionary() + { + { + operationId, new AsyncApiOperation() + { + Description = operationDescription, + Summary = operationSummary, + Channel = new AsyncApiChannelReference($"#/channels/{channelKey}"), + ExternalDocs = new AsyncApiExternalDocumentation + { + Url = new Uri(externalDocsUri), + Description = externalDocsDescription, + }, + Messages = new List + { { - { extensionKey, new AsyncApiAny(extensionString) }, + new($"#/channels/channel1/messages/{messageKeyOne}") }, - Tags = new List { - new AsyncApiTag - { - Name = tagName, - Description = tagDescription, - }, + new($"#/channels/channel1/messages/{messageKeyTwo}") + }, + }, + Extensions = new Dictionary + { + { extensionKey, new AsyncApiAny(extensionString) }, + }, + Tags = new List + { + new AsyncApiTag + { + Name = tagName, + Description = tagDescription, }, - Traits = new List + }, + Traits = new List + { + new AsyncApiOperationTrait { - new AsyncApiOperationTrait + Description = traitDescription, + Summary = traitSummary, + Tags = new List { - Description = traitDescription, - Summary = traitSummary, - Tags = new List + new AsyncApiTag { - new AsyncApiTag - { - Name = tagName, - Description = tagDescription, - }, - }, - ExternalDocs = new AsyncApiExternalDocumentation - { - Url = new Uri(externalDocsUri), - Description = externalDocsDescription, - }, - OperationId = operationId, - Extensions = new Dictionary - { - { extensionKey, new AsyncApiAny(extensionString) }, + Name = tagName, + Description = tagDescription, }, }, + ExternalDocs = new AsyncApiExternalDocumentation + { + Url = new Uri(externalDocsUri), + Description = externalDocsDescription, + }, + Extensions = new Dictionary + { + { extensionKey, new AsyncApiAny(extensionString) }, + }, }, }, } @@ -963,7 +947,7 @@ public void SerializeV2_WithFullSpec_Serializes() } [Test] - public void Read_WithAvroSchemaPayload_NoErrors() + public void V2_Read_WithAvroSchemaPayload_NoErrors() { // Arrange var yaml = @@ -1008,12 +992,12 @@ public void Read_WithAvroSchemaPayload_NoErrors() // Assert diagnostics.Errors.Should().HaveCount(0); - result.Channels.First().Value.Publish.Message.First().Payload.As().TryGetAs(out var record).Should().BeTrue(); + result.Operations.Values.FirstOrDefault(op => op.Action == AsyncApiAction.Receive)!.Messages.First().Payload.Schema.As().TryGetAs(out var record).Should().BeTrue(); record.Name.Should().Be("UserSignedUp"); } [Test] - public void Read_WithJsonSchemaReference_NoErrors() + public void V2_Read_WithJsonSchemaReference_NoErrors() { // Arrange var yaml = @@ -1060,17 +1044,21 @@ public void Read_WithJsonSchemaReference_NoErrors() // Assert diagnostics.Errors.Should().HaveCount(0); - result.Channels.First().Value.Publish.Message.First().Title.Should().Be("Message for schema validation testing that is a json object"); - result.Channels.First().Value.Publish.Message.First().Payload.As().Properties.Should().HaveCount(1); + + var message = result.Operations.Values.FirstOrDefault(op => op.Action == AsyncApiAction.Send)!.Messages.First(); + message.Title.Should().Be("Message for schema validation testing that is a json object"); + message.Payload.Schema.As().Properties.Should().HaveCount(1); } [Test] - public void Serialize_WithBindingReferences_SerializesDeserializes() + public void V2_Serialize_WithBindingReferences_SerializesDeserializes() { var expected = """ asyncapi: 2.6.0 info: + title: test + version: 1.0.0 description: test description servers: production: @@ -1080,13 +1068,16 @@ public void Serialize_WithBindingReferences_SerializesDeserializes() bindings: $ref: '#/components/serverBindings/bindings' channels: - testChannel: + 'testChannel/{some}': $ref: '#/components/channels/otherchannel' components: channels: otherchannel: publish: description: test + parameters: + some: + description: a parameter bindings: $ref: '#/components/channelBindings/bindings' serverBindings: @@ -1102,15 +1093,24 @@ public void Serialize_WithBindingReferences_SerializesDeserializes() var doc = new AsyncApiDocument(); doc.Info = new AsyncApiInfo() { + Title = "test", + Version = "1.0.0", Description = "test description", }; doc.Servers.Add("production", new AsyncApiServer { Description = "test description", Protocol = "pulsar+ssl", - Url = "example.com", - Bindings = new AsyncApiBindingsReference("#/components/serverBindings/bindings") + Host = "example.com", + Bindings = new AsyncApiBindingsReference("#/components/serverBindings/bindings"), }); + doc.Channels.Add( + "testChannel", + new AsyncApiChannelReference("#/components/channels/otherchannel")); + doc.Operations.Add( + "operation", + new AsyncApiOperationReference("#/components/operations/otherOperation")); + doc.Components = new AsyncApiComponents() { Channels = new Dictionary() @@ -1118,11 +1118,16 @@ public void Serialize_WithBindingReferences_SerializesDeserializes() { "otherchannel", new AsyncApiChannel() { - Publish = new AsyncApiOperation() + Address = "testChannel/{some}", + Parameters = new Dictionary { - Description = "test", + { "some", new AsyncApiParameter + { + Description = "a parameter", + } + }, }, - Bindings = new AsyncApiBindingsReference("#/components/channelBindings/bindings") + Bindings = new AsyncApiBindingsReference("#/components/channelBindings/bindings"), } }, }, @@ -1151,15 +1156,26 @@ public void Serialize_WithBindingReferences_SerializesDeserializes() } }, }, + Operations = new Dictionary() + { + { + "otherOperation", new AsyncApiOperation() + { + Action = AsyncApiAction.Receive, + Description = "test", + Channel = new AsyncApiChannelReference("#/channels/testChannel"), + } + }, + }, }; - doc.Channels.Add( - "testChannel", - new AsyncApiChannelReference("#/components/channels/otherchannel")); + var actual = doc.Serialize(AsyncApiVersion.AsyncApi2_0, AsyncApiFormat.Yaml); actual.Should().BePlatformAgnosticEquivalentTo(expected); - var settings = new AsyncApiReaderSettings(); - settings.Bindings = BindingsCollection.Pulsar; + var settings = new AsyncApiReaderSettings + { + Bindings = BindingsCollection.Pulsar, + }; var reader = new AsyncApiStringReader(settings); var deserialized = reader.Read(actual, out var diagnostic); var serverBindings = deserialized.Servers.First().Value.Bindings; @@ -1171,11 +1187,13 @@ public void Serialize_WithBindingReferences_SerializesDeserializes() } [Test] - public void Serializev2_WithBindings_Serializes() + public void V2_SerializeV2_WithBindings_Serializes() { var expected = """ asyncapi: 2.6.0 info: + title: test + version: 1.0.0 description: test description servers: production: @@ -1197,18 +1215,21 @@ public void Serializev2_WithBindings_Serializes() kafka: partitions: 2 replicas: 1 + components: { } """; var doc = new AsyncApiDocument(); doc.Info = new AsyncApiInfo() { + Title = "test", + Version = "1.0.0", Description = "test description", }; doc.Servers.Add("production", new AsyncApiServer { Description = "test description", Protocol = "pulsar+ssl", - Url = "example.com", + Host = "example.com", }); doc.Channels.Add( "testChannel", @@ -1224,52 +1245,60 @@ public void Serializev2_WithBindings_Serializes() } }, }, - Publish = new AsyncApiOperation + }); + doc.Operations.Add("firstOperation", new AsyncApiOperation() + { + Channel = new AsyncApiChannelReference("#/channels/testChannel"), + Action = AsyncApiAction.Receive, + Messages = new List + { + new("#/components/messages/firstMessage"), + }, + }); + + doc.Components.Messages.Add("firstMessage", new AsyncApiMessage + { + Bindings = new AsyncApiBindings + { { - Message = new List + new HttpMessageBinding { + Headers = new AsyncApiJsonSchema { - new AsyncApiMessage - { - Bindings = new AsyncApiBindings - { - { - new HttpMessageBinding - { - Headers = new AsyncApiJsonSchema - { - Description = "this mah binding", - }, - } - }, - { - new KafkaMessageBinding - { - Key = new AsyncApiJsonSchema - { - Description = "this mah other binding", - }, - } - }, - }, - } + Description = "this mah binding", }, - }, + } }, - }); - var actual = doc.Serialize(AsyncApiVersion.AsyncApi2_0, AsyncApiFormat.Yaml); + { + new KafkaMessageBinding + { + Key = new AsyncApiJsonSchema + { + Description = "this mah other binding", + }, + } + }, + }, + }); + + var outputString = new StringWriter(); + var writer = new AsyncApiYamlWriter(outputString, new AsyncApiWriterSettings { ReferenceInline = ReferenceInlineSetting.InlineReferences }); + doc.SerializeV2(writer); + var actual = outputString.ToString(); - var settings = new AsyncApiReaderSettings(); - settings.Bindings = BindingsCollection.All; + var settings = new AsyncApiReaderSettings + { + Bindings = BindingsCollection.All, + }; var reader = new AsyncApiStringReader(settings); var deserialized = reader.Read(actual, out var diagnostic); // Assert actual.Should() .BePlatformAgnosticEquivalentTo(expected); - Assert.AreEqual(2, deserialized.Channels.First().Value.Publish.Message.First().Bindings.Count); + Assert.AreEqual(2, deserialized.Operations.First().Value.Messages.First().Bindings.Count); - var binding = deserialized.Channels.First().Value.Publish.Message.First().Bindings.First(); + var binding = deserialized.Operations.First().Value.Messages.First().Bindings.First(); Assert.AreEqual("http", binding.Key); var httpBinding = binding.Value as HttpMessageBinding; diff --git a/test/ByteBard.AsyncAPI.Tests/AsyncApiDocumentV3Tests.cs b/test/ByteBard.AsyncAPI.Tests/AsyncApiDocumentV3Tests.cs new file mode 100644 index 0000000..9042997 --- /dev/null +++ b/test/ByteBard.AsyncAPI.Tests/AsyncApiDocumentV3Tests.cs @@ -0,0 +1,234 @@ +namespace ByteBard.AsyncAPI.Tests +{ + using FluentAssertions; + using ByteBard.AsyncAPI.Bindings; + using ByteBard.AsyncAPI.Models; + using ByteBard.AsyncAPI.Readers; + using NUnit.Framework; + + public class AsyncApiDocumentV3Tests : TestBase + { + [Test] + public void V3_WithComplexInput_CanReSerialize() + { + // Arrange + var expected = + """ + asyncapi: 3.0.0 + info: + title: Streetlights Kafka API + version: 1.0.0 + description: a description + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 + defaultContentType: application/json + servers: + scram-connections: + host: test.mykafkacluster.org:18092 + protocol: kafka-secure + description: Test broker secured with scramSha256 + security: + - $ref: '#/components/securitySchemes/saslScram' + tags: + - name: env:test-scram + description: This environment is meant for running internal tests through scramSha256 + - name: kind:remote + description: This server is a remote server. Not exposed by the application + - name: visibility:private + description: This resource is private and only available to certain users + mtls-connections: + host: test.mykafkacluster.org:28092 + protocol: kafka-secure + description: Test broker secured with X509 + security: + - $ref: '#/components/securitySchemes/certs' + tags: + - name: env:test-mtls + description: This environment is meant for running internal tests through mtls + - name: kind:remote + description: This server is a remote server. Not exposed by the application + - name: visibility:private + description: This resource is private and only available to certain users + channels: + lightingMeasured: + address: 'smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured' + messages: + lightMeasured: + $ref: '#/components/messages/lightMeasured' + description: The topic on which measured values may be produced and consumed. + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + lightTurnOn: + address: 'smartylighting.streetlights.1.0.action.{streetlightId}.turn.on' + messages: + turnOn: + $ref: '#/components/messages/turnOnOff' + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + lightTurnOff: + address: 'smartylighting.streetlights.1.0.action.{streetlightId}.turn.off' + messages: + turnOff: + $ref: '#/components/messages/turnOnOff' + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + lightsDim: + address: 'smartylighting.streetlights.1.0.action.{streetlightId}.dim' + messages: + dimLight: + $ref: '#/components/messages/dimLight' + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + operations: + receiveLightMeasurement: + action: receive + channel: + $ref: '#/channels/lightingMeasured' + summary: Inform about environmental lighting conditions of a particular streetlight. + traits: + - $ref: '#/components/operationTraits/kafka' + messages: + - $ref: '#/channels/lightingMeasured/messages/lightMeasured' + turnOn: + action: send + channel: + $ref: '#/channels/lightTurnOn' + traits: + - $ref: '#/components/operationTraits/kafka' + messages: + - $ref: '#/channels/lightTurnOn/messages/turnOn' + turnOff: + action: send + channel: + $ref: '#/channels/lightTurnOff' + traits: + - $ref: '#/components/operationTraits/kafka' + messages: + - $ref: '#/channels/lightTurnOff/messages/turnOff' + dimLight: + action: send + channel: + $ref: '#/channels/lightsDim' + traits: + - $ref: '#/components/operationTraits/kafka' + messages: + - $ref: '#/channels/lightsDim/messages/dimLight' + components: + schemas: + lightMeasuredPayload: + schemaFormat: application/vnd.aai.asyncapi+json;version=3.0.0 + schema: + type: object + properties: + lumens: + type: integer + description: Light intensity measured in lumens. + minimum: 0 + sentAt: + $ref: '#/components/schemas/sentAt' + turnOnOffPayload: + schemaFormat: application/vnd.aai.asyncapi+json;version=3.0.0 + schema: + type: object + properties: + command: + type: string + description: Whether to turn on or off the light. + enum: + - on + - off + sentAt: + $ref: '#/components/schemas/sentAt' + dimLightPayload: + schemaFormat: application/vnd.aai.asyncapi+json;version=3.0.0 + schema: + type: object + properties: + percentage: + type: integer + description: Percentage to which the light should be dimmed to. + maximum: 100 + minimum: 0 + sentAt: + $ref: '#/components/schemas/sentAt' + sentAt: + schemaFormat: application/vnd.aai.asyncapi+json;version=3.0.0 + schema: + type: string + format: date-time + description: Date and time when the message was sent. + messages: + lightMeasured: + payload: + $ref: '#/components/schemas/lightMeasuredPayload' + contentType: application/json + name: lightMeasured + title: Light measured + summary: Inform about environmental lighting conditions of a particular streetlight. + traits: + - $ref: '#/components/messageTraits/commonHeaders' + turnOnOff: + payload: + $ref: '#/components/schemas/turnOnOffPayload' + name: turnOnOff + title: Turn on/off + summary: Command a particular streetlight to turn the lights on or off. + traits: + - $ref: '#/components/messageTraits/commonHeaders' + dimLight: + payload: + $ref: '#/components/schemas/dimLightPayload' + name: dimLight + title: Dim light + summary: Command a particular streetlight to dim the lights. + traits: + - $ref: '#/components/messageTraits/commonHeaders' + securitySchemes: + saslScram: + type: scramSha256 + description: Provide your username and password for SASL/SCRAM authentication + certs: + type: X509 + description: Download the certificate files from service provider + parameters: + streetlightId: + description: The ID of the streetlight. + operationTraits: + kafka: + bindings: + kafka: + clientId: + type: string + enum: + - my-app-id + messageTraits: + commonHeaders: + headers: + schemaFormat: application/vnd.aai.asyncapi+json;version=3.0.0 + schema: + type: object + properties: + my-app-header: + type: integer + maximum: 100 + minimum: 0 + """; + + var reader = new AsyncApiStringReader(new AsyncApiReaderSettings { Bindings = BindingsCollection.Kafka }); + + // Act + var document = reader.Read(expected, out var diagnostics); + var reserialized = document.SerializeAsYaml(AsyncApiVersion.AsyncApi3_0); + + // Assert + diagnostics.Errors.Should().BeEmpty(); + diagnostics.Warnings.Should().BeEmpty(); + reserialized.Should().BePlatformAgnosticEquivalentTo(expected); + } + } +} diff --git a/test/ByteBard.AsyncAPI.Tests/AsyncApiLicenseTests.cs b/test/ByteBard.AsyncAPI.Tests/AsyncApiLicenseTests.cs index 8bbba5d..2fd9c07 100644 --- a/test/ByteBard.AsyncAPI.Tests/AsyncApiLicenseTests.cs +++ b/test/ByteBard.AsyncAPI.Tests/AsyncApiLicenseTests.cs @@ -14,7 +14,7 @@ public class AsyncApiLicenseTests : TestBase { [Test] - public void Serialize_WithAllProperties_Serializes() + public void V2_Serialize_WithAllProperties_Serializes() { var expected = """ { @@ -51,7 +51,7 @@ public static Stream GenerateStreamFromString(string s) } [Test] - public void LoadLicense_WithJson_Deserializes() + public void V2_LoadLicense_WithJson_Deserializes() { // Arrange var input = """ diff --git a/test/ByteBard.AsyncAPI.Tests/AsyncApiReaderTests.cs b/test/ByteBard.AsyncAPI.Tests/AsyncApiReaderTests.cs index efddac9..9c1deae 100644 --- a/test/ByteBard.AsyncAPI.Tests/AsyncApiReaderTests.cs +++ b/test/ByteBard.AsyncAPI.Tests/AsyncApiReaderTests.cs @@ -13,7 +13,7 @@ namespace ByteBard.AsyncAPI.Tests public class AsyncApiReaderTests { [Test] - public void Read_WithMissingEverything_DeserializesWithErrors() + public void V2_Read_WithMissingEverything_DeserializesWithErrors() { var yaml = @"asyncapi: 2.6.0"; var reader = new AsyncApiStringReader(); @@ -21,7 +21,7 @@ public void Read_WithMissingEverything_DeserializesWithErrors() } [Test] - public void Read_WithComponentBindings_Deserializes() + public void V2_Read_WithComponentBindings_Deserializes() { var yaml = @" asyncapi: 2.6.0 @@ -40,14 +40,16 @@ public void Read_WithComponentBindings_Deserializes() messages: WorkspaceEventPayload: schemaFormat: application/schema+yaml;version=draft-07 + payload: + type: string "; var reader = new AsyncApiStringReader(); var doc = reader.Read(yaml, out var diagnostic); - Assert.AreEqual("application/schema+yaml;version=draft-07", doc.Components.Messages["WorkspaceEventPayload"].SchemaFormat); + Assert.AreEqual("application/schema+yaml;version=draft-07", doc.Components.Messages["WorkspaceEventPayload"].Payload.SchemaFormat); } [Test] - public void Read_WithExtensionParser_Parses() + public void V2_Read_WithExtensionParser_Parses() { var extensionName = "x-someValue"; var yaml = $""" @@ -92,7 +94,7 @@ public void Read_WithExtensionParser_Parses() } [Test] - public void Read_WithUnmappedMemberHandlingError_AddsError() + public void V2_Read_WithUnmappedMemberHandlingError_AddsError() { var extensionName = "x-someValue"; var yaml = $""" @@ -137,7 +139,7 @@ public void Read_WithUnmappedMemberHandlingError_AddsError() } [Test] - public void Read_WithUnmappedMemberHandlingIgnore_NoErrors() + public void V2_Read_WithUnmappedMemberHandlingIgnore_NoErrors() { var extensionName = "x-someValue"; var yaml = $""" @@ -182,7 +184,7 @@ public void Read_WithUnmappedMemberHandlingIgnore_NoErrors() } [Test] - public void Read_WithThrowingExtensionParser_AddsToDiagnostics() + public void V2_Read_WithThrowingExtensionParser_AddsToDiagnostics() { var extensionName = "x-fail"; var yaml = $""" @@ -222,7 +224,7 @@ public void Read_WithThrowingExtensionParser_AddsToDiagnostics() } [Test] - public void Read_WithBasicPlusContact_Deserializes() + public void V2_Read_WithBasicPlusContact_Deserializes() { var yaml = """ asyncapi: 2.3.0 @@ -245,7 +247,7 @@ public void Read_WithBasicPlusContact_Deserializes() } [Test] - public void Read_WithBasicPlusExternalDocs_Deserializes() + public void V2_Read_WithBasicPlusExternalDocs_Deserializes() { var yaml = """ asyncapi: 2.3.0 @@ -270,13 +272,13 @@ public void Read_WithBasicPlusExternalDocs_Deserializes() """; var reader = new AsyncApiStringReader(); var doc = reader.Read(yaml, out var diagnostic); - var message = doc.Channels["workspace"].Publish.Message; - Assert.AreEqual(new Uri("https://example.com"), message.First().ExternalDocs.Url); - Assert.AreEqual("Find more info here", message.First().ExternalDocs.Description); + var messages = doc.Channels["workspace"].Messages.Values; + Assert.AreEqual(new Uri("https://example.com"), messages.First().ExternalDocs.Url); + Assert.AreEqual("Find more info here", messages.First().ExternalDocs.Description); } [Test] - public void Read_WithBasicPlusTag_Deserializes() + public void V2_Read_WithBasicPlusTag_Deserializes() { var yaml = """ asyncapi: 2.3.0 @@ -292,13 +294,13 @@ public void Read_WithBasicPlusTag_Deserializes() """; var reader = new AsyncApiStringReader(); var doc = reader.Read(yaml, out var diagnostic); - var tag = doc.Tags.First(); + var tag = doc.Info.Tags.First(); Assert.AreEqual("user", tag.Name); Assert.AreEqual("User-related messages", tag.Description); } [Test] - public void Read_WithBasicPlusServerDeserializes() + public void V2_Read_WithBasicPlusServerDeserializes() { var yaml = """ asyncapi: 2.3.0 @@ -310,7 +312,7 @@ public void Read_WithBasicPlusServerDeserializes() x-eventarchetype: objectchanged servers: production: - url: 'pulsar+ssl://prod.events.managed.io:1234' + url: 'prod.events.managed.io:1234' protocol: pulsar+ssl description: Pulsar broker """; @@ -318,13 +320,13 @@ public void Read_WithBasicPlusServerDeserializes() var doc = reader.Read(yaml, out var diagnostic); var server = doc.Servers.First(); Assert.AreEqual("production", server.Key); - Assert.AreEqual("pulsar+ssl://prod.events.managed.io:1234", server.Value.Url); + Assert.AreEqual("prod.events.managed.io:1234", server.Value.Host); Assert.AreEqual("pulsar+ssl", server.Value.Protocol); Assert.AreEqual("Pulsar broker", server.Value.Description); } [Test] - public void Read_WithBasicPlusServerVariablesDeserializes() + public void V2_Read_WithBasicPlusServerVariablesDeserializes() { var yaml = """ asyncapi: 2.3.0 @@ -336,28 +338,28 @@ public void Read_WithBasicPlusServerVariablesDeserializes() x-eventarchetype: objectchanged servers: production: - url: 'pulsar+ssl://prod.events.managed.io:{port}' + url: 'prod.events.managed.{security}.io:443 ' protocol: pulsar+ssl description: Pulsar broker variables: - port: - description: Secure connection (TLS) is available through port 8883. - default: '1883' + security: + description: Secure connection + default: 'secure' enum: - - '1883' - - '8883' + - 'secure' + - 'insecure' """; var reader = new AsyncApiStringReader(); var doc = reader.Read(yaml, out var diagnostic); var server = doc.Servers.First(); var variable = server.Value.Variables.First(); Assert.AreEqual("production", server.Key); - Assert.AreEqual("port", variable.Key); - Assert.AreEqual("Secure connection (TLS) is available through port 8883.", variable.Value.Description); + Assert.AreEqual("security", variable.Key); + Assert.AreEqual("Secure connection", variable.Value.Description); } [Test] - public void Read_WithBasicPlusCorrelationIDDeserializes() + public void V2_Read_WithBasicPlusCorrelationIDDeserializes() { var yaml = """ asyncapi: 2.3.0 @@ -382,13 +384,13 @@ public void Read_WithBasicPlusCorrelationIDDeserializes() """; var reader = new AsyncApiStringReader(); var doc = reader.Read(yaml, out var diagnostic); - var message = doc.Channels["workspace"].Publish.Message; - Assert.AreEqual("Default Correlation ID", message.First().CorrelationId.Description); - Assert.AreEqual("$message.header#/correlationId", message.First().CorrelationId.Location); + var messages = doc.Channels["workspace"].Messages.Values; + Assert.AreEqual("Default Correlation ID", messages.First().CorrelationId.Description); + Assert.AreEqual("$message.header#/correlationId", messages.First().CorrelationId.Location); } [Test] - public void Read_WithOneOfMessage_Reads() + public void V2_Read_WithOneOfMessage_Reads() { var yaml = """ asyncapi: 2.3.0 @@ -414,13 +416,13 @@ public void Read_WithOneOfMessage_Reads() """; var reader = new AsyncApiStringReader(); var doc = reader.Read(yaml, out var diagnostic); - var message = doc.Channels["workspace"].Publish.Message.First(); + var message = doc.Channels["workspace"].Messages.Values.First(); Assert.AreEqual("Default Correlation ID", message.CorrelationId.Description); Assert.AreEqual("$message.header#/correlationId", message.CorrelationId.Location); } [Test] - public void Read_WithBasicPlusSecuritySchemeDeserializes() + public void V2_Read_WithBasicPlusSecuritySchemeDeserializes() { var yaml = """ asyncapi: 2.3.0 @@ -453,7 +455,7 @@ public void Read_WithBasicPlusSecuritySchemeDeserializes() } [Test] - public void Read_WithWrongReference_AddsError() + public void V2_Read_WithWrongReference_AddsError() { var yaml = """ @@ -475,11 +477,11 @@ public void Read_WithWrongReference_AddsError() var reader = new AsyncApiStringReader(); var doc = reader.Read(yaml, out var diagnostic); diagnostic.Errors.Should().NotBeEmpty(); - doc.Channels.Values.First().Publish.Message.Should().BeEmpty(); + doc.Channels.Values.First().Messages.Should().BeEmpty(); } [Test] - public void Read_WithBasicPlusOAuthFlowDeserializes() + public void V2_Read_WithBasicPlusOAuthFlowDeserializes() { var yaml = """ asyncapi: 2.3.0 @@ -507,12 +509,12 @@ public void Read_WithBasicPlusOAuthFlowDeserializes() Assert.AreEqual("oauth2", scheme.Key); Assert.AreEqual(SecuritySchemeType.OAuth2, scheme.Value.Type); Assert.AreEqual(new Uri("https://example.com/api/oauth/dialog"), flow.Implicit.AuthorizationUrl); - Assert.IsTrue(flow.Implicit.Scopes.ContainsKey("write:pets")); - Assert.IsTrue(flow.Implicit.Scopes.ContainsKey("read:pets")); + Assert.IsTrue(flow.Implicit.AvailableScopes.ContainsKey("write:pets")); + Assert.IsTrue(flow.Implicit.AvailableScopes.ContainsKey("read:pets")); } [Test] - public void Read_WithServerReference_ResolvesReference() + public void V2_Read_WithServerReference_ResolvesReference() { var yaml = """ asyncapi: 2.3.0 @@ -547,11 +549,11 @@ public void Read_WithServerReference_ResolvesReference() """; var reader = new AsyncApiStringReader(); var doc = reader.Read(yaml, out var diagnostic); - Assert.AreEqual("pulsar+ssl://prod.events.managed.io:1234", doc.Servers.First().Value.Url); + Assert.AreEqual("prod.events.managed.io:1234", doc.Servers.First().Value.Host); } [Test] - public void Read_WithChannelReference_ResolvesReference() + public void V2_Read_WithChannelReference_ResolvesReference() { var yaml = """ asyncapi: 2.3.0 @@ -582,6 +584,8 @@ public void Read_WithChannelReference_ResolvesReference() messages: WorkspaceEventPayload: schemaFormat: 'application/schema+yaml;version=draft-07' + payload: + type: string securitySchemes: petstore_auth: type: oauth2 @@ -594,11 +598,11 @@ public void Read_WithChannelReference_ResolvesReference() """; var reader = new AsyncApiStringReader(); var doc = reader.Read(yaml, out var diagnostic); - Assert.AreEqual("application/schema+yaml;version=draft-07", doc.Channels.First().Value.Publish.Message.First().SchemaFormat); + Assert.AreEqual("application/schema+yaml;version=draft-07", doc.Channels.First().Value.Messages.Values.First().Payload.SchemaFormat); } [Test] - public void Read_WithBasicPlusMessageTraitsDeserializes() + public void V2_Read_WithBasicPlusMessageTraitsDeserializes() { var yaml = """ asyncapi: 2.3.0 @@ -636,8 +640,8 @@ public void Read_WithBasicPlusMessageTraitsDeserializes() var reader = new AsyncApiStringReader(); var doc = reader.Read(yaml, out var diagnostic); - Assert.AreEqual(1, doc.Channels.First().Value.Publish.Message.First().Traits.Count); - Assert.AreEqual("a common headers for common things", doc.Channels.First().Value.Publish.Message.First().Traits.First().Description); + Assert.AreEqual(1, doc.Channels.First().Value.Messages.Values.First().Traits.Count); + Assert.AreEqual("a common headers for common things", doc.Channels.First().Value.Messages.Values.First().Traits.First().Description); } /// @@ -645,7 +649,7 @@ public void Read_WithBasicPlusMessageTraitsDeserializes() /// Bug: Serializing properties multiple times - specifically Schema.OneOf was serialized into OneOf and Then. /// [Test] - public void Serialize_withOneOfSchema_DoesNotWriteThen() + public void V2_Serialize_withOneOfSchema_DoesNotWriteThen() { var yaml = """ asyncapi: 2.3.0 @@ -696,7 +700,7 @@ public void Serialize_withOneOfSchema_DoesNotWriteThen() } [Test] - public void Read_WithBasicPlusSecurityRequirementsDeserializes() + public void V2_Read_WithBasicPlusSecurityRequirementsDeserializes() { var yaml = """ asyncapi: 2.3.0 @@ -728,10 +732,10 @@ public void Read_WithBasicPlusSecurityRequirementsDeserializes() """; var reader = new AsyncApiStringReader(); var doc = reader.Read(yaml, out var diagnostic); - var requirement = doc.Servers.First().Value.Security.First().First(); - Assert.AreEqual(SecuritySchemeType.OAuth2, requirement.Key.Type); - Assert.IsTrue(requirement.Value.Contains("write:pets")); - Assert.IsTrue(requirement.Value.Contains("read:pets")); + var scheme = doc.Servers.First().Value.Security.First(); + Assert.AreEqual(SecuritySchemeType.OAuth2, scheme.Type); + Assert.IsTrue(scheme.Scopes.Contains("write:pets")); + Assert.IsTrue(scheme.Scopes.Contains("read:pets")); } } } diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/AMQP/AMQPBindings_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/AMQP/AMQPBindings_Should.cs index f724800..c8fd6e3 100644 --- a/test/ByteBard.AsyncAPI.Tests/Bindings/AMQP/AMQPBindings_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/AMQP/AMQPBindings_Should.cs @@ -11,7 +11,7 @@ public class AMQPBindings_Should { [Test] - public void AMQPChannelBinding_WithRoutingKey_SerializesAndDeserializes() + public void V2_AMQPChannelBinding_WithRoutingKey_SerializesAndDeserializes() { // Arrange var expected = @@ -56,7 +56,7 @@ public void AMQPChannelBinding_WithRoutingKey_SerializesAndDeserializes() } [Test] - public void AMQPChannelBinding_WithQueue_SerializesAndDeserializes() + public void V2_AMQPChannelBinding_WithQueue_SerializesAndDeserializes() { // Arrange var expected = @@ -101,7 +101,7 @@ public void AMQPChannelBinding_WithQueue_SerializesAndDeserializes() } [Test] - public void AMQPMessageBinding_WithFilledObject_SerializesAndDeserializes() + public void V2_AMQPMessageBinding_WithFilledObject_SerializesAndDeserializes() { // Arrange var expected = @@ -133,7 +133,7 @@ public void AMQPMessageBinding_WithFilledObject_SerializesAndDeserializes() } [Test] - public void AMQPOperationBinding_WithFilledObject_SerializesAndDeserializes() + public void V2_AMQPOperationBinding_WithFilledObject_SerializesAndDeserializes() { // Arrange var expected = diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/BindingExtensions_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/BindingExtensions_Should.cs index 57951a1..3e658ff 100644 --- a/test/ByteBard.AsyncAPI.Tests/Bindings/BindingExtensions_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/BindingExtensions_Should.cs @@ -11,7 +11,7 @@ public class BindingExtensions_Should { [Test] - public void TryGetValue_WithChannelBinding_ReturnsBinding() + public void V2_TryGetValue_WithChannelBinding_ReturnsBinding() { var channel = new AsyncApiChannel(); channel.Bindings.Add(new WebSocketsChannelBinding @@ -34,7 +34,7 @@ public void TryGetValue_WithChannelBinding_ReturnsBinding() } [Test] - public void TryGetValue_WithServerBinding_ReturnsBinding() + public void V2_TryGetValue_WithServerBinding_ReturnsBinding() { var server = new AsyncApiServer(); server.Bindings.Add(new PulsarServerBinding @@ -49,7 +49,7 @@ public void TryGetValue_WithServerBinding_ReturnsBinding() } [Test] - public void TryGetValue_WithOperationBinding_ReturnsBinding() + public void V2_TryGetValue_WithOperationBinding_ReturnsBinding() { var operation = new AsyncApiOperation(); operation.Bindings.Add(new MQTTOperationBinding @@ -66,7 +66,7 @@ public void TryGetValue_WithOperationBinding_ReturnsBinding() } [Test] - public void TryGetValue_WithMessageBinding_ReturnsBinding() + public void V2_TryGetValue_WithMessageBinding_ReturnsBinding() { var message = new AsyncApiMessage(); message.Bindings.Add(new MQTTMessageBinding diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/CustomBinding_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/CustomBinding_Should.cs index fcb65fb..6d4da98 100644 --- a/test/ByteBard.AsyncAPI.Tests/Bindings/CustomBinding_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/CustomBinding_Should.cs @@ -63,7 +63,7 @@ public override void SerializeProperties(IAsyncApiWriter writer) public class CustomBinding_Should : TestBase { [Test] - public void CustomBinding_SerializesDeserializes() + public void V2_CustomBinding_SerializesDeserializes() { // Arrange var expected = diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs index 69c78e3..3590d0a 100644 --- a/test/ByteBard.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs @@ -10,7 +10,7 @@ internal class HttpBindings_Should : TestBase { [Test] - public void HttpMessageBinding_FilledObject_SerializesAndDeserializes() + public void V2_HttpMessageBinding_FilledObject_SerializesAndDeserializes() { // Arrange var expected = @@ -44,7 +44,7 @@ public void HttpMessageBinding_FilledObject_SerializesAndDeserializes() } [Test] - public void HttpOperationBinding_FilledObject_SerializesAndDeserializes() + public void V2_HttpOperationBinding_FilledObject_SerializesAndDeserializes() { // Arrange var expected = diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/Kafka/KafkaBindings_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/Kafka/KafkaBindings_Should.cs index 4469adb..96baa94 100644 --- a/test/ByteBard.AsyncAPI.Tests/Bindings/Kafka/KafkaBindings_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/Kafka/KafkaBindings_Should.cs @@ -11,7 +11,7 @@ internal class KafkaBindings_Should : TestBase { [Test] - public void KafkaChannelBinding_WithFilledObject_SerializesAndDeserializes() + public void V2_KafkaChannelBinding_WithFilledObject_SerializesAndDeserializes() { // Arrange var expected = @@ -70,12 +70,12 @@ public void KafkaChannelBinding_WithFilledObject_SerializesAndDeserializes() } [Test] - public void KafkaServerBinding_WithFilledObject_SerializesAndDeserializes() + public void V2_KafkaServerBinding_WithFilledObject_SerializesAndDeserializes() { // Arrange var expected = """ - url: https://example.com + url: example.com protocol: kafka bindings: kafka: @@ -85,7 +85,7 @@ public void KafkaServerBinding_WithFilledObject_SerializesAndDeserializes() var server = new AsyncApiServer() { - Url = "https://example.com", + Host = "example.com", Protocol = "kafka", }; @@ -108,7 +108,7 @@ public void KafkaServerBinding_WithFilledObject_SerializesAndDeserializes() } [Test] - public void KafkaMessageBinding_WithFilledObject_SerializesAndDeserializes() + public void V2_KafkaMessageBinding_WithFilledObject_SerializesAndDeserializes() { // Arrange var expected = @@ -148,7 +148,7 @@ public void KafkaMessageBinding_WithFilledObject_SerializesAndDeserializes() } [Test] - public void KafkaOperationBinding_WithFilledObject_SerializesAndDeserializes() + public void V2_KafkaOperationBinding_WithFilledObject_SerializesAndDeserializes() { // Arrange var expected = diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/Pulsar/PulsarBindings_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/Pulsar/PulsarBindings_Should.cs index a53f73a..bb0a030 100644 --- a/test/ByteBard.AsyncAPI.Tests/Bindings/Pulsar/PulsarBindings_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/Pulsar/PulsarBindings_Should.cs @@ -12,7 +12,7 @@ internal class PulsarBindings_Should : TestBase { [Test] - public void PulsarChannelBinding_WithFilledObject_SerializesAndDeserializes() + public void V2_PulsarChannelBinding_WithFilledObject_SerializesAndDeserializes() { // Arrange var expected = @@ -68,7 +68,7 @@ public void PulsarChannelBinding_WithFilledObject_SerializesAndDeserializes() } [Test] - public void PulsarChannelBindingNamespaceDefaultToNull() + public void V2_PulsarChannelBindingNamespaceDefaultToNull() { // Arrange var actual = @@ -88,12 +88,12 @@ public void PulsarChannelBindingNamespaceDefaultToNull() } [Test] - public void PulsarServerBinding_WithFilledObject_SerializesAndDeserializes() + public void V2_PulsarServerBinding_WithFilledObject_SerializesAndDeserializes() { // Arrange var expected = """ - url: https://example.com + url: example.com protocol: pulsar bindings: pulsar: @@ -102,7 +102,7 @@ public void PulsarServerBinding_WithFilledObject_SerializesAndDeserializes() var server = new AsyncApiServer() { - Url = "https://example.com", + Host = "example.com", Protocol = "pulsar", }; @@ -124,12 +124,12 @@ public void PulsarServerBinding_WithFilledObject_SerializesAndDeserializes() } [Test] - public void ServerBindingVersionDefaultsToNull() + public void V2_ServerBindingVersionDefaultsToNull() { // Arrange var expected = """ - url: https://example.com + url: example.com protocol: pulsar bindings: pulsar: @@ -138,7 +138,7 @@ public void ServerBindingVersionDefaultsToNull() var server = new AsyncApiServer() { - Url = "https://example.com", + Host = "example.com", Protocol = "pulsar", }; @@ -162,12 +162,12 @@ public void ServerBindingVersionDefaultsToNull() } [Test] - public void ServerTenantDefaultsToNull() + public void V2_ServerTenantDefaultsToNull() { // Arrange var expected = """ - url: https://example.com + url: example.com protocol: pulsar bindings: pulsar: @@ -176,7 +176,7 @@ public void ServerTenantDefaultsToNull() var server = new AsyncApiServer() { - Url = "https://example.com", + Host = "example.com", Protocol = "pulsar", }; diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/Sns/SnsBindings_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/Sns/SnsBindings_Should.cs index 51dde1d..25a13ac 100644 --- a/test/ByteBard.AsyncAPI.Tests/Bindings/Sns/SnsBindings_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/Sns/SnsBindings_Should.cs @@ -13,7 +13,7 @@ namespace ByteBard.AsyncAPI.Tests.Bindings.Sns internal class SnsBindings_Should : TestBase { [Test] - public void SnsChannelBinding_WithFilledObject_SerializesAndDeserializes() + public void V2_SnsChannelBinding_WithFilledObject_SerializesAndDeserializes() { // Arrange var expected = @@ -180,7 +180,7 @@ public void SnsChannelBinding_WithFilledObject_SerializesAndDeserializes() } [Test] - public void SnsOperationBinding_WithFilledObject_SerializesAndDeserializes() + public void V2_SnsOperationBinding_WithFilledObject_SerializesAndDeserializes() { // Arrange var expected = diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/Sqs/SqsBindings_should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/Sqs/SqsBindings_should.cs index f082737..eac548f 100644 --- a/test/ByteBard.AsyncAPI.Tests/Bindings/Sqs/SqsBindings_should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/Sqs/SqsBindings_should.cs @@ -14,7 +14,7 @@ namespace ByteBard.AsyncAPI.Tests.Bindings.Sqs internal class SqsBindings_should { [Test] - public void SqsChannelBinding_WithFilledObject_SerializesAndDeserializes() + public void V2_SqsChannelBinding_WithFilledObject_SerializesAndDeserializes() { // Arrange var expected = @@ -267,7 +267,7 @@ public void SqsChannelBinding_WithFilledObject_SerializesAndDeserializes() } [Test] - public void SqsOperationBinding_WithFilledObject_SerializesAndDeserializes() + public void V2_SqsOperationBinding_WithFilledObject_SerializesAndDeserializes() { // Arrange var expected = diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/StringOrStringList_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/StringOrStringList_Should.cs index 17ceba2..7e344c0 100644 --- a/test/ByteBard.AsyncAPI.Tests/Bindings/StringOrStringList_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/StringOrStringList_Should.cs @@ -13,7 +13,7 @@ namespace ByteBard.AsyncAPI.Tests.Bindings public class StringOrStringList_Should : TestBase { [Test] - public void StringOrStringList_IsInitialised_WhenPassedStringOrStringList() + public void V2_StringOrStringList_IsInitialised_WhenPassedStringOrStringList() { // Arrange var stringValue = new StringOrStringList(new AsyncApiAny("AsyncApi")); @@ -31,7 +31,7 @@ public void StringOrStringList_IsInitialised_WhenPassedStringOrStringList() } [Test] - public void StringOrStringList_ThrowsArgumentException_WhenIntialisedWithoutStringOrStringList() + public void V2_StringOrStringList_ThrowsArgumentException_WhenIntialisedWithoutStringOrStringList() { // Assert var ex = Assert.Throws(() => new StringOrStringList(new AsyncApiAny(true))); @@ -41,7 +41,7 @@ public void StringOrStringList_ThrowsArgumentException_WhenIntialisedWithoutStri } [Test] - public void StringOrStringList_ThrowsArgumentException_WhenIntialisedWithListOfNonStrings() + public void V2_StringOrStringList_ThrowsArgumentException_WhenIntialisedWithListOfNonStrings() { // Assert var ex = Assert.Throws(() => new StringOrStringList( @@ -57,7 +57,7 @@ public void StringOrStringList_ThrowsArgumentException_WhenIntialisedWithListOfN } [Test] - public void StringOrStringList_WhenValueIsString_SerializesDeserializes() + public void V2_StringOrStringList_WhenValueIsString_SerializesDeserializes() { // Arrange var expected = """ @@ -87,7 +87,7 @@ public void StringOrStringList_WhenValueIsString_SerializesDeserializes() } [Test] - public void StringOrStringList_WhenValueIsStringList_SerializesDeserializes() + public void V2_StringOrStringList_WhenValueIsStringList_SerializesDeserializes() { // Arrange var expected = """ diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/WebSockets/WebSocketBindings_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/WebSockets/WebSocketBindings_Should.cs index e68aa10..1daf0d0 100644 --- a/test/ByteBard.AsyncAPI.Tests/Bindings/WebSockets/WebSocketBindings_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/WebSockets/WebSocketBindings_Should.cs @@ -10,7 +10,7 @@ public class WebSocketBindings_Should : TestBase { [Test] - public void WebSocketChannelBinding_WithFilledObject_SerializesAndDeserializes() + public void V2_WebSocketChannelBinding_WithFilledObject_SerializesAndDeserializes() { // Arrange var expected = diff --git a/test/ByteBard.AsyncAPI.Tests/ByteBard.AsyncAPI.Tests.csproj b/test/ByteBard.AsyncAPI.Tests/ByteBard.AsyncAPI.Tests.csproj index 0b80c60..36f6007 100644 --- a/test/ByteBard.AsyncAPI.Tests/ByteBard.AsyncAPI.Tests.csproj +++ b/test/ByteBard.AsyncAPI.Tests/ByteBard.AsyncAPI.Tests.csproj @@ -1,4 +1,4 @@ - + 11 @@ -20,7 +20,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -29,6 +29,15 @@ - + + + + + + PreserveNewest + + + PreserveNewest + diff --git a/test/ByteBard.AsyncAPI.Tests/MQTT/MQTTBindings_Should.cs b/test/ByteBard.AsyncAPI.Tests/MQTT/MQTTBindings_Should.cs index fcca52a..53cb570 100644 --- a/test/ByteBard.AsyncAPI.Tests/MQTT/MQTTBindings_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/MQTT/MQTTBindings_Should.cs @@ -10,12 +10,12 @@ public class MQTTBindings_Should { [Test] - public void MQTTServerBinding_FilledObject_SerializesAndDeserializes() + public void V2_MQTTServerBinding_FilledObject_SerializesAndDeserializes() { // Arrange var expected = """ - url: https://example.com + url: example.com protocol: mqtt bindings: mqtt: @@ -32,7 +32,7 @@ public void MQTTServerBinding_FilledObject_SerializesAndDeserializes() """; var server = new AsyncApiServer(); - server.Url = "https://example.com"; + server.Host = "example.com"; server.Protocol = "mqtt"; server.Bindings.Add(new MQTTServerBinding { @@ -65,7 +65,7 @@ public void MQTTServerBinding_FilledObject_SerializesAndDeserializes() } [Test] - public void MQTTOperationBinding_WithFilledObject_SerializesAndDeserializes() + public void V2_MQTTOperationBinding_WithFilledObject_SerializesAndDeserializes() { // Arrange var expected = @@ -99,7 +99,7 @@ public void MQTTOperationBinding_WithFilledObject_SerializesAndDeserializes() } [Test] - public void MQTTMessageBinding_WithFilledObject_SerializesAndDeserializes() + public void V2_MQTTMessageBinding_WithFilledObject_SerializesAndDeserializes() { // Arrange var expected = diff --git a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiAnyTests.cs b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiAnyTests.cs index 5fc000c..196bef5 100644 --- a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiAnyTests.cs +++ b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiAnyTests.cs @@ -8,7 +8,7 @@ public class AsyncApiAnyTests { [Test] - public void GetValue_ReturnsCorrectConversions() + public void V2_GetValue_ReturnsCorrectConversions() { // Arrange // Act diff --git a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiChannel_Should.cs b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiChannel_Should.cs index 59bba83..0e09214 100644 --- a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiChannel_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiChannel_Should.cs @@ -13,7 +13,7 @@ public class AsyncApiChannel_Should : TestBase { [Test] - public void AsyncApiChannel_WithInlineParameter_DoesNotCreateReference() + public void V2_AsyncApiChannel_WithInlineParameter_DoesNotCreateReference() { var input = """ @@ -32,7 +32,7 @@ public void AsyncApiChannel_WithInlineParameter_DoesNotCreateReference() } [Test] - public void AsyncApiChannel_WithWebSocketsBinding_Serializes() + public void V2_AsyncApiChannel_WithWebSocketsBinding_Serializes() { var expected = """ bindings: @@ -95,7 +95,7 @@ public void AsyncApiChannel_WithWebSocketsBinding_Serializes() } [Test] - public void AsyncApiChannel_WithKafkaBinding_Serializes() + public void V2_AsyncApiChannel_WithKafkaBinding_Serializes() { var expected = """ @@ -127,5 +127,78 @@ public void AsyncApiChannel_WithKafkaBinding_Serializes() actual.Should() .BePlatformAgnosticEquivalentTo(expected); } + + [Test] + public void V2_AsyncApiChannel_UpgradesAndNormalizesKey() + { + var yaml = + """ + asyncapi: 2.6.0 + info: + title: test spec + version: 1.0.0 + channels: + 'mychannel/{param}': + description: test channel + parameters: + param: + description: some parameter + """; + + var document = new AsyncApiStringReader().Read(yaml, out var diag); + + document.Channels.First().Key.Should().Be("mychannelparam"); + document.Channels.First().Value.Address.Should().Be("mychannel/{param}"); + document.Channels.First().Value.Parameters.First().Key.Should().Be("param"); + + var reserialized = document.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + + reserialized.Should().BePlatformAgnosticEquivalentTo(yaml); + } + + [Test] + public void V2_AsyncApiChannel_reserializes() + { + var expected = + """ + asyncapi: 2.6.0 + info: + title: test spec + version: 1.0.0 + channels: + mychannel: + description: test channel + subscribe: + description: what ever + message: + $ref: '#/components/messages/anonymous-message-1' + components: + messages: + anonymous-message-1: + payload: + type: string + """; + + var yaml = + """ + asyncapi: 2.6.0 + info: + title: test spec + version: 1.0.0 + channels: + mychannel: + description: test channel + subscribe: + description: what ever + message: + payload: + type: string + """; + + var document = new AsyncApiStringReader().Read(yaml, out var diag); + var reserialized = document.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + + reserialized.Should().BePlatformAgnosticEquivalentTo(expected); + } } } diff --git a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiContact_Should.cs b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiContact_Should.cs index fba450f..967a773 100644 --- a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiContact_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiContact_Should.cs @@ -7,7 +7,7 @@ namespace ByteBard.AsyncAPI.Tests.Models public class AsyncApiContact_Should { [Test] - public void SerializeV2_WithNullWriter_Throws() + public void V2_SerializeV2_WithNullWriter_Throws() { // Arrange var asyncApiContact = new AsyncApiContact(); diff --git a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiExternalDocumentation_Should.cs b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiExternalDocumentation_Should.cs index 7bad819..9395488 100644 --- a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiExternalDocumentation_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiExternalDocumentation_Should.cs @@ -7,7 +7,7 @@ namespace ByteBard.AsyncAPI.Tests.Models public class AsyncApiExternalDocumentation_Should { [Test] - public void SerializeV2_WithNullWriter_Throws() + public void V2_SerializeV2_WithNullWriter_Throws() { // Arrange var asyncApiExternalDocumentation = new AsyncApiExternalDocumentation(); diff --git a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiInfo_Should.cs b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiInfo_Should.cs index 56f2bc1..32acedc 100644 --- a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiInfo_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiInfo_Should.cs @@ -7,7 +7,7 @@ namespace ByteBard.AsyncAPI.Tests.Models public class AsyncApiInfo_Should { [Test] - public void SerializeV2_WithNullWriter_Throws() + public void V2_SerializeV2_WithNullWriter_Throws() { // Arrange var asyncApiInfo = new AsyncApiInfo(); diff --git a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiLicense_Should.cs b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiLicense_Should.cs index abd2c55..35b96f6 100644 --- a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiLicense_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiLicense_Should.cs @@ -7,7 +7,7 @@ namespace ByteBard.AsyncAPI.Tests.Models public class AsyncApiLicense_Should { [Test] - public void SerializeV2_WithNullWriter_Throws() + public void V2_SerializeV2_WithNullWriter_Throws() { // Arrange var asyncApiLicense = new AsyncApiLicense(); diff --git a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiMessageExample_Should.cs b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiMessageExample_Should.cs index cf94d28..f4b5aaf 100644 --- a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiMessageExample_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiMessageExample_Should.cs @@ -7,7 +7,7 @@ namespace ByteBard.AsyncAPI.Tests.Models public class AsyncApiMessageExample_Should { [Test] - public void SerializeV2_WithNullWriter_Throws() + public void V2_SerializeV2_WithNullWriter_Throws() { // Arrange var asyncApiMessageExample = new AsyncApiMessageExample(); diff --git a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs index 890d1b3..7ca108c 100644 --- a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs @@ -14,7 +14,7 @@ internal class AsyncApiMessage_Should : TestBase { [Test] - public void AsyncApiMessage_WithNoType_DeserializesToDefault() + public void V2_AsyncApiMessage_WithNoType_DeserializesToDefault() { // Arrange var expected = @@ -39,11 +39,11 @@ public void AsyncApiMessage_WithNoType_DeserializesToDefault() // Assert diagnostic.Errors.Should().BeEmpty(); - message.Payload.As().Properties.First().Value.Enum.Should().HaveCount(2); + message.Payload.Schema.As().Properties.First().Value.Enum.Should().HaveCount(2); } [Test] - public void AsyncApiMessage_WithNoSchemaFormat_DeserializesToDefault() + public void V2_AsyncApiMessage_WithNoSchemaFormat_DeserializesToDefault() { // Arrange var expected = @@ -61,11 +61,11 @@ public void AsyncApiMessage_WithNoSchemaFormat_DeserializesToDefault() // Assert diagnostic.Errors.Should().BeEmpty(); - message.SchemaFormat.Should().BeNull(); + message.Payload.SchemaFormat.Should().BeNull(); } [Test] - public void AsyncApiMessage_WithUnsupportedSchemaFormat_DeserializesWithError() + public void V2_AsyncApiMessage_WithUnsupportedSchemaFormat_DeserializesWithError() { // Arrange var expected = @@ -84,11 +84,11 @@ public void AsyncApiMessage_WithUnsupportedSchemaFormat_DeserializesWithError() // Assert diagnostic.Errors.Should().HaveCount(1); - diagnostic.Errors.First().Message.Should().StartWith("'whatever' is not a supported format"); + diagnostic.Errors.First().Message.Should().StartWith("Could not deserialize Payload. Supported formats are"); } [Test] - public void AsyncApiMessage_WithNoSchemaFormat_DoesNotSerializeSchemaFormat() + public void V2_AsyncApiMessage_WithNoSchemaFormat_DoesNotSerializeSchemaFormat() { // Arrange var expected = @@ -127,7 +127,7 @@ public void AsyncApiMessage_WithNoSchemaFormat_DoesNotSerializeSchemaFormat() } [Test] - public void AsyncApiMessage_WithJsonSchemaFormat_Serializes() + public void V2_AsyncApiMessage_WithJsonSchemaFormat_Serializes() { // Arrange var expected = @@ -142,7 +142,6 @@ public void AsyncApiMessage_WithJsonSchemaFormat_Serializes() """; var message = new AsyncApiMessage(); - message.SchemaFormat = "application/vnd.aai.asyncapi+json;version=2.6.0"; message.Payload = new AsyncApiJsonSchema() { Properties = new Dictionary() @@ -155,6 +154,7 @@ public void AsyncApiMessage_WithJsonSchemaFormat_Serializes() }, }, }; + message.Payload.SchemaFormat = "application/vnd.aai.asyncapi+json;version=2.6.0"; // Act var actual = message.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); @@ -167,7 +167,7 @@ public void AsyncApiMessage_WithJsonSchemaFormat_Serializes() } [Test] - public void AsyncApiMessage_WithAvroSchemaFormat_Serializes() + public void V2_AsyncApiMessage_WithAvroSchemaFormat_Serializes() { // Arrange var expected = @@ -186,7 +186,6 @@ public void AsyncApiMessage_WithAvroSchemaFormat_Serializes() """; var message = new AsyncApiMessage(); - message.SchemaFormat = "application/vnd.apache.avro"; var schema = new AvroRecord() { Name = "User", @@ -203,7 +202,11 @@ public void AsyncApiMessage_WithAvroSchemaFormat_Serializes() }, }, }; - message.Payload = schema; + message.Payload = new AsyncApiMultiFormatSchema + { + Schema = schema, + SchemaFormat = "application/vnd.apache.avro", + }; // Act var actual = message.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); @@ -216,7 +219,7 @@ public void AsyncApiMessage_WithAvroSchemaFormat_Serializes() } [Test] - public void AsyncApiMessage_WithAvroAsReference_Deserializes() + public void V2_AsyncApiMessage_WithAvroAsReference_Deserializes() { // Arrange var input = @@ -227,19 +230,19 @@ public void AsyncApiMessage_WithAvroAsReference_Deserializes() """; // Act - var deserializedMessage = new AsyncApiStringReader().ReadFragment(input, AsyncApiVersion.AsyncApi2_0, out _); + var deserializedMessage = new AsyncApiStringReader().ReadFragment(input, AsyncApiVersion.AsyncApi2_0, out var diag); // Assert - var payloadReference = deserializedMessage.Payload as AsyncApiAvroSchemaReference; + var payloadReference = deserializedMessage.Payload.Schema as AsyncApiAvroSchemaReference; payloadReference.UnresolvedReference.Should().BeTrue(); payloadReference.Reference.Should().NotBeNull(); - payloadReference.Reference.IsExternal.Should().BeTrue(); + payloadReference.Reference.IsExternal.Should().BeTrue(); // We push the 'real' reference to components. payloadReference.Reference.IsFragment.Should().BeTrue(); } [Test] - public void AsyncApiMessage_WithFilledObject_Serializes() + public void V2_AsyncApiMessage_WithFilledObject_Serializes() { var expected = """ @@ -468,7 +471,7 @@ public void AsyncApiMessage_WithFilledObject_Serializes() var settings = new AsyncApiReaderSettings(); settings.Bindings = BindingsCollection.All; - var deserializedMessage = new AsyncApiStringReader(settings).ReadFragment(expected, AsyncApiVersion.AsyncApi2_0, out _); + var deserializedMessage = new AsyncApiStringReader(settings).ReadFragment(expected, AsyncApiVersion.AsyncApi2_0, out var diag); // Assert actual.Should() diff --git a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiOAuthFlow_Should.cs b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiOAuthFlow_Should.cs index e66a115..3fe838c 100644 --- a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiOAuthFlow_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiOAuthFlow_Should.cs @@ -7,7 +7,7 @@ namespace ByteBard.AsyncAPI.Tests.Models public class AsyncApiOAuthFlow_Should { [Test] - public void SerializeV2_WithNullWriter_Throws() + public void V2_SerializeV2_WithNullWriter_Throws() { // Arrange var asyncApiOAuthFlow = new AsyncApiOAuthFlow(); diff --git a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiOperation_Should.cs b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiOperation_Should.cs index 3646696..27eeae6 100644 --- a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiOperation_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiOperation_Should.cs @@ -2,18 +2,20 @@ namespace ByteBard.AsyncAPI.Tests.Models { using System; using System.IO; + using System.Linq; using FluentAssertions; using ByteBard.AsyncAPI.Bindings.Http; using ByteBard.AsyncAPI.Bindings.Kafka; using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Readers; using ByteBard.AsyncAPI.Writers; using NUnit.Framework; public class AsyncApiOperation_Should : TestBase { [Test] - public void SerializeV2_WithNullWriter_Throws() + public void V2_SerializeV2_WithNullWriter_Throws() { // Arrange var asyncApiOperation = new AsyncApiOperation(); @@ -24,19 +26,176 @@ public void SerializeV2_WithNullWriter_Throws() } [Test] - public void SerializeV2_WithMultipleMessages_SerializesWithOneOf() + public void V2_Read_WithMultipleMessages_UpgradesAndReserializes() + { + // Arrange + var expected = + """ + asyncapi: 2.6.0 + info: + title: Example API with oneOf in messages + version: 1.0.0 + channels: + SomeChannel: + description: A channel where messages can be sent and received. + subscribe: + summary: Receives a message that can be either a text or a file. + message: + oneOf: + - $ref: '#/components/messages/TextMessage' + - $ref: '#/components/messages/FileMessage' + - $ref: '#/components/messages/anonymous-message-1' + - $ref: '#/components/messages/wsMessage' + components: + messages: + TextMessage: + payload: + type: object + properties: + type: + type: string + enum: + - text + content: + type: string + description: The text content of the message. + contentType: application/json + FileMessage: + payload: + type: object + properties: + type: + type: string + enum: + - file + filename: + type: string + description: The name of the file. + fileData: + type: string + format: byte + description: The file content encoded in base64. + contentType: application/json + anonymous-message-1: + payload: + type: string + contentType: application/json + description: Http Message + wsMessage: + payload: + type: string + contentType: application/json + description: web socket Message + """; + + var yaml = + """ + asyncapi: 2.6.0 + info: + title: Example API with oneOf in messages + version: 1.0.0 + + channels: + SomeChannel: + description: A channel where messages can be sent and received. + subscribe: + operationId: receiveMessage + summary: 'Receives a message that can be either a text or a file.' + message: + oneOf: + - $ref: '#/components/messages/TextMessage' + - $ref: '#/components/messages/FileMessage' + - description: Http Message + contentType: 'application/json' + payload: + type: string + - description: web socket Message + messageId: wsMessage + contentType: 'application/json' + payload: + type: string + components: + messages: + TextMessage: + contentType: 'application/json' + payload: + type: 'object' + properties: + type: + type: 'string' + enum: + - 'text' + content: + type: 'string' + description: 'The text content of the message.' + + FileMessage: + contentType: 'application/json' + payload: + type: 'object' + properties: + type: + type: 'string' + enum: + - 'file' + filename: + type: 'string' + description: 'The name of the file.' + fileData: + type: 'string' + format: 'byte' + description: 'The file content encoded in base64.' + + """; + + // Act + var document = new AsyncApiStringReader().Read(yaml, out var diagnostics); + + // Assert + document.Components.Messages.Should().HaveCount(4); + document.Operations.Should().HaveCount(1); + document.Channels.First().Value.Messages.Should().HaveCount(4); + + var channel = document.Channels.First().Value; + channel.Address.Should().Be("SomeChannel"); + + var operation = document.Operations.First().Value; + operation.Messages.Should().HaveCount(4); + operation.Channel.Description.Should().Be("A channel where messages can be sent and received."); + operation.Channel.Address.Should().Be("SomeChannel"); + Assert.AreEqual(channel, operation.Channel); + + var fileMessage = operation.Messages.First(message => message.Reference.Reference == "#/channels/SomeChannel/messages/FileMessage"); + fileMessage.Should().BeOfType(); + + var fileMessageSchema = fileMessage.Payload.Schema.As(); + fileMessageSchema.Type.Should().Be(SchemaType.Object); + fileMessageSchema.Properties.Should().HaveCount(3); + + var anonymousMessage = operation.Messages.First(message => message.Reference.Reference == "#/channels/SomeChannel/messages/anonymous-message-1"); + anonymousMessage.Description.Should().Be("Http Message"); + anonymousMessage.ContentType.Should().Be("application/json"); + anonymousMessage.Should().BeOfType(); + + var reserialized = document.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + + reserialized.Should().BePlatformAgnosticEquivalentTo(expected); + } + + [Test] + public void V2_SerializeV2_WithMultipleMessages_SerializesWithOneOf() { // Arrange var expected = """ message: oneOf: - - name: First Message - - name: Second Message + - $ref: '#/components/messages/first' + - $ref: '#/components/messages/second' """; var asyncApiOperation = new AsyncApiOperation(); - asyncApiOperation.Message.Add(new AsyncApiMessage { Name = "First Message" }); - asyncApiOperation.Message.Add(new AsyncApiMessage { Name = "Second Message" }); + asyncApiOperation.Messages.Add(new AsyncApiMessageReference("#/components/messages/first")); + asyncApiOperation.Messages.Add(new AsyncApiMessageReference("#/components/messages/second")); var outputString = new StringWriter(); var settings = new AsyncApiWriterSettings(); var writer = new AsyncApiYamlWriter(outputString, settings); @@ -52,16 +211,16 @@ public void SerializeV2_WithMultipleMessages_SerializesWithOneOf() } [Test] - public void SerializeV2_WithSingleMessage_Serializes() + public void V2_SerializeV2_WithSingleMessage_Serializes() { // Arrange var expected = """ message: - name: First Message + $ref: '#/components/messages/first' """; var asyncApiOperation = new AsyncApiOperation(); - asyncApiOperation.Message.Add(new AsyncApiMessage { Name = "First Message" }); + asyncApiOperation.Messages.Add(new AsyncApiMessageReference("#/components/messages/first")); var settings = new AsyncApiWriterSettings(); var outputString = new StringWriter(); var writer = new AsyncApiYamlWriter(outputString, settings); @@ -77,7 +236,7 @@ public void SerializeV2_WithSingleMessage_Serializes() } [Test] - public void AsyncApiOperation_WithBindings_Serializes() + public void V2_AsyncApiOperation_WithBindings_Serializes() { var expected = """ diff --git a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs index 7ee470e..ad363e4 100644 --- a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs @@ -4,17 +4,17 @@ namespace ByteBard.AsyncAPI.Tests using System.IO; using System.Linq; using System.Threading.Tasks; - using FluentAssertions; using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Readers; using ByteBard.AsyncAPI.Readers.Interface; using ByteBard.AsyncAPI.Readers.V2; + using FluentAssertions; using NUnit.Framework; public class AsyncApiReference_Should : TestBase { [Test] - public void ReferencePointers() + public void V2_ReferencePointers() { var diag = new AsyncApiDiagnostic(); var versionService = new AsyncApiV2VersionService(diag); @@ -47,7 +47,7 @@ public void ReferencePointers() } [Test] - public void Reference() + public void V2_Reference() { var json = """ @@ -70,14 +70,14 @@ public void Reference() } [Test] - public void ExternalFragmentReference_ResolvesFragtment() + public void V2_ExternalFragmentReference_ResolvesFragtment() { var externalJson = """ { "servers": [ { - "url": "wss://production.gigantic-server.com:443", + "url": "production.gigantic-server.com:443", "protocol": "wss", "protocolVersion": "1.0.0", "description": "The production API server", @@ -113,56 +113,58 @@ public void ExternalFragmentReference_ResolvesFragtment() var reference = doc.Servers.First().Value as AsyncApiServerReference; reference.Reference.FragmentId.Should().Be("/servers/0"); reference.Reference.IsFragment.Should().BeTrue(); - reference.Url.Should().Be("wss://production.gigantic-server.com:443"); + reference.Host.Should().Be("production.gigantic-server.com:443"); + reference.Protocol.Should().Be("wss"); } [Test] - public void ServerReference_WithComponentReference_ResolvesReference() + public void V2_ServerReference_WithComponentReference_ResolvesReference() { var json = """ - { - "asyncapi": "2.6.0", - "info": { }, - "servers": { - "production": { - "$ref": "#/components/servers/whatever" - } - }, - "components": { - "servers": { - "whatever": { - "url": "wss://production.gigantic-server.com:443", - "protocol": "wss", - "protocolVersion": "1.0.0", - "description": "The production API server", - "variables": { - "username": { - "default": "demo", - "description": "This value is assigned by the service provider" - }, - "password": { - "default": "demo", - "description": "This value is assigned by the service provider" + { + "asyncapi": "2.6.0", + "info": { }, + "servers": { + "production": { + "$ref": "#/components/servers/whatever" + } + }, + "components": { + "servers": { + "whatever": { + "url": "production.gigantic-server.com:443", + "protocol": "wss", + "protocolVersion": "1.0.0", + "description": "The production API server", + "variables": { + "username": { + "default": "demo", + "description": "This value is assigned by the service provider" + }, + "password": { + "default": "demo", + "description": "This value is assigned by the service provider" + } + } + } } } } - } - } - } - """; + """; var doc = new AsyncApiStringReader().Read(json, out var diag); var reference = doc.Servers.First().Value as AsyncApiServerReference; reference.Reference.ExternalResource.Should().BeNull(); reference.Reference.FragmentId.Should().Be("/components/servers/whatever"); reference.Reference.IsFragment.Should().BeTrue(); - reference.Url.Should().Be("wss://production.gigantic-server.com:443"); + reference.Host.Should().Be("production.gigantic-server.com:443"); + reference.Protocol.Should().Be("wss"); } [Test] - public void AsyncApiReference_WithExternalFragmentUriReference_AllowReference() + public void V2_AsyncApiReference_WithExternalFragmentUriReference_AllowReference() { // Arrange var actual = """ @@ -176,7 +178,7 @@ public void AsyncApiReference_WithExternalFragmentUriReference_AllowReference() // Assert diagnostic.Errors.Should().BeEmpty(); - var payload = deserialized.Payload.As(); + var payload = deserialized.Payload.Schema.As(); payload.UnresolvedReference.Should().BeTrue(); var reference = payload.Reference; @@ -191,7 +193,7 @@ public void AsyncApiReference_WithExternalFragmentUriReference_AllowReference() } [Test] - public void AsyncApiReference_WithFragmentReference_AllowReference() + public void V2_AsyncApiReference_WithFragmentReference_AllowReference() { // Arrange var actual = """ @@ -205,7 +207,7 @@ public void AsyncApiReference_WithFragmentReference_AllowReference() // Assert diagnostic.Errors.Should().BeEmpty(); - var payload = deserialized.Payload.As(); + var payload = deserialized.Payload.Schema.As(); payload.UnresolvedReference.Should().BeTrue(); var reference = payload.Reference; @@ -220,35 +222,7 @@ public void AsyncApiReference_WithFragmentReference_AllowReference() } [Test] - public void AsyncApiReference_WithInternalComponentReference_AllowReference() - { - // Arrange - var actual = """ - payload: - $ref: '#/components/schemas/test' - """; - var reader = new AsyncApiStringReader(); - - // Act - var deserialized = reader.ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out var diagnostic); - - // Assert - diagnostic.Errors.Should().BeEmpty(); - var payload = deserialized.Payload.As(); - var reference = payload.Reference; - reference.ExternalResource.Should().BeNull(); - reference.Type.Should().Be(ReferenceType.Schema); - reference.FragmentId.Should().Be("/components/schemas/test"); - reference.IsFragment.Should().BeTrue(); - reference.IsExternal.Should().BeFalse(); - - var expected = deserialized.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); - actual.Should() - .BePlatformAgnosticEquivalentTo(expected); - } - - [Test] - public void AsyncApiReference_WithExternalFragmentReference_AllowReference() + public void V2_AsyncApiReference_WithExternalFragmentReference_AllowReference() { // Arrange var actual = """ @@ -262,7 +236,7 @@ public void AsyncApiReference_WithExternalFragmentReference_AllowReference() // Assert diagnostic.Errors.Should().BeEmpty(); - var payload = deserialized.Payload.As(); + var payload = deserialized.Payload.Schema.As(); var reference = payload.Reference; reference.ExternalResource.Should().Be("./myjsonfile.json"); reference.FragmentId.Should().Be("/fragment"); @@ -275,7 +249,7 @@ public void AsyncApiReference_WithExternalFragmentReference_AllowReference() } [Test] - public void AsyncApiReference_WithExternalComponentReference_AllowReference() + public void V2_AsyncApiReference_WithExternalComponentReference_AllowReference() { // Arrange var actual = """ @@ -289,7 +263,7 @@ public void AsyncApiReference_WithExternalComponentReference_AllowReference() // Assert diagnostic.Errors.Should().BeEmpty(); - var payload = deserialized.Payload.As(); + var payload = deserialized.Payload.Schema.As(); var reference = payload.Reference; reference.ExternalResource.Should().Be("./someotherdocument.json"); reference.Type.Should().Be(ReferenceType.Schema); @@ -303,7 +277,7 @@ public void AsyncApiReference_WithExternalComponentReference_AllowReference() } [Test] - public void AsyncApiDocument_WithInternalComponentReference_ResolvesReference() + public void V2_AsyncApiDocument_WithInternalComponentReference_ResolvesReference() { // Arrange var actual = """ @@ -342,7 +316,7 @@ public void AsyncApiDocument_WithInternalComponentReference_ResolvesReference() } [Test] - public void AsyncApiDocument_WithExternalReferenceOnlySetToResolveInternalReferences_DoesNotResolve() + public void V2_AsyncApiDocument_WithExternalReferenceOnlySetToResolveInternalReferences_DoesNotResolve() { // Arrange var actual = """ @@ -378,7 +352,7 @@ public void AsyncApiDocument_WithExternalReferenceOnlySetToResolveInternalRefere } [Test] - public void AsyncApiReference_WithExternalReference_AllowsReferenceDoesNotResolve() + public void V2_AsyncApiReference_WithExternalReference_AllowsReferenceDoesNotResolve() { // Arrange var actual = """ @@ -392,7 +366,7 @@ public void AsyncApiReference_WithExternalReference_AllowsReferenceDoesNotResolv // Assert diagnostic.Errors.Should().BeEmpty(); - var payload = deserialized.Payload.As(); + var payload = deserialized.Payload.Schema.As(); var reference = payload.Reference; reference.ExternalResource.Should().Be("http://example.com/json.json"); reference.FragmentId.Should().BeNull(); @@ -408,7 +382,7 @@ public void AsyncApiReference_WithExternalReference_AllowsReferenceDoesNotResolv } [Test] - public void AsyncApiReference_WithExternalResourcesInterface_DeserializesCorrectly() + public void V2_AsyncApiReference_WithExternalResourcesInterface_DeserializesCorrectly() { var yaml = """ asyncapi: 2.3.0 @@ -428,14 +402,14 @@ public void AsyncApiReference_WithExternalResourcesInterface_DeserializesCorrect }; var reader = new AsyncApiStringReader(settings); var doc = reader.Read(yaml, out var diagnostic); - var message = doc.Channels["workspace"].Publish.Message.First(); + var message = doc.Channels["workspace"].Messages.Values.First(); message.Name.Should().Be("Test"); - var payload = message.Payload.As(); + var payload = message.Payload.Schema.As(); payload.Properties.Count.Should().Be(1); } [Test] - public void AsyncApiReference_DocumentLevelReferencePointer_DeserializesCorrectly() + public void V2_AsyncApiReference_DocumentLevelReferencePointer_DeserializesCorrectly() { var yaml = """ asyncapi: 2.3.0 @@ -454,12 +428,34 @@ public void AsyncApiReference_DocumentLevelReferencePointer_DeserializesCorrectl var reader = new AsyncApiStringReader(); var doc = reader.Read(yaml, out var diagnostic); doc.Channels.Should().HaveCount(2); - doc.Channels["other"].Publish.Message.First().Title.Should().Be("test message"); + doc.Channels["other"].Messages.Values.First().Title.Should().Be("test message"); } [Test] - public void AsyncApiReference_WithExternalAvroResource_DeserializesCorrectly() + public void V2_AsyncApiReference_WithExternalAvroResource_DeserializesCorrectlyAndUpgrades() { + var expected = + """ + asyncapi: 2.6.0 + info: + title: test + version: 1.0.0 + channels: + workspace: + publish: + message: + $ref: '#/components/messages/anonymous-message-1' + components: + messages: + anonymous-message-1: + payload: + $ref: ./some/path/to/external/payload.json + schemaFormat: application/vnd.apache.avro + name: Test + title: Test message + summary: Test. + """; + var avroPayload = """ { @@ -510,12 +506,12 @@ public void AsyncApiReference_WithExternalAvroResource_DeserializesCorrectly() }; var reader = new AsyncApiStringReader(settings); var doc = reader.Read(yaml, out var diagnostic); - var message = doc.Channels["workspace"].Publish.Message.First(); - var payload = message.Payload.As(); + var message = doc.Channels["workspace"].Messages.Values.First(); + var payload = message.Payload.Schema.As(); payload.As().Name.Should().Be("thecodebuzz_schema"); doc.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0).Should() - .BePlatformAgnosticEquivalentTo(yaml); + .BePlatformAgnosticEquivalentTo(expected); } } diff --git a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiSchema_Should.cs b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiSchema_Should.cs index bd1fee5..6706337 100644 --- a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiSchema_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiSchema_Should.cs @@ -275,7 +275,7 @@ public class AsyncApiSchema_Should : TestBase }; [Test] - public void SerializeAsJson_WithBasicSchema_V2Works() + public void V2_SerializeAsJson_WithBasicSchema_V2Works() { // Arrange var expected = @"{ }"; @@ -289,7 +289,7 @@ public void SerializeAsJson_WithBasicSchema_V2Works() } [Test] - public void SerializeAsJson_WithAdvancedSchemaNumber_V2Works() + public void V2_SerializeAsJson_WithAdvancedSchemaNumber_V2Works() { // Arrange var expected = """ @@ -317,7 +317,7 @@ public void SerializeAsJson_WithAdvancedSchemaNumber_V2Works() } [Test] - public void SerializeAsJson_WithAdvancedSchemaBigNumbers_V2Works() + public void V2_SerializeAsJson_WithAdvancedSchemaBigNumbers_V2Works() { // Arrange var expected = """ @@ -345,10 +345,10 @@ public void SerializeAsJson_WithAdvancedSchemaBigNumbers_V2Works() } [Test] - public void SerializeAsJson_WithAdvancedSchemaObject_V2Works() + public void V2_SerializeAsJson_WithAdvancedSchemaObject_V2Works() { // Arrange - string expected = this.GetTestData(); + string expected = this.GetTestData(AsyncApiVersion.AsyncApi2_0, "SerializeAsJson_WithAdvancedSchemaObject_V2Works"); // Act var actual = AdvancedSchemaObject.SerializeAsJson(AsyncApiVersion.AsyncApi2_0); @@ -359,10 +359,10 @@ public void SerializeAsJson_WithAdvancedSchemaObject_V2Works() } [Test] - public void Deserialize_WithAdvancedSchema_Works() + public void V2_Deserialize_WithAdvancedSchema_Works() { // Arrange - var json = this.GetTestData(); + var json = this.GetTestData(AsyncApiVersion.AsyncApi2_0, "Deserialize_WithAdvancedSchema_Works"); var expected = AdvancedSchemaObject; // Act @@ -373,10 +373,10 @@ public void Deserialize_WithAdvancedSchema_Works() } [Test] - public void SerializeAsJson_WithAdvancedSchemaWithAllOf_V2Works() + public void V2_SerializeAsJson_WithAdvancedSchemaWithAllOf_V2Works() { // Arrange - var expected = this.GetTestData(); + var expected = this.GetTestData(AsyncApiVersion.AsyncApi2_0, "SerializeAsJson_WithAdvancedSchemaWithAllOf_V2Works"); // Act var actual = AdvancedSchemaWithAllOf.SerializeAsJson(AsyncApiVersion.AsyncApi2_0); @@ -387,9 +387,9 @@ public void SerializeAsJson_WithAdvancedSchemaWithAllOf_V2Works() } [Theory] - [TestCase(true)] - [TestCase(false)] - public void Serialize_WithInliningOptions_ShouldInlineAccordingly(bool shouldInline) + [TestCase(ReferenceInlineSetting.InlineReferences)] + [TestCase(ReferenceInlineSetting.DoNotInlineReferences)] + public void V2_Serialize_WithInliningOptions_ShouldInlineAccordingly(ReferenceInlineSetting shouldInline) { // arrange var asyncApiDocument = new AsyncApiDocumentBuilder() @@ -406,11 +406,11 @@ public void Serialize_WithInliningOptions_ShouldInlineAccordingly(bool shouldInl }) .WithChannel("mychannel", new AsyncApiChannel() { - Publish = new AsyncApiOperation + Address = "mychannel-{parameter}", + Messages = new Dictionary { - Message = new List { - new AsyncApiMessage + "whatever", new AsyncApiMessage { Payload = new AsyncApiJsonSchema { @@ -422,10 +422,16 @@ public void Serialize_WithInliningOptions_ShouldInlineAccordingly(bool shouldInl { "testB", new AsyncApiJsonSchemaReference("#/components/schemas/testB") }, }, }, - }, + } }, }, }) + .WithOperation("operationA", new AsyncApiOperation + { + Action = AsyncApiAction.Receive, + Channel = new AsyncApiChannelReference("#/channels/mychannel"), + Messages = new List { new AsyncApiMessageReference("#/channels/mychannel/messages/whatever") } + }) .WithComponent("testD", new AsyncApiJsonSchema() { Type = SchemaType.String, Format = "uuid" }) .WithComponent("testC", new AsyncApiJsonSchema() { @@ -439,7 +445,7 @@ public void Serialize_WithInliningOptions_ShouldInlineAccordingly(bool shouldInl .Build(); var outputString = new StringWriter(); - var writer = new AsyncApiYamlWriter(outputString, new AsyncApiWriterSettings { InlineLocalReferences = shouldInline }); + var writer = new AsyncApiYamlWriter(outputString, new AsyncApiWriterSettings { ReferenceInline = shouldInline }); // Act asyncApiDocument.SerializeV2(writer); @@ -447,7 +453,78 @@ public void Serialize_WithInliningOptions_ShouldInlineAccordingly(bool shouldInl var actual = outputString.ToString(); // Assert - string expected = this.GetTestData(shouldInline + string expected = this.GetTestData( + AsyncApiVersion.AsyncApi2_0, + shouldInline == ReferenceInlineSetting.InlineReferences + ? "AsyncApiSchema_InlinedReferences" + : "AsyncApiSchema_NoInlinedReferences.yml"); + + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + } + + [Theory] + [TestCase(ReferenceInlineSetting.InlineReferences)] + [TestCase(ReferenceInlineSetting.DoNotInlineReferences)] + public void V3_Serialize_WithInliningOptions_ShouldInlineAccordingly(ReferenceInlineSetting shouldInline) + { + // arrange + var asyncApiDocument = new AsyncApiDocumentBuilder() + .WithInfo(new AsyncApiInfo + { + Title = "Streetlights Kafka API", + Version = "1.0.0", + Description = "The Smartylighting Streetlights API allows you to remotely manage the city lights.", + License = new AsyncApiLicense + { + Name = "Apache 2.0", + Url = new Uri("https://www.apache.org/licenses/LICENSE-2.0"), + }, + }) + .WithChannel("mychannel", new AsyncApiChannel() + { + Messages = new Dictionary + { + { + "whatever", new AsyncApiMessage + { + Payload = new AsyncApiJsonSchema + { + Type = SchemaType.Object, + Required = new HashSet { "testB" }, + Properties = new Dictionary + { + { "testC", new AsyncApiJsonSchemaReference("#/components/schemas/testC") }, + { "testB", new AsyncApiJsonSchemaReference("#/components/schemas/testB") }, + }, + }, + } + }, + }, + }) + .WithComponent("testD", new AsyncApiJsonSchema() { Type = SchemaType.String, Format = "uuid" }) + .WithComponent("testC", new AsyncApiJsonSchema() + { + Type = SchemaType.Object, + Properties = new Dictionary + { + { "testD", new AsyncApiJsonSchemaReference("#/components/schemas/testD") }, + }, + }) + .WithComponent("testB", new AsyncApiJsonSchema() { Description = "test", Type = SchemaType.Boolean }) + .Build(); + + var outputString = new StringWriter(); + var writer = new AsyncApiYamlWriter(outputString, new AsyncApiWriterSettings { ReferenceInline = shouldInline }); + + // Act + asyncApiDocument.SerializeV3(writer); + + var actual = outputString.ToString(); + + // Assert + string expected = this.GetTestData(AsyncApiVersion.AsyncApi3_0, + shouldInline == ReferenceInlineSetting.InlineReferences ? "AsyncApiSchema_InlinedReferences" : "AsyncApiSchema_NoInlinedReferences.yml"); @@ -456,7 +533,7 @@ public void Serialize_WithInliningOptions_ShouldInlineAccordingly(bool shouldInl } [Test] - public void SerializeV2_WithNullWriter_Throws() + public void V2_SerializeV2_WithNullWriter_Throws() { // Arrange var asyncApiLicense = new AsyncApiLicense(); @@ -471,7 +548,7 @@ public void SerializeV2_WithNullWriter_Throws() /// Bug: Serializing properties multiple times - specifically Schema.OneOf was serialized into OneOf and Then. /// [Test] - public void Serialize_WithOneOf_DoesNotWriteThen() + public void V2_Serialize_WithOneOf_DoesNotWriteThen() { var mainSchema = new AsyncApiJsonSchema(); var subSchema = new AsyncApiJsonSchema(); @@ -489,7 +566,7 @@ public void Serialize_WithOneOf_DoesNotWriteThen() /// Bug: Serializing properties multiple times - specifically Schema.AnyOf was serialized into AnyOf and If. /// [Test] - public void Serialize_WithAnyOf_DoesNotWriteIf() + public void V2_Serialize_WithAnyOf_DoesNotWriteIf() { var mainSchema = new AsyncApiJsonSchema(); var subSchema = new AsyncApiJsonSchema(); @@ -502,7 +579,7 @@ public void Serialize_WithAnyOf_DoesNotWriteIf() } [Test] - public void Deserialize_BasicExample() + public void V2_Deserialize_BasicExample() { var input = """ @@ -528,7 +605,7 @@ public void Deserialize_BasicExample() /// Bug: Serializing properties multiple times - specifically Schema.Not was serialized into Not and Else. /// [Test] - public void Serialize_WithNot_DoesNotWriteElse() + public void V2_Serialize_WithNot_DoesNotWriteElse() { var mainSchema = new AsyncApiJsonSchema(); var subSchema = new AsyncApiJsonSchema(); diff --git a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiSecurityRequirement_Should.cs b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiSecurityRequirement_Should.cs deleted file mode 100644 index 1201efd..0000000 --- a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiSecurityRequirement_Should.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace ByteBard.AsyncAPI.Tests.Models -{ - using System; - using System.Collections.Generic; - using ByteBard.AsyncAPI.Models; - using NUnit.Framework; - - public class AsyncApiSecurityRequirement_Should - { - [Test] - public void SerializeV2_WithNullWriter_Throws() - { - // Arrange - var asyncApiSecurityRequirement = new AsyncApiSecurityRequirement(); - - // Act - // Assert - Assert.Throws(() => { asyncApiSecurityRequirement.SerializeV2(null); }); - } - - [Test] - public void SerializeV2_Serializes() - { - var asyncApiSecurityRequirement = new AsyncApiSecurityRequirement(); - asyncApiSecurityRequirement.Add(new AsyncApiSecuritySchemeReference("apiKey"), new List { "string" }); - - var output = asyncApiSecurityRequirement.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); - } - } -} diff --git a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiServer_Should.cs b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiServer_Should.cs index 0f3974f..27a85fb 100644 --- a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiServer_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiServer_Should.cs @@ -1,26 +1,28 @@ namespace ByteBard.AsyncAPI.Tests.Models { - using System.Collections.Generic; + using System.Linq; using FluentAssertions; + using ByteBard.AsyncAPI.Bindings; using ByteBard.AsyncAPI.Bindings.Kafka; using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Models.Interfaces; + using ByteBard.AsyncAPI.Readers; using NUnit.Framework; internal class AsyncApiServer_Should : TestBase { [Test] - public void AsyncApiServer_Serializes() + public void V2_AsyncApiServer_Upgrades() { // Arrange - var expected = + var actual = """ - url: 'https://example.com/{channelkey}' + url: 'example.com/{channelKey}' protocol: test protocolVersion: 0.1.0 description: some description variables: - channelkey: + channelKey: description: some description security: - schem1: @@ -34,34 +36,71 @@ public void AsyncApiServer_Serializes() schemaRegistryVendor: kafka """; - var server = new AsyncApiServer - { - Url = "https://example.com/{channelkey}", - Protocol = "test", - ProtocolVersion = "0.1.0", - Description = "some description", - }; - server.Variables.Add("channelkey", new AsyncApiServerVariable { Description = "some description" }); - server.Security.Add( - new AsyncApiSecurityRequirement - { - { - new AsyncApiSecuritySchemeReference("schem1") - , new List - { - "requirement", - } - }, - }); - server.Tags.Add(new AsyncApiTag { Name = "mytag1", Description = "description of tag1" }); - server.Bindings.Add(new KafkaServerBinding - { - SchemaRegistryUrl = "http://example.com", - SchemaRegistryVendor = "kafka", - }); + var deserialized = new AsyncApiStringReader(new AsyncApiReaderSettings { Bindings = BindingsCollection.All }).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out var diag); + deserialized.Security.Should().HaveCount(1); + deserialized.Host.Should().Be("example.com"); + deserialized.PathName.Should().Be("/{channelKey}"); + deserialized.Protocol.Should().Be("test"); + deserialized.ProtocolVersion.Should().Be("0.1.0"); + deserialized.Description.Should().Be("some description"); + deserialized.Variables.Should().HaveCount(1); + deserialized.Variables["channelKey"].Description.Should().Be("some description"); + deserialized.Tags.Should().HaveCount(1); + deserialized.Tags.First().Name.Should().Be("mytag1"); + deserialized.Tags.First().Description.Should().Be("description of tag1"); + deserialized.Bindings.Should().HaveCount(1); + deserialized.Bindings["kafka"].As().SchemaRegistryUrl.Should().Be("http://example.com"); + deserialized.Bindings["kafka"].As().SchemaRegistryVendor.Should().Be("kafka"); + } + + [Test] + public void V2_AsyncApiServer_Serializes() + { + // Arrange + var expected = + """ + asyncapi: 2.6.0 + info: + title: test + version: 1.0.0 + servers: + testServer: + url: 'example.com/{channelkey}' + protocol: test + protocolVersion: 0.1.0 + description: some description + variables: + channelkey: + description: some description + security: + - schem1: + - requirement + - schem2: + - otherRequirement + tags: + - name: mytag1 + description: description of tag1 + bindings: + kafka: + schemaRegistryUrl: http://example.com + schemaRegistryVendor: kafka + channels: + test: + description: testChannel + components: + securitySchemes: + schem1: + type: http + scheme: whatever + schem2: + type: http + scheme: whatever + """; + + var deserialized = new AsyncApiStringReader(new AsyncApiReaderSettings() { Bindings = BindingsCollection.All }).Read(expected, out var diag); // Act - var actual = server.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + var actual = deserialized.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); // Assert actual.Should() @@ -69,12 +108,12 @@ public void AsyncApiServer_Serializes() } [Test] - public void AsyncApiServer_WithKafkaBinding_Serializes() + public void V2_AsyncApiServer_WithKafkaBinding_Serializes() { var expected = """ - url: - protocol: + url: example.com + protocol: test bindings: kafka: schemaRegistryUrl: http://example.com @@ -82,6 +121,8 @@ public void AsyncApiServer_WithKafkaBinding_Serializes() """; var server = new AsyncApiServer { + Host = "example.com", + Protocol = "test", Bindings = new AsyncApiBindings { { diff --git a/test/ByteBard.AsyncAPI.Tests/Models/AvroSchema_Should.cs b/test/ByteBard.AsyncAPI.Tests/Models/AvroSchema_Should.cs index a61ee8f..9d6d57b 100644 --- a/test/ByteBard.AsyncAPI.Tests/Models/AvroSchema_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Models/AvroSchema_Should.cs @@ -9,7 +9,7 @@ public class AvroSchema_Should { [Test] - public void Serialize_WithDefaultNull_SetJsonNull() + public void V2_Serialize_WithDefaultNull_SetJsonNull() { var input = """ type: record @@ -32,7 +32,7 @@ public void Serialize_WithDefaultNull_SetJsonNull() } [Test] - public void Deserialize_WithMetadata_CreatesMetadata() + public void V2_Deserialize_WithMetadata_CreatesMetadata() { var input = """ @@ -81,7 +81,7 @@ public void Deserialize_WithMetadata_CreatesMetadata() } [Test] - public void SerializeV2_SerializesCorrectly() + public void V2_SerializeV2_SerializesCorrectly() { // Arrange var expected = """ @@ -244,7 +244,7 @@ public void SerializeV2_SerializesCorrectly() } [Test] - public void SerializeV2_WithLogicalTypes_SerializesCorrectly() + public void V2_SerializeV2_WithLogicalTypes_SerializesCorrectly() { // Arrange var input = """ @@ -301,7 +301,7 @@ public void SerializeV2_WithLogicalTypes_SerializesCorrectly() } [Test] - public void ReadFragment_DeserializesCorrectly() + public void V2_ReadFragment_DeserializesCorrectly() { // Arrange var input = """ diff --git a/test/ByteBard.AsyncAPI.Tests/Serialization/AsyncApiYamlWriterTests.cs b/test/ByteBard.AsyncAPI.Tests/Serialization/AsyncApiYamlWriterTests.cs index d00a8d4..45b3f5c 100644 --- a/test/ByteBard.AsyncAPI.Tests/Serialization/AsyncApiYamlWriterTests.cs +++ b/test/ByteBard.AsyncAPI.Tests/Serialization/AsyncApiYamlWriterTests.cs @@ -7,79 +7,79 @@ internal class AsyncApiYamlWriterTests : TestBase { [Test] - public void Write_NullValue_ReturnsNull() + public void V2_Write_NullValue_ReturnsNull() => this.Compose(null, "null"); [Test] - public void Write_EmptyValue_ReturnsNull() + public void V2_Write_EmptyValue_ReturnsNull() => this.Compose(string.Empty, "''"); [Test] - public void Write_NullWordString_ReturnsWrappedValue() + public void V2_Write_NullWordString_ReturnsWrappedValue() => this.Compose("null", "'null'"); [Test] - public void Write_TildaWordString_ReturnsWrappedValue() + public void V2_Write_TildaWordString_ReturnsWrappedValue() => this.Compose("~", "'~'"); [Test] - public void Write_IntegerWithTwoPeriods_RendersPlainStyle() + public void V2_Write_IntegerWithTwoPeriods_RendersPlainStyle() => this.Compose("1.2.3", "1.2.3"); [Test] - public void Write_Float_WrappedWithQuotes() + public void V2_Write_Float_WrappedWithQuotes() => this.Compose("1.2", "'1.2'"); [Test] - public void Write_PositiveFloat_WrappedWithQuotes() + public void V2_Write_PositiveFloat_WrappedWithQuotes() => this.Compose("+1.2", "'+1.2'"); [Test] - public void Write_NegativeFloat_WrappedWithQuotes() + public void V2_Write_NegativeFloat_WrappedWithQuotes() => this.Compose("-1.2", "'-1.2'"); [Test] - public void Write_PositiveInfinityFloat_WrappedWithQuotes() + public void V2_Write_PositiveInfinityFloat_WrappedWithQuotes() => this.Compose(".inf", "'.inf'"); [Test] - public void Write_NegativeInfinityFloat_WrappedWithQuotes() + public void V2_Write_NegativeInfinityFloat_WrappedWithQuotes() => this.Compose("-.inf", "'-.inf'"); [Test] - public void Write_NanFloat_WrappedWithQuotes() + public void V2_Write_NanFloat_WrappedWithQuotes() => this.Compose(".nan", "'.nan'"); [Test] - public void Write_TrueString_WrappedWithQuotes() + public void V2_Write_TrueString_WrappedWithQuotes() => this.Compose("true", "'true'"); [Test] - public void Write_FalseString_WrappedWithQuotes() + public void V2_Write_FalseString_WrappedWithQuotes() => this.Compose("false", "'false'"); [Test] - public void Write_DateTimeSlashString_NotWrappedWithQuotes() + public void V2_Write_DateTimeSlashString_NotWrappedWithQuotes() => this.Compose("12/31/2022 23:59:59", "12/31/2022 23:59:59"); [Test] - public void Write_DateTimeDashString_NotWrappedWithQuotes() + public void V2_Write_DateTimeDashString_NotWrappedWithQuotes() => this.Compose("2022-12-31 23:59:59", "2022-12-31 23:59:59"); [Test] - public void Write_DateTimeISOString_NotWrappedWithQuotes() + public void V2_Write_DateTimeISOString_NotWrappedWithQuotes() => this.Compose("2022-12-31T23:59:59Z", "2022-12-31T23:59:59Z"); [Test] - public void Write_DateTimeCanonicalString_NotWrappedWithQuotes() + public void V2_Write_DateTimeCanonicalString_NotWrappedWithQuotes() => this.Compose("2001-12-15T02:59:43.1Z", "2001-12-15T02:59:43.1Z"); [Test] - public void Write_DateTimeSpacedString_NotWrappedWithQuotes() + public void V2_Write_DateTimeSpacedString_NotWrappedWithQuotes() => this.Compose("2001-12-14 21:59:43.10 -5", "2001-12-14 21:59:43.10 -5"); [Test] - public void Write_DateString_NotWrappedWithQuotes() + public void V2_Write_DateString_NotWrappedWithQuotes() => this.Compose("2002-12-14", "2002-12-14"); [Test] diff --git a/test/ByteBard.AsyncAPI.Tests/TestBase.cs b/test/ByteBard.AsyncAPI.Tests/TestBase.cs index f166ede..06005a3 100644 --- a/test/ByteBard.AsyncAPI.Tests/TestBase.cs +++ b/test/ByteBard.AsyncAPI.Tests/TestBase.cs @@ -43,14 +43,18 @@ public void Log(string message) /// /// The type to return. /// The name of the resource file with an optional extension. - /// The result. - protected T GetTestData([CallerMemberName] string resourceName = "") + /// The version. + /// + /// The result. + /// + /// No case has been defined to convering a resource into '{resultType.FullName}'. You can add a new one. + protected T GetTestData(AsyncApiVersion version, [CallerMemberName] string resourceName = "") { string searchPattern = string.IsNullOrWhiteSpace(Path.GetExtension(resourceName)) ? $"{resourceName}.*" : resourceName; - - string testDataDirectory = Path.Combine(Environment.CurrentDirectory, "TestData"); + var versionFolder = version == AsyncApiVersion.AsyncApi2_0 ? "V2_TestData" : "V3_TestData"; + string testDataDirectory = Path.Combine(Environment.CurrentDirectory, versionFolder); string? testDataPath = Directory.GetFiles(testDataDirectory, searchPattern) .FirstOrDefault(); diff --git a/test/ByteBard.AsyncAPI.Tests/TestData/AsyncApiSchema_InlinedReferences.yml b/test/ByteBard.AsyncAPI.Tests/V2_TestData/AsyncApiSchema_InlinedReferences.yml similarity index 95% rename from test/ByteBard.AsyncAPI.Tests/TestData/AsyncApiSchema_InlinedReferences.yml rename to test/ByteBard.AsyncAPI.Tests/V2_TestData/AsyncApiSchema_InlinedReferences.yml index 54f78b6..0ad18c5 100644 --- a/test/ByteBard.AsyncAPI.Tests/TestData/AsyncApiSchema_InlinedReferences.yml +++ b/test/ByteBard.AsyncAPI.Tests/V2_TestData/AsyncApiSchema_InlinedReferences.yml @@ -7,7 +7,7 @@ info: name: Apache 2.0 url: https://www.apache.org/licenses/LICENSE-2.0 channels: - mychannel: + 'mychannel-{parameter}': publish: message: payload: diff --git a/test/ByteBard.AsyncAPI.Tests/TestData/AsyncApiSchema_NoInlinedReferences.yml b/test/ByteBard.AsyncAPI.Tests/V2_TestData/AsyncApiSchema_NoInlinedReferences.yml similarity index 96% rename from test/ByteBard.AsyncAPI.Tests/TestData/AsyncApiSchema_NoInlinedReferences.yml rename to test/ByteBard.AsyncAPI.Tests/V2_TestData/AsyncApiSchema_NoInlinedReferences.yml index 8308baf..02a0110 100644 --- a/test/ByteBard.AsyncAPI.Tests/TestData/AsyncApiSchema_NoInlinedReferences.yml +++ b/test/ByteBard.AsyncAPI.Tests/V2_TestData/AsyncApiSchema_NoInlinedReferences.yml @@ -7,7 +7,7 @@ info: name: Apache 2.0 url: https://www.apache.org/licenses/LICENSE-2.0 channels: - mychannel: + 'mychannel-{parameter}': publish: message: payload: diff --git a/test/ByteBard.AsyncAPI.Tests/TestData/Deserialize_WithAdvancedSchema_Works.json b/test/ByteBard.AsyncAPI.Tests/V2_TestData/Deserialize_WithAdvancedSchema_Works.json similarity index 100% rename from test/ByteBard.AsyncAPI.Tests/TestData/Deserialize_WithAdvancedSchema_Works.json rename to test/ByteBard.AsyncAPI.Tests/V2_TestData/Deserialize_WithAdvancedSchema_Works.json diff --git a/test/ByteBard.AsyncAPI.Tests/TestData/SerializeAsJson_WithAdvancedSchemaObject_V2Works.json b/test/ByteBard.AsyncAPI.Tests/V2_TestData/SerializeAsJson_WithAdvancedSchemaObject_V2Works.json similarity index 100% rename from test/ByteBard.AsyncAPI.Tests/TestData/SerializeAsJson_WithAdvancedSchemaObject_V2Works.json rename to test/ByteBard.AsyncAPI.Tests/V2_TestData/SerializeAsJson_WithAdvancedSchemaObject_V2Works.json diff --git a/test/ByteBard.AsyncAPI.Tests/TestData/SerializeAsJson_WithAdvancedSchemaWithAllOf_V2Works.json b/test/ByteBard.AsyncAPI.Tests/V2_TestData/SerializeAsJson_WithAdvancedSchemaWithAllOf_V2Works.json similarity index 100% rename from test/ByteBard.AsyncAPI.Tests/TestData/SerializeAsJson_WithAdvancedSchemaWithAllOf_V2Works.json rename to test/ByteBard.AsyncAPI.Tests/V2_TestData/SerializeAsJson_WithAdvancedSchemaWithAllOf_V2Works.json diff --git a/test/ByteBard.AsyncAPI.Tests/V3_TestData/AsyncApiSchema_InlinedReferences.yml b/test/ByteBard.AsyncAPI.Tests/V3_TestData/AsyncApiSchema_InlinedReferences.yml new file mode 100644 index 0000000..6157421 --- /dev/null +++ b/test/ByteBard.AsyncAPI.Tests/V3_TestData/AsyncApiSchema_InlinedReferences.yml @@ -0,0 +1,27 @@ +asyncapi: 3.0.0 +info: + title: Streetlights Kafka API + version: 1.0.0 + description: The Smartylighting Streetlights API allows you to remotely manage the city lights. + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 +channels: + mychannel: + messages: + whatever: + payload: + type: object + required: + - testB + properties: + testC: + type: object + properties: + testD: + type: string + format: uuid + testB: + type: boolean + description: test +components: { } \ No newline at end of file diff --git a/test/ByteBard.AsyncAPI.Tests/V3_TestData/AsyncApiSchema_NoInlinedReferences.yml b/test/ByteBard.AsyncAPI.Tests/V3_TestData/AsyncApiSchema_NoInlinedReferences.yml new file mode 100644 index 0000000..e55a67b --- /dev/null +++ b/test/ByteBard.AsyncAPI.Tests/V3_TestData/AsyncApiSchema_NoInlinedReferences.yml @@ -0,0 +1,34 @@ +asyncapi: 3.0.0 +info: + title: Streetlights Kafka API + version: 1.0.0 + description: The Smartylighting Streetlights API allows you to remotely manage the city lights. + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 +channels: + mychannel: + messages: + whatever: + payload: + type: object + required: + - testB + properties: + testC: + $ref: '#/components/schemas/testC' + testB: + $ref: '#/components/schemas/testB' +components: + schemas: + testD: + type: string + format: uuid + testC: + type: object + properties: + testD: + $ref: '#/components/schemas/testD' + testB: + type: boolean + description: test \ No newline at end of file diff --git a/test/ByteBard.AsyncAPI.Tests/Validation/ValidationRuleTests.cs b/test/ByteBard.AsyncAPI.Tests/Validation/ValidationRuleTests.cs index c84e8c9..6aedaf1 100644 --- a/test/ByteBard.AsyncAPI.Tests/Validation/ValidationRuleTests.cs +++ b/test/ByteBard.AsyncAPI.Tests/Validation/ValidationRuleTests.cs @@ -1,123 +1,167 @@ -namespace ByteBard.AsyncAPI.Tests.Validation +namespace ByteBard.AsyncAPI.Tests.Validation; + +using System.Linq; +using FluentAssertions; +using ByteBard.AsyncAPI.Readers; +using NUnit.Framework; + +public class ValidationRuleTests { - using FluentAssertions; - using ByteBard.AsyncAPI.Readers; - using NUnit.Framework; - using System.Linq; + [Test] + public void V2_OperationId_WithNonUniqueKey_DiagnosticsError() + { + var input = + """ + asyncapi: 2.6.0 + info: + title: Chat Application + version: 1.0.0 + servers: + testing: + url: test.mosquitto.org:1883 + protocol: mqtt + description: Test broker + channels: + chat/{personId}: + publish: + operationId: onMessageReceieved + message: + name: text + payload: + type: string + chat/{personIdentity}: + publish: + operationId: onMessageReceieved + message: + name: text + payload: + type: string + """; - public class ValidationRuleTests - { - [Test] - [TestCase("chat-{person-id}")] - public void ChannelKey_WithInvalidParameter_DiagnosticsError(string channelKey) - { - var input = - $""" - asyncapi: 2.6.0 - info: - title: Chat Application - version: 1.0.0 - servers: - testing: - url: test.mosquitto.org:1883 - protocol: mqtt - description: Test broker - channels: - {channelKey}: - publish: - operationId: onMessageReceieved - message: - name: text - payload: - type: string - subscribe: - operationId: sendMessage - message: - name: text - payload: - type: string - """; + new AsyncApiStringReader().Read(input, out var diagnostic); + diagnostic.Errors.First().Message.Should().Be("OperationId: 'onMessageReceieved' is not unique."); + diagnostic.Errors.First().Pointer.Should().Be("#/channels/chat~1{personIdentity}"); + } - var document = new AsyncApiStringReader().Read(input, out var diagnostic); - diagnostic.Errors.First().Message.Should().Be($"The key '{channelKey}' in 'channels' MUST match the regular expression '^[a-zA-Z0-9\\.\\-_]+$'."); - diagnostic.Errors.First().Pointer.Should().Be("#/channels"); - } + [Test] + public void V3_OperationChannel_NotReferencingARootChannel_DiagnosticsError() + { + var input = + """ + asyncapi: 3.0.0 + info: + title: Chat Application + version: 1.0.0 + servers: + testing: + host: test.mosquitto.org:1883 + protocol: mqtt + description: Test broker + channels: + chatPersonId: + address: chat.{personId} + messages: + messageReceived: + name: text + payload: + type: string + operations: + onMessageReceived: + title: Message received + channel: + $ref: '#/components/channels/secondChannel' + messages: + - $ref: '#/channels/chatPersonId/messages/messageReceived' + components: + channels: + secondChannel: + address: chat.{secondChannel} + """; - [Test] - public void ChannelKey_WithNonUniqueKey_DiagnosticsError() - { - var input = - """ - asyncapi: 2.6.0 - info: - title: Chat Application - version: 1.0.0 - servers: - testing: - url: test.mosquitto.org:1883 - protocol: mqtt - description: Test broker - channels: - chat/{personId}: - publish: - operationId: onMessageReceieved - message: - name: text - payload: - type: string - chat/{personIdentity}: - publish: - operationId: onMessageReceieved - message: - name: text - payload: - type: string - """; + new AsyncApiStringReader().Read(input, out var diagnostic); + diagnostic.Errors.First().Message.Should().Be("The operation 'Message received' MUST point to a channel definition located in the root Channels Object."); + diagnostic.Errors.First().Pointer.Should().Be("#/operations/onMessageReceived"); + } - var document = new AsyncApiStringReader().Read(input, out var diagnostic); - diagnostic.Errors.First().Message.Should().Be("Channel signature 'chat/{}' MUST be unique."); - diagnostic.Errors.First().Pointer.Should().Be("#/channels"); - } + [Test] + public void V3_OperationMessage_NotReferencingARootChannel_DiagnosticsError() + { + var input = + """ + asyncapi: 3.0.0 + info: + title: Chat Application + version: 1.0.0 + servers: + testing: + host: test.mosquitto.org:1883 + protocol: mqtt + description: Test broker + channels: + chatPersonId: + address: chat.{personId} + messages: + messageReceived: + name: text + payload: + type: string + operations: + onMessageReceived: + title: Message received + channel: + $ref: '#/channels/chatPersonId' + messages: + - $ref: '#/components/messages/messageSent' + components: + messages: + messageSent: + name: text + payload: + type: string + """; - [Test] - [TestCase("chat")] - [TestCase("/some/chat/{personId}")] - [TestCase("chat-{personId}")] - [TestCase("chat-{person_id}")] - [TestCase("chat-{person%2Did}")] - [TestCase("chat-{personId2}")] - public void ChannelKey_WithValidKey_Success(string channelKey) - { - var input = - $""" - asyncapi: 2.6.0 - info: - title: Chat Application - version: 1.0.0 - servers: - testing: - url: test.mosquitto.org:1883 - protocol: mqtt - description: Test broker - channels: - {channelKey}: - publish: - operationId: onMessageReceieved - message: - name: text - payload: - type: string - subscribe: - operationId: sendMessage - message: - name: text - payload: - type: string - """; + new AsyncApiStringReader().Read(input, out var diagnostic); + diagnostic.Errors.First().Message.Should().Be("The messages of operation 'Message received' MUST be a subset of the referenced channels messages."); + diagnostic.Errors.First().Pointer.Should().Be("#/operations/onMessageReceived"); + } - var document = new AsyncApiStringReader().Read(input, out var diagnostic); - diagnostic.Errors.Should().BeEmpty(); - } - } + [Test] + [TestCase("chat")] + [TestCase("/some/chat/{personId}")] + [TestCase("chat-{personId}")] + [TestCase("chat-{person_id}")] + [TestCase("chat-{person%2Did}")] + [TestCase("chat-{personId2}")] + public void ChannelKey_WithValidKey_Success(string channelKey) + { + var input = + $""" + asyncapi: 2.6.0 + info: + title: Chat Application + version: 1.0.0 + servers: + testing: + url: test.mosquitto.org:1883 + protocol: mqtt + description: Test broker + channels: + {channelKey}: + publish: + operationId: onMessageReceieved + message: + name: text + payload: + type: string + subscribe: + operationId: sendMessage + message: + name: text + payload: + type: string + """; -} + new AsyncApiStringReader().Read(input, out var diagnostic); + diagnostic.Errors.Should().BeEmpty(); + } +} \ No newline at end of file diff --git a/test/ByteBard.AsyncAPI.Tests/Validation/ValidationRulesetTests.cs b/test/ByteBard.AsyncAPI.Tests/Validation/ValidationRulesetTests.cs index ec5902b..b3f9ec0 100644 --- a/test/ByteBard.AsyncAPI.Tests/Validation/ValidationRulesetTests.cs +++ b/test/ByteBard.AsyncAPI.Tests/Validation/ValidationRulesetTests.cs @@ -6,7 +6,7 @@ public class ValidationRuleSetTests { [Test] - public void DefaultRuleSet_ReturnsTheCorrectRules() + public void V2_DefaultRuleSet_ReturnsTheCorrectRules() { // Arrange var ruleSet = new ValidationRuleSet(); @@ -20,7 +20,7 @@ public void DefaultRuleSet_ReturnsTheCorrectRules() } [Test] - public void DefaultRuleSet_PropertyReturnsTheCorrectRules() + public void V2_DefaultRuleSet_PropertyReturnsTheCorrectRules() { // Arrange & Act var ruleSet = ValidationRuleSet.GetDefaultRuleSet(); @@ -33,7 +33,7 @@ public void DefaultRuleSet_PropertyReturnsTheCorrectRules() Assert.IsNotEmpty(rules); // Update the number if you add new default rule(s). - Assert.AreEqual(18, rules.Count); + Assert.AreEqual(26, rules.Count); } } }