From 13ca4b0b7ef70e0c1d9283e5430d92b3d003fd15 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 3 Nov 2025 09:27:16 -0500 Subject: [PATCH 01/26] feat: adds 1.1 version Signed-off-by: Vincent Biret --- src/lib/Models/OverlayVersion.cs | 5 +++++ src/lib/PublicAPI.Unshipped.txt | 1 + 2 files changed, 6 insertions(+) diff --git a/src/lib/Models/OverlayVersion.cs b/src/lib/Models/OverlayVersion.cs index be82b58..b3bc6fd 100644 --- a/src/lib/Models/OverlayVersion.cs +++ b/src/lib/Models/OverlayVersion.cs @@ -11,4 +11,9 @@ public enum OverlaySpecVersion /// See: https://spec.openapis.org/overlay/v1.0.0.html /// Overlay1_0 = 1, + /// + /// The OpenAPI Overlay version 1.1.0 + /// See: https://spec.openapis.org/overlay/v1.1.0.html + /// + Overlay1_1 = 2 } \ No newline at end of file diff --git a/src/lib/PublicAPI.Unshipped.txt b/src/lib/PublicAPI.Unshipped.txt index e2d2d2e..1f4515a 100644 --- a/src/lib/PublicAPI.Unshipped.txt +++ b/src/lib/PublicAPI.Unshipped.txt @@ -104,6 +104,7 @@ BinkyLabs.OpenApi.Overlays.OverlayReaderSettings.Readers.init -> void BinkyLabs.OpenApi.Overlays.OverlayReaderSettings.TryAddReader(string! format, BinkyLabs.OpenApi.Overlays.IOverlayReader! reader) -> bool BinkyLabs.OpenApi.Overlays.OverlaySpecVersion BinkyLabs.OpenApi.Overlays.OverlaySpecVersion.Overlay1_0 = 1 -> BinkyLabs.OpenApi.Overlays.OverlaySpecVersion +BinkyLabs.OpenApi.Overlays.OverlaySpecVersion.Overlay1_1 = 2 -> BinkyLabs.OpenApi.Overlays.OverlaySpecVersion BinkyLabs.OpenApi.Overlays.OverlayYamlReader BinkyLabs.OpenApi.Overlays.OverlayYamlReader.GetJsonNodeFromStreamAsync(System.IO.Stream! input, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! BinkyLabs.OpenApi.Overlays.OverlayYamlReader.OverlayYamlReader() -> void From cbb3eb733604c1f1acbe294c95d7e70898671098 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 3 Nov 2025 09:28:37 -0500 Subject: [PATCH 02/26] feat: removes experimental tag for copy field Signed-off-by: Vincent Biret --- src/lib/Models/OverlayAction.cs | 8 -------- src/lib/Reader/V1/OverlayActionDeserializer.cs | 2 -- tests/lib/Serialization/OverlayActionTests.cs | 2 -- 3 files changed, 12 deletions(-) diff --git a/src/lib/Models/OverlayAction.cs b/src/lib/Models/OverlayAction.cs index 869fecc..f456fac 100644 --- a/src/lib/Models/OverlayAction.cs +++ b/src/lib/Models/OverlayAction.cs @@ -40,10 +40,7 @@ public class OverlayAction : IOverlaySerializable, IOverlayExtensible /// /// A string value that indicates that the target object or array MUST be copied to the location indicated by this string, which MUST be a JSON Pointer. /// This field is mutually exclusive with the and fields. - /// This field is experimental and not part of the OpenAPI Overlay specification v1.0.0. - /// This field is an implementation of the copy proposal. /// - [Experimental("BOO001", UrlFormat = "https://github.com/OAI/Overlay-Specification/pull/150")] public string? Copy { get; set; } /// @@ -64,19 +61,15 @@ public void SerializeAsV1(IOpenApiWriter writer) { writer.WriteOptionalObject("update", Update, (w, s) => w.WriteAny(s)); } -#pragma warning disable BOO001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. if (Copy != null) { writer.WriteProperty("x-copy", Copy); } -#pragma warning restore BOO001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. writer.WriteOverlayExtensions(Extensions, OverlaySpecVersion.Overlay1_0); writer.WriteEndObject(); } -#pragma warning disable BOO001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - private (bool, JsonPath?, PathResult?) ValidateBeforeApplying(JsonNode documentJsonNode, OverlayDiagnostic overlayDiagnostic, int index) { if (string.IsNullOrEmpty(Target)) @@ -193,7 +186,6 @@ private bool CopyNodes(PathResult parseResult, JsonNode documentJsonNode, Overla } return true; } -#pragma warning restore BOO001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. private static string GetPointer(int index) => $"$.actions[{index}]"; diff --git a/src/lib/Reader/V1/OverlayActionDeserializer.cs b/src/lib/Reader/V1/OverlayActionDeserializer.cs index 6ff0786..4531c0d 100644 --- a/src/lib/Reader/V1/OverlayActionDeserializer.cs +++ b/src/lib/Reader/V1/OverlayActionDeserializer.cs @@ -17,9 +17,7 @@ internal static partial class OverlayV1Deserializer } }, { "update", (o, v) => o.Update = v.CreateAny() }, -#pragma warning disable BOO001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. { "x-copy", (o, v) => o.Copy = v.GetScalarValue() }, -#pragma warning restore BOO001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. }; public static readonly PatternFieldMap ActionPatternFields = new() { diff --git a/tests/lib/Serialization/OverlayActionTests.cs b/tests/lib/Serialization/OverlayActionTests.cs index d3d268e..d551ee3 100644 --- a/tests/lib/Serialization/OverlayActionTests.cs +++ b/tests/lib/Serialization/OverlayActionTests.cs @@ -8,8 +8,6 @@ using ParsingContext = BinkyLabs.OpenApi.Overlays.Reader.ParsingContext; -#pragma warning disable BOO001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - namespace BinkyLabs.OpenApi.Overlays.Tests; public class OverlayActionTests From c16efaea826e611b31daaa65bb5beeb467efe320 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 3 Nov 2025 09:40:43 -0500 Subject: [PATCH 03/26] feat: adds serialization infrastructure for 1.1 Signed-off-by: Vincent Biret --- src/lib/Interfaces/IOverlaySerializable.cs | 9 +++++++++ src/lib/Models/OverlayAction.cs | 14 +++++++------- src/lib/Models/OverlayDocument.cs | 18 +++++++++--------- src/lib/Models/OverlayInfo.cs | 12 ++++++------ src/lib/PublicAPI.Unshipped.txt | 4 ++++ 5 files changed, 35 insertions(+), 22 deletions(-) diff --git a/src/lib/Interfaces/IOverlaySerializable.cs b/src/lib/Interfaces/IOverlaySerializable.cs index 4f139b0..eb52e91 100644 --- a/src/lib/Interfaces/IOverlaySerializable.cs +++ b/src/lib/Interfaces/IOverlaySerializable.cs @@ -5,6 +5,10 @@ namespace BinkyLabs.OpenApi.Overlays; /// /// Represents an OpenAPI overlay element that comes with serialization functionality. /// +/// +/// This interface defines methods for serializing an object to different versions of the OpenAPI Overlay specification. +/// This interface should only be implemented by this library, and not by external consumers as we will add new methods without bumping major versions which will lead to source breaking changes. +/// public interface IOverlaySerializable { /// @@ -12,4 +16,9 @@ public interface IOverlaySerializable /// /// A Microsoft.OpenAPI writer void SerializeAsV1(IOpenApiWriter writer); + /// + /// Serializes the object to the OpenAPI Overlay v1.1 format. + /// + /// A Microsoft.OpenAPI writer + void SerializeAsV1_1(IOpenApiWriter writer); } \ No newline at end of file diff --git a/src/lib/Models/OverlayAction.cs b/src/lib/Models/OverlayAction.cs index f456fac..3fe20fe 100644 --- a/src/lib/Models/OverlayAction.cs +++ b/src/lib/Models/OverlayAction.cs @@ -46,11 +46,11 @@ public class OverlayAction : IOverlaySerializable, IOverlayExtensible /// public IDictionary? Extensions { get; set; } - /// - /// Serializes the action object as an OpenAPI Overlay v1.0.0 JSON object. - /// - /// The OpenAPI writer to use for serialization. - public void SerializeAsV1(IOpenApiWriter writer) + /// + public void SerializeAsV1(IOpenApiWriter writer) => SerializeInternal(writer, OverlaySpecVersion.Overlay1_0, "x-copy"); + /// + public void SerializeAsV1_1(IOpenApiWriter writer) => SerializeInternal(writer, OverlaySpecVersion.Overlay1_1, "copy"); + private void SerializeInternal(IOpenApiWriter writer, OverlaySpecVersion version, string copyFieldName) { writer.WriteStartObject(); writer.WriteRequiredProperty("target", Target); @@ -63,10 +63,10 @@ public void SerializeAsV1(IOpenApiWriter writer) } if (Copy != null) { - writer.WriteProperty("x-copy", Copy); + writer.WriteProperty(copyFieldName, Copy); } - writer.WriteOverlayExtensions(Extensions, OverlaySpecVersion.Overlay1_0); + writer.WriteOverlayExtensions(Extensions, version); writer.WriteEndObject(); } diff --git a/src/lib/Models/OverlayDocument.cs b/src/lib/Models/OverlayDocument.cs index ff1869e..e540293 100644 --- a/src/lib/Models/OverlayDocument.cs +++ b/src/lib/Models/OverlayDocument.cs @@ -37,24 +37,24 @@ public class OverlayDocument : IOverlaySerializable, IOverlayExtensible /// public IDictionary? Extensions { get; set; } - /// - /// Serializes the overlay document as an OpenAPI Overlay v1.0.0 JSON object. - /// - /// The OpenAPI writer to use for serialization. - public void SerializeAsV1(IOpenApiWriter writer) + /// + public void SerializeAsV1(IOpenApiWriter writer) => SerializeInternal(writer, OverlaySpecVersion.Overlay1_0, (w, obj) => obj.SerializeAsV1(w)); + /// + public void SerializeAsV1_1(IOpenApiWriter writer) => SerializeInternal(writer, OverlaySpecVersion.Overlay1_1, (w, obj) => obj.SerializeAsV1_1(w)); + private void SerializeInternal(IOpenApiWriter writer, OverlaySpecVersion version, Action serializeAction) { writer.WriteStartObject(); - writer.WriteRequiredProperty("overlay", "1.0.0"); + writer.WriteRequiredProperty("overlay", Overlay); if (Info != null) { - writer.WriteRequiredObject("info", Info, (w, obj) => obj.SerializeAsV1(w)); + writer.WriteRequiredObject("info", Info, serializeAction); } writer.WriteProperty("extends", Extends); if (Actions != null) { - writer.WriteRequiredCollection("actions", Actions, (w, action) => action.SerializeAsV1(w)); + writer.WriteRequiredCollection("actions", Actions, serializeAction); } - writer.WriteOverlayExtensions(Extensions, OverlaySpecVersion.Overlay1_0); + writer.WriteOverlayExtensions(Extensions, version); writer.WriteEndObject(); } diff --git a/src/lib/Models/OverlayInfo.cs b/src/lib/Models/OverlayInfo.cs index 3ebb254..a07146e 100644 --- a/src/lib/Models/OverlayInfo.cs +++ b/src/lib/Models/OverlayInfo.cs @@ -22,16 +22,16 @@ public class OverlayInfo : IOverlaySerializable, IOverlayExtensible /// public IDictionary? Extensions { get; set; } - /// - /// Serializes the info object as an OpenAPI Overlay v1.0.0 JSON object. - /// - /// The OpenAPI writer to use for serialization. - public void SerializeAsV1(IOpenApiWriter writer) + /// + public void SerializeAsV1(IOpenApiWriter writer) => SerializeInternal(writer, OverlaySpecVersion.Overlay1_0); + /// + public void SerializeAsV1_1(IOpenApiWriter writer) => SerializeInternal(writer, OverlaySpecVersion.Overlay1_1); + private void SerializeInternal(IOpenApiWriter writer, OverlaySpecVersion version) { writer.WriteStartObject(); writer.WriteProperty("title", Title); writer.WriteProperty("version", Version); - writer.WriteOverlayExtensions(Extensions, OverlaySpecVersion.Overlay1_0); + writer.WriteOverlayExtensions(Extensions, version); writer.WriteEndObject(); } } \ No newline at end of file diff --git a/src/lib/PublicAPI.Unshipped.txt b/src/lib/PublicAPI.Unshipped.txt index 1f4515a..9039b91 100644 --- a/src/lib/PublicAPI.Unshipped.txt +++ b/src/lib/PublicAPI.Unshipped.txt @@ -9,6 +9,7 @@ BinkyLabs.OpenApi.Overlays.IOverlayReader.GetJsonNodeFromStreamAsync(System.IO.S BinkyLabs.OpenApi.Overlays.IOverlayReader.ReadAsync(System.IO.Stream! input, System.Uri! location, BinkyLabs.OpenApi.Overlays.OverlayReaderSettings! settings, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! BinkyLabs.OpenApi.Overlays.IOverlaySerializable BinkyLabs.OpenApi.Overlays.IOverlaySerializable.SerializeAsV1(Microsoft.OpenApi.IOpenApiWriter! writer) -> void +BinkyLabs.OpenApi.Overlays.IOverlaySerializable.SerializeAsV1_1(Microsoft.OpenApi.IOpenApiWriter! writer) -> void BinkyLabs.OpenApi.Overlays.JsonNodeExtension BinkyLabs.OpenApi.Overlays.JsonNodeExtension.JsonNodeExtension(System.Text.Json.Nodes.JsonNode! jsonNode) -> void BinkyLabs.OpenApi.Overlays.JsonNodeExtension.Node.get -> System.Text.Json.Nodes.JsonNode! @@ -24,6 +25,7 @@ BinkyLabs.OpenApi.Overlays.OverlayAction.OverlayAction() -> void BinkyLabs.OpenApi.Overlays.OverlayAction.Remove.get -> bool? BinkyLabs.OpenApi.Overlays.OverlayAction.Remove.set -> void BinkyLabs.OpenApi.Overlays.OverlayAction.SerializeAsV1(Microsoft.OpenApi.IOpenApiWriter! writer) -> void +BinkyLabs.OpenApi.Overlays.OverlayAction.SerializeAsV1_1(Microsoft.OpenApi.IOpenApiWriter! writer) -> void 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? @@ -66,6 +68,7 @@ BinkyLabs.OpenApi.Overlays.OverlayDocument.Info.set -> void BinkyLabs.OpenApi.Overlays.OverlayDocument.Overlay.get -> string? BinkyLabs.OpenApi.Overlays.OverlayDocument.OverlayDocument() -> void BinkyLabs.OpenApi.Overlays.OverlayDocument.SerializeAsV1(Microsoft.OpenApi.IOpenApiWriter! writer) -> void +BinkyLabs.OpenApi.Overlays.OverlayDocument.SerializeAsV1_1(Microsoft.OpenApi.IOpenApiWriter! writer) -> void BinkyLabs.OpenApi.Overlays.OverlayException BinkyLabs.OpenApi.Overlays.OverlayException.OverlayException() -> void BinkyLabs.OpenApi.Overlays.OverlayException.OverlayException(string! message) -> void @@ -78,6 +81,7 @@ BinkyLabs.OpenApi.Overlays.OverlayInfo.Extensions.get -> System.Collections.Gene BinkyLabs.OpenApi.Overlays.OverlayInfo.Extensions.set -> void BinkyLabs.OpenApi.Overlays.OverlayInfo.OverlayInfo() -> void BinkyLabs.OpenApi.Overlays.OverlayInfo.SerializeAsV1(Microsoft.OpenApi.IOpenApiWriter! writer) -> void +BinkyLabs.OpenApi.Overlays.OverlayInfo.SerializeAsV1_1(Microsoft.OpenApi.IOpenApiWriter! writer) -> void BinkyLabs.OpenApi.Overlays.OverlayInfo.Title.get -> string? BinkyLabs.OpenApi.Overlays.OverlayInfo.Title.set -> void BinkyLabs.OpenApi.Overlays.OverlayInfo.Version.get -> string? From 0f701ae91388aa21dc54c32f91674958d23b7850 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 3 Nov 2025 10:16:54 -0500 Subject: [PATCH 04/26] feat: adds deserialization infrastructure for version 1.1 Signed-off-by: Vincent Biret --- src/lib/Reader/ParseNodes/FixedFieldMap.cs | 12 ++++- src/lib/Reader/ParseNodes/PatternFieldMap.cs | 12 ++++- src/lib/Reader/ParsingContext.cs | 16 +++++-- .../Reader/V1/OverlayActionDeserializer.cs | 10 ++-- .../Reader/V1/OverlayDocumentDeserializer.cs | 10 ++-- src/lib/Reader/V1/OverlayInfoDeserializer.cs | 10 ++-- src/lib/Reader/V1/OverlayV1Deserializer.cs | 8 ++-- src/lib/Reader/V1/OverlayV1VersionService.cs | 6 +-- .../Reader/V1_1/OverlayActionDeserializer.cs | 20 ++++++++ .../V1_1/OverlayDocumentDeserializer.cs | 27 +++++++++++ .../Reader/V1_1/OverlayInfoDeserializer.cs | 19 ++++++++ .../Reader/V1_1/OverlayV1_1Deserializer.cs | 12 +++++ .../Reader/V1_1/OverlayV1_1VersionService.cs | 46 +++++++++++++++++++ 13 files changed, 181 insertions(+), 27 deletions(-) create mode 100644 src/lib/Reader/V1_1/OverlayActionDeserializer.cs create mode 100644 src/lib/Reader/V1_1/OverlayDocumentDeserializer.cs create mode 100644 src/lib/Reader/V1_1/OverlayInfoDeserializer.cs create mode 100644 src/lib/Reader/V1_1/OverlayV1_1Deserializer.cs create mode 100644 src/lib/Reader/V1_1/OverlayV1_1VersionService.cs diff --git a/src/lib/Reader/ParseNodes/FixedFieldMap.cs b/src/lib/Reader/ParseNodes/FixedFieldMap.cs index f660748..04f21f9 100644 --- a/src/lib/Reader/ParseNodes/FixedFieldMap.cs +++ b/src/lib/Reader/ParseNodes/FixedFieldMap.cs @@ -7,6 +7,14 @@ namespace BinkyLabs.OpenApi.Overlays.Reader { internal class FixedFieldMap : Dictionary> - { - } + { + public FixedFieldMap() : base() + { + + } + public FixedFieldMap(FixedFieldMap source) : base(source) + { + + } + } } \ No newline at end of file diff --git a/src/lib/Reader/ParseNodes/PatternFieldMap.cs b/src/lib/Reader/ParseNodes/PatternFieldMap.cs index bcb28e4..af8e47b 100644 --- a/src/lib/Reader/ParseNodes/PatternFieldMap.cs +++ b/src/lib/Reader/ParseNodes/PatternFieldMap.cs @@ -7,6 +7,14 @@ namespace BinkyLabs.OpenApi.Overlays.Reader { internal class PatternFieldMap : Dictionary, Action> - { - } + { + public PatternFieldMap() : base() + { + + } + public PatternFieldMap(PatternFieldMap source):base(source) + { + + } + } } \ No newline at end of file diff --git a/src/lib/Reader/ParsingContext.cs b/src/lib/Reader/ParsingContext.cs index 50904f7..7b59dca 100644 --- a/src/lib/Reader/ParsingContext.cs +++ b/src/lib/Reader/ParsingContext.cs @@ -1,15 +1,12 @@  // Licensed under the MIT license. -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.Json.Nodes; using BinkyLabs.OpenApi.Overlays.Reader.V1; +using BinkyLabs.OpenApi.Overlays.Reader.V1_1; using Microsoft.OpenApi; -using Microsoft.OpenApi.Reader; namespace BinkyLabs.OpenApi.Overlays.Reader; @@ -54,6 +51,7 @@ public ParsingContext(OverlayDiagnostic diagnostic) Diagnostic = diagnostic; } private const string OverlayV1Version = "1.0.0"; + private const string OverlayV1_1Version = "1.1.0"; /// /// Initiates the parsing process. Not thread safe and should only be called once on a parsing context @@ -77,6 +75,12 @@ public OverlayDocument Parse(JsonNode jsonNode, Uri location) this.Diagnostic.SpecificationVersion = OverlaySpecVersion.Overlay1_0; ValidateRequiredFields(doc, version); break; + case string version when OverlayV1_1Version.Equals(version, StringComparison.OrdinalIgnoreCase): + VersionService = new OverlayV1_1VersionService(Diagnostic); + doc = VersionService.LoadDocument(RootNode, location); + this.Diagnostic.SpecificationVersion = OverlaySpecVersion.Overlay1_1; + ValidateRequiredFields(doc, version); + break; default: throw new OpenApiUnsupportedSpecVersionException(inputVersion); @@ -103,6 +107,10 @@ public OverlayDocument Parse(JsonNode jsonNode, Uri location) VersionService = new OverlayV1VersionService(Diagnostic); element = this.VersionService.LoadElement(node); break; + case OverlaySpecVersion.Overlay1_1: + VersionService = new OverlayV1_1VersionService(Diagnostic); + element = this.VersionService.LoadElement(node); + break; default: throw new OpenApiUnsupportedSpecVersionException(version.ToString()); diff --git a/src/lib/Reader/V1/OverlayActionDeserializer.cs b/src/lib/Reader/V1/OverlayActionDeserializer.cs index 4531c0d..adf932b 100644 --- a/src/lib/Reader/V1/OverlayActionDeserializer.cs +++ b/src/lib/Reader/V1/OverlayActionDeserializer.cs @@ -19,10 +19,14 @@ internal static partial class OverlayV1Deserializer { "update", (o, v) => o.Update = v.CreateAny() }, { "x-copy", (o, v) => o.Copy = v.GetScalarValue() }, }; - public static readonly PatternFieldMap ActionPatternFields = new() + public static PatternFieldMap GetActionPatternFields(OverlaySpecVersion version) { - {s => s.StartsWith(OverlayConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, k, n) => o.AddExtension(k,LoadExtension(k, n))} - }; + return new PatternFieldMap() + { + {s => s.StartsWith(OverlayConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, k, n) => o.AddExtension(k,LoadExtension(k, n, version))} + }; + } + public static readonly PatternFieldMap ActionPatternFields = GetActionPatternFields(OverlaySpecVersion.Overlay1_0); public static OverlayAction LoadAction(ParseNode node) { var mapNode = node.CheckMapNode("Action"); diff --git a/src/lib/Reader/V1/OverlayDocumentDeserializer.cs b/src/lib/Reader/V1/OverlayDocumentDeserializer.cs index 4567ed6..86c20fc 100644 --- a/src/lib/Reader/V1/OverlayDocumentDeserializer.cs +++ b/src/lib/Reader/V1/OverlayDocumentDeserializer.cs @@ -11,10 +11,14 @@ internal static partial class OverlayV1Deserializer { "info", (o, v) => o.Info = LoadInfo(v) }, { "actions", (o, v) => o.Actions = v.CreateList(LoadAction) } }; - public static readonly PatternFieldMap DocumentPatternFields = new() + public static PatternFieldMap GetDocumentPatternFields(OverlaySpecVersion version) { - {s => s.StartsWith(OverlayConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, k, n) => o.AddExtension(k,LoadExtension(k, n))} - }; + return new PatternFieldMap() + { + {s => s.StartsWith(OverlayConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, k, n) => o.AddExtension(k, LoadExtension(k, n, version))} + }; + } + public static readonly PatternFieldMap DocumentPatternFields = GetDocumentPatternFields(OverlaySpecVersion.Overlay1_0); public static OverlayDocument LoadOverlayDocument(RootNode rootNode, Uri location) { var document = new OverlayDocument(); diff --git a/src/lib/Reader/V1/OverlayInfoDeserializer.cs b/src/lib/Reader/V1/OverlayInfoDeserializer.cs index 64de2fb..f603d38 100644 --- a/src/lib/Reader/V1/OverlayInfoDeserializer.cs +++ b/src/lib/Reader/V1/OverlayInfoDeserializer.cs @@ -9,10 +9,14 @@ internal static partial class OverlayV1Deserializer { "title", (o, v) => o.Title = v.GetScalarValue() }, { "version", (o, v) => o.Version = v.GetScalarValue() } }; - public static readonly PatternFieldMap InfoPatternFields = new() + public static PatternFieldMap GetInfoPatternFields(OverlaySpecVersion version) { - {s => s.StartsWith(OverlayConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, k, n) => o.AddExtension(k,LoadExtension(k, n))} - }; + return new PatternFieldMap() + { + {s => s.StartsWith(OverlayConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, k, n) => o.AddExtension(k, LoadExtension(k, n, version))} + }; + } + public static readonly PatternFieldMap InfoPatternFields = GetInfoPatternFields(OverlaySpecVersion.Overlay1_0); public static OverlayInfo LoadInfo(ParseNode node) { var mapNode = node.CheckMapNode("Info"); diff --git a/src/lib/Reader/V1/OverlayV1Deserializer.cs b/src/lib/Reader/V1/OverlayV1Deserializer.cs index 8b1756c..eb9b511 100644 --- a/src/lib/Reader/V1/OverlayV1Deserializer.cs +++ b/src/lib/Reader/V1/OverlayV1Deserializer.cs @@ -1,12 +1,10 @@ using System.Text.Json.Nodes; -using Microsoft.OpenApi; - namespace BinkyLabs.OpenApi.Overlays.Reader.V1; internal static partial class OverlayV1Deserializer { - private static void ParseMap( + internal static void ParseMap( MapNode? mapNode, T domainObject, FixedFieldMap fixedFieldMap, @@ -27,10 +25,10 @@ public static JsonNode LoadAny(ParseNode node) { return node.CreateAny(); } - private static IOverlayExtension LoadExtension(string name, ParseNode node) + private static IOverlayExtension LoadExtension(string name, ParseNode node, OverlaySpecVersion version) { if (node.Context.ExtensionParsers is not null && node.Context.ExtensionParsers.TryGetValue(name, out var parser) && parser( - node.CreateAny(), OverlaySpecVersion.Overlay1_0) is { } result) + node.CreateAny(), version) is { } result) { return result; } diff --git a/src/lib/Reader/V1/OverlayV1VersionService.cs b/src/lib/Reader/V1/OverlayV1VersionService.cs index 836089c..fad7399 100644 --- a/src/lib/Reader/V1/OverlayV1VersionService.cs +++ b/src/lib/Reader/V1/OverlayV1VersionService.cs @@ -1,11 +1,7 @@  // Licensed under the MIT license. -using System; -using System.Collections.Generic; - using Microsoft.OpenApi; -using Microsoft.OpenApi.Reader; namespace BinkyLabs.OpenApi.Overlays.Reader.V1; @@ -23,7 +19,7 @@ public OverlayV1VersionService(OverlayDiagnostic diagnostic) { } - private readonly Dictionary> _loaders = new Dictionary> + internal static readonly Dictionary> _loaders = new Dictionary> { [typeof(JsonNodeExtension)] = OverlayV1Deserializer.LoadAny, [typeof(OverlayAction)] = OverlayV1Deserializer.LoadAction, diff --git a/src/lib/Reader/V1_1/OverlayActionDeserializer.cs b/src/lib/Reader/V1_1/OverlayActionDeserializer.cs new file mode 100644 index 0000000..cf0bcea --- /dev/null +++ b/src/lib/Reader/V1_1/OverlayActionDeserializer.cs @@ -0,0 +1,20 @@ +using BinkyLabs.OpenApi.Overlays.Reader.V1; + +namespace BinkyLabs.OpenApi.Overlays.Reader.V1_1; + +internal static partial class OverlayV1_1Deserializer +{ + public static readonly FixedFieldMap ActionFixedFields = new(OverlayV1Deserializer.ActionFixedFields) + { + { "copy", (o, v) => o.Copy = v.GetScalarValue() }, + }; + public static readonly PatternFieldMap ActionPatternFields = new(OverlayV1Deserializer.GetActionPatternFields(OverlaySpecVersion.Overlay1_1)); + public static OverlayAction LoadAction(ParseNode node) + { + var mapNode = node.CheckMapNode("Action"); + var action = new OverlayAction(); + ParseMap(mapNode, action, ActionFixedFields, ActionPatternFields); + + return action; + } +} \ No newline at end of file diff --git a/src/lib/Reader/V1_1/OverlayDocumentDeserializer.cs b/src/lib/Reader/V1_1/OverlayDocumentDeserializer.cs new file mode 100644 index 0000000..42246a5 --- /dev/null +++ b/src/lib/Reader/V1_1/OverlayDocumentDeserializer.cs @@ -0,0 +1,27 @@ +using BinkyLabs.OpenApi.Overlays.Reader.V1; + +namespace BinkyLabs.OpenApi.Overlays.Reader.V1_1; + +internal static partial class OverlayV1_1Deserializer +{ + public static readonly FixedFieldMap DocumentFixedFields = new(OverlayV1Deserializer.DocumentFixedFields) + { + { "info", (o, v) => o.Info = LoadInfo(v) }, + { "actions", (o, v) => o.Actions = v.CreateList(LoadAction) } + }; + public static readonly PatternFieldMap DocumentPatternFields = OverlayV1Deserializer.GetDocumentPatternFields(OverlaySpecVersion.Overlay1_1); + public static OverlayDocument LoadOverlayDocument(RootNode rootNode, Uri location) + { + var document = new OverlayDocument(); + ParseMap(rootNode.GetMap(), document, DocumentFixedFields, DocumentPatternFields); + return document; + } + public static OverlayDocument LoadDocument(ParseNode node) + { + var mapNode = node.CheckMapNode("Document"); + var info = new OverlayDocument(); + ParseMap(mapNode, info, DocumentFixedFields, DocumentPatternFields); + + return info; + } +} \ No newline at end of file diff --git a/src/lib/Reader/V1_1/OverlayInfoDeserializer.cs b/src/lib/Reader/V1_1/OverlayInfoDeserializer.cs new file mode 100644 index 0000000..522d11a --- /dev/null +++ b/src/lib/Reader/V1_1/OverlayInfoDeserializer.cs @@ -0,0 +1,19 @@ +using BinkyLabs.OpenApi.Overlays.Reader.V1; + +using Microsoft.OpenApi; + +namespace BinkyLabs.OpenApi.Overlays.Reader.V1_1; + +internal static partial class OverlayV1_1Deserializer +{ + public static readonly FixedFieldMap InfoFixedFields = new(OverlayV1Deserializer.InfoFixedFields); + public static readonly PatternFieldMap InfoPatternFields = OverlayV1Deserializer.GetInfoPatternFields(OverlaySpecVersion.Overlay1_1); + public static OverlayInfo LoadInfo(ParseNode node) + { + var mapNode = node.CheckMapNode("Info"); + var info = new OverlayInfo(); + ParseMap(mapNode, info, InfoFixedFields, InfoPatternFields); + + return info; + } +} \ No newline at end of file diff --git a/src/lib/Reader/V1_1/OverlayV1_1Deserializer.cs b/src/lib/Reader/V1_1/OverlayV1_1Deserializer.cs new file mode 100644 index 0000000..af4f8e3 --- /dev/null +++ b/src/lib/Reader/V1_1/OverlayV1_1Deserializer.cs @@ -0,0 +1,12 @@ +using BinkyLabs.OpenApi.Overlays.Reader.V1; + +namespace BinkyLabs.OpenApi.Overlays.Reader.V1_1; + +internal static partial class OverlayV1_1Deserializer +{ + private static void ParseMap( + MapNode? mapNode, + T domainObject, + FixedFieldMap fixedFieldMap, + PatternFieldMap patternFieldMap) => OverlayV1Deserializer.ParseMap(mapNode, domainObject, fixedFieldMap, patternFieldMap); +} \ No newline at end of file diff --git a/src/lib/Reader/V1_1/OverlayV1_1VersionService.cs b/src/lib/Reader/V1_1/OverlayV1_1VersionService.cs new file mode 100644 index 0000000..fb375bd --- /dev/null +++ b/src/lib/Reader/V1_1/OverlayV1_1VersionService.cs @@ -0,0 +1,46 @@ + +// Licensed under the MIT license. + +using BinkyLabs.OpenApi.Overlays.Reader.V1; + +using Microsoft.OpenApi; + +namespace BinkyLabs.OpenApi.Overlays.Reader.V1_1; + +/// +/// The version service for the Overlay 1.1 specification. +/// +internal class OverlayV1_1VersionService : IOverlayVersionService +{ + + /// + /// Create Parsing Context + /// + /// Provide instance for diagnostic object for collecting and accessing information about the parsing. + public OverlayV1_1VersionService(OverlayDiagnostic diagnostic) + { + } + + private readonly Dictionary> _loaders = new Dictionary>(OverlayV1VersionService._loaders) + { + [typeof(OverlayInfo)] = OverlayV1_1Deserializer.LoadInfo, + [typeof(OverlayAction)] = OverlayV1_1Deserializer.LoadAction, + [typeof(OverlayDocument)] = OverlayV1_1Deserializer.LoadDocument, + }; + + public OverlayDocument LoadDocument(RootNode rootNode, Uri location) + { + return OverlayV1_1Deserializer.LoadOverlayDocument(rootNode, location); + } + + public T? LoadElement(ParseNode node) where T : IOpenApiElement + { + if (Loaders.TryGetValue(typeof(T), out var loader) && loader(node) is T result) + { + return result; + } + return default; + } + + internal Dictionary> Loaders => _loaders; +} \ No newline at end of file From aceba7f76aa17f0c9289f16afddea1b9ba924b65 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 3 Nov 2025 10:24:34 -0500 Subject: [PATCH 05/26] chore: formatting --- src/lib/Reader/ParseNodes/FixedFieldMap.cs | 6 +++--- src/lib/Reader/ParseNodes/PatternFieldMap.cs | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lib/Reader/ParseNodes/FixedFieldMap.cs b/src/lib/Reader/ParseNodes/FixedFieldMap.cs index 04f21f9..4b7375b 100644 --- a/src/lib/Reader/ParseNodes/FixedFieldMap.cs +++ b/src/lib/Reader/ParseNodes/FixedFieldMap.cs @@ -7,14 +7,14 @@ namespace BinkyLabs.OpenApi.Overlays.Reader { internal class FixedFieldMap : Dictionary> - { + { public FixedFieldMap() : base() { } public FixedFieldMap(FixedFieldMap source) : base(source) { - + } - } + } } \ No newline at end of file diff --git a/src/lib/Reader/ParseNodes/PatternFieldMap.cs b/src/lib/Reader/ParseNodes/PatternFieldMap.cs index af8e47b..f9a1d58 100644 --- a/src/lib/Reader/ParseNodes/PatternFieldMap.cs +++ b/src/lib/Reader/ParseNodes/PatternFieldMap.cs @@ -7,14 +7,14 @@ namespace BinkyLabs.OpenApi.Overlays.Reader { internal class PatternFieldMap : Dictionary, Action> - { + { public PatternFieldMap() : base() { } - public PatternFieldMap(PatternFieldMap source):base(source) + public PatternFieldMap(PatternFieldMap source) : base(source) { - + } - } + } } \ No newline at end of file From 44db9b1c8697f1b9bf65f328b0a129fb15d4db4a Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 3 Nov 2025 10:27:26 -0500 Subject: [PATCH 06/26] tests: moves v1 tests to dedicated folder Signed-off-by: Vincent Biret --- tests/lib/Serialization/{ => V1}/OverlayActionTests.cs | 2 +- tests/lib/Serialization/{ => V1}/OverlayDocumentTests.cs | 2 +- tests/lib/Serialization/{ => V1}/OverlayInfoTests.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename tests/lib/Serialization/{ => V1}/OverlayActionTests.cs (99%) rename tests/lib/Serialization/{ => V1}/OverlayDocumentTests.cs (99%) rename tests/lib/Serialization/{ => V1}/OverlayInfoTests.cs (98%) diff --git a/tests/lib/Serialization/OverlayActionTests.cs b/tests/lib/Serialization/V1/OverlayActionTests.cs similarity index 99% rename from tests/lib/Serialization/OverlayActionTests.cs rename to tests/lib/Serialization/V1/OverlayActionTests.cs index d551ee3..a475ee4 100644 --- a/tests/lib/Serialization/OverlayActionTests.cs +++ b/tests/lib/Serialization/V1/OverlayActionTests.cs @@ -10,7 +10,7 @@ namespace BinkyLabs.OpenApi.Overlays.Tests; -public class OverlayActionTests +public class OverlayActionV1Tests { [Fact] public void SerializeAsV1_ShouldWriteCorrectJson() diff --git a/tests/lib/Serialization/OverlayDocumentTests.cs b/tests/lib/Serialization/V1/OverlayDocumentTests.cs similarity index 99% rename from tests/lib/Serialization/OverlayDocumentTests.cs rename to tests/lib/Serialization/V1/OverlayDocumentTests.cs index ddc64c7..3404ea0 100644 --- a/tests/lib/Serialization/OverlayDocumentTests.cs +++ b/tests/lib/Serialization/V1/OverlayDocumentTests.cs @@ -12,7 +12,7 @@ namespace BinkyLabs.OpenApi.Overlays.Tests; -public sealed class OverlayDocumentTests : IDisposable +public sealed class OverlayDocumentV1Tests : IDisposable { [Fact] public void SerializeAsV1_ShouldWriteCorrectJson() diff --git a/tests/lib/Serialization/OverlayInfoTests.cs b/tests/lib/Serialization/V1/OverlayInfoTests.cs similarity index 98% rename from tests/lib/Serialization/OverlayInfoTests.cs rename to tests/lib/Serialization/V1/OverlayInfoTests.cs index 9ca33e3..698cba3 100644 --- a/tests/lib/Serialization/OverlayInfoTests.cs +++ b/tests/lib/Serialization/V1/OverlayInfoTests.cs @@ -10,7 +10,7 @@ namespace BinkyLabs.OpenApi.Overlays.Tests; -public class OverlayInfoTests +public class OverlayInfoV1Tests { [Fact] public void SerializeAsV1_ShouldWriteCorrectJson() From 48544181e275dc17a873df63249075f49993e168 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 3 Nov 2025 10:33:47 -0500 Subject: [PATCH 07/26] tests: adds tests for V1.1 serialization and deserialization Signed-off-by: Vincent Biret --- src/lib/Reader/ParseNodes/FixedFieldMap.cs | 7 +- .../V1_1/OverlayDocumentDeserializer.cs | 2 +- .../Serialization/V1_1/OverlayActionTests.cs | 1057 +++++++++++++++++ .../V1_1/OverlayDocumentTests.cs | 898 ++++++++++++++ .../Serialization/V1_1/OverlayInfoTests.cs | 68 ++ 5 files changed, 2028 insertions(+), 4 deletions(-) create mode 100644 tests/lib/Serialization/V1_1/OverlayActionTests.cs create mode 100644 tests/lib/Serialization/V1_1/OverlayDocumentTests.cs create mode 100644 tests/lib/Serialization/V1_1/OverlayInfoTests.cs diff --git a/src/lib/Reader/ParseNodes/FixedFieldMap.cs b/src/lib/Reader/ParseNodes/FixedFieldMap.cs index 4b7375b..7b0ae20 100644 --- a/src/lib/Reader/ParseNodes/FixedFieldMap.cs +++ b/src/lib/Reader/ParseNodes/FixedFieldMap.cs @@ -1,9 +1,6 @@  // Licensed under the MIT license. -using System; -using System.Collections.Generic; - namespace BinkyLabs.OpenApi.Overlays.Reader { internal class FixedFieldMap : Dictionary> @@ -13,6 +10,10 @@ public FixedFieldMap() : base() } public FixedFieldMap(FixedFieldMap source) : base(source) + { + + } + public FixedFieldMap(FixedFieldMap source, HashSet except) : base(source.Where(kv => !except.Contains(kv.Key))) { } diff --git a/src/lib/Reader/V1_1/OverlayDocumentDeserializer.cs b/src/lib/Reader/V1_1/OverlayDocumentDeserializer.cs index 42246a5..c4f2e94 100644 --- a/src/lib/Reader/V1_1/OverlayDocumentDeserializer.cs +++ b/src/lib/Reader/V1_1/OverlayDocumentDeserializer.cs @@ -4,7 +4,7 @@ namespace BinkyLabs.OpenApi.Overlays.Reader.V1_1; internal static partial class OverlayV1_1Deserializer { - public static readonly FixedFieldMap DocumentFixedFields = new(OverlayV1Deserializer.DocumentFixedFields) + public static readonly FixedFieldMap DocumentFixedFields = new(OverlayV1Deserializer.DocumentFixedFields, ["info", "actions"]) { { "info", (o, v) => o.Info = LoadInfo(v) }, { "actions", (o, v) => o.Actions = v.CreateList(LoadAction) } diff --git a/tests/lib/Serialization/V1_1/OverlayActionTests.cs b/tests/lib/Serialization/V1_1/OverlayActionTests.cs new file mode 100644 index 0000000..75fead0 --- /dev/null +++ b/tests/lib/Serialization/V1_1/OverlayActionTests.cs @@ -0,0 +1,1057 @@ +using System.Text.Json.Nodes; + +using BinkyLabs.OpenApi.Overlays.Reader; +using BinkyLabs.OpenApi.Overlays.Reader.V1_1; + +using Microsoft.OpenApi; +using Microsoft.OpenApi.Reader; + +using ParsingContext = BinkyLabs.OpenApi.Overlays.Reader.ParsingContext; + +namespace BinkyLabs.OpenApi.Overlays.Tests; + +public class OverlayActionV1_1Tests +{ + [Fact] + public void SerializeAsV1_1_ShouldWriteCorrectJson() + { + // Arrange + var overlayAction = new OverlayAction + { + Target = "Test Target", + Description = "Test Description", + Remove = true + }; + using var textWriter = new StringWriter(); + var writer = new OpenApiJsonWriter(textWriter); + + var expectedJson = +""" +{ + "target": "Test Target", + "description": "Test Description", + "remove": true +} +"""; + + // Act + overlayAction.SerializeAsV1_1(writer); + var jsonResult = textWriter.ToString(); + var jsonResultObject = JsonNode.Parse(jsonResult); + var expectedJsonObject = JsonNode.Parse(expectedJson); + + + // Assert + Assert.True(JsonNode.DeepEquals(jsonResultObject, expectedJsonObject), "The serialized JSON does not match the expected JSON."); + } + + [Fact] + public void SerializeAsV1_1_WithUpdate_ShouldWriteCorrectJson() + { + // Arrange + var updateNode = JsonNode.Parse(""" + { + "summary": "Updated summary", + "description": "Updated description", + "operationId": "updateOperation" + } + """); + + var overlayAction = new OverlayAction + { + Target = "Test Target", + Description = "Test Description", + Remove = false, + Update = updateNode + }; + using var textWriter = new StringWriter(); + var writer = new OpenApiJsonWriter(textWriter); + + var expectedJson = +""" +{ + "target": "Test Target", + "description": "Test Description", + "update": { + "summary": "Updated summary", + "description": "Updated description", + "operationId": "updateOperation" + } +} +"""; + + // Act + overlayAction.SerializeAsV1_1(writer); + var jsonResult = textWriter.ToString(); + var jsonResultObject = JsonNode.Parse(jsonResult); + var expectedJsonObject = JsonNode.Parse(expectedJson); + + // Assert + Assert.True(JsonNode.DeepEquals(jsonResultObject, expectedJsonObject), "The serialized JSON does not match the expected JSON."); + } + + [Fact] + public void SerializeAsV1_1_WithUpdateArray_ShouldWriteCorrectJson() + { + // Arrange + var updateNode = JsonNode.Parse(""" + ["tag1", "tag2", "tag3"] + """); + + var overlayAction = new OverlayAction + { + Target = "Test Target", + Description = "Test Description", + Update = updateNode + }; + using var textWriter = new StringWriter(); + var writer = new OpenApiJsonWriter(textWriter); + + var expectedJson = +""" +{ + "target": "Test Target", + "description": "Test Description", + "update": ["tag1", "tag2", "tag3"] +} +"""; + + // Act + overlayAction.SerializeAsV1_1(writer); + var jsonResult = textWriter.ToString(); + var jsonResultObject = JsonNode.Parse(jsonResult); + var expectedJsonObject = JsonNode.Parse(expectedJson); + + // Assert + Assert.True(JsonNode.DeepEquals(jsonResultObject, expectedJsonObject), "The serialized JSON does not match the expected JSON."); + } + + [Fact] + public void Deserialize_ShouldSetPropertiesCorrectly() + { + // Arrange + var json = """ + { + "target": "Test Target", + "description": "Test Description", + "remove": true + } + """; + var jsonNode = JsonNode.Parse(json)!; + var parsingContext = new ParsingContext(new()); + var parseNode = new MapNode(parsingContext, jsonNode); + + + // Act + var overlayAction = OverlayV1_1Deserializer.LoadAction(parseNode); + + // Assert + Assert.Equal("Test Target", overlayAction.Target); + Assert.Equal("Test Description", overlayAction.Description); + Assert.True(overlayAction.Remove); + } + + [Fact] + public void Deserialize_WithUpdate_ShouldSetPropertiesCorrectly() + { + // Arrange + var json = """ + { + "target": "Test Target", + "description": "Test Description", + "remove": false, + "update": { + "summary": "Updated summary", + "description": "Updated description", + "operationId": "updateOperation" + } + } + """; + var jsonNode = JsonNode.Parse(json)!; + var parsingContext = new ParsingContext(new()); + var parseNode = new MapNode(parsingContext, jsonNode); + + // Act + var overlayAction = OverlayV1_1Deserializer.LoadAction(parseNode); + + // Assert + Assert.Equal("Test Target", overlayAction.Target); + Assert.Equal("Test Description", overlayAction.Description); + Assert.False(overlayAction.Remove); + Assert.NotNull(overlayAction.Update); + + var updateObject = overlayAction.Update.AsObject(); + Assert.Equal("Updated summary", updateObject["summary"]?.GetValue()); + Assert.Equal("Updated description", updateObject["description"]?.GetValue()); + Assert.Equal("updateOperation", updateObject["operationId"]?.GetValue()); + } + + [Fact] + public void Deserialize_WithUpdateArray_ShouldSetPropertiesCorrectly() + { + // Arrange + var json = """ + { + "target": "Test Target", + "description": "Test Description", + "update": ["tag1", "tag2", "tag3"] + } + """; + var jsonNode = JsonNode.Parse(json)!; + var parsingContext = new ParsingContext(new()); + var parseNode = new MapNode(parsingContext, jsonNode); + + // Act + var overlayAction = OverlayV1_1Deserializer.LoadAction(parseNode); + + // Assert + Assert.Equal("Test Target", overlayAction.Target); + Assert.Equal("Test Description", overlayAction.Description); + Assert.NotNull(overlayAction.Update); + + var updateArray = overlayAction.Update.AsArray(); + Assert.Equal(3, updateArray.Count); + Assert.Equal("tag1", updateArray[0]?.GetValue()); + Assert.Equal("tag2", updateArray[1]?.GetValue()); + Assert.Equal("tag3", updateArray[2]?.GetValue()); + } + + [Fact] + public void Deserialize_WithUpdatePrimitiveValue_ShouldSetPropertiesCorrectly() + { + // Arrange + var json = """ + { + "target": "Test Target", + "description": "Test Description", + "update": "simple string value" + } + """; + var jsonNode = JsonNode.Parse(json)!; + var parsingContext = new ParsingContext(new()); + var parseNode = new MapNode(parsingContext, jsonNode); + + // Act + var overlayAction = OverlayV1_1Deserializer.LoadAction(parseNode); + + // Assert + Assert.Equal("Test Target", overlayAction.Target); + Assert.Equal("Test Description", overlayAction.Description); + Assert.NotNull(overlayAction.Update); + Assert.Equal("simple string value", overlayAction.Update.GetValue()); + } + [Fact] + public void ApplyToDocument_ShouldFailNoNullJsonNode() + { + var overlayAction = new OverlayAction + { + Target = "Test Target", + Remove = true + }; + JsonNode? jsonNode = null; + var overlayDiagnostic = new OverlayDiagnostic(); + + Assert.Throws(() => overlayAction.ApplyToDocument(jsonNode!, overlayDiagnostic, 0)); + } + [Fact] + public void ApplyToDocument_ShouldFailNoDiagnostics() + { + var overlayAction = new OverlayAction + { + Target = "Test Target", + Remove = true + }; + var jsonNode = JsonNode.Parse("{}")!; + OverlayDiagnostic? overlayDiagnostic = null; + + Assert.Throws(() => overlayAction.ApplyToDocument(jsonNode, overlayDiagnostic!, 0)); + } + [Fact] + public void ApplyToDocument_ShouldFailNoTarget() + { + var overlayAction = new OverlayAction + { + Remove = true + }; + var jsonNode = JsonNode.Parse("{}")!; + var overlayDiagnostic = new OverlayDiagnostic(); + + var result = overlayAction.ApplyToDocument(jsonNode, overlayDiagnostic, 0); + + Assert.False(result); + Assert.Single(overlayDiagnostic.Errors); + Assert.Equal("$.actions[0]", overlayDiagnostic.Errors[0].Pointer); + Assert.Equal("Target is required", overlayDiagnostic.Errors[0].Message); + } + [Fact] + public void ApplyToDocument_ShouldFailNoRemoveOrUpdate() + { + var overlayAction = new OverlayAction + { + Target = "Test Target" + }; + var jsonNode = JsonNode.Parse("{}")!; + var overlayDiagnostic = new OverlayDiagnostic(); + + var result = overlayAction.ApplyToDocument(jsonNode, overlayDiagnostic, 0); + + Assert.False(result); + Assert.Single(overlayDiagnostic.Errors); + Assert.Equal("$.actions[0]", overlayDiagnostic.Errors[0].Pointer); + Assert.Equal("At least one of 'remove', 'update' or 'x-copy' must be specified", overlayDiagnostic.Errors[0].Message); + } + [Fact] + public void ApplyToDocument_ShouldFailBothRemoveAndUpdate() + { + var overlayAction = new OverlayAction + { + Target = "Test Target", + Remove = true, + Update = JsonNode.Parse("{}") + }; + var jsonNode = JsonNode.Parse("{}")!; + var overlayDiagnostic = new OverlayDiagnostic(); + + var result = overlayAction.ApplyToDocument(jsonNode, overlayDiagnostic, 0); + + Assert.False(result); + Assert.Single(overlayDiagnostic.Errors); + Assert.Equal("$.actions[0]", overlayDiagnostic.Errors[0].Pointer); + Assert.Equal("At most one of 'remove', 'update' or 'x-copy' can be specified", overlayDiagnostic.Errors[0].Message); + } + [Fact] + public void ApplyToDocument_ShouldFailInvalidJsonPath() + { + var overlayAction = new OverlayAction + { + Target = "Test Target", + Remove = true + }; + var jsonNode = JsonNode.Parse("{}")!; + var overlayDiagnostic = new OverlayDiagnostic(); + + var result = overlayAction.ApplyToDocument(jsonNode, overlayDiagnostic, 0); + + Assert.False(result); + Assert.Single(overlayDiagnostic.Errors); + Assert.Equal("$.actions[0]", overlayDiagnostic.Errors[0].Pointer); + Assert.Equal("Invalid JSON Path: 'Test Target'", overlayDiagnostic.Errors[0].Message); + } + [Fact] + public void ApplyToDocument_ShouldRemoveANode() + { + var overlayAction = new OverlayAction + { + Target = "$.info.title", + Remove = true + }; + var jsonNode = JsonNode.Parse(""" + { + "info": { + "title": "Test API", + "version": "1.0.0" + } + } + """)!; + var overlayDiagnostic = new OverlayDiagnostic(); + + var result = overlayAction.ApplyToDocument(jsonNode, overlayDiagnostic, 0); + + Assert.True(result); + Assert.Null(jsonNode["info"]?["title"]); + Assert.Empty(overlayDiagnostic.Errors); + } + [Fact] + public void ApplyToDocument_ShouldRemoveANodeAndNotErrorInWildcard() + { + var overlayAction = new OverlayAction + { + Target = "$.components.schemas['Foo'].anyOf[*].default", + Remove = true + }; + var jsonNode = JsonNode.Parse(""" + { + "info": { + "title": "Test API", + "version": "1.0.0" + }, + "components": { + "schemas": { + "Foo": { + "anyOf": [ + { + "default": "value1" + }, + { + "type": "string" + } + ] + } + } + } + } + """)!; + var overlayDiagnostic = new OverlayDiagnostic(); + + var result = overlayAction.ApplyToDocument(jsonNode, overlayDiagnostic, 0); + + Assert.True(result); + Assert.Null(jsonNode["components"]?["schemas"]?["Foo"]?["anyOf"]?[0]?["default"]); + Assert.Null(jsonNode["components"]?["schemas"]?["Foo"]?["anyOf"]?[1]?["default"]); + Assert.Equal("string", jsonNode["components"]?["schemas"]?["Foo"]?["anyOf"]?[1]?["type"]?.GetValue()); + Assert.Empty(overlayDiagnostic.Errors); + } + [Fact] + public void ApplyToDocument_ShouldUpdateANode() + { + var overlayAction = new OverlayAction + { + Target = "$.info", + Update = JsonNode.Parse(""" + { + "title": "Updated API" + } + """) + }; + var jsonNode = JsonNode.Parse(""" + { + "info": { + "title": "Test API", + "version": "1.0.0" + } + } + """)!; + var overlayDiagnostic = new OverlayDiagnostic(); + + var result = overlayAction.ApplyToDocument(jsonNode, overlayDiagnostic, 0); + + Assert.True(result); + Assert.Equal("Updated API", jsonNode["info"]?["title"]?.GetValue()); + Assert.Empty(overlayDiagnostic.Errors); + } + + [Fact] + public void ApplyToDocument_ShouldCopySimpleValue() + { + var overlayAction = new OverlayAction + { + Target = "$.info.title", + Copy = "$.info.version" + }; + var jsonNode = JsonNode.Parse(""" + { + "info": { + "title": "Test API", + "version": "1.0.0" + } + } + """)!; + var overlayDiagnostic = new OverlayDiagnostic(); + + var result = overlayAction.ApplyToDocument(jsonNode, overlayDiagnostic, 0); + + Assert.True(result); + Assert.Equal("1.0.0", jsonNode["info"]?["title"]?.GetValue()); + Assert.Empty(overlayDiagnostic.Errors); + } + + [Fact] + public void ApplyToDocument_ShouldCopyComplexObject() + { + var overlayAction = new OverlayAction + { + Target = "$.paths['/users'].get.responses['200']", + Copy = "$.components.responses.UserResponse" + }; + var jsonNode = JsonNode.Parse(""" + { + "paths": { + "/users": { + "get": { + "responses": { + "200": { + "description": "Old description" + } + } + } + } + }, + "components": { + "responses": { + "UserResponse": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + } + } + """)!; + var overlayDiagnostic = new OverlayDiagnostic(); + + var result = overlayAction.ApplyToDocument(jsonNode, overlayDiagnostic, 0); + + Assert.True(result); + Assert.Equal("Successful response", jsonNode["paths"]?["/users"]?["get"]?["responses"]?["200"]?["description"]?.GetValue()); + Assert.NotNull(jsonNode["paths"]?["/users"]?["get"]?["responses"]?["200"]?["content"]); + Assert.Empty(overlayDiagnostic.Errors); + } + + [Fact] + public void ApplyToDocument_ShouldCopyArrayElements() + { + var overlayAction = new OverlayAction + { + Target = "$.paths['/users'].get.tags", + Copy = "$.paths['/posts'].get.tags" + }; + var jsonNode = JsonNode.Parse(""" + { + "paths": { + "/users": { + "get": { + "tags": ["user"] + } + }, + "/posts": { + "get": { + "tags": ["post", "content"] + } + } + } + } + """)!; + var overlayDiagnostic = new OverlayDiagnostic(); + + var result = overlayAction.ApplyToDocument(jsonNode, overlayDiagnostic, 0); + + Assert.True(result); + var targetTags = jsonNode["paths"]?["/users"]?["get"]?["tags"]?.AsArray(); + Assert.NotNull(targetTags); + Assert.Equal(2, targetTags.Count); + Assert.Equal("post", targetTags[0]?.GetValue()); + Assert.Equal("content", targetTags[1]?.GetValue()); + Assert.Empty(overlayDiagnostic.Errors); + } + + [Fact] + public void ApplyToDocument_ShouldCopyMultipleMatches() + { + var overlayAction = new OverlayAction + { + Target = "$.paths[*].get.summary", + Copy = "$.info.title" + }; + var jsonNode = JsonNode.Parse(""" + { + "info": { + "title": "My API" + }, + "paths": { + "/users": { + "get": { + "summary": "Get users" + } + }, + "/posts": { + "get": { + "summary": "Get posts" + } + } + } + } + """)!; + var overlayDiagnostic = new OverlayDiagnostic(); + + var result = overlayAction.ApplyToDocument(jsonNode, overlayDiagnostic, 0); + + Assert.True(result); + Assert.Equal("My API", jsonNode["paths"]?["/users"]?["get"]?["summary"]?.GetValue()); + Assert.Equal("My API", jsonNode["paths"]?["/posts"]?["get"]?["summary"]?.GetValue()); + Assert.Empty(overlayDiagnostic.Errors); + } + + [Fact] + public void ApplyToDocument_CopyShouldFailWithInvalidCopyPath() + { + var overlayAction = new OverlayAction + { + Target = "$.info.title", + Copy = "invalid path" + }; + var jsonNode = JsonNode.Parse(""" + { + "info": { + "title": "Test API" + } + } + """)!; + var overlayDiagnostic = new OverlayDiagnostic(); + + var result = overlayAction.ApplyToDocument(jsonNode, overlayDiagnostic, 0); + + Assert.False(result); + Assert.Single(overlayDiagnostic.Errors); + Assert.Equal("$.actions[0]", overlayDiagnostic.Errors[0].Pointer); + Assert.Equal("Invalid copy JSON Path: 'invalid path'", overlayDiagnostic.Errors[0].Message); + } + + [Fact] + public void ApplyToDocument_CopyShouldFailWhenCopyTargetNotFound() + { + var overlayAction = new OverlayAction + { + Target = "$.info.title", + Copy = "$.nonexistent.field" + }; + var jsonNode = JsonNode.Parse(""" + { + "info": { + "title": "Test API" + } + } + """)!; + var overlayDiagnostic = new OverlayDiagnostic(); + + var result = overlayAction.ApplyToDocument(jsonNode, overlayDiagnostic, 0); + + Assert.False(result); + Assert.Single(overlayDiagnostic.Errors); + Assert.Equal("$.actions[0]", overlayDiagnostic.Errors[0].Pointer); + Assert.Equal("Copy JSON Path '$.nonexistent.field' must match exactly one result, but matched 0", overlayDiagnostic.Errors[0].Message); + } + + [Fact] + public void ApplyToDocument_CopyShouldFailWhenCopyTargetHasNoMatches() + { + var overlayAction = new OverlayAction + { + Target = "$.info.title", + Copy = "$.paths[*].get.nonexistent" + }; + var jsonNode = JsonNode.Parse(""" + { + "info": { + "title": "Test API" + }, + "paths": { + "/users": { + "get": { + "summary": "Get users" + } + } + } + } + """)!; + var overlayDiagnostic = new OverlayDiagnostic(); + + var result = overlayAction.ApplyToDocument(jsonNode, overlayDiagnostic, 0); + + Assert.False(result); + Assert.Single(overlayDiagnostic.Errors); + Assert.Equal("$.actions[0]", overlayDiagnostic.Errors[0].Pointer); + Assert.Equal("Copy JSON Path '$.paths[*].get.nonexistent' must match exactly one result, but matched 0", overlayDiagnostic.Errors[0].Message); + } + + [Fact] + public void ApplyToDocument_CopyShouldFailWhenCopyHasMultipleMatches() + { + var overlayAction = new OverlayAction + { + Target = "$.paths[*].get.summary", + Copy = "$.paths[*].get.operationId" + }; + var jsonNode = JsonNode.Parse(""" + { + "paths": { + "/users": { + "get": { + "summary": "Get users", + "operationId": "listUsers" + } + }, + "/posts": { + "get": { + "summary": "Get posts", + "operationId": "listPosts" + } + } + } + } + """)!; + var overlayDiagnostic = new OverlayDiagnostic(); + + var result = overlayAction.ApplyToDocument(jsonNode, overlayDiagnostic, 0); + + Assert.False(result); + Assert.Single(overlayDiagnostic.Errors); + Assert.Equal("$.actions[0]", overlayDiagnostic.Errors[0].Pointer); + Assert.Equal("Copy JSON Path '$.paths[*].get.operationId' must match exactly one result, but matched 2", overlayDiagnostic.Errors[0].Message); + } + + [Fact] + public void ApplyToDocument_CopyShouldFailWhenTargetPointsToNull() + { + var overlayAction = new OverlayAction + { + Target = "$.info.nonexistent", + Copy = "$.info.title" + }; + var jsonNode = JsonNode.Parse(""" + { + "info": { + "title": "Test API", + "nonexistent": null + } + } + """)!; + var overlayDiagnostic = new OverlayDiagnostic(); + + var result = overlayAction.ApplyToDocument(jsonNode, overlayDiagnostic, 0); + + Assert.False(result); + Assert.Single(overlayDiagnostic.Errors); + Assert.Equal("$.actions[0]", overlayDiagnostic.Errors[0].Pointer); + Assert.Equal("Target '$.info.nonexistent' does not point to a valid JSON node", overlayDiagnostic.Errors[0].Message); + } + + [Fact] + public void ApplyToDocument_CopyShouldFailWhenCopyTargetPointsToNull() + { + var overlayAction = new OverlayAction + { + Target = "$.info.title", + Copy = "$.info.nonexistent" + }; + var jsonNode = JsonNode.Parse(""" + { + "info": { + "title": "Test API", + "nonexistent": null + } + } + """)!; + var overlayDiagnostic = new OverlayDiagnostic(); + + var result = overlayAction.ApplyToDocument(jsonNode, overlayDiagnostic, 0); + + Assert.False(result); + Assert.Single(overlayDiagnostic.Errors); + Assert.Equal("$.actions[0]", overlayDiagnostic.Errors[0].Pointer); + Assert.Equal("Copy target '$.info.nonexistent' does not point to a valid JSON node", overlayDiagnostic.Errors[0].Message); + } + + [Fact] + public void ApplyToDocument_CopyShouldMergeObjectsCorrectly() + { + var overlayAction = new OverlayAction + { + Target = "$.info", + Copy = "$.components.info" + }; + var jsonNode = JsonNode.Parse(""" + { + "info": { + "title": "Original API", + "version": "1.0.0" + }, + "components": { + "info": { + "title": "Updated API", + "description": "New description" + } + } + } + """)!; + var overlayDiagnostic = new OverlayDiagnostic(); + + var result = overlayAction.ApplyToDocument(jsonNode, overlayDiagnostic, 0); + + Assert.True(result); + Assert.Equal("Updated API", jsonNode["info"]?["title"]?.GetValue()); + Assert.Equal("1.0.0", jsonNode["info"]?["version"]?.GetValue()); + Assert.Equal("New description", jsonNode["info"]?["description"]?.GetValue()); + Assert.Empty(overlayDiagnostic.Errors); + } + + [Fact] + public void SerializeAsV1_1_WithCopy_ShouldWriteCorrectJson() + { + // Arrange + var overlayAction = new OverlayAction + { + Target = "$.info.title", + Description = "Copy description to title", + Copy = "$.info.description" + }; + using var textWriter = new StringWriter(); + var writer = new OpenApiJsonWriter(textWriter); + + var expectedJson = +""" +{ + "target": "$.info.title", + "description": "Copy description to title", + "copy": "$.info.description" +} +"""; + + // Act + overlayAction.SerializeAsV1_1(writer); + var jsonResult = textWriter.ToString(); + var jsonResultObject = JsonNode.Parse(jsonResult); + var expectedJsonObject = JsonNode.Parse(expectedJson); + + // Assert + Assert.True(JsonNode.DeepEquals(jsonResultObject, expectedJsonObject), "The serialized JSON does not match the expected JSON."); + } + + [Fact] + public void ApplyToDocument_WorksWithSubSequentRemoval() + { + // Given + var firstAction = new OverlayAction + { + Target = "$..[?(@['$ref'] == '#/components/schemas/Foo')]", + Update = JsonNode.Parse( +""" +{ + "anyOf": + [ + { + "$ref": "#/components/schemas/Foo" + }, + { + "type": "null" + } + ] +} +""" + ) + }; + var secondAction = new OverlayAction + { + Target = "$..[?(@['$ref'] == '#/components/schemas/Foo' && @.anyOf)]['$ref']", + Remove = true, + }; + + var documentJson = +""" +{ + "openapi": "3.0.0", + "info": { + "title": "(title)", + "version": "0.0.0" + }, + "tags": [], + "paths": {}, + "components": { + "schemas": { + "Bar": { + "type": "object", + "required": [ + "foo" + ], + "properties": { + "foo": { + "$ref": "#/components/schemas/Foo" + } + } + }, + "Baz": { + "type": "object", + "required": [ + "foo" + ], + "properties": { + "foo": { + "$ref": "#/components/schemas/Foo" + } + } + }, + "Foo": { + "type": "object" + } + } + } +} +"""; + var jsonNode = JsonNode.Parse(documentJson)!; + var overlayDiagnostic = new OverlayDiagnostic(); + + // When + var result = firstAction.ApplyToDocument(jsonNode, overlayDiagnostic, 0); + + // Then + Assert.True(result); + Assert.Empty(overlayDiagnostic.Errors); + var barFoo = Assert.IsType(jsonNode["components"]?["schemas"]?["Bar"]?["properties"]?["foo"]); + var bazFoo = Assert.IsType(jsonNode["components"]?["schemas"]?["Baz"]?["properties"]?["foo"]); + var barAnyOf = Assert.IsType(barFoo["anyOf"]); + var bazAnyOf = Assert.IsType(bazFoo["anyOf"]); + Assert.Equal(2, barAnyOf.Count); + Assert.Equal(2, bazAnyOf.Count); + Assert.True(JsonNode.DeepEquals(barAnyOf, bazAnyOf)); + Assert.NotNull(barFoo["$ref"]); + Assert.NotNull(bazFoo["$ref"]); + + result = secondAction.ApplyToDocument(jsonNode, overlayDiagnostic, 1); + + Assert.True(result); + Assert.Empty(overlayDiagnostic.Errors); + Assert.Null(barFoo["$ref"]); + Assert.Null(bazFoo["$ref"]); + } + + [Fact] + public void ApplyToDocument_CopyShouldCopySingleSourceToMultipleTargets() + { + // This test validates the new behavior where a single copy source + // is copied to multiple targets + var overlayAction = new OverlayAction + { + Target = "$.paths[*].get.operationId", + Copy = "$.info.title" + }; + var jsonNode = JsonNode.Parse(""" + { + "info": { + "title": "Common Title" + }, + "paths": { + "/users": { + "get": { + "operationId": "oldOperationId1" + } + }, + "/posts": { + "get": { + "operationId": "oldOperationId2" + } + }, + "/comments": { + "get": { + "operationId": "oldOperationId3" + } + } + } + } + """)!; + var overlayDiagnostic = new OverlayDiagnostic(); + + var result = overlayAction.ApplyToDocument(jsonNode, overlayDiagnostic, 0); + + Assert.True(result); + Assert.Empty(overlayDiagnostic.Errors); + // All three targets should now have the same value from the single copy source + Assert.Equal("Common Title", jsonNode["paths"]?["/users"]?["get"]?["operationId"]?.GetValue()); + Assert.Equal("Common Title", jsonNode["paths"]?["/posts"]?["get"]?["operationId"]?.GetValue()); + Assert.Equal("Common Title", jsonNode["paths"]?["/comments"]?["get"]?["operationId"]?.GetValue()); + } + + [Fact] + public void ApplyToDocument_CopyShouldFailWhenCopyMatchesZeroResults() + { + var overlayAction = new OverlayAction + { + Target = "$.info.title", + Copy = "$.paths[*].get.nonexistent" + }; + var jsonNode = JsonNode.Parse(""" + { + "info": { + "title": "Test API" + }, + "paths": { + "/users": { + "get": { + "summary": "Get users" + } + } + } + } + """)!; + var overlayDiagnostic = new OverlayDiagnostic(); + + var result = overlayAction.ApplyToDocument(jsonNode, overlayDiagnostic, 0); + + Assert.False(result); + Assert.Single(overlayDiagnostic.Errors); + Assert.Equal("$.actions[0]", overlayDiagnostic.Errors[0].Pointer); + Assert.Equal("Copy JSON Path '$.paths[*].get.nonexistent' must match exactly one result, but matched 0", overlayDiagnostic.Errors[0].Message); + } + + [Fact] + public void ApplyToDocument_CopyShouldFailWhenCopyMatchesTwoResults() + { + var overlayAction = new OverlayAction + { + Target = "$.info.title", + Copy = "$.paths[*].get.summary" + }; + var jsonNode = JsonNode.Parse(""" + { + "info": { + "title": "Test API" + }, + "paths": { + "/users": { + "get": { + "summary": "Get users" + } + }, + "/posts": { + "get": { + "summary": "Get posts" + } + } + } + } + """)!; + var overlayDiagnostic = new OverlayDiagnostic(); + + var result = overlayAction.ApplyToDocument(jsonNode, overlayDiagnostic, 0); + + Assert.False(result); + Assert.Single(overlayDiagnostic.Errors); + Assert.Equal("$.actions[0]", overlayDiagnostic.Errors[0].Pointer); + Assert.Equal("Copy JSON Path '$.paths[*].get.summary' must match exactly one result, but matched 2", overlayDiagnostic.Errors[0].Message); + } + + [Fact] + public void ApplyToDocument_CopyShouldSucceedWithExactlyOneMatch() + { + var overlayAction = new OverlayAction + { + Target = "$.paths['/users'].get.summary", + Copy = "$.info.description" + }; + var jsonNode = JsonNode.Parse(""" + { + "info": { + "title": "Test API", + "description": "API Description" + }, + "paths": { + "/users": { + "get": { + "summary": "Old summary" + } + } + } + } + """)!; + var overlayDiagnostic = new OverlayDiagnostic(); + + var result = overlayAction.ApplyToDocument(jsonNode, overlayDiagnostic, 0); + + Assert.True(result); + Assert.Empty(overlayDiagnostic.Errors); + Assert.Equal("API Description", jsonNode["paths"]?["/users"]?["get"]?["summary"]?.GetValue()); + } +} \ No newline at end of file diff --git a/tests/lib/Serialization/V1_1/OverlayDocumentTests.cs b/tests/lib/Serialization/V1_1/OverlayDocumentTests.cs new file mode 100644 index 0000000..6523f8c --- /dev/null +++ b/tests/lib/Serialization/V1_1/OverlayDocumentTests.cs @@ -0,0 +1,898 @@ +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading.Tasks; + +using BinkyLabs.OpenApi.Overlays.Reader; +using BinkyLabs.OpenApi.Overlays.Reader.V1_1; + +using Microsoft.OpenApi; + +using ParsingContext = BinkyLabs.OpenApi.Overlays.Reader.ParsingContext; + +namespace BinkyLabs.OpenApi.Overlays.Tests; + +public sealed class OverlayDocumentV1_1Tests : IDisposable +{ + [Fact] + public void SerializeAsV1_1_ShouldWriteCorrectJson() + { + // Arrange + var overlayDocument = new OverlayDocument + { + Info = new OverlayInfo + { + Title = "Test Overlay", + Version = "1.0.0" + }, + Extends = "x-extends", + Actions = + [ + new OverlayAction + { + Target = "Test Target", + Description = "Test Description", + Remove = true + } + ], + Extensions = new Dictionary + { + { "x-custom-extension", new JsonNodeExtension(new JsonObject { { "someProperty", "someValue" } }) } + } + }; + using var textWriter = new StringWriter(); + var writer = new OpenApiJsonWriter(textWriter); + + var expectedJson = """ + { + "overlay": "1.0.0", + "info": { + "title": "Test Overlay", + "version": "1.0.0" + }, + "extends": "x-extends", + "actions": [ + { + "target": "Test Target", + "description": "Test Description", + "remove": true + } + ], + "x-custom-extension": { + "someProperty": "someValue" + } + } + """; + + // Act + overlayDocument.SerializeAsV1_1(writer); + var jsonResult = textWriter.ToString(); + var jsonResultObject = JsonNode.Parse(jsonResult); + var expectedJsonObject = JsonNode.Parse(expectedJson); + + + // Assert + Assert.True(JsonNode.DeepEquals(jsonResultObject, expectedJsonObject), "The serialized JSON does not match the expected JSON."); + } + + [Fact] + public void Deserialize_ShouldSetPropertiesCorrectly() + { + // Arrange + var json = """ + { + "overlay": "1.0.0", + "info": { + "title": "Test Overlay", + "version": "2.0.0" + }, + "extends": "x-extends", + "actions": [ + { + "target": "Test Target", + "description": "Test Description", + "remove": true + },{ + "target": "Test Target 2", + "description": "Test Description 2", + "remove": false + } + ], + "x-custom-extension": { + "someProperty": "someValue" + } + } + """; + var jsonNode = JsonNode.Parse(json)!; + var parsingContext = new ParsingContext(new()); + var parseNode = new MapNode(parsingContext, jsonNode); + + // Act + var overlayDocument = OverlayV1_1Deserializer.LoadDocument(parseNode); + + // Assert + Assert.NotNull(overlayDocument); + Assert.Equal("1.0.0", overlayDocument.Overlay); + Assert.Equal("Test Overlay", overlayDocument.Info?.Title); + Assert.Equal("2.0.0", overlayDocument.Info?.Version); + Assert.Equal("x-extends", overlayDocument.Extends); + Assert.NotNull(overlayDocument.Extensions); + Assert.True(overlayDocument.Extensions!.ContainsKey("x-custom-extension")); + var extensionNodeValue = Assert.IsType(overlayDocument.Extensions["x-custom-extension"]); + var extensionValue = extensionNodeValue.Node; + var someProperty = Assert.IsType(extensionValue["someProperty"], exactMatch: false); + Assert.Equal("someValue", someProperty.GetValue()); + + // Assert the 2 action + Assert.NotNull(overlayDocument.Actions); + Assert.Equal(2, overlayDocument.Actions.Count); + Assert.Equal("Test Target", overlayDocument.Actions[0].Target); + Assert.Equal("Test Description", overlayDocument.Actions[0].Description); + Assert.True(overlayDocument.Actions[0].Remove); + Assert.Equal("Test Target 2", overlayDocument.Actions[1].Target); + Assert.Equal("Test Description 2", overlayDocument.Actions[1].Description); + Assert.False(overlayDocument.Actions[1].Remove); + } + + [Fact] + public void SerializeAsV1_1_WithUpdate_ShouldWriteCorrectJson() + { + // Arrange + var updateNode = JsonNode.Parse(""" + { + "summary": "Updated summary", + "description": "Updated description" + } + """); + + var overlayDocument = new OverlayDocument + { + Info = new OverlayInfo + { + Title = "Test Overlay", + Version = "1.0.0" + }, + Extends = "x-extends", + Actions = + [ + new OverlayAction + { + Target = "Test Target", + Description = "Test Description", + Update = updateNode + } + ] + }; + using var textWriter = new StringWriter(); + var writer = new OpenApiJsonWriter(textWriter); + + + var expectedJson = """ + { + "overlay": "1.0.0", + "info": { + "title": "Test Overlay", + "version": "1.0.0" + }, + "extends": "x-extends", + "actions": [ + { + "target": "Test Target", + "description": "Test Description", + "update": { + "summary": "Updated summary", + "description": "Updated description" + } + } + ] + } + """; + + // Act + overlayDocument.SerializeAsV1_1(writer); + var jsonResult = textWriter.ToString(); + var jsonResultObject = JsonNode.Parse(jsonResult); + var expectedJsonObject = JsonNode.Parse(expectedJson); + + // Assert + Assert.True(JsonNode.DeepEquals(jsonResultObject, expectedJsonObject), "The serialized JSON does not match the expected JSON."); + } + + [Fact] + public void Deserialize_WithUpdate_ShouldSetPropertiesCorrectly() + { + // Arrange + var json = """ + { + "overlay": "1.0.0", + "info": { + "title": "Test Overlay", + "version": "2.0.0" + }, + "extends": "x-extends", + "actions": [ + { + "target": "Test Target", + "description": "Test Description", + "update": { + "summary": "Updated summary", + "description": "Updated description" + } + },{ + "target": "Test Target 2", + "description": "Test Description 2", + "update": ["tag1", "tag2"] + } + ] + } + """; + var jsonNode = JsonNode.Parse(json)!; + var parsingContext = new ParsingContext(new()); + var parseNode = new MapNode(parsingContext, jsonNode); + + // Act + var overlayDocument = OverlayV1_1Deserializer.LoadDocument(parseNode); + + // Assert + Assert.NotNull(overlayDocument); + Assert.Equal("1.0.0", overlayDocument.Overlay); + Assert.Equal("Test Overlay", overlayDocument.Info?.Title); + Assert.Equal("2.0.0", overlayDocument.Info?.Version); + Assert.Equal("x-extends", overlayDocument.Extends); + + // Assert the 2 actions + Assert.NotNull(overlayDocument.Actions); + Assert.Equal(2, overlayDocument.Actions.Count); + + // First action with object update + Assert.Equal("Test Target", overlayDocument.Actions[0].Target); + Assert.Equal("Test Description", overlayDocument.Actions[0].Description); + var updateProperty = overlayDocument.Actions[0].Update; + Assert.NotNull(updateProperty); + var updateObject = updateProperty.AsObject(); + Assert.Equal("Updated summary", updateObject["summary"]?.GetValue()); + Assert.Equal("Updated description", updateObject["description"]?.GetValue()); + + // Second action with array update + Assert.Equal("Test Target 2", overlayDocument.Actions[1].Target); + Assert.Equal("Test Description 2", overlayDocument.Actions[1].Description); + var updatePropertyArray = overlayDocument.Actions[1].Update; + Assert.NotNull(updatePropertyArray); + var updateArray = updatePropertyArray.AsArray(); + Assert.Equal(2, updateArray.Count); + Assert.Equal("tag1", updateArray[0]?.GetValue()); + Assert.Equal("tag2", updateArray[1]?.GetValue()); + } + [Fact] + public void ApplyToDocument_ShouldFailNoNode() + { + var overlayDocument = new OverlayDocument + { + Actions = + [ + new OverlayAction + { + Target = "Test Target", + Description = "Test Description", + Remove = true + } + ] + }; + JsonNode? jsonNode = null; + var overlayDiagnostic = new OverlayDiagnostic(); + Assert.Throws(() => overlayDocument.ApplyToDocument(jsonNode!, overlayDiagnostic)); + } + [Fact] + public void ApplyToDocument_ShouldFailNoDiagnostic() + { + var overlayDocument = new OverlayDocument + { + Actions = + [ + new OverlayAction + { + Target = "Test Target", + Description = "Test Description", + Remove = true + } + ] + }; + var jsonNode = new JsonObject(); + OverlayDiagnostic? overlayDiagnostic = null; + Assert.Throws(() => overlayDocument.ApplyToDocument(jsonNode, overlayDiagnostic!)); + } + [Fact] + public void ApplyToDocument_ShouldApplyTheActions() + { + var overlayDocument = new OverlayDocument + { + Actions = + [ + new OverlayAction + { + Target = "$.info.title", + Description = "Test Description", + Remove = true + } + ] + }; + var jsonNode = new JsonObject + { + ["info"] = new JsonObject + { + ["title"] = "Test Title", + ["version"] = "1.0.0" + } + }; + var overlayDiagnostic = new OverlayDiagnostic(); + var result = overlayDocument.ApplyToDocument(jsonNode, overlayDiagnostic); + Assert.True(result, "ApplyToDocument should return true when actions are applied successfully."); + Assert.Empty(overlayDiagnostic.Errors); + Assert.Null(jsonNode["info"]?["title"]); + } + [Fact] + public async Task ShouldApplyTheOverlayToAnOpenApiDocumentFromYaml() + { + var yamlDocument = + """ + openapi: 3.1.0 + info: + title: Test API + version: 1.0.0 + randomProperty: randomValue + paths: + /test: + get: + summary: Test endpoint + responses: + '200': + description: OK + """; + var documentStream = new MemoryStream(); + using var writer = new StreamWriter(documentStream, leaveOpen: true); + await writer.WriteAsync(yamlDocument); + await writer.FlushAsync(); + documentStream.Seek(0, SeekOrigin.Begin); + var overlayDocument = new OverlayDocument + { + Info = new OverlayInfo + { + Title = "Test Overlay", + Version = "1.0.0" + }, + Actions = + [ + new OverlayAction + { + Target = "$.info.randomProperty", + Description = "Remove randomProperty", + Remove = true + }, + new OverlayAction + { + Target = "$.paths['/test'].get", + Description = "Update summary", + Update = new JsonObject + { + ["summary"] = "Updated summary" + } + } + ] + }; + + var tempUri = new Uri("http://example.com/overlay.yaml"); + var (document, overlayDiagnostic, openApiDiagnostic, result) = await overlayDocument.ApplyToDocumentStreamAndLoadAsync(documentStream, tempUri); + Assert.True(result, "Overlay application should succeed."); + Assert.NotNull(document); + Assert.NotNull(overlayDiagnostic); + Assert.NotNull(openApiDiagnostic); + Assert.Empty(overlayDiagnostic.Errors); + Assert.Empty(openApiDiagnostic.Errors); + Assert.Null(document.Info.Extensions); // Title should be removed + Assert.Equal("Updated summary", document.Paths["/test"]?.Operations?[HttpMethod.Get].Summary); // Summary should be updated + + } + [Fact] + public async Task ShouldApplyTheOverlayToAnOpenApiDocumentFromJson() + { + var json = + """ + { + "openapi": "3.1.0", + "info": { + "title": "Test API", + "version": "1.0.0", + "randomProperty": "randomValue" + }, + "paths": { + "/test": { + "get": { + "summary": "Test endpoint", + "responses": { + "200": { + "description": "OK" + } + } + } + } + } + } + """; + var documentStream = new MemoryStream(); + using var writer = new StreamWriter(documentStream, leaveOpen: true); + await writer.WriteAsync(json); + await writer.FlushAsync(); + documentStream.Seek(0, SeekOrigin.Begin); + var overlayDocument = new OverlayDocument + { + Info = new OverlayInfo + { + Title = "Test Overlay", + Version = "1.0.0" + }, + Actions = + [ + new OverlayAction + { + Target = "$.info.randomProperty", + Description = "Remove randomProperty", + Remove = true + }, + new OverlayAction + { + Target = "$.paths['/test'].get", + Description = "Update summary", + Update = new JsonObject + { + ["summary"] = "Updated summary" + } + } + ] + }; + + var tempUri = new Uri("http://example.com/overlay.yaml"); + var (document, overlayDiagnostic, openApiDiagnostic, result) = await overlayDocument.ApplyToDocumentStreamAndLoadAsync(documentStream, tempUri); + Assert.True(result, "Overlay application should succeed."); + Assert.NotNull(document); + Assert.NotNull(overlayDiagnostic); + Assert.NotNull(openApiDiagnostic); + Assert.Empty(overlayDiagnostic.Errors); + Assert.Empty(openApiDiagnostic.Errors); + Assert.Null(document.Info.Extensions); // Title should be removed + Assert.Equal("Updated summary", document.Paths["/test"]?.Operations?[HttpMethod.Get].Summary); // Summary should be updated + + } + + [Fact] + public async Task Load_WithValidMemoryStream_ReturnsReadResultAsync() + { + // Arrange + var json = """ + { + "openapi": "3.0.0", + "overlay": "1.0.0", + "info": { + "title": "Test Overlay", + "version": "2.0.0" + }, + "extends": "x-extends", + "x-custom-extension": { + "someProperty": "someValue" + }, + "actions": [ + { + "target": "Test Target", + "description": "Test Description", + "update": { + "summary": "Updated summary", + "description": "Updated description" + } + },{ + "target": "Test Target 2", + "description": "Test Description 2", + "update": ["tag1", "tag2"] + } + ] + } + """; + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); + + // Act + var (overlayDocument, _) = await OverlayDocument.LoadFromStreamAsync(stream); + + // Assert + Assert.NotNull(overlayDocument); + Assert.Equal("2.0.0", overlayDocument.Info?.Version); + Assert.Equal("Test Overlay", overlayDocument.Info?.Title); + Assert.Equal("1.0.0", overlayDocument.Overlay); + Assert.Equal("x-extends", overlayDocument.Extends); + Assert.NotNull(overlayDocument.Extensions); + Assert.True(overlayDocument.Extensions.ContainsKey("x-custom-extension")); + var extension = overlayDocument.Extensions["x-custom-extension"]; + var jsonNodeExtension = Assert.IsType(extension); + var node = jsonNodeExtension.Node; + Assert.NotNull(node); + Assert.Equal("someValue", node["someProperty"]?.GetValue()); + + // Assert the 2 actions + Assert.NotNull(overlayDocument.Actions); + Assert.Equal(2, overlayDocument.Actions.Count); + + // First action with object update + Assert.Equal("Test Target", overlayDocument.Actions[0].Target); + Assert.Equal("Test Description", overlayDocument.Actions[0].Description); + var updateProperty = overlayDocument.Actions[0].Update; + Assert.NotNull(updateProperty); + var updateObject = updateProperty.AsObject(); + Assert.Equal("Updated summary", updateObject["summary"]?.GetValue()); + Assert.Equal("Updated description", updateObject["description"]?.GetValue()); + + // Second action with array update + Assert.Equal("Test Target 2", overlayDocument.Actions[1].Target); + Assert.Equal("Test Description 2", overlayDocument.Actions[1].Description); + var updatePropertyArray = overlayDocument.Actions[1].Update; + Assert.NotNull(updatePropertyArray); + var updateArray = updatePropertyArray.AsArray(); + Assert.Equal(2, updateArray.Count); + Assert.Equal("tag1", updateArray[0]?.GetValue()); + Assert.Equal("tag2", updateArray[1]?.GetValue()); + + } + + + [Fact] + public async Task LoadAsync_WithValidFilePath_ReturnsReadResult() + { + // Arrange + var json = """ + { + "openapi": "3.0.0", + "overlay": "1.0.0", + "info": { + "title": "Test Overlay", + "version": "2.0.0" + }, + "extends": "x-extends", + "x-custom-extension": { + "someProperty": "someValue" + }, + "actions": [ + { + "target": "Test Target", + "description": "Test Description", + "update": { + "summary": "Updated summary", + "description": "Updated description" + } + },{ + "target": "Test Target 2", + "description": "Test Description 2", + "update": ["tag1", "tag2"] + } + ] + } + """; + + var tempFile = @"./ValidFile.json"; + await File.WriteAllTextAsync(tempFile, json); + + // Act + var (overlayDocument, _) = await OverlayDocument.LoadFromUrlAsync(tempFile); + + // Assert + Assert.NotNull(overlayDocument); + Assert.Equal("2.0.0", overlayDocument.Info?.Version); + Assert.Equal("Test Overlay", overlayDocument.Info?.Title); + Assert.Equal("1.0.0", overlayDocument.Overlay); + Assert.Equal("x-extends", overlayDocument.Extends); + Assert.NotNull(overlayDocument.Extensions); + Assert.True(overlayDocument.Extensions.ContainsKey("x-custom-extension")); + var extension = overlayDocument.Extensions["x-custom-extension"]; + var jsonNodeExtension = Assert.IsType(extension); + var node = jsonNodeExtension.Node; + Assert.NotNull(node); + Assert.Equal("someValue", node["someProperty"]?.GetValue()); + + // Assert the 2 actions + Assert.NotNull(overlayDocument.Actions); + Assert.Equal(2, overlayDocument.Actions.Count); + + // First action with object update + Assert.Equal("Test Target", overlayDocument.Actions[0].Target); + Assert.Equal("Test Description", overlayDocument.Actions[0].Description); + var updateProperty = overlayDocument.Actions[0].Update; + Assert.NotNull(updateProperty); + var updateObject = updateProperty.AsObject(); + Assert.Equal("Updated summary", updateObject["summary"]?.GetValue()); + Assert.Equal("Updated description", updateObject["description"]?.GetValue()); + + // Second action with array update + Assert.Equal("Test Target 2", overlayDocument.Actions[1].Target); + Assert.Equal("Test Description 2", overlayDocument.Actions[1].Description); + var updatePropertyArray = overlayDocument.Actions[1].Update; + Assert.NotNull(updatePropertyArray); + var updateArray = updatePropertyArray.AsArray(); + Assert.Equal(2, updateArray.Count); + Assert.Equal("tag1", updateArray[0]?.GetValue()); + Assert.Equal("tag2", updateArray[1]?.GetValue()); + } + + [Fact] + public async Task Parse_WithValidJson_ReturnsReadResultJson() + { + // Arrange + var json = """ + { + "overlay": "1.0.0", + "info": { + "title": "Test Overlay", + "version": "1.0.0" + }, + "actions": [ + { + "target": "Test Target", + "description": "Test Description", + "remove": true + } + ] + } + """; + + // Act + var (overlayDocument, _) = await OverlayDocument.ParseAsync(json); + + // Assert + Assert.NotNull(overlayDocument); + Assert.Equal("1.0.0", overlayDocument.Overlay); + Assert.Equal("Test Overlay", overlayDocument.Info?.Title); + Assert.Equal("1.0.0", overlayDocument.Info?.Version); + Assert.NotNull(overlayDocument.Actions); + Assert.Single(overlayDocument.Actions); + Assert.Equal("Test Target", overlayDocument.Actions[0].Target); + Assert.Equal("Test Description", overlayDocument.Actions[0].Description); + Assert.True(overlayDocument.Actions[0].Remove); + } + + [Fact] + public async Task Parse_WithValidJson_ReturnsReadResultYamlAsync() + { + // Arrange + var yaml = """ + overlay: 1.0.0 + info: + title: Test Overlay + version: 1.0.0 + actions: + - target: Test Target + description: Test Description + remove: true + """; + + // Act + var (overlayDocument, _) = await OverlayDocument.ParseAsync(yaml); + + // Assert + Assert.NotNull(overlayDocument); + Assert.Equal("1.0.0", overlayDocument.Overlay); + Assert.Equal("Test Overlay", overlayDocument.Info?.Title); + Assert.Equal("1.0.0", overlayDocument.Info?.Version); + Assert.NotNull(overlayDocument.Actions); + Assert.Single(overlayDocument.Actions); + Assert.Equal("Test Target", overlayDocument.Actions[0].Target); + Assert.Equal("Test Description", overlayDocument.Actions[0].Description); + Assert.True(overlayDocument.Actions[0].Remove); + } + [Fact] + public void CombineWithThrowsOnEmptyInput() + { + var overlayDocument = new OverlayDocument(); + + Assert.Throws(() => overlayDocument.CombineWith(null!)); + } + [Fact] + public void UsesMetadataFromLastDocumentWhenCombiningOverlays() + { + // Given + var overlayDocument1 = new OverlayDocument + { + Info = new OverlayInfo + { + Title = "Overlay 1", + Version = "1.0.0" + }, + Extends = "base.yaml" + }; + var overlayDocument2 = new OverlayDocument + { + Info = new OverlayInfo + { + Title = "Overlay 2", + Version = "1.0.1" + }, + Extends = "base2.yaml" + }; + + // When + var result = overlayDocument1.CombineWith(overlayDocument2); + + // Then + Assert.Equal("Overlay 2", result.Info?.Title); + Assert.Equal("1.0.1", result.Info?.Version); + Assert.Equal("base2.yaml", result.Extends); + Assert.NotNull(result.Actions); + Assert.Empty(result.Actions); + } + [Fact] + public void CombinesActionsInTheRightOrder() + { + // Given + var overlayDocument1 = new OverlayDocument + { + Actions = + [ + new OverlayAction { Target = "Target1", Description = "Description1", Remove = false }, + new OverlayAction { Target = "Target2", Description = "Description2", Remove = true } + ] + }; + var overlayDocument2 = new OverlayDocument + { + Actions = + [ + new OverlayAction { Target = "Target3", Description = "Description3", Remove = false } + ] + }; + + // When + var result = overlayDocument1.CombineWith(overlayDocument2); + + // Then + Assert.NotNull(result.Actions); + Assert.Equal(3, result.Actions.Count); + Assert.Equal("Target1", result.Actions[0].Target); + Assert.Equal("Description1", result.Actions[0].Description); + Assert.False(result.Actions[0].Remove); + Assert.Equal("Target2", result.Actions[1].Target); + Assert.Equal("Description2", result.Actions[1].Description); + Assert.True(result.Actions[1].Remove); + Assert.Equal("Target3", result.Actions[2].Target); + Assert.Equal("Description3", result.Actions[2].Description); + Assert.False(result.Actions[2].Remove); + } + + private readonly string _tempFilePath = Path.ChangeExtension(Path.GetTempFileName(), ".json"); + + [Fact] + public async Task ApplyToDocumentAsync_WithRelativePath_ShouldSucceed() + { + // Arrange + var openApiDocument = """ + { + "openapi": "3.0.1", + "info": { + "title": "Test API", + "version": "1.0.0" + }, + "paths": { + "/test": { + "get": { + "summary": "Original summary", + "responses": { + "200": { + "description": "Success" + } + } + } + } + } + } + """; + + var overlayDocument = new OverlayDocument + { + Info = new OverlayInfo { Title = "Test Overlay", Version = "1.0.0" }, + Actions = new List + { + new OverlayAction + { + Target = "$.paths['/test'].get", + Description = "Update summary", + Update = new JsonObject + { + ["summary"] = "Updated summary" + } + } + } + }; + + // Create a temporary file with a relative path + await File.WriteAllTextAsync(_tempFilePath, openApiDocument); + + // Act + var (document, overlayDiagnostic, openApiDiagnostic, result) = await overlayDocument.ApplyToDocumentAndLoadAsync(_tempFilePath); + + // Assert + Assert.True(result, "Overlay application should succeed."); + Assert.NotNull(document); + Assert.NotNull(overlayDiagnostic); + Assert.NotNull(openApiDiagnostic); + Assert.Empty(overlayDiagnostic.Errors); + Assert.Empty(openApiDiagnostic.Errors); + Assert.Equal("Updated summary", document.Paths["/test"]?.Operations?.Values?.FirstOrDefault()?.Summary); + + } + [Fact] + public async Task ApplyToDocumentAsync_ContinuesToTheNextActionWhenOneFails() + { + // Arrange + var openApiDocument = """ + { + "openapi": "3.0.1", + "info": { + "title": "Test API", + "version": "1.0.0" + }, + "paths": { + "/test": { + "get": { + "summary": "Original summary", + "responses": { + "200": { + "description": "Success" + } + } + } + } + } + } + """; + + // Create a temporary file with a relative path + await File.WriteAllTextAsync(_tempFilePath, openApiDocument); + + var overlayDocument = new OverlayDocument + { + Info = new OverlayInfo { Title = "Test Overlay", Version = "1.0.0" }, + Actions = new List + { + new OverlayAction + { + Target = "$$$$.paths['/nonexistent'].get", + Description = "This action will fail", + Update = new JsonObject + { + ["summary"] = "Should not be applied" + } + }, + new OverlayAction + { + Target = "$.paths['/test'].get", + Description = "Update summary", + Update = new JsonObject + { + ["summary"] = "Updated summary" + } + } + } + }; + + // Act + var (document, overlayDiagnostic, openApiDiagnostic, result) = await overlayDocument.ApplyToDocumentAndLoadAsync(_tempFilePath); + // Assert + Assert.False(result, "Overlay application should fail."); + Assert.NotNull(document); + Assert.NotNull(overlayDiagnostic); + Assert.NotNull(openApiDiagnostic); + Assert.Single(overlayDiagnostic.Errors); + Assert.Empty(openApiDiagnostic.Errors); + Assert.Equal("Updated summary", document.Paths["/test"]?.Operations?.Values?.FirstOrDefault()?.Summary); + } + + public void Dispose() + { + // Cleanup + if (File.Exists(_tempFilePath)) + { + File.Delete(_tempFilePath); + } + } +} \ No newline at end of file diff --git a/tests/lib/Serialization/V1_1/OverlayInfoTests.cs b/tests/lib/Serialization/V1_1/OverlayInfoTests.cs new file mode 100644 index 0000000..036fc44 --- /dev/null +++ b/tests/lib/Serialization/V1_1/OverlayInfoTests.cs @@ -0,0 +1,68 @@ +using System.Text.Json.Nodes; + +using BinkyLabs.OpenApi.Overlays.Reader; +using BinkyLabs.OpenApi.Overlays.Reader.V1_1; + +using Microsoft.OpenApi; +using Microsoft.OpenApi.Reader; + +using ParsingContext = BinkyLabs.OpenApi.Overlays.Reader.ParsingContext; + +namespace BinkyLabs.OpenApi.Overlays.Tests; + +public class OverlayInfoV1_1Tests +{ + [Fact] + public void SerializeAsV1_1_ShouldWriteCorrectJson() + { + // Arrange + var overlayInfo = new OverlayInfo + { + Title = "Test Overlay", + Version = "1.0.0" + }; + using var textWriter = new StringWriter(); + var writer = new OpenApiJsonWriter(textWriter); + + var expectedJson = +""" +{ + "title": "Test Overlay", + "version": "1.0.0" +} +"""; + + // Act + overlayInfo.SerializeAsV1_1(writer); + var jsonResult = textWriter.ToString(); + var jsonResultObject = JsonNode.Parse(jsonResult); + var expectedJsonObject = JsonNode.Parse(expectedJson); + + + // Assert + Assert.True(JsonNode.DeepEquals(jsonResultObject, expectedJsonObject), "The serialized JSON does not match the expected JSON."); + } + + [Fact] + public void Deserialize_ShouldSetPropertiesCorrectly() + { + // Arrange + var json = """ + { + "title": "Test Overlay", + "version": "1.1.0" + } + """; + var jsonNode = JsonNode.Parse(json)!; + var parsingContext = new ParsingContext(new()); + var parseNode = new MapNode(parsingContext, jsonNode); + + + // Act + var overlayInfo = OverlayV1_1Deserializer.LoadInfo(parseNode); + + // Assert + Assert.Equal("Test Overlay", overlayInfo.Title); + Assert.Equal("1.1.0", overlayInfo.Version); + } +} \ No newline at end of file From 97713ce070f3dcc584452b8a5e473e82b1f711c0 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 3 Nov 2025 10:43:12 -0500 Subject: [PATCH 08/26] tests: adds an upgrade unit test Signed-off-by: Vincent Biret --- src/lib/Models/OverlayDocument.cs | 11 +++-- tests/lib/Serialization/V1_1/UpgradeTests.cs | 47 ++++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 tests/lib/Serialization/V1_1/UpgradeTests.cs diff --git a/src/lib/Models/OverlayDocument.cs b/src/lib/Models/OverlayDocument.cs index e540293..df77430 100644 --- a/src/lib/Models/OverlayDocument.cs +++ b/src/lib/Models/OverlayDocument.cs @@ -15,9 +15,9 @@ namespace BinkyLabs.OpenApi.Overlays; public class OverlayDocument : IOverlaySerializable, IOverlayExtensible { /// - /// Gets or sets the overlay version. Default is "1.0.0". + /// Gets or sets the overlay version. Default is "1.1.0". /// - public string? Overlay { get; internal set; } = "1.0.0"; + public string? Overlay { get; internal set; } = "1.1.0"; /// /// Gets or sets the overlay info object. @@ -44,7 +44,7 @@ public class OverlayDocument : IOverlaySerializable, IOverlayExtensible private void SerializeInternal(IOpenApiWriter writer, OverlaySpecVersion version, Action serializeAction) { writer.WriteStartObject(); - writer.WriteRequiredProperty("overlay", Overlay); + writer.WriteRequiredProperty("overlay", SpecVersionToStringMap[version]); if (Info != null) { writer.WriteRequiredObject("info", Info, serializeAction); @@ -57,6 +57,11 @@ private void SerializeInternal(IOpenApiWriter writer, OverlaySpecVersion version writer.WriteOverlayExtensions(Extensions, version); writer.WriteEndObject(); } + private static readonly Dictionary SpecVersionToStringMap = new() + { + { OverlaySpecVersion.Overlay1_0, "1.0.0" }, + { OverlaySpecVersion.Overlay1_1, "1.1.0" }, + }; /// /// Parses a local file path or Url into an Open API document. diff --git a/tests/lib/Serialization/V1_1/UpgradeTests.cs b/tests/lib/Serialization/V1_1/UpgradeTests.cs new file mode 100644 index 0000000..93cb2ef --- /dev/null +++ b/tests/lib/Serialization/V1_1/UpgradeTests.cs @@ -0,0 +1,47 @@ +using System.Text.Json.Nodes; + +using Microsoft.OpenApi; + +namespace BinkyLabs.OpenApi.Overlays.Tests; + +public class UpgradeV1_1Tests +{ + [Fact] + public async Task UpgradesAV1DocumentToV1_1Async() + { + // Given + var inputJson = + """ + { + "overlay": "1.0.0", + "info": { + "title": "Test Overlay", + "version": "1.0.0" + }, + "actions": [ + { + "target": "$.info.title", + "description": "Copy description to title", + "x-copy": "$.info.description" + } + ] + } + """; + // When + var (document, _) = await OverlayDocument.ParseAsync(inputJson); + Assert.NotNull(document); + + using var textWriter = new StringWriter(); + var writer = new OpenApiJsonWriter(textWriter); + document.SerializeAsV1_1(writer); + var jsonResult = textWriter.ToString(); + var jsonResultObject = JsonNode.Parse(jsonResult); + var expectedJsonObject = JsonNode.Parse(inputJson); + expectedJsonObject!["overlay"] = "1.1.0"; + expectedJsonObject!["actions"]![0]!["copy"] = expectedJsonObject["actions"]![0]!["x-copy"]!.DeepClone(); + expectedJsonObject["actions"]![0]!.AsObject().Remove("x-copy"); + + // Then + Assert.True(JsonNode.DeepEquals(jsonResultObject, expectedJsonObject), "The upgraded JSON does not match the expected JSON."); + } +} \ No newline at end of file From 48bb79a936b343dbc6388164e3b579cb88332ea5 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 3 Nov 2025 10:44:10 -0500 Subject: [PATCH 09/26] tests: adds a downgrade scenario Signed-off-by: Vincent Biret --- tests/lib/Serialization/V1_1/UpgradeTests.cs | 40 +++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/tests/lib/Serialization/V1_1/UpgradeTests.cs b/tests/lib/Serialization/V1_1/UpgradeTests.cs index 93cb2ef..e24340e 100644 --- a/tests/lib/Serialization/V1_1/UpgradeTests.cs +++ b/tests/lib/Serialization/V1_1/UpgradeTests.cs @@ -40,8 +40,46 @@ public async Task UpgradesAV1DocumentToV1_1Async() expectedJsonObject!["overlay"] = "1.1.0"; expectedJsonObject!["actions"]![0]!["copy"] = expectedJsonObject["actions"]![0]!["x-copy"]!.DeepClone(); expectedJsonObject["actions"]![0]!.AsObject().Remove("x-copy"); - + // Then Assert.True(JsonNode.DeepEquals(jsonResultObject, expectedJsonObject), "The upgraded JSON does not match the expected JSON."); } + [Fact] + public async Task DowngradesAV1_1DocumentToV1_0Async() + { + // Given + var inputJson = + """ + { + "overlay": "1.1.0", + "info": { + "title": "Test Overlay", + "version": "1.1.0" + }, + "actions": [ + { + "target": "$.info.title", + "description": "Copy description to title", + "copy": "$.info.description" + } + ] + } + """; + // When + var (document, _) = await OverlayDocument.ParseAsync(inputJson); + Assert.NotNull(document); + + using var textWriter = new StringWriter(); + var writer = new OpenApiJsonWriter(textWriter); + document.SerializeAsV1(writer); + var jsonResult = textWriter.ToString(); + var jsonResultObject = JsonNode.Parse(jsonResult); + var expectedJsonObject = JsonNode.Parse(inputJson); + expectedJsonObject!["overlay"] = "1.0.0"; + expectedJsonObject!["actions"]![0]!["x-copy"] = expectedJsonObject["actions"]![0]!["copy"]!.DeepClone(); + expectedJsonObject["actions"]![0]!.AsObject().Remove("copy"); + + // Then + Assert.True(JsonNode.DeepEquals(jsonResultObject, expectedJsonObject), "The downgraded JSON does not match the expected JSON."); + } } \ No newline at end of file From d1eb84deaaedd76e6d101c713182b0c40c6ed63a Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 3 Nov 2025 10:44:31 -0500 Subject: [PATCH 10/26] chore: formatting --- src/lib/Reader/ParseNodes/FixedFieldMap.cs | 6 +++--- tests/lib/Serialization/V1_1/UpgradeTests.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib/Reader/ParseNodes/FixedFieldMap.cs b/src/lib/Reader/ParseNodes/FixedFieldMap.cs index 7b0ae20..047745a 100644 --- a/src/lib/Reader/ParseNodes/FixedFieldMap.cs +++ b/src/lib/Reader/ParseNodes/FixedFieldMap.cs @@ -10,9 +10,9 @@ public FixedFieldMap() : base() } public FixedFieldMap(FixedFieldMap source) : base(source) - { - - } + { + + } public FixedFieldMap(FixedFieldMap source, HashSet except) : base(source.Where(kv => !except.Contains(kv.Key))) { diff --git a/tests/lib/Serialization/V1_1/UpgradeTests.cs b/tests/lib/Serialization/V1_1/UpgradeTests.cs index e24340e..11688d2 100644 --- a/tests/lib/Serialization/V1_1/UpgradeTests.cs +++ b/tests/lib/Serialization/V1_1/UpgradeTests.cs @@ -46,7 +46,7 @@ public async Task UpgradesAV1DocumentToV1_1Async() } [Fact] public async Task DowngradesAV1_1DocumentToV1_0Async() - { + { // Given var inputJson = """ @@ -81,5 +81,5 @@ public async Task DowngradesAV1_1DocumentToV1_0Async() // Then Assert.True(JsonNode.DeepEquals(jsonResultObject, expectedJsonObject), "The downgraded JSON does not match the expected JSON."); - } + } } \ No newline at end of file From 65d2b9a9c7aef8f647baad0528acddbf7a315ac1 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 3 Nov 2025 10:45:49 -0500 Subject: [PATCH 11/26] docs: adds information about v1.1 being supported Signed-off-by: Vincent Biret --- README.md | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 70b90bd..4a23fd3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # OpenAPI Overlay Libraries for dotnet -This project provides a .NET implementation of the [OpenAPI Overlay Specification](https://spec.openapis.org/overlay/latest.html), allowing you to dynamically apply overlays (patches) to existing OpenAPI documents (v3.0+), following the official OpenAPI Overlay 1.0.0 specification. +This project provides a .NET implementation of the [OpenAPI Overlay Specification](https://spec.openapis.org/overlay/latest.html), allowing you to dynamically apply overlays (patches) to existing OpenAPI documents (v3.0+), following the official OpenAPI Overlay 1.0.0 and 1.1.0 specification. The library enables developers to programmatically apply overlays, validate them, and generate updated OpenAPI documents without relying on third-party tools like Swagger. @@ -78,17 +78,7 @@ var jsonResult = textWriter.ToString(); This library implements the following experimental features: -### Copy - -The [copy proposal](https://github.com/OAI/Overlay-Specification/pull/150) to the Overlay specification works similarly to the update action, except it sources its value from another node. This library adds a property under an experimental flag, serializes and deserializes the value, and applies a copy overlay to an OpenAPI document. - -```json -{ - "target": "$.info.title", - "description": "Copy description to title", - "x-copy": "$.info.description" -} -``` +No experimental features are implemented at this moment. ## Release notes From 4b9220245ffebca8591ddfdcd81499cc99c816b7 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 3 Nov 2025 10:53:02 -0500 Subject: [PATCH 12/26] chore: linting --- src/lib/Reader/V1/OverlayActionDeserializer.cs | 10 ++++------ src/lib/Reader/V1/OverlayDocumentDeserializer.cs | 10 ++++------ src/lib/Reader/V1/OverlayInfoDeserializer.cs | 10 ++++------ 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/lib/Reader/V1/OverlayActionDeserializer.cs b/src/lib/Reader/V1/OverlayActionDeserializer.cs index adf932b..23771d9 100644 --- a/src/lib/Reader/V1/OverlayActionDeserializer.cs +++ b/src/lib/Reader/V1/OverlayActionDeserializer.cs @@ -19,13 +19,11 @@ internal static partial class OverlayV1Deserializer { "update", (o, v) => o.Update = v.CreateAny() }, { "x-copy", (o, v) => o.Copy = v.GetScalarValue() }, }; - public static PatternFieldMap GetActionPatternFields(OverlaySpecVersion version) + public static PatternFieldMap GetActionPatternFields(OverlaySpecVersion version) => + new() { - return new PatternFieldMap() - { - {s => s.StartsWith(OverlayConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, k, n) => o.AddExtension(k,LoadExtension(k, n, version))} - }; - } + {s => s.StartsWith(OverlayConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, k, n) => o.AddExtension(k,LoadExtension(k, n, version))} + }; public static readonly PatternFieldMap ActionPatternFields = GetActionPatternFields(OverlaySpecVersion.Overlay1_0); public static OverlayAction LoadAction(ParseNode node) { diff --git a/src/lib/Reader/V1/OverlayDocumentDeserializer.cs b/src/lib/Reader/V1/OverlayDocumentDeserializer.cs index 86c20fc..cfd1bc8 100644 --- a/src/lib/Reader/V1/OverlayDocumentDeserializer.cs +++ b/src/lib/Reader/V1/OverlayDocumentDeserializer.cs @@ -11,13 +11,11 @@ internal static partial class OverlayV1Deserializer { "info", (o, v) => o.Info = LoadInfo(v) }, { "actions", (o, v) => o.Actions = v.CreateList(LoadAction) } }; - public static PatternFieldMap GetDocumentPatternFields(OverlaySpecVersion version) + public static PatternFieldMap GetDocumentPatternFields(OverlaySpecVersion version) => + new() { - return new PatternFieldMap() - { - {s => s.StartsWith(OverlayConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, k, n) => o.AddExtension(k, LoadExtension(k, n, version))} - }; - } + {s => s.StartsWith(OverlayConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, k, n) => o.AddExtension(k, LoadExtension(k, n, version))} + }; public static readonly PatternFieldMap DocumentPatternFields = GetDocumentPatternFields(OverlaySpecVersion.Overlay1_0); public static OverlayDocument LoadOverlayDocument(RootNode rootNode, Uri location) { diff --git a/src/lib/Reader/V1/OverlayInfoDeserializer.cs b/src/lib/Reader/V1/OverlayInfoDeserializer.cs index f603d38..fd68a16 100644 --- a/src/lib/Reader/V1/OverlayInfoDeserializer.cs +++ b/src/lib/Reader/V1/OverlayInfoDeserializer.cs @@ -9,13 +9,11 @@ internal static partial class OverlayV1Deserializer { "title", (o, v) => o.Title = v.GetScalarValue() }, { "version", (o, v) => o.Version = v.GetScalarValue() } }; - public static PatternFieldMap GetInfoPatternFields(OverlaySpecVersion version) + public static PatternFieldMap GetInfoPatternFields(OverlaySpecVersion version) => + new() { - return new PatternFieldMap() - { - {s => s.StartsWith(OverlayConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, k, n) => o.AddExtension(k, LoadExtension(k, n, version))} - }; - } + {s => s.StartsWith(OverlayConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, k, n) => o.AddExtension(k, LoadExtension(k, n, version))} + }; public static readonly PatternFieldMap InfoPatternFields = GetInfoPatternFields(OverlaySpecVersion.Overlay1_0); public static OverlayInfo LoadInfo(ParseNode node) { From 9fe1f3d8fa1181f58c8e05787621e870760c061b Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 3 Nov 2025 10:58:32 -0500 Subject: [PATCH 13/26] chore: linting Signed-off-by: Vincent Biret --- src/lib/Reader/ParseNodes/PatternFieldMap.cs | 8 -------- src/lib/Reader/V1/OverlayV1VersionService.cs | 2 +- src/lib/Reader/V1_1/OverlayActionDeserializer.cs | 2 +- src/lib/Reader/V1_1/OverlayV1_1VersionService.cs | 5 +++-- tests/lib/Serialization/V1_1/OverlayDocumentTests.cs | 4 ++-- 5 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/lib/Reader/ParseNodes/PatternFieldMap.cs b/src/lib/Reader/ParseNodes/PatternFieldMap.cs index f9a1d58..bcb28e4 100644 --- a/src/lib/Reader/ParseNodes/PatternFieldMap.cs +++ b/src/lib/Reader/ParseNodes/PatternFieldMap.cs @@ -8,13 +8,5 @@ namespace BinkyLabs.OpenApi.Overlays.Reader { internal class PatternFieldMap : Dictionary, Action> { - public PatternFieldMap() : base() - { - - } - public PatternFieldMap(PatternFieldMap source) : base(source) - { - - } } } \ No newline at end of file diff --git a/src/lib/Reader/V1/OverlayV1VersionService.cs b/src/lib/Reader/V1/OverlayV1VersionService.cs index fad7399..9046845 100644 --- a/src/lib/Reader/V1/OverlayV1VersionService.cs +++ b/src/lib/Reader/V1/OverlayV1VersionService.cs @@ -19,7 +19,7 @@ public OverlayV1VersionService(OverlayDiagnostic diagnostic) { } - internal static readonly Dictionary> _loaders = new Dictionary> + private static readonly Dictionary> _loaders = new() { [typeof(JsonNodeExtension)] = OverlayV1Deserializer.LoadAny, [typeof(OverlayAction)] = OverlayV1Deserializer.LoadAction, diff --git a/src/lib/Reader/V1_1/OverlayActionDeserializer.cs b/src/lib/Reader/V1_1/OverlayActionDeserializer.cs index cf0bcea..2c0aea6 100644 --- a/src/lib/Reader/V1_1/OverlayActionDeserializer.cs +++ b/src/lib/Reader/V1_1/OverlayActionDeserializer.cs @@ -8,7 +8,7 @@ internal static partial class OverlayV1_1Deserializer { { "copy", (o, v) => o.Copy = v.GetScalarValue() }, }; - public static readonly PatternFieldMap ActionPatternFields = new(OverlayV1Deserializer.GetActionPatternFields(OverlaySpecVersion.Overlay1_1)); + public static readonly PatternFieldMap ActionPatternFields = OverlayV1Deserializer.GetActionPatternFields(OverlaySpecVersion.Overlay1_1); public static OverlayAction LoadAction(ParseNode node) { var mapNode = node.CheckMapNode("Action"); diff --git a/src/lib/Reader/V1_1/OverlayV1_1VersionService.cs b/src/lib/Reader/V1_1/OverlayV1_1VersionService.cs index fb375bd..6035225 100644 --- a/src/lib/Reader/V1_1/OverlayV1_1VersionService.cs +++ b/src/lib/Reader/V1_1/OverlayV1_1VersionService.cs @@ -21,11 +21,12 @@ public OverlayV1_1VersionService(OverlayDiagnostic diagnostic) { } - private readonly Dictionary> _loaders = new Dictionary>(OverlayV1VersionService._loaders) + private readonly Dictionary> _loaders = new() { - [typeof(OverlayInfo)] = OverlayV1_1Deserializer.LoadInfo, + [typeof(JsonNodeExtension)] = OverlayV1Deserializer.LoadAny, [typeof(OverlayAction)] = OverlayV1_1Deserializer.LoadAction, [typeof(OverlayDocument)] = OverlayV1_1Deserializer.LoadDocument, + [typeof(OverlayInfo)] = OverlayV1_1Deserializer.LoadInfo, }; public OverlayDocument LoadDocument(RootNode rootNode, Uri location) diff --git a/tests/lib/Serialization/V1_1/OverlayDocumentTests.cs b/tests/lib/Serialization/V1_1/OverlayDocumentTests.cs index 6523f8c..fa9bd28 100644 --- a/tests/lib/Serialization/V1_1/OverlayDocumentTests.cs +++ b/tests/lib/Serialization/V1_1/OverlayDocumentTests.cs @@ -45,7 +45,7 @@ public void SerializeAsV1_1_ShouldWriteCorrectJson() var expectedJson = """ { - "overlay": "1.0.0", + "overlay": "1.1.0", "info": { "title": "Test Overlay", "version": "1.0.0" @@ -169,7 +169,7 @@ public void SerializeAsV1_1_WithUpdate_ShouldWriteCorrectJson() var expectedJson = """ { - "overlay": "1.0.0", + "overlay": "1.1.0", "info": { "title": "Test Overlay", "version": "1.0.0" From e457b10d0386355a7e87839c533a39f1f11db2a5 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 3 Nov 2025 10:59:58 -0500 Subject: [PATCH 14/26] chore: avoid parsing x copy for v1.1 Signed-off-by: Vincent Biret --- src/lib/Reader/V1_1/OverlayActionDeserializer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/Reader/V1_1/OverlayActionDeserializer.cs b/src/lib/Reader/V1_1/OverlayActionDeserializer.cs index 2c0aea6..85a116c 100644 --- a/src/lib/Reader/V1_1/OverlayActionDeserializer.cs +++ b/src/lib/Reader/V1_1/OverlayActionDeserializer.cs @@ -4,7 +4,7 @@ namespace BinkyLabs.OpenApi.Overlays.Reader.V1_1; internal static partial class OverlayV1_1Deserializer { - public static readonly FixedFieldMap ActionFixedFields = new(OverlayV1Deserializer.ActionFixedFields) + public static readonly FixedFieldMap ActionFixedFields = new(OverlayV1Deserializer.ActionFixedFields, ["x-copy"]) { { "copy", (o, v) => o.Copy = v.GetScalarValue() }, }; From 2368c896fd39312e21393b2d52e51fb5244ef719 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 3 Nov 2025 11:18:42 -0500 Subject: [PATCH 15/26] tests: fixes flaky unit test --- tests/lib/Serialization/V1_1/OverlayDocumentTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/Serialization/V1_1/OverlayDocumentTests.cs b/tests/lib/Serialization/V1_1/OverlayDocumentTests.cs index fa9bd28..21ce0fb 100644 --- a/tests/lib/Serialization/V1_1/OverlayDocumentTests.cs +++ b/tests/lib/Serialization/V1_1/OverlayDocumentTests.cs @@ -573,7 +573,7 @@ public async Task LoadAsync_WithValidFilePath_ReturnsReadResult() } """; - var tempFile = @"./ValidFile.json"; + var tempFile = Path.ChangeExtension(Path.GetTempFileName(), ".json"); await File.WriteAllTextAsync(tempFile, json); // Act From 838b462752ae4973352a8c6caaa6334fd6a0dcf8 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 3 Nov 2025 11:19:23 -0500 Subject: [PATCH 16/26] tests: fixes flaky unit test --- tests/lib/Serialization/V1/OverlayDocumentTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/Serialization/V1/OverlayDocumentTests.cs b/tests/lib/Serialization/V1/OverlayDocumentTests.cs index 3404ea0..bfcca5f 100644 --- a/tests/lib/Serialization/V1/OverlayDocumentTests.cs +++ b/tests/lib/Serialization/V1/OverlayDocumentTests.cs @@ -573,7 +573,7 @@ public async Task LoadAsync_WithValidFilePath_ReturnsReadResult() } """; - var tempFile = @"./ValidFile.json"; + var tempFile = Path.ChangeExtension(Path.GetTempFileName(), ".json"); await File.WriteAllTextAsync(tempFile, json); // Act From 0cb7abafaea9f3f0a58c7f93e5feacd6d3859048 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 3 Nov 2025 11:19:58 -0500 Subject: [PATCH 17/26] chore: refactoring to avoid unecessary parameters and duplicated code --- src/lib/Interfaces/IOverlayReader.cs | 3 +-- src/lib/Interfaces/IOverlayVersionService.cs | 3 +-- src/lib/PublicAPI.Unshipped.txt | 8 ++++---- src/lib/Reader/OverlayJsonReader.cs | 8 ++------ src/lib/Reader/OverlayModelFactory.cs | 19 ++----------------- src/lib/Reader/OverlayYamlReader.cs | 12 ++++-------- src/lib/Reader/ParsingContext.cs | 7 +++---- .../Reader/V1/OverlayActionDeserializer.cs | 5 +++-- .../Reader/V1/OverlayDocumentDeserializer.cs | 15 +++++---------- src/lib/Reader/V1/OverlayInfoDeserializer.cs | 5 +++-- src/lib/Reader/V1/OverlayV1VersionService.cs | 4 ++-- .../Reader/V1_1/OverlayActionDeserializer.cs | 9 +-------- .../V1_1/OverlayDocumentDeserializer.cs | 15 +-------------- .../Reader/V1_1/OverlayInfoDeserializer.cs | 9 +-------- .../Reader/V1_1/OverlayV1_1VersionService.cs | 4 ++-- 15 files changed, 35 insertions(+), 91 deletions(-) diff --git a/src/lib/Interfaces/IOverlayReader.cs b/src/lib/Interfaces/IOverlayReader.cs index a67634d..9fbcfbf 100644 --- a/src/lib/Interfaces/IOverlayReader.cs +++ b/src/lib/Interfaces/IOverlayReader.cs @@ -16,11 +16,10 @@ public interface IOverlayReader /// Async method to read the stream and parse it into an OpenAPI document. /// /// The stream input. - /// Location of where the document that is getting loaded is saved. /// The OpenApi reader settings. /// Propagates notification that an operation should be canceled. /// A task that represents the asynchronous operation, containing the read result. - Task ReadAsync(Stream input, Uri location, OverlayReaderSettings settings, CancellationToken cancellationToken = default); + Task ReadAsync(Stream input, OverlayReaderSettings settings, CancellationToken cancellationToken = default); /// /// Reads the stream and returns a JsonNode representation of the input. diff --git a/src/lib/Interfaces/IOverlayVersionService.cs b/src/lib/Interfaces/IOverlayVersionService.cs index 9c6d49a..2056f7a 100644 --- a/src/lib/Interfaces/IOverlayVersionService.cs +++ b/src/lib/Interfaces/IOverlayVersionService.cs @@ -24,7 +24,6 @@ internal interface IOverlayVersionService /// Converts a generic RootNode instance into a strongly typed OverlayDocument /// /// RootNode containing the information to be converted into an OpenAPI Document - /// Location of where the document that is getting loaded is saved /// Instance of OverlayDocument populated with data from rootNode - OverlayDocument LoadDocument(RootNode rootNode, Uri location); + OverlayDocument LoadDocument(RootNode rootNode); } \ No newline at end of file diff --git a/src/lib/PublicAPI.Unshipped.txt b/src/lib/PublicAPI.Unshipped.txt index 9039b91..b360b73 100644 --- a/src/lib/PublicAPI.Unshipped.txt +++ b/src/lib/PublicAPI.Unshipped.txt @@ -6,7 +6,7 @@ BinkyLabs.OpenApi.Overlays.IOverlayExtension BinkyLabs.OpenApi.Overlays.IOverlayExtension.Write(Microsoft.OpenApi.IOpenApiWriter! writer, BinkyLabs.OpenApi.Overlays.OverlaySpecVersion specVersion) -> void BinkyLabs.OpenApi.Overlays.IOverlayReader BinkyLabs.OpenApi.Overlays.IOverlayReader.GetJsonNodeFromStreamAsync(System.IO.Stream! input, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -BinkyLabs.OpenApi.Overlays.IOverlayReader.ReadAsync(System.IO.Stream! input, System.Uri! location, BinkyLabs.OpenApi.Overlays.OverlayReaderSettings! settings, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +BinkyLabs.OpenApi.Overlays.IOverlayReader.ReadAsync(System.IO.Stream! input, BinkyLabs.OpenApi.Overlays.OverlayReaderSettings! settings, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! BinkyLabs.OpenApi.Overlays.IOverlaySerializable BinkyLabs.OpenApi.Overlays.IOverlaySerializable.SerializeAsV1(Microsoft.OpenApi.IOpenApiWriter! writer) -> void BinkyLabs.OpenApi.Overlays.IOverlaySerializable.SerializeAsV1_1(Microsoft.OpenApi.IOpenApiWriter! writer) -> void @@ -89,7 +89,7 @@ BinkyLabs.OpenApi.Overlays.OverlayInfo.Version.set -> void BinkyLabs.OpenApi.Overlays.OverlayJsonReader BinkyLabs.OpenApi.Overlays.OverlayJsonReader.GetJsonNodeFromStreamAsync(System.IO.Stream! input, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! BinkyLabs.OpenApi.Overlays.OverlayJsonReader.OverlayJsonReader() -> void -BinkyLabs.OpenApi.Overlays.OverlayJsonReader.ReadAsync(System.IO.Stream! input, System.Uri! location, BinkyLabs.OpenApi.Overlays.OverlayReaderSettings! settings, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +BinkyLabs.OpenApi.Overlays.OverlayJsonReader.ReadAsync(System.IO.Stream! input, BinkyLabs.OpenApi.Overlays.OverlayReaderSettings! settings, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! BinkyLabs.OpenApi.Overlays.OverlayReaderException BinkyLabs.OpenApi.Overlays.OverlayReaderException.OverlayReaderException() -> void BinkyLabs.OpenApi.Overlays.OverlayReaderException.OverlayReaderException(string! message) -> void @@ -112,7 +112,7 @@ BinkyLabs.OpenApi.Overlays.OverlaySpecVersion.Overlay1_1 = 2 -> BinkyLabs.OpenAp BinkyLabs.OpenApi.Overlays.OverlayYamlReader BinkyLabs.OpenApi.Overlays.OverlayYamlReader.GetJsonNodeFromStreamAsync(System.IO.Stream! input, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! BinkyLabs.OpenApi.Overlays.OverlayYamlReader.OverlayYamlReader() -> void -BinkyLabs.OpenApi.Overlays.OverlayYamlReader.ReadAsync(System.IO.Stream! input, System.Uri! location, BinkyLabs.OpenApi.Overlays.OverlayReaderSettings! settings, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +BinkyLabs.OpenApi.Overlays.OverlayYamlReader.ReadAsync(System.IO.Stream! input, BinkyLabs.OpenApi.Overlays.OverlayReaderSettings! settings, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! BinkyLabs.OpenApi.Overlays.Reader.OverlayDiagnostic BinkyLabs.OpenApi.Overlays.Reader.OverlayDiagnostic.Errors.get -> System.Collections.Generic.IList! BinkyLabs.OpenApi.Overlays.Reader.OverlayDiagnostic.Errors.set -> void @@ -132,7 +132,7 @@ BinkyLabs.OpenApi.Overlays.Reader.ParsingContext.ExtensionParsers.get -> System. BinkyLabs.OpenApi.Overlays.Reader.ParsingContext.ExtensionParsers.set -> void BinkyLabs.OpenApi.Overlays.Reader.ParsingContext.GetFromTempStorage(string! key, object? scope = null) -> T? BinkyLabs.OpenApi.Overlays.Reader.ParsingContext.GetLocation() -> string! -BinkyLabs.OpenApi.Overlays.Reader.ParsingContext.Parse(System.Text.Json.Nodes.JsonNode! jsonNode, System.Uri! location) -> BinkyLabs.OpenApi.Overlays.OverlayDocument! +BinkyLabs.OpenApi.Overlays.Reader.ParsingContext.Parse(System.Text.Json.Nodes.JsonNode! jsonNode) -> BinkyLabs.OpenApi.Overlays.OverlayDocument! BinkyLabs.OpenApi.Overlays.Reader.ParsingContext.ParseFragment(System.Text.Json.Nodes.JsonNode! jsonNode, BinkyLabs.OpenApi.Overlays.OverlaySpecVersion version) -> T? BinkyLabs.OpenApi.Overlays.Reader.ParsingContext.ParsingContext(BinkyLabs.OpenApi.Overlays.Reader.OverlayDiagnostic! diagnostic) -> void BinkyLabs.OpenApi.Overlays.Reader.ParsingContext.PopLoop(string! loopid) -> void diff --git a/src/lib/Reader/OverlayJsonReader.cs b/src/lib/Reader/OverlayJsonReader.cs index 851c14f..9d2e442 100644 --- a/src/lib/Reader/OverlayJsonReader.cs +++ b/src/lib/Reader/OverlayJsonReader.cs @@ -19,11 +19,9 @@ public class OverlayJsonReader : IOverlayReader /// Parses the JsonNode input into an Open API document. /// /// The JsonNode input. - /// Location of where the document that is getting loaded is saved /// The Reader settings to be used during parsing. /// internal ReadResult Read(JsonNode jsonNode, - Uri location, OverlayReaderSettings settings) { ArgumentNullException.ThrowIfNull(jsonNode); @@ -41,7 +39,7 @@ internal ReadResult Read(JsonNode jsonNode, try { // Parse the OpenAPI Document - document = context.Parse(jsonNode, location); + document = context.Parse(jsonNode); } catch (OpenApiException ex) { @@ -76,12 +74,10 @@ internal ReadResult Read(JsonNode jsonNode, /// Reads the stream input asynchronously and parses it into an Open API document. /// /// Memory stream containing OpenAPI description to parse. - /// Location of where the document that is getting loaded is saved /// The Reader settings to be used during parsing. /// Propagates notifications that operations should be cancelled. /// public async Task ReadAsync(Stream input, - Uri location, OverlayReaderSettings settings, CancellationToken cancellationToken = default) { @@ -107,7 +103,7 @@ public async Task ReadAsync(Stream input, }; } - return Read(jsonNode, location, settings); + return Read(jsonNode, settings); } /// diff --git a/src/lib/Reader/OverlayModelFactory.cs b/src/lib/Reader/OverlayModelFactory.cs index a4cddfa..7428ad4 100644 --- a/src/lib/Reader/OverlayModelFactory.cs +++ b/src/lib/Reader/OverlayModelFactory.cs @@ -91,27 +91,12 @@ public static async Task ParseAsync(string input, private static readonly Lazy DefaultReaderSettings = new(() => new OverlayReaderSettings()); - private static async Task InternalLoadAsync(Stream input, string format, OverlayReaderSettings settings, CancellationToken cancellationToken = default) + private static Task InternalLoadAsync(Stream input, string format, OverlayReaderSettings settings, CancellationToken cancellationToken = default) { settings ??= DefaultReaderSettings.Value; var reader = settings.GetReader(format); - // Handle URI creation more safely for file paths - Uri location; - if (input is FileStream fileStream) - { - // Convert to absolute path and then create a file URI to handle relative paths correctly - var absolutePath = Path.GetFullPath(fileStream.Name); - location = new Uri(absolutePath, UriKind.Absolute); - } - else - { - location = new Uri(OpenApiConstants.BaseRegistryUri); - } - - var readResult = await reader.ReadAsync(input, location, settings, cancellationToken).ConfigureAwait(false); - - return readResult; + return reader.ReadAsync(input, settings, cancellationToken); } private static async Task<(Stream, string?)> RetrieveStreamAndFormatAsync(string url, OverlayReaderSettings settings, CancellationToken token = default) diff --git a/src/lib/Reader/OverlayYamlReader.cs b/src/lib/Reader/OverlayYamlReader.cs index 2779b7f..3233abd 100644 --- a/src/lib/Reader/OverlayYamlReader.cs +++ b/src/lib/Reader/OverlayYamlReader.cs @@ -19,7 +19,6 @@ public class OverlayYamlReader : IOverlayReader private const int copyBufferSize = 4096; private static readonly OverlayJsonReader _jsonReader = new(); private ReadResult Read(MemoryStream input, - Uri location, OverlayReaderSettings settings) { ArgumentNullException.ThrowIfNull(input); @@ -49,40 +48,37 @@ private ReadResult Read(MemoryStream input, }; } - return Read(jsonNode, location, settings); + return Read(jsonNode, settings); } private ReadResult Read(JsonNode jsonNode, - Uri location, OverlayReaderSettings settings) { - return _jsonReader.Read(jsonNode, location, settings); + return _jsonReader.Read(jsonNode, settings); } /// /// Reads the stream input asynchronously and parses it into an Open API document. /// /// Memory stream containing OpenAPI description to parse. - /// Location of where the document that is getting loaded is saved /// The Reader settings to be used during parsing. /// Propagates notifications that operations should be cancelled. /// public async Task ReadAsync(Stream input, - Uri location, OverlayReaderSettings settings, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(input); if (input is MemoryStream memoryStream) { - return Read(memoryStream, location, settings); + return Read(memoryStream, settings); } else { using var preparedStream = new MemoryStream(); await input.CopyToAsync(preparedStream, copyBufferSize, cancellationToken).ConfigureAwait(false); preparedStream.Position = 0; - return Read(preparedStream, location, settings); + return Read(preparedStream, settings); } } diff --git a/src/lib/Reader/ParsingContext.cs b/src/lib/Reader/ParsingContext.cs index 7b59dca..efc698f 100644 --- a/src/lib/Reader/ParsingContext.cs +++ b/src/lib/Reader/ParsingContext.cs @@ -57,9 +57,8 @@ public ParsingContext(OverlayDiagnostic diagnostic) /// Initiates the parsing process. Not thread safe and should only be called once on a parsing context /// /// Set of Json nodes to parse. - /// Location of where the document that is getting loaded is saved /// An OverlayDocument populated based on the passed yamlDocument - public OverlayDocument Parse(JsonNode jsonNode, Uri location) + public OverlayDocument Parse(JsonNode jsonNode) { RootNode = new RootNode(this, jsonNode); @@ -71,13 +70,13 @@ public OverlayDocument Parse(JsonNode jsonNode, Uri location) { case string version when OverlayV1Version.Equals(version, StringComparison.OrdinalIgnoreCase): VersionService = new OverlayV1VersionService(Diagnostic); - doc = VersionService.LoadDocument(RootNode, location); + doc = VersionService.LoadDocument(RootNode); this.Diagnostic.SpecificationVersion = OverlaySpecVersion.Overlay1_0; ValidateRequiredFields(doc, version); break; case string version when OverlayV1_1Version.Equals(version, StringComparison.OrdinalIgnoreCase): VersionService = new OverlayV1_1VersionService(Diagnostic); - doc = VersionService.LoadDocument(RootNode, location); + doc = VersionService.LoadDocument(RootNode); this.Diagnostic.SpecificationVersion = OverlaySpecVersion.Overlay1_1; ValidateRequiredFields(doc, version); break; diff --git a/src/lib/Reader/V1/OverlayActionDeserializer.cs b/src/lib/Reader/V1/OverlayActionDeserializer.cs index 23771d9..9a30aaf 100644 --- a/src/lib/Reader/V1/OverlayActionDeserializer.cs +++ b/src/lib/Reader/V1/OverlayActionDeserializer.cs @@ -25,11 +25,12 @@ public static PatternFieldMap GetActionPatternFields(OverlaySpecV {s => s.StartsWith(OverlayConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, k, n) => o.AddExtension(k,LoadExtension(k, n, version))} }; public static readonly PatternFieldMap ActionPatternFields = GetActionPatternFields(OverlaySpecVersion.Overlay1_0); - public static OverlayAction LoadAction(ParseNode node) + public static OverlayAction LoadAction(ParseNode node) => LoadActionInternal(node, ActionFixedFields, ActionPatternFields); + public static OverlayAction LoadActionInternal(ParseNode node, FixedFieldMap actionFixedFields, PatternFieldMap actionPatternFields) { var mapNode = node.CheckMapNode("Action"); var action = new OverlayAction(); - ParseMap(mapNode, action, ActionFixedFields, ActionPatternFields); + ParseMap(mapNode, action, actionFixedFields, actionPatternFields); return action; } diff --git a/src/lib/Reader/V1/OverlayDocumentDeserializer.cs b/src/lib/Reader/V1/OverlayDocumentDeserializer.cs index cfd1bc8..7c4885c 100644 --- a/src/lib/Reader/V1/OverlayDocumentDeserializer.cs +++ b/src/lib/Reader/V1/OverlayDocumentDeserializer.cs @@ -17,18 +17,13 @@ public static PatternFieldMap GetDocumentPatternFields(OverlayS {s => s.StartsWith(OverlayConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, k, n) => o.AddExtension(k, LoadExtension(k, n, version))} }; public static readonly PatternFieldMap DocumentPatternFields = GetDocumentPatternFields(OverlaySpecVersion.Overlay1_0); - public static OverlayDocument LoadOverlayDocument(RootNode rootNode, Uri location) - { - var document = new OverlayDocument(); - ParseMap(rootNode.GetMap(), document, DocumentFixedFields, DocumentPatternFields); - return document; - } - public static OverlayDocument LoadDocument(ParseNode node) + public static OverlayDocument LoadDocument(ParseNode node) => LoadDocumentInternal(node, DocumentFixedFields, DocumentPatternFields); + public static OverlayDocument LoadDocumentInternal(ParseNode node, FixedFieldMap documentFixedFields, PatternFieldMap documentPatternFields) { var mapNode = node.CheckMapNode("Document"); - var info = new OverlayDocument(); - ParseMap(mapNode, info, DocumentFixedFields, DocumentPatternFields); + var document = new OverlayDocument(); + ParseMap(mapNode, document, documentFixedFields, documentPatternFields); - return info; + return document; } } \ No newline at end of file diff --git a/src/lib/Reader/V1/OverlayInfoDeserializer.cs b/src/lib/Reader/V1/OverlayInfoDeserializer.cs index fd68a16..a531503 100644 --- a/src/lib/Reader/V1/OverlayInfoDeserializer.cs +++ b/src/lib/Reader/V1/OverlayInfoDeserializer.cs @@ -15,11 +15,12 @@ public static PatternFieldMap GetInfoPatternFields(OverlaySpecVersi {s => s.StartsWith(OverlayConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, k, n) => o.AddExtension(k, LoadExtension(k, n, version))} }; public static readonly PatternFieldMap InfoPatternFields = GetInfoPatternFields(OverlaySpecVersion.Overlay1_0); - public static OverlayInfo LoadInfo(ParseNode node) + public static OverlayInfo LoadInfo(ParseNode node) => LoadInfoInternal(node, InfoFixedFields, InfoPatternFields); + public static OverlayInfo LoadInfoInternal(ParseNode node, FixedFieldMap infoFixedFields, PatternFieldMap infoPatternFields) { var mapNode = node.CheckMapNode("Info"); var info = new OverlayInfo(); - ParseMap(mapNode, info, InfoFixedFields, InfoPatternFields); + ParseMap(mapNode, info, infoFixedFields, infoPatternFields); return info; } diff --git a/src/lib/Reader/V1/OverlayV1VersionService.cs b/src/lib/Reader/V1/OverlayV1VersionService.cs index 9046845..5906c2e 100644 --- a/src/lib/Reader/V1/OverlayV1VersionService.cs +++ b/src/lib/Reader/V1/OverlayV1VersionService.cs @@ -27,9 +27,9 @@ public OverlayV1VersionService(OverlayDiagnostic diagnostic) [typeof(OverlayInfo)] = OverlayV1Deserializer.LoadInfo, }; - public OverlayDocument LoadDocument(RootNode rootNode, Uri location) + public OverlayDocument LoadDocument(RootNode rootNode) { - return OverlayV1Deserializer.LoadOverlayDocument(rootNode, location); + return OverlayV1Deserializer.LoadDocument(rootNode.GetMap()); } public T? LoadElement(ParseNode node) where T : IOpenApiElement diff --git a/src/lib/Reader/V1_1/OverlayActionDeserializer.cs b/src/lib/Reader/V1_1/OverlayActionDeserializer.cs index 85a116c..9ec8107 100644 --- a/src/lib/Reader/V1_1/OverlayActionDeserializer.cs +++ b/src/lib/Reader/V1_1/OverlayActionDeserializer.cs @@ -9,12 +9,5 @@ internal static partial class OverlayV1_1Deserializer { "copy", (o, v) => o.Copy = v.GetScalarValue() }, }; public static readonly PatternFieldMap ActionPatternFields = OverlayV1Deserializer.GetActionPatternFields(OverlaySpecVersion.Overlay1_1); - public static OverlayAction LoadAction(ParseNode node) - { - var mapNode = node.CheckMapNode("Action"); - var action = new OverlayAction(); - ParseMap(mapNode, action, ActionFixedFields, ActionPatternFields); - - return action; - } + public static OverlayAction LoadAction(ParseNode node) => OverlayV1Deserializer.LoadActionInternal(node, ActionFixedFields, ActionPatternFields); } \ No newline at end of file diff --git a/src/lib/Reader/V1_1/OverlayDocumentDeserializer.cs b/src/lib/Reader/V1_1/OverlayDocumentDeserializer.cs index c4f2e94..f74c288 100644 --- a/src/lib/Reader/V1_1/OverlayDocumentDeserializer.cs +++ b/src/lib/Reader/V1_1/OverlayDocumentDeserializer.cs @@ -10,18 +10,5 @@ internal static partial class OverlayV1_1Deserializer { "actions", (o, v) => o.Actions = v.CreateList(LoadAction) } }; public static readonly PatternFieldMap DocumentPatternFields = OverlayV1Deserializer.GetDocumentPatternFields(OverlaySpecVersion.Overlay1_1); - public static OverlayDocument LoadOverlayDocument(RootNode rootNode, Uri location) - { - var document = new OverlayDocument(); - ParseMap(rootNode.GetMap(), document, DocumentFixedFields, DocumentPatternFields); - return document; - } - public static OverlayDocument LoadDocument(ParseNode node) - { - var mapNode = node.CheckMapNode("Document"); - var info = new OverlayDocument(); - ParseMap(mapNode, info, DocumentFixedFields, DocumentPatternFields); - - return info; - } + public static OverlayDocument LoadDocument(ParseNode node) => OverlayV1Deserializer.LoadDocumentInternal(node, DocumentFixedFields, DocumentPatternFields); } \ No newline at end of file diff --git a/src/lib/Reader/V1_1/OverlayInfoDeserializer.cs b/src/lib/Reader/V1_1/OverlayInfoDeserializer.cs index 522d11a..be7c33e 100644 --- a/src/lib/Reader/V1_1/OverlayInfoDeserializer.cs +++ b/src/lib/Reader/V1_1/OverlayInfoDeserializer.cs @@ -8,12 +8,5 @@ internal static partial class OverlayV1_1Deserializer { public static readonly FixedFieldMap InfoFixedFields = new(OverlayV1Deserializer.InfoFixedFields); public static readonly PatternFieldMap InfoPatternFields = OverlayV1Deserializer.GetInfoPatternFields(OverlaySpecVersion.Overlay1_1); - public static OverlayInfo LoadInfo(ParseNode node) - { - var mapNode = node.CheckMapNode("Info"); - var info = new OverlayInfo(); - ParseMap(mapNode, info, InfoFixedFields, InfoPatternFields); - - return info; - } + public static OverlayInfo LoadInfo(ParseNode node) => OverlayV1Deserializer.LoadInfoInternal(node, InfoFixedFields, InfoPatternFields); } \ No newline at end of file diff --git a/src/lib/Reader/V1_1/OverlayV1_1VersionService.cs b/src/lib/Reader/V1_1/OverlayV1_1VersionService.cs index 6035225..f113b0b 100644 --- a/src/lib/Reader/V1_1/OverlayV1_1VersionService.cs +++ b/src/lib/Reader/V1_1/OverlayV1_1VersionService.cs @@ -29,9 +29,9 @@ public OverlayV1_1VersionService(OverlayDiagnostic diagnostic) [typeof(OverlayInfo)] = OverlayV1_1Deserializer.LoadInfo, }; - public OverlayDocument LoadDocument(RootNode rootNode, Uri location) + public OverlayDocument LoadDocument(RootNode rootNode) { - return OverlayV1_1Deserializer.LoadOverlayDocument(rootNode, location); + return OverlayV1_1Deserializer.LoadDocument(rootNode.GetMap()); } public T? LoadElement(ParseNode node) where T : IOpenApiElement From ec487c64dd19ecb57d6db2f28ee6a55cb4b0880c Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 3 Nov 2025 11:22:05 -0500 Subject: [PATCH 18/26] chore: removes unnecessary file Signed-off-by: Vincent Biret --- src/lib/Reader/V1_1/OverlayV1_1Deserializer.cs | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 src/lib/Reader/V1_1/OverlayV1_1Deserializer.cs diff --git a/src/lib/Reader/V1_1/OverlayV1_1Deserializer.cs b/src/lib/Reader/V1_1/OverlayV1_1Deserializer.cs deleted file mode 100644 index af4f8e3..0000000 --- a/src/lib/Reader/V1_1/OverlayV1_1Deserializer.cs +++ /dev/null @@ -1,12 +0,0 @@ -using BinkyLabs.OpenApi.Overlays.Reader.V1; - -namespace BinkyLabs.OpenApi.Overlays.Reader.V1_1; - -internal static partial class OverlayV1_1Deserializer -{ - private static void ParseMap( - MapNode? mapNode, - T domainObject, - FixedFieldMap fixedFieldMap, - PatternFieldMap patternFieldMap) => OverlayV1Deserializer.ParseMap(mapNode, domainObject, fixedFieldMap, patternFieldMap); -} \ No newline at end of file From a501a302980d436183e808eb3bbb1f8b427510d7 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 3 Nov 2025 11:24:17 -0500 Subject: [PATCH 19/26] chore: removes unnecessary parameter Signed-off-by: Vincent Biret --- src/lib/Reader/ParsingContext.cs | 8 ++++---- src/lib/Reader/V1/OverlayV1VersionService.cs | 6 +----- src/lib/Reader/V1_1/OverlayV1_1VersionService.cs | 6 +----- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/lib/Reader/ParsingContext.cs b/src/lib/Reader/ParsingContext.cs index efc698f..aeaaaeb 100644 --- a/src/lib/Reader/ParsingContext.cs +++ b/src/lib/Reader/ParsingContext.cs @@ -69,13 +69,13 @@ public OverlayDocument Parse(JsonNode jsonNode) switch (inputVersion) { case string version when OverlayV1Version.Equals(version, StringComparison.OrdinalIgnoreCase): - VersionService = new OverlayV1VersionService(Diagnostic); + VersionService = new OverlayV1VersionService(); doc = VersionService.LoadDocument(RootNode); this.Diagnostic.SpecificationVersion = OverlaySpecVersion.Overlay1_0; ValidateRequiredFields(doc, version); break; case string version when OverlayV1_1Version.Equals(version, StringComparison.OrdinalIgnoreCase): - VersionService = new OverlayV1_1VersionService(Diagnostic); + VersionService = new OverlayV1_1VersionService(); doc = VersionService.LoadDocument(RootNode); this.Diagnostic.SpecificationVersion = OverlaySpecVersion.Overlay1_1; ValidateRequiredFields(doc, version); @@ -103,11 +103,11 @@ public OverlayDocument Parse(JsonNode jsonNode) switch (version) { case OverlaySpecVersion.Overlay1_0: - VersionService = new OverlayV1VersionService(Diagnostic); + VersionService = new OverlayV1VersionService(); element = this.VersionService.LoadElement(node); break; case OverlaySpecVersion.Overlay1_1: - VersionService = new OverlayV1_1VersionService(Diagnostic); + VersionService = new OverlayV1_1VersionService(); element = this.VersionService.LoadElement(node); break; default: diff --git a/src/lib/Reader/V1/OverlayV1VersionService.cs b/src/lib/Reader/V1/OverlayV1VersionService.cs index 5906c2e..9578625 100644 --- a/src/lib/Reader/V1/OverlayV1VersionService.cs +++ b/src/lib/Reader/V1/OverlayV1VersionService.cs @@ -11,11 +11,7 @@ namespace BinkyLabs.OpenApi.Overlays.Reader.V1; internal class OverlayV1VersionService : IOverlayVersionService { - /// - /// Create Parsing Context - /// - /// Provide instance for diagnostic object for collecting and accessing information about the parsing. - public OverlayV1VersionService(OverlayDiagnostic diagnostic) + public OverlayV1VersionService() { } diff --git a/src/lib/Reader/V1_1/OverlayV1_1VersionService.cs b/src/lib/Reader/V1_1/OverlayV1_1VersionService.cs index f113b0b..e3df308 100644 --- a/src/lib/Reader/V1_1/OverlayV1_1VersionService.cs +++ b/src/lib/Reader/V1_1/OverlayV1_1VersionService.cs @@ -13,11 +13,7 @@ namespace BinkyLabs.OpenApi.Overlays.Reader.V1_1; internal class OverlayV1_1VersionService : IOverlayVersionService { - /// - /// Create Parsing Context - /// - /// Provide instance for diagnostic object for collecting and accessing information about the parsing. - public OverlayV1_1VersionService(OverlayDiagnostic diagnostic) + public OverlayV1_1VersionService() { } From e3cf5ca119b65d4e7b1c2f802fc002739634903e Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 3 Nov 2025 11:32:07 -0500 Subject: [PATCH 20/26] chore: refactoring to avoid duplication Signed-off-by: Vincent Biret --- src/lib/Reader/BaseOverlayVersionService.cs | 38 +++++++++++++++++++ src/lib/Reader/V1/OverlayV1VersionService.cs | 22 ++--------- .../Reader/V1_1/OverlayV1_1VersionService.cs | 22 ++--------- 3 files changed, 46 insertions(+), 36 deletions(-) create mode 100644 src/lib/Reader/BaseOverlayVersionService.cs diff --git a/src/lib/Reader/BaseOverlayVersionService.cs b/src/lib/Reader/BaseOverlayVersionService.cs new file mode 100644 index 0000000..d3a3b78 --- /dev/null +++ b/src/lib/Reader/BaseOverlayVersionService.cs @@ -0,0 +1,38 @@ +// Licensed under the MIT license. + +using Microsoft.OpenApi; + +namespace BinkyLabs.OpenApi.Overlays.Reader; + +/// +/// Base class for overlay version services providing common functionality. +/// +internal abstract class BaseOverlayVersionService : IOverlayVersionService +{ + /// + /// Dictionary of type loaders for different overlay elements. + /// + protected abstract Dictionary> Loaders { get; } + + /// + /// Loads an OpenAPI Element from a document fragment + /// + /// Type of element to load + /// document fragment node + /// Instance of OpenAPIElement + public T? LoadElement(ParseNode node) where T : IOpenApiElement + { + if (Loaders.TryGetValue(typeof(T), out var loader) && loader(node) is T result) + { + return result; + } + return default; + } + + /// + /// Converts a generic RootNode instance into a strongly typed OverlayDocument + /// + /// RootNode containing the information to be converted into an OpenAPI Document + /// Instance of OverlayDocument populated with data from rootNode + public abstract OverlayDocument LoadDocument(RootNode rootNode); +} \ No newline at end of file diff --git a/src/lib/Reader/V1/OverlayV1VersionService.cs b/src/lib/Reader/V1/OverlayV1VersionService.cs index 9578625..0aeb1e8 100644 --- a/src/lib/Reader/V1/OverlayV1VersionService.cs +++ b/src/lib/Reader/V1/OverlayV1VersionService.cs @@ -8,13 +8,8 @@ namespace BinkyLabs.OpenApi.Overlays.Reader.V1; /// /// The version service for the Overlay 1.0 specification. /// -internal class OverlayV1VersionService : IOverlayVersionService +internal class OverlayV1VersionService : BaseOverlayVersionService { - - public OverlayV1VersionService() - { - } - private static readonly Dictionary> _loaders = new() { [typeof(JsonNodeExtension)] = OverlayV1Deserializer.LoadAny, @@ -23,19 +18,10 @@ public OverlayV1VersionService() [typeof(OverlayInfo)] = OverlayV1Deserializer.LoadInfo, }; - public OverlayDocument LoadDocument(RootNode rootNode) - { - return OverlayV1Deserializer.LoadDocument(rootNode.GetMap()); - } + protected override Dictionary> Loaders => _loaders; - public T? LoadElement(ParseNode node) where T : IOpenApiElement + public override OverlayDocument LoadDocument(RootNode rootNode) { - if (Loaders.TryGetValue(typeof(T), out var loader) && loader(node) is T result) - { - return result; - } - return default; + return OverlayV1Deserializer.LoadDocument(rootNode.GetMap()); } - - internal Dictionary> Loaders => _loaders; } \ No newline at end of file diff --git a/src/lib/Reader/V1_1/OverlayV1_1VersionService.cs b/src/lib/Reader/V1_1/OverlayV1_1VersionService.cs index e3df308..11f1d87 100644 --- a/src/lib/Reader/V1_1/OverlayV1_1VersionService.cs +++ b/src/lib/Reader/V1_1/OverlayV1_1VersionService.cs @@ -10,13 +10,8 @@ namespace BinkyLabs.OpenApi.Overlays.Reader.V1_1; /// /// The version service for the Overlay 1.1 specification. /// -internal class OverlayV1_1VersionService : IOverlayVersionService +internal class OverlayV1_1VersionService : BaseOverlayVersionService { - - public OverlayV1_1VersionService() - { - } - private readonly Dictionary> _loaders = new() { [typeof(JsonNodeExtension)] = OverlayV1Deserializer.LoadAny, @@ -25,19 +20,10 @@ public OverlayV1_1VersionService() [typeof(OverlayInfo)] = OverlayV1_1Deserializer.LoadInfo, }; - public OverlayDocument LoadDocument(RootNode rootNode) - { - return OverlayV1_1Deserializer.LoadDocument(rootNode.GetMap()); - } + protected override Dictionary> Loaders => _loaders; - public T? LoadElement(ParseNode node) where T : IOpenApiElement + public override OverlayDocument LoadDocument(RootNode rootNode) { - if (Loaders.TryGetValue(typeof(T), out var loader) && loader(node) is T result) - { - return result; - } - return default; + return OverlayV1_1Deserializer.LoadDocument(rootNode.GetMap()); } - - internal Dictionary> Loaders => _loaders; } \ No newline at end of file From e4aa1ad37a39eb1d19210fc7cf8e5ffd1fe481af Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 3 Nov 2025 11:32:27 -0500 Subject: [PATCH 21/26] chore: linting Signed-off-by: Vincent Biret --- src/lib/Reader/V1_1/OverlayV1_1VersionService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/Reader/V1_1/OverlayV1_1VersionService.cs b/src/lib/Reader/V1_1/OverlayV1_1VersionService.cs index 11f1d87..6237512 100644 --- a/src/lib/Reader/V1_1/OverlayV1_1VersionService.cs +++ b/src/lib/Reader/V1_1/OverlayV1_1VersionService.cs @@ -12,7 +12,7 @@ namespace BinkyLabs.OpenApi.Overlays.Reader.V1_1; /// internal class OverlayV1_1VersionService : BaseOverlayVersionService { - private readonly Dictionary> _loaders = new() + private static readonly Dictionary> _loaders = new() { [typeof(JsonNodeExtension)] = OverlayV1Deserializer.LoadAny, [typeof(OverlayAction)] = OverlayV1_1Deserializer.LoadAction, From d7685b793769b46771027bb7567d335a38b02f2a Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 5 Nov 2025 14:00:01 -0500 Subject: [PATCH 22/26] tests: removes duplicated tests after merge Signed-off-by: Vincent Biret --- .../V1_1/OverlayDocumentTests.cs | 337 ------------------ 1 file changed, 337 deletions(-) diff --git a/tests/lib/Serialization/V1_1/OverlayDocumentTests.cs b/tests/lib/Serialization/V1_1/OverlayDocumentTests.cs index 21ce0fb..efe0ff7 100644 --- a/tests/lib/Serialization/V1_1/OverlayDocumentTests.cs +++ b/tests/lib/Serialization/V1_1/OverlayDocumentTests.cs @@ -263,206 +263,6 @@ public void Deserialize_WithUpdate_ShouldSetPropertiesCorrectly() Assert.Equal("tag1", updateArray[0]?.GetValue()); Assert.Equal("tag2", updateArray[1]?.GetValue()); } - [Fact] - public void ApplyToDocument_ShouldFailNoNode() - { - var overlayDocument = new OverlayDocument - { - Actions = - [ - new OverlayAction - { - Target = "Test Target", - Description = "Test Description", - Remove = true - } - ] - }; - JsonNode? jsonNode = null; - var overlayDiagnostic = new OverlayDiagnostic(); - Assert.Throws(() => overlayDocument.ApplyToDocument(jsonNode!, overlayDiagnostic)); - } - [Fact] - public void ApplyToDocument_ShouldFailNoDiagnostic() - { - var overlayDocument = new OverlayDocument - { - Actions = - [ - new OverlayAction - { - Target = "Test Target", - Description = "Test Description", - Remove = true - } - ] - }; - var jsonNode = new JsonObject(); - OverlayDiagnostic? overlayDiagnostic = null; - Assert.Throws(() => overlayDocument.ApplyToDocument(jsonNode, overlayDiagnostic!)); - } - [Fact] - public void ApplyToDocument_ShouldApplyTheActions() - { - var overlayDocument = new OverlayDocument - { - Actions = - [ - new OverlayAction - { - Target = "$.info.title", - Description = "Test Description", - Remove = true - } - ] - }; - var jsonNode = new JsonObject - { - ["info"] = new JsonObject - { - ["title"] = "Test Title", - ["version"] = "1.0.0" - } - }; - var overlayDiagnostic = new OverlayDiagnostic(); - var result = overlayDocument.ApplyToDocument(jsonNode, overlayDiagnostic); - Assert.True(result, "ApplyToDocument should return true when actions are applied successfully."); - Assert.Empty(overlayDiagnostic.Errors); - Assert.Null(jsonNode["info"]?["title"]); - } - [Fact] - public async Task ShouldApplyTheOverlayToAnOpenApiDocumentFromYaml() - { - var yamlDocument = - """ - openapi: 3.1.0 - info: - title: Test API - version: 1.0.0 - randomProperty: randomValue - paths: - /test: - get: - summary: Test endpoint - responses: - '200': - description: OK - """; - var documentStream = new MemoryStream(); - using var writer = new StreamWriter(documentStream, leaveOpen: true); - await writer.WriteAsync(yamlDocument); - await writer.FlushAsync(); - documentStream.Seek(0, SeekOrigin.Begin); - var overlayDocument = new OverlayDocument - { - Info = new OverlayInfo - { - Title = "Test Overlay", - Version = "1.0.0" - }, - Actions = - [ - new OverlayAction - { - Target = "$.info.randomProperty", - Description = "Remove randomProperty", - Remove = true - }, - new OverlayAction - { - Target = "$.paths['/test'].get", - Description = "Update summary", - Update = new JsonObject - { - ["summary"] = "Updated summary" - } - } - ] - }; - - var tempUri = new Uri("http://example.com/overlay.yaml"); - var (document, overlayDiagnostic, openApiDiagnostic, result) = await overlayDocument.ApplyToDocumentStreamAndLoadAsync(documentStream, tempUri); - Assert.True(result, "Overlay application should succeed."); - Assert.NotNull(document); - Assert.NotNull(overlayDiagnostic); - Assert.NotNull(openApiDiagnostic); - Assert.Empty(overlayDiagnostic.Errors); - Assert.Empty(openApiDiagnostic.Errors); - Assert.Null(document.Info.Extensions); // Title should be removed - Assert.Equal("Updated summary", document.Paths["/test"]?.Operations?[HttpMethod.Get].Summary); // Summary should be updated - - } - [Fact] - public async Task ShouldApplyTheOverlayToAnOpenApiDocumentFromJson() - { - var json = - """ - { - "openapi": "3.1.0", - "info": { - "title": "Test API", - "version": "1.0.0", - "randomProperty": "randomValue" - }, - "paths": { - "/test": { - "get": { - "summary": "Test endpoint", - "responses": { - "200": { - "description": "OK" - } - } - } - } - } - } - """; - var documentStream = new MemoryStream(); - using var writer = new StreamWriter(documentStream, leaveOpen: true); - await writer.WriteAsync(json); - await writer.FlushAsync(); - documentStream.Seek(0, SeekOrigin.Begin); - var overlayDocument = new OverlayDocument - { - Info = new OverlayInfo - { - Title = "Test Overlay", - Version = "1.0.0" - }, - Actions = - [ - new OverlayAction - { - Target = "$.info.randomProperty", - Description = "Remove randomProperty", - Remove = true - }, - new OverlayAction - { - Target = "$.paths['/test'].get", - Description = "Update summary", - Update = new JsonObject - { - ["summary"] = "Updated summary" - } - } - ] - }; - - var tempUri = new Uri("http://example.com/overlay.yaml"); - var (document, overlayDiagnostic, openApiDiagnostic, result) = await overlayDocument.ApplyToDocumentStreamAndLoadAsync(documentStream, tempUri); - Assert.True(result, "Overlay application should succeed."); - Assert.NotNull(document); - Assert.NotNull(overlayDiagnostic); - Assert.NotNull(openApiDiagnostic); - Assert.Empty(overlayDiagnostic.Errors); - Assert.Empty(openApiDiagnostic.Errors); - Assert.Null(document.Info.Extensions); // Title should be removed - Assert.Equal("Updated summary", document.Paths["/test"]?.Operations?[HttpMethod.Get].Summary); // Summary should be updated - - } - [Fact] public async Task Load_WithValidMemoryStream_ReturnsReadResultAsync() { @@ -758,141 +558,4 @@ public void CombinesActionsInTheRightOrder() Assert.Equal("Description3", result.Actions[2].Description); Assert.False(result.Actions[2].Remove); } - - private readonly string _tempFilePath = Path.ChangeExtension(Path.GetTempFileName(), ".json"); - - [Fact] - public async Task ApplyToDocumentAsync_WithRelativePath_ShouldSucceed() - { - // Arrange - var openApiDocument = """ - { - "openapi": "3.0.1", - "info": { - "title": "Test API", - "version": "1.0.0" - }, - "paths": { - "/test": { - "get": { - "summary": "Original summary", - "responses": { - "200": { - "description": "Success" - } - } - } - } - } - } - """; - - var overlayDocument = new OverlayDocument - { - Info = new OverlayInfo { Title = "Test Overlay", Version = "1.0.0" }, - Actions = new List - { - new OverlayAction - { - Target = "$.paths['/test'].get", - Description = "Update summary", - Update = new JsonObject - { - ["summary"] = "Updated summary" - } - } - } - }; - - // Create a temporary file with a relative path - await File.WriteAllTextAsync(_tempFilePath, openApiDocument); - - // Act - var (document, overlayDiagnostic, openApiDiagnostic, result) = await overlayDocument.ApplyToDocumentAndLoadAsync(_tempFilePath); - - // Assert - Assert.True(result, "Overlay application should succeed."); - Assert.NotNull(document); - Assert.NotNull(overlayDiagnostic); - Assert.NotNull(openApiDiagnostic); - Assert.Empty(overlayDiagnostic.Errors); - Assert.Empty(openApiDiagnostic.Errors); - Assert.Equal("Updated summary", document.Paths["/test"]?.Operations?.Values?.FirstOrDefault()?.Summary); - - } - [Fact] - public async Task ApplyToDocumentAsync_ContinuesToTheNextActionWhenOneFails() - { - // Arrange - var openApiDocument = """ - { - "openapi": "3.0.1", - "info": { - "title": "Test API", - "version": "1.0.0" - }, - "paths": { - "/test": { - "get": { - "summary": "Original summary", - "responses": { - "200": { - "description": "Success" - } - } - } - } - } - } - """; - - // Create a temporary file with a relative path - await File.WriteAllTextAsync(_tempFilePath, openApiDocument); - - var overlayDocument = new OverlayDocument - { - Info = new OverlayInfo { Title = "Test Overlay", Version = "1.0.0" }, - Actions = new List - { - new OverlayAction - { - Target = "$$$$.paths['/nonexistent'].get", - Description = "This action will fail", - Update = new JsonObject - { - ["summary"] = "Should not be applied" - } - }, - new OverlayAction - { - Target = "$.paths['/test'].get", - Description = "Update summary", - Update = new JsonObject - { - ["summary"] = "Updated summary" - } - } - } - }; - - // Act - var (document, overlayDiagnostic, openApiDiagnostic, result) = await overlayDocument.ApplyToDocumentAndLoadAsync(_tempFilePath); - // Assert - Assert.False(result, "Overlay application should fail."); - Assert.NotNull(document); - Assert.NotNull(overlayDiagnostic); - Assert.NotNull(openApiDiagnostic); - Assert.Single(overlayDiagnostic.Errors); - Assert.Empty(openApiDiagnostic.Errors); - Assert.Equal("Updated summary", document.Paths["/test"]?.Operations?.Values?.FirstOrDefault()?.Summary); - } - - public void Dispose() - { - // Cleanup - if (File.Exists(_tempFilePath)) - { - File.Delete(_tempFilePath); - } - } } \ No newline at end of file From 67096a37218e1964edc015e89db74da9502f1602 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 5 Nov 2025 14:10:58 -0500 Subject: [PATCH 23/26] feat: adds description info field Signed-off-by: Vincent Biret --- src/lib/Models/OverlayInfo.cs | 13 +++ src/lib/PublicAPI.Unshipped.txt | 2 + src/lib/Reader/V1/OverlayInfoDeserializer.cs | 3 +- .../Reader/V1_1/OverlayInfoDeserializer.cs | 5 +- .../lib/Serialization/V1/OverlayInfoTests.cs | 80 +++++++++++++ .../Serialization/V1_1/OverlayInfoTests.cs | 105 ++++++++++++++++++ 6 files changed, 206 insertions(+), 2 deletions(-) diff --git a/src/lib/Models/OverlayInfo.cs b/src/lib/Models/OverlayInfo.cs index a07146e..77c296e 100644 --- a/src/lib/Models/OverlayInfo.cs +++ b/src/lib/Models/OverlayInfo.cs @@ -19,6 +19,11 @@ public class OverlayInfo : IOverlaySerializable, IOverlayExtensible /// public string? Version { get; set; } + /// + /// Gets or sets the description of the overlay. + /// + public string? Description { get; set; } + /// public IDictionary? Extensions { get; set; } @@ -31,6 +36,14 @@ private void SerializeInternal(IOpenApiWriter writer, OverlaySpecVersion version writer.WriteStartObject(); writer.WriteProperty("title", Title); writer.WriteProperty("version", Version); + + // Handle version-specific description field name + if (Description != null) + { + var descriptionFieldName = version == OverlaySpecVersion.Overlay1_0 ? "x-description" : "description"; + writer.WriteProperty(descriptionFieldName, Description); + } + writer.WriteOverlayExtensions(Extensions, version); writer.WriteEndObject(); } diff --git a/src/lib/PublicAPI.Unshipped.txt b/src/lib/PublicAPI.Unshipped.txt index b360b73..f8e8d15 100644 --- a/src/lib/PublicAPI.Unshipped.txt +++ b/src/lib/PublicAPI.Unshipped.txt @@ -77,6 +77,8 @@ BinkyLabs.OpenApi.Overlays.OverlayException.Pointer.get -> string? BinkyLabs.OpenApi.Overlays.OverlayException.Pointer.set -> void BinkyLabs.OpenApi.Overlays.OverlayExtensibleExtensions BinkyLabs.OpenApi.Overlays.OverlayInfo +BinkyLabs.OpenApi.Overlays.OverlayInfo.Description.get -> string? +BinkyLabs.OpenApi.Overlays.OverlayInfo.Description.set -> void BinkyLabs.OpenApi.Overlays.OverlayInfo.Extensions.get -> System.Collections.Generic.IDictionary? BinkyLabs.OpenApi.Overlays.OverlayInfo.Extensions.set -> void BinkyLabs.OpenApi.Overlays.OverlayInfo.OverlayInfo() -> void diff --git a/src/lib/Reader/V1/OverlayInfoDeserializer.cs b/src/lib/Reader/V1/OverlayInfoDeserializer.cs index a531503..259a72d 100644 --- a/src/lib/Reader/V1/OverlayInfoDeserializer.cs +++ b/src/lib/Reader/V1/OverlayInfoDeserializer.cs @@ -7,7 +7,8 @@ internal static partial class OverlayV1Deserializer public static readonly FixedFieldMap InfoFixedFields = new() { { "title", (o, v) => o.Title = v.GetScalarValue() }, - { "version", (o, v) => o.Version = v.GetScalarValue() } + { "version", (o, v) => o.Version = v.GetScalarValue() }, + { "x-description", (o, v) => o.Description = v.GetScalarValue() } }; public static PatternFieldMap GetInfoPatternFields(OverlaySpecVersion version) => new() diff --git a/src/lib/Reader/V1_1/OverlayInfoDeserializer.cs b/src/lib/Reader/V1_1/OverlayInfoDeserializer.cs index be7c33e..a001f7b 100644 --- a/src/lib/Reader/V1_1/OverlayInfoDeserializer.cs +++ b/src/lib/Reader/V1_1/OverlayInfoDeserializer.cs @@ -6,7 +6,10 @@ namespace BinkyLabs.OpenApi.Overlays.Reader.V1_1; internal static partial class OverlayV1_1Deserializer { - public static readonly FixedFieldMap InfoFixedFields = new(OverlayV1Deserializer.InfoFixedFields); + public static readonly FixedFieldMap InfoFixedFields = new(OverlayV1Deserializer.InfoFixedFields, ["x-description"]) + { + { "description", (o, v) => o.Description = v.GetScalarValue() } + }; public static readonly PatternFieldMap InfoPatternFields = OverlayV1Deserializer.GetInfoPatternFields(OverlaySpecVersion.Overlay1_1); public static OverlayInfo LoadInfo(ParseNode node) => OverlayV1Deserializer.LoadInfoInternal(node, InfoFixedFields, InfoPatternFields); } \ No newline at end of file diff --git a/tests/lib/Serialization/V1/OverlayInfoTests.cs b/tests/lib/Serialization/V1/OverlayInfoTests.cs index 698cba3..26f5d15 100644 --- a/tests/lib/Serialization/V1/OverlayInfoTests.cs +++ b/tests/lib/Serialization/V1/OverlayInfoTests.cs @@ -43,6 +43,38 @@ public void SerializeAsV1_ShouldWriteCorrectJson() Assert.True(JsonNode.DeepEquals(jsonResultObject, expectedJsonObject), "The serialized JSON does not match the expected JSON."); } + [Fact] + public void SerializeAsV1_WithDescription_ShouldWriteCorrectJson() + { + // Arrange + var overlayInfo = new OverlayInfo + { + Title = "Test Overlay", + Version = "1.0.0", + Description = "Test overlay description" + }; + using var textWriter = new StringWriter(); + var writer = new OpenApiJsonWriter(textWriter); + + var expectedJson = +""" +{ + "title": "Test Overlay", + "version": "1.0.0", + "x-description": "Test overlay description" +} +"""; + + // Act + overlayInfo.SerializeAsV1(writer); + var jsonResult = textWriter.ToString(); + var jsonResultObject = JsonNode.Parse(jsonResult); + var expectedJsonObject = JsonNode.Parse(expectedJson); + + // Assert + Assert.True(JsonNode.DeepEquals(jsonResultObject, expectedJsonObject), "The serialized JSON does not match the expected JSON."); + } + [Fact] public void Deserialize_ShouldSetPropertiesCorrectly() { @@ -65,4 +97,52 @@ public void Deserialize_ShouldSetPropertiesCorrectly() Assert.Equal("Test Overlay", overlayInfo.Title); Assert.Equal("1.0.0", overlayInfo.Version); } + + [Fact] + public void Deserialize_WithDescription_ShouldSetPropertiesCorrectly() + { + // Arrange + var json = """ + { + "title": "Test Overlay", + "version": "1.0.0", + "x-description": "Test overlay description" + } + """; + var jsonNode = JsonNode.Parse(json)!; + var parsingContext = new ParsingContext(new()); + var parseNode = new MapNode(parsingContext, jsonNode); + + // Act + var overlayInfo = OverlayV1Deserializer.LoadInfo(parseNode); + + // Assert + Assert.Equal("Test Overlay", overlayInfo.Title); + Assert.Equal("1.0.0", overlayInfo.Version); + Assert.Equal("Test overlay description", overlayInfo.Description); + } + + [Fact] + public void Deserialize_WithStandardDescription_ShouldIgnoreField() + { + // Arrange - In V1, "description" field should be ignored, only "x-description" is recognized + var json = """ + { + "title": "Test Overlay", + "version": "1.0.0", + "description": "This should be ignored in V1" + } + """; + var jsonNode = JsonNode.Parse(json)!; + var parsingContext = new ParsingContext(new()); + var parseNode = new MapNode(parsingContext, jsonNode); + + // Act + var overlayInfo = OverlayV1Deserializer.LoadInfo(parseNode); + + // Assert + Assert.Equal("Test Overlay", overlayInfo.Title); + Assert.Equal("1.0.0", overlayInfo.Version); + Assert.Null(overlayInfo.Description); // Should be null because "description" is not recognized in V1 + } } \ No newline at end of file diff --git a/tests/lib/Serialization/V1_1/OverlayInfoTests.cs b/tests/lib/Serialization/V1_1/OverlayInfoTests.cs index 036fc44..c2ac9d8 100644 --- a/tests/lib/Serialization/V1_1/OverlayInfoTests.cs +++ b/tests/lib/Serialization/V1_1/OverlayInfoTests.cs @@ -43,6 +43,38 @@ public void SerializeAsV1_1_ShouldWriteCorrectJson() Assert.True(JsonNode.DeepEquals(jsonResultObject, expectedJsonObject), "The serialized JSON does not match the expected JSON."); } + [Fact] + public void SerializeAsV1_1_WithDescription_ShouldWriteCorrectJson() + { + // Arrange + var overlayInfo = new OverlayInfo + { + Title = "Test Overlay", + Version = "1.1.0", + Description = "Test overlay description" + }; + using var textWriter = new StringWriter(); + var writer = new OpenApiJsonWriter(textWriter); + + var expectedJson = +""" +{ + "title": "Test Overlay", + "version": "1.1.0", + "description": "Test overlay description" +} +"""; + + // Act + overlayInfo.SerializeAsV1_1(writer); + var jsonResult = textWriter.ToString(); + var jsonResultObject = JsonNode.Parse(jsonResult); + var expectedJsonObject = JsonNode.Parse(expectedJson); + + // Assert + Assert.True(JsonNode.DeepEquals(jsonResultObject, expectedJsonObject), "The serialized JSON does not match the expected JSON."); + } + [Fact] public void Deserialize_ShouldSetPropertiesCorrectly() { @@ -65,4 +97,77 @@ public void Deserialize_ShouldSetPropertiesCorrectly() Assert.Equal("Test Overlay", overlayInfo.Title); Assert.Equal("1.1.0", overlayInfo.Version); } + + [Fact] + public void Deserialize_WithDescription_ShouldSetPropertiesCorrectly() + { + // Arrange + var json = """ + { + "title": "Test Overlay", + "version": "1.1.0", + "description": "Test overlay description" + } + """; + var jsonNode = JsonNode.Parse(json)!; + var parsingContext = new ParsingContext(new()); + var parseNode = new MapNode(parsingContext, jsonNode); + + // Act + var overlayInfo = OverlayV1_1Deserializer.LoadInfo(parseNode); + + // Assert + Assert.Equal("Test Overlay", overlayInfo.Title); + Assert.Equal("1.1.0", overlayInfo.Version); + Assert.Equal("Test overlay description", overlayInfo.Description); + } + + [Fact] + public void Deserialize_WithExtensionDescription_ShouldIgnoreXDescription() + { + // Arrange - V1.1 should ignore x-description field + var json = """ + { + "title": "Test Overlay", + "version": "1.1.0", + "x-description": "Test overlay description via extension" + } + """; + var jsonNode = JsonNode.Parse(json)!; + var parsingContext = new ParsingContext(new()); + var parseNode = new MapNode(parsingContext, jsonNode); + + // Act + var overlayInfo = OverlayV1_1Deserializer.LoadInfo(parseNode); + + // Assert + Assert.Equal("Test Overlay", overlayInfo.Title); + Assert.Equal("1.1.0", overlayInfo.Version); + Assert.Null(overlayInfo.Description); // x-description should be ignored in V1.1 + } + + [Fact] + public void Deserialize_WithBothDescriptions_ShouldUseStandardDescriptionOnly() + { + // Arrange - When both "description" and "x-description" are present, only "description" should be used (x-description ignored) + var json = """ + { + "title": "Test Overlay", + "version": "1.1.0", + "description": "Standard description", + "x-description": "Extension description" + } + """; + var jsonNode = JsonNode.Parse(json)!; + var parsingContext = new ParsingContext(new()); + var parseNode = new MapNode(parsingContext, jsonNode); + + // Act + var overlayInfo = OverlayV1_1Deserializer.LoadInfo(parseNode); + + // Assert + Assert.Equal("Test Overlay", overlayInfo.Title); + Assert.Equal("1.1.0", overlayInfo.Version); + Assert.Equal("Standard description", overlayInfo.Description); // Only standard field is recognized + } } \ No newline at end of file From c7dc93c5e8eef38a635970f838b4e305699ff603 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 5 Nov 2025 14:11:19 -0500 Subject: [PATCH 24/26] chore: removes IDisposable declaration as we cleaned up unit tests Signed-off-by: Vincent Biret --- tests/lib/Serialization/V1_1/OverlayDocumentTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/Serialization/V1_1/OverlayDocumentTests.cs b/tests/lib/Serialization/V1_1/OverlayDocumentTests.cs index efe0ff7..5b49d9e 100644 --- a/tests/lib/Serialization/V1_1/OverlayDocumentTests.cs +++ b/tests/lib/Serialization/V1_1/OverlayDocumentTests.cs @@ -12,7 +12,7 @@ namespace BinkyLabs.OpenApi.Overlays.Tests; -public sealed class OverlayDocumentV1_1Tests : IDisposable +public sealed class OverlayDocumentV1_1Tests { [Fact] public void SerializeAsV1_1_ShouldWriteCorrectJson() From 21281ffb0a325fbc95ee42d7993ea240d09b762d Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 5 Nov 2025 14:15:03 -0500 Subject: [PATCH 25/26] chore: formatting --- src/lib/Models/OverlayInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/Models/OverlayInfo.cs b/src/lib/Models/OverlayInfo.cs index 77c296e..a4ee4c7 100644 --- a/src/lib/Models/OverlayInfo.cs +++ b/src/lib/Models/OverlayInfo.cs @@ -36,14 +36,14 @@ private void SerializeInternal(IOpenApiWriter writer, OverlaySpecVersion version writer.WriteStartObject(); writer.WriteProperty("title", Title); writer.WriteProperty("version", Version); - + // Handle version-specific description field name if (Description != null) { var descriptionFieldName = version == OverlaySpecVersion.Overlay1_0 ? "x-description" : "description"; writer.WriteProperty(descriptionFieldName, Description); } - + writer.WriteOverlayExtensions(Extensions, version); writer.WriteEndObject(); } From c70c096ac7be9bb964c1d031425c985678cdc810 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 5 Nov 2025 14:17:26 -0500 Subject: [PATCH 26/26] tests: fixes test definition after merge Signed-off-by: Vincent Biret --- tests/lib/Serialization/V1_1/OverlayActionTests.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/lib/Serialization/V1_1/OverlayActionTests.cs b/tests/lib/Serialization/V1_1/OverlayActionTests.cs index 75fead0..5e732e3 100644 --- a/tests/lib/Serialization/V1_1/OverlayActionTests.cs +++ b/tests/lib/Serialization/V1_1/OverlayActionTests.cs @@ -533,9 +533,10 @@ public void ApplyToDocument_ShouldCopyArrayElements() Assert.True(result); var targetTags = jsonNode["paths"]?["/users"]?["get"]?["tags"]?.AsArray(); Assert.NotNull(targetTags); - Assert.Equal(2, targetTags.Count); - Assert.Equal("post", targetTags[0]?.GetValue()); - Assert.Equal("content", targetTags[1]?.GetValue()); + Assert.Equal(3, targetTags.Count); + Assert.Equal("user", targetTags[0]?.GetValue()); + Assert.Equal("post", targetTags[1]?.GetValue()); + Assert.Equal("content", targetTags[2]?.GetValue()); Assert.Empty(overlayDiagnostic.Errors); }