Skip to content

Commit 7b22f00

Browse files
.Net: Parse multiple servers (#9558)
### Motivation, Context and Description OpenAPI specification allows specifying multiple servers. This PR refactors the `OpenApiDocumentParser` to return a collection of servers instead of one. --------- Co-authored-by: Mark Wallace <[email protected]>
1 parent 3332079 commit 7b22f00

File tree

12 files changed

+126
-82
lines changed

12 files changed

+126
-82
lines changed

dotnet/src/Functions/Functions.OpenApi.Extensions/Extensions/ApiManifestKernelExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ public static async Task<KernelPlugin> CreatePluginFromApiManifestAsync(
148148
{
149149
foreach (var path in filteredOpenApiDocument.Paths)
150150
{
151-
var operations = OpenApiDocumentParser.CreateRestApiOperations(server, path.Key, path.Value, null, logger);
151+
var operations = OpenApiDocumentParser.CreateRestApiOperations([server], path.Key, path.Value, null, logger);
152152
foreach (RestApiOperation operation in operations)
153153
{
154154
try

dotnet/src/Functions/Functions.OpenApi/Model/RestApiOperation.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public sealed class RestApiOperation
5252
/// <summary>
5353
/// The server.
5454
/// </summary>
55-
internal RestApiOperationServer Server { get; }
55+
public IReadOnlyList<RestApiOperationServer> Servers { get; }
5656

5757
/// <summary>
5858
/// The operation parameters.
@@ -78,7 +78,7 @@ public sealed class RestApiOperation
7878
/// Creates an instance of a <see cref="RestApiOperation"/> class.
7979
/// </summary>
8080
/// <param name="id">The operation identifier.</param>
81-
/// <param name="server">The server.</param>
81+
/// <param name="servers">The servers.</param>
8282
/// <param name="path">The operation path.</param>
8383
/// <param name="method">The operation method.</param>
8484
/// <param name="description">The operation description.</param>
@@ -87,7 +87,7 @@ public sealed class RestApiOperation
8787
/// <param name="responses">The operation responses.</param>
8888
internal RestApiOperation(
8989
string id,
90-
RestApiOperationServer server,
90+
IReadOnlyList<RestApiOperationServer> servers,
9191
string path,
9292
HttpMethod method,
9393
string description,
@@ -96,7 +96,7 @@ internal RestApiOperation(
9696
IReadOnlyDictionary<string, RestApiOperationExpectedResponse>? responses = null)
9797
{
9898
this.Id = id;
99-
this.Server = server;
99+
this.Servers = servers;
100100
this.Path = path;
101101
this.Method = method;
102102
this.Description = description;
@@ -260,10 +260,10 @@ private Uri GetServerUrl(Uri? serverUrlOverride, Uri? apiHostUrl, IDictionary<st
260260
{
261261
serverUrlString = serverUrlOverride.AbsoluteUri;
262262
}
263-
else if (this.Server.Url is not null)
263+
else if (this.Servers is { Count: > 0 } servers && servers[0].Url is { } url)
264264
{
265-
serverUrlString = this.Server.Url;
266-
foreach (var variable in this.Server.Variables)
265+
serverUrlString = url;
266+
foreach (var variable in servers[0].Variables)
267267
{
268268
arguments.TryGetValue(variable.Key, out object? value);
269269
string? strValue = value as string;

dotnet/src/Functions/Functions.OpenApi/OpenApi/OpenApiDocumentParser.cs

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -158,11 +158,9 @@ private static List<RestApiOperation> ExtractRestApiOperations(OpenApiDocument d
158158
{
159159
var result = new List<RestApiOperation>();
160160

161-
var server = document.Servers.FirstOrDefault();
162-
163161
foreach (var pathPair in document.Paths)
164162
{
165-
var operations = CreateRestApiOperations(server, pathPair.Key, pathPair.Value, operationsToExclude, logger);
163+
var operations = CreateRestApiOperations(document.Servers, pathPair.Key, pathPair.Value, operationsToExclude, logger);
166164

167165
result.AddRange(operations);
168166
}
@@ -173,16 +171,15 @@ private static List<RestApiOperation> ExtractRestApiOperations(OpenApiDocument d
173171
/// <summary>
174172
/// Creates REST API operation.
175173
/// </summary>
176-
/// <param name="server">Rest server.</param>
174+
/// <param name="servers">Rest api servers.</param>
177175
/// <param name="path">Rest resource path.</param>
178176
/// <param name="pathItem">Rest resource metadata.</param>
179177
/// <param name="operationsToExclude">Optional list of operations not to import, e.g. in case they are not supported</param>
180178
/// <param name="logger">Used to perform logging.</param>
181179
/// <returns>Rest operation.</returns>
182-
internal static List<RestApiOperation> CreateRestApiOperations(OpenApiServer? server, string path, OpenApiPathItem pathItem, IList<string>? operationsToExclude, ILogger logger)
180+
internal static List<RestApiOperation> CreateRestApiOperations(IList<OpenApiServer> servers, string path, OpenApiPathItem pathItem, IList<string>? operationsToExclude, ILogger logger)
183181
{
184182
var operations = new List<RestApiOperation>();
185-
var operationServer = CreateRestApiOperationServer(server);
186183

187184
foreach (var operationPair in pathItem.Operations)
188185
{
@@ -196,14 +193,14 @@ internal static List<RestApiOperation> CreateRestApiOperations(OpenApiServer? se
196193
}
197194

198195
var operation = new RestApiOperation(
199-
operationItem.OperationId,
200-
operationServer,
201-
path,
202-
new HttpMethod(method),
203-
string.IsNullOrEmpty(operationItem.Description) ? operationItem.Summary : operationItem.Description,
204-
CreateRestApiOperationParameters(operationItem.OperationId, operationItem.Parameters),
205-
CreateRestApiOperationPayload(operationItem.OperationId, operationItem.RequestBody),
206-
CreateRestApiOperationExpectedResponses(operationItem.Responses).ToDictionary(item => item.Item1, item => item.Item2)
196+
id: operationItem.OperationId,
197+
servers: CreateRestApiOperationServers(servers),
198+
path: path,
199+
method: new HttpMethod(method),
200+
description: string.IsNullOrEmpty(operationItem.Description) ? operationItem.Summary : operationItem.Description,
201+
parameters: CreateRestApiOperationParameters(operationItem.OperationId, operationItem.Parameters),
202+
payload: CreateRestApiOperationPayload(operationItem.OperationId, operationItem.RequestBody),
203+
responses: CreateRestApiOperationExpectedResponses(operationItem.Responses).ToDictionary(item => item.Item1, item => item.Item2)
207204
)
208205
{
209206
Extensions = CreateRestApiOperationExtensions(operationItem.Extensions, logger)
@@ -216,13 +213,20 @@ internal static List<RestApiOperation> CreateRestApiOperations(OpenApiServer? se
216213
}
217214

218215
/// <summary>
219-
/// Build a <see cref="RestApiOperationServer"/> object from the given <see cref="OpenApiServer"/> object.
216+
/// Build a list of <see cref="RestApiOperationServer"/> objects from the given list of <see cref="OpenApiServer"/> objects.
220217
/// </summary>
221-
/// <param name="server">Represents the server which hosts the REST API.</param>
222-
private static RestApiOperationServer CreateRestApiOperationServer(OpenApiServer? server)
218+
/// <param name="servers">Represents servers which hosts the REST API.</param>
219+
private static List<RestApiOperationServer> CreateRestApiOperationServers(IList<OpenApiServer> servers)
223220
{
224-
var variables = server?.Variables.ToDictionary(item => item.Key, item => new RestApiOperationServerVariable(item.Value.Default, item.Value.Description, item.Value.Enum));
225-
return new(server?.Url, variables);
221+
var result = new List<RestApiOperationServer>(servers.Count);
222+
223+
foreach (var server in servers)
224+
{
225+
var variables = server.Variables.ToDictionary(item => item.Key, item => new RestApiOperationServerVariable(item.Value.Default, item.Value.Description, item.Value.Enum));
226+
result.Add(new(server?.Url, variables));
227+
}
228+
229+
return result;
226230
}
227231

228232
/// <summary>

dotnet/src/Functions/Functions.OpenApi/OpenApiKernelPluginFactory.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,8 +267,9 @@ async Task<RestApiOperationResponse> ExecuteAsync(Kernel kernel, KernelFunction
267267
{
268268
{ OpenApiKernelPluginFactory.OperationExtensionsMethodKey, operation.Method.ToString().ToUpperInvariant() },
269269
{ OpenApiKernelPluginFactory.OperationExtensionsOperationKey, operation },
270-
{ OpenApiKernelPluginFactory.OperationExtensionsServerUrlsKey, string.IsNullOrEmpty(operation.Server?.Url) ? Array.Empty<string>() : [ operation.Server!.Url! ] }
270+
{ OpenApiKernelPluginFactory.OperationExtensionsServerUrlsKey, operation.Servers is { Count: > 0 } servers && !string.IsNullOrEmpty(servers[0].Url) ? [servers[0].Url! ] : Array.Empty<string>() }
271271
};
272+
272273
if (operation.Extensions is { Count: > 0 })
273274
{
274275
additionalMetadata.Add(OpenApiKernelPluginFactory.OperationExtensionsMetadataKey, operation.Extensions);

dotnet/src/Functions/Functions.UnitTests/OpenApi/Extensions/RestApiOperationExtensionsTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ private static RestApiOperation CreateTestOperation(string method, RestApiOperat
256256
{
257257
return new RestApiOperation(
258258
id: "fake-id",
259-
server: new(url?.AbsoluteUri),
259+
servers: [new(url?.AbsoluteUri)],
260260
path: "fake-path",
261261
method: new HttpMethod(method),
262262
description: "fake-description",

dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV20Tests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ public async Task ItCanParsePutOperationMetadataSuccessfullyAsync()
113113
var putOperation = restApi.Operations.Single(o => o.Id == "SetSecret");
114114
Assert.NotNull(putOperation);
115115
Assert.Equal("Sets a secret in a specified key vault.", putOperation.Description);
116-
Assert.Equal("https://my-key-vault.vault.azure.net", putOperation.Server.Url);
116+
Assert.Equal("https://my-key-vault.vault.azure.net", putOperation.Servers[0].Url);
117117
Assert.Equal(HttpMethod.Put, putOperation.Method);
118118
Assert.Equal("/secrets/{secret-name}", putOperation.Path);
119119

@@ -276,7 +276,7 @@ public async Task ItCanWorkWithDocumentsWithoutHostAndSchemaAttributesAsync()
276276
var restApi = await this._sut.ParseAsync(stream);
277277

278278
//Assert
279-
Assert.All(restApi.Operations, (op) => Assert.Null(op.Server.Url));
279+
Assert.All(restApi.Operations, (op) => Assert.Null(op.Servers[0].Url));
280280
}
281281

282282
[Fact]

dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV30Tests.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ public async Task ItCanParsePutOperationMetadataSuccessfullyAsync()
114114
var putOperation = restApi.Operations.Single(o => o.Id == "SetSecret");
115115
Assert.NotNull(putOperation);
116116
Assert.Equal("Sets a secret in a specified key vault.", putOperation.Description);
117-
Assert.Equal("https://my-key-vault.vault.azure.net", putOperation.Server.Url);
117+
Assert.Equal("https://my-key-vault.vault.azure.net", putOperation.Servers[0].Url);
118118
Assert.Equal(HttpMethod.Put, putOperation.Method);
119119
Assert.Equal("/secrets/{secret-name}", putOperation.Path);
120120

@@ -299,7 +299,7 @@ public async Task ItCanWorkWithDocumentsWithoutServersAttributeAsync()
299299
var restApi = await this._sut.ParseAsync(stream);
300300

301301
//Assert
302-
Assert.All(restApi.Operations, (op) => Assert.Null(op.Server.Url));
302+
Assert.All(restApi.Operations, (op) => Assert.Empty(op.Servers));
303303
}
304304

305305
[Fact]
@@ -315,7 +315,7 @@ public async Task ItCanWorkWithDocumentsWithEmptyServersAttributeAsync()
315315
var restApi = await this._sut.ParseAsync(stream);
316316

317317
//Assert
318-
Assert.All(restApi.Operations, (op) => Assert.Null(op.Server.Url));
318+
Assert.All(restApi.Operations, (op) => Assert.Empty(op.Servers));
319319
}
320320

321321
[Theory]
@@ -487,6 +487,19 @@ public async Task ItCanParsePropertiesOfObjectDataTypeAsync()
487487
Assert.Null(property.Format);
488488
}
489489

490+
[Fact]
491+
public async Task ItCanParseDocumentWithMultipleServersAsync()
492+
{
493+
// Act
494+
var restApi = await this._sut.ParseAsync(this._openApiDocument);
495+
496+
// Assert
497+
Assert.All(restApi.Operations, (operation) => Assert.Equal(2, operation.Servers.Count));
498+
499+
Assert.Equal("https://my-key-vault.vault.azure.net", restApi.Operations[0].Servers[0].Url);
500+
Assert.Equal("https://ppe.my-key-vault.vault.azure.net", restApi.Operations[0].Servers[1].Url);
501+
}
502+
490503
private static MemoryStream ModifyOpenApiDocument(Stream openApiDocument, Action<JsonObject> transformer)
491504
{
492505
var json = JsonSerializer.Deserialize<JsonObject>(openApiDocument);

dotnet/src/Functions/Functions.UnitTests/OpenApi/OpenApiDocumentParserV31Tests.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ public async Task ItCanParsePutOperationMetadataSuccessfullyAsync()
114114
var putOperation = restApi.Operations.Single(o => o.Id == "SetSecret");
115115
Assert.NotNull(putOperation);
116116
Assert.Equal("Sets a secret in a specified key vault.", putOperation.Description);
117-
Assert.Equal("https://my-key-vault.vault.azure.net", putOperation.Server.Url);
117+
Assert.Equal("https://my-key-vault.vault.azure.net", putOperation.Servers[0].Url);
118118
Assert.Equal(HttpMethod.Put, putOperation.Method);
119119
Assert.Equal("/secrets/{secret-name}", putOperation.Path);
120120

@@ -276,7 +276,7 @@ public async Task ItCanWorkWithDocumentsWithoutServersAttributeAsync()
276276
var restApi = await this._sut.ParseAsync(stream);
277277

278278
//Assert
279-
Assert.All(restApi.Operations, (op) => Assert.Null(op.Server.Url));
279+
Assert.All(restApi.Operations, (op) => Assert.Empty(op.Servers));
280280
}
281281

282282
[Fact]
@@ -292,7 +292,7 @@ public async Task ItCanWorkWithDocumentsWithEmptyServersAttributeAsync()
292292
var restApi = await this._sut.ParseAsync(stream);
293293

294294
//Assert
295-
Assert.All(restApi.Operations, (op) => Assert.Null(op.Server.Url));
295+
Assert.All(restApi.Operations, (op) => Assert.Empty(op.Servers));
296296
}
297297

298298
[Theory]
@@ -464,6 +464,19 @@ public async Task ItCanParsePropertiesOfObjectDataTypeAsync()
464464
Assert.Null(property.Format);
465465
}
466466

467+
[Fact]
468+
public async Task ItCanParseDocumentWithMultipleServersAsync()
469+
{
470+
// Act
471+
var restApi = await this._sut.ParseAsync(this._openApiDocument);
472+
473+
// Assert
474+
Assert.All(restApi.Operations, (operation) => Assert.Equal(2, operation.Servers.Count));
475+
476+
Assert.Equal("https://my-key-vault.vault.azure.net", restApi.Operations[0].Servers[0].Url);
477+
Assert.Equal("https://ppe.my-key-vault.vault.azure.net", restApi.Operations[0].Servers[1].Url);
478+
}
479+
467480
private static MemoryStream ModifyOpenApiDocument(Stream openApiDocument, Action<IDictionary<string, object>> transformer)
468481
{
469482
var serializer = new SharpYaml.Serialization.Serializer();

0 commit comments

Comments
 (0)