@@ -3,6 +3,51 @@ import { validateHandler, validateRoute } from "@/core/router/validation";
33import { handleError , jssert } from "@/core/error" ;
44import logger from "@/helpers/joorLogger" ;
55
6+ /**
7+ * Router class for managing HTTP routes and their handlers.
8+ * It provides methods to register routes for different HTTP methods (GET, POST, PUT, PATCH, DELETE).
9+ * It also validates the routes and their handlers to ensure they are correctly defined.
10+ *
11+ * @class Router
12+ * @example
13+ * const router = new Router();
14+ * router.get('/api/users', (req, res) => {
15+ * res.send('User list');
16+ * });
17+ * router.post('/api/users', (req, res) => {
18+ * res.send('User created');
19+ * });
20+ * router.put('/api/users/:id', (req, res) => {
21+ * res.send(`User ${req.params.id} updated`);
22+ * });
23+ * router.delete('/api/users/:id', (req, res) => {
24+ * res.send(`User ${req.params.id} deleted`);
25+ * });
26+ * router.patch('/api/users/:id', (req, res) => {
27+ * res.send(`User ${req.params.id} partially updated`);
28+ * });
29+ * router.get('/api/users/:id', (req, res) => {
30+ * res.send(`User ${req.params.id} details`);
31+ * });
32+ * router.get('/api/users/:id/friends', (req, res) => {
33+ * res.send(`User ${req.params.id} friends`);
34+ * });
35+ * router.get('/api/users/:id/friends/:friendId', (req, res) => {
36+ * res.send(`User ${req.params.id} friend ${req.params.friendId} details`);
37+ * });
38+ *
39+ * @rules
40+ * - Routes must be unique and not conflict with existing routes.
41+ * - 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.
42+ * - Only one root level route (`/`) is allowed. Additional root level routes will be ignored.
43+ * - Handlers or middlewares must be functions.
44+ * - Routes must start with `/`.
45+ * - Routes cannot be empty.
46+ * - Routes can have multiple middlewares.
47+ * - Route handler or middleware can be synchronous or asynchronous.
48+ * - Route handler or middleware should return a `JoorResponse` object or `undefined`.
49+ * - 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.
50+ */
651class Router {
752 // Static property to store routes.
853 static routes : ROUTES = {
@@ -16,8 +61,8 @@ class Router {
1661 * @param handlers - The route handlers.
1762 */
1863 public get ( route : string , ...handlers : ROUTE_HANDLER [ ] ) {
19- this . addRoute ( "GET" , route , handlers ) ;
20- }
64+ this . addRoute ( "GET" , route , handlers ) ;
65+ }
2166
2267 /**
2368 * Registers a POST route with the specified handlers.
@@ -26,35 +71,35 @@ class Router {
2671 * @param handlers - The route handlers.
2772 */
2873 public post ( route : string , ...handlers : ROUTE_HANDLER [ ] ) {
29- this . addRoute ( "POST" , route , handlers ) ;
30- }
74+ this . addRoute ( "POST" , route , handlers ) ;
75+ }
3176 /**
3277 * Registers a PUT route with the specified handlers.
3378 *
3479 * @param route - The route path.
3580 * @param handlers - The route handlers.
3681 */
3782 public put ( route : string , ...handlers : ROUTE_HANDLER [ ] ) {
38- this . addRoute ( "PUT" , route , handlers ) ;
39- }
83+ this . addRoute ( "PUT" , route , handlers ) ;
84+ }
4085 /**
4186 * Registers a PATCH route with the specified handlers.
4287 *
4388 * @param route - The route path.
4489 * @param handlers - The route handlers.
4590 */
4691 public patch ( route : string , ...handlers : ROUTE_HANDLER [ ] ) {
47- this . addRoute ( "PATCH" , route , handlers ) ;
48- }
92+ this . addRoute ( "PATCH" , route , handlers ) ;
93+ }
4994 /**
5095 * Registers a DELETE route with the specified handlers.
5196 *
5297 * @param route - The route path.
5398 * @param handlers - The route handlers.
5499 */
55- public delete ( route : string , ...handlers : ROUTE_HANDLER [ ] ) {
56- this . addRoute ( "DELETE" , route , handlers ) ;
57- }
100+ public delete ( route : string , ...handlers : ROUTE_HANDLER [ ] ) {
101+ this . addRoute ( "DELETE" , route , handlers ) ;
102+ }
58103
59104 /**
60105 * Adds a route to the router.
@@ -66,86 +111,86 @@ class Router {
66111 * @throws {Jrror } If there is a route conflict or duplicate.
67112 */
68113 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- }
114+ httpMethod : ROUTE_METHOD ,
115+ route : ROUTE_PATH ,
116+ handlers : ROUTE_HANDLER [ ]
117+ ) {
118+ try {
119+ validateRoute ( route ) ;
120+ handlers . forEach ( validateHandler ) ;
121+ if ( ! Object . keys ( Router . routes ) . includes ( '/' ) ) {
122+ Router . routes [ '/' ] = { } ;
123+ }
79124
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- }
125+ if ( Object . keys ( Router . routes ) . length > 1 ) {
126+ Router . routes = {
127+ '/' : Router . routes [ '/' ] ,
128+ } ;
129+ logger . warn (
130+ 'Multiple root level routes detected. Only the first root level route will be considered. Rest will be ignored.'
131+ ) ;
132+ }
88133
89- const routeParts = route . split ( '/' ) . filter ( ( part ) => part !== '' ) ;
134+ const routeParts = route . split ( '/' ) . filter ( ( part ) => part !== '' ) ;
90135
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- }
136+ if ( routeParts . length === 0 ) {
137+ Router . routes [ '/' ] = {
138+ ...Router . routes [ '/' ] ,
139+ localMiddlewares : Router . routes [ '/' ] . localMiddlewares ?? [ ] ,
140+ globalMiddlewares : Router . routes [ '/' ] . globalMiddlewares ?? [ ] ,
141+ [ httpMethod ] : {
142+ handlers,
143+ } ,
144+ } ;
145+ return ;
146+ }
102147
103- let currentNode = Router . routes [ '/' ] ;
148+ let currentNode = Router . routes [ '/' ] ;
104149
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 ] ;
150+ for ( const routePart of routeParts ) {
151+ // Remove query params and hash from route
152+ const [ node ] = routePart . split ( '#' ) [ 0 ] . split ( '?' ) ;
153+ // check if current node has children
154+ currentNode . children = currentNode . children ?? { } ;
155+ // check if current node is dynamic
156+ if ( node . startsWith ( ':' ) ) {
157+ // check if current parent node has other dynamic routes
158+ const keys = Object . keys ( currentNode . children ) . filter (
159+ ( key ) => key . startsWith ( ':' ) && key !== node
160+ ) ;
161+ // check if current node has other static routes
162+ jssert (
163+ keys . length === 0 ,
164+ `Route conflict: ${ route } conflicts with existing route ${ keys [ 0 ] } . You cannot have multiple dynamic routes in same parent` ,
165+ '/route' ,
166+ 'error'
167+ )
131168 }
132-
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- )
140-
141- // after all above checks, register the route
142- currentNode [ httpMethod ] = {
143- handlers,
169+ // check if current node has the same route, if no create a new node with middlwares
170+ currentNode . children [ node ] = currentNode . children [ node ] ?? {
171+ // these middlwares will be used by all the children of this node
172+ globalMiddlewares : currentNode . globalMiddlewares ?? [ ] ,
173+ localMiddlewares : currentNode . localMiddlewares ?? [ ] ,
144174 } ;
145- } catch ( error : unknown ) {
146- handleError ( error ) ;
175+ currentNode = currentNode . children [ node ] ;
147176 }
177+
178+ // if same route with same method is already registered, show warning
179+ jssert (
180+ ! currentNode [ httpMethod ] ,
181+ `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` ,
182+ '/route' ,
183+ 'warn'
184+ )
185+
186+ // after all above checks, register the route
187+ currentNode [ httpMethod ] = {
188+ handlers,
189+ } ;
190+ } catch ( error : unknown ) {
191+ handleError ( error ) ;
148192 }
149193}
194+ }
150195
151196export default Router ;
0 commit comments