From f0c2bf5d8ffb0e6139be1c99e2170b1355bb15d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=AE=D1=80=D0=B8=D0=B9=20=D0=A2=D1=83=D1=80=D0=B1=D0=B8?= =?UTF-8?q?=D0=BD?= Date: Mon, 29 Jul 2024 17:50:33 +0300 Subject: [PATCH] Translation to component ref --- .dockerignore | 30 +++ .../AttributeDocumentProvider.cs | 242 ++++++++++++------ .../DocumentMiddleware/AsyncApiMiddleware.cs | 6 +- src/Saunter/Options/AsyncApiOptions.cs | 3 +- .../Options/Filters/IOperationFilter.cs | 1 + .../Options/Filters/OperationFilterContext.cs | 17 +- .../AsyncApiDocumentSerializeCloner.cs | 2 + .../SharedKernel/AsyncApiSchemaGenerator.cs | 63 +++-- .../Interfaces/IAsyncApiSchemaGenerator.cs | 5 +- .../Dockerfile | 28 +- .../Program.cs | 5 +- .../README.md | 7 +- ...unter.IntegrationTests.ReverseProxy.csproj | 7 + .../docker-compose.yml | 9 +- .../AssertAsyncApiDocumentHelper.cs | 35 +++ .../ClassAttributesTests.cs | 109 ++++---- .../InterfaceAttributeTests.cs | 10 +- .../MethodAttributesTests.cs | 13 +- .../AttributeProvider/OperationTraitsTests.cs | 1 + .../SharedKernel/SchemaGeneratorTests.cs | 59 +++-- 20 files changed, 434 insertions(+), 218 deletions(-) create mode 100644 .dockerignore create mode 100644 test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/AssertAsyncApiDocumentHelper.cs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..fe1152bd --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +!**/.gitignore +!.git/HEAD +!.git/config +!.git/packed-refs +!.git/refs/heads/** \ No newline at end of file diff --git a/src/Saunter/AttributeProvider/AttributeDocumentProvider.cs b/src/Saunter/AttributeProvider/AttributeDocumentProvider.cs index ffd463a6..fbe7dbb1 100644 --- a/src/Saunter/AttributeProvider/AttributeDocumentProvider.cs +++ b/src/Saunter/AttributeProvider/AttributeDocumentProvider.cs @@ -34,15 +34,30 @@ public AsyncApiDocument GetDocument(string? documentName, AsyncApiOptions option throw new ArgumentNullException(nameof(options)); } - var apiNamePair = options.NamedApis - .FirstOrDefault(c => c.Value.Id == documentName); + var asyncApiTypes = GetAsyncApiTypes(options, documentName); - var asyncApiTypes = GetAsyncApiTypes(options, apiNamePair.Key); + var apiNamePair = options.NamedApis.FirstOrDefault(c => c.Value.Id == documentName); var clone = _cloner.CloneProtype(apiNamePair.Value ?? options.AsyncApi); - GenerateChannelsFromMethods(clone, options, asyncApiTypes); - GenerateChannelsFromClasses(clone, options, asyncApiTypes); + if (string.IsNullOrWhiteSpace(clone.DefaultContentType)) + { + clone.DefaultContentType = "application/json"; + } + + var channelItems = Enumerable.Concat( + GenerateChannelsFromMethods(clone.Components, options, asyncApiTypes), + GenerateChannelsFromClasses(clone.Components, options, asyncApiTypes)); + + foreach (var item in channelItems) + { + if (!clone.Channels.TryAdd(item.Key, item.Value)) + { + clone.Channels[item.Key] = _channelUnion.Union( + clone.Channels[item.Key], + item.Value); + } + } var filterContext = new DocumentFilterContext(asyncApiTypes); @@ -55,7 +70,7 @@ public AsyncApiDocument GetDocument(string? documentName, AsyncApiOptions option return clone; } - private void GenerateChannelsFromMethods(AsyncApiDocument document, AsyncApiOptions options, TypeInfo[] asyncApiTypes) + private IEnumerable> GenerateChannelsFromMethods(AsyncApiComponents components, AsyncApiOptions options, TypeInfo[] asyncApiTypes) { var methodsWithChannelAttribute = asyncApiTypes .SelectMany(type => type.DeclaredMethods) @@ -77,9 +92,9 @@ private void GenerateChannelsFromMethods(AsyncApiDocument document, AsyncApiOpti { Servers = item.Channel.Servers?.ToList(), Description = item.Channel.Description, - Parameters = GetChannelParametersFromAttributes(item.Method), - Publish = GenerateOperationFromMethod(item.Method, OperationType.Publish, options), - Subscribe = GenerateOperationFromMethod(item.Method, OperationType.Subscribe, options), + Parameters = GetChannelParametersFromAttributes(components, item.Method), + Publish = GenerateOperationFromMethod(components, item.Method, OperationType.Publish, options), + Subscribe = GenerateOperationFromMethod(components, item.Method, OperationType.Subscribe, options), Bindings = item.Channel.BindingsRef != null ? new() { @@ -92,18 +107,13 @@ private void GenerateChannelsFromMethods(AsyncApiDocument document, AsyncApiOpti : null, }; - if (!document.Channels.TryAdd(item.Channel.Name, channelItem)) - { - document.Channels[item.Channel.Name] = _channelUnion.Union( - document.Channels[item.Channel.Name], - channelItem); - } - ApplyChannelFilters(options, item.Method, item.Channel, channelItem); + + yield return new(item.Channel.Name, channelItem); } } - private void GenerateChannelsFromClasses(AsyncApiDocument document, AsyncApiOptions options, TypeInfo[] asyncApiTypes) + private IEnumerable> GenerateChannelsFromClasses(AsyncApiComponents components, AsyncApiOptions options, TypeInfo[] asyncApiTypes) { var classesWithChannelAttribute = asyncApiTypes .Select(type => new @@ -123,9 +133,9 @@ private void GenerateChannelsFromClasses(AsyncApiDocument document, AsyncApiOpti var channelItem = new AsyncApiChannel { Description = item.Channel.Description, - Parameters = GetChannelParametersFromAttributes(item.Type), - Publish = GenerateOperationFromClass(item.Type, OperationType.Publish), - Subscribe = GenerateOperationFromClass(item.Type, OperationType.Subscribe), + Parameters = GetChannelParametersFromAttributes(components, item.Type), + Publish = GenerateOperationFromClass(components, item.Type, OperationType.Publish), + Subscribe = GenerateOperationFromClass(components, item.Type, OperationType.Subscribe), Servers = item.Channel.Servers?.ToList(), Bindings = item.Channel.BindingsRef != null ? new() @@ -139,14 +149,9 @@ private void GenerateChannelsFromClasses(AsyncApiDocument document, AsyncApiOpti : null, }; - if (!document.Channels.TryAdd(item.Channel.Name, channelItem)) - { - document.Channels[item.Channel.Name] = _channelUnion.Union( - document.Channels[item.Channel.Name], - channelItem); - } - ApplyChannelFilters(options, item.Type, item.Channel, channelItem); + + yield return new(item.Channel.Name, channelItem); } } @@ -161,30 +166,43 @@ private void ApplyChannelFilters(AsyncApiOptions options, MemberInfo member, Cha } } - private IDictionary GetChannelParametersFromAttributes(MemberInfo memberInfo) + private IDictionary GetChannelParametersFromAttributes(AsyncApiComponents components, MemberInfo memberInfo) { var attributes = memberInfo.GetCustomAttributes(); - var parameters = new Dictionary(); + var parameters = new Dictionary(attributes.Count()); - if (attributes.Any()) + foreach (var attribute in attributes) { - foreach (var attribute in attributes) + var parameterId = attribute.Name; + + if (!components.Parameters.ContainsKey(parameterId)) { + var schema = GetAsyncApiSchema(components, (TypeInfo?)attribute.Type); + var parameter = new AsyncApiParameter { Description = attribute.Description, Location = attribute.Location, - Schema = _schemaGenerator.Generate(attribute.Type), + Schema = schema, }; - parameters.Add(attribute.Name, parameter); + components.Parameters.Add(parameterId, parameter); } + + parameters.Add(parameterId, new() + { + Reference = new() + { + Id = parameterId, + Type = ReferenceType.Parameter, + } + }); } return parameters; } - private AsyncApiOperation? GenerateOperationFromMethod(MethodInfo method, OperationType operationType, AsyncApiOptions options) + private AsyncApiOperation? GenerateOperationFromMethod(AsyncApiComponents components, MethodInfo method, OperationType operationType, AsyncApiOptions options) { var operationAttribute = GetOperationAttribute(method, operationType); @@ -228,24 +246,19 @@ private IDictionary GetChannelParametersFromAttribute if (messageAttributes.Any()) { - operation.Message = GenerateMessageFromAttributes(messageAttributes); + operation.Message = GenerateMessageFromAttributes(components, messageAttributes); } else if (operationAttribute.MessagePayloadType is not null) { - operation.Message = GenerateMessageFromType(operationAttribute.MessagePayloadType.GetTypeInfo()); + operation.Message = GenerateMessageFromType(components, operationAttribute.MessagePayloadType.GetTypeInfo()); } - var filterContext = new OperationFilterContext(method, operationAttribute); - foreach (var filterType in options.OperationFilters) - { - var filter = (IOperationFilter)_serviceProvider.GetRequiredService(filterType); - filter?.Apply(operation, filterContext); - } + ApplyOprationFilters(method, options, operationAttribute, operation); return operation; } - private AsyncApiOperation? GenerateOperationFromClass(TypeInfo type, OperationType operationType) + private AsyncApiOperation? GenerateOperationFromClass(AsyncApiComponents components, TypeInfo type, OperationType operationType) { var operationAttribute = GetOperationAttribute(type, operationType); @@ -292,7 +305,7 @@ private IDictionary GetChannelParametersFromAttribute foreach (var attribute in attributes) { - var message = GenerateMessageFromAttribute(attribute); + var message = GenerateMessageFromAttribute(components, attribute); if (message != null) { @@ -313,13 +326,24 @@ private IDictionary GetChannelParametersFromAttribute }; } - private List GenerateMessageFromAttributes(IEnumerable messageAttributes) + private void ApplyOprationFilters(MethodInfo method, AsyncApiOptions options, OperationAttribute operationAttribute, AsyncApiOperation operation) + { + var filterContext = new OperationFilterContext(method, operationAttribute); + + foreach (var filterType in options.OperationFilters) + { + var filter = (IOperationFilter)_serviceProvider.GetRequiredService(filterType); + filter?.Apply(operation, filterContext); + } + } + + private List GenerateMessageFromAttributes(AsyncApiComponents components, IEnumerable messageAttributes) { List messages = new(); if (messageAttributes.Count() == 1) { - var message = GenerateMessageFromAttribute(messageAttributes.First()); + var message = GenerateMessageFromAttribute(components, messageAttributes.First()); if (message is not null) { @@ -331,7 +355,7 @@ private List GenerateMessageFromAttributes(IEnumerable GenerateMessageFromAttributes(IEnumerable GenerateMessageFromType(TypeInfo payloadType) + private List GenerateMessageFromType(AsyncApiComponents components, TypeInfo payloadType) { - var message = new AsyncApiMessage + var asyncApiSchema = GetAsyncApiSchema(components, payloadType); + + var messageId = asyncApiSchema?.Title; + + if (messageId is null) { - Payload = _schemaGenerator.Generate(payloadType), - }; + return new(); + } - message.MessageId = message.Payload?.Title; - message.Name = message.Payload?.Title; + if (!components.Messages.ContainsKey(messageId)) + { + var message = new AsyncApiMessage + { + Payload = asyncApiSchema, + MessageId = messageId, + Name = messageId, + Title = messageId, + }; - return new() { message }; + components.Messages.Add(messageId, message); + } + + return new() + { + new() + { + Reference = new() + { + Id = messageId, + Type = ReferenceType.Message, + } + } + }; } - private AsyncApiMessage? GenerateMessageFromAttribute(MessageAttribute messageAttribute) + private AsyncApiMessage? GenerateMessageFromAttribute(AsyncApiComponents components, MessageAttribute messageAttribute) { if (messageAttribute?.PayloadType == null) { return null; } - var tags = messageAttribute.Tags? - .Select(x => new AsyncApiTag { Name = x }) - .ToList() ?? new List(); + var bodySchema = GetAsyncApiSchema(components, (TypeInfo)messageAttribute.PayloadType); - var asyncApiSchema = _schemaGenerator.Generate(messageAttribute.PayloadType); + var messageId = messageAttribute.MessageId ?? bodySchema?.Title ?? messageAttribute.PayloadType.Name; - var message = new AsyncApiMessage + if (!components.Messages.ContainsKey(messageId)) { - MessageId = messageAttribute.MessageId ?? asyncApiSchema?.Title ?? messageAttribute.PayloadType.Name, - Title = messageAttribute.Title, - Summary = messageAttribute.Summary, - Description = messageAttribute.Description, - Tags = tags, - Payload = asyncApiSchema, - Headers = _schemaGenerator.Generate(messageAttribute.HeadersType), - Name = messageAttribute.Name ?? asyncApiSchema?.Title ?? messageAttribute.PayloadType.Name, - Bindings = new() + var tags = messageAttribute.Tags? + .Select(x => new AsyncApiTag { Name = x }) + .ToList() ?? new List(); + + var headersSchema = GetAsyncApiSchema(components, (TypeInfo?)messageAttribute.HeadersType); + + var message = new AsyncApiMessage { - Reference = new() + MessageId = messageId, + Title = messageAttribute.Title ?? bodySchema?.Title ?? messageAttribute.PayloadType.Name, + Name = messageAttribute.Name ?? bodySchema?.Title ?? messageAttribute.PayloadType.Name, + Summary = messageAttribute.Summary, + Description = messageAttribute.Description, + Tags = tags, + Payload = bodySchema, + Headers = headersSchema, + Bindings = new() { - Id = messageAttribute.BindingsRef, - Type = ReferenceType.MessageBindings, + Reference = new() + { + Id = messageAttribute.BindingsRef, + Type = ReferenceType.MessageBindings, + }, }, - }, + }; + + components.Messages.Add(message.MessageId, message); + } + + return new() + { + Reference = new() + { + Id = messageId, + Type = ReferenceType.Message, + } }; + } + + private AsyncApiSchema? GetAsyncApiSchema(AsyncApiComponents components, TypeInfo? payloadType) + { + var generatedSchemas = _schemaGenerator.Generate(payloadType); + + if (generatedSchemas is null) + { + return null; + } + + foreach (var asyncApiSchema in generatedSchemas.Value.All) + { + var key = asyncApiSchema.Title; - return message; + if (!components.Schemas.ContainsKey(key)) + { + components.Schemas[key] = asyncApiSchema; + } + } + + return new() + { + Title = generatedSchemas.Value.Root.Title, + Reference = new() + { + Id = generatedSchemas.Value.Root.Title, + Type = ReferenceType.Schema, + } + }; } private static TypeInfo[] GetAsyncApiTypes(AsyncApiOptions options, string? apiName) diff --git a/src/Saunter/DocumentMiddleware/AsyncApiMiddleware.cs b/src/Saunter/DocumentMiddleware/AsyncApiMiddleware.cs index 3d0dc25e..4e14d15d 100644 --- a/src/Saunter/DocumentMiddleware/AsyncApiMiddleware.cs +++ b/src/Saunter/DocumentMiddleware/AsyncApiMiddleware.cs @@ -1,6 +1,10 @@ -using System.Net; +using System.Globalization; +using System.IO; +using System.Net; using System.Threading.Tasks; +using LEGO.AsyncAPI; using LEGO.AsyncAPI.Models; +using LEGO.AsyncAPI.Writers; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using Saunter.Options; diff --git a/src/Saunter/Options/AsyncApiOptions.cs b/src/Saunter/Options/AsyncApiOptions.cs index 385bb665..ad0601a1 100644 --- a/src/Saunter/Options/AsyncApiOptions.cs +++ b/src/Saunter/Options/AsyncApiOptions.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using LEGO.AsyncAPI.Models; +using Microsoft.Extensions.Options; using Saunter.Options.Filters; namespace Saunter.Options @@ -77,6 +78,6 @@ public void AddOperationFilter() where T : IOperationFilter /// public AsyncApiMiddlewareOptions Middleware { get; } = new AsyncApiMiddlewareOptions(); - public ConcurrentDictionary NamedApis { get; set; } = new(); + public ConcurrentDictionary NamedApis { get; private set; } = new(); } } diff --git a/src/Saunter/Options/Filters/IOperationFilter.cs b/src/Saunter/Options/Filters/IOperationFilter.cs index cb8acd3a..46f7ad78 100644 --- a/src/Saunter/Options/Filters/IOperationFilter.cs +++ b/src/Saunter/Options/Filters/IOperationFilter.cs @@ -1,4 +1,5 @@ using LEGO.AsyncAPI.Models; +using Saunter.Options.Filters; public interface IOperationFilter { diff --git a/src/Saunter/Options/Filters/OperationFilterContext.cs b/src/Saunter/Options/Filters/OperationFilterContext.cs index 9d530cce..59a8f1db 100644 --- a/src/Saunter/Options/Filters/OperationFilterContext.cs +++ b/src/Saunter/Options/Filters/OperationFilterContext.cs @@ -1,15 +1,18 @@ using System.Reflection; using Saunter.AttributeProvider.Attributes; -public class OperationFilterContext +namespace Saunter.Options.Filters { - public OperationFilterContext(MethodInfo method, OperationAttribute operation) + public class OperationFilterContext { - Method = method; - Operation = operation; - } + public OperationFilterContext(MethodInfo method, OperationAttribute operation) + { + Method = method; + Operation = operation; + } - public MethodInfo Method { get; } + public MethodInfo Method { get; } - public OperationAttribute Operation { get; } + public OperationAttribute Operation { get; } + } } diff --git a/src/Saunter/SharedKernel/AsyncApiDocumentSerializeCloner.cs b/src/Saunter/SharedKernel/AsyncApiDocumentSerializeCloner.cs index f78a3f8e..45d0fd64 100644 --- a/src/Saunter/SharedKernel/AsyncApiDocumentSerializeCloner.cs +++ b/src/Saunter/SharedKernel/AsyncApiDocumentSerializeCloner.cs @@ -40,6 +40,8 @@ public AsyncApiDocument CloneProtype(AsyncApiDocument prototype) } } + cloned.Components ??= new(); + return cloned; } } diff --git a/src/Saunter/SharedKernel/AsyncApiSchemaGenerator.cs b/src/Saunter/SharedKernel/AsyncApiSchemaGenerator.cs index 8aa768ad..0d234cb4 100644 --- a/src/Saunter/SharedKernel/AsyncApiSchemaGenerator.cs +++ b/src/Saunter/SharedKernel/AsyncApiSchemaGenerator.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using LEGO.AsyncAPI.Bindings; using LEGO.AsyncAPI.Models; using Saunter.SharedKernel.Interfaces; @@ -9,12 +10,12 @@ namespace Saunter.SharedKernel { internal class AsyncApiSchemaGenerator : IAsyncApiSchemaGenerator { - public AsyncApiSchema? Generate(Type? type) + public GeneratedSchemas? Generate(Type? type) { return GenerateBranch(type, new()); } - private static AsyncApiSchema? GenerateBranch(Type? type, HashSet parents) + private static GeneratedSchemas? GenerateBranch(Type? type, HashSet parents) { if (type is null) { @@ -28,6 +29,11 @@ internal class AsyncApiSchemaGenerator : IAsyncApiSchemaGenerator Nullable = !typeInfo.IsValueType, }; + var nestedShemas = new List() + { + schema + }; + if (typeInfo.IsGenericType) { var nullableType = typeof(Nullable<>).MakeGenericType(typeInfo.GenericTypeArguments); @@ -41,17 +47,6 @@ internal class AsyncApiSchemaGenerator : IAsyncApiSchemaGenerator var name = ToNameCase(typeInfo.Name); - if (!parents.Add(type)) - { - schema.Reference = new() - { - Id = name, - Type = ReferenceType.Schema, - }; - - return schema; - } - schema.Title = name; schema.Type = MapJsonTypeToSchemaType(typeInfo); @@ -70,17 +65,45 @@ internal class AsyncApiSchemaGenerator : IAsyncApiSchemaGenerator schema.Format = schema.Title; } - return schema; + return new(schema, nestedShemas); } - schema.Properties = typeInfo + if (!parents.Add(type)) + { + schema = new() + { + Title = name, + Reference = new() + { + Id = name, + Type = ReferenceType.Schema, + } + }; + + // No new types have been created, so empty + return new(schema, Array.Empty()); + } + + var properties = typeInfo .DeclaredProperties - .Where(p => p.GetMethod is not null && !p.GetMethod.IsStatic) - .ToDictionary( - prop => ToNameCase(prop.Name), - prop => GenerateBranch(prop.PropertyType.GetTypeInfo(), parents.ToHashSet())); + .Where(p => p.GetMethod is not null && !p.GetMethod.IsStatic); + + foreach (var prop in properties) + { + var generatedSchemas = GenerateBranch(prop.PropertyType.GetTypeInfo(), parents); + if (generatedSchemas is null) + { + continue; + } + + var key = ToNameCase(prop.Name); + + schema.Properties[key] = generatedSchemas.Value.Root; + + nestedShemas.AddRange(generatedSchemas.Value.All); + } - return schema; + return new(schema, nestedShemas.DistinctBy(n => n.Title).ToArray()); } private static string ToNameCase(string name) diff --git a/src/Saunter/SharedKernel/Interfaces/IAsyncApiSchemaGenerator.cs b/src/Saunter/SharedKernel/Interfaces/IAsyncApiSchemaGenerator.cs index 8dcea0d7..e2a9d8d7 100644 --- a/src/Saunter/SharedKernel/Interfaces/IAsyncApiSchemaGenerator.cs +++ b/src/Saunter/SharedKernel/Interfaces/IAsyncApiSchemaGenerator.cs @@ -1,10 +1,13 @@ using System; +using System.Collections.Generic; using LEGO.AsyncAPI.Models; namespace Saunter.SharedKernel.Interfaces { public interface IAsyncApiSchemaGenerator { - AsyncApiSchema? Generate(Type? type); + GeneratedSchemas? Generate(Type? type); } + + public readonly record struct GeneratedSchemas(AsyncApiSchema Root, IReadOnlyCollection All); } diff --git a/test/Saunter.IntegrationTests.ReverseProxy/Dockerfile b/test/Saunter.IntegrationTests.ReverseProxy/Dockerfile index 82f40963..a80ef2c5 100644 --- a/test/Saunter.IntegrationTests.ReverseProxy/Dockerfile +++ b/test/Saunter.IntegrationTests.ReverseProxy/Dockerfile @@ -1,7 +1,23 @@ -FROM mcr.microsoft.com/dotnet/aspnet:6.0 -# Run "dotnet publish -c Release" before building this image -COPY bin/Release/net6.0/publish/ App/ +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 -WORKDIR /App -EXPOSE 5000 -ENTRYPOINT ["dotnet", "Saunter.IntegrationTests.ReverseProxy.dll"] +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["test/Saunter.IntegrationTests.ReverseProxy/Saunter.IntegrationTests.ReverseProxy.csproj", "test/Saunter.IntegrationTests.ReverseProxy/"] +COPY ["src/Saunter/Saunter.csproj", "src/Saunter/"] +RUN dotnet restore "./test/Saunter.IntegrationTests.ReverseProxy/Saunter.IntegrationTests.ReverseProxy.csproj" +COPY . . +WORKDIR "/src/test/Saunter.IntegrationTests.ReverseProxy" +RUN dotnet build "./Saunter.IntegrationTests.ReverseProxy.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./Saunter.IntegrationTests.ReverseProxy.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Saunter.IntegrationTests.ReverseProxy.dll"] \ No newline at end of file diff --git a/test/Saunter.IntegrationTests.ReverseProxy/Program.cs b/test/Saunter.IntegrationTests.ReverseProxy/Program.cs index c844b75e..4fc843bb 100644 --- a/test/Saunter.IntegrationTests.ReverseProxy/Program.cs +++ b/test/Saunter.IntegrationTests.ReverseProxy/Program.cs @@ -53,8 +53,8 @@ public void ConfigureServices(IServiceCollection services) Info = new AsyncApiInfo { Title = Environment.GetEnvironmentVariable("PATH_BASE"), - Version = "1.0.0" - } + Version = "1.0.0", + }, }; }); @@ -93,7 +93,6 @@ public void Configure(IApplicationBuilder app) } } - public class LightMeasuredEvent { public int Id { get; set; } diff --git a/test/Saunter.IntegrationTests.ReverseProxy/README.md b/test/Saunter.IntegrationTests.ReverseProxy/README.md index c0853da5..614647fe 100644 --- a/test/Saunter.IntegrationTests.ReverseProxy/README.md +++ b/test/Saunter.IntegrationTests.ReverseProxy/README.md @@ -5,11 +5,10 @@ The [docker-compose.yml](./docker-compose.yml) file sets up 3 containers 2. service-b 3. nginx reverse proxy +Running the test (from root project location): -Running the test: -``` -$ dotnet publish -c Release -$ docker-compose up +```bash +docker-compose --file ./test/Saunter.IntegrationTests.ReverseProxy/docker-compose.yml up --build ``` You should be able to access both services UI diff --git a/test/Saunter.IntegrationTests.ReverseProxy/Saunter.IntegrationTests.ReverseProxy.csproj b/test/Saunter.IntegrationTests.ReverseProxy/Saunter.IntegrationTests.ReverseProxy.csproj index a152bfc2..a13411f9 100644 --- a/test/Saunter.IntegrationTests.ReverseProxy/Saunter.IntegrationTests.ReverseProxy.csproj +++ b/test/Saunter.IntegrationTests.ReverseProxy/Saunter.IntegrationTests.ReverseProxy.csproj @@ -3,8 +3,15 @@ Exe net6.0 + cf516cef-fd3c-4a50-b81e-b2b390ecc9f7 + Linux + ..\.. + + + + diff --git a/test/Saunter.IntegrationTests.ReverseProxy/docker-compose.yml b/test/Saunter.IntegrationTests.ReverseProxy/docker-compose.yml index 696e27e5..d1ec3682 100644 --- a/test/Saunter.IntegrationTests.ReverseProxy/docker-compose.yml +++ b/test/Saunter.IntegrationTests.ReverseProxy/docker-compose.yml @@ -1,17 +1,16 @@ version: '3.7' services: - service-a: + service-a: &service build: - context: . + context: ../.. + dockerfile: test/Saunter.IntegrationTests.ReverseProxy/Dockerfile restart: always environment: - PATH_BASE=/service-a service-b: - build: - context: . - restart: always + <<: *service environment: - PATH_BASE=/service-b diff --git a/test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/AssertAsyncApiDocumentHelper.cs b/test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/AssertAsyncApiDocumentHelper.cs new file mode 100644 index 00000000..2549d432 --- /dev/null +++ b/test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/AssertAsyncApiDocumentHelper.cs @@ -0,0 +1,35 @@ +using LEGO.AsyncAPI.Models; +using Shouldly; + +namespace Saunter.Tests.AttributeProvider.DocumentGenerationTests +{ + internal static class AssertAsyncApiDocumentHelper + { + public static AsyncApiChannel AssertAndGetChannel(this AsyncApiDocument document, string key, string description) + { + document.Channels.Count.ShouldBe(1); + document.Channels.ShouldContainKey(key); + + var channel = document.Channels[key]; + channel.ShouldNotBeNull(); + channel.Description.ShouldBe(description); + + return channel; + } + + public static void AssertByMessage(this AsyncApiDocument document, AsyncApiOperation operation, params string[] messageIds) + { + operation.Message.Count.ShouldBe(messageIds.Length); + operation.Message.ShouldAllBe(c => c.Reference.Type == ReferenceType.Message); + + foreach (var messageId in messageIds) + { + operation.Message.ShouldContain(m => m.Reference.Id == messageId); + document.Components.Messages.ShouldContainKey(messageId); + + var message = document.Components.Messages[messageId]; + document.Components.Schemas.ContainsKey(message.Payload.Reference.Id); + } + } + } +} diff --git a/test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/ClassAttributesTests.cs b/test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/ClassAttributesTests.cs index 897134a8..09a307ef 100644 --- a/test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/ClassAttributesTests.cs +++ b/test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/ClassAttributesTests.cs @@ -1,9 +1,11 @@ using System; using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Reflection.Metadata; using Saunter.AttributeProvider.Attributes; using Shouldly; using Xunit; +using YamlDotNet.Core.Tokens; namespace Saunter.Tests.AttributeProvider.DocumentGenerationTests { @@ -16,28 +18,21 @@ public void GetDocument_GeneratesDocumentWithMultipleMessagesPerChannel(Type typ { // Arrange ArrangeAttributesTests.Arrange(out var options, out var documentProvider, type); + const string Key = "asw.tenant_service.tenants_history"; // Act var document = documentProvider.GetDocument(null, options); // Assert document.ShouldNotBeNull(); - document.Channels.Count.ShouldBe(1); - - var channel = document.Channels.First(); - channel.Key.ShouldBe("asw.tenant_service.tenants_history"); - channel.Value.Description.ShouldBe("Tenant events."); + var channel = document.AssertAndGetChannel(Key, "Tenant events."); - var subscribe = channel.Value.Subscribe; + var subscribe = channel.Subscribe; subscribe.ShouldNotBeNull(); subscribe.OperationId.ShouldBe("TenantMessageConsumer"); subscribe.Summary.ShouldBe("Subscribe to domains events about tenants."); - subscribe.Message.Count.ShouldBe(3); - - subscribe.Message.ShouldContain(m => m.MessageId == "tenantUpdated"); - subscribe.Message.ShouldContain(m => m.MessageId == "tenantCreated"); - subscribe.Message.ShouldContain(m => m.MessageId == "tenantRemoved"); + document.AssertByMessage(subscribe, "tenantCreated", "tenantUpdated", "tenantRemoved"); } [Theory] @@ -47,28 +42,22 @@ public void GenerateDocument_GeneratesDocumentWithMultipleMessagesPerChannelInTh { // Arrange ArrangeAttributesTests.Arrange(out var options, out var documentProvider, type); + const string Key = "asw.tenant_service.tenants_history"; // Act var document = documentProvider.GetDocument(null, options); // Assert document.ShouldNotBeNull(); - document.Channels.Count.ShouldBe(1); - var channel = document.Channels.First(); - channel.Key.ShouldBe("asw.tenant_service.tenants_history"); - channel.Value.Description.ShouldBe("Tenant events."); + var channel = document.AssertAndGetChannel(Key, "Tenant events."); - var publish = channel.Value.Publish; + var publish = channel.Publish; publish.ShouldNotBeNull(); publish.OperationId.ShouldBe("TenantMessagePublisher"); publish.Summary.ShouldBe("Publish domains events about tenants."); - publish.Message.Count.ShouldBe(3); - - publish.Message.ShouldContain(m => m.MessageId == "anyTenantCreated"); - publish.Message.ShouldContain(m => m.MessageId == "anyTenantUpdated"); - publish.Message.ShouldContain(m => m.MessageId == "anyTenantRemoved"); + document.AssertByMessage(publish, "anyTenantCreated", "anyTenantUpdated", "anyTenantRemoved"); } [Theory] @@ -78,25 +67,22 @@ public void GenerateDocument_GeneratesDocumentWithSingleMessage(Type type) { // Arrange ArrangeAttributesTests.Arrange(out var options, out var documentProvider, type); + const string Key = "asw.tenant_service.tenants_history"; // Act var document = documentProvider.GetDocument(null, options); // Assert document.ShouldNotBeNull(); - document.Channels.Count.ShouldBe(1); - var channel = document.Channels.First(); - channel.Key.ShouldBe("asw.tenant_service.tenants_history"); - channel.Value.Description.ShouldBe("Tenant events."); + var channel = document.AssertAndGetChannel(Key, "Tenant events."); - var publish = channel.Value.Publish; + var publish = channel.Publish; publish.ShouldNotBeNull(); publish.OperationId.ShouldBe("TenantSingleMessagePublisher"); publish.Summary.ShouldBe("Publish single domain event about tenants."); - publish.Message.Count.ShouldBe(1); - publish.Message[0].MessageId.ShouldBe("anyTenantCreated"); + document.AssertByMessage(publish, "anyTenantCreated"); } @@ -107,39 +93,28 @@ public void GetDocument_WhenMultipleClassesUseSameChannelKey_GeneratesDocumentWi { // Arrange ArrangeAttributesTests.Arrange(out var options, out var documentProvider, type1, type2); + const string Key = "asw.tenant_service.tenants_history"; // Act var document = documentProvider.GetDocument(null, options); // Assert document.ShouldNotBeNull(); - document.Channels.Count.ShouldBe(1); - var channel = document.Channels.First(); - channel.Key.ShouldBe("asw.tenant_service.tenants_history"); - channel.Value.Description.ShouldBe("Tenant events."); + var channel = document.AssertAndGetChannel(Key, "Tenant events."); - var subscribe = channel.Value.Subscribe; + var subscribe = channel.Subscribe; subscribe.ShouldNotBeNull(); subscribe.OperationId.ShouldBe("TenantMessageConsumer"); subscribe.Summary.ShouldBe("Subscribe to domains events about tenants."); - var publish = channel.Value.Publish; + var publish = channel.Publish; publish.ShouldNotBeNull(); publish.OperationId.ShouldBe("TenantMessagePublisher"); publish.Summary.ShouldBe("Publish domains events about tenants."); - subscribe.Message.Count.ShouldBe(3); - - subscribe.Message.ShouldContain(m => m.MessageId == "tenantCreated"); - subscribe.Message.ShouldContain(m => m.MessageId == "tenantUpdated"); - subscribe.Message.ShouldContain(m => m.MessageId == "tenantRemoved"); - - publish.Message.Count.ShouldBe(3); - - publish.Message.ShouldContain(m => m.MessageId == "tenantCreated"); - publish.Message.ShouldContain(m => m.MessageId == "tenantUpdated"); - publish.Message.ShouldContain(m => m.MessageId == "tenantRemoved"); + document.AssertByMessage(subscribe, "tenantCreated", "tenantUpdated", "tenantRemoved"); + document.AssertByMessage(publish, "tenantCreated", "tenantUpdated", "tenantRemoved"); } [Theory] @@ -149,6 +124,7 @@ public void GenerateDocument_GeneratesDocumentWithChannelParameters(Type type) { // Arrange ArrangeAttributesTests.Arrange(out var options, out var documentProvider, type); + const string Key = "asw.tenant_service.{tenant_id}.{tenant_status}"; // Act var document = documentProvider.GetDocument(null, options); @@ -156,25 +132,26 @@ public void GenerateDocument_GeneratesDocumentWithChannelParameters(Type type) // Assert document.ShouldNotBeNull(); document.Channels.Count.ShouldBe(1); + document.Channels.ShouldContainKey(Key); - var channel = document.Channels.First(); - channel.Key.ShouldBe("asw.tenant_service.{tenant_id}.{tenant_status}"); - channel.Value.Description.ShouldBe("A tenant events."); + var channel = document.Channels[Key]; - channel.Value.Parameters.Count.ShouldBe(2); - channel.Value.Parameters.ShouldContain(p => p.Key == "tenant_id" && p.Value.Schema != null && p.Value.Description == "The tenant identifier."); - channel.Value.Parameters.ShouldContain(p => p.Key == "tenant_status" && p.Value.Schema != null && p.Value.Description == "The tenant status."); + channel.Description.ShouldBe("A tenant events."); - var subscribe = channel.Value.Subscribe; + channel.Parameters.Count.ShouldBe(2); + channel.Parameters.ContainsKey("tenant_id"); + channel.Parameters.ContainsKey("tenant_status"); + + document.Components.Parameters.Count.ShouldBe(2); + document.Components.Parameters.ShouldContain(p => p.Key == "tenant_id" && p.Value.Schema != null && p.Value.Description == "The tenant identifier."); + document.Components.Parameters.ShouldContain(p => p.Key == "tenant_status" && p.Value.Schema != null && p.Value.Description == "The tenant status."); + + var subscribe = channel.Subscribe; subscribe.ShouldNotBeNull(); subscribe.OperationId.ShouldBe("OneTenantMessageConsumer"); subscribe.Summary.ShouldBe("Subscribe to domains events about a tenant."); - subscribe.Message.Count.ShouldBe(3); - - subscribe.Message.ShouldContain(m => m.MessageId == "tenantCreated"); - subscribe.Message.ShouldContain(m => m.MessageId == "tenantUpdated"); - subscribe.Message.ShouldContain(m => m.MessageId == "tenantRemoved"); + document.AssertByMessage(subscribe, "tenantCreated", "tenantUpdated", "tenantRemoved"); } [Theory] @@ -190,11 +167,23 @@ public void GenerateDocument_GeneratesDocumentWithMessageHeader(Type type) // Assert document.ShouldNotBeNull(); - document.Channels.Count.ShouldBe(1); + document.Channels.Count.ShouldBe(expected: 1); + + var channel = document.Channels.First().Value; + var messages = channel.Publish.Message; - var messages = document.Channels.First().Value.Publish.Message; messages.Count.ShouldBe(1); - messages[0].Headers.Title.ShouldBe("myMessageHeader"); + + var message = messages[0]; + + document.Components.Messages.ContainsKey(message.Reference.Id); + + var messageFromRef = document.Components.Messages[message.Reference.Id]; + + document.Components.Schemas.ContainsKey(messageFromRef.Payload.Reference.Id); + document.Components.Schemas.ContainsKey(messageFromRef.Headers.Reference.Id); + + document.Components.Schemas[messageFromRef.Headers.Reference.Id].Title.ShouldBe("myMessageHeader"); } [AsyncApi] diff --git a/test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/InterfaceAttributeTests.cs b/test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/InterfaceAttributeTests.cs index 668944e8..e758104b 100644 --- a/test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/InterfaceAttributeTests.cs +++ b/test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/InterfaceAttributeTests.cs @@ -41,19 +41,15 @@ public void AnnotatedTypesTest(Type type, string channelName) // Assert document.ShouldNotBeNull(); - document.Channels.Count.ShouldBe(1); - var channel = document.Channels.First(); - channel.Key.ShouldBe(channelName); - channel.Value.Description.ShouldBeNull(); + var channel = document.AssertAndGetChannel(channelName, null); - var publish = channel.Value.Publish; + var publish = channel.Publish; publish.ShouldNotBeNull(); publish.OperationId.ShouldBe("PublishEvent"); publish.Description.ShouldBe($"({channelName}) Subscribe to domains events about a tenant."); - publish.Message.Count.ShouldBe(1); - publish.Message[0].MessageId.ShouldBe("tenantEvent"); + document.AssertByMessage(publish, "tenantEvent"); } [AsyncApi] diff --git a/test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/MethodAttributesTests.cs b/test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/MethodAttributesTests.cs index 00cfdb06..1421dd76 100644 --- a/test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/MethodAttributesTests.cs +++ b/test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/MethodAttributesTests.cs @@ -19,22 +19,15 @@ public void GenerateDocument_GeneratesDocumentWithMultipleMessagesPerChannel() // Assert document.ShouldNotBeNull(); - document.Channels.Count.ShouldBe(1); - var channel = document.Channels.First(); - channel.Key.ShouldBe("asw.tenant_service.tenants_history"); - channel.Value.Description.ShouldBe("Tenant events."); + var channel = document.AssertAndGetChannel("asw.tenant_service.tenants_history", "Tenant events."); - var publish = channel.Value.Publish; + var publish = channel.Publish; publish.ShouldNotBeNull(); publish.OperationId.ShouldBe("TenantMessagePublisher"); publish.Summary.ShouldBe("Publish domains events about tenants."); - publish.Message.Count.ShouldBe(3); - - publish.Message.ShouldContain(m => m.MessageId == "anyTenantCreated"); - publish.Message.ShouldContain(m => m.MessageId == "anyTenantUpdated"); - publish.Message.ShouldContain(m => m.MessageId == "anyTenantRemoved"); + document.AssertByMessage(publish, "anyTenantCreated", "anyTenantUpdated", "anyTenantRemoved"); } [AsyncApi] diff --git a/test/Saunter.Tests/AttributeProvider/OperationTraitsTests.cs b/test/Saunter.Tests/AttributeProvider/OperationTraitsTests.cs index 93e9fd35..9ea6196f 100644 --- a/test/Saunter.Tests/AttributeProvider/OperationTraitsTests.cs +++ b/test/Saunter.Tests/AttributeProvider/OperationTraitsTests.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Saunter.Options; +using Saunter.Options.Filters; using Shouldly; using Xunit; diff --git a/test/Saunter.Tests/SharedKernel/SchemaGeneratorTests.cs b/test/Saunter.Tests/SharedKernel/SchemaGeneratorTests.cs index 31e89dbf..79268267 100644 --- a/test/Saunter.Tests/SharedKernel/SchemaGeneratorTests.cs +++ b/test/Saunter.Tests/SharedKernel/SchemaGeneratorTests.cs @@ -59,10 +59,11 @@ public void AsyncApiSchemaGenerator_OnGeneratePrimitive_SchemaTypeAndNameIsMatch // Assert schema.ShouldNotBeNull(); - schema.Properties.ShouldBeEmpty(); - schema.Format.ShouldBe(format); - schema.Type.ShouldBe(schemaType); - schema.Nullable.ShouldBe(nullable); + schema.Value.All.Count.ShouldBe(1); + schema.Value.Root.Properties.ShouldBeEmpty(); + schema.Value.Root.Format.ShouldBe(format); + schema.Value.Root.Type.ShouldBe(schemaType); + schema.Value.Root.Nullable.ShouldBe(nullable); } [Fact] @@ -77,24 +78,25 @@ public void AsyncApiSchemaGenerator_OnGenerateParams_SchemaIsMatch() // Assert schema.ShouldNotBeNull(); - schema.Properties.Count.ShouldBe(6); + schema.Value.All.Count.ShouldBe(8); + schema.Value.Root.Properties.Count.ShouldBe(7); - schema.Properties.ShouldContainKey("id"); - var id = schema.Properties["id"]; + schema.Value.Root.Properties.ShouldContainKey("id"); + var id = schema.Value.Root.Properties["id"]; id.Type.ShouldBe(SchemaType.String); id.Format.ShouldBe("guid"); id.Title.ShouldBe("guid"); id.Nullable.ShouldBeFalse(); - schema.Properties.ShouldContainKey("myUri"); - var myUri = schema.Properties["myUri"]; + schema.Value.Root.Properties.ShouldContainKey("myUri"); + var myUri = schema.Value.Root.Properties["myUri"]; myUri.Type.ShouldBe(SchemaType.String); myUri.Format.ShouldBe("uri"); myUri.Title.ShouldBe("uri"); myUri.Nullable.ShouldBeTrue(); - schema.Properties.ShouldContainKey("bar"); - var bar = schema.Properties["bar"]; + schema.Value.Root.Properties.ShouldContainKey("bar"); + var bar = schema.Value.Root.Properties["bar"]; bar.Type.ShouldBe(SchemaType.Object); bar.Title.ShouldBe("bar"); bar.Format.ShouldBeNull(); @@ -113,22 +115,29 @@ public void AsyncApiSchemaGenerator_OnGenerateParams_SchemaIsMatch() barCost.Format.ShouldBe("decimal"); barCost.Nullable.ShouldBeTrue(); - schema.Properties.ShouldContainKey("helloWorld"); - var helloWorld = schema.Properties["helloWorld"]; + schema.Value.Root.Properties.ShouldContainKey("helloWorld"); + var helloWorld = schema.Value.Root.Properties["helloWorld"]; helloWorld.Type.ShouldBe(SchemaType.String); helloWorld.Title.ShouldBe("string"); helloWorld.Format.ShouldBe("string"); helloWorld.Nullable.ShouldBeTrue(); - schema.Properties.ShouldContainKey("timestamp"); - var timestamp = schema.Properties["timestamp"]; + schema.Value.Root.Properties.ShouldContainKey("helloWorld2"); + var helloWorld2 = schema.Value.Root.Properties["helloWorld2"]; + helloWorld2.Type.ShouldBe(SchemaType.String); + helloWorld2.Title.ShouldBe("string"); + helloWorld2.Format.ShouldBe("string"); + helloWorld2.Nullable.ShouldBeTrue(); + + schema.Value.Root.Properties.ShouldContainKey("timestamp"); + var timestamp = schema.Value.Root.Properties["timestamp"]; timestamp.Type.ShouldBe(SchemaType.String); timestamp.Title.ShouldBe("dateTimeOffset"); timestamp.Format.ShouldBe("dateTimeOffset"); timestamp.Nullable.ShouldBeFalse(); - schema.Properties.ShouldContainKey("fooType"); - var fooType = schema.Properties["fooType"]; + schema.Value.Root.Properties.ShouldContainKey("fooType"); + var fooType = schema.Value.Root.Properties["fooType"]; fooType.Type.ShouldBe(SchemaType.String); fooType.Title.ShouldBe("fooType"); fooType.Format.ShouldBe("enum"); @@ -152,12 +161,22 @@ public void AsyncApiSchemaGenerator_OnLoopGenerate_NotFailed() // Assert schema.ShouldNotBeNull(); - schema.Properties.ShouldContainKey("ultraLoop"); - var loop = schema.Properties["ultraLoop"]; + schema.Value.All.Count.ShouldBe(1); + schema.Value.Root.Properties.Count.ShouldBe(2); + + schema.Value.Root.Properties.ShouldContainKey("ultraLoop"); + schema.Value.Root.Properties.ShouldContainKey("ultraLoop2"); + + var loop = schema.Value.Root.Properties["ultraLoop"]; loop.Reference.ShouldNotBeNull(); loop.Reference.Id.ShouldBe("loop"); loop.Reference.Type.ShouldBe(ReferenceType.Schema); + + var loop2 = schema.Value.Root.Properties["ultraLoop2"]; + loop2.Reference.ShouldNotBeNull(); + loop2.Reference.Id.ShouldBe("loop"); + loop2.Reference.Type.ShouldBe(ReferenceType.Schema); } } @@ -167,6 +186,7 @@ public class Foo public Uri MyUri { get; set; } public Bar Bar { get; set; } public string HelloWorld { get; set; } + public string HelloWorld2 { get; set; } public DateTimeOffset Timestamp { get; set; } public FooType FooType { get; set; } } @@ -182,5 +202,6 @@ public class Bar public class Loop { public Loop UltraLoop { get; set; } + public Loop UltraLoop2 { get; set; } } }