Skip to content

Commit b80e934

Browse files
MaggieKimani1baywetdarrelmiller
authored
fix: refactor ToIdentifier() to normalize flaggable enums (#2156)
* fix: refactor ToIdentifier() to normalize flaggable enums * fix: add support for casting type array to a flaggable enum * chore: add tests and update public API * Update src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs Co-authored-by: Vincent Biret <[email protected]> * chore: address PR feedback * chore: make method internal; update public API * Update src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs Co-authored-by: Darrel <[email protected]> * Update src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs Co-authored-by: Darrel <[email protected]> * fix: address more PR feedback, add and fix tests * Update src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs Co-authored-by: Vincent Biret <[email protected]> * Update src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs Co-authored-by: Vincent Biret <[email protected]> * Update src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs Co-authored-by: Vincent Biret <[email protected]> * chore: clean up --------- Co-authored-by: Vincent Biret <[email protected]> Co-authored-by: Darrel <[email protected]>
1 parent 75d7a66 commit b80e934

File tree

5 files changed

+161
-62
lines changed

5 files changed

+161
-62
lines changed

src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs

+91-44
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Linq;
67
using Microsoft.OpenApi.Exceptions;
78
using Microsoft.OpenApi.Models;
89

@@ -19,34 +20,61 @@ public static class OpenApiTypeMapper
1920
/// </summary>
2021
/// <param name="schemaType"></param>
2122
/// <returns></returns>
22-
public static string? ToIdentifier(this JsonSchemaType? schemaType)
23+
public static string[]? ToIdentifiers(this JsonSchemaType? schemaType)
2324
{
2425
if (schemaType is null)
2526
{
2627
return null;
2728
}
28-
return schemaType.Value.ToIdentifier();
29+
return schemaType.Value.ToIdentifiers();
2930
}
3031

3132
/// <summary>
3233
/// Maps a JsonSchema data type to an identifier.
3334
/// </summary>
3435
/// <param name="schemaType"></param>
3536
/// <returns></returns>
36-
public static string? ToIdentifier(this JsonSchemaType schemaType)
37+
public static string[] ToIdentifiers(this JsonSchemaType schemaType)
3738
{
38-
return schemaType switch
39-
{
40-
JsonSchemaType.Null => "null",
41-
JsonSchemaType.Boolean => "boolean",
42-
JsonSchemaType.Integer => "integer",
43-
JsonSchemaType.Number => "number",
44-
JsonSchemaType.String => "string",
45-
JsonSchemaType.Array => "array",
46-
JsonSchemaType.Object => "object",
47-
_ => null,
48-
};
39+
return schemaType.ToIdentifiersInternal().ToArray();
40+
}
41+
42+
private static readonly Dictionary<JsonSchemaType, string> allSchemaTypes = new()
43+
{
44+
{ JsonSchemaType.Boolean, "boolean" },
45+
{ JsonSchemaType.Integer, "integer" },
46+
{ JsonSchemaType.Number, "number" },
47+
{ JsonSchemaType.String, "string" },
48+
{ JsonSchemaType.Object, "object" },
49+
{ JsonSchemaType.Array, "array" },
50+
{ JsonSchemaType.Null, "null" }
51+
};
52+
53+
private static IEnumerable<string> ToIdentifiersInternal(this JsonSchemaType schemaType)
54+
{
55+
return allSchemaTypes.Where(kvp => schemaType.HasFlag(kvp.Key)).Select(static kvp => kvp.Value);
56+
}
57+
58+
/// <summary>
59+
/// Returns the first identifier from a string array.
60+
/// </summary>
61+
/// <param name="schemaType"></param>
62+
/// <returns></returns>
63+
internal static string ToFirstIdentifier(this JsonSchemaType schemaType)
64+
{
65+
return schemaType.ToIdentifiersInternal().First();
66+
}
67+
68+
/// <summary>
69+
/// Returns a single identifier from an array with only one item.
70+
/// </summary>
71+
/// <param name="schemaType"></param>
72+
/// <returns></returns>
73+
internal static string ToSingleIdentifier(this JsonSchemaType schemaType)
74+
{
75+
return schemaType.ToIdentifiersInternal().Single();
4976
}
77+
5078
#nullable restore
5179

5280
/// <summary>
@@ -70,6 +98,26 @@ public static JsonSchemaType ToJsonSchemaType(this string identifier)
7098
};
7199
}
72100

