Add 'packages/route/' from commit '8b2b0524609ab737eb5569fdf82a7ac491fdc67b'
git-subtree-dir: packages/route git-subtree-mainline:6db839928b
git-subtree-split:8b2b052460
This commit is contained in:
commit
5eef4314ec
37 changed files with 2248 additions and 0 deletions
77
packages/route/.gitignore
vendored
Normal file
77
packages/route/.gitignore
vendored
Normal file
|
@ -0,0 +1,77 @@
|
|||
# See https://www.dartlang.org/tools/private-files.html
|
||||
|
||||
# Files and directories created by pub
|
||||
.buildlog
|
||||
.packages
|
||||
.project
|
||||
.pub/
|
||||
build/
|
||||
**/packages/
|
||||
|
||||
# Files created by dart2js
|
||||
# (Most Dart developers will use pub build to compile Dart, use/modify these
|
||||
# rules if you intend to use dart2js directly
|
||||
# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
|
||||
# differentiate from explicit Javascript files)
|
||||
*.dart.js
|
||||
*.part.js
|
||||
*.js.deps
|
||||
*.js.map
|
||||
*.info.json
|
||||
|
||||
# Directory created by dartdoc
|
||||
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
|
||||
.dart_tool
|
||||
*.iml
|
6
packages/route/.idea/misc.xml
Normal file
6
packages/route/.idea/misc.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
8
packages/route/.idea/modules.xml
Normal file
8
packages/route/.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$/route.iml" filepath="$PROJECT_DIR$/route.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,8 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="tests in route" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
|
||||
<option name="filePath" value="$PROJECT_DIR$" />
|
||||
<option name="scope" value="FOLDER" />
|
||||
<option name="testRunnerOptions" value="-j4" />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
3
packages/route/.travis.yml
Normal file
3
packages/route/.travis.yml
Normal file
|
@ -0,0 +1,3 @@
|
|||
language: dart
|
||||
dart:
|
||||
- stable
|
40
packages/route/CHANGELOG.md
Normal file
40
packages/route/CHANGELOG.md
Normal file
|
@ -0,0 +1,40 @@
|
|||
# 3.1.0+1
|
||||
* Accidentally hit `CTRL-C` while uploading `3.1.0`; this version ensures everything is ok.
|
||||
|
||||
# 3.1.0
|
||||
* Add `Router.groupAsync`
|
||||
|
||||
# 3.0.6
|
||||
* Remove static default values for `middleware`.
|
||||
|
||||
# 3.0.5
|
||||
* Add `MiddlewarePipelineIterator`.
|
||||
|
||||
# 3.0.4
|
||||
* Add `RouteResult` class, which allows segments (i.e. wildcard) to
|
||||
modify the `tail`.
|
||||
* Add more wildcard tests.
|
||||
|
||||
# 3.0.3
|
||||
* Support trailing text after parameters with custom Regexes.
|
||||
|
||||
# 3.0.2
|
||||
* Support leading and trailing text for both `:parameters` and `*`
|
||||
|
||||
# 3.0.1
|
||||
* Make the callback in `Router.group` generically-typed.
|
||||
|
||||
# 3.0.0
|
||||
* Make `Router` and `Route` single-parameter generic.
|
||||
* Remove `package:browser` dependency.
|
||||
* `BrowserRouter.on` now only accepts a `String`.
|
||||
* `MiddlewarePipeline.routingResults` now accepts
|
||||
an `Iterable<RoutingResult>`, instead of just a `List`.
|
||||
* Removed deprecated `Route.as`, as well as `Router.registerMiddleware`.
|
||||
* Completely removed `Route.requestMiddleware`.
|
||||
|
||||
# 2.0.7
|
||||
* Minor strong mode updates to work with stricter Dart 2.
|
||||
|
||||
# 2.0.5
|
||||
* Patch to work with `combinator@1.0.0`.
|
21
packages/route/LICENSE
Normal file
21
packages/route/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2016 The Angel Framework
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
147
packages/route/README.md
Normal file
147
packages/route/README.md
Normal file
|
@ -0,0 +1,147 @@
|
|||
# angel_route
|
||||
|
||||
[](https://pub.dartlang.org/packages/angel_route)
|
||||
[](https://travis-ci.org/angel-dart/route)
|
||||
|
||||
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 minimal 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 = Router();
|
||||
|
||||
router.get('/users', () {});
|
||||
|
||||
router.post('/users/:id/timeline', (String id) {});
|
||||
|
||||
router.get('/square_root/:id([0-9]+)', (n) {
|
||||
return { 'result': pow(int.parse(n), 0.5) };
|
||||
});
|
||||
|
||||
// You can also have parameters auto-parsed.
|
||||
//
|
||||
// Supports int, double, and num.
|
||||
router.get('/square_root/int:id([0-9]+)', (int n) {
|
||||
return { 'result': pow(n, 0.5) };
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
}, middleware: [put, middleware, here]);
|
||||
|
||||
// Grouping can also take async callbacks.
|
||||
await router.groupAsync('/hello', (router) async {
|
||||
var name = await getNameFromFileSystem();
|
||||
router.get(name, (req, res) => '...');
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
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 router = Router();
|
||||
|
||||
router
|
||||
.chain('middleware1')
|
||||
.chain('other_middleware')
|
||||
.get('/hello', () {
|
||||
print('world');
|
||||
});
|
||||
|
||||
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<RoutingResult> onRoute`, which can be listened to for changes. It will fire `null`
|
||||
whenever no route is matched.
|
||||
|
||||
`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
|
||||
|
||||
```dart
|
||||
main() {
|
||||
final router = BrowserRouter();
|
||||
// ..
|
||||
router.onRoute.listen((route) {
|
||||
if (route == null)
|
||||
throw 404;
|
||||
else route.state['foo'] = 'bar';
|
||||
});
|
||||
|
||||
router.listen(); // Start listening
|
||||
}
|
||||
```
|
||||
|
||||
For applications where you need to access a chain of handlers, consider using
|
||||
`onResolve` instead. You can see an example in `web/shared/basic.dart`.
|
||||
|
||||
# Route Parameters
|
||||
Routes can have parameters, as seen in the above examples.
|
||||
Use [allParams](https://www.dartdocs.org/documentation/angel_route/1.0.3/angel_route/RoutingResult-class.html)
|
||||
in a `RoutingResult` to get them as a nice `Map`:
|
||||
|
||||
```dart
|
||||
var router = Router();
|
||||
router.get('/book/:id/authors', () => ...);
|
||||
|
||||
var result = router.resolve('/book/foo/authors');
|
||||
var params = result.allParams; // {'id': 'foo'};
|
||||
```
|
4
packages/route/analysis_options.yaml
Normal file
4
packages/route/analysis_options.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
include: package:pedantic/analysis_options.yaml
|
||||
analyzer:
|
||||
strong-mode:
|
||||
implicit-casts: false
|
58
packages/route/example/main.dart
Normal file
58
packages/route/example/main.dart
Normal file
|
@ -0,0 +1,58 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:angel_route/angel_route.dart';
|
||||
|
||||
main() {
|
||||
final router = Router();
|
||||
|
||||
router.get('/whois/~:user', () {});
|
||||
|
||||
router.get('/wild*', () {});
|
||||
|
||||
router.get('/ordinal/int:n([0-9]+)st', () {});
|
||||
|
||||
print(router.resolveAbsolute('/whois/~thosakwe').first.allParams);
|
||||
print(router.resolveAbsolute('/wild_thornberrys').first.route.path);
|
||||
print(router.resolveAbsolute('/ordinal/1st').first.allParams);
|
||||
|
||||
router.get('/users', () {});
|
||||
|
||||
router.post('/users/:id/timeline', (String id) {});
|
||||
|
||||
router.get('/square_root/:id([0-9]+)', (String n) {
|
||||
return {'result': pow(int.parse(n), 0.5)};
|
||||
});
|
||||
|
||||
// You can also have parameters auto-parsed.
|
||||
//
|
||||
// Supports int, double, and num.
|
||||
router.get('/square_root/int:id([0-9]+)', (int n) {
|
||||
return {'result': pow(n, 0.5)};
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
SomeQuery someQuery(id) => SomeQuery();
|
||||
|
||||
class SomeQuery {
|
||||
List<SomeQueryReview> get reviews => [
|
||||
SomeQueryReview('fake'),
|
||||
SomeQueryReview('data'),
|
||||
];
|
||||
}
|
||||
|
||||
class SomeQueryReview {
|
||||
final String id;
|
||||
|
||||
SomeQueryReview(this.id);
|
||||
}
|
5
packages/route/lib/angel_route.dart
Normal file
5
packages/route/lib/angel_route.dart
Normal file
|
@ -0,0 +1,5 @@
|
|||
library angel_route;
|
||||
|
||||
export 'src/middleware_pipeline.dart';
|
||||
export 'src/router.dart';
|
||||
export 'src/routing_exception.dart';
|
213
packages/route/lib/browser.dart
Normal file
213
packages/route/lib/browser.dart
Normal file
|
@ -0,0 +1,213 @@
|
|||
import 'dart:async' show Stream, StreamController;
|
||||
import 'dart:html';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'angel_route.dart';
|
||||
|
||||
final RegExp _hash = RegExp(r'^#/');
|
||||
final RegExp _straySlashes = RegExp(r'(^/+)|(/+$)');
|
||||
|
||||
/// A variation of the [Router] support both hash routing and push state.
|
||||
abstract class BrowserRouter<T> extends Router<T> {
|
||||
/// Fires whenever the active route changes. Fires `null` if none is selected (404).
|
||||
Stream<RoutingResult<T>> get onResolve;
|
||||
|
||||
/// Fires whenever the active route changes. Fires `null` if none is selected (404).
|
||||
Stream<Route<T>> 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 = false}) {
|
||||
return hash
|
||||
? _HashRouter<T>(listen: listen)
|
||||
: _PushStateRouter<T>(listen: listen);
|
||||
}
|
||||
|
||||
BrowserRouter._() : super();
|
||||
|
||||
void _goTo(String path);
|
||||
|
||||
/// Navigates to the path generated by calling
|
||||
/// [navigate] with the given [linkParams].
|
||||
///
|
||||
/// This always navigates to an absolute path.
|
||||
void go(List linkParams);
|
||||
|
||||
// Handles a route path, manually.
|
||||
// void handle(String path);
|
||||
|
||||
/// Begins listen for location changes.
|
||||
void listen();
|
||||
|
||||
/// Identical to [all].
|
||||
Route on(String path, T handler, {Iterable<T> middleware});
|
||||
}
|
||||
|
||||
abstract class _BrowserRouterImpl<T> extends Router<T>
|
||||
implements BrowserRouter<T> {
|
||||
bool _listening = false;
|
||||
Route _current;
|
||||
StreamController<RoutingResult<T>> _onResolve =
|
||||
StreamController<RoutingResult<T>>();
|
||||
StreamController<Route<T>> _onRoute = StreamController<Route<T>>();
|
||||
|
||||
Route get currentRoute => _current;
|
||||
|
||||
@override
|
||||
Stream<RoutingResult<T>> get onResolve => _onResolve.stream;
|
||||
|
||||
@override
|
||||
Stream<Route<T>> get onRoute => _onRoute.stream;
|
||||
|
||||
_BrowserRouterImpl({bool listen}) : super() {
|
||||
if (listen != false) this.listen();
|
||||
prepareAnchors();
|
||||
}
|
||||
|
||||
@override
|
||||
void go(Iterable linkParams) => _goTo(navigate(linkParams));
|
||||
|
||||
Route on(String path, T handler, {Iterable<T> middleware}) =>
|
||||
all(path, handler, middleware: middleware);
|
||||
|
||||
void prepareAnchors() {
|
||||
final anchors = window.document
|
||||
.querySelectorAll('a')
|
||||
.cast<AnchorElement>(); //: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();
|
||||
_goTo($a.attributes['href']);
|
||||
//go($a.attributes['href'].split('/').where((str) => str.isNotEmpty));
|
||||
});
|
||||
}
|
||||
|
||||
$a.attributes['dynamic'] = 'true';
|
||||
}
|
||||
}
|
||||
|
||||
void _listen();
|
||||
|
||||
@override
|
||||
void listen() {
|
||||
if (_listening) {
|
||||
throw StateError('The router is already listening for page changes.');
|
||||
}
|
||||
_listening = true;
|
||||
_listen();
|
||||
}
|
||||
}
|
||||
|
||||
class _HashRouter<T> extends _BrowserRouterImpl<T> {
|
||||
_HashRouter({bool listen}) : super(listen: listen) {
|
||||
if (listen) this.listen();
|
||||
}
|
||||
|
||||
@override
|
||||
void _goTo(String uri) {
|
||||
window.location.hash = '#$uri';
|
||||
}
|
||||
|
||||
void handleHash([_]) {
|
||||
final path = window.location.hash.replaceAll(_hash, '');
|
||||
var allResolved = resolveAbsolute(path);
|
||||
|
||||
final resolved = allResolved.isEmpty ? null : allResolved.first;
|
||||
|
||||
if (resolved == null) {
|
||||
_onResolve.add(null);
|
||||
_onRoute.add(_current = null);
|
||||
} else if (resolved != null && resolved.route != _current) {
|
||||
_onResolve.add(resolved);
|
||||
_onRoute.add(_current = resolved.route);
|
||||
}
|
||||
}
|
||||
|
||||
void handlePath(String path) {
|
||||
final resolved = resolveAbsolute(path).first;
|
||||
|
||||
if (resolved == null) {
|
||||
_onResolve.add(null);
|
||||
_onRoute.add(_current = null);
|
||||
} else if (resolved != null && resolved.route != _current) {
|
||||
_onResolve.add(resolved);
|
||||
_onRoute.add(_current = resolved.route);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void _listen() {
|
||||
window.onHashChange.listen(handleHash);
|
||||
handleHash();
|
||||
}
|
||||
}
|
||||
|
||||
class _PushStateRouter<T> extends _BrowserRouterImpl<T> {
|
||||
String _basePath;
|
||||
|
||||
_PushStateRouter({bool listen, Route root}) : super(listen: listen) {
|
||||
var $base = window.document.querySelector('base[href]') as BaseElement;
|
||||
|
||||
if ($base?.href?.isNotEmpty != true) {
|
||||
throw StateError(
|
||||
'You must have a <base href="<base-url-here>"> element present in your document to run the push state router.');
|
||||
}
|
||||
_basePath = $base.href.replaceAll(_straySlashes, '');
|
||||
if (listen) this.listen();
|
||||
}
|
||||
|
||||
@override
|
||||
void _goTo(String uri) {
|
||||
final resolved = resolveAbsolute(uri).first;
|
||||
var relativeUri = uri;
|
||||
|
||||
if (_basePath?.isNotEmpty == true) {
|
||||
relativeUri = p.join(_basePath, uri.replaceAll(_straySlashes, ''));
|
||||
}
|
||||
|
||||
if (resolved == null) {
|
||||
_onResolve.add(null);
|
||||
_onRoute.add(_current = null);
|
||||
} else {
|
||||
final route = resolved.route;
|
||||
window.history.pushState({'path': route.path, 'params': {}},
|
||||
route.name ?? route.path, relativeUri);
|
||||
_onResolve.add(resolved);
|
||||
_onRoute.add(_current = route);
|
||||
}
|
||||
}
|
||||
|
||||
void handleState(state) {
|
||||
if (state is Map && state.containsKey('path')) {
|
||||
var path = state['path'].toString();
|
||||
final resolved = resolveAbsolute(path).first;
|
||||
|
||||
if (resolved != null && resolved.route != _current) {
|
||||
//properties.addAll(state['properties'] ?? {});
|
||||
_onResolve.add(resolved);
|
||||
_onRoute.add(_current = resolved.route);
|
||||
} else {
|
||||
_onResolve.add(null);
|
||||
_onRoute.add(_current = null);
|
||||
}
|
||||
} else {
|
||||
_onResolve.add(null);
|
||||
_onRoute.add(_current = null);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void _listen() {
|
||||
window.onPopState.listen((e) {
|
||||
handleState(e.state);
|
||||
});
|
||||
|
||||
handleState(window.history.state);
|
||||
}
|
||||
}
|
291
packages/route/lib/src/grammar.dart
Normal file
291
packages/route/lib/src/grammar.dart
Normal file
|
@ -0,0 +1,291 @@
|
|||
part of angel_route.src.router;
|
||||
|
||||
class RouteGrammar {
|
||||
static const String notSlashRgx = r'([^/]+)';
|
||||
//static final RegExp rgx = RegExp(r'\((.+)\)');
|
||||
static final Parser<String> notSlash =
|
||||
match<String>(RegExp(notSlashRgx)).value((r) => r.span.text);
|
||||
|
||||
static final Parser<Match> regExp =
|
||||
match<Match>(RegExp(r'\(([^\n)]+)\)([^/]+)?'))
|
||||
.value((r) => r.scanner.lastMatch);
|
||||
|
||||
static final Parser<Match> parameterName =
|
||||
match<Match>(RegExp('$notSlashRgx?' r':([A-Za-z0-9_]+)' r'([^(/\n])?'))
|
||||
.value((r) => r.scanner.lastMatch);
|
||||
|
||||
static final Parser<ParameterSegment> parameterSegment = chain([
|
||||
parameterName,
|
||||
match<bool>('?').value((r) => true).opt(),
|
||||
regExp.opt(),
|
||||
]).map((r) {
|
||||
var match = r.value[0] as Match;
|
||||
var rgxMatch = r.value[2] as Match;
|
||||
|
||||
var pre = match[1] ?? '';
|
||||
var post = match[3] ?? '';
|
||||
RegExp rgx;
|
||||
|
||||
if (rgxMatch != null) {
|
||||
rgx = RegExp('(${rgxMatch[1]})');
|
||||
post = (rgxMatch[2] ?? '') + post;
|
||||
}
|
||||
|
||||
if (pre.isNotEmpty || post.isNotEmpty) {
|
||||
if (rgx != null) {
|
||||
var pattern = pre + rgx.pattern + post;
|
||||
rgx = RegExp(pattern);
|
||||
} else {
|
||||
rgx = RegExp('$pre$notSlashRgx$post');
|
||||
}
|
||||
}
|
||||
|
||||
var s = ParameterSegment(match[2], rgx);
|
||||
return r.value[1] == true ? OptionalSegment(s) : s;
|
||||
});
|
||||
|
||||
static final Parser<ParsedParameterSegment> parsedParameterSegment = chain([
|
||||
match(RegExp(r'(int|num|double)'),
|
||||
errorMessage: 'Expected "int","double", or "num".')
|
||||
.map((r) => r.span.text),
|
||||
parameterSegment,
|
||||
]).map((r) {
|
||||
return ParsedParameterSegment(
|
||||
r.value[0] as String, r.value[1] as ParameterSegment);
|
||||
});
|
||||
|
||||
static final Parser<WildcardSegment> wildcardSegment =
|
||||
match<WildcardSegment>(RegExp('$notSlashRgx?' r'\*' '$notSlashRgx?'))
|
||||
.value((r) {
|
||||
var m = r.scanner.lastMatch;
|
||||
var pre = m[1] ?? '';
|
||||
var post = m[2] ?? '';
|
||||
return WildcardSegment(pre, post);
|
||||
});
|
||||
|
||||
static final Parser<ConstantSegment> constantSegment =
|
||||
notSlash.map<ConstantSegment>((r) => ConstantSegment(r.value));
|
||||
|
||||
static final Parser<SlashSegment> slashSegment =
|
||||
match(SlashSegment.rgx).map((_) => SlashSegment());
|
||||
|
||||
static final Parser<RouteSegment> routeSegment = any([
|
||||
//slashSegment,
|
||||
parsedParameterSegment,
|
||||
parameterSegment,
|
||||
wildcardSegment,
|
||||
constantSegment
|
||||
]);
|
||||
|
||||
// static final Parser<RouteDefinition> routeDefinition = routeSegment
|
||||
// .star()
|
||||
// .map<RouteDefinition>((r) => RouteDefinition(r.value ?? []))
|
||||
// .surroundedBy(match(RegExp(r'/*')).opt());
|
||||
|
||||
static final Parser slashes = match(RegExp(r'/*'));
|
||||
|
||||
static final Parser<RouteDefinition> routeDefinition = routeSegment
|
||||
.separatedBy(slashes)
|
||||
.map<RouteDefinition>((r) => RouteDefinition(r.value ?? []))
|
||||
.surroundedBy(slashes.opt());
|
||||
}
|
||||
|
||||
class RouteDefinition {
|
||||
final List<RouteSegment> segments;
|
||||
|
||||
RouteDefinition(this.segments);
|
||||
|
||||
Parser<RouteResult> compile() {
|
||||
Parser<RouteResult> out;
|
||||
|
||||
for (int i = 0; i < segments.length; i++) {
|
||||
var s = segments[i];
|
||||
bool isLast = i == segments.length - 1;
|
||||
if (out == null) {
|
||||
out = s.compile(isLast);
|
||||
} else {
|
||||
out = s.compileNext(
|
||||
out.then(match('/')).index(0).cast<RouteResult>(), isLast);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class RouteSegment {
|
||||
Parser<RouteResult> compile(bool isLast);
|
||||
|
||||
Parser<RouteResult> compileNext(Parser<RouteResult> p, bool isLast);
|
||||
}
|
||||
|
||||
class SlashSegment implements RouteSegment {
|
||||
static final RegExp rgx = RegExp(r'/+');
|
||||
|
||||
const SlashSegment();
|
||||
|
||||
@override
|
||||
Parser<RouteResult> compile(bool isLast) {
|
||||
return match(rgx).map((_) => RouteResult({}));
|
||||
}
|
||||
|
||||
@override
|
||||
Parser<RouteResult> compileNext(Parser<RouteResult> p, bool isLast) {
|
||||
return p.then(compile(isLast)).index(0).cast<RouteResult>();
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'Slash';
|
||||
}
|
||||
|
||||
class ConstantSegment extends RouteSegment {
|
||||
final String text;
|
||||
|
||||
ConstantSegment(this.text);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Constant: $text';
|
||||
}
|
||||
|
||||
@override
|
||||
Parser<RouteResult> compile(bool isLast) {
|
||||
return match(text).map((r) => RouteResult({}));
|
||||
}
|
||||
|
||||
@override
|
||||
Parser<RouteResult> compileNext(Parser<RouteResult> p, bool isLast) {
|
||||
return p.then(compile(isLast)).index(0).cast<RouteResult>();
|
||||
}
|
||||
}
|
||||
|
||||
class WildcardSegment extends RouteSegment {
|
||||
final String pre, post;
|
||||
|
||||
WildcardSegment(this.pre, this.post);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Wildcard segment';
|
||||
}
|
||||
|
||||
String _symbol(bool isLast) {
|
||||
if (isLast) return r'.*';
|
||||
return r'[^/]*';
|
||||
}
|
||||
|
||||
RegExp _compile(bool isLast) {
|
||||
return RegExp('$pre(${_symbol(isLast)})$post');
|
||||
// if (isLast) return match(RegExp(r'.*'));
|
||||
// return match(RegExp(r'[^/]*'));
|
||||
}
|
||||
|
||||
@override
|
||||
Parser<RouteResult> compile(bool isLast) {
|
||||
return match(_compile(isLast))
|
||||
.map((r) => RouteResult({}, tail: r.scanner.lastMatch[1]));
|
||||
}
|
||||
|
||||
@override
|
||||
Parser<RouteResult> compileNext(Parser<RouteResult> p, bool isLast) {
|
||||
return p.then(compile(isLast)).map((r) {
|
||||
var items = r.value.cast<RouteResult>();
|
||||
var a = items[0], b = items[1];
|
||||
return a
|
||||
..addAll(b?.params ?? {})
|
||||
.._setTail(b?.tail);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class OptionalSegment extends ParameterSegment {
|
||||
final ParameterSegment parameter;
|
||||
|
||||
OptionalSegment(this.parameter) : super(parameter.name, parameter.regExp);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Optional: $parameter';
|
||||
}
|
||||
|
||||
@override
|
||||
Parser<RouteResult> compile(bool isLast) {
|
||||
return super.compile(isLast).opt();
|
||||
}
|
||||
|
||||
@override
|
||||
Parser<RouteResult> compileNext(Parser<RouteResult> p, bool isLast) {
|
||||
return p.then(_compile().opt()).map((r) {
|
||||
if (r.value[1] == null) return r.value[0] as RouteResult;
|
||||
return (r.value[0] as RouteResult)
|
||||
..addAll({name: Uri.decodeComponent(r.value[1] as String)});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ParameterSegment extends RouteSegment {
|
||||
final String name;
|
||||
final RegExp regExp;
|
||||
|
||||
ParameterSegment(this.name, this.regExp);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
if (regExp != null) return 'Param: $name (${regExp.pattern})';
|
||||
return 'Param: $name';
|
||||
}
|
||||
|
||||
Parser<String> _compile() {
|
||||
return regExp != null
|
||||
? match<String>(regExp).value((r) => r.scanner.lastMatch[1])
|
||||
: RouteGrammar.notSlash;
|
||||
}
|
||||
|
||||
@override
|
||||
Parser<RouteResult> compile(bool isLast) {
|
||||
return _compile()
|
||||
.map((r) => RouteResult({name: Uri.decodeComponent(r.value)}));
|
||||
}
|
||||
|
||||
@override
|
||||
Parser<RouteResult> compileNext(Parser<RouteResult> p, bool isLast) {
|
||||
return p.then(_compile()).map((r) {
|
||||
return (r.value[0] as RouteResult)
|
||||
..addAll({name: Uri.decodeComponent(r.value[1] as String)});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ParsedParameterSegment extends RouteSegment {
|
||||
final String type;
|
||||
final ParameterSegment parameter;
|
||||
|
||||
ParsedParameterSegment(this.type, this.parameter);
|
||||
|
||||
num getValue(String s) {
|
||||
switch (type) {
|
||||
case 'int':
|
||||
return int.parse(s);
|
||||
case 'double':
|
||||
return double.parse(s);
|
||||
default:
|
||||
return num.parse(s);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Parser<RouteResult> compile(bool isLast) {
|
||||
return parameter._compile().map((r) => RouteResult(
|
||||
{parameter.name: getValue(Uri.decodeComponent(r.span.text))}));
|
||||
}
|
||||
|
||||
@override
|
||||
Parser<RouteResult> compileNext(Parser<RouteResult> p, bool isLast) {
|
||||
return p.then(parameter._compile()).map((r) {
|
||||
return (r.value[0] as RouteResult)
|
||||
..addAll({
|
||||
parameter.name: getValue(Uri.decodeComponent(r.value[1] as String))
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
38
packages/route/lib/src/middleware_pipeline.dart
Normal file
38
packages/route/lib/src/middleware_pipeline.dart
Normal file
|
@ -0,0 +1,38 @@
|
|||
import 'router.dart';
|
||||
|
||||
/// A chain of arbitrary handlers obtained by routing a path.
|
||||
class MiddlewarePipeline<T> {
|
||||
/// All the possible routes that matched the given path.
|
||||
final Iterable<RoutingResult<T>> routingResults;
|
||||
List<T> _handlers;
|
||||
|
||||
/// An ordered list of every handler delegated to handle this request.
|
||||
List<T> get handlers {
|
||||
if (_handlers != null) return _handlers;
|
||||
final handlers = <T>[];
|
||||
|
||||
for (var result in routingResults) {
|
||||
handlers.addAll(result.allHandlers);
|
||||
}
|
||||
|
||||
return _handlers = handlers;
|
||||
}
|
||||
|
||||
MiddlewarePipeline(Iterable<RoutingResult<T>> routingResults)
|
||||
: this.routingResults = routingResults.toList();
|
||||
}
|
||||
|
||||
/// Iterates through a [MiddlewarePipeline].
|
||||
class MiddlewarePipelineIterator<T> extends Iterator<RoutingResult<T>> {
|
||||
final MiddlewarePipeline<T> pipeline;
|
||||
final Iterator<RoutingResult<T>> _inner;
|
||||
|
||||
MiddlewarePipelineIterator(this.pipeline)
|
||||
: _inner = pipeline.routingResults.iterator;
|
||||
|
||||
@override
|
||||
RoutingResult<T> get current => _inner.current;
|
||||
|
||||
@override
|
||||
bool moveNext() => _inner.moveNext();
|
||||
}
|
79
packages/route/lib/src/route.dart
Normal file
79
packages/route/lib/src/route.dart
Normal file
|
@ -0,0 +1,79 @@
|
|||
part of angel_route.src.router;
|
||||
|
||||
/// Represents a virtual location within an application.
|
||||
class Route<T> {
|
||||
final String method;
|
||||
final String path;
|
||||
final List<T> handlers;
|
||||
final Map<String, Map<String, dynamic>> _cache = {};
|
||||
final RouteDefinition _routeDefinition;
|
||||
String name;
|
||||
Parser<RouteResult> _parser;
|
||||
|
||||
Route(this.path, {@required this.method, @required this.handlers})
|
||||
: _routeDefinition = RouteGrammar.routeDefinition
|
||||
.parse(SpanScanner(path.replaceAll(_straySlashes, '')))
|
||||
.value {
|
||||
if (_routeDefinition?.segments?.isNotEmpty != true) {
|
||||
_parser = match('').map((r) => RouteResult({}));
|
||||
}
|
||||
}
|
||||
|
||||
factory Route.join(Route<T> a, Route<T> b) {
|
||||
var start = a.path.replaceAll(_straySlashes, '');
|
||||
var end = b.path.replaceAll(_straySlashes, '');
|
||||
return Route('$start/$end'.replaceAll(_straySlashes, ''),
|
||||
method: b.method, handlers: b.handlers);
|
||||
}
|
||||
|
||||
Parser<RouteResult> get parser => _parser ??= _routeDefinition.compile();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '$method $path => $handlers';
|
||||
}
|
||||
|
||||
Route<T> clone() {
|
||||
return Route<T>(path, method: method, handlers: handlers)
|
||||
.._cache.addAll(_cache);
|
||||
}
|
||||
|
||||
String makeUri(Map<String, dynamic> params) {
|
||||
var b = StringBuffer();
|
||||
int i = 0;
|
||||
|
||||
for (var seg in _routeDefinition.segments) {
|
||||
if (i++ > 0) b.write('/');
|
||||
if (seg is ConstantSegment) {
|
||||
b.write(seg.text);
|
||||
} else if (seg is ParameterSegment) {
|
||||
if (!params.containsKey(seg.name)) {
|
||||
throw ArgumentError('Missing parameter "${seg.name}".');
|
||||
}
|
||||
b.write(params[seg.name]);
|
||||
}
|
||||
}
|
||||
|
||||
return b.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of matching an individual route.
|
||||
class RouteResult {
|
||||
/// The parsed route parameters.
|
||||
final Map<String, dynamic> params;
|
||||
|
||||
/// Optional. An explicit "tail" value to set.
|
||||
String get tail => _tail;
|
||||
|
||||
String _tail;
|
||||
|
||||
RouteResult(this.params, {String tail}) : _tail = tail;
|
||||
|
||||
void _setTail(String v) => _tail ??= v;
|
||||
|
||||
/// Adds parameters.
|
||||
void addAll(Map<String, dynamic> map) {
|
||||
params.addAll(map);
|
||||
}
|
||||
}
|
487
packages/route/lib/src/router.dart
Normal file
487
packages/route/lib/src/router.dart
Normal file
|
@ -0,0 +1,487 @@
|
|||
library angel_route.src.router;
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:combinator/combinator.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:string_scanner/string_scanner.dart';
|
||||
|
||||
import '../string_util.dart';
|
||||
import 'routing_exception.dart';
|
||||
part 'grammar.dart';
|
||||
part 'route.dart';
|
||||
part 'routing_result.dart';
|
||||
part 'symlink_route.dart';
|
||||
|
||||
//final RegExp _param = RegExp(r':([A-Za-z0-9_]+)(\((.+)\))?');
|
||||
//final RegExp _rgxEnd = RegExp(r'\$+$');
|
||||
//final RegExp _rgxStart = RegExp(r'^\^+');
|
||||
//final RegExp _rgxStraySlashes =
|
||||
// RegExp(r'(^((\\+/)|(/))+)|(((\\+/)|(/))+$)');
|
||||
//final RegExp _slashDollar = RegExp(r'/+\$');
|
||||
final RegExp _straySlashes = RegExp(r'(^/+)|(/+$)');
|
||||
|
||||
/// An abstraction over complex [Route] trees. Use this instead of the raw API. :)
|
||||
class Router<T> {
|
||||
final Map<String, Iterable<RoutingResult<T>>> _cache = {};
|
||||
|
||||
//final List<_ChainedRouter> _chained = [];
|
||||
final List<T> _middleware = [];
|
||||
final Map<Pattern, Router<T>> _mounted = {};
|
||||
final List<Route<T>> _routes = [];
|
||||
bool _useCache = false;
|
||||
|
||||
List<T> get middleware => List<T>.unmodifiable(_middleware);
|
||||
|
||||
Map<Pattern, Router<T>> get mounted =>
|
||||
Map<Pattern, Router<T>>.unmodifiable(_mounted);
|
||||
|
||||
List<Route<T>> get routes {
|
||||
return _routes.fold<List<Route<T>>>([], (out, route) {
|
||||
if (route is SymlinkRoute<T>) {
|
||||
var childRoutes =
|
||||
route.router.routes.fold<List<Route<T>>>([], (out, r) {
|
||||
return out
|
||||
..add(
|
||||
route.path.isEmpty ? r : Route.join(route, r),
|
||||
);
|
||||
});
|
||||
|
||||
return out..addAll(childRoutes);
|
||||
} else {
|
||||
return out..add(route);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Provide a `root` to make this Router revolve around a pre-defined route.
|
||||
/// Not recommended.
|
||||
Router();
|
||||
|
||||
/// Enables the use of a cache to eliminate the overhead of consecutive resolutions of the same path.
|
||||
void enableCache() {
|
||||
_useCache = true;
|
||||
}
|
||||
|
||||
/// 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<T> addRoute(String method, String path, T handler,
|
||||
{Iterable<T> middleware}) {
|
||||
middleware ??= <T>[];
|
||||
if (_useCache == true) {
|
||||
throw StateError('Cannot add routes after caching is enabled.');
|
||||
}
|
||||
|
||||
// Check if any mounted routers can match this
|
||||
final handlers = <T>[handler];
|
||||
|
||||
if (middleware != null) handlers.insertAll(0, middleware);
|
||||
|
||||
final route = Route<T>(path, method: method, handlers: handlers);
|
||||
_routes.add(route);
|
||||
return route;
|
||||
}
|
||||
|
||||
/// Prepends the given [middleware] to any routes created
|
||||
/// by the resulting router.
|
||||
///
|
||||
/// The resulting router can be chained, too.
|
||||
_ChainedRouter<T> chain(Iterable<T> middleware) {
|
||||
var piped = _ChainedRouter<T>(this, middleware);
|
||||
var route = SymlinkRoute<T>('/', piped);
|
||||
_routes.add(route);
|
||||
return piped;
|
||||
}
|
||||
|
||||
/// Returns a [Router] with a duplicated version of this tree.
|
||||
Router<T> clone() {
|
||||
final router = Router<T>();
|
||||
final newMounted = Map<Pattern, Router<T>>.from(mounted);
|
||||
|
||||
for (var route in routes) {
|
||||
if (route is! SymlinkRoute<T>) {
|
||||
router._routes.add(route.clone());
|
||||
} else if (route is SymlinkRoute<T>) {
|
||||
final newRouter = route.router.clone();
|
||||
newMounted[route.path] = newRouter;
|
||||
final symlink = SymlinkRoute<T>(route.path, newRouter);
|
||||
router._routes.add(symlink);
|
||||
}
|
||||
}
|
||||
|
||||
return router.._mounted.addAll(newMounted);
|
||||
}
|
||||
|
||||
/// 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),
|
||||
String header = 'Dumping route tree:',
|
||||
String tab = ' '}) {
|
||||
final buf = StringBuffer();
|
||||
int tabs = 0;
|
||||
|
||||
if (header != null && header.isNotEmpty) {
|
||||
buf.writeln(header);
|
||||
}
|
||||
|
||||
buf.writeln('<root>');
|
||||
|
||||
indent() {
|
||||
for (int i = 0; i < tabs; i++) {
|
||||
buf.write(tab);
|
||||
}
|
||||
}
|
||||
|
||||
dumpRouter(Router router) {
|
||||
indent();
|
||||
tabs++;
|
||||
|
||||
for (Route route in router.routes) {
|
||||
indent();
|
||||
buf.write('- ');
|
||||
if (route is! SymlinkRoute) buf.write('${route.method} ');
|
||||
buf.write('${route.path.isNotEmpty ? route.path : '/'}');
|
||||
|
||||
if (route is SymlinkRoute<T>) {
|
||||
buf.writeln();
|
||||
dumpRouter(route.router);
|
||||
} else {
|
||||
buf.writeln(' => ${route.handlers.length} handler(s)');
|
||||
}
|
||||
}
|
||||
|
||||
tabs--;
|
||||
}
|
||||
|
||||
dumpRouter(this);
|
||||
|
||||
(callback ?? print)(buf.toString());
|
||||
}
|
||||
|
||||
/// 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.
|
||||
SymlinkRoute<T> group(String path, void callback(Router<T> router),
|
||||
{Iterable<T> middleware, String name}) {
|
||||
middleware ??= <T>[];
|
||||
final router = Router<T>().._middleware.addAll(middleware);
|
||||
callback(router);
|
||||
return mount(path, router)..name = name;
|
||||
}
|
||||
|
||||
/// Asynchronous equivalent of [group].
|
||||
Future<SymlinkRoute<T>> groupAsync(
|
||||
String path, FutureOr<void> callback(Router<T> router),
|
||||
{Iterable<T> middleware, String name}) async {
|
||||
middleware ??= <T>[];
|
||||
final router = Router<T>().._middleware.addAll(middleware);
|
||||
await callback(router);
|
||||
return mount(path, router)..name = name;
|
||||
}
|
||||
|
||||
/// 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(Iterable 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<T>) {
|
||||
search = route.router;
|
||||
}
|
||||
|
||||
resolved = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Search by path
|
||||
if (!resolved) {
|
||||
var scanner = SpanScanner(param.replaceAll(_straySlashes, ''));
|
||||
for (Route route in search.routes) {
|
||||
int pos = scanner.position;
|
||||
if (route.parser.parse(scanner).successful && scanner.isDone) {
|
||||
segments.add(route.path.replaceAll(_straySlashes, ''));
|
||||
lastRoute = route;
|
||||
|
||||
if (route is SymlinkRoute<T>) {
|
||||
search = route.router;
|
||||
}
|
||||
|
||||
resolved = true;
|
||||
break;
|
||||
} else {
|
||||
scanner.position = pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolved) {
|
||||
throw 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 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 RoutingException(
|
||||
'Link param $param is not Route, String, or Map<String, dynamic>.');
|
||||
}
|
||||
}
|
||||
|
||||
return absolute
|
||||
? '/${segments.join('/').replaceAll(_straySlashes, '')}'
|
||||
: segments.join('/');
|
||||
}
|
||||
|
||||
/// Finds the first [Route] that matches the given path,
|
||||
/// with the given method.
|
||||
bool resolve(String absolute, String relative, List<RoutingResult<T>> out,
|
||||
{String method = 'GET', bool strip = true}) {
|
||||
final cleanRelative =
|
||||
strip == false ? relative : stripStraySlashes(relative);
|
||||
var scanner = SpanScanner(cleanRelative);
|
||||
|
||||
bool crawl(Router<T> r) {
|
||||
bool success = false;
|
||||
|
||||
for (var route in r.routes) {
|
||||
int pos = scanner.position;
|
||||
|
||||
if (route is SymlinkRoute<T>) {
|
||||
if (route.parser.parse(scanner).successful) {
|
||||
var s = crawl(route.router);
|
||||
if (s) success = true;
|
||||
}
|
||||
|
||||
scanner.position = pos;
|
||||
} else if (route.method == '*' || route.method == method) {
|
||||
var parseResult = route.parser.parse(scanner);
|
||||
|
||||
if (parseResult.successful && scanner.isDone) {
|
||||
var result = RoutingResult<T>(
|
||||
parseResult: parseResult,
|
||||
params: parseResult.value.params,
|
||||
shallowRoute: route,
|
||||
shallowRouter: this,
|
||||
tail: (parseResult.value.tail ?? '') + scanner.rest);
|
||||
out.add(result);
|
||||
success = true;
|
||||
}
|
||||
|
||||
scanner.position = pos;
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
return crawl(this);
|
||||
}
|
||||
|
||||
/// Returns the result of [resolve] with [path] passed as
|
||||
/// both `absolute` and `relative`.
|
||||
Iterable<RoutingResult<T>> resolveAbsolute(String path,
|
||||
{String method = 'GET', bool strip = true}) =>
|
||||
resolveAll(path, path, method: method, strip: strip);
|
||||
|
||||
/// Finds every possible [Route] that matches the given path,
|
||||
/// with the given method.
|
||||
Iterable<RoutingResult<T>> resolveAll(String absolute, String relative,
|
||||
{String method = 'GET', bool strip = true}) {
|
||||
if (_useCache == true) {
|
||||
return _cache.putIfAbsent('$method$absolute',
|
||||
() => _resolveAll(absolute, relative, method: method, strip: strip));
|
||||
}
|
||||
|
||||
return _resolveAll(absolute, relative, method: method, strip: strip);
|
||||
}
|
||||
|
||||
Iterable<RoutingResult<T>> _resolveAll(String absolute, String relative,
|
||||
{String method = 'GET', bool strip = true}) {
|
||||
var results = <RoutingResult<T>>[];
|
||||
resolve(absolute, relative, results, method: method, strip: strip);
|
||||
|
||||
// _printDebug(
|
||||
// 'Results of $method "/${absolute.replaceAll(_straySlashes, '')}": ${results.map((r) => r.route).toList()}');
|
||||
return results;
|
||||
}
|
||||
|
||||
/// Incorporates another [Router]'s routes into this one's.
|
||||
SymlinkRoute<T> mount(String path, Router<T> router) {
|
||||
final route = SymlinkRoute<T>(path, router);
|
||||
_mounted[route.path] = router;
|
||||
_routes.add(route);
|
||||
//route._head = RegExp(route.matcher.pattern.replaceAll(_rgxEnd, ''));
|
||||
|
||||
return route;
|
||||
}
|
||||
|
||||
/// Adds a route that responds to any request matching the given path.
|
||||
Route<T> all(String path, T handler, {Iterable<T> middleware}) {
|
||||
return addRoute('*', path, handler, middleware: middleware);
|
||||
}
|
||||
|
||||
/// Adds a route that responds to a DELETE request.
|
||||
Route<T> delete(String path, T handler, {Iterable<T> middleware}) {
|
||||
return addRoute('DELETE', path, handler, middleware: middleware);
|
||||
}
|
||||
|
||||
/// Adds a route that responds to a GET request.
|
||||
Route<T> get(String path, T handler, {Iterable<T> middleware}) {
|
||||
return addRoute('GET', path, handler, middleware: middleware);
|
||||
}
|
||||
|
||||
/// Adds a route that responds to a HEAD request.
|
||||
Route<T> head(String path, T handler, {Iterable<T> middleware}) {
|
||||
return addRoute('HEAD', path, handler, middleware: middleware);
|
||||
}
|
||||
|
||||
/// Adds a route that responds to a OPTIONS request.
|
||||
Route<T> options(String path, T handler, {Iterable<T> middleware}) {
|
||||
return addRoute('OPTIONS', path, handler, middleware: middleware);
|
||||
}
|
||||
|
||||
/// Adds a route that responds to a POST request.
|
||||
Route<T> post(String path, T handler, {Iterable<T> middleware}) {
|
||||
return addRoute('POST', path, handler, middleware: middleware);
|
||||
}
|
||||
|
||||
/// Adds a route that responds to a PATCH request.
|
||||
Route<T> patch(String path, T handler, {Iterable<T> middleware}) {
|
||||
return addRoute('PATCH', path, handler, middleware: middleware);
|
||||
}
|
||||
|
||||
/// Adds a route that responds to a PUT request.
|
||||
Route put(String path, T handler, {Iterable<T> middleware}) {
|
||||
return addRoute('PUT', path, handler, middleware: middleware);
|
||||
}
|
||||
}
|
||||
|
||||
class _ChainedRouter<T> extends Router<T> {
|
||||
final List<T> _handlers = <T>[];
|
||||
Router _root;
|
||||
|
||||
_ChainedRouter.empty();
|
||||
|
||||
_ChainedRouter(Router root, Iterable<T> middleware) {
|
||||
this._root = root;
|
||||
_handlers.addAll(middleware);
|
||||
}
|
||||
|
||||
@override
|
||||
Route<T> addRoute(String method, String path, handler,
|
||||
{Iterable<T> middleware}) {
|
||||
var route = super.addRoute(method, path, handler,
|
||||
middleware: []..addAll(_handlers)..addAll(middleware ?? []));
|
||||
//_root._routes.add(route);
|
||||
return route;
|
||||
}
|
||||
|
||||
@override
|
||||
SymlinkRoute<T> group(String path, void callback(Router<T> router),
|
||||
{Iterable<T> middleware, String name}) {
|
||||
final router = _ChainedRouter<T>(
|
||||
_root, []..addAll(_handlers)..addAll(middleware ?? []));
|
||||
callback(router);
|
||||
return mount(path, router)..name = name;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SymlinkRoute<T>> groupAsync(
|
||||
String path, FutureOr<void> callback(Router<T> router),
|
||||
{Iterable<T> middleware, String name}) async {
|
||||
final router = _ChainedRouter<T>(
|
||||
_root, []..addAll(_handlers)..addAll(middleware ?? []));
|
||||
await callback(router);
|
||||
return mount(path, router)..name = name;
|
||||
}
|
||||
|
||||
@override
|
||||
SymlinkRoute<T> mount(String path, Router<T> router) {
|
||||
final route = super.mount(path, router);
|
||||
route.router._middleware.insertAll(0, _handlers);
|
||||
//_root._routes.add(route);
|
||||
return route;
|
||||
}
|
||||
|
||||
@override
|
||||
_ChainedRouter<T> chain(Iterable<T> middleware) {
|
||||
final piped = _ChainedRouter<T>.empty().._root = _root;
|
||||
piped._handlers.addAll([]..addAll(_handlers)..addAll(middleware));
|
||||
var route = SymlinkRoute<T>('/', piped);
|
||||
_routes.add(route);
|
||||
return piped;
|
||||
}
|
||||
}
|
||||
|
||||
/// Optimizes a router by condensing all its routes into one level.
|
||||
Router<T> flatten<T>(Router<T> router) {
|
||||
var flattened = Router<T>();
|
||||
|
||||
for (var route in router.routes) {
|
||||
if (route is SymlinkRoute<T>) {
|
||||
var base = route.path.replaceAll(_straySlashes, '');
|
||||
var child = flatten(route.router);
|
||||
|
||||
for (var route in child.routes) {
|
||||
var path = route.path.replaceAll(_straySlashes, '');
|
||||
var joined = '$base/$path'.replaceAll(_straySlashes, '');
|
||||
flattened.addRoute(route.method, joined.replaceAll(_straySlashes, ''),
|
||||
route.handlers.last,
|
||||
middleware:
|
||||
route.handlers.take(route.handlers.length - 1).toList());
|
||||
}
|
||||
} else {
|
||||
flattened.addRoute(route.method, route.path, route.handlers.last,
|
||||
middleware: route.handlers.take(route.handlers.length - 1).toList());
|
||||
}
|
||||
}
|
||||
|
||||
return flattened..enableCache();
|
||||
}
|
21
packages/route/lib/src/routing_exception.dart
Normal file
21
packages/route/lib/src/routing_exception.dart
Normal file
|
@ -0,0 +1,21 @@
|
|||
/// Represents an error in route configuration or navigation.
|
||||
abstract class RoutingException extends Exception {
|
||||
factory RoutingException(String message) => _RoutingExceptionImpl(message);
|
||||
|
||||
/// Occurs when trying to resolve the parent of a [Route] without a parent.
|
||||
factory RoutingException.orphan() => _RoutingExceptionImpl(
|
||||
"Tried to resolve path '..' on a route that has no parent.");
|
||||
|
||||
/// Occurs when the user attempts to navigate to a non-existent route.
|
||||
factory RoutingException.noSuchRoute(String path) => _RoutingExceptionImpl(
|
||||
"Tried to navigate to non-existent route: '$path'.");
|
||||
}
|
||||
|
||||
class _RoutingExceptionImpl implements RoutingException {
|
||||
final String message;
|
||||
|
||||
_RoutingExceptionImpl(this.message);
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
97
packages/route/lib/src/routing_result.dart
Normal file
97
packages/route/lib/src/routing_result.dart
Normal file
|
@ -0,0 +1,97 @@
|
|||
part of angel_route.src.router;
|
||||
|
||||
/// Represents a complex result of navigating to a path.
|
||||
class RoutingResult<T> {
|
||||
/// The parse result that matched the given sub-path.
|
||||
final ParseResult<RouteResult> parseResult;
|
||||
|
||||
/// A nested instance, if a sub-path was matched.
|
||||
final Iterable<RoutingResult<T>> nested;
|
||||
|
||||
/// All route params matching this route on the current sub-path.
|
||||
final Map<String, dynamic> params = {};
|
||||
|
||||
/// The [Route] that answered this sub-path.
|
||||
///
|
||||
/// This is mostly for internal use, and useless in production.
|
||||
final Route<T> shallowRoute;
|
||||
|
||||
/// The [Router] that answered this sub-path.
|
||||
///
|
||||
/// Only really for internal use.
|
||||
final Router<T> shallowRouter;
|
||||
|
||||
/// The remainder of the full path that was not matched, and was passed to [nested] routes.
|
||||
final String tail;
|
||||
|
||||
/// The [RoutingResult] that matched the most specific sub-path.
|
||||
RoutingResult<T> get deepest {
|
||||
var search = this;
|
||||
|
||||
while (search?.nested?.isNotEmpty == true) {
|
||||
search = search.nested.first;
|
||||
}
|
||||
|
||||
return search;
|
||||
}
|
||||
|
||||
/// The most specific route.
|
||||
Route<T> get route => deepest.shallowRoute;
|
||||
|
||||
/// The most specific router.
|
||||
Router<T> get router => deepest.shallowRouter;
|
||||
|
||||
/// The handlers at this sub-path.
|
||||
List<T> get handlers {
|
||||
return <T>[]
|
||||
..addAll(shallowRouter.middleware)
|
||||
..addAll(shallowRoute.handlers);
|
||||
}
|
||||
|
||||
/// All handlers on this sub-path and its children.
|
||||
List<T> get allHandlers {
|
||||
final handlers = <T>[];
|
||||
|
||||
void crawl(RoutingResult<T> result) {
|
||||
handlers.addAll(result.handlers);
|
||||
|
||||
if (result.nested?.isNotEmpty == true) {
|
||||
for (var r in result.nested) {
|
||||
crawl(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
crawl(this);
|
||||
|
||||
return handlers;
|
||||
}
|
||||
|
||||
/// All parameters on this sub-path and its children.
|
||||
Map<String, dynamic> get allParams {
|
||||
final Map<String, dynamic> params = {};
|
||||
|
||||
void crawl(RoutingResult result) {
|
||||
params.addAll(result.params);
|
||||
|
||||
if (result.nested?.isNotEmpty == true) {
|
||||
for (var r in result.nested) {
|
||||
crawl(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
crawl(this);
|
||||
return params;
|
||||
}
|
||||
|
||||
RoutingResult(
|
||||
{this.parseResult,
|
||||
Map<String, dynamic> params = const {},
|
||||
this.nested,
|
||||
this.shallowRoute,
|
||||
this.shallowRouter,
|
||||
@required this.tail}) {
|
||||
this.params.addAll(params ?? {});
|
||||
}
|
||||
}
|
9
packages/route/lib/src/symlink_route.dart
Normal file
9
packages/route/lib/src/symlink_route.dart
Normal file
|
@ -0,0 +1,9 @@
|
|||
part of angel_route.src.router;
|
||||
|
||||
/// Placeholder [Route] to serve as a symbolic link
|
||||
/// to a mounted [Router].
|
||||
class SymlinkRoute<T> extends Route<T> {
|
||||
final Router<T> router;
|
||||
SymlinkRoute(String path, this.router)
|
||||
: super(path, method: null, handlers: null);
|
||||
}
|
43
packages/route/lib/string_util.dart
Normal file
43
packages/route/lib/string_util.dart
Normal file
|
@ -0,0 +1,43 @@
|
|||
/// Helper functions to performantly transform strings, without `RegExp`.
|
||||
library angel_route.string_util;
|
||||
|
||||
/// Removes leading and trailing occurrences of a pattern from a string.
|
||||
String stripStray(String haystack, String needle) {
|
||||
int firstSlash;
|
||||
|
||||
if (haystack.startsWith(needle)) {
|
||||
firstSlash = haystack.indexOf(needle);
|
||||
if (firstSlash == -1) return haystack;
|
||||
} else {
|
||||
firstSlash = -1;
|
||||
}
|
||||
|
||||
if (firstSlash == haystack.length - 1) {
|
||||
return haystack.length == 1 ? '' : haystack.substring(0, firstSlash);
|
||||
}
|
||||
|
||||
// Find last leading index of slash
|
||||
for (int i = firstSlash + 1; i < haystack.length; i++) {
|
||||
if (haystack[i] != needle) {
|
||||
var sub = haystack.substring(i);
|
||||
|
||||
if (!sub.endsWith(needle)) return sub;
|
||||
|
||||
var lastSlash = sub.lastIndexOf(needle);
|
||||
|
||||
for (int j = lastSlash - 1; j >= 0; j--) {
|
||||
if (sub[j] != needle) {
|
||||
return sub.substring(0, j + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return lastSlash == -1 ? sub : sub.substring(0, lastSlash);
|
||||
}
|
||||
}
|
||||
|
||||
return haystack.substring(0, firstSlash);
|
||||
}
|
||||
|
||||
String stripStraySlashes(String str) => stripStray(str, '/');
|
||||
|
||||
String stripRegexStraySlashes(String str) => stripStray(str, '\\/');
|
18
packages/route/pubspec.yaml
Normal file
18
packages/route/pubspec.yaml
Normal file
|
@ -0,0 +1,18 @@
|
|||
name: angel_route
|
||||
description: A powerful, isomorphic routing library for Dart. It is mainly used in the Angel framework, but can be used in Flutter and on the Web.
|
||||
version: 3.1.0+1
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
homepage: https://github.com/angel-dart/angel_route
|
||||
environment:
|
||||
sdk: ">=2.0.0-dev <3.0.0"
|
||||
dependencies:
|
||||
combinator: ^1.0.0
|
||||
meta: ^1.0.0
|
||||
path: ^1.0.0
|
||||
string_scanner: ^1.0.0
|
||||
dev_dependencies:
|
||||
build_runner: ^0.10.0
|
||||
build_web_compilers: ^0.4.0
|
||||
http: ">=0.11.3 <0.12.0"
|
||||
pedantic: ^1.0.0
|
||||
test: ^1.0.0
|
2
packages/route/repubspec.yaml
Normal file
2
packages/route/repubspec.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
push_state:
|
||||
base: push_state/basic.html
|
19
packages/route/test/chain_nest_test.dart
Normal file
19
packages/route/test/chain_nest_test.dart
Normal file
|
@ -0,0 +1,19 @@
|
|||
import 'package:angel_route/angel_route.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
main() {
|
||||
var router = Router<String>()
|
||||
..chain(['a']).group('/b', (router) {
|
||||
router.chain(['c']).chain(['d']).group('/e', (router) {
|
||||
router.get('f', 'g');
|
||||
});
|
||||
})
|
||||
..dumpTree();
|
||||
|
||||
test('nested route groups with chain', () {
|
||||
var r = router.resolveAbsolute('/b/e/f')?.first?.route;
|
||||
expect(r, isNotNull);
|
||||
expect(r.handlers, hasLength(4));
|
||||
expect(r.handlers, equals(['a', 'c', 'd', 'g']));
|
||||
});
|
||||
}
|
44
packages/route/test/navigate_test.dart
Normal file
44
packages/route/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 = Router();
|
||||
|
||||
router.get('/', 'GET').name = 'root';
|
||||
router.get('/user/:id', 'GET');
|
||||
router.get('/first/:first/last/:last', 'GET').name = 'full_name';
|
||||
|
||||
navigate(params) {
|
||||
final uri = router.navigate(params as Iterable);
|
||||
print('Uri: $uri');
|
||||
return uri;
|
||||
}
|
||||
|
||||
router.dumpTree();
|
||||
|
||||
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('/'));
|
||||
});
|
||||
});
|
||||
}
|
43
packages/route/test/params_test.dart
Normal file
43
packages/route/test/params_test.dart
Normal file
|
@ -0,0 +1,43 @@
|
|||
import 'package:angel_route/angel_route.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
main() {
|
||||
final router = Router()..get('/hello', '')..get('/user/:id', '');
|
||||
|
||||
router.group('/book/:id', (router) {
|
||||
router.get('/reviews', '');
|
||||
router.get('/readers/:readerId', '');
|
||||
});
|
||||
|
||||
router.mount('/color', Router()..get('/:name/shades', ''));
|
||||
|
||||
setUp(router.dumpTree);
|
||||
|
||||
void expectParams(String path, Map<String, dynamic> params) {
|
||||
final p = {};
|
||||
final resolved = router.resolveAll(path, path);
|
||||
print('Resolved $path => ${resolved.map((r) => r.allParams).toList()}');
|
||||
for (final result in resolved) {
|
||||
p.addAll(result.allParams);
|
||||
}
|
||||
expect(p, equals(params));
|
||||
}
|
||||
|
||||
group('top-level', () {
|
||||
test('no params', () => expectParams('/hello', {}));
|
||||
|
||||
test('one param', () => expectParams('/user/0', {'id': '0'}));
|
||||
});
|
||||
|
||||
group('group', () {
|
||||
//test('root', () => expectParams('/book/1337', {'id': '1337'}));
|
||||
test('path', () => expectParams('/book/1337/reviews', {'id': '1337'}));
|
||||
test(
|
||||
'two params',
|
||||
() => expectParams(
|
||||
'/book/1337/readers/foo', {'id': '1337', 'readerId': 'foo'}));
|
||||
});
|
||||
|
||||
test('mount',
|
||||
() => expectParams('/color/chartreuse/shades', {'name': 'chartreuse'}));
|
||||
}
|
20
packages/route/test/parse_test.dart
Normal file
20
packages/route/test/parse_test.dart
Normal file
|
@ -0,0 +1,20 @@
|
|||
import 'package:angel_route/angel_route.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
var router = Router()
|
||||
..get('/int/int:id', '')
|
||||
..get('/double/double:id', '')
|
||||
..get('/num/num:id', '');
|
||||
|
||||
num getId(String path) {
|
||||
var result = router.resolveAbsolute(path).first;
|
||||
return result.allParams['id'] as num;
|
||||
}
|
||||
|
||||
test('parse', () {
|
||||
expect(getId('/int/2'), 2);
|
||||
expect(getId('/double/2.0'), 2.0);
|
||||
expect(getId('/num/-2.4'), -2.4);
|
||||
});
|
||||
}
|
15
packages/route/test/root_test.dart
Normal file
15
packages/route/test/root_test.dart
Normal file
|
@ -0,0 +1,15 @@
|
|||
import 'package:angel_route/angel_route.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
test('resolve / on /', () {
|
||||
var router = Router()
|
||||
..group('/', (router) {
|
||||
router.group('/', (router) {
|
||||
router.get('/', 'ok');
|
||||
});
|
||||
});
|
||||
|
||||
expect(router.resolveAbsolute('/'), isNotNull);
|
||||
});
|
||||
}
|
201
packages/route/test/server_test.dart
Normal file
201
packages/route/test/server_test.dart
Normal file
|
@ -0,0 +1,201 @@
|
|||
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';
|
||||
|
||||
const List<Map<String, String>> people = [
|
||||
{'name': 'John Smith'}
|
||||
];
|
||||
|
||||
main() {
|
||||
http.Client client;
|
||||
|
||||
final Router router = Router();
|
||||
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;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
final beatles = Router();
|
||||
|
||||
beatles.post('/spinal_clacker', (req, res) {
|
||||
res.write('come ');
|
||||
return true;
|
||||
});
|
||||
|
||||
final yellow = Router()
|
||||
..get('/submarine', (req, res) {
|
||||
res.write('we all live in a');
|
||||
return false;
|
||||
});
|
||||
|
||||
beatles.group('/big', (router) {
|
||||
router.mount('/yellow', yellow);
|
||||
});
|
||||
|
||||
beatles.all('*', (req, res) {
|
||||
res.write('together');
|
||||
return false;
|
||||
});
|
||||
|
||||
router.mount('/beatles', beatles);
|
||||
|
||||
setUp(() async {
|
||||
client = http.Client();
|
||||
|
||||
router.dumpTree();
|
||||
server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
||||
url = 'http://${server.address.address}:${server.port}';
|
||||
|
||||
server.listen((req) async {
|
||||
final res = req.response;
|
||||
|
||||
// Easy middleware pipeline
|
||||
final results =
|
||||
router.resolveAbsolute(req.uri.toString(), method: req.method);
|
||||
final pipeline = MiddlewarePipeline(results);
|
||||
|
||||
if (pipeline.handlers.isEmpty) {
|
||||
res
|
||||
..statusCode = 404
|
||||
..writeln('404 Not Found');
|
||||
} else {
|
||||
for (final handler in pipeline.handlers) {
|
||||
if (!((await handler(req, res)) as bool)) 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('mount', () {
|
||||
group('path', () {
|
||||
test('top-level', () async {
|
||||
final res = await client.post('$url/beatles/spinal_clacker');
|
||||
print('Response: ${res.body}');
|
||||
expect(res.body, equals('come together'));
|
||||
});
|
||||
|
||||
test('fallback', () async {
|
||||
final res = await client.patch('$url/beatles/muddy_water');
|
||||
print('Response: ${res.body}');
|
||||
expect(res.body, equals('together'));
|
||||
});
|
||||
|
||||
test('fallback', () async {
|
||||
final res = await client.patch('$url/beatles/spanil_clakcer');
|
||||
print('Response: ${res.body}');
|
||||
expect(res.body, equals('together'));
|
||||
});
|
||||
});
|
||||
|
||||
test('deep nested', () async {
|
||||
final res = await client.get('$url/beatles/big/yellow/submarine');
|
||||
print('Response: ${res.body}');
|
||||
expect(res.body, equals('we all live in a'));
|
||||
});
|
||||
|
||||
group('fallback', () {});
|
||||
});
|
||||
|
||||
group('404', () {
|
||||
expect404(r) => r.then((res) {
|
||||
print('Response (${res.statusCode}): ${res.body}');
|
||||
expect(res.statusCode, equals(404));
|
||||
});
|
||||
|
||||
test('path', () async {
|
||||
await expect404(client.get('$url/foo'));
|
||||
await expect404(client.get('$url/bye'));
|
||||
await expect404(client.get('$url/people/0/age'));
|
||||
await expect404(client.get('$url/beatles2'));
|
||||
});
|
||||
|
||||
test('method', () async {
|
||||
await expect404(client.head(url));
|
||||
await expect404(client.patch('$url/people'));
|
||||
await expect404(client.post('$url/people/0'));
|
||||
await expect404(client.delete('$url/beatles2/spinal_clacker'));
|
||||
});
|
||||
});
|
||||
}
|
46
packages/route/test/strip_test.dart
Normal file
46
packages/route/test/strip_test.dart
Normal file
|
@ -0,0 +1,46 @@
|
|||
import 'package:angel_route/string_util.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
main() {
|
||||
test('strip leading', () {
|
||||
var a = '///a';
|
||||
var b = stripStraySlashes(a);
|
||||
print('$a => $b');
|
||||
expect(b, 'a');
|
||||
});
|
||||
|
||||
test('strip trailing', () {
|
||||
var a = 'a///';
|
||||
var b = stripStraySlashes(a);
|
||||
print('$a => $b');
|
||||
expect(b, 'a');
|
||||
});
|
||||
|
||||
test('strip both', () {
|
||||
var a = '///a///';
|
||||
var b = stripStraySlashes(a);
|
||||
print('$a => $b');
|
||||
expect(b, 'a');
|
||||
});
|
||||
|
||||
test('intermediate slashes preserved', () {
|
||||
var a = '///a///b//';
|
||||
var b = stripStraySlashes(a);
|
||||
print('$a => $b');
|
||||
expect(b, 'a///b');
|
||||
});
|
||||
|
||||
test('only if starts with', () {
|
||||
var a = 'd///a///b//';
|
||||
var b = stripStraySlashes(a);
|
||||
print('$a => $b');
|
||||
expect(b, 'd///a///b');
|
||||
});
|
||||
|
||||
test('only if ends with', () {
|
||||
var a = '///a///b//c';
|
||||
var b = stripStraySlashes(a);
|
||||
print('$a => $b');
|
||||
expect(b, 'a///b//c');
|
||||
});
|
||||
}
|
18
packages/route/test/uri_decode_test.dart
Normal file
18
packages/route/test/uri_decode_test.dart
Normal file
|
@ -0,0 +1,18 @@
|
|||
import 'package:angel_route/angel_route.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
test('uri params decoded', () {
|
||||
var router = Router()..get('/a/:a/b/:b', '');
|
||||
|
||||
var encoded =
|
||||
'/a/' + Uri.encodeComponent('<<<') + '/b/' + Uri.encodeComponent('???');
|
||||
print(encoded);
|
||||
var result = router.resolveAbsolute(encoded).first;
|
||||
print(result.allParams);
|
||||
expect(result.allParams, {
|
||||
'a': '<<<',
|
||||
'b': '???',
|
||||
});
|
||||
});
|
||||
}
|
46
packages/route/test/wildcard_test.dart
Normal file
46
packages/route/test/wildcard_test.dart
Normal file
|
@ -0,0 +1,46 @@
|
|||
import 'package:angel_route/angel_route.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
var router = Router();
|
||||
router.get('/songs/*/key', 'of life');
|
||||
router.get('/isnt/she/*', 'lovely');
|
||||
router.all('*', 'stevie');
|
||||
|
||||
test('match until end if * is last', () {
|
||||
var result = router.resolveAbsolute('/wonder').first;
|
||||
expect(result.handlers, ['stevie']);
|
||||
});
|
||||
|
||||
test('match if not last', () {
|
||||
var result = router.resolveAbsolute('/songs/what/key').first;
|
||||
expect(result.handlers, ['of life']);
|
||||
});
|
||||
|
||||
test('match if segments before', () {
|
||||
var result =
|
||||
router.resolveAbsolute('/isnt/she/fierce%20harmonica%solo').first;
|
||||
expect(result.handlers, ['lovely']);
|
||||
});
|
||||
|
||||
test('tail explicitly set intermediate', () {
|
||||
var results = router.resolveAbsolute('/songs/in_the/key');
|
||||
var result = results.first;
|
||||
print(results.map((r) => {r.route.path: r.tail}));
|
||||
expect(result.tail, 'in_the');
|
||||
});
|
||||
|
||||
test('tail explicitly set at end', () {
|
||||
var results = router.resolveAbsolute('/isnt/she/epic');
|
||||
var result = results.first;
|
||||
print(results.map((r) => {r.route.path: r.tail}));
|
||||
expect(result.tail, 'epic');
|
||||
});
|
||||
|
||||
test('tail with trailing', () {
|
||||
var results = router.resolveAbsolute('/isnt/she/epic/fail');
|
||||
var result = results.first;
|
||||
print(results.map((r) => {r.route.path: r.tail}));
|
||||
expect(result.tail, 'epic/fail');
|
||||
});
|
||||
}
|
4
packages/route/web/hash/basic.dart
Normal file
4
packages/route/web/hash/basic.dart
Normal file
|
@ -0,0 +1,4 @@
|
|||
import 'package:angel_route/browser.dart';
|
||||
import '../shared/basic.dart';
|
||||
|
||||
main() => basic(BrowserRouter(hash: true));
|
30
packages/route/web/hash/basic.html
Normal file
30
packages/route/web/hash/basic.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
<!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.js"></script>
|
||||
</body>
|
||||
</html>
|
15
packages/route/web/index.html
Normal file
15
packages/route/web/index.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Angel Route Samples</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Angel Route Samples</h1>
|
||||
<ul>
|
||||
<li><a href="hash/basic.html">Hash-based</a></li>
|
||||
<li><a href="push_state/basic.html">Push-state</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
4
packages/route/web/push_state/basic.dart
Normal file
4
packages/route/web/push_state/basic.dart
Normal file
|
@ -0,0 +1,4 @@
|
|||
import 'package:angel_route/browser.dart';
|
||||
import '../shared/basic.dart';
|
||||
|
||||
main() => basic(BrowserRouter());
|
31
packages/route/web/push_state/basic.html
Normal file
31
packages/route/web/push_state/basic.html
Normal file
|
@ -0,0 +1,31 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<base href="/push_state">
|
||||
<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.js"></script>
|
||||
</body>
|
||||
</html>
|
37
packages/route/web/shared/basic.dart
Normal file
37
packages/route/web/shared/basic.dart
Normal file
|
@ -0,0 +1,37 @@
|
|||
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.onResolve.listen((result) {
|
||||
final route = result?.route;
|
||||
|
||||
if (route == null) {
|
||||
$h1.text = 'No Active Route';
|
||||
$ul.children
|
||||
..clear()
|
||||
..add(LIElement()..text = '(empty)');
|
||||
} else {
|
||||
$h1.text = 'Active Route: ${route.name ?? route.path}';
|
||||
$ul.children
|
||||
..clear()
|
||||
..addAll(result.allHandlers
|
||||
.map((handler) => LIElement()..text = handler.toString()));
|
||||
}
|
||||
});
|
||||
|
||||
router.get('a', 'a handler');
|
||||
|
||||
router.group('b', (router) {
|
||||
router.get('a', 'b/a handler').name = 'b/a';
|
||||
router.get('b', 'b/b handler', middleware: ['b/b middleware']).name = 'b/b';
|
||||
}, middleware: ['b middleware']);
|
||||
|
||||
router.get('c', 'c handler');
|
||||
|
||||
router
|
||||
..dumpTree()
|
||||
..listen();
|
||||
}
|
Loading…
Reference in a new issue