Skip to content

Commit 6449e6b

Browse files
committed
feat: 🎸 add code
1 parent f0fd447 commit 6449e6b

19 files changed

+1100
-4
lines changed

‎package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@
6060
"tslib": "2"
6161
},
6262
"peerDependenciesMeta": {},
63-
"dependencies": {},
63+
"dependencies": {
64+
"@jsonjoy.com/util": "^1.1.0",
65+
"sonic-forest": "^1.0.0"
66+
},
6467
"devDependencies": {
6568
"@types/benchmark": "^2.1.5",
6669
"@types/jest": "^29.5.12",

‎src/__bench__/realistic.bench.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/* tslint:disable no-console */
2+
3+
import {definitions, routes} from './routes';
4+
import {routers} from './routers';
5+
6+
const {Suite} = require('benchmark');
7+
const suite = new Suite();
8+
const noop = () => {};
9+
10+
for (const router of routers) {
11+
for (const definition of definitions) router.register(definition, noop);
12+
if (router.finalize) router.finalize();
13+
// console.log();
14+
// console.log(`Structure "${router.name}":`);
15+
// if (router.print) router.print();
16+
// console.log();
17+
// console.log(`Test "${router.name}":`);
18+
for (const [method, path] of routes) {
19+
const match = router.find(method, path);
20+
// console.log(router.name, `${method} ${path}`, match);
21+
}
22+
}
23+
24+
for (const router of routers) {
25+
const find = router.find;
26+
suite.add(`${router.name}`, () => {
27+
find('DELETE', '/api/collections/123/documents/456/revisions/789');
28+
find('GET', '/users');
29+
find('GET', '/users/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx');
30+
find('GET', '/users/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/followers');
31+
find('GET', '/users/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/followers/yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy');
32+
find('POST', '/users/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/followers/yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy');
33+
find('PUT', '/files/user123-movies/2019/01/01/1.mp4');
34+
find('GET', '/files/user123-movies/2019/01/01/1.mp4');
35+
find('GET', '/static/some/path/to/file.txt');
36+
find('GET', '/ping');
37+
find('GET', '/pong');
38+
find('GET', '/info');
39+
});
40+
}
41+
42+
for (const [method, path] of routes) {
43+
for (const router of routers) {
44+
suite.add(`${router.name}: ${method} ${path}`, () => {
45+
router.find(method, path);
46+
});
47+
}
48+
}
49+
50+
suite
51+
.on('cycle', (event: any) => {
52+
console.log(String(event.target) + `, ${Math.round(1000000000 / event.target.hz)} ns/op`);
53+
})
54+
.on('complete', () => {
55+
console.log('Fastest is ' + suite.filter('fastest').map('name'));
56+
})
57+
.run();
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/* tslint:disable no-console */
2+
3+
import {Router} from '..';
4+
5+
const router = new Router();
6+
7+
const noop = () => {};
8+
router.add('GET /user', noop);
9+
router.add('GET /user/comments', noop);
10+
router.add('GET /user/avatar', noop);
11+
router.add('GET /user/lookup/username/{username}', noop);
12+
router.add('GET /user/lookup/email/{address}', noop);
13+
router.add('GET /event/{id}', noop);
14+
router.add('GET /event/{id}/comments', noop);
15+
router.add('POST /event/{id}/comment', noop);
16+
router.add('GET /map/{location}/events', noop);
17+
router.add('GET /status', noop);
18+
router.add('GET /very/deep/nested/route/hello/there', noop);
19+
router.add('GET /static/{::\n}', noop);
20+
21+
// router.add('GET /posts', noop);
22+
// router.add('GET /posts/search', noop);
23+
// router.add('GET /posts/{post}', noop);
24+
// router.add('PUT /posts/{post}', noop);
25+
// router.add('POST /posts/{post}', noop);
26+
// router.add('DELETE /posts/{post}', noop);
27+
// router.add('POST /posts', noop);
28+
console.log(router + '');
29+
30+
const matcher = router.compile();
31+
console.log(matcher + '');
32+
33+
const operations = 1e6;
34+
35+
console.time('short static');
36+
for (let i = 0; i < operations; i++) {
37+
matcher('GET /user');
38+
}
39+
console.timeEnd('short static');
40+
41+
console.time('static with same radix');
42+
for (let i = 0; i < operations; i++) {
43+
matcher('GET /user/comments');
44+
}
45+
console.timeEnd('static with same radix');
46+
47+
console.time('dynamic route');
48+
for (let i = 0; i < operations; i++) {
49+
matcher('GET /user/lookup/username/john');
50+
}
51+
console.timeEnd('dynamic route');
52+
53+
console.time('mixed static dynamic');
54+
for (let i = 0; i < operations; i++) {
55+
matcher('GET /event/abcd1234/comments');
56+
}
57+
console.timeEnd('mixed static dynamic');
58+
59+
console.time('long static');
60+
for (let i = 0; i < operations; i++) {
61+
matcher('GET /very/deep/nested/route/hello/there');
62+
}
63+
console.timeEnd('long static');
64+
65+
console.time('wildcard');
66+
for (let i = 0; i < operations; i++) {
67+
matcher('GET /static/index.html');
68+
}
69+
console.timeEnd('wildcard');
70+
71+
console.time('all together');
72+
for (let i = 0; i < operations; i++) {
73+
matcher('GET /user');
74+
matcher('GET /user/comments');
75+
matcher('GET /user/lookup/username/john');
76+
matcher('GET /event/abcd1234/comments');
77+
matcher('GET /very/deeply/nested/route/hello/there');
78+
matcher('GET /static/index.html');
79+
}
80+
console.timeEnd('all together');

‎src/__bench__/routers.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/* tslint:disable no-console */
2+
3+
import {Router} from '..';
4+
import findMyWay from 'find-my-way';
5+
import {RouteDefinition} from './routes';
6+
7+
export type RouterDefinition = {
8+
name: string;
9+
register: (def: RouteDefinition, data: any) => void;
10+
finalize?: () => void;
11+
print?: () => void;
12+
find: (method: string, path: string) => unknown;
13+
};
14+
15+
export const routers: RouterDefinition[] = [];
16+
17+
const theRouter = new Router();
18+
let matcher: any;
19+
routers.push({
20+
name: 'json-joy router',
21+
register: ([method, steps]: RouteDefinition, data: any) => {
22+
const path = steps
23+
.map((step) => {
24+
if (typeof step === 'string') return step;
25+
if (Array.isArray(step)) {
26+
if (Array.isArray(step[0])) return `{${step[0][0]}::\n}`;
27+
else return `{${step[0]}}`;
28+
}
29+
return '';
30+
})
31+
.join('/');
32+
theRouter.add(`${method} /${path}`, data);
33+
},
34+
finalize: () => {
35+
matcher = theRouter.compile();
36+
},
37+
print: () => {
38+
console.log(theRouter + '');
39+
console.log(matcher + '');
40+
},
41+
find: (method: string, path: string) => {
42+
return matcher(method + ' ' + path);
43+
},
44+
});
45+
46+
const router = findMyWay();
47+
// router.prettyPrint();
48+
routers.push({
49+
name: 'find-my-way',
50+
register: ([method, steps]: RouteDefinition, data: any) => {
51+
const path =
52+
'/' +
53+
steps
54+
.map((step) => {
55+
if (typeof step === 'string') return step;
56+
if (Array.isArray(step)) {
57+
if (Array.isArray(step[0])) return `*`;
58+
else return `:${step[0]}`;
59+
}
60+
return '';
61+
})
62+
.join('/');
63+
router.on(method, path, data);
64+
},
65+
find: (method: string, path: string) => {
66+
return router.find(method as any, path);
67+
},
68+
});

‎src/__bench__/routes.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
export type Var = [string];
2+
export type Wildcard = [[string]];
3+
export type RouteDefinition = [method: 'GET' | 'POST' | 'DELETE' | 'PUT', path: (string | Var | Wildcard)[]];
4+
5+
export const definitions: RouteDefinition[] = [
6+
['GET', ['ping']],
7+
['GET', ['pong']],
8+
['POST', ['ping']],
9+
['POST', ['echo']],
10+
['GET', ['info']],
11+
['GET', ['types']],
12+
['PUT', ['events']],
13+
14+
['GET', ['users']],
15+
['GET', ['users', ['id']]],
16+
['DELETE', ['users', ['id']]],
17+
['POST', ['users', ['id']]],
18+
['GET', ['users', ['id'], 'followers']],
19+
['GET', ['users', ['id'], 'followers', ['friend']]],
20+
['POST', ['users', ['id'], 'followers', ['friend']]],
21+
['DELETE', ['users', ['id'], 'followers', ['friend']]],
22+
23+
['GET', ['posts']],
24+
['GET', ['posts', 'search']],
25+
['POST', ['posts']],
26+
['GET', ['posts', ['post']]],
27+
['POST', ['posts', ['post']]],
28+
['DELETE', ['posts', ['post']]],
29+
30+
['GET', ['posts', ['post'], 'tags']],
31+
['GET', ['posts', ['post'], 'tags', 'top']],
32+
['GET', ['posts', ['post'], 'tags', ['tag']]],
33+
['DELETE', ['posts', ['post'], 'tags', ['tag']]],
34+
['POST', ['posts', ['post'], 'tags']],
35+
36+
['GET', ['api', 'collections']],
37+
['POST', ['api', 'collections']],
38+
['GET', ['api', 'collections', ['id']]],
39+
['PUT', ['api', 'collections', ['id']]],
40+
['POST', ['api', 'collections', ['id']]],
41+
['DELETE', ['api', 'collections', ['id']]],
42+
43+
['GET', ['api', 'collections', ['id'], 'documents']],
44+
['POST', ['api', 'collections', ['id'], 'documents']],
45+
['GET', ['api', 'collections', ['collection'], 'documents', ['document']]],
46+
['PUT', ['api', 'collections', ['collection'], 'documents', ['document']]],
47+
['POST', ['api', 'collections', ['collection'], 'documents', ['document']]],
48+
['DELETE', ['api', 'collections', ['collection'], 'documents', ['document']]],
49+
['GET', ['api', 'collections', ['collection'], 'documents', 'search']],
50+
51+
['GET', ['api', 'collections', ['collection'], 'documents', ['document'], 'revisions']],
52+
['GET', ['api', 'collections', ['collection'], 'documents', ['document'], 'revisions', 'find']],
53+
['POST', ['api', 'collections', ['collection'], 'documents', ['document'], 'revisions']],
54+
['GET', ['api', 'collections', ['collection'], 'documents', ['document'], 'revisions', ['revision']]],
55+
['DELETE', ['api', 'collections', ['collection'], 'documents', ['document'], 'revisions', ['revision']]],
56+
57+
['GET', ['files', ['bucket'], [['path']]]],
58+
['PUT', ['files', ['bucket'], [['path']]]],
59+
['GET', ['files', 'list', ['bucket'], [['path']]]],
60+
['GET', ['files', 'list', [['path']]]],
61+
62+
['GET', ['static', [['path']]]],
63+
];
64+
65+
export const routes: [method: string, path: string][] = [
66+
['GET', '/ping'],
67+
['GET', '/pong'],
68+
['POST', '/ping'],
69+
['POST', '/echo'],
70+
['GET', '/info'],
71+
['GET', '/types'],
72+
['PUT', '/events'],
73+
['GET', '/users'],
74+
['GET', '/users/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'],
75+
['GET', '/users/123'],
76+
['DELETE', '/users/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'],
77+
['POST', '/users/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'],
78+
['GET', '/users/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/followers'],
79+
['GET', '/users/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/followers/yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy'],
80+
['POST', '/users/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/followers/yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy'],
81+
['DELETE', '/users/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/followers/yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy'],
82+
['GET', '/posts'],
83+
['GET', '/posts/search'],
84+
['POST', '/posts'],
85+
['GET', '/posts/jhasdf982lsd'],
86+
['POST', '/posts/jhasdf982lsd'],
87+
['DELETE', '/posts/jhasdf982lsd'],
88+
['GET', '/posts/jhasdf982lsd/tags'],
89+
['GET', '/posts/jhasdf982lsd/tags/top'],
90+
['GET', '/posts/jhasdf982lsd/tags/123'],
91+
['DELETE', '/posts/jhasdf982lsd/tags/123'],
92+
['POST', '/posts/jhasdf982lsd/tags'],
93+
['GET', '/api/collections'],
94+
['POST', '/api/collections'],
95+
['GET', '/api/collections/123'],
96+
['PUT', '/api/collections/123'],
97+
['POST', '/api/collections/123'],
98+
['DELETE', '/api/collections/123'],
99+
['GET', '/api/collections/123/documents'],
100+
['POST', '/api/collections/123/documents'],
101+
['GET', '/api/collections/123/documents/456'],
102+
['PUT', '/api/collections/123/documents/456'],
103+
['POST', '/api/collections/123/documents/456'],
104+
['DELETE', '/api/collections/123/documents/456'],
105+
['GET', '/api/collections/123/documents/456/revisions'],
106+
['GET', '/api/collections/123/documents/456/revisions/find'],
107+
['POST', '/api/collections/123/documents/456/revisions'],
108+
['GET', '/api/collections/123/documents/456/revisions/789'],
109+
['DELETE', '/api/collections/123/documents/456/revisions/789'],
110+
['GET', '/files/user123-movies/2019/01/01/1.mp4'],
111+
['PUT', '/files/user123-movies/2019/01/01/1.mp4'],
112+
['GET', '/static/some/path/to/file.txt'],
113+
];

‎src/__tests__/Destination.spec.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {Destination} from '../router';
2+
3+
test('can create a static route', () => {
4+
const dest = Destination.from('GET /ping', 123);
5+
expect(dest.routes).toHaveLength(1);
6+
expect(dest.data).toBe(123);
7+
expect(dest.routes[0].toText()).toBe('GET /ping');
8+
});
9+
10+
describe('.from()', () => {
11+
const assertFrom = (def: string, expected: string = def) => {
12+
const dest = Destination.from(def, null);
13+
expect(dest.routes).toHaveLength(1);
14+
expect(dest.routes[0].toText()).toBe(expected);
15+
};
16+
17+
test('can create a static route', () => {
18+
assertFrom('POST /ping');
19+
});
20+
21+
test('can use regex for or statement', () => {
22+
assertFrom('{:(POST|GET)} /ping');
23+
});
24+
25+
test('can match any method', () => {
26+
assertFrom('{method:.+: } /echo');
27+
});
28+
29+
test('can specify category variations', () => {
30+
assertFrom(
31+
'GET /{:(collection|collections)}/{collectionId}/{:blocks?}/{blockId}',
32+
'GET /{:(collection|collections)}/{collectionId::/}/{:blocks?}/{blockId::/}',
33+
);
34+
});
35+
36+
test('can parse until next slash', () => {
37+
assertFrom('GET /posts/{postId}', 'GET /posts/{postId::/}');
38+
});
39+
40+
test('can parse until next dot', () => {
41+
assertFrom('GET /files/{filename}.txt', 'GET /files/{filename::.}.txt');
42+
});
43+
44+
test('can parse until next dot and next slash', () => {
45+
assertFrom('GET /files/{filename}.{extension}', 'GET /files/{filename::.}.{extension::/}');
46+
});
47+
48+
test('can wildcard the remainder of the path', () => {
49+
assertFrom('GET /static/{path:.+}');
50+
});
51+
52+
test('when parsing one step, matches until next slash', () => {
53+
assertFrom('GET /step/{path::}', 'GET /step/{path::/}');
54+
});
55+
56+
test('when parsing one step, matches until next slash', () => {
57+
assertFrom('GET /step/{path::}', 'GET /step/{path::/}');
58+
});
59+
60+
test('can make last parameter optional', () => {
61+
assertFrom('GET /users{user:(/[^/]+)?}');
62+
});
63+
});

0 commit comments

Comments
 (0)