add(angel3): adding re-branded angel3 route package

This commit is contained in:
Patrick Stewart 2024-09-22 18:46:14 -07:00
parent e12b15f8c8
commit 3b83e34dcc
34 changed files with 2374 additions and 0 deletions

71
packages/route/.gitignore vendored Normal file
View file

@ -0,0 +1,71 @@
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
.dart_tool
.packages
.pub/
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/
### Dart template
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
# SDK 1.20 and later (no longer creates packages directories)
# Older SDK versions
# (Include if the minimum SDK version specified in pubsepc.yaml is earlier than 1.20)
.project
.buildlog
**/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
# Don't commit pubspec lock file
# (Library packages only! Remove pattern if developing an application package)
### 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:
## VsCode
.vscode/
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
.idea/
/out/
.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

12
packages/route/AUTHORS.md Normal file
View file

@ -0,0 +1,12 @@
Primary Authors
===============
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
Thomas is the current maintainer of the code base. He has refactored and migrated the
code base to support NNBD.
* __[Tobe O](thosakwe@gmail.com)__
Tobe has written much of the original code prior to NNBD migration. He has moved on and
is no longer involved with the project.

View file

@ -0,0 +1,98 @@
# Change Log
## 8.1.1
* Updated repository link
## 8.1.0
* Updated `lints` to 3.0.0
* Fixed analyser warnings
## 8.0.0
* Require Dart >= 3.0
* Updated `build_web_compilers` to 4.0.0
* Updated `http` to 1.0.0
## 7.0.0
* Require Dart >= 2.17
## 6.0.0
* Updated to 2.16.x
## 5.2.0
* Updated `package:build_runner`
* Updated `package:build_web_compiler`
## 5.1.0
* Updated to use `package:belatuk_combinator`
* Updated linter to `package:lints`
## 5.0.1
* Updated README
## 5.0.0
* Migrated to support Dart >= 2.12 NNBD
## 4.0.0
* Migrated to work with Dart >= 2.12 Non NNBD
## 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`.

29
packages/route/LICENSE Normal file
View file

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2021, dukefirehawk.com
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

136
packages/route/README.md Normal file
View file

@ -0,0 +1,136 @@
# Angel3 Route
![Pub Version (including pre-releases)](https://img.shields.io/pub/v/angel3_route?include_prereleases)
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion)
[![License](https://img.shields.io/github/license/dart-backend/angel)](https://github.com/dart-backend/angel/tree/master/packages/route/LICENSE)
A powerful, isomorphic routing library for Dart.
`angel3_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.
`angel3_route` does not require the use of [Angel 3](https://pub.dev/packages/angel3_framework), 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
- [Angel3 Route](#angel3-route)
- [Contents](#contents)
- [Examples](#examples)
- [Routing](#routing)
- [Hierarchy](#hierarchy)
- [In the Browser](#in-the-browser)
- [Route State](#route-state)
- [Route Parameters](#route-parameters)
## Examples
### Routing
If you use [Angel3](https://pub.dev/packages/angel3_framework), every `Angel` instance is a `Router` in itself.
```dart
void 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
void 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.
`angel3_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` 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 @@
include: package:lints/recommended.yaml

View file

@ -0,0 +1,58 @@
import 'dart:math';
import 'package:platform_route/platform_route.dart';
void 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,225 @@
import 'dart:async' show Stream, StreamController;
import 'dart:html';
import 'package:path/path.dart' as p;
import 'platform_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;
final StreamController<RoutingResult<T?>> _onResolve =
StreamController<RoutingResult<T?>>();
final 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 = false}) : super() {
if (listen != false) this.listen();
prepareAnchors();
}
@override
void go(Iterable linkParams) => _goTo(navigate(linkParams));
@override
Route on(String path, T handler, {Iterable<T> middleware = const []}) =>
all(path, handler, middleware: middleware);
void prepareAnchors() {
final anchors = window.document
.querySelectorAll('a')
.cast<AnchorElement>(); //:not([dynamic])');
for (final $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({required 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);
if (allResolved.isEmpty) {
// TODO: Need fixing
//_onResolve.add(null);
//_onRoute.add(_current = null);
_current = null;
} else {
var resolved = allResolved.first;
if (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.route != _current) {
_onResolve.add(resolved);
_onRoute.add(_current = resolved.route);
}
}
@override
void _listen() {
window.onHashChange.listen(handleHash);
handleHash();
}
}
class _PushStateRouter<T> extends _BrowserRouterImpl<T> {
late String _basePath;
_PushStateRouter({required bool listen}) : 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) {
relativeUri = p.join(_basePath, uri.replaceAll(_straySlashes, ''));
}
//if (resolved == null) {
// _onResolve.add(null);
// _onRoute.add(_current = null);
//} else {
final route = resolved.route;
var thisPath = route.name ?? '';
if (thisPath.isEmpty) {
thisPath = route.path;
}
window.history
.pushState({'path': route.path, 'params': {}}, thisPath, 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.route != _current) {
//properties.addAll(state['properties'] ?? {});
_onResolve.add(resolved);
_onRoute.add(_current = resolved.route);
} else {
//_onResolve.add(null);
//_onRoute.add(_current = null);
_current = null;
}
} else {
//_onResolve.add(null);
//_onRoute.add(_current = null);
_current = null;
}
}
@override
void _listen() {
window.onPopState.listen((e) {
handleState(e.state);
});
handleState(window.history.state);
}
}

