Skip to content

Commit 86e07ef

Browse files
fix(UrlMatcher): Format parent/child UrlMatchers properly
Build arrays of path parameters and query parameters for the entire path of UrlMatchers. Map/reduce them, then concatenate at the end. closes ##2504
1 parent bdfe188 commit 86e07ef

File tree

2 files changed

+80
-38
lines changed

2 files changed

+80
-38
lines changed

src/params/param.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {paramTypes} from "./paramTypes";
1111
let hasOwn = Object.prototype.hasOwnProperty;
1212
let isShorthand = cfg => ["value", "type", "squash", "array", "dynamic"].filter(hasOwn.bind(cfg || {})).length === 0;
1313

14-
enum DefType {
14+
export enum DefType {
1515
PATH, SEARCH, CONFIG
1616
}
1717

src/url/urlMatcher.ts

+79-37
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import {prop, propEq } from "../common/hof";
77
import {isArray, isString} from "../common/predicates";
88
import {Param, paramTypes} from "../params/module";
99
import {isDefined} from "../common/predicates";
10+
import {DefType} from "../params/param";
11+
import {unnestR} from "../common/common";
12+
import {arrayTuples} from "../common/common";
1013

1114
interface params {
1215
$$validates: (params: string) => Array<string>;
@@ -364,54 +367,93 @@ export class UrlMatcher {
364367
* @returns {string} the formatted URL (path and optionally search part).
365368
*/
366369
format(values = {}) {
367-
let segments: string[] = this._segments,
368-
result: string = segments[0],
369-
search: boolean = false,
370-
params: Param[] = this.parameters({inherit: false}),
371-
parent: UrlMatcher = tail(this._cache.path);
372-
373370
if (!this.validates(values)) return null;
374371

375-
function encodeDashes(str) { // Replace dashes with encoded "\-"
376-
return encodeURIComponent(str).replace(/-/g, c => `%5C%${c.charCodeAt(0).toString(16).toUpperCase()}`);
377-
}
372+
// Build the full path of UrlMatchers (including all parent UrlMatchers)
373+
let urlMatchers = this._cache.path.slice().concat(this);
374+
375+
// Extract all the static segments and Params into an ordered array
376+
let pathSegmentsAndParams: Array<string|Param> =
377+
urlMatchers.map(UrlMatcher.pathSegmentsAndParams).reduce(unnestR, []);
378378

379-
// TODO: rewrite as reduce over params with result as initial
380-
params.map((param: Param, i) => {
381-
let isPathParam = i < segments.length - 1;
382-
var isFinalPathParam = i + 2 === segments.length;
379+
// Extract the query params into a separate array
380+
let queryParams: Array<Param> =
381+
urlMatchers.map(UrlMatcher.queryParams).reduce(unnestR, []);
382+
383+
/**
384+
* Given a Param,
385+
* Applies the parameter value, then returns details about it
386+
*/
387+
function getDetails(param: Param): ParamDetails {
388+
// Normalize to typed value
383389
let value = param.value(values[param.id]);
384390
let isDefaultValue = param.isDefaultValue(value);
391+
// Check if we're in squash mode for the parameter
385392
let squash = isDefaultValue ? param.squash : false;
393+
// Allow the Parameter's Type to encode the value
386394
let encoded = param.type.encode(value);
387395

388-
if (!isPathParam) {
389-
if (encoded == null || (isDefaultValue && squash !== false)) return;
390-
if (!isArray(encoded)) encoded = [<string> encoded];
391-
if (encoded.length === 0) return;
396+
return { param, value, isDefaultValue, squash, encoded };
397+
}
392398

393-
encoded = map(<string[]> encoded, encodeURIComponent).join(`&${param.id}=`);
394-
result += (search ? '&' : '?') + (`${param.id}=${encoded}`);
395-
search = true;
396-
return;
397-
}
399+
// Build up the path-portion from the list of static segments and parameters
400+
let pathString = pathSegmentsAndParams.reduce((acc: string, x: string|Param) => {
401+
// The element is a static segment (a raw string); just append it
402+
if (isString(x)) return acc + x;
403+
404+
// Otherwise, it's a Param. Fetch details about the parameter value
405+
let {squash, encoded, param} = getDetails(<Param> x);
406+
407+
// If squash is === true, try to remove a slash from the path
408+
if (squash === true) return (acc.match(/\/$/)) ? acc.slice(0, -1) : acc;
409+
// If squash is a string, use the string for the param value
410+
if (isString(squash)) return acc + squash;
411+
if (squash !== false) return acc; // ?
412+
if (encoded == null) return acc;
413+
// If this parameter value is an array, encode the value using encodeDashes
414+
if (isArray(encoded)) return acc + map(<string[]> encoded, UrlMatcher.encodeDashes).join("-");
415+
// If the parameter type is "raw", then do not encodeURIComponent
416+
if (param.type.raw) return acc + encoded;
417+
// Encode the value
418+
return acc + encodeURIComponent(<string> encoded);
419+
}, "");
420+
421+
// Build the query string by
422+
let queryString = queryParams.map((param: Param) => {
423+
let {squash, encoded, isDefaultValue} = getDetails(param);
424+
if (encoded == null || (isDefaultValue && squash !== false)) return;
425+
if (!isArray(encoded)) encoded = [<string> encoded];
426+
if (encoded.length === 0) return;
427+
if (!param.type.raw) encoded = map(<string[]> encoded, encodeURIComponent);
428+
429+
return encoded.map(val => `${param.id}=${val}`);
430+
}).filter(identity).reduce(unnestR, []).join("&");
431+
432+
// Concat the pathstring with the queryString (if exists) and the hasString (if exists)
433+
return pathString + (queryString ? `?${queryString}` : "") + (values["#"] ? "#" + values["#"] : "");
434+
}
398435

399-
result += ((segment, result) => {
400-
if (squash === true) return segment.match(result.match(/\/$/) ? /\/?(.*)/ : /(.*)/)[1];
401-
if (isString(squash)) return squash + segment;
402-
if (squash !== false) return "";
403-
if (encoded == null) return segment;
404-
if (isArray(encoded)) return map(<string[]> encoded, encodeDashes).join("-") + segment;
405-
if (param.type.raw) return encoded + segment;
406-
return encodeURIComponent(<string> encoded) + segment;
407-
})(segments[i + 1], result);
408-
409-
if (isFinalPathParam && squash === true && result.slice(-1) === '/') result = result.slice(0, -1);
410-
});
436+
static encodeDashes(str) { // Replace dashes with encoded "\-"
437+
return encodeURIComponent(str).replace(/-/g, c => `%5C%${c.charCodeAt(0).toString(16).toUpperCase()}`);
438+
}
411439

412-
if (values["#"]) result += "#" + values["#"];
440+
/** Given a matcher, return an array with the matcher's path segments and path params, in order */
441+
static pathSegmentsAndParams(matcher: UrlMatcher) {
442+
let staticSegments = matcher._segments;
443+
let pathParams = matcher._params.filter(p => p.location === DefType.PATH);
444+
return arrayTuples(staticSegments, pathParams.concat(undefined)).reduce(unnestR, []).filter(x => x !== "" && isDefined(x));
445+
}
413446

414-
let processedParams = ['#'].concat(params.map(prop('id')));
415-
return (parent && parent.format(omit(values, processedParams)) || '') + result;
447+
/** Given a matcher, return an array with the matcher's query params */
448+
static queryParams(matcher: UrlMatcher): Param[] {
449+
return matcher._params.filter(p => p.location === DefType.SEARCH);
416450
}
417451
}
452+
453+
interface ParamDetails {
454+
param: Param;
455+
value: any;
456+
isDefaultValue: boolean;
457+
squash: (boolean|string);
458+
encoded: (string|string[]);
459+
}

0 commit comments

Comments
 (0)