Add 'packages/route/' from commit '8b2b0524609ab737eb5569fdf82a7ac491fdc67b'

git-subtree-dir: packages/route
git-subtree-mainline: 6db839928b
git-subtree-split: 8b2b052460
This commit is contained in:
Tobe O 2020-02-15 18:22:17 -05:00
commit 5eef4314ec
37 changed files with 2248 additions and 0 deletions

77
packages/route/.gitignore vendored Normal file
View 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

View 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>

View file

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

View file

@ -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>

View file

@ -0,0 +1,3 @@
language: dart
dart:
- stable

View 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
View 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
View file

@ -0,0 +1,147 @@
# angel_route
[![Pub](https://img.shields.io/pub/v/angel_route.svg)](https://pub.dartlang.org/packages/angel_route)
[![build status](https://travis-ci.org/angel-dart/route.svg)](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'};
```

View file

@ -0,0 +1,4 @@
include: package:pedantic/analysis_options.yaml
analyzer:
strong-mode:
implicit-casts: false

View 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);
}

View file

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

View 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);
}
}

View 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))
});
});
}
}

View 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();
}

View 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);
}
}

View 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();
}

View 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;
}

View 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 ?? {});
}
}

View 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);
}

View 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, '\\/');

View 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

View file

@ -0,0 +1,2 @@
push_state:
base: push_state/basic.html

View 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']));
});
}

View 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('/'));
});
});
}

View 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'}));
}

View 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);
});
}

View 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);
});
}

View 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'));
});
});
}

View 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');
});
}

View 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': '???',
});
});
}

View 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');
});
}

View file

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

View 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>

View 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>

View file

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

View 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>

View 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();
}