View file

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

View file

@ -0,0 +1,327 @@
part of 'router.dart';
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 r2 = r.value![2];
Match? rgxMatch;
if (r2 != 'NULL') {
rgxMatch = r2 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');
}
}
// TODO: relook at this later
var m2 = match[2] ?? '';
var s = ParameterSegment(m2, 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 (var i = 0; i < segments.length; i++) {
var s = segments[i];
var 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) {
var result = r.scanner.lastMatch;
if (result != null) {
//return RouteResult({}, tail: r.scanner.lastMatch![1])
return RouteResult({}, tail: result[1]);
} else {
return RouteResult({});
}
});
}
@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) {
// Return an empty RouteResult if null
if (r.value == null) {
return RouteResult({});
}
var v = r.value!;
if (v[1] == null) {
return v[0] as RouteResult;
}
return (v[0] as RouteResult)
..addAll({name: Uri.decodeComponent(v 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() {
if (regExp != null) {
return match<String>(regExp!).value((r) {
var result = r.scanner.lastMatch;
if (result != null) {
// TODO: Invalid method
//return r.scanner.lastMatch![1];
return result.toString();
} else {
return '';
}
});
} else {
return 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,50 @@
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;
final 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;
*/
if (_handlers.isNotEmpty) {
return _handlers;
}
for (var result in routingResults) {
_handlers.addAll(result.allHandlers);
}
return _handlers;
}
MiddlewarePipeline(Iterable<RoutingResult<T>> routingResults)
: routingResults = routingResults.toList();
}
/// Iterates through a [MiddlewarePipeline].
class MiddlewarePipelineIterator<T> implements 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,99 @@
part of 'router.dart';
/// Represents a virtual location within an application.
class Route<T> {
final String method;
final String path;
final Map<String, Map<String, dynamic>> _cache = {};
final RouteDefinition? _routeDefinition;
final List<T> handlers;
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({}));
}
/*
var result = RouteGrammar.routeDefinition
.parse(SpanScanner(path.replaceAll(_straySlashes, '')));
if (result.value != null) {
//throw ArgumentError('[Route] Failed to create route for $path');
_routeDefinition = result.value;
if (_routeDefinition.segments.isEmpty) {
_parser = match('').map((r) => RouteResult({}));
}
} else {
_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);
}
//List<T> get handlers => _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();
var i = 0;
if (_routeDefinition != null) {
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,493 @@
library platform_route.src.router;
import 'dart:async';
import 'package:belatuk_combinator/belatuk_combinator.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 = const []}) {
if (_useCache == true) {
throw StateError('Cannot add routes after caching is enabled.');
}
// Check if any mounted routers can match this
final handlers = <T>[handler];
//middleware ??= <T>[];
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 {
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(
{Function(String tree)? callback,
String header = 'Dumping route tree:',
String tab = ' '}) {
final buf = StringBuffer();
var tabs = 0;
if (header.isNotEmpty) {
buf.writeln(header);
}
buf.writeln('<root>');
void indent() {
for (var i = 0; i < tabs; i++) {
buf.write(tab);
}
}
void dumpRouter(Router router) {
indent();
tabs++;
for (var 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 Function(Router<T> router) callback,
{Iterable<T> middleware = const [], String name = ''}) {
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> Function(Router<T> router) callback,
{Iterable<T> middleware = const [], String name = ''}) async {
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 segments = <String>[];
Router search = this;
Route? lastRoute;
for (final param in linkParams) {
var resolved = false;
if (param is String) {
// Search by name
for (var 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 (var route in search.routes) {
var pos = scanner.position;
var parseResult = route.parser?.parse(scanner);
if (parseResult != null) {
if (parseResult.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;
}
} 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) {
var success = false;
for (var route in r.routes) {
var pos = scanner.position;
if (route is SymlinkRoute<T>) {
if (route.parser != null) {
var pp = route.parser!;
if (pp.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 != null) {
if (parseResult.successful && scanner.isDone) {
var tailResult = parseResult.value?.tail ?? '';
//print(tailResult);
var result = RoutingResult<T>(
parseResult: parseResult,
params: parseResult.value!.params,
shallowRoute: route,
shallowRouter: this,
tail: tailResult + 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 = const []}) {
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 = const []}) {
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 = const []}) {
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 = const []}) {
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 = const {}}) {
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 = const []}) {
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 = const []}) {
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 = const []}) {
return addRoute('PUT', path, handler, middleware: middleware);
}
}
class ChainedRouter<T> extends Router<T> {
final List<T> _handlers = <T>[];
Router _root;
ChainedRouter.empty() : _root = Router();
ChainedRouter(this._root, Iterable<T> middleware) {
_handlers.addAll(middleware);
}
@override
Route<T> addRoute(String method, String path, handler,
{Iterable<T> middleware = const []}) {
var route = super.addRoute(method, path, handler,
middleware: [..._handlers, ...middleware]);
//_root._routes.add(route);
return route;
}
@override
SymlinkRoute<T> group(String path, void Function(Router<T> router) callback,
{Iterable<T> middleware = const [], String? name}) {
final router = ChainedRouter<T>(_root, [..._handlers, ...middleware]);
callback(router);
return mount(path, router)..name = name;
}
@override
Future<SymlinkRoute<T>> groupAsync(
String path, FutureOr<void> Function(Router<T> router) callback,
{Iterable<T> middleware = const [], String? name}) async {
final router = ChainedRouter<T>(_root, [..._handlers, ...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([..._handlers, ...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 implements 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,95 @@
part of 'router.dart';
/// 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>[...shallowRouter.middleware, ...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 params = <String, dynamic>{};
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(
{required this.parseResult,
Map<String, dynamic> params = const {},
this.nested = const Iterable.empty(),
required this.shallowRoute,
required this.shallowRouter,
required this.tail}) {
this.params.addAll(params);
}
}

View file

@ -0,0 +1,8 @@
part of 'router.dart';
/// Placeholder [Route] to serve as a symbolic link
/// to a mounted [Router].
class SymlinkRoute<T> extends Route<T> {
final Router<T> router;
SymlinkRoute(super.path, this.router) : super(method: 'GET', handlers: <T>[]);
}

View file

@ -0,0 +1,43 @@
/// Helper functions to performantly transform strings, without `RegExp`.
library angel3_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 (var 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 (var 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,17 @@
name: platform_route
version: 9.0.0
description: A powerful, isomorphic routing library for Dart. It is mainly used in the Angel3 framework, but can be used in Flutter and on the Web.
homepage: https://angel3-framework.web.app/
repository: https://github.com/dart-backend/angel/tree/master/packages/route
environment:
sdk: '>=3.3.0 <4.0.0'
dependencies:
belatuk_combinator: ^5.1.0
string_scanner: ^1.2.0
path: ^1.8.0
dev_dependencies:
build_runner: ^2.4.0
build_web_compilers: ^4.0.0
http: ^1.0.0
test: ^1.24.0
lints: ^4.0.0

View file

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

View file

@ -0,0 +1,19 @@
import 'package:platform_route/platform_route.dart';
import 'package:test/test.dart';
void 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:platform_route/platform_route.dart';
import 'package:test/test.dart';
void main() {
final router = Router();
router.get('/', 'GET').name = 'root';
router.get('/user/:id', 'GET');
router.get('/first/:first/last/:last', 'GET').name = 'full_name';
String 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,45 @@
import 'package:platform_route/platform_route.dart';
import 'package:test/test.dart';
void 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:platform_route/platform_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:platform_route/platform_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,205 @@
import 'dart:convert';
import 'dart:io';
import 'package:platform_route/platform_route.dart';
import 'package:http/http.dart' as http;
import 'package:test/test.dart';
const List<Map<String, String>> people = [
{'name': 'John Smith'}
];
void main() {
http.Client? client;
final router = Router();
late 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(Uri.parse(url!));
print('Response: ${res.body}');
expect(res.body, equals('Root'));
});
test('path', () async {
final res = await client!.get(Uri.parse('$url/hello'));
print('Response: ${res.body}');
expect(res.body, equals('World'));
});
});
});
group('group', () {
group('top-level', () {
test('root', () async {
final res = await client!.get(Uri.parse('$url/people'));
print('Response: ${res.body}');
expect(json.decode(res.body), equals(people));
});
group('param', () {
test('root', () async {
final res = await client!.get(Uri.parse('$url/people/0'));
print('Response: ${res.body}');
expect(json.decode(res.body), equals(people.first));
});
test('path', () async {
final res = await client!.get(Uri.parse('$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(Uri.parse('$url/beatles/spinal_clacker'));
print('Response: ${res.body}');
expect(res.body, equals('come together'));
});
test('fallback', () async {
final res = await client!.patch(Uri.parse('$url/beatles/muddy_water'));
print('Response: ${res.body}');
expect(res.body, equals('together'));
});
test('fallback', () async {
final res =
await client!.patch(Uri.parse('$url/beatles/spanil_clakcer'));
print('Response: ${res.body}');
expect(res.body, equals('together'));
});
});
test('deep nested', () async {
final res =
await client!.get(Uri.parse('$url/beatles/big/yellow/submarine'));
print('Response: ${res.body}');
expect(res.body, equals('we all live in a'));
});
group('fallback', () {});
});
group('404', () {
dynamic expect404(r) => r.then((res) {
print('Response (${res.statusCode}): ${res.body}');
expect(res.statusCode, equals(404));
});
test('path', () async {
await expect404(client!.get(Uri.parse('$url/foo')));
await expect404(client!.get(Uri.parse('$url/bye')));
await expect404(client!.get(Uri.parse('$url/people/0/age')));
await expect404(client!.get(Uri.parse('$url/beatles2')));
});
test('method', () async {
await expect404(client!.head(Uri.parse(url!)));
await expect404(client!.patch(Uri.parse('$url/people')));
await expect404(client!.post(Uri.parse('$url/people/0')));
await expect404(
client!.delete(Uri.parse('$url/beatles2/spinal_clacker')));
});
});
}

View file

@ -0,0 +1,46 @@
import 'package:platform_route/string_util.dart';
import 'package:test/test.dart';
void 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:platform_route/platform_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:platform_route/platform_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:platform_route/browser.dart';
import '../shared/basic.dart';
void 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:platform_route/browser.dart';
import '../shared/basic.dart';
void 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,42 @@
import 'dart:html';
import 'package:platform_route/browser.dart';
void basic(BrowserRouter router) {
final $h1 = window.document.querySelector('h1');
final $ul = window.document.getElementById('handlers');
router.onResolve.listen((result) {
final route = result.route;
// TODO: Relook at this logic
//if (route == null) {
// $h1!.text = 'No Active Route';
// $ul!.children
// ..clear()
// ..add(LIElement()..text = '(empty)');
//} else {
if ($h1 != null && $ul != null) {
$h1.text = 'Active Route: ${route.name}';
$ul.children
..clear()
..addAll(result.allHandlers
.map((handler) => LIElement()..text = handler.toString()));
} else {
print('No active Route');
}
//}
});
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();
}