101+
/// <summary>
102+
/// Converts a schema type's identifier into the enum equivalent
103+
/// </summary>
104+
/// <param name="identifier"></param>
105+
/// <returns></returns>
106+
public static JsonSchemaType? ToJsonSchemaType(this string[] identifier)
107+
{
108+
if (identifier == null)
109+
{
110+
return null;
111+
}
112+
113+
JsonSchemaType type = 0;
114+
foreach (var id in identifier)
115+
{
116+
type |= id.ToJsonSchemaType();
117+
}
118+
return type;
119+
}
120+
73121
private static readonly Dictionary<Type, Func<OpenApiSchema>> _simpleTypeToOpenApiSchema = new()
74122
{
75123
[typeof(bool)] = () => new() { Type = JsonSchemaType.Boolean },
@@ -141,7 +189,7 @@ public static OpenApiSchema MapTypeToOpenApiPrimitiveType(this Type type)
141189
}
142190

143191
/// <summary>
144-
/// Maps an JsonSchema data type and format to a simple type.
192+
/// Maps a JsonSchema data type and format to a simple type.
145193
/// </summary>
146194
/// <param name="schema">The OpenApi data type</param>
147195
/// <returns>The simple type</returns>
@@ -153,37 +201,36 @@ public static Type MapOpenApiPrimitiveTypeToSimpleType(this OpenApiSchema schema
153201
throw new ArgumentNullException(nameof(schema));
154202
}
155203

156-
var type = ((schema.Type & ~JsonSchemaType.Null).ToIdentifier(), schema.Format?.ToLowerInvariant(), schema.Type & JsonSchemaType.Null) switch
204+
var type = (schema.Type, schema.Format?.ToLowerInvariant()) switch
157205
{
158-
("integer" or "number", "int32", JsonSchemaType.Null) => typeof(int?),
159-
("integer" or "number", "int64", JsonSchemaType.Null) => typeof(long?),
160-
("integer", null, JsonSchemaType.Null) => typeof(long?),
161-
("number", "float", JsonSchemaType.Null) => typeof(float?),
162-
("number", "double", JsonSchemaType.Null) => typeof(double?),
163-
("number", null, JsonSchemaType.Null) => typeof(double?),
164-
("number", "decimal", JsonSchemaType.Null) => typeof(decimal?),
165-
("string", "byte", JsonSchemaType.Null) => typeof(byte?),
166-
("string", "date-time", JsonSchemaType.Null) => typeof(DateTimeOffset?),
167-
("string", "uuid", JsonSchemaType.Null) => typeof(Guid?),
168-
("string", "char", JsonSchemaType.Null) => typeof(char?),
169-
("boolean", null, JsonSchemaType.Null) => typeof(bool?),
170-
("boolean", null, _) => typeof(bool),
206+
(JsonSchemaType.Integer | JsonSchemaType.Null or JsonSchemaType.Number | JsonSchemaType.Null, "int32") => typeof(int?),
207+
(JsonSchemaType.Integer | JsonSchemaType.Null or JsonSchemaType.Number | JsonSchemaType.Null, "int64") => typeof(long?),
208+
(JsonSchemaType.Integer | JsonSchemaType.Null, null) => typeof(long?),
209+
(JsonSchemaType.Number | JsonSchemaType.Null, "float") => typeof(float?),
210+
(JsonSchemaType.Number | JsonSchemaType.Null, "double") => typeof(double?),
211+
(JsonSchemaType.Number | JsonSchemaType.Null, null) => typeof(double?),
212+
(JsonSchemaType.Number | JsonSchemaType.Null, "decimal") => typeof(decimal?),
213+
(JsonSchemaType.String | JsonSchemaType.Null, "byte") => typeof(byte?),
214+
(JsonSchemaType.String | JsonSchemaType.Null, "date-time") => typeof(DateTimeOffset?),
215+
(JsonSchemaType.String | JsonSchemaType.Null, "uuid") => typeof(Guid?),
216+
(JsonSchemaType.String | JsonSchemaType.Null, "char") => typeof(char?),
217+
(JsonSchemaType.Boolean | JsonSchemaType.Null, null) => typeof(bool?),
218+
(JsonSchemaType.Boolean, null) => typeof(bool),
171219
// integer is technically not valid with format, but we must provide some compatibility
172-
("integer" or "number", "int32", _) => typeof(int),
173-
("integer" or "number", "int64", _) => typeof(long),
174-
("integer", null, _) => typeof(long),
175-
("number", "float", _) => typeof(float),
176-
("number", "double", _) => typeof(double),
177-
("number", "decimal", _) => typeof(decimal),
178-
("number", null, _) => typeof(double),
179-
("string", "byte", _) => typeof(byte),
180-
("string", "date-time", _) => typeof(DateTimeOffset),
181-
("string", "uuid", _) => typeof(Guid),
182-
("string", "duration", _) => typeof(TimeSpan),
183-
("string", "char", _) => typeof(char),
184-
("string", null, _) => typeof(string),
185-
("object", null, _) => typeof(object),
186-
("string", "uri", _) => typeof(Uri),
220+
(JsonSchemaType.Integer or JsonSchemaType.Number, "int32") => typeof(int),
221+
(JsonSchemaType.Integer or JsonSchemaType.Number, "int64") => typeof(long),
222+
(JsonSchemaType.Integer, null) => typeof(long),
223+
(JsonSchemaType.Number, "float") => typeof(float),
224+
(JsonSchemaType.Number, "double") => typeof(double),
225+
(JsonSchemaType.Number, "decimal") => typeof(decimal),
226+
(JsonSchemaType.Number, null) => typeof(double),
227+
(JsonSchemaType.String, "byte") => typeof(byte),
228+
(JsonSchemaType.String, "date-time") => typeof(DateTimeOffset),
229+
(JsonSchemaType.String, "uuid") => typeof(Guid),
230+
(JsonSchemaType.String, "char") => typeof(char),
231+
(JsonSchemaType.String, null) => typeof(string),
232+
(JsonSchemaType.Object, null) => typeof(object),
233+
(JsonSchemaType.String, "uri") => typeof(Uri),
187234
_ => typeof(string),
188235
};
189236

