Skip to content

Commit

Permalink
.Net: DefaultValue for OpenAPI payload properties (#4612)
Browse files Browse the repository at this point in the history
### Motivation and Context  
Currently, the default values of parameters in OpenAPI plugins have the
'string?' type, even though their original type specified in the OpenAPI
document schema might be different. A dedicated type conversion
functionality exists to convert from the original type to a string.
However, both the string type for the parameters/arguments and the type
conversion logic are no longer necessary, as SK has moved away from
string-type arguments and recently added support for .NET primitive
types and complex types.
   
Furthermore, the `RestApiOperationPayloadProperty` class does not have a
property for default values, so a default value specified in an OpenAPI
document is lost and cannot be accessed from the code.
   
### Description  
1. The type of the `DefaultValue` property in the
`RestApiOperationParameter` class has been changed from string? to
object? to better represent the actual type of the parameter's default
value and eliminate unnecessary conversion to string.
   
2. A new `DefaultValue` property has been added to the
`RestApiOperationPayloadProperty`, making it consistent with the
RestApiOperationParameter class.
  • Loading branch information
SergeyMenshykh authored Jan 19, 2024
1 parent b2bbd41 commit a5bc63d
Show file tree
Hide file tree
Showing 11 changed files with 533 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ private static List<RestApiOperationParameter> GetParametersFromPayloadMetadata(
expand: false,
RestApiOperationParameterLocation.Body,
RestApiOperationParameterStyle.Simple,
defaultValue: property.DefaultValue,
description: property.Description,
schema: property.Schema));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public sealed class RestApiOperationParameter
/// <summary>
/// The default value.
/// </summary>
public string? DefaultValue { get; }
public object? DefaultValue { get; }

/// <summary>
/// Specifies whether arrays and objects should generate separate parameters for each array item or object property.
Expand Down Expand Up @@ -83,7 +83,7 @@ public RestApiOperationParameter(
RestApiOperationParameterLocation location,
RestApiOperationParameterStyle? style = null,
string? arrayItemType = null,
string? defaultValue = null,
object? defaultValue = null,
string? description = null,
KernelJsonSchema? schema = null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,28 +39,37 @@ public sealed class RestApiOperationPayloadProperty
/// </summary>
public KernelJsonSchema? Schema { get; }

/// <summary>
/// The default value.
/// </summary>
public object? DefaultValue { get; }

/// <summary>
/// Creates an instance of a <see cref="RestApiOperationPayloadProperty"/> class.
/// </summary>
/// <param name="name">Property name.</param>
/// <param name="type">Property type.</param>
/// <param name="isRequired">Flag specifying if the property is required or not.</param>
/// <param name="properties">Properties.</param>
/// <param name="description">Property description.</param>
/// <param name="name">The name of the property.</param>
/// <param name="type">The type of the property.</param>
/// <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="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>
public RestApiOperationPayloadProperty(
string name,
string type,
bool isRequired,
IList<RestApiOperationPayloadProperty> properties,
string? description = null,
KernelJsonSchema? schema = null)
KernelJsonSchema? schema = null,
object? defaultValue = null)
{
this.Name = name;
this.Type = type;
this.IsRequired = isRequired;
this.Description = description;
this.Properties = properties;
this.Schema = schema;
this.DefaultValue = defaultValue;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
Expand Down Expand Up @@ -313,7 +312,8 @@ private static List<RestApiOperationPayloadProperty> GetPayloadProperties(string
requiredProperties.Contains(propertyName),
GetPayloadProperties(operationId, propertySchema, requiredProperties, level + 1),
propertySchema.Description,
propertySchema.ToJsonSchema());
propertySchema.ToJsonSchema(),
GetParameterValue(propertySchema.Default));

