2016-05-02 13:27:03 -07:00

188 lines
6.2 KiB
TypeScript

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';
import {AbstractRule, RouteRule, RedirectRule, RouteMatch, PathMatch} from './rules';
import {
Route,
AsyncRoute,
AuxRoute,
Redirect,
RouteDefinition
} from '../route_config/route_config_impl';
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);
let auxRule = new RouteRule(routePath, handler, config.name);
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);
let newRule = new RouteRule(routePath, handler, config.name);
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('/')) ?
config.path.substring(1) :
config.path;
return new ParamRoutePath(path);
}
throw new BaseException('Route must provide either a path or regex property');
}
}