src/Microsoft.OpenApi/Models/OpenApiSchema.cs

+16-10
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ internal void WriteJsonSchemaKeywords(IOpenApiWriter writer)
418418
internal void WriteAsItemsProperties(IOpenApiWriter writer)
419419
{
420420
// type
421-
writer.WriteProperty(OpenApiConstants.Type, (Type & ~JsonSchemaType.Null).ToIdentifier());
421+
writer.WriteProperty(OpenApiConstants.Type, (Type & ~JsonSchemaType.Null)?.ToFirstIdentifier());
422422

423423
// format
424424
WriteFormatProperty(writer);
@@ -634,10 +634,10 @@ private void SerializeAsV2(
634634
private void SerializeTypeProperty(JsonSchemaType? type, IOpenApiWriter writer, OpenApiSpecVersion version)
635635
{
636636
// check whether nullable is true for upcasting purposes
637-
var isNullable = (Type.HasValue && Type.Value.HasFlag(JsonSchemaType.Null)) ||
637+
var isNullable = (Type.HasValue && Type.Value.HasFlag(JsonSchemaType.Null)) ||
638638
Extensions is not null &&
639639
Extensions.TryGetValue(OpenApiConstants.NullableExtension, out var nullExtRawValue) &&
640-
nullExtRawValue is OpenApiAny { Node: JsonNode jsonNode} &&
640+
nullExtRawValue is OpenApiAny { Node: JsonNode jsonNode } &&
641641
jsonNode.GetValueKind() is JsonValueKind.True;
642642
if (type is null)
643643
{
@@ -656,14 +656,14 @@ Extensions is not null &&
656656
break;
657657
case OpenApiSpecVersion.OpenApi3_0 when isNullable && type.Value == JsonSchemaType.Null:
658658
writer.WriteProperty(OpenApiConstants.Nullable, true);
659-
writer.WriteProperty(OpenApiConstants.Type, JsonSchemaType.Object.ToIdentifier());
659+
writer.WriteProperty(OpenApiConstants.Type, JsonSchemaType.Object.ToFirstIdentifier());
660660
break;
661661
case OpenApiSpecVersion.OpenApi3_0 when isNullable && type.Value != JsonSchemaType.Null:
662662
writer.WriteProperty(OpenApiConstants.Nullable, true);
663-
writer.WriteProperty(OpenApiConstants.Type, type.Value.ToIdentifier());
663+
writer.WriteProperty(OpenApiConstants.Type, type.Value.ToFirstIdentifier());
664664
break;
665665
default:
666-
writer.WriteProperty(OpenApiConstants.Type, type.Value.ToIdentifier());
666+
writer.WriteProperty(OpenApiConstants.Type, type.Value.ToFirstIdentifier());
667667
break;
668668
}
669669
}
@@ -679,7 +679,13 @@ Extensions is not null &&
679679
var list = (from JsonSchemaType flag in jsonSchemaTypeValues
680680
where type.Value.HasFlag(flag)
681681
select flag).ToList();
682-
writer.WriteOptionalCollection(OpenApiConstants.Type, list, (w, s) => w.WriteValue(s.ToIdentifier()));
682+
writer.WriteOptionalCollection(OpenApiConstants.Type, list, (w, s) =>
683+
{
684+
foreach(var item in s.ToIdentifiers())
685+
{
686+
w.WriteValue(item);
687+
}
688+
});
683689
}
684690
}
685691
}
@@ -702,7 +708,7 @@ private static void UpCastSchemaTypeToV31(JsonSchemaType type, IOpenApiWriter wr
702708
var temporaryType = type | JsonSchemaType.Null;
703709
var list = (from JsonSchemaType flag in jsonSchemaTypeValues// Check if the flag is set in 'type' using a bitwise AND operation
704710
where temporaryType.HasFlag(flag)
705-
select flag.ToIdentifier()).ToList();
711+
select flag.ToFirstIdentifier()).ToList();
706712
if (list.Count > 1)
707713
{
708714
writer.WriteOptionalCollection(OpenApiConstants.Type, list, (w, s) => w.WriteValue(s));
@@ -739,7 +745,7 @@ private void DowncastTypeArrayToV2OrV3(JsonSchemaType schemaType, IOpenApiWriter
739745
if (schemaType.HasFlag(flag) && flag != JsonSchemaType.Null)
740746
{
741747
// Write the non-null flag value to the writer
742-
writer.WriteProperty(OpenApiConstants.Type, flag.ToIdentifier());
748+
writer.WriteProperty(OpenApiConstants.Type, flag.ToFirstIdentifier());
743749
}
744750
}
745751
writer.WriteProperty(nullableProp, true);
@@ -752,7 +758,7 @@ private void DowncastTypeArrayToV2OrV3(JsonSchemaType schemaType, IOpenApiWriter
752758
}
753759
else
754760
{
755-
writer.WriteProperty(OpenApiConstants.Type, schemaType.ToIdentifier());
761+
writer.WriteProperty(OpenApiConstants.Type, schemaType.ToFirstIdentifier());
756762
}
757763
}
758764
}

