From 495d8e635e03b427403624e7971ad081015322c3 Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Thu, 1 Dec 2022 10:40:37 +0300 Subject: [PATCH 1/6] Add OpenApiTypeMapper that maps .NET primitive types to the OpenAPI schema type equivalent --- .../Extensions/OpenApiTypeMapper.cs | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs b/src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs new file mode 100644 index 000000000..42b645b3a --- /dev/null +++ b/src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Extensions +{ + /// + /// Extension methods for . + /// + public static class OpenApiTypeMapper + { + private static readonly Dictionary> _simpleTypeToOpenApiSchema = new() + { + [typeof(bool)] = () => new OpenApiSchema { Type = "boolean" }, + [typeof(byte)] = () => new OpenApiSchema { Type = "string", Format = "byte" }, + [typeof(int)] = () => new OpenApiSchema { Type = "integer", Format = "int32" }, + [typeof(uint)] = () => new OpenApiSchema { Type = "integer", Format = "int32" }, + [typeof(long)] = () => new OpenApiSchema { Type = "integer", Format = "int64" }, + [typeof(ulong)] = () => new OpenApiSchema { Type = "integer", Format = "int64" }, + [typeof(float)] = () => new OpenApiSchema { Type = "number", Format = "float" }, + [typeof(double)] = () => new OpenApiSchema { Type = "number", Format = "double" }, + [typeof(decimal)] = () => new OpenApiSchema { Type = "number", Format = "double" }, + [typeof(DateTime)] = () => new OpenApiSchema { Type = "string", Format = "date-time" }, + [typeof(DateTimeOffset)] = () => new OpenApiSchema { Type = "string", Format = "date-time" }, + [typeof(Guid)] = () => new OpenApiSchema { Type = "string", Format = "uuid" }, + [typeof(char)] = () => new OpenApiSchema { Type = "string" }, + + // Nullable types + [typeof(bool?)] = () => new OpenApiSchema { Type = "boolean", Nullable = true }, + [typeof(byte?)] = () => new OpenApiSchema { Type = "string", Format = "byte", Nullable = true }, + [typeof(int?)] = () => new OpenApiSchema { Type = "integer", Format = "int32", Nullable = true }, + [typeof(uint?)] = () => new OpenApiSchema { Type = "integer", Format = "int32", Nullable = true }, + [typeof(long?)] = () => new OpenApiSchema { Type = "integer", Format = "int64", Nullable = true }, + [typeof(ulong?)] = () => new OpenApiSchema { Type = "integer", Format = "int64", Nullable = true }, + [typeof(float?)] = () => new OpenApiSchema { Type = "number", Format = "float", Nullable = true }, + [typeof(double?)] = () => new OpenApiSchema { Type = "number", Format = "double", Nullable = true }, + [typeof(decimal?)] = () => new OpenApiSchema { Type = "number", Format = "double", Nullable = true }, + [typeof(DateTime?)] = () => new OpenApiSchema { Type = "string", Format = "date-time", Nullable = true }, + [typeof(DateTimeOffset?)] = () => new OpenApiSchema { Type = "string", Format = "date-time", Nullable = true }, + [typeof(Guid?)] = () => new OpenApiSchema { Type = "string", Format = "uuid", Nullable = true }, + [typeof(char?)] = () => new OpenApiSchema { Type = "string", Nullable = true }, + + [typeof(Uri)] = () => new OpenApiSchema { Type = "string" }, // Uri is treated as simple string + [typeof(string)] = () => new OpenApiSchema { Type = "string" }, + [typeof(object)] = () => new OpenApiSchema { Type = "object" } + }; + + /// + /// Maps a simple type to an OpenAPI data type and format. + /// + /// Simple type. + /// + /// All the following types from http://swagger.io/specification/#data-types-12 are supported. + /// Other types including nullables and URL are also supported. + /// Common Name type format Comments + /// =========== ======= ====== ========================================= + /// integer integer int32 signed 32 bits + /// long integer int64 signed 64 bits + /// float number float + /// double number double + /// string string [empty] + /// byte string byte base64 encoded characters + /// binary string binary any sequence of octets + /// boolean boolean [empty] + /// date string date As defined by full-date - RFC3339 + /// dateTime string date-time As defined by date-time - RFC3339 + /// password string password Used to hint UIs the input needs to be obscured. + /// If the type is not recognized as "simple", System.String will be returned. + /// + public static OpenApiSchema MapTypeToOpenApiPrimitiveType(this Type type) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + return _simpleTypeToOpenApiSchema.TryGetValue(type, out var result) + ? result() + : new OpenApiSchema { Type = "string" }; + } + } +} From 83db7027a2758b3a8a8215a9096e3209fc5d2737 Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Thu, 1 Dec 2022 10:40:57 +0300 Subject: [PATCH 2/6] Add test and update public API interface --- .../Extensions/OpenApiTypeMapperTests.cs | 35 +++++++++++++++++++ .../PublicApi/PublicApi.approved.txt | 4 +++ 2 files changed, 39 insertions(+) create mode 100644 test/Microsoft.OpenApi.Tests/Extensions/OpenApiTypeMapperTests.cs diff --git a/test/Microsoft.OpenApi.Tests/Extensions/OpenApiTypeMapperTests.cs b/test/Microsoft.OpenApi.Tests/Extensions/OpenApiTypeMapperTests.cs new file mode 100644 index 000000000..450103e2c --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Extensions/OpenApiTypeMapperTests.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using FluentAssertions; +using Microsoft.OpenApi.Extensions; +using Microsoft.OpenApi.Models; +using Xunit; + +namespace Microsoft.OpenApi.Tests.Extensions +{ + public class OpenApiTypeMapperTests + { + [Theory] + [MemberData(nameof(PrimitiveTypeData))] + public void MapTypeToOpenApiPrimitiveTypeShouldSucceed(Type type, OpenApiSchema expected) + { + // Arrange & Act + var actual = OpenApiTypeMapper.MapTypeToOpenApiPrimitiveType(type); + + // Assert + actual.Should().BeEquivalentTo(expected); + } + + public static IEnumerable PrimitiveTypeData => new List + { + new object[] { typeof(int), new OpenApiSchema { Type = "integer", Format = "int32" } }, + new object[] { typeof(string), new OpenApiSchema { Type = "string" } }, + new object[] { typeof(double), new OpenApiSchema { Type = "number", Format = "double" } }, + new object[] { typeof(float?), new OpenApiSchema { Type = "number", Format = "float", Nullable = true } }, + new object[] { typeof(DateTimeOffset), new OpenApiSchema { Type = "string", Format = "date-time" } } + }; + } +} diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt index b8646cae5..98ed35ecb 100755 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt @@ -282,6 +282,10 @@ namespace Microsoft.OpenApi.Extensions public static void SerializeAsYaml(this T element, System.IO.Stream stream, Microsoft.OpenApi.OpenApiSpecVersion specVersion) where T : Microsoft.OpenApi.Interfaces.IOpenApiSerializable { } } + public static class OpenApiTypeMapper + { + public static Microsoft.OpenApi.Models.OpenApiSchema MapTypeToOpenApiPrimitiveType(this System.Type type) { } + } public static class StringExtensions { public static T GetEnumFromDisplayName(this string displayName) { } From 0b141fa92a0cebe4d056d00602a9158cbcfc84dd Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Tue, 6 Dec 2022 10:23:47 +0300 Subject: [PATCH 3/6] Map OpenApiSchema types to simple type and add test to validate --- .../Extensions/OpenApiTypeMapper.cs | 47 ++++++++++++++++++- .../Extensions/OpenApiTypeMapperTests.cs | 34 +++++++++++--- 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs b/src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs index 42b645b3a..ec7235271 100644 --- a/src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs +++ b/src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs @@ -42,7 +42,7 @@ public static class OpenApiTypeMapper [typeof(DateTimeOffset?)] = () => new OpenApiSchema { Type = "string", Format = "date-time", Nullable = true }, [typeof(Guid?)] = () => new OpenApiSchema { Type = "string", Format = "uuid", Nullable = true }, [typeof(char?)] = () => new OpenApiSchema { Type = "string", Nullable = true }, - + [typeof(Uri)] = () => new OpenApiSchema { Type = "string" }, // Uri is treated as simple string [typeof(string)] = () => new OpenApiSchema { Type = "string" }, [typeof(object)] = () => new OpenApiSchema { Type = "object" } @@ -81,5 +81,50 @@ public static OpenApiSchema MapTypeToOpenApiPrimitiveType(this Type type) ? result() : new OpenApiSchema { Type = "string" }; } + + /// + /// Maps an OpenAPI data type and format to a simple type. + /// + /// The OpenApi data type + /// The simple type + /// + public static Type MapOpenApiPrimitiveTypeToSimpleType(this OpenApiSchema schema) + { + if (schema == null) + { + throw new ArgumentNullException(nameof(schema)); + } + + var type = (schema.Type, schema.Format, schema.Nullable) switch + { + ("boolean", null, false) => typeof(bool), + ("integer", "int32", false) => typeof(int), + ("integer", "int64", false) => typeof(long), + ("number", "float", false) => typeof(float), + ("number", "double", false) => typeof(double), + ("number", "decimal", false) => typeof(decimal), + ("string", "byte", false) => typeof(byte), + ("string", "date-time", false) => typeof(DateTimeOffset), + ("string", "uuid", false) => typeof(Guid), + ("string", "duration", false) => typeof(TimeSpan), + ("string", "char", false) => typeof(char), + ("string", null, false) => typeof(string), + ("object", null, false) => typeof(object), + ("string", "uri", false) => typeof(Uri), + ("integer", "int32", true) => typeof(int?), + ("integer", "int64", true) => typeof(long?), + ("number", "float", true) => typeof(float?), + ("number", "double", 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?), + _ => typeof(string), + }; + + return type; + } } } diff --git a/test/Microsoft.OpenApi.Tests/Extensions/OpenApiTypeMapperTests.cs b/test/Microsoft.OpenApi.Tests/Extensions/OpenApiTypeMapperTests.cs index 450103e2c..c2b6d9597 100644 --- a/test/Microsoft.OpenApi.Tests/Extensions/OpenApiTypeMapperTests.cs +++ b/test/Microsoft.OpenApi.Tests/Extensions/OpenApiTypeMapperTests.cs @@ -12,6 +12,24 @@ namespace Microsoft.OpenApi.Tests.Extensions { public class OpenApiTypeMapperTests { + public static IEnumerable PrimitiveTypeData => new List + { + new object[] { typeof(int), new OpenApiSchema { Type = "integer", Format = "int32" } }, + new object[] { typeof(string), new OpenApiSchema { Type = "string" } }, + new object[] { typeof(double), new OpenApiSchema { Type = "number", Format = "double" } }, + new object[] { typeof(float?), new OpenApiSchema { Type = "number", Format = "float", Nullable = true } }, + new object[] { typeof(DateTimeOffset), new OpenApiSchema { Type = "string", Format = "date-time" } } + }; + + public static IEnumerable OpenApiDataTypes => new List + { + new object[] { new OpenApiSchema { Type = "integer", Format = "int32"}, typeof(int) }, + new object[] { new OpenApiSchema { Type = "string" }, typeof(string) }, + new object[] { new OpenApiSchema { Type = "number", Format = "double" }, typeof(double) }, + new object[] { new OpenApiSchema { Type = "number", Format = "float", Nullable = true }, typeof(float?) }, + new object[] { new OpenApiSchema { Type = "string", Format = "date-time" }, typeof(DateTimeOffset) } + }; + [Theory] [MemberData(nameof(PrimitiveTypeData))] public void MapTypeToOpenApiPrimitiveTypeShouldSucceed(Type type, OpenApiSchema expected) @@ -23,13 +41,15 @@ public void MapTypeToOpenApiPrimitiveTypeShouldSucceed(Type type, OpenApiSchema actual.Should().BeEquivalentTo(expected); } - public static IEnumerable PrimitiveTypeData => new List + [Theory] + [MemberData(nameof(OpenApiDataTypes))] + public void MapOpenApiSchemaTypeToSimpleTypeShouldSucceed(OpenApiSchema schema, Type expected) { - new object[] { typeof(int), new OpenApiSchema { Type = "integer", Format = "int32" } }, - new object[] { typeof(string), new OpenApiSchema { Type = "string" } }, - new object[] { typeof(double), new OpenApiSchema { Type = "number", Format = "double" } }, - new object[] { typeof(float?), new OpenApiSchema { Type = "number", Format = "float", Nullable = true } }, - new object[] { typeof(DateTimeOffset), new OpenApiSchema { Type = "string", Format = "date-time" } } - }; + // Arrange & Act + var actual = OpenApiTypeMapper.MapOpenApiPrimitiveTypeToSimpleType(schema); + + // Assert + actual.Should().Be(expected); + } } } From dca31513f5dafa879c2a0b36fe85dc174faa2b1e Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Tue, 6 Dec 2022 17:42:48 +0300 Subject: [PATCH 4/6] Update Uri format --- src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs b/src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs index ec7235271..d8023b4cb 100644 --- a/src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs +++ b/src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs @@ -43,7 +43,7 @@ public static class OpenApiTypeMapper [typeof(Guid?)] = () => new OpenApiSchema { Type = "string", Format = "uuid", Nullable = true }, [typeof(char?)] = () => new OpenApiSchema { Type = "string", Nullable = true }, - [typeof(Uri)] = () => new OpenApiSchema { Type = "string" }, // Uri is treated as simple string + [typeof(Uri)] = () => new OpenApiSchema { Type = "string", Format = "uri"}, // Uri is treated as simple string [typeof(string)] = () => new OpenApiSchema { Type = "string" }, [typeof(object)] = () => new OpenApiSchema { Type = "object" } }; From 2ad17ba74a3604f405b490a201c17e74966908a9 Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Tue, 6 Dec 2022 18:00:16 +0300 Subject: [PATCH 5/6] Update type and format to lowercase --- src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs b/src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs index d8023b4cb..16441a9e7 100644 --- a/src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs +++ b/src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs @@ -95,7 +95,7 @@ public static Type MapOpenApiPrimitiveTypeToSimpleType(this OpenApiSchema schema throw new ArgumentNullException(nameof(schema)); } - var type = (schema.Type, schema.Format, schema.Nullable) switch + var type = (schema.Type?.ToLowerInvariant(), schema.Format.ToLowerInvariant(), schema.Nullable) switch { ("boolean", null, false) => typeof(bool), ("integer", "int32", false) => typeof(int), From 0b4110aa9d296a220ce3516eb94949655d614cdf Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Tue, 6 Dec 2022 19:01:56 +0300 Subject: [PATCH 6/6] Fix failing tests --- src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs | 2 +- test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs b/src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs index 16441a9e7..970b3a976 100644 --- a/src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs +++ b/src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs @@ -95,7 +95,7 @@ public static Type MapOpenApiPrimitiveTypeToSimpleType(this OpenApiSchema schema throw new ArgumentNullException(nameof(schema)); } - var type = (schema.Type?.ToLowerInvariant(), schema.Format.ToLowerInvariant(), schema.Nullable) switch + var type = (schema.Type?.ToLowerInvariant(), schema.Format?.ToLowerInvariant(), schema.Nullable) switch { ("boolean", null, false) => typeof(bool), ("integer", "int32", false) => typeof(int), diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt index 98ed35ecb..5aaa55b19 100755 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt @@ -284,6 +284,7 @@ namespace Microsoft.OpenApi.Extensions } public static class OpenApiTypeMapper { + public static System.Type MapOpenApiPrimitiveTypeToSimpleType(this Microsoft.OpenApi.Models.OpenApiSchema schema) { } public static Microsoft.OpenApi.Models.OpenApiSchema MapTypeToOpenApiPrimitiveType(this System.Type type) { } } public static class StringExtensions