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/.github/dotnet/action.yaml b/.github/dotnet/action.yaml index 47858e0c..87cc1b23 100644 --- a/.github/dotnet/action.yaml +++ b/.github/dotnet/action.yaml @@ -7,4 +7,4 @@ runs: id: setup-dotnet uses: actions/setup-dotnet@v1 with: - dotnet-version: "6.0.x" + dotnet-version: "6.0.423" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b9ab2a66..897c1176 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,8 +21,14 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: setup dotnet - uses: ./.github/dotnet + + # bug of format tool in dotnet 6 + - name: setup + id: setup-dotnet + uses: actions/setup-dotnet@v1 + with: + dotnet-version: "8.0.300" + - name: dotnet format check run: dotnet format --verify-no-changes *.sln env: diff --git a/CHANGELOG.md b/CHANGELOG.md index a44ee242..63238733 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,17 @@ # Changelog + All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +## [v0.14.0] - ? +### Changed +- [Change AsyncApi data structure to LEGO AsyncAPI.NET](https://github.com/m-wild/saunter/issues/188) +- [Replace NJsonSchema with own implementation](https://github.com/m-wild/saunter/issues/188) +- [Allow usages of the annotation attributes on interfaces](https://github.com/m-wild/saunter/issues/213) +- Bump ws from 7.5.3 to 7.5.10 in /src/Saunter.UI ## [v0.13.0] - 2024-01-16 ### Changed diff --git a/README.md b/README.md index 0f555d6e..b5204572 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Saunter -![CI](https://github.com/tehmantra/saunter/workflows/CI/badge.svg) +![CI](https://github.com/asyncapi/saunter/workflows/CI/badge.svg) [![NuGet Badge](https://buildstats.info/nuget/saunter?includePreReleases=true)](https://www.nuget.org/packages/Saunter/) Saunter is an [AsyncAPI](https://github.com/asyncapi/asyncapi) documentation generator for dotnet. @@ -11,7 +11,7 @@ Saunter is an [AsyncAPI](https://github.com/asyncapi/asyncapi) documentation gen ## Getting Started -See [examples/StreetlightsAPI](https://github.com/tehmantra/saunter/blob/main/examples/StreetlightsAPI). +See [examples/StreetlightsAPI](https://github.com/asyncapi/saunter/tree/main/examples/StreetlightsAPI). 1. Install the Saunter package @@ -27,25 +27,30 @@ See [examples/StreetlightsAPI](https://github.com/tehmantra/saunter/blob/main/ex services.AddAsyncApiSchemaGeneration(options => { // Specify example type(s) from assemblies to scan. - options.AssemblyMarkerTypes = new[] {typeof(StreetlightMessageBus)}; - + options.AssemblyMarkerTypes = new[] { typeof(StreetlightMessageBus) }; + // Build as much (or as little) of the AsyncApi document as you like. // Saunter will generate Channels, Operations, Messages, etc, but you // may want to specify Info here. + options.Middleware.UiTitle = "Streetlights API"; options.AsyncApi = new AsyncApiDocument { - Info = new Info("Streetlights API", "1.0.0") + Info = new AsyncApiInfo() { - Description = "The Smartylighting Streetlights API allows you\nto remotely manage the city lights.", - License = new License("Apache 2.0") + Title = "Streetlights API", + Version = "1.0.0", + Description = "The Smartylighting Streetlights API allows you to remotely manage the city lights.", + License = new AsyncApiLicense() { - Url = "https://www.apache.org/licenses/LICENSE-2.0" + Name = "Apache 2.0", + Url = new("https://www.apache.org/licenses/LICENSE-2.0"), } }, Servers = { - { "mosquitto", new Server("test.mosquitto.org", "mqtt") } - } + ["mosquitto"] = new AsyncApiServer(){ Url = "test.mosquitto.org", Protocol = "mqtt"}, + ["webapi"] = new AsyncApiServer(){ Url = "localhost:5000", Protocol = "http"}, + }, }; }); ``` @@ -56,9 +61,11 @@ See [examples/StreetlightsAPI](https://github.com/tehmantra/saunter/blob/main/ex [AsyncApi] // Tells Saunter to scan this class. public class StreetlightMessageBus : IStreetlightMessageBus { - [Channel("publish/light/measured")] // Creates a Channel - [PublishOperation(typeof(LightMeasuredEvent), Summary = "Inform about environmental lighting conditions for a particular streetlight.")] // A simple Publish operation. - public void PublishLightMeasuredEvent(Streetlight streetlight, int lumens) {} + private const string SubscribeLightMeasuredTopic = "subscribe/light/measured"; + + [Channel(SubscribeLightMeasuredTopic, Servers = new[] { "mosquitto" })] + [SubscribeOperation(typeof(LightMeasuredEvent), "Light", Summary = "Subscribe to environmental lighting conditions for a particular streetlight.")] + public void PublishLightMeasuredEvent(Streetlight streetlight, int lumens) {} ``` 4. Add saunter middleware to host the AsyncApi json document. In the `Configure` method of `Startup.cs`: @@ -68,6 +75,8 @@ See [examples/StreetlightsAPI](https://github.com/tehmantra/saunter/blob/main/ex { endpoints.MapAsyncApiDocuments(); endpoints.MapAsyncApiUi(); + + endpoints.MapControllers(); }); ``` @@ -96,11 +105,11 @@ See [examples/StreetlightsAPI](https://github.com/tehmantra/saunter/blob/main/ex 6. Use the published AsyncAPI UI: - ![AsyncAPI UI](https://raw.githubusercontent.com/tehmantra/saunter/main/assets/asyncapi-ui-screenshot.png) + ![AsyncAPI UI](https://raw.githubusercontent.com/asyncapi/saunter/main/assets/asyncapi-ui-screenshot.png) ## Configuration -See [the options source code](https://github.com/tehmantra/saunter/blob/main/src/Saunter/AsyncApiOptions.cs) for detailed info. +See [the options source code](https://github.com/asyncapi/saunter/blob/main/src/Saunter/AsyncApiOptions.cs) for detailed info. Common options are below: @@ -109,7 +118,7 @@ services.AddAsyncApiSchemaGeneration(options => { options.AssemblyMarkerTypes = new[] { typeof(Startup) }; // Tell Saunter where to scan for your classes. - options.AddChannelItemFilter(); // Dynamically update ChanelItems + options.AddChannelFilter(); // Dynamically update ChanelItems options.AddOperationFilter(); // Dynamically update Operations options.Middleware.Route = "/asyncapi/asyncapi.json"; // AsyncAPI JSON document URL @@ -118,30 +127,6 @@ services.AddAsyncApiSchemaGeneration(options => } ``` - -## JSON Schema Settings - -The JSON schema generation can be customized using the `options.JsonSchemaGeneratorSettings`. Saunter defaults to the popular `camelCase` naming strategy for both properties and types. - -For example, setting to use PascalCase: - -```c# -services.AddAsyncApiSchemaGeneration(options => -{ - options.JsonSchemaGeneratorSettings.TypeNameGenerator = new DefaultTypeNameGenerator(); - - // Note: need to assign a new JsonSerializerSettings, not just set the properties within it. - options.JsonSchemaGeneratorSettings.SerializerSettings = new JsonSerializerSettings - { - ContractResolver = new DefaultContractResolver(), - Formatting = Formatting.Indented; - }; -} -``` - -You have access to the full range of both [NJsonSchema](https://github.com/RicoSuter/NJsonSchema) and [JSON.NET](https://github.com/JamesNK/Newtonsoft.Json) settings to configure the JSON schema generation, including custom ContractResolvers. - - ## Bindings Bindings are used to describe protocol specific information. These can be added to the AsyncAPI document and then applied to different components by setting the `BindingsRef` property in the relevant attributes `[OperationAttribute]`, `[MessageAttribute]`, `[ChannelAttribute]` @@ -155,17 +140,31 @@ services.AddAsyncApiSchemaGeneration(options => { Components = { - ChannelBindings = + ChannelBindings = { - ["my-amqp-binding"] = new ChannelBindings + ["amqpDev"] = new() { - Amqp = new AmqpChannelBinding + new AMQPChannelBinding { - Is = AmqpChannelBindingIs.RoutingKey, - Exchange = new AmqpChannelBindingExchange + Is = ChannelType.Queue, + Exchange = new() { Name = "example-exchange", - VirtualHost = "/development" + Vhost = "/development" + } + } + } + }, + OperationBindings = + { + { + "postBind", + new() + { + new HttpOperationBinding + { + Method = "POST", + Type = HttpOperationType.Response, } } } @@ -176,16 +175,16 @@ services.AddAsyncApiSchemaGeneration(options => ``` ```csharp -[Channel("light.measured", BindingsRef = "my-amqp-binding")] // Set the BindingsRef property +[Channel("light.measured", BindingsRef = "amqpDev")] // Set the BindingsRef property public void PublishLightMeasuredEvent(Streetlight streetlight, int lumens) {} ``` -Available bindings: -* [AMQP](https://github.com/tehmantra/saunter/tree/main/src/Saunter/AsyncApiSchema/v2/Bindings/Amqp) -* [HTTP](https://github.com/tehmantra/saunter/tree/main/src/Saunter/AsyncApiSchema/v2/Bindings/Http) -* [Kafka](https://github.com/tehmantra/saunter/tree/main/src/Saunter/AsyncApiSchema/v2/Bindings/Kafka) -* [MQTT](https://github.com/tehmantra/saunter/tree/main/src/Saunter/AsyncApiSchema/v2/Bindings/Mqtt) +```csharp +[PublishOperation(typeof(LightMeasuredEvent), "Light", BindingsRef = "postBind")] +public void MeasureLight([FromBody] LightMeasuredEvent lightMeasuredEvent) +``` +Available bindings: https://www.nuget.org/packages/AsyncAPI.NET.Bindings/ ## Multiple AsyncAPI documents @@ -250,10 +249,27 @@ Each document can be accessed by specifying the name in the URL } ``` +## Migration to LEGO AsyncApi.Net + +When switching to the LEGO AsyncApi.Net, we broke the public API. + +To simplify the transition to new versions of the library, this note was created. + +What was broken: + +* Namespaces have changed: + * Saunter.AsyncApiSchema.v2 -> LEGO.AsyncAPI.Models + * Saunter.Attributes; -> Saunter.AttributeProvider.Attributes +* Change the name of the data structures, add prefix 'AsyncApi' (example 'class Info' -> 'class AsyncApiInfo') +* All data structure constructors are now with the parameterless constructor + +There was no more significant changes on public API. + +Keep this in mind when planning the migration process. ## Contributing -See our [contributing guide](https://github.com/tehmantra/saunter/blob/main/CONTRIBUTING.md/CONTRIBUTING.md). +See our [contributing guide](https://github.com/asyncapi/saunter/blob/main/CONTRIBUTING.md/CONTRIBUTING.md). Feel free to get involved in the project by opening issues, or submitting pull requests. @@ -262,5 +278,4 @@ You can also find me on the [AsyncAPI community slack](https://asyncapi.com/slac ## Thanks * This project is heavily inspired by [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle.AspNetCore). -* We use [NJsonSchema](https://github.com/RicoSuter/NJsonSchema) for the JSON schema heavy lifting, - +* We use [LEGO AsyncAPI.NET](https://github.com/LEGO/AsyncAPI.NET) schema and serializing. diff --git a/examples/StreetlightsAPI/API.cs b/examples/StreetlightsAPI/API.cs index 978a0dea..07377159 100644 --- a/examples/StreetlightsAPI/API.cs +++ b/examples/StreetlightsAPI/API.cs @@ -4,7 +4,7 @@ using System.Text.Json; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using Saunter.Attributes; +using Saunter.AttributeProvider.Attributes; namespace StreetlightsAPI { @@ -51,10 +51,9 @@ public class StreetlightsController { private const string PublishLightMeasuredTopic = "publish/light/measured"; - // Simulate a database of streetlights - private static int StreetlightSeq = 2; - private static readonly List StreetlightDatabase = new List + private static int s_streetlightSeq = 2; + private static readonly List s_streetlightDatabase = new() { new Streetlight { Id = 1, Position = new [] { -36.320320, 175.485986 }, LightIntensity = new() }, }; @@ -73,7 +72,7 @@ public StreetlightsController(IStreetlightMessageBus streetlightMessageBus, ILog /// [HttpGet] [Route("api/streetlights")] - public IEnumerable Get() => StreetlightDatabase; + public IEnumerable Get() => s_streetlightDatabase; /// /// Add a new streetlight @@ -82,8 +81,8 @@ public StreetlightsController(IStreetlightMessageBus streetlightMessageBus, ILog [Route("api/streetlights")] public Streetlight Add([FromBody] Streetlight streetlight) { - streetlight.Id = StreetlightSeq++; - StreetlightDatabase.Add(streetlight); + streetlight.Id = s_streetlightSeq++; + s_streetlightDatabase.Add(streetlight); return streetlight; } @@ -91,7 +90,7 @@ public Streetlight Add([FromBody] Streetlight streetlight) /// Inform about environmental lighting conditions for a particular streetlight. /// [Channel(PublishLightMeasuredTopic, Servers = new[] { "webapi" })] - [PublishOperation(typeof(LightMeasuredEvent), "Light")] + [PublishOperation(typeof(LightMeasuredEvent), "Light", BindingsRef = "postBind")] [HttpPost] [Route(PublishLightMeasuredTopic)] public void MeasureLight([FromBody] LightMeasuredEvent lightMeasuredEvent) @@ -102,7 +101,7 @@ public void MeasureLight([FromBody] LightMeasuredEvent lightMeasuredEvent) _logger.LogInformation("Received message on {Topic} with payload {Payload} ", PublishLightMeasuredTopic, payload); - var streetlight = StreetlightDatabase.SingleOrDefault(s => s.Id == lightMeasuredEvent.Id); + var streetlight = s_streetlightDatabase.SingleOrDefault(s => s.Id == lightMeasuredEvent.Id); if (streetlight != null) { streetlight.LightIntensity.Add(new(lightMeasuredEvent.SentAt, lightMeasuredEvent.Lumens)); diff --git a/examples/StreetlightsAPI/Messaging.cs b/examples/StreetlightsAPI/Messaging.cs index 00b9e679..b95a8161 100644 --- a/examples/StreetlightsAPI/Messaging.cs +++ b/examples/StreetlightsAPI/Messaging.cs @@ -1,6 +1,6 @@ using System.Text.Json; using Microsoft.Extensions.Logging; -using Saunter.Attributes; +using Saunter.AttributeProvider.Attributes; namespace StreetlightsAPI { @@ -21,7 +21,7 @@ public StreetlightMessageBus(ILoggerFactory logger) _logger = logger.CreateLogger("Streetlight"); } - [Channel(SubscribeLightMeasuredTopic, Servers = new[] { "mosquitto" })] + [Channel(SubscribeLightMeasuredTopic, Servers = new[] { "mosquitto" }, BindingsRef = "amqpDev")] [SubscribeOperation(typeof(LightMeasuredEvent), "Light", Summary = "Subscribe to environmental lighting conditions for a particular streetlight.")] public void PublishLightMeasurement(LightMeasuredEvent lightMeasuredEvent) { diff --git a/examples/StreetlightsAPI/Program.cs b/examples/StreetlightsAPI/Program.cs index aa3f77ad..125d8483 100644 --- a/examples/StreetlightsAPI/Program.cs +++ b/examples/StreetlightsAPI/Program.cs @@ -1,4 +1,6 @@ using System.Linq; +using LEGO.AsyncAPI.Bindings.AMQP; +using LEGO.AsyncAPI.Models; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server.Features; @@ -7,7 +9,6 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Saunter; -using Saunter.AsyncApiSchema.v2; namespace StreetlightsAPI { @@ -51,19 +52,54 @@ public void ConfigureServices(IServiceCollection services) options.AsyncApi = new AsyncApiDocument { - Info = new Info("Streetlights API", "1.0.0") + Info = new AsyncApiInfo() { + Title = "Streetlights API", + Version = "1.0.0", Description = "The Smartylighting Streetlights API allows you to remotely manage the city lights.", - License = new License("Apache 2.0") + License = new AsyncApiLicense() { - Url = "https://www.apache.org/licenses/LICENSE-2.0" + Name = "Apache 2.0", + Url = new("https://www.apache.org/licenses/LICENSE-2.0"), } }, Servers = { - ["mosquitto"] = new Server("test.mosquitto.org", "mqtt"), - ["webapi"] = new Server("localhost:5000", "http"), + ["mosquitto"] = new AsyncApiServer(){ Url = "test.mosquitto.org", Protocol = "mqtt"}, + ["webapi"] = new AsyncApiServer(){ Url = "localhost:5000", Protocol = "http"}, }, + Components = new() + { + ChannelBindings = + { + ["amqpDev"] = new() + { + new AMQPChannelBinding + { + Is = ChannelType.Queue, + Exchange = new() + { + Name = "example-exchange", + Vhost = "/development" + } + } + } + }, + OperationBindings = + { + { + "postBind", + new() + { + new LEGO.AsyncAPI.Bindings.Http.HttpOperationBinding + { + Method = "POST", + Type = LEGO.AsyncAPI.Bindings.Http.HttpOperationBinding.HttpOperationType.Response, + } + } + } + } + } }; }); diff --git a/examples/StreetlightsAPI/README.md b/examples/StreetlightsAPI/README.md index e0a887b2..a0d46c76 100644 --- a/examples/StreetlightsAPI/README.md +++ b/examples/StreetlightsAPI/README.md @@ -1,47 +1,6 @@ # Streetlights API Example -This is an example implementation of the [Streetlights API from the asyncapi tutorial](https://www.asyncapi.com/docs/tutorials/streetlights/). - -The generated AsyncAPI documentation should look like this: - -```yml -asyncapi: '2.1.0' -info: - title: Streetlights API - version: '1.0.0' - description: | - The Smartylighting Streetlights API allows you - to remotely manage the city lights. - license: - name: Apache 2.0 - url: 'https://www.apache.org/licenses/LICENSE-2.0' -servers: - mosquitto: - url: mqtt://test.mosquitto.org - protocol: mqtt -channels: - light/measured: - publish: - summary: Inform about environmental lighting conditions for a particular streetlight. - operationId: onLightMeasured - message: - name: LightMeasured - payload: - type: object - properties: - id: - type: integer - minimum: 0 - description: Id of the streetlight. - lumens: - type: integer - minimum: 0 - description: Light intensity measured in lumens. - sentAt: - type: string - format: date-time - description: Date and time when the message was sent. -``` +This is an example implementation (with minor additions) of the [Streetlights API from the asyncapi tutorial](https://www.asyncapi.com/docs/tutorials/streetlights/). ## Running @@ -107,7 +66,7 @@ The generated asyncapi document is not identical to the AsyncAPI example above a ```json { - "asyncapi": "2.1.0", + "asyncapi": "2.6.0", "info": { "title": "Streetlights API", "version": "1.0.0", @@ -130,49 +89,79 @@ The generated asyncapi document is not identical to the AsyncAPI example above a "defaultContentType": "application/json", "channels": { "publish/light/measured": { + "servers": [ + "webapi" + ], "publish": { "operationId": "MeasureLight", "summary": "Inform about environmental lighting conditions for a particular streetlight.", + "tags": [ + { + "name": "Light" + } + ], + "bindings": { + "$ref": "#/components/operationBindings/postBind" + }, "message": { "$ref": "#/components/messages/lightMeasuredEvent" } } }, "subscribe/light/measured": { + "servers": [ + "mosquitto" + ], "subscribe": { "operationId": "PublishLightMeasurement", "summary": "Subscribe to environmental lighting conditions for a particular streetlight.", - "message": { - "payload": { - "$ref": "#/components/schemas/lightMeasuredEvent" + "tags": [ + { + "name": "Light" } + ], + "message": { + "$ref": "#/components/messages/lightMeasuredEvent" } + }, + "bindings": { + "$ref": "#/components/channelBindings/amqpDev" } } }, "components": { "schemas": { "lightMeasuredEvent": { - "id": "lightMeasuredEvent", + "title": "lightMeasuredEvent", "type": "object", - "additionalProperties": false, "properties": { "id": { + "title": "int32", "type": "integer", - "description": "Id of the streetlight.", "format": "int32" }, "lumens": { + "title": "int32", "type": "integer", - "description": "Light intensity measured in lumens.", "format": "int32" }, "sentAt": { + "title": "dateTime", "type": "string", - "description": "Light intensity measured in lumens.", - "format": "date-time" + "format": "dateTime" } - } + }, + "nullable": true + }, + "int32": { + "title": "int32", + "type": "integer", + "format": "int32" + }, + "dateTime": { + "title": "dateTime", + "type": "string", + "format": "dateTime" } }, "messages": { @@ -180,7 +169,23 @@ The generated asyncapi document is not identical to the AsyncAPI example above a "payload": { "$ref": "#/components/schemas/lightMeasuredEvent" }, - "name": "lightMeasuredEvent" + "name": "lightMeasuredEvent", + "title": "lightMeasuredEvent" + } + }, + "channelBindings": { + "amqpDev": { + "amqp": { + "is": "queue" + } + } + }, + "operationBindings": { + "postBind": { + "http": { + "type": "response", + "method": "POST" + } } } } diff --git a/src/Saunter/AsyncApiEndpointRouteBuilderExtensions.cs b/src/Saunter/AsyncApiEndpointRouteBuilderExtensions.cs index 9383fb92..33855bdc 100644 --- a/src/Saunter/AsyncApiEndpointRouteBuilderExtensions.cs +++ b/src/Saunter/AsyncApiEndpointRouteBuilderExtensions.cs @@ -3,6 +3,8 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using Saunter.DocumentMiddleware; +using Saunter.Options; using Saunter.UI; namespace Saunter @@ -19,7 +21,7 @@ public static IEndpointConventionBuilder MapAsyncApiDocuments( .UseMiddleware() .Build(); - var options = endpoints.ServiceProvider.GetService>(); + var options = endpoints.ServiceProvider.GetRequiredService>(); var route = options.Value.Middleware.Route; return endpoints.MapGet(route, pipeline); @@ -42,7 +44,7 @@ public static IEndpointConventionBuilder MapAsyncApiUi(this IEndpointRouteBuilde .UseMiddleware() .Build(); - var options = endpoints.ServiceProvider.GetService>(); + var options = endpoints.ServiceProvider.GetRequiredService>(); var route = options.Value.Middleware.UiBaseRoute + "{*wildcard}"; return endpoints.MapGet(route, pipeline); diff --git a/src/Saunter/AsyncApiOptions.cs b/src/Saunter/AsyncApiOptions.cs deleted file mode 100644 index cc64cc55..00000000 --- a/src/Saunter/AsyncApiOptions.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; - -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -using NJsonSchema; -using NJsonSchema.NewtonsoftJson.Generation; - -using Saunter.AsyncApiSchema.v2; -using Saunter.Generation.Filters; -using Saunter.Generation.SchemaGeneration; - -namespace Saunter -{ - public class AsyncApiOptions - { - private readonly List _documentFilters = new List(); - private readonly List _channelItemFilters = new List(); - private readonly List _operationFilters = new List(); - - - /// - /// The base asyncapi schema. This will be augmented with other information auto-discovered - /// from attributes. - /// - public AsyncApiDocument AsyncApi { get; set; } = new AsyncApiDocument(); - - /// - /// A list of marker types from assemblies to scan for Saunter attributes. - /// - public IList AssemblyMarkerTypes { get; set; } = new List(); - - /// - /// A list of filters that will be applied to the generated AsyncAPI document. - /// - public IEnumerable DocumentFilters => _documentFilters; - - /// - /// A list of filters that will be applied to any generated channels. - /// - public IEnumerable ChannelItemFilters => _channelItemFilters; - - /// - /// A list of filters that will be applied to any generated Publish/Subscribe operations. - /// - public IEnumerable OperationFilters => _operationFilters; - - - /// - /// Add a filter to be applied to the generated AsyncAPI document. - /// - public void AddDocumentFilter() where T : IDocumentFilter - { - _documentFilters.Add(typeof(T)); - } - - /// - /// Add a filter to be applied to any generated channels. - /// - public void AddChannelItemFilter() where T : IChannelItemFilter - { - _channelItemFilters.Add(typeof(T)); - } - - /// - /// Add a filter to be applied to any generated Publish/Subscribe operations. - /// - public void AddOperationFilter() where T : IOperationFilter - { - _operationFilters.Add(typeof(T)); - } - - - - /// - /// Options related to the Saunter middleware. - /// - public AsyncApiMiddlewareOptions Middleware { get; } = new AsyncApiMiddlewareOptions(); - - public ConcurrentDictionary NamedApis { get; set; } = - new ConcurrentDictionary(); - - /// - /// Settings related to the JSON Schema generation. - /// - public AsyncApiSchemaOptions SchemaOptions { get; set; } = new AsyncApiSchemaOptions(); - } - - public class AsyncApiSchemaOptions : NewtonsoftJsonSchemaGeneratorSettings - { - public AsyncApiSchemaOptions() - { - SchemaType = SchemaType.JsonSchema; // AsyncAPI uses json-schema, see https://github.com/tehmantra/saunter/pull/103#issuecomment-893267360 - TypeNameGenerator = new CamelCaseTypeNameGenerator(); - SerializerSettings = new JsonSerializerSettings() - { - ContractResolver = new CamelCasePropertyNamesContractResolver(), - NullValueHandling = NullValueHandling.Ignore, - DefaultValueHandling = DefaultValueHandling.Ignore - }; - } - } - - public class AsyncApiMiddlewareOptions - { - /// - /// The route which the AsyncAPI document will be hosted - /// - public string Route { get; set; } = "/asyncapi/asyncapi.json"; - - /// - /// The base URL for the AsyncAPI UI - /// - public string UiBaseRoute { get; set; } = "/asyncapi/ui/"; - - /// - /// The title of page for AsyncAPI UI - /// - public string UiTitle { get; set; } = "AsyncAPI"; - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/AsyncApiDocument.cs b/src/Saunter/AsyncApiSchema/v2/AsyncApiDocument.cs deleted file mode 100644 index ed38829a..00000000 --- a/src/Saunter/AsyncApiSchema/v2/AsyncApiDocument.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -using Newtonsoft.Json; - -using NJsonSchema.NewtonsoftJson.Converters; - -namespace Saunter.AsyncApiSchema.v2 -{ - [JsonConverter(typeof(JsonReferenceConverter))] - public class AsyncApiDocument : ICloneable - { - /// - /// Specifies the AsyncAPI Specification version being used. - /// - [JsonProperty("asyncapi", NullValueHandling = NullValueHandling.Ignore)] - public string AsyncApi { get; } = "2.4.0"; - - /// - /// Identifier of the application the AsyncAPI document is defining. - /// - [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] - public string Id { get; set; } - - /// - /// Provides metadata about the API. The metadata can be used by the clients if needed. - /// - [JsonProperty("info", NullValueHandling = NullValueHandling.Ignore)] - public Info Info { get; set; } - - /// - /// Provides connection details of servers. - /// - [JsonProperty("servers")] - public Dictionary Servers { get; set; } = new Dictionary(); - - /// - /// A string representing the default content type to use when encoding/decoding a message's payload. - /// The value MUST be a specific media type (e.g. application/json). - /// - [JsonProperty("defaultContentType", NullValueHandling = NullValueHandling.Ignore)] - public string DefaultContentType { get; set; } = "application/json"; - - /// - /// The available channels and messages for the API. - /// - [JsonProperty("channels")] - public IDictionary Channels { get; set; } = new Dictionary(); - - /// - /// An element to hold various schemas for the specification. - /// - [JsonProperty("components")] - public Components Components { get; set; } = new Components(); - - /// - /// A list of tags used by the specification with additional metadata. - /// Each tag name in the list MUST be unique. - /// - [JsonProperty("tags")] - public ISet Tags { get; } = new HashSet(); - - /// - /// Additional external documentation. - /// - [JsonProperty("externalDocs", NullValueHandling = NullValueHandling.Ignore)] - public ExternalDocumentation ExternalDocs { get; set; } - - [JsonIgnore] - public string DocumentName { get; set; } - - - public bool ShouldSerializeTags() - { - return Tags != null && Tags.Count > 0; - } - - public bool ShouldSerializeServers() - { - return Servers != null && Servers.Count > 0; - } - - public AsyncApiDocument Clone() - { - var clone = new AsyncApiDocument(); - clone.Info = Info; - clone.Id = Id; - clone.DefaultContentType = DefaultContentType; - clone.DocumentName = DocumentName; - clone.Channels = Channels.ToDictionary(p => p.Key, p => p.Value); - clone.Servers = Servers.ToDictionary(p => p.Key, p => p.Value); - foreach (var tag in Tags) - { - clone.Tags.Add(tag); - } - clone.ExternalDocs = ExternalDocs; - clone.Components = Components.Clone(); - - return clone; - } - - object ICloneable.Clone() - { - return Clone(); - } - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/Amqp/AmqpChannelBinding.cs b/src/Saunter/AsyncApiSchema/v2/Bindings/Amqp/AmqpChannelBinding.cs deleted file mode 100644 index b27cf426..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/Amqp/AmqpChannelBinding.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System.Runtime.Serialization; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; - -namespace Saunter.AsyncApiSchema.v2.Bindings.Amqp -{ - /// - /// https://github.com/asyncapi/bindings/blob/master/amqp/README.md#channel - /// - public class AmqpChannelBinding - { - /// - /// Defines what type of channel is it. Can be either queue or routingKey (default). - /// - [JsonProperty("is", NullValueHandling = NullValueHandling.Ignore)] - [JsonConverter(typeof(StringEnumConverter))] - public AmqpChannelBindingIs? Is { get; set; } - - /// - /// When is=routingKey, this object defines the exchange properties. - /// - [JsonProperty("exchange", NullValueHandling = NullValueHandling.Ignore)] - public AmqpChannelBindingExchange Exchange { get; set; } - - /// - /// When is=queue, this object defines the queue properties. - /// - [JsonProperty("queue", NullValueHandling = NullValueHandling.Ignore)] - public AmqpChannelBindingQueue Queue { get; set; } - - /// - /// The version of this binding. If omitted, "latest" MUST be assumed. - /// - [JsonProperty("bindingVersion", NullValueHandling = NullValueHandling.Ignore)] - public string BindingVersion { get; set; } - } - - public class AmqpChannelBindingExchange - { - /// - /// The name of the exchange. It MUST NOT exceed 255 characters long. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; set; } - - /// - /// The type of the exchange. Can be either topic, direct, fanout, default or headers. - /// - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] - public AmqpChannelBindingExchangeType? Type { get; set; } - - /// - /// Whether the exchange should survive broker restarts or not. - /// - [JsonProperty("durable", NullValueHandling = NullValueHandling.Ignore)] - public bool? Durable { get; set; } - - /// - /// Whether the exchange should be deleted when the last queue is unbound from it. - /// - [JsonProperty("autoDelete", NullValueHandling = NullValueHandling.Ignore)] - public bool? AutoDelete { get; set; } - - /// - /// The virtual host of the exchange. Defaults to /. - /// - [JsonProperty("vhost", NullValueHandling = NullValueHandling.Ignore)] - public string VirtualHost { get; set; } - } - - public class AmqpChannelBindingQueue - { - /// - /// The name of the queue. It MUST NOT exceed 255 characters long. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; set; } - - /// - /// Whether the queue should survive broker restarts or not. - /// - [JsonProperty("durable", NullValueHandling = NullValueHandling.Ignore)] - public bool? Durable { get; set; } - - /// - /// Whether the queue should be used only by one connection or not. - /// - [JsonProperty("exclusive", NullValueHandling = NullValueHandling.Ignore)] - public bool? Exclusive { get; set; } - - /// - /// Whether the queue should be deleted when the last consumer unsubscribes. - /// - [JsonProperty("autoDelete", NullValueHandling = NullValueHandling.Ignore)] - public bool? AutoDelete { get; set; } - - /// - /// The virtual host of the queue. Defaults to /. - /// - [JsonProperty("vhost", NullValueHandling = NullValueHandling.Ignore)] - public string VirtualHost { get; set; } - } - - - [JsonConverter(typeof(StringEnumConverter))] - public enum AmqpChannelBindingIs - { - [EnumMember(Value = "routingKey")] - RoutingKey, - - [EnumMember(Value = "queue")] - Queue, - } - - [JsonConverter(typeof(StringEnumConverter))] - public enum AmqpChannelBindingExchangeType - { - [EnumMember(Value = "topic")] - Topic, - - [EnumMember(Value = "direct")] - Direct, - - [EnumMember(Value = "fanout")] - Fanout, - - [EnumMember(Value = "default")] - Default, - - [EnumMember(Value = "headers")] - Headers, - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/Amqp/AmqpMessageBinding.cs b/src/Saunter/AsyncApiSchema/v2/Bindings/Amqp/AmqpMessageBinding.cs deleted file mode 100644 index 780895f2..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/Amqp/AmqpMessageBinding.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Newtonsoft.Json; - -namespace Saunter.AsyncApiSchema.v2.Bindings.Amqp -{ - /// - /// See: https://github.com/asyncapi/bindings/blob/master/amqp/README.md#message-binding-object - /// - public class AmqpMessageBinding - { - /// - /// A MIME encoding for the message content. - /// - [JsonProperty("contentEncoding", NullValueHandling = NullValueHandling.Ignore)] - public string ContentEncoding { get; set; } - - /// - /// Application-specific message type. - /// - [JsonProperty("messageType", NullValueHandling = NullValueHandling.Ignore)] - public string MessageType { get; set; } - - /// - /// The version of this binding. If omitted, "latest" MUST be assumed. - /// - [JsonProperty("bindingVersion", NullValueHandling = NullValueHandling.Ignore)] - public string BindingVersion { get; set; } - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/Amqp/AmqpOperationBinding.cs b/src/Saunter/AsyncApiSchema/v2/Bindings/Amqp/AmqpOperationBinding.cs deleted file mode 100644 index d02d26a7..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/Amqp/AmqpOperationBinding.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace Saunter.AsyncApiSchema.v2.Bindings.Amqp -{ - /// - /// See: https://github.com/asyncapi/bindings/blob/master/amqp/README.md#operation-binding-object - /// - public class AmqpOperationBinding - { - /// - /// TTL (Time-To-Live) for the message. It MUST be greater than or equal to zero. - /// - [JsonProperty("expiration", NullValueHandling = NullValueHandling.Ignore)] - public int Expiration { get; set; } - - /// - /// Identifies the user who has sent the message. - /// - [JsonProperty("userId", NullValueHandling = NullValueHandling.Ignore)] - public string UserId { get; set; } - - /// - /// The routing keys the message should be routed to at the time of publishing. - /// - [JsonProperty("cc", NullValueHandling = NullValueHandling.Ignore)] - public IList Cc { get; set; } - - /// - /// A priority for the message. - /// - [JsonProperty("priority", NullValueHandling = NullValueHandling.Ignore)] - public int Priority { get; set; } - - /// - /// Delivery mode of the message. Its value MUST be either 1 (transient) or 2 (persistent). - /// - [JsonProperty("deliveryMode", NullValueHandling = NullValueHandling.Ignore)] - public int DeliveryMode { get; set; } - - /// - /// Whether the message is mandatory or not. - /// - [JsonProperty("mandatory", NullValueHandling = NullValueHandling.Ignore)] - public bool? Mandatory { get; set; } - - /// - /// Like cc but consumers will not receive this information. - /// - [JsonProperty("bcc", NullValueHandling = NullValueHandling.Ignore)] - public IList Bcc { get; set; } - - /// - /// Name of the queue where the consumer should send the response. - /// - [JsonProperty("replyTo", NullValueHandling = NullValueHandling.Ignore)] - public string ReplyTo { get; set; } - - /// - /// Whether the message should include a timestamp or not. - /// - [JsonProperty("timestamp", NullValueHandling = NullValueHandling.Ignore)] - public bool? Timestamp { get; set; } - - /// - /// Whether the consumer should ack the message or not. - /// - [JsonProperty("ack", NullValueHandling = NullValueHandling.Ignore)] - public bool? Ack { get; set; } - - /// - /// The version of this binding. If omitted, "latest" MUST be assumed. - /// - [JsonProperty("bindingVersion", NullValueHandling = NullValueHandling.Ignore)] - public string BindingVersion { get; set; } - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/Amqp/AmqpServerBinding.cs b/src/Saunter/AsyncApiSchema/v2/Bindings/Amqp/AmqpServerBinding.cs deleted file mode 100644 index e8276cb2..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/Amqp/AmqpServerBinding.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Saunter.AsyncApiSchema.v2.Bindings.Amqp -{ - /// - /// This object MUST NOT contain any properties. Its name is reserved for future use. - /// - /// - /// See: https://github.com/asyncapi/bindings/blob/master/amqp/README.md#server-binding-object - /// - public class AmqpServerBinding - { - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/Amqp/README.md b/src/Saunter/AsyncApiSchema/v2/Bindings/Amqp/README.md deleted file mode 100644 index f5ff12c1..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/Amqp/README.md +++ /dev/null @@ -1 +0,0 @@ -https://github.com/asyncapi/bindings/blob/master/amqp/README.md \ No newline at end of file diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/ChannelBindings.cs b/src/Saunter/AsyncApiSchema/v2/Bindings/ChannelBindings.cs deleted file mode 100644 index 57b792ca..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/ChannelBindings.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Newtonsoft.Json; -using Saunter.AsyncApiSchema.v2.Bindings.Amqp; -using Saunter.AsyncApiSchema.v2.Bindings.Http; -using Saunter.AsyncApiSchema.v2.Bindings.Kafka; -using Saunter.AsyncApiSchema.v2.Bindings.Mqtt; -using Saunter.AsyncApiSchema.v2.Bindings.Pulsar; - -namespace Saunter.AsyncApiSchema.v2.Bindings -{ - /// - /// ChannelBindings can be either a the bindings or a reference to the bindings. - /// - public interface IChannelBindings { } - - /// - /// A reference to the ChannelBindings within the AsyncAPI components. - /// - public class ChannelBindingsReference : Reference, IChannelBindings - { - public ChannelBindingsReference(string id) : base(id, "#/components/channelBindings/{0}") { } - } - - public class ChannelBindings : IChannelBindings - { - [JsonProperty("amqp", NullValueHandling = NullValueHandling.Ignore)] - public AmqpChannelBinding Amqp { get; set; } - - [JsonProperty("http", NullValueHandling = NullValueHandling.Ignore)] - public HttpChannelBinding Http { get; set; } - - [JsonProperty("kafka", NullValueHandling = NullValueHandling.Ignore)] - public KafkaChannelBinding Kafka { get; set; } - - [JsonProperty("mqtt", NullValueHandling = NullValueHandling.Ignore)] - public MqttChannelBinding Mqtt { get; set; } - - [JsonProperty("pulsar", NullValueHandling = NullValueHandling.Ignore)] - public PulsarChannelBinding Pulsar { get; set; } - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/Http/HttpChannelBinding.cs b/src/Saunter/AsyncApiSchema/v2/Bindings/Http/HttpChannelBinding.cs deleted file mode 100644 index ba965c97..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/Http/HttpChannelBinding.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Saunter.AsyncApiSchema.v2.Bindings.Http -{ - /// - /// See: https://github.com/asyncapi/bindings/tree/master/http#channel-binding-object - /// This object MUST NOT contain any properties. Its name is reserved for future use. - /// - public class HttpChannelBinding - { - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/Http/HttpMessageBinding.cs b/src/Saunter/AsyncApiSchema/v2/Bindings/Http/HttpMessageBinding.cs deleted file mode 100644 index 821f77c0..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/Http/HttpMessageBinding.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Newtonsoft.Json; -using NJsonSchema; - -namespace Saunter.AsyncApiSchema.v2.Bindings.Http -{ - /// - /// See: https://github.com/asyncapi/bindings/tree/master/http#message-binding-object - /// - public class HttpMessageBinding - { - /// - /// A Schema object containing the definitions for HTTP-specific headers. - /// This schema MUST be of type object and have a properties key. - /// - [JsonProperty("headers", NullValueHandling = NullValueHandling.Ignore)] - public JsonSchema Headers { get; set; } - - /// - /// The version of this binding. If omitted, "latest" MUST be assumed. - /// - [JsonProperty("bindingVersion", NullValueHandling = NullValueHandling.Ignore)] - public string BindingVersion { get; set; } - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/Http/HttpOperationBinding.cs b/src/Saunter/AsyncApiSchema/v2/Bindings/Http/HttpOperationBinding.cs deleted file mode 100644 index ec669382..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/Http/HttpOperationBinding.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Runtime.Serialization; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using NJsonSchema; - -namespace Saunter.AsyncApiSchema.v2.Bindings.Http -{ - /// - /// See: https://github.com/asyncapi/bindings/tree/master/http#Operation-Binding-Object - /// - public class HttpOperationBinding - { - /// - /// Required. Type of operation. Its value MUST be either request or response. - /// - [JsonProperty("type")] - public HttpOperationBindingType Type { get; set; } - - /// - /// When type is request, this is the HTTP method, otherwise it MUST be ignored. - /// Its value MUST be one of GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, CONNECT, and TRACE. - /// - [JsonProperty("method", NullValueHandling = NullValueHandling.Ignore)] - public HttpOperationBindingMethod? Method { get; set; } - - /// - /// A Schema object containing the definitions for each query parameter. - /// This schema MUST be of type object and have a properties key. - /// - [JsonProperty("query", NullValueHandling = NullValueHandling.Ignore)] - public JsonSchema Query { get; set; } - - /// - /// The version of this binding. If omitted, "latest" MUST be assumed. - /// - [JsonProperty("bindingVersion", NullValueHandling = NullValueHandling.Ignore)] - public string BindingVersion { get; set; } - } - - - [JsonConverter(typeof(StringEnumConverter))] - public enum HttpOperationBindingType - { - [EnumMember(Value = "request")] - Request, - - [EnumMember(Value = "response")] - Response, - } - - [JsonConverter(typeof(StringEnumConverter))] - public enum HttpOperationBindingMethod - { - [EnumMember(Value = "GET")] - GET, - - [EnumMember(Value = "POST")] - POST, - - [EnumMember(Value = "PUT")] - PUT, - - [EnumMember(Value = "PATCH")] - PATCH, - - [EnumMember(Value = "DELETE")] - DELETE, - - [EnumMember(Value = "HEAD")] - HEAD, - - [EnumMember(Value = "OPTIONS")] - OPTIONS, - - [EnumMember(Value = "CONNECT")] - CONNECT, - - [EnumMember(Value = "TRACE")] - TRACE, - } - -} diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/Http/HttpServerBinding.cs b/src/Saunter/AsyncApiSchema/v2/Bindings/Http/HttpServerBinding.cs deleted file mode 100644 index 83044b9a..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/Http/HttpServerBinding.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Saunter.AsyncApiSchema.v2.Bindings.Http -{ - /// - /// See: https://github.com/asyncapi/bindings/tree/master/http#server-binding-object - /// This object MUST NOT contain any properties. Its name is reserved for future use. - /// - public class HttpServerBinding - { - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/Http/README.md b/src/Saunter/AsyncApiSchema/v2/Bindings/Http/README.md deleted file mode 100644 index 15545dc3..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/Http/README.md +++ /dev/null @@ -1 +0,0 @@ -https://github.com/asyncapi/bindings/blob/master/http/README.md \ No newline at end of file diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/Kafka/KafkaChannelBinding.cs b/src/Saunter/AsyncApiSchema/v2/Bindings/Kafka/KafkaChannelBinding.cs deleted file mode 100644 index fb30e634..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/Kafka/KafkaChannelBinding.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Saunter.AsyncApiSchema.v2.Bindings.Kafka -{ - /// - /// This object MUST NOT contain any properties. Its name is reserved for future use. - /// - /// - /// See: https://github.com/asyncapi/bindings/tree/master/kafka#channel-binding-object - /// - public class KafkaChannelBinding - { - } -} - diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/Kafka/KafkaMessageBinding.cs b/src/Saunter/AsyncApiSchema/v2/Bindings/Kafka/KafkaMessageBinding.cs deleted file mode 100644 index 5ea8e05e..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/Kafka/KafkaMessageBinding.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Newtonsoft.Json; -using NJsonSchema; - -namespace Saunter.AsyncApiSchema.v2.Bindings.Kafka -{ - /// - /// See: https://github.com/asyncapi/bindings/tree/master/kafka#Message-binding-object - /// - public class KafkaMessageBinding - { - /// - /// The message key. - /// - [JsonProperty("key", NullValueHandling = NullValueHandling.Ignore)] - public JsonSchema Key { get; set; } - - /// - /// The version of this binding. If omitted, "latest" MUST be assumed. - /// - [JsonProperty("bindingVersion", NullValueHandling = NullValueHandling.Ignore)] - public string BindingVersion { get; set; } - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/Kafka/KafkaOperationBinding.cs b/src/Saunter/AsyncApiSchema/v2/Bindings/Kafka/KafkaOperationBinding.cs deleted file mode 100644 index fc4748e9..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/Kafka/KafkaOperationBinding.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; -using NJsonSchema; - -namespace Saunter.AsyncApiSchema.v2.Bindings.Kafka -{ - /// - /// See: https://github.com/asyncapi/bindings/tree/master/kafka#Operation-binding-object - /// - public class KafkaOperationBinding - { - /// - /// Id of the consumer group. - /// - [JsonProperty("groupId", NullValueHandling = NullValueHandling.Ignore)] - public JsonSchema GroupId { get; set; } - - /// - /// Id of the consumer inside a consumer group. - /// - [JsonProperty("clientId", NullValueHandling = NullValueHandling.Ignore)] - public JsonSchema ClientId { get; set; } - - /// - /// The version of this binding. If omitted, "latest" MUST be assumed. - /// - [JsonProperty("bindingVersion", NullValueHandling = NullValueHandling.Ignore)] - public string BindingVersion { get; set; } - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/Kafka/KafkaServerBinding.cs b/src/Saunter/AsyncApiSchema/v2/Bindings/Kafka/KafkaServerBinding.cs deleted file mode 100644 index d000026b..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/Kafka/KafkaServerBinding.cs +++ /dev/null @@ -1,13 +0,0 @@ - -namespace Saunter.AsyncApiSchema.v2.Bindings.Kafka -{ - /// - /// This object MUST NOT contain any properties. Its name is reserved for future use. - /// - /// - /// See: https://github.com/asyncapi/bindings/tree/master/kafka#server-binding-object - /// - public class KafkaServerBinding - { - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/Kafka/README.md b/src/Saunter/AsyncApiSchema/v2/Bindings/Kafka/README.md deleted file mode 100644 index cbdb29b7..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/Kafka/README.md +++ /dev/null @@ -1 +0,0 @@ -https://github.com/asyncapi/bindings/blob/master/kafka/README.md \ No newline at end of file diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/MessageBindings.cs b/src/Saunter/AsyncApiSchema/v2/Bindings/MessageBindings.cs deleted file mode 100644 index ab4d1037..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/MessageBindings.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Newtonsoft.Json; -using Saunter.AsyncApiSchema.v2.Bindings.Amqp; -using Saunter.AsyncApiSchema.v2.Bindings.Http; -using Saunter.AsyncApiSchema.v2.Bindings.Kafka; -using Saunter.AsyncApiSchema.v2.Bindings.Mqtt; -using Saunter.AsyncApiSchema.v2.Bindings.Pulsar; - -namespace Saunter.AsyncApiSchema.v2.Bindings -{ - /// - /// MessageBindings can be either a the bindings or a reference to the bindings. - /// - public interface IMessageBindings { } - - /// - /// A reference to the MessageBindings within the AsyncAPI components. - /// - public class MessageBindingsReference : Reference, IMessageBindings - { - public MessageBindingsReference(string id) : base(id, "#/components/messageBindings/{0}") { } - } - - public class MessageBindings : IMessageBindings - { - [JsonProperty("amqp", NullValueHandling = NullValueHandling.Ignore)] - public AmqpMessageBinding Amqp { get; set; } - - [JsonProperty("http", NullValueHandling = NullValueHandling.Ignore)] - public HttpMessageBinding Http { get; set; } - - [JsonProperty("kafka", NullValueHandling = NullValueHandling.Ignore)] - public KafkaMessageBinding Kafka { get; set; } - - [JsonProperty("mqtt", NullValueHandling = NullValueHandling.Ignore)] - public MqttMessageBinding Mqtt { get; set; } - - [JsonProperty("pulsar", NullValueHandling = NullValueHandling.Ignore)] - public PulsarMessageBinding Pulsar { get; set; } - - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/Mqtt/MqttChannelBinding.cs b/src/Saunter/AsyncApiSchema/v2/Bindings/Mqtt/MqttChannelBinding.cs deleted file mode 100644 index 099222a8..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/Mqtt/MqttChannelBinding.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Saunter.AsyncApiSchema.v2.Bindings.Mqtt -{ - /// - /// This object MUST NOT contain any properties. Its name is reserved for future use. - /// - /// - /// See: https://github.com/asyncapi/bindings/blob/master/mqtt/README.md#channel-binding-object - /// - public class MqttChannelBinding - { - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/Mqtt/MqttMessageBinding.cs b/src/Saunter/AsyncApiSchema/v2/Bindings/Mqtt/MqttMessageBinding.cs deleted file mode 100644 index ac5eab33..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/Mqtt/MqttMessageBinding.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Newtonsoft.Json; - -namespace Saunter.AsyncApiSchema.v2.Bindings.Mqtt -{ - /// - /// See: https://github.com/asyncapi/bindings/blob/master/mqtt/README.md#message-binding-object - /// - public class MqttMessageBinding - { - /// - /// The version of this binding. If omitted, "latest" MUST be assumed. - /// - [JsonProperty("bindingVersion", NullValueHandling = NullValueHandling.Ignore)] - public string BindingVersion { get; set; } - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/Mqtt/MqttOperationBinding.cs b/src/Saunter/AsyncApiSchema/v2/Bindings/Mqtt/MqttOperationBinding.cs deleted file mode 100644 index 140694f4..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/Mqtt/MqttOperationBinding.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Newtonsoft.Json; - -namespace Saunter.AsyncApiSchema.v2.Bindings.Mqtt -{ - /// - /// See: https://github.com/asyncapi/bindings/blob/master/mqtt/README.md#operation-binding-object - /// - public class MqttOperationBinding - { - /// - /// Defines the Quality of Service (QoS) levels for the message flow between client and server. - /// Its value MUST be either 0 (At most once delivery), 1 (At least once delivery), or 2 (Exactly once delivery). - /// - [JsonProperty("qos", NullValueHandling = NullValueHandling.Ignore)] - public int? Qos { get; set; } - - /// - /// Whether the broker should retain the message or not. - /// - [JsonProperty("retain", NullValueHandling = NullValueHandling.Ignore)] - public bool? Retain { get; set; } - - /// - /// The version of this binding. If omitted, "latest" MUST be assumed. - /// - [JsonProperty("bindingVersion", NullValueHandling = NullValueHandling.Ignore)] - public string BindingVersion { get; set; } - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/Mqtt/MqttServerBinding.cs b/src/Saunter/AsyncApiSchema/v2/Bindings/Mqtt/MqttServerBinding.cs deleted file mode 100644 index 930c5a46..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/Mqtt/MqttServerBinding.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Newtonsoft.Json; - -namespace Saunter.AsyncApiSchema.v2.Bindings.Mqtt -{ - /// - /// See: https://github.com/asyncapi/bindings/blob/master/mqtt/README.md#server-binding-object - /// - public class MqttServerBinding - { - /// - /// The client identifier. - /// - [JsonProperty("clientId", NullValueHandling = NullValueHandling.Ignore)] - public string ClientId { get; set; } - - /// - /// Whether to create a persistent connection or not. When false, the connection will be persistent. - /// - [JsonProperty("cleanSession", NullValueHandling = NullValueHandling.Ignore)] - public bool? CleanSession { get; set; } - - /// - /// Last Will and Testament configuration. - /// - [JsonProperty("lastWill", NullValueHandling = NullValueHandling.Ignore)] - public MqttServerBindingLastWill LastWill { get; set; } - - /// - /// Interval in seconds of the longest period of time the broker and the client can endure without sending a message. - /// - [JsonProperty("keepAlive", NullValueHandling = NullValueHandling.Ignore)] - public int? KeepAlive { get; set; } - - /// - /// The version of this binding. If omitted, "latest" MUST be assumed. - /// - [JsonProperty("bindingVersion", NullValueHandling = NullValueHandling.Ignore)] - public string BindingVersion { get; set; } - } - - public class MqttServerBindingLastWill - { - /// - /// The topic where the Last Will and Testament message will be sent. - /// - [JsonProperty("topic", NullValueHandling = NullValueHandling.Ignore)] - public string Topic { get; set; } - - /// - /// Defines how hard the broker/client will try to ensure that the Last Will and Testament message is received. Its value MUST be either 0, 1 or 2. - /// - [JsonProperty("qos", NullValueHandling = NullValueHandling.Ignore)] - public int? Qos { get; set; } - - /// - /// Last Will message. - /// - [JsonProperty("message", NullValueHandling = NullValueHandling.Ignore)] - public string Message { get; set; } - - /// - /// Whether the broker should retain the Last Will and Testament message or not. - /// - [JsonProperty("retain", NullValueHandling = NullValueHandling.Ignore)] - public bool? Retain { get; set; } - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/Mqtt/README.md b/src/Saunter/AsyncApiSchema/v2/Bindings/Mqtt/README.md deleted file mode 100644 index 60122760..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/Mqtt/README.md +++ /dev/null @@ -1 +0,0 @@ -https://github.com/asyncapi/bindings/blob/master/mqtt/README.md \ No newline at end of file diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/OperationBindings.cs b/src/Saunter/AsyncApiSchema/v2/Bindings/OperationBindings.cs deleted file mode 100644 index 87035f94..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/OperationBindings.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Newtonsoft.Json; -using Saunter.AsyncApiSchema.v2.Bindings.Amqp; -using Saunter.AsyncApiSchema.v2.Bindings.Http; -using Saunter.AsyncApiSchema.v2.Bindings.Kafka; -using Saunter.AsyncApiSchema.v2.Bindings.Mqtt; -using Saunter.AsyncApiSchema.v2.Bindings.Pulsar; - -namespace Saunter.AsyncApiSchema.v2.Bindings -{ - /// - /// OperationBindings can be either a the bindings or a reference to the bindings. - /// - public interface IOperationBindings { } - - /// - /// A reference to the OperationBindings within the AsyncAPI components. - /// - public class OperationBindingsReference : Reference, IOperationBindings - { - public OperationBindingsReference(string id) : base(id, "#/components/operationBindings/{0}") { } - } - - public class OperationBindings : IOperationBindings - { - [JsonProperty("http", NullValueHandling = NullValueHandling.Ignore)] - public HttpOperationBinding Http { get; set; } - - [JsonProperty("amqp", NullValueHandling = NullValueHandling.Ignore)] - public AmqpOperationBinding Amqp { get; set; } - - [JsonProperty("kafka", NullValueHandling = NullValueHandling.Ignore)] - public KafkaOperationBinding Kafka { get; set; } - - [JsonProperty("mqtt", NullValueHandling = NullValueHandling.Ignore)] - public MqttOperationBinding Mqtt { get; set; } - - [JsonProperty("pulsar", NullValueHandling = NullValueHandling.Ignore)] - public PulsarOperationBinding Pulsar { get; set; } - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/Pulsar/PulsarChannelBinding.cs b/src/Saunter/AsyncApiSchema/v2/Bindings/Pulsar/PulsarChannelBinding.cs deleted file mode 100644 index d46a8c7d..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/Pulsar/PulsarChannelBinding.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Collections.Generic; -using System.Runtime.Serialization; -using Newtonsoft.Json; - -namespace Saunter.AsyncApiSchema.v2.Bindings.Pulsar -{ - /// - /// See: https://github.com/asyncapi/bindings/tree/master/pulsar#channel-binding-object - /// - public class PulsarChannelBinding - { - /// - /// The version of this binding. If omitted, "latest" MUST be assumed. - /// - [JsonProperty("bindingVersion", NullValueHandling = NullValueHandling.Ignore)] - public string BindingVersion { get; set; } - - /// - /// The namespace associated with the topic. - /// - [JsonProperty("namespace", NullValueHandling = NullValueHandling.Ignore)] - public string Namespace { get; set; } - - /// - /// persistence of the topic in Pulsar persistent or non-persistent. - /// - [JsonProperty("persistence", NullValueHandling = NullValueHandling.Ignore)] - public Persistence? Persistence { get; set; } - - /// - /// Topic compaction threshold given in bytes. - /// - [JsonProperty("compaction", NullValueHandling = NullValueHandling.Ignore)] - public int? Compaction { get; set; } - - /// - /// A list of clusters the topic is replicated to. - /// - [JsonProperty("geoReplication", NullValueHandling = NullValueHandling.Ignore)] - public IList GeoReplication { get; set; } - - /// - /// Topic retention policy. - /// - [JsonProperty("retention", NullValueHandling = NullValueHandling.Ignore)] - public RetentionDefinition Retention { get; set; } - - /// - /// Message Time-to-live in seconds. - /// - [JsonProperty("ttl", NullValueHandling = NullValueHandling.Ignore)] - public int? TTL { get; set; } - - /// - /// When Message deduplication is enabled, it ensures that each message produced on Pulsar topics is persisted to disk only once. - /// - [JsonProperty("deduplication", NullValueHandling = NullValueHandling.Ignore)] - public bool? Deduplication { get; set; } - } - - public enum Persistence - { - [EnumMember(Value = "persistent")] - Persistent, - [EnumMember(Value = "non-persistent")] - NonPersistent, - } - - public class RetentionDefinition - { - /// - /// Time given in Minutes. 0 = Disable message retention (by default). - /// - [JsonProperty("time", NullValueHandling = NullValueHandling.Ignore)] - public int Time { get; set; } - - /// - /// Size given in MegaBytes. 0 = Disable message retention (by default). - /// - [JsonProperty("size", NullValueHandling = NullValueHandling.Ignore)] - public int Size { get; set; } - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/Pulsar/PulsarMessageBinding.cs b/src/Saunter/AsyncApiSchema/v2/Bindings/Pulsar/PulsarMessageBinding.cs deleted file mode 100644 index d8342c1d..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/Pulsar/PulsarMessageBinding.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Saunter.AsyncApiSchema.v2.Bindings.Pulsar -{ - /// - /// This object MUST NOT contain any properties. Its name is reserved for future use. - /// - /// - /// See: https://github.com/asyncapi/bindings/blob/master/pulsar/README.md#message-binding-object - /// - public class PulsarMessageBinding - { - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/Pulsar/PulsarOperationBinding.cs b/src/Saunter/AsyncApiSchema/v2/Bindings/Pulsar/PulsarOperationBinding.cs deleted file mode 100644 index 48755b70..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/Pulsar/PulsarOperationBinding.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Saunter.AsyncApiSchema.v2.Bindings.Pulsar -{ - /// - /// This object MUST NOT contain any properties. Its name is reserved for future use. - /// - /// - /// See: https://github.com/asyncapi/bindings/blob/master/pulsar/README.md#operation-binding-object - /// - public class PulsarOperationBinding - { - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/Pulsar/PulsarServerBinding.cs b/src/Saunter/AsyncApiSchema/v2/Bindings/Pulsar/PulsarServerBinding.cs deleted file mode 100644 index af7b3c0c..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/Pulsar/PulsarServerBinding.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Newtonsoft.Json; - -namespace Saunter.AsyncApiSchema.v2.Bindings.Pulsar -{ - /// - /// See: https://github.com/asyncapi/bindings/tree/master/pulsar#server-binding-object - /// - public class PulsarServerBinding - { - /// - /// The pulsar tenant. If omitted, "public" must be assumed. - /// - [JsonProperty("tenant", NullValueHandling = NullValueHandling.Ignore)] - public string Tenant { get; set; } - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/Pulsar/README.md b/src/Saunter/AsyncApiSchema/v2/Bindings/Pulsar/README.md deleted file mode 100644 index 263c8573..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/Pulsar/README.md +++ /dev/null @@ -1 +0,0 @@ -https://github.com/asyncapi/bindings/blob/master/pulsar/README.md \ No newline at end of file diff --git a/src/Saunter/AsyncApiSchema/v2/Bindings/ServerBindings.cs b/src/Saunter/AsyncApiSchema/v2/Bindings/ServerBindings.cs deleted file mode 100644 index 4d5e996b..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Bindings/ServerBindings.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Newtonsoft.Json; -using Saunter.AsyncApiSchema.v2.Bindings.Amqp; -using Saunter.AsyncApiSchema.v2.Bindings.Http; -using Saunter.AsyncApiSchema.v2.Bindings.Kafka; -using Saunter.AsyncApiSchema.v2.Bindings.Mqtt; -using Saunter.AsyncApiSchema.v2.Bindings.Pulsar; - -namespace Saunter.AsyncApiSchema.v2.Bindings -{ - /// - /// ServerBindings can be either a the bindings or a reference to the bindings. - /// - public interface IServerBindings { } - - /// - /// A reference to the OperationBindings within the AsyncAPI components. - /// - public class ServerBindingsReference : Reference, IServerBindings - { - public ServerBindingsReference(string id) : base(id, "#/components/serverBindings/{0}") { } - } - - public class ServerBindings : IServerBindings - { - [JsonProperty("amqp", NullValueHandling = NullValueHandling.Ignore)] - public AmqpServerBinding Amqp { get; set; } - - [JsonProperty("http", NullValueHandling = NullValueHandling.Ignore)] - public HttpServerBinding Http { get; set; } - - [JsonProperty("kafka", NullValueHandling = NullValueHandling.Ignore)] - public KafkaServerBinding Kafka { get; set; } - - [JsonProperty("mqtt", NullValueHandling = NullValueHandling.Ignore)] - public MqttServerBinding Mqtt { get; set; } - - [JsonProperty("pulsar", NullValueHandling = NullValueHandling.Ignore)] - public PulsarServerBinding Pulsar { get; set; } - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/ChannelItem.cs b/src/Saunter/AsyncApiSchema/v2/ChannelItem.cs deleted file mode 100644 index ca5455e3..00000000 --- a/src/Saunter/AsyncApiSchema/v2/ChannelItem.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel; -using Newtonsoft.Json; -using Saunter.AsyncApiSchema.v2.Bindings; - -namespace Saunter.AsyncApiSchema.v2 -{ - /// - /// Describes the operations available on a single channel. - /// - public class ChannelItem - { - /// - /// An optional description of this channel item. - /// CommonMark syntax can be used for rich text representation. - /// - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public string Description { get; set; } - - /// - /// A definition of the SUBSCRIBE operation. - /// - [JsonProperty("subscribe", NullValueHandling = NullValueHandling.Ignore)] - public Operation Subscribe { get; set; } - - /// - /// A definition of the PUBLISH operation. - /// - [JsonProperty("publish", NullValueHandling = NullValueHandling.Ignore)] - public Operation Publish { get; set; } - - /// - /// A map of the parameters included in the channel name. - /// It SHOULD be present only when using channels with expressions - /// (as defined by RFC 6570 section 2.2). - /// - [JsonProperty("parameters", DefaultValueHandling = DefaultValueHandling.Ignore)] - public IDictionary Parameters { get; set; } = new Dictionary(); - - /// - /// A free-form map where the keys describe the name of the protocol - /// and the values describe protocol-specific definitions for the channel. - /// - [JsonProperty("bindings", NullValueHandling = NullValueHandling.Ignore)] - public IChannelBindings Bindings { get; set; } - - /// - /// The servers on which this channel is available, specified as an optional unordered - /// list of names (string keys) of Server Objects defined in the Servers Object (a map). - /// If servers is absent or empty then this channel must be available on all servers - /// defined in the Servers Object. - /// - [JsonProperty("servers", NullValueHandling = NullValueHandling.Ignore)] - public List Servers { get; set; } = new List(); - - public bool ShouldSerializeParameters() - { - return Parameters != null && Parameters.Count > 0; - } - - public bool ShouldSerializeServers() - { - return Servers != null && Servers.Count > 0; - } - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Components.cs b/src/Saunter/AsyncApiSchema/v2/Components.cs deleted file mode 100644 index ccff4467..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Components.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; -using NJsonSchema; -using Saunter.AsyncApiSchema.v2.Bindings; -using Saunter.AsyncApiSchema.v2.Traits; - -namespace Saunter.AsyncApiSchema.v2 -{ - public class Components : ICloneable - { - /// - /// An object to hold reusable Schema Objects. - /// - [JsonProperty("schemas", DefaultValueHandling = DefaultValueHandling.Ignore)] - public IDictionary Schemas { get; set; } = new Dictionary(); - - /// - /// An object to hold reusable Message Objects. - /// - [JsonProperty("messages", DefaultValueHandling = DefaultValueHandling.Ignore)] - public IDictionary Messages { get; set; } = new Dictionary(); - - /// - /// An object to hold reusable Security Scheme Objects. - /// - [JsonProperty("securitySchemes", DefaultValueHandling = DefaultValueHandling.Ignore)] - public IDictionary SecuritySchemes { get; set; } = new Dictionary(); - - /// - /// An object to hold reusable Parameter Objects. - /// - [JsonProperty("parameters", DefaultValueHandling = DefaultValueHandling.Ignore)] - public IDictionary Parameters { get; set; } = new Dictionary(); - - /// - /// An object to hold reusable Correlation ID Objects. - /// - [JsonProperty("correlationIds", DefaultValueHandling = DefaultValueHandling.Ignore)] - public IDictionary CorrelationIds { get; set; } = new Dictionary(); - - /// - /// An object to hold reusable Server Binding Objects. - /// - [JsonProperty("serverBindings", DefaultValueHandling = DefaultValueHandling.Ignore)] - public IDictionary ServerBindings { get; set; } = new Dictionary(); - - /// - /// An object to hold reusable Channel Binding Objects. - /// - [JsonProperty("channelBindings", DefaultValueHandling = DefaultValueHandling.Ignore)] - public IDictionary ChannelBindings { get; set; } = new Dictionary(); - - /// - /// An object to hold reusable Operation Binding Objects. - /// - [JsonProperty("operationBindings", DefaultValueHandling = DefaultValueHandling.Ignore)] - public IDictionary OperationBindings { get; set; } = new Dictionary(); - - /// - /// An object to hold reusable Message Binding Objects. - /// - [JsonProperty("messageBindings", DefaultValueHandling = DefaultValueHandling.Ignore)] - public IDictionary MessageBindings { get; set; } = new Dictionary(); - - - /// - /// An object to hold reusable Operation Trait Objects. - /// - [JsonProperty("operationTraits", DefaultValueHandling = DefaultValueHandling.Ignore)] - public IDictionary OperationTraits { get; set; } = new Dictionary(); - - /// - /// An object to hold reusable Message Trait Objects. - /// - [JsonProperty("messageTraits", DefaultValueHandling = DefaultValueHandling.Ignore)] - public IDictionary MessageTraits { get; set; } = new Dictionary(); - - - - public bool ShouldSerializeMessageTraits() - { - return MessageTraits != null && MessageTraits.Count > 0; - } - - public bool ShouldSerializeOperationTraits() - { - return OperationTraits != null && OperationTraits.Count > 0; - } - - public bool ShouldSerializeMessageBindings() - { - return MessageBindings != null && MessageBindings.Count > 0; - } - - public bool ShouldSerializeOperationBindings() - { - return OperationBindings != null && OperationBindings.Count > 0; - } - - public bool ShouldSerializeChannelBindings() - { - return ChannelBindings != null && ChannelBindings.Count > 0; - } - - public bool ShouldSerializeServerBindings() - { - return ServerBindings != null && ServerBindings.Count > 0; - } - - public bool ShouldSerializeCorrelationIds() - { - return CorrelationIds != null && CorrelationIds.Count > 0; - } - - public bool ShouldSerializeParameters() - { - return Parameters != null && Parameters.Count > 0; - } - - public bool ShouldSerializeSecuritySchemes() - { - return SecuritySchemes != null && SecuritySchemes.Count > 0; - } - - public bool ShouldSerializeMessages() - { - return Messages != null && Messages.Count > 0; - } - - public bool ShouldSerializeSchemas() - { - return Schemas != null && Schemas.Count > 0; - } - - object ICloneable.Clone() - { - return Clone(); - } - - public Components Clone() - { - var clone = new Components(); - - clone.Schemas = Schemas.ToDictionary(p => p.Key, p => p.Value); - clone.Messages = Messages.ToDictionary(p => p.Key, p => p.Value); - clone.SecuritySchemes = SecuritySchemes.ToDictionary(p => p.Key, p => p.Value); - clone.Parameters = Parameters.ToDictionary(p => p.Key, p => p.Value); - clone.CorrelationIds = CorrelationIds.ToDictionary(p => p.Key, p => p.Value); - clone.ServerBindings = ServerBindings.ToDictionary(p => p.Key, p => p.Value); - clone.ChannelBindings = ChannelBindings.ToDictionary(p => p.Key, p => p.Value); - clone.OperationBindings = OperationBindings.ToDictionary(p => p.Key, p => p.Value); - clone.MessageBindings = MessageBindings.ToDictionary(p => p.Key, p => p.Value); - clone.OperationTraits = OperationTraits.ToDictionary(p => p.Key, p => p.Value); - clone.MessageTraits = MessageTraits.ToDictionary(p => p.Key, p => p.Value); - - return clone; - } - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/CorrelationId.cs b/src/Saunter/AsyncApiSchema/v2/CorrelationId.cs deleted file mode 100644 index eaed5070..00000000 --- a/src/Saunter/AsyncApiSchema/v2/CorrelationId.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace Saunter.AsyncApiSchema.v2 -{ - /// - /// Can be either a or a to a CorrelationId. - /// - public interface ICorrelationId { } - - /// - /// A reference to a CorrelationId within the AsyncAPI components. - /// - public class CorrelationIdReference : Reference, ICorrelationId - { - public CorrelationIdReference(string id) : base(id, "#/components/correlationIds/{0}") { } - } - - - public class CorrelationId : ICorrelationId - { - public CorrelationId(string location) - { - Location = location ?? throw new ArgumentNullException(nameof(location)); - } - - /// - /// An optional description of the identifier. - /// CommonMark syntax can be used for rich text representation. - /// - [JsonProperty("description")] - public string Description { get; set; } - - /// - /// A runtime expression that specifies the location of the correlation ID. - /// - [JsonProperty("location")] - public string Location { get; } - - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/ExternalDocumentation.cs b/src/Saunter/AsyncApiSchema/v2/ExternalDocumentation.cs deleted file mode 100644 index ba0e54c8..00000000 --- a/src/Saunter/AsyncApiSchema/v2/ExternalDocumentation.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace Saunter.AsyncApiSchema.v2 -{ - public class ExternalDocumentation - { - public ExternalDocumentation(string url) - { - Url = url ?? throw new ArgumentNullException(nameof(url)); - } - - /// - /// 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. - /// Value MUST be in the format of a URL. - /// - [JsonProperty("url")] - public string Url { get; } - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Info.cs b/src/Saunter/AsyncApiSchema/v2/Info.cs deleted file mode 100644 index a73e86f1..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Info.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace Saunter.AsyncApiSchema.v2 -{ - public class Info - { - public Info(string title, string version) - { - Title = title ?? throw new ArgumentNullException(nameof(title)); - Version = version ?? throw new ArgumentNullException(nameof(version)); - } - - /// - /// The title of the application. - /// - [JsonProperty("title", NullValueHandling = NullValueHandling.Ignore)] - public string Title { get; } - - /// - /// Provides the version of the application API - /// (not to be confused with the specification version). - /// - [JsonProperty("version", NullValueHandling = NullValueHandling.Ignore)] - public string Version { get; } - - /// - /// A short description of the application. - /// CommonMark syntax can be used for rich text representation. - /// - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public string Description { get; set; } - - /// - /// A URL to the Terms of Service for the API - /// MUST be in the format of a URL. - /// - [JsonProperty("termsOfService", NullValueHandling = NullValueHandling.Ignore)] - public string TermsOfService { get; set; } - - /// - /// The contact information for the exposed API. - /// - [JsonProperty("contact", NullValueHandling = NullValueHandling.Ignore)] - public Contact Contact { get; set; } - - /// - /// The license information for the exposed API. - /// - [JsonProperty("license", NullValueHandling = NullValueHandling.Ignore)] - public License License { get; set; } - } - - public class Contact - { - /// - /// The identifying name of the contact person/organization. - /// - [JsonProperty("name")] - public string Name { get; set; } - - /// - /// The URL pointing to the contact information. - /// MUST be in the format of a URL. - /// - [JsonProperty("url")] - public string Url { get; set; } - - /// - /// The email address of the contact person/organization. - /// MUST be in the format of an email address. - /// - [JsonProperty("email")] - public string Email { get; set; } - } - - public class License - { - public License(string name) - { - Name = name; - } - - /// - /// The license name used for the API. - /// - [JsonProperty("name")] - public string Name { get; } - - /// - /// A URL to the license used for the API. - /// MUST be in the format of a URL. - /// - [JsonProperty("url")] - public string Url { get; set; } - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Message.cs b/src/Saunter/AsyncApiSchema/v2/Message.cs deleted file mode 100644 index 4d86b61f..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Message.cs +++ /dev/null @@ -1,171 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; -using NJsonSchema; -using Saunter.AsyncApiSchema.v2.Bindings; -using Saunter.AsyncApiSchema.v2.Traits; - -namespace Saunter.AsyncApiSchema.v2 -{ - /// - /// Message can either be a list of messages, a single message, or a reference to a message. - /// - public interface IMessage { } - - /// - /// A reference to a Message within the AsyncAPI components. - /// - public class MessageReference : Reference, IMessage - { - public MessageReference(string id) : base(id, "#/components/messages/{0}") { } - } - - public class Messages : IMessage - { - [JsonProperty("oneOf")] - public List OneOf { get; set; } = new List(); - } - - public class Message : IMessage - { - /// - /// Unique string used to identify the message. The id MUST be unique among all messages - /// described in the API. The messageId value is case-sensitive. Tools and libraries MAY - /// use the messageId to uniquely identify a message, therefore, it is RECOMMENDED to - /// follow common programming naming conventions. - /// - [JsonProperty("messageId", NullValueHandling = NullValueHandling.Ignore)] - public string MessageId { get; set; } - - /// - /// Schema definition of the application headers. Schema MUST be of type “object”. - /// It MUST NOT define the protocol headers. - /// - [JsonProperty("headers", NullValueHandling = NullValueHandling.Ignore)] - public JsonSchema Headers { get; set; } - - /// - /// Definition of the message payload. It can be of any type but defaults to Schema object. - /// - [JsonProperty("payload", NullValueHandling = NullValueHandling.Ignore)] - public JsonSchema Payload { get; set; } - - /// - /// Definition of the correlation ID used for message tracing or matching. - /// - [JsonProperty("correlationId", NullValueHandling = NullValueHandling.Ignore)] - public ICorrelationId CorrelationId { get; set; } - - /// - /// A string containing the name of the schema format used to define the message payload. - /// If omitted, implementations should parse the payload as a Schema object. - /// - [JsonProperty("schemaFormat", NullValueHandling = NullValueHandling.Ignore)] - public string SchemaFormat { get; set; } - - /// - /// The content type to use when encoding/decoding a message’s payload. - /// The value MUST be a specific media type (e.g. application/json). - /// - [JsonProperty("contentType", NullValueHandling = NullValueHandling.Ignore)] - public string ContentType { get; set; } - - /// - /// A machine-friendly name for the message. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; set; } - - /// - /// A human-friendly title for the message. - /// - [JsonProperty("title", NullValueHandling = NullValueHandling.Ignore)] - public string Title { get; set; } - - /// - /// A short summary of what the message is about. - /// - [JsonProperty("summary", NullValueHandling = NullValueHandling.Ignore)] - public string Summary { get; set; } - - /// - /// A verbose explanation of the message. CommonMark syntax can be used for rich text representation. - /// - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public string Description { get; set; } - - /// - /// A list of tags for API documentation control. Tags can be used for logical grouping of messages. - /// - [JsonProperty("tags", DefaultValueHandling = DefaultValueHandling.Ignore)] - public ISet Tags { get; set; } = new HashSet(); - - public bool ShouldSerializeTags() - { - return Tags != null && Tags.Count > 0; - } - - /// - /// Additional external documentation for this message. - /// - [JsonProperty("externalDocs", NullValueHandling = NullValueHandling.Ignore)] - public ExternalDocumentation ExternalDocs { get; set; } - - /// - /// A free-form map where the keys describe the name of the protocol and the values describe protocol-specific definitions for the message. - /// - [JsonProperty("bindings", NullValueHandling = NullValueHandling.Ignore)] - public IMessageBindings Bindings { get; set; } - - /// - /// An array with examples of valid message objects. - /// - [JsonProperty("examples")] - public IList Examples { get; set; } = new List(); - - public bool ShouldSerializeExamples() - { - return Examples != null && Examples.Count > 0; - } - - /// - /// A list of traits to apply to the message object. - /// Traits MUST be merged into the message object using the JSON Merge Patch algorithm in the same order they are defined here. - /// The resulting object MUST be a valid Message Object. - /// - [JsonProperty("traits")] - public IList Traits { get; set; } = new List(); - - public bool ShouldSerializeTraits() - { - return Traits != null && Traits.Count > 0; - } - } - - - public class MessageExample - { - /// - /// A machine friendly name for the example. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; set; } - - /// - /// A short summary of what the example is about. - /// - [JsonProperty("summary", NullValueHandling = NullValueHandling.Ignore)] - public string Summary { get; set; } - - /// - /// Example of headers that will be included in the message. - /// - [JsonProperty("headers", NullValueHandling = NullValueHandling.Ignore)] - public object Headers { get; set; } - - /// - /// Example message payload. - /// - [JsonProperty("payload", NullValueHandling = NullValueHandling.Ignore)] - public object Payload { get; set; } - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/OAuthFlows.cs b/src/Saunter/AsyncApiSchema/v2/OAuthFlows.cs deleted file mode 100644 index 123b160d..00000000 --- a/src/Saunter/AsyncApiSchema/v2/OAuthFlows.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace Saunter.AsyncApiSchema.v2 -{ - public class OAuthFlows - { - [JsonProperty("implicit")] - public OAuthFlow Implicit { get; set; } - - [JsonProperty("password")] - public OAuthFlow Password { get; set; } - - [JsonProperty("clientCredentials")] - public OAuthFlow ClientCredentials { get; set; } - - [JsonProperty("authorizationCode")] - public OAuthFlow AuthorizationCode { get; set; } - } - - public class OAuthFlow - { - [JsonProperty("authorizationUrl")] - public string AuthorizationUrl { get; set; } - - [JsonProperty("tokenUrl")] - public string TokenUrl { get; set; } - - [JsonProperty("refreshUrl")] - public string RefreshUrl { get; set; } - - [JsonProperty("scopes")] - public IDictionary Scopes { get; set; } = new Dictionary(); - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Operation.cs b/src/Saunter/AsyncApiSchema/v2/Operation.cs deleted file mode 100644 index 4530bfb6..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Operation.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; -using Saunter.AsyncApiSchema.v2.Bindings; -using Saunter.AsyncApiSchema.v2.Traits; - -namespace Saunter.AsyncApiSchema.v2 -{ - /// - /// Describes a publish or a subscribe operation. - /// This provides a place to document how and why messages are sent and received. - /// - public class Operation - { - /// - /// Unique string used to identify the operation. - /// The id MUST be unique among all operations described in the API. - /// The operationId value is case-sensitive. - /// Tools and libraries MAY use the operationId to uniquely identify an operation, - /// therefore, it is RECOMMENDED to follow common programming naming conventions. - /// - [JsonProperty("operationId", NullValueHandling = NullValueHandling.Ignore)] - public string OperationId { get; set; } - - /// - /// A short summary of what the operation is about. - /// - [JsonProperty("summary", NullValueHandling = NullValueHandling.Ignore)] - public string Summary { get; set; } - - /// - /// A verbose explanation of the operation. - /// CommonMark syntax can be used for rich text representation. - /// - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public string Description { get; set; } - - /// - /// A list of tags for API documentation control. Tags can be used for logical grouping of operations. - /// - [JsonProperty("tags", DefaultValueHandling = DefaultValueHandling.Ignore)] - public ISet Tags { get; set; } = new HashSet(); - - public bool ShouldSerializeTags() - { - return Tags != null && Tags.Count > 0; - } - - /// - /// Additional external documentation for this operation. - /// - [JsonProperty("externalDocs", NullValueHandling = NullValueHandling.Ignore)] - public ExternalDocumentation ExternalDocs { get; set; } - - /// - /// A free-form map where the keys describe the name of the protocol and the values describe - /// protocol-specific definitions for the operation. - /// - [JsonProperty("bindings", NullValueHandling = NullValueHandling.Ignore)] - public IOperationBindings Bindings { get; set; } - - /// - /// A definition of the message that will be published or received on this channel. - /// oneOf is allowed here to specify multiple messages, however, a message MUST be - /// valid only against one of the referenced message objects. - /// - [JsonProperty("message")] - public IMessage Message { get; set; } - - /// - /// A list of traits to apply to the operation object. Traits MUST be merged into the operation - /// object using the JSON Merge Patch algorithm in the same order they are defined here. - /// - [JsonProperty("traits", DefaultValueHandling = DefaultValueHandling.Ignore)] - public IList Traits { get; set; } = new List(); - - - public bool ShouldSerializeTraits() - { - return Traits != null && Traits.Count > 0; - } - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Parameter.cs b/src/Saunter/AsyncApiSchema/v2/Parameter.cs deleted file mode 100644 index 8df06c95..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Parameter.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Newtonsoft.Json; -using NJsonSchema; - -namespace Saunter.AsyncApiSchema.v2 -{ - /// - /// Can be either a or a reference to a parameter. - /// - public interface IParameter { } - - /// - /// A reference to a Parameter within the AsyncAPI components. - /// - public class ParameterReference : Reference, IParameter - { - private readonly AsyncApiDocument _document; - public ParameterReference(string id, AsyncApiDocument document) : base(id, "#/components/parameters/{0}") - { - _document = document; - } - - [JsonIgnore] - public Parameter Value => _document.Components.Parameters[Id]; - } - - public class Parameter : IParameter - { - /// - /// A verbose explanation of the parameter. - /// CommonMark syntax can be used for rich text representation. - /// - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public string Description { get; set; } - - - /// - /// Definition of the parameter. - /// - [JsonProperty("schema")] - public JsonSchema Schema { get; set; } - - /// - /// A runtime expression that specifies the location of the parameter value. - /// Even when a definition for the target field exists, it MUST NOT be used to validate - /// this parameter but, instead, the schema property MUST be used. - /// - [JsonProperty("location", NullValueHandling = NullValueHandling.Ignore)] - public string Location { get; set; } - - [JsonIgnore] - public string Name { get; set; } - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Reference.cs b/src/Saunter/AsyncApiSchema/v2/Reference.cs deleted file mode 100644 index 7a7cd58f..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Reference.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using Newtonsoft.Json; -using Saunter.AsyncApiSchema.v2.Traits; - -namespace Saunter.AsyncApiSchema.v2 -{ - /// - /// A reference to some other object within the AsyncAPI document. - /// - public class Reference - { - public Reference(string id, string path) - { - _id = id ?? throw new ArgumentNullException(nameof(id)); - _path = path ?? throw new ArgumentNullException(nameof(path)); - } - - private readonly string _id; - private readonly string _path; - - [JsonProperty("$ref")] - public string Ref => string.Format(_path, _id); - - [JsonIgnore()] - public string Id => _id; - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/SecurityScheme.cs b/src/Saunter/AsyncApiSchema/v2/SecurityScheme.cs deleted file mode 100644 index d577b045..00000000 --- a/src/Saunter/AsyncApiSchema/v2/SecurityScheme.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System.Runtime.Serialization; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; - -namespace Saunter.AsyncApiSchema.v2 -{ - public class SecurityScheme - { - public SecurityScheme(SecuritySchemeType type) - { - Type = type; - } - - /// - /// The type of the security scheme. - /// - [JsonProperty("type")] - public SecuritySchemeType Type { get; } - - /// - /// A short description for security scheme. - /// CommonMark syntax MAY be used for rich text representation. - /// - [JsonProperty("description")] - public string Description { get; set; } - - /// - /// The name of the header, query or cookie parameter to be used. - /// - [JsonProperty("name")] - public string Name { get; set; } - - /// - /// The location of the API key. - /// Valid values are "user" and "password" for apiKey and "query", "header" or "cookie" for httpApiKey. - /// - [JsonProperty("in")] - public string In { get; set; } - - /// - /// The name of the HTTP Authorization scheme to be used in the Authorization header as defined in RFC7235. - /// - [JsonProperty("scheme")] - public string Scheme { get; set; } - - /// - /// A hint to the client to identify how the bearer token is formatted. Bearer tokens are usually generated - /// by an authorization server, so this information is primarily for documentation purposes. - /// - [JsonProperty("bearerFormat")] - public string BearerFormat { get; set; } - - /// - /// An object containing configuration information for the flow types supported. - /// - [JsonProperty("flows")] - public OAuthFlows Flows { get; set; } - - /// - /// OpenId Connect URL to discover OAuth2 configuration values. This MUST be in the form of a URL. - /// - [JsonProperty("openIdConnectUrl")] - public string OpenIdConnectUrl { get; set; } - } - - [JsonConverter(typeof(StringEnumConverter))] - public enum SecuritySchemeType - { - [EnumMember(Value = "userPassword")] - UserPassword, - - [EnumMember(Value = "apiKey")] - ApiKey, - - [EnumMember(Value = "X509")] - X509, - - [EnumMember(Value = "symmetricEncryption")] - SymmetricEncryption, - - [EnumMember(Value = "asymmetricEncryption")] - AsymmetricEncryption, - - [EnumMember(Value = "httpApiKey")] - HttpApiKey, - - [EnumMember(Value = "http")] - Http, - - [EnumMember(Value = "oauth2")] - OAuth2, - - [EnumMember(Value = "openIdConnect")] - OpenIdConnect, - - [EnumMember(Value = "plain")] - Plain, - - [EnumMember(Value = "scramSha256")] - ScramSha256, - - [EnumMember(Value = "scramSha512")] - ScramSha512, - - [EnumMember(Value = "gssapi")] - GSSAPI, - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Server.cs b/src/Saunter/AsyncApiSchema/v2/Server.cs deleted file mode 100644 index 982fdaa4..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Server.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; -using Saunter.AsyncApiSchema.v2.Bindings; - -namespace Saunter.AsyncApiSchema.v2 -{ - public class Server - { - public Server(string url, string protocol) - { - Url = url ?? throw new ArgumentNullException(nameof(url)); - Protocol = protocol ?? throw new ArgumentNullException(nameof(protocol)); - } - - /// - /// A URL to the target host. - /// This URL supports Server Variables and MAY be relative, to indicate that the host - /// location is relative to the location where the AsyncAPI document is being served. - /// Variable substitutions will be made when a variable is named in { brackets }. - /// - [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] - public string Url { get; } - - /// - /// The protocol this URL supports for connection. - /// Supported protocol include, but are not limited to: amqp, amqps, http, https, - /// jms, kafka, kafka-secure, mqtt, secure-mqtt, stomp, stomps, ws, wss. - /// - [JsonProperty("protocol", NullValueHandling = NullValueHandling.Ignore)] - public string Protocol { get; } - - /// - /// The version of the protocol used for connection. - /// For instance: AMQP 0.9.1, HTTP 2.0, Kafka 1.0.0, etc. - /// - [JsonProperty("protocolVersion", NullValueHandling = NullValueHandling.Ignore)] - public string ProtocolVersion { get; set; } - - /// - /// An optional string describing the host designated by the URL. - /// CommonMark syntax MAY be used for rich text representation. - /// - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public string Description { get; set; } - - /// - /// A map between a variable name and its value. - /// The value is used for substitution in the server's URL template. - /// - [JsonProperty("variables", NullValueHandling = NullValueHandling.Ignore)] - public IDictionary Variables { get; set; } - - /// - /// A declaration of which security mechanisms can be used with this server. - /// The list of values includes alternative security requirement objects - /// that can be used. Only one of the security requirement objects need to - /// be satisfied to authorize a connection or operation. - /// - [JsonProperty("security", NullValueHandling = NullValueHandling.Ignore)] - public IList>> Security { get; set; } - - /// - /// A free-form map where the keys describe the name of the protocol and the - /// values describe protocol-specific definitions for the server. - /// - [JsonProperty("bindings", NullValueHandling = NullValueHandling.Ignore)] - public IServerBindings Bindings { get; set; } - } - - public class ServerVariable - { - /// - /// An enumeration of string values to be used if the substitution options are from a limited set. - /// - [JsonProperty("enum", NullValueHandling = NullValueHandling.Ignore)] - public IList Enum { get; set; } - - /// - /// The default value to use for substitution, and to send, if an alternate value is not supplied. - /// - [JsonProperty("default", NullValueHandling = NullValueHandling.Ignore)] - public string Default { get; set; } - - /// - /// An optional description for the server variable. - /// CommonMark syntax MAY be used for rich text representation. - /// - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public string Description { get; set; } - - /// - /// An array of examples of the server variable. - /// - [JsonProperty("examples", NullValueHandling = NullValueHandling.Ignore)] - public IList Examples { get; set; } - } - - public static class ServerProtocol - { - public const string Amqp = "amqp"; - - public const string Amqps = "amqps"; - - public const string Http = "http"; - - public const string Https = "https"; - - public const string Jms = "jms"; - - public const string Kafka = "kafka"; - - public const string KafkaSecure = "kafka-secure"; - - public const string Mqtt = "mqtt"; - - public const string SecureMqtt = "secure-mqtt"; - - public const string Stomp = "stomp"; - - public const string Stomps = "stomps"; - - public const string Ws = "ws"; - - public const string Wss = "wss"; - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Tag.cs b/src/Saunter/AsyncApiSchema/v2/Tag.cs deleted file mode 100644 index 714cedc3..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Tag.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace Saunter.AsyncApiSchema.v2 -{ - public class Tag - { - public Tag(string name) - { - Name = name ?? throw new ArgumentNullException(nameof(name)); - } - - /// - /// The name of the tag. - /// - [JsonProperty("name")] - public string Name { get; } - - /// - /// A short description for the tag. CommonMark syntax can be used for rich text representation. - /// - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public string Description { get; set; } - - /// - /// Additional external documentation for this tag. - /// - [JsonProperty("externalDocs", NullValueHandling = NullValueHandling.Ignore)] - public ExternalDocumentation ExternalDocs { get; set; } - - public static implicit operator Tag(string s) - { - return new Tag(s); - } - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Traits/MessageTrait.cs b/src/Saunter/AsyncApiSchema/v2/Traits/MessageTrait.cs deleted file mode 100644 index 4831eb5c..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Traits/MessageTrait.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; -using NJsonSchema; -using Saunter.AsyncApiSchema.v2.Bindings; - -namespace Saunter.AsyncApiSchema.v2.Traits -{ - /// - /// Can be either a or reference to a message trait. - /// - public interface IMessageTrait { } - - /// - /// A reference to a MessageTrait within the AsyncAPI components. - /// - public class MessageTraitReference : Reference, IMessageTrait - { - public MessageTraitReference(string id) : base(id, "#/components/messageTraits/{0}") { } - } - - public class MessageTrait : IMessageTrait - { - /// - /// Schema definition of the application headers. - /// Schema MUST be of type "object". It MUST NOT define the protocol headers. - /// - [JsonProperty("headers")] - public JsonSchema Headers { get; set; } - - /// - /// Definition of the correlation ID used for message tracing or matching. - /// - [JsonProperty("correlationId")] - public ICorrelationId CorrelationId { get; set; } - - /// - /// A string containing the name of the schema format/language used to define - /// the message payload. If omitted, implementations should parse the payload as a Schema object. - /// - [JsonProperty("schemaFormat")] - public string SchemaFormat { get; set; } - - /// - /// The content type to use when encoding/decoding a message's payload. - /// The value MUST be a specific media type (e.g. application/json). - /// When omitted, the value MUST be the one specified on the defaultContentType field. - /// - [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. - /// CommonMark syntax can be used for rich text representation. - /// - [JsonProperty("description")] - public string Description { get; set; } - - /// - /// A list of tags for API documentation control. - /// Tags can be used for logical grouping of messages. - /// - [JsonProperty("tags")] - public ISet Tags { get; set; } - - /// - /// Additional external documentation for this message. - /// - [JsonProperty("externalDocs")] - public ExternalDocumentation ExternalDocs { get; set; } - - /// - /// A free-form map where the keys describe the name of the protocol - /// and the values describe protocol-specific definitions for the message. - /// - [JsonProperty("bindings")] - public IMessageBindings Bindings { get; set; } - - /// - /// An array with examples of valid message objects. - /// - [JsonProperty("examples")] - public IList> Examples { get; set; } - } -} diff --git a/src/Saunter/AsyncApiSchema/v2/Traits/OperationTrait.cs b/src/Saunter/AsyncApiSchema/v2/Traits/OperationTrait.cs deleted file mode 100644 index 36f1b9da..00000000 --- a/src/Saunter/AsyncApiSchema/v2/Traits/OperationTrait.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; -using Saunter.AsyncApiSchema.v2.Bindings; - -namespace Saunter.AsyncApiSchema.v2.Traits -{ - /// - /// Can be either an or an reference to an operation trait. - /// - public interface IOperationTrait { } - - /// - /// A reference to an OperationTrait within the AsyncAPI components. - /// - public class OperationTraitReference : Reference, IOperationTrait - { - public OperationTraitReference(string id) : base(id, "#/components/operationTraits/{0}") { } - } - - public class OperationTrait : IOperationTrait - { - /// - /// Unique string used to identify the operation. - /// The id MUST be unique among all operations described in the API. - /// The operationId value is case-sensitive. - /// Tools and libraries MAY use the operationId to uniquely identify an operation, - /// therefore, it is RECOMMENDED to follow common programming naming conventions. - /// - [JsonProperty("operationId", NullValueHandling = NullValueHandling.Ignore)] - public string OperationId { get; set; } - - /// - /// A short summary of what the operation is about. - /// - [JsonProperty("summary", NullValueHandling = NullValueHandling.Ignore)] - public string Summary { get; set; } - - /// - /// A verbose explanation of the operation. - /// CommonMark syntax can be used for rich text representation. - /// - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public string Description { get; set; } - - /// - /// A list of tags for API documentation control. Tags can be used for logical grouping of operations. - /// - [JsonProperty("tags", NullValueHandling = NullValueHandling.Ignore)] - public ISet Tags { get; set; } - - /// - /// Additional external documentation for this operation. - /// - [JsonProperty("externalDocs", NullValueHandling = NullValueHandling.Ignore)] - public ExternalDocumentation ExternalDocs { get; set; } - - /// - /// A free-form map where the keys describe the name of the protocol and the values describe protocol-specific definitions for the operation. - /// - [JsonProperty("bindings", NullValueHandling = NullValueHandling.Ignore)] - public IOperationBindings Bindings { get; set; } - } -} diff --git a/src/Saunter/AsyncApiServiceCollectionExtensions.cs b/src/Saunter/AsyncApiServiceCollectionExtensions.cs index 288001cb..234dfecb 100644 --- a/src/Saunter/AsyncApiServiceCollectionExtensions.cs +++ b/src/Saunter/AsyncApiServiceCollectionExtensions.cs @@ -1,60 +1,65 @@ using System; - -using Microsoft.Extensions.DependencyInjection; +using LEGO.AsyncAPI.Models; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Saunter.AttributeProvider; +using Saunter.Options; +using Saunter.SharedKernel; +using Saunter.SharedKernel.Interfaces; + +namespace Saunter +{ + public static class AsyncApiServiceCollectionExtensions + { + /// + /// Add required services for AsyncAPI schema generation to the service collection. + /// + /// The collection to add services to. + /// An action used to configure the AsyncAPI options. + /// The service collection so additional calls can b e chained. + public static IServiceCollection AddAsyncApiSchemaGeneration(this IServiceCollection services, Action? setupAction = null) + { + services.AddOptions(); + + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.TryAddTransient(); + + if (setupAction != null) + { + services.Configure(setupAction); + } + + return services; + } + + /// + /// Add a named AsyncAPI document to the service collection. + /// + /// The collection to add the document to. + /// The name used to refer to the document. Used in the and in middleware HTTP paths. + /// An action used to configure the named document. + /// The service collection so additional calls can be chained. + public static IServiceCollection ConfigureNamedAsyncApi(this IServiceCollection services, string documentName, Action setupAction) + { + services.Configure(options => + { + if (options.Middleware.Route == null + || !options.Middleware.Route.ToLower().Contains("{document}") + || options.Middleware.UiBaseRoute == null + || !options.Middleware.UiBaseRoute.ToLower().Contains("{document}")) + { + options.Middleware.Route = "/asyncapi/{document}/asyncapi.json"; + options.Middleware.UiBaseRoute = "/asyncapi/{document}/ui/"; + } + + var document = options.NamedApis.GetOrAdd(documentName, _ => new AsyncApiDocument()); -using Saunter.AsyncApiSchema.v2; -using Saunter.Generation; -using Saunter.Serialization; - -namespace Saunter -{ - public static class AsyncApiServiceCollectionExtensions - { - /// - /// Add required services for AsyncAPI schema generation to the service collection. - /// - /// The collection to add services to. - /// An action used to configure the AsyncAPI options. - /// The service collection so additional calls can b e chained. - public static IServiceCollection AddAsyncApiSchemaGeneration(this IServiceCollection services, Action setupAction = null) - { - services.AddOptions(); - - services.TryAddTransient(); - services.TryAddTransient(); - services.TryAddTransient(); - - if (setupAction != null) services.Configure(setupAction); - - return services; - } - - /// - /// Add a named AsyncAPI document to the service collection. - /// - /// The collection to add the document to. - /// The name used to refer to the document. Used in the and in middleware HTTP paths. - /// An action used to configure the named document. - /// The service collection so additional calls can be chained. - public static IServiceCollection ConfigureNamedAsyncApi(this IServiceCollection services, string documentName, Action setupAction) - { - services.Configure(options => - { - if (options.Middleware.Route == null - || !options.Middleware.Route.ToLower().Contains("{document}") - || options.Middleware.UiBaseRoute == null - || !options.Middleware.UiBaseRoute.ToLower().Contains("{document}")) - { - options.Middleware.Route = "/asyncapi/{document}/asyncapi.json"; - options.Middleware.UiBaseRoute = "/asyncapi/{document}/ui/"; - } - - var document = options.NamedApis.GetOrAdd(documentName, _ => new AsyncApiDocument() { DocumentName = documentName }); - - setupAction(document); - }); - return services; - } - } + setupAction(document); + }); + return services; + } + } } diff --git a/src/Saunter/AttributeProvider/AttributeDocumentProvider.cs b/src/Saunter/AttributeProvider/AttributeDocumentProvider.cs new file mode 100644 index 00000000..fbe7dbb1 --- /dev/null +++ b/src/Saunter/AttributeProvider/AttributeDocumentProvider.cs @@ -0,0 +1,498 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using LEGO.AsyncAPI.Models; +using Microsoft.Extensions.DependencyInjection; +using Namotion.Reflection; +using Saunter.AttributeProvider.Attributes; +using Saunter.Options; +using Saunter.Options.Filters; +using Saunter.SharedKernel.Interfaces; + +namespace Saunter.AttributeProvider +{ + internal class AttributeDocumentProvider : IAsyncApiDocumentProvider + { + private readonly IServiceProvider _serviceProvider; + private readonly IAsyncApiSchemaGenerator _schemaGenerator; + private readonly IAsyncApiChannelUnion _channelUnion; + private readonly IAsyncApiDocumentCloner _cloner; + + public AttributeDocumentProvider(IServiceProvider serviceProvider, IAsyncApiSchemaGenerator schemaGenerator, IAsyncApiChannelUnion channelUnion, IAsyncApiDocumentCloner cloner) + { + _serviceProvider = serviceProvider; + _schemaGenerator = schemaGenerator; + _channelUnion = channelUnion; + _cloner = cloner; + } + + public AsyncApiDocument GetDocument(string? documentName, AsyncApiOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + var asyncApiTypes = GetAsyncApiTypes(options, documentName); + + var apiNamePair = options.NamedApis.FirstOrDefault(c => c.Value.Id == documentName); + + var clone = _cloner.CloneProtype(apiNamePair.Value ?? options.AsyncApi); + + 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); + + foreach (var filterType in options.DocumentFilters) + { + var filter = (IDocumentFilter)_serviceProvider.GetRequiredService(filterType); + filter?.Apply(clone, filterContext); + } + + return clone; + } + + private IEnumerable> GenerateChannelsFromMethods(AsyncApiComponents components, AsyncApiOptions options, TypeInfo[] asyncApiTypes) + { + var methodsWithChannelAttribute = asyncApiTypes + .SelectMany(type => type.DeclaredMethods) + .Select(method => new + { + Channel = method.GetCustomAttribute(), + Method = method, + }) + .Where(mc => mc.Channel != null); + + foreach (var item in methodsWithChannelAttribute) + { + if (item.Channel == null) + { + continue; + } + + var channelItem = new AsyncApiChannel + { + Servers = item.Channel.Servers?.ToList(), + Description = item.Channel.Description, + 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() + { + Reference = new() + { + Id = item.Channel.BindingsRef, + Type = ReferenceType.ChannelBindings, + } + } + : null, + }; + + ApplyChannelFilters(options, item.Method, item.Channel, channelItem); + + yield return new(item.Channel.Name, channelItem); + } + } + + private IEnumerable> GenerateChannelsFromClasses(AsyncApiComponents components, AsyncApiOptions options, TypeInfo[] asyncApiTypes) + { + var classesWithChannelAttribute = asyncApiTypes + .Select(type => new + { + Channel = type.GetCustomAttribute(), + Type = type, + }) + .Where(cc => cc.Channel != null); + + foreach (var item in classesWithChannelAttribute) + { + if (item.Channel == null) + { + continue; + } + + var channelItem = new AsyncApiChannel + { + Description = item.Channel.Description, + 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() + { + Reference = new() + { + Id = item.Channel.BindingsRef, + Type = ReferenceType.ChannelBindings, + } + } + : null, + }; + + ApplyChannelFilters(options, item.Type, item.Channel, channelItem); + + yield return new(item.Channel.Name, channelItem); + } + } + + private void ApplyChannelFilters(AsyncApiOptions options, MemberInfo member, ChannelAttribute channel, AsyncApiChannel channelItem) + { + var context = new ChannelFilterContext(member, channel); + + foreach (var filterType in options.ChannelFilters) + { + var filter = (IChannelFilter)_serviceProvider.GetRequiredService(filterType); + filter.Apply(channelItem, context); + } + } + + private IDictionary GetChannelParametersFromAttributes(AsyncApiComponents components, MemberInfo memberInfo) + { + var attributes = memberInfo.GetCustomAttributes(); + var parameters = new Dictionary(attributes.Count()); + + 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 = schema, + }; + + components.Parameters.Add(parameterId, parameter); + } + + parameters.Add(parameterId, new() + { + Reference = new() + { + Id = parameterId, + Type = ReferenceType.Parameter, + } + }); + } + + return parameters; + } + + private AsyncApiOperation? GenerateOperationFromMethod(AsyncApiComponents components, MethodInfo method, OperationType operationType, AsyncApiOptions options) + { + var operationAttribute = GetOperationAttribute(method, operationType); + + if (operationAttribute == null) + { + return null; + } + + var messageAttributes = method.GetCustomAttributes(); + + var tags = operationAttribute + .Tags? + .Select(x => new AsyncApiTag { Name = x }) + .ToList() ?? new List(); + + var description = operationAttribute.Description ?? + (method.GetXmlDocsRemarks() != string.Empty + ? method.GetXmlDocsRemarks() + : null); + + var bindings = operationAttribute.BindingsRef != null + ? new AsyncApiBindings() + { + Reference = new() + { + Id = operationAttribute.BindingsRef, + Type = ReferenceType.OperationBindings, + } + } + : null; + + var operation = new AsyncApiOperation + { + Tags = tags, + Description = description, + Message = new List(), + OperationId = operationAttribute.OperationId ?? method.Name, + Summary = operationAttribute.Summary ?? method.GetXmlDocsSummary(), + Bindings = bindings, + }; + + if (messageAttributes.Any()) + { + operation.Message = GenerateMessageFromAttributes(components, messageAttributes); + } + else if (operationAttribute.MessagePayloadType is not null) + { + operation.Message = GenerateMessageFromType(components, operationAttribute.MessagePayloadType.GetTypeInfo()); + } + + ApplyOprationFilters(method, options, operationAttribute, operation); + + return operation; + } + + private AsyncApiOperation? GenerateOperationFromClass(AsyncApiComponents components, TypeInfo type, OperationType operationType) + { + var operationAttribute = GetOperationAttribute(type, operationType); + + if (operationAttribute == null) + { + return null; + } + + var messages = new List(); + + var tags = operationAttribute + .Tags? + .Select(x => new AsyncApiTag() { Name = x }) + .ToList() ?? new List(); + + var operation = new AsyncApiOperation + { + Tags = tags, + Message = messages, + OperationId = operationAttribute.OperationId ?? type.Name, + Summary = operationAttribute.Summary ?? type.GetXmlDocsSummary(), + Description = operationAttribute.Description ?? (type.GetXmlDocsRemarks() != "" ? type.GetXmlDocsRemarks() : null), + Bindings = operationAttribute.BindingsRef != null + ? new() + { + Reference = new() + { + Id = operationAttribute.BindingsRef, + Type = ReferenceType.OperationBindings + } + } + : null, + }; + + var attributes = type + .DeclaredMethods + .Select(m => new + { + MessageAttributes = m.GetCustomAttributes(), + Method = m, + }) + .Where(x => x.MessageAttributes.Any()) + .SelectMany(x => x.MessageAttributes); + + foreach (var attribute in attributes) + { + var message = GenerateMessageFromAttribute(components, attribute); + + if (message != null) + { + messages.Add(message); + } + } + + return operation; + } + + private static OperationAttribute? GetOperationAttribute(MemberInfo typeOrMethod, OperationType operationType) + { + return operationType switch + { + OperationType.Publish => typeOrMethod.GetCustomAttribute(), + OperationType.Subscribe => typeOrMethod.GetCustomAttribute(), + _ => null, + }; + } + + 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(components, messageAttributes.First()); + + if (message is not null) + { + messages.Add(message); + } + + return messages; + } + + foreach (var attribute in messageAttributes) + { + var message = GenerateMessageFromAttribute(components, attribute); + + if (message != null) + { + messages.Add(message); + } + } + + return messages; + } + + private List GenerateMessageFromType(AsyncApiComponents components, TypeInfo payloadType) + { + var asyncApiSchema = GetAsyncApiSchema(components, payloadType); + + var messageId = asyncApiSchema?.Title; + + if (messageId is null) + { + return new(); + } + + if (!components.Messages.ContainsKey(messageId)) + { + var message = new AsyncApiMessage + { + Payload = asyncApiSchema, + MessageId = messageId, + Name = messageId, + Title = messageId, + }; + + components.Messages.Add(messageId, message); + } + + return new() + { + new() + { + Reference = new() + { + Id = messageId, + Type = ReferenceType.Message, + } + } + }; + } + + private AsyncApiMessage? GenerateMessageFromAttribute(AsyncApiComponents components, MessageAttribute messageAttribute) + { + if (messageAttribute?.PayloadType == null) + { + return null; + } + + var bodySchema = GetAsyncApiSchema(components, (TypeInfo)messageAttribute.PayloadType); + + var messageId = messageAttribute.MessageId ?? bodySchema?.Title ?? messageAttribute.PayloadType.Name; + + if (!components.Messages.ContainsKey(messageId)) + { + var tags = messageAttribute.Tags? + .Select(x => new AsyncApiTag { Name = x }) + .ToList() ?? new List(); + + var headersSchema = GetAsyncApiSchema(components, (TypeInfo?)messageAttribute.HeadersType); + + var message = new AsyncApiMessage + { + 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() + { + 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; + + 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) + { + var asyncApiTypes = options + .AsyncApiSchemaTypes + .Where(t => t.GetCustomAttribute()?.DocumentName == apiName) + .ToArray(); + + return asyncApiTypes; + } + } +} diff --git a/src/Saunter/Attributes/AsyncApiAttribute.cs b/src/Saunter/AttributeProvider/Attributes/AsyncApiAttribute.cs similarity index 68% rename from src/Saunter/Attributes/AsyncApiAttribute.cs rename to src/Saunter/AttributeProvider/Attributes/AsyncApiAttribute.cs index b92b3baf..a191edca 100644 --- a/src/Saunter/Attributes/AsyncApiAttribute.cs +++ b/src/Saunter/AttributeProvider/Attributes/AsyncApiAttribute.cs @@ -1,6 +1,6 @@ using System; -namespace Saunter.Attributes +namespace Saunter.AttributeProvider.Attributes { /// /// Marks a class or interface as containing asyncapi channels. @@ -8,9 +8,9 @@ namespace Saunter.Attributes [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)] public sealed class AsyncApiAttribute : Attribute { - public string DocumentName { get; } + public string? DocumentName { get; } - public AsyncApiAttribute(string documentName = null) + public AsyncApiAttribute(string? documentName = null) { DocumentName = documentName; } diff --git a/src/Saunter/Attributes/ChannelAttribute.cs b/src/Saunter/AttributeProvider/Attributes/ChannelAttribute.cs similarity index 87% rename from src/Saunter/Attributes/ChannelAttribute.cs rename to src/Saunter/AttributeProvider/Attributes/ChannelAttribute.cs index de8939d8..48432150 100644 --- a/src/Saunter/Attributes/ChannelAttribute.cs +++ b/src/Saunter/AttributeProvider/Attributes/ChannelAttribute.cs @@ -1,6 +1,6 @@ using System; -namespace Saunter.Attributes +namespace Saunter.AttributeProvider.Attributes { [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Interface)] public class ChannelAttribute : Attribute @@ -16,13 +16,13 @@ public class ChannelAttribute : Attribute /// An optional description of this channel item. /// CommonMark syntax can be used for rich text representation. /// - public string Description { get; set; } + public string? Description { get; set; } /// /// The name of a channel bindings item to reference. /// The bindings must be added to components/channelBindings with the same name. /// - public string BindingsRef { get; set; } + public string? BindingsRef { get; set; } /// /// The servers on which this channel is available, specified as an optional unordered @@ -35,6 +35,7 @@ public class ChannelAttribute : Attribute public ChannelAttribute(string name) { Name = name ?? throw new ArgumentNullException(nameof(name)); + Servers = Array.Empty(); } } } diff --git a/src/Saunter/Attributes/ChannelParameterAttribute.cs b/src/Saunter/AttributeProvider/Attributes/ChannelParameterAttribute.cs similarity index 78% rename from src/Saunter/Attributes/ChannelParameterAttribute.cs rename to src/Saunter/AttributeProvider/Attributes/ChannelParameterAttribute.cs index aab0c95a..1bc5fc79 100644 --- a/src/Saunter/Attributes/ChannelParameterAttribute.cs +++ b/src/Saunter/AttributeProvider/Attributes/ChannelParameterAttribute.cs @@ -1,6 +1,6 @@ using System; -namespace Saunter.Attributes +namespace Saunter.AttributeProvider.Attributes { [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true)] public class ChannelParameterAttribute : Attribute @@ -15,8 +15,8 @@ public ChannelParameterAttribute(string name, Type type) public Type Type { get; } - public string Description { get; set; } + public string? Description { get; set; } - public string Location { get; set; } + public string? Location { get; set; } } } diff --git a/src/Saunter/Attributes/MessageAttribute.cs b/src/Saunter/AttributeProvider/Attributes/MessageAttribute.cs similarity index 82% rename from src/Saunter/Attributes/MessageAttribute.cs rename to src/Saunter/AttributeProvider/Attributes/MessageAttribute.cs index 86cf877a..ac218d36 100644 --- a/src/Saunter/Attributes/MessageAttribute.cs +++ b/src/Saunter/AttributeProvider/Attributes/MessageAttribute.cs @@ -1,6 +1,6 @@ using System; -namespace Saunter.Attributes +namespace Saunter.AttributeProvider.Attributes { [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public class MessageAttribute : Attribute @@ -8,6 +8,7 @@ public class MessageAttribute : Attribute public MessageAttribute(Type payloadType) { PayloadType = payloadType; + Tags = Array.Empty(); } public MessageAttribute(Type payloadType, params string[] tags) @@ -24,35 +25,35 @@ public MessageAttribute(Type payloadType, params string[] tags) /// /// The type to use to generate the message headers schema. /// - public Type HeadersType { get; set; } + public Type? HeadersType { get; set; } /// /// A machine-friendly name for the message. /// Defaults to the generated schemaId. /// - public string Name { get; set; } + public string? Name { get; set; } /// /// A human-friendly title for the message. /// - public string Title { get; set; } + public string? Title { get; set; } /// /// A short summary of what the message is about. /// - public string Summary { get; set; } + public string? Summary { get; set; } /// /// A verbose explanation of the message. /// CommonMark syntax can be used for rich text representation. /// - public string Description { get; set; } + public string? Description { get; set; } /// /// The name of a message bindings item to reference. /// The bindings must be added to components/messageBindings with the same name. /// - public string BindingsRef { get; set; } + public string? BindingsRef { get; set; } /// /// Unique string used to identify the message. The id MUST be unique among all messages @@ -60,7 +61,7 @@ public MessageAttribute(Type payloadType, params string[] tags) /// use the messageId to uniquely identify a message, therefore, it is RECOMMENDED to /// follow common programming naming conventions. /// - public string MessageId { get; set; } + public string? MessageId { get; set; } /// /// A list of tags for API documentation control. Tags can be used for logical grouping of messages. diff --git a/src/Saunter/Attributes/OperationAttribute.cs b/src/Saunter/AttributeProvider/Attributes/OperationAttribute.cs similarity index 87% rename from src/Saunter/Attributes/OperationAttribute.cs rename to src/Saunter/AttributeProvider/Attributes/OperationAttribute.cs index 77f26621..b5bdd6ec 100644 --- a/src/Saunter/Attributes/OperationAttribute.cs +++ b/src/Saunter/AttributeProvider/Attributes/OperationAttribute.cs @@ -1,18 +1,18 @@ using System; -namespace Saunter.Attributes +namespace Saunter.AttributeProvider.Attributes { [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Interface)] public abstract class OperationAttribute : Attribute { public OperationType OperationType { get; protected set; } - public Type MessagePayloadType { get; protected set; } + public Type? MessagePayloadType { get; protected set; } /// /// A short summary of what the operation is about. /// - public string Summary { get; set; } + public string? Summary { get; set; } /// /// Unique string used to identify the operation. @@ -21,24 +21,24 @@ public abstract class OperationAttribute : Attribute /// Tools and libraries MAY use the operationId to uniquely identify an operation, /// therefore, it is RECOMMENDED to follow common programming naming conventions. /// - public string OperationId { get; set; } + public string? OperationId { get; set; } /// /// A verbose explanation of the operation. /// CommonMark syntax can be used for rich text representation. /// - public string Description { get; set; } + public string? Description { get; set; } /// /// The name of an operation bindings item to reference. /// The bindings must be added to components/operationBindings with the same name. /// - public string BindingsRef { get; set; } + public string? BindingsRef { get; set; } /// /// A list of tags for API documentation control. Tags can be used for logical grouping of operations. /// - public string[] Tags { get; protected set; } + public string[] Tags { get; protected set; } = Array.Empty(); } public class PublishOperationAttribute : OperationAttribute diff --git a/src/Saunter/AsyncApiMiddleware.cs b/src/Saunter/DocumentMiddleware/AsyncApiMiddleware.cs similarity index 64% rename from src/Saunter/AsyncApiMiddleware.cs rename to src/Saunter/DocumentMiddleware/AsyncApiMiddleware.cs index eb9169f7..32b24208 100644 --- a/src/Saunter/AsyncApiMiddleware.cs +++ b/src/Saunter/DocumentMiddleware/AsyncApiMiddleware.cs @@ -1,24 +1,23 @@ -using System.Net; +using System.IO; +using System.Net; using System.Threading.Tasks; +using LEGO.AsyncAPI.Models; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; -using Saunter.Serialization; -using Saunter.Utils; +using Saunter.Options; -namespace Saunter +namespace Saunter.DocumentMiddleware { - public class AsyncApiMiddleware + internal class AsyncApiMiddleware { private readonly RequestDelegate _next; private readonly IAsyncApiDocumentProvider _asyncApiDocumentProvider; - private readonly IAsyncApiDocumentSerializer _asyncApiDocumentSerializer; private readonly AsyncApiOptions _options; - public AsyncApiMiddleware(RequestDelegate next, IOptions options, IAsyncApiDocumentProvider asyncApiDocumentProvider, IAsyncApiDocumentSerializer asyncApiDocumentSerializer) + public AsyncApiMiddleware(RequestDelegate next, IOptions options, IAsyncApiDocumentProvider asyncApiDocumentProvider) { _next = next; _asyncApiDocumentProvider = asyncApiDocumentProvider; - _asyncApiDocumentSerializer = asyncApiDocumentSerializer; _options = options.Value; } @@ -30,23 +29,22 @@ public async Task Invoke(HttpContext context) return; } - var prototype = _options.AsyncApi; - if (context.TryGetDocument(out var documentName) && !_options.NamedApis.TryGetValue(documentName, out prototype)) + if (context.TryGetDocument(out var documentName) && !_options.NamedApis.TryGetValue(documentName, out _)) { await _next(context); return; } - var asyncApiSchema = _asyncApiDocumentProvider.GetDocument(_options, prototype); + var asyncApiSchema = _asyncApiDocumentProvider.GetDocument(documentName, _options); - await RespondWithAsyncApiSchemaJson(context.Response, asyncApiSchema, _asyncApiDocumentSerializer); + await RespondWithAsyncApiSchemaJson(context.Response, asyncApiSchema); } - private static async Task RespondWithAsyncApiSchemaJson(HttpResponse response, AsyncApiSchema.v2.AsyncApiDocument asyncApiSchema, IAsyncApiDocumentSerializer asyncApiDocumentSerializer) + private static async Task RespondWithAsyncApiSchemaJson(HttpResponse response, AsyncApiDocument asyncApiSchema) { - var asyncApiSchemaJson = asyncApiDocumentSerializer.Serialize(asyncApiSchema); + var asyncApiSchemaJson = asyncApiSchema.SerializeAsJson(LEGO.AsyncAPI.AsyncApiVersion.AsyncApi2_0); response.StatusCode = (int)HttpStatusCode.OK; - response.ContentType = asyncApiDocumentSerializer.ContentType; + response.ContentType = "application/json"; await response.WriteAsync(asyncApiSchemaJson); } diff --git a/src/Saunter/Utils/RouteMatchingExtensions.cs b/src/Saunter/DocumentMiddleware/RouteMatchingExtensions.cs similarity index 74% rename from src/Saunter/Utils/RouteMatchingExtensions.cs rename to src/Saunter/DocumentMiddleware/RouteMatchingExtensions.cs index 4beda04c..9dc43163 100644 --- a/src/Saunter/Utils/RouteMatchingExtensions.cs +++ b/src/Saunter/DocumentMiddleware/RouteMatchingExtensions.cs @@ -1,10 +1,11 @@ -using Microsoft.AspNetCore.Http; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Template; -namespace Saunter.Utils +namespace Saunter.DocumentMiddleware { - public static class RouteMatchingExtensions + internal static class RouteMatchingExtensions { public static bool IsMatchingRoute(this PathString path, string pattern) { @@ -15,7 +16,7 @@ public static bool IsMatchingRoute(this PathString path, string pattern) return matcher.TryMatch(path, values); } - public static bool TryGetDocument(this HttpContext context, out string document) + public static bool TryGetDocument(this HttpContext context, [MaybeNullWhen(false)] out string document) { var values = context.Request.RouteValues; if (!values.TryGetValue("document", out var value) || value == null) @@ -24,7 +25,7 @@ public static bool TryGetDocument(this HttpContext context, out string document) return false; } - document = value.ToString(); + document = value.ToString() ?? string.Empty; return true; } } diff --git a/src/Saunter/Generation/AsyncApiDocumentProvider.cs b/src/Saunter/Generation/AsyncApiDocumentProvider.cs deleted file mode 100644 index 6bce8fbb..00000000 --- a/src/Saunter/Generation/AsyncApiDocumentProvider.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using Saunter.AsyncApiSchema.v2; -using Saunter.Attributes; - -namespace Saunter.Generation -{ - public class AsyncApiDocumentProvider : IAsyncApiDocumentProvider - { - private readonly IDocumentGenerator _documentGenerator; - private readonly IServiceProvider _serviceProvider; - - public AsyncApiDocumentProvider(IDocumentGenerator documentGenerator, IServiceProvider serviceProvider) - { - _documentGenerator = documentGenerator; - _serviceProvider = serviceProvider; - } - - public AsyncApiDocument GetDocument(AsyncApiOptions options, AsyncApiDocument prototype) - { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - var asyncApiTypes = GetAsyncApiTypes(options, prototype); - - var document = _documentGenerator.GenerateDocument(asyncApiTypes, options, prototype, _serviceProvider); - - return document; - } - - - /// - /// Get all types with an from assemblies . - /// - private static TypeInfo[] GetAsyncApiTypes(AsyncApiOptions options, AsyncApiDocument prototype) - { - var assembliesToScan = options.AssemblyMarkerTypes.Select(t => t.Assembly).Distinct(); - - var asyncApiTypes = assembliesToScan - .SelectMany(a => a.DefinedTypes.Where(t => t.GetCustomAttribute()?.DocumentName == prototype.DocumentName)) - .ToArray(); - - return asyncApiTypes; - } - } -} diff --git a/src/Saunter/Generation/DocumentGenerator.cs b/src/Saunter/Generation/DocumentGenerator.cs deleted file mode 100644 index d19bfe81..00000000 --- a/src/Saunter/Generation/DocumentGenerator.cs +++ /dev/null @@ -1,328 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Microsoft.Extensions.DependencyInjection; -using Namotion.Reflection; -using NJsonSchema.Generation; -using Saunter.AsyncApiSchema.v2; -using Saunter.AsyncApiSchema.v2.Bindings; -using Saunter.Attributes; -using Saunter.Generation.Filters; -using Saunter.Generation.SchemaGeneration; -using Saunter.Utils; - -namespace Saunter.Generation -{ - public class DocumentGenerator : IDocumentGenerator - { - public DocumentGenerator() - { - } - - public AsyncApiSchema.v2.AsyncApiDocument GenerateDocument(TypeInfo[] asyncApiTypes, AsyncApiOptions options, AsyncApiDocument prototype, IServiceProvider serviceProvider) - { - var asyncApiSchema = prototype.Clone(); - - var schemaResolver = new AsyncApiSchemaResolver(asyncApiSchema, options.SchemaOptions); - - var generator = new JsonSchemaGenerator(options.SchemaOptions); - asyncApiSchema.Channels = GenerateChannels(asyncApiTypes, schemaResolver, options, generator, serviceProvider); - - var filterContext = new DocumentFilterContext(asyncApiTypes, schemaResolver, generator); - foreach (var filterType in options.DocumentFilters) - { - var filter = (IDocumentFilter)serviceProvider.GetRequiredService(filterType); - filter?.Apply(asyncApiSchema, filterContext); - } - - return asyncApiSchema; - } - - /// - /// Generate the Channels section of an AsyncApi schema. - /// - private static IDictionary GenerateChannels(TypeInfo[] asyncApiTypes, AsyncApiSchemaResolver schemaResolver, AsyncApiOptions options, JsonSchemaGenerator jsonSchemaGenerator, IServiceProvider serviceProvider) - { - var channels = new Dictionary(); - - channels.AddRange(GenerateChannelsFromMethods(asyncApiTypes, schemaResolver, options, jsonSchemaGenerator, serviceProvider)); - channels.AddRange(GenerateChannelsFromClasses(asyncApiTypes, schemaResolver, options, jsonSchemaGenerator, serviceProvider)); - return channels; - } - - /// - /// Generate the Channels section of the AsyncApi schema from the - /// on methods. - /// - private static IDictionary GenerateChannelsFromMethods(IEnumerable asyncApiTypes, AsyncApiSchemaResolver schemaResolver, AsyncApiOptions options, JsonSchemaGenerator jsonSchemaGenerator, IServiceProvider serviceProvider) - { - var channels = new Dictionary(); - - var methodsWithChannelAttribute = asyncApiTypes - .SelectMany(type => type.DeclaredMethods) - .Select(method => new - { - Channel = method.GetCustomAttribute(), - Method = method, - }) - .Where(mc => mc.Channel != null); - - foreach (var mc in methodsWithChannelAttribute) - { - if (mc.Channel == null) continue; - - var channelItem = new ChannelItem - { - Description = mc.Channel.Description, - Parameters = GetChannelParametersFromAttributes(mc.Method, schemaResolver, jsonSchemaGenerator), - Publish = GenerateOperationFromMethod(mc.Method, schemaResolver, OperationType.Publish, options, jsonSchemaGenerator, serviceProvider), - Subscribe = GenerateOperationFromMethod(mc.Method, schemaResolver, OperationType.Subscribe, options, jsonSchemaGenerator, serviceProvider), - Bindings = mc.Channel.BindingsRef != null ? new ChannelBindingsReference(mc.Channel.BindingsRef) : null, - Servers = mc.Channel.Servers?.ToList(), - }; - channels.AddOrAppend(mc.Channel.Name, channelItem); - - var context = new ChannelItemFilterContext(mc.Method, schemaResolver, jsonSchemaGenerator, mc.Channel); - foreach (var filterType in options.ChannelItemFilters) - { - var filter = (IChannelItemFilter)serviceProvider.GetRequiredService(filterType); - filter.Apply(channelItem, context); - } - } - - return channels; - } - - /// - /// Generate the Channels section of the AsyncApi schema from the - /// on classes. - /// - private static IDictionary GenerateChannelsFromClasses(IEnumerable asyncApiTypes, AsyncApiSchemaResolver schemaResolver, AsyncApiOptions options, JsonSchemaGenerator jsonSchemaGenerator, IServiceProvider serviceProvider) - { - var channels = new Dictionary(); - - var classesWithChannelAttribute = asyncApiTypes - .Select(type => new - { - Channel = type.GetCustomAttribute(), - Type = type, - }) - .Where(cc => cc.Channel != null); - - foreach (var cc in classesWithChannelAttribute) - { - if (cc.Channel == null) continue; - - var channelItem = new ChannelItem - { - Description = cc.Channel.Description, - Parameters = GetChannelParametersFromAttributes(cc.Type, schemaResolver, jsonSchemaGenerator), - Publish = GenerateOperationFromClass(cc.Type, schemaResolver, OperationType.Publish, jsonSchemaGenerator), - Subscribe = GenerateOperationFromClass(cc.Type, schemaResolver, OperationType.Subscribe, jsonSchemaGenerator), - Bindings = cc.Channel.BindingsRef != null ? new ChannelBindingsReference(cc.Channel.BindingsRef) : null, - Servers = cc.Channel.Servers?.ToList(), - }; - - channels.AddOrAppend(cc.Channel.Name, channelItem); - - var context = new ChannelItemFilterContext(cc.Type, schemaResolver, jsonSchemaGenerator, cc.Channel); - foreach (var filterType in options.ChannelItemFilters) - { - var filter = (IChannelItemFilter)serviceProvider.GetRequiredService(filterType); - filter.Apply(channelItem, context); - } - } - - return channels; - } - - /// - /// Generate the an operation of an AsyncApi Channel for the given method. - /// - private static Operation GenerateOperationFromMethod(MethodInfo method, AsyncApiSchemaResolver schemaResolver, OperationType operationType, AsyncApiOptions options, JsonSchemaGenerator jsonSchemaGenerator, IServiceProvider serviceProvider) - { - var operationAttribute = GetOperationAttribute(method, operationType); - if (operationAttribute == null) - { - return null; - } - - IEnumerable messageAttributes = method.GetCustomAttributes(); - var message = messageAttributes.Any() - ? GenerateMessageFromAttributes(messageAttributes, schemaResolver, jsonSchemaGenerator) - : GenerateMessageFromType(operationAttribute.MessagePayloadType, schemaResolver, jsonSchemaGenerator); - - var operation = new Operation - { - OperationId = operationAttribute.OperationId ?? method.Name, - Summary = operationAttribute.Summary ?? method.GetXmlDocsSummary(), - Description = operationAttribute.Description ?? (method.GetXmlDocsRemarks() != "" ? method.GetXmlDocsRemarks() : null), - Message = message, - Bindings = operationAttribute.BindingsRef != null ? new OperationBindingsReference(operationAttribute.BindingsRef) : null, - Tags = new HashSet(operationAttribute.Tags?.Select(x => new Tag(x)) ?? new List()) - }; - - var filterContext = new OperationFilterContext(method, schemaResolver, jsonSchemaGenerator, operationAttribute); - foreach (var filterType in options.OperationFilters) - { - var filter = (IOperationFilter)serviceProvider.GetRequiredService(filterType); - filter?.Apply(operation, filterContext); - } - - return operation; - } - - /// - /// Generate the an operation of an AsyncApi Channel for the given class. - /// - private static Operation GenerateOperationFromClass(TypeInfo type, AsyncApiSchemaResolver schemaResolver, OperationType operationType, JsonSchemaGenerator jsonSchemaGenerator) - { - var operationAttribute = GetOperationAttribute(type, operationType); - if (operationAttribute == null) - { - return null; - } - - var messages = new Messages(); - var operation = new Operation - { - OperationId = operationAttribute.OperationId ?? type.Name, - Summary = operationAttribute.Summary ?? type.GetXmlDocsSummary(), - Description = operationAttribute.Description ?? (type.GetXmlDocsRemarks() != "" ? type.GetXmlDocsRemarks() : null), - Message = messages, - Bindings = operationAttribute.BindingsRef != null ? new OperationBindingsReference(operationAttribute.BindingsRef) : null, - Tags = new HashSet(operationAttribute.Tags?.Select(x => new Tag(x)) ?? new List()) - }; - - var methodsWithMessageAttribute = type.DeclaredMethods - .Select(method => new - { - MessageAttributes = method.GetCustomAttributes(), - Method = method, - }) - .Where(mm => mm.MessageAttributes.Any()); - - foreach (MessageAttribute messageAttribute in methodsWithMessageAttribute.SelectMany(x => x.MessageAttributes)) - { - var message = GenerateMessageFromAttribute(messageAttribute, schemaResolver, jsonSchemaGenerator); - if (message != null) - { - messages.OneOf.Add(message); - } - } - - if (messages.OneOf.Count == 1) - { - operation.Message = messages.OneOf.First(); - } - - return operation; - } - - private static OperationAttribute GetOperationAttribute(MemberInfo typeOrMethod, OperationType operationType) - { - switch (operationType) - { - case OperationType.Publish: - var publishOperationAttribute = typeOrMethod.GetCustomAttribute(); - return (OperationAttribute)publishOperationAttribute; - - case OperationType.Subscribe: - var subscribeOperationAttribute = typeOrMethod.GetCustomAttribute(); - return (OperationAttribute)subscribeOperationAttribute; - - default: - return null; - } - } - - private static IMessage GenerateMessageFromAttributes(IEnumerable messageAttributes, AsyncApiSchemaResolver schemaResolver, JsonSchemaGenerator jsonSchemaGenerator) - { - if (messageAttributes.Count() == 1) - { - return GenerateMessageFromAttribute(messageAttributes.First(), schemaResolver, jsonSchemaGenerator); - } - - var messages = new Messages(); - foreach (MessageAttribute messageAttribute in messageAttributes) - { - var message = GenerateMessageFromAttribute(messageAttribute, schemaResolver, jsonSchemaGenerator); - if (message != null) - { - messages.OneOf.Add(message); - } - } - - if (messages.OneOf.Count == 1) - { - return messages.OneOf.First(); - } - - return messages; - } - - private static IMessage GenerateMessageFromAttribute(MessageAttribute messageAttribute, AsyncApiSchemaResolver schemaResolver, JsonSchemaGenerator jsonSchemaGenerator) - { - if (messageAttribute?.PayloadType == null) - { - return null; - } - - var message = new Message - { - MessageId = messageAttribute.MessageId, - Payload = jsonSchemaGenerator.Generate(messageAttribute.PayloadType, schemaResolver), - Headers = messageAttribute.HeadersType != null ? jsonSchemaGenerator.Generate(messageAttribute.HeadersType, schemaResolver) : null, - Title = messageAttribute.Title, - Summary = messageAttribute.Summary, - Description = messageAttribute.Description, - Bindings = messageAttribute.BindingsRef != null ? new MessageBindingsReference(messageAttribute.BindingsRef) : null, - Tags = new HashSet(messageAttribute.Tags?.Select(x => new Tag(x)) ?? new List()) - }; - message.Name = messageAttribute.Name ?? message.Payload.ActualSchema.Id; - - return schemaResolver.GetMessageOrReference(message); - } - - - private static IMessage GenerateMessageFromType(Type payloadType, AsyncApiSchemaResolver schemaResolver, JsonSchemaGenerator jsonSchemaGenerator) - { - if (payloadType == null) - { - return null; - } - - var message = new Message - { - Payload = jsonSchemaGenerator.Generate(payloadType, schemaResolver), - }; - message.Name = message.Payload.Id; - - return schemaResolver.GetMessageOrReference(message); - } - - private static IDictionary GetChannelParametersFromAttributes(MemberInfo memberInfo, AsyncApiSchemaResolver schemaResolver, JsonSchemaGenerator jsonSchemaGenerator) - { - IEnumerable attributes = memberInfo.GetCustomAttributes(); - var parameters = new Dictionary(); - if (attributes.Any()) - { - foreach (ChannelParameterAttribute attribute in attributes) - { - var parameter = schemaResolver.GetParameterOrReference(new Parameter - { - Description = attribute.Description, - Name = attribute.Name, - Schema = jsonSchemaGenerator.Generate(attribute.Type, schemaResolver), - Location = attribute.Location, - }); - - parameters.Add(attribute.Name, parameter); - } - } - - return parameters; - } - } -} diff --git a/src/Saunter/Generation/Filters/IChannelItemFilter.cs b/src/Saunter/Generation/Filters/IChannelItemFilter.cs deleted file mode 100644 index 7f372331..00000000 --- a/src/Saunter/Generation/Filters/IChannelItemFilter.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Reflection; -using NJsonSchema.Generation; -using Saunter.AsyncApiSchema.v2; -using Saunter.Attributes; - -namespace Saunter.Generation.Filters -{ - public interface IChannelItemFilter - { - void Apply(ChannelItem channelItem, ChannelItemFilterContext context); - } - - public class ChannelItemFilterContext - { - public ChannelItemFilterContext(MemberInfo member, JsonSchemaResolver schemaResolver, JsonSchemaGenerator schemaGenerator, ChannelAttribute channel) - { - Member = member; - SchemaResolver = schemaResolver; - SchemaGenerator = schemaGenerator; - Channel = channel; - } - - public MemberInfo Member { get; } - - public JsonSchemaResolver SchemaResolver { get; } - - public JsonSchemaGenerator SchemaGenerator { get; } - - public ChannelAttribute Channel { get; } - } -} diff --git a/src/Saunter/Generation/Filters/IDocumentFilter.cs b/src/Saunter/Generation/Filters/IDocumentFilter.cs deleted file mode 100644 index 9627c44c..00000000 --- a/src/Saunter/Generation/Filters/IDocumentFilter.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; -using NJsonSchema.Generation; -using Saunter.AsyncApiSchema.v2; -using Saunter.Generation.SchemaGeneration; - -namespace Saunter.Generation.Filters -{ - public interface IDocumentFilter - { - void Apply(AsyncApiDocument document, DocumentFilterContext context); - } - - public class DocumentFilterContext - { - public DocumentFilterContext(IEnumerable asyncApiTypes, JsonSchemaResolver schemaResolver, JsonSchemaGenerator schemaGenerator) - { - AsyncApiTypes = asyncApiTypes; - SchemaResolver = schemaResolver; - SchemaGenerator = schemaGenerator; - } - - public IEnumerable AsyncApiTypes { get; } - - public JsonSchemaResolver SchemaResolver { get; } - - public JsonSchemaGenerator SchemaGenerator { get; } - - } -} diff --git a/src/Saunter/Generation/Filters/IOperationFilter.cs b/src/Saunter/Generation/Filters/IOperationFilter.cs deleted file mode 100644 index d76810ba..00000000 --- a/src/Saunter/Generation/Filters/IOperationFilter.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Reflection; -using NJsonSchema.Generation; -using Saunter.AsyncApiSchema.v2; -using Saunter.Attributes; -using Saunter.Generation.SchemaGeneration; - -namespace Saunter.Generation.Filters -{ - public interface IOperationFilter - { - void Apply(Operation operation, OperationFilterContext context); - } - - public class OperationFilterContext - { - public OperationFilterContext(MethodInfo method, JsonSchemaResolver schemaResolver, JsonSchemaGenerator schemaGenerator, OperationAttribute operation) - { - Method = method; - SchemaResolver = schemaResolver; - SchemaGenerator = schemaGenerator; - Operation = operation; - } - - public MethodInfo Method { get; } - - public JsonSchemaResolver SchemaResolver { get; } - - public JsonSchemaGenerator SchemaGenerator { get; } - - public OperationAttribute Operation { get; } - } -} diff --git a/src/Saunter/Generation/IDocumentGenerator.cs b/src/Saunter/Generation/IDocumentGenerator.cs deleted file mode 100644 index 6c9935dc..00000000 --- a/src/Saunter/Generation/IDocumentGenerator.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Reflection; -using Saunter.AsyncApiSchema.v2; - -namespace Saunter.Generation -{ - public interface IDocumentGenerator - { - AsyncApiDocument GenerateDocument(TypeInfo[] asyncApiTypes, AsyncApiOptions options, AsyncApiDocument prototype, IServiceProvider serviceProvider); - } -} diff --git a/src/Saunter/Generation/SchemaGeneration/AsyncApiSchemaResolver.cs b/src/Saunter/Generation/SchemaGeneration/AsyncApiSchemaResolver.cs deleted file mode 100644 index 39447766..00000000 --- a/src/Saunter/Generation/SchemaGeneration/AsyncApiSchemaResolver.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Linq; -using NJsonSchema; -using NJsonSchema.Generation; -using Saunter.AsyncApiSchema.v2; - -namespace Saunter.Generation.SchemaGeneration -{ - public class AsyncApiSchemaResolver : JsonSchemaResolver - { - private readonly AsyncApiDocument _document; - private readonly JsonSchemaGeneratorSettings _settings; - - public AsyncApiSchemaResolver(AsyncApiDocument document, AsyncApiSchemaOptions settings) - : base(document, settings) - { - _document = document; - _settings = settings; - } - - public override void AppendSchema(JsonSchema schema, string typeNameHint) - { - if (schema == null) - throw new ArgumentNullException(nameof(schema)); - if (schema == RootObject) - throw new ArgumentException("The root schema cannot be appended."); - - if (!_document.Components.Schemas.Values.Contains(schema)) - { - var schemaId = _settings.TypeNameGenerator.Generate(schema, typeNameHint, _document.Components.Schemas.Keys.Select(k => k.ToString())); - - if (!string.IsNullOrEmpty(schemaId) && !_document.Components.Schemas.ContainsKey(schemaId)) - { - _document.Components.Schemas.Add(schemaId, schema); - schema.Id = schemaId; - } - else - _document.Components.Schemas.Add("ref_" + Guid.NewGuid().ToString().Replace("-", "_"), schema); - } - } - - public IMessage GetMessageOrReference(Message message) - { - var id = message.Name; - if (id == null) - { - return message; - } - - if (!_document.Components.Messages.ContainsKey(id)) - { - _document.Components.Messages.Add(id, message); - message.Payload = new JsonSchema() - { - Reference = message.Payload - }; - } - - if (message.Headers != null) - { - // the headers schema is stored under components/schema; make - // sure to use the reference in the message instead of storing - // the complete schema again - message.Headers = new JsonSchema() - { - Reference = message.Headers - }; - } - - return new MessageReference(id); - } - - public IParameter GetParameterOrReference(Parameter parameter) - { - var id = parameter.Name; - if (id == null) - { - return parameter; - } - - if (!_document.Components.Parameters.ContainsKey(id)) - { - _document.Components.Parameters.Add(id, parameter); - } - - return new ParameterReference(id, _document); - } - } -} diff --git a/src/Saunter/Generation/SchemaGeneration/CamelCaseTypeNameGenerator.cs b/src/Saunter/Generation/SchemaGeneration/CamelCaseTypeNameGenerator.cs deleted file mode 100644 index 40d07ac5..00000000 --- a/src/Saunter/Generation/SchemaGeneration/CamelCaseTypeNameGenerator.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; -using NJsonSchema; - -namespace Saunter.Generation.SchemaGeneration -{ - public class CamelCaseTypeNameGenerator : DefaultTypeNameGenerator - { - public override string Generate(JsonSchema schema, string typeNameHint, IEnumerable reservedTypeNames) - { - return CamelCase(base.Generate(schema, typeNameHint, reservedTypeNames)); - } - - protected override string Generate(JsonSchema schema, string typeNameHint) - { - return CamelCase(base.Generate(schema, typeNameHint)); - } - - private string CamelCase(string name) - { - if (string.IsNullOrEmpty(name)) - { - return name; - } - - return char.ToLower(name[0]) + name.Substring(1); - } - } -} diff --git a/src/Saunter/IAsyncApiDocumentProvider.cs b/src/Saunter/IAsyncApiDocumentProvider.cs index a3f9814a..7ee5e15b 100644 --- a/src/Saunter/IAsyncApiDocumentProvider.cs +++ b/src/Saunter/IAsyncApiDocumentProvider.cs @@ -1,9 +1,10 @@ -using Saunter.AsyncApiSchema.v2; +using LEGO.AsyncAPI.Models; +using Saunter.Options; namespace Saunter { public interface IAsyncApiDocumentProvider { - AsyncApiDocument GetDocument(AsyncApiOptions options, AsyncApiDocument prototype); + AsyncApiDocument GetDocument(string? documentName, AsyncApiOptions options); } } diff --git a/src/Saunter/Options/AsyncApiMiddlewareOptions.cs b/src/Saunter/Options/AsyncApiMiddlewareOptions.cs new file mode 100644 index 00000000..435e04fa --- /dev/null +++ b/src/Saunter/Options/AsyncApiMiddlewareOptions.cs @@ -0,0 +1,20 @@ +namespace Saunter.Options +{ + public class AsyncApiMiddlewareOptions + { + /// + /// The route which the AsyncAPI document will be hosted + /// + public string Route { get; set; } = "/asyncapi/asyncapi.json"; + + /// + /// The base URL for the AsyncAPI UI + /// + public string UiBaseRoute { get; set; } = "/asyncapi/ui/"; + + /// + /// The title of page for AsyncAPI UI + /// + public string UiTitle { get; set; } = "AsyncAPI"; + } +} diff --git a/src/Saunter/Options/AsyncApiOptions.cs b/src/Saunter/Options/AsyncApiOptions.cs new file mode 100644 index 00000000..385bb665 --- /dev/null +++ b/src/Saunter/Options/AsyncApiOptions.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using LEGO.AsyncAPI.Models; +using Saunter.Options.Filters; + +namespace Saunter.Options +{ + public class AsyncApiOptions + { + private readonly List _documentFilters = new(); + private readonly List _channelFilters = new(); + private readonly List _operationFilters = new(); + + /// + /// The base asyncapi schema. This will be augmented with other information auto-discovered + /// from attributes. + /// + public AsyncApiDocument AsyncApi { get; set; } = new AsyncApiDocument(); + + /// + /// A list of marker types from assemblies to scan for Saunter attributes. + /// + public IList AssemblyMarkerTypes { get; set; } = new List(); + + internal virtual IReadOnlyCollection AsyncApiSchemaTypes => AssemblyMarkerTypes + .Select(t => t.Assembly) + .Distinct() + .SelectMany(a => a.DefinedTypes) + .ToImmutableHashSet(); + + /// + /// A list of filters that will be applied to the generated AsyncAPI document. + /// + public IEnumerable DocumentFilters => _documentFilters; + + /// + /// A list of filters that will be applied to any generated channels. + /// + public IEnumerable ChannelFilters => _channelFilters; + + /// + /// A list of filters that will be applied to any generated Publish/Subscribe operations. + /// + public IEnumerable OperationFilters => _operationFilters; + + + /// + /// Add a filter to be applied to the generated AsyncAPI document. + /// + public void AddDocumentFilter() where T : IDocumentFilter + { + _documentFilters.Add(typeof(T)); + } + + /// + /// Add a filter to be applied to any generated channels. + /// + public void AddAsyncApiChannelFilter() where T : IChannelFilter + { + _channelFilters.Add(typeof(T)); + } + + /// + /// Add a filter to be applied to any generated Publish/Subscribe operations. + /// + public void AddOperationFilter() where T : IOperationFilter + { + _operationFilters.Add(typeof(T)); + } + + /// + /// Options related to the Saunter middleware. + /// + public AsyncApiMiddlewareOptions Middleware { get; } = new AsyncApiMiddlewareOptions(); + + public ConcurrentDictionary NamedApis { get; set; } = new(); + } +} diff --git a/src/Saunter/Options/Filters/ChannelFilterContext.cs b/src/Saunter/Options/Filters/ChannelFilterContext.cs new file mode 100644 index 00000000..d3e6c23e --- /dev/null +++ b/src/Saunter/Options/Filters/ChannelFilterContext.cs @@ -0,0 +1,15 @@ +using System.Reflection; +using Saunter.AttributeProvider.Attributes; + +public class ChannelFilterContext +{ + public ChannelFilterContext(MemberInfo member, ChannelAttribute channel) + { + Member = member; + Channel = channel; + } + + public MemberInfo Member { get; } + + public ChannelAttribute Channel { get; } +} diff --git a/src/Saunter/Options/Filters/DocumentFilterContext.cs b/src/Saunter/Options/Filters/DocumentFilterContext.cs new file mode 100644 index 00000000..bc6801fa --- /dev/null +++ b/src/Saunter/Options/Filters/DocumentFilterContext.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; + +public class DocumentFilterContext +{ + public DocumentFilterContext(IEnumerable asyncApiTypes) + { + AsyncApiTypes = asyncApiTypes; + } + + public IEnumerable AsyncApiTypes { get; } +} diff --git a/src/Saunter/Options/Filters/IChannelFilter.cs b/src/Saunter/Options/Filters/IChannelFilter.cs new file mode 100644 index 00000000..0d5d407b --- /dev/null +++ b/src/Saunter/Options/Filters/IChannelFilter.cs @@ -0,0 +1,6 @@ +using LEGO.AsyncAPI.Models; + +public interface IChannelFilter +{ + void Apply(AsyncApiChannel channel, ChannelFilterContext context); +} diff --git a/src/Saunter/Options/Filters/IDocumentFilter.cs b/src/Saunter/Options/Filters/IDocumentFilter.cs new file mode 100644 index 00000000..bacb26f9 --- /dev/null +++ b/src/Saunter/Options/Filters/IDocumentFilter.cs @@ -0,0 +1,9 @@ +using LEGO.AsyncAPI.Models; + +namespace Saunter.Options.Filters +{ + public interface IDocumentFilter + { + void Apply(AsyncApiDocument document, DocumentFilterContext context); + } +} diff --git a/src/Saunter/Options/Filters/IOperationFilter.cs b/src/Saunter/Options/Filters/IOperationFilter.cs new file mode 100644 index 00000000..46f7ad78 --- /dev/null +++ b/src/Saunter/Options/Filters/IOperationFilter.cs @@ -0,0 +1,7 @@ +using LEGO.AsyncAPI.Models; +using Saunter.Options.Filters; + +public interface IOperationFilter +{ + void Apply(AsyncApiOperation operation, OperationFilterContext context); +} diff --git a/src/Saunter/Options/Filters/OperationFilterContext.cs b/src/Saunter/Options/Filters/OperationFilterContext.cs new file mode 100644 index 00000000..59a8f1db --- /dev/null +++ b/src/Saunter/Options/Filters/OperationFilterContext.cs @@ -0,0 +1,18 @@ +using System.Reflection; +using Saunter.AttributeProvider.Attributes; + +namespace Saunter.Options.Filters +{ + public class OperationFilterContext + { + public OperationFilterContext(MethodInfo method, OperationAttribute operation) + { + Method = method; + Operation = operation; + } + + public MethodInfo Method { get; } + + public OperationAttribute Operation { get; } + } +} diff --git a/src/Saunter/Saunter.csproj b/src/Saunter/Saunter.csproj index 89875ef2..a51a2e5d 100644 --- a/src/Saunter/Saunter.csproj +++ b/src/Saunter/Saunter.csproj @@ -1,7 +1,8 @@  - + net6.0 + enable true Saunter Saunter @@ -31,7 +32,6 @@ - @@ -44,21 +44,23 @@ - - - + - + - + UI\package.json - + + + + + diff --git a/src/Saunter/Serialization/IAsyncApiDocumentSerializer.cs b/src/Saunter/Serialization/IAsyncApiDocumentSerializer.cs deleted file mode 100644 index 05562c33..00000000 --- a/src/Saunter/Serialization/IAsyncApiDocumentSerializer.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Saunter.AsyncApiSchema.v2; - -namespace Saunter.Serialization -{ - public interface IAsyncApiDocumentSerializer - { - string ContentType { get; } - - string Serialize(AsyncApiDocument document); - - Task DeserializeAsync(string data, CancellationToken cancellationToken); - } -} diff --git a/src/Saunter/Serialization/NewtonsoftAsyncApiDocumentSerializer.cs b/src/Saunter/Serialization/NewtonsoftAsyncApiDocumentSerializer.cs deleted file mode 100644 index 40a4d069..00000000 --- a/src/Saunter/Serialization/NewtonsoftAsyncApiDocumentSerializer.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Newtonsoft.Json; -using NJsonSchema; -using NJsonSchema.Infrastructure; -using Saunter.AsyncApiSchema.v2; -using Saunter.Generation.SchemaGeneration; - -namespace Saunter.Serialization -{ - public class NewtonsoftAsyncApiDocumentSerializer : IAsyncApiDocumentSerializer - { - public string ContentType => "application/json"; - - public string Serialize(AsyncApiDocument document) - { - var contractResolver = JsonSchema.CreateJsonSerializerContractResolver(SchemaType.JsonSchema); - return JsonSchemaSerialization.ToJson(document, SchemaType.JsonSchema, contractResolver, Formatting.Indented); - } - - public async Task DeserializeAsync(string data, CancellationToken cancellationToken) - { - var contractResolver = JsonSchema.CreateJsonSerializerContractResolver(SchemaType.JsonSchema); - return await JsonSchemaSerialization.FromJsonAsync(data, SchemaType.JsonSchema, null, document => - { - var schemaResolver = new AsyncApiSchemaResolver(document, new AsyncApiSchemaOptions()); - return new JsonReferenceResolver(schemaResolver); - }, contractResolver, cancellationToken).ConfigureAwait(false); - } - } -} diff --git a/src/Saunter/SharedKernel/AsyncApiChannelUnion.cs b/src/Saunter/SharedKernel/AsyncApiChannelUnion.cs new file mode 100644 index 00000000..5a491a4e --- /dev/null +++ b/src/Saunter/SharedKernel/AsyncApiChannelUnion.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using LEGO.AsyncAPI.Models; +using LEGO.AsyncAPI.Models.Interfaces; +using Saunter.SharedKernel.Interfaces; + +namespace Saunter.SharedKernel +{ + internal class AsyncApiChannelUnion : IAsyncApiChannelUnion + { + public AsyncApiChannel Union(AsyncApiChannel source, AsyncApiChannel additionaly) + { + if (source.Publish is not null && additionaly.Publish is not null) + { + throw new InvalidOperationException("Publish operation conflict"); + } + + if (source.Subscribe is not null && additionaly.Subscribe is not null) + { + throw new InvalidOperationException("Subscribe operation conflict"); + } + + if (source.Reference is not null && additionaly.Reference is not null) + { + throw new InvalidOperationException("Reference operation conflict"); + } + + var publishOperation = source.Publish ?? additionaly.Publish; + var subscribeOperation = source.Subscribe ?? additionaly.Subscribe; + var reference = source.Reference ?? additionaly.Reference; + + if (reference is not null && (publishOperation is not null || subscribeOperation is not null)) + { + throw new InvalidOperationException("Reference operation conflict"); + } + + var servers = source.Servers?.Any() == true + ? source.Servers + : additionaly.Servers + ?? new List(); + + var bindings = source.Bindings?.Any() == true + ? source.Bindings + : additionaly.Bindings + ?? new(); + + var parameters = source.Parameters?.Any() == true + ? source.Parameters + : additionaly.Parameters + ?? new Dictionary(); + + var extensions = source.Extensions?.Any() == true + ? source.Extensions + : additionaly.Extensions + ?? new Dictionary(); + + return new() + { + Publish = publishOperation, + Subscribe = subscribeOperation, + + Servers = servers, + Bindings = bindings, + Parameters = parameters, + Extensions = extensions, + + Reference = reference, + Description = source.Description ?? additionaly.Description, + }; + } + } +} diff --git a/src/Saunter/SharedKernel/AsyncApiDocumentSerializeCloner.cs b/src/Saunter/SharedKernel/AsyncApiDocumentSerializeCloner.cs new file mode 100644 index 00000000..a9beee48 --- /dev/null +++ b/src/Saunter/SharedKernel/AsyncApiDocumentSerializeCloner.cs @@ -0,0 +1,55 @@ +using LEGO.AsyncAPI; +using LEGO.AsyncAPI.Bindings; +using LEGO.AsyncAPI.Models; +using LEGO.AsyncAPI.Readers; +using Microsoft.Extensions.Logging; +using Saunter.SharedKernel.Interfaces; + +namespace Saunter.SharedKernel +{ + internal class AsyncApiDocumentSerializeCloner : IAsyncApiDocumentCloner + { + private readonly ILogger _logger; + + public AsyncApiDocumentSerializeCloner(ILogger logger) + { + _logger = logger; + } + + public AsyncApiDocument CloneProtype(AsyncApiDocument prototype) + { + var jsonView = prototype.Serialize(AsyncApiVersion.AsyncApi2_0, AsyncApiFormat.Json); + + var settings = new AsyncApiReaderSettings + { + Bindings = BindingsCollection.All, + }; + + var reader = new AsyncApiStringReader(settings); + + var cloned = reader.Read(jsonView, out var diagnostic); + + if (diagnostic is not null) + { + foreach (var item in diagnostic.Errors) + { + var ignore = !item.Message.Contains("The field 'channels' in 'document' object is REQUIRED"); + + if (ignore) + { + _logger.LogError("Error while clone protype: {Error}", item); + } + } + + foreach (var item in diagnostic.Warnings) + { + _logger.LogWarning("Warning while clone protype: {Error}", item); + } + } + + cloned.Components ??= new(); + + return cloned; + } + } +} diff --git a/src/Saunter/SharedKernel/AsyncApiSchemaGenerator.cs b/src/Saunter/SharedKernel/AsyncApiSchemaGenerator.cs new file mode 100644 index 00000000..dd00820f --- /dev/null +++ b/src/Saunter/SharedKernel/AsyncApiSchemaGenerator.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using LEGO.AsyncAPI.Models; +using Saunter.SharedKernel.Interfaces; + +namespace Saunter.SharedKernel +{ + internal class AsyncApiSchemaGenerator : IAsyncApiSchemaGenerator + { + public GeneratedSchemas? Generate(Type? type) + { + return GenerateBranch(type, new()); + } + + private static GeneratedSchemas? GenerateBranch(Type? type, HashSet parents) + { + if (type is null) + { + return null; + } + + var typeInfo = type.GetTypeInfo(); + + var schema = new AsyncApiSchema + { + Nullable = !typeInfo.IsValueType, + }; + + var nestedShemas = new List() + { + schema + }; + + if (typeInfo.IsGenericType) + { + var nullableType = typeof(Nullable<>).MakeGenericType(typeInfo.GenericTypeArguments); + + if (typeInfo == nullableType) + { + schema.Nullable = true; + typeInfo = typeInfo.GenericTypeArguments[0].GetTypeInfo(); + } + } + + var name = ToNameCase(typeInfo.Name); + + schema.Title = name; + schema.Type = MapJsonTypeToSchemaType(typeInfo); + + if (schema.Type is not SchemaType.Object and not SchemaType.Array) + { + if (typeInfo.IsEnum) + { + schema.Format = "enum"; + schema.Enum = typeInfo + .GetEnumNames() + .Select(e => new AsyncApiAny(e)) + .ToList(); + } + else + { + schema.Format = schema.Title; + } + + return new(schema, nestedShemas); + } + + 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); + + 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 new(schema, nestedShemas.DistinctBy(n => n.Title).ToArray()); + } + + private static string ToNameCase(string name) + { + return char.ToLowerInvariant(name[0]) + name[1..]; + } + + private static readonly TypeInfo s_boolTypeInfo = typeof(bool).GetTypeInfo(); + + private static readonly TypeInfo[] s_stringTypeInfos = new TypeInfo[] + { + typeof(string).GetTypeInfo(), + typeof(DateTime).GetTypeInfo(), + typeof(DateTimeOffset).GetTypeInfo(), + typeof(TimeSpan).GetTypeInfo(), + typeof(Guid).GetTypeInfo(), + typeof(Uri).GetTypeInfo(), + typeof(DateOnly).GetTypeInfo(), + typeof(TimeOnly).GetTypeInfo(), + }; + + private static readonly TypeInfo[] s_intergerTypeInfos = new TypeInfo[] + { + typeof(byte).GetTypeInfo(), + typeof(short).GetTypeInfo(), + typeof(int).GetTypeInfo(), + typeof(long).GetTypeInfo(), + typeof(uint).GetTypeInfo(), + typeof(ushort).GetTypeInfo(), + typeof(ulong).GetTypeInfo(), + }; + + private static readonly TypeInfo[] s_floatTypeInfos = new TypeInfo[] + { + typeof(float).GetTypeInfo(), + typeof(decimal).GetTypeInfo(), + typeof(double).GetTypeInfo(), + }; + + private static SchemaType? MapJsonTypeToSchemaType(TypeInfo typeInfo) + { + if (typeInfo == s_boolTypeInfo) + { + return SchemaType.Boolean; + } + + if (typeInfo.IsEnum) + { + return SchemaType.String; + } + + if (s_stringTypeInfos.Contains(typeInfo)) + { + return SchemaType.String; + } + + if (s_intergerTypeInfos.Contains(typeInfo)) + { + return SchemaType.Integer; + } + + if (s_floatTypeInfos.Contains(typeInfo)) + { + return SchemaType.Number; + } + + if (typeInfo.IsArray || (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(IEnumerable<>))) + { + return SchemaType.Array; + } + + return SchemaType.Object; + } + } +} diff --git a/src/Saunter/SharedKernel/Interfaces/IAsyncApiChannelUnion.cs b/src/Saunter/SharedKernel/Interfaces/IAsyncApiChannelUnion.cs new file mode 100644 index 00000000..3e5fc98e --- /dev/null +++ b/src/Saunter/SharedKernel/Interfaces/IAsyncApiChannelUnion.cs @@ -0,0 +1,9 @@ +using LEGO.AsyncAPI.Models; + +namespace Saunter.SharedKernel.Interfaces +{ + public interface IAsyncApiChannelUnion + { + AsyncApiChannel Union(AsyncApiChannel source, AsyncApiChannel additionaly); + } +} diff --git a/src/Saunter/SharedKernel/Interfaces/IAsyncApiDocumentCloner.cs b/src/Saunter/SharedKernel/Interfaces/IAsyncApiDocumentCloner.cs new file mode 100644 index 00000000..9c67ea5a --- /dev/null +++ b/src/Saunter/SharedKernel/Interfaces/IAsyncApiDocumentCloner.cs @@ -0,0 +1,9 @@ +using LEGO.AsyncAPI.Models; + +namespace Saunter.SharedKernel.Interfaces +{ + public interface IAsyncApiDocumentCloner + { + AsyncApiDocument CloneProtype(AsyncApiDocument prototype); + } +} diff --git a/src/Saunter/SharedKernel/Interfaces/IAsyncApiSchemaGenerator.cs b/src/Saunter/SharedKernel/Interfaces/IAsyncApiSchemaGenerator.cs new file mode 100644 index 00000000..e2a9d8d7 --- /dev/null +++ b/src/Saunter/SharedKernel/Interfaces/IAsyncApiSchemaGenerator.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using LEGO.AsyncAPI.Models; + +namespace Saunter.SharedKernel.Interfaces +{ + public interface IAsyncApiSchemaGenerator + { + GeneratedSchemas? Generate(Type? type); + } + + public readonly record struct GeneratedSchemas(AsyncApiSchema Root, IReadOnlyCollection All); +} diff --git a/src/Saunter/UI/AsyncApiUiMiddleware.cs b/src/Saunter/UI/AsyncApiUiMiddleware.cs index 25af8a7d..5aaa2efb 100644 --- a/src/Saunter/UI/AsyncApiUiMiddleware.cs +++ b/src/Saunter/UI/AsyncApiUiMiddleware.cs @@ -1,6 +1,6 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net; using System.Net.Mime; using System.Text; @@ -8,17 +8,16 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; -using Microsoft.AspNetCore.Routing.Template; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Saunter.Utils; +using Saunter.DocumentMiddleware; +using Saunter.Options; namespace Saunter.UI { - public class AsyncApiUiMiddleware + internal class AsyncApiUiMiddleware { private readonly AsyncApiOptions _options; private readonly StaticFileMiddleware _staticFiles; @@ -33,17 +32,28 @@ public AsyncApiUiMiddleware(RequestDelegate next, IOptions opti RequestPath = UiBaseRoute, FileProvider = fileProvider, }; - _staticFiles = new StaticFileMiddleware(next, env, Options.Create(staticFileOptions), loggerFactory); + + _staticFiles = new StaticFileMiddleware( + next, + env, + Microsoft.Extensions.Options.Options.Create(staticFileOptions), + loggerFactory); + _namedStaticFiles = new Dictionary(); - foreach (var namedApi in _options.NamedApis) + foreach (var namedApi in _options.NamedApis.Select(c => c.Key)) { var namedStaticFileOptions = new StaticFileOptions { - RequestPath = UiBaseRoute.Replace("{document}", namedApi.Key), + RequestPath = UiBaseRoute.Replace("{document}", namedApi), FileProvider = fileProvider, }; - _namedStaticFiles.Add(namedApi.Key, new StaticFileMiddleware(next, env, Options.Create(namedStaticFileOptions), loggerFactory)); + + _namedStaticFiles.Add(namedApi, new StaticFileMiddleware( + next, + env, + Microsoft.Extensions.Options.Options.Create(namedStaticFileOptions), + loggerFactory)); } } @@ -96,25 +106,28 @@ public async Task Invoke(HttpContext context) private async Task RespondWithAsyncApiHtml(HttpResponse response, string route) { - using (var stream = GetType().Assembly.GetManifestResourceStream($"{GetType().Namespace}.index.html")) - using (var reader = new StreamReader(stream)) - { - var indexHtml = new StringBuilder(await reader.ReadToEndAsync()); + var name = $"{GetType().Namespace}.index.html"; - // Replace dynamic content such as the AsyncAPI document url - foreach (var replacement in new Dictionary - { - ["{{title}}"] = _options.Middleware.UiTitle, - ["{{asyncApiDocumentUrl}}"] = route, - }) - { - indexHtml.Replace(replacement.Key, replacement.Value); - } + using var stream = GetType().Assembly.GetManifestResourceStream(name) + ?? throw new FileNotFoundException($"Not found html file {name}"); + + using var reader = new StreamReader(stream); + + var indexHtml = new StringBuilder(await reader.ReadToEndAsync()); - response.StatusCode = (int)HttpStatusCode.OK; - response.ContentType = MediaTypeNames.Text.Html; - await response.WriteAsync(indexHtml.ToString(), Encoding.UTF8); + // Replace dynamic content such as the AsyncAPI document url + foreach (var replacement in new Dictionary + { + ["{{title}}"] = _options.Middleware.UiTitle, + ["{{asyncApiDocumentUrl}}"] = route, + }) + { + indexHtml.Replace(replacement.Key, replacement.Value); } + + response.StatusCode = (int)HttpStatusCode.OK; + response.ContentType = MediaTypeNames.Text.Html; + await response.WriteAsync(indexHtml.ToString(), Encoding.UTF8); } private bool IsRequestingUiBase(HttpRequest request) diff --git a/src/Saunter/Utils/ChannelItemExtensions.cs b/src/Saunter/Utils/ChannelItemExtensions.cs deleted file mode 100644 index dac1e97a..00000000 --- a/src/Saunter/Utils/ChannelItemExtensions.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; -using Saunter.AsyncApiSchema.v2; - -namespace Saunter.Utils -{ - public static class ChannelItemExtensions - { - public static void AddRange(this IDictionary source, IDictionary channels) - { - if (channels == null) - { - return; - } - - foreach (var channel in channels) - { - source.AddOrAppend(channel.Key, channel.Value); - } - } - - public static void AddOrAppend(this IDictionary source, string key, ChannelItem channel) - { - if (source.TryGetValue(key, out var existing)) - { - if (existing.Publish != null && channel.Publish != null) - throw new ArgumentException("An item with the same key and with an existing publish operation has already been added to the channel collection."); - - if (existing.Subscribe != null && channel.Subscribe != null) - throw new ArgumentException("An item with the same key and with an existing subscribe operation has already been added to the channel collection."); - - existing.Description ??= channel.Description; - existing.Parameters ??= channel.Parameters; - existing.Publish ??= channel.Publish; - existing.Subscribe ??= channel.Subscribe; - existing.Bindings ??= channel.Bindings; - existing.Servers ??= channel.Servers; - } - else - { - source.Add(key, channel); - } - } - } -} diff --git a/src/Saunter/Utils/Reflection.cs b/src/Saunter/Utils/Reflection.cs deleted file mode 100644 index 3f348a10..00000000 --- a/src/Saunter/Utils/Reflection.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Reflection; - -namespace Saunter.Utils -{ - internal static class Reflection - { - public static bool HasCustomAttribute(this TypeInfo typeInfo) where T : Attribute - { - return typeInfo.GetCustomAttribute() != null; - } - } -} diff --git a/test/Saunter.IntegrationTests.ReverseProxy/Dockerfile b/test/Saunter.IntegrationTests.ReverseProxy/Dockerfile index 28cbc8d1..a80ef2c5 100644 --- a/test/Saunter.IntegrationTests.ReverseProxy/Dockerfile +++ b/test/Saunter.IntegrationTests.ReverseProxy/Dockerfile @@ -1,10 +1,23 @@ -FROM mcr.microsoft.com/dotnet/aspnet:5.0 +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 -# Run "dotnet publish -c Release" before building this image -COPY bin/Release/net5.0/publish/ App/ +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 -WORKDIR /App +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./Saunter.IntegrationTests.ReverseProxy.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false -EXPOSE 5000 - -ENTRYPOINT ["dotnet", "Saunter.IntegrationTests.ReverseProxy.dll"] +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 0cdfb927..4fc843bb 100644 --- a/test/Saunter.IntegrationTests.ReverseProxy/Program.cs +++ b/test/Saunter.IntegrationTests.ReverseProxy/Program.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using LEGO.AsyncAPI.Models; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server.Features; @@ -9,8 +10,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Saunter.AsyncApiSchema.v2; -using Saunter.Attributes; +using Saunter.AttributeProvider.Attributes; namespace Saunter.IntegrationTests.ReverseProxy { @@ -50,7 +50,11 @@ public void ConfigureServices(IServiceCollection services) options.AsyncApi = new AsyncApiDocument { - Info = new Info(Environment.GetEnvironmentVariable("PATH_BASE"), "1.0.0") + Info = new AsyncApiInfo + { + Title = Environment.GetEnvironmentVariable("PATH_BASE"), + Version = "1.0.0", + }, }; }); @@ -80,7 +84,6 @@ public void Configure(IApplicationBuilder app) endpoints.MapControllers(); }); - // Print the AsyncAPI doc location var logger = app.ApplicationServices.GetService().CreateLogger(); var addresses = app.ServerFeatures.Get().Addresses; @@ -90,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 0ea9e98c..614647fe 100644 --- a/test/Saunter.IntegrationTests.ReverseProxy/README.md +++ b/test/Saunter.IntegrationTests.ReverseProxy/README.md @@ -5,13 +5,11 @@ 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: +```bash +docker-compose --file ./test/Saunter.IntegrationTests.ReverseProxy/docker-compose.yml up --build ``` -$ dotnet publish -c Release -$ docker-compose up -``` - You should be able to access both services UI * http://localhost:5000/service-a/asyncapi/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.MarkerTypeTests/Broker.cs b/test/Saunter.Tests.MarkerTypeTests/Broker.cs index 64cc4973..e7f92e41 100644 --- a/test/Saunter.Tests.MarkerTypeTests/Broker.cs +++ b/test/Saunter.Tests.MarkerTypeTests/Broker.cs @@ -1,4 +1,4 @@ -using Saunter.Attributes; +using Saunter.AttributeProvider.Attributes; namespace Saunter.Tests.MarkerTypeTests { diff --git a/test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/ArrangeAttributesTests.cs b/test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/ArrangeAttributesTests.cs new file mode 100644 index 00000000..43b357d2 --- /dev/null +++ b/test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/ArrangeAttributesTests.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.Logging.Testing; +using Saunter.AttributeProvider; +using Saunter.Options; +using Saunter.SharedKernel; + +namespace Saunter.Tests.AttributeProvider.DocumentGenerationTests +{ + internal static class ArrangeAttributesTests + { + private sealed class FakeAsyncApiOptions : AsyncApiOptions + { + private readonly TypeInfo[] _types; + + public FakeAsyncApiOptions(Type[] types) + { + _types = types.Select(t => t.GetTypeInfo()).ToArray(); + } + + internal override IReadOnlyCollection AsyncApiSchemaTypes => _types; + } + + public static void Arrange(out AsyncApiOptions options, out AttributeDocumentProvider documentProvider, params Type[] targetTypes) + { + options = new FakeAsyncApiOptions(targetTypes); + + documentProvider = new AttributeDocumentProvider( + ActivatorServiceProvider.Instance, + new AsyncApiSchemaGenerator(), + new AsyncApiChannelUnion(), + new AsyncApiDocumentSerializeCloner(new FakeLogger())); + } + } +} 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/Generation/DocumentGeneratorTests/ClassAttributesTests.cs b/test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/ClassAttributesTests.cs similarity index 63% rename from test/Saunter.Tests/Generation/DocumentGeneratorTests/ClassAttributesTests.cs rename to test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/ClassAttributesTests.cs index c384d641..56566fad 100644 --- a/test/Saunter.Tests/Generation/DocumentGeneratorTests/ClassAttributesTests.cs +++ b/test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/ClassAttributesTests.cs @@ -1,14 +1,11 @@ using System; using System.ComponentModel.DataAnnotations; using System.Linq; -using System.Reflection; -using Saunter.AsyncApiSchema.v2; -using Saunter.Attributes; -using Saunter.Generation; +using Saunter.AttributeProvider.Attributes; using Shouldly; using Xunit; -namespace Saunter.Tests.Generation.DocumentGeneratorTests +namespace Saunter.Tests.AttributeProvider.DocumentGenerationTests { public class ClassAttributesTests { @@ -18,95 +15,72 @@ public class ClassAttributesTests public void GetDocument_GeneratesDocumentWithMultipleMessagesPerChannel(Type type) { // Arrange - var options = new AsyncApiOptions(); - var documentGenerator = new DocumentGenerator(); + ArrangeAttributesTests.Arrange(out var options, out var documentProvider, type); + const string Key = "asw.tenant_service.tenants_history"; // Act - var document = documentGenerator.GenerateDocument(new[] { type.GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance); + 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 messages = subscribe.Message.ShouldBeOfType(); - messages.OneOf.Count.ShouldBe(3); - - messages.OneOf.OfType().ShouldContain(m => m.Id == "tenantCreated"); - messages.OneOf.OfType().ShouldContain(m => m.Id == "tenantUpdated"); - messages.OneOf.OfType().ShouldContain(m => m.Id == "tenantRemoved"); + document.AssertByMessage(subscribe, "tenantCreated", "tenantUpdated", "tenantRemoved"); } - [Theory] [InlineData(typeof(TenantGenericMessagePublisher))] [InlineData(typeof(ITenantGenericMessagePublisher))] public void GenerateDocument_GeneratesDocumentWithMultipleMessagesPerChannelInTheSameMethod(Type type) { // Arrange - var options = new AsyncApiOptions(); - var documentGenerator = new DocumentGenerator(); + ArrangeAttributesTests.Arrange(out var options, out var documentProvider, type); + const string Key = "asw.tenant_service.tenants_history"; // Act - var document = documentGenerator.GenerateDocument(new[] { type.GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance); + 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."); - var messages = publish.Message.ShouldBeOfType(); - messages.OneOf.Count.ShouldBe(3); - - messages.OneOf.OfType().ShouldContain(m => m.Id == "anyTenantCreated"); - messages.OneOf.OfType().ShouldContain(m => m.Id == "anyTenantUpdated"); - messages.OneOf.OfType().ShouldContain(m => m.Id == "anyTenantRemoved"); + document.AssertByMessage(publish, "anyTenantCreated", "anyTenantUpdated", "anyTenantRemoved"); } - [Theory] [InlineData(typeof(TenantSingleMessagePublisher))] [InlineData(typeof(ITenantSingleMessagePublisher))] public void GenerateDocument_GeneratesDocumentWithSingleMessage(Type type) { // Arrange - var options = new AsyncApiOptions(); - var documentGenerator = new DocumentGenerator(); + ArrangeAttributesTests.Arrange(out var options, out var documentProvider, type); + const string Key = "asw.tenant_service.tenants_history"; // Act - var document = documentGenerator.GenerateDocument(new[] { type.GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance); + 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."); - var message = publish.Message.ShouldBeOfType(); - message.Id.ShouldBe("anyTenantCreated"); + document.AssertByMessage(publish, "anyTenantCreated"); } @@ -116,108 +90,99 @@ public void GenerateDocument_GeneratesDocumentWithSingleMessage(Type type) public void GetDocument_WhenMultipleClassesUseSameChannelKey_GeneratesDocumentWithMultipleMessagesPerChannel(Type type1, Type type2) { // Arrange - var options = new AsyncApiOptions(); - var documentGenerator = new DocumentGenerator(); + ArrangeAttributesTests.Arrange(out var options, out var documentProvider, type1, type2); + const string Key = "asw.tenant_service.tenants_history"; // Act - var document = documentGenerator.GenerateDocument(new[] - { - type1.GetTypeInfo(), - type2.GetTypeInfo() - }, options, options.AsyncApi, ActivatorServiceProvider.Instance); + 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."); - - var subscribeMessages = subscribe.Message.ShouldBeOfType(); - subscribeMessages.OneOf.Count.ShouldBe(3); - - subscribeMessages.OneOf.OfType().ShouldContain(m => m.Id == "tenantCreated"); - subscribeMessages.OneOf.OfType().ShouldContain(m => m.Id == "tenantUpdated"); - subscribeMessages.OneOf.OfType().ShouldContain(m => m.Id == "tenantRemoved"); - - var publishMessages = subscribe.Message.ShouldBeOfType(); - publishMessages.OneOf.Count.ShouldBe(3); - - publishMessages.OneOf.OfType().ShouldContain(m => m.Id == "tenantCreated"); - publishMessages.OneOf.OfType().ShouldContain(m => m.Id == "tenantUpdated"); - publishMessages.OneOf.OfType().ShouldContain(m => m.Id == "tenantRemoved"); + document.AssertByMessage(subscribe, "tenantCreated", "tenantUpdated", "tenantRemoved"); + document.AssertByMessage(publish, "tenantCreated", "tenantUpdated", "tenantRemoved"); } - [Theory] [InlineData(typeof(OneTenantMessageConsumer))] [InlineData(typeof(IOneTenantMessageConsumer))] public void GenerateDocument_GeneratesDocumentWithChannelParameters(Type type) { // Arrange - var options = new AsyncApiOptions(); - var documentGenerator = new DocumentGenerator(); + ArrangeAttributesTests.Arrange(out var options, out var documentProvider, type); + const string Key = "asw.tenant_service.{tenant_id}.{tenant_status}"; // Act - var document = documentGenerator.GenerateDocument(new[] { type.GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance); + var document = documentProvider.GetDocument(null, options); // Assert document.ShouldNotBeNull(); document.Channels.Count.ShouldBe(1); + document.Channels.ShouldContainKey(Key); + + var channel = document.Channels[Key]; + + channel.Description.ShouldBe("A tenant events."); + + channel.Parameters.Count.ShouldBe(2); + channel.Parameters.ContainsKey("tenant_id"); + channel.Parameters.ContainsKey("tenant_status"); - var channel = document.Channels.First(); - channel.Key.ShouldBe("asw.tenant_service.{tenant_id}.{tenant_status}"); - channel.Value.Description.ShouldBe("A tenant events."); - channel.Value.Parameters.Count.ShouldBe(2); - channel.Value.Parameters.Values.OfType().ShouldContain(p => p.Id == "tenant_id" && p.Value.Schema != null && p.Value.Description == "The tenant identifier."); - channel.Value.Parameters.Values.OfType().ShouldContain(p => p.Id == "tenant_status" && p.Value.Schema != null && p.Value.Description == "The 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.Value.Subscribe; + var subscribe = channel.Subscribe; subscribe.ShouldNotBeNull(); subscribe.OperationId.ShouldBe("OneTenantMessageConsumer"); subscribe.Summary.ShouldBe("Subscribe to domains events about a tenant."); - var messages = subscribe.Message.ShouldBeOfType(); - messages.OneOf.Count.ShouldBe(3); - - messages.OneOf.OfType().ShouldContain(m => m.Id == "tenantCreated"); - messages.OneOf.OfType().ShouldContain(m => m.Id == "tenantUpdated"); - messages.OneOf.OfType().ShouldContain(m => m.Id == "tenantRemoved"); + document.AssertByMessage(subscribe, "tenantCreated", "tenantUpdated", "tenantRemoved"); } - [Theory] [InlineData(typeof(MyMessagePublisher))] [InlineData(typeof(IMyMessagePublisher))] public void GenerateDocument_GeneratesDocumentWithMessageHeader(Type type) { // Arrange - var options = new AsyncApiOptions(); - var documentGenerator = new DocumentGenerator(); + ArrangeAttributesTests.Arrange(out var options, out var documentProvider, type); // Act - var document = documentGenerator.GenerateDocument(new[] { type.GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance); + var document = documentProvider.GetDocument(null, options); // Assert document.ShouldNotBeNull(); + document.Channels.Count.ShouldBe(expected: 1); - document.Components.Schemas.Values.ShouldContain(t => t.Id == "myMessageHeader"); - var message = document.Components.Messages.Values.First(); - message.Headers.Reference.Id.ShouldBe("myMessageHeader"); - } + var channel = document.Channels.First().Value; + var messages = channel.Publish.Message; + messages.Count.ShouldBe(1); + + 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] [Channel("channel.my.message")] @@ -229,7 +194,7 @@ public void PublishMyMessage() { } } [AsyncApi] - [Channel("channel.my.message")] + [Channel("channel.my.message.interface")] [PublishOperation] public interface IMyMessagePublisher { diff --git a/test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/InterfaceAttributeTests.cs b/test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/InterfaceAttributeTests.cs new file mode 100644 index 00000000..92356986 --- /dev/null +++ b/test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/InterfaceAttributeTests.cs @@ -0,0 +1,95 @@ +using System; +using Saunter.AttributeProvider.Attributes; +using Shouldly; +using Xunit; + +namespace Saunter.Tests.AttributeProvider.DocumentGenerationTests +{ + public class InterfaceAttributeTests + { + [Theory] + [InlineData(typeof(IServiceEvents))] + [InlineData(typeof(ServiceEventsFromInterface))] + [InlineData(typeof(ServiceEventsFromAnnotatedInterface))] + // Check that annotations are not inherited from the interface + public void NonAnnotatedTypesTest(Type type) + { + // Arrange + ArrangeAttributesTests.Arrange(out var options, out var documentProvider, type); + + // Act + var document = documentProvider.GetDocument(null, options); + + // Assert + document.ShouldNotBeNull(); + document.Channels.Count.ShouldBe(0); + } + + [Theory] + [InlineData(typeof(IAnnotatedServiceEvents), "interface.event.service.anotated.interface")] + [InlineData(typeof(AnnotatedServiceEventsFromAnnotatedInterface), "class.event.service.anotated.interface")] + [InlineData(typeof(SecondAnnotatedServiceEventsFromAnnotatedInterface), "class.event.secondservice.anotated.interface")] + // Check that the actual type's annotation takes precedence of the inherited interface + public void AnnotatedTypesTest(Type type, string channelName) + { + // Arrange + ArrangeAttributesTests.Arrange(out var options, out var documentProvider, type); + + // Act + var document = documentProvider.GetDocument(null, options); + + // Assert + document.ShouldNotBeNull(); + + var channel = document.AssertAndGetChannel(channelName, null); + + var publish = channel.Publish; + publish.ShouldNotBeNull(); + publish.OperationId.ShouldBe("PublishEvent"); + publish.Description.ShouldBe($"({channelName}) Subscribe to domains events about a tenant."); + + document.AssertByMessage(publish, "tenantEvent"); + } + + [AsyncApi] + private interface IAnnotatedServiceEvents + { + [Channel("interface.event.service.anotated.interface")] + [PublishOperation(typeof(TenantEvent), Description = "(interface.event.service.anotated.interface) Subscribe to domains events about a tenant.")] + void PublishEvent(TenantEvent evt); + } + + private interface IServiceEvents + { + void PublishEvent(TenantEvent evt); + } + + private class ServiceEventsFromInterface : IServiceEvents + { + public void PublishEvent(TenantEvent evt) { } + } + + private class ServiceEventsFromAnnotatedInterface : IAnnotatedServiceEvents + { + public void PublishEvent(TenantEvent evt) { } + } + + [AsyncApi] + private class AnnotatedServiceEventsFromAnnotatedInterface : IAnnotatedServiceEvents + { + [Channel("class.event.service.anotated.interface")] + [PublishOperation(typeof(TenantEvent), Description = "(class.event.service.anotated.interface) Subscribe to domains events about a tenant.")] + public void PublishEvent(TenantEvent evt) { } + } + + [AsyncApi] + private class SecondAnnotatedServiceEventsFromAnnotatedInterface : IAnnotatedServiceEvents + { + [Channel("class.event.secondservice.anotated.interface")] + [PublishOperation(typeof(TenantEvent), Description = "(class.event.secondservice.anotated.interface) Subscribe to domains events about a tenant.")] + public void PublishEvent(TenantEvent evt) { } + } + + private class TenantEvent { } + } +} diff --git a/test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/MethodAttributesTests.cs b/test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/MethodAttributesTests.cs new file mode 100644 index 00000000..6a9ad1d6 --- /dev/null +++ b/test/Saunter.Tests/AttributeProvider/DocumentGenerationTests/MethodAttributesTests.cs @@ -0,0 +1,124 @@ +using System; +using LEGO.AsyncAPI.Bindings.Kafka; +using Saunter.AttributeProvider.Attributes; +using Shouldly; +using Xunit; + +namespace Saunter.Tests.AttributeProvider.DocumentGenerationTests +{ + public class MethodAttributesTests + { + [Fact] + public void GenerateDocument_GeneratesDocumentWithMultipleMessagesPerChannel() + { + // Arrange + ArrangeAttributesTests.Arrange(out var options, out var documentProvider, typeof(TenantMessagePublisher)); + + // Act + var document = documentProvider.GetDocument(null, options); + + // Assert + document.ShouldNotBeNull(); + + var channel = document.AssertAndGetChannel("asw.tenant_service.tenants_history", "Tenant events."); + + var publish = channel.Publish; + publish.ShouldNotBeNull(); + publish.OperationId.ShouldBe("TenantMessagePublisher"); + publish.Summary.ShouldBe("Publish domains events about tenants."); + + document.AssertByMessage(publish, "anyTenantCreated", "anyTenantUpdated", "anyTenantRemoved"); + } + + [AsyncApi] + public class TenantMessagePublisher : ITenantMessagePublisher + { + [Channel("asw.tenant_service.tenants_history", Description = "Tenant events.")] + [PublishOperation(OperationId = "TenantMessagePublisher", Summary = "Publish domains events about tenants.")] + [Message(typeof(AnyTenantCreated))] + [Message(typeof(AnyTenantUpdated))] + [Message(typeof(AnyTenantRemoved))] + public void PublishTenantEvent(Guid tenantId, TEvent @event) + where TEvent : IEvent + { + } + } + + [Fact] + public void GenerateDocument_GeneratesDocumentWithKafkaOperationBinding() + { + // Arrange + ArrangeAttributesTests.Arrange(out var options, out var documentProvider, typeof(TenantMessagePublisherWithBind)); + + options.AsyncApi.Components = new() + { + OperationBindings = + { + { + "sample_kaffka", + new() + { + new KafkaOperationBinding() + { + ClientId = new() + { + Type = LEGO.AsyncAPI.Models.SchemaType.Integer, + } + } + } + } + } + }; + + // Act + var document = documentProvider.GetDocument(null, options); + + // Assert + document.ShouldNotBeNull(); + + var channel = document.AssertAndGetChannel("asw.tenant_service.tenants_history.with_bind", "Tenant events."); + + var publish = channel.Publish; + publish.ShouldNotBeNull(); + publish.OperationId.ShouldBe("TenantMessagePublisher"); + publish.Summary.ShouldBe("Publish domains events about tenants."); + publish.Bindings.Reference.Reference.ShouldBe("#/components/operationBindings/sample_kaffka"); + + document.AssertByMessage(publish, "anyTenantCreated"); + + document.Components.OperationBindings.ShouldContainKey("sample_kaffka"); + var bindMap = document.Components.OperationBindings["sample_kaffka"]; + var operationBinding = bindMap["kafka"]; + var kafkaOperationBinding = operationBinding.ShouldBeOfType(); + + kafkaOperationBinding.ClientId.ShouldNotBeNull(); + kafkaOperationBinding.ClientId.Type.ShouldBe(LEGO.AsyncAPI.Models.SchemaType.Integer); + } + + [AsyncApi] + public class TenantMessagePublisherWithBind : ITenantMessagePublisher + { + [Channel("asw.tenant_service.tenants_history.with_bind", Description = "Tenant events.")] + [PublishOperation(OperationId = "TenantMessagePublisher", Summary = "Publish domains events about tenants.", BindingsRef = "sample_kaffka")] + [Message(typeof(AnyTenantCreated))] + public void PublishTenantEvent(Guid tenantId, TEvent @event) + where TEvent : IEvent + { + } + } + } + + public class AnyTenantCreated : IEvent { } + + public class AnyTenantUpdated : IEvent { } + + public class AnyTenantRemoved : IEvent { } + + public interface IEvent { } + + public interface ITenantMessagePublisher + { + void PublishTenantEvent(Guid tenantId, TEvent @event) + where TEvent : IEvent; + } +} diff --git a/test/Saunter.Tests/AttributeProvider/DocumentProviderTests/AsyncApiTypesTests.cs b/test/Saunter.Tests/AttributeProvider/DocumentProviderTests/AsyncApiTypesTests.cs new file mode 100644 index 00000000..59842f53 --- /dev/null +++ b/test/Saunter.Tests/AttributeProvider/DocumentProviderTests/AsyncApiTypesTests.cs @@ -0,0 +1,41 @@ +using LEGO.AsyncAPI.Models; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Saunter.Options; +using Saunter.Tests.MarkerTypeTests; +using Shouldly; +using Xunit; + +namespace Saunter.Tests.AttributeProvider.DocumentProviderTests +{ + public class AsyncApiTypesTests + { + [Fact] + public void GetDocument_GeneratesDocumentWithMultipleMessagesPerChannel() + { + var services = new ServiceCollection() as IServiceCollection; + + services.AddFakeLogging(); + services.AddAsyncApiSchemaGeneration(o => + { + o.AsyncApi = new AsyncApiDocument + { + Info = new() + { + Title = GetType().FullName, + Version = "1.0.0" + }, + }; + o.AssemblyMarkerTypes = new[] { typeof(AnotherSamplePublisher), typeof(SampleConsumer) }; + }); + + using var serviceprovider = services.BuildServiceProvider(); + + var documentProvider = serviceprovider.GetRequiredService(); + var options = serviceprovider.GetRequiredService>().Value; + var document = documentProvider.GetDocument(null, options); + + document.ShouldNotBeNull(); + } + } +} diff --git a/test/Saunter.Tests/Generation/Filters/DocumentFilterTests.cs b/test/Saunter.Tests/AttributeProvider/Filters/DocumentFilterTests.cs similarity index 51% rename from test/Saunter.Tests/Generation/Filters/DocumentFilterTests.cs rename to test/Saunter.Tests/AttributeProvider/Filters/DocumentFilterTests.cs index c2074cc8..ac424d90 100644 --- a/test/Saunter.Tests/Generation/Filters/DocumentFilterTests.cs +++ b/test/Saunter.Tests/AttributeProvider/Filters/DocumentFilterTests.cs @@ -1,12 +1,11 @@ using System.Collections.Generic; -using System.Reflection; -using Saunter.AsyncApiSchema.v2; -using Saunter.Generation; -using Saunter.Generation.Filters; +using LEGO.AsyncAPI.Models; +using Saunter.Options.Filters; +using Saunter.Tests.AttributeProvider.DocumentGenerationTests; using Shouldly; using Xunit; -namespace Saunter.Tests.Generation.Filters +namespace Saunter.Tests.AttributeProvider.Filters { public class DocumentFilterTests { @@ -14,12 +13,12 @@ public class DocumentFilterTests public void DocumentFilterIsAppliedToAsyncApiDocument() { // Arrange - var options = new AsyncApiOptions(); - var documentGenerator = new DocumentGenerator(); + ArrangeAttributesTests.Arrange(out var options, out var documentProvider, GetType()); - // Act options.AddDocumentFilter(); - var document = documentGenerator.GenerateDocument(new[] { GetType().GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance); + + // Act + var document = documentProvider.GetDocument(null, options); // Assert document.ShouldNotBeNull(); @@ -31,30 +30,30 @@ public void DocumentFilterIsAppliedToAsyncApiDocument() public void DocumentNameIsAppliedToAsyncApiDocument() { // Arrange - const string documentName = "Test Document"; - var options = new AsyncApiOptions(); - options.AsyncApi.DocumentName = documentName; - var documentGenerator = new DocumentGenerator(); + const string DocumentName = "Test Document"; - // Act + ArrangeAttributesTests.Arrange(out var options, out var documentProvider, GetType()); + + options.NamedApis[DocumentName] = new(); options.AddDocumentFilter(); - var document = documentGenerator.GenerateDocument(new[] { GetType().GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance); + + // Act + var document = documentProvider.GetDocument(DocumentName, options); // Assert document.ShouldNotBeNull(); - document.DocumentName.ShouldBe(documentName); } private class ExampleDocumentFilter : IDocumentFilter { public void Apply(AsyncApiDocument document, DocumentFilterContext context) { - var channel = new ChannelItem + var channel = new AsyncApiChannel { Description = "an example channel for testing" }; - document.Channels.Add(new KeyValuePair("foo", channel)); + document.Channels.Add(new KeyValuePair("foo", channel)); } } } diff --git a/test/Saunter.Tests/AttributeProvider/OperationTraitsTests.cs b/test/Saunter.Tests/AttributeProvider/OperationTraitsTests.cs new file mode 100644 index 00000000..9ea6196f --- /dev/null +++ b/test/Saunter.Tests/AttributeProvider/OperationTraitsTests.cs @@ -0,0 +1,67 @@ +using LEGO.AsyncAPI.Models; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Saunter.Options; +using Saunter.Options.Filters; +using Shouldly; +using Xunit; + +namespace Saunter.Tests.AttributeProvider +{ + public class OperationTraitsTests + { + [Fact] + public void Example_OperationTraits() + { + // TODO: this is not really a test, just an example of how you might use OperationTraits... + + var services = new ServiceCollection() as IServiceCollection; + + services.AddFakeLogging(); + services.AddAsyncApiSchemaGeneration(o => + { + o.AsyncApi = new AsyncApiDocument + { + Info = new AsyncApiInfo + { + Title = GetType().FullName, + Version = "1.0.0" + }, + Components = new() + { + OperationTraits = + { + ["exampleTrait"] = new AsyncApiOperationTrait { Description = "This is an example trait" } + } + } + }; + + o.AddOperationFilter(); + }); + + using var serviceprovider = services.BuildServiceProvider(); + + var documentProvider = serviceprovider.GetRequiredService(); + var options = serviceprovider.GetRequiredService>().Value; + var document = documentProvider.GetDocument(null, options); + + document.Components.OperationTraits.ShouldContainKey("exampleTrait"); + } + + + private class TestOperationTraitsFilter : IOperationFilter + { + public void Apply(AsyncApiOperation operation, OperationFilterContext context) + { + operation.Traits.Add(new AsyncApiOperationTrait() + { + Reference = new() + { + Id = "exampleTrait", + Type = ReferenceType.OperationTrait, + }, + }); + } + } + } +} diff --git a/test/Saunter.Tests/Generation/DocumentGeneratorTests/InterfaceAttributeTests.cs b/test/Saunter.Tests/Generation/DocumentGeneratorTests/InterfaceAttributeTests.cs deleted file mode 100644 index 4fa225c8..00000000 --- a/test/Saunter.Tests/Generation/DocumentGeneratorTests/InterfaceAttributeTests.cs +++ /dev/null @@ -1,107 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Linq; -using System.Reflection; -using Saunter.AsyncApiSchema.v2; -using Saunter.Attributes; -using Saunter.Generation; -using Shouldly; -using Xunit; - -namespace Saunter.Tests.Generation.DocumentGeneratorTests -{ - public class InterfaceAttributeTests - { - [Theory] - [InlineData(typeof(IServiceEvents))] - [InlineData(typeof(ServiceEventsFromInterface))] - [InlineData(typeof(ServiceEventsFromAnnotatedInterface))] // Check that annotations are not inherited from the interface - public void NonAnnotatedTypesTest(Type type) - { - // Arrange - var options = new AsyncApiOptions(); - var documentGenerator = new DocumentGenerator(); - - // Act - var document = documentGenerator.GenerateDocument(new[] { type.GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance); - - // Assert - document.ShouldNotBeNull(); - document.Channels.Count.ShouldBe(0); - } - - [Theory] - [InlineData(typeof(IAnnotatedServiceEvents), "interface")] - [InlineData(typeof(AnnotatedServiceEventsFromInterface), "class")] - [InlineData(typeof(AnnotatedServiceEventsFromAnnotatedInterface), "class")] // Check that the actual type's annotation takes precedence of the inherited interface - public void AnnotatedTypesTest(Type type, string source) - { - // Arrange - var options = new AsyncApiOptions(); - var documentGenerator = new DocumentGenerator(); - - // Act - var document = documentGenerator.GenerateDocument(new[] { type.GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance); - - // Assert - document.ShouldNotBeNull(); - document.Channels.Count.ShouldBe(1); - - var channel = document.Channels.First(); - channel.Key.ShouldBe($"{source}.event"); - channel.Value.Description.ShouldBeNull(); - - var publish = channel.Value.Publish; - publish.ShouldNotBeNull(); - publish.OperationId.ShouldBe("PublishEvent"); - publish.Description.ShouldBe($"({source}) Subscribe to domains events about a tenant."); - - var messageRef = publish.Message.ShouldBeOfType(); - messageRef.Id.ShouldBe("tenantEvent"); - } - - [AsyncApi] - private interface IAnnotatedServiceEvents - { - [Channel("interface.event")] - [PublishOperation(typeof(TenantEvent), Description = "(interface) Subscribe to domains events about a tenant.")] - void PublishEvent(TenantEvent evt); - } - - private interface IServiceEvents - { - void PublishEvent(TenantEvent evt); - } - - private class ServiceEventsFromInterface : IServiceEvents - { - public void PublishEvent(TenantEvent evt) { } - } - - private class ServiceEventsFromAnnotatedInterface : IAnnotatedServiceEvents - { - public void PublishEvent(TenantEvent evt) { } - } - - [AsyncApi] - private class AnnotatedServiceEventsFromInterface : IAnnotatedServiceEvents - { - [Channel("class.event")] - [PublishOperation(typeof(TenantEvent), Description = "(class) Subscribe to domains events about a tenant.")] - public void PublishEvent(TenantEvent evt) { } - } - - [AsyncApi] - private class AnnotatedServiceEventsFromAnnotatedInterface : IAnnotatedServiceEvents - { - [Channel("class.event")] - [PublishOperation(typeof(TenantEvent), Description = "(class) Subscribe to domains events about a tenant.")] - public void PublishEvent(TenantEvent evt) { } - } - - private class TenantEvent { } - } -} diff --git a/test/Saunter.Tests/Generation/DocumentGeneratorTests/MethodAttributesTests.cs b/test/Saunter.Tests/Generation/DocumentGeneratorTests/MethodAttributesTests.cs deleted file mode 100644 index c9df1034..00000000 --- a/test/Saunter.Tests/Generation/DocumentGeneratorTests/MethodAttributesTests.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using Microsoft.Extensions.Options; -using Saunter.AsyncApiSchema.v2; -using Saunter.Attributes; -using Saunter.Generation; -using Shouldly; -using Xunit; - -namespace Saunter.Tests.Generation.DocumentGeneratorTests -{ - public class MethodAttributesTests - { - [Fact] - public void GenerateDocument_GeneratesDocumentWithMultipleMessagesPerChannel() - { - // Arrange - var options = new AsyncApiOptions(); - var documentGenerator = new DocumentGenerator(); - - // Act - var document = documentGenerator.GenerateDocument(new[] { typeof(TenantMessagePublisher).GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance); - - // 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 publish = channel.Value.Publish; - publish.ShouldNotBeNull(); - publish.OperationId.ShouldBe("TenantMessagePublisher"); - publish.Summary.ShouldBe("Publish domains events about tenants."); - - var messages = publish.Message.ShouldBeOfType(); - messages.OneOf.Count.ShouldBe(3); - - messages.OneOf.OfType().ShouldContain(m => m.Id == "anyTenantCreated"); - messages.OneOf.OfType().ShouldContain(m => m.Id == "anyTenantUpdated"); - messages.OneOf.OfType().ShouldContain(m => m.Id == "anyTenantRemoved"); - } - - [AsyncApi] - public class TenantMessagePublisher : ITenantMessagePublisher - { - [Channel("asw.tenant_service.tenants_history", Description = "Tenant events.")] - [PublishOperation(OperationId = "TenantMessagePublisher", Summary = "Publish domains events about tenants.")] - [Message(typeof(AnyTenantCreated))] - [Message(typeof(AnyTenantUpdated))] - [Message(typeof(AnyTenantRemoved))] - public void PublishTenantEvent(Guid tenantId, TEvent @event) - where TEvent : IEvent - { - } - } - } - - public class AnyTenantCreated : IEvent { } - - public class AnyTenantUpdated : IEvent { } - - public class AnyTenantRemoved : IEvent { } - - public interface IEvent { } - - public interface ITenantMessagePublisher - { - void PublishTenantEvent(Guid tenantId, TEvent @event) - where TEvent : IEvent; - } -} diff --git a/test/Saunter.Tests/Generation/DocumentProviderTests/AsyncApiTypesTests.cs b/test/Saunter.Tests/Generation/DocumentProviderTests/AsyncApiTypesTests.cs deleted file mode 100644 index c3692640..00000000 --- a/test/Saunter.Tests/Generation/DocumentProviderTests/AsyncApiTypesTests.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Saunter.AsyncApiSchema.v2; -using Saunter.Attributes; -using Saunter.Tests.MarkerTypeTests; -using Shouldly; -using Xunit; - -namespace Saunter.Tests.Generation.DocumentProviderTests -{ - public class AsyncApiTypesTests - { - [Fact] - public void GetDocument_GeneratesDocumentWithMultipleMessagesPerChannel() - { - var services = new ServiceCollection() as IServiceCollection; - services.AddAsyncApiSchemaGeneration(o => - { - o.AsyncApi = new AsyncApiDocument - { - Info = new Info(GetType().FullName, "1.0.0") - }; - o.AssemblyMarkerTypes = new[] { typeof(AnotherSamplePublisher), typeof(SampleConsumer) }; - }); - - using (var serviceprovider = services.BuildServiceProvider()) - { - var documentProvider = serviceprovider.GetRequiredService(); - var options = serviceprovider.GetRequiredService>().Value; - var document = documentProvider.GetDocument(options, options.AsyncApi); - - document.ShouldNotBeNull(); - } - } - - - } -} diff --git a/test/Saunter.Tests/Generation/OperationTraitsTests.cs b/test/Saunter.Tests/Generation/OperationTraitsTests.cs deleted file mode 100644 index 56ee526a..00000000 --- a/test/Saunter.Tests/Generation/OperationTraitsTests.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Saunter.AsyncApiSchema.v2; -using Saunter.AsyncApiSchema.v2.Traits; -using Saunter.Generation; -using Saunter.Generation.Filters; -using Shouldly; -using Xunit; - -namespace Saunter.Tests.Generation -{ - public class OperationTraitsTests - { - [Fact] - public void Example_OperationTraits() - { - // TODO: this is not really a test, just an example of how you might use OperationTraits... - - var services = new ServiceCollection() as IServiceCollection; - services.AddAsyncApiSchemaGeneration(o => - { - o.AsyncApi = new AsyncApiDocument - { - Info = new Info(GetType().FullName, "1.0.0"), - Components = - { - OperationTraits = - { - ["exampleTrait"] = new OperationTrait { Description = "This is an example trait" } - } - } - }; - - o.AddOperationFilter(); - }); - - using (var serviceprovider = services.BuildServiceProvider()) - { - var documentProvider = serviceprovider.GetRequiredService(); - var options = serviceprovider.GetRequiredService>().Value; - var document = documentProvider.GetDocument(options, options.AsyncApi); - - document.Components.OperationTraits.ShouldContainKey("exampleTrait"); - } - } - - - private class TestOperationTraitsFilter : IOperationFilter - { - public void Apply(Operation publishOperation, OperationFilterContext context) - { - publishOperation.Traits.Add(new OperationTraitReference("exampleTrait")); - } - } - } -} diff --git a/test/Saunter.Tests/Generation/SchemaGeneration/SchemaGenerationTests.cs b/test/Saunter.Tests/Generation/SchemaGeneration/SchemaGenerationTests.cs deleted file mode 100644 index 41d11a8c..00000000 --- a/test/Saunter.Tests/Generation/SchemaGeneration/SchemaGenerationTests.cs +++ /dev/null @@ -1,245 +0,0 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Runtime.Serialization; - -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -using NJsonSchema; -using NJsonSchema.Generation; -using NJsonSchema.NewtonsoftJson.Converters; - -using Saunter.AsyncApiSchema.v2; -using Saunter.Generation.SchemaGeneration; -using Saunter.Tests.Utils; - -using Shouldly; - -using Xunit; - -using JsonInheritanceAttribute = NJsonSchema.NewtonsoftJson.Converters.JsonInheritanceAttribute; - -namespace Saunter.Tests.Generation.SchemaGeneration -{ - public class SchemaGenerationTests - { - private readonly AsyncApiSchemaResolver _schemaResolver; - private readonly JsonSchemaGenerator _schemaGenerator; - - public SchemaGenerationTests() - { - var settings = new AsyncApiSchemaOptions() - { - TypeNameGenerator = new CamelCaseTypeNameGenerator(), - SerializerSettings = new JsonSerializerSettings() - { - ContractResolver = new CamelCasePropertyNamesContractResolver() - }, - }; - _schemaResolver = new AsyncApiSchemaResolver(new AsyncApiDocument(), settings); - _schemaGenerator = new JsonSchemaGenerator(settings); - } - - [Fact] - public void GenerateSchema_GenerateSchemaFromTypeWithProperties_GeneratesSchemaCorrectly() - { - var type = typeof(Foo); - - var schema = _schemaGenerator.Generate(type, _schemaResolver); - - schema.ShouldNotBeNull(); - _schemaResolver.Schemas.ShouldNotBeNull(); - ResolverShouldHaveValidFooSchema(); - - var barSchema = _schemaResolver.Schemas.FirstOrDefault(sh => sh.Id == "bar"); - barSchema.ShouldNotBeNull(); - barSchema.Properties.Count.ShouldBe(2); - barSchema.Properties.ContainsKey("name").ShouldBeTrue(); - barSchema.Properties.ContainsKey("cost").ShouldBeTrue(); - } - - private void ResolverShouldHaveValidFooSchema() - { - var fooSchema = _schemaResolver.Schemas.FirstOrDefault(sh => sh.Id == "foo"); - fooSchema.ShouldNotBeNull(); - fooSchema.RequiredProperties.Count.ShouldBe(2); - fooSchema.RequiredProperties.Contains("id").ShouldBeTrue(); - fooSchema.RequiredProperties.Contains("bar").ShouldBeTrue(); - fooSchema.Properties.Count.ShouldBe(5); - fooSchema.Properties.ContainsKey("id").ShouldBeTrue(); - fooSchema.Properties.ContainsKey("bar").ShouldBeTrue(); - fooSchema.Properties.ContainsKey("fooType").ShouldBeTrue(); - fooSchema.Properties.ContainsKey("hello").ShouldBeTrue(); - fooSchema.Properties.ContainsKey("world").ShouldBeTrue(); - } - - [Fact] - public void GenerateSchema_GenerateSchemaFromTypeWithFields_GeneratesSchemaCorrectly() - { - var type = typeof(Book); - - var schema = _schemaGenerator.Generate(type, _schemaResolver); - - schema.ShouldNotBeNull(); - _schemaResolver.Schemas.ShouldNotBeNull(); - var bookSchema = _schemaResolver.Schemas.FirstOrDefault(sh => sh.Id == "book"); - bookSchema.ShouldNotBeNull(); - bookSchema.Properties.Count.ShouldBe(4); - - ResolverShouldHaveValidFooSchema(); - } - - [Fact] - public void GenerateSchema_GenerateSchemaFromClassWithDiscriminator_GeneratesSchemaCorrectly() - { - var type = typeof(PetOwner); - - var schema = _schemaGenerator.Generate(type, _schemaResolver); - - schema.ShouldNotBeNull(); - - _schemaResolver.Schemas.ShouldNotBeNull(); - var petSchema = _schemaResolver.Schemas.FirstOrDefault(s => s.Id == "pet"); - petSchema.Discriminator.ShouldBe("petType"); - - schema.Properties["pet"].IsNullable(SchemaType.JsonSchema).ShouldBeTrue(); - - var catSchema = _schemaResolver.Schemas.FirstOrDefault(s => s.Id == "cat"); - var catProperties = catSchema.MergeAllProperties(); - catProperties.Count.ShouldBe(3); - catProperties.ContainsKey("petType").ShouldBeTrue(); - catProperties.ContainsKey("name").ShouldBeTrue(); - catProperties.ContainsKey("huntingSkill").ShouldBeTrue(); - - var dogSchema = _schemaResolver.Schemas.FirstOrDefault(s => s.Id == "dog"); - var dogProperties = dogSchema.MergeAllProperties(); - dogProperties.Count.ShouldBe(3); - dogProperties.ContainsKey("petType").ShouldBeTrue(); - dogProperties.ContainsKey("name").ShouldBeTrue(); - dogProperties.ContainsKey("packSize").ShouldBeTrue(); - } - - [Fact()] - public void GenerateSchema_GenerateSchemaFromInterfaceWithDiscriminator_GeneratesSchemaCorrectly() - { - var type = typeof(IPetOwner); - - var schema = _schemaGenerator.Generate(type, _schemaResolver); - - schema.ShouldNotBeNull(); - _schemaResolver.Schemas.ShouldNotBeNull(); - var ipetSchema = _schemaResolver.Schemas.FirstOrDefault(s => s.Id == "iPet"); - ipetSchema.Discriminator.ShouldBe("petType"); - - schema.Properties["pet"].IsNullable(SchemaType.JsonSchema).ShouldBeTrue(); - - var catSchema = _schemaResolver.Schemas.FirstOrDefault(s => s.Id == "cat"); - var catProperties = catSchema.MergeAllProperties(); - catProperties.Count.ShouldBe(3); - catProperties.ContainsKey("petType").ShouldBeTrue(); - catProperties.ContainsKey("name").ShouldBeTrue(); - catProperties.ContainsKey("huntingSkill").ShouldBeTrue(); - - var dogSchema = _schemaResolver.Schemas.FirstOrDefault(s => s.Id == "dog"); - var dogProperties = dogSchema.MergeAllProperties(); - dogProperties.Count.ShouldBe(3); - dogProperties.ContainsKey("petType").ShouldBeTrue(); - dogProperties.ContainsKey("name").ShouldBeTrue(); - dogProperties.ContainsKey("packSize").ShouldBeTrue(); - } - } - - public class Foo - { - [Required] - public Guid Id { get; set; } - - [JsonIgnore] - [System.Text.Json.Serialization.JsonIgnore] - public string Ignore { get; set; } - - [Required] - public Bar Bar { get; set; } - - [JsonProperty("hello")] - public string HelloWorld { get; set; } - - [DataMember(Name = "myworld")] - public string World { get; set; } - - public FooType FooType { get; set; } - } - - public enum FooType - { - Foo, - Bar - } - - public class Bar - { - public string Name { get; set; } - - public decimal? Cost { get; set; } - } - - public class Book - { - public readonly string Name; - - public readonly string Author; - - public readonly int NumberOfPages; - - public readonly Foo Foo; - - public Book(string name, string author, int numberOfPages, Foo foo) - { - Author = author; - Name = name; - NumberOfPages = numberOfPages; - Foo = foo; - } - } - - public class PetOwner - { - public Pet Pet { get; set; } - } - - public class IPetOwner - { - public IPet Pet { get; set; } - } - - [JsonConverter(typeof(JsonInheritanceConverter), "petType")] - [JsonInheritance("cat", typeof(Cat))] - [JsonInheritance("dog", typeof(Dog))] - public interface IPet - { - string PetType { get; } - - string Name { get; } - } - - [JsonConverter(typeof(JsonInheritanceConverter), "petType")] - [KnownType(typeof(Cat))] - [KnownType(typeof(Dog))] - public abstract class Pet : IPet - { - public string PetType { get; set; } - - public string Name { get; set; } - } - - public class Cat : Pet - { - public string HuntingSkill { get; set; } - } - - public class Dog : Pet - { - public string PackSize { get; set; } - } -} diff --git a/test/Saunter.Tests/Saunter.Tests.csproj b/test/Saunter.Tests/Saunter.Tests.csproj index 8f47b9f1..6adf9641 100644 --- a/test/Saunter.Tests/Saunter.Tests.csproj +++ b/test/Saunter.Tests/Saunter.Tests.csproj @@ -2,7 +2,6 @@ net6.0 - false @@ -17,6 +16,7 @@ + diff --git a/test/Saunter.Tests/ServiceCollectionTests.cs b/test/Saunter.Tests/ServiceCollectionTests.cs index 1d097c7b..9b6f1530 100644 --- a/test/Saunter.Tests/ServiceCollectionTests.cs +++ b/test/Saunter.Tests/ServiceCollectionTests.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; +using LEGO.AsyncAPI.Models; using Microsoft.Extensions.DependencyInjection; -using Saunter.AsyncApiSchema.v2; -using Saunter.Generation; +using Saunter.Options; using Shouldly; using Xunit; @@ -12,44 +12,62 @@ namespace Saunter.Tests /// public class ServiceCollectionTests { - [Fact] public void TestAddAsyncApiSchemaGeneration() { var services = new ServiceCollection() as IServiceCollection; + + services.AddFakeLogging(); services.AddAsyncApiSchemaGeneration(options => { - options.AsyncApi = new AsyncApiSchema.v2.AsyncApiDocument + options.AsyncApi = new AsyncApiDocument { Id = "urn:com:example:example-events", - Info = new Info("Example API", "2019.01.12345") + Info = new() { + Title = "Example API", + Version = "2019.01.12345", Description = "An example API with events", - Contact = new Contact + Contact = new AsyncApiContact() { Email = "michael@mwild.me", Name = "Michael Wildman", - Url = "https://mwild.me/", + Url = new("https://mwild.me/"), + }, + License = new AsyncApiLicense() + { + Name = "MIT", }, - License = new License("MIT"), - TermsOfService = "https://mwild.me/tos", + TermsOfService = new("https://mwild.me/tos"), + }, + Tags = + { + new() { Name = "example" }, + new() { Name = "event" } }, - Tags = { "example", "event" }, Servers = { { "development", - new Server("rabbitmq.dev.mwild.me", "amqp") + new AsyncApiServer { - Security = new List>> { new Dictionary> { { "user-password", new List() } }} + Protocol = "amqp", + Url = "rabbitmq.dev.mwild.me", + Security = new List + { + new() + { + { new AsyncApiSecurityScheme() { Type= SecuritySchemeType.UserPassword }, new List() } + } + } } } }, Components = { - SecuritySchemes = new Dictionary + SecuritySchemes = new Dictionary { - { "user-password", new SecurityScheme(SecuritySchemeType.Http) } + { "user-password", new AsyncApiSecurityScheme(){ Type = SecuritySchemeType.Http } } } } }; @@ -59,7 +77,7 @@ public void TestAddAsyncApiSchemaGeneration() var provider = sp.GetRequiredService(); - var document = provider.GetDocument(new AsyncApiOptions(), new AsyncApiDocument()); + var document = provider.GetDocument(null, new AsyncApiOptions()); document.ShouldNotBeNull(); } diff --git a/test/Saunter.Tests/SharedKernel/ChannelUnionTests.cs b/test/Saunter.Tests/SharedKernel/ChannelUnionTests.cs new file mode 100644 index 00000000..eb7b42ff --- /dev/null +++ b/test/Saunter.Tests/SharedKernel/ChannelUnionTests.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using LEGO.AsyncAPI.Models; +using Saunter.SharedKernel; +using Shouldly; +using Xunit; + +namespace Saunter.Tests.SharedKernel +{ + public class ChannelUnionTests + { + public static IEnumerable GetOnUnionConflictData() + { + yield return new AsyncApiChannel[] + { + new() { Publish = new() }, + new() { Publish = new() }, + }; + + yield return new AsyncApiChannel[] + { + new() { Subscribe = new() }, + new() { Subscribe = new() }, + }; + + yield return new AsyncApiChannel[] + { + new() { Reference = new() }, + new() { Reference = new() }, + }; + + yield return new AsyncApiChannel[] + { + new() { Reference = new() }, + new() { Subscribe = new() }, + }; + + yield return new AsyncApiChannel[] + { + new() { Reference = new() }, + new() { Publish = new() }, + }; + } + + [Theory] + [MemberData(nameof(GetOnUnionConflictData))] + public void AsyncApiChannelUnion_OnUnion_Conflict(AsyncApiChannel source, AsyncApiChannel additionaly) + { + // Arrange + AsyncApiChannelUnion channelUnion = new(); + + // Act + var actual = () => channelUnion.Union(source, additionaly); + + // Assert + Should.Throw(actual); + } + + public static IEnumerable GetOnUnionSuccessMerge() + { + yield return new AsyncApiChannel[] + { + new() { }, + new() { Publish = new(), Subscribe = new() }, + new() { Publish = new(), Subscribe = new() }, + }; + yield return new AsyncApiChannel[] + { + new() { Publish = new(), Subscribe = new() }, + new() { }, + new() { Publish = new(), Subscribe = new() }, + }; + yield return new AsyncApiChannel[] + { + new() { Publish = new() }, + new() { Subscribe = new() }, + new() { Publish = new(), Subscribe = new() }, + }; + yield return new AsyncApiChannel[] + { + new() { Subscribe = new() }, + new() { Publish = new() }, + new() { Publish = new(), Subscribe = new() }, + }; + yield return new AsyncApiChannel[] + { + new() { Reference = new() }, + new() { }, + new() { Reference = new() }, + }; + yield return new AsyncApiChannel[] + { + new() { }, + new() { Reference = new() }, + new() { Reference = new() }, + }; + yield return new AsyncApiChannel[] + { + new() + { + Description = "description", + Servers = new List() { "server1", "server2", }, + Parameters = new Dictionary() + { + { "test", new() { Description = "description" } } + }, + }, + new() { }, + new() + { + Description = "description", + Servers = new List() { "server1", "server2", }, + Parameters = new Dictionary() + { + { "test", new() { Description = "description" } } + }, + }, + }; + } + + [Theory] + [MemberData(nameof(GetOnUnionSuccessMerge))] + public void AsyncApiChannelUnion_OnUnion_SuccessMerge(AsyncApiChannel source, AsyncApiChannel additionaly, AsyncApiChannel expected) + { + // Arrange + AsyncApiChannelUnion channelUnion = new(); + + // Act + var actual = channelUnion.Union(source, additionaly); + + // Assert + actual.ShouldNotBeNull(); + + actual.Description.ShouldBe(expected.Description); + actual.Servers.ShouldBe(expected.Servers); + + actual.Parameters.Count.ShouldBe(expected.Parameters.Count); + + foreach (var item in expected.Parameters) + { + actual.Parameters.ShouldContainKey(item.Key); + } + + if (actual.Publish is null) + { + expected.Publish.ShouldBeNull(); + } + else + { + expected.Publish.ShouldNotBeNull(); + actual.Publish.OperationId.ShouldBe(expected.Publish.OperationId); + } + + if (actual.Subscribe is null) + { + expected.Subscribe.ShouldBeNull(); + } + else + { + expected.Subscribe.ShouldNotBeNull(); + actual.Subscribe.OperationId.ShouldBe(expected.Subscribe.OperationId); + } + + if (actual.Reference is null) + { + expected.Reference.ShouldBeNull(); + } + else + { + expected.Reference.ShouldNotBeNull(); + actual.Reference.Id.ShouldBe(expected.Reference.Id); + actual.Reference.Type.ShouldBe(expected.Reference.Type); + } + } + } +} diff --git a/test/Saunter.Tests/SharedKernel/DocumentSerializeClonerTests.cs b/test/Saunter.Tests/SharedKernel/DocumentSerializeClonerTests.cs new file mode 100644 index 00000000..9fc46539 --- /dev/null +++ b/test/Saunter.Tests/SharedKernel/DocumentSerializeClonerTests.cs @@ -0,0 +1,364 @@ +using LEGO.AsyncAPI.Models; +using Microsoft.Extensions.Logging.Testing; +using Saunter.SharedKernel; +using Xunit; + +namespace Saunter.Tests.SharedKernel +{ + public class DocumentSerializeClonerTests + { + private readonly AsyncApiDocumentSerializeCloner _cloner; + + public DocumentSerializeClonerTests() + { + _cloner = new AsyncApiDocumentSerializeCloner(new FakeLogger()); + } + + [Fact] + public void CloneProtype_ShouldCloneDocumentSuccessfully() + { + // Arrange + var prototype = new AsyncApiDocument() + { + Id = "id document", + Asyncapi = "2.6.0", + Info = new() + { + Version = "1.0.0", + Title = "title", + Description = "description", + License = new() + { + Url = new("http://localhost:9200"), + Name = "test", + }, + Contact = new() + { + Url = new("http://localhost:9201"), + Name = "contact", + Email = "gmail.ru", + }, + TermsOfService = new("http://localhost:9202"), + }, + Tags = + { + new() + { + Name = "test", + Description = "descriptions", + ExternalDocs = new() + { + Url = new("http://localhost:9203"), + Description = "ExternalDocs", + }, + } + }, + DefaultContentType = "default/type", + ExternalDocs = new() + { + Url = new("http://localhost:9204"), + Description = "tester", + }, + Servers = + { + { + "one", + new() + { + Url = "hellowa", + Description = "server desc", + Protocol = "kaffka", + ProtocolVersion = "0.0.1", + Tags = { new() { Name = "kaffka tag" } }, + Variables = + { + { + "var", + new() + { + Default = "default", + Description = "default var", + Enum = { "q", "w", "e" }, + Examples = { "example one" }, + } + } + } + } + } + }, + Channels = + { + { + "channel", + new() + { + Description = "description channel", + Servers = { "one" }, + Parameters = + { + { + "params" , + new() + { + Location = "default", + Description = "default var", + Schema = new(){ Type = SchemaType.String }, + } + } + }, + Publish = new() + { + Description = "operation descr", + ExternalDocs=new() + { + Url = new("http://localhost:9205"), + Description = "tester", + }, + OperationId = "1", + Tags = { new() { Name = "tag" } }, + Summary = "my summary", + Message = + { + new() + { + Summary = "message summary", + Description = "message description", + ContentType = "application/json", + ExternalDocs = new() + { + Url = new("http://localhost:9206"), + Description = "message tester", + }, + MessageId = "message 1", + Name = "message name 1", + Title = "message title 1", + Payload = new() + { + Deprecated = true, + Description = "payload", + Type = SchemaType.String, + Default = new("empty"), + ReadOnly = false, + WriteOnly = true, + Title = "title test", + }, + }, + }, + }, + Subscribe = new() + { + Description = "subscribe operation descr", + ExternalDocs=new() + { + Url = new("http://localhost:9207"), + Description = "subscribe tester", + }, + OperationId = "subscribe 1", + Tags = { new() { Name = "subscribe tag" } }, + Summary = "subscribe my summary", + Message = + { + new() + { + MessageId = "subscribe message 1", + Summary = "subscribe message summary", + Description = "subscribe message description", + ContentType = "application/json", + ExternalDocs = new() + { + Url = new("http://localhost:9208"), + Description = "subscribe message tester", + }, + Name = "subscribe message name 1", + Title = "subscribe message title 1", + Payload = new() + { + Deprecated = true, + Description = "subscribe payload", + Type = SchemaType.String, + Default = new("empty"), + ReadOnly = true, + WriteOnly = false, + Title = "title test", + }, + }, + }, + }, + } + } + } + }; + + // Act + var result = _cloner.CloneProtype(prototype); + + // Assert + Assert.NotNull(result); + Assert.Equal(prototype.Id, result.Id); + Assert.Equal(prototype.Asyncapi, result.Asyncapi); + + // Verify Info + Assert.Equal(prototype.Info.Version, result.Info.Version); + Assert.Equal(prototype.Info.Title, result.Info.Title); + Assert.Equal(prototype.Info.Description, result.Info.Description); + Assert.Equal(prototype.Info.License.Url, result.Info.License.Url); + Assert.Equal(prototype.Info.License.Name, result.Info.License.Name); + Assert.Equal(prototype.Info.Contact.Url, result.Info.Contact.Url); + Assert.Equal(prototype.Info.Contact.Name, result.Info.Contact.Name); + Assert.Equal(prototype.Info.Contact.Email, result.Info.Contact.Email); + Assert.Equal(prototype.Info.TermsOfService, result.Info.TermsOfService); + + // Verify DefaultContentType + Assert.Equal(prototype.DefaultContentType, result.DefaultContentType); + + // Verify ExternalDocs + Assert.Equal(prototype.ExternalDocs.Url, result.ExternalDocs.Url); + Assert.Equal(prototype.ExternalDocs.Description, result.ExternalDocs.Description); + + // Verify Tags + Assert.Equal(prototype.Tags.Count, result.Tags.Count); + for (var i = 0; i < prototype.Tags.Count; i++) + { + Assert.Equal(prototype.Tags[i].Name, result.Tags[i].Name); + Assert.Equal(prototype.Tags[i].Description, result.Tags[i].Description); + Assert.Equal(prototype.Tags[i].ExternalDocs.Url, result.Tags[i].ExternalDocs.Url); + Assert.Equal(prototype.Tags[i].ExternalDocs.Description, result.Tags[i].ExternalDocs.Description); + } + + // Verify Servers + Assert.Equal(prototype.Servers.Count, result.Servers.Count); + + foreach (var serverKey in prototype.Servers.Keys) + { + var prototypeServer = prototype.Servers[serverKey]; + var resultServer = result.Servers[serverKey]; + + Assert.Equal(prototypeServer.Url, resultServer.Url); + Assert.Equal(prototypeServer.Description, resultServer.Description); + Assert.Equal(prototypeServer.Protocol, resultServer.Protocol); + Assert.Equal(prototypeServer.ProtocolVersion, resultServer.ProtocolVersion); + Assert.Equal(prototypeServer.Tags.Count, resultServer.Tags.Count); + + for (var i = 0; i < prototypeServer.Tags.Count; i++) + { + Assert.Equal(prototypeServer.Tags[i].Name, resultServer.Tags[i].Name); + } + + Assert.Equal(prototypeServer.Variables.Count, resultServer.Variables.Count); + + foreach (var variableKey in prototypeServer.Variables.Keys) + { + var prototypeVariable = prototypeServer.Variables[variableKey]; + var resultVariable = resultServer.Variables[variableKey]; + + Assert.Equal(prototypeVariable.Default, resultVariable.Default); + Assert.Equal(prototypeVariable.Description, resultVariable.Description); + Assert.Equal(prototypeVariable.Enum, resultVariable.Enum); + Assert.Equal(prototypeVariable.Examples, resultVariable.Examples); + } + } + + // Verify Channels + Assert.Equal(prototype.Channels.Count, result.Channels.Count); + foreach (var channelKey in prototype.Channels.Keys) + { + var prototypeChannel = prototype.Channels[channelKey]; + var resultChannel = result.Channels[channelKey]; + + Assert.Equal(prototypeChannel.Description, resultChannel.Description); + Assert.Equal(prototypeChannel.Servers, resultChannel.Servers); + Assert.Equal(prototypeChannel.Parameters.Count, resultChannel.Parameters.Count); + + foreach (var parameterKey in prototypeChannel.Parameters.Keys) + { + var prototypeParameter = prototypeChannel.Parameters[parameterKey]; + var resultParameter = resultChannel.Parameters[parameterKey]; + + Assert.Equal(prototypeParameter.Description, resultParameter.Description); + Assert.Equal(prototypeParameter.Schema.Type, resultParameter.Schema.Type); + Assert.Equal(prototypeParameter.Location, resultParameter.Location); + } + + // Verify Publish + Assert.Equal(prototypeChannel.Publish.Description, resultChannel.Publish.Description); + Assert.Equal(prototypeChannel.Publish.ExternalDocs.Url, resultChannel.Publish.ExternalDocs.Url); + Assert.Equal(prototypeChannel.Publish.ExternalDocs.Description, resultChannel.Publish.ExternalDocs.Description); + Assert.Equal(prototypeChannel.Publish.OperationId, resultChannel.Publish.OperationId); + Assert.Equal(prototypeChannel.Publish.Tags.Count, resultChannel.Publish.Tags.Count); + + for (var i = 0; i < prototypeChannel.Publish.Tags.Count; i++) + { + Assert.Equal(prototypeChannel.Publish.Tags[i].Name, resultChannel.Publish.Tags[i].Name); + } + + Assert.Equal(prototypeChannel.Publish.Summary, resultChannel.Publish.Summary); + Assert.Equal(prototypeChannel.Publish.Message.Count, resultChannel.Publish.Message.Count); + + for (var i = 0; i < prototypeChannel.Publish.Message.Count; i++) + { + var prototypeMessage = prototypeChannel.Publish.Message[i]; + var resultMessage = resultChannel.Publish.Message[i]; + + // TODO: bug? + // Assert.Equal(prototypeMessage.MessageId, resultMessage.MessageId); + + Assert.Equal(prototypeMessage.Summary, resultMessage.Summary); + Assert.Equal(prototypeMessage.Description, resultMessage.Description); + Assert.Equal(prototypeMessage.SchemaFormat, resultMessage.SchemaFormat); + Assert.Equal(prototypeMessage.ContentType, resultMessage.ContentType); + Assert.Equal(prototypeMessage.ExternalDocs.Url, resultMessage.ExternalDocs.Url); + Assert.Equal(prototypeMessage.ExternalDocs.Description, resultMessage.ExternalDocs.Description); + Assert.Equal(prototypeMessage.Name, resultMessage.Name); + Assert.Equal(prototypeMessage.Title, resultMessage.Title); + Assert.Equal(prototypeMessage.Payload.Deprecated, resultMessage.Payload.Deprecated); + Assert.Equal(prototypeMessage.Payload.Description, resultMessage.Payload.Description); + Assert.Equal(prototypeMessage.Payload.Type, resultMessage.Payload.Type); + Assert.Equal(prototypeMessage.Payload.Default.GetValueOrDefault(), resultMessage.Payload.Default.GetValueOrDefault()); + Assert.Equal(prototypeMessage.Payload.ReadOnly, resultMessage.Payload.ReadOnly); + Assert.Equal(prototypeMessage.Payload.WriteOnly, resultMessage.Payload.WriteOnly); + Assert.Equal(prototypeMessage.Payload.Title, resultMessage.Payload.Title); + } + + // Verify Subscribe + Assert.Equal(prototypeChannel.Subscribe.Description, resultChannel.Subscribe.Description); + Assert.Equal(prototypeChannel.Subscribe.ExternalDocs.Url, resultChannel.Subscribe.ExternalDocs.Url); + Assert.Equal(prototypeChannel.Subscribe.ExternalDocs.Description, resultChannel.Subscribe.ExternalDocs.Description); + Assert.Equal(prototypeChannel.Subscribe.OperationId, resultChannel.Subscribe.OperationId); + Assert.Equal(prototypeChannel.Subscribe.Tags.Count, resultChannel.Subscribe.Tags.Count); + + for (var i = 0; i < prototypeChannel.Subscribe.Tags.Count; i++) + { + Assert.Equal(prototypeChannel.Subscribe.Tags[i].Name, resultChannel.Subscribe.Tags[i].Name); + } + + Assert.Equal(prototypeChannel.Subscribe.Summary, resultChannel.Subscribe.Summary); + Assert.Equal(prototypeChannel.Subscribe.Message.Count, resultChannel.Subscribe.Message.Count); + + for (var i = 0; i < prototypeChannel.Subscribe.Message.Count; i++) + { + var prototypeMessage = prototypeChannel.Subscribe.Message[i]; + var resultMessage = resultChannel.Subscribe.Message[i]; + + // TODO: bug? + // Assert.Equal(prototypeMessage.MessageId, resultMessage.MessageId); + + Assert.Equal(prototypeMessage.Summary, resultMessage.Summary); + Assert.Equal(prototypeMessage.Description, resultMessage.Description); + Assert.Equal(prototypeMessage.SchemaFormat, resultMessage.SchemaFormat); + Assert.Equal(prototypeMessage.ContentType, resultMessage.ContentType); + Assert.Equal(prototypeMessage.ExternalDocs.Url, resultMessage.ExternalDocs.Url); + Assert.Equal(prototypeMessage.ExternalDocs.Description, resultMessage.ExternalDocs.Description); + Assert.Equal(prototypeMessage.Name, resultMessage.Name); + Assert.Equal(prototypeMessage.Title, resultMessage.Title); + Assert.Equal(prototypeMessage.Payload.Deprecated, resultMessage.Payload.Deprecated); + Assert.Equal(prototypeMessage.Payload.Description, resultMessage.Payload.Description); + Assert.Equal(prototypeMessage.Payload.Type, resultMessage.Payload.Type); + Assert.Equal(prototypeMessage.Payload.Default.GetValueOrDefault(), resultMessage.Payload.Default.GetValueOrDefault()); + Assert.Equal(prototypeMessage.Payload.ReadOnly, resultMessage.Payload.ReadOnly); + Assert.Equal(prototypeMessage.Payload.WriteOnly, resultMessage.Payload.WriteOnly); + Assert.Equal(prototypeMessage.Payload.Title, resultMessage.Payload.Title); + } + } + } + } +} diff --git a/test/Saunter.Tests/SharedKernel/SchemaGeneratorTests.cs b/test/Saunter.Tests/SharedKernel/SchemaGeneratorTests.cs new file mode 100644 index 00000000..79268267 --- /dev/null +++ b/test/Saunter.Tests/SharedKernel/SchemaGeneratorTests.cs @@ -0,0 +1,207 @@ +using System; +using System.Linq; +using LEGO.AsyncAPI.Models; +using Saunter.SharedKernel; +using Shouldly; +using Xunit; + +namespace Saunter.Tests.SharedKernel +{ + public class SchemaGeneratorTests + { + [Theory] + [InlineData(typeof(bool), "boolean", SchemaType.Boolean, false)] + [InlineData(typeof(byte), "byte", SchemaType.Integer, false)] + [InlineData(typeof(short), "int16", SchemaType.Integer, false)] + [InlineData(typeof(ushort), "uInt16", SchemaType.Integer, false)] + [InlineData(typeof(int), "int32", SchemaType.Integer, false)] + [InlineData(typeof(uint), "uInt32", SchemaType.Integer, false)] + [InlineData(typeof(long), "int64", SchemaType.Integer, false)] + [InlineData(typeof(ulong), "uInt64", SchemaType.Integer, false)] + [InlineData(typeof(decimal), "decimal", SchemaType.Number, false)] + [InlineData(typeof(float), "single", SchemaType.Number, false)] + [InlineData(typeof(double), "double", SchemaType.Number, false)] + [InlineData(typeof(bool?), "boolean", SchemaType.Boolean, true)] + [InlineData(typeof(byte?), "byte", SchemaType.Integer, true)] + [InlineData(typeof(short?), "int16", SchemaType.Integer, true)] + [InlineData(typeof(ushort?), "uInt16", SchemaType.Integer, true)] + [InlineData(typeof(int?), "int32", SchemaType.Integer, true)] + [InlineData(typeof(uint?), "uInt32", SchemaType.Integer, true)] + [InlineData(typeof(long?), "int64", SchemaType.Integer, true)] + [InlineData(typeof(ulong?), "uInt64", SchemaType.Integer, true)] + [InlineData(typeof(decimal?), "decimal", SchemaType.Number, true)] + [InlineData(typeof(float?), "single", SchemaType.Number, true)] + [InlineData(typeof(double?), "double", SchemaType.Number, true)] + [InlineData(typeof(string), "string", SchemaType.String, true)] + [InlineData(typeof(DateTime), "dateTime", SchemaType.String, false)] + [InlineData(typeof(DateTimeOffset), "dateTimeOffset", SchemaType.String, false)] + [InlineData(typeof(DateOnly), "dateOnly", SchemaType.String, false)] + [InlineData(typeof(TimeOnly), "timeOnly", SchemaType.String, false)] + [InlineData(typeof(TimeSpan), "timeSpan", SchemaType.String, false)] + [InlineData(typeof(Guid), "guid", SchemaType.String, false)] + [InlineData(typeof(DateTime?), "dateTime", SchemaType.String, true)] + [InlineData(typeof(DateTimeOffset?), "dateTimeOffset", SchemaType.String, true)] + [InlineData(typeof(DateOnly?), "dateOnly", SchemaType.String, true)] + [InlineData(typeof(TimeOnly?), "timeOnly", SchemaType.String, true)] + [InlineData(typeof(TimeSpan?), "timeSpan", SchemaType.String, true)] + [InlineData(typeof(Guid?), "guid", SchemaType.String, true)] + [InlineData(typeof(Uri), "uri", SchemaType.String, true)] + [InlineData(typeof(object), null, SchemaType.Object, true)] + [InlineData(typeof(int[]), null, SchemaType.Array, true)] + [InlineData(typeof(object[]), null, SchemaType.Array, true)] + public void AsyncApiSchemaGenerator_OnGeneratePrimitive_SchemaTypeAndNameIsMatch(Type type, string format, SchemaType schemaType, bool nullable) + { + // Arrange + AsyncApiSchemaGenerator generator = new(); + + // Act + var schema = generator.Generate(type); + + // Assert + schema.ShouldNotBeNull(); + 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] + public void AsyncApiSchemaGenerator_OnGenerateParams_SchemaIsMatch() + { + // Arrange + AsyncApiSchemaGenerator generator = new(); + var type = typeof(Foo); + + // Act + var schema = generator.Generate(type); + + // Assert + schema.ShouldNotBeNull(); + schema.Value.All.Count.ShouldBe(8); + schema.Value.Root.Properties.Count.ShouldBe(7); + + 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.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.Value.Root.Properties.ShouldContainKey("bar"); + var bar = schema.Value.Root.Properties["bar"]; + bar.Type.ShouldBe(SchemaType.Object); + bar.Title.ShouldBe("bar"); + bar.Format.ShouldBeNull(); + + bar.Properties.ShouldContainKey("name"); + var barName = bar.Properties["name"]; + barName.Type.ShouldBe(SchemaType.String); + barName.Title.ShouldBe("string"); + barName.Format.ShouldBe("string"); + barName.Nullable.ShouldBeTrue(); + + bar.Properties.ShouldContainKey("cost"); + var barCost = bar.Properties["cost"]; + barCost.Type.ShouldBe(SchemaType.Number); + barCost.Title.ShouldBe("decimal"); + barCost.Format.ShouldBe("decimal"); + barCost.Nullable.ShouldBeTrue(); + + 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.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.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"); + fooType.Nullable.ShouldBeFalse(); + + fooType.Enum + .Select(s => s.GetValue()) + .SequenceEqual(Enum.GetNames()) + .ShouldBeTrue(); + } + + [Fact] + public void AsyncApiSchemaGenerator_OnLoopGenerate_NotFailed() + { + // Arrange + AsyncApiSchemaGenerator generator = new(); + var type = typeof(Loop); + + // Act + var schema = generator.Generate(type); + + // Assert + schema.ShouldNotBeNull(); + + 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); + } + } + + public class Foo + { + public Guid Id { get; set; } + 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; } + } + + public enum FooType { Foo, Bar } + + public class Bar + { + public string Name { get; set; } + public decimal? Cost { get; set; } + } + + public class Loop + { + public Loop UltraLoop { get; set; } + public Loop UltraLoop2 { get; set; } + } +} diff --git a/test/Saunter.Tests/Utils/NJsonSchemaExtensions.cs b/test/Saunter.Tests/Utils/NJsonSchemaExtensions.cs deleted file mode 100644 index 7a36ab6d..00000000 --- a/test/Saunter.Tests/Utils/NJsonSchemaExtensions.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using NJsonSchema; - -namespace Saunter.Tests.Utils -{ - public static class NJsonSchemaExtensions - { - public static IDictionary MergeAllProperties(this JsonSchema s) - { - var result = new Dictionary(); - foreach (var property in s.ActualProperties) - { - result[property.Key] = property.Value; - } - - foreach (var sub in s.AllInheritedSchemas) - { - foreach (var property in sub.Properties) - { - result[property.Key] = property.Value; - } - } - - return result; - } - } -} diff --git a/test/Saunter.Tests/Utils/ReflectionTests.cs b/test/Saunter.Tests/Utils/ReflectionTests.cs deleted file mode 100644 index 935fc0e0..00000000 --- a/test/Saunter.Tests/Utils/ReflectionTests.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.ComponentModel; -using System.ComponentModel.DataAnnotations; -using System.Reflection; -using Saunter.Utils; -using Shouldly; -using Xunit; - -namespace Saunter.Tests.Utils -{ - public class ReflectionTests - { - private class ExampleForTestingAttribute : Attribute { } - - [ExampleForTesting] - private class TypeWithAttribute { } - - [Fact] - public void HasCustomAttribute_True_WhenTypeHasCustomAttribute() - { - var type = typeof(TypeWithAttribute).GetTypeInfo(); - - type.HasCustomAttribute().ShouldBeTrue(); - } - - private class TypeWithoutAttribute { } - - [Fact] - public void HasCustomAttribute_False_WhenTypeDoesNotHaveCustomAttribute() - { - var type = typeof(TypeWithoutAttribute).GetTypeInfo(); - - type.HasCustomAttribute().ShouldBeFalse(); - } - } -} diff --git a/test/Saunter.Tests/Utils/SerializerTests.cs b/test/Saunter.Tests/Utils/SerializerTests.cs deleted file mode 100644 index fcb6d1c3..00000000 --- a/test/Saunter.Tests/Utils/SerializerTests.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Reflection; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Saunter.Generation; -using Saunter.Serialization; -using Saunter.Tests.Generation.DocumentGeneratorTests; -using Saunter.Utils; -using Shouldly; -using Xunit; - -namespace Saunter.Tests.Utils -{ - public abstract class SerializerTests - { - private readonly DocumentGenerator _documentGenerator; - private readonly AsyncApiOptions _options; - - public SerializerTests() - { - _options = new AsyncApiOptions(); - _documentGenerator = new DocumentGenerator(); - } - - protected abstract IAsyncApiDocumentSerializer CreateSerializer(); - - [Fact] - public void TestSerialize() - { - var doc = _documentGenerator.GenerateDocument(new[] { typeof(MethodAttributesTests.TenantMessagePublisher).GetTypeInfo() }, _options, _options.AsyncApi, ActivatorServiceProvider.Instance); - var serializedDoc = CreateSerializer().Serialize(doc); - - serializedDoc.ShouldNotBeNullOrWhiteSpace(); - } - } - - public class NewtonsoftSerializerTests : SerializerTests - { - protected override IAsyncApiDocumentSerializer CreateSerializer() - { - return new NewtonsoftAsyncApiDocumentSerializer(); - } - } -}