rax-radix-tree 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,84 @@
1
+ const assert = require('assert');
2
+ const { parsePath } = require('../radix-tree');
3
+
4
+ describe('parsePath', function() {
5
+ it('should handle path without asterisk', function() {
6
+ const result = parsePath('/api/users');
7
+ assert.deepStrictEqual(result, {
8
+ treePath: '/api/users',
9
+ path: '/api/users',
10
+ matchType: 'equals',
11
+ pathPattern: undefined
12
+ });
13
+ });
14
+
15
+ it('should handle path with a single asterisk', function() {
16
+ const result = parsePath('/api/users/*');
17
+ assert.deepStrictEqual(result, {
18
+ treePath: '/api/users/',
19
+ path: '/api/users/*',
20
+ matchType: 'prefix',
21
+ pathPattern: '^\\/api\\/users\\/[^/]*$'
22
+ });
23
+ });
24
+
25
+ it('should handle path with two consecutive asterisks', function() {
26
+ const result = parsePath('/api/**');
27
+ assert.deepStrictEqual(result, {
28
+ treePath: '/api/',
29
+ path: '/api/**',
30
+ matchType: 'prefix',
31
+ pathPattern: '^\\/api\\/.*$'
32
+ });
33
+ });
34
+
35
+ it('should handle complex path with multiple asterisks', function() {
36
+ const result = parsePath('/api/*/users/**/profile');
37
+ assert.deepStrictEqual(result, {
38
+ treePath: '/api/',
39
+ path: '/api/*/users/**/profile',
40
+ matchType: 'prefix',
41
+ pathPattern: '^\\/api\\/[^/]*\\/users\\/.*\\/profile$'
42
+ });
43
+ });
44
+
45
+ it('should handle path starting with asterisk', function() {
46
+ const result = parsePath('**.jpg');
47
+ assert.deepStrictEqual(result, {
48
+ treePath: 'gpj.',
49
+ path: '**.jpg',
50
+ matchType: 'suffix',
51
+ pathPattern: '^gpj\\..*$'
52
+ });
53
+ });
54
+
55
+ it('should handle path with special characters', function() {
56
+ const result = parsePath('/api/users?id=*');
57
+ assert.deepStrictEqual(result, {
58
+ treePath: '/api/users?id=',
59
+ path: '/api/users?id=*',
60
+ matchType: 'prefix',
61
+ pathPattern: '^\\/api\\/users\\?id=[^/]*$'
62
+ });
63
+ });
64
+
65
+ it('should handle path with more special characters', function() {
66
+ const result = parsePath('/api/users+/[profile].{json}');
67
+ assert.deepStrictEqual(result, {
68
+ treePath: '/api/users+/[profile].{json}',
69
+ path: '/api/users+/[profile].{json}',
70
+ matchType: 'equals',
71
+ pathPattern: undefined,
72
+ });
73
+ });
74
+
75
+ it('should handle empty path', function() {
76
+ const result = parsePath('');
77
+ assert.deepStrictEqual(result, {
78
+ treePath: '',
79
+ path: '',
80
+ matchType: 'equals',
81
+ pathPattern: undefined
82
+ });
83
+ });
84
+ });
@@ -0,0 +1,217 @@
1
+ const assert = require('assert');
2
+ const { RadixTree } = require('../index');
3
+
4
+ describe('RadixTree', function() {
5
+ describe('Construction and Initialization', function() {
6
+ it('should correctly initialize RadixTree', function() {
7
+ const routes = [
8
+ { paths: ['/api/users'], methods: ['GET'], meta: { id: 1 } },
9
+ { paths: ['/api/users/*'], methods: ['POST'], meta: { id: 2 } },
10
+ ];
11
+ const tree = new RadixTree(routes);
12
+ assert(tree instanceof RadixTree);
13
+ });
14
+ });
15
+
16
+ describe('Route Matching', function() {
17
+ let tree;
18
+
19
+ beforeEach(function() {
20
+ const routes = [
21
+ { paths: ['/api/users'], meta: { id: 1 } },
22
+ { paths: ['/api/users'], meta: { id: 2 } },
23
+ { paths: ['/api/users/*'], meta: { id: 3 } },
24
+ { paths: ['/api/posts/**'], meta: { id: 4 } },
25
+ { paths: ['/api/comments'], meta: { id: 5 } },
26
+ { paths: ['/service/**', '/servicev2/**'], meta: { id: 6 } },
27
+ { paths: ['/service/user/*'], meta: { id: 7 } },
28
+ { paths: ['**.jpg', '*.png'], meta: { id: 8 } },
29
+ { paths: ['**/test.jpg'], meta: { id: 9 } },
30
+ { paths: ['/static/**'], meta: { id: 10 } },
31
+ ];
32
+ tree = new RadixTree(routes, null);
33
+ });
34
+
35
+ it('should match exact paths', function() {
36
+ const result = tree.findAllRoutes('/api/users');
37
+ assert.strictEqual(result.length, 2);
38
+ assert.deepStrictEqual(result.map(r => r.meta.id), [1, 2]);
39
+ });
40
+
41
+ it('should match paths with a single asterisk', function() {
42
+ const result = tree.findAllRoutes('/api/users/123');
43
+ assert.strictEqual(result.length, 1);
44
+ assert.strictEqual(result[0].meta.id, 3);
45
+ });
46
+
47
+ it('should match normal paths with double asterisks', function() {
48
+ const result = tree.findAllRoutes('/api/posts/2023');
49
+ assert.strictEqual(result.length, 1);
50
+ assert.strictEqual(result[0].meta.id, 4);
51
+ });
52
+
53
+ it('should match suffix .jpg file in nested directories', function() {
54
+ const result = tree.findAllRoutes('/2023/05/01.jpg');
55
+ assert.strictEqual(result.length, 1);
56
+ assert.strictEqual(result[0].meta.id, 8);
57
+ });
58
+
59
+ it('should match suffix .png file in root directory', function() {
60
+ const result = tree.findAllRoutes('01.png');
61
+ assert.strictEqual(result.length, 1);
62
+ assert.strictEqual(result[0].meta.id, 8);
63
+ });
64
+
65
+ it('should test high priority suffix matching functionality', function() {
66
+ const result = tree.findAllRoutes('/static/img/test.jpg');
67
+ assert.strictEqual(result.length, 3);
68
+ assert.deepStrictEqual(result.map(r => r.meta.id), [9, 8, 10]);
69
+ });
70
+
71
+ it('should match paths with double asterisks', function() {
72
+ const result = tree.findAllRoutes('/api/posts/2023/05/01');
73
+ assert.strictEqual(result.length, 1);
74
+ assert.strictEqual(result[0].meta.id, 4);
75
+ });
76
+
77
+ it('should not match non-existent paths', function() {
78
+ const result = tree.findAllRoutes('/api/nonexistent');
79
+ assert.strictEqual(result.length, 0);
80
+ });
81
+
82
+ it('should correctly handle prefix matching', function() {
83
+ const result = tree.findAllRoutes('/api/users/profile');
84
+ assert.strictEqual(result.length, 1);
85
+ assert.strictEqual(result[0].meta.id, 3);
86
+ });
87
+
88
+ it('should match normal paths with service/**', function() {
89
+ const result = tree.findAllRoutes('/service/user/123');
90
+ assert.strictEqual(result.length, 2);
91
+ assert.deepStrictEqual(result.map(r => r.meta.id), [7, 6]);
92
+ });
93
+
94
+ it('should match normal paths with servicev2/**', function() {
95
+ const result = tree.findAllRoutes('/servicev2/user/123');
96
+ assert.strictEqual(result.length, 1);
97
+ assert.deepStrictEqual(result.map(r => r.meta.id), [6]);
98
+ });
99
+
100
+ });
101
+
102
+ describe('Adding Any Route Configuration', function() {
103
+ let tree;
104
+ beforeEach(function() {
105
+ const routes = [
106
+ { paths: ['/**'], meta: { id: 10 } },
107
+ { paths: ['/api/users/*/posts'], meta: { id: 11 } },
108
+ { paths: ['/api/users/**/comments'], meta: { id: 12 } },
109
+ ];
110
+ tree = new RadixTree(routes, null);
111
+ });
112
+ it('should match any path', function() {
113
+ const result = tree.findAllRoutes('/users/123/posts');
114
+ assert.strictEqual(result.length, 1);
115
+ assert.strictEqual(result[0].meta.id, 10);
116
+ });
117
+ it('should match any path 2', function() {
118
+ const result = tree.findAllRoutes('/index');
119
+ assert.strictEqual(result.length, 1);
120
+ assert.strictEqual(result[0].meta.id, 10);
121
+ });
122
+ it('should match any path 3', function() {
123
+ const result = tree.findAllRoutes('/');
124
+ assert.strictEqual(result.length, 1);
125
+ assert.strictEqual(result[0].meta.id, 10);
126
+ });
127
+ });
128
+
129
+ describe('Method Matching', function() {
130
+ let tree;
131
+ beforeEach(function() {
132
+ const routes = [
133
+ { paths: ['/api/**'], meta: { id: 1 } },
134
+ { methods: ['POST'], paths: ['/**'], meta: { id: 2 } },
135
+ { methods: ['ALL'], paths: ['/api/users'], meta: { id: 5 } },
136
+ { methods: ['POST'], paths: ['/api/users'], meta: { id: 6} },
137
+ { methods: ['GET'], paths: ['/api/users'], meta: { id: 7} },
138
+ { methods: ['GET', 'POST'], paths: ['/api/users/**'], meta: { id: 8} },
139
+ ];
140
+ tree = new RadixTree(routes, null);
141
+ });
142
+
143
+ it('should match routes without specified method (treated as ALL)', function() {
144
+ const result = tree.findAllRoutes('/api/posts', 'GET');
145
+ assert.strictEqual(result.length, 1);
146
+ assert.strictEqual(result[0].meta.id, 1);
147
+ });
148
+
149
+ it('should match routes with specified method', function() {
150
+ const result = tree.findAllRoutes('/api/users', 'POST');
151
+ assert.strictEqual(result.length, 4);
152
+ assert.deepStrictEqual(result.map(r => r.meta.id), [6, 5, 1, 2]);
153
+ // assert.deepStrictEqual(result.map(r => r.meta.id).sort(), [5, 6, 8]);
154
+ });
155
+
156
+ it('should match routes with ALL method', function() {
157
+ const result = tree.findAllRoutes('/api/users', 'PUT');
158
+ assert.strictEqual(result.length, 2);
159
+ assert.deepStrictEqual(result.map(r => r.meta.id), [5, 1]);
160
+ });
161
+
162
+ it('should match multiple matching routes', function() {
163
+ const result = tree.findAllRoutes('/api/users/123', 'GET');
164
+ assert.strictEqual(result.length, 2);
165
+ assert.deepStrictEqual(result.map(r => r.meta.id), [8, 1]);
166
+ });
167
+
168
+ it('should not match routes with non-matching method', function() {
169
+ const result = tree.findAllRoutes('/api/users', 'DELETE');
170
+ assert.strictEqual(result.length, 2);
171
+ assert.deepStrictEqual(result.map(r => r.meta.id), [5, 1]);
172
+ });
173
+
174
+ it('should match global POST route', function() {
175
+ const result = tree.findAllRoutes('/random/path', 'POST');
176
+ assert.strictEqual(result.length, 1);
177
+ assert.strictEqual(result[0].meta.id, 2);
178
+ });
179
+ });
180
+
181
+ describe('Complex Route Matching', function() {
182
+ let tree;
183
+
184
+ beforeEach(function() {
185
+ const routes = [
186
+ { paths: ['/api/items'], meta: { id: 1 } },
187
+ { paths: ['/api/items/*'], meta: { id: 2 } },
188
+ { paths: ['/api/items/**'], meta: { id: 3 } },
189
+
190
+ { paths: ['/api/users/**'], meta: { id: 10 } },
191
+ { paths: ['/api/users/*/posts'], meta: { id: 11 } },
192
+ { paths: ['/api/users/**/comments'], meta: { id: 12 } },
193
+ ];
194
+ tree = new RadixTree(routes, null);
195
+ });
196
+
197
+ it('should correctly distinguish between single asterisk and double asterisk matching', function() {
198
+ let result = tree.findAllRoutes('/api/items/123');
199
+ assert.strictEqual(result.length, 2);
200
+ assert.deepStrictEqual(result.map(r => r.meta.id).sort(), [2, 3]);
201
+
202
+ result = tree.findAllRoutes('/api/items/123/456');
203
+ assert.strictEqual(result.length, 1);
204
+ assert.strictEqual(result[0].meta.id, 3);
205
+ });
206
+
207
+ it('should correctly handle complex route patterns', function() {
208
+ let result = tree.findAllRoutes('/api/users/123/posts');
209
+ assert.strictEqual(result.length, 2);
210
+ assert.deepStrictEqual(result.map(r => r.meta.id), [11, 10]);
211
+
212
+ result = tree.findAllRoutes('/api/users/123/profile/comments');
213
+ assert.strictEqual(result.length, 2);
214
+ assert.deepStrictEqual(result.map(r => r.meta.id), [12, 10]);
215
+ });
216
+ });
217
+ });
@@ -0,0 +1,316 @@
1
+ const assert = require('assert');
2
+ const { HttpRadixTree } = require('../http-radix-tree');
3
+
4
+ describe('HttpRadixTree', function () {
5
+
6
+ describe('Http Route Matching By Host', function () {
7
+ let tree;
8
+
9
+ beforeEach(function () {
10
+ const routes = [
11
+ { paths: ['/api/**'], meta: { id: 1 }, hosts: ['test.com'] },
12
+ { paths: ['/api/users/**'], meta: { id: 3 }, hosts: ['a.com', 'b.com'] },
13
+ { paths: ['/api/users/**'], meta: { id: 4 }, hosts: ['*.c.com'] },
14
+ { paths: ['/api/users/**'], meta: { id: 10 }, remoteAddrs: ['127.0.0.1', '192.168.1.3'] },
15
+ { paths: ['/api/users/**'], meta: { id: 11 }, remoteAddrs: ['192.168.1.1/24'] },
16
+ { paths: ['/api/users/**'], meta: { id: 12 }, remoteAddrs: ['192.168.1.1/16'] },
17
+ { paths: ['/api/users/**'], meta: { id: 20 }, exprs: ['age >= 10 and x_id mod 20 == 0'] },
18
+ { paths: ['/api/users/**'], meta: { id: 21 }, exprs: ['name in ("tom", "lily")'] },
19
+ ];
20
+ tree = new HttpRadixTree(routes);
21
+ });
22
+
23
+ it('path not found', function () {
24
+ const result = tree.findRoute({
25
+ path: '/notfound',
26
+ method: 'GET',
27
+ headers: {
28
+ host: 'a.com',
29
+ },
30
+ });
31
+ assert.strictEqual(result, null);
32
+ });
33
+
34
+ it('path matched, but host not matched', function () {
35
+ const result = tree.findRoute({
36
+ path: '/api/users/1',
37
+ method: 'GET',
38
+ headers: {
39
+ host: 'x.com',
40
+ },
41
+ });
42
+ assert.strictEqual(result, null);
43
+ });
44
+
45
+ it('path matched, host matched with a.com', function () {
46
+ const result = tree.findRoute({
47
+ path: '/api/users/1',
48
+ method: 'GET',
49
+ headers: {
50
+ host: 'a.com',
51
+ },
52
+ });
53
+ assert.notStrictEqual(result, null);
54
+ assert.strictEqual(result.meta.id, 3);
55
+ });
56
+
57
+ it('path matched, host matched with b.com', function () {
58
+ const result = tree.findRoute({
59
+ path: '/api/users/1',
60
+ method: 'GET',
61
+ headers: {
62
+ host: 'b.com',
63
+ },
64
+ });
65
+ assert.notStrictEqual(result, null);
66
+ assert.strictEqual(result.meta.id, 3);
67
+ });
68
+
69
+ it('path matched, host matched with c.com', function () {
70
+ const result = tree.findRoute({
71
+ path: '/api/users/1',
72
+ method: 'GET',
73
+ headers: {
74
+ host: 'c.com',
75
+ },
76
+ });
77
+ assert.notStrictEqual(result, null);
78
+ assert.strictEqual(result.meta.id, 4);
79
+ });
80
+
81
+ it('path matched, host matched with www.c.com', function () {
82
+ const result = tree.findRoute({
83
+ path: '/api/users/1',
84
+ method: 'GET',
85
+ headers: {
86
+ host: 'www.c.com',
87
+ },
88
+ });
89
+ assert.notStrictEqual(result, null);
90
+ assert.strictEqual(result.meta.id, 4);
91
+ });
92
+
93
+ it('path matched, remoteAddr matched with 127.0.0.1', function () {
94
+ const result = tree.findRoute({
95
+ path: '/api/users/1',
96
+ method: 'GET',
97
+ remoteAddr: '127.0.0.1',
98
+ });
99
+ assert.notStrictEqual(result, null);
100
+ assert.strictEqual(result.meta.id, 10);
101
+ });
102
+
103
+ it('path matched, remoteAddr matched with 192.168.1.3', function () {
104
+ const result = tree.findRoute({
105
+ path: '/api/users/1',
106
+ method: 'GET',
107
+ remoteAddr: '192.168.1.3',
108
+ });
109
+ assert.notStrictEqual(result, null);
110
+ assert.strictEqual(result.meta.id, 10);
111
+ });
112
+
113
+ it('path matched, remoteAddr matched with IP in 192.168.1.0/24 subnet', function () {
114
+ const result = tree.findRoute({
115
+ path: '/api/users/1',
116
+ method: 'GET',
117
+ remoteAddr: '192.168.1.100',
118
+ });
119
+ assert.notStrictEqual(result, null);
120
+ assert.strictEqual(result.meta.id, 11);
121
+ });
122
+
123
+ it('path matched, host matched with IP in 192.168.0.0/16 subnet', function () {
124
+ const result = tree.findRoute({
125
+ path: '/api/users/1',
126
+ method: 'GET',
127
+ remoteAddr: '192.168.2.1',
128
+ });
129
+ assert.notStrictEqual(result, null);
130
+ assert.strictEqual(result.meta.id, 12);
131
+ });
132
+
133
+ it('path matched, but remoteAddr not matched', function () {
134
+ const result = tree.findRoute({
135
+ path: '/api/users/1',
136
+ method: 'GET',
137
+ remoteAddr: '10.0.0.1',
138
+ });
139
+ assert.strictEqual(result, null);
140
+ });
141
+
142
+
143
+ it('should match expression condition: age >= 10 and x_id divisible by 20', function () {
144
+ const result = tree.findRoute({
145
+ path: '/api/users/profile',
146
+ method: 'GET',
147
+ args: { age: '25' },
148
+ headers: { 'x-id': '40' },
149
+ });
150
+ assert.notStrictEqual(result, null);
151
+ assert.strictEqual(result.meta.id, 20);
152
+ });
153
+
154
+ it('should not match: empty variables', function () {
155
+ const result = tree.findRoute({
156
+ path: '/api/users/profile',
157
+ method: 'GET',
158
+ args: {},
159
+ headers: {},
160
+ });
161
+ assert.strictEqual(result, null);
162
+ });
163
+
164
+ it('should not match: age < 10', function () {
165
+ const result = tree.findRoute({
166
+ path: '/api/users/profile',
167
+ method: 'GET',
168
+ args: { age: '9' },
169
+ headers: { 'x-id': '40' },
170
+ });
171
+ assert.strictEqual(result, null);
172
+ });
173
+
174
+ it('should not match: x_id not divisible by 20', function () {
175
+ const result = tree.findRoute({
176
+ path: '/api/users/profile',
177
+ method: 'GET',
178
+ args: { age: '25' },
179
+ headers: { 'x-id': '41' },
180
+ });
181
+ assert.strictEqual(result, null);
182
+ });
183
+
184
+ it('should match edge case: age = 10 and x_id = 0', function () {
185
+ const result = tree.findRoute({
186
+ path: '/api/users/profile',
187
+ method: 'GET',
188
+ args: { age: '10' },
189
+ headers: { 'x-id': '0' },
190
+ });
191
+ assert.notStrictEqual(result, null);
192
+ assert.strictEqual(result.meta.id, 20);
193
+ });
194
+
195
+ it('should match large numbers: both age and x_id are large', function () {
196
+ const result = tree.findRoute({
197
+ path: '/api/users/profile',
198
+ method: 'GET',
199
+ args: { age: '100' },
200
+ headers: { 'x-id': '1000' },
201
+ });
202
+ assert.notStrictEqual(result, null);
203
+ assert.strictEqual(result.meta.id, 20);
204
+ });
205
+
206
+ it('should match: name is tom', function () {
207
+ const result = tree.findRoute({
208
+ path: '/api/users/profile',
209
+ method: 'GET',
210
+ args: { name: 'tom' },
211
+ });
212
+ assert.notStrictEqual(result, null);
213
+ assert.strictEqual(result.meta.id, 21);
214
+ });
215
+
216
+ it('should match: name is lily', function () {
217
+ const result = tree.findRoute({
218
+ path: '/api/users/profile',
219
+ method: 'GET',
220
+ args: { name: 'lily' },
221
+ });
222
+ assert.notStrictEqual(result, null);
223
+ assert.strictEqual(result.meta.id, 21);
224
+ });
225
+
226
+ it('should not match: name is other value', function () {
227
+ const result = tree.findRoute({
228
+ path: '/api/users/profile',
229
+ method: 'GET',
230
+ args: { name: 'john' },
231
+ });
232
+ assert.strictEqual(result, null);
233
+ });
234
+
235
+ it('should not match: name parameter missing', function () {
236
+ const result = tree.findRoute({
237
+ path: '/api/users/profile',
238
+ method: 'GET',
239
+ args: {},
240
+ });
241
+ assert.strictEqual(result, null);
242
+ });
243
+
244
+ it('should match: name is tom (case sensitive)', function () {
245
+ const result = tree.findRoute({
246
+ path: '/api/users/profile',
247
+ method: 'GET',
248
+ args: { name: 'TOM' },
249
+ });
250
+ assert.strictEqual(result, null);
251
+ });
252
+ });
253
+
254
+ describe('Http Route Matching By Host Extension', function () {
255
+ let tree;
256
+
257
+ beforeEach(function () {
258
+ const routes = [
259
+ { paths: ['/**'], meta: { id: 30 }, hosts: ['example.com']},
260
+ { paths: ['/**'], meta: { id: 31 }, hosts: ['*.example.com']},
261
+ { paths: ['/**'], meta: { id: 32 }, hosts: ['api.example.com']}
262
+
263
+ ];
264
+ tree = new HttpRadixTree(routes);
265
+ });
266
+
267
+ it('should match exact host example.com', function () {
268
+ const result = tree.findRoute({
269
+ path: '/',
270
+ method: 'GET',
271
+ headers: { host: 'example.com' },
272
+ });
273
+ assert.notStrictEqual(result, null);
274
+ assert.strictEqual(result.meta.id, 30);
275
+ });
276
+
277
+ it('should match wildcard subdomain of example.com', function () {
278
+ const result = tree.findRoute({
279
+ path: '/about',
280
+ method: 'GET',
281
+ headers: { host: 'blog.example.com' },
282
+ });
283
+ assert.notStrictEqual(result, null);
284
+ assert.strictEqual(result.meta.id, 31);
285
+ });
286
+
287
+ it('should match specific subdomain api.example.com', function () {
288
+ const result = tree.findRoute({
289
+ path: '/users',
290
+ method: 'GET',
291
+ headers: { host: 'api.example.com' },
292
+ });
293
+ assert.notStrictEqual(result, null);
294
+ assert.strictEqual(result.meta.id, 32);
295
+ });
296
+
297
+ it('should not match unrelated domain', function () {
298
+ const result = tree.findRoute({
299
+ path: '/',
300
+ method: 'GET',
301
+ headers: { host: 'unrelated.com' },
302
+ });
303
+ assert.strictEqual(result, null);
304
+ });
305
+
306
+ it('should match example.com with longer path', function () {
307
+ const result = tree.findRoute({
308
+ path: '/some/long/path',
309
+ method: 'GET',
310
+ headers: { host: 'example.com' },
311
+ });
312
+ assert.notStrictEqual(result, null);
313
+ assert.strictEqual(result.meta.id, 30);
314
+ });
315
+ });
316
+ });