Need to fix clone...
This commit is contained in:
parent
5897a9839b
commit
5d5ac7bef9
38 changed files with 465 additions and 1390 deletions
|
@ -1,6 +0,0 @@
|
||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="All Route Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" folderName="Route Tests" singleton="true">
|
|
||||||
<option name="filePath" value="$PROJECT_DIR$/test/route/all_test.dart" />
|
|
||||||
<method />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
|
@ -1,6 +0,0 @@
|
||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="All Router Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" folderName="Router Tests" singleton="true">
|
|
||||||
<option name="filePath" value="$PROJECT_DIR$/test/router/all_test.dart" />
|
|
||||||
<method />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
|
@ -1,6 +0,0 @@
|
||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="All Server Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" folderName="Server Tests" singleton="true">
|
|
||||||
<option name="filePath" value="$PROJECT_DIR$/test/server/all_test.dart" />
|
|
||||||
<method />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
|
@ -1,6 +0,0 @@
|
||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="Chain" type="DartTestRunConfigurationType" factoryName="Dart Test" folderName="Router Tests" singleton="true">
|
|
||||||
<option name="filePath" value="$PROJECT_DIR$/test/chain/all_test.dart" />
|
|
||||||
<method />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
|
@ -1,6 +0,0 @@
|
||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="Fallback" type="DartTestRunConfigurationType" factoryName="Dart Test" folderName="Router Tests" singleton="true">
|
|
||||||
<option name="filePath" value="$PROJECT_DIR$/test/router/fallback.dart" />
|
|
||||||
<method />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
|
@ -1,6 +0,0 @@
|
||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="Method Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" folderName="Router Tests" singleton="true">
|
|
||||||
<option name="filePath" value="$PROJECT_DIR$/test/method/all_test.dart" />
|
|
||||||
<method />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
6
.idea/runConfigurations/Navigate_Tests.xml
Normal file
6
.idea/runConfigurations/Navigate_Tests.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="Navigate Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">
|
||||||
|
<option name="filePath" value="$PROJECT_DIR$/test/navigate_test.dart" />
|
||||||
|
<method />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
|
@ -1,6 +0,0 @@
|
||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="No Params" type="DartTestRunConfigurationType" factoryName="Dart Test" folderName="Route Tests" singleton="true">
|
|
||||||
<option name="filePath" value="$PROJECT_DIR$/test/route/no_params.dart" />
|
|
||||||
<method />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
|
@ -1,6 +0,0 @@
|
||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="Parse Params" type="DartTestRunConfigurationType" factoryName="Dart Test" folderName="Route Tests" singleton="true">
|
|
||||||
<option name="filePath" value="$PROJECT_DIR$/test/route/parse_params.dart" />
|
|
||||||
<method />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
6
.idea/runConfigurations/Server_Tests.xml
Normal file
6
.idea/runConfigurations/Server_Tests.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="Server Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">
|
||||||
|
<option name="filePath" value="$PROJECT_DIR$/test/server_test.dart" />
|
||||||
|
<method />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
|
@ -1,6 +0,0 @@
|
||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="Use" type="DartTestRunConfigurationType" factoryName="Dart Test" folderName="Router Tests" singleton="true">
|
|
||||||
<option name="filePath" value="$PROJECT_DIR$/test/use.dart" />
|
|
||||||
<method />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
|
@ -1,6 +0,0 @@
|
||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="With Params" type="DartTestRunConfigurationType" factoryName="Dart Test" folderName="Route Tests" singleton="true">
|
|
||||||
<option name="filePath" value="$PROJECT_DIR$/test/route/with_params.dart" />
|
|
||||||
<method />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
|
@ -1,5 +1,6 @@
|
||||||
/// A powerful, isomorphic routing library for Dart.
|
|
||||||
library angel_route;
|
library angel_route;
|
||||||
|
|
||||||
|
export 'src/extensible.dart';
|
||||||
|
export 'src/middleware_pipeline.dart';
|
||||||
export 'src/router.dart';
|
export 'src/router.dart';
|
||||||
export 'src/routing_exception.dart';
|
export 'src/routing_exception.dart';
|
138
lib/browser.dart
138
lib/browser.dart
|
@ -1,138 +0,0 @@
|
||||||
import 'dart:async' show Stream, StreamController;
|
|
||||||
import 'dart:html' show AnchorElement, window;
|
|
||||||
import 'angel_route.dart';
|
|
||||||
|
|
||||||
final RegExp _hash = new RegExp(r'^#/');
|
|
||||||
|
|
||||||
/// A variation of the [Router] support both hash routing and push state.
|
|
||||||
abstract class BrowserRouter extends Router {
|
|
||||||
/// Fires whenever the active route changes. Fires `null` if none is selected (404).
|
|
||||||
Stream<Route> get onRoute;
|
|
||||||
|
|
||||||
/// Set `hash` to true to use hash routing instead of push state.
|
|
||||||
/// `listen` as `true` will call `listen` after initialization.
|
|
||||||
factory BrowserRouter({bool hash: false, bool listen: true, Route root}) {
|
|
||||||
return hash
|
|
||||||
? new _HashRouter(listen: listen, root: root)
|
|
||||||
: new _PushStateRouter(listen: listen, root: root);
|
|
||||||
}
|
|
||||||
|
|
||||||
BrowserRouter._([Route root]) : super(root: root);
|
|
||||||
|
|
||||||
/// Calls `goTo` on the [Route] matching `path`.
|
|
||||||
void go(String path, [Map params]);
|
|
||||||
|
|
||||||
/// Navigates to the given route.
|
|
||||||
void goTo(Route route, [Map params]);
|
|
||||||
|
|
||||||
/// Begins listen for location changes.
|
|
||||||
void listen();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BrowserRouterImpl extends Router implements BrowserRouter {
|
|
||||||
Route _current;
|
|
||||||
StreamController<Route> _onRoute = new StreamController<Route>();
|
|
||||||
Route get currentRoute => _current;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Stream<Route> get onRoute => _onRoute.stream;
|
|
||||||
|
|
||||||
_BrowserRouterImpl({bool listen, Route root}) : super(root: root) {
|
|
||||||
if (listen) this.listen();
|
|
||||||
prepareAnchors();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void go(String path, [Map params]) {
|
|
||||||
final resolved = resolve(path);
|
|
||||||
|
|
||||||
if (resolved != null)
|
|
||||||
goTo(resolved, params);
|
|
||||||
else
|
|
||||||
throw new RoutingException.noSuchRoute(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void listen() {
|
|
||||||
normalize();
|
|
||||||
}
|
|
||||||
|
|
||||||
void prepareAnchors() {
|
|
||||||
final anchors = window.document.querySelectorAll('a:not([dynamic])');
|
|
||||||
|
|
||||||
for (final AnchorElement $a in anchors) {
|
|
||||||
if ($a.attributes.containsKey('href') &&
|
|
||||||
!$a.attributes.containsKey('download') &&
|
|
||||||
!$a.attributes.containsKey('target') &&
|
|
||||||
$a.attributes['rel'] != 'external') {
|
|
||||||
$a.onClick.listen((e) {
|
|
||||||
e.preventDefault();
|
|
||||||
go($a.attributes['href']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$a.attributes['dynamic'] = 'true';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _HashRouter extends _BrowserRouterImpl {
|
|
||||||
_HashRouter({bool listen, Route root}) : super(listen: listen, root: root) {
|
|
||||||
if (listen) this.listen();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void goTo(Route route, [Map params]) {
|
|
||||||
route.state.properties.addAll(params ?? {});
|
|
||||||
window.location.hash = '#/${route.makeUri(params)}';
|
|
||||||
_onRoute.add(route);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void listen() {
|
|
||||||
super.listen();
|
|
||||||
window.onHashChange.listen((_) {
|
|
||||||
final path = window.location.hash.replaceAll(_hash, '');
|
|
||||||
final resolved = resolveOnRoot(path);
|
|
||||||
|
|
||||||
if (resolved == null || (path.isEmpty && resolved == root)) {
|
|
||||||
_onRoute.add(_current = null);
|
|
||||||
} else if (resolved != null && resolved != _current) {
|
|
||||||
goTo(resolved);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PushStateRouter extends _BrowserRouterImpl {
|
|
||||||
_PushStateRouter({bool listen, Route root})
|
|
||||||
: super(listen: listen, root: root) {
|
|
||||||
if (listen) this.listen();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void goTo(Route route, [Map params]) {
|
|
||||||
window.history.pushState(
|
|
||||||
{'path': route.path, 'params': params ?? {}, 'properties': properties},
|
|
||||||
route.name ?? route.path,
|
|
||||||
route.makeUri(params));
|
|
||||||
_onRoute.add(_current = route..state.properties.addAll(params ?? {}));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void listen() {
|
|
||||||
super.listen();
|
|
||||||
window.onPopState.listen((e) {
|
|
||||||
if (e.state is Map && e.state.containsKey('path')) {
|
|
||||||
final resolved = resolve(e.state['path']);
|
|
||||||
|
|
||||||
if (resolved != _current) {
|
|
||||||
properties.addAll(e.state['properties'] ?? {});
|
|
||||||
_onRoute.add(_current = resolved
|
|
||||||
..state.properties.addAll(e.state['params'] ?? {}));
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
_onRoute.add(_current = null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
17
lib/src/middleware_pipeline.dart
Normal file
17
lib/src/middleware_pipeline.dart
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import 'router.dart';
|
||||||
|
|
||||||
|
class MiddlewarePipeline {
|
||||||
|
final List<RoutingResult> routingResults;
|
||||||
|
|
||||||
|
List get handlers {
|
||||||
|
final handlers = [];
|
||||||
|
|
||||||
|
for (RoutingResult result in routingResults) {
|
||||||
|
handlers.addAll(result.allHandlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
return handlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
MiddlewarePipeline(this.routingResults);
|
||||||
|
}
|
|
@ -351,7 +351,7 @@ class Route {
|
||||||
final pathString = _pathify(path).replaceAll(new RegExp('\/'), r'\/');
|
final pathString = _pathify(path).replaceAll(new RegExp('\/'), r'\/');
|
||||||
Iterable<Match> matches = _param.allMatches(pathString);
|
Iterable<Match> matches = _param.allMatches(pathString);
|
||||||
_printDebug(
|
_printDebug(
|
||||||
'All param names parsed in $pathString: ${matches.map((m) => m.group(0))}');
|
'All param names parsed in "$pathString": ${matches.map((m) => m.group(0))}');
|
||||||
|
|
||||||
for (int i = 0; i < matches.length && i < values.length; i++) {
|
for (int i = 0; i < matches.length && i < values.length; i++) {
|
||||||
Match match = matches.elementAt(i);
|
Match match = matches.elementAt(i);
|
||||||
|
|
|
@ -2,8 +2,9 @@ library angel_route.src.router;
|
||||||
|
|
||||||
import 'extensible.dart';
|
import 'extensible.dart';
|
||||||
import 'routing_exception.dart';
|
import 'routing_exception.dart';
|
||||||
|
part 'symlink_route.dart';
|
||||||
part 'route.dart';
|
part 'route.dart';
|
||||||
|
part 'routing_result.dart';
|
||||||
|
|
||||||
final RegExp _param = new RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?');
|
final RegExp _param = new RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?');
|
||||||
final RegExp _rgxEnd = new RegExp(r'\$+$');
|
final RegExp _rgxEnd = new RegExp(r'\$+$');
|
||||||
|
@ -14,23 +15,27 @@ final RegExp _slashDollar = new RegExp(r'/+\$');
|
||||||
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
|
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
|
||||||
|
|
||||||
/// An abstraction over complex [Route] trees. Use this instead of the raw API. :)
|
/// An abstraction over complex [Route] trees. Use this instead of the raw API. :)
|
||||||
class Router extends Extensible {
|
class Router {
|
||||||
Route _root;
|
final List _middleware = [];
|
||||||
|
final Map<Pattern, Router> _mounted = {};
|
||||||
|
final List<Route> _routes = [];
|
||||||
|
|
||||||
/// Set to `true` to print verbose debug output when interacting with this route.
|
/// Set to `true` to print verbose debug output when interacting with this route.
|
||||||
bool debug = false;
|
bool debug = false;
|
||||||
|
|
||||||
|
List get middleware => new List.unmodifiable(_middleware);
|
||||||
|
|
||||||
|
Map<Pattern, Router> get mounted =>
|
||||||
|
new Map<Pattern, Router>.unmodifiable(_mounted);
|
||||||
|
|
||||||
/// Additional filters to be run on designated requests.
|
/// Additional filters to be run on designated requests.
|
||||||
Map<String, dynamic> requestMiddleware = {};
|
Map<String, dynamic> requestMiddleware = {};
|
||||||
|
|
||||||
/// The single [Route] that serves as the root of the hierarchy.
|
List<Route> get routes => new List<Route>.unmodifiable(_routes);
|
||||||
Route get root => _root;
|
|
||||||
|
|
||||||
/// Provide a `root` to make this Router revolve around a pre-defined route.
|
/// Provide a `root` to make this Router revolve around a pre-defined route.
|
||||||
/// Not recommended.
|
/// Not recommended.
|
||||||
Router({this.debug: false, Route root}) {
|
Router({this.debug: false});
|
||||||
_root = (_root = root ?? new _RootRoute())..debug = debug;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _printDebug(msg) {
|
void _printDebug(msg) {
|
||||||
if (debug == true) print(msg);
|
if (debug == true) print(msg);
|
||||||
|
@ -40,142 +45,79 @@ class Router extends Extensible {
|
||||||
/// for requests with the given method (case-insensitive).
|
/// for requests with the given method (case-insensitive).
|
||||||
/// Provide '*' as the method to respond to all methods.
|
/// Provide '*' as the method to respond to all methods.
|
||||||
Route addRoute(String method, Pattern path, Object handler,
|
Route addRoute(String method, Pattern path, Object handler,
|
||||||
{List middleware}) {
|
{List middleware: const []}) {
|
||||||
List handlers = [];
|
// Check if any mounted routers can match this
|
||||||
|
final handlers = [handler];
|
||||||
|
|
||||||
handlers
|
if (middleware != null) handlers.addAll(middleware);
|
||||||
..addAll(middleware ?? [])
|
|
||||||
..add(handler);
|
|
||||||
|
|
||||||
if (path is RegExp) {
|
final route =
|
||||||
return root.child(path, debug: debug, handlers: handlers, method: method);
|
new Route(path, debug: debug, method: method, handlers: handlers);
|
||||||
} else {
|
_routes.add(route);
|
||||||
// if (path.toString().replaceAll(_straySlashes, '').isEmpty || true) {
|
return route;
|
||||||
return root.child(path.toString(),
|
|
||||||
debug: debug, handlers: handlers, method: method);
|
|
||||||
}
|
|
||||||
/* else {
|
|
||||||
var segments = path
|
|
||||||
.toString()
|
|
||||||
.split('/')
|
|
||||||
.where((str) => str.isNotEmpty)
|
|
||||||
.toList(growable: false);
|
|
||||||
Route result;
|
|
||||||
|
|
||||||
if (segments.isEmpty) {
|
|
||||||
return new Route('/', debug: debug, handlers: handlers, method: method)
|
|
||||||
..debug = debug;
|
|
||||||
} else {
|
|
||||||
result = resolveOnRoot(segments[0],
|
|
||||||
filter: (route) => route.method == method || route.method == '*');
|
|
||||||
|
|
||||||
if (result != null) {
|
|
||||||
if (segments.length > 1) {
|
|
||||||
_printDebug('Resolved: ${result} for "${segments[0]}"');
|
|
||||||
segments = segments.skip(1).toList(growable: false);
|
|
||||||
|
|
||||||
Route existing;
|
|
||||||
|
|
||||||
do {
|
|
||||||
existing = result.resolve(segments[0],
|
|
||||||
filter: (route) =>
|
|
||||||
route.method == method || route.method == '*');
|
|
||||||
|
|
||||||
if (existing != null) {
|
|
||||||
result = existing;
|
|
||||||
}
|
|
||||||
} while (existing != null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < segments.length; i++) {
|
|
||||||
final segment = segments[i];
|
|
||||||
|
|
||||||
if (i == segments.length - 1) {
|
|
||||||
if (result == null) {
|
|
||||||
result = root.child(segment,
|
|
||||||
debug: debug, handlers: handlers, method: method);
|
|
||||||
} else {
|
|
||||||
result = result.child(segment,
|
|
||||||
debug: debug, handlers: handlers, method: method);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (result == null) {
|
|
||||||
result = root.child(segment, debug: debug, method: "*");
|
|
||||||
} else {
|
|
||||||
result = result.child(segment, debug: debug, method: "*");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result..debug = debug;
|
|
||||||
} */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a [Router] with a duplicated version of this tree.
|
/// Returns a [Router] with a duplicated version of this tree.
|
||||||
Router clone({bool normalize: true}) {
|
Router clone() {
|
||||||
final router = new Router(debug: debug);
|
final router = new Router(debug: debug);
|
||||||
|
final newMounted = new Map.from(mounted);
|
||||||
|
|
||||||
_copy(Route route, Route parent) {
|
for (Route route in routes) {
|
||||||
final r = route.clone();
|
if (route is! SymlinkRoute) {
|
||||||
parent._children.add(r.._parent = parent);
|
router._routes.add(route.clone());
|
||||||
|
} else if (route is SymlinkRoute) {
|
||||||
route.children.forEach((child) => _copy(child, r));
|
router._routes.add(new SymlinkRoute(route.path, route.pattern,
|
||||||
|
newMounted[route.pattern] = route.router.clone()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
root.children.forEach((child) => _copy(child, router.root));
|
return router.._mounted.addAll(newMounted);
|
||||||
|
|
||||||
if (normalize) router.normalize();
|
|
||||||
|
|
||||||
return router;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a visual representation of the route hierarchy and
|
/// Creates a visual representation of the route hierarchy and
|
||||||
/// passes it to a callback. If none is provided, `print` is called.
|
/// passes it to a callback. If none is provided, `print` is called.
|
||||||
void dumpTree(
|
void dumpTree(
|
||||||
{callback(String tree),
|
{callback(String tree),
|
||||||
header: 'Dumping route tree:',
|
String header: 'Dumping route tree:',
|
||||||
tab: ' ',
|
String tab: ' ',
|
||||||
showMatchers: false}) {
|
bool showMatchers: false}) {
|
||||||
var tabs = 0;
|
|
||||||
final buf = new StringBuffer();
|
final buf = new StringBuffer();
|
||||||
|
int tabs = 0;
|
||||||
|
|
||||||
void dumpRoute(Route route, {Pattern replace: null}) {
|
if (header != null && header.isNotEmpty) {
|
||||||
for (var i = 0; i < tabs; i++) buf.write(tab);
|
buf.writeln(header);
|
||||||
|
|
||||||
if (route == root)
|
|
||||||
buf.writeln('(root)');
|
|
||||||
else {
|
|
||||||
buf.write('- ${route.method} ');
|
|
||||||
|
|
||||||
var p =
|
|
||||||
replace != null ? route.path.replaceAll(replace, '') : route.path;
|
|
||||||
p = p.replaceAll(_straySlashes, '');
|
|
||||||
|
|
||||||
if (p.isEmpty)
|
|
||||||
buf.write("'/'");
|
|
||||||
else
|
|
||||||
buf.write("'${p.replaceAll(_straySlashes, '')}'");
|
|
||||||
|
|
||||||
if (showMatchers) {
|
|
||||||
buf.write(' (matcher: ${route.matcher.pattern})');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (route.handlers.isNotEmpty)
|
indent() {
|
||||||
buf.writeln(' => ${route.handlers.length} handler(s)');
|
for (int i = 0; i < tabs; i++) buf.write(tab);
|
||||||
else
|
|
||||||
buf.writeln();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dumpRouter(Router router) {
|
||||||
|
indent();
|
||||||
|
buf.writeln('- <root>');
|
||||||
tabs++;
|
tabs++;
|
||||||
route.children.forEach((r) => dumpRoute(r, replace: route.path));
|
|
||||||
|
for (Route route in router.routes) {
|
||||||
|
indent();
|
||||||
|
buf.write('- ${route.path.isNotEmpty ? route.path : '/'}');
|
||||||
|
|
||||||
|
if (route is SymlinkRoute) {
|
||||||
|
buf.writeln();
|
||||||
|
tabs++;
|
||||||
|
dumpRouter(route.router);
|
||||||
|
tabs--;
|
||||||
|
} else {
|
||||||
|
if (showMatchers) buf.write(' (${route.matcher.pattern})');
|
||||||
|
|
||||||
|
buf.writeln(' => ${route.handlers.length} handler(s)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tabs--;
|
tabs--;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (header != null && header.isNotEmpty) buf.writeln(header);
|
dumpRouter(this);
|
||||||
|
|
||||||
dumpRoute(root);
|
|
||||||
(callback ?? print)(buf.toString());
|
(callback ?? print)(buf.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,26 +126,104 @@ class Router extends Extensible {
|
||||||
///
|
///
|
||||||
/// Returns the created route.
|
/// Returns the created route.
|
||||||
/// You can also register middleware within the router.
|
/// You can also register middleware within the router.
|
||||||
Route group(Pattern path, void callback(Router router),
|
SymlinkRoute group(Pattern path, void callback(Router router),
|
||||||
{Iterable middleware: const [],
|
{Iterable middleware: const [],
|
||||||
String method: "*",
|
|
||||||
String name: null,
|
String name: null,
|
||||||
String namespace: null}) {
|
String namespace: null}) {
|
||||||
final route =
|
final router = new Router(debug: debug).._middleware.addAll(middleware);
|
||||||
root.child(path, handlers: middleware, method: method, name: name);
|
|
||||||
final router = new Router(root: route);
|
|
||||||
callback(router);
|
callback(router);
|
||||||
|
|
||||||
// Let's copy middleware, heeding the optional middleware namespace.
|
return mount(path, router, namespace: namespace).._name = name;
|
||||||
String middlewarePrefix = namespace != null ? "$namespace." : "";
|
|
||||||
|
|
||||||
Map copiedMiddleware = new Map.from(router.requestMiddleware);
|
|
||||||
for (String middlewareName in copiedMiddleware.keys) {
|
|
||||||
requestMiddleware["$middlewarePrefix$middlewareName"] =
|
|
||||||
copiedMiddleware[middlewareName];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return route;
|
/// Generates a URI string based on the given input.
|
||||||
|
/// Handy when you have named routes.
|
||||||
|
///
|
||||||
|
/// Each item in `linkParams` should be a [Route],
|
||||||
|
/// `String` or `Map<String, dynamic>`.
|
||||||
|
///
|
||||||
|
/// Strings should be route names, namespaces, or paths.
|
||||||
|
/// Maps should be parameters, which will be filled
|
||||||
|
/// into the previous route.
|
||||||
|
///
|
||||||
|
/// Paths and segments should correspond to the way
|
||||||
|
/// you declared them.
|
||||||
|
///
|
||||||
|
/// For example, if you declared a route group on
|
||||||
|
/// `'users/:id'`, it would not be resolved if you
|
||||||
|
/// passed `'users'` in [linkParams].
|
||||||
|
///
|
||||||
|
/// Leading and trailing slashes are automatically
|
||||||
|
/// removed.
|
||||||
|
///
|
||||||
|
/// Set [absolute] to `true` to insert a forward slash
|
||||||
|
/// before the generated path.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// ```dart
|
||||||
|
/// router.navigate(['users/:id', {'id': '1337'}, 'profile']);
|
||||||
|
/// ```
|
||||||
|
String navigate(List linkParams, {bool absolute: true}) {
|
||||||
|
final List<String> segments = [];
|
||||||
|
Router search = this;
|
||||||
|
Route lastRoute;
|
||||||
|
|
||||||
|
for (final param in linkParams) {
|
||||||
|
bool resolved = false;
|
||||||
|
|
||||||
|
if (param is String) {
|
||||||
|
// Search by name
|
||||||
|
for (Route route in search.routes) {
|
||||||
|
if (route.name == param) {
|
||||||
|
segments.add(route.path.replaceAll(_straySlashes, ''));
|
||||||
|
lastRoute = route;
|
||||||
|
|
||||||
|
if (route is SymlinkRoute) {
|
||||||
|
search = route.router;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search by path
|
||||||
|
for (Route route in search.routes) {
|
||||||
|
if (route.match(param) != null) {
|
||||||
|
segments.add(route.path.replaceAll(_straySlashes, ''));
|
||||||
|
lastRoute = route;
|
||||||
|
|
||||||
|
if (route is SymlinkRoute) {
|
||||||
|
search = route.router;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resolved) {
|
||||||
|
throw new RoutingException(
|
||||||
|
'Cannot resolve route for link param "$param".');
|
||||||
|
}
|
||||||
|
} else if (param is Route) {
|
||||||
|
segments.add(param.path.replaceAll(_straySlashes, ''));
|
||||||
|
} else if (param is Map<String, dynamic>) {
|
||||||
|
if (lastRoute == null) {
|
||||||
|
throw new RoutingException(
|
||||||
|
'Maps in link params must be preceded by a Route or String.');
|
||||||
|
} else {
|
||||||
|
segments.removeLast();
|
||||||
|
segments.add(lastRoute.makeUri(param).replaceAll(_straySlashes, ''));
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
throw new RoutingException(
|
||||||
|
'Link param $param is not Route, String, or Map<String, dynamic>.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return absolute
|
||||||
|
? '/${segments.join('/').replaceAll(_straySlashes, '')}'
|
||||||
|
: segments.join('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Assigns a middleware to a name for convenience.
|
/// Assigns a middleware to a name for convenience.
|
||||||
|
@ -211,188 +231,68 @@ class Router extends Extensible {
|
||||||
requestMiddleware[name] = middleware;
|
requestMiddleware[name] = middleware;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finds the first [Route] that matches the given path.
|
|
||||||
///
|
|
||||||
/// You can pass an additional filter to determine which
|
|
||||||
/// routes count as matches.
|
|
||||||
Route resolveOnRoot(String path, {bool filter(Route route)}) =>
|
|
||||||
root.resolve(path, filter: filter);
|
|
||||||
|
|
||||||
/// Finds the first [Route] that matches the given path,
|
/// Finds the first [Route] that matches the given path,
|
||||||
/// with the given method.
|
/// with the given method.
|
||||||
Route resolve(String path, {String method: 'GET'}) {
|
RoutingResult resolve(String fullPath, String path, {String method: 'GET'}) {
|
||||||
final String _path = path.replaceAll(_straySlashes, '');
|
final cleanFullPath = fullPath.replaceAll(_straySlashes, '');
|
||||||
final segments = _path.split('/').where((str) => str.isNotEmpty);
|
final cleanPath = path.replaceAll(_straySlashes, '');
|
||||||
_printDebug('Segments: $segments');
|
|
||||||
return _resolve(root, _path, method, segments.isNotEmpty ? segments.first : '', segments.skip(1));
|
for (Route route in routes) {
|
||||||
|
if (route is SymlinkRoute && route._head != null) {
|
||||||
|
final match = route._head.firstMatch(cleanFullPath);
|
||||||
|
|
||||||
|
if (match != null) {
|
||||||
|
final tail = cleanPath
|
||||||
|
.replaceFirst(match[0], '')
|
||||||
|
.replaceAll(_straySlashes, '');
|
||||||
|
_printDebug('Matched head "${match[0]}" to $route. Tail: "$tail"');
|
||||||
|
final nested =
|
||||||
|
route.router.resolve(cleanFullPath, tail, method: method);
|
||||||
|
return new RoutingResult(
|
||||||
|
match: match,
|
||||||
|
nested: nested,
|
||||||
|
params: route.parseParameters(cleanPath),
|
||||||
|
sourceRoute: route,
|
||||||
|
sourceRouter: this,
|
||||||
|
tail: tail);
|
||||||
|
}
|
||||||
|
} else if (route.method == '*' || route.method == method) {
|
||||||
|
final match = route.match(cleanPath);
|
||||||
|
|
||||||
|
if (match != null) {
|
||||||
|
return new RoutingResult(
|
||||||
|
match: match,
|
||||||
|
params: route.parseParameters(cleanPath),
|
||||||
|
sourceRoute: route,
|
||||||
|
sourceRouter: this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finds every possible [Route] that matches the given path,
|
/// Finds every possible [Route] that matches the given path,
|
||||||
/// with the given method.
|
/// with the given method.
|
||||||
///
|
Iterable<RoutingResult> resolveAll(String fullPath, String path,
|
||||||
/// This is preferable to [resolve].
|
{String method: 'GET'}) {
|
||||||
/// Keep in mind that this function uses either a [linearClone] or a [clone], and thus
|
final router = clone();
|
||||||
/// will not return the same exact routes from the original tree.
|
final List<RoutingResult> results = [];
|
||||||
Iterable<Route> resolveAll(String path,
|
var result = router.resolve(fullPath, path, method: method);
|
||||||
{bool linear: true, String method: 'GET', bool normalizeClone: true}) {
|
|
||||||
final router = linear
|
|
||||||
? linearClone(normalize: normalizeClone)
|
|
||||||
: clone(normalize: normalizeClone);
|
|
||||||
final routes = [];
|
|
||||||
var resolved = router.resolve(path, method: method);
|
|
||||||
|
|
||||||
while (resolved != null) {
|
while (result != null) {
|
||||||
try {
|
results.add(result);
|
||||||
routes.add(resolved);
|
result.deepestRouter._routes.remove(result.deepestRoute);
|
||||||
router.root._children.remove(resolved);
|
result = router.resolve(fullPath, path, method: method);
|
||||||
|
|
||||||
resolved = router.resolve(path, method: method);
|
|
||||||
} catch (e) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return routes.where((route) => route != null);
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
_validHead(RegExp rgx) {
|
_validHead(RegExp rgx) {
|
||||||
return !rgx.hasMatch('');
|
return !rgx.hasMatch('');
|
||||||
}
|
}
|
||||||
|
|
||||||
_resolve(Route ref, String fullPath, String method, String head,
|
|
||||||
Iterable<String> tail) {
|
|
||||||
_printDebug('$method $fullPath on $ref: head: $head, tail: ${tail.join(
|
|
||||||
'/')}');
|
|
||||||
|
|
||||||
// Does the index route match?
|
|
||||||
if (ref.matcher.hasMatch(fullPath)) {
|
|
||||||
final index = ref.indexRoute;
|
|
||||||
|
|
||||||
for (Route child in ref.allIndices) {
|
|
||||||
_printDebug('Possible index: $child');
|
|
||||||
|
|
||||||
if (child == child.indexRoute && ['*', method].contains(child.method)) {
|
|
||||||
_printDebug('Possible index was exact match: $child');
|
|
||||||
return child;
|
|
||||||
}
|
|
||||||
|
|
||||||
final resolved = _resolve(child, fullPath, method, head, tail);
|
|
||||||
|
|
||||||
if (resolved != null) {
|
|
||||||
_printDebug('Resolved from possible index: $resolved');
|
|
||||||
return resolved;
|
|
||||||
} else
|
|
||||||
_printDebug('Possible index returned null: $child');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (['*', method].contains(index.method)) {
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Now, let's check if any route's head matches the
|
|
||||||
// given head. If so, we try to resolve with that
|
|
||||||
// given head. If so, we try to resolve with that
|
|
||||||
// route, using a head corresponding to the one we
|
|
||||||
// matched.
|
|
||||||
for (Route child in ref.children) {
|
|
||||||
if (child._head != null &&
|
|
||||||
child._head.hasMatch(fullPath) &&
|
|
||||||
_validHead(child._head)) {
|
|
||||||
final newHead = child._head
|
|
||||||
.firstMatch(fullPath)
|
|
||||||
.group(0)
|
|
||||||
.replaceAll(_straySlashes, '');
|
|
||||||
final newTail = fullPath
|
|
||||||
.replaceAll(child._head, '')
|
|
||||||
.replaceAll(_straySlashes, '')
|
|
||||||
.split('/')
|
|
||||||
.where((str) => str.isNotEmpty);
|
|
||||||
final resolved = _resolve(child, fullPath, method, newHead, newTail);
|
|
||||||
|
|
||||||
if (resolved != null) {
|
|
||||||
_printDebug(
|
|
||||||
'Head match: $resolved from head: ${child._head.pattern}');
|
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
} else if (child._head != null) {
|
|
||||||
_printDebug(
|
|
||||||
'Head ${child._head.pattern} on $child failed to match $fullPath');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to match children by full path
|
|
||||||
for (Route child in ref.children) {
|
|
||||||
if (child.matcher.hasMatch(fullPath)) {
|
|
||||||
final resolved = _resolve(child, fullPath, method, head, tail);
|
|
||||||
|
|
||||||
if (resolved != null) {
|
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_printDebug(
|
|
||||||
'Could not match full path $fullPath to matcher ${child.matcher.pattern}.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tail.isEmpty)
|
|
||||||
return null;
|
|
||||||
else {
|
|
||||||
return _resolve(
|
|
||||||
ref, fullPath, method, head + '/' + tail.first, tail.skip(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Flattens the route tree into a linear list, in-place.
|
|
||||||
void flatten() {
|
|
||||||
_root = linearClone().root;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a [Router] with a linear version of this tree.
|
|
||||||
Router linearClone({bool normalize: true}) {
|
|
||||||
final router = new Router(debug: debug);
|
|
||||||
|
|
||||||
if (normalize) this.normalize();
|
|
||||||
|
|
||||||
_flatten(Route parent, Route route) {
|
|
||||||
// if (route.children.isNotEmpty && route.method == '*') return;
|
|
||||||
|
|
||||||
final r = new Route._base();
|
|
||||||
|
|
||||||
r
|
|
||||||
.._handlers.addAll(route.handlerSequence)
|
|
||||||
.._head = route._head
|
|
||||||
.._matcher = route.matcher
|
|
||||||
.._method = route.method
|
|
||||||
.._name = route.name
|
|
||||||
.._parent = route.parent // router.root
|
|
||||||
.._path = route.path;
|
|
||||||
|
|
||||||
// New matcher
|
|
||||||
final part1 = parent.matcher.pattern
|
|
||||||
.replaceAll(_rgxStart, '')
|
|
||||||
.replaceAll(_rgxEnd, '')
|
|
||||||
.replaceAll(_rgxStraySlashes, '')
|
|
||||||
.replaceAll(_straySlashes, '');
|
|
||||||
final part2 = route.matcher.pattern
|
|
||||||
.replaceAll(_rgxStart, '')
|
|
||||||
.replaceAll(_rgxEnd, '')
|
|
||||||
.replaceAll(_rgxStraySlashes, '')
|
|
||||||
.replaceAll(_straySlashes, '');
|
|
||||||
|
|
||||||
final m = '$part1\\/$part2'.replaceAll(_rgxStraySlashes, '');
|
|
||||||
|
|
||||||
// r._matcher = new RegExp('^$m\$');
|
|
||||||
_printDebug('Matcher of flattened route: ${r.matcher.pattern}');
|
|
||||||
|
|
||||||
router.root._children.add(r);
|
|
||||||
route.children.forEach((child) => _flatten(route, child));
|
|
||||||
}
|
|
||||||
|
|
||||||
root._children.forEach((child) => _flatten(root, child));
|
|
||||||
return router;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Incorporates another [Router]'s routes into this one's.
|
/// Incorporates another [Router]'s routes into this one's.
|
||||||
///
|
///
|
||||||
/// If `hooked` is set to `true` and a [Service] is provided,
|
/// If `hooked` is set to `true` and a [Service] is provided,
|
||||||
|
@ -403,7 +303,7 @@ class Router extends Extensible {
|
||||||
/// For example, if the [Router] has a middleware 'y', and the `namespace`
|
/// For example, if the [Router] has a middleware 'y', and the `namespace`
|
||||||
/// is 'x', then that middleware will be available as 'x.y' in the main router.
|
/// is 'x', then that middleware will be available as 'x.y' in the main router.
|
||||||
/// These namespaces can be nested.
|
/// These namespaces can be nested.
|
||||||
void mount(Pattern path, Router router,
|
SymlinkRoute mount(Pattern path, Router router,
|
||||||
{bool hooked: true, String namespace: null}) {
|
{bool hooked: true, String namespace: null}) {
|
||||||
// Let's copy middleware, heeding the optional middleware namespace.
|
// Let's copy middleware, heeding the optional middleware namespace.
|
||||||
String middlewarePrefix = namespace != null ? "$namespace." : "";
|
String middlewarePrefix = namespace != null ? "$namespace." : "";
|
||||||
|
@ -414,91 +314,11 @@ class Router extends Extensible {
|
||||||
copiedMiddleware[middlewareName];
|
copiedMiddleware[middlewareName];
|
||||||
}
|
}
|
||||||
|
|
||||||
// final route = root.addChild(router.root, join: false);
|
final route = new SymlinkRoute(path, path, _mounted[path] = router);
|
||||||
final route = root.child(path, debug: debug).addChild(router.root);
|
_routes.add(route);
|
||||||
route.debug = debug;
|
route._head = new RegExp(route.matcher.pattern.replaceAll(_rgxEnd, ''));
|
||||||
|
|
||||||
if (path is! RegExp) {
|
return route.._name = namespace;
|
||||||
// Correct mounted path manually...
|
|
||||||
final clean = route.matcher.pattern
|
|
||||||
.replaceAll(_rgxStart, '')
|
|
||||||
.replaceAll(_rgxEnd, '')
|
|
||||||
.replaceAll(_rgxStraySlashes, '');
|
|
||||||
route._matcher = new RegExp('^$clean\$');
|
|
||||||
|
|
||||||
final _path = path.toString().replaceAll(_straySlashes, '');
|
|
||||||
|
|
||||||
_migrateRoute(Route r) {
|
|
||||||
r._path = '$_path/${r.path}'.replaceAll(_straySlashes, '');
|
|
||||||
var m = r.matcher.pattern
|
|
||||||
.replaceAll(_rgxStart, '')
|
|
||||||
.replaceAll(_rgxEnd, '')
|
|
||||||
.replaceAll(_rgxStraySlashes, '')
|
|
||||||
.replaceAll(_straySlashes, '');
|
|
||||||
|
|
||||||
final m1 = _matcherify(_path)
|
|
||||||
.replaceAll(_rgxStart, '')
|
|
||||||
.replaceAll(_rgxEnd, '')
|
|
||||||
.replaceAll(_rgxStraySlashes, '')
|
|
||||||
.replaceAll(_straySlashes, '');
|
|
||||||
|
|
||||||
m = '$m1/$m'
|
|
||||||
.replaceAll(_rgxStraySlashes, '')
|
|
||||||
.replaceAll(_straySlashes, '');
|
|
||||||
|
|
||||||
r._matcher = new RegExp('^$m\$');
|
|
||||||
_printDebug(
|
|
||||||
'New matcher on route in mounted router: ${r.matcher.pattern}');
|
|
||||||
|
|
||||||
if (r._head != null) {
|
|
||||||
final head = r._head.pattern
|
|
||||||
.replaceAll(_rgxStart, '')
|
|
||||||
.replaceAll(_rgxEnd, '')
|
|
||||||
.replaceAll(_rgxStraySlashes, '')
|
|
||||||
.replaceAll('\\/', '/')
|
|
||||||
.replaceAll(_straySlashes, '');
|
|
||||||
r._head = new RegExp(_matcherify('$_path/$head')
|
|
||||||
.replaceAll(_rgxEnd, '')
|
|
||||||
.replaceAll(_rgxStraySlashes, ''));
|
|
||||||
_printDebug('Head of migrated route: ${r._head.pattern}');
|
|
||||||
}
|
|
||||||
|
|
||||||
r.children.forEach(_migrateRoute);
|
|
||||||
}
|
|
||||||
|
|
||||||
route.children.forEach(_migrateRoute);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Removes empty routes that could complicate route resolution.
|
|
||||||
void normalize() {
|
|
||||||
_printDebug('Normalizing route tree...');
|
|
||||||
|
|
||||||
_normalize(Route route, int index) {
|
|
||||||
var merge = route.path.replaceAll(_straySlashes, '').isEmpty &&
|
|
||||||
route.children.isNotEmpty;
|
|
||||||
merge = merge || route.children.length == 1;
|
|
||||||
|
|
||||||
if (merge) {
|
|
||||||
_printDebug('Erasing this route: $route');
|
|
||||||
// route.parent._handlers.addAll(route.handlers);
|
|
||||||
|
|
||||||
for (Route child in route.children) {
|
|
||||||
route.parent._children.insert(index, child.._parent = route.parent);
|
|
||||||
child._handlers.insertAll(0, route.handlers);
|
|
||||||
}
|
|
||||||
|
|
||||||
route.parent._children.remove(route);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < route.children.length; i++) {
|
|
||||||
_normalize(route.children[i], i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < root.children.length; i++) {
|
|
||||||
_normalize(root.children[i], i);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a route that responds to any request matching the given path.
|
/// Adds a route that responds to any request matching the given path.
|
||||||
|
@ -541,10 +361,3 @@ class Router extends Extensible {
|
||||||
return addRoute('PUT', path, handler, middleware: middleware);
|
return addRoute('PUT', path, handler, middleware: middleware);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RootRoute extends Route {
|
|
||||||
_RootRoute() : super("/", method: '*', name: "<root>");
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => "ROOT";
|
|
||||||
}
|
|
||||||
|
|
47
lib/src/routing_result.dart
Normal file
47
lib/src/routing_result.dart
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
part of angel_route.src.router;
|
||||||
|
|
||||||
|
class RoutingResult {
|
||||||
|
final Match match;
|
||||||
|
final RoutingResult nested;
|
||||||
|
final Map<String, dynamic> params = {};
|
||||||
|
final Route sourceRoute;
|
||||||
|
final Router sourceRouter;
|
||||||
|
final String tail;
|
||||||
|
|
||||||
|
RoutingResult get deepest {
|
||||||
|
var search = this;
|
||||||
|
|
||||||
|
while (search.nested != null) search = search.nested;
|
||||||
|
|
||||||
|
return search;
|
||||||
|
}
|
||||||
|
|
||||||
|
Route get deepestRoute => deepest.sourceRoute;
|
||||||
|
Router get deepestRouter => deepest.sourceRouter;
|
||||||
|
|
||||||
|
List get handlers {
|
||||||
|
return []..addAll(sourceRouter.middleware)..addAll(sourceRoute.handlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
List get allHandlers {
|
||||||
|
final handlers = [];
|
||||||
|
var search = this;
|
||||||
|
|
||||||
|
while (search != null) {
|
||||||
|
handlers.addAll(search.handlers);
|
||||||
|
search = search.nested;
|
||||||
|
}
|
||||||
|
|
||||||
|
return handlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
RoutingResult(
|
||||||
|
{this.match,
|
||||||
|
Map<String, dynamic> params: const {},
|
||||||
|
this.nested,
|
||||||
|
this.sourceRoute,
|
||||||
|
this.sourceRouter,
|
||||||
|
this.tail}) {
|
||||||
|
this.params.addAll(params ?? {});
|
||||||
|
}
|
||||||
|
}
|
10
lib/src/symlink_route.dart
Normal file
10
lib/src/symlink_route.dart
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
part of angel_route.src.router;
|
||||||
|
|
||||||
|
/// Placeholder [Route] to serve as a symbolic link
|
||||||
|
/// to a mounted [Router].
|
||||||
|
class SymlinkRoute extends Route {
|
||||||
|
final Pattern pattern;
|
||||||
|
final Router router;
|
||||||
|
|
||||||
|
SymlinkRoute(Pattern path, this.pattern, this.router) : super(path);
|
||||||
|
}
|
|
@ -1,8 +0,0 @@
|
||||||
import 'package:test/test.dart';
|
|
||||||
import 'route/all_test.dart' as route;
|
|
||||||
import 'router/all_test.dart' as router;
|
|
||||||
|
|
||||||
main() {
|
|
||||||
group('route', route.main);
|
|
||||||
group('router', router.main);
|
|
||||||
}
|
|
|
@ -1,177 +0,0 @@
|
||||||
import 'dart:io';
|
|
||||||
import 'package:angel_route/angel_route.dart';
|
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
main() {
|
|
||||||
http.Client client;
|
|
||||||
Router router;
|
|
||||||
HttpServer server;
|
|
||||||
String url;
|
|
||||||
|
|
||||||
setUp(() async {
|
|
||||||
client = new http.Client();
|
|
||||||
router = new Router();
|
|
||||||
server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 0);
|
|
||||||
url = 'http://${server.address.address}:${server.port}';
|
|
||||||
|
|
||||||
router.get('/hello', (req) {
|
|
||||||
req.response.write('world');
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/sandwich', (req) {
|
|
||||||
req.response.write('pb');
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
router.all('/sandwich', (req) {
|
|
||||||
req.response.write('&j');
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
router.all('/chain', (req) {
|
|
||||||
req.response.write('PassTo');
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
router.group('/group/:id', (router) {
|
|
||||||
router.get('/fun', (req) {
|
|
||||||
req.response.write(' and fun!');
|
|
||||||
return false;
|
|
||||||
}, middleware: [
|
|
||||||
(req) {
|
|
||||||
req.response.write(' is cool');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}, middleware: [
|
|
||||||
(req) {
|
|
||||||
req.response.write('Dart');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
final beatles = new Router();
|
|
||||||
|
|
||||||
beatles.get('/come-together', (req) {
|
|
||||||
req.response.write('spinal');
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
beatles.all('*', (req) {
|
|
||||||
req.response.write('-clacker');
|
|
||||||
return !req.uri.toString().contains('come-together');
|
|
||||||
});
|
|
||||||
|
|
||||||
router.mount('/beatles', beatles);
|
|
||||||
|
|
||||||
router.all('*', (req) {
|
|
||||||
req.response.write('Fallback');
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
router
|
|
||||||
..normalize()
|
|
||||||
..dumpTree(showMatchers: true);
|
|
||||||
|
|
||||||
server.listen((request) async {
|
|
||||||
final resolved =
|
|
||||||
router.resolveAll(request.uri.path, method: request.method);
|
|
||||||
|
|
||||||
if (resolved.isEmpty) {
|
|
||||||
request.response.statusCode = 404;
|
|
||||||
request.response.write('404 Not Found');
|
|
||||||
await request.response.close();
|
|
||||||
} else {
|
|
||||||
print('Resolved ${request.uri} => $resolved');
|
|
||||||
|
|
||||||
// Easy middleware pipeline
|
|
||||||
final pipeline = [];
|
|
||||||
|
|
||||||
for (Route route in resolved) {
|
|
||||||
pipeline.addAll(route.handlerSequence);
|
|
||||||
}
|
|
||||||
|
|
||||||
print('Pipeline: ${pipeline.length} handler(s)');
|
|
||||||
|
|
||||||
for (final handler in pipeline) {
|
|
||||||
if (handler(request) != true) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
await request.response.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tearDown(() async {
|
|
||||||
client.close();
|
|
||||||
client = null;
|
|
||||||
router = null;
|
|
||||||
url = null;
|
|
||||||
await server.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('hello', () async {
|
|
||||||
final response = await client.get('$url/hello');
|
|
||||||
print('Response: ${response.body}');
|
|
||||||
expect(response.body, equals('world'));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('sandwich', () async {
|
|
||||||
final response = await client.get('$url/sandwich');
|
|
||||||
print('Response: ${response.body}');
|
|
||||||
expect(response.body, equals('pb&j'));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('chain', () async {
|
|
||||||
final response = await client.get('$url/chain');
|
|
||||||
print('Response: ${response.body}');
|
|
||||||
expect(response.body, equals('PassToFallback'));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('fallback', () async {
|
|
||||||
final response = await client.get('$url/fallback');
|
|
||||||
print('Response: ${response.body}');
|
|
||||||
expect(response.body, equals('Fallback'));
|
|
||||||
});
|
|
||||||
|
|
||||||
group('group', () {
|
|
||||||
test('fun', () async {
|
|
||||||
final response = await client.get('$url/group/abc/fun');
|
|
||||||
print('Response: ${response.body}');
|
|
||||||
expect(response.body, equals('Dart is cool and fun!'));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('fallback', () async {
|
|
||||||
final response = await client.get('$url/group/abc');
|
|
||||||
print('Response: ${response.body}');
|
|
||||||
expect(response.body, equals('Fallback'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('beatles', () {
|
|
||||||
test('spinal clacker', () async {
|
|
||||||
final response = await client.get('$url/beatles/come-together');
|
|
||||||
print('Response: ${response.body}');
|
|
||||||
expect(response.body, equals('spinal-clacker'));
|
|
||||||
});
|
|
||||||
|
|
||||||
group('fallback', () {
|
|
||||||
setUp(() {
|
|
||||||
router.linearClone().dumpTree(header: 'LINEAR', showMatchers: true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('non-existent', () async {
|
|
||||||
var response = await client.get('$url/beatles/ringo-starr');
|
|
||||||
print('Response: ${response.body}');
|
|
||||||
expect(response.body, equals('-clackerFallback'));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('root', () async {
|
|
||||||
var response = await client.get('$url/beatles');
|
|
||||||
print('Response: ${response.body}');
|
|
||||||
expect(response.body, equals('Fallback'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,123 +0,0 @@
|
||||||
import 'package:angel_route/angel_route.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
main() {
|
|
||||||
var router = new Router(debug: true);
|
|
||||||
final getFoo = router.get('/foo', 'GET');
|
|
||||||
final postFoo = router.post('/foo', 'POST');
|
|
||||||
|
|
||||||
Route getFooBar, postFooBar, patchFooBarId;
|
|
||||||
|
|
||||||
router.group('/foo/bar', (router) {
|
|
||||||
getFooBar = router.get('/', 'GET');
|
|
||||||
postFooBar = router.post('/', 'POST');
|
|
||||||
patchFooBarId = router.patch('/:id([0-9]+)', 'PATCH');
|
|
||||||
});
|
|
||||||
|
|
||||||
final Router books = new Router();
|
|
||||||
|
|
||||||
final getBooks = books.get('/', 'GET');
|
|
||||||
final postBooks = books.post('/', 'POST');
|
|
||||||
final getBooksFoo = books.get('/foo', 'GET');
|
|
||||||
final postBooksFoo = books.post('/foo', 'POST');
|
|
||||||
|
|
||||||
Route getBooksChapters,
|
|
||||||
postBooksChapters,
|
|
||||||
getBooksChaptersReviews,
|
|
||||||
postBooksChaptersReviews;
|
|
||||||
|
|
||||||
books.group('/:id/chapters', (router) {
|
|
||||||
getBooksChapters = router.get('/', 'GET');
|
|
||||||
postBooksChapters = router.post('/', 'POST');
|
|
||||||
|
|
||||||
router.group('/:id([A-Za-z]+)/reviews', (router) {
|
|
||||||
getBooksChaptersReviews = router.get('/', 'GET');
|
|
||||||
postBooksChaptersReviews = router.post('/', 'POST');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.mount('/books', books);
|
|
||||||
router.normalize();
|
|
||||||
|
|
||||||
group('top level', () {
|
|
||||||
test('get', () => expect(router.resolve('/foo'), equals(getFoo)));
|
|
||||||
|
|
||||||
test('post', () {
|
|
||||||
router.dumpTree();
|
|
||||||
expect(router.resolve('/foo', method: 'POST'), equals(postFoo));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('group', () {
|
|
||||||
test('get', () {
|
|
||||||
expect(router.resolve('/foo/bar'), equals(getFooBar));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('post', () {
|
|
||||||
expect(router.resolve('/foo/bar', method: 'POST'), equals(postFooBar));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('patch+id', () {
|
|
||||||
router.dumpTree();
|
|
||||||
expect(
|
|
||||||
router.resolve('/foo/bar/2', method: 'PATCH'), equals(patchFooBarId));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('404', () {
|
|
||||||
expect(router.resolve('/foo/bar/A', method: 'PATCH'), isNull);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('mount', () {
|
|
||||||
group('no params', () {
|
|
||||||
test('get', () {
|
|
||||||
expect(router.resolve('/books'), equals(getBooks));
|
|
||||||
expect(router.resolve('/books/foo'), equals(getBooksFoo));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('post', () {
|
|
||||||
expect(router.resolve('/books', method: 'POST'), equals(postBooks));
|
|
||||||
expect(
|
|
||||||
router.resolve('/books/foo', method: 'POST'), equals(postBooksFoo));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('with params', () {
|
|
||||||
test('1 param', () {
|
|
||||||
expect(router.resolve('/books/abc/chapters'), equals(getBooksChapters));
|
|
||||||
expect(router.resolve('/books/abc/chapters', method: 'POST'),
|
|
||||||
equals(postBooksChapters));
|
|
||||||
});
|
|
||||||
|
|
||||||
group('2 params', () {
|
|
||||||
setUp(router.dumpTree);
|
|
||||||
|
|
||||||
test('get', () {
|
|
||||||
expect(router.resolve('/books/abc/chapters/ABC/reviews'),
|
|
||||||
equals(getBooksChaptersReviews));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('post', () {
|
|
||||||
expect(
|
|
||||||
router.resolve('/books/abc/chapters/ABC/reviews', method: 'POST'),
|
|
||||||
equals(postBooksChaptersReviews));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('404', () {
|
|
||||||
expect(router.resolve('/books/abc/chapters/1'), isNull);
|
|
||||||
expect(router.resolve('/books/abc/chapters/12'), isNull);
|
|
||||||
expect(router.resolve('/books/abc/chapters/13.!'), isNull);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('flatten', () {
|
|
||||||
router.dumpTree(header: 'BEFORE FLATTENING:');
|
|
||||||
final flat = router..flatten();
|
|
||||||
|
|
||||||
for (Route route in flat.root.children) {
|
|
||||||
print('${route.method} ${route.path} => ${route.matcher.pattern}');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
44
test/navigate_test.dart
Normal file
44
test/navigate_test.dart
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import 'package:angel_route/angel_route.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
main() {
|
||||||
|
final router = new Router();
|
||||||
|
|
||||||
|
router.get('/', 'GET').as('root');
|
||||||
|
router.get('/user/:id', 'GET');
|
||||||
|
router.get('/first/:first/last/:last', 'GET').as('full_name');
|
||||||
|
|
||||||
|
navigate(params) {
|
||||||
|
final uri = router.navigate(params);
|
||||||
|
print('Uri: $uri');
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.dumpTree(showMatchers: true);
|
||||||
|
|
||||||
|
group('top-level', () {
|
||||||
|
test('named', () {
|
||||||
|
expect(navigate(['root']), equals('/'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('params', () {
|
||||||
|
expect(
|
||||||
|
navigate([
|
||||||
|
'user/:id',
|
||||||
|
{'id': 1337}
|
||||||
|
]),
|
||||||
|
equals('/user/1337'));
|
||||||
|
|
||||||
|
expect(
|
||||||
|
navigate([
|
||||||
|
'full_name',
|
||||||
|
{'first': 'John', 'last': 'Smith'}
|
||||||
|
]),
|
||||||
|
equals('/first/John/last/Smith'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('root', () {
|
||||||
|
expect(navigate(['/']), equals('/'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,10 +0,0 @@
|
||||||
import 'package:test/test.dart';
|
|
||||||
import 'no_params.dart' as no_params;
|
|
||||||
import 'parse_params.dart' as parse_params;
|
|
||||||
import 'with_params.dart' as with_params;
|
|
||||||
|
|
||||||
main() {
|
|
||||||
group('parse params', parse_params.main);
|
|
||||||
group('no params', no_params.main);
|
|
||||||
group('with params', with_params.main);
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
import 'package:angel_route/angel_route.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
main() {
|
|
||||||
final foo = new Route.build('/foo', handlers: ['bar']);
|
|
||||||
final bar = foo.child('/bar');
|
|
||||||
final baz = bar.child('//////baz//////', handlers: ['hello', 'world']);
|
|
||||||
|
|
||||||
test('matching', () {
|
|
||||||
expect(foo.children.length, equals(1));
|
|
||||||
expect(foo.handlers.length, equals(1));
|
|
||||||
expect(foo.handlerSequence.length, equals(1));
|
|
||||||
expect(foo.path, equals('foo'));
|
|
||||||
expect(foo.match('/foo'), isNotNull);
|
|
||||||
expect(foo.match('/bar'), isNull);
|
|
||||||
expect(foo.match('/foolish'), isNull);
|
|
||||||
expect(foo.parent, isNull);
|
|
||||||
expect(foo.absoluteParent, equals(foo));
|
|
||||||
|
|
||||||
expect(bar.path, equals('foo/bar'));
|
|
||||||
expect(bar.children.length, equals(1));
|
|
||||||
expect(bar.handlers, isEmpty);
|
|
||||||
expect(bar.handlerSequence.length, equals(1));
|
|
||||||
expect(bar.match('/foo/bar'), isNotNull);
|
|
||||||
expect(bar.match('/bar'), isNull);
|
|
||||||
expect(bar.match('/foo/bar/2'), isNull);
|
|
||||||
expect(bar.parent, equals(foo));
|
|
||||||
expect(baz.absoluteParent, equals(foo));
|
|
||||||
|
|
||||||
expect(baz.children, isEmpty);
|
|
||||||
expect(baz.handlers.length, equals(2));
|
|
||||||
expect(baz.handlerSequence.length, equals(3));
|
|
||||||
expect(baz.path, equals('foo/bar/baz'));
|
|
||||||
expect(baz.match('/foo/bar/baz'), isNotNull);
|
|
||||||
expect(baz.match('/foo/bat/baz'), isNull);
|
|
||||||
expect(baz.match('/foo/bar/baz/1'), isNull);
|
|
||||||
expect(baz.parent, equals(bar));
|
|
||||||
expect(baz.absoluteParent, equals(foo));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('hierarchy', () {
|
|
||||||
expect(foo.resolve('bar'), equals(bar));
|
|
||||||
expect(foo.resolve('bar/baz'), equals(baz));
|
|
||||||
|
|
||||||
expect(bar.resolve('..'), equals(foo));
|
|
||||||
expect(bar.resolve('/bar/baz'), equals(baz));
|
|
||||||
expect(bar.resolve('../bar'), equals(bar));
|
|
||||||
|
|
||||||
expect(baz.resolve('..'), equals(bar));
|
|
||||||
expect(baz.resolve('../..'), equals(foo));
|
|
||||||
expect(baz.resolve('../baz'), equals(baz));
|
|
||||||
expect(baz.resolve('../../bar'), equals(bar));
|
|
||||||
expect(baz.resolve('../../bar/baz'), equals(baz));
|
|
||||||
expect(baz.resolve('/bar'), equals(bar));
|
|
||||||
expect(baz.resolve('/bar/baz'), equals(baz));
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
import 'package:angel_route/angel_route.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
p(x) {
|
|
||||||
print(x);
|
|
||||||
return x;
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
final foo = new Route('/foo/:id');
|
|
||||||
final bar = foo.child('/bar/:barId/baz');
|
|
||||||
|
|
||||||
test('make uri', () {
|
|
||||||
expect(p(foo.makeUri({'id': 1337})), equals('foo/1337'));
|
|
||||||
expect(p(bar.makeUri({'id': 1337, 'barId': 12})),
|
|
||||||
equals('foo/1337/bar/12/baz'));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('parse', () {
|
|
||||||
final fooParams = foo.parseParameters('foo/1337/////');
|
|
||||||
expect(p(fooParams), equals({'id': 1337}));
|
|
||||||
|
|
||||||
final barParams = bar.parseParameters('/////foo/1337/bar/12/baz');
|
|
||||||
expect(p(barParams), equals({'id': 1337, 'barId': 12}));
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
import 'package:angel_route/angel_route.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
main() {
|
|
||||||
final foo = new Router().root;
|
|
||||||
final fooById = foo.child(':id((?!bar)[0-9]+)', handlers: ['bar']);
|
|
||||||
final bar = fooById.child('bar');
|
|
||||||
final baz = bar.child('//////baz//////', handlers: ['hello', 'world']);
|
|
||||||
final bazById = baz.child(':bazId([A-Za-z]+)?');
|
|
||||||
new Router(root: foo).dumpTree();
|
|
||||||
|
|
||||||
test('matching', () {
|
|
||||||
expect(fooById.children.length, equals(1));
|
|
||||||
expect(fooById.handlers.length, equals(1));
|
|
||||||
expect(fooById.handlerSequence.length, equals(1));
|
|
||||||
expect(fooById.path, equals(':id'));
|
|
||||||
expect(fooById.match('/2'), isNotNull);
|
|
||||||
expect(fooById.match('/aaa'), isNull);
|
|
||||||
expect(fooById.match('/bar'), isNull);
|
|
||||||
expect(fooById.match('lish'), isNull);
|
|
||||||
expect(fooById.parent, equals(foo));
|
|
||||||
expect(fooById.absoluteParent, equals(foo));
|
|
||||||
|
|
||||||
expect(bar.path, equals(':id/bar'));
|
|
||||||
expect(bar.children.length, equals(1));
|
|
||||||
expect(bar.handlers, isEmpty);
|
|
||||||
expect(bar.handlerSequence.length, equals(1));
|
|
||||||
expect(bar.match('/2/bar'), isNotNull);
|
|
||||||
expect(bar.match('/bar'), isNull);
|
|
||||||
expect(bar.match('/a/bar'), isNull);
|
|
||||||
expect(bar.parent, equals(fooById));
|
|
||||||
expect(baz.absoluteParent, equals(foo));
|
|
||||||
|
|
||||||
expect(baz.children.length, equals(1));
|
|
||||||
expect(baz.handlers.length, equals(2));
|
|
||||||
expect(baz.handlerSequence.length, equals(3));
|
|
||||||
expect(baz.path, equals(':id/bar/baz'));
|
|
||||||
expect(baz.match('/2A/bar/baz'), isNull);
|
|
||||||
expect(baz.match('/2/bar/baz'), isNotNull);
|
|
||||||
expect(baz.match('/1337/bar/baz'), isNotNull);
|
|
||||||
expect(baz.match('/bat/baz'), isNull);
|
|
||||||
expect(baz.match('/bar/baz/1'), isNull);
|
|
||||||
expect(baz.parent, equals(bar));
|
|
||||||
expect(baz.absoluteParent, equals(foo));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('hierarchy', () {
|
|
||||||
expect(fooById.resolve('/2'), equals(fooById));
|
|
||||||
|
|
||||||
expect(fooById.resolve('/2/bar'), equals(bar));
|
|
||||||
expect(fooById.resolve('/bar'), isNull);
|
|
||||||
expect(fooById.resolve('/a/bar'), isNull);
|
|
||||||
expect(fooById.resolve('1337/bar/baz'), equals(baz));
|
|
||||||
|
|
||||||
expect(bar.resolve('..'), equals(fooById));
|
|
||||||
|
|
||||||
new Router(root: bar.parent).dumpTree(header: "POOP");
|
|
||||||
expect(bar.parent.resolve('bar/baz'), equals(baz));
|
|
||||||
expect(bar.resolve('/2/bar/baz'), equals(baz));
|
|
||||||
expect(bar.resolve('../bar'), equals(bar));
|
|
||||||
|
|
||||||
expect(baz.resolve('..'), equals(bar));
|
|
||||||
expect(baz.resolve('../..'), equals(fooById));
|
|
||||||
expect(baz.resolve('../baz'), equals(baz));
|
|
||||||
expect(baz.resolve('../../bar'), equals(bar));
|
|
||||||
expect(baz.resolve('../../bar/baz'), equals(baz));
|
|
||||||
expect(baz.resolve('/2/bar'), equals(bar));
|
|
||||||
expect(baz.resolve('/1337/bar/baz'), equals(baz));
|
|
||||||
|
|
||||||
expect(bar.resolve('/2/bar/baz/e'), equals(bazById));
|
|
||||||
expect(bar.resolve('baz/e'), equals(bazById));
|
|
||||||
expect(fooById.resolve('/2/bar/baz/e'), equals(bazById));
|
|
||||||
expect(fooById.resolve('/2/bar/baz/2'), isNull);
|
|
||||||
expect(fooById.resolve('/2a/bar/baz/e'), isNull);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
import 'package:angel_route/angel_route.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
import 'fallback.dart' as fallback;
|
|
||||||
|
|
||||||
final ABC = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
|
||||||
|
|
||||||
main() {
|
|
||||||
final router = new Router();
|
|
||||||
final indexRoute = router.get('/', () => ':)');
|
|
||||||
final fizz = router.post('/user/fizz', null);
|
|
||||||
final deleteUserById =
|
|
||||||
router.delete('/user/:id/detail', (id) => num.parse(id));
|
|
||||||
|
|
||||||
Route lower;
|
|
||||||
|
|
||||||
router.group('/letter///', (router) {
|
|
||||||
lower = router
|
|
||||||
.get('/:id([A-Za-z])', (id) => ABC.indexOf(id[0]))
|
|
||||||
.child('////lower', handlers: [(String id) => id.toLowerCase()[0]]);
|
|
||||||
|
|
||||||
lower.parent
|
|
||||||
.child('/upper', handlers: [(String id) => id.toUpperCase()[0]]);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.dumpTree(header: "ROUTER TESTS");
|
|
||||||
|
|
||||||
test('extensible', () {
|
|
||||||
router['two'] = 2;
|
|
||||||
expect(router.two, equals(2));
|
|
||||||
});
|
|
||||||
|
|
||||||
group('fallback', fallback.main);
|
|
||||||
|
|
||||||
test('hierarchy', () {
|
|
||||||
expect(lower.absoluteParent, equals(router.root));
|
|
||||||
expect(lower.parent.path, equals('letter/:id'));
|
|
||||||
expect(lower.resolve('../upper').path, equals('letter/:id/upper'));
|
|
||||||
expect(lower.resolve('/user/34/detail'), equals(deleteUserById));
|
|
||||||
expect(deleteUserById.resolve('../../fizz'), equals(fizz));
|
|
||||||
}, skip: 'Hierarchy is deprecated.');
|
|
||||||
|
|
||||||
test('resolve', () {
|
|
||||||
expect(router.resolveOnRoot('/'), equals(indexRoute));
|
|
||||||
expect(router.resolveOnRoot('user/1337/detail'), equals(deleteUserById));
|
|
||||||
expect(router.resolveOnRoot('/user/1337/detail'), equals(deleteUserById));
|
|
||||||
expect(router.resolveOnRoot('letter/a/lower'), equals(lower));
|
|
||||||
expect(router.resolveOnRoot('letter/2/lower'), isNull);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
import 'package:angel_route/angel_route.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
bool checkPost(Route route) => route.method == "POST";
|
|
||||||
|
|
||||||
main() {
|
|
||||||
final router = new Router();
|
|
||||||
|
|
||||||
final userById = router.group('/user', (router) {
|
|
||||||
router.get('/:id', (id) => 'User $id');
|
|
||||||
}).resolve(':id');
|
|
||||||
|
|
||||||
final fallback = router.get('*', () => 'fallback');
|
|
||||||
|
|
||||||
test('resolve', () {
|
|
||||||
expect(router.resolveOnRoot('/foo'), equals(fallback));
|
|
||||||
expect(router.resolveOnRoot('/user/:id'), equals(userById));
|
|
||||||
expect(router.resolveOnRoot('/user/:id', filter: checkPost), isNull);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,110 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:math' as math;
|
|
||||||
import 'dart:io';
|
|
||||||
import 'package:angel_route/angel_route.dart';
|
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
typedef Future<bool> RequestHandler(HttpRequest request);
|
|
||||||
|
|
||||||
final String MIDDLEWARE_GREETING = 'Hi, I am a middleware!';
|
|
||||||
|
|
||||||
main() {
|
|
||||||
http.Client client;
|
|
||||||
Router router;
|
|
||||||
HttpServer server;
|
|
||||||
String url;
|
|
||||||
|
|
||||||
setUp(() async {
|
|
||||||
client = new http.Client();
|
|
||||||
router = new Router(debug: true);
|
|
||||||
server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 0);
|
|
||||||
url = 'http://${server.address.address}:${server.port}';
|
|
||||||
|
|
||||||
server.listen((request) async {
|
|
||||||
final resolved = router.resolve(request.uri.path, method: request.method);
|
|
||||||
|
|
||||||
if (resolved == null) {
|
|
||||||
request.response.statusCode = 404;
|
|
||||||
request.response.write('404 Not Found');
|
|
||||||
await request.response.close();
|
|
||||||
} else {
|
|
||||||
// Easy middleware pipeline
|
|
||||||
for (final handler in resolved.handlerSequence) {
|
|
||||||
if (handler is String) {
|
|
||||||
if (!await router.requestMiddleware[handler](request)) break;
|
|
||||||
} else if (!await handler(request)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await request.response.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('foo', (HttpRequest request) async {
|
|
||||||
request.response.write('bar');
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
Route square;
|
|
||||||
|
|
||||||
square = router.post('square/:num([0-9]+)', (HttpRequest request) async {
|
|
||||||
final params = square.parseParameters(request.uri.toString());
|
|
||||||
final squared = math.pow(params['num'], 2);
|
|
||||||
request.response.statusCode = squared;
|
|
||||||
request.response.write(squared);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
router.group('todos', (router) {
|
|
||||||
router.get('/', (HttpRequest request) async {
|
|
||||||
print('TODO INDEX???');
|
|
||||||
request.response.write([]);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}, middleware: [
|
|
||||||
(HttpRequest request) async {
|
|
||||||
request.response.write(MIDDLEWARE_GREETING);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
router.dumpTree();
|
|
||||||
});
|
|
||||||
|
|
||||||
tearDown(() async {
|
|
||||||
client.close();
|
|
||||||
client = null;
|
|
||||||
router = null;
|
|
||||||
url = null;
|
|
||||||
await server.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
group('group', () {
|
|
||||||
test('todo index', () async {
|
|
||||||
final response = await client.get('$url/todos');
|
|
||||||
expect(response.statusCode, equals(200));
|
|
||||||
expect(response.body, equals('$MIDDLEWARE_GREETING[]'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('top-level route', () {
|
|
||||||
test('no params', () async {
|
|
||||||
final response = await client.get('$url/foo');
|
|
||||||
expect(response.statusCode, equals(200));
|
|
||||||
expect(response.body, equals('bar'));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('with params', () async {
|
|
||||||
final response = await client.post('$url/square/16');
|
|
||||||
expect(response.statusCode, equals(256));
|
|
||||||
expect(response.body, equals(response.statusCode.toString()));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('throw 404', () async {
|
|
||||||
final response = await client.get('$url/abc');
|
|
||||||
expect(response.statusCode, equals(404));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
126
test/server_test.dart
Normal file
126
test/server_test.dart
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:angel_route/angel_route.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
main() {
|
||||||
|
http.Client client;
|
||||||
|
final people = [
|
||||||
|
{'name': 'John Smith'}
|
||||||
|
];
|
||||||
|
final Router router = new Router(debug: true);
|
||||||
|
HttpServer server;
|
||||||
|
String url;
|
||||||
|
|
||||||
|
router.get('/', (req, res) {
|
||||||
|
res.write('Root');
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/hello', (req, res) {
|
||||||
|
res.write('World');
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
router.group('/people', (router) {
|
||||||
|
router.get('/', (req, res) {
|
||||||
|
res.write(JSON.encode(people));
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
router.group('/:id', (router) {
|
||||||
|
router.get('/', (req, res) {
|
||||||
|
// In a real application, we would take the param,
|
||||||
|
// but not here...
|
||||||
|
res.write(JSON.encode(people.first));
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/name', (req, res) {
|
||||||
|
// In a real application, we would take the param,
|
||||||
|
// but not here...
|
||||||
|
res.write(JSON.encode(people.first['name']));
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
client = new http.Client();
|
||||||
|
|
||||||
|
router.dumpTree();
|
||||||
|
server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 0);
|
||||||
|
url = 'http://${server.address.address}:${server.port}';
|
||||||
|
|
||||||
|
server.listen((req) async {
|
||||||
|
final res = req.response;
|
||||||
|
|
||||||
|
// Easy middleware pipeline
|
||||||
|
final results = router.resolveAll(req.uri.toString(), req.uri.toString(),
|
||||||
|
method: req.method);
|
||||||
|
final pipeline = new MiddlewarePipeline(results);
|
||||||
|
|
||||||
|
if (pipeline.handlers.isEmpty) {
|
||||||
|
res
|
||||||
|
..statusCode = HttpStatus.NOT_FOUND
|
||||||
|
..writeln('404 Not Found');
|
||||||
|
} else {
|
||||||
|
for (final handler in pipeline.handlers) {
|
||||||
|
if (!await handler(req, res)) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await res.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await server.close(force: true);
|
||||||
|
client.close();
|
||||||
|
client = null;
|
||||||
|
url = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
group('top-level', () {
|
||||||
|
group('get', () {
|
||||||
|
test('root', () async {
|
||||||
|
final res = await client.get(url);
|
||||||
|
print('Response: ${res.body}');
|
||||||
|
expect(res.body, equals('Root'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('path', () async {
|
||||||
|
final res = await client.get('$url/hello');
|
||||||
|
print('Response: ${res.body}');
|
||||||
|
expect(res.body, equals('World'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('group', () {
|
||||||
|
group('top-level', () {
|
||||||
|
test('root', () async {
|
||||||
|
final res = await client.get('$url/people');
|
||||||
|
print('Response: ${res.body}');
|
||||||
|
expect(JSON.decode(res.body), equals(people));
|
||||||
|
});
|
||||||
|
|
||||||
|
group('param', () {
|
||||||
|
test('root', () async {
|
||||||
|
final res = await client.get('$url/people/0');
|
||||||
|
print('Response: ${res.body}');
|
||||||
|
expect(JSON.decode(res.body), equals(people.first));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('path', () async {
|
||||||
|
final res = await client.get('$url/people/0/name');
|
||||||
|
print('Response: ${res.body}');
|
||||||
|
expect(JSON.decode(res.body), equals(people.first['name']));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('use', () {});
|
||||||
|
}
|
|
@ -1,35 +0,0 @@
|
||||||
import 'package:angel_route/angel_route.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
final String ARTIFICIAL_INDEX = 'artificial index';
|
|
||||||
|
|
||||||
tattle(x) => 'This ${x.runtimeType}.debug = ${x.debug}';
|
|
||||||
tattleAll(x) => x.map(tattle).join('\n');
|
|
||||||
|
|
||||||
main() {
|
|
||||||
final parent = new Router(debug: true);
|
|
||||||
final child = new Router(debug: true);
|
|
||||||
Route a, b, c;
|
|
||||||
|
|
||||||
a = child.get('a', ['c']);
|
|
||||||
child.group('b', (router) {
|
|
||||||
b = router.get('/', ARTIFICIAL_INDEX);
|
|
||||||
c = router.post('c', 'Hello nested');
|
|
||||||
});
|
|
||||||
|
|
||||||
parent.mount('child', child);
|
|
||||||
parent.dumpTree(header: tattleAll([parent, child, a]));
|
|
||||||
|
|
||||||
group('no params', () {
|
|
||||||
test('resolve', () {
|
|
||||||
expect(child.resolveOnRoot('a'), equals(a));
|
|
||||||
expect(child.resolveOnRoot('b'), equals(b));
|
|
||||||
expect(child.resolveOnRoot('b/c'), equals(c));
|
|
||||||
|
|
||||||
expect(parent.resolveOnRoot('child/a'), equals(a));
|
|
||||||
expect(parent.resolveOnRoot('a'), isNull);
|
|
||||||
expect(parent.resolveOnRoot('child/b'), equals(b));
|
|
||||||
expect(parent.resolveOnRoot('child/b/c'), equals(c));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
import 'package:angel_route/browser.dart';
|
|
||||||
import '../shared/basic.dart';
|
|
||||||
|
|
||||||
main() => basic(new BrowserRouter(hash: true));
|
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport"
|
|
||||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
|
||||||
<title>Hash Router</title>
|
|
||||||
<style>
|
|
||||||
#routes li {
|
|
||||||
display: inline;
|
|
||||||
margin-right: 1em;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<ul id="routes">
|
|
||||||
<li><a href="/a">Route A</a></li>
|
|
||||||
<li><a href="/b">Route B</a></li>
|
|
||||||
<li><a href="/b/a">Route B/A</a></li>
|
|
||||||
<li><a href="/b/b">Route B/B</a></li>
|
|
||||||
<li><a href="/c">Route C</a></li>
|
|
||||||
</ul>
|
|
||||||
<h1>No Active Route</h1>
|
|
||||||
<i>Handler Sequence:</i>
|
|
||||||
<ul id="handlers">
|
|
||||||
<li>(empty)</li>
|
|
||||||
</ul>
|
|
||||||
<script src="basic.dart" type="application/dart"></script>
|
|
||||||
<script src="packages/browser/dart.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,5 +0,0 @@
|
||||||
import 'package:angel_route/browser.dart';
|
|
||||||
import '../shared/basic.dart';
|
|
||||||
|
|
||||||
main() => basic(new BrowserRouter());
|
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport"
|
|
||||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
|
||||||
<title>Push State Router</title>
|
|
||||||
<style>
|
|
||||||
#routes li {
|
|
||||||
display: inline;
|
|
||||||
margin-right: 1em;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<ul id="routes">
|
|
||||||
<li><a href="/a">Route A</a></li>
|
|
||||||
<li><a href="/b">Route B</a></li>
|
|
||||||
<li><a href="/b/a">Route B/A</a></li>
|
|
||||||
<li><a href="/b/b">Route B/B</a></li>
|
|
||||||
<li><a href="/c">Route C</a></li>
|
|
||||||
</ul>
|
|
||||||
<h1>No Active Route</h1>
|
|
||||||
<i>Handler Sequence:</i>
|
|
||||||
<ul id="handlers">
|
|
||||||
<li>(empty)</li>
|
|
||||||
</ul>
|
|
||||||
<script src="basic.dart" type="application/dart"></script>
|
|
||||||
<script src="packages/browser/dart.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,34 +0,0 @@
|
||||||
import 'dart:html';
|
|
||||||
import 'package:angel_route/browser.dart';
|
|
||||||
|
|
||||||
basic(BrowserRouter router) {
|
|
||||||
final $h1 = window.document.querySelector('h1');
|
|
||||||
final $ul = window.document.getElementById('handlers');
|
|
||||||
|
|
||||||
router.onRoute.listen((route) {
|
|
||||||
if (route == null) {
|
|
||||||
$h1.text = 'No Active Route';
|
|
||||||
$ul.children
|
|
||||||
..clear()
|
|
||||||
..add(new LIElement()..text = '(empty)');
|
|
||||||
} else {
|
|
||||||
$h1.text = 'Active Route: ${route.path}';
|
|
||||||
$ul.children
|
|
||||||
..clear()
|
|
||||||
..addAll(route.handlerSequence
|
|
||||||
.map((handler) => new LIElement()..text = handler.toString()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('a', 'a handler');
|
|
||||||
|
|
||||||
router.group('b', (router) {
|
|
||||||
print(router.root);
|
|
||||||
router.get('a', 'b/a handler');
|
|
||||||
router.get('b', 'b/b handler', middleware: ['b/b middleware']);
|
|
||||||
}, middleware: ['b middleware']);
|
|
||||||
|
|
||||||
router.get('c', 'c handler');
|
|
||||||
|
|
||||||
router.dumpTree();
|
|
||||||
}
|
|
Loading…
Reference in a new issue