diff --git a/src/ByteBard.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs b/src/ByteBard.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs index 9b77719..a544830 100644 --- a/src/ByteBard.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs +++ b/src/ByteBard.AsyncAPI.Readers/AsyncApiJsonDocumentReader.cs @@ -410,6 +410,11 @@ private IAsyncApiSerializable ResolveStreamReference(Stream stream, IAsyncApiRef result = this.ReadFragment(json, diagnostic.SpecificationVersion, out fragmentDiagnostic); } + if (reference is AsyncApiMultiFormatSchemaReference) + { + result = this.ReadFragment(json, diagnostic.SpecificationVersion, out fragmentDiagnostic); + } + break; case ReferenceType.Server: diff --git a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs index 3fb980b..868ace6 100644 --- a/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs +++ b/src/ByteBard.AsyncAPI.Readers/V2/AsyncApiV2VersionService.cs @@ -36,6 +36,8 @@ public AsyncApiV2VersionService(AsyncApiDiagnostic diagnostic) [typeof(AsyncApiAvroSchema)] = AsyncApiAvroSchemaDeserializer.LoadSchema, [typeof(AsyncApiJsonSchema)] = AsyncApiV2Deserializer.LoadJsonSchemaPayload, [typeof(AsyncApiAvroSchema)] = AsyncApiV2Deserializer.LoadAvroPayload, + // 2.x has no Multi Format Schema Object. Only the local reference fallback asks for one, and it reads the document after the upgrade to 3.0. + [typeof(AsyncApiMultiFormatSchema)] = AsyncApiV3Deserializer.LoadMultiFormatSchema, [typeof(AsyncApiSecurityScheme)] = AsyncApiV2Deserializer.LoadSecurityScheme, [typeof(AsyncApiServer)] = AsyncApiV2Deserializer.LoadServer, [typeof(AsyncApiServerVariable)] = AsyncApiV2Deserializer.LoadServerVariable, diff --git a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs index c883748..f68a4ef 100644 --- a/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs +++ b/test/ByteBard.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs @@ -29,6 +29,19 @@ public class AsyncApiReference_Should : TestBase } """; + private const string ExternalPayloads = + """ + { + "schemas": { + "plain": { "type": "string" }, + "multiFormat": { + "schemaFormat": "application/vnd.aai.asyncapi+json;version=3.0.0", + "schema": { "type": "integer" } + } + } + } + """; + private static readonly TestCaseData[] ExternalFragmentCases = { new TestCaseData("/components/schemas/plain", SchemaType.String).SetArgDisplayNames("nested path"), @@ -138,6 +151,182 @@ private static AsyncApiDocument ReadWithExternalSchemas(string yaml, out AsyncAp return new AsyncApiStringReader(settings).Read(yaml, out diagnostic); } + [TestCase("payloads.json#/schemas/plain", SchemaType.String)] + [TestCase("payloads.json#/schemas/multiFormat", SchemaType.Integer)] + public void V3_ExternalPayloadReference_ResolvesPayload(string reference, SchemaType expectedType) + { + var yaml = + $""" + asyncapi: 3.0.0 + info: + title: test + version: 1.0.0 + channels: + workspace: + messages: + message: + payload: + $ref: '{reference}' + """; + + var settings = new AsyncApiReaderSettings + { + ReferenceResolution = ReferenceResolutionSetting.ResolveAllReferences, + ExternalReferenceLoader = new MockStringLoader(ExternalPayloads), + }; + + var doc = new AsyncApiStringReader(settings).Read(yaml, out var diagnostic); + + diagnostic.Errors.Should().BeEmpty(); + var payload = doc.Channels["workspace"].Messages["message"].Payload; + payload.Should().BeOfType(); + payload.As().UnresolvedReference.Should().BeFalse(); + payload.Schema.As().Type.Should().Be(expectedType); + doc.SerializeAsYaml(AsyncApiVersion.AsyncApi3_0).Should().Contain($"$ref: {reference}"); + } + + [Test] + public void V3_ExternalPayloadReference_WithoutFragment_ResolvesPayload() + { + var yaml = + """ + asyncapi: 3.0.0 + info: + title: test + version: 1.0.0 + channels: + workspace: + messages: + message: + payload: + $ref: './payload.json' + """; + + var settings = new AsyncApiReaderSettings + { + ReferenceResolution = ReferenceResolutionSetting.ResolveAllReferences, + ExternalReferenceLoader = new MockStringLoader("""{ "type": "boolean" }"""), + }; + + var doc = new AsyncApiStringReader(settings).Read(yaml, out var diagnostic); + + diagnostic.Errors.Should().BeEmpty(); + doc.Channels["workspace"].Messages["message"].Payload.Schema.As().Type.Should().Be(SchemaType.Boolean); + } + + // A local payload reference that is not a registered component is resolved by falling back to the whole document. + [TestCase("2.6.0")] + [TestCase("3.0.0")] + public void LocalPayloadReference_NotInWorkspace_ResolvesPayload(string version) + { + var yaml = version.StartsWith("2") + ? """ + asyncapi: 2.6.0 + info: + title: test + version: 1.0.0 + channels: + workspace: + publish: + message: + payload: + $ref: '#/components/messages/shared/payload' + components: + messages: + shared: + payload: + type: string + """ + : """ + asyncapi: 3.0.0 + info: + title: test + version: 1.0.0 + channels: + workspace: + messages: + message: + payload: + $ref: '#/components/messages/shared/payload' + components: + messages: + shared: + payload: + type: string + """; + + var doc = new AsyncApiStringReader().Read(yaml, out var diagnostic); + + diagnostic.Errors.Should().BeEmpty(); + var payload = doc.Channels["workspace"].Messages.Values.First().Payload; + payload.Should().BeOfType(); + payload.Schema.As().Type.Should().Be(SchemaType.String); + } + + // A local Avro payload reference that is not a registered component is resolved by falling back to the whole document. + [TestCase("2.6.0")] + [TestCase("3.0.0")] + public void LocalAvroPayloadReference_NotInWorkspace_ResolvesPayload(string version) + { + var yaml = version.StartsWith("2") + ? """ + asyncapi: 2.6.0 + info: + title: test + version: 1.0.0 + channels: + workspace: + publish: + message: + schemaFormat: application/vnd.apache.avro;version=1.9.0 + payload: + $ref: '#/components/messages/shared/payload' + components: + messages: + shared: + schemaFormat: application/vnd.apache.avro;version=1.9.0 + payload: + type: record + name: User + fields: + - name: name + type: string + """ + : """ + asyncapi: 3.0.0 + info: + title: test + version: 1.0.0 + channels: + workspace: + messages: + message: + payload: + $ref: '#/components/messages/shared/payload' + components: + messages: + shared: + payload: + schemaFormat: application/vnd.apache.avro;version=1.9.0 + schema: + type: record + name: User + fields: + - name: name + type: string + """; + + var doc = new AsyncApiStringReader().Read(yaml, out var diagnostic); + + diagnostic.Errors.Should().BeEmpty(); + var payload = doc.Channels["workspace"].Messages.Values.First().Payload; + payload.Should().BeOfType(); + payload.SchemaFormat.Should().StartWith("application/vnd.apache.avro"); + var record = payload.Schema.As().As(); + record.Name.Should().Be("User"); + record.Fields.Should().ContainSingle().Which.Name.Should().Be("name"); + } + [Test] public void V2_ReferencePointers() { @@ -815,4 +1004,4 @@ public Task LoadAsync(Uri baseUri, Uri uri) return Task.FromResult(this.Load(baseUri, uri)); } } -} \ No newline at end of file +}