diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index c172d84c7..fcffa471b 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -290,6 +290,21 @@ public void SerializeAsV2(IOpenApiWriter writer) writer.WriteEndObject(); } + private static string ParseServerUrl(OpenApiServer server) + { + var parsedUrl = server.Url; + + var variables = server.Variables; + foreach (var variable in variables) + { + if (!string.IsNullOrEmpty(variable.Value.Default)) + { + parsedUrl = parsedUrl.Replace($"{{{variable.Key}}}", variable.Value.Default); + } + } + return parsedUrl; + } + private static void WriteHostInfoV2(IOpenApiWriter writer, IList servers) { if (servers == null || !servers.Any()) @@ -299,11 +314,11 @@ private static void WriteHostInfoV2(IOpenApiWriter writer, IList // Arbitrarily choose the first server given that V2 only allows // one host, port, and base path. - var firstServer = servers.First(); + var serverUrl = ParseServerUrl(servers.First()); // Divide the URL in the Url property into host and basePath required in OpenAPI V2 // The Url property cannotcontain path templating to be valid for V2 serialization. - var firstServerUrl = new Uri(firstServer.Url, UriKind.RelativeOrAbsolute); + var firstServerUrl = new Uri(serverUrl, UriKind.RelativeOrAbsolute); // host if (firstServerUrl.IsAbsoluteUri) @@ -337,7 +352,7 @@ private static void WriteHostInfoV2(IOpenApiWriter writer, IList var schemes = servers.Select( s => { - Uri.TryCreate(s.Url, UriKind.RelativeOrAbsolute, out var url); + Uri.TryCreate(ParseServerUrl(s), UriKind.RelativeOrAbsolute, out var url); return url; }) .Where( diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.SerializeAdvancedDocumentWithServerVariableAsV2JsonWorks_produceTerseOutput=False.verified.txt b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.SerializeAdvancedDocumentWithServerVariableAsV2JsonWorks_produceTerseOutput=False.verified.txt new file mode 100644 index 000000000..1656b2bf7 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.SerializeAdvancedDocumentWithServerVariableAsV2JsonWorks_produceTerseOutput=False.verified.txt @@ -0,0 +1,417 @@ +{ + "swagger": "2.0", + "info": { + "title": "Swagger Petstore (Simple)", + "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", + "termsOfService": "http://helloreverb.com/terms/", + "contact": { + "name": "Swagger API team", + "url": "http://swagger.io", + "email": "foo@example.com" + }, + "license": { + "name": "MIT", + "url": "http://opensource.org/licenses/MIT" + }, + "version": "1.0.0" + }, + "host": "your-resource-name.openai.azure.com", + "basePath": "/openai", + "schemes": [ + "https" + ], + "paths": { + "/pets": { + "get": { + "description": "Returns all pets from the system that the user has access to", + "operationId": "findPets", + "produces": [ + "application/json", + "application/xml", + "text/html" + ], + "parameters": [ + { + "in": "query", + "name": "tags", + "description": "tags to filter by", + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + }, + { + "in": "query", + "name": "limit", + "description": "maximum number of results to return", + "type": "integer", + "format": "int32" + } + ], + "responses": { + "200": { + "description": "pet response", + "schema": { + "type": "array", + "items": { + "required": [ + "id", + "name" + ], + "type": "object", + "properties": { + "id": { + "format": "int64", + "type": "integer" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + } + } + }, + "4XX": { + "description": "unexpected client error", + "schema": { + "required": [ + "code", + "message" + ], + "type": "object", + "properties": { + "code": { + "format": "int32", + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + }, + "5XX": { + "description": "unexpected server error", + "schema": { + "required": [ + "code", + "message" + ], + "type": "object", + "properties": { + "code": { + "format": "int32", + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + } + } + }, + "post": { + "description": "Creates a new pet in the store. Duplicates are allowed", + "operationId": "addPet", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json", + "text/html" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "Pet to add to the store", + "required": true, + "schema": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "format": "int64", + "type": "integer" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + } + } + ], + "responses": { + "200": { + "description": "pet response", + "schema": { + "required": [ + "id", + "name" + ], + "type": "object", + "properties": { + "id": { + "format": "int64", + "type": "integer" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + } + }, + "4XX": { + "description": "unexpected client error", + "schema": { + "required": [ + "code", + "message" + ], + "type": "object", + "properties": { + "code": { + "format": "int32", + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + }, + "5XX": { + "description": "unexpected server error", + "schema": { + "required": [ + "code", + "message" + ], + "type": "object", + "properties": { + "code": { + "format": "int32", + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + } + } + } + }, + "/pets/{id}": { + "get": { + "description": "Returns a user based on a single ID, if the user does not have access to the pet", + "operationId": "findPetById", + "produces": [ + "application/json", + "application/xml", + "text/html" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "description": "ID of pet to fetch", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "200": { + "description": "pet response", + "schema": { + "required": [ + "id", + "name" + ], + "type": "object", + "properties": { + "id": { + "format": "int64", + "type": "integer" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + } + }, + "4XX": { + "description": "unexpected client error", + "schema": { + "required": [ + "code", + "message" + ], + "type": "object", + "properties": { + "code": { + "format": "int32", + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + }, + "5XX": { + "description": "unexpected server error", + "schema": { + "required": [ + "code", + "message" + ], + "type": "object", + "properties": { + "code": { + "format": "int32", + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + } + } + }, + "delete": { + "description": "deletes a single pet based on the ID supplied", + "operationId": "deletePet", + "produces": [ + "text/html" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "description": "ID of pet to delete", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "204": { + "description": "pet deleted" + }, + "4XX": { + "description": "unexpected client error", + "schema": { + "required": [ + "code", + "message" + ], + "type": "object", + "properties": { + "code": { + "format": "int32", + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + }, + "5XX": { + "description": "unexpected server error", + "schema": { + "required": [ + "code", + "message" + ], + "type": "object", + "properties": { + "code": { + "format": "int32", + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + } + } + } + } + }, + "definitions": { + "pet": { + "required": [ + "id", + "name" + ], + "type": "object", + "properties": { + "id": { + "format": "int64", + "type": "integer" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "newPet": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "id": { + "format": "int64", + "type": "integer" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "errorModel": { + "required": [ + "code", + "message" + ], + "type": "object", + "properties": { + "code": { + "format": "int32", + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.SerializeAdvancedDocumentWithServerVariableAsV2JsonWorks_produceTerseOutput=True.verified.txt b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.SerializeAdvancedDocumentWithServerVariableAsV2JsonWorks_produceTerseOutput=True.verified.txt new file mode 100644 index 000000000..3670fba11 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.SerializeAdvancedDocumentWithServerVariableAsV2JsonWorks_produceTerseOutput=True.verified.txt @@ -0,0 +1 @@ +{"swagger":"2.0","info":{"title":"Swagger Petstore (Simple)","description":"A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification","termsOfService":"http://helloreverb.com/terms/","contact":{"name":"Swagger API team","url":"http://swagger.io","email":"foo@example.com"},"license":{"name":"MIT","url":"http://opensource.org/licenses/MIT"},"version":"1.0.0"},"host":"your-resource-name.openai.azure.com","basePath":"/openai","schemes":["https"],"paths":{"/pets":{"get":{"description":"Returns all pets from the system that the user has access to","operationId":"findPets","produces":["application/json","application/xml","text/html"],"parameters":[{"in":"query","name":"tags","description":"tags to filter by","type":"array","items":{"type":"string"},"collectionFormat":"multi"},{"in":"query","name":"limit","description":"maximum number of results to return","type":"integer","format":"int32"}],"responses":{"200":{"description":"pet response","schema":{"type":"array","items":{"required":["id","name"],"type":"object","properties":{"id":{"format":"int64","type":"integer"},"name":{"type":"string"},"tag":{"type":"string"}}}}},"4XX":{"description":"unexpected client error","schema":{"required":["code","message"],"type":"object","properties":{"code":{"format":"int32","type":"integer"},"message":{"type":"string"}}}},"5XX":{"description":"unexpected server error","schema":{"required":["code","message"],"type":"object","properties":{"code":{"format":"int32","type":"integer"},"message":{"type":"string"}}}}}},"post":{"description":"Creates a new pet in the store. Duplicates are allowed","operationId":"addPet","consumes":["application/json"],"produces":["application/json","text/html"],"parameters":[{"in":"body","name":"body","description":"Pet to add to the store","required":true,"schema":{"required":["name"],"type":"object","properties":{"id":{"format":"int64","type":"integer"},"name":{"type":"string"},"tag":{"type":"string"}}}}],"responses":{"200":{"description":"pet response","schema":{"required":["id","name"],"type":"object","properties":{"id":{"format":"int64","type":"integer"},"name":{"type":"string"},"tag":{"type":"string"}}}},"4XX":{"description":"unexpected client error","schema":{"required":["code","message"],"type":"object","properties":{"code":{"format":"int32","type":"integer"},"message":{"type":"string"}}}},"5XX":{"description":"unexpected server error","schema":{"required":["code","message"],"type":"object","properties":{"code":{"format":"int32","type":"integer"},"message":{"type":"string"}}}}}}},"/pets/{id}":{"get":{"description":"Returns a user based on a single ID, if the user does not have access to the pet","operationId":"findPetById","produces":["application/json","application/xml","text/html"],"parameters":[{"in":"path","name":"id","description":"ID of pet to fetch","required":true,"type":"integer","format":"int64"}],"responses":{"200":{"description":"pet response","schema":{"required":["id","name"],"type":"object","properties":{"id":{"format":"int64","type":"integer"},"name":{"type":"string"},"tag":{"type":"string"}}}},"4XX":{"description":"unexpected client error","schema":{"required":["code","message"],"type":"object","properties":{"code":{"format":"int32","type":"integer"},"message":{"type":"string"}}}},"5XX":{"description":"unexpected server error","schema":{"required":["code","message"],"type":"object","properties":{"code":{"format":"int32","type":"integer"},"message":{"type":"string"}}}}}},"delete":{"description":"deletes a single pet based on the ID supplied","operationId":"deletePet","produces":["text/html"],"parameters":[{"in":"path","name":"id","description":"ID of pet to delete","required":true,"type":"integer","format":"int64"}],"responses":{"204":{"description":"pet deleted"},"4XX":{"description":"unexpected client error","schema":{"required":["code","message"],"type":"object","properties":{"code":{"format":"int32","type":"integer"},"message":{"type":"string"}}}},"5XX":{"description":"unexpected server error","schema":{"required":["code","message"],"type":"object","properties":{"code":{"format":"int32","type":"integer"},"message":{"type":"string"}}}}}}}},"definitions":{"pet":{"required":["id","name"],"type":"object","properties":{"id":{"format":"int64","type":"integer"},"name":{"type":"string"},"tag":{"type":"string"}}},"newPet":{"required":["name"],"type":"object","properties":{"id":{"format":"int64","type":"integer"},"name":{"type":"string"},"tag":{"type":"string"}}},"errorModel":{"required":["code","message"],"type":"object","properties":{"code":{"format":"int32","type":"integer"},"message":{"type":"string"}}}}} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs index 6e3200957..924699bdf 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs @@ -981,6 +981,305 @@ public class OpenApiDocumentTests } }; + public OpenApiDocument AdvancedDocumentWithServerVariable = new OpenApiDocument + { + Info = new OpenApiInfo + { + Version = "1.0.0", + Title = "Swagger Petstore (Simple)", + Description = + "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", + TermsOfService = new Uri("http://helloreverb.com/terms/"), + Contact = new OpenApiContact + { + Name = "Swagger API team", + Email = "foo@example.com", + Url = new Uri("http://swagger.io") + }, + License = new OpenApiLicense + { + Name = "MIT", + Url = new Uri("http://opensource.org/licenses/MIT") + } + }, + Servers = new List + { + new OpenApiServer + { + Url = "https://{endpoint}/openai", + Variables = new Dictionary + { + ["endpoint"] = new OpenApiServerVariable + { + Default = "your-resource-name.openai.azure.com" + } + } + } + }, + Paths = new OpenApiPaths + { + ["/pets"] = new OpenApiPathItem + { + Operations = new Dictionary + { + [OperationType.Get] = new OpenApiOperation + { + Description = "Returns all pets from the system that the user has access to", + OperationId = "findPets", + Parameters = new List + { + new OpenApiParameter + { + Name = "tags", + In = ParameterLocation.Query, + Description = "tags to filter by", + Required = false, + Schema = new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Type = "string" + } + } + }, + new OpenApiParameter + { + Name = "limit", + In = ParameterLocation.Query, + Description = "maximum number of results to return", + Required = false, + Schema = new OpenApiSchema + { + Type = "integer", + Format = "int32" + } + } + }, + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse + { + Description = "pet response", + Content = new Dictionary + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "array", + Items = PetSchema + } + }, + ["application/xml"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "array", + Items = PetSchema + } + } + } + }, + ["4XX"] = new OpenApiResponse + { + Description = "unexpected client error", + Content = new Dictionary + { + ["text/html"] = new OpenApiMediaType + { + Schema = ErrorModelSchema + } + } + }, + ["5XX"] = new OpenApiResponse + { + Description = "unexpected server error", + Content = new Dictionary + { + ["text/html"] = new OpenApiMediaType + { + Schema = ErrorModelSchema + } + } + } + } + }, + [OperationType.Post] = new OpenApiOperation + { + Description = "Creates a new pet in the store. Duplicates are allowed", + OperationId = "addPet", + RequestBody = new OpenApiRequestBody + { + Description = "Pet to add to the store", + Required = true, + Content = new Dictionary + { + ["application/json"] = new OpenApiMediaType + { + Schema = NewPetSchema + } + } + }, + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse + { + Description = "pet response", + Content = new Dictionary + { + ["application/json"] = new OpenApiMediaType + { + Schema = PetSchema + }, + } + }, + ["4XX"] = new OpenApiResponse + { + Description = "unexpected client error", + Content = new Dictionary + { + ["text/html"] = new OpenApiMediaType + { + Schema = ErrorModelSchema + } + } + }, + ["5XX"] = new OpenApiResponse + { + Description = "unexpected server error", + Content = new Dictionary + { + ["text/html"] = new OpenApiMediaType + { + Schema = ErrorModelSchema + } + } + } + } + } + } + }, + ["/pets/{id}"] = new OpenApiPathItem + { + Operations = new Dictionary + { + [OperationType.Get] = new OpenApiOperation + { + Description = + "Returns a user based on a single ID, if the user does not have access to the pet", + OperationId = "findPetById", + Parameters = new List + { + new OpenApiParameter + { + Name = "id", + In = ParameterLocation.Path, + Description = "ID of pet to fetch", + Required = true, + Schema = new OpenApiSchema + { + Type = "integer", + Format = "int64" + } + } + }, + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse + { + Description = "pet response", + Content = new Dictionary + { + ["application/json"] = new OpenApiMediaType + { + Schema = PetSchema + }, + ["application/xml"] = new OpenApiMediaType + { + Schema = PetSchema + } + } + }, + ["4XX"] = new OpenApiResponse + { + Description = "unexpected client error", + Content = new Dictionary + { + ["text/html"] = new OpenApiMediaType + { + Schema = ErrorModelSchema + } + } + }, + ["5XX"] = new OpenApiResponse + { + Description = "unexpected server error", + Content = new Dictionary + { + ["text/html"] = new OpenApiMediaType + { + Schema = ErrorModelSchema + } + } + } + } + }, + [OperationType.Delete] = new OpenApiOperation + { + Description = "deletes a single pet based on the ID supplied", + OperationId = "deletePet", + Parameters = new List + { + new OpenApiParameter + { + Name = "id", + In = ParameterLocation.Path, + Description = "ID of pet to delete", + Required = true, + Schema = new OpenApiSchema + { + Type = "integer", + Format = "int64" + } + } + }, + Responses = new OpenApiResponses + { + ["204"] = new OpenApiResponse + { + Description = "pet deleted" + }, + ["4XX"] = new OpenApiResponse + { + Description = "unexpected client error", + Content = new Dictionary + { + ["text/html"] = new OpenApiMediaType + { + Schema = ErrorModelSchema + } + } + }, + ["5XX"] = new OpenApiResponse + { + Description = "unexpected server error", + Content = new Dictionary + { + ["text/html"] = new OpenApiMediaType + { + Schema = ErrorModelSchema + } + } + } + } + } + } + } + }, + Components = AdvancedComponents + }; + private readonly ITestOutputHelper _output; public OpenApiDocumentTests(ITestOutputHelper output) @@ -1022,6 +1321,23 @@ public async Task SerializeAdvancedDocumentWithReferenceAsV3JsonWorks(bool produ await Verifier.Verify(outputStringWriter).UseParameters(produceTerseOutput); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task SerializeAdvancedDocumentWithServerVariableAsV2JsonWorks(bool produceTerseOutput) + { + // Arrange + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter, new OpenApiJsonWriterSettings { Terse = produceTerseOutput }); + + // Act + AdvancedDocumentWithServerVariable.SerializeAsV2(writer); + writer.Flush(); + + // Assert + await Verifier.Verify(outputStringWriter).UseParameters(produceTerseOutput); + } + [Theory] [InlineData(true)] [InlineData(false)]