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

Remove constraints from route parameters in path #2231

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
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,32 @@ public static void GetAdditionalMetadata(this ApiDescription apiDescription,

customAttributes = Enumerable.Empty<object>();
}

internal static string RelativePathSansParameterConstraints(this ApiDescription apiDescription)
{
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be done with a Regex replace? I feel that would be a little cleaner but not sure about perf implications.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps! Admittedly, I'm by now means a regex expert but things get pretty gnarly when you try to capture all of the requirements for the matcher in regex form (matching '?', '=', or ':', handling regex constraints, etc).

In generally, we've avoid parsing routes with regex over in ASP.NET Core. We'll usually roll out token enumerator implementation (ref) to handle this.

I do agree that some code comments might make this a little bit easier to read/maintain.

var routeTemplate = apiDescription.RelativePath;

// We want to filter out qualifiers that indicate a constract (":")
// a default value ("=") or an optional parameter ("?")
while (routeTemplate.IndexOfAny(new[] { ':', '=', '?' }) != -1)
{
var startIndex = routeTemplate.IndexOfAny(new[] { ':', '=', '?' }) ;
var tokenStart = startIndex + 1;
// Only find a single instance of a '}' after our start
// character to avoid capturing escaped curly braces
// in a regular expression constraint
findEndBrace:
var endIndex = routeTemplate.IndexOf('}', tokenStart);
if (endIndex < routeTemplate.Length - 1 && routeTemplate[endIndex + 1] == '}')
{
tokenStart = endIndex + 2;
goto findEndBrace;
}

routeTemplate = routeTemplate.Remove(startIndex, endIndex - startIndex);
}

return routeTemplate;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ private OpenApiPaths GeneratePaths(IEnumerable<ApiDescription> apiDescriptions,
{
var apiDescriptionsByPath = apiDescriptions
.OrderBy(_options.SortKeySelector)
.GroupBy(apiDesc => apiDesc.RelativePath);
.GroupBy(apiDesc => apiDesc.RelativePathSansParameterConstraints());

var paths = new OpenApiPaths();
foreach (var group in apiDescriptionsByPath)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,22 @@ public void GetSwagger_GeneratesSwaggerDocument_ForApiDescriptionsWithMatchingGr
Assert.Equal(new[] { OperationType.Post, OperationType.Get }, document.Paths["/resource"].Operations.Keys);
}

[Fact]
public void GetSwagger_GeneratesSwaggerDocument_ForApiDescriptionsWithVariousRelativePaths()
[Theory]
[InlineData("resources/{id}", "/resources/{id}")]
[InlineData("{category}/{product?}/{sku}", "/{category}/{product}/{sku}")]
[InlineData("{area=Home}/{controller:required}/{id=0:int}", "/{area}/{controller}/{id}")]
[InlineData("{category}/product/{group?}", "/{category}/product/{group}")]
[InlineData("{category:int}/product/{group:range(10, 20)?}", "/{category}/product/{group}")]
[InlineData("{person:int}/{ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}", "/{person}/{ssn}")]
[InlineData("{person:int}/{ssn:regex(^(?=.*kind)(?=.*good).*$)}", "/{person}/{ssn}")]
public void GetSwagger_GeneratesSwaggerDocument_ForApiDescriptionsWithConstrainedRelativePaths(string path, string expectedPath)
{
var subject = Subject(
apiDescriptions: new[]
{
ApiDescriptionFactory.Create<FakeController>(
c => nameof(c.ActionWithNoParameters), groupName: "v1", httpMethod: "POST", relativePath: "resource/{id}"),
c => nameof(c.ActionWithNoParameters), groupName: "v1", httpMethod: "POST", relativePath: path),

ApiDescriptionFactory.Create<FakeController>(
c => nameof(c.ActionWithNoParameters), groupName: "v1", httpMethod: "GET", relativePath: "resource/{id?}"),

ApiDescriptionFactory.Create<FakeController>(
c => nameof(c.ActionWithNoParameters), groupName: "v1", httpMethod: "POST", relativePath: "resource/foo"),
},
options: new SwaggerGeneratorOptions
{
Expand All @@ -76,7 +78,7 @@ public void GetSwagger_GeneratesSwaggerDocument_ForApiDescriptionsWithVariousRel

Assert.Equal("V1", document.Info.Version);
Assert.Equal("Test API", document.Info.Title);
Assert.Equal(new[] { "/resource/{id}", "/resource/{id?}", "/resource/foo" }, document.Paths.Keys.ToArray());
Assert.Equal(new[] { expectedPath }, document.Paths.Keys.ToArray());
}

[Fact]
Expand Down