Skip to content

Commit 0924abb

Browse files
committed
omit absent query params from match result
Absent query parameters are now omitted from the result object rather than set to empty string, so callers can use `vars.param ?? default`. This also distinguishes 'param absent' from 'param present but empty' (e.g. ?q= returns {q: ''}). Also removes dead code: the isQuery field was always false since query parts go to a separate array, and the (?:\\?.*)?$ regex suffix was unreachable since pathPart already excludes the query string.
1 parent 3caa7e4 commit 0924abb

File tree

2 files changed

+25
-45
lines changed

2 files changed

+25
-45
lines changed

src/shared/uriTemplate.ts

Lines changed: 17 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -255,31 +255,26 @@ export class UriTemplate {
255255

256256
// Build regex pattern for path (non-query) parts
257257
let pattern = '^';
258-
const names: Array<{ name: string; exploded: boolean; isQuery: boolean }> = [];
258+
const names: Array<{ name: string; exploded: boolean }> = [];
259259
const queryParts: Array<{ name: string; exploded: boolean }> = [];
260260

261261
for (const part of this.parts) {
262262
if (typeof part === 'string') {
263263
pattern += this.escapeRegExp(part);
264+
} else if (part.operator === '?' || part.operator === '&') {
265+
for (const name of part.names) {
266+
queryParts.push({ name, exploded: part.exploded });
267+
}
264268
} else {
265-
if (part.operator === '?' || part.operator === '&') {
266-
// Collect query parameter names for later extraction
267-
for (const name of part.names) {
268-
queryParts.push({ name, exploded: part.exploded });
269-
}
270-
} else {
271-
// Handle non-query parts normally
272-
const patterns = this.partToRegExp(part);
273-
for (const { pattern: partPattern, name } of patterns) {
274-
pattern += partPattern;
275-
names.push({ name, exploded: part.exploded, isQuery: false });
276-
}
269+
const patterns = this.partToRegExp(part);
270+
for (const { pattern: partPattern, name } of patterns) {
271+
pattern += partPattern;
272+
names.push({ name, exploded: part.exploded });
277273
}
278274
}
279275
}
280276

281-
// Match the path part (without query parameters)
282-
pattern += '(?:\\?.*)?$'; // Allow optional query string at the end
277+
pattern += '$';
283278
UriTemplate.validateLength(pattern, MAX_REGEX_LENGTH, 'Generated regex pattern');
284279
const regex = new RegExp(pattern);
285280
const match = pathPart.match(regex);
@@ -288,29 +283,16 @@ export class UriTemplate {
288283

289284
const result: Variables = {};
290285

291-
// Extract non-query parameters
292-
let matchIndex = 0;
293-
for (const { name, exploded, isQuery } of names) {
294-
if (!isQuery) {
295-
const value = match[matchIndex + 1];
296-
const cleanName = name.replace('*', '');
297-
298-
if (exploded && value && value.includes(',')) {
299-
result[cleanName] = value.split(',');
300-
} else {
301-
result[cleanName] = value;
302-
}
303-
matchIndex++;
304-
}
286+
for (const [i, { name, exploded }] of names.entries()) {
287+
const value = match[i + 1]!;
288+
const cleanName = name.replace('*', '');
289+
result[cleanName] = exploded && value.includes(',') ? value.split(',') : value;
305290
}
306291

307-
// Extract query parameters from query string
308292
if (queryParts.length > 0) {
309293
const queryParams = new Map<string, string>();
310294
if (queryPart) {
311-
// Parse query string
312-
const pairs = queryPart.split('&');
313-
for (const pair of pairs) {
295+
for (const pair of queryPart.split('&')) {
314296
const equalIndex = pair.indexOf('=');
315297
if (equalIndex !== -1) {
316298
const key = decodeURIComponent(pair.slice(0, equalIndex));
@@ -320,18 +302,12 @@ export class UriTemplate {
320302
}
321303
}
322304

323-
// Extract values for each expected query parameter
324305
for (const { name, exploded } of queryParts) {
325306
const cleanName = name.replace('*', '');
326307
const value = queryParams.get(cleanName);
327308

328-
if (value === undefined) {
329-
result[cleanName] = '';
330-
} else if (exploded && value.includes(',')) {
331-
result[cleanName] = value.split(',');
332-
} else {
333-
result[cleanName] = value;
334-
}
309+
if (value === undefined) continue;
310+
result[cleanName] = exploded && value.includes(',') ? value.split(',') : value;
335311
}
336312
}
337313

test/shared/uriTemplate.test.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ describe('UriTemplate', () => {
194194
it('should handle partial query parameter matches correctly', () => {
195195
const template = new UriTemplate('/search{?q,page}');
196196
const match = template.match('/search?q=test');
197-
expect(match).toEqual({ q: 'test', page: '' });
197+
expect(match).toEqual({ q: 'test' });
198198
expect(template.variableNames).toEqual(['q', 'page']);
199199
});
200200

@@ -215,10 +215,16 @@ describe('UriTemplate', () => {
215215
it('should match omitted query parameters', () => {
216216
const template = new UriTemplate('/search{?q,page}');
217217
const match = template.match('/search');
218-
expect(match).toEqual({ q: '', page: '' });
218+
expect(match).toEqual({});
219219
expect(template.variableNames).toEqual(['q', 'page']);
220220
});
221221

222+
it('should distinguish absent from empty query parameters', () => {
223+
const template = new UriTemplate('/search{?q,page}');
224+
const match = template.match('/search?q=');
225+
expect(match).toEqual({ q: '' });
226+
});
227+
222228
it('should match nested path segments with query parameters', () => {
223229
const template = new UriTemplate('/api/{version}/{resource}{?apiKey,q,p,sort}');
224230
const match = template.match('/api/v1/users?apiKey=testkey&q=user');
@@ -227,8 +233,6 @@ describe('UriTemplate', () => {
227233
resource: 'users',
228234
apiKey: 'testkey',
229235
q: 'user',
230-
p: '',
231-
sort: ''
232236
});
233237
expect(template.variableNames).toEqual(['version', 'resource', 'apiKey', 'q', 'p', 'sort']);
234238
});

0 commit comments

Comments
 (0)