Skip to content

Commit a34a427

Browse files
committed
refactor: Make the router class more readable
1 parent 1aa286e commit a34a427

3 files changed

Lines changed: 359 additions & 205 deletions

File tree

src/core/router/router.ts

Lines changed: 136 additions & 204 deletions
Original file line numberDiff line numberDiff line change
@@ -1,219 +1,151 @@
1-
import Jrror, { JoorError } from '@/core/error';
2-
import { validateHandler, validateRoute } from '@/core/router/validation';
3-
import logger from '@/helpers/joorLogger';
4-
import { ROUTE_HANDLER, ROUTES, ROUTE_METHOD, ROUTE_PATH } from '@/types/route';
5-
/**
6-
* Class representing a Router.
7-
*
8-
* @example
9-
* const router = new Router();
10-
* router.get('/', async (req) => {
11-
* const response = new JoorResponse();
12-
* response.setHeaders({ 'Content-Type': 'application/json' });
13-
* response.setBody({ message: 'Hello World' });
14-
* return response;
15-
* });
16-
*
17-
* @example
18-
* const router = new Router();
19-
* router.post('/submit', async (req) => {
20-
* const data = req.body;
21-
* // Process the data
22-
* const response = new JoorResponse();
23-
* response.setHeaders({ 'Content-Type': 'application/json' });
24-
* response.setBody({ status: 'success', data });
25-
* return response;
26-
* });
27-
*
28-
* @example
29-
* const router = new Router();
30-
* router.put('/update/:id', async (req) => {
31-
* const { id } = req.params;
32-
* const data = req.body;
33-
* // Update the resource with the given id
34-
* const response = new JoorResponse();
35-
* response.setHeaders({ 'Content-Type': 'application/json' });
36-
* response.setBody({ status: 'updated', id, data });
37-
* return response;
38-
* });
39-
*
40-
* @example
41-
* const router = new Router();
42-
* router.delete('/delete/:id', async (req) => {
43-
* const { id } = req.params;
44-
* // Delete the resource with the given id
45-
* const response = new JoorResponse();
46-
* response.setHeaders({ 'Content-Type': 'application/json' });
47-
* response.setBody({ status: 'deleted', id });
48-
* return response;
49-
* });
50-
*
51-
* @rules
52-
* - Routes must be unique and not conflict with existing routes.
53-
* - Dynamic routes (e.g., `/update/:id`) should not conflict with other dynamic routes in the same parent. Foe example, `/update/:id` and `/update/:name` are not allowed.
54-
* - Only one root level route (`/`) is allowed. Additional root level routes will be ignored.
55-
* - Handlers or middlewares must be functions.
56-
* - Routes must start with `/`.
57-
* - Routes cannot be empty.
58-
* - Routes can have multiple middlewares.
59-
* - Route handler or middleware can be synchronous or asynchronous.
60-
* - Route handler or middleware should return a `JoorResponse` object or `undefined`.
61-
* - If handler or middleware returns `undefined`, the request will be passed to the next handler or middleware, otherwise it will be sent as a response.
62-
*/
63-
class Router {
64-
/**
65-
* Static property to store routes.
66-
*/
67-
static routes: ROUTES = {
68-
'/': {},
69-
} as ROUTES;
1+
import { ROUTE_METHOD, ROUTE_PATH, ROUTES, ROUTE_HANDLER } from "@/types/route";
2+
import { validateHandler, validateRoute } from "@/core/router/validation";
3+
import { handleError, jssert } from "@/core/error";
4+
import logger from "@/helpers/joorLogger";
705

71-
/**
72-
* Registers a GET route with the specified handlers.
73-
*
74-
* @param route - The route path.
75-
* @param handlers - The route handlers.
76-
*/
77-
public get(route: ROUTE_PATH, ...handlers: ROUTE_HANDLER[]) {
78-
this.addRoute('GET', route, handlers);
79-
}
6+
class Router {
7+
// Static property to store routes.
8+
static routes: ROUTES = {
9+
"/": {},
10+
} as ROUTES;
8011

81-
/**
82-
* Registers a POST route with the specified handlers.
83-
*
84-
* @param route - The route path.
85-
* @param handlers - The route handlers.
86-
*/
87-
public post(route: ROUTE_PATH, ...handlers: ROUTE_HANDLER[]) {
88-
this.addRoute('POST', route, handlers);
89-
}
12+
/**
13+
* Registers a GET route with the specified handlers.
14+
*
15+
* @param route - The route path.
16+
* @param handlers - The route handlers.
17+
*/
18+
public get(route: string, ...handlers: ROUTE_HANDLER[]) {
19+
this.addRoute("GET", route, handlers);
20+
}
9021

91-
/**
92-
* Registers a PUT route with the specified handlers.
93-
*
94-
* @param route - The route path.
95-
* @param handlers - The route handlers.
96-
*/
97-
public put(route: ROUTE_PATH, ...handlers: ROUTE_HANDLER[]) {
98-
this.addRoute('PUT', route, handlers);
99-
}
22+
/**
23+
* Registers a POST route with the specified handlers.
24+
*
25+
* @param route - The route path.
26+
* @param handlers - The route handlers.
27+
*/
28+
public post(route: string, ...handlers: ROUTE_HANDLER[]) {
29+
this.addRoute("POST", route, handlers);
30+
}
31+
/**
32+
* Registers a PUT route with the specified handlers.
33+
*
34+
* @param route - The route path.
35+
* @param handlers - The route handlers.
36+
*/
37+
public put(route: string, ...handlers: ROUTE_HANDLER[]) {
38+
this.addRoute("PUT", route, handlers);
39+
}
40+
/**
41+
* Registers a PATCH route with the specified handlers.
42+
*
43+
* @param route - The route path.
44+
* @param handlers - The route handlers.
45+
*/
46+
public patch(route: string, ...handlers: ROUTE_HANDLER[]) {
47+
this.addRoute("PATCH", route, handlers);
48+
}
49+
/**
50+
* Registers a DELETE route with the specified handlers.
51+
*
52+
* @param route - The route path.
53+
* @param handlers - The route handlers.
54+
*/
55+
public delete(route: string, ...handlers: ROUTE_HANDLER[]) {
56+
this.addRoute("DELETE", route, handlers);
57+
}
10058

101-
/**
102-
* Registers a PATCH route with the specified handlers.
103-
*
104-
* @param route - The route path.
105-
* @param handlers - The route handlers.
106-
*/
107-
public patch(route: ROUTE_PATH, ...handlers: ROUTE_HANDLER[]) {
108-
this.addRoute('PATCH', route, handlers);
109-
}
59+
/**
60+
* Adds a route to the router.
61+
*
62+
* @private
63+
* @param httpMethod - The HTTP method.
64+
* @param route - The route path.
65+
* @param handlers - The route handlers.
66+
* @throws {Jrror} If there is a route conflict or duplicate.
67+
*/
68+
private addRoute(
69+
httpMethod: ROUTE_METHOD,
70+
route: ROUTE_PATH,
71+
handlers: ROUTE_HANDLER[]
72+
) {
73+
try {
74+
validateRoute(route);
75+
handlers.forEach(validateHandler);
76+
if (!Object.keys(Router.routes).includes('/')) {
77+
Router.routes['/'] = {};
78+
}
11079

111-
/**
112-
* Registers a DELETE route with the specified handlers.
113-
*
114-
* @param route - The route path.
115-
* @param handlers - The route handlers.
116-
*/
117-
public delete(route: ROUTE_PATH, ...handlers: ROUTE_HANDLER[]) {
118-
this.addRoute('DELETE', route, handlers);
119-
}
120-
/**
121-
* Adds a route to the router.
122-
*
123-
* @private
124-
* @param httpMethod - The HTTP method.
125-
* @param route - The route path.
126-
* @param handlers - The route handlers.
127-
* @throws {Jrror} If there is a route conflict or duplicate.
128-
*/
129-
private addRoute(
130-
httpMethod: ROUTE_METHOD,
131-
route: ROUTE_PATH,
132-
handlers: ROUTE_HANDLER[]
133-
) {
134-
try {
135-
validateRoute(route);
136-
handlers.forEach(validateHandler);
137-
if (!Object.keys(Router.routes).includes('/')) {
138-
Router.routes['/'] = {};
139-
}
80+
if (Object.keys(Router.routes).length > 1) {
81+
Router.routes = {
82+
'/': Router.routes['/'],
83+
};
84+
logger.warn(
85+
'Multiple root level routes detected. Only the first root level route will be considered. Rest will be ignored.'
86+
);
87+
}
14088

141-
if (Object.keys(Router.routes).length > 1) {
142-
Router.routes = {
143-
'/': Router.routes['/'],
144-
};
145-
logger.warn(
146-
'Multiple root level routes detected. Only the first root level route will be considered. Rest will be ignored.'
147-
);
148-
}
89+
const routeParts = route.split('/').filter((part) => part !== '');
14990

150-
const routeParts = route.split('/').filter((part) => part !== '');
91+
if (routeParts.length === 0) {
92+
Router.routes['/'] = {
93+
...Router.routes['/'],
94+
localMiddlewares: Router.routes['/'].localMiddlewares ?? [],
95+
globalMiddlewares: Router.routes['/'].globalMiddlewares ?? [],
96+
[httpMethod]: {
97+
handlers,
98+
},
99+
};
100+
return;
101+
}
151102

152-
if (routeParts.length === 0) {
153-
Router.routes['/'] = {
154-
...Router.routes['/'],
155-
localMiddlewares: Router.routes['/'].localMiddlewares ?? [],
156-
globalMiddlewares: Router.routes['/'].globalMiddlewares ?? [],
157-
[httpMethod]: {
158-
handlers,
159-
},
160-
};
161-
return;
162-
}
103+
let currentNode = Router.routes['/'];
163104

164-
let currentNode = Router.routes['/'];
105+
for (const routePart of routeParts) {
106+
// Remove query params and hash from route
107+
const [node] = routePart.split('#')[0].split('?');
108+
// check if current node has children
109+
currentNode.children = currentNode.children ?? {};
110+
// check if current node is dynamic
111+
if (node.startsWith(':')) {
112+
// check if current parent node has other dynamic routes
113+
const keys = Object.keys(currentNode.children).filter(
114+
(key) => key.startsWith(':') && key !== node
115+
);
116+
// check if current node has other static routes
117+
jssert(
118+
keys.length === 0,
119+
`Route conflict: ${route} conflicts with existing route ${keys[0]}. You cannot have multiple dynamic routes in same parent`,
120+
'/route',
121+
'error'
122+
)
123+
}
124+
// check if current node has the same route, if no create a new node with middlwares
125+
currentNode.children[node] = currentNode.children[node] ?? {
126+
// these middlwares will be used by all the children of this node
127+
globalMiddlewares: currentNode.globalMiddlewares ?? [],
128+
localMiddlewares: currentNode.localMiddlewares ?? [],
129+
};
130+
currentNode = currentNode.children[node];
131+
}
165132

166-
for (const routePart of routeParts) {
167-
// Remove query params and hash from route
168-
const [node] = routePart.split('#')[0].split('?');
169-
// check if current node has children
170-
currentNode.children = currentNode.children ?? {};
171-
// check if current node is dynamic
172-
if (node.startsWith(':')) {
173-
// check if current parent node has other dynamic routes
174-
const keys = Object.keys(currentNode.children).filter(
175-
(key) => key.startsWith(':') && key !== node
176-
);
133+
// if same route with same method is already registered, show warning
134+
jssert(
135+
!currentNode[httpMethod],
136+
`Route conflict: ${route} with ${httpMethod} method has already been registered. Trying to register the same route will override the previous one, and there might be unintended behaviors`,
137+
'/route',
138+
'warn'
139+
)
177140

178-
if (keys.length !== 0) {
179-
throw new Jrror({
180-
code: 'route-conflict',
181-
message: `Route conflict: ${route} conflicts with existing route ${keys[0]}. You cannot have multiple dynamic routes in same parent`,
182-
type: 'error',
183-
docsPath: '/routing',
184-
});
185-
}
141+
// after all above checks, register the route
142+
currentNode[httpMethod] = {
143+
handlers,
144+
};
145+
} catch (error: unknown) {
146+
handleError(error);
186147
}
187-
// check if current node has the same route, if no create a new node with middlwares
188-
currentNode.children[node] = currentNode.children[node] ?? {
189-
// these middlwares will be used by all the children of this node
190-
globalMiddlewares: currentNode.globalMiddlewares ?? [],
191-
localMiddlewares: currentNode.localMiddlewares ?? [],
192-
};
193-
currentNode = currentNode.children[node];
194-
}
195-
196-
// if same route with same method is already registered, show warning
197-
if (currentNode[httpMethod]) {
198-
throw new Jrror({
199-
code: 'route-duplicate',
200-
message: `Route conflict: ${route} with ${httpMethod} method has already been registered. Trying to register the same route will override the previous one, and there might be unintended behaviors`,
201-
type: 'warn',
202-
docsPath: '/routing',
203-
});
204-
}
205-
// after all above checks, register the route
206-
currentNode[httpMethod] = {
207-
handlers,
208-
};
209-
} catch (error: unknown) {
210-
if (error instanceof Jrror || error instanceof JoorError) {
211-
error.handle();
212-
} else {
213-
logger.error('Router Error: ', error);
214-
}
215148
}
216-
}
217149
}
218150

219-
export default Router;
151+
export default Router;

0 commit comments

Comments
 (0)