1.0.0-dev

This commit is contained in:
thosakwe 2016-10-12 13:58:32 -04:00
parent 58d0c51c4e
commit 064c831db0
27 changed files with 1050 additions and 36 deletions

46
.gitignore vendored
View file

@ -25,5 +25,49 @@ doc/api/
# Don't commit pubspec lock file
# (Library packages only! Remove pattern if developing an application package)
pubspec.lock
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/workspace.xml
.idea/tasks.xml
.idea/dictionaries
.idea/vcs.xml
.idea/jsLibraryMappings.xml
# Sensitive or high-churn files:
.idea/dataSources.ids
.idea/dataSources.xml
.idea/dataSources.local.xml
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml
# Gradle:
.idea/gradle.xml
.idea/libraries
# Mongo Explorer plugin:
.idea/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
.idea

23
.idea/angel_route.iml Normal file
View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.pub" />
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/packages" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/test/packages" />
<excludeFolder url="file://$MODULE_DIR$/test/route/packages" />
<excludeFolder url="file://$MODULE_DIR$/test/router/packages" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
<excludeFolder url="file://$MODULE_DIR$/web/hash/packages" />
<excludeFolder url="file://$MODULE_DIR$/web/packages" />
<excludeFolder url="file://$MODULE_DIR$/web/push_state/packages" />
<excludeFolder url="file://$MODULE_DIR$/web/shared/packages" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="application" />
<orderEntry type="library" name="Dart Packages" level="project" />
</component>
</module>

38
.idea/misc.xml Normal file
View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
<component name="ProjectInspectionProfilesVisibleTreeState">
<entry key="Project Default">
<profile-state>
<expanded-state>
<State>
<id />
</State>
<State>
<id>General</id>
</State>
<State>
<id>XPath</id>
</State>
</expanded-state>
<selected-state>
<State>
<id>AngularJS</id>
</State>
</selected-state>
</profile-state>
</entry>
</component>
<component name="ProjectLevelVcsManager" settingsEditedManually="false">
<OptionsSetting value="true" id="Add" />
<OptionsSetting value="true" id="Remove" />
<OptionsSetting value="true" id="Checkout" />
<OptionsSetting value="true" id="Update" />
<OptionsSetting value="true" id="Status" />
<OptionsSetting value="true" id="Edit" />
<ConfirmationsSetting value="0" id="Add" />
<ConfirmationsSetting value="0" id="Remove" />
</component>
</project>

8
.idea/modules.xml Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/angel_route.iml" filepath="$PROJECT_DIR$/.idea/angel_route.iml" />
</modules>
</component>
</project>

View file

@ -0,0 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test/all_tests.dart" />
<method />
</configuration>
</component>

137
README.md
View file

