Skip to content

Commit

Permalink
.Net: [OpenApi][DataTypeSupport][Part2] Headers serialization (#4133)
Browse files Browse the repository at this point in the history
This is the second PR to add header serialization. The first PR can be
found at #4122.
   
### Description  
This PR introduces the following changes:  
- A new `SimpleStyleParameterSerializer` is added to serialize headers
in accordance with the OpenAPI specification -
https://swagger.io/docs/specification/serialization/
<img width="526" alt="image"
src="https://github.com/microsoft/semantic-kernel/assets/68852919/b2fc55a1-ac55-40e1-8b32-2f2decda4bc3">
- The `RestApiOperation.RestARenderHeaders` method has been updated to
use the serializer.


### Contribution Checklist

<!-- Before submitting this PR, please make sure: -->

- [x] The code builds clean without any errors or warnings
- [x] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [x] All unit tests pass, and I have added new tests where possible
- [x] I didn't break anyone 😄
  • Loading branch information
SergeyMenshykh committed Dec 10, 2023
1 parent e5de7e7 commit 87f1475
Show file tree
Hide file tree
Showing 14 changed files with 292 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.SemanticKernel.Plugins.OpenApi.Builders.Serialization;
using Microsoft.SemanticKernel.Plugins.OpenApi.Model;
using Microsoft.SemanticKernel.Plugins.OpenApi.Serialization;

namespace Microsoft.SemanticKernel.Plugins.OpenApi.Builders;

Expand Down
32 changes: 22 additions & 10 deletions dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using Microsoft.SemanticKernel.Plugins.OpenApi.Serialization;

namespace Microsoft.SemanticKernel.Plugins.OpenApi.Model;

Expand Down Expand Up @@ -119,24 +120,30 @@ public Uri BuildOperationUrl(IDictionary<string, string> arguments, Uri? serverU
{
var headers = new Dictionary<string, string>();

var headersMetadata = this.Parameters.Where(p => p.Location == RestApiOperationParameterLocation.Header);
var parameters = this.Parameters.Where(p => p.Location == RestApiOperationParameterLocation.Header);

foreach (var headerMetadata in headersMetadata)
foreach (var parameter in parameters)
{
var headerName = headerMetadata.Name;

// Try to resolve header value in arguments.
if (arguments.TryGetValue(headerName, out string? value) && value is not null)
if (!arguments.TryGetValue(parameter.Name, out string? argument) || argument is null)
{
headers.Add(headerName, value!);
// Throw an exception if the parameter is a required one but no value is provided.
if (parameter.IsRequired)
{
throw new KernelException($"No argument is provided for the '{parameter.Name}' required parameter of the operation - '{this.Id}'.");
}

// Skipping not required parameter if no argument provided for it.
continue;
}

// If a parameter is required, its value should always be provided.
if (headerMetadata.IsRequired)
var parameterStyle = parameter.Style ?? RestApiOperationParameterStyle.Simple;

if (!s_parameterSerializers.TryGetValue(parameterStyle, out var serializer))
{
throw new KernelException($"No argument or value is provided for the '{headerName}' required header of the operation - '{this.Id}'.'");
throw new KernelException($"The headers parameter '{parameterStyle}' serialization style is not supported.");
}

headers.Add(parameter.Name, serializer.Invoke(parameter, argument));
}

return headers;
Expand Down Expand Up @@ -208,5 +215,10 @@ private Uri GetServerUrl(Uri? serverUrlOverride, Uri? apiHostUrl)

private static readonly Regex s_urlParameterMatch = new(@"\{([\w-]+)\}");

private static readonly Dictionary<RestApiOperationParameterStyle, Func<RestApiOperationParameter, string, string>> s_parameterSerializers = new()
{
{ RestApiOperationParameterStyle.Simple, SimpleStyleParameterSerializer.Serialize },
};

# endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
using System.Text.Json.Nodes;
using System.Web;

namespace Microsoft.SemanticKernel.Plugins.OpenApi.Builders.Serialization;
namespace Microsoft.SemanticKernel.Plugins.OpenApi.Serialization;

/// <summary>
/// This class provides methods for serializing values of array parameters.
Expand Down Expand Up @@ -35,14 +35,15 @@ public static string SerializeArrayAsSeparateParameters(string name, JsonArray a
/// </summary>
/// <param name="array">The array containing the items to be serialized.</param>
/// <param name="delimiter">The delimiter used to separate items.</param>
/// <param name="encode">Flag specifying whether to encode items or not.</param>
/// <returns>A string containing the serialized parameter.</returns>
public static string SerializeArrayAsDelimitedValues(JsonArray array, string delimiter)
public static string SerializeArrayAsDelimitedValues(JsonArray array, string delimiter, bool encode = true)
{
var values = new List<string?>();

foreach (var item in array)
{
values.Add(HttpUtility.UrlEncode(item?.ToString()));
values.Add(encode ? HttpUtility.UrlEncode(item?.ToString()) : item?.ToString());
}

return string.Join(delimiter, values);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using System.Web;
using Microsoft.SemanticKernel.Plugins.OpenApi.Model;

namespace Microsoft.SemanticKernel.Plugins.OpenApi.Builders.Serialization;
namespace Microsoft.SemanticKernel.Plugins.OpenApi.Serialization;

/// <summary>
/// Serializes REST API operation parameter of the 'Form' style.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
using System.Text.Json.Nodes;
using Microsoft.SemanticKernel.Plugins.OpenApi.Model;

namespace Microsoft.SemanticKernel.Plugins.OpenApi.Builders.Serialization;
namespace Microsoft.SemanticKernel.Plugins.OpenApi.Serialization;

/// <summary>
/// Serializes REST API operation parameter of the 'PipeDelimited' style.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Text.Json.Nodes;
using Microsoft.SemanticKernel.Plugins.OpenApi.Model;

namespace Microsoft.SemanticKernel.Plugins.OpenApi.Serialization;

/// <summary>
/// Serializes REST API operation parameter of the 'Simple' style.
/// </summary>
internal static class SimpleStyleParameterSerializer
{
/// <summary>
/// Serializes a REST API operation `Simple` style parameter.
/// </summary>
/// <param name="parameter">The REST API operation parameter to serialize.</param>
/// <param name="argument">The parameter argument.</param>
/// <returns>The serialized parameter.</returns>
public static string Serialize(RestApiOperationParameter parameter, string argument)
{
const string ArrayType = "array";

Verify.NotNull(parameter);

if (parameter.Style != RestApiOperationParameterStyle.Simple)
{
throw new ArgumentException($"Unexpected Rest API operation parameter style - `{parameter.Style}`", nameof(parameter));
}

// Serializing parameters of array type.
if (parameter.Type == ArrayType)
{
return SerializeArrayParameter(parameter, argument);
}

// Serializing parameters of primitive - integer, string, etc type.
return argument;
}

/// <summary>
/// Serializes an array-type parameter.
/// </summary>
/// <param name="parameter">The REST API operation parameter to serialize.</param>
/// <param name="argument">The argument value.</param>
/// <returns>The serialized parameter string.</returns>
private static string SerializeArrayParameter(RestApiOperationParameter parameter, string argument)
{
if (JsonNode.Parse(argument) is not JsonArray array)
{
throw new KernelException($"Can't deserialize parameter name '{parameter.Name}' argument '{argument}' to JSON array.");
}

return ArrayParameterValueSerializer.SerializeArrayAsDelimitedValues(array, delimiter: ",", encode: false); //1,2,3
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
using System.Text.Json.Nodes;
using Microsoft.SemanticKernel.Plugins.OpenApi.Model;

namespace Microsoft.SemanticKernel.Plugins.OpenApi.Builders.Serialization;
namespace Microsoft.SemanticKernel.Plugins.OpenApi.Serialization;

/// <summary>
/// Serializes REST API operation parameter of the 'SpaceDelimited' style.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

using System;
using System.Text.Json.Nodes;
using Microsoft.SemanticKernel.Plugins.OpenApi.Builders.Serialization;
using Microsoft.SemanticKernel.Plugins.OpenApi.Serialization;
using Xunit;

namespace SemanticKernel.Functions.UnitTests.OpenApi.Builders.Serialization;
Expand Down Expand Up @@ -92,7 +92,7 @@ public void ItShouldAllowDuplicatesWhenCreatingParameterWithDelimitedValuePerArr
public void ItShouldEncodeSpecialSymbolsInSeparateParameterValues(string specialSymbol, string encodedEquivalent)
{
// Arrange
var array = new JsonArray($"{specialSymbol}");
var array = new JsonArray(specialSymbol);

// Act
var result = ArrayParameterValueSerializer.SerializeArrayAsSeparateParameters("id", array, delimiter: "&");
Expand All @@ -111,7 +111,7 @@ public void ItShouldEncodeSpecialSymbolsInSeparateParameterValues(string special
public void ItShouldEncodeSpecialSymbolsInDelimitedParameterValues(string specialSymbol, string encodedEquivalent)
{
// Arrange
var array = new JsonArray($"{specialSymbol}");
var array = new JsonArray(specialSymbol);

// Act
var result = ArrayParameterValueSerializer.SerializeArrayAsDelimitedValues(array, delimiter: "%20");
Expand All @@ -121,4 +121,23 @@ public void ItShouldEncodeSpecialSymbolsInDelimitedParameterValues(string specia

Assert.EndsWith(encodedEquivalent, result, StringComparison.Ordinal);
}

[Theory]
[InlineData(":", ":")]
[InlineData("/", "/")]
[InlineData("?", "?")]
[InlineData("#", "#")]
public void ItShouldNotEncodeSpecialSymbolsInDelimitedParameterValuesIfEncodingDisabled(string specialSymbol, string expectedValue)
{
// Arrange
var array = new JsonArray(specialSymbol);

// Act
var result = ArrayParameterValueSerializer.SerializeArrayAsDelimitedValues(array, delimiter: ",", encode: false);

// Assert
Assert.NotNull(result);

Assert.EndsWith(expectedValue, result, StringComparison.Ordinal);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using Microsoft.SemanticKernel.Plugins.OpenApi.Builders.Serialization;
using Microsoft.SemanticKernel.Plugins.OpenApi.Model;
using Microsoft.SemanticKernel.Plugins.OpenApi.Serialization;
using Xunit;

namespace SemanticKernel.Functions.UnitTests.OpenApi.Builders.Serialization;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using Microsoft.SemanticKernel.Plugins.OpenApi.Builders.Serialization;
using Microsoft.SemanticKernel.Plugins.OpenApi.Model;
using Microsoft.SemanticKernel.Plugins.OpenApi.Serialization;
using Xunit;

namespace SemanticKernel.Functions.UnitTests.OpenApi.Builders.Serialization;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using Microsoft.SemanticKernel.Plugins.OpenApi.Model;
using Microsoft.SemanticKernel.Plugins.OpenApi.Serialization;
using Xunit;

namespace SemanticKernel.Functions.UnitTests.OpenApi.Builders.Serialization;

public class SimpleStyleParametersSerializerTests
{
[Fact]
public void ItShouldCreateParameterWithCommaSeparatedValuePerArrayItem()
{
// Arrange
var parameter = new RestApiOperationParameter(name: "id", type: "array", isRequired: true, expand: false, location: RestApiOperationParameterLocation.Header, style: RestApiOperationParameterStyle.Simple, arrayItemType: "integer");

// Act
var result = SimpleStyleParameterSerializer.Serialize(parameter, "[1,2,3]");

// Assert
Assert.NotNull(result);

Assert.Equal("1,2,3", result);
}

[Fact]
public void ItShouldCreateParameterForPrimitiveValue()
{
// Arrange
var parameter = new RestApiOperationParameter(name: "id", type: "integer", isRequired: true, expand: false, location: RestApiOperationParameterLocation.Header, style: RestApiOperationParameterStyle.Simple);

// Act
var result = SimpleStyleParameterSerializer.Serialize(parameter, "28");

// Assert
Assert.NotNull(result);

Assert.Equal("28", result);
}

[Theory]
[InlineData(":", ":")]
[InlineData("/", "/")]
[InlineData("?", "?")]
[InlineData("#", "#")]
public void ItShouldNotEncodeSpecialSymbolsInPrimitiveParameterValues(string specialSymbol, string expectedSymbol)
{
// Arrange
var parameter = new RestApiOperationParameter(name: "id", type: "string", isRequired: true, expand: false, location: RestApiOperationParameterLocation.Header, style: RestApiOperationParameterStyle.Simple);

// Act
var result = SimpleStyleParameterSerializer.Serialize(parameter, $"fake_query_param_value{specialSymbol}");

// Assert
Assert.NotNull(result);

Assert.EndsWith(expectedSymbol, result, StringComparison.Ordinal);
}

[Theory]
[InlineData(":", ":")]
[InlineData("/", "/")]
[InlineData("?", "?")]
[InlineData("#", "#")]
public void ItShouldEncodeSpecialSymbolsInCommaSeparatedParameterValues(string specialSymbol, string expectedSymbol)
{
// Arrange
var parameter = new RestApiOperationParameter(name: "id", type: "array", isRequired: true, expand: false, location: RestApiOperationParameterLocation.Header, style: RestApiOperationParameterStyle.Simple);

// Act
var result = SimpleStyleParameterSerializer.Serialize(parameter, $"[\"{specialSymbol}\"]");

// Assert
Assert.NotNull(result);

Assert.EndsWith(expectedSymbol, result, StringComparison.Ordinal);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using Microsoft.SemanticKernel.Plugins.OpenApi.Builders.Serialization;
using Microsoft.SemanticKernel.Plugins.OpenApi.Model;
using Microsoft.SemanticKernel.Plugins.OpenApi.Serialization;
using Xunit;

namespace SemanticKernel.Functions.UnitTests.OpenApi.Builders.Serialization;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,8 @@ public async Task ItShouldAddHeadersToHttpRequestAsync()
// Arrange
var parameters = new List<RestApiOperationParameter>
{
new RestApiOperationParameter(
name: "fake-header",
type: "string",
isRequired: true,
expand: false,
location: RestApiOperationParameterLocation.Header,
style: RestApiOperationParameterStyle.Simple)
new(name: "X-H1", type: "string", isRequired: true, expand: false, location: RestApiOperationParameterLocation.Header, style: RestApiOperationParameterStyle.Simple),
new(name: "X-H2", type: "array", isRequired: true, expand: false, location: RestApiOperationParameterLocation.Header, style: RestApiOperationParameterStyle.Simple)
};

var operation = new RestApiOperation(
Expand All @@ -201,19 +196,22 @@ public async Task ItShouldAddHeadersToHttpRequestAsync()

var arguments = new KernelArguments
{
{ "fake-header", "fake-header-value" }
["X-H1"] = "fake-header-value",
["X-H2"] = "[1,2,3]"
};

var sut = new RestApiOperationRunner(this._httpClient, this._authenticationHandlerMock.Object);
var sut = new RestApiOperationRunner(this._httpClient, this._authenticationHandlerMock.Object, userAgent: "fake-agent");

// Act
await sut.RunAsync(operation, arguments);

// Assert - 2 headers: 1 from the test and the useragent added internally
// Assert - 3 headers: 2 from the test and the User-Agent added internally
Assert.NotNull(this._httpMessageHandlerStub.RequestHeaders);
Assert.Equal(2, this._httpMessageHandlerStub.RequestHeaders.Count());
Assert.Equal(3, this._httpMessageHandlerStub.RequestHeaders.Count());

Assert.Contains(this._httpMessageHandlerStub.RequestHeaders, h => h.Key == "fake-header" && h.Value.Contains("fake-header-value"));
Assert.Contains(this._httpMessageHandlerStub.RequestHeaders, h => h.Key == "User-Agent" && h.Value.Contains("fake-agent"));
Assert.Contains(this._httpMessageHandlerStub.RequestHeaders, h => h.Key == "X-H1" && h.Value.Contains("fake-header-value"));
Assert.Contains(this._httpMessageHandlerStub.RequestHeaders, h => h.Key == "X-H2" && h.Value.Contains("1,2,3"));
}

[Fact]
Expand All @@ -222,7 +220,7 @@ public async Task ItShouldAddUserAgentHeaderToHttpRequestIfConfiguredAsync()
// Arrange
var parameters = new List<RestApiOperationParameter>
{
new RestApiOperationParameter(
new(
name: "fake-header",
type: "string",
isRequired: true,
Expand Down
Loading

0 comments on commit 87f1475

Please sign in to comment.