diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiServerExtensions.cs b/src/Microsoft.OpenApi/Extensions/OpenApiServerExtensions.cs
new file mode 100644
index 000000000..b885cb235
--- /dev/null
+++ b/src/Microsoft.OpenApi/Extensions/OpenApiServerExtensions.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.OpenApi.Models;
+using Microsoft.OpenApi.Properties;
+
+namespace Microsoft.OpenApi.Extensions;
+
+///
+/// Extension methods for serialization.
+///
+public static class OpenApiServerExtensions
+{
+ ///
+ /// Replaces URL variables in a server's URL
+ ///
+ /// The OpenAPI server object
+ /// The server variable values that will be used to replace the default values.
+ /// A URL with the provided variables substituted.
+ ///
+ /// Thrown when:
+ /// 1. A substitution has no valid value in both the supplied dictionary and the default
+ /// 2. A substitution's value is not available in the enum provided
+ ///
+ public static string ReplaceServerUrlVariables(this OpenApiServer server, IDictionary values = null)
+ {
+ var parsedUrl = server.Url;
+ foreach (var variable in server.Variables)
+ {
+ // Try to get the value from the provided values
+ if (values is not { } v || !v.TryGetValue(variable.Key, out var value) || string.IsNullOrEmpty(value))
+ {
+ // Fall back to the default value
+ value = variable.Value.Default;
+ }
+
+ // Validate value
+ if (string.IsNullOrEmpty(value))
+ {
+ // According to the spec, the variable's default value is required.
+ // This code path should be hit when a value isn't provided & a default value isn't available
+ throw new ArgumentException(
+ string.Format(SRResource.ParseServerUrlDefaultValueNotAvailable, variable.Key), nameof(server));
+ }
+
+ // If an enum is provided, the array should not be empty & the value should exist in the enum
+ if (variable.Value.Enum is {} e && (e.Count == 0 || !e.Contains(value)))
+ {
+ throw new ArgumentException(
+ string.Format(SRResource.ParseServerUrlValueNotValid, value, variable.Key), nameof(values));
+ }
+
+ parsedUrl = parsedUrl.Replace($"{{{variable.Key}}}", value);
+ }
+
+ return parsedUrl;
+ }
+}
diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs
index 712bddb03..201b321f1 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs
@@ -8,6 +8,7 @@
using System.Security.Cryptography;
using System.Text;
using Microsoft.OpenApi.Exceptions;
+using Microsoft.OpenApi.Extensions;
using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Services;
using Microsoft.OpenApi.Writers;
@@ -283,14 +284,7 @@ public void SerializeAsV2(IOpenApiWriter writer)
private static string ParseServerUrl(OpenApiServer server)
{
- var parsedUrl = server.Url;
-
- var variables = server.Variables;
- foreach (var variable in variables.Where(static x => !string.IsNullOrEmpty(x.Value.Default)))
- {
- parsedUrl = parsedUrl.Replace($"{{{variable.Key}}}", variable.Value.Default);
- }
- return parsedUrl;
+ return server.ReplaceServerUrlVariables(new Dictionary(0));
}
private static void WriteHostInfoV2(IOpenApiWriter writer, IList servers)
diff --git a/src/Microsoft.OpenApi/Models/OpenApiServerVariable.cs b/src/Microsoft.OpenApi/Models/OpenApiServerVariable.cs
index fe5293cd4..4ab8bdcaa 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiServerVariable.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiServerVariable.cs
@@ -26,7 +26,10 @@ public class OpenApiServerVariable : IOpenApiSerializable, IOpenApiExtensible
///
/// An enumeration of string values to be used if the substitution options are from a limited set.
///
- public List Enum { get; set; } = new();
+ ///
+ /// If the server variable in the OpenAPI document has no enum
member, this property will be null.
+ ///
+ public List Enum { get; set; }
///
/// This object MAY be extended with Specification Extensions.
diff --git a/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs b/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs
index 1a9ab3014..511f300f7 100644
--- a/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs
+++ b/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs
@@ -1,7 +1,6 @@
//------------------------------------------------------------------------------
//
// This code was generated by a tool.
-// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@@ -222,6 +221,24 @@ internal static string OpenApiWriterExceptionGenericError {
}
}
+ ///
+ /// Looks up a localized string similar to Invalid server variable '{0}'. A value was not provided and no default value was provided..
+ ///
+ internal static string ParseServerUrlDefaultValueNotAvailable {
+ get {
+ return ResourceManager.GetString("ParseServerUrlDefaultValueNotAvailable", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Value '{0}' is not a valid value for variable '{1}'. If an enum is provided, it should not be empty and the value provided should exist in the enum.
+ ///
+ internal static string ParseServerUrlValueNotValid {
+ get {
+ return ResourceManager.GetString("ParseServerUrlValueNotValid", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to The given primitive type '{0}' is not supported..
///
diff --git a/src/Microsoft.OpenApi/Properties/SRResource.resx b/src/Microsoft.OpenApi/Properties/SRResource.resx
index f0bb497d3..0effa1d44 100644
--- a/src/Microsoft.OpenApi/Properties/SRResource.resx
+++ b/src/Microsoft.OpenApi/Properties/SRResource.resx
@@ -225,4 +225,10 @@
OpenAPI document must be added to an OpenApiWorkspace to be able to resolve external references.
+
+ Invalid server variable '{0}'. A value was not provided and no default value was provided.
+
+
+ Value '{0}' is not a valid value for variable '{1}'. If an enum is provided, it should not be empty and the value provided should exist in the enum
+
diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiServerRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiServerRules.cs
index dd11a661d..35d4b9a25 100644
--- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiServerRules.cs
+++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiServerRules.cs
@@ -26,9 +26,32 @@ public static class OpenApiServerRules
context.CreateError(nameof(ServerRequiredFields),
String.Format(SRResource.Validation_FieldIsRequired, "url", "server"));
}
+
+ context.Exit();
+ context.Enter("variables");
+ foreach (var variable in server.Variables)
+ {
+ context.Enter(variable.Key);
+ ValidateServerVariableRequiredFields(context, variable.Key, variable.Value);
+ context.Exit();
+ }
context.Exit();
});
// add more rules
+
+ ///
+ /// Validate required fields in server variable
+ ///
+ private static void ValidateServerVariableRequiredFields(IValidationContext context, string key, OpenApiServerVariable item)
+ {
+ context.Enter("default");
+ if (string.IsNullOrEmpty(item.Default))
+ {
+ context.CreateError("ServerVariableMustHaveDefaultValue",
+ String.Format(SRResource.Validation_FieldIsRequired, "default", key));
+ }
+ context.Exit();
+ }
}
}
diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs
index d67c0054f..bb3db096f 100644
--- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs
+++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs
@@ -1367,5 +1367,70 @@ public void ParseDocumetWithWrongReferenceTypeShouldReturnADiagnosticError()
diagnostic.Errors.Should().BeEquivalentTo(new List {
new( new OpenApiException("Invalid Reference Type 'Schema'.")) });
}
+
+ [Fact]
+ public void ParseBasicDocumentWithServerVariableShouldSucceed()
+ {
+ var openApiDoc = new OpenApiStringReader().Read("""
+ openapi : 3.0.0
+ info:
+ title: The API
+ version: 0.9.1
+ servers:
+ - url: http://www.example.org/api/{version}
+ description: The http endpoint
+ variables:
+ version:
+ default: v2
+ enum: [v1, v2]
+ paths: {}
+ """, out var diagnostic);
+
+ diagnostic.Should().BeEquivalentTo(
+ new OpenApiDiagnostic { SpecificationVersion = OpenApiSpecVersion.OpenApi3_0 });
+
+ openApiDoc.Should().BeEquivalentTo(
+ new OpenApiDocument
+ {
+ Info = new()
+ {
+ Title = "The API",
+ Version = "0.9.1",
+ },
+ Servers =
+ {
+ new OpenApiServer
+ {
+ Url = "http://www.example.org/api/{version}",
+ Description = "The http endpoint",
+ Variables = new Dictionary
+ {
+ {"version", new OpenApiServerVariable {Default = "v2", Enum = ["v1", "v2"]}}
+ }
+ }
+ },
+ Paths = new()
+ });
+ }
+
+ [Fact]
+ public void ParseBasicDocumentWithServerVariableAndNoDefaultShouldFail()
+ {
+ var openApiDoc = new OpenApiStringReader().Read("""
+ openapi : 3.0.0
+ info:
+ title: The API
+ version: 0.9.1
+ servers:
+ - url: http://www.example.org/api/{version}
+ description: The http endpoint
+ variables:
+ version:
+ enum: [v1, v2]
+ paths: {}
+ """, out var diagnostic);
+
+ diagnostic.Errors.Should().NotBeEmpty();
+ }
}
}
diff --git a/test/Microsoft.OpenApi.Tests/Extensions/OpenApiServerExtensionsTests.cs b/test/Microsoft.OpenApi.Tests/Extensions/OpenApiServerExtensionsTests.cs
new file mode 100644
index 000000000..b8f581541
--- /dev/null
+++ b/test/Microsoft.OpenApi.Tests/Extensions/OpenApiServerExtensionsTests.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Collections.Generic;
+using FluentAssertions;
+using Microsoft.OpenApi.Extensions;
+using Microsoft.OpenApi.Models;
+using Xunit;
+
+namespace Microsoft.OpenApi.Tests.Extensions;
+
+public class OpenApiServerExtensionsTests
+{
+ [Fact]
+ public void ShouldSubstituteServerVariableWithProvidedValues()
+ {
+ var variable = new OpenApiServer
+ {
+ Url = "http://example.com/api/{version}",
+ Description = string.Empty,
+ Variables = new Dictionary
+ {
+ { "version", new OpenApiServerVariable { Default = "v1", Enum = ["v1", "v2"]} }
+ }
+ };
+
+ var url = variable.ReplaceServerUrlVariables(new Dictionary {{"version", "v2"}});
+
+ url.Should().Be("http://example.com/api/v2");
+ }
+
+ [Fact]
+ public void ShouldSubstituteServerVariableWithDefaultValues()
+ {
+ var variable = new OpenApiServer
+ {
+ Url = "http://example.com/api/{version}",
+ Description = string.Empty,
+ Variables = new Dictionary
+ {
+ { "version", new OpenApiServerVariable { Default = "v1", Enum = ["v1", "v2"]} }
+ }
+ };
+
+ var url = variable.ReplaceServerUrlVariables(new Dictionary(0));
+
+ url.Should().Be("http://example.com/api/v1");
+ }
+
+ [Fact]
+ public void ShouldFailIfNoValueIsAvailable()
+ {
+ var variable = new OpenApiServer
+ {
+ Url = "http://example.com/api/{version}",
+ Description = string.Empty,
+ Variables = new Dictionary
+ {
+ { "version", new OpenApiServerVariable { Enum = ["v1", "v2"]} }
+ }
+ };
+
+ Assert.Throws(() =>
+ {
+ variable.ReplaceServerUrlVariables(new Dictionary(0));
+ });
+ }
+
+ [Fact]
+ public void ShouldFailIfProvidedValueIsNotInEnum()
+ {
+ var variable = new OpenApiServer
+ {
+ Url = "http://example.com/api/{version}",
+ Description = string.Empty,
+ Variables = new Dictionary
+ {
+ { "version", new OpenApiServerVariable { Enum = ["v1", "v2"]} }
+ }
+ };
+
+ Assert.Throws(() =>
+ {
+ variable.ReplaceServerUrlVariables(new Dictionary {{"version", "v3"}});
+ });
+ }
+
+ [Fact]
+ public void ShouldFailIfEnumIsEmpty()
+ {
+ var variable = new OpenApiServer
+ {
+ Url = "http://example.com/api/{version}",
+ Description = string.Empty,
+ Variables = new Dictionary
+ {
+ { "version", new OpenApiServerVariable { Enum = []} }
+ }
+ };
+
+ Assert.Throws(() =>
+ {
+ variable.ReplaceServerUrlVariables(new Dictionary {{"version", "v1"}});
+ });
+ }
+}
diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
index fbfe564f3..9483e5f6e 100755
--- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
+++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
@@ -285,6 +285,10 @@ namespace Microsoft.OpenApi.Extensions
public static void SerializeAsYaml(this T element, System.IO.Stream stream, Microsoft.OpenApi.OpenApiSpecVersion specVersion)
where T : Microsoft.OpenApi.Interfaces.IOpenApiSerializable { }
}
+ public static class OpenApiServerExtensions
+ {
+ public static string ReplaceServerUrlVariables(this Microsoft.OpenApi.Models.OpenApiServer server, System.Collections.Generic.IDictionary values = null) { }
+ }
public static class OpenApiTypeMapper
{
public static System.Type MapOpenApiPrimitiveTypeToSimpleType(this Microsoft.OpenApi.Models.OpenApiSchema schema) { }