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
33 changes: 29 additions & 4 deletions src/OpenApiValidate/Helpers/OpenApiExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,22 +67,45 @@ out IOpenApiPathItem path
{
var requestPathString = new PathString(requestPath);

IOpenApiPathItem? matchingTemplatePathItem = null;

foreach (var kvp in paths)
{
var specPath = new PathString(kvp.Key);
if (IsPathMatch(specPath, requestPathString))

if (!IsPathMatch(specPath, requestPathString, out var isTemplatePath))
{
continue;
}

if (isTemplatePath)
{
path = kvp.Value;
return true;
matchingTemplatePathItem = kvp.Value;
continue;
}

path = kvp.Value;
return true;
}

if (matchingTemplatePathItem is not null)
{
path = matchingTemplatePathItem;
return true;
}

path = null!;
return false;
}

private static bool IsPathMatch(PathString specPath, PathString requestPath)
private static bool IsPathMatch(
PathString specPath,
PathString requestPath,
out bool isTemplatePath
)
{
isTemplatePath = false;

if (specPath.Segments.Length != requestPath.Segments.Length)
{
return false;
Expand All @@ -95,6 +118,7 @@ private static bool IsPathMatch(PathString specPath, PathString requestPath)
if (segment.StartsWith('{') && segment.EndsWith('}'))
{
// Is template parameter, so skip checking
isTemplatePath = true;
continue;
}

Expand All @@ -105,6 +129,7 @@ private static bool IsPathMatch(PathString specPath, PathString requestPath)
)
)
{
isTemplatePath = false;
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

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

Setting isTemplatePath to false is unnecessary here since the function returns false immediately after. This assignment has no effect and adds confusion about the variable's purpose.

Suggested change
isTemplatePath = false;

Copilot uses AI. Check for mistakes.
return false;
}
}
Expand Down
54 changes: 54 additions & 0 deletions test/OpenApiValidate.Tests/ResponseValidatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,60 @@ public async Task Petstore_DeletePet()
validateAction.ShouldNotThrow();
}

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

var validator = new OpenApiValidator(openApiDocument);

var request = new Request("GET", new Uri("http://api.example.com/v1/user/abcdef"));
var response = new Response(200, "application/json", """{"id":"abcdef"}""");

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

validateAction.ShouldNotThrow();
}

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

var validator = new OpenApiValidator(openApiDocument);

var request = new Request("GET", new Uri("http://api.example.com/v1/user/me"));
var response = new Response(200, "application/json", """{"id":"abcdef"}""");

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

validateAction.ShouldNotThrow();
}

[Fact]
public async Task LiteralAndTemplatedPath_DeleteMeUser()
{
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"));
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
52 changes: 52 additions & 0 deletions test/OpenApiValidate.Tests/TestData/LiteralAndTemplatedPath.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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