result.Add(property);
}
Expand All @@ -326,7 +326,7 @@ private static List<RestApiOperationPayloadProperty> GetPayloadProperties(string
/// </summary>
/// <param name="valueMetadata">The value metadata.</param>
/// <returns>The parameter value.</returns>
private static string? GetParameterValue(IOpenApiAny valueMetadata)
private static object? GetParameterValue(IOpenApiAny valueMetadata)
{
if (valueMetadata is not IOpenApiPrimitive value)
{
Expand All @@ -335,17 +335,17 @@ private static List<RestApiOperationPayloadProperty> GetPayloadProperties(string

return value.PrimitiveType switch
{
PrimitiveType.Integer => ((OpenApiInteger)value).Value.ToString(CultureInfo.InvariantCulture),
PrimitiveType.Long => ((OpenApiLong)value).Value.ToString(CultureInfo.InvariantCulture),
PrimitiveType.Float => ((OpenApiFloat)value).Value.ToString(CultureInfo.InvariantCulture),
PrimitiveType.Double => ((OpenApiDouble)value).Value.ToString(CultureInfo.InvariantCulture),
PrimitiveType.String => ((OpenApiString)value).Value.ToString(CultureInfo.InvariantCulture),
PrimitiveType.Byte => Convert.ToBase64String(((OpenApiByte)value).Value),
PrimitiveType.Binary => Encoding.UTF8.GetString(((OpenApiBinary)value).Value),
PrimitiveType.Boolean => ((OpenApiBoolean)value).Value.ToString(CultureInfo.InvariantCulture),
PrimitiveType.Date => ((OpenApiDate)value).Value.ToString("o").Substring(0, 10),
PrimitiveType.DateTime => ((OpenApiDateTime)value).Value.ToString(CultureInfo.InvariantCulture),
PrimitiveType.Password => ((OpenApiPassword)value).Value.ToString(CultureInfo.InvariantCulture),
PrimitiveType.Integer => ((OpenApiInteger)value).Value,
PrimitiveType.Long => ((OpenApiLong)value).Value,
PrimitiveType.Float => ((OpenApiFloat)value).Value,
PrimitiveType.Double => ((OpenApiDouble)value).Value,
PrimitiveType.String => ((OpenApiString)value).Value,
PrimitiveType.Byte => ((OpenApiByte)value).Value,
PrimitiveType.Binary => ((OpenApiBinary)value).Value,
PrimitiveType.Boolean => ((OpenApiBoolean)value).Value,
PrimitiveType.Date => ((OpenApiDate)value).Value,
PrimitiveType.DateTime => ((OpenApiDateTime)value).Value,
PrimitiveType.Password => ((OpenApiPassword)value).Value,
_ => throw new KernelException($"The value type - {value.PrimitiveType} is not supported."),
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,18 +280,53 @@ private static RestApiOperation CreateTestOperation(string method, RestApiOperat

private static RestApiOperationPayload CreateTestJsonPayload()
{
var name = new RestApiOperationPayloadProperty("name", "string", true, new List<RestApiOperationPayloadProperty>(), "The name.");

var leader = new RestApiOperationPayloadProperty("leader", "string", true, new List<RestApiOperationPayloadProperty>(), "The leader.");

var landmarks = new RestApiOperationPayloadProperty("landmarks", "array", false, new List<RestApiOperationPayloadProperty>(), "The landmarks.");
var location = new RestApiOperationPayloadProperty("location", "object", true, new[] { landmarks }, "The location.");

var rulingCouncil = new RestApiOperationPayloadProperty("rulingCouncil", "object", true, new[] { leader }, "The ruling council.");

var population = new RestApiOperationPayloadProperty("population", "integer", true, new List<RestApiOperationPayloadProperty>(), "The population.");

var hasMagicWards = new RestApiOperationPayloadProperty("hasMagicWards", "boolean", false, new List<RestApiOperationPayloadProperty>());
var name = new RestApiOperationPayloadProperty(
name: "name",
type: "string",
isRequired: true,
properties: new List<RestApiOperationPayloadProperty>(),
description: "The name.");

var leader = new RestApiOperationPayloadProperty(
name: "leader",
type: "string",
isRequired: true,
properties: new List<RestApiOperationPayloadProperty>(),
description: "The leader.");

var landmarks = new RestApiOperationPayloadProperty(
name: "landmarks",
type: "array",
isRequired: false,
properties: new List<RestApiOperationPayloadProperty>(),
description: "The landmarks.");

var location = new RestApiOperationPayloadProperty(
name: "location",
type: "object",
isRequired: true,
properties: new[] { landmarks },
description: "The location.");

var rulingCouncil = new RestApiOperationPayloadProperty(
name: "rulingCouncil",
type: "object",
isRequired: true,
properties: new[] { leader },
description: "The ruling council.");

var population = new RestApiOperationPayloadProperty(
name: "population",
type: "integer",
isRequired: true,
properties: new List<RestApiOperationPayloadProperty>(),
description: "The population.");

var hasMagicWards = new RestApiOperationPayloadProperty(
name: "hasMagicWards",
type: "boolean",
isRequired: false,
properties: new List<RestApiOperationPayloadProperty>());

return new RestApiOperationPayload("application/json", new[] { name, location, rulingCouncil, population, hasMagicWards });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ public async Task ItCanExtractSimpleTypeHeaderParameterMetadataSuccessfullyAsync
var apiVersion = GetParameterMetadata(operations, "SetSecret", RestApiOperationParameterLocation.Header, "X-API-Version");

Assert.Equal("integer", apiVersion.Type);
Assert.Equal("10", apiVersion.DefaultValue);
Assert.Equal(10, apiVersion.DefaultValue);
Assert.Equal("Requested API version.", apiVersion.Description);
Assert.True(apiVersion.IsRequired);
}
Expand Down Expand Up @@ -225,7 +225,7 @@ public async Task ItCanExtractAllPathsAsOperationsAsync()
var operations = await this._sut.ParseAsync(this._openApiDocument);

// Assert
Assert.Equal(3, operations.Count);
Assert.Equal(4, operations.Count);
}

[Fact]
Expand Down Expand Up @@ -293,6 +293,65 @@ public async Task ItCanParseResponsesSuccessfullyAsync()
JsonSerializer.Serialize(response.Schema));
}

[Fact]
public async Task ItCanWorkWithDefaultParametersOfVariousTypesAsync()
{
//Act
var operations = await this._sut.ParseAsync(this._openApiDocument);

//Assert
Assert.NotNull(operations);
Assert.True(operations.Any());

var operation = operations.Single(o => o.Id == "TestDefaultValues");
Assert.NotNull(operation);

var parameters = operation.GetParameters();
Assert.Equal(11, parameters.Count);

var stringParameter = parameters.Single(p => p.Name == "string-parameter");
Assert.Equal("string-value", stringParameter.DefaultValue);

var booleanParameter = parameters.Single(p => p.Name == "boolean-parameter");
Assert.True(booleanParameter.DefaultValue is bool value);

var integerParameter = parameters.Single(p => p.Name == "integer-parameter");
Assert.True(integerParameter.DefaultValue is int);
Assert.Equal(281, integerParameter.DefaultValue);

var longParameter = parameters.Single(p => p.Name == "long-parameter");
Assert.True(longParameter.DefaultValue is long);
Assert.Equal((long)-2814, longParameter.DefaultValue);

var floatParameter = parameters.Single(p => p.Name == "float-parameter");
Assert.True(floatParameter.DefaultValue is float);
Assert.Equal((float)12.01, floatParameter.DefaultValue);

var doubleParameter = parameters.Single(p => p.Name == "double-parameter");
Assert.True(doubleParameter.DefaultValue is double);
Assert.Equal((double)-12.01, doubleParameter.DefaultValue);

var encodedCharactersParameter = parameters.Single(p => p.Name == "encoded-characters-parameter");
Assert.True(encodedCharactersParameter.DefaultValue is byte[]);
Assert.Equal(new byte[] { 1, 2, 3, 4, 5 }, encodedCharactersParameter.DefaultValue);

var binaryDataParameter = parameters.Single(p => p.Name == "binary-data-parameter");
Assert.True(binaryDataParameter.DefaultValue is byte[]);
Assert.Equal(new byte[] { 50, 51, 52, 53, 54 }, binaryDataParameter.DefaultValue);

var dateParameter = parameters.Single(p => p.Name == "date-parameter");
Assert.True(dateParameter.DefaultValue is DateTime);
Assert.Equal(new DateTime(2017, 07, 21), dateParameter.DefaultValue);

var dateTimeParameter = parameters.Single(p => p.Name == "date-time-parameter");
Assert.True(dateTimeParameter.DefaultValue is DateTimeOffset);
Assert.Equal(new DateTimeOffset(2017, 07, 21, 17, 32, 28, TimeSpan.Zero), dateTimeParameter.DefaultValue);

var passwordParameter = parameters.Single(p => p.Name == "password-parameter");
Assert.True(passwordParameter.DefaultValue is string);
Assert.Equal("password-value", passwordParameter.DefaultValue);
}

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 @@ -161,7 +161,7 @@ public async Task ItCanExtractSimpleTypeHeaderParameterMetadataSuccessfullyAsync
var apiVersion = GetParameterMetadata(operations, "SetSecret", RestApiOperationParameterLocation.Header, "X-API-Version");

Assert.Equal("integer", apiVersion.Type);
Assert.Equal("10", apiVersion.DefaultValue);
Assert.Equal(10, apiVersion.DefaultValue);
Assert.Equal("Requested API version.", apiVersion.Description);
Assert.True(apiVersion.IsRequired);
}
Expand Down Expand Up @@ -226,7 +226,7 @@ public async Task ItCanExtractAllPathsAsOperationsAsync()
var operations = await this._sut.ParseAsync(this._openApiDocument);

// Assert
Assert.Equal(3, operations.Count);
Assert.Equal(4, operations.Count);
}

[Fact]
Expand Down Expand Up @@ -366,6 +366,65 @@ public async Task ItCanParseResponsesSuccessfullyAsync()
JsonSerializer.Serialize(response.Schema));
}

