diff --git a/.asyncapi-tool b/.asyncapi-tool index 4f80ea8a..5207f552 100644 --- a/.asyncapi-tool +++ b/.asyncapi-tool @@ -1,6 +1,6 @@ { "title": "AsyncAPI.NET", - "description": "The officially maintained fork of the AsyncAPI.NET SDK contains a useful object model for AsyncAPI documents in .NET along with common (de)serializers to extract raw AsyncApi JSON and YAML documents from the model.", + "description": "The official continuation of LEGO.AsyncAPI.NET from the original author and maintainer. The SDK contains a useful object model for AsyncAPI 3.0 documents in .NET. Full JsonSchema and Avro support", "links": { "websiteUrl": "https://github.com/ByteBardOrg/AsyncAPI.NET/", "repoUrl": "https://github.com/ByteBardOrg/AsyncAPI.NET" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b00e7db..5757f776 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,24 @@ on: - '!**/*.md' workflow_dispatch: jobs: + obfuscan: + if: github.event_name == 'pull_request' + name: Obfuscan + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + issues: write + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: Scan PR diff + uses: ByteBardOrg/obfuscan-action@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + fail-on: block + build: runs-on: ${{ matrix.os }} @@ -23,12 +41,14 @@ jobs: steps: - uses: actions/checkout@v2 - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.0.x' - include-prerelease: true + dotnet-version: | + 8.0.x + 9.0.x + 10.0.x - name: Restore dependencies - run: dotnet restore + run: dotnet restore - name: Build run: dotnet build --no-restore - name: Test diff --git a/.github/workflows/pr-title-lint.yml b/.github/workflows/pr-title-lint.yml index abe0796b..4858b426 100644 --- a/.github/workflows/pr-title-lint.yml +++ b/.github/workflows/pr-title-lint.yml @@ -4,6 +4,10 @@ on: pull_request_target: types: [opened, reopened, synchronize, edited, ready_for_review] +permissions: + contents: read + pull-requests: write + jobs: lint-pr-title: name: Lint PR title diff --git a/.github/workflows/release-beta.yml b/.github/workflows/release-beta.yml index aed27546..ae2b2ccb 100644 --- a/.github/workflows/release-beta.yml +++ b/.github/workflows/release-beta.yml @@ -38,8 +38,13 @@ jobs: - name: Checkout repository uses: actions/checkout@v1 - - name: Setup .NET Core @ Latest - uses: actions/setup-dotnet@v1 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.x + 9.0.x + 10.0.x - run: echo ${{env.GITHUB_RUN_NUMBER_WITH_OFFSET}} - name: Build ${{ matrix.package-name }} project and pack NuGet package diff --git a/.github/workflows/release-package.yml b/.github/workflows/release-package.yml index 48484f33..6e087253 100644 --- a/.github/workflows/release-package.yml +++ b/.github/workflows/release-package.yml @@ -13,42 +13,27 @@ jobs: name: Check release steps: - name: Checkout repository - uses: actions/checkout@v1 + uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Get release token + id: get-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_SECRET }} - name: Semantic Release uses: cycjimmy/semantic-release-action@v3 id: semantic with: extra_plugins: | - conventional-changelog-conventionalcommits - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Get token - if: steps.semantic.outputs.new_release_published == 'true' - id: get_token - uses: actions/create-github-app-token@v1 - with: - app-id: ${{ secrets.APP_ID }} - private-key: ${{ secrets.APP_SECRET }} - - - name: Commit changes - if: steps.semantic.outputs.new_release_published == 'true' + @semantic-release/changelog@6.0.3 + @semantic-release/git@10.0.1 env: - GITHUB_TOKEN: ${{ steps.get_token.outputs.token }} - FILE_TO_COMMIT: CHANGELOG.md - DESTINATION_BRANCH: ${{ github.ref }} - run: | - cat CHANGELOG.md - # export MESSAGE="chore: update $FILE_TO_COMMIT" - # export SHA=$( git rev-parse $DESTINATION_BRANCH:$FILE_TO_COMMIT ) - # export CONTENT=$( base64 -i $FILE_TO_COMMIT ) - # gh api --method PUT /repos/:owner/:repo/contents/$FILE_TO_COMMIT \ - # --field message="$MESSAGE" \ - # --field content="$CONTENT" \ - # --field encoding="base64" \ - # --field branch="$DESTINATION_BRANCH" \ - # --field sha="$SHA" + GITHUB_TOKEN: ${{ steps.get-token.outputs.token }} outputs: trigger_release: ${{ steps.semantic.outputs.new_release_published }} @@ -66,9 +51,14 @@ jobs: - name: Checkout repository uses: actions/checkout@v1 - - name: Setup .NET Core @ Latest + - name: Setup .NET if: needs.prereleaseCheck.outputs.trigger_release == 'true' - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.x + 9.0.x + 10.0.x - name: Build ${{ matrix.package-name }} project and pack NuGet package if: needs.prereleaseCheck.outputs.trigger_release == 'true' diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cd77092..ab561d65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,38 +1,170 @@ -## [2.1.1](https://github.com/ByteBardOrg/AsyncAPI.NET/compare/v2.1.0...v2.1.1) (2025-08-28) -### Bug Fixes -* discriminator constant for writing specs. ([b9657e0](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/b9657e0e8c0ad1e82d54c40cec1f34d50402e5e1)) -* large number parsing and add non-negative validation. ([cf9f212](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/cf9f212e9e34c21eabfb2e5ada3949255950c205)) -# [2.1.0](https://github.com/ByteBardOrg/AsyncAPI.NET/compare/v2.0.1...v2.1.0) (2025-08-08) +# Changelog + +Package versions follow this library's semantic versioning and do not correspond directly to AsyncAPI specification versions. For example, package 2.0.0 introduced the AsyncAPI 3.0 object model, while package 3.0.0 later moved V3 document output to AsyncAPI 3.1.0. + +## [3.0.1](https://github.com/ByteBardOrg/AsyncAPI.NET/compare/v3.0.0...v3.0.1) (2026-08-02) + ### Bug Fixes -* parameter reference resolution during V3 upgrade ([#15](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/15)) ([ccd6ff3](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/ccd6ff3a1e5359512ab12b9a8d1375049839571b)) -* set AMQP binding properties required ([#17](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/17)) ([34e2733](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/34e273346b3e09c1fdacbdbc77254740e1b14e29)) -### Features -* custom Schema Parser support ([#13](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/13)) ([d852d02](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/d852d025a6fae626a1e2e008b77a2a479eaae7df)) -## [2.0.1](https://github.com/ByteBardOrg/AsyncAPI.NET/compare/v2.0.0...v2.0.1) (2025-05-31) + +* corrected security-scheme validation so `name` is required for `HttpApiKey` rather than `ApiKey` schemes ([#36](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/36)) ([0496eb5](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/0496eb5549abf3095e960ed870fa25eb71f463c6)) + +## [3.0.0](https://github.com/ByteBardOrg/AsyncAPI.NET/compare/v2.1.2...v3.0.0) (2026-06-10) + +Package 3.0.0 builds on the shared AsyncAPI 2.6/3.0 object model introduced in package 2.0.0. This release moves V3 output to AsyncAPI 3.1.0 and introduces breaking binding and Avro API changes. + +### AsyncAPI 3.1 + +* V3 documents now serialize and normalize to `asyncapi: 3.1.0`; the existing `AsyncApiVersion.AsyncApi3_0` selector continues to represent the V3 specification family ([1985b38](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/1985b385949ef2b4b7195f72f7534415f210d8b7)) +* referenced schemas, servers, channels, messages, parameters, traits, security schemes, and binding components are now deserialized using the containing document's AsyncAPI version instead of always using V2 rules ([eeb9d8f](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/eeb9d8f8aedd811b2fc4c32c692868ff47a00824)) +* root servers are registered under `#/servers/{name}` so channel server references and root-server aliases resolve consistently through `AsyncApiWorkspace` ([eeb9d8f](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/eeb9d8f8aedd811b2fc4c32c692868ff47a00824)) +* V2 channel server entries now accept both bare server names and canonical `#/servers/...` references without double-prefixing the path ([eeb9d8f](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/eeb9d8f8aedd811b2fc4c32c692868ff47a00824)) + +### Binding Serialization + +* binding collections now dispatch to the requested `SerializeV2` or `SerializeV3` implementation instead of silently using the V2 representation for both versions ([#27](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/27)) ([7cea38b](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/7cea38beda2ff5340020ca3c9262268c723a065f)) +* V3 channels now serialize external documentation and bindings through their V3 serializers ([#27](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/27)) ([7cea38b](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/7cea38beda2ff5340020ca3c9262268c723a065f)) +* added parent serialization context to `AsyncApiWorkspace`, allowing bindings to inspect their containing channel, operation, or message ([#27](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/27)) ([7cea38b](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/7cea38beda2ff5340020ca3c9262268c723a065f)) +* HTTP operation bindings now infer V2 `type: request` or `type: response` from `AsyncApiOperation.Action`, omit `type` in V3, and use the matching V2 or V3 query-schema serializer ([#27](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/27)) ([7cea38b](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/7cea38beda2ff5340020ca3c9262268c723a065f)) +* added `HttpMessageBinding.StatusCode`, emitted numerically for V3 and omitted when targeting V2 ([#27](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/27)) ([7cea38b](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/7cea38beda2ff5340020ca3c9262268c723a065f)) +* HTTP bindings now default to binding version `0.2.0` for AsyncAPI V2 and `0.3.0` for V3 ([#27](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/27)) ([7cea38b](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/7cea38beda2ff5340020ca3c9262268c723a065f)) +* AMQP, Kafka, MQTT, Pulsar, SNS, SQS, and WebSockets bindings now expose explicit V2 and V3 serialization paths while retaining their existing common representation ([#27](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/27)) ([7cea38b](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/7cea38beda2ff5340020ca3c9262268c723a065f)) + +### Avro Schemas + +* added `AvroNamedType` for Avro name references, kept distinct from AsyncAPI `$ref` references ([#32](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/32)) ([310868a](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/310868a6e6cb9d0a4feba3ba0b473c031e1d494e)) +* added namespace-aware resolution of previously declared records, enums, fixed schemas, and named duration schemas ([#32](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/32)) ([310868a](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/310868a6e6cb9d0a4feba3ba0b473c031e1d494e)) +* added support for self-recursive Avro records and named references within record fields, arrays, maps, and unions ([#32](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/32)) ([310868a](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/310868a6e6cb9d0a4feba3ba0b473c031e1d494e)) +* expanded Avro maps to accept any `AsyncApiAvroSchema` as their value schema, including named and complex types ([#32](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/32)) ([310868a](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/310868a6e6cb9d0a4feba3ba0b473c031e1d494e)) +* added recursive, cycle-safe Avro traversal and warnings for named types that cannot be resolved ([#32](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/32)) ([310868a](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/310868a6e6cb9d0a4feba3ba0b473c031e1d494e)) +* added an explicit conversion from primitive `AsyncApiAvroSchema` values back to `AvroPrimitiveType` ([#32](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/32)) ([310868a](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/310868a6e6cb9d0a4feba3ba0b473c031e1d494e)) + +### Validation + +* added `ValidationRule.ApplicableVersions`, `AsyncApiVersionRuleAttribute`, and version-filtered rule lookup so V2 and V3 rules run only against their intended document family ([#25](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/25)) ([5e4ed51](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/5e4ed51045b97f595d2e4d6287406b227763d43d)) +* stopped applying V3-only operation action, channel-reference, and message-subset rules to operations upgraded from V2 documents ([#25](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/25)) ([5e4ed51](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/5e4ed51045b97f595d2e4d6287406b227763d43d)) +* added V2-specific validation requiring a non-empty Channels Object while allowing V3 documents to omit channels ([#25](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/25)) ([5e4ed51](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/5e4ed51045b97f595d2e4d6287406b227763d43d)) +* fragment validation warnings are now reported through `AsyncApiDiagnostic.Warnings` instead of being promoted to errors ([#32](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/32)) ([310868a](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/310868a6e6cb9d0a4feba3ba0b473c031e1d494e)) + +### Framework Support + +* added `net9.0` and `net10.0` package targets while retaining `netstandard2.0`, `netstandard2.1`, and `net8.0` ([#33](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/33)) ([723a99d](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/723a99dc487fe39708803c394facd948899a8f68)) + ### Bug Fixes -* readd netstandard target to ensure source generator compat ([b28d6ff](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/b28d6ff2a3c7b9df13d2ed9bab88e38593280ce6)) -# [2.0.0](https://github.com/ByteBardOrg/AsyncAPI.NET/compare/v1.0.0...v2.0.0) (2025-05-25) -### Features -* feat!: full v3 support (#8) ([2b98f81](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/2b98f81c4adbd61f8b981bc2247e7f008f598310)), closes [#8](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/8) + +* corrected variable handling, nesting, and reference-name behavior during document deserialization ([#29](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/29)) ([eeb9d8f](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/eeb9d8f8aedd811b2fc4c32c692868ff47a00824)) + ### BREAKING CHANGES -* All models have changed to reflect v3.0 -### Bug Fixes -* message inference ([#10](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/10)) ([c201b13](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/c201b13a70f37d853aa063f74ff8a89faf2bf27f)) -# 1.0.0 (2025-03-28) -### Features -* AsyncAPI v2.6 full support ([f0ef397](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/f0ef397944bfbc124c16b7769c2512fcf17d5fb9)) -* + +* custom `AsyncApiBinding` implementations must now override both `SerializeV2` and `SerializeV3`; `SerializeProperties` alone is no longer sufficient. +* `HttpOperationBinding.Type` was removed. Set the containing `AsyncApiOperation.Action`; V2 output derives `request` or `response` from that action. +* **avro:** `AvroMap.Values` now uses `AsyncApiAvroSchema` instead of `AvroPrimitiveType`. + +## [2.1.2](https://github.com/ByteBardOrg/AsyncAPI.NET/compare/v2.1.1...v2.1.2) (2025-12-21) + +### Reference Handling + +* added base-URI-aware loading for relative external references in readers and stream loaders ([#21](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/21)) ([07d912d](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/07d912dbfe3a5a2500f066477387b278e4cca824)) +* added `AsyncApiReaderSettings.BaseUri` for resolving relative references ([#21](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/21)) ([07d912d](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/07d912dbfe3a5a2500f066477387b278e4cca824)) +* made `AsyncApiWalker` safely traverse operations whose optional reply address or channel reference is absent ([#23](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/23)) ([7085d23](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/7085d23f7441fe87645562192b0efe3c8f85c9f1)) + +### BREAKING CHANGES + +* custom `IStreamLoader` implementations must accept both the base URI and requested URI in `Load(Uri baseUri, Uri uri)` and `LoadAsync(Uri baseUri, Uri uri)`. + +## [2.1.1](https://github.com/ByteBardOrg/AsyncAPI.NET/compare/v2.1.0...v2.1.1) (2025-08-28) + +### Serialization and Validation + +* corrected discriminator serialization to use the expected constant name ([b9657e0](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/b9657e0e8c0ad1e82d54c40cec1f34d50402e5e1)) +* converted out-of-range integer constraints in JSON Schema into reader diagnostics instead of unhandled parsing failures, and corrected non-negative numeric validation ([cf9f212](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/cf9f212e9e34c21eabfb2e5ada3949255950c205)) + +## [2.1.0](https://github.com/ByteBardOrg/AsyncAPI.NET/compare/v2.0.1...v2.1.0) (2025-08-08) + +### AsyncAPI 3 Improvements + +* added pluggable schema parsers for custom multi-format schema payloads ([#13](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/13)) ([d852d02](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/d852d025a6fae626a1e2e008b77a2a479eaae7df)) +* fixed parameter reference resolution while upgrading V2 documents into the shared V3-shaped model ([#15](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/15)) ([ccd6ff3](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/ccd6ff3a1e5359512ab12b9a8d1375049839571b)) +* corrected required AMQP binding properties in the shared V2/V3 binding model ([#17](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/17)) ([34e2733](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/34e273346b3e09c1fdacbdbc77254740e1b14e29)) + +### BREAKING CHANGES + +* `AMQPOperationBinding.Expiration`, `Priority`, `DeliveryMode`, `Mandatory`, `Timestamp`, and `Ack` are now non-nullable and serialize as required properties; `UserId` is also emitted as required. +* `AsyncApiSchemaDeserializer` was renamed to `AsyncApiJsonSchemaDeserializer` as part of the pluggable schema-parser API. + ## [2.0.1](https://github.com/ByteBardOrg/AsyncAPI.NET/compare/v2.0.0...v2.0.1) (2025-05-31) + ### Bug Fixes -* readd netstandard target to ensure source generator compat ([b28d6ff](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/b28d6ff2a3c7b9df13d2ed9bab88e38593280ce6)) -# [2.0.0](https://github.com/ByteBardOrg/AsyncAPI.NET/compare/v1.0.0...v2.0.0) (2025-05-25) -### Features -* feat!: full v3 support (#8) ([2b98f81](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/2b98f81c4adbd61f8b981bc2247e7f008f598310)), closes [#8](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/8) +* re-add the .NET Standard target to ensure source generator compatibility ([b28d6ff](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/b28d6ff2a3c7b9df13d2ed9bab88e38593280ce6)) + +## [2.0.0](https://github.com/ByteBardOrg/AsyncAPI.NET/compare/v1.0.0...v2.0.0) (2025-05-25) + +Package 2.0.0 replaced the V2-shaped public API with a shared AsyncAPI 3.0-shaped object model. The same model can read AsyncAPI 2.x or 3.x documents and serialize back to either AsyncAPI 2.6 or 3.0. This package major version is independent of the AsyncAPI specification version. + +### AsyncAPI 3.0 Object Model + +* added native AsyncAPI 3.0 readers, writers, validation, and `AsyncApiVersion.AsyncApi3_0` selection ([#8](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/8)) ([2b98f81](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/2b98f81c4adbd61f8b981bc2247e7f008f598310)) +* added root `AsyncApiDocument.Operations`; operations now use `AsyncApiOperation.Action` and an `AsyncApiChannelReference` instead of living under each channel as `Publish` and `Subscribe` ([#8](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/8)) ([2b98f81](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/2b98f81c4adbd61f8b981bc2247e7f008f598310)) +* separated channel identity from its address through `AsyncApiChannel.Address` and added channel-level message maps, titles, summaries, tags, and external documentation ([#8](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/8)) ([2b98f81](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/2b98f81c4adbd61f8b981bc2247e7f008f598310)) +* changed operations to reference a subset of their channel's messages through `IList` and added operation titles and request/reply support ([#8](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/8)) ([2b98f81](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/2b98f81c4adbd61f8b981bc2247e7f008f598310)) +* added `AsyncApiOperationReply`, `AsyncApiOperationReplyAddress`, their typed references, and reusable reply and reply-address components ([#8](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/8)) ([2b98f81](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/2b98f81c4adbd61f8b981bc2247e7f008f598310)) +* moved root tags and external documentation to `AsyncApiInfo`, matching their AsyncAPI 3 placement ([#8](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/8)) ([2b98f81](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/2b98f81c4adbd61f8b981bc2247e7f008f598310)) + +### Messages and Schemas + +* added `AsyncApiMultiFormatSchema` and `IAsyncApiSchema` so message payloads, headers, and reusable schemas can represent JSON Schema, Avro, and custom schema formats through one model ([#8](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/8)) ([2b98f81](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/2b98f81c4adbd61f8b981bc2247e7f008f598310)) +* moved message `schemaFormat` into the payload's multi-format schema wrapper and changed message headers to use the same abstraction ([#8](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/8)) ([2b98f81](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/2b98f81c4adbd61f8b981bc2247e7f008f598310)) +* changed component schemas from `AsyncApiJsonSchema` to `AsyncApiMultiFormatSchema` and added `AsyncApiMultiFormatSchemaReference` ([#8](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/8)) ([2b98f81](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/2b98f81c4adbd61f8b981bc2247e7f008f598310)) +* added implicit conversion from `AsyncApiJsonSchema` to `AsyncApiMultiFormatSchema` to simplify JSON Schema construction ([#8](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/8)) ([2b98f81](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/2b98f81c4adbd61f8b981bc2247e7f008f598310)) + +### Servers, Components, and References + +* replaced `AsyncApiServer.Url` with `Host` and `PathName`, and added server title, summary, and external documentation fields ([#8](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/8)) ([2b98f81](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/2b98f81c4adbd61f8b981bc2247e7f008f598310)) +* expanded components with reusable operations, replies, reply addresses, external documentation, tags, server variables, and all four binding component categories ([#8](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/8)) ([2b98f81](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/2b98f81c4adbd61f8b981bc2247e7f008f598310)) +* added typed references for operations, replies, reply addresses, tags, external documentation, and multi-format schemas ([#8](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/8)) ([2b98f81](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/2b98f81c4adbd61f8b981bc2247e7f008f598310)) +* added value equality to `AsyncApiReference` and expanded workspace registration for V3 components, root channels, and channel messages ([#8](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/8)) ([2b98f81](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/2b98f81c4adbd61f8b981bc2247e7f008f598310)) + +### Security, Parameters, and Traits + +* replaced `AsyncApiSecurityRequirement` with security scheme objects and references carrying required scopes; added V3 security scheme factories and separated required scopes from OAuth flow `AvailableScopes` ([#8](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/8)) ([2b98f81](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/2b98f81c4adbd61f8b981bc2247e7f008f598310)) +* replaced parameter `Schema` with the V3 `Enum`, `Default`, and `Examples` fields, with V2 schema reconstruction during downgrade serialization ([#8](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/8)) ([2b98f81](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/2b98f81c4adbd61f8b981bc2247e7f008f598310)) +* updated operation and message traits for V3 titles, security, and multi-format headers ([#8](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/8)) ([2b98f81](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/2b98f81c4adbd61f8b981bc2247e7f008f598310)) + +### V2 Compatibility + +* V2 channel keys are retained as `AsyncApiChannel.Address`, while normalized channel identifiers are used by the shared V3-shaped model ([#8](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/8)) ([2b98f81](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/2b98f81c4adbd61f8b981bc2247e7f008f598310)) +* V2 `subscribe` and `publish` operations are promoted to root operations with `Send` and `Receive` actions and channel references ([#8](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/8)) ([2b98f81](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/2b98f81c4adbd61f8b981bc2247e7f008f598310)) +* V2 inline messages and `message.oneOf` entries are promoted into channel message maps and operation message references ([#8](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/8)) ([2b98f81](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/2b98f81c4adbd61f8b981bc2247e7f008f598310)) +* V2 `operationId` and `messageId` values become operation and message dictionary keys in the shared model ([#8](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/8)) ([2b98f81](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/2b98f81c4adbd61f8b981bc2247e7f008f598310)) +* V2 server URLs are split into V3 host and pathname fields when read and recombined when serialized back to V2 ([#8](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/8)) ([2b98f81](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/2b98f81c4adbd61f8b981bc2247e7f008f598310)) +* V2 serialization reconstructs channel `subscribe` and `publish`, emits one message directly or multiple messages through `oneOf`, and flattens multi-format schemas back into V2 payload and component schema objects ([#8](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/8)) ([2b98f81](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/2b98f81c4adbd61f8b981bc2247e7f008f598310)) +* operations with no explicit message references now infer their V2 message or `oneOf` entries from the referenced channel's message map ([#10](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/10)) ([c201b13](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/c201b13a70f37d853aa063f74ff8a89faf2bf27f)) + ### BREAKING CHANGES -* All models have changed to reflect v3.0 + +* The public object model now follows AsyncAPI 3.0. Existing applications must migrate channel operations, message schemas, server URLs, security requirements, parameters, traits, components, and references to the new shapes described above. +* `AsyncApiChannel.Publish` and `Subscribe` were removed; place operations in `AsyncApiDocument.Operations` and set their `Action` and `Channel`. +* `AsyncApiChannel.Servers` now contains `AsyncApiServerReference` values instead of server-name strings. +* `AsyncApiOperation.Message` was replaced by `Messages`, containing `AsyncApiMessageReference` values into the referenced channel's message map. +* `AsyncApiOperation.OperationId`, `AsyncApiOperationTrait.OperationId`, `AsyncApiMessage.MessageId`, and `AsyncApiMessageTrait.MessageId` were removed; use operation, channel-message, or component dictionary keys as identity. +* `AsyncApiMessage.Payload`, `AsyncApiMessage.Headers`, and `AsyncApiComponents.Schemas` now use `AsyncApiMultiFormatSchema`. +* `IAsyncApiMessagePayload` was replaced by `IAsyncApiSchema`; schema helper extensions now operate on the new interface. +* `AsyncApiServer.Url` was replaced by `Host` and `PathName`. +* `AsyncApiSecurityRequirement` was removed; use security scheme references with `Scopes`. +* `AsyncApiParameter.Schema` was replaced by `Enum`, `Default`, and `Examples`. +* `AsyncApiOAuthFlow.Scopes` was renamed to `AvailableScopes` to distinguish offered scopes from scopes required by a security-scheme reference. +* `AsyncApiReaderSettings.BaseUrl` was removed. +* `AsyncApiWriterSettings.InlineLocalReferences` is now controlled through `ReferenceInline` rather than being set directly. +* custom `IAsyncApiSerializable` implementations must implement `SerializeV3`. + +## [1.0.1](https://github.com/ByteBardOrg/AsyncAPI.NET/compare/v1.0.0...v1.0.1) (2025-04-24) + ### Bug Fixes -* message inference ([#10](https://github.com/ByteBardOrg/AsyncAPI.NET/issues/10)) ([c201b13](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/c201b13a70f37d853aa063f74ff8a89faf2bf27f)) -# 1.0.0 (2025-03-28) + +* missing `messageId` serialization ([0a1d70f](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/0a1d70f21802f0059b3f9a7ba4e215e5d27b450e)) + +## 1.0.0 (2025-03-28) + ### Features + * AsyncAPI v2.6 full support ([f0ef397](https://github.com/ByteBardOrg/AsyncAPI.NET/commit/f0ef397944bfbc124c16b7769c2512fcf17d5fb9)) diff --git a/Common.Build.props b/Common.Build.props index 7f7d0ea6..69bcd2ce 100644 --- a/Common.Build.props +++ b/Common.Build.props @@ -2,7 +2,7 @@ 10 - netstandard2.0;netstandard2.1;net8 + netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0 disable ByteBard https://github.com/ByteBardOrg/AsyncAPI.NET @@ -21,4 +21,4 @@ - \ No newline at end of file + diff --git a/examples/ByteBard.AsyncAPI.Readers.JsonExample/ByteBard.AsyncAPI.Readers.JsonExample.csproj b/examples/ByteBard.AsyncAPI.Readers.JsonExample/ByteBard.AsyncAPI.Readers.JsonExample.csproj index e47ee8b5..871197d8 100644 --- a/examples/ByteBard.AsyncAPI.Readers.JsonExample/ByteBard.AsyncAPI.Readers.JsonExample.csproj +++ b/examples/ByteBard.AsyncAPI.Readers.JsonExample/ByteBard.AsyncAPI.Readers.JsonExample.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net10.0 disable enable diff --git a/examples/ByteBard.AsyncAPI.Readers.YamlExample/ByteBard.AsyncAPI.Readers.YamlExample.csproj b/examples/ByteBard.AsyncAPI.Readers.YamlExample/ByteBard.AsyncAPI.Readers.YamlExample.csproj index 2ebfbf6e..8e87447d 100644 --- a/examples/ByteBard.AsyncAPI.Readers.YamlExample/ByteBard.AsyncAPI.Readers.YamlExample.csproj +++ b/examples/ByteBard.AsyncAPI.Readers.YamlExample/ByteBard.AsyncAPI.Readers.YamlExample.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net10.0 disable enable diff --git a/examples/ByteBard.AsyncAPI.Writers.Example/ByteBard.AsyncAPI.Writers.Example.csproj b/examples/ByteBard.AsyncAPI.Writers.Example/ByteBard.AsyncAPI.Writers.Example.csproj index 4e9e52b9..c76ec4c9 100644 --- a/examples/ByteBard.AsyncAPI.Writers.Example/ByteBard.AsyncAPI.Writers.Example.csproj +++ b/examples/ByteBard.AsyncAPI.Writers.Example/ByteBard.AsyncAPI.Writers.Example.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net10.0 enable enable diff --git a/release.config.js b/release.config.js index 5348414c..9b0e4dbd 100644 --- a/release.config.js +++ b/release.config.js @@ -1,13 +1,21 @@ module.exports = { branches: ["v2", "vnext"], plugins: [ - "@semantic-release/commit-analyzer", - "@semantic-release/release-notes-generator", - [ - "@semantic-release/changelog", - { - "changelogFile": "CHANGELOG.md" - } - ], - ] -} + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + [ + "@semantic-release/changelog", + { + changelogFile: "CHANGELOG.md", + changelogTitle: "# Changelog\n\nPackage versions follow this library's semantic versioning and do not correspond directly to AsyncAPI specification versions. For example, package 2.0.0 introduced the AsyncAPI 3.0 object model, while package 3.0.0 later moved V3 document output to AsyncAPI 3.1.0.", + }, + ], + [ + "@semantic-release/git", + { + assets: ["CHANGELOG.md"], + message: "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}", + }, + ], + ], +}; diff --git a/src/ByteBard.AsyncAPI.Bindings/AMQP/AMQPChannelBinding.cs b/src/ByteBard.AsyncAPI.Bindings/AMQP/AMQPChannelBinding.cs index 81e070f2..410ca45b 100644 --- a/src/ByteBard.AsyncAPI.Bindings/AMQP/AMQPChannelBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/AMQP/AMQPChannelBinding.cs @@ -53,9 +53,16 @@ public class AMQPChannelBinding : ChannelBinding { "vhost", (a, n) => { a.Vhost = n.GetScalarValue(); } }, }; - /// - /// Serialize to AsyncAPI V2 document without using reference. - /// + public override void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeV3(writer); + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeProperties(writer); + } + public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) diff --git a/src/ByteBard.AsyncAPI.Bindings/AMQP/AMQPMessageBinding.cs b/src/ByteBard.AsyncAPI.Bindings/AMQP/AMQPMessageBinding.cs index 42abf499..8fb273bc 100644 --- a/src/ByteBard.AsyncAPI.Bindings/AMQP/AMQPMessageBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/AMQP/AMQPMessageBinding.cs @@ -20,6 +20,16 @@ public class AMQPMessageBinding : MessageBinding /// public string MessageType { get; set; } + public override void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeV3(writer); + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeProperties(writer); + } + public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) diff --git a/src/ByteBard.AsyncAPI.Bindings/AMQP/AMQPOperationBinding.cs b/src/ByteBard.AsyncAPI.Bindings/AMQP/AMQPOperationBinding.cs index dbc7f5d4..7f49bd36 100644 --- a/src/ByteBard.AsyncAPI.Bindings/AMQP/AMQPOperationBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/AMQP/AMQPOperationBinding.cs @@ -72,9 +72,16 @@ public class AMQPOperationBinding : OperationBinding { "ack", (a, n) => { a.Ack = n.GetBooleanValue(); } }, }; - /// - /// Serialize to AsyncAPI V2 document without using reference. - /// + public override void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeV3(writer); + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeProperties(writer); + } + public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) diff --git a/src/ByteBard.AsyncAPI.Bindings/Http/HttpMessageBinding.cs b/src/ByteBard.AsyncAPI.Bindings/Http/HttpMessageBinding.cs index 362f1ee1..5a76874e 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Http/HttpMessageBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Http/HttpMessageBinding.cs @@ -1,6 +1,7 @@ namespace ByteBard.AsyncAPI.Bindings.Http { using System; + using System.Net; using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Readers; using ByteBard.AsyncAPI.Readers.ParseNodes; @@ -9,17 +10,42 @@ namespace ByteBard.AsyncAPI.Bindings.Http /// /// Binding class for http messaging channels. /// + /// + /// The 'statusCode' field exists in AsyncAPI V3 but not in V2. + /// public class HttpMessageBinding : MessageBinding { + private const string V2BindingVersion = "0.2.0"; + private const string V3BindingVersion = "0.3.0"; + /// /// A Schema object containing the definitions for HTTP-specific headers. This schema MUST be of type object and have a properties key. /// public AsyncApiJsonSchema Headers { get; set; } /// - /// Serialize to AsyncAPI V2 document without using reference. + /// The HTTP response status code according to RFC 9110. `statusCode` is only relevant for messages referenced by the Operation Reply Object. + /// Note: This field is only serialized in AsyncAPI V3. /// - public override void SerializeProperties(IAsyncApiWriter writer) + public HttpStatusCode? StatusCode { get; set; } + + public override string BindingKey => "http"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + { "headers", (a, n) => { a.Headers = AsyncApiJsonSchemaDeserializer.LoadSchema(n); } }, + { "statusCode", (a, n) => + { + if (int.TryParse(n.GetScalarValue(), out var code)) + { + a.StatusCode = (HttpStatusCode)code; + } + } + }, + }; + + public override void SerializeV2(IAsyncApiWriter writer) { if (writer is null) { @@ -27,20 +53,35 @@ public override void SerializeProperties(IAsyncApiWriter writer) } writer.WriteStartObject(); - writer.WriteOptionalObject(AsyncApiConstants.Headers, this.Headers, (w, h) => h.SerializeV2(w)); - writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion); + writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion ?? V2BindingVersion); writer.WriteExtensions(this.Extensions); - writer.WriteEndObject(); } - public override string BindingKey => "http"; + public override void SerializeV3(IAsyncApiWriter writer) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } - protected override FixedFieldMap FixedFieldMap => new() + writer.WriteStartObject(); + writer.WriteOptionalObject(AsyncApiConstants.Headers, this.Headers, (w, h) => h.SerializeV3(w)); + + if (this.StatusCode.HasValue) + { + writer.WriteRequiredProperty(AsyncApiConstants.StatusCode, (int)this.StatusCode.Value); + } + + writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion ?? V3BindingVersion); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + + public override void SerializeProperties(IAsyncApiWriter writer) { - { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, - { "headers", (a, n) => { a.Headers = AsyncApiJsonSchemaDeserializer.LoadSchema(n); } }, - }; + this.SerializeV3(writer); + } } } diff --git a/src/ByteBard.AsyncAPI.Bindings/Http/HttpOperationBinding.cs b/src/ByteBard.AsyncAPI.Bindings/Http/HttpOperationBinding.cs index 6d5126f2..78b0c7f0 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Http/HttpOperationBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Http/HttpOperationBinding.cs @@ -10,8 +10,17 @@ namespace ByteBard.AsyncAPI.Bindings.Http /// /// Binding class for http operations. /// + /// + /// The 'type' field exists in AsyncAPI V2 but is removed in V3 (inferred from operation action). + /// public class HttpOperationBinding : OperationBinding { + private const string V2BindingVersion = "0.2.0"; + private const string V3BindingVersion = "0.3.0"; + + /// + /// Represents the HTTP operation type (used in V2 serialization). + /// public enum HttpOperationType { [Display("request")] @@ -22,12 +31,7 @@ public enum HttpOperationType } /// - /// REQUIRED. Type of operation. Its value MUST be either request or response. - /// - 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. + /// The HTTP method, e.g. GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, CONNECT, and TRACE. /// public string Method { get; set; } @@ -36,10 +40,16 @@ public enum HttpOperationType /// public AsyncApiJsonSchema Query { get; set; } - /// - /// Serialize to AsyncAPI V2 document without using reference. - /// - public override void SerializeProperties(IAsyncApiWriter writer) + public override string BindingKey => "http"; + + protected override FixedFieldMap FixedFieldMap => new() + { + { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, + { "method", (a, n) => { a.Method = n.GetScalarValue(); } }, + { "query", (a, n) => { a.Query = AsyncApiJsonSchemaDeserializer.LoadSchema(n); } }, + }; + + public override void SerializeV2(IAsyncApiWriter writer) { if (writer is null) { @@ -48,22 +58,50 @@ public override void SerializeProperties(IAsyncApiWriter writer) writer.WriteStartObject(); - writer.WriteRequiredProperty(AsyncApiConstants.Type, this.Type.GetDisplayName()); + var typeValue = this.InferTypeFromContext(writer); + if (typeValue.HasValue) + { + writer.WriteRequiredProperty(AsyncApiConstants.Type, typeValue.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.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion ?? V2BindingVersion); writer.WriteExtensions(this.Extensions); writer.WriteEndObject(); } - protected override FixedFieldMap FixedFieldMap => new() + public override void SerializeV3(IAsyncApiWriter writer) { - { "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 = AsyncApiJsonSchemaDeserializer.LoadSchema(n); } }, - }; + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } - public override string BindingKey => "http"; + writer.WriteStartObject(); + writer.WriteOptionalProperty(AsyncApiConstants.Method, this.Method); + writer.WriteOptionalObject(AsyncApiConstants.Query, this.Query, (w, h) => h.SerializeV3(w)); + writer.WriteOptionalProperty(AsyncApiConstants.BindingVersion, this.BindingVersion ?? V3BindingVersion); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + + public override void SerializeProperties(IAsyncApiWriter writer) + { + this.SerializeV3(writer); + } + + private HttpOperationType? InferTypeFromContext(IAsyncApiWriter writer) + { + var parentOperation = writer.Workspace?.GetSerializationContext(); + if (parentOperation == null) + { + return null; + } + + return parentOperation.Action == AsyncApiAction.Send + ? HttpOperationType.Request + : HttpOperationType.Response; + } } } diff --git a/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaChannelBinding.cs b/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaChannelBinding.cs index 7e5fc793..bb96af5a 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaChannelBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaChannelBinding.cs @@ -54,9 +54,16 @@ public class KafkaChannelBinding : ChannelBinding { "confluent.value.subject.name.strategy", (a, n) => { a.ConfluentValueSubjectName = n.GetScalarValue(); } }, }; - /// - /// Serialize to AsyncAPI V2 document without using reference. - /// + public override void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeV3(writer); + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeProperties(writer); + } + public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) diff --git a/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaMessageBinding.cs b/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaMessageBinding.cs index 59ead5fc..1a8a9619 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaMessageBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaMessageBinding.cs @@ -35,6 +35,16 @@ public class KafkaMessageBinding : MessageBinding /// The version of this binding. If omitted, "latest" MUST be assumed. /// + public override void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeV3(writer); + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeProperties(writer); + } + public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) @@ -54,12 +64,6 @@ public override void SerializeProperties(IAsyncApiWriter writer) writer.WriteEndObject(); } - /// - /// Serializes the v2. - /// - /// The writer. - /// writer. - public override string BindingKey => "kafka"; protected override FixedFieldMap FixedFieldMap => new() diff --git a/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaOperationBinding.cs b/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaOperationBinding.cs index 8c7081e5..4b3988d1 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaOperationBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaOperationBinding.cs @@ -30,9 +30,16 @@ public class KafkaOperationBinding : OperationBinding { "clientId", (a, n) => { a.ClientId = AsyncApiJsonSchemaDeserializer.LoadSchema(n); } }, }; - /// - /// Serialize to AsyncAPI V2 document without using reference. - /// + public override void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeV3(writer); + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeProperties(writer); + } + public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) diff --git a/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaServerBinding.cs b/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaServerBinding.cs index d95bf5dc..9c6b5699 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaServerBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaServerBinding.cs @@ -29,9 +29,16 @@ public class KafkaServerBinding : ServerBinding { "schemaRegistryVendor", (a, n) => { a.SchemaRegistryVendor = n.GetScalarValue(); } }, }; - /// - /// Serialize to AsyncAPI V2 document without using reference. - /// + public override void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeV3(writer); + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeProperties(writer); + } + public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) diff --git a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs index e3ab20d2..44cec884 100644 --- a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs @@ -33,6 +33,16 @@ public class MQTTMessageBinding : MessageBinding /// public string ResponseTopic { get; set; } + public override void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeV3(writer); + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeProperties(writer); + } + public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) diff --git a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs index f9a1e740..fb593aaf 100644 --- a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTOperationBinding.cs @@ -34,9 +34,16 @@ public class MQTTOperationBinding : OperationBinding { "messageExpiryInterval", (a, n) => { a.MessageExpiryInterval = n.GetIntegerValueOrDefault(); } }, }; - /// - /// Serialize to AsyncAPI V2 document without using reference. - /// + public override void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeV3(writer); + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeProperties(writer); + } + public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) diff --git a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTServerBinding.cs b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTServerBinding.cs index 79e0ce69..a137f8b6 100644 --- a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTServerBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTServerBinding.cs @@ -66,9 +66,16 @@ public class MQTTServerBinding : ServerBinding { "retain", (a, n) => { a.Retain = n.GetBooleanValue(); } }, }; - /// - /// Serialize to AsyncAPI V2 document without using reference. - /// + public override void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeV3(writer); + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeProperties(writer); + } + public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) diff --git a/src/ByteBard.AsyncAPI.Bindings/Pulsar/PulsarChannelBinding.cs b/src/ByteBard.AsyncAPI.Bindings/Pulsar/PulsarChannelBinding.cs index b1fda8dc..8bc218e4 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Pulsar/PulsarChannelBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Pulsar/PulsarChannelBinding.cs @@ -46,6 +46,16 @@ public class PulsarChannelBinding : ChannelBinding public override string BindingKey => "pulsar"; + public override void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeV3(writer); + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeProperties(writer); + } + public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) diff --git a/src/ByteBard.AsyncAPI.Bindings/Pulsar/PulsarServerBinding.cs b/src/ByteBard.AsyncAPI.Bindings/Pulsar/PulsarServerBinding.cs index c283ce56..1611da73 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Pulsar/PulsarServerBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Pulsar/PulsarServerBinding.cs @@ -23,6 +23,16 @@ public class PulsarServerBinding : ServerBinding { "tenant", (a, n) => { a.Tenant = n.GetScalarValue(); } }, }; + public override void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeV3(writer); + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeProperties(writer); + } + public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) diff --git a/src/ByteBard.AsyncAPI.Bindings/Sns/SnsChannelBinding.cs b/src/ByteBard.AsyncAPI.Bindings/Sns/SnsChannelBinding.cs index c8a8f21c..64c99df3 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Sns/SnsChannelBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Sns/SnsChannelBinding.cs @@ -61,7 +61,16 @@ public class SnsChannelBinding : ChannelBinding { "condition", (a, n) => { a.Condition = Condition.Parse(n); } }, }; - /// + public override void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeV3(writer); + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeProperties(writer); + } + public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) diff --git a/src/ByteBard.AsyncAPI.Bindings/Sns/SnsOperationBinding.cs b/src/ByteBard.AsyncAPI.Bindings/Sns/SnsOperationBinding.cs index 45bf7a08..b49ac99a 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Sns/SnsOperationBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Sns/SnsOperationBinding.cs @@ -73,6 +73,16 @@ public class SnsOperationBinding : OperationBinding { "maxReceivesPerSecond", (a, n) => { a.MaxReceivesPerSecond = n.GetIntegerValue(); } }, }; + public override void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeV3(writer); + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeProperties(writer); + } + public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) diff --git a/src/ByteBard.AsyncAPI.Bindings/Sqs/SqsChannelBinding.cs b/src/ByteBard.AsyncAPI.Bindings/Sqs/SqsChannelBinding.cs index 0d85d231..f72c187e 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Sqs/SqsChannelBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Sqs/SqsChannelBinding.cs @@ -68,6 +68,16 @@ public class SqsChannelBinding : ChannelBinding { "condition", (a, n) => { a.Condition = Condition.Parse(n); } }, }; + public override void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeV3(writer); + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeProperties(writer); + } + public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) diff --git a/src/ByteBard.AsyncAPI.Bindings/Sqs/SqsOperationBinding.cs b/src/ByteBard.AsyncAPI.Bindings/Sqs/SqsOperationBinding.cs index 7583b3d8..2b6c0b0e 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Sqs/SqsOperationBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Sqs/SqsOperationBinding.cs @@ -60,6 +60,16 @@ public class SqsOperationBinding : OperationBinding { "condition", (a, n) => { a.Condition = Condition.Parse(n); } }, }; + public override void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeV3(writer); + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeProperties(writer); + } + public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) diff --git a/src/ByteBard.AsyncAPI.Bindings/WebSockets/WebSocketsChannelBinding.cs b/src/ByteBard.AsyncAPI.Bindings/WebSockets/WebSocketsChannelBinding.cs index fe24b87b..70556343 100644 --- a/src/ByteBard.AsyncAPI.Bindings/WebSockets/WebSocketsChannelBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/WebSockets/WebSocketsChannelBinding.cs @@ -33,6 +33,16 @@ public class WebSocketsChannelBinding : ChannelBinding { "headers", (a, n) => { a.Headers = AsyncApiJsonSchemaDeserializer.LoadSchema(n); } }, }; + public override void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeV3(writer); + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeProperties(writer); + } + public override void SerializeProperties(IAsyncApiWriter writer) { if (writer is null) diff --git a/src/ByteBard.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs b/src/ByteBard.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs index ab5586ef..e51742d5 100644 --- a/src/ByteBard.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs +++ b/src/ByteBard.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs @@ -139,6 +139,7 @@ public T ReadFragment(JsonNode input, AsyncApiVersion version, out AsyncApiDi where T : IAsyncApiElement { diagnostic = new AsyncApiDiagnostic(); + diagnostic.SpecificationVersion = version; this.context ??= new ParsingContext(diagnostic, this.settings) { ExtensionParsers = this.settings.ExtensionParsers, @@ -164,10 +165,15 @@ public T ReadFragment(JsonNode input, AsyncApiVersion version, out AsyncApiDi if (this.settings.RuleSet != null && this.settings.RuleSet.Rules.Count > 0) { var errors = element.Validate(this.settings.RuleSet); - foreach (var item in errors) + foreach (var item in errors.OfType()) { diagnostic.Errors.Add(item); } + + foreach (var item in errors.OfType()) + { + diagnostic.Warnings.Add(item); + } } return (T)element; @@ -280,7 +286,7 @@ private IAsyncApiSerializable ResolveExternalReference(AsyncApiDiagnostic diagno } else { - stream = loader.Load(new Uri(reference.Reference.ExternalResource, UriKind.RelativeOrAbsolute)); + stream = loader.Load(this.settings.BaseUri, new Uri(reference.Reference.ExternalResource, UriKind.RelativeOrAbsolute)); this.context.Workspace.RegisterComponent(reference.Reference.ExternalResource, stream); } @@ -310,7 +316,7 @@ private async Task ResolveExternalReferenceAsync(AsyncApi } else { - stream = await loader.LoadAsync(new Uri(reference.Reference.ExternalResource, UriKind.RelativeOrAbsolute)); + stream = await loader.LoadAsync(this.settings.BaseUri, new Uri(reference.Reference.ExternalResource, UriKind.RelativeOrAbsolute)); this.context.Workspace.RegisterComponent(reference.Reference.ExternalResource, stream); } @@ -363,58 +369,61 @@ private IAsyncApiSerializable ResolveStreamReference(Stream stream, IAsyncApiRef } } - AsyncApiDiagnostic fragmentDiagnostic = new AsyncApiDiagnostic(); + AsyncApiDiagnostic fragmentDiagnostic = new AsyncApiDiagnostic + { + SpecificationVersion = diagnostic.SpecificationVersion, + }; IAsyncApiSerializable result = null; switch (reference.Reference.Type) { case ReferenceType.Schema: if (reference is AsyncApiJsonSchemaReference) { - result = this.ReadFragment(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + result = this.ReadFragment(json, diagnostic.SpecificationVersion, out fragmentDiagnostic); } if (reference is AsyncApiAvroSchemaReference) { - result = this.ReadFragment(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + result = this.ReadFragment(json, diagnostic.SpecificationVersion, out fragmentDiagnostic); } break; case ReferenceType.Server: - result = this.ReadFragment(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + result = this.ReadFragment(json, diagnostic.SpecificationVersion, out fragmentDiagnostic); break; case ReferenceType.Channel: - result = this.ReadFragment(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + result = this.ReadFragment(json, diagnostic.SpecificationVersion, out fragmentDiagnostic); break; case ReferenceType.Message: - result = this.ReadFragment(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + result = this.ReadFragment(json, diagnostic.SpecificationVersion, out fragmentDiagnostic); break; case ReferenceType.SecurityScheme: - result = this.ReadFragment(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + result = this.ReadFragment(json, diagnostic.SpecificationVersion, out fragmentDiagnostic); break; case ReferenceType.Parameter: - result = this.ReadFragment(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + result = this.ReadFragment(json, diagnostic.SpecificationVersion, out fragmentDiagnostic); break; case ReferenceType.CorrelationId: - result = this.ReadFragment(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + result = this.ReadFragment(json, diagnostic.SpecificationVersion, out fragmentDiagnostic); break; case ReferenceType.OperationTrait: - result = this.ReadFragment(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + result = this.ReadFragment(json, diagnostic.SpecificationVersion, out fragmentDiagnostic); break; case ReferenceType.MessageTrait: - result = this.ReadFragment(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + result = this.ReadFragment(json, diagnostic.SpecificationVersion, out fragmentDiagnostic); break; case ReferenceType.ServerBindings: - result = this.ReadFragment>(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + result = this.ReadFragment>(json, diagnostic.SpecificationVersion, out fragmentDiagnostic); break; case ReferenceType.ChannelBindings: - result = this.ReadFragment>(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + result = this.ReadFragment>(json, diagnostic.SpecificationVersion, out fragmentDiagnostic); break; case ReferenceType.OperationBindings: - result = this.ReadFragment>(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + result = this.ReadFragment>(json, diagnostic.SpecificationVersion, out fragmentDiagnostic); break; case ReferenceType.MessageBindings: - result = this.ReadFragment>(json, AsyncApiVersion.AsyncApi2_0, out fragmentDiagnostic); + result = this.ReadFragment>(json, diagnostic.SpecificationVersion, out fragmentDiagnostic); break; default: diagnostic.Errors.Add(new AsyncApiError(reference.Reference.Reference, "Could not resolve reference.")); diff --git a/src/ByteBard.AsyncAPI.Readers/AsyncApiReaderSettings.cs b/src/ByteBard.AsyncAPI.Readers/AsyncApiReaderSettings.cs index 111bf26c..9881dd07 100644 --- a/src/ByteBard.AsyncAPI.Readers/AsyncApiReaderSettings.cs +++ b/src/ByteBard.AsyncAPI.Readers/AsyncApiReaderSettings.cs @@ -70,6 +70,11 @@ public ICollection> /// public bool LeaveStreamOpen { get; set; } + /// + /// Uri where relative references should be resolved from when using the External Reference Loader. + /// + public Uri? BaseUri { get; set; } + /// /// External reference reader implementation provided by users for reading external resources. /// diff --git a/src/ByteBard.AsyncAPI.Readers/Interface/IStreamLoader.cs b/src/ByteBard.AsyncAPI.Readers/Interface/IStreamLoader.cs index a74f45fd..c770e75a 100644 --- a/src/ByteBard.AsyncAPI.Readers/Interface/IStreamLoader.cs +++ b/src/ByteBard.AsyncAPI.Readers/Interface/IStreamLoader.cs @@ -6,8 +6,8 @@ namespace ByteBard.AsyncAPI.Readers.Interface public interface IStreamLoader { - Task LoadAsync(Uri uri); + Task LoadAsync(Uri baseUri, Uri uri); - Stream Load(Uri uri); + Stream Load(Uri baseUri, Uri uri); } } \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI.Readers/Schemas/AsyncApiAvroSchemaDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/Schemas/AsyncApiAvroSchemaDeserializer.cs index d67777ea..fb749e87 100644 --- a/src/ByteBard.AsyncAPI.Readers/Schemas/AsyncApiAvroSchemaDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/Schemas/AsyncApiAvroSchemaDeserializer.cs @@ -1,5 +1,6 @@ -namespace ByteBard.AsyncAPI.Readers +namespace ByteBard.AsyncAPI.Readers { + using System.Collections.Generic; using ByteBard.AsyncAPI.Exceptions; using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Models.Avro.LogicalTypes; @@ -8,61 +9,61 @@ public class AsyncApiAvroSchemaDeserializer { - private static readonly FixedFieldMap FieldFixedFields = new() - { - { "name", (a, n) => a.Name = n.GetScalarValue() }, - { "type", (a, n) => a.Type = LoadSchema(n) }, - { "doc", (a, n) => a.Doc = n.GetScalarValue() }, - { "default", (a, n) => a.Default = n.CreateAny() }, - { "aliases", (a, n) => a.Aliases = n.CreateSimpleList(n2 => n2.GetScalarValue()) }, - { "order", (a, n) => a.Order = n.GetScalarValue().GetEnumFromDisplayName() }, + private static readonly ISet FieldPropertyNames = new HashSet + { + "name", + "type", + "doc", + "default", + "aliases", + "order", }; - private static readonly FixedFieldMap RecordFixedFields = new() + private static readonly ISet RecordPropertyNames = new HashSet { - { "type", (a, n) => { } }, - { "name", (a, n) => a.Name = n.GetScalarValue() }, - { "doc", (a, n) => a.Doc = n.GetScalarValue() }, - { "namespace", (a, n) => a.Namespace = n.GetScalarValue() }, - { "aliases", (a, n) => a.Aliases = n.CreateSimpleList(n2 => n2.GetScalarValue()) }, - { "fields", (a, n) => a.Fields = n.CreateList(LoadField) }, + "type", + "name", + "doc", + "namespace", + "aliases", + "fields", }; - private static readonly FixedFieldMap EnumFixedFields = new() + private static readonly ISet EnumPropertyNames = new HashSet { - { "type", (a, n) => { } }, - { "name", (a, n) => a.Name = n.GetScalarValue() }, - { "doc", (a, n) => a.Doc = n.GetScalarValue() }, - { "namespace", (a, n) => a.Namespace = n.GetScalarValue() }, - { "aliases", (a, n) => a.Aliases = n.CreateSimpleList(n2 => n2.GetScalarValue()) }, - { "symbols", (a, n) => a.Symbols = n.CreateSimpleList(n2 => n2.GetScalarValue()) }, - { "default", (a, n) => a.Default = n.GetScalarValue() }, + "type", + "name", + "doc", + "namespace", + "aliases", + "symbols", + "default", }; - private static readonly FixedFieldMap FixedFixedFields = new() + private static readonly ISet FixedPropertyNames = new HashSet { - { "type", (a, n) => { } }, - { "name", (a, n) => a.Name = n.GetScalarValue() }, - { "namespace", (a, n) => a.Namespace = n.GetScalarValue() }, - { "aliases", (a, n) => a.Aliases = n.CreateSimpleList(n2 => n2.GetScalarValue()) }, - { "size", (a, n) => a.Size = int.Parse(n.GetScalarValue(), n.Context.Settings.CultureInfo) }, + "type", + "name", + "namespace", + "aliases", + "size", }; - private static readonly FixedFieldMap ArrayFixedFields = new() + private static readonly ISet ArrayPropertyNames = new HashSet { - { "type", (a, n) => { } }, - { "items", (a, n) => a.Items = LoadSchema(n) }, + "type", + "items", }; - private static readonly FixedFieldMap MapFixedFields = new() + private static readonly ISet MapPropertyNames = new HashSet { - { "type", (a, n) => { } }, - { "values", (a, n) => a.Values = n.GetScalarValue().GetEnumFromDisplayName() }, + "type", + "values", }; - private static readonly FixedFieldMap UnionFixedFields = new() + private static readonly ISet PrimitivePropertyNames = new HashSet { - { "types", (a, n) => a.Types = n.CreateList(LoadSchema) }, + "type", }; private static readonly FixedFieldMap DecimalFixedFields = new() @@ -119,119 +120,79 @@ public class AsyncApiAvroSchemaDeserializer { "size", (a, n) => { } }, }; - private static readonly PatternFieldMap RecordMetadataPatternFields = - new() + private static readonly PatternFieldMap DecimalMetadataPatternFields = new() { { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, }; - private static readonly PatternFieldMap FieldMetadataPatternFields = - new() - { - { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, - }; - - private static readonly PatternFieldMap EnumMetadataPatternFields = - new() - { - { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, - }; - - private static readonly PatternFieldMap FixedMetadataPatternFields = - new() - { - { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, - }; - - private static readonly PatternFieldMap ArrayMetadataPatternFields = - new() - { - { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, - }; - - private static readonly PatternFieldMap MapMetadataPatternFields = - new() - { - { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, - }; - - private static readonly PatternFieldMap UnionMetadataPatternFields = - new() - { - { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, - }; - - private static readonly PatternFieldMap DecimalMetadataPatternFields = - new() + private static readonly PatternFieldMap UUIDMetadataPatternFields = new() { { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, }; - private static readonly PatternFieldMap UUIDMetadataPatternFields = - new() + private static readonly PatternFieldMap DateMetadataPatternFields = new() { { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, }; - private static readonly PatternFieldMap DateMetadataPatternFields = - new() + private static readonly PatternFieldMap TimeMillisMetadataPatternFields = new() { { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, }; - private static readonly PatternFieldMap TimeMillisMetadataPatternFields = - new() + private static readonly PatternFieldMap TimeMicrosMetadataPatternFields = new() { { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, }; - private static readonly PatternFieldMap TimeMicrosMetadataPatternFields = - new() + private static readonly PatternFieldMap TimestampMillisMetadataPatternFields = new() { { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, }; - private static readonly PatternFieldMap TimestampMillisMetadataPatternFields = - new() + private static readonly PatternFieldMap TimestampMicrosMetadataPatternFields = new() { { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, }; - private static readonly PatternFieldMap TimestampMicrosMetadataPatternFields = - new() + private static readonly PatternFieldMap DurationMetadataPatternFields = new() { { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, }; - private static readonly PatternFieldMap DurationMetadataPatternFields = - new() - { - { s => s.StartsWith(string.Empty), (a, p, n) => a.Metadata[p] = n.CreateAny() }, - }; + private readonly Dictionary namedTypes = new Dictionary(); + private readonly Stack namespaces = new Stack(); + + private string CurrentNamespace => this.namespaces.Count > 0 ? this.namespaces.Peek() : null; public static AsyncApiAvroSchema LoadSchema(ParseNode node) { + return new AsyncApiAvroSchemaDeserializer().LoadSchemaCore(node); + } + + private AsyncApiAvroSchema LoadSchemaCore(ParseNode node) + { + if (node is PropertyNode propertyNode) + { + node = propertyNode.Value; + } + if (node is ValueNode valueNode) { - return new AvroPrimitive(valueNode.GetScalarValue().GetEnumFromDisplayName()); + return this.LoadStringSchema(valueNode.GetScalarValue()); } - if (node is ListNode) + if (node is ListNode listNode) { var union = new AvroUnion(); - foreach (var item in node as ListNode) + foreach (var item in listNode) { - union.Types.Add(LoadSchema(item)); + union.Types.Add(this.LoadSchemaCore(item)); } return union; } - if (node is PropertyNode propertyNode) - { - node = propertyNode.Value; - } - if (node is MapNode mapNode) { var pointer = mapNode.GetReferencePointer(); @@ -244,37 +205,32 @@ public static AsyncApiAvroSchema LoadSchema(ParseNode node) var isLogicalType = mapNode["logicalType"] != null; if (isLogicalType) { - return LoadLogicalType(mapNode); + return this.LoadLogicalType(mapNode); } var type = mapNode["type"]?.Value.GetScalarValue(); switch (type) { case "record": - var record = new AvroRecord(); - mapNode.ParseFields(record, RecordFixedFields, RecordMetadataPatternFields); - return record; + return this.LoadRecord(mapNode); case "enum": - var @enum = new AvroEnum(); - mapNode.ParseFields(@enum, EnumFixedFields, EnumMetadataPatternFields); - return @enum; + return this.LoadEnum(mapNode); case "fixed": - var @fixed = new AvroFixed(); - mapNode.ParseFields(@fixed, FixedFixedFields, FixedMetadataPatternFields); - return @fixed; + return this.LoadFixed(mapNode); case "array": - var array = new AvroArray(); - mapNode.ParseFields(array, ArrayFixedFields, ArrayMetadataPatternFields); - return array; + return this.LoadArray(mapNode); case "map": - var map = new AvroMap(); - mapNode.ParseFields(map, MapFixedFields, MapMetadataPatternFields); - return map; + return this.LoadMap(mapNode); case "union": - var union = new AvroUnion(); - mapNode.ParseFields(union, UnionFixedFields, UnionMetadataPatternFields); - return union; + return this.LoadUnion(mapNode); default: + if (type != null) + { + var schema = this.LoadStringSchema(type); + this.ParseMetadata(mapNode, schema.Metadata, PrimitivePropertyNames); + return schema; + } + throw new AsyncApiException($"Unsupported type: {type}"); } } @@ -282,7 +238,135 @@ public static AsyncApiAvroSchema LoadSchema(ParseNode node) throw new AsyncApiReaderException("Invalid node type"); } - private static AsyncApiAvroSchema LoadLogicalType(MapNode mapNode) + private AvroRecord LoadRecord(MapNode mapNode) + { + var record = new AvroRecord + { + Name = this.GetStringValue(mapNode, "name"), + Namespace = this.GetStringValue(mapNode, "namespace"), + Doc = this.GetStringValue(mapNode, "doc"), + }; + + var aliases = mapNode["aliases"]?.Value; + if (aliases != null) + { + record.Aliases = aliases.CreateSimpleList(n => n.GetScalarValue()); + } + + this.RegisterNamedType(record, record.Name, record.Namespace); + + this.namespaces.Push(this.GetNamespaceForNamedType(record.Name, record.Namespace)); + try + { + var fields = mapNode["fields"]?.Value; + if (fields != null) + { + record.Fields = fields.CreateList(this.LoadField); + } + } + finally + { + this.namespaces.Pop(); + } + + this.ParseMetadata(mapNode, record.Metadata, RecordPropertyNames); + return record; + } + + private AvroEnum LoadEnum(MapNode mapNode) + { + var @enum = new AvroEnum + { + Name = this.GetStringValue(mapNode, "name"), + Namespace = this.GetStringValue(mapNode, "namespace"), + Doc = this.GetStringValue(mapNode, "doc"), + Default = this.GetStringValue(mapNode, "default"), + }; + + var aliases = mapNode["aliases"]?.Value; + if (aliases != null) + { + @enum.Aliases = aliases.CreateSimpleList(n => n.GetScalarValue()); + } + + var symbols = mapNode["symbols"]?.Value; + if (symbols != null) + { + @enum.Symbols = symbols.CreateSimpleList(n => n.GetScalarValue()); + } + + this.RegisterNamedType(@enum, @enum.Name, @enum.Namespace); + this.ParseMetadata(mapNode, @enum.Metadata, EnumPropertyNames); + return @enum; + } + + private AvroFixed LoadFixed(MapNode mapNode) + { + var @fixed = new AvroFixed + { + Name = this.GetStringValue(mapNode, "name"), + Namespace = this.GetStringValue(mapNode, "namespace"), + }; + + var aliases = mapNode["aliases"]?.Value; + if (aliases != null) + { + @fixed.Aliases = aliases.CreateSimpleList(n => n.GetScalarValue()); + } + + var size = mapNode["size"]?.Value; + if (size != null) + { + @fixed.Size = int.Parse(size.GetScalarValue(), size.Context.Settings.CultureInfo); + } + + this.RegisterNamedType(@fixed, @fixed.Name, @fixed.Namespace); + this.ParseMetadata(mapNode, @fixed.Metadata, FixedPropertyNames); + return @fixed; + } + + private AvroArray LoadArray(MapNode mapNode) + { + var array = new AvroArray(); + var items = mapNode["items"]?.Value; + if (items != null) + { + array.Items = this.LoadSchemaCore(items); + } + + this.ParseMetadata(mapNode, array.Metadata, ArrayPropertyNames); + return array; + } + + private AvroMap LoadMap(MapNode mapNode) + { + var map = new AvroMap(); + var values = mapNode["values"]?.Value; + if (values != null) + { + map.Values = this.LoadSchemaCore(values); + } + + this.ParseMetadata(mapNode, map.Metadata, MapPropertyNames); + return map; + } + + private AvroUnion LoadUnion(MapNode mapNode) + { + var union = new AvroUnion(); + var types = mapNode["types"]?.Value; + if (types is ListNode listNode) + { + foreach (var item in listNode) + { + union.Types.Add(this.LoadSchemaCore(item)); + } + } + + return union; + } + + private AsyncApiAvroSchema LoadLogicalType(MapNode mapNode) { var type = mapNode["logicalType"]?.Value.GetScalarValue(); switch (type) @@ -318,21 +402,136 @@ private static AsyncApiAvroSchema LoadLogicalType(MapNode mapNode) case "duration": var duration = new AvroDuration(); mapNode.ParseFields(duration, DurationFixedFields, DurationMetadataPatternFields); + this.RegisterNamedType(duration, duration.Name, duration.Namespace); return duration; default: throw new AsyncApiException($"Unsupported type: {type}"); } } - private static AvroField LoadField(ParseNode node) + private AvroField LoadField(ParseNode node) { var mapNode = node.CheckMapNode("field"); - var field = new AvroField(); + var field = new AvroField + { + Name = this.GetStringValue(mapNode, "name"), + Doc = this.GetStringValue(mapNode, "doc"), + }; - mapNode.ParseFields(field, FieldFixedFields, FieldMetadataPatternFields); + var type = mapNode["type"]?.Value; + if (type != null) + { + field.Type = this.LoadSchemaCore(type); + } + + var @default = mapNode["default"]?.Value; + if (@default != null) + { + field.Default = @default.CreateAny(); + } + var aliases = mapNode["aliases"]?.Value; + if (aliases != null) + { + field.Aliases = aliases.CreateSimpleList(n => n.GetScalarValue()); + } + + var order = mapNode["order"]?.Value; + if (order != null) + { + field.Order = order.GetScalarValue().GetEnumFromDisplayName(); + } + + this.ParseMetadata(mapNode, field.Metadata, FieldPropertyNames); return field; + } + private AsyncApiAvroSchema LoadStringSchema(string type) + { + if (this.IsPrimitiveType(type)) + { + return new AvroPrimitive(type.GetEnumFromDisplayName()); + } + + var fullName = this.GetReferenceFullName(type); + this.namedTypes.TryGetValue(fullName, out var target); + return new AvroNamedType(type, target); + } + + private void RegisterNamedType(AsyncApiAvroSchema schema, string name, string @namespace) + { + var fullName = this.GetFullName(name, @namespace); + if (fullName != null) + { + this.namedTypes[fullName] = schema; + } + } + + private string GetReferenceFullName(string name) + { + if (name == null || name.IndexOf('.') >= 0) + { + return name; + } + + var @namespace = this.CurrentNamespace; + return string.IsNullOrEmpty(@namespace) ? name : $"{@namespace}.{name}"; + } + + private string GetFullName(string name, string @namespace) + { + if (name == null || name.IndexOf('.') >= 0) + { + return name; + } + + @namespace ??= this.CurrentNamespace; + return string.IsNullOrEmpty(@namespace) ? name : $"{@namespace}.{name}"; + } + + private string GetNamespaceForNamedType(string name, string @namespace) + { + if (name != null && name.IndexOf('.') >= 0) + { + var lastDot = name.LastIndexOf('.'); + return lastDot > 0 ? name.Substring(0, lastDot) : string.Empty; + } + + return @namespace ?? this.CurrentNamespace; + } + + private string GetStringValue(MapNode mapNode, string propertyName) + { + return mapNode[propertyName]?.Value.GetScalarValue(); + } + + private bool IsPrimitiveType(string type) + { + switch (type) + { + case "null": + case "boolean": + case "int": + case "long": + case "float": + case "double": + case "bytes": + case "string": + return true; + default: + return false; + } + } + + private void ParseMetadata(MapNode mapNode, IDictionary metadata, ISet fixedFields) + { + foreach (var propertyNode in mapNode) + { + if (!fixedFields.Contains(propertyNode.Name)) + { + metadata[propertyNode.Name] = propertyNode.Value.CreateAny(); + } + } } } -} \ No newline at end of file +} diff --git a/src/ByteBard.AsyncAPI.Readers/Services/DefaultStreamLoader.cs b/src/ByteBard.AsyncAPI.Readers/Services/DefaultStreamLoader.cs index 6ccd69ba..74b11dd6 100644 --- a/src/ByteBard.AsyncAPI.Readers/Services/DefaultStreamLoader.cs +++ b/src/ByteBard.AsyncAPI.Readers/Services/DefaultStreamLoader.cs @@ -11,40 +11,54 @@ internal class DefaultStreamLoader : IStreamLoader { private static readonly HttpClient HttpClient = new HttpClient(); - public Stream Load(Uri uri) + public Stream Load(Uri baseUri, Uri uri) { try { - switch (uri.Scheme) + if (uri.IsAbsoluteUri) { - case "file": - return File.OpenRead(uri.AbsolutePath); - case "http": - case "https": - return HttpClient.GetStreamAsync(uri).GetAwaiter().GetResult(); - default: - throw new ArgumentException("Unsupported scheme"); + switch (uri.Scheme.ToLowerInvariant()) + { + case "file": + return File.OpenRead(uri.LocalPath); + case "http": + case "https": + return HttpClient.GetStreamAsync(uri).GetAwaiter().GetResult(); + default: + throw new ArgumentException("Unsupported scheme"); + } + } + else + { + return File.OpenRead(new Uri(baseUri, uri).LocalPath); } } catch (Exception ex) { - throw new AsyncApiReaderException($"Something went wrong trying to fetch '{uri.OriginalString}. {ex.Message}'", ex); + throw new AsyncApiReaderException($"Something went wrong trying to fetch '{uri.OriginalString}'. {ex.Message}", ex); } } - public async Task LoadAsync(Uri uri) + public async Task LoadAsync(Uri baseUri, Uri uri) { try { - switch (uri.Scheme) + if (uri.IsAbsoluteUri) + { + switch (uri.Scheme.ToLowerInvariant()) + { + case "file": + return File.OpenRead(uri.LocalPath); + case "http": + case "https": + return await HttpClient.GetStreamAsync(uri); + default: + throw new ArgumentException("Unsupported scheme"); + } + } + else { - case "file": - return File.OpenRead(uri.AbsolutePath); - case "http": - case "https": - return await HttpClient.GetStreamAsync(uri); - default: - throw new ArgumentException("Unsupported scheme"); + return File.OpenRead(new Uri(baseUri, uri).LocalPath); } } catch (Exception ex) @@ -53,4 +67,4 @@ public async Task LoadAsync(Uri uri) } } } -} \ No newline at end of file +} diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiChannelDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiChannelDeserializer.cs index df26b176..424da0ae 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiChannelDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiChannelDeserializer.cs @@ -11,13 +11,19 @@ internal static partial class AsyncApiV2Deserializer private static readonly FixedFieldMap ChannelFixedFields = new() { { "description", (a, n) => { a.Description = n.GetScalarValue(); } }, - { "servers", (a, n) => { a.Servers = n.CreateSimpleList(s => new AsyncApiServerReference("#/servers/" + s.GetScalarValue())); } }, + { "servers", (a, n) => { a.Servers = n.CreateSimpleList(s => new AsyncApiServerReference(GetServerReferenceKey(s))); } }, { "subscribe", (a, n) => { /* happens after initial reading */ } }, { "publish", (a, n) => { /* happens after initial reading */ } }, { "parameters", (a, n) => { a.Parameters = n.CreateMap(LoadParameter); } }, { "bindings", (a, n) => { a.Bindings = LoadChannelBindings(n); } }, }; + private static string GetServerReferenceKey(ValueNode valueNode) + { + var stringValue = valueNode.GetScalarValue(); + return stringValue.StartsWith("#/servers/") ? stringValue : "#/servers/" + stringValue; + } + private static readonly PatternFieldMap ChannelPatternFields = new() { diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiDocumentDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiDocumentDeserializer.cs index 38890a0d..f032ab69 100644 --- a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiDocumentDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiDocumentDeserializer.cs @@ -8,7 +8,7 @@ internal static partial class AsyncApiV3Deserializer { private static FixedFieldMap asyncApiFixedFields = new() { - { "asyncapi", (a, n) => { a.Asyncapi = "3.0.0"; } }, + { "asyncapi", (a, n) => { a.Asyncapi = "3.1.0"; } }, { "id", (a, n) => a.Id = n.GetScalarValue() }, { "info", (a, n) => a.Info = LoadInfo(n) }, { "servers", (a, n) => a.Servers = n.CreateMap(LoadServer) }, diff --git a/src/ByteBard.AsyncAPI/AsyncApiWorkspace.cs b/src/ByteBard.AsyncAPI/AsyncApiWorkspace.cs index 94f4c23a..3f488939 100644 --- a/src/ByteBard.AsyncAPI/AsyncApiWorkspace.cs +++ b/src/ByteBard.AsyncAPI/AsyncApiWorkspace.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.IO; + using System.Linq; using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Models.Interfaces; @@ -13,6 +14,23 @@ public class AsyncApiWorkspace public AsyncApiDocument RootDocument { get; private set; } + /// + /// Stack for tracking parent context during serialization. + /// Allows bindings to access their parent operation, channel, or message. + /// + public Stack SerializationContext { get; } = new(); + + /// + /// Gets the first item of the specified type from the serialization context. + /// + /// The type to find in the context. + /// The first matching item, or default if not found. + public T GetSerializationContext() + where T : class + { + return this.SerializationContext.OfType().FirstOrDefault(); + } + public void RegisterComponents(AsyncApiDocument document) { if (document?.Components == null) @@ -186,6 +204,24 @@ public void RegisterComponents(AsyncApiDocument document) this.RegisterComponent(location + "/messages/" + message.Key, message.Value); } } + + string serverBaseUri = "#/servers/"; + foreach (var server in document.Servers) + { + var registerableServerValue = server.Value; + if (server.Value is IAsyncApiReferenceable serverReference) + { + if (serverReference.Reference.IsExternal) + { + continue; + } + + registerableServerValue = this.ResolveReference(serverReference.Reference); + } + + location = serverBaseUri + server.Key; + this.RegisterComponent(location, registerableServerValue); + } } public bool RegisterComponent(string location, T component) diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiBinding.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiBinding.cs index 22730d76..68287a86 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiBinding.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiBinding.cs @@ -13,25 +13,9 @@ public abstract class AsyncApiBinding : IBinding public string BindingVersion { get; set; } - public void SerializeV2(IAsyncApiWriter writer) - { - this.SerializeCore(writer); - } + public abstract void SerializeV2(IAsyncApiWriter writer); - public void SerializeV3(IAsyncApiWriter writer) - { - this.SerializeCore(writer); - } - - private void SerializeCore(IAsyncApiWriter writer) - { - if (writer is null) - { - throw new ArgumentNullException(nameof(writer)); - } - - this.SerializeProperties(writer); - } + public abstract void SerializeV3(IAsyncApiWriter writer); public abstract void SerializeProperties(IAsyncApiWriter writer); } diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiBindings{TBinding}.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiBindings{TBinding}.cs index 2bebd67f..4a2149be 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiBindings{TBinding}.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiBindings{TBinding}.cs @@ -13,15 +13,28 @@ public class AsyncApiBindings : IDictionary, IAsyncA public virtual void SerializeV2(IAsyncApiWriter writer) { - this.SerializeCore(writer); - } + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } - public virtual void SerializeV3(IAsyncApiWriter writer) - { - this.SerializeCore(writer); + writer.WriteStartObject(); + + foreach (var binding in this) + { + var bindingType = binding.Key; + var bindingValue = binding.Value; + + writer.WritePropertyName(bindingType); + + bindingValue.SerializeV2(writer); + } + + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); } - private void SerializeCore(IAsyncApiWriter writer) + public virtual void SerializeV3(IAsyncApiWriter writer) { if (writer is null) { @@ -37,7 +50,7 @@ private void SerializeCore(IAsyncApiWriter writer) writer.WritePropertyName(bindingType); - bindingValue.SerializeV2(writer); + bindingValue.SerializeV3(writer); } writer.WriteExtensions(this.Extensions); diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiChannel.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiChannel.cs index 945be14f..87b0a25b 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiChannel.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiChannel.cs @@ -75,41 +75,49 @@ public virtual void SerializeV2(IAsyncApiWriter writer) throw new ArgumentNullException(nameof(writer)); } - writer.WriteStartObject(); + writer.Workspace?.SerializationContext.Push(this); + try + { + writer.WriteStartObject(); - // description - writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description); + // description + writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description); - // servers - writer.WriteOptionalCollection(AsyncApiConstants.Servers, this.Servers.Select(s => s.Reference.FragmentId).ToList(), (w, s) => w.WriteValue(s)); + // servers + writer.WriteOptionalCollection(AsyncApiConstants.Servers, this.Servers.Select(s => s.Reference.FragmentId).ToList(), (w, s) => w.WriteValue(s)); - var operations = writer.Workspace.RootDocument?.Operations.Values.Where(operation => CheckOperationChannel(operation, writer)).ToList(); + var operations = writer.Workspace.RootDocument?.Operations.Values.Where(operation => CheckOperationChannel(operation, writer)).ToList(); - // subscribe (Now Send) - writer.WriteOptionalObject(AsyncApiConstants.Subscribe, operations?.FirstOrDefault(o => o.Action == AsyncApiAction.Send), (w, s) => s?.SerializeV2(w)); + // subscribe (Now Send) + writer.WriteOptionalObject(AsyncApiConstants.Subscribe, operations?.FirstOrDefault(o => o.Action == AsyncApiAction.Send), (w, s) => s?.SerializeV2(w)); - // publish (Now Receive) - writer.WriteOptionalObject(AsyncApiConstants.Publish, operations?.FirstOrDefault(o => o.Action == AsyncApiAction.Receive), (w, s) => s?.SerializeV2(w)); + // publish (Now Receive) + writer.WriteOptionalObject(AsyncApiConstants.Publish, operations?.FirstOrDefault(o => o.Action == AsyncApiAction.Receive), (w, s) => s?.SerializeV2(w)); - // parameters - writer.WriteOptionalMap(AsyncApiConstants.Parameters, this.Parameters, (writer, key, component) => - { - if (component is AsyncApiParameterReference reference) + // parameters + writer.WriteOptionalMap(AsyncApiConstants.Parameters, this.Parameters, (writer, key, component) => { - reference.SerializeV2(writer); - } - else - { - component.SerializeV2(writer); - } - }); - - writer.WriteOptionalObject(AsyncApiConstants.Bindings, this.Bindings, (w, t) => t.SerializeV2(w)); - - // extensions - writer.WriteExtensions(this.Extensions); - - writer.WriteEndObject(); + if (component is AsyncApiParameterReference reference) + { + reference.SerializeV2(writer); + } + else + { + component.SerializeV2(writer); + } + }); + + writer.WriteOptionalObject(AsyncApiConstants.Bindings, this.Bindings, (w, t) => t.SerializeV2(w)); + + // extensions + writer.WriteExtensions(this.Extensions); + + writer.WriteEndObject(); + } + finally + { + writer.Workspace?.SerializationContext.Pop(); + } } public virtual void SerializeV3(IAsyncApiWriter writer) @@ -119,25 +127,33 @@ public virtual void SerializeV3(IAsyncApiWriter writer) throw new ArgumentNullException(nameof(writer)); } - writer.WriteStartObject(); - - writer.WriteOptionalProperty(AsyncApiConstants.Address, this.Address); - writer.WriteRequiredMap(AsyncApiConstants.Messages, this.Messages, (w, k, m) => m.SerializeV3(w)); - writer.WriteOptionalProperty(AsyncApiConstants.Title, this.Title); - writer.WriteOptionalProperty(AsyncApiConstants.Summary, this.Summary); - writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description); - writer.WriteOptionalCollection(AsyncApiConstants.Servers, this.Servers, (w, s) => s.Reference.SerializeV3(w)); - if (this.Address.IsChannelAddressExpression()) + writer.Workspace?.SerializationContext.Push(this); + try { - writer.WriteOptionalMap(AsyncApiConstants.Parameters, this.Parameters, (w, key, p) => p.SerializeV3(w)); - } + writer.WriteStartObject(); + + writer.WriteOptionalProperty(AsyncApiConstants.Address, this.Address); + writer.WriteRequiredMap(AsyncApiConstants.Messages, this.Messages, (w, k, m) => m.SerializeV3(w)); + writer.WriteOptionalProperty(AsyncApiConstants.Title, this.Title); + writer.WriteOptionalProperty(AsyncApiConstants.Summary, this.Summary); + writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description); + writer.WriteOptionalCollection(AsyncApiConstants.Servers, this.Servers, (w, s) => s.Reference.SerializeV3(w)); + if (this.Address.IsChannelAddressExpression()) + { + writer.WriteOptionalMap(AsyncApiConstants.Parameters, this.Parameters, (w, key, p) => p.SerializeV3(w)); + } - writer.WriteOptionalCollection(AsyncApiConstants.Tags, this.Tags, (w, t) => t.SerializeV3(w)); - writer.WriteOptionalObject(AsyncApiConstants.ExternalDocs, this.ExternalDocs, (w, s) => s.SerializeV2(w)); - writer.WriteOptionalObject(AsyncApiConstants.Bindings, this.Bindings, (w, t) => t.SerializeV2(w)); - writer.WriteExtensions(this.Extensions); + writer.WriteOptionalCollection(AsyncApiConstants.Tags, this.Tags, (w, t) => t.SerializeV3(w)); + writer.WriteOptionalObject(AsyncApiConstants.ExternalDocs, this.ExternalDocs, (w, s) => s.SerializeV3(w)); + writer.WriteOptionalObject(AsyncApiConstants.Bindings, this.Bindings, (w, t) => t.SerializeV3(w)); + writer.WriteExtensions(this.Extensions); - writer.WriteEndObject(); + writer.WriteEndObject(); + } + finally + { + writer.Workspace?.SerializationContext.Pop(); + } } private bool CheckOperationChannel(AsyncApiOperation operation, IAsyncApiWriter writer) diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiConstants.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiConstants.cs index 1f3d84e8..d38469c5 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiConstants.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiConstants.cs @@ -114,6 +114,7 @@ public static class AsyncApiConstants public const string SchemaFormat = "schemaFormat"; public const string ContentType = "contentType"; public const string BindingVersion = "bindingVersion"; + public const string StatusCode = "statusCode"; public const string Key = "key"; public const string Method = "method"; public const string SchemaIdPayloadEncoding = "schemaIdPayloadEncoding"; diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiDocument.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiDocument.cs index 6af7345c..d489f69b 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiDocument.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiDocument.cs @@ -147,7 +147,7 @@ public void SerializeV3(IAsyncApiWriter writer) writer.WriteStartObject(); // asyncApi - writer.WriteRequiredProperty(AsyncApiConstants.AsyncApi, "3.0.0"); + writer.WriteRequiredProperty(AsyncApiConstants.AsyncApi, "3.1.0"); // info writer.WriteRequiredObject(AsyncApiConstants.Info, this.Info, (w, i) => i.SerializeV3(w)); diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiMessage.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiMessage.cs index 3f5adf70..05a168e8 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiMessage.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiMessage.cs @@ -88,25 +88,33 @@ public virtual void SerializeV2(IAsyncApiWriter writer) throw new ArgumentNullException(nameof(writer)); } - writer.WriteStartObject(); - writer.WriteOptionalObject(AsyncApiConstants.Headers, this.Headers, (w, h) => h.Schema.SerializeV2(w)); - writer.WriteOptionalObject(AsyncApiConstants.Payload, this.Payload, (w, p) => p.SerializeV2(w)); - writer.WriteOptionalObject(AsyncApiConstants.CorrelationId, this.CorrelationId, (w, c) => c.SerializeV2(w)); - writer.WriteOptionalProperty(AsyncApiConstants.SchemaFormat, this.Payload?.SchemaFormat); - writer.WriteOptionalProperty(AsyncApiConstants.ContentType, this.ContentType); - writer.WriteOptionalProperty(AsyncApiConstants.Name, this.Name); - writer.WriteOptionalProperty(AsyncApiConstants.Title, this.Title); - writer.WriteOptionalProperty(AsyncApiConstants.Summary, this.Summary); - writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description); - writer.WriteOptionalCollection(AsyncApiConstants.Tags, this.Tags, (w, t) => t.SerializeV2(w)); - writer.WriteOptionalObject(AsyncApiConstants.ExternalDocs, this.ExternalDocs, (w, e) => e.SerializeV2(w)); - - writer.WriteOptionalObject(AsyncApiConstants.Bindings, this.Bindings, (w, t) => t.SerializeV2(w)); - writer.WriteOptionalCollection(AsyncApiConstants.Examples, this.Examples, (w, e) => e.SerializeV2(w)); - - writer.WriteOptionalCollection(AsyncApiConstants.Traits, this.Traits, (w, t) => t.SerializeV2(w)); - writer.WriteExtensions(this.Extensions); - writer.WriteEndObject(); + writer.Workspace?.SerializationContext.Push(this); + try + { + writer.WriteStartObject(); + writer.WriteOptionalObject(AsyncApiConstants.Headers, this.Headers, (w, h) => h.Schema.SerializeV2(w)); + writer.WriteOptionalObject(AsyncApiConstants.Payload, this.Payload, (w, p) => p.SerializeV2(w)); + writer.WriteOptionalObject(AsyncApiConstants.CorrelationId, this.CorrelationId, (w, c) => c.SerializeV2(w)); + writer.WriteOptionalProperty(AsyncApiConstants.SchemaFormat, this.Payload?.SchemaFormat); + writer.WriteOptionalProperty(AsyncApiConstants.ContentType, this.ContentType); + writer.WriteOptionalProperty(AsyncApiConstants.Name, this.Name); + writer.WriteOptionalProperty(AsyncApiConstants.Title, this.Title); + writer.WriteOptionalProperty(AsyncApiConstants.Summary, this.Summary); + writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description); + writer.WriteOptionalCollection(AsyncApiConstants.Tags, this.Tags, (w, t) => t.SerializeV2(w)); + writer.WriteOptionalObject(AsyncApiConstants.ExternalDocs, this.ExternalDocs, (w, e) => e.SerializeV2(w)); + + writer.WriteOptionalObject(AsyncApiConstants.Bindings, this.Bindings, (w, t) => t.SerializeV2(w)); + writer.WriteOptionalCollection(AsyncApiConstants.Examples, this.Examples, (w, e) => e.SerializeV2(w)); + + writer.WriteOptionalCollection(AsyncApiConstants.Traits, this.Traits, (w, t) => t.SerializeV2(w)); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + finally + { + writer.Workspace?.SerializationContext.Pop(); + } } public virtual void SerializeV3(IAsyncApiWriter writer) @@ -116,24 +124,32 @@ public virtual void SerializeV3(IAsyncApiWriter writer) throw new ArgumentNullException(nameof(writer)); } - writer.WriteStartObject(); - writer.WriteOptionalObject(AsyncApiConstants.Headers, this.Headers, (w, h) => h.SerializeV3(w)); - writer.WriteOptionalObject(AsyncApiConstants.Payload, this.Payload, (w, p) => p.SerializeV3(w)); - writer.WriteOptionalObject(AsyncApiConstants.CorrelationId, this.CorrelationId, (w, c) => c.SerializeV3(w)); - writer.WriteOptionalProperty(AsyncApiConstants.ContentType, this.ContentType); - writer.WriteOptionalProperty(AsyncApiConstants.Name, this.Name); - writer.WriteOptionalProperty(AsyncApiConstants.Title, this.Title); - writer.WriteOptionalProperty(AsyncApiConstants.Summary, this.Summary); - writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description); - writer.WriteOptionalCollection(AsyncApiConstants.Tags, this.Tags, (w, t) => t.SerializeV3(w)); - writer.WriteOptionalObject(AsyncApiConstants.ExternalDocs, this.ExternalDocs, (w, e) => e.SerializeV3(w)); - - writer.WriteOptionalObject(AsyncApiConstants.Bindings, this.Bindings, (w, t) => t.SerializeV3(w)); - writer.WriteOptionalCollection(AsyncApiConstants.Examples, this.Examples, (w, e) => e.SerializeV3(w)); - - writer.WriteOptionalCollection(AsyncApiConstants.Traits, this.Traits, (w, t) => t.SerializeV3(w)); - writer.WriteExtensions(this.Extensions); - writer.WriteEndObject(); + writer.Workspace?.SerializationContext.Push(this); + try + { + writer.WriteStartObject(); + writer.WriteOptionalObject(AsyncApiConstants.Headers, this.Headers, (w, h) => h.SerializeV3(w)); + writer.WriteOptionalObject(AsyncApiConstants.Payload, this.Payload, (w, p) => p.SerializeV3(w)); + writer.WriteOptionalObject(AsyncApiConstants.CorrelationId, this.CorrelationId, (w, c) => c.SerializeV3(w)); + writer.WriteOptionalProperty(AsyncApiConstants.ContentType, this.ContentType); + writer.WriteOptionalProperty(AsyncApiConstants.Name, this.Name); + writer.WriteOptionalProperty(AsyncApiConstants.Title, this.Title); + writer.WriteOptionalProperty(AsyncApiConstants.Summary, this.Summary); + writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description); + writer.WriteOptionalCollection(AsyncApiConstants.Tags, this.Tags, (w, t) => t.SerializeV3(w)); + writer.WriteOptionalObject(AsyncApiConstants.ExternalDocs, this.ExternalDocs, (w, e) => e.SerializeV3(w)); + + writer.WriteOptionalObject(AsyncApiConstants.Bindings, this.Bindings, (w, t) => t.SerializeV3(w)); + writer.WriteOptionalCollection(AsyncApiConstants.Examples, this.Examples, (w, e) => e.SerializeV3(w)); + + writer.WriteOptionalCollection(AsyncApiConstants.Traits, this.Traits, (w, t) => t.SerializeV3(w)); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + finally + { + writer.Workspace?.SerializationContext.Pop(); + } } } } \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiOperation.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiOperation.cs index d7ea083c..9b04d306 100644 --- a/src/ByteBard.AsyncAPI/Models/AsyncApiOperation.cs +++ b/src/ByteBard.AsyncAPI/Models/AsyncApiOperation.cs @@ -80,33 +80,41 @@ public virtual void SerializeV2(IAsyncApiWriter writer) throw new ArgumentNullException(nameof(writer)); } - writer.WriteStartObject(); - - // writer.WriteOptionalProperty(AsyncApiConstants.OperationId, this.OperationId); - writer.WriteOptionalProperty(AsyncApiConstants.Summary, this.Summary); - writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description); - writer.WriteOptionalCollection(AsyncApiConstants.Security, this.Security, (w, t) => this.SerializeAsSecurityRequirement(t, w)); - writer.WriteOptionalCollection(AsyncApiConstants.Tags, this.Tags, (w, t) => t.SerializeV2(w)); - writer.WriteOptionalObject(AsyncApiConstants.ExternalDocs, this.ExternalDocs, (w, e) => e.SerializeV2(w)); - - writer.WriteOptionalObject(AsyncApiConstants.Bindings, this.Bindings, (w, t) => t.SerializeV2(w)); - writer.WriteOptionalCollection(AsyncApiConstants.Traits, this.Traits, (w, t) => t.SerializeV2(w)); - IEnumerable messages = this.Messages.Any() ? this.Messages : this.Channel?.Messages.Values; - - if (messages?.Count() > 1) + writer.Workspace?.SerializationContext.Push(this); + try { - writer.WritePropertyName(AsyncApiConstants.Message); writer.WriteStartObject(); - writer.WriteOptionalCollection(AsyncApiConstants.OneOf, messages, (w, t) => t.SerializeV2(w)); + + // writer.WriteOptionalProperty(AsyncApiConstants.OperationId, this.OperationId); + writer.WriteOptionalProperty(AsyncApiConstants.Summary, this.Summary); + writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description); + writer.WriteOptionalCollection(AsyncApiConstants.Security, this.Security, (w, t) => this.SerializeAsSecurityRequirement(t, w)); + writer.WriteOptionalCollection(AsyncApiConstants.Tags, this.Tags, (w, t) => t.SerializeV2(w)); + writer.WriteOptionalObject(AsyncApiConstants.ExternalDocs, this.ExternalDocs, (w, e) => e.SerializeV2(w)); + + writer.WriteOptionalObject(AsyncApiConstants.Bindings, this.Bindings, (w, t) => t.SerializeV2(w)); + writer.WriteOptionalCollection(AsyncApiConstants.Traits, this.Traits, (w, t) => t.SerializeV2(w)); + IEnumerable messages = this.Messages.Any() ? this.Messages : this.Channel?.Messages.Values; + + if (messages?.Count() > 1) + { + writer.WritePropertyName(AsyncApiConstants.Message); + writer.WriteStartObject(); + writer.WriteOptionalCollection(AsyncApiConstants.OneOf, messages, (w, t) => t.SerializeV2(w)); + writer.WriteEndObject(); + } + else + { + writer.WriteOptionalObject(AsyncApiConstants.Message, messages?.FirstOrDefault(), (w, m) => m.SerializeV2(w)); + } + + writer.WriteExtensions(this.Extensions); writer.WriteEndObject(); } - else + finally { - writer.WriteOptionalObject(AsyncApiConstants.Message, messages?.FirstOrDefault(), (w, m) => m.SerializeV2(w)); + writer.Workspace?.SerializationContext.Pop(); } - - writer.WriteExtensions(this.Extensions); - writer.WriteEndObject(); } public virtual void SerializeV3(IAsyncApiWriter writer) @@ -116,21 +124,29 @@ public virtual void SerializeV3(IAsyncApiWriter writer) throw new ArgumentNullException(nameof(writer)); } - writer.WriteStartObject(); - writer.WriteRequiredProperty(AsyncApiConstants.Action, this.Action.GetDisplayName()); - writer.WriteRequiredObject(AsyncApiConstants.Channel, this.Channel, (w, c) => c.Reference.SerializeV3(w)); - writer.WriteOptionalProperty(AsyncApiConstants.Title, this.Title); - writer.WriteOptionalProperty(AsyncApiConstants.Summary, this.Summary); - writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description); - writer.WriteOptionalCollection(AsyncApiConstants.Security, this.Security, (w, t) => t.SerializeV3(w)); - writer.WriteOptionalCollection(AsyncApiConstants.Tags, this.Tags, (w, t) => t.SerializeV3(w)); - writer.WriteOptionalObject(AsyncApiConstants.ExternalDocs, this.ExternalDocs, (w, e) => e.SerializeV3(w)); - writer.WriteOptionalObject(AsyncApiConstants.Bindings, this.Bindings, (w, t) => t.SerializeV3(w)); - writer.WriteOptionalCollection(AsyncApiConstants.Traits, this.Traits, (w, t) => t.SerializeV3(w)); - writer.WriteOptionalCollection(AsyncApiConstants.Messages, this.Messages, (w, m) => m.Reference.SerializeV3(w)); - writer.WriteOptionalObject(AsyncApiConstants.Reply, this.Reply, (w, t) => t.SerializeV3(w)); - writer.WriteExtensions(this.Extensions); - writer.WriteEndObject(); + writer.Workspace?.SerializationContext.Push(this); + try + { + writer.WriteStartObject(); + writer.WriteRequiredProperty(AsyncApiConstants.Action, this.Action.GetDisplayName()); + writer.WriteRequiredObject(AsyncApiConstants.Channel, this.Channel, (w, c) => c.Reference.SerializeV3(w)); + writer.WriteOptionalProperty(AsyncApiConstants.Title, this.Title); + writer.WriteOptionalProperty(AsyncApiConstants.Summary, this.Summary); + writer.WriteOptionalProperty(AsyncApiConstants.Description, this.Description); + writer.WriteOptionalCollection(AsyncApiConstants.Security, this.Security, (w, t) => t.SerializeV3(w)); + writer.WriteOptionalCollection(AsyncApiConstants.Tags, this.Tags, (w, t) => t.SerializeV3(w)); + writer.WriteOptionalObject(AsyncApiConstants.ExternalDocs, this.ExternalDocs, (w, e) => e.SerializeV3(w)); + writer.WriteOptionalObject(AsyncApiConstants.Bindings, this.Bindings, (w, t) => t.SerializeV3(w)); + writer.WriteOptionalCollection(AsyncApiConstants.Traits, this.Traits, (w, t) => t.SerializeV3(w)); + writer.WriteOptionalCollection(AsyncApiConstants.Messages, this.Messages, (w, m) => m.Reference.SerializeV3(w)); + writer.WriteOptionalObject(AsyncApiConstants.Reply, this.Reply, (w, t) => t.SerializeV3(w)); + writer.WriteExtensions(this.Extensions); + writer.WriteEndObject(); + } + finally + { + writer.Workspace?.SerializationContext.Pop(); + } } private void SerializeAsSecurityRequirement(AsyncApiSecurityScheme scheme, IAsyncApiWriter w) diff --git a/src/ByteBard.AsyncAPI/Models/Avro/AsyncApiAvroSchema.cs b/src/ByteBard.AsyncAPI/Models/Avro/AsyncApiAvroSchema.cs index bf847dee..1ba1de5b 100644 --- a/src/ByteBard.AsyncAPI/Models/Avro/AsyncApiAvroSchema.cs +++ b/src/ByteBard.AsyncAPI/Models/Avro/AsyncApiAvroSchema.cs @@ -1,5 +1,6 @@ namespace ByteBard.AsyncAPI.Models { + using System; using System.Collections.Generic; using ByteBard.AsyncAPI.Models.Interfaces; using ByteBard.AsyncAPI.Writers; @@ -18,6 +19,27 @@ public static implicit operator AsyncApiAvroSchema(AvroPrimitiveType type) return new AvroPrimitive(type); } + public static explicit operator AvroPrimitiveType(AsyncApiAvroSchema schema) + { + if (schema is AvroPrimitive primitive) + { + return primitive.Type switch + { + "null" => AvroPrimitiveType.Null, + "boolean" => AvroPrimitiveType.Boolean, + "int" => AvroPrimitiveType.Int, + "long" => AvroPrimitiveType.Long, + "float" => AvroPrimitiveType.Float, + "double" => AvroPrimitiveType.Double, + "bytes" => AvroPrimitiveType.Bytes, + "string" => AvroPrimitiveType.String, + _ => throw new InvalidCastException($"Avro schema type '{primitive.Type}' is not a primitive type."), + }; + } + + throw new InvalidCastException($"Avro schema type '{schema?.Type}' is not a primitive type."); + } + public abstract void SerializeV2(IAsyncApiWriter writer); public abstract void SerializeV3(IAsyncApiWriter writer); @@ -41,4 +63,4 @@ public virtual T As() return this as T; } } -} \ No newline at end of file +} diff --git a/src/ByteBard.AsyncAPI/Models/Avro/AvroMap.cs b/src/ByteBard.AsyncAPI/Models/Avro/AvroMap.cs index 3799c7ac..55db22bd 100644 --- a/src/ByteBard.AsyncAPI/Models/Avro/AvroMap.cs +++ b/src/ByteBard.AsyncAPI/Models/Avro/AvroMap.cs @@ -1,5 +1,6 @@ namespace ByteBard.AsyncAPI.Models { + using System; using System.Collections.Generic; using System.Linq; using ByteBard.AsyncAPI.Writers; @@ -8,7 +9,7 @@ public class AvroMap : AsyncApiAvroSchema { public override string Type { get; } = "map"; - public AvroPrimitiveType Values { get; set; } + public AsyncApiAvroSchema Values { get; set; } /// /// A map of properties not in the schema, but added as additional metadata. @@ -17,19 +18,24 @@ public class AvroMap : AsyncApiAvroSchema public override void SerializeV2(IAsyncApiWriter writer) { - this.SerializeCore(writer); + this.SerializeCore(writer, (w, s) => s.SerializeV2(w)); } public override void SerializeV3(IAsyncApiWriter writer) { - this.SerializeCore(writer); + this.SerializeCore(writer, (w, s) => s.SerializeV3(w)); } public void SerializeCore(IAsyncApiWriter writer) + { + this.SerializeCore(writer, (w, s) => s.SerializeV2(w)); + } + + private void SerializeCore(IAsyncApiWriter writer, Action action) { writer.WriteStartObject(); writer.WriteOptionalProperty("type", this.Type); - writer.WriteRequiredProperty("values", this.Values.GetDisplayName()); + writer.WriteRequiredObject("values", this.Values, action); if (this.Metadata.Any()) { foreach (var item in this.Metadata) @@ -49,4 +55,4 @@ public void SerializeCore(IAsyncApiWriter writer) writer.WriteEndObject(); } } -} \ No newline at end of file +} diff --git a/src/ByteBard.AsyncAPI/Models/Avro/AvroNamedType.cs b/src/ByteBard.AsyncAPI/Models/Avro/AvroNamedType.cs new file mode 100644 index 00000000..1f53a2e7 --- /dev/null +++ b/src/ByteBard.AsyncAPI/Models/Avro/AvroNamedType.cs @@ -0,0 +1,74 @@ +namespace ByteBard.AsyncAPI.Models +{ + using System.Collections.Generic; + using ByteBard.AsyncAPI.Writers; + + public class AvroNamedType : AsyncApiAvroSchema + { + private IDictionary metadata = new Dictionary(); + + public AvroNamedType(string name, AsyncApiAvroSchema target = null) + { + this.Name = name; + this.Target = target; + } + + public string Name { get; set; } + + public AsyncApiAvroSchema Target { get; set; } + + public override string Type => this.Name; + + public override IDictionary Metadata + { + get => this.Target?.Metadata ?? this.metadata; + set + { + if (this.Target != null) + { + this.Target.Metadata = value; + return; + } + + this.metadata = value ?? new Dictionary(); + } + } + + public override T As() + { + var result = base.As(); + return result ?? this.Target?.As(); + } + + public override bool Is() + { + return base.Is() || this.Target?.Is() == true; + } + + public override bool TryGetAs(out T result) + { + if (base.TryGetAs(out result)) + { + return true; + } + + if (this.Target != null) + { + return this.Target.TryGetAs(out result); + } + + result = default; + return false; + } + + public override void SerializeV2(IAsyncApiWriter writer) + { + writer.WriteValue(this.Name); + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + writer.WriteValue(this.Name); + } + } +} diff --git a/src/ByteBard.AsyncAPI/Services/AsyncApiWalker.cs b/src/ByteBard.AsyncAPI/Services/AsyncApiWalker.cs index fbb72710..5fc5e7ad 100644 --- a/src/ByteBard.AsyncAPI/Services/AsyncApiWalker.cs +++ b/src/ByteBard.AsyncAPI/Services/AsyncApiWalker.cs @@ -9,6 +9,7 @@ public class AsyncApiWalker { private readonly AsyncApiVisitorBase visitor; private readonly Stack schemaLoop = new(); + private readonly Stack avroSchemaLoop = new(); public AsyncApiWalker(AsyncApiVisitorBase visitor) { @@ -23,6 +24,7 @@ public void Walk(AsyncApiDocument doc) } this.schemaLoop.Clear(); + this.avroSchemaLoop.Clear(); this.visitor.Visit(doc); @@ -391,13 +393,53 @@ internal void Walk(IAsyncApiSchema payload) internal void Walk(AsyncApiAvroSchema schema) { + if (schema == null) + { + return; + } + if (schema is AsyncApiAvroSchemaReference reference) { this.Walk(reference as IAsyncApiReferenceable); return; } + if (this.avroSchemaLoop.Contains(schema)) + { + return; + } + + this.avroSchemaLoop.Push(schema); + this.visitor.Visit(schema); + + switch (schema) + { + case AvroRecord record: + this.Walk("fields", () => + { + foreach (var field in record.Fields) + { + this.Walk(field.Name, () => this.Walk("type", () => this.Walk(field.Type))); + } + }); + break; + case AvroArray array: + this.Walk("items", () => this.Walk(array.Items)); + break; + case AvroMap map: + this.Walk("values", () => this.Walk(map.Values)); + break; + case AvroUnion union: + foreach (var type in union.Types) + { + this.Walk("types", () => this.Walk(type)); + } + + break; + } + + this.avroSchemaLoop.Pop(); } internal void Walk(AsyncApiJsonSchema schema) @@ -587,8 +629,15 @@ private void Walk(AsyncApiOperationReply reply) this.visitor.Visit(reply); - this.Walk(reply.Address); - this.Walk(reply.Channel as IAsyncApiReferenceable); + if (reply.Address != null) + { + this.Walk(reply.Address); + } + + if (reply.Channel != null) + { + this.Walk(reply.Channel as IAsyncApiReferenceable); + } foreach (var message in reply.Messages) { diff --git a/src/ByteBard.AsyncAPI/Validation/AsyncApiValidator.cs b/src/ByteBard.AsyncAPI/Validation/AsyncApiValidator.cs index a34f9968..61a6e36f 100644 --- a/src/ByteBard.AsyncAPI/Validation/AsyncApiValidator.cs +++ b/src/ByteBard.AsyncAPI/Validation/AsyncApiValidator.cs @@ -14,6 +14,7 @@ public class AsyncApiValidator : AsyncApiVisitorBase, IValidationContext private readonly ValidationRuleSet ruleSet; private readonly IList errors = new List(); private readonly IList warnings = new List(); + private AsyncApiVersion? documentVersion; /// /// Create a vistor that will validate an AsyncApiDocument. @@ -23,6 +24,7 @@ public AsyncApiValidator(ValidationRuleSet ruleSet, AsyncApiDocument rootDocumen { this.ruleSet = ruleSet; this.RootDocument = rootDocument; + this.documentVersion = this.GetDocumentVersion(); } public AsyncApiDocument RootDocument { get; } @@ -198,11 +200,26 @@ private void Validate(object item, Type type) type = typeof(IAsyncApiReferenceable); } - var rules = this.ruleSet.FindRules(type); + var rules = this.ruleSet.FindRules(type, this.documentVersion); foreach (var rule in rules) { rule.Evaluate(this as IValidationContext, item); } } + + private AsyncApiVersion? GetDocumentVersion() + { + if (this.RootDocument?.Asyncapi?.StartsWith("2") == true) + { + return AsyncApiVersion.AsyncApi2_0; + } + + if (this.RootDocument?.Asyncapi?.StartsWith("3") == true) + { + return AsyncApiVersion.AsyncApi3_0; + } + + return null; + } } } diff --git a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiAvroRules.cs b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiAvroRules.cs index 78cf1c34..0a289be5 100644 --- a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiAvroRules.cs +++ b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiAvroRules.cs @@ -58,5 +58,17 @@ public static class AsyncApiMessagePayloadRules context.Exit(); }); + + public static ValidationRule NamedTypeMustResolve => + new ValidationRule( + (context, schema) => + { + if (schema is AvroNamedType namedType && namedType.Target == null) + { + context.CreateWarning( + nameof(NamedTypeMustResolve), + $"Avro named type '{namedType.Name}' is referenced but was not defined before use."); + } + }); } } diff --git a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs index f1f8f4f6..b96f94d2 100644 --- a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs +++ b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs @@ -1,4 +1,4 @@ -namespace ByteBard.AsyncAPI.Validation.Rules +namespace ByteBard.AsyncAPI.Validation.Rules { using System; using System.Collections.Generic; @@ -75,6 +75,22 @@ public static class AsyncApiDocumentRules } } + context.Exit(); + }); + + [AsyncApiVersionRule(AsyncApiVersion.AsyncApi2_0)] + public static ValidationRule V2ChannelsRequired => + new ValidationRule( + (context, document) => + { + context.Enter("channels"); + if (document.Channels == null || document.Channels.Count == 0) + { + context.CreateError( + nameof(DocumentRequiredFields), + string.Format(Resource.Validation_FieldRequired, "channels", "document")); + } + context.Exit(); }); } diff --git a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiOperationRules.cs b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiOperationRules.cs index 85548b82..60be46e8 100644 --- a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiOperationRules.cs +++ b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiOperationRules.cs @@ -10,6 +10,7 @@ namespace ByteBard.AsyncAPI.Validation.Rules [AsyncApiRule] public static class AsyncApiOperationRules { + [AsyncApiVersionRule(AsyncApiVersion.AsyncApi3_0)] public static ValidationRule OperationRequiredFields => new ValidationRule( (context, operation) => @@ -35,6 +36,7 @@ public static class AsyncApiOperationRules context.Exit(); }); + [AsyncApiVersionRule(AsyncApiVersion.AsyncApi3_0)] public static ValidationRule OperationChannelReference => new ValidationRule( (context, operation) => @@ -56,6 +58,7 @@ public static class AsyncApiOperationRules } }); + [AsyncApiVersionRule(AsyncApiVersion.AsyncApi3_0)] public static ValidationRule OperationMessages => new ValidationRule( (context, operation) => diff --git a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiSecuritySchemaRules.cs b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiSecuritySchemaRules.cs index 821178c2..7529cd87 100644 --- a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiSecuritySchemaRules.cs +++ b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiSecuritySchemaRules.cs @@ -96,7 +96,7 @@ private static bool IsFieldRequired(this AsyncApiSecurityScheme sc, string field private static readonly Dictionary> RequiredFieldsByType = new() { - { "name", sc => sc.Type is SecuritySchemeType.ApiKey }, + { "name", sc => sc.Type is SecuritySchemeType.HttpApiKey }, { "in", sc => sc.Type is SecuritySchemeType.ApiKey or SecuritySchemeType.HttpApiKey }, { "scheme", sc => sc.Type is SecuritySchemeType.Http }, { "flows", sc => sc.Type is SecuritySchemeType.OAuth2 }, diff --git a/src/ByteBard.AsyncAPI/Validation/ValidationRule.cs b/src/ByteBard.AsyncAPI/Validation/ValidationRule.cs index db4187da..ba654268 100644 --- a/src/ByteBard.AsyncAPI/Validation/ValidationRule.cs +++ b/src/ByteBard.AsyncAPI/Validation/ValidationRule.cs @@ -12,6 +12,12 @@ public abstract class ValidationRule /// internal abstract Type ElementType { get; } + /// + /// Gets or sets the AsyncAPI versions this rule applies to. + /// Null means the rule applies to all versions. + /// + public AsyncApiVersion[] ApplicableVersions { get; internal set; } + /// /// Validate the object. /// diff --git a/src/ByteBard.AsyncAPI/Validation/ValidationRuleSet.cs b/src/ByteBard.AsyncAPI/Validation/ValidationRuleSet.cs index 46b7181c..8d70cbac 100644 --- a/src/ByteBard.AsyncAPI/Validation/ValidationRuleSet.cs +++ b/src/ByteBard.AsyncAPI/Validation/ValidationRuleSet.cs @@ -30,6 +30,25 @@ public IList FindRules(Type type) return results ?? this.emptyRules; } + /// + /// Retrieve the rules that are related to a specific type and version. + /// + /// The type that is to be validated. + /// The AsyncAPI version to filter rules by. If null, all rules are returned. + /// Either the rules related to the type and version, or an empty list. + public IList FindRules(Type type, AsyncApiVersion? version) + { + var allRules = this.FindRules(type); + if (version == null) + { + return allRules; + } + + return allRules.Where(r => + r.ApplicableVersions == null || + r.ApplicableVersions.Contains(version.Value)).ToList(); + } + /// /// Gets the default validation rule sets. /// @@ -161,19 +180,25 @@ private static ValidationRuleSet BuildDefaultRuleSet() ValidationRuleSet ruleSet = new ValidationRuleSet(); Type validationRuleType = typeof(ValidationRule); - IEnumerable rules = typeof(ValidationRuleSet).Assembly.GetTypes() + IEnumerable ruleProperties = typeof(ValidationRuleSet).Assembly.GetTypes() .Where(t => t.IsClass && t != typeof(object) && t.GetCustomAttributes(typeof(AsyncApiRuleAttribute), false).Any()) .SelectMany(t2 => t2.GetProperties(BindingFlags.Static | BindingFlags.Public) .Where(p => validationRuleType.IsAssignableFrom(p.PropertyType))); - foreach (var property in rules) + foreach (var property in ruleProperties) { var propertyValue = property.GetValue(null); // static property ValidationRule rule = propertyValue as ValidationRule; if (rule != null) { + var versionAttribute = property.GetCustomAttribute(); + if (versionAttribute != null) + { + rule.ApplicableVersions = versionAttribute.Versions; + } + ruleSet.Add(rule); } } @@ -186,4 +211,15 @@ private static ValidationRuleSet BuildDefaultRuleSet() public class AsyncApiRuleAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] + public class AsyncApiVersionRuleAttribute : Attribute + { + public AsyncApiVersion[] Versions { get; } + + public AsyncApiVersionRuleAttribute(params AsyncApiVersion[] versions) + { + Versions = versions; + } + } } diff --git a/test/ByteBard.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs b/test/ByteBard.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs index a1ff9e9a..53868165 100644 --- a/test/ByteBard.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs +++ b/test/ByteBard.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs @@ -1208,6 +1208,7 @@ public void V2_SerializeV2_WithBindings_Serializes() http: headers: description: this mah binding + bindingVersion: 0.2.0 kafka: key: description: this mah other binding diff --git a/test/ByteBard.AsyncAPI.Tests/AsyncApiDocumentV3Tests.cs b/test/ByteBard.AsyncAPI.Tests/AsyncApiDocumentV3Tests.cs index 9d3211dd..13bb615f 100644 --- a/test/ByteBard.AsyncAPI.Tests/AsyncApiDocumentV3Tests.cs +++ b/test/ByteBard.AsyncAPI.Tests/AsyncApiDocumentV3Tests.cs @@ -17,7 +17,7 @@ public void V3_WithComplexInput_CanReSerialize() // Arrange var expected = """ - asyncapi: 3.0.0 + asyncapi: 3.1.0 info: title: Streetlights Kafka API version: 1.0.0 diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/CustomBinding_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/CustomBinding_Should.cs index 6d4da98b..7b86241a 100644 --- a/test/ByteBard.AsyncAPI.Tests/Bindings/CustomBinding_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/CustomBinding_Should.cs @@ -48,6 +48,16 @@ public class MyBinding : ChannelBinding { "nestedConfiguration", (a, n) => { a.NestedConfiguration = n.ParseMapWithExtensions(NestedConfiguration.FixedFieldMap); } }, }; + public override void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeV3(writer); + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeProperties(writer); + } + public override void SerializeProperties(IAsyncApiWriter writer) { writer.WriteStartObject(); diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs index 3590d0ab..b66eb677 100644 --- a/test/ByteBard.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/Http/HttpBindings_Should.cs @@ -1,5 +1,7 @@ namespace ByteBard.AsyncAPI.Tests.Bindings.Http { + using System.Linq; + using System.Net; using FluentAssertions; using ByteBard.AsyncAPI.Bindings; using ByteBard.AsyncAPI.Bindings.Http; @@ -19,6 +21,7 @@ public void V2_HttpMessageBinding_FilledObject_SerializesAndDeserializes() http: headers: description: this mah binding + bindingVersion: 0.2.0 """; var message = new AsyncApiMessage(); @@ -29,6 +32,7 @@ public void V2_HttpMessageBinding_FilledObject_SerializesAndDeserializes() { Description = "this mah binding", }, + BindingVersion = "0.2.0", }); // Act @@ -44,41 +48,146 @@ public void V2_HttpMessageBinding_FilledObject_SerializesAndDeserializes() } [Test] - public void V2_HttpOperationBinding_FilledObject_SerializesAndDeserializes() + public void V2_HttpOperationBinding_RoundTrip_PreservesTypeFromOperationAction() { // Arrange - var expected = + var input = """ - bindings: - http: - type: request - method: POST - query: - description: this mah query + asyncapi: 2.6.0 + info: + title: Test + version: 1.0.0 + channels: + test: + subscribe: + bindings: + http: + type: request + method: POST + query: + description: query params + bindingVersion: 0.2.0 + """; + + // Act + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.Http; + var document = new AsyncApiStringReader(settings).Read(input, out _); + var output = document.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + + // Assert + var httpBinding = document.Operations.Values.First().Bindings["http"] as HttpOperationBinding; + httpBinding.Method.Should().Be("POST"); + httpBinding.Query.Description.Should().Be("query params"); + + output.Should().Contain("type: request"); + output.Should().Contain("method: POST"); + output.Should().Contain("bindingVersion: 0.2.0"); + } + + [Test] + public void V2_HttpOperationBinding_RoundTrip_InfersResponseTypeFromPublishAction() + { + // Arrange + var input = + """ + asyncapi: 2.6.0 + info: + title: Test + version: 1.0.0 + channels: + test: + publish: + bindings: + http: + type: response """; - var operation = new AsyncApiOperation(); + // Act + var settings = new AsyncApiReaderSettings(); + settings.Bindings = BindingsCollection.Http; + var document = new AsyncApiStringReader(settings).Read(input, out _); + var output = document.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + + // Assert + var operation = document.Operations.Values.First(); + operation.Action.Should().Be(AsyncApiAction.Receive); + + output.Should().Contain("type: response"); + output.Should().Contain("bindingVersion: 0.2.0"); + } + + [Test] + public void V3_HttpOperationBinding_OmitsTypeField() + { + // Arrange + var operation = new AsyncApiOperation + { + Action = AsyncApiAction.Send, + }; operation.Bindings.Add(new HttpOperationBinding { - Type = HttpOperationBinding.HttpOperationType.Request, Method = "POST", Query = new AsyncApiJsonSchema { - Description = "this mah query", + Description = "query params", }, }); // Act - var actual = operation.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); - var settings = new AsyncApiReaderSettings(); - settings.Bindings = BindingsCollection.Http; - var binding = new AsyncApiStringReader(settings).ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out _); + var actual = operation.SerializeAsYaml(AsyncApiVersion.AsyncApi3_0); // Assert - actual.Should() - .BePlatformAgnosticEquivalentTo(expected); - binding.Should().BeEquivalentTo(operation); + actual.Should().NotContain("type:"); + actual.Should().Contain("method: POST"); + actual.Should().Contain("bindingVersion: 0.3.0"); + } + + [Test] + public void V3_HttpMessageBinding_IncludesStatusCode() + { + // Arrange + var message = new AsyncApiMessage(); + + message.Bindings.Add(new HttpMessageBinding + { + Headers = new AsyncApiJsonSchema + { + Description = "response headers", + }, + StatusCode = HttpStatusCode.OK, + }); + + // Act + var actual = message.SerializeAsYaml(AsyncApiVersion.AsyncApi3_0); + + // Assert + actual.Should().Contain("statusCode: 200"); + actual.Should().Contain("bindingVersion: 0.3.0"); + } + + [Test] + public void V2_HttpMessageBinding_OmitsStatusCode() + { + // Arrange + var message = new AsyncApiMessage(); + + message.Bindings.Add(new HttpMessageBinding + { + Headers = new AsyncApiJsonSchema + { + Description = "response headers", + }, + StatusCode = HttpStatusCode.OK, + }); + + // Act + var actual = message.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + + // Assert + actual.Should().NotContain("statusCode"); + actual.Should().Contain("bindingVersion: 0.2.0"); } } } diff --git a/test/ByteBard.AsyncAPI.Tests/Bindings/StringOrStringList_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/StringOrStringList_Should.cs index 7e344c09..dbe4c35e 100644 --- a/test/ByteBard.AsyncAPI.Tests/Bindings/StringOrStringList_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Bindings/StringOrStringList_Should.cs @@ -131,6 +131,16 @@ public class StringOrStringListTestBinding : ChannelBinding "testBinding"; + public override void SerializeV2(IAsyncApiWriter writer) + { + this.SerializeV3(writer); + } + + public override void SerializeV3(IAsyncApiWriter writer) + { + this.SerializeProperties(writer); + } + public override void SerializeProperties(IAsyncApiWriter writer) { writer.WriteStartObject(); diff --git a/test/ByteBard.AsyncAPI.Tests/ByteBard.AsyncAPI.Tests.csproj b/test/ByteBard.AsyncAPI.Tests/ByteBard.AsyncAPI.Tests.csproj index 36f60070..27f4ba12 100644 --- a/test/ByteBard.AsyncAPI.Tests/ByteBard.AsyncAPI.Tests.csproj +++ b/test/ByteBard.AsyncAPI.Tests/ByteBard.AsyncAPI.Tests.csproj @@ -2,7 +2,7 @@ 11 - net8.0 + net8.0;net9.0;net10.0 disable enable false diff --git a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs index 7ca108cb..e77acf5f 100644 --- a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs @@ -282,6 +282,7 @@ public void V2_AsyncApiMessage_WithFilledObject_Serializes() examples: - cKey: c dKey: 1 + bindingVersion: 0.2.0 examples: - payload: PropA: a @@ -379,6 +380,7 @@ public void V2_AsyncApiMessage_WithFilledObject_Serializes() { "http", new HttpMessageBinding { + BindingVersion = "0.2.0", Headers = new AsyncApiJsonSchema { Title = "SchemaTitle", diff --git a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiOperation_Should.cs b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiOperation_Should.cs index 27eeae6a..27391a66 100644 --- a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiOperation_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiOperation_Should.cs @@ -238,6 +238,7 @@ public void V2_SerializeV2_WithSingleMessage_Serializes() [Test] public void V2_AsyncApiOperation_WithBindings_Serializes() { + // Arrange var expected = """ bindings: @@ -246,6 +247,7 @@ public void V2_AsyncApiOperation_WithBindings_Serializes() method: PUT query: description: some query + bindingVersion: 0.2.0 kafka: groupId: description: some Id @@ -255,12 +257,12 @@ public void V2_AsyncApiOperation_WithBindings_Serializes() var operation = new AsyncApiOperation { + Action = AsyncApiAction.Send, Bindings = new AsyncApiBindings { { new HttpOperationBinding { - Type = HttpOperationBinding.HttpOperationType.Request, Method = "PUT", Query = new AsyncApiJsonSchema { @@ -284,6 +286,7 @@ public void V2_AsyncApiOperation_WithBindings_Serializes() }, }; + // Act var actual = operation.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); // Assert diff --git a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs index ad363e44..8350ed8b 100644 --- a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs @@ -525,7 +525,7 @@ public MockStringLoader(string input) private readonly string input; - public Stream Load(Uri uri) + public Stream Load(Uri baseUri, Uri uri) { var stream = new MemoryStream(); var writer = new StreamWriter(stream); @@ -535,9 +535,9 @@ public Stream Load(Uri uri) return stream; } - public Task LoadAsync(Uri uri) + public Task LoadAsync(Uri baseUri, Uri uri) { - return Task.FromResult(this.Load(uri)); + return Task.FromResult(this.Load(baseUri, uri)); } } @@ -564,7 +564,7 @@ public class MockJsonSchemaLoader : IStreamLoader description: Light intensity measured in lumens. """; - public Stream Load(Uri uri) + public Stream Load(Uri baseUri, Uri uri) { var stream = new MemoryStream(); var writer = new StreamWriter(stream); @@ -581,9 +581,9 @@ public Stream Load(Uri uri) return stream; } - public Task LoadAsync(Uri uri) + public Task LoadAsync(Uri baseUri, Uri uri) { - return Task.FromResult(this.Load(uri)); + return Task.FromResult(this.Load(baseUri, uri)); } } } \ No newline at end of file diff --git a/test/ByteBard.AsyncAPI.Tests/Models/AvroSchema_Should.cs b/test/ByteBard.AsyncAPI.Tests/Models/AvroSchema_Should.cs index 9d6d57b0..30a56d3d 100644 --- a/test/ByteBard.AsyncAPI.Tests/Models/AvroSchema_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Models/AvroSchema_Should.cs @@ -1,9 +1,12 @@ namespace ByteBard.AsyncAPI.Tests.Models { using System.Collections.Generic; + using System.Linq; using FluentAssertions; + using ByteBard.AsyncAPI.Extensions; using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Readers; + using ByteBard.AsyncAPI.Validations; using NUnit.Framework; public class AvroSchema_Should @@ -462,5 +465,276 @@ public void V2_ReadFragment_DeserializesCorrectly() actual.Should() .BeEquivalentTo(expected); } + + [Test] + public void V2_ReadFragment_WithRecursiveNamedType_DeserializesCorrectly() + { + var input = """ + { + "type": "record", + "name": "LongList", + "aliases": ["LinkedLongs"], + "fields" : [ + {"name": "value", "type": "long"}, + {"name": "next", "type": ["null", "LongList"]} + ] + } + """; + + var actual = new AsyncApiStringReader().ReadFragment(input, AsyncApiVersion.AsyncApi2_0, out var diagnostic); + + diagnostic.Errors.Should().BeEmpty(); + + var record = actual.As(); + var union = record.Fields[1].Type.As(); + var namedType = union.Types[1].As(); + + namedType.Name.Should().Be("LongList"); + namedType.Target.Should().BeSameAs(record); + + var serialized = actual.SerializeAsJson(AsyncApiVersion.AsyncApi2_0); + serialized.Should().Contain("\"LongList\""); + serialized.Should().NotContain("$ref"); + + actual.Validate(ValidationRuleSet.GetDefaultRuleSet()) + .OfType() + .Should() + .BeEmpty(); + } + + [Test] + public void V2_Serialize_WithRecursiveNamedType_WritesNamedTypeAsString() + { + var expected = """ + type: record + name: LongList + fields: + - name: value + type: long + - name: next + type: + - 'null' + - LongList + """; + + var record = new AvroRecord + { + Name = "LongList", + }; + + record.Fields = new List + { + new AvroField + { + Name = "value", + Type = AvroPrimitiveType.Long, + }, + new AvroField + { + Name = "next", + Type = new AvroUnion + { + Types = new List + { + AvroPrimitiveType.Null, + new AvroNamedType("LongList", record), + }, + }, + }, + }; + + var actual = record.SerializeAsYaml(AsyncApiVersion.AsyncApi2_0); + + actual.Should().BePlatformAgnosticEquivalentTo(expected); + } + + [Test] + public void V2_ReadFragment_WithMapValuesNamedType_DeserializesCorrectly() + { + var input = """ + { + "type": "record", + "name": "Container", + "namespace": "example", + "fields" : [ + { + "name": "item", + "type": { + "type": "record", + "name": "Item", + "fields": [ + {"name": "id", "type": "string"} + ] + } + }, + { + "name": "itemsByKey", + "type": { + "type": "map", + "values": "Item" + } + } + ] + } + """; + + var actual = new AsyncApiStringReader().ReadFragment(input, AsyncApiVersion.AsyncApi2_0, out var diagnostic); + + diagnostic.Errors.Should().BeEmpty(); + + var record = actual.As(); + var item = record.Fields[0].Type.As(); + var map = record.Fields[1].Type.As(); + var namedType = map.Values.As(); + + namedType.Name.Should().Be("Item"); + namedType.Target.Should().BeSameAs(item); + + var serialized = actual.SerializeAsJson(AsyncApiVersion.AsyncApi2_0); + serialized.Should().Contain("\"values\": \"Item\""); + serialized.Should().NotContain("$ref"); + + actual.Validate(ValidationRuleSet.GetDefaultRuleSet()) + .OfType() + .Should() + .BeEmpty(); + } + + [Test] + public void V2_Validate_WithUnresolvedNamedType_CreatesWarning() + { + var input = """ + { + "type": "record", + "name": "Container", + "fields" : [ + {"name": "missing", "type": "MissingType"} + ] + } + """; + + var actual = new AsyncApiStringReader().ReadFragment(input, AsyncApiVersion.AsyncApi2_0, out var diagnostic); + + diagnostic.Errors.Should().BeEmpty(); + diagnostic.Warnings.Should() + .ContainSingle(w => w.Message == "Avro named type 'MissingType' is referenced but was not defined before use."); + + actual.Validate(ValidationRuleSet.GetDefaultRuleSet()) + .OfType() + .Should() + .ContainSingle(w => w.Message == "Avro named type 'MissingType' is referenced but was not defined before use."); + } + + [Test] + public void V2_Validate_WithUnresolvedMapValuesNamedType_CreatesWarning() + { + var input = """ + { + "type": "record", + "name": "Container", + "fields" : [ + { + "name": "itemsByKey", + "type": { + "type": "map", + "values": "MissingType" + } + } + ] + } + """; + + var actual = new AsyncApiStringReader().ReadFragment(input, AsyncApiVersion.AsyncApi2_0, out var diagnostic); + + diagnostic.Errors.Should().BeEmpty(); + diagnostic.Warnings.Should() + .ContainSingle(w => w.Message == "Avro named type 'MissingType' is referenced but was not defined before use."); + + actual.Validate(ValidationRuleSet.GetDefaultRuleSet()) + .OfType() + .Should() + .ContainSingle(w => w.Message == "Avro named type 'MissingType' is referenced but was not defined before use."); + } + + [Test] + public void V2_ReadDocument_WithRecursiveNamedType_DeserializesAndValidates() + { + var input = """ + asyncapi: '2.6.0' + info: + title: Avro named type test + version: '1.0.0' + channels: + list: + publish: + message: + name: ListMessage + payload: + type: record + name: LongList + fields: + - name: value + type: long + - name: next + type: + - 'null' + - LongList + schemaFormat: application/vnd.apache.avro + """; + + var document = new AsyncApiStringReader().Read(input, out var diagnostic); + + diagnostic.Errors.Should().BeEmpty(); + diagnostic.Warnings.Should().BeEmpty(); + + var message = document.Operations.Values.First(operation => operation.Action == AsyncApiAction.Receive).Messages.First(); + var record = message.Payload.Schema.As(); + var union = record.Fields[1].Type.As(); + var namedType = union.Types[1].As(); + + namedType.Name.Should().Be("LongList"); + namedType.Target.Should().BeSameAs(record); + } + + [Test] + public void V2_ReadDocument_WithUnresolvedNamedType_CreatesWarning() + { + var input = """ + asyncapi: '2.6.0' + info: + title: Avro named type test + version: '1.0.0' + channels: + list: + publish: + message: + name: ListMessage + payload: + type: record + name: LongList + fields: + - name: value + type: long + - name: next + type: + - 'null' + - MissingType + schemaFormat: application/vnd.apache.avro + """; + + new AsyncApiStringReader().Read(input, out var diagnostic); + + diagnostic.Errors.Should().BeEmpty(); + diagnostic.Warnings.Should() + .ContainSingle(w => w.Message == "Avro named type 'MissingType' is referenced but was not defined before use."); + } + + [Test] + public void V2_AvroSchema_WithPrimitiveSchema_ConvertsToPrimitiveType() + { + AsyncApiAvroSchema schema = AvroPrimitiveType.String; + + ((AvroPrimitiveType)schema).Should().Be(AvroPrimitiveType.String); + } } } diff --git a/test/ByteBard.AsyncAPI.Tests/V3_TestData/AsyncApiSchema_InlinedReferences.yml b/test/ByteBard.AsyncAPI.Tests/V3_TestData/AsyncApiSchema_InlinedReferences.yml index 6157421f..68a53d4c 100644 --- a/test/ByteBard.AsyncAPI.Tests/V3_TestData/AsyncApiSchema_InlinedReferences.yml +++ b/test/ByteBard.AsyncAPI.Tests/V3_TestData/AsyncApiSchema_InlinedReferences.yml @@ -1,4 +1,4 @@ -asyncapi: 3.0.0 +asyncapi: 3.1.0 info: title: Streetlights Kafka API version: 1.0.0 diff --git a/test/ByteBard.AsyncAPI.Tests/V3_TestData/AsyncApiSchema_NoInlinedReferences.yml b/test/ByteBard.AsyncAPI.Tests/V3_TestData/AsyncApiSchema_NoInlinedReferences.yml index e55a67b0..468be332 100644 --- a/test/ByteBard.AsyncAPI.Tests/V3_TestData/AsyncApiSchema_NoInlinedReferences.yml +++ b/test/ByteBard.AsyncAPI.Tests/V3_TestData/AsyncApiSchema_NoInlinedReferences.yml @@ -1,4 +1,4 @@ -asyncapi: 3.0.0 +asyncapi: 3.1.0 info: title: Streetlights Kafka API version: 1.0.0 diff --git a/test/ByteBard.AsyncAPI.Tests/Validation/ValidationRuleTests.cs b/test/ByteBard.AsyncAPI.Tests/Validation/ValidationRuleTests.cs index 6aedaf16..6fe766cf 100644 --- a/test/ByteBard.AsyncAPI.Tests/Validation/ValidationRuleTests.cs +++ b/test/ByteBard.AsyncAPI.Tests/Validation/ValidationRuleTests.cs @@ -2,11 +2,104 @@ using System.Linq; using FluentAssertions; +using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Readers; +using ByteBard.AsyncAPI.Validations; using NUnit.Framework; public class ValidationRuleTests { + [Test] + public void V2_DocumentWithNoChannels_ShouldError() + { + // arrange + var input = + """ + asyncapi: 2.6.0 + info: + title: Chat Application + version: 1.0.0 + """; + + // act + new AsyncApiStringReader().Read(input, out var diagnostic); + + // assert + diagnostic.Errors.Should().Contain(e => e.Message == "The field 'channels' in 'document' object is REQUIRED."); + } + + [Test] + public void V2_DocumentWithChannels_ShouldPass() + { + // arrange + var input = + """ + asyncapi: 2.6.0 + info: + title: Chat Application + version: 1.0.0 + channels: + chat: + publish: + operationId: onMessageReceived + message: + name: text + payload: + type: string + """; + + // act + new AsyncApiStringReader().Read(input, out var diagnostic); + + // assert + diagnostic.Errors.Should().NotContain(e => e.Message.Contains("channels")); + } + + [Test] + public void V3_DocumentWithNoChannels_ShouldPass() + { + // arrange + var input = + """ + asyncapi: 3.0.0 + info: + title: Chat Application + version: 1.0.0 + """; + + // act + new AsyncApiStringReader().Read(input, out var diagnostic); + + // assert + diagnostic.Errors.Should().NotContain(e => e.Message.Contains("channels") && e.Message.Contains("REQUIRED")); + } + + [Test] + public void VersionAwareRuleSet_V2Rule_DoesNotRunOnV3Document() + { + // arrange + var ruleSet = ValidationRuleSet.GetDefaultRuleSet(); + + // act + var rules = ruleSet.FindRules(typeof(AsyncApiDocument), AsyncApiVersion.AsyncApi3_0); + + // assert + rules.Should().NotContain(r => r.ApplicableVersions != null && r.ApplicableVersions.Contains(AsyncApiVersion.AsyncApi2_0) && !r.ApplicableVersions.Contains(AsyncApiVersion.AsyncApi3_0)); + } + + [Test] + public void VersionAwareRuleSet_V3Rule_DoesNotRunOnV2Document() + { + // arrange + var ruleSet = ValidationRuleSet.GetDefaultRuleSet(); + + // act + var rules = ruleSet.FindRules(typeof(AsyncApiOperation), AsyncApiVersion.AsyncApi2_0); + + // assert + rules.Should().NotContain(r => r.ApplicableVersions != null && r.ApplicableVersions.Contains(AsyncApiVersion.AsyncApi3_0) && !r.ApplicableVersions.Contains(AsyncApiVersion.AsyncApi2_0)); + } + [Test] public void V2_OperationId_WithNonUniqueKey_DiagnosticsError() { diff --git a/test/ByteBard.AsyncAPI.Tests/Validation/ValidationRulesetTests.cs b/test/ByteBard.AsyncAPI.Tests/Validation/ValidationRulesetTests.cs index 2620d9b4..6a92c427 100644 --- a/test/ByteBard.AsyncAPI.Tests/Validation/ValidationRulesetTests.cs +++ b/test/ByteBard.AsyncAPI.Tests/Validation/ValidationRulesetTests.cs @@ -33,7 +33,7 @@ public void V2_DefaultRuleSet_PropertyReturnsTheCorrectRules() Assert.IsNotEmpty(rules); // Update the number if you add new default rule(s). - Assert.AreEqual(27, rules.Count); + Assert.AreEqual(29, rules.Count); } } }