Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
89 changes: 62 additions & 27 deletions src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.OpenApi.Exceptions;
using Microsoft.OpenApi.Models;

Expand All @@ -19,7 +20,7 @@
/// </summary>
/// <param name="schemaType"></param>
/// <returns></returns>
public static string? ToIdentifier(this JsonSchemaType? schemaType)
public static string[]? ToIdentifier(this JsonSchemaType? schemaType)
{
if (schemaType is null)
{
Expand All @@ -33,20 +34,32 @@
/// </summary>
/// <param name="schemaType"></param>
/// <returns></returns>
public static string? ToIdentifier(this JsonSchemaType schemaType)
public static string[] ToIdentifier(this JsonSchemaType schemaType)
{
return schemaType switch
{
JsonSchemaType.Null => "null",
JsonSchemaType.Boolean => "boolean",
JsonSchemaType.Integer => "integer",
JsonSchemaType.Number => "number",
JsonSchemaType.String => "string",
JsonSchemaType.Array => "array",
JsonSchemaType.Object => "object",
_ => null,
};
var types = new List<string>();

if (schemaType.HasFlag(JsonSchemaType.Boolean)) types.Add("boolean");

Check warning on line 41 in src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs

View workflow job for this annotation

GitHub Actions / Build

Define a constant instead of using this literal 'boolean' 4 times. (https://rules.sonarsource.com/csharp/RSPEC-1192)
if (schemaType.HasFlag(JsonSchemaType.Integer)) types.Add("integer");

Check warning on line 42 in src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs

View workflow job for this annotation

GitHub Actions / Build

Define a constant instead of using this literal 'integer' 8 times. (https://rules.sonarsource.com/csharp/RSPEC-1192)
if (schemaType.HasFlag(JsonSchemaType.Number)) types.Add("number");

Check warning on line 43 in src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs

View workflow job for this annotation

GitHub Actions / Build

Define a constant instead of using this literal 'number' 14 times. (https://rules.sonarsource.com/csharp/RSPEC-1192)
if (schemaType.HasFlag(JsonSchemaType.String)) types.Add("string");

Check warning on line 44 in src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs

View workflow job for this annotation

GitHub Actions / Build

Define a constant instead of using this literal 'string' 13 times. (https://rules.sonarsource.com/csharp/RSPEC-1192)
if (schemaType.HasFlag(JsonSchemaType.Object)) types.Add("object");
if (schemaType.HasFlag(JsonSchemaType.Array)) types.Add("array");
if (schemaType.HasFlag(JsonSchemaType.Null)) types.Add("null");

return types.ToArray();
}

/// <summary>
/// Returns the first identifier from a string array.
/// </summary>
/// <param name="schemaType"></param>
/// <returns></returns>
internal static string FirstIdentifier(this JsonSchemaType schemaType)
{
var identifier = schemaType.ToIdentifier();
return identifier[0];
}

#nullable restore

/// <summary>
Expand All @@ -61,7 +74,7 @@
"null" => JsonSchemaType.Null,
"boolean" => JsonSchemaType.Boolean,
"integer" or "int" => JsonSchemaType.Integer,
"number" or "double" or "float" or "decimal"=> JsonSchemaType.Number,

Check warning on line 77 in src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs

View workflow job for this annotation

GitHub Actions / Build

Define a constant instead of using this literal 'double' 7 times. (https://rules.sonarsource.com/csharp/RSPEC-1192)

Check warning on line 77 in src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs

View workflow job for this annotation

GitHub Actions / Build

Define a constant instead of using this literal 'float' 5 times. (https://rules.sonarsource.com/csharp/RSPEC-1192)
"string" => JsonSchemaType.String,
"array" => JsonSchemaType.Array,
"object" => JsonSchemaType.Object,
Expand All @@ -70,18 +83,38 @@
};
}

/// <summary>
/// Converts a schema type's identifier into the enum equivalent
/// </summary>
/// <param name="identifier"></param>
/// <returns></returns>
public static JsonSchemaType? ToJsonSchemaType(this string[] identifier)
{
if (identifier == null)
{
return null;
}

JsonSchemaType type = 0;
foreach (var id in identifier)
{
type |= id.ToJsonSchemaType();
}
return type;
}

private static readonly Dictionary<Type, Func<OpenApiSchema>> _simpleTypeToOpenApiSchema = new()
{
[typeof(bool)] = () => new() { Type = JsonSchemaType.Boolean },
[typeof(byte)] = () => new() { Type = JsonSchemaType.String, Format = "byte" },
[typeof(int)] = () => new() { Type = JsonSchemaType.Integer, Format = "int32" },

Check warning on line 110 in src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs

View workflow job for this annotation

GitHub Actions / Build

Define a constant instead of using this literal 'int32' 6 times. (https://rules.sonarsource.com/csharp/RSPEC-1192)
[typeof(uint)] = () => new() { Type = JsonSchemaType.Integer, Format = "int32" },
[typeof(long)] = () => new() { Type = JsonSchemaType.Integer, Format = "int64" },

Check warning on line 112 in src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs

View workflow job for this annotation

GitHub Actions / Build

Define a constant instead of using this literal 'int64' 6 times. (https://rules.sonarsource.com/csharp/RSPEC-1192)
[typeof(ulong)] = () => new() { Type = JsonSchemaType.Integer, Format = "int64" },
[typeof(float)] = () => new() { Type = JsonSchemaType.Number, Format = "float" },
[typeof(double)] = () => new() { Type = JsonSchemaType.Number, Format = "double" },
[typeof(decimal)] = () => new() { Type = JsonSchemaType.Number, Format = "double" },
[typeof(DateTime)] = () => new() { Type = JsonSchemaType.String, Format = "date-time" },

Check warning on line 117 in src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs

View workflow job for this annotation

GitHub Actions / Build

Define a constant instead of using this literal 'date-time' 6 times. (https://rules.sonarsource.com/csharp/RSPEC-1192)
[typeof(DateTimeOffset)] = () => new() { Type = JsonSchemaType.String, Format = "date-time" },
[typeof(Guid)] = () => new() { Type = JsonSchemaType.String, Format = "uuid" },
[typeof(char)] = () => new() { Type = JsonSchemaType.String },
Expand Down Expand Up @@ -141,7 +174,7 @@
}

/// <summary>
/// Maps an JsonSchema data type and format to a simple type.
/// Maps a JsonSchema data type and format to a simple type.
/// </summary>
/// <param name="schema">The OpenApi data type</param>
/// <returns>The simple type</returns>
Expand All @@ -152,21 +185,23 @@
{
throw new ArgumentNullException(nameof(schema));
}
var isNullable = (schema.Type & JsonSchemaType.Null) == JsonSchemaType.Null;
var nonNullable = (schema.Type & ~JsonSchemaType.Null)?.FirstIdentifier();

var type = ((schema.Type & ~JsonSchemaType.Null).ToIdentifier(), schema.Format?.ToLowerInvariant(), schema.Type & JsonSchemaType.Null) switch
var type = (nonNullable, schema.Format?.ToLowerInvariant(), isNullable) switch
{
("integer" or "number", "int32", JsonSchemaType.Null) => typeof(int?),
("integer" or "number", "int64", JsonSchemaType.Null) => typeof(long?),
("integer", null, JsonSchemaType.Null) => typeof(long?),
("number", "float", JsonSchemaType.Null) => typeof(float?),
("number", "double", JsonSchemaType.Null) => typeof(double?),
("number", null, JsonSchemaType.Null) => typeof(double?),
("number", "decimal", JsonSchemaType.Null) => typeof(decimal?),
("string", "byte", JsonSchemaType.Null) => typeof(byte?),
("string", "date-time", JsonSchemaType.Null) => typeof(DateTimeOffset?),
("string", "uuid", JsonSchemaType.Null) => typeof(Guid?),
("string", "char", JsonSchemaType.Null) => typeof(char?),
("boolean", null, JsonSchemaType.Null) => typeof(bool?),
("integer" or "number", "int32", true) => typeof(int?),
("integer" or "number", "int64", true) => typeof(long?),
("integer", null, true) => typeof(long?),
("number", "float", true) => typeof(float?),
("number", "double", true) => typeof(double?),
("number", null, true) => typeof(double?),
("number", "decimal", true) => typeof(decimal?),
("string", "byte", true) => typeof(byte?),
("string", "date-time", true) => typeof(DateTimeOffset?),
("string", "uuid", true) => typeof(Guid?),
("string", "char", true) => typeof(char?),
("boolean", null, true) => typeof(bool?),
("boolean", null, _) => typeof(bool),
// integer is technically not valid with format, but we must provide some compatibility
("integer" or "number", "int32", _) => typeof(int),
Expand Down
26 changes: 16 additions & 10 deletions src/Microsoft.OpenApi/Models/OpenApiSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ internal void WriteJsonSchemaKeywords(IOpenApiWriter writer)
internal void WriteAsItemsProperties(IOpenApiWriter writer)
{
// type
writer.WriteProperty(OpenApiConstants.Type, (Type & ~JsonSchemaType.Null).ToIdentifier());
writer.WriteProperty(OpenApiConstants.Type, (Type & ~JsonSchemaType.Null)?.FirstIdentifier());

// format
WriteFormatProperty(writer);
Expand Down Expand Up @@ -629,10 +629,10 @@ private void SerializeAsV2(
private void SerializeTypeProperty(JsonSchemaType? type, IOpenApiWriter writer, OpenApiSpecVersion version)
{
// check whether nullable is true for upcasting purposes
var isNullable = (Type.HasValue && Type.Value.HasFlag(JsonSchemaType.Null)) ||
var isNullable = (Type.HasValue && Type.Value.HasFlag(JsonSchemaType.Null)) ||
Extensions is not null &&
Extensions.TryGetValue(OpenApiConstants.NullableExtension, out var nullExtRawValue) &&
nullExtRawValue is OpenApiAny { Node: JsonNode jsonNode} &&
nullExtRawValue is OpenApiAny { Node: JsonNode jsonNode } &&
jsonNode.GetValueKind() is JsonValueKind.True;
if (type is null)
{
Expand All @@ -651,14 +651,14 @@ Extensions is not null &&
break;
case OpenApiSpecVersion.OpenApi3_0 when isNullable && type.Value == JsonSchemaType.Null:
writer.WriteProperty(OpenApiConstants.Nullable, true);
writer.WriteProperty(OpenApiConstants.Type, JsonSchemaType.Object.ToIdentifier());
writer.WriteProperty(OpenApiConstants.Type, JsonSchemaType.Object.FirstIdentifier());
break;
case OpenApiSpecVersion.OpenApi3_0 when isNullable && type.Value != JsonSchemaType.Null:
writer.WriteProperty(OpenApiConstants.Nullable, true);
writer.WriteProperty(OpenApiConstants.Type, type.Value.ToIdentifier());
writer.WriteProperty(OpenApiConstants.Type, type.Value.FirstIdentifier());
break;
default:
writer.WriteProperty(OpenApiConstants.Type, type.Value.ToIdentifier());
writer.WriteProperty(OpenApiConstants.Type, type.Value.FirstIdentifier());
break;
}
}
Expand All @@ -674,7 +674,13 @@ Extensions is not null &&
var list = (from JsonSchemaType flag in jsonSchemaTypeValues
where type.Value.HasFlag(flag)
select flag).ToList();
writer.WriteOptionalCollection(OpenApiConstants.Type, list, (w, s) => w.WriteValue(s.ToIdentifier()));
writer.WriteOptionalCollection(OpenApiConstants.Type, list, (w, s) =>
{
foreach(var item in s.ToIdentifier())
{
w.WriteValue(item);
}
});
}
}
}
Expand All @@ -697,7 +703,7 @@ private static void UpCastSchemaTypeToV31(JsonSchemaType type, IOpenApiWriter wr
var temporaryType = type | JsonSchemaType.Null;
var list = (from JsonSchemaType flag in jsonSchemaTypeValues// Check if the flag is set in 'type' using a bitwise AND operation
where temporaryType.HasFlag(flag)
select flag.ToIdentifier()).ToList();
select flag.FirstIdentifier()).ToList();
if (list.Count > 1)
{
writer.WriteOptionalCollection(OpenApiConstants.Type, list, (w, s) => w.WriteValue(s));
Expand Down Expand Up @@ -734,7 +740,7 @@ private void DowncastTypeArrayToV2OrV3(JsonSchemaType schemaType, IOpenApiWriter
if (schemaType.HasFlag(flag) && flag != JsonSchemaType.Null)
{
// Write the non-null flag value to the writer
writer.WriteProperty(OpenApiConstants.Type, flag.ToIdentifier());
writer.WriteProperty(OpenApiConstants.Type, flag.FirstIdentifier());
}
}
writer.WriteProperty(nullableProp, true);
Expand All @@ -747,7 +753,7 @@ private void DowncastTypeArrayToV2OrV3(JsonSchemaType schemaType, IOpenApiWriter
}
else
{
writer.WriteProperty(OpenApiConstants.Type, schemaType.ToIdentifier());
writer.WriteProperty(OpenApiConstants.Type, schemaType.FirstIdentifier());
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/Microsoft.OpenApi/Validations/Rules/RuleHelpers.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.OpenApi.Extensions;
Expand Down Expand Up @@ -55,7 +56,7 @@
// convert value to JsonElement and access the ValueKind property to determine the type.
var valueKind = value.GetValueKind();

var type = schema.Type.ToIdentifier();
var type = (schema.Type & ~JsonSchemaType.Null)?.FirstIdentifier();
var format = schema.Format;

// Before checking the type, check first if the schema allows null.
Expand Down Expand Up @@ -136,7 +137,7 @@
return;
}

if (type is "integer" or "number" && format is "int32")

Check warning on line 140 in src/Microsoft.OpenApi/Validations/Rules/RuleHelpers.cs

View workflow job for this annotation

GitHub Actions / Build

Define a constant instead of using this literal 'number' 5 times. (https://rules.sonarsource.com/csharp/RSPEC-1192)
{
if (valueKind is not JsonValueKind.Number)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using FluentAssertions;
using FluentAssertions.Equivalency;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Extensions;
using Microsoft.OpenApi.Models.Interfaces;
using Microsoft.OpenApi.Reader;
using Microsoft.OpenApi.Tests;
Expand All @@ -31,7 +32,7 @@ public static MemoryStream GetMemoryStream(string fileName)

public OpenApiSchemaTests()
{
OpenApiReaderRegistry.RegisterReader("yaml", new OpenApiYamlReader());
OpenApiReaderRegistry.RegisterReader("yaml", new OpenApiYamlReader());
}

[Fact]
Expand Down Expand Up @@ -298,8 +299,8 @@ public void CloningSchemaWithExamplesAndEnumsShouldSucceed()
clone.Default = 6;

// Assert
Assert.Equivalent(new int[] {1, 2, 3, 4}, clone.Enum.Select(static x => x.GetValue<int>()).ToArray());
Assert.Equivalent(new int[] {2, 3, 4}, clone.Examples.Select(static x => x.GetValue<int>()).ToArray());
Assert.Equivalent(new int[] { 1, 2, 3, 4 }, clone.Enum.Select(static x => x.GetValue<int>()).ToArray());
Assert.Equivalent(new int[] { 2, 3, 4 }, clone.Examples.Select(static x => x.GetValue<int>()).ToArray());
Assert.Equivalent(6, clone.Default.GetValue<int>());
}

Expand Down Expand Up @@ -398,7 +399,7 @@ public void SerializeSchemaWithTypeArrayAndNullableDoesntEmitType()
schema.SerializeAsV2(new OpenApiYamlWriter(writer));
var schemaString = writer.ToString();

Assert.Equal(expected.MakeLineBreaksEnvironmentNeutral(), schemaString.MakeLineBreaksEnvironmentNeutral());
Assert.Equal(expected.MakeLineBreaksEnvironmentNeutral(), schemaString.MakeLineBreaksEnvironmentNeutral());
}

[Theory]
Expand Down Expand Up @@ -506,7 +507,7 @@ public async Task ParseSchemaWithConstWorks()
}

[Fact]
public void ParseSchemaWithUnrecognizedKeywordsWorks()
public void ParseSchemaWithUnrecognizedKeywordsWorks()
{
var input = @"{
""type"": ""string"",
Expand All @@ -520,5 +521,36 @@ public void ParseSchemaWithUnrecognizedKeywordsWorks()
Assert.Equal(2, schema.UnrecognizedKeywords.Count);
}

[Theory]
[InlineData(JsonSchemaType.Integer | JsonSchemaType.String, new[] { "integer", "string" })]
[InlineData(JsonSchemaType.Integer | JsonSchemaType.Null, new[] { "integer", "null" })]
[InlineData(JsonSchemaType.Integer, new[] { "integer" })]
public void NormalizeFlaggableJsonSchemaTypeEnumWorks(JsonSchemaType type, string[] expected)
{
var schema = new OpenApiSchema
{
Type = type
};

var actual = schema.Type.ToIdentifier();
Assert.Equal(expected, actual);
}

[Theory]
[InlineData(new[] { "integer", "string" }, JsonSchemaType.Integer | JsonSchemaType.String)]
[InlineData(new[] { "integer", "null" }, JsonSchemaType.Integer | JsonSchemaType.Null)]
[InlineData(new[] { "integer" }, JsonSchemaType.Integer)]
public void ArrayIdentifierToEnumConversionWorks(string[] type, JsonSchemaType expected)
{
var actual = type.ToJsonSchemaType();
Assert.Equal(expected, actual);
}

[Fact]
public void StringIdentifierToEnumConversionWorks()
{
var actual = "integer".ToJsonSchemaType();
Assert.Equal(JsonSchemaType.Integer, actual);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,10 @@ namespace Microsoft.OpenApi.Extensions
{
public static System.Type MapOpenApiPrimitiveTypeToSimpleType(this Microsoft.OpenApi.Models.OpenApiSchema schema) { }
public static Microsoft.OpenApi.Models.OpenApiSchema MapTypeToOpenApiPrimitiveType(this System.Type type) { }
public static string? ToIdentifier(this Microsoft.OpenApi.Models.JsonSchemaType schemaType) { }
public static string? ToIdentifier(this Microsoft.OpenApi.Models.JsonSchemaType? schemaType) { }
public static string[] ToIdentifier(this Microsoft.OpenApi.Models.JsonSchemaType schemaType) { }
public static string[]? ToIdentifier(this Microsoft.OpenApi.Models.JsonSchemaType? schemaType) { }
public static Microsoft.OpenApi.Models.JsonSchemaType ToJsonSchemaType(this string identifier) { }
public static Microsoft.OpenApi.Models.JsonSchemaType? ToJsonSchemaType(this string[] identifier) { }
}
}
namespace Microsoft.OpenApi.Interfaces
Expand Down