Skip to content
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
5 changes: 5 additions & 0 deletions src/Microsoft.OpenApi/Models/OpenApiConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,11 @@
/// </summary>
public const string PatternProperties = "patternProperties";

/// <summary>
/// Extension: x-jsonschema-patternProperties
/// </summary>
public const string PatternPropertiesExtension = "x-jsonschema-patternProperties";

/// <summary>
/// Field: AdditionalProperties
/// </summary>
Expand Down Expand Up @@ -753,17 +758,17 @@
/// <summary>
/// Field: V3 JsonSchema Reference Uri
/// </summary>
public const string V3ReferenceUri = "https://registry/components/schemas/";

Check warning on line 761 in src/Microsoft.OpenApi/Models/OpenApiConstants.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor your code not to use hardcoded absolute paths or URIs.

/// <summary>
/// Field: V2 JsonSchema Reference Uri
/// </summary>
public const string V2ReferenceUri = "https://registry/definitions/";

Check warning on line 766 in src/Microsoft.OpenApi/Models/OpenApiConstants.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor your code not to use hardcoded absolute paths or URIs.

/// <summary>
/// The default registry uri for OpenApi documents and workspaces
/// </summary>
public const string BaseRegistryUri = "https://openapi.net/";

Check warning on line 771 in src/Microsoft.OpenApi/Models/OpenApiConstants.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor your code not to use hardcoded absolute paths or URIs.

/// <summary>
/// The components path segment in a $ref value.
Expand Down
12 changes: 12 additions & 0 deletions src/Microsoft.OpenApi/Models/OpenApiSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@
/// Initializes a copy of <see cref="IOpenApiSchema"/> object
/// </summary>
/// <param name="schema">The schema object to copy from.</param>
internal OpenApiSchema(IOpenApiSchema schema)

Check warning on line 270 in src/Microsoft.OpenApi/Models/OpenApiSchema.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this constructor to reduce its Cognitive Complexity from 21 to the 15 allowed.
{
Utils.CheckArgumentNull(schema);
Title = schema.Title ?? Title;
Expand Down Expand Up @@ -348,11 +348,11 @@
SerializeInternal(writer, OpenApiSpecVersion.OpenApi3_0, (writer, element) => element.SerializeAsV3(writer));
}

private static void SerializeBounds(IOpenApiWriter writer, OpenApiSpecVersion version, string propertyName, string exclusivePropertyName, string isExclusivePropertyName, string? value, string? exclusiveValue, bool? isExclusiveValue)

Check warning on line 351 in src/Microsoft.OpenApi/Models/OpenApiSchema.cs

View workflow job for this annotation

GitHub Actions / Build

Method has 8 parameters, which is greater than the 7 authorized.

Check warning on line 351 in src/Microsoft.OpenApi/Models/OpenApiSchema.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.
{
if (version >= OpenApiSpecVersion.OpenApi3_1)
{
if (!string.IsNullOrEmpty(exclusiveValue) && exclusiveValue is not null)

Check warning on line 355 in src/Microsoft.OpenApi/Models/OpenApiSchema.cs

View workflow job for this annotation

GitHub Actions / Build

Change this condition so that it does not always evaluate to 'True'.
{
// was explicitly set in the document or object model
writer.WritePropertyName(exclusivePropertyName);
Expand Down Expand Up @@ -561,6 +561,12 @@
writer.WritePropertyName(OpenApiConstants.UnevaluatedPropertiesExtension);
writer.WriteValue(false);
}

// Write patternProperties as an extension
if (PatternProperties is { Count: > 0 })
{
writer.WriteOptionalMap(OpenApiConstants.PatternPropertiesExtension, PatternProperties, callback);
}
}

// extensions
Expand Down Expand Up @@ -841,6 +847,12 @@
writer.WriteValue(false);
}

// Write patternProperties as an extension
if (PatternProperties is { Count: > 0 })
{
writer.WriteOptionalMap(OpenApiConstants.PatternPropertiesExtension, PatternProperties, (w, s) => s.SerializeAsV2(w));
}

// extensions
writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi2_0);

Expand Down
1 change: 1 addition & 0 deletions src/Microsoft.OpenApi/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#nullable enable
const Microsoft.OpenApi.OpenApiConstants.PatternPropertiesExtension = "x-jsonschema-patternProperties" -> string!
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
/// </summary>
internal static partial class OpenApiV2Deserializer
{
private static readonly FixedFieldMap<OpenApiSchema> _openApiSchemaFixedFields = new()

Check warning on line 17 in src/Microsoft.OpenApi/Reader/V2/OpenApiSchemaDeserializer.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this field to reduce its Cognitive Complexity from 30 to the 15 allowed.
{
{
"title",
Expand Down Expand Up @@ -241,6 +241,10 @@
"example",
(o, n, _) => o.Example = n.CreateAny()
},
{
OpenApiConstants.PatternPropertiesExtension,
(o, n, t) => o.PatternProperties = n.CreateMap(LoadSchema, t)
},
};

private static readonly PatternFieldMap<OpenApiSchema> _openApiSchemaPatternFields = new PatternFieldMap<OpenApiSchema>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
/// </summary>
internal static partial class OpenApiV3Deserializer
{
private static readonly FixedFieldMap<OpenApiSchema> _openApiSchemaFixedFields = new()

Check warning on line 17 in src/Microsoft.OpenApi/Reader/V3/OpenApiSchemaDeserializer.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this field to reduce its Cognitive Complexity from 42 to the 15 allowed.
{
{
"title",
Expand Down Expand Up @@ -276,6 +276,10 @@
}
}
},
{
OpenApiConstants.PatternPropertiesExtension,
(o, n, t) => o.PatternProperties = n.CreateMap(LoadSchema, t)
},
};

