Skip to content

Commit bd8c278

Browse files
committed
fix(core): match multi-variable URI templates like {a,b} (#2166)
`UriTemplate.expand` already joins multi-name expansions with commas, but `match` only emitted one regex capture per part and assigned the whole captured run to the first name. Anything past the first variable silently never matched and resources registered against templates like `data://users/{userId,format}` were unreachable. Emit one capture per name with literal commas between them in `partToRegExp`, mirroring what `expandPart` produces, so round tripping through expand and match recovers the original variables. Path / label / fragment operators get their existing literal prefix on the first capture; the bare and reserved cases just sit at the current position.
1 parent 16d13ab commit bd8c278

2 files changed

Lines changed: 54 additions & 0 deletions

File tree

packages/core/src/shared/uriTemplate.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,41 @@ export class UriTemplate {
222222
return patterns;
223223
}
224224

225+
// Multi-variable expressions like `{a,b}` or `{/a,b}` expand each value
226+
// and join them with commas (see `expandPart`). Mirror that on the way
227+
// back by emitting one capture per name with literal commas between
228+
// them. Without this, only the first name was assigned and the rest of
229+
// the path silently never matched (#2166).
230+
if (part.names.length > 1) {
231+
let firstPrefix: string;
232+
switch (part.operator) {
233+
case '/': {
234+
firstPrefix = '/';
235+
break;
236+
}
237+
case '.': {
238+
firstPrefix = String.raw`\.`;
239+
break;
240+
}
241+
case '#': {
242+
firstPrefix = '#';
243+
break;
244+
}
245+
default: {
246+
firstPrefix = '';
247+
}
248+
}
249+
for (let i = 0; i < part.names.length; i++) {
250+
const name = part.names[i]!;
251+
const prefix = i === 0 ? firstPrefix : ',';
252+
patterns.push({
253+
pattern: prefix + '([^/,]+)',
254+
name
255+
});
256+
}
257+
return patterns;
258+
}
259+
225260
let pattern: string;
226261
const name = part.name;
227262

packages/core/test/shared/uriTemplate.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,25 @@ describe('UriTemplate', () => {
9898
expect(match).toEqual({ username: 'fred', postId: '123' });
9999
});
100100

101+
it('should match a comma-separated multi-variable expression (#2166)', () => {
102+
const template = new UriTemplate('/users/{userId,format}');
103+
const match = template.match('/users/42,json');
104+
expect(match).toEqual({ userId: '42', format: 'json' });
105+
});
106+
107+
it('should round-trip expand and match for a multi-variable expression (#2166)', () => {
108+
const template = new UriTemplate('data://users/{userId,format}');
109+
const expanded = template.expand({ userId: '42', format: 'json' });
110+
expect(expanded).toBe('data://users/42,json');
111+
expect(template.match(expanded)).toEqual({ userId: '42', format: 'json' });
112+
});
113+
114+
it('should match a multi-variable expression with the path operator (#2166)', () => {
115+
const template = new UriTemplate('{/userId,format}');
116+
const match = template.match('/42,json');
117+
expect(match).toEqual({ userId: '42', format: 'json' });
118+
});
119+
101120
it('should return null for non-matching URIs', () => {
102121
const template = new UriTemplate('/users/{username}');
103122
const match = template.match('/posts/123');

0 commit comments

Comments
 (0)