Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for Enum collections types described as string collections #1846

Merged
merged 3 commits into from
Sep 15, 2022
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
8 changes: 8 additions & 0 deletions src/Kiota.Builder/Extensions/OpenApiSchemaExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ public static bool IsOneOf(this OpenApiSchema schema)
{
return schema?.OneOf?.Count(IsSemanticallyMeaningful) > 1;
}
public static bool IsEnum(this OpenApiSchema schema)
{
return schema?.Enum?.Any() ?? false;
}
public static bool IsComposedEnum(this OpenApiSchema schema)
{
return ((schema.IsAnyOf() && schema.AnyOf.Any(x => x.IsEnum())) || (schema.IsOneOf() && schema.OneOf.Any(x => x.IsEnum())));
}
private static bool IsSemanticallyMeaningful(this OpenApiSchema schema)
{
return schema.Properties.Any() || schema.Items != null || !string.IsNullOrEmpty(schema.Type) || !string.IsNullOrEmpty(schema.Format) || !string.IsNullOrEmpty(schema.Reference?.Id);
Expand Down
11 changes: 7 additions & 4 deletions src/Kiota.Builder/KiotaBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,7 @@ private static CodeType GetPrimitiveType(OpenApiSchema typeSchema, string childT
var typeName = typeNames.FirstOrDefault(static x => !string.IsNullOrEmpty(x) && !typeNamesToSkip.Contains(x));

var isExternal = false;
if (typeSchema?.Items?.Enum?.Any() ?? false)
if (typeSchema?.Items?.IsEnum() ?? false)
typeName = childType;
else {
var format = typeSchema?.Format ?? typeSchema?.Items?.Format;
Expand Down Expand Up @@ -961,7 +961,7 @@ private CodeTypeBase CreateModelDeclarations(OpenApiUrlTreeNode currentNode, Ope
return CreateComposedModelDeclaration(currentNode, schema, operation, suffix, codeNamespace, isRequestBody);
}

if(schema.IsObject() || schema.Properties.Any() || schema.Enum.Any() || !string.IsNullOrEmpty(schema.AdditionalProperties?.Type)) {
if(schema.IsObject() || schema.Properties.Any() || schema.IsEnum() || !string.IsNullOrEmpty(schema.AdditionalProperties?.Type)) {
// no inheritance or union type, often empty definitions with only additional properties are used as property bags.
return CreateModelDeclarationAndType(currentNode, schema, operation, codeNamespace, suffix, response: responseValue, typeNameForInlineSchema: typeNameForInlineSchema, isRequestBody);
}
Expand All @@ -980,7 +980,10 @@ private CodeTypeBase CreateModelDeclarations(OpenApiUrlTreeNode currentNode, Ope
private CodeTypeBase CreateCollectionModelDeclaration(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, OpenApiOperation operation, CodeNamespace codeNamespace, string typeNameForInlineSchema, bool isRequestBody)
{
CodeTypeBase type = GetPrimitiveType(schema?.Items, string.Empty);
if (string.IsNullOrEmpty(type?.Name))
bool isEnumOrComposedCollectionType = (schema?.Items.IsEnum() ?? false) //the collection could be an enum type so override with strong type instead of string type.
|| ((schema?.Items.IsComposedEnum() ?? false) && string.IsNullOrEmpty(schema?.Items.Format));//the collection could be a composed type with an enum type so override with strong type instead of string type.
if ( string.IsNullOrEmpty(type?.Name)
|| isEnumOrComposedCollectionType)
{
var targetNamespace = schema?.Items == null ? codeNamespace : GetShortestNamespace(codeNamespace, schema.Items);
type = CreateModelDeclarations(currentNode, schema?.Items, operation, targetNamespace, default , typeNameForInlineSchema: typeNameForInlineSchema, isRequestBody: isRequestBody);
Expand All @@ -1002,7 +1005,7 @@ private CodeElement AddModelDeclarationIfDoesntExist(OpenApiUrlTreeNode currentN
var existingDeclaration = GetExistingDeclaration(currentNamespace, currentNode, declarationName);
if(existingDeclaration == null) // we can find it in the components
{
if(schema.Enum.Any()) {
if(schema.IsEnum()) {
var newEnum = new CodeEnum {
Name = declarationName,//TODO set the flag property
Description = currentNode.GetPathItemDescription(Constants.DefaultOpenApiLabel),
Expand Down
8 changes: 4 additions & 4 deletions src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -324,8 +324,8 @@ private string GetDeserializationMethodName(CodeTypeBase propType, CodeMethod me
var collectionMethod = propType.IsArray ? "?.ToArray()" : "?.ToList()";
if (currentType.TypeDefinition == null)
return $"GetCollectionOfPrimitiveValues<{propertyType}>(){collectionMethod}";
else if (currentType.TypeDefinition is CodeEnum enumType)
return $"GetCollectionOfEnumValues<{enumType.Name.ToFirstCharacterUpperCase()}>(){collectionMethod}";
else if (currentType.TypeDefinition is CodeEnum)
return $"GetCollectionOfEnumValues<{propertyType.TrimEnd('?')}>(){collectionMethod}";
else
return $"GetCollectionOfObjectValues<{propertyType}>({propertyType}.CreateFromDiscriminatorValue){collectionMethod}";
} else if (currentType.TypeDefinition is CodeEnum enumType)
Expand Down Expand Up @@ -567,8 +567,8 @@ private string GetSerializationMethodName(CodeTypeBase propType, CodeMethod meth
if (isCollection)
if (currentType.TypeDefinition == null)
return $"WriteCollectionOfPrimitiveValues<{propertyType}>";
else if (currentType.TypeDefinition is CodeEnum enumType)
return $"WriteCollectionOfEnumValues<{enumType.Name.ToFirstCharacterUpperCase()}>";
else if (currentType.TypeDefinition is CodeEnum)
return $"WriteCollectionOfEnumValues<{propertyType.TrimEnd('?')}>";
else
return $"WriteCollectionOfObjectValues<{propertyType}>";
else if (currentType.TypeDefinition is CodeEnum enumType)
Expand Down
181 changes: 181 additions & 0 deletions tests/Kiota.Builder.Tests/KiotaBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2986,6 +2986,187 @@ public void IdsResultInIndexers(){
Assert.Null(modelsNS.FindChildByName<CodeClass>("With", false));
}
[Fact]
public void HandlesCollectionOfEnumSchemasInAnyOfWithNullable(){
var enumSchema = new OpenApiSchema
{
Title = "riskLevel",
Enum = new List<IOpenApiAny>
{
new OpenApiString("low"),
new OpenApiString("medium"),
new OpenApiString("high"),
new OpenApiString("hidden"),
new OpenApiString("none"),
new OpenApiString("unknownFutureValue")
},
Type = "string"
};
var myObjectSchema = new OpenApiSchema {
Title = "conditionalAccessConditionSet",
Type = "object",
Properties = new Dictionary<string, OpenApiSchema> {
{
"signInRiskLevels", new OpenApiSchema {
Type = "array",
Items = new OpenApiSchema
{
AnyOf = new List<OpenApiSchema>
{
enumSchema,
new OpenApiSchema
{
Type = "object",
Nullable = true
}
}
}
}
}
},
Reference = new OpenApiReference {
Id = "myobject",
Type = ReferenceType.Schema
},
UnresolvedReference = false
};

var document = new OpenApiDocument
{
Paths = new OpenApiPaths
{
["answer"] = new OpenApiPathItem
{
Operations = {
[OperationType.Get] = new OpenApiOperation
{
Responses = new OpenApiResponses
{
["200"] = new OpenApiResponse {
Content = {
["application/json"] = new OpenApiMediaType {
Schema = myObjectSchema
}
}
},
}
}
}
}
},
Components = new() {
Schemas = new Dictionary<string, OpenApiSchema> {
{
"myobject", myObjectSchema
},
{
"riskLevel", enumSchema
}
},

}
};
var mockLogger = new Mock<ILogger<KiotaBuilder>>();
var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "TestClient", ClientNamespaceName = "TestSdk", ApiRootUrl = "https://localhost" });
var node = builder.CreateUriSpace(document);
var codeModel = builder.CreateSourceModel(node);
var modelsNS = codeModel.FindNamespaceByName("TestSdk.Models");
Assert.NotNull(modelsNS);
var responseClass = modelsNS.Classes.FirstOrDefault(x => x.IsOfKind(CodeClassKind.Model) && x.Name.Equals("myobject", StringComparison.OrdinalIgnoreCase));
Assert.NotNull(responseClass);
var property = responseClass.Properties.FirstOrDefault(x => x.IsOfKind(CodePropertyKind.Custom) && x.Name.Equals("signInRiskLevels", StringComparison.OrdinalIgnoreCase));
Assert.NotNull(property);
Assert.NotEmpty(property.Type.Name);
var codeType = property.Type as CodeType;
Assert.NotNull(codeType);
Assert.IsType<CodeEnum>(codeType.TypeDefinition);// Ensure the collection is a codeEnum
Assert.Equal(CodeTypeBase.CodeTypeCollectionKind.Complex,codeType.CollectionKind);// Ensure the collection is a codeEnum
}
[Fact]
public void HandlesCollectionOfEnumSchemas(){
var enumSchema = new OpenApiSchema
{
Title = "riskLevel",
Enum = new List<IOpenApiAny>
{
new OpenApiString("low"),
new OpenApiString("medium"),
new OpenApiString("high"),
new OpenApiString("hidden"),
new OpenApiString("none"),
new OpenApiString("unknownFutureValue")
},
Type = "string"
};
var myObjectSchema = new OpenApiSchema {
Title = "conditionalAccessConditionSet",
Type = "object",
Properties = new Dictionary<string, OpenApiSchema> {
{
"signInRiskLevels", new OpenApiSchema {
Type = "array",
Items = enumSchema
}
}
},
Reference = new OpenApiReference {
Id = "myobject",
Type = ReferenceType.Schema
},
UnresolvedReference = false
};

var document = new OpenApiDocument
{
Paths = new OpenApiPaths
{
["answer"] = new OpenApiPathItem
{
Operations = {
[OperationType.Get] = new OpenApiOperation
{
Responses = new OpenApiResponses
{
["200"] = new OpenApiResponse {
Content = {
["application/json"] = new OpenApiMediaType {
Schema = myObjectSchema
}
}
},
}
}
}
}
},
Components = new() {
Schemas = new Dictionary<string, OpenApiSchema> {
{
"myobject", myObjectSchema
},
{
"riskLevel", enumSchema
}
},

}
};
var mockLogger = new Mock<ILogger<KiotaBuilder>>();
var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "TestClient", ClientNamespaceName = "TestSdk", ApiRootUrl = "https://localhost" });
var node = builder.CreateUriSpace(document);
var codeModel = builder.CreateSourceModel(node);
var modelsNS = codeModel.FindNamespaceByName("TestSdk.Models");
Assert.NotNull(modelsNS);
var responseClass = modelsNS.Classes.FirstOrDefault(x => x.IsOfKind(CodeClassKind.Model) && x.Name.Equals("myobject", StringComparison.OrdinalIgnoreCase));
Assert.NotNull(responseClass);
var property = responseClass.Properties.FirstOrDefault(x => x.IsOfKind(CodePropertyKind.Custom) && x.Name.Equals("signInRiskLevels", StringComparison.OrdinalIgnoreCase));
Assert.NotNull(property);
Assert.NotEmpty(property.Type.Name);
var codeType = property.Type as CodeType;
Assert.NotNull(codeType);
Assert.IsType<CodeEnum>(codeType.TypeDefinition);// Ensure the collection is a codeEnum
Assert.Equal(CodeTypeBase.CodeTypeCollectionKind.Complex,codeType.CollectionKind);// Ensure the collection is a codeEnum
}
[Fact]
public void InlinePropertiesGenerateTypes(){
var myObjectSchema = new OpenApiSchema {
Type = "object",
Expand Down