@mojojs/core
Version:
Real-time web framework
290 lines • 8.8 kB
JavaScript
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