add(angel3): adding re-branded angel3 route package
This commit is contained in:
parent
e12b15f8c8
commit
3b83e34dcc
34 changed files with 2374 additions and 0 deletions
71
packages/route/.gitignore
vendored
Normal file
71
packages/route/.gitignore
vendored
Normal 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
12
packages/route/AUTHORS.md
Normal 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.
|
98
packages/route/CHANGELOG.md
Normal file
98
packages/route/CHANGELOG.md
Normal 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
29
packages/route/LICENSE
Normal 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
136
packages/route/README.md
Normal 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'};
|
||||
```
|
1
packages/route/analysis_options.yaml
Normal file
1
packages/route/analysis_options.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
include: package:lints/recommended.yaml
|
58
packages/route/example/main.dart
Normal file
58
packages/route/example/main.dart
Normal file
|
@ -0,0 +1,58 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package: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);
|
||||
}
|
225
packages/route/lib/browser.dart
Normal file
225
packages/route/lib/browser.dart
Normal 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);
|
||||
}
|
||||
}
|
5
packages/route/lib/platform_route.dart
Normal file
5
packages/route/lib/platform_route.dart
Normal file
|
@ -0,0 +1,5 @@
|
|||
library platform_route;
|
||||
|
||||
export 'src/middleware_pipeline.dart';
|
||||
export 'src/router.dart';
|
||||
export 'src/routing_exception.dart';
|
327
packages/route/lib/src/grammar.dart
Normal file
327
packages/route/lib/src/grammar.dart
Normal 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))
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
50
packages/route/lib/src/middleware_pipeline.dart
Normal file
50
packages/route/lib/src/middleware_pipeline.dart
Normal 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();
|
||||
}
|
99
packages/route/lib/src/route.dart
Normal file
99
packages/route/lib/src/route.dart
Normal 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);
|
||||
}
|
||||
}
|
493
packages/route/lib/src/router.dart
Normal file
493
packages/route/lib/src/router.dart
Normal 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();
|
||||
}
|
21
packages/route/lib/src/routing_exception.dart
Normal file
21
packages/route/lib/src/routing_exception.dart
Normal file
|
@ -0,0 +1,21 @@
|
|||
/// Represents an error in route configuration or navigation.
|
||||
abstract class RoutingException 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;
|
||||
}
|
95
packages/route/lib/src/routing_result.dart
Normal file
95
packages/route/lib/src/routing_result.dart
Normal 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);
|
||||
}
|
||||
}
|
8
packages/route/lib/src/symlink_route.dart
Normal file
8
packages/route/lib/src/symlink_route.dart
Normal 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>[]);
|
||||
}
|
43
packages/route/lib/string_util.dart
Normal file
43
packages/route/lib/string_util.dart
Normal file
|
@ -0,0 +1,43 @@
|
|||
/// Helper functions to performantly transform strings, without `RegExp`.
|
||||
library 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, '\\/');
|
17
packages/route/pubspec.yaml
Normal file
17
packages/route/pubspec.yaml
Normal 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
|
2
packages/route/repubspec.yaml
Normal file
2
packages/route/repubspec.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
push_state:
|
||||
base: push_state/basic.html
|
19
packages/route/test/chain_nest_test.dart
Normal file
19
packages/route/test/chain_nest_test.dart
Normal file
|
@ -0,0 +1,19 @@
|
|||
import 'package: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']));
|
||||
});
|
||||
}
|
44
packages/route/test/navigate_test.dart
Normal file
44
packages/route/test/navigate_test.dart
Normal 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('/'));
|
||||
});
|
||||
});
|
||||
}
|
45
packages/route/test/params_test.dart
Normal file
45
packages/route/test/params_test.dart
Normal 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'}));
|
||||
}
|
20
packages/route/test/parse_test.dart
Normal file
20
packages/route/test/parse_test.dart
Normal 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);
|
||||
});
|
||||
}
|
15
packages/route/test/root_test.dart
Normal file
15
packages/route/test/root_test.dart
Normal 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);
|
||||
});
|
||||
}
|
205
packages/route/test/server_test.dart
Normal file
205
packages/route/test/server_test.dart
Normal 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')));
|
||||
});
|
||||
});
|
||||
}
|
46
packages/route/test/strip_test.dart
Normal file
46
packages/route/test/strip_test.dart
Normal 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');
|
||||
});
|
||||
}
|
18
packages/route/test/uri_decode_test.dart
Normal file
18
packages/route/test/uri_decode_test.dart
Normal file
|
@ -0,0 +1,18 @@
|
|||
import 'package: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': '???',
|
||||
});
|
||||
});
|
||||
}
|
46
packages/route/test/wildcard_test.dart
Normal file
46
packages/route/test/wildcard_test.dart
Normal 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');
|
||||
});
|
||||
}
|
4
packages/route/web/hash/basic.dart
Normal file
4
packages/route/web/hash/basic.dart
Normal file
|
@ -0,0 +1,4 @@
|
|||
import 'package:platform_route/browser.dart';
|
||||
import '../shared/basic.dart';
|
||||
|
||||
void main() => basic(BrowserRouter(hash: true));
|
30
packages/route/web/hash/basic.html
Normal file
30
packages/route/web/hash/basic.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<title>Hash Router</title>
|
||||
<style>
|
||||
#routes li {
|
||||
display: inline;
|
||||
margin-right: 1em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<ul id="routes">
|
||||
<li><a href="/a">Route A</a></li>
|
||||
<li><a href="/b">Route B</a></li>
|
||||
<li><a href="/b/a">Route B/A</a></li>
|
||||
<li><a href="/b/b">Route B/B</a></li>
|
||||
<li><a href="/c">Route C</a></li>
|
||||
</ul>
|
||||
<h1>No Active Route</h1>
|
||||
<i>Handler Sequence:</i>
|
||||
<ul id="handlers">
|
||||
<li>(empty)</li>
|
||||
</ul>
|
||||
<script src="basic.dart.js"></script>
|
||||
</body>
|
||||
</html>
|
15
packages/route/web/index.html
Normal file
15
packages/route/web/index.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Angel Route Samples</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Angel Route Samples</h1>
|
||||
<ul>
|
||||
<li><a href="hash/basic.html">Hash-based</a></li>
|
||||
<li><a href="push_state/basic.html">Push-state</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
4
packages/route/web/push_state/basic.dart
Normal file
4
packages/route/web/push_state/basic.dart
Normal file
|
@ -0,0 +1,4 @@
|
|||
import 'package:platform_route/browser.dart';
|
||||
import '../shared/basic.dart';
|
||||
|
||||
void main() => basic(BrowserRouter());
|
31
packages/route/web/push_state/basic.html
Normal file
31
packages/route/web/push_state/basic.html
Normal file
|
@ -0,0 +1,31 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<base href="/push_state">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<title>Push State Router</title>
|
||||
<style>
|
||||
#routes li {
|
||||
display: inline;
|
||||
margin-right: 1em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<ul id="routes">
|
||||
<li><a href="/a">Route A</a></li>
|
||||
<li><a href="/b">Route B</a></li>
|
||||
<li><a href="/b/a">Route B/A</a></li>
|
||||
<li><a href="/b/b">Route B/B</a></li>
|
||||
<li><a href="/c">Route C</a></li>
|
||||
</ul>
|
||||
<h1>No Active Route</h1>
|
||||
<i>Handler Sequence:</i>
|
||||
<ul id="handlers">
|
||||
<li>(empty)</li>
|
||||
</ul>
|
||||
<script src="basic.dart.js"></script>
|
||||
</body>
|
||||
</html>
|
42
packages/route/web/shared/basic.dart
Normal file
42
packages/route/web/shared/basic.dart
Normal 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();
|
||||
}
|
Loading…
Reference in a new issue