2019-04-26 22:35:10 +00:00
"use strict" ;
import RouterContext from './RouterContext.mjs' ;
2019-04-28 19:45:51 +00:00
import pathspec _to _regex from '../Shared/Pathspec.mjs' ;
2019-04-26 22:35:10 +00:00
/ * *
* A standalone HTTP router that ' s based on the principle of middleware .
* Based on rill ( see the npm package bearing the name ) , but stripped down and
* simplified .
2019-04-27 14:27:09 +00:00
* @ param { Boolean } verbose Whether to be verbose and log a bunch of things to the console . Useful for debugging .
2019-04-26 22:35:10 +00:00
* /
2019-04-27 14:28:00 +00:00
class ServerRouter
2019-04-26 22:35:10 +00:00
{
constructor ( verbose = false ) {
2019-04-27 16:41:45 +00:00
/ * *
* The actions to run in turn .
* @ private
* @ type { Array < Function > }
* /
2019-04-26 22:35:10 +00:00
this . actions = [ ] ;
2022-02-07 17:38:10 +00:00
/** Whether to activate verbose mode. Useful for debugging the router. */
2019-04-26 22:35:10 +00:00
this . verbose = verbose ;
this . default _action = async ( ctx ) => {
let message = ` No route was found for ' ${ ctx . request . url } '. ` ;
if ( this . verbose ) {
message += ` \n \n Registered Actions: \n ` ;
for ( let action of this . actions ) {
message += ` - ${ action . toString ( ) } \n ` ;
}
}
ctx . send . plain ( 404 , message ) ;
}
}
2019-04-27 14:27:09 +00:00
/ * *
* Shortcut function for attaching an action to any request method .
* Full function : on
* @ param { string | RegExp } pathspec The pathspec that the route should match against .
* @ param { Function } action The function to execute when this route is matches . gets passed 2 parameters : context ( or type RequestContext ) and next ( a function ) . context contains the request / response objects , and next ( ) should be called if the action is middleware .
* /
2019-04-26 22:35:10 +00:00
any ( pathspec , action ) { this . on ( "*" , pathspec , action ) ; }
2019-04-27 14:27:09 +00:00
/ * *
* Shortcut function for attaching an action to head requests . Full function : on
* @ param { string | RegExp } pathspec The pathspec that the route should match against .
* @ param { Fuction } action The function to execute .
2020-01-13 00:18:19 +00:00
* @ example
* router . head ( "/location/:name/status" , ( context , _next ) => {
* context . send . plain ( 200 , ` Coming in from ${ context . params . type } - status ok! ` ) ;
* } ) ;
2019-04-27 14:27:09 +00:00
* /
2019-04-26 22:35:10 +00:00
head ( pathspec , action ) { this . on ( [ "head" ] , pathspec , action ) ; }
2019-04-27 14:27:09 +00:00
/ * *
* Shortcut function for attaching an action to get requests . Full function : on
* @ param { string | RegExp } pathspec The pathspec that the route should match against .
* @ param { Fuction } action The function to execute .
2020-01-13 00:18:19 +00:00
* @ example
* router . get ( "/tree/:type" , ( context , _next ) => {
* context . send . plain ( 200 , ` Hello, I am a ${ context . params . type } ` ) ;
* } ) ;
2019-04-27 14:27:09 +00:00
* /
2019-04-26 22:35:10 +00:00
get ( pathspec , action ) { this . on ( [ "get" ] , pathspec , action ) ; }
2019-04-27 14:27:09 +00:00
/ * *
* Shortcut function for attaching an action to post requests . Full function : on
* @ param { string | RegExp } pathspec The pathspec that the route should match against .
* @ param { Fuction } action The function to execute .
2020-01-13 00:18:19 +00:00
* @ example
* router . post ( "/loaction/:name/update" , ( context , _next ) => {
* context . send . plain ( 501 , ` Hello from ${ context . params . name } ! I don't yet support updating my firmware. \n You asked me to update to version ${ context . post _data . version } . ` ) ;
* // See https://github.com/sbrl/powahroot/blob/master/examples/parse_post_data.mjs for an example of parsing post data with middleware that works with the above (context.post_data is not created automatically by powahroot)
* } ) ;
2019-04-27 14:27:09 +00:00
* /
2019-04-26 22:35:10 +00:00
post ( pathspec , action ) { this . on ( [ "post" ] , pathspec , action ) ; }
2019-04-27 14:27:09 +00:00
/ * *
* Shortcut function for attaching an action to put requests . Full function : on
* @ param { string | RegExp } pathspec The pathspec that the route should match against .
* @ param { Fuction } action The function to execute .
* /
2019-04-26 22:35:10 +00:00
put ( pathspec , action ) { this . on ( [ "put" ] , pathspec , action ) ; }
2019-04-27 14:27:09 +00:00
/ * *
* Shortcut function for attaching an action to delete requests . Full function : on
* @ param { string | RegExp } pathspec The pathspec that the route should match against .
* @ param { Fuction } action The function to execute .
* /
2019-04-26 22:35:10 +00:00
delete ( pathspec , action ) { this . on ( [ "delete" ] , pathspec , action ) ; }
2019-04-27 14:27:09 +00:00
/ * *
* Shortcut function for attaching an action to options requests . Full function : on
* @ param { string | RegExp } pathspec The pathspec that the route should match against .
* @ param { Fuction } action The function to execute .
2020-01-13 00:18:19 +00:00
* @ example
* router . options ( "/location/:name/status" , ( context , _next ) => {
* context . send . plain ( 200 , ` I support these actions: \n - GET: Get my status \n - POST: Set my status \n - OPTIONS: Show this message ` ) ; ;
* } ) ;
2019-04-27 14:27:09 +00:00
* /
2019-04-26 22:35:10 +00:00
options ( pathspec , action ) { this . on ( [ "options" ] , pathspec , action ) ; }
/ * *
* Execute the specified action for requests matching the given parameters .
2019-04-27 16:41:45 +00:00
* TODO : Consider merging with on _all and refactoring to take an object literal which we can destructure instead
2019-04-26 22:35:10 +00:00
* @ param { array } methods The HTTP methods to run the action for . Include '*' to specify all methods .
* @ param { string | regex } pathspec The specification of the paths that this action should match against . May be a regular expression .
2019-04-27 16:41:45 +00:00
* @ param { Function } action The action to execute . Will be passed the parameters ` context ` ( RouterContext ) and ` next ` ( Function ) .
2020-01-13 00:18:19 +00:00
* @ example
* router . on ( [ "get" ] , "/garden/:vegetable" , ( context ) => {
* context . send ( 201 , ` Planted ${ context . params . vegetable } ` ) ;
* } ) ;
2019-04-26 22:35:10 +00:00
* /
on ( methods , pathspec , action ) {
2019-04-26 23:38:02 +00:00
let regex _info = pathspec instanceof RegExp ? { regex : pathspec , tokens : [ ] } : pathspec _to _regex ( pathspec ) ;
2019-04-26 22:35:10 +00:00
// next must be a generator that returns each action in turn
this . actions . push ( async ( context , next ) => {
const matches = context . url . pathname . match ( regex _info . regex ) ;
if ( this . verbose ) console . error ( ` [router/verbose] [ ${ methods . join ( ", " ) } -> ${ pathspec } ] Matches: ` , matches ) ;
if ( ( methods . indexOf ( context . request . method . toLowerCase ( ) ) > - 1 || methods . indexOf ( "*" ) > - 1 ) && matches ) {
if ( this . verbose ) console . error ( ` [router/verbose] Match found! Executing action. ` ) ;
for ( let i = 1 ; i < matches . length ; i ++ ) { // Skip the top-level group
context . params [ regex _info . tokens [ i - 1 ] ] = matches [ i ] ;
}
await action ( context , next ) ;
}
else {
if ( this . verbose ) console . error ( ` [router/verbose] Nope, didn't match. Moving on ` ) ;
await next ( ) ;
}
} ) ;
}
2019-04-27 14:27:09 +00:00
/ * *
* Adds a route that matches against every request .
* Usually useful for middleware ( e . g . error handlers , request loggers , authentication handlers , etc . ) .
* @ param { Function } action The function to execute .
2020-01-13 00:18:19 +00:00
* @ example
* router . on _all ( ( context ) => {
* context . send . plain ( 200 , "Hello, world!" ) ;
* } ) ;
2019-04-27 14:27:09 +00:00
* /
2019-04-26 22:35:10 +00:00
on _all ( action ) {
this . actions . push ( action ) ;
}
/ * *
* Runs the specified action for requests if the provided testing function
* returns true .
* @ param { Function } test The testing function . Will be passed the context as it ' s only parameter .
* @ param { Function } action The action to run if the test returns true .
* /
onif ( test , action ) {
this . actions . push ( async ( context , next ) => {
let test _result = test ( context ) ;
if ( this . verbose ) console . error ( "[router/verbose] Test action result: " , test _result ) ;
if ( test _result )
await action ( context , next ) ;
else
await next ( context ) ;
} )
}
/ * *
* Handles the specified request .
2022-02-12 15:50:41 +00:00
* Don ' t forget to await the Promise !
* @ param { http . ClientRequest } request The request to handle .
* @ param { http . ServerResponse } response The response object to use to send the response .
* @ return { Promise } A Promise that resolves when handling is complete .
2020-01-13 00:18:19 +00:00
* @ example
2022-02-12 15:50:41 +00:00
* const server = http . createServer ( async ( request , response ) => {
* await router . handle ( request , response ) ;
2020-01-13 00:18:19 +00:00
* } ) ;
* server . listen ( 3500 , "127.0.0.1" , ( ) => console . log ( "Listening on http://127.0.0.1:3500/" ) ) ;
2019-04-26 22:35:10 +00:00
* /
async handle ( request , response ) {
let context = new RouterContext ( request , response ) ,
iterator = this . iterate ( ) ;
// Begin the middleware execution
this . gen _next ( iterator , context ) ( ) ;
}
/ * *
* Returns an anonymous function that , when called , will execute the next
* item of middleware .
* It achieves this via a combination of a generator , anonymous function
* scope abuse , being recursive , and magic .
2019-04-27 14:27:09 +00:00
* You shouldn ' t need to call this directly .
* @ private
2019-04-26 22:35:10 +00:00
* @ param { Generator } iterator The generator that emits the middleware .
* @ param { Object } context The context of the request .
* @ return { Function } A magic next function .
* /
gen _next ( iterator , context ) {
let next _raw = iterator . next ( ) ;
// Return the default action if we've reached the end of the line
if ( next _raw . done )
return async ( ) => {
this . default _action ( context ) ;
} ;
return ( async ( ) => {
if ( this . verbose ) console . error ( ` [router/verbose] Executing ${ next _raw . value } ` ) ;
await next _raw . value ( context , this . gen _next ( iterator , context ) ) ;
} ) . bind ( this ) ; // Don't forget to bind each successive function to this context
}
/ * *
* Iterates over all the generated middleware .
* @ return { Generator } A generator that returns each successive piece of middleware in turn .
2019-04-27 16:41:45 +00:00
* @ generator
2019-04-26 22:35:10 +00:00
* /
* iterate ( ) {
for ( let action of this . actions ) {
yield action ;
}
}
}
2019-04-27 14:28:00 +00:00
export default ServerRouter ;