diff --git a/dotnet/src/Functions/Functions.OpenApi/Extensions/RestApiOperationExtensions.cs b/dotnet/src/Functions/Functions.OpenApi/Extensions/RestApiOperationExtensions.cs index 09414ee0c339..ea9678b062be 100644 --- a/dotnet/src/Functions/Functions.OpenApi/Extensions/RestApiOperationExtensions.cs +++ b/dotnet/src/Functions/Functions.OpenApi/Extensions/RestApiOperationExtensions.cs @@ -172,14 +172,15 @@ private static List 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)); } diff --git a/dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperationParameter.cs b/dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperationParameter.cs index c6d8f3f1c8a0..e4e2f9a1d5a4 100644 --- a/dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperationParameter.cs +++ b/dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperationParameter.cs @@ -22,6 +22,12 @@ public sealed class RestApiOperationParameter /// public string Type { get; } + /// + /// 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 + /// + public string? Format { get; } + /// /// The parameter description. /// @@ -74,6 +80,8 @@ public sealed class RestApiOperationParameter /// Type of array item for parameters of "array" type. /// The parameter default value. /// The parameter description. + /// 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 /// The parameter schema. public RestApiOperationParameter( string name, @@ -85,6 +93,7 @@ public RestApiOperationParameter( string? arrayItemType = null, object? defaultValue = null, string? description = null, + string? format = null, KernelJsonSchema? schema = null) { this.Name = name; @@ -96,6 +105,7 @@ public RestApiOperationParameter( this.ArrayItemType = arrayItemType; this.DefaultValue = defaultValue; this.Description = description; + this.Format = format; this.Schema = schema; } } diff --git a/dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperationPayloadProperty.cs b/dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperationPayloadProperty.cs index f83152ea1d0e..b1c8be08aa7c 100644 --- a/dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperationPayloadProperty.cs +++ b/dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperationPayloadProperty.cs @@ -19,6 +19,12 @@ public sealed class RestApiOperationPayloadProperty /// public string Type { get; } + /// + /// 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 + /// + public string? Format { get; } + /// /// The property description. /// @@ -52,6 +58,8 @@ public sealed class RestApiOperationPayloadProperty /// A flag specifying if the property is required or not. /// A list of properties for the payload property. /// A description of the property. + /// 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 /// The schema of the payload property. /// The default value of the property. /// Returns a new instance of the class. @@ -61,6 +69,7 @@ public RestApiOperationPayloadProperty( bool isRequired, IList properties, string? description = null, + string? format = null, KernelJsonSchema? schema = null, object? defaultValue = null) { @@ -70,6 +79,7 @@ public RestApiOperationPayloadProperty( this.Description = description; this.Properties = properties; this.Schema = schema; + this.Format = format; this.DefaultValue = defaultValue; } } diff --git a/dotnet/src/Functions/Functions.OpenApi/OpenApi/OpenApiDocumentParser.cs b/dotnet/src/Functions/Functions.OpenApi/OpenApi/OpenApiDocumentParser.cs index 7fe7bd28e9ac..2d6b856b4700 100644 --- a/dotnet/src/Functions/Functions.OpenApi/OpenApi/OpenApiDocumentParser.cs +++ b/dotnet/src/Functions/Functions.OpenApi/OpenApi/OpenApiDocumentParser.cs @@ -291,6 +291,7 @@ private static List CreateRestApiOperationParameters( parameter.Schema.Items?.Type, GetParameterValue(parameter.Schema.Default, "parameter", parameter.Name), parameter.Description, + parameter.Schema.Format, parameter.Schema.ToJsonSchema() ); @@ -371,6 +372,7 @@ private static List GetPayloadProperties(string requiredProperties.Contains(propertyName), GetPayloadProperties(operationId, propertySchema, requiredProperties, level + 1), propertySchema.Description, + propertySchema.Format, propertySchema.ToJsonSchema(), GetParameterValue(propertySchema.Default, "payload property", propertyName)); diff --git a/dotnet/src/Functions/Functions.OpenApi/OpenApiKernelPluginFactory.cs b/dotnet/src/Functions/Functions.OpenApi/OpenApiKernelPluginFactory.cs index b0dc3b05ba39..fe72d9e40b78 100644 --- a/dotnet/src/Functions/Functions.OpenApi/OpenApiKernelPluginFactory.cs +++ b/dotnet/src/Functions/Functions.OpenApi/OpenApiKernelPluginFactory.cs @@ -255,7 +255,7 @@ async Task 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(); @@ -361,6 +361,34 @@ private static string ConvertOperationIdToValidFunctionName(string operationId, return result; } + /// + /// Converts the parameter type to a C# object. + /// + /// The REST API operation parameter. + /// + 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 + }; + } + /// /// Used to convert operationId to SK function names. /// @@ -373,5 +401,4 @@ private static string ConvertOperationIdToValidFunctionName(string operationId, #endif #endregion - } diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV20Tests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV20Tests.cs index 5e088eca8d06..1e3109c0c1ff 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV20Tests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV20Tests.cs @@ -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] @@ -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 operations, string operationId, RestApiOperationParameterLocation location, string name) { diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV30Tests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV30Tests.cs index d12871d192d0..cb9eec5eb508 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV30Tests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV30Tests.cs @@ -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] @@ -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 transformer) { var json = JsonSerializer.Deserialize(openApiDocument); diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV31Tests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV31Tests.cs index 65d9ca5eccd5..60e182f1bfc6 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV31Tests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV31Tests.cs @@ -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] @@ -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> transformer) { var serializer = new SharpYaml.Serialization.Serializer(); diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiKernelPluginFactoryTests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiKernelPluginFactoryTests.cs index 44cc13c9d364..11c7b910d8b9 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiKernelPluginFactoryTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiKernelPluginFactoryTests.cs @@ -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 _)); } @@ -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() { diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV2_0.json b/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV2_0.json index b323f1c50f47..f01ab6609171 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV2_0.json +++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV2_0.json @@ -349,6 +349,84 @@ "key2": "value2" } } + }, + "/test-parameter-data-types/{string-parameter}": { + "put": { + "description": "Operation to test parameter data types.", + "operationId": "TestParameterDataTypes", + "parameters": [ + { + "in": "path", + "name": "string-parameter", + "default": "string-value", + "required": true, + "type": "string" + }, + { + "in": "query", + "name": "boolean-parameter", + "default": true, + "type": "boolean" + }, + { + "in": "query", + "name": "number-parameter", + "default": -12.01, + "type": "number" + }, + { + "in": "header", + "name": "int32-parameter", + "type": "integer", + "format": "int32" + }, + { + "in": "header", + "name": "int64-parameter", + "type": "integer", + "format": "int64" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "properties": { + "attributes": { + "description": "attributes", + "properties": { + "double-parameter": { + "type": "number", + "format": "double", + "default": -12.01 + } + }, + "type": "object" + }, + "float-parameter": { + "type": "number", + "format": "float", + "default": 12.01 + }, + "integer-parameter": { + "type": "integer", + "default": 123 + } + }, + "type": "object" + } + } + ], + "responses": { + "200": { + "description": "The OK response", + "schema": { + "type": "string" + } + } + }, + "summary": "Get secret" + } } }, "produces": [], diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV3_0.json b/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV3_0.json index 118c08dbbf6c..9b9a9ed48585 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV3_0.json +++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV3_0.json @@ -331,6 +331,94 @@ "key2": "value2" } } + }, + "/test-parameter-data-types/{string-parameter}": { + "put": { + "summary": "Get secret", + "description": "Operation to test parameter data types.", + "operationId": "TestParameterDataTypes", + "parameters": [ + { + "name": "string-parameter", + "in": "path", + "required": true, + "schema": { + "type": "string", + "default": "string-value" + } + }, + { + "name": "boolean-parameter", + "in": "query", + "schema": { + "type": "boolean", + "default": true + } + }, + { + "name": "number-parameter", + "in": "query", + "schema": { + "type": "number", + "default": -12.01 + } + }, + { + "name": "int32-parameter", + "in": "header", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "int64-parameter", + "in": "header", + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "attributes": { + "type": "object", + "properties": { + "double-parameter": { + "type": "number", + "format": "double", + "default": -12.01 + } + }, + "description": "attributes" + }, + "float-parameter": { + "type": "number", + "format": "float", + "default": 12.01 + }, + "integer-parameter": { + "type": "integer", + "default": 123 + } + } + } + } + }, + "required": true, + "x-bodyName": "body" + }, + "responses": { + "200": { + "description": "The OK response" + } + } + } } }, "components": { diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV3_1.yaml b/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV3_1.yaml index aa0a4b0535c4..2ecbdde154a0 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV3_1.yaml +++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/TestPlugins/documentV3_1.yaml @@ -222,6 +222,64 @@ paths: x-object-extension: key1: value1 key2: value2 + '/test-parameter-data-types/{string-parameter}': + put: + summary: Get secret + description: Operation to test parameter data types. + operationId: TestParameterDataTypes + parameters: + - name: string-parameter + in: path + required: true + schema: + type: string + default: string-value + - name: boolean-parameter + in: query + schema: + type: boolean + default: true + - name: number-parameter + in: query + schema: + type: number + default: -12.01 + - name: int32-parameter + in: header + schema: + type: integer + format: int32 + - name: int64-parameter + in: header + schema: + type: integer + format: int64 + requestBody: + content: + application/json: + schema: + type: object + properties: + attributes: + type: object + properties: + double-parameter: + type: number + format: double + default: -12.01 + description: attributes + float-parameter: + type: number + format: float + default: 12.01 + integer-parameter: + type: integer + default: 123 + required: true + x-bodyName: body + responses: + '200': + description: The OK response components: securitySchemes: oauth2_auth: