diff --git a/src/Microsoft.OpenApi.OData.Reader/Common/Utils.cs b/src/Microsoft.OpenApi.OData.Reader/Common/Utils.cs index 3c34a0be..4f821154 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Common/Utils.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Common/Utils.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.OpenApi.OData.Vocabulary; namespace Microsoft.OpenApi.OData.Common @@ -106,5 +107,13 @@ internal static string CheckArgumentNullOrEmpty(string value, string parameterNa return value; } + + /// + /// Lowers the first character of the string. + /// + /// The input string. + /// The changed string. + internal static string ToFirstCharacterLowerCase(this string input) + => string.IsNullOrEmpty(input) ? input : $"{char.ToLowerInvariant(input.FirstOrDefault())}{input.Substring(1)}"; } } \ No newline at end of file diff --git a/src/Microsoft.OpenApi.OData.Reader/Generator/OpenApiSchemaGenerator.cs b/src/Microsoft.OpenApi.OData.Reader/Generator/OpenApiSchemaGenerator.cs index fb7815bb..e551c27d 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Generator/OpenApiSchemaGenerator.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Generator/OpenApiSchemaGenerator.cs @@ -15,6 +15,7 @@ using Microsoft.OpenApi.Exceptions; using System.Linq; using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.OData.OpenApiExtensions; namespace Microsoft.OpenApi.OData.Generator { @@ -130,7 +131,7 @@ internal static IEnumerable GetAllCollectionEntityTypes(this Enumerable.Empty()) .Union(context.Model .SchemaElements - .OfType() + .OfType() .SelectMany(x => x.NavigationProperties()) .Where(x => x.TargetMultiplicity() == EdmMultiplicity.Many) .Select(x => x.Type.ToStructuredType())) @@ -209,7 +210,7 @@ public static OpenApiSchema CreateEnumTypeSchema(this ODataContext context, IEdm Utils.CheckArgumentNull(context, nameof(context)); Utils.CheckArgumentNull(enumType, nameof(enumType)); - OpenApiSchema schema = new OpenApiSchema + OpenApiSchema schema = new() { // An enumeration type is represented as a Schema Object of type string Type = "string", @@ -221,16 +222,40 @@ public static OpenApiSchema CreateEnumTypeSchema(this ODataContext context, IEdm // whose value is the value of the unqualified annotation Core.Description of the enumeration type. Description = context.Model.GetDescriptionAnnotation(enumType) }; + var extension = (context.Settings.OpenApiSpecVersion == OpenApiSpecVersion.OpenApi2_0 || + context.Settings.OpenApiSpecVersion == OpenApiSpecVersion.OpenApi3_0 ) && + context.Settings.AddEnumDescriptionExtension ? + new OpenApiEnumValuesDescriptionExtension { + EnumName = enumType.Name, + } : + null; // Enum value is an array that contains a string with the member name for each enumeration member. foreach (IEdmEnumMember member in enumType.Members) { schema.Enum.Add(new OpenApiString(member.Name)); + AddEnumDescription(member, extension, context); } + if(extension?.ValuesDescriptions.Any() ?? false) + schema.Extensions.Add(extension.Name, extension); schema.Title = enumType.Name; return schema; } + private static void AddEnumDescription(IEdmEnumMember member, OpenApiEnumValuesDescriptionExtension target, ODataContext context) + { + if (target == null) + return; + + var enumDescription = context.Model.GetDescriptionAnnotation(member); + if(!string.IsNullOrEmpty(enumDescription)) + target.ValuesDescriptions.Add(new EnumDescription + { + Name = member.Name, + Value = member.Name, + Description = enumDescription + }); + } /// /// Create a for a . diff --git a/src/Microsoft.OpenApi.OData.Reader/OpenApiConvertSettings.cs b/src/Microsoft.OpenApi.OData.Reader/OpenApiConvertSettings.cs index a32ce8f2..5770b484 100644 --- a/src/Microsoft.OpenApi.OData.Reader/OpenApiConvertSettings.cs +++ b/src/Microsoft.OpenApi.OData.Reader/OpenApiConvertSettings.cs @@ -208,6 +208,13 @@ public string PathPrefix /// public bool EnableDeprecationInformation { get; set; } = true; + /// + /// Gets/sets a value indicating whether or not to add a "x-ms-enum" extension to the enum type schema for V2 and V3 descriptions. + /// V3.1 will won't add the extension. + /// https://github.com/Azure/autorest/blob/main/docs/extensions/readme.md#x-ms-enum + /// + public bool AddEnumDescriptionExtension { get; set; } = false; + internal OpenApiConvertSettings Clone() { var newSettings = new OpenApiConvertSettings @@ -243,6 +250,7 @@ internal OpenApiConvertSettings Clone() EnableODataTypeCast = this.EnableODataTypeCast, RequireDerivedTypesConstraintForODataTypeCastSegments = this.RequireDerivedTypesConstraintForODataTypeCastSegments, EnableDeprecationInformation = this.EnableDeprecationInformation, + AddEnumDescriptionExtension = this.AddEnumDescriptionExtension, }; return newSettings; diff --git a/src/Microsoft.OpenApi.OData.Reader/OpenApiExtensions/OpenApiDeprecationExtension.cs b/src/Microsoft.OpenApi.OData.Reader/OpenApiExtensions/OpenApiDeprecationExtension.cs index 69c0069d..a062443d 100644 --- a/src/Microsoft.OpenApi.OData.Reader/OpenApiExtensions/OpenApiDeprecationExtension.cs +++ b/src/Microsoft.OpenApi.OData.Reader/OpenApiExtensions/OpenApiDeprecationExtension.cs @@ -4,8 +4,8 @@ // ------------------------------------------------------------ using System; -using System.Linq; using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.OData.Common; using Microsoft.OpenApi.Writers; namespace Microsoft.OpenApi.OData.OpenApiExtensions; @@ -46,17 +46,15 @@ public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion) writer.WriteStartObject(); if(RemovalDate.HasValue) - writer.WriteProperty(ToFirstCharacterLowerCase(nameof(RemovalDate)), RemovalDate.Value); + writer.WriteProperty(nameof(RemovalDate).ToFirstCharacterLowerCase(), RemovalDate.Value); if(Date.HasValue) - writer.WriteProperty(ToFirstCharacterLowerCase(nameof(Date)), Date.Value); + writer.WriteProperty(nameof(Date).ToFirstCharacterLowerCase(), Date.Value); if(!string.IsNullOrEmpty(Version)) - writer.WriteProperty(ToFirstCharacterLowerCase(nameof(Version)), Version); + writer.WriteProperty(nameof(Version).ToFirstCharacterLowerCase(), Version); if(!string.IsNullOrEmpty(Description)) - writer.WriteProperty(ToFirstCharacterLowerCase(nameof(Description)), Description); + writer.WriteProperty(nameof(Description).ToFirstCharacterLowerCase(), Description); writer.WriteEndObject(); } - } - private static string ToFirstCharacterLowerCase(string input) - => string.IsNullOrEmpty(input) ? input : $"{char.ToLowerInvariant(input.FirstOrDefault())}{input.Substring(1)}"; + } } \ No newline at end of file diff --git a/src/Microsoft.OpenApi.OData.Reader/OpenApiExtensions/OpenApiEnumValuesDescriptionExtension.cs b/src/Microsoft.OpenApi.OData.Reader/OpenApiExtensions/OpenApiEnumValuesDescriptionExtension.cs new file mode 100644 index 00000000..13028562 --- /dev/null +++ b/src/Microsoft.OpenApi.OData.Reader/OpenApiExtensions/OpenApiEnumValuesDescriptionExtension.cs @@ -0,0 +1,74 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.OData.Common; +using Microsoft.OpenApi.Writers; + +namespace Microsoft.OpenApi.OData.OpenApiExtensions; + +/// +/// Extension element for OpenAPI to add enum values descriptions. +/// Based of the AutoRest specification https://github.com/Azure/autorest/blob/main/docs/extensions/readme.md#x-ms-enum +/// +internal class OpenApiEnumValuesDescriptionExtension : IOpenApiExtension +{ + /// + /// Name of the extension as used in the description. + /// + public string Name => "x-ms-enum"; + + /// + /// The of the enum. + /// + public string EnumName { get; set; } + + /// + /// Descriptions for the enum symbols, where the value MUST match the enum symbols in the main description + /// + public List ValuesDescriptions { get; set; } = new(); + + /// + public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion) + { + if(writer == null) + throw new ArgumentNullException(nameof(writer)); + if((specVersion == OpenApiSpecVersion.OpenApi2_0 || specVersion == OpenApiSpecVersion.OpenApi3_0) && + !string.IsNullOrEmpty(EnumName) && + ValuesDescriptions.Any()) + { // when we upgrade to 3.1, we don't need to write this extension as JSON schema will support writing enum values + writer.WriteStartObject(); + writer.WriteProperty(nameof(Name).ToFirstCharacterLowerCase(), EnumName); + writer.WriteProperty("modelAsString", false); + writer.WriteRequiredCollection("values", ValuesDescriptions, (w, x) => { + w.WriteStartObject(); + w.WriteProperty(nameof(x.Value).ToFirstCharacterLowerCase(), x.Value); + w.WriteProperty(nameof(x.Description).ToFirstCharacterLowerCase(), x.Description); + w.WriteProperty(nameof(x.Name).ToFirstCharacterLowerCase(), x.Name); + w.WriteEndObject(); + }); + writer.WriteEndObject(); + } + } +} + +internal class EnumDescription : IOpenApiElement +{ + /// + /// The description for the enum symbol + /// + public string Description { get; set; } + /// + /// The symbol for the enum symbol to use for code-generation + /// + public string Name { get; set; } + /// + /// The symbol as described in the main enum schema. + /// + public string Value { get; set; } +} diff --git a/src/Microsoft.OpenApi.OData.Reader/PublicAPI.Unshipped.txt b/src/Microsoft.OpenApi.OData.Reader/PublicAPI.Unshipped.txt index b2576112..62ac6f8b 100644 --- a/src/Microsoft.OpenApi.OData.Reader/PublicAPI.Unshipped.txt +++ b/src/Microsoft.OpenApi.OData.Reader/PublicAPI.Unshipped.txt @@ -179,6 +179,8 @@ Microsoft.OpenApi.OData.OpenApiConvertSettings.Version.get -> System.Version Microsoft.OpenApi.OData.OpenApiConvertSettings.Version.set -> void Microsoft.OpenApi.OData.OpenApiConvertSettings.EnableDeprecationInformation.get -> bool Microsoft.OpenApi.OData.OpenApiConvertSettings.EnableDeprecationInformation.set -> void +Microsoft.OpenApi.OData.OpenApiConvertSettings.AddEnumDescriptionExtension.get -> bool +Microsoft.OpenApi.OData.OpenApiConvertSettings.AddEnumDescriptionExtension.set -> void override Microsoft.OpenApi.OData.Edm.ODataDollarCountSegment.GetPathItemName(Microsoft.OpenApi.OData.OpenApiConvertSettings settings, System.Collections.Generic.HashSet parameters) -> string override Microsoft.OpenApi.OData.Edm.ODataDollarCountSegment.Identifier.get -> string override Microsoft.OpenApi.OData.Edm.ODataDollarCountSegment.EntityType.get -> Microsoft.OData.Edm.IEdmEntityType diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/EdmModelOpenApiExtensionsTest.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/EdmModelOpenApiExtensionsTest.cs index 0a71c2ec..6749c98e 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/EdmModelOpenApiExtensionsTest.cs +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/EdmModelOpenApiExtensionsTest.cs @@ -210,6 +210,7 @@ public void TripServiceMetadataToOpenApiJsonWorks(OpenApiSpecVersion specVersion IEEE754Compatible = true, OpenApiSpecVersion = specVersion, AddSingleQuotesForStringParameters = true, + AddEnumDescriptionExtension = true, }; // Act string json = WriteEdmModelAsOpenApi(model, OpenApiFormat.Json, settings); @@ -242,6 +243,7 @@ public void TripServiceMetadataToOpenApiYamlWorks(OpenApiSpecVersion specVersion IEEE754Compatible = true, OpenApiSpecVersion = specVersion, AddSingleQuotesForStringParameters = true, + AddEnumDescriptionExtension = true, }; // Act diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/OpenApiExtensions/OpenApiEnumValuesDescriptionExtensionTexts.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/OpenApiExtensions/OpenApiEnumValuesDescriptionExtensionTexts.cs new file mode 100644 index 00000000..1c1feeb9 --- /dev/null +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/OpenApiExtensions/OpenApiEnumValuesDescriptionExtensionTexts.cs @@ -0,0 +1,75 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------ + +using System.Collections.Generic; +using System.IO; +using Microsoft.OpenApi.Writers; +using Xunit; + +namespace Microsoft.OpenApi.OData.OpenApiExtensions.Tests; + +public class OpenApiEnumValuesDescriptionExtensionTexts +{ + [Fact] + public void ExtensionNameMatchesExpected() + { + // Arrange + OpenApiEnumValuesDescriptionExtension extension = new(); + + // Act + string name = extension.Name; + string expectedName = "x-ms-enum"; + + // Assert + Assert.Equal(expectedName, name); + } + + [Fact] + public void WritesNothingWhenNoValues() + { + // Arrange + OpenApiEnumValuesDescriptionExtension extension = new(); + using TextWriter sWriter = new StringWriter(); + OpenApiJsonWriter writer = new(sWriter); + + // Act + extension.Write(writer, OpenApiSpecVersion.OpenApi3_0); + string result = sWriter.ToString(); + + // Assert + Assert.Null(extension.EnumName); + Assert.Empty(extension.ValuesDescriptions); + Assert.Empty(result); + } + [Fact] + public void WritesEnumDescription() + { + // Arrange + OpenApiEnumValuesDescriptionExtension extension = new(); + extension.EnumName = "TestEnum"; + extension.ValuesDescriptions = new() + { + new() { + Description = "TestDescription", + Value = "TestValue", + Name = "TestName" + } + }; + using TextWriter sWriter = new StringWriter(); + OpenApiJsonWriter writer = new(sWriter); + + // Act + extension.Write(writer, OpenApiSpecVersion.OpenApi3_0); + string result = sWriter.ToString(); + + // Assert + Assert.Contains("values", result); + Assert.Contains("modelAsString\": false", result); + Assert.Contains("name\": \"TestEnum", result); + Assert.Contains("description\": \"TestDescription", result); + Assert.Contains("value\": \"TestValue", result); + Assert.Contains("name\": \"TestName", result); + } +} \ No newline at end of file diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Resources/TripService.OData.xml b/test/Microsoft.OpenAPI.OData.Reader.Tests/Resources/TripService.OData.xml index 2b185b00..ab636673 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Resources/TripService.OData.xml +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Resources/TripService.OData.xml @@ -144,10 +144,17 @@ - - - - + + + + + + + + + + + diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Resources/TripService.OpenApi.V2.json b/test/Microsoft.OpenAPI.OData.Reader.Tests/Resources/TripService.OpenApi.V2.json index 6a8e934a..fce7d605 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Resources/TripService.OpenApi.V2.json +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Resources/TripService.OpenApi.V2.json @@ -28511,12 +28511,34 @@ }, "Microsoft.OData.Service.Sample.TrippinInMemory.Models.PersonGender": { "title": "PersonGender", + "description": "Gender of the person.", "enum": [ "Male", "Female", "Unknow" ], - "type": "string" + "type": "string", + "x-ms-enum": { + "name": "PersonGender", + "modelAsString": false, + "values": [ + { + "value": "Male", + "description": "The Male gender.", + "name": "Male" + }, + { + "value": "Female", + "description": "The Female gender.", + "name": "Female" + }, + { + "value": "Unknow", + "description": "Unknown gender or prefers not to say.", + "name": "Unknow" + } + ] + } }, "Microsoft.OData.Service.Sample.TrippinInMemory.Models.Feature": { "title": "Feature", diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Resources/TripService.OpenApi.V2.yaml b/test/Microsoft.OpenAPI.OData.Reader.Tests/Resources/TripService.OpenApi.V2.yaml index 80aa6474..0bdaee62 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Resources/TripService.OpenApi.V2.yaml +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Resources/TripService.OpenApi.V2.yaml @@ -20199,11 +20199,25 @@ definitions: $ref: '#/definitions/Microsoft.OData.Service.Sample.TrippinInMemory.Models.Person' Microsoft.OData.Service.Sample.TrippinInMemory.Models.PersonGender: title: PersonGender + description: Gender of the person. enum: - Male - Female - Unknow type: string + x-ms-enum: + name: PersonGender + modelAsString: false + values: + - value: Male + description: The Male gender. + name: Male + - value: Female + description: The Female gender. + name: Female + - value: Unknow + description: Unknown gender or prefers not to say. + name: Unknow Microsoft.OData.Service.Sample.TrippinInMemory.Models.Feature: title: Feature enum: diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Resources/TripService.OpenApi.json b/test/Microsoft.OpenAPI.OData.Reader.Tests/Resources/TripService.OpenApi.json index caa35c91..127be552 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Resources/TripService.OpenApi.json +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Resources/TripService.OpenApi.json @@ -31962,7 +31962,29 @@ "Female", "Unknow" ], - "type": "string" + "type": "string", + "description": "Gender of the person.", + "x-ms-enum": { + "name": "PersonGender", + "modelAsString": false, + "values": [ + { + "value": "Male", + "description": "The Male gender.", + "name": "Male" + }, + { + "value": "Female", + "description": "The Female gender.", + "name": "Female" + }, + { + "value": "Unknow", + "description": "Unknown gender or prefers not to say.", + "name": "Unknow" + } + ] + } }, "Microsoft.OData.Service.Sample.TrippinInMemory.Models.Feature": { "title": "Feature", diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Resources/TripService.OpenApi.yaml b/test/Microsoft.OpenAPI.OData.Reader.Tests/Resources/TripService.OpenApi.yaml index 8267bb53..396afddf 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Resources/TripService.OpenApi.yaml +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Resources/TripService.OpenApi.yaml @@ -22279,6 +22279,20 @@ components: - Female - Unknow type: string + description: Gender of the person. + x-ms-enum: + name: PersonGender + modelAsString: false + values: + - value: Male + description: The Male gender. + name: Male + - value: Female + description: The Female gender. + name: Female + - value: Unknow + description: Unknown gender or prefers not to say. + name: Unknow Microsoft.OData.Service.Sample.TrippinInMemory.Models.Feature: title: Feature enum: