From 93a71aa6eee4e835a1830a9e7fc6f62f9980948c Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 23 Mar 2025 11:58:36 +0000 Subject: refactor(router): reorganize Router class into separate index file and remove legacy router.js --- source/index.js | 2 +- source/library/router.js | 107 ---------------------------------- source/library/router/index.js | 107 ++++++++++++++++++++++++++++++++++ source/library/router/index.md | 127 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 235 insertions(+), 108 deletions(-) delete mode 100644 source/library/router.js create mode 100644 source/library/router/index.js create mode 100644 source/library/router/index.md (limited to 'source') diff --git a/source/index.js b/source/index.js index bed631f..e0a6b04 100644 --- a/source/index.js +++ b/source/index.js @@ -1,4 +1,4 @@ -import Router from "./library/router.js"; +import Router from "./library/router/index.js"; import talent96 from "./data/talent96.js"; diff --git a/source/library/router.js b/source/library/router.js deleted file mode 100644 index d0914ff..0000000 --- a/source/library/router.js +++ /dev/null @@ -1,107 +0,0 @@ -/* - Router class to handle routing of requests based on the request path. - - @param {Request} request - the incoming request object - @param {Environment} environment - the environment object - @param {Context} context - the context object - - @returns {Response} a new Response object -*/ -export default class Router { - constructor(request, environment, context) { - this.request = request; - this.environment = environment; - this.context = context; - this.routes = new Map(); // Use Map for faster lookups - this.parameters = {}; // Store extracted route parameters - } - - /* - Add a new route to the router. - - @param {string} path - the path to match - @param {function} handler - the handler function to call if the path matches - - @returns {Router} the router object - */ - add(path, handler) { - if (typeof path !== "string") { - throw new TypeError("Path must be a string"); - } - if (typeof handler !== "function") { - throw new TypeError("Handler must be a function"); - } - - const normalizedPath = path.replace(/\/+$/, ""); // Normalize path - const pathRegex = new RegExp( - "^" + - normalizedPath - .replace(/:[^/]+/g, "([^/]+)") // Convert :param to regex group - .replace(/\//g, "\\/") + - "$" - ); - this.routes.set(pathRegex, { handler, originalPath: normalizedPath }); - return this; - } - - /* - Helper function to respond with a JSON object. - - @param {object} body - the JSON object to respond with - - @returns {Response} a new Response object - */ - respond(body, status = 200) { - return new Response(JSON.stringify(body), { - status, - headers: { - "Access-Control-Allow-Origin": "*", - "Content-Type": "application/json", - }, - }); - } - - /* - Route the request to the appropriate handler based on the request path. - - @returns {Response} a new Response object - */ - route() { - try { - const url = new URL(this.request.url); - const normalizedPath = url.pathname.replace(/\/+$/, ""); // Remove trailing slashes - - for (const [pathRegex, { handler, originalPath }] of this.routes) { - const match = normalizedPath.match(pathRegex); - if (match) { - // Extract parameters - const paramNames = [...originalPath.matchAll(/:([^/]+)/g)].map( - (m) => m[1] - ); - this.parameters = paramNames.reduce((params, name, index) => { - params[name] = match[index + 1]; - return params; - }, {}); - - return handler(this.request, this.environment, this.context); - } - } - - // Return 404 if route not found - return this.respond( - { - error: `Route not found: ${normalizedPath}`, - }, - 404 - ); - } catch (error) { - // Handle unexpected errors - return this.respond( - { - error: `Internal Server Error: ${error.message}`, - }, - 500 - ); - } - } -} diff --git a/source/library/router/index.js b/source/library/router/index.js new file mode 100644 index 0000000..d0914ff --- /dev/null +++ b/source/library/router/index.js @@ -0,0 +1,107 @@ +/* + Router class to handle routing of requests based on the request path. + + @param {Request} request - the incoming request object + @param {Environment} environment - the environment object + @param {Context} context - the context object + + @returns {Response} a new Response object +*/ +export default class Router { + constructor(request, environment, context) { + this.request = request; + this.environment = environment; + this.context = context; + this.routes = new Map(); // Use Map for faster lookups + this.parameters = {}; // Store extracted route parameters + } + + /* + Add a new route to the router. + + @param {string} path - the path to match + @param {function} handler - the handler function to call if the path matches + + @returns {Router} the router object + */ + add(path, handler) { + if (typeof path !== "string") { + throw new TypeError("Path must be a string"); + } + if (typeof handler !== "function") { + throw new TypeError("Handler must be a function"); + } + + const normalizedPath = path.replace(/\/+$/, ""); // Normalize path + const pathRegex = new RegExp( + "^" + + normalizedPath + .replace(/:[^/]+/g, "([^/]+)") // Convert :param to regex group + .replace(/\//g, "\\/") + + "$" + ); + this.routes.set(pathRegex, { handler, originalPath: normalizedPath }); + return this; + } + + /* + Helper function to respond with a JSON object. + + @param {object} body - the JSON object to respond with + + @returns {Response} a new Response object + */ + respond(body, status = 200) { + return new Response(JSON.stringify(body), { + status, + headers: { + "Access-Control-Allow-Origin": "*", + "Content-Type": "application/json", + }, + }); + } + + /* + Route the request to the appropriate handler based on the request path. + + @returns {Response} a new Response object + */ + route() { + try { + const url = new URL(this.request.url); + const normalizedPath = url.pathname.replace(/\/+$/, ""); // Remove trailing slashes + + for (const [pathRegex, { handler, originalPath }] of this.routes) { + const match = normalizedPath.match(pathRegex); + if (match) { + // Extract parameters + const paramNames = [...originalPath.matchAll(/:([^/]+)/g)].map( + (m) => m[1] + ); + this.parameters = paramNames.reduce((params, name, index) => { + params[name] = match[index + 1]; + return params; + }, {}); + + return handler(this.request, this.environment, this.context); + } + } + + // Return 404 if route not found + return this.respond( + { + error: `Route not found: ${normalizedPath}`, + }, + 404 + ); + } catch (error) { + // Handle unexpected errors + return this.respond( + { + error: `Internal Server Error: ${error.message}`, + }, + 500 + ); + } + } +} diff --git a/source/library/router/index.md b/source/library/router/index.md new file mode 100644 index 0000000..65e01ce --- /dev/null +++ b/source/library/router/index.md @@ -0,0 +1,127 @@ +# Router Class Documentation + +The `Router` class is designed to handle routing of HTTP requests based on the request path. It allows you to define routes with dynamic parameters and associate them with handler functions. + +## Constructor + +### `new Router(request, environment, context)` + +Creates a new instance of the `Router` class. + +- **Parameters:** + + - `request` (Request): The incoming request object. + - `environment` (Environment): The environment object. + - `context` (Context): The context object. + +- **Returns:** A new `Router` instance. + +--- + +## Methods + +### `add(path, handler)` + +Adds a new route to the router. + +- **Parameters:** + + - `path` (string): The path to match. Dynamic segments can be defined using `:paramName`. + - `handler` (function): The handler function to call if the path matches. The handler receives `request`, `environment`, and `context` as arguments. + +- **Returns:** The `Router` instance for chaining. + +- **Example:** + ```javascript + router.add("/users/:id", (request, environment, context) => { + const userId = router.parameters.id; + return new Response(`User ID: ${userId}`); + }); + ``` + +--- + +### `respond(body, status = 200)` + +Helper function to create a JSON response. + +- **Parameters:** + + - `body` (object): The JSON object to respond with. + - `status` (number, optional): The HTTP status code. Defaults to `200`. + +- **Returns:** A `Response` object. + +- **Example:** + ```javascript + return router.respond({ message: "Success" }, 200); + ``` + +--- + +### `route()` + +Routes the incoming request to the appropriate handler based on the request path. + +- **Returns:** A `Response` object. + +- **Behavior:** + + - Matches the request path against the registered routes. + - Extracts dynamic parameters from the path and stores them in `router.parameters`. + - Calls the corresponding handler function. + - Returns a `404` response if no route matches. + - Returns a `500` response in case of unexpected errors. + +- **Example:** + ```javascript + const response = router.route(); + ``` + +--- + +## Example Usage + +```javascript +import Router from "./router.js"; + +const router = new Router(request, environment, context); + +router + .add("/hello", () => { + return new Response("Hello, world!"); + }) + .add("/users/:id", (request, environment, context) => { + const userId = router.parameters.id; + return new Response(`User ID: ${userId}`); + }); + +const response = router.route(); +``` + +--- + +## Error Handling + +- **404 Not Found:** If no route matches the request path, the `Router` responds with: + + ```json + { + "error": "Route not found: /path" + } + ``` + +- **500 Internal Server Error:** If an unexpected error occurs, the `Router` responds with: + ```json + { + "error": "Internal Server Error: error message" + } + ``` + +--- + +## Notes + +- Paths are normalized by removing trailing slashes. +- Dynamic parameters in paths (e.g., `:id`) are extracted and stored in the `parameters` property of the `Router` instance. +- The `add` method uses regular expressions to match paths efficiently. -- cgit v1.2.3