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

- adds support for enum types in query parameters #3477

Merged
merged 8 commits into from
Oct 17, 2023
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Added support for enum query parameter types. [#2490](https://github.com/microsoft/kiota/issues/2490)
- Support for primary error message in PHP [#3276](https://github.com/microsoft/kiota/issues/3276)

### Changed

- Fixed missing imports for method parameters that are query parameters.
- Fixed query parameters type mapping for arrays. [#3354](https://github.com/microsoft/kiota/issues/3354)
- Fixed bug where base64url types would not be generated properly in Java.
- Fixed bug where symbol name cleanup would not work on forward single quotes characters [#3426](https://github.com/microsoft/kiota/issues/3426).

Expand Down
108 changes: 79 additions & 29 deletions src/Kiota.Builder/KiotaBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1223,7 +1223,7 @@ openApiExtension is OpenApiPrimaryErrorMessageExtension primaryErrorMessageExten
};
}
private const string RequestBodyPlainTextContentType = "text/plain";
private static readonly HashSet<string> noContentStatusCodes = new() { "201", "202", "204", "205" };
private static readonly HashSet<string> noContentStatusCodes = new(StringComparer.OrdinalIgnoreCase) { "201", "202", "204", "205" };
private static readonly HashSet<string> errorStatusCodes = new(Enumerable.Range(400, 599).Select(static x => x.ToString(CultureInfo.InvariantCulture))
.Concat(new[] { FourXXError, FiveXXError }), StringComparer.OrdinalIgnoreCase);
private const string FourXXError = "4XX";
Expand Down Expand Up @@ -1793,35 +1793,49 @@ private CodeElement AddModelDeclarationIfDoesntExist(OpenApiUrlTreeNode currentN
{
if (GetExistingDeclaration(currentNamespace, currentNode, declarationName) is not CodeElement existingDeclaration) // we can find it in the components
{
if (schema.IsEnum())
{
var schemaDescription = schema.Description.CleanupDescription();
OpenApiEnumFlagsExtension? enumFlagsExtension = null;
if (schema.Extensions.TryGetValue(OpenApiEnumFlagsExtension.Name, out var rawExtension) &&
rawExtension is OpenApiEnumFlagsExtension flagsExtension)
{
enumFlagsExtension = flagsExtension;
}
var newEnum = new CodeEnum
{
Name = declarationName,
Flags = enumFlagsExtension?.IsFlags ?? false,
Documentation = new()
{
Description = !string.IsNullOrEmpty(schemaDescription) || !string.IsNullOrEmpty(schema.Reference?.Id) ?
schemaDescription : // if it's a referenced component, we shouldn't use the path item description as it makes it indeterministic
currentNode.GetPathItemDescription(Constants.DefaultOpenApiLabel),
},
Deprecation = schema.GetDeprecationInformation(),
};
SetEnumOptions(schema, newEnum);
return currentNamespace.AddEnum(newEnum).First();
}
if (AddEnumDeclaration(currentNode, schema, declarationName, currentNamespace) is CodeEnum enumDeclaration)
return enumDeclaration;

return AddModelClass(currentNode, schema, declarationName, currentNamespace, inheritsFrom);
}
return existingDeclaration;
}
private CodeEnum? AddEnumDeclarationIfDoesntExist(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, string declarationName, CodeNamespace currentNamespace)
{
if (GetExistingDeclaration(currentNamespace, currentNode, declarationName) is not CodeEnum existingDeclaration) // we can find it in the components
{
return AddEnumDeclaration(currentNode, schema, declarationName, currentNamespace);
}
return existingDeclaration;
}
private static CodeEnum? AddEnumDeclaration(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, string declarationName, CodeNamespace currentNamespace)
{
if (schema.IsEnum())
{
var schemaDescription = schema.Description.CleanupDescription();
OpenApiEnumFlagsExtension? enumFlagsExtension = null;
if (schema.Extensions.TryGetValue(OpenApiEnumFlagsExtension.Name, out var rawExtension) &&
rawExtension is OpenApiEnumFlagsExtension flagsExtension)
{
enumFlagsExtension = flagsExtension;
}
var newEnum = new CodeEnum
{
Name = declarationName,
Flags = enumFlagsExtension?.IsFlags ?? false,
Documentation = new()
{
Description = !string.IsNullOrEmpty(schemaDescription) || !string.IsNullOrEmpty(schema.Reference?.Id) ?
schemaDescription : // if it's a referenced component, we shouldn't use the path item description as it makes it indeterministic
currentNode.GetPathItemDescription(Constants.DefaultOpenApiLabel),
},
Deprecation = schema.GetDeprecationInformation(),
};
SetEnumOptions(schema, newEnum);
return currentNamespace.AddEnum(newEnum).First();
}
return default;
}
private static void SetEnumOptions(OpenApiSchema schema, CodeEnum target)
{
OpenApiEnumValuesDescriptionExtension? extensionInformation = null;
Expand Down Expand Up @@ -2278,16 +2292,39 @@ internal static void AddSerializationMembers(CodeClass model, bool includeAdditi
},
}).First();
foreach (var parameter in parameters)
AddPropertyForQueryParameter(parameter, parameterClass);
AddPropertyForQueryParameter(node, operationType, parameter, parameterClass);

return parameterClass;
}

return null;
}
private void AddPropertyForQueryParameter(OpenApiParameter parameter, CodeClass parameterClass)
private void AddPropertyForQueryParameter(OpenApiUrlTreeNode node, OperationType operationType, OpenApiParameter parameter, CodeClass parameterClass)
{
var resultType = GetPrimitiveType(parameter.Schema) ?? new CodeType()
CodeType? resultType = default;
var addBackwardCompatibleParameter = false;
if (parameter.Schema.IsEnum())
{
var schema = parameter.Schema;
var codeNamespace = schema.IsReferencedSchema() switch
{
true => GetShortestNamespace(parameterClass.GetImmediateParentOfType<CodeNamespace>(), schema), // referenced schema
false => parameterClass.GetImmediateParentOfType<CodeNamespace>(), // Inline schema, i.e. specific to the Operation
};
var shortestNamespace = GetShortestNamespace(codeNamespace, schema);
var enumName = schema.GetSchemaName().CleanupSymbolName();
if (string.IsNullOrEmpty(enumName))
enumName = $"{operationType.ToString().ToFirstCharacterUpperCase()}{parameter.Name.CleanupSymbolName().ToFirstCharacterUpperCase()}QueryParameterType";
if (AddEnumDeclarationIfDoesntExist(node, schema, enumName, shortestNamespace) is { } enumDeclaration)
{
resultType = new CodeType
{
TypeDefinition = enumDeclaration,
};
addBackwardCompatibleParameter = true;
}
}
resultType ??= GetPrimitiveType(parameter.Schema) ?? new CodeType()
{
// since its a query parameter default to string if there is no schema
// it also be an object type, but we'd need to create the model in that case and there's no standard on how to serialize those as query parameters
Expand All @@ -2314,7 +2351,20 @@ private void AddPropertyForQueryParameter(OpenApiParameter parameter, CodeClass

if (!parameterClass.ContainsPropertyWithWireName(prop.WireName))
{
parameterClass.AddProperty(prop);
if (addBackwardCompatibleParameter && config.IncludeBackwardCompatible && config.Language is GenerationLanguage.CSharp or GenerationLanguage.Go)
{ //TODO remove for v2
var modernProp = (CodeProperty)prop.Clone();
modernProp.Name = $"{prop.Name}As{modernProp.Type.Name.ToFirstCharacterUpperCase()}";
modernProp.SerializationName = prop.WireName;
prop.Deprecation = new($"This property is deprecated, use {modernProp.Name} instead", IsDeprecated: true);
prop.Type = GetDefaultQueryParameterType();
prop.Type.CollectionKind = modernProp.Type.CollectionKind;
parameterClass.AddProperty(modernProp, prop);
}
else
{
parameterClass.AddProperty(prop);
}
}
else
{
Expand Down
2 changes: 1 addition & 1 deletion src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -725,7 +725,7 @@ currentClass.StartBlock is ClassDeclaration currentClassDeclaration &&
.Distinct();
var methodsParametersTypes = methods
.SelectMany(static x => x.Parameters)
.Where(static x => x.IsOfKind(CodeParameterKind.Custom, CodeParameterKind.RequestBody, CodeParameterKind.RequestConfiguration))
.Where(static x => x.IsOfKind(CodeParameterKind.Custom, CodeParameterKind.RequestBody, CodeParameterKind.RequestConfiguration, CodeParameterKind.QueryParameter))
.Select(static x => x.Type)
.Distinct();
var indexerTypes = currentClassChildren
Expand Down
107 changes: 106 additions & 1 deletion tests/Kiota.Builder.Tests/KiotaBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3895,7 +3895,7 @@ public void MapsQueryParameterTypes(string type, string format, string expected)
[OperationType.Get] = new OpenApiOperation
{
Parameters = new List<OpenApiParameter> {
new OpenApiParameter {
new() {
Name = "query",
In = ParameterLocation.Query,
Schema = new OpenApiSchema {
Expand Down Expand Up @@ -3924,6 +3924,111 @@ public void MapsQueryParameterTypes(string type, string format, string expected)
Assert.Equal(expected, property.Type.Name);
Assert.True(property.Type.AllTypes.First().IsExternal);
}
[Fact]
public void MapsQueryParameterArrayTypes()
{
var document = new OpenApiDocument
{
Paths = new OpenApiPaths
{
["primitive"] = new OpenApiPathItem
{
Operations = {
[OperationType.Get] = new OpenApiOperation
{
Parameters = new List<OpenApiParameter> {
new() {
Name = "query",
In = ParameterLocation.Query,
Schema = new OpenApiSchema {
Type = "array",
Items = new OpenApiSchema {
Type = "integer",
Format = "int64"
}
}
}
},
Responses = new OpenApiResponses
{
["204"] = new OpenApiResponse()
}
}
}
}
},
};
var mockLogger = new Mock<ILogger<KiotaBuilder>>();
var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", ApiRootUrl = "https://localhost" }, _httpClient);
var node = builder.CreateUriSpace(document);
var codeModel = builder.CreateSourceModel(node);
var queryParameters = codeModel.FindChildByName<CodeClass>("primitiveRequestBuilderGetQueryParameters");
Assert.NotNull(queryParameters);
var property = queryParameters.Properties.First(static x => x.Name.Equals("query", StringComparison.OrdinalIgnoreCase));
Assert.NotNull(property);
Assert.Equal("int64", property.Type.Name);
Assert.Equal(CodeTypeBase.CodeTypeCollectionKind.Array, property.Type.CollectionKind);
Assert.True(property.Type.AllTypes.First().IsExternal);
}
[InlineData(GenerationLanguage.CSharp)]
[InlineData(GenerationLanguage.Java)]
[Theory]
public void MapsEnumQueryParameterType(GenerationLanguage generationLanguage)
{
var document = new OpenApiDocument
{
Paths = new OpenApiPaths
{
["primitive"] = new OpenApiPathItem
{
Operations = {
[OperationType.Get] = new OpenApiOperation
{
Parameters = new List<OpenApiParameter> {
new() {
Name = "query",
In = ParameterLocation.Query,
Schema = new OpenApiSchema {
Type = "string",
Enum = new List<IOpenApiAny> {
new OpenApiString("value1"),
new OpenApiString("value2")
}
}
}
},
Responses = new OpenApiResponses
{
["204"] = new OpenApiResponse()
}
}
}
}
},
};
var mockLogger = new Mock<ILogger<KiotaBuilder>>();
var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Graph", ApiRootUrl = "https://localhost", Language = generationLanguage }, _httpClient);
var node = builder.CreateUriSpace(document);
var codeModel = builder.CreateSourceModel(node);
var queryParameters = codeModel.FindChildByName<CodeClass>("primitiveRequestBuilderGetQueryParameters");
Assert.NotNull(queryParameters);
var backwardCompatibleProperty = queryParameters.Properties.FirstOrDefault(static x => x.Name.Equals("query", StringComparison.OrdinalIgnoreCase));
Assert.NotNull(backwardCompatibleProperty);
if (generationLanguage is GenerationLanguage.CSharp)
{
Assert.Equal("string", backwardCompatibleProperty.Type.Name);
Assert.True(backwardCompatibleProperty.Type.AllTypes.First().IsExternal);
Assert.True(backwardCompatibleProperty.Deprecation.IsDeprecated);
var property = queryParameters.Properties.FirstOrDefault(static x => x.Name.Equals("queryAsGetQueryQueryParameterType", StringComparison.OrdinalIgnoreCase));
Assert.NotNull(property);
Assert.Equal("GetQueryQueryParameterType", property.Type.Name);
}
else
{
Assert.Equal("GetQueryQueryParameterType", backwardCompatibleProperty.Type.Name);
Assert.False(backwardCompatibleProperty.Deprecation.IsDeprecated);
}
}
[InlineData(true)]
[InlineData(false)]
[Theory]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ public void ExtendABaseClass()
public async void AddsImportsToRequestConfigClasses()
{
var queryParamClass = new CodeClass { Name = "TestRequestQueryParameter", Kind = CodeClassKind.QueryParameters };
var modelsNamespace = root.AddNamespace("Models");
var eventStatus = new CodeEnum { Name = "EventStatus" };
modelsNamespace.AddEnum(eventStatus);
queryParamClass.AddProperty(new[]
{
new CodeProperty
Expand Down Expand Up @@ -201,6 +204,20 @@ public async void AddsImportsToRequestConfigClasses()
Name = "dateonly"
},
},
new CodeProperty
{
Name = "status",
Kind = CodePropertyKind.QueryParameter,
Documentation = new()
{
Description = "Filter by status",
},
Type = new CodeType
{
Name = "EventStatus",
TypeDefinition = eventStatus
},
}
});
root.AddClass(queryParamClass);
parentClass.Kind = CodeClassKind.RequestConfiguration;
Expand All @@ -219,6 +236,7 @@ public async void AddsImportsToRequestConfigClasses()

Assert.Contains("use DateTime;", result);
Assert.Contains("use Microsoft\\Kiota\\Abstractions\\Types\\Date;", result);
Assert.Contains("use Microsoft\\Graph\\Models\\EventStatus;", result);

}
}