UNPKG

@mojojs/core

Version:

Real-time web framework

290 lines 8.8 kB
import { Pattern } from './pattern.js'; /** * Route class. */ export class Route { constructor() { /** * The children of this route, used for nesting routes. */ this.children = []; /** * Custom route name, if defined. */ this.customName = undefined; /** * Default route name. */ this.defaultName = undefined; /** * Allow `under` semantics for this route. */ this.underRoute = false; /** * Restrict HTTP methods this route is allowed to handle, defaults to no restrictions. */ this.methods = []; /** * Pattern for this route. */ this.pattern = new Pattern(); /** * Activate conditions for this route. */ this.requirements = []; /** * Activate `websocket` semantics for this route. */ this.websocketRoute = false; this._parent = undefined; this._root = undefined; } /** * Add a child to this route. */ addChild(child) { this.children.push(child); child.remove().parent = this; child.root = this.root; return child; } /** * Generate route matching any of the listed HTTP request methods or all. * @example * // Route with pattern and destination * route.any('/user').to('User#whatever'); * * // Route with HTTP methods, pattern, restrictive placeholders and destination * route.any(['DELETE', 'PUT'], '/:foo', {foo: /\w+/}).to('Foo#bar'); * * // Route with pattern, name and destination * route.any('/:foo').name('foo_route').to('Foo#bar'); * * // Route with pattern, condition and destination * route.any('/').requires({agent: /Firefox/}).to('Foo#bar'); * * // Route with pattern and a closure as destination * route.any('/:foo', async ctx => ctx.render({text: 'Hello World!'})); */ any(...args) { const child = new Route(); const childPattern = child.pattern; for (const arg of args) { if (typeof arg === 'string') { child.defaultName = arg.replace(/[^0-9a-z]+/gi, '_').replace(/^_|_$/g, ''); childPattern.parse(arg); } else if (Array.isArray(arg)) { child.methods = arg; } else if (typeof arg === 'function') { childPattern.defaults.fn = arg; } else if (typeof arg === 'object' && arg !== null) { Object.assign(childPattern.constraints, arg); } } childPattern.types = this.root?.types ?? {}; return this.addChild(child); } /** * Generate route matching only `DELETE` requests. * @example * // Route with destination * route.delete('/user').to('User#remove'); */ delete(...args) { return this.any(['DELETE'], ...args); } /** * Generate route matching only `GET` requests. * @example * // Route with destination * route.get('/user').to('User#show'); */ get(...args) { return this.any(['GET'], ...args); } /** * Check if this route has a WebSocket ancestor and cache the result for future checks. */ hasWebSocket() { return this._branch() .map(route => route.websocketRoute) .includes(true); } /** * Check if this route qualifies as an endpoint. */ isEndpoint() { return this.children.length === 0; } /** * The name of this route, defaults to an automatically generated name based on the route pattern. Note that the name * `current` is reserved for referring to the current route. */ name(name) { this.customName = name; return this; } /** * Generate route matching only `OPTIONS` requests. * @example * // Route with destination * route.options('/user').to('User#overview'); */ options(...args) { return this.any(['OPTIONS'], ...args); } /** * The parent of this route. */ get parent() { return this._parent?.deref(); } set parent(parent) { this._parent = parent === undefined ? undefined : new WeakRef(parent); } /** * Generate route matching only `PATCH` requests. * @example * // Route with destination * route.patch('/user').to('User#update'); */ patch(...args) { return this.any(['PATCH'], ...args); } /** * Generate route matching only `POST` requests. * @example * // Route with destination * route.post('/user').to('User#create'); */ post(...args) { return this.any(['POST'], ...args); } /** * Generate route matching only `PUT` requests. * @example * // Route with destination * route.put('/user').to('User#replace'); */ put(...args) { return this.any(['PUT'], ...args); } /** * Remove route from parent. */ remove() { const { parent } = this; if (parent === undefined) return this; this.parent = undefined; parent.children = parent.children.filter(route => route !== this); return this; } /** * Render route with parameters into a path. */ render(values = {}) { const parts = []; const branch = this._branch(); for (let i = 0; i < branch.length - 1; i++) { const route = branch[i]; parts.push(route.pattern.render(values, { isEndpoint: route.isEndpoint() })); } return parts.reverse().join(''); } /** * Activate conditions for this route. Note that this automatically disables the routing cache, since conditions are * too complex for caching. */ requires(condition, requirement) { const { root } = this; if (root === undefined) return this; this.requirements.push({ condition, requirement }); root.cache = null; return this; } /** * Return the `Router` object this route is a descendant of. */ get root() { return this._root?.deref(); } set root(root) { this._root = root === undefined ? undefined : new WeakRef(root); } /** * Suggested HTTP method for reaching this route, `GET` and `POST` are preferred. */ suggestedMethod() { const suggestions = []; for (const route of this._branch()) { const { methods } = route; if (methods.length <= 0) continue; suggestions.push(...(suggestions.length > 0 ? suggestions.filter(method => methods.includes(method)) : methods)); } const hasGet = suggestions.includes('GET'); if (suggestions.includes('POST') === true && hasGet === false) return 'POST'; return hasGet === true ? 'GET' : (suggestions[0] ?? 'GET'); } /** * Set default parameters for this route. */ to(...targets) { const { defaults } = this.pattern; for (const target of targets) { if (typeof target === 'string') { const parts = target.split('#'); if (parts[0] !== '') defaults.controller = parts[0]; if (parts.length > 1 && parts[1] !== '') defaults.action = parts[1]; } else if (typeof target === 'function') { defaults.fn = target; } else { Object.assign(defaults, target); } } return this; } /** * Generate route for a nested route with its own intermediate destination. * @example * // Intermediate destination and prefix shared between two routes * const auth = app.under('/user').to('User#auth'); * auth.get('/show').to('User#show'); * auth.post('/create').to('User#create'); */ under(...args) { const child = this.any(...args); child.underRoute = true; return child; } /** * Generate route matching only WebSocket handshake requests. * @example * // Route with destination * app.websocket('/echo').to('Example#echo'); */ websocket(...args) { const child = this.any(...args); child.websocketRoute = true; return child; } _branch() { const branch = [this]; let current = branch[0]; while ((current = current.parent) !== undefined) { branch.push(current); } return branch; } } //# sourceMappingURL=route.js.map