Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OpenApiSchema refactor to remove recursive keywords #1899

Merged
merged 5 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 14 additions & 28 deletions src/Microsoft.OpenApi/Models/OpenApiSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
/// <summary>
/// $vocabulary- used in meta-schemas to identify the vocabularies available for use in schemas described by that meta-schema.
/// </summary>
public virtual string Vocabulary { get; set; }
public virtual IDictionary<string, bool> Vocabulary { get; set; }

/// <summary>
/// $dynamicRef - an applicator that allows for deferring the full resolution until runtime, at which point it is resolved each time it is encountered while evaluating an instance
Expand All @@ -55,16 +55,6 @@
/// </summary>
public virtual string DynamicAnchor { get; set; }

/// <summary>
/// $recursiveAnchor - used to construct recursive schemas i.e one that has a reference to its own root, identified by the empty fragment URI reference ("#")
/// </summary>
public virtual string RecursiveAnchor { get; set; }

/// <summary>
/// $recursiveRef - used to construct recursive schemas i.e one that has a reference to its own root, identified by the empty fragment URI reference ("#")
/// </summary>
public virtual string RecursiveRef { get; set; }

/// <summary>
/// $defs - reserves a location for schema authors to inline re-usable JSON Schemas into a more general schema.
/// The keyword does not directly affect the validation result
Expand Down Expand Up @@ -358,11 +348,9 @@
Id = schema?.Id ?? Id;
Schema = schema?.Schema ?? Schema;
Comment = schema?.Comment ?? Comment;
Vocabulary = schema?.Vocabulary ?? Vocabulary;
Vocabulary = schema?.Vocabulary != null ? new Dictionary<string, bool>(schema.Vocabulary) : null;

Check warning

Code scanning / CodeQL

Virtual call in constructor or destructor Warning

Avoid virtual calls in a constructor or destructor.
DynamicAnchor = schema?.DynamicAnchor ?? DynamicAnchor;
DynamicRef = schema?.DynamicRef ?? DynamicRef;
RecursiveAnchor = schema?.RecursiveAnchor ?? RecursiveAnchor;
RecursiveRef = schema?.RecursiveRef ?? RecursiveRef;
Definitions = schema?.Definitions != null ? new Dictionary<string, OpenApiSchema>(schema.Definitions) : null;
UnevaluatedProperties = schema?.UnevaluatedProperties ?? UnevaluatedProperties;
V31ExclusiveMaximum = schema?.V31ExclusiveMaximum ?? V31ExclusiveMaximum;
Expand Down Expand Up @@ -490,30 +478,30 @@
SerializeTypeProperty(Type, writer, version);

// allOf
writer.WriteOptionalCollection(OpenApiConstants.AllOf, AllOf, (w, s) => s.SerializeAsV3(w));
writer.WriteOptionalCollection(OpenApiConstants.AllOf, AllOf, callback);

// anyOf
writer.WriteOptionalCollection(OpenApiConstants.AnyOf, AnyOf, (w, s) => s.SerializeAsV3(w));
writer.WriteOptionalCollection(OpenApiConstants.AnyOf, AnyOf, callback);

// oneOf
writer.WriteOptionalCollection(OpenApiConstants.OneOf, OneOf, (w, s) => s.SerializeAsV3(w));
writer.WriteOptionalCollection(OpenApiConstants.OneOf, OneOf, callback);

// not
writer.WriteOptionalObject(OpenApiConstants.Not, Not, (w, s) => s.SerializeAsV3(w));
writer.WriteOptionalObject(OpenApiConstants.Not, Not, callback);

// items
writer.WriteOptionalObject(OpenApiConstants.Items, Items, (w, s) => s.SerializeAsV3(w));
writer.WriteOptionalObject(OpenApiConstants.Items, Items, callback);

// properties
writer.WriteOptionalMap(OpenApiConstants.Properties, Properties, (w, s) => s.SerializeAsV3(w));
writer.WriteOptionalMap(OpenApiConstants.Properties, Properties, callback);

// additionalProperties
if (AdditionalPropertiesAllowed)
{
writer.WriteOptionalObject(
OpenApiConstants.AdditionalProperties,
AdditionalProperties,
(w, s) => s.SerializeAsV3(w));
callback);
}
else
{
Expand All @@ -536,7 +524,7 @@
}

