From 6322fde90d8bbbdf0ed8c43357e570fbbe4474d8 Mon Sep 17 00:00:00 2001 From: Caleb Kiage <747955+calebkiage@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:39:52 +0300 Subject: [PATCH 1/7] Add server variable substitution logic. --- .../Extensions/OpenApiServerExtensions.cs | 42 +++++++++ .../Models/OpenApiDocument.cs | 10 +-- .../Properties/SRResource.Designer.cs | 10 ++- .../Properties/SRResource.resx | 3 + .../V3Tests/OpenApiDocumentTests.cs | 65 ++++++++++++++ .../OpenApiServerExtensionsTests.cs | 85 +++++++++++++++++++ 6 files changed, 206 insertions(+), 9 deletions(-) create mode 100644 src/Microsoft.OpenApi/Extensions/OpenApiServerExtensions.cs create mode 100644 test/Microsoft.OpenApi.Tests/Extensions/OpenApiServerExtensionsTests.cs diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiServerExtensions.cs b/src/Microsoft.OpenApi/Extensions/OpenApiServerExtensions.cs new file mode 100644 index 000000000..7bad639d7 --- /dev/null +++ b/src/Microsoft.OpenApi/Extensions/OpenApiServerExtensions.cs @@ -0,0 +1,42 @@ +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. + /// + /// If a substitution has no value in the supplied dictionary or the default + /// + public static string ReplaceServerUrlVariables(this OpenApiServer server, IDictionary values) + { + var parsedUrl = server.Url; + foreach (var variable in server.Variables) + { + // Try to get the value from the passed in values + values.TryGetValue(variable.Key, out var actualValue); + // Fall back to the default value + if (string.IsNullOrEmpty(actualValue)) { actualValue = variable.Value.Default; } + if (string.IsNullOrEmpty(actualValue)) + { + // 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)); + } + parsedUrl = parsedUrl.Replace($"{{{variable.Key}}}", actualValue); + } + 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/Properties/SRResource.Designer.cs b/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs index 1a9ab3014..7f7356a98 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,15 @@ 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 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..f84834cef 100644 --- a/src/Microsoft.OpenApi/Properties/SRResource.resx +++ b/src/Microsoft.OpenApi/Properties/SRResource.resx @@ -225,4 +225,7 @@ 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. + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs index d67c0054f..62f04b97b 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(Skip = "This test requires a validation for the Server variable object to be added.")] + 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..c1ba80046 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Extensions/OpenApiServerExtensionsTests.cs @@ -0,0 +1,85 @@ +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", "v2"}}); + }); + } +} From 7bd19b9dafdba07d97b633c468a335914e40cc03 Mon Sep 17 00:00:00 2001 From: Caleb Kiage <747955+calebkiage@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:57:05 +0300 Subject: [PATCH 2/7] Add URL substitution enum value validation --- .../Extensions/OpenApiServerExtensions.cs | 10 +++++++++- .../Properties/SRResource.Designer.cs | 9 +++++++++ src/Microsoft.OpenApi/Properties/SRResource.resx | 3 +++ .../Extensions/OpenApiServerExtensionsTests.cs | 2 +- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiServerExtensions.cs b/src/Microsoft.OpenApi/Extensions/OpenApiServerExtensions.cs index 7bad639d7..74801f4da 100644 --- a/src/Microsoft.OpenApi/Extensions/OpenApiServerExtensions.cs +++ b/src/Microsoft.OpenApi/Extensions/OpenApiServerExtensions.cs @@ -17,7 +17,9 @@ public static class OpenApiServerExtensions /// The server variable values that will be used to replace the default values. /// A URL with the provided variables substituted. /// - /// If a substitution has no value in the supplied dictionary or the default + /// 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) { @@ -35,6 +37,12 @@ public static string ReplaceServerUrlVariables(this OpenApiServer server, IDicti throw new ArgumentException( string.Format(SRResource.ParseServerUrlDefaultValueNotAvailable, variable.Key), nameof(server)); } + + if (variable.Value.Enum?.Contains(actualValue) != true) + { + throw new ArgumentException( + string.Format(SRResource.ParseServerUrlValueNotValid, actualValue, variable.Key), nameof(values)); + } parsedUrl = parsedUrl.Replace($"{{{variable.Key}}}", actualValue); } return parsedUrl; diff --git a/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs b/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs index 7f7356a98..c0a2f8904 100644 --- a/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs +++ b/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs @@ -230,6 +230,15 @@ internal static string ParseServerUrlDefaultValueNotAvailable { } } + /// + /// Looks up a localized string similar to Value '{0}' is not a valid value for variable '{1}'.. + /// + 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 f84834cef..2b9347c35 100644 --- a/src/Microsoft.OpenApi/Properties/SRResource.resx +++ b/src/Microsoft.OpenApi/Properties/SRResource.resx @@ -228,4 +228,7 @@ 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}'. + diff --git a/test/Microsoft.OpenApi.Tests/Extensions/OpenApiServerExtensionsTests.cs b/test/Microsoft.OpenApi.Tests/Extensions/OpenApiServerExtensionsTests.cs index c1ba80046..b93581da6 100644 --- a/test/Microsoft.OpenApi.Tests/Extensions/OpenApiServerExtensionsTests.cs +++ b/test/Microsoft.OpenApi.Tests/Extensions/OpenApiServerExtensionsTests.cs @@ -79,7 +79,7 @@ public void ShouldFailIfProvidedValueIsNotInEnum() Assert.Throws(() => { - variable.ReplaceServerUrlVariables(new Dictionary {{"version", "v2"}}); + variable.ReplaceServerUrlVariables(new Dictionary {{"version", "v3"}}); }); } } From 8f4307994c46069dbbfa8f6ebf284deea64e527c Mon Sep 17 00:00:00 2001 From: Caleb Kiage <747955+calebkiage@users.noreply.github.com> Date: Mon, 19 Aug 2024 19:21:51 +0300 Subject: [PATCH 3/7] Add validation for server variable default value --- .../Validations/Rules/OpenApiServerRules.cs | 22 +++++++++++++++++++ .../V3Tests/OpenApiDocumentTests.cs | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiServerRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiServerRules.cs index dd11a661d..c478ca0a9 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiServerRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiServerRules.cs @@ -26,9 +26,31 @@ 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.Value); + context.Exit(); + } }); // add more rules + + /// + /// Validate required fields in server variable + /// + private static void ValidateServerVariableRequiredFields(IValidationContext context, OpenApiServerVariable item) + { + context.Enter("default"); + if (string.IsNullOrEmpty(item.Default)) + { + context.CreateError("ServerVariableMustHaveDefaultValue", + String.Format(SRResource.Validation_FieldIsRequired, "default", "variables")); + } + context.Exit(); + } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs index 62f04b97b..bb3db096f 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs @@ -1413,7 +1413,7 @@ public void ParseBasicDocumentWithServerVariableShouldSucceed() }); } - [Fact(Skip = "This test requires a validation for the Server variable object to be added.")] + [Fact] public void ParseBasicDocumentWithServerVariableAndNoDefaultShouldFail() { var openApiDoc = new OpenApiStringReader().Read(""" From 4c299f3bd28224e83d73625177bd452641e9fc7b Mon Sep 17 00:00:00 2001 From: Caleb Kiage <747955+calebkiage@users.noreply.github.com> Date: Tue, 20 Aug 2024 12:50:31 +0300 Subject: [PATCH 4/7] Fix some failing tests Update error message for server variable --- .../Extensions/OpenApiServerExtensions.cs | 5 ++++- .../Validations/Rules/OpenApiServerRules.cs | 7 ++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiServerExtensions.cs b/src/Microsoft.OpenApi/Extensions/OpenApiServerExtensions.cs index 74801f4da..174193d04 100644 --- a/src/Microsoft.OpenApi/Extensions/OpenApiServerExtensions.cs +++ b/src/Microsoft.OpenApi/Extensions/OpenApiServerExtensions.cs @@ -30,6 +30,7 @@ public static string ReplaceServerUrlVariables(this OpenApiServer server, IDicti values.TryGetValue(variable.Key, out var actualValue); // Fall back to the default value if (string.IsNullOrEmpty(actualValue)) { actualValue = variable.Value.Default; } + if (string.IsNullOrEmpty(actualValue)) { // According to the spec, the variable's default value is required. @@ -38,13 +39,15 @@ public static string ReplaceServerUrlVariables(this OpenApiServer server, IDicti string.Format(SRResource.ParseServerUrlDefaultValueNotAvailable, variable.Key), nameof(server)); } - if (variable.Value.Enum?.Contains(actualValue) != true) + if (variable.Value.Enum is { Count: > 0 } e && !e.Contains(actualValue)) { throw new ArgumentException( string.Format(SRResource.ParseServerUrlValueNotValid, actualValue, variable.Key), nameof(values)); } + parsedUrl = parsedUrl.Replace($"{{{variable.Key}}}", actualValue); } + return parsedUrl; } } diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiServerRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiServerRules.cs index c478ca0a9..35d4b9a25 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiServerRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiServerRules.cs @@ -32,9 +32,10 @@ public static class OpenApiServerRules foreach (var variable in server.Variables) { context.Enter(variable.Key); - ValidateServerVariableRequiredFields(context, variable.Value); + ValidateServerVariableRequiredFields(context, variable.Key, variable.Value); context.Exit(); } + context.Exit(); }); // add more rules @@ -42,13 +43,13 @@ public static class OpenApiServerRules /// /// Validate required fields in server variable /// - private static void ValidateServerVariableRequiredFields(IValidationContext context, OpenApiServerVariable item) + 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", "variables")); + String.Format(SRResource.Validation_FieldIsRequired, "default", key)); } context.Exit(); } From 8f5e449dbd167fbebaa7c8d03c8511a20e01fa10 Mon Sep 17 00:00:00 2001 From: Caleb Kiage <747955+calebkiage@users.noreply.github.com> Date: Tue, 20 Aug 2024 15:42:02 +0300 Subject: [PATCH 5/7] Simplify code --- .../Extensions/OpenApiServerExtensions.cs | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiServerExtensions.cs b/src/Microsoft.OpenApi/Extensions/OpenApiServerExtensions.cs index 174193d04..b3a69f017 100644 --- a/src/Microsoft.OpenApi/Extensions/OpenApiServerExtensions.cs +++ b/src/Microsoft.OpenApi/Extensions/OpenApiServerExtensions.cs @@ -21,17 +21,19 @@ public static class OpenApiServerExtensions /// 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) + 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 passed in values - values.TryGetValue(variable.Key, out var actualValue); - // Fall back to the default value - if (string.IsNullOrEmpty(actualValue)) { actualValue = variable.Value.Default; } + // 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; + } - if (string.IsNullOrEmpty(actualValue)) + 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 @@ -39,13 +41,13 @@ public static string ReplaceServerUrlVariables(this OpenApiServer server, IDicti string.Format(SRResource.ParseServerUrlDefaultValueNotAvailable, variable.Key), nameof(server)); } - if (variable.Value.Enum is { Count: > 0 } e && !e.Contains(actualValue)) + if (variable.Value.Enum is { Count: > 0 } e && !e.Contains(value)) { throw new ArgumentException( - string.Format(SRResource.ParseServerUrlValueNotValid, actualValue, variable.Key), nameof(values)); + string.Format(SRResource.ParseServerUrlValueNotValid, value, variable.Key), nameof(values)); } - parsedUrl = parsedUrl.Replace($"{{{variable.Key}}}", actualValue); + parsedUrl = parsedUrl.Replace($"{{{variable.Key}}}", value); } return parsedUrl; From a63aa29cff0f8171d9861f8946c301debe5b84db Mon Sep 17 00:00:00 2001 From: Caleb Kiage <747955+calebkiage@users.noreply.github.com> Date: Thu, 22 Aug 2024 13:36:15 +0300 Subject: [PATCH 6/7] add new extension function to public api --- test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt | 4 ++++ 1 file changed, 4 insertions(+) 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) { } From be027b33e2cbd2c560803e4e1dcd584a44fd2d56 Mon Sep 17 00:00:00 2001 From: Caleb Kiage <747955+calebkiage@users.noreply.github.com> Date: Thu, 22 Aug 2024 16:33:37 +0300 Subject: [PATCH 7/7] allow null enums --- .../Extensions/OpenApiServerExtensions.cs | 4 +++- .../Models/OpenApiServerVariable.cs | 5 ++++- .../Properties/SRResource.Designer.cs | 2 +- .../Properties/SRResource.resx | 2 +- .../OpenApiServerExtensionsTests.cs | 19 +++++++++++++++++++ 5 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiServerExtensions.cs b/src/Microsoft.OpenApi/Extensions/OpenApiServerExtensions.cs index b3a69f017..b885cb235 100644 --- a/src/Microsoft.OpenApi/Extensions/OpenApiServerExtensions.cs +++ b/src/Microsoft.OpenApi/Extensions/OpenApiServerExtensions.cs @@ -33,6 +33,7 @@ public static string ReplaceServerUrlVariables(this OpenApiServer server, IDicti value = variable.Value.Default; } + // Validate value if (string.IsNullOrEmpty(value)) { // According to the spec, the variable's default value is required. @@ -41,7 +42,8 @@ public static string ReplaceServerUrlVariables(this OpenApiServer server, IDicti string.Format(SRResource.ParseServerUrlDefaultValueNotAvailable, variable.Key), nameof(server)); } - if (variable.Value.Enum is { Count: > 0 } e && !e.Contains(value)) + // 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)); 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 c0a2f8904..511f300f7 100644 --- a/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs +++ b/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs @@ -231,7 +231,7 @@ internal static string ParseServerUrlDefaultValueNotAvailable { } /// - /// Looks up a localized string similar to Value '{0}' is not a valid value for variable '{1}'.. + /// 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 { diff --git a/src/Microsoft.OpenApi/Properties/SRResource.resx b/src/Microsoft.OpenApi/Properties/SRResource.resx index 2b9347c35..0effa1d44 100644 --- a/src/Microsoft.OpenApi/Properties/SRResource.resx +++ b/src/Microsoft.OpenApi/Properties/SRResource.resx @@ -229,6 +229,6 @@ 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}'. + 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/test/Microsoft.OpenApi.Tests/Extensions/OpenApiServerExtensionsTests.cs b/test/Microsoft.OpenApi.Tests/Extensions/OpenApiServerExtensionsTests.cs index b93581da6..b8f581541 100644 --- a/test/Microsoft.OpenApi.Tests/Extensions/OpenApiServerExtensionsTests.cs +++ b/test/Microsoft.OpenApi.Tests/Extensions/OpenApiServerExtensionsTests.cs @@ -82,4 +82,23 @@ public void ShouldFailIfProvidedValueIsNotInEnum() 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"}}); + }); + } }