Skip to content
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
62 changes: 54 additions & 8 deletions src/OpenApiValidate/Helpers/OpenApiExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ out IOpenApiPathItem path
{
var requestPathString = new PathString(requestPath);

TemplateMatchScore bestMatchScore = TemplateMatchScore.Min;
IOpenApiPathItem? matchingTemplatePathItem = null;

foreach (var kvp in paths)
Expand All @@ -80,7 +81,12 @@ out IOpenApiPathItem path

if (isTemplatePath)
{
matchingTemplatePathItem = kvp.Value;
var matchScore = GetTemplateMatchScore(specPath);
if (matchScore.BetterThan(bestMatchScore))
{
bestMatchScore = matchScore;
matchingTemplatePathItem = kvp.Value;
}
continue;
}

Expand All @@ -98,6 +104,29 @@ out IOpenApiPathItem path
return false;
}

private static TemplateMatchScore GetTemplateMatchScore(PathString specPath)
{
var literalSegmentCount = 0;
var literalPrefixCount = 0;

for (var i = 0; i < specPath.Segments.Length; i++)
{
if (IsTemplateSegment(specPath.Segments[i]))
{
continue;
}

literalSegmentCount++;

if (literalPrefixCount == i)
{
literalPrefixCount++;
}
}

return new TemplateMatchScore(literalSegmentCount, literalPrefixCount);
}

private static bool IsPathMatch(
PathString specPath,
PathString requestPath,
Expand All @@ -115,19 +144,14 @@ out bool isTemplatePath
{
var segment = specPath.Segments[i];

if (segment.StartsWith('{') && segment.EndsWith('}'))
if (IsTemplateSegment(segment))
Comment thread
AButler marked this conversation as resolved.
{
// Is template parameter, so skip checking
isTemplatePath = true;
continue;
}

if (
!segment.Equals(
requestPath.Segments[i],
StringComparison.InvariantCultureIgnoreCase
)
)
if (!segment.Equals(requestPath.Segments[i], StringComparison.OrdinalIgnoreCase))
{
isTemplatePath = false;
return false;
Expand All @@ -136,4 +160,26 @@ out bool isTemplatePath

return true;
}

private static bool IsTemplateSegment(string segment)
{
return segment.StartsWith('{') && segment.EndsWith('}');
}

private class TemplateMatchScore(int literalSegmentCount, int literalPrefixCount)
{
public static readonly TemplateMatchScore Min = new(int.MinValue, int.MinValue);

public int LiteralSegmentCount { get; } = literalSegmentCount;
public int LiteralPrefixCount { get; } = literalPrefixCount;

public bool BetterThan(TemplateMatchScore other)
{
return LiteralSegmentCount > other.LiteralSegmentCount
|| (
LiteralSegmentCount == other.LiteralSegmentCount
&& LiteralPrefixCount > other.LiteralPrefixCount
);
}
}
}
42 changes: 42 additions & 0 deletions test/OpenApiValidate.Tests/ResponseValidatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,48 @@ public async Task LiteralAndTemplatedPath_DeleteMeUser()
validateAction.ShouldNotThrow();
}

[Fact]
public async Task LiteralAndTemplatedPath_DeleteMeUserRole()
{
var openApiDocument = await GetDocument("TestData/LiteralAndTemplatedPath.yaml");

var validator = new OpenApiValidator(openApiDocument);

var request = new Request(
"DELETE",
new Uri("http://api.example.com/v1/user/me/role/admin")
);
var response = new Response(204);

var validateAction = () =>
{
validator.Validate(request, response);
};

validateAction.ShouldNotThrow();
}

[Fact]
public async Task LiteralAndTemplatedPath_DeleteMeUserRole_ReversedDefinitionOrder()
{
var openApiDocument = await GetDocument("TestData/LiteralAndTemplatedPathReversed.yaml");

var validator = new OpenApiValidator(openApiDocument);

var request = new Request(
"DELETE",
new Uri("http://api.example.com/v1/user/me/role/admin")
);
var response = new Response(204);

var validateAction = () =>
{
validator.Validate(request, response);
};

validateAction.ShouldNotThrow();
}

private static async Task<OpenApiDocument> GetDocument(string filename)
{
var settings = new OpenApiReaderSettings();
Expand Down
35 changes: 34 additions & 1 deletion test/OpenApiValidate.Tests/TestData/LiteralAndTemplatedPath.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,37 @@ paths:
summary: Deletes my user.
responses:
"204":
description: User deleted successfully
description: User deleted successfully

/user/me/role/{roleId}:
get:
summary: Returns a single role
responses:
"200":
description: A role object
content:
application/json:
schema:
type: object
properties:
id:
type: string
delete:
summary: Deletes a role
responses:
"204":
description: Role deleted successfully

/user/{userId}/role/{roleId}:
get:
summary: Returns a single role
responses:
"200":
description: A role object
content:
application/json:
schema:
type: object
properties:
id:
type: string
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
openapi: 3.0.0

info:
title: Sample API
description: Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML.
version: 0.1.9

servers:
- url: http://api.example.com/v1
description: Optional server description, e.g. Main (production) server
- url: http://staging-api.example.com
description: Optional server description, e.g. Internal staging server for testing

paths:
/user/{UserId}:
get:
summary: Returns a single user
parameters:
- name: UserId
in: path
required: true
schema:
type: string
responses:
"200":
description: A user object
content:
application/json:
schema:
type: object
properties:
id:
type: string

/user/me:
get:
summary: Returns my user.
responses:
"200":
description: A user object
content:
application/json:
schema:
type: object
properties:
id:
type: string
delete:
summary: Deletes my user.
responses:
"204":
description: User deleted successfully

/user/{userId}/role/{roleId}:
get:
summary: Returns a single role
responses:
"200":
description: A role object
content:
application/json:
schema:
type: object
properties:
id:
type: string

/user/me/role/{roleId}:
get:
summary: Returns a single role
responses:
"200":
description: A role object
content:
application/json:
schema:
type: object
properties:
id:
type: string
delete:
summary: Deletes a role
responses:
"204":
description: Role deleted successfully