diff --git a/README.md b/README.md index 8f5fbc7..70b90bd 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ dotnet add package BinkyLabs.OpenApi.Overlays The following example illustrates how you can load or parse an Overlay document from JSON or YAML. ```csharp -var overlayDocument = await OverlayDocument.LoadFromUrlAsync("https://source/overlay.json"); +var (overlayDocument) = await OverlayDocument.LoadFromUrlAsync("https://source/overlay.json"); ``` ### Applying an Overlay document to an OpenAPI document @@ -29,7 +29,7 @@ var overlayDocument = await OverlayDocument.LoadFromUrlAsync("https://source/ove The following example illustrates how you can apply an Overlay document to an OpenAPI document. ```csharp -var resultOpenApiDocument = await overlayDocument.ApplyToDocumentAsync("https://source/openapi.json"); +var (resultOpenApiDocument) = await overlayDocument.ApplyToDocumentAndLoadAsync("https://source/openapi.json"); ``` ### Applying multiple Overlay documents to an OpenAPI document @@ -39,7 +39,7 @@ The following example illustrates how you can apply multiple Overlay documents t ```csharp var combinedOverlay = overlayDocument1.CombineWith(overlayDocument2); // order matters during the combination, the actions will be appended -var resultOpenApiDocument = await combinedOverlay.ApplyToDocumentAsync("https://source/openapi.json"); +var (resultOpenApiDocument) = await combinedOverlay.ApplyToDocumentAndLoadAsync("https://source/openapi.json"); ``` ### Serializing an Overlay document diff --git a/src/lib/BinkyLabs.OpenApi.Overlays.csproj b/src/lib/BinkyLabs.OpenApi.Overlays.csproj index 74ed82d..993d35c 100644 --- a/src/lib/BinkyLabs.OpenApi.Overlays.csproj +++ b/src/lib/BinkyLabs.OpenApi.Overlays.csproj @@ -36,8 +36,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -46,14 +46,14 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/src/lib/Models/OverlayDocument.cs b/src/lib/Models/OverlayDocument.cs index 9f0bea6..ff1869e 100644 --- a/src/lib/Models/OverlayDocument.cs +++ b/src/lib/Models/OverlayDocument.cs @@ -126,7 +126,7 @@ internal bool ApplyToDocument(JsonNode jsonNode, OverlayDiagnostic overlayDiagno /// Settings to use when reading the document. /// Cancellation token to cancel the operation. /// The OpenAPI document after applying the action. - public async Task ApplyToExtendedDocumentAsync(string? format = default, OverlayReaderSettings? readerSettings = default, CancellationToken cancellationToken = default) + public async Task ApplyToExtendedDocumentAsync(string? format = default, OverlayReaderSettings? readerSettings = default, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(Extends)) { @@ -135,6 +135,24 @@ public async Task ApplyToExtendedDocumentAsync(string? return await ApplyToDocumentAsync(Extends, format, readerSettings, cancellationToken).ConfigureAwait(false); } + /// + /// Applies the action to an OpenAPI document loaded from the extends property. + /// The document is read in the specified format (e.g., JSON or YAML). + /// + /// The format of the document (e.g., JSON or YAML). + /// Settings to use when reading the document. + /// Cancellation token to cancel the operation. + /// The OpenAPI document after applying the action. + public async Task ApplyToExtendedDocumentAndLoadAsync(string? format = default, OverlayReaderSettings? readerSettings = default, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(Extends)) + { + throw new InvalidOperationException("The 'extends' property must be set to apply the overlay to an extended document."); + } + var jsonResult = await ApplyToExtendedDocumentAsync(format, readerSettings, cancellationToken).ConfigureAwait(false); + return LoadDocument(jsonResult, new Uri(Extends), format ?? string.Empty, readerSettings); + } + /// /// Applies the action to an OpenAPI document loaded from a specified path or URI. /// The document is read in the specified format (e.g., JSON or YAML). @@ -144,7 +162,7 @@ public async Task ApplyToExtendedDocumentAsync(string? /// Settings to use when reading the document. /// Cancellation token to cancel the operation. /// The OpenAPI document after applying the action. - public async Task ApplyToDocumentAsync(string documentPathOrUri, string? format = default, OverlayReaderSettings? readerSettings = default, CancellationToken cancellationToken = default) + public async Task ApplyToDocumentAsync(string documentPathOrUri, string? format = default, OverlayReaderSettings? readerSettings = default, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(documentPathOrUri); readerSettings ??= new OverlayReaderSettings(); @@ -164,6 +182,23 @@ public async Task ApplyToDocumentAsync(string document using var fileStream = new FileStream(documentPathOrUri, FileMode.Open, FileAccess.Read); await fileStream.CopyToAsync(input, cancellationToken).ConfigureAwait(false); } + var result = await ApplyToDocumentStreamAsync(input, format, readerSettings, cancellationToken).ConfigureAwait(false); + await input.DisposeAsync().ConfigureAwait(false); + return result; + } + + /// + /// Applies the action to an OpenAPI document loaded from a specified path or URI. + /// The document is read in the specified format (e.g., JSON or YAML). + /// + /// Path or URI to the OpenAPI document. + /// The format of the document (e.g., JSON or YAML). + /// Settings to use when reading the document. + /// Cancellation token to cancel the operation. + /// The OpenAPI document after applying the action. + public async Task ApplyToDocumentAndLoadAsync(string documentPathOrUri, string? format = default, OverlayReaderSettings? readerSettings = default, CancellationToken cancellationToken = default) + { + var jsonResult = await ApplyToDocumentAsync(documentPathOrUri, format, readerSettings, cancellationToken).ConfigureAwait(false); // Convert file paths to absolute paths before creating URI to handle relative paths correctly Uri uri; @@ -178,9 +213,8 @@ public async Task ApplyToDocumentAsync(string document var absolutePath = Path.GetFullPath(documentPathOrUri); uri = new Uri(absolutePath, UriKind.Absolute); } - var result = await ApplyToDocumentStreamAsync(input, uri, format, readerSettings, cancellationToken).ConfigureAwait(false); - await input.DisposeAsync().ConfigureAwait(false); - return result; + + return LoadDocument(jsonResult, uri, format ?? string.Empty, readerSettings); } /// @@ -188,12 +222,11 @@ public async Task ApplyToDocumentAsync(string document /// The document is read in the specified format (e.g., JSON or YAML). /// /// A stream containing the OpenAPI document. - /// The URI location of the document, used for to load external references. /// The format of the document (e.g., JSON or YAML). /// Settings to use when reading the document. /// Cancellation token to cancel the operation. /// The OpenAPI document after applying the action. - public async Task ApplyToDocumentStreamAsync(Stream input, Uri location, string? format = default, OverlayReaderSettings? readerSettings = default, CancellationToken cancellationToken = default) + public async Task ApplyToDocumentStreamAsync(Stream input, string? format = default, OverlayReaderSettings? readerSettings = default, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(input); readerSettings ??= new OverlayReaderSettings(); @@ -213,21 +246,48 @@ public async Task ApplyToDocumentStreamAsync(Stream in throw new InvalidOperationException("Failed to parse the OpenAPI document."); var overlayDiagnostic = new OverlayDiagnostic(); var result = ApplyToDocument(jsonNode, overlayDiagnostic); - var openAPIJsonReader = new OpenApiJsonReader(); - var (openAPIDocument, openApiDiagnostic) = openAPIJsonReader.Read(jsonNode, location, readerSettings.OpenApiSettings); - if (openApiDiagnostic is not null) + return new OverlayApplicationResultOfJsonNode { - openApiDiagnostic.Format = format; - } - return new OverlayApplicationResult - { - Document = openAPIDocument, + Document = jsonNode, Diagnostic = overlayDiagnostic, - OpenApiDiagnostic = openApiDiagnostic, IsSuccessful = result, + OpenApiDiagnostic = new OpenApiDiagnostic() + { + Format = format + } }; } /// + /// Applies the action to an OpenAPI document loaded from a specified path or URI. + /// The document is read in the specified format (e.g., JSON or YAML). + /// + /// A stream containing the OpenAPI document. + /// The URI location of the document, used for to load external references. + /// The format of the document (e.g., JSON or YAML). + /// Settings to use when reading the document. + /// Cancellation token to cancel the operation. + /// The OpenAPI document after applying the action. + public async Task ApplyToDocumentStreamAndLoadAsync(Stream input, Uri location, string? format = default, OverlayReaderSettings? readerSettings = default, CancellationToken cancellationToken = default) + { + var jsonResult = await ApplyToDocumentStreamAsync(input, format, readerSettings, cancellationToken).ConfigureAwait(false); + return LoadDocument(jsonResult, location, format ?? string.Empty, readerSettings); + } + internal static OverlayApplicationResultOfOpenApiDocument LoadDocument(OverlayApplicationResultOfJsonNode jsonResult, Uri location, string format, OverlayReaderSettings? readerSettings) + { + readerSettings ??= new OverlayReaderSettings(); + var openAPIJsonReader = new OpenApiJsonReader(); + if (jsonResult.Document is null) + { + return OverlayApplicationResultOfOpenApiDocument.FromJsonResultWithFailedLoad(jsonResult); + } + var (openAPIDocument, openApiDiagnostic) = openAPIJsonReader.Read(jsonResult.Document, location, readerSettings.OpenApiSettings); + if (openApiDiagnostic is not null && !string.IsNullOrEmpty(format)) + { + openApiDiagnostic.Format = format; + } + return OverlayApplicationResultOfOpenApiDocument.FromJsonResult(jsonResult, openAPIDocument, openApiDiagnostic); + } + /// /// Combines this overlay document with another overlay document. /// The returned document will be a new document, and its metadata (info, etc.) will be the one from the other document. /// The actions from both documents will be merged. The current document actions will be first, and the ones from the other document will be next. diff --git a/src/lib/OverlayApplicationResult.cs b/src/lib/OverlayApplicationResult.cs index 979885d..f33e4bf 100644 --- a/src/lib/OverlayApplicationResult.cs +++ b/src/lib/OverlayApplicationResult.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Nodes; + using BinkyLabs.OpenApi.Overlays.Reader; using Microsoft.OpenApi; @@ -8,12 +10,50 @@ namespace BinkyLabs.OpenApi.Overlays; /// /// Result of applying overlays to an OpenAPI document /// -public class OverlayApplicationResult +public class OverlayApplicationResultOfOpenApiDocument : OverlayApplicationResult +{ + internal static OverlayApplicationResultOfOpenApiDocument FromJsonResultWithFailedLoad(OverlayApplicationResultOfJsonNode jsonResult) + { + ArgumentNullException.ThrowIfNull(jsonResult); + return new OverlayApplicationResultOfOpenApiDocument + { + Document = null, + Diagnostic = jsonResult.Diagnostic, + // maintains source format information + OpenApiDiagnostic = jsonResult.OpenApiDiagnostic, + IsSuccessful = false, + }; + } + internal static OverlayApplicationResultOfOpenApiDocument FromJsonResult(OverlayApplicationResultOfJsonNode jsonResult, OpenApiDocument? document, OpenApiDiagnostic? openApiDiagnostic) + { + ArgumentNullException.ThrowIfNull(jsonResult); + return new OverlayApplicationResultOfOpenApiDocument + { + Document = document, + Diagnostic = jsonResult.Diagnostic, + // maintains source format information + OpenApiDiagnostic = openApiDiagnostic ?? jsonResult.OpenApiDiagnostic, + IsSuccessful = jsonResult.IsSuccessful, + }; + } +} + +/// +/// Result of applying overlays to an OpenAPI document +/// +public class OverlayApplicationResultOfJsonNode : OverlayApplicationResult +{ +} + +/// +/// Result of applying overlays to an OpenAPI document +/// +public class OverlayApplicationResult { /// /// The resulting OpenAPI document after applying overlays, or null if application failed /// - public OpenApiDocument? Document { get; init; } + public T? Document { get; init; } /// /// Diagnostics from applying the overlays /// @@ -35,7 +75,7 @@ public class OverlayApplicationResult /// Diagnostics from applying the overlays /// Diagnostics from reading the updated OpenAPI document /// Indicates whether the overlay application was successful - public void Deconstruct(out OpenApiDocument? document, out OverlayDiagnostic diagnostic, out OpenApiDiagnostic? openApiDiagnostic, out bool isSuccessful) + public void Deconstruct(out T? document, out OverlayDiagnostic diagnostic, out OpenApiDiagnostic? openApiDiagnostic, out bool isSuccessful) { document = Document; diagnostic = Diagnostic; @@ -48,7 +88,7 @@ public void Deconstruct(out OpenApiDocument? document, out OverlayDiagnostic dia /// The resulting OpenAPI document after applying overlays, or null if application failed /// Diagnostics from applying the overlays /// Diagnostics from reading the updated OpenAPI document - public void Deconstruct(out OpenApiDocument? document, out OverlayDiagnostic diagnostic, out OpenApiDiagnostic? openApiDiagnostic) + public void Deconstruct(out T? document, out OverlayDiagnostic diagnostic, out OpenApiDiagnostic? openApiDiagnostic) { Deconstruct(out document, out diagnostic, out openApiDiagnostic, out _); } @@ -57,7 +97,7 @@ public void Deconstruct(out OpenApiDocument? document, out OverlayDiagnostic dia /// /// The resulting OpenAPI document after applying overlays, or null if application failed /// Diagnostics from applying the overlays - public void Deconstruct(out OpenApiDocument? document, out OverlayDiagnostic diagnostic) + public void Deconstruct(out T? document, out OverlayDiagnostic diagnostic) { Deconstruct(out document, out diagnostic, out _); } @@ -65,7 +105,7 @@ public void Deconstruct(out OpenApiDocument? document, out OverlayDiagnostic dia /// Deconstructs the OverlayApplicationResult into its components /// /// The resulting OpenAPI document after applying overlays, or null if application failed - public void Deconstruct(out OpenApiDocument? document) + public void Deconstruct(out T? document) { Deconstruct(out document, out _); } diff --git a/src/lib/PublicAPI.Unshipped.txt b/src/lib/PublicAPI.Unshipped.txt index 07925a9..e2d2d2e 100644 --- a/src/lib/PublicAPI.Unshipped.txt +++ b/src/lib/PublicAPI.Unshipped.txt @@ -28,27 +28,34 @@ BinkyLabs.OpenApi.Overlays.OverlayAction.Target.get -> string? BinkyLabs.OpenApi.Overlays.OverlayAction.Target.set -> void BinkyLabs.OpenApi.Overlays.OverlayAction.Update.get -> System.Text.Json.Nodes.JsonNode? BinkyLabs.OpenApi.Overlays.OverlayAction.Update.set -> void -BinkyLabs.OpenApi.Overlays.OverlayApplicationResult -BinkyLabs.OpenApi.Overlays.OverlayApplicationResult.Deconstruct(out Microsoft.OpenApi.OpenApiDocument? document) -> void -BinkyLabs.OpenApi.Overlays.OverlayApplicationResult.Deconstruct(out Microsoft.OpenApi.OpenApiDocument? document, out BinkyLabs.OpenApi.Overlays.Reader.OverlayDiagnostic! diagnostic) -> void -BinkyLabs.OpenApi.Overlays.OverlayApplicationResult.Deconstruct(out Microsoft.OpenApi.OpenApiDocument? document, out BinkyLabs.OpenApi.Overlays.Reader.OverlayDiagnostic! diagnostic, out Microsoft.OpenApi.Reader.OpenApiDiagnostic? openApiDiagnostic) -> void -BinkyLabs.OpenApi.Overlays.OverlayApplicationResult.Deconstruct(out Microsoft.OpenApi.OpenApiDocument? document, out BinkyLabs.OpenApi.Overlays.Reader.OverlayDiagnostic! diagnostic, out Microsoft.OpenApi.Reader.OpenApiDiagnostic? openApiDiagnostic, out bool isSuccessful) -> void -BinkyLabs.OpenApi.Overlays.OverlayApplicationResult.OverlayApplicationResult() -> void -BinkyLabs.OpenApi.Overlays.OverlayApplicationResult.Diagnostic.get -> BinkyLabs.OpenApi.Overlays.Reader.OverlayDiagnostic! -BinkyLabs.OpenApi.Overlays.OverlayApplicationResult.Diagnostic.init -> void -BinkyLabs.OpenApi.Overlays.OverlayApplicationResult.Document.get -> Microsoft.OpenApi.OpenApiDocument? -BinkyLabs.OpenApi.Overlays.OverlayApplicationResult.Document.init -> void -BinkyLabs.OpenApi.Overlays.OverlayApplicationResult.IsSuccessful.get -> bool -BinkyLabs.OpenApi.Overlays.OverlayApplicationResult.IsSuccessful.init -> void -BinkyLabs.OpenApi.Overlays.OverlayApplicationResult.OpenApiDiagnostic.get -> Microsoft.OpenApi.Reader.OpenApiDiagnostic? -BinkyLabs.OpenApi.Overlays.OverlayApplicationResult.OpenApiDiagnostic.init -> void +BinkyLabs.OpenApi.Overlays.OverlayApplicationResult +BinkyLabs.OpenApi.Overlays.OverlayApplicationResult.Deconstruct(out T? document) -> void +BinkyLabs.OpenApi.Overlays.OverlayApplicationResult.Deconstruct(out T? document, out BinkyLabs.OpenApi.Overlays.Reader.OverlayDiagnostic! diagnostic) -> void +BinkyLabs.OpenApi.Overlays.OverlayApplicationResult.Deconstruct(out T? document, out BinkyLabs.OpenApi.Overlays.Reader.OverlayDiagnostic! diagnostic, out Microsoft.OpenApi.Reader.OpenApiDiagnostic? openApiDiagnostic) -> void +BinkyLabs.OpenApi.Overlays.OverlayApplicationResult.Deconstruct(out T? document, out BinkyLabs.OpenApi.Overlays.Reader.OverlayDiagnostic! diagnostic, out Microsoft.OpenApi.Reader.OpenApiDiagnostic? openApiDiagnostic, out bool isSuccessful) -> void +BinkyLabs.OpenApi.Overlays.OverlayApplicationResult.Diagnostic.get -> BinkyLabs.OpenApi.Overlays.Reader.OverlayDiagnostic! +BinkyLabs.OpenApi.Overlays.OverlayApplicationResult.Diagnostic.init -> void +BinkyLabs.OpenApi.Overlays.OverlayApplicationResult.Document.get -> T? +BinkyLabs.OpenApi.Overlays.OverlayApplicationResult.Document.init -> void +BinkyLabs.OpenApi.Overlays.OverlayApplicationResult.IsSuccessful.get -> bool +BinkyLabs.OpenApi.Overlays.OverlayApplicationResult.IsSuccessful.init -> void +BinkyLabs.OpenApi.Overlays.OverlayApplicationResult.OpenApiDiagnostic.get -> Microsoft.OpenApi.Reader.OpenApiDiagnostic? +BinkyLabs.OpenApi.Overlays.OverlayApplicationResult.OpenApiDiagnostic.init -> void +BinkyLabs.OpenApi.Overlays.OverlayApplicationResult.OverlayApplicationResult() -> void +BinkyLabs.OpenApi.Overlays.OverlayApplicationResultOfJsonNode +BinkyLabs.OpenApi.Overlays.OverlayApplicationResultOfJsonNode.OverlayApplicationResultOfJsonNode() -> void +BinkyLabs.OpenApi.Overlays.OverlayApplicationResultOfOpenApiDocument +BinkyLabs.OpenApi.Overlays.OverlayApplicationResultOfOpenApiDocument.OverlayApplicationResultOfOpenApiDocument() -> void BinkyLabs.OpenApi.Overlays.OverlayConstants BinkyLabs.OpenApi.Overlays.OverlayDocument BinkyLabs.OpenApi.Overlays.OverlayDocument.Actions.get -> System.Collections.Generic.IList? BinkyLabs.OpenApi.Overlays.OverlayDocument.Actions.set -> void -BinkyLabs.OpenApi.Overlays.OverlayDocument.ApplyToDocumentAsync(string! documentPathOrUri, string? format = null, BinkyLabs.OpenApi.Overlays.OverlayReaderSettings? readerSettings = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -BinkyLabs.OpenApi.Overlays.OverlayDocument.ApplyToDocumentStreamAsync(System.IO.Stream! input, System.Uri! location, string? format = null, BinkyLabs.OpenApi.Overlays.OverlayReaderSettings? readerSettings = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -BinkyLabs.OpenApi.Overlays.OverlayDocument.ApplyToExtendedDocumentAsync(string? format = null, BinkyLabs.OpenApi.Overlays.OverlayReaderSettings? readerSettings = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +BinkyLabs.OpenApi.Overlays.OverlayDocument.ApplyToDocumentAndLoadAsync(string! documentPathOrUri, string? format = null, BinkyLabs.OpenApi.Overlays.OverlayReaderSettings? readerSettings = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +BinkyLabs.OpenApi.Overlays.OverlayDocument.ApplyToDocumentAsync(string! documentPathOrUri, string? format = null, BinkyLabs.OpenApi.Overlays.OverlayReaderSettings? readerSettings = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +BinkyLabs.OpenApi.Overlays.OverlayDocument.ApplyToDocumentStreamAndLoadAsync(System.IO.Stream! input, System.Uri! location, string? format = null, BinkyLabs.OpenApi.Overlays.OverlayReaderSettings? readerSettings = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +BinkyLabs.OpenApi.Overlays.OverlayDocument.ApplyToDocumentStreamAsync(System.IO.Stream! input, string? format = null, BinkyLabs.OpenApi.Overlays.OverlayReaderSettings? readerSettings = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +BinkyLabs.OpenApi.Overlays.OverlayDocument.ApplyToExtendedDocumentAndLoadAsync(string? format = null, BinkyLabs.OpenApi.Overlays.OverlayReaderSettings? readerSettings = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +BinkyLabs.OpenApi.Overlays.OverlayDocument.ApplyToExtendedDocumentAsync(string? format = null, BinkyLabs.OpenApi.Overlays.OverlayReaderSettings? readerSettings = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! BinkyLabs.OpenApi.Overlays.OverlayDocument.CombineWith(params BinkyLabs.OpenApi.Overlays.OverlayDocument![]! others) -> BinkyLabs.OpenApi.Overlays.OverlayDocument! BinkyLabs.OpenApi.Overlays.OverlayDocument.Extends.get -> string? BinkyLabs.OpenApi.Overlays.OverlayDocument.Extends.set -> void @@ -128,6 +135,7 @@ BinkyLabs.OpenApi.Overlays.Reader.ParsingContext.PushLoop(string! loopId, string BinkyLabs.OpenApi.Overlays.Reader.ParsingContext.SetTempStorage(string! key, object? value, object? scope = null) -> void BinkyLabs.OpenApi.Overlays.Reader.ParsingContext.StartObject(string! objectName) -> void BinkyLabs.OpenApi.Overlays.ReadResult +BinkyLabs.OpenApi.Overlays.ReadResult.Deconstruct(out BinkyLabs.OpenApi.Overlays.OverlayDocument? document) -> void BinkyLabs.OpenApi.Overlays.ReadResult.Deconstruct(out BinkyLabs.OpenApi.Overlays.OverlayDocument? document, out BinkyLabs.OpenApi.Overlays.Reader.OverlayDiagnostic? diagnostic) -> void BinkyLabs.OpenApi.Overlays.ReadResult.Diagnostic.get -> BinkyLabs.OpenApi.Overlays.Reader.OverlayDiagnostic? BinkyLabs.OpenApi.Overlays.ReadResult.Diagnostic.set -> void diff --git a/src/lib/Reader/ReadResult.cs b/src/lib/Reader/ReadResult.cs index 2323519..21759fa 100644 --- a/src/lib/Reader/ReadResult.cs +++ b/src/lib/Reader/ReadResult.cs @@ -27,4 +27,12 @@ public void Deconstruct(out OverlayDocument? document, out OverlayDiagnostic? di document = Document; diagnostic = Diagnostic; } + /// + /// Deconstructs the result for easier assignment on the client application. + /// + /// The parsed overlay document. + public void Deconstruct(out OverlayDocument? document) + { + Deconstruct(out document, out _); + } } \ No newline at end of file diff --git a/src/tool/OverlayCliApp.cs b/src/tool/OverlayCliApp.cs index 46b3151..376619c 100644 --- a/src/tool/OverlayCliApp.cs +++ b/src/tool/OverlayCliApp.cs @@ -3,12 +3,17 @@ using System.CommandLine; using System.IO; using System.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using BinkyLabs.OpenApi.Overlays.Reader; using Microsoft.OpenApi; +using Microsoft.OpenApi.Reader; +using Microsoft.OpenApi.YamlReader; + +using SharpYaml.Serialization; namespace BinkyLabs.OpenApi.Overlays.Cli; @@ -17,28 +22,45 @@ internal static class OverlayCliApp public static async Task RunAsync(string[] args, CancellationToken cancellationToken = default) { var rootCommand = new RootCommand("BinkyLabs OpenAPI Overlays CLI - Apply overlays to OpenAPI documents"); - var applyCommand = CreateApplyCommand(); + var applyCommand = CreateApplyCommand("apply", "Apply one or more overlays to an OpenAPI document", ApplyOverlaysAsync); rootCommand.Add(applyCommand); + var applyAndNormalizeCommand = CreateApplyCommand("apply-and-normalize", "Apply one or more overlays to an OpenAPI document, and normalize the output with OpenAPI.net", ApplyOverlaysAndNormalizeAsync); + rootCommand.Add(applyAndNormalizeCommand); return await rootCommand.Parse(args).InvokeAsync(cancellationToken: cancellationToken); } - private static Command CreateApplyCommand() + private static Argument CreateInputArgument() { - var applyCommand = new Command("apply", "Apply one or more overlays to an OpenAPI document"); - - var inputArgument = new Argument("input") - { - Description = "Path to the input OpenAPI document" - }; + var inputArgument = new Argument("input") { Description = "Path to the input OpenAPI document" }; + return inputArgument; + } + private static Option CreateOverlayOption() + { var overlayOption = new Option("--overlay") { Description = "Path to overlay file(s). Can be specified multiple times." }; overlayOption.Aliases.Add("-o"); overlayOption.Arity = ArgumentArity.OneOrMore; overlayOption.Required = true; + return overlayOption; + } + private static Option CreateOutputOption() + { var outputOption = new Option("--output") { Description = "Path for the output file" }; outputOption.Aliases.Add("-out"); outputOption.Required = true; + return outputOption; + } + + private static Command CreateApplyCommand(string name, string description, Func applyAsync) + { + var applyCommand = new Command(name, description); + + var inputArgument = CreateInputArgument(); + + var overlayOption = CreateOverlayOption(); + + var outputOption = CreateOutputOption(); applyCommand.Add(inputArgument); applyCommand.Add(overlayOption); @@ -62,17 +84,18 @@ private static Command CreateApplyCommand() return 1; } - await HandleApplyCommandAsync(input, overlays ?? [], output, cancellationToken); + await HandleCommandAsync(input, overlays ?? [], output, applyAsync, cancellationToken); return 0; }); return applyCommand; } - private static async Task HandleApplyCommandAsync( + private static async Task HandleCommandAsync( string input, string[] overlays, string output, + Func applyAsync, CancellationToken cancellationToken) { try @@ -108,7 +131,7 @@ private static async Task HandleApplyCommandAsync( cancellationToken.ThrowIfCancellationRequested(); - await ApplyOverlaysAsync(input, overlays, output, cancellationToken); + await applyAsync(input, overlays, output, cancellationToken).ConfigureAwait(false); Console.WriteLine("Overlays applied successfully!"); } @@ -124,63 +147,92 @@ private static async Task HandleApplyCommandAsync( } } - private static async Task ApplyOverlaysAsync( - string inputPath, + private static async Task<(OverlayDocument CombinedOverlay, List AllDiagnostics)> LoadAndCombineOverlaysAsync( string[] overlayPaths, - string outputPath, - CancellationToken cancellationToken) + CancellationToken cancellationToken = default) { - try + Console.WriteLine("Processing input document..."); + + var allDiagnostics = new List(); + var overlayDocuments = new List(); + + foreach (var overlayPath in overlayPaths) { - Console.WriteLine("Processing input document..."); + Console.WriteLine($"Loading overlay: {Path.GetFileName(overlayPath)}..."); + cancellationToken.ThrowIfCancellationRequested(); - var allDiagnostics = new List(); - var overlayDocuments = new List(); + using var overlayStream = new FileStream(overlayPath, FileMode.Open, FileAccess.Read); - foreach (var overlayPath in overlayPaths) + var (overlayDocument, overlayDiagnostic) = await OverlayDocument.LoadFromStreamAsync(overlayStream, cancellationToken: cancellationToken); + + if (overlayDocument == null) { - Console.WriteLine($"Loading overlay: {Path.GetFileName(overlayPath)}..."); - cancellationToken.ThrowIfCancellationRequested(); + throw new InvalidOperationException($"Failed to load overlay: {overlayPath}. Errors: {string.Join(", ", overlayDiagnostic?.Errors.Select(e => e.Message) ?? Array.Empty())}"); + } - using var overlayStream = new FileStream(overlayPath, FileMode.Open, FileAccess.Read); + overlayDocuments.Add(overlayDocument); - var (overlayDocument, overlayDiagnostic) = await OverlayDocument.LoadFromStreamAsync(overlayStream, cancellationToken: cancellationToken); + if (overlayDiagnostic != null) + { + allDiagnostics.Add(overlayDiagnostic); + } + } - if (overlayDocument == null) - { - throw new InvalidOperationException($"Failed to load overlay: {overlayPath}. Errors: {string.Join(", ", overlayDiagnostic?.Errors.Select(e => e.Message) ?? Array.Empty())}"); - } + var combinedOverlay = overlayDocuments.Count switch + { + 0 => throw new InvalidOperationException("No overlays to apply."), + 1 => overlayDocuments[0], + _ => overlayDocuments[0].CombineWith([.. overlayDocuments[1..]]), + }; - overlayDocuments.Add(overlayDocument); + return (combinedOverlay, allDiagnostics); + } - if (overlayDiagnostic != null) - { - allDiagnostics.Add(overlayDiagnostic); - } + private static void DisplayWarnings(List allDiagnostics) + { + var allWarnings = allDiagnostics.SelectMany(static d => d.Warnings).ToArray(); + if (allWarnings.Length > 0) + { + Console.WriteLine($"Warnings during processing:"); + foreach (var warning in allWarnings) + { + Console.WriteLine($" - {warning.Message}"); } + } + } - var combinedOverlay = overlayDocuments.Count switch - { - 0 => throw new InvalidOperationException("No overlays to apply."), - 1 => overlayDocuments[0], - _ => overlayDocuments[0].CombineWith([.. overlayDocuments[1..]]), - }; + private static void CheckDiagnosticsAndThrowIfErrors( + OpenApiDiagnostic? openApiDocumentDiagnostic, + OverlayDiagnostic applyOverlayDiagnostic) + { + if (openApiDocumentDiagnostic is { Errors.Count: > 0 }) + { + var errorMessages = string.Join(", ", openApiDocumentDiagnostic.Errors.Select(static e => e.Message)); + throw new InvalidOperationException($"Failed to apply overlays. Errors: {errorMessages}"); + } + else if (applyOverlayDiagnostic is { Errors.Count: > 0 }) + { + var errorMessages = string.Join(", ", applyOverlayDiagnostic.Errors.Select(static e => e.Message)); + throw new InvalidOperationException($"Failed to apply overlays. Errors: {errorMessages}"); + } + } - var (openApiDocument, applyOverlayDiagnostic, openApiDocumentDiagnostic, _) = await combinedOverlay.ApplyToDocumentAsync(inputPath, cancellationToken: cancellationToken); + private static async Task ApplyOverlaysAndNormalizeAsync( + string inputPath, + string[] overlayPaths, + string outputPath, + CancellationToken cancellationToken = default) + { + try + { + var (combinedOverlay, allDiagnostics) = await LoadAndCombineOverlaysAsync(overlayPaths, cancellationToken); + + var (openApiDocument, applyOverlayDiagnostic, openApiDocumentDiagnostic, _) = await combinedOverlay.ApplyToDocumentAndLoadAsync(inputPath, cancellationToken: cancellationToken); allDiagnostics.Add(applyOverlayDiagnostic); if (openApiDocument is null) { - if (openApiDocumentDiagnostic is { Errors.Count: > 0 }) - { - var errorMessages = string.Join(", ", openApiDocumentDiagnostic.Errors.Select(static e => e.Message)); - throw new InvalidOperationException($"Failed to apply overlays. Errors: {errorMessages}"); - } - else if (applyOverlayDiagnostic is { Errors.Count: > 0 }) - { - var errorMessages = string.Join(", ", applyOverlayDiagnostic.Errors.Select(static e => e.Message)); - throw new InvalidOperationException($"Failed to apply overlays. Errors: {errorMessages}"); - } + CheckDiagnosticsAndThrowIfErrors(openApiDocumentDiagnostic, applyOverlayDiagnostic); throw new InvalidOperationException("OpenApiDocument is null after applying overlays."); } @@ -190,15 +242,55 @@ private static async Task ApplyOverlaysAsync( await openApiDocument.SerializeAsync(outputStream, openApiDocumentDiagnostic?.SpecificationVersion ?? OpenApiSpecVersion.OpenApi3_1, openApiDocumentDiagnostic?.Format ?? OpenApiConstants.Json, cancellationToken); - var allWarnings = allDiagnostics.SelectMany(static d => d.Warnings).ToArray(); - if (allWarnings.Length > 0) + DisplayWarnings(allDiagnostics); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + throw new InvalidOperationException($"Failed to apply overlays: {ex.Message}", ex); + } + } + + private static async Task ApplyOverlaysAsync( + string inputPath, + string[] overlayPaths, + string outputPath, + CancellationToken cancellationToken = default) + { + try + { + var (combinedOverlay, allDiagnostics) = await LoadAndCombineOverlaysAsync(overlayPaths, cancellationToken); + + var (jsonNode, applyOverlayDiagnostic, openApiDocumentDiagnostic, _) = await combinedOverlay.ApplyToDocumentAsync(inputPath, cancellationToken: cancellationToken); + allDiagnostics.Add(applyOverlayDiagnostic); + + if (jsonNode is null) { - Console.WriteLine($"Warnings during processing:"); - foreach (var warning in allWarnings) - { - Console.WriteLine($" - {warning.Message}"); - } + CheckDiagnosticsAndThrowIfErrors(openApiDocumentDiagnostic, applyOverlayDiagnostic); + throw new InvalidOperationException("JsonNode is null after applying overlays."); + } + + Console.WriteLine("Writing output document..."); + + using var outputStream = new FileStream(outputPath, FileMode.Create, FileAccess.Write); + + switch (openApiDocumentDiagnostic?.Format) + { + case "yml": + case "yaml": + var yamlStream = new YamlStream(new YamlDocument(jsonNode.ToYamlNode())); + var writer = new StreamWriter(outputStream); + yamlStream.Save(writer); + break; + case "json": + await JsonSerializer.SerializeAsync(outputStream, jsonNode, cancellationToken: cancellationToken).ConfigureAwait(false); + break; + default: + throw new NotImplementedException($"'{openApiDocumentDiagnostic?.Format}' output format is not yet implemented."); } + + await outputStream.FlushAsync(cancellationToken).ConfigureAwait(false); + + DisplayWarnings(allDiagnostics); } catch (Exception ex) when (ex is not OperationCanceledException) { diff --git a/tests/lib/Serialization/OverlayDocumentTests.cs b/tests/lib/Serialization/OverlayDocumentTests.cs index e7b7f3b..ddc64c7 100644 --- a/tests/lib/Serialization/OverlayDocumentTests.cs +++ b/tests/lib/Serialization/OverlayDocumentTests.cs @@ -381,7 +381,7 @@ public async Task ShouldApplyTheOverlayToAnOpenApiDocumentFromYaml() }; var tempUri = new Uri("http://example.com/overlay.yaml"); - var (document, overlayDiagnostic, openApiDiagnostic, result) = await overlayDocument.ApplyToDocumentStreamAsync(documentStream, tempUri); + var (document, overlayDiagnostic, openApiDiagnostic, result) = await overlayDocument.ApplyToDocumentStreamAndLoadAsync(documentStream, tempUri); Assert.True(result, "Overlay application should succeed."); Assert.NotNull(document); Assert.NotNull(overlayDiagnostic); @@ -451,7 +451,7 @@ public async Task ShouldApplyTheOverlayToAnOpenApiDocumentFromJson() }; var tempUri = new Uri("http://example.com/overlay.yaml"); - var (document, overlayDiagnostic, openApiDiagnostic, result) = await overlayDocument.ApplyToDocumentStreamAsync(documentStream, tempUri); + var (document, overlayDiagnostic, openApiDiagnostic, result) = await overlayDocument.ApplyToDocumentStreamAndLoadAsync(documentStream, tempUri); Assert.True(result, "Overlay application should succeed."); Assert.NotNull(document); Assert.NotNull(overlayDiagnostic); @@ -808,7 +808,7 @@ public async Task ApplyToDocumentAsync_WithRelativePath_ShouldSucceed() await File.WriteAllTextAsync(_tempFilePath, openApiDocument); // Act - var (document, overlayDiagnostic, openApiDiagnostic, result) = await overlayDocument.ApplyToDocumentAsync(_tempFilePath); + var (document, overlayDiagnostic, openApiDiagnostic, result) = await overlayDocument.ApplyToDocumentAndLoadAsync(_tempFilePath); // Assert Assert.True(result, "Overlay application should succeed."); @@ -876,7 +876,7 @@ public async Task ApplyToDocumentAsync_ContinuesToTheNextActionWhenOneFails() }; // Act - var (document, overlayDiagnostic, openApiDiagnostic, result) = await overlayDocument.ApplyToDocumentAsync(_tempFilePath); + var (document, overlayDiagnostic, openApiDiagnostic, result) = await overlayDocument.ApplyToDocumentAndLoadAsync(_tempFilePath); // Assert Assert.False(result, "Overlay application should fail."); Assert.NotNull(document);