2016-04-28 17:50:03 -07:00
import { isBlank , isPresent , isFunction } from '../../src/facade/lang' ;
import { BaseException } from '../../src/facade/exceptions' ;
import { Map } from '../../src/facade/collection' ;
import { PromiseWrapper } from '../../src/facade/async' ;
2016-02-09 11:12:41 -08:00
import { AbstractRule , RouteRule , RedirectRule , RouteMatch , PathMatch } from './rules' ;
2016-04-12 09:40:37 -07:00
import {
Route ,
AsyncRoute ,
AuxRoute ,
Redirect ,
RouteDefinition
} from '../route_config/route_config_impl' ;
2016-02-09 11:12:41 -08:00
import { AsyncRouteHandler } from './route_handlers/async_route_handler' ;
import { SyncRouteHandler } from './route_handlers/sync_route_handler' ;
import { RoutePath } from './route_paths/route_path' ;
import { ParamRoutePath } from './route_paths/param_route_path' ;
import { RegexRoutePath } from './route_paths/regex_route_path' ;
import { Url } from '../url_parser' ;
import { ComponentInstruction } from '../instruction' ;
/**
* A `RuleSet` is responsible for recognizing routes for a particular component.
* It is consumed by `RouteRegistry`, which knows how to recognize an entire hierarchy of
* components.
*/
export class RuleSet {
rulesByName = new Map < string , RouteRule >();
// map from name to rule
auxRulesByName = new Map < string , RouteRule >();
// map from starting path to rule
auxRulesByPath = new Map < string , RouteRule >();
// TODO: optimize this into a trie
rules : AbstractRule [] = [];
// the rule to use automatically when recognizing or generating from this rule set
defaultRule : RouteRule = null ;
/**
* Configure additional rules in this rule set from a route definition
* @returns {boolean} true if the config is terminal
*/
config ( config : RouteDefinition ) : boolean {
let handler ;
if ( isPresent ( config . name ) && config . name [ 0 ]. toUpperCase () != config . name [ 0 ]) {
let suggestedName = config . name [ 0 ]. toUpperCase () + config . name . substring ( 1 );
throw new BaseException (
`Route " ${ config . path } " with name " ${ config . name } " does not begin with an uppercase letter. Route names should be CamelCase like " ${ suggestedName } ".` );
}
if ( config instanceof AuxRoute ) {
handler = new SyncRouteHandler ( config . component , config . data );
let routePath = this . _getRoutePath ( config );
2016-03-30 11:26:31 -07:00
let auxRule = new RouteRule ( routePath , handler , config . name );
2016-02-09 11:12:41 -08:00
this . auxRulesByPath . set ( routePath . toString (), auxRule );
if ( isPresent ( config . name )) {
this . auxRulesByName . set ( config . name , auxRule );
}
return auxRule . terminal ;
}
let useAsDefault = false ;
if ( config instanceof Redirect ) {
let routePath = this . _getRoutePath ( config );
let redirector = new RedirectRule ( routePath , config . redirectTo );
this . _assertNoHashCollision ( redirector . hash , config . path );
this . rules . push ( redirector );
return true ;
}
if ( config instanceof Route ) {
handler = new SyncRouteHandler ( config . component , config . data );
useAsDefault = isPresent ( config . useAsDefault ) && config . useAsDefault ;
} else if ( config instanceof AsyncRoute ) {
handler = new AsyncRouteHandler ( config . loader , config . data );
useAsDefault = isPresent ( config . useAsDefault ) && config . useAsDefault ;
}
let routePath = this . _getRoutePath ( config );
2016-03-30 11:26:31 -07:00
let newRule = new RouteRule ( routePath , handler , config . name );
2016-02-09 11:12:41 -08:00
this . _assertNoHashCollision ( newRule . hash , config . path );
if ( useAsDefault ) {
if ( isPresent ( this . defaultRule )) {
throw new BaseException ( `Only one route can be default` );
}
this . defaultRule = newRule ;
}
this . rules . push ( newRule );
if ( isPresent ( config . name )) {
this . rulesByName . set ( config . name , newRule );
}
return newRule . terminal ;
}
/**
* Given a URL, returns a list of `RouteMatch`es, which are partial recognitions for some route.
*/
recognize ( urlParse : Url ) : Promise < RouteMatch >[] {
var solutions = [];
this . rules . forEach (( routeRecognizer : AbstractRule ) => {
var pathMatch = routeRecognizer . recognize ( urlParse );
if ( isPresent ( pathMatch )) {
solutions . push ( pathMatch );
}
});
// handle cases where we are routing just to an aux route
if ( solutions . length == 0 && isPresent ( urlParse ) && urlParse . auxiliary . length > 0 ) {
return [ PromiseWrapper . resolve ( new PathMatch ( null , null , urlParse . auxiliary ))];
}
return solutions ;
}
recognizeAuxiliary ( urlParse : Url ) : Promise < RouteMatch >[] {
var routeRecognizer : RouteRule = this . auxRulesByPath . get ( urlParse . path );
if ( isPresent ( routeRecognizer )) {
return [ routeRecognizer . recognize ( urlParse )];
}
return [ PromiseWrapper . resolve ( null )];
}
hasRoute ( name : string ) : boolean { return this . rulesByName . has ( name ); }
componentLoaded ( name : string ) : boolean {
return this . hasRoute ( name ) && isPresent ( this . rulesByName . get ( name ). handler . componentType );
}
loadComponent ( name : string ) : Promise < any > {
return this . rulesByName . get ( name ). handler . resolveComponentType ();
}
generate ( name : string , params : any ) : ComponentInstruction {
var rule : RouteRule = this . rulesByName . get ( name );
if ( isBlank ( rule )) {
return null ;
}
return rule . generate ( params );
}
generateAuxiliary ( name : string , params : any ) : ComponentInstruction {
var rule : RouteRule = this . auxRulesByName . get ( name );
if ( isBlank ( rule )) {
return null ;
}
return rule . generate ( params );
}
private _assertNoHashCollision ( hash : string , path ) {
this . rules . forEach (( rule ) => {
if ( hash == rule . hash ) {
throw new BaseException (
`Configuration ' ${ path } ' conflicts with existing route ' ${ rule . path } '` );
}
});
}
private _getRoutePath ( config : RouteDefinition ) : RoutePath {
if ( isPresent ( config . regex )) {
if ( isFunction ( config . serializer )) {
return new RegexRoutePath ( config . regex , config . serializer );
} else {
throw new BaseException (
`Route provides a regex property, ' ${ config . regex } ', but no serializer property` );
}
}
if ( isPresent ( config . path )) {
// Auxiliary routes do not have a slash at the start
let path = ( config instanceof AuxRoute && config . path . startsWith ( '/' )) ?
2016-04-12 09:40:37 -07:00
config . path . substring ( 1 ) :
config . path ;
2016-02-09 11:12:41 -08:00
return new ParamRoutePath ( path );
}
throw new BaseException ( 'Route must provide either a path or regex property' );
}
}