From 07d912dbfe3a5a2500f066477387b278e4cca824 Mon Sep 17 00:00:00 2001 From: Alex Wichmann Date: Sun, 7 Dec 2025 23:45:38 +0100 Subject: [PATCH 01/15] fix: try read relative uri's as files. (#21) * Fix: Try read relative uri's as files. Signed-off-by: Alex Wichmann * Update src/ByteBard.AsyncAPI.Readers/Services/DefaultStreamLoader.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Alex Wichmann * Apply suggestion from @VisualBean Signed-off-by: Alex Wichmann * Apply suggestion from @VisualBean Signed-off-by: Alex Wichmann * Apply suggestion from @VisualBean Signed-off-by: Alex Wichmann * Apply suggestion from @VisualBean Signed-off-by: Alex Wichmann * Refactor method signatures to include `baseUri` parameter Updated all methods in `IStreamLoader`, `DefaultStreamLoader`, and related tests to accept both `baseUri` and `uri`. Added a new property `BaseUri` to `AsyncApiReaderSettings` for resolving relative references. Adjusted the implementation of external reference loading in `AsyncApiJsonDocumentReader`. --------- Signed-off-by: Alex Wichmann Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../AsyncApiJsonDocumentReader.cs | 4 +- .../AsyncApiReaderSettings.cs | 5 ++ .../Interface/IStreamLoader.cs | 4 +- .../Services/DefaultStreamLoader.cs | 54 ++++++++++++------- .../Models/AsyncApiReference_Should.cs | 12 ++--- 5 files changed, 49 insertions(+), 30 deletions(-) diff --git a/src/ByteBard.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs b/src/ByteBard.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs index ab5586e..d0cf8c3 100644 --- a/src/ByteBard.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs +++ b/src/ByteBard.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs @@ -280,7 +280,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 +310,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); } diff --git a/src/ByteBard.AsyncAPI.Readers/AsyncApiReaderSettings.cs b/src/ByteBard.AsyncAPI.Readers/AsyncApiReaderSettings.cs index 111bf26..9881dd0 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 a74f45f..c770e75 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/Services/DefaultStreamLoader.cs b/src/ByteBard.AsyncAPI.Readers/Services/DefaultStreamLoader.cs index 6ccd69b..74b11dd 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/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs index ad363e4..8350ed8 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 From 7085d23f7441fe87645562192b0efe3c8f85c9f1 Mon Sep 17 00:00:00 2001 From: Alex Wichmann Date: Sun, 7 Dec 2025 23:46:26 +0100 Subject: [PATCH 02/15] fix: nullref on walking optional reply property refs (#23) * Fix: Try read relative uri's as files. Signed-off-by: Alex Wichmann * Update src/ByteBard.AsyncAPI.Readers/Services/DefaultStreamLoader.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Alex Wichmann * Apply suggestion from @VisualBean Signed-off-by: Alex Wichmann * Apply suggestion from @VisualBean Signed-off-by: Alex Wichmann * Apply suggestion from @VisualBean Signed-off-by: Alex Wichmann * Apply suggestion from @VisualBean Signed-off-by: Alex Wichmann * Refactor method signatures to include `baseUri` parameter Updated all methods in `IStreamLoader`, `DefaultStreamLoader`, and related tests to accept both `baseUri` and `uri`. Added a new property `BaseUri` to `AsyncApiReaderSettings` for resolving relative references. Adjusted the implementation of external reference loading in `AsyncApiJsonDocumentReader`. * Refactor `AsyncApiWalker` to ensure null checks before walking references Added explicit null checks for `reply.Address` and `reply.Channel`. This prevents potential NullReferenceException when these properties are not initialized. This change ensures that the walker safely handles cases where optional fields might be missing, improving robustness of the code. --------- Signed-off-by: Alex Wichmann Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/ByteBard.AsyncAPI/Services/AsyncApiWalker.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/ByteBard.AsyncAPI/Services/AsyncApiWalker.cs b/src/ByteBard.AsyncAPI/Services/AsyncApiWalker.cs index fbb7271..21c610d 100644 --- a/src/ByteBard.AsyncAPI/Services/AsyncApiWalker.cs +++ b/src/ByteBard.AsyncAPI/Services/AsyncApiWalker.cs @@ -587,8 +587,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) { @@ -1211,4 +1218,4 @@ public void Walk(IAsyncApiElement element) } } } -} +} \ No newline at end of file From 1985b385949ef2b4b7195f72f7534415f210d8b7 Mon Sep 17 00:00:00 2001 From: Alex Wichmann Date: Wed, 11 Feb 2026 14:23:14 +0100 Subject: [PATCH 03/15] chore: upgrade to 3.1.0 (#26) * upgrade to 3.1.0 * Delete .todo directory Signed-off-by: Alex Wichmann --------- Signed-off-by: Alex Wichmann --- src/ByteBard.AsyncAPI/Models/AsyncApiDocument.cs | 2 +- test/ByteBard.AsyncAPI.Tests/AsyncApiDocumentV3Tests.cs | 2 +- .../V3_TestData/AsyncApiSchema_InlinedReferences.yml | 2 +- .../V3_TestData/AsyncApiSchema_NoInlinedReferences.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiDocument.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiDocument.cs index 6af7345..d489f69 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/test/ByteBard.AsyncAPI.Tests/AsyncApiDocumentV3Tests.cs b/test/ByteBard.AsyncAPI.Tests/AsyncApiDocumentV3Tests.cs index 9d3211d..13bb615 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/V3_TestData/AsyncApiSchema_InlinedReferences.yml b/test/ByteBard.AsyncAPI.Tests/V3_TestData/AsyncApiSchema_InlinedReferences.yml index 6157421..68a53d4 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 e55a67b..468be33 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 From 5e4ed51045b97f595d2e4d6287406b227763d43d Mon Sep 17 00:00:00 2001 From: Alex Wichmann Date: Wed, 11 Feb 2026 21:51:07 +0100 Subject: [PATCH 04/15] fix: add `ApplicableVersions` property to validation rules (#25) * Add `ApplicableVersions` property to validation rules Added a new public property `AsyncApiVersion[] ApplicableVersions` in the base class `ValidationRule`. Updated existing rule classes (`OperationRequiredFields`, etc.) with `[AsyncApiVersionRule]` attributes. Modified `ValidationRuleSet` to set applicable versions for each rule based on custom attribute. * Update test/ByteBard.AsyncAPI.Tests/Validation/ValidationRuleTests.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Alex Wichmann * fix coderabbit fubar * refactor validate to keep version --------- Signed-off-by: Alex Wichmann Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../Validation/AsyncApiValidator.cs | 19 +++- .../Validation/Rules/AsyncApiDocumentRules.cs | 18 +++- .../Rules/AsyncApiOperationRules.cs | 3 + .../Validation/ValidationRule.cs | 6 ++ .../Validation/ValidationRuleSet.cs | 40 +++++++- .../Validation/ValidationRuleTests.cs | 93 +++++++++++++++++++ .../Validation/ValidationRulesetTests.cs | 2 +- 7 files changed, 176 insertions(+), 5 deletions(-) diff --git a/src/ByteBard.AsyncAPI/Validation/AsyncApiValidator.cs b/src/ByteBard.AsyncAPI/Validation/AsyncApiValidator.cs index a34f996..61a6e36 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/AsyncApiDocumentRules.cs b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiDocumentRules.cs index f1f8f4f..b96f94d 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 85548b8..60be46e 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/ValidationRule.cs b/src/ByteBard.AsyncAPI/Validation/ValidationRule.cs index db4187d..ba65426 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 46b7181..8d70cba 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/Validation/ValidationRuleTests.cs b/test/ByteBard.AsyncAPI.Tests/Validation/ValidationRuleTests.cs index 6aedaf1..6fe766c 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 2620d9b..b5ad517 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(28, rules.Count); } } } From 7cea38beda2ff5340020ca3c9262268c723a065f Mon Sep 17 00:00:00 2001 From: Alex Wichmann Date: Sun, 15 Feb 2026 22:36:55 +0100 Subject: [PATCH 05/15] refactor(bindings)!: binding serialization for AsyncAPI v2/v3 (#27) * Refactors binding serialization for AsyncAPI v2/v3 Consolidates binding serialization logic for AsyncAPI v2 and v3. The change introduces `SerializeV2` and `SerializeV3` methods in binding classes, deprecating the old `SerializeProperties` to streamline the serialization process. Additionally, it adds a `SerializationContext` to the `AsyncApiWorkspace` to enable bindings to access parent context during serialization. This addresses inconsistencies in binding serialization across different AsyncAPI versions, and allows http bindings to serialize according to the spec. * Update src/ByteBard.AsyncAPI.Bindings/Http/HttpMessageBinding.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Alex Wichmann * Update src/ByteBard.AsyncAPI/Models/AsyncApiChannel.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Alex Wichmann --------- Signed-off-by: Alex Wichmann Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../AMQP/AMQPChannelBinding.cs | 13 +- .../AMQP/AMQPMessageBinding.cs | 10 ++ .../AMQP/AMQPOperationBinding.cs | 13 +- .../Http/HttpMessageBinding.cs | 61 ++++++-- .../Http/HttpOperationBinding.cs | 76 ++++++--- .../Kafka/KafkaChannelBinding.cs | 13 +- .../Kafka/KafkaMessageBinding.cs | 16 +- .../Kafka/KafkaOperationBinding.cs | 13 +- .../Kafka/KafkaServerBinding.cs | 13 +- .../MQTT/MQTTMessageBinding.cs | 10 ++ .../MQTT/MQTTOperationBinding.cs | 13 +- .../MQTT/MQTTServerBinding.cs | 13 +- .../Pulsar/PulsarChannelBinding.cs | 10 ++ .../Pulsar/PulsarServerBinding.cs | 10 ++ .../Sns/SnsChannelBinding.cs | 11 +- .../Sns/SnsOperationBinding.cs | 10 ++ .../Sqs/SqsChannelBinding.cs | 10 ++ .../Sqs/SqsOperationBinding.cs | 10 ++ .../WebSockets/WebSocketsChannelBinding.cs | 10 ++ src/ByteBard.AsyncAPI/AsyncApiWorkspace.cs | 18 +++ .../Models/AsyncApiBinding.cs | 20 +-- .../Models/AsyncApiBindings{TBinding}.cs | 27 +++- .../Models/AsyncApiChannel.cs | 104 +++++++------ .../Models/AsyncApiConstants.cs | 1 + .../Models/AsyncApiMessage.cs | 90 ++++++----- .../Models/AsyncApiOperation.cs | 88 ++++++----- .../AsyncApiDocumentV2Tests.cs | 1 + .../Bindings/CustomBinding_Should.cs | 10 ++ .../Bindings/Http/HttpBindings_Should.cs | 145 +++++++++++++++--- .../Bindings/StringOrStringList_Should.cs | 10 ++ .../Models/AsyncApiMessage_Should.cs | 2 + .../Models/AsyncApiOperation_Should.cs | 5 +- 32 files changed, 638 insertions(+), 218 deletions(-) diff --git a/src/ByteBard.AsyncAPI.Bindings/AMQP/AMQPChannelBinding.cs b/src/ByteBard.AsyncAPI.Bindings/AMQP/AMQPChannelBinding.cs index 81e070f..410ca45 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 42abf49..8fb273b 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 dbc7f5d..7f49bd3 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 362f1ee..5a76874 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 6d5126f..78b0c7f 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 7e5fc79..bb96af5 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 59ead5f..1a8a961 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 8c7081e..4b3988d 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 d95bf5d..9c6b569 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 e3ab20d..44cec88 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 f9a1e74..fb593aa 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 79e0ce6..a137f8b 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 b1fda8d..8bc218e 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 c283ce5..1611da7 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 c8a8f21..64c99df 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 45bf7a0..b49ac99 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 0d85d23..f72c187 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 7583b3d..2b6c0b0 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 fe24b87..7055634 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/AsyncApiWorkspace.cs b/src/ByteBard.AsyncAPI/AsyncApiWorkspace.cs index 94f4c23..ff3ddae 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) diff --git a/src/ByteBard.AsyncAPI/Models/AsyncApiBinding.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiBinding.cs index 22730d7..68287a8 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 2bebd67..4a2149b 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 945be14..87b0a25 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 1f3d84e..d38469c 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/AsyncApiMessage.cs b/src/ByteBard.AsyncAPI/Models/AsyncApiMessage.cs index 3f5adf7..05a168e 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 d7ea083..9b04d30 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/test/ByteBard.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs b/test/ByteBard.AsyncAPI.Tests/AsyncApiDocumentV2Tests.cs index a1ff9e9..5386816 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/Bindings/CustomBinding_Should.cs b/test/ByteBard.AsyncAPI.Tests/Bindings/CustomBinding_Should.cs index 6d4da98..7b86241 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 3590d0a..b66eb67 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 7e344c0..dbe4c35 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/Models/AsyncApiMessage_Should.cs b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiMessage_Should.cs index 7ca108c..e77acf5 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 27eeae6..27391a6 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 From eeb9d8f8aedd811b2fc4c32c692868ff47a00824 Mon Sep 17 00:00:00 2001 From: Arkadiusz Biel <2244074+bielu@users.noreply.github.com> Date: Wed, 25 Feb 2026 08:39:21 +0000 Subject: [PATCH 06/15] fix: small adjustments in deserialization validation (#29) * Small adjustments in deserialization validation * fix: correct validation rules for v2 * fix: remove redudant rule * fix: add v3 host property to map There is bug that validation parse v2 twice, it causes url to be already mapped to host which cause issues. As dirty fix i added 2 properties related to v3... * Revert "fix: add v3 host property to map" This reverts commit df52c0630e699dba7bbca5e897bf1b365b931775. * fix: add server to components * fix: solve variable conflict * fix: make sure nesting is correct * fix: correcred reference name --- .../AsyncApiJsonDocumentReader.cs | 34 +++++++++++-------- .../V2/AsyncApiChannelDeserializer.cs | 8 ++++- .../V3/AsyncApiDocumentDeserializer.cs | 2 +- src/ByteBard.AsyncAPI/AsyncApiWorkspace.cs | 18 ++++++++++ 4 files changed, 45 insertions(+), 17 deletions(-) diff --git a/src/ByteBard.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs b/src/ByteBard.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs index d0cf8c3..c02632b 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, @@ -363,58 +364,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/V2/AsyncApiChannelDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiChannelDeserializer.cs index df26b17..424da0a 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 38890a0..f032ab6 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 ff3ddae..3f48893 100644 --- a/src/ByteBard.AsyncAPI/AsyncApiWorkspace.cs +++ b/src/ByteBard.AsyncAPI/AsyncApiWorkspace.cs @@ -204,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) From 46671f6c2d2194c24a81782a59fcde2a9fdba1f5 Mon Sep 17 00:00:00 2001 From: Alex Wichmann Date: Mon, 8 Jun 2026 08:27:17 +0200 Subject: [PATCH 07/15] ci: add Obfuscan workflow for pull requests --- .github/workflows/ci.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b00e7d..a57eb67 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: read + 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 }} From 5a276186e2e1e5908b1aebabcf93bde41934c1af Mon Sep 17 00:00:00 2001 From: Alex Wichmann Date: Mon, 8 Jun 2026 09:40:25 +0200 Subject: [PATCH 08/15] Update ci.yml Signed-off-by: Alex Wichmann --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a57eb67..5d3fcc9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - pull-requests: read + pull-requests: write issues: write steps: - uses: actions/checkout@v4 From 310868a6e6cb9d0a4feba3ba0b473c031e1d494e Mon Sep 17 00:00:00 2001 From: Alex Wichmann Date: Mon, 8 Jun 2026 09:42:56 +0200 Subject: [PATCH 09/15] fix(avro)!: support named type references (#32) * feat: add Obfuscan workflow for pull requests Adds a new job named Obfuscan to the CI workflow to scan the pull request diff using the ByteBardOrg/obfuscan-action. This job runs only when a pull request is opened, and it uses the head SHA of the pull request to check for potential issues in the code changes before the main build proceeds. * fix(avro)!: support named type references Adds Avro-specific named type handling for schemas that reference previously defined records, enums, or fixed types by name. Introduces `AvroNamedType`, keeps AsyncAPI `$ref` handling unchanged, supports recursive schemas like `LongList`, and widens map `values` to accept any Avro schema instead of only primitive types. Existing construction with `AvroPrimitiveType` is preserved through the existing implicit conversion to `AsyncApiAvroSchema`, and primitive schema values can be converted back with an explicit cast. BREAKING CHANGE: `AvroMap.Values` now uses `AsyncApiAvroSchema` instead of `AvroPrimitiveType`. * refactor: error and warning collection in document reader Changed how validation errors and warnings are added to the diagnostic collection. Previously, all items from the validation result were iterated over. This change explicitly separates the handling of AsyncApiValidatorError into diagnostic.Errors and AsyncApiValidatorWarning into diagnostic.Warnings, ensuring correct categorization of validation feedback. * docs: add schema wiki page * remove docs file --- .../AsyncApiJsonDocumentReader.cs | 7 +- .../Schemas/AsyncApiAvroSchemaDeserializer.cs | 455 +++++++++++++----- .../Models/Avro/AsyncApiAvroSchema.cs | 24 +- src/ByteBard.AsyncAPI/Models/Avro/AvroMap.cs | 16 +- .../Models/Avro/AvroNamedType.cs | 74 +++ .../Services/AsyncApiWalker.cs | 44 +- .../Validation/Rules/AsyncApiAvroRules.cs | 12 + .../Models/AvroSchema_Should.cs | 274 +++++++++++ .../Validation/ValidationRulesetTests.cs | 2 +- 9 files changed, 771 insertions(+), 137 deletions(-) create mode 100644 src/ByteBard.AsyncAPI/Models/Avro/AvroNamedType.cs diff --git a/src/ByteBard.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs b/src/ByteBard.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs index c02632b..e51742d 100644 --- a/src/ByteBard.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs +++ b/src/ByteBard.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs @@ -165,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; diff --git a/src/ByteBard.AsyncAPI.Readers/Schemas/AsyncApiAvroSchemaDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/Schemas/AsyncApiAvroSchemaDeserializer.cs index d67777e..fb749e8 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/Models/Avro/AsyncApiAvroSchema.cs b/src/ByteBard.AsyncAPI/Models/Avro/AsyncApiAvroSchema.cs index bf847de..1ba1de5 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 3799c7a..55db22b 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 0000000..1f53a2e --- /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 21c610d..5fc5e7a 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) @@ -1218,4 +1260,4 @@ public void Walk(IAsyncApiElement element) } } } -} \ No newline at end of file +} diff --git a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiAvroRules.cs b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiAvroRules.cs index 78cf1c3..0a289be 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/test/ByteBard.AsyncAPI.Tests/Models/AvroSchema_Should.cs b/test/ByteBard.AsyncAPI.Tests/Models/AvroSchema_Should.cs index 9d6d57b..30a56d3 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/Validation/ValidationRulesetTests.cs b/test/ByteBard.AsyncAPI.Tests/Validation/ValidationRulesetTests.cs index b5ad517..6a92c42 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(28, rules.Count); + Assert.AreEqual(29, rules.Count); } } } From 723a99dc487fe39708803c394facd948899a8f68 Mon Sep 17 00:00:00 2001 From: Alex Wichmann Date: Mon, 8 Jun 2026 09:43:11 +0200 Subject: [PATCH 10/15] build: update build .net build targets (#33) * build: update build .net build targets * reset ci.yml --- .github/workflows/ci.yml | 10 ++++++---- .github/workflows/release-beta.yml | 9 +++++++-- .github/workflows/release-package.yml | 9 +++++++-- Common.Build.props | 4 ++-- .../ByteBard.AsyncAPI.Readers.JsonExample.csproj | 2 +- .../ByteBard.AsyncAPI.Readers.YamlExample.csproj | 2 +- .../ByteBard.AsyncAPI.Writers.Example.csproj | 2 +- .../ByteBard.AsyncAPI.Tests.csproj | 2 +- 8 files changed, 26 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d3fcc9..5757f77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,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/release-beta.yml b/.github/workflows/release-beta.yml index aed2754..ae2b2cc 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 48484f3..98e8b99 100644 --- a/.github/workflows/release-package.yml +++ b/.github/workflows/release-package.yml @@ -66,9 +66,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/Common.Build.props b/Common.Build.props index 7f7d0ea..69bcd2c 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 e47ee8b..871197d 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 2ebfbf6..8e87447 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 4e9e52b..c76ec4c 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/test/ByteBard.AsyncAPI.Tests/ByteBard.AsyncAPI.Tests.csproj b/test/ByteBard.AsyncAPI.Tests/ByteBard.AsyncAPI.Tests.csproj index 36f6007..27f4ba1 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 From d60f7c1059e0f7f317ebab1c6d9f8f403a38a055 Mon Sep 17 00:00:00 2001 From: Alex Wichmann Date: Mon, 8 Jun 2026 09:45:07 +0200 Subject: [PATCH 11/15] Potential fix for code scanning alert no. 4: Workflow does not contain permissions (#34) Signed-off-by: Alex Wichmann Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/pr-title-lint.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/pr-title-lint.yml b/.github/workflows/pr-title-lint.yml index abe0796..4858b42 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 From 5935b7676efc0dce0058b731526f9187b3de0e89 Mon Sep 17 00:00:00 2001 From: Alex Wichmann Date: Sun, 21 Jun 2026 20:47:32 +0200 Subject: [PATCH 12/15] Update description in .asyncapi-tool file Signed-off-by: Alex Wichmann --- .asyncapi-tool | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.asyncapi-tool b/.asyncapi-tool index 4f80ea8..5207f55 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" From 0496eb5549abf3095e960ed870fa25eb71f463c6 Mon Sep 17 00:00:00 2001 From: Arkadiusz Biel <2244074+bielu@users.noreply.github.com> Date: Mon, 6 Jul 2026 05:44:40 +0100 Subject: [PATCH 13/15] fix: corrected rule (#36) --- .../Validation/Rules/AsyncApiSecuritySchemaRules.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiSecuritySchemaRules.cs b/src/ByteBard.AsyncAPI/Validation/Rules/AsyncApiSecuritySchemaRules.cs index 821178c..7529cd8 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 }, From 4d9db864b784fd97f474c1d0ca978c3203995fd8 Mon Sep 17 00:00:00 2001 From: Alex Wichmann Date: Fri, 14 Aug 2026 00:40:27 +0200 Subject: [PATCH 14/15] chore: update changelog. --- CHANGELOG.md | 188 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 160 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cd7709..ab561d6 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)) From 70b38855fdca1d7cbeec2943f16bdd8b739efa25 Mon Sep 17 00:00:00 2001 From: Alex Wichmann Date: Fri, 14 Aug 2026 00:41:12 +0200 Subject: [PATCH 15/15] ci: update semantic release configuration --- .github/workflows/release-package.yml | 43 +++++++++------------------ release.config.js | 28 ++++++++++------- 2 files changed, 32 insertions(+), 39 deletions(-) diff --git a/.github/workflows/release-package.yml b/.github/workflows/release-package.yml index 98e8b99..6e08725 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 }} diff --git a/release.config.js b/release.config.js index 5348414..9b0e4db 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}", + }, + ], + ], +};