Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add server variable substitution logic. #1783

Merged
merged 7 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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));
}

calebkiage marked this conversation as resolved.
Show resolved Hide resolved
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 @@

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 Expand Up @@ -441,7 +435,7 @@
return null;
}

// Todo: Verify if we need to check to see if this external reference is actually targeted at this document.

Check warning on line 438 in src/Microsoft.OpenApi/Models/OpenApiDocument.cs

View workflow job for this annotation

GitHub Actions / Build

Complete the task associated to this 'TODO' comment. (https://rules.sonarsource.com/csharp/RSPEC-1135)
if (useExternal)
{
if (this.Workspace == null)
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
Loading