From 120dd418418a5f8d7725dc23bdf7025472baf631 Mon Sep 17 00:00:00 2001 From: VisualBean Date: Tue, 17 Jun 2025 01:00:44 +0200 Subject: [PATCH 1/5] custom formats for schemas --- .../Http/HttpMessageBinding.cs | 2 +- .../Http/HttpOperationBinding.cs | 2 +- .../Kafka/KafkaMessageBinding.cs | 2 +- .../Kafka/KafkaOperationBinding.cs | 4 +- .../MQTT/MQTTMessageBinding.cs | 2 +- .../WebSockets/WebSocketsChannelBinding.cs | 4 +- .../AsyncApiJsonDocumentReader.cs | 3 ++ .../AsyncApiReaderSettings.cs | 3 ++ .../ParsingContext.cs | 2 + .../AsyncApiAvroSchemaDeserializer.cs | 0 .../AsyncApiJsonSchemaDeserializer.cs} | 2 +- .../Schemas/AvroSchemaParser.cs | 22 ++++++++ .../Schemas/ISchemaParser.cs | 11 ++++ .../Schemas/JsonSchemaParser.cs | 22 ++++++++ .../Schemas/SchemaParserRegistry.cs | 51 +++++++++++++++++++ .../V2/AsyncApiComponentsDeserializer.cs | 2 +- .../V2/AsyncApiMessageDeserializer.cs | 4 +- .../V2/AsyncApiMessageTraitDeserializer.cs | 2 +- .../V2/AsyncApiParameterDeserializer.cs | 2 +- .../V2/AsyncApiV2VersionService.cs | 2 +- .../AsyncApiMultiFormatSchemaDeserializer.cs | 49 +++--------------- .../V3/AsyncApiParameterDeserializer.cs | 2 +- .../V3/AsyncApiV3VersionService.cs | 2 +- 23 files changed, 139 insertions(+), 58 deletions(-) rename src/ByteBard.AsyncAPI.Readers/{V2 => Schemas}/AsyncApiAvroSchemaDeserializer.cs (100%) rename src/ByteBard.AsyncAPI.Readers/{V2/AsyncApiSchemaDeserializer.cs => Schemas/AsyncApiJsonSchemaDeserializer.cs} (99%) create mode 100644 src/ByteBard.AsyncAPI.Readers/Schemas/AvroSchemaParser.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/Schemas/ISchemaParser.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/Schemas/JsonSchemaParser.cs create mode 100644 src/ByteBard.AsyncAPI.Readers/Schemas/SchemaParserRegistry.cs diff --git a/src/ByteBard.AsyncAPI.Bindings/Http/HttpMessageBinding.cs b/src/ByteBard.AsyncAPI.Bindings/Http/HttpMessageBinding.cs index d5c8f49..362f1ee 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Http/HttpMessageBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Http/HttpMessageBinding.cs @@ -40,7 +40,7 @@ public override void SerializeProperties(IAsyncApiWriter writer) protected override FixedFieldMap FixedFieldMap => new() { { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, - { "headers", (a, n) => { a.Headers = AsyncApiSchemaDeserializer.LoadSchema(n); } }, + { "headers", (a, n) => { a.Headers = AsyncApiJsonSchemaDeserializer.LoadSchema(n); } }, }; } } diff --git a/src/ByteBard.AsyncAPI.Bindings/Http/HttpOperationBinding.cs b/src/ByteBard.AsyncAPI.Bindings/Http/HttpOperationBinding.cs index 8587a1d..6d5126f 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Http/HttpOperationBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Http/HttpOperationBinding.cs @@ -61,7 +61,7 @@ public override void SerializeProperties(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 = AsyncApiSchemaDeserializer.LoadSchema(n); } }, + { "query", (a, n) => { a.Query = AsyncApiJsonSchemaDeserializer.LoadSchema(n); } }, }; public override string BindingKey => "http"; diff --git a/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaMessageBinding.cs b/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaMessageBinding.cs index 6e7304a..59ead5f 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaMessageBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaMessageBinding.cs @@ -65,7 +65,7 @@ public override void SerializeProperties(IAsyncApiWriter writer) protected override FixedFieldMap FixedFieldMap => new() { { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, - { "key", (a, n) => { a.Key = AsyncApiSchemaDeserializer.LoadSchema(n); } }, + { "key", (a, n) => { a.Key = AsyncApiJsonSchemaDeserializer.LoadSchema(n); } }, { "schemaIdLocation", (a, n) => { a.SchemaIdLocation = n.GetScalarValue(); } }, { "schemaIdPayloadEncoding", (a, n) => { a.SchemaIdPayloadEncoding = n.GetScalarValue(); } }, { "schemaLookupStrategy", (a, n) => { a.SchemaLookupStrategy = n.GetScalarValue(); } }, diff --git a/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaOperationBinding.cs b/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaOperationBinding.cs index 2102dde..8c7081e 100644 --- a/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaOperationBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/Kafka/KafkaOperationBinding.cs @@ -26,8 +26,8 @@ public class KafkaOperationBinding : OperationBinding protected override FixedFieldMap FixedFieldMap => new() { { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, - { "groupId", (a, n) => { a.GroupId = AsyncApiSchemaDeserializer.LoadSchema(n); } }, - { "clientId", (a, n) => { a.ClientId = AsyncApiSchemaDeserializer.LoadSchema(n); } }, + { "groupId", (a, n) => { a.GroupId = AsyncApiJsonSchemaDeserializer.LoadSchema(n); } }, + { "clientId", (a, n) => { a.ClientId = AsyncApiJsonSchemaDeserializer.LoadSchema(n); } }, }; /// diff --git a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs index 6aeccf6..e3ab20d 100644 --- a/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/MQTT/MQTTMessageBinding.cs @@ -55,7 +55,7 @@ public override void SerializeProperties(IAsyncApiWriter writer) protected override FixedFieldMap FixedFieldMap => new() { { "payloadFormatIndicator", (a, n) => { a.PayloadFormatIndicator = n.GetIntegerValueOrDefault(); } }, - { "correlationData", (a, n) => { a.CorrelationData = AsyncApiSchemaDeserializer.LoadSchema(n); } }, + { "correlationData", (a, n) => { a.CorrelationData = AsyncApiJsonSchemaDeserializer.LoadSchema(n); } }, { "contentType", (a, n) => { a.ContentType = n.GetScalarValue(); } }, { "responseTopic", (a, n) => { a.ResponseTopic = n.GetScalarValue(); } }, }; diff --git a/src/ByteBard.AsyncAPI.Bindings/WebSockets/WebSocketsChannelBinding.cs b/src/ByteBard.AsyncAPI.Bindings/WebSockets/WebSocketsChannelBinding.cs index 2a1f17c..fe24b87 100644 --- a/src/ByteBard.AsyncAPI.Bindings/WebSockets/WebSocketsChannelBinding.cs +++ b/src/ByteBard.AsyncAPI.Bindings/WebSockets/WebSocketsChannelBinding.cs @@ -29,8 +29,8 @@ public class WebSocketsChannelBinding : ChannelBinding { { "bindingVersion", (a, n) => { a.BindingVersion = n.GetScalarValue(); } }, { "method", (a, n) => { a.Method = n.GetScalarValue(); } }, - { "query", (a, n) => { a.Query = AsyncApiSchemaDeserializer.LoadSchema(n); } }, - { "headers", (a, n) => { a.Headers = AsyncApiSchemaDeserializer.LoadSchema(n); } }, + { "query", (a, n) => { a.Query = AsyncApiJsonSchemaDeserializer.LoadSchema(n); } }, + { "headers", (a, n) => { a.Headers = AsyncApiJsonSchemaDeserializer.LoadSchema(n); } }, }; public override void SerializeProperties(IAsyncApiWriter writer) diff --git a/src/ByteBard.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs b/src/ByteBard.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs index 9cb862a..ab5586e 100644 --- a/src/ByteBard.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs +++ b/src/ByteBard.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs @@ -50,6 +50,7 @@ public AsyncApiDocument Read(JsonNode input, out AsyncApiDiagnostic diagnostic) ChannelBindingParsers = this.settings.Bindings.OfType>().ToDictionary(b => b.BindingKey, b => b), OperationBindingParsers = this.settings.Bindings.OfType>().ToDictionary(b => b.BindingKey, b => b), MessageBindingParsers = this.settings.Bindings.OfType>().ToDictionary(b => b.BindingKey, b => b), + SchemaParserRegistry = this.settings.SchemaParserRegistry, }; AsyncApiDocument document = null; @@ -90,6 +91,7 @@ public async Task ReadAsync(JsonNode input, CancellationToken cancel ChannelBindingParsers = this.settings.Bindings.OfType>().ToDictionary(b => b.BindingKey, b => b), OperationBindingParsers = this.settings.Bindings.OfType>().ToDictionary(b => b.BindingKey, b => b), MessageBindingParsers = this.settings.Bindings.OfType>().ToDictionary(b => b.BindingKey, b => b), + SchemaParserRegistry = this.settings.SchemaParserRegistry, }; AsyncApiDocument document = null; @@ -144,6 +146,7 @@ public T ReadFragment(JsonNode input, AsyncApiVersion version, out AsyncApiDi ChannelBindingParsers = this.settings.Bindings.OfType>().ToDictionary(b => b.BindingKey, b => b), OperationBindingParsers = this.settings.Bindings.OfType>().ToDictionary(b => b.BindingKey, b => b), MessageBindingParsers = this.settings.Bindings.OfType>().ToDictionary(b => b.BindingKey, b => b), + SchemaParserRegistry = this.settings.SchemaParserRegistry, }; IAsyncApiElement element = null; diff --git a/src/ByteBard.AsyncAPI.Readers/AsyncApiReaderSettings.cs b/src/ByteBard.AsyncAPI.Readers/AsyncApiReaderSettings.cs index 54baa13..55575d1 100644 --- a/src/ByteBard.AsyncAPI.Readers/AsyncApiReaderSettings.cs +++ b/src/ByteBard.AsyncAPI.Readers/AsyncApiReaderSettings.cs @@ -3,6 +3,7 @@ namespace ByteBard.AsyncAPI.Readers using System; using System.Collections.Generic; using System.IO; + using System.Runtime.CompilerServices; using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Models.Interfaces; using ByteBard.AsyncAPI.Readers.Interface; @@ -56,6 +57,8 @@ public ICollection> { get; set; } = new List>(); + public SchemaParserRegistry SchemaParserRegistry { get; set; } = new SchemaParserRegistry(); + /// /// Rules to use for validating AsyncApi specification. If none are provided a default set of rules are applied. /// diff --git a/src/ByteBard.AsyncAPI.Readers/ParsingContext.cs b/src/ByteBard.AsyncAPI.Readers/ParsingContext.cs index e546d64..9b8fc9a 100644 --- a/src/ByteBard.AsyncAPI.Readers/ParsingContext.cs +++ b/src/ByteBard.AsyncAPI.Readers/ParsingContext.cs @@ -36,6 +36,8 @@ internal Dictionary> ExtensionPars internal Dictionary> OperationBindingParsers { get; set; } = new(); internal Dictionary> MessageBindingParsers { get; set; } = new(); + + internal SchemaParserRegistry SchemaParserRegistry { get; set; } = new(); internal RootNode RootNode { get; set; } diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiAvroSchemaDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/Schemas/AsyncApiAvroSchemaDeserializer.cs similarity index 100% rename from src/ByteBard.AsyncAPI.Readers/V2/AsyncApiAvroSchemaDeserializer.cs rename to src/ByteBard.AsyncAPI.Readers/Schemas/AsyncApiAvroSchemaDeserializer.cs diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiSchemaDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/Schemas/AsyncApiJsonSchemaDeserializer.cs similarity index 99% rename from src/ByteBard.AsyncAPI.Readers/V2/AsyncApiSchemaDeserializer.cs rename to src/ByteBard.AsyncAPI.Readers/Schemas/AsyncApiJsonSchemaDeserializer.cs index 1cecdf7..8a170e9 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiSchemaDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/Schemas/AsyncApiJsonSchemaDeserializer.cs @@ -6,7 +6,7 @@ namespace ByteBard.AsyncAPI.Readers using ByteBard.AsyncAPI.Models; using ByteBard.AsyncAPI.Readers.ParseNodes; - public class AsyncApiSchemaDeserializer + public class AsyncApiJsonSchemaDeserializer { private static readonly FixedFieldMap schemaFixedFields = new() { diff --git a/src/ByteBard.AsyncAPI.Readers/Schemas/AvroSchemaParser.cs b/src/ByteBard.AsyncAPI.Readers/Schemas/AvroSchemaParser.cs new file mode 100644 index 0000000..2ec559a --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/Schemas/AvroSchemaParser.cs @@ -0,0 +1,22 @@ +namespace ByteBard.AsyncAPI.Readers; + +using System.Collections.Generic; +using Models.Interfaces; +using ParseNodes; + +public class AvroSchemaParser : ISchemaParser +{ + public IAsyncApiSchema LoadSchema(ParseNode parseNode) + { + return AsyncApiAvroSchemaDeserializer.LoadSchema(parseNode); + } + + public IEnumerable SupportedFormats => new List + { + "application/vnd.apache.avro", + "application/vnd.apache.avro+json", + "application/vnd.apache.avro+yaml", + "application/vnd.apache.avro+json;version=1.9.0", + "application/vnd.apache.avro+yaml;version=1.9.0", + }; +} \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI.Readers/Schemas/ISchemaParser.cs b/src/ByteBard.AsyncAPI.Readers/Schemas/ISchemaParser.cs new file mode 100644 index 0000000..06df5e5 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/Schemas/ISchemaParser.cs @@ -0,0 +1,11 @@ +namespace ByteBard.AsyncAPI.Readers; + +using System.Collections.Generic; +using Models.Interfaces; +using ParseNodes; + +public interface ISchemaParser +{ + IAsyncApiSchema LoadSchema(ParseNode node); + IEnumerable SupportedFormats { get; } +} \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI.Readers/Schemas/JsonSchemaParser.cs b/src/ByteBard.AsyncAPI.Readers/Schemas/JsonSchemaParser.cs new file mode 100644 index 0000000..540903d --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/Schemas/JsonSchemaParser.cs @@ -0,0 +1,22 @@ +namespace ByteBard.AsyncAPI.Readers; + +using System.Collections.Generic; +using Models.Interfaces; +using ParseNodes; + +public class JsonSchemaParser: ISchemaParser +{ + public IAsyncApiSchema LoadSchema(ParseNode node) + { + return AsyncApiJsonSchemaDeserializer.LoadSchema(node); + } + + public IEnumerable SupportedFormats => new List + { + "application/vnd.aai.asyncapi", + "application/vnd.aai.asyncapi+json", + "application/vnd.aai.asyncapi+yaml", + "application/schema+json;version=draft-07", + "application/schema+yaml;version=draft-07", + }; +} \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI.Readers/Schemas/SchemaParserRegistry.cs b/src/ByteBard.AsyncAPI.Readers/Schemas/SchemaParserRegistry.cs new file mode 100644 index 0000000..b30a715 --- /dev/null +++ b/src/ByteBard.AsyncAPI.Readers/Schemas/SchemaParserRegistry.cs @@ -0,0 +1,51 @@ +namespace ByteBard.AsyncAPI.Readers; + +using System.Collections.Generic; +using System.Linq; + +public class SchemaParserRegistry +{ + public SchemaParserRegistry() + { + this.RegisterParser(new JsonSchemaParser()); + this.RegisterParser(new AvroSchemaParser()); + } + + private readonly Dictionary parsers = new(); + private readonly Dictionary formatToPrefix = new(); + + public void RegisterParser(ISchemaParser deserializer) + { + foreach (var format in deserializer.SupportedFormats) + { + this.parsers[format] = deserializer; + this.formatToPrefix[format] = format; + } + } + + public ISchemaParser GetParser(string format) + { + if (string.IsNullOrEmpty(format)) + { + return this.parsers.Values.FirstOrDefault(d => d is JsonSchemaParser); + } + + if (this.parsers.TryGetValue(format, out var parser)) + { + return parser; + } + + var matchingPrefix = this.formatToPrefix.Keys.FirstOrDefault(prefix => format.StartsWith(prefix)); + if (matchingPrefix != null) + { + return this.parsers[matchingPrefix]; + } + + return null; + } + + public IEnumerable GetSupportedFormats() + { + return this.parsers.Keys; + } +} \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiComponentsDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiComponentsDeserializer.cs index da5dbfd..a4386dc 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiComponentsDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiComponentsDeserializer.cs @@ -43,7 +43,7 @@ private static AsyncApiMultiFormatSchema LoadMultiSchemaFormat(ParseNode node) { var schemas = new AsyncApiMultiFormatSchema { - Schema = AsyncApiSchemaDeserializer.LoadSchema(node), + Schema = AsyncApiJsonSchemaDeserializer.LoadSchema(node), }; return schemas; diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageDeserializer.cs index d995c0f..618f2f8 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageDeserializer.cs @@ -85,7 +85,7 @@ private static IAsyncApiSchema LoadPayload(ParseNode n, string format) case null: case "": case var _ when SupportedJsonSchemaFormats.Where(s => format.StartsWith(s)).Any(): - return AsyncApiSchemaDeserializer.LoadSchema(n); + return AsyncApiJsonSchemaDeserializer.LoadSchema(n); case var _ when SupportedAvroSchemaFormats.Where(s => format.StartsWith(s)).Any(): return AsyncApiAvroSchemaDeserializer.LoadSchema(n); default: @@ -143,7 +143,7 @@ public static AsyncApiMessage LoadMessage(ParseNode node) if (mapNode["headers"] != null) { - message.Headers = new AsyncApiMultiFormatSchema { Schema = AsyncApiSchemaDeserializer.LoadSchema(mapNode["headers"].Value) }; + message.Headers = new AsyncApiMultiFormatSchema { Schema = AsyncApiJsonSchemaDeserializer.LoadSchema(mapNode["headers"].Value) }; } if (mapNode["payload"] != null) diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageTraitDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageTraitDeserializer.cs index a013c01..73e479c 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageTraitDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiMessageTraitDeserializer.cs @@ -8,7 +8,7 @@ internal static partial class AsyncApiV2Deserializer { private static FixedFieldMap messageTraitFixedFields = new() { - { "headers", (a, n) => { a.Headers = new AsyncApiMultiFormatSchema { Schema = AsyncApiSchemaDeserializer.LoadSchema(n) }; } }, + { "headers", (a, n) => { a.Headers = new AsyncApiMultiFormatSchema { Schema = AsyncApiJsonSchemaDeserializer.LoadSchema(n) }; } }, { "correlationId", (a, n) => { a.CorrelationId = LoadCorrelationId(n); } }, { "schemaFormat", (a, n) => { a.Headers.SchemaFormat = n.GetScalarValue(); } }, { "contentType", (a, n) => { a.ContentType = n.GetScalarValue(); } }, diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiParameterDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiParameterDeserializer.cs index 762ce2d..91e2d4f 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiParameterDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiParameterDeserializer.cs @@ -22,7 +22,7 @@ internal static partial class AsyncApiV2Deserializer private static void LoadParameterFromSchema(AsyncApiParameter instance, ParseNode node) { - var schema = AsyncApiSchemaDeserializer.LoadSchema(node); + var schema = AsyncApiJsonSchemaDeserializer.LoadSchema(node); if (schema.Enum.Any()) { instance.Enum = schema.Enum.Select(e => e.GetValue()).ToList(); diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs index ec05781..3fb980b 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs @@ -32,7 +32,7 @@ public AsyncApiV2VersionService(AsyncApiDiagnostic diagnostic) [typeof(AsyncApiOAuthFlows)] = AsyncApiV2Deserializer.LoadOAuthFlows, [typeof(AsyncApiOperation)] = AsyncApiV2Deserializer.LoadOperation, [typeof(AsyncApiParameter)] = AsyncApiV2Deserializer.LoadParameter, - [typeof(AsyncApiJsonSchema)] = AsyncApiSchemaDeserializer.LoadSchema, + [typeof(AsyncApiJsonSchema)] = AsyncApiJsonSchemaDeserializer.LoadSchema, [typeof(AsyncApiAvroSchema)] = AsyncApiAvroSchemaDeserializer.LoadSchema, [typeof(AsyncApiJsonSchema)] = AsyncApiV2Deserializer.LoadJsonSchemaPayload, [typeof(AsyncApiAvroSchema)] = AsyncApiV2Deserializer.LoadAvroPayload, diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMultiFormatSchemaDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMultiFormatSchemaDeserializer.cs index 5b132ca..c53f81a 100644 --- a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMultiFormatSchemaDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiMultiFormatSchemaDeserializer.cs @@ -31,12 +31,11 @@ public static AsyncApiMultiFormatSchema LoadMultiFormatSchema(ParseNode node) var format = mapNode["schemaFormat"].Value.GetScalarValue(); var schema = mapNode["schema"].Value; - schemaFormat.Schema = LoadSchema(schema, LoadSchemaFormat(format)); + schemaFormat.Schema = LoadSchema(schema, format); schemaFormat.SchemaFormat = format; return schemaFormat; - } - + private static IAsyncApiSchema LoadSchema(ParseNode n, string format) { if (n == null) @@ -44,47 +43,15 @@ private static IAsyncApiSchema LoadSchema(ParseNode n, string format) return null; } - switch (format) - { - case null: - case "": - case var _ when SupportedJsonSchemaFormats.Where(s => format.StartsWith(s)).Any(): - return AsyncApiSchemaDeserializer.LoadSchema(n); - case var _ when SupportedAvroSchemaFormats.Where(s => format.StartsWith(s)).Any(): - return AsyncApiAvroSchemaDeserializer.LoadSchema(n); - default: - var supportedFormats = SupportedJsonSchemaFormats.Concat(SupportedAvroSchemaFormats); - throw new AsyncApiException($"Could not deserialize Schema. Supported formats are {string.Join(", ", supportedFormats)}"); - } - } - - static readonly IEnumerable SupportedJsonSchemaFormats = new List - { - "application/vnd.aai.asyncapi+json", - "application/vnd.aai.asyncapi+yaml", - "application/vnd.aai.asyncapi", - "application/schema+json;version=draft-07", - "application/schema+yaml;version=draft-07", - }; - - static readonly IEnumerable SupportedAvroSchemaFormats = new List - { - "application/vnd.apache.avro", - "application/vnd.apache.avro+json", - "application/vnd.apache.avro+yaml", - "application/vnd.apache.avro+json;version=1.9.0", - "application/vnd.apache.avro+yaml;version=1.9.0", - }; - - private static string LoadSchemaFormat(string schemaFormat) - { - var supportedFormats = SupportedJsonSchemaFormats.Concat(SupportedAvroSchemaFormats); - if (!supportedFormats.Where(s => schemaFormat.StartsWith(s)).Any()) + var registry = n.Context.SchemaParserRegistry; + var parser = registry.GetParser(format); + if (parser != null) { - throw new AsyncApiException($"'{schemaFormat}' is not a supported format. Supported formats are {string.Join(", ", supportedFormats)}"); + return parser.LoadSchema(n); } - return schemaFormat; + var supportedFormats = registry.GetSupportedFormats(); + throw new AsyncApiException($"Could not deserialize Schema. Supported formats are {string.Join(", ", supportedFormats)}"); } } } \ No newline at end of file diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiParameterDeserializer.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiParameterDeserializer.cs index 721fd2c..e83328e 100644 --- a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiParameterDeserializer.cs +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiParameterDeserializer.cs @@ -24,7 +24,7 @@ internal static partial class AsyncApiV3Deserializer private static void LoadParameterFromSchema(AsyncApiParameter instance, ParseNode node) { - var schema = AsyncApiSchemaDeserializer.LoadSchema(node); + var schema = AsyncApiJsonSchemaDeserializer.LoadSchema(node); if (schema.Enum.Any()) { instance.Enum = schema.Enum.Select(e => e.GetValue()).ToList(); diff --git a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiV3VersionService.cs b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiV3VersionService.cs index d644807..88672a0 100644 --- a/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiV3VersionService.cs +++ b/src/ByteBard.AsyncAPI.Readers/V3/AsyncApiV3VersionService.cs @@ -34,7 +34,7 @@ public AsyncApiV3VersionService(AsyncApiDiagnostic diagnostic) [typeof(AsyncApiOperationReply)] = AsyncApiV3Deserializer.LoadOperationReply, [typeof(AsyncApiOperationReplyAddress)] = AsyncApiV3Deserializer.LoadOperationReplyAddress, [typeof(AsyncApiParameter)] = AsyncApiV3Deserializer.LoadParameter, - [typeof(AsyncApiJsonSchema)] = AsyncApiSchemaDeserializer.LoadSchema, + [typeof(AsyncApiJsonSchema)] = AsyncApiJsonSchemaDeserializer.LoadSchema, [typeof(AsyncApiAvroSchema)] = AsyncApiAvroSchemaDeserializer.LoadSchema, [typeof(AsyncApiSecurityScheme)] = AsyncApiV3Deserializer.LoadSecurityScheme, [typeof(AsyncApiMultiFormatSchema)] = AsyncApiV3Deserializer.LoadMultiFormatSchema, From e9de510f56304f1a55501c45573acdbb40d4862f Mon Sep 17 00:00:00 2001 From: VisualBean Date: Wed, 18 Jun 2025 23:01:35 +0200 Subject: [PATCH 2/5] More custom schema --- .../Schemas/AvroSchemaParser.cs | 2 +- .../Schemas/ISchemaParser.cs | 2 +- .../Schemas/JsonSchemaParser.cs | 2 +- .../Schemas/SchemaParserRegistry.cs | 6 +- .../Models/CustomSchema_Should.cs | 142 ++++++++++++++++++ 5 files changed, 148 insertions(+), 6 deletions(-) create mode 100644 test/ByteBard.AsyncAPI.Tests/Models/CustomSchema_Should.cs diff --git a/src/ByteBard.AsyncAPI.Readers/Schemas/AvroSchemaParser.cs b/src/ByteBard.AsyncAPI.Readers/Schemas/AvroSchemaParser.cs index 2ec559a..083d8ac 100644 --- a/src/ByteBard.AsyncAPI.Readers/Schemas/AvroSchemaParser.cs +++ b/src/ByteBard.AsyncAPI.Readers/Schemas/AvroSchemaParser.cs @@ -4,7 +4,7 @@ namespace ByteBard.AsyncAPI.Readers; using Models.Interfaces; using ParseNodes; -public class AvroSchemaParser : ISchemaParser +public class AvroSchemaParser : IAsyncApiSchemaParser { public IAsyncApiSchema LoadSchema(ParseNode parseNode) { diff --git a/src/ByteBard.AsyncAPI.Readers/Schemas/ISchemaParser.cs b/src/ByteBard.AsyncAPI.Readers/Schemas/ISchemaParser.cs index 06df5e5..8f03aeb 100644 --- a/src/ByteBard.AsyncAPI.Readers/Schemas/ISchemaParser.cs +++ b/src/ByteBard.AsyncAPI.Readers/Schemas/ISchemaParser.cs @@ -4,7 +4,7 @@ namespace ByteBard.AsyncAPI.Readers; using Models.Interfaces; using ParseNodes; -public interface ISchemaParser +public interface IAsyncApiSchemaParser { IAsyncApiSchema LoadSchema(ParseNode node); IEnumerable SupportedFormats { get; } diff --git a/src/ByteBard.AsyncAPI.Readers/Schemas/JsonSchemaParser.cs b/src/ByteBard.AsyncAPI.Readers/Schemas/JsonSchemaParser.cs index 540903d..1e92e22 100644 --- a/src/ByteBard.AsyncAPI.Readers/Schemas/JsonSchemaParser.cs +++ b/src/ByteBard.AsyncAPI.Readers/Schemas/JsonSchemaParser.cs @@ -4,7 +4,7 @@ namespace ByteBard.AsyncAPI.Readers; using Models.Interfaces; using ParseNodes; -public class JsonSchemaParser: ISchemaParser +public class JsonSchemaParser: IAsyncApiSchemaParser { public IAsyncApiSchema LoadSchema(ParseNode node) { diff --git a/src/ByteBard.AsyncAPI.Readers/Schemas/SchemaParserRegistry.cs b/src/ByteBard.AsyncAPI.Readers/Schemas/SchemaParserRegistry.cs index b30a715..4a1c666 100644 --- a/src/ByteBard.AsyncAPI.Readers/Schemas/SchemaParserRegistry.cs +++ b/src/ByteBard.AsyncAPI.Readers/Schemas/SchemaParserRegistry.cs @@ -11,10 +11,10 @@ public SchemaParserRegistry() this.RegisterParser(new AvroSchemaParser()); } - private readonly Dictionary parsers = new(); + private readonly Dictionary parsers = new(); private readonly Dictionary formatToPrefix = new(); - public void RegisterParser(ISchemaParser deserializer) + public void RegisterParser(IAsyncApiSchemaParser deserializer) { foreach (var format in deserializer.SupportedFormats) { @@ -23,7 +23,7 @@ public void RegisterParser(ISchemaParser deserializer) } } - public ISchemaParser GetParser(string format) + public IAsyncApiSchemaParser GetParser(string format) { if (string.IsNullOrEmpty(format)) { diff --git a/test/ByteBard.AsyncAPI.Tests/Models/CustomSchema_Should.cs b/test/ByteBard.AsyncAPI.Tests/Models/CustomSchema_Should.cs new file mode 100644 index 0000000..b2d679a --- /dev/null +++ b/test/ByteBard.AsyncAPI.Tests/Models/CustomSchema_Should.cs @@ -0,0 +1,142 @@ +namespace ByteBard.AsyncAPI.Tests.Models; + +using System.Collections.Generic; +using System.Linq; +using AsyncAPI.Models; +using AsyncAPI.Models.Interfaces; +using AsyncAPI.Writers; +using FluentAssertions; +using NUnit.Framework; +using Readers; +using Readers.ParseNodes; + +public class CustomSchema_Should +{ + public enum MySchemaType + { + One, + Two, + } + + public class MySchema : IAsyncApiSchema + { + public MySchemaType Type { get; set; } + + public int Length { get; set; } + + public string Description { get; set; } + + public IEnumerable Children { get; set; } + + public void SerializeV2(IAsyncApiWriter writer) + { + // not being tested + throw new System.NotImplementedException(); + } + + public void SerializeV3(IAsyncApiWriter writer) + { + // not being tested + throw new System.NotImplementedException(); + } + } + + public class MySchemaParser : IAsyncApiSchemaParser + { + private readonly static FixedFieldMap SchemaFixedFields = new() + { + { + "type", + (schema, node) => { schema.Type = node.GetScalarValue().GetEnumFromDisplayName(); } + }, + { + "length", (schema, node) => { schema.Length = node.GetIntegerValue(); } + }, + { + "description", (schema, node) => { schema.Description = node.GetScalarValueOrDefault("No description"); } + }, + { + "children", (schema, node) => { schema.Children = node.CreateList(Parse); } + }, + }; + + private static MySchema Parse(ParseNode node) + { + var mapNode = node.CheckMapNode("arbitrary string"); + + var schema = new MySchema(); + + // map each propery against the fixedFieldMap + foreach (var property in mapNode) + { + property.ParseField(schema, SchemaFixedFields, null); + } + + return schema; + } + + public IAsyncApiSchema LoadSchema(ParseNode node) + { + return Parse(node); + } + + public IEnumerable SupportedFormats => new string[] { "application/myschema+json" }; + } + + [Test] + public void Document_WithCustomSchema_Deserializes() + { + // Arrange + var settings = new AsyncApiReaderSettings(); + settings.SchemaParserRegistry.RegisterParser(new MySchemaParser()); + + var reader = new AsyncApiStringReader(settings); + + var input = + """ + asyncapi: 3.0.0 + info: + title: test + version: 1.0.0 + + channels: + workspace: + messages: + workspaceEvent: + $ref: '#/components/messages/WorkspaceEventPayload' + components: + messages: + WorkspaceEventPayload: + contentType: text/plain + payload: + schemaFormat: application/myschema+json + schema: + type: one + description: a test description + length: 1 + children: + - type: two + length: 2 + + """; + + // Act + var document = reader.Read(input, out var diagnostic); + + // Assert + diagnostic.Errors.Should().HaveCount(0); + + var schema = document.Components.Messages.Values.First().Payload; + schema.SchemaFormat.Should().Be("application/myschema+json"); + + var myschema = schema.Schema.As(); + myschema.Description.Should().Be("a test description"); + myschema.Length.Should().Be(1); + myschema.Type.Should().Be(MySchemaType.One); + myschema.Children.Should().HaveCount(1); + var child = myschema.Children.First(); + + child.Type.Should().Be(MySchemaType.Two); + child.Length.Should().Be(2); + } +} \ No newline at end of file From 9f307afc765f55216ec8fefc9d96387583cdb6f2 Mon Sep 17 00:00:00 2001 From: VisualBean Date: Wed, 18 Jun 2025 23:06:44 +0200 Subject: [PATCH 3/5] read only --- src/ByteBard.AsyncAPI.Readers/AsyncApiReaderSettings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ByteBard.AsyncAPI.Readers/AsyncApiReaderSettings.cs b/src/ByteBard.AsyncAPI.Readers/AsyncApiReaderSettings.cs index 55575d1..111bf26 100644 --- a/src/ByteBard.AsyncAPI.Readers/AsyncApiReaderSettings.cs +++ b/src/ByteBard.AsyncAPI.Readers/AsyncApiReaderSettings.cs @@ -57,8 +57,8 @@ public ICollection> { get; set; } = new List>(); - public SchemaParserRegistry SchemaParserRegistry { get; set; } = new SchemaParserRegistry(); - + public SchemaParserRegistry SchemaParserRegistry { get; } = new SchemaParserRegistry(); + /// /// Rules to use for validating AsyncApi specification. If none are provided a default set of rules are applied. /// From 3978b30c11517172229b219e82e73bc70bfc1bdc Mon Sep 17 00:00:00 2001 From: VisualBean Date: Wed, 18 Jun 2025 23:16:56 +0200 Subject: [PATCH 4/5] additional tests --- .../SchemaParserRegistryTests.cs | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 test/ByteBard.AsyncAPI.Tests/SchemaParserRegistryTests.cs diff --git a/test/ByteBard.AsyncAPI.Tests/SchemaParserRegistryTests.cs b/test/ByteBard.AsyncAPI.Tests/SchemaParserRegistryTests.cs new file mode 100644 index 0000000..5e32809 --- /dev/null +++ b/test/ByteBard.AsyncAPI.Tests/SchemaParserRegistryTests.cs @@ -0,0 +1,81 @@ +namespace ByteBard.AsyncAPI.Tests; +using NUnit.Framework; +using Readers; + +public class SchemaParserRegistryTests +{ + private SchemaParserRegistry registry; + + public SchemaParserRegistryTests() + { + this.registry = new SchemaParserRegistry(); + } + + [Test] + public void GetParser_WithNullFormat_ReturnsJsonSchemaParser() + { + // Arrange + // Act + var result = this.registry.GetParser(null); + + // Assert + Assert.IsNotNull(result); + Assert.True(result is JsonSchemaParser); + } + + [Test] + public void GetParser_WithEmptyFormat_ReturnsJsonSchemaParser() + { + // Arrange + // Act + var result = this.registry.GetParser(string.Empty); + + // Assert + Assert.IsNotNull(result); + Assert.True(result is JsonSchemaParser); + } + + [Test] + public void GetParser_WithExactKeyMatch_ReturnsCorrectParser() + { + // Arrange + // Act + var result = this.registry.GetParser("application/vnd.aai.asyncapi+json"); + + // Assert + Assert.IsNotNull(result); + } + + [Test] + public void GetParser_WithPrefixMatch_ReturnsCorrectParser() + { + // Arrange + // Act + var result = this.registry.GetParser("application/vnd.aai.asyncapi+json;charset=utf-8"); + + // Assert + Assert.IsNotNull(result); + } + + [Test] + public void GetParser_WithUnknownFormat_ReturnsNull() + { + // Arrange + // Act + var result = this.registry.GetParser("application/unknown+format"); + + // Assert + Assert.IsNull(result); + } + + [Test] + public void GetParser_WithNonMatchingPrefix_ReturnsNull() + { + // Arrange + // Act + var result = this.registry.GetParser("text/plain"); + + // Assert + Assert.IsNull(result); + } +} \ No newline at end of file From 5c269de629a1cacc26a35cf519eeb78714d59c54 Mon Sep 17 00:00:00 2001 From: VisualBean Date: Wed, 18 Jun 2025 23:23:35 +0200 Subject: [PATCH 5/5] for displayattribute --- test/ByteBard.AsyncAPI.Tests/Models/CustomSchema_Should.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/ByteBard.AsyncAPI.Tests/Models/CustomSchema_Should.cs b/test/ByteBard.AsyncAPI.Tests/Models/CustomSchema_Should.cs index b2d679a..74b5773 100644 --- a/test/ByteBard.AsyncAPI.Tests/Models/CustomSchema_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Models/CustomSchema_Should.cs @@ -5,6 +5,7 @@ namespace ByteBard.AsyncAPI.Tests.Models; using AsyncAPI.Models; using AsyncAPI.Models.Interfaces; using AsyncAPI.Writers; +using Attributes; using FluentAssertions; using NUnit.Framework; using Readers; @@ -14,7 +15,9 @@ public class CustomSchema_Should { public enum MySchemaType { + [Display("one")] One, + [Display("two")] Two, }