Skip to content

Commit 7568e30

Browse files
authored
Merge pull request #474 from fabich/feature/add-path-signature-unique-validation
[472] Add path signature unique validator
2 parents 8c8c22f + 5140baf commit 7568e30

File tree

7 files changed

+94
-3
lines changed

7 files changed

+94
-3
lines changed

src/Microsoft.OpenApi/Properties/SRResource.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Microsoft.OpenApi/Properties/SRResource.resx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,9 @@
210210
<data name="Validation_PathItemMustBeginWithSlash" xml:space="preserve">
211211
<value>The path item name '{0}' MUST begin with a slash.</value>
212212
</data>
213+
<data name="Validation_PathSignatureMustBeUnique" xml:space="preserve">
214+
<value>The path signature '{0}' MUST be unique.</value>
215+
</data>
213216
<data name="Validation_RuleAddTwice" xml:space="preserve">
214217
<value>The same rule cannot be in the same rule set twice.</value>
215218
</data>
@@ -222,4 +225,4 @@
222225
<data name="WorkspaceRequredForExternalReferenceResolution" xml:space="preserve">
223226
<value>OpenAPI document must be added to an OpenApiWorkspace to be able to resolve external references.</value>
224227
</data>
225-
</root>
228+
</root>

src/Microsoft.OpenApi/Services/OpenApiWalker.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ internal void Walk(OpenApiPaths paths)
229229
_visitor.CurrentKeys.Path = null;
230230
}
231231
}
232+
232233
}
233234

234235
/// <summary>
@@ -1058,6 +1059,7 @@ internal void Walk(IOpenApiElement element)
10581059
case OpenApiOAuthFlow e: Walk(e); break;
10591060
case OpenApiOperation e: Walk(e); break;
10601061
case OpenApiParameter e: Walk(e); break;
1062+
case OpenApiPaths e: Walk(e); break;
10611063
case OpenApiRequestBody e: Walk(e); break;
10621064
case OpenApiResponse e: Walk(e); break;
10631065
case OpenApiSchema e: Walk(e); break;

src/Microsoft.OpenApi/Validations/Rules/OpenApiPathsRules.cs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33

4+
using System;
5+
using System.Collections.Generic;
6+
using System.Text.RegularExpressions;
47
using Microsoft.OpenApi.Models;
58
using Microsoft.OpenApi.Properties;
69

@@ -23,7 +26,7 @@ public static class OpenApiPathsRules
2326
{
2427
context.Enter(pathName);
2528

26-
if (pathName == null || !pathName.StartsWith("/"))
29+
if (pathName == null || !pathName.StartsWith("/", StringComparison.OrdinalIgnoreCase))
2730
{
2831
context.CreateError(nameof(PathNameMustBeginWithSlash),
2932
string.Format(SRResource.Validation_PathItemMustBeginWithSlash, pathName));
@@ -33,6 +36,30 @@ public static class OpenApiPathsRules
3336
}
3437
});
3538

39+
private static readonly Regex regexPath = new Regex("\\{([^/]+)\\}", RegexOptions.Compiled, TimeSpan.FromMilliseconds(100));
40+
/// <summary>
41+
/// A relative path to an individual endpoint. The field name MUST begin with a slash.
42+
/// </summary>
43+
public static ValidationRule<OpenApiPaths> PathMustBeUnique =>
44+
new ValidationRule<OpenApiPaths>(
45+
(context, item) =>
46+
{
47+
var hashSet = new HashSet<string>();
48+
49+
foreach (var path in item.Keys)
50+
{
51+
context.Enter(path);
52+
53+
var pathSignature = regexPath.Replace(path, "{}");
54+
55+
if (!hashSet.Add(pathSignature))
56+
context.CreateError(nameof(PathMustBeUnique),
57+
string.Format(SRResource.Validation_PathSignatureMustBeUnique, pathSignature));
58+
59+
context.Exit();
60+
}
61+
});
62+
3663
// add more rules
3764
}
3865
}

test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1431,6 +1431,7 @@ namespace Microsoft.OpenApi.Validations.Rules
14311431
[Microsoft.OpenApi.Validations.Rules.OpenApiRule]
14321432
public static class OpenApiPathsRules
14331433
{
1434+
public static Microsoft.OpenApi.Validations.ValidationRule<Microsoft.OpenApi.Models.OpenApiPaths> PathMustBeUnique { get; }
14341435
public static Microsoft.OpenApi.Validations.ValidationRule<Microsoft.OpenApi.Models.OpenApiPaths> PathNameMustBeginWithSlash { get; }
14351436
}
14361437
[Microsoft.OpenApi.Validations.Rules.OpenApiRule]
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using System.Linq;
2+
using FluentAssertions;
3+
using Microsoft.OpenApi.Extensions;
4+
using Microsoft.OpenApi.Models;
5+
using Microsoft.OpenApi.Properties;
6+
using Xunit;
7+
8+
namespace Microsoft.OpenApi.Validations.Tests
9+
{
10+
public class OpenApiPathsValidationTests
11+
{
12+
[Fact]
13+
public void ValidatePathsMustBeginWithSlash()
14+
{
15+
// Arrange
16+
var error = string.Format(SRResource.Validation_PathItemMustBeginWithSlash, "pets/{petId}");
17+
var paths = new OpenApiPaths
18+
{
19+
{"pets/{petId}",new OpenApiPathItem() }
20+
};
21+
22+
// Act
23+
var errors = paths.Validate(ValidationRuleSet.GetDefaultRuleSet());
24+
25+
// Assert
26+
errors.Should().NotBeEmpty();
27+
errors.Select(e => e.Message).Should().BeEquivalentTo(error);
28+
}
29+
30+
[Fact]
31+
public void ValidatePathsAreUnique()
32+
{
33+
// Arrange
34+
var error = string.Format(SRResource.Validation_PathSignatureMustBeUnique, "/pets/{}");
35+
var paths = new OpenApiPaths
36+
{
37+
{"/pets/{petId}",new OpenApiPathItem() },
38+
{"/pets/{name}",new OpenApiPathItem() }
39+
};
40+
41+
// Act
42+
var errors = paths.Validate(ValidationRuleSet.GetDefaultRuleSet());
43+
44+
// Assert
45+
errors.Should().NotBeEmpty();
46+
errors.Select(e => e.Message).Should().BeEquivalentTo(error);
47+
}
48+
}
49+
}

test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public void DefaultRuleSetPropertyReturnsTheCorrectRules()
3535
Assert.NotEmpty(rules);
3636

3737
// Update the number if you add new default rule(s).
38-
Assert.Equal(22, rules.Count);
38+
Assert.Equal(23, rules.Count);
3939
}
4040
}
4141
}

0 commit comments

Comments
 (0)