Added packages shelf_router and shelf_router_generator
diff --git a/pkgs/shelf_router/CHANGELOG.md b/pkgs/shelf_router/CHANGELOG.md
new file mode 100644
index 0000000..ed39b04
--- /dev/null
+++ b/pkgs/shelf_router/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Changelog
+
+## 0.7.0
+
+ * Initial release
diff --git a/pkgs/shelf_router/LICENSE b/pkgs/shelf_router/LICENSE
new file mode 100644
index 0000000..7a4a3ea
--- /dev/null
+++ b/pkgs/shelf_router/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
\ No newline at end of file
diff --git a/pkgs/shelf_router/README.md b/pkgs/shelf_router/README.md
new file mode 100644
index 0000000..87addb8
--- /dev/null
+++ b/pkgs/shelf_router/README.md
@@ -0,0 +1,33 @@
+# Web Request Router for Shelf
+
+[Shelf](https://pub.dartlang.org/packages/shelf) makes it easy to build web
+applications in Dart by composing request handlers. This package offers a
+request router for Shelf, matching request to handlers using route patterns.
+
+**Disclaimer:** This is not an officially supported Google product.
+
+Also see the `shelf_router_generator` package for how to automatically generate
+a `Route` using the `Route` annotation in this package.
+
+## Example
+
+```dart
+import 'package:shelf_router/shelf_router.dart';
+import 'package:shelf/shelf.dart';
+import 'package:shelf/shelf_io.dart' as io;
+
+var app = Router();
+
+app.get('/hello', (Request request) {
+ return Response.ok('hello-world');
+});
+
+app.get('/user/<user>', (Request request, String user) {
+ return Response.ok('hello $user');
+});
+
+var server = await io.serve(app.handler, 'localhost', 8080);
+```
+
+See reference documentation of `Router` class for more information.
+
diff --git a/pkgs/shelf_router/analysis_options.yaml b/pkgs/shelf_router/analysis_options.yaml
new file mode 100644
index 0000000..108d105
--- /dev/null
+++ b/pkgs/shelf_router/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:pedantic/analysis_options.yaml
diff --git a/pkgs/shelf_router/example/main.dart b/pkgs/shelf_router/example/main.dart
new file mode 100644
index 0000000..e95344d
--- /dev/null
+++ b/pkgs/shelf_router/example/main.dart
@@ -0,0 +1,85 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async' show Future;
+import 'package:shelf_router/shelf_router.dart';
+import 'package:shelf/shelf.dart';
+import 'package:shelf/shelf_io.dart' as shelf_io;
+
+class Service {
+ // The [Router] can be used to create a handler, which can be used with
+ // [shelf_io.serve].
+ Handler get handler {
+ final router = Router();
+
+ // Handlers can be added with `router.<verb>('<route>', handler)`, the
+ // '<route>' may embed URL-parameters, and these may be taken as parameters
+ // by the handler (but either all URL parameters or no URL parameters, must
+ // be taken parameters by the handler).
+ router.get('/say-hi/<name>', (Request request, String name) {
+ return Response.ok('hi $name');
+ });
+
+ // Embedded URL parameters may also be associated with a regular-expression
+ // that the pattern must match.
+ router.get('/user/<userId|[0-9]+>', (Request request, String userId) {
+ return Response.ok('User has the user-number: $userId');
+ });
+
+ // Handlers can be asynchronous (returning `FutureOr` is also allowed).
+ router.get('/wave', (Request request) async {
+ await Future.delayed(Duration(milliseconds: 100));
+ return Response.ok('_o/');
+ });
+
+ // Other routers can be mounted...
+ router.mount('/api/', Api().router);
+
+ // You can catch all verbs and use a URL-parameter with a regular expression
+ // that matches everything to catch app.
+ router.all('/<ignored|.*>', (Request request) {
+ return Response.notFound('Page not found');
+ });
+
+ return router.handler;
+ }
+}
+
+class Api {
+ Future<Response> _messages(Request request) async {
+ return Response.ok('[]');
+ }
+
+ // By exposing a [Router] for an object, it can be mounted in other routers.
+ Router get router {
+ final router = Router();
+
+ // A handler can have more that one route.
+ router.get('/messages', _messages);
+ router.get('/messages/', _messages);
+
+ // This nested catch-all, will only catch /api/.* when mounted above.
+ // Notice that ordering if annotated handlers and mounts is significant.
+ router.all('/<ignored|.*>', (Request request) => Response.notFound('null'));
+
+ return router;
+ }
+}
+
+// Run shelf server and host a [Service] instance on port 8080.
+void main() async {
+ final service = Service();
+ final server = await shelf_io.serve(service.handler, 'localhost', 8080);
+ print('Server running on localhost:${server.port}');
+}
diff --git a/pkgs/shelf_router/lib/shelf_router.dart b/pkgs/shelf_router/lib/shelf_router.dart
new file mode 100644
index 0000000..9492dda
--- /dev/null
+++ b/pkgs/shelf_router/lib/shelf_router.dart
@@ -0,0 +1,85 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/// A request routing library for shelf.
+///
+/// When writing a shelf web server it is often desirable to route requests to
+/// different handlers based on HTTP method and path patterns. The following
+/// example demonstrates how to do this using [Router].
+///
+/// **Example**
+/// ```dart
+/// import 'package:shelf_router/shelf_router.dart';
+/// import 'package:shelf/shelf.dart' show Request, Response;
+/// import 'package:shelf/shelf_io.dart' as io;
+///
+/// void main() async {
+/// // Create a router
+/// final router = Router();
+///
+/// // Handle GET requests with a path matching ^/say-hello/[^\]*$
+/// router.get('/say-hello/<name>', (Request request, String name) async {
+/// return Response.ok('hello $name');
+/// });
+///
+/// // Listen for requests on port localhost:8080
+/// await io.serve(router.handler, 'localhost', 8080);
+/// }
+/// ```
+///
+/// As it is often useful to organize request handlers in classes, methods can
+/// be annotated with the [Route] annotation, allowing the
+/// `shelf_router_generator` package to generated a method for creating a
+/// [Router] wrapping the class.
+///
+/// To automatically generate add the `shelf_router_generator` and
+/// `build_runner` packages to `dev_dependencies`. The follow the example
+/// below and generate code using `pub run build_runner build`.
+///
+/// **Example**, assume file name is `hello.dart`.
+/// ```dart
+/// import 'package:shelf_router/shelf_router.dart';
+/// import 'package:shelf/shelf.dart' show Request, Response;
+/// import 'package:shelf/shelf_io.dart' as io;
+///
+/// // include the generated part, assumes current file is 'hello.dart'.
+/// part 'hello.g.dart';
+///
+/// class HelloService {
+/// // Annotate a handler with the `Route` annotation.
+/// @Route.get('/say-hello/<name>')
+/// Future<Response> _sayHello(Request request, String name) async {
+/// return Response.ok('hello $name');
+/// }
+///
+/// // Use the generated function `_$<ClassName>Router(<ClassName> instance)`
+/// // to create a getter returning a `Router` for this instance of
+/// // `HelloService`
+/// Router get router => _$HelloServiceRouter(this);
+/// }
+///
+/// void main() async {
+/// // Create a `HelloService` instance
+/// final service = HelloService();
+///
+/// await io.serve(service.router.handler, 'localhost', 8080);
+/// }
+/// ```
+///
+library shelf_router;
+
+import 'src/router.dart';
+import 'src/route.dart';
+export 'src/router.dart';
+export 'src/route.dart';
diff --git a/pkgs/shelf_router/lib/src/route.dart b/pkgs/shelf_router/lib/src/route.dart
new file mode 100644
index 0000000..10a8104
--- /dev/null
+++ b/pkgs/shelf_router/lib/src/route.dart
@@ -0,0 +1,90 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'router.dart';
+
+/// Annotation for handler methods that requests should be routed when using
+/// package `shelf_router_generator`.
+///
+/// The `shelf_router_generator` packages makes it easy to generate a function
+/// that wraps your class and returns a [Router] that forwards requests to
+/// annotated methods. Simply add the `shelf_router_generator` and
+/// `build_runner` packages to `dev_dependencies`, write as illustrated in the
+/// following example and run `pub run build_runner build` to generate code.
+///
+/// **Example**
+/// ```dart
+/// // Always import 'shelf_router' without 'show' or 'as'.
+/// import 'package:shelf_router/shelf_router.dart';
+/// import 'package:shelf/shelf.dart' show Request, Response;
+///
+/// // Include generated code, this assumes current file is 'my_service.dart'.
+/// part 'my_service.g.dart';
+///
+/// class MyService {
+/// @Route.get('/say-hello/<name>')
+/// Future<Response> _sayHello(Request request, String name) async {
+/// return Response.ok('hello $name');
+/// }
+///
+/// /// Get a router for this service.
+/// Router get router => _$MyServiceRouter(this);
+/// }
+/// ```
+///
+/// It is also permitted to annotate public members, the only requirement is
+/// that the member has a signature accepted by [Router] as `handler`.
+class Route {
+ /// HTTP verb for requests routed to the annotated method.
+ final String verb;
+
+ /// HTTP route for request routed to the annotated method.
+ final String route;
+
+ /// Create an annotation that routes requests matching [verb] and [route] to
+ /// the annotated method.
+ const Route(this.verb, this.route);
+
+ /// Route all requests matching [route] to annotated method.
+ const Route.all(this.route) : verb = r'$all';
+
+ /// Route `GET` requests matching [route] to annotated method.
+ const Route.get(this.route) : verb = 'GET';
+
+ /// Route `HEAD` requests matching [route] to annotated method.
+ const Route.head(this.route) : verb = 'HEAD';
+
+ /// Route `POST` requests matching [route] to annotated method.
+ const Route.post(this.route) : verb = 'POST';
+
+ /// Route `PUT` requests matching [route] to annotated method.
+ const Route.put(this.route) : verb = 'PUT';
+
+ /// Route `DELETE` requests matching [route] to annotated method.
+ const Route.delete(this.route) : verb = 'DELETE';
+
+ /// Route `CONNECT` requests matching [route] to annotated method.
+ const Route.connect(this.route) : verb = 'CONNECT';
+
+ /// Route `OPTIONS` requests matching [route] to annotated method.
+ const Route.options(this.route) : verb = 'OPTIONS';
+
+ /// Route `TRACE` requests matching [route] to annotated method.
+ const Route.trace(this.route) : verb = 'TRACE';
+
+ /// Route `MOUNT` requests matching [route] to annotated method.
+ const Route.mount(String prefix)
+ : verb = r'$mount',
+ route = prefix;
+}
diff --git a/pkgs/shelf_router/lib/src/router.dart b/pkgs/shelf_router/lib/src/router.dart
new file mode 100644
index 0000000..4f054f1
--- /dev/null
+++ b/pkgs/shelf_router/lib/src/router.dart
@@ -0,0 +1,163 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'router_entry.dart' show RouterEntry;
+import 'package:shelf/shelf.dart';
+import 'package:http_methods/http_methods.dart';
+
+/// Get a URL parameter captured by the [Router].
+String params(Request request, String name) {
+ ArgumentError.checkNotNull(request, 'request');
+ ArgumentError.checkNotNull(name, 'name');
+
+ final p = request.context['shelf_router/params'];
+ if (!(p is Map<String, String>)) {
+ throw new Exception('no such parameter $name');
+ }
+ final value = (p as Map<String, String>)[name];
+ if (value == null) {
+ throw new Exception('no such parameter $name');
+ }
+ return value;
+}
+
+/// A shelf [Router] routes requests to handlers based on HTTP verb and route
+/// pattern.
+///
+/// ```dart
+/// import 'package:shelf_router/shelf_router.dart';
+/// import 'package:shelf/shelf.dart';
+/// import 'package:shelf/shelf_io.dart' as io;
+///
+/// var app = Router();
+///
+/// // Route pattern parameters can be specified <paramName>
+/// app.get('/users/<userName>/whoami', (Request request) async {
+/// // The matched values can be read with params(request, param)
+/// var userName = params(request, 'userName');
+/// return Response.ok('You are ${userName}');
+/// });
+///
+/// // The matched value can also be taken as parameter, if the handler given
+/// // doesn't implement Handler, it's assumed to take all parameters in the
+/// // order they appear in the route pattern.
+/// app.get('/users/<userName>/say-hello', (Request request, String userName) async {
+/// return Response.ok('Hello ${uName}');
+/// });
+///
+/// // It is possible to have multiple parameters, and if desired a custom
+/// // regular expression can be specified with <paramName|REGEXP>, where
+/// // REGEXP is a regular expression (leaving out ^ and $).
+/// // If no regular expression is specified `[^/]+` will be used.
+/// app.get('/users/<userName>/messages/<msgId|\d+>', (Request request) async {
+/// var msgId = int.parse(params(request, 'msgId'));
+/// return Response.ok(message.getById(msgId));
+/// });
+///
+/// var server = await io.serve(app.handler, 'localhost', 8080);
+/// ```
+///
+/// If multiple routes match the same request, the handler for the first
+/// route is called. If the handler returns `null` the next matching handler
+/// will be attempted.
+///
+///
+class Router {
+ final List<RouterEntry> _routes = [];
+
+ /// Add [handler] for [verb] requests to [route].
+ void add(String verb, String route, dynamic handler) {
+ ArgumentError.checkNotNull(verb, 'verb');
+ if (!isHttpMethod(verb)) {
+ throw ArgumentError.value(verb, 'verb', 'expected a valid HTTP method');
+ }
+ verb = verb.toUpperCase();
+
+ _routes.add(RouterEntry(verb, route, handler));
+ }
+
+ /// Handle all request to [route] using [handler].
+ void all(String route, dynamic handler) {
+ _routes.add(RouterEntry('ALL', route, handler));
+ }
+
+ /// Mount a router below a prefix.
+ ///
+ /// In this case prefix may not contain any parameters, nor
+ void mount(String prefix, Router router) {
+ ArgumentError.checkNotNull(prefix, 'prefix');
+ ArgumentError.checkNotNull(router, 'router');
+ if (!prefix.startsWith('/') || !prefix.endsWith('/')) {
+ throw ArgumentError.value(
+ prefix, 'prefix', 'must start and end with a slash');
+ }
+
+ final handler = router.handler;
+ // first slash is always in request.handlerPath
+ final path = prefix.substring(1);
+ all(prefix + '<path|[^]*>', (Request request) {
+ return handler(request.change(path: path));
+ });
+ }
+
+ /// Get a [Handler] that will route incoming requests to registered handlers.
+ Handler get handler {
+ // Note: this is a great place to optimize the implementation by building
+ // a trie for faster matching... left as an exercise for the reader :)
+ return (Request request) async {
+ for (var route in _routes) {
+ if (route.verb != request.method.toUpperCase() && route.verb != 'ALL') {
+ continue;
+ }
+ var params = route.match('/' + request.url.path);
+ if (params != null) {
+ var res = await route.invoke(request, params);
+ if (res != null) {
+ return res;
+ }
+ }
+ }
+ return null;
+ };
+ }
+
+ // Handlers for all methods
+
+ /// Handle `GET` request to [route] using [handler].
+ void get(String route, dynamic handler) => add('GET', route, handler);
+
+ /// Handle `HEAD` request to [route] using [handler].
+ void head(String route, dynamic handler) => add('HEAD', route, handler);
+
+ /// Handle `POST` request to [route] using [handler].
+ void post(String route, dynamic handler) => add('POST', route, handler);
+
+ /// Handle `PUT` request to [route] using [handler].
+ void put(String route, dynamic handler) => add('PUT', route, handler);
+
+ /// Handle `DELETE` request to [route] using [handler].
+ void delete(String route, dynamic handler) => add('DELETE', route, handler);
+
+ /// Handle `CONNECT` request to [route] using [handler].
+ void connect(String route, dynamic handler) => add('CONNECT', route, handler);
+
+ /// Handle `OPTIONS` request to [route] using [handler].
+ void options(String route, dynamic handler) => add('OPTIONS', route, handler);
+
+ /// Handle `TRACE` request to [route] using [handler].
+ void trace(String route, dynamic handler) => add('TRACE', route, handler);
+
+ /// Handle `PATCH` request to [route] using [handler].
+ void patch(String route, dynamic handler) => add('PATCH', route, handler);
+}
diff --git a/pkgs/shelf_router/lib/src/router_entry.dart b/pkgs/shelf_router/lib/src/router_entry.dart
new file mode 100644
index 0000000..0225b6d
--- /dev/null
+++ b/pkgs/shelf_router/lib/src/router_entry.dart
@@ -0,0 +1,101 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async';
+import 'package:shelf/shelf.dart';
+
+/// Check if the [regexp] is non-capturing.
+bool _isNoCapture(String regexp) {
+ ArgumentError.checkNotNull(regexp, 'regexp');
+
+ // Construct a new regular expression matching anything containing regexp,
+ // then match with empty-string and count number of groups.
+ return RegExp('^(?:$regexp)|.*\$').firstMatch('').groupCount == 0;
+}
+
+/// Entry in the router.
+///
+/// This class implements the logic for matching the path pattern.
+class RouterEntry {
+ /// Pattern for parsing the route pattern
+ static final RegExp _parser = RegExp(r'([^<]*)(?:<([^>|]+)(?:\|([^>]*))?>)?');
+
+ final String verb, route;
+ final dynamic _handler;
+
+ /// Expression that the request path must match.
+ ///
+ /// This also captures any parameters in the route pattern.
+ RegExp _routePattern;
+
+ /// Names for the parameters in the route pattern.
+ List<String> _params = [];
+
+ /// List of parameter names in the route pattern.
+ List<String> get params => _params.toList(); // exposed for using generator.
+
+ RouterEntry(this.verb, this.route, this._handler) {
+ ArgumentError.checkNotNull(verb, 'verb');
+ ArgumentError.checkNotNull(route, 'route');
+ ArgumentError.checkNotNull(_handler, 'handler');
+ if (!route.startsWith('/')) {
+ throw ArgumentError.value(
+ route, 'route', 'expected route to start with a slash');
+ }
+ if (!(_handler is Function)) {
+ throw ArgumentError.value(_handler, 'handler', 'expected a function');
+ }
+
+ var pattern = '';
+ for (var m in _parser.allMatches(route)) {
+ pattern += RegExp.escape(m[1]);
+ if (m[2] != null) {
+ _params.add(m[2]);
+ if (m[3] != null && !_isNoCapture(m[3])) {
+ throw ArgumentError.value(
+ route, 'route', 'expression for "${m[2]}" is capturing');
+ }
+ pattern += '(${m[3] ?? r'[^/]+'})';
+ }
+ }
+ _routePattern = RegExp('^$pattern\$');
+ }
+
+ /// Returns a map from parameter name to value, if the path matches the
+ /// route pattern. Otherwise returns null.
+ Map<String, String> match(String path) {
+ // Check if path matches the route pattern
+ var m = _routePattern.firstMatch(path);
+ if (m == null) {
+ return null;
+ }
+ // Construct map from parameter name to matched value
+ var params = <String, String>{};
+ for (int i = 0; i < _params.length; i++) {
+ // first group is always the full match, we ignore this group.
+ params[_params[i]] = m[i + 1];
+ }
+ return params;
+ }
+
+ // invoke handler with given request and params
+ Future<Response> invoke(Request request, Map<String, String> params) async {
+ request = request.change(context: {'shelf_router/params': params});
+ if (_handler is Handler || _params.isEmpty) {
+ return await _handler(request);
+ }
+ return await Function.apply(
+ _handler, [request]..addAll(_params.map((n) => params[n])));
+ }
+}
diff --git a/pkgs/shelf_router/mono_pkg.yaml b/pkgs/shelf_router/mono_pkg.yaml
new file mode 100644
index 0000000..1b2bf49
--- /dev/null
+++ b/pkgs/shelf_router/mono_pkg.yaml
@@ -0,0 +1,8 @@
+dart:
+- stable
+stages:
+- analyze:
+ - dartanalyzer
+ - dartfmt
+- unit_test:
+ - test
diff --git a/pkgs/shelf_router/pubspec.yaml b/pkgs/shelf_router/pubspec.yaml
new file mode 100644
index 0000000..f866f32
--- /dev/null
+++ b/pkgs/shelf_router/pubspec.yaml
@@ -0,0 +1,21 @@
+name: shelf_router
+version: 0.7.0
+description: A router for the shelf package.
+description: |
+ A convinent request router for the shelf web-framework, with support for
+ URL-parameters, nested routers and routers generated from source annotations
+ using the shelf_router_generator package.
+homepage: https://github.com/google/dart-neats/tree/master/shelf_router
+repository: https://github.com/google/dart-neats.git
+issue_tracker: https://github.com/google/dart-neats/labels/pkg:shelf_router
+dependencies:
+ shelf: ^0.7.3
+ http_methods: ^1.0.0
+dev_dependencies:
+ test: ^1.5.1
+ pedantic: ^1.4.0
+dependency_overrides:
+ http_methods:
+ path: ../http_methods
+environment:
+ sdk: '>=2.0.0 <3.0.0'
\ No newline at end of file
diff --git a/pkgs/shelf_router/test/route_entry_test.dart b/pkgs/shelf_router/test/route_entry_test.dart
new file mode 100644
index 0000000..ca21ef3
--- /dev/null
+++ b/pkgs/shelf_router/test/route_entry_test.dart
@@ -0,0 +1,73 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:shelf_router/src/router_entry.dart' show RouterEntry;
+import 'package:test/test.dart';
+
+void main() {
+ void testPattern(
+ String pattern, {
+ Map<String, Map<String, String>> match = const {},
+ List<String> notMatch = const [],
+ }) {
+ group('RouterEntry: "$pattern"', () {
+ final r = RouterEntry('GET', pattern, () => null);
+ for (final e in match.entries) {
+ test('Matches "${e.key}"', () {
+ expect(r.match(e.key), equals(e.value));
+ });
+ }
+ for (final v in notMatch) {
+ test('NotMatch "$v"', () {
+ expect(r.match(v), isNull);
+ });
+ }
+ });
+ }
+
+ testPattern('/hello', match: {
+ '/hello': {},
+ }, notMatch: [
+ '/not-hello',
+ '/',
+ ]);
+
+ testPattern(r'/user/<user>/groups/<group|\d+>', match: {
+ '/user/jonasfj/groups/42': {
+ 'user': 'jonasfj',
+ 'group': '42',
+ },
+ '/user/jonasfj/groups/0': {
+ 'user': 'jonasfj',
+ 'group': '0',
+ },
+ '/user/123/groups/101': {
+ 'user': '123',
+ 'group': '101',
+ },
+ }, notMatch: [
+ '/user/',
+ '/user/jonasfj/groups/5-3',
+ '/user/jonasfj/test/groups/5',
+ '/user/jonasfjtest/groups/4/',
+ '/user/jonasfj/groups/',
+ '/not-hello',
+ '/',
+ ]);
+
+ test('non-capture regex only', () {
+ expect(() => RouterEntry('GET', '/users/<user|([^]*)>/info', null),
+ throwsA(anything));
+ });
+}
diff --git a/pkgs/shelf_router/test/router_test.dart b/pkgs/shelf_router/test/router_test.dart
new file mode 100644
index 0000000..6c09d5c
--- /dev/null
+++ b/pkgs/shelf_router/test/router_test.dart
@@ -0,0 +1,118 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+@TestOn('vm')
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:http/http.dart' as http;
+import 'package:shelf/shelf.dart';
+import 'package:shelf/shelf_io.dart' as io;
+import 'package:test/test.dart';
+
+import 'package:shelf_router/shelf_router.dart';
+
+void main() {
+ // Create a server that listens on localhost for testing
+ io.IOServer server;
+
+ setUp(() async {
+ try {
+ server = await io.IOServer.bind(InternetAddress.loopbackIPv6, 0);
+ } on SocketException catch (_) {
+ server = await io.IOServer.bind(InternetAddress.loopbackIPv4, 0);
+ }
+ });
+
+ tearDown(() => server.close());
+
+ Future<String> get(String path) => http.read(server.url.toString() + path);
+
+ test('get sync/async handler', () async {
+ var app = Router();
+
+ app.get('/sync-hello', (Request request) {
+ return Response.ok('hello-world');
+ });
+
+ app.get('/async-hello', (Request request) async {
+ return Future.microtask(() {
+ return Response.ok('hello-world');
+ });
+ });
+
+ // check that catch-alls work
+ app.all('/<path|[^]*>', (Request request) {
+ return Response.ok('not-found');
+ });
+
+ server.mount(app.handler);
+
+ expect(await get('/sync-hello'), 'hello-world');
+ expect(await get('/async-hello'), 'hello-world');
+ expect(await get('/wrong-path'), 'not-found');
+ });
+
+ test('params', () async {
+ var app = Router();
+
+ app.get(r'/user/<user>/groups/<group|\d+>', (Request request) {
+ final user = params(request, 'user');
+ final group = params(request, 'group');
+ return Response.ok('$user / $group');
+ });
+
+ server.mount(app.handler);
+
+ expect(await get('/user/jonasfj/groups/42'), 'jonasfj / 42');
+ });
+
+ test('params by arguments', () async {
+ var app = Router();
+
+ app.get(r'/user/<user>/groups/<group|\d+>',
+ (Request request, String user, String group) {
+ return Response.ok('$user / $group');
+ });
+
+ server.mount(app.handler);
+
+ expect(await get('/user/jonasfj/groups/42'), 'jonasfj / 42');
+ });
+
+ test('mount(Router)', () async {
+ var api = Router();
+ api.get('/user/<user>/info', (Request request, String user) {
+ return Response.ok('Hello $user');
+ });
+
+ var app = Router();
+ app.get('/hello', (Request request) {
+ return Response.ok('hello-world');
+ });
+
+ app.mount('/api/', api);
+
+ app.all('/<_|[^]*>', (Request request) {
+ return Response.notFound('catch-all-handler');
+ });
+
+ server.mount(app.handler);
+
+ expect(await get('/hello'), 'hello-world');
+ expect(await get('/api/user/jonasfj/info'), 'Hello jonasfj');
+ expect(get('/api/user/jonasfj/info-wrong'), throwsA(anything));
+ });
+}
diff --git a/pkgs/shelf_router_generator/CHANGELOG.md b/pkgs/shelf_router_generator/CHANGELOG.md
new file mode 100644
index 0000000..ed39b04
--- /dev/null
+++ b/pkgs/shelf_router_generator/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Changelog
+
+## 0.7.0
+
+ * Initial release
diff --git a/pkgs/shelf_router_generator/LICENSE b/pkgs/shelf_router_generator/LICENSE
new file mode 100644
index 0000000..7a4a3ea
--- /dev/null
+++ b/pkgs/shelf_router_generator/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
\ No newline at end of file
diff --git a/pkgs/shelf_router_generator/README.md b/pkgs/shelf_router_generator/README.md
new file mode 100644
index 0000000..00fa3a1
--- /dev/null
+++ b/pkgs/shelf_router_generator/README.md
@@ -0,0 +1,54 @@
+Shelf Router Generator
+======================
+
+[Shelf](https://pub.dartlang.org/packages/shelf) makes it easy to build web
+applications in Dart by composing request handlers. The `shelf_router` package
+offers a request router for Shelf. this package enables generating a
+`shelf_route.Router` from annotations in code.
+
+**Disclaimer:** This is not an officially supported Google product.
+
+## Example
+
+```dart
+import 'package:shelf/shelf.dart';
+import 'package:shelf_router/shelf_router.dart';
+
+part 'userservice.g.dart';
+
+class UserService {
+ final DatabaseConnection connection;
+ UserService(this.connection);
+
+ @Route.get('/users/')
+ Future<Response> listUsers(Request request) async {
+ return Response.ok('["user1"]');
+ }
+
+ @Route.get('/users/<userId>')
+ Future<Response> fetchUser(Request request, String userId) async {
+ if (userId == 'user1') {
+ return Response.ok('user1');
+ }
+ return Response.notFound('no such user');
+ }
+
+ // Create router using the generate function defined in 'userservice.g.dart'.
+ Router get router => _$UserServiceRouter(this);
+}
+
+void main() async {
+ // You can setup context, database connections, cache connections, email
+ // services, before you create an instance of your service.
+ var connection = await DatabaseConnection.connect('localhost:1234');
+
+ // Create an instance of your service, usine one of the constructors you've
+ // defined.
+ var service = UserService(connection);
+ // Service request using the router, note the router can also be mounted.
+ var router = service.router;
+ var server = await io.serve(router.handler, 'localhost', 8080);
+}
+```
+
+
diff --git a/pkgs/shelf_router_generator/analysis_options.yaml b/pkgs/shelf_router_generator/analysis_options.yaml
new file mode 100644
index 0000000..108d105
--- /dev/null
+++ b/pkgs/shelf_router_generator/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:pedantic/analysis_options.yaml
diff --git a/pkgs/shelf_router_generator/build.yaml b/pkgs/shelf_router_generator/build.yaml
new file mode 100644
index 0000000..d6561c0
--- /dev/null
+++ b/pkgs/shelf_router_generator/build.yaml
@@ -0,0 +1,14 @@
+targets:
+ $default:
+ builders:
+ shelf_router_generator|shelf_router:
+ enabled: true
+
+builders:
+ shelf_router:
+ import: "package:shelf_router_generator/builder.dart"
+ builder_factories: ["shelfRouter"]
+ build_extensions: {".dart": [".shelf_router.g.part"]}
+ auto_apply: dependents
+ build_to: cache
+ applies_builders: ["source_gen|combining_builder"]
diff --git a/pkgs/shelf_router_generator/example/main.dart b/pkgs/shelf_router_generator/example/main.dart
new file mode 100644
index 0000000..837557b
--- /dev/null
+++ b/pkgs/shelf_router_generator/example/main.dart
@@ -0,0 +1,84 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async' show Future;
+import 'package:shelf_router/shelf_router.dart';
+import 'package:shelf/shelf.dart';
+import 'package:shelf/shelf_io.dart' as shelf_io;
+
+// Generated code will be written to 'main.g.dart'
+part 'main.g.dart';
+
+class Service {
+ // A handler is annotated with @Route.<verb>('<route>'), the '<route>' may
+ // embed URL-parameters, and these may be taken as parameters by the handler.
+ // But either all URL-parameters or none of the URL parameters must be taken
+ // as parameters by the handler.
+ @Route.get('/say-hi/<name>')
+ Response _hi(Request request, String name) {
+ return Response.ok('hi $name');
+ }
+
+ // Embedded URL parameters may also be associated with a regular-expression
+ // that the pattern must match.
+ @Route.get('/user/<userId|[0-9]+>')
+ Response _user(Request request, String userId) {
+ return Response.ok('User has the user-number: $userId');
+ }
+
+ // Handlers can be asynchronous (returning `FutureOr` is also allowed).
+ @Route.get('/wave')
+ Future<Response> _wave(Request request) async {
+ await Future.delayed(Duration(milliseconds: 100));
+ return Response.ok('_o/');
+ }
+
+ // Other routers can be mounted...
+ @Route.mount('/api/')
+ Router get _api => Api().router;
+
+ // You can catch all verbs and use a URL-parameter with a regular expression
+ // that matches everything to catch app.
+ @Route.all('/<ignored|.*>')
+ Response _404(Request request) => Response.notFound('Page not found');
+
+ // The generated function _$ServiceRouter can be used to get a [Handler]
+ // for this object. This can be used with [shelf_io.serve].
+ Handler get handler => _$ServiceRouter(this).handler;
+}
+
+class Api {
+ // A handler can have more that one route :)
+ @Route.get('/messages')
+ @Route.get('/messages/')
+ Future<Response> _messages(Request request) async {
+ return Response.ok('[]');
+ }
+
+ // This nested catch-all, will only catch /api/.* when mounted above.
+ // Notice that ordering if annotated handlers and mounts is significant.
+ @Route.all('/<ignored|.*>')
+ Response _404(Request request) => Response.notFound('null');
+
+ // The generated function _$ApiRouter can be used to expose a [Router] for
+ // this object.
+ Router get router => _$ApiRouter(this);
+}
+
+// Run shelf server and host a [Service] instance on port 8080.
+void main() async {
+ final service = Service();
+ final server = await shelf_io.serve(service.handler, 'localhost', 8080);
+ print('Server running on localhost:${server.port}');
+}
diff --git a/pkgs/shelf_router_generator/example/main.g.dart b/pkgs/shelf_router_generator/example/main.g.dart
new file mode 100644
index 0000000..8d7a50b
--- /dev/null
+++ b/pkgs/shelf_router_generator/example/main.g.dart
@@ -0,0 +1,25 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'main.dart';
+
+// **************************************************************************
+// ShelfRouterGenerator
+// **************************************************************************
+
+Router _$ServiceRouter(Service service) {
+ final router = Router();
+ router.add('GET', '/say-hi/<name>', service._hi);
+ router.add('GET', '/user/<userId|[0-9]+>', service._user);
+ router.add('GET', '/wave', service._wave);
+ router.mount('/api/', service._api);
+ router.all('/<ignored|.*>', service._404);
+ return router;
+}
+
+Router _$ApiRouter(Api service) {
+ final router = Router();
+ router.add('GET', '/messages', service._messages);
+ router.add('GET', '/messages/', service._messages);
+ router.all('/<ignored|.*>', service._404);
+ return router;
+}
diff --git a/pkgs/shelf_router_generator/lib/builder.dart b/pkgs/shelf_router_generator/lib/builder.dart
new file mode 100644
index 0000000..d61303b
--- /dev/null
+++ b/pkgs/shelf_router_generator/lib/builder.dart
@@ -0,0 +1,34 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/// This library provides a [Builder] for generating functions that can create
+/// a [shelf_router.Router] based on annotated members.
+///
+/// This is **not intended** for consumption, this library should be used by
+/// running `pub run build_runner build`. Using this library through other means
+/// is not supported and may break arbitrarily.
+library builder;
+
+import 'package:build/build.dart';
+import 'package:source_gen/source_gen.dart';
+import 'package:shelf_router/shelf_router.dart' as shelf_router;
+import 'src/shelf_router_generator.dart';
+
+/// A [Builder] that generates a `_$<className>Router(<className> service)`
+/// function for each class `<className>` containing a member annotated with
+/// [shelf_router.Route].
+Builder shelfRouter(BuilderOptions _) => SharedPartBuilder(
+ [ShelfRouterGenerator()],
+ 'shelf_router',
+ );
diff --git a/pkgs/shelf_router_generator/lib/src/shelf_router_generator.dart b/pkgs/shelf_router_generator/lib/src/shelf_router_generator.dart
new file mode 100644
index 0000000..c26af53
--- /dev/null
+++ b/pkgs/shelf_router_generator/lib/src/shelf_router_generator.dart
@@ -0,0 +1,301 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async' show Future;
+import 'package:analyzer/dart/element/element.dart'
+ show ClassElement, ElementKind, ExecutableElement;
+import 'package:analyzer/dart/element/type.dart' show ParameterizedType;
+import 'package:build/build.dart' show BuildStep, log;
+import 'package:code_builder/code_builder.dart' as code;
+import 'package:http_methods/http_methods.dart' show isHttpMethod;
+import 'package:meta/meta.dart';
+import 'package:source_gen/source_gen.dart' as g;
+import 'package:shelf/shelf.dart' as shelf;
+import 'package:shelf_router/shelf_router.dart' as shelf_router;
+import 'package:shelf_router/src/router_entry.dart' show RouterEntry;
+
+// Type checkers that we need later
+final _routeType = g.TypeChecker.fromRuntime(shelf_router.Route);
+final _routerType = g.TypeChecker.fromRuntime(shelf_router.Router);
+final _responseType = g.TypeChecker.fromRuntime(shelf.Response);
+final _requestType = g.TypeChecker.fromRuntime(shelf.Request);
+final _stringType = g.TypeChecker.fromRuntime(String);
+
+/// A representation of a handler that was annotated with [Route].
+class _Handler {
+ final String verb, route;
+ final ExecutableElement element;
+
+ _Handler(this.verb, this.route, this.element);
+}
+
+/// Find members of a class annotated with [shelf_router.Route].
+List<ExecutableElement> getAnnotatedElementsOrderBySourceOffset(
+ ClassElement cls) {
+ return <ExecutableElement>[]
+ ..addAll(cls.methods.where(_routeType.hasAnnotationOfExact))
+ ..addAll(cls.accessors.where(_routeType.hasAnnotationOfExact))
+ ..sort((a, b) => (a.nameOffset ?? -1).compareTo(b.nameOffset ?? -1));
+}
+
+/// Generate a `_$<className>Router(<className> service)` method that returns a
+/// [shelf_router.Router] configured based on annotated handlers.
+code.Method _buildRouterMethod({
+ @required ClassElement classElement,
+ @required List<_Handler> handlers,
+}) =>
+ code.Method(
+ (b) => b
+ ..name = '_\$${classElement.name}Router'
+ ..requiredParameters.add(
+ code.Parameter((b) => b
+ ..name = 'service'
+ ..type = code.refer(classElement.name)),
+ )
+ ..returns = code.refer('Router')
+ ..body = code.Block(
+ (b) => b
+ ..addExpression(
+ code.refer('Router').newInstance([]).assignFinal('router'))
+ ..statements.addAll(handlers.map((h) => _buildAddHandlerCode(
+ router: code.refer('router'),
+ service: code.refer('service'),
+ handler: h,
+ )))
+ ..addExpression(code.refer('router').returned),
+ ),
+ );
+
+/// Generate the code statement that adds [handler] from [service] to [router].
+code.Code _buildAddHandlerCode({
+ @required code.Reference router,
+ @required code.Reference service,
+ @required _Handler handler,
+}) {
+ switch (handler.verb) {
+ case r'$mount':
+ return router.property('mount').call([
+ code.literal(handler.route),
+ service.property(handler.element.name),
+ ]).statement;
+ case r'$all':
+ return router.property('all').call([
+ code.literal(handler.route),
+ service.property(handler.element.name),
+ ]).statement;
+ default:
+ return router.property('add').call([
+ code.literal(handler.verb.toUpperCase()),
+ code.literal(handler.route),
+ service.property(handler.element.name),
+ ]).statement;
+ }
+}
+
+class ShelfRouterGenerator extends g.Generator {
+ @override
+ Future<String> generate(g.LibraryReader library, BuildStep step) async {
+ // Create a map from ClassElement to list of annotated elements sorted by
+ // offset in source code, this is not type checked yet.
+ final classes = <ClassElement, List<_Handler>>{};
+ for (final cls in library.classes) {
+ final elements = getAnnotatedElementsOrderBySourceOffset(cls);
+ if (elements.isEmpty) {
+ continue;
+ }
+ log.info('found shelf_router.Route annotations in ${cls.name}');
+
+ classes[cls] = elements
+ .map((e) => _routeType.annotationsOfExact(e).map((a) => _Handler(
+ a.getField('verb').toStringValue(),
+ a.getField('route').toStringValue(),
+ e,
+ )))
+ .expand((i) => i)
+ .toList();
+ }
+ if (classes.isEmpty) {
+ return null; // nothing to do if nothing was annotated
+ }
+
+ // Run type check to ensure method and getters have the right signatures.
+ for (final handler in classes.values.expand((i) => i)) {
+ // If the verb is $mount, then it's not a handler, but a mount.
+ if (handler.verb.toLowerCase() == r'$mount') {
+ _typeCheckMount(handler);
+ } else {
+ _typeCheckHandler(handler);
+ }
+ }
+
+ // Build library and emit code with all generate methods.
+ final methods = classes.entries.map((e) => _buildRouterMethod(
+ classElement: e.key,
+ handlers: e.value,
+ ));
+ return code.Library((b) => b.body.addAll(methods))
+ .accept(code.DartEmitter())
+ .toString();
+ }
+}
+
+/// Type checks for the case where [shelf_router.Route] is used to annotate
+/// shelf request handler.
+void _typeCheckHandler(_Handler h) {
+ if (h.element.isStatic) {
+ throw g.InvalidGenerationSourceError(
+ 'The shelf_router.Route annotation cannot be used on static members',
+ element: h.element);
+ }
+
+ // Check the verb, note that $all is a special value for handling all verbs.
+ if (!isHttpMethod(h.verb) && h.verb != r'$all') {
+ throw g.InvalidGenerationSourceError(
+ 'The verb "${h.verb}" used in shelf_router.Route annotation must be '
+ 'a valid HTTP method',
+ element: h.element);
+ }
+
+ // Check that this shouldn't have been annotated with Route.mount
+ if (h.element.kind == ElementKind.GETTER) {
+ throw g.InvalidGenerationSourceError(
+ 'Only the shelf_router.Route.mount annotation can only be used on a '
+ 'getter, and only if it returns a shelf_router.Router',
+ element: h.element);
+ }
+
+ // Check that this is indeed a method
+ if (h.element.kind != ElementKind.METHOD) {
+ throw g.InvalidGenerationSourceError(
+ 'The shelf_router.Route annotation can only be used on request '
+ 'handling methods',
+ element: h.element);
+ }
+
+ // Check the route can parse
+ List<String> params;
+ try {
+ params = RouterEntry(h.verb, h.route, () => null).params;
+ } on ArgumentError catch (e) {
+ throw g.InvalidGenerationSourceError(
+ e.toString(),
+ element: h.element,
+ );
+ }
+
+ // Ensure that the first parameter is shelf.Request
+ if (h.element.parameters.isEmpty) {
+ throw g.InvalidGenerationSourceError(
+ 'The shelf_router.Route annotation can only be used on shelf request '
+ 'handlers accept a shelf.Request parameter',
+ element: h.element);
+ }
+ for (final p in h.element.parameters) {
+ if (p.isOptional) {
+ throw g.InvalidGenerationSourceError(
+ 'The shelf_router.Route annotation can only be used on shelf '
+ 'request handlers accept a shelf.Request parameter and/or a '
+ 'shelf.Request parameter and all string parameters in the route, '
+ 'optional parameters are not permitted',
+ element: p);
+ }
+ }
+ if (!_requestType.isExactlyType(h.element.parameters.first.type)) {
+ throw g.InvalidGenerationSourceError(
+ 'The shelf_router.Route annotation can only be used on shelf request '
+ 'handlers accept a shelf.Request parameter as first parameter',
+ element: h.element);
+ }
+ if (h.element.parameters.length > 1) {
+ if (h.element.parameters.length != params.length + 1) {
+ throw g.InvalidGenerationSourceError(
+ 'The shelf_router.Route annotation can only be used on shelf '
+ 'request handlers accept a shelf.Request parameter and/or a '
+ 'shelf.Request parameter and all string parameters in the route',
+ element: h.element);
+ }
+ for (int i = 0; i < params.length; i++) {
+ final p = h.element.parameters[i + 1];
+ if (p.name != params[i]) {
+ throw g.InvalidGenerationSourceError(
+ 'The shelf_router.Route annotation can only be used on shelf '
+ 'request handlers accept a shelf.Request parameter and/or a '
+ 'shelf.Request parameter and all string parameters in the route, '
+ 'the "${p.name}" parameter should be named "${params[i]}"',
+ element: p);
+ }
+ if (!_stringType.isExactlyType(p.type)) {
+ throw g.InvalidGenerationSourceError(
+ 'The shelf_router.Route annotation can only be used on shelf '
+ 'request handlers accept a shelf.Request parameter and/or a '
+ 'shelf.Request parameter and all string parameters in the route, '
+ 'the "${p.name}" parameter is not of type string',
+ element: p);
+ }
+ }
+ }
+
+ // Check the return value of the method.
+ var returnType = h.element.returnType;
+ // Unpack Future<T> and FutureOr<T> wrapping of responseType
+ if (returnType.isDartAsyncFuture || returnType.isDartAsyncFutureOr) {
+ returnType = (returnType as ParameterizedType).typeArguments.first;
+ }
+ if (!_responseType.isAssignableFromType(returnType)) {
+ throw g.InvalidGenerationSourceError(
+ 'The shelf_router.Route annotation can only be used on shelf request '
+ 'handlers that return shelf.Response, Future<shelf.Response> or '
+ 'FutureOr<shelf.Response>, and not "${h.element.returnType}"',
+ element: h.element);
+ }
+}
+
+/// Type checks for the case where [shelf_router.Route.mount] is used to annotate
+/// a getter that returns a [shelf_router.Router].
+void _typeCheckMount(_Handler h) {
+ if (h.element.isStatic) {
+ throw g.InvalidGenerationSourceError(
+ 'The shelf_router.Route annotation cannot be used on static members',
+ element: h.element);
+ }
+
+ // Check that this should have been annotated with Route.mount
+ if (h.element.kind != ElementKind.GETTER) {
+ throw g.InvalidGenerationSourceError(
+ 'The shelf_router.Route.mount annotation can only be used on a '
+ 'getter that returns shelf_router.Router',
+ element: h.element);
+ }
+
+ // Sanity checks for the prefix
+ if (!h.route.startsWith('/') || !h.route.endsWith('/')) {
+ throw g.InvalidGenerationSourceError(
+ 'The prefix "${h.route}" in shelf_router.Route.mount(prefix) '
+ 'annotation must begin and end with a slash',
+ element: h.element);
+ }
+ if (h.route.contains('<')) {
+ throw g.InvalidGenerationSourceError(
+ 'The prefix "${h.route}" in shelf_router.Route.mount(prefix) '
+ 'annotation cannot contain <',
+ element: h.element);
+ }
+
+ if (!_routerType.isAssignableFromType(h.element.returnType)) {
+ throw g.InvalidGenerationSourceError(
+ 'The shelf_router.Route.mount annotation can only be used on a '
+ 'getter that returns shelf_router.Router',
+ element: h.element);
+ }
+}
diff --git a/pkgs/shelf_router_generator/mono_pkg.yaml b/pkgs/shelf_router_generator/mono_pkg.yaml
new file mode 100644
index 0000000..1b2bf49
--- /dev/null
+++ b/pkgs/shelf_router_generator/mono_pkg.yaml
@@ -0,0 +1,8 @@
+dart:
+- stable
+stages:
+- analyze:
+ - dartanalyzer
+ - dartfmt
+- unit_test:
+ - test
diff --git a/pkgs/shelf_router_generator/pubspec.yaml b/pkgs/shelf_router_generator/pubspec.yaml
new file mode 100644
index 0000000..445b15b
--- /dev/null
+++ b/pkgs/shelf_router_generator/pubspec.yaml
@@ -0,0 +1,28 @@
+name: shelf_router_generator
+version: 0.7.0
+description: |
+ A package:build compatible builder for generating request routers for the
+ shelf web-framework based on source annotations.
+homepage: https://github.com/google/dart-neats/tree/master/shelf_router_generator
+repository: https://github.com/google/dart-neats.git
+issue_tracker: https://github.com/google/dart-neats/labels/pkg:shelf_router_generator
+dependencies:
+ build: ^1.0.0
+ build_config: ^0.3.1
+ source_gen: ^0.9.1
+ analyzer: ^0.35.0
+ shelf_router: ^1.0.0
+ code_builder: ^3.2.0
+ shelf: ^0.7.3
+ meta: ^1.1.7
+dependency_overrides:
+ shelf_router:
+ path: ../shelf_router
+ http_methods:
+ path: ../http_methods
+dev_dependencies:
+ test: ^1.5.3
+ http: ^0.12.0+1
+ build_verify: ^1.1.1
+ build_runner: ^1.2.8
+ pedantic: ^1.4.0
\ No newline at end of file
diff --git a/pkgs/shelf_router_generator/test/ensure_build_test.dart b/pkgs/shelf_router_generator/test/ensure_build_test.dart
new file mode 100644
index 0000000..cad26fe
--- /dev/null
+++ b/pkgs/shelf_router_generator/test/ensure_build_test.dart
@@ -0,0 +1,22 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:build_verify/build_verify.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test('ensure_build', () {
+ expectBuildClean(packageRelativeDirectory: 'shelf_router_generator');
+ });
+}
diff --git a/pkgs/shelf_router_generator/test/server/api.dart b/pkgs/shelf_router_generator/test/server/api.dart
new file mode 100644
index 0000000..a8247f6
--- /dev/null
+++ b/pkgs/shelf_router_generator/test/server/api.dart
@@ -0,0 +1,33 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async' show Future;
+import 'package:shelf/shelf.dart';
+import 'package:shelf_router/shelf_router.dart';
+
+part 'api.g.dart';
+
+class Api {
+ @Route.get('/time')
+ Response _time(Request request) {
+ return Response.ok('it is about now');
+ }
+
+ @Route.get('/to-uppercase/<word|.*>')
+ Future<Response> _toUpperCase(Request request, String word) async {
+ return Response.ok(word.toUpperCase());
+ }
+
+ Router get router => _$ApiRouter(this);
+}
diff --git a/pkgs/shelf_router_generator/test/server/api.g.dart b/pkgs/shelf_router_generator/test/server/api.g.dart
new file mode 100644
index 0000000..9c617f4
--- /dev/null
+++ b/pkgs/shelf_router_generator/test/server/api.g.dart
@@ -0,0 +1,14 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'api.dart';
+
+// **************************************************************************
+// ShelfRouterGenerator
+// **************************************************************************
+
+Router _$ApiRouter(Api service) {
+ final router = Router();
+ router.add('GET', '/time', service._time);
+ router.add('GET', '/to-uppercase/<word|.*>', service._toUpperCase);
+ return router;
+}
diff --git a/pkgs/shelf_router_generator/test/server/server.dart b/pkgs/shelf_router_generator/test/server/server.dart
new file mode 100644
index 0000000..19b68b3
--- /dev/null
+++ b/pkgs/shelf_router_generator/test/server/server.dart
@@ -0,0 +1,37 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async' show Future;
+import 'dart:io' show HttpServer;
+import 'package:shelf/shelf_io.dart' as shelf_io;
+import 'service.dart';
+
+class Server {
+ final _service = Service();
+ HttpServer _server;
+
+ Future<void> start() async {
+ _server = await shelf_io.serve(_service.router.handler, 'localhost', 0);
+ }
+
+ Future<void> stop() {
+ return _server.close();
+ }
+
+ Uri get uri => Uri(
+ scheme: 'http',
+ host: 'localhost',
+ port: _server.port,
+ );
+}
diff --git a/pkgs/shelf_router_generator/test/server/service.dart b/pkgs/shelf_router_generator/test/server/service.dart
new file mode 100644
index 0000000..1803972
--- /dev/null
+++ b/pkgs/shelf_router_generator/test/server/service.dart
@@ -0,0 +1,53 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async' show Future, FutureOr;
+import 'package:shelf/shelf.dart';
+import 'package:shelf_router/shelf_router.dart';
+import 'api.dart';
+
+part 'service.g.dart';
+
+class Service {
+ @Route.get('/say-hello')
+ @Route.get('/say-hello/')
+ Response _sayHello(Request request) {
+ return Response.ok('hello world');
+ }
+
+ @Route.get('/wave')
+ FutureOr<Response> _wave(Request request) async {
+ await Future.delayed(Duration(milliseconds: 50));
+ return Response.ok('_o/');
+ }
+
+ @Route.get('/greet/<user>')
+ Future<Response> _greet(Request request, String user) async {
+ return Response.ok('Greetings, $user');
+ }
+
+ @Route.get('/hi/<user>')
+ Future<Response> _hi(Request request) async {
+ final name = params(request, 'user');
+ return Response.ok('hi $name');
+ }
+
+ @Route.mount('/api/')
+ Router get _api => Api().router;
+
+ @Route.all('/<_|.*>')
+ Response _index(Request request) => Response.ok('nothing-here');
+
+ Router get router => _$ServiceRouter(this);
+}
diff --git a/pkgs/shelf_router_generator/test/server/service.g.dart b/pkgs/shelf_router_generator/test/server/service.g.dart
new file mode 100644
index 0000000..b08a5f1
--- /dev/null
+++ b/pkgs/shelf_router_generator/test/server/service.g.dart
@@ -0,0 +1,19 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'service.dart';
+
+// **************************************************************************
+// ShelfRouterGenerator
+// **************************************************************************
+
+Router _$ServiceRouter(Service service) {
+ final router = Router();
+ router.add('GET', '/say-hello', service._sayHello);
+ router.add('GET', '/say-hello/', service._sayHello);
+ router.add('GET', '/wave', service._wave);
+ router.add('GET', '/greet/<user>', service._greet);
+ router.add('GET', '/hi/<user>', service._hi);
+ router.mount('/api/', service._api);
+ router.all('/<_|.*>', service._index);
+ return router;
+}
diff --git a/pkgs/shelf_router_generator/test/server_test.dart b/pkgs/shelf_router_generator/test/server_test.dart
new file mode 100644
index 0000000..85b0852
--- /dev/null
+++ b/pkgs/shelf_router_generator/test/server_test.dart
@@ -0,0 +1,56 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:test/test.dart';
+import 'package:meta/meta.dart';
+import 'package:http/http.dart' as http;
+import 'server/server.dart';
+
+void main() {
+ final server = Server();
+ setUpAll(() => server.start());
+ tearDownAll(() => server.stop());
+
+ testGet({
+ @required String path,
+ @required String result,
+ }) =>
+ test('GET $path', () async {
+ final result = await http.read(server.uri.resolve(path));
+ expect(result, equals(result));
+ });
+
+ // Test simple handlers
+ testGet(path: '/say-hello', result: 'hello world');
+ testGet(path: '/say-hello/', result: 'hello world');
+ testGet(path: '/wave', result: '_o/');
+ testGet(path: '/greet/jonasfj', result: 'Greetings, jonasfj');
+ testGet(path: '/greet/sigurdm', result: 'Greetings, sigurdm');
+ testGet(path: '/hi/jonasfj', result: 'hi jonasfj');
+ testGet(path: '/hi/sigurdm', result: 'hi sigurdm');
+
+ // Test /api/
+ testGet(path: '/api/time', result: 'it is about now');
+ testGet(path: '/api/to-uppercase/wEiRd%20Word', result: 'WEIRD WORD');
+ testGet(path: '/api/to-uppercase/wEiRd Word', result: 'WEIRD WORD');
+
+ // Test the catch all handler
+ testGet(path: '/', result: 'nothing-here');
+ testGet(path: '/wrong-path', result: 'nothing-here');
+ testGet(path: '/hi/sigurdm/ups', result: 'nothing-here');
+ testGet(path: '/api/to-uppercase/too/many/slashs', result: 'nothing-here');
+ testGet(path: '/api/', result: 'nothing-here');
+ testGet(path: '/api/time/', result: 'nothing-here'); // notice the extra slash
+ testGet(path: '/api/tim', result: 'nothing-here');
+}