src/Microsoft.OpenApi/Validations/Rules/RuleHelpers.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33

4+
using System.Linq;
45
using System.Text.Json;
56
using System.Text.Json.Nodes;
67
using Microsoft.OpenApi.Extensions;
@@ -55,7 +56,7 @@ public static void ValidateDataTypeMismatch(
5556
// convert value to JsonElement and access the ValueKind property to determine the type.
5657
var valueKind = value.GetValueKind();
5758

58-
var type = schema.Type.ToIdentifier();
59+
var type = (schema.Type & ~JsonSchemaType.Null)?.ToFirstIdentifier();
5960
var format = schema.Format;
6061

6162
// Before checking the type, check first if the schema allows null.

test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs

+49-5
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@
99
using FluentAssertions;
1010
using FluentAssertions.Equivalency;
1111
using Microsoft.OpenApi.Models;
12+
using Microsoft.OpenApi.Extensions;
1213
using Microsoft.OpenApi.Models.Interfaces;
1314
using Microsoft.OpenApi.Reader;
1415
using Microsoft.OpenApi.Tests;
1516
using Microsoft.OpenApi.Writers;
1617
using Xunit;
18+
using Microsoft.OpenApi.Exceptions;
19+
using System;
1720

1821
namespace Microsoft.OpenApi.Readers.Tests.V31Tests
1922
{
@@ -31,7 +34,7 @@ public static MemoryStream GetMemoryStream(string fileName)
3134

3235
public OpenApiSchemaTests()
3336
{
34-
OpenApiReaderRegistry.RegisterReader("yaml", new OpenApiYamlReader());
37+
OpenApiReaderRegistry.RegisterReader("yaml", new OpenApiYamlReader());
3538
}
3639

3740
[Fact]
@@ -317,8 +320,8 @@ public void CloningSchemaWithExamplesAndEnumsShouldSucceed()
317320
clone.Default = 6;
318321

319322
// Assert
320-
Assert.Equivalent(new int[] {1, 2, 3, 4}, clone.Enum.Select(static x => x.GetValue<int>()).ToArray());
321-
Assert.Equivalent(new int[] {2, 3, 4}, clone.Examples.Select(static x => x.GetValue<int>()).ToArray());
323+
Assert.Equivalent(new int[] { 1, 2, 3, 4 }, clone.Enum.Select(static x => x.GetValue<int>()).ToArray());
324+
Assert.Equivalent(new int[] { 2, 3, 4 }, clone.Examples.Select(static x => x.GetValue<int>()).ToArray());
322325
Assert.Equivalent(6, clone.Default.GetValue<int>());
323326
}
324327

@@ -417,7 +420,7 @@ public void SerializeSchemaWithTypeArrayAndNullableDoesntEmitType()
417420
schema.SerializeAsV2(new OpenApiYamlWriter(writer));
418421
var schemaString = writer.ToString();
419422

420-
Assert.Equal(expected.MakeLineBreaksEnvironmentNeutral(), schemaString.MakeLineBreaksEnvironmentNeutral());
423+
Assert.Equal(expected.MakeLineBreaksEnvironmentNeutral(), schemaString.MakeLineBreaksEnvironmentNeutral());
421424
}
422425

