diff --git a/src/shared/uriTemplate.test.ts b/src/shared/uriTemplate.test.ts index 8ec4fb73..7582fcee 100644 --- a/src/shared/uriTemplate.test.ts +++ b/src/shared/uriTemplate.test.ts @@ -273,4 +273,52 @@ describe("UriTemplate", () => { expect(() => template.expand(vars)).not.toThrow(); }); }); + + describe("variable classification", () => { + it("should identify path variables", () => { + const template = new UriTemplate( + "/users/{id}/posts/{postId}{?limit,offset}" + ); + expect(template.pathVariableNames).toEqual(["id", "postId"]); + }); + + it("should identify query variables", () => { + const template = new UriTemplate( + "/users/{id}/posts/{postId}{?limit,offset}" + ); + expect(template.queryVariableNames).toEqual(["limit", "offset"]); + }); + + it("should classify different operators correctly", () => { + const template = new UriTemplate( + "{base}{+path}{#fragment}{.ext}{/segments}{?query}{&more}" + ); + expect(template.pathVariableNames).toEqual([ + "base", + "path", + "fragment", + "ext", + "segments", + ]); + expect(template.queryVariableNames).toEqual(["query", "more"]); + }); + + it("should handle templates with only path variables", () => { + const template = new UriTemplate("/users/{id}/profile"); + expect(template.pathVariableNames).toEqual(["id"]); + expect(template.queryVariableNames).toEqual([]); + }); + + it("should handle templates with only query variables", () => { + const template = new UriTemplate("/search{?q,limit,offset}"); + expect(template.pathVariableNames).toEqual([]); + expect(template.queryVariableNames).toEqual(["q", "limit", "offset"]); + }); + + it("should handle templates with no variables", () => { + const template = new UriTemplate("/static/path"); + expect(template.pathVariableNames).toEqual([]); + expect(template.queryVariableNames).toEqual([]); + }); + }); }); diff --git a/src/shared/uriTemplate.ts b/src/shared/uriTemplate.ts index 982589ac..5fbb9324 100644 --- a/src/shared/uriTemplate.ts +++ b/src/shared/uriTemplate.ts @@ -40,6 +40,41 @@ export class UriTemplate { return this.parts.flatMap((part) => typeof part === 'string' ? [] : part.names); } + /** + * Returns variable names used in path-like expansions. + * These include simple expansion, reserved expansion, fragment expansion, + * label expansion, and path segment expansion. + */ + get pathVariableNames(): string[] { + return this.parts + .filter((part) => typeof part !== "string") + .filter((part) => { + // Path-like expansions: simple, reserved, fragment, label, path segments + return ( + part.operator === "" || + part.operator === "+" || + part.operator === "#" || + part.operator === "." || + part.operator === "/" + ); + }) + .flatMap((part) => part.names); + } + + /** + * Returns variable names used in query-like expansions. + * These include form-style query and query continuation expansions. + */ + get queryVariableNames(): string[] { + return this.parts + .filter((part) => typeof part !== "string") + .filter((part) => { + // Query-like expansions: form-style query and continuation + return part.operator === "?" || part.operator === "&"; + }) + .flatMap((part) => part.names); + } + constructor(template: string) { UriTemplate.validateLength(template, MAX_TEMPLATE_LENGTH, "Template"); this.template = template;