private static readonly PatternFieldMap<OpenApiSchema> _openApiSchemaPatternFields = new()
Expand Down
136 changes: 136 additions & 0 deletions test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1312,6 +1312,142 @@ public async Task SerializeUnevaluatedPropertiesTrueNotEmittedInEarlierVersions(
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
}

// PatternProperties tests
[Theory]
[InlineData(OpenApiSpecVersion.OpenApi3_1)]
[InlineData(OpenApiSpecVersion.OpenApi3_2)]
public async Task SerializePatternPropertiesAsKeywordInV31AndV32(OpenApiSpecVersion version)
{
var expected = @"{ ""patternProperties"": { ""^[a-z]+"": { ""type"": ""string"" } } }";
// Given - patternProperties should be emitted as a standard keyword in v3.1+
var schema = new OpenApiSchema
{
PatternProperties = new Dictionary<string, IOpenApiSchema>
{
["^[a-z]+"] = new OpenApiSchema { Type = JsonSchemaType.String }
}
};

// When
var actual = await schema.SerializeAsJsonAsync(version);

// Then
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
}

[Theory]
[InlineData(OpenApiSpecVersion.OpenApi2_0)]
[InlineData(OpenApiSpecVersion.OpenApi3_0)]
public async Task SerializePatternPropertiesAsExtensionInEarlierVersions(OpenApiSpecVersion version)
{
var expected = @"{ ""x-jsonschema-patternProperties"": { ""^[a-z]+"": { ""type"": ""string"" } } }";
// Given - patternProperties should be emitted as extension in versions < 3.1
var schema = new OpenApiSchema
{
PatternProperties = new Dictionary<string, IOpenApiSchema>
{
["^[a-z]+"] = new OpenApiSchema { Type = JsonSchemaType.String }
}
};

// When
var actual = await schema.SerializeAsJsonAsync(version);

// Then
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
}

[Theory]
[InlineData(OpenApiSpecVersion.OpenApi2_0)]
[InlineData(OpenApiSpecVersion.OpenApi3_0)]
public async Task SerializeEmptyPatternPropertiesNotEmittedInEarlierVersions(OpenApiSpecVersion version)
{
var expected = @"{ }";
// Given - empty patternProperties should not emit extension
var schema = new OpenApiSchema
{
PatternProperties = new Dictionary<string, IOpenApiSchema>()
};

// When
var actual = await schema.SerializeAsJsonAsync(version);

// Then
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(actual)));
}

[Fact]
public void DeserializePatternPropertiesExtensionInV2AssignsPatternPropertiesProperty()
{
// Given - a V2 document with x-jsonschema-patternProperties extension in a definition
var jsonContent = """
{
"swagger": "2.0",
"info": { "title": "Test", "version": "1.0" },
"paths": {},
"definitions": {
"TestSchema": {
"type": "object",
"x-jsonschema-patternProperties": {
"^[a-z]+": { "type": "string" }
}
}
}
}
""";

// When
var readResult = OpenApiDocument.Parse(jsonContent, "json");

// Then
Assert.Empty(readResult.Diagnostic.Errors);
var schema = readResult.Document.Components.Schemas["TestSchema"];
Assert.NotNull(schema);
Assert.NotNull(schema.PatternProperties);
Assert.Single(schema.PatternProperties);
Assert.True(schema.PatternProperties.ContainsKey("^[a-z]+"));
Assert.Equal(JsonSchemaType.String, schema.PatternProperties["^[a-z]+"].Type);
// Extension should NOT be present on the schema (it was consumed)
Assert.True(schema.Extensions is null || !schema.Extensions.ContainsKey("x-jsonschema-patternProperties"));
}

[Fact]
public void DeserializePatternPropertiesExtensionInV3AssignsPatternPropertiesProperty()
{
// Given - a V3 document with x-jsonschema-patternProperties extension in a component schema
var jsonContent = """
{
"openapi": "3.0.0",
"info": { "title": "Test", "version": "1.0" },
"paths": {},
"components": {
"schemas": {
"TestSchema": {
"type": "object",
"x-jsonschema-patternProperties": {
"^[a-z]+": { "type": "string" }
}
}
}
}
}
""";

// When
var readResult = OpenApiDocument.Parse(jsonContent, "json");

// Then
Assert.Empty(readResult.Diagnostic.Errors);
var schema = readResult.Document.Components.Schemas["TestSchema"];
Assert.NotNull(schema);
Assert.NotNull(schema.PatternProperties);
Assert.Single(schema.PatternProperties);
Assert.True(schema.PatternProperties.ContainsKey("^[a-z]+"));
Assert.Equal(JsonSchemaType.String, schema.PatternProperties["^[a-z]+"].Type);
// Extension should NOT be present on the schema (it was consumed)
Assert.True(schema.Extensions is null || !schema.Extensions.ContainsKey("x-jsonschema-patternProperties"));
}

internal class SchemaVisitor : OpenApiVisitorBase
{
public List<string> Titles = new();
Expand Down
Loading