@ -1,2 +1,137 @@
# angel_route
Advanced routing API, supports both server and browser.
A powerful, isomorphic routing library for Dart.
This API is a huge improvement over the original [Angel](https://github.com/angel-dart/angel)
routing system, and thus deserves to be its own individual project.
`angel_route` exposes a routing system that takes the shape of a tree. This tree structure
can be easily navigated, in a fashion somewhat similar to a filesystem. The `Router` API
is a very straightforward interface that allows for your code to take a shape similar to
the route tree. Users of Laravel and Express will be very happy.
`angel_route` does not require the use of [Angel](https://github.com/angel-dart/angel),
and has no dependencies. Thus, it can be used in any application, regardless of
framework. This includes Web apps, Flutter apps, CLI apps, and smaller servers which do
not need all the features of the Angel framework.
# Contents
* [Examples](#examples)
* [Routing](#routing)
* [Tree Hierarchy and Path Resolution](#hierarchy)
* [In the Browser](#in-the-browser)
* [Route State](#route-state)
* [Route Parameters](#route-parameters)
# Examples
## Routing
If you use [Angel](https://github.com/angel-dart/angel), every `Angel` instance is
a `Router` in itself.
```dart
main() {
final router = new Router();
router.get('/users', () {});
router.post('/users/:id/timeline', (String id) {});
router.get('/square_root/:id([0-9]+)', (String id) {
final n = num.parse(id);
return {'result': pow(n, 2) };
});
router.group('/show/:id', (router) {
router.get('/reviews', (id) {
return someQuery(id).reviews;
});
// Optionally restrict params to a RegExp
router.get('/reviews/:reviewId([A-Za-z0-9_]+)', (id, reviewId) {
return someQuery(id).reviews.firstWhere(
(r) => r.id == reviewId);
});
}, before: [put, middleware, here]);
}
```
The default `Router` does not give any notification of routes being changed, because
there is no inherent stream of URL's for it to listen to. This is good, because a server
needs a lot of flexibility with which to handle requests.
## Hierarchy
```dart
main() {
final foo = new Route('/');
final bar = foo.child('bar');
final baz = foo.child('baz');
final a = bar.child('a');
/*
* Relative paths:
* a.resolve('../baz') = baz;
* bar.resolve('a') = a;
*
* Absolute paths:
* a.resolve('/bar/a') = a;
*/
}
```
```dart
main() {
final router = new Router();
router.group('/user/:id', (router) {
router.get('/balance', (id) async {
final user = await someQuery(id);
return user.balance;
});
});
}
```
See [the tests](test/route/no_params.dart) for good examples.
# In the Browser
Supports both hashed routes and pushState. The `BrowserRouter` interface exposes
a `Stream<Route> onRoute`, which can be listened to for changes. It will fire `null`
whenever no route is matched.
```dart
main() {
}
```
`angel_route` will also automatically intercept `<a>` elements and redirect them to
your routes.
To prevent this for a given anchor, do any of the following:
* Do not provide an `href`
* Provide a `download` or `target` attribute on the element
* Set `rel="external"`
# Route State
Routes can have state via the `Extensible` class, which is a simple proxy over a `Map`.
This does not require reflection, and can be used in all Dart environments.
```dart
main() {
final router = new BrowserRouter();
// ..
router.onRoute.listen((route) {
if (route == null)
throw 404;
else route.state.foo = 'bar';
});
}
```
# Route Parameters
Routes can have parameters, as seen in the above examples.
If a parameter is a numeber, then it will automatically be parsed.

View file

@ -1,3 +1,5 @@
library angel_route;
export 'src/route.dart';
export 'src/route.dart';
export 'src/router.dart';
export 'src/routing_exception.dart';

123
lib/browser.dart Normal file
View file

@ -0,0 +1,123 @@
import 'dart:async' show Stream, StreamController;
import 'dart:convert' show JSON;
import 'dart:html' show AnchorElement, window;
import 'angel_route.dart';
final RegExp _hash = new RegExp(r'^#/');
abstract class BrowserRouter extends Router {
Stream<Route> get onRoute;
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);
void go(String path, [Map params]);
void goTo(Route route, [Map params]);
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) {
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);
}
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() {
window.onHashChange.listen((_) {
final path = window.location.hash.replaceAll(_hash, '');
final resolved = resolve(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() {
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);
});
}
}

31
lib/src/extensible.dart Normal file
View file

@ -0,0 +1,31 @@
final RegExp _equ = new RegExp(r'=$');
final RegExp _sym = new RegExp(r'Symbol\("([^"]+)"\)');
/// Supports accessing members of a Map as though they were actual members.
///
/// No longer requires reflection. :)
@proxy
class Extensible {
/// A set of custom properties that can be assigned to the server.
///
/// Useful for configuration and extension.
Map properties = {};
noSuchMethod(Invocation invocation) {
if (invocation.memberName != null) {
String name = _sym.firstMatch(invocation.memberName.toString()).group(1);
if (invocation.isMethod) {
return Function.apply(properties[name], invocation.positionalArguments,
invocation.namedArguments);
} else if (invocation.isGetter) {
return properties[name];
} else if (invocation.isSetter) {
return properties[name.replaceAll(_equ, '')] =
invocation.positionalArguments.first;
}
}
super.noSuchMethod(invocation);
}
}

View file

@ -1,66 +1,160 @@
final RegExp _rgxEnd = new RegExp(r'\$');
final RegExp _rgxStart = new RegExp(r'\^');
import 'extensible.dart';
import 'routing_exception.dart';
final RegExp _param = new RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?');
final RegExp _rgxEnd = new RegExp(r'\$+$');
final RegExp _rgxStart = new RegExp(r'^\^+');
final RegExp _rgxStraySlashes = new RegExp(r'(^((\\/)|(/))+)|(((\\/)|(/))+$)');
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
String _matcherify(String path, {bool expand: true}) {
var p = path.replaceAll(new RegExp(r'\/\*$'), "*").replaceAll('/', r'\/');
if (expand) {
var match = _param.firstMatch(p);
while (match != null) {
if (match.group(3) == null)
p = p.replaceAll(match[0], '([^\/]+)');
else
p = p.replaceAll(match[0], '(${match[3]})');
match = _param.firstMatch(p);
}
}
p = p.replaceAll(new RegExp('\\*'), '.*');
p = '^$p\$';
return p;
}
String _pathify(String path) {
var p = path.replaceAll(_straySlashes, '');
Map<String, String> replace = {};
for (Match match in _param.allMatches(p)) {
if (match[3] != null) replace[match[0]] = ':${match[1]}';
}
replace.forEach((k, v) {
p = p.replaceAll(k, v);
});
return p;
}
class Route {
final List<Route> _children = [];
final List _handlers = [];
RegExp _matcher;
String _name;
Route _parent;
String _path;
String _pathified;
RegExp _resolver;
List<Route> get children => new List.unmodifiable(_children);
List get handlers => new List.unmodifiable(_handlers);
RegExp get matcher => _matcher;
final String method;
final String name;
String get name => _name;
Route get parent => _parent;
String get path => _path;
final Extensible state = new Extensible();
Route get absoluteParent {
Route result = this;
while (result.parent != null) result = result.parent;
return result;
}
/// Backtracks up the hierarchy, and builds
/// a sequential list of all handlers from both
/// this route, and every found parent route.
///
/// The resulting list puts handlers from routes
/// higher in the tree at lower indices. Thus,
/// this can be used in a routing-enabled application
/// to evaluate multiple middleware on a single route,
/// and apply them to all children.
List get handlerSequence {
final result = [];
var r = this;
while (r != null) {
result.insertAll(0, r.handlers);
r = r.parent;
}
return result;
}
Route(Pattern path,
{Iterable<Route> children: const [],
Iterable handlers: const [],
this.method: "GET",
this.name: null}) {
String name: null}) {
if (children != null) _children.addAll(children);
if (handlers != null) _handlers.addAll(handlers);
_name = name;
if (path is RegExp) {
_matcher = path;
_path = path.pattern;
} else {
_matcher = new RegExp(_path = path
.toString()
.replaceAll(_straySlashes, '')
.replaceAll(new RegExp(r'\/\*$'), "*")
.replaceAll(new RegExp('\/'), r'\/')
.replaceAll(new RegExp(':[a-zA-Z_]+'), '([^\/]+)')
.replaceAll(new RegExp('\\*'), '.*'));
_matcher = new RegExp(
_matcherify(path.toString().replaceAll(_straySlashes, '')));
_path = _pathified = _pathify(path.toString());
_resolver = new RegExp(_matcherify(
path.toString().replaceAll(_straySlashes, ''),
expand: false));
}
}
factory Route.join(Route parent, Route child) {
final String path1 = parent.path.replaceAll(_straySlashes, '');
final String path2 = child.path.replaceAll(_straySlashes, '');
final String pattern1 = parent.matcher.pattern.replaceAll(_rgxEnd, '');
final String pattern2 = child.matcher.pattern.replaceAll(_rgxStart, '');
final String path1 = parent.path
.replaceAll(_rgxStart, '')
.replaceAll(_rgxEnd, '')
.replaceAll(_straySlashes, '');
final String path2 = child.path
.replaceAll(_rgxStart, '')
.replaceAll(_rgxEnd, '')
.replaceAll(_straySlashes, '');
final String pattern1 = parent.matcher.pattern
.replaceAll(_rgxEnd, '')
.replaceAll(_rgxStraySlashes, '');
final String pattern2 = child.matcher.pattern
.replaceAll(_rgxStart, '')
.replaceAll(_rgxStraySlashes, '');
final route = new Route(new RegExp('$pattern1/$pattern2'),
final route = new Route('$path1/$path2',
children: child.children,
handlers: child.handlers,
method: child.method,
name: child.name);
String separator = (pattern1.isEmpty || pattern1 == '^') ? '' : '\\/';
return route
..parent = parent
.._path = '$path1/$path2';
.._matcher = new RegExp('$pattern1$separator$pattern2')
.._parent = parent;
}
List<Route> addAll(Iterable<Route> routes, {bool join: true}) {
return routes.map((route) => addChild(route, join: join)).toList();
}
Route addChild(Route route, {bool join: true}) {
Route created = join ? new Route.join(this, route) : route;
Route created = join ? new Route.join(this, route) : route.._parent = this;
_children.add(created);
return created;
}
/// Assigns a name to this route.
Route as(String name) => this.._name = name;
Route child(Pattern path,
{Iterable<Route> children: const [],
Iterable handlers: const [],
@ -71,28 +165,109 @@ class Route {
return addChild(route);
}
Match match(String path) => matcher.firstMatch(path);
/// Generates a URI to this route with the given parameters.
String makeUri([Map<String, dynamic> params]) {
String result = _pathified;
if (params != null) {
for (String key in (params.keys)) {
result = result.replaceAll(
new RegExp(":$key" + r"\??"), params[key].toString());
}
}
Route resolve(String path) {
if (path.isEmpty ||
path == '.' ||
path.replaceAll(_straySlashes, '').isEmpty) {
return result.replaceAll("*", "");
}
Match match(String path) =>
matcher.firstMatch(path.replaceAll(_straySlashes, ''));
/// Extracts route parameters from a given path.
Map parseParameters(String requestPath) {
Map result = {};
Iterable<String> values = _parseParameters(requestPath.replaceAll(_straySlashes, ''));
Iterable<Match> matches = _param.allMatches(
_pathified.replaceAll(new RegExp('\/'), r'\/'));
for (int i = 0; i < matches.length; i++) {
Match match = matches.elementAt(i);
String paramName = match.group(1);
String value = values.elementAt(i);
num numValue = num.parse(value, (_) => double.NAN);
if (!numValue.isNaN)
result[paramName] = numValue;
else
result[paramName] = value;
}
return result;
}
_parseParameters(String requestPath) sync* {
Match routeMatch = matcher.firstMatch(requestPath);
for (int i = 1; i <= routeMatch.groupCount; i++)
yield routeMatch.group(i);
}
Route resolve(String path, [bool filter(Route route)]) {
final _filter = filter ?? (_) => true;
if (path.isEmpty || path == '.' && _filter(this)) {
return this;
} else if (path.replaceAll(_straySlashes, '').isEmpty) {
for (Route route in children) {
final stub = route.path.replaceAll(this.path, '');
if (stub == '/' || stub.isEmpty && _filter(route)) return route;
}
if (_filter(this))
return this;
else
return null;
} else if (path == '..') {
if (parent != null)
return parent;
else
throw new RoutingException.orphan();
} else if (path.startsWith('/') &&
path.length > 1 &&
path[1] != '/' &&
absoluteParent != null) {
return absoluteParent.resolve(path.substring(1), _filter);
} else {
final segments = path.split('/');
if (segments[0] == '..') {
if (parent != null)
return parent.resolve(segments.skip(1).join('/'), _filter);
else
throw new RoutingException.orphan();
}
for (Route route in children) {
final subPath = '${this.path}/${segments[0]}';
if (route.match(subPath) != null) {
if (segments.length == 1)
if (route.match(subPath) != null ||
route._resolver.firstMatch(subPath) != null) {
if (segments.length == 1 && _filter(route))
return route;
else {
return route.resolve(segments.skip(1).join('/'));
return route.resolve(segments.skip(1).join('/'), _filter);
}
}
}
// Try to match the whole route, if nothing else works
for (Route route in children) {
if ((route.match(path) != null ||
route._resolver.firstMatch(path) != null) &&
_filter(route))
return route;
else if ((route.match('/$path') != null ||
route._resolver.firstMatch('/$path') != null) &&
_filter(route)) return route;
}
return null;
}
}

168
lib/src/router.dart Normal file
View file

@ -0,0 +1,168 @@
import 'extensible.dart';
import 'route.dart';
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
class Router extends Extensible {
/// Additional filters to be run on designated requests.
Map<String, dynamic> requestMiddleware = {};
/// The single [Route] that serves as the root of the hierarchy.
final Route root;
Router([Route root]) : this.root = root ?? new Route('/', name: '<root>');
/// Adds a route that responds to the given path
/// for requests with the given method (case-insensitive).
/// Provide '*' as the method to respond to all methods.
Route addRoute(String method, Pattern path, Object handler,
{List middleware}) {
List handlers = [];
handlers
..addAll(middleware ?? [])
..add(handler);
return root.child(path, handlers: handlers, method: method);
}
/// Creates a visual representation of the route hierarchy and
/// passes it to a callback. If none is provided, `print` is called.
void dumpTree(
{callback(String tree), header: 'Dumping route tree:', tab: ' '}) {
var tabs = 0;
final buf = new StringBuffer();
void dumpRoute(Route route, {String replace: null}) {
for (var i = 0; i < tabs; i++) buf.write(tab);
buf.write('- ${route.method} ');
final p =
replace != null ? route.path.replaceAll(replace, '') : route.path;
if (p.isEmpty)
buf.write("'/'");
else
buf.write("'${p.replaceAll(_straySlashes, '')}'");
buf.write(' => ');
if (route.handlers.isNotEmpty)
buf.writeln('${route.handlers.length} handler(s)');
else
buf.writeln();
tabs++;
route.children.forEach((r) => dumpRoute(r, replace: route.path));
tabs--;
}
if (header != null && header.isNotEmpty) buf.writeln(header);
dumpRoute(root);
(callback ?? print)(buf);
}
/// Creates a route, and allows you to add child routes to it
/// via a [Router] instance.
///
/// Returns the created route.
/// You can also register middleware within the router.
Route group(Pattern path, void callback(Router router),
{Iterable middleware: const [],
String method: "*",
String name: null,
String namespace: null}) {
final route =
root.child(path, handlers: middleware, method: method, name: name);
final router = new Router(route);
callback(router);
// Let's copy middleware, heeding the optional middleware namespace.
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;
}
/// Assigns a middleware to a name for convenience.
registerMiddleware(String 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 resolve(String path, [bool filter(Route route)]) =>
root.resolve(path, filter);
/// Incorporates another [Router]'s routes into this one's.
///
/// If `hooked` is set to `true` and a [Service] is provided,
/// then that service will be wired to a [HookedService] proxy.
/// If a `namespace` is provided, then any middleware
/// from the provided [Router] will be prefixed by that namespace,
/// with a dot.
/// 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.
/// These namespaces can be nested.
void use(Pattern path, Router router,
{bool hooked: true, String namespace: null}) {
// Let's copy middleware, heeding the optional middleware namespace.
String middlewarePrefix = namespace != null ? "$namespace." : "";
Map copiedMiddleware = new Map.from(router.requestMiddleware);
for (String middlewareName in copiedMiddleware.keys) {
requestMiddleware["$middlewarePrefix$middlewareName"] =
copiedMiddleware[middlewareName];
}
root.addChild(router.root);
}
/// Adds a route that responds to any request matching the given path.
Route all(Pattern path, Object handler, {List middleware}) {
return addRoute('*', path, handler, middleware: middleware);
}
/// Adds a route that responds to a DELETE request.
Route delete(Pattern path, Object handler, {List middleware}) {
return addRoute('DELETE', path, handler, middleware: middleware);
}
/// Adds a route that responds to a GET request.
Route get(Pattern path, Object handler, {List middleware}) {
return addRoute('GET', path, handler, middleware: middleware);
}
/// Adds a route that responds to a HEAD request.
Route head(Pattern path, Object handler, {List middleware}) {
return addRoute('HEAD', path, handler, middleware: middleware);
}
/// Adds a route that responds to a OPTIONS request.
Route options(Pattern path, Object handler, {List middleware}) {
return addRoute('OPTIONS', path, handler, middleware: middleware);
}
/// Adds a route that responds to a POST request.
Route post(Pattern path, Object handler, {List middleware}) {
return addRoute('POST', path, handler, middleware: middleware);
}
/// Adds a route that responds to a PATCH request.
Route patch(Pattern path, Object handler, {List middleware}) {
return addRoute('PATCH', path, handler, middleware: middleware);
}
/// Adds a route that responds to a PUT request.
Route put(Pattern path, Object handler, {List middleware}) {
return addRoute('PUT', path, handler, middleware: middleware);
}
}

View file

@ -0,0 +1,14 @@
abstract class RoutingException extends Exception {
factory RoutingException(String message) => new _RoutingExceptionImpl(message);
factory RoutingException.orphan() => new _RoutingExceptionImpl("Tried to resolve path '..' on a route that has no parent.");
factory RoutingException.noSuchRoute(String path) => new _RoutingExceptionImpl("Tried to navigate to non-existent route: '$path'.");
}
class _RoutingExceptionImpl implements RoutingException {
final String message;
_RoutingExceptionImpl(this.message);
@override
String toString() => message;
}

View file

@ -1,7 +1,8 @@
name: angel_route
description: Advanced routing API, supports both server and browser.
description: A powerful, isomorphic routing library for Dart.
version: 1.0.0-dev
author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/angel_route
dev_dependencies:
browser: ">=0.10.0 < 0.11.0"
test: ">=0.12.15 <0.13.0"

View file

@ -1,8 +1,8 @@
import '../lib/angel_route.dart';
import 'package:test/test.dart';
import 'route/all_tests.dart' as route;
import 'router/all_tests.dart' as router;
main() {
final foo = new Route('/foo');
final bar = foo.child('/bar');
print(foo.path);
print(bar.path);
group('route', route.main);
group('router', router.main);
}

1
test/packages Symbolic link
View file

@ -0,0 +1 @@
../packages

10
test/route/all_tests.dart Normal file
View file

@ -0,0 +1,10 @@
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);
}

57
test/route/no_params.dart Normal file
View file

@ -0,0 +1,57 @@
import 'package:angel_route/angel_route.dart';
import 'package:test/test.dart';
main() {
final foo = new Route('/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
test/route/packages Symbolic link
View file

@ -0,0 +1 @@
../../packages

View file

@ -0,0 +1,26 @@
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}));
});
}

View file

@ -0,0 +1,3 @@
main() {
}

View file

@ -0,0 +1,42 @@
import 'package:angel_route/angel_route.dart';
import 'package:test/test.dart';
final ABC = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
main() {
final router = new Router();
final indexRoute = router.get('/', () => ':)');
final userById = router.delete('/user/:id/detail', (id) => num.parse(id));
Route lower;
final letters = 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();
test('extensible', () {
router.two = 2;
expect(router.properties['two'], equals(2));
});
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(userById));
});
test('resolve', () {
expect(router.resolve('/'), equals(indexRoute));
expect(router.resolve('user/1337/detail'), equals(userById));
expect(router.resolve('/user/1337/detail'), equals(userById));
expect(router.resolve('letter/a/lower'), equals(lower));
expect(router.resolve('letter/2/lower'), isNull);
});
}

1
test/router/packages Symbolic link
View file

@ -0,0 +1 @@
../../packages

5
web/hash/basic.dart Normal file
View file

@ -0,0 +1,5 @@
import 'package:angel_route/browser.dart';
import '../shared/basic.dart';
main() => basic(new BrowserRouter(hash: true));

31
web/hash/basic.html Normal file
View file

@ -0,0 +1,31 @@
<!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>

View file

@ -0,0 +1,5 @@
import 'package:angel_route/browser.dart';
import '../shared/basic.dart';
main() => basic(new BrowserRouter());

31
web/push_state/basic.html Normal file
View file

@ -0,0 +1,31 @@
<!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>

33
web/shared/basic.dart Normal file
View file

@ -0,0 +1,33 @@
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) {
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();
}