@zentjs/zentjs 0.0.2
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.
- package/LICENSE +28 -0
- package/README.md +1584 -0
- package/package.json +85 -0
- package/src/core/application.mjs +1039 -0
- package/src/core/context.mjs +33 -0
- package/src/errors/error-handler.mjs +88 -0
- package/src/errors/http-error.mjs +86 -0
- package/src/hooks/lifecycle.mjs +175 -0
- package/src/http/request.mjs +166 -0
- package/src/http/response.mjs +151 -0
- package/src/index.mjs +33 -0
- package/src/middleware/pipeline.mjs +77 -0
- package/src/plugins/body-parser.mjs +165 -0
- package/src/plugins/cors.mjs +183 -0
- package/src/plugins/manager.mjs +112 -0
- package/src/plugins/request-metrics.mjs +74 -0
- package/src/router/index.mjs +198 -0
- package/src/router/node.mjs +72 -0
- package/src/router/radix-tree.mjs +313 -0
- package/src/utils/http-status.mjs +31 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { RadixTree } from './radix-tree.mjs';
|
|
2
|
+
|
|
3
|
+
const HTTP_METHODS = [
|
|
4
|
+
'GET',
|
|
5
|
+
'POST',
|
|
6
|
+
'PUT',
|
|
7
|
+
'PATCH',
|
|
8
|
+
'DELETE',
|
|
9
|
+
'HEAD',
|
|
10
|
+
'OPTIONS',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Router público.
|
|
15
|
+
* Responsabilidade única: API de conveniência para registro e busca de rotas.
|
|
16
|
+
* Delega toda a lógica de armazenamento/lookup para a RadixTree.
|
|
17
|
+
*/
|
|
18
|
+
export class Router {
|
|
19
|
+
/** @type {RadixTree} */
|
|
20
|
+
#tree;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {object} [opts]
|
|
24
|
+
* @param {boolean} [opts.ignoreTrailingSlash=true]
|
|
25
|
+
* @param {boolean} [opts.caseSensitive=false]
|
|
26
|
+
*/
|
|
27
|
+
constructor(opts = {}) {
|
|
28
|
+
this.#tree = new RadixTree(opts);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Registra uma rota completa.
|
|
33
|
+
* @param {object} definition
|
|
34
|
+
* @param {string} definition.method - HTTP method
|
|
35
|
+
* @param {string} definition.path
|
|
36
|
+
* @param {Function} definition.handler
|
|
37
|
+
* @param {Function[]} [definition.middlewares]
|
|
38
|
+
* @param {object} [definition.hooks]
|
|
39
|
+
*/
|
|
40
|
+
route({ method, path, handler, middlewares = [], hooks = {}, ...meta }) {
|
|
41
|
+
this.#tree.add(method.toUpperCase(), path, {
|
|
42
|
+
handler,
|
|
43
|
+
middlewares,
|
|
44
|
+
hooks,
|
|
45
|
+
...meta,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Busca rota pelo método e path.
|
|
51
|
+
* @param {string} method
|
|
52
|
+
* @param {string} path
|
|
53
|
+
* @returns {{ route: object, params: Record<string, string> }}
|
|
54
|
+
*/
|
|
55
|
+
find(method, path) {
|
|
56
|
+
return this.#tree.find(method, path);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Registra uma rota para todos os métodos HTTP.
|
|
61
|
+
* @param {string} path
|
|
62
|
+
* @param {Function} handler
|
|
63
|
+
* @param {object} [opts]
|
|
64
|
+
*/
|
|
65
|
+
all(path, handler, opts = {}) {
|
|
66
|
+
for (const method of HTTP_METHODS) {
|
|
67
|
+
this.route({ method, path, handler, ...opts });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Agrupa rotas sob um prefixo com middlewares/hooks compartilhados.
|
|
73
|
+
* @param {string} prefix - Prefixo do grupo (ex: '/api/v1')
|
|
74
|
+
* @param {object} [opts] - Opções do grupo
|
|
75
|
+
* @param {Function[]} [opts.middlewares] - Middlewares do grupo
|
|
76
|
+
* @param {object} [opts.hooks] - Hooks do grupo
|
|
77
|
+
* @param {Function} callback - Recebe um RouteGroup para registrar rotas
|
|
78
|
+
*/
|
|
79
|
+
group(prefix, ...args) {
|
|
80
|
+
const opts = typeof args[0] === 'function' ? {} : args.shift() || {};
|
|
81
|
+
const callback = args[0];
|
|
82
|
+
|
|
83
|
+
const routeGroup = new RouteGroup(this, prefix, opts);
|
|
84
|
+
callback(routeGroup);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Proxy leve para registro de rotas dentro de um grupo.
|
|
90
|
+
* Responsabilidade única: prefixar paths e mesclar middlewares/hooks do grupo.
|
|
91
|
+
*/
|
|
92
|
+
class RouteGroup {
|
|
93
|
+
/** @type {Router} */
|
|
94
|
+
#router;
|
|
95
|
+
|
|
96
|
+
/** @type {string} */
|
|
97
|
+
#prefix;
|
|
98
|
+
|
|
99
|
+
/** @type {Function[]} */
|
|
100
|
+
#middlewares;
|
|
101
|
+
|
|
102
|
+
/** @type {object} */
|
|
103
|
+
#hooks;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {Router} router
|
|
107
|
+
* @param {string} prefix
|
|
108
|
+
* @param {object} opts
|
|
109
|
+
*/
|
|
110
|
+
constructor(router, prefix, opts = {}) {
|
|
111
|
+
this.#router = router;
|
|
112
|
+
this.#prefix = prefix.replace(/\/+$/, '');
|
|
113
|
+
this.#middlewares = opts.middlewares || [];
|
|
114
|
+
this.#hooks = opts.hooks || {};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Registra rota no grupo, mesclando prefixo + middlewares + hooks.
|
|
119
|
+
* @param {object} definition
|
|
120
|
+
*/
|
|
121
|
+
route({ method, path, handler, middlewares = [], hooks = {} }) {
|
|
122
|
+
this.#router.route({
|
|
123
|
+
method,
|
|
124
|
+
path: this.#prefix + (path === '/' ? '' : path),
|
|
125
|
+
handler,
|
|
126
|
+
middlewares: [...this.#middlewares, ...middlewares],
|
|
127
|
+
hooks: this.#mergeHooks(this.#hooks, hooks),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Registra rota para todos os métodos HTTP.
|
|
133
|
+
* @param {string} path
|
|
134
|
+
* @param {Function} handler
|
|
135
|
+
* @param {object} [opts]
|
|
136
|
+
*/
|
|
137
|
+
all(path, handler, opts = {}) {
|
|
138
|
+
for (const method of HTTP_METHODS) {
|
|
139
|
+
this.route({ method, path, handler, ...opts });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Sub-grupo dentro do grupo atual.
|
|
145
|
+
* @param {string} prefix
|
|
146
|
+
* @param {...any} args
|
|
147
|
+
*/
|
|
148
|
+
group(prefix, ...args) {
|
|
149
|
+
const opts = typeof args[0] === 'function' ? {} : args.shift() || {};
|
|
150
|
+
const callback = args[0];
|
|
151
|
+
|
|
152
|
+
const fullPrefix = this.#prefix + prefix.replace(/\/+$/, '');
|
|
153
|
+
const mergedOpts = {
|
|
154
|
+
middlewares: [...this.#middlewares, ...(opts.middlewares || [])],
|
|
155
|
+
hooks: this.#mergeHooks(this.#hooks, opts.hooks || {}),
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const subGroup = new RouteGroup(this.#router, fullPrefix, mergedOpts);
|
|
159
|
+
callback(subGroup);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Mescla hooks de dois escopos. Hooks do filho são executados após os do pai.
|
|
164
|
+
* @param {object} parent
|
|
165
|
+
* @param {object} child
|
|
166
|
+
* @returns {object}
|
|
167
|
+
*/
|
|
168
|
+
#mergeHooks(parent, child) {
|
|
169
|
+
const merged = { ...parent };
|
|
170
|
+
for (const [key, fns] of Object.entries(child)) {
|
|
171
|
+
merged[key] = [
|
|
172
|
+
...(merged[key] || []),
|
|
173
|
+
...(Array.isArray(fns) ? fns : [fns]),
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
return merged;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Gera métodos de conveniência: router.get(), router.post(), etc.
|
|
181
|
+
for (const method of HTTP_METHODS) {
|
|
182
|
+
/**
|
|
183
|
+
* @param {string} path
|
|
184
|
+
* @param {Function} handler
|
|
185
|
+
* @param {object} [opts] - { middlewares?, hooks? }
|
|
186
|
+
*/
|
|
187
|
+
Router.prototype[method.toLowerCase()] = function (path, handler, opts = {}) {
|
|
188
|
+
this.route({ method, path, handler, ...opts });
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
RouteGroup.prototype[method.toLowerCase()] = function (
|
|
192
|
+
path,
|
|
193
|
+
handler,
|
|
194
|
+
opts = {}
|
|
195
|
+
) {
|
|
196
|
+
this.route({ method, path, handler, ...opts });
|
|
197
|
+
};
|
|
198
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nó da Radix Tree.
|
|
3
|
+
* Responsabilidade única: armazenar um fragmento do path e seus filhos/handlers.
|
|
4
|
+
*/
|
|
5
|
+
export class Node {
|
|
6
|
+
/** @type {string} Fragmento do path */
|
|
7
|
+
prefix;
|
|
8
|
+
|
|
9
|
+
/** @type {Map<string, Node>} Filhos indexados pelo primeiro char */
|
|
10
|
+
children;
|
|
11
|
+
|
|
12
|
+
/** @type {Node | null} Filho de parâmetro (:param) */
|
|
13
|
+
paramChild;
|
|
14
|
+
|
|
15
|
+
/** @type {string | null} Nome do parâmetro (ex: 'id') */
|
|
16
|
+
paramName;
|
|
17
|
+
|
|
18
|
+
/** @type {Node | null} Filho wildcard (*) */
|
|
19
|
+
wildcardChild;
|
|
20
|
+
|
|
21
|
+
/** @type {string | null} Nome do wildcard */
|
|
22
|
+
wildcardName;
|
|
23
|
+
|
|
24
|
+
/** @type {Map<string, object>} Handlers por método HTTP */
|
|
25
|
+
handlers;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} [prefix='']
|
|
29
|
+
*/
|
|
30
|
+
constructor(prefix = '') {
|
|
31
|
+
this.prefix = prefix;
|
|
32
|
+
this.children = new Map();
|
|
33
|
+
this.paramChild = null;
|
|
34
|
+
this.paramName = null;
|
|
35
|
+
this.wildcardChild = null;
|
|
36
|
+
this.wildcardName = null;
|
|
37
|
+
this.handlers = new Map();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Adiciona um handler para um método HTTP.
|
|
42
|
+
* @param {string} method
|
|
43
|
+
* @param {object} route - { handler, hooks, middlewares }
|
|
44
|
+
*/
|
|
45
|
+
addHandler(method, route) {
|
|
46
|
+
this.handlers.set(method, route);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Busca o handler para um método HTTP.
|
|
51
|
+
* @param {string} method
|
|
52
|
+
* @returns {object | undefined}
|
|
53
|
+
*/
|
|
54
|
+
getHandler(method) {
|
|
55
|
+
return this.handlers.get(method);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @returns {boolean} Nó possui pelo menos um handler registrado
|
|
60
|
+
*/
|
|
61
|
+
get hasHandlers() {
|
|
62
|
+
return this.handlers.size > 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Retorna os métodos HTTP registrados neste nó.
|
|
67
|
+
* @returns {string[]}
|
|
68
|
+
*/
|
|
69
|
+
get allowedMethods() {
|
|
70
|
+
return [...this.handlers.keys()];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { MethodNotAllowedError, NotFoundError } from '../errors/http-error.mjs';
|
|
2
|
+
import { Node } from './node.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Radix Tree (Patricia Trie) para roteamento HTTP.
|
|
6
|
+
* Responsabilidade única: inserir e buscar rotas por path.
|
|
7
|
+
* Complexidade de lookup: O(k) onde k = comprimento do path.
|
|
8
|
+
*/
|
|
9
|
+
export class RadixTree {
|
|
10
|
+
/** @type {Node} */
|
|
11
|
+
#root;
|
|
12
|
+
|
|
13
|
+
/** @type {boolean} */
|
|
14
|
+
#ignoreTrailingSlash;
|
|
15
|
+
|
|
16
|
+
/** @type {boolean} */
|
|
17
|
+
#caseSensitive;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {object} [opts]
|
|
21
|
+
* @param {boolean} [opts.ignoreTrailingSlash=true]
|
|
22
|
+
* @param {boolean} [opts.caseSensitive=false]
|
|
23
|
+
*/
|
|
24
|
+
constructor(opts = {}) {
|
|
25
|
+
this.#root = new Node();
|
|
26
|
+
this.#ignoreTrailingSlash = opts.ignoreTrailingSlash ?? true;
|
|
27
|
+
this.#caseSensitive = opts.caseSensitive ?? false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Concatena segmentos a partir de um índice sem criar slice intermediário.
|
|
32
|
+
* @param {string[]} segments
|
|
33
|
+
* @param {number} startIndex
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
#joinSegmentsFrom(segments, startIndex) {
|
|
37
|
+
if (startIndex >= segments.length) return '';
|
|
38
|
+
|
|
39
|
+
let result = segments[startIndex];
|
|
40
|
+
for (let i = startIndex + 1; i < segments.length; i++) {
|
|
41
|
+
result += `/${segments[i]}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Registra uma rota na árvore.
|
|
49
|
+
* @param {string} method - HTTP method (GET, POST, etc.)
|
|
50
|
+
* @param {string} path - Route path (ex: /users/:id/posts)
|
|
51
|
+
* @param {object} route - { handler, hooks?, middlewares? }
|
|
52
|
+
*/
|
|
53
|
+
add(method, path, route) {
|
|
54
|
+
const segments = this.#splitPath(this.#normalizePath(path));
|
|
55
|
+
let current = this.#root;
|
|
56
|
+
|
|
57
|
+
for (const segment of segments) {
|
|
58
|
+
if (segment.startsWith(':')) {
|
|
59
|
+
// Segmento de parâmetro
|
|
60
|
+
const paramName = segment.slice(1);
|
|
61
|
+
|
|
62
|
+
if (!current.paramChild) {
|
|
63
|
+
current.paramChild = new Node(segment);
|
|
64
|
+
current.paramChild.paramName = paramName;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
current = current.paramChild;
|
|
68
|
+
} else if (segment.startsWith('*')) {
|
|
69
|
+
// Segmento wildcard
|
|
70
|
+
const wildcardName = segment.slice(1) || 'wildcard';
|
|
71
|
+
|
|
72
|
+
if (!current.wildcardChild) {
|
|
73
|
+
current.wildcardChild = new Node(segment);
|
|
74
|
+
current.wildcardChild.wildcardName = wildcardName;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
current = current.wildcardChild;
|
|
78
|
+
break; // Wildcard consome o resto do path
|
|
79
|
+
} else {
|
|
80
|
+
// Segmento estático
|
|
81
|
+
current = this.#insertStatic(current, segment);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
current.addHandler(method, route);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Busca uma rota na árvore.
|
|
90
|
+
* @param {string} method - HTTP method
|
|
91
|
+
* @param {string} path - Request path
|
|
92
|
+
* @returns {{ route: object, params: Record<string, string> }}
|
|
93
|
+
* @throws {NotFoundError} Rota não encontrada
|
|
94
|
+
* @throws {MethodNotAllowedError} Método não permitido
|
|
95
|
+
*/
|
|
96
|
+
find(method, path) {
|
|
97
|
+
const normalizedPath = this.#normalizePath(path);
|
|
98
|
+
const segments = this.#splitPath(normalizedPath);
|
|
99
|
+
const params = {};
|
|
100
|
+
|
|
101
|
+
const node = this.#search(this.#root, segments, 0, params);
|
|
102
|
+
|
|
103
|
+
if (!node) {
|
|
104
|
+
throw new NotFoundError(`Route not found: ${method} ${path}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const route = node.getHandler(method);
|
|
108
|
+
|
|
109
|
+
if (!route) {
|
|
110
|
+
const error = new MethodNotAllowedError(
|
|
111
|
+
`Method ${method} not allowed for ${path}`
|
|
112
|
+
);
|
|
113
|
+
error.allowedMethods = node.allowedMethods;
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { route, params };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Busca recursiva na árvore.
|
|
122
|
+
* @param {Node} node
|
|
123
|
+
* @param {string[]} segments
|
|
124
|
+
* @param {number} index
|
|
125
|
+
* @param {Record<string, string>} params
|
|
126
|
+
* @returns {Node | null}
|
|
127
|
+
*/
|
|
128
|
+
#search(node, segments, index, params) {
|
|
129
|
+
// Todos os segmentos consumidos — retorna o nó se tem handlers
|
|
130
|
+
if (index === segments.length) {
|
|
131
|
+
return node.hasHandlers ? node : null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const segment = segments[index];
|
|
135
|
+
|
|
136
|
+
// 1. Tenta match estático (prioridade mais alta)
|
|
137
|
+
const staticChild = this.#findStaticChild(node, segment);
|
|
138
|
+
if (staticChild) {
|
|
139
|
+
const result = this.#search(staticChild, segments, index + 1, params);
|
|
140
|
+
if (result) return result;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 2. Tenta match por parâmetro
|
|
144
|
+
if (node.paramChild) {
|
|
145
|
+
params[node.paramChild.paramName] = segment;
|
|
146
|
+
const result = this.#search(node.paramChild, segments, index + 1, params);
|
|
147
|
+
if (result) return result;
|
|
148
|
+
delete params[node.paramChild.paramName]; // Backtrack
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 3. Tenta match wildcard (prioridade mais baixa)
|
|
152
|
+
if (node.wildcardChild) {
|
|
153
|
+
params[node.wildcardChild.wildcardName] = this.#joinSegmentsFrom(
|
|
154
|
+
segments,
|
|
155
|
+
index
|
|
156
|
+
);
|
|
157
|
+
return node.wildcardChild;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Insere um segmento estático na árvore, dividindo nós quando necessário.
|
|
165
|
+
* @param {Node} parent
|
|
166
|
+
* @param {string} segment
|
|
167
|
+
* @returns {Node}
|
|
168
|
+
*/
|
|
169
|
+
#insertStatic(parent, segment) {
|
|
170
|
+
const key = this.#segmentKey(segment);
|
|
171
|
+
const existing = parent.children.get(key);
|
|
172
|
+
|
|
173
|
+
if (!existing) {
|
|
174
|
+
const child = new Node(segment);
|
|
175
|
+
parent.children.set(key, child);
|
|
176
|
+
return child;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const existingPrefix = this.#caseSensitive
|
|
180
|
+
? existing.prefix
|
|
181
|
+
: existing.prefix.toLowerCase();
|
|
182
|
+
const newSegment = this.#caseSensitive ? segment : segment.toLowerCase();
|
|
183
|
+
|
|
184
|
+
// Calcula o prefixo comum
|
|
185
|
+
const commonLen = this.#commonPrefixLength(existingPrefix, newSegment);
|
|
186
|
+
|
|
187
|
+
// Prefixo idêntico — avança para o nó existente
|
|
188
|
+
if (commonLen === existing.prefix.length && commonLen === segment.length) {
|
|
189
|
+
return existing;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// O segmento existente é um prefixo do novo — continua na subárvore
|
|
193
|
+
if (commonLen === existing.prefix.length) {
|
|
194
|
+
const remainder = segment.slice(commonLen);
|
|
195
|
+
return this.#insertStatic(existing, remainder);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Split: dividir o nó existente
|
|
199
|
+
const splitNode = new Node(existing.prefix.slice(0, commonLen));
|
|
200
|
+
|
|
201
|
+
// O antigo nó vira filho do split
|
|
202
|
+
existing.prefix = existing.prefix.slice(commonLen);
|
|
203
|
+
const existingNewKey = this.#segmentKey(existing.prefix);
|
|
204
|
+
splitNode.children.set(existingNewKey, existing);
|
|
205
|
+
|
|
206
|
+
// Atualiza o parent para apontar pro split
|
|
207
|
+
parent.children.set(key, splitNode);
|
|
208
|
+
|
|
209
|
+
// Se o segmento novo é exatamente o prefixo comum, o splitNode é o destino
|
|
210
|
+
if (commonLen === segment.length) {
|
|
211
|
+
return splitNode;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Cria novo nó para o restante do segmento
|
|
215
|
+
const remainder = segment.slice(commonLen);
|
|
216
|
+
const newChild = new Node(remainder);
|
|
217
|
+
const newKey = this.#segmentKey(remainder);
|
|
218
|
+
splitNode.children.set(newKey, newChild);
|
|
219
|
+
|
|
220
|
+
return newChild;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Busca um filho estático que corresponda ao segmento.
|
|
225
|
+
* @param {Node} node
|
|
226
|
+
* @param {string} segment
|
|
227
|
+
* @returns {Node | null}
|
|
228
|
+
*/
|
|
229
|
+
#findStaticChild(node, segment) {
|
|
230
|
+
const key = this.#segmentKey(segment);
|
|
231
|
+
const child = node.children.get(key);
|
|
232
|
+
|
|
233
|
+
if (!child) return null;
|
|
234
|
+
|
|
235
|
+
const childPrefix = this.#caseSensitive
|
|
236
|
+
? child.prefix
|
|
237
|
+
: child.prefix.toLowerCase();
|
|
238
|
+
const target = this.#caseSensitive ? segment : segment.toLowerCase();
|
|
239
|
+
|
|
240
|
+
if (target === childPrefix) {
|
|
241
|
+
return child;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// O prefix do filho é início do segmento — continua descendo
|
|
245
|
+
if (target.startsWith(childPrefix)) {
|
|
246
|
+
const remainder = segment.slice(child.prefix.length);
|
|
247
|
+
return this.#findStaticChild(child, remainder);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Calcula a chave de indexação para um segmento.
|
|
255
|
+
* @param {string} segment
|
|
256
|
+
* @returns {string}
|
|
257
|
+
*/
|
|
258
|
+
#segmentKey(segment) {
|
|
259
|
+
if (segment === '') return '';
|
|
260
|
+
return this.#caseSensitive ? segment[0] : segment[0].toLowerCase();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Calcula o comprimento do prefixo comum entre duas strings.
|
|
265
|
+
* @param {string} a
|
|
266
|
+
* @param {string} b
|
|
267
|
+
* @returns {number}
|
|
268
|
+
*/
|
|
269
|
+
#commonPrefixLength(a, b) {
|
|
270
|
+
const len = Math.min(a.length, b.length);
|
|
271
|
+
let i = 0;
|
|
272
|
+
while (i < len && a[i] === b[i]) i++;
|
|
273
|
+
return i;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Normaliza o path — remove trailing slash, garante leading slash.
|
|
278
|
+
* @param {string} path
|
|
279
|
+
* @returns {string}
|
|
280
|
+
*/
|
|
281
|
+
#normalizePath(path) {
|
|
282
|
+
if (!path || path === '/') return '/';
|
|
283
|
+
|
|
284
|
+
let normalized = path.startsWith('/') ? path : '/' + path;
|
|
285
|
+
|
|
286
|
+
if (this.#ignoreTrailingSlash && normalized.length > 1) {
|
|
287
|
+
normalized = normalized.replace(/\/+$/, '');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return normalized;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Divide o path em segmentos.
|
|
295
|
+
* Preserva trailing slash como segmento vazio quando ignoreTrailingSlash é false.
|
|
296
|
+
* @param {string} path
|
|
297
|
+
* @returns {string[]}
|
|
298
|
+
*/
|
|
299
|
+
#splitPath(path) {
|
|
300
|
+
if (path === '/') return [];
|
|
301
|
+
// Remove o leading '/' e faz split
|
|
302
|
+
const parts = path.slice(1).split('/');
|
|
303
|
+
// Remove trailing empty segment apenas quando trailing slash é ignorada
|
|
304
|
+
if (
|
|
305
|
+
this.#ignoreTrailingSlash &&
|
|
306
|
+
parts.length > 0 &&
|
|
307
|
+
parts[parts.length - 1] === ''
|
|
308
|
+
) {
|
|
309
|
+
parts.pop();
|
|
310
|
+
}
|
|
311
|
+
return parts;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const HttpStatus = {
|
|
2
|
+
OK: 200,
|
|
3
|
+
CREATED: 201,
|
|
4
|
+
NO_CONTENT: 204,
|
|
5
|
+
FOUND: 302,
|
|
6
|
+
BAD_REQUEST: 400,
|
|
7
|
+
UNAUTHORIZED: 401,
|
|
8
|
+
FORBIDDEN: 403,
|
|
9
|
+
NOT_FOUND: 404,
|
|
10
|
+
METHOD_NOT_ALLOWED: 405,
|
|
11
|
+
CONFLICT: 409,
|
|
12
|
+
UNPROCESSABLE_ENTITY: 422,
|
|
13
|
+
TOO_MANY_REQUESTS: 429,
|
|
14
|
+
INTERNAL_SERVER_ERROR: 500,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const HttpStatusText = {
|
|
18
|
+
[HttpStatus.OK]: 'OK',
|
|
19
|
+
[HttpStatus.CREATED]: 'Created',
|
|
20
|
+
[HttpStatus.NO_CONTENT]: 'No Content',
|
|
21
|
+
[HttpStatus.FOUND]: 'Found',
|
|
22
|
+
[HttpStatus.BAD_REQUEST]: 'Bad Request',
|
|
23
|
+
[HttpStatus.UNAUTHORIZED]: 'Unauthorized',
|
|
24
|
+
[HttpStatus.FORBIDDEN]: 'Forbidden',
|
|
25
|
+
[HttpStatus.NOT_FOUND]: 'Not Found',
|
|
26
|
+
[HttpStatus.METHOD_NOT_ALLOWED]: 'Method Not Allowed',
|
|
27
|
+
[HttpStatus.CONFLICT]: 'Conflict',
|
|
28
|
+
[HttpStatus.UNPROCESSABLE_ENTITY]: 'Unprocessable Entity',
|
|
29
|
+
[HttpStatus.TOO_MANY_REQUESTS]: 'Too Many Requests',
|
|
30
|
+
[HttpStatus.INTERNAL_SERVER_ERROR]: 'Internal Server Error',
|
|
31
|
+
};
|