Skip to content

Commit

Permalink
Add server variable substitution logic. (#1783)
Browse files Browse the repository at this point in the history
* Add server variable substitution logic.
  • Loading branch information
calebkiage authored Aug 22, 2024
1 parent aa62dc0 commit 42dda81
Show file tree
Hide file tree
Showing 9 changed files with 283 additions and 10 deletions.
57 changes: 57 additions & 0 deletions src/Microsoft.OpenApi/Extensions/OpenApiServerExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Properties;

namespace Microsoft.OpenApi.Extensions;

/// <summary>
/// Extension methods for <see cref="OpenApiServer"/> serialization.
/// </summary>
public static class OpenApiServerExtensions
{
/// <summary>
/// Replaces URL variables in a server's URL
/// </summary>
/// <param name="server">The OpenAPI server object</param>
/// <param name="values">The server variable values that will be used to replace the default values.</param>
/// <returns>A URL with the provided variables substituted.</returns>
/// <exception cref="ArgumentException">
/// 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
/// </exception>
public static string ReplaceServerUrlVariables(this OpenApiServer server, IDictionary<string, string> 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;
}
}
10 changes: 2 additions & 8 deletions src/Microsoft.OpenApi/Models/OpenApiDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<string, string>(0));
}

private static void WriteHostInfoV2(IOpenApiWriter writer, IList<OpenApiServer> servers)
Expand Down
5 changes: 4 additions & 1 deletion src/Microsoft.OpenApi/Models/OpenApiServerVariable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ public class OpenApiServerVariable : IOpenApiSerializable, IOpenApiExtensible
/// <summary>
/// An enumeration of string values to be used if the substitution options are from a limited set.
/// </summary>
public List<string> Enum { get; set; } = new();
/// <remarks>
/// If the server variable in the OpenAPI document has no <code>enum</code> member, this property will be null.
/// </remarks>
public List<string> Enum { get; set; }

/// <summary>
/// This object MAY be extended with Specification Extensions.
Expand Down
19 changes: 18 additions & 1 deletion src/Microsoft.OpenApi/Properties/SRResource.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/Microsoft.OpenApi/Properties/SRResource.resx
Original file line number Diff line number Diff line change
Expand Up @@ -225,4 +225,10 @@
<data name="WorkspaceRequredForExternalReferenceResolution" xml:space="preserve">
<value>OpenAPI document must be added to an OpenApiWorkspace to be able to resolve external references.</value>
</data>
<data name="ParseServerUrlDefaultValueNotAvailable" xml:space="preserve">
<value>Invalid server variable '{0}'. A value was not provided and no default value was provided.</value>
</data>
<data name="ParseServerUrlValueNotValid" xml:space="preserve">
<value>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</value>
</data>
</root>
23 changes: 23 additions & 0 deletions src/Microsoft.OpenApi/Validations/Rules/OpenApiServerRules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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

/// <summary>
/// Validate required fields in server variable
/// </summary>
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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1367,5 +1367,70 @@ public void ParseDocumetWithWrongReferenceTypeShouldReturnADiagnosticError()
diagnostic.Errors.Should().BeEquivalentTo(new List<OpenApiError> {
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<string, OpenApiServerVariable>
{
{"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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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<string, OpenApiServerVariable>
{
{ "version", new OpenApiServerVariable { Default = "v1", Enum = ["v1", "v2"]} }
}
};

var url = variable.ReplaceServerUrlVariables(new Dictionary<string, string> {{"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<string, OpenApiServerVariable>
{
{ "version", new OpenApiServerVariable { Default = "v1", Enum = ["v1", "v2"]} }
}
};

var url = variable.ReplaceServerUrlVariables(new Dictionary<string, string>(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<string, OpenApiServerVariable>
{
{ "version", new OpenApiServerVariable { Enum = ["v1", "v2"]} }
}
};

Assert.Throws<ArgumentException>(() =>
{
variable.ReplaceServerUrlVariables(new Dictionary<string, string>(0));
});
}

[Fact]
public void ShouldFailIfProvidedValueIsNotInEnum()
{
var variable = new OpenApiServer
{
Url = "http://example.com/api/{version}",
Description = string.Empty,
Variables = new Dictionary<string, OpenApiServerVariable>
{
{ "version", new OpenApiServerVariable { Enum = ["v1", "v2"]} }
}
};

Assert.Throws<ArgumentException>(() =>
{
variable.ReplaceServerUrlVariables(new Dictionary<string, string> {{"version", "v3"}});
});
}

[Fact]
public void ShouldFailIfEnumIsEmpty()
{
var variable = new OpenApiServer
{
Url = "http://example.com/api/{version}",
Description = string.Empty,
Variables = new Dictionary<string, OpenApiServerVariable>
{
{ "version", new OpenApiServerVariable { Enum = []} }
}
};

Assert.Throws<ArgumentException>(() =>
{
variable.ReplaceServerUrlVariables(new Dictionary<string, string> {{"version", "v1"}});
});
}
}
4 changes: 4 additions & 0 deletions test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,10 @@ namespace Microsoft.OpenApi.Extensions
public static void SerializeAsYaml<T>(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<string, string> values = null) { }
}
public static class OpenApiTypeMapper
{
public static System.Type MapOpenApiPrimitiveTypeToSimpleType(this Microsoft.OpenApi.Models.OpenApiSchema schema) { }
Expand Down

0 comments on commit 42dda81

Please sign in to comment.