From 75d7a662fc873566e50191127e4082b4ecf5ca7a Mon Sep 17 00:00:00 2001 From: Michael Wamae <68949852+Michael-Wamae@users.noreply.github.com> Date: Thu, 20 Feb 2025 18:44:24 +0300 Subject: [PATCH] feat: add support for dependentRequired --- .../Models/Interfaces/IOpenApiSchema.cs | 7 +++- .../Models/OpenApiConstants.cs | 5 +++ src/Microsoft.OpenApi/Models/OpenApiSchema.cs | 5 +++ .../References/OpenApiSchemaReference.cs | 3 ++ .../Reader/ParseNodes/MapNode.cs | 28 ++++++++++++++++ .../Reader/ParseNodes/ParseNode.cs | 7 +++- .../Reader/V31/OpenApiSchemaDeserializer.cs | 9 +++++- .../Writers/OpenApiWriterBase.cs | 19 +++++++++++ .../Writers/OpenApiWriterExtensions.cs | 19 +++++++++++ .../V31Tests/OpenApiDocumentTests.cs | 32 +++++++++++++++++++ .../V31Tests/OpenApiSchemaTests.cs | 19 +++++++++++ .../documentWithReusablePaths.yaml | 10 ++++++ .../OpenApiDocument/documentWithWebhooks.yaml | 10 ++++++ .../Samples/OpenApiSchema/jsonSchema.json | 15 +++++++++ .../PublicApi/PublicApi.approved.txt | 6 ++++ 15 files changed, 191 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchema.cs b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchema.cs index 9ff8e8389..6cf093499 100644 --- a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchema.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Text.Json.Nodes; using Microsoft.OpenApi.Interfaces; @@ -299,4 +299,9 @@ public interface IOpenApiSchema : IOpenApiDescribedElement, IOpenApiSerializable /// Annotations are NOT (de)serialized with the schema and can be used for custom properties. /// public IDictionary Annotations { get; } + + /// + /// Follow JSON Schema definition:https://json-schema.org/draft/2020-12/json-schema-validation#section-6.5.4 + /// + public IDictionary> DependentRequired { get; } } diff --git a/src/Microsoft.OpenApi/Models/OpenApiConstants.cs b/src/Microsoft.OpenApi/Models/OpenApiConstants.cs index 1c016f4c4..ef3053784 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiConstants.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiConstants.cs @@ -720,6 +720,11 @@ public static class OpenApiConstants /// public const string NullableExtension = "x-nullable"; + /// + /// Field: DependentRequired + /// + public const string DependentRequired = "dependentRequired"; + #region V2.0 /// diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index cfed33744..da93b17ca 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -176,6 +176,9 @@ public class OpenApiSchema : IOpenApiReferenceable, IOpenApiExtensible, IOpenApi /// public IDictionary Annotations { get; set; } + /// + public IDictionary> DependentRequired { get; set; } = new Dictionary>(); + /// /// Parameterless constructor /// @@ -239,6 +242,7 @@ internal OpenApiSchema(IOpenApiSchema schema) Extensions = schema.Extensions != null ? new Dictionary(schema.Extensions) : null; Annotations = schema.Annotations != null ? new Dictionary(schema.Annotations) : null; UnrecognizedKeywords = schema.UnrecognizedKeywords != null ? new Dictionary(schema.UnrecognizedKeywords) : null; + DependentRequired = schema.DependentRequired != null ? new Dictionary>(schema.DependentRequired) : null; } /// @@ -408,6 +412,7 @@ internal void WriteJsonSchemaKeywords(IOpenApiWriter writer) writer.WriteProperty(OpenApiConstants.UnevaluatedProperties, UnevaluatedProperties, false); writer.WriteOptionalCollection(OpenApiConstants.Examples, Examples, (nodeWriter, s) => nodeWriter.WriteAny(s)); writer.WriteOptionalMap(OpenApiConstants.PatternProperties, PatternProperties, (w, s) => s.SerializeAsV31(w)); + writer.WriteOptionalMap(OpenApiConstants.DependentRequired, DependentRequired, (w, s) => w.WriteValue(s)); } internal void WriteAsItemsProperties(IOpenApiWriter writer) diff --git a/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs b/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs index 9252d6b89..746af1d80 100644 --- a/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs +++ b/src/Microsoft.OpenApi/Models/References/OpenApiSchemaReference.cs @@ -154,6 +154,9 @@ public string Description /// public IDictionary Annotations { get => Target?.Annotations; } + /// + public IDictionary> DependentRequired { get => Target?.DependentRequired; } + /// public override void SerializeAsV31(IOpenApiWriter writer) { diff --git a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs index 6aced216f..4988756d2 100644 --- a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs +++ b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs @@ -103,6 +103,34 @@ public override Dictionary CreateSimpleMap(Func map) return nodes.ToDictionary(k => k.key, v => v.value); } + public override Dictionary> CreateArrayMap(Func map, OpenApiDocument openApiDocument) + { + var jsonMap = _node ?? throw new OpenApiReaderException($"Expected map while parsing {typeof(T).Name}", Context); + + var nodes = jsonMap.Select(n => + { + var key = n.Key; + try + { + Context.StartObject(key); + JsonArray arrayNode = n.Value is JsonArray value + ? value + : throw new OpenApiReaderException($"Expected array while parsing {typeof(T).Name}", Context); + + ISet values = new HashSet(arrayNode.Select(item => map(new ValueNode(Context, item), openApiDocument))); + + return (key, values); + + } + finally + { + Context.EndObject(); + } + }); + + return nodes.ToDictionary(kvp => kvp.key, kvp => kvp.values); + } + public IEnumerator GetEnumerator() { return _nodes.GetEnumerator(); diff --git a/src/Microsoft.OpenApi/Reader/ParseNodes/ParseNode.cs b/src/Microsoft.OpenApi/Reader/ParseNodes/ParseNode.cs index 9fbf3f47a..798795350 100644 --- a/src/Microsoft.OpenApi/Reader/ParseNodes/ParseNode.cs +++ b/src/Microsoft.OpenApi/Reader/ParseNodes/ParseNode.cs @@ -84,6 +84,11 @@ public virtual string GetScalarValue() public virtual List CreateListOfAny() { throw new OpenApiReaderException("Cannot create a list from this type of node.", Context); - } + } + + public virtual Dictionary> CreateArrayMap(Func map, OpenApiDocument openApiDocument) + { + throw new OpenApiReaderException("Cannot create array map from this type of node.", Context); + } } } diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs index fcd97fca2..02039cebd 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using Microsoft.OpenApi.Extensions; @@ -234,6 +234,13 @@ internal static partial class OpenApiV31Deserializer "deprecated", (o, n, _) => o.Deprecated = bool.Parse(n.GetScalarValue()) }, + { + "dependentRequired", + (o, n, doc) => + { + o.DependentRequired = n.CreateArrayMap((n2, p) => n2.GetScalarValue(), doc); + } + }, }; private static readonly PatternFieldMap _openApiSchemaPatternFields = new() diff --git a/src/Microsoft.OpenApi/Writers/OpenApiWriterBase.cs b/src/Microsoft.OpenApi/Writers/OpenApiWriterBase.cs index 7626f5908..aa515af7e 100644 --- a/src/Microsoft.OpenApi/Writers/OpenApiWriterBase.cs +++ b/src/Microsoft.OpenApi/Writers/OpenApiWriterBase.cs @@ -210,6 +210,21 @@ public virtual void WriteValue(bool value) Writer.Write(value.ToString().ToLower()); } + /// + /// Writes an enumerable collection as an array + /// + /// The enumerable collection to write. + /// The type of elements in the collection. + public virtual void WriteEnumerable(IEnumerable collection) + { + WriteStartArray(); + foreach (var item in collection) + { + WriteValue(item); + } + WriteEndArray(); + } + /// /// Write object value. /// @@ -264,6 +279,10 @@ public virtual void WriteValue(object value) { WriteValue((DateTimeOffset)value); } + else if (value is IEnumerable enumerable) + { + WriteEnumerable(enumerable); + } else { throw new OpenApiWriterException(string.Format(SRResource.OpenApiUnsupportedValueType, type.FullName)); diff --git a/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs b/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs index 8c49a2960..0e0256f79 100644 --- a/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs +++ b/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs @@ -311,6 +311,25 @@ public static void WriteOptionalMap( } } + /// + /// Write the optional Open API element map (string to array mapping). + /// + /// The Open API writer. + /// The property name. + /// The map values. + /// The map element writer action. + public static void WriteOptionalMap( + this IOpenApiWriter writer, + string name, + IDictionary> elements, + Action> action) + { + if (elements != null && elements.Any()) + { + writer.WriteMapInternal(name, elements, action); + } + } + /// /// Write the optional Open API element map. /// diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDocumentTests.cs index 6f955e62f..e5523a08c 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDocumentTests.cs @@ -46,6 +46,10 @@ public async Task ParseDocumentWithWebhooksShouldSucceed() "id", "name" }, + DependentRequired = new Dictionary> + { + { "tag", new HashSet { "category" } } + }, Properties = new Dictionary { ["id"] = new OpenApiSchema() @@ -61,6 +65,10 @@ public async Task ParseDocumentWithWebhooksShouldSucceed() { Type = JsonSchemaType.String }, + ["category"] = new OpenApiSchema() + { + Type = JsonSchemaType.String, + }, } }, ["newPetSchema"] = new OpenApiSchema() @@ -70,6 +78,10 @@ public async Task ParseDocumentWithWebhooksShouldSucceed() { "name" }, + DependentRequired = new Dictionary> + { + { "tag", new HashSet { "category" } } + }, Properties = new Dictionary { ["id"] = new OpenApiSchema() @@ -85,6 +97,10 @@ public async Task ParseDocumentWithWebhooksShouldSucceed() { Type = JsonSchemaType.String }, + ["category"] = new OpenApiSchema() + { + Type = JsonSchemaType.String, + }, } } } @@ -222,6 +238,10 @@ public async Task ParseDocumentsWithReusablePathItemInWebhooksSucceeds() "id", "name" }, + DependentRequired = new Dictionary> + { + { "tag", new HashSet { "category" } } + }, Properties = new Dictionary { ["id"] = new OpenApiSchema() @@ -237,6 +257,10 @@ public async Task ParseDocumentsWithReusablePathItemInWebhooksSucceeds() { Type = JsonSchemaType.String }, + ["category"] = new OpenApiSchema() + { + Type = JsonSchemaType.String, + }, } }, ["newPetSchema"] = new OpenApiSchema() @@ -246,6 +270,10 @@ public async Task ParseDocumentsWithReusablePathItemInWebhooksSucceeds() { "name" }, + DependentRequired = new Dictionary> + { + { "tag", new HashSet { "category" } } + }, Properties = new Dictionary { ["id"] = new OpenApiSchema() @@ -261,6 +289,10 @@ public async Task ParseDocumentsWithReusablePathItemInWebhooksSucceeds() { Type = JsonSchemaType.String }, + ["category"] = new OpenApiSchema() + { + Type = JsonSchemaType.String, + }, } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs index 555b71c54..127cbe689 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs @@ -68,6 +68,10 @@ public async Task ParseBasicV31SchemaShouldSucceed() "veggieName", "veggieLike" }, + DependentRequired = new Dictionary> + { + { "veggieType", new HashSet { "veggieColor", "veggieSize" } } + }, Properties = new Dictionary { ["veggieName"] = new OpenApiSchema @@ -79,6 +83,21 @@ public async Task ParseBasicV31SchemaShouldSucceed() { Type = JsonSchemaType.Boolean, Description = "Do I like this vegetable?" + }, + ["veggieType"] = new OpenApiSchema + { + Type = JsonSchemaType.String, + Description = "The type of vegetable (e.g., root, leafy, etc.)." + }, + ["veggieColor"] = new OpenApiSchema + { + Type = JsonSchemaType.String, + Description = "The color of the vegetable." + }, + ["veggieSize"] = new OpenApiSchema + { + Type = JsonSchemaType.String, + Description = "The size of the vegetable." } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiDocument/documentWithReusablePaths.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiDocument/documentWithReusablePaths.yaml index 2ce75167e..148ff40c2 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiDocument/documentWithReusablePaths.yaml +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiDocument/documentWithReusablePaths.yaml @@ -13,6 +13,9 @@ components: required: - id - name + dependentRequired: + tag: + - category properties: id: type: integer @@ -21,10 +24,15 @@ components: type: string tag: type: string + category: + type: string newPetSchema: type: object required: - name + dependentRequired: + tag: + - category properties: id: type: integer @@ -33,6 +41,8 @@ components: type: string tag: type: string + category: + type: string pathItems: pets: get: diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiDocument/documentWithWebhooks.yaml b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiDocument/documentWithWebhooks.yaml index 5b535a55e..ee15f6849 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiDocument/documentWithWebhooks.yaml +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiDocument/documentWithWebhooks.yaml @@ -59,6 +59,9 @@ components: required: - id - name + dependentRequired: + tag: + - category properties: id: type: integer @@ -67,10 +70,15 @@ components: type: string tag: type: string + category: + type: string newPetSchema: type: object required: - name + dependentRequired: + tag: + - category properties: id: type: integer @@ -78,4 +86,6 @@ components: name: type: string tag: + type: string + category: type: string \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiSchema/jsonSchema.json b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiSchema/jsonSchema.json index 84b1ea211..4a16ab4f5 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiSchema/jsonSchema.json +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/Samples/OpenApiSchema/jsonSchema.json @@ -26,7 +26,22 @@ "veggieLike": { "type": "boolean", "description": "Do I like this vegetable?" + }, + "veggieType": { + "type": "string", + "description": "The type of vegetable (e.g., root, leafy, etc.)." + }, + "veggieColor": { + "type": "string", + "description": "The color of the vegetable." + }, + "veggieSize": { + "type": "string", + "description": "The size of the vegetable." } + }, + "dependentRequired": { + "veggieType": [ "veggieColor", "veggieSize" ] } } } diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt index 2883a61bb..d08428d5d 100644 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt @@ -420,6 +420,7 @@ namespace Microsoft.OpenApi.Models.Interfaces string Const { get; } System.Text.Json.Nodes.JsonNode Default { get; } System.Collections.Generic.IDictionary Definitions { get; } + System.Collections.Generic.IDictionary> DependentRequired { get; } bool Deprecated { get; } Microsoft.OpenApi.Models.OpenApiDiscriminator Discriminator { get; } string DynamicAnchor { get; } @@ -561,6 +562,7 @@ namespace Microsoft.OpenApi.Models public const string Definitions = "definitions"; public const string Defs = "$defs"; public const string Delete = "delete"; + public const string DependentRequired = "dependentRequired"; public const string Deprecated = "deprecated"; public const string Description = "description"; public const string Discriminator = "discriminator"; @@ -1021,6 +1023,7 @@ namespace Microsoft.OpenApi.Models public string Const { get; set; } public System.Text.Json.Nodes.JsonNode Default { get; set; } public System.Collections.Generic.IDictionary Definitions { get; set; } + public System.Collections.Generic.IDictionary> DependentRequired { get; set; } public bool Deprecated { get; set; } public string Description { get; set; } public Microsoft.OpenApi.Models.OpenApiDiscriminator Discriminator { get; set; } @@ -1373,6 +1376,7 @@ namespace Microsoft.OpenApi.Models.References public string Const { get; } public System.Text.Json.Nodes.JsonNode Default { get; } public System.Collections.Generic.IDictionary Definitions { get; } + public System.Collections.Generic.IDictionary> DependentRequired { get; } public bool Deprecated { get; } public string Description { get; set; } public Microsoft.OpenApi.Models.OpenApiDiscriminator Discriminator { get; } @@ -1969,6 +1973,7 @@ namespace Microsoft.OpenApi.Writers protected void VerifyCanWritePropertyName(string name) { } public abstract void WriteEndArray(); public abstract void WriteEndObject(); + public virtual void WriteEnumerable(System.Collections.Generic.IEnumerable collection) { } public virtual void WriteIndentation() { } public abstract void WriteNull(); public abstract void WritePropertyName(string name); @@ -1993,6 +1998,7 @@ namespace Microsoft.OpenApi.Writers public static void WriteOptionalCollection(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IEnumerable elements, System.Action action) { } public static void WriteOptionalCollection(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IEnumerable elements, System.Action action) { } public static void WriteOptionalMap(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IDictionary elements, System.Action action) { } + public static void WriteOptionalMap(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IDictionary> elements, System.Action> action) { } public static void WriteOptionalMap(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IDictionary elements, System.Action action) { } public static void WriteOptionalMap(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IDictionary elements, System.Action action) { } public static void WriteOptionalMap(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IDictionary elements, System.Action action)