// discriminator
writer.WriteOptionalObject(OpenApiConstants.Discriminator, Discriminator, (w, s) => s.SerializeAsV3(w));
writer.WriteOptionalObject(OpenApiConstants.Discriminator, Discriminator, callback);

// readOnly
writer.WriteProperty(OpenApiConstants.ReadOnly, ReadOnly, false);
Expand All @@ -548,7 +536,7 @@
writer.WriteOptionalObject(OpenApiConstants.Xml, Xml, (w, s) => s.SerializeAsV2(w));

// externalDocs
writer.WriteOptionalObject(OpenApiConstants.ExternalDocs, ExternalDocs, (w, s) => s.SerializeAsV3(w));
writer.WriteOptionalObject(OpenApiConstants.ExternalDocs, ExternalDocs, callback);

// example
writer.WriteOptionalObject(OpenApiConstants.Example, Example, (w, e) => w.WriteAny(e));
Expand All @@ -557,7 +545,7 @@
writer.WriteProperty(OpenApiConstants.Deprecated, Deprecated, false);

// extensions
writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0);
writer.WriteExtensions(Extensions, version);

writer.WriteEndObject();
}
Expand All @@ -574,12 +562,10 @@
writer.WriteProperty(OpenApiConstants.Id, Id);
writer.WriteProperty(OpenApiConstants.DollarSchema, Schema);
writer.WriteProperty(OpenApiConstants.Comment, Comment);
writer.WriteProperty(OpenApiConstants.Vocabulary, Vocabulary);
writer.WriteOptionalMap(OpenApiConstants.Defs, Definitions, (w, s) => s.SerializeAsV3(w));
writer.WriteOptionalMap(OpenApiConstants.Vocabulary, Vocabulary, (w, s) => w.WriteValue(s));
writer.WriteOptionalMap(OpenApiConstants.Defs, Definitions, (w, s) => s.SerializeAsV31(w));
writer.WriteProperty(OpenApiConstants.DynamicRef, DynamicRef);
writer.WriteProperty(OpenApiConstants.DynamicAnchor, DynamicAnchor);
writer.WriteProperty(OpenApiConstants.RecursiveAnchor, RecursiveAnchor);
writer.WriteProperty(OpenApiConstants.RecursiveRef, RecursiveRef);
writer.WriteProperty(OpenApiConstants.V31ExclusiveMaximum, V31ExclusiveMaximum);
writer.WriteProperty(OpenApiConstants.V31ExclusiveMinimum, V31ExclusiveMinimum);
writer.WriteProperty(OpenApiConstants.UnevaluatedProperties, UnevaluatedProperties, false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,12 @@ internal OpenApiSchemaReference(OpenApiSchema target, string referenceId)
/// <inheritdoc/>
public override string Comment { get => Target.Comment; set => Target.Comment = value; }
/// <inheritdoc/>
public override string Vocabulary { get => Target.Vocabulary; set => Target.Vocabulary = value; }
public override IDictionary<string, bool> Vocabulary { get => Target.Vocabulary; set => Target.Vocabulary = value; }
/// <inheritdoc/>
public override string DynamicRef { get => Target.DynamicRef; set => Target.DynamicRef = value; }
/// <inheritdoc/>
public override string DynamicAnchor { get => Target.DynamicAnchor; set => Target.DynamicAnchor = value; }
/// <inheritdoc/>
public override string RecursiveAnchor { get => Target.RecursiveAnchor; set => Target.RecursiveAnchor = value; }
/// <inheritdoc/>
public override string RecursiveRef { get => Target.RecursiveRef; set => Target.RecursiveRef = value; }
/// <inheritdoc/>
public override IDictionary<string, OpenApiSchema> Definitions { get => Target.Definitions; set => Target.Definitions = value; }
/// <inheritdoc/>
public override decimal? V31ExclusiveMaximum { get => Target.V31ExclusiveMaximum; set => Target.V31ExclusiveMaximum = value; }
Expand Down
10 changes: 1 addition & 9 deletions src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ internal static partial class OpenApiV31Deserializer
},
{
"$vocabulary",
(o, n, _) => o.Vocabulary = n.GetScalarValue()
(o, n, _) => o.Vocabulary = n.CreateSimpleMap(LoadBool)
},
{
"$dynamicRef",
Expand All @@ -42,14 +42,6 @@ internal static partial class OpenApiV31Deserializer
"$dynamicAnchor",
(o, n, _) => o.DynamicAnchor = n.GetScalarValue()
},
{
"$recursiveAnchor",
(o, n, _) => o.RecursiveAnchor = n.GetScalarValue()
},
{
"$recursiveRef",
(o, n, _) => o.RecursiveRef = n.GetScalarValue()
},
{
"$defs",
(o, n, t) => o.Definitions = n.CreateMap(LoadSchema, t)
Expand Down
5 changes: 5 additions & 0 deletions src/Microsoft.OpenApi/Reader/V31/OpenApiV31Deserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ private static string LoadString(ParseNode node)
return node.GetScalarValue();
}

