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
9 changes: 9 additions & 0 deletions src/Microsoft.OpenApi.OData.Reader/Common/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.OpenApi.OData.Vocabulary;

namespace Microsoft.OpenApi.OData.Common
Expand Down Expand Up @@ -106,5 +107,13 @@ internal static string CheckArgumentNullOrEmpty(string value, string parameterNa

return value;
}

/// <summary>
/// Lowers the first character of the string.
/// </summary>
/// <param name="input">The input string.</param>
/// <returns>The changed string.</returns>
internal static string ToFirstCharacterLowerCase(this string input)
=> string.IsNullOrEmpty(input) ? input : $"{char.ToLowerInvariant(input.FirstOrDefault())}{input.Substring(1)}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -130,7 +131,7 @@ internal static IEnumerable<IEdmStructuredType> GetAllCollectionEntityTypes(this
Enumerable.Empty<IEdmStructuredType>())
.Union(context.Model
.SchemaElements
.OfType<IEdmStructuredType>()
.OfType<IEdmStructuredType>()
.SelectMany(x => x.NavigationProperties())
.Where(x => x.TargetMultiplicity() == EdmMultiplicity.Many)
.Select(x => x.Type.ToStructuredType()))
Expand Down Expand Up @@ -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",
Expand All @@ -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
});
}

/// <summary>
/// Create a <see cref="OpenApiSchema"/> for a <see cref="IEdmStructuredType"/>.
Expand Down
8 changes: 8 additions & 0 deletions src/Microsoft.OpenApi.OData.Reader/OpenApiConvertSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,13 @@ public string PathPrefix
/// </summary>
public bool EnableDeprecationInformation { get; set; } = true;

/// <summary>
/// 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
/// </summary>
public bool AddEnumDescriptionExtension { get; set; } = false;

internal OpenApiConvertSettings Clone()
{
var newSettings = new OpenApiConvertSettings
Expand Down Expand Up @@ -243,6 +250,7 @@ internal OpenApiConvertSettings Clone()
EnableODataTypeCast = this.EnableODataTypeCast,
RequireDerivedTypesConstraintForODataTypeCastSegments = this.RequireDerivedTypesConstraintForODataTypeCastSegments,
EnableDeprecationInformation = this.EnableDeprecationInformation,
AddEnumDescriptionExtension = this.AddEnumDescriptionExtension,
};

return newSettings;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)}";
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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
/// </summary>
internal class OpenApiEnumValuesDescriptionExtension : IOpenApiExtension
{
/// <summary>
/// Name of the extension as used in the description.
/// </summary>
public string Name => "x-ms-enum";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static or static readonly since it's constant?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this property is required by the interface, and this syntax makes it readonly.


/// <summary>
/// The of the enum.
/// </summary>
public string EnumName { get; set; }

/// <summary>
/// Descriptions for the enum symbols, where the value MUST match the enum symbols in the main description
/// </summary>
public List<EnumDescription> ValuesDescriptions { get; set; } = new();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't need the "set" for the collection since you have new(), right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the set is here so people can assign the whole collection (used in unit tests)


/// <inheritdoc />
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
{
/// <summary>
/// The description for the enum symbol
/// </summary>
public string Description { get; set; }
/// <summary>
/// The symbol for the enum symbol to use for code-generation
/// </summary>
public string Name { get; set; }
/// <summary>
/// The symbol as described in the main enum schema.
/// </summary>
public string Value { get; set; }
}
2 changes: 2 additions & 0 deletions src/Microsoft.OpenApi.OData.Reader/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> parameters) -> string
override Microsoft.OpenApi.OData.Edm.ODataDollarCountSegment.Identifier.get -> string
override Microsoft.OpenApi.OData.Edm.ODataDollarCountSegment.EntityType.get -> Microsoft.OData.Edm.IEdmEntityType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -242,6 +243,7 @@ public void TripServiceMetadataToOpenApiYamlWorks(OpenApiSpecVersion specVersion
IEEE754Compatible = true,
OpenApiSpecVersion = specVersion,
AddSingleQuotesForStringParameters = true,
AddEnumDescriptionExtension = true,
};

// Act
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,17 @@
<NavigationProperty Name="DirectReports" Type="Collection(Microsoft.OData.Service.Sample.TrippinInMemory.Models.Person)" />
</EntityType>
<EnumType Name="PersonGender">
<Member Name="Male" Value="0" />
<Member Name="Female" Value="1" />
<Member Name="Unknow" Value="2" />
</EnumType>
<Annotation Term="Org.OData.Core.V1.Description" String="Gender of the person." />
<Member Name="Male" Value="0">
<Annotation Term="Org.OData.Core.V1.Description" String="The Male gender." />
</Member>
<Member Name="Female" Value="1">
<Annotation Term="Org.OData.Core.V1.Description" String="The Female gender." />
</Member>
<Member Name="Unknow" Value="2">
<Annotation Term="Org.OData.Core.V1.Description" String="Unknown gender or prefers not to say." />
</Member>
</EnumType>
<EnumType Name="Feature">
<Member Name="Feature1" Value="0" />
<Member Name="Feature2" Value="1" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28511,12 +28511,34 @@
},
"Microsoft.OData.Service.Sample.TrippinInMemory.Models.PersonGender": {
"title": "PersonGender",
"description": "Gender of the person.",
"enum": [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we extend the item in the array to be a complex object, which has the "name" and description?

In this case, we don't need to add an extension to repeat the name or value for enum member?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here I'm just implementing this spec so people also get compatibility with auto rest should they be using it afterwards https://github.com/Azure/autorest/blob/main/docs/extensions/readme.md#x-ms-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",
Expand Down
Loading