diff --git a/src/ApiCodeGenerator.AsyncApi/ApiCodeGenerator.AsyncApi.csproj b/src/ApiCodeGenerator.AsyncApi/ApiCodeGenerator.AsyncApi.csproj index e1feae4..eb28084 100644 --- a/src/ApiCodeGenerator.AsyncApi/ApiCodeGenerator.AsyncApi.csproj +++ b/src/ApiCodeGenerator.AsyncApi/ApiCodeGenerator.AsyncApi.csproj @@ -27,7 +27,8 @@ - + + diff --git a/src/ApiCodeGenerator.AsyncApi/AsyncApiContentGenerator.cs b/src/ApiCodeGenerator.AsyncApi/AsyncApiContentGenerator.cs index 621f670..4c81399 100644 --- a/src/ApiCodeGenerator.AsyncApi/AsyncApiContentGenerator.cs +++ b/src/ApiCodeGenerator.AsyncApi/AsyncApiContentGenerator.cs @@ -92,10 +92,23 @@ private static async Task LoadDocumentAsync(GeneratorContext c var data = await context.DocumentReader!.ReadToEndAsync(); data = InvokePreprocessors(data, context.Preprocessors, context.DocumentPath, context.Logger); - var documentTask = data.StartsWith("{") - ? AsyncApiDocument.FromJsonAsync(data) - : AsyncApiDocument.FromYamlAsync(data); - var document = await documentTask.ConfigureAwait(false); + AsyncApiDocument document; + try + { + document = await AsyncApiDocument.FromJsonAsync(data, context.DocumentPath).ConfigureAwait(false); + } + catch (JsonException ex) + { + try + { + document = await AsyncApiDocument.FromYamlAsync(data, context.DocumentPath).ConfigureAwait(false); + } + catch (YamlDotNet.Core.YamlException ex2) + { + throw new InvalidOperationException( + $"Can not read document as JSON ({ex.Message}) or YAML ({ex2.Message})."); + } + } document = InvokePreprocessors(document, context.Preprocessors, context.DocumentPath, context.Logger); return document; diff --git a/src/ApiCodeGenerator.AsyncApi/DOM/AsyncApiDocument.cs b/src/ApiCodeGenerator.AsyncApi/DOM/AsyncApiDocument.cs index 1d6d54f..940d204 100644 --- a/src/ApiCodeGenerator.AsyncApi/DOM/AsyncApiDocument.cs +++ b/src/ApiCodeGenerator.AsyncApi/DOM/AsyncApiDocument.cs @@ -2,12 +2,13 @@ using Newtonsoft.Json.Linq; using NJsonSchema; using NJsonSchema.Generation; +using NJsonSchema.Yaml; using YamlDotNet.Serialization; namespace ApiCodeGenerator.AsyncApi.DOM { #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - public class AsyncApiDocument + public class AsyncApiDocument : IDocumentPathProvider { private static readonly JsonSerializerSettings JSONSERIALIZERSETTINGS = new() { @@ -41,14 +42,27 @@ public class AsyncApiDocument [JsonProperty("externalDocs", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] public ICollection? ExternalDocs { get; set; } + [JsonIgnore] + public string? DocumentPath { get; set; } + /// /// Load document from JSON text. /// /// JSON text. /// AsyncApi document object model. public static Task FromJsonAsync(string data) + => FromJsonAsync(data, null); + + /// + /// Load document from JSON text. + /// + /// JSON text. + /// Path to document. + /// AsyncApi document object model. + public static Task FromJsonAsync(string data, string? documentPath) { var document = JsonConvert.DeserializeObject(data, JSONSERIALIZERSETTINGS)!; + document.DocumentPath = documentPath; return UpdateSchemaReferencesAsync(document); } @@ -58,6 +72,15 @@ public static Task FromJsonAsync(string data) /// YAML text. /// AsyncApi document object model. public static Task FromYamlAsync(string data) + => FromYamlAsync(data, null); + + /// + /// Load document from YAML text. + /// + /// YAML text. + /// Path to document. + /// AsyncApi document object model. + public static Task FromYamlAsync(string data, string? documentPath) { var deserializer = new DeserializerBuilder().Build(); using var reader = new StringReader(data); @@ -66,14 +89,17 @@ public static Task FromYamlAsync(string data) var jObject = JObject.FromObject(yamlDocument)!; var serializer = JsonSerializer.Create(JSONSERIALIZERSETTINGS); var doc = jObject.ToObject(serializer)!; + doc.DocumentPath = documentPath; return UpdateSchemaReferencesAsync(doc); } - private static Task UpdateSchemaReferencesAsync(AsyncApiDocument document) - => JsonSchemaReferenceUtilities.UpdateSchemaReferencesAsync( - document, - new(new AsyncApiSchemaResolver(document, new SystemTextJsonSchemaGeneratorSettings()))) - .ContinueWith(t => document); + private static async Task UpdateSchemaReferencesAsync(AsyncApiDocument document) + { + await JsonSchemaReferenceUtilities.UpdateSchemaReferencesAsync( + document, + new JsonAndYamlReferenceResolver(new AsyncApiSchemaResolver(document, new SystemTextJsonSchemaGeneratorSettings()))); + return document; + } } #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. } diff --git a/src/ApiCodeGenerator.OpenApi/ApiCodeGenerator.OpenApi.csproj b/src/ApiCodeGenerator.OpenApi/ApiCodeGenerator.OpenApi.csproj index 3dff82d..50b5434 100644 --- a/src/ApiCodeGenerator.OpenApi/ApiCodeGenerator.OpenApi.csproj +++ b/src/ApiCodeGenerator.OpenApi/ApiCodeGenerator.OpenApi.csproj @@ -16,9 +16,10 @@ + - \ No newline at end of file + diff --git a/src/ApiCodeGenerator.OpenApi/ContentGeneratorBase.cs b/src/ApiCodeGenerator.OpenApi/ContentGeneratorBase.cs index f92eecb..0aff74d 100644 --- a/src/ApiCodeGenerator.OpenApi/ContentGeneratorBase.cs +++ b/src/ApiCodeGenerator.OpenApi/ContentGeneratorBase.cs @@ -11,6 +11,7 @@ using NSwag; using NSwag.CodeGeneration; using NSwag.CodeGeneration.CSharp; +using YamlDotNet.Core; namespace ApiCodeGenerator.OpenApi { @@ -126,9 +127,24 @@ protected static async Task ReadAndProcessOpenApiDocument(Gener var documentStr = context.DocumentReader!.ReadToEnd(); documentStr = InvokePreprocessors(documentStr, context.Preprocessors, context.DocumentPath, context.Logger); - var openApiDocument = !(documentStr.StartsWith("{") && documentStr.EndsWith("}")) - ? await OpenApiYamlDocument.FromYamlAsync(documentStr) - : await OpenApiDocument.FromJsonAsync(documentStr); + OpenApiDocument openApiDocument; + + try + { + openApiDocument = await OpenApiDocument.FromJsonAsync(documentStr, context.DocumentPath); + } + catch (JsonException ex) + { + try + { + openApiDocument = await OpenApiYamlDocument.FromYamlAsync(documentStr, context.DocumentPath); + } + catch (YamlException ex2) + { + throw new InvalidOperationException( + $"Can not read document as JSON ({ex.Message}) or YAML ({ex2.Message})."); + } + } openApiDocument = InvokePreprocessors(openApiDocument, context.Preprocessors, context.DocumentPath, context.Logger); return openApiDocument; diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/AsyncApiContentGeneratorTests.cs b/test/ApiCodeGenerator.AsyncApi.Tests/AsyncApiContentGeneratorTests.cs index 38b4d27..c43ef14 100644 --- a/test/ApiCodeGenerator.AsyncApi.Tests/AsyncApiContentGeneratorTests.cs +++ b/test/ApiCodeGenerator.AsyncApi.Tests/AsyncApiContentGeneratorTests.cs @@ -164,6 +164,22 @@ public async Task LoadApiDocument_WithModelPreprocess() Assert.That(sch, Is.EqualTo("{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"properties\":{\"processedModel\":{}}}")); } + [TestCase("externalRef.json")] + [TestCase("externalRef.yaml")] + public async Task LoadApiDocument_WithExternalRef(string documentPath) + { + var settingsJson = new JObject(); + var context = CreateContext(settingsJson); + context.DocumentReader = await TestHelpers.LoadApiDocumentAsync(documentPath); + context.DocumentPath = documentPath; + + var contentGenerator = (FakeContentGenerator)await FakeContentGenerator.CreateAsync(context); + + var document = contentGenerator.Document; + + Assert.NotNull(document.Components?.Messages["lightMeasured"].Reference); + } + private static Func?, object?> GetSettingsFactory(string json) => (t, s, v) => (s ?? new()).Deserialize(new StringReader(json), t); diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/asyncApi/externalRef.json b/test/ApiCodeGenerator.AsyncApi.Tests/asyncApi/externalRef.json new file mode 100644 index 0000000..bdb2233 --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/asyncApi/externalRef.json @@ -0,0 +1,15 @@ +{ + "asyncapi": "2.6.0", + "info": { + "title": "Document with reference to external schema", + "version": "1.0" + }, + "channels": {}, + "components": { + "messages": { + "lightMeasured": { + "$ref": "asyncapi.json#/components/messages/lightMeasured" + } + } + } +} diff --git a/test/ApiCodeGenerator.AsyncApi.Tests/asyncApi/externalRef.yaml b/test/ApiCodeGenerator.AsyncApi.Tests/asyncApi/externalRef.yaml new file mode 100644 index 0000000..2f63c62 --- /dev/null +++ b/test/ApiCodeGenerator.AsyncApi.Tests/asyncApi/externalRef.yaml @@ -0,0 +1,9 @@ +asyncapi: "2.6.0" +info: + title: Document with reference to external schema + version: "1.0" +channels: {} +components: + messages: + lightMeasured: + $ref: asyncapi.yml#/components/messages/lightMeasured diff --git a/test/ApiCodeGenerator.OpenApi.Tests/ContentGeneratorFactoryTests.cs b/test/ApiCodeGenerator.OpenApi.Tests/ContentGeneratorFactoryTests.cs index a2e8dc0..5cf9070 100644 --- a/test/ApiCodeGenerator.OpenApi.Tests/ContentGeneratorFactoryTests.cs +++ b/test/ApiCodeGenerator.OpenApi.Tests/ContentGeneratorFactoryTests.cs @@ -158,7 +158,8 @@ public async Task LoadOpenApiDocument_WithModelPreprocess() public async Task LoadOpenApiDocument_FromYaml() { var context = CreateContext(new()); - var schemaText = await File.ReadAllTextAsync(TestHelpers.GetSwaggerPath("testSchema.yaml")); + context.DocumentPath = TestHelpers.GetSwaggerPath("testSchema.yaml"); + var schemaText = await File.ReadAllTextAsync(context.DocumentPath); context.DocumentReader = new StringReader(schemaText); var gen = (CSharpClientContentGenerator)await CSharpClientContentGenerator.CreateAsync(context); @@ -166,6 +167,23 @@ public async Task LoadOpenApiDocument_FromYaml() var openApiDocument = GetDocument(gen.Generator); Assert.NotNull(openApiDocument); + Assert.NotNull(openApiDocument!.DocumentPath); + } + + [TestCase("externalRef.json")] + public async Task LoadOpenApiDocument_ExternalRef(string documentPath) + { + var context = CreateContext(new()); + context.DocumentPath = TestHelpers.GetSwaggerPath(documentPath); + var documentContent = await File.ReadAllTextAsync(context.DocumentPath); + context.DocumentReader = new StringReader(documentContent); + + var gen = (CSharpClientContentGenerator)await CSharpClientContentGenerator.CreateAsync(context); + + var openApiDocument = GetDocument(gen.Generator); + + Assert.NotNull(openApiDocument); + Assert.NotNull(openApiDocument!.Definitions["test"].AllOf.Single().Reference); } private GeneratorContext CreateContext(JObject settingsJson, Core.ExtensionManager.Extensions? extension = null) diff --git a/test/ApiCodeGenerator.OpenApi.Tests/swagger/externalRef.json b/test/ApiCodeGenerator.OpenApi.Tests/swagger/externalRef.json new file mode 100644 index 0000000..a10c58b --- /dev/null +++ b/test/ApiCodeGenerator.OpenApi.Tests/swagger/externalRef.json @@ -0,0 +1,19 @@ +{ + "openapi": "3.0.0", + "components": { + "schemas": { + "test": { + "allOf": [ + { + "$ref": "testSchema.json#/definitions/testOperResponse" + } + ], + "properties": { + "prop": { + "type": "integer" + } + } + } + } + } +}