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);
}
}
}