From bb3dbba6136eb00da6ba1c599e9e634441e71256 Mon Sep 17 00:00:00 2001 From: Gennady Pundikov Date: Mon, 3 Mar 2025 12:11:01 +0300 Subject: [PATCH 1/3] Support AsyncApi3 --- src/ApiCodeGenerator.AsyncApi/AcgExtension.cs | 2 +- .../Amqp/CSharp/CSharpAmqpServiceGenerator.cs | 4 +- .../CSharp/Models/CSharpAmqpOperationModel.cs | 8 +- .../AsyncApiContentGenerator.cs | 184 ++++++------- .../AsyncApiGeneratorBase.cs | 53 ++-- .../CSharp/CSharpGeneratorBase.cs | 38 ++- .../CSharp/Models/CSharpOperationModel.cs | 42 ++- .../CSharp/Models/CSharpParameterModel.cs | 6 +- .../ClientGeneratorBaseSettings.cs | 60 +++-- .../DOM/AsyncApiDocument.cs | 122 +++------ .../DOM/AsyncApiSchema.cs | 11 + .../DOM/Bindings/Amqp/Channel.cs | 6 +- .../DOM/Bindings/Amqp/Message.cs | 2 +- .../DOM/Bindings/Amqp/OperationBase.cs | 2 +- src/ApiCodeGenerator.AsyncApi/DOM/Channel.cs | 54 ++-- .../DOM/ChannelBindings.cs | 2 +- .../DOM/Components.cs | 70 ++++- src/ApiCodeGenerator.AsyncApi/DOM/Contact.cs | 6 +- .../DOM/CorrelationId.cs | 14 + .../DOM/ExtensionRefObject.cs | 25 ++ .../DOM/ExternalDocumentation.cs | 24 +- src/ApiCodeGenerator.AsyncApi/DOM/Info.cs | 61 +++-- .../DOM/Internal/NamedReferenceDictionary.cs | 99 +++++++ src/ApiCodeGenerator.AsyncApi/DOM/License.cs | 19 +- src/ApiCodeGenerator.AsyncApi/DOM/Message.cs | 63 ++++- .../DOM/MessageBindings.cs | 2 +- .../DOM/MessageExample.cs | 19 ++ .../DOM/NamedReference.cs | 13 + .../DOM/Operation.cs | 54 ++-- .../DOM/OperationAction.cs | 10 + .../DOM/OperationBindings.cs | 2 +- .../DOM/OperationReply.cs | 16 ++ .../DOM/OperationReplyAddress.cs | 12 + .../DOM/Parameter.cs | 22 +- .../DOM/RefObject.cs | 28 +- .../DOM/Reference.cs | 48 ++++ .../Security/ApiKeySecuritySchemaLocations.cs | 8 + .../DOM/Security/ApiKeySecurityScheme.cs | 16 ++ .../Security/AuthorizationCodeOAuthFlow.cs | 12 + .../Security/ClientCredentialsOAuthFlow.cs | 9 + .../HttpApiKeySecuritySchemaLocations.cs | 9 + .../DOM/Security/HttpApiKeySecurityScheme.cs | 19 ++ .../DOM/Security/HttpSecurityScheme.cs | 19 ++ .../DOM/Security/ImplicitOAuthFlow.cs | 9 + .../DOM/Security/OAuth2SecurityScheme.cs | 19 ++ .../DOM/Security/OAuthFlow.cs | 12 + .../DOM/Security/OAuthFlows.cs | 19 ++ .../Security/OpenIdConnectSecurityScheme.cs | 19 ++ .../DOM/Security/PasswordOAuthFlow.cs | 9 + .../DOM/Security/SecurityScheme.cs | 24 ++ .../DOM/SecurityRequirement.cs | 9 - .../Serialization/AsyncApiReferenceUpdater.cs | 86 ++++++ .../Serialization/AsyncApiSchemaConverter.cs | 60 +++++ .../Serialization}/AsyncApiSchemaResolver.cs | 11 +- .../DOM/Serialization/AsyncApiSerializer.cs | 87 +++++++ .../DOM/Serialization/IDocumentAware.cs | 6 + .../DOM/Serialization/InheritanceConverter.cs | 62 ++++- .../DOM/Serialization/RefObjectConverter.cs | 66 +++++ src/ApiCodeGenerator.AsyncApi/DOM/Server.cs | 45 ++-- .../DOM/ServerBindings.cs | 9 +- .../DOM/ServerVariable.cs | 23 +- src/ApiCodeGenerator.AsyncApi/DOM/Tag.cs | 21 +- .../DOM/Traits/ITraitsAware.cs | 11 + .../DOM/Traits/MessageTraits.cs | 74 ++++++ .../DOM/Traits/OperationTraits.cs | 50 ++++ .../DOM/Traits/Traits.cs | 28 ++ .../DOM/Traits/TraitsExtensions.cs | 25 ++ ...aultTemplateFactory.GetToolchainVersion.cs | 2 - .../DefaultParameterNameGenerator.cs | 4 +- .../IOperationNameGenerator.cs | 10 +- .../IParameterNameGenerator.cs | 6 +- ...ltipleClientsFromFirstTagAndOperationId.cs | 12 + .../ParameterNameGeneratorWithReplace.cs | 9 +- .../SingleClientFromOperationId.cs | 12 + ...ltipleClientsFromFirstTagAndOperationId.cs | 13 - .../SingleClientFromOperationId.cs | 13 - .../Converters/SettingsConverter.cs | 1 - .../DefaultTemplateFactory.cs | 1 - .../Helpers/SettingsHelpers.cs | 2 +- .../PropertyNameGeneratorWithReplace.cs | 4 +- .../AmqpFunctionalTests.cs | 9 +- .../ApiCodeGenerator.AsyncApi.Tests.csproj | 1 + .../AsyncApiContentGeneratorTests.cs | 42 +-- .../FunctionalTests.cs | 1 - .../GlobalUsings.cs | 4 + .../Infrastructure/DeepEqualHelper.cs | 8 + .../Infrastructure/FakeContentGenerator.cs | 1 - .../Infrastructure/FakeModelPreprocessor.cs | 9 +- .../Infrastructure/FakeTextPreprocessor.cs | 7 +- .../Infrastructure/TestHelpers.Amqp.cs | 30 +-- .../Infrastructure/TestHelpers.cs | 26 +- .../SerializationV3/AsyncApiDocumentTests.cs | 125 +++++++++ .../SerializationV3/ChannelBindingsTests.cs | 44 ++++ .../SerializationV3/ChannelTests.cs | 202 +++++++++++++++ .../SerializationV3/ContactTests.cs | 37 +++ .../SerializationV3/CorrelationIdTests.cs | 56 ++++ .../ExternalDocumentationTests.cs | 53 ++++ .../SerializationV3/InfoTests.cs | 97 +++++++ .../SerializationV3/LicenseTests.cs | 45 ++++ .../SerializationV3/MessageBindingsTests.cs | 44 ++++ .../SerializationV3/MessageExampleTests.cs | 64 +++++ .../SerializationV3/MessageTests.cs | 188 ++++++++++++++ .../SerializationV3/MessageTraitTests.cs | 171 ++++++++++++ .../SerializationV3/OAuthFlowsTests.cs | 192 ++++++++++++++ .../SerializationV3/OperationBindingsTests.cs | 44 ++++ .../OperationReplyAddressTests.cs | 60 +++++ .../SerializationV3/OperationReplyTests.cs | 133 ++++++++++ .../SerializationV3/OperationTraitTests.cs | 151 +++++++++++ .../SerializationV3/OpertationTests.cs | 244 ++++++++++++++++++ .../SerializationV3/ParameterTests.cs | 47 ++++ .../SerializationV3/SecuritySchemeTests.cs | 160 ++++++++++++ .../SerializationV3/ServerBindingsTests.cs | 44 ++++ .../SerializationV3/ServerTests.cs | 187 ++++++++++++++ .../SerializationV3/TagTests.cs | 78 ++++++ .../SerializationV3/TestBase.cs | 68 +++++ 115 files changed, 4207 insertions(+), 633 deletions(-) create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/AsyncApiSchema.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/CorrelationId.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/ExtensionRefObject.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Internal/NamedReferenceDictionary.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/MessageExample.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/NamedReference.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/OperationAction.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/OperationReply.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/OperationReplyAddress.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Reference.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Security/ApiKeySecuritySchemaLocations.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Security/ApiKeySecurityScheme.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Security/AuthorizationCodeOAuthFlow.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Security/ClientCredentialsOAuthFlow.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Security/HttpApiKeySecuritySchemaLocations.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Security/HttpApiKeySecurityScheme.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Security/HttpSecurityScheme.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Security/ImplicitOAuthFlow.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Security/OAuth2SecurityScheme.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Security/OAuthFlow.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Security/OAuthFlows.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Security/OpenIdConnectSecurityScheme.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Security/PasswordOAuthFlow.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Security/SecurityScheme.cs delete mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/SecurityRequirement.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiReferenceUpdater.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSchemaConverter.cs rename src/ApiCodeGenerator.AsyncApi/{ => DOM/Serialization}/AsyncApiSchemaResolver.cs (61%) create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSerializer.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Serialization/IDocumentAware.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Serialization/RefObjectConverter.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Traits/ITraitsAware.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Traits/MessageTraits.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Traits/OperationTraits.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Traits/Traits.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Traits/TraitsExtensions.cs rename src/ApiCodeGenerator.AsyncApi/{ => NameGenerators}/DefaultParameterNameGenerator.cs (87%) rename src/ApiCodeGenerator.AsyncApi/{OperationNameGenerators => NameGenerators}/IOperationNameGenerator.cs (57%) rename src/ApiCodeGenerator.AsyncApi/{ => NameGenerators}/IParameterNameGenerator.cs (66%) create mode 100644 src/ApiCodeGenerator.AsyncApi/NameGenerators/MultipleClientsFromFirstTagAndOperationId.cs rename src/ApiCodeGenerator.AsyncApi/{ => NameGenerators}/ParameterNameGeneratorWithReplace.cs (89%) create mode 100644 src/ApiCodeGenerator.AsyncApi/NameGenerators/SingleClientFromOperationId.cs delete mode 100644 src/ApiCodeGenerator.AsyncApi/OperationNameGenerators/MultipleClientsFromFirstTagAndOperationId.cs delete mode 100644 src/ApiCodeGenerator.AsyncApi/OperationNameGenerators/SingleClientFromOperationId.cs create mode 100644 test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/DeepEqualHelper.cs create mode 100644 test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/AsyncApiDocumentTests.cs create mode 100644 test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ChannelBindingsTests.cs create mode 100644 test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ChannelTests.cs create mode 100644 test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ContactTests.cs create mode 100644 test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/CorrelationIdTests.cs create mode 100644 test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ExternalDocumentationTests.cs create mode 100644 test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/InfoTests.cs create mode 100644 test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/LicenseTests.cs create mode 100644 test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/MessageBindingsTests.cs create mode 100644 test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/MessageExampleTests.cs create mode 100644 test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/MessageTests.cs create mode 100644 test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/MessageTraitTests.cs create mode 100644 test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OAuthFlowsTests.cs create mode 100644 test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OperationBindingsTests.cs create mode 100644 test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OperationReplyAddressTests.cs create mode 100644 test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OperationReplyTests.cs create mode 100644 test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OperationTraitTests.cs create mode 100644 test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OpertationTests.cs create mode 100644 test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ParameterTests.cs create mode 100644 test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/SecuritySchemeTests.cs create mode 100644 test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ServerBindingsTests.cs create mode 100644 test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ServerTests.cs create mode 100644 test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/TagTests.cs create mode 100644 test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/TestBase.cs diff --git a/src/ApiCodeGenerator.AsyncApi/AcgExtension.cs b/src/ApiCodeGenerator.AsyncApi/AcgExtension.cs index 6cec70c..7d65fc6 100644 --- a/src/ApiCodeGenerator.AsyncApi/AcgExtension.cs +++ b/src/ApiCodeGenerator.AsyncApi/AcgExtension.cs @@ -1,6 +1,6 @@ using ApiCodeGenerator.Abstraction; using ApiCodeGenerator.AsyncApi.CSharp; -using ApiCodeGenerator.AsyncApi.OperationNameGenerators; +using ApiCodeGenerator.AsyncApi.NameGenerators; namespace ApiCodeGenerator.AsyncApi { diff --git a/src/ApiCodeGenerator.AsyncApi/Amqp/CSharp/CSharpAmqpServiceGenerator.cs b/src/ApiCodeGenerator.AsyncApi/Amqp/CSharp/CSharpAmqpServiceGenerator.cs index a17242c..e91f708 100644 --- a/src/ApiCodeGenerator.AsyncApi/Amqp/CSharp/CSharpAmqpServiceGenerator.cs +++ b/src/ApiCodeGenerator.AsyncApi/Amqp/CSharp/CSharpAmqpServiceGenerator.cs @@ -32,8 +32,8 @@ protected override IEnumerable GenerateClientTypes(string classNam .Append(poolArtifact); } - protected override CSharpOperationModel CreateOperationModel(string name, string channelPath, Channel channel, Operation operation) - => new CSharpAmqpOperationModel(name, channelPath, channel, operation, Settings, Resolver); + protected override CSharpOperationModel CreateOperationModel(string name, Operation operation) + => new CSharpAmqpOperationModel(name, operation, Settings, Resolver); protected CodeArtifact GenerateChannelPool(string className, CSharpOperationModel[] operations) { diff --git a/src/ApiCodeGenerator.AsyncApi/Amqp/CSharp/Models/CSharpAmqpOperationModel.cs b/src/ApiCodeGenerator.AsyncApi/Amqp/CSharp/Models/CSharpAmqpOperationModel.cs index 8a73e8c..bd86c23 100644 --- a/src/ApiCodeGenerator.AsyncApi/Amqp/CSharp/Models/CSharpAmqpOperationModel.cs +++ b/src/ApiCodeGenerator.AsyncApi/Amqp/CSharp/Models/CSharpAmqpOperationModel.cs @@ -9,14 +9,12 @@ public class CSharpAmqpOperationModel : CSharpOperationModel { public CSharpAmqpOperationModel( string name, - string channelPath, - DOM.Channel channel, Operation operation, CSharpGeneratorBaseSettings settings, CSharpTypeResolver typeResolver) - : base(name, channelPath, channel, operation, settings, typeResolver) + : base(name, operation, settings, typeResolver) { - var channelBinding = channel.Bindings?.Amqp?.ActualObject; + var channelBinding = operation.Channel.ActualObject.Bindings?.ActualObject.Amqp; if (channelBinding != null) { var exchange = channelBinding.Exchange; @@ -44,7 +42,7 @@ public CSharpAmqpOperationModel( } } - var operationBinding = operation.Bindings?.Amqp?.ActualObject; + var operationBinding = operation.Bindings?.ActualObject.Amqp; if (operationBinding != null) { Bcc = operationBinding.Bcc.ToArray(); diff --git a/src/ApiCodeGenerator.AsyncApi/AsyncApiContentGenerator.cs b/src/ApiCodeGenerator.AsyncApi/AsyncApiContentGenerator.cs index 4c81399..3c681c6 100644 --- a/src/ApiCodeGenerator.AsyncApi/AsyncApiContentGenerator.cs +++ b/src/ApiCodeGenerator.AsyncApi/AsyncApiContentGenerator.cs @@ -2,131 +2,131 @@ using ApiCodeGenerator.Abstraction; using ApiCodeGenerator.AsyncApi.CSharp; using ApiCodeGenerator.AsyncApi.DOM; +using ApiCodeGenerator.AsyncApi.DOM.Serialization; using ApiCodeGenerator.Core.Converters; using Newtonsoft.Json; -namespace ApiCodeGenerator.AsyncApi +namespace ApiCodeGenerator.AsyncApi; + +public abstract class AsyncApiContentGenerator : IContentGenerator + where TContentGenerator : AsyncApiContentGenerator, new() + where TGenerator : CSharpGeneratorBase + where TSettings : CSharpGeneratorBaseSettings, new() { - public abstract class AsyncApiContentGenerator : IContentGenerator - where TContentGenerator : AsyncApiContentGenerator, new() - where TGenerator : CSharpGeneratorBase - where TSettings : CSharpGeneratorBaseSettings, new() - { - internal static readonly string[] UNWRAP_PROPS = [ - nameof(CSharpGeneratorBaseSettings.CSharpGeneratorSettings), - ]; + internal static readonly string[] UNWRAP_PROPS = [ + nameof(CSharpGeneratorBaseSettings.CSharpGeneratorSettings), + ]; - protected TGenerator Generator { get; private set; } = null!; + protected TGenerator Generator { get; private set; } = null!; - protected TSettings Settings { get; private set; } = null!; + protected TSettings Settings { get; private set; } = null!; - public static async Task CreateAsync(GeneratorContext context) - { - var document = await LoadDocumentAsync(context); - var variables = GetAdditionalVariables(document); - var settings = LoadSettings(context, variables); - var resolver = CSharpGeneratorBase.CreateResolver(document, settings); + public static async Task CreateAsync(GeneratorContext context) + { + var document = await LoadDocumentAsync(context); + var variables = GetAdditionalVariables(document); + var settings = LoadSettings(context, variables); + var resolver = CSharpGeneratorBase.CreateResolver(document, settings); - var generator = (TGenerator)Activator.CreateInstance(typeof(TGenerator), document, settings, resolver); + var generator = (TGenerator)Activator.CreateInstance(typeof(TGenerator), document, settings, resolver); - var contentGenerator = new TContentGenerator() - { - Generator = generator, - Settings = settings, - }; - return contentGenerator; - } + var contentGenerator = new TContentGenerator() + { + Generator = generator, + Settings = settings, + }; + return contentGenerator; + } - public virtual string Generate() => Generator.Generate(); + public virtual string Generate() => Generator.Generate(); - [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "Удобнее рядом с использующим кодом")] - private static readonly Regex SEM_VER_PARSER = new( - @"^(?\d+)\.(?\d+)\.(?\d+)(?:-(?[0-9A-Za-z-]+))?(?:\+(?[0-9A-Za-z-]+))?$", - RegexOptions.Singleline | RegexOptions.Compiled); + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "Удобнее рядом с использующим кодом")] + private static readonly Regex SEM_VER_PARSER = new( + @"^(?\d+)\.(?\d+)\.(?\d+)(?:-(?[0-9A-Za-z-]+))?(?:\+(?[0-9A-Za-z-]+))?$", + RegexOptions.Singleline | RegexOptions.Compiled); - protected static IReadOnlyDictionary? GetAdditionalVariables(AsyncApiDocument apiDocument) + protected static IReadOnlyDictionary? GetAdditionalVariables(AsyncApiDocument apiDocument) + { + var version = apiDocument.Info?.Version; + if (!string.IsNullOrEmpty(version)) { - var version = apiDocument.Info?.Version; - if (!string.IsNullOrEmpty(version)) + var match = SEM_VER_PARSER.Match(version); + if (match.Success) { - var match = SEM_VER_PARSER.Match(version); - if (match.Success) + return new Dictionary { - return new Dictionary - { - ["Version.Major"] = match.Groups["major"].Value, - ["Version.Minor"] = match.Groups["minor"].Value, - ["Version.Patch"] = match.Groups["patch"].Value, - ["Version.Prerelease"] = match.Groups["Prerelease"].Value, - ["Version.Build"] = match.Groups["buildmetadata"].Value, - }; - } + ["Version.Major"] = match.Groups["major"].Value, + ["Version.Minor"] = match.Groups["minor"].Value, + ["Version.Patch"] = match.Groups["patch"].Value, + ["Version.Prerelease"] = match.Groups["Prerelease"].Value, + ["Version.Build"] = match.Groups["buildmetadata"].Value, + }; } - - return null; } - protected static T InvokePreprocessors(T data, - Preprocessors? preprocessors, - string? filePath, - ILogger? logger) + return null; + } + + protected static T InvokePreprocessors(T data, + Preprocessors? preprocessors, + string? filePath, + ILogger? logger) + { + if (preprocessors?.TryGetValue(typeof(T), out var documentPreprocessors) == true) { - if (preprocessors?.TryGetValue(typeof(T), out var documentPreprocessors) == true) + foreach (var processor in documentPreprocessors) { - foreach (var processor in documentPreprocessors) + data = processor switch { - data = processor switch - { - Func p => p.Invoke(data, filePath), - Func p => p.Invoke(data, filePath, logger), - _ => data, - }; - } + Func p => p.Invoke(data, filePath), + Func p => p.Invoke(data, filePath, logger), + _ => data, + }; } - - return data; } - private static async Task LoadDocumentAsync(GeneratorContext context) - { - var data = await context.DocumentReader!.ReadToEndAsync(); - data = InvokePreprocessors(data, context.Preprocessors, context.DocumentPath, context.Logger); + return data; + } + + private static async Task LoadDocumentAsync(GeneratorContext context) + { + var data = await context.DocumentReader!.ReadToEndAsync(); + data = InvokePreprocessors(data, context.Preprocessors, context.DocumentPath, context.Logger); - AsyncApiDocument document; + AsyncApiDocument document; + try + { + document = await AsyncApiSerializer.FromJsonAsync(data, context.DocumentPath).ConfigureAwait(false); + } + catch (JsonException ex) + { try { - document = await AsyncApiDocument.FromJsonAsync(data, context.DocumentPath).ConfigureAwait(false); + document = await AsyncApiSerializer.FromYamlAsync(data, context.DocumentPath).ConfigureAwait(false); } - catch (JsonException ex) + catch (YamlDotNet.Core.YamlException ex2) { - try - { - document = await AsyncApiDocument.FromYamlAsync(data, context.DocumentPath).ConfigureAwait(false); - } - catch (YamlDotNet.Core.YamlException ex2) - { - throw new InvalidOperationException( - $"Can not read document as JSON ({ex.Message}) or YAML ({ex2.Message})."); - } + throw new InvalidOperationException( + $"Can not read document as JSON ({ex.Message}) or YAML ({ex2.Message})."); } - - document = InvokePreprocessors(document, context.Preprocessors, context.DocumentPath, context.Logger); - return document; } - private static TSettings LoadSettings(GeneratorContext context, IReadOnlyDictionary? variables) + document = InvokePreprocessors(document, context.Preprocessors, context.DocumentPath, context.Logger); + return document; + } + + private static TSettings LoadSettings(GeneratorContext context, IReadOnlyDictionary? variables) + { + var serializer = new JsonSerializer { - var serializer = new JsonSerializer + Converters = { - Converters = - { - new SettingsConverter( - typeof(TSettings), - UNWRAP_PROPS, - (s, p, v) => Helpers.SettingsHelpers.SetSpecialSettings(context.Extensions, (ClientGeneratorBaseSettings)s, p, v)), - }, - }; - return context.GetSettings(serializer, variables) ?? new(); - } + new SettingsConverter( + typeof(TSettings), + UNWRAP_PROPS, + (s, p, v) => Helpers.SettingsHelpers.SetSpecialSettings(context.Extensions, (ClientGeneratorBaseSettings)s, p, v)), + }, + }; + return context.GetSettings(serializer, variables) ?? new(); } } diff --git a/src/ApiCodeGenerator.AsyncApi/AsyncApiGeneratorBase.cs b/src/ApiCodeGenerator.AsyncApi/AsyncApiGeneratorBase.cs index 5fddd8f..3301098 100644 --- a/src/ApiCodeGenerator.AsyncApi/AsyncApiGeneratorBase.cs +++ b/src/ApiCodeGenerator.AsyncApi/AsyncApiGeneratorBase.cs @@ -1,40 +1,39 @@ using ApiCodeGenerator.AsyncApi.DOM; using NJsonSchema.CodeGeneration; -namespace ApiCodeGenerator.AsyncApi +namespace ApiCodeGenerator.AsyncApi; + +public abstract class AsyncApiGeneratorBase { - public abstract class AsyncApiGeneratorBase + public AsyncApiGeneratorBase(AsyncApiDocument document, ClientGeneratorBaseSettings settings, TypeResolverBase resolver) { - public AsyncApiGeneratorBase(AsyncApiDocument document, ClientGeneratorBaseSettings settings, TypeResolverBase resolver) - { - Document = document; - BaseSettings = settings; - Resolver = resolver; - } - - protected ClientGeneratorBaseSettings BaseSettings { get; private set; } + Document = document; + BaseSettings = settings; + Resolver = resolver; + } - protected AsyncApiDocument Document { get; } + protected ClientGeneratorBaseSettings BaseSettings { get; private set; } - protected TypeResolverBase Resolver { get; } + protected AsyncApiDocument Document { get; } - public string Generate() - { - var clientTypes = GenerateAllClientTypes() - .Where(ca => ca.Type != CodeArtifactType.Class || BaseSettings.GenerateClientClasses) - .Where(ca => ca.Type != CodeArtifactType.Interface || BaseSettings.GenerateClientInterfaces) - .ToArray(); - var dtoTypes = BaseSettings.GenerateDtoTypes - ? GenerateDtoTypes().ToArray() - : []; + protected TypeResolverBase Resolver { get; } - return GenerateFile(clientTypes, dtoTypes, ClientGeneratorOutputType.Full); - } + public string Generate() + { + var clientTypes = GenerateAllClientTypes() + .Where(ca => ca.Type != CodeArtifactType.Class || BaseSettings.GenerateClientClasses) + .Where(ca => ca.Type != CodeArtifactType.Interface || BaseSettings.GenerateClientInterfaces) + .ToArray(); + var dtoTypes = BaseSettings.GenerateDtoTypes + ? GenerateDtoTypes().ToArray() + : []; + + return GenerateFile(clientTypes, dtoTypes, ClientGeneratorOutputType.Full); + } - protected abstract IEnumerable GenerateAllClientTypes(); + protected abstract IEnumerable GenerateAllClientTypes(); - protected abstract IEnumerable GenerateDtoTypes(); + protected abstract IEnumerable GenerateDtoTypes(); - protected abstract string GenerateFile(IEnumerable clientTypes, IEnumerable dtoTypes, ClientGeneratorOutputType outputType); - } + protected abstract string GenerateFile(IEnumerable clientTypes, IEnumerable dtoTypes, ClientGeneratorOutputType outputType); } diff --git a/src/ApiCodeGenerator.AsyncApi/CSharp/CSharpGeneratorBase.cs b/src/ApiCodeGenerator.AsyncApi/CSharp/CSharpGeneratorBase.cs index 2e27db4..4156925 100644 --- a/src/ApiCodeGenerator.AsyncApi/CSharp/CSharpGeneratorBase.cs +++ b/src/ApiCodeGenerator.AsyncApi/CSharp/CSharpGeneratorBase.cs @@ -1,5 +1,6 @@ using ApiCodeGenerator.AsyncApi.CSharp.Models; using ApiCodeGenerator.AsyncApi.DOM; +using NJsonSchema; using NJsonSchema.CodeGeneration; using NJsonSchema.CodeGeneration.CSharp; @@ -22,12 +23,14 @@ protected CSharpGeneratorBase(AsyncApiDocument document, CSharpGeneratorBaseSett public static TypeResolverBase CreateResolver(AsyncApiDocument apiDocument, TSettings settings) { var schemas = apiDocument.Components?.Schemas - ?? new Dictionary(); + ?? new Dictionary(); schemas.TryGetValue("Exception", out var exceptionSchema); var resolver = new CSharpTypeResolver(settings.CSharpGeneratorSettings, exceptionSchema); - resolver.RegisterSchemaDefinitions(exceptionSchema is null - ? schemas - : schemas.Where(i => i.Value != exceptionSchema).ToDictionary(i => i.Key, i => i.Value)); + resolver.RegisterSchemaDefinitions( + (exceptionSchema is null + ? schemas + : schemas.Where(i => i.Value != exceptionSchema)) + .ToDictionary(i => i.Key, i => (JsonSchema)i.Value)); return resolver; } @@ -85,26 +88,15 @@ protected override IEnumerable GenerateDtoTypes() protected IEnumerable CreateOperationModels() { - if (Document.Channels is not null) + foreach (var operation in Document.Operations) { - foreach (var channel in Document.Channels) - { - if (channel.Value.Publish is not null && Settings.OperationTypes.HasFlag(OperationTypes.Publish)) - { - yield return CreateOperationModelInternal(channel.Key, channel.Value, channel.Value.Publish); - } - - if (channel.Value.Subscribe is not null && Settings.OperationTypes.HasFlag(OperationTypes.Subscribe)) - { - yield return CreateOperationModelInternal(channel.Key, channel.Value, channel.Value.Subscribe); - } - } + yield return CreateOperationModelInternal(operation.Value); } } - protected virtual CSharpOperationModel CreateOperationModel(string name, string channelName, Channel channel, Operation operation) + protected virtual CSharpOperationModel CreateOperationModel(string name, Operation operation) { - return new CSharpOperationModel(name, channelName, channel, operation, Settings, Resolver); + return new CSharpOperationModel(name, operation, Settings, Resolver); } protected override string GenerateFile(IEnumerable clientTypes, IEnumerable dtoTypes, ClientGeneratorOutputType outputType) @@ -135,11 +127,11 @@ protected virtual void FillDisabledWarningsList(IDictionary warn { } - private CSharpOperationModel CreateOperationModelInternal(string channelPath, Channel channel, Operation operation) + private CSharpOperationModel CreateOperationModelInternal(NamedReference operation) { - var operationName = Settings.OperationNameGenerator.GetOperationName(Document, channelPath, channel.Subscribe == operation, operation); - var operationModel = CreateOperationModel(operationName, channelPath, channel, operation); - operationModel.ControllerName = Settings.OperationNameGenerator.GetClientName(Document, channelPath, channel.Subscribe == operation, operation); + var operationName = Settings.OperationNameGenerator.GetOperationName(Document, operation); + var operationModel = CreateOperationModel(operationName, operation); + operationModel.ControllerName = Settings.OperationNameGenerator.GetClientName(Document, operation); return operationModel; } } diff --git a/src/ApiCodeGenerator.AsyncApi/CSharp/Models/CSharpOperationModel.cs b/src/ApiCodeGenerator.AsyncApi/CSharp/Models/CSharpOperationModel.cs index 9a7ce72..f7b0b66 100644 --- a/src/ApiCodeGenerator.AsyncApi/CSharp/Models/CSharpOperationModel.cs +++ b/src/ApiCodeGenerator.AsyncApi/CSharp/Models/CSharpOperationModel.cs @@ -11,23 +11,25 @@ public class CSharpOperationModel public CSharpOperationModel( string operationName, - string channelName, - Channel channel, Operation operation, CSharpGeneratorBaseSettings settings, CSharpTypeResolver typeResolver) { - ChannelName = channelName; - Channel = channel; + Channel = operation.Channel.ActualObject; + ChannelAddress = Channel.Address; Operation = operation; OperationName = ConversionUtilities.ConvertToUpperCamelCase(operationName, true); _typeResolver = typeResolver; - Parameters = Channel.Parameters - .Select(cp => + + var actualParameters = Channel.Parameters?.Values.ToArray() ?? []; + Parameters = actualParameters + .Select((cp, ind) => new CSharpParameterModel( - settings.ParameterNameGenerator.Generate(cp.Key, cp.Value, Channel.Parameters.Values), - cp.Value, - ResolveParameterType(cp.Key, cp.Value))) + settings.ParameterNameGenerator.Generate( + cp.ObjectId ?? $"param{ind}", + cp, + actualParameters), + cp)) .ToArray(); Description = !string.IsNullOrEmpty(operation.Summary) @@ -36,7 +38,7 @@ public CSharpOperationModel( HasDescription = !string.IsNullOrEmpty(Description); } - public string ChannelName { get; } + public string? ChannelAddress { get; } public string ControllerName { get; set; } = string.Empty; @@ -44,15 +46,13 @@ public CSharpOperationModel( public bool HasDescription { get; } - public bool HasPublish => Channel.Publish == Operation; - - public string OperationId => Operation.OperationId ?? string.Empty; + public bool HasPublish => Operation.Action == OperationAction.Send; public string OperationName { get; } public CSharpParameterModel[] Parameters { get; } - public string PayloadType => _payloadType ??= ResolvePayloadType(Operation.Message.ActualObject.Payload.ActualSchema, hint: null); + public string PayloadType => _payloadType ??= ResolvePayloadType(Operation.Messages.First().ActualObject.Payload?.ActualObject.ActualSchema, hint: null); protected Channel Channel { get; } @@ -62,21 +62,9 @@ protected virtual string ResolvePayloadType(JsonSchema jsonSchema, string? hint) { if (!jsonSchema.HasTypeNameTitle && string.IsNullOrEmpty(hint)) { - hint = ConversionUtilities.ConvertToUpperCamelCase($"{Operation.Message.ActualObject.Name}Payload", false); + hint = ConversionUtilities.ConvertToUpperCamelCase($"{Operation.Messages.First().ActualObject.Name}Payload", false); } return _typeResolver.Resolve(jsonSchema, false, hint); } - - protected virtual string ResolveParameterType(string parameterName, Parameter operationParameter) - { - var schema = operationParameter.ActualObject.Schema.ActualSchema; - var typeNameHint = !schema.HasTypeNameTitle - ? parameterName - : null; - - var isNullable = schema.IsNullable(SchemaType.OpenApi3); - - return _typeResolver.Resolve(schema, isNullable, typeNameHint); - } } diff --git a/src/ApiCodeGenerator.AsyncApi/CSharp/Models/CSharpParameterModel.cs b/src/ApiCodeGenerator.AsyncApi/CSharp/Models/CSharpParameterModel.cs index 064d08a..93e591f 100644 --- a/src/ApiCodeGenerator.AsyncApi/CSharp/Models/CSharpParameterModel.cs +++ b/src/ApiCodeGenerator.AsyncApi/CSharp/Models/CSharpParameterModel.cs @@ -1,6 +1,5 @@ using ApiCodeGenerator.AsyncApi.DOM; using NJsonSchema; -using NJsonSchema.CodeGeneration.CSharp; namespace ApiCodeGenerator.AsyncApi.CSharp.Models; @@ -9,14 +8,11 @@ public class CSharpParameterModel private readonly string _parameterName; private readonly Parameter _parameter; - public CSharpParameterModel(string parameterName, Parameter parameter, string parameterType) + public CSharpParameterModel(string parameterName, Parameter parameter) { _parameterName = parameterName; _parameter = parameter; - ParameterType = parameterType; } public string CamelCaseParameterName => ConversionUtilities.ConvertToLowerCamelCase(_parameterName, true); - - public string ParameterType { get; } } diff --git a/src/ApiCodeGenerator.AsyncApi/ClientGeneratorBaseSettings.cs b/src/ApiCodeGenerator.AsyncApi/ClientGeneratorBaseSettings.cs index 607d25e..c294b08 100644 --- a/src/ApiCodeGenerator.AsyncApi/ClientGeneratorBaseSettings.cs +++ b/src/ApiCodeGenerator.AsyncApi/ClientGeneratorBaseSettings.cs @@ -1,46 +1,44 @@ -using ApiCodeGenerator.AsyncApi.OperationNameGenerators; -using NJsonSchema; +using ApiCodeGenerator.AsyncApi.NameGenerators; using NJsonSchema.CodeGeneration; -namespace ApiCodeGenerator.AsyncApi +namespace ApiCodeGenerator.AsyncApi; + +/// Settings for the ClientGeneratorBase. +public abstract class ClientGeneratorBaseSettings { - /// Settings for the ClientGeneratorBase. - public abstract class ClientGeneratorBaseSettings + /// Initializes a new instance of the class. + protected ClientGeneratorBaseSettings() { - /// Initializes a new instance of the class. - protected ClientGeneratorBaseSettings() - { - OperationNameGenerator = new SingleClientFromOperationId(); - ParameterNameGenerator = new DefaultParameterNameGenerator(); + OperationNameGenerator = new SingleClientFromOperationId(); + ParameterNameGenerator = new DefaultParameterNameGenerator(); - ExcludedParameterNames = new string[0]; - } + ExcludedParameterNames = new string[0]; + } - /// Gets the code generator settings. - public abstract CodeGeneratorSettingsBase CodeGeneratorSettings { get; } + /// Gets the code generator settings. + public abstract CodeGeneratorSettingsBase CodeGeneratorSettings { get; } - /// Gets or sets the class name of the service client or controller. - public string ClassName { get; set; } = "Client"; + /// Gets or sets the class name of the service client or controller. + public string ClassName { get; set; } = "Client"; - /// Gets or sets a value indicating whether to generate DTO classes (default: true). - public bool GenerateDtoTypes { get; set; } = true; + /// Gets or sets a value indicating whether to generate DTO classes (default: true). + public bool GenerateDtoTypes { get; set; } = true; - /// Gets or sets a value indicating whether to generate interfaces for the client classes (default: false). - public bool GenerateClientInterfaces { get; set; } + /// Gets or sets a value indicating whether to generate interfaces for the client classes (default: false). + public bool GenerateClientInterfaces { get; set; } - /// Gets or sets a value indicating whether to generate client types (default: true). - public bool GenerateClientClasses { get; set; } = true; + /// Gets or sets a value indicating whether to generate client types (default: true). + public bool GenerateClientClasses { get; set; } = true; - /// Gets or sets the operation name generator. - public IOperationNameGenerator OperationNameGenerator { get; set; } + /// Gets or sets the operation name generator. + public IOperationNameGenerator OperationNameGenerator { get; set; } - /// Gets or sets a value indicating whether to reorder parameters (required first, optional at the end) and generate optional parameters. - public bool GenerateOptionalParameters { get; set; } + /// Gets or sets a value indicating whether to reorder parameters (required first, optional at the end) and generate optional parameters. + public bool GenerateOptionalParameters { get; set; } - /// Gets or sets the parameter name generator. - public IParameterNameGenerator ParameterNameGenerator { get; set; } + /// Gets or sets the parameter name generator. + public IParameterNameGenerator ParameterNameGenerator { get; set; } - /// Gets or sets the globally excluded parameter names. - public string[] ExcludedParameterNames { get; set; } - } + /// Gets or sets the globally excluded parameter names. + public string[] ExcludedParameterNames { get; set; } } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/AsyncApiDocument.cs b/src/ApiCodeGenerator.AsyncApi/DOM/AsyncApiDocument.cs index 940d204..6cb1676 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/AsyncApiDocument.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/AsyncApiDocument.cs @@ -1,105 +1,43 @@ using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using NJsonSchema; -using NJsonSchema.Generation; -using NJsonSchema.Yaml; -using YamlDotNet.Serialization; -namespace ApiCodeGenerator.AsyncApi.DOM -{ -#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - public class AsyncApiDocument : IDocumentPathProvider - { - private static readonly JsonSerializerSettings JSONSERIALIZERSETTINGS = new() - { - PreserveReferencesHandling = PreserveReferencesHandling.None, - MetadataPropertyHandling = MetadataPropertyHandling.Ignore, - ConstructorHandling = ConstructorHandling.Default, - ReferenceLoopHandling = ReferenceLoopHandling.Serialize, - }; - - [JsonProperty(PropertyName = "asyncApi", Order = 1, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public string AsyncApi { get; set; } - - [JsonProperty(PropertyName = "info", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public Info? Info { get; set; } - - [JsonProperty(PropertyName = "servers", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public IDictionary Servers { get; set; } +namespace ApiCodeGenerator.AsyncApi.DOM; - [JsonProperty("defaultContentType")] - public string? DefaultContentType { get; set; } - - [JsonProperty("channels", DefaultValueHandling = DefaultValueHandling.Populate)] - public IDictionary? Channels { get; set; } = new Dictionary(); - - [JsonProperty("components", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public virtual Components? Components { get; set; } - - [JsonProperty("tags", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public virtual ICollection? Tags { get; set; } +public class AsyncApiDocument : JsonExtensionObject, IDocumentPathProvider +{ + /// Specifies the AsyncAPI Specification version being used. + [JsonProperty(PropertyName = "asyncApi", Order = 1, Required = Required.Always)] + public string AsyncApi { get; set; } = "3.0.0"; - [JsonProperty("externalDocs", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public ICollection? ExternalDocs { get; set; } + /// Identifier of the application the AsyncAPI document is defining. + [JsonProperty(PropertyName = "id")] + public string? Id { get; set; } - [JsonIgnore] - public string? DocumentPath { get; set; } + /// Provides metadata about the API. The metadata can be used by the clients if needed. + [JsonProperty(PropertyName = "info", Required = Required.Always)] + public Info Info { get; set; } = new(); - /// - /// Load document from JSON text. - /// - /// JSON text. - /// AsyncApi document object model. - public static Task FromJsonAsync(string data) - => FromJsonAsync(data, null); + /// Provides connection details of servers. + [JsonProperty(PropertyName = "servers")] + public IDictionary>? Servers { get; set; } - /// - /// Load document from JSON text. - /// - /// JSON text. - /// Path to document. - /// AsyncApi document object model. - public static Task FromJsonAsync(string data, string? documentPath) - { - var document = JsonConvert.DeserializeObject(data, JSONSERIALIZERSETTINGS)!; - document.DocumentPath = documentPath; - return UpdateSchemaReferencesAsync(document); - } + /// Default content type to use when encoding/decoding a message's payload. + [JsonProperty("defaultContentType")] + public string? DefaultContentType { get; set; } - /// - /// Load document from YAML text. - /// - /// YAML text. - /// AsyncApi document object model. - public static Task FromYamlAsync(string data) - => FromYamlAsync(data, null); + /// The channels used by this application. + [JsonProperty("channels")] + public IDictionary> Channels { get; } = new Internal.NamedReferenceDictionary(); - /// - /// Load document from YAML text. - /// - /// YAML text. - /// Path to document. - /// AsyncApi document object model. - public static Task FromYamlAsync(string data, string? documentPath) - { - var deserializer = new DeserializerBuilder().Build(); - using var reader = new StringReader(data); - var yamlDocument = deserializer.Deserialize(reader)!; + /// The operations this application MUST implement. + [JsonProperty("operations")] + public IDictionary> Operations { get; } = new Internal.NamedReferenceDictionary(); - var jObject = JObject.FromObject(yamlDocument)!; - var serializer = JsonSerializer.Create(JSONSERIALIZERSETTINGS); - var doc = jObject.ToObject(serializer)!; - doc.DocumentPath = documentPath; - return UpdateSchemaReferencesAsync(doc); - } + /// An element to hold various reusable objects for the specification. + [JsonProperty("components", ObjectCreationHandling = ObjectCreationHandling.Reuse)] + public Components Components { get; set; } = new(); - private static async Task UpdateSchemaReferencesAsync(AsyncApiDocument document) - { - await JsonSchemaReferenceUtilities.UpdateSchemaReferencesAsync( - document, - new JsonAndYamlReferenceResolver(new AsyncApiSchemaResolver(document, new SystemTextJsonSchemaGeneratorSettings()))); - return document; - } - } -#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + /// + [JsonIgnore] + public string? DocumentPath { get; set; } } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/AsyncApiSchema.cs b/src/ApiCodeGenerator.AsyncApi/DOM/AsyncApiSchema.cs new file mode 100644 index 0000000..0ce38e2 --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/AsyncApiSchema.cs @@ -0,0 +1,11 @@ +using ApiCodeGenerator.AsyncApi.DOM.Serialization; +using Newtonsoft.Json; +using NJsonSchema; + +namespace ApiCodeGenerator.AsyncApi.DOM; + +[JsonConverter(typeof(AsyncApiSchemaConverter))] +public class AsyncApiSchema : JsonSchema +{ + public required string SchemaFormat { get; set; } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Bindings/Amqp/Channel.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Bindings/Amqp/Channel.cs index 356a7fd..e979205 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/Bindings/Amqp/Channel.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Bindings/Amqp/Channel.cs @@ -2,7 +2,7 @@ namespace ApiCodeGenerator.AsyncApi.DOM.Bindings.Amqp; -public class Channel : RefObject +public class Channel { private IDictionary? _additionalProperties; @@ -17,13 +17,13 @@ public class Channel : RefObject /// When is=routingKey, this object defines the exchange properties. /// [JsonProperty("exchange", Required = Required.DisallowNull, NullValueHandling = NullValueHandling.Ignore)] - public Exchange Exchange { get; set; } = default!; + public Exchange? Exchange { get; set; } /// /// When is=queue, this object defines the queue properties. /// [JsonProperty("queue", Required = Required.DisallowNull, NullValueHandling = NullValueHandling.Ignore)] - public Queue Queue { get; set; } = default!; + public Queue? Queue { get; set; } /// /// The version of this binding. If omitted, 'latest' MUST be assumed. diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Bindings/Amqp/Message.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Bindings/Amqp/Message.cs index 757dd06..284418d 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/Bindings/Amqp/Message.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Bindings/Amqp/Message.cs @@ -4,7 +4,7 @@ namespace ApiCodeGenerator.AsyncApi.DOM.Bindings.Amqp; /// This object contains information about the message representation in AMQP. /// [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.2.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] -public partial class Message : RefObject +public partial class Message { /// /// A MIME encoding for the message content. diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Bindings/Amqp/OperationBase.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Bindings/Amqp/OperationBase.cs index d016436..641387b 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/Bindings/Amqp/OperationBase.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Bindings/Amqp/OperationBase.cs @@ -9,7 +9,7 @@ namespace ApiCodeGenerator.AsyncApi.DOM.Bindings.Amqp; [Serialization.KnownType("0.2.0", typeof(OperationV0_2))] [Serialization.KnownType("0.3.0", typeof(OperationV0_3))] [Serialization.KnownType("latest", typeof(OperationV0_3))] -public abstract class OperationBase : RefObject +public abstract class OperationBase { /// /// TTL (Time-To-Live) for the message. It MUST be greater than or equal to zero. diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Channel.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Channel.cs index f947784..95ef0e0 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/Channel.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Channel.cs @@ -1,27 +1,47 @@ using Newtonsoft.Json; -namespace ApiCodeGenerator.AsyncApi.DOM +namespace ApiCodeGenerator.AsyncApi.DOM; + +public class Channel : ExtensionRefObject { - public class Channel - { - [JsonProperty("bindings", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public ChannelBindings? Bindings { get; set; } + /// An optional string representation of this channel's address. + /// The address is typically the "topic name", "routing key", "event type", or "path". + [JsonProperty("address")] + public string? Address { get; set; } + + /// A map of the messages that will be sent to this channel by any application at any time. + [JsonProperty("messages")] + public IDictionary> Messages { get; } = new Internal.NamedReferenceDictionary(); + + /// A human-friendly title for the channel. + [JsonProperty("title")] + public string? Title { get; set; } + + /// A short summary of the channel. + [JsonProperty("summary")] + public string? Summary { get; set; } - [JsonProperty("description", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public string? Description { get; set; } + /// An optional description of this channel. + [JsonProperty("description")] + public string? Description { get; set; } - [JsonProperty("parameters", DefaultValueHandling = DefaultValueHandling.Ignore)] - public IDictionary Parameters { get; set; } = new Dictionary(); + /// An array of $ref pointers to the definition of the servers in which this channel is available. + [JsonProperty("servers")] + public ICollection>? Servers { get; set; } - [JsonProperty("publish")] - public Operation? Publish { get; set; } + /// A map of the parameters included in the channel address. + [JsonProperty("parameters")] + public IDictionary>? Parameters { get; } = new Internal.NamedReferenceDictionary(); - [JsonProperty("subscribe", DefaultValueHandling = DefaultValueHandling.Ignore)] - public Operation? Subscribe { get; set; } + /// A list of tags for logical grouping of channels. + [JsonProperty("tags")] + public ICollection>? Tags { get; set; } - [JsonExtensionData] - public IDictionary ExtensionData { get; set; } = new Dictionary(); - } + /// Additional external documentation for this channel. + [JsonProperty("externalDocs")] + public Reference? ExternalDocs { get; set; } - // #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + /// A map where the keys describe the name of the protocol and the values describe protocol-specific definitions for the channe. + [JsonProperty("bindings")] + public Reference? Bindings { get; set; } } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/ChannelBindings.cs b/src/ApiCodeGenerator.AsyncApi/DOM/ChannelBindings.cs index 6f762b1..95626a2 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/ChannelBindings.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/ChannelBindings.cs @@ -1,6 +1,6 @@ namespace ApiCodeGenerator.AsyncApi.DOM; -public class ChannelBindings : RefObject +public class ChannelBindings : ExtensionRefObject { public Bindings.Amqp.Channel? Amqp { get; set; } } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Components.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Components.cs index d377d69..ac1a1b8 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/Components.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Components.cs @@ -1,23 +1,65 @@ +using ApiCodeGenerator.AsyncApi.DOM.Traits; using Newtonsoft.Json; using NJsonSchema; -namespace ApiCodeGenerator.AsyncApi.DOM +namespace ApiCodeGenerator.AsyncApi.DOM; + +public class Components : JsonExtensionObject { - public class Components - { - [JsonProperty("messages", DefaultValueHandling = DefaultValueHandling.Populate)] - public IDictionary Messages { get; } = new Dictionary(); + [JsonProperty("messages")] + public IDictionary>? Messages { get; set; } + + [JsonProperty("parameters")] + public IDictionary>? Parameters { get; set; } + + [JsonProperty("schemas")] + public IDictionary? Schemas { get; set; } + + [JsonProperty("servers")] + public IDictionary>? Servers { get; set; } + + [JsonProperty("serverVariables")] + public IDictionary>? ServerVariables { get; set; } + + [JsonProperty("channels")] + public IDictionary>? Channels { get; set; } + + [JsonProperty("operations")] + public IDictionary>? Operations { get; set; } + + [JsonProperty("securitySchemes")] + public IDictionary>? SecuritySchemes { get; set; } + + [JsonProperty("correlationIds")] + public IDictionary>? CorrelationIds { get; set; } + + [JsonProperty("replies")] + public IDictionary>? Replies { get; set; } + + [JsonProperty("replyAddresses")] + public IDictionary>? ReplyAddresses { get; set; } + + [JsonProperty("externalDocs")] + public IDictionary>? ExternalDocs { get; set; } + + [JsonProperty("tags")] + public IDictionary>? Tags { get; set; } + + [JsonProperty("operationTraits")] + public IDictionary>? OperationTraits { get; set; } + + [JsonProperty("messageTraits")] + public IDictionary>? MessageTraits { get; set; } - [JsonProperty("parameters", DefaultValueHandling = DefaultValueHandling.Populate)] - public IDictionary Parameters { get; } = new Dictionary(); + [JsonProperty("serverBindings")] + public IDictionary>? ServerBindings { get; set; } - [JsonProperty("schemas", DefaultValueHandling = DefaultValueHandling.Populate)] - public IDictionary Schemas { get; } = new Dictionary(); + [JsonProperty("channelBindings")] + public IDictionary>? ChannelBindings { get; set; } - [JsonProperty("servers", DefaultValueHandling = DefaultValueHandling.Populate)] - public IDictionary Servers { get; } = new Dictionary(); + [JsonProperty("operationBindings")] + public IDictionary>? OperationBindings { get; set; } - [JsonProperty("serverVariables", DefaultValueHandling = DefaultValueHandling.Populate)] - public IDictionary ServerVariables { get; } = new Dictionary(); - } + [JsonProperty("messageBindings")] + public IDictionary>? MessageBindings { get; set; } } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Contact.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Contact.cs index 7e7c74c..eb663d9 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/Contact.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Contact.cs @@ -6,14 +6,14 @@ namespace ApiCodeGenerator.AsyncApi.DOM; public class Contact : JsonExtensionObject { /// Gets or sets the name. - [JsonProperty(PropertyName = "name", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + [JsonProperty(PropertyName = "name")] public string? Name { get; set; } /// Gets or sets the contact URL. - [JsonProperty(PropertyName = "url", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + [JsonProperty(PropertyName = "url")] public string? Url { get; set; } /// Gets or sets the contact email. - [JsonProperty(PropertyName = "email", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + [JsonProperty(PropertyName = "email")] public string? Email { get; set; } } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/CorrelationId.cs b/src/ApiCodeGenerator.AsyncApi/DOM/CorrelationId.cs new file mode 100644 index 0000000..5f80ea4 --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/CorrelationId.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace ApiCodeGenerator.AsyncApi.DOM; + +public class CorrelationId : ExtensionRefObject +{ + /// An optional description of the identifier. + [JsonProperty("description")] + public string? Description { get; set; } + + /// A runtime expression that specifies the location of the correlation ID. + [JsonProperty("location", Required = Required.Always)] + public required string Location { get; set; } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/ExtensionRefObject.cs b/src/ApiCodeGenerator.AsyncApi/DOM/ExtensionRefObject.cs new file mode 100644 index 0000000..c98be5e --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/ExtensionRefObject.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; +using NJsonSchema; +using NJsonSchema.References; + +namespace ApiCodeGenerator.AsyncApi.DOM; + +/// +/// Base object for referenced objects and may be extended. +/// +public class ExtensionRefObject : JsonExtensionObject, IJsonReference +{ + [JsonIgnore] + IJsonReference IJsonReference.ActualObject => this; + + [JsonIgnore] + object? IJsonReference.PossibleRoot { get; } + + [JsonIgnore] + IJsonReference? IJsonReferenceBase.Reference { get; set; } + + string? IJsonReferenceBase.ReferencePath { get; set; } + + [JsonIgnore] + string? IDocumentPathProvider.DocumentPath { get; set; } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/ExternalDocumentation.cs b/src/ApiCodeGenerator.AsyncApi/DOM/ExternalDocumentation.cs index 44b95b3..de6ab50 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/ExternalDocumentation.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/ExternalDocumentation.cs @@ -1,8 +1,24 @@ using Newtonsoft.Json; -namespace ApiCodeGenerator.AsyncApi.DOM +namespace ApiCodeGenerator.AsyncApi.DOM; + +/// +/// External documentation. +/// +public class ExternalDocumentation : ExtensionRefObject { - public class ExternalDocumentation - { - } + /// + /// A short description of the target documentation. + /// + /// + /// CommonMark syntax can be used for rich text representation. + /// + [JsonProperty("description")] + public string? Description { get; set; } + + /// + /// The URL for the target documentation. + /// + [JsonProperty("url", Required = Required.Always)] + public required Uri Url { get; set; } } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Info.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Info.cs index 939c323..fb6c22e 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/Info.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Info.cs @@ -1,32 +1,39 @@ using Newtonsoft.Json; using NJsonSchema; -namespace ApiCodeGenerator.AsyncApi.DOM +namespace ApiCodeGenerator.AsyncApi.DOM; + +public class Info : JsonExtensionObject { - public class Info : JsonExtensionObject - { - /// Gets or sets the title. - [JsonProperty(PropertyName = "title", Required = Required.Always, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public string Title { get; set; } = "Swagger specification"; - - /// Gets or sets the description. - [JsonProperty(PropertyName = "description", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public string? Description { get; set; } - - /// Gets or sets the terms of service. - [JsonProperty(PropertyName = "termsOfService", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public string? TermsOfService { get; set; } - - /// Gets or sets the contact information. - [JsonProperty(PropertyName = "contact", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public Contact? Contact { get; set; } - - /// Gets or sets the license information. - [JsonProperty(PropertyName = "license", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public License? License { get; set; } - - /// Gets or sets the API version. - [JsonProperty(PropertyName = "version", Required = Required.Always, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public string Version { get; set; } = "1.0.0"; - } + /// Gets or sets the title. + [JsonProperty(PropertyName = "title", Required = Required.Always)] + public string Title { get; set; } = "Swagger specification"; + + /// Gets or sets the description. + [JsonProperty(PropertyName = "description")] + public string? Description { get; set; } + + /// Gets or sets the terms of service. + [JsonProperty(PropertyName = "termsOfService")] + public string? TermsOfService { get; set; } + + /// Gets or sets the contact information. + [JsonProperty(PropertyName = "contact")] + public Contact? Contact { get; set; } + + /// Gets or sets the license information. + [JsonProperty(PropertyName = "license")] + public License? License { get; set; } + + /// Gets or sets the API version. + [JsonProperty(PropertyName = "version", Required = Required.Always)] + public string Version { get; set; } = "1.0.0"; + + /// A list of tags used by the specification with additional metadata. + [JsonProperty("tags")] + public ICollection>? Tags { get; set; } + + /// Additional external documentation of the exposed API. + [JsonProperty("externalDocs")] + public Reference? ExternalDocs { get; set; } } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Internal/NamedReferenceDictionary.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Internal/NamedReferenceDictionary.cs new file mode 100644 index 0000000..cb0b58f --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Internal/NamedReferenceDictionary.cs @@ -0,0 +1,99 @@ +using System.Collections; +using NJsonSchema.References; + +namespace ApiCodeGenerator.AsyncApi.DOM.Internal; + +internal sealed class NamedReferenceDictionary : IDictionary>, IDictionary + where T : IJsonReference +{ + private readonly Dictionary> _dictionary = new(); + + public ICollection Keys => _dictionary.Keys; + + public ICollection> Values => _dictionary.Values; + + public int Count => _dictionary.Count; + + public bool IsReadOnly => ((ICollection>>)_dictionary).IsReadOnly; + + public bool IsFixedSize => ((IDictionary)_dictionary).IsFixedSize; + + public bool IsSynchronized => ((ICollection)_dictionary).IsSynchronized; + + public object SyncRoot => ((ICollection)_dictionary).SyncRoot; + + ICollection IDictionary.Keys => ((IDictionary)_dictionary).Keys; + + ICollection IDictionary.Values => ((IDictionary)_dictionary).Values; + + public NamedReference this[string key] + { + get => _dictionary[key]; + set + { + _dictionary[key] = value; + value.ObjectId = key; + } + } + + public object this[object key] + { + get => ((IDictionary)_dictionary)[key]; + set + { + ((IDictionary)_dictionary)[key] = value; + if (value is NamedReference n) + { + n.ObjectId = key.ToString(); + } + } + } + + public void Add(string key, NamedReference value) + { + _dictionary.Add(key, value); + value.ObjectId = key; + } + + public void Add(KeyValuePair> item) + { + ((ICollection>>)_dictionary).Add(item); + item.Value.ObjectId = item.Key; + } + + public void Add(object key, object value) + { + ((IDictionary)_dictionary).Add(key, value); + + if (value is NamedReference n) + { + n.ObjectId = key.ToString(); + } + } + + public void Clear() => ((ICollection>>)_dictionary).Clear(); + + public bool Contains(KeyValuePair> item) => ((ICollection>>)_dictionary).Contains(item); + + public bool Contains(object key) => ((IDictionary)_dictionary).Contains(key); + + public bool ContainsKey(string key) => ((IDictionary>)_dictionary).ContainsKey(key); + + public void CopyTo(KeyValuePair>[] array, int arrayIndex) => ((ICollection>>)_dictionary).CopyTo(array, arrayIndex); + + public void CopyTo(Array array, int index) => ((ICollection)_dictionary).CopyTo(array, index); + + public IEnumerator>> GetEnumerator() => ((IEnumerable>>)_dictionary).GetEnumerator(); + + public bool Remove(string key) => ((IDictionary>)_dictionary).Remove(key); + + public bool Remove(KeyValuePair> item) => ((ICollection>>)_dictionary).Remove(item); + + public void Remove(object key) => ((IDictionary)_dictionary).Remove(key); + + public bool TryGetValue(string key, out NamedReference value) => ((IDictionary>)_dictionary).TryGetValue(key, out value); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_dictionary).GetEnumerator(); + + IDictionaryEnumerator IDictionary.GetEnumerator() => ((IDictionary)_dictionary).GetEnumerator(); +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/License.cs b/src/ApiCodeGenerator.AsyncApi/DOM/License.cs index 7c1abb0..2e7089d 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/License.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/License.cs @@ -1,16 +1,15 @@ using Newtonsoft.Json; using NJsonSchema; -namespace ApiCodeGenerator.AsyncApi.DOM +namespace ApiCodeGenerator.AsyncApi.DOM; + +public class License : JsonExtensionObject { - public class License : JsonExtensionObject - { - /// Gets or sets the name. - [JsonProperty(PropertyName = "name", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, Required = Required.Always)] - public required string Name { get; set; } + /// Gets or sets the name. + [JsonProperty(PropertyName = "name", Required = Required.Always)] + public required string Name { get; set; } - /// Gets or sets the license URL. - [JsonProperty(PropertyName = "url", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public string? Url { get; set; } - } + /// Gets or sets the license URL. + [JsonProperty(PropertyName = "url")] + public string? Url { get; set; } } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Message.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Message.cs index a1647f6..a5e2935 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/Message.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Message.cs @@ -1,17 +1,58 @@ +using ApiCodeGenerator.AsyncApi.DOM.Traits; using Newtonsoft.Json; -using NJsonSchema; -namespace ApiCodeGenerator.AsyncApi.DOM +namespace ApiCodeGenerator.AsyncApi.DOM; + +public class Message : ExtensionRefObject, ITraitsAware { - public class Message : RefObject - { - [JsonProperty("bindings")] - public MessageBindings? Bindings { get; set; } + /// Schema definition of the application headers. + [JsonProperty("headers")] + public Reference? Headers { get; set; } + + /// Definition of the message payload. + [JsonProperty("payload")] + public Reference? Payload { get; set; } + + /// Definition of the correlation ID used for message tracing or matching. + [JsonProperty("correlationId")] + public Reference? CorrelationId { get; set; } + + /// The content type to use when encoding/decoding a message's payload. + [JsonProperty("contentType")] + public string? ContentType { get; set; } + + /// A machine-friendly name for the message. + [JsonProperty("name")] + public string? Name { get; set; } + + /// A human-friendly title for the message. + [JsonProperty("title")] + public string? Title { get; set; } + + /// A short summary of what the message is about. + [JsonProperty("summary")] + public string? Summary { get; set; } + + /// A verbose explanation of the message. + [JsonProperty("description")] + public string? Description { get; set; } + + /// A list of tags for logical grouping and categorization of messages. + [JsonProperty("tags")] + public ICollection>? Tags { get; set; } + + /// Additional external documentation for this message. + [JsonProperty("externalDocs")] + public Reference? ExternalDocs { get; set; } + + /// A map where the keys describe the name of the protocol and the values describe protocol-specific definitions for the message. + [JsonProperty("bindings")] + public Reference? Bindings { get; set; } - [JsonProperty("name")] - public string Name { get; set; } = default!; + /// List of examples. + [JsonProperty("examples")] + public ICollection? Examples { get; set; } - [JsonProperty("payload")] - public JsonSchema Payload { get; set; } = default!; - } + [JsonProperty("traits")] + public Reference? Traits { get; set; } } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/MessageBindings.cs b/src/ApiCodeGenerator.AsyncApi/DOM/MessageBindings.cs index 788f25d..3321457 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/MessageBindings.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/MessageBindings.cs @@ -1,6 +1,6 @@ namespace ApiCodeGenerator.AsyncApi.DOM; -public class MessageBindings : RefObject +public class MessageBindings : ExtensionRefObject { public Bindings.Amqp.Message? Amqp { get; set; } } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/MessageExample.cs b/src/ApiCodeGenerator.AsyncApi/DOM/MessageExample.cs new file mode 100644 index 0000000..a6bf6b1 --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/MessageExample.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; +using NJsonSchema; + +namespace ApiCodeGenerator.AsyncApi.DOM; + +public class MessageExample : JsonExtensionObject +{ + [JsonProperty("headers")] + public IDictionary? Headers { get; set; } + + [JsonProperty("payload")] + public IDictionary? Payload { get; set; } + + [JsonProperty("name")] + public string? Name { get; set; } + + [JsonProperty("summary")] + public string? Summary { get; set; } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/NamedReference.cs b/src/ApiCodeGenerator.AsyncApi/DOM/NamedReference.cs new file mode 100644 index 0000000..85884cc --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/NamedReference.cs @@ -0,0 +1,13 @@ +using NJsonSchema.References; + +namespace ApiCodeGenerator.AsyncApi.DOM; + +public sealed class NamedReference : Reference + where T : IJsonReference +{ + public NamedReference() + { + } + + public string? ObjectId { get; internal set; } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Operation.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Operation.cs index a32c64b..8f4fde6 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/Operation.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Operation.cs @@ -1,32 +1,44 @@ +using ApiCodeGenerator.AsyncApi.DOM.Security; +using ApiCodeGenerator.AsyncApi.DOM.Traits; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -namespace ApiCodeGenerator.AsyncApi.DOM +namespace ApiCodeGenerator.AsyncApi.DOM; + +public class Operation : ExtensionRefObject, ITraitsAware { - public class Operation : RefObject - { - [JsonProperty("bindings", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public OperationBindings? Bindings { get; set; } + [JsonProperty("action", Required = Required.Always)] + public required OperationAction Action { get; set; } + + [JsonProperty("channel", Required = Required.Always)] + public required Reference Channel { get; set; } + + [JsonProperty("title")] + public string? Title { get; set; } + + [JsonProperty("summary")] + public string? Summary { get; set; } + + [JsonProperty("description")] + public string? Description { get; set; } - [JsonProperty("description")] - public string? Description { get; set; } + [JsonProperty("security")] + public ICollection>? Security { get; set; } - [JsonProperty("message")] - public Message Message { get; set; } = default!; + [JsonProperty("tags")] + public ICollection>? Tags { get; set; } - [JsonProperty("operationId")] - public string? OperationId { get; set; } + [JsonProperty("externalDocs")] + public Reference? ExternalDocs { get; set; } - [JsonProperty("summary")] - public string? Summary { get; set; } + [JsonProperty("bindings")] + public Reference? Bindings { get; set; } - [JsonProperty("tags")] - public ICollection? Tags { get; set; } + [JsonProperty("traits")] + public Reference? Traits { get; set; } - [JsonProperty("traits")] - public JToken? Traits { get; set; } + [JsonProperty("messages")] + public ICollection>? Messages { get; set; } = default!; - [JsonExtensionData] - public IDictionary ExtensionData { get; set; } = new Dictionary(); - } + [JsonProperty("reply")] + public Reference? Reply { get; set; } } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/OperationAction.cs b/src/ApiCodeGenerator.AsyncApi/DOM/OperationAction.cs new file mode 100644 index 0000000..8ef67ff --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/OperationAction.cs @@ -0,0 +1,10 @@ +namespace ApiCodeGenerator.AsyncApi.DOM; + +public enum OperationAction +{ + /// Send message. + Send, + + /// Receive message. + Receive, +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/OperationBindings.cs b/src/ApiCodeGenerator.AsyncApi/DOM/OperationBindings.cs index 83f213e..07d2b27 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/OperationBindings.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/OperationBindings.cs @@ -1,6 +1,6 @@ namespace ApiCodeGenerator.AsyncApi.DOM; -public class OperationBindings : RefObject +public class OperationBindings : ExtensionRefObject { public Bindings.Amqp.OperationBase? Amqp { get; set; } } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/OperationReply.cs b/src/ApiCodeGenerator.AsyncApi/DOM/OperationReply.cs new file mode 100644 index 0000000..1e71205 --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/OperationReply.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace ApiCodeGenerator.AsyncApi.DOM; + +public class OperationReply : ExtensionRefObject +{ + /// Definition of the address that implementations MUST use for the reply. + [JsonProperty("address")] + public Reference? Address { get; set; } + + [JsonProperty("channel")] + public Reference? Channel { get; set; } + + [JsonProperty("messages")] + public ICollection>? Messages { get; set; } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/OperationReplyAddress.cs b/src/ApiCodeGenerator.AsyncApi/DOM/OperationReplyAddress.cs new file mode 100644 index 0000000..7a2541c --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/OperationReplyAddress.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace ApiCodeGenerator.AsyncApi.DOM; + +public class OperationReplyAddress : ExtensionRefObject +{ + [JsonProperty("description")] + public string? Description { get; set; } + + [JsonProperty("location", Required = Required.Always)] + public required string Location { get; set; } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Parameter.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Parameter.cs index c98d84a..016131c 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/Parameter.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Parameter.cs @@ -1,14 +1,26 @@ -using System.Text.Json.Serialization; using Newtonsoft.Json; -using NJsonSchema; namespace ApiCodeGenerator.AsyncApi.DOM; -public class Parameter : RefObject +public class Parameter : ExtensionRefObject { + /// An enumeration of string values to be used if the substitution options are from a limited set. + [JsonProperty("enum")] + public ICollection? Enum { get; set; } + + /// The default value to use for substitution, and to send, if an alternate value is not supplied. + [JsonProperty("default")] + public string? Default { get; set; } + + /// An optional description for the parameter. [JsonProperty("description", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] public string? Description { get; set; } - [JsonProperty("schema", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public JsonSchema Schema { get; set; } = default!; + /// An array of examples of the parameter value. + [JsonProperty("examples")] + public ICollection? Examples { get; set; } + + /// A runtime expression that specifies the location of the parameter value. + [JsonProperty("location")] + public string? Location { get; set; } } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/RefObject.cs b/src/ApiCodeGenerator.AsyncApi/DOM/RefObject.cs index c56f613..82c78a0 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/RefObject.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/RefObject.cs @@ -1,34 +1,20 @@ -using Newtonsoft.Json; using NJsonSchema; using NJsonSchema.References; namespace ApiCodeGenerator.AsyncApi.DOM; -public class RefObject : IJsonReference -where T : RefObject +/// +/// Base object for referenced objects. +/// +public abstract class RefObject : IJsonReference { - [JsonProperty("$ref", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public string? ReferencePath { get; set; } + IJsonReference IJsonReference.ActualObject => this; - [JsonIgnore] - public T? Reference - { - get => (T?)((IJsonReference)this).Reference; - set => ((IJsonReference)this).Reference = value; - } + object? IJsonReference.PossibleRoot => null; - [JsonIgnore] - public T ActualObject => Reference ?? (T)this; + string? IJsonReferenceBase.ReferencePath { get; set; } - [JsonIgnore] - IJsonReference IJsonReference.ActualObject => ActualObject; - - [JsonIgnore] - object? IJsonReference.PossibleRoot { get; } - - [JsonIgnore] IJsonReference? IJsonReferenceBase.Reference { get; set; } - [JsonIgnore] string? IDocumentPathProvider.DocumentPath { get; set; } } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Reference.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Reference.cs new file mode 100644 index 0000000..84dacac --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Reference.cs @@ -0,0 +1,48 @@ +using Newtonsoft.Json; +using NJsonSchema; +using NJsonSchema.References; + +namespace ApiCodeGenerator.AsyncApi.DOM; + +/// +/// Reference to object. +/// +/// If property allows reference to an object, use this class as the property type. +/// Target object type. +[JsonConverter(typeof(Serialization.RefObjectConverter))] +public class Reference : IJsonReference + where T : IJsonReference +{ + public Reference() + { + } + + protected Reference(T actualObj) + { + ((IJsonReferenceBase)this).Reference = actualObj; + } + + [JsonIgnore] + public T ActualObject => (T)((IJsonReferenceBase)this).Reference!; + + public string? ReferencePath { get; set; } + + [JsonIgnore] + IJsonReference IJsonReference.ActualObject => ActualObject; + + [JsonIgnore] + object? IJsonReference.PossibleRoot { get; } + + [JsonIgnore] + IJsonReference? IJsonReferenceBase.Reference { get; set; } + + [JsonIgnore] + string? IDocumentPathProvider.DocumentPath { get; set; } + + public static implicit operator T(Reference reference) + { + return reference.ActualObject; + } + + public static implicit operator Reference(T actualObj) => new Reference(actualObj); +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Security/ApiKeySecuritySchemaLocations.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Security/ApiKeySecuritySchemaLocations.cs new file mode 100644 index 0000000..ab8f7de --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Security/ApiKeySecuritySchemaLocations.cs @@ -0,0 +1,8 @@ +namespace ApiCodeGenerator.AsyncApi.DOM.Security; + +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1602:Enumeration items should be documented", Justification = "Reviewed")] +public enum ApiKeySecuritySchemaLocations +{ + User, + Password, +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Security/ApiKeySecurityScheme.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Security/ApiKeySecurityScheme.cs new file mode 100644 index 0000000..c806b3b --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Security/ApiKeySecurityScheme.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace ApiCodeGenerator.AsyncApi.DOM.Security; + +public class ApiKeySecurityScheme : SecurityScheme +{ + internal const string SchemaType = "apiKey"; + + internal ApiKeySecurityScheme() + : base(SchemaType) + { + } + + [JsonProperty("in", Required = Required.Always)] + public ApiKeySecuritySchemaLocations In { get; set; } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Security/AuthorizationCodeOAuthFlow.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Security/AuthorizationCodeOAuthFlow.cs new file mode 100644 index 0000000..9ccebcf --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Security/AuthorizationCodeOAuthFlow.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace ApiCodeGenerator.AsyncApi.DOM.Security; + +public class AuthorizationCodeOAuthFlow : OAuthFlow +{ + [JsonProperty("authorizationUrl", Required = Required.Always)] + public required string AuthorizationUrl { get; set; } + + [JsonProperty("tokenUrl", Required = Required.Always)] + public required string TokenUrl { get; set; } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Security/ClientCredentialsOAuthFlow.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Security/ClientCredentialsOAuthFlow.cs new file mode 100644 index 0000000..a530d6b --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Security/ClientCredentialsOAuthFlow.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace ApiCodeGenerator.AsyncApi.DOM.Security; + +public class ClientCredentialsOAuthFlow : OAuthFlow +{ + [JsonProperty("tokenUrl", Required = Required.Always)] + public required string TokenUrl { get; set; } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Security/HttpApiKeySecuritySchemaLocations.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Security/HttpApiKeySecuritySchemaLocations.cs new file mode 100644 index 0000000..f22d7b5 --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Security/HttpApiKeySecuritySchemaLocations.cs @@ -0,0 +1,9 @@ +namespace ApiCodeGenerator.AsyncApi.DOM.Security; + +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1602:Enumeration items should be documented", Justification = "Reviewed")] +public enum HttpApiKeySecuritySchemaLocations +{ + Query, + Header, + Cookie, +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Security/HttpApiKeySecurityScheme.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Security/HttpApiKeySecurityScheme.cs new file mode 100644 index 0000000..6a2c996 --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Security/HttpApiKeySecurityScheme.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace ApiCodeGenerator.AsyncApi.DOM.Security; + +public class HttpApiKeySecurityScheme : SecurityScheme +{ + internal const string SchemaType = "httpApiKey"; + + internal HttpApiKeySecurityScheme() + : base(SchemaType) + { + } + + [JsonProperty("name", Required = Required.Always)] + public required string Name { get; set; } + + [JsonProperty("in", Required = Required.Always)] + public HttpApiKeySecuritySchemaLocations In { get; set; } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Security/HttpSecurityScheme.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Security/HttpSecurityScheme.cs new file mode 100644 index 0000000..0ca3fbf --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Security/HttpSecurityScheme.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace ApiCodeGenerator.AsyncApi.DOM.Security; + +public class HttpSecurityScheme : SecurityScheme +{ + internal const string SchemaType = "http"; + + internal HttpSecurityScheme() + : base(SchemaType) + { + } + + [JsonProperty("scheme", Required = Required.Always)] + public required string Scheme { get; set; } + + [JsonProperty("bearerFormat")] + public string? BearerFormat { get; set; } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Security/ImplicitOAuthFlow.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Security/ImplicitOAuthFlow.cs new file mode 100644 index 0000000..5a879a6 --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Security/ImplicitOAuthFlow.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace ApiCodeGenerator.AsyncApi.DOM.Security; + +public class ImplicitOAuthFlow : OAuthFlow +{ + [JsonProperty("authorizationUrl", Required = Required.Always)] + public required string AuthorizationUrl { get; set; } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Security/OAuth2SecurityScheme.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Security/OAuth2SecurityScheme.cs new file mode 100644 index 0000000..6631640 --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Security/OAuth2SecurityScheme.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace ApiCodeGenerator.AsyncApi.DOM.Security; + +public class OAuth2SecurityScheme : SecurityScheme +{ + internal const string SchemaType = "oauth2"; + + internal OAuth2SecurityScheme() + : base(SchemaType) + { + } + + [JsonProperty("flows", Required = Required.Always)] + public required OAuthFlows Flows { get; set; } + + [JsonProperty("scopes")] + public ICollection? Scopes { get; set; } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Security/OAuthFlow.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Security/OAuthFlow.cs new file mode 100644 index 0000000..e186632 --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Security/OAuthFlow.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace ApiCodeGenerator.AsyncApi.DOM.Security; + +public class OAuthFlow +{ + [JsonProperty("refreshUrl")] + public string? RefreshUrl { get; set; } + + [JsonProperty("availableScopes", Required = Required.Always)] + public required IDictionary AvailableScopes { get; set; } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Security/OAuthFlows.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Security/OAuthFlows.cs new file mode 100644 index 0000000..8e8af6d --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Security/OAuthFlows.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; +using NJsonSchema; + +namespace ApiCodeGenerator.AsyncApi.DOM.Security; + +public class OAuthFlows : JsonExtensionObject +{ + [JsonProperty("implicit")] + public ImplicitOAuthFlow? Implicit { get; set; } + + [JsonProperty("password")] + public PasswordOAuthFlow? Password { get; set; } + + [JsonProperty("clientCredentials")] + public ClientCredentialsOAuthFlow? ClientCredentials { get; set; } + + [JsonProperty("authorizationCode")] + public AuthorizationCodeOAuthFlow? AuthorizationCode { get; set; } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Security/OpenIdConnectSecurityScheme.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Security/OpenIdConnectSecurityScheme.cs new file mode 100644 index 0000000..847e57e --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Security/OpenIdConnectSecurityScheme.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace ApiCodeGenerator.AsyncApi.DOM.Security; + +public class OpenIdConnectSecurityScheme : SecurityScheme +{ + internal const string SchemaType = "openIdConnect"; + + internal OpenIdConnectSecurityScheme() + : base(SchemaType) + { + } + + [JsonProperty("openIdConnectUrl", Required = Required.Always)] + public required string OpenIdConnectUrl { get; set; } + + [JsonProperty("scopes")] + public ICollection? Scopes { get; set; } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Security/PasswordOAuthFlow.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Security/PasswordOAuthFlow.cs new file mode 100644 index 0000000..1392e79 --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Security/PasswordOAuthFlow.cs @@ -0,0 +1,9 @@ +using Newtonsoft.Json; + +namespace ApiCodeGenerator.AsyncApi.DOM.Security; + +public class PasswordOAuthFlow : OAuthFlow +{ + [JsonProperty("tokenUrl", Required = Required.Always)] + public required string TokenUrl { get; set; } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Security/SecurityScheme.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Security/SecurityScheme.cs new file mode 100644 index 0000000..2841d0f --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Security/SecurityScheme.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; + +namespace ApiCodeGenerator.AsyncApi.DOM.Security; + +[JsonConverter(typeof(Serialization.InheritanceConverter), nameof(Type))] +[Serialization.KnownType(ApiKeySecurityScheme.SchemaType, typeof(ApiKeySecurityScheme))] +[Serialization.KnownType(HttpApiKeySecurityScheme.SchemaType, typeof(HttpApiKeySecurityScheme))] +[Serialization.KnownType(HttpSecurityScheme.SchemaType, typeof(HttpSecurityScheme))] +[Serialization.KnownType(OAuth2SecurityScheme.SchemaType, typeof(OAuth2SecurityScheme))] +[Serialization.KnownType(OpenIdConnectSecurityScheme.SchemaType, typeof(OpenIdConnectSecurityScheme))] +[Serialization.KnownType("*", typeof(SecurityScheme))] +public class SecurityScheme : ExtensionRefObject +{ + public SecurityScheme(string type) + { + Type = type; + } + + [JsonProperty("type")] + public string Type { get; } + + [JsonProperty("description")] + public string? Description { get; set; } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/SecurityRequirement.cs b/src/ApiCodeGenerator.AsyncApi/DOM/SecurityRequirement.cs deleted file mode 100644 index f739009..0000000 --- a/src/ApiCodeGenerator.AsyncApi/DOM/SecurityRequirement.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Newtonsoft.Json; - -namespace ApiCodeGenerator.AsyncApi.DOM -{ - - public class SecurityRequirement - { - } -} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiReferenceUpdater.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiReferenceUpdater.cs new file mode 100644 index 0000000..b3c550c --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiReferenceUpdater.cs @@ -0,0 +1,86 @@ +using Newtonsoft.Json.Serialization; +using NJsonSchema; +using NJsonSchema.References; +using NJsonSchema.Visitors; + +namespace ApiCodeGenerator.AsyncApi.DOM.Serialization; + +internal sealed class AsyncApiReferenceUpdater : AsyncJsonReferenceVisitorBase +{ + private readonly AsyncApiDocument _rootObject; + private readonly JsonReferenceResolver _referenceResolver; + private readonly IContractResolver _contractResolver; + + public AsyncApiReferenceUpdater(AsyncApiDocument rootObject, JsonReferenceResolver referenceResolver, IContractResolver contractResolver) + : base(contractResolver) + { + _rootObject = rootObject; + _referenceResolver = referenceResolver; + _contractResolver = contractResolver; + } + + protected override async Task VisitAsync(object obj, string path, string? typeNameHint, ISet checkedObjects, Action replacer, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (obj == null || checkedObjects.Contains(obj)) + { + return; + } + + if (obj is IDocumentAware documentAware) + { + documentAware.Document = _rootObject; + } + + // customize resolve reference for Reference<> + if (IsReference(obj.GetType()) && obj is IJsonReference jr) + { + checkedObjects.Add(obj); + + if (jr.Reference is not null && jr.ReferencePath is null) + { + // if inline object declaration without $ref then needed visit nested object + await VisitAsync(jr.Reference, + path, + typeNameHint, + checkedObjects, + o => jr.Reference = (IJsonReference)o, + cancellationToken); + return; + } + else if (jr.Reference is null && jr.ReferencePath is not null) + { + // if set reference path need resol reference and set Reference property + var newReference = await VisitJsonReferenceAsync(jr, path, typeNameHint, cancellationToken).ConfigureAwait(false); + if (newReference != jr) + { + jr.Reference = newReference.ActualObject; + return; + } + } + } + + await base.VisitAsync(obj, path, typeNameHint, checkedObjects, replacer, cancellationToken); + } + + protected override async Task VisitJsonReferenceAsync(IJsonReference reference, string path, string? typeNameHint, CancellationToken cancellationToken) + { + if (reference.ReferencePath != null && reference.Reference == null) + { + var targetType = reference.GetType(); + var target = await _referenceResolver + .ResolveReferenceAsync(_rootObject, reference.ReferencePath, targetType, _contractResolver, cancellationToken); + return target; + } + + return reference; + } + + private static bool IsReference(Type type) + { + return type.IsGenericType + && ( + type.GetGenericTypeDefinition() == typeof(Reference<>) + || type.GetGenericTypeDefinition() == typeof(NamedReference<>)); + } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSchemaConverter.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSchemaConverter.cs new file mode 100644 index 0000000..c047823 --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSchemaConverter.cs @@ -0,0 +1,60 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace ApiCodeGenerator.AsyncApi.DOM.Serialization; + +internal class AsyncApiSchemaConverter : JsonConverter +{ + public const string AsyncApi = "application/vnd.aai.asyncapi"; + public const string AsyncApi3 = AsyncApi + ";version=3.0.0"; + public const string JsonSchema07 = "application/schema+json;version=draft-07"; + public const string JsonSchema07Yaml = "application/schema+yaml;version=draft-07"; + public const string OpenApi = "application/vnd.oai.openapi"; + + public override bool CanWrite => false; + + public override bool CanConvert(Type objectType) => objectType == typeof(AsyncApiSchema); + + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + var jobj = (JObject)JToken.ReadFrom(reader); + var format = AsyncApi3; + JToken schemaDefinition = jobj; + if (jobj.Property("schemaFormat") != null) + { + format = jobj.GetValue("schemaFormat")!.Value() + ?? throw new JsonSerializationException("SchemaFormat property must contain the value"); + schemaDefinition = jobj.GetValue("schema") + ?? throw new JsonSerializationException("Schema property must contain the value"); + } + + if (jobj.Property("$ref") != null) + { + var schema = new AsyncApiSchema { SchemaFormat = "$ref" }; + serializer.Populate(jobj.CreateReader(), schema); + return schema; + } + + return DeserializeSchema(format, schemaDefinition, serializer); + } + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + => throw new NotImplementedException(); + + private AsyncApiSchema DeserializeSchema(string format, JToken schemaDefinition, JsonSerializer serializer) + { + if (format.StartsWith(AsyncApi) + || format.StartsWith(OpenApi) + || format == JsonSchema07 + || format == JsonSchema07Yaml) + { + var aaSchema = new AsyncApiSchema { SchemaFormat = format }; + serializer.Populate(schemaDefinition.CreateReader(), aaSchema); + return aaSchema; + } + else + { + throw new NotSupportedException($"'{format}' is not supported format."); + } + } +} diff --git a/src/ApiCodeGenerator.AsyncApi/AsyncApiSchemaResolver.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSchemaResolver.cs similarity index 61% rename from src/ApiCodeGenerator.AsyncApi/AsyncApiSchemaResolver.cs rename to src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSchemaResolver.cs index 064f484..e77f2ae 100644 --- a/src/ApiCodeGenerator.AsyncApi/AsyncApiSchemaResolver.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSchemaResolver.cs @@ -1,8 +1,7 @@ -using ApiCodeGenerator.AsyncApi.DOM; using NJsonSchema; using NJsonSchema.Generation; -namespace ApiCodeGenerator.AsyncApi; +namespace ApiCodeGenerator.AsyncApi.DOM.Serialization; internal class AsyncApiSchemaResolver : JsonSchemaResolver { @@ -19,10 +18,12 @@ public AsyncApiSchemaResolver(AsyncApiDocument rootObject, JsonSchemaGeneratorSe public override void AppendSchema(JsonSchema schema, string? typeNameHint) { - if (Document.Components?.Schemas?.Values.Contains(schema) != true) + // append schemas loaded from external documents + if (Document.Components.Schemas?.Values.Contains(schema) != true) { - var typeName = _typenameGenerator.Generate(schema, typeNameHint, Document.Components!.Schemas.Keys); - Document.Components!.Schemas[typeName] = schema; + Document.Components.Schemas ??= new Dictionary(); + var typeName = _typenameGenerator.Generate(schema, typeNameHint, Document.Components.Schemas.Keys); + Document.Components.Schemas[typeName] = (AsyncApiSchema)schema; } } } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSerializer.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSerializer.cs new file mode 100644 index 0000000..a8fe077 --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSerializer.cs @@ -0,0 +1,87 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; +using NJsonSchema.Generation; +using NJsonSchema.Yaml; +using YamlDotNet.Serialization; + +namespace ApiCodeGenerator.AsyncApi.DOM.Serialization; + +public static class AsyncApiSerializer +{ + private static readonly JsonSerializerSettings JSONSERIALIZERSETTINGS = new() + { + PreserveReferencesHandling = PreserveReferencesHandling.None, + MetadataPropertyHandling = MetadataPropertyHandling.Ignore, + ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, + ReferenceLoopHandling = ReferenceLoopHandling.Serialize, + }; + + /// + /// Load document from JSON text. + /// + /// JSON text. + /// AsyncApi document object model. + public static Task FromJsonAsync(string data) + => FromJsonAsync(data, null); + + /// + /// Load document from JSON text. + /// + /// JSON text. + /// Path to document. + /// AsyncApi document object model. + public static Task FromJsonAsync(string data, string? documentPath) + { + var jObject = JObject.Parse(data); + return FromJObject(jObject, documentPath); + } + + /// + /// Load document from YAML text. + /// + /// YAML text. + /// AsyncApi document object model. + public static Task FromYamlAsync(string data) + => FromYamlAsync(data, null); + + /// + /// Load document from YAML text. + /// + /// YAML text. + /// Path to document. + /// AsyncApi document object model. + public static Task FromYamlAsync(string data, string? documentPath) + { + JObject jObject = ParseYaml(data); + return FromJObject(jObject, documentPath); + } + + private static JObject ParseYaml(string data) + { + var deserializer = new DeserializerBuilder().Build(); + using var reader = new StringReader(data); + var yamlDocument = deserializer.Deserialize(reader)!; + + return JObject.FromObject(yamlDocument)!; + } + + private static Task FromJObject(JObject jObject, string? documentPath) + { + var serializer = JsonSerializer.Create(JSONSERIALIZERSETTINGS); + var doc = serializer.Deserialize(jObject.CreateReader())!; + doc.DocumentPath = documentPath; + return UpdateSchemaReferencesAsync(doc, serializer.ContractResolver); + } + + private static async Task UpdateSchemaReferencesAsync(AsyncApiDocument document, IContractResolver contractResolver) + { + await new AsyncApiReferenceUpdater( + document, + new JsonAndYamlReferenceResolver(new AsyncApiSchemaResolver(document, new SystemTextJsonSchemaGeneratorSettings())), + contractResolver) + .VisitAsync(document, default) + .ConfigureAwait(false); + return document; + } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/IDocumentAware.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/IDocumentAware.cs new file mode 100644 index 0000000..7ab0cf6 --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/IDocumentAware.cs @@ -0,0 +1,6 @@ +namespace ApiCodeGenerator.AsyncApi.DOM.Serialization; + +public interface IDocumentAware +{ + internal AsyncApiDocument? Document { get; set; } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/InheritanceConverter.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/InheritanceConverter.cs index 51d17e8..efcf9f8 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/InheritanceConverter.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/InheritanceConverter.cs @@ -1,5 +1,4 @@ -using System; -using System.Reflection; +using System.Reflection; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; @@ -11,11 +10,17 @@ namespace ApiCodeGenerator.AsyncApi.DOM.Serialization /// Конвертируемый тип. public class InheritanceConverter : JsonConverter { - private static readonly Dictionary> _factories = GetFactories(); + private const BindingFlags CtorBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + private static readonly Dictionary> _factories = GetFactories(); private readonly string _discriminator; - private readonly string _defaultValue; + private readonly string? _defaultValue; - public InheritanceConverter(string discriminator, string defaultValue) + public InheritanceConverter(string discriminator) + : this(discriminator, null) + { + } + + public InheritanceConverter(string discriminator, string? defaultValue) { _discriminator = discriminator; _defaultValue = defaultValue; @@ -34,8 +39,20 @@ public override object ReadJson(JsonReader reader, Type objectType, object? exis { var jobj = Newtonsoft.Json.Linq.JObject.Load(reader); var contract = serializer.ContractResolver.ResolveContract(objectType) as JsonObjectContract; + + // if its ref skip discriminator logic + if (jobj.TryGetValue("$ref", out var @ref)) + { + var ctor = objectType.GetConstructor(Type.EmptyTypes) + ?? objectType.GetConstructor([typeof(string)]) + ?? throw new InvalidOperationException($"Ctor not found. Type: {objectType.FullName}"); + var target = ctor.Invoke(ctor.GetParameters().Length == 1 ? [string.Empty] : []); + serializer.Populate(jobj.CreateReader(), target); + return target; + } + var discriminatorProperty = contract?.Properties.FirstOrDefault(jp => jp.UnderlyingName == _discriminator); - if (discriminatorProperty == null) + if (discriminatorProperty is null) { throw new InvalidOperationException($"Property '{_discriminator}' not found in type '{objectType}'."); } @@ -44,14 +61,22 @@ public override object ReadJson(JsonReader reader, Type objectType, object? exis jobj.GetValue(discriminatorProperty.PropertyName, StringComparison.OrdinalIgnoreCase)?.ToString() ?? _defaultValue; - if (discriminatorValue is not null && _factories.TryGetValue(discriminatorValue, out var factory)) + if (discriminatorValue is null) { - var target = factory()!; + throw new JsonSerializationException($"Required property '{_discriminator}' not found in JSON. Path '{reader.Path}'."); + } + + if (_factories.TryGetValue(discriminatorValue, out var factory) + || _factories.TryGetValue("*", out factory)) + { + var target = factory(discriminatorValue)!; serializer.Populate(jobj.CreateReader(), target); return target; } - - throw new NotSupportedException($"Not supported item type: {discriminatorValue}"); + else + { + throw new NotSupportedException($"Not supported item type: {discriminatorValue}"); + } } /// @@ -60,7 +85,7 @@ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer throw new NotSupportedException(); } - private static Dictionary> GetFactories() + private static Dictionary> GetFactories() { return typeof(T) .GetCustomAttributes() @@ -68,8 +93,19 @@ private static Dictionary> GetFactories() i => i.DiscriminatorValue, GetFactory); - static Func GetFactory(KnownTypeAttribute attr) - => () => (T)Activator.CreateInstance(attr.Type); + static Func GetFactory(KnownTypeAttribute attr) + { + var ctor = attr.Type.GetConstructor(CtorBindingFlags, null, [typeof(string)], null); + if (ctor is not null) + { + return (t) => (T)ctor.Invoke([t]); + } + else + { + ctor = attr.Type.GetConstructor(CtorBindingFlags, null, Type.EmptyTypes, null); + return (t) => (T)ctor.Invoke([]); + } + } } } } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/RefObjectConverter.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/RefObjectConverter.cs new file mode 100644 index 0000000..da1d8ef --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/RefObjectConverter.cs @@ -0,0 +1,66 @@ +using Newtonsoft.Json; +using NJsonSchema.References; + +namespace ApiCodeGenerator.AsyncApi.DOM.Serialization; + +internal class RefObjectConverter : JsonConverter +{ + public override bool CanWrite => false; + + public override bool CanConvert(Type objectType) => objectType.IsGenericType && + (objectType.GetGenericTypeDefinition() == typeof(Reference<>) || + objectType.GetGenericTypeDefinition() == typeof(NamedReference<>)); + + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + reader.Read(); + var refObj = (IJsonReference)Activator.CreateInstance(objectType); + if (reader.TokenType == JsonToken.PropertyName + && reader.Value?.ToString() == "$ref") + { + var refPath = reader.ReadAsString(); + refObj.ReferencePath = refPath; + reader.Read(); + } + else + { + var targetType = objectType.GetGenericArguments().Single(); + var target = serializer.Deserialize(new CustomJsonReader(reader), targetType); + refObj.Reference = (IJsonReference?)target; + } + + return refObj; + } + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => throw new NotImplementedException(); + + // Special reader. Wraps original reader and emulate state before call 'Read' in converter + private sealed class CustomJsonReader : JsonReader + { + private readonly JsonReader _reader; + private bool _readed = false; + + public CustomJsonReader(JsonReader reader) + { + _reader = reader; + } + + public override JsonToken TokenType => _readed ? _reader.TokenType : JsonToken.StartObject; + + public override int Depth => _reader.Depth; + + public override string Path => _reader.Path; + + public override char QuoteChar { get => _reader.QuoteChar; protected set => base.QuoteChar = value; } + + public override object? Value => _reader.Value; + + public override Type? ValueType => _reader.ValueType; + + public override bool Read() => _readed ? _reader.Read() : _readed = true; + + public override void Close() => _reader.Close(); + + protected override void Dispose(bool disposing) => ((IDisposable)_reader).Dispose(); + } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Server.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Server.cs index 56af368..78bc8c9 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/Server.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Server.cs @@ -1,31 +1,36 @@ using Newtonsoft.Json; -namespace ApiCodeGenerator.AsyncApi.DOM +namespace ApiCodeGenerator.AsyncApi.DOM; + +public class Server : RefObject { - public class Server : RefObject - { - [JsonProperty("url")] - public required string Url { get; set; } + [JsonProperty("host", Required = Required.Always)] + public required string Host { get; set; } + + [JsonProperty("protocol", Required = Required.Always)] + public required string Protocol { get; set; } + + [JsonProperty("pathname")] + public string? PathName { get; set; } - [JsonProperty("protocol")] - public required string Protocol { get; set; } + [JsonProperty("protocolVersion")] + public string? ProtocolVersion { get; set; } - [JsonProperty("protocolVersion")] - public string? ProtocolVersion { get; set; } + [JsonProperty("description")] + public string? Description { get; set; } - [JsonProperty("description")] - public string? Description { get; set; } + [JsonProperty("variables")] + public IDictionary>? Variables { get; set; } - [JsonProperty("variables")] - public IDictionary? Variables { get; set; } + [JsonProperty("security")] + public ICollection>? Security { get; set; } - [JsonProperty("security")] - public ICollection? Security { get; set; } + [JsonProperty("tags")] + public ICollection>? Tags { get; set; } - [JsonProperty("tags")] - public ICollection? Tags { get; set; } + [JsonProperty("externalDocs")] + public Reference? ExternalDocs { get; set; } - [JsonProperty("bindings")] - public ServerBindings? Bindings { get; set; } - } + [JsonProperty("bindings")] + public Reference? Bindings { get; set; } } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/ServerBindings.cs b/src/ApiCodeGenerator.AsyncApi/DOM/ServerBindings.cs index b02819c..217bf2e 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/ServerBindings.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/ServerBindings.cs @@ -1,9 +1,6 @@ -using Newtonsoft.Json; +namespace ApiCodeGenerator.AsyncApi.DOM; -namespace ApiCodeGenerator.AsyncApi.DOM +public class ServerBindings : ExtensionRefObject { - public class ServerBindings : RefObject - { - public Bindings.Amqp.Server? Amqp { get; set; } - } + public Bindings.Amqp.Server? Amqp { get; set; } } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/ServerVariable.cs b/src/ApiCodeGenerator.AsyncApi/DOM/ServerVariable.cs index 16eef55..9abc19d 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/ServerVariable.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/ServerVariable.cs @@ -1,19 +1,18 @@ using Newtonsoft.Json; -namespace ApiCodeGenerator.AsyncApi.DOM +namespace ApiCodeGenerator.AsyncApi.DOM; + +public class ServerVariable : ExtensionRefObject { - public class ServerVariable : RefObject - { - [JsonProperty("description")] - public string? Description { get; set; } + [JsonProperty("description")] + public string? Description { get; set; } - [JsonProperty("enum")] - public ICollection? Enum { get; set; } + [JsonProperty("enum")] + public ICollection? Enum { get; set; } - [JsonProperty("default")] - public string? Default { get; set; } + [JsonProperty("default")] + public string? Default { get; set; } - [JsonProperty("examples")] - public ICollection? Examples { get; set; } - } + [JsonProperty("examples")] + public ICollection? Examples { get; set; } } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Tag.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Tag.cs index 49bb9d5..d2ae7ef 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/Tag.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Tag.cs @@ -1,14 +1,15 @@ using Newtonsoft.Json; -namespace ApiCodeGenerator.AsyncApi.DOM +namespace ApiCodeGenerator.AsyncApi.DOM; + +public class Tag : ExtensionRefObject { - public class Tag - { - [JsonProperty("name", Required = Required.Always)] - public string Name { get; set; } = default!; - - [JsonProperty("description")] - public string? Description { get; set; } - } -#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + [JsonProperty("name", Required = Required.Always)] + public required string Name { get; set; } + + [JsonProperty("description")] + public string? Description { get; set; } + + [JsonProperty("externalDocs")] + public Reference? ExternalDocs { get; set; } } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Traits/ITraitsAware.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Traits/ITraitsAware.cs new file mode 100644 index 0000000..a710ace --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Traits/ITraitsAware.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; + +namespace ApiCodeGenerator.AsyncApi.DOM.Traits; + +public interface ITraitsAware + where TEntity : class, ITraitsAware + where TTraits : Traits +{ + [JsonProperty("traits")] + public Reference? Traits { get; set; } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Traits/MessageTraits.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Traits/MessageTraits.cs new file mode 100644 index 0000000..9598a62 --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Traits/MessageTraits.cs @@ -0,0 +1,74 @@ +using Newtonsoft.Json; + +namespace ApiCodeGenerator.AsyncApi.DOM.Traits; + +public class MessageTraits : Traits +{ + [JsonProperty("headers")] + public Reference? Headers { get; set; } + + [JsonProperty("correlationId")] + public Reference? CorrelationId { get; set; } + + [JsonProperty("contentType")] + public string? ContentType { get; set; } + + [JsonProperty("bindings")] + public Reference? Bindings { get; set; } + + [JsonProperty("examples")] + public ICollection? Examples { get; set; } + + internal override void ApplyTo(Message target, bool overwrite) + { + if (Title is not null && (target.Title is null || overwrite)) + { + target.Title = Title; + } + + if (Summary is not null && (target.Summary is null || overwrite)) + { + target.Summary = Summary; + } + + if (Description is not null && (target.Description is null || overwrite)) + { + target.Description = Description; + } + + if (Tags is not null && (target.Tags is null || overwrite)) + { + target.Tags = Tags; + } + + if (ExternalDocs is not null && (target.ExternalDocs is null || overwrite)) + { + target.ExternalDocs = ExternalDocs; + } + + if (Headers is not null && (target.Headers is null || overwrite)) + { + target.Headers = Headers; + } + + if (CorrelationId is not null && (target.CorrelationId is null || overwrite)) + { + target.CorrelationId = CorrelationId; + } + + if (ContentType is not null && (target.ContentType is null || overwrite)) + { + target.ContentType = ContentType; + } + + if (Bindings is not null && (target.Bindings is null || overwrite)) + { + target.Bindings = Bindings; + } + + if (Examples is not null && (target.Examples is null || overwrite)) + { + target.Examples = Examples; + } + } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Traits/OperationTraits.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Traits/OperationTraits.cs new file mode 100644 index 0000000..8e9ad95 --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Traits/OperationTraits.cs @@ -0,0 +1,50 @@ +using Newtonsoft.Json; + +namespace ApiCodeGenerator.AsyncApi.DOM.Traits; + +public class OperationTraits : Traits +{ + [JsonProperty("security")] + public ICollection>? Security { get; set; } + + [JsonProperty("bindings")] + public Reference? Bindings { get; set; } + + internal override void ApplyTo(Operation target, bool overwrite) + { + if (Title is not null && (target.Title is null || overwrite)) + { + target.Title = Title; + } + + if (Summary is not null && (target.Summary is null || overwrite)) + { + target.Summary = Summary; + } + + if (Description is not null && (target.Description is null || overwrite)) + { + target.Description = Description; + } + + if (Tags is not null && (target.Tags is null || overwrite)) + { + target.Tags = Tags; + } + + if (ExternalDocs is not null && (target.ExternalDocs is null || overwrite)) + { + target.ExternalDocs = ExternalDocs; + } + + if (Security is not null && (target.Security is null || overwrite)) + { + target.Security = Security; + } + + if (Bindings is not null && (target.Bindings is null || overwrite)) + { + target.Bindings = Bindings; + } + } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Traits/Traits.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Traits/Traits.cs new file mode 100644 index 0000000..a7bd288 --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Traits/Traits.cs @@ -0,0 +1,28 @@ +using ApiCodeGenerator.AsyncApi.DOM.Serialization; +using Newtonsoft.Json; + +namespace ApiCodeGenerator.AsyncApi.DOM.Traits; + +public abstract class Traits : ExtensionRefObject, IDocumentAware + where TTraits : Traits + where TTarget : class +{ + [JsonProperty("title")] + public string? Title { get; set; } + + [JsonProperty("summary")] + public string? Summary { get; set; } + + [JsonProperty("description")] + public string? Description { get; set; } + + [JsonProperty("tags")] + public ICollection>? Tags { get; set; } + + [JsonProperty("externalDocs")] + public Reference? ExternalDocs { get; set; } + + AsyncApiDocument? IDocumentAware.Document { get; set; } + + internal abstract void ApplyTo(TTarget target, bool overwrite); +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Traits/TraitsExtensions.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Traits/TraitsExtensions.cs new file mode 100644 index 0000000..8874f5b --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Traits/TraitsExtensions.cs @@ -0,0 +1,25 @@ +using ApiCodeGenerator.AsyncApi.DOM.Serialization; + +namespace ApiCodeGenerator.AsyncApi.DOM.Traits; + +public static class TraitsExtensions +{ + public static TEntity ApplyTraits(this TEntity entity) + where TEntity : class, ITraitsAware, new() + where TTraits : Traits + { + var traits = entity.Traits?.ActualObject; + if (traits is not null) + { + var version = ((IDocumentAware)traits).Document?.AsyncApi ?? "3.0.0"; + var overwrite = version.StartsWith("2."); + var target = new TEntity(); + traits.ApplyTo(target, overwrite); + return target; + } + else + { + return entity; + } + } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DefaultTemplateFactory.GetToolchainVersion.cs b/src/ApiCodeGenerator.AsyncApi/DefaultTemplateFactory.GetToolchainVersion.cs index 8e4c0a5..837d92d 100644 --- a/src/ApiCodeGenerator.AsyncApi/DefaultTemplateFactory.GetToolchainVersion.cs +++ b/src/ApiCodeGenerator.AsyncApi/DefaultTemplateFactory.GetToolchainVersion.cs @@ -1,5 +1,3 @@ -using System; - namespace ApiCodeGenerator.AsyncApi; [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1601:PartialElementsMustBeDocumented", Justification = "Reviewed.")] diff --git a/src/ApiCodeGenerator.AsyncApi/DefaultParameterNameGenerator.cs b/src/ApiCodeGenerator.AsyncApi/NameGenerators/DefaultParameterNameGenerator.cs similarity index 87% rename from src/ApiCodeGenerator.AsyncApi/DefaultParameterNameGenerator.cs rename to src/ApiCodeGenerator.AsyncApi/NameGenerators/DefaultParameterNameGenerator.cs index d190741..b7944fc 100644 --- a/src/ApiCodeGenerator.AsyncApi/DefaultParameterNameGenerator.cs +++ b/src/ApiCodeGenerator.AsyncApi/NameGenerators/DefaultParameterNameGenerator.cs @@ -1,7 +1,7 @@ using ApiCodeGenerator.AsyncApi.DOM; using NJsonSchema; -namespace ApiCodeGenerator.AsyncApi; +namespace ApiCodeGenerator.AsyncApi.NameGenerators; public class DefaultParameterNameGenerator : IParameterNameGenerator { @@ -20,6 +20,6 @@ public static string GetVariableName(string parameterName) firstCharacterMustBeAlpha: true); } - public string Generate(string parameterName, Parameter parameter, IEnumerable allParameters) + public string Generate(string parameterName, Parameter parameter, IEnumerable> allParameters) => GetVariableName(parameterName); } diff --git a/src/ApiCodeGenerator.AsyncApi/OperationNameGenerators/IOperationNameGenerator.cs b/src/ApiCodeGenerator.AsyncApi/NameGenerators/IOperationNameGenerator.cs similarity index 57% rename from src/ApiCodeGenerator.AsyncApi/OperationNameGenerators/IOperationNameGenerator.cs rename to src/ApiCodeGenerator.AsyncApi/NameGenerators/IOperationNameGenerator.cs index 553de2d..ffe8f4e 100644 --- a/src/ApiCodeGenerator.AsyncApi/OperationNameGenerators/IOperationNameGenerator.cs +++ b/src/ApiCodeGenerator.AsyncApi/NameGenerators/IOperationNameGenerator.cs @@ -1,23 +1,19 @@ using ApiCodeGenerator.AsyncApi.DOM; -namespace ApiCodeGenerator.AsyncApi; +namespace ApiCodeGenerator.AsyncApi.NameGenerators; /// Generates the client and operation name for a given operation. public interface IOperationNameGenerator { /// Gets the client name for a given operation (may be empty). /// The Swagger document. - /// The Channel path. - /// True if subscribe operation. /// The operation. /// The client name. - string GetClientName(AsyncApiDocument document, string path, bool subscribe, Operation operation); + public string GetClientName(AsyncApiDocument document, NamedReference operation); /// Gets the operation name for a given operation. /// The Swagger document. - /// The Channel path. - /// True if subscribe operation. /// The operation. /// The operation name. - string GetOperationName(AsyncApiDocument document, string path, bool subscribe, Operation operation); + public string GetOperationName(AsyncApiDocument document, NamedReference operation); } diff --git a/src/ApiCodeGenerator.AsyncApi/IParameterNameGenerator.cs b/src/ApiCodeGenerator.AsyncApi/NameGenerators/IParameterNameGenerator.cs similarity index 66% rename from src/ApiCodeGenerator.AsyncApi/IParameterNameGenerator.cs rename to src/ApiCodeGenerator.AsyncApi/NameGenerators/IParameterNameGenerator.cs index b703907..abf27cc 100644 --- a/src/ApiCodeGenerator.AsyncApi/IParameterNameGenerator.cs +++ b/src/ApiCodeGenerator.AsyncApi/NameGenerators/IParameterNameGenerator.cs @@ -1,4 +1,6 @@ -namespace ApiCodeGenerator.AsyncApi; +using ApiCodeGenerator.AsyncApi.DOM; + +namespace ApiCodeGenerator.AsyncApi.NameGenerators; /// The parameter name generator interface. public interface IParameterNameGenerator @@ -8,5 +10,5 @@ public interface IParameterNameGenerator /// The parameter. /// All parameters. /// Generated parameter name. - string Generate(string parameterName, DOM.Parameter parameter, IEnumerable allParameters); + public string Generate(string parameterName, Parameter parameter, IEnumerable> allParameters); } diff --git a/src/ApiCodeGenerator.AsyncApi/NameGenerators/MultipleClientsFromFirstTagAndOperationId.cs b/src/ApiCodeGenerator.AsyncApi/NameGenerators/MultipleClientsFromFirstTagAndOperationId.cs new file mode 100644 index 0000000..81fc8d0 --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/NameGenerators/MultipleClientsFromFirstTagAndOperationId.cs @@ -0,0 +1,12 @@ +using ApiCodeGenerator.AsyncApi.DOM; + +namespace ApiCodeGenerator.AsyncApi.NameGenerators; + +public class MultipleClientsFromFirstTagAndOperationId : IOperationNameGenerator +{ + public string GetClientName(AsyncApiDocument document, NamedReference operation) + => operation.ActualObject.Tags?.FirstOrDefault()?.ActualObject.Name ?? string.Empty; + + public string GetOperationName(AsyncApiDocument document, NamedReference operation) + => operation.ObjectId!; +} diff --git a/src/ApiCodeGenerator.AsyncApi/ParameterNameGeneratorWithReplace.cs b/src/ApiCodeGenerator.AsyncApi/NameGenerators/ParameterNameGeneratorWithReplace.cs similarity index 89% rename from src/ApiCodeGenerator.AsyncApi/ParameterNameGeneratorWithReplace.cs rename to src/ApiCodeGenerator.AsyncApi/NameGenerators/ParameterNameGeneratorWithReplace.cs index 94791e3..82ae96a 100644 --- a/src/ApiCodeGenerator.AsyncApi/ParameterNameGeneratorWithReplace.cs +++ b/src/ApiCodeGenerator.AsyncApi/NameGenerators/ParameterNameGeneratorWithReplace.cs @@ -1,9 +1,6 @@ -using System.Collections.Generic; -using System.Linq; +using ApiCodeGenerator.AsyncApi.DOM; -using ApiCodeGenerator.AsyncApi.DOM; - -namespace ApiCodeGenerator.AsyncApi; +namespace ApiCodeGenerator.AsyncApi.NameGenerators; /// /// Заменяет части названий параметров. @@ -31,7 +28,7 @@ public ParameterNameGeneratorWithReplace(IDictionary replaceMap, /// Текущий параметр. /// Все параметры. /// Название параметра. - public string Generate(string parameterName, Parameter parameter, IEnumerable allParameters) + public string Generate(string parameterName, Parameter parameter, IEnumerable> allParameters) { var res = _replaceMap.Aggregate(parameterName, (current, replaceOption) => current.Replace(replaceOption.Key, replaceOption.Value)); diff --git a/src/ApiCodeGenerator.AsyncApi/NameGenerators/SingleClientFromOperationId.cs b/src/ApiCodeGenerator.AsyncApi/NameGenerators/SingleClientFromOperationId.cs new file mode 100644 index 0000000..2c72f35 --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/NameGenerators/SingleClientFromOperationId.cs @@ -0,0 +1,12 @@ +using ApiCodeGenerator.AsyncApi.DOM; + +namespace ApiCodeGenerator.AsyncApi.NameGenerators; + +public class SingleClientFromOperationId : IOperationNameGenerator +{ + public string GetClientName(AsyncApiDocument document, NamedReference operation) + => string.Empty; + + public string GetOperationName(AsyncApiDocument document, NamedReference operation) + => operation.ObjectId!; +} diff --git a/src/ApiCodeGenerator.AsyncApi/OperationNameGenerators/MultipleClientsFromFirstTagAndOperationId.cs b/src/ApiCodeGenerator.AsyncApi/OperationNameGenerators/MultipleClientsFromFirstTagAndOperationId.cs deleted file mode 100644 index 4c63a86..0000000 --- a/src/ApiCodeGenerator.AsyncApi/OperationNameGenerators/MultipleClientsFromFirstTagAndOperationId.cs +++ /dev/null @@ -1,13 +0,0 @@ -using ApiCodeGenerator.AsyncApi.DOM; - -namespace ApiCodeGenerator.AsyncApi.OperationNameGenerators; - -public class MultipleClientsFromFirstTagAndOperationId : IOperationNameGenerator -{ - public string GetClientName(AsyncApiDocument document, string path, bool subscribe, Operation operation) - => operation.Tags?.FirstOrDefault()?.Name ?? string.Empty; - - public string GetOperationName(AsyncApiDocument document, string path, bool subscribe, Operation operation) - => operation.OperationId - ?? throw new InvalidOperationException($"Get operation name failed. OperationId in channel '{path}' not set."); -} diff --git a/src/ApiCodeGenerator.AsyncApi/OperationNameGenerators/SingleClientFromOperationId.cs b/src/ApiCodeGenerator.AsyncApi/OperationNameGenerators/SingleClientFromOperationId.cs deleted file mode 100644 index 83754ca..0000000 --- a/src/ApiCodeGenerator.AsyncApi/OperationNameGenerators/SingleClientFromOperationId.cs +++ /dev/null @@ -1,13 +0,0 @@ -using ApiCodeGenerator.AsyncApi.DOM; - -namespace ApiCodeGenerator.AsyncApi.OperationNameGenerators; - -public class SingleClientFromOperationId : IOperationNameGenerator -{ - public string GetClientName(AsyncApiDocument document, string path, bool subscribe, Operation operation) - => string.Empty; - - public string GetOperationName(AsyncApiDocument document, string path, bool subscribe, Operation operation) - => operation.OperationId - ?? throw new InvalidOperationException($"Get operation name failed. OperationId in channel '{path}' not set."); -} diff --git a/src/ApiCodeGenerator.OpenApi/Converters/SettingsConverter.cs b/src/ApiCodeGenerator.OpenApi/Converters/SettingsConverter.cs index 4a6178a..ad2dad8 100644 --- a/src/ApiCodeGenerator.OpenApi/Converters/SettingsConverter.cs +++ b/src/ApiCodeGenerator.OpenApi/Converters/SettingsConverter.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Text; using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/src/ApiCodeGenerator.OpenApi/DefaultTemplateFactory.cs b/src/ApiCodeGenerator.OpenApi/DefaultTemplateFactory.cs index 095203c..a17d3e5 100644 --- a/src/ApiCodeGenerator.OpenApi/DefaultTemplateFactory.cs +++ b/src/ApiCodeGenerator.OpenApi/DefaultTemplateFactory.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; diff --git a/src/ApiCodeGenerator.OpenApi/Helpers/SettingsHelpers.cs b/src/ApiCodeGenerator.OpenApi/Helpers/SettingsHelpers.cs index b68e7ec..dbd45da 100644 --- a/src/ApiCodeGenerator.OpenApi/Helpers/SettingsHelpers.cs +++ b/src/ApiCodeGenerator.OpenApi/Helpers/SettingsHelpers.cs @@ -5,7 +5,7 @@ using Newtonsoft.Json.Linq; #if ASYNC_API -using ApiCodeGenerator.AsyncApi.OperationNameGenerators; +using ApiCodeGenerator.AsyncApi.NameGenerators; namespace ApiCodeGenerator.AsyncApi.Helpers; #else diff --git a/src/ApiCodeGenerator.OpenApi/PropertyNameGeneratorWithReplace.cs b/src/ApiCodeGenerator.OpenApi/PropertyNameGeneratorWithReplace.cs index 6565da8..d347b04 100644 --- a/src/ApiCodeGenerator.OpenApi/PropertyNameGeneratorWithReplace.cs +++ b/src/ApiCodeGenerator.OpenApi/PropertyNameGeneratorWithReplace.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using System.Text; using NJsonSchema; using NJsonSchema.CodeGeneration; using NJsonSchema.CodeGeneration.CSharp; diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/AmqpFunctionalTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/AmqpFunctionalTests.cs index 0c07756..502f820 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/AmqpFunctionalTests.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/AmqpFunctionalTests.cs @@ -1,7 +1,6 @@ using ApiCodeGenerator.AsyncApi.Amqp.CSharp; using ApiCodeGenerator.AsyncApi.DOM; using ApiCodeGenerator.AsyncApi.DOM.Bindings.Amqp; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using static ApiCodeGenerator.AsyncApi.Tests.Infrastructure.TestHelpers; @@ -65,7 +64,7 @@ public async Task Generate_ChannelBindingPublisher() }; var channelBinding = new ChannelBindings { - Amqp = new() + Amqp = new DOM.Bindings.Amqp.Channel { Is = ChannelType.RoutingKey, Exchange = new() @@ -103,7 +102,7 @@ public async Task Generate_ChannelBindingPublisher() // Assert string[] expectedPublisherCode = [ GetExpectedSummary("Inform about environmental lighting conditions of a particular streetlight.", 8) + - GetExpectedAmqpPublisherCode("ReceiveLightMeasurement", "LightMeasuredPayload", identCnt: 8, channelBinding.Amqp.Exchange) + GetExpectedAmqpPublisherCode("ReceiveLightMeasurement", "LightMeasuredPayload", identCnt: 8, channelBinding.Amqp?.Exchange) ]; var expectedCode = GetExpectedCode( GetExpectedAmqpServiceCode(className, identCnt: 4, expectedPublisherCode) + "\n" + @@ -133,7 +132,7 @@ public async Task Generate_ChannelBindingSubscriber() }; var channelBinding = new ChannelBindings { - Amqp = new() + Amqp = new DOM.Bindings.Amqp.Channel { Is = ChannelType.RoutingKey, Exchange = new() @@ -179,7 +178,7 @@ public async Task Generate_ChannelBindingSubscriber() ]; var expectedCode = GetExpectedCode( GetExpectedAmqpServiceCode(className, identCnt: 4, expectedSubscriberCode) + "\n" + - GetExpectedPoolCode(className, identCnt: 4, channel) + "\n", + GetExpectedPoolCode(className, identCnt: 4) + "\n", GetExpectedDtoCode(), ns, GetAmqpUsings() + "\n"); diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/ApiCodeGenerator.AsyncApi.Tests.csproj b/test/ApiCodeGenerator.AsyncApi.Tests/ApiCodeGenerator.AsyncApi.Tests.csproj index 98da215..900d39e 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/ApiCodeGenerator.AsyncApi.Tests.csproj +++ b/test/ApiCodeGenerator.AsyncApi.Tests/ApiCodeGenerator.AsyncApi.Tests.csproj @@ -22,6 +22,7 @@ + diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/AsyncApiContentGeneratorTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/AsyncApiContentGeneratorTests.cs index c43ef14..a29aae5 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/AsyncApiContentGeneratorTests.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/AsyncApiContentGeneratorTests.cs @@ -1,7 +1,9 @@ using System.Collections.ObjectModel; using ApiCodeGenerator.AsyncApi.DOM; +using ApiCodeGenerator.AsyncApi.NameGenerators; using Moq; using Newtonsoft.Json.Linq; +using NJsonSchema.References; using NUnit.Framework.Constraints; namespace ApiCodeGenerator.AsyncApi.Tests; @@ -64,7 +66,7 @@ public async Task Load_OperationGenerator() var settings = fakeGen.FakeGenerator.Settings; Assert.NotNull(settings); Assert.NotNull(settings.OperationNameGenerator); - Assert.IsInstanceOf(settings.OperationNameGenerator); + Assert.IsInstanceOf(settings.OperationNameGenerator); } [Test] @@ -104,8 +106,8 @@ public async Task LoadApiDocument_WithTextPreprocess() var apiDocument = gen.Document; Assert.NotNull(apiDocument); - Assert.That(apiDocument?.Components?.Schemas, Does.ContainKey(schemaName)); - var sch = apiDocument?.Components?.Schemas[schemaName].ToJson(Newtonsoft.Json.Formatting.None); + Assert.That(apiDocument.Components.Schemas, Does.ContainKey(schemaName)); + var sch = apiDocument.Components.Schemas[schemaName].ToJson(Newtonsoft.Json.Formatting.None); Assert.That(sch, Is.EqualTo("{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"processed\":{}}")); } @@ -131,8 +133,8 @@ public async Task LoadApiDocument_WithTextPreprocess_Log() var apiDocument = gen.Document; Assert.NotNull(apiDocument); - Assert.That(apiDocument?.Components?.Schemas, Does.ContainKey(schemaName)); - var sch = apiDocument?.Components?.Schemas[schemaName].ToJson(Newtonsoft.Json.Formatting.None); + Assert.That(apiDocument.Components.Schemas, Does.ContainKey(schemaName)); + var sch = apiDocument.Components.Schemas[schemaName].ToJson(Newtonsoft.Json.Formatting.None); Assert.That(sch, Is.EqualTo("{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"processed\":{}}")); logger.Verify(l => l.LogWarning(It.IsAny(), filePath, It.IsAny())); } @@ -160,7 +162,7 @@ public async Task LoadApiDocument_WithModelPreprocess() Assert.NotNull(apiDocument); Assert.That(apiDocument?.Components?.Schemas, Does.ContainKey(schemaName)); - var sch = apiDocument?.Components?.Schemas[schemaName].ToJson(Newtonsoft.Json.Formatting.None); + var sch = apiDocument?.Components?.Schemas?[schemaName].ToJson(Newtonsoft.Json.Formatting.None); Assert.That(sch, Is.EqualTo("{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"properties\":{\"processedModel\":{}}}")); } @@ -177,7 +179,7 @@ public async Task LoadApiDocument_WithExternalRef(string documentPath) var document = contentGenerator.Document; - Assert.NotNull(document.Components?.Messages["lightMeasured"].Reference); + Assert.NotNull((document.Components?.Messages?["lightMeasured"] as IJsonReference)?.Reference); } private static Func?, object?> GetSettingsFactory(string json) @@ -220,32 +222,32 @@ private void ValidateDocument(AsyncApiDocument document) .And.ContainKey("mtls-connections")); // Resolve $ref in channel defintion - var actualChannel = document.Channels?[channelPrefix + "event.{streetlightId}.lighting.measured"]; + var actualChannel = document.Channels?[channelPrefix + "event.{streetlightId}.lighting.measured"].ActualObject; Assert.That(actualChannel, Is.Not.Null .And.Property("Publish").Not.Null .And.Property("Subscribe").Null); - Assert.That(actualChannel?.Parameters, + Assert.That(actualChannel.Parameters, Is.Not.Null .And.ContainKey("streetlightId")); - Assert.That(actualChannel?.Parameters["streetlightId"], + Assert.That(actualChannel.Parameters["streetlightId"], Is.Not.Null .And.Property("ReferencePath").EqualTo("#/components/parameters/streetlightId") - .And.Property("Reference").EqualTo(document.Components?.Parameters["streetlightId"])); - Assert.That(actualChannel?.Publish?.Message, - Is.Not.Null - .And.Property("ReferencePath").EqualTo("#/components/messages/lightMeasured") - .And.Property("Reference").EqualTo(document.Components?.Messages["lightMeasured"])); + .And.Property("Reference").EqualTo(document.Components.Parameters["streetlightId"])); + // Assert.That(actualChannel?.Publish?.Message, + // Is.Not.Null + // .And.Property("ReferencePath").EqualTo("#/components/messages/lightMeasured") + // .And.Property("Reference").EqualTo(document.Components?.Messages["lightMeasured"])); // Resolve $ref in message definition - var actualMessage = document.Components?.Messages["turnOnOff"]; + var actualMessage = document.Components.Messages["turnOnOff"].ActualObject; Assert.That(actualMessage, Is.Not.Null); - Assert.That(actualMessage?.Payload, + Assert.That(actualMessage.Payload, Is.Not.Null - .And.Property("Reference").EqualTo(document.Components?.Schemas["turnOnOffPayload"])); + .And.Property("Reference").EqualTo(document.Components.Schemas["turnOnOffPayload"])); // Resolve $ref in schema definition - Assert.That(document.Components?.Schemas["turnOnOffPayload"]?.ActualProperties, + Assert.That(document.Components.Schemas["turnOnOffPayload"]?.ActualProperties, Is.Not.Null .And.ContainKey("command")); @@ -264,7 +266,7 @@ private void ValidateDocument(AsyncApiDocument document) // Resolve $ref in server variables Assert.Multiple(() => { - var variables = document.Components?.Servers["mtls-connections"].Variables; + var variables = document.Components?.Servers["mtls-connections"].ActualObject.Variables; Assert.That(variables, Is.Not.Null .And.ContainKey("someRefVariable") diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/FunctionalTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/FunctionalTests.cs index 4b34a70..1a1f1fc 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/FunctionalTests.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/FunctionalTests.cs @@ -1,4 +1,3 @@ -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using static ApiCodeGenerator.AsyncApi.Tests.Infrastructure.TestHelpers; diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/GlobalUsings.cs b/test/ApiCodeGenerator.AsyncApi.Tests/GlobalUsings.cs index 39608c1..3855f84 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/GlobalUsings.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/GlobalUsings.cs @@ -1,5 +1,9 @@ global using System.Diagnostics.CodeAnalysis; global using ApiCodeGenerator.Abstraction; global using ApiCodeGenerator.AsyncApi.CSharp; +global using ApiCodeGenerator.AsyncApi.DOM.Serialization; global using ApiCodeGenerator.AsyncApi.Tests.Infrastructure; +global using DeepEqual.Syntax; +global using Newtonsoft.Json; global using NUnit.Framework; +global using static ApiCodeGenerator.AsyncApi.Tests.Infrastructure.DeepEqualHelper; diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/DeepEqualHelper.cs b/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/DeepEqualHelper.cs new file mode 100644 index 0000000..ab82954 --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/DeepEqualHelper.cs @@ -0,0 +1,8 @@ +using DeepEqual; + +namespace ApiCodeGenerator.AsyncApi.Tests.Infrastructure; + +internal static class DeepEqualHelper +{ + public static IComparison IgnoreUnmatchedProperties { get; } = new ComparisonBuilder().IgnoreUnmatchedProperties().Create(); +} diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/FakeContentGenerator.cs b/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/FakeContentGenerator.cs index a1dc0a0..9c67bf3 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/FakeContentGenerator.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/FakeContentGenerator.cs @@ -1,4 +1,3 @@ -using ApiCodeGenerator.AsyncApi; using ApiCodeGenerator.AsyncApi.DOM; namespace ApiCodeGenerator.AsyncApi.Tests.Infrastructure; diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/FakeModelPreprocessor.cs b/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/FakeModelPreprocessor.cs index e52128d..327495b 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/FakeModelPreprocessor.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/FakeModelPreprocessor.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using ApiCodeGenerator.AsyncApi.DOM; +using ApiCodeGenerator.AsyncApi.DOM; using Newtonsoft.Json.Linq; using NJsonSchema; @@ -23,7 +18,7 @@ public FakeModelPreprocessor(string settingsJson) public AsyncApiDocument Process(AsyncApiDocument document, string? fileName) { Invocactions.Add(new(Settings, true, [document, fileName])); - document.Components?.Schemas.Values.First().Properties.Add( + document.Components.Schemas?.Values.First().Properties.Add( "processedModel", new JsonSchemaProperty()); return document; diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/FakeTextPreprocessor.cs b/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/FakeTextPreprocessor.cs index 061874e..b55d060 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/FakeTextPreprocessor.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/FakeTextPreprocessor.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Linq; namespace ApiCodeGenerator.AsyncApi.Tests.Infrastructure { diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/TestHelpers.Amqp.cs b/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/TestHelpers.Amqp.cs index c275ac8..80290c9 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/TestHelpers.Amqp.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/TestHelpers.Amqp.cs @@ -1,7 +1,4 @@ -using System.Collections; -using ApiCodeGenerator.AsyncApi.DOM; using ApiCodeGenerator.AsyncApi.DOM.Bindings.Amqp; -using YamlDotNet.Core.Tokens; namespace ApiCodeGenerator.AsyncApi.Tests.Infrastructure; @@ -51,7 +48,6 @@ public static string GetExpectedAmqpPublisherCode( string name, string payloadType, int identCnt, - Exchange? exchange = null, OperationBase? operationBinding = null) { @@ -170,9 +166,6 @@ public static string GetExpectedAmqpSubscriberCode( } public static string GetExpectedPoolCode(string className, int identCnt) - => GetExpectedPoolCode(className, identCnt, new DOM.Channel() { Publish = new() { OperationId = "receiveLightMeasurement" } }); - - public static string GetExpectedPoolCode(string className, int identCnt, params DOM.Channel[] channels) { var ident = new string(' ', identCnt); return @@ -276,21 +269,22 @@ public static string GetExpectedPoolCode(string className, int identCnt, params IEnumerable GetChannelDeclarations() { - foreach (var channel in channels) - { - var code = (channel.Publish ?? channel.Subscribe)?.OperationId switch - { - "receiveLightMeasurement" => "_channels.Add(\"receiveLightMeasurement\", CreateChannel(connection));", - "dimLight" => GetSubscriberChannelDeclaration(channel, "dimLight"), - _ => throw new InvalidOperationException("Unknown operationId"), - }; - yield return $"{ident} {code}\n"; - } + // foreach (var channel in channels) + // { + // var code = (channel.Publish ?? channel.Subscribe)?.OperationId switch + // { + // "receiveLightMeasurement" => "_channels.Add(\"receiveLightMeasurement\", CreateChannel(connection));", + // "dimLight" => GetSubscriberChannelDeclaration(channel, "dimLight"), + // _ => throw new InvalidOperationException("Unknown operationId"), + // }; + // yield return $"{ident} {code}\n"; + // } + yield break; } string GetSubscriberChannelDeclaration(DOM.Channel channel, string operationId) { - var queue = channel.Bindings?.Amqp?.Queue; + var queue = channel.Bindings?.ActualObject?.Amqp?.Queue; object? prefetchCount = 0; object? confirm = false; queue?.AdditionalProperties?.TryGetValue("x-prefetch-count", out prefetchCount); diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/TestHelpers.cs b/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/TestHelpers.cs index 2496b61..b2bffa7 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/TestHelpers.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/TestHelpers.cs @@ -1,6 +1,4 @@ -using System.Security.Cryptography; -using ApiCodeGenerator.Core.Converters; -using Newtonsoft.Json; +using ApiCodeGenerator.Core.Converters; namespace ApiCodeGenerator.AsyncApi.Tests.Infrastructure; @@ -261,6 +259,28 @@ public static IReadOnlyDictionary> GetOperatio kv => kv.Key, kv => (IReadOnlyCollection)[kv.Value]); + public static string ToYaml(this object obj, byte indent) + { + var serializer = new YamlDotNet.Serialization.SerializerBuilder() + .WithQuotingNecessaryStrings(true) + .WithDefaultScalarStyle(YamlDotNet.Core.ScalarStyle.SingleQuoted) + .WithNamingConvention(YamlDotNet.Serialization.NamingConventions.CamelCaseNamingConvention.Instance) + .ConfigureDefaultValuesHandling(YamlDotNet.Serialization.DefaultValuesHandling.OmitDefaults) + .Build(); + var yaml = serializer.Serialize(obj); + if (indent > 0) + { + var prefix = "\n" + new string(' ', indent); + return yaml.Replace("\n", prefix); + } + + return yaml; + } + + public static T? GetValueOrNull(this IDictionary? dict, string key) + where T : class + => dict?.TryGetValue(key, out var value) == true ? value : null; + private static Task CreateGenerator(TextReader document, CSharpClientGeneratorSettings settings) { var context = new GeneratorContext((t, s, v) => settings, new Core.ExtensionManager.Extensions(), new Dictionary()) diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/AsyncApiDocumentTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/AsyncApiDocumentTests.cs new file mode 100644 index 0000000..0e5f00f --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/AsyncApiDocumentTests.cs @@ -0,0 +1,125 @@ +using ApiCodeGenerator.AsyncApi.DOM; + +namespace ApiCodeGenerator.AsyncApi.Tests.SerializationV3; + +public class AsyncApiDocumentTests : TestBase +{ + [TestCase("asyncapi: '3.0.0'")] + [TestCase("info: { title: 'test', version: '1.0' }")] + public void RequiredProperties(string yaml) + => RequiredPropertiesTest(yaml); + + [Test] + public async Task ReadExtensions() + { + const string propName = "x-test"; + const string value = "d9c4b3d4-dc0c-44d7-ac89-aebf9c09cc0b"; + + var yaml = $$""" + {{YamlHeader}} + {{propName}}: '{{value}}' + """; + + var document = await AsyncApiSerializer.FromYamlAsync(yaml); + Assert.That(document.ExtensionData, Is.Not.Null + .And.ContainKey(propName)); + Assert.That(document.ExtensionData[propName], Is.EqualTo(value)); + } + + [Test] + public async Task ReadProperties() + { + var expected = new + { + AsyncApi = "3.0.0.", + Id = "http://test.uri", + Info = new { Title = "title", Version = "1.0" }, + Servers = new Dictionary { ["s"] = new { Host = "host", Protocol = "http" } }, + DefaultContentType = "application/json", + Channels = new Dictionary { ["c"] = new { Description = "e7450db0-8194-48e6-89e2-0078e4e4cf90" } }, + Operations = new Dictionary { ["o"] = new { Action = OperationAction.Receive, Channel = new Dictionary { ["$ref"] = "#/channels/c" } } }, + Components = new { }, + }; + + var yaml = $""" + {YamlHeader} + {expected.ToYaml(indent: 0)} + """; + await ReadPropertiesTest(yaml, expected, d => d); + } + + [Test] + public async Task ServersRef() + { + const string refPath = "#/components/servers/amqp"; + var expected = new { Host = "host", Protocol = "http" }; + var yaml = $""" + {YamlHeader} + servers: + s: + $ref: '{refPath}' + components: + servers: + amqp: + {expected.ToYaml(indent: 6)} + """; + await ResolveReferernceTest(yaml, refPath, expected, d => d.Servers.GetValueOrNull("s")); + } + + [Test] + public async Task ChannelsRef() + { + const string refPath = "#/components/channels/ch1"; + const string chName = "c"; + var expected = new { Description = "356b0029-1630-4fdb-9306-76f0462dd177" }; + var yaml = $""" + {YamlHeader} + channels: + {chName}: + $ref: '{refPath}' + components: + channels: + ch1: + {expected.ToYaml(indent: 6)} + """; + + NamedReference? channelRef = null; + await ResolveReferernceTest(yaml, refPath, expected, d => channelRef = d.Channels.GetValueOrNull(chName)); + + Assert.NotNull(channelRef); + Assert.That(channelRef.ObjectId, Is.EqualTo(chName)); + } + + [Test] + public async Task OperationsRef() + { + const string refPath = "#/components/operations/op1"; + const string opName = "op"; + var expected = new + { + Action = OperationAction.Receive, + Description = "a6974e21-8326-4f36-a893-a05a00e91d2f", + Channel = new Dictionary { ["$ref"] = "#/components/channels/ch1" }, + }; + + var yaml = $""" + {YamlHeader} + operations: + {opName}: + $ref: '{refPath}' + components: + channels: + ch1: + address: queue + operations: + op1: + {expected.ToYaml(indent: 6)} + """; + + NamedReference? operationRef = null; + await ResolveReferernceTest(yaml, refPath, expected, d => operationRef = d.Operations.GetValueOrNull(opName)); + + Assert.NotNull(operationRef); + Assert.That(operationRef.ObjectId, Is.EqualTo(opName)); + } +} diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ChannelBindingsTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ChannelBindingsTests.cs new file mode 100644 index 0000000..33a2625 --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ChannelBindingsTests.cs @@ -0,0 +1,44 @@ +using ApiCodeGenerator.AsyncApi.DOM.Bindings.Amqp; + +namespace ApiCodeGenerator.AsyncApi.Tests.SerializationV3; + +public class ChannelBindingsTests : TestBase +{ + private const string ChannelBindingName = "cb"; + private const string ChannelBindingDefinition = $""" + components: + channelBindings: + {ChannelBindingName}: + """; + + [Test] + public async Task ReadProperties() + { + var expected = new + { + Amqp = new { Is = ChannelType.RoutingKey }, + }; + + var yaml = $""" + {YamlHeader} + {ChannelBindingDefinition} + {expected.ToYaml(indent: 6)} + """; + + await ReadPropertiesTest(yaml, expected, d => d.Components.ChannelBindings.GetValueOrNull(ChannelBindingName)); + } + + [Test] + public async Task ReadExtensions() + { + const string value = "9925bcaf-c31d-4213-be34-aca6cc928b16"; + var yaml = + $$""" + {{YamlHeader}} + {{ChannelBindingDefinition}} + {0}: '{{value}}' + """; + + await ReadExtensionsTest(yaml, value, d => d.Components.ChannelBindings.GetValueOrNull(ChannelBindingName)); + } +} diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ChannelTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ChannelTests.cs new file mode 100644 index 0000000..7b5f807 --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ChannelTests.cs @@ -0,0 +1,202 @@ +using ApiCodeGenerator.AsyncApi.DOM; +using NJsonSchema.References; + +namespace ApiCodeGenerator.AsyncApi.Tests.SerializationV3; + +public class ChannelTests : TestBase +{ + private const string ChannelId = "c"; + private const string ChannelDefinition = $""" + components: + channels: + {ChannelId}: + """; + + [Test] + public async Task ReadProperties() + { + var expected = new + { + Address = "5f8fccfd-b175-43ec-a0c4-6cc2ab821282", + Messages = new Dictionary { ["m"] = new { Name = "98b3a608-5115-47fd-8cb4-84e063eacd50" } }, + Title = "315cbf60-5ebd-4275-b022-30c508145a00", + Summary = "8274d7f9-41d0-4586-a53a-fa7ece4a5f74", + Desciption = "d2914978-d650-41ba-ac03-d542e33670dd", + Servers = new object[0], // only refs + Parameters = new Dictionary(), + Tags = new[] { new { Name = "Tag" } }, + ExternalDocs = new { Url = "9c13a80d-f7c0-4682-95bb-ec1d2a55a169" }, + Bindings = new object(), + }; + + var yaml = $""" + {YamlHeader} + {ChannelDefinition} + {expected.ToYaml(indent: 6)} + """; + + await ReadPropertiesTest(yaml, expected, d => d.Components?.Channels?.GetValueOrNull(ChannelId)); + } + + [Test] + public async Task RedExtensions() + { + const string value = "078cc5b9-34d9-4a7d-ae24-ef66b235750a"; + + var yaml = $$""" + {{YamlHeader}} + {{ChannelDefinition}} + {0}: '{{value}}' + """; + + await ReadExtensionsTest(yaml, value, d => d.Components.Channels.GetValueOrNull(ChannelId)); + } + + [Test] + public async Task MessagesRef() + { + const string refPath = "#/components/messages/m"; + const string msgName = "msg"; + var expected = new { Name = "dcea8ef8-fa0e-4594-8246-5947dc7fea67" }; + + var yaml = $""" + {YamlHeader} + {ChannelDefinition} + messages: + {msgName}: + $ref: '{refPath}' + messages: + m: + {expected.ToYaml(indent: 6)} + """; + + NamedReference? messageRef = null; + await ChResolveReferenceTest(yaml, refPath, expected, c => messageRef = c.Messages?.GetValueOrNull(msgName)); + + Assert.NotNull(messageRef); + Assert.AreEqual(msgName, messageRef.ObjectId); + } + + [Test] + public async Task ServersRef() + { + const string refPath = "#/components/servers/s"; + var expected = new { Host = "host", Protocol = "amqp" }; + + var yaml = $""" + {YamlHeader} + {ChannelDefinition} + servers: + - $ref: '{refPath}' + servers: + s: + {expected.ToYaml(indent: 6)} + """; + + await ChResolveReferenceTest(yaml, refPath, expected, c => + { + Assert.That(c.Servers, Is.Not.Null.And.Count.EqualTo(1)); + return c.Servers.Single(); + }); + } + + [Test] + public async Task ParametersRef() + { + const string refPath = "#/components/parameters/p"; + const string paramName = "param"; + var expected = new { Description = "dcea8ef8-fa0e-4594-8246-5947dc7fea67" }; + + var yaml = $""" + {YamlHeader} + {ChannelDefinition} + parameters: + {paramName}: + $ref: '{refPath}' + parameters: + p: + {expected.ToYaml(indent: 6)} + """; + + NamedReference? messageRef = null; + await ChResolveReferenceTest(yaml, refPath, expected, c => messageRef = c.Parameters?.GetValueOrNull(paramName)); + + Assert.NotNull(messageRef); + Assert.AreEqual(paramName, messageRef.ObjectId); + } + + [Test] + public async Task TagsRef() + { + const string refPath = "#/components/tags/t"; + var expected = new { Name = "ce5ac997-52ff-471a-b183-f880eb5cd219" }; + + var yaml = $""" + {YamlHeader} + {ChannelDefinition} + tags: + - $ref: '{refPath}' + tags: + t: + {expected.ToYaml(indent: 6)} + """; + + await ChResolveReferenceTest(yaml, refPath, expected, c => + { + Assert.That(c.Tags, Is.Not.Null.And.Count.EqualTo(1)); + return c.Tags.Single(); + }); + } + + [Test] + public async Task ExternalDocsRef() + { + const string refPath = "#/components/externalDocs/d"; + var expected = new { Url = "http://tempuri.org/628248e3-25e9-487b-a8ca-4a5d05689ce1" }; + + var yaml = $""" + {YamlHeader} + {ChannelDefinition} + externalDocs: + $ref: '{refPath}' + externalDocs: + d: + {expected.ToYaml(indent: 6)} + """; + + await ChResolveReferenceTest(yaml, refPath, expected, c => c.ExternalDocs); + } + + [Test] + public async Task BindingsRef() + { + const string refPath = "#/components/channelBindings/b"; + var expected = new { Amqp = new { Exchange = new { Name = "ex" } } }; + var yaml = $""" + {YamlHeader} + {ChannelDefinition} + bindings: + $ref: '{refPath}' + channelBindings: + b: + {expected.ToYaml(indent: 6)} + """; + + await ChResolveReferenceTest(yaml, refPath, expected, c => c.Bindings); + } + + protected Task ChResolveReferenceTest(string yaml, string refPath, object expected, Func?> getRef) + where T : IJsonReference + { + return ResolveReferernceTest( + yaml, + refPath, + expected, + d => + { + var channel = d.Components.Channels.GetValueOrNull(ChannelId); + Assert.That(channel, Is.Not.Null); + return getRef(channel.ActualObject); + }); + } +} diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ContactTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ContactTests.cs new file mode 100644 index 0000000..53cbb6b --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ContactTests.cs @@ -0,0 +1,37 @@ +namespace ApiCodeGenerator.AsyncApi.Tests.SerializationV3; + +public class ContactTests : TestBase +{ + [Test] + public async Task ReadProperties() + { + var expected = new + { + Name = "e4d497d5-7bc8-4e4b-bcfc-e8ae344b8ed7", + Url = "c2152991-4910-4c8a-873a-dab78489cbb8", + Email = "0a147392-90bf-4592-981a-28b0223c9750", + }; + + var yaml = $""" + {YamlHeader} + contact: + {expected.ToYaml(indent: 4)} + """; + + await ReadPropertiesTest(yaml, expected, d => d.Info?.Contact); + } + + [Test] + public async Task ReadExtensions() + { + const string value = "c6801f22-76b8-4385-bfb5-9619bc8c456c"; + + var yaml = $$""" + {{YamlHeader}} + contact: + {0}: '{{value}}' + """; + + await ReadExtensionsTest(yaml, value, d => d.Info.Contact); + } +} diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/CorrelationIdTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/CorrelationIdTests.cs new file mode 100644 index 0000000..5cf6451 --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/CorrelationIdTests.cs @@ -0,0 +1,56 @@ +namespace ApiCodeGenerator.AsyncApi.Tests.SerializationV3; + +public class CorrelationIdTests : TestBase +{ + public const string CorrelationIdName = "cid"; + + private const string CorrelationIdDefinition = $""" + components: + correlationIds: + {CorrelationIdName}: + """; + + [Test] + public void RequiredProperties() + { + var yaml = $""" + {YamlHeader} + {CorrelationIdDefinition} + description: '' + """; + + RequiredPropertiesTest(yaml); + } + + [Test] + public async Task ReadExtensions() + { + const string value = "66229402-0446-4d81-8609-54311653f4c4"; + + var yaml = $$""" + {{YamlHeader}} + {{CorrelationIdDefinition}} + {0}: '{{value}}' + location: '' + """; + await ReadExtensionsTest(yaml, value, d => d.Components.CorrelationIds.GetValueOrNull(CorrelationIdName)); + } + + [Test] + public async Task ReadProperties() + { + var expected = new + { + Location = "b643f3c8-ba0b-4be2-8f31-140375cabbc3", + Description = "04fae75c-1c24-4daa-ba4c-6851e821a4f1", + }; + + var yaml = $""" + {YamlHeader} + {CorrelationIdDefinition} + {expected.ToYaml(indent: 6)} + """; + + await ReadPropertiesTest(yaml, expected, d => d.Components.CorrelationIds.GetValueOrNull(CorrelationIdName)); + } +} diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ExternalDocumentationTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ExternalDocumentationTests.cs new file mode 100644 index 0000000..1a88482 --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ExternalDocumentationTests.cs @@ -0,0 +1,53 @@ +namespace ApiCodeGenerator.AsyncApi.Tests.SerializationV3; + +public class ExternalDocumentationTests : TestBase +{ + private const string ExternalDocName = "tg"; + private const string ExternalDocDefinition = $""" + components: + externalDocs: + {ExternalDocName}: + """; + + [Test] + public void RequiredProperties() + { + var yaml = $""" + {YamlHeader} + {ExternalDocDefinition} + description: '' + """; + RequiredPropertiesTest(yaml); + } + + [Test] + public async Task ReadProperties() + { + var expected = new + { + Url = "bc3f36a6-6fd8-427b-b352-4ebcb6b15e29", + Description = "d2def82f-4d14-46e1-bd76-82f7a21cb383", + }; + + var yaml = $""" + {YamlHeader} + {ExternalDocDefinition} + {expected.ToYaml(indent: 6)} + """; + + await ReadPropertiesTest(yaml, expected, d => d.Components.ExternalDocs.GetValueOrNull(ExternalDocName)); + } + + [Test] + public async Task ReadExtensions() + { + const string value = "8131db35-db20-4253-baa7-0ab127376a52"; + var yaml = $$""" + {{YamlHeader}} + {{ExternalDocDefinition}} + url: 't' + {0}: '{{value}}' + """; + await ReadExtensionsTest(yaml, value, d => d.Components.ExternalDocs.GetValueOrNull(ExternalDocName)); + } +} diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/InfoTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/InfoTests.cs new file mode 100644 index 0000000..2f4d3dc --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/InfoTests.cs @@ -0,0 +1,97 @@ +namespace ApiCodeGenerator.AsyncApi.Tests.SerializationV3; + +public class InfoTests : TestBase +{ + [TestCase(""" + asyncapi: '3.0.0' + info: + title: test + """, + TestName = $"{nameof(RequiredProperties)} - without version")] + [TestCase(""" + asyncapi: '3.0.0' + info: + version: test + """, + TestName = $"{nameof(RequiredProperties)} - without title")] + public void RequiredProperties(string yaml) + => RequiredPropertiesTest(yaml); + + [Test] + public async Task ReadProperties() + { + var expected = new + { + Title = "test", + Version = "1.0", + Description = "d5e4f0fb-b463-42a9-91ed-adf139ef62a7", + TermsOfService = "1650e42e-d753-41d4-8811-3a6df9c576e4", + Contact = new { }, + License = new { Name = "MIT" }, + Tags = new[] { new { Name = "TAG" } }, + ExternalDocs = new { Url = "url" }, + }; + + var yaml = $""" + {YamlHeader} + {expected.ToYaml(indent: 2)} + """; + + await ReadPropertiesTest(yaml, expected, d => d.Info); + } + + [Test] + public async Task ReadExtensions() + { + const string value = "d923ab53-46c1-4407-8334-e65301ae85f8"; + + var yaml = $$""" + {{YamlHeader}} + {0}: '{{value}}' + """; + + await ReadExtensionsTest(yaml, value, d => d.Info); + } + + [Test] + public async Task TagsRef() + { + const string refPath = "#/components/tags/tag"; + var expected = new { Name = "9a51d619-07aa-46fe-a800-3b75729ccc6f" }; + + var yaml = $""" + {YamlHeader} + tags: + - $ref: '{refPath}' + components: + tags: + tag: + {expected.ToYaml(indent: 6)} + """; + + await ResolveReferernceTest(yaml, refPath, expected, d => + { + Assert.That(d.Info.Tags, Is.Not.Null.And.Count.EqualTo(1)); + return d.Info.Tags.First(); + }); + } + + [Test] + public async Task ExternalDocsRef() + { + const string refPath = "#/components/externalDocs/d"; + var expected = new { Url = "http://some.url" }; + + var yaml = $$""" + {{YamlHeader}} + externalDocs: + $ref: '{{refPath}}' + components: + externalDocs: + d: + {{expected.ToYaml(indent: 6)}} + """; + + await ResolveReferernceTest(yaml, refPath, expected, d => d.Info.ExternalDocs); + } +} diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/LicenseTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/LicenseTests.cs new file mode 100644 index 0000000..9ad0365 --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/LicenseTests.cs @@ -0,0 +1,45 @@ +namespace ApiCodeGenerator.AsyncApi.Tests.SerializationV3; + +public class LicenseTests : TestBase +{ + [TestCase($$""" + {{YamlHeader}} + license: {} + """, + TestName = $"{nameof(RequiredProperties)} - without name")] + public void RequiredProperties(string yaml) + => RequiredPropertiesTest(yaml); + + [Test] + public async Task ReadProperties() + { + var expected = new + { + Name = "ecbe7a6c-0823-4f57-ac8c-b088aed19684", + Url = "7045c6cf-d4e7-42ee-b53d-a9d8f538c4bb", + }; + + var yaml = $""" + {YamlHeader} + license: + {expected.ToYaml(indent: 4)} + """; + + await ReadPropertiesTest(yaml, expected, d => d.Info.License); + } + + [Test] + public async Task ReadExtensions() + { + const string value = "b0bd3938-5582-41f4-8833-9e30a9f28984"; + + var yaml = $$""" + {{YamlHeader}} + license: + name: MIT + {0}: '{{value}}' + """; + + await ReadExtensionsTest(yaml, value, d => d.Info.License); + } +} diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/MessageBindingsTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/MessageBindingsTests.cs new file mode 100644 index 0000000..314b26f --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/MessageBindingsTests.cs @@ -0,0 +1,44 @@ +using ApiCodeGenerator.AsyncApi.DOM.Bindings.Amqp; + +namespace ApiCodeGenerator.AsyncApi.Tests.SerializationV3; + +public class MessageBindingsTests : TestBase +{ + private const string MessageBindingName = "cb"; + private const string MessageBindingDefinition = $""" + components: + messageBindings: + {MessageBindingName}: + """; + + [Test] + public async Task ReadProperties() + { + var expected = new + { + Amqp = new Message(), + }; + + var yaml = $""" + {YamlHeader} + {MessageBindingDefinition} + {expected.ToYaml(indent: 6)} + """; + + await ReadPropertiesTest(yaml, expected, d => d.Components.MessageBindings.GetValueOrNull(MessageBindingName)); + } + + [Test] + public async Task ReadExtensions() + { + const string value = "a3442910-6806-4de9-93f8-b3ab5f22cfe6"; + var yaml = + $$""" + {{YamlHeader}} + {{MessageBindingDefinition}} + {0}: '{{value}}' + """; + + await ReadExtensionsTest(yaml, value, d => d.Components.MessageBindings.GetValueOrNull(MessageBindingName)); + } +} diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/MessageExampleTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/MessageExampleTests.cs new file mode 100644 index 0000000..392d7e8 --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/MessageExampleTests.cs @@ -0,0 +1,64 @@ +namespace ApiCodeGenerator.AsyncApi.Tests.SerializationV3; + +public class MessageExampleTests : TestBase +{ + [Test] + public async Task ReadProperties() + { + var expected = new + { + Headers = new Dictionary + { + ["h1"] = "7392eebf-f805-4c6e-847a-fa2faea18470", + }, + Payload = new Dictionary + { + ["m1"] = new { Title = "d256e84a-7f91-496e-a73b-9ad884dcf78b" }, + }, + Name = "57a4d75a-b4ec-401a-b804-d4cb9d605e60", + Summary = "1bf70ff6-c6f9-478f-bfca-2c3dcba1c206", + }; + + var yaml = $""" + {YamlHeader} + components: + messages: + m1: + examples: + - {expected.ToYaml(indent: 8)} + """; + + var document = await AsyncApiSerializer.FromYamlAsync(yaml); + + Assert.That(document, Is.Not.Null); + var msg = document.Components.Messages.GetValueOrNull("m1"); + Assert.That(msg?.ActualObject, Is.Not.Null); + Assert.That(msg.ActualObject.Examples, Is.Not.Null.And.Count.EqualTo(1)); + var expl = msg.ActualObject.Examples.Single(); + Assert.That(expl, Is.Not.Null); + expl.ShouldDeepEqual(expected, IgnoreUnmatchedProperties); + } + + [Test] + public async Task ReadExtension() + { + const string value = "424f5f4e-9e95-4e59-bc2a-2fb074e75680"; + const string MessageName = "msg1"; + + var yaml = $$""" + {{YamlHeader}} + components: + messages: + {{MessageName}}: + examples: + - {0}: '{{value}}' + """; + await ReadExtensionsTest(yaml, value, d => + { + var msg = d.Components.Messages.GetValueOrNull(MessageName); + Assert.That(msg, Is.Not.Null); + Assert.That(msg.ActualObject.Examples, Is.Not.Null.And.Count.EqualTo(1)); + return msg.ActualObject.Examples.Single(); + }); + } +} diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/MessageTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/MessageTests.cs new file mode 100644 index 0000000..58e2448 --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/MessageTests.cs @@ -0,0 +1,188 @@ +using ApiCodeGenerator.AsyncApi.DOM; +using NJsonSchema.References; + +namespace ApiCodeGenerator.AsyncApi.Tests.SerializationV3; + +public class MessageTests : TestBase +{ + private const string MessageName = "msg"; + + private const string MessageDefinition = $""" + components: + messages: + {MessageName}: + """; + + [Test] + public async Task ReadProperties() + { + var expected = new + { + Headers = new { SchemaFormat = "application/vnd.aai.asyncapi;version=3.0.0", Schema = new object() }, + Payload = new { SchemaFormat = "application/vnd.aai.asyncapi;version=3.0.0", Schema = new object() }, + CorrelationId = new { Location = "53e2420a-b0f2-4401-825b-b7bdd3a3df41" }, + ContentType = "7e0c3c05-4437-42b9-b52a-a1e85ec98a95", + Name = "fff3e3a5-b266-43be-908a-04a47d626890", + Title = "33629d50-2a44-409e-9687-700da06606de", + Summary = "17e58801-223a-4b0d-abe2-2c95c492ab7b", + Description = "7dc15aa6-4eb7-427b-9b64-d2bbf7c380b3", + Tags = new[] { new { Name = "62b89c8d-cb2d-457e-9553-c7be93de7e71" } }, + ExternalDocs = new { Url = "039a707a-85c5-474e-bfe8-be156f813e8f" }, + Bindings = new { Amqp = new object() }, + Examples = new[] { new { Name = "59d53925-c20f-4cda-b819-ea9b28af10fc" } }, + Traits = new { Title = "11690732-96c8-498c-9f9f-b54d5278f118" }, + }; + + var yaml = $""" + {YamlHeader} + {MessageDefinition} + {expected.ToYaml(indent: 6)} + """; + + await ReadPropertiesTest(yaml, expected, d => d.Components.Messages.GetValueOrNull(MessageName)); + } + + [Test] + public async Task HeadersRef() + { + const string refPath = "#/components/schemas/h"; + var expected = new { Title = "80313f49-cfa9-4e1c-aaf8-631c4144cf5c" }; + var yaml = $""" + {YamlHeader} + {MessageDefinition} + headers: + $ref: '{refPath}' + schemas: + h: + {expected.ToYaml(indent: 6)} + """; + + await MessageResolveReferernceTest(yaml, refPath, expected, m => m.Headers); + } + + [Test] + public async Task PayloadRef() + { + const string refPath = "#/components/schemas/p"; + var expected = new { Title = "d1301309-a6b6-4fde-988c-e1503ffd60fe" }; + var yaml = $""" + {YamlHeader} + {MessageDefinition} + payload: + $ref: '{refPath}' + schemas: + p: + {expected.ToYaml(indent: 6)} + """; + + await MessageResolveReferernceTest(yaml, refPath, expected, m => m.Payload); + } + + [Test] + public async Task CorrelationIdRef() + { + const string refPath = "#/components/correlationIds/cid"; + var expected = new { Location = "d1301309-a6b6-4fde-988c-e1503ffd60fe" }; + var yaml = $""" + {YamlHeader} + {MessageDefinition} + correlationId: + $ref: '{refPath}' + correlationIds: + cid: + {expected.ToYaml(indent: 6)} + """; + + await MessageResolveReferernceTest(yaml, refPath, expected, m => m.CorrelationId); + } + + [Test] + public async Task TagsRef() + { + const string refPath = "#/components/tags/tag"; + var expected = new { Name = "70664197-f27f-4352-b3e8-a0a1672e3163" }; + + var yaml = $""" + {YamlHeader} + {MessageDefinition} + tags: + - $ref: '{refPath}' + tags: + tag: + {expected.ToYaml(indent: 6)} + """; + + await MessageResolveReferernceTest(yaml, refPath, expected, o => + { + Assert.That(o.Tags, Is.Not.Null.And.Count.EqualTo(1)); + return o.Tags.Single(); + }); + } + + [Test] + public async Task ExternalDocsRef() + { + const string refPath = "#/components/externalDocs/d"; + var expected = new { Url = "http://some.url" }; + + var yaml = $""" + {YamlHeader} + {MessageDefinition} + externalDocs: + $ref: '{refPath}' + externalDocs: + d: + {expected.ToYaml(indent: 6)} + """; + + await MessageResolveReferernceTest(yaml, refPath, expected, o => o.ExternalDocs); + } + + [Test] + public async Task BindingsRef() + { + const string refPath = "#/components/messageBindings/b"; + var expected = new { Amqp = new { BindingVersion = "latest" } }; + + var yaml = $""" + {YamlHeader} + {MessageDefinition} + bindings: + $ref: '{refPath}' + messageBindings: + b: + {expected.ToYaml(indent: 6)} + """; + + await MessageResolveReferernceTest(yaml, refPath, expected, o => o.Bindings); + } + + [Test] + public async Task TraitsRef() + { + const string refPath = "#/components/messageTraits/t"; + var expected = new { Title = "9f7b58c5-63f5-4209-910a-b786c571eee4" }; + + var yaml = $""" + {YamlHeader} + {MessageDefinition} + traits: + $ref: '{refPath}' + messageTraits: + t: + {expected.ToYaml(indent: 6)} + """; + + await MessageResolveReferernceTest(yaml, refPath, expected, o => o.Traits); + } + + private Task MessageResolveReferernceTest(string yaml, string refPath, object expected, Func getRef) + where T : IJsonReference + => ResolveReferernceTest(yaml, refPath, expected, d => + { + Assert.That(d.Components.Messages, Is.Not.Null.And.ContainKey(MessageName)); + var msg = d.Components.Messages[MessageName]; + Assert.That(msg, Is.Not.Null); + return getRef(msg); + }); +} diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/MessageTraitTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/MessageTraitTests.cs new file mode 100644 index 0000000..58673cd --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/MessageTraitTests.cs @@ -0,0 +1,171 @@ +using ApiCodeGenerator.AsyncApi.DOM; +using ApiCodeGenerator.AsyncApi.DOM.Traits; +using NJsonSchema.References; + +namespace ApiCodeGenerator.AsyncApi.Tests.SerializationV3; + +public class MessageTraitTests : TestBase +{ + private const string MessageTraitsName = "mt"; + private const string MessageTraitsDefinition = $""" + components: + messageTraits: + {MessageTraitsName}: + """; + + [Test] + public async Task ReadProperties() + { + var expected = new + { + Headers = new { SchemaFormat = "application/vnd.aai.asyncapi;version=3.0.0", Schema = new object() }, + CorrelationId = new { Location = "2dd758cc-325b-4c1c-ba69-203ecd84f18e" }, + ContentType = "4b25742d-b1b7-491e-90ab-89eb0e14ddd8", + Name = "c616dbd9-5b60-4252-8b94-93598dc44c07", + Title = "0ab88636-d582-477d-9c59-8c4fdae57794", + Summary = "57c99c48-1d60-4340-b8ab-0cf92e7a2fa4", + Description = "206d1eff-46d4-4617-ab28-f8d0cfd63ac2", + Tags = new[] { new { Name = "tag" } }, + ExternalDocs = new { Url = "http://tempuri.org" }, + Bindings = new { Amqp = new { BindingVersion = "latest" } }, + Examples = new[] { new { Name = "abf70bc1-b535-451e-b18c-ea1dbdb8abeb" } }, + }; + + var yaml = $""" + {YamlHeader} + {MessageTraitsDefinition} + {expected.ToYaml(indent: 6)} + """; + + Reference? traitsRef = null; + await ReadPropertiesTest(yaml, expected, d => traitsRef = d.Components.MessageTraits.GetValueOrNull(MessageTraitsName)); + + Assert.NotNull(((IDocumentAware?)traitsRef?.ActualObject)?.Document); + + } + + [Test] + public async Task ReadExtensions() + { + const string value = "3be8a059-ecba-4ecd-98ee-01f22b2a725f"; + var yaml = + $$""" + {{YamlHeader}} + {{MessageTraitsDefinition}} + {0}: '{{value}}' + """; + + await ReadExtensionsTest(yaml, value, d => d.Components.MessageTraits.GetValueOrNull(MessageTraitsName)); + } + + [Test] + public async Task HeadersRef() + { + const string refPath = "#/components/schemas/h"; + var expected = new { Title = "1d69d02c-4aae-4235-8cdb-414929f8132b" }; + + var yaml = $""" + {YamlHeader} + {MessageTraitsDefinition} + headers: + $ref: '{refPath}' + schemas: + h: + {expected.ToYaml(indent: 6)} + """; + + await MessageTraitsResolveReferenceTest(yaml, refPath, expected, o => o.Headers); + } + + [Test] + public async Task CorrelationIdRef() + { + const string refPath = "#/components/correlationIds/cid"; + var expected = new { Location = "5994d55b-efe8-4587-b28a-d98d5c859971" }; + + var yaml = $""" + {YamlHeader} + {MessageTraitsDefinition} + correlationId: + $ref: '{refPath}' + correlationIds: + cid: + {expected.ToYaml(indent: 6)} + """; + + await MessageTraitsResolveReferenceTest(yaml, refPath, expected, o => o.CorrelationId); + } + + [Test] + public async Task TagsRef() + { + const string refPath = "#/components/tags/tag"; + var expected = new { Name = "5d9e4ea2-1fac-45fd-b37c-dfdfb62a9a74" }; + + var yaml = $""" + {YamlHeader} + {MessageTraitsDefinition} + tags: + - $ref: '{refPath}' + tags: + tag: + {expected.ToYaml(indent: 6)} + """; + + await MessageTraitsResolveReferenceTest(yaml, refPath, expected, o => + { + Assert.That(o.Tags, Is.Not.Null.And.Count.EqualTo(1)); + return o.Tags.Single(); + }); + } + + [Test] + public async Task ExternalDocsRef() + { + const string refPath = "#/components/externalDocs/d"; + var expected = new { Url = "http://some.url" }; + + var yaml = $""" + {YamlHeader} + {MessageTraitsDefinition} + externalDocs: + $ref: '{refPath}' + externalDocs: + d: + {expected.ToYaml(indent: 6)} + """; + + await MessageTraitsResolveReferenceTest(yaml, refPath, expected, o => o.ExternalDocs); + } + + [Test] + public async Task BindingsRef() + { + const string refPath = "#/components/messageBindings/b"; + var expected = new { Amqp = new { BindingVersion = "latest" } }; + + var yaml = $""" + {YamlHeader} + {MessageTraitsDefinition} + bindings: + $ref: '{refPath}' + messageBindings: + b: + {expected.ToYaml(indent: 6)} + """; + + await MessageTraitsResolveReferenceTest(yaml, refPath, expected, o => o.Bindings); + } + + private Task MessageTraitsResolveReferenceTest(string yaml, string refPath, object expected, Func getRef) + where T : IJsonReference + { + return ResolveReferernceTest(yaml, refPath, expected, d => + { + Assert.That(d.Components.MessageTraits, Is.Not.Null.And.ContainKey(MessageTraitsName)); + var op = d.Components.MessageTraits[MessageTraitsName]; + Assert.That(op, Is.Not.Null); + return getRef(op.ActualObject); + }); + } +} diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OAuthFlowsTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OAuthFlowsTests.cs new file mode 100644 index 0000000..211feeb --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OAuthFlowsTests.cs @@ -0,0 +1,192 @@ +using ApiCodeGenerator.AsyncApi.DOM; +using ApiCodeGenerator.AsyncApi.DOM.Security; + +namespace ApiCodeGenerator.AsyncApi.Tests.SerializationV3; + +public class OAuthFlowsTests : TestBase +{ + private const string FlowsDefintition = """ + components: + securitySchemes: + s: + type: oauth2 + flows: + """; + + [Test] + public async Task ReadExtensions() + { + const string value = "57e5afe1-d77d-483e-a8cf-e0337f7e23b0"; + var yaml = $$""" + {{YamlHeader}} + {{FlowsDefintition}} + {0}: '{{value}}' + """; + await ReadExtensionsTest(yaml, value, GetOAuthFlows); + } + + [Test] + public async Task ReadProperties() + { +#pragma warning disable SA1312 // Variable names should begin with lower-case letter + Dictionary AvailableScopes = new() { ["dbe82a7c-ef56-41fa-8aa7-042414c2f8fd"] = "bf230c6e-c872-4957-b1fc-8db5009cdee3" }; +#pragma warning restore SA1312 // Variable names should begin with lower-case letter + + var expected = new + { + Implicit = new + { + AuthorizationUrl = "992741d0-0e5b-4e48-b8f9-f05b7732e431", + RefreshUrl = "69e52c5e-8b38-4b3b-b720-a884d330f13e", + AvailableScopes, + }, + Password = new + { + TokenUrl = "eda8952e-42f4-4c6c-9131-72615a62a6f9", + RefreshUrl = "d39266f4-59bb-46ee-b45f-f0b40520d057", + AvailableScopes, + }, + ClientCredentials = new + { + TokenUrl = "ea510232-8fad-4e27-9ad0-6840fdbe2d50", + RefreshUrl = "845c5f38-4b54-45b6-9810-32377673c541", + AvailableScopes, + }, + AuthorizationCode = new + { + AuthorizationUrl = "061605f9-c061-4e9d-a4f4-e302f2fcea65", + TokenUrl = "93e9eacc-f575-4ffc-9d73-8a1521384bac", + RefreshUrl = "4d40c092-b3c8-47fe-9209-db3ba6eff7d5", + AvailableScopes, + }, + }; + + var yaml = $""" + {YamlHeader} + {FlowsDefintition} + {expected.ToYaml(indent: 8)} + """; + + await ReadPropertiesTest(yaml, expected, GetOAuthFlows); + } + + [TestCaseSource(nameof(GetRequiredPropertiesCases))] + public void RequiredProperties(object expected) + { + var yaml = $""" + {YamlHeader} + {FlowsDefintition} + {expected.ToYaml(indent: 8)} + """; + + RequiredPropertiesTest(yaml); + } + + private static IEnumerable GetRequiredPropertiesCases() + { +#pragma warning disable SA1312 // Variable names should begin with lower-case letter + Dictionary AvailableScopes = new() { ["dbe82a7c-ef56-41fa-8aa7-042414c2f8fd"] = "bf230c6e-c872-4957-b1fc-8db5009cdee3" }; +#pragma warning restore SA1312 // Variable names should begin with lower-case letter + + yield return new TestCaseData( + new + { + Implicit = new + { + AuthorizationUrl = "992741d0-0e5b-4e48-b8f9-f05b7732e431", + }, + }) + .SetArgDisplayNames("implicit: without availableScopes"); + + yield return new TestCaseData( + new + { + Implicit = new + { + AvailableScopes, + }, + }) + .SetArgDisplayNames("implicit: without authorizationUrl"); + + yield return new TestCaseData( + new + { + Password = new + { + TokenUrl = "ea73d2a8-968e-4de8-a5c4-c50d1b27343f", + }, + }) + .SetArgDisplayNames("password: without availableScopes"); + + yield return new TestCaseData( + new + { + Password = new + { + AvailableScopes, + }, + }) + .SetArgDisplayNames("password: without tokenUrl"); + + yield return new TestCaseData( + new + { + ClientCredentials = new + { + TokenUrl = "ea73d2a8-968e-4de8-a5c4-c50d1b27343f", + }, + }) + .SetArgDisplayNames("clientCredentials: without availableScopes"); + + yield return new TestCaseData( + new + { + ClientCredentials = new + { + AvailableScopes, + }, + }) + .SetArgDisplayNames("clientCredentials: without tokenUrl"); + + yield return new TestCaseData( + new + { + AuthorizationCode = new + { + TokenUrl = "69071823-16c5-476a-b9bb-1bcb4926ad38", + AuthorizationUrl = "cd147b08-42d3-480b-a9ea-9167e9dc8aef", + }, + }) + .SetArgDisplayNames("authorizationCode: without availableScopes"); + + yield return new TestCaseData( + new + { + AuthorizationCode = new + { + AvailableScopes, + AuthorizationUrl = "88cfe0a6-8ed3-4d59-a405-76a708899cb1", + }, + }) + .SetArgDisplayNames("authorizationCode: without tokenUrl"); + + yield return new TestCaseData( + new + { + AuthorizationCode = new + { + TokenUrl = "d88c1bd8-ea04-40d6-9add-b1a345a4d6ae", + AvailableScopes, + }, + }) + .SetArgDisplayNames("authorizationCode: without authorizationurl"); + } + + private static OAuthFlows GetOAuthFlows(AsyncApiDocument document) + { + var scheme = document.Components.SecuritySchemes.GetValueOrNull("s"); + Assert.That(scheme?.ActualObject, Is.Not.Null.And.TypeOf()); + var oa2scheme = (OAuth2SecurityScheme)scheme; + return oa2scheme.Flows; + } +} diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OperationBindingsTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OperationBindingsTests.cs new file mode 100644 index 0000000..08892d9 --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OperationBindingsTests.cs @@ -0,0 +1,44 @@ +using ApiCodeGenerator.AsyncApi.DOM.Bindings.Amqp; + +namespace ApiCodeGenerator.AsyncApi.Tests.SerializationV3; + +public class OperationBindingsTests : TestBase +{ + private const string OperationBindingName = "cb"; + private const string OperationBindingDefinition = $""" + components: + operationBindings: + {OperationBindingName}: + """; + + [Test] + public async Task ReadProperties() + { + var expected = new + { + Amqp = new OperationV0_3(), + }; + + var yaml = $""" + {YamlHeader} + {OperationBindingDefinition} + {expected.ToYaml(indent: 6)} + """; + + await ReadPropertiesTest(yaml, expected, d => d.Components.OperationBindings.GetValueOrNull(OperationBindingName)); + } + + [Test] + public async Task ReadExtensions() + { + const string value = "c4a6976e-a5af-4dd1-851c-bcbc73ee89b5"; + var yaml = + $$""" + {{YamlHeader}} + {{OperationBindingDefinition}} + {0}: '{{value}}' + """; + + await ReadExtensionsTest(yaml, value, d => d.Components.OperationBindings.GetValueOrNull(OperationBindingName)); + } +} diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OperationReplyAddressTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OperationReplyAddressTests.cs new file mode 100644 index 0000000..4a2127e --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OperationReplyAddressTests.cs @@ -0,0 +1,60 @@ +namespace ApiCodeGenerator.AsyncApi.Tests.SerializationV3; + +public class OperationReplyAddressTests : TestBase +{ + private const string OperationReplyAddressName = "oa"; + + private const string OperationReplyAddressDefinition = $""" + components: + replyAddresses: + {OperationReplyAddressName}: + """; + + [Test] + public void RequiredProperties() + { + const string yaml = $$""" + {{YamlHeader}} + {{OperationReplyAddressDefinition}} {} + """; + + RequiredPropertiesTest(yaml); + } + + [Test] + public async Task ReadProperties() + { + var expected = new + { + Description = "7de3980c-e8d4-40ba-801b-3de6702cbaf0", + Location = "1cede9bc-81cf-4149-a5ca-212dfe36c50c", + }; + + var yaml = $""" + {YamlHeader} + {OperationReplyAddressDefinition} + {expected.ToYaml(indent: 6)} + """; + + await ReadPropertiesTest(yaml, expected, d => + { + Assert.That(d.Components.ReplyAddresses, Is.Not.Null); + return d.Components.ReplyAddresses.GetValueOrNull(OperationReplyAddressName)?.ActualObject; + }); + } + + [Test] + public async Task ReadExtensions() + { + const string value = "18763153-b2db-4bab-acea-6b26311d561f"; + var yaml = + $$""" + {{YamlHeader}} + {{OperationReplyAddressDefinition}} + location: loc + {0}: '{{value}}' + """; + + await ReadExtensionsTest(yaml, value, d => d.Components.ReplyAddresses.GetValueOrNull(OperationReplyAddressName)); + } +} diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OperationReplyTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OperationReplyTests.cs new file mode 100644 index 0000000..e708d98 --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OperationReplyTests.cs @@ -0,0 +1,133 @@ +using ApiCodeGenerator.AsyncApi.DOM; +using NJsonSchema.References; + +namespace ApiCodeGenerator.AsyncApi.Tests.SerializationV3; + +public class OperationReplyTests : TestBase +{ + private const string OperationReplyName = "or"; + + private const string OperationReplyDefinition = $""" + components: + replies: + {OperationReplyName}: + """; + + [Test] + public async Task ReadProperties() + { + var expected = new + { + Address = new + { + ActualObject = new { Location = "bad10799-02b2-4f3c-b3a9-fb3315806e94" }, + }, + }; + + var yaml = $""" + {YamlHeader} + {OperationReplyDefinition} + address: + {expected.Address.ActualObject.ToYaml(indent: 8)} + """; + + await ReadPropertiesTest(yaml, expected, d => d.Components.Replies.GetValueOrNull(OperationReplyName)); + } + + [Test] + public async Task ReadExtensions() + { + const string value = "18763153-b2db-4bab-acea-6b26311d561f"; + var yaml = + $$""" + {{YamlHeader}} + {{OperationReplyDefinition}} + {0}: '{{value}}' + """; + + await ReadExtensionsTest(yaml, value, d => d.Components.Replies.GetValueOrNull(OperationReplyName)); + } + + [Test] + public async Task AddressRef() + { + const string refPath = "#/components/replyAddresses/ra"; + var expected = new + { + Location = "a244ba5d-70aa-4f91-ada0-f2e8b1a99e69", + }; + var yaml = $""" + {YamlHeader} + {OperationReplyDefinition} + address: + $ref: '{refPath}' + replyAddresses: + ra: + {expected.ToYaml(indent: 6)} + """; + + await OperationReplyResolveReferenceTest(yaml, refPath, expected, r => r.Address); + } + + [Test] + public async Task ChannelRef() + { + const string refPath = "#/components/channels/c"; + var expected = new + { + Address = "c51efc7d-aff5-44be-9336-918a26e3897f", + }; + var yaml = $""" + {YamlHeader} + {OperationReplyDefinition} + channel: + $ref: '{refPath}' + channels: + c: + {expected.ToYaml(indent: 6)} + """; + + await OperationReplyResolveReferenceTest(yaml, refPath, expected, r => r.Channel); + } + + [Test] + public async Task MessagesRef() + { + const string refPath = "#/components/messages/m"; + var expected = new + { + ContentType = "36e871ad-75b3-45ba-b1bf-56e74733cb20", + }; + var yaml = $""" + {YamlHeader} + {OperationReplyDefinition} + messages: + - $ref: '{refPath}' + messages: + m: + {expected.ToYaml(indent: 6)} + """; + + await OperationReplyResolveReferenceTest(yaml, refPath, expected, r => + { + Assert.That(r.Messages, Is.Not.Null.And.Count.EqualTo(1)); + return r.Messages.Single(); + }); + } + + private Task OperationReplyResolveReferenceTest(string yaml, string refPath, object expected, Func?> getRef) + where T : IJsonReference + { + return ResolveReferernceTest( + yaml, + refPath, + expected, + d => + { + Assert.That(d.Components.Replies, Is.Not.Null.And.ContainKey(OperationReplyName)); + var reply = d.Components.Replies[OperationReplyName]; + Assert.That(reply, Is.Not.Null); + return getRef(reply.ActualObject); + }); + } +} diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OperationTraitTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OperationTraitTests.cs new file mode 100644 index 0000000..ff8c95d --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OperationTraitTests.cs @@ -0,0 +1,151 @@ +using ApiCodeGenerator.AsyncApi.DOM; +using ApiCodeGenerator.AsyncApi.DOM.Traits; +using NJsonSchema.References; + +namespace ApiCodeGenerator.AsyncApi.Tests.SerializationV3; + +public class OperationTraitTests : TestBase +{ + private const string OperationTraitsName = "ot"; + private const string OperationTraitsDefinition = $""" + components: + operationTraits: + {OperationTraitsName}: + """; + + [Test] + public async Task ReadProperties() + { + var expected = new + { + Title = "0796b4fe-a20b-4614-ba5e-e204252316e3", + Summary = "9ced3caa-39a9-4ab1-b382-9164fdbbb634", + Description = "fd67aa43-f1a9-4cb7-9c7f-748706b00d30", + Security = new[] { new { Type = "plain" } }, + Tags = new[] { new { Name = "tag" } }, + ExternalDocs = new { Url = "http://tempuri.org" }, + Bindings = new { Amqp = new { Expiration = 2 } }, + }; + + var yaml = $""" + {YamlHeader} + {OperationTraitsDefinition} + {expected.ToYaml(indent: 6)} + """; + + Reference? traitsRef = null; + await ReadPropertiesTest(yaml, expected, d => traitsRef = d.Components.OperationTraits.GetValueOrNull(OperationTraitsName)); + + Assert.NotNull(((IDocumentAware?)traitsRef?.ActualObject)?.Document); + } + + [Test] + public async Task ReadExtensions() + { + const string value = "6a85de5c-84f2-4b9e-9e37-498953703255"; + var yaml = + $$""" + {{YamlHeader}} + {{OperationTraitsDefinition}} + {0}: '{{value}}' + """; + + await ReadExtensionsTest(yaml, value, d => d.Components.OperationTraits.GetValueOrNull(OperationTraitsName)); + } + + [Test] + public async Task SecurityRef() + { + const string refPath = "#/components/securitySchemes/plain"; + var expected = new { Type = "plain" }; + + var yaml = $""" + {YamlHeader} + {OperationTraitsDefinition} + security: + - $ref: '{refPath}' + securitySchemes: + plain: + {expected.ToYaml(indent: 6)} + """; + + await OperationTraitsResolveReferenceTest(yaml, refPath, expected, o => + { + Assert.That(o.Security, Is.Not.Null.And.Count.EqualTo(1)); + return o.Security.Single(); + }); + } + + [Test] + public async Task TagsRef() + { + const string refPath = "#/components/tags/tag"; + var expected = new { Name = "2afe15f5-0aab-4ad7-af7a-28734bb34cef" }; + + var yaml = $""" + {YamlHeader} + {OperationTraitsDefinition} + tags: + - $ref: '{refPath}' + tags: + tag: + {expected.ToYaml(indent: 6)} + """; + + await OperationTraitsResolveReferenceTest(yaml, refPath, expected, o => + { + Assert.That(o.Tags, Is.Not.Null.And.Count.EqualTo(1)); + return o.Tags.Single(); + }); + } + + [Test] + public async Task ExternalDocsRef() + { + const string refPath = "#/components/externalDocs/d"; + var expected = new { Url = "http://some.url" }; + + var yaml = $""" + {YamlHeader} + {OperationTraitsDefinition} + externalDocs: + $ref: '{refPath}' + externalDocs: + d: + {expected.ToYaml(indent: 6)} + """; + + await OperationTraitsResolveReferenceTest(yaml, refPath, expected, o => o.ExternalDocs); + } + + [Test] + public async Task BindingsRef() + { + const string refPath = "#/components/operationBindings/b"; + var expected = new { Amqp = new { Ack = true } }; + + var yaml = $""" + {YamlHeader} + {OperationTraitsDefinition} + bindings: + $ref: '{refPath}' + operationBindings: + b: + {expected.ToYaml(indent: 6)} + """; + + await OperationTraitsResolveReferenceTest(yaml, refPath, expected, o => o.Bindings); + } + + private Task OperationTraitsResolveReferenceTest(string yaml, string refPath, object expected, Func?> getRef) + where T : IJsonReference + { + return ResolveReferernceTest(yaml, refPath, expected, d => + { + Assert.That(d.Components.OperationTraits, Is.Not.Null.And.ContainKey(OperationTraitsName)); + var op = d.Components.OperationTraits[OperationTraitsName]; + Assert.That(op, Is.Not.Null); + return getRef(op.ActualObject); + }); + } +} diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OpertationTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OpertationTests.cs new file mode 100644 index 0000000..744d147 --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OpertationTests.cs @@ -0,0 +1,244 @@ +using ApiCodeGenerator.AsyncApi.DOM; +using NJsonSchema.References; + +namespace ApiCodeGenerator.AsyncApi.Tests.SerializationV3; + +public class OpertationTests : TestBase +{ + private const string OperationName = "op1"; + private const string OperationDefinition = $""" + components: + channels: + ch: + title: ch + operations: + {OperationName}: + channel: + $ref: '#/components/channels/ch' + """ + "\r\n"; + + [TestCase(YamlHeader + """ + components: + operations: + op1: + action: send + """, + TestName = nameof(RequiredProperties) + "- without channel")] + [TestCase( + OperationDefinition, + TestName = nameof(RequiredProperties) + "- without action")] + public void RequiredProperties(string yaml) + { + RequiredPropertiesTest(yaml); + } + + [Test] + public async Task ReadExtensions() + { + const string value = "18763153-b2db-4bab-acea-6b26311d561f"; + var yaml = + $$""" + {{YamlHeader}} + {{OperationDefinition}} + action: send + {0}: '{{value}}' + """; + + await ReadExtensionsTest(yaml, value, d => d.Components.Operations.GetValueOrNull(OperationName)); + } + + [Test] + public async Task ReadProperties() + { + var expected = new + { + Action = "Send", + Title = "1aeb7ca8-5b11-4404-a7ff-766f129e2a4b", + Summary = "656021a0-69f7-4322-8703-50bbd74a6ac4", + Description = "12b13814-4919-4e20-8b4f-373c4e983ca3", + SecurtityScheme = new { Type = "test" }, + Tags = new[] { new { Name = "tag" } }, + ExternalDocs = new { Url = "url" }, + Bindings = new { Amqp = new object() }, + Traits = new { Title = "abf76138-4a3b-43d6-b4b6-488a4e7a69e6" }, + Messages = new object[0], + Reply = new { Address = new { Location = "e6c1c3d4-691e-476d-ba69-65be200066f9" } }, + }; + + var yaml = $""" + {YamlHeader} + {OperationDefinition} + {expected.ToYaml(indent: 6)} + """; + + Reference? operation = null; + await ReadPropertiesTest(yaml, expected, d => operation = d.Components.Operations.GetValueOrNull(OperationName)); + Assert.That(operation!.ActualObject.Channel, Is.Not.Null); + Assert.That(operation!.ActualObject.Channel.ActualObject.Title, Is.EqualTo("ch")); + } + + [Test] + public async Task SecurityRef() + { + const string refPath = "#/components/securitySchemes/plain"; + var expected = new { Type = "plain" }; + + var yaml = $""" + {YamlHeader} + {OperationDefinition} + action: send + security: + - $ref: '{refPath}' + securitySchemes: + plain: + {expected.ToYaml(indent: 6)} + """; + + await OperationResolveReferenceTest(yaml, refPath, expected, o => + { + Assert.That(o.Security, Is.Not.Null.And.Count.EqualTo(1)); + return o.Security.Single(); + }); + } + + [Test] + public async Task TagsRef() + { + const string refPath = "#/components/tags/tag"; + var expected = new { Name = "2afe15f5-0aab-4ad7-af7a-28734bb34cef" }; + + var yaml = $""" + {YamlHeader} + {OperationDefinition} + action: send + tags: + - $ref: '{refPath}' + tags: + tag: + {expected.ToYaml(indent: 6)} + """; + + await OperationResolveReferenceTest(yaml, refPath, expected, o => + { + Assert.That(o.Tags, Is.Not.Null.And.Count.EqualTo(1)); + return o.Tags.Single(); + }); + } + + [Test] + public async Task ExternalDocsRef() + { + const string refPath = "#/components/externalDocs/d"; + var expected = new { Url = "http://some.url" }; + + var yaml = $""" + {YamlHeader} + {OperationDefinition} + action: send + externalDocs: + $ref: '{refPath}' + externalDocs: + d: + {expected.ToYaml(indent: 6)} + """; + + await OperationResolveReferenceTest(yaml, refPath, expected, o => o.ExternalDocs); + } + + [Test] + public async Task BindingsRef() + { + const string refPath = "#/components/operationBindings/b"; + var expected = new { Amqp = new { Ack = true } }; + + var yaml = $""" + {YamlHeader} + {OperationDefinition} + action: send + bindings: + $ref: '{refPath}' + operationBindings: + b: + {expected.ToYaml(indent: 6)} + """; + + await OperationResolveReferenceTest(yaml, refPath, expected, o => o.Bindings); + } + + [Test] + public async Task TraitsRef() + { + const string refPath = "#/components/operationTraits/t"; + var expected = new { Title = "ea361810-a63f-434e-91f1-a343f23d97c4" }; + + var yaml = $""" + {YamlHeader} + {OperationDefinition} + action: send + traits: + $ref: '{refPath}' + operationTraits: + t: + {expected.ToYaml(indent: 6)} + """; + + await OperationResolveReferenceTest(yaml, refPath, expected, o => o.Traits); + } + + [Test] + public async Task MessageRef() + { + const string refPath = "#/components/messages/m"; + var expected = new { Name = "61dbf59b-25fb-4004-a4a8-fef224e284c1" }; + + var yaml = $""" + {YamlHeader} + {OperationDefinition} + action: send + messages: + - $ref: '{refPath}' + messages: + m: + {expected.ToYaml(indent: 6)} + """; + + await OperationResolveReferenceTest(yaml, refPath, expected, o => + { + Assert.That(o.Messages, Is.Not.Null.And.Count.EqualTo(1)); + return o.Messages.Single(); + }); + } + + [Test] + public async Task ReplyRef() + { + const string refPath = "#/components/replies/r"; + var expected = new { Address = new { ActualObject = new { Location = "a504b027-8b98-40ad-991c-ad6dfd37839e" } } }; + + var yaml = $""" + {YamlHeader} + {OperationDefinition} + action: send + reply: + $ref: '{refPath}' + replies: + r: + address: + location: {expected.Address.ActualObject.Location} + """; + + await OperationResolveReferenceTest(yaml, refPath, expected, s => s.Reply); + } + + private Task OperationResolveReferenceTest(string yaml, string refPath, object expected, Func?> getRef) + where T : IJsonReference + { + return ResolveReferernceTest(yaml, refPath, expected, d => + { + Assert.That(d.Components.Operations, Is.Not.Null.And.ContainKey(OperationName)); + var op = d.Components.Operations[OperationName]; + Assert.That(op, Is.Not.Null); + return getRef(op.ActualObject); + }); + } +} diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ParameterTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ParameterTests.cs new file mode 100644 index 0000000..67d305e --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ParameterTests.cs @@ -0,0 +1,47 @@ +namespace ApiCodeGenerator.AsyncApi.Tests.SerializationV3; + +public class ParameterTests : TestBase +{ + private const string ParameterName = "p"; + + private const string ParameterDefinition = $""" + components: + parameters: + {ParameterName}: + """; + + [Test] + public async Task ReadProperties() + { + var expected = new + { + Enum = new[] { "bbcd2c7d-e07b-4b87-8a97-6d4691768c8a" }, + Default = "d1874489-f30d-42e5-9194-34a85dac4066", + Description = "71b2d238-bb46-4efb-bf78-d748239a8b0e", + Examples = new[] { "a24945c0-4f6c-448c-892f-f10d1b77cf64" }, + Location = "60270d0a-e624-4afe-a847-2624a87a6fb3", + }; + + var yaml = $""" + {YamlHeader} + {ParameterDefinition} + {expected.ToYaml(indent: 6)} + """; + + await ReadPropertiesTest(yaml, expected, d => d.Components.Parameters.GetValueOrNull(ParameterName)); + } + + [Test] + public async Task ReadExtensions() + { + const string value = "18763153-b2db-4bab-acea-6b26311d561f"; + var yaml = + $$""" + {{YamlHeader}} + {{ParameterDefinition}} + {0}: '{{value}}' + """; + + await ReadExtensionsTest(yaml, value, d => d.Components.Parameters.GetValueOrNull(ParameterName)); + } +} diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/SecuritySchemeTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/SecuritySchemeTests.cs new file mode 100644 index 0000000..ec5b88f --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/SecuritySchemeTests.cs @@ -0,0 +1,160 @@ +using ApiCodeGenerator.AsyncApi.DOM.Security; +using NUnit.Framework.Internal; + +namespace ApiCodeGenerator.AsyncApi.Tests.SerializationV3; + +public class SecuritySchemeTests : TestBase +{ + private const string SchemeName = "sec"; + + private const string SchemeDefinintion = $""" + components: + securitySchemes: + {SchemeName}: + """; + + [TestCaseSource(nameof(GetRequiredPropertiesCases))] + public void RequiredProperties(string yaml) + => RequiredPropertiesTest(yaml); + + [Test] + public async Task ReadExtensions() + { + const string value = "dce52072-8342-4ad9-a311-b763b70cb645"; + var yaml = $$""" + {{YamlHeader}} + {{SchemeDefinintion}} + {0}: '{{value}}' + type: plain + """; + await ReadExtensionsTest(yaml, value, d => d.Components.SecuritySchemes.GetValueOrNull(SchemeName)); + } + + [TestCaseSource(nameof(GetReadPropertiesCases))] + public async Task ReadProperties(object expected) + { + var yaml = $""" + {YamlHeader} + {SchemeDefinintion} + {expected.ToYaml(indent: 6)} + """; + await ReadPropertiesTest(yaml, expected, d => d.Components.SecuritySchemes.GetValueOrNull(SchemeName)); + } + + private static IEnumerable GetRequiredPropertiesCases() + { + yield return new TestCaseData($""" + {YamlHeader} + {SchemeDefinintion} + description: '' + """) + .SetArgDisplayNames("Type not set"); + + yield return new TestCaseData($""" + {YamlHeader} + {SchemeDefinintion} + type: apiKey + """) + .SetArgDisplayNames("ApiKey without 'in'"); + + yield return new TestCaseData($""" + {YamlHeader} + {SchemeDefinintion} + type: http + """) + .SetArgDisplayNames("Http without 'scheme'"); + + yield return new TestCaseData($""" + {YamlHeader} + {SchemeDefinintion} + type: httpApiKey + name: '123' + """) + .SetArgDisplayNames("HttpApiKey without 'in'"); + + yield return new TestCaseData($""" + {YamlHeader} + {SchemeDefinintion} + type: httpApiKey + in: cookie + """) + .SetArgDisplayNames("HttpApiKey without 'name'"); + + yield return new TestCaseData($""" + {YamlHeader} + {SchemeDefinintion} + type: oauth2 + """) + .SetArgDisplayNames("OAuth2 without 'flows'"); + + yield return new TestCaseData($""" + {YamlHeader} + {SchemeDefinintion} + type: openIdConnect + """) + .SetArgDisplayNames("OpenIdConnect without 'openIdConnectUrl'"); + } + + private static IEnumerable GetReadPropertiesCases() + { + yield return new TestCaseData( + new + { + Type = "apiKey", + Description = "8dfc9713-c472-422f-80fb-ec0e59334ec7", + In = ApiKeySecuritySchemaLocations.Password, + }) + .SetArgDisplayNames("ApiKey"); + + yield return new TestCaseData( + new + { + Type = "httpApiKey", + Description = "4acd16d5-5c6c-4c67-bf28-7b8f9244eeba", + In = HttpApiKeySecuritySchemaLocations.Cookie, + Name = "e9ae9169-6751-4b95-a90b-9abea3852c07", + }) + .SetArgDisplayNames("HttpApiKey"); + + yield return new TestCaseData( + new + { + Type = "http", + Description = "38e593e6-2baf-4f67-bafc-c4097f0a52aa", + Scheme = "Bearer", + BearerFormat = "7dbccdae-df1f-4b7d-80ac-0fc0c082e157", + }) + .SetArgDisplayNames("Http"); + + yield return new TestCaseData( + new + { + Type = "openIdConnect", + Description = "d21a4c72-bc8b-4253-8d7a-1e598a3e2183", + OpenIdConnectUrl = "42d7b73b-d8e0-4476-a514-6845920c7cb9", + Scopes = new[] { "s1", "s2" }, + }) + .SetArgDisplayNames("OpenIdConnect"); + + yield return new TestCaseData( + new + { + Type = "oauth2", + Description = "9b47bfac-db22-44e2-857f-d2e175b34c5f", + Flows = new object(), + Scopes = new[] { "s1", "s2" }, + }) + .SetArgDisplayNames("OAuth2"); + + foreach (var t in new[] { "userPassword", "X509", "symmetricEncryption", "asymmetricEncryption", "plain", "scramSha256", "scramSha512", "gssapi" }) + { + yield return new TestCaseData( + new + { + Type = t, + Description = "8abd50e4-9995-4491-bea5-244149e84e30", + }) + .SetArgDisplayNames(t); + } + } +} diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ServerBindingsTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ServerBindingsTests.cs new file mode 100644 index 0000000..ab58d4a --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ServerBindingsTests.cs @@ -0,0 +1,44 @@ +using ApiCodeGenerator.AsyncApi.DOM.Bindings.Amqp; + +namespace ApiCodeGenerator.AsyncApi.Tests.SerializationV3; + +public class ServerBindingsTests : TestBase +{ + private const string ServerBindingName = "sb"; + private const string ServerBindingDefinition = $""" + components: + serverBindings: + {ServerBindingName}: + """; + + [Test] + public async Task ReadProperties() + { + var expected = new + { + Amqp = new Server(), + }; + + var yaml = $""" + {YamlHeader} + {ServerBindingDefinition} + {expected.ToYaml(indent: 6)} + """; + + await ReadPropertiesTest(yaml, expected, d => d.Components.ServerBindings.GetValueOrNull(ServerBindingName)); + } + + [Test] + public async Task ReadExtensions() + { + const string value = "1294d95f-6662-4c0b-a0d9-12eb319ac4c7"; + var yaml = + $$""" + {{YamlHeader}} + {{ServerBindingDefinition}} + {0}: '{{value}}' + """; + + await ReadExtensionsTest(yaml, value, d => d.Components.ServerBindings.GetValueOrNull(ServerBindingName)); + } +} diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ServerTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ServerTests.cs new file mode 100644 index 0000000..7aee020 --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ServerTests.cs @@ -0,0 +1,187 @@ +using ApiCodeGenerator.AsyncApi.DOM; +using NJsonSchema.References; + +namespace ApiCodeGenerator.AsyncApi.Tests.SerializationV3; + +public class ServerTests : TestBase +{ + private const string ServerName = "test"; + + private const string ServerDefinition = """ + components: + servers: + test: + """; + + private const string ServerDefinitionWithReq = $""" + {ServerDefinition} + protocol: 'http' + host: 'host' + """; + + [TestCase($""" + {YamlHeader} + components: + servers: + test: + host: '' + """, + TestName = $"{nameof(RequiredProperties)} - without protocol")] + [TestCase($""" + {YamlHeader} + components: + servers: + test: + protocol: '' + """, + TestName = $"{nameof(RequiredProperties)} - without host")] + public void RequiredProperties(string yaml) + => RequiredPropertiesTest(yaml); + + [Test] + public async Task ReadProperties() + { + var expected = new + { + Host = "tempuri.org", + Protocol = "https", + ProtocolVersion = "10.0", + Pathname = "242f423a-22c6-431e-8f9f-bcda007aa246", + Description = "8c3f9bff-fbb6-45cb-849f-273268b3d36a", + Title = "8d76741a-3edd-48ff-ac91-f7f13c226eb1", + Summary = "64ab03dc-17a2-4447-8951-ba343f7458cf", + Variables = new Dictionary() { ["v"] = new { Default = "3cd855b0-170f-49a9-8c3a-4b957b1ec591" } }, + Security = new[] { new { Type = "plain" } }, + Tags = new object[0], + ExternalDocs = new { Url = "7bb3a3d6-64d8-421a-852f-534ff54580e2" }, + Bindings = new object(), + }; + + var yaml = $""" + {YamlHeader} + {ServerDefinition} + {expected.ToYaml(indent: 6)} + """; + + await ReadPropertiesTest(yaml, expected, d => d.Components.Servers.GetValueOrNull(ServerName)); + } + + [Test] + public async Task VariableRef() + { + const string NAME = "varRef"; + const string refPath = "#/components/serverVariables/v"; + var expected = new { Default = "1055e5fe-1737-4ba6-9320-2cfa22df54d2" }; + + var yaml = $""" + {YamlHeader} + {ServerDefinitionWithReq} + variables: + {NAME}: + $ref: '#/components/serverVariables/v' + serverVariables: + v: + {expected.ToYaml(indent: 6)} + """; + + await ServerResolveReferenceTest(yaml, refPath, expected, s => + { + Assert.That(s.Variables, Is.Not.Null.And.ContainKey(NAME)); + return s.Variables[NAME]; + }); + } + + [Test] + public async Task SecurityRef() + { + const string refPath = "#/components/securitySchemes/plain"; + var expected = new { Type = "plain" }; + + var yaml = $""" + {YamlHeader} + {ServerDefinitionWithReq} + security: + - $ref: '{refPath}' + securitySchemes: + plain: + {expected.ToYaml(indent: 6)} + """; + + await ServerResolveReferenceTest(yaml, refPath, expected, s => + { + Assert.That(s.Security, Is.Not.Null.And.Count.EqualTo(1)); + return s.Security.Single(); + }); + } + + [Test] + public async Task TagsRef() + { + const string refPath = "#/components/tags/tag"; + var expected = new { Name = "2afe15f5-0aab-4ad7-af7a-28734bb34cef" }; + + var yaml = $""" + {YamlHeader} + {ServerDefinitionWithReq} + tags: + - $ref: '{refPath}' + tags: + tag: + {expected.ToYaml(indent: 6)} + """; + + await ServerResolveReferenceTest(yaml, refPath, expected, s => + { + Assert.That(s.Tags, Is.Not.Null.And.Count.EqualTo(1)); + return s.Tags.Single(); + }); + } + + [Test] + public async Task ExternalDocsRef() + { + const string refPath = "#/components/externalDocs/d"; + var expected = new { Url = "http://some.url" }; + + var yaml = $""" + {YamlHeader} + {ServerDefinitionWithReq} + externalDocs: + $ref: '{refPath}' + externalDocs: + d: + {expected.ToYaml(indent: 6)} + """; + + await ServerResolveReferenceTest(yaml, refPath, expected, s => s.ExternalDocs); + } + + [Test] + public async Task BindingsRef() + { + const string refPath = "#/components/serverBindings/b"; + var expected = new { Amqp = new object() }; + + var yaml = $""" + {YamlHeader} + {ServerDefinitionWithReq} + bindings: + $ref: '{refPath}' + serverBindings: + b: + {expected.ToYaml(indent: 6)} + """; + + await ServerResolveReferenceTest(yaml, refPath, expected, s => s.Bindings); + } + + private Task ServerResolveReferenceTest(string yaml, string refPath, object expected, Func?> getRef) + where T : IJsonReference + => ResolveReferernceTest(yaml, refPath, expected, d => + { + Assert.That(d.Components.Servers, Is.Not.Null.And.ContainKey(ServerName)); + var server = d.Components.Servers.GetValueOrNull(ServerName); + Assert.That(server, Is.Not.Null); + return getRef(server.ActualObject); + }); +} diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/TagTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/TagTests.cs new file mode 100644 index 0000000..ca71862 --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/TagTests.cs @@ -0,0 +1,78 @@ +namespace ApiCodeGenerator.AsyncApi.Tests.SerializationV3; + +public class TagTests : TestBase +{ + private const string TagName = "tg"; + private const string TagDefinition = $""" + components: + tags: + {TagName}: + """; + + [Test] + public void RequiredProperties() + { + var yaml = $""" + {YamlHeader} + {TagDefinition} + description: '' + """; + RequiredPropertiesTest(yaml); + } + + [Test] + public async Task ReadProperties() + { + var expected = new + { + Name = "070e42d2-dcdb-4f9e-a437-6699abdea7f9", + Description = "ef001bd0-f0aa-49d9-8e24-991c9904abd5", + ExternalDocs = new { Url = "dbbd5cc5-acbf-4bff-a547-f34d97b776a4" }, + }; + + var yaml = $""" + {YamlHeader} + {TagDefinition} + {expected.ToYaml(indent: 6)} + """; + + await ReadPropertiesTest(yaml, expected, d => d.Components.Tags.GetValueOrNull(TagName)); + } + + [Test] + public async Task ReadExtensions() + { + const string value = "528df6af-a938-488b-a177-e3fd0173a90e"; + var yaml = $$""" + {{YamlHeader}} + {{TagDefinition}} + name: 't' + {0}: '{{value}}' + """; + await ReadExtensionsTest(yaml, value, d => d.Components.Tags.GetValueOrNull(TagName)); + } + + [Test] + public async Task ExternalDocsRef() + { + const string refPath = "#/components/externalDocs/ed"; + var expected = new { Url = "b537cbb2-c126-43f3-8a29-feb0652b4fc6" }; + var yaml = $""" + {YamlHeader} + {TagDefinition} + name: 'tag' + externalDocs: + $ref: '{refPath}' + externalDocs: + ed: + {expected.ToYaml(indent: 6)} + """; + + await ResolveReferernceTest(yaml, refPath, expected, d => + { + var tag = d.Components.Tags.GetValueOrNull(TagName); + Assert.That(tag, Is.Not.Null); + return tag.ActualObject.ExternalDocs; + }); + } +} diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/TestBase.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/TestBase.cs new file mode 100644 index 0000000..9a1135a --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/TestBase.cs @@ -0,0 +1,68 @@ +using ApiCodeGenerator.AsyncApi.DOM; +using NJsonSchema; +using NJsonSchema.References; + +namespace ApiCodeGenerator.AsyncApi.Tests.SerializationV3; + +public abstract class TestBase +{ + protected const string YamlHeader = """ + asyncapi: '3.0.0' + info: + title: t + version: '1.0' + + """; + + protected void RequiredPropertiesTest(string yaml) + { + var ex = Assert.ThrowsAsync(() => AsyncApiSerializer.FromYamlAsync(yaml)); + Assert.That(ex, Is.Not.Null); + Assert.That(ex.Message, Does.StartWith("Required property ")); + } + + protected async Task ReadExtensionsTest(string yaml, string value, Func getter) + { + const string propName = "x-test"; + + var document = await AsyncApiSerializer.FromYamlAsync(string.Format(yaml, propName)); + + var obj = getter(document); + Assert.That(obj, Is.Not.Null); + if (obj is IJsonReference jr) + { + obj = jr.ActualObject; + } + + Assert.That(obj, Is.Not.Null.And.InstanceOf()); + var extObj = (JsonExtensionObject)obj!; + Assert.That(extObj.ExtensionData, Is.Not.Null.And.ContainKey(propName)); + Assert.That(extObj.ExtensionData[propName], Is.EqualTo(value)); + } + + protected async Task ReadPropertiesTest(string yaml, object expected, Func getter) + { + var document = await AsyncApiSerializer.FromYamlAsync(yaml); + + var obj = getter(document); + + Assert.That(obj, Is.Not.Null); + if (obj is IJsonReference jr) + { + obj = jr.ActualObject; + } + + obj.ShouldDeepEqual(expected, IgnoreUnmatchedProperties); + } + + protected async Task ResolveReferernceTest(string yaml, string refPath, object expected, Func getRef) + where T : IJsonReference + { + var document = await AsyncApiSerializer.FromYamlAsync(yaml); + + var objRef = getRef(document); + Assert.That(objRef, Is.Not.Null); + Assert.That(objRef.ReferencePath, Is.EqualTo(refPath)); + objRef.ActualObject.ShouldDeepEqual(expected, IgnoreUnmatchedProperties); + } +} From 82c518f382c8aff1c3d1a00f0ed28986a5d880df Mon Sep 17 00:00:00 2001 From: Gennady Pundikov Date: Thu, 29 May 2025 09:10:11 +0300 Subject: [PATCH 2/3] AsyncApi3 test and read V2 --- .../CSharp/CSharpGeneratorBase.cs | 5 +- .../CSharp/Models/CSharpOperationModel.cs | 2 +- .../CSharp/Models/CSharpParameterModel.cs | 2 + .../CSharp/OperationTypes.cs | 6 +- .../DOM/AsyncApiDocument.cs | 2 +- .../DOM/AsyncApiSchema.cs | 11 +- .../DOM/Bindings/Amqp/ChannelType.cs | 8 +- src/ApiCodeGenerator.AsyncApi/DOM/Channel.cs | 2 +- .../DOM/Components.cs | 6 +- .../DOM/Internal/NamedReferenceDictionary.cs | 5 +- src/ApiCodeGenerator.AsyncApi/DOM/Message.cs | 2 +- .../DOM/NamedReference.cs | 7 + .../DOM/Operation.cs | 2 +- .../DOM/OperationAction.cs | 4 +- .../Serialization/AsyncApiReferenceUpdater.cs | 2 +- .../Serialization/AsyncApiSchemaConverter.cs | 18 +- .../AsyncApiSerializationException.cs | 14 + .../Serialization/AsyncApiSerializer.V2.cs | 41 ++ .../DOM/Serialization/AsyncApiSerializer.cs | 68 +- .../DOM/Serialization/InheritanceConverter.cs | 7 +- .../DOM/Serialization/RefObjectConverter.cs | 7 +- .../DOM/Traits/ITraitsAware.cs | 2 +- .../DOM/Traits/TraitsExtensions.cs | 8 +- .../DOM/V2/ChannelItem.cs | 27 + .../DOM/V2/ChannelItemConverter.cs | 237 +++++++ .../DOM/V2/ContractResolver.cs | 37 ++ .../DOM/V2/Message.cs | 56 ++ .../DOM/V2/MessageConverter.cs | 54 ++ .../DOM/V2/Operation.cs | 39 ++ .../DOM/V2/ReferenceResolver.cs | 108 ++++ .../DOM/V2/SecurityRequirement.cs | 7 + .../DOM/V2/Server.cs | 30 + .../DOM/V2/ServerConverter.cs | 47 ++ .../Templates/Client.Class.liquid | 6 +- .../Templates/Client.Interface.liquid | 2 +- ...d => Client.Operation.Receive.Body.liquid} | 0 ...quid => Client.Operation.Send.Body.liquid} | 0 .../TemplatesAmqp/Client.ChannelPool.liquid | 6 +- ...d => Client.Operation.Receive.Body.liquid} | 4 +- ...quid => Client.Operation.Send.Body.liquid} | 6 +- .../AmqpFunctionalTests.cs | 41 +- .../AsyncApiContentGeneratorTests.cs | 144 +---- .../FunctionalTests.cs | 8 +- .../Infrastructure/FakeTextPreprocessor.cs | 5 + .../Infrastructure/TestHelpers.Amqp.cs | 44 +- .../Infrastructure/TestHelpers.cs | 12 +- .../SerializationV2/MigrationTests.cs | 588 ++++++++++++++++++ .../SerializationV3/AsyncApiDocumentTests.cs | 8 +- .../SerializationV3/CorrelationIdTests.cs | 2 +- .../ExternalDocumentationTests.cs | 2 +- .../SerializationV3/InfoTests.cs | 10 +- .../SerializationV3/LicenseTests.cs | 7 +- .../SerializationV3/MessageTests.cs | 6 +- .../SerializationV3/OAuthFlowsTests.cs | 49 +- .../OperationReplyAddressTests.cs | 2 +- .../SerializationV3/OpertationTests.cs | 18 +- .../SerializationV3/SecuritySchemeTests.cs | 39 +- .../SerializationV3/ServerTests.cs | 10 +- .../SerializationV3/TagTests.cs | 2 +- .../SerializationV3/TestBase.cs | 14 +- .../asyncApi/asyncapi.yml | 177 +++--- 61 files changed, 1697 insertions(+), 388 deletions(-) create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSerializationException.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSerializer.V2.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/V2/ChannelItem.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/V2/ChannelItemConverter.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/V2/ContractResolver.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/V2/Message.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/V2/MessageConverter.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/V2/Operation.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/V2/ReferenceResolver.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/V2/SecurityRequirement.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/V2/Server.cs create mode 100644 src/ApiCodeGenerator.AsyncApi/DOM/V2/ServerConverter.cs rename src/ApiCodeGenerator.AsyncApi/Templates/{Client.Opertaion.Subscribe.Body.liquid => Client.Operation.Receive.Body.liquid} (100%) rename src/ApiCodeGenerator.AsyncApi/Templates/{Client.Opertaion.Publish.Body.liquid => Client.Operation.Send.Body.liquid} (100%) rename src/ApiCodeGenerator.AsyncApi/TemplatesAmqp/{Client.Opertaion.Subscribe.Body.liquid => Client.Operation.Receive.Body.liquid} (91%) rename src/ApiCodeGenerator.AsyncApi/TemplatesAmqp/{Client.Opertaion.Publish.Body.liquid => Client.Operation.Send.Body.liquid} (88%) create mode 100644 test/ApiCodeGenerator.AsyncApi.Tests/SerializationV2/MigrationTests.cs diff --git a/src/ApiCodeGenerator.AsyncApi/CSharp/CSharpGeneratorBase.cs b/src/ApiCodeGenerator.AsyncApi/CSharp/CSharpGeneratorBase.cs index 4156925..48e82b1 100644 --- a/src/ApiCodeGenerator.AsyncApi/CSharp/CSharpGeneratorBase.cs +++ b/src/ApiCodeGenerator.AsyncApi/CSharp/CSharpGeneratorBase.cs @@ -90,7 +90,10 @@ protected IEnumerable CreateOperationModels() { foreach (var operation in Document.Operations) { - yield return CreateOperationModelInternal(operation.Value); + if (Settings.OperationTypes.HasFlag((OperationTypes)(int)operation.Value.ActualObject.Action)) + { + yield return CreateOperationModelInternal(operation.Value); + } } } diff --git a/src/ApiCodeGenerator.AsyncApi/CSharp/Models/CSharpOperationModel.cs b/src/ApiCodeGenerator.AsyncApi/CSharp/Models/CSharpOperationModel.cs index f7b0b66..47b1df1 100644 --- a/src/ApiCodeGenerator.AsyncApi/CSharp/Models/CSharpOperationModel.cs +++ b/src/ApiCodeGenerator.AsyncApi/CSharp/Models/CSharpOperationModel.cs @@ -46,7 +46,7 @@ public CSharpOperationModel( public bool HasDescription { get; } - public bool HasPublish => Operation.Action == OperationAction.Send; + public bool HasSend => Operation.Action == OperationAction.Send; public string OperationName { get; } diff --git a/src/ApiCodeGenerator.AsyncApi/CSharp/Models/CSharpParameterModel.cs b/src/ApiCodeGenerator.AsyncApi/CSharp/Models/CSharpParameterModel.cs index 93e591f..a35d4bd 100644 --- a/src/ApiCodeGenerator.AsyncApi/CSharp/Models/CSharpParameterModel.cs +++ b/src/ApiCodeGenerator.AsyncApi/CSharp/Models/CSharpParameterModel.cs @@ -15,4 +15,6 @@ public CSharpParameterModel(string parameterName, Parameter parameter) } public string CamelCaseParameterName => ConversionUtilities.ConvertToLowerCamelCase(_parameterName, true); + + public virtual string ParameterType => "string"; } diff --git a/src/ApiCodeGenerator.AsyncApi/CSharp/OperationTypes.cs b/src/ApiCodeGenerator.AsyncApi/CSharp/OperationTypes.cs index dcd211e..85bdc79 100644 --- a/src/ApiCodeGenerator.AsyncApi/CSharp/OperationTypes.cs +++ b/src/ApiCodeGenerator.AsyncApi/CSharp/OperationTypes.cs @@ -3,7 +3,9 @@ namespace ApiCodeGenerator.AsyncApi.CSharp; [Flags] public enum OperationTypes { - Publish = 1, - Subscribe = 2, + Send = 1, + Receive = 2, All = 3, + Subscribe = 1, + Publish = 2, } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/AsyncApiDocument.cs b/src/ApiCodeGenerator.AsyncApi/DOM/AsyncApiDocument.cs index 6cb1676..74c643f 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/AsyncApiDocument.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/AsyncApiDocument.cs @@ -35,7 +35,7 @@ public class AsyncApiDocument : JsonExtensionObject, IDocumentPathProvider /// An element to hold various reusable objects for the specification. [JsonProperty("components", ObjectCreationHandling = ObjectCreationHandling.Reuse)] - public Components Components { get; set; } = new(); + public Components Components { get; } = new(); /// [JsonIgnore] diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/AsyncApiSchema.cs b/src/ApiCodeGenerator.AsyncApi/DOM/AsyncApiSchema.cs index 0ce38e2..dafafa8 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/AsyncApiSchema.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/AsyncApiSchema.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using ApiCodeGenerator.AsyncApi.DOM.Serialization; using Newtonsoft.Json; using NJsonSchema; @@ -7,5 +8,13 @@ namespace ApiCodeGenerator.AsyncApi.DOM; [JsonConverter(typeof(AsyncApiSchemaConverter))] public class AsyncApiSchema : JsonSchema { - public required string SchemaFormat { get; set; } + public const string AsyncApi = "application/vnd.aai.asyncapi"; + public const string AsyncApi3 = AsyncApi + ";version=3.0.0"; + public const string JsonSchema07 = "application/schema+json;version=draft-07"; + public const string JsonSchema07Yaml = "application/schema+yaml;version=draft-07"; + public const string OpenApi = "application/vnd.oai.openapi"; + + [JsonProperty("schemaFormat", DefaultValueHandling = DefaultValueHandling.Ignore)] + [DefaultValue(AsyncApi3)] + public required string SchemaFormat { get; set; } = AsyncApi3; } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Bindings/Amqp/ChannelType.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Bindings/Amqp/ChannelType.cs index c5f3df6..fe92aa8 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/Bindings/Amqp/ChannelType.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Bindings/Amqp/ChannelType.cs @@ -3,10 +3,10 @@ namespace ApiCodeGenerator.AsyncApi.DOM.Bindings.Amqp; public enum ChannelType { #pragma warning disable SA1602 // Enumeration items should be documented - [System.Runtime.Serialization.EnumMember(Value = @"queue")] - Queue = 0, - [System.Runtime.Serialization.EnumMember(Value = @"routingKey")] - RoutingKey = 1, + RoutingKey = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"queue")] + Queue = 1, #pragma warning restore SA1602 // Enumeration items should be documented } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Channel.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Channel.cs index 95ef0e0..0dffab1 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/Channel.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Channel.cs @@ -31,7 +31,7 @@ public class Channel : ExtensionRefObject /// A map of the parameters included in the channel address. [JsonProperty("parameters")] - public IDictionary>? Parameters { get; } = new Internal.NamedReferenceDictionary(); + public IDictionary> Parameters { get; } = new Internal.NamedReferenceDictionary(); /// A list of tags for logical grouping of channels. [JsonProperty("tags")] diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Components.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Components.cs index ac1a1b8..c282c22 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/Components.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Components.cs @@ -1,10 +1,9 @@ using ApiCodeGenerator.AsyncApi.DOM.Traits; using Newtonsoft.Json; -using NJsonSchema; namespace ApiCodeGenerator.AsyncApi.DOM; -public class Components : JsonExtensionObject +public class Components { [JsonProperty("messages")] public IDictionary>? Messages { get; set; } @@ -62,4 +61,7 @@ public class Components : JsonExtensionObject [JsonProperty("messageBindings")] public IDictionary>? MessageBindings { get; set; } + + [JsonExtensionData] + public IDictionary? ExtensionData { get; set; } } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Internal/NamedReferenceDictionary.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Internal/NamedReferenceDictionary.cs index cb0b58f..0bb6891 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/Internal/NamedReferenceDictionary.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Internal/NamedReferenceDictionary.cs @@ -32,7 +32,10 @@ public NamedReference this[string key] set { _dictionary[key] = value; - value.ObjectId = key; + if (value != null) + { + value.ObjectId = key; + } } } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Message.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Message.cs index a5e2935..66f6c9a 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/Message.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Message.cs @@ -54,5 +54,5 @@ public class Message : ExtensionRefObject, ITraitsAware public ICollection? Examples { get; set; } [JsonProperty("traits")] - public Reference? Traits { get; set; } + public ICollection>? Traits { get; set; } } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/NamedReference.cs b/src/ApiCodeGenerator.AsyncApi/DOM/NamedReference.cs index 85884cc..043423b 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/NamedReference.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/NamedReference.cs @@ -9,5 +9,12 @@ public NamedReference() { } + private NamedReference(T actualObject) + : base(actualObject) + { + } + public string? ObjectId { get; internal set; } + + public static implicit operator NamedReference(T actualObj) => new NamedReference(actualObj); } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Operation.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Operation.cs index 8f4fde6..c0bb5bf 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/Operation.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Operation.cs @@ -34,7 +34,7 @@ public class Operation : ExtensionRefObject, ITraitsAware? Bindings { get; set; } [JsonProperty("traits")] - public Reference? Traits { get; set; } + public ICollection>? Traits { get; set; } [JsonProperty("messages")] public ICollection>? Messages { get; set; } = default!; diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/OperationAction.cs b/src/ApiCodeGenerator.AsyncApi/DOM/OperationAction.cs index 8ef67ff..6974817 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/OperationAction.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/OperationAction.cs @@ -3,8 +3,8 @@ namespace ApiCodeGenerator.AsyncApi.DOM; public enum OperationAction { /// Send message. - Send, + Send = 1, /// Receive message. - Receive, + Receive = 2, } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiReferenceUpdater.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiReferenceUpdater.cs index b3c550c..315ab81 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiReferenceUpdater.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiReferenceUpdater.cs @@ -70,7 +70,7 @@ protected override async Task VisitJsonReferenceAsync(IJsonRefer var targetType = reference.GetType(); var target = await _referenceResolver .ResolveReferenceAsync(_rootObject, reference.ReferencePath, targetType, _contractResolver, cancellationToken); - return target; + reference.Reference = target; } return reference; diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSchemaConverter.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSchemaConverter.cs index c047823..8e69d15 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSchemaConverter.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSchemaConverter.cs @@ -5,12 +5,6 @@ namespace ApiCodeGenerator.AsyncApi.DOM.Serialization; internal class AsyncApiSchemaConverter : JsonConverter { - public const string AsyncApi = "application/vnd.aai.asyncapi"; - public const string AsyncApi3 = AsyncApi + ";version=3.0.0"; - public const string JsonSchema07 = "application/schema+json;version=draft-07"; - public const string JsonSchema07Yaml = "application/schema+yaml;version=draft-07"; - public const string OpenApi = "application/vnd.oai.openapi"; - public override bool CanWrite => false; public override bool CanConvert(Type objectType) => objectType == typeof(AsyncApiSchema); @@ -18,7 +12,7 @@ internal class AsyncApiSchemaConverter : JsonConverter public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { var jobj = (JObject)JToken.ReadFrom(reader); - var format = AsyncApi3; + var format = AsyncApiSchema.AsyncApi3; JToken schemaDefinition = jobj; if (jobj.Property("schemaFormat") != null) { @@ -39,14 +33,14 @@ internal class AsyncApiSchemaConverter : JsonConverter } public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - => throw new NotImplementedException(); + => throw new NotSupportedException(); private AsyncApiSchema DeserializeSchema(string format, JToken schemaDefinition, JsonSerializer serializer) { - if (format.StartsWith(AsyncApi) - || format.StartsWith(OpenApi) - || format == JsonSchema07 - || format == JsonSchema07Yaml) + if (format.StartsWith(AsyncApiSchema.AsyncApi) + || format.StartsWith(AsyncApiSchema.OpenApi) + || format == AsyncApiSchema.JsonSchema07 + || format == AsyncApiSchema.JsonSchema07Yaml) { var aaSchema = new AsyncApiSchema { SchemaFormat = format }; serializer.Populate(schemaDefinition.CreateReader(), aaSchema); diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSerializationException.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSerializationException.cs new file mode 100644 index 0000000..ade265e --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSerializationException.cs @@ -0,0 +1,14 @@ +namespace ApiCodeGenerator.AsyncApi.DOM.Serialization; + +public class AsyncApiSerializationException : Exception +{ + public AsyncApiSerializationException(string message) + : this(message, null) + { + } + + public AsyncApiSerializationException(string message, Exception? innerException) + : base(message, innerException) + { + } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSerializer.V2.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSerializer.V2.cs new file mode 100644 index 0000000..8352a40 --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSerializer.V2.cs @@ -0,0 +1,41 @@ +using ApiCodeGenerator.AsyncApi.DOM.V2; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace ApiCodeGenerator.AsyncApi.DOM.Serialization; + +/// +/// The AsyncAPI v2 document reader. +/// +public static partial class AsyncApiSerializer +{ + private static AsyncApiDocument DeserializeV2(JObject jObject, JsonSerializerSettings serializerSettings, out JsonSerializer serializer) + { + serializer = JsonSerializer.Create(serializerSettings); + var document = new AsyncApiDocument(); + ReferenceResolver referenceResolver = new(jObject); + serializer.ContractResolver = new V2.ContractResolver(new ChannelItemConverter(document, referenceResolver)); // needed for set converters + serializer.Converters.Add(new ServerConverter(referenceResolver)); + serializer.Converters.Add(new MessageConverter()); + serializer.Populate(jObject.CreateReader(), document); + MigrateTags(document, serializer); + MigrateExternalDocs(document, serializer); + return document; + } + + private static void MigrateTags(AsyncApiDocument document, JsonSerializer serializer) + { + if (document.ExtensionData?.TryGetValue("tags", out var tags) == true && tags is JArray tagsArr) + { + document.Info.Tags = tagsArr.ToObject>>(serializer); + } + } + + private static void MigrateExternalDocs(AsyncApiDocument document, JsonSerializer serialzer) + { + if (document.ExtensionData?.TryGetValue("externalDocs", out var externalDocs) == true && externalDocs is JObject extDocsObj) + { + document.Info.ExternalDocs = extDocsObj.ToObject(serialzer)!; + } + } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSerializer.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSerializer.cs index a8fe077..081f69f 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSerializer.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSerializer.cs @@ -4,10 +4,14 @@ using NJsonSchema.Generation; using NJsonSchema.Yaml; using YamlDotNet.Serialization; +using YamlException = YamlDotNet.Core.YamlException; namespace ApiCodeGenerator.AsyncApi.DOM.Serialization; -public static class AsyncApiSerializer +/// +/// The AsyncAPI v3 document reader. +/// +public static partial class AsyncApiSerializer { private static readonly JsonSerializerSettings JSONSERIALIZERSETTINGS = new() { @@ -33,8 +37,17 @@ public static Task FromJsonAsync(string data) /// AsyncApi document object model. public static Task FromJsonAsync(string data, string? documentPath) { - var jObject = JObject.Parse(data); - return FromJObject(jObject, documentPath); + try + { + var jObject = JObject.Parse(data); + return FromJObject(jObject, documentPath); + } + catch (JsonSerializationException jsonEx) + { + throw new AsyncApiSerializationException( + $"Json parsing failed. {jsonEx.Message}", + jsonEx); + } } /// @@ -53,8 +66,23 @@ public static Task FromYamlAsync(string data) /// AsyncApi document object model. public static Task FromYamlAsync(string data, string? documentPath) { - JObject jObject = ParseYaml(data); - return FromJObject(jObject, documentPath); + try + { + JObject jObject = ParseYaml(data); + return FromJObject(jObject, documentPath); + } + catch (YamlException yamlEx) + { + throw new AsyncApiSerializationException( + $"Yaml parsing filed. {yamlEx.Message}", + yamlEx); + } + catch (JsonSerializationException jsonEx) + { + throw new AsyncApiSerializationException( + $"Json parsing failed. {jsonEx.Message}", + jsonEx); + } } private static JObject ParseYaml(string data) @@ -68,12 +96,38 @@ private static JObject ParseYaml(string data) private static Task FromJObject(JObject jObject, string? documentPath) { - var serializer = JsonSerializer.Create(JSONSERIALIZERSETTINGS); - var doc = serializer.Deserialize(jObject.CreateReader())!; + var version = GetDocumentVersion(jObject); + var majorVersion = new string(version.TakeWhile(x => x != '.').ToArray()); + JsonSerializer serializer; + var doc = majorVersion switch + { + "2" => DeserializeV2(jObject, JSONSERIALIZERSETTINGS, out serializer), + "3" => DeserializeV3(jObject, JSONSERIALIZERSETTINGS, out serializer), + _ => throw new AsyncApiSerializationException($"Version '{version}' not supported."), + }; doc.DocumentPath = documentPath; return UpdateSchemaReferencesAsync(doc, serializer.ContractResolver); } + private static string GetDocumentVersion(JObject jObject) + { + string asyncApiVersion; + if (!jObject.TryGetValue("asyncapi", out var asyncApiVersionToken) + || asyncApiVersionToken.Type != JTokenType.String + || string.IsNullOrEmpty(asyncApiVersion = asyncApiVersionToken.ToString())) + { + throw new JsonSerializationException($"Required property 'asyncapi' not was found or its value is not a string."); + } + + return asyncApiVersion; + } + + private static AsyncApiDocument DeserializeV3(JObject jObject, JsonSerializerSettings serializerSettings, out JsonSerializer serializer) + { + serializer = JsonSerializer.Create(serializerSettings); + return serializer.Deserialize(jObject.CreateReader())!; + } + private static async Task UpdateSchemaReferencesAsync(AsyncApiDocument document, IContractResolver contractResolver) { await new AsyncApiReferenceUpdater( diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/InheritanceConverter.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/InheritanceConverter.cs index efcf9f8..063fef4 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/InheritanceConverter.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/InheritanceConverter.cs @@ -63,7 +63,12 @@ public override object ReadJson(JsonReader reader, Type objectType, object? exis if (discriminatorValue is null) { - throw new JsonSerializationException($"Required property '{_discriminator}' not found in JSON. Path '{reader.Path}'."); + throw new JsonSerializationException($"Required property '{discriminatorProperty.PropertyName}' not found in JSON. Path '{reader.Path}'."); + } + + if (!discriminatorProperty.Writable && discriminatorProperty.PropertyName is not null) + { + jobj.Remove(discriminatorProperty.PropertyName); } if (_factories.TryGetValue(discriminatorValue, out var factory) diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/RefObjectConverter.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/RefObjectConverter.cs index da1d8ef..d206600 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/RefObjectConverter.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/RefObjectConverter.cs @@ -13,6 +13,11 @@ public override bool CanConvert(Type objectType) => objectType.IsGenericType && public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + reader.Read(); var refObj = (IJsonReference)Activator.CreateInstance(objectType); if (reader.TokenType == JsonToken.PropertyName @@ -32,7 +37,7 @@ public override bool CanConvert(Type objectType) => objectType.IsGenericType && return refObj; } - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => throw new NotImplementedException(); + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => throw new NotSupportedException(); // Special reader. Wraps original reader and emulate state before call 'Read' in converter private sealed class CustomJsonReader : JsonReader diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Traits/ITraitsAware.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Traits/ITraitsAware.cs index a710ace..7c1d9ff 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/Traits/ITraitsAware.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Traits/ITraitsAware.cs @@ -7,5 +7,5 @@ public interface ITraitsAware where TTraits : Traits { [JsonProperty("traits")] - public Reference? Traits { get; set; } + public ICollection>? Traits { get; set; } } diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Traits/TraitsExtensions.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Traits/TraitsExtensions.cs index 8874f5b..191d9df 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/Traits/TraitsExtensions.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Traits/TraitsExtensions.cs @@ -8,13 +8,17 @@ public static TEntity ApplyTraits(this TEntity entity) where TEntity : class, ITraitsAware, new() where TTraits : Traits { - var traits = entity.Traits?.ActualObject; + var traits = entity.Traits; if (traits is not null) { var version = ((IDocumentAware)traits).Document?.AsyncApi ?? "3.0.0"; var overwrite = version.StartsWith("2."); var target = new TEntity(); - traits.ApplyTo(target, overwrite); + foreach (var t in traits) + { + t.ActualObject.ApplyTo(target, overwrite); + } + return target; } else diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/V2/ChannelItem.cs b/src/ApiCodeGenerator.AsyncApi/DOM/V2/ChannelItem.cs new file mode 100644 index 0000000..5f3fc92 --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/V2/ChannelItem.cs @@ -0,0 +1,27 @@ +using Newtonsoft.Json; + +namespace ApiCodeGenerator.AsyncApi.DOM.V2; + +internal class ChannelItem : RefObject +{ + [JsonProperty("bindings")] + public Reference? Bindings { get; set; } + + [JsonProperty("description")] + public string? Description { get; set; } + + [JsonProperty("parameters")] + public IDictionary>? Parameters { get; set; } + + [JsonProperty("publish")] + public Operation? Publish { get; set; } + + [JsonProperty("subscribe")] + public Operation? Subscribe { get; set; } + + [JsonProperty("servers")] + public string[]? Servers { get; set; } + + [JsonExtensionData] + public IDictionary? ExtensionData { get; set; } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/V2/ChannelItemConverter.cs b/src/ApiCodeGenerator.AsyncApi/DOM/V2/ChannelItemConverter.cs new file mode 100644 index 0000000..1386c20 --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/V2/ChannelItemConverter.cs @@ -0,0 +1,237 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NJsonSchema.References; + +namespace ApiCodeGenerator.AsyncApi.DOM.V2; + +internal class ChannelItemConverter : JsonConverter +{ + private readonly AsyncApiDocument _document; + private readonly ReferenceResolver _referenceResolver; + + public ChannelItemConverter(AsyncApiDocument document, ReferenceResolver referenceResolver) + { + _document = document; + _referenceResolver = referenceResolver; + } + + public override bool CanWrite => false; + + public static bool CanConvert_(Type objectType) => + typeof(IDictionary>) == objectType + || typeof(IDictionary>) == objectType; + + public override bool CanConvert(Type objectType) => CanConvert_(objectType); + + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + + var targetRefType = objectType.GetGenericArguments()[1]; + var contract = serializer.ContractResolver.ResolveContract(objectType); + if (targetRefType.GetGenericTypeDefinition() == typeof(NamedReference<>)) + { + return ReadJson>(reader, existingValue, serializer, c => c); + } + else if (targetRefType.GetGenericTypeDefinition() == typeof(Reference<>)) + { + return ReadJson>(reader, existingValue, serializer, c => c); + } + + return null; + } + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => throw new NotSupportedException(); + + private static string GetChannelId(string address) + { + var segments = address.Split(['/', '.', '{', '}'], StringSplitOptions.RemoveEmptyEntries) + .Select((s, i) => (i == 0 ? s.Substring(0, 1).ToLowerInvariant() : s.Substring(0, 1).ToUpperInvariant()) + s.Substring(1)); + return string.Join(string.Empty, segments); + } + + private IDictionary ReadJson( + JsonReader reader, + object? existingValue, + JsonSerializer serializer, + Func refFactory) + where TRef : Reference, new() + { + var channelItems = serializer.Deserialize>>(reader)!; + var dictionary = existingValue as IDictionary ?? new Dictionary(); + + foreach (var item in channelItems) + { + var address = item.Key; + + var channelId = GetChannelId(address); + TRef? newRef = null; + if (item.Value is not null) + { + if (string.IsNullOrEmpty(item.Value.ReferencePath)) + { + var isComponent = reader.Path.StartsWith("components.channels"); + if (isComponent) + { + address = GetAddressFromRef(address); + } + + var channelItem = item.Value.ActualObject; + var channel = ChannelItemToV3(channelItem, address); + ReadOperations(channelItem, channelId, channel.Messages, serializer, isComponent); + newRef = refFactory(channel); + } + else + { + newRef = new() { ReferencePath = item.Value.ReferencePath }; + } + } + + dictionary[channelId] = newRef!; + } + + return dictionary; + } + + private string GetAddressFromRef(string address) + { + var refPath = $"#/components/channels/{address}"; + return _referenceResolver.GetChannelIdByRefPath(refPath); + } + + private void ReadOperations( + ChannelItem channelItem, + string channelId, + IDictionary> channelMessages, + JsonSerializer serializer, + bool isComponent) + { + var channelRef = $"#/{(isComponent ? "components/" : string.Empty)}channels/{channelId}"; + var path = $"{(isComponent ? "components." : string.Empty)}channels.{channelId}"; + var counter = 1; + ReadOperation(channelItem.Subscribe, OperationAction.Send); + ReadOperation(channelItem.Publish, OperationAction.Receive); + + void ReadOperation(Operation? op, OperationAction action) + { + if (op is null) + { + return; + } + + var operation = new DOM.Operation + { + Action = action, + Channel = new Reference { ReferencePath = channelRef }, + Bindings = op.Bindings, + Description = op.Desciption, + ExtensionData = op.ExtensionData, + ExternalDocs = op.ExternalDocs, + Security = _referenceResolver.GetSecuritySchemeRefs(op.Security, path + ".security"), + Summary = op.Summary, + Tags = op.Tags, + Traits = op.Traits, + }; + + if (op.Message is not null && op.Message.Type == JTokenType.Object) + { + var oneOf = (JArray?)op.Message.GetValue("oneOf"); + var messagesV2 = oneOf is null + ? Enumerable.Repeat(op.Message.ToObject>(serializer)!, 1) + : oneOf.ToObject[]>(serializer)!; + + List> operationMessages = new(); + + foreach (var message in messagesV2) + { + if (!string.IsNullOrEmpty(message.ReferencePath)) + { + var name = message.ReferencePath!.Substring(message.ReferencePath.LastIndexOf("/") + 1); + if (!channelMessages.ContainsKey(name)) + { + channelMessages.Add(name, new() { ReferencePath = message.ReferencePath }); + } + + operationMessages.Add(new Reference { ReferencePath = $"{channelRef}/messages/{name}" }); + } + else + { + var msg2 = message.ActualObject; + var name = msg2.MessageId ?? $"msg{counter++}"; + if (string.IsNullOrEmpty(msg2.MessageId) || !channelMessages.ContainsKey(msg2.MessageId!)) + { + var msg3 = MessageConverter.MessageToV3(msg2, serializer); + channelMessages.Add(name, msg3); + } + + operationMessages.Add(new Reference { ReferencePath = $"{channelRef}/messages/{name}" }); + } + } + + operation.Messages = operationMessages.ToArray(); + } + + var operName = op.OperationId ?? $"channelId{action}"; + if (isComponent) + { + (_document.Components.Operations ??= new Dictionary>()).Add(operName, operation); + var operRef = new NamedReference { ReferencePath = $"#/components/operations/{operName}" }; + _document.Operations.Add(operName, operRef); + } + else + { + _document.Operations.Add(operName, operation); + } + } + } + + private Channel ChannelItemToV3(ChannelItem channelItem, string address) + { + Channel result = new() + { + Address = address, + Bindings = channelItem.Bindings, + Description = channelItem.Description, + ExtensionData = channelItem.ExtensionData, + Servers = GetServers(_referenceResolver), + }; + PopulateParameters(result); + return result; + + ICollection>? GetServers(ReferenceResolver serverResolver) + { + if (channelItem.Servers is not null) + { + return channelItem.Servers + .Select(serverResolver.GetServerRefByName) + .Where(r => !string.IsNullOrEmpty(r)) + .Select(r => new Reference { ReferencePath = r }) + .ToArray(); + } + + return null; + } + + void PopulateParameters(Channel result) + { + if (channelItem.Parameters is not null) + { + foreach (var pair in channelItem.Parameters) + { + var r = pair.Value.ReferencePath; + if (r is not null) + { + result.Parameters.Add(pair.Key, new() { ReferencePath = r }); + } + else + { + result.Parameters.Add(pair.Key, pair.Value.ActualObject); + } + } + } + } + } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/V2/ContractResolver.cs b/src/ApiCodeGenerator.AsyncApi/DOM/V2/ContractResolver.cs new file mode 100644 index 0000000..acf6728 --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/V2/ContractResolver.cs @@ -0,0 +1,37 @@ +using System.Reflection; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace ApiCodeGenerator.AsyncApi.DOM.V2; + +internal class ContractResolver : DefaultContractResolver +{ + private readonly ChannelItemConverter _channelItemConverter; + + public ContractResolver(ChannelItemConverter channelItemConverter) + { + _channelItemConverter = channelItemConverter; + } + + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + { + var jProperty = base.CreateProperty(member, memberSerialization); + if (member is PropertyInfo propertyInfo && ChannelItemConverter.CanConvert_(propertyInfo.PropertyType)) + { + jProperty.Converter = _channelItemConverter; + } + + return jProperty; + } + + protected override JsonConverter? ResolveContractConverter(Type objectType) + { + // disable converter defined on JsonExtensionObject for use converter added to serializer settings + if (MessageConverter.CanConvert_(objectType)) + { + return null; + } + + return base.ResolveContractConverter(objectType); + } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/V2/Message.cs b/src/ApiCodeGenerator.AsyncApi/DOM/V2/Message.cs new file mode 100644 index 0000000..6455d74 --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/V2/Message.cs @@ -0,0 +1,56 @@ +using ApiCodeGenerator.AsyncApi.DOM.Traits; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace ApiCodeGenerator.AsyncApi.DOM.V2; + +internal class Message : RefObject +{ + [JsonProperty("messageId")] + public string? MessageId { get; set; } + + [JsonProperty("headers")] + public Reference? Headers { get; set; } + + [JsonProperty("payload")] + public JToken? Payload { get; set; } + + [JsonProperty("correlationId")] + public Reference? CorrelationId { get; set; } + + [JsonProperty("schemaFormat")] + public string? SchemaFormat { get; set; } + + [JsonProperty("contentType")] + public string? ContentType { get; set; } + + [JsonProperty("name")] + public string? Name { get; set; } + + [JsonProperty("title")] + public string? Title { get; set; } + + [JsonProperty("summary")] + public string? Summary { get; set; } + + [JsonProperty("description")] + public string? Description { get; set; } + + [JsonProperty("tags")] + public Tag[]? Tags { get; set; } + + [JsonProperty("externalDocs")] + public ExternalDocumentation? ExternalDocs { get; set; } + + [JsonProperty("bindings")] + public Reference? Bindings { get; set; } + + [JsonProperty("examples")] + public ICollection? Examples { get; set; } + + [JsonProperty("traits")] + public ICollection>? Traits { get; set; } + + [JsonExtensionData] + public IDictionary? ExtensionData { get; set; } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/V2/MessageConverter.cs b/src/ApiCodeGenerator.AsyncApi/DOM/V2/MessageConverter.cs new file mode 100644 index 0000000..3be80bd --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/V2/MessageConverter.cs @@ -0,0 +1,54 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace ApiCodeGenerator.AsyncApi.DOM.V2; + +internal sealed class MessageConverter : JsonConverter +{ + public override bool CanWrite => false; + + public static bool CanConvert_(Type objectType) => typeof(DOM.Message) == objectType; + + public override bool CanConvert(Type objectType) => CanConvert_(objectType); + + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + + var message = serializer.Deserialize(reader)!; + return MessageToV3(message, serializer); + } + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => throw new NotSupportedException(); + + internal static DOM.Message MessageToV3(Message message, JsonSerializer serializer) + { + var payloadDef = string.IsNullOrEmpty(message.SchemaFormat) || message.SchemaFormat!.StartsWith(AsyncApiSchema.AsyncApi) + ? message.Payload + : new JObject( + new JProperty("SchemaFormat", message.SchemaFormat), + new JProperty("Schema", message.Payload)); + + var payload = payloadDef?.ToObject(serializer); + return new() + { + Bindings = message.Bindings, + ContentType = message.ContentType, + CorrelationId = message.CorrelationId, + Description = message.Description, + Examples = message.Examples, + ExtensionData = message.ExtensionData, + ExternalDocs = message.ExternalDocs is null ? null : (Reference)message.ExternalDocs, + Headers = message.Headers, + Name = message.Name, + Payload = payload is null ? null : (Reference)payload, + Summary = message.Summary, + Tags = message.Tags is null ? null : [.. message.Tags], + Title = message.Title, + Traits = message.Traits, + }; + } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/V2/Operation.cs b/src/ApiCodeGenerator.AsyncApi/DOM/V2/Operation.cs new file mode 100644 index 0000000..1d2038a --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/V2/Operation.cs @@ -0,0 +1,39 @@ +using ApiCodeGenerator.AsyncApi.DOM.Traits; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OperationV3 = ApiCodeGenerator.AsyncApi.DOM.Operation; + +namespace ApiCodeGenerator.AsyncApi.DOM.V2; + +internal class Operation +{ + [JsonProperty("operationId")] + public string? OperationId { get; set; } + + [JsonProperty("summary")] + public string? Summary { get; set; } + + [JsonProperty("description")] + public string? Desciption { get; set; } + + [JsonProperty("security")] + public SecurityRequirement[]? Security { get; set; } + + [JsonProperty("tags")] + public ICollection>? Tags { get; set; } + + [JsonProperty("externalDocs")] + public Reference? ExternalDocs { get; set; } + + [JsonProperty("bindings")] + public Reference? Bindings { get; set; } + + [JsonProperty("traits")] + public ICollection>? Traits { get; set; } + + [JsonProperty("message")] + public JObject? Message { get; set; } + + [JsonExtensionData] + public IDictionary? ExtensionData { get; set; } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/V2/ReferenceResolver.cs b/src/ApiCodeGenerator.AsyncApi/DOM/V2/ReferenceResolver.cs new file mode 100644 index 0000000..b401f8d --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/V2/ReferenceResolver.cs @@ -0,0 +1,108 @@ +using ApiCodeGenerator.AsyncApi.DOM.Serialization; +using Newtonsoft.Json.Linq; + +namespace ApiCodeGenerator.AsyncApi.DOM.V2; + +internal sealed class ReferenceResolver +{ + private readonly JObject _rootObject; + + internal ReferenceResolver(JObject rootObject) + { + _rootObject = rootObject; + } + + internal string? GetServerRefByName(string name) + { + var serverDefinitions1 = _rootObject["servers"]?.Children(); + var serverDefinitions2 = _rootObject["components"]?["servers"]?.Children(); + var serverDefinitions = (serverDefinitions1, serverDefinitions2) switch + { + (null, not null) => serverDefinitions2, + (not null, null) => serverDefinitions1, + (not null, not null) => serverDefinitions1.Value.Concat(serverDefinitions2), + _ => null, + }; + + return GetRef(serverDefinitions.FirstOrDefault(d => d.Name == name)); + } + + /// + /// Search channel referenced to channel component. + /// + /// Reference path. + /// Name of channel item + internal string GetChannelIdByRefPath(string refPath) + { + var refs = _rootObject["channels"]? + .Children() + .Where(p => + { + var chDef = p.Parent![p.Name]!; + return chDef.Type == JTokenType.Object && chDef["$ref"]?.ToString() == refPath; + }) + .ToArray(); + + if (refs?.Any() != true) + { + throw new AsyncApiSerializationException($"Channel with reference '{refPath}' was not found."); + } + else if (refs.Length > 1) + { + throw new AsyncApiSerializationException($"More than one channel with the '{refPath}' link was found."); + } + else + { + return refs[0].Name; + } + } + + internal Reference[]? GetSecuritySchemeRefs(SecurityRequirement[]? requirements, string path) + { + if (requirements is null) + { + return null; + } + + var names = requirements.SelectMany(i => i.Keys).Distinct(); + var result = new List>(requirements.Length); + var securitySchemes = _rootObject["components"]?["securitySchemes"]?.Children().Select(p => p.Name).ToArray(); + foreach (var name in names) + { + if (securitySchemes?.Contains(name) == true) + { + result.Add(new() { ReferencePath = $"#/components/securitySchemes/{name}" }); + } + else + { + throw new AsyncApiSerializationException($"Security scheme '{name}' not found. Path: {path}"); + } + } + + return result.ToArray(); + } + + private string? GetRef(JToken? token) + { + var segments = GetSegments(); + if (segments.Any()) + { + return string.Join("/", segments.Reverse().Prepend("#")); + } + + return null; + + IEnumerable GetSegments() + { + while (token is not null) + { + if (token is JProperty prop) + { + yield return prop.Name; + } + + token = token.Parent; + } + } + } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/V2/SecurityRequirement.cs b/src/ApiCodeGenerator.AsyncApi/DOM/V2/SecurityRequirement.cs new file mode 100644 index 0000000..eee7697 --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/V2/SecurityRequirement.cs @@ -0,0 +1,7 @@ +using Newtonsoft.Json; + +namespace ApiCodeGenerator.AsyncApi.DOM.V2; + +internal class SecurityRequirement : Dictionary +{ +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/V2/Server.cs b/src/ApiCodeGenerator.AsyncApi/DOM/V2/Server.cs new file mode 100644 index 0000000..a5efff7 --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/V2/Server.cs @@ -0,0 +1,30 @@ +using Newtonsoft.Json; + +namespace ApiCodeGenerator.AsyncApi.DOM.V2; + +internal class Server +{ + [JsonProperty("url", Required = Required.Always)] + public required string Url { get; set; } + + [JsonProperty("protocol", Required = Required.Always)] + public required string Protocol { get; set; } + + [JsonProperty("protocolVersion")] + public string? ProtocolVersion { get; set; } + + [JsonProperty("description")] + public string? Description { get; set; } + + [JsonProperty("variables")] + public IDictionary>? Variables { get; set; } + + [JsonProperty("security")] + public SecurityRequirement[]? Security { get; set; } + + [JsonProperty("tags")] + public ICollection>? Tags { get; set; } + + [JsonProperty("bindings")] + public Reference? Bindings { get; set; } +} diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/V2/ServerConverter.cs b/src/ApiCodeGenerator.AsyncApi/DOM/V2/ServerConverter.cs new file mode 100644 index 0000000..7f0484c --- /dev/null +++ b/src/ApiCodeGenerator.AsyncApi/DOM/V2/ServerConverter.cs @@ -0,0 +1,47 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using ServerV2 = ApiCodeGenerator.AsyncApi.DOM.V2.Server; +using ServerV3 = ApiCodeGenerator.AsyncApi.DOM.Server; + +namespace ApiCodeGenerator.AsyncApi.DOM.V2; + +internal class ServerConverter : JsonConverter +{ + private readonly ReferenceResolver _referenceResolver; + + public ServerConverter(ReferenceResolver referenceResolver) + { + _referenceResolver = referenceResolver; + } + + public override bool CanConvert(Type objectType) => typeof(ServerV3) == objectType; + + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + var serverV2 = serializer.Deserialize(reader) + ?? throw new JsonSerializationException( + $"Required property '{reader.Path.Substring(reader.Path.LastIndexOf('.') + 1)}' " + + $"expects a value but got null. Path '{reader.Path.Substring(0, reader.Path.LastIndexOf('.'))}'."); + + return ToV3(serverV2, reader.Path); + } + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => throw new NotImplementedException(); + + private DOM.Server ToV3(ServerV2 serverV2, string path) + { + var uri = new Uri(serverV2.Url); + return new() + { + Bindings = serverV2.Bindings, + Host = serverV2.Url.Contains('/') ? uri.Host : serverV2.Url, + Protocol = serverV2.Protocol, + Description = serverV2.Description, + PathName = uri.PathAndQuery, + ProtocolVersion = serverV2.ProtocolVersion, + Tags = serverV2.Tags, + Variables = serverV2.Variables, + Security = _referenceResolver.GetSecuritySchemeRefs(serverV2.Security, path), + }; + } +} diff --git a/src/ApiCodeGenerator.AsyncApi/Templates/Client.Class.liquid b/src/ApiCodeGenerator.AsyncApi/Templates/Client.Class.liquid index 90bb3d6..451d9a4 100644 --- a/src/ApiCodeGenerator.AsyncApi/Templates/Client.Class.liquid +++ b/src/ApiCodeGenerator.AsyncApi/Templates/Client.Class.liquid @@ -14,15 +14,15 @@ /// {%- endif -%} {%- capture parametersText %} {%- template Client.Operation.Parameters -%} {% endcapture -%} - {%- if operation.HasPublish -%} + {%- if operation.HasSend -%} public Task {{operation.OperationName}}({{parametersText}}, {{operation.PayloadType}} payload) { - {% template Client.Opertaion.Publish.Body | tab %} + {% template Client.Operation.Send.Body | tab %} } {%- else -%} public void {{operation.OperationName}}({{parametersText}}, Action<{{operation.PayloadType}}> callback) { - {% template Client.Opertaion.Subscribe.Body | tab %} + {% template Client.Operation.Receive.Body | tab %} } {%- endif -%} diff --git a/src/ApiCodeGenerator.AsyncApi/Templates/Client.Interface.liquid b/src/ApiCodeGenerator.AsyncApi/Templates/Client.Interface.liquid index c99732e..b682ad8 100644 --- a/src/ApiCodeGenerator.AsyncApi/Templates/Client.Interface.liquid +++ b/src/ApiCodeGenerator.AsyncApi/Templates/Client.Interface.liquid @@ -14,7 +14,7 @@ /// {%- endif -%} {%- capture parametersText %} {%- template Client.Operation.Parameters -%} {% endcapture -%} - {%- if operation.HasPublish -%} + {%- if operation.HasSend -%} public Task {{operation.OperationName}}({{parametersText}}, {{operation.PayloadType}} payload); {%- else -%} public void {{operation.OperationName}}({{parametersText}}, Action<{{operation.PayloadType}}> callback); diff --git a/src/ApiCodeGenerator.AsyncApi/Templates/Client.Opertaion.Subscribe.Body.liquid b/src/ApiCodeGenerator.AsyncApi/Templates/Client.Operation.Receive.Body.liquid similarity index 100% rename from src/ApiCodeGenerator.AsyncApi/Templates/Client.Opertaion.Subscribe.Body.liquid rename to src/ApiCodeGenerator.AsyncApi/Templates/Client.Operation.Receive.Body.liquid diff --git a/src/ApiCodeGenerator.AsyncApi/Templates/Client.Opertaion.Publish.Body.liquid b/src/ApiCodeGenerator.AsyncApi/Templates/Client.Operation.Send.Body.liquid similarity index 100% rename from src/ApiCodeGenerator.AsyncApi/Templates/Client.Opertaion.Publish.Body.liquid rename to src/ApiCodeGenerator.AsyncApi/Templates/Client.Operation.Send.Body.liquid diff --git a/src/ApiCodeGenerator.AsyncApi/TemplatesAmqp/Client.ChannelPool.liquid b/src/ApiCodeGenerator.AsyncApi/TemplatesAmqp/Client.ChannelPool.liquid index dd29cdc..19641f8 100644 --- a/src/ApiCodeGenerator.AsyncApi/TemplatesAmqp/Client.ChannelPool.liquid +++ b/src/ApiCodeGenerator.AsyncApi/TemplatesAmqp/Client.ChannelPool.liquid @@ -36,11 +36,11 @@ public class {{ChannelPoolType}} //: IChannelPool _connection = connection; {%- for operation in OperationModels -%} - {%- if operation.HasPublish -%} - _channels.Add("{{operation.OperationId}}", CreateChannel(connection)); + {%- if operation.HasSend -%} + _channels.Add("{{operation.OperationName}}", CreateChannel(connection)); {%- else -%} _channels.Add( - "{{operation.OperationId}}", + "{{operation.OperationName}}", CreateChannel( connection, {{operation.PrefetchCount}}, diff --git a/src/ApiCodeGenerator.AsyncApi/TemplatesAmqp/Client.Opertaion.Subscribe.Body.liquid b/src/ApiCodeGenerator.AsyncApi/TemplatesAmqp/Client.Operation.Receive.Body.liquid similarity index 91% rename from src/ApiCodeGenerator.AsyncApi/TemplatesAmqp/Client.Opertaion.Subscribe.Body.liquid rename to src/ApiCodeGenerator.AsyncApi/TemplatesAmqp/Client.Operation.Receive.Body.liquid index f1fba11..dd335a8 100644 --- a/src/ApiCodeGenerator.AsyncApi/TemplatesAmqp/Client.Opertaion.Subscribe.Body.liquid +++ b/src/ApiCodeGenerator.AsyncApi/TemplatesAmqp/Client.Operation.Receive.Body.liquid @@ -3,9 +3,9 @@ {% endcomment -%} var queue = "{{operation.QueueName}}"; // queue from specification -var channel = _channelPool.GetChannel("{{operation.OperationId}}"); +var channel = _channelPool.GetChannel("{{operation.OperationName}}"); var exchange = "{{operation.ExchangeName}}"; -var routingKey = "{{operation.ChannelName}}"; +var routingKey = "{{operation.ChannelAddress}}"; // TODO: declare passive? channel.QueueDeclare(queue); diff --git a/src/ApiCodeGenerator.AsyncApi/TemplatesAmqp/Client.Opertaion.Publish.Body.liquid b/src/ApiCodeGenerator.AsyncApi/TemplatesAmqp/Client.Operation.Send.Body.liquid similarity index 88% rename from src/ApiCodeGenerator.AsyncApi/TemplatesAmqp/Client.Opertaion.Publish.Body.liquid rename to src/ApiCodeGenerator.AsyncApi/TemplatesAmqp/Client.Operation.Send.Body.liquid index 6a6d787..3c7c853 100644 --- a/src/ApiCodeGenerator.AsyncApi/TemplatesAmqp/Client.Opertaion.Publish.Body.liquid +++ b/src/ApiCodeGenerator.AsyncApi/TemplatesAmqp/Client.Operation.Send.Body.liquid @@ -3,9 +3,9 @@ {% endcomment -%} var exchange = "{{operation.ExchangeName}}"; -var routingKey = "{{operation.ChannelName}}"; +var routingKey = "{{operation.ChannelAddress}}"; -var channel = _channelPool.GetChannel("{{operation.OperationId}}"); +var channel = _channelPool.GetChannel("{{operation.OperationName}}"); var exchangeProps = new Dictionary { {%- if operation.HasCc -%} @@ -33,7 +33,7 @@ channel.ExchangeDeclare( var props = channel.CreateBasicProperties(); -props.CorrelationId = "{{operation.OperationId}}_{{operation.ChannelName}}"; +props.CorrelationId = "{{operation.OperationName}}_{{operation.ChannelAddress}}"; {%- if operation.ReplyTo != '' -%} props.ReplyTo = "{{operation.ReplyTo}}"; {% endif -%} diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/AmqpFunctionalTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/AmqpFunctionalTests.cs index 502f820..0aa85b3 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/AmqpFunctionalTests.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/AmqpFunctionalTests.cs @@ -22,7 +22,7 @@ public async Task Generate() Namespace = ns, GenerateDataAnnotations = false, }, - OperationTypes = OperationTypes.Publish, + OperationTypes = OperationTypes.Receive, }; var context = new GeneratorContext((t, s, v) => settings, new Core.ExtensionManager.Extensions(), new Dictionary()) @@ -31,6 +31,10 @@ public async Task Generate() }; var generator = await CSharpAmqpContentGenerator.CreateAsync(context); + var channel = new DOM.Channel + { + Address = "smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured", + }; // Act var code = generator.Generate(); @@ -38,7 +42,7 @@ public async Task Generate() // Assert var expectedCode = GetExpectedCode( GetExpectedAmqpServiceCode(className, identCnt: 4) + "\n" + - GetExpectedPoolCode(className, identCnt: 4) + "\n", + GetExpectedPoolCode(className, identCnt: 4, channel) + "\n", GetExpectedDtoCode(), ns, GetAmqpUsings() + "\n"); @@ -47,7 +51,7 @@ public async Task Generate() } [Test] - public async Task Generate_ChannelBindingPublisher() + public async Task Generate_ChannelBindingReceiver() { const string ns = "MyNS"; const string className = "LightinService"; @@ -60,7 +64,7 @@ public async Task Generate_ChannelBindingPublisher() Namespace = ns, GenerateDataAnnotations = false, }, - OperationTypes = OperationTypes.Publish, + OperationTypes = OperationTypes.Receive, }; var channelBinding = new ChannelBindings { @@ -100,13 +104,15 @@ public async Task Generate_ChannelBindingPublisher() var code = generator.Generate(); // Assert + var document = GetDocument(generator); + var channel = document.Channels!.Values.First(); string[] expectedPublisherCode = [ GetExpectedSummary("Inform about environmental lighting conditions of a particular streetlight.", 8) + - GetExpectedAmqpPublisherCode("ReceiveLightMeasurement", "LightMeasuredPayload", identCnt: 8, channelBinding.Amqp?.Exchange) + GetExpectedAmqpReceiverCode("ReceiveLightMeasurement", "LightMeasuredPayload", identCnt: 8, channelBinding.Amqp) ]; var expectedCode = GetExpectedCode( GetExpectedAmqpServiceCode(className, identCnt: 4, expectedPublisherCode) + "\n" + - GetExpectedPoolCode(className, identCnt: 4) + "\n", + GetExpectedPoolCode(className, identCnt: 4, channel) + "\n", GetExpectedDtoCode(), ns, GetAmqpUsings() + "\n"); @@ -115,7 +121,7 @@ public async Task Generate_ChannelBindingPublisher() } [Test] - public async Task Generate_ChannelBindingSubscriber() + public async Task Generate_ChannelBindingSender() { const string ns = "MyNS"; const string className = "LightinService"; @@ -128,7 +134,7 @@ public async Task Generate_ChannelBindingSubscriber() Namespace = ns, GenerateDataAnnotations = false, }, - OperationTypes = OperationTypes.Subscribe, + OperationTypes = OperationTypes.Send, }; var channelBinding = new ChannelBindings { @@ -174,11 +180,11 @@ public async Task Generate_ChannelBindingSubscriber() var document = GetDocument(generator); var channel = document.Channels!.Values.First(); string[] expectedSubscriberCode = [ - GetExpectedAmqpSubscriberCode("DimLight", "DimLightPayload", identCnt: 8, channelBinding.Amqp) + GetExpectedAmqpSenderCode("DimLight", "DimLightPayload", identCnt: 8, channelBinding.Amqp.Exchange) ]; var expectedCode = GetExpectedCode( GetExpectedAmqpServiceCode(className, identCnt: 4, expectedSubscriberCode) + "\n" + - GetExpectedPoolCode(className, identCnt: 4) + "\n", + GetExpectedPoolCode(className, identCnt: 4, channel) + "\n", GetExpectedDtoCode(), ns, GetAmqpUsings() + "\n"); @@ -200,7 +206,7 @@ public async Task Generate_OperationBinding() Namespace = ns, GenerateDataAnnotations = false, }, - OperationTypes = OperationTypes.Publish, + OperationTypes = OperationTypes.Send, }; var operationBinding = new OperationV0_3 @@ -216,8 +222,10 @@ public async Task Generate_OperationBinding() var documentReader = await LoadApiDocumentAsync("asyncapi.json"); var json = JToken.ReadFrom(new JsonTextReader(documentReader)); - var channelJson = ((JProperty)json["channels"]!.First!).Value; - channelJson["publish"]!["bindings"] = JObject.FromObject(new OperationBindings { Amqp = operationBinding }); + var channelProperty = (JProperty)json["channels"]!.Last!; + var channelJson = channelProperty.Value; + channelJson["subscribe"]!["bindings"] = JObject.FromObject(new OperationBindings { Amqp = operationBinding }); + json["channels"] = new JObject { channelProperty }; // for test use only last channel var context = new GeneratorContext((t, s, v) => settings, new Core.ExtensionManager.Extensions(), new Dictionary()) { @@ -230,13 +238,14 @@ public async Task Generate_OperationBinding() var code = generator.Generate(); // Assert + var document = GetDocument(generator); + var channel = document.Channels!.Values.First(); string[] expectedPublisherCode = [ - GetExpectedSummary("Inform about environmental lighting conditions of a particular streetlight.", 8) + - GetExpectedAmqpPublisherCode("ReceiveLightMeasurement", "LightMeasuredPayload", identCnt: 8, operationBinding: operationBinding) + GetExpectedAmqpSenderCode("DimLight", "DimLightPayload", identCnt: 8, operationBinding: operationBinding) ]; var expectedCode = GetExpectedCode( GetExpectedAmqpServiceCode(className, identCnt: 4, expectedPublisherCode) + "\n" + - GetExpectedPoolCode(className, identCnt: 4) + "\n", + GetExpectedPoolCode(className, identCnt: 4, channel) + "\n", GetExpectedDtoCode(), ns, GetAmqpUsings() + "\n"); diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/AsyncApiContentGeneratorTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/AsyncApiContentGeneratorTests.cs index a29aae5..5c5cb64 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/AsyncApiContentGeneratorTests.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/AsyncApiContentGeneratorTests.cs @@ -10,25 +10,6 @@ namespace ApiCodeGenerator.AsyncApi.Tests; public class AsyncApiContentGeneratorTests { - [TestCase("asyncapi.json")] - [TestCase("asyncapi.yml")] - public async Task LoadApiDocument(string fileName) - { - var extensions = new Core.ExtensionManager.Extensions(); - - var context = new GeneratorContext( - settingsFactory: (t, s, v) => null, - extensions, - variables: new Dictionary()) - { - DocumentReader = await TestHelpers.LoadApiDocumentAsync(fileName), - }; - - var contentGenerator = (FakeContentGenerator)await FakeContentGenerator.CreateAsync(context); - - ValidateDocument(contentGenerator.Document); - } - [Test] public async Task LoadSettingsAsync() { @@ -148,7 +129,19 @@ public async Task LoadApiDocument_WithModelPreprocess() Func dlgt = new FakeModelPreprocessor("{}").Process; var context = CreateContext(settingsJson); - context.DocumentReader = new StringReader("{\"components\":{\"schemas\":{\"" + schemaName + "\":{\"$schema\":\"http://json-schema.org/draft-04/schema#\"}}}}"); + context.DocumentReader = new StringReader($$""" + { + "asyncapi":"3.0.0", + "info":{"title":"", "version": "1.0"} + "components":{ + "schemas":{ + "{{schemaName}}":{ + "$schema":"http://json-schema.org/draft-04/schema#" + } + } + } + } + """); context.Preprocessors = new Preprocessors( new Dictionary @@ -185,107 +178,6 @@ public async Task LoadApiDocument_WithExternalRef(string documentPath) private static Func?, object?> GetSettingsFactory(string json) => (t, s, v) => (s ?? new()).Deserialize(new StringReader(json), t); - private void ValidateDocument(AsyncApiDocument document) - { - Assert.NotNull(document); - Assert.AreEqual("Streetlights Kafka API", document.Info?.Title); - - const string channelPrefix = "smartylighting.streetlights.1.0."; - Assert.That(document.Channels, - Is.Not.Null - .And.ContainKey(channelPrefix + "event.{streetlightId}.lighting.measured") - .And.ContainKey(channelPrefix + "action.{streetlightId}.turn.on") - .And.ContainKey(channelPrefix + "action.{streetlightId}.turn.off") - .And.ContainKey(channelPrefix + "action.{streetlightId}.dim")); - - Assert.NotNull(document.Components); - Assert.That(document.Components?.Messages, - Is.Not.Null - .And.ContainKey("lightMeasured") - .And.ContainKey("turnOnOff") - .And.ContainKey("dimLight")); - - Assert.That(document.Components?.Parameters, - Is.Not.Null - .And.ContainKey("streetlightId")); - - Assert.That(document.Components?.Schemas, - Is.Not.Null - .And.ContainKey("lightMeasuredPayload") - .And.ContainKey("turnOnOffPayload") - .And.ContainKey("dimLightPayload") - .And.ContainKey("sentAt")); - - Assert.That(document.Servers, - Is.Not.Null - .And.ContainKey("scram-connections") - .And.ContainKey("mtls-connections")); - - // Resolve $ref in channel defintion - var actualChannel = document.Channels?[channelPrefix + "event.{streetlightId}.lighting.measured"].ActualObject; - Assert.That(actualChannel, - Is.Not.Null - .And.Property("Publish").Not.Null - .And.Property("Subscribe").Null); - Assert.That(actualChannel.Parameters, - Is.Not.Null - .And.ContainKey("streetlightId")); - Assert.That(actualChannel.Parameters["streetlightId"], - Is.Not.Null - .And.Property("ReferencePath").EqualTo("#/components/parameters/streetlightId") - .And.Property("Reference").EqualTo(document.Components.Parameters["streetlightId"])); - // Assert.That(actualChannel?.Publish?.Message, - // Is.Not.Null - // .And.Property("ReferencePath").EqualTo("#/components/messages/lightMeasured") - // .And.Property("Reference").EqualTo(document.Components?.Messages["lightMeasured"])); - - // Resolve $ref in message definition - var actualMessage = document.Components.Messages["turnOnOff"].ActualObject; - Assert.That(actualMessage, Is.Not.Null); - Assert.That(actualMessage.Payload, - Is.Not.Null - .And.Property("Reference").EqualTo(document.Components.Schemas["turnOnOffPayload"])); - - // Resolve $ref in schema definition - Assert.That(document.Components.Schemas["turnOnOffPayload"]?.ActualProperties, - Is.Not.Null - .And.ContainKey("command")); - - //Read server object - Assert.That(document.Servers["scram-connections"], - Is.Not.Null - .And.Property("Url").EqualTo("test.mykafkacluster.org:18092") - .And.Property("Protocol").EqualTo("kafka-secure") - .And.Property("Description").EqualTo("Test broker secured with scramSha256")); - - // Resolve $ref in servers - Assert.That(document.Servers["mtls-connections"], - Is.Not.Null - .And.Property("Reference").EqualTo(document.Components?.Servers["mtls-connections"])); - - // Resolve $ref in server variables - Assert.Multiple(() => - { - var variables = document.Components?.Servers["mtls-connections"].ActualObject.Variables; - Assert.That(variables, - Is.Not.Null - .And.ContainKey("someRefVariable") - .And.ContainKey("someVariable")); - - Assert.That(variables!["someRefVariable"], - Is.Not.Null - .And.Property("Reference").EqualTo(document.Components?.ServerVariables["someRefVariable"])); - }); - - //Read server variables - Assert.That(document.Components?.ServerVariables["someRefVariable"], - new PredicateConstraint(a => - a.Description == "Some ref variable" - && a.Enum?.FirstOrDefault() == "def" - && a.Default == "def" - && a.Examples?.FirstOrDefault() == "exam")); - } - private GeneratorContext CreateContext(JObject settingsJson, Core.ExtensionManager.Extensions? extension = null) { extension ??= new(); @@ -294,7 +186,15 @@ private GeneratorContext CreateContext(JObject settingsJson, Core.ExtensionManag extension, new ReadOnlyDictionary(new Dictionary())) { - DocumentReader = new StringReader("{}"), + DocumentReader = new StringReader(""" + { + "asyncapi":"3.0.0", + "info":{ + "title": "", + "version": "1.0" + } + } + """), }; } } diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/FunctionalTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/FunctionalTests.cs index 1a1f1fc..7f08a70 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/FunctionalTests.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/FunctionalTests.cs @@ -170,9 +170,9 @@ public async Task GenerateMultipleClients() private static string[] GetExpectedOperationsCode(string[]? bodyLines) => [ TestHelpers.GetExpectedSummary("Inform about environmental lighting conditions of a particular streetlight.", 4 + 4) + - GetExpectedPublisherCode("ReceiveLightMeasurement", "LightMeasuredPayload", 4 + 4, bodyLines), - GetExpectedSubscriberCode("TurnOn", "TurnOnOffPayload", 4 + 4, bodyLines), - GetExpectedSubscriberCode("TurnOff", "TurnOnOffPayload", 4 + 4, bodyLines), - GetExpectedSubscriberCode("DimLight", "DimLightPayload", 4 + 4, bodyLines), + GetExpectedReceiverCode("ReceiveLightMeasurement", "LightMeasuredPayload", 4 + 4, bodyLines), + GetExpectedSenderCode("TurnOn", "TurnOnOffPayload", 4 + 4, bodyLines), + GetExpectedSenderCode("TurnOff", "TurnOnOffPayload", 4 + 4, bodyLines), + GetExpectedSenderCode("DimLight", "DimLightPayload", 4 + 4, bodyLines), ]; } diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/FakeTextPreprocessor.cs b/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/FakeTextPreprocessor.cs index b55d060..53e253a 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/FakeTextPreprocessor.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/FakeTextPreprocessor.cs @@ -18,6 +18,11 @@ public string Process(string data, string? fileName) Invocactions.Add(new(Settings, true, [data, fileName])); return """ { + "asyncapi":"3.0.0", + "info":{ + "title": "", + "version": "1.0" + }, "components":{ "schemas":{ "schemaName":{ diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/TestHelpers.Amqp.cs b/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/TestHelpers.Amqp.cs index 80290c9..0a95bf9 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/TestHelpers.Amqp.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/TestHelpers.Amqp.cs @@ -7,7 +7,7 @@ internal static partial class TestHelpers public static string GetExpectedAmqpServiceCode(string className, int identCnt) => GetExpectedAmqpServiceCode(className, identCnt, [ GetExpectedSummary("Inform about environmental lighting conditions of a particular streetlight.", identCnt + 4) + - GetExpectedAmqpPublisherCode("ReceiveLightMeasurement", "LightMeasuredPayload", identCnt + 4) + GetExpectedAmqpReceiverCode("ReceiveLightMeasurement", "LightMeasuredPayload", identCnt + 4) ]); public static string GetExpectedAmqpServiceCode(string className, int identCnt, params string[] operationsCode) @@ -44,7 +44,7 @@ public static string GetExpectedAmqpServiceCode(string className, int identCnt, ident + "}\n"; } - public static string GetExpectedAmqpPublisherCode( + public static string GetExpectedAmqpSenderCode( string name, string payloadType, int identCnt, @@ -53,9 +53,9 @@ public static string GetExpectedAmqpPublisherCode( { string[] body = [ $"var exchange = \"{exchange?.Name}\";", - "var routingKey = \"smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured\";", + "var routingKey = \"smartylighting.streetlights.1.0.action.{streetlightId}.dim\";", string.Empty, - "var channel = _channelPool.GetChannel(\"receiveLightMeasurement\");", + $"var channel = _channelPool.GetChannel(\"{name}\");", "var exchangeProps = new Dictionary", "{", .. GetExchangeProps(operationBinding), @@ -70,7 +70,7 @@ .. GetExchangeProps(operationBinding), string.Empty, "var props = channel.CreateBasicProperties();", string.Empty, - "props.CorrelationId = \"receiveLightMeasurement_smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured\";", + $"props.CorrelationId = \"{name}_smartylighting.streetlights.1.0.action.{{streetlightId}}.dim\";", $"props.DeliveryMode = {((int?)operationBinding?.DeliveryMode) ?? 1};", $"props.Priority = {operationBinding?.Priority ?? 0};", $"props.Expiration = \"{operationBinding?.Expiration ?? 1000}\";", @@ -87,7 +87,7 @@ .. GetExchangeProps(operationBinding), string.Empty, "return Task.CompletedTask;", ]; - return GetExpectedPublisherCode(name, payloadType, identCnt, body); + return GetExpectedSenderCode(name, payloadType, identCnt, body); static IEnumerable GetExchangeProps(OperationBase? operationBinding) { @@ -116,7 +116,7 @@ static IEnumerable GetExchangeProps(OperationBase? operationBinding) } } - public static string GetExpectedAmqpSubscriberCode( + public static string GetExpectedAmqpReceiverCode( string name, string payloadType, int identCnt, @@ -124,9 +124,9 @@ public static string GetExpectedAmqpSubscriberCode( { string[] body = [ $"var queue = \"{channelBinding?.Queue.Name}\"; // queue from specification", - "var channel = _channelPool.GetChannel(\"dimLight\");", + $"var channel = _channelPool.GetChannel(\"{name}\");", $"var exchange = \"{channelBinding?.Exchange.Name}\";", - "var routingKey = \"smartylighting.streetlights.1.0.action.{streetlightId}.dim\";", + "var routingKey = \"smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured\";", string.Empty, "// TODO: declare passive?", "channel.QueueDeclare(queue);", @@ -162,10 +162,10 @@ public static string GetExpectedAmqpSubscriberCode( " consumer: consumer);", ]; - return GetExpectedSubscriberCode(name, payloadType, identCnt, body); + return GetExpectedReceiverCode(name, payloadType, identCnt, body); } - public static string GetExpectedPoolCode(string className, int identCnt) + public static string GetExpectedPoolCode(string className, int identCnt, params DOM.Channel[] channels) { var ident = new string(' ', identCnt); return @@ -269,16 +269,18 @@ public static string GetExpectedPoolCode(string className, int identCnt) IEnumerable GetChannelDeclarations() { - // foreach (var channel in channels) - // { - // var code = (channel.Publish ?? channel.Subscribe)?.OperationId switch - // { - // "receiveLightMeasurement" => "_channels.Add(\"receiveLightMeasurement\", CreateChannel(connection));", - // "dimLight" => GetSubscriberChannelDeclaration(channel, "dimLight"), - // _ => throw new InvalidOperationException("Unknown operationId"), - // }; - // yield return $"{ident} {code}\n"; - // } + foreach (var channel in channels) + { + var code = channel.Address switch + { + "smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured" + => GetSubscriberChannelDeclaration(channel, "ReceiveLightMeasurement"), + "smartylighting.streetlights.1.0.action.{streetlightId}.dim" + => "_channels.Add(\"DimLight\", CreateChannel(connection));", + _ => throw new InvalidOperationException("Unknown operationId"), + }; + yield return $"{ident} {code}\n"; + } yield break; } diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/TestHelpers.cs b/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/TestHelpers.cs index b2bffa7..2289ff5 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/TestHelpers.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/Infrastructure/TestHelpers.cs @@ -167,8 +167,8 @@ public static string GetExpectedSummary(string text, int identCnt) ident + "/// \n"; } - public static string GetExpectedPublisherCode(string name, string payloadType, int identCnt) - => GetExpectedPublisherCode(name, payloadType, identCnt, []); + public static string GetExpectedSenderCode(string name, string payloadType, int identCnt) + => GetExpectedSenderCode(name, payloadType, identCnt, []); /// /// Возвращает ожидаемый код паблишера. @@ -178,7 +178,7 @@ public static string GetExpectedPublisherCode(string name, string payloadType, i /// Количество лидирующих пробелов. /// Тело метода. Если null то тело не формируется. Если пустой массив то в теле пишется 'return Task.CompleetedTask'. /// Строка с кодом паблишера. - public static string GetExpectedPublisherCode(string name, string payloadType, int identCnt, string[]? bodyLines) + public static string GetExpectedSenderCode(string name, string payloadType, int identCnt, string[]? bodyLines) { var ident = new string(' ', identCnt); var body = bodyLines switch @@ -197,8 +197,8 @@ public static string GetExpectedPublisherCode(string name, string payloadType, i + bodyBlock; } - public static string GetExpectedSubscriberCode(string name, string payloadType, int identCnt) - => GetExpectedSubscriberCode(name, payloadType, identCnt, []); + public static string GetExpectedReceiverCode(string name, string payloadType, int identCnt) + => GetExpectedReceiverCode(name, payloadType, identCnt, []); /// /// Возвращает ожидаемый код подписчика. @@ -208,7 +208,7 @@ public static string GetExpectedSubscriberCode(string name, string payloadType, /// Количество лидирующих пробелов. /// Тело метода. Если null то тело не формируется. Если пустой массив то в теле пишется 'return Task.CompleetedTask'. /// Строка с кодом подписчика. - public static string GetExpectedSubscriberCode(string name, string payloadType, int identCnt, string[]? bodyLines) + public static string GetExpectedReceiverCode(string name, string payloadType, int identCnt, string[]? bodyLines) { var ident = new string(' ', identCnt); var body = bodyLines switch diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV2/MigrationTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV2/MigrationTests.cs new file mode 100644 index 0000000..619f3d3 --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV2/MigrationTests.cs @@ -0,0 +1,588 @@ +using System.Threading.Tasks; +using ApiCodeGenerator.AsyncApi.DOM; +using ApiCodeGenerator.AsyncApi.DOM.Traits; +using DeepEqual; + +namespace ApiCodeGenerator.AsyncApi.Tests.SerializationV2; + +public class MigrationTests : SerializationV3.TestBase +{ + private const string YamlHeaderV2 = """ + asyncapi: '2.6.0' + info: + title: title + version: '1.0' + channels: + t: + """; + + // https://www.asyncapi.com/docs/migration/migrating-to-v3#moved-metadata + [Test] + public async Task MoveExternalDocsIntoInfo() + { + var expected = new + { + Url = "bc7f6fa0-3b78-43e5-b316-ab7042418373", + Description = "94403503-b006-43f1-9630-ee2393e01295", + }; + var yaml = $""" + {YamlHeaderV2} + externalDocs: + {expected.ToYaml(indent: 2)} + """; + + await ReadPropertiesTest(yaml, expected, d => d.Info.ExternalDocs); + } + + // https://www.asyncapi.com/docs/migration/migrating-to-v3#moved-metadata + [Test] + public async Task MoveTagsIntoInfo() + { + var expected = new[] + { + new + { + ActualObject = new + { + Name = "658ddd81-da20-42be-a1f8-c117e6ffd2cc", + Description = "e52bcd9d-2179-49e4-ad76-cfacc6f83310", + }, + }, + }; + var yaml = $""" + {YamlHeaderV2} + tags: + {expected.Select(i => i.ActualObject).ToYaml(indent: 2)} + """; + + await ReadPropertiesTest(yaml, expected, d => d.Info.Tags); + } + + // https://www.asyncapi.com/docs/migration/migrating-to-v3#server-url-splitting-up + [TestCaseSource(nameof(GetServerUrlSplitingCases))] + public async Task ServerUrlSpliting(string yaml, object expected, Func getter) + { + await ReadPropertiesTest(yaml, expected, getter); + } + + [Test] + public void ServerUrlSpliting_UrlRequired() + { + var yaml = $""" + {YamlHeaderV2} + servers: + s: + protocol: amqp + """; + RequiredPropertiesTest(yaml, "url"); + } + + [Test] + public async Task ReadChannels() + { + const string ChannelId = "user/signedup"; + const string expectedChannelId = "userSignedup"; + var expected = new Channel + { + Address = ChannelId, + Bindings = new ChannelBindings + { + Amqp = new() { Is = DOM.Bindings.Amqp.ChannelType.Queue }, + }, + Description = "302d93cb-42b0-4ba8-b8ca-bf94585934a0", + Messages = + { + ["msg1"] = new NamedReference { ReferencePath = "#/components/messages/msg1" }, + }, + Parameters = + { + ["p1"] = new Parameter() { Default = "257b6392-b4a3-436c-908f-d20949b8d351" }, + }, + Servers = [new() { ReferencePath = "#/servers/dev" }], + ExtensionData = null, + }; + + var yaml = $""" + {YamlHeaderV2} + {ChannelId}: + bindings: + amqp: + is: {expected.Bindings.ActualObject.Amqp!.Is} + description: '{expected.Description}' + parameters: + {expected.Parameters.Single().Key}: + {expected.Parameters.Single().Value.ActualObject.ToYaml(indent: 8)} + servers: + - dev + publish: + message: + $ref: '{expected.Messages.Single().Value.ReferencePath}' + components: + messages: + msg1: + payload: + type: object + properties: + displayName: + type: string + description: Name of the user + servers: + dev: + url: amqp://host + protocol: amqp + """; + var comparison = new ComparisonBuilder() + .IgnoreUnmatchedProperties() + .IgnoreProperty>(r => r.ActualObject) // ignore prop for expected.Servers + .IgnoreProperty>(r => r.ActualObject) // ignore prop for expected.Messages + .Create(); + + await ReadPropertiesTest( + yaml, + expected, + d => + { + Assert.That(d.Channels, Is.Not.Null.And.ContainKey(expectedChannelId)); + var channel1 = d.Channels[expectedChannelId]; + Assert.NotNull(channel1); + return channel1; + }, + comparison); + } + + [Test] + public async Task ReadChannelsRef() + { + const string ChannelId = "user/signedup"; + const string componentChannelId = "userSignedup"; + const string refPath = $"#/components/channels/{componentChannelId}"; + var expected = new Channel + { + Address = ChannelId, + Bindings = new ChannelBindings + { + Amqp = new() { Is = DOM.Bindings.Amqp.ChannelType.Queue }, + }, + Description = "302d93cb-42b0-4ba8-b8ca-bf94585934a0", + Messages = + { + ["msg1"] = new NamedReference { ReferencePath = "#/components/messages/msg1" }, + }, + Parameters = + { + ["p1"] = new Parameter() { Default = "257b6392-b4a3-436c-908f-d20949b8d351" }, + }, + Servers = [new() { ReferencePath = "#/servers/dev" }], + ExtensionData = null, + }; + + var yaml = $""" + {YamlHeaderV2} + {ChannelId}: + $ref: '{refPath}' + + components: + channels: + {componentChannelId}: + bindings: + amqp: + is: {expected.Bindings.ActualObject.Amqp!.Is} + description: '{expected.Description}' + parameters: + {expected.Parameters.Single().Key}: + {expected.Parameters.Single().Value.ActualObject.ToYaml(indent: 8)} + servers: + - dev + publish: + message: + $ref: '{expected.Messages.Single().Value.ReferencePath}' + messages: + msg1: + payload: + type: object + properties: + displayName: + type: string + description: Name of the user + servers: + dev: + url: amqp://host + protocol: amqp + """; + var comparison = new ComparisonBuilder() + .IgnoreUnmatchedProperties() + .IgnoreProperty>(r => r.ActualObject) // ignore prop for expected.Servers + .IgnoreProperty>(r => r.ActualObject) // ignore prop for expected.Messages + .Create(); + + await ReadPropertiesTest( + yaml, + expected, + d => + { + Assert.That(d.Components.Channels, Is.Not.Null.And.ContainKey(componentChannelId)); + var channel1 = d.Components.Channels[componentChannelId]; + Assert.NotNull(channel1); + + Assert.That(d.Channels, Is.Not.Null.And.ContainKey(componentChannelId)); + var channelRef = d.Channels[componentChannelId]; + Assert.That(channelRef.ReferencePath, Is.EqualTo(refPath)); + Assert.That(channelRef.ActualObject, Is.SameAs(channel1.ActualObject)); + return channel1; + }, + comparison); + } + + [TestCaseSource(nameof(GetReadOperationCases))] + public Task ReadOperations(string yaml, object expectedOperation, Func> getOperation) + { + var comparison = new ComparisonBuilder() + .IgnoreUnmatchedProperties() + .IgnoreProperty>(r => r.ActualObject) + .IgnoreProperty>(r => r.ActualObject) + .Create(); + return ReadPropertiesTest(yaml, expectedOperation, getOperation, comparison); + } + + [Test] + public async Task ReadMessages() + { + const string MessageId = "11b8fbfa"; + var expected = new + { + // payloads and headers are checked separately + CorrelationId = new { Location = "46e5b57a-4326-4f01-98e8-87bcf9d19804" }, + ContentType = "25d353d7-8e43-496f-a8a0-8295ed7de850", + Name = "526231ea-da38-43e1-8bbb-6e33ce896a39", + Title = "bab568de-0467-42c1-bf94-a4e48212c93e", + Summary = "4d5eee97-88d3-402b-b24f-054e69d8ffa9", + Description = "d9d51365-0ef3-464d-b60a-28e7ac4aa5cd", + Tags = new[] { new { Name = "3b009642-5a68-41dd-a685-ed16c9c1d32f" } }, + ExternalDocs = new { Url = "a86e2dc6-ab10-4fbd-81d8-b08c898c0748" }, + Bindings = new { Amqp = new object() }, + Examples = new[] { new { Name = "9189f33b-3900-40d4-aae1-da322d5b32d8" } }, + Traits = new[] { new { Title = "29d601e6-d6cb-4532-a905-8cf4d3035b3e" } }, + }; + + var yaml = $""" + {YamlHeaderV2} + publish: + message: + messageId: '{MessageId}' + headers: + type: object + payload: + type: object + schemaFormat: {AsyncApiSchema.OpenApi} + {expected.ToYaml(indent: 8)} + """; + + await ReadPropertiesTest(yaml, expected, d => + { + var channel = d.Channels.GetValueOrNull("t"); + Assert.NotNull(channel, "Channel 't' not found"); + var msg = channel.ActualObject.Messages.GetValueOrNull(MessageId)?.ActualObject; + Assert.NotNull(msg, "Message '{0}' not found", MessageId); + Assert.That(msg.Headers?.ActualObject, Is.Not.Null.And.Property("SchemaFormat").EqualTo(AsyncApiSchema.AsyncApi3)); + Assert.That(msg.Payload?.ActualObject, Is.Not.Null.And.Property("SchemaFormat").EqualTo(AsyncApiSchema.OpenApi)); + return msg; + }); + } + + [Test] + public async Task ReadMessagesRef() + { + const string MessageId = "aaeb8fd4"; + var expected = new + { + // payloads and headers are checked separately + CorrelationId = new { Location = "744840e3-c593-41c9-a6e1-05e08fb6d059" }, + ContentType = "9092dabc-f628-4123-944c-c77525f99c47", + Name = "08c29430-58b2-4e7c-b091-f62022088725", + Title = "bd6e8c55-9791-426c-b51c-599217c7f849", + Summary = "0aa73c1c-b1d7-493b-88eb-917f11b83730", + Description = "c03de12d-1297-4e84-a481-ae2c39cae951", + Tags = new[] { new { Name = "4e2a254c-1a47-4bd3-a75f-aab6c69d13f3" } }, + ExternalDocs = new { Url = "e9d9d011-30a4-4300-8172-e0d368462c19" }, + Bindings = new { Amqp = new object() }, + Examples = new[] { new { Name = "4da980d2-3060-4bfd-90fc-b8031f2fda80" } }, + Traits = new[] { new { Title = "b9f09244-1cd3-446e-ae06-80614d94368a" } }, + }; + + var yaml = $""" + {YamlHeaderV2} + components: + messages: + {MessageId}: + messageId: '{MessageId}' + headers: + type: object + payload: + type: object + schemaFormat: {AsyncApiSchema.OpenApi} + {expected.ToYaml(indent: 8)} + """; + + await ReadPropertiesTest(yaml, expected, d => + { + var msg = d.Components.Messages.GetValueOrNull(MessageId)?.ActualObject; + Assert.NotNull(msg, "Message '{0}' not found", MessageId); + Assert.That(msg.Headers?.ActualObject, Is.Not.Null.And.Property("SchemaFormat").EqualTo(AsyncApiSchema.AsyncApi3)); + Assert.That(msg.Payload?.ActualObject, Is.Not.Null.And.Property("SchemaFormat").EqualTo(AsyncApiSchema.OpenApi)); + return msg; + }); + } + + [Test] + public async Task ReadServerSecurityRef() + { + const string refPath = "#/components/securitySchemes/oauth"; + var expected = new DOM.Security.OAuth2SecurityScheme + { + Flows = new() + { + Implicit = new() + { + AuthorizationUrl = "http://example.com", + AvailableScopes = new Dictionary(), + }, + }, + Scopes = ["test"], + }; + + var yaml = $""" + {YamlHeaderV2} + servers: + test: + url: test.mykafkacluster.org:28092 + protocol: kafka-secure + security: + - oauth: ["test"] + components: + securitySchemes: + oauth: + {expected.ToYaml(indent: 6)} + """; + + await ResolveReferernceTest(yaml, refPath, expected, d => + { + var server = d.Servers.GetValueOrNull("test")?.ActualObject; + Assert.NotNull(server, "Server 'test' not found."); + Assert.That(server.Security, Is.Not.Null.And.Count.EqualTo(1)); + return server.Security.Single(); + }); + } + + [Test] + public async Task ReadOperationSecurityRef() + { + const string refPath = "#/components/securitySchemes/oauth"; + var expected = new DOM.Security.OAuth2SecurityScheme + { + Flows = new() + { + Implicit = new() + { + AuthorizationUrl = "http://example.com", + AvailableScopes = new Dictionary(), + }, + }, + Scopes = ["test"], + }; + + var yaml = $""" + {YamlHeaderV2} + publish: + operationId: op1 + security: + - oauth: ["test"] + components: + securitySchemes: + oauth: + {expected.ToYaml(indent: 6)} + """; + + await ResolveReferernceTest(yaml, refPath, expected, d => + { + var operation = d.Operations.GetValueOrNull("op1")?.ActualObject; + Assert.NotNull(operation, "Operation 'op1' not found."); + Assert.That(operation.Security, Is.Not.Null.And.Count.EqualTo(1)); + return operation.Security.Single(); + }); + } + + private static IEnumerable GetServerUrlSplitingCases() + { + var expected = new + { + Host = "host", + PathName = "/path", + Protocol = "amqp", + }; + + var url = $"{expected.Protocol}://{expected.Host}{expected.PathName}"; + + yield return + new TestCaseData($""" + {YamlHeaderV2} + servers: + s: + url: '{url}' + protocol: amqp + """, + expected, + (AsyncApiDocument d) => d.Servers.GetValueOrNull("s")?.ActualObject) + .SetArgDisplayNames("servers"); + + yield return + new TestCaseData($""" + {YamlHeaderV2} + components: + servers: + s: + url: '{url}' + protocol: amqp + """, + expected, + (AsyncApiDocument d) => d.Components.Servers.GetValueOrNull("s")?.ActualObject) + .SetArgDisplayNames("components.servers"); + + yield return + new TestCaseData($""" + {YamlHeaderV2} + servers: + s: + $ref: '#/components/servers/s' + components: + servers: + s: + url: '{url}' + protocol: amqp + """, + expected, + (AsyncApiDocument d) => d.Servers.GetValueOrNull("s")?.ActualObject) + .SetArgDisplayNames("servers.s.$ref"); + } + + private static IEnumerable GetReadOperationCases() + { + // channels.subscribe + { + var expected = CreateOperation(OperationAction.Send); + + yield return new TestCaseData( + $""" + {YamlHeaderV2} + subscribe: + operationId: op1 + bindings: + {expected.Bindings!.ActualObject.ToYaml(indent: 8)} + description: '{expected.Description}' + externalDocs: + url: '{expected.ExternalDocs!.ActualObject.Url}' + message: + messageId: msg1 + summary: '{expected.Summary}' + tags: + - {expected.Tags!.Single().ActualObject.ToYaml(indent: 8)} + traits: + - {expected.Traits!.Single().ActualObject.ToYaml(indent: 8)} + """, + expected, + (AsyncApiDocument d) => d.Operations.GetValueOrNull("op1")) + .SetArgDisplayNames("channels.subscribe"); + } + + // channels.publish + { + var expected = CreateOperation(OperationAction.Receive); + yield return new TestCaseData( + $""" + {YamlHeaderV2} + publish: + operationId: op1 + bindings: + {expected.Bindings!.ActualObject.ToYaml(indent: 8)} + description: '{expected.Description}' + externalDocs: + url: '{expected.ExternalDocs!.ActualObject.Url}' + message: + messageId: msg1 + summary: '{expected.Summary}' + tags: + - {expected.Tags!.Single().ActualObject.ToYaml(indent: 8)} + traits: + - {expected.Traits!.Single().ActualObject.ToYaml(indent: 8)} + """, + expected, + (AsyncApiDocument d) => d.Operations.GetValueOrNull("op1")) + .SetArgDisplayNames("channels.publish"); + } + + //channelsRef.publish + { + var expected = CreateOperation(OperationAction.Receive, "#/components/"); + expected.Channel.ReferencePath = "#/components/channels/t"; + const string refPath = "#/components/operations/op1"; + + yield return new TestCaseData( + $""" + {YamlHeaderV2} + $ref: '#/components/channels/t' + components: + channels: + t: + publish: + operationId: op1 + bindings: + {expected.Bindings!.ActualObject.ToYaml(indent: 10)} + description: '{expected.Description}' + externalDocs: + url: '{expected.ExternalDocs!.ActualObject.Url}' + message: + messageId: msg1 + summary: '{expected.Summary}' + tags: + - {expected.Tags!.Single().ActualObject.ToYaml(indent: 10)} + traits: + - {expected.Traits!.Single().ActualObject.ToYaml(indent: 10)} + """, + expected, + (AsyncApiDocument d) => + { + var op = d.Components.Operations.GetValueOrNull("op1"); + Assert.That(op, Is.Not.Null); + var opRef = d.Operations.GetValueOrNull("op1"); + Assert.That(opRef, Is.Not.Null); + Assert.That(opRef.ReferencePath, Is.EqualTo(refPath)); + Assert.That(opRef.ActualObject, Is.SameAs(op.ActualObject)); + + return op; + }) + .SetArgDisplayNames("channelsRef.publish"); + } + + Operation CreateOperation(OperationAction action, string refRoot = "#/") + { + return new Operation + { + Action = action, + Bindings = new OperationBindings + { + Amqp = new DOM.Bindings.Amqp.OperationV0_3 + { + Ack = true, + }, + }, + Channel = new Reference { ReferencePath = refRoot + "channels/t" }, + Description = "40750c12-74b3-471d-83a1-c4c802362784", + ExternalDocs = new ExternalDocumentation { Url = new Uri("http://example.com/") }, + Messages = [new Reference { ReferencePath = refRoot + "channels/t/messages/msg1" }], + /* The Security property will be tested in other tests */ + Summary = "ee1c906e-0fc7-437e-8abb-4b9530bc51f8", + Tags = [new Tag { Name = "2a372cb5-c2ed-4d05-95a1-b2d481d3d1f5" }], + Traits = [new OperationTraits { Summary = "bb1719cc-6fc5-45a9-a1ff-08a292be1144" }], + }; + } + } +} diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/AsyncApiDocumentTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/AsyncApiDocumentTests.cs index 0e5f00f..fa8acec 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/AsyncApiDocumentTests.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/AsyncApiDocumentTests.cs @@ -4,10 +4,10 @@ namespace ApiCodeGenerator.AsyncApi.Tests.SerializationV3; public class AsyncApiDocumentTests : TestBase { - [TestCase("asyncapi: '3.0.0'")] - [TestCase("info: { title: 'test', version: '1.0' }")] - public void RequiredProperties(string yaml) - => RequiredPropertiesTest(yaml); + [TestCase("asyncapi: '3.0.0'", "info")] + [TestCase("info: { title: 'test', version: '1.0' }", "asyncapi")] + public void RequiredProperties(string yaml, string propName) + => RequiredPropertiesTest(yaml, propName); [Test] public async Task ReadExtensions() diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/CorrelationIdTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/CorrelationIdTests.cs index 5cf6451..eb41a54 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/CorrelationIdTests.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/CorrelationIdTests.cs @@ -19,7 +19,7 @@ public void RequiredProperties() description: '' """; - RequiredPropertiesTest(yaml); + RequiredPropertiesTest(yaml, "location"); } [Test] diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ExternalDocumentationTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ExternalDocumentationTests.cs index 1a88482..9bf5a32 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ExternalDocumentationTests.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ExternalDocumentationTests.cs @@ -17,7 +17,7 @@ public void RequiredProperties() {ExternalDocDefinition} description: '' """; - RequiredPropertiesTest(yaml); + RequiredPropertiesTest(yaml, "url"); } [Test] diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/InfoTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/InfoTests.cs index 2f4d3dc..83078c2 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/InfoTests.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/InfoTests.cs @@ -7,15 +7,17 @@ public class InfoTests : TestBase info: title: test """, - TestName = $"{nameof(RequiredProperties)} - without version")] + "version", + TestName = $"{nameof(RequiredProperties)}(version)")] [TestCase(""" asyncapi: '3.0.0' info: version: test """, - TestName = $"{nameof(RequiredProperties)} - without title")] - public void RequiredProperties(string yaml) - => RequiredPropertiesTest(yaml); + "title", + TestName = $"{nameof(RequiredProperties)}(title)")] + public void RequiredProperties(string yaml, string propName) + => RequiredPropertiesTest(yaml, propName); [Test] public async Task ReadProperties() diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/LicenseTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/LicenseTests.cs index 9ad0365..f52f0d2 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/LicenseTests.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/LicenseTests.cs @@ -6,9 +6,10 @@ public class LicenseTests : TestBase {{YamlHeader}} license: {} """, - TestName = $"{nameof(RequiredProperties)} - without name")] - public void RequiredProperties(string yaml) - => RequiredPropertiesTest(yaml); + "name", + TestName = $"{nameof(RequiredProperties)}(name)")] + public void RequiredProperties(string yaml, string propName) + => RequiredPropertiesTest(yaml, propName); [Test] public async Task ReadProperties() diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/MessageTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/MessageTests.cs index 58e2448..c5cc627 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/MessageTests.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/MessageTests.cs @@ -30,7 +30,7 @@ public async Task ReadProperties() ExternalDocs = new { Url = "039a707a-85c5-474e-bfe8-be156f813e8f" }, Bindings = new { Amqp = new object() }, Examples = new[] { new { Name = "59d53925-c20f-4cda-b819-ea9b28af10fc" } }, - Traits = new { Title = "11690732-96c8-498c-9f9f-b54d5278f118" }, + Traits = new[] { new { Title = "11690732-96c8-498c-9f9f-b54d5278f118" } }, }; var yaml = $""" @@ -167,13 +167,13 @@ public async Task TraitsRef() {YamlHeader} {MessageDefinition} traits: - $ref: '{refPath}' + - $ref: '{refPath}' messageTraits: t: {expected.ToYaml(indent: 6)} """; - await MessageResolveReferernceTest(yaml, refPath, expected, o => o.Traits); + await MessageResolveReferernceTest(yaml, refPath, expected, o => o.Traits?.Single()); } private Task MessageResolveReferernceTest(string yaml, string refPath, object expected, Func getRef) diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OAuthFlowsTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OAuthFlowsTests.cs index 211feeb..8c9fd93 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OAuthFlowsTests.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OAuthFlowsTests.cs @@ -71,7 +71,7 @@ public async Task ReadProperties() } [TestCaseSource(nameof(GetRequiredPropertiesCases))] - public void RequiredProperties(object expected) + public void RequiredProperties(object expected, string propName) { var yaml = $""" {YamlHeader} @@ -79,7 +79,7 @@ public void RequiredProperties(object expected) {expected.ToYaml(indent: 8)} """; - RequiredPropertiesTest(yaml); + RequiredPropertiesTest(yaml, propName); } private static IEnumerable GetRequiredPropertiesCases() @@ -95,8 +95,9 @@ private static IEnumerable GetRequiredPropertiesCases() { AuthorizationUrl = "992741d0-0e5b-4e48-b8f9-f05b7732e431", }, - }) - .SetArgDisplayNames("implicit: without availableScopes"); + }, + "availableScopes") + .SetArgDisplayNames("type: implicit", "availableScopes"); yield return new TestCaseData( new @@ -105,8 +106,9 @@ private static IEnumerable GetRequiredPropertiesCases() { AvailableScopes, }, - }) - .SetArgDisplayNames("implicit: without authorizationUrl"); + }, + "authorizationUrl") + .SetArgDisplayNames("type: implicit", "authorizationUrl"); yield return new TestCaseData( new @@ -115,8 +117,9 @@ private static IEnumerable GetRequiredPropertiesCases() { TokenUrl = "ea73d2a8-968e-4de8-a5c4-c50d1b27343f", }, - }) - .SetArgDisplayNames("password: without availableScopes"); + }, + "availableScopes") + .SetArgDisplayNames("type: password", "availableScopes"); yield return new TestCaseData( new @@ -125,8 +128,9 @@ private static IEnumerable GetRequiredPropertiesCases() { AvailableScopes, }, - }) - .SetArgDisplayNames("password: without tokenUrl"); + }, + "tokenUrl") + .SetArgDisplayNames("type: password", "tokenUrl"); yield return new TestCaseData( new @@ -135,8 +139,9 @@ private static IEnumerable GetRequiredPropertiesCases() { TokenUrl = "ea73d2a8-968e-4de8-a5c4-c50d1b27343f", }, - }) - .SetArgDisplayNames("clientCredentials: without availableScopes"); + }, + "availableScopes") + .SetArgDisplayNames("type: clientCredentials"); yield return new TestCaseData( new @@ -145,8 +150,9 @@ private static IEnumerable GetRequiredPropertiesCases() { AvailableScopes, }, - }) - .SetArgDisplayNames("clientCredentials: without tokenUrl"); + }, + "tokenUrl") + .SetArgDisplayNames("type: clientCredentials", "tokenUrl"); yield return new TestCaseData( new @@ -156,8 +162,9 @@ private static IEnumerable GetRequiredPropertiesCases() TokenUrl = "69071823-16c5-476a-b9bb-1bcb4926ad38", AuthorizationUrl = "cd147b08-42d3-480b-a9ea-9167e9dc8aef", }, - }) - .SetArgDisplayNames("authorizationCode: without availableScopes"); + }, + "availableScopes") + .SetArgDisplayNames("type: authorizationCode", "availableScopes"); yield return new TestCaseData( new @@ -167,8 +174,9 @@ private static IEnumerable GetRequiredPropertiesCases() AvailableScopes, AuthorizationUrl = "88cfe0a6-8ed3-4d59-a405-76a708899cb1", }, - }) - .SetArgDisplayNames("authorizationCode: without tokenUrl"); + }, + "tokenUrl") + .SetArgDisplayNames("type: authorizationCode", "tokenUrl"); yield return new TestCaseData( new @@ -178,8 +186,9 @@ private static IEnumerable GetRequiredPropertiesCases() TokenUrl = "d88c1bd8-ea04-40d6-9add-b1a345a4d6ae", AvailableScopes, }, - }) - .SetArgDisplayNames("authorizationCode: without authorizationurl"); + }, + "authorizationUrl") + .SetArgDisplayNames("type: authorizationCode", "authorizationUrl"); } private static OAuthFlows GetOAuthFlows(AsyncApiDocument document) diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OperationReplyAddressTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OperationReplyAddressTests.cs index 4a2127e..9bf82a1 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OperationReplyAddressTests.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OperationReplyAddressTests.cs @@ -18,7 +18,7 @@ public void RequiredProperties() {{OperationReplyAddressDefinition}} {} """; - RequiredPropertiesTest(yaml); + RequiredPropertiesTest(yaml, "location"); } [Test] diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OpertationTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OpertationTests.cs index 744d147..6d7c477 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OpertationTests.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/OpertationTests.cs @@ -23,13 +23,15 @@ public class OpertationTests : TestBase op1: action: send """, - TestName = nameof(RequiredProperties) + "- without channel")] - [TestCase( + "channel", + TestName = $"{nameof(RequiredProperties)}(channel)")] + [TestCase(YamlHeader + OperationDefinition, - TestName = nameof(RequiredProperties) + "- without action")] - public void RequiredProperties(string yaml) + "action", + TestName = $"{nameof(RequiredProperties)}(action)")] + public void RequiredProperties(string yaml, string propName) { - RequiredPropertiesTest(yaml); + RequiredPropertiesTest(yaml, propName); } [Test] @@ -60,7 +62,7 @@ public async Task ReadProperties() Tags = new[] { new { Name = "tag" } }, ExternalDocs = new { Url = "url" }, Bindings = new { Amqp = new object() }, - Traits = new { Title = "abf76138-4a3b-43d6-b4b6-488a4e7a69e6" }, + Traits = new[] { new { Title = "abf76138-4a3b-43d6-b4b6-488a4e7a69e6" } }, Messages = new object[0], Reply = new { Address = new { Location = "e6c1c3d4-691e-476d-ba69-65be200066f9" } }, }; @@ -176,13 +178,13 @@ public async Task TraitsRef() {OperationDefinition} action: send traits: - $ref: '{refPath}' + - $ref: '{refPath}' operationTraits: t: {expected.ToYaml(indent: 6)} """; - await OperationResolveReferenceTest(yaml, refPath, expected, o => o.Traits); + await OperationResolveReferenceTest(yaml, refPath, expected, o => o.Traits?.Single()); } [Test] diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/SecuritySchemeTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/SecuritySchemeTests.cs index ec5b88f..8d52bb8 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/SecuritySchemeTests.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/SecuritySchemeTests.cs @@ -14,8 +14,8 @@ public class SecuritySchemeTests : TestBase """; [TestCaseSource(nameof(GetRequiredPropertiesCases))] - public void RequiredProperties(string yaml) - => RequiredPropertiesTest(yaml); + public void RequiredProperties(string yaml, string propName) + => RequiredPropertiesTest(yaml, propName); [Test] public async Task ReadExtensions() @@ -47,52 +47,59 @@ private static IEnumerable GetRequiredPropertiesCases() {YamlHeader} {SchemeDefinintion} description: '' - """) - .SetArgDisplayNames("Type not set"); + """, + "type") + .SetArgDisplayNames("yaml", "type"); yield return new TestCaseData($""" {YamlHeader} {SchemeDefinintion} type: apiKey - """) - .SetArgDisplayNames("ApiKey without 'in'"); + """, + "in") + .SetArgDisplayNames("ApiKey", "in"); yield return new TestCaseData($""" {YamlHeader} {SchemeDefinintion} type: http - """) - .SetArgDisplayNames("Http without 'scheme'"); + """, + "scheme") + .SetArgDisplayNames("Http", "scheme"); yield return new TestCaseData($""" {YamlHeader} {SchemeDefinintion} type: httpApiKey name: '123' - """) - .SetArgDisplayNames("HttpApiKey without 'in'"); + """, + "in") + .SetArgDisplayNames("HttpApiKey", "in"); yield return new TestCaseData($""" {YamlHeader} {SchemeDefinintion} type: httpApiKey in: cookie - """) - .SetArgDisplayNames("HttpApiKey without 'name'"); + """, + "name") + .SetArgDisplayNames("HttpApiKey", "name"); yield return new TestCaseData($""" {YamlHeader} {SchemeDefinintion} type: oauth2 - """) - .SetArgDisplayNames("OAuth2 without 'flows'"); + """, + "flows") + .SetArgDisplayNames("OAuth2", "flows"); yield return new TestCaseData($""" {YamlHeader} {SchemeDefinintion} type: openIdConnect - """) - .SetArgDisplayNames("OpenIdConnect without 'openIdConnectUrl'"); + """, + "openIdConnectUrl") + .SetArgDisplayNames("OpenIdConnect", "openIdConnectUrl"); } private static IEnumerable GetReadPropertiesCases() diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ServerTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ServerTests.cs index 7aee020..c462674 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ServerTests.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/ServerTests.cs @@ -26,7 +26,8 @@ public class ServerTests : TestBase test: host: '' """, - TestName = $"{nameof(RequiredProperties)} - without protocol")] + "protocol", + TestName = $"{nameof(RequiredProperties)}(protocol)")] [TestCase($""" {YamlHeader} components: @@ -34,9 +35,10 @@ public class ServerTests : TestBase test: protocol: '' """, - TestName = $"{nameof(RequiredProperties)} - without host")] - public void RequiredProperties(string yaml) - => RequiredPropertiesTest(yaml); + "host", + TestName = $"{nameof(RequiredProperties)}(host)")] + public void RequiredProperties(string yaml, string propName) + => RequiredPropertiesTest(yaml, propName); [Test] public async Task ReadProperties() diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/TagTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/TagTests.cs index ca71862..4f054d3 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/TagTests.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/TagTests.cs @@ -17,7 +17,7 @@ public void RequiredProperties() {TagDefinition} description: '' """; - RequiredPropertiesTest(yaml); + RequiredPropertiesTest(yaml, "name"); } [Test] diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/TestBase.cs b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/TestBase.cs index 9a1135a..9d439d0 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/TestBase.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/SerializationV3/TestBase.cs @@ -14,11 +14,11 @@ public abstract class TestBase """; - protected void RequiredPropertiesTest(string yaml) + protected void RequiredPropertiesTest(string yaml, string propName) { - var ex = Assert.ThrowsAsync(() => AsyncApiSerializer.FromYamlAsync(yaml)); + var ex = Assert.ThrowsAsync(() => AsyncApiSerializer.FromYamlAsync(yaml)); Assert.That(ex, Is.Not.Null); - Assert.That(ex.Message, Does.StartWith("Required property ")); + Assert.That(ex.Message, Does.StartWith($"Json parsing failed. Required property '{propName}'")); } protected async Task ReadExtensionsTest(string yaml, string value, Func getter) @@ -40,7 +40,11 @@ protected async Task ReadExtensionsTest(string yaml, string value, Func getter) + protected async Task ReadPropertiesTest( + string yaml, + object expected, + Func getter, + DeepEqual.IComparison? comparison = null) { var document = await AsyncApiSerializer.FromYamlAsync(yaml); @@ -52,7 +56,7 @@ protected async Task ReadPropertiesTest(string yaml, object expected, Func(string yaml, string refPath, object expected, Func getRef) diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/asyncApi/asyncapi.yml b/test/ApiCodeGenerator.AsyncApi.Tests/asyncApi/asyncapi.yml index 80457a0..4848404 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/asyncApi/asyncapi.yml +++ b/test/ApiCodeGenerator.AsyncApi.Tests/asyncApi/asyncapi.yml @@ -1,4 +1,4 @@ -asyncapi: "2.6.0" +asyncapi: 3.0.0 info: title: Streetlights Kafka API version: "1.0.0" @@ -12,89 +12,115 @@ info: * Receive real-time information about environmental lighting conditions 📈 license: name: Apache 2.0 - url: https://www.apache.org/licenses/LICENSE-2.0 - + url: "https://www.apache.org/licenses/LICENSE-2.0" +defaultContentType: application/json servers: scram-connections: - url: test.mykafkacluster.org:18092 + host: "test.mykafkacluster.org:18092" protocol: kafka-secure description: Test broker secured with scramSha256 security: - - saslScram: [] + - $ref: "#/components/securitySchemes/saslScram" tags: - name: "env:test-scram" - description: "This environment is meant for running internal tests through scramSha256" + 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" + 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" - + description: This resource is private and only available to certain users mtls-connections: - $ref: "#/components/servers/mtls-connections" - -defaultContentType: application/json - + 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: - smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured: + 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" - publish: - summary: Inform about environmental lighting conditions of a particular streetlight. - operationId: receiveLightMeasurement - traits: - - $ref: "#/components/operationTraits/kafka" - message: - $ref: "#/components/messages/lightMeasured" - tags: - - name: pub - - smartylighting.streetlights.1.0.action.{streetlightId}.turn.on: + lightTurnOn: + address: "smartylighting.streetlights.1.0.action.{streetlightId}.turn.on" + messages: + turnOn: + $ref: "#/components/messages/turnOnOff" parameters: streetlightId: $ref: "#/components/parameters/streetlightId" - subscribe: - operationId: turnOn - traits: - - $ref: "#/components/operationTraits/kafka" - message: + lightTurnOff: + address: "smartylighting.streetlights.1.0.action.{streetlightId}.turn.off" + messages: + turnOff: $ref: "#/components/messages/turnOnOff" - tags: - - name: sub - - smartylighting.streetlights.1.0.action.{streetlightId}.turn.off: parameters: streetlightId: $ref: "#/components/parameters/streetlightId" - subscribe: - operationId: turnOff - traits: - - $ref: "#/components/operationTraits/kafka" - message: - $ref: "#/components/messages/turnOnOff" - tags: - - name: sub - - smartylighting.streetlights.1.0.action.{streetlightId}.dim: + lightsDim: + address: "smartylighting.streetlights.1.0.action.{streetlightId}.dim" + messages: + dimLight: + $ref: "#/components/messages/dimLight" parameters: streetlightId: $ref: "#/components/parameters/streetlightId" - subscribe: - operationId: dimLight - traits: - - $ref: "#/components/operationTraits/kafka" - message: - $ref: "#/components/messages/dimLight" - tags: - - name: sub - +operations: + receiveLightMeasurement: + action: receive + channel: + $ref: "#/channels/lightingMeasured" + summary: >- + Inform about environmental lighting conditions of a particular + streetlight. + traits: + - $ref: "#/components/operationTraits/kafka" + messages: + - $ref: "#/channels/lightingMeasured/messages/lightMeasured" + turnOn: + action: send + channel: + $ref: "#/channels/lightTurnOn" + traits: + - $ref: "#/components/operationTraits/kafka" + messages: + - $ref: "#/channels/lightTurnOn/messages/turnOn" + turnOff: + action: send + channel: + $ref: "#/channels/lightTurnOff" + traits: + - $ref: "#/components/operationTraits/kafka" + messages: + - $ref: "#/channels/lightTurnOff/messages/turnOff" + dimLight: + action: send + channel: + $ref: "#/channels/lightsDim" + traits: + - $ref: "#/components/operationTraits/kafka" + messages: + - $ref: "#/channels/lightsDim/messages/dimLight" components: messages: lightMeasured: name: lightMeasured title: Light measured - summary: Inform about environmental lighting conditions of a particular streetlight. + summary: >- + Inform about environmental lighting conditions of a particular + streetlight. contentType: application/json traits: - $ref: "#/components/messageTraits/commonHeaders" @@ -116,7 +142,6 @@ components: - $ref: "#/components/messageTraits/commonHeaders" payload: $ref: "#/components/schemas/dimLightPayload" - schemas: lightMeasuredPayload: type: object @@ -133,8 +158,8 @@ components: command: type: string enum: - - on - - off + - "on" + - "off" description: Whether to turn on or off the light. sentAt: $ref: "#/components/schemas/sentAt" @@ -152,7 +177,6 @@ components: type: string format: date-time description: Date and time when the message was sent. - securitySchemes: saslScram: type: scramSha256 @@ -160,13 +184,9 @@ components: certs: type: X509 description: Download the certificate files from service provider - parameters: streetlightId: description: The ID of the streetlight. - schema: - type: string - messageTraits: commonHeaders: headers: @@ -176,40 +196,11 @@ components: type: integer minimum: 0 maximum: 100 - operationTraits: kafka: bindings: kafka: clientId: type: string - enum: ["my-app-id"] - - servers: - mtls-connections: - url: test.mykafkacluster.org:28092 - protocol: kafka-secure - description: Test broker secured with X509 - security: - - 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" - variables: - someRefVariable: - $ref: "#/components/serverVariables/someRefVariable" - someVariable: - description: Some variable - - serverVariables: - someRefVariable: - description: Some ref variable - default: def - enum: - - def - examples: - - exam + enum: + - my-app-id From b5ea470614ada84c24ed59c47541f889fe529030 Mon Sep 17 00:00:00 2001 From: Gennady Pundikov Date: Tue, 24 Jun 2025 14:39:12 +0300 Subject: [PATCH 3/3] Fix after merge --- .../DOM/Serialization/AsyncApiSerializer.cs | 44 +++++++++++++++---- .../FunctionalTests.cs | 2 +- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSerializer.cs b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSerializer.cs index 081f69f..49ef470 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSerializer.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/Serialization/AsyncApiSerializer.cs @@ -1,6 +1,8 @@ +using System.Runtime.CompilerServices; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; +using NJsonSchema.CodeGeneration; using NJsonSchema.Generation; using NJsonSchema.Yaml; using YamlDotNet.Serialization; @@ -64,12 +66,12 @@ public static Task FromYamlAsync(string data) /// YAML text. /// Path to document. /// AsyncApi document object model. - public static Task FromYamlAsync(string data, string? documentPath) + public static async Task FromYamlAsync(string data, string? documentPath) { try { JObject jObject = ParseYaml(data); - return FromJObject(jObject, documentPath); + return await FromJObject(jObject, documentPath); } catch (YamlException yamlEx) { @@ -94,7 +96,7 @@ private static JObject ParseYaml(string data) return JObject.FromObject(yamlDocument)!; } - private static Task FromJObject(JObject jObject, string? documentPath) + private static async Task FromJObject(JObject jObject, string? documentPath) { var version = GetDocumentVersion(jObject); var majorVersion = new string(version.TakeWhile(x => x != '.').ToArray()); @@ -106,7 +108,9 @@ private static Task FromJObject(JObject jObject, string? docum _ => throw new AsyncApiSerializationException($"Version '{version}' not supported."), }; doc.DocumentPath = documentPath; - return UpdateSchemaReferencesAsync(doc, serializer.ContractResolver); + await UpdateSchemaReferencesAsync(doc, serializer.ContractResolver); + BuildAsyncApiDescriminatorMapping(doc); + return doc; } private static string GetDocumentVersion(JObject jObject) @@ -128,14 +132,38 @@ private static AsyncApiDocument DeserializeV3(JObject jObject, JsonSerializerSet return serializer.Deserialize(jObject.CreateReader())!; } - private static async Task UpdateSchemaReferencesAsync(AsyncApiDocument document, IContractResolver contractResolver) - { - await new AsyncApiReferenceUpdater( + private static async Task UpdateSchemaReferencesAsync(AsyncApiDocument document, IContractResolver contractResolver) + => await new AsyncApiReferenceUpdater( document, new JsonAndYamlReferenceResolver(new AsyncApiSchemaResolver(document, new SystemTextJsonSchemaGeneratorSettings())), contractResolver) .VisitAsync(document, default) .ConfigureAwait(false); - return document; + + private static void BuildAsyncApiDescriminatorMapping(AsyncApiDocument document) + { + foreach (var schema in document.Components?.Schemas?.Values ?? []) + { + if (schema.SchemaFormat.StartsWith(AsyncApiSchema.AsyncApi)) + { + var discriminatorPropName = schema.DiscriminatorObject?.PropertyName; + if (discriminatorPropName != null) + { + var derivedSchemas = schema.GetDerivedSchemas(document); + foreach (var item in derivedSchemas) + { + var derivedSchema = item.Key; + if ((derivedSchema.Properties.TryGetValue(discriminatorPropName, out var discriminatorProp) + || derivedSchema.AllOf?.FirstOrDefault(i => i != schema && i.Properties.ContainsKey(discriminatorPropName))?.Properties.TryGetValue(discriminatorPropName, out discriminatorProp) == true) + && discriminatorProp.ExtensionData?.TryGetValue("const", out var constValue) == true) + { + var constValueStr = constValue!.ToString(); + discriminatorProp.ParentSchema!.Properties.Remove(discriminatorPropName); + schema.DiscriminatorObject!.Mapping.Add(constValueStr, derivedSchema); + } + } + } + } + } } } diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/FunctionalTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/FunctionalTests.cs index 25598bf..a854aab 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/FunctionalTests.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/FunctionalTests.cs @@ -172,7 +172,7 @@ public async Task GenerateMultipleClients() public async Task GenerateDiscriminator() { var yaml = """ - asyncapi: 2.0 + asyncapi: 3.0.0 info: { title: 'dd', version: '1.0' } components: schemas: