Skip to content

Commit

Permalink
.Net: Map OpenAPI parameter type to KernelParameterMetadata (#8198)
Browse files Browse the repository at this point in the history
### Motivation, Context and Description
Today, the OpenAPI functionality that maps the data type of a parameter
from the OpenAPI spec to `KernelParameterMetadata` supports only the
mapping for string and boolean types. This PR extends the mapping to
support other primitive types like `number`, `integer`, and `object`.

Closes: #8182

Out of scope:
- Support for the `array` data type.
  • Loading branch information
SergeyMenshykh authored Aug 16, 2024
1 parent 9db225c commit 184c7c0
Show file tree
Hide file tree
Showing 12 changed files with 438 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -172,14 +172,15 @@ private static List<RestApiOperationParameter> GetParametersFromPayloadMetadata(
if (!property.Properties.Any())
{
parameters.Add(new RestApiOperationParameter(
parameterName,
property.Type,
property.IsRequired,
name: parameterName,
type: property.Type,
isRequired: property.IsRequired,
expand: false,
RestApiOperationParameterLocation.Body,
RestApiOperationParameterStyle.Simple,
location: RestApiOperationParameterLocation.Body,
style: RestApiOperationParameterStyle.Simple,
defaultValue: property.DefaultValue,
description: property.Description,
format: property.Format,
schema: property.Schema));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ public sealed class RestApiOperationParameter
/// </summary>
public string Type { get; }

/// <summary>
/// The parameter type modifier that refines the generic parameter type to a more specific one.
/// More details can be found at https://swagger.io/docs/specification/data-models/data-types
/// </summary>
public string? Format { get; }

/// <summary>
/// The parameter description.
/// </summary>
Expand Down Expand Up @@ -74,6 +80,8 @@ public sealed class RestApiOperationParameter
/// <param name="arrayItemType">Type of array item for parameters of "array" type.</param>
/// <param name="defaultValue">The parameter default value.</param>
/// <param name="description">The parameter description.</param>
/// <param name="format">The parameter type modifier that refines the generic parameter type to a more specific one.
/// More details can be found at https://swagger.io/docs/specification/data-models/data-types</param>
/// <param name="schema">The parameter schema.</param>
public RestApiOperationParameter(
string name,
Expand All @@ -85,6 +93,7 @@ public RestApiOperationParameter(
string? arrayItemType = null,
object? defaultValue = null,
string? description = null,
string? format = null,
KernelJsonSchema? schema = null)
{
this.Name = name;
Expand All @@ -96,6 +105,7 @@ public RestApiOperationParameter(
this.ArrayItemType = arrayItemType;
this.DefaultValue = defaultValue;
this.Description = description;
this.Format = format;
this.Schema = schema;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ public sealed class RestApiOperationPayloadProperty
/// </summary>
public string Type { get; }

/// <summary>
/// The property type modifier that refines the generic parameter type to a more specific one.
/// More details can be found at https://swagger.io/docs/specification/data-models/data-types
/// </summary>
public string? Format { get; }

/// <summary>
/// The property description.
/// </summary>
Expand Down Expand Up @@ -52,6 +58,8 @@ public sealed class RestApiOperationPayloadProperty
/// <param name="isRequired">A flag specifying if the property is required or not.</param>
/// <param name="properties">A list of properties for the payload property.</param>
/// <param name="description">A description of the property.</param>
/// <param name="format">The parameter type modifier that refines the generic parameter type to a more specific one.
/// More details can be found at https://swagger.io/docs/specification/data-models/data-types</param>
/// <param name="schema">The schema of the payload property.</param>
/// <param name="defaultValue">The default value of the property.</param>
/// <returns>Returns a new instance of the <see cref="RestApiOperationPayloadProperty"/> class.</returns>
Expand All @@ -61,6 +69,7 @@ public RestApiOperationPayloadProperty(
bool isRequired,
IList<RestApiOperationPayloadProperty> properties,
string? description = null,
string? format = null,
KernelJsonSchema? schema = null,
object? defaultValue = null)
{
Expand All @@ -70,6 +79,7 @@ public RestApiOperationPayloadProperty(
this.Description = description;
this.Properties = properties;
this.Schema = schema;
this.Format = format;
this.DefaultValue = defaultValue;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ private static List<RestApiOperationParameter> CreateRestApiOperationParameters(
parameter.Schema.Items?.Type,
GetParameterValue(parameter.Schema.Default, "parameter", parameter.Name),
parameter.Description,
parameter.Schema.Format,
parameter.Schema.ToJsonSchema()
);

Expand Down Expand Up @@ -371,6 +372,7 @@ private static List<RestApiOperationPayloadProperty> GetPayloadProperties(string
requiredProperties.Contains(propertyName),
GetPayloadProperties(operationId, propertySchema, requiredProperties, level + 1),
propertySchema.Description,
propertySchema.Format,
propertySchema.ToJsonSchema(),
GetParameterValue(propertySchema.Default, "payload property", propertyName));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ async Task<RestApiOperationResponse> ExecuteAsync(Kernel kernel, KernelFunction
Description = $"{p.Description ?? p.Name}",
DefaultValue = p.DefaultValue ?? string.Empty,
IsRequired = p.IsRequired,
ParameterType = p.Type switch { "string" => typeof(string), "boolean" => typeof(bool), _ => null },
ParameterType = ConvertParameterDataType(p),
Schema = p.Schema ?? (p.Type is null ? null : KernelJsonSchema.Parse($$"""{"type":"{{p.Type}}"}""")),
})
.ToList();
Expand Down Expand Up @@ -361,6 +361,34 @@ private static string ConvertOperationIdToValidFunctionName(string operationId,
return result;
}

/// <summary>
/// Converts the parameter type to a C# <see cref="Type"/> object.
/// </summary>
/// <param name="parameter">The REST API operation parameter.</param>
/// <returns></returns>
private static Type? ConvertParameterDataType(RestApiOperationParameter parameter)
{
return parameter.Type switch
{
"string" => typeof(string),
"boolean" => typeof(bool),
"number" => parameter.Format switch
{
"float" => typeof(float),
"double" => typeof(double),
_ => typeof(double)
},
"integer" => parameter.Format switch
{
"int32" => typeof(int),
"int64" => typeof(long),
_ => typeof(long)
},
"object" => typeof(object),
_ => null
};
}

/// <summary>
/// Used to convert operationId to SK function names.
/// </summary>
Expand All @@ -373,5 +401,4 @@ private static string ConvertOperationIdToValidFunctionName(string operationId,
#endif

#endregion

}
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ public async Task ItCanExtractAllPathsAsOperationsAsync()
var restApi = await this._sut.ParseAsync(this._openApiDocument);

// Assert
Assert.Equal(5, restApi.Operations.Count);
Assert.Equal(6, restApi.Operations.Count);
}

[Fact]
Expand Down Expand Up @@ -366,6 +366,44 @@ public async Task ItCanParseRestApiInfoAsync()
Assert.NotEmpty(restApi.Info.Description);
}

[Theory]
[InlineData("string-parameter", "string", null)]
[InlineData("boolean-parameter", "boolean", null)]
[InlineData("number-parameter", "number", null)]
[InlineData("float-parameter", "number", "float")]
[InlineData("double-parameter", "number", "double")]
[InlineData("integer-parameter", "integer", null)]
[InlineData("int32-parameter", "integer", "int32")]
[InlineData("int64-parameter", "integer", "int64")]
public async Task ItCanParseParametersOfPrimitiveDataTypeAsync(string name, string type, string? format)
{
// Arrange & Act
var restApiSpec = await this._sut.ParseAsync(this._openApiDocument);

// Assert
var parameters = restApiSpec.Operations.Single(o => o.Id == "TestParameterDataTypes").GetParameters();

var parameter = parameters.FirstOrDefault(p => p.Name == name);
Assert.NotNull(parameter);

Assert.Equal(type, parameter.Type);
Assert.Equal(format, parameter.Format);
}

[Fact]
public async Task ItCanParsePropertiesOfObjectDataTypeAsync()
{
// Arrange & Act
var restApiSpec = await this._sut.ParseAsync(this._openApiDocument);

// Assert
var properties = restApiSpec.Operations.Single(o => o.Id == "TestParameterDataTypes").Payload!.Properties;

var property = properties.Single(p => p.Name == "attributes");
Assert.Equal("object", property.Type);
Assert.Null(property.Format);
}

private static RestApiOperationParameter GetParameterMetadata(IList<RestApiOperation> operations, string operationId,
RestApiOperationParameterLocation location, string name)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ public async Task ItCanExtractAllPathsAsOperationsAsync()
var restApi = await this._sut.ParseAsync(this._openApiDocument);

// Assert
Assert.Equal(5, restApi.Operations.Count);
Assert.Equal(6, restApi.Operations.Count);
}

[Fact]
Expand Down Expand Up @@ -439,6 +439,44 @@ public async Task ItCanParseRestApiInfoAsync()
Assert.NotEmpty(restApi.Info.Description);
}

[Theory]
[InlineData("string-parameter", "string", null)]
[InlineData("boolean-parameter", "boolean", null)]
[InlineData("number-parameter", "number", null)]
[InlineData("float-parameter", "number", "float")]
[InlineData("double-parameter", "number", "double")]
[InlineData("integer-parameter", "integer", null)]
[InlineData("int32-parameter", "integer", "int32")]
[InlineData("int64-parameter", "integer", "int64")]
public async Task ItCanParseParametersOfPrimitiveDataTypeAsync(string name, string type, string? format)
{
// Arrange & Act
var restApiSpec = await this._sut.ParseAsync(this._openApiDocument);

// Assert
var parameters = restApiSpec.Operations.Single(o => o.Id == "TestParameterDataTypes").GetParameters();

var parameter = parameters.FirstOrDefault(p => p.Name == name);
Assert.NotNull(parameter);

Assert.Equal(type, parameter.Type);
Assert.Equal(format, parameter.Format);
}

[Fact]
public async Task ItCanParsePropertiesOfObjectDataTypeAsync()
{
// Arrange & Act
var restApiSpec = await this._sut.ParseAsync(this._openApiDocument);

// Assert
var properties = restApiSpec.Operations.Single(o => o.Id == "TestParameterDataTypes").Payload!.Properties;

var property = properties.Single(p => p.Name == "attributes");
Assert.Equal("object", property.Type);
Assert.Null(property.Format);
}

private static MemoryStream ModifyOpenApiDocument(Stream openApiDocument, Action<JsonObject> transformer)
{
var json = JsonSerializer.Deserialize<JsonObject>(openApiDocument);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ public async Task ItCanExtractAllPathsAsOperationsAsync()
var restApi = await this._sut.ParseAsync(this._openApiDocument);

// Assert
Assert.Equal(5, restApi.Operations.Count);
Assert.Equal(6, restApi.Operations.Count);
}

[Fact]
Expand Down Expand Up @@ -416,6 +416,44 @@ public async Task ItCanParseRestApiInfoAsync()
Assert.NotEmpty(restApi.Info.Description);
}

[Theory]
[InlineData("string-parameter", "string", null)]
[InlineData("boolean-parameter", "boolean", null)]
[InlineData("number-parameter", "number", null)]
[InlineData("float-parameter", "number", "float")]
[InlineData("double-parameter", "number", "double")]
[InlineData("integer-parameter", "integer", null)]
[InlineData("int32-parameter", "integer", "int32")]
[InlineData("int64-parameter", "integer", "int64")]
public async Task ItCanParseParametersOfPrimitiveDataTypeAsync(string name, string type, string? format)
{
// Arrange & Act
var restApiSpec = await this._sut.ParseAsync(this._openApiDocument);

// Assert
var parameters = restApiSpec.Operations.Single(o => o.Id == "TestParameterDataTypes").GetParameters();

var parameter = parameters.FirstOrDefault(p => p.Name == name);
Assert.NotNull(parameter);

Assert.Equal(type, parameter.Type);
Assert.Equal(format, parameter.Format);
}

[Fact]
public async Task ItCanParsePropertiesOfObjectDataTypeAsync()
{
// Arrange & Act
var restApiSpec = await this._sut.ParseAsync(this._openApiDocument);

// Assert
var properties = restApiSpec.Operations.Single(o => o.Id == "TestParameterDataTypes").Payload!.Properties;

var property = properties.Single(p => p.Name == "attributes");
Assert.Equal("object", property.Type);
Assert.Null(property.Format);
}

private static MemoryStream ModifyOpenApiDocument(Stream openApiDocument, Action<IDictionary<string, object>> transformer)
{
var serializer = new SharpYaml.Serialization.Serializer();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ public async Task ItShouldHandleEmptyOperationNameAsync()
var plugin = await OpenApiKernelPluginFactory.CreateFromOpenApiAsync("fakePlugin", content, this._executionParameters);

// Assert
Assert.Equal(5, plugin.Count());
Assert.Equal(6, plugin.Count());
Assert.True(plugin.TryGetFunction("GetSecretsSecretname", out var _));
}

Expand All @@ -324,10 +324,48 @@ public async Task ItShouldHandleNullOperationNameAsync()
var plugin = await OpenApiKernelPluginFactory.CreateFromOpenApiAsync("fakePlugin", content, this._executionParameters);

// Assert
Assert.Equal(5, plugin.Count());
Assert.Equal(6, plugin.Count());
Assert.True(plugin.TryGetFunction("GetSecretsSecretname", out var _));
}

[Theory]
[InlineData("string_parameter", typeof(string))]
[InlineData("boolean_parameter", typeof(bool))]
[InlineData("number_parameter", typeof(double))]
[InlineData("float_parameter", typeof(float))]
[InlineData("double_parameter", typeof(double))]
[InlineData("integer_parameter", typeof(long))]
[InlineData("int32_parameter", typeof(int))]
[InlineData("int64_parameter", typeof(long))]
public async Task ItShouldMapPropertiesOfPrimitiveDataTypeToKernelParameterMetadataAsync(string name, Type type)
{
// Arrange & Act
this._executionParameters.EnableDynamicPayload = true;

var plugin = await OpenApiKernelPluginFactory.CreateFromOpenApiAsync("fakePlugin", this._openApiDocument, this._executionParameters);

var parametersMetadata = plugin["TestParameterDataTypes"].Metadata.Parameters;

// Assert
var parameterMetadata = parametersMetadata.First(p => p.Name == name);

Assert.Equal(type, parameterMetadata.ParameterType);
}

[Fact]
public async Task ItShouldMapPropertiesOfObjectDataTypeToKernelParameterMetadataAsync()
{
// Arrange & Act
var plugin = await OpenApiKernelPluginFactory.CreateFromOpenApiAsync("fakePlugin", this._openApiDocument, this._executionParameters);

var parametersMetadata = plugin["TestParameterDataTypes"].Metadata.Parameters;

// Assert
var parameterMetadata = parametersMetadata.First(p => p.Name == "payload");

Assert.Equal(typeof(object), parameterMetadata.ParameterType);
}

[Fact]
public async Task ItShouldUseCustomHttpResponseContentReaderAsync()
{
Expand Down
Loading

0 comments on commit 184c7c0

Please sign in to comment.