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

.Net: Handle missing operation id in OpenApi spec #7344

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -164,7 +165,7 @@ internal static async Task<KernelPlugin> CreateOpenApiPluginAsync(
catch (Exception ex) when (!ex.IsCriticalException())
{
//Logging the exception and keep registering other Rest functions
logger.LogWarning(ex, "Something went wrong while rendering the Rest function. Function: {0}.{1}. Error: {2}",
logger.LogWarning(ex, "Something went wrong while rendering the Rest function. Function: {PluginName}.{OperationId}. Error: {Message}",
pluginName, operation.Id, ex.Message);
}
}
Expand Down Expand Up @@ -274,7 +275,7 @@ async Task<RestApiOperationResponse> ExecuteAsync(Kernel kernel, KernelFunction
method: ExecuteAsync,
new KernelFunctionFromMethodOptions
{
FunctionName = ConvertOperationIdToValidFunctionName(operation.Id, logger),
FunctionName = ConvertOperationToValidFunctionName(operation, logger),
Description = operation.Description,
Parameters = parameters,
ReturnParameter = returnParameter,
Expand All @@ -291,6 +292,37 @@ async Task<RestApiOperationResponse> ExecuteAsync(Kernel kernel, KernelFunction
/// <summary>The metadata property bag key to use for the list of extension values provided in the swagger file at the operation level.</summary>
private const string OperationExtensionsMetadataKey = "operation-extensions";

/// <summary>
/// Converts operation id to valid <see cref="KernelFunction"/> name.
/// A function name can contain only ASCII letters, digits, and underscores.
/// </summary>
/// <param name="operation">The REST API operation.</param>
/// <param name="logger">The logger.</param>
/// <returns>Valid KernelFunction name.</returns>
private static string ConvertOperationToValidFunctionName(RestApiOperation operation, ILogger logger)
{
if (!string.IsNullOrWhiteSpace(operation.Id))
{
return ConvertOperationIdToValidFunctionName(operationId: operation.Id, logger: logger);
}

// Tokenize operation path on forward and back slashes
string[] tokens = operation.Path.Split('/', '\\');
StringBuilder result = new();
result.Append(CultureInfo.CurrentCulture.TextInfo.ToTitleCase(operation.Method.ToString()));

foreach (string token in tokens)
{
// Removes all characters that are not ASCII letters, digits, and underscores.
string formattedToken = RemoveInvalidCharsRegex().Replace(token, "");
Copy link
Contributor

@RogerBarret0 RogerBarret0 Jul 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit. string.Empty

Suggested change
string formattedToken = RemoveInvalidCharsRegex().Replace(token, "");
string formattedToken = RemoveInvalidCharsRegex().Replace(token, string.Empty);

result.Append(CultureInfo.CurrentCulture.TextInfo.ToTitleCase(formattedToken.ToLower(CultureInfo.CurrentCulture)));
}

logger.LogInformation("""Operation method "{Method}" with path "{Path}" converted to "{Result}" to comply with SK Function name requirements. Use "{Result}" when invoking function.""", operation.Method, operation.Path, result, result);

return result.ToString();
}

/// <summary>
/// Converts operation id to valid <see cref="KernelFunction"/> name.
/// A function name can contain only ASCII letters, digits, and underscores.
Expand Down Expand Up @@ -323,7 +355,7 @@ private static string ConvertOperationIdToValidFunctionName(string operationId,
result += CultureInfo.CurrentCulture.TextInfo.ToTitleCase(formattedToken.ToLower(CultureInfo.CurrentCulture));
}

logger.LogInformation("""Operation name "{0}" converted to "{1}" to comply with SK Function name requirements. Use "{2}" when invoking function.""", operationId, result, result);
logger.LogInformation("""Operation name "{OperationId}" converted to "{Result}" to comply with SK Function name requirements. Use "{Result}" when invoking function.""", operationId, result, result);

return result;
}
Expand All @@ -336,7 +368,7 @@ private static string ConvertOperationIdToValidFunctionName(string operationId,
private static partial Regex RemoveInvalidCharsRegex();
#else
private static Regex RemoveInvalidCharsRegex() => s_removeInvalidCharsRegex;
private static readonly Regex s_removeInvalidCharsRegex = new("[^0-9A-Za-z_]", RegexOptions.Compiled);
private static readonly Regex s_removeInvalidCharsRegex = new("[^0-9A-Za-z_./-/{/}]", RegexOptions.Compiled);
#endif

#endregion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,44 @@ public async Task ItShouldReplicateMetadataToOperationAsync(string documentFileN
Assert.Contains("x-object-extension", nonNullOperationExtensions.Keys);
}

[Fact]
public async Task ItShouldHandleEmptyOperationNameAsync()
{
// Arrange
var openApiDocument = ResourcePluginsProvider.LoadFromResource("documentV3_0.json");

using var content = OpenApiTestHelper.ModifyOpenApiDocument(openApiDocument, (doc) =>
{
doc["paths"]!["/secrets/{secret-name}"]!["get"]!["operationId"] = "";
});

// Act
var plugin = await OpenApiKernelPluginFactory.CreateFromOpenApiAsync("fakePlugin", content, this._executionParameters);

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

[Fact]
public async Task ItShouldHandleNullOperationNameAsync()
{
// Arrange
var openApiDocument = ResourcePluginsProvider.LoadFromResource("documentV3_0.json");

using var content = OpenApiTestHelper.ModifyOpenApiDocument(openApiDocument, (doc) =>
{
doc["paths"]!["/secrets/{secret-name}"]!["get"]!.AsObject().Remove("operationId");
});

// Act
var plugin = await OpenApiKernelPluginFactory.CreateFromOpenApiAsync("fakePlugin", content, this._executionParameters);

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

[Fact]
public void Dispose()
{
Expand Down
Loading