diff --git a/CHANGELOG.md b/CHANGELOG.md index efb62f45..4e5bda60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [4.1.0](https://github.com/LEGO/AsyncAPI.NET/compare/v4.0.2...v4.1.0) (2023-09-27) + + +### Features + +* **bindings:** update FilterPolicy to match AWS API ([#128](https://github.com/LEGO/AsyncAPI.NET/issues/128)) ([5b64654](https://github.com/LEGO/AsyncAPI.NET/commit/5b6465474ae09d42a27377bf04d58fdbd1dd8a59)) + ## [4.0.2](https://github.com/LEGO/AsyncAPI.NET/compare/v4.0.1...v4.0.2) (2023-08-01) diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/Consumer.cs b/src/LEGO.AsyncAPI.Bindings/Sns/Consumer.cs index 46548977..262521d0 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sns/Consumer.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sns/Consumer.cs @@ -3,6 +3,7 @@ namespace LEGO.AsyncAPI.Bindings.Sns using System; using System.Collections.Generic; using LEGO.AsyncAPI.Attributes; + using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Models.Interfaces; using LEGO.AsyncAPI.Writers; @@ -20,8 +21,14 @@ public class Consumer : IAsyncApiExtensible /// /// Only receive a subset of messages from the channel, determined by this policy. + /// Depending on the FilterPolicyScope, a map of either a message attribute or message body to an array of possible matches. The match may be a simple string for an exact match, but it may also be an object that represents a constraint and values for that constraint. /// - public FilterPolicy FilterPolicy { get; set; } + public AsyncApiAny FilterPolicy { get; set; } + + /// + /// Determines whether the FilterPolicy applies to MessageAttributes or MessageBody. + /// + public FilterPolicyScope FilterPolicyScope { get; set; } /// /// If true AWS SNS attributes are removed from the body, and for SQS, SNS message attributes are copied to SQS message attributes. If false the SNS attributes are included in the body. @@ -55,7 +62,8 @@ public void Serialize(IAsyncApiWriter writer) writer.WriteStartObject(); writer.WriteRequiredProperty("protocol", this.Protocol.GetDisplayName()); writer.WriteRequiredObject("endpoint", this.Endpoint, (w, e) => e.Serialize(w)); - writer.WriteOptionalObject("filterPolicy", this.FilterPolicy, (w, f) => f.Serialize(w)); + writer.WriteOptionalObject("filterPolicy", this.FilterPolicy, (w, f) => f.Write(w)); + writer.WriteOptionalProperty("filterPolicyScope", this.FilterPolicyScope.GetDisplayName()); writer.WriteRequiredProperty("rawMessageDelivery", this.RawMessageDelivery); writer.WriteOptionalObject("redrivePolicy", this.RedrivePolicy, (w, p) => p.Serialize(w)); writer.WriteOptionalObject("deliveryPolicy", this.DeliveryPolicy, (w, p) => p.Serialize(w)); @@ -77,4 +85,10 @@ public enum Protocol [Display("lambda")] Lambda, [Display("firehose")] Firehose, } + + public enum FilterPolicyScope + { + [Display("MessageAttributes")] MessageAttributes, + [Display("MessageBody")] MessageBody, + } } \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/FilterPolicy.cs b/src/LEGO.AsyncAPI.Bindings/Sns/FilterPolicy.cs deleted file mode 100644 index f24ae280..00000000 --- a/src/LEGO.AsyncAPI.Bindings/Sns/FilterPolicy.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace LEGO.AsyncAPI.Bindings.Sns -{ - using System; - using System.Collections.Generic; - using LEGO.AsyncAPI.Models; - using LEGO.AsyncAPI.Models.Interfaces; - using LEGO.AsyncAPI.Writers; - - public class FilterPolicy : IAsyncApiExtensible - { - /// - /// A map of a message attribute to an array of possible matches. The match may be a simple string for an exact match, but it may also be an object that represents a constraint and values for that constraint. - /// - public AsyncApiAny Attributes { get; set; } - - public IDictionary Extensions { get; set; } = new Dictionary(); - - public void Serialize(IAsyncApiWriter writer) - { - if (writer is null) - { - throw new ArgumentNullException(nameof(writer)); - } - - writer.WriteStartObject(); - writer.WriteRequiredObject("attributes", this.Attributes, (w, a) => w.WriteAny(a)); - writer.WriteExtensions(this.Extensions); - writer.WriteEndObject(); - } - } -} \ No newline at end of file diff --git a/src/LEGO.AsyncAPI.Bindings/Sns/SnsOperationBinding.cs b/src/LEGO.AsyncAPI.Bindings/Sns/SnsOperationBinding.cs index e474e4a6..ffab9afb 100644 --- a/src/LEGO.AsyncAPI.Bindings/Sns/SnsOperationBinding.cs +++ b/src/LEGO.AsyncAPI.Bindings/Sns/SnsOperationBinding.cs @@ -47,18 +47,14 @@ public class SnsOperationBinding : OperationBinding { { "protocol", (a, n) => { a.Protocol = n.GetScalarValue().GetEnumFromDisplayName(); } }, { "endpoint", (a, n) => { a.Endpoint = n.ParseMapWithExtensions(this.identifierFixFields); } }, - { "filterPolicy", (a, n) => { a.FilterPolicy = n.ParseMapWithExtensions(this.filterPolicyFixedFields); } }, + { "filterPolicy", (a, n) => { a.FilterPolicy = n.CreateAny(); } }, + { "filterPolicyScope", (a, n) => { a.FilterPolicyScope = n.GetScalarValue().GetEnumFromDisplayName(); } }, { "rawMessageDelivery", (a, n) => { a.RawMessageDelivery = n.GetBooleanValue(); } }, { "redrivePolicy", (a, n) => { a.RedrivePolicy = n.ParseMapWithExtensions(this.redrivePolicyFixedFields); } }, { "deliveryPolicy", (a, n) => { a.DeliveryPolicy = n.ParseMapWithExtensions(this.deliveryPolicyFixedFields); } }, { "displayName", (a, n) => { a.DisplayName = n.GetScalarValue(); } }, }; - private FixedFieldMap filterPolicyFixedFields => new() - { - { "attributes", (a, n) => { a.Attributes = n.CreateAny(); } }, - }; - private FixedFieldMap redrivePolicyFixedFields => new() { { "deadLetterQueue", (a, n) => { a.DeadLetterQueue = n.ParseMapWithExtensions(identifierFixFields); } }, diff --git a/src/LEGO.AsyncAPI.Readers/AsyncApiYamlDocumentReader.cs b/src/LEGO.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs similarity index 96% rename from src/LEGO.AsyncAPI.Readers/AsyncApiYamlDocumentReader.cs rename to src/LEGO.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs index fccdf3e3..5c0826af 100644 --- a/src/LEGO.AsyncAPI.Readers/AsyncApiYamlDocumentReader.cs +++ b/src/LEGO.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs @@ -1,10 +1,11 @@ -// Copyright (c) The LEGO Group. All rights reserved. +// Copyright (c) The LEGO Group. All rights reserved. namespace LEGO.AsyncAPI.Readers { using System.Collections.Generic; using System.Linq; using System.Text.Json.Nodes; + using System.Threading; using System.Threading.Tasks; using LEGO.AsyncAPI.Exceptions; using LEGO.AsyncAPI.Extensions; @@ -76,7 +77,7 @@ public AsyncApiDocument Read(JsonNode input, out AsyncApiDiagnostic diagnostic) return document; } - public Task ReadAsync(JsonNode input) + public async Task ReadAsync(JsonNode input, CancellationToken cancellationToken = default) { var diagnostic = new AsyncApiDiagnostic(); var context = new ParsingContext(diagnostic) @@ -106,11 +107,11 @@ public Task ReadAsync(JsonNode input) } } - return Task.FromResult(new ReadResult + return new ReadResult { AsyncApiDocument = document, AsyncApiDiagnostic = diagnostic, - }); + }; } private void ResolveReferences(AsyncApiDiagnostic diagnostic, AsyncApiDocument document) diff --git a/src/LEGO.AsyncAPI.Readers/AsyncApiReaderSettings.cs b/src/LEGO.AsyncAPI.Readers/AsyncApiReaderSettings.cs index 134acc75..7f759107 100644 --- a/src/LEGO.AsyncAPI.Readers/AsyncApiReaderSettings.cs +++ b/src/LEGO.AsyncAPI.Readers/AsyncApiReaderSettings.cs @@ -18,7 +18,7 @@ public enum ReferenceResolutionSetting DoNotResolveReferences, /// - /// ResolveAllReferences, effectively inlining them. + /// Resolve internal component references and inline them. /// ResolveReferences, } diff --git a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs index 92a34c64..42562d9a 100644 --- a/src/LEGO.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs +++ b/src/LEGO.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs @@ -67,9 +67,21 @@ public AsyncApiReference ConvertToAsyncApiReference( Id = reference, }; } + + var asyncApiReference = new AsyncApiReference(); + if (reference.StartsWith("/")) + { + asyncApiReference.IsFragment = true; + } + + asyncApiReference.ExternalResource = segments[0]; + + return asyncApiReference; + } else if (segments.Length == 2) { + // Local reference if (reference.StartsWith("#")) { try @@ -84,7 +96,7 @@ public AsyncApiReference ConvertToAsyncApiReference( } var id = segments[1]; - + var asyncApiReference = new AsyncApiReference(); if (id.StartsWith("/components/")) { var localSegments = segments[1].Split('/'); @@ -103,12 +115,16 @@ public AsyncApiReference ConvertToAsyncApiReference( id = localSegments[3]; } - - return new AsyncApiReference + else { - Type = type, - Id = id, - }; + asyncApiReference.IsFragment = true; + } + + asyncApiReference.ExternalResource = segments[0]; + asyncApiReference.Type = type; + asyncApiReference.Id = id; + + return asyncApiReference; } } diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiDocument.cs b/src/LEGO.AsyncAPI/Models/AsyncApiDocument.cs index b28168e2..1a5b7c71 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiDocument.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiDocument.cs @@ -150,7 +150,7 @@ internal T ResolveReference(AsyncApiReference reference) where T : class, IAs return this.ResolveReference(reference) as T; } - public IAsyncApiReferenceable ResolveReference(AsyncApiReference reference) + internal IAsyncApiReferenceable ResolveReference(AsyncApiReference reference) { if (reference == null) { diff --git a/src/LEGO.AsyncAPI/Models/AsyncApiReference.cs b/src/LEGO.AsyncAPI/Models/AsyncApiReference.cs index 2403b4ad..4f9660d1 100644 --- a/src/LEGO.AsyncAPI/Models/AsyncApiReference.cs +++ b/src/LEGO.AsyncAPI/Models/AsyncApiReference.cs @@ -11,6 +11,14 @@ namespace LEGO.AsyncAPI.Models /// public class AsyncApiReference : IAsyncApiSerializable { + /// + /// External resource in the reference. + /// It maybe: + /// 1. a absolute/relative file path, for example: ../commons/pet.json + /// 2. a Url, for example: http://localhost/pet.json + /// + public string ExternalResource { get; set; } + /// /// Gets or sets the element type referenced. /// @@ -27,17 +35,37 @@ public class AsyncApiReference : IAsyncApiSerializable public AsyncApiDocument HostDocument { get; set; } = null; /// - /// Gets the full reference string for v2.3. + /// Gets a flag indicating whether a file is a valid OpenAPI document or a fragment + /// + public bool IsFragment { get; set; } = false; + + /// + /// Gets a flag indicating whether this reference is an external reference. + /// + public bool IsExternal => this.ExternalResource != null; + + /// + /// Gets the full reference string for v2. /// public string Reference { get { + if (this.IsExternal) + { + return this.GetExternalReferenceV2(); + } + if (!this.Type.HasValue) { throw new ArgumentNullException(nameof(this.Type)); } + //if (this.Type == ReferenceType.SecurityScheme) + //{ + // return this.Id; + //} + return "#/components/" + this.Type.GetDisplayName() + "/" + this.Id; } } @@ -67,6 +95,21 @@ public void SerializeV2(IAsyncApiWriter writer) writer.WriteEndObject(); } + private string GetExternalReferenceV2() + { + if (this.Id != null) + { + if (this.IsFragment) + { + return this.ExternalResource + "#" + this.Id; + } + + return this.ExternalResource + "#/components/" + this.Type.GetDisplayName() + "/" + this.Id; + } + + return this.ExternalResource; + } + public void Write(IAsyncApiWriter writer) { this.SerializeV2(writer); diff --git a/src/LEGO.AsyncAPI/Services/AsyncApiReferenceResolver.cs b/src/LEGO.AsyncAPI/Services/AsyncApiReferenceResolver.cs index ca345b3f..88808cb8 100644 --- a/src/LEGO.AsyncAPI/Services/AsyncApiReferenceResolver.cs +++ b/src/LEGO.AsyncAPI/Services/AsyncApiReferenceResolver.cs @@ -154,7 +154,7 @@ public override void Visit(AsyncApiSchema schema) this.ResolveMap(schema.Properties); } - private void ResolveObject(T entity, Action assign) where T : class, IAsyncApiReferenceable + private void ResolveObject(T entity, Action assign) where T : class, IAsyncApiReferenceable, new() { if (entity == null) { @@ -184,7 +184,7 @@ private void ResolveObject(T entity, Action assign) where T : class, IAsyn } } - private void ResolveMap(IDictionary map) where T : class, IAsyncApiReferenceable + private void ResolveMap(IDictionary map) where T : class, IAsyncApiReferenceable, new() { if (map == null) { @@ -201,8 +201,17 @@ private void ResolveMap(IDictionary map) where T : class, IAsyncAp } } - private T ResolveReference(AsyncApiReference reference) where T : class, IAsyncApiReferenceable + private T ResolveReference(AsyncApiReference reference) where T : class, IAsyncApiReferenceable, new() { + if (reference.IsExternal) + { + return new () + { + UnresolvedReference = true, + Reference = reference, + }; + } + try { return this.currentDocument.ResolveReference(reference) as T; diff --git a/test/LEGO.AsyncAPI.Tests/Bindings/Sns/SnsBindings_Should.cs b/test/LEGO.AsyncAPI.Tests/Bindings/Sns/SnsBindings_Should.cs index 0ae28e88..607510d9 100644 --- a/test/LEGO.AsyncAPI.Tests/Bindings/Sns/SnsBindings_Should.cs +++ b/test/LEGO.AsyncAPI.Tests/Bindings/Sns/SnsBindings_Should.cs @@ -164,20 +164,18 @@ public void SnsOperationBinding_WithFilledObject_SerializesAndDeserializes() x-identifierExtension: identifierXPropertyName: identifierXPropertyValue filterPolicy: - attributes: - store: - - asyncapi_corp - contact: dec.kolakowski - event: - - anything-but: order_cancelled - order_key: - transient: by_area - customer_interests: - - rugby - - football - - baseball - x-filterPolicyExtension: - filterPolicyXPropertyName: filterPolicyXPropertyValue + store: + - asyncapi_corp + contact: dec.kolakowski + event: + - anything-but: order_cancelled + order_key: + transient: by_area + customer_interests: + - rugby + - football + - baseball + filterPolicyScope: MessageAttributes rawMessageDelivery: false redrivePolicy: deadLetterQueue: @@ -250,47 +248,35 @@ public void SnsOperationBinding_WithFilledObject_SerializesAndDeserializes() }, }, }, - FilterPolicy = new FilterPolicy() - { - Attributes = new AsyncApiObject() + FilterPolicy = new AsyncApiObject() + { + { "store", new AsyncApiArray() { new AsyncApiAny("asyncapi_corp") } }, + { "contact", new AsyncApiAny("dec.kolakowski") }, { - { "store", new AsyncApiArray() { new AsyncApiAny("asyncapi_corp") } }, - { "contact", new AsyncApiAny("dec.kolakowski") }, - { - "event", new AsyncApiArray() - { - new AsyncApiObject() - { - { "anything-but", new AsyncApiAny("order_cancelled") }, - }, - } - }, + "event", new AsyncApiArray() { - "order_key", new AsyncApiObject() + new AsyncApiObject() { - { "transient", new AsyncApiAny("by_area") }, - } - }, + { "anything-but", new AsyncApiAny("order_cancelled") }, + }, + } + }, + { + "order_key", new AsyncApiObject() { - "customer_interests", new AsyncApiArray() - { - new AsyncApiAny("rugby"), - new AsyncApiAny("football"), - new AsyncApiAny("baseball"), - } - }, + { "transient", new AsyncApiAny("by_area") }, + } }, - Extensions = new Dictionary() { + "customer_interests", new AsyncApiArray() { - "x-filterPolicyExtension", - new AsyncApiObject() - { - { "filterPolicyXPropertyName", new AsyncApiAny("filterPolicyXPropertyValue") }, - } - }, + new AsyncApiAny("rugby"), + new AsyncApiAny("football"), + new AsyncApiAny("baseball"), + } }, }, + FilterPolicyScope = FilterPolicyScope.MessageAttributes, RawMessageDelivery = false, RedrivePolicy = new RedrivePolicy() { diff --git a/test/LEGO.AsyncAPI.Tests/ReferenceTests.cs b/test/LEGO.AsyncAPI.Tests/ReferenceTests.cs new file mode 100644 index 00000000..8a12a8ca --- /dev/null +++ b/test/LEGO.AsyncAPI.Tests/ReferenceTests.cs @@ -0,0 +1,64 @@ +// Copyright (c) The LEGO Group. All rights reserved. + +namespace LEGO.AsyncAPI.Tests +{ + using FluentAssertions; + using LEGO.AsyncAPI.Models; + using LEGO.AsyncAPI.Readers; + using NUnit.Framework; + + public class AsyncApiReference_Should + { + + [Test] + public void AsyncApiReference_WithExternalFragmentReference_AllowReference() + { + var actual = @"payload: + $ref: 'http://example.com/some-resource#/path/to/external/fragment' +"; + var reader = new AsyncApiStringReader(); + var deserialized = reader.ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out var diagnostic); + + diagnostic.Errors.Should().BeEmpty(); + var reference = deserialized.Payload.Reference; + reference.ExternalResource.Should().Be("http://example.com/some-resource"); + reference.Id.Should().Be("/path/to/external/fragment"); + reference.IsFragment.Should().BeTrue(); + reference.IsExternal.Should().BeTrue(); + } + + [Test] + public void AsyncApiReference_WithFragmentReference_AllowReference() + { + var actual = @"payload: + $ref: '/fragments/myFragment' +"; + var reader = new AsyncApiStringReader(); + var deserialized = reader.ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out var diagnostic); + + diagnostic.Errors.Should().BeEmpty(); + var reference = deserialized.Payload.Reference; + reference.ExternalResource.Should().Be("/fragments/myFragment"); + reference.Id.Should().BeNull(); + reference.IsFragment.Should().BeTrue(); + reference.IsExternal.Should().BeTrue(); + } + + [Test] + public void AsyncApiReference_WithExternalReference_AllowsReferenceDoesNotResolve() + { + var actual = @"payload: + $ref: http://example.com/json.json +"; + + var reader = new AsyncApiStringReader(); + var deserialized = reader.ReadFragment(actual, AsyncApiVersion.AsyncApi2_0, out var diagnostic); + diagnostic.Errors.Should().BeEmpty(); + var reference = deserialized.Payload.Reference; + reference.ExternalResource.Should().Be("http://example.com/json.json"); + reference.Id.Should().BeNull(); + reference.IsExternal.Should().BeTrue(); + diagnostic.Errors.Should().BeEmpty(); + } + } +} \ No newline at end of file