[Fact]
public async Task ItCanWorkWithDefaultParametersOfVariousTypesAsync()
{
//Act
var operations = await this._sut.ParseAsync(this._openApiDocument);

//Assert
Assert.NotNull(operations);
Assert.True(operations.Any());

var operation = operations.Single(o => o.Id == "TestDefaultValues");
Assert.NotNull(operation);

var parameters = operation.GetParameters();
Assert.Equal(11, parameters.Count);

var stringParameter = parameters.Single(p => p.Name == "string-parameter");
Assert.Equal("string-value", stringParameter.DefaultValue);

var booleanParameter = parameters.Single(p => p.Name == "boolean-parameter");
Assert.True(booleanParameter.DefaultValue is bool value);

var integerParameter = parameters.Single(p => p.Name == "integer-parameter");
Assert.True(integerParameter.DefaultValue is int);
Assert.Equal(281, integerParameter.DefaultValue);

var longParameter = parameters.Single(p => p.Name == "long-parameter");
Assert.True(longParameter.DefaultValue is long);
Assert.Equal((long)-2814, longParameter.DefaultValue);

var floatParameter = parameters.Single(p => p.Name == "float-parameter");
Assert.True(floatParameter.DefaultValue is float);
Assert.Equal((float)12.01, floatParameter.DefaultValue);

var doubleParameter = parameters.Single(p => p.Name == "double-parameter");
Assert.True(doubleParameter.DefaultValue is double);
Assert.Equal((double)-12.01, doubleParameter.DefaultValue);

var encodedCharactersParameter = parameters.Single(p => p.Name == "encoded-characters-parameter");
Assert.True(encodedCharactersParameter.DefaultValue is byte[]);
Assert.Equal(new byte[] { 1, 2, 3, 4, 5 }, encodedCharactersParameter.DefaultValue);

var binaryDataParameter = parameters.Single(p => p.Name == "binary-data-parameter");
Assert.True(binaryDataParameter.DefaultValue is byte[]);
Assert.Equal(new byte[] { 50, 51, 52, 53, 54 }, binaryDataParameter.DefaultValue);

var dateParameter = parameters.Single(p => p.Name == "date-parameter");
Assert.True(dateParameter.DefaultValue is DateTime);
Assert.Equal(new DateTime(2017, 07, 21), dateParameter.DefaultValue);

var dateTimeParameter = parameters.Single(p => p.Name == "date-time-parameter");
Assert.True(dateTimeParameter.DefaultValue is DateTimeOffset);
Assert.Equal(new DateTimeOffset(2017, 07, 21, 17, 32, 28, TimeSpan.Zero), dateTimeParameter.DefaultValue);

var passwordParameter = parameters.Single(p => p.Name == "password-parameter");
Assert.True(passwordParameter.DefaultValue is string);
Assert.Equal("password-value", passwordParameter.DefaultValue);
}

private static MemoryStream ModifyOpenApiDocument(Stream openApiDocument, Action<JsonObject> transformer)
{
var json = JsonSerializer.Deserialize<JsonObject>(openApiDocument);
Expand Down
Loading

0 comments on commit a5bc63d

Please sign in to comment.