Skip to content

Commit 90a1127

Browse files
committed
Support params in spec paths, by using regexes as Trie keys
This requires a fairly sweeping change everywhere from objects to ES6, which is the main thrust of all this, and then lots of careful handling of paths that are arrays of string & regex, rather than just single strings.
1 parent 42def9d commit 90a1127

14 files changed

+453
-166
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
declare module '@httptoolkit/serialize-javascript' {
2+
import * as serializeToJs from 'serialize-javascript';
3+
export = serializeToJs;
4+
}

package-lock.json

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,14 @@
4040
"openapi3-ts": "^1.2.0"
4141
},
4242
"devDependencies": {
43+
"@httptoolkit/serialize-javascript": "^1.7.0-sets-and-maps",
4344
"@types/chai": "^4.1.7",
4445
"@types/fs-extra": "^5.0.4",
4546
"@types/globby": "^8.0.0",
4647
"@types/lodash": "^4.14.120",
4748
"@types/mocha": "^5.2.5",
4849
"@types/node": "^11.9.0",
50+
"@types/serialize-javascript": "^1.5.0",
4951
"@types/swagger-parser": "^4.0.2",
5052
"chai": "^4.2.0",
5153
"fs-extra": "^7.0.1",

src/buildtime/build-all.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as path from 'path';
22
import * as fs from 'fs-extra';
3+
import * as serializeToJs from '@httptoolkit/serialize-javascript';
34

45
import { generateApis } from "./generate-apis";
56
import { buildTrie } from './build-index';
@@ -12,12 +13,13 @@ export async function buildAll(globs: string[]) {
1213
console.log('APIs generated and written to disk');
1314

1415
await fs.writeFile(
15-
path.join('api', '_index.json'),
16-
JSON.stringify(buildTrie(index))
16+
path.join('api', '_index.js'),
17+
'module.exports = ' + serializeToJs(buildTrie(index), {
18+
unsafe: true // We're not embedded in HTML, we don't want XSS escaping
19+
})
1720
);
1821

19-
const indexSize = Object.keys(index).length;
20-
console.log(`Index trie for ${indexSize} entries generated and written to disk`);
22+
console.log(`Index trie for ${index.size} entries generated and written to disk`);
2123
}
2224

2325
if (require.main === module) {

src/buildtime/build-index.ts

Lines changed: 66 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,67 @@
11
import * as _ from 'lodash';
2-
import { TrieData, isLeafValue } from "../runtime/trie";
2+
import { TrieData, isLeafValue, TrieValue } from "../runtime/trie";
33

4-
export function buildTrie(map: { [key: string]: string | string[] }): TrieData {
4+
type TrieInput = Map<
5+
Array<string | RegExp>,
6+
string | string[]
7+
>;
8+
9+
export function buildTrie(map: TrieInput): TrieData {
510
return optimizeTrie(buildNaiveTrie(map));
611
}
712

813
// Build a simple naive trie. One level per char, string nodes at the leaves,
914
// and '' keys for leaf nodes half way down paths.
10-
export function buildNaiveTrie(map: { [key: string]: string | string[] }): TrieData {
11-
const root = <TrieData>{};
15+
export function buildNaiveTrie(map: TrieInput): TrieData {
16+
const root = <TrieData>new Map();
1217

1318
// For each key, make a new level for each char in the key (or use an existing
1419
// level), and place the leaf when we get to the end of the key.
1520

16-
_.forEach(map, (value, key) => {
21+
for (let [keys, value] of map) {
1722
let trie: TrieData = root;
18-
_.forEach(key, (char, i) => {
19-
let nextStep = trie[char];
2023

21-
if (i === key.length - 1) {
24+
const keyChunks = _.flatMap<string | RegExp, string | RegExp>(keys, (key) => {
25+
if (_.isRegExp(key)) return key;
26+
else return key.split('');
27+
});
28+
_.forEach(keyChunks, (chunk, i) => {
29+
let nextStep = chunk instanceof RegExp ?
30+
trie.get(_.find([...trie.keys()], k => _.isEqual(chunk, k))!) :
31+
trie.get(chunk);
32+
33+
let isLastChunk = i === keyChunks.length - 1;
34+
35+
if (isLastChunk) {
2236
// We're done - write our value into trie[char]
2337

2438
if (isLeafValue(nextStep)) {
2539
throw new Error('Duplicate key'); // Should really never happen
2640
} else if (typeof nextStep === 'object') {
2741
// We're half way down another key - add an empty branch
28-
nextStep[''] = value;
42+
nextStep.set('', value);
2943
} else {
3044
// We're a fresh leaf at the end of a branch
31-
trie[char] = value;
45+
trie.set(chunk, value);
3246
}
3347
} else {
3448
// We have more to go - iterate into trie[char]
3549

3650
if (isLeafValue(nextStep)) {
3751
// We're at what is currently a leaf value
3852
// Transform it into a node with '' for the value.
39-
nextStep = { '': nextStep };
40-
trie[char] = nextStep;
53+
nextStep = new Map([['', nextStep]]);
54+
trie.set(chunk, nextStep);
4155
} else if (typeof nextStep === 'undefined') {
4256
// We're adding a new branch to the trie
43-
nextStep = {};
44-
trie[char] = nextStep;
57+
nextStep = new Map();
58+
trie.set(chunk, nextStep);
4559
}
4660

4761
trie = nextStep;
4862
}
4963
});
50-
});
64+
}
5165

5266
return root;
5367
}
@@ -59,45 +73,55 @@ export function buildNaiveTrie(map: { [key: string]: string | string[] }): TrieD
5973
export function optimizeTrie(trie: TrieData): TrieData {
6074
if (_.isString(trie)) return trie;
6175

62-
const keys = Object.keys(trie);
76+
const keys = [...trie.keys()].filter(k => k !== '');
6377

6478
if (keys.length === 0) return trie;
6579

6680
if (keys.length === 1) {
81+
// If this level has one string key, combine it with the level below
6782
const [key] = keys;
68-
const child = trie[key]!;
83+
const child = trie.get(key)!;
6984

85+
// If the child is a final value, we can't combine this key with it, and we're done
86+
// TODO: Could optimize further here, and pull the child up in this case?
87+
// (Only if trie.size === 1 too). Seems unnecessary for now, a little risky.
7088
if (isLeafValue(child)) return trie;
7189

72-
// Don't combine if our child has a leaf node attached - this would break
73-
// search (en route leaf nodes need to always be under '' keys)
74-
if (!child['']) {
75-
// Return the only child, with every key prefixed with this key
76-
const newChild = optimizeTrie(
77-
_.mapKeys(child, (_value, childKey) => key + childKey)
90+
if (
91+
// Don't combine if our child has a leaf node attached - this would break
92+
// search (en route leaf nodes need to always be under '' keys)
93+
!child.get('') &&
94+
// If this key or any child key is a regex, we don't try to combine the
95+
// keys together. It's possible to do so, but a little messy,
96+
// not strictly necessary, and hurts runtime perf (testing up to N regexes
97+
// is worse than testing 1 regex + 1 string hash lookup).
98+
!_.isRegExp(keys[0]) &&
99+
!_.some([...child.keys()], k => _.isRegExp(k))
100+
) {
101+
// Replace this node with the only child, with every key prefixed with this key
102+
const collapsedChild = mapMap(child, (childKey, value) =>
103+
// We know keys are strings because we checked above
104+
[key + (childKey as string), value]
78105
);
79-
80-
return newChild;
81-
}
82-
}
83-
84-
if (keys.length === 2 && _.includes(keys, '')) {
85-
const [key] = keys.filter(k => k !== '');
86-
const child = trie[key]!;
87-
88-
const childKeys = Object.keys(child);
89-
if (!isLeafValue(child) && childKeys.length === 1 && childKeys[0] !== '') {
90-
// If child has only one key and it's not '', pull it up.
91-
return optimizeTrie({
92-
'': trie[''],
93-
[key + childKeys[0]]: child[childKeys[0]]
94-
});
106+
// We might still have an en-route leaf node at this level - don't lose it.
107+
if (trie.get('')) collapsedChild.set('', trie.get(''));
108+
// Then we reoptimize this same level again (we might be able to to collapse further)
109+
return optimizeTrie(collapsedChild);
95110
}
96111
}
97112

98113
// Recursive DFS through the child values to optimize them in turn
99-
return _.mapValues(trie, (child) => {
100-
if (isLeafValue(child)) return child;
101-
else return optimizeTrie(child!);
114+
return mapMap(trie, (key, child): [string | RegExp, TrieValue] => {
115+
if (isLeafValue(child)) return [key, child];
116+
else return [key, optimizeTrie(child!)];
102117
});
118+
}
119+
120+
function mapMap<K, V, K2, V2>(
121+
map: Map<K, V>,
122+
mapping: (a: K, b: V) => [K2, V2]
123+
): Map<K2, V2> {
124+
return new Map(
125+
Array.from(map, ([k, v]) => mapping(k, v))
126+
);
103127
}

0 commit comments

Comments
 (0)