From 1380b879845ceff7c7caa18012b4402192035188 Mon Sep 17 00:00:00 2001 From: Andrew Butler <1628649+AButler@users.noreply.github.com> Date: Wed, 28 Jan 2026 16:19:00 +0000 Subject: [PATCH 1/2] Add support for literal and templated paths (literal takes precendence) --- .../Helpers/OpenApiExtensions.cs | 33 ++++++++++-- .../ResponseValidatorTests.cs | 54 +++++++++++++++++++ .../TestData/LiteralAndTemplatedPath.yaml | 52 ++++++++++++++++++ 3 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 test/OpenApiValidate.Tests/TestData/LiteralAndTemplatedPath.yaml diff --git a/src/OpenApiValidate/Helpers/OpenApiExtensions.cs b/src/OpenApiValidate/Helpers/OpenApiExtensions.cs index 68058d3..f7a436c 100644 --- a/src/OpenApiValidate/Helpers/OpenApiExtensions.cs +++ b/src/OpenApiValidate/Helpers/OpenApiExtensions.cs @@ -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; @@ -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; } @@ -105,6 +129,7 @@ private static bool IsPathMatch(PathString specPath, PathString requestPath) ) ) { + isTemplatePath = false; return false; } } diff --git a/test/OpenApiValidate.Tests/ResponseValidatorTests.cs b/test/OpenApiValidate.Tests/ResponseValidatorTests.cs index 52bb911..8cc1ff0 100644 --- a/test/OpenApiValidate.Tests/ResponseValidatorTests.cs +++ b/test/OpenApiValidate.Tests/ResponseValidatorTests.cs @@ -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 GetDocument(string filename) { var settings = new OpenApiReaderSettings(); diff --git a/test/OpenApiValidate.Tests/TestData/LiteralAndTemplatedPath.yaml b/test/OpenApiValidate.Tests/TestData/LiteralAndTemplatedPath.yaml new file mode 100644 index 0000000..b09c100 --- /dev/null +++ b/test/OpenApiValidate.Tests/TestData/LiteralAndTemplatedPath.yaml @@ -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 \ No newline at end of file From c6abe9567094cd22d0761fc5000c2a384b3a8d39 Mon Sep 17 00:00:00 2001 From: Andrew Butler <1628649+AButler@users.noreply.github.com> Date: Wed, 28 Jan 2026 16:26:31 +0000 Subject: [PATCH 2/2] Corrected spacing Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../TestData/LiteralAndTemplatedPath.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/OpenApiValidate.Tests/TestData/LiteralAndTemplatedPath.yaml b/test/OpenApiValidate.Tests/TestData/LiteralAndTemplatedPath.yaml index b09c100..114b025 100644 --- a/test/OpenApiValidate.Tests/TestData/LiteralAndTemplatedPath.yaml +++ b/test/OpenApiValidate.Tests/TestData/LiteralAndTemplatedPath.yaml @@ -46,7 +46,7 @@ paths: id: type: string delete: - summary: Deletes my user. - responses: - "204": - description: User deleted successfully \ No newline at end of file + summary: Deletes my user. + responses: + "204": + description: User deleted successfully \ No newline at end of file