diff --git a/.asyncapi-tool b/.asyncapi-tool index 71583182..c15324ea 100644 --- a/.asyncapi-tool +++ b/.asyncapi-tool @@ -14,7 +14,7 @@ "categories": [ "converters", "code-first", - "validator", + "validator" ], "hasCommercial": false } diff --git a/.github/labeler.yml b/.github/labeler.yml index 4561e7f9..13e2f191 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -4,6 +4,8 @@ ci/cd: - .github/**/* asyncapi.readers: - src/LEGO.AsyncAPI.Readers/**/* +asyncapi.bindings: +- src/LEGO.AsyncAPI.Bindings/**/* asyncapi.models: - src/LEGO.AsyncAPI/**/* asyncapi.tests: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 447ff2b5..3d1d849d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,14 +1,16 @@ name: Build & Test on: push: - branches: [ main ] + branches: [ main, vnext ] paths: - 'src/**' + - 'test/**' - '!**/*.md' pull_request: - branches: [ main ] + branches: [ main, vnext ] paths: - 'src/**' + - 'test/**' - '!**/*.md' workflow_dispatch: jobs: @@ -23,7 +25,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' include-prerelease: true - name: Restore dependencies run: dotnet restore diff --git a/.github/workflows/release-beta.yml b/.github/workflows/release-beta.yml new file mode 100644 index 00000000..9cbebb05 --- /dev/null +++ b/.github/workflows/release-beta.yml @@ -0,0 +1,46 @@ +name: Publish beta NuGet package +on: + workflow_dispatch: + push: + branches: + - vnext +jobs: + check: + runs-on: ubuntu-latest + name: Check release + environment: AsyncAPI + steps: + - name: Checkout repository + uses: actions/checkout@v1 + + - name: Semantic Release + id: semantic + uses: cycjimmy/semantic-release-action@v3 + with: + dry_run: true + ci: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + outputs: + version: ${{ steps.semantic.outputs.new_release_version }} + + pre-release: + runs-on: ubuntu-latest + name: Publish NuGet packages + needs: check + environment: AsyncAPI + strategy: + matrix: + package-name: [ "LEGO.AsyncAPI", "LEGO.AsyncAPI.Readers", "LEGO.AsyncAPI.Bindings" ] + steps: + - name: Checkout repository + uses: actions/checkout@v1 + + - name: Setup .NET Core @ Latest + uses: actions/setup-dotnet@v1 + + - name: Build ${{ matrix.package-name }} project and pack NuGet package + run: dotnet pack src/${{ matrix.package-name }}/${{ matrix.package-name }}.csproj -c Release -o out-${{ matrix.package-name }} -p:PackageVersion=${{ needs.check.outputs.version }}-beta.${{github.run_number}} + + - name: Push generated package to GitHub Packages registry + run: dotnet nuget push out-${{ matrix.package-name }}/*.nupkg -s https://api.nuget.org/v3/index.json --skip-duplicate -n --api-key ${{secrets.NUGET}} diff --git a/.github/workflows/release-internal.yml b/.github/workflows/release-internal.yml deleted file mode 100644 index 8b54315b..00000000 --- a/.github/workflows/release-internal.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Publish internal NuGet package -on: - push: - branches: [ main ] - paths: - - 'src/LEGO.AsyncAPI/**' - - 'src/LEGO.AsyncAPI.Readers/**' - - 'src/LEGO.AsyncAPI.Writers/**' - - ".github/workflows/release-package.yml" - - '!**/*.md' - workflow_dispatch: - -jobs: - release: - runs-on: ubuntu-latest - name: Publish NuGet packages - strategy: - matrix: - package-name: [ "LEGO.AsyncAPI", "LEGO.AsyncAPI.Readers"] - steps: - - name: Checkout repository - uses: actions/checkout@v1 - - - name: Setup .NET Core @ Latest - uses: actions/setup-dotnet@v1 - with: - source-url: https://nuget.pkg.github.com/LEGO/index.json - env: - NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} - - - name: Build ${{ matrix.package-name }} project and pack NuGet package - run: dotnet pack src/${{ matrix.package-name }}/${{ matrix.package-name }}.csproj -c Release -o out-${{ matrix.package-name }} -p:PackageVersion=0.2.$GITHUB_RUN_NUMBER.0-prerelease - - - name: Push generated package to GitHub Packages registry - run: dotnet nuget push out-${{ matrix.package-name }}/*.nupkg --skip-duplicate -n --api-key ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/release-manual.yml b/.github/workflows/release-manual.yml new file mode 100644 index 00000000..0eeca64b --- /dev/null +++ b/.github/workflows/release-manual.yml @@ -0,0 +1,28 @@ +name: Publish custom NuGet package version +on: + workflow_dispatch: + inputs: + package_version: + description: 'NuGet package version (e.g. 6.0.0-beta.1041)' + required: true + +jobs: + pre-release: + runs-on: ubuntu-latest + name: Publish NuGet packages + environment: AsyncAPI + strategy: + matrix: + package-name: [ "LEGO.AsyncAPI", "LEGO.AsyncAPI.Readers", "LEGO.AsyncAPI.Bindings" ] + steps: + - name: Checkout repository + uses: actions/checkout@v1 + + - name: Setup .NET Core @ Latest + uses: actions/setup-dotnet@v1 + + - name: Build ${{ matrix.package-name }} project and pack NuGet package + run: dotnet pack src/${{ matrix.package-name }}/${{ matrix.package-name }}.csproj -c Release -o out-${{ matrix.package-name }} -p:PackageVersion=${{ github.event.inputs.package_version }} + + - name: Push generated package to NuGet + run: dotnet nuget push out-${{ matrix.package-name }}/*.nupkg -s https://api.nuget.org/v3/index.json --skip-duplicate -n --api-key ${{secrets.NUGET}} diff --git a/.github/workflows/release-package.yml b/.github/workflows/release-package.yml index 0f064176..9e52a9fe 100644 --- a/.github/workflows/release-package.yml +++ b/.github/workflows/release-package.yml @@ -56,7 +56,7 @@ jobs: environment: AsyncAPI strategy: matrix: - package-name: [ "LEGO.AsyncAPI", "LEGO.AsyncAPI.Readers" ] + package-name: [ "LEGO.AsyncAPI", "LEGO.AsyncAPI.Readers", "LEGO.AsyncAPI.Bindings" ] steps: - name: Checkout repository uses: actions/checkout@v1 diff --git a/AsyncAPI.sln b/AsyncAPI.sln index bf944501..db79f153 100644 --- a/AsyncAPI.sln +++ b/AsyncAPI.sln @@ -12,8 +12,11 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DE167614-5BCB-4046-BD4C-ABB70E9F3462}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + Common.Build.props = Common.Build.props EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LEGO.AsyncAPI.Bindings", "src\LEGO.AsyncAPI.Bindings\LEGO.AsyncAPI.Bindings.csproj", "{33CA31F4-ECFE-4227-BFE9-F49783DD29A0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,6 +35,10 @@ Global {7D9C6FBA-4B6F-48A0-B3F5-E7357021F8F9}.Debug|Any CPU.Build.0 = Debug|Any CPU {7D9C6FBA-4B6F-48A0-B3F5-E7357021F8F9}.Release|Any CPU.ActiveCfg = Release|Any CPU {7D9C6FBA-4B6F-48A0-B3F5-E7357021F8F9}.Release|Any CPU.Build.0 = Release|Any CPU + {33CA31F4-ECFE-4227-BFE9-F49783DD29A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {33CA31F4-ECFE-4227-BFE9-F49783DD29A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {33CA31F4-ECFE-4227-BFE9-F49783DD29A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {33CA31F4-ECFE-4227-BFE9-F49783DD29A0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/CHANGELOG.md b/CHANGELOG.md index f493bf3f..7dea98ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,132 @@ +## [5.2.4](https://github.com/LEGO/AsyncAPI.NET/compare/v5.2.3...v5.2.4) (2024-07-29) + + +### Bug Fixes + +* remove persistence nullability ([52165f2](https://github.com/LEGO/AsyncAPI.NET/commit/52165f213502d9436e25a4e761804b5796b5de8c)) + +## [5.2.3](https://github.com/LEGO/AsyncAPI.NET/compare/v5.2.2...v5.2.3) (2024-07-29) + + +### Bug Fixes + +* add missing walk and visit methods for bindings. ([#191](https://github.com/LEGO/AsyncAPI.NET/issues/191)) ([b8307c5](https://github.com/LEGO/AsyncAPI.NET/commit/b8307c57a6f9bc7c546702c24dffdfb1833aa5d3)) + +## [5.2.2](https://github.com/LEGO/AsyncAPI.NET/compare/v5.2.1...v5.2.2) (2024-07-29) + + +### Bug Fixes + +* correct typing of exclusive maximums and minimums for draft7 jso… ([#188](https://github.com/LEGO/AsyncAPI.NET/issues/188)) ([fb50d00](https://github.com/LEGO/AsyncAPI.NET/commit/fb50d00192896f9ac26e65df1e991854b33aa17c)) +* resolving wrong reference ([#180](https://github.com/LEGO/AsyncAPI.NET/issues/180)) ([47685cd](https://github.com/LEGO/AsyncAPI.NET/commit/47685cd19c7e58391625be043b1e5d82c49eedc8)) + +## [5.2.1](https://github.com/LEGO/AsyncAPI.NET/compare/v5.2.0...v5.2.1) (2024-06-12) + + +### Bug Fixes + +* inline channel parameters should not deserialize as references ([#172](https://github.com/LEGO/AsyncAPI.NET/issues/172)) ([7fd3af0](https://github.com/LEGO/AsyncAPI.NET/commit/7fd3af0e6669d18e805e0bab9cafac819ea64c1d)) + +# [5.2.0](https://github.com/LEGO/AsyncAPI.NET/compare/v5.1.1...v5.2.0) (2024-03-30) + + +### Features + +* add cultureinfo to reader/writer settings. ([#152](https://github.com/LEGO/AsyncAPI.NET/issues/152)) ([0199420](https://github.com/LEGO/AsyncAPI.NET/commit/01994205ecde4e17317762374b03ec23aad17022)) +* **bindings:** add amqp bindings ([#153](https://github.com/LEGO/AsyncAPI.NET/issues/153)) ([8d128db](https://github.com/LEGO/AsyncAPI.NET/commit/8d128db869d8164cfaad156d4f29a7130a00827e)) +* **bindings:** add mqtt bindings ([#154](https://github.com/LEGO/AsyncAPI.NET/issues/154)) ([f5529e0](https://github.com/LEGO/AsyncAPI.NET/commit/f5529e0e96d139e0cb1958d6b0620ed826e21cb5)) +* improve AsyncApiAny api surface. ([9063f4e](https://github.com/LEGO/AsyncAPI.NET/commit/9063f4e4f19929f8ccbdee5bd46dd9e27a3e0c08)) +* targetframework to netstandard2.0 ([#150](https://github.com/LEGO/AsyncAPI.NET/issues/150)) ([9291da6](https://github.com/LEGO/AsyncAPI.NET/commit/9291da603335fd202b59f421945629952f136296)) + +## [5.1.1](https://github.com/LEGO/AsyncAPI.NET/compare/v5.1.0...v5.1.1) (2024-02-16) + + +### Bug Fixes + +* long values for missing retention properties ([93ba475](https://github.com/LEGO/AsyncAPI.NET/commit/93ba4755babd05a0d21f3530aab417eeed3b7073)) + +# [5.1.0](https://github.com/LEGO/AsyncAPI.NET/compare/v5.0.0...v5.1.0) (2024-02-15) + + +### Bug Fixes + +* updated topic configuration data types ([eace86d](https://github.com/LEGO/AsyncAPI.NET/commit/eace86dde4fc704d4652d19e7073be3b37ade6c7)) + + +### Features + +* added new topic configuration properties ([4a6c6a8](https://github.com/LEGO/AsyncAPI.NET/commit/4a6c6a8fed3a970153bd511daad5e614dbcdf2df)) + +# [5.0.0](https://github.com/LEGO/AsyncAPI.NET/compare/v4.1.0...v5.0.0) (2023-12-14) + + +### Bug Fixes + +* add type to references, always. ([#139](https://github.com/LEGO/AsyncAPI.NET/issues/139)) ([3031023](https://github.com/LEGO/AsyncAPI.NET/commit/30310232bb3869258486d9f7f85721d4e3fb46eb)) +* added missing mapping for ordering ([#138](https://github.com/LEGO/AsyncAPI.NET/issues/138)) ([510426e](https://github.com/LEGO/AsyncAPI.NET/commit/510426e200b4fe97ad1b8e9a6e94a615593c2a3c)) +* patternProperties should also be walked as a reference ([#133](https://github.com/LEGO/AsyncAPI.NET/issues/133)) ([dc544f1](https://github.com/LEGO/AsyncAPI.NET/commit/dc544f1c01be3b95ded08ee894453ce8529eafb3)) + + +* chore(settings)!: make reader bindings IEnumerable to allow for simpler usage ([e1f8c87](https://github.com/LEGO/AsyncAPI.NET/commit/e1f8c8766767ce642546a911810064a5234f04c3)) + + +### Features + +* allow non-component references ([#132](https://github.com/LEGO/AsyncAPI.NET/issues/132)) ([71fe571](https://github.com/LEGO/AsyncAPI.NET/commit/71fe571a3db0b4fbc13f4573b4d4b53f4f6b0911)) +* **bindings:** add high throughput fifo properties ([#135](https://github.com/LEGO/AsyncAPI.NET/issues/135)) ([44ffcf4](https://github.com/LEGO/AsyncAPI.NET/commit/44ffcf4ceaf06a5168597e1eeb9407f09d47ab23)) + + +### BREAKING CHANGES + +* changes how bindings are applied. + +# [4.1.0](https://github.com/LEGO/AsyncAPI.NET/compare/v4.0.2...v4.1.0) (2023-09-27) + + +### Features + +* **bindings:** update FilterPolicy to match AWS API ([#128](https://github.com/LEGO/AsyncAPI.NET/issues/128)) ([5b64654](https://github.com/LEGO/AsyncAPI.NET/commit/5b6465474ae09d42a27377bf04d58fdbd1dd8a59)) + +## [4.0.2](https://github.com/LEGO/AsyncAPI.NET/compare/v4.0.1...v4.0.2) (2023-08-01) + + +### Bug Fixes + +* add missing properties to json schema ([#124](https://github.com/LEGO/AsyncAPI.NET/issues/124)) ([adcd017](https://github.com/LEGO/AsyncAPI.NET/commit/adcd017b3ff6875eddac9649c2c95c398e49dec0)) +* nullref if type is not set on jsonschema when using enum. ([#123](https://github.com/LEGO/AsyncAPI.NET/issues/123)) ([e53db72](https://github.com/LEGO/AsyncAPI.NET/commit/e53db729813bd76c17a335baf9bf0d0efc34e0bc)) +* parse const keyword in a schema object ([#121](https://github.com/LEGO/AsyncAPI.NET/issues/121)) ([22b329c](https://github.com/LEGO/AsyncAPI.NET/commit/22b329c6c8068e4ff2090cb6dd11bab2d5a254a5)) + +## [4.0.1](https://github.com/LEGO/AsyncAPI.NET/compare/v4.0.0...v4.0.1) (2023-07-11) + + +### Bug Fixes + +* add ability to have 'false' as the value for 'additionalproperties' ([#118](https://github.com/LEGO/AsyncAPI.NET/issues/118)) ([9e4867f](https://github.com/LEGO/AsyncAPI.NET/commit/9e4867fbec9377964489e53c71f38a239e359cdf)) +* async schema deserializer "additionalProperties" not deserializing JsonSchema correctly ([#120](https://github.com/LEGO/AsyncAPI.NET/issues/120)) ([3761f52](https://github.com/LEGO/AsyncAPI.NET/commit/3761f521570268febb8b00fde9896379acb7047b)) + +# [4.0.0](https://github.com/LEGO/AsyncAPI.NET/compare/v3.0.2...v4.0.0) (2023-06-12) + + +### Bug Fixes + +* add setter to BindingParsers collection ([211646e](https://github.com/LEGO/AsyncAPI.NET/commit/211646e95b82b3e32563fe75c57656cd6882267b)) + + +### Features + +* **jsonschema**!: type as flag rather than list (#115) ([d44efb0](https://github.com/LEGO/AsyncAPI.NET/commit/d44efb048402c70377064b87bd962b0e455e08b3)), closes [#115](https://github.com/LEGO/AsyncAPI.NET/issues/115) +* **jsonschema**!: changed out decimal for double to allow for bigger numbers ([ab00976](https://github.com/LEGO/AsyncAPI.NET/commit/ab009764a916171c8926c129384ce18b3162e71e)) +* **bindings**!: separate bindings and allow for custom bindings. (#107) ([d38c33f](https://github.com/LEGO/AsyncAPI.NET/commit/d38c33f14d6de73e2563e29534965b06d423edac)), closes [#107](https://github.com/LEGO/AsyncAPI.NET/issues/107) +* **bindings:** add SNS AWS bindings ([#108](https://github.com/LEGO/AsyncAPI.NET/issues/108)) ([d48f166](https://github.com/LEGO/AsyncAPI.NET/commit/d48f1669ebfd9ad3f661b2b5928df1d622a4e7ba)) +* **bindings:** add SQS AWS Bindings ([#113](https://github.com/LEGO/AsyncAPI.NET/issues/113)) ([4a93c7a](https://github.com/LEGO/AsyncAPI.NET/commit/4a93c7a26dbc0dd28914ac96575070deb0a6d2c1)) + + +### BREAKING CHANGES + +* The type of `Type` in JsonSchema is now a Flags enum, rather than a List of enum. This provides an easier to use interface, for adding and checking types. +* 3 properties, previously of type `decimal` in the JsonSchema type have been changed to `double`. +* Bindings have been moved to a separate project + ## [3.0.2](https://github.com/LEGO/AsyncAPI.NET/compare/v3.0.1...v3.0.2) (2023-03-30) diff --git a/CODEOWNERS b/CODEOWNERS index f1759cb1..2e87be63 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1 @@ -* @VisualBean +* @VisualBean @UlrikSandberg diff --git a/Common.Build.props b/Common.Build.props new file mode 100644 index 00000000..ba84a0f8 --- /dev/null +++ b/Common.Build.props @@ -0,0 +1,20 @@ + + + + 10 + netstandard2.0;net6 + disable + The LEGO Group + https://github.com/LEGO/AsyncAPI.NET + README.md + https://github.com/LEGO/AsyncAPI.NET + asyncapi .net openapi documentation + logo.png + LICENSE.txt + + + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE.txt similarity index 100% rename from LICENSE rename to LICENSE.txt diff --git a/README.md b/README.md index cacd288a..ce65fd33 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,17 @@ The AsyncAPI.NET SDK contains a useful object model for the AsyncAPI specificati ## Installation Install the NuGet packages: +### AsyncAPI.NET +[![Nuget](https://img.shields.io/nuget/v/AsyncAPI.NET?label=AsyncAPI.NET&style=for-the-badge)](https://www.nuget.org/packages/AsyncAPI.NET/) +[![Nuget](https://img.shields.io/nuget/vpre/AsyncAPI.NET?label=AsyncAPI.NET-Preview&style=for-the-badge)](https://www.nuget.org/packages/AsyncAPI.NET/) -[![Nuget](https://img.shields.io/nuget/v/AsyncAPI.NET.Readers?label=AsyncAPI.NET.Readers&style=for-the-badge)](https://www.nuget.org/packages/AsyncAPI.NET.Readers/) -[![Nuget](https://img.shields.io/nuget/v/AsyncAPI.NET?label=AsyncAPI.NET&style=for-the-badge)](https://www.nuget.org/packages/AsyncAPI.NET/) +### AsyncAPI.Readers +[![Nuget](https://img.shields.io/nuget/v/AsyncAPI.NET.Readers?label=AsyncAPI.Readers&style=for-the-badge)](https://www.nuget.org/packages/AsyncAPI.NET.Readers/) +[![Nuget](https://img.shields.io/nuget/vpre/AsyncAPI.NET.Readers?label=AsyncAPI.Readers-Preview&style=for-the-badge)](https://www.nuget.org/packages/AsyncAPI.NET.Readers/) +### AsyncAPI.Bindings +[![Nuget](https://img.shields.io/nuget/v/AsyncAPI.NET.Bindings?label=AsyncAPI.Bindings&style=for-the-badge)](https://www.nuget.org/packages/AsyncAPI.NET.Bindings/) +[![Nuget](https://img.shields.io/nuget/vpre/AsyncAPI.NET.Bindings?label=AsyncAPI.Bindings-Preview&style=for-the-badge)](https://www.nuget.org/packages/AsyncAPI.NET.Bindings/) ## Example Usage Main classes to know: @@ -71,6 +78,16 @@ var stream = await httpClient.GetStreamAsync("master/examples/streetlights-kafka var asyncApiDocument = new AsyncApiStreamReader().Read(stream, out var diagnostic); ``` +### Bindings +To add support for reading bindings, simply add the bindings you wish to support, to the `Bindings` collection of `AsyncApiReaderSettings`. +There is a nifty helper to add different types of bindings, or like in the example `All` of them. + +```csharp +var settings = new AsyncApiReaderSettings(); +settings.Bindings = BindingsCollection.All; +var asyncApiDocument = new AsyncApiStringReader(settings).Read(stream, out var diagnostic); +``` + ## Attribution * [OpenAPI.Net](https://github.com/microsoft/OpenAPI.NET) - [MIT License](https://github.com/microsoft/OpenAPI.NET/blob/vnext/LICENSE) diff --git a/media/logo.png b/media/logo.png new file mode 100644 index 00000000..1a25d72c Binary files /dev/null and b/media/logo.png differ diff --git a/release.config.js b/release.config.js index 7c6ba358..7cf37e87 100644 --- a/release.config.js +++ b/release.config.js @@ -1,5 +1,5 @@ module.exports = { - branches: "main", + branches: ["main", "vnext"], plugins: [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", diff --git a/src/LEGO.AsyncAPI.Bindings/AMQP/AMQPChannelBinding.cs b/src/LEGO.AsyncAPI.Bindings/AMQP/AMQPChannelBinding.cs new file mode 100644 index 00000000..ebfed688 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/AMQP/AMQPChannelBinding.cs @@ -0,0 +1,86 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.AMQP +{ + using System; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Readers.ParseNodes; + using LEGO.AsyncAPI.Writers; + + /// + /// Binding class for AMQP channel settings. + /// + public class AMQPChannelBinding : ChannelBinding + { + /// + /// Defines what type of channel is it. Can be either queue or routingKey. + /// + public ChannelType Is { get; set; } + + /// + /// When is=routingKey, this object defines the exchange properties. + /// + public Exchange Exchange { get; set; } + + /// + /// When is=queue, this object defines the queue properties. + /// + public Queue Queue { get; set; } + + public override string BindingKey => "amqp"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + { "is", (a, n) => { a.Is = n.GetScalarValue().GetEnumFromDisplayName(); } }, + { "exchange", (a, n) => { a.Exchange = n.ParseMap(ExchangeFixedFields); } }, + { "queue", (a, n) => { a.Queue = n.ParseMap(QueueFixedFields); } }, + }; + + private static FixedFieldMap ExchangeFixedFields = new() + { + { "name", (a, n) => { a.Name = n.GetScalarValue(); } }, + { "durable", (a, n) => { a.Durable = n.GetBooleanValue(); } }, + { "type", (a, n) => { a.Type = n.GetScalarValue().GetEnumFromDisplayName(); } }, + { "autoDelete", (a, n) => { a.AutoDelete = n.GetBooleanValue(); } }, + { "vhost", (a, n) => { a.Vhost = n.GetScalarValue(); } }, + }; + + private static FixedFieldMap QueueFixedFields = new() + { + { "name", (a, n) => { a.Name = n.GetScalarValue(); } }, + { "durable", (a, n) => { a.Durable = n.GetBooleanValue(); } }, + { "exclusive", (a, n) => { a.Exclusive = n.GetBooleanValue(); } }, + { "autoDelete", (a, n) => { a.AutoDelete = n.GetBooleanValue(); } }, + { "vhost", (a, n) => { a.Vhost = n.GetScalarValue(); } }, + }; + + /// + /// Serialize to AsyncAPI V2 document without using reference. + /// + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteRequiredProperty("is", this.Is.GetDisplayName()); + switch (this.Is) + { + case ChannelType.RoutingKey: + writer.WriteOptionalObject("exchange", this.Exchange, (w, t) => t.Serialize(w)); + break; + case ChannelType.Queue: + writer.WriteOptionalObject("queue", this.Queue, (w, t) => t.Serialize(w)); + break; + } + + writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); + writer.WriteExtensions(this.Extensions); + + writer.WriteEndObject(); + } + } +} diff --git a/src/LEGO.AsyncAPI.Bindings/AMQP/AMQPMessageBinding.cs b/src/LEGO.AsyncAPI.Bindings/AMQP/AMQPMessageBinding.cs new file mode 100644 index 00000000..73a68049 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/AMQP/AMQPMessageBinding.cs @@ -0,0 +1,51 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.AMQP +{ + using System; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Readers.ParseNodes; + using LEGO.AsyncAPI.Writers; + + /// + /// Binding class for AMQP messages. + /// + public class AMQPMessageBinding : MessageBinding + { + /// + /// A MIME encoding for the message content. + /// + public string ContentEncoding { get; set; } + + /// + /// Application-specific message type. + /// + public string MessageType { get; set; } + + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + + writer.WriteOptionalProperty("contentEncoding", this.ContentEncoding); + writer.WriteOptionalProperty("messageType", this.MessageType); + writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); + writer.WriteExtensions(this.Extensions); + + writer.WriteEndObject(); + } + + public override string BindingKey => "amqp"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + { "contentEncoding", (a, n) => { a.ContentEncoding = n.GetScalarValue(); } }, + { "messageType", (a, n) => { a.MessageType = n.GetScalarValue(); } }, + }; + } +} diff --git a/src/LEGO.AsyncAPI.Bindings/AMQP/AMQPOperationBinding.cs b/src/LEGO.AsyncAPI.Bindings/AMQP/AMQPOperationBinding.cs new file mode 100644 index 00000000..86dc74ef --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/AMQP/AMQPOperationBinding.cs @@ -0,0 +1,103 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.AMQP +{ + using System; + using System.Collections.Generic; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Readers; + using LEGO.AsyncAPI.Readers.ParseNodes; + using LEGO.AsyncAPI.Writers; + + /// + /// Binding class for AMQP operations. + /// + public class AMQPOperationBinding : OperationBinding + { + /// + /// TTL (Time-To-Live) for the message. It MUST be greater than or equal to zero. + /// + public uint? Expiration { get; set; } + + /// + /// Identifies the user who has sent the message. + /// + public string UserId { get; set; } + + /// + /// The routing keys the message should be routed to at the time of publishing. + /// + public List Cc { get; set; } = new List(); + + /// + /// A priority for the message. + /// + public int? Priority { get; set; } + + /// + /// Delivery mode of the message. Its value MUST be either 1 (transient) or 2 (persistent). + /// + public DeliveryMode? DeliveryMode { get; set; } + + /// + /// Whether the message is mandatory or not. + /// + public bool? Mandatory { get; set; } + + /// + /// Like cc but consumers will not receive this information. + /// + public List Bcc { get; set; } = new List(); + + /// + /// Whether the message should include a timestamp or not. + /// + public bool? Timestamp { get; set; } + + /// + /// Whether the consumer should ack the message or not. + /// + public bool? Ack { get; set; } + + public override string BindingKey => "amqp"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + { "expiration", (a, n) => { a.Expiration = (uint?)n.GetIntegerValueOrDefault(); } }, + { "userId", (a, n) => { a.UserId = n.GetScalarValueOrDefault(); } }, + { "cc", (a, n) => { a.Cc = n.CreateSimpleList(s => s.GetScalarValue()); } }, + { "priority", (a, n) => { a.Priority = n.GetIntegerValueOrDefault(); } }, + { "deliveryMode", (a, n) => { a.DeliveryMode = (DeliveryMode?)n.GetIntegerValueOrDefault(); } }, + { "mandatory", (a, n) => { a.Mandatory = n.GetBooleanValueOrDefault(); } }, + { "bcc", (a, n) => { a.Bcc = n.CreateSimpleList(s => s.GetScalarValue()); } }, + { "timestamp", (a, n) => { a.Timestamp = n.GetBooleanValueOrDefault(); } }, + { "ack", (a, n) => { a.Ack = n.GetBooleanValueOrDefault(); } }, + }; + + /// + /// Serialize to AsyncAPI V2 document without using reference. + /// + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteOptionalProperty("expiration", (int)this.Expiration); + writer.WriteOptionalProperty("userId", this.UserId); + writer.WriteOptionalCollection("cc", this.Cc, (w, s) => w.WriteValue(s)); + writer.WriteOptionalProperty("priority", this.Priority); + writer.WriteOptionalProperty("deliveryMode", (int?)this.DeliveryMode); + writer.WriteOptionalProperty("mandatory", this.Mandatory); + writer.WriteOptionalCollection("bcc", this.Bcc, (w, s) => w.WriteValue(s)); + writer.WriteOptionalProperty("timestamp", this.Timestamp); + writer.WriteOptionalProperty("ack", this.Ack); + writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} diff --git a/src/LEGO.AsyncAPI.Bindings/AMQP/ChannelType.cs b/src/LEGO.AsyncAPI.Bindings/AMQP/ChannelType.cs new file mode 100644 index 00000000..a817ea49 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/AMQP/ChannelType.cs @@ -0,0 +1,15 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.AMQP +{ + using LEGO.AsyncAPI.Attributes; + + public enum ChannelType + { + [Display("routingKey")] + RoutingKey = 0, + + [Display("queue")] + Queue, + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/AMQP/DeliveryMode.cs b/src/LEGO.AsyncAPI.Bindings/AMQP/DeliveryMode.cs new file mode 100644 index 00000000..77b855b5 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/AMQP/DeliveryMode.cs @@ -0,0 +1,15 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.AMQP +{ + using LEGO.AsyncAPI.Attributes; + + public enum DeliveryMode + { + [Display("transient")] + Transient = 1, + + [Display("persistent")] + Persistent = 2, + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/AMQP/Exchange.cs b/src/LEGO.AsyncAPI.Bindings/AMQP/Exchange.cs new file mode 100644 index 00000000..66815b3c --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/AMQP/Exchange.cs @@ -0,0 +1,50 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.AMQP +{ + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + + /// + /// Represents an exchange configuration. + /// + public class Exchange : IAsyncApiElement + { + /// + /// The name of the exchange. It MUST NOT exceed 255 characters long. + /// + public string Name { get; set; } + + /// + /// The type of the exchange. Can be either topic, direct, fanout, default, or headers. + /// + public ExchangeType Type { get; set; } + + /// + /// Whether the exchange should survive broker restarts or not. + /// + public bool Durable { get; set; } + + /// + /// Whether the exchange should be deleted when the last queue is unbound from it. + /// + public bool AutoDelete { get; set; } + + /// + /// The virtual host of the exchange. Defaults to /. + /// + public string Vhost { get; set; } = "/"; + + public void Serialize(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteRequiredProperty(AsyncApiConstants.Name, this.Name); + writer.WriteRequiredProperty(AsyncApiConstants.Type, this.Type.GetDisplayName()); + writer.WriteRequiredProperty("durable", this.Durable); + writer.WriteRequiredProperty("autoDelete", this.AutoDelete); + writer.WriteRequiredProperty("vhost", this.Vhost); + writer.WriteEndObject(); + } + } +} diff --git a/src/LEGO.AsyncAPI.Bindings/AMQP/ExchangeType.cs b/src/LEGO.AsyncAPI.Bindings/AMQP/ExchangeType.cs new file mode 100644 index 00000000..1cfde013 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/AMQP/ExchangeType.cs @@ -0,0 +1,24 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.AMQP +{ + using LEGO.AsyncAPI.Attributes; + + public enum ExchangeType + { + [Display("default")] + Default = 0, + + [Display("topic")] + Topic, + + [Display("direct")] + Direct, + + [Display("fanout")] + Fanout, + + [Display("headers")] + Headers, + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/AMQP/Queue.cs b/src/LEGO.AsyncAPI.Bindings/AMQP/Queue.cs new file mode 100644 index 00000000..a3cf25d3 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/AMQP/Queue.cs @@ -0,0 +1,50 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.AMQP +{ + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + + /// + /// Represents a queue configuration. + /// + public class Queue : IAsyncApiElement + { + /// + /// The name of the queue. It MUST NOT exceed 255 characters long. + /// + public string Name { get; set; } + + /// + /// Whether the queue should survive broker restarts or not. + /// + public bool Durable { get; set; } + + /// + /// Whether the queue should be used only by one connection or not. + /// + public bool Exclusive { get; set; } + + /// + /// Whether the queue should be deleted when the last consumer unsubscribes. + /// + public bool AutoDelete { get; set; } + + /// + /// The virtual host of the queue. Defaults to /. + /// + public string Vhost { get; set; } = "/"; + + public void Serialize(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteRequiredProperty(AsyncApiConstants.Name, this.Name); + writer.WriteRequiredProperty("durable", this.Durable); + writer.WriteRequiredProperty("exclusive", this.Exclusive); + writer.WriteRequiredProperty("autoDelete", this.AutoDelete); + writer.WriteRequiredProperty("vhost", this.Vhost); + writer.WriteEndObject(); + } + } +} diff --git a/src/LEGO.AsyncAPI.Bindings/Binding.cs b/src/LEGO.AsyncAPI.Bindings/Binding.cs new file mode 100644 index 00000000..e7c57400 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Binding.cs @@ -0,0 +1,14 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings +{ + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Readers.Interface; + using LEGO.AsyncAPI.Readers.ParseNodes; + + public abstract class Binding : AsyncApiBinding, IBindingParser + where T : IBinding, new() + { + public abstract T LoadBinding(PropertyNode node); + } +} diff --git a/src/LEGO.AsyncAPI.Bindings/BindingHelpers.cs b/src/LEGO.AsyncAPI.Bindings/BindingHelpers.cs new file mode 100644 index 00000000..424afc9e --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/BindingHelpers.cs @@ -0,0 +1,49 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings +{ + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Readers; + using LEGO.AsyncAPI.Readers.ParseNodes; + + public static class BindingHelpers + { + public static T ParseMap(this ParseNode node, FixedFieldMap fixedFieldMap) + where T : new() + { + var mapNode = node.CheckMapNode(node.Context.GetLocation()); + if (mapNode == null) + { + return default(T); + } + + var instance = new T(); + + foreach (var propertyNode in mapNode) + { + propertyNode.ParseField(instance, fixedFieldMap, null); + } + + return instance; + } + + public static T ParseMapWithExtensions(this ParseNode node, FixedFieldMap fixedFieldMap) + where T : IAsyncApiExtensible, new() + { + var mapNode = node.CheckMapNode(node.Context.GetLocation()); + if (mapNode == null) + { + return default(T); + } + + var instance = new T(); + + foreach (var propertyNode in mapNode) + { + propertyNode.ParseField(instance, fixedFieldMap, ExtensionHelpers.GetExtensionsFieldMap()); + } + + return instance; + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/BindingsCollection.cs b/src/LEGO.AsyncAPI.Bindings/BindingsCollection.cs new file mode 100644 index 00000000..4fd5560f --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/BindingsCollection.cs @@ -0,0 +1,111 @@ +// Copyright (c) The LEGO Group. All rights reserved. +namespace LEGO.AsyncAPI.Bindings +{ + using System; + using System.Collections.Generic; + using LEGO.AsyncAPI.Bindings.AMQP; + using LEGO.AsyncAPI.Bindings.Http; + using LEGO.AsyncAPI.Bindings.Kafka; + using LEGO.AsyncAPI.Bindings.MQTT; + using LEGO.AsyncAPI.Bindings.Pulsar; + using LEGO.AsyncAPI.Bindings.Sns; + using LEGO.AsyncAPI.Bindings.Sqs; + using LEGO.AsyncAPI.Bindings.WebSockets; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Readers.Interface; + + public static class BindingsCollection + { + public static TCollection Add( + this TCollection destination, + IEnumerable source) + where TCollection : ICollection + { + if (destination == null) + { + throw new ArgumentNullException(nameof(destination)); + } + + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (destination is List list) + { + list.AddRange(source); + return destination; + } + + foreach (var item in source) + { + destination.Add(item); + } + + return destination; + } + + public static IEnumerable> All => new List> + { + Pulsar, + Kafka, + Http, + Websockets, + Sqs, + Sns, + AMQP, + MQTT, + }; + + public static IEnumerable> Http => new List> + { + new HttpOperationBinding(), + new HttpMessageBinding(), + }; + + public static IEnumerable> Websockets => new List> + { + new WebSocketsChannelBinding(), + }; + + public static IEnumerable> Kafka => new List> + { + new KafkaServerBinding(), + new KafkaChannelBinding(), + new KafkaOperationBinding(), + new KafkaMessageBinding(), + }; + + public static IEnumerable> Pulsar => new List> + { + new PulsarServerBinding(), + new PulsarChannelBinding(), + }; + + public static IEnumerable> Sqs => new List> + { + new SqsChannelBinding(), + new SqsOperationBinding(), + }; + + public static IEnumerable> Sns => new List> + { + new SnsChannelBinding(), + new SnsOperationBinding(), + }; + + public static IEnumerable> AMQP => new List> + { + new AMQPChannelBinding(), + new AMQPOperationBinding(), + new AMQPMessageBinding(), + }; + + public static IEnumerable> MQTT => new List> + { + new MQTTServerBinding(), + new MQTTOperationBinding(), + new MQTTMessageBinding(), + }; + } +} diff --git a/src/LEGO.AsyncAPI.Bindings/ChannelBinding{T}.cs b/src/LEGO.AsyncAPI.Bindings/ChannelBinding{T}.cs new file mode 100644 index 00000000..d792809c --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/ChannelBinding{T}.cs @@ -0,0 +1,16 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings +{ + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Readers; + using LEGO.AsyncAPI.Readers.ParseNodes; + + public abstract class ChannelBinding : Binding, IChannelBinding + where T : IChannelBinding, new() + { + protected abstract FixedFieldMap FixedFieldMap { get; } + + public override T LoadBinding(PropertyNode node) => BindingDeserializer.LoadBinding("ChannelBinding", node.Value, this.FixedFieldMap); + } +} diff --git a/src/LEGO.AsyncAPI.Bindings/Http/HttpMessageBinding.cs b/src/LEGO.AsyncAPI.Bindings/Http/HttpMessageBinding.cs new file mode 100644 index 00000000..7a5731ef --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Http/HttpMessageBinding.cs @@ -0,0 +1,48 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.Http +{ + using System; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Readers; + using LEGO.AsyncAPI.Readers.ParseNodes; + using LEGO.AsyncAPI.Writers; + + /// + /// Binding class for http messaging channels. + /// + public class HttpMessageBinding : MessageBinding + { + /// + /// A Schema object containing the definitions for HTTP-specific headers. This schema MUST be of type object and have a properties key. + /// + public AsyncApiSchema Headers { get; set; } + + /// + /// Serialize to AsyncAPI V2 document without using reference. + /// + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + + writer.WriteOptionalObject(AsyncApiConstants.Headers, this.Headers, (w, h) => h.SerializeV2(w)); + writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); + writer.WriteExtensions(this.Extensions); + + writer.WriteEndObject(); + } + + public override string BindingKey => "http"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + { "headers", (a, n) => { a.Headers = JsonSchemaDeserializer.LoadSchema(n); } }, + }; + } +} diff --git a/src/LEGO.AsyncAPI/Models/Bindings/Http/HttpOperationBinding.cs b/src/LEGO.AsyncAPI.Bindings/Http/HttpOperationBinding.cs similarity index 55% rename from src/LEGO.AsyncAPI/Models/Bindings/Http/HttpOperationBinding.cs rename to src/LEGO.AsyncAPI.Bindings/Http/HttpOperationBinding.cs index 27a91798..f70858c2 100644 --- a/src/LEGO.AsyncAPI/Models/Bindings/Http/HttpOperationBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/Http/HttpOperationBinding.cs @@ -1,21 +1,32 @@ // Copyright (c) The LEGO Group. All rights reserved. -namespace LEGO.AsyncAPI.Models.Bindings.Http +namespace LEGO.AsyncAPI.Bindings.Http { using System; - using System.Collections.Generic; - using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Attributes; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Readers; + using LEGO.AsyncAPI.Readers.ParseNodes; using LEGO.AsyncAPI.Writers; /// /// Binding class for http operations. /// - public class HttpOperationBinding : IOperationBinding + public class HttpOperationBinding : OperationBinding { + public enum HttpOperationType + { + [Display("request")] + Request, + + [Display("response")] + Response, + } + /// /// REQUIRED. Type of operation. Its value MUST be either request or response. /// - public string Type { get; set; } + public HttpOperationType? 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. @@ -27,15 +38,10 @@ public class HttpOperationBinding : IOperationBinding /// public AsyncApiSchema Query { get; set; } - /// - /// The version of this binding. If omitted, "latest" MUST be assumed. - /// - public string BindingVersion { get; set; } - /// /// Serialize to AsyncAPI V2 document without using reference. /// - public void SerializeV2WithoutReference(IAsyncApiWriter writer) + public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) { @@ -44,37 +50,22 @@ public void SerializeV2WithoutReference(IAsyncApiWriter writer) writer.WriteStartObject(); - writer.WriteRequiredProperty(AsyncApiConstants.Type, this.Type); + writer.WriteRequiredProperty(AsyncApiConstants.Type, this.Type.GetDisplayName()); writer.WriteOptionalProperty(AsyncApiConstants.Method, this.Method); writer.WriteOptionalObject(AsyncApiConstants.Query, this.Query, (w, h) => h.SerializeV2(w)); writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); - + writer.WriteExtensions(this.Extensions); writer.WriteEndObject(); } - public void SerializeV2(IAsyncApiWriter writer) + protected override FixedFieldMap FixedFieldMap => new() { - if (writer is null) - { - throw new ArgumentNullException(nameof(writer)); - } - - if (this.Reference != null && !writer.GetSettings().ShouldInlineReference(this.Reference)) - { - this.Reference.SerializeV2(writer); - return; - } - - this.SerializeV2WithoutReference(writer); - } - - /// - public IDictionary Extensions { get; set; } = new Dictionary(); - - public bool UnresolvedReference { get; set; } - - public AsyncApiReference Reference { get; set; } + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + { "type", (a, n) => { a.Type = n.GetScalarValue().GetEnumFromDisplayName(); } }, + { "method", (a, n) => { a.Method = n.GetScalarValue(); } }, + { "query", (a, n) => { a.Query = JsonSchemaDeserializer.LoadSchema(n); } }, + }; - BindingType IBinding.Type => BindingType.Http; + public override string BindingKey => "http"; } } diff --git a/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaChannelBinding.cs b/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaChannelBinding.cs new file mode 100644 index 00000000..90b7c6ff --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaChannelBinding.cs @@ -0,0 +1,80 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.Kafka +{ + using System; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Readers.ParseNodes; + using LEGO.AsyncAPI.Writers; + + /// + /// Binding class for Kafka channel settings. + /// + public class KafkaChannelBinding : ChannelBinding + { + /// + /// Kafka topic name if different from channel name. + /// + public string Topic { get; set; } + + /// + /// Number of partitions configured on this topic (useful to know how many parallel consumers you may run). + /// + public int? Partitions { get; set; } + + /// + /// Number of replicas configured on this topic. + /// + public int? Replicas { get; set; } + + /// + /// Topic configuration properties that are relevant for the API. + /// + public TopicConfigurationObject TopicConfiguration { get; set; } + + public override string BindingKey => "kafka"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + { "topic", (a, n) => { a.Topic = n.GetScalarValue(); } }, + { "partitions", (a, n) => { a.Partitions = n.GetIntegerValue(); } }, + { "topicConfiguration", (a, n) => { a.TopicConfiguration = n.ParseMap(kafkaChannelTopicConfigurationObjectFixedFields); } }, + { "replicas", (a, n) => { a.Replicas = n.GetIntegerValue(); } }, + }; + + private static FixedFieldMap kafkaChannelTopicConfigurationObjectFixedFields = new() + { + { "cleanup.policy", (a, n) => { a.CleanupPolicy = n.CreateSimpleList(s => s.GetScalarValue()); } }, + { "retention.ms", (a, n) => { a.RetentionMilliseconds = n.GetLongValue(); } }, + { "retention.bytes", (a, n) => { a.RetentionBytes = n.GetLongValue(); } }, + { "delete.retention.ms", (a, n) => { a.DeleteRetentionMilliseconds = n.GetLongValue(); } }, + { "max.message.bytes", (a, n) => { a.MaxMessageBytes = n.GetIntegerValue(); } }, + { "confluent.key.schema.validation", (a, n) => { a.ConfluentKeySchemaValidation = n.GetBooleanValue(); } }, + { "confluent.key.subject.name.strategy", (a, n) => { a.ConfluentKeySubjectName = n.GetScalarValue(); } }, + { "confluent.value.schema.validation", (a, n) => { a.ConfluentValueSchemaValidation = n.GetBooleanValue(); } }, + { "confluent.value.subject.name.strategy", (a, n) => { a.ConfluentValueSubjectName = n.GetScalarValue(); } }, + }; + + /// + /// Serialize to AsyncAPI V2 document without using reference. + /// + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteOptionalProperty(AsyncApiConstants.Topic, this.Topic); + writer.WriteOptionalProperty(AsyncApiConstants.Partitions, this.Partitions); + writer.WriteOptionalProperty(AsyncApiConstants.Replicas, this.Replicas); + writer.WriteOptionalObject(AsyncApiConstants.TopicConfiguration, this.TopicConfiguration, (w, t) => t.Serialize(w)); + writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); + writer.WriteExtensions(this.Extensions); + + writer.WriteEndObject(); + } + } +} diff --git a/src/LEGO.AsyncAPI/Models/Bindings/Kafka/KafkaMessageBinding.cs b/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaMessageBinding.cs similarity index 56% rename from src/LEGO.AsyncAPI/Models/Bindings/Kafka/KafkaMessageBinding.cs rename to src/LEGO.AsyncAPI.Bindings/Kafka/KafkaMessageBinding.cs index 1fd4bb90..2f665560 100644 --- a/src/LEGO.AsyncAPI/Models/Bindings/Kafka/KafkaMessageBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaMessageBinding.cs @@ -1,16 +1,17 @@ // Copyright (c) The LEGO Group. All rights reserved. -namespace LEGO.AsyncAPI.Models.Bindings.Kafka +namespace LEGO.AsyncAPI.Bindings.Kafka { using System; - using System.Collections.Generic; - using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Readers; + using LEGO.AsyncAPI.Readers.ParseNodes; using LEGO.AsyncAPI.Writers; /// /// Binding class for kafka messages. /// - public class KafkaMessageBinding : IMessageBinding + public class KafkaMessageBinding : MessageBinding { /// /// The message key. NOTE: You can also use the reference object way. @@ -35,22 +36,8 @@ public class KafkaMessageBinding : IMessageBinding /// /// The version of this binding. If omitted, "latest" MUST be assumed. /// - public string BindingVersion { get; set; } - /// - /// Indicates if object is populated with data or is just a reference to the data. - /// - public bool UnresolvedReference { get; set; } - - /// - /// Reference object. - /// - public AsyncApiReference Reference { get; set; } - - /// - /// Serialize to AsyncAPI V2 document without using reference. - /// - public void SerializeV2WithoutReference(IAsyncApiWriter writer) + public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) { @@ -64,6 +51,7 @@ public void SerializeV2WithoutReference(IAsyncApiWriter writer) writer.WriteOptionalProperty(AsyncApiConstants.SchemaIdPayloadEncoding, this.SchemaIdPayloadEncoding); writer.WriteOptionalProperty(AsyncApiConstants.SchemaLookupStrategy, this.SchemaLookupStrategy); writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); + writer.WriteExtensions(this.Extensions); writer.WriteEndObject(); } @@ -72,29 +60,17 @@ public void SerializeV2WithoutReference(IAsyncApiWriter writer) /// Serializes the v2. /// /// The writer. - /// writer - public void SerializeV2(IAsyncApiWriter writer) - { - if (writer is null) - { - throw new ArgumentNullException(nameof(writer)); - } - - if (this.Reference != null && !writer.GetSettings().ShouldInlineReference(this.Reference)) - { - this.Reference.SerializeV2(writer); - return; - } + /// writer. - this.SerializeV2WithoutReference(writer); - } + public override string BindingKey => "kafka"; - /// - /// Gets or sets this object MAY be extended with Specification Extensions. - /// To protect the API from leaking the underlying JSON library, the extension data extraction is handled by a customer resolver. - /// - public IDictionary Extensions { get; set; } = new Dictionary(); - - public BindingType Type => BindingType.Kafka; + protected override FixedFieldMap FixedFieldMap => new() + { + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + { "key", (a, n) => { a.Key = JsonSchemaDeserializer.LoadSchema(n); } }, + { "schemaIdLocation", (a, n) => { a.SchemaIdLocation = n.GetScalarValue(); } }, + { "schemaIdPayloadEncoding", (a, n) => { a.SchemaIdPayloadEncoding = n.GetScalarValue(); } }, + { "schemaLookupStrategy", (a, n) => { a.SchemaLookupStrategy = n.GetScalarValue(); } }, + }; } } diff --git a/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaOperationBinding.cs b/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaOperationBinding.cs new file mode 100644 index 00000000..53db7ae0 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaOperationBinding.cs @@ -0,0 +1,53 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.Kafka +{ + using System; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Readers; + using LEGO.AsyncAPI.Readers.ParseNodes; + using LEGO.AsyncAPI.Writers; + + /// + /// Binding class for Kafka operations. + /// + public class KafkaOperationBinding : OperationBinding + { + /// + /// Id of the consumer group. + /// + public AsyncApiSchema GroupId { get; set; } + + /// + /// Id of the consumer inside a consumer group. + /// + public AsyncApiSchema ClientId { get; set; } + + public override string BindingKey => "kafka"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + { "groupId", (a, n) => { a.GroupId = JsonSchemaDeserializer.LoadSchema(n); } }, + { "clientId", (a, n) => { a.ClientId = JsonSchemaDeserializer.LoadSchema(n); } }, + }; + + /// + /// Serialize to AsyncAPI V2 document without using reference. + /// + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteOptionalObject(AsyncApiConstants.GroupId, this.GroupId, (w, h) => h.SerializeV2(w)); + writer.WriteOptionalObject(AsyncApiConstants.ClientId, this.ClientId, (w, h) => h.SerializeV2(w)); + writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); + + writer.WriteEndObject(); + } + } +} diff --git a/src/LEGO.AsyncAPI/Models/Bindings/Kafka/KafkaServerBinding.cs b/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaServerBinding.cs similarity index 51% rename from src/LEGO.AsyncAPI/Models/Bindings/Kafka/KafkaServerBinding.cs rename to src/LEGO.AsyncAPI.Bindings/Kafka/KafkaServerBinding.cs index bd9d6b19..c679d46b 100644 --- a/src/LEGO.AsyncAPI/Models/Bindings/Kafka/KafkaServerBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/Kafka/KafkaServerBinding.cs @@ -1,44 +1,40 @@ // Copyright (c) The LEGO Group. All rights reserved. -namespace LEGO.AsyncAPI.Models.Bindings.Kafka +namespace LEGO.AsyncAPI.Bindings.Kafka { using System; - using System.Collections.Generic; - using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Readers.ParseNodes; using LEGO.AsyncAPI.Writers; /// /// Binding class for Kafka server settings. /// - public class KafkaServerBinding : IServerBinding + public class KafkaServerBinding : ServerBinding { /// - /// API URL for the Schema Registry used when producing Kafka messages (if a Schema Registry was used) + /// API URL for the Schema Registry used when producing Kafka messages (if a Schema Registry was used). /// public string SchemaRegistryUrl { get; set; } /// - /// The vendor of Schema Registry and Kafka serdes library that should be used (e.g. apicurio, confluent, ibm, or karapace) + /// The vendor of Schema Registry and Kafka serdes library that should be used (e.g. apicurio, confluent, ibm, or karapace). /// public string SchemaRegistryVendor { get; set; } - /// - /// The version of this binding. - /// - public string BindingVersion { get; set; } - - public BindingType Type => BindingType.Kafka; - - public bool UnresolvedReference { get; set; } - - public AsyncApiReference Reference { get; set; } + public override string BindingKey => "kafka"; - public IDictionary Extensions { get; set; } = new Dictionary(); + protected override FixedFieldMap FixedFieldMap => new() + { + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + { "schemaRegistryUrl", (a, n) => { a.SchemaRegistryUrl = n.GetScalarValue(); } }, + { "schemaRegistryVendor", (a, n) => { a.SchemaRegistryVendor = n.GetScalarValue(); } }, + }; /// /// Serialize to AsyncAPI V2 document without using reference. /// - public void SerializeV2WithoutReference(IAsyncApiWriter writer) + public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) { @@ -49,24 +45,8 @@ public void SerializeV2WithoutReference(IAsyncApiWriter writer) writer.WriteOptionalProperty(AsyncApiConstants.SchemaRegistryUrl, this.SchemaRegistryUrl); writer.WriteOptionalProperty(AsyncApiConstants.SchemaRegistryVendor, this.SchemaRegistryVendor); writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); - + writer.WriteExtensions(this.Extensions); writer.WriteEndObject(); } - - public void SerializeV2(IAsyncApiWriter writer) - { - if (writer is null) - { - throw new ArgumentNullException(nameof(writer)); - } - - if (this.Reference != null && !writer.GetSettings().ShouldInlineReference(this.Reference)) - { - this.Reference.SerializeV2(writer); - return; - } - - this.SerializeV2WithoutReference(writer); - } } } diff --git a/src/LEGO.AsyncAPI.Bindings/Kafka/TopicConfigurationObject.cs b/src/LEGO.AsyncAPI.Bindings/Kafka/TopicConfigurationObject.cs new file mode 100644 index 00000000..cbf7c955 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Kafka/TopicConfigurationObject.cs @@ -0,0 +1,78 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.Kafka +{ + using System; + using System.Collections.Generic; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + + public class TopicConfigurationObject : IAsyncApiElement + { + /// + /// The cleanup.policy configuration option. + /// + public List CleanupPolicy { get; set; } + + /// + /// The retention.ms configuration option. + /// + public long? RetentionMilliseconds { get; set; } + + /// + /// The retention.bytes configuration option. + /// + public long? RetentionBytes { get; set; } + + /// + /// The delete.retention.ms configuration option. + /// + public long? DeleteRetentionMilliseconds { get; set; } + + /// + /// The max.message.bytes configuration option. + /// + public int? MaxMessageBytes { get; set; } + + /// + /// The confluent.key.schema.validation configuration option. + /// + public bool? ConfluentKeySchemaValidation { get; set; } + + /// + /// The confluent.key.subject.name.strategy configuration option. + /// + public string ConfluentKeySubjectName { get; set; } + + /// + /// The confluent.value.schema.validation configuration option. + /// + public bool? ConfluentValueSchemaValidation { get; set; } + + /// + /// The confluent.value.subject.name.strategy configuration option. + /// + public string ConfluentValueSubjectName { get; set; } + + public void Serialize(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteOptionalCollection(AsyncApiConstants.CleanupPolicy, this.CleanupPolicy, (w, s) => w.WriteValue(s)); + writer.WriteOptionalProperty(AsyncApiConstants.RetentionMilliseconds, this.RetentionMilliseconds); + writer.WriteOptionalProperty(AsyncApiConstants.RetentionBytes, this.RetentionBytes); + writer.WriteOptionalProperty(AsyncApiConstants.DeleteRetentionMilliseconds, this.DeleteRetentionMilliseconds); + writer.WriteOptionalProperty(AsyncApiConstants.MaxMessageBytes, this.MaxMessageBytes); + writer.WriteOptionalProperty(AsyncApiConstants.ConfluentKeySchemaValidation, this.ConfluentKeySchemaValidation); + writer.WriteOptionalProperty(AsyncApiConstants.ConfluentKeySubjectName, this.ConfluentKeySubjectName); + writer.WriteOptionalProperty(AsyncApiConstants.ConfluentValueSchemaValidation, this.ConfluentValueSchemaValidation); + writer.WriteOptionalProperty(AsyncApiConstants.ConfluentValueSubjectName, this.ConfluentValueSubjectName); + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/LEGO.AsyncAPI.Bindings.csproj b/src/LEGO.AsyncAPI.Bindings/LEGO.AsyncAPI.Bindings.csproj new file mode 100644 index 00000000..9c0b025e --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/LEGO.AsyncAPI.Bindings.csproj @@ -0,0 +1,29 @@ + + + + AsyncAPI.NET Bindings + AsyncAPI.NET.Bindings + LEGO.AsyncAPI.Bindings + LEGO.AsyncAPI.Bindings + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/src/LEGO.AsyncAPI.Bindings/MQTT/LastWill.cs b/src/LEGO.AsyncAPI.Bindings/MQTT/LastWill.cs new file mode 100644 index 00000000..23118bfa --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/MQTT/LastWill.cs @@ -0,0 +1,48 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.MQTT +{ + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + using System; + + public class LastWill : IAsyncApiElement + { + /// + /// The topic where the Last Will and Testament message will be sent. + /// + 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. + /// + public uint? QoS { get; set; } + + /// + /// Last Will message. + /// + public string Message { get; set; } + + /// + /// Whether the broker should retain the Last Will and Testament message or not. + /// + public bool Retain { get; set; } + + public void Serialize(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteRequiredProperty("topic", this.Topic); + writer.WriteOptionalProperty("qos", (int?)this.QoS); + writer.WriteOptionalProperty("message", this.Message); + writer.WriteRequiredProperty("retain", this.Retain); + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs b/src/LEGO.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs new file mode 100644 index 00000000..b48e5ae9 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs @@ -0,0 +1,65 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.MQTT +{ + using System; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Readers; + using LEGO.AsyncAPI.Readers.ParseNodes; + using LEGO.AsyncAPI.Writers; + + /// + /// Binding class for MQTT messages. + /// + public class MQTTMessageBinding : MessageBinding + { + /// + /// Indicates the format of the payload. + /// Either: 0 (zero) for unspecified bytes, or 1 for UTF-8 encoded character data. + /// + public int? PayloadFormatIndicator { get; set; } + + /// + /// Correlation Data is used to identify the request the response message is for. + /// + public AsyncApiSchema CorrelationData { get; set; } + + /// + /// String describing the content type of the message payload. + /// This should not conflict with the contentType field of the associated AsyncAPI Message object. + /// + public string ContentType { get; set; } + + /// + /// The topic (channel URI) for a response message. + /// + public string ResponseTopic { get; set; } + + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteOptionalProperty("payloadFormatIndicator", this.PayloadFormatIndicator); + writer.WriteOptionalObject("correlationData", this.CorrelationData, (w, h) => h.SerializeV2(w)); + writer.WriteOptionalProperty("contentType", this.ContentType); + writer.WriteOptionalProperty("responseTopic", this.ResponseTopic); + writer.WriteOptionalProperty("bindingVersion", this.BindingVersion); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + + public override string BindingKey => "mqtt"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "payloadFormatIndicator", (a, n) => { a.PayloadFormatIndicator = n.GetIntegerValueOrDefault(); } }, + { "correlationData", (a, n) => { a.CorrelationData = JsonSchemaDeserializer.LoadSchema(n); } }, + { "contentType", (a, n) => { a.ContentType = n.GetScalarValue(); } }, + { "responseTopic", (a, n) => { a.ResponseTopic = n.GetScalarValue(); } }, + }; + } +} diff --git a/src/LEGO.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs b/src/LEGO.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs new file mode 100644 index 00000000..d3155ecd --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs @@ -0,0 +1,61 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.MQTT +{ + using System; + using System.Collections.Generic; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Readers; + using LEGO.AsyncAPI.Readers.ParseNodes; + using LEGO.AsyncAPI.Writers; + + /// + /// Binding class for MQTT operations. + /// + public class MQTTOperationBinding : OperationBinding + { + /// + /// 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). + /// + public int QoS { get; set; } + + /// + /// Whether the broker should retain the message or not. + /// + public bool Retain { get; set; } + + /// + /// Interval in seconds or a Schema Object containing the definition of the lifetime of the message. + /// + public int? MessageExpiryInterval { get; set; } + + public override string BindingKey => "mqtt"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "qos", (a, n) => { a.QoS = n.GetIntegerValue(); } }, + { "retain", (a, n) => { a.Retain = n.GetBooleanValue(); } }, + { "messageExpiryInterval", (a, n) => { a.MessageExpiryInterval = n.GetIntegerValueOrDefault(); } }, + }; + + /// + /// Serialize to AsyncAPI V2 document without using reference. + /// + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteRequiredProperty("qos", this.QoS); + writer.WriteRequiredProperty("retain", this.Retain); + writer.WriteOptionalProperty("messageExpiryInterval", this.MessageExpiryInterval); + writer.WriteOptionalProperty("bindingVersion", this.BindingVersion); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} diff --git a/src/LEGO.AsyncAPI.Bindings/MQTT/MQTTServerBinding.cs b/src/LEGO.AsyncAPI.Bindings/MQTT/MQTTServerBinding.cs new file mode 100644 index 00000000..03b88b23 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/MQTT/MQTTServerBinding.cs @@ -0,0 +1,93 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.MQTT +{ + using System; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Readers.ParseNodes; + using LEGO.AsyncAPI.Writers; + + /// + /// Binding class for MQTT channel settings. + /// + public class MQTTServerBinding : ServerBinding + { + /// + /// The client identifier. + /// + public string ClientId { get; set; } + + /// + /// Whether to create a persistent connection or not. + /// When false, the connection will be persistent. + /// This is called clean start in MQTTv5. + /// + public bool? CleanSession { get; set; } + + /// + /// Last Will and Testament configuration. + /// + public LastWill LastWill { get; set; } + + /// + /// Interval in seconds of the longest period of time + /// the broker and the client can endure without sending a message. + /// + public int? KeepAlive { get; set; } + + /// + /// Interval in seconds the broker maintains a session + /// for a disconnected client until this interval expires. + /// + public int? SessionExpiryInterval { get; set; } + + /// + /// Number of bytes representing the maximum packet size + /// the client is willing to accept. + /// + public int? MaximumPacketSize { get; set; } + + public override string BindingKey => "mqtt"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + { "clientId", (a, n) => { a.ClientId = n.GetScalarValue(); } }, + { "cleanSession", (a, n) => { a.CleanSession = n.GetBooleanValueOrDefault(); } }, + { "lastWill", (a, n) => { a.LastWill = n.ParseMap(LastWillFixedFields); } }, + { "keepAlive", (a, n) => { a.KeepAlive = n.GetIntegerValueOrDefault(); } }, + { "sessionExpiryInterval", (a, n) => { a.SessionExpiryInterval = n.GetIntegerValueOrDefault(); } }, + { "maximumPacketSize", (a, n) => { a.MaximumPacketSize = n.GetIntegerValueOrDefault(); } }, + }; + + private static FixedFieldMap LastWillFixedFields = new() + { + { "topic", (a, n) => { a.Topic = n.GetScalarValue(); } }, + { "qos", (a, n) => { a.QoS = (uint?)n.GetIntegerValueOrDefault(); } }, + { "message", (a, n) => { a.Message = n.GetScalarValue(); } }, + { "retain", (a, n) => { a.Retain = n.GetBooleanValue(); } }, + }; + + /// + /// Serialize to AsyncAPI V2 document without using reference. + /// + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteRequiredProperty("clientId", this.ClientId); + writer.WriteOptionalProperty("cleanSession", this.CleanSession); + writer.WriteOptionalObject("lastWill", this.LastWill, (w, l) => l.Serialize(w)); + writer.WriteOptionalProperty("keepAlive", this.KeepAlive); + writer.WriteOptionalProperty("sessionExpiryInterval", this.SessionExpiryInterval); + writer.WriteOptionalProperty("maximumPacketSize", this.MaximumPacketSize); + writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} diff --git a/src/LEGO.AsyncAPI.Bindings/MessageBinding{T}.cs b/src/LEGO.AsyncAPI.Bindings/MessageBinding{T}.cs new file mode 100644 index 00000000..3e3956f9 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/MessageBinding{T}.cs @@ -0,0 +1,16 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings +{ + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Readers; + using LEGO.AsyncAPI.Readers.ParseNodes; + + public abstract class MessageBinding : Binding, IMessageBinding + where T : IMessageBinding, new() + { + protected abstract FixedFieldMap FixedFieldMap { get; } + + public override T LoadBinding(PropertyNode node) => BindingDeserializer.LoadBinding("MessageBinding", node.Value, this.FixedFieldMap); + } +} diff --git a/src/LEGO.AsyncAPI.Bindings/OperationBinding{T}.cs b/src/LEGO.AsyncAPI.Bindings/OperationBinding{T}.cs new file mode 100644 index 00000000..626250ee --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/OperationBinding{T}.cs @@ -0,0 +1,16 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings +{ + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Readers; + using LEGO.AsyncAPI.Readers.ParseNodes; + + public abstract class OperationBinding : Binding, IOperationBinding + where T : IOperationBinding, new() + { + protected abstract FixedFieldMap FixedFieldMap { get; } + + public override T LoadBinding(PropertyNode node) => BindingDeserializer.LoadBinding("OperationBinding", node.Value, this.FixedFieldMap); + } +} diff --git a/src/LEGO.AsyncAPI/Models/Bindings/Pulsar/Persistence.cs b/src/LEGO.AsyncAPI.Bindings/Pulsar/Persistence.cs similarity index 100% rename from src/LEGO.AsyncAPI/Models/Bindings/Pulsar/Persistence.cs rename to src/LEGO.AsyncAPI.Bindings/Pulsar/Persistence.cs diff --git a/src/LEGO.AsyncAPI/Models/Bindings/Pulsar/PulsarChannelBinding.cs b/src/LEGO.AsyncAPI.Bindings/Pulsar/PulsarChannelBinding.cs similarity index 57% rename from src/LEGO.AsyncAPI/Models/Bindings/Pulsar/PulsarChannelBinding.cs rename to src/LEGO.AsyncAPI.Bindings/Pulsar/PulsarChannelBinding.cs index 7a4f504f..88435b74 100644 --- a/src/LEGO.AsyncAPI/Models/Bindings/Pulsar/PulsarChannelBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/Pulsar/PulsarChannelBinding.cs @@ -1,16 +1,15 @@ // Copyright (c) The LEGO Group. All rights reserved. -namespace LEGO.AsyncAPI.Models.Bindings.Pulsar +namespace LEGO.AsyncAPI.Bindings.Pulsar { using System; using System.Collections.Generic; - using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Models.Bindings.Pulsar; + using LEGO.AsyncAPI.Readers.ParseNodes; using LEGO.AsyncAPI.Writers; - /// - /// Binding class for Pulsar server settings. - /// - public class PulsarChannelBinding : IChannelBinding + public class PulsarChannelBinding : ChannelBinding { /// /// The namespace associated with the topic. @@ -20,7 +19,7 @@ public class PulsarChannelBinding : IChannelBinding /// /// persistence of the topic in Pulsar persistent or non-persistent. /// - public Persistence? Persistence { get; set; } + public Persistence Persistence { get; set; } /// /// Topic compaction threshold given in bytes. @@ -38,7 +37,7 @@ public class PulsarChannelBinding : IChannelBinding public RetentionDefinition Retention { get; set; } /// - /// Message Time-to-live in seconds. + /// Message Time-to-live in seconds. /// public int? TTL { get; set; } @@ -47,22 +46,9 @@ public class PulsarChannelBinding : IChannelBinding /// public bool? Deduplication { get; set; } - /// - /// The version of this binding. - public string BindingVersion { get; set; } - - public BindingType Type => BindingType.Pulsar; - - public bool UnresolvedReference { get; set; } - - public AsyncApiReference Reference { get; set; } - - public IDictionary Extensions { get; set; } = new Dictionary(); + public override string BindingKey => "pulsar"; - /// - /// Serialize to AsyncAPI V2 document without using reference. - /// - public void SerializeV2WithoutReference(IAsyncApiWriter writer) + public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) { @@ -78,24 +64,26 @@ public void SerializeV2WithoutReference(IAsyncApiWriter writer) writer.WriteOptionalProperty(AsyncApiConstants.TTL, this.TTL); writer.WriteOptionalProperty(AsyncApiConstants.Deduplication, this.Deduplication); writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); - + writer.WriteExtensions(this.Extensions); writer.WriteEndObject(); } - public void SerializeV2(IAsyncApiWriter writer) + protected override FixedFieldMap FixedFieldMap => new() { - if (writer is null) - { - throw new ArgumentNullException(nameof(writer)); - } - - if (this.Reference != null && !writer.GetSettings().ShouldInlineReference(this.Reference)) - { - this.Reference.SerializeV2(writer); - return; - } - - this.SerializeV2WithoutReference(writer); - } + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + { "namespace", (a, n) => { a.Namespace = n.GetScalarValue(); } }, + { "persistence", (a, n) => { a.Persistence = n.GetScalarValue().GetEnumFromDisplayName(); } }, + { "compaction", (a, n) => { a.Compaction = n.GetIntegerValue(); } }, + { "retention", (a, n) => { a.Retention = n.ParseMap(this.pulsarServerBindingRetentionFixedFields); } }, + { "geo-replication", (a, n) => { a.GeoReplication = n.CreateSimpleList(s => s.GetScalarValue()); } }, + { "ttl", (a, n) => { a.TTL = n.GetIntegerValue(); } }, + { "deduplication", (a, n) => { a.Deduplication = n.GetBooleanValue(); } }, + }; + + private FixedFieldMap pulsarServerBindingRetentionFixedFields = new() + { + { "time", (a, n) => { a.Time = n.GetIntegerValue(); } }, + { "size", (a, n) => { a.Size = n.GetIntegerValue(); } }, + }; } } diff --git a/src/LEGO.AsyncAPI.Bindings/Pulsar/PulsarServerBinding.cs b/src/LEGO.AsyncAPI.Bindings/Pulsar/PulsarServerBinding.cs new file mode 100644 index 00000000..e767443d --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Pulsar/PulsarServerBinding.cs @@ -0,0 +1,42 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.Pulsar +{ + using System; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Readers.ParseNodes; + using LEGO.AsyncAPI.Writers; + + /// + /// Binding class for Pulsar server settings. + /// + public class PulsarServerBinding : ServerBinding + { + /// + /// The pulsar tenant. If omitted, "public" must be assumed. + /// + public string Tenant { get; set; } + + public override string BindingKey => "pulsar"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + { "tenant", (a, n) => { a.Tenant = n.GetScalarValue(); } }, + }; + + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteOptionalProperty(AsyncApiConstants.Tenant, this.Tenant); + writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} diff --git a/src/LEGO.AsyncAPI/Models/Bindings/Pulsar/RetentionDefinition.cs b/src/LEGO.AsyncAPI.Bindings/Pulsar/RetentionDefinition.cs similarity index 100% rename from src/LEGO.AsyncAPI/Models/Bindings/Pulsar/RetentionDefinition.cs rename to src/LEGO.AsyncAPI.Bindings/Pulsar/RetentionDefinition.cs diff --git a/src/LEGO.AsyncAPI.Bindings/ServerBinding{T}.cs b/src/LEGO.AsyncAPI.Bindings/ServerBinding{T}.cs new file mode 100644 index 00000000..6c1c58cf --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/ServerBinding{T}.cs @@ -0,0 +1,16 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings +{ + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Readers; + using LEGO.AsyncAPI.Readers.ParseNodes; + + public abstract class ServerBinding : Binding, IServerBinding + where T : IServerBinding, new() + { + protected abstract FixedFieldMap FixedFieldMap { get; } + + public override T LoadBinding(PropertyNode node) => BindingDeserializer.LoadBinding("ServerBinding", node.Value, this.FixedFieldMap); + } +} diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/Condition.cs b/src/LEGO.AsyncAPI.Bindings/Sns/Condition.cs new file mode 100644 index 00000000..38b21da9 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sns/Condition.cs @@ -0,0 +1,67 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.Sns; + +using System; +using System.Collections.Generic; +using System.Linq; +using LEGO.AsyncAPI.Models.Interfaces; +using LEGO.AsyncAPI.Readers.ParseNodes; +using LEGO.AsyncAPI.Writers; + +public class Condition : IAsyncApiElement +{ + public Dictionary> Value { get; private set; } + + public Condition(Dictionary> value) + { + this.Value = value; + } + + public void Serialize(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + foreach (var conditionValue in this.Value) + { + writer.WriteRequiredMap(conditionValue.Key, conditionValue.Value, (w, t) => t.Value.Write(w)); + } + + writer.WriteEndObject(); + } + + public static Condition Parse(ParseNode node) + { + switch (node) + { + case MapNode mapNode: + { + var conditionValues = new Dictionary>(); + foreach (var conditionNode in mapNode) + { + switch (conditionNode.Value) + { + case MapNode conditionValueNode: + conditionValues.Add(conditionNode.Name, new Dictionary(conditionValueNode.Select(x => + new KeyValuePair(x.Name, StringOrStringList.Parse(x.Value))) + .ToDictionary(x => x.Key, x => x.Value))); + break; + default: + throw new ArgumentException($"An error occured while parsing a {nameof(Condition)} node. " + + $"AWS condition values should be one or more key value pairs."); + } + } + + return new Condition(conditionValues); + } + + default: + throw new ArgumentException($"An error occured while parsing a {nameof(Condition)} node. " + + $"Node should contain a collection of condition types."); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/Consumer.cs b/src/LEGO.AsyncAPI.Bindings/Sns/Consumer.cs new file mode 100644 index 00000000..688c186f --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sns/Consumer.cs @@ -0,0 +1,96 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.Sns +{ + using System; + using System.Collections.Generic; + using LEGO.AsyncAPI.Attributes; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + + public class Consumer : IAsyncApiExtensible + { + /// + /// The protocol that this endpoint will receive messages by. + /// + public Protocol Protocol { get; set; } + + /// + /// The endpoint messages are delivered to. + /// + public Identifier Endpoint { get; set; } + + /// + /// Only receive a subset of messages from the channel, determined by this policy. + /// Depending on the FilterPolicyScope, a map of either a message attribute or message body to an array of possible matches. The match may be a simple string for an exact match, but it may also be an object that represents a constraint and values for that constraint. + /// + public AsyncApiAny FilterPolicy { get; set; } + + /// + /// Determines whether the FilterPolicy applies to MessageAttributes or MessageBody. + /// + public FilterPolicyScope FilterPolicyScope { get; set; } + + /// + /// If true AWS SNS attributes are removed from the body, and for SQS, SNS message attributes are copied to SQS message attributes. If false the SNS attributes are included in the body. + /// + public bool RawMessageDelivery { get; set; } + + /// + /// Prevent poison pill messages by moving un-processable messages to an SQS dead letter queue. + /// + public RedrivePolicy RedrivePolicy { get; set; } + + /// + /// Policy for retries to HTTP. The parameter is for that SNS Subscription and overrides any policy on the SNS Topic. + /// + public DeliveryPolicy DeliveryPolicy { get; set; } + + /// + /// The display name to use with an SNS subscription. + /// + public string DisplayName { get; set; } + + public IDictionary Extensions { get; set; } = new Dictionary(); + + public void Serialize(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteRequiredProperty("protocol", this.Protocol.GetDisplayName()); + writer.WriteRequiredObject("endpoint", this.Endpoint, (w, e) => e.Serialize(w)); + writer.WriteOptionalObject("filterPolicy", this.FilterPolicy, (w, f) => f.Write(w)); + writer.WriteOptionalProperty("filterPolicyScope", this.FilterPolicyScope.GetDisplayName()); + writer.WriteRequiredProperty("rawMessageDelivery", this.RawMessageDelivery); + writer.WriteOptionalObject("redrivePolicy", this.RedrivePolicy, (w, p) => p.Serialize(w)); + writer.WriteOptionalObject("deliveryPolicy", this.DeliveryPolicy, (w, p) => p.Serialize(w)); + writer.WriteOptionalProperty("displayName", this.DisplayName); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } + + public enum Protocol + { + [Display("http")] Http, + [Display("https")] Https, + [Display("email")] Email, + [Display("email-json")] EmailJson, + [Display("sms")] Sms, + [Display("sqs")] Sqs, + [Display("application")] Application, + [Display("lambda")] Lambda, + [Display("firehose")] Firehose, + } + + public enum FilterPolicyScope + { + [Display("MessageAttributes")] MessageAttributes, + [Display("MessageBody")] MessageBody, + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/DeliveryPolicy.cs b/src/LEGO.AsyncAPI.Bindings/Sns/DeliveryPolicy.cs new file mode 100644 index 00000000..9ed78b19 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sns/DeliveryPolicy.cs @@ -0,0 +1,83 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.Sns +{ + using System; + using System.Collections.Generic; + using LEGO.AsyncAPI.Attributes; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + + public class DeliveryPolicy : IAsyncApiExtensible + { + /// + /// The minimum delay for a retry in seconds. + /// + public int? MinDelayTarget { get; set; } + + /// + /// The maximum delay for a retry in seconds. + /// + public int? MaxDelayTarget { get; set; } + + /// + /// The total number of retries, including immediate, pre-backoff, backoff, and post-backoff retries. + /// + public int? NumRetries { get; set; } + + /// + /// The number of immediate retries (with no delay). + /// + public int? NumNoDelayRetries { get; set; } + + /// + /// The number of immediate retries (with delay). + /// + public int? NumMinDelayRetries { get; set; } + + /// + /// The number of post-backoff phase retries, with the maximum delay between retries. + /// + public int? NumMaxDelayRetries { get; set; } + + /// + /// The algorithm for backoff between retries. + /// + public BackoffFunction BackoffFunction { get; set; } + + /// + /// The maximum number of deliveries per second, per subscription. + /// + public int? MaxReceivesPerSecond { get; set; } + + public IDictionary Extensions { get; set; } = new Dictionary(); + + public void Serialize(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteOptionalProperty("minDelayTarget", this.MinDelayTarget); + writer.WriteOptionalProperty("maxDelayTarget", this.MaxDelayTarget); + writer.WriteOptionalProperty("numRetries", this.NumRetries); + writer.WriteOptionalProperty("numNoDelayRetries", this.NumNoDelayRetries); + writer.WriteOptionalProperty("numMinDelayRetries", this.NumMinDelayRetries); + writer.WriteOptionalProperty("numMaxDelayRetries", this.NumMaxDelayRetries); + writer.WriteOptionalProperty("backoffFunction", this.BackoffFunction.GetDisplayName()); + writer.WriteOptionalProperty("maxReceivesPerSecond", this.MaxReceivesPerSecond); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } + + public enum BackoffFunction + { + [Display("arithmetic")] Arithmetic, + [Display("exponential")] Exponential, + [Display("geometric")] Geometric, + [Display("linear")] Linear, + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/Identifier.cs b/src/LEGO.AsyncAPI.Bindings/Sns/Identifier.cs new file mode 100644 index 00000000..960c1c54 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sns/Identifier.cs @@ -0,0 +1,41 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.Sns +{ + using System; + using System.Collections.Generic; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + + public class Identifier : IAsyncApiExtensible + { + public string Url { get; set; } + + public string Email { get; set; } + + public string Phone { get; set; } + + public string Arn { get; set; } + + public string Name { get; set; } + + public IDictionary Extensions { get; set; } = new Dictionary(); + + public void Serialize(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteOptionalProperty("url", this.Url); + writer.WriteOptionalProperty("email", this.Email); + writer.WriteOptionalProperty("phone", this.Phone); + writer.WriteOptionalProperty("arn", this.Arn); + writer.WriteOptionalProperty("name", this.Name); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/Ordering.cs b/src/LEGO.AsyncAPI.Bindings/Sns/Ordering.cs new file mode 100644 index 00000000..23f69f52 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sns/Ordering.cs @@ -0,0 +1,47 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.Sns +{ + using System; + using System.Collections.Generic; + using LEGO.AsyncAPI.Attributes; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + + public class Ordering : IAsyncApiExtensible + { + /// + /// What type of SNS Topic is this? + /// + public OrderingType Type { get; set; } + + /// + /// True to turn on de-duplication of messages for a channel. + /// + public bool ContentBasedDeduplication { get; set; } + + public IDictionary Extensions { get; set; } = new Dictionary(); + + public void Serialize(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteRequiredProperty("type", this.Type.GetDisplayName()); + writer.WriteOptionalProperty("contentBasedDeduplication", this.ContentBasedDeduplication); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } + + public enum OrderingType + { + [Display("standard")] + Standard, + [Display("FIFO")] + Fifo, + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/Policy.cs b/src/LEGO.AsyncAPI.Bindings/Sns/Policy.cs new file mode 100644 index 00000000..a4232e5e --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sns/Policy.cs @@ -0,0 +1,32 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.Sns +{ + using System; + using System.Collections.Generic; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + + public class Policy : IAsyncApiExtensible + { + /// + /// An array of statement objects, each of which controls a permission for this topic. + /// + public List Statements { get; set; } + + public IDictionary Extensions { get; set; } = new Dictionary(); + + public void Serialize(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteOptionalCollection("statements", this.Statements, (w, t) => t.Serialize(w)); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/Principal.cs b/src/LEGO.AsyncAPI.Bindings/Sns/Principal.cs new file mode 100644 index 00000000..a803f6c2 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sns/Principal.cs @@ -0,0 +1,63 @@ +namespace LEGO.AsyncAPI.Bindings.Sns; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using LEGO.AsyncAPI.Models.Interfaces; +using LEGO.AsyncAPI.Readers.ParseNodes; +using LEGO.AsyncAPI.Writers; + +public abstract class Principal : IAsyncApiElement +{ + public abstract void Serialize(IAsyncApiWriter writer); + + public static Principal Parse(ParseNode node) + { + switch (node) + { + case ValueNode: + var nodeValue = node.GetScalarValue(); + if (!IsStarString(nodeValue)) + { + throw new ArgumentException($"An error occured while parsing a {nameof(Principal)} node. " + + $"Principal value without a property name can only be a string value of '*'."); + } + + return new PrincipalStar(); + + case MapNode mapNode: + { + var propertyNode = mapNode.First(); + if (!IsValidPrincipalProperty(propertyNode.Name)) + { + throw new ArgumentException($"An error occured while parsing a {nameof(Principal)} node. " + + $"Node should contain a valid AWS principal property name."); + } + + var principalValue = new KeyValuePair( + propertyNode.Name, + StringOrStringList.Parse(propertyNode.Value)); + + return new PrincipalObject(principalValue); + } + + default: + throw new ArgumentException($"An error occured while parsing a {nameof(Principal)} node. " + + $"Node should contain a string value of '*' or a valid AWS principal property."); + } + } + + private static bool IsStarString(JsonNode value) + { + var element = JsonDocument.Parse(value.ToJsonString()).RootElement; + + return element.ValueKind == JsonValueKind.String && element.ValueEquals("*"); + } + + private static bool IsValidPrincipalProperty(string property) + { + return new[] { "AWS", "Service" }.Contains(property); + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/PrincipalObject.cs b/src/LEGO.AsyncAPI.Bindings/Sns/PrincipalObject.cs new file mode 100644 index 00000000..209be8bf --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sns/PrincipalObject.cs @@ -0,0 +1,27 @@ +namespace LEGO.AsyncAPI.Bindings.Sns; + +using System; +using System.Collections.Generic; +using LEGO.AsyncAPI.Writers; + +public class PrincipalObject : Principal +{ + public KeyValuePair Value { get; private set; } + + public PrincipalObject(KeyValuePair value) + { + this.Value = value; + } + + public override void Serialize(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteRequiredObject(this.Value.Key, this.Value.Value, (w, t) => t.Value.Write(w)); + writer.WriteEndObject(); + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/PrincipalStar.cs b/src/LEGO.AsyncAPI.Bindings/Sns/PrincipalStar.cs new file mode 100644 index 00000000..c885d252 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sns/PrincipalStar.cs @@ -0,0 +1,24 @@ +namespace LEGO.AsyncAPI.Bindings.Sns; + +using System; +using LEGO.AsyncAPI.Writers; + +public class PrincipalStar : Principal +{ + public string Value { get; private set; } + + public PrincipalStar() + { + this.Value = "*"; + } + + public override void Serialize(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteValue(this.Value); + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/RedrivePolicy.cs b/src/LEGO.AsyncAPI.Bindings/Sns/RedrivePolicy.cs new file mode 100644 index 00000000..01e6d7e2 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sns/RedrivePolicy.cs @@ -0,0 +1,38 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.Sns +{ + using System; + using System.Collections.Generic; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + + public class RedrivePolicy : IAsyncApiExtensible + { + /// + /// Prevent poison pill messages by moving un-processable messages to an SQS dead letter queue. + /// + public Identifier DeadLetterQueue { get; set; } + + /// + /// The number of times a message is delivered to the source queue before being moved to the dead-letter queue. + /// + public int? MaxReceiveCount { get; set; } + + public IDictionary Extensions { get; set; } = new Dictionary(); + + public void Serialize(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteRequiredObject("deadLetterQueue", this.DeadLetterQueue, (w, q) => q.Serialize(w)); + writer.WriteOptionalProperty("maxReceiveCount", this.MaxReceiveCount); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/SnsChannelBinding.cs b/src/LEGO.AsyncAPI.Bindings/Sns/SnsChannelBinding.cs new file mode 100644 index 00000000..4394cdd1 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sns/SnsChannelBinding.cs @@ -0,0 +1,83 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.Sns +{ + using System; + using System.Collections.Generic; + using LEGO.AsyncAPI.Bindings; + using LEGO.AsyncAPI.Readers.ParseNodes; + using LEGO.AsyncAPI.Writers; + + /// + /// Binding class for SNS channel settings. + /// + public class SnsChannelBinding : ChannelBinding + { + /// + /// The name of the topic. Can be different from the channel name to allow flexibility around AWS resource naming limitations. + /// + public string Name { get; set; } + + /// + /// By default, we assume an unordered SNS topic. This field allows configuration of a FIFO SNS Topic. + /// + public Ordering Ordering { get; set; } + + /// + /// The security policy for the SNS Topic. + /// + public Policy Policy { get; set; } + + /// + /// Key-value pairs that represent AWS tags on the topic. + /// + public Dictionary Tags { get; set; } + + public override string BindingKey => "sns"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "name", (a, n) => { a.Name = n.GetScalarValue(); } }, + { "ordering", (a, n) => { a.Ordering = n.ParseMapWithExtensions(this.orderingFixedFields); } }, + { "policy", (a, n) => { a.Policy = n.ParseMapWithExtensions(this.policyFixedFields); } }, + { "tags", (a, n) => { a.Tags = n.CreateSimpleMap(s => s.GetScalarValue()); } }, + }; + + private FixedFieldMap orderingFixedFields = new() + { + { "type", (a, n) => { a.Type = n.GetScalarValue().GetEnumFromDisplayName(); } }, + { "contentBasedDeduplication", (a, n) => { a.ContentBasedDeduplication = n.GetBooleanValue(); } }, + }; + + private FixedFieldMap policyFixedFields = new() + { + { "statements", (a, n) => { a.Statements = n.CreateList(s => s.ParseMapWithExtensions(statementFixedFields)); } }, + }; + + private static FixedFieldMap statementFixedFields = new() + { + { "effect", (a, n) => { a.Effect = n.GetScalarValue().GetEnumFromDisplayName(); } }, + { "principal", (a, n) => { a.Principal = Principal.Parse(n); } }, + { "action", (a, n) => { a.Action = StringOrStringList.Parse(n); } }, + { "resource", (a, n) => { a.Resource = StringOrStringList.Parse(n); } }, + { "condition", (a, n) => { a.Condition = Condition.Parse(n); } }, + }; + + /// + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteRequiredProperty("name", this.Name); + writer.WriteOptionalObject("ordering", this.Ordering, (w, t) => t.Serialize(w)); + writer.WriteOptionalObject("policy", this.Policy, (w, t) => t.Serialize(w)); + writer.WriteOptionalMap("tags", this.Tags, (w, t) => w.WriteValue(t)); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/SnsOperationBinding.cs b/src/LEGO.AsyncAPI.Bindings/Sns/SnsOperationBinding.cs new file mode 100644 index 00000000..350b7a59 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sns/SnsOperationBinding.cs @@ -0,0 +1,93 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.Sns +{ + using System; + using System.Collections.Generic; + using LEGO.AsyncAPI.Readers.ParseNodes; + using LEGO.AsyncAPI.Writers; + + /// + /// This object contains information about the operation representation in SNS. + /// + public class SnsOperationBinding : OperationBinding + { + /// + /// Often we can assume that the SNS Topic is the channel name-we provide this field in case the you need to supply the ARN, or the Topic name is not the channel name in the AsyncAPI document. + /// + public Identifier Topic { get; set; } + + /// + /// The protocols that listen to this topic and their endpoints. + /// + public List Consumers { get; set; } + + /// + /// Policy for retries to HTTP. The field is the default for HTTP receivers of the SNS Topic which may be overridden by a specific consumer. + /// + public DeliveryPolicy DeliveryPolicy { get; set; } + + public override string BindingKey => "sns"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "topic", (a, n) => { a.Topic = n.ParseMapWithExtensions(this.identifierFixFields); } }, + { "consumers", (a, n) => { a.Consumers = n.CreateList(s => s.ParseMapWithExtensions(this.consumerFixedFields)); } }, + { "deliveryPolicy", (a, n) => { a.DeliveryPolicy = n.ParseMapWithExtensions(this.deliveryPolicyFixedFields); } }, + }; + + private FixedFieldMap identifierFixFields => new() + { + { "url", (a, n) => { a.Url = n.GetScalarValue(); } }, + { "email", (a, n) => { a.Email = n.GetScalarValue(); } }, + { "phone", (a, n) => { a.Phone = n.GetScalarValue(); } }, + { "arn", (a, n) => { a.Arn = n.GetScalarValue(); } }, + { "name", (a, n) => { a.Name = n.GetScalarValue(); } }, + }; + + private FixedFieldMap consumerFixedFields => new() + { + { "protocol", (a, n) => { a.Protocol = n.GetScalarValue().GetEnumFromDisplayName(); } }, + { "endpoint", (a, n) => { a.Endpoint = n.ParseMapWithExtensions(this.identifierFixFields); } }, + { "filterPolicy", (a, n) => { a.FilterPolicy = n.CreateAny(); } }, + { "filterPolicyScope", (a, n) => { a.FilterPolicyScope = n.GetScalarValue().GetEnumFromDisplayName(); } }, + { "rawMessageDelivery", (a, n) => { a.RawMessageDelivery = n.GetBooleanValue(); } }, + { "redrivePolicy", (a, n) => { a.RedrivePolicy = n.ParseMapWithExtensions(this.redrivePolicyFixedFields); } }, + { "deliveryPolicy", (a, n) => { a.DeliveryPolicy = n.ParseMapWithExtensions(this.deliveryPolicyFixedFields); } }, + { "displayName", (a, n) => { a.DisplayName = n.GetScalarValue(); } }, + }; + + private FixedFieldMap redrivePolicyFixedFields => new() + { + { "deadLetterQueue", (a, n) => { a.DeadLetterQueue = n.ParseMapWithExtensions(identifierFixFields); } }, + { "maxReceiveCount", (a, n) => { a.MaxReceiveCount = n.GetIntegerValue(); } }, + }; + + private FixedFieldMap deliveryPolicyFixedFields => new() + { + { "minDelayTarget", (a, n) => { a.MinDelayTarget = n.GetIntegerValue(); } }, + { "maxDelayTarget", (a, n) => { a.MaxDelayTarget = n.GetIntegerValue(); } }, + { "numRetries", (a, n) => { a.NumRetries = n.GetIntegerValue(); } }, + { "numNoDelayRetries", (a, n) => { a.NumNoDelayRetries = n.GetIntegerValue(); } }, + { "numMinDelayRetries", (a, n) => { a.NumMinDelayRetries = n.GetIntegerValue(); } }, + { "numMaxDelayRetries", (a, n) => { a.NumMaxDelayRetries = n.GetIntegerValue(); } }, + { "backoffFunction", (a, n) => { a.BackoffFunction = n.GetScalarValue().GetEnumFromDisplayName(); } }, + { "maxReceivesPerSecond", (a, n) => { a.MaxReceivesPerSecond = n.GetIntegerValue(); } }, + }; + + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteOptionalObject("topic", this.Topic, (w, t) => t.Serialize(w)); + writer.WriteOptionalCollection("consumers", this.Consumers, (w, c) => c.Serialize(w)); + writer.WriteOptionalObject("deliveryPolicy", this.DeliveryPolicy, (w, p) => p.Serialize(w)); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/Statement.cs b/src/LEGO.AsyncAPI.Bindings/Sns/Statement.cs new file mode 100644 index 00000000..da93cfbf --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sns/Statement.cs @@ -0,0 +1,65 @@ +// Copyright (c) The LEGO Group. All rights reserved. +namespace LEGO.AsyncAPI.Bindings.Sns +{ + using System; + using System.Collections.Generic; + using LEGO.AsyncAPI.Attributes; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + + public class Statement : IAsyncApiExtensible + { + /// + /// Indicates whether the policy allows or denies access. + /// + public Effect Effect { get; set; } + + /// + /// The AWS account(s) or resource ARN(s) that this statement applies to. + /// + public Principal Principal { get; set; } + + /// + /// The SNS permission being allowed or denied e.g. sns:Publish. + /// + public StringOrStringList Action { get; set; } + + /// + /// The resource(s) that this policy applies to. + /// + public StringOrStringList? Resource { get; set; } + + /// + /// Specific circumstances under which the policy grants permission. + /// + public Condition Condition { get; set; } + + public IDictionary Extensions { get; set; } = new Dictionary(); + + public void Serialize(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteRequiredProperty("effect", this.Effect.GetDisplayName()); + writer.WriteRequiredObject("principal", this.Principal, (w, t) => t.Serialize(w)); + writer.WriteRequiredObject("action", this.Action, (w, t) => t.Value.Write(w)); + writer.WriteOptionalObject("resource", this.Resource, (w, t) => t?.Value.Write(w)); + writer.WriteOptionalObject("condition", this.Condition, (w, t) => t.Serialize(w)); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } + + public enum Effect + { + [Display("Allow")] + Allow, + [Display("Deny")] + Deny, + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/Condition.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/Condition.cs new file mode 100644 index 00000000..93bdf733 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/Condition.cs @@ -0,0 +1,67 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.Sqs; + +using System; +using System.Collections.Generic; +using System.Linq; +using LEGO.AsyncAPI.Models.Interfaces; +using LEGO.AsyncAPI.Readers.ParseNodes; +using LEGO.AsyncAPI.Writers; + +public class Condition : IAsyncApiElement +{ + public Dictionary> Value { get; private set; } + + public Condition(Dictionary> value) + { + this.Value = value; + } + + public void Serialize(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + foreach (var conditionValue in this.Value) + { + writer.WriteRequiredMap(conditionValue.Key, conditionValue.Value, (w, t) => t.Value.Write(w)); + } + + writer.WriteEndObject(); + } + + public static Condition Parse(ParseNode node) + { + switch (node) + { + case MapNode mapNode: + { + var conditionValues = new Dictionary>(); + foreach (var conditionNode in mapNode) + { + switch (conditionNode.Value) + { + case MapNode conditionValueNode: + conditionValues.Add(conditionNode.Name, new Dictionary(conditionValueNode.Select(x => + new KeyValuePair(x.Name, StringOrStringList.Parse(x.Value))) + .ToDictionary(x => x.Key, x => x.Value))); + break; + default: + throw new ArgumentException($"An error occured while parsing a {nameof(Condition)} node. " + + $"AWS condition values should be one or more key value pairs."); + } + } + + return new Condition(conditionValues); + } + + default: + throw new ArgumentException($"An error occured while parsing a {nameof(Condition)} node. " + + $"Node should contain a collection of AWS condition types."); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/Identifier.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/Identifier.cs new file mode 100644 index 00000000..6e7aff23 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/Identifier.cs @@ -0,0 +1,32 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.Sqs +{ + using System; + using System.Collections.Generic; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + + public class Identifier : IAsyncApiExtensible + { + public string Arn { get; set; } + + public string Name { get; set; } + + public IDictionary Extensions { get; set; } = new Dictionary(); + + public void Serialize(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteOptionalProperty("arn", this.Arn); + writer.WriteOptionalProperty("name", this.Name); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/Policy.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/Policy.cs new file mode 100644 index 00000000..2fd9f68b --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/Policy.cs @@ -0,0 +1,32 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.Sqs +{ + using System; + using System.Collections.Generic; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + + public class Policy : IAsyncApiExtensible + { + /// + /// An array of statement objects, each of which controls a permission for this topic. + /// + public List Statements { get; set; } + + public IDictionary Extensions { get; set; } = new Dictionary(); + + public void Serialize(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteOptionalCollection("statements", this.Statements, (w, t) => t.Serialize(w)); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/Principal.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/Principal.cs new file mode 100644 index 00000000..2821f952 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/Principal.cs @@ -0,0 +1,65 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.Sqs; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using LEGO.AsyncAPI.Models.Interfaces; +using LEGO.AsyncAPI.Readers.ParseNodes; +using LEGO.AsyncAPI.Writers; + +public abstract class Principal : IAsyncApiElement +{ + public abstract void Serialize(IAsyncApiWriter writer); + + public static Principal Parse(ParseNode node) + { + switch (node) + { + case ValueNode: + var nodeValue = node.GetScalarValue(); + if (!IsStarString(nodeValue)) + { + throw new ArgumentException($"An error occured while parsing a {nameof(Principal)} node. " + + $"Principal value without a property name can only be a string value of '*'."); + } + + return new PrincipalStar(); + + case MapNode mapNode: + { + var propertyNode = mapNode.First(); + if (!IsValidPrincipalProperty(propertyNode.Name)) + { + throw new ArgumentException($"An error occured while parsing a {nameof(Principal)} node. " + + $"Node should contain a valid AWS principal property name."); + } + + var principalValue = new KeyValuePair( + propertyNode.Name, + StringOrStringList.Parse(propertyNode.Value)); + + return new PrincipalObject(principalValue); + } + + default: + throw new ArgumentException($"An error occured while parsing a {nameof(Principal)} node. " + + $"Node should contain a string value of '*' or a valid AWS principal property."); + } + } + + private static bool IsStarString(JsonNode value) + { + var element = JsonDocument.Parse(value.ToJsonString()).RootElement; + + return element.ValueKind == JsonValueKind.String && element.ValueEquals("*"); + } + + private static bool IsValidPrincipalProperty(string property) + { + return new[] { "AWS", "Service" }.Contains(property); + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/PrincipalObject.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/PrincipalObject.cs new file mode 100644 index 00000000..61e6e546 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/PrincipalObject.cs @@ -0,0 +1,27 @@ +namespace LEGO.AsyncAPI.Bindings.Sqs; + +using System; +using System.Collections.Generic; +using LEGO.AsyncAPI.Writers; + +public class PrincipalObject : Principal +{ + public KeyValuePair Value { get; private set; } + + public PrincipalObject(KeyValuePair value) + { + this.Value = value; + } + + public override void Serialize(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteRequiredObject(this.Value.Key, this.Value.Value, (w, t) => t.Value.Write(w)); + writer.WriteEndObject(); + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/PrincipalStar.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/PrincipalStar.cs new file mode 100644 index 00000000..1705b966 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/PrincipalStar.cs @@ -0,0 +1,24 @@ +namespace LEGO.AsyncAPI.Bindings.Sqs; + +using System; +using LEGO.AsyncAPI.Writers; + +public class PrincipalStar : Principal +{ + public string Value { get; private set; } + + public PrincipalStar() + { + this.Value = "*"; + } + + public override void Serialize(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteValue(this.Value); + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/Queue.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/Queue.cs new file mode 100644 index 00000000..33166af5 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/Queue.cs @@ -0,0 +1,105 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.Sqs +{ + using System; + using System.Collections.Generic; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + using LEGO.AsyncAPI.Attributes; + + public class Queue : IAsyncApiExtensible + { + /// + /// The name of the queue. When an SNS Operation Binding Object references an SQS queue by name, the identifier should be the one in this field. + /// + public string Name { get; set; } + + /// + /// Is this a FIFO queue? + /// + public bool FifoQueue { get; set; } + + /// + /// Specifies whether message deduplication occurs at the message group or queue level. Valid values are messageGroup and queue (default). + /// + public DeduplicationScope? DeduplicationScope { get; set; } + + /// + /// Specifies whether the FIFO queue throughput quota applies to the entire queue or per message group. Valid values are perQueue (default) and perMessageGroupId. + /// + public FifoThroughputLimit? FifoThroughputLimit { get; set; } + + /// + /// The number of seconds to delay before a message sent to the queue can be received. used to create a delay queue. + /// + public int? DeliveryDelay { get; set; } + + /// + /// The length of time, in seconds, that a consumer locks a message - hiding it from reads - before it is unlocked and can be read again. + /// + public int? VisibilityTimeout { get; set; } + + /// + /// Determines if the queue uses short polling or long polling. Set to zero the queue reads available messages and returns immediately. Set to a non-zero integer, long polling waits the specified number of seconds for messages to arrive before returning. + /// + public int? ReceiveMessageWaitTime { get; set; } + + /// + /// How long to retain a message on the queue in seconds, unless deleted. + /// + public int? MessageRetentionPeriod { get; set; } + + /// + /// Prevent poison pill messages by moving un-processable messages to an SQS dead letter queue. + /// + public RedrivePolicy RedrivePolicy { get; set; } + + /// + /// The security policy for the SQS Queue + /// + public Policy Policy { get; set; } + + /// + /// Key-value pairs that represent AWS tags on the topic. + /// + public Dictionary Tags { get; set; } + + public IDictionary Extensions { get; set; } = new Dictionary(); + + public void Serialize(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteRequiredProperty("name", this.Name); + writer.WriteOptionalProperty("fifoQueue", this.FifoQueue); + writer.WriteOptionalProperty("deduplicationScope", this.DeduplicationScope?.GetDisplayName()); + writer.WriteOptionalProperty("fifoThroughputLimit", this.FifoThroughputLimit?.GetDisplayName()); + writer.WriteOptionalProperty("deliveryDelay", this.DeliveryDelay); + writer.WriteOptionalProperty("visibilityTimeout", this.VisibilityTimeout); + writer.WriteOptionalProperty("receiveMessageWaitTime", this.ReceiveMessageWaitTime); + writer.WriteOptionalProperty("messageRetentionPeriod", this.MessageRetentionPeriod); + writer.WriteOptionalObject("redrivePolicy", this.RedrivePolicy, (w, p) => p.Serialize(w)); + writer.WriteOptionalObject("policy", this.Policy, (w, p) => p.Serialize(w)); + writer.WriteOptionalMap("tags", this.Tags, (w, t) => w.WriteValue(t)); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } + + public enum DeduplicationScope + { + [Display("queue")] Queue, + [Display("messageGroup")] MessageGroup, + } + + public enum FifoThroughputLimit + { + [Display("perQueue")] PerQueue, + [Display("perMessageGroupId")] PerMessageGroupId, + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/RedrivePolicy.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/RedrivePolicy.cs new file mode 100644 index 00000000..923d668c --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/RedrivePolicy.cs @@ -0,0 +1,38 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.Sqs +{ + using System; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + using System.Collections.Generic; + + public class RedrivePolicy : IAsyncApiExtensible + { + /// + /// Prevent poison pill messages by moving un-processable messages to an SQS dead letter queue. + /// + public Identifier DeadLetterQueue { get; set; } + + /// + /// The number of times a message is delivered to the source queue before being moved to the dead-letter queue. + /// + public int? MaxReceiveCount { get; set; } + + public IDictionary Extensions { get; set; } = new Dictionary(); + + public void Serialize(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteRequiredObject("deadLetterQueue", this.DeadLetterQueue, (w, q) => q.Serialize(w)); + writer.WriteOptionalProperty("maxReceiveCount", this.MaxReceiveCount); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/SqsChannelBinding.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/SqsChannelBinding.cs new file mode 100644 index 00000000..bd806071 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/SqsChannelBinding.cs @@ -0,0 +1,87 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.Sqs +{ + using System; + using LEGO.AsyncAPI.Readers.ParseNodes; + using LEGO.AsyncAPI.Writers; + + /// + /// This object contains information about the channel representation in SQS. + /// + public class SqsChannelBinding : ChannelBinding + { + /// + /// A definition of the queue that will be used as the channel. + /// + public Queue Queue { get; set; } + + /// + /// A definition of the queue that will be used for un-processable messages. + /// + public Queue DeadLetterQueue { get; set; } + + public override string BindingKey => "sqs"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "queue", (a, n) => { a.Queue = n.ParseMapWithExtensions(this.queueFixedFields); } }, + { "deadLetterQueue", (a, n) => { a.DeadLetterQueue = n.ParseMapWithExtensions(this.queueFixedFields); } }, + }; + + private FixedFieldMap queueFixedFields => new() + { + { "name", (a, n) => { a.Name = n.GetScalarValue(); } }, + { "fifoQueue", (a, n) => { a.FifoQueue = n.GetBooleanValue(); } }, + { "deduplicationScope", (a, n) => { a.DeduplicationScope = n.GetScalarValue().GetEnumFromDisplayName(); } }, + { "fifoThroughputLimit", (a, n) => { a.FifoThroughputLimit = n.GetScalarValue().GetEnumFromDisplayName(); } }, + { "deliveryDelay", (a, n) => { a.DeliveryDelay = n.GetIntegerValue(); } }, + { "visibilityTimeout", (a, n) => { a.VisibilityTimeout = n.GetIntegerValue(); } }, + { "receiveMessageWaitTime", (a, n) => { a.ReceiveMessageWaitTime = n.GetIntegerValue(); } }, + { "messageRetentionPeriod", (a, n) => { a.MessageRetentionPeriod = n.GetIntegerValue(); } }, + { "redrivePolicy", (a, n) => { a.RedrivePolicy = n.ParseMapWithExtensions(this.redrivePolicyFixedFields); } }, + { "policy", (a, n) => { a.Policy = n.ParseMapWithExtensions(this.policyFixedFields); } }, + { "tags", (a, n) => { a.Tags = n.CreateSimpleMap(s => s.GetScalarValue()); } }, + }; + + private FixedFieldMap redrivePolicyFixedFields => new() + { + { "deadLetterQueue", (a, n) => { a.DeadLetterQueue = n.ParseMapWithExtensions(identifierFixFields); } }, + { "maxReceiveCount", (a, n) => { a.MaxReceiveCount = n.GetIntegerValue(); } }, + }; + + private static FixedFieldMap identifierFixFields => new() + { + { "arn", (a, n) => { a.Arn = n.GetScalarValue(); } }, + { "name", (a, n) => { a.Name = n.GetScalarValue(); } }, + }; + + private FixedFieldMap policyFixedFields = new() + { + { "statements", (a, n) => { a.Statements = n.CreateList(s => s.ParseMapWithExtensions(statementFixedFields)); } }, + }; + + private static FixedFieldMap statementFixedFields = new() + { + { "effect", (a, n) => { a.Effect = n.GetScalarValue().GetEnumFromDisplayName(); } }, + { "principal", (a, n) => { a.Principal = Principal.Parse(n); } }, + { "action", (a, n) => { a.Action = StringOrStringList.Parse(n); } }, + { "resource", (a, n) => { a.Resource = StringOrStringList.Parse(n); } }, + { "condition", (a, n) => { a.Condition = Condition.Parse(n); } }, + }; + + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteRequiredObject("queue", this.Queue, (w, q) => q.Serialize(w)); + writer.WriteOptionalObject("deadLetterQueue", this.DeadLetterQueue, (w, q) => q.Serialize(w)); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/SqsOperationBinding.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/SqsOperationBinding.cs new file mode 100644 index 00000000..0beb89b8 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/SqsOperationBinding.cs @@ -0,0 +1,78 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.Sqs +{ + using System; + using System.Collections.Generic; + using LEGO.AsyncAPI.Readers.ParseNodes; + using LEGO.AsyncAPI.Writers; + + public class SqsOperationBinding : OperationBinding + { + /// + /// Queue objects that are either the endpoint for an SNS Operation Binding Object, or the deadLetterQueue of the SQS Operation Binding Object + /// + public List Queues { get; set; } + + public override string BindingKey => "sqs"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "queues", (a, n) => { a.Queues = n.CreateList(s => s.ParseMapWithExtensions(this.queueFixedFields)); } }, + }; + + private FixedFieldMap queueFixedFields => new() + { + { "name", (a, n) => { a.Name = n.GetScalarValue(); } }, + { "fifoQueue", (a, n) => { a.FifoQueue = n.GetBooleanValue(); } }, + { "deduplicationScope", (a, n) => { a.DeduplicationScope = n.GetScalarValue().GetEnumFromDisplayName(); } }, + { "fifoThroughputLimit", (a, n) => { a.FifoThroughputLimit = n.GetScalarValue().GetEnumFromDisplayName(); } }, + { "deliveryDelay", (a, n) => { a.DeliveryDelay = n.GetIntegerValue(); } }, + { "visibilityTimeout", (a, n) => { a.VisibilityTimeout = n.GetIntegerValue(); } }, + { "receiveMessageWaitTime", (a, n) => { a.ReceiveMessageWaitTime = n.GetIntegerValue(); } }, + { "messageRetentionPeriod", (a, n) => { a.MessageRetentionPeriod = n.GetIntegerValue(); } }, + { "redrivePolicy", (a, n) => { a.RedrivePolicy = n.ParseMapWithExtensions(this.redrivePolicyFixedFields); } }, + { "policy", (a, n) => { a.Policy = n.ParseMapWithExtensions(this.policyFixedFields); } }, + { "tags", (a, n) => { a.Tags = n.CreateSimpleMap(s => s.GetScalarValue()); } }, + }; + + private FixedFieldMap redrivePolicyFixedFields => new() + { + { "deadLetterQueue", (a, n) => { a.DeadLetterQueue = n.ParseMapWithExtensions(identifierFixFields); } }, + { "maxReceiveCount", (a, n) => { a.MaxReceiveCount = n.GetIntegerValue(); } }, + }; + + private static FixedFieldMap identifierFixFields => new() + { + { "arn", (a, n) => { a.Arn = n.GetScalarValue(); } }, + { "name", (a, n) => { a.Name = n.GetScalarValue(); } }, + }; + + private FixedFieldMap policyFixedFields = new() + { + { "statements", (a, n) => { a.Statements = n.CreateList(s => s.ParseMapWithExtensions(statementFixedFields)); } }, + }; + + private static FixedFieldMap statementFixedFields = new() + { + { "effect", (a, n) => { a.Effect = n.GetScalarValue().GetEnumFromDisplayName(); } }, + { "principal", (a, n) => { a.Principal = Principal.Parse(n); } }, + { "action", (a, n) => { a.Action = StringOrStringList.Parse(n); } }, + { "resource", (a, n) => { a.Resource = StringOrStringList.Parse(n); } }, + { "condition", (a, n) => { a.Condition = Condition.Parse(n); } }, + }; + + public override void SerializeProperties(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteRequiredCollection("queues", this.Queues, (w, t) => t.Serialize(w)); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sqs/Statement.cs b/src/LEGO.AsyncAPI.Bindings/Sqs/Statement.cs new file mode 100644 index 00000000..4a9c5303 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/Sqs/Statement.cs @@ -0,0 +1,66 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings.Sqs +{ + using System; + using System.Collections.Generic; + using LEGO.AsyncAPI.Attributes; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + + public class Statement : IAsyncApiExtensible + { + /// + /// Indicates whether the policy allows or denies access. + /// + public Effect Effect { get; set; } + + /// + /// The AWS account(s) or resource ARN(s) that this statement applies to. + /// + public Principal Principal { get; set; } + + /// + /// The SNS permission being allowed or denied e.g. sns:Publish. + /// + public StringOrStringList Action { get; set; } + + /// + /// The resource(s) that this policy applies to. + /// + public StringOrStringList? Resource { get; set; } + + /// + /// Specific circumstances under which the policy grants permission. + /// + public Condition Condition { get; set; } + + public IDictionary Extensions { get; set; } = new Dictionary(); + + public void Serialize(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + writer.WriteStartObject(); + writer.WriteRequiredProperty("effect", this.Effect.GetDisplayName()); + writer.WriteRequiredObject("principal", this.Principal, (w, t) => t.Serialize(w)); + writer.WriteRequiredObject("action", this.Action, (w, t) => t.Value.Write(w)); + writer.WriteOptionalObject("resource", this.Resource, (w, t) => t?.Value.Write(w)); + writer.WriteOptionalObject("condition", this.Condition, (w, t) => t.Serialize(w)); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } + + public enum Effect + { + [Display("allow")] + Allow, + [Display("deny")] + Deny, + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/StringOrStringList.cs b/src/LEGO.AsyncAPI.Bindings/StringOrStringList.cs new file mode 100644 index 00000000..b9946f08 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/StringOrStringList.cs @@ -0,0 +1,61 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings +{ + using System; + using System.Linq; + using System.Text.Json; + using System.Text.Json.Nodes; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Readers.ParseNodes; + + public class StringOrStringList : IAsyncApiElement + { + public StringOrStringList(AsyncApiAny value) + { + this.Value = value.GetNode() switch + { + JsonArray array => IsValidStringList(array) ? new AsyncApiAny(array) : throw new ArgumentException($"{nameof(StringOrStringList)} value should only contain string items."), + JsonValue jValue => IsString(jValue) ? new AsyncApiAny(jValue) : throw new ArgumentException($"{nameof(StringOrStringList)} should be a string value or a string list."), + _ => throw new ArgumentException($"{nameof(StringOrStringList)} should be a string value or a string list."), + }; + } + + public AsyncApiAny Value { get; } + + public static StringOrStringList Parse(ParseNode node) + { + switch (node) + { + case ValueNode: + return new StringOrStringList(new AsyncApiAny(node.GetScalarValue())); + case ListNode listNode: + { + var jsonArray = new JsonArray(); + foreach (var item in listNode) + { + jsonArray.Add(item.GetScalarValue()); + } + + return new StringOrStringList(new AsyncApiAny(jsonArray)); + } + + default: + throw new ArgumentException($"An error occured while parsing a {nameof(StringOrStringList)} node. " + + $"Node should contain a string value or a list of strings."); + } + } + + private static bool IsString(JsonNode value) + { + var element = JsonDocument.Parse(value.ToJsonString()).RootElement; + return element.ValueKind == JsonValueKind.String; + } + + private static bool IsValidStringList(JsonArray array) + { + return array.All(x => IsString(x)); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/Bindings/WebSockets/WebSocketsChannelBinding.cs b/src/LEGO.AsyncAPI.Bindings/WebSockets/WebSocketsChannelBinding.cs similarity index 57% rename from src/LEGO.AsyncAPI/Models/Bindings/WebSockets/WebSocketsChannelBinding.cs rename to src/LEGO.AsyncAPI.Bindings/WebSockets/WebSocketsChannelBinding.cs index 7aa2e793..c87393bb 100644 --- a/src/LEGO.AsyncAPI/Models/Bindings/WebSockets/WebSocketsChannelBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/WebSockets/WebSocketsChannelBinding.cs @@ -1,13 +1,14 @@ // Copyright (c) The LEGO Group. All rights reserved. -namespace LEGO.AsyncAPI.Models.Bindings.WebSockets +namespace LEGO.AsyncAPI.Bindings.WebSockets { using System; - using System.Collections.Generic; - using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Readers; + using LEGO.AsyncAPI.Readers.ParseNodes; using LEGO.AsyncAPI.Writers; - public class WebSocketsChannelBinding : IChannelBinding + public class WebSocketsChannelBinding : ChannelBinding { /// /// The HTTP method t use when establishing the connection. Its value MUST be either 'GET' or 'POST'. @@ -24,18 +25,17 @@ public class WebSocketsChannelBinding : IChannelBinding /// public AsyncApiSchema Headers { get; set; } - public string BindingVersion { get; set; } + public override string BindingKey => "websockets"; - public bool UnresolvedReference { get; set; } - - public AsyncApiReference Reference { get; set; } - - public IDictionary Extensions { get; set; } = - new Dictionary(); - - public BindingType Type => BindingType.Websockets; + protected override FixedFieldMap FixedFieldMap => new() + { + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + { "method", (a, n) => { a.Method = n.GetScalarValue(); } }, + { "query", (a, n) => { a.Query = JsonSchemaDeserializer.LoadSchema(n); } }, + { "headers", (a, n) => { a.Headers = JsonSchemaDeserializer.LoadSchema(n); } }, + }; - public void SerializeV2WithoutReference(IAsyncApiWriter writer) + public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) { @@ -48,24 +48,8 @@ public void SerializeV2WithoutReference(IAsyncApiWriter writer) writer.WriteOptionalObject(AsyncApiConstants.Query, this.Query, (w, h) => h.SerializeV2(w)); writer.WriteOptionalObject(AsyncApiConstants.Headers, this.Headers, (w, h) => h.SerializeV2(w)); writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); - + writer.WriteExtensions(this.Extensions); writer.WriteEndObject(); } - - public void SerializeV2(IAsyncApiWriter writer) - { - if (writer is null) - { - throw new ArgumentNullException(nameof(writer)); - } - - if (this.Reference != null && !writer.GetSettings().ShouldInlineReference(this.Reference)) - { - this.Reference.SerializeV2(writer); - return; - } - - this.SerializeV2WithoutReference(writer); - } } -} \ No newline at end of file +} diff --git a/src/LEGO.AsyncAPI.Bindings/stylecop.json b/src/LEGO.AsyncAPI.Bindings/stylecop.json new file mode 100644 index 00000000..0a8f4661 --- /dev/null +++ b/src/LEGO.AsyncAPI.Bindings/stylecop.json @@ -0,0 +1,15 @@ +{ + // ACTION REQUIRED: This file was automatically added to your project, but it + // will not take effect until additional steps are taken to enable it. See the + // following page for additional information: + // + // https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md + + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "The LEGO Group", + "xmlHeader": false + } + } +} diff --git a/src/LEGO.AsyncAPI.Readers/AsyncApiYamlDocumentReader.cs b/src/LEGO.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs similarity index 63% rename from src/LEGO.AsyncAPI.Readers/AsyncApiYamlDocumentReader.cs rename to src/LEGO.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs index 655f2847..3a973654 100644 --- a/src/LEGO.AsyncAPI.Readers/AsyncApiYamlDocumentReader.cs +++ b/src/LEGO.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs @@ -4,6 +4,8 @@ namespace LEGO.AsyncAPI.Readers { using System.Collections.Generic; using System.Linq; + using System.Text.Json.Nodes; + using System.Threading; using System.Threading.Tasks; using LEGO.AsyncAPI.Exceptions; using LEGO.AsyncAPI.Extensions; @@ -11,20 +13,19 @@ namespace LEGO.AsyncAPI.Readers using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Readers.Interface; using LEGO.AsyncAPI.Validations; - using YamlDotNet.RepresentationModel; /// - /// Service class for converting contents of TextReader into AsyncApiDocument instances + /// Service class for converting contents of TextReader into AsyncApiDocument instances. /// - internal class AsyncApiYamlDocumentReader : IAsyncApiReader + internal class AsyncApiJsonDocumentReader : IAsyncApiReader { private readonly AsyncApiReaderSettings settings; /// - /// Create stream reader with custom settings if desired. + /// Initializes a new instance of the class. /// - /// - public AsyncApiYamlDocumentReader(AsyncApiReaderSettings settings = null) + /// The settings used to read json. + public AsyncApiJsonDocumentReader(AsyncApiReaderSettings settings = null) { this.settings = settings ?? new AsyncApiReaderSettings(); } @@ -33,14 +34,18 @@ public AsyncApiYamlDocumentReader(AsyncApiReaderSettings settings = null) /// Reads the stream input and parses it into an AsyncApi document. /// /// TextReader containing AsyncApi description to parse. - /// Returns diagnostic object containing errors detected during parsing - /// Instance of newly created AsyncApiDocument - public AsyncApiDocument Read(YamlDocument input, out AsyncApiDiagnostic diagnostic) + /// Returns diagnostic object containing errors detected during parsing. + /// Instance of newly created AsyncApiDocument. + public AsyncApiDocument Read(JsonNode input, out AsyncApiDiagnostic diagnostic) { diagnostic = new AsyncApiDiagnostic(); - var context = new ParsingContext(diagnostic) + var context = new ParsingContext(diagnostic, this.settings) { ExtensionParsers = this.settings.ExtensionParsers, + ServerBindingParsers = this.settings.Bindings.OfType>().ToDictionary(b => b.BindingKey, b => b), + ChannelBindingParsers = this.settings.Bindings.OfType>().ToDictionary(b => b.BindingKey, b => b), + OperationBindingParsers = this.settings.Bindings.OfType>().ToDictionary(b => b.BindingKey, b => b), + MessageBindingParsers = this.settings.Bindings.OfType>().ToDictionary(b => b.BindingKey, b => b), }; AsyncApiDocument document = null; @@ -58,12 +63,12 @@ public AsyncApiDocument Read(YamlDocument input, out AsyncApiDiagnostic diagnost if (this.settings.RuleSet != null && this.settings.RuleSet.Rules.Count > 0) { var asyncApiErrors = document.Validate(this.settings.RuleSet); - foreach (var item in asyncApiErrors.Where(e => e is AsyncApiValidatorError)) + foreach (var item in asyncApiErrors.OfType()) { diagnostic.Errors.Add(item); } - foreach (var item in asyncApiErrors.Where(e => e is AsyncApiValidatorWarning)) + foreach (var item in asyncApiErrors.OfType()) { diagnostic.Warnings.Add(item); } @@ -72,10 +77,10 @@ public AsyncApiDocument Read(YamlDocument input, out AsyncApiDiagnostic diagnost return document; } - public async Task ReadAsync(YamlDocument input) + public async Task ReadAsync(JsonNode input, CancellationToken cancellationToken = default) { var diagnostic = new AsyncApiDiagnostic(); - var context = new ParsingContext(diagnostic) + var context = new ParsingContext(diagnostic, this.settings) { ExtensionParsers = this.settings.ExtensionParsers, }; @@ -95,11 +100,16 @@ public async Task ReadAsync(YamlDocument input) // Validate the document if (this.settings.RuleSet != null && this.settings.RuleSet.Rules.Count > 0) { - var errors = document.Validate(this.settings.RuleSet); - foreach (var item in errors) + var asyncApiErrors = document.Validate(this.settings.RuleSet); + foreach (var item in asyncApiErrors.OfType()) { diagnostic.Errors.Add(item); } + + foreach (var item in asyncApiErrors.OfType()) + { + diagnostic.Warnings.Add(item); + } } return new ReadResult @@ -109,41 +119,24 @@ public async Task ReadAsync(YamlDocument input) }; } - private void ResolveReferences(AsyncApiDiagnostic diagnostic, AsyncApiDocument document) - { - var errors = new List(); - - // Resolve References if requested - switch (this.settings.ReferenceResolution) - { - case ReferenceResolutionSetting.ResolveReferences: - errors.AddRange(document.ResolveReferences()); - break; - case ReferenceResolutionSetting.DoNotResolveReferences: - break; - } - - foreach (var item in errors) - { - diagnostic.Errors.Add(item); - } - } - - /// /// Reads the stream input and parses the fragment of an AsyncApi description into an AsyncApi Element. /// /// TextReader containing AsyncApi description to parse. /// Version of the AsyncApi specification that the fragment conforms to. - /// Returns diagnostic object containing errors detected during parsing - /// Instance of newly created AsyncApiDocument - public T ReadFragment(YamlDocument input, AsyncApiVersion version, out AsyncApiDiagnostic diagnostic) + /// Returns diagnostic object containing errors detected during parsing. + /// Instance of newly created AsyncApiDocument. + public T ReadFragment(JsonNode input, AsyncApiVersion version, out AsyncApiDiagnostic diagnostic) where T : IAsyncApiElement { diagnostic = new AsyncApiDiagnostic(); - var context = new ParsingContext(diagnostic) + var context = new ParsingContext(diagnostic, this.settings) { ExtensionParsers = this.settings.ExtensionParsers, + ServerBindingParsers = this.settings.Bindings.OfType>().ToDictionary(b => b.BindingKey, b => b), + ChannelBindingParsers = this.settings.Bindings.OfType>().ToDictionary(b => b.BindingKey, b => b), + OperationBindingParsers = this.settings.Bindings.OfType>().ToDictionary(b => b.BindingKey, b => b), + MessageBindingParsers = this.settings.Bindings.OfType>().ToDictionary(b => b.BindingKey, b => b), }; IAsyncApiElement element = null; @@ -169,5 +162,26 @@ public T ReadFragment(YamlDocument input, AsyncApiVersion version, out AsyncA return (T)element; } + + private void ResolveReferences(AsyncApiDiagnostic diagnostic, AsyncApiDocument document) + { + var errors = new List(); + + // Resolve References if requested + switch (this.settings.ReferenceResolution) + { + case ReferenceResolutionSetting.ResolveReferences: + errors.AddRange(document.ResolveReferences()); + break; + + case ReferenceResolutionSetting.DoNotResolveReferences: + break; + } + + foreach (var item in errors) + { + diagnostic.Errors.Add(item); + } + } } -} +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Readers/AsyncApiReaderSettings.cs b/src/LEGO.AsyncAPI.Readers/AsyncApiReaderSettings.cs index 21fe1c73..6ca2e129 100644 --- a/src/LEGO.AsyncAPI.Readers/AsyncApiReaderSettings.cs +++ b/src/LEGO.AsyncAPI.Readers/AsyncApiReaderSettings.cs @@ -5,7 +5,9 @@ namespace LEGO.AsyncAPI.Readers using System; using System.Collections.Generic; using System.IO; + using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Readers.Interface; using LEGO.AsyncAPI.Validations; public enum ReferenceResolutionSetting @@ -16,15 +18,15 @@ public enum ReferenceResolutionSetting DoNotResolveReferences, /// - /// ResolveAllReferences, effectively inlining them. + /// Resolve internal component references and inline them. /// ResolveReferences, } /// - /// Configuration settings to control how AsyncApi documents are parsed + /// Configuration settings to control how AsyncApi documents are parsed. /// - public class AsyncApiReaderSettings + public class AsyncApiReaderSettings : AsyncApiSettings { /// /// Indicates how references in the source document should be handled. @@ -35,10 +37,15 @@ public class AsyncApiReaderSettings /// /// Dictionary of parsers for converting extensions into strongly typed classes. /// - public Dictionary> + public Dictionary> ExtensionParsers { get; set; } = - new Dictionary>(); + new Dictionary>(); + + public IEnumerable> + Bindings + { get; set; } = + new List>(); /// /// Rules to use for validating AsyncApi specification. If none are provided a default set of rules are applied. diff --git a/src/LEGO.AsyncAPI.Readers/AsyncApiStreamReader.cs b/src/LEGO.AsyncAPI.Readers/AsyncApiStreamReader.cs index da04716d..2cbf47ac 100644 --- a/src/LEGO.AsyncAPI.Readers/AsyncApiStreamReader.cs +++ b/src/LEGO.AsyncAPI.Readers/AsyncApiStreamReader.cs @@ -3,13 +3,14 @@ namespace LEGO.AsyncAPI.Readers { using System.IO; + using System.Threading; using System.Threading.Tasks; using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Readers.Interface; /// - /// Service class for converting streams into AsyncApiDocument instances + /// Service class for converting streams into AsyncApiDocument instances. /// public class AsyncApiStreamReader : IAsyncApiReader { @@ -33,7 +34,7 @@ public AsyncApiStreamReader(AsyncApiReaderSettings settings = null) public AsyncApiDocument Read(Stream input, out AsyncApiDiagnostic diagnostic) { var reader = new StreamReader(input); - var result = new AsyncApiTextReaderReader(this.settings).Read(reader, out diagnostic); + var result = new AsyncApiTextReader(this.settings).Read(reader, out diagnostic); if (!this.settings.LeaveStreamOpen) { reader.Dispose(); @@ -46,8 +47,11 @@ public AsyncApiDocument Read(Stream input, out AsyncApiDiagnostic diagnostic) /// Reads the stream input and parses it into an AsyncApi document. /// /// Stream containing AsyncApi description to parse. - /// Instance result containing newly created AsyncApiDocument and diagnostics object from the process - public async Task ReadAsync(Stream input) + /// The cancellation token. + /// + /// Instance result containing newly created AsyncApiDocument and diagnostics object from the process. + /// + public async Task ReadAsync(Stream input, CancellationToken cancellationToken) { MemoryStream bufferedStream; if (input is MemoryStream) @@ -65,7 +69,7 @@ public async Task ReadAsync(Stream input) var reader = new StreamReader(bufferedStream); - return await new AsyncApiTextReaderReader(this.settings).ReadAsync(reader); + return await new AsyncApiTextReader(this.settings).ReadAsync(reader, cancellationToken); } /// @@ -73,14 +77,14 @@ public async Task ReadAsync(Stream input) /// /// Stream containing AsyncApi description to parse. /// Version of the AsyncApi specification that the fragment conforms to. - /// Returns diagnostic object containing errors detected during parsing - /// Instance of newly created AsyncApiDocument + /// Returns diagnostic object containing errors detected during parsing. + /// Instance of newly created AsyncApiDocument. public T ReadFragment(Stream input, AsyncApiVersion version, out AsyncApiDiagnostic diagnostic) where T : IAsyncApiReferenceable { using (var reader = new StreamReader(input)) { - return new AsyncApiTextReaderReader(this.settings).ReadFragment(reader, version, out diagnostic); + return new AsyncApiTextReader(this.settings).ReadFragment(reader, version, out diagnostic); } } } diff --git a/src/LEGO.AsyncAPI.Readers/AsyncApiStringReader.cs b/src/LEGO.AsyncAPI.Readers/AsyncApiStringReader.cs index c6680620..90d3f84b 100644 --- a/src/LEGO.AsyncAPI.Readers/AsyncApiStringReader.cs +++ b/src/LEGO.AsyncAPI.Readers/AsyncApiStringReader.cs @@ -8,14 +8,14 @@ namespace LEGO.AsyncAPI.Readers using LEGO.AsyncAPI.Readers.Interface; /// - /// Service class for converting strings into AsyncApiDocument instances + /// Service class for converting strings into AsyncApiDocument instances. /// public class AsyncApiStringReader : IAsyncApiReader { private readonly AsyncApiReaderSettings settings; /// - /// Constructor tha allows reader to use non-default settings + /// Constructor tha allows reader to use non-default settings. /// /// public AsyncApiStringReader(AsyncApiReaderSettings settings = null) @@ -30,7 +30,7 @@ public AsyncApiDocument Read(string input, out AsyncApiDiagnostic diagnostic) { using (var reader = new StringReader(input)) { - return new AsyncApiTextReaderReader(this.settings).Read(reader, out diagnostic); + return new AsyncApiTextReader(this.settings).Read(reader, out diagnostic); } } @@ -42,7 +42,7 @@ public T ReadFragment(string input, AsyncApiVersion version, out AsyncApiDiag { using (var reader = new StringReader(input)) { - return new AsyncApiTextReaderReader(this.settings).ReadFragment(reader, version, out diagnostic); + return new AsyncApiTextReader(this.settings).ReadFragment(reader, version, out diagnostic); } } } diff --git a/src/LEGO.AsyncAPI.Readers/AsyncApiTextReader.cs b/src/LEGO.AsyncAPI.Readers/AsyncApiTextReader.cs index 51817948..eeece6a7 100644 --- a/src/LEGO.AsyncAPI.Readers/AsyncApiTextReader.cs +++ b/src/LEGO.AsyncAPI.Readers/AsyncApiTextReader.cs @@ -4,17 +4,20 @@ namespace LEGO.AsyncAPI.Readers { using System.IO; using System.Linq; + using System.Runtime.CompilerServices; + using System.Text.Json; + using System.Text.Json.Nodes; + using System.Threading; using System.Threading.Tasks; using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Readers.Interface; - using YamlDotNet.Core; using YamlDotNet.RepresentationModel; /// - /// Service class for converting contents of TextReader into AsyncApiDocument instances + /// Service class for converting contents of TextReader into AsyncApiDocument instances. /// - public class AsyncApiTextReaderReader : IAsyncApiReader + public class AsyncApiTextReader : IAsyncApiReader { private readonly AsyncApiReaderSettings settings; @@ -22,7 +25,7 @@ public class AsyncApiTextReaderReader : IAsyncApiReader /// - public AsyncApiTextReaderReader(AsyncApiReaderSettings settings = null) + public AsyncApiTextReader(AsyncApiReaderSettings settings = null) { this.settings = settings ?? new AsyncApiReaderSettings(); } @@ -31,45 +34,48 @@ public AsyncApiTextReaderReader(AsyncApiReaderSettings settings = null) /// Reads the stream input and parses it into an AsyncApi document. /// /// TextReader containing AsyncApi description to parse. - /// Returns diagnostic object containing errors detected during parsing - /// Instance of newly created AsyncApiDocument + /// Returns diagnostic object containing errors detected during parsing. + /// Instance of newly created AsyncApiDocument. public AsyncApiDocument Read(TextReader input, out AsyncApiDiagnostic diagnostic) { - YamlDocument yamlDocument; + JsonNode jsonNode; // Parse the YAML/JSON text in the TextReader into the YamlDocument try { - yamlDocument = LoadYamlDocument(input); + jsonNode = LoadYamlDocument(input, this.settings); } - catch (YamlException ex) + catch (JsonException ex) { diagnostic = new AsyncApiDiagnostic(); - diagnostic.Errors.Add(new AsyncApiError($"#line={ex.Start.Line}", ex.Message)); + diagnostic.Errors.Add(new AsyncApiError($"#line={ex.LineNumber}", ex.Message)); return new AsyncApiDocument(); } - return new AsyncApiYamlDocumentReader(this.settings).Read(yamlDocument, out diagnostic); + return new AsyncApiJsonDocumentReader(this.settings).Read(jsonNode, out diagnostic); } /// - /// Reads the content of the TextReader. If there are references to external documents then they will be read asynchronously. + /// Reads the content of the TextReader. /// /// TextReader containing AsyncApi description to parse. - /// A ReadResult instance that contains the resulting AsyncApiDocument and a diagnostics instance. - public async Task ReadAsync(TextReader input) + /// The cancellation token. + /// + /// A ReadResult instance that contains the resulting AsyncApiDocument and a diagnostics instance. + /// + public async Task ReadAsync(TextReader input, CancellationToken cancellationToken) { - YamlDocument yamlDocument; + JsonNode jsonNode; // Parse the YAML/JSON text in the TextReader into the YamlDocument try { - yamlDocument = LoadYamlDocument(input); + jsonNode = LoadYamlDocument(input, this.settings); } - catch (YamlException ex) + catch (JsonException ex) { var diagnostic = new AsyncApiDiagnostic(); - diagnostic.Errors.Add(new AsyncApiError($"#line={ex.Start.Line}", ex.Message)); + diagnostic.Errors.Add(new AsyncApiError($"#line={ex.LineNumber}", ex.Message)); return new ReadResult { AsyncApiDocument = null, @@ -77,7 +83,7 @@ public async Task ReadAsync(TextReader input) }; } - return await new AsyncApiYamlDocumentReader(this.settings).ReadAsync(yamlDocument); + return await new AsyncApiJsonDocumentReader(this.settings).ReadAsync(jsonNode, cancellationToken); } /// @@ -85,39 +91,39 @@ public async Task ReadAsync(TextReader input) /// /// TextReader containing AsyncApi description to parse. /// Version of the AsyncApi specification that the fragment conforms to. - /// Returns diagnostic object containing errors detected during parsing - /// Instance of newly created AsyncApiDocument + /// Returns diagnostic object containing errors detected during parsing. + /// Instance of newly created AsyncApiDocument. public T ReadFragment(TextReader input, AsyncApiVersion version, out AsyncApiDiagnostic diagnostic) where T : IAsyncApiElement { - YamlDocument yamlDocument; + JsonNode jsonNode; // Parse the YAML/JSON try { - yamlDocument = LoadYamlDocument(input); + jsonNode = LoadYamlDocument(input, this.settings); } - catch (YamlException ex) + catch (JsonException ex) { diagnostic = new AsyncApiDiagnostic(); - diagnostic.Errors.Add(new AsyncApiError($"#line={ex.Start.Line}", ex.Message)); + diagnostic.Errors.Add(new AsyncApiError($"#line={ex.LineNumber}", ex.Message)); return default; } - return new AsyncApiYamlDocumentReader(this.settings).ReadFragment(yamlDocument, version, + return new AsyncApiJsonDocumentReader(this.settings).ReadFragment(jsonNode, version, out diagnostic); } /// - /// Helper method to turn streams into YamlDocument + /// Helper method to turn streams into YamlDocument. /// - /// Stream containing YAML formatted text - /// Instance of a YamlDocument - static YamlDocument LoadYamlDocument(TextReader input) + /// Stream containing YAML formatted text. + /// Instance of a YamlDocument. + static JsonNode LoadYamlDocument(TextReader input, AsyncApiReaderSettings settings) { var yamlStream = new YamlStream(); yamlStream.Load(input); - return yamlStream.Documents.First(); + return yamlStream.Documents.First().ToJsonNode(settings); } } } diff --git a/src/LEGO.AsyncAPI.Readers/BindingDeserializer.cs b/src/LEGO.AsyncAPI.Readers/BindingDeserializer.cs new file mode 100644 index 00000000..5744985d --- /dev/null +++ b/src/LEGO.AsyncAPI.Readers/BindingDeserializer.cs @@ -0,0 +1,31 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Readers +{ + using LEGO.AsyncAPI.Extensions; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Readers.ParseNodes; + + public class BindingDeserializer + { + public static T LoadBinding(string nodeName, ParseNode node, FixedFieldMap fieldMap) + where T : IBinding, new() + { + var mapNode = node.CheckMapNode(nodeName); + var binding = new T(); + + AsyncApiV2Deserializer.ParseMap(mapNode, binding, fieldMap, BindingPatternExtensionFields()); + + return binding; + } + + private static PatternFieldMap BindingPatternExtensionFields() + where T : IBinding, new() + { + return new() + { + { s => s.StartsWith("x-"), (a, p, n) => a.AddExtension(p, AsyncApiV2Deserializer.LoadExtension(p, n)) }, + }; + } + } +} diff --git a/src/LEGO.AsyncAPI.Readers/Bindings/AsyncApiHttpBindingsDeserializer.cs b/src/LEGO.AsyncAPI.Readers/Bindings/AsyncApiHttpBindingsDeserializer.cs deleted file mode 100644 index 72c2579b..00000000 --- a/src/LEGO.AsyncAPI.Readers/Bindings/AsyncApiHttpBindingsDeserializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Readers -{ - using LEGO.AsyncAPI.Models.Bindings.Http; - using LEGO.AsyncAPI.Readers.ParseNodes; - - internal static partial class AsyncApiV2Deserializer - { - private static FixedFieldMap httpMessageBindingFixedFields = new() - { - { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, - { "headers", (a, n) => { a.Headers = LoadSchema(n); } }, - }; - - private static FixedFieldMap httpOperationBindingFixedFields = new() - { - { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, - { "type", (a, n) => { a.Type = n.GetScalarValue(); } }, - { "method", (a, n) => { a.Method = n.GetScalarValue(); } }, - { "query", (a, n) => { a.Query = LoadSchema(n); } }, - }; - - } -} diff --git a/src/LEGO.AsyncAPI.Readers/Bindings/AsyncApiKafkaBindingsDeserializer.cs b/src/LEGO.AsyncAPI.Readers/Bindings/AsyncApiKafkaBindingsDeserializer.cs deleted file mode 100644 index 0d80eb8f..00000000 --- a/src/LEGO.AsyncAPI.Readers/Bindings/AsyncApiKafkaBindingsDeserializer.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Readers -{ - using LEGO.AsyncAPI.Models.Bindings.Kafka; - using LEGO.AsyncAPI.Readers.ParseNodes; - - internal static partial class AsyncApiV2Deserializer - { - private static FixedFieldMap kafkaServerBindingFixedFields = new() - { - { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, - { "schemaRegistryUrl", (a, n) => { a.SchemaRegistryUrl = n.GetScalarValue(); } }, - { "schemaRegistryVendor", (a, n) => { a.SchemaRegistryVendor = n.GetScalarValue(); } }, - }; - - private static FixedFieldMap kafkaChannelBindingFixedFields = new() - { - { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, - { "topic", (a, n) => { a.Topic = n.GetScalarValue(); } }, - { "partitions", (a, n) => { a.Partitions = n.GetIntegerValue(); } }, - { "topicConfiguration", (a, n) => { a.TopicConfiguration = LoadTopicConfiguration(n); } }, - { "replicas", (a, n) => { a.Replicas = n.GetIntegerValue(); } }, - }; - - private static FixedFieldMap kafkaChannelTopicConfigurationObjectFixedFields = new() - { - { "cleanup.policy", (a, n) => { a.CleanupPolicy = n.CreateSimpleList(s => s.GetScalarValue()); } }, - { "retention.ms", (a, n) => { a.RetentionMiliseconds = n.GetIntegerValue(); } }, - { "retention.bytes", (a, n) => { a.RetentionBytes = n.GetIntegerValue(); } }, - { "delete.retention.ms", (a, n) => { a.DeleteRetentionMiliseconds = n.GetIntegerValue(); } }, - { "max.message.bytes", (a, n) => { a.MaxMessageBytes = n.GetIntegerValue(); } }, - }; - - private static FixedFieldMap kafkaOperationBindingFixedFields = new() - { - { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, - { "groupId", (a, n) => { a.GroupId = LoadSchema(n); } }, - { "clientId", (a, n) => { a.ClientId = LoadSchema(n); } }, - }; - - private static FixedFieldMap kafkaMessageBindingFixedFields = new() - { - { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, - { "key", (a, n) => { a.Key = LoadSchema(n); } }, - { "schemaIdLocation", (a, n) => { a.SchemaIdLocation = n.GetScalarValue(); } }, - { "schemaIdPayloadEncoding", (a, n) => { a.SchemaIdPayloadEncoding = n.GetScalarValue(); } }, - { "schemaLookupStrategy", (a, n) => { a.SchemaLookupStrategy = n.GetScalarValue(); } }, - }; - - private static TopicConfigurationObject LoadTopicConfiguration(ParseNode node) - { - var mapNode = node.CheckMapNode("topicConfiguration"); - var retention = new TopicConfigurationObject(); - ParseMap(mapNode, retention, kafkaChannelTopicConfigurationObjectFixedFields, null); - return retention; - } - } -} diff --git a/src/LEGO.AsyncAPI.Readers/Bindings/AsyncApiPulsarBindingsDeserializer.cs b/src/LEGO.AsyncAPI.Readers/Bindings/AsyncApiPulsarBindingsDeserializer.cs deleted file mode 100644 index 6250a064..00000000 --- a/src/LEGO.AsyncAPI.Readers/Bindings/AsyncApiPulsarBindingsDeserializer.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Readers -{ - using LEGO.AsyncAPI.Models.Bindings.Pulsar; - using LEGO.AsyncAPI.Readers.ParseNodes; - using LEGO.AsyncAPI.Writers; - - internal static partial class AsyncApiV2Deserializer - { - private static FixedFieldMap pulsarServerBindingFixedFields = new () - { - { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, - { "tenant", (a, n) => { a.Tenant = n.GetScalarValue(); } }, - }; - - private static FixedFieldMap pulsarChannelBindingFixedFields = new () - { - { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, - { "namespace", (a, n) => { a.Namespace = n.GetScalarValue(); } }, - { "persistence", (a, n) => { a.Persistence = n.GetScalarValue().GetEnumFromDisplayName(); } }, - { "compaction", (a, n) => { a.Compaction = n.GetIntegerValue(); } }, - { "retention", (a, n) => { a.Retention = LoadRetention(n); } }, - { "geo-replication", (a, n) => { a.GeoReplication = n.CreateSimpleList(s => s.GetScalarValue()); } }, - { "ttl", (a, n) => { a.TTL = n.GetIntegerValue(); } }, - { "deduplication", (a, n) => { a.Deduplication = n.GetBooleanValue(); } }, - }; - - private static FixedFieldMap pulsarServerBindingRetentionFixedFields = new () - { - { "time", (a, n) => { a.Time = n.GetIntegerValue(); } }, - { "size", (a, n) => { a.Size = n.GetIntegerValue(); } }, - }; - - private static RetentionDefinition LoadRetention(ParseNode node) - { - var mapNode = node.CheckMapNode("retention"); - var retention = new RetentionDefinition(); - ParseMap(mapNode, retention, pulsarServerBindingRetentionFixedFields, null); - return retention; - } - } -} diff --git a/src/LEGO.AsyncAPI.Readers/Bindings/AsyncApiWebSocketsBindingsDeserializer.cs b/src/LEGO.AsyncAPI.Readers/Bindings/AsyncApiWebSocketsBindingsDeserializer.cs deleted file mode 100644 index 4d4af870..00000000 --- a/src/LEGO.AsyncAPI.Readers/Bindings/AsyncApiWebSocketsBindingsDeserializer.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Readers -{ - using LEGO.AsyncAPI.Readers.ParseNodes; - using Models.Bindings.WebSockets; - - internal static partial class AsyncApiV2Deserializer - { - private static FixedFieldMap webSocketsChannelBindingFixedFields = new() - { - { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, - { "method", (a, n) => { a.Method = n.GetScalarValue(); } }, - { "query", (a, n) => { a.Query = LoadSchema(n); } }, - { "headers", (a, n) => { a.Headers = LoadSchema(n); } }, - }; - } -} diff --git a/src/LEGO.AsyncAPI.Readers/Exceptions/AsyncApiReaderException.cs b/src/LEGO.AsyncAPI.Readers/Exceptions/AsyncApiReaderException.cs index c58ccf1c..61ae4124 100644 --- a/src/LEGO.AsyncAPI.Readers/Exceptions/AsyncApiReaderException.cs +++ b/src/LEGO.AsyncAPI.Readers/Exceptions/AsyncApiReaderException.cs @@ -4,31 +4,28 @@ namespace LEGO.AsyncAPI.Readers.Exceptions { using System; using LEGO.AsyncAPI.Exceptions; - using YamlDotNet.RepresentationModel; [Serializable] public class AsyncApiReaderException : AsyncApiException { - public AsyncApiReaderException() { } + public AsyncApiReaderException() + { + } public AsyncApiReaderException(string message) - : base(message) { } - - public AsyncApiReaderException(string message, ParsingContext context) : base(message) { - this.Pointer = context.GetLocation(); } - public AsyncApiReaderException(string message, YamlNode node) + public AsyncApiReaderException(string message, ParsingContext context) : base(message) { - // This only includes line because using a char range causes tests to break due to CR/LF & LF differences - // See https://tools.ietf.org/html/rfc5147 for syntax - this.Pointer = $"#line={node.Start.Line}"; + this.Pointer = context.GetLocation(); } public AsyncApiReaderException(string message, Exception innerException) - : base(message, innerException) { } + : base(message, innerException) + { + } } } diff --git a/src/LEGO.AsyncAPI.Readers/Exceptions/AsyncApiUnsupportedSpecVersionException.cs b/src/LEGO.AsyncAPI.Readers/Exceptions/AsyncApiUnsupportedSpecVersionException.cs index ac183421..d68da4bf 100644 --- a/src/LEGO.AsyncAPI.Readers/Exceptions/AsyncApiUnsupportedSpecVersionException.cs +++ b/src/LEGO.AsyncAPI.Readers/Exceptions/AsyncApiUnsupportedSpecVersionException.cs @@ -11,12 +11,13 @@ namespace LEGO.AsyncAPI.Readers.Exceptions [Serializable] public class AsyncApiUnsupportedSpecVersionException : Exception { - const string MessagePattern = "AsyncApi specification version '{0}' is not supported."; + private const string MessagePattern = "AsyncApi specification version '{0}' is not supported."; /// - /// Initializes the class with a specification version. + /// Initializes a new instance of the class. /// /// Version that caused this exception to be thrown. + /// The settings used for reading and writing. public AsyncApiUnsupportedSpecVersionException(string specificationVersion) : base(string.Format(CultureInfo.InvariantCulture, MessagePattern, specificationVersion)) { @@ -24,10 +25,11 @@ public AsyncApiUnsupportedSpecVersionException(string specificationVersion) } /// - /// Initializes the class with a specification version and + /// Initializes a new instance of the class. /// inner exception. /// /// Version that caused this exception to be thrown. + /// The setting used for reading and writing /// Inner exception that caused this exception to be thrown. public AsyncApiUnsupportedSpecVersionException(string specificationVersion, Exception innerException) : base(string.Format(CultureInfo.InvariantCulture, MessagePattern, specificationVersion), innerException) diff --git a/src/LEGO.AsyncAPI.Readers/Interface/IAsyncApiReader.cs b/src/LEGO.AsyncAPI.Readers/Interface/IAsyncApiReader.cs index 6938ea88..ef2fb45f 100644 --- a/src/LEGO.AsyncAPI.Readers/Interface/IAsyncApiReader.cs +++ b/src/LEGO.AsyncAPI.Readers/Interface/IAsyncApiReader.cs @@ -4,7 +4,8 @@ namespace LEGO.AsyncAPI.Readers.Interface { using LEGO.AsyncAPI.Models; - public interface IAsyncApiReader where TDiagnostic : IDiagnostic + public interface IAsyncApiReader + where TDiagnostic : IDiagnostic { AsyncApiDocument Read(TInput input, out TDiagnostic diagnostic); } diff --git a/src/LEGO.AsyncAPI.Readers/Interface/IAsyncApiVersionService.cs b/src/LEGO.AsyncAPI.Readers/Interface/IAsyncApiVersionService.cs index 61922eef..c2345ac1 100644 --- a/src/LEGO.AsyncAPI.Readers/Interface/IAsyncApiVersionService.cs +++ b/src/LEGO.AsyncAPI.Readers/Interface/IAsyncApiVersionService.cs @@ -10,7 +10,8 @@ internal interface IAsyncApiVersionService { AsyncApiReference ConvertToAsyncApiReference(string reference, ReferenceType? type); - T LoadElement(ParseNode node) where T : IAsyncApiElement; + T LoadElement(ParseNode node) + where T : IAsyncApiElement; AsyncApiDocument LoadDocument(RootNode rootNode); } diff --git a/src/LEGO.AsyncAPI.Readers/Interface/IBindingParser{T}.cs b/src/LEGO.AsyncAPI.Readers/Interface/IBindingParser{T}.cs new file mode 100644 index 00000000..9c83fe4d --- /dev/null +++ b/src/LEGO.AsyncAPI.Readers/Interface/IBindingParser{T}.cs @@ -0,0 +1,12 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Readers.Interface +{ + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Readers.ParseNodes; + + public interface IBindingParser : IBinding + { + T LoadBinding(PropertyNode node); + } +} diff --git a/src/LEGO.AsyncAPI.Readers/LEGO.AsyncAPI.Readers.csproj b/src/LEGO.AsyncAPI.Readers/LEGO.AsyncAPI.Readers.csproj index d1e8bd74..53071ca0 100644 --- a/src/LEGO.AsyncAPI.Readers/LEGO.AsyncAPI.Readers.csproj +++ b/src/LEGO.AsyncAPI.Readers/LEGO.AsyncAPI.Readers.csproj @@ -1,18 +1,11 @@  - + - net6.0 - disable disable - The LEGO Group - https://github.com/LEGO/AsyncAPI.NET - README.md - AsyncAPI.NET Readers for JSON and YAML documents - asyncapi .net openapi documentation - AsyncAPI.NET.Readers - LEGO.AsyncAPI.Readers - LEGO.AsyncAPI.Readers - https://github.com/LEGO/AsyncAPI.NET + AsyncAPI.NET Readers for JSON and YAML documents + AsyncAPI.NET.Readers + LEGO.AsyncAPI.Readers + LEGO.AsyncAPI.Readers @@ -24,7 +17,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -34,14 +27,12 @@ + - - True - \ - + - + diff --git a/src/LEGO.AsyncAPI.Readers/ParseNodes/AnyFieldMapParameter.cs b/src/LEGO.AsyncAPI.Readers/ParseNodes/AnyFieldMapParameter.cs index 5b2073ed..76e9008a 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/AnyFieldMapParameter.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/AnyFieldMapParameter.cs @@ -4,13 +4,12 @@ namespace LEGO.AsyncAPI.Readers.ParseNodes { using System; using LEGO.AsyncAPI.Models; - using LEGO.AsyncAPI.Models.Interfaces; internal class AnyFieldMapParameter { public AnyFieldMapParameter( - Func propertyGetter, - Action propertySetter, + Func propertyGetter, + Action propertySetter, Func schemaGetter) { this.PropertyGetter = propertyGetter; @@ -18,9 +17,9 @@ public AnyFieldMapParameter( this.SchemaGetter = schemaGetter; } - public Func PropertyGetter { get; } + public Func PropertyGetter { get; } - public Action PropertySetter { get; } + public Action PropertySetter { get; } public Func SchemaGetter { get; } } diff --git a/src/LEGO.AsyncAPI.Readers/ParseNodes/AnyListFieldMapProperty.cs b/src/LEGO.AsyncAPI.Readers/ParseNodes/AnyListFieldMapParameter{T}.cs similarity index 56% rename from src/LEGO.AsyncAPI.Readers/ParseNodes/AnyListFieldMapProperty.cs rename to src/LEGO.AsyncAPI.Readers/ParseNodes/AnyListFieldMapParameter{T}.cs index 13c44f33..ee1af993 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/AnyListFieldMapProperty.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/AnyListFieldMapParameter{T}.cs @@ -1,18 +1,16 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. +// Copyright (c) The LEGO Group. All rights reserved. namespace LEGO.AsyncAPI.Readers.ParseNodes { using System; using System.Collections.Generic; using LEGO.AsyncAPI.Models; - using LEGO.AsyncAPI.Models.Interfaces; internal class AnyListFieldMapParameter { public AnyListFieldMapParameter( - Func> propertyGetter, - Action> propertySetter, + Func> propertyGetter, + Action> propertySetter, Func schemaGetter) { this.PropertyGetter = propertyGetter; @@ -20,9 +18,9 @@ public AnyListFieldMapParameter( this.SchemaGetter = schemaGetter; } - public Func> PropertyGetter { get; } + public Func> PropertyGetter { get; } - public Action> PropertySetter { get; } + public Action> PropertySetter { get; } public Func SchemaGetter { get; } } diff --git a/src/LEGO.AsyncAPI.Readers/ParseNodes/AnyMapFieldMapParameter{T,U}.cs b/src/LEGO.AsyncAPI.Readers/ParseNodes/AnyMapFieldMapParameter{T,U}.cs index 8b852453..2399fe31 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/AnyMapFieldMapParameter{T,U}.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/AnyMapFieldMapParameter{T,U}.cs @@ -5,14 +5,13 @@ namespace LEGO.AsyncAPI.Readers.ParseNodes using System; using System.Collections.Generic; using LEGO.AsyncAPI.Models; - using LEGO.AsyncAPI.Models.Interfaces; internal class AnyMapFieldMapParameter { public AnyMapFieldMapParameter( Func> propertyMapGetter, - Func propertyGetter, - Action propertySetter, + Func propertyGetter, + Action propertySetter, Func schemaGetter) { this.PropertyMapGetter = propertyMapGetter; @@ -23,9 +22,9 @@ public AnyMapFieldMapParameter( public Func> PropertyMapGetter { get; } - public Func PropertyGetter { get; } + public Func PropertyGetter { get; } - public Action PropertySetter { get; } + public Action PropertySetter { get; } public Func SchemaGetter { get; } } diff --git a/src/LEGO.AsyncAPI.Readers/ParseNodes/AsyncApiAnyConverter.cs b/src/LEGO.AsyncAPI.Readers/ParseNodes/AsyncApiAnyConverter.cs deleted file mode 100644 index ae73281e..00000000 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/AsyncApiAnyConverter.cs +++ /dev/null @@ -1,248 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Readers.ParseNodes -{ - using System; - using System.Globalization; - using System.Linq; - using System.Text; - using LEGO.AsyncAPI.Models; - using LEGO.AsyncAPI.Models.Any; - using LEGO.AsyncAPI.Models.Interfaces; - - internal static class AsyncApiAnyConverter - { - public static IAsyncApiAny GetSpecificAsyncApiAny(IAsyncApiAny asyncApiAny, AsyncApiSchema schema = null) - { - if (asyncApiAny is AsyncApiArray asyncApiArray) - { - var newArray = new AsyncApiArray(); - foreach (var element in asyncApiArray) - { - newArray.Add(GetSpecificAsyncApiAny(element, schema?.Items)); - } - - return newArray; - } - - if (asyncApiAny is AsyncApiObject asyncApiObject) - { - var newObject = new AsyncApiObject(); - - foreach (var key in asyncApiObject.Keys.ToList()) - { - if (schema?.Properties != null && schema.Properties.TryGetValue(key, out var property)) - { - newObject[key] = GetSpecificAsyncApiAny(asyncApiObject[key], property); - } - else - { - newObject[key] = GetSpecificAsyncApiAny(asyncApiObject[key], schema?.AdditionalProperties); - } - } - - return newObject; - } - - if (!(asyncApiAny is AsyncApiString)) - { - return asyncApiAny; - } - - var value = ((AsyncApiString)asyncApiAny).Value; - var type = schema?.Type; - var format = schema?.Format; - - if (((AsyncApiString)asyncApiAny).IsExplicit()) - { - if (schema == null) - { - if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateTimeValue)) - { - return new AsyncApiDateTime(dateTimeValue); - } - } - else if (type.Contains(SchemaType.String)) - { - if (format == "byte") - { - try - { - return new AsyncApiByte(Convert.FromBase64String(value)); - } - catch (FormatException) - { } - } - - if (format == "binary") - { - try - { - return new AsyncApiBinary(Encoding.UTF8.GetBytes(value)); - } - catch (EncoderFallbackException) - { } - } - - if (format == "date") - { - if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateValue)) - { - return new AsyncApiDate(dateValue.Date); - } - } - - if (format == "date-time") - { - if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateTimeValue)) - { - return new AsyncApiDateTime(dateTimeValue); - } - } - } - - return asyncApiAny; - } - - if (value == null || value == "null") - { - return new AsyncApiNull(); - } - - if (schema?.Type == null) - { - if (value == "true") - { - return new AsyncApiBoolean(true); - } - - if (value == "false") - { - return new AsyncApiBoolean(false); - } - - if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) - { - return new AsyncApiInteger(intValue); - } - - if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var longValue)) - { - return new AsyncApiLong(longValue); - } - - if (double.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var doubleValue)) - { - return new AsyncApiDouble(doubleValue); - } - - if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateTimeValue)) - { - return new AsyncApiDateTime(dateTimeValue); - } - } - else - { - if (type.Contains(SchemaType.Integer) && format == "int32") - { - if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) - { - return new AsyncApiInteger(intValue); - } - } - - if (type.Contains(SchemaType.Integer) && format == "int64") - { - if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var longValue)) - { - return new AsyncApiLong(longValue); - } - } - - if (type.Contains(SchemaType.Integer)) - { - if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) - { - return new AsyncApiInteger(intValue); - } - } - - if (type.Contains(SchemaType.Number) && format == "float") - { - if (float.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var floatValue)) - { - return new AsyncApiFloat(floatValue); - } - } - - if (type.Contains(SchemaType.Number) && format == "double") - { - if (double.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var doubleValue)) - { - return new AsyncApiDouble(doubleValue); - } - } - - if (type.Contains(SchemaType.Number)) - { - if (double.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var doubleValue)) - { - return new AsyncApiDouble(doubleValue); - } - } - - if (type.Contains(SchemaType.String) && format == "byte") - { - try - { - return new AsyncApiByte(Convert.FromBase64String(value)); - } - catch (FormatException) - { } - } - - // binary - if (type.Contains(SchemaType.String) && format == "binary") - { - try - { - return new AsyncApiBinary(Encoding.UTF8.GetBytes(value)); - } - catch (EncoderFallbackException) - { } - } - - if (type.Contains(SchemaType.String) && format == "date") - { - if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateValue)) - { - return new AsyncApiDate(dateValue.Date); - } - } - - if (type.Contains(SchemaType.String) && format == "date-time") - { - if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateTimeValue)) - { - return new AsyncApiDateTime(dateTimeValue); - } - } - - if (type.Contains(SchemaType.String)) - { - return asyncApiAny; - } - - if (type.Contains(SchemaType.Boolean)) - { - if (bool.TryParse(value, out var booleanValue)) - { - return new AsyncApiBoolean(booleanValue); - } - } - } - - return asyncApiAny; - } - } -} diff --git a/src/LEGO.AsyncAPI.Readers/ParseNodes/FixedFieldMap.cs b/src/LEGO.AsyncAPI.Readers/ParseNodes/FixedFieldMap.cs index efb046dd..316a927a 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/FixedFieldMap.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/FixedFieldMap.cs @@ -5,7 +5,7 @@ namespace LEGO.AsyncAPI.Readers.ParseNodes using System; using System.Collections.Generic; - internal class FixedFieldMap : Dictionary> + public class FixedFieldMap : Dictionary> { } -} \ No newline at end of file +} diff --git a/src/LEGO.AsyncAPI.Readers/ParseNodes/JsonPointerExtensions.cs b/src/LEGO.AsyncAPI.Readers/ParseNodes/JsonPointerExtensions.cs index 2cb6302c..acf5857e 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/JsonPointerExtensions.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/JsonPointerExtensions.cs @@ -3,38 +3,31 @@ namespace LEGO.AsyncAPI.Readers.ParseNodes { using System; - using YamlDotNet.RepresentationModel; + using System.Text.Json.Nodes; public static class JsonPointerExtensions { - public static YamlNode Find(this JsonPointer currentPointer, YamlNode baseYamlNode) + public static JsonNode Find(this JsonPointer currentPointer, JsonNode baseJsonNode) { if (currentPointer.Tokens.Length == 0) { - return baseYamlNode; + return baseJsonNode; } try { - var pointer = baseYamlNode; + var pointer = baseJsonNode; foreach (var token in currentPointer.Tokens) { - var sequence = pointer as YamlSequenceNode; + var sequence = pointer as JsonArray; - if (sequence != null) + if (sequence != null && int.TryParse(token, out var tokenValue)) { - pointer = sequence.Children[Convert.ToInt32(token)]; + pointer = sequence[tokenValue]; } - else + else if (pointer is JsonObject map && !map.TryGetPropertyValue(token, out pointer)) { - var map = pointer as YamlMappingNode; - if (map != null) - { - if (!map.Children.TryGetValue(new YamlScalarNode(token), out pointer)) - { - return null; - } - } + return null; } } diff --git a/src/LEGO.AsyncAPI.Readers/ParseNodes/ListNode.cs b/src/LEGO.AsyncAPI.Readers/ParseNodes/ListNode.cs index c402936e..cce1d406 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/ListNode.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/ListNode.cs @@ -6,16 +6,15 @@ namespace LEGO.AsyncAPI.Readers.ParseNodes using System.Collections; using System.Collections.Generic; using System.Linq; - using LEGO.AsyncAPI.Models.Any; - using LEGO.AsyncAPI.Models.Interfaces; + using System.Text.Json.Nodes; + using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Readers.Exceptions; - using YamlDotNet.RepresentationModel; - internal class ListNode : ParseNode, IEnumerable + public class ListNode : ParseNode, IEnumerable { - private readonly YamlSequenceNode nodeList; + private readonly JsonArray nodeList; - public ListNode(ParsingContext context, YamlSequenceNode sequenceNode) + public ListNode(ParsingContext context, JsonArray sequenceNode) : base( context) { @@ -27,15 +26,15 @@ public override List CreateList(Func map) if (this.nodeList == null) { throw new AsyncApiReaderException( - $"Expected list at line {this.nodeList.Start.Line} while parsing {typeof(T).Name}", this.nodeList); + $"Expected list while parsing {typeof(T).Name}"); } - return this.nodeList.Select(n => map(new MapNode(this.Context, n as YamlMappingNode))) + return this.nodeList.Select(n => map(new MapNode(this.Context, n as JsonObject))) .Where(i => i != null) .ToList(); } - public override List CreateListOfAny() + public override List CreateListOfAny() { return this.nodeList.Select(n => ParseNode.Create(this.Context, n).CreateAny()) .Where(i => i != null) @@ -47,7 +46,7 @@ public override List CreateSimpleList(Func map) if (this.nodeList == null) { throw new AsyncApiReaderException( - $"Expected list at line {this.nodeList.Start.Line} while parsing {typeof(T).Name}", this.nodeList); + $"Expected list while parsing {typeof(T).Name}"); } return this.nodeList.Select(n => map(new ValueNode(this.Context, n))).ToList(); @@ -63,15 +62,9 @@ IEnumerator IEnumerable.GetEnumerator() return this.GetEnumerator(); } - public override IAsyncApiAny CreateAny() + public override AsyncApiAny CreateAny() { - var array = new AsyncApiArray(); - foreach (var node in this) - { - array.Add(node.CreateAny()); - } - - return array; + return new AsyncApiAny(this.nodeList); } } } diff --git a/src/LEGO.AsyncAPI.Readers/ParseNodes/MapNode.cs b/src/LEGO.AsyncAPI.Readers/ParseNodes/MapNode.cs index 41da8d03..b087c875 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/MapNode.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/MapNode.cs @@ -6,37 +6,36 @@ namespace LEGO.AsyncAPI.Readers.ParseNodes using System.Collections; using System.Collections.Generic; using System.Linq; + using System.Text.Json; + using System.Text.Json.Nodes; + using LEGO.AsyncAPI.Exceptions; using LEGO.AsyncAPI.Models; - using LEGO.AsyncAPI.Models.Any; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Readers.Exceptions; - using YamlDotNet.RepresentationModel; - using YamlDotNet.Serialization; - internal class MapNode : ParseNode, IEnumerable + public class MapNode : ParseNode, IEnumerable { - private readonly YamlMappingNode node; + private readonly JsonObject node; private readonly List nodes; - public MapNode(ParsingContext context, string yamlString) - : this(context, (YamlMappingNode)YamlHelper.ParseYamlString(yamlString)) + public MapNode(ParsingContext context, string jsonString) + : this(context, JsonNode.Parse(jsonString)) { } - public MapNode(ParsingContext context, YamlNode node) + public MapNode(ParsingContext context, JsonNode node) : base( context) { - if (!(node is YamlMappingNode mapNode)) + if (!(node is JsonObject mapNode)) { throw new AsyncApiReaderException("Expected map.", this.Context); } this.node = mapNode; - this.nodes = this.node.Children - .Select(kvp => new PropertyNode(this.Context, kvp.Key.GetScalarValue(), kvp.Value)) - .Cast() + this.nodes = this.node + .Select(node => new PropertyNode(this.Context, node.Key, node.Value)) .ToList(); } @@ -44,8 +43,7 @@ public PropertyNode this[string key] { get { - YamlNode node; - if (this.node.Children.TryGetValue(new YamlScalarNode(key), out node)) + if (this.node.TryGetPropertyValue(key, out var node)) { return new PropertyNode(this.Context, key, node); } @@ -56,23 +54,23 @@ public PropertyNode this[string key] public override Dictionary CreateMap(Func map) { - var yamlMap = this.node; - if (yamlMap == null) + var jsonMap = this.node; + if (jsonMap == null) { throw new AsyncApiReaderException($"Expected map while parsing {typeof(T).Name}", this.Context); } - var nodes = yamlMap.Select( + var nodes = jsonMap.Select( n => { - var key = n.Key.GetScalarValue(); + var key = n.Key; T value; try { this.Context.StartObject(key); - value = n.Value as YamlMappingNode == null - ? default(T) - : map(new MapNode(this.Context, n.Value as YamlMappingNode)); + value = n.Value is JsonObject + ? map(new MapNode(this.Context, n.Value)) + : default(T); } finally { @@ -81,83 +79,35 @@ public override Dictionary CreateMap(Func map) return new { - key = key, - value = value, + key, + value, }; }); return nodes.ToDictionary(k => k.key, v => v.value); } - public override Dictionary CreateBindingMapWithReference( - ReferenceType referenceType, - Func map) - { - var yamlMap = this.node; - if (yamlMap == null) - { - throw new AsyncApiReaderException($"Expected map while parsing {typeof(T).Name}", this.Context); - } - - var nodes = yamlMap.Select( - n => - { - var key = n.Key.GetScalarValue(); - (string key, T value) entry; - try - { - this.Context.StartObject(key); - entry = ( - key: key, - value: map(new PropertyNode(this.Context, key, n.Value)) - ); - - if (entry.value == null) - { - return default; - } - - if (entry.value.Reference == null) - { - entry.value.Reference = new AsyncApiReference() - { - Type = referenceType, - Id = entry.key, - }; - } - } - finally - { - this.Context.EndObject(); - } - - return entry; - } - ); - return nodes.Where(n => n != default).ToDictionary(k => k.key, v => v.value); - } - public override Dictionary CreateMapWithReference( ReferenceType referenceType, Func map) { - var yamlMap = this.node; - if (yamlMap == null) + var jsonMap = this.node; + if (jsonMap == null) { throw new AsyncApiReaderException($"Expected map while parsing {typeof(T).Name}", this.Context); } - var nodes = yamlMap.Select( + var nodes = jsonMap.Select( n => { - var key = n.Key.GetScalarValue(); + var key = n.Key; (string key, T value) entry; try { this.Context.StartObject(key); entry = ( - key: key, - value: map(new MapNode(this.Context, (YamlMappingNode)n.Value)) + key, + value: map(new MapNode(this.Context, n.Value)) ); if (entry.value == null) { @@ -186,26 +136,26 @@ public override Dictionary CreateMapWithReference( public override Dictionary CreateSimpleMap(Func map) { - var yamlMap = this.node; - if (yamlMap == null) + var jsonMap = this.node; + if (jsonMap == null) { throw new AsyncApiReaderException($"Expected map while parsing {typeof(T).Name}", this.Context); } - var nodes = yamlMap.Select( + var nodes = jsonMap.Select( n => { - var key = n.Key.GetScalarValue(); + var key = n.Key; try { this.Context.StartObject(key); - YamlScalarNode scalarNode = n.Value as YamlScalarNode; + JsonValue scalarNode = n.Value as JsonValue; if (scalarNode == null) { throw new AsyncApiReaderException($"Expected scalar while parsing {typeof(T).Name}", this.Context); } - return (key, value: map(new ValueNode(this.Context, (YamlScalarNode)n.Value))); + return (key, value: map(new ValueNode(this.Context, n.Value))); } finally { @@ -227,8 +177,7 @@ IEnumerator IEnumerable.GetEnumerator() public override string GetRaw() { - var x = new SerializerBuilder().JsonCompatible().Build(); - return x.Serialize(this.node); + return JsonSerializer.Serialize(this.node); } public T GetReferencedObject(ReferenceType referenceType, string referenceId) @@ -243,36 +192,32 @@ public T GetReferencedObject(ReferenceType referenceType, string referenceId) public string GetReferencePointer() { - YamlNode refNode; - - if (!this.node.Children.TryGetValue(new YamlScalarNode("$ref"), out refNode)) + if (!this.node.TryGetPropertyValue("$ref", out JsonNode refNode)) { return null; } - return refNode.GetScalarValue(); + return this.ToScalarValue(refNode); } public string GetScalarValue(ValueNode key) { - var scalarNode = this.node.Children[new YamlScalarNode(key.GetScalarValue())] as YamlScalarNode; - if (scalarNode == null) - { - throw new AsyncApiReaderException($"Expected scalar at line {this.node.Start.Line} for key {key.GetScalarValue()}", this.Context); - } + var scalarNode = this.node[key.GetScalarValue()] is JsonValue jsonValue + ? jsonValue + : throw new AsyncApiReaderException($"Expected scalar value while parsing {key.GetScalarValue()}", this.Context); - return scalarNode.Value; + return this.ToScalarValue(scalarNode); } - public override IAsyncApiAny CreateAny() + public override AsyncApiAny CreateAny() { - var apiObject = new AsyncApiObject(); - foreach (var node in this) - { - apiObject.Add(node.Name, node.Value.CreateAny()); - } + return new AsyncApiAny(this.node); + } - return apiObject; + private string ToScalarValue(JsonNode node) + { + var scalarNode = node is JsonValue value ? value : throw new AsyncApiException($"Expected scalar value"); + return Convert.ToString(scalarNode.GetValue(), this.Context.Settings.CultureInfo); } } } diff --git a/src/LEGO.AsyncAPI.Readers/ParseNodes/ParseNode.cs b/src/LEGO.AsyncAPI.Readers/ParseNodes/ParseNode.cs index aa106cd2..da3e8a1a 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/ParseNode.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/ParseNode.cs @@ -4,12 +4,12 @@ namespace LEGO.AsyncAPI.Readers.ParseNodes { using System; using System.Collections.Generic; + using System.Text.Json.Nodes; using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Readers.Exceptions; - using YamlDotNet.RepresentationModel; - internal abstract class ParseNode + public abstract class ParseNode { protected ParseNode(ParsingContext parsingContext) { @@ -28,19 +28,19 @@ public MapNode CheckMapNode(string nodeName) return mapNode; } - public static ParseNode Create(ParsingContext context, YamlNode node) + public static ParseNode Create(ParsingContext context, JsonNode node) { - if (node is YamlSequenceNode listNode) + if (node is JsonArray listNode) { return new ListNode(context, listNode); } - if (node is YamlMappingNode mapNode) + if (node is JsonObject mapNode) { return new MapNode(context, mapNode); } - return new ValueNode(context, node as YamlScalarNode); + return new ValueNode(context, node as JsonValue); } public virtual List CreateList(Func map) @@ -79,7 +79,7 @@ public virtual Dictionary CreateSimpleMap(Func map) throw new AsyncApiReaderException("Cannot create simple map from this type of node.", this.Context); } - public virtual IAsyncApiAny CreateAny() + public virtual AsyncApiAny CreateAny() { throw new AsyncApiReaderException("Cannot create an Any object this type of node.", this.Context); } @@ -94,7 +94,7 @@ public virtual string GetScalarValue() throw new AsyncApiReaderException("Cannot create a scalar value from this type of node.", this.Context); } - public virtual string GetScalarValueOrDefault(string defaultValue) + public virtual string GetScalarValueOrDefault(string defaultValue = null) { throw new AsyncApiReaderException("Cannot create a scalar value from this type of node.", this.Context); } @@ -104,7 +104,7 @@ public virtual bool GetBooleanValue() throw new AsyncApiReaderException("Cannot create a scalar value from this type of node.", this.Context); } - public virtual bool? GetBooleanValueOrDefault(bool? defaultValue) + public virtual bool? GetBooleanValueOrDefault(bool? defaultValue = null) { throw new AsyncApiReaderException("Cannot create a scalar value from this type of node.", this.Context); } @@ -114,7 +114,7 @@ public virtual int GetIntegerValue() throw new AsyncApiReaderException("Cannot create a scalar value from this type of node.", this.Context); } - public virtual int? GetIntegerValueOrDefault(int? defaultValue) + public virtual int? GetIntegerValueOrDefault(int? defaultValue = null) { throw new AsyncApiReaderException("Cannot create a scalar value from this type of node.", this.Context); } @@ -124,12 +124,12 @@ public virtual long GetLongValue() throw new AsyncApiReaderException("Cannot create a scalar value from this type of node.", this.Context); } - public virtual long? GetLongValueOrDefault(long? defaultValue) + public virtual long? GetLongValueOrDefault(long? defaultValue = null) { throw new AsyncApiReaderException("Cannot create a scalar value from this type of node.", this.Context); } - public virtual List CreateListOfAny() + public virtual List CreateListOfAny() { throw new AsyncApiReaderException("Cannot create a list from this type of node.", this.Context); } diff --git a/src/LEGO.AsyncAPI.Readers/ParseNodes/PatternFieldMap.cs b/src/LEGO.AsyncAPI.Readers/ParseNodes/PatternFieldMap.cs index 0c1112ad..040a79e7 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/PatternFieldMap.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/PatternFieldMap.cs @@ -5,7 +5,7 @@ namespace LEGO.AsyncAPI.Readers.ParseNodes using System; using System.Collections.Generic; - internal class PatternFieldMap : Dictionary, Action> + public class PatternFieldMap : Dictionary, Action> { } } \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Readers/ParseNodes/PropertyNode.cs b/src/LEGO.AsyncAPI.Readers/ParseNodes/PropertyNode.cs index 99e1a33e..e0359659 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/PropertyNode.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/PropertyNode.cs @@ -5,15 +5,14 @@ namespace LEGO.AsyncAPI.Readers.ParseNodes using System; using System.Collections.Generic; using System.Linq; + using System.Text.Json.Nodes; using LEGO.AsyncAPI.Exceptions; using LEGO.AsyncAPI.Models; - using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Readers.Exceptions; - using YamlDotNet.RepresentationModel; - internal class PropertyNode : ParseNode + public class PropertyNode : ParseNode { - public PropertyNode(ParsingContext context, string name, YamlNode node) + public PropertyNode(ParsingContext context, string name, JsonNode node) : base( context) { @@ -84,10 +83,5 @@ public void ParseField( } } } - - public override IAsyncApiAny CreateAny() - { - throw new NotImplementedException(); - } } } diff --git a/src/LEGO.AsyncAPI.Readers/ParseNodes/RootNode.cs b/src/LEGO.AsyncAPI.Readers/ParseNodes/RootNode.cs index a5034d2d..82e39349 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/RootNode.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/RootNode.cs @@ -2,34 +2,33 @@ namespace LEGO.AsyncAPI.Readers.ParseNodes { - using YamlDotNet.RepresentationModel; + using System.Text.Json.Nodes; internal class RootNode : ParseNode { - private readonly YamlDocument yamlDocument; + private readonly JsonNode jsonNode; public RootNode( ParsingContext context, - YamlDocument yamlDocument) + JsonNode jsonNode) : base(context) { - this.yamlDocument = yamlDocument; + this.jsonNode = jsonNode; } public ParseNode Find(JsonPointer referencePointer) { - var yamlNode = referencePointer.Find(this.yamlDocument.RootNode); - if (yamlNode == null) + if (referencePointer.Find(this.jsonNode) is not JsonNode jsonNode) { return null; } - return Create(this.Context, yamlNode); + return Create(this.Context, jsonNode); } public MapNode GetMap() { - return new MapNode(this.Context, (YamlMappingNode)this.yamlDocument.RootNode); + return new MapNode(this.Context, this.jsonNode); } } } \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Readers/ParseNodes/ValueNode.cs b/src/LEGO.AsyncAPI.Readers/ParseNodes/ValueNode.cs index 63e0238e..201ab6f7 100644 --- a/src/LEGO.AsyncAPI.Readers/ParseNodes/ValueNode.cs +++ b/src/LEGO.AsyncAPI.Readers/ParseNodes/ValueNode.cs @@ -2,23 +2,23 @@ namespace LEGO.AsyncAPI.Readers.ParseNodes { - using LEGO.AsyncAPI.Models.Any; - using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Exceptions; + using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Readers.Exceptions; - using YamlDotNet.Core; - using YamlDotNet.RepresentationModel; + using System; + using System.Text.Json.Nodes; - internal class ValueNode : ParseNode + public class ValueNode : ParseNode { - private readonly YamlScalarNode node; - - public ValueNode(ParsingContext context, YamlNode node) + private readonly JsonNode node; + private string cachedScalarValue; + public ValueNode(ParsingContext context, JsonNode node) : base( context) { - if (!(node is YamlScalarNode scalarNode)) + if (!(node is JsonValue scalarNode)) { - throw new AsyncApiReaderException("Expected a value.", node); + throw new AsyncApiReaderException("Expected a value."); } this.node = scalarNode; @@ -26,14 +26,22 @@ public ValueNode(ParsingContext context, YamlNode node) public override string GetScalarValue() { - return this.node.Value; + if (this.cachedScalarValue == null) + { + // TODO: Update this property to use the .ToString() or JsonReader. + var scalarNode = this.node is JsonValue value ? value : throw new AsyncApiException($"Expected scalar value"); + this.cachedScalarValue = Convert.ToString(scalarNode.GetValue(), this.Context.Settings.CultureInfo); + } + + return this.cachedScalarValue; } - public override string GetScalarValueOrDefault(string defaultValue) + public override string GetScalarValueOrDefault(string defaultValue = null) { - if (this.node.Value is not null) + var value = this.GetScalarValue(); + if (value is not null) { - return this.node.Value; + return value; } return defaultValue; @@ -41,17 +49,17 @@ public override string GetScalarValueOrDefault(string defaultValue) public override int GetIntegerValue() { - if (int.TryParse(this.node.Value, out int value)) + if (int.TryParse(this.GetScalarValue(), out int value)) { return value; } - throw new AsyncApiReaderException("Value could not parse to integer", this.node); + throw new AsyncApiReaderException("Value could not parse to integer."); } - public override int? GetIntegerValueOrDefault(int? defaultValue) + public override int? GetIntegerValueOrDefault(int? defaultValue = null) { - if (int.TryParse(this.node.Value, out int value)) + if (int.TryParse(this.GetScalarValue(), out int value)) { return value; } @@ -61,17 +69,17 @@ public override int GetIntegerValue() public override long GetLongValue() { - if (long.TryParse(this.node.Value, out long value)) + if (long.TryParse(this.GetScalarValue(), out long value)) { return value; } - throw new AsyncApiReaderException("Value could not parse to long", this.node); + throw new AsyncApiReaderException("Value could not parse to long."); } - public override long? GetLongValueOrDefault(long? defaultValue) + public override long? GetLongValueOrDefault(long? defaultValue = null) { - if (long.TryParse(this.node.Value, out long value)) + if (long.TryParse(this.GetScalarValue(), out long value)) { return value; } @@ -81,17 +89,17 @@ public override long GetLongValue() public override bool GetBooleanValue() { - if (bool.TryParse(this.node.Value, out bool value)) + if (bool.TryParse(this.GetScalarValue(), out bool value)) { return value; } - throw new AsyncApiReaderException("Value could not parse to bool", this.node); + throw new AsyncApiReaderException("Value could not parse to bool."); } - public override bool? GetBooleanValueOrDefault(bool? defaultValue) + public override bool? GetBooleanValueOrDefault(bool? defaultValue = null) { - if (bool.TryParse(this.node.Value, out bool value)) + if (bool.TryParse(this.GetScalarValue(), out bool value)) { return value; } @@ -99,10 +107,10 @@ public override bool GetBooleanValue() return defaultValue; } - public override IAsyncApiAny CreateAny() + public override AsyncApiAny CreateAny() { var value = this.GetScalarValue(); - return new AsyncApiString(value, this.node.Style == ScalarStyle.SingleQuoted || this.node.Style == ScalarStyle.DoubleQuoted || this.node.Style == ScalarStyle.Literal || this.node.Style == ScalarStyle.Folded); + return new AsyncApiAny(this.node); } } } \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Readers/ParsingContext.cs b/src/LEGO.AsyncAPI.Readers/ParsingContext.cs index 63e8d1a6..9bcc051b 100644 --- a/src/LEGO.AsyncAPI.Readers/ParsingContext.cs +++ b/src/LEGO.AsyncAPI.Readers/ParsingContext.cs @@ -5,40 +5,69 @@ namespace LEGO.AsyncAPI.Readers using System; using System.Collections.Generic; using System.Linq; + using System.Text.Json.Nodes; using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Readers.Exceptions; using LEGO.AsyncAPI.Readers.Interface; using LEGO.AsyncAPI.Readers.ParseNodes; using LEGO.AsyncAPI.Readers.V2; - using YamlDotNet.RepresentationModel; public class ParsingContext { - private readonly Stack currentLocation = new (); + private readonly Stack currentLocation = new(); - internal Dictionary> ExtensionParsers + internal Dictionary> ExtensionParsers { get; set; } - = new (); + = new(); + + internal Dictionary> ServerBindingParsers { get; set; } = new(); + + internal Dictionary> ChannelBindingParsers { get; set; } = new(); + + internal Dictionary> OperationBindingParsers { get; set; } = new(); + + internal Dictionary> MessageBindingParsers { get; set; } = new(); internal RootNode RootNode { get; set; } - internal List Tags { get; private set; } = new (); + internal List Tags { get; private set; } = new(); public AsyncApiDiagnostic Diagnostic { get; } + /// + /// Gets the settings used fore reading json. + /// + public AsyncApiReaderSettings Settings { get; } + + ///// + ///// Initializes a new instance of the class. + ///// + /// The diagnostics. + [Obsolete($"Please use the overloaded version that takes in an instance of {nameof(AsyncApiReaderSettings)} isntead.")] public ParsingContext(AsyncApiDiagnostic diagnostic) + : this(diagnostic, new AsyncApiReaderSettings()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The diagnostics. + /// The settings used to read json. + public ParsingContext(AsyncApiDiagnostic diagnostic, AsyncApiReaderSettings settings) { this.Diagnostic = diagnostic; + this.Settings = settings; } - internal AsyncApiDocument Parse(YamlDocument yamlDocument) + internal AsyncApiDocument Parse(JsonNode jsonNode) { - this.RootNode = new RootNode(this, yamlDocument); + this.RootNode = new RootNode(this, jsonNode); var inputVersion = GetVersion(this.RootNode); @@ -59,9 +88,10 @@ internal AsyncApiDocument Parse(YamlDocument yamlDocument) return doc; } - internal T ParseFragment(YamlDocument yamlDocument, AsyncApiVersion version) where T : IAsyncApiElement + internal T ParseFragment(JsonNode jsonNode, AsyncApiVersion version) + where T : IAsyncApiElement { - var node = ParseNode.Create(this, yamlDocument.RootNode); + var node = ParseNode.Create(this, jsonNode); T element = default(T); @@ -79,13 +109,7 @@ internal T ParseFragment(YamlDocument yamlDocument, AsyncApiVersion version) private static string GetVersion(RootNode rootNode) { var versionNode = rootNode.Find(new JsonPointer("/asyncapi")); - - if (versionNode != null) - { - return versionNode.GetScalarValue(); - } - - return versionNode?.GetScalarValue(); + return versionNode?.GetScalarValue().Replace("\"", string.Empty); } internal IAsyncApiVersionService VersionService { get; set; } @@ -97,7 +121,8 @@ public void EndObject() public string GetLocation() { - return "#/" + string.Join("/", + return "#/" + string.Join( + "/", this.currentLocation.Reverse().Select(s => s.Replace("~", "~0").Replace("/", "~1")).ToArray()); } diff --git a/src/LEGO.AsyncAPI.Readers/Services/DefaultStreamLoader.cs b/src/LEGO.AsyncAPI.Readers/Services/DefaultStreamLoader.cs index acf94265..6e506730 100644 --- a/src/LEGO.AsyncAPI.Readers/Services/DefaultStreamLoader.cs +++ b/src/LEGO.AsyncAPI.Readers/Services/DefaultStreamLoader.cs @@ -13,7 +13,6 @@ internal class DefaultStreamLoader : IStreamLoader private readonly Uri baseUrl; private HttpClient httpClient = new HttpClient(); - public DefaultStreamLoader(Uri baseUrl) { this.baseUrl = baseUrl; diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiBindingDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiBindingDeserializer.cs deleted file mode 100644 index 7c997dc0..00000000 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiBindingDeserializer.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Readers -{ - using LEGO.AsyncAPI.Extensions; - using LEGO.AsyncAPI.Models; - using LEGO.AsyncAPI.Models.Interfaces; - using LEGO.AsyncAPI.Readers.ParseNodes; - using System; - - internal static partial class AsyncApiV2Deserializer - { - private static Type messageBindingType = typeof(IMessageBinding); - private static Type operationBindingType = typeof(IOperationBinding); - private static Type channelBindingType = typeof(IChannelBinding); - private static Type serverBindingType = typeof(IServerBinding); - - private static PatternFieldMap BindingPatternExtensionFields() - where T : IBinding, new() - { - return new() - { - { s => s.StartsWith("x-"), (a, p, n) => a.AddExtension(p, LoadExtension(p, n)) }, - }; - } - - internal static T LoadBinding(string nodeName, ParseNode node, FixedFieldMap fieldMap) - where T : IBinding, new() - { - var mapNode = node.CheckMapNode(nodeName); - var pointer = mapNode.GetReferencePointer(); - if (pointer != null) - { - ReferenceType referenceType = ReferenceType.None; - var bindingType = typeof(T); - - if (bindingType.IsAssignableTo(messageBindingType)) - { - referenceType = ReferenceType.MessageBinding; - } - - if (bindingType.IsAssignableTo(operationBindingType)) - { - referenceType = ReferenceType.OperationBinding; - } - - if (bindingType.IsAssignableTo(channelBindingType)) - { - referenceType = ReferenceType.ChannelBinding; - } - - if (bindingType.IsAssignableTo(serverBindingType)) - { - referenceType = ReferenceType.ServerBinding; - } - - if (referenceType == ReferenceType.None) - { - throw new ArgumentException($"ReferenceType not found {typeof(T).Name}"); - } - - return mapNode.GetReferencedObject(referenceType, pointer); - } - - var binding = new T(); - - ParseMap(mapNode, binding, fieldMap, BindingPatternExtensionFields()); - - return binding; - } - } -} diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiChannelBindingDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiChannelBindingDeserializer.cs index c6e56700..1aab9e85 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiChannelBindingDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiChannelBindingDeserializer.cs @@ -4,19 +4,21 @@ namespace LEGO.AsyncAPI.Readers { using LEGO.AsyncAPI.Exceptions; using LEGO.AsyncAPI.Models; - using LEGO.AsyncAPI.Models.Bindings; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Readers.ParseNodes; - using LEGO.AsyncAPI.Writers; internal static partial class AsyncApiV2Deserializer { internal static AsyncApiBindings LoadChannelBindings(ParseNode node) { - var mapNode = node.CheckMapNode("channelBinding"); + var mapNode = node.CheckMapNode("channelBindings"); + var pointer = mapNode.GetReferencePointer(); + if (pointer != null) + { + return mapNode.GetReferencedObject>(ReferenceType.ChannelBindings, pointer); + } var channelBindings = new AsyncApiBindings(); - foreach (var property in mapNode) { var channelBinding = LoadChannelBinding(property); @@ -35,21 +37,23 @@ internal static AsyncApiBindings LoadChannelBindings(ParseNode return channelBindings; } - internal static IChannelBinding LoadChannelBinding(ParseNode node) + private static IChannelBinding LoadChannelBinding(ParseNode node) { var property = node as PropertyNode; - var bindingType = property.Name.GetEnumFromDisplayName(); - switch (bindingType) + try + { + if (node.Context.ChannelBindingParsers.TryGetValue(property.Name, out var parser)) + { + return parser.LoadBinding(property); + } + } + catch (AsyncApiException ex) { - case BindingType.Kafka: - return LoadBinding("ChannelBinding", property.Value, kafkaChannelBindingFixedFields); - case BindingType.Pulsar: - return LoadBinding("ChannelBinding", property.Value, pulsarChannelBindingFixedFields); - case BindingType.Websockets: - return LoadBinding("ChannelBinding", property.Value, webSocketsChannelBindingFixedFields); - default: - throw new AsyncApiException($"ChannelBinding {property.Name} is not supported"); + ex.Pointer = node.Context.GetLocation(); + node.Context.Diagnostic.Errors.Add(new AsyncApiError(ex)); } + + return null; } } } diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiChannelDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiChannelDeserializer.cs index 8211da1d..1d6acae6 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiChannelDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiChannelDeserializer.cs @@ -8,18 +8,18 @@ namespace LEGO.AsyncAPI.Readers internal static partial class AsyncApiV2Deserializer { - private static readonly FixedFieldMap ChannelFixedFields = new () + private static readonly FixedFieldMap ChannelFixedFields = new() { { "description", (a, n) => { a.Description = n.GetScalarValue(); } }, { "servers", (a, n) => { a.Servers = n.CreateSimpleList(s => s.GetScalarValue()); } }, { "subscribe", (a, n) => { a.Subscribe = LoadOperation(n); } }, { "publish", (a, n) => { a.Publish = LoadOperation(n); } }, - { "parameters", (a, n) => { a.Parameters = n.CreateMapWithReference(ReferenceType.Parameter, LoadParameter); } }, + { "parameters", (a, n) => { a.Parameters = n.CreateMap(LoadParameter); } }, { "bindings", (a, n) => { a.Bindings = LoadChannelBindings(n); } }, }; private static readonly PatternFieldMap ChannelPatternFields = - new () + new() { { s => s.StartsWith("x-"), (a, p, n) => a.AddExtension(p, LoadExtension(p, n)) }, }; diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiComponentsDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiComponentsDeserializer.cs index fa475105..3b63db28 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiComponentsDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiComponentsDeserializer.cs @@ -10,7 +10,7 @@ internal static partial class AsyncApiV2Deserializer { private static FixedFieldMap componentsFixedFields = new() { - { "schemas", (a, n) => a.Schemas = n.CreateMapWithReference(ReferenceType.Schema, LoadSchema) }, + { "schemas", (a, n) => a.Schemas = n.CreateMapWithReference(ReferenceType.Schema, JsonSchemaDeserializer.LoadSchema) }, { "servers", (a, n) => a.Servers = n.CreateMapWithReference(ReferenceType.Server, LoadServer) }, { "channels", (a, n) => a.Channels = n.CreateMapWithReference(ReferenceType.Channel, LoadChannel) }, { "messages", (a, n) => a.Messages = n.CreateMapWithReference(ReferenceType.Message, LoadMessage) }, @@ -19,10 +19,10 @@ internal static partial class AsyncApiV2Deserializer { "correlationIds", (a, n) => a.CorrelationIds = n.CreateMapWithReference(ReferenceType.CorrelationId, LoadCorrelationId) }, { "operationTraits", (a, n) => a.OperationTraits = n.CreateMapWithReference(ReferenceType.OperationTrait, LoadOperationTrait) }, { "messageTraits", (a, n) => a.MessageTraits = n.CreateMapWithReference(ReferenceType.MessageTrait, LoadMessageTrait) }, - { "serverBindings", (a, n) => a.ServerBindings = n.CreateMapWithReference(ReferenceType.ServerBinding, LoadServerBinding) }, - { "channelBindings", (a, n) => a.ChannelBindings = n.CreateMapWithReference(ReferenceType.ChannelBinding, LoadChannelBinding) }, - { "operationBindings", (a, n) => a.OperationBindings = n.CreateBindingMapWithReference(ReferenceType.OperationBinding, LoadOperationBinding) }, - { "messageBindings", (a, n) => a.MessageBindings = n.CreateMapWithReference(ReferenceType.MessageBinding, LoadMessageBinding) }, + { "serverBindings", (a, n) => a.ServerBindings = n.CreateMapWithReference(ReferenceType.ServerBindings, LoadServerBindings) }, + { "channelBindings", (a, n) => a.ChannelBindings = n.CreateMapWithReference(ReferenceType.ChannelBindings, LoadChannelBindings) }, + { "operationBindings", (a, n) => a.OperationBindings = n.CreateMapWithReference(ReferenceType.OperationBindings, LoadOperationBindings) }, + { "messageBindings", (a, n) => a.MessageBindings = n.CreateMapWithReference(ReferenceType.MessageBindings, LoadMessageBindings) }, }; private static PatternFieldMap componentsPatternFields = diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiDeserializer.cs index 64be8b77..fcb0d91e 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiDeserializer.cs @@ -5,14 +5,13 @@ namespace LEGO.AsyncAPI.Readers using System.Collections.Generic; using System.Linq; using LEGO.AsyncAPI.Exceptions; - using LEGO.AsyncAPI.Expressions; using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Readers.ParseNodes; internal static partial class AsyncApiV2Deserializer { - private static void ParseMap( + internal static void ParseMap( MapNode mapNode, T domainObject, FixedFieldMap fixedFieldMap, @@ -29,7 +28,7 @@ private static void ParseMap( } } - private static void ProcessAnyFields( + internal static void ProcessAnyFields( MapNode mapNode, T domainObject, AnyFieldMap anyFieldMap) @@ -40,11 +39,15 @@ private static void ProcessAnyFields( { mapNode.Context.StartObject(anyFieldName); - var convertedAsyncApiAny = AsyncApiAnyConverter.GetSpecificAsyncApiAny( - anyFieldMap[anyFieldName].PropertyGetter(domainObject), - anyFieldMap[anyFieldName].SchemaGetter(domainObject)); - - anyFieldMap[anyFieldName].PropertySetter(domainObject, convertedAsyncApiAny); + var anyFieldValue = anyFieldMap[anyFieldName].PropertyGetter(domainObject); + if (anyFieldValue == null) + { + anyFieldMap[anyFieldName].PropertySetter(domainObject, null); + } + else + { + anyFieldMap[anyFieldName].PropertySetter(domainObject, anyFieldValue); + } } catch (AsyncApiException exception) { @@ -58,7 +61,7 @@ private static void ProcessAnyFields( } } - private static void ProcessAnyListFields( + internal static void ProcessAnyListFields( MapNode mapNode, T domainObject, AnyListFieldMap anyListFieldMap) @@ -67,16 +70,13 @@ private static void ProcessAnyListFields( { try { - var newProperty = new List(); + var newProperty = new List(); mapNode.Context.StartObject(anyListFieldName); foreach (var propertyElement in anyListFieldMap[anyListFieldName].PropertyGetter(domainObject)) { - newProperty.Add( - AsyncApiAnyConverter.GetSpecificAsyncApiAny( - propertyElement, - anyListFieldMap[anyListFieldName].SchemaGetter(domainObject))); + newProperty.Add(propertyElement); } anyListFieldMap[anyListFieldName].PropertySetter(domainObject, newProperty); @@ -102,7 +102,7 @@ private static void ProcessAnyMapFields( { try { - var newProperty = new List(); + var newProperty = new List(); mapNode.Context.StartObject(anyMapFieldName); @@ -114,11 +114,7 @@ private static void ProcessAnyMapFields( { var any = anyMapFieldMap[anyMapFieldName].PropertyGetter(propertyMapElement.Value); - var newAny = AsyncApiAnyConverter.GetSpecificAsyncApiAny( - any, - anyMapFieldMap[anyMapFieldName].SchemaGetter(domainObject)); - - anyMapFieldMap[anyMapFieldName].PropertySetter(propertyMapElement.Value, newAny); + anyMapFieldMap[anyMapFieldName].PropertySetter(propertyMapElement.Value, any); } } } @@ -134,46 +130,27 @@ private static void ProcessAnyMapFields( } } - private static RuntimeExpression LoadRuntimeExpression(ParseNode node) + public static AsyncApiAny LoadAny(ParseNode node) { - var value = node.GetScalarValue(); - return RuntimeExpression.Build(value); + return node.CreateAny(); } - private static RuntimeExpressionAnyWrapper LoadRuntimeExpressionAnyWrapper(ParseNode node) + public static IAsyncApiExtension LoadExtension(string name, ParseNode node) { - var value = node.GetScalarValue(); - - if (value != null && value.StartsWith("$")) + try { - return new RuntimeExpressionAnyWrapper + if (node.Context.ExtensionParsers.TryGetValue(name, out var parser)) { - Expression = RuntimeExpression.Build(value), - }; - } - - return new RuntimeExpressionAnyWrapper - { - Any = AsyncApiAnyConverter.GetSpecificAsyncApiAny(node.CreateAny()), - }; - } - - public static IAsyncApiAny LoadAny(ParseNode node) - { - return AsyncApiAnyConverter.GetSpecificAsyncApiAny(node.CreateAny()); - } - - private static IAsyncApiExtension LoadExtension(string name, ParseNode node) - { - if (node.Context.ExtensionParsers.TryGetValue(name, out var parser)) - { - return parser( - AsyncApiAnyConverter.GetSpecificAsyncApiAny(node.CreateAny())); + return parser(node.CreateAny()); + } } - else + catch (AsyncApiException ex) { - return AsyncApiAnyConverter.GetSpecificAsyncApiAny(node.CreateAny()); + ex.Pointer = node.Context.GetLocation(); + node.Context.Diagnostic.Errors.Add(new AsyncApiError(ex)); } + + return node.CreateAny(); } private static string LoadString(ParseNode node) diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageBindingDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageBindingDeserializer.cs index c3198737..9a180e84 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageBindingDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageBindingDeserializer.cs @@ -4,12 +4,8 @@ namespace LEGO.AsyncAPI.Readers { using LEGO.AsyncAPI.Exceptions; using LEGO.AsyncAPI.Models; - using LEGO.AsyncAPI.Models.Bindings; - using LEGO.AsyncAPI.Models.Bindings.Http; - using LEGO.AsyncAPI.Models.Bindings.Kafka; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Readers.ParseNodes; - using LEGO.AsyncAPI.Writers; internal static partial class AsyncApiV2Deserializer { @@ -40,16 +36,20 @@ internal static AsyncApiBindings LoadMessageBindings(ParseNode internal static IMessageBinding LoadMessageBinding(ParseNode node) { var property = node as PropertyNode; - var bindingType = property.Name.GetEnumFromDisplayName(); - switch (bindingType) + try { - case BindingType.Kafka: - return LoadBinding("MessageBinding", property.Value, kafkaMessageBindingFixedFields); - case BindingType.Http: - return LoadBinding("MessageBinding", property.Value, httpMessageBindingFixedFields); - default: - throw new AsyncApiException($"MessageBinding {property.Name} is not supported"); + if (node.Context.MessageBindingParsers.TryGetValue(property.Name, out var parser)) + { + return parser.LoadBinding(property); + } } + catch (AsyncApiException ex) + { + ex.Pointer = node.Context.GetLocation(); + node.Context.Diagnostic.Errors.Add(new AsyncApiError(ex)); + } + + return null; } } } diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageDeserializer.cs index 97e40ba7..4c16bf22 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageDeserializer.cs @@ -21,10 +21,10 @@ internal static partial class AsyncApiV2Deserializer "messageId", (a, n) => { a.MessageId = n.GetScalarValue(); } }, { - "headers", (a, n) => { a.Headers = LoadSchema(n); } + "headers", (a, n) => { a.Headers = JsonSchemaDeserializer.LoadSchema(n); } }, { - "payload", (a, n) => { a.Payload = LoadSchema(n); } + "payload", (a, n) => { a.Payload = JsonSchemaDeserializer.LoadSchema(n); } }, { "correlationId", (a, n) => { a.CorrelationId = LoadCorrelationId(n); } diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageTraitDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageTraitDeserializer.cs index 7e142ee2..eca8af64 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageTraitDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiMessageTraitDeserializer.cs @@ -11,7 +11,7 @@ internal static partial class AsyncApiV2Deserializer private static FixedFieldMap messageTraitFixedFields = new() { { "messageId", (a, n) => { a.MessageId = n.GetScalarValue(); } }, - { "headers", (a, n) => { a.Headers = LoadSchema(n); } }, + { "headers", (a, n) => { a.Headers = JsonSchemaDeserializer.LoadSchema(n); } }, { "correlationId", (a, n) => { a.CorrelationId = LoadCorrelationId(n); } }, { "schemaFormat", (a, n) => { a.SchemaFormat = n.GetScalarValue(); } }, { "contentType", (a, n) => { a.ContentType = n.GetScalarValue(); } }, diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiOperationBindingDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiOperationBindingDeserializer.cs index 03631600..4d4eb85f 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiOperationBindingDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiOperationBindingDeserializer.cs @@ -4,16 +4,14 @@ namespace LEGO.AsyncAPI.Readers { using LEGO.AsyncAPI.Exceptions; using LEGO.AsyncAPI.Models; - using LEGO.AsyncAPI.Models.Bindings; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Readers.ParseNodes; - using LEGO.AsyncAPI.Writers; internal static partial class AsyncApiV2Deserializer { internal static AsyncApiBindings LoadOperationBindings(ParseNode node) { - var mapNode = node.CheckMapNode("operationBinding"); + var mapNode = node.CheckMapNode("operationBindings"); var operationBindings = new AsyncApiBindings(); @@ -38,16 +36,20 @@ internal static AsyncApiBindings LoadOperationBindings(ParseN internal static IOperationBinding LoadOperationBinding(ParseNode node) { var property = node as PropertyNode; - var bindingType = property.Name.GetEnumFromDisplayName(); - switch (bindingType) + try { - case BindingType.Kafka: - return LoadBinding("OperationBinding", property.Value, kafkaOperationBindingFixedFields); - case BindingType.Http: - return LoadBinding("OperationBinding", property.Value, httpOperationBindingFixedFields); - default: - throw new AsyncApiException($"OperationBinding {property.Name} is not supported"); + if (node.Context.OperationBindingParsers.TryGetValue(property.Name, out var parser)) + { + return parser.LoadBinding(property); + } } + catch (AsyncApiException ex) + { + ex.Pointer = node.Context.GetLocation(); + node.Context.Diagnostic.Errors.Add(new AsyncApiError(ex)); + } + + return null; } } } diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiOperationDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiOperationDeserializer.cs index 723a20fa..8a9a0505 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiOperationDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiOperationDeserializer.cs @@ -10,7 +10,7 @@ namespace LEGO.AsyncAPI.Readers internal static partial class AsyncApiV2Deserializer { private static readonly FixedFieldMap operationFixedFields = - new () + new() { { "operationId", (a, n) => { a.OperationId = n.GetScalarValue(); } @@ -54,7 +54,7 @@ private static IList LoadMessages(ParseNode n) } private static readonly PatternFieldMap operationPatternFields = - new () + new() { { s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n)) }, }; diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiOperationTraitDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiOperationTraitDeserializer.cs index 699795c8..561456e9 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiOperationTraitDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiOperationTraitDeserializer.cs @@ -33,6 +33,7 @@ public static AsyncApiOperationTrait LoadOperationTrait(ParseNode node) { return mapNode.GetReferencedObject(ReferenceType.OperationTrait, pointer); } + var operationTrait = new AsyncApiOperationTrait(); ParseMap(mapNode, operationTrait, operationTraitFixedFields, operationTraitPatternFields); diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiParameterDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiParameterDeserializer.cs index 3841fbf0..bff810f1 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiParameterDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiParameterDeserializer.cs @@ -11,7 +11,7 @@ internal static partial class AsyncApiV2Deserializer private static FixedFieldMap parameterFixedFields = new() { { "description", (a, n) => { a.Description = n.GetScalarValue(); } }, - { "schema", (a, n) => { a.Schema = LoadSchema(n); } }, + { "schema", (a, n) => { a.Schema = JsonSchemaDeserializer.LoadSchema(n); } }, { "location", (a, n) => { a.Location = n.GetScalarValue(); } }, }; diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiSchemaDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiSchemaDeserializer.cs index e0504733..1934cb0d 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiSchemaDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiSchemaDeserializer.cs @@ -9,7 +9,7 @@ namespace LEGO.AsyncAPI.Readers using LEGO.AsyncAPI.Readers.ParseNodes; using LEGO.AsyncAPI.Writers; - internal static partial class AsyncApiV2Deserializer + public class JsonSchemaDeserializer { private static readonly FixedFieldMap schemaFixedFields = new() { @@ -21,11 +21,24 @@ internal static partial class AsyncApiV2Deserializer { if (n.GetType() == typeof(ValueNode)) { - a.Type = new List { n.GetScalarValue().GetEnumFromDisplayName() }; + a.Type = n.GetScalarValue().GetEnumFromDisplayName(); } - else + + if (n.GetType() == typeof(ListNode)) { - a.Type = new List(n.CreateSimpleList(n2 => n2.GetScalarValue().GetEnumFromDisplayName())); + SchemaType? initialValue = null; + foreach (var node in n as ListNode) + { + if (initialValue == null) + { + initialValue = node.GetScalarValue().GetEnumFromDisplayName(); + continue; + } + + initialValue |= node.GetScalarValue().GetEnumFromDisplayName(); + } + + a.Type = initialValue; } } }, @@ -37,58 +50,67 @@ internal static partial class AsyncApiV2Deserializer "multipleOf", (a, n) => { - a.MultipleOf = decimal.Parse(n.GetScalarValue(), NumberStyles.Float, CultureInfo.InvariantCulture); + a.MultipleOf = double.Parse(n.GetScalarValue(), NumberStyles.Float, n.Context.Settings.CultureInfo); } }, { "maximum", (a, n) => { - a.Maximum = decimal.Parse(n.GetScalarValue(), NumberStyles.Float, CultureInfo.InvariantCulture); + a.Maximum = double.Parse(n.GetScalarValue(), NumberStyles.Float, n.Context.Settings.CultureInfo); } }, { - "exclusiveMaximum", (a, n) => { a.ExclusiveMaximum = bool.Parse(n.GetScalarValue()); } + "exclusiveMaximum", (a, n) => + { + a.ExclusiveMaximum = double.Parse(n.GetScalarValue(), NumberStyles.Float, n.Context.Settings.CultureInfo); + } }, { "minimum", (a, n) => { - a.Minimum = decimal.Parse(n.GetScalarValue(), NumberStyles.Float, CultureInfo.InvariantCulture); + a.Minimum = double.Parse(n.GetScalarValue(), NumberStyles.Float, n.Context.Settings.CultureInfo); } }, { - "exclusiveMinimum", (a, n) => { a.ExclusiveMinimum = bool.Parse(n.GetScalarValue()); } + "exclusiveMinimum", (a, n) => + { + a.ExclusiveMinimum = double.Parse(n.GetScalarValue(), NumberStyles.Float, n.Context.Settings.CultureInfo); + } }, { - "maxLength", (a, n) => { a.MaxLength = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } + "maxLength", (a, n) => { a.MaxLength = int.Parse(n.GetScalarValue(), n.Context.Settings.CultureInfo); } }, { - "minLength", (a, n) => { a.MinLength = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } + "minLength", (a, n) => { a.MinLength = int.Parse(n.GetScalarValue(), n.Context.Settings.CultureInfo); } }, { "pattern", (a, n) => { a.Pattern = n.GetScalarValue(); } }, { - "maxItems", (a, n) => { a.MaxItems = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } + "maxItems", (a, n) => { a.MaxItems = int.Parse(n.GetScalarValue(), n.Context.Settings.CultureInfo); } }, { - "minItems", (a, n) => { a.MinItems = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } + "minItems", (a, n) => { a.MinItems = int.Parse(n.GetScalarValue(), n.Context.Settings.CultureInfo); } }, { "uniqueItems", (a, n) => { a.UniqueItems = bool.Parse(n.GetScalarValue()); } }, { "maxProperties", - (a, n) => { a.MaxProperties = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } + (a, n) => { a.MaxProperties = int.Parse(n.GetScalarValue(), n.Context.Settings.CultureInfo); } }, { "minProperties", - (a, n) => { a.MinProperties = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } + (a, n) => { a.MinProperties = int.Parse(n.GetScalarValue(), n.Context.Settings.CultureInfo); } }, { "enum", (a, n) => { a.Enum = n.CreateListOfAny(); } }, + { + "const", (a, n) => { a.Const = n.CreateAny(); } + }, { "examples", (a, n) => { a.Examples = n.CreateListOfAny(); } }, @@ -111,10 +133,49 @@ internal static partial class AsyncApiV2Deserializer "properties", (a, n) => { a.Properties = n.CreateMap(LoadSchema); } }, { - "additionalProperties", (a, n) => { a.AdditionalProperties = LoadSchema(n); } + "additionalProperties", (a, n) => + { + if (n is ValueNode && n.GetBooleanValueOrDefault(null) == false) + { + a.AdditionalProperties = new FalseApiSchema(); + } + else + { + a.AdditionalProperties = LoadSchema(n); + } + } }, { - "items", (a, n) => { a.Items = LoadSchema(n); } + "items", (a, n) => + { + if (n is ValueNode && n.GetBooleanValueOrDefault(null) == false) + { + a.Items = new FalseApiSchema(); + } + else + { + a.Items = LoadSchema(n); + } + } + }, + { + "additionalItems", (a, n) => + { + if (n is ValueNode && n.GetBooleanValueOrDefault(null) == false) + { + a.AdditionalItems = new FalseApiSchema(); + } + else + { + a.AdditionalItems = LoadSchema(n); + } + } + }, + { + "patternProperties", (a, n) => { a.PatternProperties = n.CreateMap(LoadSchema); } + }, + { + "propertyNames", (a, n) => { a.PropertyNames = LoadSchema(n); } }, { "contains", (a, n) => { a.Contains = LoadSchema(n); } @@ -144,17 +205,20 @@ internal static partial class AsyncApiV2Deserializer "discriminator", (a, n) => { a.Discriminator = n.GetScalarValue(); } }, { - "externalDocs", (a, n) => { a.ExternalDocs = LoadExternalDocs(n); } + "externalDocs", (a, n) => { a.ExternalDocs = AsyncApiV2Deserializer.LoadExternalDocs(n); } }, { "deprecated", (a, n) => { a.Deprecated = bool.Parse(n.GetScalarValue()); } }, + { + "nullable", (a, n) => { a.Nullable = n.GetBooleanValue(); } + }, }; private static readonly PatternFieldMap schemaPatternFields = new() { - { s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n)) }, + { s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, AsyncApiV2Deserializer.LoadExtension(p, n)) }, }; private static readonly AnyFieldMap schemaAnyFields = new() @@ -201,8 +265,8 @@ public static AsyncApiSchema LoadSchema(ParseNode node) propertyNode.ParseField(schema, schemaFixedFields, schemaPatternFields); } - ProcessAnyFields(mapNode, schema, schemaAnyFields); - ProcessAnyListFields(mapNode, schema, schemaAnyListFields); + AsyncApiV2Deserializer.ProcessAnyFields(mapNode, schema, schemaAnyFields); + AsyncApiV2Deserializer.ProcessAnyListFields(mapNode, schema, schemaAnyListFields); return schema; } diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiSecuritySchemeDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiSecuritySchemeDeserializer.cs index 75ad46dd..92708116 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiSecuritySchemeDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiSecuritySchemeDeserializer.cs @@ -45,7 +45,7 @@ internal static partial class AsyncApiV2Deserializer }; private static readonly PatternFieldMap securitySchemePatternFields = - new () + new() { { s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n)) }, }; diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiServerBindingDeserializer.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiServerBindingDeserializer.cs index 632d85ab..44c821d3 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiServerBindingDeserializer.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiServerBindingDeserializer.cs @@ -4,19 +4,21 @@ namespace LEGO.AsyncAPI.Readers { using LEGO.AsyncAPI.Exceptions; using LEGO.AsyncAPI.Models; - using LEGO.AsyncAPI.Models.Bindings; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Readers.ParseNodes; - using LEGO.AsyncAPI.Writers; internal static partial class AsyncApiV2Deserializer { internal static AsyncApiBindings LoadServerBindings(ParseNode node) { - var mapNode = node.CheckMapNode("serverBinding"); + var mapNode = node.CheckMapNode("serverBindings"); + var pointer = mapNode.GetReferencePointer(); + if (pointer != null) + { + return mapNode.GetReferencedObject>(ReferenceType.ServerBindings, pointer); + } var serverBindings = new AsyncApiBindings(); - foreach (var property in mapNode) { var serverBinding = LoadServerBinding(property); @@ -38,16 +40,20 @@ internal static AsyncApiBindings LoadServerBindings(ParseNode no internal static IServerBinding LoadServerBinding(ParseNode node) { var property = node as PropertyNode; - var bindingType = property.Name.GetEnumFromDisplayName(); - switch (bindingType) + try + { + if (node.Context.ServerBindingParsers.TryGetValue(property.Name, out var parser)) + { + return parser.LoadBinding(property); + } + } + catch (AsyncApiException ex) { - case BindingType.Kafka: - return LoadBinding("ServerBinding", property.Value, kafkaServerBindingFixedFields); - case BindingType.Pulsar: - return LoadBinding("ServerBinding", property.Value, pulsarServerBindingFixedFields); - default: - throw new AsyncApiException($"ServerBinding {property.Name} is not supported"); + ex.Pointer = node.Context.GetLocation(); + node.Context.Diagnostic.Errors.Add(new AsyncApiError(ex)); } + + return null; } } } diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs index b4f34131..10edd3ba 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs @@ -16,7 +16,7 @@ internal class AsyncApiV2VersionService : IAsyncApiVersionService public AsyncApiDiagnostic Diagnostic { get; } /// - /// Create Parsing Context + /// Create Parsing Context. /// /// Provide instance for diagnostic object for collecting and accessing information about the parsing. public AsyncApiV2VersionService(AsyncApiDiagnostic diagnostic) @@ -26,7 +26,7 @@ public AsyncApiV2VersionService(AsyncApiDiagnostic diagnostic) private IDictionary> loaders = new Dictionary> { - [typeof(IAsyncApiAny)] = AsyncApiV2Deserializer.LoadAny, + [typeof(AsyncApiAny)] = AsyncApiV2Deserializer.LoadAny, [typeof(AsyncApiComponents)] = AsyncApiV2Deserializer.LoadComponents, [typeof(AsyncApiExternalDocumentation)] = AsyncApiV2Deserializer.LoadExternalDocs, [typeof(AsyncApiInfo)] = AsyncApiV2Deserializer.LoadInfo, @@ -35,7 +35,7 @@ public AsyncApiV2VersionService(AsyncApiDiagnostic diagnostic) [typeof(AsyncApiOAuthFlows)] = AsyncApiV2Deserializer.LoadOAuthFlows, [typeof(AsyncApiOperation)] = AsyncApiV2Deserializer.LoadOperation, [typeof(AsyncApiParameter)] = AsyncApiV2Deserializer.LoadParameter, - [typeof(AsyncApiSchema)] = AsyncApiV2Deserializer.LoadSchema, + [typeof(AsyncApiSchema)] = JsonSchemaDeserializer.LoadSchema, [typeof(AsyncApiSecurityRequirement)] = AsyncApiV2Deserializer.LoadSecurityRequirement, [typeof(AsyncApiSecurityScheme)] = AsyncApiV2Deserializer.LoadSecurityScheme, [typeof(AsyncApiServer)] = AsyncApiV2Deserializer.LoadServer, @@ -48,68 +48,86 @@ public AsyncApiV2VersionService(AsyncApiDiagnostic diagnostic) /// /// Parse the string to a object. /// - /// The URL of the reference - /// The type of object referenced based on the context of the reference + /// The URL of the reference. + /// The type of object referenced based on the context of the reference. public AsyncApiReference ConvertToAsyncApiReference( string reference, ReferenceType? type) { - if (!string.IsNullOrWhiteSpace(reference)) + if (string.IsNullOrWhiteSpace(reference)) { - var segments = reference.Split('#'); - if (segments.Length == 1) + throw new AsyncApiException($"The reference string '{reference}' has invalid format."); + } + + var segments = reference.Split('#'); + if (segments.Length == 1) + { + if (type == ReferenceType.SecurityScheme) { - if (type == ReferenceType.SecurityScheme) + return new AsyncApiReference { - return new AsyncApiReference - { - Type = type, - Id = reference, - }; - } + Type = type, + Id = reference, + }; } - else if (segments.Length == 2) + + var asyncApiReference = new AsyncApiReference(); + asyncApiReference.Type = type; + if (reference.StartsWith("/")) { - if (reference.StartsWith("#")) + asyncApiReference.IsFragment = true; + } + + asyncApiReference.ExternalResource = segments[0]; + + return asyncApiReference; + } + else if (segments.Length == 2) + { + // Local reference + if (reference.StartsWith("#")) + { + try { - try - { - return this.ParseReference(segments[1]); - } - catch (AsyncApiException ex) - { - this.Diagnostic.Errors.Add(new AsyncApiError(ex)); - return null; - } + return this.ParseReference(segments[1]); } + catch (AsyncApiException ex) + { + this.Diagnostic.Errors.Add(new AsyncApiError(ex)); + return null; + } + } - var id = segments[1]; - - if (id.StartsWith("/components/")) + var id = segments[1]; + var asyncApiReference = new AsyncApiReference(); + if (id.StartsWith("/components/")) + { + var localSegments = segments[1].Split('/'); + var referencedType = localSegments[2].GetEnumFromDisplayName(); + if (type == null) { - var localSegments = segments[1].Split('/'); - var referencedType = localSegments[2].GetEnumFromDisplayName(); - if (type == null) - { - type = referencedType; - } - else + type = referencedType; + } + else + { + if (type != referencedType) { - if (type != referencedType) - { - throw new AsyncApiException("Referenced type mismatch"); - } + throw new AsyncApiException("Referenced type mismatch"); } - - id = localSegments[3]; } - return new AsyncApiReference - { - Type = type, - Id = id, - }; + id = localSegments[3]; } + else + { + asyncApiReference.IsFragment = true; + } + + asyncApiReference.ExternalResource = segments[0]; + asyncApiReference.Type = type; + asyncApiReference.Id = id; + + return asyncApiReference; } throw new AsyncApiException($"The reference string '{reference}' has invalid format."); @@ -120,7 +138,8 @@ public AsyncApiDocument LoadDocument(RootNode rootNode) return AsyncApiV2Deserializer.LoadAsyncApi(rootNode); } - public T LoadElement(ParseNode node) where T : IAsyncApiElement + public T LoadElement(ParseNode node) + where T : IAsyncApiElement { return (T)this.loaders[typeof(T)](node); } diff --git a/src/LEGO.AsyncAPI.Readers/V2/ExtensionHelpers.cs b/src/LEGO.AsyncAPI.Readers/V2/ExtensionHelpers.cs new file mode 100644 index 00000000..60689b31 --- /dev/null +++ b/src/LEGO.AsyncAPI.Readers/V2/ExtensionHelpers.cs @@ -0,0 +1,43 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Readers +{ + using LEGO.AsyncAPI.Exceptions; + using LEGO.AsyncAPI.Extensions; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Readers.ParseNodes; + + public static class ExtensionHelpers + { + public static PatternFieldMap GetExtensionsFieldMap() + where T : IAsyncApiExtensible + { + return new() + { + { + s => s.StartsWith("x-"), + (a, p, n) => a.AddExtension(p, LoadExtension(p, n)) + }, + }; + } + + public static IAsyncApiExtension LoadExtension(string name, ParseNode node) + { + try + { + if (node.Context.ExtensionParsers.TryGetValue(name, out var parser)) + { + return parser(node.CreateAny()); + } + } + catch (AsyncApiException ex) + { + ex.Pointer = node.Context.GetLocation(); + node.Context.Diagnostic.Errors.Add(new AsyncApiError(ex)); + } + + return node.CreateAny(); + } + } +} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Readers/YamlConverter.cs b/src/LEGO.AsyncAPI.Readers/YamlConverter.cs new file mode 100644 index 00000000..648ee50a --- /dev/null +++ b/src/LEGO.AsyncAPI.Readers/YamlConverter.cs @@ -0,0 +1,73 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Readers +{ + using System; + using System.Globalization; + using System.Text.Json.Nodes; + using YamlDotNet.Core; + using YamlDotNet.RepresentationModel; + + internal static class YamlConverter + { + public static JsonNode ToJsonNode(this YamlDocument yamlDocument, AsyncApiReaderSettings settings) + { + return yamlDocument.RootNode.ToJsonNode(settings); + } + + public static JsonObject ToJsonObject(this YamlMappingNode yamlMappingNode, AsyncApiReaderSettings settings) + { + var node = new JsonObject(); + foreach (var keyValuePair in yamlMappingNode) + { + var key = ((YamlScalarNode)keyValuePair.Key).Value!; + node[key] = keyValuePair.Value.ToJsonNode(settings); + } + + return node; + } + + public static JsonArray ToJsonArray(this YamlSequenceNode yaml, AsyncApiReaderSettings settings) + { + var node = new JsonArray(); + foreach (var value in yaml) + { + node.Add(value.ToJsonNode(settings)); + } + + return node; + } + + public static JsonNode ToJsonNode(this YamlNode yaml, AsyncApiReaderSettings settings) + { + return yaml switch + { + YamlMappingNode map => map.ToJsonObject(settings), + YamlSequenceNode seq => seq.ToJsonArray(settings), + YamlScalarNode scalar => scalar.ToJsonValue(settings), + _ => throw new NotSupportedException("This yaml isn't convertible to JSON"), + }; + } + + private static JsonValue ToJsonValue(this YamlScalarNode yaml, AsyncApiReaderSettings settings) + { + switch (yaml.Style) + { + case ScalarStyle.Plain: + return decimal.TryParse(yaml.Value, NumberStyles.Float, settings.CultureInfo, out var d) + ? JsonValue.Create(d) + : bool.TryParse(yaml.Value, out var b) + ? JsonValue.Create(b) + : JsonValue.Create(yaml.Value) !; + case ScalarStyle.SingleQuoted: + case ScalarStyle.DoubleQuoted: + case ScalarStyle.Literal: + case ScalarStyle.Folded: + case ScalarStyle.Any: + return JsonValue.Create(yaml.Value); + default: + throw new ArgumentOutOfRangeException(); + } + } + } +} diff --git a/src/LEGO.AsyncAPI.Readers/YamlHelper.cs b/src/LEGO.AsyncAPI.Readers/YamlHelper.cs deleted file mode 100644 index f29522f0..00000000 --- a/src/LEGO.AsyncAPI.Readers/YamlHelper.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Readers -{ - using System.IO; - using System.Linq; - using LEGO.AsyncAPI.Exceptions; - using YamlDotNet.RepresentationModel; - - internal static class YamlHelper - { - public static string GetScalarValue(this YamlNode node) - { - var scalarNode = node as YamlScalarNode; - if (scalarNode == null) - { - throw new AsyncApiException($"Expected scalar at line {node.Start.Line}"); - } - - return scalarNode.Value; - } - - public static YamlNode ParseYamlString(string yamlString) - { - var reader = new StringReader(yamlString); - var yamlStream = new YamlStream(); - yamlStream.Load(reader); - - var yamlDocument = yamlStream.Documents.First(); - return yamlDocument.RootNode; - } - } -} diff --git a/src/LEGO.AsyncAPI/AsyncApiSettings.cs b/src/LEGO.AsyncAPI/AsyncApiSettings.cs new file mode 100644 index 00000000..ba9e0189 --- /dev/null +++ b/src/LEGO.AsyncAPI/AsyncApiSettings.cs @@ -0,0 +1,31 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI +{ + using System.Globalization; + + /// + /// Base class for setting common acorss the various projects in the solution. + /// + public abstract class AsyncApiSettings + { + /// + /// Initializes a new instance of the class. + /// + protected AsyncApiSettings() + { + this.DateTimeFormat = "yyyy-MM-ddTHH:mm:ss.fffzzz"; + this.CultureInfo = CultureInfo.InvariantCulture; + } + + /// + /// Gets the format used for reading and writing date time structures. + /// + public string DateTimeFormat { get; } + + /// + /// Gets the culture info used for strings. + /// + public CultureInfo CultureInfo { get; } + } +} diff --git a/src/LEGO.AsyncAPI/EnumExtensions.cs b/src/LEGO.AsyncAPI/EnumExtensions.cs index d7e2a751..3cca149d 100644 --- a/src/LEGO.AsyncAPI/EnumExtensions.cs +++ b/src/LEGO.AsyncAPI/EnumExtensions.cs @@ -3,6 +3,7 @@ namespace LEGO.AsyncAPI { using System; + using System.Collections.Generic; using System.Linq; using System.Reflection; using LEGO.AsyncAPI.Attributes; @@ -17,7 +18,8 @@ public static class EnumExtensions /// /// The attribute of the specified type or null. /// - public static T GetAttributeOfType(this Enum enumValue) where T : Attribute + public static T GetAttributeOfType(this Enum enumValue) + where T : Attribute { var type = enumValue.GetType(); var memInfo = type.GetMember(enumValue.ToString()).First(); @@ -38,5 +40,17 @@ public static string GetDisplayName(this Enum enumValue) var attribute = enumValue.GetAttributeOfType(); return attribute == null ? enumValue.ToString() : attribute.Name; } + + public static IEnumerable GetFlags(this Enum input) + where TEnum : Enum + { + foreach (TEnum value in System.Enum.GetValues(input.GetType())) + { + if (input.HasFlag(value)) + { + yield return value; + } + } + } } } diff --git a/src/LEGO.AsyncAPI/Exceptions/AsyncApiException.cs b/src/LEGO.AsyncAPI/Exceptions/AsyncApiException.cs index 73a25acd..d841c002 100644 --- a/src/LEGO.AsyncAPI/Exceptions/AsyncApiException.cs +++ b/src/LEGO.AsyncAPI/Exceptions/AsyncApiException.cs @@ -39,7 +39,7 @@ public AsyncApiException(string message, Exception innerException) /// JSON Pointer as per https://tools.ietf.org/html/rfc6901 /// If the document fails to parse as JSON/YAML then the fragment will be based on /// a text/plain pointer as defined in https://tools.ietf.org/html/rfc5147 - /// Currently only line= is provided because using char= causes tests to break due to CR/LF and LF differences + /// Currently only line= is provided because using char= causes tests to break due to CR/LF and LF differences. /// public string Pointer { get; set; } } diff --git a/src/LEGO.AsyncAPI/Expressions/BodyExpression.cs b/src/LEGO.AsyncAPI/Expressions/BodyExpression.cs deleted file mode 100644 index 202812a5..00000000 --- a/src/LEGO.AsyncAPI/Expressions/BodyExpression.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Expressions -{ - /// - /// Body expression. - /// - public sealed class BodyExpression : SourceExpression - { - /// - /// body string - /// - public const string Body = "body"; - - /// - /// Prefix for a pointer - /// - public const string PointerPrefix = "#"; - - /// - /// Initializes a new instance of the class. - /// - public BodyExpression() - : base(null) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// a JSON Pointer [RFC 6901](https://tools.ietf.org/html/rfc6901). - public BodyExpression(JsonPointer pointer) - : base(pointer?.ToString()) - { - if (pointer == null) - { - throw Error.ArgumentNull(nameof(pointer)); - } - } - - /// - /// Gets the expression string. - /// - public override string Expression - { - get - { - if (string.IsNullOrWhiteSpace(this.Value)) - { - return Body; - } - - return Body + PointerPrefix + this.Value; - } - } - - /// - /// Gets the fragment string. - /// - public string Fragment - { - get - { - return this.Value; - } - } - } -} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Expressions/CompositeExpression.cs b/src/LEGO.AsyncAPI/Expressions/CompositeExpression.cs deleted file mode 100644 index 22ef38da..00000000 --- a/src/LEGO.AsyncAPI/Expressions/CompositeExpression.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Expressions -{ - using System.Collections.Generic; - using System.Linq; - using System.Text.RegularExpressions; - - /// - /// String literal with embedded expressions - /// - public class CompositeExpression : RuntimeExpression - { - private readonly string template; - private Regex expressionPattern = new Regex(@"{(?\$[^}]*)"); - - /// - /// Expressions embedded into string literal - /// - public List ContainedExpressions = new List(); - - /// - /// Create a composite expression from a string literal with an embedded expression - /// - /// - public CompositeExpression(string expression) - { - this.template = expression; - - // Extract subexpressions and convert to RuntimeExpressions - var matches = this.expressionPattern.Matches(expression); - - foreach (var item in matches.Cast()) - { - var value = item.Groups["exp"].Captures.Cast().First().Value; - this.ContainedExpressions.Add(RuntimeExpression.Build(value)); - } - } - - /// - /// Return original string literal with embedded expression - /// - public override string Expression => this.template; - } -} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Expressions/HeaderExpression.cs b/src/LEGO.AsyncAPI/Expressions/HeaderExpression.cs deleted file mode 100644 index 753b1218..00000000 --- a/src/LEGO.AsyncAPI/Expressions/HeaderExpression.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Expressions -{ - /// - /// Header expression, The token identifier in header is case-insensitive. - /// - public class HeaderExpression : SourceExpression - { - /// - /// header. string - /// - public const string Header = "header."; - - /// - /// Initializes a new instance of the class. - /// - /// The token string, it's case-insensitive. - public HeaderExpression(string token) - : base(token) - { - if (string.IsNullOrWhiteSpace(token)) - { - throw Error.ArgumentNullOrWhiteSpace(nameof(token)); - } - } - - /// - /// Gets the expression string. - /// - public override string Expression - { - get - { - return Header + this.Value; - } - } - - /// - /// Gets the token string. - /// - public string Token - { - get - { - return this.Value; - } - } - } -} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Expressions/MethodExpression.cs b/src/LEGO.AsyncAPI/Expressions/MethodExpression.cs deleted file mode 100644 index 150e12a0..00000000 --- a/src/LEGO.AsyncAPI/Expressions/MethodExpression.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Expressions -{ - /// - /// Method expression. - /// - public sealed class MethodExpression : RuntimeExpression - { - /// - /// $method. string - /// - public const string Method = "$method"; - - /// - /// Gets the expression string. - /// - public override string Expression { get; } = Method; - - /// - /// Private constructor. - /// - public MethodExpression() - { - } - } -} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Expressions/PathExpression.cs b/src/LEGO.AsyncAPI/Expressions/PathExpression.cs deleted file mode 100644 index de6eab39..00000000 --- a/src/LEGO.AsyncAPI/Expressions/PathExpression.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Expressions -{ - /// - /// Path expression, the name in path is case-sensitive. - /// - public sealed class PathExpression : SourceExpression - { - /// - /// path. string - /// - public const string Path = "path."; - - /// - /// Initializes a new instance of the class. - /// - /// The name string, it's case-insensitive. - public PathExpression(string name) - : base(name) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw Error.ArgumentNullOrWhiteSpace(nameof(name)); - } - } - - /// - /// Gets the expression string. - /// - public override string Expression - { - get - { - return Path + this.Value; - } - } - - /// - /// Gets the name string. - /// - public string Name - { - get - { - return this.Value; - } - } - } -} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Expressions/QueryExpression.cs b/src/LEGO.AsyncAPI/Expressions/QueryExpression.cs deleted file mode 100644 index 4d415f0c..00000000 --- a/src/LEGO.AsyncAPI/Expressions/QueryExpression.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Expressions -{ - /// - /// Query expression, the name in query is case-sensitive. - /// - public sealed class QueryExpression : SourceExpression - { - /// - /// query. string - /// - public const string Query = "query."; - - /// - /// Initializes a new instance of the class. - /// - /// The name string, it's case-insensitive. - public QueryExpression(string name) - : base(name) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw Error.ArgumentNullOrWhiteSpace(nameof(name)); - } - } - - /// - /// Gets the expression string. - /// - public override string Expression - { - get - { - return Query + this.Value; - } - } - - /// - /// Gets the name string. - /// - public string Name - { - get - { - return this.Value; - } - } - } -} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Expressions/RequestExpression.cs b/src/LEGO.AsyncAPI/Expressions/RequestExpression.cs deleted file mode 100644 index 8b18b0ab..00000000 --- a/src/LEGO.AsyncAPI/Expressions/RequestExpression.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Expressions -{ - /// - /// $request. expression. - /// - public sealed class RequestExpression : RuntimeExpression - { - /// - /// $request. string - /// - public const string Request = "$request."; - - /// - /// Initializes a new instance of the class. - /// - /// The source of the request. - public RequestExpression(SourceExpression source) - { - this.Source = source ?? throw Error.ArgumentNull(nameof(source)); - } - - /// - /// Gets the expression string. - /// - public override string Expression => Request + this.Source.Expression; - - /// - /// The expression. - /// - public SourceExpression Source { get; } - } -} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Expressions/ResponseExpression.cs b/src/LEGO.AsyncAPI/Expressions/ResponseExpression.cs deleted file mode 100644 index d276f4c7..00000000 --- a/src/LEGO.AsyncAPI/Expressions/ResponseExpression.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Expressions -{ - /// - /// $response. expression. - /// - public sealed class ResponseExpression : RuntimeExpression - { - /// - /// $response. string - /// - public const string Response = "$response."; - - /// - /// Initializes a new instance of the class. - /// - /// The source of the response. - public ResponseExpression(SourceExpression source) - { - this.Source = source ?? throw Error.ArgumentNull(nameof(source)); - } - - /// - /// Gets the expression string. - /// - public override string Expression => Response + this.Source.Expression; - - /// - /// The expression. - /// - public SourceExpression Source { get; } - } -} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Expressions/RuntimeExpressions.cs b/src/LEGO.AsyncAPI/Expressions/RuntimeExpressions.cs deleted file mode 100644 index 2c88373a..00000000 --- a/src/LEGO.AsyncAPI/Expressions/RuntimeExpressions.cs +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Expressions -{ - using System; - using LEGO.AsyncAPI.Exceptions; - - /// - /// Base class for the AsyncApi runtime expression. - /// - public abstract class RuntimeExpression : IEquatable - { - /// - /// The dollar sign prefix for a runtime expression. - /// - public const string Prefix = "$"; - - /// - /// The expression string. - /// - public abstract string Expression { get; } - - /// - /// Build the runtime expression from input string. - /// - /// The runtime expression. - /// The built runtime expression object. - public static RuntimeExpression Build(string expression) - { - if (string.IsNullOrWhiteSpace(expression)) - { - throw Error.ArgumentNullOrWhiteSpace(nameof(expression)); - } - - if (!expression.StartsWith(Prefix)) - { - return new CompositeExpression(expression); - } - - // $url - if (expression == UrlExpression.Url) - { - return new UrlExpression(); - } - - // $method - if (expression == MethodExpression.Method) - { - return new MethodExpression(); - } - - // $statusCode - if (expression == StatusCodeExpression.StatusCode) - { - return new StatusCodeExpression(); - } - - // $request. - if (expression.StartsWith(RequestExpression.Request)) - { - var subString = expression.Substring(RequestExpression.Request.Length); - var source = SourceExpression.Build(subString); - return new RequestExpression(source); - } - - // $response. - if (expression.StartsWith(ResponseExpression.Response)) - { - var subString = expression.Substring(ResponseExpression.Response.Length); - var source = SourceExpression.Build(subString); - return new ResponseExpression(source); - } - - throw new AsyncApiException(string.Format("The runtime expression '{0}' has invalid format.", expression)); - } - - /// - /// GetHashCode implementation for IEquatable. - /// - public override int GetHashCode() - { - return this.Expression.GetHashCode(); - } - - /// - /// Equals implementation for IEquatable. - /// - public override bool Equals(object obj) - { - return this.Equals(obj as RuntimeExpression); - } - - /// - /// Equals implementation for object of the same type. - /// - public bool Equals(RuntimeExpression obj) - { - return obj != null && obj.Expression == this.Expression; - } - - /// - public override string ToString() - { - return this.Expression; - } - } -} diff --git a/src/LEGO.AsyncAPI/Expressions/SourceExpression.cs b/src/LEGO.AsyncAPI/Expressions/SourceExpression.cs deleted file mode 100644 index c5bbde98..00000000 --- a/src/LEGO.AsyncAPI/Expressions/SourceExpression.cs +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Expressions -{ - using LEGO.AsyncAPI.Exceptions; - - /// - /// Source expression. - /// - public abstract class SourceExpression : RuntimeExpression - { - /// - /// Initializes a new instance of the class. - /// - /// The value string. - protected SourceExpression(string value) - { - this.Value = value; - } - - /// - /// Gets the expression string. - /// - protected string Value { get; } - - /// - /// Build the source expression from input string. - /// - /// The source expression. - /// The built source expression. - public new static SourceExpression Build(string expression) - { - if (!string.IsNullOrWhiteSpace(expression)) - { - var expressions = expression.Split('.'); - if (expressions.Length == 2) - { - if (expression.StartsWith(HeaderExpression.Header)) - { - // header. - return new HeaderExpression(expressions[1]); - } - - if (expression.StartsWith(QueryExpression.Query)) - { - // query. - return new QueryExpression(expressions[1]); - } - - if (expression.StartsWith(PathExpression.Path)) - { - // path. - return new PathExpression(expressions[1]); - } - } - - // body - if (expression.StartsWith(BodyExpression.Body)) - { - var subString = expression.Substring(BodyExpression.Body.Length); - if (string.IsNullOrEmpty(subString)) - { - return new BodyExpression(); - } - - return new BodyExpression(new JsonPointer(subString)); - } - } - - throw new AsyncApiException(string.Format("The source expression '{0}' has invalid format.", expression)); - } - } -} diff --git a/src/LEGO.AsyncAPI/Expressions/StatusCodeExpression.cs b/src/LEGO.AsyncAPI/Expressions/StatusCodeExpression.cs deleted file mode 100644 index c8ef862e..00000000 --- a/src/LEGO.AsyncAPI/Expressions/StatusCodeExpression.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Expressions -{ - /// - /// StatusCode expression. - /// - public sealed class StatusCodeExpression : RuntimeExpression - { - /// - /// $statusCode string. - /// - public const string StatusCode = "$statusCode"; - - /// - /// Gets the expression string. - /// - public override string Expression { get; } = StatusCode; - - /// - /// Private constructor. - /// - public StatusCodeExpression() - { - } - } -} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Expressions/UrlExpression.cs b/src/LEGO.AsyncAPI/Expressions/UrlExpression.cs deleted file mode 100644 index 83e01aa4..00000000 --- a/src/LEGO.AsyncAPI/Expressions/UrlExpression.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Expressions -{ - /// - /// Url expression. - /// - public sealed class UrlExpression : RuntimeExpression - { - /// - /// $url string. - /// - public const string Url = "$url"; - - /// - /// Gets the expression string. - /// - public override string Expression { get; } = Url; - - /// - /// Private constructor. - /// - public UrlExpression() - { - } - } -} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Extensions/AsyncApiElementExtensions.cs b/src/LEGO.AsyncAPI/Extensions/AsyncApiElementExtensions.cs index 309a8b64..1215b86b 100644 --- a/src/LEGO.AsyncAPI/Extensions/AsyncApiElementExtensions.cs +++ b/src/LEGO.AsyncAPI/Extensions/AsyncApiElementExtensions.cs @@ -10,15 +10,15 @@ namespace LEGO.AsyncAPI.Extensions using LEGO.AsyncAPI.Validations; /// - /// Extension methods that apply across all AsyncAPIElements + /// Extension methods that apply across all AsyncAPIElements. /// public static class AsyncApiElementExtensions { /// - /// Validate element and all child elements + /// Validate element and all child elements. /// - /// Element to validate - /// Optional set of rules to use for validation + /// Element to validate. + /// Optional set of rules to use for validation. /// An IEnumerable of errors. This function will never return null. public static IEnumerable Validate(this IAsyncApiElement element, ValidationRuleSet ruleSet) { diff --git a/src/LEGO.AsyncAPI/Extensions/AsyncApiExtensibleExtensions.cs b/src/LEGO.AsyncAPI/Extensions/AsyncApiExtensibleExtensions.cs index 81b1eabd..8fdb50e5 100644 --- a/src/LEGO.AsyncAPI/Extensions/AsyncApiExtensibleExtensions.cs +++ b/src/LEGO.AsyncAPI/Extensions/AsyncApiExtensibleExtensions.cs @@ -2,17 +2,18 @@ namespace LEGO.AsyncAPI.Extensions { + using System.Collections.Generic; using LEGO.AsyncAPI.Exceptions; using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Models.Interfaces; /// - /// Extension methods to verify validatity and add an extension to Extensions property. + /// Extension methods to verify validity and add an extension to Extensions property. /// public static class AsyncApiExtensibleExtensions { /// - /// Add extension into the Extensions + /// Add extension into the Extensions. /// /// . /// The extensible AsyncApi element. @@ -38,5 +39,25 @@ public static void AddExtension(this T element, string name, IAsyncApiExtensi element.Extensions[name] = any ?? throw Error.ArgumentNull(nameof(any)); } + + /// + /// Tries the get value or default. + /// + /// + /// The dictionary. + /// The key. + /// The value. + /// + public static bool TryGetValueOrDefault(this IDictionary dictionary, string key, out T value) + { + if (dictionary.TryGetValue(key, out var extension)) + { + value = AsyncApiAny.FromExtensionOrDefault(extension); + return true; + } + + value = default(T); + return false; + } } } \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/LEGO.AsyncAPI.csproj b/src/LEGO.AsyncAPI/LEGO.AsyncAPI.csproj index 8d4b9501..a321a003 100644 --- a/src/LEGO.AsyncAPI/LEGO.AsyncAPI.csproj +++ b/src/LEGO.AsyncAPI/LEGO.AsyncAPI.csproj @@ -1,19 +1,11 @@  - + - net6.0 - disable - The LEGO Group - https://github.com/LEGO/AsyncAPI.NET - README.md - AsyncAPI.NET models - asyncapi .net openapi documentation - AsyncAPI.NET - LEGO.AsyncAPI - LEGO.AsyncAPI - https://github.com/LEGO/AsyncAPI.NET + AsyncAPI.NET models + AsyncAPI.NET + LEGO.AsyncAPI + LEGO.AsyncAPI - @@ -23,27 +15,21 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive + <_Parameter1>$(MSBuildProjectName).Tests - - - - True - \ - - - + - + True @@ -51,7 +37,7 @@ Resource.resx - + ResXFileCodeGenerator diff --git a/src/LEGO.AsyncAPI/Models/Any/AnyType.cs b/src/LEGO.AsyncAPI/Models/Any/AnyType.cs deleted file mode 100644 index 5e608514..00000000 --- a/src/LEGO.AsyncAPI/Models/Any/AnyType.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Models.Any -{ - using LEGO.AsyncAPI.Models.Interfaces; - - /// - /// Type of an . - /// - public enum AnyType - { - /// - /// Primitive. - /// - Primitive, - - /// - /// Null. - /// - Null, - - /// - /// Array. - /// - Array, - - /// - /// Object. - /// - Object, - } -} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/Any/AsyncAPIArray.cs b/src/LEGO.AsyncAPI/Models/Any/AsyncAPIArray.cs index 87d11aeb..d40208bd 100644 --- a/src/LEGO.AsyncAPI/Models/Any/AsyncAPIArray.cs +++ b/src/LEGO.AsyncAPI/Models/Any/AsyncAPIArray.cs @@ -1,25 +1,44 @@ // Copyright (c) The LEGO Group. All rights reserved. -namespace LEGO.AsyncAPI.Models.Any +namespace LEGO.AsyncAPI.Models { - using System.Collections.Generic; + using System; + using System.Collections.ObjectModel; + using System.Text.Json.Nodes; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Writers; - /// - /// AsyncApi array. - /// - public class AsyncApiArray : List, IAsyncApiAny + [Obsolete("Please use AsyncApiAny instead")] + public class AsyncApiArray : Collection, IAsyncApiExtension, IAsyncApiElement { - /// - /// The type of . - /// - public AnyType AnyType { get; } = AnyType.Array; + public static explicit operator AsyncApiArray(AsyncApiAny any) + { + var a = new AsyncApiArray(); + if (any.GetNode() is JsonArray arr) + { + foreach (var item in arr) + { + a.Add(new AsyncApiAny(item)); + } + } + + return a; + } + + public static implicit operator AsyncApiAny(AsyncApiArray arr) + { + var jArray = new JsonArray(); + foreach (var item in arr) + { + jArray.Add(item.GetNode()); + } + + return new AsyncApiAny(jArray); + } /// - /// Write out contents of AsyncApiArray to passed writer. + /// Serialize AsyncApiObject to writer. /// - /// Instance of JSON or YAML writer. public void Write(IAsyncApiWriter writer) { writer.WriteStartArray(); diff --git a/src/LEGO.AsyncAPI/Models/Any/AsyncAPIBoolean.cs b/src/LEGO.AsyncAPI/Models/Any/AsyncAPIBoolean.cs deleted file mode 100644 index 7a063be0..00000000 --- a/src/LEGO.AsyncAPI/Models/Any/AsyncAPIBoolean.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Models.Any -{ - using LEGO.AsyncAPI.Models.Interfaces; - - /// - /// AsyncApi boolean. - /// - public class AsyncApiBoolean : AsyncApiPrimitive - { - /// - /// Initializes the class. - /// - /// - public AsyncApiBoolean(bool value) - : base(value) - { - } - - /// - /// Primitive type this object represents. - /// - public override PrimitiveType PrimitiveType { get; } = PrimitiveType.Boolean; - } -} diff --git a/src/LEGO.AsyncAPI/Models/Any/AsyncAPIDouble.cs b/src/LEGO.AsyncAPI/Models/Any/AsyncAPIDouble.cs deleted file mode 100644 index dc54727e..00000000 --- a/src/LEGO.AsyncAPI/Models/Any/AsyncAPIDouble.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Models.Any -{ - using LEGO.AsyncAPI.Models.Interfaces; - - /// - /// AsyncApi Double - /// - public class AsyncApiDouble : AsyncApiPrimitive - { - /// - /// Initializes the class. - /// - public AsyncApiDouble(double value) - : base(value) - { - } - - /// - /// Primitive type this object represents. - /// - public override PrimitiveType PrimitiveType { get; } = PrimitiveType.Double; - } -} diff --git a/src/LEGO.AsyncAPI/Models/Any/AsyncAPILong.cs b/src/LEGO.AsyncAPI/Models/Any/AsyncAPILong.cs deleted file mode 100644 index 3376d853..00000000 --- a/src/LEGO.AsyncAPI/Models/Any/AsyncAPILong.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Models.Any -{ - using LEGO.AsyncAPI.Models.Interfaces; - - /// - /// AsyncApi long. - /// - public class AsyncApiLong : AsyncApiPrimitive - { - /// - /// Initializes the class. - /// - public AsyncApiLong(long value) - : base(value) - { - } - - /// - /// Primitive type this object represents. - /// - public override PrimitiveType PrimitiveType { get; } = PrimitiveType.Long; - } -} diff --git a/src/LEGO.AsyncAPI/Models/Any/AsyncAPINull.cs b/src/LEGO.AsyncAPI/Models/Any/AsyncAPINull.cs deleted file mode 100644 index 18cfe97d..00000000 --- a/src/LEGO.AsyncAPI/Models/Any/AsyncAPINull.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Models.Any -{ - using LEGO.AsyncAPI.Models.Interfaces; - using LEGO.AsyncAPI.Writers; - - /// - /// AsyncApi null. - /// - public class AsyncApiNull : IAsyncApiAny - { - /// - /// The type of . - /// - public AnyType AnyType { get; } = AnyType.Null; - - /// - /// Write out null representation. - /// - /// - public void Write(IAsyncApiWriter writer) - { - writer.WriteAny(this); - } - } -} diff --git a/src/LEGO.AsyncAPI/Models/Any/AsyncAPIObject.cs b/src/LEGO.AsyncAPI/Models/Any/AsyncAPIObject.cs index fff2e840..7f87ac2e 100644 --- a/src/LEGO.AsyncAPI/Models/Any/AsyncAPIObject.cs +++ b/src/LEGO.AsyncAPI/Models/Any/AsyncAPIObject.cs @@ -1,20 +1,29 @@ // Copyright (c) The LEGO Group. All rights reserved. -namespace LEGO.AsyncAPI.Models.Any +namespace LEGO.AsyncAPI.Models { + using System; using System.Collections.Generic; + using System.Text.Json.Nodes; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Writers; /// /// AsyncApi object. /// - public class AsyncApiObject : Dictionary, IAsyncApiAny + [Obsolete("Please use AsyncApiAny instead")] + public class AsyncApiObject : Dictionary, IAsyncApiExtension, IAsyncApiElement { - /// - /// Type of . - /// - public AnyType AnyType { get; } = AnyType.Object; + public static implicit operator AsyncApiAny(AsyncApiObject obj) + { + var jObject = new JsonObject(); + foreach (var item in obj) + { + jObject.Add(item.Key, item.Value.GetNode()); + } + + return new AsyncApiAny(jObject); + } /// /// Serialize AsyncApiObject to writer. diff --git a/src/LEGO.AsyncAPI/Models/Any/AsyncAPIString.cs b/src/LEGO.AsyncAPI/Models/Any/AsyncAPIString.cs deleted file mode 100644 index 389545e2..00000000 --- a/src/LEGO.AsyncAPI/Models/Any/AsyncAPIString.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -namespace LEGO.AsyncAPI.Models.Any -{ - using LEGO.AsyncAPI.Models.Interfaces; - - /// - /// AsyncApi string type. - /// - public class AsyncApiString : AsyncApiPrimitive - { - private bool isExplicit; - private bool isRawString; - - /// - /// Initializes the class. - /// - /// - public AsyncApiString(string value) - : this(value, false) - { - } - - /// - /// Initializes the class. - /// - /// - /// Used to indicate if a string is quoted. - public AsyncApiString(string value, bool isExplicit) - : base(value) - { - this.isExplicit = isExplicit; - } - - /// - /// Initializes the class. - /// - /// - /// Used to indicate if a string is quoted. - /// Used to indicate to the writer that the value should be written without encoding. - public AsyncApiString(string value, bool isExplicit, bool isRawString) - : base(value) - { - this.isExplicit = isExplicit; - this.isRawString = isRawString; - } - - /// - /// The primitive class this object represents. - /// - public override PrimitiveType PrimitiveType { get; } = PrimitiveType.String; - - /// - /// True if string was specified explicitly by the means of double quotes, single quotes, or literal or folded style. - /// - public bool IsExplicit() - { - return this.isExplicit; - } - - /// - /// True if the writer should process the value as supplied without encoding. - /// - public bool IsRawString() - { - return this.isRawString; - } - } -} diff --git a/src/LEGO.AsyncAPI/Models/Any/AsyncApiAny.cs b/src/LEGO.AsyncAPI/Models/Any/AsyncApiAny.cs new file mode 100644 index 00000000..5c5f2b31 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/Any/AsyncApiAny.cs @@ -0,0 +1,154 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + using System.Collections.Generic; + using System.Text.Json; + using System.Text.Json.Nodes; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + + /// + /// AsyncApiAny. + /// + /// + /// + public class AsyncApiAny : IAsyncApiElement, IAsyncApiExtension + { + private JsonSerializerOptions options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + + private JsonNode node; + + /// + /// Initializes a new instance of the class. + /// + /// The node. + public AsyncApiAny(JsonNode node) + { + this.node = node; + } + + /// + /// Initializes a new instance of the class. + /// + /// The object. + public AsyncApiAny(object obj) + { + this.node = JsonNode.Parse(JsonSerializer.Serialize(obj, this.options)); + } + + /// + /// Initializes a new instance of the class. + /// + /// The node. + public AsyncApiAny(JsonArray node) + { + this.node = node; + } + + /// + /// Initializes a new instance of the class. + /// + /// The node. + public AsyncApiAny(JsonObject node) + { + this.node = node; + } + + /// + /// Converts to from an Extension. + /// + /// T. + /// The extension. + /// . + public static T FromExtensionOrDefault(IAsyncApiExtension extension) + { + if (extension is AsyncApiAny any) + { + return any.GetValueOrDefault(); + } + else + { + return default(T); + } + } + + /// + /// Gets the node. + /// + /// . + /// + /// The node. + /// + public JsonNode GetNode() => this.node; + + /// + /// Gets the value. + /// + /// . + /// . + public T GetValue() + { + if (this.node == null) + { + return default(T); + } + + if (this.node is JsonValue) + { + return this.node.GetValue(); + } + + return JsonSerializer.Deserialize(this.node.ToJsonString()); + } + + /// + /// Gets the value or default. + /// + /// . + /// or default. + public T GetValueOrDefault() + { + try + { + return this.GetValue(); + } + catch (System.Exception) + { + return default(T); + } + } + + /// + /// Tries the get value. + /// + /// . + /// The value. + /// true if the value could be converted, otherwise false. + public bool TryGetValue(out T value) + { + try + { + value = this.GetValue(); + return true; + } + catch (System.Exception) + { + value = default(T); + return false; + } + } + + /// + /// Writes the Any type. + /// + /// The writer. + public void Write(IAsyncApiWriter writer) + { + writer.WriteAny(new AsyncApiAny(this.node)); + } + } +} diff --git a/src/LEGO.AsyncAPI/Models/Any/AsyncApiBinary.cs b/src/LEGO.AsyncAPI/Models/Any/AsyncApiBinary.cs deleted file mode 100644 index d6f62fe1..00000000 --- a/src/LEGO.AsyncAPI/Models/Any/AsyncApiBinary.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Models.Any -{ - using LEGO.AsyncAPI.Models.Interfaces; - - /// - /// AsyncApi binary. - /// - public class AsyncApiBinary : AsyncApiPrimitive - { - /// - /// Initializes the class. - /// - /// - public AsyncApiBinary(byte[] value) - : base(value) - { - } - - /// - /// Primitive type this object represents. - /// - public override PrimitiveType PrimitiveType { get; } = PrimitiveType.Binary; - } -} diff --git a/src/LEGO.AsyncAPI/Models/Any/AsyncApiByte.cs b/src/LEGO.AsyncAPI/Models/Any/AsyncApiByte.cs deleted file mode 100644 index 511e3d7b..00000000 --- a/src/LEGO.AsyncAPI/Models/Any/AsyncApiByte.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Models.Any -{ - using LEGO.AsyncAPI.Models.Interfaces; - - /// - /// AsyncApi Byte - /// - public class AsyncApiByte : AsyncApiPrimitive - { - /// - /// Initializes the class. - /// - public AsyncApiByte(byte value) - : this(new byte[] { value }) - { - } - - /// - /// Initializes the class. - /// - public AsyncApiByte(byte[] value) - : base(value) - { - } - - /// - /// Primitive type this object represents. - /// - public override PrimitiveType PrimitiveType { get; } = PrimitiveType.Byte; - } -} diff --git a/src/LEGO.AsyncAPI/Models/Any/AsyncApiDate.cs b/src/LEGO.AsyncAPI/Models/Any/AsyncApiDate.cs deleted file mode 100644 index a4c3a063..00000000 --- a/src/LEGO.AsyncAPI/Models/Any/AsyncApiDate.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Models.Any -{ - using System; - using LEGO.AsyncAPI.Models.Interfaces; - - /// - /// AsyncApi Date - /// - public class AsyncApiDate : AsyncApiPrimitive - { - /// - /// Initializes the class. - /// - public AsyncApiDate(DateTime value) - : base(value) - { - } - - /// - /// Primitive type this object represents. - /// - public override PrimitiveType PrimitiveType { get; } = PrimitiveType.Date; - } -} diff --git a/src/LEGO.AsyncAPI/Models/Any/AsyncApiDateTime.cs b/src/LEGO.AsyncAPI/Models/Any/AsyncApiDateTime.cs deleted file mode 100644 index 1bf213c6..00000000 --- a/src/LEGO.AsyncAPI/Models/Any/AsyncApiDateTime.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Models.Any -{ - using System; - using LEGO.AsyncAPI.Models.Interfaces; - - /// - /// AsyncApi Datetime - /// - public class AsyncApiDateTime : AsyncApiPrimitive - { - /// - /// Initializes the class. - /// - public AsyncApiDateTime(DateTimeOffset value) - : base(value) - { - } - - /// - /// Primitive type this object represents. - /// - public override PrimitiveType PrimitiveType { get; } = PrimitiveType.DateTime; - } -} diff --git a/src/LEGO.AsyncAPI/Models/Any/AsyncApiFloat.cs b/src/LEGO.AsyncAPI/Models/Any/AsyncApiFloat.cs deleted file mode 100644 index 03ca5cb2..00000000 --- a/src/LEGO.AsyncAPI/Models/Any/AsyncApiFloat.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Models.Any -{ - using LEGO.AsyncAPI.Models.Interfaces; - - /// - /// AsyncApi Float - /// - public class AsyncApiFloat : AsyncApiPrimitive - { - /// - /// Initializes the class. - /// - public AsyncApiFloat(float value) - : base(value) - { - } - - /// - /// Primitive type this object represents. - /// - public override PrimitiveType PrimitiveType { get; } = PrimitiveType.Float; - } -} diff --git a/src/LEGO.AsyncAPI/Models/Any/AsyncApiInteger.cs b/src/LEGO.AsyncAPI/Models/Any/AsyncApiInteger.cs deleted file mode 100644 index 33d2b9d7..00000000 --- a/src/LEGO.AsyncAPI/Models/Any/AsyncApiInteger.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Models.Any -{ - using LEGO.AsyncAPI.Models.Interfaces; - - /// - /// AsyncApi Integer - /// - public class AsyncApiInteger : AsyncApiPrimitive - { - /// - /// Initializes the class. - /// - public AsyncApiInteger(int value) - : base(value) - { - } - - /// - /// Primitive type this object represents. - /// - public override PrimitiveType PrimitiveType { get; } = PrimitiveType.Integer; - } -} diff --git a/src/LEGO.AsyncAPI/Models/Any/AsyncApiPrimitive{T}.cs b/src/LEGO.AsyncAPI/Models/Any/AsyncApiPrimitive{T}.cs deleted file mode 100644 index 99ac381d..00000000 --- a/src/LEGO.AsyncAPI/Models/Any/AsyncApiPrimitive{T}.cs +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Models.Any -{ - using System; - using System.Text; - using LEGO.AsyncAPI.Models.Interfaces; - using LEGO.AsyncAPI.Writers; - - /// - /// AsyncApi primitive class. - /// - /// - public abstract class AsyncApiPrimitive : IAsyncApiPrimitive - { - /// - /// Initializes the class with the given value. - /// - /// - public AsyncApiPrimitive(T value) - { - this.Value = value; - } - - /// - /// The kind of . - /// - public AnyType AnyType { get; } = AnyType.Primitive; - - /// - /// The primitive class this object represents. - /// - public abstract PrimitiveType PrimitiveType { get; } - - /// - /// Value of this . - /// - public T Value { get; } - - /// - /// Write out content of primitive element. - /// - /// - public void Write(IAsyncApiWriter writer) - { - switch (this.PrimitiveType) - { - case PrimitiveType.Integer: - var intValue = (AsyncApiInteger)(IAsyncApiPrimitive)this; - writer.WriteValue(intValue.Value); - break; - - case PrimitiveType.Long: - var longValue = (AsyncApiLong)(IAsyncApiPrimitive)this; - writer.WriteValue(longValue.Value); - break; - - case PrimitiveType.Float: - var floatValue = (AsyncApiFloat)(IAsyncApiPrimitive)this; - writer.WriteValue(floatValue.Value); - break; - - case PrimitiveType.Double: - var doubleValue = (AsyncApiDouble)(IAsyncApiPrimitive)this; - writer.WriteValue(doubleValue.Value); - break; - - case PrimitiveType.String: - var stringValue = (AsyncApiString)(IAsyncApiPrimitive)this; - if (stringValue.IsRawString()) - { - writer.WriteRaw(stringValue.Value); - } - else - { - writer.WriteValue(stringValue.Value); - } - - break; - - case PrimitiveType.Byte: - var byteValue = (AsyncApiByte)(IAsyncApiPrimitive)this; - if (byteValue.Value == null) - { - writer.WriteNull(); - } - else - { - writer.WriteValue(Convert.ToBase64String(byteValue.Value)); - } - - break; - - case PrimitiveType.Binary: - var binaryValue = (AsyncApiBinary)(IAsyncApiPrimitive)this; - if (binaryValue.Value == null) - { - writer.WriteNull(); - } - else - { - writer.WriteValue(Encoding.UTF8.GetString(binaryValue.Value)); - } - - break; - - case PrimitiveType.Boolean: - var boolValue = (AsyncApiBoolean)(IAsyncApiPrimitive)this; - writer.WriteValue(boolValue.Value); - break; - - case PrimitiveType.Date: - var dateValue = (AsyncApiDate)(IAsyncApiPrimitive)this; - writer.WriteValue(dateValue.Value); - break; - - case PrimitiveType.DateTime: - var dateTimeValue = (AsyncApiDateTime)(IAsyncApiPrimitive)this; - writer.WriteValue(dateTimeValue.Value); - break; - - default: - throw new AsyncApiWriterException( - string.Format( - "The given primitive type '{0}' is not supported.", - this.PrimitiveType)); - } - } - } -} diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiBinding.cs b/src/LEGO.AsyncAPI/Models/AsyncApiBinding.cs new file mode 100644 index 00000000..4034b4e9 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/AsyncApiBinding.cs @@ -0,0 +1,41 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Bindings +{ + using System; + using System.Collections.Generic; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + + public abstract class AsyncApiBinding : IBinding + { + public abstract string BindingKey { get; } + + public bool UnresolvedReference { get; set; } + + public AsyncApiReference Reference { get; set; } + + public IDictionary Extensions { get; set; } = new Dictionary(); + + public string BindingVersion { get; set; } + + public void SerializeV2(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (this.Reference != null && !writer.GetSettings().ShouldInlineReference(this.Reference)) + { + this.Reference.SerializeV2(writer); + return; + } + + this.SerializeProperties(writer); + } + + public abstract void SerializeProperties(IAsyncApiWriter writer); + } +} diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiBindings.cs b/src/LEGO.AsyncAPI/Models/AsyncApiBindings{TBinding}.cs similarity index 83% rename from src/LEGO.AsyncAPI/Models/AsyncApiBindings.cs rename to src/LEGO.AsyncAPI/Models/AsyncApiBindings{TBinding}.cs index b2784755..f11858aa 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiBindings.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiBindings{TBinding}.cs @@ -4,11 +4,10 @@ namespace LEGO.AsyncAPI.Models { using System; using System.Collections.Generic; - using LEGO.AsyncAPI.Models.Bindings; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Writers; - public class AsyncApiBindings : Dictionary, IAsyncApiSerializable, IAsyncApiReferenceable + public class AsyncApiBindings : Dictionary, IAsyncApiReferenceable where TBinding : IBinding { public bool UnresolvedReference { get; set; } @@ -17,12 +16,11 @@ public class AsyncApiBindings : Dictionary, IAs public void Add(TBinding binding) { - this[binding.Type] = binding; + this[binding.BindingKey] = binding; } public void SerializeV2(IAsyncApiWriter writer) { - if (writer is null) { throw new ArgumentNullException(nameof(writer)); @@ -51,7 +49,7 @@ public void SerializeV2WithoutReference(IAsyncApiWriter writer) var bindingType = binding.Key; var bindingValue = binding.Value; - writer.WritePropertyName(bindingType.GetDisplayName()); + writer.WritePropertyName(bindingType); bindingValue.SerializeV2(writer); } diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiComponents.cs b/src/LEGO.AsyncAPI/Models/AsyncApiComponents.cs index 9e8e6e02..6c8a2a4c 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiComponents.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiComponents.cs @@ -69,22 +69,22 @@ public class AsyncApiComponents : IAsyncApiExtensible, IAsyncApiSerializable /// /// An object to hold reusable Server Bindings Objects. /// - public IDictionary ServerBindings { get; set; } = new Dictionary(); + public IDictionary> ServerBindings { get; set; } = new Dictionary>(); /// /// An object to hold reusable Channel Bindings Objects. /// - public IDictionary ChannelBindings { get; set; } = new Dictionary(); + public IDictionary> ChannelBindings { get; set; } = new Dictionary>(); /// /// An object to hold reusable Operation Bindings Objects. /// - public IDictionary OperationBindings { get; set; } = new Dictionary(); + public IDictionary> OperationBindings { get; set; } = new Dictionary>(); /// /// An object to hold reusable Message Bindings Objects. /// - public IDictionary MessageBindings { get; set; } = new Dictionary(); + public IDictionary> MessageBindings { get; set; } = new Dictionary>(); public IDictionary Extensions { get; set; } = new Dictionary(); @@ -311,7 +311,7 @@ public void SerializeV2(IAsyncApiWriter writer) (w, key, component) => { if (component.Reference != null && - component.Reference.Type == ReferenceType.ServerBinding && + component.Reference.Type == ReferenceType.ServerBindings && component.Reference.Id == key) { component.SerializeV2WithoutReference(w); @@ -329,7 +329,7 @@ public void SerializeV2(IAsyncApiWriter writer) (w, key, component) => { if (component.Reference != null && - component.Reference.Type == ReferenceType.ChannelBinding && + component.Reference.Type == ReferenceType.ChannelBindings && component.Reference.Id == key) { component.SerializeV2WithoutReference(w); @@ -347,7 +347,7 @@ public void SerializeV2(IAsyncApiWriter writer) (w, key, component) => { if (component.Reference != null && - component.Reference.Type == ReferenceType.OperationBinding && + component.Reference.Type == ReferenceType.OperationBindings && component.Reference.Id == key) { component.SerializeV2WithoutReference(w); @@ -365,7 +365,7 @@ public void SerializeV2(IAsyncApiWriter writer) (w, key, component) => { if (component.Reference != null && - component.Reference.Type == ReferenceType.MessageBinding && + component.Reference.Type == ReferenceType.MessageBindings && component.Reference.Id == key) { component.SerializeV2WithoutReference(w); diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiConstants.cs b/src/LEGO.AsyncAPI/Models/AsyncApiConstants.cs index 2d3ae13c..5806e8f9 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiConstants.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiConstants.cs @@ -132,11 +132,18 @@ public static class AsyncApiConstants public const string ServerVariables = "serverVariables"; public const string MessageId = "messageId"; public const string CleanupPolicy = "cleanup.policy"; - public const string RetentionMiliseconds = "retention.ms"; + public const string RetentionMilliseconds = "retention.ms"; public const string RetentionBytes = "retention.bytes"; - public const string DeleteRetentionMiliseconds = "delete.retention.ms"; + public const string DeleteRetentionMilliseconds = "delete.retention.ms"; public const string MaxMessageBytes = "max.message.bytes"; + public const string ConfluentKeySchemaValidation = "confluent.key.schema.validation"; + public const string ConfluentKeySubjectName = "confluent.key.subject.name.strategy"; + public const string ConfluentValueSchemaValidation = "confluent.value.schema.validation"; + public const string ConfluentValueSubjectName = "confluent.value.subject.name.strategy"; public const string TopicConfiguration = "topicConfiguration"; public const string GeoReplication = "geo-replication"; + public const string AdditionalItems = "additionalItems"; + public const string PropertyNames = "propertyNames"; + public const string PatternProperties = "patternProperties"; } } diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiDocument.cs b/src/LEGO.AsyncAPI/Models/AsyncApiDocument.cs index 1558dcc2..b6488883 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiDocument.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiDocument.cs @@ -6,8 +6,8 @@ namespace LEGO.AsyncAPI.Models using System.Collections.Generic; using LEGO.AsyncAPI.Exceptions; using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Services; using LEGO.AsyncAPI.Writers; - using Services; /// /// This is the root document object for the API specification. It combines resource listing and API declaration together into one document. @@ -46,7 +46,7 @@ public class AsyncApiDocument : IAsyncApiExtensible, IAsyncApiSerializable /// /// REQUIRED. The available channels and messages for the API. /// - public IDictionary Channels { get; set; } = new Dictionary(); + public IDictionary Channels { get; set; } /// /// an element to hold various schemas for the specification. @@ -145,12 +145,13 @@ public IEnumerable ResolveReferences() return resolver.Errors; } - internal T ResolveReference(AsyncApiReference reference) where T : class, IAsyncApiReferenceable + internal T ResolveReference(AsyncApiReference reference) + where T : class, IAsyncApiReferenceable { return this.ResolveReference(reference) as T; } - public IAsyncApiReferenceable ResolveReference(AsyncApiReference reference) + internal IAsyncApiReferenceable ResolveReference(AsyncApiReference reference) { if (reference == null) { @@ -189,13 +190,13 @@ public IAsyncApiReferenceable ResolveReference(AsyncApiReference reference) return this.Components.OperationTraits[reference.Id]; case ReferenceType.MessageTrait: return this.Components.MessageTraits[reference.Id]; - case ReferenceType.ServerBinding: + case ReferenceType.ServerBindings: return this.Components.ServerBindings[reference.Id]; - case ReferenceType.ChannelBinding: + case ReferenceType.ChannelBindings: return this.Components.ChannelBindings[reference.Id]; - case ReferenceType.OperationBinding: + case ReferenceType.OperationBindings: return this.Components.OperationBindings[reference.Id]; - case ReferenceType.MessageBinding: + case ReferenceType.MessageBindings: return this.Components.MessageBindings[reference.Id]; default: throw new AsyncApiException("Invalid reference type."); diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiMessageExample.cs b/src/LEGO.AsyncAPI/Models/AsyncApiMessageExample.cs index f4bfa083..3af20b39 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiMessageExample.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiMessageExample.cs @@ -15,12 +15,12 @@ public class AsyncApiMessageExample : IAsyncApiExtensible, IAsyncApiSerializable /// /// Gets or sets the value of this field MUST validate against the Message Object's headers field. /// - public IDictionary Headers { get; set; } = new Dictionary(); + public IDictionary Headers { get; set; } = new Dictionary(); /// /// Gets or sets the value of this field MUST validate against the Message Object's payload field. /// - public IAsyncApiAny Payload { get; set; } + public AsyncApiAny Payload { get; set; } /// /// a machine-friendly name. diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiOAuthFlow.cs b/src/LEGO.AsyncAPI/Models/AsyncApiOAuthFlow.cs index 77650dc1..5144fe10 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiOAuthFlow.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiOAuthFlow.cs @@ -37,7 +37,7 @@ public class AsyncApiOAuthFlow : IAsyncApiSerializable, IAsyncApiExtensible public IDictionary Extensions { get; set; } = new Dictionary(); /// - /// Serialize to Async Api v2.4 + /// Serialize to Async Api v2.4. /// public void SerializeV2(IAsyncApiWriter writer) { diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiOAuthFlows.cs b/src/LEGO.AsyncAPI/Models/AsyncApiOAuthFlows.cs index 17fabac7..a24f19ad 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiOAuthFlows.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiOAuthFlows.cs @@ -13,7 +13,7 @@ namespace LEGO.AsyncAPI.Models public class AsyncApiOAuthFlows : IAsyncApiSerializable, IAsyncApiExtensible { /// - /// Configuration for the OAuth Implicit flow + /// Configuration for the OAuth Implicit flow. /// public AsyncApiOAuthFlow Implicit { get; set; } @@ -38,7 +38,7 @@ public class AsyncApiOAuthFlows : IAsyncApiSerializable, IAsyncApiExtensible public IDictionary Extensions { get; set; } = new Dictionary(); /// - /// Serialize to Async Api v2.4 + /// Serialize to Async Api v2.4. /// public void SerializeV2(IAsyncApiWriter writer) { diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiOperation.cs b/src/LEGO.AsyncAPI/Models/AsyncApiOperation.cs index 5a55f387..ae3a6ff6 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiOperation.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiOperation.cs @@ -29,7 +29,7 @@ public class AsyncApiOperation : IAsyncApiSerializable, IAsyncApiExtensible public string Description { get; set; } /// - /// A declaration of which security mechanisms can be used with this server. The list of values includes alternative security requirement objects that can be used. Only one of the security requirement objects need to be satisfied to authorize a connection or operation + /// A declaration of which security mechanisms can be used with this server. The list of values includes alternative security requirement objects that can be used. Only one of the security requirement objects need to be satisfied to authorize a connection or operation. /// public IList Security { get; set; } = new List(); diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiReference.cs b/src/LEGO.AsyncAPI/Models/AsyncApiReference.cs index 2403b4ad..4f9660d1 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiReference.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiReference.cs @@ -11,6 +11,14 @@ namespace LEGO.AsyncAPI.Models /// public class AsyncApiReference : IAsyncApiSerializable { + /// + /// External resource in the reference. + /// It maybe: + /// 1. a absolute/relative file path, for example: ../commons/pet.json + /// 2. a Url, for example: http://localhost/pet.json + /// + public string ExternalResource { get; set; } + /// /// Gets or sets the element type referenced. /// @@ -27,17 +35,37 @@ public class AsyncApiReference : IAsyncApiSerializable public AsyncApiDocument HostDocument { get; set; } = null; /// - /// Gets the full reference string for v2.3. + /// Gets a flag indicating whether a file is a valid OpenAPI document or a fragment + /// + public bool IsFragment { get; set; } = false; + + /// + /// Gets a flag indicating whether this reference is an external reference. + /// + public bool IsExternal => this.ExternalResource != null; + + /// + /// Gets the full reference string for v2. /// public string Reference { get { + if (this.IsExternal) + { + return this.GetExternalReferenceV2(); + } + if (!this.Type.HasValue) { throw new ArgumentNullException(nameof(this.Type)); } + //if (this.Type == ReferenceType.SecurityScheme) + //{ + // return this.Id; + //} + return "#/components/" + this.Type.GetDisplayName() + "/" + this.Id; } } @@ -67,6 +95,21 @@ public void SerializeV2(IAsyncApiWriter writer) writer.WriteEndObject(); } + private string GetExternalReferenceV2() + { + if (this.Id != null) + { + if (this.IsFragment) + { + return this.ExternalResource + "#" + this.Id; + } + + return this.ExternalResource + "#/components/" + this.Type.GetDisplayName() + "/" + this.Id; + } + + return this.ExternalResource; + } + public void Write(IAsyncApiWriter writer) { this.SerializeV2(writer); diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiSchema.cs b/src/LEGO.AsyncAPI/Models/AsyncApiSchema.cs index b72b452b..0931c953 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiSchema.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiSchema.cs @@ -19,9 +19,9 @@ public class AsyncApiSchema : IAsyncApiReferenceable, IAsyncApiExtensible, IAsyn public string Title { get; set; } /// - /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html + /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public IList Type { get; set; } + public SchemaType? Type { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. @@ -37,22 +37,22 @@ public class AsyncApiSchema : IAsyncApiReferenceable, IAsyncApiExtensible, IAsyn /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public decimal? Maximum { get; set; } + public double? Maximum { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public bool? ExclusiveMaximum { get; set; } + public double? ExclusiveMaximum { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public decimal? Minimum { get; set; } + public double? Minimum { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public bool? ExclusiveMinimum { get; set; } + public double? ExclusiveMinimum { get; set; } /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. @@ -73,7 +73,7 @@ public class AsyncApiSchema : IAsyncApiReferenceable, IAsyncApiExtensible, IAsyn /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public decimal? MultipleOf { get; set; } + public double? MultipleOf { get; set; } /// /// Follow JSON Schema definition: https://tools.ietf.org/html/draft-fge-json-schema-validation-00 @@ -81,7 +81,7 @@ public class AsyncApiSchema : IAsyncApiReferenceable, IAsyncApiExtensible, IAsyn /// Unlike JSON Schema, the value MUST conform to the defined type for the Schema Object defined at the same level. /// For example, if type is string, then default can be "foo" but cannot be 1. /// - public IAsyncApiAny Default { get; set; } + public AsyncApiAny Default { get; set; } /// /// a value indicating whether relevant only for Schema "properties" definitions. Declares the property as "read only". @@ -159,6 +159,13 @@ public class AsyncApiSchema : IAsyncApiReferenceable, IAsyncApiExtensible, IAsyn /// public AsyncApiSchema Items { get; set; } + /// + /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html + /// Value MUST be an object and not an array. Inline or referenced schema MUST be of a Schema Object + /// and not a standard JSON Schema. items MUST be present if the type is array. + /// + public AsyncApiSchema AdditionalItems { get; set; } + /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// @@ -197,6 +204,8 @@ public class AsyncApiSchema : IAsyncApiReferenceable, IAsyncApiExtensible, IAsyn /// public AsyncApiSchema AdditionalProperties { get; set; } + public IDictionary PatternProperties { get; set; } = new Dictionary(); + /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// @@ -211,17 +220,17 @@ public class AsyncApiSchema : IAsyncApiReferenceable, IAsyncApiExtensible, IAsyn /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public IList Enum { get; set; } = new List(); + public IList Enum { get; set; } = new List(); /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public IList Examples { get; set; } = new List(); + public IList Examples { get; set; } = new List(); /// /// follow JSON Schema definition: https://json-schema.org/draft-07/json-schema-release-notes.html. /// - public IAsyncApiAny Const { get; set; } + public AsyncApiAny Const { get; set; } /// /// a value indicating whether allows sending a null value for the defined schema. Default value is false. @@ -257,13 +266,14 @@ public void SerializeV2WithoutReference(IAsyncApiWriter writer) // type if (this.Type != null) { - if (this.Type.Count == 1) + var types = EnumExtensions.GetFlags(this.Type.Value); + if (types.Count() == 1) { - writer.WriteOptionalProperty(AsyncApiConstants.Type, this.Type.First().GetDisplayName()); + writer.WriteOptionalProperty(AsyncApiConstants.Type, types.First().GetDisplayName()); } else { - writer.WriteOptionalCollection(AsyncApiConstants.Type, this.Type.Select(t => t.GetDisplayName()), (w, s) => w.WriteValue(s)); + writer.WriteOptionalCollection(AsyncApiConstants.Type, types.Select(t => t.GetDisplayName()), (w, s) => w.WriteValue(s)); } } @@ -334,7 +344,24 @@ public void SerializeV2WithoutReference(IAsyncApiWriter writer) writer.WriteOptionalCollection(AsyncApiConstants.Required, this.Required, (w, s) => w.WriteValue(s)); // items - writer.WriteOptionalObject(AsyncApiConstants.Items, this.Items, (w, s) => s.SerializeV2(w)); + if (this.Items is FalseApiSchema) + { + writer.WriteOptionalProperty(AsyncApiConstants.Items, false); + } + else + { + writer.WriteOptionalObject(AsyncApiConstants.Items, this.Items, (w, s) => s.SerializeV2(w)); + } + + // additionalItems + if (this.AdditionalItems is FalseApiSchema) + { + writer.WriteOptionalProperty(AsyncApiConstants.AdditionalItems, false); + } + else + { + writer.WriteOptionalObject(AsyncApiConstants.AdditionalItems, this.AdditionalItems, (w, s) => s.SerializeV2(w)); + } // maxItems writer.WriteOptionalProperty(AsyncApiConstants.MaxItems, this.MaxItems); @@ -355,7 +382,18 @@ public void SerializeV2WithoutReference(IAsyncApiWriter writer) writer.WriteOptionalProperty(AsyncApiConstants.MinProperties, this.MinProperties); // additionalProperties - writer.WriteOptionalObject(AsyncApiConstants.AdditionalProperties, this.AdditionalProperties, (w, s) => s.SerializeV2(w)); + if (this.AdditionalProperties is FalseApiSchema) + { + writer.WriteOptionalProperty(AsyncApiConstants.AdditionalProperties, false); + } + else + { + writer.WriteOptionalObject(AsyncApiConstants.AdditionalProperties, this.AdditionalProperties, (w, s) => s.SerializeV2(w)); + } + + writer.WriteOptionalMap(AsyncApiConstants.PatternProperties, this.PatternProperties, (w, s) => s.SerializeV2(w)); + + writer.WriteOptionalObject(AsyncApiConstants.PropertyNames, this.PropertyNames, (w, s) => s.SerializeV2(w)); // discriminator writer.WriteOptionalProperty(AsyncApiConstants.Discriminator, this.Discriminator); diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiSecurityRequirement.cs b/src/LEGO.AsyncAPI/Models/AsyncApiSecurityRequirement.cs index 889ce3bb..475497d2 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiSecurityRequirement.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiSecurityRequirement.cs @@ -44,7 +44,7 @@ public void SerializeV2(IAsyncApiWriter writer) continue; } - //securityScheme.SerializeV2(writer); + // securityScheme.SerializeV2(writer); writer.WritePropertyName(securityScheme.Reference.Id); writer.WriteStartArray(); diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiSecurityScheme.cs b/src/LEGO.AsyncAPI/Models/AsyncApiSecurityScheme.cs index 3374fb9f..08fd3d67 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiSecurityScheme.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiSecurityScheme.cs @@ -58,7 +58,7 @@ public class AsyncApiSecurityScheme : IAsyncApiSerializable, IAsyncApiReferencea public IDictionary Extensions { get; set; } = new Dictionary(); /// - /// Indicates if object is populated with data or is just a reference to the data + /// Indicates if object is populated with data or is just a reference to the data. /// public bool UnresolvedReference { get; set; } diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiSerializableExtensions.cs b/src/LEGO.AsyncAPI/Models/AsyncApiSerializableExtensions.cs index 1339fd6c..1d1bd35a 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiSerializableExtensions.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiSerializableExtensions.cs @@ -3,48 +3,78 @@ namespace LEGO.AsyncAPI.Models { using System; - using System.Globalization; using System.IO; using LEGO.AsyncAPI.Exceptions; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Writers; + /// + /// Contains extension methods for working with async api elements. + /// public static class AsyncApiSerializableExtensions { /// /// Serialize the to the AsyncApi document (JSON) using the given stream and specification version. /// - /// the + /// the . /// The AsyncApi element. /// The output stream. /// The AsyncApi specification version. public static void SerializeAsJson(this T element, Stream stream, AsyncApiVersion specificationVersion) where T : IAsyncApiSerializable { - element.Serialize(stream, specificationVersion, AsyncApiFormat.Json); + element.SerializeAsJson(stream, specificationVersion, AsyncApiWriterSettings.Default); + } + + /// + /// Serialize the to the AsyncApi document (JSON) using the given stream and specification version. + /// + /// the . + /// The AsyncApi element. + /// The output stream. + /// The AsyncApi specification version. + /// The settings used for writing + public static void SerializeAsJson(this T element, Stream stream, AsyncApiVersion specificationVersion, AsyncApiWriterSettings settings) + where T : IAsyncApiSerializable + { + element.Serialize(stream, specificationVersion, AsyncApiFormat.Json, settings); } /// /// Serializes the to the AsyncApi document (YAML) using the given stream and specification version. /// - /// the + /// the . /// The AsyncApi element. /// The output stream. /// The AsyncApi specification version. public static void SerializeAsYaml(this T element, Stream stream, AsyncApiVersion specificationVersion) where T : IAsyncApiSerializable { - element.Serialize(stream, specificationVersion, AsyncApiFormat.Yaml); + element.SerializeAsYaml(stream, specificationVersion, AsyncApiWriterSettings.Default); + } + + /// + /// Serializes the to the AsyncApi document (YAML) using the given stream and specification version. + /// + /// the . + /// The AsyncApi element. + /// The output stream. + /// The AsyncApi specification version. + /// The settings used for writing + public static void SerializeAsYaml(this T element, Stream stream, AsyncApiVersion specificationVersion, AsyncApiWriterSettings settings) + where T : IAsyncApiSerializable + { + element.Serialize(stream, specificationVersion, AsyncApiFormat.Yaml, settings); } /// /// Serializes the to the AsyncApi document using /// the given stream, specification version and the format. /// - /// the + /// the . /// The AsyncApi element. /// The given stream. - /// The AsyncApi specification version. + /// The AsyncApi specification version. /// The output format (JSON or YAML). public static void Serialize( this T element, @@ -53,19 +83,19 @@ public static void Serialize( AsyncApiFormat format) where T : IAsyncApiSerializable { - element.Serialize(stream, specificationVersion, format, null); + element.Serialize(stream, specificationVersion, format, new AsyncApiWriterSettings()); } /// /// Serializes the to the AsyncApi document using /// the given stream, specification version and the format. /// - /// the + /// the . /// The AsyncApi element. /// The given stream. /// The AsyncApi specification version. /// The output format (JSON or YAML). - /// Provide configuration settings for controlling writing output + /// Provide configuration settings for controlling writing output. public static void Serialize( this T element, Stream stream, @@ -79,7 +109,12 @@ public static void Serialize( throw new ArgumentNullException(nameof(stream)); } - var streamWriter = new FormattingStreamWriter(stream, CultureInfo.InvariantCulture); + if (settings is null) + { + throw new ArgumentNullException(nameof(settings)); + } + + var streamWriter = new FormattingStreamWriter(stream, settings.CultureInfo); IAsyncApiWriter writer = format switch { @@ -93,14 +128,14 @@ public static void Serialize( /// /// Serializes the to AsyncApi document using the given specification version and writer. /// - /// the + /// the . /// The AsyncApi element. /// The output writer. /// The specification version. /// /// element /// or - /// writer + /// writer. /// /// specification version '{specificationVersion}' is not supported. public static void Serialize(this T element, IAsyncApiWriter writer, AsyncApiVersion specificationVersion) @@ -119,18 +154,19 @@ public static void Serialize(this T element, IAsyncApiWriter writer, AsyncApi switch (specificationVersion) { case AsyncApiVersion.AsyncApi2_0: - element.SerializeV2(writer); - break; + element.SerializeV2(writer); + break; default: - throw new AsyncApiException($"specification version '{specificationVersion}' is not supported."); + throw new AsyncApiException($"specification version '{specificationVersion}' is not supported."); } + writer.Flush(); } /// /// Serializes the to the AsyncApi document as a string in JSON format. /// - /// the + /// the . /// The AsyncApi element. /// The AsyncApi specification version. public static string SerializeAsJson( @@ -144,7 +180,7 @@ public static string SerializeAsJson( /// /// Serializes the to the AsyncApi document as a string in YAML format. /// - /// the + /// the . /// The AsyncApi element. /// The AsyncApi specification version. public static string SerializeAsYaml( @@ -158,7 +194,7 @@ public static string SerializeAsYaml( /// /// Serializes the to the AsyncApi document as a string in the given format. /// - /// the + /// the . /// The AsyncApi element. /// The AsyncApi specification version. /// AsyncApi document format. diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiWriterExtensions.cs b/src/LEGO.AsyncAPI/Models/AsyncApiWriterExtensions.cs index 45e61cbf..c9a5c309 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiWriterExtensions.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiWriterExtensions.cs @@ -4,7 +4,7 @@ namespace LEGO.AsyncAPI.Models { using LEGO.AsyncAPI.Writers; - internal static class AsyncApiWriterExtensions + public static class AsyncApiWriterExtensions { internal static AsyncApiWriterSettings GetSettings(this IAsyncApiWriter asyncApiWriter) { diff --git a/src/LEGO.AsyncAPI/Models/BindingExtensions.cs b/src/LEGO.AsyncAPI/Models/BindingExtensions.cs new file mode 100644 index 00000000..3bcab20f --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/BindingExtensions.cs @@ -0,0 +1,62 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + using System; + using LEGO.AsyncAPI.Models.Interfaces; + + public static class BindingExtensions + { + public static bool TryGetValue(this AsyncApiBindings bindings, out TBinding binding) + where TBinding : class, IServerBinding + { + if (bindings.TryGetValue(Activator.CreateInstance().BindingKey, out var serverBinding)) + { + binding = serverBinding as TBinding; + return true; + } + + binding = default; + return false; + } + + public static bool TryGetValue(this AsyncApiBindings bindings, out TBinding binding) + where TBinding : class, IChannelBinding + { + if (bindings.TryGetValue(Activator.CreateInstance().BindingKey, out var channelBinding)) + { + binding = channelBinding as TBinding; + return true; + } + + binding = default; + return false; + } + + public static bool TryGetValue(this AsyncApiBindings bindings, out TBinding binding) + where TBinding : class, IOperationBinding + { + if (bindings.TryGetValue(Activator.CreateInstance().BindingKey, out var operationBinding)) + { + binding = operationBinding as TBinding; + return true; + } + + binding = default; + return false; + } + + public static bool TryGetValue(this AsyncApiBindings bindings, out TBinding binding) + where TBinding : class, IMessageBinding + { + if (bindings.TryGetValue(Activator.CreateInstance().BindingKey, out var messageBinding)) + { + binding = messageBinding as TBinding; + return true; + } + + binding = default; + return false; + } + } +} diff --git a/src/LEGO.AsyncAPI/Models/Bindings/BindingType.cs b/src/LEGO.AsyncAPI/Models/Bindings/BindingType.cs deleted file mode 100644 index b88d3d5d..00000000 --- a/src/LEGO.AsyncAPI/Models/Bindings/BindingType.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Models.Bindings -{ - using LEGO.AsyncAPI.Attributes; - - public enum BindingType - { - [Display("kafka")] - Kafka, - - [Display("http")] - Http, - - [Display("websockets")] - Websockets, - - [Display("pulsar")] - Pulsar, - } -} diff --git a/src/LEGO.AsyncAPI/Models/Bindings/Http/HttpMessageBinding.cs b/src/LEGO.AsyncAPI/Models/Bindings/Http/HttpMessageBinding.cs deleted file mode 100644 index eeb5d05f..00000000 --- a/src/LEGO.AsyncAPI/Models/Bindings/Http/HttpMessageBinding.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Models.Bindings.Http -{ - using System; - using System.Collections.Generic; - using LEGO.AsyncAPI.Models.Interfaces; - using LEGO.AsyncAPI.Writers; - - /// - /// Binding class for http messaging channels. - /// - public class HttpMessageBinding : IMessageBinding - { - - /// - /// A Schema object containing the definitions for HTTP-specific headers. This schema MUST be of type object and have a properties key. - /// - public AsyncApiSchema Headers { get; set; } - - /// - /// The version of this binding. If omitted, "latest" MUST be assumed. - /// - public string BindingVersion { get; set; } - - /// - /// Indicates if object is populated with data or is just a reference to the data - /// - public bool UnresolvedReference { get; set; } - - /// - /// Reference object. - /// - public AsyncApiReference Reference { get; set; } - - /// - /// Serialize to AsyncAPI V2 document without using reference. - /// - public void SerializeV2WithoutReference(IAsyncApiWriter writer) - { - if (writer is null) - { - throw new ArgumentNullException(nameof(writer)); - } - - writer.WriteStartObject(); - - writer.WriteOptionalObject(AsyncApiConstants.Headers, this.Headers, (w, h) => h.SerializeV2(w)); - writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); - - writer.WriteEndObject(); - } - - public void SerializeV2(IAsyncApiWriter writer) - { - if (writer is null) - { - throw new ArgumentNullException(nameof(writer)); - } - - if (this.Reference != null && !writer.GetSettings().ShouldInlineReference(this.Reference)) - { - this.Reference.SerializeV2(writer); - return; - } - - this.SerializeV2WithoutReference(writer); - } - - /// - public IDictionary Extensions { get; set; } = new Dictionary(); - - public BindingType Type => BindingType.Http; - } -} diff --git a/src/LEGO.AsyncAPI/Models/Bindings/Kafka/KafkaChannelBinding.cs b/src/LEGO.AsyncAPI/Models/Bindings/Kafka/KafkaChannelBinding.cs deleted file mode 100644 index 998c47b3..00000000 --- a/src/LEGO.AsyncAPI/Models/Bindings/Kafka/KafkaChannelBinding.cs +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Models.Bindings.Kafka -{ - using System; - using System.Collections.Generic; - using LEGO.AsyncAPI.Models.Interfaces; - using LEGO.AsyncAPI.Writers; - - /// - /// Binding class for Kafka channel settings. - /// - public class KafkaChannelBinding : IChannelBinding - { - /// - /// Kafka topic name if different from channel name. - /// - public string Topic { get; set; } - - /// - /// Number of partitions configured on this topic (useful to know how many parallel consumers you may run). - /// - public int? Partitions { get; set; } - - /// - /// Number of replicas configured on this topic. - /// - public int? Replicas { get; set; } - - /// - /// Topic configuration properties that are relevant for the API. - /// - public TopicConfigurationObject TopicConfiguration { get; set; } - - /// - /// The version of this binding. If omitted, "latest" MUST be assumed. - /// - public string BindingVersion { get; set; } - - public BindingType Type => BindingType.Kafka; - - public bool UnresolvedReference { get; set; } - - public AsyncApiReference Reference { get; set; } - - public IDictionary Extensions { get; set; } = new Dictionary(); - - /// - /// Serialize to AsyncAPI V2 document without using reference. - /// - public void SerializeV2WithoutReference(IAsyncApiWriter writer) - { - if (writer is null) - { - throw new ArgumentNullException(nameof(writer)); - } - - writer.WriteStartObject(); - writer.WriteOptionalProperty(AsyncApiConstants.Topic, this.Topic); - writer.WriteOptionalProperty(AsyncApiConstants.Partitions, this.Partitions); - writer.WriteOptionalProperty(AsyncApiConstants.Replicas, this.Replicas); - writer.WriteOptionalObject(AsyncApiConstants.TopicConfiguration, this.TopicConfiguration, (w, t) => t.Serialize(w)); - writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); - - writer.WriteEndObject(); - } - - public void SerializeV2(IAsyncApiWriter writer) - { - if (writer is null) - { - throw new ArgumentNullException(nameof(writer)); - } - - if (this.Reference != null && !writer.GetSettings().ShouldInlineReference(this.Reference)) - { - this.Reference.SerializeV2(writer); - return; - } - - this.SerializeV2WithoutReference(writer); - } - } -} diff --git a/src/LEGO.AsyncAPI/Models/Bindings/Kafka/KafkaOperationBinding.cs b/src/LEGO.AsyncAPI/Models/Bindings/Kafka/KafkaOperationBinding.cs deleted file mode 100644 index 85ec18d6..00000000 --- a/src/LEGO.AsyncAPI/Models/Bindings/Kafka/KafkaOperationBinding.cs +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Models.Bindings.Kafka -{ - using System; - using System.Collections.Generic; - using LEGO.AsyncAPI.Models.Interfaces; - using LEGO.AsyncAPI.Writers; - - /// - /// Binding class for Kafka operations. - /// - public class KafkaOperationBinding : IOperationBinding - { - /// - /// Id of the consumer group. - /// - public AsyncApiSchema GroupId { get; set; } - - /// - /// Id of the consumer inside a consumer group. - /// - public AsyncApiSchema ClientId { get; set; } - - /// - /// The version of this binding. If omitted, "latest" MUST be assumed. - /// - public string BindingVersion { get; set; } - - /// - public IDictionary Extensions { get; set; } = new Dictionary(); - - public bool UnresolvedReference { get; set; } - - public AsyncApiReference Reference { get; set; } - - public BindingType Type => BindingType.Kafka; - - /// - /// Serialize to AsyncAPI V2 document without using reference. - /// - public void SerializeV2WithoutReference(IAsyncApiWriter writer) - { - if (writer is null) - { - throw new ArgumentNullException(nameof(writer)); - } - - writer.WriteStartObject(); - writer.WriteOptionalObject(AsyncApiConstants.GroupId, this.GroupId, (w, h) => h.SerializeV2(w)); - writer.WriteOptionalObject(AsyncApiConstants.ClientId, this.ClientId, (w, h) => h.SerializeV2(w)); - writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); - - writer.WriteEndObject(); - } - - public void SerializeV2(IAsyncApiWriter writer) - { - if (writer is null) - { - throw new ArgumentNullException(nameof(writer)); - } - - if (this.Reference != null && !writer.GetSettings().ShouldInlineReference(this.Reference)) - { - this.Reference.SerializeV2(writer); - return; - } - - this.SerializeV2WithoutReference(writer); - } - } -} diff --git a/src/LEGO.AsyncAPI/Models/Bindings/Kafka/TopicConfigurationObject.cs b/src/LEGO.AsyncAPI/Models/Bindings/Kafka/TopicConfigurationObject.cs deleted file mode 100644 index 5a1ccffe..00000000 --- a/src/LEGO.AsyncAPI/Models/Bindings/Kafka/TopicConfigurationObject.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -using LEGO.AsyncAPI.Models.Interfaces; -using LEGO.AsyncAPI.Writers; -using System; -using System.Collections.Generic; - -namespace LEGO.AsyncAPI.Models.Bindings.Kafka -{ - public class TopicConfigurationObject : IAsyncApiElement - { - /// - /// The cleanup.policy configuration option. - /// - public List CleanupPolicy { get; set; } - - /// - /// The retention.ms configuration option. - /// - public int? RetentionMiliseconds { get; set; } - - /// - /// The retention.bytes configuration option. - /// - public int? RetentionBytes { get; set; } - - /// - /// The delete.retention.ms configuration option. - /// - public int? DeleteRetentionMiliseconds { get; set; } - - /// - /// The max.message.bytes configuration option. - /// - public int? MaxMessageBytes { get; set; } - - public void Serialize(IAsyncApiWriter writer) - { - if (writer is null) - { - throw new ArgumentNullException(nameof(writer)); - } - - writer.WriteStartObject(); - writer.WriteOptionalCollection(AsyncApiConstants.CleanupPolicy, this.CleanupPolicy, (w, s) => w.WriteValue(s)); - writer.WriteOptionalProperty(AsyncApiConstants.RetentionMiliseconds, this.RetentionMiliseconds); - writer.WriteOptionalProperty(AsyncApiConstants.RetentionBytes, this.RetentionBytes); - writer.WriteOptionalProperty(AsyncApiConstants.DeleteRetentionMiliseconds, this.DeleteRetentionMiliseconds); - writer.WriteOptionalProperty(AsyncApiConstants.MaxMessageBytes, this.MaxMessageBytes); - writer.WriteEndObject(); - } - } -} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/Bindings/Pulsar/PulsarServerBinding.cs b/src/LEGO.AsyncAPI/Models/Bindings/Pulsar/PulsarServerBinding.cs deleted file mode 100644 index 2fc0e15c..00000000 --- a/src/LEGO.AsyncAPI/Models/Bindings/Pulsar/PulsarServerBinding.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Models.Bindings.Pulsar -{ - using System; - using System.Collections.Generic; - using LEGO.AsyncAPI.Models.Interfaces; - using LEGO.AsyncAPI.Writers; - - /// - /// Binding class for Pulsar server settings. - /// - public class PulsarServerBinding : IServerBinding - { - /// - /// The pulsar tenant. If omitted, "public" must be assumed. - /// - public string Tenant { get; set; } - - /// - /// The version of this binding. - public string BindingVersion { get; set; } - - public BindingType Type => BindingType.Pulsar; - - public bool UnresolvedReference { get; set; } - - public AsyncApiReference Reference { get; set; } - - public IDictionary Extensions { get; set; } = new Dictionary(); - - /// - /// Serialize to AsyncAPI V2 document without using reference. - /// - public void SerializeV2WithoutReference(IAsyncApiWriter writer) - { - if (writer is null) - { - throw new ArgumentNullException(nameof(writer)); - } - - writer.WriteStartObject(); - - writer.WriteOptionalProperty(AsyncApiConstants.Tenant, this.Tenant); - writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); - - writer.WriteEndObject(); - } - - public void SerializeV2(IAsyncApiWriter writer) - { - if (writer is null) - { - throw new ArgumentNullException(nameof(writer)); - } - - if (this.Reference != null && !writer.GetSettings().ShouldInlineReference(this.Reference)) - { - this.Reference.SerializeV2(writer); - return; - } - - this.SerializeV2WithoutReference(writer); - } - } -} diff --git a/src/LEGO.AsyncAPI/Models/Interfaces/IAsyncApiAny.cs b/src/LEGO.AsyncAPI/Models/Interfaces/IAsyncApiAny.cs deleted file mode 100644 index dfabf5b3..00000000 --- a/src/LEGO.AsyncAPI/Models/Interfaces/IAsyncApiAny.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Models.Interfaces -{ - using LEGO.AsyncAPI.Models.Any; - - /// - /// Base interface for all the types that represent AsyncAPI Any. - /// - public interface IAsyncApiAny : IAsyncApiElement, IAsyncApiExtension - { - /// - /// Gets type of an . - /// - AnyType AnyType { get; } - } -} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/Interfaces/IAsyncApiPrimitive.cs b/src/LEGO.AsyncAPI/Models/Interfaces/IAsyncApiPrimitive.cs deleted file mode 100644 index 618a66bd..00000000 --- a/src/LEGO.AsyncAPI/Models/Interfaces/IAsyncApiPrimitive.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Models.Interfaces -{ - /// - /// Primitive type. - /// - public enum PrimitiveType - { - /// - /// Integer - /// - Integer, - - /// - /// Long - /// - Long, - - /// - /// Float - /// - Float, - - /// - /// Double - /// - Double, - - /// - /// String - /// - String, - - /// - /// Byte - /// - Byte, - - /// - /// Binary - /// - Binary, - - /// - /// Boolean - /// - Boolean, - - /// - /// Date - /// - Date, - - /// - /// DateTime - /// - DateTime, - } - - /// - /// Base interface for the Primitive type. - /// - public interface IAsyncApiPrimitive : IAsyncApiAny - { - /// - /// Primitive type. - /// - PrimitiveType PrimitiveType { get; } - } -} diff --git a/src/LEGO.AsyncAPI/Models/Interfaces/IAsyncApiReferenceable.cs b/src/LEGO.AsyncAPI/Models/Interfaces/IAsyncApiReferenceable.cs index c3cdd7fa..9f0bc64c 100644 --- a/src/LEGO.AsyncAPI/Models/Interfaces/IAsyncApiReferenceable.cs +++ b/src/LEGO.AsyncAPI/Models/Interfaces/IAsyncApiReferenceable.cs @@ -7,7 +7,7 @@ namespace LEGO.AsyncAPI.Models.Interfaces public interface IAsyncApiReferenceable : IAsyncApiSerializable { /// - /// Indicates if object is populated with data or is just a reference to the data + /// Indicates if object is populated with data or is just a reference to the data. /// bool UnresolvedReference { get; set; } diff --git a/src/LEGO.AsyncAPI/Models/Interfaces/IBinding.cs b/src/LEGO.AsyncAPI/Models/Interfaces/IBinding.cs index c427db45..f39bc06f 100644 --- a/src/LEGO.AsyncAPI/Models/Interfaces/IBinding.cs +++ b/src/LEGO.AsyncAPI/Models/Interfaces/IBinding.cs @@ -1,14 +1,12 @@ // Copyright (c) The LEGO Group. All rights reserved. namespace LEGO.AsyncAPI.Models.Interfaces { - using LEGO.AsyncAPI.Models.Bindings; - /// /// Describes a protocol-specific binding. /// - public interface IBinding : IAsyncApiReferenceable, IAsyncApiExtensible + public interface IBinding : IAsyncApiSerializable, IAsyncApiExtensible { - public BindingType Type { get; } + public string BindingKey { get; } public string BindingVersion { get; set; } } diff --git a/src/LEGO.AsyncAPI/Models/JsonSchema/FalseApiSchema.cs b/src/LEGO.AsyncAPI/Models/JsonSchema/FalseApiSchema.cs new file mode 100644 index 00000000..01f313e5 --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/JsonSchema/FalseApiSchema.cs @@ -0,0 +1,12 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + /// + /// An object representing 'false' for properties of AsyncApiSchema that can be false OR a schema. + /// + /// + public class FalseApiSchema : AsyncApiSchema + { + } +} diff --git a/src/LEGO.AsyncAPI/Models/ReferenceType.cs b/src/LEGO.AsyncAPI/Models/ReferenceType.cs index 8195e197..8903dd73 100644 --- a/src/LEGO.AsyncAPI/Models/ReferenceType.cs +++ b/src/LEGO.AsyncAPI/Models/ReferenceType.cs @@ -56,22 +56,22 @@ public enum ReferenceType /// /// ServerBindings item. /// - [Display("serverBindings")] ServerBinding, + [Display("serverBindings")] ServerBindings, /// /// ChannelBindings item. /// - [Display("channelBindings")] ChannelBinding, + [Display("channelBindings")] ChannelBindings, /// /// OperationBindings item. /// - [Display("operationBindings")] OperationBinding, + [Display("operationBindings")] OperationBindings, /// /// MessageBindings item. /// - [Display("messageBindings")] MessageBinding, + [Display("messageBindings")] MessageBindings, /// /// Examples item. @@ -82,6 +82,10 @@ public enum ReferenceType /// Headers item. /// [Display("headers")] Header, - ServerVariable, + + /// + /// The server variable + /// + [Display("serverVariable")] ServerVariable, } -} \ No newline at end of file +} diff --git a/src/LEGO.AsyncAPI/Models/RuntimeExpressionAnyWrapper.cs b/src/LEGO.AsyncAPI/Models/RuntimeExpressionAnyWrapper.cs deleted file mode 100644 index 1e9817af..00000000 --- a/src/LEGO.AsyncAPI/Models/RuntimeExpressionAnyWrapper.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Models -{ - using LEGO.AsyncAPI.Expressions; - using LEGO.AsyncAPI.Models.Interfaces; - using LEGO.AsyncAPI.Writers; - - /// - /// The wrapper either for or - /// - public class RuntimeExpressionAnyWrapper : IAsyncApiElement - { - private IAsyncApiAny any; - private RuntimeExpression expression; - - /// - /// Gets/Sets the - /// - public IAsyncApiAny Any - { - get - { - return this.any; - } - - set - { - this.expression = null; - this.any = value; - } - } - - /// - /// Gets/Set the - /// - public RuntimeExpression Expression - { - get - { - return this.expression; - } - - set - { - this.any = null; - this.expression = value; - } - } - - /// - /// Write - /// - public void WriteValue(IAsyncApiWriter writer) - { - if (writer == null) - { - throw Error.ArgumentNull(nameof(writer)); - } - - if (this.any != null) - { - writer.WriteAny(this.any); - } - else if (this.expression != null) - { - writer.WriteValue(this.expression.Expression); - } - } - } -} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Models/SchemaType.cs b/src/LEGO.AsyncAPI/Models/SchemaType.cs index 5951d7b5..33e56e22 100644 --- a/src/LEGO.AsyncAPI/Models/SchemaType.cs +++ b/src/LEGO.AsyncAPI/Models/SchemaType.cs @@ -2,29 +2,31 @@ namespace LEGO.AsyncAPI.Models { + using System; using LEGO.AsyncAPI.Attributes; + [Flags] public enum SchemaType { [Display("null")] - Null, + Null = 1, [Display("boolean")] - Boolean, + Boolean = 2, [Display("object")] - Object, + Object = 4, [Display("array")] - Array, + Array = 8, [Display("number")] - Number, + Number = 16, [Display("string")] - String, + String = 32, [Display("integer")] - Integer, + Integer = 64, } } diff --git a/src/LEGO.AsyncAPI/Models/SchemaTypeHelpers.cs b/src/LEGO.AsyncAPI/Models/SchemaTypeHelpers.cs new file mode 100644 index 00000000..a24a00fb --- /dev/null +++ b/src/LEGO.AsyncAPI/Models/SchemaTypeHelpers.cs @@ -0,0 +1,20 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Models +{ + using System.Collections.Generic; + + public static class SchemaTypeHelpers + { + public static IEnumerable GetFlags(SchemaType input) + { + foreach (SchemaType value in System.Enum.GetValues(input.GetType())) + { + if (input.HasFlag(value)) + { + yield return value; + } + } + } + } +} diff --git a/src/LEGO.AsyncAPI/Models/SecuritySchemeType.cs b/src/LEGO.AsyncAPI/Models/SecuritySchemeType.cs index 02fec71b..d910832c 100644 --- a/src/LEGO.AsyncAPI/Models/SecuritySchemeType.cs +++ b/src/LEGO.AsyncAPI/Models/SecuritySchemeType.cs @@ -27,51 +27,51 @@ public enum SecuritySchemeType /// /// Symmetric Encryption. /// - [Display("symmetricEncryption")]SymmetricEncryption, + [Display("symmetricEncryption")] SymmetricEncryption, /// /// Asymmetric Encryption. /// - [Display("asymmetricEncryption")]AsymmetricEncryption, + [Display("asymmetricEncryption")] AsymmetricEncryption, /// /// Api Key. /// - [Display("httpApiKey")]HttpApiKey, + [Display("httpApiKey")] HttpApiKey, /// /// Basic or Bearer token authorization header. /// - [Display("http")]Http, + [Display("http")] Http, /// /// OAuth2. /// - [Display("oauth2")]OAuth2, + [Display("oauth2")] OAuth2, /// /// OIDC. /// - [Display("openIdConnect")]OpenIdConnect, + [Display("openIdConnect")] OpenIdConnect, /// /// Plain. /// - [Display("plain")]Plain, + [Display("plain")] Plain, /// /// Sha256. /// - [Display("scramSha256")]ScramSha256, + [Display("scramSha256")] ScramSha256, /// /// Sha512. /// - [Display("scramSha512")]ScramSha512, + [Display("scramSha512")] ScramSha512, /// /// GssApi. /// - [Display("gssapi")]Gssapi, + [Display("gssapi")] Gssapi, } } \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Services/AsyncApiReferenceResolver.cs b/src/LEGO.AsyncAPI/Services/AsyncApiReferenceResolver.cs index 85f2d4da..b3c693e0 100644 --- a/src/LEGO.AsyncAPI/Services/AsyncApiReferenceResolver.cs +++ b/src/LEGO.AsyncAPI/Services/AsyncApiReferenceResolver.cs @@ -5,12 +5,12 @@ namespace LEGO.AsyncAPI.Services using System; using System.Collections.Generic; using System.Linq; - using Exceptions; - using Models; - using Models.Interfaces; + using LEGO.AsyncAPI.Exceptions; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Models.Interfaces; /// - /// This class is used to walk an AsyncApiDocument and convert unresolved references to references to populated objects + /// This class is used to walk an AsyncApiDocument and convert unresolved references to references to populated objects. /// internal class AsyncApiReferenceResolver : AsyncApiVisitorBase { @@ -64,8 +64,7 @@ public override void Visit(AsyncApiDocument doc) public override void Visit(AsyncApiChannel channel) { this.ResolveMap(channel.Parameters); - var bindingDictionary = channel.Bindings.Select(binding => binding.Value).ToDictionary(x => x.Type.GetDisplayName()); - this.ResolveMap(bindingDictionary); + this.ResolveObject(channel.Bindings, r => channel.Bindings = r); } public override void Visit(AsyncApiMessageTrait trait) @@ -81,8 +80,7 @@ public override void Visit(AsyncApiOperation operation) { this.ResolveList(operation.Message); this.ResolveList(operation.Traits); - var bindingDictionary = operation.Bindings.Select(binding => binding.Value).ToDictionary(x => x.Type.GetDisplayName()); - this.ResolveMap(bindingDictionary); + this.ResolveObject(operation.Bindings, r => operation.Bindings = r); } public override void Visit(AsyncApiMessage message) @@ -91,19 +89,12 @@ public override void Visit(AsyncApiMessage message) this.ResolveObject(message.Payload, r => message.Payload = r); this.ResolveList(message.Traits); this.ResolveObject(message.CorrelationId, r => message.CorrelationId = r); - var bindingDictionary = message.Bindings.Select(binding => binding.Value).ToDictionary(x => x.Type.GetDisplayName()); - this.ResolveMap(bindingDictionary); + this.ResolveObject(message.Bindings, r => message.Bindings = r); } - /// - /// Resolve all references to bindings. - /// - public override void Visit(AsyncApiBindings bindings) + public override void Visit(AsyncApiServer server) { - foreach (var binding in bindings.Values.ToList()) - { - this.ResolveObject(binding, resolvedBinding => bindings[binding.Type] = resolvedBinding); - } + this.ResolveObject(server.Bindings, r => server.Bindings = r); } /// @@ -128,7 +119,7 @@ public override void Visit(AsyncApiSecurityRequirement securityRequirement) } /// - /// Resolve all references to parameters + /// Resolve all references to parameters. /// public override void Visit(IList parameters) { @@ -136,7 +127,7 @@ public override void Visit(IList parameters) } /// - /// Resolve all references used in a parameter + /// Resolve all references used in a parameter. /// public override void Visit(AsyncApiParameter parameter) { @@ -144,7 +135,7 @@ public override void Visit(AsyncApiParameter parameter) } /// - /// Resolve all references used in a schema + /// Resolve all references used in a schema. /// public override void Visit(AsyncApiSchema schema) { @@ -163,7 +154,8 @@ public override void Visit(AsyncApiSchema schema) this.ResolveMap(schema.Properties); } - private void ResolveObject(T entity, Action assign) where T : class, IAsyncApiReferenceable + private void ResolveObject(T entity, Action assign) + where T : class, IAsyncApiReferenceable, new() { if (entity == null) { @@ -176,7 +168,8 @@ private void ResolveObject(T entity, Action assign) where T : class, IAsyn } } - private void ResolveList(IList list) where T : class, IAsyncApiReferenceable, new() + private void ResolveList(IList list) + where T : class, IAsyncApiReferenceable, new() { if (list == null) { @@ -193,7 +186,8 @@ private void ResolveObject(T entity, Action assign) where T : class, IAsyn } } - private void ResolveMap(IDictionary map) where T : class, IAsyncApiReferenceable + private void ResolveMap(IDictionary map) + where T : class, IAsyncApiReferenceable, new() { if (map == null) { @@ -210,11 +204,27 @@ private void ResolveMap(IDictionary map) where T : class, IAsyncAp } } - private T ResolveReference(AsyncApiReference reference) where T : class, IAsyncApiReferenceable + private T ResolveReference(AsyncApiReference reference) + where T : class, IAsyncApiReferenceable, new() { + if (reference.IsExternal) + { + return new() + { + UnresolvedReference = true, + Reference = reference, + }; + } + try { - return this.currentDocument.ResolveReference(reference) as T; + var resolvedReference = this.currentDocument.ResolveReference(reference) as T; + if (resolvedReference == null) + { + throw new AsyncApiException($"Cannot resolve reference '{reference.Reference}' to '{typeof(T).Name}'."); + } + + return resolvedReference; } catch (AsyncApiException ex) { diff --git a/src/LEGO.AsyncAPI/Services/AsyncApiVisitorBase.cs b/src/LEGO.AsyncAPI/Services/AsyncApiVisitorBase.cs index 9519f3ba..899731d0 100644 --- a/src/LEGO.AsyncAPI/Services/AsyncApiVisitorBase.cs +++ b/src/LEGO.AsyncAPI/Services/AsyncApiVisitorBase.cs @@ -7,22 +7,23 @@ namespace LEGO.AsyncAPI.Services using System.Linq; using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Models.Interfaces; + /// - /// AsyncApi visitor base provides common logic for concrete visitors + /// AsyncApi visitor base provides common logic for concrete visitors. /// public abstract class AsyncApiVisitorBase { private readonly Stack path = new Stack(); /// - /// Properties available to identify context of where an object is within AsyncApi Document + /// Properties available to identify context of where an object is within AsyncApi Document. /// public CurrentKeys CurrentKeys { get; } = new CurrentKeys(); /// /// Allow Rule to indicate validation error occured at a deeper context level. /// - /// Identifier for context + /// Identifier for context. public void Enter(string segment) { this.path.Push(segment); @@ -37,80 +38,82 @@ public void Exit() } /// - /// Pointer to source of validation error in document + /// Pointer to source of validation error in document. /// public string PathString { get { - return "#/" + String.Join("/", this.path.Reverse()); + return "#/" + string.Join("/", this.path.Reverse()); } } /// - /// Visits + /// Visits . /// public virtual void Visit(AsyncApiDocument doc) { } - public virtual void Visit(IDictionary anys) - { } + public virtual void Visit(IDictionary anys) + { + } public virtual void Visit(IList traits) - { } + { + } + /// - /// Visits + /// Visits . /// public virtual void Visit(AsyncApiInfo info) { } /// - /// Visits + /// Visits . /// public virtual void Visit(AsyncApiContact contact) { } - /// - /// Visits + /// Visits . /// public virtual void Visit(AsyncApiLicense license) { } /// - /// Visits list of + /// Visits list of . /// public virtual void Visit(IList servers) { } /// - /// Visits + /// Visits . /// public virtual void Visit(AsyncApiServer server) { } /// - /// Visits + /// Visits . /// public virtual void Visit(AsyncApiServerVariable serverVariable) { } /// - /// Visits + /// Visits . /// public virtual void Visit(AsyncApiOperation operation) { } /// - /// Visits list of + /// Visits list of . /// public virtual void Visit(IList parameters) { @@ -121,29 +124,28 @@ public virtual void Visit(IDictionary parameters) } /// - /// Visits + /// Visits . /// public virtual void Visit(AsyncApiParameter parameter) { } /// - /// Visits + /// Visits . /// public virtual void Visit(AsyncApiComponents components) { } - /// - /// Visits + /// Visits . /// public virtual void Visit(AsyncApiExternalDocumentation externalDocs) { } /// - /// Visits + /// Visits . /// public virtual void Visit(AsyncApiSchema schema) { @@ -158,86 +160,80 @@ public virtual void Visit(IList messages) } /// - /// Visits + /// Visits . /// public virtual void Visit(AsyncApiTag tag) { } /// - /// Visits + /// Visits . /// public virtual void Visit(AsyncApiOAuthFlow asyncApiOAuthFlow) { } /// - /// Visits - /// - public virtual void Visit(AsyncApiBindings bindings) - where TBinding : class, IBinding - { - } - - /// - /// Visits + /// Visits . /// public virtual void Visit(AsyncApiSecurityRequirement securityRequirement) { } /// - /// Visits + /// Visits . /// public virtual void Visit(AsyncApiSecurityScheme securityScheme) { } /// - /// Visits list of + /// Visits list of . /// - public virtual void Visit(IList AsyncApiTags) + public virtual void Visit(IList asyncApiTags) { } /// - /// Visits list of + /// Visits list of . /// - public virtual void Visit(IList AsyncApiSecurityRequirements) + public virtual void Visit(IList asyncApiSecurityRequirements) { } /// - /// Visits + /// Visits . /// - public virtual void Visit(IAsyncApiExtensible AsyncApiExtensible) + public virtual void Visit(IAsyncApiExtensible asyncApiExtensible) { } public virtual void Visit(AsyncApiCorrelationId correlationId) - { } + { + } public virtual void Visit(AsyncApiMessageTrait trait) - { } + { + } /// - /// Visits + /// Visits . /// - public virtual void Visit(IAsyncApiExtension AsyncApiExtension) + public virtual void Visit(IAsyncApiExtension asyncApiExtension) { } /// - /// Visits a dictionary of server variables + /// Visits a dictionary of server variables. /// public virtual void Visit(IDictionary serverVariables) { } /// - /// Visits IAsyncApiReferenceable instances that are references and not in components + /// Visits IAsyncApiReferenceable instances that are references and not in components. /// - /// referenced object + /// referenced object. public virtual void Visit(IAsyncApiReferenceable referencable) { } @@ -250,6 +246,38 @@ public virtual void Visit(IDictionary channels) { } + public virtual void Visit(AsyncApiBindings bindings) + { + } + + public virtual void Visit(IServerBinding binding) + { + } + + public virtual void Visit(AsyncApiBindings bindings) + { + } + + public virtual void Visit(IChannelBinding binding) + { + } + + public virtual void Visit(AsyncApiBindings bindings) + { + } + + public virtual void Visit(IOperationBinding binding) + { + } + + public virtual void Visit(AsyncApiBindings bindings) + { + } + + public virtual void Visit(IMessageBinding binding) + { + } + public virtual void Visit(AsyncApiChannel channel) { } diff --git a/src/LEGO.AsyncAPI/Services/AsyncApiWalker.cs b/src/LEGO.AsyncAPI/Services/AsyncApiWalker.cs index c3ea6af8..844ab7e9 100644 --- a/src/LEGO.AsyncAPI/Services/AsyncApiWalker.cs +++ b/src/LEGO.AsyncAPI/Services/AsyncApiWalker.cs @@ -73,6 +73,50 @@ internal void Walk(AsyncApiComponents components) } }); + this.Walk(AsyncApiConstants.ServerBindings, () => + { + if (components.ServerBindings != null) + { + foreach (var item in components.ServerBindings) + { + this.Walk(item.Key, () => this.Walk(item.Value, isComponent: true)); + } + } + }); + + this.Walk(AsyncApiConstants.ChannelBindings, () => + { + if (components.ChannelBindings != null) + { + foreach (var item in components.ChannelBindings) + { + this.Walk(item.Key, () => this.Walk(item.Value, isComponent: true)); + } + } + }); + + this.Walk(AsyncApiConstants.OperationBindings, () => + { + if (components.OperationBindings != null) + { + foreach (var item in components.OperationBindings) + { + this.Walk(item.Key, () => this.Walk(item.Value, isComponent: true)); + } + } + }); + + this.Walk(AsyncApiConstants.MessageBindings, () => + { + if (components.MessageBindings != null) + { + foreach (var item in components.MessageBindings) + { + this.Walk(item.Key, () => this.Walk(item.Value, isComponent: true)); + } + } + }); + this.Walk(AsyncApiConstants.Parameters, () => { if (components.Parameters != null) @@ -375,6 +419,17 @@ internal void Walk(AsyncApiSchema schema, bool isComponent = false) this.Walk("additionalProperties", () => this.Walk(schema.AdditionalProperties)); } + if (schema.PatternProperties != null) + { + this.Walk("patternProperties", () => + { + foreach (var item in schema.PatternProperties) + { + this.Walk(item.Key, () => this.Walk(item.Value)); + } + }); + } + if (schema.PropertyNames != null) { this.Walk("propertyNames", () => this.Walk(schema.PropertyNames)); @@ -532,48 +587,120 @@ internal void Walk(AsyncApiMessageTrait trait, bool isComponent = false) this.Walk(trait as IAsyncApiExtensible); } - internal void Walk(AsyncApiBindings serverBindings) + internal void Walk(AsyncApiBindings serverBindings, bool isComponent = false) { - if (serverBindings is null) + if (serverBindings == null || this.ProcessAsReference(serverBindings, isComponent)) { return; } this.visitor.Visit(serverBindings); - this.Walk(serverBindings as IAsyncApiExtensible); + if (serverBindings != null) + { + foreach (var binding in serverBindings) + { + this.visitor.CurrentKeys.ServerBinding = binding.Key; + this.Walk(binding.Key, () => this.Walk(binding.Value)); + this.visitor.CurrentKeys.ServerBinding = null; + } + } } - internal void Walk(AsyncApiBindings channelBindings) + internal void Walk(IServerBinding binding) { - if (channelBindings is null) + if (binding == null) + { + return; + } + + this.visitor.Visit(binding); + } + + internal void Walk(AsyncApiBindings channelBindings, bool isComponent = false) + { + if (channelBindings == null || this.ProcessAsReference(channelBindings, isComponent)) { return; } this.visitor.Visit(channelBindings); - this.Walk(channelBindings as IAsyncApiExtensible); + if (channelBindings != null) + { + foreach (var binding in channelBindings) + { + this.visitor.CurrentKeys.ChannelBinding = binding.Key; + this.Walk(binding.Key, () => this.Walk(binding.Value)); + this.visitor.CurrentKeys.ChannelBinding = null; + } + } + } + + internal void Walk(IChannelBinding binding) + { + if (binding == null) + { + return; + } + + this.visitor.Visit(binding); } - internal void Walk(AsyncApiBindings operationBindings) + internal void Walk(AsyncApiBindings operationBindings, bool isComponent = false) { - if (operationBindings is null) + if (operationBindings == null || this.ProcessAsReference(operationBindings, isComponent)) { return; } this.visitor.Visit(operationBindings); - this.Walk(operationBindings as IAsyncApiExtensible); + if (operationBindings != null) + { + foreach (var binding in operationBindings) + { + this.visitor.CurrentKeys.OperationBinding = binding.Key; + this.Walk(binding.Key, () => this.Walk(binding.Value)); + this.visitor.CurrentKeys.OperationBinding = null; + } + } } - internal void Walk(AsyncApiBindings messageBindings) + internal void Walk(IOperationBinding binding) { - if (messageBindings is null) + if (binding == null) + { + return; + } + + this.visitor.Visit(binding); + } + + internal void Walk(AsyncApiBindings messageBindings, bool isComponent = false) + { + if (messageBindings == null || this.ProcessAsReference(messageBindings, isComponent)) { return; } this.visitor.Visit(messageBindings); - this.Walk(messageBindings as IAsyncApiExtensible); + if (messageBindings != null) + { + foreach (var binding in messageBindings) + { + this.visitor.CurrentKeys.MessageBinding = binding.Key; + this.Walk(binding.Key, () => this.Walk(binding.Value)); + this.visitor.CurrentKeys.MessageBinding = null; + } + } + } + + internal void Walk(IMessageBinding binding) + { + if (binding == null) + { + return; + } + + this.visitor.Visit(binding); } internal void Walk(IList examples) @@ -612,7 +739,7 @@ internal void Walk(AsyncApiMessageExample example) this.Walk(example as IAsyncApiExtensible); } - internal void Walk(IDictionary anys) + internal void Walk(IDictionary anys) { if (anys == null) { @@ -703,7 +830,6 @@ internal void Walk(AsyncApiServer server, bool isComponent = false) this.visitor.Visit(server); this.Walk(AsyncApiConstants.Variables, () => this.Walk(server.Variables)); this.Walk(AsyncApiConstants.Security, () => this.Walk(server.Security)); - this.Walk(AsyncApiConstants.Bindings, () => this.Walk(server.Bindings)); this.visitor.Visit(server as IAsyncApiExtensible); } @@ -828,7 +954,7 @@ internal void Walk(AsyncApiContact contact) this.visitor.Visit(contact); } - internal void Walk(IAsyncApiAny any) + internal void Walk(AsyncApiAny any) { if (any == null) { diff --git a/src/LEGO.AsyncAPI/Services/CurrentKeys.cs b/src/LEGO.AsyncAPI/Services/CurrentKeys.cs index f070a15c..f610aabb 100644 --- a/src/LEGO.AsyncAPI/Services/CurrentKeys.cs +++ b/src/LEGO.AsyncAPI/Services/CurrentKeys.cs @@ -4,6 +4,14 @@ namespace LEGO.AsyncAPI.Services { public class CurrentKeys { + public string ServerBinding { get; internal set; } + + public string ChannelBinding { get; internal set; } + + public string OperationBinding { get; internal set; } + + public string MessageBinding { get; internal set; } + public string Channel { get; internal set; } public string Extension { get; internal set; } diff --git a/src/LEGO.AsyncAPI/Validation/AsyncApiValidationError.cs b/src/LEGO.AsyncAPI/Validation/AsyncApiValidationError.cs index 863607bf..614bc1fe 100644 --- a/src/LEGO.AsyncAPI/Validation/AsyncApiValidationError.cs +++ b/src/LEGO.AsyncAPI/Validation/AsyncApiValidationError.cs @@ -5,7 +5,7 @@ namespace LEGO.AsyncAPI.Validations using LEGO.AsyncAPI.Models; /// - /// Errors detected when validating an AsyncApi Element + /// Errors detected when validating an AsyncApi Element. /// public class AsyncApiValidatorError : AsyncApiError { diff --git a/src/LEGO.AsyncAPI/Validation/AsyncApiValidator.cs b/src/LEGO.AsyncAPI/Validation/AsyncApiValidator.cs index 0e9a567e..3f8bf392 100644 --- a/src/LEGO.AsyncAPI/Validation/AsyncApiValidator.cs +++ b/src/LEGO.AsyncAPI/Validation/AsyncApiValidator.cs @@ -18,7 +18,7 @@ public class AsyncApiValidator : AsyncApiVisitorBase, IValidationContext private readonly IList warnings = new List(); /// - /// Create a vistor that will validate an AsyncApiDocument + /// Create a vistor that will validate an AsyncApiDocument. /// /// public AsyncApiValidator(ValidationRuleSet ruleSet) @@ -77,81 +77,89 @@ public void AddWarning(AsyncApiValidatorWarning warning) } /// - /// Execute validation rules against an + /// Execute validation rules against an . /// - /// The object to be validated + /// The object to be validated. public override void Visit(AsyncApiDocument item) => this.Validate(item); /// - /// Execute validation rules against an + /// Execute validation rules against an . /// - /// The object to be validated + /// The object to be validated. public override void Visit(AsyncApiInfo item) => this.Validate(item); /// - /// Execute validation rules against an + /// Execute validation rules against an . /// - /// The object to be validated + /// The object to be validated. public override void Visit(AsyncApiContact item) => this.Validate(item); /// - /// Execute validation rules against an + /// Execute validation rules against an . /// - /// The object to be validated + /// The object to be validated. public override void Visit(AsyncApiComponents item) => this.Validate(item); /// - /// Execute validation rules against an + /// Execute validation rules against an . /// - /// The object to be validated + /// The object to be validated. public override void Visit(AsyncApiLicense item) => this.Validate(item); /// - /// Execute validation rules against an + /// Execute validation rules against an . /// - /// The object to be validated + /// The object to be validated. public override void Visit(AsyncApiOAuthFlow item) => this.Validate(item); /// - /// Execute validation rules against an + /// Execute validation rules against an . /// - /// The object to be validated + /// The object to be validated. public override void Visit(AsyncApiTag item) => this.Validate(item); /// - /// Execute validation rules against an + /// Execute validation rules against an . /// - /// The object to be validated + /// The object to be validated. public override void Visit(AsyncApiParameter item) => this.Validate(item); /// - /// Execute validation rules against an + /// Execute validation rules against an . /// - /// The object to be validated + /// The object to be validated. public override void Visit(AsyncApiSchema item) => this.Validate(item); /// - /// Execute validation rules against an + /// Execute validation rules against an . /// - /// The object to be validated + /// The object to be validated. public override void Visit(AsyncApiServer item) => this.Validate(item); + public override void Visit(IServerBinding item) => this.Validate(item); + + public override void Visit(IChannelBinding item) => this.Validate(item); + + public override void Visit(IOperationBinding item) => this.Validate(item); + + public override void Visit(IMessageBinding item) => this.Validate(item); + /// - /// Execute validation rules against an + /// Execute validation rules against an . /// - /// The object to be validated + /// The object to be validated. public override void Visit(IAsyncApiExtensible item) => this.Validate(item); /// - /// Execute validation rules against an + /// Execute validation rules against an . /// - /// The object to be validated + /// The object to be validated. public override void Visit(IAsyncApiExtension item) => this.Validate(item, item.GetType()); /// - /// Execute validation rules against a list of + /// Execute validation rules against a list of . /// - /// The object to be validated + /// The object to be validated. public override void Visit(IList items) => this.Validate(items, items.GetType()); private void Validate(T item) diff --git a/src/LEGO.AsyncAPI/Validation/AsyncApiValidatorWarning.cs b/src/LEGO.AsyncAPI/Validation/AsyncApiValidatorWarning.cs index f0d5eb08..98304250 100644 --- a/src/LEGO.AsyncAPI/Validation/AsyncApiValidatorWarning.cs +++ b/src/LEGO.AsyncAPI/Validation/AsyncApiValidatorWarning.cs @@ -5,7 +5,7 @@ namespace LEGO.AsyncAPI.Validations using LEGO.AsyncAPI.Models; /// - /// Warnings detected when validating an AsyncApi Element + /// Warnings detected when validating an AsyncApi Element. /// public class AsyncApiValidatorWarning : AsyncApiError { diff --git a/src/LEGO.AsyncAPI/Validation/IValidationContext.cs b/src/LEGO.AsyncAPI/Validation/IValidationContext.cs index fdb658a1..51ce0977 100644 --- a/src/LEGO.AsyncAPI/Validation/IValidationContext.cs +++ b/src/LEGO.AsyncAPI/Validation/IValidationContext.cs @@ -3,7 +3,7 @@ namespace LEGO.AsyncAPI.Validations { /// - /// Constrained interface used to provide context to rule implementation + /// Constrained interface used to provide context to rule implementation. /// public interface IValidationContext { @@ -22,7 +22,7 @@ public interface IValidationContext /// /// Allow Rule to indicate validation error occured at a deeper context level. /// - /// Identifier for context + /// Identifier for context. void Enter(string segment); /// @@ -31,7 +31,7 @@ public interface IValidationContext void Exit(); /// - /// Pointer to source of validation error in document + /// Pointer to source of validation error in document. /// string PathString { get; } } diff --git a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiContactRules.cs b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiContactRules.cs index 019c2ef4..e42f2ec2 100644 --- a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiContactRules.cs +++ b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiContactRules.cs @@ -2,13 +2,27 @@ namespace LEGO.AsyncAPI.Validation.Rules { + using System.Net.Mail; using LEGO.AsyncAPI.Models; - using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Validations; [AsyncApiRule] public static class AsyncApiContactRules { + private static bool IsEmailAddress(this string input) + { + try + { + _ = new MailAddress(input); + } + catch (System.Exception) + { + return false; + } + + return true; + } + public static ValidationRule EmailMustBeEmailFormat => new ValidationRule( (context, contact) => @@ -34,9 +48,9 @@ public static class AsyncApiContactRules context.Enter("url"); if (contact != null && contact.Url != null && !contact.Url.IsAbsoluteUri) { - context.CreateError( - nameof(ContactUrlMustBeAbsolute), - string.Format(Resource.Validation_MustBeAbsoluteUrl, "url", "contact")); + context.CreateError( + nameof(ContactUrlMustBeAbsolute), + string.Format(Resource.Validation_MustBeAbsoluteUrl, "url", "contact")); } context.Exit(); diff --git a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiCorrelationIdRules.cs b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiCorrelationIdRules.cs index 5422eea8..b401088e 100644 --- a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiCorrelationIdRules.cs +++ b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiCorrelationIdRules.cs @@ -4,7 +4,6 @@ namespace LEGO.AsyncAPI.Validation.Rules { using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Validations; - using System.Linq; [AsyncApiRule] public static class AsyncApiCorrelationIdRules @@ -22,7 +21,6 @@ public static class AsyncApiCorrelationIdRules } context.Exit(); - }); } } \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs index 3cba8059..264c1611 100644 --- a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs +++ b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs @@ -2,7 +2,6 @@ namespace LEGO.AsyncAPI.Validation.Rules { - using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using LEGO.AsyncAPI.Models; @@ -31,7 +30,7 @@ public static class AsyncApiDocumentRules context.Exit(); context.Enter("channels"); - if (document.Channels == null || !document.Channels.Keys.Any()) + if (document.Channels == null) { context.CreateError( nameof(DocumentRequiredFields), diff --git a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiExtensionRules.cs b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiExtensionRules.cs index 8ce2a099..a2d27039 100644 --- a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiExtensionRules.cs +++ b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiExtensionRules.cs @@ -2,7 +2,6 @@ namespace LEGO.AsyncAPI.Validation.Rules { - using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Validations; @@ -26,6 +25,7 @@ public static class AsyncApiExtensionRules string.Format(Resource.Validation_ExtensionNameMustBeginWithXDash, extensible.Key, context.PathString)); } } + context.Exit(); }); } diff --git a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiExternalDocumentationRules.cs b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiExternalDocumentationRules.cs index e080e7b4..f5fd53ba 100644 --- a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiExternalDocumentationRules.cs +++ b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiExternalDocumentationRules.cs @@ -21,7 +21,6 @@ public static class AsyncApiExternalDocumentationRules } context.Exit(); - }); public static ValidationRule ExternalDocumentationUrlMustBeAbsolute => @@ -34,7 +33,6 @@ public static class AsyncApiExternalDocumentationRules context.CreateError( nameof(ExternalDocumentationUrlMustBeAbsolute), string.Format(Resource.Validation_MustBeAbsoluteUrl, "url", "externalDocumentation")); - } context.Exit(); diff --git a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiInfoRules.cs b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiInfoRules.cs index 88b9187c..342a1739 100644 --- a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiInfoRules.cs +++ b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiInfoRules.cs @@ -43,7 +43,6 @@ public static class AsyncApiInfoRules context.CreateError( nameof(TermsOfServiceUrlMustBeAbsolute), string.Format(Resource.Validation_MustBeAbsoluteUrl, "termsOfService", "info")); - } context.Exit(); diff --git a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiLicenseRules.cs b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiLicenseRules.cs index ce735c93..0708c13b 100644 --- a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiLicenseRules.cs +++ b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiLicenseRules.cs @@ -3,7 +3,6 @@ namespace LEGO.AsyncAPI.Validation.Rules { using LEGO.AsyncAPI.Models; - using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Validations; [AsyncApiRule] @@ -28,14 +27,12 @@ public static class AsyncApiLicenseRules new ValidationRule( (context, license) => { - context.Enter("url"); if (license.Url != null && !license.Url.IsAbsoluteUri) { context.CreateError( nameof(LicenseUrlMustBeAbsolute), string.Format(Resource.Validation_MustBeAbsoluteUrl, "url", "license")); - } context.Exit(); diff --git a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiOAuthFlowRules.cs b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiOAuthFlowRules.cs index e1649d4e..457a03ec 100644 --- a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiOAuthFlowRules.cs +++ b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiOAuthFlowRules.cs @@ -42,7 +42,6 @@ public static class AsyncApiOAuthFlowRules } context.Exit(); - }); public static ValidationRule OAuthFlowUrlMustBeAbsolute => @@ -55,7 +54,6 @@ public static class AsyncApiOAuthFlowRules context.CreateError( nameof(OAuthFlowUrlMustBeAbsolute), string.Format(Resource.Validation_MustBeAbsoluteUrl, "authorizationUrl", "flow")); - } context.Exit(); @@ -66,7 +64,6 @@ public static class AsyncApiOAuthFlowRules context.CreateError( nameof(OAuthFlowUrlMustBeAbsolute), string.Format(Resource.Validation_MustBeAbsoluteUrl, "tokenUrl", "flow")); - } context.Exit(); diff --git a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiTagRules.cs b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiTagRules.cs index bb421471..6e676edb 100644 --- a/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiTagRules.cs +++ b/src/LEGO.AsyncAPI/Validation/Rules/AsyncApiTagRules.cs @@ -4,7 +4,6 @@ namespace LEGO.AsyncAPI.Validation.Rules { using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Validations; - using System.Linq; [AsyncApiRule] public static class AsyncApiTagRules @@ -22,7 +21,6 @@ public static class AsyncApiTagRules } context.Exit(); - }); } } \ No newline at end of file diff --git a/src/LEGO.AsyncAPI/Validation/Rules/RuleHelpers.cs b/src/LEGO.AsyncAPI/Validation/Rules/RuleHelpers.cs deleted file mode 100644 index 058ff27e..00000000 --- a/src/LEGO.AsyncAPI/Validation/Rules/RuleHelpers.cs +++ /dev/null @@ -1,268 +0,0 @@ -// Copyright (c) The LEGO Group. All rights reserved. - -namespace LEGO.AsyncAPI.Validation.Rules -{ - using System.Net.Mail; - using LEGO.AsyncAPI.Models; - using LEGO.AsyncAPI.Models.Any; - using LEGO.AsyncAPI.Models.Interfaces; - using LEGO.AsyncAPI.Validations; - - internal static class RuleHelpers - { - internal const string DataTypeMismatchedErrorMessage = "Data and type mismatch found."; - - /// - /// Input string must be in the format of an email address. - /// - /// The input string. - /// True if it's an email address. Otherwise False. - public static bool IsEmailAddress(this string input) - { - try - { - _ = new MailAddress(input); - } - catch (System.Exception) - { - return false; - } - - return true; - } - - public static void ValidateDataTypeMismatch( - IValidationContext context, - string ruleName, - IAsyncApiAny value, - AsyncApiSchema schema) - { - if (schema == null) - { - return; - } - - var types = schema.Type; - var format = schema.Format; - var nullable = schema.Nullable; - - // Before checking the type, check first if the schema allows null. - // If so and the data given is also null, this is allowed for any type. - if (nullable) - { - if (value is AsyncApiNull) - { - return; - } - } - - foreach (var type in types) - { - if (type == SchemaType.Object) - { - // It is not against the spec to have a string representing an object value. - // To represent examples of media types that cannot naturally be represented in JSON or YAML, - // a string value can contain the example with escaping where necessary - if (value is AsyncApiString) - { - return; - } - - // If value is not a string and also not an object, there is a data mismatch. - if (!(value is AsyncApiObject)) - { - context.CreateWarning( - ruleName, - DataTypeMismatchedErrorMessage); - return; - } - - var anyObject = (AsyncApiObject)value; - - foreach (var key in anyObject.Keys) - { - context.Enter(key); - - if (schema.Properties != null && schema.Properties.ContainsKey(key)) - { - ValidateDataTypeMismatch(context, ruleName, anyObject[key], schema.Properties[key]); - } - else - { - ValidateDataTypeMismatch(context, ruleName, anyObject[key], schema.AdditionalProperties); - } - - context.Exit(); - } - - return; - } - - if (type == SchemaType.Array) - { - // It is not against the spec to have a string representing an array value. - // To represent examples of media types that cannot naturally be represented in JSON or YAML, - // a string value can contain the example with escaping where necessary - if (value is AsyncApiString) - { - return; - } - - // If value is not a string and also not an array, there is a data mismatch. - if (!(value is AsyncApiArray)) - { - context.CreateWarning( - ruleName, - DataTypeMismatchedErrorMessage); - return; - } - - var anyArray = (AsyncApiArray)value; - - for (int i = 0; i < anyArray.Count; i++) - { - context.Enter(i.ToString()); - - ValidateDataTypeMismatch(context, ruleName, anyArray[i], schema.Items); - - context.Exit(); - } - - return; - } - - if (type == SchemaType.Integer && format == "int32") - { - if (!(value is AsyncApiInteger)) - { - context.CreateWarning( - ruleName, - DataTypeMismatchedErrorMessage); - } - - return; - } - - if (type == SchemaType.Integer && format == "int64") - { - if (!(value is AsyncApiLong)) - { - context.CreateWarning( - ruleName, - DataTypeMismatchedErrorMessage); - } - - return; - } - - if (type == SchemaType.Integer && !(value is AsyncApiInteger)) - { - if (!(value is AsyncApiInteger)) - { - context.CreateWarning( - ruleName, - DataTypeMismatchedErrorMessage); - } - - return; - } - - if (type == SchemaType.Number && format == "float") - { - if (!(value is AsyncApiFloat)) - { - context.CreateWarning( - ruleName, - DataTypeMismatchedErrorMessage); - } - - return; - } - - if (type == SchemaType.Number && format == "double") - { - if (!(value is AsyncApiDouble)) - { - context.CreateWarning( - ruleName, - DataTypeMismatchedErrorMessage); - } - - return; - } - - if (type == SchemaType.Number) - { - if (!(value is AsyncApiDouble)) - { - context.CreateWarning( - ruleName, - DataTypeMismatchedErrorMessage); - } - - return; - } - - if (type == SchemaType.String && format == "byte") - { - if (!(value is AsyncApiByte)) - { - context.CreateWarning( - ruleName, - DataTypeMismatchedErrorMessage); - } - - return; - } - - if (type == SchemaType.String && format == "date") - { - if (!(value is AsyncApiDate)) - { - context.CreateWarning( - ruleName, - DataTypeMismatchedErrorMessage); - } - - return; - } - - if (type == SchemaType.String && format == "date-time") - { - if (!(value is AsyncApiDateTime)) - { - context.CreateWarning( - ruleName, - DataTypeMismatchedErrorMessage); - } - - return; - } - - if (type == SchemaType.String) - { - if (!(value is AsyncApiString)) - { - context.CreateWarning( - ruleName, - DataTypeMismatchedErrorMessage); - } - - return; - } - - if (type == SchemaType.Boolean) - { - if (!(value is AsyncApiBoolean)) - { - context.CreateWarning( - ruleName, - DataTypeMismatchedErrorMessage); - } - - return; - } - } - } - } -} diff --git a/src/LEGO.AsyncAPI/Validation/ValidationExtensions.cs b/src/LEGO.AsyncAPI/Validation/ValidationExtensions.cs index 46c2c5d2..eb2e2deb 100644 --- a/src/LEGO.AsyncAPI/Validation/ValidationExtensions.cs +++ b/src/LEGO.AsyncAPI/Validation/ValidationExtensions.cs @@ -3,12 +3,12 @@ namespace LEGO.AsyncAPI.Validations { /// - /// Helper methods to simplify creating validation rules + /// Helper methods to simplify creating validation rules. /// public static class ValidationContextExtensions { /// - /// Helper method to simplify validation rules + /// Helper method to simplify validation rules. /// public static void CreateError(this IValidationContext context, string ruleName, string message) { @@ -17,7 +17,7 @@ public static void CreateError(this IValidationContext context, string ruleName, } /// - /// Helper method to simplify validation rules + /// Helper method to simplify validation rules. /// public static void CreateWarning(this IValidationContext context, string ruleName, string message) { diff --git a/src/LEGO.AsyncAPI/Validation/ValidationRule.cs b/src/LEGO.AsyncAPI/Validation/ValidationRule.cs index d2e48704..a6b1b0b3 100644 --- a/src/LEGO.AsyncAPI/Validation/ValidationRule.cs +++ b/src/LEGO.AsyncAPI/Validation/ValidationRule.cs @@ -1,10 +1,8 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. +// Copyright (c) The LEGO Group. All rights reserved. namespace LEGO.AsyncAPI.Validations { using System; - using LEGO.AsyncAPI.Models.Interfaces; /// /// Class containing validation rule logic. @@ -23,48 +21,4 @@ public abstract class ValidationRule /// The object item. internal abstract void Evaluate(IValidationContext context, object item); } - - /// - /// Class containing validation rule logic for . - /// - /// - public class ValidationRule : ValidationRule where T : IAsyncApiElement - { - private readonly Action validate; - - /// - /// Initializes a new instance of the class. - /// - /// Action to perform the validation. - public ValidationRule(Action validate) - { - this.validate = validate ?? throw Error.ArgumentNull(nameof(validate)); - } - - internal override Type ElementType - { - get { return typeof(T); } - } - - internal override void Evaluate(IValidationContext context, object item) - { - if (context == null) - { - throw Error.ArgumentNull(nameof(context)); - } - - if (item == null) - { - return; - } - - if (!(item is T)) - { - throw Error.Argument(string.Format("Input type must be of type {0}", typeof(T).FullName)); - } - - T typedItem = (T)item; - this.validate(context, typedItem); - } - } } diff --git a/src/LEGO.AsyncAPI/Validation/ValidationRuleSet.cs b/src/LEGO.AsyncAPI/Validation/ValidationRuleSet.cs index 83490856..2ea5d0f3 100644 --- a/src/LEGO.AsyncAPI/Validation/ValidationRuleSet.cs +++ b/src/LEGO.AsyncAPI/Validation/ValidationRuleSet.cs @@ -1,5 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. +// Copyright (c) The LEGO Group. All rights reserved. namespace LEGO.AsyncAPI.Validations { @@ -22,9 +21,9 @@ public sealed class ValidationRuleSet : IEnumerable private IList emptyRules = new List(); /// - /// Retrieve the rules that are related to a specific type + /// Retrieve the rules that are related to a specific type. /// - /// The type that is to be validated + /// The type that is to be validated. /// Either the rules related to the type, or an empty list. public IList FindRules(Type type) { @@ -55,7 +54,7 @@ public static ValidationRuleSet GetDefaultRuleSet() } /// - /// Return Ruleset with no rules + /// Return Ruleset with no rules. /// public static ValidationRuleSet GetEmptyRuleSet() { diff --git a/src/LEGO.AsyncAPI/Validation/ValidationRule{T}.cs b/src/LEGO.AsyncAPI/Validation/ValidationRule{T}.cs new file mode 100644 index 00000000..658183f3 --- /dev/null +++ b/src/LEGO.AsyncAPI/Validation/ValidationRule{T}.cs @@ -0,0 +1,52 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Validations +{ + using System; + using LEGO.AsyncAPI.Models.Interfaces; + + /// + /// Class containing validation rule logic for . + /// + /// + public class ValidationRule : ValidationRule + where T : IAsyncApiElement + { + private readonly Action validate; + + /// + /// Initializes a new instance of the class. + /// + /// Action to perform the validation. + public ValidationRule(Action validate) + { + this.validate = validate ?? throw Error.ArgumentNull(nameof(validate)); + } + + internal override Type ElementType + { + get { return typeof(T); } + } + + internal override void Evaluate(IValidationContext context, object item) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (item == null) + { + return; + } + + if (!(item is T)) + { + throw Error.Argument(string.Format("Input type must be of type {0}", typeof(T).FullName)); + } + + T typedItem = (T)item; + this.validate(context, typedItem); + } + } +} diff --git a/src/LEGO.AsyncAPI/Writers/AsyncApiWriterAnyExtensions.cs b/src/LEGO.AsyncAPI/Writers/AsyncApiWriterAnyExtensions.cs index 15f8c06e..9a317cd1 100644 --- a/src/LEGO.AsyncAPI/Writers/AsyncApiWriterAnyExtensions.cs +++ b/src/LEGO.AsyncAPI/Writers/AsyncApiWriterAnyExtensions.cs @@ -4,13 +4,15 @@ namespace LEGO.AsyncAPI.Writers { using System; using System.Collections.Generic; - using LEGO.AsyncAPI.Models.Any; + using System.Text.Json; + using System.Text.Json.Nodes; + using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Models.Interfaces; public static class AsyncApiWriterAnyExtensions { /// - /// Write the specification extensions + /// Write the specification extensions. /// /// The AsyncApi writer. /// The specification extensions. @@ -27,45 +29,57 @@ public static void WriteExtensions(this IAsyncApiWriter writer, IDictionary - /// Write the value. + /// Write the value. /// /// The AsyncApi Any type. /// The AsyncApi writer. - /// The Any value - public static void WriteAny(this IAsyncApiWriter writer, T any) where T : IAsyncApiAny + /// The Any value. + public static void WriteAny(this IAsyncApiWriter writer, AsyncApiAny any) { if (writer is null) { throw new ArgumentNullException(nameof(writer)); } - if (any == null) + if (any.GetNode() == null) { writer.WriteNull(); return; } - switch (any.AnyType) + var node = any.GetNode(); + + var element = JsonDocument.Parse(node.ToJsonString()).RootElement; + switch (element.ValueKind) { - case AnyType.Array: // Array - writer.WriteArray(any as AsyncApiArray); + case JsonValueKind.Array: // Array + writer.WriteArray(node as JsonArray); break; - case AnyType.Object: // Object - writer.WriteObject(any as AsyncApiObject); + case JsonValueKind.Object: // Object + writer.WriteObject(node as JsonObject); break; - case AnyType.Primitive: // Primitive - writer.WritePrimitive(any as IAsyncApiPrimitive); + case JsonValueKind.String: + case JsonValueKind.Number: + case JsonValueKind.False or JsonValueKind.True: + writer.WritePrimitive(element); break; - case AnyType.Null: // null + case JsonValueKind.Null: // null writer.WriteNull(); break; default: @@ -73,7 +87,7 @@ public static void WriteAny(this IAsyncApiWriter writer, T any) where T : IAs } } - private static void WriteArray(this IAsyncApiWriter writer, AsyncApiArray array) + private static void WriteArray(this IAsyncApiWriter writer, JsonArray array) { if (writer is null) { @@ -89,13 +103,13 @@ private static void WriteArray(this IAsyncApiWriter writer, AsyncApiArray array) foreach (var item in array) { - writer.WriteAny(item); + writer.WriteAny(new AsyncApiAny(item)); } writer.WriteEndArray(); } - private static void WriteObject(this IAsyncApiWriter writer, AsyncApiObject entity) + private static void WriteObject(this IAsyncApiWriter writer, JsonObject entity) { if (writer is null) { @@ -112,25 +126,59 @@ private static void WriteObject(this IAsyncApiWriter writer, AsyncApiObject enti foreach (var item in entity) { writer.WritePropertyName(item.Key); - writer.WriteAny(item.Value); + writer.WriteAny(new AsyncApiAny(item.Value)); } writer.WriteEndObject(); } - private static void WritePrimitive(this IAsyncApiWriter writer, IAsyncApiPrimitive primitive) + private static void WritePrimitive(this IAsyncApiWriter writer, JsonElement primitive) { if (writer is null) { throw new ArgumentNullException(nameof(writer)); } - if (primitive is null) + if (primitive.ValueKind == JsonValueKind.String) + { + if (primitive.TryGetDateTime(out var dateTime)) + { + writer.WriteValue(dateTime); + } + else if (primitive.TryGetDateTimeOffset(out var dateTimeOffset)) + { + writer.WriteValue(dateTimeOffset); + } + else + { + writer.WriteValue(primitive.GetString()); + } + } + + if (primitive.ValueKind == JsonValueKind.Number) { - throw new ArgumentNullException(nameof(primitive)); + if (primitive.TryGetDecimal(out var decimalValue)) + { + writer.WriteValue(decimalValue); + } + else if (primitive.TryGetDouble(out var doubleValue)) + { + writer.WriteValue(doubleValue); + } + else if (primitive.TryGetInt64(out var longValue)) + { + writer.WriteValue(longValue); + } + else if (primitive.TryGetInt32(out var intValue)) + { + writer.WriteValue(intValue); + } } - primitive.Write(writer); + if (primitive.ValueKind is JsonValueKind.True or JsonValueKind.False) + { + writer.WriteValue(primitive.GetBoolean()); + } } } } diff --git a/src/LEGO.AsyncAPI/Writers/AsyncApiWriterBase.cs b/src/LEGO.AsyncAPI/Writers/AsyncApiWriterBase.cs index 86c94be4..7cc6ee02 100644 --- a/src/LEGO.AsyncAPI/Writers/AsyncApiWriterBase.cs +++ b/src/LEGO.AsyncAPI/Writers/AsyncApiWriterBase.cs @@ -177,7 +177,7 @@ public virtual void WriteValue(long value) /// The DateTime value. public virtual void WriteValue(DateTime value) { - this.WriteValue(value.ToString("o")); + this.WriteValue(value.ToString(this.Settings.DateTimeFormat, this.Settings.CultureInfo)); } /// @@ -186,7 +186,7 @@ public virtual void WriteValue(DateTime value) /// The DateTimeOffset value. public virtual void WriteValue(DateTimeOffset value) { - this.WriteValue(value.ToString("o")); + this.WriteValue(value.ToString(this.Settings.DateTimeFormat, this.Settings.CultureInfo)); } /// @@ -384,7 +384,7 @@ private bool IsScopeType(ScopeType type) /// Verifies whether a property name can be written based on whether /// the property name is a valid string and whether the current scope is an object scope. /// - /// property name + /// property name. protected void VerifyCanWritePropertyName(string name) { if (string.IsNullOrWhiteSpace(name)) diff --git a/src/LEGO.AsyncAPI/Writers/AsyncApiWriterExtensions.cs b/src/LEGO.AsyncAPI/Writers/AsyncApiWriterExtensions.cs index 5953e9d1..25777d51 100644 --- a/src/LEGO.AsyncAPI/Writers/AsyncApiWriterExtensions.cs +++ b/src/LEGO.AsyncAPI/Writers/AsyncApiWriterExtensions.cs @@ -93,6 +93,7 @@ public static void WriteOptionalProperty( /// /// Write a primitive property. /// + /// . /// The writer. /// The property name. /// The property value. @@ -110,6 +111,7 @@ public static void WriteOptionalProperty(this IAsyncApiWriter writer, string /// /// Write a string/number property. /// + /// . /// The writer. /// The property name. /// The property value. @@ -124,7 +126,7 @@ public static void WriteRequiredProperty(this IAsyncApiWriter writer, string /// /// Write the optional AsyncApi object/element. /// - /// The AsyncApi element type. + /// The AsyncApi element type. . /// The AsyncApi writer. /// The property name. /// The property value. @@ -138,6 +140,12 @@ public static void WriteOptionalObject( { if (value != null) { + if (value is IAsyncApiReferenceable refer && refer.Reference != null) + { + writer.WriteRequiredObject(name, value, action); + return; + } + var values = value as IEnumerable; if (values != null && !values.GetEnumerator().MoveNext()) { @@ -151,7 +159,7 @@ public static void WriteOptionalObject( /// /// Write the required AsyncApi object/element. /// - /// The AsyncApi element type. + /// The AsyncApi element type. . /// The AsyncApi writer. /// The property name. /// The property value. @@ -199,7 +207,7 @@ public static void WriteOptionalCollection( /// /// Write the optional AsyncApi object/element collection. /// - /// The AsyncApi element type. + /// The AsyncApi element type. . /// The AsyncApi writer. /// The property name. /// The collection values. @@ -220,7 +228,7 @@ public static void WriteOptionalCollection( /// /// Write the required AsyncApi object/element collection. /// - /// The AsyncApi element type. + /// The AsyncApi element type. . /// The AsyncApi writer. /// The property name. /// The collection values. @@ -274,18 +282,16 @@ public static void WriteRequiredMap( this IAsyncApiWriter writer, string name, IDictionary elements, - Action action) where T : IAsyncApiElement + Action action) + where T : IAsyncApiElement { - if (elements != null && elements.Any()) - { - writer.WriteMapInternal(name, elements, action); - } + writer.WriteMapInternal(name, elements, action); } /// /// Write the optional AsyncApi element map. /// - /// The AsyncApi element type. + /// The AsyncApi element type. . /// The AsyncApi writer. /// The property name. /// The map values. @@ -306,7 +312,7 @@ public static void WriteOptionalMap( /// /// Write the optional AsyncApi element map. /// - /// The AsyncApi element type. + /// The AsyncApi element type. . /// The AsyncApi writer. /// The property name. /// The map values. @@ -327,7 +333,7 @@ public static void WriteOptionalMap( /// /// Write the required AsyncApi element map. /// - /// The AsyncApi element type. + /// The AsyncApi element type. . /// The AsyncApi writer. /// The property name. /// The map values. diff --git a/src/LEGO.AsyncAPI/Writers/AsyncApiWriterSettings.cs b/src/LEGO.AsyncAPI/Writers/AsyncApiWriterSettings.cs index b6edd619..2b663bb2 100644 --- a/src/LEGO.AsyncAPI/Writers/AsyncApiWriterSettings.cs +++ b/src/LEGO.AsyncAPI/Writers/AsyncApiWriterSettings.cs @@ -4,11 +4,31 @@ namespace LEGO.AsyncAPI.Writers { using LEGO.AsyncAPI.Models; - public class AsyncApiWriterSettings + /// + /// Contains settings for writing async api. + /// + public class AsyncApiWriterSettings : AsyncApiSettings { private ReferenceInlineSetting referenceInline = ReferenceInlineSetting.DoNotInlineReferences; - internal LoopDetector LoopDetector { get; } = new LoopDetector(); + static AsyncApiWriterSettings() + { + Default = new AsyncApiWriterSettings(); + } + + /// + /// Initializes a new instance of the class. + /// + public AsyncApiWriterSettings() + { + this.InlineReferences = false; + this.LoopDetector = new LoopDetector(); + } + + /// + /// Gets the default settings to use for writing async api. + /// + public static AsyncApiWriterSettings Default { get; } /// /// Gets or sets indicates how references in the source document should be handled. @@ -38,9 +58,19 @@ public ReferenceInlineSetting ReferenceInline /// /// Gets or sets a value indicating whether indicates if local references should be rendered as an inline object. /// - public bool InlineReferences { get; set; } = false; + public bool InlineReferences { get; set; } - internal bool ShouldInlineReference(AsyncApiReference reference) + /// + /// Figures out if a loop exists. + /// + internal LoopDetector LoopDetector { get; } + + /// + /// Returns back if the refernece should be inlined or not. + /// + /// The refernece. + /// True if it should be inlined otherwise false. + public bool ShouldInlineReference(AsyncApiReference reference) { return this.InlineReferences; } diff --git a/src/LEGO.AsyncAPI/Writers/AsyncApiYamlWriter.cs b/src/LEGO.AsyncAPI/Writers/AsyncApiYamlWriter.cs index dc834c48..d0fab76e 100644 --- a/src/LEGO.AsyncAPI/Writers/AsyncApiYamlWriter.cs +++ b/src/LEGO.AsyncAPI/Writers/AsyncApiYamlWriter.cs @@ -1,17 +1,40 @@ // Copyright (c) The LEGO Group. All rights reserved. +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; + namespace LEGO.AsyncAPI.Writers { - using System.IO; - + /// + /// Used to conver an AsyncApi schema into a yaml document. + /// public class AsyncApiYamlWriter : AsyncApiWriterBase { + private static readonly Regex YamlNumberRegex; + private static readonly char[] YamlIndicators; + private static readonly string[] YamlPlainStringForbiddenCobinations; + private static readonly string[] YamlPlainStringForbiddenTerminals; + private static readonly char[] YamlControlCharacters; + + static AsyncApiYamlWriter() + { + YamlNumberRegex = new Regex("^[+-]?[0-9]*\\.?[0-9]*$", RegexOptions.Compiled); + YamlIndicators = new char[] { '-', '?', ':', ',', '{', '}', '[', ']', '&', '*', '#', '?', '|', '-', '>', '!', '%', '@', '`', '\'', '"', }; + YamlPlainStringForbiddenCobinations = new string[] { ": ", " #", "[", "]", "{", "}", ",", }; + YamlPlainStringForbiddenTerminals = new string[] { ":" }; + YamlControlCharacters = new char[] { '\0', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\a', '\b', '\t', '\n', '\v', '\f', '\r', '\x0e', '\x0f', '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1a', '\x1b', '\x1c', '\x1d', '\x1e', '\x1f', }; + } + /// /// Initializes a new instance of the class. /// /// The text writer. + [Obsolete($"Please use overridden constructor that takes in a {nameof(AsyncApiWriterSettings)} instance.")] public AsyncApiYamlWriter(TextWriter textWriter) - : this(textWriter, null) + : this(textWriter, new AsyncApiWriterSettings()) { } @@ -19,14 +42,14 @@ public AsyncApiYamlWriter(TextWriter textWriter) /// Initializes a new instance of the class. /// /// The text writer. - /// + /// The settings used to read and write yaml public AsyncApiYamlWriter(TextWriter textWriter, AsyncApiWriterSettings settings) : base(textWriter, settings) { } /// - /// Allow rendering of multi-line strings using YAML | syntax + /// Allow rendering of multi-line strings using YAML | syntax. /// public bool UseLiteralStyle { get; set; } @@ -153,7 +176,7 @@ public override void WritePropertyName(string name) this.WriteIndentation(); } - name = name.GetYamlCompatibleString(); + name = this.GetYamlCompatibleString(name); this.Writer.Write(name); this.Writer.Write(":"); @@ -171,7 +194,7 @@ public override void WriteValue(string value) { this.WriteValueSeparator(); - value = value.GetYamlCompatibleString(); + value = this.GetYamlCompatibleString(value); this.Writer.Write(value); } @@ -196,7 +219,7 @@ public override void WriteValue(string value) this.IncreaseIndentation(); - using (var reader = new StringReader(value)) + using (StringReader reader = new(value)) { bool firstLine = true; while (reader.ReadLine() is var line && line != null) @@ -316,5 +339,113 @@ public override void WriteRaw(string value) this.WriteValueSeparator(); this.Writer.Write(value); } + + /// + /// Escapes all special characters and put the string in quotes if necessary to + /// get a YAML-compatible string. + /// + /// The string to turn into yaml. + /// The string as yaml. + internal string GetYamlCompatibleString(string input) + { + if (input == null) + { + return "null"; + } + + switch (input.ToLower()) + { + case "": + return "''"; + + case "~": + // Example 2.20. Floating Point + case "-.inf": + case ".inf": + case ".nan": + // Example 2.21. Miscellaneous + case "null": + + // Booleans + case "true": + case "false": + return $"'{input}'"; + } + + // If string includes a control character, wrapping in double quote is required. + if (input.Any(c => YamlControlCharacters.Contains(c))) + { + // Replace the backslash first, so that the new backslashes created by other Replaces are not duplicated. + input = input.Replace("\\", "\\\\"); + + // Escape the double quotes. + input = input.Replace("\"", "\\\""); + + // Escape all the control characters. + input = input.Replace("\0", "\\0"); + input = input.Replace("\x01", "\\x01"); + input = input.Replace("\x02", "\\x02"); + input = input.Replace("\x03", "\\x03"); + input = input.Replace("\x04", "\\x04"); + input = input.Replace("\x05", "\\x05"); + input = input.Replace("\x06", "\\x06"); + input = input.Replace("\a", "\\a"); + input = input.Replace("\b", "\\b"); + input = input.Replace("\t", "\\t"); + input = input.Replace("\n", "\\n"); + input = input.Replace("\v", "\\v"); + input = input.Replace("\f", "\\f"); + input = input.Replace("\r", "\\r"); + input = input.Replace("\x0e", "\\x0e"); + input = input.Replace("\x0f", "\\x0f"); + input = input.Replace("\x10", "\\x10"); + input = input.Replace("\x11", "\\x11"); + input = input.Replace("\x12", "\\x12"); + input = input.Replace("\x13", "\\x13"); + input = input.Replace("\x14", "\\x14"); + input = input.Replace("\x15", "\\x15"); + input = input.Replace("\x16", "\\x16"); + input = input.Replace("\x17", "\\x17"); + input = input.Replace("\x18", "\\x18"); + input = input.Replace("\x19", "\\x19"); + input = input.Replace("\x1a", "\\x1a"); + input = input.Replace("\x1b", "\\x1b"); + input = input.Replace("\x1c", "\\x1c"); + input = input.Replace("\x1d", "\\x1d"); + input = input.Replace("\x1e", "\\x1e"); + input = input.Replace("\x1f", "\\x1f"); + + return $"\"{input}\""; + } + + // If string + // 1) includes a character forbidden in plain string, + // 2) starts with an indicator, OR + // 3) has trailing/leading white spaces, + // wrap the string in single quote. + // http://www.yaml.org/spec/1.2/spec.html#style/flow/plain + if (YamlPlainStringForbiddenCobinations.Any(fc => input.Contains(fc)) || + YamlIndicators.Any(i => input.StartsWith(i.ToString())) || + YamlPlainStringForbiddenTerminals.Any(i => input.EndsWith(i.ToString())) || + input.Trim() != input) + { + // Escape single quotes with two single quotes. + input = input.Replace("'", "''"); + + return $"'{input}'"; + } + + // If string can be mistaken as a number, a boolean, or a timestamp, + // wrap it in quot number, a boolean, or a timestamp + if (decimal.TryParse(input, NumberStyles.Float, this.Settings.CultureInfo, out decimal _) || + bool.TryParse(input, out bool _) || + DateTime.TryParseExact(input, this.Settings.DateTimeFormat, this.Settings.CultureInfo, DateTimeStyles.RoundtripKind, out DateTime _)) + { + return $"'{input}'"; + } + + // Handle numbers + return YamlNumberRegex.IsMatch(input) ? $"'{input}'" : input; + } } } diff --git a/src/LEGO.AsyncAPI/Writers/AsyncJsonWriterSettings.cs b/src/LEGO.AsyncAPI/Writers/AsyncJsonWriterSettings.cs index d29a6a8b..662cb89a 100644 --- a/src/LEGO.AsyncAPI/Writers/AsyncJsonWriterSettings.cs +++ b/src/LEGO.AsyncAPI/Writers/AsyncJsonWriterSettings.cs @@ -8,7 +8,8 @@ public class AsyncJsonWriterSettings : AsyncApiWriterSettings /// Initializes a new instance of the class. /// public AsyncJsonWriterSettings() - { } + { + } /// /// Indicates whether or not the produced document will be written in a compact or pretty fashion. diff --git a/src/LEGO.AsyncAPI/Writers/SpecialCharacterStringExtensions.cs b/src/LEGO.AsyncAPI/Writers/SpecialCharacterStringExtensions.cs index 1a091066..f2f6fbe2 100644 --- a/src/LEGO.AsyncAPI/Writers/SpecialCharacterStringExtensions.cs +++ b/src/LEGO.AsyncAPI/Writers/SpecialCharacterStringExtensions.cs @@ -1,200 +1,14 @@ -// Copyright (c) The LEGO Group. All rights reserved. +// Copyright (c) The LEGO Group. All rights reserved. namespace LEGO.AsyncAPI.Writers { using System; using System.Globalization; using System.Linq; + using System.Text.RegularExpressions; public static class SpecialCharacterStringExtensions { - // Plain style strings cannot start with indicators. - // http://www.yaml.org/spec/1.2/spec.html#indicator// - private static readonly char[] yamlIndicators = - { - '-', - '?', - ':', - ',', - '{', - '}', - '[', - ']', - '&', - '*', - '#', - '?', - '|', - '-', - '>', - '!', - '%', - '@', - '`', - '\'', - '"', - }; - - // Plain style strings cannot contain these character combinations. - // http://www.yaml.org/spec/1.2/spec.html#style/flow/plain - private static readonly string[] yamlPlainStringForbiddenCombinations = - { - ": ", - " #", - - // These are technically forbidden only inside flow collections, but - // for the sake of simplicity, we will never allow them in our generated plain string. - "[", - "]", - "{", - "}", - ",", - }; - - // Plain style strings cannot end with these characters. - // http://www.yaml.org/spec/1.2/spec.html#style/flow/plain - private static readonly string[] yamlPlainStringForbiddenTerminals = - { - ":", - }; - - // Double-quoted strings are needed for these non-printable control characters. - // http://www.yaml.org/spec/1.2/spec.html#style/flow/double-quoted - private static readonly char[] yamlControlCharacters = - { - '\0', - '\x01', - '\x02', - '\x03', - '\x04', - '\x05', - '\x06', - '\a', - '\b', - '\t', - '\n', - '\v', - '\f', - '\r', - '\x0e', - '\x0f', - '\x10', - '\x11', - '\x12', - '\x13', - '\x14', - '\x15', - '\x16', - '\x17', - '\x18', - '\x19', - '\x1a', - '\x1b', - '\x1c', - '\x1d', - '\x1e', - '\x1f', - }; - - /// - /// Escapes all special characters and put the string in quotes if necessary to - /// get a YAML-compatible string. - /// - internal static string GetYamlCompatibleString(this string input) - { - // If string is an empty string, wrap it in quote to ensure it is not recognized as null. - if (input == "") - { - return "''"; - } - - // If string is the word null, wrap it in quote to ensure it is not recognized as empty scalar null. - if (input == "null") - { - return "'null'"; - } - - // If string is the letter ~, wrap it in quote to ensure it is not recognized as empty scalar null. - if (input == "~") - { - return "'~'"; - } - - // If string includes a control character, wrapping in double quote is required. - if (input.Any(c => yamlControlCharacters.Contains(c))) - { - // Replace the backslash first, so that the new backslashes created by other Replaces are not duplicated. - input = input.Replace("\\", "\\\\"); - - // Escape the double quotes. - input = input.Replace("\"", "\\\""); - - // Escape all the control characters. - input = input.Replace("\0", "\\0"); - input = input.Replace("\x01", "\\x01"); - input = input.Replace("\x02", "\\x02"); - input = input.Replace("\x03", "\\x03"); - input = input.Replace("\x04", "\\x04"); - input = input.Replace("\x05", "\\x05"); - input = input.Replace("\x06", "\\x06"); - input = input.Replace("\a", "\\a"); - input = input.Replace("\b", "\\b"); - input = input.Replace("\t", "\\t"); - input = input.Replace("\n", "\\n"); - input = input.Replace("\v", "\\v"); - input = input.Replace("\f", "\\f"); - input = input.Replace("\r", "\\r"); - input = input.Replace("\x0e", "\\x0e"); - input = input.Replace("\x0f", "\\x0f"); - input = input.Replace("\x10", "\\x10"); - input = input.Replace("\x11", "\\x11"); - input = input.Replace("\x12", "\\x12"); - input = input.Replace("\x13", "\\x13"); - input = input.Replace("\x14", "\\x14"); - input = input.Replace("\x15", "\\x15"); - input = input.Replace("\x16", "\\x16"); - input = input.Replace("\x17", "\\x17"); - input = input.Replace("\x18", "\\x18"); - input = input.Replace("\x19", "\\x19"); - input = input.Replace("\x1a", "\\x1a"); - input = input.Replace("\x1b", "\\x1b"); - input = input.Replace("\x1c", "\\x1c"); - input = input.Replace("\x1d", "\\x1d"); - input = input.Replace("\x1e", "\\x1e"); - input = input.Replace("\x1f", "\\x1f"); - - return $"\"{input}\""; - } - - // If string - // 1) includes a character forbidden in plain string, - // 2) starts with an indicator, OR - // 3) has trailing/leading white spaces, - // wrap the string in single quote. - // http://www.yaml.org/spec/1.2/spec.html#style/flow/plain - if (yamlPlainStringForbiddenCombinations.Any(fc => input.Contains(fc)) || - yamlIndicators.Any(i => input.StartsWith(i.ToString())) || - yamlPlainStringForbiddenTerminals.Any(i => input.EndsWith(i.ToString())) || - input.Trim() != input) - { - // Escape single quotes with two single quotes. - input = input.Replace("'", "''"); - - return $"'{input}'"; - } - - // If string can be mistaken as a number, a boolean, or a timestamp, - // wrap it in quote to indicate that this is indeed a string, not a number, a boolean, or a timestamp - if (decimal.TryParse(input, NumberStyles.Float, CultureInfo.InvariantCulture, out var _) || - bool.TryParse(input, out var _) || - DateTime.TryParse(input, out var _)) - { - return $"'{input}'"; - } - - return input; - } - /// /// Handles control characters and backslashes and adds double quotes /// to get JSON-compatible string. diff --git a/src/LEGO.AsyncAPI/Writers/StringExtensions.cs b/src/LEGO.AsyncAPI/Writers/StringExtensions.cs index 15697bd3..e4efe5f7 100644 --- a/src/LEGO.AsyncAPI/Writers/StringExtensions.cs +++ b/src/LEGO.AsyncAPI/Writers/StringExtensions.cs @@ -4,7 +4,7 @@ namespace LEGO.AsyncAPI.Writers { using System; using System.Reflection; - using Attributes; + using LEGO.AsyncAPI.Attributes; public static class StringExtensions { diff --git a/src/LEGO.AsyncAPI/Writers/WriterConstants.cs b/src/LEGO.AsyncAPI/Writers/WriterConstants.cs index f6bde088..be76ff29 100644 --- a/src/LEGO.AsyncAPI/Writers/WriterConstants.cs +++ b/src/LEGO.AsyncAPI/Writers/WriterConstants.cs @@ -86,27 +86,27 @@ internal static class WriterConstants internal const string NameValueSeparatorWhiteSpaceSuffix = " "; /// - /// The white space for empty object + /// The white space for empty object. /// internal const string WhiteSpaceForEmptyObject = " "; /// - /// The white space for empty array + /// The white space for empty array. /// internal const string WhiteSpaceForEmptyArray = " "; /// - /// The prefix of array item + /// The prefix of array item. /// internal const string PrefixOfArrayItem = "- "; /// - /// The white space for indent + /// The white space for indent. /// internal const string WhiteSpaceForIndent = " "; /// - /// Empty object + /// Empty object. /// /// To indicate empty object in YAML. internal const string EmptyObject = "{ }"; diff --git a/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentBuilder.cs b/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentBuilder.cs index 49b32d80..886fb7bf 100644 --- a/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentBuilder.cs +++ b/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentBuilder.cs @@ -1,8 +1,11 @@ -namespace LEGO.AsyncAPI.Tests +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Tests { + using System; + using System.Collections.Generic; using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Models.Interfaces; - using System; internal class AsyncApiDocumentBuilder { @@ -40,6 +43,10 @@ public AsyncApiDocumentBuilder WithDefaultContentType(string contentType = "appl public AsyncApiDocumentBuilder WithChannel(string key, AsyncApiChannel channel) { + if (this.document.Channels == null) + { + this.document.Channels = new Dictionary(); + } this.document.Channels.Add(key, channel); return this; } @@ -154,47 +161,47 @@ public AsyncApiDocumentBuilder WithComponent(string key, AsyncApiMessageTrait me return this; } - public AsyncApiDocumentBuilder WithComponent(string key, IServerBinding serverBinding) + public AsyncApiDocumentBuilder WithComponent(string key, AsyncApiBindings serverBindings) { if (this.document.Components == null) { this.document.Components = new AsyncApiComponents(); } - this.document.Components.ServerBindings.Add(key, serverBinding); + this.document.Components.ServerBindings.Add(key, serverBindings); return this; } - public AsyncApiDocumentBuilder WithComponent(string key, IChannelBinding channelBinding) + public AsyncApiDocumentBuilder WithComponent(string key, AsyncApiBindings channelBindings) { if (this.document.Components == null) { this.document.Components = new AsyncApiComponents(); } - this.document.Components.ChannelBindings.Add(key, channelBinding); + this.document.Components.ChannelBindings.Add(key, channelBindings); return this; } - public AsyncApiDocumentBuilder WithComponent(string key, IOperationBinding operationBinding) + public AsyncApiDocumentBuilder WithComponent(string key, AsyncApiBindings operationBindings) { if (this.document.Components == null) { this.document.Components = new AsyncApiComponents(); } - this.document.Components.OperationBindings.Add(key, operationBinding); + this.document.Components.OperationBindings.Add(key, operationBindings); return this; } - public AsyncApiDocumentBuilder WithComponent(string key, IMessageBinding messageBinding) + public AsyncApiDocumentBuilder WithComponent(string key, AsyncApiBindings messageBindings) { if (this.document.Components == null) { this.document.Components = new AsyncApiComponents(); } - this.document.Components.MessageBindings.Add(key, messageBinding); + this.document.Components.MessageBindings.Add(key, messageBindings); return this; } diff --git a/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs b/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs index 3f4f6591..13ae85e5 100644 --- a/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs +++ b/test/LEGO.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs @@ -1,217 +1,229 @@ -namespace LEGO.AsyncAPI.Tests +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Tests { using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; + using FluentAssertions; + using LEGO.AsyncAPI.Bindings; + using LEGO.AsyncAPI.Bindings.Http; + using LEGO.AsyncAPI.Bindings.Kafka; + using LEGO.AsyncAPI.Bindings.Pulsar; using LEGO.AsyncAPI.Models; - using LEGO.AsyncAPI.Models.Any; - using LEGO.AsyncAPI.Models.Bindings; - using LEGO.AsyncAPI.Models.Bindings.Http; - using LEGO.AsyncAPI.Models.Bindings.Kafka; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Readers; using LEGO.AsyncAPI.Writers; using NUnit.Framework; - public class AsyncApiDocumentV2Tests + public class ExtensionClass + { + public string Key { get; set; } + + public long OtherKey { get; set; } + } + + public class AsyncApiDocumentV2Tests : TestBase { [Test] public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() { // Arrange var expected = -@"asyncapi: '2.6.0' -info: - title: Streetlights Kafka API - version: 1.0.0 - description: The Smartylighting Streetlights API allows you to remotely manage the city lights. - license: - name: Apache 2.0 - url: https://www.apache.org/licenses/LICENSE-2.0 -servers: - scram-connections: - url: test.mykafkacluster.org:18092 - protocol: kafka-secure - description: Test broker secured with scramSha256 - security: - - saslScram: [] - tags: - - name: env:test-scram - description: This environment is meant for running internal tests through scramSha256 - - name: kind:remote - description: This server is a remote server. Not exposed by the application - - name: visibility:private - description: This resource is private and only available to certain users - mtls-connections: - url: test.mykafkacluster.org:28092 - protocol: kafka-secure - description: Test broker secured with X509 - security: - - certs: [] - tags: - - name: env:test-mtls - description: This environment is meant for running internal tests through mtls - - name: kind:remote - description: This server is a remote server. Not exposed by the application - - name: visibility:private - description: This resource is private and only available to certain users -defaultContentType: application/json -channels: - 'smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured': - description: The topic on which measured values may be produced and consumed. - publish: - operationId: receiveLightMeasurement - summary: Inform about environmental lighting conditions of a particular streetlight. - traits: - - $ref: '#/components/operationTraits/kafka' - message: - $ref: '#/components/messages/lightMeasured' - parameters: - streetlightId: - $ref: '#/components/parameters/streetlightId' - 'smartylighting.streetlights.1.0.action.{streetlightId}.turn.on': - subscribe: - operationId: turnOn - traits: - - $ref: '#/components/operationTraits/kafka' - message: - $ref: '#/components/messages/turnOnOff' - parameters: - streetlightId: - $ref: '#/components/parameters/streetlightId' - 'smartylighting.streetlights.1.0.action.{streetlightId}.turn.off': - subscribe: - operationId: turnOff - traits: - - $ref: '#/components/operationTraits/kafka' - message: - $ref: '#/components/messages/turnOnOff' - parameters: - streetlightId: - $ref: '#/components/parameters/streetlightId' - 'smartylighting.streetlights.1.0.action.{streetlightId}.dim': - subscribe: - operationId: dimLight - traits: - - $ref: '#/components/operationTraits/kafka' - message: - $ref: '#/components/messages/dimLight' - parameters: - streetlightId: - $ref: '#/components/parameters/streetlightId' -components: - schemas: - lightMeasuredPayload: - type: object - properties: - lumens: - type: integer - description: Light intensity measured in lumens. - minimum: 0 - sentAt: - $ref: '#/components/schemas/sentAt' - turnOnOffPayload: - type: object - properties: - command: - type: string - description: Whether to turn on or off the light. - enum: - - on - - off - sentAt: - $ref: '#/components/schemas/sentAt' - dimLightPayload: - type: object - properties: - percentage: - type: integer - description: Percentage to which the light should be dimmed to. - maximum: 100 - minimum: 0 - sentAt: - $ref: '#/components/schemas/sentAt' - sentAt: - type: string - format: date-time - description: Date and time when the message was sent. - messages: - lightMeasured: - payload: - $ref: '#/components/schemas/lightMeasuredPayload' - contentType: application/json - name: lightMeasured - title: Light measured - summary: Inform about environmental lighting conditions of a particular streetlight. - traits: - - $ref: '#/components/messageTraits/commonHeaders' - turnOnOff: - payload: - $ref: '#/components/schemas/turnOnOffPayload' - name: turnOnOff - title: Turn on/off - summary: Command a particular streetlight to turn the lights on or off. - traits: - - $ref: '#/components/messageTraits/commonHeaders' - dimLight: - payload: - $ref: '#/components/schemas/dimLightPayload' - name: dimLight - title: Dim light - summary: Command a particular streetlight to dim the lights. - traits: - - $ref: '#/components/messageTraits/commonHeaders' - securitySchemes: - saslScram: - type: scramSha256 - description: Provide your username and password for SASL/SCRAM authentication - certs: - type: X509 - description: Download the certificate files from service provider - parameters: - streetlightId: - description: The ID of the streetlight. - schema: - type: string - operationTraits: - kafka: - bindings: - kafka: - clientId: - type: string - enum: - - my-app-id - messageTraits: - commonHeaders: - headers: - type: object - properties: - my-app-header: - type: integer - maximum: 100 - minimum: 0"; + """ + asyncapi: 2.6.0 + info: + title: Streetlights Kafka API + version: 1.0.0 + description: The Smartylighting Streetlights API allows you to remotely manage the city lights. + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 + servers: + scram-connections: + url: test.mykafkacluster.org:18092 + protocol: kafka-secure + description: Test broker secured with scramSha256 + security: + - saslScram: [] + tags: + - name: env:test-scram + description: This environment is meant for running internal tests through scramSha256 + - name: kind:remote + description: This server is a remote server. Not exposed by the application + - name: visibility:private + description: This resource is private and only available to certain users + mtls-connections: + url: test.mykafkacluster.org:28092 + protocol: kafka-secure + description: Test broker secured with X509 + security: + - certs: [] + tags: + - name: env:test-mtls + description: This environment is meant for running internal tests through mtls + - name: kind:remote + description: This server is a remote server. Not exposed by the application + - name: visibility:private + description: This resource is private and only available to certain users + defaultContentType: application/json + channels: + 'smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured': + description: The topic on which measured values may be produced and consumed. + publish: + operationId: receiveLightMeasurement + summary: Inform about environmental lighting conditions of a particular streetlight. + traits: + - $ref: '#/components/operationTraits/kafka' + message: + $ref: '#/components/messages/lightMeasured' + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + 'smartylighting.streetlights.1.0.action.{streetlightId}.turn.on': + subscribe: + operationId: turnOn + traits: + - $ref: '#/components/operationTraits/kafka' + message: + $ref: '#/components/messages/turnOnOff' + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + 'smartylighting.streetlights.1.0.action.{streetlightId}.turn.off': + subscribe: + operationId: turnOff + traits: + - $ref: '#/components/operationTraits/kafka' + message: + $ref: '#/components/messages/turnOnOff' + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + 'smartylighting.streetlights.1.0.action.{streetlightId}.dim': + subscribe: + operationId: dimLight + traits: + - $ref: '#/components/operationTraits/kafka' + message: + $ref: '#/components/messages/dimLight' + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + components: + schemas: + lightMeasuredPayload: + type: object + properties: + lumens: + type: integer + description: Light intensity measured in lumens. + minimum: 0 + sentAt: + $ref: '#/components/schemas/sentAt' + turnOnOffPayload: + type: object + properties: + command: + type: string + description: Whether to turn on or off the light. + enum: + - on + - off + sentAt: + $ref: '#/components/schemas/sentAt' + dimLightPayload: + type: object + properties: + percentage: + type: integer + description: Percentage to which the light should be dimmed to. + maximum: 100 + minimum: 0 + sentAt: + $ref: '#/components/schemas/sentAt' + sentAt: + type: string + format: date-time + description: Date and time when the message was sent. + messages: + lightMeasured: + payload: + $ref: '#/components/schemas/lightMeasuredPayload' + contentType: application/json + name: lightMeasured + title: Light measured + summary: Inform about environmental lighting conditions of a particular streetlight. + traits: + - $ref: '#/components/messageTraits/commonHeaders' + turnOnOff: + payload: + $ref: '#/components/schemas/turnOnOffPayload' + name: turnOnOff + title: Turn on/off + summary: Command a particular streetlight to turn the lights on or off. + traits: + - $ref: '#/components/messageTraits/commonHeaders' + dimLight: + payload: + $ref: '#/components/schemas/dimLightPayload' + name: dimLight + title: Dim light + summary: Command a particular streetlight to dim the lights. + traits: + - $ref: '#/components/messageTraits/commonHeaders' + securitySchemes: + saslScram: + type: scramSha256 + description: Provide your username and password for SASL/SCRAM authentication + certs: + type: X509 + description: Download the certificate files from service provider + parameters: + streetlightId: + description: The ID of the streetlight. + schema: + type: string + operationTraits: + kafka: + bindings: + kafka: + clientId: + type: string + enum: + - my-app-id + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + maximum: 100 + minimum: 0 + """; - var asyncApiDocument = new AsyncApiDocumentBuilder() - .WithInfo(new AsyncApiInfo - { - Title = "Streetlights Kafka API", - Version = "1.0.0", - Description = "The Smartylighting Streetlights API allows you to remotely manage the city lights.", - License = new AsyncApiLicense + var asyncApiDocument = new AsyncApiDocumentBuilder() + .WithInfo(new AsyncApiInfo { - Name = "Apache 2.0", - Url = new Uri("https://www.apache.org/licenses/LICENSE-2.0"), - }, - }) - .WithServer("scram-connections", new AsyncApiServer - { - Url = "test.mykafkacluster.org:18092", - Protocol = "kafka-secure", - Description = "Test broker secured with scramSha256", - Security = new List + Title = "Streetlights Kafka API", + Version = "1.0.0", + Description = "The Smartylighting Streetlights API allows you to remotely manage the city lights.", + License = new AsyncApiLicense + { + Name = "Apache 2.0", + Url = new Uri("https://www.apache.org/licenses/LICENSE-2.0"), + }, + }) + .WithServer("scram-connections", new AsyncApiServer { + Url = "test.mykafkacluster.org:18092", + Protocol = "kafka-secure", + Description = "Test broker secured with scramSha256", + Security = new List + { new AsyncApiSecurityRequirement { { @@ -225,9 +237,9 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() }, new List() }, }, - }, - Tags = new List - { + }, + Tags = new List + { new AsyncApiTag { Name = "env:test-scram", @@ -243,15 +255,15 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Name = "visibility:private", Description = "This resource is private and only available to certain users", }, - }, - }) - .WithServer("mtls-connections", new AsyncApiServer - { - Url = "test.mykafkacluster.org:28092", - Protocol = "kafka-secure", - Description = "Test broker secured with X509", - Security = new List + }, + }) + .WithServer("mtls-connections", new AsyncApiServer { + Url = "test.mykafkacluster.org:28092", + Protocol = "kafka-secure", + Description = "Test broker secured with X509", + Security = new List + { new AsyncApiSecurityRequirement { { @@ -265,9 +277,9 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() }, new List() }, }, - }, - Tags = new List - { + }, + Tags = new List + { new AsyncApiTag { Name = "env:test-mtls", @@ -283,16 +295,16 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Name = "visibility:private", Description = "This resource is private and only available to certain users", }, - }, - }) - .WithDefaultContentType() - .WithChannel( - "smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured", - new AsyncApiChannel() - { - Description = "The topic on which measured values may be produced and consumed.", - Parameters = new Dictionary + }, + }) + .WithDefaultContentType() + .WithChannel( + "smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured", + new AsyncApiChannel() { + Description = "The topic on which measured values may be produced and consumed.", + Parameters = new Dictionary + { { "streetlightId", new AsyncApiParameter() { @@ -303,13 +315,13 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() }, } }, - }, - Publish = new AsyncApiOperation() - { - Summary = "Inform about environmental lighting conditions of a particular streetlight.", - OperationId = "receiveLightMeasurement", - Traits = new List + }, + Publish = new AsyncApiOperation() { + Summary = "Inform about environmental lighting conditions of a particular streetlight.", + OperationId = "receiveLightMeasurement", + Traits = new List + { new AsyncApiOperationTrait() { Reference = new AsyncApiReference() @@ -318,9 +330,9 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Type = ReferenceType.OperationTrait, }, }, - }, - Message = new List - { + }, + Message = new List + { new AsyncApiMessage() { Reference = new AsyncApiReference() @@ -329,15 +341,15 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Type = ReferenceType.Message, }, }, + }, }, - }, - }) - .WithChannel( - "smartylighting.streetlights.1.0.action.{streetlightId}.turn.on", - new AsyncApiChannel() - { - Parameters = new Dictionary + }) + .WithChannel( + "smartylighting.streetlights.1.0.action.{streetlightId}.turn.on", + new AsyncApiChannel() { + Parameters = new Dictionary + { { "streetlightId", new AsyncApiParameter() { @@ -348,12 +360,12 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() }, } }, - }, - Subscribe = new AsyncApiOperation() - { - OperationId = "turnOn", - Traits = new List + }, + Subscribe = new AsyncApiOperation() { + OperationId = "turnOn", + Traits = new List + { new AsyncApiOperationTrait() { Reference = new AsyncApiReference() @@ -362,9 +374,9 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Type = ReferenceType.OperationTrait, }, }, - }, - Message = new List - { + }, + Message = new List + { new AsyncApiMessage() { Reference = new AsyncApiReference() @@ -373,15 +385,15 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Type = ReferenceType.Message, }, }, + }, }, - }, - }) - .WithChannel( - "smartylighting.streetlights.1.0.action.{streetlightId}.turn.off", - new AsyncApiChannel() - { - Parameters = new Dictionary + }) + .WithChannel( + "smartylighting.streetlights.1.0.action.{streetlightId}.turn.off", + new AsyncApiChannel() { + Parameters = new Dictionary + { { "streetlightId", new AsyncApiParameter() { @@ -392,12 +404,12 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() }, } }, - }, - Subscribe = new AsyncApiOperation() - { - OperationId = "turnOff", - Traits = new List + }, + Subscribe = new AsyncApiOperation() { + OperationId = "turnOff", + Traits = new List + { new AsyncApiOperationTrait() { Reference = new AsyncApiReference() @@ -406,9 +418,9 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Type = ReferenceType.OperationTrait, }, }, - }, - Message = new List - { + }, + Message = new List + { new AsyncApiMessage() { Reference = new AsyncApiReference() @@ -417,15 +429,15 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Type = ReferenceType.Message, }, }, + }, }, - }, - }) - .WithChannel( - "smartylighting.streetlights.1.0.action.{streetlightId}.dim", - new AsyncApiChannel() - { - Parameters = new Dictionary + }) + .WithChannel( + "smartylighting.streetlights.1.0.action.{streetlightId}.dim", + new AsyncApiChannel() { + Parameters = new Dictionary + { { "streetlightId", new AsyncApiParameter() { @@ -436,12 +448,12 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() }, } }, - }, - Subscribe = new AsyncApiOperation() - { - OperationId = "dimLight", - Traits = new List + }, + Subscribe = new AsyncApiOperation() { + OperationId = "dimLight", + Traits = new List + { new AsyncApiOperationTrait() { Reference = new AsyncApiReference() @@ -450,9 +462,9 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Type = ReferenceType.OperationTrait, }, }, - }, - Message = new List - { + }, + Message = new List + { new AsyncApiMessage() { Reference = new AsyncApiReference() @@ -461,17 +473,17 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Type = ReferenceType.Message, }, }, + }, }, - }, - }) - .WithComponent("lightMeasured", new AsyncApiMessage() - { - Name = "lightMeasured", - Title = "Light measured", - Summary = "Inform about environmental lighting conditions of a particular streetlight.", - ContentType = "application/json", - Traits = new List() + }) + .WithComponent("lightMeasured", new AsyncApiMessage() { + Name = "lightMeasured", + Title = "Light measured", + Summary = "Inform about environmental lighting conditions of a particular streetlight.", + ContentType = "application/json", + Traits = new List() + { new AsyncApiMessageTrait() { Reference = new AsyncApiReference() @@ -480,23 +492,23 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Id = "commonHeaders", }, }, - }, - Payload = new AsyncApiSchema() - { - Reference = new AsyncApiReference() + }, + Payload = new AsyncApiSchema() { - Type = ReferenceType.Schema, - Id = "lightMeasuredPayload", + Reference = new AsyncApiReference() + { + Type = ReferenceType.Schema, + Id = "lightMeasuredPayload", + }, }, - }, - }) - .WithComponent("turnOnOff", new AsyncApiMessage() - { - Name = "turnOnOff", - Title = "Turn on/off", - Summary = "Command a particular streetlight to turn the lights on or off.", - Traits = new List() + }) + .WithComponent("turnOnOff", new AsyncApiMessage() { + Name = "turnOnOff", + Title = "Turn on/off", + Summary = "Command a particular streetlight to turn the lights on or off.", + Traits = new List() + { new AsyncApiMessageTrait() { Reference = new AsyncApiReference() @@ -505,23 +517,23 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Id = "commonHeaders", }, }, - }, - Payload = new AsyncApiSchema() - { - Reference = new AsyncApiReference() + }, + Payload = new AsyncApiSchema() { - Type = ReferenceType.Schema, - Id = "turnOnOffPayload", + Reference = new AsyncApiReference() + { + Type = ReferenceType.Schema, + Id = "turnOnOffPayload", + }, }, - }, - }) - .WithComponent("dimLight", new AsyncApiMessage() - { - Name = "dimLight", - Title = "Dim light", - Summary = "Command a particular streetlight to dim the lights.", - Traits = new List() + }) + .WithComponent("dimLight", new AsyncApiMessage() { + Name = "dimLight", + Title = "Dim light", + Summary = "Command a particular streetlight to dim the lights.", + Traits = new List() + { new AsyncApiMessageTrait() { Reference = new AsyncApiReference() @@ -530,25 +542,25 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() Id = "commonHeaders", }, }, - }, - Payload = new AsyncApiSchema() - { - Reference = new AsyncApiReference() + }, + Payload = new AsyncApiSchema() { - Type = ReferenceType.Schema, - Id = "dimLightPayload", + Reference = new AsyncApiReference() + { + Type = ReferenceType.Schema, + Id = "dimLightPayload", + }, }, - }, - }) - .WithComponent("lightMeasuredPayload", new AsyncApiSchema() - { - Type = new List { SchemaType.Object }, - Properties = new Dictionary() + }) + .WithComponent("lightMeasuredPayload", new AsyncApiSchema() { + Type = SchemaType.Object, + Properties = new Dictionary() + { { "lumens", new AsyncApiSchema() { - Type = new List { SchemaType.Integer }, + Type = SchemaType.Integer, Minimum = 0, Description = "Light intensity measured in lumens.", } @@ -563,23 +575,23 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() }, } }, - }, - }) - .WithComponent("turnOnOffPayload", new AsyncApiSchema() - { - Type = new List { SchemaType.Object }, - Properties = new Dictionary() + }, + }) + .WithComponent("turnOnOffPayload", new AsyncApiSchema() { + Type = SchemaType.Object, + Properties = new Dictionary() + { { "command", new AsyncApiSchema() { - Type = new List { SchemaType.String }, - Enum = new List + Type = SchemaType.String, + Enum = new List { - new AsyncApiString("on"), - new AsyncApiString("off"), + new AsyncApiAny("on"), + new AsyncApiAny("off"), }, - Description = "Whether to turn on or off the light." + Description = "Whether to turn on or off the light.", } }, { @@ -592,17 +604,17 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() }, } }, - }, - }) - .WithComponent("dimLightPayload", new AsyncApiSchema() - { - Type = new List { SchemaType.Object }, - Properties = new Dictionary() + }, + }) + .WithComponent("dimLightPayload", new AsyncApiSchema() { + Type = SchemaType.Object, + Properties = new Dictionary() + { { "percentage", new AsyncApiSchema() { - Type = new List { SchemaType.Integer }, + Type = SchemaType.Integer, Description = "Percentage to which the light should be dimmed to.", Minimum = 0, Maximum = 100, @@ -618,192 +630,191 @@ public void AsyncApiDocument_WithStreetLightsExample_SerializesAndDeserializes() }, } }, - }, - }) - .WithComponent("sentAt", new AsyncApiSchema() - { - Type = new List { SchemaType.String }, - Format = "date-time", - Description = "Date and time when the message was sent.", - - }) - .WithComponent("saslScram", new AsyncApiSecurityScheme - { - Type = SecuritySchemeType.ScramSha256, - Description = "Provide your username and password for SASL/SCRAM authentication", - }) - .WithComponent("certs", new AsyncApiSecurityScheme - { - Type = SecuritySchemeType.X509, - Description = "Download the certificate files from service provider", - }) - .WithComponent("streetlightId", new AsyncApiParameter() - { - Description = "The ID of the streetlight.", - Schema = new AsyncApiSchema() + }, + }) + .WithComponent("sentAt", new AsyncApiSchema() { - Type = new List { SchemaType.String }, - }, - }) - .WithComponent("commonHeaders", new AsyncApiMessageTrait() - { - Headers = new AsyncApiSchema() + Type = SchemaType.String, + Format = "date-time", + Description = "Date and time when the message was sent.", + }) + .WithComponent("saslScram", new AsyncApiSecurityScheme { - Type = new List { SchemaType.Object }, - Properties = new Dictionary() + Type = SecuritySchemeType.ScramSha256, + Description = "Provide your username and password for SASL/SCRAM authentication", + }) + .WithComponent("certs", new AsyncApiSecurityScheme + { + Type = SecuritySchemeType.X509, + Description = "Download the certificate files from service provider", + }) + .WithComponent("streetlightId", new AsyncApiParameter() + { + Description = "The ID of the streetlight.", + Schema = new AsyncApiSchema() + { + Type = SchemaType.String, + }, + }) + .WithComponent("commonHeaders", new AsyncApiMessageTrait() + { + Headers = new AsyncApiSchema() { + Type = SchemaType.Object, + Properties = new Dictionary() + { { "my-app-header", new AsyncApiSchema() { - Type = new List { SchemaType.Integer }, + Type = SchemaType.Integer, Minimum = 0, Maximum = 100, } }, + }, }, - }, - }) - .WithComponent("kafka", new AsyncApiOperationTrait() - { - Bindings = new AsyncApiBindings() + }) + .WithComponent("kafka", new AsyncApiOperationTrait() { + Bindings = new AsyncApiBindings() + { { - BindingType.Kafka, new KafkaOperationBinding() + "kafka", new KafkaOperationBinding() { ClientId = new AsyncApiSchema() { - Type = new List { SchemaType.String }, - Enum = new List + Type = SchemaType.String, + Enum = new List { - new AsyncApiString("my-app-id"), + new AsyncApiAny("my-app-id"), }, }, } }, - }, - }) - .Build(); + }, + }) + .Build(); // Act var actual = asyncApiDocument.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); - actual = actual.MakeLineBreaksEnvironmentNeutral(); - expected = expected.MakeLineBreaksEnvironmentNeutral(); - // Assert - Assert.AreEqual(actual, expected); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); } [Test] public void SerializeV2_WithFullSpec_Serializes() { var expected = - @"asyncapi: '2.6.0' -info: - title: apiTitle - version: apiVersion - description: description - termsOfService: https://example.com/termsOfService - contact: - name: contactName - url: https://example.com/contact - email: contactEmail - license: - name: licenseName - url: https://example.com/license - x-extension: value - x-extension: value -id: documentId -servers: - myServer: - url: https://example.com/server - protocol: KafkaProtocol - protocolVersion: protocolVersion - description: serverDescription - security: - - securitySchemeName: - - requirementItem -channels: - channel1: - description: channelDescription - subscribe: - operationId: myOperation - summary: operationSummary - description: operationDescription - tags: - - name: tagName - description: tagDescription - externalDocs: - description: externalDocsDescription - url: https://example.com/externalDocs - traits: - - operationId: myOperation - summary: traitSummary - description: traitDescription - tags: - - name: tagName - description: tagDescription - externalDocs: - description: externalDocsDescription - url: https://example.com/externalDocs - x-extension: value - message: - oneOf: - - contentType: contentType - name: messageName - title: messageTitle - summary: messageSummary - description: messageDescription - - correlationId: - description: correlationDescription - location: correlationLocation - x-extension: value - schemaFormat: schemaFormat - contentType: contentType - name: messageName - title: messageTitle - summary: messageSummary - description: messageDescription - traits: - - headers: - title: schemaTitle - description: schemaDescription - writeOnly: true - examples: - - key: value - otherKey: 9223372036854775807 - name: traitName - title: traitTitle - summary: traitSummary - description: traitDescription - tags: - - name: tagName - description: tagDescription - externalDocs: - description: externalDocsDescription - url: https://example.com/externalDocs - examples: - - name: exampleName - summary: exampleSummary - payload: - key: value - otherKey: 9223372036854775807 + """ + asyncapi: 2.6.0 + info: + title: apiTitle + version: apiVersion + description: description + termsOfService: https://example.com/termsOfService + contact: + name: contactName + url: https://example.com/contact + email: contactEmail + license: + name: licenseName + url: https://example.com/license x-extension: value - x-extension: value - x-extension: value - x-extension: value -components: - securitySchemes: - securitySchemeName: - type: oauth2 - description: securitySchemeDescription - flows: - implicit: - authorizationUrl: https://example.com/authorization - tokenUrl: https://example.com/tokenUrl - refreshUrl: https://example.com/refresh - scopes: - securitySchemeScopeKey: securitySchemeScopeValue - x-extension: value"; + x-extension: value + id: documentId + servers: + myServer: + url: https://example.com/server + protocol: KafkaProtocol + protocolVersion: protocolVersion + description: serverDescription + security: + - securitySchemeName: + - requirementItem + channels: + channel1: + description: channelDescription + subscribe: + operationId: myOperation + summary: operationSummary + description: operationDescription + tags: + - name: tagName + description: tagDescription + externalDocs: + description: externalDocsDescription + url: https://example.com/externalDocs + traits: + - operationId: myOperation + summary: traitSummary + description: traitDescription + tags: + - name: tagName + description: tagDescription + externalDocs: + description: externalDocsDescription + url: https://example.com/externalDocs + x-extension: value + message: + oneOf: + - contentType: contentType + name: messageName + title: messageTitle + summary: messageSummary + description: messageDescription + - correlationId: + description: correlationDescription + location: correlationLocation + x-extension: value + schemaFormat: schemaFormat + contentType: contentType + name: messageName + title: messageTitle + summary: messageSummary + description: messageDescription + traits: + - headers: + title: schemaTitle + description: schemaDescription + writeOnly: true + examples: + - key: value + otherKey: 9223372036854775807 + name: traitName + title: traitTitle + summary: traitSummary + description: traitDescription + tags: + - name: tagName + description: tagDescription + externalDocs: + description: externalDocsDescription + url: https://example.com/externalDocs + examples: + - name: exampleName + summary: exampleSummary + payload: + key: value + otherKey: 9223372036854775807 + x-extension: value + x-extension: value + x-extension: value + x-extension: value + components: + securitySchemes: + securitySchemeName: + type: oauth2 + description: securitySchemeDescription + flows: + implicit: + authorizationUrl: https://example.com/authorization + tokenUrl: https://example.com/tokenUrl + refreshUrl: https://example.com/refresh + scopes: + securitySchemeScopeKey: securitySchemeScopeValue + x-extension: value + """; // Arrange var title = "apiTitle"; @@ -836,10 +847,8 @@ public void SerializeV2_WithFullSpec_Serializes() string traitTitle = "traitTitle"; string schemaTitle = "schemaTitle"; string schemaDescription = "schemaDescription"; - string anyKey = "key"; - string anyOtherKey = "otherKey"; string anyStringValue = "value"; - long anyLongValue = Int64.MaxValue; + long anyLongValue = long.MaxValue; string exampleSummary = "exampleSummary"; string exampleName = "exampleName"; string traitDescription = "traitDescription"; @@ -862,6 +871,7 @@ public void SerializeV2_WithFullSpec_Serializes() string refreshUrl = "https://example.com/refresh"; string authorizationUrl = "https://example.com/authorization"; string requirementString = "requirementItem"; + var document = new AsyncApiDocument() { Id = documentId, @@ -890,7 +900,7 @@ public void SerializeV2_WithFullSpec_Serializes() AuthorizationUrl = new Uri(authorizationUrl), Extensions = new Dictionary { - { extensionKey, new AsyncApiString(extensionString) }, + { extensionKey, new AsyncApiAny(extensionString) }, }, }, }, @@ -945,14 +955,14 @@ public void SerializeV2_WithFullSpec_Serializes() Url = new Uri(licenseUri), Extensions = new Dictionary { - { extensionKey, new AsyncApiString(extensionString) }, + { extensionKey, new AsyncApiAny(extensionString) }, }, }, Version = apiVersion, TermsOfService = new Uri(termsOfServiceUri), Extensions = new Dictionary { - { extensionKey, new AsyncApiString(extensionString) }, + { extensionKey, new AsyncApiAny(extensionString) }, }, }, Channels = new Dictionary @@ -998,7 +1008,7 @@ public void SerializeV2_WithFullSpec_Serializes() Description = correlationDescription, Extensions = new Dictionary { - { extensionKey, new AsyncApiString(extensionString) }, + { extensionKey, new AsyncApiAny(extensionString) }, }, }, Traits = new List @@ -1012,13 +1022,13 @@ public void SerializeV2_WithFullSpec_Serializes() Title = schemaTitle, WriteOnly = true, Description = schemaDescription, - Examples = new List + Examples = new List { - new AsyncApiObject + new AsyncApiAny(new ExtensionClass { - { anyKey, new AsyncApiString(anyStringValue) }, - { anyOtherKey, new AsyncApiLong(anyLongValue) }, - }, + Key = anyStringValue, + OtherKey = anyLongValue, + }), }, }, Examples = new List @@ -1027,14 +1037,14 @@ public void SerializeV2_WithFullSpec_Serializes() { Summary = exampleSummary, Name = exampleName, - Payload = new AsyncApiObject + Payload = new AsyncApiAny(new ExtensionClass { - { anyKey, new AsyncApiString(anyStringValue) }, - { anyOtherKey, new AsyncApiLong(anyLongValue) }, - }, + Key = anyStringValue, + OtherKey = anyLongValue, + }), Extensions = new Dictionary { - { extensionKey, new AsyncApiString(extensionString) }, + { extensionKey, new AsyncApiAny(extensionString) }, }, }, }, @@ -1055,20 +1065,20 @@ public void SerializeV2_WithFullSpec_Serializes() }, Extensions = new Dictionary { - { extensionKey, new AsyncApiString(extensionString) }, + { extensionKey, new AsyncApiAny(extensionString) }, }, }, }, Extensions = new Dictionary { - { extensionKey, new AsyncApiString(extensionString) }, + { extensionKey, new AsyncApiAny(extensionString) }, }, } }, }, Extensions = new Dictionary { - { extensionKey, new AsyncApiString(extensionString) }, + { extensionKey, new AsyncApiAny(extensionString) }, }, Tags = new List { @@ -1100,7 +1110,7 @@ public void SerializeV2_WithFullSpec_Serializes() OperationId = operationId, Extensions = new Dictionary { - { extensionKey, new AsyncApiString(extensionString) }, + { extensionKey, new AsyncApiAny(extensionString) }, }, }, }, @@ -1110,51 +1120,143 @@ public void SerializeV2_WithFullSpec_Serializes() }, }; - var outputString = new StringWriter(CultureInfo.InvariantCulture); + var outputString = new StringWriter(); var writer = new AsyncApiYamlWriter(outputString); // Act document.SerializeV2(writer); var actual = outputString.GetStringBuilder().ToString(); - actual = actual.MakeLineBreaksEnvironmentNeutral(); - expected = expected.MakeLineBreaksEnvironmentNeutral(); - // Assert - Assert.AreEqual(actual, expected); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + } + + [Test] + public void Serialize_WithBindingReferences_SerializesDeserializes() + { + var doc = new AsyncApiDocument(); + doc.Info = new AsyncApiInfo() + { + Description = "test description", + }; + doc.Servers.Add("production", new AsyncApiServer + { + Description = "test description", + Protocol = "pulsar+ssl", + Url = "example.com", + Bindings = new AsyncApiBindings() + { + Reference = new AsyncApiReference() + { + Type = ReferenceType.ServerBindings, + Id = "bindings", + }, + }, + }); + doc.Components = new AsyncApiComponents() + { + Channels = new Dictionary() + { + { + "otherchannel", new AsyncApiChannel() + { + Publish = new AsyncApiOperation() + { + Description = "test", + }, + Bindings = new AsyncApiBindings() + { + Reference = new AsyncApiReference() + { + Type = ReferenceType.ChannelBindings, + Id = "bindings", + }, + }, + } + }, + }, + ServerBindings = new Dictionary>() + { + { + "bindings", new AsyncApiBindings() + { + new PulsarServerBinding() + { + Tenant = "staging", + }, + } + }, + }, + ChannelBindings = new Dictionary>() + { + { + "bindings", new AsyncApiBindings() + { + new PulsarChannelBinding() + { + Namespace = "users", + Persistence = AsyncAPI.Models.Bindings.Pulsar.Persistence.Persistent, + }, + } + }, + }, + }; + if (doc.Channels == null) + { + doc.Channels = new Dictionary(); + } + doc.Channels.Add( + "testChannel", + new AsyncApiChannel + { + Reference = new AsyncApiReference() + { + Type = ReferenceType.Channel, + Id = "otherchannel", + }, + }); + var actual = doc.Serialize(AsyncApiVersion.AsyncApi2_0, AsyncApiFormat.Yaml); + + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.Pulsar; + var reader = new AsyncApiStringReader(settings); + var deserialized = reader.Read(actual, out var diagnostic); } [Test] public void Serializev2_WithBindings_Serializes() { - var expected = @"asyncapi: '2.6.0' -info: - description: test description -servers: - production: - url: example.com - protocol: pulsar+ssl - description: test description -channels: - testChannel: - publish: - message: - bindings: - http: - headers: - description: this mah binding - kafka: - key: - description: this mah other binding - bindings: - kafka: - partitions: 2 - replicas: 1"; + var expected = """ + asyncapi: 2.6.0 + info: + description: test description + servers: + production: + url: example.com + protocol: pulsar+ssl + description: test description + channels: + testChannel: + publish: + message: + bindings: + http: + headers: + description: this mah binding + kafka: + key: + description: this mah other binding + bindings: + kafka: + partitions: 2 + replicas: 1 + """; var doc = new AsyncApiDocument(); doc.Info = new AsyncApiInfo() { - Description = "test description" + Description = "test description", }; doc.Servers.Add("production", new AsyncApiServer { @@ -1162,7 +1264,12 @@ public void Serializev2_WithBindings_Serializes() Protocol = "pulsar+ssl", Url = "example.com", }); - doc.Channels.Add("testChannel", + if (doc.Channels == null) + { + doc.Channels = new Dictionary(); + } + doc.Channels.Add( + "testChannel", new AsyncApiChannel { Bindings = new AsyncApiBindings @@ -1202,7 +1309,6 @@ public void Serializev2_WithBindings_Serializes() }, } }, - }, } }, @@ -1211,21 +1317,52 @@ public void Serializev2_WithBindings_Serializes() }); var actual = doc.Serialize(AsyncApiVersion.AsyncApi2_0, AsyncApiFormat.Yaml); - var reader = new AsyncApiStringReader(); + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.All; + var reader = new AsyncApiStringReader(settings); var deserialized = reader.Read(actual, out var diagnostic); - actual = actual.MakeLineBreaksEnvironmentNeutral(); - expected = expected.MakeLineBreaksEnvironmentNeutral(); - // Assert - Assert.AreEqual(actual, expected); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); Assert.AreEqual(2, deserialized.Channels.First().Value.Publish.Message.First().Bindings.Count); var binding = deserialized.Channels.First().Value.Publish.Message.First().Bindings.First(); - Assert.AreEqual(BindingType.Http, binding.Key); + Assert.AreEqual("http", binding.Key); var httpBinding = binding.Value as HttpMessageBinding; Assert.AreEqual("this mah binding", httpBinding.Headers.Description); } + + + + [Test] + public void SerializeV2_EmptyChannelObject_DeserializeAndSerializePreserveChannelObject() + { + // Arrange + var spec = """ + asyncapi: 2.6.0 + info: + title: Spec with missing channel info + description: test description + servers: + production: + url: example.com + protocol: pulsar+ssl + description: test description + channels: { } + """; + + var settings = new AsyncApiReaderSettings(); + var reader = new AsyncApiStringReader(settings); + + // Act + var deserialized = reader.Read(spec, out var diagnostic); + var actual = deserialized.Serialize(AsyncApiVersion.AsyncApi2_0, AsyncApiFormat.Yaml); + + // Assert + actual.Should() + .BePlatformAgnosticEquivalentTo(spec); + } } -} +} \ No newline at end of file diff --git a/test/LEGO.AsyncAPI.Tests/AsyncApiLicenseTests.cs b/test/LEGO.AsyncAPI.Tests/AsyncApiLicenseTests.cs index 7059b6fe..1edcbe37 100644 --- a/test/LEGO.AsyncAPI.Tests/AsyncApiLicenseTests.cs +++ b/test/LEGO.AsyncAPI.Tests/AsyncApiLicenseTests.cs @@ -1,45 +1,45 @@ -using FluentAssertions; -using LEGO.AsyncAPI.Models; -using LEGO.AsyncAPI.Models.Any; -using LEGO.AsyncAPI.Models.Interfaces; -using LEGO.AsyncAPI.Readers; -using LEGO.AsyncAPI.Readers.ParseNodes; -using NUnit.Framework; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using YamlDotNet.RepresentationModel; +// Copyright (c) The LEGO Group. All rights reserved. namespace LEGO.AsyncAPI.Tests { - public class AsyncApiLicenseTests + using System; + using System.Collections.Generic; + using System.IO; + using System.Text.Json.Nodes; + using FluentAssertions; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Readers; + using LEGO.AsyncAPI.Readers.ParseNodes; + using NUnit.Framework; + + public class AsyncApiLicenseTests : TestBase { [Test] public void Serialize_WithAllProperties_Serializes() { - var expected = @"{ - ""name"": ""test"", - ""url"": ""https://example.com/license"", - ""x-extension"": ""value"" -}"; + var expected = """ + { + "name": "test", + "url": "https://example.com/license", + "x-extension": "value" + } + """; var license = new AsyncApiLicense() { Name = "test", Url = new Uri("https://example.com/license"), Extensions = new Dictionary { - ["x-extension"] = new AsyncApiString("value"), + ["x-extension"] = new AsyncApiAny("value"), }, }; var actual = license.SerializeAsJson(AsyncApiVersion.AsyncApi2_0); // Assert - actual = actual.MakeLineBreaksEnvironmentNeutral(); - expected = expected.MakeLineBreaksEnvironmentNeutral(); - - Assert.AreEqual(expected, actual); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); } public static Stream GenerateStreamFromString(string s) @@ -56,22 +56,21 @@ public static Stream GenerateStreamFromString(string s) public void LoadLicense_WithJson_Deserializes() { // Arrange - var input = @"{ - ""name"": ""test"", - ""url"": ""https://example.com/license"", - ""x-extension"": ""value"" -}"; + var input = """ + { + "name": "test", + "url": "https://example.com/license", + "x-extension": "value" + } + """; using (var stream = GenerateStreamFromString(input)) { - var yamlStream = new YamlStream(); - yamlStream.Load(new StreamReader(stream)); - var yamlNode = yamlStream.Documents.First().RootNode; - var diagnostic = new AsyncApiDiagnostic(); - var context = new ParsingContext(diagnostic); + var settings = new AsyncApiReaderSettings(); + var context = new ParsingContext(diagnostic, settings); - var node = new MapNode(context, (YamlMappingNode)yamlNode); + var node = new MapNode(context, JsonNode.Parse(stream)); // Act var actual = AsyncApiV2Deserializer.LoadLicense(node); @@ -83,7 +82,7 @@ public void LoadLicense_WithJson_Deserializes() Url = new Uri("https://example.com/license"), Extensions = new Dictionary { - ["x-extension"] = new AsyncApiString("value"), + ["x-extension"] = new AsyncApiAny("value"), }, }; actual.Should().BeEquivalentTo( diff --git a/test/LEGO.AsyncAPI.Tests/AsyncApiReaderTests.cs b/test/LEGO.AsyncAPI.Tests/AsyncApiReaderTests.cs index 303b5fa9..8953f2cd 100644 --- a/test/LEGO.AsyncAPI.Tests/AsyncApiReaderTests.cs +++ b/test/LEGO.AsyncAPI.Tests/AsyncApiReaderTests.cs @@ -1,8 +1,15 @@ +// Copyright (c) The LEGO Group. All rights reserved. + namespace LEGO.AsyncAPI.Tests { using System; + using System.Collections.Generic; using System.Linq; + using System.Text.Json.Nodes; + using FluentAssertions; + using LEGO.AsyncAPI.Exceptions; using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Readers; using NUnit.Framework; @@ -16,190 +23,280 @@ public void Read_WithMissingEverything_DeserializesWithErrors() var doc = reader.Read(yaml, out var diagnostic); } - [Test] - public void Read_WithBasicPlusContact_Deserializes() - { - var yaml = @"asyncapi: 2.3.0 -info: - title: test - version: 1.0.0 - contact: - name: API Support - url: https://www.example.com/support - email: support@example.com -channels: - workspace: - x-eventarchetype: objectchanged -"; - var reader = new AsyncApiStringReader(); - var doc = reader.Read(yaml, out var diagnostic); - Assert.AreEqual("support@example.com", doc.Info.Contact.Email); - Assert.AreEqual(new Uri("https://www.example.com/support"), doc.Info.Contact.Url); - Assert.AreEqual("API Support", doc.Info.Contact.Name); - } + [Test] + public void Read_WithExtensionParser_Parses() + { + var extensionName = "x-someValue"; + var yaml = $""" + asyncapi: 2.3.0 + info: + title: test + version: 1.0.0 + contact: + name: API Support + url: https://www.example.com/support + email: support@example.com + channels: + workspace: + {extensionName}: onetwothreefour + """; + Func valueExtensionParser = (any) => + { + if (any.TryGetValue(out var value)) + { + if (value == "onetwothreefour") + { + return new AsyncApiAny(1234); + } + } + + return new AsyncApiAny("No value provided"); + }; + + var settings = new AsyncApiReaderSettings + { + ExtensionParsers = new Dictionary> + { + { extensionName, valueExtensionParser }, + }, + }; + + var reader = new AsyncApiStringReader(settings); + var doc = reader.Read(yaml, out var diagnostic); + Assert.AreEqual((doc.Channels["workspace"].Extensions[extensionName] as AsyncApiAny).GetValue(), 1234); + } + + [Test] + public void Read_WithThrowingExtensionParser_AddsToDiagnostics() + { + var extensionName = "x-fail"; + var yaml = $""" + asyncapi: 2.3.0 + info: + title: test + version: 1.0.0 + contact: + name: API Support + url: https://www.example.com/support + email: support@example.com + channels: + workspace: + {extensionName}: onetwothreefour + """; + Func failingExtensionParser = (any) => + { + throw new AsyncApiException("Failed to parse"); + }; + + var settings = new AsyncApiReaderSettings + { + ExtensionParsers = new Dictionary> + { + { extensionName, failingExtensionParser }, + }, + }; + + var reader = new AsyncApiStringReader(settings); + var doc = reader.Read(yaml, out var diagnostic); + + Assert.IsNotEmpty(diagnostic.Errors); + + var error = diagnostic.Errors.First(); + Assert.AreEqual("#/channels/workspace/x-fail", error.Pointer); + Assert.AreEqual("Failed to parse", error.Message); + } + + [Test] + public void Read_WithBasicPlusContact_Deserializes() + { + var yaml = """ + asyncapi: 2.3.0 + info: + title: test + version: 1.0.0 + contact: + name: API Support + url: https://www.example.com/support + email: support@example.com + channels: + workspace: + x-eventarchetype: objectchanged + """; + var reader = new AsyncApiStringReader(); + var doc = reader.Read(yaml, out var diagnostic); + Assert.AreEqual("support@example.com", doc.Info.Contact.Email); + Assert.AreEqual(new Uri("https://www.example.com/support"), doc.Info.Contact.Url); + Assert.AreEqual("API Support", doc.Info.Contact.Name); + } - [Test] - public void Read_WithBasicPlusExternalDocs_Deserializes() - { - var yaml = @"asyncapi: 2.3.0 -info: - title: test - version: 1.0.0 -channels: - workspace: - publish: - bindings: - http: - type: response - message: - $ref: '#/components/messages/WorkspaceEventPayload' -components: - messages: - WorkspaceEventPayload: - schemaFormat: application/schema+yaml;version=draft-07 - externalDocs: - description: Find more info here - url: https://example.com -"; - var reader = new AsyncApiStringReader(); - var doc = reader.Read(yaml, out var diagnostic); - var message = doc.Channels["workspace"].Publish.Message; - Assert.AreEqual(new Uri("https://example.com"), message.First().ExternalDocs.Url); - Assert.AreEqual("Find more info here", message.First().ExternalDocs.Description); - } + [Test] + public void Read_WithBasicPlusExternalDocs_Deserializes() + { + var yaml = """ + asyncapi: 2.3.0 + info: + title: test + version: 1.0.0 + channels: + workspace: + publish: + bindings: + http: + type: response + message: + $ref: '#/components/messages/WorkspaceEventPayload' + components: + messages: + WorkspaceEventPayload: + schemaFormat: application/schema+yaml;version=draft-07 + externalDocs: + description: Find more info here + url: https://example.com + """; + var reader = new AsyncApiStringReader(); + var doc = reader.Read(yaml, out var diagnostic); + var message = doc.Channels["workspace"].Publish.Message; + Assert.AreEqual(new Uri("https://example.com"), message.First().ExternalDocs.Url); + Assert.AreEqual("Find more info here", message.First().ExternalDocs.Description); + } - [Test] - public void Read_WithBasicPlusTag_Deserializes() - { - var yaml = @"asyncapi: 2.3.0 -info: - title: test - version: 1.0.0 -channels: - workspace: - x-eventarchetype: objectchanged -tags: - - name: user - description: User-related messages -"; - var reader = new AsyncApiStringReader(); - var doc = reader.Read(yaml, out var diagnostic); - var tag = doc.Tags.First(); - Assert.AreEqual("user", tag.Name); - Assert.AreEqual("User-related messages", tag.Description); - } + [Test] + public void Read_WithBasicPlusTag_Deserializes() + { + var yaml = """ + asyncapi: 2.3.0 + info: + title: test + version: 1.0.0 + channels: + workspace: + x-eventarchetype: objectchanged + tags: + - name: user + description: User-related messages + """; + var reader = new AsyncApiStringReader(); + var doc = reader.Read(yaml, out var diagnostic); + var tag = doc.Tags.First(); + Assert.AreEqual("user", tag.Name); + Assert.AreEqual("User-related messages", tag.Description); + } - [Test] - public void Read_WithBasicPlusServerDeserializes() - { - var yaml = @"asyncapi: 2.3.0 -info: - title: test - version: 1.0.0 -channels: - workspace: - x-eventarchetype: objectchanged -servers: - production: - url: 'pulsar+ssl://prod.events.managed.io:1234' - protocol: pulsar+ssl - description: Pulsar broker -"; - var reader = new AsyncApiStringReader(); - var doc = reader.Read(yaml, out var diagnostic); - var server = doc.Servers.First(); - Assert.AreEqual("production", server.Key); - Assert.AreEqual("pulsar+ssl://prod.events.managed.io:1234", server.Value.Url); - Assert.AreEqual("pulsar+ssl", server.Value.Protocol); - Assert.AreEqual("Pulsar broker", server.Value.Description); - } + [Test] + public void Read_WithBasicPlusServerDeserializes() + { + var yaml = """ + asyncapi: 2.3.0 + info: + title: test + version: 1.0.0 + channels: + workspace: + x-eventarchetype: objectchanged + servers: + production: + url: 'pulsar+ssl://prod.events.managed.io:1234' + protocol: pulsar+ssl + description: Pulsar broker + """; + var reader = new AsyncApiStringReader(); + var doc = reader.Read(yaml, out var diagnostic); + var server = doc.Servers.First(); + Assert.AreEqual("production", server.Key); + Assert.AreEqual("pulsar+ssl://prod.events.managed.io:1234", server.Value.Url); + Assert.AreEqual("pulsar+ssl", server.Value.Protocol); + Assert.AreEqual("Pulsar broker", server.Value.Description); + } - [Test] - public void Read_WithBasicPlusServerVariablesDeserializes() - { - var yaml = @"asyncapi: 2.3.0 -info: - title: test - version: 1.0.0 -channels: - workspace: - x-eventarchetype: objectchanged -servers: - production: - url: 'pulsar+ssl://prod.events.managed.io:{port}' - protocol: pulsar+ssl - description: Pulsar broker - variables: - port: - description: Secure connection (TLS) is available through port 8883. - default: '1883' - enum: - - '1883' - - '8883' -"; - var reader = new AsyncApiStringReader(); - var doc = reader.Read(yaml, out var diagnostic); - var server = doc.Servers.First(); - var variable = server.Value.Variables.First(); - Assert.AreEqual("production", server.Key); - Assert.AreEqual("port", variable.Key); - Assert.AreEqual("Secure connection (TLS) is available through port 8883.", variable.Value.Description); - } + [Test] + public void Read_WithBasicPlusServerVariablesDeserializes() + { + var yaml = """ + asyncapi: 2.3.0 + info: + title: test + version: 1.0.0 + channels: + workspace: + x-eventarchetype: objectchanged + servers: + production: + url: 'pulsar+ssl://prod.events.managed.io:{port}' + protocol: pulsar+ssl + description: Pulsar broker + variables: + port: + description: Secure connection (TLS) is available through port 8883. + default: '1883' + enum: + - '1883' + - '8883' + """; + var reader = new AsyncApiStringReader(); + var doc = reader.Read(yaml, out var diagnostic); + var server = doc.Servers.First(); + var variable = server.Value.Variables.First(); + Assert.AreEqual("production", server.Key); + Assert.AreEqual("port", variable.Key); + Assert.AreEqual("Secure connection (TLS) is available through port 8883.", variable.Value.Description); + } - [Test] - public void Read_WithBasicPlusCorrelationIDDeserializes() - { - var yaml = @"asyncapi: 2.3.0 -info: - title: test - version: 1.0.0 -channels: - workspace: - publish: - bindings: - http: - type: response - message: - $ref: '#/components/messages/WorkspaceEventPayload' -components: - messages: - WorkspaceEventPayload: - schemaFormat: application/schema+yaml;version=draft-07 - correlationId: - description: Default Correlation ID - location: $message.header#/correlationId -"; - var reader = new AsyncApiStringReader(); - var doc = reader.Read(yaml, out var diagnostic); - var message = doc.Channels["workspace"].Publish.Message; - Assert.AreEqual("Default Correlation ID", message.First().CorrelationId.Description); - Assert.AreEqual("$message.header#/correlationId", message.First().CorrelationId.Location); - } + [Test] + public void Read_WithBasicPlusCorrelationIDDeserializes() + { + var yaml = """ + asyncapi: 2.3.0 + info: + title: test + version: 1.0.0 + channels: + workspace: + publish: + bindings: + http: + type: response + message: + $ref: '#/components/messages/WorkspaceEventPayload' + components: + messages: + WorkspaceEventPayload: + schemaFormat: application/schema+yaml;version=draft-07 + correlationId: + description: Default Correlation ID + location: $message.header#/correlationId + """; + var reader = new AsyncApiStringReader(); + var doc = reader.Read(yaml, out var diagnostic); + var message = doc.Channels["workspace"].Publish.Message; + Assert.AreEqual("Default Correlation ID", message.First().CorrelationId.Description); + Assert.AreEqual("$message.header#/correlationId", message.First().CorrelationId.Location); + } [Test] public void Read_WithOneOfMessage_Reads() { - var yaml = @"asyncapi: 2.3.0 -info: - title: test - version: 1.0.0 -channels: - workspace: - publish: - bindings: - http: - type: response - message: - oneOf: - - $ref: '#/components/messages/WorkspaceEventPayload' -components: - messages: - WorkspaceEventPayload: - schemaFormat: application/schema+yaml;version=draft-07 - correlationId: - description: Default Correlation ID - location: $message.header#/correlationId -"; + var yaml = """ + asyncapi: 2.3.0 + info: + title: test + version: 1.0.0 + channels: + workspace: + publish: + bindings: + http: + type: response + message: + oneOf: + - $ref: '#/components/messages/WorkspaceEventPayload' + components: + messages: + WorkspaceEventPayload: + schemaFormat: application/schema+yaml;version=draft-07 + correlationId: + description: Default Correlation ID + location: $message.header#/correlationId + """; var reader = new AsyncApiStringReader(); var doc = reader.Read(yaml, out var diagnostic); var message = doc.Channels["workspace"].Publish.Message.First(); @@ -208,102 +305,131 @@ public void Read_WithOneOfMessage_Reads() } [Test] - public void Read_WithBasicPlusSecuritySchemeDeserializes() - { - var yaml = @"asyncapi: 2.3.0 -info: - title: test - version: 1.0.0 -channels: - workspace: - publish: - bindings: - http: - type: response - message: - $ref: '#/components/messages/WorkspaceEventPayload' -components: - messages: - WorkspaceEventPayload: - schemaFormat: application/schema+yaml;version=draft-07 - securitySchemes: - saslScram: - type: scramSha256 - description: Provide your username and password for SASL/SCRAM authentication -"; - var reader = new AsyncApiStringReader(); - var doc = reader.Read(yaml, out var diagnostic); - var scheme = doc.Components.SecuritySchemes.First(); - Assert.AreEqual("saslScram", scheme.Key); - Assert.AreEqual(SecuritySchemeType.ScramSha256, scheme.Value.Type); - Assert.AreEqual("Provide your username and password for SASL/SCRAM authentication", scheme.Value.Description); - } + public void Read_WithBasicPlusSecuritySchemeDeserializes() + { + var yaml = """ + asyncapi: 2.3.0 + info: + title: test + version: 1.0.0 + channels: + workspace: + publish: + bindings: + http: + type: response + message: + $ref: '#/components/messages/WorkspaceEventPayload' + components: + messages: + WorkspaceEventPayload: + schemaFormat: application/schema+yaml;version=draft-07 + securitySchemes: + saslScram: + type: scramSha256 + description: Provide your username and password for SASL/SCRAM authentication + """; + var reader = new AsyncApiStringReader(); + var doc = reader.Read(yaml, out var diagnostic); + var scheme = doc.Components.SecuritySchemes.First(); + Assert.AreEqual("saslScram", scheme.Key); + Assert.AreEqual(SecuritySchemeType.ScramSha256, scheme.Value.Type); + Assert.AreEqual("Provide your username and password for SASL/SCRAM authentication", scheme.Value.Description); + } + + [Test] + public void Read_WithWrongReference_AddsError() + { + var yaml = + """ + asyncapi: 2.3.0 + info: + title: test + version: 1.0.0 + channels: + workspace: + publish: + message: + $ref: '#/components/securitySchemes/saslScram' + components: + securitySchemes: + saslScram: + type: scramSha256 + description: Provide your username and password for SASL/SCRAM authentication + """; + var reader = new AsyncApiStringReader(); + var doc = reader.Read(yaml, out var diagnostic); + diagnostic.Errors.Should().NotBeEmpty(); + doc.Channels.Values.First().Publish.Message.First().Should().BeNull(); + } - [Test] - public void Read_WithBasicPlusOAuthFlowDeserializes() - { - var yaml = @"asyncapi: 2.3.0 -info: - title: test - version: 1.0.0 -channels: - workspace: - x-something: yes -components: - securitySchemes: - oauth2: - type: oauth2 - flows: - implicit: - authorizationUrl: https://example.com/api/oauth/dialog - scopes: - write:pets: modify pets in your account - read:pets: read your pets -"; - var reader = new AsyncApiStringReader(); - var doc = reader.Read(yaml, out var diagnostic); - var scheme = doc.Components.SecuritySchemes.First(); - var flow = scheme.Value.Flows; - Assert.AreEqual("oauth2", scheme.Key); - Assert.AreEqual(SecuritySchemeType.OAuth2, scheme.Value.Type); - Assert.AreEqual(new Uri("https://example.com/api/oauth/dialog"), flow.Implicit.AuthorizationUrl); - Assert.IsTrue(flow.Implicit.Scopes.ContainsKey("write:pets")); - Assert.IsTrue(flow.Implicit.Scopes.ContainsKey("read:pets")); - } + [Test] + public void Read_WithBasicPlusOAuthFlowDeserializes() + { + var yaml = """ + asyncapi: 2.3.0 + info: + title: test + version: 1.0.0 + channels: + workspace: + x-something: yes + components: + securitySchemes: + oauth2: + type: oauth2 + flows: + implicit: + authorizationUrl: https://example.com/api/oauth/dialog + scopes: + write:pets: modify pets in your account + read:pets: read your pets + """; + var reader = new AsyncApiStringReader(); + var doc = reader.Read(yaml, out var diagnostic); + var scheme = doc.Components.SecuritySchemes.First(); + var flow = scheme.Value.Flows; + Assert.AreEqual("oauth2", scheme.Key); + Assert.AreEqual(SecuritySchemeType.OAuth2, scheme.Value.Type); + Assert.AreEqual(new Uri("https://example.com/api/oauth/dialog"), flow.Implicit.AuthorizationUrl); + Assert.IsTrue(flow.Implicit.Scopes.ContainsKey("write:pets")); + Assert.IsTrue(flow.Implicit.Scopes.ContainsKey("read:pets")); + } [Test] public void Read_WithServerReference_ResolvesReference() { - var yaml = @"asyncapi: 2.3.0 -info: - title: test - version: 1.0.0 -servers: - production: - $ref: '#/components/servers/production' -channels: - workspace: - x-something: yes -components: - servers: - production: - url: 'pulsar+ssl://prod.events.managed.io:1234' - protocol: pulsar+ssl - description: Pulsar broker - security: - - petstore_auth: - - write:pets - - read:pets - securitySchemes: - petstore_auth: - type: oauth2 - flows: - implicit: - authorizationUrl: https://example.com/api/oauth/dialog - scopes: - write:pets: modify pets in your account - read:pets: read your pets -"; + var yaml = """ + asyncapi: 2.3.0 + info: + title: test + version: 1.0.0 + servers: + production: + $ref: '#/components/servers/production' + channels: + workspace: + x-something: yes + components: + servers: + production: + url: 'pulsar+ssl://prod.events.managed.io:1234' + protocol: pulsar+ssl + description: Pulsar broker + security: + - petstore_auth: + - write:pets + - read:pets + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: https://example.com/api/oauth/dialog + scopes: + write:pets: modify pets in your account + read:pets: read your pets + """; var reader = new AsyncApiStringReader(); var doc = reader.Read(yaml, out var diagnostic); Assert.AreEqual("pulsar+ssl://prod.events.managed.io:1234", doc.Servers.First().Value.Url); @@ -312,44 +438,45 @@ public void Read_WithServerReference_ResolvesReference() [Test] public void Read_WithChannelReference_ResolvesReference() { - var yaml = @"asyncapi: 2.3.0 -info: - title: test - version: 1.0.0 -servers: - production: - $ref: '#/components/servers/production' -channels: - workspace: - $ref: '#/components/channels/workspace' -components: - channels: - workspace: - publish: - message: - $ref: '#/components/messages/WorkspaceEventPayload' - servers: - production: - url: 'pulsar+ssl://prod.events.managed.io:1234' - protocol: pulsar+ssl - description: Pulsar broker - security: - - petstore_auth: - - write:pets - - read:pets - messages: - WorkspaceEventPayload: - schemaFormat: 'application/schema+yaml;version=draft-07' - securitySchemes: - petstore_auth: - type: oauth2 - flows: - implicit: - authorizationUrl: https://example.com/api/oauth/dialog - scopes: - write:pets: modify pets in your account - read:pets: read your pets -"; + var yaml = """ + asyncapi: 2.3.0 + info: + title: test + version: 1.0.0 + servers: + production: + $ref: '#/components/servers/production' + channels: + workspace: + $ref: '#/components/channels/workspace' + components: + channels: + workspace: + publish: + message: + $ref: '#/components/messages/WorkspaceEventPayload' + servers: + production: + url: 'pulsar+ssl://prod.events.managed.io:1234' + protocol: pulsar+ssl + description: Pulsar broker + security: + - petstore_auth: + - write:pets + - read:pets + messages: + WorkspaceEventPayload: + schemaFormat: 'application/schema+yaml;version=draft-07' + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: https://example.com/api/oauth/dialog + scopes: + write:pets: modify pets in your account + read:pets: read your pets + """; var reader = new AsyncApiStringReader(); var doc = reader.Read(yaml, out var diagnostic); Assert.AreEqual("application/schema+yaml;version=draft-07", doc.Channels.First().Value.Publish.Message.First().SchemaFormat); @@ -358,38 +485,39 @@ public void Read_WithChannelReference_ResolvesReference() [Test] public void Read_WithBasicPlusMessageTraitsDeserializes() { - var yaml = @"asyncapi: 2.3.0 -info: - title: test - version: 1.0.0 -channels: - workspace: - publish: - bindings: - http: - type: response - message: - $ref: '#/components/messages/WorkspaceEventPayload' -components: - messages: - WorkspaceEventPayload: - schemaFormat: application/schema+yaml;version=draft-07 - externalDocs: - description: Find more info here - url: https://example.com - traits: - - $ref: '#/components/messageTraits/commonHeaders' - messageTraits: - commonHeaders: - description: a common headers for common things - headers: - type: object - properties: - my-app-header: - type: integer - minimum: 0 - maximum: 100 -"; + var yaml = """ + asyncapi: 2.3.0 + info: + title: test + version: 1.0.0 + channels: + workspace: + publish: + bindings: + http: + type: response + message: + $ref: '#/components/messages/WorkspaceEventPayload' + components: + messages: + WorkspaceEventPayload: + schemaFormat: application/schema+yaml;version=draft-07 + externalDocs: + description: Find more info here + url: https://example.com + traits: + - $ref: '#/components/messageTraits/commonHeaders' + messageTraits: + commonHeaders: + description: a common headers for common things + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + """; var reader = new AsyncApiStringReader(); var doc = reader.Read(yaml, out var diagnostic); @@ -404,44 +532,46 @@ public void Read_WithBasicPlusMessageTraitsDeserializes() [Test] public void Serialize_withOneOfSchema_DoesNotWriteThen() { - var yaml = @"asyncapi: 2.3.0 -info: - title: test - version: 1.0.0 -defaultContentType: application/json -channels: - channel1: - publish: - operationId: channel1 - summary: tthe first channel - description: a channel of great importance - message: - $ref: '#/components/messages/item1' -components: - schemas: - item2: - type: object - properties: - icon: - description: Theme icon - oneOf: - - type: 'null' - - $ref: '#/components/schemas/item3' - item3: - type: object - properties: - title: - type: string - description: The title. - format: string - messages: - item1: - payload: - $ref: '#/components/schemas/item2' - name: item1 - title: item 1 - summary: the first item - description: a first item for firsting the items"; + var yaml = """ + asyncapi: 2.3.0 + info: + title: test + version: 1.0.0 + defaultContentType: application/json + channels: + channel1: + publish: + operationId: channel1 + summary: tthe first channel + description: a channel of great importance + message: + $ref: '#/components/messages/item1' + components: + schemas: + item2: + type: object + properties: + icon: + description: Theme icon + oneOf: + - type: 'null' + - $ref: '#/components/schemas/item3' + item3: + type: object + properties: + title: + type: string + description: The title. + format: string + messages: + item1: + payload: + $ref: '#/components/schemas/item2' + name: item1 + title: item 1 + summary: the first item + description: a first item for firsting the items + """; var reader = new AsyncApiStringReader(); var doc = reader.Read(yaml, out var diagnostic); @@ -453,39 +583,40 @@ public void Serialize_withOneOfSchema_DoesNotWriteThen() [Test] public void Read_WithBasicPlusSecurityRequirementsDeserializes() { - var yaml = @"asyncapi: 2.3.0 -info: - title: test - version: 1.0.0 -servers: - production: - url: 'pulsar+ssl://prod.events.managed.io:1234' - protocol: pulsar+ssl - description: Pulsar broker - security: - - petstore_auth: - - write:pets - - read:pets -channels: - workspace: - x-something: yes -components: - securitySchemes: - petstore_auth: - type: oauth2 - flows: - implicit: - authorizationUrl: https://example.com/api/oauth/dialog - scopes: - write:pets: modify pets in your account - read:pets: read your pets -"; + var yaml = """ + asyncapi: 2.3.0 + info: + title: test + version: 1.0.0 + servers: + production: + url: 'pulsar+ssl://prod.events.managed.io:1234' + protocol: pulsar+ssl + description: Pulsar broker + security: + - petstore_auth: + - write:pets + - read:pets + channels: + workspace: + x-something: yes + components: + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: https://example.com/api/oauth/dialog + scopes: + write:pets: modify pets in your account + read:pets: read your pets + """; var reader = new AsyncApiStringReader(); var doc = reader.Read(yaml, out var diagnostic); var requirement = doc.Servers.First().Value.Security.First().First(); Assert.AreEqual(SecuritySchemeType.OAuth2, requirement.Key.Type); Assert.IsTrue(requirement.Value.Contains("write:pets")); Assert.IsTrue(requirement.Value.Contains("read:pets")); - } + } } } diff --git a/test/LEGO.AsyncAPI.Tests/Bindings/AMQP/AMQPBindings_Should.cs b/test/LEGO.AsyncAPI.Tests/Bindings/AMQP/AMQPBindings_Should.cs new file mode 100644 index 00000000..5ef0d8d1 --- /dev/null +++ b/test/LEGO.AsyncAPI.Tests/Bindings/AMQP/AMQPBindings_Should.cs @@ -0,0 +1,185 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Tests.Bindings.AMQP +{ + using System.Collections.Generic; + using FluentAssertions; + using LEGO.AsyncAPI.Bindings; + using LEGO.AsyncAPI.Bindings.AMQP; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Readers; + using NUnit.Framework; + + public class AMQPBindings_Should + { + [Test] + public void AMQPChannelBinding_WithRoutingKey_SerializesAndDeserializes() + { + // Arrange + var expected = + """ + bindings: + amqp: + is: routingKey + exchange: + name: myExchange + type: topic + durable: true + autoDelete: false + vhost: / + """; + + var channel = new AsyncApiChannel(); + channel.Bindings.Add(new AMQPChannelBinding + { + Is = ChannelType.RoutingKey, + Exchange = new Exchange + { + Name = "myExchange", + Type = ExchangeType.Topic, + Durable = true, + AutoDelete = false, + Vhost = "/", + }, + }); + + // Act + var actual = channel.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + + // Assert + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.AMQP; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + + // Assert + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + binding.Should().BeEquivalentTo(channel); + } + + [Test] + public void AMQPChannelBinding_WithQueue_SerializesAndDeserializes() + { + // Arrange + var expected = + """ + bindings: + amqp: + is: queue + queue: + name: my-queue-name + durable: true + exclusive: true + autoDelete: false + vhost: / + """; + + var channel = new AsyncApiChannel(); + channel.Bindings.Add(new AMQPChannelBinding + { + Is = ChannelType.Queue, + Queue = new Queue + { + Name = "my-queue-name", + Durable = true, + Exclusive = true, + AutoDelete = false, + Vhost = "/", + }, + }); + + // Act + var actual = channel.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + + // Assert + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.AMQP; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + + // Assert + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + binding.Should().BeEquivalentTo(channel); + } + + [Test] + public void AMQPMessageBinding_WithFilledObject_SerializesAndDeserializes() + { + // Arrange + var expected = + """ + bindings: + amqp: + contentEncoding: gzip + messageType: user.signup + """; + + var message = new AsyncApiMessage(); + + message.Bindings.Add(new AMQPMessageBinding + { + ContentEncoding = "gzip", + MessageType = "user.signup", + }); + + // Act + var actual = message.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.AMQP; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + + // Assert + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + binding.Should().BeEquivalentTo(message); + } + + [Test] + public void AMQPOperationBinding_WithFilledObject_SerializesAndDeserializes() + { + // Arrange + var expected = + """ + bindings: + amqp: + expiration: 100000 + userId: guest + cc: + - user.logs + priority: 10 + deliveryMode: 2 + mandatory: false + bcc: + - external.audit + timestamp: true + ack: false + """; + + var operation = new AsyncApiOperation(); + operation.Bindings.Add(new AMQPOperationBinding + { + Expiration = 100000, + UserId = "guest", + Cc = new List { "user.logs" }, + Priority = 10, + DeliveryMode = DeliveryMode.Persistent, + Mandatory = false, + Bcc = new List { "external.audit" }, + Timestamp = true, + Ack = false, + }); ; + + // Act + var actual = operation.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.AMQP; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + + // Assert + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + binding.Should().BeEquivalentTo(operation); + } + } +} diff --git a/test/LEGO.AsyncAPI.Tests/Bindings/BindingExtensions_Should.cs b/test/LEGO.AsyncAPI.Tests/Bindings/BindingExtensions_Should.cs new file mode 100644 index 00000000..3806fc03 --- /dev/null +++ b/test/LEGO.AsyncAPI.Tests/Bindings/BindingExtensions_Should.cs @@ -0,0 +1,89 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Tests.Bindings.WebSockets +{ + using System.Linq; + using FluentAssertions; + using LEGO.AsyncAPI.Bindings.MQTT; + using LEGO.AsyncAPI.Bindings.Pulsar; + using LEGO.AsyncAPI.Bindings.WebSockets; + using LEGO.AsyncAPI.Models; + using NUnit.Framework; + + public class BindingExtensions_Should + { + [Test] + public void TryGetValue_WithChannelBinding_ReturnsBinding() + { + var channel = new AsyncApiChannel(); + channel.Bindings.Add(new WebSocketsChannelBinding + { + Method = "POST", + Query = new AsyncApiSchema + { + Description = "this mah query", + }, + Headers = new AsyncApiSchema + { + Description = "this mah binding", + }, + }); + + var result = channel.Bindings.TryGetValue(out var channelBinding); + result.Should().BeTrue(); + channelBinding.Should().NotBeNull(); + channelBinding.Should().BeEquivalentTo(channel.Bindings.First().Value); + } + + [Test] + public void TryGetValue_WithServerBinding_ReturnsBinding() + { + var server = new AsyncApiServer(); + server.Bindings.Add(new PulsarServerBinding + { + Tenant = "test tenant", + }); + + var result = server.Bindings.TryGetValue(out var serverBinding); + result.Should().BeTrue(); + serverBinding.Should().NotBeNull(); + serverBinding.Should().BeEquivalentTo(server.Bindings.First().Value); + } + + [Test] + public void TryGetValue_WithOperationBinding_ReturnsBinding() + { + var operation = new AsyncApiOperation(); + operation.Bindings.Add(new MQTTOperationBinding + { + QoS = 23, + MessageExpiryInterval = 1, + Retain = true, + }); + + var result = operation.Bindings.TryGetValue(out var operationBinding); + result.Should().BeTrue(); + operationBinding.Should().NotBeNull(); + operationBinding.Should().BeEquivalentTo(operation.Bindings.First().Value); + } + + [Test] + public void TryGetValue_WithMessageBinding_ReturnsBinding() + { + var message = new AsyncApiMessage(); + message.Bindings.Add(new MQTTMessageBinding + { + PayloadFormatIndicator = 2, + CorrelationData = new AsyncApiSchema + { + Description = "Test", + }, + }); + + var result = message.Bindings.TryGetValue(out var messageBinding); + result.Should().BeTrue(); + messageBinding.Should().NotBeNull(); + messageBinding.Should().BeEquivalentTo(message.Bindings.First().Value); + } + } +} diff --git a/test/LEGO.AsyncAPI.Tests/Bindings/CustomBinding_Should.cs b/test/LEGO.AsyncAPI.Tests/Bindings/CustomBinding_Should.cs new file mode 100644 index 00000000..448e588b --- /dev/null +++ b/test/LEGO.AsyncAPI.Tests/Bindings/CustomBinding_Should.cs @@ -0,0 +1,122 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Tests.Bindings +{ + using System.Collections.Generic; + using FluentAssertions; + using LEGO.AsyncAPI.Bindings; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Readers; + using LEGO.AsyncAPI.Readers.ParseNodes; + using LEGO.AsyncAPI.Writers; + using NUnit.Framework; + + public class NestedConfiguration : IAsyncApiExtensible + { + public string Name { get; set; } + + public IDictionary Extensions { get; set; } = new Dictionary(); + + public static FixedFieldMap FixedFieldMap = new() + { + { "name", (a, n) => { a.Name = n.GetScalarValue(); } }, + }; + + public void SerializeProperties(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteOptionalProperty("name", this.Name); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } + + public class MyBinding : ChannelBinding + { + public string Custom { get; set; } + + public override string BindingKey => "my"; + + public NestedConfiguration NestedConfiguration { get; set; } + + public AsyncApiAny Any { get; set; } + + protected override FixedFieldMap FixedFieldMap => new FixedFieldMap() + { + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + { "custom", (a, n) => { a.Custom = n.GetScalarValue(); } }, + { "any", (a, n) => { a.Any = n.CreateAny(); } }, + { "nestedConfiguration", (a, n) => { a.NestedConfiguration = n.ParseMapWithExtensions(NestedConfiguration.FixedFieldMap); } }, + }; + + public override void SerializeProperties(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteRequiredProperty("custom", this.Custom); + writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); + writer.WriteRequiredObject("any", this.Any, (w, p) => w.WriteAny(p)); + writer.WriteOptionalObject("nestedConfiguration", this.NestedConfiguration, (w, r) => r.SerializeProperties(w)); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + } + + public class CustomBinding_Should : TestBase + { + [Test] + public void CustomBinding_SerializesDeserializes() + { + // Arrange + var expected = + """ + bindings: + my: + custom: someValue + bindingVersion: 0.1.0 + any: + anyKeyName: anyValue + nestedConfiguration: + name: nested + x-myNestedExtension: nestedValue + x-myextension: someValue + """; + + var channel = new AsyncApiChannel(); + channel.Bindings.Add(new MyBinding + { + Custom = "someValue", + Any = new AsyncApiAny(new Dictionary() + { + { "anyKeyName", "anyValue" }, + }), + BindingVersion = "0.1.0", + NestedConfiguration = new NestedConfiguration() + { + Name = "nested", + Extensions = new Dictionary() + { + { "x-myNestedExtension", new AsyncApiAny("nestedValue") }, + }, + }, + Extensions = new Dictionary() + { + { "x-myextension", new AsyncApiAny("someValue") }, + }, + }); + + // Act + var actual = channel.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + + // Assert + var settings = new AsyncApiReaderSettings(); + settings.Bindings = new[] { new MyBinding() }; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + + // Assert + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + binding.Should().BeEquivalentTo(channel); + } + } +} diff --git a/test/LEGO.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs b/test/LEGO.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs index 01ce4885..f2c17c6c 100644 --- a/test/LEGO.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs @@ -1,22 +1,27 @@ -namespace LEGO.AsyncAPI.Tests.Bindings.Http +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Tests.Bindings.Http { using FluentAssertions; + using LEGO.AsyncAPI.Bindings; + using LEGO.AsyncAPI.Bindings.Http; using LEGO.AsyncAPI.Models; - using LEGO.AsyncAPI.Models.Bindings.Http; using LEGO.AsyncAPI.Readers; using NUnit.Framework; - internal class HttpBindings_Should + internal class HttpBindings_Should : TestBase { [Test] public void HttpMessageBinding_FilledObject_SerializesAndDeserializes() { // Arrange var expected = -@"bindings: - http: - headers: - description: this mah binding"; + """ + bindings: + http: + headers: + description: this mah binding + """; var message = new AsyncApiMessage(); @@ -30,13 +35,13 @@ public void HttpMessageBinding_FilledObject_SerializesAndDeserializes() // Act var actual = message.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); - actual = actual.MakeLineBreaksEnvironmentNeutral(); - expected = expected.MakeLineBreaksEnvironmentNeutral(); - - var binding = new AsyncApiStringReader().ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.Http; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); // Assert - Assert.AreEqual(actual, expected); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); binding.Should().BeEquivalentTo(message); } @@ -45,18 +50,20 @@ public void HttpOperationBinding_FilledObject_SerializesAndDeserializes() { // Arrange var expected = -@"bindings: - http: - type: request - method: POST - query: - description: this mah query"; + """ + bindings: + http: + type: request + method: POST + query: + description: this mah query + """; var operation = new AsyncApiOperation(); operation.Bindings.Add(new HttpOperationBinding { - Type = "request", + Type = HttpOperationBinding.HttpOperationType.Request, Method = "POST", Query = new AsyncApiSchema { @@ -66,13 +73,13 @@ public void HttpOperationBinding_FilledObject_SerializesAndDeserializes() // Act var actual = operation.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); - actual = actual.MakeLineBreaksEnvironmentNeutral(); - expected = expected.MakeLineBreaksEnvironmentNeutral(); - - var binding = new AsyncApiStringReader().ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.Http; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); // Assert - Assert.AreEqual(actual, expected); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); binding.Should().BeEquivalentTo(operation); } } diff --git a/test/LEGO.AsyncAPI.Tests/Bindings/Kafka/KafkaBindings_Should.cs b/test/LEGO.AsyncAPI.Tests/Bindings/Kafka/KafkaBindings_Should.cs index 49e1e515..2c5c6f3f 100644 --- a/test/LEGO.AsyncAPI.Tests/Bindings/Kafka/KafkaBindings_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Bindings/Kafka/KafkaBindings_Should.cs @@ -1,32 +1,41 @@ -namespace LEGO.AsyncAPI.Tests.Bindings.Kafka +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Tests.Bindings.Kafka { + using System.Collections.Generic; using FluentAssertions; + using LEGO.AsyncAPI.Bindings; + using LEGO.AsyncAPI.Bindings.Kafka; using LEGO.AsyncAPI.Models; - using LEGO.AsyncAPI.Models.Bindings.Kafka; using LEGO.AsyncAPI.Readers; using NUnit.Framework; - using System.Collections.Generic; - internal class KafkaBindings_Should + internal class KafkaBindings_Should : TestBase { [Test] public void KafkaChannelBinding_WithFilledObject_SerializesAndDeserializes() { // Arrange var expected = -@"bindings: - kafka: - topic: myTopic - partitions: 5 - replicas: 4 - topicConfiguration: - cleanup.policy: - - delete - - compact - retention.ms: 1 - retention.bytes: 2 - delete.retention.ms: 3 - max.message.bytes: 4"; + """ + bindings: + kafka: + topic: myTopic + partitions: 5 + replicas: 4 + topicConfiguration: + cleanup.policy: + - delete + - compact + retention.ms: 15552000000 + retention.bytes: 2 + delete.retention.ms: 3 + max.message.bytes: 4 + confluent.key.schema.validation: true + confluent.key.subject.name.strategy: TopicNameStrategy + confluent.value.schema.validation: true + confluent.value.subject.name.strategy: TopicNameStrategy + """; var channel = new AsyncApiChannel(); channel.Bindings.Add(new KafkaChannelBinding @@ -37,10 +46,14 @@ public void KafkaChannelBinding_WithFilledObject_SerializesAndDeserializes() TopicConfiguration = new TopicConfigurationObject() { CleanupPolicy = new List { "delete", "compact" }, - RetentionMiliseconds = 1, + RetentionMilliseconds = 15552000000, RetentionBytes = 2, - DeleteRetentionMiliseconds = 3, + DeleteRetentionMilliseconds = 3, MaxMessageBytes = 4, + ConfluentKeySchemaValidation = true, + ConfluentKeySubjectName = "TopicNameStrategy", + ConfluentValueSchemaValidation = true, + ConfluentValueSubjectName = "TopicNameStrategy", }, }); @@ -48,13 +61,13 @@ public void KafkaChannelBinding_WithFilledObject_SerializesAndDeserializes() var actual = channel.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); // Assert - actual = actual.MakeLineBreaksEnvironmentNeutral(); - expected = expected.MakeLineBreaksEnvironmentNeutral(); - - var binding = new AsyncApiStringReader().ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.Kafka; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); // Assert - Assert.AreEqual(actual, expected); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); binding.Should().BeEquivalentTo(channel); } @@ -63,12 +76,14 @@ public void KafkaServerBinding_WithFilledObject_SerializesAndDeserializes() { // Arrange var expected = -@"url: https://example.com -protocol: kafka -bindings: - kafka: - schemaRegistryUrl: https://example.com/schemaregistry - schemaRegistryVendor: confluent"; + """ + url: https://example.com + protocol: kafka + bindings: + kafka: + schemaRegistryUrl: https://example.com/schemaregistry + schemaRegistryVendor: confluent + """; var server = new AsyncApiServer() { @@ -84,13 +99,13 @@ public void KafkaServerBinding_WithFilledObject_SerializesAndDeserializes() // Act var actual = server.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); - actual = actual.MakeLineBreaksEnvironmentNeutral(); - expected = expected.MakeLineBreaksEnvironmentNeutral(); - - var binding = new AsyncApiStringReader().ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.Kafka; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); // Assert - Assert.AreEqual(actual, expected); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); binding.Should().BeEquivalentTo(server); } @@ -99,13 +114,15 @@ public void KafkaMessageBinding_WithFilledObject_SerializesAndDeserializes() { // Arrange var expected = -@"bindings: - kafka: - key: - description: this mah other binding - SchemaIdLocation: test - schemaIdPayloadEncoding: test - schemaLookupStrategy: header"; + """ + bindings: + kafka: + key: + description: this mah other binding + SchemaIdLocation: test + schemaIdPayloadEncoding: test + schemaLookupStrategy: header + """; var message = new AsyncApiMessage(); @@ -122,13 +139,13 @@ public void KafkaMessageBinding_WithFilledObject_SerializesAndDeserializes() // Act var actual = message.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); - actual = actual.MakeLineBreaksEnvironmentNeutral(); - expected = expected.MakeLineBreaksEnvironmentNeutral(); - - var binding = new AsyncApiStringReader().ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.Kafka; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); // Assert - Assert.AreEqual(actual, expected); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); binding.Should().BeEquivalentTo(message); } @@ -137,12 +154,14 @@ public void KafkaOperationBinding_WithFilledObject_SerializesAndDeserializes() { // Arrange var expected = -@"bindings: - kafka: - groupId: - description: this mah groupId - clientId: - description: this mah clientId"; + """ + bindings: + kafka: + groupId: + description: this mah groupId + clientId: + description: this mah clientId + """; var operation = new AsyncApiOperation(); @@ -160,13 +179,14 @@ public void KafkaOperationBinding_WithFilledObject_SerializesAndDeserializes() // Act var actual = operation.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); - actual = actual.MakeLineBreaksEnvironmentNeutral(); - expected = expected.MakeLineBreaksEnvironmentNeutral(); - var binding = new AsyncApiStringReader().ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.Kafka; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); // Assert - Assert.AreEqual(actual, expected); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); binding.Should().BeEquivalentTo(operation); } } diff --git a/test/LEGO.AsyncAPI.Tests/Bindings/Pulsar/PulsarBindings_Should.cs b/test/LEGO.AsyncAPI.Tests/Bindings/Pulsar/PulsarBindings_Should.cs index 05fd4f98..7e69a0b7 100644 --- a/test/LEGO.AsyncAPI.Tests/Bindings/Pulsar/PulsarBindings_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Bindings/Pulsar/PulsarBindings_Should.cs @@ -1,35 +1,38 @@ -using LEGO.AsyncAPI.Models.Bindings; - +// Copyright (c) The LEGO Group. All rights reserved. namespace LEGO.AsyncAPI.Tests.Bindings.Pulsar { + using System.Collections.Generic; using FluentAssertions; + using LEGO.AsyncAPI.Bindings; + using LEGO.AsyncAPI.Bindings.Pulsar; using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Models.Bindings.Pulsar; using LEGO.AsyncAPI.Readers; using NUnit.Framework; - using System.Collections.Generic; - internal class PulsarBindings_Should + internal class PulsarBindings_Should : TestBase { [Test] public void PulsarChannelBinding_WithFilledObject_SerializesAndDeserializes() { // Arrange var expected = -@"bindings: - pulsar: - namespace: staging - persistence: persistent - compaction: 1000 - geo-replication: - - us-east1 - - us-west1 - retention: - time: 7 - size: 1000 - ttl: 360 - deduplication: true - bindingVersion: 0.1.0"; + """ + bindings: + pulsar: + namespace: staging + persistence: persistent + compaction: 1000 + geo-replication: + - us-east1 + - us-west1 + retention: + time: 7 + size: 1000 + ttl: 360 + deduplication: true + bindingVersion: 0.1.0 + """; var channel = new AsyncApiChannel(); channel.Bindings.Add(new PulsarChannelBinding @@ -55,14 +58,13 @@ public void PulsarChannelBinding_WithFilledObject_SerializesAndDeserializes() // Act var actual = channel.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); - // Assert - actual = actual.MakeLineBreaksEnvironmentNeutral(); - expected = expected.MakeLineBreaksEnvironmentNeutral(); - - var binding = new AsyncApiStringReader().ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.Pulsar; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); // Assert - Assert.AreEqual(actual, expected); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); binding.Should().BeEquivalentTo(channel); } @@ -71,37 +73,19 @@ public void PulsarChannelBindingNamespaceDefaultToNull() { // Arrange var actual = - @"bindings: - pulsar: - persistence: persistent"; + """ + bindings: + pulsar: + persistence: persistent + """; // Act - // Assert - var binding = new AsyncApiStringReader().ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); - - Assert.AreEqual(null, ((PulsarChannelBinding)binding.Bindings[BindingType.Pulsar]).Namespace); - } + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.Pulsar; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); - [Test] - public void PulsarChannelBindingPropertiesExceptNamespaceDefaultToNull() - { - // Arrange - var actual = - @"bindings: - pulsar: - namespace: staging"; - - // Act // Assert - var binding = new AsyncApiStringReader().ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); - var pulsarBinding = ((PulsarChannelBinding) binding.Bindings[BindingType.Pulsar]); - - Assert.AreEqual(null, pulsarBinding.Persistence); - Assert.AreEqual(null, pulsarBinding.Compaction); - Assert.AreEqual(null, pulsarBinding.GeoReplication); - Assert.AreEqual(null, pulsarBinding.Retention); - Assert.AreEqual(null, pulsarBinding.TTL); - Assert.AreEqual(null, pulsarBinding.Deduplication); + Assert.AreEqual(null, ((PulsarChannelBinding)binding.Bindings["pulsar"]).Namespace); } [Test] @@ -109,11 +93,13 @@ public void PulsarServerBinding_WithFilledObject_SerializesAndDeserializes() { // Arrange var expected = -@"url: https://example.com -protocol: pulsar -bindings: - pulsar: - tenant: contoso"; + """ + url: https://example.com + protocol: pulsar + bindings: + pulsar: + tenant: contoso + """; var server = new AsyncApiServer() { @@ -128,13 +114,13 @@ public void PulsarServerBinding_WithFilledObject_SerializesAndDeserializes() // Act var actual = server.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); - actual = actual.MakeLineBreaksEnvironmentNeutral(); - expected = expected.MakeLineBreaksEnvironmentNeutral(); - - var binding = new AsyncApiStringReader().ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.Pulsar; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); // Assert - Assert.AreEqual(actual, expected); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); binding.Should().BeEquivalentTo(server); } @@ -143,11 +129,13 @@ public void ServerBindingVersionDefaultsToNull() { // Arrange var expected = - @"url: https://example.com -protocol: pulsar -bindings: - pulsar: - tenant: contoso"; + """ + url: https://example.com + protocol: pulsar + bindings: + pulsar: + tenant: contoso + """; var server = new AsyncApiServer() { @@ -163,14 +151,14 @@ public void ServerBindingVersionDefaultsToNull() // Act var actual = server.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); - actual = actual.MakeLineBreaksEnvironmentNeutral(); - expected = expected.MakeLineBreaksEnvironmentNeutral(); - - var binding = new AsyncApiStringReader().ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.Pulsar; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); // Assert - Assert.AreEqual(actual, expected); - Assert.AreEqual(null, ((PulsarServerBinding)binding.Bindings[BindingType.Pulsar]).BindingVersion); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + Assert.AreEqual(null, ((PulsarServerBinding)binding.Bindings["pulsar"]).BindingVersion); binding.Should().BeEquivalentTo(server); } @@ -179,11 +167,13 @@ public void ServerTenantDefaultsToNull() { // Arrange var expected = - @"url: https://example.com -protocol: pulsar -bindings: - pulsar: - bindingVersion: latest"; + """ + url: https://example.com + protocol: pulsar + bindings: + pulsar: + bindingVersion: latest + """; var server = new AsyncApiServer() { @@ -199,14 +189,14 @@ public void ServerTenantDefaultsToNull() // Act var actual = server.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); - actual = actual.MakeLineBreaksEnvironmentNeutral(); - expected = expected.MakeLineBreaksEnvironmentNeutral(); - - var binding = new AsyncApiStringReader().ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.Pulsar; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); // Assert - Assert.AreEqual(actual, expected); - Assert.AreEqual(null, ((PulsarServerBinding)binding.Bindings[BindingType.Pulsar]).Tenant); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + Assert.AreEqual(null, ((PulsarServerBinding)binding.Bindings["pulsar"]).Tenant); binding.Should().BeEquivalentTo(server); } } diff --git a/test/LEGO.AsyncAPI.Tests/Bindings/Sns/SnsBindings_Should.cs b/test/LEGO.AsyncAPI.Tests/Bindings/Sns/SnsBindings_Should.cs new file mode 100644 index 00000000..daed9e88 --- /dev/null +++ b/test/LEGO.AsyncAPI.Tests/Bindings/Sns/SnsBindings_Should.cs @@ -0,0 +1,439 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Tests.Bindings.Sns +{ + using System.Collections.Generic; + using System.Linq; + using FluentAssertions; + using LEGO.AsyncAPI.Bindings; + using LEGO.AsyncAPI.Bindings.Sns; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Readers; + using NUnit.Framework; + + internal class SnsBindings_Should : TestBase + { + [Test] + public void SnsChannelBinding_WithFilledObject_SerializesAndDeserializes() + { + // Arrange + var expected = + """ + bindings: + sns: + name: myTopic + ordering: + type: FIFO + contentBasedDeduplication: true + x-orderingExtension: + orderingXPropertyName: orderingXPropertyValue + policy: + statements: + - effect: Deny + principal: '*' + action: + - sns:Publish + - sns:Delete + condition: + StringEquals: + aws:username: + - johndoe + - mrsmith + - effect: Allow + principal: + AWS: + - arn:aws:iam::123456789012:user/alex.wichmann + - arn:aws:iam::123456789012:user/dec.kolakowski + action: sns:Create + condition: + NumericLessThanEquals: + aws:MultiFactorAuthAge: '3600' + x-statementExtension: + statementXPropertyName: statementXPropertyValue + x-policyExtension: + policyXPropertyName: policyXPropertyValue + tags: + owner: AsyncAPI.NET + platform: AsyncAPIOrg + x-bindingExtension: + bindingXPropertyName: bindingXPropertyValue + """; + + var channel = new AsyncApiChannel(); + channel.Bindings.Add(new SnsChannelBinding() + { + Name = "myTopic", + Ordering = new Ordering() + { + Type = OrderingType.Fifo, + ContentBasedDeduplication = true, + Extensions = new Dictionary() + { + { + "x-orderingExtension", + new AsyncApiAny(new Dictionary() + { + { "orderingXPropertyName", "orderingXPropertyValue" }, + }) + }, + }, + }, + Policy = new Policy() + { + Statements = new List() + { + new Statement() + { + Effect = Effect.Deny, + Principal = new PrincipalStar(), + Action = new StringOrStringList(new AsyncApiAny(new List() + { + "sns:Publish", + "sns:Delete", + })), + Condition = new Condition(new Dictionary> + { + { + "StringEquals", new Dictionary + { + { + "aws:username", new StringOrStringList(new AsyncApiAny(new List() { "johndoe", "mrsmith" })) + }, + } + }, + }), + }, + new Statement() + { + Effect = Effect.Allow, + Principal = new PrincipalObject(new KeyValuePair( + "AWS", new StringOrStringList(new AsyncApiAny(new List + { "arn:aws:iam::123456789012:user/alex.wichmann", "arn:aws:iam::123456789012:user/dec.kolakowski" })))), + Action = new StringOrStringList(new AsyncApiAny("sns:Create")), + Condition = new Condition(new Dictionary> + { + { + "NumericLessThanEquals", new Dictionary + { + { + "aws:MultiFactorAuthAge", new StringOrStringList(new AsyncApiAny("3600")) + }, + } + }, + }), + Extensions = new Dictionary() + { + { + "x-statementExtension", + new AsyncApiAny(new Dictionary() + { + { "statementXPropertyName", "statementXPropertyValue" }, + }) + }, + }, + }, + }, + Extensions = new Dictionary() + { + { + "x-policyExtension", + new AsyncApiAny(new Dictionary() + { + { "policyXPropertyName", "policyXPropertyValue" }, + }) + }, + }, + }, + Tags = new Dictionary() + { + { "owner", "AsyncAPI.NET" }, + { "platform", "AsyncAPIOrg" }, + }, + Extensions = new Dictionary() + { + { + "x-bindingExtension", + new AsyncApiAny(new Dictionary() + { + { "bindingXPropertyName", "bindingXPropertyValue" }, + }) + }, + }, + }); + + // Act + var actual = channel.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + + // Assert + var settings = new AsyncApiReaderSettings + { + Bindings = BindingsCollection.Sns, + }; + + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + + // Assert + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + + var expectedSnsBinding = (SnsChannelBinding)channel.Bindings.Values.First(); + expectedSnsBinding.Should().BeEquivalentTo((SnsChannelBinding)binding.Bindings.Values.First(), options => options.IgnoringCyclicReferences()); + } + + [Test] + public void SnsOperationBinding_WithFilledObject_SerializesAndDeserializes() + { + // Arrange + var expected = + """ + bindings: + sns: + topic: + name: someTopic + x-identifierExtension: + identifierXPropertyName: identifierXPropertyValue + consumers: + - protocol: sqs + endpoint: + name: someQueue + x-identifierExtension: + identifierXPropertyName: identifierXPropertyValue + filterPolicy: + store: + - asyncapi_corp + contact: dec.kolakowski + event: + - anything-but: order_cancelled + order_key: + transient: by_area + customer_interests: + - rugby + - football + - baseball + filterPolicyScope: MessageAttributes + rawMessageDelivery: false + redrivePolicy: + deadLetterQueue: + arn: arn:aws:SQS:eu-west-1:0000000:123456789 + x-identifierExtension: + identifierXPropertyName: identifierXPropertyValue + maxReceiveCount: 25 + x-redrivePolicyExtension: + redrivePolicyXPropertyName: redrivePolicyXPropertyValue + deliveryPolicy: + minDelayTarget: 10 + maxDelayTarget: 100 + numRetries: 5 + numNoDelayRetries: 2 + numMinDelayRetries: 3 + numMaxDelayRetries: 5 + backoffFunction: linear + maxReceivesPerSecond: 2 + x-deliveryPolicyExtension: + deliveryPolicyXPropertyName: deliveryPolicyXPropertyValue + x-consumerExtension: + consumerXPropertyName: consumerXPropertyValue + deliveryPolicy: + minDelayTarget: 10 + maxDelayTarget: 100 + numRetries: 5 + numNoDelayRetries: 2 + numMinDelayRetries: 3 + numMaxDelayRetries: 5 + backoffFunction: geometric + maxReceivesPerSecond: 10 + x-deliveryPolicyExtension: + deliveryPolicyXPropertyName: deliveryPolicyXPropertyValue + x-bindingExtension: + bindingXPropertyName: bindingXPropertyValue + """; + + var operation = new AsyncApiOperation(); + operation.Bindings.Add(new SnsOperationBinding() + { + Topic = new Identifier() + { + Name = "someTopic", + Extensions = new Dictionary() + { + { + "x-identifierExtension", + new AsyncApiAny(new Dictionary() + { + { "identifierXPropertyName", "identifierXPropertyValue" }, + }) + }, + }, + }, + Consumers = new List() + { + new Consumer() + { + Protocol = Protocol.Sqs, + Endpoint = new Identifier() + { + Name = "someQueue", + Extensions = new Dictionary() + { + { + "x-identifierExtension", + new AsyncApiAny(new Dictionary() + { + { "identifierXPropertyName", "identifierXPropertyValue" }, + }) + }, + }, + }, + FilterPolicy = new AsyncApiAny(new Dictionary() + { + { "store", new List() { "asyncapi_corp" } }, + { "contact", "dec.kolakowski" }, + { + "event", new List>() + { + new Dictionary() + { + { "anything-but", "order_cancelled" }, + }, + } + }, + { + "order_key", new Dictionary() + { + { "transient", "by_area" }, + } + }, + { + "customer_interests", new List() + { + "rugby", + "football", + "baseball", + } + }, + }), + FilterPolicyScope = FilterPolicyScope.MessageAttributes, + RawMessageDelivery = false, + RedrivePolicy = new RedrivePolicy() + { + DeadLetterQueue = new Identifier() + { + Arn = "arn:aws:SQS:eu-west-1:0000000:123456789", + Extensions = new Dictionary() + { + { + "x-identifierExtension", + new AsyncApiAny(new Dictionary() + { + { "identifierXPropertyName", "identifierXPropertyValue" }, + }) + }, + }, + }, + MaxReceiveCount = 25, + Extensions = new Dictionary() + { + { + "x-redrivePolicyExtension", + new AsyncApiAny(new Dictionary() + { + { "redrivePolicyXPropertyName", "redrivePolicyXPropertyValue" }, + }) + }, + }, + }, + DeliveryPolicy = new DeliveryPolicy() + { + MinDelayTarget = 10, + MaxDelayTarget = 100, + NumRetries = 5, + NumNoDelayRetries = 2, + NumMinDelayRetries = 3, + NumMaxDelayRetries = 5, + BackoffFunction = BackoffFunction.Linear, + MaxReceivesPerSecond = 2, + Extensions = new Dictionary() + { + { + "x-deliveryPolicyExtension", + new AsyncApiAny(new Dictionary() + { + { "deliveryPolicyXPropertyName", "deliveryPolicyXPropertyValue" }, + }) + }, + }, + }, + Extensions = new Dictionary() + { + { + "x-consumerExtension", + new AsyncApiAny(new Dictionary() + { + { "consumerXPropertyName", "consumerXPropertyValue" }, + }) + }, + }, + }, + }, + DeliveryPolicy = new DeliveryPolicy() + { + MinDelayTarget = 10, + MaxDelayTarget = 100, + NumRetries = 5, + NumNoDelayRetries = 2, + NumMinDelayRetries = 3, + NumMaxDelayRetries = 5, + BackoffFunction = BackoffFunction.Geometric, + MaxReceivesPerSecond = 10, + Extensions = new Dictionary() + { + { + "x-deliveryPolicyExtension", + new AsyncApiAny(new Dictionary() + { + { "deliveryPolicyXPropertyName", "deliveryPolicyXPropertyValue" }, + }) + }, + }, + }, + Extensions = new Dictionary() + { + { + "x-bindingExtension", + new AsyncApiAny(new Dictionary() + { + { "bindingXPropertyName", "bindingXPropertyValue" }, + }) + }, + }, + }); + + // Act + var actual = operation.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + + // Assert + var settings = new AsyncApiReaderSettings + { + Bindings = BindingsCollection.Sns, + }; + + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + var binding2 = new AsyncApiStringReader(settings).ReadFragment(expected, AsyncApiVersion.AsyncApi2_0, out _); + binding2.Bindings.First().Value.Extensions.TryGetValue("x-bindingExtension", out IAsyncApiExtension any); + var val = AsyncApiAny.FromExtensionOrDefault(any); + + // Assert + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + + var expectedSnsBinding = (SnsOperationBinding)operation.Bindings.Values.First(); + expectedSnsBinding.Should().BeEquivalentTo((SnsOperationBinding)binding.Bindings.Values.First(), options => options.IgnoringCyclicReferences()); + } + + class ExtensionClass + { + public string bindingXPropertyName { get; set; } + } + } +} \ No newline at end of file diff --git a/test/LEGO.AsyncAPI.Tests/Bindings/Sqs/SqsBindings_should.cs b/test/LEGO.AsyncAPI.Tests/Bindings/Sqs/SqsBindings_should.cs new file mode 100644 index 00000000..c3f7ff9d --- /dev/null +++ b/test/LEGO.AsyncAPI.Tests/Bindings/Sqs/SqsBindings_should.cs @@ -0,0 +1,505 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Tests.Bindings.Sqs +{ + using System.Collections.Generic; + using System.Linq; + using FluentAssertions; + using LEGO.AsyncAPI.Bindings; + using LEGO.AsyncAPI.Bindings.Sqs; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Readers; + using NUnit.Framework; + using BindingsCollection = LEGO.AsyncAPI.Bindings.BindingsCollection; + + internal class SqsBindings_should + { + [Test] + public void SqsChannelBinding_WithFilledObject_SerializesAndDeserializes() + { + // Arrange + var expected = + """ + bindings: + sqs: + queue: + name: myQueue + fifoQueue: true + deduplicationScope: messageGroup + fifoThroughputLimit: perMessageGroupId + deliveryDelay: 30 + visibilityTimeout: 60 + receiveMessageWaitTime: 0 + messageRetentionPeriod: 86400 + redrivePolicy: + deadLetterQueue: + arn: arn:aws:SQS:eu-west-1:0000000:123456789 + x-identifierExtension: + identifierXPropertyName: identifierXPropertyValue + maxReceiveCount: 15 + x-redrivePolicyExtension: + redrivePolicyXPropertyName: redrivePolicyXPropertyValue + policy: + statements: + - effect: deny + principal: + AWS: arn:aws:iam::123456789012:user/alex.wichmann + action: + - sqs:SendMessage + - sqs:ReceiveMessage + condition: + StringEquals: + aws:username: + - johndoe + - mrsmith + x-statementExtension: + statementXPropertyName: statementXPropertyValue + - effect: allow + principal: + AWS: + - arn:aws:iam::123456789012:user/alex.wichmann + - arn:aws:iam::123456789012:user/dec.kolakowski + action: sqs:CreateQueue + condition: + NumericLessThanEquals: + aws:MultiFactorAuthAge: '3600' + x-policyExtension: + policyXPropertyName: policyXPropertyValue + tags: + owner: AsyncAPI.NET + platform: AsyncAPIOrg + x-queueExtension: + queueXPropertyName: queueXPropertyValue + deadLetterQueue: + name: myQueue_error + deliveryDelay: 0 + visibilityTimeout: 0 + receiveMessageWaitTime: 0 + messageRetentionPeriod: 604800 + policy: + statements: + - effect: allow + principal: + Service: s3.amazonaws.com + action: + - sqs:* + x-internalObject: + myExtensionPropertyName: myExtensionPropertyValue + """; + + var channel = new AsyncApiChannel(); + channel.Bindings.Add(new SqsChannelBinding() + { + Queue = new Queue() + { + Name = "myQueue", + FifoQueue = true, + DeduplicationScope = DeduplicationScope.MessageGroup, + FifoThroughputLimit = FifoThroughputLimit.PerMessageGroupId, + DeliveryDelay = 30, + VisibilityTimeout = 60, + ReceiveMessageWaitTime = 0, + MessageRetentionPeriod = 86400, + RedrivePolicy = new RedrivePolicy() + { + DeadLetterQueue = new Identifier() + { + Arn = "arn:aws:SQS:eu-west-1:0000000:123456789", + Extensions = new Dictionary() + { + { + "x-identifierExtension", + new AsyncApiAny(new Dictionary + { + { "identifierXPropertyName", "identifierXPropertyValue" }, + }) + }, + }, + }, + MaxReceiveCount = 15, + Extensions = new Dictionary() + { + { + "x-redrivePolicyExtension", + new AsyncApiAny(new Dictionary + { + { "redrivePolicyXPropertyName", "redrivePolicyXPropertyValue" }, + }) + }, + }, + }, + Policy = new Policy() + { + Statements = new List() + { + new Statement() + { + Effect = Effect.Deny, + Principal = new PrincipalObject(new KeyValuePair( + "AWS", new StringOrStringList(new AsyncApiAny("arn:aws:iam::123456789012:user/alex.wichmann")))), + Action = new StringOrStringList(new AsyncApiAny(new List + { + "sqs:SendMessage", + "sqs:ReceiveMessage", + })), + Condition = new Condition(new Dictionary> + { + { + "StringEquals", new Dictionary + { + { + "aws:username", new StringOrStringList(new AsyncApiAny(new List { "johndoe", "mrsmith" })) + }, + } + }, + }), + Extensions = new Dictionary() + { + { + "x-statementExtension", + new AsyncApiAny(new Dictionary + { + { "statementXPropertyName", "statementXPropertyValue" }, + }) + }, + }, + }, + new Statement() + { + Effect = Effect.Allow, + Principal = new PrincipalObject(new KeyValuePair( + "AWS", new StringOrStringList(new AsyncApiAny(new List + { "arn:aws:iam::123456789012:user/alex.wichmann", "arn:aws:iam::123456789012:user/dec.kolakowski" })))), + Action = new StringOrStringList(new AsyncApiAny("sqs:CreateQueue")), + Condition = new Condition(new Dictionary> + { + { + "NumericLessThanEquals", new Dictionary + { + { + "aws:MultiFactorAuthAge", new StringOrStringList(new AsyncApiAny("3600")) + }, + } + }, + }), + }, + }, + Extensions = new Dictionary() + { + { + "x-policyExtension", + new AsyncApiAny(new Dictionary + { + { "policyXPropertyName", "policyXPropertyValue" }, + }) + }, + }, + }, + Tags = new Dictionary() + { + { "owner", "AsyncAPI.NET" }, + { "platform", "AsyncAPIOrg" }, + }, + Extensions = new Dictionary() + { + { + "x-queueExtension", + new AsyncApiAny(new Dictionary + { + { "queueXPropertyName", "queueXPropertyValue" }, + }) + }, + }, + }, + DeadLetterQueue = new Queue() + { + Name = "myQueue_error", + FifoQueue = false, + DeliveryDelay = 0, + VisibilityTimeout = 0, + ReceiveMessageWaitTime = 0, + MessageRetentionPeriod = 604800, + Policy = new Policy() + { + Statements = new List() + { + new Statement() + { + Effect = Effect.Allow, + Principal = new PrincipalObject(new KeyValuePair( + "Service", new StringOrStringList(new AsyncApiAny("s3.amazonaws.com")))), + Action = new StringOrStringList(new AsyncApiAny(new List + { + "sqs:*", + })), + }, + }, + }, + }, + Extensions = new Dictionary() + { + { + "x-internalObject", new AsyncApiAny(new Dictionary + { + { "myExtensionPropertyName", "myExtensionPropertyValue" }, + }) + }, + }, + }); + + // Act + var actual = channel.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + + // Assert + var settings = new AsyncApiReaderSettings + { + Bindings = BindingsCollection.Sqs, + }; + var binding = + new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + + // Assert + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + binding.Should().BeEquivalentTo(channel); + + var expectedSqsBinding = (SqsChannelBinding)channel.Bindings.Values.First(); + expectedSqsBinding.Should().BeEquivalentTo((SqsChannelBinding)binding.Bindings.Values.First(), options => options.IgnoringCyclicReferences()); + } + + [Test] + public void SqsOperationBinding_WithFilledObject_SerializesAndDeserializes() + { + // Arrange + var expected = + """ + bindings: + sqs: + queues: + - name: myQueue + deliveryDelay: 30 + visibilityTimeout: 60 + receiveMessageWaitTime: 0 + messageRetentionPeriod: 86400 + redrivePolicy: + deadLetterQueue: + arn: arn:aws:SQS:eu-west-1:0000000:123456789 + x-identifierExtension: + identifierXPropertyName: identifierXPropertyValue + maxReceiveCount: 15 + x-redrivePolicyExtension: + redrivePolicyXPropertyName: redrivePolicyXPropertyValue + policy: + statements: + - effect: deny + principal: + AWS: arn:aws:iam::123456789012:user/alex.wichmann + action: + - sqs:SendMessage + - sqs:ReceiveMessage + x-statementExtension: + statementXPropertyName: statementXPropertyValue + - effect: allow + principal: + AWS: + - arn:aws:iam::123456789012:user/alex.wichmann + - arn:aws:iam::123456789012:user/dec.kolakowski + action: sqs:CreateQueue + x-policyExtension: + policyXPropertyName: policyXPropertyValue + tags: + owner: AsyncAPI.NET + platform: AsyncAPIOrg + x-queueExtension: + queueXPropertyName: queueXPropertyValue + - name: myQueue_error + deliveryDelay: 0 + visibilityTimeout: 0 + receiveMessageWaitTime: 0 + messageRetentionPeriod: 604800 + policy: + statements: + - effect: allow + principal: + AWS: arn:aws:iam::123456789012:user/alex.wichmann + action: + - sqs:* + x-queueExtension: + queueXPropertyName: queueXPropertyValue + x-internalObject: + myExtensionPropertyName: myExtensionPropertyValue + """; + + var operation = new AsyncApiOperation(); + operation.Bindings.Add(new SqsOperationBinding() + { + Queues = new List() + { + new Queue() + { + Name = "myQueue", + FifoQueue = false, + DeduplicationScope = null, + FifoThroughputLimit = null, + DeliveryDelay = 30, + VisibilityTimeout = 60, + ReceiveMessageWaitTime = 0, + MessageRetentionPeriod = 86400, + RedrivePolicy = new RedrivePolicy() + { + DeadLetterQueue = new Identifier() + { + Arn = "arn:aws:SQS:eu-west-1:0000000:123456789", + Extensions = new Dictionary() + { + { + "x-identifierExtension", + new AsyncApiAny(new Dictionary + { + { "identifierXPropertyName", "identifierXPropertyValue" }, + }) + }, + }, + }, + MaxReceiveCount = 15, + Extensions = new Dictionary() + { + { + "x-redrivePolicyExtension", + new AsyncApiAny(new Dictionary + { + { "redrivePolicyXPropertyName", "redrivePolicyXPropertyValue" }, + }) + }, + }, + }, + Policy = new Policy() + { + Statements = new List() + { + new Statement() + { + Effect = Effect.Deny, + Principal = new PrincipalObject(new KeyValuePair( + "AWS", new StringOrStringList(new AsyncApiAny("arn:aws:iam::123456789012:user/alex.wichmann")))), + Action = new StringOrStringList(new AsyncApiAny(new List() + { + "sqs:SendMessage", + "sqs:ReceiveMessage", + })), + Extensions = new Dictionary() + { + { + "x-statementExtension", + new AsyncApiAny(new Dictionary() + { + { "statementXPropertyName", "statementXPropertyValue" }, + }) + }, + }, + }, + new Statement() + { + Effect = Effect.Allow, + Principal = new PrincipalObject(new KeyValuePair( + "AWS", new StringOrStringList(new AsyncApiAny(new List + { "arn:aws:iam::123456789012:user/alex.wichmann", "arn:aws:iam::123456789012:user/dec.kolakowski" })))), + Action = new StringOrStringList(new AsyncApiAny("sqs:CreateQueue")), + }, + }, + Extensions = new Dictionary() + { + { + "x-policyExtension", + new AsyncApiAny(new Dictionary + { + { "policyXPropertyName", "policyXPropertyValue" }, + }) + }, + }, + }, + Tags = new Dictionary() + { + { "owner", "AsyncAPI.NET" }, + { "platform", "AsyncAPIOrg" }, + }, + Extensions = new Dictionary() + { + { + "x-queueExtension", + new AsyncApiAny(new Dictionary() + { + { "queueXPropertyName", "queueXPropertyValue" }, + }) + }, + }, + }, + new Queue() + { + Name = "myQueue_error", + FifoQueue = false, + DeliveryDelay = 0, + VisibilityTimeout = 0, + ReceiveMessageWaitTime = 0, + MessageRetentionPeriod = 604800, + Policy = new Policy() + { + Statements = new List() + { + new Statement() + { + Effect = Effect.Allow, + Principal = new PrincipalObject(new KeyValuePair( + "AWS", new StringOrStringList(new AsyncApiAny("arn:aws:iam::123456789012:user/alex.wichmann")))), + Action = new StringOrStringList(new AsyncApiAny(new List + { + "sqs:*", + })), + }, + }, + }, + Extensions = new Dictionary() + { + { + "x-queueExtension", + new AsyncApiAny(new Dictionary() + { + { "queueXPropertyName", "queueXPropertyValue" }, + }) + }, + }, + }, + }, + Extensions = new Dictionary() + { + { + "x-internalObject", new AsyncApiAny(new Dictionary() + { + { "myExtensionPropertyName", "myExtensionPropertyValue" }, + }) + }, + }, + }); + + // Act + var actual = operation.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + + // Assert + var settings = new AsyncApiReaderSettings + { + Bindings = BindingsCollection.Sqs, + }; + + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + + // Assert + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + binding.Should().BeEquivalentTo(operation); + + var expectedSqsBinding = (SqsOperationBinding)operation.Bindings.Values.First(); + expectedSqsBinding.Should().BeEquivalentTo((SqsOperationBinding)binding.Bindings.Values.First(), options => options.IgnoringCyclicReferences()); + } + } +} \ No newline at end of file diff --git a/test/LEGO.AsyncAPI.Tests/Bindings/StringOrStringList_Should.cs b/test/LEGO.AsyncAPI.Tests/Bindings/StringOrStringList_Should.cs new file mode 100644 index 00000000..c437f15c --- /dev/null +++ b/test/LEGO.AsyncAPI.Tests/Bindings/StringOrStringList_Should.cs @@ -0,0 +1,149 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Tests.Bindings +{ + using System; + using System.Collections.Generic; + using System.Linq; + using FluentAssertions; + using LEGO.AsyncAPI.Bindings; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Readers; + using LEGO.AsyncAPI.Readers.ParseNodes; + using LEGO.AsyncAPI.Writers; + using NUnit.Framework; + + public class StringOrStringList_Should : TestBase + { + [Test] + public void StringOrStringList_IsInitialised_WhenPassedStringOrStringList() + { + // Arrange + var stringValue = new StringOrStringList(new AsyncApiAny("AsyncApi")); + var listValue = new StringOrStringList( + new AsyncApiAny(new List() + { + "Async", + "Api", + })); + + // Assert + stringValue.Value.GetValue().Should().Be("AsyncApi"); + listValue.Value.GetValue>() + .Should().BeEquivalentTo(new List() { "Async", "Api" }); + } + + [Test] + public void StringOrStringList_ThrowsArgumentException_WhenIntialisedWithoutStringOrStringList() + { + // Assert + var ex = Assert.Throws(() => new StringOrStringList(new AsyncApiAny(true))); + + // Assert + ex.Message.Should().Be("StringOrStringList should be a string value or a string list."); + } + + [Test] + public void StringOrStringList_ThrowsArgumentException_WhenIntialisedWithListOfNonStrings() + { + // Assert + var ex = Assert.Throws(() => new StringOrStringList( + new AsyncApiAny(new List() + { + "x", + 1, + "y", + }))); + + // Assert + ex.Message.Should().Be("StringOrStringList value should only contain string items."); + } + + [Test] + public void StringOrStringList_WhenValueIsString_SerializesDeserializes() + { + // Arrange + var expected = """ + bindings: + testBinding: + testProperty: someValue + """; + + var channel = new AsyncApiChannel(); + channel.Bindings.Add(new StringOrStringListTestBinding + { + TestProperty = new StringOrStringList(new AsyncApiAny("someValue")), + }); + + // Act + var actual = channel.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + + // Assert + var settings = new AsyncApiReaderSettings(); + settings.Bindings = new[] { new StringOrStringListTestBinding() }; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + + // Assert + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + binding.Should().BeEquivalentTo(channel); + } + + [Test] + public void StringOrStringList_WhenValueIsStringList_SerializesDeserializes() + { + // Arrange + var expected = """ + bindings: + testBinding: + testProperty: + - someValue01 + - someValue02 + - someValue03 + """; + + var channel = new AsyncApiChannel(); + channel.Bindings.Add(new StringOrStringListTestBinding + { + TestProperty = new StringOrStringList(new AsyncApiAny(new List + { + "someValue01", + "someValue02", + "someValue03", + })), + }); + + // Act + var actual = channel.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + + // Assert + var settings = new AsyncApiReaderSettings(); + settings.Bindings = new[] { new StringOrStringListTestBinding() }; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + + // Assert + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + binding.Should().BeEquivalentTo(channel); + } + } + + public class StringOrStringListTestBinding : ChannelBinding + { + public StringOrStringList TestProperty { get; set; } + + public override string BindingKey => "testBinding"; + + public override void SerializeProperties(IAsyncApiWriter writer) + { + writer.WriteStartObject(); + writer.WriteRequiredObject("testProperty", this.TestProperty, (w, t) => t.Value.Write(w)); + writer.WriteEndObject(); + } + + protected override FixedFieldMap FixedFieldMap => new() + { + { "testProperty", (a, n) => { a.TestProperty = new StringOrStringList(n.CreateAny()); } }, + }; + } +} \ No newline at end of file diff --git a/test/LEGO.AsyncAPI.Tests/Bindings/WebSockets/WebSocketBindings_Should.cs b/test/LEGO.AsyncAPI.Tests/Bindings/WebSockets/WebSocketBindings_Should.cs index 0f934b91..c37ada58 100644 --- a/test/LEGO.AsyncAPI.Tests/Bindings/WebSockets/WebSocketBindings_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Bindings/WebSockets/WebSocketBindings_Should.cs @@ -1,25 +1,30 @@ -namespace LEGO.AsyncAPI.Tests.Bindings.WebSockets +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Tests.Bindings.WebSockets { using FluentAssertions; + using LEGO.AsyncAPI.Bindings; + using LEGO.AsyncAPI.Bindings.WebSockets; using LEGO.AsyncAPI.Models; - using LEGO.AsyncAPI.Models.Bindings.WebSockets; using LEGO.AsyncAPI.Readers; using NUnit.Framework; - - internal class WebSocketBindings_Should + + public class WebSocketBindings_Should : TestBase { [Test] public void WebSocketChannelBinding_WithFilledObject_SerializesAndDeserializes() { // Arrange var expected = -@"bindings: - websockets: - method: POST - query: - description: this mah query - headers: - description: this mah binding"; + """ + bindings: + websockets: + method: POST + query: + description: this mah query + headers: + description: this mah binding + """; var channel = new AsyncApiChannel(); channel.Bindings.Add(new WebSocketsChannelBinding @@ -37,13 +42,15 @@ public void WebSocketChannelBinding_WithFilledObject_SerializesAndDeserializes() // Act var actual = channel.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); - actual = actual.MakeLineBreaksEnvironmentNeutral(); - expected = expected.MakeLineBreaksEnvironmentNeutral(); - var binding = new AsyncApiStringReader().ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.Websockets; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); // Assert - Assert.AreEqual(actual, expected); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + binding.Should().BeEquivalentTo(channel); } } diff --git a/test/LEGO.AsyncAPI.Tests/FluentAssertionExtensions.cs b/test/LEGO.AsyncAPI.Tests/FluentAssertionExtensions.cs new file mode 100644 index 00000000..31fec68a --- /dev/null +++ b/test/LEGO.AsyncAPI.Tests/FluentAssertionExtensions.cs @@ -0,0 +1,91 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Tests +{ + using System; + using System.IO; + using FluentAssertions; + using FluentAssertions.Primitives; + using NUnit.Framework; + + /// + /// Contains extension methods for working with fluent assertions. + /// + internal static class FluentAssertionExtensions + { + private static readonly char[] SeperatorChars; + + static FluentAssertionExtensions() + { + SeperatorChars = new[] + { + '\r', + '\n', + }; + } + + /// + /// Checks if the string is equal to other be ingores platform spesefic features + /// line new line breaks. This also checks to validate strings that are multiple lines + /// are the same number of lines. + /// + /// The assertion object. + /// The actaul value. + public static void BePlatformAgnosticEquivalentTo( + this StringAssertions assertions, + string input) + { + TestContext context = TestContext.CurrentContext; + StringSplitOptions splitOptions = StringSplitOptions.RemoveEmptyEntries; + string[] expected = assertions.Subject.Split(SeperatorChars, splitOptions); + string[] actual = input.Split(SeperatorChars, splitOptions); + + // So we don't go out of range + int minLength = Math.Min(expected.Length, actual.Length); + const int previewSize = 3; + + for (int i = 0; i < minLength; i++) + { + string actaulLine = actual[i]; + string expectedLine = expected[i]; + + if (!string.Equals(actaulLine, expectedLine)) + { + TestContext.WriteLine($"The line {i} does not match"); + TestContext.WriteLine("-----------------"); + + // Show lines above + for (int x = previewSize - 1; x >= 1; x--) + { + int index = i - x; + if (index >= 0) + { + TestContext.WriteLine($" {index:00}|{actual[index]}"); + } + } + + TestContext.WriteLine($"- {i:00}|{expectedLine}"); + TestContext.WriteLine($"+ {i:00}|{actaulLine}"); + + for (int x = 1; x < previewSize + 1; x++) + { + int index = i + x; + if (index < actual.Length) + { + TestContext.WriteLine($" {index:00}|{actual[index]}"); + } + else + { + TestContext.WriteLine("\\ end of file \\"); + break; + } + } + + Assert.Fail(); + } + } + + actual.Length.Should().Be(expected.Length); + } + } +} diff --git a/test/LEGO.AsyncAPI.Tests/LEGO.AsyncAPI.Tests.csproj b/test/LEGO.AsyncAPI.Tests/LEGO.AsyncAPI.Tests.csproj index c55609e5..bd35d2ae 100644 --- a/test/LEGO.AsyncAPI.Tests/LEGO.AsyncAPI.Tests.csproj +++ b/test/LEGO.AsyncAPI.Tests/LEGO.AsyncAPI.Tests.csproj @@ -1,51 +1,35 @@  - - net6.0 - disable - enable + + 11 + net8.0 + disable + enable + false + $(NoWarn);SA1600 + - false - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/test/LEGO.AsyncAPI.Tests/MQTT/MQTTBindings_Should.cs b/test/LEGO.AsyncAPI.Tests/MQTT/MQTTBindings_Should.cs new file mode 100644 index 00000000..bbccf86e --- /dev/null +++ b/test/LEGO.AsyncAPI.Tests/MQTT/MQTTBindings_Should.cs @@ -0,0 +1,141 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Tests.Bindings.MQTT +{ + using FluentAssertions; + using LEGO.AsyncAPI.Bindings; + using LEGO.AsyncAPI.Bindings.MQTT; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Readers; + using NUnit.Framework; + + public class MQTTBindings_Should + { + [Test] + public void MQTTServerBinding_FilledObject_SerializesAndDeserializes() + { + // Arrange + var expected = + """ + url: https://example.com + protocol: mqtt + bindings: + mqtt: + clientId: guest + cleanSession: true + lastWill: + topic: /last-wills + qos: 2 + message: Guest gone offline. + retain: false + keepAlive: 60 + sessionExpiryInterval: 600 + maximumPacketSize: 1200 + """; + + var server = new AsyncApiServer(); + server.Url = "https://example.com"; + server.Protocol = "mqtt"; + server.Bindings.Add(new MQTTServerBinding + { + ClientId = "guest", + CleanSession = true, + LastWill = new LastWill + { + Topic = "/last-wills", + QoS = 2, + Message = "Guest gone offline.", + Retain = false, + }, + KeepAlive = 60, + SessionExpiryInterval = 600, + MaximumPacketSize = 1200, + }); + + // Act + var actual = server.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + + // Assert + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.MQTT; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + + // Assert + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + binding.Should().BeEquivalentTo(server); + } + + [Test] + public void MQTTOperationBinding_WithFilledObject_SerializesAndDeserializes() + { + // Arrange + var expected = + """ + bindings: + mqtt: + qos: 2 + retain: true + messageExpiryInterval: 60 + """; + + var operation = new AsyncApiOperation(); + operation.Bindings.Add(new MQTTOperationBinding + { + QoS = 2, + Retain = true, + MessageExpiryInterval = 60, + }); + + // Act + var actual = operation.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.MQTT; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + + // Assert + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + binding.Should().BeEquivalentTo(operation); + } + + [Test] + public void MQTTMessageBinding_WithFilledObject_SerializesAndDeserializes() + { + // Arrange + var expected = + """ + bindings: + mqtt: + correlationData: + type: string + format: uuid + contentType: application/json + """; + + var message = new AsyncApiMessage(); + + message.Bindings.Add(new MQTTMessageBinding + { + ContentType = "application/json", + CorrelationData = new AsyncApiSchema + { + Type = SchemaType.String, + Format = "uuid", + }, + }); + + // Act + var actual = message.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.MQTT; + var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + + // Assert + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + binding.Should().BeEquivalentTo(message); + } + } +} diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiAnyTests.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiAnyTests.cs new file mode 100644 index 00000000..6c93e483 --- /dev/null +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiAnyTests.cs @@ -0,0 +1,55 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +using LEGO.AsyncAPI.Models; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace LEGO.AsyncAPI.Tests +{ + public class AsyncApiAnyTests + { + [Test] + public void GetValue_ReturnsCorrectConversions() + { + // Arrange + // Act + var a = new AsyncApiAny("string"); + var b = new AsyncApiAny(1); + var c = new AsyncApiAny(1.1); + var d = new AsyncApiAny(true); + var e = new AsyncApiAny(new MyType("test")); + var f = new AsyncApiAny(new List() { "test", "test2" }); + var g = new AsyncApiAny(new List() { "test", "test2" }.AsEnumerable()); + var h = new AsyncApiAny(new List() { new MyType("test") }); + var i = new AsyncApiAny(new Dictionary() { { "t", 2 } }); + var j = new AsyncApiAny(new Dictionary() { { "t", new MyType("test") } }); + + // Assert + Assert.AreEqual("string", a.GetValue()); + Assert.AreEqual(1, b.GetValue()); + Assert.AreEqual(1.1, c.GetValue()); + Assert.AreEqual(true, d.GetValue()); + Assert.NotNull(e.GetValue()); + Assert.IsNotEmpty(f.GetValue>()); + Assert.IsNotEmpty(f.GetValue>()); + Assert.IsNotEmpty(g.GetValue>()); + Assert.IsNotEmpty(g.GetValue>()); + Assert.IsNotEmpty(h.GetValue>()); + Assert.IsNotEmpty(h.GetValue>()); + Assert.IsNotEmpty(i.GetValue>()); + Assert.IsNotEmpty(j.GetValue>()); + } + + class MyType + { + public MyType(string value) + { + this.Value = value; + } + + public string Value { get; set; } + } + } +} diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiChannel_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiChannel_Should.cs index dfaa6764..50c6768b 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiChannel_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiChannel_Should.cs @@ -1,29 +1,55 @@ -namespace LEGO.AsyncAPI.Tests.Models +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Tests.Models { using System.Collections.Generic; + using System.Linq; + using FluentAssertions; + using LEGO.AsyncAPI.Bindings.Kafka; + using LEGO.AsyncAPI.Bindings.WebSockets; using LEGO.AsyncAPI.Models; - using LEGO.AsyncAPI.Models.Bindings.Kafka; - using LEGO.AsyncAPI.Models.Bindings.WebSockets; using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Readers; using NUnit.Framework; - internal class AsyncApiChannel_Should + internal class AsyncApiChannel_Should : TestBase { + [Test] + public void AsyncApiChannel_WithInlineParameter_DoesNotCreateReference() + { + var input = + """ + parameters: + id: + description: ids + schema: + type: string + enum: + - 08735ae0-6a1a-4578-8b4a-35aa26d15993 + - 97845c62-329c-4d87-ad24-4f611b909a10 + """; + + var channel = new AsyncApiStringReader().ReadFragment(input, AsyncApiVersion.AsyncApi2_0, out var _ ); + channel.Parameters.First().Value.Reference.Should().BeNull(); + } + [Test] public void AsyncApiChannel_WithWebSocketsBinding_Serializes() { - var expected = @"bindings: - websockets: - method: POST - query: - properties: - index: - description: the index - headers: - properties: - x-correlation-id: - description: the correlationid - bindingVersion: 0.1.0"; + var expected = """ + bindings: + websockets: + method: POST + query: + properties: + index: + description: the index + headers: + properties: + x-correlation-id: + description: the correlationid + bindingVersion: 0.1.0 + """; var channel = new AsyncApiChannel { @@ -65,22 +91,22 @@ public void AsyncApiChannel_WithWebSocketsBinding_Serializes() var actual = channel.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); - actual = actual.MakeLineBreaksEnvironmentNeutral(); - expected = expected.MakeLineBreaksEnvironmentNeutral(); - // Assert - Assert.AreEqual(actual, expected); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); } [Test] public void AsyncApiChannel_WithKafkaBinding_Serializes() { var expected = -@"bindings: - kafka: - topic: topic - partitions: 5 - replicas: 2"; + """ + bindings: + kafka: + topic: topic + partitions: 5 + replicas: 2 + """; var channel = new AsyncApiChannel { @@ -94,17 +120,14 @@ public void AsyncApiChannel_WithKafkaBinding_Serializes() Replicas = 2, } }, - }, }; var actual = channel.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); - actual = actual.MakeLineBreaksEnvironmentNeutral(); - expected = expected.MakeLineBreaksEnvironmentNeutral(); - // Assert - Assert.AreEqual(actual, expected); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); } } } diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiContact_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiContact_Should.cs index 817e2bc3..4cdbf307 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiContact_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiContact_Should.cs @@ -1,9 +1,11 @@ -using System; -using LEGO.AsyncAPI.Models; -using NUnit.Framework; +// Copyright (c) The LEGO Group. All rights reserved. namespace LEGO.AsyncAPI.Tests.Models { + using System; + using LEGO.AsyncAPI.Models; + using NUnit.Framework; + public class AsyncApiContact_Should { [Test] diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiExternalDocumentation_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiExternalDocumentation_Should.cs index 0721426c..e6720030 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiExternalDocumentation_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiExternalDocumentation_Should.cs @@ -1,9 +1,11 @@ -using System; -using LEGO.AsyncAPI.Models; -using NUnit.Framework; +// Copyright (c) The LEGO Group. All rights reserved. namespace LEGO.AsyncAPI.Tests.Models { + using System; + using LEGO.AsyncAPI.Models; + using NUnit.Framework; + public class AsyncApiExternalDocumentation_Should { [Test] diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiInfo_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiInfo_Should.cs index 31fdb8d0..7c4e9a73 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiInfo_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiInfo_Should.cs @@ -1,9 +1,11 @@ -using System; -using LEGO.AsyncAPI.Models; -using NUnit.Framework; +// Copyright (c) The LEGO Group. All rights reserved. namespace LEGO.AsyncAPI.Tests.Models { + using System; + using LEGO.AsyncAPI.Models; + using NUnit.Framework; + public class AsyncApiInfo_Should { [Test] diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiLicense_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiLicense_Should.cs index 5cd8f59a..eb412d21 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiLicense_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiLicense_Should.cs @@ -1,9 +1,11 @@ -using System; -using LEGO.AsyncAPI.Models; -using NUnit.Framework; +// Copyright (c) The LEGO Group. All rights reserved. namespace LEGO.AsyncAPI.Tests.Models { + using System; + using LEGO.AsyncAPI.Models; + using NUnit.Framework; + public class AsyncApiLicense_Should { [Test] diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiMessageExample_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiMessageExample_Should.cs index 3df3266e..b6f40e8c 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiMessageExample_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiMessageExample_Should.cs @@ -1,9 +1,11 @@ -using System; -using LEGO.AsyncAPI.Models; -using NUnit.Framework; +// Copyright (c) The LEGO Group. All rights reserved. namespace LEGO.AsyncAPI.Tests.Models { + using System; + using LEGO.AsyncAPI.Models; + using NUnit.Framework; + public class AsyncApiMessageExample_Should { [Test] diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs index d2dabcd3..a37f1503 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs @@ -1,210 +1,246 @@ -namespace LEGO.AsyncAPI.Tests.Models +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Tests.Models { using System; using System.Collections.Generic; using System.Linq; using FluentAssertions; + using LEGO.AsyncAPI.Bindings; + using LEGO.AsyncAPI.Bindings.Http; using LEGO.AsyncAPI.Models; - using LEGO.AsyncAPI.Models.Any; - using LEGO.AsyncAPI.Models.Bindings; - using LEGO.AsyncAPI.Models.Bindings.Http; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Readers; using NUnit.Framework; - internal class AsyncApiMessage_Should + internal class AsyncApiMessage_Should : TestBase { - [Test] - public void AsyncApiMessage_WithNoSchemaFormat_DeserializesToDefault() - { - // Arrange - var expected = -@"payload: - properties: - propertyA: - type: - - string - - 'null'"; + [Test] + public void AsyncApiMessage_WithNoType_DeserializesToDefault() + { + // Arrange + var expected = + """ + { + "payload": { + "type": "object", + "properties": { + "someProp": { + "enum": [ + "test", + "test2" + ] + } + } + } + } + """; - // Act - var message = new AsyncApiStringReader().ReadFragment(expected, AsyncApiVersion.AsyncApi2_0, out var diagnostic); + // Act + var message = new AsyncApiStringReader().ReadFragment(expected, AsyncApiVersion.AsyncApi2_0, out var diagnostic); - // Assert - diagnostic.Errors.Should().BeEmpty(); - message.SchemaFormat.Should().BeNull(); - } + // Assert + diagnostic.Errors.Should().BeEmpty(); + message.Payload.Properties.First().Value.Enum.Should().HaveCount(2); + } - [Test] - public void AsyncApiMessage_WithUnsupportedSchemaFormat_DeserializesWithError() - { - // Arrange - var expected = - @"payload: - properties: - propertyA: - type: - - string - - 'null' -schemaFormat: application/vnd.apache.avro;version=1.9.0"; + [Test] + public void AsyncApiMessage_WithNoSchemaFormat_DeserializesToDefault() + { + // Arrange + var expected = + """ + payload: + properties: + propertyA: + type: + - 'null' + - string + """; - // Act - new AsyncApiStringReader().ReadFragment(expected, AsyncApiVersion.AsyncApi2_0, out var diagnostic); + // Act + var message = new AsyncApiStringReader().ReadFragment(expected, AsyncApiVersion.AsyncApi2_0, out var diagnostic); - // Assert - diagnostic.Errors.Should().HaveCount(1); - diagnostic.Errors.First().Message.Should().StartWith("'application/vnd.apache.avro;version=1.9.0' is not a supported format"); - } + // Assert + diagnostic.Errors.Should().BeEmpty(); + message.SchemaFormat.Should().BeNull(); + } - [Test] - public void AsyncApiMessage_WithNoSchemaFormat_DoesNotSerializeSchemaFormat() - { - // Arrange - var expected = - @"payload: - properties: - propertyA: - type: - - string - - 'null'"; + [Test] + public void AsyncApiMessage_WithUnsupportedSchemaFormat_DeserializesWithError() + { + // Arrange + var expected = + """ + payload: + properties: + propertyA: + type: + - 'null' + - string + schemaFormat: application/vnd.apache.avro;version=1.9.0 + """; - var message = new AsyncApiMessage(); - message.Payload = new AsyncApiSchema() - { - Properties = new Dictionary() + // Act + new AsyncApiStringReader().ReadFragment(expected, AsyncApiVersion.AsyncApi2_0, out var diagnostic); + + // Assert + diagnostic.Errors.Should().HaveCount(1); + diagnostic.Errors.First().Message.Should().StartWith("'application/vnd.apache.avro;version=1.9.0' is not a supported format"); + } + + [Test] + public void AsyncApiMessage_WithNoSchemaFormat_DoesNotSerializeSchemaFormat() + { + // Arrange + var expected = + """ + payload: + properties: + propertyA: + type: + - 'null' + - string + """; + + var message = new AsyncApiMessage(); + message.Payload = new AsyncApiSchema() + { + Properties = new Dictionary() { { "propertyA", new AsyncApiSchema() { - Type = new List { SchemaType.String, SchemaType.Null }, + Type = SchemaType.String | SchemaType.Null, } }, }, - }; + }; - // Act - var actual = message.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + // Act + var actual = message.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); - actual = actual.MakeLineBreaksEnvironmentNeutral(); - expected = expected.MakeLineBreaksEnvironmentNeutral(); - var deserializedMessage = new AsyncApiStringReader().ReadFragment(expected, AsyncApiVersion.AsyncApi2_0, out _); + var deserializedMessage = new AsyncApiStringReader().ReadFragment(expected, AsyncApiVersion.AsyncApi2_0, out _); - // Assert - Assert.AreEqual(actual, expected); - message.Should().BeEquivalentTo(deserializedMessage); + // Assert + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + message.Should().BeEquivalentTo(deserializedMessage); } - [Test] - public void AsyncApiMessage_WithSchemaFormat_Serializes() - { - // Arrange - var expected = - @"payload: - properties: - propertyA: - type: - - string - - 'null' -schemaFormat: application/vnd.aai.asyncapi+json;version=2.6.0"; + [Test] + public void AsyncApiMessage_WithSchemaFormat_Serializes() + { + // Arrange + var expected = + """ + payload: + properties: + propertyA: + type: + - 'null' + - string + schemaFormat: application/vnd.aai.asyncapi+json;version=2.6.0 + """; - var message = new AsyncApiMessage(); - message.SchemaFormat = "application/vnd.aai.asyncapi+json;version=2.6.0"; - message.Payload = new AsyncApiSchema() - { - Properties = new Dictionary() + var message = new AsyncApiMessage(); + message.SchemaFormat = "application/vnd.aai.asyncapi+json;version=2.6.0"; + message.Payload = new AsyncApiSchema() + { + Properties = new Dictionary() { { "propertyA", new AsyncApiSchema() { - Type = new List { SchemaType.String, SchemaType.Null }, + Type = SchemaType.String | SchemaType.Null, } }, }, - }; - - // Act - var actual = message.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); - - actual = actual.MakeLineBreaksEnvironmentNeutral(); - expected = expected.MakeLineBreaksEnvironmentNeutral(); + }; - var deserializedMessage = new AsyncApiStringReader().ReadFragment(expected, AsyncApiVersion.AsyncApi2_0, out _); + // Act + var actual = message.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + var deserializedMessage = new AsyncApiStringReader().ReadFragment(expected, AsyncApiVersion.AsyncApi2_0, out _); - // Assert - Assert.AreEqual(actual, expected); - message.Should().BeEquivalentTo(deserializedMessage); - } + // Assert + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + message.Should().BeEquivalentTo(deserializedMessage); + } - [Test] + [Test] public void AsyncApiMessage_WithFilledObject_Serializes() { var expected = -@"headers: - title: HeaderTitle - description: HeaderDescription - writeOnly: true - examples: - - x-correlation-id: nil -payload: - properties: - propA: - type: string - propB: - type: string -correlationId: - description: CorrelationDescription - location: Header - x-extension-a: a -contentType: MessageContentType -name: MessageName -title: MessageTitle -summary: MessageSummary -description: MessageDescription -tags: - - name: tagA - description: a -externalDocs: - description: example docs description - url: https://example.com/docs -bindings: - http: - headers: - title: SchemaTitle - description: SchemaDescription - writeOnly: true - examples: - - cKey: c - dKey: 1 -examples: - - payload: - PropA: a - PropB: b -traits: - - headers: - title: SchemaTitle - description: SchemaDescription - writeOnly: true - examples: - - eKey: e - fKey: 1 - name: MessageTraitName - title: MessageTraitTitle - summary: MessageTraitSummary - description: MessageTraitDescription - tags: - - name: tagB - description: b - externalDocs: - description: example docs description - url: https://example.com/docs - examples: - - name: MessageExampleName - summary: MessageExampleSummary - payload: - gKey: g - hKey: true - x-extension-b: b - x-extension-c: c"; + """ + headers: + title: HeaderTitle + description: HeaderDescription + writeOnly: true + examples: + - x-correlation-id: nil + payload: + properties: + propA: + type: string + propB: + type: string + correlationId: + description: CorrelationDescription + location: Header + x-extension-a: a + contentType: MessageContentType + name: MessageName + title: MessageTitle + summary: MessageSummary + description: MessageDescription + tags: + - name: tagA + description: a + externalDocs: + description: example docs description + url: https://example.com/docs + bindings: + http: + headers: + title: SchemaTitle + description: SchemaDescription + writeOnly: true + examples: + - cKey: c + dKey: 1 + examples: + - payload: + PropA: a + PropB: b + traits: + - headers: + title: SchemaTitle + description: SchemaDescription + writeOnly: true + examples: + - eKey: e + fKey: 1 + name: MessageTraitName + title: MessageTraitTitle + summary: MessageTraitSummary + description: MessageTraitDescription + tags: + - name: tagB + description: b + externalDocs: + description: example docs description + url: https://example.com/docs + examples: + - name: MessageExampleName + summary: MessageExampleSummary + payload: + gKey: g + hKey: true + x-extension-b: b + x-extension-c: c + """; var message = new AsyncApiMessage { @@ -213,12 +249,12 @@ public void AsyncApiMessage_WithFilledObject_Serializes() Title = "HeaderTitle", WriteOnly = true, Description = "HeaderDescription", - Examples = new List + Examples = new List { - new AsyncApiObject + new AsyncApiAny(new Dictionary { - { "x-correlation-id", new AsyncApiString("nil") }, - }, + { "x-correlation-id", "nil" }, + }), }, }, Payload = new AsyncApiSchema() @@ -228,19 +264,13 @@ public void AsyncApiMessage_WithFilledObject_Serializes() { "propA", new AsyncApiSchema() { - Type = new List() - { - SchemaType.String, - }, + Type = SchemaType.String, } }, { "propB", new AsyncApiSchema() { - Type = new List() - { - SchemaType.String, - }, + Type = SchemaType.String, } }, }, @@ -251,7 +281,7 @@ public void AsyncApiMessage_WithFilledObject_Serializes() Description = "CorrelationDescription", Extensions = new Dictionary { - { "x-extension-a", new AsyncApiString("a") }, + { "x-extension-a", new AsyncApiAny("a") }, }, }, ContentType = "MessageContentType", @@ -275,20 +305,20 @@ public void AsyncApiMessage_WithFilledObject_Serializes() Bindings = new AsyncApiBindings() { { - BindingType.Http, new HttpMessageBinding + "http", new HttpMessageBinding { Headers = new AsyncApiSchema { Title = "SchemaTitle", WriteOnly = true, Description = "SchemaDescription", - Examples = new List + Examples = new List { - new AsyncApiObject + new AsyncApiAny(new Dictionary { - { "cKey", new AsyncApiString("c") }, - { "dKey", new AsyncApiInteger(1) }, - }, + { "cKey", "c" }, + { "dKey", 1 }, + }), }, }, } @@ -298,11 +328,11 @@ public void AsyncApiMessage_WithFilledObject_Serializes() { new AsyncApiMessageExample { - Payload = new AsyncApiObject() + Payload = new AsyncApiAny(new Dictionary() { - { "PropA", new AsyncApiString("a") }, - { "PropB", new AsyncApiString("b") }, - }, + { "PropA", "a" }, + { "PropB", "b" }, + }), }, }, Traits = new List @@ -316,13 +346,13 @@ public void AsyncApiMessage_WithFilledObject_Serializes() Title = "SchemaTitle", WriteOnly = true, Description = "SchemaDescription", - Examples = new List + Examples = new List { - new AsyncApiObject + new AsyncApiAny(new Dictionary { - { "eKey", new AsyncApiString("e") }, - { "fKey", new AsyncApiInteger(1) }, - }, + { "eKey", "e" }, + { "fKey", 1 }, + }), }, }, Examples = new List @@ -331,14 +361,14 @@ public void AsyncApiMessage_WithFilledObject_Serializes() { Summary = "MessageExampleSummary", Name = "MessageExampleName", - Payload = new AsyncApiObject + Payload = new AsyncApiAny(new Dictionary { - { "gKey", new AsyncApiString("g") }, - { "hKey", new AsyncApiBoolean(true) }, - }, + { "gKey", "g" }, + { "hKey", true }, + }), Extensions = new Dictionary { - { "x-extension-b", new AsyncApiString("b") }, + { "x-extension-b", new AsyncApiAny("b") }, }, }, }, @@ -359,7 +389,7 @@ public void AsyncApiMessage_WithFilledObject_Serializes() }, Extensions = new Dictionary { - { "x-extension-c", new AsyncApiString("c") }, + { "x-extension-c", new AsyncApiAny("c") }, }, }, }, @@ -367,13 +397,13 @@ public void AsyncApiMessage_WithFilledObject_Serializes() var actual = message.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); - actual = actual.MakeLineBreaksEnvironmentNeutral(); - expected = expected.MakeLineBreaksEnvironmentNeutral(); - - var deserializedMessage = new AsyncApiStringReader().ReadFragment(expected, AsyncApiVersion.AsyncApi2_0, out _); + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.All; + var deserializedMessage = new AsyncApiStringReader(settings).ReadFragment(expected, AsyncApiVersion.AsyncApi2_0, out _); // Assert - Assert.AreEqual(actual, expected); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); message.Should().BeEquivalentTo(deserializedMessage); } } diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiOAuthFlow_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiOAuthFlow_Should.cs index 207fe140..93fc79e1 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiOAuthFlow_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiOAuthFlow_Should.cs @@ -1,9 +1,11 @@ -using System; -using LEGO.AsyncAPI.Models; -using NUnit.Framework; +// Copyright (c) The LEGO Group. All rights reserved. namespace LEGO.AsyncAPI.Tests.Models { + using System; + using LEGO.AsyncAPI.Models; + using NUnit.Framework; + public class AsyncApiOAuthFlow_Should { [Test] diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiOperation_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiOperation_Should.cs index f0778cec..5597b211 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiOperation_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiOperation_Should.cs @@ -1,16 +1,19 @@ -using System; -using System.Globalization; -using System.IO; -using LEGO.AsyncAPI.Models; -using LEGO.AsyncAPI.Models.Bindings.Http; -using LEGO.AsyncAPI.Models.Bindings.Kafka; -using LEGO.AsyncAPI.Models.Interfaces; -using LEGO.AsyncAPI.Writers; -using NUnit.Framework; +// Copyright (c) The LEGO Group. All rights reserved. namespace LEGO.AsyncAPI.Tests.Models { - public class AsyncApiOperation_Should + using System; + using System.Globalization; + using System.IO; + using FluentAssertions; + using LEGO.AsyncAPI.Bindings.Http; + using LEGO.AsyncAPI.Bindings.Kafka; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Models.Interfaces; + using LEGO.AsyncAPI.Writers; + using NUnit.Framework; + + public class AsyncApiOperation_Should : TestBase { [Test] public void SerializeV2_WithNullWriter_Throws() @@ -27,66 +30,72 @@ public void SerializeV2_WithNullWriter_Throws() public void SerializeV2_WithMultipleMessages_SerializesWithOneOf() { // Arrange - var expected = @"message: - oneOf: - - name: First Message - - name: Second Message"; + var expected = """ + message: + oneOf: + - name: First Message + - name: Second Message + """; var asyncApiOperation = new AsyncApiOperation(); asyncApiOperation.Message.Add(new AsyncApiMessage { Name = "First Message" }); asyncApiOperation.Message.Add(new AsyncApiMessage { Name = "Second Message" }); - var outputString = new StringWriter(CultureInfo.InvariantCulture); - var writer = new AsyncApiYamlWriter(outputString); + var outputString = new StringWriter(); + var settings = new AsyncApiWriterSettings(); + var writer = new AsyncApiYamlWriter(outputString, settings); // Act asyncApiOperation.SerializeV2(writer); // Assert var actual = outputString.GetStringBuilder().ToString(); - actual = actual.MakeLineBreaksEnvironmentNeutral(); - expected = expected.MakeLineBreaksEnvironmentNeutral(); - Assert.AreEqual(actual, expected); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); } [Test] public void SerializeV2_WithSingleMessage_Serializes() { // Arrange - var expected = @"message: - name: First Message"; + var expected = """ + message: + name: First Message + """; var asyncApiOperation = new AsyncApiOperation(); asyncApiOperation.Message.Add(new AsyncApiMessage { Name = "First Message" }); - var outputString = new StringWriter(CultureInfo.InvariantCulture); - var writer = new AsyncApiYamlWriter(outputString); + var settings = new AsyncApiWriterSettings(); + var outputString = new StringWriter(); + var writer = new AsyncApiYamlWriter(outputString, settings); // Act asyncApiOperation.SerializeV2(writer); // Assert var actual = outputString.GetStringBuilder().ToString(); - actual = actual.MakeLineBreaksEnvironmentNeutral(); - expected = expected.MakeLineBreaksEnvironmentNeutral(); - Assert.AreEqual(actual, expected); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); } [Test] public void AsyncApiOperation_WithBindings_Serializes() { var expected = -@"bindings: - http: - type: type - method: PUT - query: - description: some query - kafka: - groupId: - description: some Id - clientId: - description: some Id"; + """ + bindings: + http: + type: request + method: PUT + query: + description: some query + kafka: + groupId: + description: some Id + clientId: + description: some Id + """; var operation = new AsyncApiOperation { @@ -95,7 +104,7 @@ public void AsyncApiOperation_WithBindings_Serializes() { new HttpOperationBinding { - Type = "type", + Type = HttpOperationBinding.HttpOperationType.Request, Method = "PUT", Query = new AsyncApiSchema { @@ -121,11 +130,9 @@ public void AsyncApiOperation_WithBindings_Serializes() var actual = operation.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); - actual = actual.MakeLineBreaksEnvironmentNeutral(); - expected = expected.MakeLineBreaksEnvironmentNeutral(); - // Assert - Assert.AreEqual(actual, expected); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); } } } diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs new file mode 100644 index 00000000..c4491e40 --- /dev/null +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs @@ -0,0 +1,254 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Tests +{ + using FluentAssertions; + using FluentAssertions.Primitives; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Readers; + using NUnit.Framework; + using System.Linq; + + public class AsyncApiReference_Should : TestBase + { + [Test] + public void AsyncApiReference_WithExternalFragmentUriReference_AllowReference() + { + // Arrange + var actual = """ + payload: + $ref: http://example.com/some-resource#/path/to/external/fragment + """; + var reader = new AsyncApiStringReader(); + + // Act + var deserialized = reader.ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out var diagnostic); + + // Assert + diagnostic.Errors.Should().BeEmpty(); + deserialized.Payload.UnresolvedReference.Should().BeTrue(); + + var reference = deserialized.Payload.Reference; + reference.ExternalResource.Should().Be("http://example.com/some-resource"); + reference.Id.Should().Be("/path/to/external/fragment"); + reference.IsFragment.Should().BeTrue(); + reference.IsExternal.Should().BeTrue(); + reference.Type.Should().Be(ReferenceType.Schema); + var expected = deserialized.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + } + + [Test] + public void AsyncApiReference_WithFragmentReference_AllowReference() + { + // Arrange + var actual = """ + payload: + $ref: /fragments/myFragment + """; + var reader = new AsyncApiStringReader(); + + // Act + var deserialized = reader.ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out var diagnostic); + + // Assert + diagnostic.Errors.Should().BeEmpty(); + deserialized.Payload.UnresolvedReference.Should().BeTrue(); + + var reference = deserialized.Payload.Reference; + reference.Type.Should().Be(ReferenceType.Schema); + reference.ExternalResource.Should().Be("/fragments/myFragment"); + reference.Id.Should().BeNull(); + reference.IsFragment.Should().BeTrue(); + reference.IsExternal.Should().BeTrue(); + var expected = deserialized.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + } + + [Test] + public void AsyncApiReference_WithInternalComponentReference_AllowReference() + { + // Arrange + var actual = """ + payload: + $ref: '#/components/schemas/test' + """; + var reader = new AsyncApiStringReader(); + + // Act + var deserialized = reader.ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out var diagnostic); + + // Assert + diagnostic.Errors.Should().BeEmpty(); + var reference = deserialized.Payload.Reference; + reference.ExternalResource.Should().BeNull(); + reference.Type.Should().Be(ReferenceType.Schema); + reference.Id.Should().Be("test"); + reference.IsFragment.Should().BeFalse(); + reference.IsExternal.Should().BeFalse(); + + var expected = deserialized.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + } + + [Test] + public void AsyncApiReference_WithExternalFragmentReference_AllowReference() + { + // Arrange + var actual = """ + payload: + $ref: ./myjsonfile.json#/fragment + """; + var reader = new AsyncApiStringReader(); + + // Act + var deserialized = reader.ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out var diagnostic); + + // Assert + diagnostic.Errors.Should().BeEmpty(); + var reference = deserialized.Payload.Reference; + reference.ExternalResource.Should().Be("./myjsonfile.json"); + reference.Id.Should().Be("/fragment"); + reference.IsFragment.Should().BeTrue(); + reference.IsExternal.Should().BeTrue(); + + var expected = deserialized.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + } + + [Test] + public void AsyncApiReference_WithExternalComponentReference_AllowReference() + { + // Arrange + var actual = """ + payload: + $ref: ./someotherdocument.json#/components/schemas/test + """; + var reader = new AsyncApiStringReader(); + + // Act + var deserialized = reader.ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out var diagnostic); + + // Assert + diagnostic.Errors.Should().BeEmpty(); + var reference = deserialized.Payload.Reference; + reference.ExternalResource.Should().Be("./someotherdocument.json"); + reference.Type.Should().Be(ReferenceType.Schema); + reference.Id.Should().Be("test"); + reference.IsFragment.Should().BeFalse(); + reference.IsExternal.Should().BeTrue(); + + var expected = deserialized.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + } + + [Test] + public void AsyncApiDocument_WithInternalComponentReference_ResolvesReference() + { + // Arrange + var actual = """ + asyncapi: 2.6.0 + info: + title: My AsyncAPI Document + version: 1.0.0 + channels: + myChannel: + $ref: '#/components/channels/myChannel' + components: + channels: + myChannel: + description: customDescription + """; + + var settings = new AsyncApiReaderSettings() + { + ReferenceResolution = ReferenceResolutionSetting.ResolveReferences, + }; + var reader = new AsyncApiStringReader(settings); + + // Act + var deserialized = reader.Read(actual, out var diagnostic); + + // Assert + diagnostic.Errors.Should().BeEmpty(); + var channel = deserialized.Channels.First().Value; + + channel.UnresolvedReference.Should().BeFalse(); + channel.Description.Should().Be("customDescription"); + channel.Reference.ExternalResource.Should().BeNull(); + channel.Reference.Id.Should().Be("myChannel"); + channel.Reference.IsExternal.Should().BeFalse(); + channel.Reference.Type.Should().Be(ReferenceType.Channel); + } + + [Test] + public void AsyncApiDocument_WithExternalReference_DoesNotResolve() + { + // Arrange + var actual = """ + asyncapi: 2.6.0 + info: + title: My AsyncAPI Document + version: 1.0.0 + channels: + myChannel: + $ref: http://example.com/channel.json + """; + + var settings = new AsyncApiReaderSettings() + { + ReferenceResolution = ReferenceResolutionSetting.ResolveReferences, + }; + var reader = new AsyncApiStringReader(settings); + + // Act + var deserialized = reader.Read(actual, out var diagnostic); + + // Assert + diagnostic.Errors.Should().BeEmpty(); + var channel = deserialized.Channels.First().Value; + + channel.UnresolvedReference.Should().BeTrue(); + channel.Description.Should().BeNull(); + channel.Reference.ExternalResource.Should().Be("http://example.com/channel.json"); + channel.Reference.Type.Should().Be(ReferenceType.Channel); + channel.Reference.Id.Should().BeNull(); + channel.Reference.IsExternal.Should().BeTrue(); + channel.Reference.IsFragment.Should().BeFalse(); + } + + [Test] + public void AsyncApiReference_WithExternalReference_AllowsReferenceDoesNotResolve() + { + // Arrange + var actual = """ + payload: + $ref: http://example.com/json.json + """; + var reader = new AsyncApiStringReader(); + + // Act + var deserialized = reader.ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out var diagnostic); + + // Assert + diagnostic.Errors.Should().BeEmpty(); + var reference = deserialized.Payload.Reference; + reference.ExternalResource.Should().Be("http://example.com/json.json"); + reference.Id.Should().BeNull(); + reference.IsExternal.Should().BeTrue(); + reference.IsFragment.Should().BeFalse(); + diagnostic.Errors.Should().BeEmpty(); + + var expected = deserialized.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + + expected + .Should() + .BePlatformAgnosticEquivalentTo(actual); + } + } +} \ No newline at end of file diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiSchema_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiSchema_Should.cs index 57101ed9..ec7c023f 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiSchema_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiSchema_Should.cs @@ -1,79 +1,399 @@ -using LEGO.AsyncAPI.Models; -using LEGO.AsyncAPI.Writers; -using NUnit.Framework; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; +// Copyright (c) The LEGO Group. All rights reserved. namespace LEGO.AsyncAPI.Tests.Models { - public class AsyncApiSchema_Should + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using FluentAssertions; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Readers; + using LEGO.AsyncAPI.Writers; + using NUnit.Framework; + + public class AsyncApiSchema_Should : TestBase { - private string NoInlinedReferences => - @"asyncapi: '2.6.0' -info: - title: Streetlights Kafka API - version: 1.0.0 - description: The Smartylighting Streetlights API allows you to remotely manage the city lights. - license: - name: Apache 2.0 - url: https://www.apache.org/licenses/LICENSE-2.0 -channels: - mychannel: - publish: - message: - payload: - type: object - required: - - testB - properties: - testC: - $ref: '#/components/schemas/testC' - testB: - $ref: '#/components/schemas/testB' -components: - schemas: - testD: - type: string - format: uuid - testC: - type: object - properties: - testD: - $ref: '#/components/schemas/testD' - testB: - type: boolean - description: test"; - - private string InlinedReferences => - @"asyncapi: '2.6.0' -info: - title: Streetlights Kafka API - version: 1.0.0 - description: The Smartylighting Streetlights API allows you to remotely manage the city lights. - license: - name: Apache 2.0 - url: https://www.apache.org/licenses/LICENSE-2.0 -channels: - mychannel: - publish: - message: - payload: - type: object - required: - - testB - properties: - testC: - type: object - properties: - testD: - type: string - format: uuid - testB: - type: boolean - description: test -components: { }"; + public static AsyncApiSchema BasicSchema = new AsyncApiSchema(); + + public static AsyncApiSchema AdvancedSchemaNumber = new AsyncApiSchema + { + Title = "title1", + MultipleOf = 3, + Maximum = 42, + ExclusiveMinimum = 42, + Minimum = 10, + Default = new AsyncApiAny(15), + Type = SchemaType.Integer, + Nullable = true, + ExternalDocs = new AsyncApiExternalDocumentation + { + Url = new Uri("http://example.com/externalDocs"), + }, + }; + + public static AsyncApiSchema AdvancedSchemaBigNumbers = new AsyncApiSchema + { + Title = "title1", + MultipleOf = 3, + Maximum = double.MaxValue, + ExclusiveMinimum = double.MinValue, + Minimum = double.MinValue, + Default = new AsyncApiAny(15), + Type = SchemaType.Integer, + Nullable = true, + ExternalDocs = new AsyncApiExternalDocumentation + { + Url = new Uri("http://example.com/externalDocs"), + }, + }; + + public static AsyncApiSchema AdvancedSchemaObject = new AsyncApiSchema + { + Title = "title1", + Properties = new Dictionary + { + ["property1"] = new AsyncApiSchema + { + Properties = new Dictionary + { + ["property2"] = new AsyncApiSchema + { + Type = SchemaType.Integer, + }, + ["property3"] = new AsyncApiSchema + { + Type = SchemaType.String, + MaxLength = 15, + }, + }, + AdditionalProperties = new FalseApiSchema(), + Items = new FalseApiSchema(), + AdditionalItems = new FalseApiSchema(), + }, + ["property4"] = new AsyncApiSchema + { + Properties = new Dictionary + { + ["property5"] = new AsyncApiSchema + { + Properties = new Dictionary + { + ["property6"] = new AsyncApiSchema + { + Type = SchemaType.Boolean, + }, + }, + }, + ["property7"] = new AsyncApiSchema + { + Type = SchemaType.String, + MinLength = 2, + }, + }, + PatternProperties = new Dictionary() + { + { + "^S_", + new AsyncApiSchema() + { + Type = SchemaType.String, + } + }, + { + "^I_", new AsyncApiSchema() + { + Type = SchemaType.Integer, + } + }, + }, + PropertyNames = new AsyncApiSchema() + { + Pattern = "^[A-Za-z_][A-Za-z0-9_]*$", + }, + AdditionalProperties = new AsyncApiSchema + { + Properties = new Dictionary + { + ["Property8"] = new AsyncApiSchema + { + Type = SchemaType.String | SchemaType.Null, + }, + }, + }, + Items = new AsyncApiSchema + { + Properties = new Dictionary + { + ["Property9"] = new AsyncApiSchema + { + Type = SchemaType.String | SchemaType.Null, + }, + }, + }, + AdditionalItems = new AsyncApiSchema + { + Properties = new Dictionary + { + ["Property10"] = new AsyncApiSchema + { + Type = SchemaType.String | SchemaType.Null, + }, + }, + }, + }, + ["property11"] = new AsyncApiSchema + { + Const = new AsyncApiAny("aSpecialConstant"), + }, + }, + Nullable = true, + ExternalDocs = new AsyncApiExternalDocumentation + { + Url = new Uri("http://example.com/externalDocs"), + }, + }; + + public static AsyncApiSchema AdvancedSchemaWithAllOf = new AsyncApiSchema + { + Title = "title1", + AllOf = new List + { + new AsyncApiSchema + { + Title = "title2", + Properties = new Dictionary + { + ["property1"] = new AsyncApiSchema + { + Type = SchemaType.Integer, + }, + ["property2"] = new AsyncApiSchema + { + Type = SchemaType.String, + MaxLength = 15, + }, + }, + }, + new AsyncApiSchema + { + Title = "title3", + Properties = new Dictionary + { + ["property3"] = new AsyncApiSchema + { + Properties = new Dictionary + { + ["property4"] = new AsyncApiSchema + { + Type = SchemaType.Boolean , + }, + }, + }, + ["property5"] = new AsyncApiSchema + { + Type = SchemaType.String, + MinLength = 2, + }, + }, + Nullable = true, + }, + }, + Nullable = true, + ExternalDocs = new AsyncApiExternalDocumentation + { + Url = new Uri("http://example.com/externalDocs"), + }, + }; + + public static AsyncApiSchema ReferencedSchema = new AsyncApiSchema + { + Title = "title1", + MultipleOf = 3, + Maximum = 42, + ExclusiveMinimum = 42, + Minimum = 10, + Default = new AsyncApiAny(15), + Type = SchemaType.Integer, + + Nullable = true, + ExternalDocs = new AsyncApiExternalDocumentation + { + Url = new Uri("http://example.com/externalDocs"), + }, + + Reference = new AsyncApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1", + }, + }; + + public static AsyncApiSchema AdvancedSchemaWithRequiredPropertiesObject = new AsyncApiSchema + { + Title = "title1", + Required = new HashSet() { "property1" }, + Properties = new Dictionary + { + ["property1"] = new AsyncApiSchema + { + Required = new HashSet() { "property3" }, + Properties = new Dictionary + { + ["property2"] = new AsyncApiSchema + { + Type = SchemaType.Integer, + }, + ["property3"] = new AsyncApiSchema + { + Type = SchemaType.String, + MaxLength = 15, + ReadOnly = true, + }, + }, + ReadOnly = true, + }, + ["property4"] = new AsyncApiSchema + { + Properties = new Dictionary + { + ["property5"] = new AsyncApiSchema + { + Properties = new Dictionary + { + ["property6"] = new AsyncApiSchema + { + Type = SchemaType.Boolean, + }, + }, + }, + ["property7"] = new AsyncApiSchema + { + Type = SchemaType.String, + MinLength = 2, + }, + }, + ReadOnly = true, + }, + }, + Nullable = true, + ExternalDocs = new AsyncApiExternalDocumentation + { + Url = new Uri("http://example.com/externalDocs"), + }, + }; + + [Test] + public void SerializeAsJson_WithBasicSchema_V2Works() + { + // Arrange + var expected = @"{ }"; + + // Act + var actual = BasicSchema.SerializeAsJson(AsyncApiVersion.AsyncApi2_0); + + // Assert + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + } + + [Test] + public void SerializeAsJson_WithAdvancedSchemaNumber_V2Works() + { + // Arrange + var expected = """ + { + "title": "title1", + "type": "integer", + "maximum": 42, + "minimum": 10, + "exclusiveMinimum": 42, + "multipleOf": 3, + "default": 15, + "nullable": true, + "externalDocs": { + "url": "http://example.com/externalDocs" + } + } + """; + + // Act + var actual = AdvancedSchemaNumber.SerializeAsJson(AsyncApiVersion.AsyncApi2_0); + + // Assert + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + } + + [Test] + public void SerializeAsJson_WithAdvancedSchemaBigNumbers_V2Works() + { + // Arrange + var expected = """ + { + "title": "title1", + "type": "integer", + "maximum": 1.7976931348623157E+308, + "minimum": -1.7976931348623157E+308, + "exclusiveMinimum": -1.7976931348623157E+308, + "multipleOf": 3, + "default": 15, + "nullable": true, + "externalDocs": { + "url": "http://example.com/externalDocs" + } + } + """; + + // Act + var actual = AdvancedSchemaBigNumbers.SerializeAsJson(AsyncApiVersion.AsyncApi2_0); + + // Assert + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + } + + [Test] + public void SerializeAsJson_WithAdvancedSchemaObject_V2Works() + { + // Arrange + string expected = this.GetTestData(); + + // Act + var actual = AdvancedSchemaObject.SerializeAsJson(AsyncApiVersion.AsyncApi2_0); + + // Assert + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + } + + [Test] + public void Deserialize_WithAdvancedSchema_Works() + { + // Arrange + var json = GetTestData(); + var expected = AdvancedSchemaObject; + + // Act + var actual = new AsyncApiStringReader().ReadFragment(json, AsyncApiVersion.AsyncApi2_0, out var _diagnostics); + + // Assert + actual.Should().BeEquivalentTo(expected); + } + + [Test] + public void SerializeAsJson_WithAdvancedSchemaWithAllOf_V2Works() + { + // Arrange + var expected = this.GetTestData(); + + // Act + var actual = AdvancedSchemaWithAllOf.SerializeAsJson(AsyncApiVersion.AsyncApi2_0); + + // Assert + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); + } [Theory] [TestCase(true)] @@ -103,7 +423,7 @@ public void Serialize_WithInliningOptions_ShouldInlineAccordingly(bool shouldInl { Payload = new AsyncApiSchema { - Type = new List { SchemaType.Object }, + Type = SchemaType.Object, Required = new HashSet { "testB" }, Properties = new Dictionary { @@ -115,42 +435,33 @@ public void Serialize_WithInliningOptions_ShouldInlineAccordingly(bool shouldInl }, }, }) - .WithComponent("testD", new AsyncApiSchema() { Type = new List { SchemaType.String }, Format = "uuid" }) + .WithComponent("testD", new AsyncApiSchema() { Type = SchemaType.String, Format = "uuid" }) .WithComponent("testC", new AsyncApiSchema() { - Type = new List { SchemaType.Object }, + Type = SchemaType.Object, Properties = new Dictionary { { "testD", new AsyncApiSchema { Reference = new AsyncApiReference { Type = ReferenceType.Schema, Id = "testD" } } }, }, }) - .WithComponent("testB", new AsyncApiSchema() { Description = "test", Type = new List { SchemaType.Boolean } }) + .WithComponent("testB", new AsyncApiSchema() { Description = "test", Type = SchemaType.Boolean }) .Build(); - var outputString = new StringWriter(CultureInfo.InvariantCulture); + var outputString = new StringWriter(); var writer = new AsyncApiYamlWriter(outputString, new AsyncApiWriterSettings { InlineReferences = shouldInline }); // Act asyncApiDocument.SerializeV2(writer); var actual = outputString.ToString(); - actual = actual.MakeLineBreaksEnvironmentNeutral(); - - string expected = string.Empty; // Assert - if (shouldInline) - { - expected = this.InlinedReferences; - } - else - { - expected = this.NoInlinedReferences; - } - - expected = expected.MakeLineBreaksEnvironmentNeutral(); + string expected = this.GetTestData(shouldInline + ? "AsyncApiSchema_InlinedReferences" + : "AsyncApiSchema_NoInlinedReferences.yml"); - Assert.AreEqual(actual, expected); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); } [Test] @@ -173,7 +484,7 @@ public void Serialize_WithOneOf_DoesNotWriteThen() { var mainSchema = new AsyncApiSchema(); var subSchema = new AsyncApiSchema(); - subSchema.Properties.Add("title", new AsyncApiSchema() { Type = new List { SchemaType.String } }); + subSchema.Properties.Add("title", new AsyncApiSchema() { Type = SchemaType.String }); mainSchema.OneOf = new List() { subSchema }; var yaml = mainSchema.Serialize(AsyncApiVersion.AsyncApi2_0, AsyncApiFormat.Yaml); @@ -191,7 +502,7 @@ public void Serialize_WithAnyOf_DoesNotWriteIf() { var mainSchema = new AsyncApiSchema(); var subSchema = new AsyncApiSchema(); - subSchema.Properties.Add("title", new AsyncApiSchema() { Type = new List { SchemaType.String } }); + subSchema.Properties.Add("title", new AsyncApiSchema() { Type = SchemaType.String }); mainSchema.AnyOf = new List() { subSchema }; var yaml = mainSchema.Serialize(AsyncApiVersion.AsyncApi2_0, AsyncApiFormat.Yaml); @@ -208,7 +519,7 @@ public void Serialize_WithNot_DoesNotWriteElse() { var mainSchema = new AsyncApiSchema(); var subSchema = new AsyncApiSchema(); - subSchema.Properties.Add("title", new AsyncApiSchema() { Type = new List { SchemaType.String } }); + subSchema.Properties.Add("title", new AsyncApiSchema() { Type = SchemaType.String }); mainSchema.Not = subSchema; var yaml = mainSchema.Serialize(AsyncApiVersion.AsyncApi2_0, AsyncApiFormat.Yaml); diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiSecurityRequirement_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiSecurityRequirement_Should.cs index f8872233..b92224df 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiSecurityRequirement_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiSecurityRequirement_Should.cs @@ -1,9 +1,12 @@ -using System; -using LEGO.AsyncAPI.Models; -using NUnit.Framework; +// Copyright (c) The LEGO Group. All rights reserved. namespace LEGO.AsyncAPI.Tests.Models { + using System; + using System.Collections.Generic; + using LEGO.AsyncAPI.Models; + using NUnit.Framework; + public class AsyncApiSecurityRequirement_Should { [Test] @@ -16,5 +19,14 @@ public void SerializeV2_WithNullWriter_Throws() // Assert Assert.Throws(() => { asyncApiSecurityRequirement.SerializeV2(null); }); } + + [Test] + public void SerializeV2_Serializes() + { + var asyncApiSecurityRequirement = new AsyncApiSecurityRequirement(); + asyncApiSecurityRequirement.Add(new AsyncApiSecurityScheme { Type = SecuritySchemeType.ApiKey }, new List { "string" }); + + var output = asyncApiSecurityRequirement.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + } } } diff --git a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiServer_Should.cs b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiServer_Should.cs index 52010434..8b14560b 100644 --- a/test/LEGO.AsyncAPI.Tests/Models/AsyncApiServer_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Models/AsyncApiServer_Should.cs @@ -1,35 +1,40 @@ -using LEGO.AsyncAPI.Models; -using LEGO.AsyncAPI.Models.Bindings.Kafka; -using LEGO.AsyncAPI.Models.Interfaces; -using NUnit.Framework; -using System.Collections.Generic; +// Copyright (c) The LEGO Group. All rights reserved. namespace LEGO.AsyncAPI.Tests.Models { - internal class AsyncApiServer_Should + using System.Collections.Generic; + using FluentAssertions; + using LEGO.AsyncAPI.Bindings.Kafka; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Models.Interfaces; + using NUnit.Framework; + + internal class AsyncApiServer_Should : TestBase { [Test] public void AsyncApiServer_Serializes() { // Arrange var expected = -@"url: 'https://example.com/{channelkey}' -protocol: test -protocolVersion: 0.1.0 -description: some description -variables: - channelkey: - description: some description -security: - - schem1: - - requirement -tags: - - name: mytag1 - description: description of tag1 -bindings: - kafka: - schemaRegistryUrl: http://example.com - schemaRegistryVendor: kafka"; + """ + url: 'https://example.com/{channelkey}' + protocol: test + protocolVersion: 0.1.0 + description: some description + variables: + channelkey: + description: some description + security: + - schem1: + - requirement + tags: + - name: mytag1 + description: description of tag1 + bindings: + kafka: + schemaRegistryUrl: http://example.com + schemaRegistryVendor: kafka + """; var server = new AsyncApiServer { @@ -57,7 +62,7 @@ public void AsyncApiServer_Serializes() } }, }); - server.Tags.Add(new AsyncApiTag { Name = "mytag1", Description ="description of tag1" }); + server.Tags.Add(new AsyncApiTag { Name = "mytag1", Description = "description of tag1" }); server.Bindings.Add(new KafkaServerBinding { SchemaRegistryUrl = "http://example.com", @@ -66,24 +71,24 @@ public void AsyncApiServer_Serializes() // Act var actual = server.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); - - // Assert - actual = actual.MakeLineBreaksEnvironmentNeutral(); - expected = expected.MakeLineBreaksEnvironmentNeutral(); - Assert.AreEqual(actual, expected); + // Assert + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); } [Test] public void AsyncApiServer_WithKafkaBinding_Serializes() { var expected = -@"url: -protocol: -bindings: - kafka: - schemaRegistryUrl: http://example.com - schemaRegistryVendor: kafka"; + """ + url: + protocol: + bindings: + kafka: + schemaRegistryUrl: http://example.com + schemaRegistryVendor: kafka + """; var server = new AsyncApiServer { Bindings = new AsyncApiBindings @@ -100,11 +105,9 @@ public void AsyncApiServer_WithKafkaBinding_Serializes() var actual = server.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); - actual = actual.MakeLineBreaksEnvironmentNeutral(); - expected = expected.MakeLineBreaksEnvironmentNeutral(); - // Assert - Assert.AreEqual(actual, expected); + actual.Should() + .BePlatformAgnosticEquivalentTo(expected); } } } diff --git a/test/LEGO.AsyncAPI.Tests/Serialization/AsyncApiYamlWriterTests.cs b/test/LEGO.AsyncAPI.Tests/Serialization/AsyncApiYamlWriterTests.cs new file mode 100644 index 00000000..c364d345 --- /dev/null +++ b/test/LEGO.AsyncAPI.Tests/Serialization/AsyncApiYamlWriterTests.cs @@ -0,0 +1,153 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Tests.Writers +{ + using LEGO.AsyncAPI.Writers; + using NUnit.Framework; + using System; + using System.IO; + + internal class AsyncApiYamlWriterTests : TestBase + { + [Test] + public void Write_NullValue_ReturnsNull() + => this.Compose(null, "null"); + + [Test] + public void Write_EmptyValue_ReturnsNull() + => this.Compose(string.Empty, "''"); + + [Test] + public void Write_NullWordString_ReturnsWrappedValue() + => this.Compose("null", "'null'"); + + [Test] + public void Write_TildaWordString_ReturnsWrappedValue() + => this.Compose("~", "'~'"); + + [Test] + public void Write_IntegerWithTwoPeriods_RendersPlainStyle() + => this.Compose("1.2.3", "1.2.3"); + + [Test] + public void Write_Float_WrappedWithQuotes() + => this.Compose("1.2", "'1.2'"); + + [Test] + public void Write_PositiveFloat_WrappedWithQuotes() + => this.Compose("+1.2", "'+1.2'"); + + [Test] + public void Write_NegativeFloat_WrappedWithQuotes() + => this.Compose("-1.2", "'-1.2'"); + + [Test] + public void Write_PositiveInfinityFloat_WrappedWithQuotes() + => this.Compose(".inf", "'.inf'"); + + [Test] + public void Write_NegativeInfinityFloat_WrappedWithQuotes() + => this.Compose("-.inf", "'-.inf'"); + + [Test] + public void Write_NanFloat_WrappedWithQuotes() + => this.Compose(".nan", "'.nan'"); + + [Test] + public void Write_TrueString_WrappedWithQuotes() + => this.Compose("true", "'true'"); + + [Test] + public void Write_FalseString_WrappedWithQuotes() + => this.Compose("false", "'false'"); + + [Test] + public void Write_DateTimeSlashString_NotWrappedWithQuotes() + => this.Compose("12/31/2022 23:59:59", "12/31/2022 23:59:59"); + + [Test] + public void Write_DateTimeDashString_NotWrappedWithQuotes() + => this.Compose("2022-12-31 23:59:59", "2022-12-31 23:59:59"); + + [Test] + public void Write_DateTimeISOString_NotWrappedWithQuotes() + => this.Compose("2022-12-31T23:59:59Z", "2022-12-31T23:59:59Z"); + + [Test] + public void Write_DateTimeCanonicalString_NotWrappedWithQuotes() + => this.Compose("2001-12-15T02:59:43.1Z", "2001-12-15T02:59:43.1Z"); + + [Test] + public void Write_DateTimeSpacedString_NotWrappedWithQuotes() + => this.Compose("2001-12-14 21:59:43.10 -5", "2001-12-14 21:59:43.10 -5"); + + [Test] + public void Write_DateString_NotWrappedWithQuotes() + => this.Compose("2002-12-14", "2002-12-14"); + + [Test] + [TestCase("\0", "\"\\0\"")] + [TestCase("\x01", "\"\\x01\"")] + [TestCase("\x02", "\"\\x02\"")] + [TestCase("\x03", "\"\\x03\"")] + [TestCase("\x04", "\"\\x04\"")] + [TestCase("\x05", "\"\\x05\"")] + [TestCase("\x06", "\"\\x06\"")] + [TestCase("\a", "\"\\a\"")] + [TestCase("\b", "\"\\b\"")] + [TestCase("\t", "\"\\t\"")] + [TestCase("\n", "\"\\n\"")] + [TestCase("\v", "\"\\v\"")] + [TestCase("\f", "\"\\f\"")] + [TestCase("\r", "\"\\r\"")] + [TestCase("\x0e", "\"\\x0e\"")] + [TestCase("\x0f", "\"\\x0f\"")] + [TestCase("\x10", "\"\\x10\"")] + [TestCase("\x11", "\"\\x11\"")] + [TestCase("\x12", "\"\\x12\"")] + [TestCase("\x13", "\"\\x13\"")] + [TestCase("\x14", "\"\\x14\"")] + [TestCase("\x15", "\"\\x15\"")] + [TestCase("\x16", "\"\\x16\"")] + [TestCase("\x17", "\"\\x17\"")] + [TestCase("\x18", "\"\\x18\"")] + [TestCase("\x19", "\"\\x19\"")] + [TestCase("\x1a", "\"\\x1a\"")] + [TestCase("\x1b", "\"\\x1b\"")] + [TestCase("\x1c", "\"\\x1c\"")] + [TestCase("\x1d", "\"\\x1d\"")] + [TestCase("\x1e", "\"\\x1e\"")] + [TestCase("\x1f", "\"\\x1f\"")] + public void Write_ControlCharacters_AreEscaped(string input, string expected) + => this.Compose(input, expected); + + private void Compose( + string? input, + string expected) + { + // It's a property + expected = $"Value: {expected}"; + + using (MemoryStream stream = new MemoryStream()) + using (StreamWriter writer = new StreamWriter(stream)) + { + AsyncApiWriterSettings settings = new AsyncApiWriterSettings(); + AsyncApiYamlWriter yamlWriter = new AsyncApiYamlWriter(writer, settings); + yamlWriter.WriteStartObject(); + yamlWriter.WritePropertyName("Value"); + yamlWriter.WriteValue(input); + yamlWriter.WriteEndObject(); + yamlWriter.Flush(); + stream.Position = 0; + + using (StreamReader reader = new StreamReader(stream)) + { + string actual = reader.ReadToEnd(); + this.Log($"Expected: <{expected}>"); + this.Log($"Actual: <{actual}>"); + Assert.AreEqual(expected, actual); + } + } + } + } +} diff --git a/test/LEGO.AsyncAPI.Tests/StringExtensions.cs b/test/LEGO.AsyncAPI.Tests/StringExtensions.cs deleted file mode 100644 index 6a3c89fd..00000000 --- a/test/LEGO.AsyncAPI.Tests/StringExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace LEGO.AsyncAPI.Tests -{ - using System; - - public static class StringExtensions - { - public static string MakeLineBreaksEnvironmentNeutral(this string input) - { - return input.Replace("\r\n", "\n") - .Replace("\r", "\n") - .Replace("\n", Environment.NewLine); - } - } -} diff --git a/test/LEGO.AsyncAPI.Tests/TestBase.cs b/test/LEGO.AsyncAPI.Tests/TestBase.cs new file mode 100644 index 00000000..17309617 --- /dev/null +++ b/test/LEGO.AsyncAPI.Tests/TestBase.cs @@ -0,0 +1,81 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Tests +{ + using System; + using System.Diagnostics; + using System.IO; + using System.Linq; + using System.Runtime.CompilerServices; + using NUnit.Framework; + + /// + /// Base class for unit tests across the project. Can contain + /// helper methods for working with unit tests. + /// + public abstract class TestBase + { + /// + /// Initializes a new instance of the class. + /// + protected TestBase() + { + this.TestContext = TestContext.CurrentContext; + } + + /// + /// Gets the current context of the running text. + /// + protected TestContext TestContext { get; } + + /// + /// Writes information to the console which will only be + /// printed when running in debug mode. + /// + /// The message to print. + [Conditional("DEBUG")] + public void Log(string message) + { + TestContext.WriteLine(message); + } + + /// + /// Attempts to find the first file that matches the name of the active unit test + /// and returns it as an expected type. + /// + /// The type to return + /// The name of the resource file with an optional extension. + /// The result + protected T GetTestData([CallerMemberName] string resourceName = "") + { + string searchPattern = string.IsNullOrWhiteSpace(Path.GetExtension(resourceName)) + ? $"{resourceName}.*" + : resourceName; + + string testDataDirectory = Path.Combine(Environment.CurrentDirectory, "TestData"); + + string? testDataPath = Directory.GetFiles(testDataDirectory, searchPattern) + .FirstOrDefault(); + + Assume.That(File.Exists(testDataPath), $"No test data file named '{resourceName}' exists in directory '{testDataDirectory}'"); + + object? result = null; + Type resultType = typeof(T); + + if (typeof(string) == resultType) + { + result = File.ReadAllText(testDataPath); + } + else if (typeof(string[]) == resultType) + { + result = File.ReadAllLines(testDataPath); + } + else + { + throw new NotImplementedException($"No case has been defined to convering a resource into '{resultType.FullName}'. You can add a new one."); + } + + return (T)result!; + } + } +} diff --git a/test/LEGO.AsyncAPI.Tests/TestData/AsyncApiSchema_InlinedReferences.yml b/test/LEGO.AsyncAPI.Tests/TestData/AsyncApiSchema_InlinedReferences.yml new file mode 100644 index 00000000..54f78b6d --- /dev/null +++ b/test/LEGO.AsyncAPI.Tests/TestData/AsyncApiSchema_InlinedReferences.yml @@ -0,0 +1,27 @@ +asyncapi: 2.6.0 +info: + title: Streetlights Kafka API + version: 1.0.0 + description: The Smartylighting Streetlights API allows you to remotely manage the city lights. + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 +channels: + mychannel: + publish: + message: + payload: + type: object + required: + - testB + properties: + testC: + type: object + properties: + testD: + type: string + format: uuid + testB: + type: boolean + description: test +components: { } \ No newline at end of file diff --git a/test/LEGO.AsyncAPI.Tests/TestData/AsyncApiSchema_NoInlinedReferences.yml b/test/LEGO.AsyncAPI.Tests/TestData/AsyncApiSchema_NoInlinedReferences.yml new file mode 100644 index 00000000..8308baf0 --- /dev/null +++ b/test/LEGO.AsyncAPI.Tests/TestData/AsyncApiSchema_NoInlinedReferences.yml @@ -0,0 +1,34 @@ +asyncapi: 2.6.0 +info: + title: Streetlights Kafka API + version: 1.0.0 + description: The Smartylighting Streetlights API allows you to remotely manage the city lights. + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 +channels: + mychannel: + publish: + message: + payload: + type: object + required: + - testB + properties: + testC: + $ref: '#/components/schemas/testC' + testB: + $ref: '#/components/schemas/testB' +components: + schemas: + testD: + type: string + format: uuid + testC: + type: object + properties: + testD: + $ref: '#/components/schemas/testD' + testB: + type: boolean + description: test \ No newline at end of file diff --git a/test/LEGO.AsyncAPI.Tests/TestData/Deserialize_WithAdvancedSchema_Works.json b/test/LEGO.AsyncAPI.Tests/TestData/Deserialize_WithAdvancedSchema_Works.json new file mode 100644 index 00000000..f6aa8ca2 --- /dev/null +++ b/test/LEGO.AsyncAPI.Tests/TestData/Deserialize_WithAdvancedSchema_Works.json @@ -0,0 +1,82 @@ +{ + "title": "title1", + "properties": { + "property1": { + "items": false, + "additionalItems": false, + "properties": { + "property2": { + "type": "integer" + }, + "property3": { + "type": "string", + "maxLength": 15 + } + }, + "additionalProperties": false + }, + "property4": { + "items": { + "properties": { + "Property9": { + "type": [ + "null", + "string" + ] + } + } + }, + "additionalItems": { + "properties": { + "Property10": { + "type": [ + "null", + "string" + ] + } + } + }, + "properties": { + "property5": { + "properties": { + "property6": { + "type": "boolean" + } + } + }, + "property7": { + "type": "string", + "minLength": 2 + } + }, + "additionalProperties": { + "properties": { + "Property8": { + "type": [ + "null", + "string" + ] + } + } + }, + "patternProperties": { + "^S_": { + "type": "string" + }, + "^I_": { + "type": "integer" + } + }, + "propertyNames": { + "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" + } + }, + "property11": { + "const": "aSpecialConstant" + } + }, + "nullable": true, + "externalDocs": { + "url": "http://example.com/externalDocs" + } +} \ No newline at end of file diff --git a/test/LEGO.AsyncAPI.Tests/TestData/SerializeAsJson_WithAdvancedSchemaObject_V2Works.json b/test/LEGO.AsyncAPI.Tests/TestData/SerializeAsJson_WithAdvancedSchemaObject_V2Works.json new file mode 100644 index 00000000..7c7e7c17 --- /dev/null +++ b/test/LEGO.AsyncAPI.Tests/TestData/SerializeAsJson_WithAdvancedSchemaObject_V2Works.json @@ -0,0 +1,82 @@ +{ + "title": "title1", + "properties": { + "property1": { + "items": false, + "additionalItems": false, + "properties": { + "property2": { + "type": "integer" + }, + "property3": { + "type": "string", + "maxLength": 15 + } + }, + "additionalProperties": false + }, + "property4": { + "items": { + "properties": { + "Property9": { + "type": [ + "null", + "string" + ] + } + } + }, + "additionalItems": { + "properties": { + "Property10": { + "type": [ + "null", + "string" + ] + } + } + }, + "properties": { + "property5": { + "properties": { + "property6": { + "type": "boolean" + } + } + }, + "property7": { + "type": "string", + "minLength": 2 + } + }, + "additionalProperties": { + "properties": { + "Property8": { + "type": [ + "null", + "string" + ] + } + } + }, + "patternProperties": { + "^S_": { + "type": "string" + }, + "^I_": { + "type": "integer" + } + }, + "propertyNames": { + "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" + } + }, + "property11": { + "const": "aSpecialConstant" + } + }, + "nullable": true, + "externalDocs": { + "url": "http://example.com/externalDocs" + } +} diff --git a/test/LEGO.AsyncAPI.Tests/TestData/SerializeAsJson_WithAdvancedSchemaWithAllOf_V2Works.json b/test/LEGO.AsyncAPI.Tests/TestData/SerializeAsJson_WithAdvancedSchemaWithAllOf_V2Works.json new file mode 100644 index 00000000..b0539186 --- /dev/null +++ b/test/LEGO.AsyncAPI.Tests/TestData/SerializeAsJson_WithAdvancedSchemaWithAllOf_V2Works.json @@ -0,0 +1,38 @@ +{ + "title": "title1", + "allOf": [ + { + "title": "title2", + "properties": { + "property1": { + "type": "integer" + }, + "property2": { + "type": "string", + "maxLength": 15 + } + } + }, + { + "title": "title3", + "properties": { + "property3": { + "properties": { + "property4": { + "type": "boolean" + } + } + }, + "property5": { + "type": "string", + "minLength": 2 + } + }, + "nullable": true + } + ], + "nullable": true, + "externalDocs": { + "url": "http://example.com/externalDocs" + } +} \ No newline at end of file diff --git a/test/LEGO.AsyncAPI.Tests/Validation/ValidationRulesetTests.cs b/test/LEGO.AsyncAPI.Tests/Validation/ValidationRulesetTests.cs index 70929de1..48446738 100644 --- a/test/LEGO.AsyncAPI.Tests/Validation/ValidationRulesetTests.cs +++ b/test/LEGO.AsyncAPI.Tests/Validation/ValidationRulesetTests.cs @@ -1,8 +1,10 @@ -using LEGO.AsyncAPI.Validations; -using NUnit.Framework; +// Copyright (c) The LEGO Group. All rights reserved. namespace LEGO.AsyncAPI.Tests.Validation { + using LEGO.AsyncAPI.Validations; + using NUnit.Framework; + public class ValidationRuleSetTests { [Test] diff --git a/test/LEGO.AsyncAPI.Tests/stylecop.json b/test/LEGO.AsyncAPI.Tests/stylecop.json new file mode 100644 index 00000000..0a8f4661 --- /dev/null +++ b/test/LEGO.AsyncAPI.Tests/stylecop.json @@ -0,0 +1,15 @@ +{ + // ACTION REQUIRED: This file was automatically added to your project, but it + // will not take effect until additional steps are taken to enable it. See the + // following page for additional information: + // + // https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md + + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "The LEGO Group", + "xmlHeader": false + } + } +}