diff --git a/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs b/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs index 2b1fc7542..abf945258 100644 --- a/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs +++ b/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs @@ -339,6 +339,15 @@ internal static string Validation_PathItemMustBeginWithSlash { } } + /// + /// Looks up a localized string similar to The path signature '{0}' MUST begin be unique.. + /// + internal static string Validation_PathSignatureMustBeUnique { + get { + return ResourceManager.GetString("Validation_PathSignatureMustBeUnique", resourceCulture); + } + } + /// /// Looks up a localized string similar to The same rule cannot be in the same rule set twice.. /// diff --git a/src/Microsoft.OpenApi/Properties/SRResource.resx b/src/Microsoft.OpenApi/Properties/SRResource.resx index 3e6822ce4..38c4763d4 100644 --- a/src/Microsoft.OpenApi/Properties/SRResource.resx +++ b/src/Microsoft.OpenApi/Properties/SRResource.resx @@ -210,6 +210,9 @@ The path item name '{0}' MUST begin with a slash. + + The path signature '{0}' MUST be unique. + The same rule cannot be in the same rule set twice. @@ -222,4 +225,4 @@ OpenAPI document must be added to an OpenApiWorkspace to be able to resolve external references. - \ No newline at end of file + diff --git a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs index a0d28d7af..7ce011f4b 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs @@ -229,6 +229,7 @@ internal void Walk(OpenApiPaths paths) _visitor.CurrentKeys.Path = null; } } + } /// @@ -1058,6 +1059,7 @@ internal void Walk(IOpenApiElement element) case OpenApiOAuthFlow e: Walk(e); break; case OpenApiOperation e: Walk(e); break; case OpenApiParameter e: Walk(e); break; + case OpenApiPaths e: Walk(e); break; case OpenApiRequestBody e: Walk(e); break; case OpenApiResponse e: Walk(e); break; case OpenApiSchema e: Walk(e); break; diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiPathsRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiPathsRules.cs index d4e4f5727..c25ca8aff 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiPathsRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiPathsRules.cs @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Properties; @@ -23,7 +26,7 @@ public static class OpenApiPathsRules { context.Enter(pathName); - if (pathName == null || !pathName.StartsWith("/")) + if (pathName == null || !pathName.StartsWith("/", StringComparison.OrdinalIgnoreCase)) { context.CreateError(nameof(PathNameMustBeginWithSlash), string.Format(SRResource.Validation_PathItemMustBeginWithSlash, pathName)); @@ -33,6 +36,30 @@ public static class OpenApiPathsRules } }); + private static readonly Regex regexPath = new Regex("\\{([^/]+)\\}", RegexOptions.Compiled, TimeSpan.FromMilliseconds(100)); + /// + /// A relative path to an individual endpoint. The field name MUST begin with a slash. + /// + public static ValidationRule PathMustBeUnique => + new ValidationRule( + (context, item) => + { + var hashSet = new HashSet(); + + foreach (var path in item.Keys) + { + context.Enter(path); + + var pathSignature = regexPath.Replace(path, "{}"); + + if (!hashSet.Add(pathSignature)) + context.CreateError(nameof(PathMustBeUnique), + string.Format(SRResource.Validation_PathSignatureMustBeUnique, pathSignature)); + + context.Exit(); + } + }); + // add more rules } } diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt index b6828dff7..a5309e46d 100755 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt @@ -1431,6 +1431,7 @@ namespace Microsoft.OpenApi.Validations.Rules [Microsoft.OpenApi.Validations.Rules.OpenApiRule] public static class OpenApiPathsRules { + public static Microsoft.OpenApi.Validations.ValidationRule PathMustBeUnique { get; } public static Microsoft.OpenApi.Validations.ValidationRule PathNameMustBeginWithSlash { get; } } [Microsoft.OpenApi.Validations.Rules.OpenApiRule] diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiPathsValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiPathsValidationTests.cs new file mode 100644 index 000000000..23a0a3e0f --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiPathsValidationTests.cs @@ -0,0 +1,49 @@ +using System.Linq; +using FluentAssertions; +using Microsoft.OpenApi.Extensions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; +using Xunit; + +namespace Microsoft.OpenApi.Validations.Tests +{ + public class OpenApiPathsValidationTests + { + [Fact] + public void ValidatePathsMustBeginWithSlash() + { + // Arrange + var error = string.Format(SRResource.Validation_PathItemMustBeginWithSlash, "pets/{petId}"); + var paths = new OpenApiPaths + { + {"pets/{petId}",new OpenApiPathItem() } + }; + + // Act + var errors = paths.Validate(ValidationRuleSet.GetDefaultRuleSet()); + + // Assert + errors.Should().NotBeEmpty(); + errors.Select(e => e.Message).Should().BeEquivalentTo(error); + } + + [Fact] + public void ValidatePathsAreUnique() + { + // Arrange + var error = string.Format(SRResource.Validation_PathSignatureMustBeUnique, "/pets/{}"); + var paths = new OpenApiPaths + { + {"/pets/{petId}",new OpenApiPathItem() }, + {"/pets/{name}",new OpenApiPathItem() } + }; + + // Act + var errors = paths.Validate(ValidationRuleSet.GetDefaultRuleSet()); + + // Assert + errors.Should().NotBeEmpty(); + errors.Select(e => e.Message).Should().BeEquivalentTo(error); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs b/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs index fcafaab21..708d6ee64 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs @@ -35,7 +35,7 @@ public void DefaultRuleSetPropertyReturnsTheCorrectRules() Assert.NotEmpty(rules); // Update the number if you add new default rule(s). - Assert.Equal(22, rules.Count); + Assert.Equal(23, rules.Count); } } }