Skip to content

Commit fccddde

Browse files
authored
feat(matching): add isIdentical predicate (#157)
The isIdentical predicate determines if two paths are identical and thus invalid. Refs #98
1 parent 8f3dfbb commit fccddde

File tree

6 files changed

+108
-4
lines changed

6 files changed

+108
-4
lines changed

README.md

+24-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
Each template expression in the path MUST correspond to a path parameter that is included in the [Path Item](https://spec.openapis.org/oas/v3.1.1.html#path-item-object) itself and/or in each of the Path Item's [Operations](https://spec.openapis.org/oas/v3.1.1.html#operation-object).
1212
An exception is if the path item is empty, for example due to ACL constraints, matching path parameters are not required.
1313

14-
`openapi-path-templating` is a **parser**, **validator**, and **resolver** for OpenAPI Path Templating,
14+
`openapi-path-templating` is a **parser**, **validator**, **resolver** and **matcher** for OpenAPI Path Templating,
1515
which played a [foundational role](https://github.com/OAI/OpenAPI-Specification/pull/4244) in defining the official ANBF grammar for OpenAPI Path Templating.
1616

1717
It supports Path Templating defined in following OpenAPI specification versions:
@@ -47,6 +47,7 @@ It supports Path Templating defined in following OpenAPI specification versions:
4747
- [Parsing](#parsing)
4848
- [Validation](#validation)
4949
- [Resolution](#resolution)
50+
- [Matching](#matching)
5051
- [Grammar](#grammar)
5152
- [More about OpenAPI Path Templating](#more-about-openapi-path-templating)
5253
- [License](#license)
@@ -64,7 +65,7 @@ You can install `openapi-path-templating` using `npm`:
6465

6566
### Usage
6667

67-
`openapi-path-templating` currently supports **parsing**, **validation** and **resolution**.
68+
`openapi-path-templating` currently supports **parsing**, **validation**, **resolution** and **matching**.
6869
Both parser and validator are based on a superset of [ABNF](https://www.rfc-editor.org/rfc/rfc5234) ([SABNF](https://github.com/ldthomas/apg-js2/blob/master/SABNF.md))
6970
and use [apg-lite](https://github.com/ldthomas/apg-lite) parser generator.
7071

@@ -213,6 +214,27 @@ resolve('/pets/{petId}', { petId: '/?#' }, {
213214
}); // => "/pets//?#"
214215
```
215216

217+
#### Matching
218+
219+
Path templating matching in OpenAPI prioritizes concrete paths over parameterized ones,
220+
treats paths with identical structures but different parameter names as invalid,
221+
and considers paths with overlapping patterns that could match the same request as potentially ambiguous.
222+
223+
##### Predicates
224+
225+
**isIdentical**
226+
227+
Determines whether two path templates are structurally identical, meaning they have the same sequence
228+
of literals and template expressions, regardless of template expression names. In the OpenAPI context,
229+
such identical paths are considered invalid due to potential conflicts in routing.
230+
231+
```js
232+
import { isIdentical } from 'openapi-path-templating';
233+
234+
isIdentical('/pets/{petId}', '/pets/{name}'); // => true
235+
isIdentical('/pets/{petId}', '/animals/{name}'); // => false
236+
```
237+
216238
#### Grammar
217239

218240
New grammar instance can be created in following way:

package.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "openapi-path-templating",
33
"version": "2.0.2",
4-
"description": "OpenAPI Path Templating parser, validator and resolver.",
4+
"description": "OpenAPI Path Templating parser, validator, resolver and matcher.",
55
"main": "./cjs/index.cjs",
66
"types": "./types/index.d.ts",
77
"exports": {
@@ -41,7 +41,9 @@
4141
"templating",
4242
"parser",
4343
"validator",
44-
"resolver"
44+
"resolver",
45+
"matcher",
46+
"matching"
4547
],
4648
"author": "Vladimír Gorej <[email protected]>",
4749
"license": "Apache-2.0",

src/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export { default as Grammar } from './path-templating.js';
22
export { default as test } from './test.js';
33
export { default as parse } from './parse/index.js';
44
export { default as resolve, encodePathComponent } from './resolve.js';
5+
export { isIdentical } from './predicates.js';

src/predicates.js

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import parse from './parse/index.js';
2+
3+
export const isIdentical = (pathTemplate1, pathTemplate2) => {
4+
if (typeof pathTemplate1 !== 'string') return false;
5+
if (typeof pathTemplate2 !== 'string') return false;
6+
7+
const parseResult1 = parse(pathTemplate1);
8+
const parseResult2 = parse(pathTemplate2);
9+
const parts1 = [];
10+
const parts2 = [];
11+
12+
if (!parseResult1.result.success) return false;
13+
if (!parseResult2.result.success) return false;
14+
15+
parseResult1.ast.translate(parts1);
16+
parseResult2.ast.translate(parts2);
17+
18+
if (parts1.length !== parts2.length) return false;
19+
20+
for (let i = 1; i < parts1.length; i++) {
21+
const [type1, value1] = parts1[i];
22+
const [type2, value2] = parts2[i];
23+
24+
if (type1 !== type2) return false;
25+
if (type1 === 'template-expression' || type1 === 'template-expression-param-name') continue;
26+
if (value1 !== value2) return false;
27+
}
28+
29+
return true;
30+
};

test/predicates.js

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { assert } from 'chai';
2+
3+
import { isIdentical } from '../src/predicates.js';
4+
5+
describe('predicates', function () {
6+
context('isIdentical', function () {
7+
specify('should consider path templates identical', function () {
8+
assert.isTrue(isIdentical('/pets/{petId}', '/pets/{name}'));
9+
assert.isTrue(isIdentical('/pets/{petId}/owners/{ownerId}', '/pets/{id}/owners/{id}'));
10+
assert.isTrue(isIdentical('/pets/static/segment', '/pets/static/segment'));
11+
assert.isTrue(isIdentical('/{entity}/{id}', '/{resource}/{key}'));
12+
assert.isTrue(isIdentical('/pets/{petId}/', '/pets/{name}/'));
13+
});
14+
15+
specify('should not consider path templates identical', function () {
16+
assert.isFalse(isIdentical('/', '/{petId}'));
17+
assert.isFalse(isIdentical('/pets/{petId}', '/animals/{name}'));
18+
assert.isFalse(isIdentical('/pets/{petId}', '/pets/{petId}/owners/{ownerId}'));
19+
assert.isFalse(isIdentical('/pets/static/segment', '/pets/different/segment'));
20+
assert.isFalse(isIdentical('/pets/{petId}/', '/pets/{petId}'));
21+
assert.isFalse(isIdentical('/pets/{petId}/owners/{id}', '/pets/{petId}/owners/static'));
22+
});
23+
24+
specify('should return false on invalid path templates', function () {
25+
assert.isFalse(isIdentical('/a', 'b'));
26+
assert.isFalse(isIdentical('a', '/b'));
27+
assert.isFalse(isIdentical('a', 'b'));
28+
assert.isFalse(isIdentical('', '/'));
29+
});
30+
31+
specify('should handle complex cases correctly', function () {
32+
assert.isTrue(isIdentical('/pets/{petId}/owners/{ownerId}', '/pets/{id}/owners/{id}'));
33+
assert.isTrue(isIdentical('/{entity}/{id}/{action}', '/{resource}/{key}/{operation}'));
34+
assert.isFalse(isIdentical('/pets/{petId}/owners/{ownerId}', '/pets/{id}/{ownerId}'));
35+
assert.isFalse(isIdentical('/pets/{petId}/{ownerId}', '/pets/{id}/{id}/actions'));
36+
assert.isFalse(isIdentical('/pets/{petId}/{ownerId}', '/pets/{petId}/{ownerId}/actions'));
37+
});
38+
39+
specify('should handle non-string inputs gracefully', function () {
40+
assert.isFalse(isIdentical(123, 456));
41+
assert.isFalse(isIdentical({}, {}));
42+
assert.isFalse(isIdentical([], []));
43+
assert.isFalse(isIdentical(1, 2));
44+
assert.isFalse(isIdentical(null, '/b'));
45+
assert.isFalse(isIdentical(undefined, undefined));
46+
});
47+
});
48+
});

types/index.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,4 @@ export function test(pathTemplate: string, options?: TestOptions): boolean;
5151
export function resolve(pathTemplate: string, parameters: Parameters, options?: ResolveOptions): string;
5252
export function encodePathComponent(parameterValue: string): string;
5353
export function Grammar(): Grammar;
54+
export function isIdentical(pathTemplate1: string, pathTemplate2: string): boolean;

0 commit comments

Comments
 (0)