1.0.0-dev
This commit is contained in:
parent
58d0c51c4e
commit
064c831db0
27 changed files with 1050 additions and 36 deletions
46
.gitignore
vendored
46
.gitignore
vendored
|
@ -25,5 +25,49 @@ doc/api/
|
||||||
# Don't commit pubspec lock file
|
# Don't commit pubspec lock file
|
||||||
# (Library packages only! Remove pattern if developing an application package)
|
# (Library packages only! Remove pattern if developing an application package)
|
||||||
pubspec.lock
|
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
23
.idea/angel_route.iml
Normal 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
38
.idea/misc.xml
Normal 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
8
.idea/modules.xml
Normal 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>
|
6
.idea/runConfigurations/All_Tests.xml
Normal file
6
.idea/runConfigurations/All_Tests.xml
Normal 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
137
README.md
|
@ -1,2 +1,137 @@
|
||||||
# angel_route
|
# 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.
|
|
@ -1,3 +1,5 @@
|
||||||
library angel_route;
|
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
123
lib/browser.dart
Normal 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
31
lib/src/extensible.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,66 +1,160 @@
|
||||||
final RegExp _rgxEnd = new RegExp(r'\$');
|
import 'extensible.dart';
|
||||||
final RegExp _rgxStart = new RegExp(r'\^');
|
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'(^/+)|(/+$)');
|
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 {
|
class Route {
|
||||||
final List<Route> _children = [];
|
final List<Route> _children = [];
|
||||||
final List _handlers = [];
|
final List _handlers = [];
|
||||||
RegExp _matcher;
|
RegExp _matcher;
|
||||||
|
String _name;
|
||||||
Route _parent;
|
Route _parent;
|
||||||
String _path;
|
String _path;
|
||||||
|
String _pathified;
|
||||||
|
RegExp _resolver;
|
||||||
List<Route> get children => new List.unmodifiable(_children);
|
List<Route> get children => new List.unmodifiable(_children);
|
||||||
List get handlers => new List.unmodifiable(_handlers);
|
List get handlers => new List.unmodifiable(_handlers);
|
||||||
RegExp get matcher => _matcher;
|
RegExp get matcher => _matcher;
|
||||||
final String method;
|
final String method;
|
||||||
final String name;
|
String get name => _name;
|
||||||
Route get parent => _parent;
|
Route get parent => _parent;
|
||||||
String get path => _path;
|
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,
|
Route(Pattern path,
|
||||||
{Iterable<Route> children: const [],
|
{Iterable<Route> children: const [],
|
||||||
Iterable handlers: const [],
|
Iterable handlers: const [],
|
||||||
this.method: "GET",
|
this.method: "GET",
|
||||||
this.name: null}) {
|
String name: null}) {
|
||||||
if (children != null) _children.addAll(children);
|
if (children != null) _children.addAll(children);
|
||||||
if (handlers != null) _handlers.addAll(handlers);
|
if (handlers != null) _handlers.addAll(handlers);
|
||||||
|
_name = name;
|
||||||
|
|
||||||
if (path is RegExp) {
|
if (path is RegExp) {
|
||||||
_matcher = path;
|
_matcher = path;
|
||||||
_path = path.pattern;
|
_path = path.pattern;
|
||||||
} else {
|
} else {
|
||||||
_matcher = new RegExp(_path = path
|
_matcher = new RegExp(
|
||||||
.toString()
|
_matcherify(path.toString().replaceAll(_straySlashes, '')));
|
||||||
.replaceAll(_straySlashes, '')
|
_path = _pathified = _pathify(path.toString());
|
||||||
.replaceAll(new RegExp(r'\/\*$'), "*")
|
_resolver = new RegExp(_matcherify(
|
||||||
.replaceAll(new RegExp('\/'), r'\/')
|
path.toString().replaceAll(_straySlashes, ''),
|
||||||
.replaceAll(new RegExp(':[a-zA-Z_]+'), '([^\/]+)')
|
expand: false));
|
||||||
.replaceAll(new RegExp('\\*'), '.*'));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
factory Route.join(Route parent, Route child) {
|
factory Route.join(Route parent, Route child) {
|
||||||
final String path1 = parent.path.replaceAll(_straySlashes, '');
|
final String path1 = parent.path
|
||||||
final String path2 = child.path.replaceAll(_straySlashes, '');
|
.replaceAll(_rgxStart, '')
|
||||||
final String pattern1 = parent.matcher.pattern.replaceAll(_rgxEnd, '');
|
.replaceAll(_rgxEnd, '')
|
||||||
final String pattern2 = child.matcher.pattern.replaceAll(_rgxStart, '');
|
.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,
|
children: child.children,
|
||||||
handlers: child.handlers,
|
handlers: child.handlers,
|
||||||
method: child.method,
|
method: child.method,
|
||||||
name: child.name);
|
name: child.name);
|
||||||
|
|
||||||
|
String separator = (pattern1.isEmpty || pattern1 == '^') ? '' : '\\/';
|
||||||
|
|
||||||
return route
|
return route
|
||||||
..parent = parent
|
.._matcher = new RegExp('$pattern1$separator$pattern2')
|
||||||
.._path = '$path1/$path2';
|
.._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 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);
|
_children.add(created);
|
||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Assigns a name to this route.
|
||||||
|
Route as(String name) => this.._name = name;
|
||||||
|
|
||||||
Route child(Pattern path,
|
Route child(Pattern path,
|
||||||
{Iterable<Route> children: const [],
|
{Iterable<Route> children: const [],
|
||||||
Iterable handlers: const [],
|
Iterable handlers: const [],
|
||||||
|
@ -71,28 +165,109 @@ class Route {
|
||||||
return addChild(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) {
|
return result.replaceAll("*", "");
|
||||||
if (path.isEmpty ||
|
}
|
||||||
path == '.' ||
|
|
||||||
path.replaceAll(_straySlashes, '').isEmpty) {
|
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;
|
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 {
|
} else {
|
||||||
final segments = path.split('/');
|
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) {
|
for (Route route in children) {
|
||||||
final subPath = '${this.path}/${segments[0]}';
|
final subPath = '${this.path}/${segments[0]}';
|
||||||
|
|
||||||
if (route.match(subPath) != null) {
|
if (route.match(subPath) != null ||
|
||||||
if (segments.length == 1)
|
route._resolver.firstMatch(subPath) != null) {
|
||||||
|
if (segments.length == 1 && _filter(route))
|
||||||
return route;
|
return route;
|
||||||
else {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
168
lib/src/router.dart
Normal file
168
lib/src/router.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
14
lib/src/routing_exception.dart
Normal file
14
lib/src/routing_exception.dart
Normal 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;
|
||||||
|
}
|
|
@ -1,7 +1,8 @@
|
||||||
name: angel_route
|
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
|
version: 1.0.0-dev
|
||||||
author: Tobe O <thosakwe@gmail.com>
|
author: Tobe O <thosakwe@gmail.com>
|
||||||
homepage: https://github.com/angel-dart/angel_route
|
homepage: https://github.com/angel-dart/angel_route
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
browser: ">=0.10.0 < 0.11.0"
|
||||||
test: ">=0.12.15 <0.13.0"
|
test: ">=0.12.15 <0.13.0"
|
|
@ -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() {
|
main() {
|
||||||
final foo = new Route('/foo');
|
group('route', route.main);
|
||||||
final bar = foo.child('/bar');
|
group('router', router.main);
|
||||||
print(foo.path);
|
|
||||||
print(bar.path);
|
|
||||||
}
|
}
|
1
test/packages
Symbolic link
1
test/packages
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../packages
|
10
test/route/all_tests.dart
Normal file
10
test/route/all_tests.dart
Normal 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
57
test/route/no_params.dart
Normal 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
1
test/route/packages
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../packages
|
26
test/route/parse_params.dart
Normal file
26
test/route/parse_params.dart
Normal 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}));
|
||||||
|
});
|
||||||
|
}
|
3
test/route/with_params.dart
Normal file
3
test/route/with_params.dart
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
main() {
|
||||||
|
|
||||||
|
}
|
42
test/router/all_tests.dart
Normal file
42
test/router/all_tests.dart
Normal 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
1
test/router/packages
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../packages
|
5
web/hash/basic.dart
Normal file
5
web/hash/basic.dart
Normal 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
31
web/hash/basic.html
Normal 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>
|
5
web/push_state/basic.dart
Normal file
5
web/push_state/basic.dart
Normal 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
31
web/push_state/basic.html
Normal 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
33
web/shared/basic.dart
Normal 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();
|
||||||
|
}
|
Loading…
Reference in a new issue