423426
[Theory]
@@ -525,7 +528,7 @@ public async Task ParseSchemaWithConstWorks()
525528
}
526529

527530
[Fact]
528-
public void ParseSchemaWithUnrecognizedKeywordsWorks()
531+
public void ParseSchemaWithUnrecognizedKeywordsWorks()
529532
{
530533
var input = @"{
531534
""type"": ""string"",
@@ -539,5 +542,46 @@ public void ParseSchemaWithUnrecognizedKeywordsWorks()
539542
Assert.Equal(2, schema.UnrecognizedKeywords.Count);
540543
}
541544

545+
[Theory]
546+
[InlineData(JsonSchemaType.Integer | JsonSchemaType.String, new[] { "integer", "string" })]
547+
[InlineData(JsonSchemaType.Integer | JsonSchemaType.Null, new[] { "integer", "null" })]
548+
[InlineData(JsonSchemaType.Integer, new[] { "integer" })]
549+
public void NormalizeFlaggableJsonSchemaTypeEnumWorks(JsonSchemaType type, string[] expected)
550+
{
551+
var schema = new OpenApiSchema
552+
{
553+
Type = type
554+
};
555+
556+
var actual = schema.Type.ToIdentifiers();
557+
Assert.Equal(expected, actual);
558+
}
559+
560+
[Theory]
561+
[InlineData(new[] { "integer", "string" }, JsonSchemaType.Integer | JsonSchemaType.String)]
562+
[InlineData(new[] { "integer", "null" }, JsonSchemaType.Integer | JsonSchemaType.Null)]
563+
[InlineData(new[] { "integer" }, JsonSchemaType.Integer)]
564+
public void ArrayIdentifierToEnumConversionWorks(string[] type, JsonSchemaType expected)
565+
{
566+
var actual = type.ToJsonSchemaType();
567+
Assert.Equal(expected, actual);
568+
}
569+
570+
[Fact]
571+
public void StringIdentifierToEnumConversionWorks()
572+
{
573+
var actual = "integer".ToJsonSchemaType();
574+
Assert.Equal(JsonSchemaType.Integer, actual);
575+
}
576+
577+
[Fact]
578+
public void ReturnSingleIdentifierWorks()
579+
{
580+
var type = JsonSchemaType.Integer;
581+
var types = JsonSchemaType.Integer | JsonSchemaType.Null;
582+
583+
Assert.Equal("integer", type.ToSingleIdentifier());
584+
Assert.Throws<InvalidOperationException>(() => types.ToSingleIdentifier());
585+
}
542586
}
543587
}

test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt

+3-2
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,10 @@ namespace Microsoft.OpenApi.Extensions
190190
{
191191
public static System.Type MapOpenApiPrimitiveTypeToSimpleType(this Microsoft.OpenApi.Models.OpenApiSchema schema) { }
192192
public static Microsoft.OpenApi.Models.OpenApiSchema MapTypeToOpenApiPrimitiveType(this System.Type type) { }
193-
public static string? ToIdentifier(this Microsoft.OpenApi.Models.JsonSchemaType schemaType) { }
194-
public static string? ToIdentifier(this Microsoft.OpenApi.Models.JsonSchemaType? schemaType) { }
193+
public static string[] ToIdentifiers(this Microsoft.OpenApi.Models.JsonSchemaType schemaType) { }
194+
public static string[]? ToIdentifiers(this Microsoft.OpenApi.Models.JsonSchemaType? schemaType) { }
195195
public static Microsoft.OpenApi.Models.JsonSchemaType ToJsonSchemaType(this string identifier) { }
196+
public static Microsoft.OpenApi.Models.JsonSchemaType? ToJsonSchemaType(this string[] identifier) { }
196197
}
197198
}
198199
namespace Microsoft.OpenApi.Interfaces

0 commit comments

Comments
 (0)