-
Notifications
You must be signed in to change notification settings - Fork 36
/
PathResolver.ts
146 lines (130 loc) · 4.77 KB
/
PathResolver.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import {escapeRegExp} from 'lodash';
const TEMPLATE_RE = /^(.*?){(.*?)}(.*)$/;
import { ParametersMap } from '../../types';
export type PathParserFunction = (pathname: string) => {matched: string, rawPathParams: ParametersMap<any>} | null;
/**
* @param path - The path to check.
* @returns true if the specified path uses templating, false otherwise.
*/
export function hasTemplates(path: string) : boolean {
return !!TEMPLATE_RE.exec(path);
}
/**
* Given a path containing template parts (e.g. "/foo/{bar}/baz"), returns
* a regular expression that matches the path, and a list of parameters found.
*
* @param path - The path to convert.
* @param options.openEnded - If true, then the returned `regex` will
* accept extra input at the end of the path.
*
* @returns A `{regex, params, parser}`, where:
* - `params` is a list of parameters found in the path.
* - `regex` is a regular expression that will match the path. When calling
* `match = regex.exec(str)`, each parameter in `params[i]` will be present
* in `match[i+1]`.
* - `parser` is a `fn(str)` that, given a path, will return null if
* the string does not match, and a `{matched, pathParams}` object if the
* path matches. `pathParams` is an object where keys are parameter names
* and values are strings from the `path`. `matched` is full string matched
* by the regex.
*/
export function compileTemplatePath(
path: string,
options: {
openEnded?: boolean
} = {}
) : {
params: string[],
regex: RegExp,
parser: PathParserFunction
} {
const params : string[] = [];
// Split up the path at each parameter.
const regexParts : string[] = [];
let remainingPath = path;
let tempateMatch;
do {
tempateMatch = TEMPLATE_RE.exec(remainingPath);
if(tempateMatch) {
regexParts.push(tempateMatch[1]);
params.push(tempateMatch[2]);
remainingPath = tempateMatch[3];
}
} while(tempateMatch);
regexParts.push(remainingPath);
const regexStr = regexParts.map(escapeRegExp).join('([^/]*)');
const regex = options.openEnded ? new RegExp(`^${regexStr}`) : new RegExp(`^${regexStr}$`);
const parser = (urlPathname: string) => {
const match = regex.exec(urlPathname);
if(match) {
return {
matched: match[0],
rawPathParams: params.reduce(
(result: ParametersMap<string | string[]>, paramName, index) => {
result[paramName] = match[index + 1];
return result;
},
{}
)
};
} else {
return null;
}
};
return {regex, params, parser};
}
export default class PathResolver<T> {
// Static paths with no templating are stored in a hash, for easy lookup.
private readonly _staticPaths: {[key: string]: T};
// Paths with templates are stored in an array, with parser functions that
// recognize the path.
private readonly _dynamicPaths: {parser: PathParserFunction, value: T}[];
// TODO: Pass in variable styles. Some variable styles start with a special
// character, and we can check to see if the character is there or not.
// (Or, replace this whole class with a uri-template engine.)
constructor() {
this._staticPaths = Object.create(null);
this._dynamicPaths = [];
}
registerPath(path: string, value: T) {
if(!path.startsWith('/')) {
throw new Error(`Invalid path "${path}"`);
}
if(hasTemplates(path)) {
const {parser} = compileTemplatePath(path);
this._dynamicPaths.push({value, parser});
} else {
this._staticPaths[path] = value;
}
}
/**
* Given a `pathname` from a URL (e.g. "/foo/bar") this will return the
* a static path if one exists, otherwise a path with templates if one
* exists.
*
* @param urlPathname - The pathname to search for.
* @returns A `{value, rawPathParams} object if a path is matched, or
* undefined if there was no match.
*/
resolvePath(urlPathname: string) {
let value : T | undefined = this._staticPaths[urlPathname];
let rawPathParams : ParametersMap<string | string[]> | undefined;
if(!value) {
for(const dynamicPath of this._dynamicPaths) {
const matched = dynamicPath.parser(urlPathname);
if(matched) {
value = dynamicPath.value;
rawPathParams = matched.rawPathParams;
}
}
}
if(value) {
return {
value,
rawPathParams
};
} else {
return undefined;
}
}
}