private static bool LoadBool(ParseNode node)
{
return bool.Parse(node.GetScalarValue());
}

private static (string, string) GetReferenceIdAndExternalResource(string pointer)
{
/* Check whether the reference pointer is a URL
Expand Down
19 changes: 19 additions & 0 deletions src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,25 @@ public static void WriteOptionalMap(
}
}

/// <summary>
/// Write the optional Open API element map (string to string mapping).
/// </summary>
/// <param name="writer">The Open API writer.</param>
/// <param name="name">The property name.</param>
/// <param name="elements">The map values.</param>
/// <param name="action">The map element writer action.</param>
public static void WriteOptionalMap(
this IOpenApiWriter writer,
string name,
IDictionary<string, bool> elements,
Action<IOpenApiWriter, bool> action)
{
if (elements != null && elements.Any())
{
writer.WriteMapInternal(name, elements, action);
}
}

/// <summary>
/// Write the optional Open API element map.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

using System.Collections.Generic;
Expand Down Expand Up @@ -404,5 +404,51 @@ public void LoadSchemaWithNullableExtensionAsV31Works(string filePath)
// Assert
schema.Type.Should().BeEquivalentTo(new string[] { "string", "null" });
}

[Fact]
public void SerializeSchemaWithJsonSchemaKeywordsWorks()
{
// Arrange
var expected = @"$id: https://example.com/schemas/person.schema.yaml
$schema: https://json-schema.org/draft/2020-12/schema
$comment: A schema defining a person object with optional references to dynamic components.
$vocabulary:
https://json-schema.org/draft/2020-12/vocab/core: true
https://json-schema.org/draft/2020-12/vocab/applicator: true
https://json-schema.org/draft/2020-12/vocab/validation: true
https://json-schema.org/draft/2020-12/vocab/meta-data: false
https://json-schema.org/draft/2020-12/vocab/format-annotation: false
$dynamicAnchor: addressDef
title: Person
required:
- name
type: object
properties:
name:
$comment: The person's full name
type: string
age:
$comment: Age must be a non-negative integer
minimum: 0
type: integer
address:
$comment: Reference to an address definition which can change dynamically
$dynamicRef: '#addressDef'
description: Schema for a person object
";
var path = Path.Combine(SampleFolderPath, "schemaWithJsonSchemaKeywords.yaml");

// Act
var schema = OpenApiModelFactory.Load<OpenApiSchema>(path, OpenApiSpecVersion.OpenApi3_1, out _);

// serialization
var writer = new StringWriter();
schema.SerializeAsV31(new OpenApiYamlWriter(writer));
var schemaString = writer.ToString();

// Assert
schema.Vocabulary.Keys.Count.Should().Be(5);
schemaString.MakeLineBreaksEnvironmentNeutral().Should().Be(expected.MakeLineBreaksEnvironmentNeutral());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
$schema: "https://json-schema.org/draft/2020-12/schema"
$id: "https://example.com/schemas/person.schema.yaml"
$comment: "A schema defining a person object with optional references to dynamic components."
$vocabulary:
"https://json-schema.org/draft/2020-12/vocab/core": true
"https://json-schema.org/draft/2020-12/vocab/applicator": true
"https://json-schema.org/draft/2020-12/vocab/validation": true
"https://json-schema.org/draft/2020-12/vocab/meta-data": false
"https://json-schema.org/draft/2020-12/vocab/format-annotation": false

title: "Person"
description: "Schema for a person object"
type: "object"

properties:
name:
type: "string"
$comment: "The person's full name"
age:
type: "integer"
minimum: 0
$comment: "Age must be a non-negative integer"
address:
$dynamicRef: "#addressDef"
$comment: "Reference to an address definition which can change dynamically"

required:
- name

$dynamicAnchor: "addressDef"
definitions:
address:
$dynamicAnchor: "addressDef"
type: "object"
properties:
street:
type: "string"
city:
type: "string"
postalCode:
type: "string"
9 changes: 3 additions & 6 deletions test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
Original file line number Diff line number Diff line change
Expand Up @@ -901,8 +901,6 @@ namespace Microsoft.OpenApi.Models
public virtual System.Collections.Generic.IDictionary<string, Microsoft.OpenApi.Models.OpenApiSchema> PatternProperties { get; set; }
public virtual System.Collections.Generic.IDictionary<string, Microsoft.OpenApi.Models.OpenApiSchema> Properties { get; set; }
public virtual bool ReadOnly { get; set; }
public virtual string RecursiveAnchor { get; set; }
public virtual string RecursiveRef { get; set; }
public virtual Microsoft.OpenApi.Models.OpenApiReference Reference { get; set; }
public virtual System.Collections.Generic.ISet<string> Required { get; set; }
public virtual string Schema { get; set; }
Expand All @@ -914,7 +912,7 @@ namespace Microsoft.OpenApi.Models
public virtual bool UnresolvedReference { get; set; }
public virtual decimal? V31ExclusiveMaximum { get; set; }
public virtual decimal? V31ExclusiveMinimum { get; set; }
public virtual string Vocabulary { get; set; }
public virtual System.Collections.Generic.IDictionary<string, bool> Vocabulary { get; set; }
public virtual bool WriteOnly { get; set; }
public virtual Microsoft.OpenApi.Models.OpenApiXml Xml { get; set; }
public virtual void SerializeAsV2(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { }
Expand Down Expand Up @@ -1243,8 +1241,6 @@ namespace Microsoft.OpenApi.Models.References
public override System.Collections.Generic.IDictionary<string, Microsoft.OpenApi.Models.OpenApiSchema> PatternProperties { get; set; }
public override System.Collections.Generic.IDictionary<string, Microsoft.OpenApi.Models.OpenApiSchema> Properties { get; set; }
public override bool ReadOnly { get; set; }
public override string RecursiveAnchor { get; set; }
public override string RecursiveRef { get; set; }
public override System.Collections.Generic.ISet<string> Required { get; set; }
public override string Schema { get; set; }
public override string Title { get; set; }
Expand All @@ -1254,7 +1250,7 @@ namespace Microsoft.OpenApi.Models.References
public override bool? UniqueItems { get; set; }
public override decimal? V31ExclusiveMaximum { get; set; }
public override decimal? V31ExclusiveMinimum { get; set; }
public override string Vocabulary { get; set; }
public override System.Collections.Generic.IDictionary<string, bool> Vocabulary { get; set; }
public override bool WriteOnly { get; set; }
public override Microsoft.OpenApi.Models.OpenApiXml Xml { get; set; }
public override void SerializeAsV2(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { }
Expand Down Expand Up @@ -1849,6 +1845,7 @@ namespace Microsoft.OpenApi.Writers
{
public static void WriteOptionalCollection(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IEnumerable<string> elements, System.Action<Microsoft.OpenApi.Writers.IOpenApiWriter, string> action) { }
public static void WriteOptionalCollection<T>(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IEnumerable<T> elements, System.Action<Microsoft.OpenApi.Writers.IOpenApiWriter, T> action) { }
public static void WriteOptionalMap(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IDictionary<string, bool> elements, System.Action<Microsoft.OpenApi.Writers.IOpenApiWriter, bool> action) { }
public static void WriteOptionalMap(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IDictionary<string, string> elements, System.Action<Microsoft.OpenApi.Writers.IOpenApiWriter, string> action) { }
public static void WriteOptionalMap<T>(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IDictionary<string, T> elements, System.Action<Microsoft.OpenApi.Writers.IOpenApiWriter, T> action)
where T : Microsoft.OpenApi.Interfaces.IOpenApiElement { }
Expand Down
Loading