From 703ed562e6448ad3d9d22e709278ced41b710136 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Wed, 3 Jul 2024 03:02:49 +0800 Subject: [PATCH 01/35] chore: WIP --- .gitattributes | 2 - .gitignore | 8 +- CHANGELOG.md | 3 + ...ysis_options.yaml => analysis_options.yaml | 6 +- lib/spry.dart | 0 lib/src/app.dart | 7 + lib/src/composable/next.dart | 10 + lib/src/create_app.dart | 17 ++ lib/src/create_stack_handler.dart | 17 ++ lib/src/define_handler.dart | 19 ++ lib/src/event.dart | 1 + lib/src/handler.dart | 5 + lib/src/routing/create_router.dart | 51 +++++ lib/src/routing/router.dart | 5 + packages/spry/.gitignore | 6 - packages/spry/CHANGELOG.md | 216 ------------------ packages/spry/LICENSE | 1 - packages/spry/README.md | 1 - packages/spry/example/main.dart | 14 -- packages/spry/lib/spry.dart | 53 ----- .../src/_internal/application+factory.dart | 17 -- .../src/_internal/application+powered_by.dart | 11 - .../lib/src/_internal/iterable_utils.dart | 10 - .../spry/lib/src/_internal/map+value_of.dart | 10 - .../spry/lib/src/_internal/request+clone.dart | 22 -- packages/spry/lib/src/_internal/request.dart | 108 --------- packages/spry/lib/src/_internal/response.dart | 144 ------------ .../spry/lib/src/_internal/stream+clone.dart | 28 --- .../spry/lib/src/application+encoding.dart | 27 --- .../spry/lib/src/application+handler.dart | 165 ------------- packages/spry/lib/src/application+listen.dart | 12 - packages/spry/lib/src/application+run.dart | 45 ---- packages/spry/lib/src/application.dart | 108 --------- packages/spry/lib/src/exception/abort.dart | 61 ----- .../lib/src/exception/abort_exception.dart | 7 - .../src/exception/application+exceptions.dart | 14 -- .../lib/src/exception/exception_filter.dart | 7 - .../lib/src/exception/exception_source.dart | 24 -- .../spry/lib/src/exception/exceptions.dart | 110 --------- .../lib/src/exception/exceptions_builder.dart | 6 - .../lib/src/exception/returow_exception.dart | 3 - .../spry/lib/src/handler/closure_handler.dart | 19 -- packages/spry/lib/src/handler/handler.dart | 6 - .../middleware/application+middleware.dart | 19 -- .../src/middleware/middleware+handler.dart | 40 ---- .../spry/lib/src/middleware/middleware.dart | 9 - .../lib/src/middleware/middleware_stack.dart | 50 ---- .../lib/src/request/request+application.dart | 11 - .../spry/lib/src/request/request+clone.dart | 10 - .../spry/lib/src/request/request+fetch.dart | 102 --------- .../lib/src/request/request+formdata.dart | 49 ---- .../spry/lib/src/request/request+json.dart | 40 ---- .../spry/lib/src/request/request+locals.dart | 10 - .../spry/lib/src/request/request+params.dart | 17 -- .../spry/lib/src/request/request+route.dart | 12 - .../src/request/request+search_params.dart | 21 -- .../spry/lib/src/request/request+text.dart | 40 ---- .../lib/src/response/response+is_closed.dart | 9 - .../spry/lib/src/response/responsible.dart | 210 ----------------- packages/spry/lib/src/routing/route.dart | 27 --- .../lib/src/routing/route_collection.dart | 11 - packages/spry/lib/src/routing/routes.dart | 18 -- .../src/routing/routes_builder+closure.dart | 36 --- .../lib/src/routing/routes_builder+group.dart | 82 ------- .../src/routing/routes_builder+methods.dart | 49 ---- .../src/routing/routes_builder+websocket.dart | 79 ------- .../spry/lib/src/routing/routes_builder.dart | 6 - packages/spry/pubspec.yaml | 18 -- .../spry/test/_internal/test_request.dart | 47 ---- .../exceptions/rethrow_exception_test.dart | 49 ---- pubspec.yaml | 16 ++ 71 files changed, 162 insertions(+), 2331 deletions(-) delete mode 100644 .gitattributes create mode 100644 CHANGELOG.md rename packages/spry/analysis_options.yaml => analysis_options.yaml (95%) create mode 100644 lib/spry.dart create mode 100644 lib/src/app.dart create mode 100644 lib/src/composable/next.dart create mode 100644 lib/src/create_app.dart create mode 100644 lib/src/create_stack_handler.dart create mode 100644 lib/src/define_handler.dart create mode 100644 lib/src/event.dart create mode 100644 lib/src/handler.dart create mode 100644 lib/src/routing/create_router.dart create mode 100644 lib/src/routing/router.dart delete mode 100644 packages/spry/.gitignore delete mode 100644 packages/spry/CHANGELOG.md delete mode 120000 packages/spry/LICENSE delete mode 120000 packages/spry/README.md delete mode 100644 packages/spry/example/main.dart delete mode 100644 packages/spry/lib/spry.dart delete mode 100644 packages/spry/lib/src/_internal/application+factory.dart delete mode 100644 packages/spry/lib/src/_internal/application+powered_by.dart delete mode 100644 packages/spry/lib/src/_internal/iterable_utils.dart delete mode 100644 packages/spry/lib/src/_internal/map+value_of.dart delete mode 100644 packages/spry/lib/src/_internal/request+clone.dart delete mode 100644 packages/spry/lib/src/_internal/request.dart delete mode 100644 packages/spry/lib/src/_internal/response.dart delete mode 100644 packages/spry/lib/src/_internal/stream+clone.dart delete mode 100644 packages/spry/lib/src/application+encoding.dart delete mode 100644 packages/spry/lib/src/application+handler.dart delete mode 100644 packages/spry/lib/src/application+listen.dart delete mode 100644 packages/spry/lib/src/application+run.dart delete mode 100644 packages/spry/lib/src/application.dart delete mode 100644 packages/spry/lib/src/exception/abort.dart delete mode 100644 packages/spry/lib/src/exception/abort_exception.dart delete mode 100644 packages/spry/lib/src/exception/application+exceptions.dart delete mode 100644 packages/spry/lib/src/exception/exception_filter.dart delete mode 100644 packages/spry/lib/src/exception/exception_source.dart delete mode 100644 packages/spry/lib/src/exception/exceptions.dart delete mode 100644 packages/spry/lib/src/exception/exceptions_builder.dart delete mode 100644 packages/spry/lib/src/exception/returow_exception.dart delete mode 100644 packages/spry/lib/src/handler/closure_handler.dart delete mode 100644 packages/spry/lib/src/handler/handler.dart delete mode 100644 packages/spry/lib/src/middleware/application+middleware.dart delete mode 100644 packages/spry/lib/src/middleware/middleware+handler.dart delete mode 100644 packages/spry/lib/src/middleware/middleware.dart delete mode 100644 packages/spry/lib/src/middleware/middleware_stack.dart delete mode 100644 packages/spry/lib/src/request/request+application.dart delete mode 100644 packages/spry/lib/src/request/request+clone.dart delete mode 100644 packages/spry/lib/src/request/request+fetch.dart delete mode 100644 packages/spry/lib/src/request/request+formdata.dart delete mode 100644 packages/spry/lib/src/request/request+json.dart delete mode 100644 packages/spry/lib/src/request/request+locals.dart delete mode 100644 packages/spry/lib/src/request/request+params.dart delete mode 100644 packages/spry/lib/src/request/request+route.dart delete mode 100644 packages/spry/lib/src/request/request+search_params.dart delete mode 100644 packages/spry/lib/src/request/request+text.dart delete mode 100644 packages/spry/lib/src/response/response+is_closed.dart delete mode 100644 packages/spry/lib/src/response/responsible.dart delete mode 100644 packages/spry/lib/src/routing/route.dart delete mode 100644 packages/spry/lib/src/routing/route_collection.dart delete mode 100644 packages/spry/lib/src/routing/routes.dart delete mode 100644 packages/spry/lib/src/routing/routes_builder+closure.dart delete mode 100644 packages/spry/lib/src/routing/routes_builder+group.dart delete mode 100644 packages/spry/lib/src/routing/routes_builder+methods.dart delete mode 100644 packages/spry/lib/src/routing/routes_builder+websocket.dart delete mode 100644 packages/spry/lib/src/routing/routes_builder.dart delete mode 100644 packages/spry/pubspec.yaml delete mode 100644 packages/spry/test/_internal/test_request.dart delete mode 100644 packages/spry/test/exceptions/rethrow_exception_test.dart create mode 100644 pubspec.yaml diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index dfe0770..0000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# Auto detect text files and perform LF normalization -* text=auto diff --git a/.gitignore b/.gitignore index 40b878d..3cceda5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,7 @@ -node_modules/ \ No newline at end of file +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..effe43c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/packages/spry/analysis_options.yaml b/analysis_options.yaml similarity index 95% rename from packages/spry/analysis_options.yaml rename to analysis_options.yaml index c805c98..dee8927 100644 --- a/packages/spry/analysis_options.yaml +++ b/analysis_options.yaml @@ -12,6 +12,7 @@ # The core lints are also what is used by pub.dev for scoring packages. include: package:lints/recommended.yaml + # Uncomment the following section to specify additional rules. # linter: @@ -19,8 +20,9 @@ include: package:lints/recommended.yaml # - camel_case_types # analyzer: -# enable-experiment: -# - inline-class +# exclude: +# - path/to/excluded/files/** + # For more information about the core and recommended set of lints, see # https://dart.dev/go/core-lints diff --git a/lib/spry.dart b/lib/spry.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/src/app.dart b/lib/src/app.dart new file mode 100644 index 0000000..4e8f073 --- /dev/null +++ b/lib/src/app.dart @@ -0,0 +1,7 @@ +import 'handler.dart'; + +abstract interface class App { + void use(Handler handler); + + Handler get handler; +} diff --git a/lib/src/composable/next.dart b/lib/src/composable/next.dart new file mode 100644 index 0000000..6d608a3 --- /dev/null +++ b/lib/src/composable/next.dart @@ -0,0 +1,10 @@ +Future Function()? _effect; + +Future next() async { + await _effect?.call(); + _effect = null; +} + +void setNext(Future Function() next) { + _effect = next; +} diff --git a/lib/src/create_app.dart b/lib/src/create_app.dart new file mode 100644 index 0000000..0e9f0a9 --- /dev/null +++ b/lib/src/create_app.dart @@ -0,0 +1,17 @@ +import 'app.dart'; +import 'create_stack_handler.dart'; +import 'handler.dart'; + +App createApp() => _AppImpl(); + +final class _AppImpl implements App { + final handlerStack = []; + + @override + void use(Handler handler) { + handlerStack.add(handler); + } + + @override + Handler get handler => createStackHandler(handlerStack.reversed); +} diff --git a/lib/src/create_stack_handler.dart b/lib/src/create_stack_handler.dart new file mode 100644 index 0000000..bc938a3 --- /dev/null +++ b/lib/src/create_stack_handler.dart @@ -0,0 +1,17 @@ +import 'composable/next.dart'; +import 'define_handler.dart'; +import 'event.dart'; +import 'handler.dart'; + +Handler createStackHandler(Iterable stack) { + final handle = stack.fold Function(Event)>( + (_) async {}, + (child, handler) => (event) async { + setNext(() => child(event)); + + return handler.handle(event); + }, + ); + + return defineHandler(handle); +} diff --git a/lib/src/define_handler.dart b/lib/src/define_handler.dart new file mode 100644 index 0000000..9bd33f1 --- /dev/null +++ b/lib/src/define_handler.dart @@ -0,0 +1,19 @@ +import 'dart:async'; + +import 'event.dart'; +import 'handler.dart'; + +Handler defineHandler(FutureOr Function(Event event) handle) { + return _ClosureHandler(handle); +} + +final class _ClosureHandler implements Handler { + const _ClosureHandler(this._closure); + + final FutureOr Function(Event event) _closure; + + @override + Future handle(Event event) async { + return _closure(event); + } +} diff --git a/lib/src/event.dart b/lib/src/event.dart new file mode 100644 index 0000000..d29f67a --- /dev/null +++ b/lib/src/event.dart @@ -0,0 +1 @@ +abstract interface class Event {} diff --git a/lib/src/handler.dart b/lib/src/handler.dart new file mode 100644 index 0000000..d092020 --- /dev/null +++ b/lib/src/handler.dart @@ -0,0 +1,5 @@ +import 'event.dart'; + +abstract interface class Handler { + Future handle(Event event); +} diff --git a/lib/src/routing/create_router.dart b/lib/src/routing/create_router.dart new file mode 100644 index 0000000..0410c40 --- /dev/null +++ b/lib/src/routing/create_router.dart @@ -0,0 +1,51 @@ +import 'package:routingkit/routingkit.dart' as routingkit; + +import '../composable/next.dart'; +import '../create_stack_handler.dart'; +import '../define_handler.dart'; +import '../event.dart'; +import '../handler.dart'; +import 'router.dart'; + +Router createRouter() { + final inner = routingkit.createRouter(); + + return _RouterImpl(inner); +} + +typedef _HandleWith = Future Function(Event, Handler); + +final class _RouterImpl implements Router { + _RouterImpl(this.inner); + + _HandleWith? handleWith; + final handlerStack = []; + final routingkit.Router inner; + + @override + Future handle(Event event) { + final (_, route) = inner.lookup('/'); + + return switch ((route, handleWith)) { + (Handler handler, _HandleWith handle) => handle(event, handler), + (Handler handler, _) => handler.handle(event), + _ => throw 000, + }; + } + + @override + void use(Handler handler) { + handlerStack.add(handler); + handleWith = handlerStack.reversed.fold<_HandleWith>( + defaultRouterHandle, + (child, current) => (event, handler) async { + setNext(() => child(event, handler)); + + return current.handle(event); + }, + ); + } + + static Future defaultRouterHandle(Event event, Handler handler) => + handler.handle(event); +} diff --git a/lib/src/routing/router.dart b/lib/src/routing/router.dart new file mode 100644 index 0000000..3dbba3b --- /dev/null +++ b/lib/src/routing/router.dart @@ -0,0 +1,5 @@ +import '../handler.dart'; + +abstract interface class Router implements Handler { + void use(Handler handler); +} diff --git a/packages/spry/.gitignore b/packages/spry/.gitignore deleted file mode 100644 index 698723a..0000000 --- a/packages/spry/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -# Files and directories created by pub. -.dart_tool/ - -# Omit committing pubspec.lock for library packages; see -# https://dart.dev/guides/libraries/private-files#pubspeclock. -pubspec.lock diff --git a/packages/spry/CHANGELOG.md b/packages/spry/CHANGELOG.md deleted file mode 100644 index ce2385a..0000000 --- a/packages/spry/CHANGELOG.md +++ /dev/null @@ -1,216 +0,0 @@ -# Spry v3.3.1 - - To install Spry v3.3.1 run the following command: - - ```bash - dart pub add spry:3.3.1 - ``` - - or update your `pubspec.yaml` file: - - ```yaml - dependencies: - spry: ^3.3.1 - ``` - - ## What's Changed - - - fix(request): Fetch client using mook request - -# Spry v3.3.0 - -To install Spry v3.3.0 run the following command: - -```bash -dart pub add spry:3.3.0 -``` - -or update your `pubspec.yaml` file: - -```yaml -dependencies: - spry: ^3.3.0 -``` - -## What's Changed - -- **Feature**: Support `request.fetch`, see `webfetch` fetch function. - -# Spry v3.2.3 - -To install Spry v3.2.3 run the following command: - -```bash -dart pub add spry:3.2.3 -``` - -Or update your `pubspec.yaml` file: - -```yaml -dependencies: - spry: ^3.2.3 -``` - -## What's Changed - -- fix `request.json()` being incorrectly set to not lock (`false`). - -# Spry v3.2.2 - -To install Spry v3.2.2 run the following command: - -```bash -dart pub add spry:3.2.2 -``` - -Or update your `pubspec.yaml` file: - -```yaml -dependencies: - spry: ^3.2.2 -``` - -## What's Changed - -- fix response not using encoding -- fix `FormData` response charset -- `Responsesible` support `HttpClientResponse` of `dart:io` -- `Responsesible` support `HttpResponse` of `dart:io` -- `Responsesible` support `TypedData` - -# Spry v3.2.1 - -To install Spry v3.2.1 run the following command: - -```bash -dart pub add spry:3.2.1 -``` - -Or update your `pubspec.yaml` file: - -```yaml -dependencies: - spry: ^3.2.1 -``` - -## What's Changed - -1. fix type version information - -# Spry v3.2.0 - -To install Spry v3.2.0 run the following command: - -```bash -dart pub add spry:3.2.0 -``` - -Or update your `pubspec.yaml` file: - -```yaml -dependencies: - spry: ^3.2.0 -``` - -## What's Changed - -1. **Docs**: fix `route.path` to `route.segments`, Thanks [@utamori](https://github.com/utamori) -2. **Feature**: Support `RethrowEception`, Catch exception to next filter process -3. **Feature**: `request.json` supports implicit type conversion -4. **Feature**: Added built-in `Blob` export -5. **Feature**: Support closure-style middleware -6. **Feature**: Support custom `x-powered-by` header value in `app.poweredBy` - -## Credits - -- @utamori - [GitHub Profile](https://github.com/utamori) - -# Spry v3.1.2 - -To install Spry v3.1.2 run the following command: - -```bash -dart pub add spry:3.1.2 -``` - -Or update your `pubspec.yaml` file: - -```yaml -dependencies: - spry: ^3.1.2 -``` - -## What's Changed - -1. **BUG**: Fixed Iterable responsible cannot be correctly JSON serialized. -2. **BUG**: Fixed middleware handler not being able to call next correctly. -3. `app.group` adds closure suggested parameter name - -# Spry v3.1.1 - -To install Spry v3.1.1 run the following command: - -```bash -dart pub add spry:3.1.1 -``` - -Or update your `pubspec.yaml` file: - -```yaml -dependencies: - spry: ^3.1.1 -``` - -## What's Changed - -1. **Bug**: Fix global middleware not being used -2. **Feature**: Support a `Iterable` of `Middleware` stack support making handler. -3. **Feature**: Public `ClosureHandler` class. - -# Spry 3.1.0 - -To install Spry 3.1.0 run the following command: - -```bash -dart pub add spry -``` - -Or update your `pubspec.yaml` file: - -```yaml -dependencies: - spry: ^3.1.0 -``` - -## What's Changed - -1. **Feature**: `Application` support late initialization HTTP server -2. **Feature**: `Application` support with HTTP server factory. -3. **Docs**: Add `Application` document. -4. **Bug**: Fix `addRoute` without parameter signature of `T` type. -5. **Feature**: Support web socket. -6. **Docs**: Add web socket document. - -# Spry 3.0.0 - -To install Spry 3.0.0 run the following command: - -```bash -dart pub add spry -``` - -Or update your `pubspec.yaml` file: - -```yaml -dependencies: - spry: ^3.0.0 -``` - -## What's Changed - -1. **Breaking Change**: Rewrite the entire design of the framework, and the API is completely different from the previous version. -2. Cancel the request dialect and use `HttpRequest` of `dart:io` -3. Cancel the response dialect and use `HttpResponse` of `dart:io` -4. perform magic on the `HttpRequest` and `HttpResponse` objects to make them easier to use and more powerful -5. Better performing router -6. Let you learn less, you only need to know `dart:io` diff --git a/packages/spry/LICENSE b/packages/spry/LICENSE deleted file mode 120000 index 30cff74..0000000 --- a/packages/spry/LICENSE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE \ No newline at end of file diff --git a/packages/spry/README.md b/packages/spry/README.md deleted file mode 120000 index fe84005..0000000 --- a/packages/spry/README.md +++ /dev/null @@ -1 +0,0 @@ -../../README.md \ No newline at end of file diff --git a/packages/spry/example/main.dart b/packages/spry/example/main.dart deleted file mode 100644 index 47968a4..0000000 --- a/packages/spry/example/main.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:spry/spry.dart'; - -final app = Application.late(); - -void main(List args) async { - app.get('hello', (request) => 'Hello, Spry!'); - app.get('test', (request) { - return request.fetch('/hello'); - }); - - await app.run(port: 3000); - - print('Listening on http://localhost:3000'); -} diff --git a/packages/spry/lib/spry.dart b/packages/spry/lib/spry.dart deleted file mode 100644 index 1d07379..0000000 --- a/packages/spry/lib/spry.dart +++ /dev/null @@ -1,53 +0,0 @@ -library; - -// Application -export 'src/application.dart'; -export 'src/application+encoding.dart'; -export 'src/application+handler.dart'; -export 'src/application+listen.dart'; -export 'src/application+run.dart'; - -// Exception -export 'src/exception/abort.dart'; -export 'src/exception/abort_exception.dart'; -export 'src/exception/exception_filter.dart'; -export 'src/exception/exception_source.dart'; -export 'src/exception/exceptions.dart'; -export 'src/exception/exceptions_builder.dart'; -export 'src/exception/application+exceptions.dart'; -export 'src/exception/returow_exception.dart'; - -// Handler -export 'src/handler/handler.dart'; -export 'src/handler/closure_handler.dart'; - -// Middleware -export 'src/middleware/application+middleware.dart'; -export 'src/middleware/middleware.dart'; -export 'src/middleware/middleware_stack.dart'; -export 'src/middleware/middleware+handler.dart'; - -// Request -export 'src/request/request+application.dart'; -export 'src/request/request+clone.dart'; -export 'src/request/request+formdata.dart'; -export 'src/request/request+locals.dart'; -export 'src/request/request+params.dart'; -export 'src/request/request+route.dart'; -export 'src/request/request+search_params.dart'; -export 'src/request/request+json.dart'; -export 'src/request/request+text.dart'; -export 'src/request/request+fetch.dart' show Request$Fetch; - -// Response -export 'src/response/response+is_closed.dart'; -export 'src/response/responsible.dart'; - -// Routing -export 'src/routing/route.dart'; -export 'src/routing/route_collection.dart'; -export 'src/routing/routes.dart'; -export 'src/routing/routes_builder.dart'; -export 'src/routing/routes_builder+closure.dart'; -export 'src/routing/routes_builder+group.dart'; -export 'src/routing/routes_builder+methods.dart'; diff --git a/packages/spry/lib/src/_internal/application+factory.dart b/packages/spry/lib/src/_internal/application+factory.dart deleted file mode 100644 index 0c9cfff..0000000 --- a/packages/spry/lib/src/_internal/application+factory.dart +++ /dev/null @@ -1,17 +0,0 @@ -// ignore_for_file: file_names - -import 'dart:async'; -import 'dart:io'; - -import '../application.dart'; -import 'map+value_of.dart'; - -typedef ServerFactory = FutureOr Function(Application application); - -extension Application$Factory on Application { - /// Returns spry application server factory. - ServerFactory? get factory { - if (locals[#spry.server.initialized] == true) return null; - return locals.valueOf(#spry.server.factory, (_) => null); - } -} diff --git a/packages/spry/lib/src/_internal/application+powered_by.dart b/packages/spry/lib/src/_internal/application+powered_by.dart deleted file mode 100644 index 6380922..0000000 --- a/packages/spry/lib/src/_internal/application+powered_by.dart +++ /dev/null @@ -1,11 +0,0 @@ -// ignore_for_file: file_names - -import '../application.dart'; - -extension Application$PowerdBy on Application { - void configurationPoweredBy() { - final value = poweredBy ?? 'Spry/${Application.version}'; - - server.defaultResponseHeaders.set('x-powered-by', value); - } -} diff --git a/packages/spry/lib/src/_internal/iterable_utils.dart b/packages/spry/lib/src/_internal/iterable_utils.dart deleted file mode 100644 index 8206150..0000000 --- a/packages/spry/lib/src/_internal/iterable_utils.dart +++ /dev/null @@ -1,10 +0,0 @@ -extension IterableUtils on Iterable { - /// Returns first where or null. - T? firstWhereOrNull(bool Function(T element) test) { - for (final element in this) { - if (test(element)) return element; - } - - return null; - } -} diff --git a/packages/spry/lib/src/_internal/map+value_of.dart b/packages/spry/lib/src/_internal/map+value_of.dart deleted file mode 100644 index 29b3614..0000000 --- a/packages/spry/lib/src/_internal/map+value_of.dart +++ /dev/null @@ -1,10 +0,0 @@ -// ignore_for_file: file_names - -extension MapValueOf on Map { - T valueOf(K key, T Function(V? value) factory) { - return switch (this[key]) { - T value => value, - V? value => factory(value), - }; - } -} diff --git a/packages/spry/lib/src/_internal/request+clone.dart b/packages/spry/lib/src/_internal/request+clone.dart deleted file mode 100644 index 658e2ce..0000000 --- a/packages/spry/lib/src/_internal/request+clone.dart +++ /dev/null @@ -1,22 +0,0 @@ -// ignore_for_file: file_names - -import 'request.dart'; -import 'stream+clone.dart'; - -extension SpryRequest$Clone on SpryRequest { - SpryRequest clone() { - final (stream1, stream2) = stream.clone(); - - // Replace the original stream with the clone. - stream = stream1; - - // Create a new request with the cloned stream. - return SpryRequest( - application: application, - request: request, - response: response, - stream: stream2, - locals: locals, - ); - } -} diff --git a/packages/spry/lib/src/_internal/request.dart b/packages/spry/lib/src/_internal/request.dart deleted file mode 100644 index 6357754..0000000 --- a/packages/spry/lib/src/_internal/request.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:typed_data'; - -import '../application.dart'; -import '../exception/abort.dart'; -import 'response.dart'; - -class SpryRequest extends Stream implements HttpRequest { - /// Resolve a [SpryRequest] from the given [request]. - factory SpryRequest.of(HttpRequest request) { - if (request is SpryRequest) return request; - throw Abort( - HttpStatus.internalServerError, - message: - 'The request magic only runs within the Spry framework and cannot be accessed by regular HTTP requests.', - ); - } - - Stream stream; - - final HttpRequest request; - - @override - final SpryResponse response; - - final Application application; - final Map locals; - - /// Creates a new [SpryRequest] with the given [application], [request] and - /// [stream]. - SpryRequest({ - required this.application, - required this.request, - required this.response, - required this.stream, - required this.locals, - }); - - /// Creates a new [SpryRequest] with the given [application] and [request]. - factory SpryRequest.from({ - required Application application, - required HttpRequest request, - }) { - if (request is SpryRequest) return request; - - final response = SpryResponse( - application: application, - response: request.response, - ); - - return SpryRequest( - application: application, - request: request, - response: response, - stream: request, - locals: {}, - ); - } - - @override - StreamSubscription listen( - void Function(Uint8List event)? onData, { - Function? onError, - void Function()? onDone, - bool? cancelOnError, - }) { - return stream.listen( - onData, - onError: onError, - onDone: onDone, - cancelOnError: cancelOnError, - ); - } - - @override - X509Certificate? get certificate => request.certificate; - - @override - HttpConnectionInfo? get connectionInfo => request.connectionInfo; - - @override - int get contentLength => request.contentLength; - - @override - List get cookies => request.cookies; - - @override - HttpHeaders get headers => request.headers; - - @override - String get method => request.method; - - @override - bool get persistentConnection => request.persistentConnection; - - @override - String get protocolVersion => request.protocolVersion; - - @override - Uri get requestedUri => request.requestedUri; - - @override - HttpSession get session => request.session; - - @override - Uri get uri => request.uri; -} diff --git a/packages/spry/lib/src/_internal/response.dart b/packages/spry/lib/src/_internal/response.dart deleted file mode 100644 index b5c28d7..0000000 --- a/packages/spry/lib/src/_internal/response.dart +++ /dev/null @@ -1,144 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import '../application.dart'; -import '../exception/abort.dart'; - -class SpryResponse implements HttpResponse { - final HttpResponse response; - final Application application; - - SpryResponse({ - required this.response, - required this.application, - }); - - factory SpryResponse.of(HttpResponse response) { - if (response is SpryResponse) return response; - - throw Abort( - HttpStatus.internalServerError, - message: - 'The response magic only runs within the Spry framework and cannot be accessed by regular HTTP responses.', - ); - } - - bool _isClosed = false; - - /// Whether the response is closed. - bool get isClosed => _isClosed; - - /// Safely closes the response. - Future safeClose() async { - if (!isClosed) { - return await close(); - } - } - - @override - bool get bufferOutput => response.bufferOutput; - - @override - set bufferOutput(bool bufferOutput) => response.bufferOutput = bufferOutput; - - @override - int get contentLength => response.contentLength; - - @override - set contentLength(int contentLength) => - response.contentLength = contentLength; - - @override - Duration? get deadline => response.deadline; - - @override - set deadline(Duration? deadline) => response.deadline = deadline; - - @override - Encoding get encoding => response.encoding; - - @override - set encoding(Encoding encoding) { - // Bad state: IOSink encoding is not mutable - // - // dart:_http/http_impl.dart:1048:7 - } - - @override - bool get persistentConnection => response.persistentConnection; - - @override - set persistentConnection(bool persistentConnection) => - response.persistentConnection = persistentConnection; - - @override - String get reasonPhrase => response.reasonPhrase; - - @override - set reasonPhrase(String reasonPhrase) => response.reasonPhrase = reasonPhrase; - - @override - int get statusCode => response.statusCode; - - @override - set statusCode(int statusCode) => response.statusCode = statusCode; - - @override - void add(List data) => response.add(data); - - @override - void addError(Object error, [StackTrace? stackTrace]) => - response.addError(error, stackTrace); - - @override - Future addStream(Stream> stream) => response.addStream(stream); - - @override - Future close() { - _isClosed = true; - - return response.close(); - } - - @override - HttpConnectionInfo? get connectionInfo => response.connectionInfo; - - @override - List get cookies => response.cookies; - - @override - Future detachSocket({bool writeHeaders = true}) { - _isClosed = true; - - return response.detachSocket(writeHeaders: writeHeaders); - } - - @override - Future get done => response.done; - - @override - Future flush() => response.flush(); - - @override - HttpHeaders get headers => response.headers; - - @override - Future redirect(Uri location, {int status = HttpStatus.movedTemporarily}) { - _isClosed = true; - - return response.redirect(location, status: status); - } - - @override - void write(Object? object) => response.write(object); - - @override - void writeAll(Iterable objects, [String separator = ""]) => - response.writeAll(objects, separator); - - @override - void writeCharCode(int charCode) => response.writeCharCode(charCode); - - @override - void writeln([Object? object = ""]) => response.writeln(object); -} diff --git a/packages/spry/lib/src/_internal/stream+clone.dart b/packages/spry/lib/src/_internal/stream+clone.dart deleted file mode 100644 index de6e4e0..0000000 --- a/packages/spry/lib/src/_internal/stream+clone.dart +++ /dev/null @@ -1,28 +0,0 @@ -// ignore_for_file: file_names - -import 'dart:async'; - -extension Stream$Clone on Stream { - /// Clone current stream, returns two streams. - (Stream, Stream) clone() { - final controller1 = StreamController(); - final controller2 = StreamController(); - - listen( - (event) { - controller1.add(event); - controller2.add(event); - }, - onError: (error) { - controller1.addError(error); - controller2.addError(error); - }, - onDone: () { - controller1.close(); - controller2.close(); - }, - ); - - return (controller1.stream, controller2.stream); - } -} diff --git a/packages/spry/lib/src/application+encoding.dart b/packages/spry/lib/src/application+encoding.dart deleted file mode 100644 index 91e85fe..0000000 --- a/packages/spry/lib/src/application+encoding.dart +++ /dev/null @@ -1,27 +0,0 @@ -// ignore_for_file: file_names - -import 'dart:convert'; - -import '_internal/map+value_of.dart'; -import 'application.dart'; - -extension Application$Encoding on Application { - /// Returns global encoding. - Encoding get encoding { - return locals.valueOf(#spry.encoding, (name) { - if (name is String?) { - final encoding = Encoding.getByName(name); - if (encoding != null) return encoding; - } - - // Default encoding. - return utf8; - }); - } - - /// Sets global encoding. - set encoding(Encoding encoding) { - locals[#spry.encoding] = encoding; - logger.config('Encoding set to ${encoding.name}'); - } -} diff --git a/packages/spry/lib/src/application+handler.dart b/packages/spry/lib/src/application+handler.dart deleted file mode 100644 index 203a1b7..0000000 --- a/packages/spry/lib/src/application+handler.dart +++ /dev/null @@ -1,165 +0,0 @@ -// ignore_for_file: file_names - -import 'dart:io'; - -import 'package:routingkit/routingkit.dart'; - -import '_internal/application+factory.dart'; -import '_internal/application+powered_by.dart'; -import '_internal/map+value_of.dart'; -import '_internal/request.dart'; -import 'application.dart'; -import 'exception/abort.dart'; -import 'exception/application+exceptions.dart'; -import 'exception/exception_source.dart'; -import 'handler/handler.dart'; -import 'middleware/application+middleware.dart'; -import 'middleware/middleware+handler.dart'; -import 'request/request+params.dart'; -import 'response/responsible.dart'; -import 'routing/route.dart'; - -extension Application$Handler on Application { - Handler get handler => _handler; -} - -extension on Application { - _ApplicationHandler get _handler { - if (locals[#spry.server.initialized] != true && factory == null) { - final error = StateError( - 'HTTP Server not initialized, You must call `app.run()` bootstrap your Spry application.'); - logger.severe(error.message, error); - - throw error; - } - - return locals.valueOf( - #spry.handler, - (_) => _ApplicationHandler(this)..initialize(), - ); - } -} - -class _ApplicationHandler implements Handler { - final Application application; - late final Router<(Route, Handler)> router; - - _ApplicationHandler(this.application) { - application.locals[#spry.handler] = this; - router = TrieRouter( - caseSensitive: application.routes.caseSensitive, - logger: application.logger, - ); - - application.logger.config('Routes created'); - } - - /// Initializes the router handler. - void initialize() { - for (final route in application.routes) { - final segments = route.segments.where((element) { - if (element is ConstSegment) { - return element.value.isNotEmpty; - } - - return true; - }); - - // Makes the handler with application middleware stack. - final handler = application.middleware.makeHandler(route.handler); - - router.register( - (route, handler), - [ConstSegment(route.method.toUpperCase()), ...segments], - ); - } - - application.logger.config('Routes initialized'); - } - - /// Handler for incoming requests. - @override - Future handle(HttpRequest incoming) async { - if (application.factory != null) { - application.server = await application.factory!(application); - application.locals[#spry.server.initialized] = true; - application.configurationPoweredBy(); - } - - final request = - SpryRequest.from(application: application, request: incoming); - final response = request.response; - - try { - final (route, handler) = lookup(request); - request.locals[#spry.request.route] = route; - - // Runs the handler and gets the result. - final result = await handler.handle(request); - - // If result is exception or error, throws it. - if (result is Exception || result is Error) { - throw result!; - } - - // Result is null or response is closed, returns it. - if (!response.isClosed) { - await Responsible.create(result).respond(request); - } - - return result; - } catch (error, stackTrace) { - final source = ExceptionSource( - exception: error, - stackTrace: stackTrace, - responseClosedFactory: () => response.isClosed, - ); - - await application.exceptions.process(source, request); - } finally { - // Safely closes the response. - await response.safeClose(); - } - - return null; - } - - /// Lookup the handler for the given request. - (Route, Handler) lookup(SpryRequest request) { - final method = request.method.toUpperCase(); - final segments = request.uri.pathSegments; - - // If the request is `HEAD`, finds the `HEAD` handler, if not found, - // try to find the `GET` handler. - if (method == 'HEAD') { - final head = router.lookup( - ['HEAD', ...segments], - request.params, - ); - if (head != null) return head; - - final get = router.lookup( - ['GET', ...segments], - request.params, - ); - if (get != null) return get; - } - - final result = router.lookup( - [method, ...segments], - request.params, - ); - - if (result == null) { - final error = Abort(HttpStatus.notFound); - application.logger.warning( - 'No handler found for ${request.method} ${request.uri}', - error, - ); - - throw error; - } - - return result; - } -} diff --git a/packages/spry/lib/src/application+listen.dart b/packages/spry/lib/src/application+listen.dart deleted file mode 100644 index 649b6be..0000000 --- a/packages/spry/lib/src/application+listen.dart +++ /dev/null @@ -1,12 +0,0 @@ -// ignore_for_file: file_names - -import 'dart:async'; -import 'dart:io'; - -import 'application+handler.dart'; -import 'application.dart'; - -extension Application$Listen on Application { - /// Simply listen to the [HttpServer] and handle incoming requests. - StreamSubscription listen() => server.listen(handler.handle); -} diff --git a/packages/spry/lib/src/application+run.dart b/packages/spry/lib/src/application+run.dart deleted file mode 100644 index b7e6ae5..0000000 --- a/packages/spry/lib/src/application+run.dart +++ /dev/null @@ -1,45 +0,0 @@ -// ignore_for_file: file_names - -import 'dart:async'; -import 'dart:io'; - -import '_internal/application+factory.dart'; -import '_internal/application+powered_by.dart'; -import 'application.dart'; -import 'application+listen.dart'; - -extension Application$Run on Application { - /// Runs the spry application. - Future> run({ - required int port, - dynamic address, - int backlog = 0, - bool v6Only = false, - bool shared = false, - }) async { - if (factory != null) { - logger.warning( - 'HTTP Server already initialized, you should should use `app.listen()`', - ); - - server = await factory!(this); - locals[#spry.server.initialized] = true; - configurationPoweredBy(); - - return listen(); - } else if (locals[#spry.server.initialized] == true) { - logger.warning( - 'HTTP Server already initialized, you should should use `app.listen()`', - ); - return listen(); - } - - server = await HttpServer.bind( - address ?? InternetAddress.loopbackIPv4, port, - backlog: backlog, v6Only: v6Only, shared: shared); - locals[#spry.server.initialized] = true; - configurationPoweredBy(); - - return listen(); - } -} diff --git a/packages/spry/lib/src/application.dart b/packages/spry/lib/src/application.dart deleted file mode 100644 index 4408e4d..0000000 --- a/packages/spry/lib/src/application.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:logging/logging.dart'; - -import '_internal/application+factory.dart'; -import '_internal/map+value_of.dart'; -import 'routing/route.dart'; -import 'routing/routes.dart'; -import 'routing/routes_builder.dart'; - -class Application implements RoutesBuilder { - /// Spry application version. - static const version = '3.2.2'; - - /// Returns application binded server. - late final HttpServer server; - - /// Returns application locals. - late final Map locals; - - /// Creates a new Spry application with the given [server]. - /// - /// If [locals] is provided, it will be created a new empty map. - /// - /// ```dart - /// main() async { - /// final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 8080); - /// final app = Application(server); - /// } - Application(this.server, {Map? locals}) { - this.locals = locals ?? {}; - this.locals[#spry.server.initialized] = true; - - poweredBy = 'Spry/$version'; - } - - /// Creates a new Spry application with late initialization of [server]. - /// - /// ```dart - /// final app = Application.late(); - /// - /// main() async { - /// ... - /// await app.run(port: 3000); - /// } - /// ``` - /// - /// **NOTE**: This contructor will create simple http server, You must use - /// `app.run()` to start the server. - Application.late([Map? locals]) { - this.locals = locals ?? {}; - this.locals[#spry.server.initialized] = false; - } - - /// Creates a new Spry application with the given [factory]. - Application.factory(ServerFactory factory) { - locals = {}; - locals[#spry.server.initialized] = false; - locals[#spry.server.factory] = factory; - } - - /// Simple create application factory. - static Future create({ - required int port, - dynamic address, - int backlog = 0, - bool v6Only = false, - bool shared = false, - }) async { - final server = await HttpServer.bind( - address ?? InternetAddress.loopbackIPv4, port, - backlog: backlog, v6Only: v6Only, shared: shared); - return Application(server); - } - - /// Returns spry application logger. - Logger get logger { - return locals.valueOf(#spry.logger, (_) { - return locals[#spry.logger] = Logger('spry'); - }); - } - - /// Returns spry application routes. - Routes get routes { - return locals.valueOf(#spry.routes, (_) { - return locals[#spry.routes] = Routes(); - }); - } - - /// Returns spry application default `x-powered-by` header value. - String? get poweredBy => locals[#spry.powered_by]?.toString(); - - /// Sets spry application default `x-powered-by` header value. - set poweredBy(String? value) { - locals[#spry.powered_by] = value; - if (locals[#spry.server.initialized] == true) { - if (value == null) { - return server.defaultResponseHeaders.removeAll('x-powered-by'); - } - - server.defaultResponseHeaders.set('x-powered-by', value); - } - } - - @override - void addRoute(Route route) => routes.addRoute(route); -} diff --git a/packages/spry/lib/src/exception/abort.dart b/packages/spry/lib/src/exception/abort.dart deleted file mode 100644 index de9ad2c..0000000 --- a/packages/spry/lib/src/exception/abort.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'dart:io'; - -import 'package:webfetch/webfetch.dart'; - -import 'abort_exception.dart'; - -class Abort implements AbortException { - @override - late final String message; - - @override - final int status; - - /// Creates a new http status abort exception - /// - /// - [status] is the http status code. - /// - [message] is the message describing the abort. - /// - /// ```dart - /// throw Abort(HttpStatus.notFound, message: 'Page not found'); - /// ``` - Abort(this.status, {String? message}) { - this.message = message ?? status.httpReasonPhrase; - } - - /// Creates a new redirecting exception. - /// - /// ``` - /// throw Abort.redirect('/login'); - /// ``` - static Never redirect( - String location, { - int status = HttpStatus.movedTemporarily, - String? message, - }) { - final info = _RedirectInfo( - method: 'GET', - statusCode: status, - location: Uri.parse(location), - ); - - throw RedirectException(message ?? status.httpReasonPhrase, [info]); - } -} - -class _RedirectInfo implements RedirectInfo { - @override - final Uri location; - - @override - final String method; - - @override - final int statusCode; - - const _RedirectInfo({ - required this.location, - required this.statusCode, - required this.method, - }); -} diff --git a/packages/spry/lib/src/exception/abort_exception.dart b/packages/spry/lib/src/exception/abort_exception.dart deleted file mode 100644 index e7d8255..0000000 --- a/packages/spry/lib/src/exception/abort_exception.dart +++ /dev/null @@ -1,7 +0,0 @@ -abstract interface class AbortException implements Exception { - /// The message describing the abort. - String get message; - - /// The status code of the abort. - int get status; -} diff --git a/packages/spry/lib/src/exception/application+exceptions.dart b/packages/spry/lib/src/exception/application+exceptions.dart deleted file mode 100644 index 1943119..0000000 --- a/packages/spry/lib/src/exception/application+exceptions.dart +++ /dev/null @@ -1,14 +0,0 @@ -// ignore_for_file: file_names - -import '../_internal/map+value_of.dart'; -import '../application.dart'; -import 'exceptions.dart'; - -extension Application$Exceptions on Application { - /// Returns spry application exceptions. - Exceptions get exceptions { - return locals.valueOf(#spry.exceptions, (_) { - return locals[#spry.exceptions] = Exceptions(this); - }); - } -} diff --git a/packages/spry/lib/src/exception/exception_filter.dart b/packages/spry/lib/src/exception/exception_filter.dart deleted file mode 100644 index d9575fd..0000000 --- a/packages/spry/lib/src/exception/exception_filter.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'dart:io'; - -import 'exception_source.dart'; - -abstract interface class ExceptionFilter { - Future process(ExceptionSource source, HttpRequest request); -} diff --git a/packages/spry/lib/src/exception/exception_source.dart b/packages/spry/lib/src/exception/exception_source.dart deleted file mode 100644 index 2bb2154..0000000 --- a/packages/spry/lib/src/exception/exception_source.dart +++ /dev/null @@ -1,24 +0,0 @@ -class ExceptionSource { - final T exception; - final StackTrace stackTrace; - - final bool Function() _responseClosedFactory; - - const ExceptionSource({ - required this.exception, - required this.stackTrace, - required bool Function() responseClosedFactory, - }) : _responseClosedFactory = responseClosedFactory; - - /// Returns `true` if the response is closed. - bool get isResponseClosed => _responseClosedFactory(); - - /// Creates a new [ExceptionSource] with the given [exception]. - ExceptionSource cast(R exception) { - return ExceptionSource( - exception: exception, - stackTrace: stackTrace, - responseClosedFactory: _responseClosedFactory, - ); - } -} diff --git a/packages/spry/lib/src/exception/exceptions.dart b/packages/spry/lib/src/exception/exceptions.dart deleted file mode 100644 index 70536cd..0000000 --- a/packages/spry/lib/src/exception/exceptions.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'dart:io'; - -import '../_internal/iterable_utils.dart'; -import '../application.dart'; -import 'abort.dart'; -import 'abort_exception.dart'; -import 'exception_filter.dart'; -import 'exception_source.dart'; -import 'exceptions_builder.dart'; -import 'returow_exception.dart'; - -class Exceptions extends Iterable - implements ExceptionsBuilder, ExceptionFilter { - Exceptions(Application application) : _application = application; - - /// Inner list of filters. - final _filters = []; - - final Application _application; - - @override - void addFilter(ExceptionFilter filter) => _filters.add(filter); - - @override - Iterator get iterator => _filters.iterator; - - @override - Future process(ExceptionSource source, HttpRequest request) async { - return await handle(this, source, request); - } -} - -extension on Exceptions { - Future handle(Iterable filters, ExceptionSource source, - HttpRequest request) async { - final filter = - filters.firstWhereOrNull((element) => element.matches(source)); - if (filter == null) { - return defaultProcess(source, request); - } - - try { - return await filter.process(source, request); - } on RethrowException { - final withoutFilters = filters.skipWhile((element) => element == filter); - - return await handle(withoutFilters, source, request); - } catch (error, stackTrace) { - _application.logger.severe( - 'Exception thrown while processing request', error, stackTrace); - - return await defaultProcess(source, request); - } - } - - Future defaultProcess( - ExceptionSource source, - HttpRequest request, - ) { - return switch (source.exception) { - RedirectException e => handleRedirect(source.cast(e), request), - AbortException e => handleAbort(source.cast(e), request), - Error e => handleAbort( - source.cast(Abort(HttpStatus.internalServerError, - message: Error.safeToString(e))), - request), - Exception e => handleAbort( - source.cast( - Abort(HttpStatus.internalServerError, message: e.toString())), - request), - _ => handleAbort( - source.cast(Abort(HttpStatus.internalServerError)), - request, - ), - }; - } - - Future handleRedirect( - ExceptionSource source, - HttpRequest request, - ) async { - if (source.exception.redirects.isEmpty) { - return process( - source.cast(Abort(HttpStatus.internalServerError)), - request, - ); - } - - final response = request.response; - final info = source.exception.redirects.last; - - await response.redirect(info.location, status: info.statusCode); - } - - Future handleAbort( - ExceptionSource source, - HttpRequest request, - ) async { - final response = request.response; - if (source.isResponseClosed) return; - - response.statusCode = source.exception.status; - response.write(source.exception.message); - } -} - -extension on ExceptionFilter { - /// Matches the exception type. - bool matches(ExceptionSource source) => source.exception is T; -} diff --git a/packages/spry/lib/src/exception/exceptions_builder.dart b/packages/spry/lib/src/exception/exceptions_builder.dart deleted file mode 100644 index aacada0..0000000 --- a/packages/spry/lib/src/exception/exceptions_builder.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'exception_filter.dart'; - -abstract interface class ExceptionsBuilder { - /// Adds a new exception filter to the list of filters. - void addFilter(ExceptionFilter filter); -} diff --git a/packages/spry/lib/src/exception/returow_exception.dart b/packages/spry/lib/src/exception/returow_exception.dart deleted file mode 100644 index 1757fd4..0000000 --- a/packages/spry/lib/src/exception/returow_exception.dart +++ /dev/null @@ -1,3 +0,0 @@ -class RethrowException implements Exception { - const RethrowException(); -} diff --git a/packages/spry/lib/src/handler/closure_handler.dart b/packages/spry/lib/src/handler/closure_handler.dart deleted file mode 100644 index 309ebb6..0000000 --- a/packages/spry/lib/src/handler/closure_handler.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'handler.dart'; - -class ClosureHandler implements Handler { - final FutureOr Function(HttpRequest) _closure; - - const ClosureHandler(FutureOr Function(HttpRequest request) closure) - : _closure = closure; - - @override - FutureOr handle(HttpRequest request) => _closure(request); -} - -extension FutureOrClosure$MakeHandler on FutureOr Function(HttpRequest) { - /// Returns a [Handler] that wraps the this. - Handler makeHandler() => ClosureHandler(this); -} diff --git a/packages/spry/lib/src/handler/handler.dart b/packages/spry/lib/src/handler/handler.dart deleted file mode 100644 index c5522e2..0000000 --- a/packages/spry/lib/src/handler/handler.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -abstract interface class Handler { - FutureOr handle(HttpRequest request); -} diff --git a/packages/spry/lib/src/middleware/application+middleware.dart b/packages/spry/lib/src/middleware/application+middleware.dart deleted file mode 100644 index d413966..0000000 --- a/packages/spry/lib/src/middleware/application+middleware.dart +++ /dev/null @@ -1,19 +0,0 @@ -// ignore_for_file: file_names - -import '../_internal/map+value_of.dart'; -import '../application.dart'; -import 'middleware_stack.dart'; - -extension Application$Middleware on Application { - static const _key = #spry.middleware.stack; - - /// Returns current application global middleware stack. - MiddlewareStack get middleware { - return locals.valueOf(_key, (_) { - final stack = MiddlewareStack(this); - logger.config('Created middleware stack'); - - return locals[_key] = stack; - }); - } -} diff --git a/packages/spry/lib/src/middleware/middleware+handler.dart b/packages/spry/lib/src/middleware/middleware+handler.dart deleted file mode 100644 index 73e0d0e..0000000 --- a/packages/spry/lib/src/middleware/middleware+handler.dart +++ /dev/null @@ -1,40 +0,0 @@ -// ignore_for_file: file_names - -import 'dart:async'; -import 'dart:io'; - -import '../handler/handler.dart'; -import 'middleware.dart'; - -extension Middleware$Handler on Iterable { - /// Returns a [Handler] that wraps the [handler] with the [Middleware] stack. - Handler makeHandler(Handler handler) => - _MiddlewareHandler(handler, this); -} - -class _MiddlewareHandler implements Handler { - final Handler handler; - final Iterable middleware; - - const _MiddlewareHandler(this.handler, this.middleware); - - @override - Future handle(HttpRequest request) async { - // Create a completer that will complete when the handler completes. - final completer = Completer.sync(); - - // Create a next function that will process the next middleware. - final next = middleware.reversed.fold( - () async => completer.complete(await handler.handle(request)), - (next, middleware) => () => middleware.process(request, next), - ); - - // Process the middleware stack and return the future. - return next().then((_) => completer.future); - } -} - -extension on Iterable { - /// Reverse the order of the elements in this iterable. - Iterable get reversed => toList(growable: false).reversed; -} diff --git a/packages/spry/lib/src/middleware/middleware.dart b/packages/spry/lib/src/middleware/middleware.dart deleted file mode 100644 index 83d85bc..0000000 --- a/packages/spry/lib/src/middleware/middleware.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'dart:io'; - -/// Middleware next callback function. -typedef Next = Future Function(); - -abstract interface class Middleware { - /// Processes the [request] and calls the [next] middleware. - Future process(HttpRequest request, Next next); -} diff --git a/packages/spry/lib/src/middleware/middleware_stack.dart b/packages/spry/lib/src/middleware/middleware_stack.dart deleted file mode 100644 index 7bf2cf3..0000000 --- a/packages/spry/lib/src/middleware/middleware_stack.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import '../application.dart'; -import 'middleware.dart'; - -class MiddlewareStack extends Iterable { - final Application _application; - - MiddlewareStack(Application application) : _application = application; - - /// Internal middleware stack. - final _stack = []; - - @override - Iterator get iterator => _stack.iterator; - - /// Adds a [Middleware] to the stack. - /// - /// If [prepend] is `true`, the [middleware] will be added to the beginning of - /// the stack. - void use(Middleware middleware, {bool prepend = false}) { - if (prepend) { - _application.logger.config('Prepending middleware: $middleware'); - - return _stack.insert(0, middleware); - } - - _application.logger.config('Appending middleware: $middleware'); - _stack.add(middleware); - } - - /// Adds a closure style [Middleware] to the stack. - void closure( - FutureOr Function(HttpRequest request, Next next) process, { - bool prepend = false, - }) { - return use(_ClosureMifddleware(process), prepend: prepend); - } -} - -class _ClosureMifddleware implements Middleware { - final FutureOr Function(HttpRequest request, Next next) closure; - - const _ClosureMifddleware(this.closure); - - @override - Future process(HttpRequest request, Next next) async => - closure(request, next); -} diff --git a/packages/spry/lib/src/request/request+application.dart b/packages/spry/lib/src/request/request+application.dart deleted file mode 100644 index ad93f0c..0000000 --- a/packages/spry/lib/src/request/request+application.dart +++ /dev/null @@ -1,11 +0,0 @@ -// ignore_for_file: file_names - -import 'dart:io'; - -import '../_internal/request.dart'; -import '../application.dart'; - -extension Request$Application on HttpRequest { - /// The [Application] instance that is handling this request. - Application get application => SpryRequest.of(this).application; -} diff --git a/packages/spry/lib/src/request/request+clone.dart b/packages/spry/lib/src/request/request+clone.dart deleted file mode 100644 index 3cd9894..0000000 --- a/packages/spry/lib/src/request/request+clone.dart +++ /dev/null @@ -1,10 +0,0 @@ -// ignore_for_file: file_names - -import 'dart:io'; - -import '../_internal/request.dart'; -import '../_internal/request+clone.dart'; - -extension Request$Clone on HttpRequest { - HttpRequest clone() => SpryRequest.of(this).clone(); -} diff --git a/packages/spry/lib/src/request/request+fetch.dart b/packages/spry/lib/src/request/request+fetch.dart deleted file mode 100644 index 7470887..0000000 --- a/packages/spry/lib/src/request/request+fetch.dart +++ /dev/null @@ -1,102 +0,0 @@ -// ignore_for_file: file_names - -import 'dart:async'; -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:webfetch/webfetch.dart' hide fetch; -import 'package:webfetch/webfetch.dart' as webfetch show fetch, Client; - -import '../_internal/map+value_of.dart'; -import 'request+locals.dart'; - -extension Request$Fetch on HttpRequest { - Fetch get fetch => webfetch.fetch.use(client); -} - -extension on HttpRequest { - static const key = #spry.request.fetch.client; - - webfetch.Client get client { - return locals.valueOf(key, (_) { - return locals[key] = FetchClient(requestedUri); - }); - } -} - -class FetchClient implements webfetch.Client { - final Uri base; - - const FetchClient(this.base); - - @override - Future send(Request request, {bool keepalive = false}) { - return webfetch.fetch( - SpryFetchRequest(request, URL(request.url, base).toString()), - keepalive: keepalive, - ); - } -} - -class SpryFetchRequest implements Request { - const SpryFetchRequest(this.request, this.url); - - final Request request; - - @override - final String url; - - @override - Future arrayBuffer() => request.arrayBuffer(); - - @override - Future blob() => request.blob(); - - @override - Stream? get body => request.body; - - @override - bool get bodyUsed => request.bodyUsed; - - @override - RequestCache get cache => request.cache; - - @override - Request clone() => SpryFetchRequest(request.clone(), url); - - @override - RequestCredentials get credentials => request.credentials; - - @override - RequestDestination get destination => request.destination; - - @override - Future formData() => request.formData(); - - @override - Headers get headers => request.headers; - - @override - String get integrity => request.integrity; - - @override - Future json() => request.json(); - - @override - String get method => request.method; - - @override - RequestMode get mode => request.mode; - - @override - RequestRedirect get redirect => request.redirect; - - @override - String get referrer => request.referrer; - - @override - ReferrerPolicy get referrerPolicy => request.referrerPolicy; - - @override - Future text() => request.text(); -} diff --git a/packages/spry/lib/src/request/request+formdata.dart b/packages/spry/lib/src/request/request+formdata.dart deleted file mode 100644 index cc5d026..0000000 --- a/packages/spry/lib/src/request/request+formdata.dart +++ /dev/null @@ -1,49 +0,0 @@ -// ignore_for_file: file_names - -import 'dart:convert'; -import 'dart:io'; - -import 'package:webfetch/webfetch.dart' show FormData, URLSearchParams; - -import 'request+locals.dart'; - -export 'package:webfetch/webfetch.dart' show FormData, Blob; - -extension Request$FormData on HttpRequest { - static const _key = #spry.request.formdata; - - /// Returns the [FormData] from the request. - Future formData() async { - final existing = locals[_key]; - if (existing is FormData) return existing; - - // If current is urlencoded - if (headers.contentType?.mimeType.toLowerCase() == - 'application/x-www-form-urlencoded') { - return urlencodedFormData(); - } - - final parameters = headers.contentType?.parameters - .map((key, value) => MapEntry(key.toLowerCase(), value)); - final boundary = parameters?['boundary']; - if (boundary != null) { - return locals[_key] = await FormData.decode(this, boundary); - } - - /// Creates and returns an empty [FormData]. - return locals[_key] = FormData(); - } -} - -extension on HttpRequest { - Future urlencodedFormData() async { - final formData = FormData(); - final value = await utf8.decodeStream(this); - final params = URLSearchParams(value); - for (final (name, value) in params.entries()) { - formData.append(name, value); - } - - return locals[Request$FormData._key] = formData; - } -} diff --git a/packages/spry/lib/src/request/request+json.dart b/packages/spry/lib/src/request/request+json.dart deleted file mode 100644 index 4820e94..0000000 --- a/packages/spry/lib/src/request/request+json.dart +++ /dev/null @@ -1,40 +0,0 @@ -// ignore_for_file: file_names - -import 'dart:io'; -import 'dart:convert' as convert; - -import 'package:spry/src/_internal/map+value_of.dart'; - -import 'request+application.dart'; -import 'request+locals.dart'; -import 'request+text.dart'; - -extension Request$Json on HttpRequest { - static const _key = #spry.request.cached.json; - static const _lock = #spry.request.cached.json.lock; - static const _codecKey = #spry.json.codec; - - /// Returns the request body as a JSON object. - Future json() async { - final existing = locals[_key]; - if (locals[_lock] == true) return existing; - - // Try decode the request body as a JSON object. - try { - return locals[_key] = jsonCodec.decode(await text()); - } catch (_) { - return locals[_key] = null; - } finally { - locals[_lock] = true; - } - } - - /// Returns JSON codec. - convert.JsonCodec get jsonCodec { - return locals.valueOf(_codecKey, (_) { - return application.locals.valueOf(_codecKey, (_) { - return convert.JsonCodec(); - }); - }); - } -} diff --git a/packages/spry/lib/src/request/request+locals.dart b/packages/spry/lib/src/request/request+locals.dart deleted file mode 100644 index b18e3bb..0000000 --- a/packages/spry/lib/src/request/request+locals.dart +++ /dev/null @@ -1,10 +0,0 @@ -// ignore_for_file: file_names - -import 'dart:io'; - -import '../_internal/request.dart'; - -extension Request$Locals on HttpRequest { - /// The local storage for this request. - Map get locals => SpryRequest.of(this).locals; -} diff --git a/packages/spry/lib/src/request/request+params.dart b/packages/spry/lib/src/request/request+params.dart deleted file mode 100644 index 403d3b7..0000000 --- a/packages/spry/lib/src/request/request+params.dart +++ /dev/null @@ -1,17 +0,0 @@ -// ignore_for_file: file_names - -import 'dart:io'; - -import 'package:routingkit/routingkit.dart'; - -import '../_internal/map+value_of.dart'; -import 'request+locals.dart'; - -extension Request$Params on HttpRequest { - /// Returns current request parameters. - Params get params { - return locals.valueOf(#spry.request.params, (_) { - return locals[#spry.request.params] = Params(); - }); - } -} diff --git a/packages/spry/lib/src/request/request+route.dart b/packages/spry/lib/src/request/request+route.dart deleted file mode 100644 index caad0a7..0000000 --- a/packages/spry/lib/src/request/request+route.dart +++ /dev/null @@ -1,12 +0,0 @@ -// ignore_for_file: file_names - -import 'dart:io'; - -import '../_internal/map+value_of.dart'; -import '../routing/route.dart'; -import 'request+locals.dart'; - -extension Request$Route on HttpRequest { - /// Returns current request route. - Route? get route => locals.valueOf(#spry.request.route, (_) => null); -} diff --git a/packages/spry/lib/src/request/request+search_params.dart b/packages/spry/lib/src/request/request+search_params.dart deleted file mode 100644 index c05c415..0000000 --- a/packages/spry/lib/src/request/request+search_params.dart +++ /dev/null @@ -1,21 +0,0 @@ -// ignore_for_file: file_names - -import 'dart:io'; - -import 'package:webfetch/webfetch.dart' show URLSearchParams; - -import '../_internal/map+value_of.dart'; -import 'request+locals.dart'; - -export 'package:webfetch/webfetch.dart' show URLSearchParams; - -extension Request$SearchParams on HttpRequest { - static const _key = #spry.request.searchParams; - - /// Returns the [URLSearchParams] from the request. - URLSearchParams get searchParams { - return locals.valueOf(_key, (_) { - return locals[_key] = URLSearchParams(uri.queryParametersAll); - }); - } -} diff --git a/packages/spry/lib/src/request/request+text.dart b/packages/spry/lib/src/request/request+text.dart deleted file mode 100644 index c3e1dc9..0000000 --- a/packages/spry/lib/src/request/request+text.dart +++ /dev/null @@ -1,40 +0,0 @@ -// ignore_for_file: file_names - -import 'dart:convert'; -import 'dart:io'; - -import '../application+encoding.dart'; -import 'request+application.dart'; -import 'request+locals.dart'; - -extension Request$Text on HttpRequest { - static const _key = #spry.request.cached.text; - - /// Returns the request body as a string. - Future text() async { - final existing = locals[_key]; - if (existing != null) return existing; - - // Try decode the request body as a string. - try { - return locals[_key] = await encoding.decodeStream(this); - } catch (_) { - return locals[_key] = ''; - } - } -} - -extension on HttpRequest { - Encoding get encoding { - // Find the encoding from the content-type header. - final contentType = headers.contentType; - if (contentType != null) { - final charset = contentType.parameters['charset']; - if (charset != null) { - return Encoding.getByName(charset) ?? application.encoding; - } - } - - return application.encoding; - } -} diff --git a/packages/spry/lib/src/response/response+is_closed.dart b/packages/spry/lib/src/response/response+is_closed.dart deleted file mode 100644 index d7f0035..0000000 --- a/packages/spry/lib/src/response/response+is_closed.dart +++ /dev/null @@ -1,9 +0,0 @@ -// ignore_for_file: file_names - -import 'dart:io'; - -import '../_internal/response.dart'; - -extension Response$IsClosed on HttpResponse { - bool get isClosed => (this as SpryResponse).isClosed; -} diff --git a/packages/spry/lib/src/response/responsible.dart b/packages/spry/lib/src/response/responsible.dart deleted file mode 100644 index e73745b..0000000 --- a/packages/spry/lib/src/response/responsible.dart +++ /dev/null @@ -1,210 +0,0 @@ -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:path/path.dart' show basename; -import 'package:webfetch/webfetch.dart' as webfetch; - -// ignore: implementation_imports -import 'package:webfetch/src/_internal/generate_boundary.dart' - show generateBoundary; - -import '../application+encoding.dart'; -import '../request/request+application.dart'; -import '../request/request+json.dart'; - -abstract interface class Responsible { - /// Responds to the [request]. - Future respond(HttpRequest request); - - /// Creates a [Responsible] from [object?]. - /// - /// ```dart - /// final respoinsible = Responsible.create(object); - /// ``` - /// - /// ## Supported types - /// - [Responsible] - /// - [Map] - /// - [Iterable] - /// - [Stream>] - /// - [webfetch.FormData] - /// - [webfetch.Response] - /// - [File] - /// - [null] - /// - Dart scalar types, Eg: `String`, `int`, `double`, `bool`... - factory Responsible.create(Object? object) { - return switch (object) { - webfetch.Blob blob => _StreamResponsible(blob.stream()), - webfetch.FormData formData => _FormDataResponsible(formData), - webfetch.Response response => _WebfetchResponseResponsible(response), - Responsible responsible => responsible, - HttpClientResponse response => _HttpClientResponsesible(response), - HttpResponse _ => const _NullResponsible(), - Map map => _JsonResponsible(map), - Iterable iterable => _JsonResponsible(iterable.toList(growable: false)), - Stream> stream => _StreamResponsible(stream), - TypedData typedData => _TypedDataResponsible(typedData), - File file => _FileResponsible(file), - null => const _NullResponsible(), - _ => _NominalResponsible(object), - }; - } -} - -class _TypedDataResponsible implements Responsible { - final TypedData typedData; - - const _TypedDataResponsible(this.typedData); - - @override - Future respond(HttpRequest request) async { - final response = request.response; - final encoding = request.application.encoding; - final bytes = typedData.buffer.asUint8List(); - - response.headers.contentType = response.headers.contentType = - ContentType('application', 'octet-stream', charset: encoding.name); - response.add(bytes); - } -} - -class _HttpClientResponsesible implements Responsible { - final HttpClientResponse response; - - const _HttpClientResponsesible(this.response); - - @override - Future respond(HttpRequest request) async { - final innerResponse = request.response; - - innerResponse.statusCode = response.statusCode; - innerResponse.contentLength = response.contentLength; - innerResponse.cookies.addAll(response.cookies); - response.headers.forEach((name, values) { - for (final value in values) { - innerResponse.headers.add(name, value); - } - }); - - await for (final chunk in response) { - innerResponse.add(chunk); - } - } -} - -class _NullResponsible implements Responsible { - const _NullResponsible(); - - @override - Future respond(HttpRequest request) async {} -} - -class _NominalResponsible implements Responsible { - final Object? object; - - const _NominalResponsible(this.object); - - @override - Future respond(HttpRequest request) async { - request.response.write(object); - } -} - -class _FileResponsible implements Responsible { - final File file; - - const _FileResponsible(this.file); - - @override - Future respond(HttpRequest request) async { - final response = request.response; - - // Set download headers - response.headers.contentType = ContentType.binary; - response.headers.add( - 'Content-Disposition', - 'attachment; filename="${Uri.encodeComponent(basename(file.path))}"', - ); - - await response.addStream(file.openRead()); - } -} - -class _WebfetchResponseResponsible implements Responsible { - final webfetch.Response response; - - const _WebfetchResponseResponsible(this.response); - - @override - Future respond(HttpRequest request) async { - for (final (name, value) in response.headers.entries()) { - request.response.headers.add(name, value); - } - for (final cookie in response.headers.getSetCookie()) { - request.response.cookies.add(Cookie.fromSetCookieValue(cookie)); - } - - final stream = response.body; - if (stream != null) { - await request.response.addStream(stream); - } - } -} - -class _FormDataResponsible implements Responsible { - final webfetch.FormData formData; - - const _FormDataResponsible(this.formData); - - @override - Future respond(HttpRequest request) async { - final response = request.response; - final boundary = this.boundary; - - final stream = webfetch.FormData.encode(formData, boundary); - response.headers.contentType = ContentType( - 'multipart', - 'form-data', - parameters: { - 'boundary': boundary, - }, - charset: request.application.encoding.name, - ); - - await response.addStream(stream); - } - - String get boundary { - final value = generateBoundary(); - return '--spry--${value.substring(value.length - 60)}'; - } -} - -class _StreamResponsible implements Responsible { - final Stream> stream; - - const _StreamResponsible(this.stream); - - @override - Future respond(HttpRequest request) async { - await request.response.addStream(stream); - } -} - -class _JsonResponsible implements Responsible { - final Object object; - - const _JsonResponsible(this.object); - - @override - Future respond(HttpRequest request) async { - final response = request.response; - final encoding = request.application.encoding; - final value = request.jsonCodec.encode(object); - final bytes = encoding.encode(value); - - response.headers.contentType = response.headers.contentType = - ContentType('application', 'json', charset: encoding.name); - response.add(bytes); - } -} diff --git a/packages/spry/lib/src/routing/route.dart b/packages/spry/lib/src/routing/route.dart deleted file mode 100644 index 4f09fba..0000000 --- a/packages/spry/lib/src/routing/route.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:routingkit/routingkit.dart'; - -import '../handler/handler.dart'; - -class Route { - final String method; - final Iterable segments; - final Handler handler; - - const Route({ - required this.method, - required this.segments, - required this.handler, - }); - - Route copyWith({ - String? method, - Iterable? path, - Handler? handler, - }) { - return Route( - method: method ?? this.method, - segments: path ?? this.segments, - handler: handler ?? this.handler, - ); - } -} diff --git a/packages/spry/lib/src/routing/route_collection.dart b/packages/spry/lib/src/routing/route_collection.dart deleted file mode 100644 index 2eab349..0000000 --- a/packages/spry/lib/src/routing/route_collection.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'routes_builder.dart'; - -abstract interface class RouteCollection { - /// Initialize the [RouteCollection]. - void boot(RoutesBuilder routes); -} - -extension RouteCollectionRegister on RoutesBuilder { - /// Regsiter a [RouteCollection] to the incoming router. - void register(RouteCollection collection) => collection.boot(this); -} diff --git a/packages/spry/lib/src/routing/routes.dart b/packages/spry/lib/src/routing/routes.dart deleted file mode 100644 index 80c80e1..0000000 --- a/packages/spry/lib/src/routing/routes.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'route.dart'; -import 'routes_builder.dart'; - -class Routes extends Iterable implements RoutesBuilder { - Routes(); - - /// Inner list of routes. - final _inner = []; - - /// Sets or returns case sensitive mode. - bool caseSensitive = false; - - @override - void addRoute(Route route) => _inner.add(route); - - @override - Iterator get iterator => _inner.iterator; -} diff --git a/packages/spry/lib/src/routing/routes_builder+closure.dart b/packages/spry/lib/src/routing/routes_builder+closure.dart deleted file mode 100644 index bdecbe1..0000000 --- a/packages/spry/lib/src/routing/routes_builder+closure.dart +++ /dev/null @@ -1,36 +0,0 @@ -// ignore_for_file: file_names - -import 'dart:async'; -import 'dart:io'; - -import 'package:routingkit/routingkit.dart'; - -import '../handler/closure_handler.dart'; -import 'route.dart'; -import 'routes_builder.dart'; - -extension RoutesBuilder$Closure on RoutesBuilder { - /// Registers a route that responds to the given [method] and [path] with the - /// result of the [closure]. - /// - /// ```dart - /// app.on( - /// (request) async => 'Hello, world!', - /// method: 'GET', - /// path: '/say-hello', - /// ); - /// ``` - void on( - FutureOr Function(HttpRequest request) closure, { - required String method, - required String path, - }) { - final route = Route( - handler: closure.makeHandler(), - method: method, - segments: path.asSegments, - ); - - return addRoute(route); - } -} diff --git a/packages/spry/lib/src/routing/routes_builder+group.dart b/packages/spry/lib/src/routing/routes_builder+group.dart deleted file mode 100644 index fa8d8e8..0000000 --- a/packages/spry/lib/src/routing/routes_builder+group.dart +++ /dev/null @@ -1,82 +0,0 @@ -// ignore_for_file: file_names - -import 'package:routingkit/routingkit.dart'; - -import '../middleware/middleware.dart'; -import '../middleware/middleware+handler.dart'; -import 'route.dart'; -import 'routes_builder.dart'; - -extension RoutesBuilder$Group on RoutesBuilder { - /// Creates and returns a new [RoutesBuilder] wrapped in the supplied array of - /// [Middleware] and paths. - /// - /// ```dart - /// final routes = app.groupd(path: '/api', middleware: [authMiddleware]); - /// - /// routes.get('/users', (req) => 'users'); - /// routes.get('/posts', (req) => 'posts'); - /// - /// // GET /api/users -> users - /// // GET /api/posts -> posts - /// ``` - /// - /// - [middleware] - The middleware to wrap the [RoutesBuilder] in. - /// - [path] - The path to wrap the [RoutesBuilder] in. - RoutesBuilder groupd({Iterable? middleware, String? path}) { - RoutesBuilder current = this; - - if (path != null && path.isNotEmpty) { - current = _PathGroupedRoutesBuilder(current, path.asSegments); - } - - if (middleware != null && middleware.isNotEmpty) { - current = _MiddlewareGroupedRoutesBuilder(current, middleware); - } - - return current; - } - - /// Creates and returns a new [RoutesBuilder] wrapped in the supplied array of - /// [Middleware] with closure containing routes. - /// - /// @see [groupd] - RoutesBuilder group( - void Function(RoutesBuilder routes) closure, { - Iterable? middleware, - String? path, - }) { - final routes = groupd(middleware: middleware, path: path); - closure(routes); - - return routes; - } -} - -class _PathGroupedRoutesBuilder implements RoutesBuilder { - final RoutesBuilder parent; - final Iterable segments; - - const _PathGroupedRoutesBuilder(this.parent, this.segments); - - @override - void addRoute(Route route) { - return parent.addRoute(route.copyWith( - path: [...segments, ...route.segments], - )); - } -} - -class _MiddlewareGroupedRoutesBuilder implements RoutesBuilder { - final RoutesBuilder parent; - final Iterable middleware; - - const _MiddlewareGroupedRoutesBuilder(this.parent, this.middleware); - - @override - void addRoute(Route route) { - return parent.addRoute(route.copyWith( - handler: middleware.makeHandler(route.handler), - )); - } -} diff --git a/packages/spry/lib/src/routing/routes_builder+methods.dart b/packages/spry/lib/src/routing/routes_builder+methods.dart deleted file mode 100644 index 043e027..0000000 --- a/packages/spry/lib/src/routing/routes_builder+methods.dart +++ /dev/null @@ -1,49 +0,0 @@ -// ignore_for_file: file_names - -import 'dart:async'; -import 'dart:io'; - -import 'routes_builder.dart'; -import 'routes_builder+closure.dart'; - -extension RoutesBuilder$Methods on RoutesBuilder { - /// Registers a `GET` route that responds to with the result of the [closure]. - /// - /// ```dart - /// app.get('/say-hello', (request) async => 'Hello, world!'); - /// ``` - void get(String path, FutureOr Function(HttpRequest request) closure) => - on(closure, method: 'GET', path: path); - - /// Registers a `POST` route that responds to with the result of the - /// [closure]. - /// - /// ```dart - /// app.post('/say/:name', (request) async => request.params.get('name')); - /// ``` - void post( - String path, FutureOr Function(HttpRequest request) closure) => - on(closure, method: 'POST', path: path); - - /// Registers a `PUT` route that responds to with the result of the [closure]. - void put(String path, FutureOr Function(HttpRequest request) closure) => - on(closure, method: 'PUT', path: path); - - /// Registers a `PATCH` route that responds to with the result of the - /// [closure]. - void patch( - String path, FutureOr Function(HttpRequest request) closure) => - on(closure, method: 'PATCH', path: path); - - /// Registers a `DELETE` route that responds to with the result of the - /// [closure]. - void delete( - String path, FutureOr Function(HttpRequest request) closure) => - on(closure, method: 'DELETE', path: path); - - /// Registers a `HEAD` route that responds to with the result of the - /// [closure]. - void head( - String path, FutureOr Function(HttpRequest request) closure) => - on(closure, method: 'HEAD', path: path); -} diff --git a/packages/spry/lib/src/routing/routes_builder+websocket.dart b/packages/spry/lib/src/routing/routes_builder+websocket.dart deleted file mode 100644 index 63a6006..0000000 --- a/packages/spry/lib/src/routing/routes_builder+websocket.dart +++ /dev/null @@ -1,79 +0,0 @@ -// ignore_for_file: file_names - -import 'dart:async'; -import 'dart:io'; - -import 'package:routingkit/routingkit.dart'; - -import '../exception/abort.dart'; -import '../handler/handler.dart'; -import 'route.dart'; -import 'routes_builder.dart'; - -extension RoutesBuilder$WebSocket on RoutesBuilder { - /// Adds a [WebSocket] route to to the routes. - /// - /// The [T] type argument is [fallback] return type. - /// - /// ```dart - /// app.ws('/ws', (websocket, request) { - /// websocket.listen((message) { - /// websocket.add(message); - /// }); - /// }); - /// ``` - /// - /// - [path] is the websocket listening path. - /// - [closure] is the websocket handler closure. - /// - [compression] is the compression options. default is [CompressionOptions.compressionDefault]. - /// - [protocolSelector] is the protocol selector. default is `null`. - /// - [fallback] is the fallback handler closure. default is `null`. - void ws( - String path, - FutureOr Function(WebSocket ws, HttpRequest request) closure, { - CompressionOptions compression = CompressionOptions.compressionDefault, - FutureOr Function(List protocols)? protocolSelector, - FutureOr Function(HttpRequest request)? fallback, - }) { - final handler = _WebSocketHandler( - closure: closure, fallback: fallback, compression: compression); - final route = - Route(handler: handler, segments: path.asSegments, method: 'GET'); - - return addRoute(route); - } -} - -class _WebSocketHandler implements Handler { - final FutureOr Function(HttpRequest request)? fallback; - final FutureOr Function(WebSocket websocket, HttpRequest request) - closure; - final FutureOr Function(List protocols)? protocolSelector; - final CompressionOptions compression; - - const _WebSocketHandler({ - required this.closure, - required this.compression, - this.fallback, - this.protocolSelector, - }); - - @override - Future handle(HttpRequest request) async { - if (!WebSocketTransformer.isUpgradeRequest(request)) { - return (fallback ?? defaultFallback).call(request); - } - - final websocket = await WebSocketTransformer.upgrade(request, - compression: compression, protocolSelector: protocolSelector); - await closure(websocket, request); - - // Await for websocket to close. - return websocket.done.then((_) => null); - } - - /// Default fallback handler closure. - Never defaultFallback(HttpRequest request) { - throw Abort(HttpStatus.upgradeRequired, message: 'Upgrade Required'); - } -} diff --git a/packages/spry/lib/src/routing/routes_builder.dart b/packages/spry/lib/src/routing/routes_builder.dart deleted file mode 100644 index 102a4d8..0000000 --- a/packages/spry/lib/src/routing/routes_builder.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'route.dart'; - -abstract interface class RoutesBuilder { - /// Adds a route to the routes. - void addRoute(Route route); -} diff --git a/packages/spry/pubspec.yaml b/packages/spry/pubspec.yaml deleted file mode 100644 index 4036ec6..0000000 --- a/packages/spry/pubspec.yaml +++ /dev/null @@ -1,18 +0,0 @@ -name: spry -description: Spry is an HTTP middleware framework for Dart to make web applications and APIs server more enjoyable to write. -version: 3.3.1 -homepage: https://spry.fun -repository: https://github.com/medz/spry - -environment: - sdk: ^3.2.0 - -dependencies: - logging: ^1.2.0 - path: ^1.9.0 - routingkit: ^0.2.0 - webfetch: ^0.0.17 - -dev_dependencies: - lints: ">=3.0.0 <5.0.0" - test: ^1.23.1 diff --git a/packages/spry/test/_internal/test_request.dart b/packages/spry/test/_internal/test_request.dart deleted file mode 100644 index 6dda258..0000000 --- a/packages/spry/test/_internal/test_request.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:typed_data'; - -class TestRequest extends Stream implements HttpRequest { - @override - X509Certificate? get certificate => throw UnimplementedError(); - - @override - HttpConnectionInfo? get connectionInfo => throw UnimplementedError(); - - @override - int get contentLength => throw UnimplementedError(); - - @override - List get cookies => throw UnimplementedError(); - - @override - HttpHeaders get headers => throw UnimplementedError(); - - @override - StreamSubscription listen(void Function(Uint8List event)? onData, - {Function? onError, void Function()? onDone, bool? cancelOnError}) { - throw UnimplementedError(); - } - - @override - String get method => throw UnimplementedError(); - - @override - bool get persistentConnection => throw UnimplementedError(); - - @override - String get protocolVersion => throw UnimplementedError(); - - @override - Uri get requestedUri => throw UnimplementedError(); - - @override - HttpResponse get response => throw UnimplementedError(); - - @override - HttpSession get session => throw UnimplementedError(); - - @override - Uri get uri => throw UnimplementedError(); -} diff --git a/packages/spry/test/exceptions/rethrow_exception_test.dart b/packages/spry/test/exceptions/rethrow_exception_test.dart deleted file mode 100644 index 7c05892..0000000 --- a/packages/spry/test/exceptions/rethrow_exception_test.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'dart:io'; - -import 'package:spry/spry.dart'; -import 'package:test/test.dart'; - -import '../_internal/test_request.dart'; - -class _Filter1 implements ExceptionFilter { - @override - Future process(ExceptionSource source, HttpRequest request) { - if (source.exception is _Exception) { - throw const RethrowException(); - } - - fail('Exception was not rethrown'); - } -} - -class _Filter2 implements ExceptionFilter<_Exception> { - @override - Future process( - ExceptionSource<_Exception> source, HttpRequest request) async {} -} - -class _Exception implements Exception {} - -void main() { - late final Application app; - late final HttpRequest request; - - setUp(() { - app = Application.late(); - request = TestRequest(); - - app.exceptions.addFilter(_Filter1()); - app.exceptions.addFilter(_Filter2()); - }); - - test('rethrow exception', () async { - final exception = _Exception(); - final source = ExceptionSource( - exception: exception, - stackTrace: StackTrace.empty, - responseClosedFactory: () => true, - ); - - await app.exceptions.process(source, request); - }); -} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..bad2ee9 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,16 @@ +name: spry +description: A starting point for Dart libraries or applications. +version: 1.0.0 +# repository: https://github.com/my_org/my_repo + +environment: + sdk: ^3.4.3 + +# Add regular dependencies here. +dependencies: + routingkit: ^1.0.0 + # path: ^1.8.0 + +dev_dependencies: + lints: ^3.0.0 + test: ^1.24.0 From a5e31d26eaf021c9208e6885dbc084b2bc219b59 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Thu, 4 Jul 2024 00:38:30 +0800 Subject: [PATCH 02/35] chore: WIP --- lib/src/_core_keys.dart | 1 + lib/src/app.dart | 2 + lib/src/composable/get_context.dart | 5 ++ lib/src/composable/get_raw_event.dart | 8 ++ lib/src/context+app.dart | 9 +++ lib/src/context.dart | 7 ++ lib/src/create_app.dart | 17 ---- lib/src/create_stack_handler.dart | 17 ---- lib/src/event.dart | 8 +- lib/src/routing/_routing_keys.dart | 3 + lib/src/routing/composable/get_params.dart | 11 +++ lib/src/routing/composable/route_utils.dart | 22 ++++++ lib/src/routing/route.dart | 3 + .../routing/{ => utils}/create_router.dart | 38 ++++++--- lib/src/utils/create_app.dart | 78 +++++++++++++++++++ lib/src/utils/create_event_context.dart | 56 +++++++++++++ pubspec.yaml | 2 +- 17 files changed, 240 insertions(+), 47 deletions(-) create mode 100644 lib/src/_core_keys.dart create mode 100644 lib/src/composable/get_context.dart create mode 100644 lib/src/composable/get_raw_event.dart create mode 100644 lib/src/context+app.dart create mode 100644 lib/src/context.dart delete mode 100644 lib/src/create_app.dart delete mode 100644 lib/src/create_stack_handler.dart create mode 100644 lib/src/routing/_routing_keys.dart create mode 100644 lib/src/routing/composable/get_params.dart create mode 100644 lib/src/routing/composable/route_utils.dart create mode 100644 lib/src/routing/route.dart rename lib/src/routing/{ => utils}/create_router.dart (55%) create mode 100644 lib/src/utils/create_app.dart create mode 100644 lib/src/utils/create_event_context.dart diff --git a/lib/src/_core_keys.dart b/lib/src/_core_keys.dart new file mode 100644 index 0000000..256a8a5 --- /dev/null +++ b/lib/src/_core_keys.dart @@ -0,0 +1 @@ +const kAppInstance = #spry.app; diff --git a/lib/src/app.dart b/lib/src/app.dart index 4e8f073..e2a2093 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -1,7 +1,9 @@ +import 'context.dart'; import 'handler.dart'; abstract interface class App { void use(Handler handler); Handler get handler; + Context get context; } diff --git a/lib/src/composable/get_context.dart b/lib/src/composable/get_context.dart new file mode 100644 index 0000000..a07b1c0 --- /dev/null +++ b/lib/src/composable/get_context.dart @@ -0,0 +1,5 @@ +import '../context.dart'; +import '../event.dart'; +import 'get_raw_event.dart'; + +Context getContext(Event event) => getRawEvent(event).context; diff --git a/lib/src/composable/get_raw_event.dart b/lib/src/composable/get_raw_event.dart new file mode 100644 index 0000000..bd5f96a --- /dev/null +++ b/lib/src/composable/get_raw_event.dart @@ -0,0 +1,8 @@ +import '../event.dart'; + +T getRawEvent(Event event) { + return switch (event.raw) { + T event => event, + _ => throw TypeError(), + }; +} diff --git a/lib/src/context+app.dart b/lib/src/context+app.dart new file mode 100644 index 0000000..f6ef6a3 --- /dev/null +++ b/lib/src/context+app.dart @@ -0,0 +1,9 @@ +// ignore_for_file: file_names + +import '_core_keys.dart'; +import 'app.dart'; +import 'context.dart'; + +extension ContextApp on Context { + App get app => get(kAppInstance); +} diff --git a/lib/src/context.dart b/lib/src/context.dart new file mode 100644 index 0000000..c9f8750 --- /dev/null +++ b/lib/src/context.dart @@ -0,0 +1,7 @@ +abstract interface class Context { + T get(Object key); + T? getOrNull(Object key); + T set(Object key, T value); + T upsert(Object key, T Function() create); + bool has(Object key); +} diff --git a/lib/src/create_app.dart b/lib/src/create_app.dart deleted file mode 100644 index 0e9f0a9..0000000 --- a/lib/src/create_app.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'app.dart'; -import 'create_stack_handler.dart'; -import 'handler.dart'; - -App createApp() => _AppImpl(); - -final class _AppImpl implements App { - final handlerStack = []; - - @override - void use(Handler handler) { - handlerStack.add(handler); - } - - @override - Handler get handler => createStackHandler(handlerStack.reversed); -} diff --git a/lib/src/create_stack_handler.dart b/lib/src/create_stack_handler.dart deleted file mode 100644 index bc938a3..0000000 --- a/lib/src/create_stack_handler.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'composable/next.dart'; -import 'define_handler.dart'; -import 'event.dart'; -import 'handler.dart'; - -Handler createStackHandler(Iterable stack) { - final handle = stack.fold Function(Event)>( - (_) async {}, - (child, handler) => (event) async { - setNext(() => child(event)); - - return handler.handle(event); - }, - ); - - return defineHandler(handle); -} diff --git a/lib/src/event.dart b/lib/src/event.dart index d29f67a..af71df1 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -1 +1,7 @@ -abstract interface class Event {} +import 'context.dart'; + +abstract interface class RawEvent { + Context get context; +} + +extension type const Event(RawEvent raw) {} diff --git a/lib/src/routing/_routing_keys.dart b/lib/src/routing/_routing_keys.dart new file mode 100644 index 0000000..9848e67 --- /dev/null +++ b/lib/src/routing/_routing_keys.dart @@ -0,0 +1,3 @@ +const kRouter = #spry.router; +const kRoute = #spry.route.instance; +const kParams = #spry.route.params; diff --git a/lib/src/routing/composable/get_params.dart b/lib/src/routing/composable/get_params.dart new file mode 100644 index 0000000..6edfb2f --- /dev/null +++ b/lib/src/routing/composable/get_params.dart @@ -0,0 +1,11 @@ +import 'package:routingkit/routingkit.dart' as routingkit show Params; + +import '../../composable/get_context.dart'; +import '../../event.dart'; +import '../_routing_keys.dart'; + +typedef Params = routingkit.Params; + +Params getRouteParams(Event event) { + return getContext(event).upsert(kParams, () => Params()); +} diff --git a/lib/src/routing/composable/route_utils.dart b/lib/src/routing/composable/route_utils.dart new file mode 100644 index 0000000..ccb8255 --- /dev/null +++ b/lib/src/routing/composable/route_utils.dart @@ -0,0 +1,22 @@ +import 'package:routingkit/routingkit.dart' show Router; + +import '../../composable/get_context.dart'; +import '../../event.dart'; +import '../_routing_keys.dart'; +import '../route.dart'; + +Route? getRoute(Event event) => getContext(event).get(kRoute); + +String? getRouteId(Event event) => getRoute(event)?.id; + +String makeRoutePath( + Event event, + String route, { + Map? params, + Iterable? wildcard, + String? catchall, +}) { + return getContext(event) + .get(kRouter) + .buildPath(route, params: params, wildcard: wildcard, catchall: catchall); +} diff --git a/lib/src/routing/route.dart b/lib/src/routing/route.dart new file mode 100644 index 0000000..d99f7df --- /dev/null +++ b/lib/src/routing/route.dart @@ -0,0 +1,3 @@ +abstract interface class Route { + String get id; +} diff --git a/lib/src/routing/create_router.dart b/lib/src/routing/utils/create_router.dart similarity index 55% rename from lib/src/routing/create_router.dart rename to lib/src/routing/utils/create_router.dart index 0410c40..1ca3c70 100644 --- a/lib/src/routing/create_router.dart +++ b/lib/src/routing/utils/create_router.dart @@ -1,11 +1,12 @@ import 'package:routingkit/routingkit.dart' as routingkit; -import '../composable/next.dart'; -import '../create_stack_handler.dart'; -import '../define_handler.dart'; -import '../event.dart'; -import '../handler.dart'; -import 'router.dart'; +import '../../composable/get_context.dart'; +import '../../composable/next.dart'; +import '../../event.dart'; +import '../../handler.dart'; +import '../_routing_keys.dart'; +import '../route.dart'; +import '../router.dart'; Router createRouter() { final inner = routingkit.createRouter(); @@ -24,12 +25,20 @@ final class _RouterImpl implements Router { @override Future handle(Event event) { - final (_, route) = inner.lookup('/'); + final context = getContext(event); + final result = inner.lookup('/'); - return switch ((route, handleWith)) { - (Handler handler, _HandleWith handle) => handle(event, handler), - (Handler handler, _) => handler.handle(event), - _ => throw 000, + if (result == null) { + throw 111; + } + + context.set(kRouter, inner); + context.set(kRoute, _RouteImpl(result.route)); + context.set(kParams, result.params); + + return switch (handleWith) { + _HandleWith handle => handle(event, result.value), + _ => result.value.handle(event), }; } @@ -49,3 +58,10 @@ final class _RouterImpl implements Router { static Future defaultRouterHandle(Event event, Handler handler) => handler.handle(event); } + +final class _RouteImpl implements Route { + const _RouteImpl(this.id); + + @override + final String id; +} diff --git a/lib/src/utils/create_app.dart b/lib/src/utils/create_app.dart new file mode 100644 index 0000000..47bc40b --- /dev/null +++ b/lib/src/utils/create_app.dart @@ -0,0 +1,78 @@ +import '../_core_keys.dart'; +import '../app.dart'; +import '../composable/next.dart'; +import '../context.dart'; +import '../define_handler.dart'; +import '../event.dart'; +import '../handler.dart'; + +App createApp() { + final app = _AppImpl(); + + app.context = _AppContext(); + app.context.set(kAppInstance, app); + + return app; +} + +final class _AppImpl implements App { + final handlerStack = []; + + @override + void use(Handler handler) { + handlerStack.add(handler); + } + + @override + Handler get handler => _createStackHandler(handlerStack.reversed); + + @override + late final Context context; +} + +final class _AppContext implements Context { + final Map locals = {}; + + @override + T? getOrNull(Object key) { + return switch (locals[key]) { + T value => value, + _ => null, + }; + } + + @override + T get(Object key) { + return switch (getOrNull(key)) { + T value => value, + _ => throw 111, + }; + } + + @override + bool has(Object key) => locals.containsKey(key); + + @override + T set(Object key, T value) => locals[key] = value; + + @override + T upsert(Object key, T Function() create) { + return switch (locals[key]) { + T value => value, + _ => set(key, create()), + }; + } +} + +Handler _createStackHandler(Iterable stack) { + final handle = stack.fold Function(Event)>( + (_) async {}, + (child, handler) => (event) async { + setNext(() => child(event)); + + return handler.handle(event); + }, + ); + + return defineHandler(handle); +} diff --git a/lib/src/utils/create_event_context.dart b/lib/src/utils/create_event_context.dart new file mode 100644 index 0000000..b01b8e0 --- /dev/null +++ b/lib/src/utils/create_event_context.dart @@ -0,0 +1,56 @@ +import '../app.dart'; +import '../context.dart'; + +Context createEventContext(App app, [Map? locals]) { + final context = _RequestEventContext(app); + + if (locals != null && locals.isNotEmpty) { + for (final e in locals.entries) { + context.set(e.key, e.value); + } + } + + return context; +} + +class _RequestEventContext implements Context { + _RequestEventContext(this.app); + + final App app; + final Map locals = {}; + + @override + T? getOrNull(Object key) { + return switch (locals[key]) { + T value => value, + _ => app.context.getOrNull(key), + }; + } + + @override + T get(Object key) { + return switch (getOrNull(key)) { + T value => value, + _ => throw 111, + }; + } + + @override + bool has(Object key) { + return switch (locals.containsKey(key)) { + false => app.context.has(key), + _ => true, + }; + } + + @override + T set(Object key, T value) => locals[key] = value; + + @override + T upsert(Object key, T Function() create) { + return switch (has(key)) { + true => get(key), + _ => set(key, create()), + }; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index bad2ee9..4ae6316 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,7 +8,7 @@ environment: # Add regular dependencies here. dependencies: - routingkit: ^1.0.0 + routingkit: ^1.1.0 # path: ^1.8.0 dev_dependencies: From c7d11c9904bbdd0bd0f47b2ccb32d228398926ca Mon Sep 17 00:00:00 2001 From: Seven Du Date: Thu, 4 Jul 2024 01:20:16 +0800 Subject: [PATCH 03/35] chore: WIP --- lib/src/app.dart | 3 +-- lib/src/event.dart | 7 ++++++- lib/src/routing/_routing_keys.dart | 1 + lib/src/routing/routes_builder.dart | 1 + lib/src/routing/utils/create_router.dart | 13 ++++++++++-- lib/src/utils/create_app.dart | 19 ++--------------- lib/src/{ => utils}/define_handler.dart | 4 ++-- lib/src/utils/define_stack_handler.dart | 26 ++++++++++++++++++++++++ 8 files changed, 50 insertions(+), 24 deletions(-) create mode 100644 lib/src/routing/routes_builder.dart rename lib/src/{ => utils}/define_handler.dart (87%) create mode 100644 lib/src/utils/define_stack_handler.dart diff --git a/lib/src/app.dart b/lib/src/app.dart index e2a2093..4b49d0d 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -3,7 +3,6 @@ import 'handler.dart'; abstract interface class App { void use(Handler handler); - - Handler get handler; Context get context; + Handler get handler; } diff --git a/lib/src/event.dart b/lib/src/event.dart index af71df1..0bb82a9 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -2,6 +2,11 @@ import 'context.dart'; abstract interface class RawEvent { Context get context; + Uri get uri; + String get method; } -extension type const Event(RawEvent raw) {} +extension type const Event(RawEvent raw) { + Uri get uri => raw.uri; + String get method => raw.method.toUpperCase(); +} diff --git a/lib/src/routing/_routing_keys.dart b/lib/src/routing/_routing_keys.dart index 9848e67..7853a58 100644 --- a/lib/src/routing/_routing_keys.dart +++ b/lib/src/routing/_routing_keys.dart @@ -1,3 +1,4 @@ const kRouter = #spry.router; const kRoute = #spry.route.instance; const kParams = #spry.route.params; +const kAllMethod = '#spry.__all_method__'; diff --git a/lib/src/routing/routes_builder.dart b/lib/src/routing/routes_builder.dart new file mode 100644 index 0000000..cfc223d --- /dev/null +++ b/lib/src/routing/routes_builder.dart @@ -0,0 +1 @@ +abstract interface class RoutesBuilder {} diff --git a/lib/src/routing/utils/create_router.dart b/lib/src/routing/utils/create_router.dart index 1ca3c70..cbd3a71 100644 --- a/lib/src/routing/utils/create_router.dart +++ b/lib/src/routing/utils/create_router.dart @@ -26,15 +26,24 @@ final class _RouterImpl implements Router { @override Future handle(Event event) { final context = getContext(event); - final result = inner.lookup('/'); + final result = switch (event.method) { + 'HEAD' => switch (inner.lookup('HEAD/${event.uri.path}')) { + routingkit.Result result => result, + _ => inner.lookup('GET/${event.uri.path}'), + }, + String method => switch (inner.lookup('$method/${event.uri.path}')) { + routingkit.Result result => result, + _ => inner.lookup('$kAllMethod/${event.uri.path}'), + }, + }; if (result == null) { throw 111; } context.set(kRouter, inner); - context.set(kRoute, _RouteImpl(result.route)); context.set(kParams, result.params); + context.set(kRoute, _RouteImpl(result.route.split('/').skip(1).join('/'))); return switch (handleWith) { _HandleWith handle => handle(event, result.value), diff --git a/lib/src/utils/create_app.dart b/lib/src/utils/create_app.dart index 47bc40b..acb0d2c 100644 --- a/lib/src/utils/create_app.dart +++ b/lib/src/utils/create_app.dart @@ -1,10 +1,8 @@ import '../_core_keys.dart'; import '../app.dart'; -import '../composable/next.dart'; import '../context.dart'; -import '../define_handler.dart'; -import '../event.dart'; import '../handler.dart'; +import 'define_stack_handler.dart'; App createApp() { final app = _AppImpl(); @@ -24,7 +22,7 @@ final class _AppImpl implements App { } @override - Handler get handler => _createStackHandler(handlerStack.reversed); + Handler get handler => defineStackHandler(handlerStack); @override late final Context context; @@ -63,16 +61,3 @@ final class _AppContext implements Context { }; } } - -Handler _createStackHandler(Iterable stack) { - final handle = stack.fold Function(Event)>( - (_) async {}, - (child, handler) => (event) async { - setNext(() => child(event)); - - return handler.handle(event); - }, - ); - - return defineHandler(handle); -} diff --git a/lib/src/define_handler.dart b/lib/src/utils/define_handler.dart similarity index 87% rename from lib/src/define_handler.dart rename to lib/src/utils/define_handler.dart index 9bd33f1..f1fd9e1 100644 --- a/lib/src/define_handler.dart +++ b/lib/src/utils/define_handler.dart @@ -1,7 +1,7 @@ import 'dart:async'; -import 'event.dart'; -import 'handler.dart'; +import '../event.dart'; +import '../handler.dart'; Handler defineHandler(FutureOr Function(Event event) handle) { return _ClosureHandler(handle); diff --git a/lib/src/utils/define_stack_handler.dart b/lib/src/utils/define_stack_handler.dart new file mode 100644 index 0000000..b1b4be2 --- /dev/null +++ b/lib/src/utils/define_stack_handler.dart @@ -0,0 +1,26 @@ +import '../composable/next.dart'; +import '../handler.dart'; +import '../event.dart'; +import 'define_handler.dart'; + +Handler defineStackHandler(Iterable handlers) { + final handle = handlers.reversed.fold Function(Event)>( + (_) async {}, + (child, handler) => (event) async { + setNext(() => child(event)); + + return handler.handle(event); + }, + ); + + return defineHandler(handle); +} + +extension on Iterable { + Iterable get reversed { + return switch (this) { + List(reversed: final reversed) => reversed, + _ => toList().reversed, + }; + } +} From bb26488d89e9ff85c70c601e654d5c22a51fcdd7 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Thu, 4 Jul 2024 03:51:54 +0800 Subject: [PATCH 04/35] chore: WIP --- example/plain.dart | 32 ++++++++ lib/plain.dart | 89 +++++++++++++++++++++ lib/spry.dart | 24 ++++++ lib/src/composable/get_context.dart | 3 +- lib/src/composable/get_raw_event.dart | 8 -- lib/src/event.dart | 14 ++-- lib/src/request.dart | 8 ++ lib/src/response.dart | 8 ++ lib/src/routing/_routing_keys.dart | 2 +- lib/src/routing/composable.dart | 34 ++++++++ lib/src/routing/composable/get_params.dart | 11 --- lib/src/routing/composable/route_utils.dart | 22 ----- lib/src/routing/router.dart | 3 +- lib/src/routing/routes_builder+group.dart | 51 ++++++++++++ lib/src/routing/routes_builder+methods.dart | 15 ++++ lib/src/routing/routes_builder.dart | 6 +- lib/src/routing/utils/create_router.dart | 5 ++ lib/src/utils/create_app.dart | 23 ++++-- 18 files changed, 299 insertions(+), 59 deletions(-) create mode 100644 example/plain.dart create mode 100644 lib/plain.dart delete mode 100644 lib/src/composable/get_raw_event.dart create mode 100644 lib/src/request.dart create mode 100644 lib/src/response.dart create mode 100644 lib/src/routing/composable.dart delete mode 100644 lib/src/routing/composable/get_params.dart delete mode 100644 lib/src/routing/composable/route_utils.dart create mode 100644 lib/src/routing/routes_builder+group.dart create mode 100644 lib/src/routing/routes_builder+methods.dart diff --git a/example/plain.dart b/example/plain.dart new file mode 100644 index 0000000..71defab --- /dev/null +++ b/example/plain.dart @@ -0,0 +1,32 @@ +import 'package:spry/plain.dart'; +import 'package:spry/spry.dart'; + +main() async { + final app = createApp(); + + app.use(defineHandler((event) { + print(1); + next(); + })); + + app.use(defineHandler((event) { + print(event.method); + print(event.uri.path); + next(); + })); + + final router = createRouter(); + + router.get('/users/:name', defineHandler((event) { + print(getRouteParam(event, 'name')); + })); + + app.use(router); + + final handler = toPlainHandler(app); + final request = createPlainRequest( + method: 'get', + uri: Uri.parse('spry:///users/seven?a=2'), + ); + await handler(request); +} diff --git a/lib/plain.dart b/lib/plain.dart new file mode 100644 index 0000000..79cf1da --- /dev/null +++ b/lib/plain.dart @@ -0,0 +1,89 @@ +import 'dart:typed_data'; + +import 'spry.dart'; + +class PlainRequest implements Request { + const PlainRequest({ + required this.method, + required this.uri, + required this.headers, + required this.body, + this.locals, + }); + + @override + final String method; + + @override + final Uri uri; + @override + final Iterable<(String, String)> headers; + @override + final Stream body; + final Map? locals; +} + +class PlainResponse implements Response { + @override + Stream? body; + + @override + int status = 200; + + @override + String statusText = 'No Content'; + + @override + List<(String, String)> get headers => []; +} + +class _PlainRawEvent implements RawEvent { + const _PlainRawEvent({ + required this.context, + required this.request, + required this.response, + }); + + @override + final Context context; + + @override + final Request request; + + @override + final Response response; +} + +typedef PlainHandler = Future Function(PlainRequest request); + +PlainHandler toPlainHandler(App app) { + return (request) async { + final response = PlainResponse(); + final raw = _PlainRawEvent( + context: createEventContext(app, request.locals), + request: request, + response: PlainResponse(), + ); + final event = createRequestEvent(raw); + + await app.handler.handle(event); + + return response; + }; +} + +PlainRequest createPlainRequest({ + required String method, + required Uri uri, + Iterable<(String, String)>? headers, + Stream? body, + Map? locals, +}) { + return PlainRequest( + method: method, + uri: uri, + headers: headers ?? const [], + body: body ?? Stream.empty(), + locals: locals, + ); +} diff --git a/lib/spry.dart b/lib/spry.dart index e69de29..1d587a7 100644 --- a/lib/spry.dart +++ b/lib/spry.dart @@ -0,0 +1,24 @@ +export 'src/app.dart'; +export 'src/context.dart'; +export 'src/context+app.dart'; +export 'src/event.dart'; +export 'src/handler.dart'; +export 'src/request.dart'; +export 'src/response.dart'; + +export 'src/utils/create_app.dart'; +export 'src/utils/create_event_context.dart'; +export 'src/utils/define_handler.dart'; +export 'src/utils/define_stack_handler.dart'; + +export 'src/composable/get_context.dart'; +export 'src/composable/next.dart'; + +export 'src/routing/composable.dart'; +export 'src/routing/route.dart'; +export 'src/routing/router.dart'; +export 'src/routing/routes_builder.dart'; +export 'src/routing/routes_builder+group.dart'; +export 'src/routing/routes_builder+methods.dart'; + +export 'src/routing/utils/create_router.dart'; diff --git a/lib/src/composable/get_context.dart b/lib/src/composable/get_context.dart index a07b1c0..0f8439d 100644 --- a/lib/src/composable/get_context.dart +++ b/lib/src/composable/get_context.dart @@ -1,5 +1,4 @@ import '../context.dart'; import '../event.dart'; -import 'get_raw_event.dart'; -Context getContext(Event event) => getRawEvent(event).context; +Context getContext(Event event) => event.raw.context; diff --git a/lib/src/composable/get_raw_event.dart b/lib/src/composable/get_raw_event.dart deleted file mode 100644 index bd5f96a..0000000 --- a/lib/src/composable/get_raw_event.dart +++ /dev/null @@ -1,8 +0,0 @@ -import '../event.dart'; - -T getRawEvent(Event event) { - return switch (event.raw) { - T event => event, - _ => throw TypeError(), - }; -} diff --git a/lib/src/event.dart b/lib/src/event.dart index 0bb82a9..4a3dc01 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -1,12 +1,16 @@ import 'context.dart'; +import 'request.dart'; +import 'response.dart'; abstract interface class RawEvent { Context get context; - Uri get uri; - String get method; + Request get request; + Response get response; } -extension type const Event(RawEvent raw) { - Uri get uri => raw.uri; - String get method => raw.method.toUpperCase(); +extension type const Event._(RawEvent raw) { + Uri get uri => raw.request.uri; + String get method => raw.request.method.toUpperCase(); } + +Event createRequestEvent(RawEvent raw) => Event._(raw); diff --git a/lib/src/request.dart b/lib/src/request.dart new file mode 100644 index 0000000..8ab78a0 --- /dev/null +++ b/lib/src/request.dart @@ -0,0 +1,8 @@ +import 'dart:typed_data'; + +abstract interface class Request { + String get method; + Uri get uri; + Iterable<(String, String)> get headers; + Stream get body; +} diff --git a/lib/src/response.dart b/lib/src/response.dart new file mode 100644 index 0000000..a3878ed --- /dev/null +++ b/lib/src/response.dart @@ -0,0 +1,8 @@ +import 'dart:typed_data'; + +abstract interface class Response { + abstract int status; + abstract String statusText; + List<(String, String)> get headers; + Stream? body; +} diff --git a/lib/src/routing/_routing_keys.dart b/lib/src/routing/_routing_keys.dart index 7853a58..bba37cd 100644 --- a/lib/src/routing/_routing_keys.dart +++ b/lib/src/routing/_routing_keys.dart @@ -1,4 +1,4 @@ const kRouter = #spry.router; const kRoute = #spry.route.instance; const kParams = #spry.route.params; -const kAllMethod = '#spry.__all_method__'; +const kAllMethod = '#SPRY.__ALL_METHOD__'; diff --git a/lib/src/routing/composable.dart b/lib/src/routing/composable.dart new file mode 100644 index 0000000..2547280 --- /dev/null +++ b/lib/src/routing/composable.dart @@ -0,0 +1,34 @@ +import 'package:routingkit/routingkit.dart' as routingkit; + +import '../composable/get_context.dart'; +import '../event.dart'; +import '_routing_keys.dart'; +import 'route.dart'; + +typedef Params = routingkit.Params; + +Params getRouteParams(Event event) { + return getContext(event).upsert(kParams, () => Params()); +} + +String? getRouteCatchallParam(Event event) => getRouteParams(event).catchall; +String? getRouteParam(Event event, String name) => + getRouteParams(event).call(name); +Iterable getRouteParamValues(Event event, String name) => + getRouteParams(event).valuesOf(name); + +Route? getRoute(Event event) => getContext(event).get(kRoute); + +String? getRouteId(Event event) => getRoute(event)?.id; + +String makeRoutePath( + Event event, + String route, { + Map? params, + Iterable? wildcard, + String? catchall, +}) { + return getContext(event) + .get(kRouter) + .buildPath(route, params: params, wildcard: wildcard, catchall: catchall); +} diff --git a/lib/src/routing/composable/get_params.dart b/lib/src/routing/composable/get_params.dart deleted file mode 100644 index 6edfb2f..0000000 --- a/lib/src/routing/composable/get_params.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:routingkit/routingkit.dart' as routingkit show Params; - -import '../../composable/get_context.dart'; -import '../../event.dart'; -import '../_routing_keys.dart'; - -typedef Params = routingkit.Params; - -Params getRouteParams(Event event) { - return getContext(event).upsert(kParams, () => Params()); -} diff --git a/lib/src/routing/composable/route_utils.dart b/lib/src/routing/composable/route_utils.dart deleted file mode 100644 index ccb8255..0000000 --- a/lib/src/routing/composable/route_utils.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:routingkit/routingkit.dart' show Router; - -import '../../composable/get_context.dart'; -import '../../event.dart'; -import '../_routing_keys.dart'; -import '../route.dart'; - -Route? getRoute(Event event) => getContext(event).get(kRoute); - -String? getRouteId(Event event) => getRoute(event)?.id; - -String makeRoutePath( - Event event, - String route, { - Map? params, - Iterable? wildcard, - String? catchall, -}) { - return getContext(event) - .get(kRouter) - .buildPath(route, params: params, wildcard: wildcard, catchall: catchall); -} diff --git a/lib/src/routing/router.dart b/lib/src/routing/router.dart index 3dbba3b..af2edd4 100644 --- a/lib/src/routing/router.dart +++ b/lib/src/routing/router.dart @@ -1,5 +1,6 @@ import '../handler.dart'; +import 'routes_builder.dart'; -abstract interface class Router implements Handler { +abstract interface class Router implements Handler, RoutesBuilder { void use(Handler handler); } diff --git a/lib/src/routing/routes_builder+group.dart b/lib/src/routing/routes_builder+group.dart new file mode 100644 index 0000000..7a29378 --- /dev/null +++ b/lib/src/routing/routes_builder+group.dart @@ -0,0 +1,51 @@ +// ignore_for_file: file_names + +import '../handler.dart'; +import '../utils/define_stack_handler.dart'; +import 'routes_builder.dart'; + +extension RoutesBuilderGroup on RoutesBuilder { + RoutesBuilder grouped({String? route, Handler? handler}) { + return _GroupedRoutesBuilder( + parent: this, + route: route, + handler: handler, + ); + } + + void group(void Function(RoutesBuilder routes) closure, + {String? route, Handler? handler}) { + return closure(grouped(route: route, handler: handler)); + } +} + +class _GroupedRoutesBuilder implements RoutesBuilder { + const _GroupedRoutesBuilder({ + required this.parent, + required this.route, + required this.handler, + }); + + final RoutesBuilder parent; + final String? route; + final Handler? handler; + + @override + void on(String method, String route, Handler handler) { + parent.on(method, resolveRoute(route), resolveHandler(handler)); + } + + String resolveRoute(String route) { + return switch (this.route) { + String prefix => '$prefix/$route', + _ => route, + }; + } + + Handler resolveHandler(Handler handler) { + return switch (this.handler) { + Handler parent => defineStackHandler([parent, handler]), + _ => handler, + }; + } +} diff --git a/lib/src/routing/routes_builder+methods.dart b/lib/src/routing/routes_builder+methods.dart new file mode 100644 index 0000000..2294fc0 --- /dev/null +++ b/lib/src/routing/routes_builder+methods.dart @@ -0,0 +1,15 @@ +// ignore_for_file: file_names + +import '../handler.dart'; +import '_routing_keys.dart'; +import 'routes_builder.dart'; + +extension RoutesBuilderMethods on RoutesBuilder { + void all(String route, Handler handler) => on(kAllMethod, route, handler); + void get(String route, Handler handler) => on('GET', route, handler); + void post(String route, Handler handler) => on('POST', route, handler); + void put(String route, Handler handler) => on('PUT', route, handler); + void patch(String route, Handler handler) => on('PATCH', route, handler); + void delete(String route, Handler handler) => on('DELETE', route, handler); + void head(String route, Handler handler) => on('HEAD', route, handler); +} diff --git a/lib/src/routing/routes_builder.dart b/lib/src/routing/routes_builder.dart index cfc223d..6d3c296 100644 --- a/lib/src/routing/routes_builder.dart +++ b/lib/src/routing/routes_builder.dart @@ -1 +1,5 @@ -abstract interface class RoutesBuilder {} +import '../handler.dart'; + +abstract interface class RoutesBuilder { + void on(String method, String route, Handler handler); +} diff --git a/lib/src/routing/utils/create_router.dart b/lib/src/routing/utils/create_router.dart index cbd3a71..94212f6 100644 --- a/lib/src/routing/utils/create_router.dart +++ b/lib/src/routing/utils/create_router.dart @@ -64,6 +64,11 @@ final class _RouterImpl implements Router { ); } + @override + void on(String method, String route, Handler handler) { + inner.register('${method.toUpperCase()}/$route', handler); + } + static Future defaultRouterHandle(Event event, Handler handler) => handler.handle(event); } diff --git a/lib/src/utils/create_app.dart b/lib/src/utils/create_app.dart index acb0d2c..0c90e79 100644 --- a/lib/src/utils/create_app.dart +++ b/lib/src/utils/create_app.dart @@ -4,28 +4,35 @@ import '../context.dart'; import '../handler.dart'; import 'define_stack_handler.dart'; -App createApp() { - final app = _AppImpl(); +App createApp({Map? locals}) { + final context = _AppContext(); + if (locals != null && locals.isNotEmpty) { + for (final e in locals.entries) { + context.set(e.key, e.value); + } + } - app.context = _AppContext(); - app.context.set(kAppInstance, app); + final app = _AppImpl(context); + context.set(kAppInstance, app); return app; } final class _AppImpl implements App { + _AppImpl(this.context); + final handlerStack = []; @override - void use(Handler handler) { - handlerStack.add(handler); - } + final Context context; @override Handler get handler => defineStackHandler(handlerStack); @override - late final Context context; + void use(Handler handler) { + handlerStack.add(handler); + } } final class _AppContext implements Context { From 2eb3fc7a01f1cab7734749f0fcacd6a16698ebc9 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Thu, 4 Jul 2024 14:16:28 +0800 Subject: [PATCH 05/35] chore: WIP --- example/plain.dart | 12 ++- lib/plain.dart | 6 ++ lib/src/composable/request_body_utils.dart | 29 +++++++ lib/src/composable/request_utils.dart | 99 ++++++++++++++++++++++ lib/src/event.dart | 1 + lib/src/routing/composable.dart | 8 +- 6 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 lib/src/composable/request_body_utils.dart create mode 100644 lib/src/composable/request_utils.dart diff --git a/example/plain.dart b/example/plain.dart index 71defab..aa3fa03 100644 --- a/example/plain.dart +++ b/example/plain.dart @@ -21,12 +21,22 @@ main() async { print(getRouteParam(event, 'name')); })); + router.group(route: 'demo', (routes) { + routes.all('haha', defineHandler((event) { + print('3'); + })); + + routes.all('/', defineHandler((event) { + print('demo root'); + })); + }); + app.use(router); final handler = toPlainHandler(app); final request = createPlainRequest( method: 'get', - uri: Uri.parse('spry:///users/seven?a=2'), + uri: Uri.parse('/demo/haha'), ); await handler(request); } diff --git a/lib/plain.dart b/lib/plain.dart index 79cf1da..1e10985 100644 --- a/lib/plain.dart +++ b/lib/plain.dart @@ -16,10 +16,13 @@ class PlainRequest implements Request { @override final Uri uri; + @override final Iterable<(String, String)> headers; + @override final Stream body; + final Map? locals; } @@ -52,6 +55,9 @@ class _PlainRawEvent implements RawEvent { @override final Response response; + + @override + String? get clientAddress => null; } typedef PlainHandler = Future Function(PlainRequest request); diff --git a/lib/src/composable/request_body_utils.dart b/lib/src/composable/request_body_utils.dart new file mode 100644 index 0000000..4ac4c4c --- /dev/null +++ b/lib/src/composable/request_body_utils.dart @@ -0,0 +1,29 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import '../event.dart'; + +/// Returns request body stream. +Stream getBodyStream(Event event) { + return event.raw.request.body; +} + +/// Returns request raw body. +Future getRawBody(Event event) async { + final raw = []; + await for (final chunk in getBodyStream(event)) { + raw.addAll(chunk); + } + + return Uint8List.fromList(raw); +} + +/// Returns the request text body. +Future getTextBody(Event event) { + return utf8.decodeStream(getBodyStream(event)); +} + +/// Returns the request JSON body +Future getJSONBody(Event event) async { + final text = await getTextBody(event); +} diff --git a/lib/src/composable/request_utils.dart b/lib/src/composable/request_utils.dart new file mode 100644 index 0000000..6603107 --- /dev/null +++ b/lib/src/composable/request_utils.dart @@ -0,0 +1,99 @@ +import '../event.dart'; + +/// Returns the query params from request URI. +/// +/// ## Example +/// ```dart +/// defineHandler((event) { +/// final query = getQuery(event); // {"key": "value", ...} +/// }); +///``` +Map getQuery(Event event) { + return event.uri.queryParameters; +} + +/// Returns the query params all values from request URI. +/// +/// ## Example +/// ```dart +/// defineHandler((event) { +/// final queryAll = getQueryParamsAll(event); // {"key": ["value1", "value2"]} +/// }); +/// ``` +Map> getQueryAll(Event event) { + return event.uri.queryParametersAll; +} + +/// Returns validated query params. +T getValidatedQuery( + Event event, T Function(Map> query) validator) { + return validator(event.uri.queryParametersAll); +} + +/// Returns request header entries. +Iterable<(String, String)> getHeaderEntries(Event event) { + return event.raw.request.headers; +} + +/// Returns request header values +Iterable getHeaderValues(Event event, String name) { + final lowerName = name.toLowerCase(); + + return getHeaderEntries(event) + .where((e) => e.$1.toLowerCase() == lowerName) + .map((e) => e.$2.trim()); +} + +/// Returns request header. +String getHeader(Event event, String name) { + return getHeaderValues(event, name).join(', ').trim(); +} + +/// Returns the request hostname +String getRequestHost(Event event, {bool forwarded = false}) { + if (forwarded) { + return switch (getHeader(event, 'x-forwarded-host')) { + String(isEmpty: true) => event.uri.host, + String host => host, + }; + } + + return event.uri.scheme; +} + +/// Returns the request protocol. +String getRequestProtocol(Event event, {bool forwarded = false}) { + if (forwarded) { + return switch (getHeader(event, 'x-forwarded-proto')) { + String(isEmpty: true) => event.uri.scheme, + String proto => proto, + }; + } + + return event.uri.scheme; +} + +/// Returns the request [Uri]. +Uri getRequestURI(Event event, {bool forwarded = false}) { + if (!forwarded) return event.uri; + + return event.uri.replace( + scheme: getRequestProtocol(event, forwarded: true), + host: getRequestHost(event, forwarded: true), + ); +} + +/// Returns client address +String? getClientAddress(Event event, {bool forwarded = false}) { + if (event.raw.clientAddress != null && + event.raw.clientAddress?.isNotEmpty == true) { + return event.raw.clientAddress!; + } else if (forwarded) { + return switch (getHeaderValues(event, 'x-forwarded-for')) { + Iterable(isEmpty: true) => null, + Iterable(last: final address) => address, + }; + } + + return null; +} diff --git a/lib/src/event.dart b/lib/src/event.dart index 4a3dc01..824bcc4 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -6,6 +6,7 @@ abstract interface class RawEvent { Context get context; Request get request; Response get response; + String? get clientAddress; } extension type const Event._(RawEvent raw) { diff --git a/lib/src/routing/composable.dart b/lib/src/routing/composable.dart index 2547280..1f09376 100644 --- a/lib/src/routing/composable.dart +++ b/lib/src/routing/composable.dart @@ -11,15 +11,17 @@ Params getRouteParams(Event event) { return getContext(event).upsert(kParams, () => Params()); } +T getValidatedRouteParams(Event event, T Function(Params params) validator) { + return validator(getRouteParams(event)); +} + String? getRouteCatchallParam(Event event) => getRouteParams(event).catchall; String? getRouteParam(Event event, String name) => getRouteParams(event).call(name); Iterable getRouteParamValues(Event event, String name) => getRouteParams(event).valuesOf(name); -Route? getRoute(Event event) => getContext(event).get(kRoute); - -String? getRouteId(Event event) => getRoute(event)?.id; +Route? getRoute(Event event) => getContext(event).getOrNull(kRoute); String makeRoutePath( Event event, From 5af893b2b7a8d7f2110e1331b49e3ab23bef7000 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Sun, 7 Jul 2024 06:04:59 +0800 Subject: [PATCH 06/35] chore: WIP --- demo.dart | 6 + lib/plain.dart | 95 -------------- lib/spry.dart | 34 +++-- lib/src/_core_keys.dart | 1 - lib/src/app.dart | 8 -- lib/src/composable/get_context.dart | 4 - lib/src/composable/next.dart | 10 -- lib/src/composable/request_body_utils.dart | 29 ----- lib/src/composable/request_utils.dart | 99 --------------- lib/src/context+app.dart | 9 -- lib/src/context.dart | 7 -- lib/src/event.dart | 18 +-- lib/src/handler.dart | 5 - lib/src/http/headers/headers+get.dart | 17 +++ lib/src/http/headers/headers+has.dart | 10 ++ lib/src/http/headers/headers+keys.dart | 9 ++ lib/src/http/headers/headers+rebuild.dart | 14 +++ lib/src/http/headers/headers+to_builder.dart | 8 ++ lib/src/http/headers/headers.dart | 13 ++ lib/src/http/headers/headers_builder+set.dart | 10 ++ lib/src/http/headers/headers_builder.dart | 39 ++++++ .../http/http_message/http_message+json.dart | 15 +++ .../http/http_message/http_message+text.dart | 14 +++ lib/src/http/http_message/http_message.dart | 10 ++ lib/src/http/http_status_reason_phrase.dart | 91 ++++++++++++++ lib/src/http/request.dart | 3 + lib/src/http/response.dart | 117 ++++++++++++++++++ lib/src/locals/locals+get_or_null.dart | 13 ++ lib/src/locals/locals.dart | 16 +++ lib/src/request.dart | 8 -- lib/src/response.dart | 8 -- lib/src/routing/_routing_keys.dart | 4 - lib/src/routing/composable.dart | 36 ------ lib/src/routing/route.dart | 3 - lib/src/routing/router.dart | 6 - lib/src/routing/routes_builder+group.dart | 51 -------- lib/src/routing/routes_builder+methods.dart | 15 --- lib/src/routing/routes_builder.dart | 5 - lib/src/routing/utils/create_router.dart | 81 ------------ lib/src/spry.dart | 5 + lib/src/utils/create_app.dart | 70 ----------- lib/src/utils/create_event_context.dart | 56 --------- lib/src/utils/define_handler.dart | 19 --- lib/src/utils/define_stack_handler.dart | 26 ---- pubspec.yaml | 7 +- test/http/headers_builder_test.dart | 49 ++++++++ test/http/headers_test.dart | 94 ++++++++++++++ test/http/http_message_test.dart | 38 ++++++ test/http/response_test.dart | 60 +++++++++ test/locals/locals_test.dart | 37 ++++++ 50 files changed, 705 insertions(+), 697 deletions(-) create mode 100644 demo.dart delete mode 100644 lib/plain.dart delete mode 100644 lib/src/_core_keys.dart delete mode 100644 lib/src/app.dart delete mode 100644 lib/src/composable/get_context.dart delete mode 100644 lib/src/composable/next.dart delete mode 100644 lib/src/composable/request_body_utils.dart delete mode 100644 lib/src/composable/request_utils.dart delete mode 100644 lib/src/context+app.dart delete mode 100644 lib/src/context.dart delete mode 100644 lib/src/handler.dart create mode 100644 lib/src/http/headers/headers+get.dart create mode 100644 lib/src/http/headers/headers+has.dart create mode 100644 lib/src/http/headers/headers+keys.dart create mode 100644 lib/src/http/headers/headers+rebuild.dart create mode 100644 lib/src/http/headers/headers+to_builder.dart create mode 100644 lib/src/http/headers/headers.dart create mode 100644 lib/src/http/headers/headers_builder+set.dart create mode 100644 lib/src/http/headers/headers_builder.dart create mode 100644 lib/src/http/http_message/http_message+json.dart create mode 100644 lib/src/http/http_message/http_message+text.dart create mode 100644 lib/src/http/http_message/http_message.dart create mode 100644 lib/src/http/http_status_reason_phrase.dart create mode 100644 lib/src/http/request.dart create mode 100644 lib/src/http/response.dart create mode 100644 lib/src/locals/locals+get_or_null.dart create mode 100644 lib/src/locals/locals.dart delete mode 100644 lib/src/request.dart delete mode 100644 lib/src/response.dart delete mode 100644 lib/src/routing/_routing_keys.dart delete mode 100644 lib/src/routing/composable.dart delete mode 100644 lib/src/routing/route.dart delete mode 100644 lib/src/routing/router.dart delete mode 100644 lib/src/routing/routes_builder+group.dart delete mode 100644 lib/src/routing/routes_builder+methods.dart delete mode 100644 lib/src/routing/routes_builder.dart delete mode 100644 lib/src/routing/utils/create_router.dart create mode 100644 lib/src/spry.dart delete mode 100644 lib/src/utils/create_app.dart delete mode 100644 lib/src/utils/create_event_context.dart delete mode 100644 lib/src/utils/define_handler.dart delete mode 100644 lib/src/utils/define_stack_handler.dart create mode 100644 test/http/headers_builder_test.dart create mode 100644 test/http/headers_test.dart create mode 100644 test/http/http_message_test.dart create mode 100644 test/http/response_test.dart create mode 100644 test/locals/locals_test.dart diff --git a/demo.dart b/demo.dart new file mode 100644 index 0000000..1bc0492 --- /dev/null +++ b/demo.dart @@ -0,0 +1,6 @@ +import 'dart:io'; + +demo(HttpRequest req, HttpResponse res) { + req.response.encoding; + res.flush; +} diff --git a/lib/plain.dart b/lib/plain.dart deleted file mode 100644 index 1e10985..0000000 --- a/lib/plain.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'dart:typed_data'; - -import 'spry.dart'; - -class PlainRequest implements Request { - const PlainRequest({ - required this.method, - required this.uri, - required this.headers, - required this.body, - this.locals, - }); - - @override - final String method; - - @override - final Uri uri; - - @override - final Iterable<(String, String)> headers; - - @override - final Stream body; - - final Map? locals; -} - -class PlainResponse implements Response { - @override - Stream? body; - - @override - int status = 200; - - @override - String statusText = 'No Content'; - - @override - List<(String, String)> get headers => []; -} - -class _PlainRawEvent implements RawEvent { - const _PlainRawEvent({ - required this.context, - required this.request, - required this.response, - }); - - @override - final Context context; - - @override - final Request request; - - @override - final Response response; - - @override - String? get clientAddress => null; -} - -typedef PlainHandler = Future Function(PlainRequest request); - -PlainHandler toPlainHandler(App app) { - return (request) async { - final response = PlainResponse(); - final raw = _PlainRawEvent( - context: createEventContext(app, request.locals), - request: request, - response: PlainResponse(), - ); - final event = createRequestEvent(raw); - - await app.handler.handle(event); - - return response; - }; -} - -PlainRequest createPlainRequest({ - required String method, - required Uri uri, - Iterable<(String, String)>? headers, - Stream? body, - Map? locals, -}) { - return PlainRequest( - method: method, - uri: uri, - headers: headers ?? const [], - body: body ?? Stream.empty(), - locals: locals, - ); -} diff --git a/lib/spry.dart b/lib/spry.dart index 1d587a7..51879c0 100644 --- a/lib/spry.dart +++ b/lib/spry.dart @@ -1,24 +1,18 @@ -export 'src/app.dart'; -export 'src/context.dart'; -export 'src/context+app.dart'; -export 'src/event.dart'; -export 'src/handler.dart'; -export 'src/request.dart'; -export 'src/response.dart'; +export 'src/http/headers/headers.dart'; +export 'src/http/headers/headers+get.dart'; +export 'src/http/headers/headers+has.dart'; +export 'src/http/headers/headers+keys.dart'; +export 'src/http/headers/headers+rebuild.dart'; +export 'src/http/headers/headers+to_builder.dart'; -export 'src/utils/create_app.dart'; -export 'src/utils/create_event_context.dart'; -export 'src/utils/define_handler.dart'; -export 'src/utils/define_stack_handler.dart'; +export 'src/http/headers/headers_builder.dart'; +export 'src/http/headers/headers_builder+set.dart'; -export 'src/composable/get_context.dart'; -export 'src/composable/next.dart'; +export 'src/http/http_message/http_message.dart'; +export 'src/http/http_message/http_message+text.dart'; +export 'src/http/http_message/http_message+json.dart'; -export 'src/routing/composable.dart'; -export 'src/routing/route.dart'; -export 'src/routing/router.dart'; -export 'src/routing/routes_builder.dart'; -export 'src/routing/routes_builder+group.dart'; -export 'src/routing/routes_builder+methods.dart'; +export 'src/http/response.dart'; -export 'src/routing/utils/create_router.dart'; +export 'src/locals/locals.dart' hide LocalsImpl; +export 'src/locals/locals+get_or_null.dart'; diff --git a/lib/src/_core_keys.dart b/lib/src/_core_keys.dart deleted file mode 100644 index 256a8a5..0000000 --- a/lib/src/_core_keys.dart +++ /dev/null @@ -1 +0,0 @@ -const kAppInstance = #spry.app; diff --git a/lib/src/app.dart b/lib/src/app.dart deleted file mode 100644 index 4b49d0d..0000000 --- a/lib/src/app.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'context.dart'; -import 'handler.dart'; - -abstract interface class App { - void use(Handler handler); - Context get context; - Handler get handler; -} diff --git a/lib/src/composable/get_context.dart b/lib/src/composable/get_context.dart deleted file mode 100644 index 0f8439d..0000000 --- a/lib/src/composable/get_context.dart +++ /dev/null @@ -1,4 +0,0 @@ -import '../context.dart'; -import '../event.dart'; - -Context getContext(Event event) => event.raw.context; diff --git a/lib/src/composable/next.dart b/lib/src/composable/next.dart deleted file mode 100644 index 6d608a3..0000000 --- a/lib/src/composable/next.dart +++ /dev/null @@ -1,10 +0,0 @@ -Future Function()? _effect; - -Future next() async { - await _effect?.call(); - _effect = null; -} - -void setNext(Future Function() next) { - _effect = next; -} diff --git a/lib/src/composable/request_body_utils.dart b/lib/src/composable/request_body_utils.dart deleted file mode 100644 index 4ac4c4c..0000000 --- a/lib/src/composable/request_body_utils.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -import '../event.dart'; - -/// Returns request body stream. -Stream getBodyStream(Event event) { - return event.raw.request.body; -} - -/// Returns request raw body. -Future getRawBody(Event event) async { - final raw = []; - await for (final chunk in getBodyStream(event)) { - raw.addAll(chunk); - } - - return Uint8List.fromList(raw); -} - -/// Returns the request text body. -Future getTextBody(Event event) { - return utf8.decodeStream(getBodyStream(event)); -} - -/// Returns the request JSON body -Future getJSONBody(Event event) async { - final text = await getTextBody(event); -} diff --git a/lib/src/composable/request_utils.dart b/lib/src/composable/request_utils.dart deleted file mode 100644 index 6603107..0000000 --- a/lib/src/composable/request_utils.dart +++ /dev/null @@ -1,99 +0,0 @@ -import '../event.dart'; - -/// Returns the query params from request URI. -/// -/// ## Example -/// ```dart -/// defineHandler((event) { -/// final query = getQuery(event); // {"key": "value", ...} -/// }); -///``` -Map getQuery(Event event) { - return event.uri.queryParameters; -} - -/// Returns the query params all values from request URI. -/// -/// ## Example -/// ```dart -/// defineHandler((event) { -/// final queryAll = getQueryParamsAll(event); // {"key": ["value1", "value2"]} -/// }); -/// ``` -Map> getQueryAll(Event event) { - return event.uri.queryParametersAll; -} - -/// Returns validated query params. -T getValidatedQuery( - Event event, T Function(Map> query) validator) { - return validator(event.uri.queryParametersAll); -} - -/// Returns request header entries. -Iterable<(String, String)> getHeaderEntries(Event event) { - return event.raw.request.headers; -} - -/// Returns request header values -Iterable getHeaderValues(Event event, String name) { - final lowerName = name.toLowerCase(); - - return getHeaderEntries(event) - .where((e) => e.$1.toLowerCase() == lowerName) - .map((e) => e.$2.trim()); -} - -/// Returns request header. -String getHeader(Event event, String name) { - return getHeaderValues(event, name).join(', ').trim(); -} - -/// Returns the request hostname -String getRequestHost(Event event, {bool forwarded = false}) { - if (forwarded) { - return switch (getHeader(event, 'x-forwarded-host')) { - String(isEmpty: true) => event.uri.host, - String host => host, - }; - } - - return event.uri.scheme; -} - -/// Returns the request protocol. -String getRequestProtocol(Event event, {bool forwarded = false}) { - if (forwarded) { - return switch (getHeader(event, 'x-forwarded-proto')) { - String(isEmpty: true) => event.uri.scheme, - String proto => proto, - }; - } - - return event.uri.scheme; -} - -/// Returns the request [Uri]. -Uri getRequestURI(Event event, {bool forwarded = false}) { - if (!forwarded) return event.uri; - - return event.uri.replace( - scheme: getRequestProtocol(event, forwarded: true), - host: getRequestHost(event, forwarded: true), - ); -} - -/// Returns client address -String? getClientAddress(Event event, {bool forwarded = false}) { - if (event.raw.clientAddress != null && - event.raw.clientAddress?.isNotEmpty == true) { - return event.raw.clientAddress!; - } else if (forwarded) { - return switch (getHeaderValues(event, 'x-forwarded-for')) { - Iterable(isEmpty: true) => null, - Iterable(last: final address) => address, - }; - } - - return null; -} diff --git a/lib/src/context+app.dart b/lib/src/context+app.dart deleted file mode 100644 index f6ef6a3..0000000 --- a/lib/src/context+app.dart +++ /dev/null @@ -1,9 +0,0 @@ -// ignore_for_file: file_names - -import '_core_keys.dart'; -import 'app.dart'; -import 'context.dart'; - -extension ContextApp on Context { - App get app => get(kAppInstance); -} diff --git a/lib/src/context.dart b/lib/src/context.dart deleted file mode 100644 index c9f8750..0000000 --- a/lib/src/context.dart +++ /dev/null @@ -1,7 +0,0 @@ -abstract interface class Context { - T get(Object key); - T? getOrNull(Object key); - T set(Object key, T value); - T upsert(Object key, T Function() create); - bool has(Object key); -} diff --git a/lib/src/event.dart b/lib/src/event.dart index 824bcc4..d29f67a 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -1,17 +1 @@ -import 'context.dart'; -import 'request.dart'; -import 'response.dart'; - -abstract interface class RawEvent { - Context get context; - Request get request; - Response get response; - String? get clientAddress; -} - -extension type const Event._(RawEvent raw) { - Uri get uri => raw.request.uri; - String get method => raw.request.method.toUpperCase(); -} - -Event createRequestEvent(RawEvent raw) => Event._(raw); +abstract interface class Event {} diff --git a/lib/src/handler.dart b/lib/src/handler.dart deleted file mode 100644 index d092020..0000000 --- a/lib/src/handler.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'event.dart'; - -abstract interface class Handler { - Future handle(Event event); -} diff --git a/lib/src/http/headers/headers+get.dart b/lib/src/http/headers/headers+get.dart new file mode 100644 index 0000000..6ad73a1 --- /dev/null +++ b/lib/src/http/headers/headers+get.dart @@ -0,0 +1,17 @@ +// ignore_for_file: file_names + +import 'headers.dart'; + +extension HeadersGet on Headers { + Iterable getAll(String name) { + final normalizedName = name.toLowerCase(); + return where((e) => e.$1.toLowerCase() == normalizedName).map((e) => e.$2); + } + + String? get(String name) { + return switch (getAll(name)) { + Iterable(isNotEmpty: true, join: final join) => join(', '), + _ => null, + }; + } +} diff --git a/lib/src/http/headers/headers+has.dart b/lib/src/http/headers/headers+has.dart new file mode 100644 index 0000000..d5d23aa --- /dev/null +++ b/lib/src/http/headers/headers+has.dart @@ -0,0 +1,10 @@ +// ignore_for_file: file_names + +import 'headers.dart'; + +extension HeadersHas on Headers { + bool has(String name) { + final normalizedName = name.toLowerCase(); + return any((e) => e.$1.toLowerCase() == normalizedName); + } +} diff --git a/lib/src/http/headers/headers+keys.dart b/lib/src/http/headers/headers+keys.dart new file mode 100644 index 0000000..96bcdc8 --- /dev/null +++ b/lib/src/http/headers/headers+keys.dart @@ -0,0 +1,9 @@ +// ignore_for_file: file_names + +import 'headers.dart'; + +extension HeadersKeys on Headers { + Iterable get keys { + return map((e) => e.$1).toSet(); + } +} diff --git a/lib/src/http/headers/headers+rebuild.dart b/lib/src/http/headers/headers+rebuild.dart new file mode 100644 index 0000000..63364a9 --- /dev/null +++ b/lib/src/http/headers/headers+rebuild.dart @@ -0,0 +1,14 @@ +// ignore_for_file: file_names + +import 'headers.dart'; +import 'headers_builder.dart'; +import 'headers+to_builder.dart'; + +extension HeadersRebuild on Headers { + Headers rebuild(void Function(HeadersBuilder builder) updates) { + final builder = toBuilder(); + updates(builder); + + return builder.toHeaders(); + } +} diff --git a/lib/src/http/headers/headers+to_builder.dart b/lib/src/http/headers/headers+to_builder.dart new file mode 100644 index 0000000..c31c00b --- /dev/null +++ b/lib/src/http/headers/headers+to_builder.dart @@ -0,0 +1,8 @@ +// ignore_for_file: file_names + +import 'headers.dart'; +import 'headers_builder.dart'; + +extension HeadersToBuilder on Headers { + HeadersBuilder toBuilder() => HeadersBuilder(this); +} diff --git a/lib/src/http/headers/headers.dart b/lib/src/http/headers/headers.dart new file mode 100644 index 0000000..0d21b15 --- /dev/null +++ b/lib/src/http/headers/headers.dart @@ -0,0 +1,13 @@ +abstract interface class Headers implements Iterable<(String, String)> { + const factory Headers([Iterable<(String, String)> init]) = _HeadersImpl; +} + +final class _HeadersImpl extends Iterable<(String, String)> implements Headers { + const _HeadersImpl([this.locals = const []]); + + final Iterable<(String, String)> locals; + + @override + // TODO: implement iterator + Iterator<(String, String)> get iterator => locals.iterator; +} diff --git a/lib/src/http/headers/headers_builder+set.dart b/lib/src/http/headers/headers_builder+set.dart new file mode 100644 index 0000000..d351d21 --- /dev/null +++ b/lib/src/http/headers/headers_builder+set.dart @@ -0,0 +1,10 @@ +// ignore_for_file: file_names + +import 'headers_builder.dart'; + +extension HeadersBuilderSet on HeadersBuilder { + void set(String name, String value) { + remove(name); + add(name, value); + } +} diff --git a/lib/src/http/headers/headers_builder.dart b/lib/src/http/headers/headers_builder.dart new file mode 100644 index 0000000..ae9bb8e --- /dev/null +++ b/lib/src/http/headers/headers_builder.dart @@ -0,0 +1,39 @@ +import 'headers.dart'; + +abstract interface class HeadersBuilder { + factory HeadersBuilder([Iterable<(String, String)>? init]) { + final builder = _HeadersBuilderImpl(); + + if (init != null && init.isNotEmpty) { + for (final (name, value) in init) { + builder.add(name, value); + } + } + + return builder; + } + + void remove(String name); + void add(String name, String value); + Headers toHeaders(); +} + +final class _HeadersBuilderImpl implements HeadersBuilder { + final locals = <(String, String)>[]; + + @override + void add(String name, String value) { + if (name.isNotEmpty && value.isNotEmpty) { + locals.add((name.toLowerCase(), value)); + } + } + + @override + void remove(String name) { + final normalizedName = name.toLowerCase(); + locals.removeWhere((element) => element.$1 == normalizedName); + } + + @override + Headers toHeaders() => Headers(locals); +} diff --git a/lib/src/http/http_message/http_message+json.dart b/lib/src/http/http_message/http_message+json.dart new file mode 100644 index 0000000..a4e89f1 --- /dev/null +++ b/lib/src/http/http_message/http_message+json.dart @@ -0,0 +1,15 @@ +// ignore_for_file: file_names + +import 'dart:convert'; + +import 'http_message.dart'; +import 'http_message+text.dart'; + +extension HttpMessageJson on HttpMessage { + Future json() async { + return switch (await text()) { + String text => jsonDecode(text), + _ => null, + }; + } +} diff --git a/lib/src/http/http_message/http_message+text.dart b/lib/src/http/http_message/http_message+text.dart new file mode 100644 index 0000000..ad68154 --- /dev/null +++ b/lib/src/http/http_message/http_message+text.dart @@ -0,0 +1,14 @@ +// ignore_for_file: file_names + +import 'dart:typed_data'; + +import 'http_message.dart'; + +extension HttpMessageText on HttpMessage { + Future text() async { + return switch (body) { + Stream stream => encoding.decodeStream(stream), + _ => null, + }; + } +} diff --git a/lib/src/http/http_message/http_message.dart b/lib/src/http/http_message/http_message.dart new file mode 100644 index 0000000..77c645b --- /dev/null +++ b/lib/src/http/http_message/http_message.dart @@ -0,0 +1,10 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import '../headers/headers.dart'; + +abstract interface class HttpMessage { + Encoding get encoding; + Headers get headers; + Stream? get body; +} diff --git a/lib/src/http/http_status_reason_phrase.dart b/lib/src/http/http_status_reason_phrase.dart new file mode 100644 index 0000000..357b1fa --- /dev/null +++ b/lib/src/http/http_status_reason_phrase.dart @@ -0,0 +1,91 @@ +/// HTTP status code reason phrases. +extension HttpStatusReasonPhrase on int { + /// Returns the reason phrase for the HTTP status code. + String get httpStatusReasonPhrase { + return switch (this) { + // 1xx Informational + 100 => 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + + // 2xx Success + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', + 208 => 'Already Reported', + 226 => 'IM Used', + + // 3xx Redirection + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 306 => 'unused', + 307 => 'Temporary Redirect', + 308 => 'Permanent Redirect', + + // 4xx Client Error + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Payload Too Large', + 414 => 'URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Range Not Satisfiable', + 417 => 'Expectation Failed', + 418 => "I'm a teapot", + 421 => 'Misdirected Request', + 422 => 'Unprocessable Entity', + 423 => 'Locked', + 424 => 'Failed Dependency', + 425 => 'Too Early', + 426 => 'Upgrade Required', + 428 => 'Precondition Required', + 429 => 'Too Many Requests', + 431 => 'Request Header Fields Too Large', + 451 => 'Unavailable For Legal Reasons', + + // 5xx Server Error + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 506 => 'Variant Also Negotiates', + 507 => 'Insufficient Storage', + 508 => 'Loop Detected', + 510 => 'Not Extended', + 511 => 'Network Authentication Required', + + // Unofficial codes + 103 => 'Checkpoint', + 420 => 'Enhance Your Calm', + 450 => 'Blocked by Windows Parental Controls', + 498 => 'Invalid Token', + 499 => 'Token Required', + 509 => 'Bandwidth Limit Exceeded', + 530 => 'Site is frozen', + 598 => 'Network read timeout error', + 599 => 'Network connect timeout error', + _ => 'Unknown', + }; + } +} diff --git a/lib/src/http/request.dart b/lib/src/http/request.dart new file mode 100644 index 0000000..c2747b7 --- /dev/null +++ b/lib/src/http/request.dart @@ -0,0 +1,3 @@ +import 'http_message/http_message.dart'; + +abstract interface class Request implements HttpMessage {} diff --git a/lib/src/http/response.dart b/lib/src/http/response.dart new file mode 100644 index 0000000..408a58b --- /dev/null +++ b/lib/src/http/response.dart @@ -0,0 +1,117 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'headers/headers.dart'; +import 'headers/headers+rebuild.dart'; +import 'headers/headers_builder+set.dart'; +import 'http_message/http_message.dart'; +import 'http_status_reason_phrase.dart'; + +abstract interface class Response implements HttpMessage { + const factory Response( + final Stream? body, { + final int status, + final String statusText, + final Headers headers, + final Encoding encoding, + }) = _ResponseImpl; + + factory Response.text( + final String body, { + final int status = 200, + final String? statusText, + final Headers headers = const Headers(), + final Encoding encoding = utf8, + }) { + return _ResponseImpl( + Stream.value(Uint8List.fromList(encoding.encode(body))), + status: status, + statusText: statusText, + headers: headers + .resetOf('content-length', body.length.toString()) + .resetOf('content-type', 'text/plain; charset=utf-8'), + ); + } + + factory Response.json( + final body, { + final int status = 200, + final String? statusText, + final Headers headers = const Headers(), + final Encoding encoding = utf8, + }) { + final bytes = Uint8List.fromList(encoding.encode(json.encode(body))); + + return _ResponseImpl( + Stream.value(bytes), + status: status, + statusText: statusText, + headers: headers + .resetOf('content-length', bytes.lengthInBytes.toString()) + .resetOf('content-type', 'application/json; charset=utf-8'), + ); + } + + factory Response.formURLEncoded( + final Map form, { + final int status = 200, + final String? statusText, + final Headers headers = const Headers(), + final Encoding encoding = utf8, + }) { + final text = form.entries.map((e) { + return '${Uri.encodeQueryComponent(e.key)}=${Uri.encodeQueryComponent(e.value)}'; + }).join('&'); + final bytes = Uint8List.fromList(encoding.encode(text)); + + return _ResponseImpl( + Stream.value(bytes), + status: status, + statusText: statusText, + headers: headers + .resetOf('content-length', bytes.lengthInBytes.toString()) + .resetOf('content-type', + 'application/x-www-form-urlencoded; charset=utf-8'), + ); + } + + int get status; + String get statusText; +} + +final class _ResponseImpl implements Response { + const _ResponseImpl( + this.body, { + this.status = 200, + final String? statusText, + this.headers = const Headers(), + final Encoding? encoding, + }) : statusReasonPhrase = statusText, + _encoding = encoding; + + final String? statusReasonPhrase; + final Encoding? _encoding; + + @override + Encoding get encoding => _encoding ?? utf8; + + @override + final int status; + + @override + final Headers headers; + + @override + final Stream? body; + + @override + String get statusText => statusReasonPhrase ?? status.httpStatusReasonPhrase; +} + +extension on Headers { + Headers resetOf(String name, String value) { + return rebuild((builder) { + builder.set(name, value); + }); + } +} diff --git a/lib/src/locals/locals+get_or_null.dart b/lib/src/locals/locals+get_or_null.dart new file mode 100644 index 0000000..f4e6810 --- /dev/null +++ b/lib/src/locals/locals+get_or_null.dart @@ -0,0 +1,13 @@ +// ignore_for_file: file_names + +import 'locals.dart'; + +extension LocalsGetOrNull on Locals { + T? getOrNull(Object key) { + try { + return get(key); + } catch (_) { + return null; + } + } +} diff --git a/lib/src/locals/locals.dart b/lib/src/locals/locals.dart new file mode 100644 index 0000000..b9f479a --- /dev/null +++ b/lib/src/locals/locals.dart @@ -0,0 +1,16 @@ +abstract final class Locals { + T get(Object key); + void set(Object key, T value); +} + +final class LocalsImpl implements Locals { + final Map locals = {}; + + @override + T get(Object key) => locals[key]; + + @override + void set(Object key, T value) { + locals[key] = value; + } +} diff --git a/lib/src/request.dart b/lib/src/request.dart deleted file mode 100644 index 8ab78a0..0000000 --- a/lib/src/request.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'dart:typed_data'; - -abstract interface class Request { - String get method; - Uri get uri; - Iterable<(String, String)> get headers; - Stream get body; -} diff --git a/lib/src/response.dart b/lib/src/response.dart deleted file mode 100644 index a3878ed..0000000 --- a/lib/src/response.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'dart:typed_data'; - -abstract interface class Response { - abstract int status; - abstract String statusText; - List<(String, String)> get headers; - Stream? body; -} diff --git a/lib/src/routing/_routing_keys.dart b/lib/src/routing/_routing_keys.dart deleted file mode 100644 index bba37cd..0000000 --- a/lib/src/routing/_routing_keys.dart +++ /dev/null @@ -1,4 +0,0 @@ -const kRouter = #spry.router; -const kRoute = #spry.route.instance; -const kParams = #spry.route.params; -const kAllMethod = '#SPRY.__ALL_METHOD__'; diff --git a/lib/src/routing/composable.dart b/lib/src/routing/composable.dart deleted file mode 100644 index 1f09376..0000000 --- a/lib/src/routing/composable.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:routingkit/routingkit.dart' as routingkit; - -import '../composable/get_context.dart'; -import '../event.dart'; -import '_routing_keys.dart'; -import 'route.dart'; - -typedef Params = routingkit.Params; - -Params getRouteParams(Event event) { - return getContext(event).upsert(kParams, () => Params()); -} - -T getValidatedRouteParams(Event event, T Function(Params params) validator) { - return validator(getRouteParams(event)); -} - -String? getRouteCatchallParam(Event event) => getRouteParams(event).catchall; -String? getRouteParam(Event event, String name) => - getRouteParams(event).call(name); -Iterable getRouteParamValues(Event event, String name) => - getRouteParams(event).valuesOf(name); - -Route? getRoute(Event event) => getContext(event).getOrNull(kRoute); - -String makeRoutePath( - Event event, - String route, { - Map? params, - Iterable? wildcard, - String? catchall, -}) { - return getContext(event) - .get(kRouter) - .buildPath(route, params: params, wildcard: wildcard, catchall: catchall); -} diff --git a/lib/src/routing/route.dart b/lib/src/routing/route.dart deleted file mode 100644 index d99f7df..0000000 --- a/lib/src/routing/route.dart +++ /dev/null @@ -1,3 +0,0 @@ -abstract interface class Route { - String get id; -} diff --git a/lib/src/routing/router.dart b/lib/src/routing/router.dart deleted file mode 100644 index af2edd4..0000000 --- a/lib/src/routing/router.dart +++ /dev/null @@ -1,6 +0,0 @@ -import '../handler.dart'; -import 'routes_builder.dart'; - -abstract interface class Router implements Handler, RoutesBuilder { - void use(Handler handler); -} diff --git a/lib/src/routing/routes_builder+group.dart b/lib/src/routing/routes_builder+group.dart deleted file mode 100644 index 7a29378..0000000 --- a/lib/src/routing/routes_builder+group.dart +++ /dev/null @@ -1,51 +0,0 @@ -// ignore_for_file: file_names - -import '../handler.dart'; -import '../utils/define_stack_handler.dart'; -import 'routes_builder.dart'; - -extension RoutesBuilderGroup on RoutesBuilder { - RoutesBuilder grouped({String? route, Handler? handler}) { - return _GroupedRoutesBuilder( - parent: this, - route: route, - handler: handler, - ); - } - - void group(void Function(RoutesBuilder routes) closure, - {String? route, Handler? handler}) { - return closure(grouped(route: route, handler: handler)); - } -} - -class _GroupedRoutesBuilder implements RoutesBuilder { - const _GroupedRoutesBuilder({ - required this.parent, - required this.route, - required this.handler, - }); - - final RoutesBuilder parent; - final String? route; - final Handler? handler; - - @override - void on(String method, String route, Handler handler) { - parent.on(method, resolveRoute(route), resolveHandler(handler)); - } - - String resolveRoute(String route) { - return switch (this.route) { - String prefix => '$prefix/$route', - _ => route, - }; - } - - Handler resolveHandler(Handler handler) { - return switch (this.handler) { - Handler parent => defineStackHandler([parent, handler]), - _ => handler, - }; - } -} diff --git a/lib/src/routing/routes_builder+methods.dart b/lib/src/routing/routes_builder+methods.dart deleted file mode 100644 index 2294fc0..0000000 --- a/lib/src/routing/routes_builder+methods.dart +++ /dev/null @@ -1,15 +0,0 @@ -// ignore_for_file: file_names - -import '../handler.dart'; -import '_routing_keys.dart'; -import 'routes_builder.dart'; - -extension RoutesBuilderMethods on RoutesBuilder { - void all(String route, Handler handler) => on(kAllMethod, route, handler); - void get(String route, Handler handler) => on('GET', route, handler); - void post(String route, Handler handler) => on('POST', route, handler); - void put(String route, Handler handler) => on('PUT', route, handler); - void patch(String route, Handler handler) => on('PATCH', route, handler); - void delete(String route, Handler handler) => on('DELETE', route, handler); - void head(String route, Handler handler) => on('HEAD', route, handler); -} diff --git a/lib/src/routing/routes_builder.dart b/lib/src/routing/routes_builder.dart deleted file mode 100644 index 6d3c296..0000000 --- a/lib/src/routing/routes_builder.dart +++ /dev/null @@ -1,5 +0,0 @@ -import '../handler.dart'; - -abstract interface class RoutesBuilder { - void on(String method, String route, Handler handler); -} diff --git a/lib/src/routing/utils/create_router.dart b/lib/src/routing/utils/create_router.dart deleted file mode 100644 index 94212f6..0000000 --- a/lib/src/routing/utils/create_router.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:routingkit/routingkit.dart' as routingkit; - -import '../../composable/get_context.dart'; -import '../../composable/next.dart'; -import '../../event.dart'; -import '../../handler.dart'; -import '../_routing_keys.dart'; -import '../route.dart'; -import '../router.dart'; - -Router createRouter() { - final inner = routingkit.createRouter(); - - return _RouterImpl(inner); -} - -typedef _HandleWith = Future Function(Event, Handler); - -final class _RouterImpl implements Router { - _RouterImpl(this.inner); - - _HandleWith? handleWith; - final handlerStack = []; - final routingkit.Router inner; - - @override - Future handle(Event event) { - final context = getContext(event); - final result = switch (event.method) { - 'HEAD' => switch (inner.lookup('HEAD/${event.uri.path}')) { - routingkit.Result result => result, - _ => inner.lookup('GET/${event.uri.path}'), - }, - String method => switch (inner.lookup('$method/${event.uri.path}')) { - routingkit.Result result => result, - _ => inner.lookup('$kAllMethod/${event.uri.path}'), - }, - }; - - if (result == null) { - throw 111; - } - - context.set(kRouter, inner); - context.set(kParams, result.params); - context.set(kRoute, _RouteImpl(result.route.split('/').skip(1).join('/'))); - - return switch (handleWith) { - _HandleWith handle => handle(event, result.value), - _ => result.value.handle(event), - }; - } - - @override - void use(Handler handler) { - handlerStack.add(handler); - handleWith = handlerStack.reversed.fold<_HandleWith>( - defaultRouterHandle, - (child, current) => (event, handler) async { - setNext(() => child(event, handler)); - - return current.handle(event); - }, - ); - } - - @override - void on(String method, String route, Handler handler) { - inner.register('${method.toUpperCase()}/$route', handler); - } - - static Future defaultRouterHandle(Event event, Handler handler) => - handler.handle(event); -} - -final class _RouteImpl implements Route { - const _RouteImpl(this.id); - - @override - final String id; -} diff --git a/lib/src/spry.dart b/lib/src/spry.dart new file mode 100644 index 0000000..9eec8b1 --- /dev/null +++ b/lib/src/spry.dart @@ -0,0 +1,5 @@ +import 'locals/locals.dart'; + +class Spry { + final Locals locals = LocalsImpl(); +} diff --git a/lib/src/utils/create_app.dart b/lib/src/utils/create_app.dart deleted file mode 100644 index 0c90e79..0000000 --- a/lib/src/utils/create_app.dart +++ /dev/null @@ -1,70 +0,0 @@ -import '../_core_keys.dart'; -import '../app.dart'; -import '../context.dart'; -import '../handler.dart'; -import 'define_stack_handler.dart'; - -App createApp({Map? locals}) { - final context = _AppContext(); - if (locals != null && locals.isNotEmpty) { - for (final e in locals.entries) { - context.set(e.key, e.value); - } - } - - final app = _AppImpl(context); - context.set(kAppInstance, app); - - return app; -} - -final class _AppImpl implements App { - _AppImpl(this.context); - - final handlerStack = []; - - @override - final Context context; - - @override - Handler get handler => defineStackHandler(handlerStack); - - @override - void use(Handler handler) { - handlerStack.add(handler); - } -} - -final class _AppContext implements Context { - final Map locals = {}; - - @override - T? getOrNull(Object key) { - return switch (locals[key]) { - T value => value, - _ => null, - }; - } - - @override - T get(Object key) { - return switch (getOrNull(key)) { - T value => value, - _ => throw 111, - }; - } - - @override - bool has(Object key) => locals.containsKey(key); - - @override - T set(Object key, T value) => locals[key] = value; - - @override - T upsert(Object key, T Function() create) { - return switch (locals[key]) { - T value => value, - _ => set(key, create()), - }; - } -} diff --git a/lib/src/utils/create_event_context.dart b/lib/src/utils/create_event_context.dart deleted file mode 100644 index b01b8e0..0000000 --- a/lib/src/utils/create_event_context.dart +++ /dev/null @@ -1,56 +0,0 @@ -import '../app.dart'; -import '../context.dart'; - -Context createEventContext(App app, [Map? locals]) { - final context = _RequestEventContext(app); - - if (locals != null && locals.isNotEmpty) { - for (final e in locals.entries) { - context.set(e.key, e.value); - } - } - - return context; -} - -class _RequestEventContext implements Context { - _RequestEventContext(this.app); - - final App app; - final Map locals = {}; - - @override - T? getOrNull(Object key) { - return switch (locals[key]) { - T value => value, - _ => app.context.getOrNull(key), - }; - } - - @override - T get(Object key) { - return switch (getOrNull(key)) { - T value => value, - _ => throw 111, - }; - } - - @override - bool has(Object key) { - return switch (locals.containsKey(key)) { - false => app.context.has(key), - _ => true, - }; - } - - @override - T set(Object key, T value) => locals[key] = value; - - @override - T upsert(Object key, T Function() create) { - return switch (has(key)) { - true => get(key), - _ => set(key, create()), - }; - } -} diff --git a/lib/src/utils/define_handler.dart b/lib/src/utils/define_handler.dart deleted file mode 100644 index f1fd9e1..0000000 --- a/lib/src/utils/define_handler.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'dart:async'; - -import '../event.dart'; -import '../handler.dart'; - -Handler defineHandler(FutureOr Function(Event event) handle) { - return _ClosureHandler(handle); -} - -final class _ClosureHandler implements Handler { - const _ClosureHandler(this._closure); - - final FutureOr Function(Event event) _closure; - - @override - Future handle(Event event) async { - return _closure(event); - } -} diff --git a/lib/src/utils/define_stack_handler.dart b/lib/src/utils/define_stack_handler.dart deleted file mode 100644 index b1b4be2..0000000 --- a/lib/src/utils/define_stack_handler.dart +++ /dev/null @@ -1,26 +0,0 @@ -import '../composable/next.dart'; -import '../handler.dart'; -import '../event.dart'; -import 'define_handler.dart'; - -Handler defineStackHandler(Iterable handlers) { - final handle = handlers.reversed.fold Function(Event)>( - (_) async {}, - (child, handler) => (event) async { - setNext(() => child(event)); - - return handler.handle(event); - }, - ); - - return defineHandler(handle); -} - -extension on Iterable { - Iterable get reversed { - return switch (this) { - List(reversed: final reversed) => reversed, - _ => toList().reversed, - }; - } -} diff --git a/pubspec.yaml b/pubspec.yaml index 4ae6316..e2b4edd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,16 +1,13 @@ name: spry description: A starting point for Dart libraries or applications. version: 1.0.0 -# repository: https://github.com/my_org/my_repo environment: sdk: ^3.4.3 -# Add regular dependencies here. dependencies: routingkit: ^1.1.0 - # path: ^1.8.0 dev_dependencies: - lints: ^3.0.0 - test: ^1.24.0 + lints: ^4.0.0 + test: ^1.25.8 diff --git a/test/http/headers_builder_test.dart b/test/http/headers_builder_test.dart new file mode 100644 index 0000000..df1b06d --- /dev/null +++ b/test/http/headers_builder_test.dart @@ -0,0 +1,49 @@ +import 'package:spry/spry.dart'; +import 'package:test/test.dart'; + +void main() { + test('init values', () { + expect(HeadersBuilder().toHeaders(), isEmpty); + expect(HeadersBuilder([('a', '1')]).toHeaders(), [('a', '1')]); + }); + + test('.add', () { + final builder = HeadersBuilder(); + + builder.add('a', '1'); + builder.add('a', '1'); + builder.add('b', '1'); + + final headers = builder.toHeaders(); + expect(headers.length, equals(3)); + expect(headers.get('a'), equals('1, 1')); + expect(headers.get('b'), equals('1')); + expect(headers.get('c'), isNull); + }); + + test('.remove', () { + final builder = HeadersBuilder([ + ('a', '1'), + ('a', '1'), + ('b', '1'), + ]); + + builder.remove('a'); + + expect(builder.toHeaders(), [('b', '1')]); + }); + + test('.toHeaders', () { + expect(HeadersBuilder().toHeaders(), isA()); + }); + + test('.set', () { + final builder = HeadersBuilder([ + ('a', '1'), + ('a', '1'), + ]); + + builder.set('a', '2'); + expect(builder.toHeaders(), [('a', '2')]); + }); +} diff --git a/test/http/headers_test.dart b/test/http/headers_test.dart new file mode 100644 index 0000000..b79d6ed --- /dev/null +++ b/test/http/headers_test.dart @@ -0,0 +1,94 @@ +import 'package:spry/spry.dart'; +import 'package:test/test.dart'; + +void main() { + test('impl Iterable<(String, String)>', () { + expect(Headers(), isA>()); + }); + + test('init values', () { + expect(Headers(), isEmpty); + + final headers = Headers([ + ('a', '1'), + ('a', '2'), + ('b', '1'), + ]); + expect(headers.length, equals(3)); + }); + + test('name ignore case', () { + final headers = Headers([('BaR', 'foo')]); + + expect(headers.getAll('bAr'), ['foo']); + expect(headers.get('bar'), 'foo'); + }); + + test('.getAll', () { + final headers = Headers([ + ('a', '1'), + ('a', '2'), + ('b', '1'), + ]); + + expect(headers.getAll('a'), ['1', '2']); + expect(headers.getAll('b'), ['1']); + expect(headers.getAll('c'), isEmpty); + }); + + test('.get', () { + final headers = Headers([ + ('a', '1'), + ('a', '2'), + ('b', '1'), + ]); + + expect(headers.get('a'), equals('1, 2')); + expect(headers.get('b'), equals('1')); + expect(headers.get('c'), isNull); + }); + + test('.has', () { + final headers = Headers([ + ('a', '1'), + ('a', '2'), + ('b', '1'), + ]); + + expect(headers.has('a'), equals(true)); + expect(headers.has('b'), equals(true)); + expect(headers.has('c'), equals(false)); + }); + + test('.keys', () { + final headers = Headers([ + ('a', '1'), + ('a', '2'), + ('b', '1'), + ]); + + expect(headers.keys, ['a', 'b']); + }); + + test('.rebuild', () { + bool rebuildEffect = false; + final headers = Headers([('a', '1')]); + final rebuilt = headers.rebuild((builder) { + builder.add('b', '1'); + builder.remove('a'); + rebuildEffect = true; + }); + + expect(headers.length, equals(1)); + expect(headers.get('a'), equals('1')); + expect(rebuilt.get('a'), isNull); + expect(rebuilt.get('b'), equals('1')); + expect(rebuildEffect, equals(true)); + }); + + test('.toBuilder', () { + final headers = Headers(); + + expect(headers.toBuilder(), isA()); + }); +} diff --git a/test/http/http_message_test.dart b/test/http/http_message_test.dart new file mode 100644 index 0000000..bee1ec9 --- /dev/null +++ b/test/http/http_message_test.dart @@ -0,0 +1,38 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:spry/spry.dart'; +import 'package:test/test.dart'; + +final class TestHttpMessage implements HttpMessage { + const TestHttpMessage({ + this.headers = const Headers(), + this.body, + }); + + @override + final Stream? body; + + @override + final Headers headers; + + @override + Utf8Codec get encoding => utf8; +} + +void main() { + test('.text', () async { + final value = 'abc123'; + final message = TestHttpMessage(body: Stream.value(utf8.encode(value))); + + expect(await message.text(), equals(value)); + }); + + test('.json', () async { + final value = [1, 'a', 2.3]; + final message = + TestHttpMessage(body: Stream.value(utf8.encode(json.encode(value)))); + + expect(await message.json(), value); + }); +} diff --git a/test/http/response_test.dart b/test/http/response_test.dart new file mode 100644 index 0000000..f20e9a9 --- /dev/null +++ b/test/http/response_test.dart @@ -0,0 +1,60 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:spry/spry.dart'; +import 'package:test/test.dart'; + +void main() { + test('default factory', () async { + const res = Response(null); + + expect(res, equals(const Response(null))); + expect(res.body, isNull); + expect(res.status, equals(200)); + expect(res.statusText, equals('OK')); + expect(await res.text(), isNull); + }); + + test('text factory', () async { + final res = Response.text('foo'); + final contentType = ContentType.parse(res.headers.get('content-type')!); + + expect(res.headers.get('content-length'), equals('3')); + expect(contentType.primaryType, equals('text')); + expect(contentType.subType, equals('plain')); + expect(contentType.charset, equals('utf-8')); + expect(await res.text(), equals('foo')); + }); + + test('json factory', () async { + final res = Response.json([1, 2]); + final contentType = ContentType.parse(res.headers.get('content-type')!); + + expect(contentType.primaryType, equals('application')); + expect(contentType.subType, equals('json')); + expect(contentType.charset, equals('utf-8')); + expect( + res.headers.get('content-length'), + equals(json.encode([1, 2]).length.toString()), + ); + expect(await res.json(), [1, 2]); + }); + + test('.status', () { + const res1 = Response(null, status: 200); + const res2 = Response(null, status: 999); + + expect(res1.status, equals(200)); + expect(res2.status, equals(999)); + }); + + test('.statusText', () { + const res1 = Response(null, status: 200); + const res2 = Response(null, status: 999); + const res3 = Response(null, statusText: 'test'); + + expect(res1.statusText, equals('OK')); + expect(res2.statusText, equals('Unknown')); + expect(res3.statusText, equals('test')); + }); +} diff --git a/test/locals/locals_test.dart b/test/locals/locals_test.dart new file mode 100644 index 0000000..a43b3df --- /dev/null +++ b/test/locals/locals_test.dart @@ -0,0 +1,37 @@ +import 'package:spry/spry.dart'; +import 'package:spry/src/locals/locals.dart'; +import 'package:test/test.dart'; + +void main() { + test('.set', () { + final locals = LocalsImpl(); + + locals.set('a', 1); + locals.set(#app, 2); + + expect(locals.locals['a'], equals(1)); + expect(locals.locals[#app], equals(2)); + expect(locals.locals[1], isNull); + }); + + test('.get', () { + final locals = LocalsImpl(); + + locals.set(#app, 1); + + expect(locals.get(#app), equals(1)); + expect(locals.get(#app), equals(1)); + expectLater(() => locals.get(#app), throwsA(isA())); + }); + + test('.getOrNull', () { + final locals = LocalsImpl(); + + locals.set(#app, 1); + + expect(locals.getOrNull(#app), equals(1)); + expect(locals.getOrNull(#app), equals(1)); + expect(locals.getOrNull(#app), isNull); + expect(locals.getOrNull('demo'), isNull); + }); +} From 66dd23624a2b3b00aff862f570882636fb7c78b0 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Sun, 7 Jul 2024 08:01:30 +0800 Subject: [PATCH 07/35] chore: WIP --- example/main.dart | 45 ++++++++++++++++++++++++++ example/plain.dart | 42 ------------------------ lib/spry.dart | 10 ++++++ lib/src/_constant.dart | 1 + lib/src/event.dart | 1 - lib/src/event/event+app.dart | 9 ++++++ lib/src/event/event.dart | 5 +++ lib/src/handler/_closure_handler.dart | 20 ++++++++++++ lib/src/handler/handler.dart | 6 ++++ lib/src/locals/_locals+get_or_set.dart | 16 +++++++++ lib/src/locals/locals.dart | 6 ++++ lib/src/spry+handler.dart | 37 +++++++++++++++++++++ lib/src/spry.dart | 5 +++ lib/src/utils/next.dart | 13 ++++++++ 14 files changed, 173 insertions(+), 43 deletions(-) create mode 100644 example/main.dart delete mode 100644 example/plain.dart create mode 100644 lib/src/_constant.dart delete mode 100644 lib/src/event.dart create mode 100644 lib/src/event/event+app.dart create mode 100644 lib/src/event/event.dart create mode 100644 lib/src/handler/_closure_handler.dart create mode 100644 lib/src/handler/handler.dart create mode 100644 lib/src/locals/_locals+get_or_set.dart create mode 100644 lib/src/spry+handler.dart create mode 100644 lib/src/utils/next.dart diff --git a/example/main.dart b/example/main.dart new file mode 100644 index 0000000..29e59c3 --- /dev/null +++ b/example/main.dart @@ -0,0 +1,45 @@ +import 'package:spry/spry.dart'; +import 'package:spry/src/_constant.dart'; +import 'package:spry/src/locals/locals.dart'; + +class ExampleEvent implements Event { + @override + final Locals locals = LocalsImpl(); +} + +void main() async { + final app = Spry(); + + app.use((event) { + print(1); + }); + + app.use((event) async { + final res = await next(event); + print(2); + + return res; + }); + + app.use((event) { + print(3); + }); + + app.use((event) { + print(4); + + // 中断向内嵌套,返回 Response 或者其他任何信息 + return const Response(null); + }); + + app.use((event) { + print('Unable to execute'); + }); + + final handle = app.handler.handle; + final event = ExampleEvent(); + + event.locals.set(kAppInstance, app); + + await handle(event); +} diff --git a/example/plain.dart b/example/plain.dart deleted file mode 100644 index aa3fa03..0000000 --- a/example/plain.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:spry/plain.dart'; -import 'package:spry/spry.dart'; - -main() async { - final app = createApp(); - - app.use(defineHandler((event) { - print(1); - next(); - })); - - app.use(defineHandler((event) { - print(event.method); - print(event.uri.path); - next(); - })); - - final router = createRouter(); - - router.get('/users/:name', defineHandler((event) { - print(getRouteParam(event, 'name')); - })); - - router.group(route: 'demo', (routes) { - routes.all('haha', defineHandler((event) { - print('3'); - })); - - routes.all('/', defineHandler((event) { - print('demo root'); - })); - }); - - app.use(router); - - final handler = toPlainHandler(app); - final request = createPlainRequest( - method: 'get', - uri: Uri.parse('/demo/haha'), - ); - await handler(request); -} diff --git a/lib/spry.dart b/lib/spry.dart index 51879c0..4d7d892 100644 --- a/lib/spry.dart +++ b/lib/spry.dart @@ -1,3 +1,8 @@ +export 'src/event/event.dart'; +export 'src/event/event+app.dart'; + +export 'src/handler/handler.dart'; + export 'src/http/headers/headers.dart'; export 'src/http/headers/headers+get.dart'; export 'src/http/headers/headers+has.dart'; @@ -16,3 +21,8 @@ export 'src/http/response.dart'; export 'src/locals/locals.dart' hide LocalsImpl; export 'src/locals/locals+get_or_null.dart'; + +export 'src/utils/next.dart'; + +export 'src/spry.dart'; +export 'src/spry+handler.dart'; diff --git a/lib/src/_constant.dart b/lib/src/_constant.dart new file mode 100644 index 0000000..256a8a5 --- /dev/null +++ b/lib/src/_constant.dart @@ -0,0 +1 @@ +const kAppInstance = #spry.app; diff --git a/lib/src/event.dart b/lib/src/event.dart deleted file mode 100644 index d29f67a..0000000 --- a/lib/src/event.dart +++ /dev/null @@ -1 +0,0 @@ -abstract interface class Event {} diff --git a/lib/src/event/event+app.dart b/lib/src/event/event+app.dart new file mode 100644 index 0000000..ac0aeab --- /dev/null +++ b/lib/src/event/event+app.dart @@ -0,0 +1,9 @@ +// ignore_for_file: file_names + +import '../_constant.dart'; +import '../spry.dart'; +import 'event.dart'; + +extension EventApp on Event { + Spry get app => locals.get(kAppInstance); +} diff --git a/lib/src/event/event.dart b/lib/src/event/event.dart new file mode 100644 index 0000000..ea91e90 --- /dev/null +++ b/lib/src/event/event.dart @@ -0,0 +1,5 @@ +import '../locals/locals.dart'; + +abstract interface class Event { + Locals get locals; +} diff --git a/lib/src/handler/_closure_handler.dart b/lib/src/handler/_closure_handler.dart new file mode 100644 index 0000000..0c1c9f6 --- /dev/null +++ b/lib/src/handler/_closure_handler.dart @@ -0,0 +1,20 @@ +import 'dart:async'; + +import '../event/event.dart'; +import '../http/response.dart'; +import '../utils/next.dart'; +import 'handler.dart'; + +final class ClosureHandler implements Handler { + const ClosureHandler(this.closure); + + final FutureOr Function(Event) closure; + + @override + Future handle(Event event) async { + return switch (await closure(event)) { + Response response => response, + _ => next(event), + }; + } +} diff --git a/lib/src/handler/handler.dart b/lib/src/handler/handler.dart new file mode 100644 index 0000000..d8be5af --- /dev/null +++ b/lib/src/handler/handler.dart @@ -0,0 +1,6 @@ +import '../event/event.dart'; +import '../http/response.dart'; + +abstract interface class Handler { + Future handle(Event event); +} diff --git a/lib/src/locals/_locals+get_or_set.dart b/lib/src/locals/_locals+get_or_set.dart new file mode 100644 index 0000000..aa3250e --- /dev/null +++ b/lib/src/locals/_locals+get_or_set.dart @@ -0,0 +1,16 @@ +// ignore_for_file: file_names + +import 'locals.dart'; + +extension LocalsGetOrSet on Locals { + T getOrSet(Object key, T Function() creates) { + try { + return get(key); + } catch (_) { + final value = creates(); + set(key, value); + + return value; + } + } +} diff --git a/lib/src/locals/locals.dart b/lib/src/locals/locals.dart index b9f479a..46fa882 100644 --- a/lib/src/locals/locals.dart +++ b/lib/src/locals/locals.dart @@ -1,6 +1,7 @@ abstract final class Locals { T get(Object key); void set(Object key, T value); + void remove(Object key); } final class LocalsImpl implements Locals { @@ -13,4 +14,9 @@ final class LocalsImpl implements Locals { void set(Object key, T value) { locals[key] = value; } + + @override + void remove(Object key) { + locals.remove(key); + } } diff --git a/lib/src/spry+handler.dart b/lib/src/spry+handler.dart new file mode 100644 index 0000000..a3b0f77 --- /dev/null +++ b/lib/src/spry+handler.dart @@ -0,0 +1,37 @@ +// ignore_for_file: file_names + +import 'dart:async'; + +import 'event/event.dart'; +import 'handler/_closure_handler.dart'; +import 'handler/handler.dart'; +import 'http/response.dart'; +import 'locals/_locals+get_or_set.dart'; +import 'utils/next.dart'; +import 'spry.dart'; + +extension SpryHandler on Spry { + Handler get handler { + final closure = handlers.reversed.fold(next, (effect, current) { + return (event) { + event.locals.set(next, effect); + + return current.handle(event); + }; + }); + + return ClosureHandler(closure); + } + + void use(FutureOr Function(Event event) closure) { + handlers.add(ClosureHandler(closure)); + } +} + +extension on Spry { + static const key = #spry.app.handlers; + + List get handlers { + return locals.getOrSet>(key, () => []); + } +} diff --git a/lib/src/spry.dart b/lib/src/spry.dart index 9eec8b1..f2bd07a 100644 --- a/lib/src/spry.dart +++ b/lib/src/spry.dart @@ -1,5 +1,10 @@ +import '_constant.dart'; import 'locals/locals.dart'; class Spry { + Spry() { + locals.set(kAppInstance, this); + } + final Locals locals = LocalsImpl(); } diff --git a/lib/src/utils/next.dart b/lib/src/utils/next.dart new file mode 100644 index 0000000..c15707e --- /dev/null +++ b/lib/src/utils/next.dart @@ -0,0 +1,13 @@ +import '../event/event.dart'; +import '../http/response.dart'; +import '../locals/locals+get_or_null.dart'; + +Future next(Event event) async { + final effect = event.locals.getOrNull Function(Event)>(next); + event.locals.remove(next); + + return switch (effect) { + Future Function(Event) next => next(event), + _ => const Response(null), + }; +} From c50be2a03e33803897bb5b0955d3c6967062fc55 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Sun, 7 Jul 2024 09:42:36 +0800 Subject: [PATCH 08/35] chore: WIP --- example/main.dart | 4 +- lib/spry.dart | 7 ++-- lib/src/_constant.dart | 1 - lib/src/event/event+app.dart | 3 +- lib/src/event/event.dart | 9 ++++- lib/src/handler/_closure_handler.dart | 1 + lib/src/locals/locals.dart | 27 +++++++++++++- lib/src/platform/platform+create_handler.dart | 20 ++++++++++ lib/src/platform/platform.dart | 13 +++++++ lib/src/platform/platform_handler.dart | 1 + lib/src/spry+create_platform_handler.dart | 11 ++++++ lib/src/spry+handler.dart | 37 ------------------- lib/src/spry+use.dart | 14 +++++++ lib/src/spry.dart | 7 +--- lib/src/utils/_spry_internal_utils.dart | 28 ++++++++++++++ 15 files changed, 130 insertions(+), 53 deletions(-) delete mode 100644 lib/src/_constant.dart create mode 100644 lib/src/platform/platform+create_handler.dart create mode 100644 lib/src/platform/platform.dart create mode 100644 lib/src/platform/platform_handler.dart create mode 100644 lib/src/spry+create_platform_handler.dart delete mode 100644 lib/src/spry+handler.dart create mode 100644 lib/src/spry+use.dart create mode 100644 lib/src/utils/_spry_internal_utils.dart diff --git a/example/main.dart b/example/main.dart index 29e59c3..5b267bf 100644 --- a/example/main.dart +++ b/example/main.dart @@ -1,5 +1,5 @@ import 'package:spry/spry.dart'; -import 'package:spry/src/_constant.dart'; + import 'package:spry/src/locals/locals.dart'; class ExampleEvent implements Event { @@ -39,7 +39,7 @@ void main() async { final handle = app.handler.handle; final event = ExampleEvent(); - event.locals.set(kAppInstance, app); + event.locals.set(Spry, app); await handle(event); } diff --git a/lib/spry.dart b/lib/spry.dart index 4d7d892..8f57ebd 100644 --- a/lib/spry.dart +++ b/lib/spry.dart @@ -1,4 +1,4 @@ -export 'src/event/event.dart'; +export 'src/event/event.dart' hide EventImpl; export 'src/event/event+app.dart'; export 'src/handler/handler.dart'; @@ -19,10 +19,11 @@ export 'src/http/http_message/http_message+json.dart'; export 'src/http/response.dart'; -export 'src/locals/locals.dart' hide LocalsImpl; +export 'src/locals/locals.dart' hide AppLocals, EventLocals; export 'src/locals/locals+get_or_null.dart'; export 'src/utils/next.dart'; export 'src/spry.dart'; -export 'src/spry+handler.dart'; +export 'src/spry+create_platform_handler.dart'; +export 'src/spry+use.dart'; diff --git a/lib/src/_constant.dart b/lib/src/_constant.dart deleted file mode 100644 index 256a8a5..0000000 --- a/lib/src/_constant.dart +++ /dev/null @@ -1 +0,0 @@ -const kAppInstance = #spry.app; diff --git a/lib/src/event/event+app.dart b/lib/src/event/event+app.dart index ac0aeab..23e4c78 100644 --- a/lib/src/event/event+app.dart +++ b/lib/src/event/event+app.dart @@ -1,9 +1,8 @@ // ignore_for_file: file_names -import '../_constant.dart'; import '../spry.dart'; import 'event.dart'; extension EventApp on Event { - Spry get app => locals.get(kAppInstance); + Spry get app => locals.get(Spry); } diff --git a/lib/src/event/event.dart b/lib/src/event/event.dart index ea91e90..3927013 100644 --- a/lib/src/event/event.dart +++ b/lib/src/event/event.dart @@ -1,5 +1,12 @@ import '../locals/locals.dart'; -abstract interface class Event { +abstract final class Event { Locals get locals; } + +final class EventImpl implements Event { + EventImpl(Locals appLocals) : locals = EventLocals(appLocals); + + @override + final Locals locals; +} diff --git a/lib/src/handler/_closure_handler.dart b/lib/src/handler/_closure_handler.dart index 0c1c9f6..2b91fc0 100644 --- a/lib/src/handler/_closure_handler.dart +++ b/lib/src/handler/_closure_handler.dart @@ -14,6 +14,7 @@ final class ClosureHandler implements Handler { Future handle(Event event) async { return switch (await closure(event)) { Response response => response, + // TODO _ => next(event), }; } diff --git a/lib/src/locals/locals.dart b/lib/src/locals/locals.dart index 46fa882..6168388 100644 --- a/lib/src/locals/locals.dart +++ b/lib/src/locals/locals.dart @@ -4,7 +4,7 @@ abstract final class Locals { void remove(Object key); } -final class LocalsImpl implements Locals { +final class AppLocals implements Locals { final Map locals = {}; @override @@ -20,3 +20,28 @@ final class LocalsImpl implements Locals { locals.remove(key); } } + +final class EventLocals implements Locals { + EventLocals(this.appLocals); + + final Locals appLocals; + final Map locals = {}; + + @override + T get(Object key) { + return switch (locals[key]) { + null => appLocals.get(key), + Object value => value as T, + }; + } + + @override + void remove(Object key) { + locals.remove(key); + } + + @override + void set(Object key, T value) { + locals[key] = value; + } +} diff --git a/lib/src/platform/platform+create_handler.dart b/lib/src/platform/platform+create_handler.dart new file mode 100644 index 0000000..1a2373b --- /dev/null +++ b/lib/src/platform/platform+create_handler.dart @@ -0,0 +1,20 @@ +// ignore_for_file: file_names + +import '../event/event.dart'; +import '../spry.dart'; +import '../utils/_spry_internal_utils.dart'; +import 'platform.dart'; +import 'platform_handler.dart'; + +extension PlatformAdapterCreateHandler on Platform { + PlatformHandler createHandler(Spry app) { + final handle = app.createHandle(); + + return (T request) async { + final event = EventImpl(app.locals); + final response = await handle(event); + + return respond(event, request, response); + }; + } +} diff --git a/lib/src/platform/platform.dart b/lib/src/platform/platform.dart new file mode 100644 index 0000000..d0827f7 --- /dev/null +++ b/lib/src/platform/platform.dart @@ -0,0 +1,13 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import '../event/event.dart'; +import '../http/headers/headers.dart'; +import '../http/response.dart'; + +abstract interface class Platform { + FutureOr getRequestURI(Event event, T request); + FutureOr getRequestHeaders(Event event, T request); + Stream? getRequestBody(Event event, T request); + FutureOr respond(Event event, T request, Response response); +} diff --git a/lib/src/platform/platform_handler.dart b/lib/src/platform/platform_handler.dart new file mode 100644 index 0000000..3bb7320 --- /dev/null +++ b/lib/src/platform/platform_handler.dart @@ -0,0 +1 @@ +typedef PlatformHandler = Future Function(T request); diff --git a/lib/src/spry+create_platform_handler.dart b/lib/src/spry+create_platform_handler.dart new file mode 100644 index 0000000..e09c668 --- /dev/null +++ b/lib/src/spry+create_platform_handler.dart @@ -0,0 +1,11 @@ +// ignore_for_file: file_names + +import 'platform/platform.dart'; +import 'platform/platform+create_handler.dart'; +import 'platform/platform_handler.dart'; +import 'spry.dart'; + +extension SpryCreatePlatfromHandler on Spry { + PlatformHandler createPlatformHandler(Platform platform) => + platform.createHandler(this); +} diff --git a/lib/src/spry+handler.dart b/lib/src/spry+handler.dart deleted file mode 100644 index a3b0f77..0000000 --- a/lib/src/spry+handler.dart +++ /dev/null @@ -1,37 +0,0 @@ -// ignore_for_file: file_names - -import 'dart:async'; - -import 'event/event.dart'; -import 'handler/_closure_handler.dart'; -import 'handler/handler.dart'; -import 'http/response.dart'; -import 'locals/_locals+get_or_set.dart'; -import 'utils/next.dart'; -import 'spry.dart'; - -extension SpryHandler on Spry { - Handler get handler { - final closure = handlers.reversed.fold(next, (effect, current) { - return (event) { - event.locals.set(next, effect); - - return current.handle(event); - }; - }); - - return ClosureHandler(closure); - } - - void use(FutureOr Function(Event event) closure) { - handlers.add(ClosureHandler(closure)); - } -} - -extension on Spry { - static const key = #spry.app.handlers; - - List get handlers { - return locals.getOrSet>(key, () => []); - } -} diff --git a/lib/src/spry+use.dart b/lib/src/spry+use.dart new file mode 100644 index 0000000..e0995d9 --- /dev/null +++ b/lib/src/spry+use.dart @@ -0,0 +1,14 @@ +// ignore_for_file: file_names + +import 'dart:async'; + +import 'event/event.dart'; +import 'handler/_closure_handler.dart'; +import 'utils/_spry_internal_utils.dart'; +import 'spry.dart'; + +extension SpryHandler on Spry { + void use(FutureOr Function(Event event) closure) { + addHandler(ClosureHandler(closure)); + } +} diff --git a/lib/src/spry.dart b/lib/src/spry.dart index f2bd07a..a1ee842 100644 --- a/lib/src/spry.dart +++ b/lib/src/spry.dart @@ -1,10 +1,5 @@ -import '_constant.dart'; import 'locals/locals.dart'; class Spry { - Spry() { - locals.set(kAppInstance, this); - } - - final Locals locals = LocalsImpl(); + late final Locals locals = AppLocals()..set(Spry, this); } diff --git a/lib/src/utils/_spry_internal_utils.dart b/lib/src/utils/_spry_internal_utils.dart new file mode 100644 index 0000000..b46053d --- /dev/null +++ b/lib/src/utils/_spry_internal_utils.dart @@ -0,0 +1,28 @@ +import '../event/event.dart'; +import '../handler/handler.dart'; +import '../http/response.dart'; +import '../locals/_locals+get_or_set.dart'; +import '../spry.dart'; +import 'next.dart'; + +extension SpryInternalUtils on Spry { + void addHandler(Handler handler) => handlers.add(handler); + + Future Function(Event) createHandle() { + return handlers.reversed.fold(next, (effect, current) { + return (event) { + event.locals.set(next, effect); + + return current.handle(event); + }; + }); + } +} + +extension on Spry { + static const handlersKey = #spry.app.handlers; + + List get handlers { + return locals.getOrSet>(handlersKey, () => []); + } +} From 76b9285764af795749751a855a74d8765727ebc6 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Mon, 8 Jul 2024 01:00:41 +0800 Subject: [PATCH 09/35] chore: WIP --- demo.dart | 23 ++++++-- example/main.dart | 36 ++++-------- lib/plain.dart | 58 +++++++++++++++++++ lib/spry.dart | 5 ++ lib/src/event/event.dart | 14 ++++- lib/src/http/request.dart | 5 +- lib/src/platform/platform+create_handler.dart | 58 +++++++++++++++++-- lib/src/platform/platform.dart | 8 +-- lib/src/routing/route.dart | 5 ++ lib/src/routing/routes_builder.dart | 5 ++ lib/src/spry.dart | 38 +++++++++++- lib/src/types.dart | 1 + lib/src/utils/_spry_internal_utils.dart | 13 +++-- test/locals/locals_test.dart | 6 +- 14 files changed, 222 insertions(+), 53 deletions(-) create mode 100644 lib/plain.dart create mode 100644 lib/src/routing/route.dart create mode 100644 lib/src/routing/routes_builder.dart create mode 100644 lib/src/types.dart diff --git a/demo.dart b/demo.dart index 1bc0492..3e6c0b0 100644 --- a/demo.dart +++ b/demo.dart @@ -1,6 +1,21 @@ -import 'dart:io'; +import 'dart:convert'; -demo(HttpRequest req, HttpResponse res) { - req.response.encoding; - res.flush; +demo() { + final types = + 'text/plain; charset=utf-8, application/json; charset=utf-8'.split(','); + for (final type in types) { + for (final param in type.split(';')) { + final kv = param.trim().toLowerCase().split('='); + if (kv.length == 2 && kv[0] == 'charset') { + final encoding = Encoding.getByName(kv[1].trim()); + if (encoding != null) { + return encoding; + } + } + } + } +} + +main() { + print(demo()); } diff --git a/example/main.dart b/example/main.dart index 5b267bf..71248a2 100644 --- a/example/main.dart +++ b/example/main.dart @@ -1,11 +1,7 @@ +import 'package:spry/plain.dart'; import 'package:spry/spry.dart'; -import 'package:spry/src/locals/locals.dart'; - -class ExampleEvent implements Event { - @override - final Locals locals = LocalsImpl(); -} +const plain = PlainPlatform(); void main() async { final app = Spry(); @@ -14,32 +10,20 @@ void main() async { print(1); }); - app.use((event) async { - final res = await next(event); - print(2); - - return res; - }); - app.use((event) { - print(3); - }); - - app.use((event) { - print(4); - - // 中断向内嵌套,返回 Response 或者其他任何信息 - return const Response(null); + print(2); }); app.use((event) { - print('Unable to execute'); + print(event.request.uri); }); - final handle = app.handler.handle; - final event = ExampleEvent(); + final handler = plain.createHandler(app); + // OR + // final handler = app.createPlatformHandler(plain); - event.locals.set(Spry, app); + final request = PlainRequest(method: 'get', uri: Uri(path: '/haha')); + final response = await handler(request); - await handle(event); + print(response); } diff --git a/lib/plain.dart b/lib/plain.dart new file mode 100644 index 0000000..56e16c8 --- /dev/null +++ b/lib/plain.dart @@ -0,0 +1,58 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'spry.dart'; + +class PlainRequest implements Request { + PlainRequest({ + required this.method, + required this.uri, + this.headers = const Headers(), + this.body, + }); + + @override + Stream? body; + + @override + Encoding get encoding => utf8; + + @override + Headers headers; + + @override + Uri uri; + + @override + final String method; +} + +class PlainPlatform implements Platform { + const PlainPlatform(); + + @override + Stream? getRequestBody(Event event, PlainRequest request) { + return request.body; + } + + @override + Headers getRequestHeaders(Event event, PlainRequest request) { + return request.headers; + } + + @override + Uri getRequestURI(Event event, PlainRequest request) { + return request.uri; + } + + @override + Future respond( + Event event, PlainRequest request, Response response) async { + return response; + } + + @override + String getRequestMethod(Event event, PlainRequest request) { + return request.method; + } +} diff --git a/lib/spry.dart b/lib/spry.dart index 8f57ebd..00c9d38 100644 --- a/lib/spry.dart +++ b/lib/spry.dart @@ -17,11 +17,16 @@ export 'src/http/http_message/http_message.dart'; export 'src/http/http_message/http_message+text.dart'; export 'src/http/http_message/http_message+json.dart'; +export 'src/http/request.dart'; export 'src/http/response.dart'; export 'src/locals/locals.dart' hide AppLocals, EventLocals; export 'src/locals/locals+get_or_null.dart'; +export 'src/platform/platform.dart'; +export 'src/platform/platform+create_handler.dart'; +export 'src/platform/platform_handler.dart'; + export 'src/utils/next.dart'; export 'src/spry.dart'; diff --git a/lib/src/event/event.dart b/lib/src/event/event.dart index 3927013..77f089b 100644 --- a/lib/src/event/event.dart +++ b/lib/src/event/event.dart @@ -1,12 +1,22 @@ +import '../http/request.dart'; import '../locals/locals.dart'; abstract final class Event { Locals get locals; + Request get request; } final class EventImpl implements Event { - EventImpl(Locals appLocals) : locals = EventLocals(appLocals); + EventImpl({ + required this.appLocals, + required this.request, + }); + + final Locals appLocals; + + @override + late final Locals locals = EventLocals(appLocals); @override - final Locals locals; + final Request request; } diff --git a/lib/src/http/request.dart b/lib/src/http/request.dart index c2747b7..9a64ce4 100644 --- a/lib/src/http/request.dart +++ b/lib/src/http/request.dart @@ -1,3 +1,6 @@ import 'http_message/http_message.dart'; -abstract interface class Request implements HttpMessage {} +abstract interface class Request implements HttpMessage { + String get method; + Uri get uri; +} diff --git a/lib/src/platform/platform+create_handler.dart b/lib/src/platform/platform+create_handler.dart index 1a2373b..a9a99f6 100644 --- a/lib/src/platform/platform+create_handler.dart +++ b/lib/src/platform/platform+create_handler.dart @@ -1,6 +1,12 @@ // ignore_for_file: file_names +import 'dart:convert'; +import 'dart:typed_data'; + import '../event/event.dart'; +import '../http/headers/headers.dart'; +import '../http/headers/headers+get.dart'; +import '../http/request.dart'; import '../spry.dart'; import '../utils/_spry_internal_utils.dart'; import 'platform.dart'; @@ -8,13 +14,55 @@ import 'platform_handler.dart'; extension PlatformAdapterCreateHandler on Platform { PlatformHandler createHandler(Spry app) { - final handle = app.createHandle(); + final handleWith = app.createHandleWith(); + + return (T raw) async { + final request = _RequestImpl(); + final event = EventImpl(appLocals: app.locals, request: request); - return (T request) async { - final event = EventImpl(app.locals); - final response = await handle(event); + request.method = getRequestMethod(event, raw).toUpperCase(); + request.uri = getRequestURI(event, raw); + request.headers = getRequestHeaders(event, raw); + request.body = getRequestBody(event, raw); - return respond(event, request, response); + final response = await handleWith(event); + + return respond(event, raw, response); }; } } + +class _RequestImpl implements Request { + @override + late Stream? body; + + @override + Encoding get encoding => headers.contentTypeCharset; + + @override + late final Headers headers; + + @override + late final Uri uri; + + @override + late final String method; +} + +extension on Headers { + Encoding get contentTypeCharset { + for (final type in getAll('content-type')) { + for (final param in type.split(';')) { + final kv = param.trim().toLowerCase().split('='); + if (kv.length == 2 && kv[0] == 'charset') { + final encoding = Encoding.getByName(kv[1].trim()); + if (encoding != null) { + return encoding; + } + } + } + } + + return utf8; + } +} diff --git a/lib/src/platform/platform.dart b/lib/src/platform/platform.dart index d0827f7..5a8b1d5 100644 --- a/lib/src/platform/platform.dart +++ b/lib/src/platform/platform.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:typed_data'; import '../event/event.dart'; @@ -6,8 +5,9 @@ import '../http/headers/headers.dart'; import '../http/response.dart'; abstract interface class Platform { - FutureOr getRequestURI(Event event, T request); - FutureOr getRequestHeaders(Event event, T request); + String getRequestMethod(Event event, T request); + Uri getRequestURI(Event event, T request); + Headers getRequestHeaders(Event event, T request); Stream? getRequestBody(Event event, T request); - FutureOr respond(Event event, T request, Response response); + Future respond(Event event, T request, Response response); } diff --git a/lib/src/routing/route.dart b/lib/src/routing/route.dart new file mode 100644 index 0000000..6723dbf --- /dev/null +++ b/lib/src/routing/route.dart @@ -0,0 +1,5 @@ +final class Route { + const Route({required this.id}); + + final String id; +} diff --git a/lib/src/routing/routes_builder.dart b/lib/src/routing/routes_builder.dart new file mode 100644 index 0000000..2651bc3 --- /dev/null +++ b/lib/src/routing/routes_builder.dart @@ -0,0 +1,5 @@ +import '../handler/handler.dart'; + +abstract interface class RoutesBuilder { + void addRoute(String method, String route, Handler handler); +} diff --git a/lib/src/spry.dart b/lib/src/spry.dart index a1ee842..77da9a0 100644 --- a/lib/src/spry.dart +++ b/lib/src/spry.dart @@ -1,5 +1,39 @@ +import 'handler/handler.dart'; import 'locals/locals.dart'; +import 'routing/routes_builder.dart'; +import 'types.dart'; -class Spry { - late final Locals locals = AppLocals()..set(Spry, this); +class Spry implements RoutesBuilder { + const Spry._({required this.locals, required this.router}); + + factory Spry({ + final Map? locals, + final Router? router, + final RouterDriver routerDriver = const RadixTrieRouterDriver(), + final bool caseSensitive = false, + }) { + final appLocals = AppLocals(); + if (locals != null && locals.isNotEmpty) { + appLocals.locals.addAll(locals); + } + + final app = Spry._( + locals: appLocals, + router: switch (router) { + Router router => router, + _ => createRouter(driver: routerDriver, caseSensitive: caseSensitive) + }, + ); + appLocals.set(Spry, app); + + return app; + } + + final Locals locals; + final Router router; + + @override + void addRoute(String method, String route, Handler handler) { + router.register('${method.toUpperCase()}/$route', handler); + } } diff --git a/lib/src/types.dart b/lib/src/types.dart new file mode 100644 index 0000000..8c789d2 --- /dev/null +++ b/lib/src/types.dart @@ -0,0 +1 @@ +export 'package:routingkit/routingkit.dart' hide kCatchall; diff --git a/lib/src/utils/_spry_internal_utils.dart b/lib/src/utils/_spry_internal_utils.dart index b46053d..522d45f 100644 --- a/lib/src/utils/_spry_internal_utils.dart +++ b/lib/src/utils/_spry_internal_utils.dart @@ -8,14 +8,15 @@ import 'next.dart'; extension SpryInternalUtils on Spry { void addHandler(Handler handler) => handlers.add(handler); - Future Function(Event) createHandle() { - return handlers.reversed.fold(next, (effect, current) { - return (event) { - event.locals.set(next, effect); + Future Function(Handler, Event) createHandleWith() { + return handlers.reversed.fold( + (handler, event) => handler.handle(event), + (effect, current) => (handler, event) { + event.locals.set(next, (event) => effect(handler, event)); return current.handle(event); - }; - }); + }, + ); } } diff --git a/test/locals/locals_test.dart b/test/locals/locals_test.dart index a43b3df..be105a2 100644 --- a/test/locals/locals_test.dart +++ b/test/locals/locals_test.dart @@ -4,7 +4,7 @@ import 'package:test/test.dart'; void main() { test('.set', () { - final locals = LocalsImpl(); + final locals = AppLocals(); locals.set('a', 1); locals.set(#app, 2); @@ -15,7 +15,7 @@ void main() { }); test('.get', () { - final locals = LocalsImpl(); + final locals = AppLocals(); locals.set(#app, 1); @@ -25,7 +25,7 @@ void main() { }); test('.getOrNull', () { - final locals = LocalsImpl(); + final locals = AppLocals(); locals.set(#app, 1); From 849795326bc6f874a986e037f84636adf37c1569 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Mon, 8 Jul 2024 01:53:26 +0800 Subject: [PATCH 10/35] chore: WIP --- lib/spry.dart | 1 + lib/src/platform/platform+create_handler.dart | 38 +++++++++++++++++-- lib/src/routing/routes_builder+all.dart | 5 +++ lib/src/spry+fallback.dart | 38 +++++++++++++++++++ 4 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 lib/src/routing/routes_builder+all.dart create mode 100644 lib/src/spry+fallback.dart diff --git a/lib/spry.dart b/lib/spry.dart index 00c9d38..365b7c7 100644 --- a/lib/spry.dart +++ b/lib/spry.dart @@ -32,3 +32,4 @@ export 'src/utils/next.dart'; export 'src/spry.dart'; export 'src/spry+create_platform_handler.dart'; export 'src/spry+use.dart'; +export 'src/spry+fallback.dart'; diff --git a/lib/src/platform/platform+create_handler.dart b/lib/src/platform/platform+create_handler.dart index a9a99f6..a534b68 100644 --- a/lib/src/platform/platform+create_handler.dart +++ b/lib/src/platform/platform+create_handler.dart @@ -4,10 +4,15 @@ import 'dart:convert'; import 'dart:typed_data'; import '../event/event.dart'; +import '../handler/handler.dart'; import '../http/headers/headers.dart'; import '../http/headers/headers+get.dart'; import '../http/request.dart'; +import '../routing/route.dart'; import '../spry.dart'; +import '../spry+fallback.dart'; +import '../types.dart'; +import '../routing/routes_builder+all.dart'; import '../utils/_spry_internal_utils.dart'; import 'platform.dart'; import 'platform_handler.dart'; @@ -20,14 +25,24 @@ extension PlatformAdapterCreateHandler on Platform { final request = _RequestImpl(); final event = EventImpl(appLocals: app.locals, request: request); - request.method = getRequestMethod(event, raw).toUpperCase(); + request.method = getRequestMethod(event, raw).toUpperCase().trim(); request.uri = getRequestURI(event, raw); request.headers = getRequestHeaders(event, raw); request.body = getRequestBody(event, raw); - final response = await handleWith(event); + final result = + app.router.findDefinedRoute(request.method, request.uri.path); + final handler = switch (result) { + Result(value: final handler) => handler, + _ => app.getFallback(), + }; - return respond(event, raw, response); + event.locals.set(Params, result?.params); + if (result != null) { + event.locals.set(Route, Route(id: result.route)); + } + + return respond(event, raw, await handleWith(handler, event)); }; } } @@ -66,3 +81,20 @@ extension on Headers { return utf8; } } + +extension on Router { + Result? findDefinedRoute(String method, String path) { + return switch (method) { + RoutesBuilderAll.kAllMethod => + lookup('${RoutesBuilderAll.kAllMethod}/$path'), + 'HEAD' => switch (lookup('HEAD/$path')) { + Result result => result, + _ => findDefinedRoute('GET', path), + }, + String method => switch (lookup('$method/$path')) { + Result result => result, + _ => findDefinedRoute(RoutesBuilderAll.kAllMethod, path), + }, + }; + } +} diff --git a/lib/src/routing/routes_builder+all.dart b/lib/src/routing/routes_builder+all.dart new file mode 100644 index 0000000..e38d40b --- /dev/null +++ b/lib/src/routing/routes_builder+all.dart @@ -0,0 +1,5 @@ +import 'routes_builder.dart'; + +extension RoutesBuilderAll on RoutesBuilder { + static const kAllMethod = '#SPRY/__ALL__'; +} diff --git a/lib/src/spry+fallback.dart b/lib/src/spry+fallback.dart new file mode 100644 index 0000000..9d15b38 --- /dev/null +++ b/lib/src/spry+fallback.dart @@ -0,0 +1,38 @@ +// ignore_for_file: file_names + +import 'dart:async'; + +import 'package:spry/src/http/response.dart'; + +import 'event/event.dart'; +import 'handler/_closure_handler.dart'; +import 'handler/handler.dart'; +import 'locals/locals+get_or_null.dart'; +import 'spry.dart'; + +extension SpryFallback on Spry { + void fallback(FutureOr Function(Event event) closure) { + locals.set(_FailbackHandler, ClosureHandler(closure)); + } + + Handler getFallback() { + return switch (locals.getOrNull(_FailbackHandler)) { + Handler handler => handler, + _ => const _DefaultFailbackHandler(), + }; + } +} + +abstract final class _FailbackHandler implements Handler { + const _FailbackHandler(); +} + +final class _DefaultFailbackHandler extends _FailbackHandler { + const _DefaultFailbackHandler(); + + @override + Future handle(Event event) { + // TODO: implement handle + throw UnimplementedError(); + } +} From 6fe0a1f272d99014133284390317d04f1239d4cc Mon Sep 17 00:00:00 2001 From: Seven Du Date: Mon, 8 Jul 2024 03:00:43 +0800 Subject: [PATCH 11/35] chore: WIP --- example/main.dart | 2 +- lib/spry.dart | 10 +++++++ lib/src/errors/spry_error.dart | 29 +++++++++++++++++++ lib/src/event/event+params.dart | 11 +++++++ lib/src/event/event+route.dart | 9 ++++++ lib/src/event/event+set_headers.dart | 14 +++++++++ lib/src/event/event+uri.dart | 7 +++++ lib/src/handler/_closure_handler.dart | 5 ++++ lib/src/http/response.dart | 7 +++-- lib/src/platform/platform+create_handler.dart | 23 ++++++++++++++- lib/src/utils/next.dart | 20 ++++++++++++- 11 files changed, 131 insertions(+), 6 deletions(-) create mode 100644 lib/src/errors/spry_error.dart create mode 100644 lib/src/event/event+params.dart create mode 100644 lib/src/event/event+route.dart create mode 100644 lib/src/event/event+set_headers.dart create mode 100644 lib/src/event/event+uri.dart diff --git a/example/main.dart b/example/main.dart index 71248a2..cacbe3a 100644 --- a/example/main.dart +++ b/example/main.dart @@ -25,5 +25,5 @@ void main() async { final request = PlainRequest(method: 'get', uri: Uri(path: '/haha')); final response = await handler(request); - print(response); + print(await response.text()); } diff --git a/lib/spry.dart b/lib/spry.dart index 365b7c7..c2df2f5 100644 --- a/lib/spry.dart +++ b/lib/spry.dart @@ -1,5 +1,11 @@ +export 'src/errors/spry_error.dart'; + export 'src/event/event.dart' hide EventImpl; export 'src/event/event+app.dart'; +export 'src/event/event+params.dart'; +export 'src/event/event+route.dart'; +export 'src/event/event+set_headers.dart'; +export 'src/event/event+uri.dart'; export 'src/handler/handler.dart'; @@ -27,6 +33,10 @@ export 'src/platform/platform.dart'; export 'src/platform/platform+create_handler.dart'; export 'src/platform/platform_handler.dart'; +export 'src/routing/route.dart'; +export 'src/routing/routes_builder.dart'; +export 'src/routing/routes_builder+all.dart'; + export 'src/utils/next.dart'; export 'src/spry.dart'; diff --git a/lib/src/errors/spry_error.dart b/lib/src/errors/spry_error.dart new file mode 100644 index 0000000..f6a92f8 --- /dev/null +++ b/lib/src/errors/spry_error.dart @@ -0,0 +1,29 @@ +import 'dart:convert'; + +import '../http/headers/headers.dart'; +import '../http/response.dart'; + +class SpryError extends Error { + SpryError._(this.message, [this.response]); + SpryError.response(this.message, Response this.response); + + factory SpryError( + String message, { + int status = 200, + String? statusText, + Headers headers = const Headers(), + Encoding encoding = utf8, + }) { + final response = Response.text( + message, + status: status, + statusText: statusText, + headers: headers, + ); + + return SpryError._(message, response); + } + + final String message; + final Response? response; +} diff --git a/lib/src/event/event+params.dart b/lib/src/event/event+params.dart new file mode 100644 index 0000000..9f00373 --- /dev/null +++ b/lib/src/event/event+params.dart @@ -0,0 +1,11 @@ +// ignore_for_file: file_names + +import '../locals/_locals+get_or_set.dart'; +import '../types.dart'; +import 'event.dart'; + +extension EventParams on Event { + Params get params { + return locals.getOrSet(Params, Params.new); + } +} diff --git a/lib/src/event/event+route.dart b/lib/src/event/event+route.dart new file mode 100644 index 0000000..d8db70f --- /dev/null +++ b/lib/src/event/event+route.dart @@ -0,0 +1,9 @@ +// ignore_for_file: file_names + +import '../locals/locals+get_or_null.dart'; +import '../routing/route.dart'; +import 'event.dart'; + +extension EventRoute on Event { + Route? get route => locals.getOrNull(Route); +} diff --git a/lib/src/event/event+set_headers.dart b/lib/src/event/event+set_headers.dart new file mode 100644 index 0000000..2bd8191 --- /dev/null +++ b/lib/src/event/event+set_headers.dart @@ -0,0 +1,14 @@ +// ignore_for_file: file_names + +import '../locals/_locals+get_or_set.dart'; +import 'event.dart'; + +extension EventSetHeaders on Event { + static const kResponsibleHeaders = #spry.event.responsible.headers; + + void setHeaders(Map headers) { + final responsibleHeaders = locals.getOrSet>( + kResponsibleHeaders, Map.new); + responsibleHeaders.addAll(headers); + } +} diff --git a/lib/src/event/event+uri.dart b/lib/src/event/event+uri.dart new file mode 100644 index 0000000..6720981 --- /dev/null +++ b/lib/src/event/event+uri.dart @@ -0,0 +1,7 @@ +// ignore_for_file: file_names + +import 'event.dart'; + +extension EventUri on Event { + Uri get uri => request.uri; +} diff --git a/lib/src/handler/_closure_handler.dart b/lib/src/handler/_closure_handler.dart index 2b91fc0..b7ebcd8 100644 --- a/lib/src/handler/_closure_handler.dart +++ b/lib/src/handler/_closure_handler.dart @@ -1,7 +1,9 @@ import 'dart:async'; import '../event/event.dart'; +import '../event/event+set_headers.dart'; import '../http/response.dart'; +import '../locals/locals+get_or_null.dart'; import '../utils/next.dart'; import 'handler.dart'; @@ -12,6 +14,9 @@ final class ClosureHandler implements Handler { @override Future handle(Event event) async { + final headers = event.locals + .getOrNull>(EventSetHeaders.kResponsibleHeaders); + return switch (await closure(event)) { Response response => response, // TODO diff --git a/lib/src/http/response.dart b/lib/src/http/response.dart index 408a58b..c579c02 100644 --- a/lib/src/http/response.dart +++ b/lib/src/http/response.dart @@ -29,7 +29,7 @@ abstract interface class Response implements HttpMessage { statusText: statusText, headers: headers .resetOf('content-length', body.length.toString()) - .resetOf('content-type', 'text/plain; charset=utf-8'), + .resetOf('content-type', 'text/plain; charset=${encoding.name}'), ); } @@ -48,7 +48,8 @@ abstract interface class Response implements HttpMessage { statusText: statusText, headers: headers .resetOf('content-length', bytes.lengthInBytes.toString()) - .resetOf('content-type', 'application/json; charset=utf-8'), + .resetOf( + 'content-type', 'application/json; charset=${encoding.name}'), ); } @@ -71,7 +72,7 @@ abstract interface class Response implements HttpMessage { headers: headers .resetOf('content-length', bytes.lengthInBytes.toString()) .resetOf('content-type', - 'application/x-www-form-urlencoded; charset=utf-8'), + 'application/x-www-form-urlencoded; charset=${encoding.name}'), ); } diff --git a/lib/src/platform/platform+create_handler.dart b/lib/src/platform/platform+create_handler.dart index a534b68..f06d1cf 100644 --- a/lib/src/platform/platform+create_handler.dart +++ b/lib/src/platform/platform+create_handler.dart @@ -3,11 +3,13 @@ import 'dart:convert'; import 'dart:typed_data'; +import '../errors/spry_error.dart'; import '../event/event.dart'; import '../handler/handler.dart'; import '../http/headers/headers.dart'; import '../http/headers/headers+get.dart'; import '../http/request.dart'; +import '../http/response.dart'; import '../routing/route.dart'; import '../spry.dart'; import '../spry+fallback.dart'; @@ -42,7 +44,10 @@ extension PlatformAdapterCreateHandler on Platform { event.locals.set(Route, Route(id: result.route)); } - return respond(event, raw, await handleWith(handler, event)); + final response = + await safeCreateResponse(() => handleWith(handler, event)); + + return respond(event, raw, response); }; } } @@ -98,3 +103,19 @@ extension on Router { }; } } + +extension on Platform { + Future safeCreateResponse( + Future Function() creates) async { + try { + return await creates(); + } on SpryError catch (error) { + return switch (error.response) { + Response response => response, + _ => Response.text(error.message, status: 500), + }; + } catch (e) { + return Response.text(Error.safeToString(e), status: 500); + } + } +} diff --git a/lib/src/utils/next.dart b/lib/src/utils/next.dart index c15707e..ada09d3 100644 --- a/lib/src/utils/next.dart +++ b/lib/src/utils/next.dart @@ -1,13 +1,31 @@ +import '../event/event+set_headers.dart'; import '../event/event.dart'; +import '../http/headers/headers_builder.dart'; +import '../http/headers/headers_builder+set.dart'; import '../http/response.dart'; import '../locals/locals+get_or_null.dart'; Future next(Event event) async { + final headers = event.locals + .getOrNull>(EventSetHeaders.kResponsibleHeaders); final effect = event.locals.getOrNull Function(Event)>(next); event.locals.remove(next); return switch (effect) { Future Function(Event) next => next(event), - _ => const Response(null), + _ => _createDefaultResponse(headers), }; } + +Response _createDefaultResponse(Map? headers) { + if (headers == null || headers.isEmpty) { + return const Response(null); + } + + final builder = HeadersBuilder(); + for (final header in headers.entries) { + builder.set(header.key, header.value); + } + + return Response(null, headers: builder.toHeaders()); +} From 6da6c97ae3f5d93370e7b07ebf445df50db0a032 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Mon, 8 Jul 2024 06:02:18 +0800 Subject: [PATCH 12/35] chore: WIP --- demo.dart | 21 ---- example/main.dart | 5 +- lib/spry.dart | 5 + lib/src/cookie/cookies.dart | 44 +++++++ lib/src/event/event+cookies.dart | 115 ++++++++++++++++++ lib/src/http/headers/headers_builder.dart | 8 +- lib/src/http/response+copy_with.dart | 20 +++ lib/src/platform/platform+create_handler.dart | 13 +- lib/src/utils/_event_internal_utils.dart | 104 ++++++++++++++++ pubspec.yaml | 1 + 10 files changed, 312 insertions(+), 24 deletions(-) delete mode 100644 demo.dart create mode 100644 lib/src/cookie/cookies.dart create mode 100644 lib/src/event/event+cookies.dart create mode 100644 lib/src/http/response+copy_with.dart create mode 100644 lib/src/utils/_event_internal_utils.dart diff --git a/demo.dart b/demo.dart deleted file mode 100644 index 3e6c0b0..0000000 --- a/demo.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'dart:convert'; - -demo() { - final types = - 'text/plain; charset=utf-8, application/json; charset=utf-8'.split(','); - for (final type in types) { - for (final param in type.split(';')) { - final kv = param.trim().toLowerCase().split('='); - if (kv.length == 2 && kv[0] == 'charset') { - final encoding = Encoding.getByName(kv[1].trim()); - if (encoding != null) { - return encoding; - } - } - } - } -} - -main() { - print(demo()); -} diff --git a/example/main.dart b/example/main.dart index cacbe3a..ec1aae0 100644 --- a/example/main.dart +++ b/example/main.dart @@ -8,10 +8,13 @@ void main() async { app.use((event) { print(1); + event.cookies.set('a', '1'); }); app.use((event) { print(2); + print(event.cookies.get('a')); + event.cookies.delete('a'); }); app.use((event) { @@ -25,5 +28,5 @@ void main() async { final request = PlainRequest(method: 'get', uri: Uri(path: '/haha')); final response = await handler(request); - print(await response.text()); + print(response.headers); } diff --git a/lib/spry.dart b/lib/spry.dart index c2df2f5..faf081f 100644 --- a/lib/spry.dart +++ b/lib/spry.dart @@ -1,7 +1,10 @@ +export 'src/cookie/cookies.dart'; + export 'src/errors/spry_error.dart'; export 'src/event/event.dart' hide EventImpl; export 'src/event/event+app.dart'; +export 'src/event/event+cookies.dart'; export 'src/event/event+params.dart'; export 'src/event/event+route.dart'; export 'src/event/event+set_headers.dart'; @@ -24,7 +27,9 @@ export 'src/http/http_message/http_message+text.dart'; export 'src/http/http_message/http_message+json.dart'; export 'src/http/request.dart'; + export 'src/http/response.dart'; +export 'src/http/response+copy_with.dart'; export 'src/locals/locals.dart' hide AppLocals, EventLocals; export 'src/locals/locals+get_or_null.dart'; diff --git a/lib/src/cookie/cookies.dart b/lib/src/cookie/cookies.dart new file mode 100644 index 0000000..9552c74 --- /dev/null +++ b/lib/src/cookie/cookies.dart @@ -0,0 +1,44 @@ +enum SameSite { lax, strict, none } + +abstract interface class Cookies { + String? get(String name, {String Function(String value)? decode}); + + Iterable<(String, String)> getAll({String Function(String value)? decode}); + + void set( + String name, + String value, { + DateTime? expires, + int? maxAge, + String? domain, + String? path, + bool secure = false, + bool httpOnly = false, + SameSite? sameSite, + String Function(String value)? encode, + }); + + void delete( + String name, { + DateTime? expires, + int? maxAge, + String? domain, + String? path, + bool secure = false, + bool httpOnly = false, + SameSite? sameSite, + }); + + String serialize( + String name, + String value, { + DateTime? expires, + int? maxAge, + String? domain, + String? path, + bool secure = false, + bool httpOnly = false, + SameSite? sameSite, + String Function(String value)? encode, + }); +} diff --git a/lib/src/event/event+cookies.dart b/lib/src/event/event+cookies.dart new file mode 100644 index 0000000..c80c715 --- /dev/null +++ b/lib/src/event/event+cookies.dart @@ -0,0 +1,115 @@ +// ignore_for_file: file_names + +import '../cookie/cookies.dart'; +import '../locals/_locals+get_or_set.dart'; +import '../utils/_event_internal_utils.dart'; +import 'event.dart'; + +extension EventCookies on Event { + Cookies get cookies { + return locals.getOrSet(Cookies, () => _CookiesImpl(this)); + } +} + +final class _CookiesImpl implements Cookies { + static const defaultEncode = Uri.encodeComponent; + static const defaultDecode = Uri.decodeComponent; + + const _CookiesImpl(this.event); + + final Event event; + + @override + String? get(String name, {String Function(String value)? decode}) { + final normalizedName = name.toLowerCase(); + for (final cookie in event.responseCookies) { + if (cookie.name.toLowerCase() == normalizedName) { + return (decode ?? defaultDecode).call(cookie.value); + } + } + + final requestCookieValue = event.requestCookies[normalizedName]; + if (requestCookieValue != null) { + return (decode ?? defaultDecode).call(requestCookieValue); + } + + return null; + } + + @override + Iterable<(String, String)> getAll( + {String Function(String value)? decode}) sync* { + final inner = decode ?? defaultDecode; + + yield* event.requestCookies.entries.map((e) => (e.key, inner(e.value))); + yield* event.responseCookies.map((e) => (e.name, inner(e.value))); + } + + @override + void set(String name, String value, + {DateTime? expires, + int? maxAge, + String? domain, + String? path, + bool secure = false, + bool httpOnly = false, + SameSite? sameSite, + String Function(String value)? encode}) { + event.responseCookies.add(SetCookie( + name, + (encode ?? defaultEncode).call(value), + expires: expires, + maxAge: maxAge, + domain: domain, + path: path, + secure: secure, + httpOnly: httpOnly, + sameSite: sameSite, + )); + } + + @override + void delete(String name, + {DateTime? expires, + int? maxAge, + String? domain, + String? path, + bool secure = false, + bool httpOnly = false, + SameSite? sameSite}) { + event.responseCookies + .removeWhere((e) => e.name.toLowerCase() == name.toLowerCase()); + + set(name, '', + expires: expires ?? DateTime.now(), + maxAge: maxAge, + domain: domain, + path: path, + secure: secure, + httpOnly: httpOnly, + sameSite: sameSite); + } + + @override + String serialize(String name, String value, + {DateTime? expires, + int? maxAge, + String? domain, + String? path, + bool secure = false, + bool httpOnly = false, + SameSite? sameSite, + String Function(String value)? encode}) { + return SetCookie( + name, + (encode ?? defaultEncode).call(value), + expires: expires, + maxAge: maxAge, + domain: domain, + path: path, + secure: secure, + httpOnly: httpOnly, + sameSite: sameSite, + ).toString(); + } +} diff --git a/lib/src/http/headers/headers_builder.dart b/lib/src/http/headers/headers_builder.dart index ae9bb8e..0e7aa3e 100644 --- a/lib/src/http/headers/headers_builder.dart +++ b/lib/src/http/headers/headers_builder.dart @@ -14,6 +14,7 @@ abstract interface class HeadersBuilder { } void remove(String name); + void removeWhere(bool Function(String name, String value) test); void add(String name, String value); Headers toHeaders(); } @@ -31,7 +32,12 @@ final class _HeadersBuilderImpl implements HeadersBuilder { @override void remove(String name) { final normalizedName = name.toLowerCase(); - locals.removeWhere((element) => element.$1 == normalizedName); + removeWhere((name, _) => name.toLowerCase() == normalizedName); + } + + @override + void removeWhere(bool Function(String name, String value) test) { + locals.removeWhere((element) => test(element.$1, element.$2)); } @override diff --git a/lib/src/http/response+copy_with.dart b/lib/src/http/response+copy_with.dart new file mode 100644 index 0000000..85f7bbb --- /dev/null +++ b/lib/src/http/response+copy_with.dart @@ -0,0 +1,20 @@ +import 'dart:typed_data'; + +import 'headers/headers.dart'; +import 'response.dart'; + +extension ResponseCopyWith on Response { + Response copyWith({ + int? status, + String? statusText, + Headers? headers, + Stream? body, + }) { + return Response( + body ?? this.body, + status: status ?? this.status, + statusText: statusText ?? this.statusText, + headers: headers ?? this.headers, + ); + } +} diff --git a/lib/src/platform/platform+create_handler.dart b/lib/src/platform/platform+create_handler.dart index f06d1cf..5d8cb60 100644 --- a/lib/src/platform/platform+create_handler.dart +++ b/lib/src/platform/platform+create_handler.dart @@ -7,14 +7,17 @@ import '../errors/spry_error.dart'; import '../event/event.dart'; import '../handler/handler.dart'; import '../http/headers/headers.dart'; +import '../http/headers/headers+rebuild.dart'; import '../http/headers/headers+get.dart'; import '../http/request.dart'; import '../http/response.dart'; +import '../http/response+copy_with.dart'; import '../routing/route.dart'; import '../spry.dart'; import '../spry+fallback.dart'; import '../types.dart'; import '../routing/routes_builder+all.dart'; +import '../utils/_event_internal_utils.dart'; import '../utils/_spry_internal_utils.dart'; import 'platform.dart'; import 'platform_handler.dart'; @@ -47,7 +50,15 @@ extension PlatformAdapterCreateHandler on Platform { final response = await safeCreateResponse(() => handleWith(handler, event)); - return respond(event, raw, response); + return respond( + event, + raw, + response.copyWith(headers: response.headers.rebuild((builder) { + for (final cookie in event.responseCookies) { + builder.add('set-cookie', cookie.toString()); + } + })), + ); }; } } diff --git a/lib/src/utils/_event_internal_utils.dart b/lib/src/utils/_event_internal_utils.dart new file mode 100644 index 0000000..5bb2388 --- /dev/null +++ b/lib/src/utils/_event_internal_utils.dart @@ -0,0 +1,104 @@ +import 'package:http_parser/http_parser.dart'; +import 'package:spry/src/http/headers/headers+get.dart'; +import 'package:spry/src/locals/_locals+get_or_set.dart'; + +import '../cookie/cookies.dart'; +import '../event/event.dart'; +import '../http/headers/headers.dart'; + +extension EventInternalUtils on Event { + Map get requestCookies { + return locals.getOrSet>( + #spry.event.request.cookies, + () => request.headers.cookies, + ); + } + + List get responseCookies { + return locals.getOrSet>( + #spry.event.response.cookies, + () => [], + ); + } +} + +class SetCookie { + const SetCookie( + this.name, + this.value, { + this.expires, + this.maxAge, + this.domain, + this.path, + this.secure = false, + this.httpOnly = false, + this.sameSite, + }); + + final String name; + final String value; + final DateTime? expires; + final int? maxAge; + final String? domain; + final String? path; + final bool secure; + final bool httpOnly; + final SameSite? sameSite; + + @override + toString() { + final buffer = StringBuffer() + ..write(name) + ..write('=') + ..write(value); + + if (expires != null) { + buffer + ..write('; Expires=') + ..write(formatHttpDate(expires!)); + } + + if (maxAge != null) { + buffer + ..write('; MaxAge=') + ..write(maxAge); + } + + if (domain != null) { + buffer + ..write('Domain=') + ..write(domain); + } + + if (path != null) { + buffer + ..write('; Path=') + ..write(path); + } + + if (secure) buffer.write('; Secure'); + if (httpOnly) buffer.write('; HttpOnly'); + if (sameSite != null) { + buffer.write('; SameSite='); + buffer.write(switch (sameSite!) { + SameSite.lax => 'Lax', + SameSite.strict => 'Strict', + SameSite.none => 'None', + }); + } + + return buffer.toString(); + } +} + +extension on Headers { + Map get cookies { + final entries = + getAll('cookie').map((e) => e.split(';')).expand((e) => e).map((e) { + final [name, ...values] = e.split('='); + return MapEntry(name.toLowerCase().trim(), values.join('=').trim()); + }); + + return Map.fromEntries(entries); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index e2b4edd..cbf5819 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,6 +6,7 @@ environment: sdk: ^3.4.3 dependencies: + http_parser: ^4.1.0 routingkit: ^1.1.0 dev_dependencies: From e28ead2d36f95ac86c320b87d7eab758275e89f8 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Mon, 8 Jul 2024 15:54:54 +0800 Subject: [PATCH 13/35] chore: WIP --- example/main.dart | 3 -- lib/spry.dart | 14 +++----- .../_event_internal_utils.dart | 2 +- lib/src/{ => composable}/cookie/cookies.dart | 0 .../cookie}/event+cookies.dart | 9 ++--- .../{ => composable}/errors/spry_error.dart | 4 +-- ...sure_handler.dart => closure_handler.dart} | 0 lib/src/platform/platform+create_handler.dart | 36 +++---------------- lib/src/spry+add_handler.dart | 9 +++++ lib/src/spry+fallback.dart | 2 +- lib/src/spry+use.dart | 4 +-- lib/src/utils/_spry_internal_utils.dart | 14 +++----- 12 files changed, 34 insertions(+), 63 deletions(-) rename lib/src/{utils => composable}/_event_internal_utils.dart (98%) rename lib/src/{ => composable}/cookie/cookies.dart (100%) rename lib/src/{event => composable/cookie}/event+cookies.dart (93%) rename lib/src/{ => composable}/errors/spry_error.dart (88%) rename lib/src/handler/{_closure_handler.dart => closure_handler.dart} (100%) create mode 100644 lib/src/spry+add_handler.dart diff --git a/example/main.dart b/example/main.dart index ec1aae0..ee64c04 100644 --- a/example/main.dart +++ b/example/main.dart @@ -8,13 +8,10 @@ void main() async { app.use((event) { print(1); - event.cookies.set('a', '1'); }); app.use((event) { print(2); - print(event.cookies.get('a')); - event.cookies.delete('a'); }); app.use((event) { diff --git a/lib/spry.dart b/lib/spry.dart index faf081f..fed8e26 100644 --- a/lib/spry.dart +++ b/lib/spry.dart @@ -1,16 +1,17 @@ -export 'src/cookie/cookies.dart'; - -export 'src/errors/spry_error.dart'; +export 'src/spry.dart'; +export 'src/spry+create_platform_handler.dart'; +export 'src/spry+use.dart'; +export 'src/spry+fallback.dart'; export 'src/event/event.dart' hide EventImpl; export 'src/event/event+app.dart'; -export 'src/event/event+cookies.dart'; export 'src/event/event+params.dart'; export 'src/event/event+route.dart'; export 'src/event/event+set_headers.dart'; export 'src/event/event+uri.dart'; export 'src/handler/handler.dart'; +export 'src/handler/closure_handler.dart'; export 'src/http/headers/headers.dart'; export 'src/http/headers/headers+get.dart'; @@ -43,8 +44,3 @@ export 'src/routing/routes_builder.dart'; export 'src/routing/routes_builder+all.dart'; export 'src/utils/next.dart'; - -export 'src/spry.dart'; -export 'src/spry+create_platform_handler.dart'; -export 'src/spry+use.dart'; -export 'src/spry+fallback.dart'; diff --git a/lib/src/utils/_event_internal_utils.dart b/lib/src/composable/_event_internal_utils.dart similarity index 98% rename from lib/src/utils/_event_internal_utils.dart rename to lib/src/composable/_event_internal_utils.dart index 5bb2388..09c87ee 100644 --- a/lib/src/utils/_event_internal_utils.dart +++ b/lib/src/composable/_event_internal_utils.dart @@ -2,9 +2,9 @@ import 'package:http_parser/http_parser.dart'; import 'package:spry/src/http/headers/headers+get.dart'; import 'package:spry/src/locals/_locals+get_or_set.dart'; -import '../cookie/cookies.dart'; import '../event/event.dart'; import '../http/headers/headers.dart'; +import 'cookie/cookies.dart'; extension EventInternalUtils on Event { Map get requestCookies { diff --git a/lib/src/cookie/cookies.dart b/lib/src/composable/cookie/cookies.dart similarity index 100% rename from lib/src/cookie/cookies.dart rename to lib/src/composable/cookie/cookies.dart diff --git a/lib/src/event/event+cookies.dart b/lib/src/composable/cookie/event+cookies.dart similarity index 93% rename from lib/src/event/event+cookies.dart rename to lib/src/composable/cookie/event+cookies.dart index c80c715..a48e641 100644 --- a/lib/src/event/event+cookies.dart +++ b/lib/src/composable/cookie/event+cookies.dart @@ -1,9 +1,10 @@ // ignore_for_file: file_names -import '../cookie/cookies.dart'; -import '../locals/_locals+get_or_set.dart'; -import '../utils/_event_internal_utils.dart'; -import 'event.dart'; +import 'package:spry/spry.dart'; +import 'package:spry/src/composable/_event_internal_utils.dart'; +import 'package:spry/src/locals/_locals+get_or_set.dart'; + +import 'cookies.dart'; extension EventCookies on Event { Cookies get cookies { diff --git a/lib/src/errors/spry_error.dart b/lib/src/composable/errors/spry_error.dart similarity index 88% rename from lib/src/errors/spry_error.dart rename to lib/src/composable/errors/spry_error.dart index f6a92f8..cc9fc18 100644 --- a/lib/src/errors/spry_error.dart +++ b/lib/src/composable/errors/spry_error.dart @@ -1,7 +1,7 @@ import 'dart:convert'; -import '../http/headers/headers.dart'; -import '../http/response.dart'; +import '../../http/headers/headers.dart'; +import '../../http/response.dart'; class SpryError extends Error { SpryError._(this.message, [this.response]); diff --git a/lib/src/handler/_closure_handler.dart b/lib/src/handler/closure_handler.dart similarity index 100% rename from lib/src/handler/_closure_handler.dart rename to lib/src/handler/closure_handler.dart diff --git a/lib/src/platform/platform+create_handler.dart b/lib/src/platform/platform+create_handler.dart index 5d8cb60..609f902 100644 --- a/lib/src/platform/platform+create_handler.dart +++ b/lib/src/platform/platform+create_handler.dart @@ -3,21 +3,16 @@ import 'dart:convert'; import 'dart:typed_data'; -import '../errors/spry_error.dart'; import '../event/event.dart'; import '../handler/handler.dart'; import '../http/headers/headers.dart'; -import '../http/headers/headers+rebuild.dart'; import '../http/headers/headers+get.dart'; import '../http/request.dart'; -import '../http/response.dart'; -import '../http/response+copy_with.dart'; import '../routing/route.dart'; import '../spry.dart'; import '../spry+fallback.dart'; import '../types.dart'; import '../routing/routes_builder+all.dart'; -import '../utils/_event_internal_utils.dart'; import '../utils/_spry_internal_utils.dart'; import 'platform.dart'; import 'platform_handler.dart'; @@ -30,6 +25,8 @@ extension PlatformAdapterCreateHandler on Platform { final request = _RequestImpl(); final event = EventImpl(appLocals: app.locals, request: request); + event.locals.set(Platform, this); + request.method = getRequestMethod(event, raw).toUpperCase().trim(); request.uri = getRequestURI(event, raw); request.headers = getRequestHeaders(event, raw); @@ -47,18 +44,9 @@ extension PlatformAdapterCreateHandler on Platform { event.locals.set(Route, Route(id: result.route)); } - final response = - await safeCreateResponse(() => handleWith(handler, event)); + final response = await handleWith(handler, event); - return respond( - event, - raw, - response.copyWith(headers: response.headers.rebuild((builder) { - for (final cookie in event.responseCookies) { - builder.add('set-cookie', cookie.toString()); - } - })), - ); + return respond(event, raw, response); }; } } @@ -114,19 +102,3 @@ extension on Router { }; } } - -extension on Platform { - Future safeCreateResponse( - Future Function() creates) async { - try { - return await creates(); - } on SpryError catch (error) { - return switch (error.response) { - Response response => response, - _ => Response.text(error.message, status: 500), - }; - } catch (e) { - return Response.text(Error.safeToString(e), status: 500); - } - } -} diff --git a/lib/src/spry+add_handler.dart b/lib/src/spry+add_handler.dart new file mode 100644 index 0000000..ad4bd10 --- /dev/null +++ b/lib/src/spry+add_handler.dart @@ -0,0 +1,9 @@ +// ignore_for_file: file_names + +import 'handler/handler.dart'; +import 'spry.dart'; +import 'utils/_spry_internal_utils.dart'; + +extension SpryAddHandler on Spry { + void addHandler(Handler handler) => handlers.add(handler); +} diff --git a/lib/src/spry+fallback.dart b/lib/src/spry+fallback.dart index 9d15b38..9558f39 100644 --- a/lib/src/spry+fallback.dart +++ b/lib/src/spry+fallback.dart @@ -5,7 +5,7 @@ import 'dart:async'; import 'package:spry/src/http/response.dart'; import 'event/event.dart'; -import 'handler/_closure_handler.dart'; +import 'handler/closure_handler.dart'; import 'handler/handler.dart'; import 'locals/locals+get_or_null.dart'; import 'spry.dart'; diff --git a/lib/src/spry+use.dart b/lib/src/spry+use.dart index e0995d9..05dd2a9 100644 --- a/lib/src/spry+use.dart +++ b/lib/src/spry+use.dart @@ -3,9 +3,9 @@ import 'dart:async'; import 'event/event.dart'; -import 'handler/_closure_handler.dart'; -import 'utils/_spry_internal_utils.dart'; +import 'handler/closure_handler.dart'; import 'spry.dart'; +import 'spry+add_handler.dart'; extension SpryHandler on Spry { void use(FutureOr Function(Event event) closure) { diff --git a/lib/src/utils/_spry_internal_utils.dart b/lib/src/utils/_spry_internal_utils.dart index 522d45f..231b2e0 100644 --- a/lib/src/utils/_spry_internal_utils.dart +++ b/lib/src/utils/_spry_internal_utils.dart @@ -6,7 +6,11 @@ import '../spry.dart'; import 'next.dart'; extension SpryInternalUtils on Spry { - void addHandler(Handler handler) => handlers.add(handler); + static const handlersKey = #spry.app.handlers; + + List get handlers { + return locals.getOrSet>(handlersKey, () => []); + } Future Function(Handler, Event) createHandleWith() { return handlers.reversed.fold( @@ -19,11 +23,3 @@ extension SpryInternalUtils on Spry { ); } } - -extension on Spry { - static const handlersKey = #spry.app.handlers; - - List get handlers { - return locals.getOrSet>(handlersKey, () => []); - } -} From eb34290ee873aec80ece9c797aa73dfc14a2e9db Mon Sep 17 00:00:00 2001 From: Seven Du Date: Mon, 8 Jul 2024 16:09:18 +0800 Subject: [PATCH 14/35] chore: WIP --- lib/spry.dart | 7 +++--- lib/src/composable/_event_internal_utils.dart | 2 +- lib/src/composable/cookie/event+cookies.dart | 1 - lib/src/event/event+method.dart | 7 ++++++ lib/src/event/event+params.dart | 2 +- lib/src/event/event+set_headers.dart | 14 ------------ lib/src/handler/closure_handler.dart | 5 ----- ...get_or_set.dart => locals+get_or_set.dart} | 0 lib/src/spry+fallback.dart | 6 ++--- lib/src/utils/_spry_internal_utils.dart | 2 +- lib/src/utils/next.dart | 22 ++----------------- 11 files changed, 18 insertions(+), 50 deletions(-) create mode 100644 lib/src/event/event+method.dart delete mode 100644 lib/src/event/event+set_headers.dart rename lib/src/locals/{_locals+get_or_set.dart => locals+get_or_set.dart} (100%) diff --git a/lib/spry.dart b/lib/spry.dart index fed8e26..e1b9e8d 100644 --- a/lib/spry.dart +++ b/lib/spry.dart @@ -1,3 +1,5 @@ +library spry; + export 'src/spry.dart'; export 'src/spry+create_platform_handler.dart'; export 'src/spry+use.dart'; @@ -5,9 +7,9 @@ export 'src/spry+fallback.dart'; export 'src/event/event.dart' hide EventImpl; export 'src/event/event+app.dart'; +export 'src/event/event+method.dart'; export 'src/event/event+params.dart'; export 'src/event/event+route.dart'; -export 'src/event/event+set_headers.dart'; export 'src/event/event+uri.dart'; export 'src/handler/handler.dart'; @@ -19,7 +21,6 @@ export 'src/http/headers/headers+has.dart'; export 'src/http/headers/headers+keys.dart'; export 'src/http/headers/headers+rebuild.dart'; export 'src/http/headers/headers+to_builder.dart'; - export 'src/http/headers/headers_builder.dart'; export 'src/http/headers/headers_builder+set.dart'; @@ -28,12 +29,12 @@ export 'src/http/http_message/http_message+text.dart'; export 'src/http/http_message/http_message+json.dart'; export 'src/http/request.dart'; - export 'src/http/response.dart'; export 'src/http/response+copy_with.dart'; export 'src/locals/locals.dart' hide AppLocals, EventLocals; export 'src/locals/locals+get_or_null.dart'; +export 'src/locals/locals+get_or_set.dart'; export 'src/platform/platform.dart'; export 'src/platform/platform+create_handler.dart'; diff --git a/lib/src/composable/_event_internal_utils.dart b/lib/src/composable/_event_internal_utils.dart index 09c87ee..876339f 100644 --- a/lib/src/composable/_event_internal_utils.dart +++ b/lib/src/composable/_event_internal_utils.dart @@ -1,6 +1,6 @@ import 'package:http_parser/http_parser.dart'; import 'package:spry/src/http/headers/headers+get.dart'; -import 'package:spry/src/locals/_locals+get_or_set.dart'; +import 'package:spry/src/locals/locals+get_or_set.dart'; import '../event/event.dart'; import '../http/headers/headers.dart'; diff --git a/lib/src/composable/cookie/event+cookies.dart b/lib/src/composable/cookie/event+cookies.dart index a48e641..56b8b3f 100644 --- a/lib/src/composable/cookie/event+cookies.dart +++ b/lib/src/composable/cookie/event+cookies.dart @@ -2,7 +2,6 @@ import 'package:spry/spry.dart'; import 'package:spry/src/composable/_event_internal_utils.dart'; -import 'package:spry/src/locals/_locals+get_or_set.dart'; import 'cookies.dart'; diff --git a/lib/src/event/event+method.dart b/lib/src/event/event+method.dart new file mode 100644 index 0000000..cb0d27f --- /dev/null +++ b/lib/src/event/event+method.dart @@ -0,0 +1,7 @@ +// ignore_for_file: file_names + +import 'event.dart'; + +extension EventMethod on Event { + String get method => request.method; +} diff --git a/lib/src/event/event+params.dart b/lib/src/event/event+params.dart index 9f00373..87fd7bd 100644 --- a/lib/src/event/event+params.dart +++ b/lib/src/event/event+params.dart @@ -1,6 +1,6 @@ // ignore_for_file: file_names -import '../locals/_locals+get_or_set.dart'; +import '../locals/locals+get_or_set.dart'; import '../types.dart'; import 'event.dart'; diff --git a/lib/src/event/event+set_headers.dart b/lib/src/event/event+set_headers.dart deleted file mode 100644 index 2bd8191..0000000 --- a/lib/src/event/event+set_headers.dart +++ /dev/null @@ -1,14 +0,0 @@ -// ignore_for_file: file_names - -import '../locals/_locals+get_or_set.dart'; -import 'event.dart'; - -extension EventSetHeaders on Event { - static const kResponsibleHeaders = #spry.event.responsible.headers; - - void setHeaders(Map headers) { - final responsibleHeaders = locals.getOrSet>( - kResponsibleHeaders, Map.new); - responsibleHeaders.addAll(headers); - } -} diff --git a/lib/src/handler/closure_handler.dart b/lib/src/handler/closure_handler.dart index b7ebcd8..2b91fc0 100644 --- a/lib/src/handler/closure_handler.dart +++ b/lib/src/handler/closure_handler.dart @@ -1,9 +1,7 @@ import 'dart:async'; import '../event/event.dart'; -import '../event/event+set_headers.dart'; import '../http/response.dart'; -import '../locals/locals+get_or_null.dart'; import '../utils/next.dart'; import 'handler.dart'; @@ -14,9 +12,6 @@ final class ClosureHandler implements Handler { @override Future handle(Event event) async { - final headers = event.locals - .getOrNull>(EventSetHeaders.kResponsibleHeaders); - return switch (await closure(event)) { Response response => response, // TODO diff --git a/lib/src/locals/_locals+get_or_set.dart b/lib/src/locals/locals+get_or_set.dart similarity index 100% rename from lib/src/locals/_locals+get_or_set.dart rename to lib/src/locals/locals+get_or_set.dart diff --git a/lib/src/spry+fallback.dart b/lib/src/spry+fallback.dart index 9558f39..f5bc048 100644 --- a/lib/src/spry+fallback.dart +++ b/lib/src/spry+fallback.dart @@ -9,6 +9,7 @@ import 'handler/closure_handler.dart'; import 'handler/handler.dart'; import 'locals/locals+get_or_null.dart'; import 'spry.dart'; +import 'utils/next.dart'; extension SpryFallback on Spry { void fallback(FutureOr Function(Event event) closure) { @@ -31,8 +32,5 @@ final class _DefaultFailbackHandler extends _FailbackHandler { const _DefaultFailbackHandler(); @override - Future handle(Event event) { - // TODO: implement handle - throw UnimplementedError(); - } + Future handle(Event event) => next(event); } diff --git a/lib/src/utils/_spry_internal_utils.dart b/lib/src/utils/_spry_internal_utils.dart index 231b2e0..1cc6602 100644 --- a/lib/src/utils/_spry_internal_utils.dart +++ b/lib/src/utils/_spry_internal_utils.dart @@ -1,7 +1,7 @@ import '../event/event.dart'; import '../handler/handler.dart'; import '../http/response.dart'; -import '../locals/_locals+get_or_set.dart'; +import '../locals/locals+get_or_set.dart'; import '../spry.dart'; import 'next.dart'; diff --git a/lib/src/utils/next.dart b/lib/src/utils/next.dart index ada09d3..aebeae0 100644 --- a/lib/src/utils/next.dart +++ b/lib/src/utils/next.dart @@ -1,31 +1,13 @@ -import '../event/event+set_headers.dart'; import '../event/event.dart'; -import '../http/headers/headers_builder.dart'; -import '../http/headers/headers_builder+set.dart'; import '../http/response.dart'; import '../locals/locals+get_or_null.dart'; Future next(Event event) async { - final headers = event.locals - .getOrNull>(EventSetHeaders.kResponsibleHeaders); final effect = event.locals.getOrNull Function(Event)>(next); event.locals.remove(next); return switch (effect) { - Future Function(Event) next => next(event), - _ => _createDefaultResponse(headers), + Future Function(Event) handle => handle(event), + _ => const Response(null), }; } - -Response _createDefaultResponse(Map? headers) { - if (headers == null || headers.isEmpty) { - return const Response(null); - } - - final builder = HeadersBuilder(); - for (final header in headers.entries) { - builder.set(header.key, header.value); - } - - return Response(null, headers: builder.toHeaders()); -} From 88e1d99d0c47e118764fc302113a566875aadb54 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Mon, 8 Jul 2024 17:08:25 +0800 Subject: [PATCH 15/35] chore: WIP --- lib/src/composable/_event_internal_utils.dart | 104 ---------------- lib/src/composable/cookie/event+cookies.dart | 115 ------------------ lib/src/composable/errors/spry_error.dart | 29 ----- lib/src/routing/routes_builder+all.dart | 5 - .../cookie/_event_internal_utils.dart | 101 +++++++++++++++ .../composable/cookie/cookies.dart | 0 packages/composable/cookie/event+cookies.dart | 115 ++++++++++++++++++ .gitignore => packages/spry/.gitignore | 0 CHANGELOG.md => packages/spry/CHANGELOG.md | 0 .../spry/analysis_options.yaml | 0 {example => packages/spry/example}/main.dart | 0 {lib => packages/spry/lib}/plain.dart | 2 + {lib => packages/spry/lib}/spry.dart | 2 + .../spry/lib}/src/event/event+app.dart | 0 .../spry/lib}/src/event/event+method.dart | 0 .../spry/lib}/src/event/event+params.dart | 0 .../spry/lib}/src/event/event+route.dart | 0 .../spry/lib}/src/event/event+uri.dart | 0 .../spry/lib}/src/event/event.dart | 0 .../lib}/src/handler/closure_handler.dart | 0 .../spry/lib}/src/handler/handler.dart | 0 .../lib}/src/http/headers/headers+get.dart | 0 .../lib}/src/http/headers/headers+has.dart | 0 .../lib}/src/http/headers/headers+keys.dart | 0 .../src/http/headers/headers+rebuild.dart | 0 .../src/http/headers/headers+to_builder.dart | 0 .../spry/lib}/src/http/headers/headers.dart | 0 .../src/http/headers/headers_builder+set.dart | 0 .../src/http/headers/headers_builder.dart | 0 .../http/http_message/http_message+json.dart | 0 .../http/http_message/http_message+text.dart | 0 .../src/http/http_message/http_message.dart | 0 .../src/http/http_status_reason_phrase.dart | 0 .../spry/lib}/src/http/request.dart | 0 .../lib}/src/http/response+copy_with.dart | 0 .../spry/lib}/src/http/response.dart | 0 .../lib}/src/locals/locals+get_or_null.dart | 0 .../lib}/src/locals/locals+get_or_set.dart | 0 .../spry/lib}/src/locals/locals.dart | 0 .../src/platform/platform+create_handler.dart | 0 .../spry/lib}/src/platform/platform.dart | 0 .../lib}/src/platform/platform_handler.dart | 0 .../spry/lib}/src/routing/route.dart | 0 .../lib/src/routing/routes_builder+all.dart | 15 +++ .../src/routing/routes_builder+methods.dart | 33 +++++ .../lib/src/routing/routes_builder+on.dart | 12 ++ .../spry/lib}/src/routing/routes_builder.dart | 0 .../spry/lib}/src/spry+add_handler.dart | 0 .../src/spry+create_platform_handler.dart | 0 .../spry/lib}/src/spry+fallback.dart | 3 +- {lib => packages/spry/lib}/src/spry+use.dart | 0 {lib => packages/spry/lib}/src/spry.dart | 0 {lib => packages/spry/lib}/src/types.dart | 0 .../lib}/src/utils/_spry_internal_utils.dart | 0 .../spry/lib}/src/utils/next.dart | 0 pubspec.yaml => packages/spry/pubspec.yaml | 0 .../spry/test}/http/headers_builder_test.dart | 0 .../spry/test}/http/headers_test.dart | 0 .../spry/test}/http/http_message_test.dart | 0 .../spry/test}/http/response_test.dart | 0 .../spry/test}/locals/locals_test.dart | 0 61 files changed, 281 insertions(+), 255 deletions(-) delete mode 100644 lib/src/composable/_event_internal_utils.dart delete mode 100644 lib/src/composable/cookie/event+cookies.dart delete mode 100644 lib/src/composable/errors/spry_error.dart delete mode 100644 lib/src/routing/routes_builder+all.dart create mode 100644 packages/composable/cookie/_event_internal_utils.dart rename {lib/src => packages}/composable/cookie/cookies.dart (100%) create mode 100644 packages/composable/cookie/event+cookies.dart rename .gitignore => packages/spry/.gitignore (100%) rename CHANGELOG.md => packages/spry/CHANGELOG.md (100%) rename analysis_options.yaml => packages/spry/analysis_options.yaml (100%) rename {example => packages/spry/example}/main.dart (100%) rename {lib => packages/spry/lib}/plain.dart (97%) rename {lib => packages/spry/lib}/spry.dart (94%) rename {lib => packages/spry/lib}/src/event/event+app.dart (100%) rename {lib => packages/spry/lib}/src/event/event+method.dart (100%) rename {lib => packages/spry/lib}/src/event/event+params.dart (100%) rename {lib => packages/spry/lib}/src/event/event+route.dart (100%) rename {lib => packages/spry/lib}/src/event/event+uri.dart (100%) rename {lib => packages/spry/lib}/src/event/event.dart (100%) rename {lib => packages/spry/lib}/src/handler/closure_handler.dart (100%) rename {lib => packages/spry/lib}/src/handler/handler.dart (100%) rename {lib => packages/spry/lib}/src/http/headers/headers+get.dart (100%) rename {lib => packages/spry/lib}/src/http/headers/headers+has.dart (100%) rename {lib => packages/spry/lib}/src/http/headers/headers+keys.dart (100%) rename {lib => packages/spry/lib}/src/http/headers/headers+rebuild.dart (100%) rename {lib => packages/spry/lib}/src/http/headers/headers+to_builder.dart (100%) rename {lib => packages/spry/lib}/src/http/headers/headers.dart (100%) rename {lib => packages/spry/lib}/src/http/headers/headers_builder+set.dart (100%) rename {lib => packages/spry/lib}/src/http/headers/headers_builder.dart (100%) rename {lib => packages/spry/lib}/src/http/http_message/http_message+json.dart (100%) rename {lib => packages/spry/lib}/src/http/http_message/http_message+text.dart (100%) rename {lib => packages/spry/lib}/src/http/http_message/http_message.dart (100%) rename {lib => packages/spry/lib}/src/http/http_status_reason_phrase.dart (100%) rename {lib => packages/spry/lib}/src/http/request.dart (100%) rename {lib => packages/spry/lib}/src/http/response+copy_with.dart (100%) rename {lib => packages/spry/lib}/src/http/response.dart (100%) rename {lib => packages/spry/lib}/src/locals/locals+get_or_null.dart (100%) rename {lib => packages/spry/lib}/src/locals/locals+get_or_set.dart (100%) rename {lib => packages/spry/lib}/src/locals/locals.dart (100%) rename {lib => packages/spry/lib}/src/platform/platform+create_handler.dart (100%) rename {lib => packages/spry/lib}/src/platform/platform.dart (100%) rename {lib => packages/spry/lib}/src/platform/platform_handler.dart (100%) rename {lib => packages/spry/lib}/src/routing/route.dart (100%) create mode 100644 packages/spry/lib/src/routing/routes_builder+all.dart create mode 100644 packages/spry/lib/src/routing/routes_builder+methods.dart create mode 100644 packages/spry/lib/src/routing/routes_builder+on.dart rename {lib => packages/spry/lib}/src/routing/routes_builder.dart (100%) rename {lib => packages/spry/lib}/src/spry+add_handler.dart (100%) rename {lib => packages/spry/lib}/src/spry+create_platform_handler.dart (100%) rename {lib => packages/spry/lib}/src/spry+fallback.dart (94%) rename {lib => packages/spry/lib}/src/spry+use.dart (100%) rename {lib => packages/spry/lib}/src/spry.dart (100%) rename {lib => packages/spry/lib}/src/types.dart (100%) rename {lib => packages/spry/lib}/src/utils/_spry_internal_utils.dart (100%) rename {lib => packages/spry/lib}/src/utils/next.dart (100%) rename pubspec.yaml => packages/spry/pubspec.yaml (100%) rename {test => packages/spry/test}/http/headers_builder_test.dart (100%) rename {test => packages/spry/test}/http/headers_test.dart (100%) rename {test => packages/spry/test}/http/http_message_test.dart (100%) rename {test => packages/spry/test}/http/response_test.dart (100%) rename {test => packages/spry/test}/locals/locals_test.dart (100%) diff --git a/lib/src/composable/_event_internal_utils.dart b/lib/src/composable/_event_internal_utils.dart deleted file mode 100644 index 876339f..0000000 --- a/lib/src/composable/_event_internal_utils.dart +++ /dev/null @@ -1,104 +0,0 @@ -import 'package:http_parser/http_parser.dart'; -import 'package:spry/src/http/headers/headers+get.dart'; -import 'package:spry/src/locals/locals+get_or_set.dart'; - -import '../event/event.dart'; -import '../http/headers/headers.dart'; -import 'cookie/cookies.dart'; - -extension EventInternalUtils on Event { - Map get requestCookies { - return locals.getOrSet>( - #spry.event.request.cookies, - () => request.headers.cookies, - ); - } - - List get responseCookies { - return locals.getOrSet>( - #spry.event.response.cookies, - () => [], - ); - } -} - -class SetCookie { - const SetCookie( - this.name, - this.value, { - this.expires, - this.maxAge, - this.domain, - this.path, - this.secure = false, - this.httpOnly = false, - this.sameSite, - }); - - final String name; - final String value; - final DateTime? expires; - final int? maxAge; - final String? domain; - final String? path; - final bool secure; - final bool httpOnly; - final SameSite? sameSite; - - @override - toString() { - final buffer = StringBuffer() - ..write(name) - ..write('=') - ..write(value); - - if (expires != null) { - buffer - ..write('; Expires=') - ..write(formatHttpDate(expires!)); - } - - if (maxAge != null) { - buffer - ..write('; MaxAge=') - ..write(maxAge); - } - - if (domain != null) { - buffer - ..write('Domain=') - ..write(domain); - } - - if (path != null) { - buffer - ..write('; Path=') - ..write(path); - } - - if (secure) buffer.write('; Secure'); - if (httpOnly) buffer.write('; HttpOnly'); - if (sameSite != null) { - buffer.write('; SameSite='); - buffer.write(switch (sameSite!) { - SameSite.lax => 'Lax', - SameSite.strict => 'Strict', - SameSite.none => 'None', - }); - } - - return buffer.toString(); - } -} - -extension on Headers { - Map get cookies { - final entries = - getAll('cookie').map((e) => e.split(';')).expand((e) => e).map((e) { - final [name, ...values] = e.split('='); - return MapEntry(name.toLowerCase().trim(), values.join('=').trim()); - }); - - return Map.fromEntries(entries); - } -} diff --git a/lib/src/composable/cookie/event+cookies.dart b/lib/src/composable/cookie/event+cookies.dart deleted file mode 100644 index 56b8b3f..0000000 --- a/lib/src/composable/cookie/event+cookies.dart +++ /dev/null @@ -1,115 +0,0 @@ -// ignore_for_file: file_names - -import 'package:spry/spry.dart'; -import 'package:spry/src/composable/_event_internal_utils.dart'; - -import 'cookies.dart'; - -extension EventCookies on Event { - Cookies get cookies { - return locals.getOrSet(Cookies, () => _CookiesImpl(this)); - } -} - -final class _CookiesImpl implements Cookies { - static const defaultEncode = Uri.encodeComponent; - static const defaultDecode = Uri.decodeComponent; - - const _CookiesImpl(this.event); - - final Event event; - - @override - String? get(String name, {String Function(String value)? decode}) { - final normalizedName = name.toLowerCase(); - for (final cookie in event.responseCookies) { - if (cookie.name.toLowerCase() == normalizedName) { - return (decode ?? defaultDecode).call(cookie.value); - } - } - - final requestCookieValue = event.requestCookies[normalizedName]; - if (requestCookieValue != null) { - return (decode ?? defaultDecode).call(requestCookieValue); - } - - return null; - } - - @override - Iterable<(String, String)> getAll( - {String Function(String value)? decode}) sync* { - final inner = decode ?? defaultDecode; - - yield* event.requestCookies.entries.map((e) => (e.key, inner(e.value))); - yield* event.responseCookies.map((e) => (e.name, inner(e.value))); - } - - @override - void set(String name, String value, - {DateTime? expires, - int? maxAge, - String? domain, - String? path, - bool secure = false, - bool httpOnly = false, - SameSite? sameSite, - String Function(String value)? encode}) { - event.responseCookies.add(SetCookie( - name, - (encode ?? defaultEncode).call(value), - expires: expires, - maxAge: maxAge, - domain: domain, - path: path, - secure: secure, - httpOnly: httpOnly, - sameSite: sameSite, - )); - } - - @override - void delete(String name, - {DateTime? expires, - int? maxAge, - String? domain, - String? path, - bool secure = false, - bool httpOnly = false, - SameSite? sameSite}) { - event.responseCookies - .removeWhere((e) => e.name.toLowerCase() == name.toLowerCase()); - - set(name, '', - expires: expires ?? DateTime.now(), - maxAge: maxAge, - domain: domain, - path: path, - secure: secure, - httpOnly: httpOnly, - sameSite: sameSite); - } - - @override - String serialize(String name, String value, - {DateTime? expires, - int? maxAge, - String? domain, - String? path, - bool secure = false, - bool httpOnly = false, - SameSite? sameSite, - String Function(String value)? encode}) { - return SetCookie( - name, - (encode ?? defaultEncode).call(value), - expires: expires, - maxAge: maxAge, - domain: domain, - path: path, - secure: secure, - httpOnly: httpOnly, - sameSite: sameSite, - ).toString(); - } -} diff --git a/lib/src/composable/errors/spry_error.dart b/lib/src/composable/errors/spry_error.dart deleted file mode 100644 index cc9fc18..0000000 --- a/lib/src/composable/errors/spry_error.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'dart:convert'; - -import '../../http/headers/headers.dart'; -import '../../http/response.dart'; - -class SpryError extends Error { - SpryError._(this.message, [this.response]); - SpryError.response(this.message, Response this.response); - - factory SpryError( - String message, { - int status = 200, - String? statusText, - Headers headers = const Headers(), - Encoding encoding = utf8, - }) { - final response = Response.text( - message, - status: status, - statusText: statusText, - headers: headers, - ); - - return SpryError._(message, response); - } - - final String message; - final Response? response; -} diff --git a/lib/src/routing/routes_builder+all.dart b/lib/src/routing/routes_builder+all.dart deleted file mode 100644 index e38d40b..0000000 --- a/lib/src/routing/routes_builder+all.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'routes_builder.dart'; - -extension RoutesBuilderAll on RoutesBuilder { - static const kAllMethod = '#SPRY/__ALL__'; -} diff --git a/packages/composable/cookie/_event_internal_utils.dart b/packages/composable/cookie/_event_internal_utils.dart new file mode 100644 index 0000000..2724741 --- /dev/null +++ b/packages/composable/cookie/_event_internal_utils.dart @@ -0,0 +1,101 @@ +// import 'package:http_parser/http_parser.dart'; +// import 'package:spry/spry.dart'; + +// import 'cookies.dart'; + +// extension EventInternalUtils on Event { +// Map get requestCookies { +// return locals.getOrSet>( +// #spry.event.request.cookies, +// () => request.headers.cookies, +// ); +// } + +// List get responseCookies { +// return locals.getOrSet>( +// #spry.event.response.cookies, +// () => [], +// ); +// } +// } + +// class SetCookie { +// const SetCookie( +// this.name, +// this.value, { +// this.expires, +// this.maxAge, +// this.domain, +// this.path, +// this.secure = false, +// this.httpOnly = false, +// this.sameSite, +// }); + +// final String name; +// final String value; +// final DateTime? expires; +// final int? maxAge; +// final String? domain; +// final String? path; +// final bool secure; +// final bool httpOnly; +// final SameSite? sameSite; + +// @override +// toString() { +// final buffer = StringBuffer() +// ..write(name) +// ..write('=') +// ..write(value); + +// if (expires != null) { +// buffer +// ..write('; Expires=') +// ..write(formatHttpDate(expires!)); +// } + +// if (maxAge != null) { +// buffer +// ..write('; MaxAge=') +// ..write(maxAge); +// } + +// if (domain != null) { +// buffer +// ..write('Domain=') +// ..write(domain); +// } + +// if (path != null) { +// buffer +// ..write('; Path=') +// ..write(path); +// } + +// if (secure) buffer.write('; Secure'); +// if (httpOnly) buffer.write('; HttpOnly'); +// if (sameSite != null) { +// buffer.write('; SameSite='); +// buffer.write(switch (sameSite!) { +// SameSite.lax => 'Lax', +// SameSite.strict => 'Strict', +// SameSite.none => 'None', +// }); +// } + +// return buffer.toString(); +// } +// } + +// extension on Headers { +// Map get cookies { +// final entries = +// getAll('cookie').map((e) => e.split(';')).expand((e) => e).map((e) { +// final [name, ...values] = e.split('='); +// return MapEntry(name.toLowerCase().trim(), values.join('=').trim()); +// }); + +// return Map.fromEntries(entries); +// } +// } diff --git a/lib/src/composable/cookie/cookies.dart b/packages/composable/cookie/cookies.dart similarity index 100% rename from lib/src/composable/cookie/cookies.dart rename to packages/composable/cookie/cookies.dart diff --git a/packages/composable/cookie/event+cookies.dart b/packages/composable/cookie/event+cookies.dart new file mode 100644 index 0000000..275acd9 --- /dev/null +++ b/packages/composable/cookie/event+cookies.dart @@ -0,0 +1,115 @@ +// // ignore_for_file: file_names + +// import 'package:spry/spry.dart'; + +// import 'cookies.dart'; +// import '_event_internal_utils.dart'; + +// extension EventCookies on Event { +// Cookies get cookies { +// return locals.getOrSet(Cookies, () => _CookiesImpl(this)); +// } +// } + +// final class _CookiesImpl implements Cookies { +// static const defaultEncode = Uri.encodeComponent; +// static const defaultDecode = Uri.decodeComponent; + +// const _CookiesImpl(this.event); + +// final Event event; + +// @override +// String? get(String name, {String Function(String value)? decode}) { +// final normalizedName = name.toLowerCase(); +// for (final cookie in event.responseCookies) { +// if (cookie.name.toLowerCase() == normalizedName) { +// return (decode ?? defaultDecode).call(cookie.value); +// } +// } + +// final requestCookieValue = event.requestCookies[normalizedName]; +// if (requestCookieValue != null) { +// return (decode ?? defaultDecode).call(requestCookieValue); +// } + +// return null; +// } + +// @override +// Iterable<(String, String)> getAll( +// {String Function(String value)? decode}) sync* { +// final inner = decode ?? defaultDecode; + +// yield* event.requestCookies.entries.map((e) => (e.key, inner(e.value))); +// yield* event.responseCookies.map((e) => (e.name, inner(e.value))); +// } + +// @override +// void set(String name, String value, +// {DateTime? expires, +// int? maxAge, +// String? domain, +// String? path, +// bool secure = false, +// bool httpOnly = false, +// SameSite? sameSite, +// String Function(String value)? encode}) { +// event.responseCookies.add(SetCookie( +// name, +// (encode ?? defaultEncode).call(value), +// expires: expires, +// maxAge: maxAge, +// domain: domain, +// path: path, +// secure: secure, +// httpOnly: httpOnly, +// sameSite: sameSite, +// )); +// } + +// @override +// void delete(String name, +// {DateTime? expires, +// int? maxAge, +// String? domain, +// String? path, +// bool secure = false, +// bool httpOnly = false, +// SameSite? sameSite}) { +// event.responseCookies +// .removeWhere((e) => e.name.toLowerCase() == name.toLowerCase()); + +// set(name, '', +// expires: expires ?? DateTime.now(), +// maxAge: maxAge, +// domain: domain, +// path: path, +// secure: secure, +// httpOnly: httpOnly, +// sameSite: sameSite); +// } + +// @override +// String serialize(String name, String value, +// {DateTime? expires, +// int? maxAge, +// String? domain, +// String? path, +// bool secure = false, +// bool httpOnly = false, +// SameSite? sameSite, +// String Function(String value)? encode}) { +// return SetCookie( +// name, +// (encode ?? defaultEncode).call(value), +// expires: expires, +// maxAge: maxAge, +// domain: domain, +// path: path, +// secure: secure, +// httpOnly: httpOnly, +// sameSite: sameSite, +// ).toString(); +// } +// } diff --git a/.gitignore b/packages/spry/.gitignore similarity index 100% rename from .gitignore rename to packages/spry/.gitignore diff --git a/CHANGELOG.md b/packages/spry/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to packages/spry/CHANGELOG.md diff --git a/analysis_options.yaml b/packages/spry/analysis_options.yaml similarity index 100% rename from analysis_options.yaml rename to packages/spry/analysis_options.yaml diff --git a/example/main.dart b/packages/spry/example/main.dart similarity index 100% rename from example/main.dart rename to packages/spry/example/main.dart diff --git a/lib/plain.dart b/packages/spry/lib/plain.dart similarity index 97% rename from lib/plain.dart rename to packages/spry/lib/plain.dart index 56e16c8..e0160c8 100644 --- a/lib/plain.dart +++ b/packages/spry/lib/plain.dart @@ -1,3 +1,5 @@ +library spry.platform.plain; + import 'dart:convert'; import 'dart:typed_data'; diff --git a/lib/spry.dart b/packages/spry/lib/spry.dart similarity index 94% rename from lib/spry.dart rename to packages/spry/lib/spry.dart index e1b9e8d..6005077 100644 --- a/lib/spry.dart +++ b/packages/spry/lib/spry.dart @@ -42,6 +42,8 @@ export 'src/platform/platform_handler.dart'; export 'src/routing/route.dart'; export 'src/routing/routes_builder.dart'; +export 'src/routing/routes_builder+on.dart'; export 'src/routing/routes_builder+all.dart'; +export 'src/routing/routes_builder+methods.dart'; export 'src/utils/next.dart'; diff --git a/lib/src/event/event+app.dart b/packages/spry/lib/src/event/event+app.dart similarity index 100% rename from lib/src/event/event+app.dart rename to packages/spry/lib/src/event/event+app.dart diff --git a/lib/src/event/event+method.dart b/packages/spry/lib/src/event/event+method.dart similarity index 100% rename from lib/src/event/event+method.dart rename to packages/spry/lib/src/event/event+method.dart diff --git a/lib/src/event/event+params.dart b/packages/spry/lib/src/event/event+params.dart similarity index 100% rename from lib/src/event/event+params.dart rename to packages/spry/lib/src/event/event+params.dart diff --git a/lib/src/event/event+route.dart b/packages/spry/lib/src/event/event+route.dart similarity index 100% rename from lib/src/event/event+route.dart rename to packages/spry/lib/src/event/event+route.dart diff --git a/lib/src/event/event+uri.dart b/packages/spry/lib/src/event/event+uri.dart similarity index 100% rename from lib/src/event/event+uri.dart rename to packages/spry/lib/src/event/event+uri.dart diff --git a/lib/src/event/event.dart b/packages/spry/lib/src/event/event.dart similarity index 100% rename from lib/src/event/event.dart rename to packages/spry/lib/src/event/event.dart diff --git a/lib/src/handler/closure_handler.dart b/packages/spry/lib/src/handler/closure_handler.dart similarity index 100% rename from lib/src/handler/closure_handler.dart rename to packages/spry/lib/src/handler/closure_handler.dart diff --git a/lib/src/handler/handler.dart b/packages/spry/lib/src/handler/handler.dart similarity index 100% rename from lib/src/handler/handler.dart rename to packages/spry/lib/src/handler/handler.dart diff --git a/lib/src/http/headers/headers+get.dart b/packages/spry/lib/src/http/headers/headers+get.dart similarity index 100% rename from lib/src/http/headers/headers+get.dart rename to packages/spry/lib/src/http/headers/headers+get.dart diff --git a/lib/src/http/headers/headers+has.dart b/packages/spry/lib/src/http/headers/headers+has.dart similarity index 100% rename from lib/src/http/headers/headers+has.dart rename to packages/spry/lib/src/http/headers/headers+has.dart diff --git a/lib/src/http/headers/headers+keys.dart b/packages/spry/lib/src/http/headers/headers+keys.dart similarity index 100% rename from lib/src/http/headers/headers+keys.dart rename to packages/spry/lib/src/http/headers/headers+keys.dart diff --git a/lib/src/http/headers/headers+rebuild.dart b/packages/spry/lib/src/http/headers/headers+rebuild.dart similarity index 100% rename from lib/src/http/headers/headers+rebuild.dart rename to packages/spry/lib/src/http/headers/headers+rebuild.dart diff --git a/lib/src/http/headers/headers+to_builder.dart b/packages/spry/lib/src/http/headers/headers+to_builder.dart similarity index 100% rename from lib/src/http/headers/headers+to_builder.dart rename to packages/spry/lib/src/http/headers/headers+to_builder.dart diff --git a/lib/src/http/headers/headers.dart b/packages/spry/lib/src/http/headers/headers.dart similarity index 100% rename from lib/src/http/headers/headers.dart rename to packages/spry/lib/src/http/headers/headers.dart diff --git a/lib/src/http/headers/headers_builder+set.dart b/packages/spry/lib/src/http/headers/headers_builder+set.dart similarity index 100% rename from lib/src/http/headers/headers_builder+set.dart rename to packages/spry/lib/src/http/headers/headers_builder+set.dart diff --git a/lib/src/http/headers/headers_builder.dart b/packages/spry/lib/src/http/headers/headers_builder.dart similarity index 100% rename from lib/src/http/headers/headers_builder.dart rename to packages/spry/lib/src/http/headers/headers_builder.dart diff --git a/lib/src/http/http_message/http_message+json.dart b/packages/spry/lib/src/http/http_message/http_message+json.dart similarity index 100% rename from lib/src/http/http_message/http_message+json.dart rename to packages/spry/lib/src/http/http_message/http_message+json.dart diff --git a/lib/src/http/http_message/http_message+text.dart b/packages/spry/lib/src/http/http_message/http_message+text.dart similarity index 100% rename from lib/src/http/http_message/http_message+text.dart rename to packages/spry/lib/src/http/http_message/http_message+text.dart diff --git a/lib/src/http/http_message/http_message.dart b/packages/spry/lib/src/http/http_message/http_message.dart similarity index 100% rename from lib/src/http/http_message/http_message.dart rename to packages/spry/lib/src/http/http_message/http_message.dart diff --git a/lib/src/http/http_status_reason_phrase.dart b/packages/spry/lib/src/http/http_status_reason_phrase.dart similarity index 100% rename from lib/src/http/http_status_reason_phrase.dart rename to packages/spry/lib/src/http/http_status_reason_phrase.dart diff --git a/lib/src/http/request.dart b/packages/spry/lib/src/http/request.dart similarity index 100% rename from lib/src/http/request.dart rename to packages/spry/lib/src/http/request.dart diff --git a/lib/src/http/response+copy_with.dart b/packages/spry/lib/src/http/response+copy_with.dart similarity index 100% rename from lib/src/http/response+copy_with.dart rename to packages/spry/lib/src/http/response+copy_with.dart diff --git a/lib/src/http/response.dart b/packages/spry/lib/src/http/response.dart similarity index 100% rename from lib/src/http/response.dart rename to packages/spry/lib/src/http/response.dart diff --git a/lib/src/locals/locals+get_or_null.dart b/packages/spry/lib/src/locals/locals+get_or_null.dart similarity index 100% rename from lib/src/locals/locals+get_or_null.dart rename to packages/spry/lib/src/locals/locals+get_or_null.dart diff --git a/lib/src/locals/locals+get_or_set.dart b/packages/spry/lib/src/locals/locals+get_or_set.dart similarity index 100% rename from lib/src/locals/locals+get_or_set.dart rename to packages/spry/lib/src/locals/locals+get_or_set.dart diff --git a/lib/src/locals/locals.dart b/packages/spry/lib/src/locals/locals.dart similarity index 100% rename from lib/src/locals/locals.dart rename to packages/spry/lib/src/locals/locals.dart diff --git a/lib/src/platform/platform+create_handler.dart b/packages/spry/lib/src/platform/platform+create_handler.dart similarity index 100% rename from lib/src/platform/platform+create_handler.dart rename to packages/spry/lib/src/platform/platform+create_handler.dart diff --git a/lib/src/platform/platform.dart b/packages/spry/lib/src/platform/platform.dart similarity index 100% rename from lib/src/platform/platform.dart rename to packages/spry/lib/src/platform/platform.dart diff --git a/lib/src/platform/platform_handler.dart b/packages/spry/lib/src/platform/platform_handler.dart similarity index 100% rename from lib/src/platform/platform_handler.dart rename to packages/spry/lib/src/platform/platform_handler.dart diff --git a/lib/src/routing/route.dart b/packages/spry/lib/src/routing/route.dart similarity index 100% rename from lib/src/routing/route.dart rename to packages/spry/lib/src/routing/route.dart diff --git a/packages/spry/lib/src/routing/routes_builder+all.dart b/packages/spry/lib/src/routing/routes_builder+all.dart new file mode 100644 index 0000000..59e06a9 --- /dev/null +++ b/packages/spry/lib/src/routing/routes_builder+all.dart @@ -0,0 +1,15 @@ +// ignore_for_file: file_names + +import 'dart:async'; + +import '../event/event.dart'; +import 'routes_builder.dart'; +import 'routes_builder+on.dart'; + +extension RoutesBuilderAll on RoutesBuilder { + static const kAllMethod = '#SPRY/__ALL__'; + + void all(String route, FutureOr Function(Event event) closure) { + return on(kAllMethod, route, closure); + } +} diff --git a/packages/spry/lib/src/routing/routes_builder+methods.dart b/packages/spry/lib/src/routing/routes_builder+methods.dart new file mode 100644 index 0000000..bce3afb --- /dev/null +++ b/packages/spry/lib/src/routing/routes_builder+methods.dart @@ -0,0 +1,33 @@ +// ignore_for_file: file_names + +import 'dart:async'; + +import '../event/event.dart'; +import 'routes_builder.dart'; +import 'routes_builder+on.dart'; + +extension RoutesBuildrMethods on RoutesBuilder { + void get(String route, FutureOr Function(Event event) closure) { + on('GET', route, closure); + } + + void post(String route, FutureOr Function(Event event) closure) { + on('POST', route, closure); + } + + void put(String route, FutureOr Function(Event event) closure) { + on('PUT', route, closure); + } + + void patch(String route, FutureOr Function(Event event) closure) { + on('PATCH', route, closure); + } + + void delete(String route, FutureOr Function(Event event) closure) { + on('DELETE', route, closure); + } + + void head(String route, FutureOr Function(Event event) closure) { + on('HEAD', route, closure); + } +} diff --git a/packages/spry/lib/src/routing/routes_builder+on.dart b/packages/spry/lib/src/routing/routes_builder+on.dart new file mode 100644 index 0000000..44e702c --- /dev/null +++ b/packages/spry/lib/src/routing/routes_builder+on.dart @@ -0,0 +1,12 @@ +import 'dart:async'; + +import '../event/event.dart'; +import '../handler/closure_handler.dart'; +import 'routes_builder.dart'; + +extension RoutesBuilderOn on RoutesBuilder { + void on( + String method, String route, FutureOr Function(Event event) closure) { + addRoute(method, route, ClosureHandler(closure)); + } +} diff --git a/lib/src/routing/routes_builder.dart b/packages/spry/lib/src/routing/routes_builder.dart similarity index 100% rename from lib/src/routing/routes_builder.dart rename to packages/spry/lib/src/routing/routes_builder.dart diff --git a/lib/src/spry+add_handler.dart b/packages/spry/lib/src/spry+add_handler.dart similarity index 100% rename from lib/src/spry+add_handler.dart rename to packages/spry/lib/src/spry+add_handler.dart diff --git a/lib/src/spry+create_platform_handler.dart b/packages/spry/lib/src/spry+create_platform_handler.dart similarity index 100% rename from lib/src/spry+create_platform_handler.dart rename to packages/spry/lib/src/spry+create_platform_handler.dart diff --git a/lib/src/spry+fallback.dart b/packages/spry/lib/src/spry+fallback.dart similarity index 94% rename from lib/src/spry+fallback.dart rename to packages/spry/lib/src/spry+fallback.dart index f5bc048..af5d29b 100644 --- a/lib/src/spry+fallback.dart +++ b/packages/spry/lib/src/spry+fallback.dart @@ -2,11 +2,10 @@ import 'dart:async'; -import 'package:spry/src/http/response.dart'; - import 'event/event.dart'; import 'handler/closure_handler.dart'; import 'handler/handler.dart'; +import 'http/response.dart'; import 'locals/locals+get_or_null.dart'; import 'spry.dart'; import 'utils/next.dart'; diff --git a/lib/src/spry+use.dart b/packages/spry/lib/src/spry+use.dart similarity index 100% rename from lib/src/spry+use.dart rename to packages/spry/lib/src/spry+use.dart diff --git a/lib/src/spry.dart b/packages/spry/lib/src/spry.dart similarity index 100% rename from lib/src/spry.dart rename to packages/spry/lib/src/spry.dart diff --git a/lib/src/types.dart b/packages/spry/lib/src/types.dart similarity index 100% rename from lib/src/types.dart rename to packages/spry/lib/src/types.dart diff --git a/lib/src/utils/_spry_internal_utils.dart b/packages/spry/lib/src/utils/_spry_internal_utils.dart similarity index 100% rename from lib/src/utils/_spry_internal_utils.dart rename to packages/spry/lib/src/utils/_spry_internal_utils.dart diff --git a/lib/src/utils/next.dart b/packages/spry/lib/src/utils/next.dart similarity index 100% rename from lib/src/utils/next.dart rename to packages/spry/lib/src/utils/next.dart diff --git a/pubspec.yaml b/packages/spry/pubspec.yaml similarity index 100% rename from pubspec.yaml rename to packages/spry/pubspec.yaml diff --git a/test/http/headers_builder_test.dart b/packages/spry/test/http/headers_builder_test.dart similarity index 100% rename from test/http/headers_builder_test.dart rename to packages/spry/test/http/headers_builder_test.dart diff --git a/test/http/headers_test.dart b/packages/spry/test/http/headers_test.dart similarity index 100% rename from test/http/headers_test.dart rename to packages/spry/test/http/headers_test.dart diff --git a/test/http/http_message_test.dart b/packages/spry/test/http/http_message_test.dart similarity index 100% rename from test/http/http_message_test.dart rename to packages/spry/test/http/http_message_test.dart diff --git a/test/http/response_test.dart b/packages/spry/test/http/response_test.dart similarity index 100% rename from test/http/response_test.dart rename to packages/spry/test/http/response_test.dart diff --git a/test/locals/locals_test.dart b/packages/spry/test/locals/locals_test.dart similarity index 100% rename from test/locals/locals_test.dart rename to packages/spry/test/locals/locals_test.dart From 04990284fa2cfe5bd07201e740310df9245126a8 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Tue, 9 Jul 2024 01:39:11 +0800 Subject: [PATCH 16/35] chore: WIP --- packages/spry/example/main.dart | 24 ++-- packages/spry/lib/exception_filter.dart | 37 ++++++ packages/spry/lib/plain.dart | 7 ++ packages/spry/lib/spry.dart | 6 + packages/spry/lib/src/_constants.dart | 1 + .../spry/lib/src/handler/closure_handler.dart | 6 +- packages/spry/lib/src/http/response.dart | 23 ---- .../src/platform/platform+create_handler.dart | 2 + packages/spry/lib/src/platform/platform.dart | 17 +++ .../spry/lib/src/responsible/responsible.dart | 117 ++++++++++++++++++ .../lib/src/routing/routes_builder+ws.dart | 51 ++++++++ packages/spry/lib/src/spry+fallback.dart | 19 +-- packages/spry/lib/src/types.dart | 1 + .../src/websocket/compression_options.dart | 49 ++++++++ packages/spry/pubspec.yaml | 2 +- 15 files changed, 311 insertions(+), 51 deletions(-) create mode 100644 packages/spry/lib/exception_filter.dart create mode 100644 packages/spry/lib/src/_constants.dart create mode 100644 packages/spry/lib/src/responsible/responsible.dart create mode 100644 packages/spry/lib/src/routing/routes_builder+ws.dart create mode 100644 packages/spry/lib/src/websocket/compression_options.dart diff --git a/packages/spry/example/main.dart b/packages/spry/example/main.dart index ee64c04..468f858 100644 --- a/packages/spry/example/main.dart +++ b/packages/spry/example/main.dart @@ -1,29 +1,21 @@ import 'package:spry/plain.dart'; import 'package:spry/spry.dart'; -const plain = PlainPlatform(); - void main() async { + // Creates a Spry application. final app = Spry(); - app.use((event) { - print(1); - }); - - app.use((event) { - print(2); - }); + // Adds a `GET /hello` route, Response body with 'Hello Spry!' + app.get('hello', (event) => 'Hello Spry!'); - app.use((event) { - print(event.request.uri); - }); + // Creates a plain platfrom. + const plain = PlainPlatform(); + // Creates a plain platfrom handler. final handler = plain.createHandler(app); - // OR - // final handler = app.createPlatformHandler(plain); - final request = PlainRequest(method: 'get', uri: Uri(path: '/haha')); + final request = PlainRequest(method: 'get', uri: Uri(path: 'hello')); final response = await handler(request); - print(response.headers); + print(await response.text()); // Hello Spry! } diff --git a/packages/spry/lib/exception_filter.dart b/packages/spry/lib/exception_filter.dart new file mode 100644 index 0000000..794fa0b --- /dev/null +++ b/packages/spry/lib/exception_filter.dart @@ -0,0 +1,37 @@ +import 'dart:async'; + +import 'spry.dart'; + +class ExceptionSource { + const ExceptionSource(this.exception, this.stackTrace); + + final T exception; + final StackTrace stackTrace; +} + +abstract interface class ExceptionFilter { + Future process(Event event, ExceptionSource source); +} + +Future Function(Event event) defineExceptionFilter( + FutureOr Function(Event event, ExceptionSource source) + process) { + return (Event event) async { + try { + return await next(event); + } catch (exception, stackTrace) { + if (exception is T) { + return process(event, ExceptionSource(exception as T, stackTrace)); + } else if (exception is Response) { + return exception; + } + + rethrow; + } + }; +} + +Future Function(Event event) withExceptionFilter( + ExceptionFilter filter) { + return defineExceptionFilter(filter.process); +} diff --git a/packages/spry/lib/plain.dart b/packages/spry/lib/plain.dart index e0160c8..337e66b 100644 --- a/packages/spry/lib/plain.dart +++ b/packages/spry/lib/plain.dart @@ -57,4 +57,11 @@ class PlainPlatform implements Platform { String getRequestMethod(Event event, PlainRequest request) { return request.method; } + + @override + Future upgradeWebSocket( + Event event, PlainRequest request, UpgradeWebSocketOptions options) { + // TODO: implement upgradeWebSocket + throw UnimplementedError(); + } } diff --git a/packages/spry/lib/spry.dart b/packages/spry/lib/spry.dart index 6005077..dcd7784 100644 --- a/packages/spry/lib/spry.dart +++ b/packages/spry/lib/spry.dart @@ -4,6 +4,7 @@ export 'src/spry.dart'; export 'src/spry+create_platform_handler.dart'; export 'src/spry+use.dart'; export 'src/spry+fallback.dart'; +export 'src/types.dart'; export 'src/event/event.dart' hide EventImpl; export 'src/event/event+app.dart'; @@ -40,10 +41,15 @@ export 'src/platform/platform.dart'; export 'src/platform/platform+create_handler.dart'; export 'src/platform/platform_handler.dart'; +export 'src/responsible/responsible.dart'; + export 'src/routing/route.dart'; export 'src/routing/routes_builder.dart'; export 'src/routing/routes_builder+on.dart'; export 'src/routing/routes_builder+all.dart'; export 'src/routing/routes_builder+methods.dart'; +export 'src/routing/routes_builder+ws.dart'; export 'src/utils/next.dart'; + +export 'src/websocket/compression_options.dart'; diff --git a/packages/spry/lib/src/_constants.dart b/packages/spry/lib/src/_constants.dart new file mode 100644 index 0000000..a1caa50 --- /dev/null +++ b/packages/spry/lib/src/_constants.dart @@ -0,0 +1 @@ +const kRawRequest = #spry.event.request.raw; diff --git a/packages/spry/lib/src/handler/closure_handler.dart b/packages/spry/lib/src/handler/closure_handler.dart index 2b91fc0..0361b9f 100644 --- a/packages/spry/lib/src/handler/closure_handler.dart +++ b/packages/spry/lib/src/handler/closure_handler.dart @@ -2,6 +2,7 @@ import 'dart:async'; import '../event/event.dart'; import '../http/response.dart'; +import '../responsible/responsible.dart'; import '../utils/next.dart'; import 'handler.dart'; @@ -13,9 +14,10 @@ final class ClosureHandler implements Handler { @override Future handle(Event event) async { return switch (await closure(event)) { + null => next(event), Response response => response, - // TODO - _ => next(event), + Responsible responsible => responsible.createResponse(event), + Object value => Responsible.of(event, value).createResponse(event), }; } } diff --git a/packages/spry/lib/src/http/response.dart b/packages/spry/lib/src/http/response.dart index c579c02..c48a016 100644 --- a/packages/spry/lib/src/http/response.dart +++ b/packages/spry/lib/src/http/response.dart @@ -53,29 +53,6 @@ abstract interface class Response implements HttpMessage { ); } - factory Response.formURLEncoded( - final Map form, { - final int status = 200, - final String? statusText, - final Headers headers = const Headers(), - final Encoding encoding = utf8, - }) { - final text = form.entries.map((e) { - return '${Uri.encodeQueryComponent(e.key)}=${Uri.encodeQueryComponent(e.value)}'; - }).join('&'); - final bytes = Uint8List.fromList(encoding.encode(text)); - - return _ResponseImpl( - Stream.value(bytes), - status: status, - statusText: statusText, - headers: headers - .resetOf('content-length', bytes.lengthInBytes.toString()) - .resetOf('content-type', - 'application/x-www-form-urlencoded; charset=${encoding.name}'), - ); - } - int get status; String get statusText; } diff --git a/packages/spry/lib/src/platform/platform+create_handler.dart b/packages/spry/lib/src/platform/platform+create_handler.dart index 609f902..d34c13b 100644 --- a/packages/spry/lib/src/platform/platform+create_handler.dart +++ b/packages/spry/lib/src/platform/platform+create_handler.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:typed_data'; +import '../_constants.dart'; import '../event/event.dart'; import '../handler/handler.dart'; import '../http/headers/headers.dart'; @@ -26,6 +27,7 @@ extension PlatformAdapterCreateHandler on Platform { final event = EventImpl(appLocals: app.locals, request: request); event.locals.set(Platform, this); + event.locals.set(kRawRequest, raw); request.method = getRequestMethod(event, raw).toUpperCase().trim(); request.uri = getRequestURI(event, raw); diff --git a/packages/spry/lib/src/platform/platform.dart b/packages/spry/lib/src/platform/platform.dart index 5a8b1d5..258fe37 100644 --- a/packages/spry/lib/src/platform/platform.dart +++ b/packages/spry/lib/src/platform/platform.dart @@ -3,11 +3,28 @@ import 'dart:typed_data'; import '../event/event.dart'; import '../http/headers/headers.dart'; import '../http/response.dart'; +import '../types.dart'; +import '../websocket/compression_options.dart'; + +final class UpgradeWebSocketOptions { + const UpgradeWebSocketOptions({ + required this.compression, + required this.supportProtocols, + required this.headers, + }); + + final CompressionOptions compression; + final Headers headers; + final Iterable supportProtocols; +} abstract interface class Platform { String getRequestMethod(Event event, T request); Uri getRequestURI(Event event, T request); Headers getRequestHeaders(Event event, T request); Stream? getRequestBody(Event event, T request); + Future upgradeWebSocket( + Event event, T request, UpgradeWebSocketOptions options); + Future respond(Event event, T request, Response response); } diff --git a/packages/spry/lib/src/responsible/responsible.dart b/packages/spry/lib/src/responsible/responsible.dart new file mode 100644 index 0000000..0432783 --- /dev/null +++ b/packages/spry/lib/src/responsible/responsible.dart @@ -0,0 +1,117 @@ +import 'dart:typed_data'; + +import '../event/event.dart'; +import '../http/response.dart'; +import '../locals/locals+get_or_set.dart'; + +abstract interface class Responsible { + Future createResponse(Event event); + + static void add(Event event, Responsible Function(T value) factory) { + if (has(event, factory)) { + return; + } + + event.responsibleNodes.add(_ResponsibleNode( + id: factory, + match: (value) => value is T, + create: (value) => factory(value), + )); + } + + static bool has(Event event, Responsible Function(T value) factory) { + return event.responsibleNodes.any((node) => node.id == factory); + } + + static Responsible of(Event event, T value) { + final node = + event.responsibleNodes.firstWhereOrNull((node) => node.match(value)); + if (node != null) { + return node.create(value); + } + + return switch (value) { + Responsible responsible => responsible, + Stream raw => _RawResponsible(raw), + Stream> raw => _RawResponsible(raw.map(Uint8List.fromList)), + Map value => _JsonResponsible(value), + List value => _JsonResponsible(value), + String text => _TextResponsible(text), + num(toString: final toString) => _TextResponsible(toString()), + bool(toString: final toString) => _TextResponsible(toString()), + _ => _fallbackOf(value), + }; + } + + static Responsible _fallbackOf(value) { + try { + return _JsonResponsible(value.toJson()); + } catch (_) {} + + throw Exception('The ${value.runtimeType} value is not responsible.'); + } +} + +class _ResponsibleNode { + const _ResponsibleNode({ + required this.id, + required this.match, + required this.create, + }); + + final Object id; + final bool Function(dynamic value) match; + final Responsible Function(dynamic value) create; +} + +class _TextResponsible implements Responsible { + const _TextResponsible(this.text); + + final String text; + + @override + Future createResponse(Event event) async { + return Response.text(text); + } +} + +class _RawResponsible implements Responsible { + const _RawResponsible(this.raw); + + final Stream raw; + + @override + Future createResponse(Event event) async { + return Response(raw); + } +} + +class _JsonResponsible implements Responsible { + const _JsonResponsible(this.value); + + final dynamic value; + + @override + Future createResponse(Event event) async { + return Response.json(value); + } +} + +extension on Event { + static const key = #spry.responsible.nodes; + + List<_ResponsibleNode> get responsibleNodes { + return locals.getOrSet>( + key, () => <_ResponsibleNode>[]); + } +} + +extension on Iterable { + T? firstWhereOrNull(bool Function(T element) test) { + for (final element in this) { + if (test(element)) return element; + } + + return null; + } +} diff --git a/packages/spry/lib/src/routing/routes_builder+ws.dart b/packages/spry/lib/src/routing/routes_builder+ws.dart new file mode 100644 index 0000000..bd411ed --- /dev/null +++ b/packages/spry/lib/src/routing/routes_builder+ws.dart @@ -0,0 +1,51 @@ +// ignore_for_file: file_names + +import 'dart:async'; + +import '../_constants.dart'; +import '../event/event.dart'; +import '../http/headers/headers.dart'; +import '../http/response.dart'; +import '../platform/platform.dart'; +import '../types.dart'; +import '../websocket/compression_options.dart'; +import 'routes_builder.dart'; +import 'routes_builder+all.dart'; + +extension RoutesBuilderWS on RoutesBuilder { + void ws( + String route, + FutureOr Function(Event event, WebSocket ws) closure, { + FutureOr Function(Event event)? fallback, + CompressionOptions compression = const CompressionOptions(), + FutureOr Function(Event event)? makeHeaders, + Iterable? supportProtocols, + }) { + all(route, (event) async { + final options = UpgradeWebSocketOptions( + compression: compression, + supportProtocols: supportProtocols ?? const [], + headers: switch (await makeHeaders?.call(event)) { + Headers headers => headers, + _ => const Headers(), + }, + ); + final webSocket = await event.platform + .upgradeWebSocket(event, event.rawRequest, options); + if (webSocket == null) { + if (fallback != null) { + return fallback(event); + } + + return Response(null, status: 426); + } + + await closure(event, webSocket); + }); + } +} + +extension on Event { + get rawRequest => locals.get(kRawRequest); + Platform get platform => locals.get(Platform); +} diff --git a/packages/spry/lib/src/spry+fallback.dart b/packages/spry/lib/src/spry+fallback.dart index af5d29b..51e84d3 100644 --- a/packages/spry/lib/src/spry+fallback.dart +++ b/packages/spry/lib/src/spry+fallback.dart @@ -8,28 +8,29 @@ import 'handler/handler.dart'; import 'http/response.dart'; import 'locals/locals+get_or_null.dart'; import 'spry.dart'; -import 'utils/next.dart'; extension SpryFallback on Spry { void fallback(FutureOr Function(Event event) closure) { - locals.set(_FailbackHandler, ClosureHandler(closure)); + locals.set(_FallbackHandler, ClosureHandler(closure)); } Handler getFallback() { - return switch (locals.getOrNull(_FailbackHandler)) { + return switch (locals.getOrNull(_FallbackHandler)) { Handler handler => handler, - _ => const _DefaultFailbackHandler(), + _ => const _DefaultFallbackHandler(), }; } } -abstract final class _FailbackHandler implements Handler { - const _FailbackHandler(); +abstract final class _FallbackHandler implements Handler { + const _FallbackHandler(); } -final class _DefaultFailbackHandler extends _FailbackHandler { - const _DefaultFailbackHandler(); +final class _DefaultFallbackHandler extends _FallbackHandler { + const _DefaultFallbackHandler(); @override - Future handle(Event event) => next(event); + Future handle(Event event) async { + return Response.text('Not Found.', status: 404); + } } diff --git a/packages/spry/lib/src/types.dart b/packages/spry/lib/src/types.dart index 8c789d2..cdde499 100644 --- a/packages/spry/lib/src/types.dart +++ b/packages/spry/lib/src/types.dart @@ -1 +1,2 @@ export 'package:routingkit/routingkit.dart' hide kCatchall; +export 'package:web_socket/web_socket.dart'; diff --git a/packages/spry/lib/src/websocket/compression_options.dart b/packages/spry/lib/src/websocket/compression_options.dart new file mode 100644 index 0000000..f523958 --- /dev/null +++ b/packages/spry/lib/src/websocket/compression_options.dart @@ -0,0 +1,49 @@ +/// Options controlling compression in a [WebSocket]. +/// +/// A [CompressionOptions] instance can be passed to [WebSocket.connect], or +/// used in other similar places where [WebSocket] compression is configured. +/// +/// In most cases the default [compressionDefault] is sufficient, but in some +/// situations, it might be desirable to use different compression parameters, +/// for example to preserve memory on small devices. +class CompressionOptions { + const CompressionOptions( + {this.clientNoContextTakeover = false, + this.serverNoContextTakeover = false, + this.clientMaxWindowBits, + this.serverMaxWindowBits, + this.enabled = true}); + + /// Whether the client will reuse its compression instances. + final bool clientNoContextTakeover; + + /// Whether the server will reuse its compression instances. + final bool serverNoContextTakeover; + + /// The maximal window size bit count requested by the client. + /// + /// The windows size for the compression is always a power of two, so the + /// number of bits precisely determines the window size. + /// + /// If set to `null`, the client has no preference, and the compression can + /// use up to its default maximum window size of 15 bits depending on the + /// server's preference. + final int? clientMaxWindowBits; + + /// The maximal window size bit count requested by the server. + /// + /// The windows size for the compression is always a power of two, so the + /// number of bits precisely determines the window size. + /// + /// If set to `null`, the server has no preference, and the compression can + /// use up to its default maximum window size of 15 bits depending on the + /// client's preference. + final int? serverMaxWindowBits; + + /// Whether WebSocket compression is enabled. + /// + /// If not enabled, the remaining fields have no effect, and the + /// [compressionOff] instance can, and should, be reused instead of creating a + /// new instance with compression disabled. + final bool enabled; +} diff --git a/packages/spry/pubspec.yaml b/packages/spry/pubspec.yaml index cbf5819..82fc0c2 100644 --- a/packages/spry/pubspec.yaml +++ b/packages/spry/pubspec.yaml @@ -6,8 +6,8 @@ environment: sdk: ^3.4.3 dependencies: - http_parser: ^4.1.0 routingkit: ^1.1.0 + web_socket: ^0.1.5 dev_dependencies: lints: ^4.0.0 From e3583a2397e9830fe3f4f439e9febba762327a82 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Tue, 9 Jul 2024 02:08:12 +0800 Subject: [PATCH 17/35] chore: WIP --- packages/spry/lib/plain.dart | 9 +++++---- packages/spry/lib/spry.dart | 2 ++ .../spry/lib/src/{_constants.dart => constants.dart} | 0 packages/spry/lib/src/event/event+headers.dart | 8 ++++++++ packages/spry/pubspec.yaml | 2 +- 5 files changed, 16 insertions(+), 5 deletions(-) rename packages/spry/lib/src/{_constants.dart => constants.dart} (100%) create mode 100644 packages/spry/lib/src/event/event+headers.dart diff --git a/packages/spry/lib/plain.dart b/packages/spry/lib/plain.dart index 337e66b..3d02afa 100644 --- a/packages/spry/lib/plain.dart +++ b/packages/spry/lib/plain.dart @@ -27,6 +27,8 @@ class PlainRequest implements Request { @override final String method; + + WebSocket? ws; } class PlainPlatform implements Platform { @@ -59,9 +61,8 @@ class PlainPlatform implements Platform { } @override - Future upgradeWebSocket( - Event event, PlainRequest request, UpgradeWebSocketOptions options) { - // TODO: implement upgradeWebSocket - throw UnimplementedError(); + Future upgradeWebSocket(Event event, PlainRequest request, + UpgradeWebSocketOptions options) async { + return null; } } diff --git a/packages/spry/lib/spry.dart b/packages/spry/lib/spry.dart index dcd7784..6ff63ca 100644 --- a/packages/spry/lib/spry.dart +++ b/packages/spry/lib/spry.dart @@ -1,5 +1,6 @@ library spry; +export 'src/constants.dart'; export 'src/spry.dart'; export 'src/spry+create_platform_handler.dart'; export 'src/spry+use.dart'; @@ -8,6 +9,7 @@ export 'src/types.dart'; export 'src/event/event.dart' hide EventImpl; export 'src/event/event+app.dart'; +export 'src/event/event+headers.dart'; export 'src/event/event+method.dart'; export 'src/event/event+params.dart'; export 'src/event/event+route.dart'; diff --git a/packages/spry/lib/src/_constants.dart b/packages/spry/lib/src/constants.dart similarity index 100% rename from packages/spry/lib/src/_constants.dart rename to packages/spry/lib/src/constants.dart diff --git a/packages/spry/lib/src/event/event+headers.dart b/packages/spry/lib/src/event/event+headers.dart new file mode 100644 index 0000000..6a6b4dc --- /dev/null +++ b/packages/spry/lib/src/event/event+headers.dart @@ -0,0 +1,8 @@ +// ignore_for_file: file_names + +import '../http/headers/headers.dart'; +import 'event.dart'; + +extension EventHeaders on Event { + Headers get headers => request.headers; +} diff --git a/packages/spry/pubspec.yaml b/packages/spry/pubspec.yaml index 82fc0c2..ebb1d78 100644 --- a/packages/spry/pubspec.yaml +++ b/packages/spry/pubspec.yaml @@ -1,6 +1,6 @@ name: spry description: A starting point for Dart libraries or applications. -version: 1.0.0 +version: 4.0.0-dev.0 environment: sdk: ^3.4.3 From 1a135be539d8d6b5b67c23c193fcb67f6c382633 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Tue, 9 Jul 2024 03:27:19 +0800 Subject: [PATCH 18/35] chore: WIP --- packages/spry/lib/io.dart | 117 ++++++++++++++++++ packages/spry/lib/spry.dart | 1 + .../spry/lib/src/event/event+responded.dart | 14 +++ packages/spry/lib/src/event/event+uri.dart | 2 +- .../src/platform/platform+create_handler.dart | 2 +- packages/spry/lib/src/platform/platform.dart | 4 +- .../lib/src/routing/routes_builder+ws.dart | 8 +- 7 files changed, 141 insertions(+), 7 deletions(-) create mode 100644 packages/spry/lib/io.dart create mode 100644 packages/spry/lib/src/event/event+responded.dart diff --git a/packages/spry/lib/io.dart b/packages/spry/lib/io.dart new file mode 100644 index 0000000..7f59f56 --- /dev/null +++ b/packages/spry/lib/io.dart @@ -0,0 +1,117 @@ +library spry.platform.io; + +import 'dart:io' hide WebSocket; +import 'dart:io' as io show CompressionOptions; +import 'dart:typed_data'; + +import 'package:web_socket/io_web_socket.dart'; + +import 'spry.dart'; + +class IOPlatform implements Platform { + const IOPlatform(); + + @override + Stream? getRequestBody(Event event, HttpRequest request) { + return request; + } + + @override + Headers getRequestHeaders(Event event, HttpRequest request) { + final builder = HeadersBuilder(); + request.headers.forEach((name, values) { + for (final value in values) { + builder.add(name, value); + } + }); + + return builder.toHeaders(); + } + + @override + String getRequestMethod(Event event, HttpRequest request) { + return request.method; + } + + @override + Uri getRequestURI(Event event, HttpRequest request) { + return request.requestedUri; + } + + @override + Future respond( + Event event, HttpRequest request, Response response) async { + final httpResponse = request.response; + if (event.responded) { + await httpResponse.close(); + return; + } + + httpResponse.statusCode = response.status; + httpResponse.reasonPhrase = response.statusText; + + for (final (name, value) in response.headers) { + httpResponse.headers.add(name, value); + } + + final body = response.body; + if (body != null) { + await httpResponse.addStream(body); + } + + await httpResponse.close(); + } + + @override + Future upgradeWebSocket( + Event event, HttpRequest request, UpgradeWebSocketOptions options) async { + if (!WebSocketTransformer.isUpgradeRequest(request)) { + return null; + } + + final response = request.response; + for (final (name, value) in options.headers) { + response.headers.add(name, value); + } + + final websocket = await WebSocketTransformer.upgrade( + request, + compression: options.ioCompressionOptions, + protocolSelector: _createProtocolSelector(options.supportedProtocols), + ); + + return IOWebSocket.fromWebSocket(websocket); + } + + static Future Function(Iterable)? _createProtocolSelector( + Iterable? supportedProtocols) { + if (supportedProtocols == null || supportedProtocols.isEmpty) { + return null; + } + + final normalizedSupportedProtocols = + supportedProtocols.map((e) => e.trim().toLowerCase()); + + return (protocols) async { + for (final protocol in protocols) { + if (normalizedSupportedProtocols.contains(protocol)) { + return protocol; + } + } + + throw WebSocketException('Unsupported WebSocket protocol'); + }; + } +} + +extension on UpgradeWebSocketOptions { + io.CompressionOptions get ioCompressionOptions { + return io.CompressionOptions( + enabled: compression.enabled, + clientMaxWindowBits: compression.clientMaxWindowBits, + serverMaxWindowBits: compression.serverMaxWindowBits, + clientNoContextTakeover: compression.clientNoContextTakeover, + serverNoContextTakeover: compression.serverNoContextTakeover, + ); + } +} diff --git a/packages/spry/lib/spry.dart b/packages/spry/lib/spry.dart index 6ff63ca..3357bbf 100644 --- a/packages/spry/lib/spry.dart +++ b/packages/spry/lib/spry.dart @@ -12,6 +12,7 @@ export 'src/event/event+app.dart'; export 'src/event/event+headers.dart'; export 'src/event/event+method.dart'; export 'src/event/event+params.dart'; +export 'src/event/event+responded.dart'; export 'src/event/event+route.dart'; export 'src/event/event+uri.dart'; diff --git a/packages/spry/lib/src/event/event+responded.dart b/packages/spry/lib/src/event/event+responded.dart new file mode 100644 index 0000000..a176519 --- /dev/null +++ b/packages/spry/lib/src/event/event+responded.dart @@ -0,0 +1,14 @@ +// ignore_for_file: file_names + +import '../locals/locals+get_or_null.dart'; +import 'event.dart'; + +const _key = #spry.event.response.responded; + +extension EventResponded on Event { + bool get responded => locals.getOrNull(_key) == true; + + set responded(bool status) { + locals.set(_key, status); + } +} diff --git a/packages/spry/lib/src/event/event+uri.dart b/packages/spry/lib/src/event/event+uri.dart index 6720981..19c4498 100644 --- a/packages/spry/lib/src/event/event+uri.dart +++ b/packages/spry/lib/src/event/event+uri.dart @@ -2,6 +2,6 @@ import 'event.dart'; -extension EventUri on Event { +extension EventURI on Event { Uri get uri => request.uri; } diff --git a/packages/spry/lib/src/platform/platform+create_handler.dart b/packages/spry/lib/src/platform/platform+create_handler.dart index d34c13b..d0d2343 100644 --- a/packages/spry/lib/src/platform/platform+create_handler.dart +++ b/packages/spry/lib/src/platform/platform+create_handler.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'dart:typed_data'; -import '../_constants.dart'; +import '../constants.dart'; import '../event/event.dart'; import '../handler/handler.dart'; import '../http/headers/headers.dart'; diff --git a/packages/spry/lib/src/platform/platform.dart b/packages/spry/lib/src/platform/platform.dart index 258fe37..8f5b341 100644 --- a/packages/spry/lib/src/platform/platform.dart +++ b/packages/spry/lib/src/platform/platform.dart @@ -9,13 +9,13 @@ import '../websocket/compression_options.dart'; final class UpgradeWebSocketOptions { const UpgradeWebSocketOptions({ required this.compression, - required this.supportProtocols, required this.headers, + this.supportedProtocols, }); final CompressionOptions compression; final Headers headers; - final Iterable supportProtocols; + final Iterable? supportedProtocols; } abstract interface class Platform { diff --git a/packages/spry/lib/src/routing/routes_builder+ws.dart b/packages/spry/lib/src/routing/routes_builder+ws.dart index bd411ed..e3687c5 100644 --- a/packages/spry/lib/src/routing/routes_builder+ws.dart +++ b/packages/spry/lib/src/routing/routes_builder+ws.dart @@ -2,8 +2,9 @@ import 'dart:async'; -import '../_constants.dart'; +import '../constants.dart'; import '../event/event.dart'; +import '../event/event+responded.dart'; import '../http/headers/headers.dart'; import '../http/response.dart'; import '../platform/platform.dart'; @@ -19,12 +20,12 @@ extension RoutesBuilderWS on RoutesBuilder { FutureOr Function(Event event)? fallback, CompressionOptions compression = const CompressionOptions(), FutureOr Function(Event event)? makeHeaders, - Iterable? supportProtocols, + Iterable? supportedProtocols, }) { all(route, (event) async { final options = UpgradeWebSocketOptions( compression: compression, - supportProtocols: supportProtocols ?? const [], + supportedProtocols: supportedProtocols, headers: switch (await makeHeaders?.call(event)) { Headers headers => headers, _ => const Headers(), @@ -41,6 +42,7 @@ extension RoutesBuilderWS on RoutesBuilder { } await closure(event, webSocket); + event.responded = true; }); } } From 4690ee104ac39053664d2ba56f6e71403c283e82 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Tue, 9 Jul 2024 11:42:42 +0800 Subject: [PATCH 19/35] chore: WIP --- packages/spry/lib/constants.dart | 5 ++ packages/spry/lib/spry.dart | 6 +-- packages/spry/lib/src/constants.dart | 1 - packages/spry/lib/src/event/event+app.dart | 3 +- packages/spry/lib/src/event/event+params.dart | 3 +- .../spry/lib/src/event/event+responded.dart | 14 ----- packages/spry/lib/src/event/event+route.dart | 3 +- packages/spry/lib/src/event/event.dart | 24 ++++----- packages/spry/lib/src/locals/locals.dart | 44 +-------------- .../src/platform/platform+create_handler.dart | 54 ++++++++++++++++--- .../spry/lib/src/responsible/responsible.dart | 6 +-- .../lib/src/routing/routes_builder+ws.dart | 19 +++---- packages/spry/lib/src/spry+fallback.dart | 4 +- packages/spry/lib/src/spry.dart | 22 +++++++- .../lib/src/utils/_spry_internal_utils.dart | 7 +-- .../lib/src/websocket/websocket_hooks.dart | 3 ++ packages/spry/test/locals/locals_test.dart | 13 +++-- 17 files changed, 119 insertions(+), 112 deletions(-) create mode 100644 packages/spry/lib/constants.dart delete mode 100644 packages/spry/lib/src/constants.dart delete mode 100644 packages/spry/lib/src/event/event+responded.dart create mode 100644 packages/spry/lib/src/websocket/websocket_hooks.dart diff --git a/packages/spry/lib/constants.dart b/packages/spry/lib/constants.dart new file mode 100644 index 0000000..f403fe6 --- /dev/null +++ b/packages/spry/lib/constants.dart @@ -0,0 +1,5 @@ +const kAppInstance = #spry.app; +const kPlatform = #spry.platform; +const kRawRequest = #spry.event.request.raw; +const kEventParams = #spry.event.params; +const kEventRoute = #spry.event.route; diff --git a/packages/spry/lib/spry.dart b/packages/spry/lib/spry.dart index 3357bbf..a6481b8 100644 --- a/packages/spry/lib/spry.dart +++ b/packages/spry/lib/spry.dart @@ -1,18 +1,16 @@ library spry; -export 'src/constants.dart'; export 'src/spry.dart'; export 'src/spry+create_platform_handler.dart'; export 'src/spry+use.dart'; export 'src/spry+fallback.dart'; export 'src/types.dart'; -export 'src/event/event.dart' hide EventImpl; +export 'src/event/event.dart'; export 'src/event/event+app.dart'; export 'src/event/event+headers.dart'; export 'src/event/event+method.dart'; export 'src/event/event+params.dart'; -export 'src/event/event+responded.dart'; export 'src/event/event+route.dart'; export 'src/event/event+uri.dart'; @@ -36,7 +34,7 @@ export 'src/http/request.dart'; export 'src/http/response.dart'; export 'src/http/response+copy_with.dart'; -export 'src/locals/locals.dart' hide AppLocals, EventLocals; +export 'src/locals/locals.dart'; export 'src/locals/locals+get_or_null.dart'; export 'src/locals/locals+get_or_set.dart'; diff --git a/packages/spry/lib/src/constants.dart b/packages/spry/lib/src/constants.dart deleted file mode 100644 index a1caa50..0000000 --- a/packages/spry/lib/src/constants.dart +++ /dev/null @@ -1 +0,0 @@ -const kRawRequest = #spry.event.request.raw; diff --git a/packages/spry/lib/src/event/event+app.dart b/packages/spry/lib/src/event/event+app.dart index 23e4c78..4fea579 100644 --- a/packages/spry/lib/src/event/event+app.dart +++ b/packages/spry/lib/src/event/event+app.dart @@ -1,8 +1,9 @@ // ignore_for_file: file_names +import '../../constants.dart'; import '../spry.dart'; import 'event.dart'; extension EventApp on Event { - Spry get app => locals.get(Spry); + Spry get app => locals.get(kAppInstance); } diff --git a/packages/spry/lib/src/event/event+params.dart b/packages/spry/lib/src/event/event+params.dart index 87fd7bd..6ec9635 100644 --- a/packages/spry/lib/src/event/event+params.dart +++ b/packages/spry/lib/src/event/event+params.dart @@ -1,11 +1,12 @@ // ignore_for_file: file_names +import '../../constants.dart'; import '../locals/locals+get_or_set.dart'; import '../types.dart'; import 'event.dart'; extension EventParams on Event { Params get params { - return locals.getOrSet(Params, Params.new); + return locals.getOrSet(kEventParams, Params.new); } } diff --git a/packages/spry/lib/src/event/event+responded.dart b/packages/spry/lib/src/event/event+responded.dart deleted file mode 100644 index a176519..0000000 --- a/packages/spry/lib/src/event/event+responded.dart +++ /dev/null @@ -1,14 +0,0 @@ -// ignore_for_file: file_names - -import '../locals/locals+get_or_null.dart'; -import 'event.dart'; - -const _key = #spry.event.response.responded; - -extension EventResponded on Event { - bool get responded => locals.getOrNull(_key) == true; - - set responded(bool status) { - locals.set(_key, status); - } -} diff --git a/packages/spry/lib/src/event/event+route.dart b/packages/spry/lib/src/event/event+route.dart index d8db70f..83c1d5f 100644 --- a/packages/spry/lib/src/event/event+route.dart +++ b/packages/spry/lib/src/event/event+route.dart @@ -1,9 +1,10 @@ // ignore_for_file: file_names +import '../../constants.dart'; import '../locals/locals+get_or_null.dart'; import '../routing/route.dart'; import 'event.dart'; extension EventRoute on Event { - Route? get route => locals.getOrNull(Route); + Route? get route => locals.getOrNull(kEventRoute); } diff --git a/packages/spry/lib/src/event/event.dart b/packages/spry/lib/src/event/event.dart index 77f089b..efb3829 100644 --- a/packages/spry/lib/src/event/event.dart +++ b/packages/spry/lib/src/event/event.dart @@ -1,22 +1,22 @@ import '../http/request.dart'; import '../locals/locals.dart'; -abstract final class Event { +abstract interface class Event { Locals get locals; Request get request; } -final class EventImpl implements Event { - EventImpl({ - required this.appLocals, - required this.request, - }); +// final class EventImpl implements Event { +// EventImpl({ +// required this.appLocals, +// required this.request, +// }); - final Locals appLocals; +// final Locals appLocals; - @override - late final Locals locals = EventLocals(appLocals); +// @override +// late final Locals locals = EventLocals(appLocals); - @override - final Request request; -} +// @override +// final Request request; +// } diff --git a/packages/spry/lib/src/locals/locals.dart b/packages/spry/lib/src/locals/locals.dart index 6168388..443c92e 100644 --- a/packages/spry/lib/src/locals/locals.dart +++ b/packages/spry/lib/src/locals/locals.dart @@ -1,47 +1,5 @@ -abstract final class Locals { +abstract interface class Locals { T get(Object key); void set(Object key, T value); void remove(Object key); } - -final class AppLocals implements Locals { - final Map locals = {}; - - @override - T get(Object key) => locals[key]; - - @override - void set(Object key, T value) { - locals[key] = value; - } - - @override - void remove(Object key) { - locals.remove(key); - } -} - -final class EventLocals implements Locals { - EventLocals(this.appLocals); - - final Locals appLocals; - final Map locals = {}; - - @override - T get(Object key) { - return switch (locals[key]) { - null => appLocals.get(key), - Object value => value as T, - }; - } - - @override - void remove(Object key) { - locals.remove(key); - } - - @override - void set(Object key, T value) { - locals[key] = value; - } -} diff --git a/packages/spry/lib/src/platform/platform+create_handler.dart b/packages/spry/lib/src/platform/platform+create_handler.dart index d0d2343..28c84d2 100644 --- a/packages/spry/lib/src/platform/platform+create_handler.dart +++ b/packages/spry/lib/src/platform/platform+create_handler.dart @@ -3,7 +3,9 @@ import 'dart:convert'; import 'dart:typed_data'; -import '../constants.dart'; +import 'package:spry/src/locals/locals.dart'; + +import '../../constants.dart'; import '../event/event.dart'; import '../handler/handler.dart'; import '../http/headers/headers.dart'; @@ -24,10 +26,12 @@ extension PlatformAdapterCreateHandler on Platform { return (T raw) async { final request = _RequestImpl(); - final event = EventImpl(appLocals: app.locals, request: request); + final locals = _RequestEventLocals(app); + + locals.set(kPlatform, this); + locals.set(kRawRequest, raw); - event.locals.set(Platform, this); - event.locals.set(kRawRequest, raw); + final event = _RequestEvent(locals: locals, request: request); request.method = getRequestMethod(event, raw).toUpperCase().trim(); request.uri = getRequestURI(event, raw); @@ -41,9 +45,9 @@ extension PlatformAdapterCreateHandler on Platform { _ => app.getFallback(), }; - event.locals.set(Params, result?.params); + locals.set(kEventParams, result?.params); if (result != null) { - event.locals.set(Route, Route(id: result.route)); + locals.set(kEventRoute, Route(id: result.route)); } final response = await handleWith(handler, event); @@ -104,3 +108,41 @@ extension on Router { }; } } + +class _RequestEvent implements Event { + const _RequestEvent({ + required this.request, + required this.locals, + }); + + @override + final _RequestEventLocals locals; + + @override + final Request request; +} + +class _RequestEventLocals implements Locals { + _RequestEventLocals(this.app); + + final Spry app; + final Map locals = {}; + + @override + T get(Object key) { + return switch (locals[key]) { + T value => value, + _ => app.locals.get(key), + }; + } + + @override + void remove(Object key) { + locals.remove(key); + } + + @override + void set(Object key, T value) { + locals[key] = value; + } +} diff --git a/packages/spry/lib/src/responsible/responsible.dart b/packages/spry/lib/src/responsible/responsible.dart index 0432783..975dc06 100644 --- a/packages/spry/lib/src/responsible/responsible.dart +++ b/packages/spry/lib/src/responsible/responsible.dart @@ -98,11 +98,11 @@ class _JsonResponsible implements Responsible { } extension on Event { - static const key = #spry.responsible.nodes; - List<_ResponsibleNode> get responsibleNodes { return locals.getOrSet>( - key, () => <_ResponsibleNode>[]); + #spry.responsible.nodes, + () => <_ResponsibleNode>[], + ); } } diff --git a/packages/spry/lib/src/routing/routes_builder+ws.dart b/packages/spry/lib/src/routing/routes_builder+ws.dart index e3687c5..f980fe5 100644 --- a/packages/spry/lib/src/routing/routes_builder+ws.dart +++ b/packages/spry/lib/src/routing/routes_builder+ws.dart @@ -2,9 +2,8 @@ import 'dart:async'; -import '../constants.dart'; +import '../../constants.dart'; import '../event/event.dart'; -import '../event/event+responded.dart'; import '../http/headers/headers.dart'; import '../http/response.dart'; import '../platform/platform.dart'; @@ -23,6 +22,8 @@ extension RoutesBuilderWS on RoutesBuilder { Iterable? supportedProtocols, }) { all(route, (event) async { + final raw = event.locals.get(kRawRequest); + final platform = event.locals.get(kPlatform); final options = UpgradeWebSocketOptions( compression: compression, supportedProtocols: supportedProtocols, @@ -31,9 +32,9 @@ extension RoutesBuilderWS on RoutesBuilder { _ => const Headers(), }, ); - final webSocket = await event.platform - .upgradeWebSocket(event, event.rawRequest, options); - if (webSocket == null) { + final websocket = await platform.upgradeWebSocket(event, raw, options); + + if (websocket == null) { if (fallback != null) { return fallback(event); } @@ -41,13 +42,7 @@ extension RoutesBuilderWS on RoutesBuilder { return Response(null, status: 426); } - await closure(event, webSocket); - event.responded = true; + await closure(event, websocket); }); } } - -extension on Event { - get rawRequest => locals.get(kRawRequest); - Platform get platform => locals.get(Platform); -} diff --git a/packages/spry/lib/src/spry+fallback.dart b/packages/spry/lib/src/spry+fallback.dart index 51e84d3..a692206 100644 --- a/packages/spry/lib/src/spry+fallback.dart +++ b/packages/spry/lib/src/spry+fallback.dart @@ -11,11 +11,11 @@ import 'spry.dart'; extension SpryFallback on Spry { void fallback(FutureOr Function(Event event) closure) { - locals.set(_FallbackHandler, ClosureHandler(closure)); + locals.set(#spry.app.fallback, ClosureHandler(closure)); } Handler getFallback() { - return switch (locals.getOrNull(_FallbackHandler)) { + return switch (locals.getOrNull(#spry.app.fallback)) { Handler handler => handler, _ => const _DefaultFallbackHandler(), }; diff --git a/packages/spry/lib/src/spry.dart b/packages/spry/lib/src/spry.dart index 77da9a0..516f95e 100644 --- a/packages/spry/lib/src/spry.dart +++ b/packages/spry/lib/src/spry.dart @@ -1,3 +1,4 @@ +import '../constants.dart'; import 'handler/handler.dart'; import 'locals/locals.dart'; import 'routing/routes_builder.dart'; @@ -12,7 +13,7 @@ class Spry implements RoutesBuilder { final RouterDriver routerDriver = const RadixTrieRouterDriver(), final bool caseSensitive = false, }) { - final appLocals = AppLocals(); + final appLocals = _AppLocals(); if (locals != null && locals.isNotEmpty) { appLocals.locals.addAll(locals); } @@ -24,7 +25,7 @@ class Spry implements RoutesBuilder { _ => createRouter(driver: routerDriver, caseSensitive: caseSensitive) }, ); - appLocals.set(Spry, app); + appLocals.set(kAppInstance, app); return app; } @@ -37,3 +38,20 @@ class Spry implements RoutesBuilder { router.register('${method.toUpperCase()}/$route', handler); } } + +final class _AppLocals implements Locals { + final Map locals = {}; + + @override + T get(Object key) => locals[key]; + + @override + void set(Object key, T value) { + locals[key] = value; + } + + @override + void remove(Object key) { + locals.remove(key); + } +} diff --git a/packages/spry/lib/src/utils/_spry_internal_utils.dart b/packages/spry/lib/src/utils/_spry_internal_utils.dart index 1cc6602..9569c53 100644 --- a/packages/spry/lib/src/utils/_spry_internal_utils.dart +++ b/packages/spry/lib/src/utils/_spry_internal_utils.dart @@ -6,10 +6,11 @@ import '../spry.dart'; import 'next.dart'; extension SpryInternalUtils on Spry { - static const handlersKey = #spry.app.handlers; - List get handlers { - return locals.getOrSet>(handlersKey, () => []); + return locals.getOrSet>( + #spry.app.handlers, + () => [], + ); } Future Function(Handler, Event) createHandleWith() { diff --git a/packages/spry/lib/src/websocket/websocket_hooks.dart b/packages/spry/lib/src/websocket/websocket_hooks.dart new file mode 100644 index 0000000..b069054 --- /dev/null +++ b/packages/spry/lib/src/websocket/websocket_hooks.dart @@ -0,0 +1,3 @@ +abstract interface class WebSocketHooks {} + +// abstract interface class diff --git a/packages/spry/test/locals/locals_test.dart b/packages/spry/test/locals/locals_test.dart index be105a2..5b2066b 100644 --- a/packages/spry/test/locals/locals_test.dart +++ b/packages/spry/test/locals/locals_test.dart @@ -1,21 +1,20 @@ import 'package:spry/spry.dart'; -import 'package:spry/src/locals/locals.dart'; import 'package:test/test.dart'; void main() { test('.set', () { - final locals = AppLocals(); + final locals = Spry().locals; locals.set('a', 1); locals.set(#app, 2); - expect(locals.locals['a'], equals(1)); - expect(locals.locals[#app], equals(2)); - expect(locals.locals[1], isNull); + expect(locals.get('a'), equals(1)); + expect(locals.get(#app), equals(2)); + expect(locals.get(1), isNull); }); test('.get', () { - final locals = AppLocals(); + final locals = Spry().locals; locals.set(#app, 1); @@ -25,7 +24,7 @@ void main() { }); test('.getOrNull', () { - final locals = AppLocals(); + final locals = Spry().locals; locals.set(#app, 1); From 5770920a62bf090524b649931ccf27329918d92d Mon Sep 17 00:00:00 2001 From: Seven Du Date: Tue, 9 Jul 2024 15:27:38 +0800 Subject: [PATCH 20/35] chore: Support websocket --- packages/spry/example/main.dart | 6 +- packages/spry/lib/io.dart | 101 ++++++++++--- packages/spry/lib/plain.dart | 10 +- packages/spry/lib/spry.dart | 4 - packages/spry/lib/src/event/event.dart | 15 -- .../lib/src/locals/locals+get_or_null.dart | 2 +- packages/spry/lib/src/platform/platform.dart | 16 --- .../lib/src/routing/routes_builder+ws.dart | 48 ------- .../lib/src/spry+create_platform_handler.dart | 11 -- packages/spry/lib/src/types.dart | 1 - .../src/websocket/compression_options.dart | 49 ------- .../lib/src/websocket/websocket_hooks.dart | 3 - packages/spry/lib/websocket.dart | 136 ++++++++++++++++++ packages/spry/pubspec.yaml | 1 - 14 files changed, 226 insertions(+), 177 deletions(-) delete mode 100644 packages/spry/lib/src/routing/routes_builder+ws.dart delete mode 100644 packages/spry/lib/src/spry+create_platform_handler.dart delete mode 100644 packages/spry/lib/src/websocket/compression_options.dart delete mode 100644 packages/spry/lib/src/websocket/websocket_hooks.dart create mode 100644 packages/spry/lib/websocket.dart diff --git a/packages/spry/example/main.dart b/packages/spry/example/main.dart index 468f858..3d06644 100644 --- a/packages/spry/example/main.dart +++ b/packages/spry/example/main.dart @@ -8,12 +8,8 @@ void main() async { // Adds a `GET /hello` route, Response body with 'Hello Spry!' app.get('hello', (event) => 'Hello Spry!'); - // Creates a plain platfrom. - const plain = PlainPlatform(); - // Creates a plain platfrom handler. - final handler = plain.createHandler(app); - + final handler = app.toPlainHandler(); final request = PlainRequest(method: 'get', uri: Uri(path: 'hello')); final response = await handler(request); diff --git a/packages/spry/lib/io.dart b/packages/spry/lib/io.dart index 7f59f56..5eff297 100644 --- a/packages/spry/lib/io.dart +++ b/packages/spry/lib/io.dart @@ -1,14 +1,18 @@ library spry.platform.io; -import 'dart:io' hide WebSocket; -import 'dart:io' as io show CompressionOptions; +import 'dart:async'; +import 'dart:io'; import 'dart:typed_data'; -import 'package:web_socket/io_web_socket.dart'; - import 'spry.dart'; +import 'websocket.dart' hide CompressionOptions; + +const _kUpgradedWebSocket = #spry.io.upgraded.websocket; -class IOPlatform implements Platform { +class IOPlatform + implements + Platform, + WebSocketPlatform { const IOPlatform(); @override @@ -41,12 +45,11 @@ class IOPlatform implements Platform { @override Future respond( Event event, HttpRequest request, Response response) async { - final httpResponse = request.response; - if (event.responded) { - await httpResponse.close(); + if (event.locals.getOrNull(_kUpgradedWebSocket) == true) { return; } + final httpResponse = request.response; httpResponse.statusCode = response.status; httpResponse.reasonPhrase = response.statusText; @@ -63,13 +66,16 @@ class IOPlatform implements Platform { } @override - Future upgradeWebSocket( - Event event, HttpRequest request, UpgradeWebSocketOptions options) async { - if (!WebSocketTransformer.isUpgradeRequest(request)) { - return null; + websocket(Event event, HttpRequest request, Hooks hooks) async { + if (event.locals.getOrNull(_kUpgradedWebSocket) == true) { + throw HttpException('The current request has been upgraded to WebSocket', + uri: event.uri); + } else if (!WebSocketTransformer.isUpgradeRequest(request)) { + return hooks.fallback(event); } final response = request.response; + final options = await hooks.onUpgrade(event); for (final (name, value) in options.headers) { response.headers.add(name, value); } @@ -77,10 +83,27 @@ class IOPlatform implements Platform { final websocket = await WebSocketTransformer.upgrade( request, compression: options.ioCompressionOptions, - protocolSelector: _createProtocolSelector(options.supportedProtocols), + protocolSelector: _createProtocolSelector(options.protocols), + ); + final peer = _IOPeer(event, websocket); + + websocket.listen( + (payload) async { + final message = switch (payload) { + Uint8List bytes => Message.bytes(bytes), + List bytes => Message.bytes(Uint8List.fromList(bytes)), + String text => Message.text(text), + _ => throw WebSocketException('Unsupported payload message.'), + }; + + return hooks.onMessage(peer, message); + }, + onError: (error) => hooks.onError(peer, error), + onDone: () => hooks.onClose(peer, + code: websocket.closeCode, reason: websocket.closeReason), ); - return IOWebSocket.fromWebSocket(websocket); + event.locals.set(_kUpgradedWebSocket, true); } static Future Function(Iterable)? _createProtocolSelector( @@ -104,9 +127,15 @@ class IOPlatform implements Platform { } } -extension on UpgradeWebSocketOptions { - io.CompressionOptions get ioCompressionOptions { - return io.CompressionOptions( +extension SpryToIOHandler on Spry { + Future Function(HttpRequest) toIOHandler() { + return const IOPlatform().createHandler(this); + } +} + +extension on CreatePeerOptions { + CompressionOptions get ioCompressionOptions { + return CompressionOptions( enabled: compression.enabled, clientMaxWindowBits: compression.clientMaxWindowBits, serverMaxWindowBits: compression.serverMaxWindowBits, @@ -115,3 +144,41 @@ extension on UpgradeWebSocketOptions { ); } } + +class _IOPeer implements Peer { + const _IOPeer(this.event, this.websocket); + + final Event event; + final WebSocket websocket; + + @override + Locals get locals => event.locals; + + @override + ReadyState get readyState => ReadyState(websocket.readyState); + + @override + Request get request => event.request; + + @override + void send(Uint8List message) { + websocket.add(message); + } + + @override + void sendText(String message) { + websocket.add(message); + } + + @override + Future close([int? code, String? reason]) { + // TODO: implement close + throw UnimplementedError(); + } + + @override + String get extensions => websocket.extensions; + + @override + String? get protocol => websocket.protocol; +} diff --git a/packages/spry/lib/plain.dart b/packages/spry/lib/plain.dart index 3d02afa..86ebb3f 100644 --- a/packages/spry/lib/plain.dart +++ b/packages/spry/lib/plain.dart @@ -27,8 +27,6 @@ class PlainRequest implements Request { @override final String method; - - WebSocket? ws; } class PlainPlatform implements Platform { @@ -59,10 +57,10 @@ class PlainPlatform implements Platform { String getRequestMethod(Event event, PlainRequest request) { return request.method; } +} - @override - Future upgradeWebSocket(Event event, PlainRequest request, - UpgradeWebSocketOptions options) async { - return null; +extension SpryToPlainHandler on Spry { + Future Function(PlainRequest) toPlainHandler() { + return const PlainPlatform().createHandler(this); } } diff --git a/packages/spry/lib/spry.dart b/packages/spry/lib/spry.dart index a6481b8..633256d 100644 --- a/packages/spry/lib/spry.dart +++ b/packages/spry/lib/spry.dart @@ -1,7 +1,6 @@ library spry; export 'src/spry.dart'; -export 'src/spry+create_platform_handler.dart'; export 'src/spry+use.dart'; export 'src/spry+fallback.dart'; export 'src/types.dart'; @@ -49,8 +48,5 @@ export 'src/routing/routes_builder.dart'; export 'src/routing/routes_builder+on.dart'; export 'src/routing/routes_builder+all.dart'; export 'src/routing/routes_builder+methods.dart'; -export 'src/routing/routes_builder+ws.dart'; export 'src/utils/next.dart'; - -export 'src/websocket/compression_options.dart'; diff --git a/packages/spry/lib/src/event/event.dart b/packages/spry/lib/src/event/event.dart index efb3829..57f15e8 100644 --- a/packages/spry/lib/src/event/event.dart +++ b/packages/spry/lib/src/event/event.dart @@ -5,18 +5,3 @@ abstract interface class Event { Locals get locals; Request get request; } - -// final class EventImpl implements Event { -// EventImpl({ -// required this.appLocals, -// required this.request, -// }); - -// final Locals appLocals; - -// @override -// late final Locals locals = EventLocals(appLocals); - -// @override -// final Request request; -// } diff --git a/packages/spry/lib/src/locals/locals+get_or_null.dart b/packages/spry/lib/src/locals/locals+get_or_null.dart index f4e6810..1f37c01 100644 --- a/packages/spry/lib/src/locals/locals+get_or_null.dart +++ b/packages/spry/lib/src/locals/locals+get_or_null.dart @@ -5,7 +5,7 @@ import 'locals.dart'; extension LocalsGetOrNull on Locals { T? getOrNull(Object key) { try { - return get(key); + return get(key); } catch (_) { return null; } diff --git a/packages/spry/lib/src/platform/platform.dart b/packages/spry/lib/src/platform/platform.dart index 8f5b341..11274bc 100644 --- a/packages/spry/lib/src/platform/platform.dart +++ b/packages/spry/lib/src/platform/platform.dart @@ -3,28 +3,12 @@ import 'dart:typed_data'; import '../event/event.dart'; import '../http/headers/headers.dart'; import '../http/response.dart'; -import '../types.dart'; -import '../websocket/compression_options.dart'; - -final class UpgradeWebSocketOptions { - const UpgradeWebSocketOptions({ - required this.compression, - required this.headers, - this.supportedProtocols, - }); - - final CompressionOptions compression; - final Headers headers; - final Iterable? supportedProtocols; -} abstract interface class Platform { String getRequestMethod(Event event, T request); Uri getRequestURI(Event event, T request); Headers getRequestHeaders(Event event, T request); Stream? getRequestBody(Event event, T request); - Future upgradeWebSocket( - Event event, T request, UpgradeWebSocketOptions options); Future respond(Event event, T request, Response response); } diff --git a/packages/spry/lib/src/routing/routes_builder+ws.dart b/packages/spry/lib/src/routing/routes_builder+ws.dart deleted file mode 100644 index f980fe5..0000000 --- a/packages/spry/lib/src/routing/routes_builder+ws.dart +++ /dev/null @@ -1,48 +0,0 @@ -// ignore_for_file: file_names - -import 'dart:async'; - -import '../../constants.dart'; -import '../event/event.dart'; -import '../http/headers/headers.dart'; -import '../http/response.dart'; -import '../platform/platform.dart'; -import '../types.dart'; -import '../websocket/compression_options.dart'; -import 'routes_builder.dart'; -import 'routes_builder+all.dart'; - -extension RoutesBuilderWS on RoutesBuilder { - void ws( - String route, - FutureOr Function(Event event, WebSocket ws) closure, { - FutureOr Function(Event event)? fallback, - CompressionOptions compression = const CompressionOptions(), - FutureOr Function(Event event)? makeHeaders, - Iterable? supportedProtocols, - }) { - all(route, (event) async { - final raw = event.locals.get(kRawRequest); - final platform = event.locals.get(kPlatform); - final options = UpgradeWebSocketOptions( - compression: compression, - supportedProtocols: supportedProtocols, - headers: switch (await makeHeaders?.call(event)) { - Headers headers => headers, - _ => const Headers(), - }, - ); - final websocket = await platform.upgradeWebSocket(event, raw, options); - - if (websocket == null) { - if (fallback != null) { - return fallback(event); - } - - return Response(null, status: 426); - } - - await closure(event, websocket); - }); - } -} diff --git a/packages/spry/lib/src/spry+create_platform_handler.dart b/packages/spry/lib/src/spry+create_platform_handler.dart deleted file mode 100644 index e09c668..0000000 --- a/packages/spry/lib/src/spry+create_platform_handler.dart +++ /dev/null @@ -1,11 +0,0 @@ -// ignore_for_file: file_names - -import 'platform/platform.dart'; -import 'platform/platform+create_handler.dart'; -import 'platform/platform_handler.dart'; -import 'spry.dart'; - -extension SpryCreatePlatfromHandler on Spry { - PlatformHandler createPlatformHandler(Platform platform) => - platform.createHandler(this); -} diff --git a/packages/spry/lib/src/types.dart b/packages/spry/lib/src/types.dart index cdde499..8c789d2 100644 --- a/packages/spry/lib/src/types.dart +++ b/packages/spry/lib/src/types.dart @@ -1,2 +1 @@ export 'package:routingkit/routingkit.dart' hide kCatchall; -export 'package:web_socket/web_socket.dart'; diff --git a/packages/spry/lib/src/websocket/compression_options.dart b/packages/spry/lib/src/websocket/compression_options.dart deleted file mode 100644 index f523958..0000000 --- a/packages/spry/lib/src/websocket/compression_options.dart +++ /dev/null @@ -1,49 +0,0 @@ -/// Options controlling compression in a [WebSocket]. -/// -/// A [CompressionOptions] instance can be passed to [WebSocket.connect], or -/// used in other similar places where [WebSocket] compression is configured. -/// -/// In most cases the default [compressionDefault] is sufficient, but in some -/// situations, it might be desirable to use different compression parameters, -/// for example to preserve memory on small devices. -class CompressionOptions { - const CompressionOptions( - {this.clientNoContextTakeover = false, - this.serverNoContextTakeover = false, - this.clientMaxWindowBits, - this.serverMaxWindowBits, - this.enabled = true}); - - /// Whether the client will reuse its compression instances. - final bool clientNoContextTakeover; - - /// Whether the server will reuse its compression instances. - final bool serverNoContextTakeover; - - /// The maximal window size bit count requested by the client. - /// - /// The windows size for the compression is always a power of two, so the - /// number of bits precisely determines the window size. - /// - /// If set to `null`, the client has no preference, and the compression can - /// use up to its default maximum window size of 15 bits depending on the - /// server's preference. - final int? clientMaxWindowBits; - - /// The maximal window size bit count requested by the server. - /// - /// The windows size for the compression is always a power of two, so the - /// number of bits precisely determines the window size. - /// - /// If set to `null`, the server has no preference, and the compression can - /// use up to its default maximum window size of 15 bits depending on the - /// client's preference. - final int? serverMaxWindowBits; - - /// Whether WebSocket compression is enabled. - /// - /// If not enabled, the remaining fields have no effect, and the - /// [compressionOff] instance can, and should, be reused instead of creating a - /// new instance with compression disabled. - final bool enabled; -} diff --git a/packages/spry/lib/src/websocket/websocket_hooks.dart b/packages/spry/lib/src/websocket/websocket_hooks.dart deleted file mode 100644 index b069054..0000000 --- a/packages/spry/lib/src/websocket/websocket_hooks.dart +++ /dev/null @@ -1,3 +0,0 @@ -abstract interface class WebSocketHooks {} - -// abstract interface class diff --git a/packages/spry/lib/websocket.dart b/packages/spry/lib/websocket.dart new file mode 100644 index 0000000..e41a11a --- /dev/null +++ b/packages/spry/lib/websocket.dart @@ -0,0 +1,136 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'constants.dart'; +import 'spry.dart'; + +extension type const ReadyState(int code) implements int { + static const unknown = ReadyState(-1); + static const connecting = ReadyState(0); + static const open = ReadyState(1); + static const closing = ReadyState(2); + static const closed = ReadyState(3); +} + +abstract interface class Peer implements Event { + ReadyState get readyState; + String? get protocol; + String get extensions; + + void send(Uint8List message); + void sendText(String message); + Future close([int? code, String? reason]); +} + +class Message { + const Message._(this.raw); + + factory Message.text(String text) => Message._(text); + factory Message.bytes(Uint8List bytes) => Message._(bytes); + + final dynamic raw; // Uint8List or String + + String text() { + return switch (raw) { + String value => value, + _ => utf8.decode(raw), + }; + } + + Uint8List bytes() { + return switch (raw) { + Uint8List bytes => bytes, + _ => utf8.encode(raw), + }; + } +} + +/// Options controlling compression in a [WebSocket]. +/// +/// A [CompressionOptions] instance can be passed to [WebSocket.connect], or +/// used in other similar places where [WebSocket] compression is configured. +/// +/// In most cases the default [compressionDefault] is sufficient, but in some +/// situations, it might be desirable to use different compression parameters, +/// for example to preserve memory on small devices. +class CompressionOptions { + const CompressionOptions( + {this.clientNoContextTakeover = false, + this.serverNoContextTakeover = false, + this.clientMaxWindowBits, + this.serverMaxWindowBits, + this.enabled = true}); + + /// Whether the client will reuse its compression instances. + final bool clientNoContextTakeover; + + /// Whether the server will reuse its compression instances. + final bool serverNoContextTakeover; + + /// The maximal window size bit count requested by the client. + /// + /// The windows size for the compression is always a power of two, so the + /// number of bits precisely determines the window size. + /// + /// If set to `null`, the client has no preference, and the compression can + /// use up to its default maximum window size of 15 bits depending on the + /// server's preference. + final int? clientMaxWindowBits; + + /// The maximal window size bit count requested by the server. + /// + /// The windows size for the compression is always a power of two, so the + /// number of bits precisely determines the window size. + /// + /// If set to `null`, the server has no preference, and the compression can + /// use up to its default maximum window size of 15 bits depending on the + /// client's preference. + final int? serverMaxWindowBits; + + /// Whether WebSocket compression is enabled. + /// + /// If not enabled, the remaining fields have no effect, and the + /// [compressionOff] instance can, and should, be reused instead of creating a + /// new instance with compression disabled. + final bool enabled; +} + +class CreatePeerOptions { + const CreatePeerOptions({ + required this.compression, + required this.headers, + this.protocols, + }); + + final CompressionOptions compression; + final Headers headers; + final Iterable? protocols; +} + +abstract interface class WebSocketPlatform { + FutureOr websocket(Event event, T request, Hooks hooks); +} + +abstract interface class Hooks { + FutureOr onUpgrade(Event event); + FutureOr onMessage(Peer peer, Message message); + FutureOr onClose(Peer peer, {int? code, String? reason}); + FutureOr onError(Peer peer, dynamic error); + FutureOr fallback(Event event); +} + +extension RoutesBuilderWS on RoutesBuilder { + void ws(String route, Hooks hooks) { + all(route, (event) async { + final platform = event.locals.getOrNull(kPlatform); + if (platform == null) { + return hooks.fallback(event); + } + + final request = event.locals.get(kRawRequest); + + return platform.websocket(event, request, hooks); + }); + } +} diff --git a/packages/spry/pubspec.yaml b/packages/spry/pubspec.yaml index ebb1d78..9e68444 100644 --- a/packages/spry/pubspec.yaml +++ b/packages/spry/pubspec.yaml @@ -7,7 +7,6 @@ environment: dependencies: routingkit: ^1.1.0 - web_socket: ^0.1.5 dev_dependencies: lints: ^4.0.0 From e4c6a262ace686fd43f24522badf53f100e892db Mon Sep 17 00:00:00 2001 From: Seven Du Date: Tue, 9 Jul 2024 15:49:16 +0800 Subject: [PATCH 21/35] chore: support get client address --- packages/spry/lib/io.dart | 9 +++++++++ packages/spry/lib/plain.dart | 5 +++++ packages/spry/lib/spry.dart | 1 + .../lib/src/event/event+get_client_address.dart | 14 ++++++++++++++ packages/spry/lib/src/platform/platform.dart | 1 + 5 files changed, 30 insertions(+) create mode 100644 packages/spry/lib/src/event/event+get_client_address.dart diff --git a/packages/spry/lib/io.dart b/packages/spry/lib/io.dart index 5eff297..69f5955 100644 --- a/packages/spry/lib/io.dart +++ b/packages/spry/lib/io.dart @@ -42,6 +42,15 @@ class IOPlatform return request.requestedUri; } + @override + String getClientAddress(Event event, HttpRequest request) { + if (request.connectionInfo != null) { + return '${request.connectionInfo?.remoteAddress.host}:${request.connectionInfo?.remotePort}'; + } + + return ''; + } + @override Future respond( Event event, HttpRequest request, Response response) async { diff --git a/packages/spry/lib/plain.dart b/packages/spry/lib/plain.dart index 86ebb3f..2016a19 100644 --- a/packages/spry/lib/plain.dart +++ b/packages/spry/lib/plain.dart @@ -57,6 +57,11 @@ class PlainPlatform implements Platform { String getRequestMethod(Event event, PlainRequest request) { return request.method; } + + @override + String getClientAddress(Event event, PlainRequest request) { + return ''; + } } extension SpryToPlainHandler on Spry { diff --git a/packages/spry/lib/spry.dart b/packages/spry/lib/spry.dart index 633256d..7cfa6b3 100644 --- a/packages/spry/lib/spry.dart +++ b/packages/spry/lib/spry.dart @@ -7,6 +7,7 @@ export 'src/types.dart'; export 'src/event/event.dart'; export 'src/event/event+app.dart'; +export 'src/event/event+get_client_address.dart'; export 'src/event/event+headers.dart'; export 'src/event/event+method.dart'; export 'src/event/event+params.dart'; diff --git a/packages/spry/lib/src/event/event+get_client_address.dart b/packages/spry/lib/src/event/event+get_client_address.dart new file mode 100644 index 0000000..a2185ad --- /dev/null +++ b/packages/spry/lib/src/event/event+get_client_address.dart @@ -0,0 +1,14 @@ +// ignore_for_file: file_names + +import '../../constants.dart'; +import '../platform/platform.dart'; +import 'event.dart'; + +extension EventGetClientAddress on Event { + String getClientAddress() { + final raw = locals.get(kRawRequest); + final platform = locals.get(kPlatform); + + return platform.getClientAddress(this, raw); + } +} diff --git a/packages/spry/lib/src/platform/platform.dart b/packages/spry/lib/src/platform/platform.dart index 11274bc..844234f 100644 --- a/packages/spry/lib/src/platform/platform.dart +++ b/packages/spry/lib/src/platform/platform.dart @@ -5,6 +5,7 @@ import '../http/headers/headers.dart'; import '../http/response.dart'; abstract interface class Platform { + String getClientAddress(Event event, T request); String getRequestMethod(Event event, T request); Uri getRequestURI(Event event, T request); Headers getRequestHeaders(Event event, T request); From 68d8218dd79b581d86c5e3a5359ac57a430d5cad Mon Sep 17 00:00:00 2001 From: Seven Du Date: Tue, 9 Jul 2024 15:52:41 +0800 Subject: [PATCH 22/35] chore: WIP --- CHANGELOG.md | 3 +++ examples/README.md | 1 + packages/spry/CHANGELOG.md | 4 +--- packages/spry/LICENSE | 1 + packages/spry/README.md | 1 + packages/spry/example/README.md | 1 + packages/spry/example/main.dart | 17 ----------------- 7 files changed, 8 insertions(+), 20 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 examples/README.md mode change 100644 => 120000 packages/spry/CHANGELOG.md create mode 120000 packages/spry/LICENSE create mode 120000 packages/spry/README.md create mode 120000 packages/spry/example/README.md delete mode 100644 packages/spry/example/main.dart diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..effe43c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..86e1eef --- /dev/null +++ b/examples/README.md @@ -0,0 +1 @@ +# Spry Examples diff --git a/packages/spry/CHANGELOG.md b/packages/spry/CHANGELOG.md deleted file mode 100644 index effe43c..0000000 --- a/packages/spry/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -## 1.0.0 - -- Initial version. diff --git a/packages/spry/CHANGELOG.md b/packages/spry/CHANGELOG.md new file mode 120000 index 0000000..699cc9e --- /dev/null +++ b/packages/spry/CHANGELOG.md @@ -0,0 +1 @@ +../../CHANGELOG.md \ No newline at end of file diff --git a/packages/spry/LICENSE b/packages/spry/LICENSE new file mode 120000 index 0000000..30cff74 --- /dev/null +++ b/packages/spry/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/packages/spry/README.md b/packages/spry/README.md new file mode 120000 index 0000000..fe84005 --- /dev/null +++ b/packages/spry/README.md @@ -0,0 +1 @@ +../../README.md \ No newline at end of file diff --git a/packages/spry/example/README.md b/packages/spry/example/README.md new file mode 120000 index 0000000..8712a94 --- /dev/null +++ b/packages/spry/example/README.md @@ -0,0 +1 @@ +../../../examples/README.md \ No newline at end of file diff --git a/packages/spry/example/main.dart b/packages/spry/example/main.dart deleted file mode 100644 index 3d06644..0000000 --- a/packages/spry/example/main.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:spry/plain.dart'; -import 'package:spry/spry.dart'; - -void main() async { - // Creates a Spry application. - final app = Spry(); - - // Adds a `GET /hello` route, Response body with 'Hello Spry!' - app.get('hello', (event) => 'Hello Spry!'); - - // Creates a plain platfrom handler. - final handler = app.toPlainHandler(); - final request = PlainRequest(method: 'get', uri: Uri(path: 'hello')); - final response = await handler(request); - - print(await response.text()); // Hello Spry! -} From 2fa987fbe0146856375b2362348938000e2d2ef1 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Tue, 9 Jul 2024 15:57:34 +0800 Subject: [PATCH 23/35] chore: WIP --- packages/spry_cookie/.gitignore | 7 ++++ packages/spry_cookie/CHANGELOG.md | 1 + packages/spry_cookie/LICENSE | 1 + packages/spry_cookie/README.md | 39 +++++++++++++++++++ packages/spry_cookie/analysis_options.yaml | 30 ++++++++++++++ packages/spry_cookie/example/README.md | 1 + packages/spry_cookie/lib/spry_cookie.dart | 1 + .../spry_cookie/lib/src/spry_cookie_base.dart | 6 +++ packages/spry_cookie/pubspec.yaml | 16 ++++++++ 9 files changed, 102 insertions(+) create mode 100644 packages/spry_cookie/.gitignore create mode 120000 packages/spry_cookie/CHANGELOG.md create mode 120000 packages/spry_cookie/LICENSE create mode 100644 packages/spry_cookie/README.md create mode 100644 packages/spry_cookie/analysis_options.yaml create mode 120000 packages/spry_cookie/example/README.md create mode 100644 packages/spry_cookie/lib/spry_cookie.dart create mode 100644 packages/spry_cookie/lib/src/spry_cookie_base.dart create mode 100644 packages/spry_cookie/pubspec.yaml diff --git a/packages/spry_cookie/.gitignore b/packages/spry_cookie/.gitignore new file mode 100644 index 0000000..3cceda5 --- /dev/null +++ b/packages/spry_cookie/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/packages/spry_cookie/CHANGELOG.md b/packages/spry_cookie/CHANGELOG.md new file mode 120000 index 0000000..699cc9e --- /dev/null +++ b/packages/spry_cookie/CHANGELOG.md @@ -0,0 +1 @@ +../../CHANGELOG.md \ No newline at end of file diff --git a/packages/spry_cookie/LICENSE b/packages/spry_cookie/LICENSE new file mode 120000 index 0000000..30cff74 --- /dev/null +++ b/packages/spry_cookie/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/packages/spry_cookie/README.md b/packages/spry_cookie/README.md new file mode 100644 index 0000000..8b55e73 --- /dev/null +++ b/packages/spry_cookie/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/packages/spry_cookie/analysis_options.yaml b/packages/spry_cookie/analysis_options.yaml new file mode 100644 index 0000000..dee8927 --- /dev/null +++ b/packages/spry_cookie/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/packages/spry_cookie/example/README.md b/packages/spry_cookie/example/README.md new file mode 120000 index 0000000..8712a94 --- /dev/null +++ b/packages/spry_cookie/example/README.md @@ -0,0 +1 @@ +../../../examples/README.md \ No newline at end of file diff --git a/packages/spry_cookie/lib/spry_cookie.dart b/packages/spry_cookie/lib/spry_cookie.dart new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/packages/spry_cookie/lib/spry_cookie.dart @@ -0,0 +1 @@ + diff --git a/packages/spry_cookie/lib/src/spry_cookie_base.dart b/packages/spry_cookie/lib/src/spry_cookie_base.dart new file mode 100644 index 0000000..e8a6f15 --- /dev/null +++ b/packages/spry_cookie/lib/src/spry_cookie_base.dart @@ -0,0 +1,6 @@ +// TODO: Put public facing types in this file. + +/// Checks if you are awesome. Spoiler: you are. +class Awesome { + bool get isAwesome => true; +} diff --git a/packages/spry_cookie/pubspec.yaml b/packages/spry_cookie/pubspec.yaml new file mode 100644 index 0000000..8047721 --- /dev/null +++ b/packages/spry_cookie/pubspec.yaml @@ -0,0 +1,16 @@ +name: spry_cookie +description: A starting point for Dart libraries or applications. +version: 1.0.0 +# repository: https://github.com/my_org/my_repo + +environment: + sdk: ^3.4.3 + +dependencies: + http_parser: ^4.1.0 + spry: + path: ../spry + +dev_dependencies: + lints: ^3.0.0 + test: ^1.24.0 From 333a72dfbc5f61b9975331dc882baec7039d09a7 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Tue, 9 Jul 2024 18:14:42 +0800 Subject: [PATCH 24/35] chore: WIP --- README.md | 16 +--- packages/spry/lib/constants.dart | 14 ++++ packages/spry/lib/exception_filter.dart | 59 ++++++++++++++- packages/spry/lib/io.dart | 6 +- packages/spry/lib/plain.dart | 9 ++- packages/spry/lib/spry.dart | 1 + packages/spry/lib/src/event/event+app.dart | 1 + .../src/event/event+get_client_address.dart | 6 ++ .../spry/lib/src/event/event+headers.dart | 1 + packages/spry/lib/src/event/event+method.dart | 3 +- packages/spry/lib/src/event/event+params.dart | 1 + packages/spry/lib/src/event/event+route.dart | 3 + packages/spry/lib/src/event/event+uri.dart | 14 ++++ packages/spry/lib/src/event/event.dart | 4 + .../spry/lib/src/handler/closure_handler.dart | 8 +- packages/spry/lib/src/handler/handler.dart | 2 + .../lib/src/http/headers/headers+get.dart | 4 + .../lib/src/http/headers/headers+has.dart | 1 + .../lib/src/http/headers/headers+keys.dart | 1 + .../lib/src/http/headers/headers+rebuild.dart | 1 + .../src/http/headers/headers+to_builder.dart | 1 + .../spry/lib/src/http/headers/headers.dart | 4 + .../src/http/headers/headers_builder+set.dart | 1 + .../lib/src/http/headers/headers_builder.dart | 9 +++ .../http/http_message/http_message+json.dart | 1 + .../http/http_message/http_message+text.dart | 1 + .../src/http/http_message/http_message.dart | 6 ++ packages/spry/lib/src/http/request.dart | 17 +++++ .../spry/lib/src/http/response+copy_with.dart | 1 + packages/spry/lib/src/http/response.dart | 7 ++ .../lib/src/locals/locals+get_or_null.dart | 1 + .../lib/src/locals/locals+get_or_set.dart | 3 + packages/spry/lib/src/locals/locals.dart | 6 ++ .../src/platform/platform+create_handler.dart | 1 + packages/spry/lib/src/platform/platform.dart | 13 ++++ .../lib/src/platform/platform_handler.dart | 1 + .../spry/lib/src/responsible/responsible.dart | 7 ++ packages/spry/lib/src/routing/route.dart | 2 + .../lib/src/routing/routes_builder+all.dart | 2 + .../src/routing/routes_builder+methods.dart | 14 ++++ .../lib/src/routing/routes_builder+on.dart | 1 + .../spry/lib/src/routing/routes_builder.dart | 2 + packages/spry/lib/src/spry+add_handler.dart | 1 + packages/spry/lib/src/spry+fallback.dart | 2 + packages/spry/lib/src/spry+use.dart | 1 + packages/spry/lib/src/spry.dart | 5 ++ packages/spry/lib/src/utils/next.dart | 21 ++++++ packages/spry/lib/websocket.dart | 75 ++++++++++++++++++- packages/spry/pubspec.yaml | 4 +- 49 files changed, 341 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index b0a05fb..2c5d11e 100644 --- a/README.md +++ b/README.md @@ -6,29 +6,23 @@ [![X (twitter)](https://img.shields.io/badge/twitter-%40shiweidu-blue.svg)](https://twitter.com/shiweidu) [![Documentation](https://img.shields.io/badge/docs-spry.fun-brightgreen.svg)](https://spry.fun/) -Spry is an HTTP middleware framework for Dart to make web applications and APIs server more enjoyable to write. +Spry is a lightweight, composable Dart web framework designed to work collaboratively with various runtime platforms. ```dart import 'package:spry/spry.dart'; main() async { - final app = Application.late(); + final app = Spry(); app.get("hello", (request) => "Hello, Spry!"); - - await app.run(port: 3000); } ``` 👉 [**Learn more about Spry at Spry Documentation.**](https://spry.fun/) -## Philosophy - -Spry is a framework for building web applications and APIs. It's designed around dart:io, no boring creations, just lots of magic. - ## Sponsors -Spry framework is an [MIT licensed](https://github.com/medz/spry/blob/main/LICENSE) open source project with its ongoing development made possible entirely by the support of these awesome backers. If you'd like to join them, please consider [sponsoring Seven(@medz)](https://github.com/sponsors/odroe) or [sponsor us on OpenCollective](https://opencollective.com/openodroe) development. +Spry framework is an [MIT licensed](https://github.com/medz/spry/blob/main/LICENSE) open source project with its ongoing development made possible entirely by the support of these awesome backers. If you'd like to join them, please consider [sponsoring Seven(@medz)](https://github.com/sponsors/odroe) development.

@@ -43,7 +37,3 @@ We welcome contributions! Please read our [contributing guide](CONTRIBUTING.md) Thank you to all the people who already contributed to Odroe! [![Contributors](https://opencollective.com/openodroe/contributors.svg?width=890)](https://github.com/odroe/prisma-dart/graphs/contributors) - -## Code of Conduct - -This project has adopted the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). For more information see the [Code of Conduct FAQ](https://www.contributor-covenant.org/faq) or contact [hello@odroe.com](mailto:hello@odroe.com) with any additional questions or comments. diff --git a/packages/spry/lib/constants.dart b/packages/spry/lib/constants.dart index f403fe6..8b82441 100644 --- a/packages/spry/lib/constants.dart +++ b/packages/spry/lib/constants.dart @@ -1,5 +1,19 @@ +/// Spry constants +/// +/// This library exports [Spry] internal content keys stored in [Locals]. +library spry.constants; + +/// Store the key of the [Spry] application instance in [Locals]. const kAppInstance = #spry.app; + +/// The key stored in [Locals] for instances implemented on the [Platform]. const kPlatform = #spry.platform; + +/// This constant is the key stored in [Locals] for the RAW request const kRawRequest = #spry.event.request.raw; + +/// The key stored in [Locals] for routing [Params]. const kEventParams = #spry.event.params; + +/// This is the key stored in [Locals] for storing matching route. const kEventRoute = #spry.event.route; diff --git a/packages/spry/lib/exception_filter.dart b/packages/spry/lib/exception_filter.dart index 794fa0b..7d2f4dd 100644 --- a/packages/spry/lib/exception_filter.dart +++ b/packages/spry/lib/exception_filter.dart @@ -1,27 +1,73 @@ +/// Spry exception filter library. +library spry.exception_filter; + import 'dart:async'; import 'spry.dart'; +/// Exception source, used for packaging [Exception] and [Error], as well as [StackTrace]. class ExceptionSource { const ExceptionSource(this.exception, this.stackTrace); + /// Returns the [Exception] or [Error]. final T exception; + + /// Returns current error [StackTrace]. final StackTrace stackTrace; } +/// Exception filter interface. +/// +/// The use of interfaces is to create filters in a more standardized manner. +/// +/// ## Example +/// ```dart +/// class MyException {} +/// +/// class MyExceptionFilter implements ExceptionFilter { +/// Future process(Event event, ExceptionSource source) { +/// return Response.text('My exception'); +/// } +/// } +/// +/// app.use(withExceptionFilter(MyExceptionFilter())); +/// ``` abstract interface class ExceptionFilter { - Future process(Event event, ExceptionSource source); + /// Handling exceptions. + /// + /// If you don't want to handle the current exception, you can directly + /// return null, which can transfer the exception to the upper level registered + /// filter for processing. + Future process(Event event, ExceptionSource source); } +/// Define exception filter. +/// +/// Many times, using [ExceptionFilter] to create filters can be cumbersome, +/// especially when we have a simple exception that requires friendly handling. +/// Using [defineExceptionFilter] is an easier option. +/// +/// ```dart +/// app.use(defineExceptionFilter((event, source) { +/// return Response.json(status: 412, { +/// "message": Error.safeToString(source.exception), +/// "code": -1, +/// }); +/// })); +/// ``` Future Function(Event event) defineExceptionFilter( - FutureOr Function(Event event, ExceptionSource source) + FutureOr Function(Event event, ExceptionSource source) process) { return (Event event) async { try { return await next(event); } catch (exception, stackTrace) { if (exception is T) { - return process(event, ExceptionSource(exception as T, stackTrace)); + final response = + await process(event, ExceptionSource(exception as T, stackTrace)); + if (response != null) { + return response; + } } else if (exception is Response) { return exception; } @@ -31,6 +77,13 @@ Future Function(Event event) defineExceptionFilter( }; } +/// Convert [ExceptionFilter] to handle. +/// +/// [withExceptionFilter] can register filters to Spry with semantic meaning. +/// +/// ```dart +/// app.use(withExceptionFilter(...)); +/// ``` Future Function(Event event) withExceptionFilter( ExceptionFilter filter) { return defineExceptionFilter(filter.process); diff --git a/packages/spry/lib/io.dart b/packages/spry/lib/io.dart index 69f5955..74dfe29 100644 --- a/packages/spry/lib/io.dart +++ b/packages/spry/lib/io.dart @@ -1,3 +1,4 @@ +/// This library implements support for the 'dart: io' platform. library spry.platform.io; import 'dart:async'; @@ -9,6 +10,7 @@ import 'websocket.dart' hide CompressionOptions; const _kUpgradedWebSocket = #spry.io.upgraded.websocket; +/// `dart:io` platform. class IOPlatform implements Platform, @@ -136,8 +138,10 @@ class IOPlatform } } +/// Add the [toIOHandler] auxiliary method to [Spry]. extension SpryToIOHandler on Spry { - Future Function(HttpRequest) toIOHandler() { + /// Returns the [Spry] application to an [HttpServer] compatible processor in 'dart:io'. + PlatformHandler toIOHandler() { return const IOPlatform().createHandler(this); } } diff --git a/packages/spry/lib/plain.dart b/packages/spry/lib/plain.dart index 2016a19..414df5a 100644 --- a/packages/spry/lib/plain.dart +++ b/packages/spry/lib/plain.dart @@ -1,3 +1,4 @@ +/// This library implements the implementation of Spring plain requests. library spry.platform.plain; import 'dart:convert'; @@ -5,6 +6,7 @@ import 'dart:typed_data'; import 'spry.dart'; +/// Plain Request. class PlainRequest implements Request { PlainRequest({ required this.method, @@ -29,6 +31,9 @@ class PlainRequest implements Request { final String method; } +/// Plain Platform. +/// +/// **NOTE**: The plain platform does not support websocket. class PlainPlatform implements Platform { const PlainPlatform(); @@ -64,8 +69,10 @@ class PlainPlatform implements Platform { } } +/// Add the [toPlainHandler] helper method to the [Spry] application. extension SpryToPlainHandler on Spry { - Future Function(PlainRequest) toPlainHandler() { + /// **NOTE**: The plain platform does not support websocket. + PlatformHandler toPlainHandler() { return const PlainPlatform().createHandler(this); } } diff --git a/packages/spry/lib/spry.dart b/packages/spry/lib/spry.dart index 7cfa6b3..7bba646 100644 --- a/packages/spry/lib/spry.dart +++ b/packages/spry/lib/spry.dart @@ -1,3 +1,4 @@ +/// Spry library. library spry; export 'src/spry.dart'; diff --git a/packages/spry/lib/src/event/event+app.dart b/packages/spry/lib/src/event/event+app.dart index 4fea579..87fa8ff 100644 --- a/packages/spry/lib/src/event/event+app.dart +++ b/packages/spry/lib/src/event/event+app.dart @@ -5,5 +5,6 @@ import '../spry.dart'; import 'event.dart'; extension EventApp on Event { + /// Returns the [Spry] instance for the request event. Spry get app => locals.get(kAppInstance); } diff --git a/packages/spry/lib/src/event/event+get_client_address.dart b/packages/spry/lib/src/event/event+get_client_address.dart index a2185ad..7d5fe42 100644 --- a/packages/spry/lib/src/event/event+get_client_address.dart +++ b/packages/spry/lib/src/event/event+get_client_address.dart @@ -5,6 +5,12 @@ import '../platform/platform.dart'; import 'event.dart'; extension EventGetClientAddress on Event { + /// Returns client address. + /// + /// Value formated of `:port`. + /// + /// The returned value comes from the Platform implementation. + /// If the platform does not support it, an empty string will be returned. String getClientAddress() { final raw = locals.get(kRawRequest); final platform = locals.get(kPlatform); diff --git a/packages/spry/lib/src/event/event+headers.dart b/packages/spry/lib/src/event/event+headers.dart index 6a6b4dc..291cd0a 100644 --- a/packages/spry/lib/src/event/event+headers.dart +++ b/packages/spry/lib/src/event/event+headers.dart @@ -4,5 +4,6 @@ import '../http/headers/headers.dart'; import 'event.dart'; extension EventHeaders on Event { + /// Access to the normalized request [Headers]. Headers get headers => request.headers; } diff --git a/packages/spry/lib/src/event/event+method.dart b/packages/spry/lib/src/event/event+method.dart index cb0d27f..e3f0d37 100644 --- a/packages/spry/lib/src/event/event+method.dart +++ b/packages/spry/lib/src/event/event+method.dart @@ -3,5 +3,6 @@ import 'event.dart'; extension EventMethod on Event { - String get method => request.method; + /// Access to the normalized (uppercase) request method. + String get method => request.method.toUpperCase().trim(); } diff --git a/packages/spry/lib/src/event/event+params.dart b/packages/spry/lib/src/event/event+params.dart index 6ec9635..37c13b6 100644 --- a/packages/spry/lib/src/event/event+params.dart +++ b/packages/spry/lib/src/event/event+params.dart @@ -6,6 +6,7 @@ import '../types.dart'; import 'event.dart'; extension EventParams on Event { + /// Returns the [Params] of dynamic routing. Params get params { return locals.getOrSet(kEventParams, Params.new); } diff --git a/packages/spry/lib/src/event/event+route.dart b/packages/spry/lib/src/event/event+route.dart index 83c1d5f..1036d52 100644 --- a/packages/spry/lib/src/event/event+route.dart +++ b/packages/spry/lib/src/event/event+route.dart @@ -6,5 +6,8 @@ import '../routing/route.dart'; import 'event.dart'; extension EventRoute on Event { + /// Return [Route], when the route has not yet started matching + /// or has not been matched to return null, usually this situation + /// is when the route is registered and has entered the fallback processor. Route? get route => locals.getOrNull(kEventRoute); } diff --git a/packages/spry/lib/src/event/event+uri.dart b/packages/spry/lib/src/event/event+uri.dart index 19c4498..d300d97 100644 --- a/packages/spry/lib/src/event/event+uri.dart +++ b/packages/spry/lib/src/event/event+uri.dart @@ -3,5 +3,19 @@ import 'event.dart'; extension EventURI on Event { + /// The requested URI for the request event. + /// + /// If the request URI is absolute (e.g. 'https://www.example.com/foo') then + /// it is returned as-is. Otherwise, the returned URI is reconstructed by + /// using the request URI path (e.g. '/foo') and HTTP header fields. + /// + /// To reconstruct the scheme, the 'X-Forwarded-Proto' header is used. If it + /// is not present then the socket type of the connection is used i.e. if + /// the connection is made through a [SecureSocket] then the scheme is + /// 'https', otherwise it is 'http'. + /// + /// To reconstruct the host, the 'X-Forwarded-Host' header is used. If it is + /// not present then the 'Host' header is used. If neither is present then + /// the host name of the server is used. Uri get uri => request.uri; } diff --git a/packages/spry/lib/src/event/event.dart b/packages/spry/lib/src/event/event.dart index 57f15e8..df5cfec 100644 --- a/packages/spry/lib/src/event/event.dart +++ b/packages/spry/lib/src/event/event.dart @@ -1,7 +1,11 @@ import '../http/request.dart'; import '../locals/locals.dart'; +/// Request event. abstract interface class Event { + /// Returns current request event locals. Locals get locals; + + /// Return the request object for the request event. Request get request; } diff --git a/packages/spry/lib/src/handler/closure_handler.dart b/packages/spry/lib/src/handler/closure_handler.dart index 0361b9f..ccb6b8e 100644 --- a/packages/spry/lib/src/handler/closure_handler.dart +++ b/packages/spry/lib/src/handler/closure_handler.dart @@ -6,14 +6,16 @@ import '../responsible/responsible.dart'; import '../utils/next.dart'; import 'handler.dart'; +/// Implement a [Handler] that supports [Responsible] return values. final class ClosureHandler implements Handler { - const ClosureHandler(this.closure); + const ClosureHandler(FutureOr Function(Event) closure) + : _closure = closure; - final FutureOr Function(Event) closure; + final FutureOr Function(Event) _closure; @override Future handle(Event event) async { - return switch (await closure(event)) { + return switch (await _closure(event)) { null => next(event), Response response => response, Responsible responsible => responsible.createResponse(event), diff --git a/packages/spry/lib/src/handler/handler.dart b/packages/spry/lib/src/handler/handler.dart index d8be5af..2c28707 100644 --- a/packages/spry/lib/src/handler/handler.dart +++ b/packages/spry/lib/src/handler/handler.dart @@ -1,6 +1,8 @@ import '../event/event.dart'; import '../http/response.dart'; +/// Spry application handler interface. abstract interface class Handler { + /// Handle a request [Event] and returns the [Response]. Future handle(Event event); } diff --git a/packages/spry/lib/src/http/headers/headers+get.dart b/packages/spry/lib/src/http/headers/headers+get.dart index 6ad73a1..922740e 100644 --- a/packages/spry/lib/src/http/headers/headers+get.dart +++ b/packages/spry/lib/src/http/headers/headers+get.dart @@ -3,11 +3,15 @@ import 'headers.dart'; extension HeadersGet on Headers { + /// Returns all header values for [name]. Iterable getAll(String name) { final normalizedName = name.toLowerCase(); return where((e) => e.$1.toLowerCase() == normalizedName).map((e) => e.$2); } + /// Returns value for [name]. + /// + /// If the header is multi valued, use `, ` for connection. String? get(String name) { return switch (getAll(name)) { Iterable(isNotEmpty: true, join: final join) => join(', '), diff --git a/packages/spry/lib/src/http/headers/headers+has.dart b/packages/spry/lib/src/http/headers/headers+has.dart index d5d23aa..e609ae7 100644 --- a/packages/spry/lib/src/http/headers/headers+has.dart +++ b/packages/spry/lib/src/http/headers/headers+has.dart @@ -3,6 +3,7 @@ import 'headers.dart'; extension HeadersHas on Headers { + /// Check if a header exists. bool has(String name) { final normalizedName = name.toLowerCase(); return any((e) => e.$1.toLowerCase() == normalizedName); diff --git a/packages/spry/lib/src/http/headers/headers+keys.dart b/packages/spry/lib/src/http/headers/headers+keys.dart index 96bcdc8..9abe83b 100644 --- a/packages/spry/lib/src/http/headers/headers+keys.dart +++ b/packages/spry/lib/src/http/headers/headers+keys.dart @@ -3,6 +3,7 @@ import 'headers.dart'; extension HeadersKeys on Headers { + /// Returns all header keys. Iterable get keys { return map((e) => e.$1).toSet(); } diff --git a/packages/spry/lib/src/http/headers/headers+rebuild.dart b/packages/spry/lib/src/http/headers/headers+rebuild.dart index 63364a9..6e339fc 100644 --- a/packages/spry/lib/src/http/headers/headers+rebuild.dart +++ b/packages/spry/lib/src/http/headers/headers+rebuild.dart @@ -5,6 +5,7 @@ import 'headers_builder.dart'; import 'headers+to_builder.dart'; extension HeadersRebuild on Headers { + /// Rebuilding the [Headers] object Headers rebuild(void Function(HeadersBuilder builder) updates) { final builder = toBuilder(); updates(builder); diff --git a/packages/spry/lib/src/http/headers/headers+to_builder.dart b/packages/spry/lib/src/http/headers/headers+to_builder.dart index c31c00b..6d285b8 100644 --- a/packages/spry/lib/src/http/headers/headers+to_builder.dart +++ b/packages/spry/lib/src/http/headers/headers+to_builder.dart @@ -4,5 +4,6 @@ import 'headers.dart'; import 'headers_builder.dart'; extension HeadersToBuilder on Headers { + /// Creates a [HeadersBuilder] using the current [Headers]. HeadersBuilder toBuilder() => HeadersBuilder(this); } diff --git a/packages/spry/lib/src/http/headers/headers.dart b/packages/spry/lib/src/http/headers/headers.dart index 0d21b15..d84c99d 100644 --- a/packages/spry/lib/src/http/headers/headers.dart +++ b/packages/spry/lib/src/http/headers/headers.dart @@ -1,4 +1,8 @@ +/// HTTP Headers. abstract interface class Headers implements Iterable<(String, String)> { + /// Creates a new [Headers]. + /// + /// The [init] is default headers of key-value parts. const factory Headers([Iterable<(String, String)> init]) = _HeadersImpl; } diff --git a/packages/spry/lib/src/http/headers/headers_builder+set.dart b/packages/spry/lib/src/http/headers/headers_builder+set.dart index d351d21..75bba98 100644 --- a/packages/spry/lib/src/http/headers/headers_builder+set.dart +++ b/packages/spry/lib/src/http/headers/headers_builder+set.dart @@ -3,6 +3,7 @@ import 'headers_builder.dart'; extension HeadersBuilderSet on HeadersBuilder { + /// Set a header that will be overwritten if [name] exists. void set(String name, String value) { remove(name); add(name, value); diff --git a/packages/spry/lib/src/http/headers/headers_builder.dart b/packages/spry/lib/src/http/headers/headers_builder.dart index 0e7aa3e..3ca983d 100644 --- a/packages/spry/lib/src/http/headers/headers_builder.dart +++ b/packages/spry/lib/src/http/headers/headers_builder.dart @@ -1,6 +1,8 @@ import 'headers.dart'; +/// [Headers] builder. abstract interface class HeadersBuilder { + /// Creates a new [HeadersBuilder]. factory HeadersBuilder([Iterable<(String, String)>? init]) { final builder = _HeadersBuilderImpl(); @@ -13,9 +15,16 @@ abstract interface class HeadersBuilder { return builder; } + /// Remove a header for [name]. void remove(String name); + + /// Remove headers for where test. void removeWhere(bool Function(String name, String value) test); + + /// Adds a new header. void add(String name, String value); + + /// Building [Headers]. Headers toHeaders(); } diff --git a/packages/spry/lib/src/http/http_message/http_message+json.dart b/packages/spry/lib/src/http/http_message/http_message+json.dart index a4e89f1..0fec3a0 100644 --- a/packages/spry/lib/src/http/http_message/http_message+json.dart +++ b/packages/spry/lib/src/http/http_message/http_message+json.dart @@ -6,6 +6,7 @@ import 'http_message.dart'; import 'http_message+text.dart'; extension HttpMessageJson on HttpMessage { + /// Returns the body as JSON. Future json() async { return switch (await text()) { String text => jsonDecode(text), diff --git a/packages/spry/lib/src/http/http_message/http_message+text.dart b/packages/spry/lib/src/http/http_message/http_message+text.dart index ad68154..9b6b4da 100644 --- a/packages/spry/lib/src/http/http_message/http_message+text.dart +++ b/packages/spry/lib/src/http/http_message/http_message+text.dart @@ -5,6 +5,7 @@ import 'dart:typed_data'; import 'http_message.dart'; extension HttpMessageText on HttpMessage { + /// Returns the Request/Response body as [String]. Future text() async { return switch (body) { Stream stream => encoding.decodeStream(stream), diff --git a/packages/spry/lib/src/http/http_message/http_message.dart b/packages/spry/lib/src/http/http_message/http_message.dart index 77c645b..8a21ee5 100644 --- a/packages/spry/lib/src/http/http_message/http_message.dart +++ b/packages/spry/lib/src/http/http_message/http_message.dart @@ -3,8 +3,14 @@ import 'dart:typed_data'; import '../headers/headers.dart'; +/// HTTP message universal interface. abstract interface class HttpMessage { + /// Body encoding Encoding get encoding; + + /// Request/Response headers. Headers get headers; + + /// Request/Response body. Stream? get body; } diff --git a/packages/spry/lib/src/http/request.dart b/packages/spry/lib/src/http/request.dart index 9a64ce4..2fb34d7 100644 --- a/packages/spry/lib/src/http/request.dart +++ b/packages/spry/lib/src/http/request.dart @@ -1,6 +1,23 @@ import 'http_message/http_message.dart'; +/// Spry request interface. abstract interface class Request implements HttpMessage { + /// The request method of the client. String get method; + + /// The requested URI for the request event. + /// + /// If the request URI is absolute (e.g. 'https://www.example.com/foo') then + /// it is returned as-is. Otherwise, the returned URI is reconstructed by + /// using the request URI path (e.g. '/foo') and HTTP header fields. + /// + /// To reconstruct the scheme, the 'X-Forwarded-Proto' header is used. If it + /// is not present then the socket type of the connection is used i.e. if + /// the connection is made through a [SecureSocket] then the scheme is + /// 'https', otherwise it is 'http'. + /// + /// To reconstruct the host, the 'X-Forwarded-Host' header is used. If it is + /// not present then the 'Host' header is used. If neither is present then + /// the host name of the server is used. Uri get uri; } diff --git a/packages/spry/lib/src/http/response+copy_with.dart b/packages/spry/lib/src/http/response+copy_with.dart index 85f7bbb..4b45c0a 100644 --- a/packages/spry/lib/src/http/response+copy_with.dart +++ b/packages/spry/lib/src/http/response+copy_with.dart @@ -4,6 +4,7 @@ import 'headers/headers.dart'; import 'response.dart'; extension ResponseCopyWith on Response { + /// Copy a [Response] Response copyWith({ int? status, String? statusText, diff --git a/packages/spry/lib/src/http/response.dart b/packages/spry/lib/src/http/response.dart index c48a016..ef7106a 100644 --- a/packages/spry/lib/src/http/response.dart +++ b/packages/spry/lib/src/http/response.dart @@ -7,7 +7,9 @@ import 'headers/headers_builder+set.dart'; import 'http_message/http_message.dart'; import 'http_status_reason_phrase.dart'; +/// Spry response interface. abstract interface class Response implements HttpMessage { + /// Creates a new [Response]. const factory Response( final Stream? body, { final int status, @@ -16,6 +18,7 @@ abstract interface class Response implements HttpMessage { final Encoding encoding, }) = _ResponseImpl; + /// Creates a new [Response] from text. factory Response.text( final String body, { final int status = 200, @@ -33,6 +36,7 @@ abstract interface class Response implements HttpMessage { ); } + /// Creates a new [Response] from JSON. factory Response.json( final body, { final int status = 200, @@ -53,7 +57,10 @@ abstract interface class Response implements HttpMessage { ); } + /// Response status. int get status; + + /// Response status code reason phrases. String get statusText; } diff --git a/packages/spry/lib/src/locals/locals+get_or_null.dart b/packages/spry/lib/src/locals/locals+get_or_null.dart index 1f37c01..d96d1ed 100644 --- a/packages/spry/lib/src/locals/locals+get_or_null.dart +++ b/packages/spry/lib/src/locals/locals+get_or_null.dart @@ -3,6 +3,7 @@ import 'locals.dart'; extension LocalsGetOrNull on Locals { + /// Gets a type of [T] value for [key], If value type is not [T] returns null. T? getOrNull(Object key) { try { return get(key); diff --git a/packages/spry/lib/src/locals/locals+get_or_set.dart b/packages/spry/lib/src/locals/locals+get_or_set.dart index aa3250e..3371457 100644 --- a/packages/spry/lib/src/locals/locals+get_or_set.dart +++ b/packages/spry/lib/src/locals/locals+get_or_set.dart @@ -3,6 +3,9 @@ import 'locals.dart'; extension LocalsGetOrSet on Locals { + /// Gets or set a type of [T] value from [key]. + /// + /// If the value type is not [T], using [creates] value set it. T getOrSet(Object key, T Function() creates) { try { return get(key); diff --git a/packages/spry/lib/src/locals/locals.dart b/packages/spry/lib/src/locals/locals.dart index 443c92e..09063fb 100644 --- a/packages/spry/lib/src/locals/locals.dart +++ b/packages/spry/lib/src/locals/locals.dart @@ -1,5 +1,11 @@ +/// [Spry]/[Event] locals. abstract interface class Locals { + /// Gets a type of [T] value for [key]. T get(Object key); + + /// Sets a type of [T] value. void set(Object key, T value); + + /// Remove a value for [key]. void remove(Object key); } diff --git a/packages/spry/lib/src/platform/platform+create_handler.dart b/packages/spry/lib/src/platform/platform+create_handler.dart index 28c84d2..0582473 100644 --- a/packages/spry/lib/src/platform/platform+create_handler.dart +++ b/packages/spry/lib/src/platform/platform+create_handler.dart @@ -21,6 +21,7 @@ import 'platform.dart'; import 'platform_handler.dart'; extension PlatformAdapterCreateHandler on Platform { + /// Creates a platform handler. PlatformHandler createHandler(Spry app) { final handleWith = app.createHandleWith(); diff --git a/packages/spry/lib/src/platform/platform.dart b/packages/spry/lib/src/platform/platform.dart index 844234f..2553e74 100644 --- a/packages/spry/lib/src/platform/platform.dart +++ b/packages/spry/lib/src/platform/platform.dart @@ -4,12 +4,25 @@ import '../event/event.dart'; import '../http/headers/headers.dart'; import '../http/response.dart'; +/// Spry platform interface. abstract interface class Platform { + /// Gets a client address. + /// + /// If platform not support, returns a empty string. String getClientAddress(Event event, T request); + + /// Gets a request method. String getRequestMethod(Event event, T request); + + /// Gets a request [Uri]. Uri getRequestURI(Event event, T request); + + /// Gets a request [Headers]. Headers getRequestHeaders(Event event, T request); + + /// Gets a request body stream. Stream? getRequestBody(Event event, T request); + /// Respond to a response. Future respond(Event event, T request, Response response); } diff --git a/packages/spry/lib/src/platform/platform_handler.dart b/packages/spry/lib/src/platform/platform_handler.dart index 3bb7320..ff11d6c 100644 --- a/packages/spry/lib/src/platform/platform_handler.dart +++ b/packages/spry/lib/src/platform/platform_handler.dart @@ -1 +1,2 @@ +/// Platform handler. typedef PlatformHandler = Future Function(T request); diff --git a/packages/spry/lib/src/responsible/responsible.dart b/packages/spry/lib/src/responsible/responsible.dart index 975dc06..3c3058d 100644 --- a/packages/spry/lib/src/responsible/responsible.dart +++ b/packages/spry/lib/src/responsible/responsible.dart @@ -4,9 +4,14 @@ import '../event/event.dart'; import '../http/response.dart'; import '../locals/locals+get_or_set.dart'; +/// Responsible interface. +/// +/// Any object that implements it can be directly returned in [CloseHandler]. abstract interface class Responsible { + /// Creates a [Response] for current respinsible. Future createResponse(Event event); + /// Adds a [Responsible] factory. static void add(Event event, Responsible Function(T value) factory) { if (has(event, factory)) { return; @@ -19,10 +24,12 @@ abstract interface class Responsible { )); } + /// Has a [Responsible] factory is added. static bool has(Event event, Responsible Function(T value) factory) { return event.responsibleNodes.any((node) => node.id == factory); } + /// Resolve a [Response] of [Event] and [value]. static Responsible of(Event event, T value) { final node = event.responsibleNodes.firstWhereOrNull((node) => node.match(value)); diff --git a/packages/spry/lib/src/routing/route.dart b/packages/spry/lib/src/routing/route.dart index 6723dbf..7504018 100644 --- a/packages/spry/lib/src/routing/route.dart +++ b/packages/spry/lib/src/routing/route.dart @@ -1,5 +1,7 @@ +/// Info about the current route final class Route { const Route({required this.id}); + /// The ID of the current route. final String id; } diff --git a/packages/spry/lib/src/routing/routes_builder+all.dart b/packages/spry/lib/src/routing/routes_builder+all.dart index 59e06a9..fc99ea6 100644 --- a/packages/spry/lib/src/routing/routes_builder+all.dart +++ b/packages/spry/lib/src/routing/routes_builder+all.dart @@ -7,8 +7,10 @@ import 'routes_builder.dart'; import 'routes_builder+on.dart'; extension RoutesBuilderAll on RoutesBuilder { + /// Spry all request method. static const kAllMethod = '#SPRY/__ALL__'; + /// adds a all request method route. void all(String route, FutureOr Function(Event event) closure) { return on(kAllMethod, route, closure); } diff --git a/packages/spry/lib/src/routing/routes_builder+methods.dart b/packages/spry/lib/src/routing/routes_builder+methods.dart index bce3afb..1a0acc6 100644 --- a/packages/spry/lib/src/routing/routes_builder+methods.dart +++ b/packages/spry/lib/src/routing/routes_builder+methods.dart @@ -7,26 +7,40 @@ import 'routes_builder.dart'; import 'routes_builder+on.dart'; extension RoutesBuildrMethods on RoutesBuilder { + /// Registers a `GET` route that responds to with the result of the [closure]. + /// + /// ```dart + /// app.get('/say-hello', (request) async => 'Hello, world!'); + /// ``` void get(String route, FutureOr Function(Event event) closure) { on('GET', route, closure); } + /// Registers a `POST` route that responds to with the result of the [closure]. + /// + /// ```dart + /// app.post('/say/:name', (request) async => request.params.get('name')); + /// ``` void post(String route, FutureOr Function(Event event) closure) { on('POST', route, closure); } + /// Registers a `PUT` route that responds to with the result of the [closure]. void put(String route, FutureOr Function(Event event) closure) { on('PUT', route, closure); } + /// Registers a `PATCH` route that responds to with the result of the [closure]. void patch(String route, FutureOr Function(Event event) closure) { on('PATCH', route, closure); } + /// Registers a `DELETE` route that responds to with the result of the [closure]. void delete(String route, FutureOr Function(Event event) closure) { on('DELETE', route, closure); } + /// Registers a `HEAD` route that responds to with the result of the [closure]. void head(String route, FutureOr Function(Event event) closure) { on('HEAD', route, closure); } diff --git a/packages/spry/lib/src/routing/routes_builder+on.dart b/packages/spry/lib/src/routing/routes_builder+on.dart index 44e702c..587a5ab 100644 --- a/packages/spry/lib/src/routing/routes_builder+on.dart +++ b/packages/spry/lib/src/routing/routes_builder+on.dart @@ -5,6 +5,7 @@ import '../handler/closure_handler.dart'; import 'routes_builder.dart'; extension RoutesBuilderOn on RoutesBuilder { + /// Adds a closure handler. void on( String method, String route, FutureOr Function(Event event) closure) { addRoute(method, route, ClosureHandler(closure)); diff --git a/packages/spry/lib/src/routing/routes_builder.dart b/packages/spry/lib/src/routing/routes_builder.dart index 2651bc3..63a8b24 100644 --- a/packages/spry/lib/src/routing/routes_builder.dart +++ b/packages/spry/lib/src/routing/routes_builder.dart @@ -1,5 +1,7 @@ import '../handler/handler.dart'; +/// Spry routes builder. abstract interface class RoutesBuilder { + /// Adds a route for [method]/[route]/[handler]. void addRoute(String method, String route, Handler handler); } diff --git a/packages/spry/lib/src/spry+add_handler.dart b/packages/spry/lib/src/spry+add_handler.dart index ad4bd10..114f593 100644 --- a/packages/spry/lib/src/spry+add_handler.dart +++ b/packages/spry/lib/src/spry+add_handler.dart @@ -5,5 +5,6 @@ import 'spry.dart'; import 'utils/_spry_internal_utils.dart'; extension SpryAddHandler on Spry { + /// Adds a handler. void addHandler(Handler handler) => handlers.add(handler); } diff --git a/packages/spry/lib/src/spry+fallback.dart b/packages/spry/lib/src/spry+fallback.dart index a692206..242ac4e 100644 --- a/packages/spry/lib/src/spry+fallback.dart +++ b/packages/spry/lib/src/spry+fallback.dart @@ -10,10 +10,12 @@ import 'locals/locals+get_or_null.dart'; import 'spry.dart'; extension SpryFallback on Spry { + /// Registering a handler for a failed routing. void fallback(FutureOr Function(Event event) closure) { locals.set(#spry.app.fallback, ClosureHandler(closure)); } + /// Gets fallback handler. Handler getFallback() { return switch (locals.getOrNull(#spry.app.fallback)) { Handler handler => handler, diff --git a/packages/spry/lib/src/spry+use.dart b/packages/spry/lib/src/spry+use.dart index 05dd2a9..9604714 100644 --- a/packages/spry/lib/src/spry+use.dart +++ b/packages/spry/lib/src/spry+use.dart @@ -8,6 +8,7 @@ import 'spry.dart'; import 'spry+add_handler.dart'; extension SpryHandler on Spry { + /// Adds a closure handler. void use(FutureOr Function(Event event) closure) { addHandler(ClosureHandler(closure)); } diff --git a/packages/spry/lib/src/spry.dart b/packages/spry/lib/src/spry.dart index 516f95e..3c489f7 100644 --- a/packages/spry/lib/src/spry.dart +++ b/packages/spry/lib/src/spry.dart @@ -4,9 +4,11 @@ import 'locals/locals.dart'; import 'routing/routes_builder.dart'; import 'types.dart'; +/// Spry application. class Spry implements RoutesBuilder { const Spry._({required this.locals, required this.router}); + /// Creates a new Spry application/ factory Spry({ final Map? locals, final Router? router, @@ -30,7 +32,10 @@ class Spry implements RoutesBuilder { return app; } + /// Application locals. final Locals locals; + + /// Spry using [Router]. final Router router; @override diff --git a/packages/spry/lib/src/utils/next.dart b/packages/spry/lib/src/utils/next.dart index aebeae0..7717214 100644 --- a/packages/spry/lib/src/utils/next.dart +++ b/packages/spry/lib/src/utils/next.dart @@ -2,6 +2,27 @@ import '../event/event.dart'; import '../http/response.dart'; import '../locals/locals+get_or_null.dart'; +/// Spry allows you to register multiple handlers using 'use', +/// and ultimately they will be executed in the order of registration. +/// +/// But sometimes, we hope that the later registration will be +/// executed first, and then return to the current handler execution +/// after completion, which is the usage scenario of 'next'. +/// +/// ## Example +/// ```dart +/// app.use((event) { print(1); return next(); }); +/// +/// app.use((event) async { +/// final response = await next(event); +/// print(2); +/// return response; +/// }); +/// +/// app.use((event) { print(3) }); +/// +/// # Results: 1,3,2 +/// ``` Future next(Event event) async { final effect = event.locals.getOrNull Function(Event)>(next); event.locals.remove(next); diff --git a/packages/spry/lib/websocket.dart b/packages/spry/lib/websocket.dart index e41a11a..ac1513b 100644 --- a/packages/spry/lib/websocket.dart +++ b/packages/spry/lib/websocket.dart @@ -1,3 +1,6 @@ +/// This library implements WebSockets support for Spry +library spry.websocket; + import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; @@ -5,32 +8,61 @@ import 'dart:typed_data'; import 'constants.dart'; import 'spry.dart'; +/// WebSocket's ready state. extension type const ReadyState(int code) implements int { + /// Unknown ready state. static const unknown = ReadyState(-1); + + /// Connecting ready state. static const connecting = ReadyState(0); + + /// Open ready state. static const open = ReadyState(1); + + /// Closing ready state. static const closing = ReadyState(2); + + /// Closed ready state. static const closed = ReadyState(3); } +/// Peer object allows easily interacting with connected clients. abstract interface class Peer implements Event { + /// Returns websocket [ReadyState]. ReadyState get readyState; + + /// Returns the websocket selected protocol. + /// + /// If server-side no configured protocols, the [protocol] value is null. String? get protocol; + + /// Returns the websocket cliend-side request extensions. String get extensions; + /// Send a bytes [message] to the connected client void send(Uint8List message); + + /// Send a [String] message to the connected client. void sendText(String message); + + /// Close websocket connect. Future close([int? code, String? reason]); } +/// WebSocket message, class Message { const Message._(this.raw); + /// Creates a [String] message. factory Message.text(String text) => Message._(text); + + /// Creates a [Uint8List] message. factory Message.bytes(Uint8List bytes) => Message._(bytes); - final dynamic raw; // Uint8List or String + /// Message raw data, Types: Uint8List or String + final dynamic raw; + /// Returns the message text. String text() { return switch (raw) { String value => value, @@ -38,6 +70,7 @@ class Message { }; } + /// Returns the message bytes. Uint8List bytes() { return switch (raw) { Uint8List bytes => bytes, @@ -96,6 +129,7 @@ class CompressionOptions { final bool enabled; } +/// Create [Peer] options. class CreatePeerOptions { const CreatePeerOptions({ required this.compression, @@ -103,24 +137,63 @@ class CreatePeerOptions { this.protocols, }); + /// WebSocket compression options. final CompressionOptions compression; + + /// Response headers attached when upgrading websocket. final Headers headers; + + /// Define the protocols supported by server side. final Iterable? protocols; } +/// WebSocket platform interface. +/// +/// Usually, it is used together with the Platform, and when implementing +/// the Spry [Platform] interface, if the platform supports WebSocket, +/// then you should use it. +/// +/// ```dart +/// class MyPlatform implements Platform, WebSocketPlatform { +/// // ... +/// } +/// ``` abstract interface class WebSocketPlatform { + /// Upgrading websocket. + /// + /// The return value can be any data you need, for example, in the event + /// of a failure, you can return a [Response] or the result of a fallback call. + /// Due to the different contents returned by different platforms, the return + /// value depends on your implementation. FutureOr websocket(Event event, T request, Hooks hooks); } +/// WebSocket hooks interface. +/// +/// It is used to standardize WebSocket events for various platforms. abstract interface class Hooks { + /// Called when upgrading request to WebSocket, returns [CreatePeerOptions]. FutureOr onUpgrade(Event event); + + /// Hook when receiving messages from connected clients. FutureOr onMessage(Peer peer, Message message); + + /// Received a hook from a connected client or actively closed the websocket + /// call on the server side. FutureOr onClose(Peer peer, {int? code, String? reason}); + + /// Hook for errors from the server side FutureOr onError(Peer peer, dynamic error); + + /// When the request does not support upgrading or fails to upgrade, + /// its return value is the same as that of a normal routing handler, + /// which can be [Responsible] or [Response] supported data. FutureOr fallback(Event event); } +/// Add the [ws] routing method to Spry [RoutesBuilder]. extension RoutesBuilderWS on RoutesBuilder { + /// Register a websocket route. void ws(String route, Hooks hooks) { all(route, (event) async { final platform = event.locals.getOrNull(kPlatform); diff --git a/packages/spry/pubspec.yaml b/packages/spry/pubspec.yaml index 9e68444..eeb87b6 100644 --- a/packages/spry/pubspec.yaml +++ b/packages/spry/pubspec.yaml @@ -1,6 +1,8 @@ name: spry -description: A starting point for Dart libraries or applications. +description: Spry is a lightweight, composable Dart web framework designed to work collaboratively with various runtime platforms. version: 4.0.0-dev.0 +homepage: https://spry.fub +repository: https://github.com/medz/spry environment: sdk: ^3.4.3 From 61c3de47a2ecb8b4296a0aac161f6dff21f1c489 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Tue, 9 Jul 2024 21:08:37 +0800 Subject: [PATCH 25/35] chore: Support cookies --- .../cookie/_event_internal_utils.dart | 101 ----------- packages/composable/cookie/cookies.dart | 44 ----- packages/composable/cookie/event+cookies.dart | 115 ------------- packages/spry_cookie/lib/spry_cookie.dart | 4 + .../spry_cookie/lib/src/_cookies_impl.dart | 158 ++++++++++++++++++ packages/spry_cookie/lib/src/_internal.dart | 108 ++++++++++++ packages/spry_cookie/lib/src/cookies.dart | 53 ++++++ .../spry_cookie/lib/src/event+cookies.dart | 19 +++ .../spry_cookie/lib/src/spry_cookie_base.dart | 6 - packages/spry_cookie/lib/src/utils.dart | 58 +++++++ packages/spry_cookie/pubspec.yaml | 7 +- .../spry_cookie/test/spry_cookies_test.dart | 27 +++ 12 files changed, 431 insertions(+), 269 deletions(-) delete mode 100644 packages/composable/cookie/_event_internal_utils.dart delete mode 100644 packages/composable/cookie/cookies.dart delete mode 100644 packages/composable/cookie/event+cookies.dart create mode 100644 packages/spry_cookie/lib/src/_cookies_impl.dart create mode 100644 packages/spry_cookie/lib/src/_internal.dart create mode 100644 packages/spry_cookie/lib/src/cookies.dart create mode 100644 packages/spry_cookie/lib/src/event+cookies.dart delete mode 100644 packages/spry_cookie/lib/src/spry_cookie_base.dart create mode 100644 packages/spry_cookie/lib/src/utils.dart create mode 100644 packages/spry_cookie/test/spry_cookies_test.dart diff --git a/packages/composable/cookie/_event_internal_utils.dart b/packages/composable/cookie/_event_internal_utils.dart deleted file mode 100644 index 2724741..0000000 --- a/packages/composable/cookie/_event_internal_utils.dart +++ /dev/null @@ -1,101 +0,0 @@ -// import 'package:http_parser/http_parser.dart'; -// import 'package:spry/spry.dart'; - -// import 'cookies.dart'; - -// extension EventInternalUtils on Event { -// Map get requestCookies { -// return locals.getOrSet>( -// #spry.event.request.cookies, -// () => request.headers.cookies, -// ); -// } - -// List get responseCookies { -// return locals.getOrSet>( -// #spry.event.response.cookies, -// () => [], -// ); -// } -// } - -// class SetCookie { -// const SetCookie( -// this.name, -// this.value, { -// this.expires, -// this.maxAge, -// this.domain, -// this.path, -// this.secure = false, -// this.httpOnly = false, -// this.sameSite, -// }); - -// final String name; -// final String value; -// final DateTime? expires; -// final int? maxAge; -// final String? domain; -// final String? path; -// final bool secure; -// final bool httpOnly; -// final SameSite? sameSite; - -// @override -// toString() { -// final buffer = StringBuffer() -// ..write(name) -// ..write('=') -// ..write(value); - -// if (expires != null) { -// buffer -// ..write('; Expires=') -// ..write(formatHttpDate(expires!)); -// } - -// if (maxAge != null) { -// buffer -// ..write('; MaxAge=') -// ..write(maxAge); -// } - -// if (domain != null) { -// buffer -// ..write('Domain=') -// ..write(domain); -// } - -// if (path != null) { -// buffer -// ..write('; Path=') -// ..write(path); -// } - -// if (secure) buffer.write('; Secure'); -// if (httpOnly) buffer.write('; HttpOnly'); -// if (sameSite != null) { -// buffer.write('; SameSite='); -// buffer.write(switch (sameSite!) { -// SameSite.lax => 'Lax', -// SameSite.strict => 'Strict', -// SameSite.none => 'None', -// }); -// } - -// return buffer.toString(); -// } -// } - -// extension on Headers { -// Map get cookies { -// final entries = -// getAll('cookie').map((e) => e.split(';')).expand((e) => e).map((e) { -// final [name, ...values] = e.split('='); -// return MapEntry(name.toLowerCase().trim(), values.join('=').trim()); -// }); - -// return Map.fromEntries(entries); -// } -// } diff --git a/packages/composable/cookie/cookies.dart b/packages/composable/cookie/cookies.dart deleted file mode 100644 index 9552c74..0000000 --- a/packages/composable/cookie/cookies.dart +++ /dev/null @@ -1,44 +0,0 @@ -enum SameSite { lax, strict, none } - -abstract interface class Cookies { - String? get(String name, {String Function(String value)? decode}); - - Iterable<(String, String)> getAll({String Function(String value)? decode}); - - void set( - String name, - String value, { - DateTime? expires, - int? maxAge, - String? domain, - String? path, - bool secure = false, - bool httpOnly = false, - SameSite? sameSite, - String Function(String value)? encode, - }); - - void delete( - String name, { - DateTime? expires, - int? maxAge, - String? domain, - String? path, - bool secure = false, - bool httpOnly = false, - SameSite? sameSite, - }); - - String serialize( - String name, - String value, { - DateTime? expires, - int? maxAge, - String? domain, - String? path, - bool secure = false, - bool httpOnly = false, - SameSite? sameSite, - String Function(String value)? encode, - }); -} diff --git a/packages/composable/cookie/event+cookies.dart b/packages/composable/cookie/event+cookies.dart deleted file mode 100644 index 275acd9..0000000 --- a/packages/composable/cookie/event+cookies.dart +++ /dev/null @@ -1,115 +0,0 @@ -// // ignore_for_file: file_names - -// import 'package:spry/spry.dart'; - -// import 'cookies.dart'; -// import '_event_internal_utils.dart'; - -// extension EventCookies on Event { -// Cookies get cookies { -// return locals.getOrSet(Cookies, () => _CookiesImpl(this)); -// } -// } - -// final class _CookiesImpl implements Cookies { -// static const defaultEncode = Uri.encodeComponent; -// static const defaultDecode = Uri.decodeComponent; - -// const _CookiesImpl(this.event); - -// final Event event; - -// @override -// String? get(String name, {String Function(String value)? decode}) { -// final normalizedName = name.toLowerCase(); -// for (final cookie in event.responseCookies) { -// if (cookie.name.toLowerCase() == normalizedName) { -// return (decode ?? defaultDecode).call(cookie.value); -// } -// } - -// final requestCookieValue = event.requestCookies[normalizedName]; -// if (requestCookieValue != null) { -// return (decode ?? defaultDecode).call(requestCookieValue); -// } - -// return null; -// } - -// @override -// Iterable<(String, String)> getAll( -// {String Function(String value)? decode}) sync* { -// final inner = decode ?? defaultDecode; - -// yield* event.requestCookies.entries.map((e) => (e.key, inner(e.value))); -// yield* event.responseCookies.map((e) => (e.name, inner(e.value))); -// } - -// @override -// void set(String name, String value, -// {DateTime? expires, -// int? maxAge, -// String? domain, -// String? path, -// bool secure = false, -// bool httpOnly = false, -// SameSite? sameSite, -// String Function(String value)? encode}) { -// event.responseCookies.add(SetCookie( -// name, -// (encode ?? defaultEncode).call(value), -// expires: expires, -// maxAge: maxAge, -// domain: domain, -// path: path, -// secure: secure, -// httpOnly: httpOnly, -// sameSite: sameSite, -// )); -// } - -// @override -// void delete(String name, -// {DateTime? expires, -// int? maxAge, -// String? domain, -// String? path, -// bool secure = false, -// bool httpOnly = false, -// SameSite? sameSite}) { -// event.responseCookies -// .removeWhere((e) => e.name.toLowerCase() == name.toLowerCase()); - -// set(name, '', -// expires: expires ?? DateTime.now(), -// maxAge: maxAge, -// domain: domain, -// path: path, -// secure: secure, -// httpOnly: httpOnly, -// sameSite: sameSite); -// } - -// @override -// String serialize(String name, String value, -// {DateTime? expires, -// int? maxAge, -// String? domain, -// String? path, -// bool secure = false, -// bool httpOnly = false, -// SameSite? sameSite, -// String Function(String value)? encode}) { -// return SetCookie( -// name, -// (encode ?? defaultEncode).call(value), -// expires: expires, -// maxAge: maxAge, -// domain: domain, -// path: path, -// secure: secure, -// httpOnly: httpOnly, -// sameSite: sameSite, -// ).toString(); -// } -// } diff --git a/packages/spry_cookie/lib/spry_cookie.dart b/packages/spry_cookie/lib/spry_cookie.dart index 8b13789..ee397e0 100644 --- a/packages/spry_cookie/lib/spry_cookie.dart +++ b/packages/spry_cookie/lib/spry_cookie.dart @@ -1 +1,5 @@ +library spry.cookies; +export 'src/cookies.dart'; +export 'src/event+cookies.dart'; +export 'src/utils.dart'; diff --git a/packages/spry_cookie/lib/src/_cookies_impl.dart b/packages/spry_cookie/lib/src/_cookies_impl.dart new file mode 100644 index 0000000..caf51ad --- /dev/null +++ b/packages/spry_cookie/lib/src/_cookies_impl.dart @@ -0,0 +1,158 @@ +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; +import 'package:spry/spry.dart'; + +import '_internal.dart'; +import 'cookies.dart'; + +final class CookiesImpl implements Cookies { + const CookiesImpl(this.event, [this.hmac]); + + final Event event; + final Hmac? hmac; + + @override + String? get(String name, {String Function(String value)? decode}) { + final normalizedName = name.toLowerCase(); + for (final cookie in event.responseCookies) { + if (cookie.name.toLowerCase() == normalizedName) { + return decodeSignedValue(cookie.value); + } + } + + final requestCookieValue = event.requestCookies[normalizedName]; + if (requestCookieValue != null) { + return decodeSignedValue(requestCookieValue); + } + + return null; + } + + @override + Iterable<(String, String)> getAll() sync* { + for (final cookie in event.requestCookies.entries) { + final value = decodeSignedValue(cookie.value); + if (value == null) continue; + + yield (cookie.key, value); + } + + for (final cookie in event.responseCookies) { + final value = decodeSignedValue(cookie.value); + if (value == null) continue; + + yield (cookie.name, value); + } + } + + @override + void set( + String name, + String value, { + DateTime? expires, + int? maxAge, + String? domain, + String? path, + bool? secure, + bool? httpOnly, + SameSite? sameSite, + bool? partitioned, + }) { + event.responseCookies.add(SetCookie( + name, + encodeSignedValue(value), + expires: expires, + maxAge: maxAge, + domain: domain, + path: path, + secure: secure, + httpOnly: httpOnly, + sameSite: sameSite, + partitioned: partitioned, + )); + } + + @override + void delete( + String name, { + DateTime? expires, + int? maxAge, + String? domain, + String? path, + bool? secure, + bool? httpOnly, + SameSite? sameSite, + bool? partitioned, + }) { + event.responseCookies + .removeWhere((e) => e.name.toLowerCase() == name.toLowerCase()); + + set(name, '', + expires: expires ?? DateTime.now(), + maxAge: maxAge, + domain: domain, + path: path, + secure: secure, + httpOnly: httpOnly, + sameSite: sameSite, + partitioned: partitioned); + } + + @override + String serialize( + String name, + String value, { + DateTime? expires, + int? maxAge, + String? domain, + String? path, + bool? secure, + bool? httpOnly, + SameSite? sameSite, + bool signed = true, + bool? partitioned, + }) { + return SetCookie( + name, + signed ? encodeSignedValue(value) : value, + expires: expires, + maxAge: maxAge, + domain: domain, + path: path, + secure: secure, + httpOnly: httpOnly, + sameSite: sameSite, + partitioned: partitioned, + ).toString(); + } + + String encodeSignedValue(String value) { + if (value.isEmpty) return ''; + if (hmac == null) return Uri.encodeComponent(value); + + final bytes = utf8.encode(value); + final hex = base64Url.encode(hmac!.convert(bytes).bytes); + final encoded = base64Url.encode(bytes); + + return Uri.encodeComponent('$encoded.$hex'.replaceAll('=', '')); + } + + String? decodeSignedValue(String signed) { + if (hmac == null) return Uri.decodeComponent(signed); + + final parts = signed.split('.'); + if (parts.length < 2) return null; + + final [...encodedParts, sign] = parts; + final bytes = base64Url.decode(Uri.decodeComponent(encodedParts.join('.'))); + final hex = + base64Url.encode(hmac!.convert(bytes).bytes).replaceAll('=', ''); + + if (hex == Uri.decodeComponent(sign)) { + return utf8.decode(bytes); + } + + return null; + } +} diff --git a/packages/spry_cookie/lib/src/_internal.dart b/packages/spry_cookie/lib/src/_internal.dart new file mode 100644 index 0000000..3dfa292 --- /dev/null +++ b/packages/spry_cookie/lib/src/_internal.dart @@ -0,0 +1,108 @@ +import 'package:http_parser/http_parser.dart'; +import 'package:spry/spry.dart'; + +import 'cookies.dart'; + +const kCookiesInstance = #spry.cookies; +const kRequestCookies = #spry.event.request.cookies; +const kResponseCookies = #spry.event.response.cookies; + +extension EventInternalUtils on Event { + Map get requestCookies { + return locals.getOrSet>( + kRequestCookies, + () => request.headers.cookies, + ); + } + + List get responseCookies { + return locals.getOrSet>( + kResponseCookies, + () => [], + ); + } +} + +class SetCookie { + SetCookie( + this.name, + this.value, { + this.expires, + this.maxAge, + this.domain, + this.path, + this.secure, + this.httpOnly, + this.sameSite, + this.partitioned, + }); + + final String name; + final String value; + DateTime? expires; + int? maxAge; + String? domain; + String? path; + bool? secure; + bool? httpOnly; + SameSite? sameSite; + bool? partitioned; + + @override + String toString() { + final buffer = StringBuffer() + ..write(name) + ..write('=') + ..write(value); + + if (expires != null) { + buffer + ..write('; Expires=') + ..write(formatHttpDate(expires!)); + } + + if (maxAge != null) { + buffer + ..write('; Max-Age=') + ..write(maxAge); + } + + if (domain != null) { + buffer + ..write('; Domain=') + ..write(domain); + } + + if (path != null) { + buffer + ..write('; Path=') + ..write(path); + } + + if (secure == true) buffer.write('; Secure'); + if (httpOnly == true) buffer.write('; HttpOnly'); + if (partitioned == true) buffer.write('; Partitioned'); + if (sameSite != null) { + buffer.write('; SameSite='); + buffer.write(switch (sameSite!) { + SameSite.lax => 'Lax', + SameSite.strict => 'Strict', + SameSite.none => 'None', + }); + } + + return buffer.toString(); + } +} + +extension on Headers { + Map get cookies { + final entries = + getAll('cookie').map((e) => e.split(';')).expand((e) => e).map((e) { + final [name, ...values] = e.split('='); + return MapEntry(name.toLowerCase().trim(), values.join('=').trim()); + }); + + return Map.fromEntries(entries); + } +} diff --git a/packages/spry_cookie/lib/src/cookies.dart b/packages/spry_cookie/lib/src/cookies.dart new file mode 100644 index 0000000..28b5f83 --- /dev/null +++ b/packages/spry_cookie/lib/src/cookies.dart @@ -0,0 +1,53 @@ +/// [Set-Cookie#samesitesamesite-value](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value) +enum SameSite { lax, strict, none } + +/// Spry cookies container. +abstract interface class Cookies { + /// Gets a Request/Response cookie value. + String? get(String name); + + /// Gets all Request/Response cookies. + Iterable<(String, String)> getAll(); + + /// Sets a new cookie. + void set( + String name, + String value, { + DateTime? expires, + int? maxAge, + String? domain, + String? path, + bool? secure, + bool? httpOnly, + SameSite? sameSite, + bool? partitioned, + }); + + /// Deletes a cookie. + void delete( + String name, { + DateTime? expires, + int? maxAge, + String? domain, + String? path, + bool? secure, + bool? httpOnly, + SameSite? sameSite, + bool? partitioned, + }); + + /// Serialize a cookie. + String serialize( + String name, + String value, { + DateTime? expires, + int? maxAge, + String? domain, + String? path, + bool? secure, + bool? httpOnly, + SameSite? sameSite, + bool? partitioned, + bool signed, + }); +} diff --git a/packages/spry_cookie/lib/src/event+cookies.dart b/packages/spry_cookie/lib/src/event+cookies.dart new file mode 100644 index 0000000..ef39452 --- /dev/null +++ b/packages/spry_cookie/lib/src/event+cookies.dart @@ -0,0 +1,19 @@ +// ignore_for_file: file_names + +import 'package:spry/spry.dart'; + +import '_internal.dart'; +import 'cookies.dart'; + +extension EventCookies on Event { + /// Returns the [Cookies] for the current [Event]. + Cookies get cookies { + final instance = locals.getOrNull(kCookiesInstance); + if (instance == null) { + throw Exception( + 'Cookies are not enabled. Please enable using `app.use(cookie())`'); + } + + return instance; + } +} diff --git a/packages/spry_cookie/lib/src/spry_cookie_base.dart b/packages/spry_cookie/lib/src/spry_cookie_base.dart deleted file mode 100644 index e8a6f15..0000000 --- a/packages/spry_cookie/lib/src/spry_cookie_base.dart +++ /dev/null @@ -1,6 +0,0 @@ -// TODO: Put public facing types in this file. - -/// Checks if you are awesome. Spoiler: you are. -class Awesome { - bool get isAwesome => true; -} diff --git a/packages/spry_cookie/lib/src/utils.dart b/packages/spry_cookie/lib/src/utils.dart new file mode 100644 index 0000000..f53424a --- /dev/null +++ b/packages/spry_cookie/lib/src/utils.dart @@ -0,0 +1,58 @@ +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; +import 'package:spry/spry.dart'; + +import '_cookies_impl.dart'; +import '_internal.dart'; +import 'cookies.dart'; + +/// Creates a cookie support handler closure. +Future Function(Event) cookie({ + String? secret, + Hash algorithm = sha256, + bool autoSecureSet = true, + DateTime? expires, + int? maxAge, + String? domain, + String? path, + bool? secure, + bool? httpOnly, + SameSite? sameSite, + bool? partitioned, +}) { + final hmac = switch (secret) { + String secret => Hmac(algorithm, utf8.encode(secret)), + _ => null + }; + + return (event) async { + event.locals.set(kCookiesInstance, CookiesImpl(event, hmac)); + + final response = await next(event); + final cookies = event.responseCookies; + final builder = response.headers.toBuilder(); + final autoSecure = switch (autoSecureSet) { + true => event.uri.isScheme('https') || event.uri.isScheme('wss'), + _ => false, + }; + + for (final cookie in cookies) { + cookie + ..expires = cookie.expires ?? expires + ..maxAge = cookie.maxAge ?? maxAge + ..domain = cookie.domain ?? domain + ..path = cookie.path ?? path + ..secure = cookie.secure ?? secure ?? autoSecure + ..httpOnly = cookie.httpOnly ?? httpOnly + ..sameSite = cookie.sameSite ?? sameSite + ..partitioned = cookie.partitioned ?? partitioned; + + builder.add('set-cookie', cookie.toString()); + } + + cookies.clear(); + + return response.copyWith(headers: builder.toHeaders()); + }; +} diff --git a/packages/spry_cookie/pubspec.yaml b/packages/spry_cookie/pubspec.yaml index 8047721..cc354f6 100644 --- a/packages/spry_cookie/pubspec.yaml +++ b/packages/spry_cookie/pubspec.yaml @@ -1,15 +1,16 @@ name: spry_cookie description: A starting point for Dart libraries or applications. version: 1.0.0 -# repository: https://github.com/my_org/my_repo +homepage: https://spry.fun/cookies +repository: https://github.com/medz/spry environment: sdk: ^3.4.3 dependencies: + crypto: ^3.0.3 http_parser: ^4.1.0 - spry: - path: ../spry + spry: ^4.0.0-dev dev_dependencies: lints: ^3.0.0 diff --git a/packages/spry_cookie/test/spry_cookies_test.dart b/packages/spry_cookie/test/spry_cookies_test.dart new file mode 100644 index 0000000..c606527 --- /dev/null +++ b/packages/spry_cookie/test/spry_cookies_test.dart @@ -0,0 +1,27 @@ +import 'package:spry/spry.dart'; +import 'package:spry/plain.dart'; +import 'package:spry_cookie/spry_cookie.dart'; + +void main() async { + final app = Spry(); + + app.use(cookie( + domain: 'spry.fun', + secret: "", + )); + + app.all('/', (event) { + event.cookies + .set('a', '你好', httpOnly: false, expires: DateTime.now(), maxAge: 12); + + print(event.cookies.get('a')); + }); + + final handler = app.toPlainHandler(); + final request = PlainRequest(method: 'get', uri: Uri(scheme: 'https')); + final response = await handler(request); + + for (final header in response.headers) { + print(header); + } +} From a7cceee09b5bc3b67079f2cab2c553aa32504fef Mon Sep 17 00:00:00 2001 From: Seven Du Date: Tue, 9 Jul 2024 21:48:03 +0800 Subject: [PATCH 26/35] chore: cookies --- .github/dependabot.yml | 6 ++ .github/workflows/test.yml | 6 +- .../spry_cookie/lib/src/_cookies_impl.dart | 1 + packages/spry_cookie/lib/src/utils.dart | 45 +++++++++++++- packages/spry_cookie/pubspec.yaml | 4 +- packages/spry_cookie/test/cookies_test.dart | 59 +++++++++++++++++++ .../spry_cookie/test/spry_cookies_test.dart | 27 --------- 7 files changed, 113 insertions(+), 35 deletions(-) create mode 100644 packages/spry_cookie/test/cookies_test.dart delete mode 100644 packages/spry_cookie/test/spry_cookies_test.dart diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2a1dba6..97910d9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,3 +10,9 @@ updates: directory: /packages/spry schedule: interval: daily + + # spry_cookie + - package-ecosystem: pub + directory: /packages/spry_cookie + schedule: + interval: daily diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index af84f77..00272af 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: Test +name: test on: push: @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - packages: [spry] + packages: [spry, spry_cookie] steps: - uses: actions/checkout@v4 - uses: dart-lang/setup-dart@v1 @@ -23,6 +23,6 @@ jobs: - name: Analyze run: dart analyze working-directory: packages/${{ matrix.packages }} - - name: Test + - name: run tests run: dart test working-directory: packages/${{ matrix.packages }} diff --git a/packages/spry_cookie/lib/src/_cookies_impl.dart b/packages/spry_cookie/lib/src/_cookies_impl.dart index caf51ad..e678277 100644 --- a/packages/spry_cookie/lib/src/_cookies_impl.dart +++ b/packages/spry_cookie/lib/src/_cookies_impl.dart @@ -139,6 +139,7 @@ final class CookiesImpl implements Cookies { } String? decodeSignedValue(String signed) { + if (signed.isEmpty) return null; if (hmac == null) return Uri.decodeComponent(signed); final parts = signed.split('.'); diff --git a/packages/spry_cookie/lib/src/utils.dart b/packages/spry_cookie/lib/src/utils.dart index f53424a..fa9797f 100644 --- a/packages/spry_cookie/lib/src/utils.dart +++ b/packages/spry_cookie/lib/src/utils.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'package:crypto/crypto.dart'; @@ -7,8 +8,9 @@ import '_cookies_impl.dart'; import '_internal.dart'; import 'cookies.dart'; -/// Creates a cookie support handler closure. -Future Function(Event) cookie({ +/// Wrap a handle closure with cookies. +FutureOr Function(Event) cookieWith( + FutureOr Function(Event event) closure, { String? secret, Hash algorithm = sha256, bool autoSecureSet = true, @@ -25,11 +27,18 @@ Future Function(Event) cookie({ String secret => Hmac(algorithm, utf8.encode(secret)), _ => null }; + final handler = switch (closure) { + next => null, + _ => ClosureHandler(closure), + }; return (event) async { event.locals.set(kCookiesInstance, CookiesImpl(event, hmac)); - final response = await next(event); + final response = await switch (handler) { + Handler(handle: final handle) => handle(event), + _ => next(event), + }; final cookies = event.responseCookies; final builder = response.headers.toBuilder(); final autoSecure = switch (autoSecureSet) { @@ -56,3 +65,33 @@ Future Function(Event) cookie({ return response.copyWith(headers: builder.toHeaders()); }; } + +/// Creates a cookie support handler closure. +FutureOr Function(Event) cookie({ + String? secret, + Hash algorithm = sha256, + bool autoSecureSet = true, + DateTime? expires, + int? maxAge, + String? domain, + String? path, + bool? secure, + bool? httpOnly, + SameSite? sameSite, + bool? partitioned, +}) { + return cookieWith( + next, + secret: secret, + algorithm: algorithm, + autoSecureSet: autoSecureSet, + expires: expires, + maxAge: maxAge, + domain: domain, + path: path, + secure: secure, + httpOnly: httpOnly, + sameSite: sameSite, + partitioned: partitioned, + ); +} diff --git a/packages/spry_cookie/pubspec.yaml b/packages/spry_cookie/pubspec.yaml index cc354f6..c1a10ab 100644 --- a/packages/spry_cookie/pubspec.yaml +++ b/packages/spry_cookie/pubspec.yaml @@ -13,5 +13,5 @@ dependencies: spry: ^4.0.0-dev dev_dependencies: - lints: ^3.0.0 - test: ^1.24.0 + lints: ^4.0.0 + test: ^1.25.8 diff --git a/packages/spry_cookie/test/cookies_test.dart b/packages/spry_cookie/test/cookies_test.dart new file mode 100644 index 0000000..c1cfd25 --- /dev/null +++ b/packages/spry_cookie/test/cookies_test.dart @@ -0,0 +1,59 @@ +import 'package:spry/spry.dart'; +import 'package:spry/plain.dart'; +import 'package:spry_cookie/spry_cookie.dart'; +import 'package:test/test.dart'; + +void main() async { + test('cookies get set correctly', () async { + final app = Spry()..use(cookie()); + + app.get('/test', (event) { + event.cookies.set('foo', 'bar'); + + expect(event.cookies.get('foo'), equals('bar')); + }); + + final handler = app.toPlainHandler(); + final request = PlainRequest(method: 'get', uri: Uri(path: "/test")); + final response = await handler(request); + + expect(response.headers.get('set-cookie'), 'foo=bar'); + }); + + test('should set multiple cookies', () async { + final app = Spry(); + + app.use(cookie()); + app.use((event) async { + event.cookies.set('middleware', '1'); + + final response = await next(event); + + expect(event.cookies.get('foo'), equals('foo')); + expect(event.cookies.get('bar'), equals('bar')); + expect(event.cookies.get('wee'), equals('wer')); + expect(event.cookies.get('middleware'), isNull); + + return response; + }); + app.get('/test', (event) { + expect(event.cookies.get('middleware'), equals('1')); + + event.cookies + ..set('foo', 'foo') + ..set('bar', 'bar') + ..set('wee', 'wer') + ..delete('middleware'); + }); + + final handler = app.toPlainHandler(); + final request = PlainRequest(method: 'get', uri: Uri(path: '/test')); + final response = await handler(request); + final cookies = response.headers.get('set-cookie'); + + expect(cookies, contains("foo=foo")); + expect(cookies, contains("bar=bar")); + expect(cookies, contains("wee=wer")); + expect(cookies, contains("middleware=;")); + }); +} diff --git a/packages/spry_cookie/test/spry_cookies_test.dart b/packages/spry_cookie/test/spry_cookies_test.dart deleted file mode 100644 index c606527..0000000 --- a/packages/spry_cookie/test/spry_cookies_test.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:spry/spry.dart'; -import 'package:spry/plain.dart'; -import 'package:spry_cookie/spry_cookie.dart'; - -void main() async { - final app = Spry(); - - app.use(cookie( - domain: 'spry.fun', - secret: "", - )); - - app.all('/', (event) { - event.cookies - .set('a', '你好', httpOnly: false, expires: DateTime.now(), maxAge: 12); - - print(event.cookies.get('a')); - }); - - final handler = app.toPlainHandler(); - final request = PlainRequest(method: 'get', uri: Uri(scheme: 'https')); - final response = await handler(request); - - for (final header in response.headers) { - print(header); - } -} From 99c3083d989a6cdd252fd2fa61ed90b2cd48b59d Mon Sep 17 00:00:00 2001 From: Seven Du Date: Tue, 9 Jul 2024 23:01:55 +0800 Subject: [PATCH 27/35] chore: * --- packages/spry/lib/spry.dart | 1 + .../spry/lib/src/handler/closure_handler.dart | 2 +- .../lib/src/routing/routes_builder+group.dart | 91 +++++++++++++++++++ packages/spry/test/routing/group_test.dart | 55 +++++++++++ 4 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 packages/spry/lib/src/routing/routes_builder+group.dart create mode 100644 packages/spry/test/routing/group_test.dart diff --git a/packages/spry/lib/spry.dart b/packages/spry/lib/spry.dart index 7bba646..c3721bd 100644 --- a/packages/spry/lib/spry.dart +++ b/packages/spry/lib/spry.dart @@ -50,5 +50,6 @@ export 'src/routing/routes_builder.dart'; export 'src/routing/routes_builder+on.dart'; export 'src/routing/routes_builder+all.dart'; export 'src/routing/routes_builder+methods.dart'; +export 'src/routing/routes_builder+group.dart'; export 'src/utils/next.dart'; diff --git a/packages/spry/lib/src/handler/closure_handler.dart b/packages/spry/lib/src/handler/closure_handler.dart index ccb6b8e..84493c9 100644 --- a/packages/spry/lib/src/handler/closure_handler.dart +++ b/packages/spry/lib/src/handler/closure_handler.dart @@ -8,7 +8,7 @@ import 'handler.dart'; /// Implement a [Handler] that supports [Responsible] return values. final class ClosureHandler implements Handler { - const ClosureHandler(FutureOr Function(Event) closure) + const ClosureHandler(FutureOr Function(Event event) closure) : _closure = closure; final FutureOr Function(Event) _closure; diff --git a/packages/spry/lib/src/routing/routes_builder+group.dart b/packages/spry/lib/src/routing/routes_builder+group.dart new file mode 100644 index 0000000..3f94b72 --- /dev/null +++ b/packages/spry/lib/src/routing/routes_builder+group.dart @@ -0,0 +1,91 @@ +// ignore_for_file: file_names + +import '../handler/closure_handler.dart'; +import '../handler/handler.dart'; +import '../utils/next.dart'; +import 'routes_builder.dart'; + +extension RoutesBuilderGroup on RoutesBuilder { + /// Creates and returns a new [RoutesBuilder] wrapped in the supplied array of + /// [Middleware] and paths. + /// + /// ```dart + /// final routes = app.groupd(route: '/admin', uses: [ + /// ClosureHandler(cookie()), // Admin need cookie. + /// ]); + /// + /// routes.get('/users', (event) => 'users'); + /// routes.get('/posts', (event) => 'posts'); + /// + /// // GET /admin/users -> users + /// // GET /admin/posts -> posts + /// ``` + /// + /// - [uses] - The [Handler]s to wrap the [RoutesBuilder] in. + /// - [path] - The path to wrap the [RoutesBuilder] in. + RoutesBuilder groupd({String? route, Iterable? uses}) { + RoutesBuilder current = this; + + if (route != null && route.isNotEmpty) { + current = _RouteGroupRoutesBuilder(current, route); + } + + if (uses != null && uses.isNotEmpty) { + final handlers = switch (uses) { + List(reversed: final reversed) => reversed, + Iterable(toList: final toList) => toList().reversed, + }; + + current = _UsesGroupRoutesBuilder(current, handlers); + } + + return current; + } + + /// Creates and returns a new [RoutesBuilder] wrapped in the supplied array of + /// [Handler] with closure containing routes. + /// + /// @see [groupd] + T group( + T Function(RoutesBuilder routes) closure, { + String? route, + Iterable? uses, + }) { + return closure(groupd(route: route, uses: uses)); + } +} + +class _RouteGroupRoutesBuilder implements RoutesBuilder { + const _RouteGroupRoutesBuilder(this.parent, this.prefix); + + final RoutesBuilder parent; + final String prefix; + + @override + void addRoute(String method, String route, Handler handler) { + parent.addRoute(method, '$prefix/$route', handler); + } +} + +class _UsesGroupRoutesBuilder implements RoutesBuilder { + const _UsesGroupRoutesBuilder(this.parent, this.handlers); + + final RoutesBuilder parent; + final Iterable handlers; + + @override + void addRoute(String method, String route, Handler handler) { + parent.addRoute(method, route, createHandlerWith(handler)); + } + + Handler createHandlerWith(Handler handler) { + return handlers.fold( + handler, + (effect, current) => ClosureHandler((event) { + event.locals.set(next, effect.handle); + + return current.handle(event); + }), + ); + } +} diff --git a/packages/spry/test/routing/group_test.dart b/packages/spry/test/routing/group_test.dart new file mode 100644 index 0000000..406859b --- /dev/null +++ b/packages/spry/test/routing/group_test.dart @@ -0,0 +1,55 @@ +import 'package:spry/plain.dart'; +import 'package:spry/spry.dart'; +import 'package:test/test.dart'; + +void main() { + test('group route', () async { + final app = Spry(); + + app.group(route: '/api', (routes) { + routes.get('1', (_) => '/api/1'); + routes.get('2', (_) => '/api/2'); + routes.get(':name', (event) => '/api/${event.params('name')}'); + }); + + final handler = app.toPlainHandler(); + + final paths = ['/api/1', '/api/2', '/api/name', '/api/test']; + for (final path in paths) { + final request = PlainRequest(method: 'get', uri: Uri(path: path)); + final response = await handler(request); + + expect(await response.text(), equals(path)); + } + }); + + test('group uses', () async { + final app = Spry(); + final group1 = app.groupd(uses: [ + ClosureHandler((event) async { + event.locals.set('group', 1); + + return next(event); + }), + ]); + final group2 = app.groupd(uses: [ + ClosureHandler((event) async { + event.locals.set('group', 2); + + return next(event); + }), + ]); + + group1.get('/test1', (event) => event.locals.get('group')); + group2.get('/test2', (event) => event.locals.get('group')); + + final handler = app.toPlainHandler(); + final routes = [('/test1', '1'), ('/test2', '2')]; + for (final route in routes) { + final request = PlainRequest(method: 'get', uri: Uri(path: route.$1)); + final response = await handler(request); + + expect(await response.text(), equals(route.$2)); + } + }); +} From bcebef5bd721dd8ae7e8f82c3f97dc4e95aee3a8 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Tue, 9 Jul 2024 23:06:05 +0800 Subject: [PATCH 28/35] fix(io): websocket close --- packages/spry/lib/io.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/spry/lib/io.dart b/packages/spry/lib/io.dart index 74dfe29..b8cb0ca 100644 --- a/packages/spry/lib/io.dart +++ b/packages/spry/lib/io.dart @@ -185,8 +185,7 @@ class _IOPeer implements Peer { @override Future close([int? code, String? reason]) { - // TODO: implement close - throw UnimplementedError(); + return websocket.close(code, reason); } @override From f2163abf22bf70d6fc91833f86bfbfdc35227c64 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Wed, 10 Jul 2024 14:51:41 +0800 Subject: [PATCH 29/35] chore: WIP --- packages/spry/lib/io.dart | 8 -------- packages/spry/lib/plain.dart | 10 ---------- packages/spry/pubspec.yaml | 2 +- packages/spry/test/routing/group_test.dart | 7 ++++--- 4 files changed, 5 insertions(+), 22 deletions(-) diff --git a/packages/spry/lib/io.dart b/packages/spry/lib/io.dart index b8cb0ca..e002e01 100644 --- a/packages/spry/lib/io.dart +++ b/packages/spry/lib/io.dart @@ -138,14 +138,6 @@ class IOPlatform } } -/// Add the [toIOHandler] auxiliary method to [Spry]. -extension SpryToIOHandler on Spry { - /// Returns the [Spry] application to an [HttpServer] compatible processor in 'dart:io'. - PlatformHandler toIOHandler() { - return const IOPlatform().createHandler(this); - } -} - extension on CreatePeerOptions { CompressionOptions get ioCompressionOptions { return CompressionOptions( diff --git a/packages/spry/lib/plain.dart b/packages/spry/lib/plain.dart index 414df5a..e98e013 100644 --- a/packages/spry/lib/plain.dart +++ b/packages/spry/lib/plain.dart @@ -32,8 +32,6 @@ class PlainRequest implements Request { } /// Plain Platform. -/// -/// **NOTE**: The plain platform does not support websocket. class PlainPlatform implements Platform { const PlainPlatform(); @@ -68,11 +66,3 @@ class PlainPlatform implements Platform { return ''; } } - -/// Add the [toPlainHandler] helper method to the [Spry] application. -extension SpryToPlainHandler on Spry { - /// **NOTE**: The plain platform does not support websocket. - PlatformHandler toPlainHandler() { - return const PlainPlatform().createHandler(this); - } -} diff --git a/packages/spry/pubspec.yaml b/packages/spry/pubspec.yaml index eeb87b6..726c102 100644 --- a/packages/spry/pubspec.yaml +++ b/packages/spry/pubspec.yaml @@ -1,6 +1,6 @@ name: spry description: Spry is a lightweight, composable Dart web framework designed to work collaboratively with various runtime platforms. -version: 4.0.0-dev.0 +version: 4.0.0-dev.1 homepage: https://spry.fub repository: https://github.com/medz/spry diff --git a/packages/spry/test/routing/group_test.dart b/packages/spry/test/routing/group_test.dart index 406859b..7698c5f 100644 --- a/packages/spry/test/routing/group_test.dart +++ b/packages/spry/test/routing/group_test.dart @@ -3,6 +3,8 @@ import 'package:spry/spry.dart'; import 'package:test/test.dart'; void main() { + const plain = PlainPlatform(); + test('group route', () async { final app = Spry(); @@ -12,8 +14,7 @@ void main() { routes.get(':name', (event) => '/api/${event.params('name')}'); }); - final handler = app.toPlainHandler(); - + final handler = plain.createHandler(app); final paths = ['/api/1', '/api/2', '/api/name', '/api/test']; for (final path in paths) { final request = PlainRequest(method: 'get', uri: Uri(path: path)); @@ -43,7 +44,7 @@ void main() { group1.get('/test1', (event) => event.locals.get('group')); group2.get('/test2', (event) => event.locals.get('group')); - final handler = app.toPlainHandler(); + final handler = plain.createHandler(app); final routes = [('/test1', '1'), ('/test2', '2')]; for (final route in routes) { final request = PlainRequest(method: 'get', uri: Uri(path: route.$1)); From 2385b4cecc4005a00af4183b8c98927f6d3b448a Mon Sep 17 00:00:00 2001 From: Seven Du Date: Wed, 10 Jul 2024 15:12:08 +0800 Subject: [PATCH 30/35] chore: * --- CHANGELOG.md | 20 ++++++++- .../spry/lib/src/http/response+copy_with.dart | 2 + .../lib/src/routing/routes_builder+on.dart | 2 + packages/spry/pubspec.yaml | 4 +- packages/spry_cookie/README.md | 44 +++++-------------- packages/spry_cookie/pubspec.yaml | 4 +- packages/spry_cookie/test/cookies_test.dart | 6 ++- 7 files changed, 42 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index effe43c..c98d720 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ -## 1.0.0 +# Spry v4.0.0-dev.2 -- Initial version. +To install Spry v4.0.0-dev.2 run this command: + +```bash +dart pub add spry:^4.0.0-dev.2 +``` + +Or update your `pubspec.yaml` file: + +```yaml +dependencies: + spry: ^4.0.0-dev.2 +``` + +## What's Changed + +* fixed website url +* fixed static analysis diff --git a/packages/spry/lib/src/http/response+copy_with.dart b/packages/spry/lib/src/http/response+copy_with.dart index 4b45c0a..871f3d8 100644 --- a/packages/spry/lib/src/http/response+copy_with.dart +++ b/packages/spry/lib/src/http/response+copy_with.dart @@ -1,3 +1,5 @@ +// ignore_for_file: file_names + import 'dart:typed_data'; import 'headers/headers.dart'; diff --git a/packages/spry/lib/src/routing/routes_builder+on.dart b/packages/spry/lib/src/routing/routes_builder+on.dart index 587a5ab..6f2df03 100644 --- a/packages/spry/lib/src/routing/routes_builder+on.dart +++ b/packages/spry/lib/src/routing/routes_builder+on.dart @@ -1,3 +1,5 @@ +// ignore_for_file: file_names + import 'dart:async'; import '../event/event.dart'; diff --git a/packages/spry/pubspec.yaml b/packages/spry/pubspec.yaml index 726c102..c0fe135 100644 --- a/packages/spry/pubspec.yaml +++ b/packages/spry/pubspec.yaml @@ -1,7 +1,7 @@ name: spry description: Spry is a lightweight, composable Dart web framework designed to work collaboratively with various runtime platforms. -version: 4.0.0-dev.1 -homepage: https://spry.fub +version: 4.0.0-dev.2 +homepage: https://spry.fun repository: https://github.com/medz/spry environment: diff --git a/packages/spry_cookie/README.md b/packages/spry_cookie/README.md index 8b55e73..4e3e5c8 100644 --- a/packages/spry_cookie/README.md +++ b/packages/spry_cookie/README.md @@ -1,39 +1,19 @@ - +Cookies Support for [Spry](https://spry.fun). -TODO: Put a short description of the package here that helps potential users -know whether this package might be useful for them. +## Installation -## Features - -TODO: List what your package can do. Maybe include images, gifs, or videos. - -## Getting started - -TODO: List prerequisites and provide or point to information on how to -start using the package. - -## Usage - -TODO: Include short and useful examples for package users. Add longer examples -to `/example` folder. - -```dart -const like = 'sample'; +```bash +dart pub add spry_cookie ``` -## Additional information +## Documentation -TODO: Tell users more about the package: where to find more information, how to -contribute to the package, how to file issues, what response they can expect -from the package authors, and more. +Related documentation is available in the [Cookies section of the Spry website](https://spry.fun/cookies). diff --git a/packages/spry_cookie/pubspec.yaml b/packages/spry_cookie/pubspec.yaml index c1a10ab..3eaf03e 100644 --- a/packages/spry_cookie/pubspec.yaml +++ b/packages/spry_cookie/pubspec.yaml @@ -1,6 +1,6 @@ name: spry_cookie -description: A starting point for Dart libraries or applications. -version: 1.0.0 +description: Cookies Support for Spry. +version: 0.0.0-dev.0 homepage: https://spry.fun/cookies repository: https://github.com/medz/spry diff --git a/packages/spry_cookie/test/cookies_test.dart b/packages/spry_cookie/test/cookies_test.dart index c1cfd25..823eb93 100644 --- a/packages/spry_cookie/test/cookies_test.dart +++ b/packages/spry_cookie/test/cookies_test.dart @@ -4,6 +4,8 @@ import 'package:spry_cookie/spry_cookie.dart'; import 'package:test/test.dart'; void main() async { + const plain = PlainPlatform(); + test('cookies get set correctly', () async { final app = Spry()..use(cookie()); @@ -13,7 +15,7 @@ void main() async { expect(event.cookies.get('foo'), equals('bar')); }); - final handler = app.toPlainHandler(); + final handler = plain.createHandler(app); final request = PlainRequest(method: 'get', uri: Uri(path: "/test")); final response = await handler(request); @@ -46,7 +48,7 @@ void main() async { ..delete('middleware'); }); - final handler = app.toPlainHandler(); + final handler = plain.createHandler(app); final request = PlainRequest(method: 'get', uri: Uri(path: '/test')); final response = await handler(request); final cookies = response.headers.get('set-cookie'); From 899bf6372fe59eb90ef76a6f4584c056584187a0 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Wed, 10 Jul 2024 19:35:56 +0800 Subject: [PATCH 31/35] chore: * --- .gitignore | 1 + CHANGELOG.md | 19 ++ README.md | 8 +- bun.lockb | Bin 47639 -> 50619 bytes docs/.vitepress/config.mts | 72 +++-- docs/advanced/application.md | 174 ---------- docs/advanced/handler.md | 68 ---- docs/advanced/middleware.md | 132 -------- docs/advanced/sessions.md | 23 -- docs/advanced/websockets.md | 107 ------ docs/basics/controllers.md | 76 ----- docs/basics/exceptions.md | 140 -------- docs/basics/request.md | 158 --------- docs/basics/response.md | 29 -- docs/basics/routing.md | 394 ----------------------- docs/getting-started/hello-world.md | 19 -- docs/getting-started/index.md | 31 -- docs/getting-started/installation.md | 24 -- docs/guide/getting-started.md | 75 +++++ docs/guide/what-is-spry.md | 5 + docs/index.md | 94 +++++- docs/platforms/index.md | 0 docs/public/code.png | Bin 0 -> 44703 bytes docs/public/spry.svg | 5 + examples/simple-io/.gitignore | 3 + examples/simple-io/analysis_options.yaml | 30 ++ examples/simple-io/app.dart | 23 ++ examples/simple-io/pubspec.lock | 28 ++ examples/simple-io/pubspec.yaml | 15 + package.json | 2 +- packages/spry/lib/src/http/response.dart | 14 +- 31 files changed, 337 insertions(+), 1432 deletions(-) create mode 100644 .gitignore delete mode 100644 docs/advanced/application.md delete mode 100644 docs/advanced/handler.md delete mode 100644 docs/advanced/middleware.md delete mode 100644 docs/advanced/sessions.md delete mode 100644 docs/advanced/websockets.md delete mode 100644 docs/basics/controllers.md delete mode 100644 docs/basics/exceptions.md delete mode 100644 docs/basics/request.md delete mode 100644 docs/basics/response.md delete mode 100644 docs/basics/routing.md delete mode 100644 docs/getting-started/hello-world.md delete mode 100644 docs/getting-started/index.md delete mode 100644 docs/getting-started/installation.md create mode 100644 docs/guide/getting-started.md create mode 100644 docs/guide/what-is-spry.md create mode 100644 docs/platforms/index.md create mode 100644 docs/public/code.png create mode 100644 docs/public/spry.svg create mode 100644 examples/simple-io/.gitignore create mode 100644 examples/simple-io/analysis_options.yaml create mode 100644 examples/simple-io/app.dart create mode 100644 examples/simple-io/pubspec.lock create mode 100644 examples/simple-io/pubspec.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/CHANGELOG.md b/CHANGELOG.md index c98d720..80d14e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +# Spry v4.0.0-dev.3 + +To install Spry v4.0.0-dev.3 run this command: + +```bash +dart pub add spry:^4.0.0-dev.3 +``` + +Or update your `pubspec.yaml` file: + +```yaml +dependencies: + spry: ^4.0.0-dev.3 +``` + +## What's Changed + +* fixed text response length calculation error. + # Spry v4.0.0-dev.2 To install Spry v4.0.0-dev.2 run this command: diff --git a/README.md b/README.md index 2c5d11e..4908178 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,14 @@ Spry is a lightweight, composable Dart web framework designed to work collaborat ```dart import 'package:spry/spry.dart'; -main() async { +main() { final app = Spry(); - app.get("hello", (request) => "Hello, Spry!"); + app.get("hello", (event) => "Hello, Spry!"); } ``` -👉 [**Learn more about Spry at Spry Documentation.**](https://spry.fun/) +👉 [**Learn more about Spry at documentation website.**](https://spry.fun/guide/getting-started) ## Sponsors @@ -36,4 +36,4 @@ We welcome contributions! Please read our [contributing guide](CONTRIBUTING.md) Thank you to all the people who already contributed to Odroe! -[![Contributors](https://opencollective.com/openodroe/contributors.svg?width=890)](https://github.com/odroe/prisma-dart/graphs/contributors) +[![Contributors](https://contrib.rocks/image?repo=medz/spry)](https://github.com/odroe/prisma-dart/graphs/contributors) diff --git a/bun.lockb b/bun.lockb index 86964ca658d91ba6da2eafb08470fcfa816e9089..9558b1b4691eb629687d4867571944893ce761b8 100755 GIT binary patch delta 16900 zcmd^md0foh_xQ{-?M*vP`>I8n_6;SKN{dQSiPEMf?Td<-At6M$6~#kEls05v%U;${ z_ML|aStHW#-1p48@_ans=kxpgzMsE-_ciBz?m6e)bI;xH`#txp|3o{oN}zyc@WE&G zn+yBRWWD8Y;nb>f7RB{nDJKgXMg$UV&0qQh~1*k<>o+C=3aFY^K z(;`w+DM3^Ug#olTU@^c6VF`)sND5_MdK8gw6^%lX27Vu4DZp5rIN&azWdMVYw5+5k zN(RvKKzq@7{AqmoE1+e8?<>eFCn_P$n4QLsN_FNl=I|4e29bs&3Q;H$kkLdKji#z` zrh##)K-vOU0j$EO1pq4o{TwQ#2Dk;V4&VyF8h~^7{MmrDfwl*%1y~)hI$#PPKM~>O zKLr@`cL2uatOl%1p>Vhyh~S2bN)1nE$3;>WL9G;kjsZLp@Kin?&&RraECYB1$orwS za07MmaR(o_@NqpKm-2BgA14!-L*a%%L=F^q@UaaakK$uFz)%*K%E#}Z+c15fk1z0X zY7BcW8+tJT09R-{G#~EKgMe{^_W>Uol^Y+HGM5#bO6kEg2N-E#;o)K7aZ&exfosBo zc98@OL&YdEDxQ56_*lUyXg4fx0t{tx_W~bm;#NcdVXOuHgyku7A|pUVQ3M)#f%_UT zHi(^Slo1n_Mv(*>8}<|O*z!Jzy zK$&9=U5+)3fgylL@mHX6QQD(;ro4qAgGW&zVBDEqkPdg6Fv#&JxC(r*hI0S8vC*)%S{|sPYac9BtrZ4^@c$IzV%&^OC-JT+g4Li1`w9nl$Gd0ic=iVxv>`-rY9j%j{tCFq0 zcjx^1alc5zm{aRyN)A`)K8&HdEA0yW`MKFv-EidD467eei&nYbv;C4LA)bW_kqupg zd6F~bapJ=xA>uJ#*%mvmR!TX!+?BF>V}r`+Ow>$2$Z4;TRz0oXa-g&D(8O97 z_NUPl_n0l%##AnDFHY z`_TAm3!(TwA3QkmN3Gd0Th(K6Zw>vOb6!=ITh-mQSD5L;?lWnJ0#SjG26N`2 z)v4E+!mr$rdp1J!$gIiONkPa2yU|P_aR3s*0Pn$zUZKC!f)vyYWhi*sB!Y0T; zT$D;hD<$k@bZHcdF|Y+-EYJbeVHww3Cn6$K1p?>a2}B{O<6i}+SxrzkMf=Eovo>nf1+{Nr=-oOrl z8$%ds43ml?OxD@}wuXNx5j9IlFsvattkV=IfTD+!Bd)Zl2qedPsK{5vn)(*CN=%@6 z2qQ6uy=V>SHUW_!Dr8vG1VqqEhCR()1hp~j1yf)V3`Pu@@pOm;p-YT$g1WFA1rxEw z5c4BqHy}2Ph?&CT$0lNB5DOz>-yz162Euw1jTlnn=@5w`ggUSSgc7lw!Ppas@iI(+ zg^DNL3^ASoy$}n)IcQU)k(jJKqXZx%3d&;C+K)$f>Ze#f+gHTT!-VxkiN!h^!k z{Eo?hmHXG+kaF6;FvUeBfQQG=FIGPN9n%2T>VHX@_&er4U^@R*=eOT6?O-hbk`gt` ziAwyHiJSP$@0g;njhI70uu@ROP&vFNB`G0yMSIaY*m!Ipg%D~NwPrj63imGzeu_9uC8C^s*z)avVU84) zh7A%c#;pm1mm)$;Qx#DO;A5MHE1=#)qRJB(ny{ne)oD@hqguB{X>@l?i5KnH*&R3ABL6MSq2tdtZCm=BFB`W4a>DrspDHM!xfw0dI0b|%0D6Tw`z_=noeEQ$Vyz>8pjJ%Tmr-ER0q9pMR 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 Im@D>w08NHbGXMYp delta 15392 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 ap!vrcXxuR^^z^tcH=vq_N1s?H{{H|chGW|R diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 356c5d0..c6bfaca 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -1,52 +1,58 @@ -import { defineConfig } from 'vitepress'; +import { defineConfig } from "vitepress"; -// https://vitepress.dev/reference/site-config export default defineConfig({ - title: 'Spry', - titleTemplate: 'Spry: :title', + title: "Spry", + titleTemplate: "Spry: :title", description: - 'An HTTP middleware framework for Dart to make web applications and APIs server more enjoyable to write.', + "Spry is a lightweight, composable Dart web framework designed to work collaboratively with various runtime platforms.", + head: [["link", { rel: "icon", type: "image/svg+xml", href: "/spry.svg" }]], + sitemap: { + hostname: "https://spry.fun", + }, + cleanUrls: true, themeConfig: { + logo: { + src: "/spry.svg", + alt: "Spry", + }, editLink: { - pattern: ':repo/edit/:branch/:path', + pattern: "https://github.com/medz/spry/edit/main/docs/:path", }, - sidebar: [ + nav: [ { - text: 'Getting Started', - items: [ - { text: 'Introduction', link: '/getting-started/' }, - { text: 'Installation', link: '/getting-started/installation' }, - { text: 'Hello World', link: '/getting-started/hello-world' }, - ], + text: "Guide", + link: "/guide/what-is-spry", + activeMatch: "^/guide/.*?", }, { - text: 'Basics', - items: [ - { text: 'Routing', link: '/basics/routing' }, - { text: 'Controllers', link: '/basics/controllers' }, - { text: 'Request', link: '/basics/request' }, - { text: 'Response', link: '/basics/response' }, - { text: 'Errors & Exceptions', link: '/basics/exceptions' }, - ], + text: "Platforms", + link: "/platforms/", + activeMatch: "^/platforms/.*?", }, { - text: 'Advanced', - items: [ - { text: 'Handler', link: '/advanced/handler' }, - { text: 'Middleware', link: '/advanced/middleware' }, - { text: 'Sessions', link: '/advanced/sessions' }, - { text: 'Application', link: '/advanced/application' }, - { text: 'WebSockets', link: '/advanced/websockets' }, - ], + text: "Examples", + link: "https://github.com/medz/spry/tree/main/examples", }, ], - + sidebar: { + "/guide": [ + { text: "What is Spry?", link: "/guide/what-is-spry" }, + { + text: "Getting Started", + link: "/guide/getting-started", + }, + ], + }, socialLinks: [ - { icon: 'github', link: 'https://github.com/medz/spry' }, + { icon: "github", link: "https://github.com/medz/spry" }, { - icon: 'twitter', - link: 'https://twitter.com/shiweidu', + icon: "twitter", + link: "https://twitter.com/shiweidu", }, ], + footer: { + message: "Released under the MIT License.", + copyright: `Copyright © ${new Date().getFullYear()} Seven Du`, + }, }, }); diff --git a/docs/advanced/application.md b/docs/advanced/application.md deleted file mode 100644 index f31c863..0000000 --- a/docs/advanced/application.md +++ /dev/null @@ -1,174 +0,0 @@ ---- -title: Advanced → Application ---- - -# Application - -Application is the cornerstone of Spry, it carries all the magic of Spry! - -## Create With HTTP Server (Recommended) - -The recommended way to create a Spry Application is to pass it any concrete implementation of `HttpServer` in `dart:io`. - -```dart -import 'dart:io'; - -main() async { - final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 3000); - final app = Application(server); -} -``` - -::: tip - -If you want to use `bindSecure` to create an HTTPS server, you must create the Application this way. - -::: - -## Simple Create - -You can also create a simple `HttpServer` server through `Application.create` and bind it to the Spry Application. - -```dart -import 'package:spry/spry.dart'; - -main() async { - final app = await Application.create(port: 3000); -} -``` - -`port` is required and is used to specify the port that the server listens on. It also has some optional parameters: - -| Argument | Type | Default | Description | -| --------- | --------- | ------------------------------ | -------------------------- | -| `address` | `dynamic` | `InternetAddress.loopbackIPv4` | HTTP Server listen address | -| `port` | `int` | - | HTTP Server listen port | -| `backlog` | `int` | `0` | HTTP Server backlog | -| `shared` | `bool` | `false` | HTTP Server shared | -| `v6Only` | `bool` | `false` | HTTP Server v6Only | - -## Late Initialization - -You can also create an Application that lazily initializes the HTTP Server through `Application.late`. This method allows you to create the Application in a separate file and lazily create the HTTP Server when your application is running. - -```dart -import 'package:spry/spry.dart'; - -final app = Application.late(); -``` - -::: danger - -Applications that delay the creation of HTTP Server cannot call the `app.listen` method to listen for requests. You must use the `app.run` method to start the Application. - -```dart -final app = Application.late(); - -main() async { - app.get("hello", (request) => "Hello, Spry!"); - - await app.run(port: 3000); // [!code focus] -} -``` - -::: - -## Create with HTTP Server Factory - -In addition to using `Application.late` to create Application statically, you can also use HTTP Server factory to create Application through `Application.factory`. - -```dart -import 'dart:io'; - -Future serverFactory(Application app) async { - ... -} - -final app = Application.factory(serverFactory); -``` - -The factory's type signature is `FutureOr Function(Application)`, which receives an Application instance and returns a `Future` or `HttpServer` instance. - -This approach is particularly useful if you are mixing multiple frameworks or if you need to defer creating an HTTP server based on configuration. - -::: warning - -The Application instance accepted by the HTTP Server factory does not yet contain `server` information. You cannot get it because it is a property of the `late final` signature, and you cannot set it. To properly set the Application's `server` property, you can only return an `HttpServer` instance from the factory. - -::: - -## Listen requests - -You can enable request monitoring through `app.listen`. - -::: warning - -Note that if you use `Application.late` to create the Application, you cannot use `app.listen` to listen for requests. You must use the `app.run` method to start the Application. - -::: - -## Run Application - -It is a unique method that only serves the Application created by `Application.late`. It will create an HTTP Server and listen for requests while your application is running. - -```dart -final app = Application.late(); - -main() async { - app.get("hello", (request) => "Hello, Spry!"); - - await app.run(port: 3000); // [!code focus] -} -``` - -## Handle Requests - -When you need to customize sending requests instead of Spry listening for requests autonomously, you can obtain Spry's request processing entry through `app.handler`. - -It is an instance that implements the `Handler` interface and is usually useful when testing or when you handle HTTP Server listen yourself. - -```dart -main () async { - final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 3000); - final app = Application(server); - - app.get("hello", (request) => "Hello, Spry!"); - - await (final request in server) { - if (request.method == 'post') { - // You can customize your request here - } - - // Or otherwise, you can use Spry handler - await app.handler.handle(request); - } -} -``` - -## Configuration - -The Spry application works out of the box and does not require much configuration. Because it has really few configurations, but there are some custom configurations that can make your application more flexible. - -## Encoding - -You can set the encoding of the request body through `app.encoding`. The default is `utf-8`. - -```dart -app.encoding = utf8; -``` - -## `x-powered-by` - -You can set the `x-powered-by` header through `app.poweredBy`. The default is `Spry/`. - -```dart -app.poweredBy = "Spry"; -``` - -::: tip - -If you need to remove the `x-powered-by` header, you can set it to `null`. - -> `Application.late` and `Application.factory` This operation has no effect. You can use `server.defaultResponseHeaders` to remove `x-powered-by` headers. - -::: diff --git a/docs/advanced/handler.md b/docs/advanced/handler.md deleted file mode 100644 index 46cfaaf..0000000 --- a/docs/advanced/handler.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: Advanced → Handler ---- - -# Handler - -`Handler` is the core concept in Spry, but it is not unique to Spry. Because `HttpServer` in `dart:io` also handles requests through `FutureOr Function(HttpRequest)`. - -The Handler in Spry only encapsulates its specific structure. In most cases, you do not need to implement the Handler interface or use it unless you deeply customize Spry. - -## Handler interface - -The Handler interface has a `T` type parameter and a `handle` method. `T` represents the return value type of the `handle` method. - -```dart -class MyHandler implements Handler { - @override - T handle(HttpRequest request) { - return 'Hello, World!'; - } -} -``` - -Or use an `async` method: - -```dart -class MyHandler implements Handler { - @override - Future handle(HttpRequest request) async { - return 'Hello, World!'; - } -} -``` - -The type of `handle` is `FutureOr Function(HttpRequest)`, which is consistent with the requirements in `dart:io`. - -## Closure Handler - -Closure Handler is the most common handler when registering routes, and it is also the recommended method. Usually, you can directly pass in a Closure that conforms to the `FutureOr Function(HttpRequest)` type when registering a route. - -```dart -router.get('/hello', (request) async { - return 'Hello, World!'; -}); -``` - -You can use a `ClosureHandler` to wrap a Closure when you definitely think a `Handler` is needed somewhere else. - -```dart -final handler = ClosureHandler((request) async { - return 'Hello, World!'; -}); - -app.get('/hello', handler.handle); -``` - -Although this may seem redundant, it makes it easier to use Handler when needed. For example, if you expect to register a route via `app.routes.addRoute`, then you need a `Handler`. - -```dart -final handler = ClosureHandler((request) async { - return 'Hello, World!'; -}); -final route = Route(method: "GET", segments: '/hello'.asSegements, handler: handler); - -app.routes.addRoute(route); -``` - -These functions are reserved for users who perform in-depth customization based on Spry. Generally, you do not need to use them. diff --git a/docs/advanced/middleware.md b/docs/advanced/middleware.md deleted file mode 100644 index 93505e4..0000000 --- a/docs/advanced/middleware.md +++ /dev/null @@ -1,132 +0,0 @@ ---- -title: Advanced → Middleware ---- - -# Middleware - -Middleware is a logical chain between request/response and handler. It allows you to perform some operations before the request reaches the handler or after it leaves the handler. - -## Configiration - -You can use `app.middleware.use` to add a Middleware to all Handlers. - -```dart -app.middleware.use(MyMiddleware()); -``` - -You can also add Middleware to a routing group or a single route through `app.group`/`app.groupd`. - -```dart -app - .groupd(middleware: [MyMiddleware()]) - .get("/user", (req, res) => res.ok("Hello")); -``` - -### Ordering - -The order of adding Middleware is very important. Before entering the Handler, the order of adding Middleware will be executed. After leaving the Handler, the order of adding Middleware will be executed in the reverse order. - -```dart -app.middleware.use(MyMiddleware1()); -app.middleware.use(MyMiddleware2()); - -app.get("/user", (req, res) => res.ok("Hello")); -``` - -`GET /user` request will be executed in the following order: - -```txt -MyMiddleware1 → MyMiddleware2 → Handler → MyMiddleware2 → MyMiddleware1 -``` - -In the routing group, it is easy for us to control the order of Middleware, but the order of global Middleware is not so easy to control. In order to solve this problem, you can pass the `prepend` parameter to `app.middleware.use` to control it. - -```dart -app.middleware.use(, prepend: true); -``` - -This way, the middleware will be added at the top of the stack instead of the bottom. - -## Creating Middleware - -To create a middleware, you should implement the `Middleware` interface: - -```dart -class MyMiddleware implements Middleware { - @override - Future process(HttpRequest request, Next next) async { - // Before ... - await next(); - // After ... - } -} -``` - -The `next` function is used to notify the next Middleware in the processing chain. If there is no next Middleware, the Handler will be executed. - -::: warning - -Since the result of `next` is a `Future`, you must use `await` to wait for the execution result of `next`. - -If you don't want to wait for it, then you should use `return` to return the result of `next`. - -> If you neither wait for the execution of `next()` nor return the result of `next()`, and your `process` is operated by other logic of `response`, then it is very likely to cause Spry to exit abnormally, and Cannot be captured. - -::: - -## Before middleware - -Depending on the `Next` notification design, you can perform some operations before notifying the next Middleware. - -```dart -class MyMiddleware implements Middleware { - @override - Future process(HttpRequest request, Next next) async { - // Before logic here ... - - return next(); - } -} -``` - -Moreover, we recommend that your front-end middleware use `return next()` to return the execution result of `next`. This avoids continuing to operate the response when the response has been closed. - -## After middleware - -You can perform some operations after waiting for `next()` to complete. - -```dart -class MyMiddleware implements Middleware { - @override - Future process(HttpRequest request, Next next) async { - await next(); - - // After logic here ... - } -} -``` - -Please be sure to add `await next()` before your code, otherwise there is no guarantee that your middleware will be executed after the Handler. - -## Make handler with middleware stack - -It is convenient to set up middleware groups by grouping public routes, but sometimes we just want to add a set of middleware to a separate Handler. At this time, the grouping function seems a bit redundant, and you also need to reorganize your routing structure. - -Spry supports the ability to make handlers for data of type `Iterable`, so you can add a set of middleware to a single Handler without using grouping. - -```dart -final middleware = [MyMiddleware1(), MyMiddleware2()]; -final handler = middleware.makeHandler(baseHandler); -``` - -## Closure-style Middleware - -Sometimes, you may not want to implement the `Middleware` interface, but want to use closures directly to implement middleware. At this time, you can use `app.middleware.closure` to add a closure-style middleware. - -```dart -app.middleware.closure((request, next) async { - // Before ... - await next(); - // After ... -}); -``` diff --git a/docs/advanced/sessions.md b/docs/advanced/sessions.md deleted file mode 100644 index 90d1ad8..0000000 --- a/docs/advanced/sessions.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: Advanced → Sessions ---- - -# Sessions - -Sessions allow you to persist a user's data between multiple requests. Sessions work by creating and returning a unique cookie alongside the HTTP response when a new session is initialized. Browsers will automatically detect this cookie and include it in future requests. This allows Vapor to automatically restore a specific user's session in your request handler. - -Sessions are great for front-end web applications built in Spry that serve HTML directly to web browsers. For APIs, we recommend using stateless, token-based authentication to persist user data between requests. - -## Overview - -Spry does not encapsulate `Cookie` and `Session`, but directly uses the settings of `dart:io` - -- `request.cookies` - Get all cookies in the request -- `request.response.cookies` - Set cookies in the response (`set-cookie`) -- `request.session` - Manage Session - -::: warning - -Currently Spry does not allow you to customize the session driver, but we will support it in a future version. - -::: diff --git a/docs/advanced/websockets.md b/docs/advanced/websockets.md deleted file mode 100644 index e2fef9f..0000000 --- a/docs/advanced/websockets.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -title: Advanced → Websockets ---- - -# WebSockets - -[WebSocket](https://en.wikipedia.org/wiki/WebSocket) is a protocol that allows for persistent, full-duplex communication between a client and server. This is useful for applications that require real-time updates, such as chat rooms, news feeds, and more. - -Spry allows you to create `dart:io` based WebSocket servers to handle messages. You can use the routing API to add WebSocket endpoints to an existing Spry Application. - -```dart -import 'package:spry/spry.dart'; - -app.ws("echo", (ws, request) { - print("WebSocket connected"); - - ... -}); -``` - -WebSocket routing can be divided and registered with middleware through `app.group`, etc. just like ordinary routing. - -## Message encoding - -`WebSocket` comes from `dart:io`, the messages it sends and receives have `dynamic` type signature, but the actual situation is that Dart does not support `Union` type, so its correct message signature should be `List | String `. - -::: tip - -See [`dart:io` → `WebSocket`](https://api.dart.dev/stable/dart-io/WebSocket-class.html) for more information. - -::: - -## Listening for messages - -Because `app.ws` accepts a `FutureOr` callback, you can use `await` to wait for messages. - -```dart -app.ws("echo", (ws, request) async { - print("WebSocket connected"); - - await for (final message in ws) { - print("Received: $message"); - } -}); -``` - -Of course, you can also use `ws.listen` to listen for messages. - -```dart -app.ws("echo", (ws, request) { - print("WebSocket connected"); - - ws.listen((message) { - print("Received: $message"); - }); -}); -``` - -## Sending messages - -`ws` is duplex, so you can use `add` and `addStream` to send messages. Below is an example of returning a received message to the client. - -```dart -app.ws("echo", (ws, request) { - print("WebSocket connected"); - - ws.listen((message) { - print("Received: $message"); - - // Send the message back to the client - ws.add(message); - }); -}); -``` - -## Closing the connection - -When you are finished communicating with the client, you must close the connection using the `close` method. **Otherwise the WebSocket connection will remain open**. - -```dart -app.ws("echo", (ws, request) { - print("WebSocket connected"); - - ws.listen((message) { - print("Received: $message"); - - // Send the message back to the client - ws.add(message); - - // Close the connection - ws.close(); - }); -}); -``` - -## Learn more - -- [WebSocket class](https://api.dart.dev/stable/dart-io/WebSocket-class.html) -- [WebSocket protocol](https://tools.ietf.org/html/rfc6455) - -::: warning - -1. In WebSocket routing, Exception Filter does not work because WebSocket routing does not throw exceptions. When your WebSocket route throws an exception, it automatically closes the connection. -2. You should avoid using middleware that operates `request.response`, because `response` is closed at this time, which will cause exceptions. -3. The `request` passed to closure is only used as Context, but you cannot read the body because the body is empty when WebSocket establishes the connection. - -::: diff --git a/docs/basics/controllers.md b/docs/basics/controllers.md deleted file mode 100644 index 70c4d58..0000000 --- a/docs/basics/controllers.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -title: Basic → Controllers ---- - -# Controllers - -`Controller` are a great way to group different logic of your application, and most Controllers have the capability to accept multiple requests and respond as needed. - -Spry allows you to register routes by implementing a `RouteCollection` in your controller, so you can register routes in your controller. - -Now, let's create a `TodosController` to handle routing for our Todo application. - -```dart -class TodosController implements RouteCollection { - @override - void boot(RoutesBuilder routes) { - final todos = routes.group('/todos'); - - todos.get('/', index); - todos.post('/', create); - - todos.group(':id', (todo) { - todo.get('/', show); - todo.put('/', update); - todo.delete('/', delete); - }); - } - - /// Index todos. - Future> index(HttpRequest request) { - return Todo.all(); - } - - /// Create a todo. - Future create(HttpRequest request) async { - return Todo.create(await request.json()); - } - - /// Show a todo. - Future show(HttpRequest request) async { - final id = request.param.get('id'); - final todo = await Todo.find(id); - - if (todo == null) { - throw Abort(404, 'Todo not found.'); - } - - return todo; - } - - /// Update a todo. - Future update(HttpRequest request) async { - final id = request.param.get('id'); - final todo = await Todo.find(id); - - if (todo == null) { - throw Abort(404, 'Todo not found.'); - } - - return todo.update(await request.json()); - } - - /// Delete a todo. - Future delete(HttpRequest request) async { - final id = request.param.get('id'); - - await Todo.delete(id); - } -} -``` - -Then, we just need to register the `TodosController` into our application and it's ready to use. - -```dart -app.register(TodosController()); -``` diff --git a/docs/basics/exceptions.md b/docs/basics/exceptions.md deleted file mode 100644 index 4d17c20..0000000 --- a/docs/basics/exceptions.md +++ /dev/null @@ -1,140 +0,0 @@ ---- -title: Basics → Errors & Exceptions ---- - -# Errors & Exceptions - -Spry's error handling is based on the `Exception` interface. Route processing can throw or return objects that implement the `Exception`/`Error` interface through `throw`. Throwing or returning Dart's `Exception`/`Error` will result in a `500` status response. - -If you want Spry to correctly change the error state on your own custom errors or exceptions, you need to implement the `AbortException` interface. - -```dart -class MyException implements AbortException { - @override - int get status => 400; - - final String message; - - const MyException(this.message); -} -``` - -## Abort - -Spry provides a default exception class named `Abort`, which follows the `AbortException` and `Exception` interfaces. You can initialize it with HTTP status codes and optional exception messages. - -```dart -// 404 Exception, default message is "Not Found" -throw Abort(404); - -// Unauthorized Exception, custom message -throw Abort(401, "Invalid Credentials"); -``` - -Too cumbersome for jump times in `dart:io`. If you want to implement a jump without a `response` object, you can use the `Abort.redirect` method. - -```dart -// Redirect to /login, default status is 302, message is "Found" -Abort.redirect("/login"); - -// Redirect to /login, custom status and message -Abort.redirect("/login", message: "Please login first"); - -// Redirect to /login, custom HTTP status -Abort.redirect("/login", status: 308); -``` - -## Abort Exception - -By default, any `Exception`/`Error` will result in a `500 Internal Server Error` response. - -You can use the `AbortException` interface to implement a series of custom exceptions, which will be handled correctly. - -```dart -class UnauthorizedException implements AbortException { - @override - int get status => 401; - - final String message; - - const UnauthorizedException(this.message); -} -``` - -As in the above example, by creating the `UnauthorizedException` class and implementing the `AbortException` interface, you can correctly return the `401` status code when the exception is thrown. - -::: warning - -For the `30x` state, `AbortException` will not automatically jump because Spry relies on the `RedirectException` interface in `dart:io` to respond to the jump. If you want to implement a jump, you can use the `Abort.redirect` method or implement the `RedirectException` interface. - -::: - -## Exception Filters - -Spry comes with a built-in exception layer that handles all unhandled exceptions in the application. This layer catches exceptions when they are not handled by application code and then automatically sends an appropriate user-friendly response. - -```txt - Request → Middleware → Handler -Response ← Exception Layer ← Middleware ← Handler -``` - -You can customize the exception layer by implementing the `ExceptionFilter` interface. - -```dart -class MyExceptionFilter implements ExceptionFilter { - @override - Future process(ExceptionSource source, HttpRequest request) { - // ... - } -} -``` - -Then register `MyExceptionFilter` to the `Application` instance. - -```dart -app.exceptions.addFilter(MyExceptionFilter()); -``` - -::: tip - -The registration order of filters is **very important**, because the exception layer matches filters according to the registration order. You should avoid registering more general exception filters before exception-specific filters. - -::: - -### Exception Source - -`ExceptionSource` is the exception source received by the exception filter, which contains the detailed information of the exception. For example the `exception`, `stackTrace` and `isResponseClosed` properties. - -The `exception` property is the exception object received by the exception filter, the `stackTrace` property is the stack information of the exception, and the `isResponseClosed` property is a Boolean value indicating whether the response has been closed. - -### Catch everything - -When you just want to change the response, creating a broader exception filter is a good choice. Since textual exceptions are built into response, you can create an exception filter that catches all `Exception` and return a JSON response. - -```dart -class JsonExceptionFilter implements ExceptionFilter { - @override - Future process(ExceptionSource source, HttpRequest request) async { - final response = source.response; - - response.headers.contentType = ContentType.json; - response.write(jsonEncode({ - "message": source.exception.toString(), - })); - } -} -``` - -## Rethrow Exception - -In your Exception filter, especially a broad filter, you may want to pass exceptions to other filters for handling. You can throw the `RethrowException` class to achieve this functionality. - -```dart -class MyExceptionFilter implements ExceptionFilter { - @override - Future process(ExceptionSource source, HttpRequest request) async { - // ... - throw RethrowException(); - } -} -``` diff --git a/docs/basics/request.md b/docs/basics/request.md deleted file mode 100644 index dd5029c..0000000 --- a/docs/basics/request.md +++ /dev/null @@ -1,158 +0,0 @@ ---- -title: Basics → Request ---- - -# Request - -Spry's request object is the `HttpRequest` object from `dart:io`. We assume that you are already familiar with it. If you are not familiar with it, you can check out [dart:io → HTTP Request](https://api.dart.dev/stable/dart-io/HttpRequest-class.html) documentation. - -Next, let’s take a look at the magic Spry adds to `HttpRequest`: - -[[toc]] - -## Application - -On any request, you have access to the Spry `Application` instance. - -::: warning - -You can modify the runtime configuration to a limited extent. Of course, modifications such as `routes`, `middleware`, etc. will be invalid after your Spry application has been started. - -::: - -```dart -app.get('/config', (request) { - return { - "encoding": request.application.encoding.name, - }; -}); -``` - -## Clone - -Theoretically, `dart:io`'s `HttpRequest` is designed based on `Stream`. Once you read the stream data, you cannot read it again. - -```dart -request.listen((event) { ... }); // ✅ -request.listen((event) { ... }); // ❌ Error: Stream has already been listened to. -``` - -But we usually have to read data in some special Handler or middleware, but do not expect to affect subsequent Handlers or middleware. At this time, we can use the `clone` method to clone a new `HttpRequest ` Object. - -```dart -final request2 = request.clone(); - -request2.listen((event) { ... }); // ✅ -request.listen((event) { ... }); // ✅ -``` - -## Form Data - -Spry adds a `formData` method to `HttpRequest` for parsing data in `application/x-www-form-urlencoded` and `multipart/form-data` formats. - -```dart -final formData = await request.formData(); - -for (final (name, _) in formData.entries()) { - print("Form Data: $name"); -} -``` - -::: tip - -`FormData` is exported by `Spry`, but the implementation comes from [`package:webfetch`](https://pub.dev/packages/webfetch), which is based on [MDN FormData](https://developer.mozilla.org /en-US/docs/Web/API/FormData). - -::: - -## JSON - -Spry adds a `json` method to `HttpRequest` for parsing data in `application/json` format. - -```dart -final json = await request.json(); -``` - -::: warning - -The `json` method usually treats the incoming data as text, and then uses the `jsonDecode` method in `dart:convert` to parse the data, so if your data is not in `application/json` format, then you can use `text ` method to parse the data. - -::: - -### JSON performance - -The performance of Dart's built-in json parser is not good. You can use middleware to implement custom JSON data parsing. - -You can configure `#spry.json.codec` to tell Spry what `JsonCodec` should be used to parse JSON data. - -```dart -app.locals[#spry.json.codec] = convert.json; // This is default. -``` - -::: tip - -`spry.json.codec` also works with `request.locals`, but is useful with specifying `JsonCodec` for a single use. - -::: - -::: warning - -Your custom JSON parser must implement the `JsonCodec` interface. - -For more information about `JsonCodec`, check out the [dart:convert → JsonCodec](https://api.dart.dev/stable/dart-convert/JsonCodec-class.html) documentation. - -::: - -## Text - -Spry adds `text` to `HttpRequest` to help you read the body as text. - -```dart -final text = await request.text(); -``` - -## URL Search Params - -In addition to accessing URL information through `request.url`, Spry adds a `searchParams` attribute for parsing query parameters in the URL. - -```dart -print(request.searchParams.get('name')); -``` - -::: tip - -`searchParams` returns a `URLSearchParams` object, which is exported by `Spry`, but the implementation comes from [`package:webfetch`](https://pub.dev/packages/webfetch), which is based on [MDN URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams). - -::: - -## Route - -You can access the routing information of the current request through `request.route`. - -```dart -print(request.route.path); -``` - -::: tip - -For routing parameters, you can check out the [Basics → Routing](/basics/routing.md#route-parameters) documentation. - -::: - -## `fetch` - -Used to retrieve data from the network. - -> It also supports you to ignore HTTP schema and host, and directly use path to obtain data from other handlers - -```dart -// Remote with fetch -app.get('github/:name', (request) { - return request.fetch('https://api.github.com/users/${request.param('name')}'); -}); - -// Fetch profile -app.get('users/:id', (request) => ...); -app.get('profile/:id', (request) { - return request.fetch('/users/${request.param('id')}'); -}); -``` diff --git a/docs/basics/response.md b/docs/basics/response.md deleted file mode 100644 index 36b2bb0..0000000 --- a/docs/basics/response.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: Basics → Response ---- - -# Response - -Spry's response object is the `HttpResponse` object from `dart:io`. We assume that you are already familiar with it. If you are not familiar with it, you can check out [dart:io → HTTP Response](https://api.dart.dev/stable/dart-io/HttpResponse-class.html) documentation. - -Throughout the request cycle, Response has no special magic, because in the design of `dart:io`, `HttpResponse` is based on `IOSink`, which is good enough to use! - -But `IOSonk` has a defective design, that is, we don’t know when `IOSink` has been closed. So we added an `isClosed` attribute to `HttpResponse` to determine whether `IOSink` has been closed. - -```dart -if (!response.isClosed) { - response.write("Hello World!"); -} -``` - -It is very useful for post-post middleware. We can determine whether it has been closed to avoid exceptions caused by writing input when it is closed. - -::: danger - -Please note that in the response design in `dart:io`, the headers operation must be performed before writing the body data, otherwise an exception will be thrown. - -Once you start writing body data, the headers will be locked and you will not be able to modify the headers again. - -> We recommend that you use `Responsible` to return data, which will automatically handle these problems for you. - -::: diff --git a/docs/basics/routing.md b/docs/basics/routing.md deleted file mode 100644 index 1eda296..0000000 --- a/docs/basics/routing.md +++ /dev/null @@ -1,394 +0,0 @@ ---- -title: Basics → Routing ---- - -# Routing - -Routing is the process of finding an appropriate request handler for an incoming request. The routing core of Spry is a high-performance Trie-node router based on [RoutingKit](https://pub.dev/packages/routingkit). - -[[toc]] - -## Overview - -To understand how routing works in Spry, first you should understand the basics about HTTP requests. Take a look at the sample request below: - -```http -GET /hello/spry HTTP/1.1 -host: spry.fun -content-length: 0 -``` - -This is a simple `GET` HTTP request to `/hello/spry`. If you enter the URL below into your browser's address bar, your browser will send this request. - -```http -http://spry.fun/hello/spry -``` - -### HTTP Method - -The first part of the request is the HTTP method. `GET` is the most common HTTP method, here are some other common HTTP methods: - -- `GET`: Get a resource -- `POST`: Create a resource -- `PUT`: Update a resource -- `DELETE`: Delete a resource -- `PATCH`: Update some properties of a resource - -### Request Path - -After the HTTP method is the request's URI. It consists of a path starting with `/` and an optional query string after `?`. The HTTP method and path are what Spry uses to route requests. - -### Router Methods - -Let's see how this request is handled in Spry: - -```dart -app.get('/hello/spry', (request) { - return 'Hello, Spry!'; -}); -``` - -All common HTTP methods can be used as methods of `Application`. They accept a string representing the path, separated by `/`. -Note that `Application` and `RoutesBuilder` do not build in all HTTP methods, you can use `on` to write manually: - -```dart -app.on( - method: "get", path: "/hello/spry", - (request) => "Hello, Spry!", -); -``` - -After registering this route, the sample HTTP request above will get the following HTTP response: - -```http -HTTP/1.1 200 OK -content-length: 12 -content-type: text/plain; charset=utf-8 - -Hello, Spry! -``` - -### Route Parameters - -Now that we've successfully routed requests based on HTTP method and path, let me try to make the path dynamic. - -::: warning - -The `spry` name is hardcoded in both the path and the response. Let's make it dynamic so that you can access `/hello/` and get a response. - -::: - -```dart -app.get('/hello/:name', (request) { - final name = request.params.get("name"); - return 'Hello, $name!'; -}); -``` - -By using a path segment prefixed with `:` we indicate to the route that this is a dynamic path parameter. Now, any string provided here will match this route. We can then access the value of the string using `request.params`. - -If we run the sample request again, you will still get a response greeting `spry`. But now you can add any name after `/hello/` and see it in the response. Let's try `/hello/dart`. - -::: code-group - -```http [request] -GET /hello/dart HTTP/1.1 -content-length: 0 -``` - -```http [response] -HTTP/1.1 200 OK -content-length: 12 - -Hello, dart! -``` - -::: - -Now that you know the basics, check out the other sections to learn more. - -## Routes - -### Methods - -You can use a variety of HTTP method helpers to register routes directly to your Spry `Application`: - -```dart -app.get("/foo/bar", (request) { - // ... -}); -``` - -Route handlers support you to return any `Responsible` content, including `String`, `Map`, `List`, `File`, `Stream`, etc. - -You can also specify the type of the return value of the route handler through the `T` type parameter: - -```dart -app.get("/foo", (request) { - return "bar"; -}); -``` - -This is a list of built-in HTTP methods: - -- `get` -- `post` -- `put` -- `patch` -- `delete` -- `head` - -Procesing HTTP method helpers, there is also an `on` function that accepts the HTTP method as an input parameter: - -```dart -app.on( - method: "get", - path: "/foo/bar", - (request) => { ... }, -); -``` - -### Path Segments - -Each route registration method accepts a string representation of `Segment`, and has the following four situations: - -- Constant (`foo`) -- Parameter (`:foo`) -- Anything (`*`) -- Catchall (`**`) - -#### Constant Segment - -This is a static Segment, only requests with an exact match string at this location are allowed. - -```dart -app.get("/foo/bar", (request) { - // ... -}); -``` - -#### Parameter Segment - -This is a parameter Segment, any string at this location will be allowed. Parameter Segment is specified with a `:` prefix, the string after `:` will be used as the parameter name. - -```dart -app.get("/foo/:bar", (request) { - // ... -}); -``` - -#### Anything Segment - -This is the same as the Parameter Segment, except that the parameter value is discarded. It is specified with a `*` prefix. - -```dart -app.get("/foo/*/baz", (request) { - // ... -}); -``` - -#### Catchall Segement - -This is a dynamic route component that matches one or more Segments, specified with `**`. Any string in the request will be allowed to match this location or after this location. - -```dart -// GET /foo/bar -// GET /foo/bar/baz -// ... -app.get("/foo/**", (request) { - // ... -}); -``` - -### Parameters - -When using parameter Segment (prefixed with `:`), the URI value for that location will be stored in `request.params`. You can access the value using the name in Path Sgements: - -```dart -app.get("/foo/:bar", (request) { - final bar = request.params.get("bar"); - // ... -}); -``` - -:: tip - -We can be sure that `request.params.get` will never return `null` here because our path contains `:bar`. But if the parameter is processed in advance by middleware or other programs, we need to consider the `null` situation. - -::: - -Values matched via Catchall (`**`) or Anything (`*`) Segment will be stored in `request.params` as `Iterable`. You can access them using `request.params.catchall`: - -```dart -app.get("/foo/**", (request) { - final catchall = request.params.catchall; - // ... -}); -``` - -::: tip - -If your path contains multiple Parameter Segments, such as `/foo/:bar/:bar`, you use `request.params.get('bar')` only returns the first value, you can use `request.params.getAll('bar')` to get all values. - -::: - -### Body - -Spry not create a new HTTP request, in `dart:io`, `HttpRequest` itself is a `Stream`. You can read the stream data of the request directly. - -```dart -app.post("/foo", (request) async { - await (final chunk in request) { - request.response.write(chunk); - } -}); -``` - -You can also send a `Stream` as the body of the response when the Handler returns `Stream`: - -```dart -app.post("/foo", (request) { - return File("foo.txt").openRead(); -}); -``` - -Of course, you can return any data without calling `write` or other methods of `request.response`. It supports `String`, `Map`, `List`, `File`, `Stream>`, etc. Of course, you can return an instance that implements `Responsible`. - -### Case Insensitive Routing - -By default, Spry's routes are case-insensitive. If you want to maintain case-sensitivity, please configure it before calling `app.listen()`: - -```dart -app.routes.caseSensitive = true; -``` - -### View All Routes - -Do you want to view all registered routes? You can use the `app.routes` property to view all registered routes: - -```dart -for (final route in app.routes) { - print('${route.method} ${route.segments}'); -} -``` - -## Route Groups - -Route grouping allows you to create a group of routes with specific route prefixes or specific middleware. The grouping function supports both builder and closure syntax. - -All grouping methods return a `RoutesBuilder` instance, which means you can infinitely mix, match, and nest groups with other route building methods. - -::: tip - -Route groups can help you better organize your routes, but they are not required. - -::: - -### Path Prefix - -Path prefix routing groups allow you to add a prefix path before a routing group. - -```dart -final users = app.groupd(path: "/users"); - -// GET /users -users.get("/", (request) => ...); - -// POST /users -users.post("/", (request) => ...); - -// GET /users/:id -users.get("/:id", (request) => ...); -``` - -Any path component you can pass to helper methods such as `get`, `post` can be passed to `groupd`. -There is another syntax based on closures: - -```dart -app.group(path: "/users", (routes) { - // GET /users - routes.get("/", (request) => ...); - - // POST /users - routes.post("/", (request) => ...); - - // GET /users/:id - routes.get(":id", (request) => ...); -}); -``` - -Nested path prefixes allow you to define your CRUD API more concisely: - -```dart -app.group(path: "/users", (users) { - // GET /users - users.get('/', (request) => ...); - // POST /users - users.post('/', (request) => ...); - - users.group(path: ":id", (user) { - // GET /users/:id - user.get('/', (request) => ...); - // PUT /users/:id - user.put('/', (request) => ...); - // DELETE /users/:id - user.delete('/', (request) => ...); - }); -}); -``` - -### Middleware - -In addition to path prefixes, routing groups also allow you to add middleware to routing groups. - -```dart -app.get('fast-thing', (request) => ...); -app.group(middleware: [SlowMiddleware()], (routes) { - routes.get('slow-thing', (request) => ...); -}); -``` - -This is particularly useful for protecting a subset of routes with different authentication middleware. - -```dart -app.post('/login', (request) => ...); - -final auth = app.groupd(middleware: [AuthMiddleware()]); -auth.get('/profile', (request) => ...); -auth.get('/logout', (request) => ...); -``` - -## Redirects - -Redirects are particularly useful in many scenarios. There is a `redirect` method defined in `HttpResponse` of `dart:io`, which you can use to redirect to another URL. - -```dart -app.get('/foo', (request) { - request.response.redirect(Url.parse("/bar")); -}); -``` - -But it’s not practical enough, so `Spry` also allows you to throw `RedirectException` in `dart:io` to implement redirection: - -```dart -app.get('/foo', (request) { - throw RedirectException("", [ - RedirectInfo(...), - ... - ]); -}); -``` - -::: warning - -`RedirectInfo` is an abstract class in `dart:io`, you need to implement it yourself. - -::: - -Of course, since Spry has the magic, you can use `Abort.redirect` to simplify redirection: - -```dart -app.get('/foo', (request) { - throw Abort.redirect('/bar'); -}); -``` diff --git a/docs/getting-started/hello-world.md b/docs/getting-started/hello-world.md deleted file mode 100644 index 8bb17a3..0000000 --- a/docs/getting-started/hello-world.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: Getting Started → Hello world ---- - -# Hello world - -```dart -import 'package:spry/spry.dart'; - -final app = Application.late(); - -main() async { - app.get("hello", (request) => "Hello, Spry!"); - - await app.run(port: 3000); -} -``` - -The application creates a server and listens for requests on port `3000`, responding to the `/` path for `GET` requests. For all other requests, it will return a `404` response. diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md deleted file mode 100644 index 6c67ff7..0000000 --- a/docs/getting-started/index.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: Getting Started → Introduction ---- - -# Introduction - -Welcome to the Spry documentation! Spry is a web framework for Dart that allows you to write backends, web application APIs, and HTTP servers in Dart. Spry is written in Dart, a modern, powerful, and secure language that offers many advantages over more traditional server languages. - -If you encounter any problems during use, please submit them in [GitHub Discussions](https://github.com/medz/spry/discussions). - -[[toc]] - -## Why Spry? - -Spry has **magic**, lightweight APIs design, using the classes you are familiar with in `dart:io`, you have no learning burden! - -## Performance - -In various frameworks, they always use strange ways to match routes, and one of the common ways is regular expressions. In Spry, thanks to the Trie tree route matching of [RoutingKit](https://pub.dev/packages/routingkit), the performance has been greatly improved. - -## Hard to catch errors - -When the application is running, if an abnormal error occurs. Usually you have to set up a `try/catch` to capture, and then you handle it in the corresponding `catch`. There is no way to do it more elegantly. - -In Spry, the **Exception filters** mechanism is designed. You only need to inject the exception filters you need to catch exception errors during application running, and then you can handle them in a more elegant way. - -## Elegant Magic - -When you decide to use a web framework, you need to learn a variety of new APIs and concepts. You need to follow all kinds of unique rules, even request/response is dialect! - -In Spry, you don’t need to learn any new APIs, you just need to use the classes in dart:io that you are familiar with, and you can start your development journey. At the same time, Spry injects a lot of magic into request/response in `dart:io` to make your development easier. diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md deleted file mode 100644 index c338b22..0000000 --- a/docs/getting-started/installation.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: Getting Started → Installation ---- - -# Installation - -## Prerequisites - -Dart SDK `^3.2.0` or higher. - -## Install Spry - -Add the following to your `pubspec.yaml` file: - -```yaml -dependencies: - spry: latest -``` - -Or, can install with `pub` command: - -```bash -dart pub add spry -``` diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md new file mode 100644 index 0000000..11ffe4f --- /dev/null +++ b/docs/guide/getting-started.md @@ -0,0 +1,75 @@ +--- +title: Guide → Getting Started +--- + +# Getting Started + +## Installation + +Install Spry run this command: + +```bash +dart pub add spry +``` + +Or, update your `pubspec.yaml` file: + +```yaml +dependencies: + spry: +``` + +## Quick Start + +Creates a new file `app.dart`(or `main.dart` | `server.dart`): + +::: code-group + +<<< ../../examples/simple-io/app.dart + +::: + +Now run the development server using `dart run`: + +```bash +dart run app.dart +``` + +And tadaa! We have a web server running locally. + +## What happened? + +Okay, let's now break down our example: + +We first created an [Spry application][/guide/app] using `Spry()`. +`app` is a tiny server capable of matching requests, generating response and handling lifecycle hooks (such as errors): + +```dart +final app = Spry(); +``` + +Then we adds our first endpoint. in Spry, we define request handlers using a closure preceded by a `FutureOr Function(Event)` type: + +```dart +app.get('/', (event) => ...); +``` + +What is beautiful in Spry is that all you have to do to make a response, is to simply return it! Responses can be simple string, JSON objects, Uint8List or streams. + +```dart +return '⚡️ Tadaa!'; +``` + +We then use Spry’s built-in `dart:io` platform support to wrap the app instance into a handler that `HttpServer` can use: + +```dart +final handler = const IOPlatform().createHandler(app); +``` + +Finally, we create an HTTP server from `dart:io` and listen for requests to pass to the Spry app: + +```dart +final server = await HttpServer.bind('127.0.0.1', 3000); + +server.listen(handler); +``` diff --git a/docs/guide/what-is-spry.md b/docs/guide/what-is-spry.md new file mode 100644 index 0000000..7c9136c --- /dev/null +++ b/docs/guide/what-is-spry.md @@ -0,0 +1,5 @@ +--- +title: Guide → What is Spry? +--- + + diff --git a/docs/index.md b/docs/index.md index bc25290..98e9a27 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,25 +1,87 @@ --- -# https://vitepress.dev/reference/default-theme-home-page layout: home -title: Spry framework -titleTemplate: false +title: A lightweight and composable web framework. hero: - name: Spry framework - text: A middleware-style framework for Dart - tagline: Make web applications and APIs more enjoyable to write. + name: ⚡️ Spry + text: A lightweight and composable web framework + tagline: Performance · Powerful · Joyful + image: /code.png actions: - - theme: brand - text: ⚡️ Getting Started - link: /getting-started/ + - text: What is Spry? + link: /guide/what-is-spry + - theme: alt + text: Getting Started + link: /guide/getting-started - theme: alt text: View on GitHub link: https://github.com/medz/spry - features: - - title: Performance - details: Spry is built on top of Dart's `dart:io` library, which is highly optimized for performance, Routing with Trie tree - - title: Dart Primitive - details: Get rid of boring creation, just like you are familiar with dart:io, no need to learn new APIs - - title: Magic - details: Request/Respons have not changed, but they have a lot of magic to make your development easier. + - icon: 🛸 + title: Runtime Agnostic + details: Your code is implemented through the platform and can be compiled into any runtime for your application. + - icon: ✨ + title: Small & Tree-shakable + details: Spry core is supper lightweight & tree-shakable, Only the extensions you use will be included in the final bundle. + - icon: 🧩 + title: Composable + details: Extend your application and add capabilities, Your codebase will scale with your project. + - icon: 🌲 + title: Fast Router + details: Default to use high-performance Radix-Trie router from RoutingKit
View RoutingKit →. + - icon: 🤖 + title: Made for Humans + details: Elegant minimal API implementation and editing interface abstraction. + - icon: 🎉 + title: Responsible + details: Your handlers can intuitively return content without building complex Response objects. --- + + + + + + + + + + + + + + + + + diff --git a/docs/platforms/index.md b/docs/platforms/index.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/public/code.png b/docs/public/code.png new file mode 100644 index 0000000000000000000000000000000000000000..264e50c00627752b5213eb7bdfab87d117978f7a GIT binary patch literal 44703 zcmeFZWl&t*)&`gm+-Y2bySsZx@ZjFK1t+*eaCg^W!QI^Ja(Q;z;m#@NeF{L6VdZQ+)I09q`SYx5=>3;1Sh# zzvRHb-r6gQi@qrzBiMiQhWL%7n255A&QXS=hq5?!;CTOPu!yZQ!^eZ`*?>e*ZL=8o z6DW8NsW1zge0gkmM<&NFU!4(=S+UrpRs;jk0>lGEF{o=H-4Y|&b-iiC}xrHmZc%ch5xw5(3qdCnDN%Q@DSEz zrKE-?hN!WC3JMDPf?i(e;h%7~R^GW_!$rUO<5ISPfcBH2`#GGJoSvQz4Un>>8Bc7u z5eqFl5a3>L``a4N(S3fiVqtyuDUgOMUs$+({^U18=RHaP4N}7A&#O}}{%wI=R=SO_ zZ@6ggnZU{7*7rZ2_x0J;z$-qsV&>Nijtq0}CJqcJr%l}X+XBJ=2IKVI=8)&DMUu&{Cd?+crduJI8O+6*6q`s(qCd~96&YNn_aLULp@jzMEF z)|6fK3n*L4>cCa{Nu4PKiL0`dh4XR_)w*16UH({KBPt~SXIpIy0&>r-+;(u|rOnl)Jz({R}|CdbQu8k-K`=MU{BE$<^~YrKO)AMm5xr=j?R!sl!Z;~nW3xtO2r zqBOUR(&6rft&Cq_&h-DP%h^z$0oZaJT_V>e9ba~Y%cP$Zs3CB0SWHZD(N(oMq`BnA zMp9C4fk$Iw2`nK4xIND>YJwYtWsWfiQ5^&fL}49H0tS${WCe{9H)za zYW7ufqPATn9CaeTkR<-hwB6ZGRFqk9rKOg*Zl}r1t=gyNZO>nPAYtw$o>HHYXF(Go zz`+s5XnmW##?j^cR!z?jeO%PS5P)uiV%@PS_QCO?7+ zoWE}3Dn~k`%v6+unHT8c`ySm-lMkNNp5j5K7-QteZ@XF>SAjpa*^x3jS@hB7Zy?GP&a{P}qLqyX$<=I*mi zfcmg&m5Qjtd$|N($s$CiZD;5|rf&=u4uCJ`!k>@3R@g)iLesS1>qg+BC>TP20BHN} zMZEnsl^v7SqwhaQ7QQmo%%mU8$~=g@ulf@| z3r`?vWo${}Z8dwArua$L@;xgL7bJ1Fd?!Tx0iQkTg*7>0oy?w`2u9+w!-k18gXKln zz4QT7-2M4i!wX5|cL-CBS;P6t8aRJhw6G(EyuPo6NN%&8B(SzOAfPe+si+q5?qLRZ zzv0EUX((fzNuJSchv|&taY!X{XOwDucW`~#KL5q{+_lLACNRXih50PZsf>7pFOH@u zZJONMBvpEOwEGXl`OXU{MC}Ur5bywWmD&yWF5lRXxM9X-po<>2nR|Jk{S21s z@{owJDf{e<3XGgAPyJq(8nsN4N<&f3;%$ryd!Xl8*v7;@m zS}srO#GOX*(zLuy`i|!>x_CYNkX~;F-77v^gLEjNtX`3N-R|Pz`M# z{u)YTL;ANdbP(5{cU0^c1pClXq zQ=i_`9}4KigrpU-Wnti-?r!-%`Q)1b7@~^}Lpg6Ab)i2dre^vEj}D|Qt<<+N%S6>( zchth0|v=vw4NH^*D;VX(eq% zLIje(O7+{p=kKCWYT*@8^cHi%2Xn#k8=XFOS07EBA_)Z3NCljU;`1JT?Go}=3D8ZV zi_5^W> zt2olJ!Q%zvyF*F{!$eYFG=%)I?)lMazfF#XF;diz8?O{#n_e%~T&M zppB~ikMp1t`~v2u8Y2yVcC17P%0c0sj|=AtM4=t*Q{F&aHYO&Vfsqu2}T#C#aAvcp<|Wh4Z)? zau6^PCkjKVYs(=J?fmdh_ijbF(-1Ogj?*9KIwUGFQsix{A+tnqM;7E=>b0El4z|Q6 z>r6Mn&fDoo7rlw79}>nC3e4*6q=)ec#B`)XRxw1qbS;%|1S z%sAx5#-=L5*ZaA#84j%k0cWcJ#7{Z$jVfQd99I}KA%jGN#M`SDJKH-^xox$g1EbUH znD4jEv93qL0dssmrZKnhv1l{~PzIY;(`aiMvF|tQ%3D9A3bHE;IIa4NQ~dhbj-U-C zSdoezY(A|5k@G8`noIn1`F(rpbm8qTerxMa=*NztP=fnM2A<2E*w6}PT#F5K0$Rau zU9tANMz|nq*OabdX>*;gY4?Uh*N#%QQ?D-EcUr)ydKVN(wPlkH^P*?HoeCzgO#$;x zw~}-AS(mBEzNgL3dj8X5lkF6gd-<9lZ=^P{euU=W*?N1l0H{?b+p%c^rC_6ZqW6y9 z9-_TVf9mSWcIg0r8>{C?a7juf-tieWsz=?@jz{m>IF-Qn>~jX^)xIB~f8Qv+_bk&zb#`aiyYP(&8^iytW#%Zt-Vc9}k`fL>nb)yvEdr+9oTexUpN0JQ%-(ewA zO;P-M>OR4TZL~ptub(Da{#<5s?b(~LUU~&CclVN(^DiOZVn~u}?W$vzk4mzvl^XoT zve+zG+rufc-qHt|qxnOKW$VDlTmy109?2<`V}K=mq~)4IcUnC<3l8~HC$i*XlgPQ^ z_!1Erw4XnhH3Nxbs)R&FzoK$96>iXtq}!JDAb0Eh$|l$<>B3F;|WyM{K_a=d*RNjRGILNq1H@p_DmaT7#YcL%+fa>psPicNf6y?0Cca$T2s)&}3(b=*R>@_`Fk8O@R#g2%7k`qq)7_1q8b z_ko`Z&k%SR+sR1MS-)Acaon=-jHWRiSF$w-ycbvjr_^1T(|5FHvmFki zLDl4=znRS=0UyP^@0AUh+BF}$Eii5d+M`TL!hy0xggTF0kK8sHJc_a#qh-eHaOSZw z)rJ{II;Xlhbg$=o`l0m-r`f^z2^ldiEC+u?qB18d|D1T=`+FKS8TNi**vo z>6B5{9E*=CaM^!j2`PWHQ*G9NI#s_q;tBS*yH;R`V~zMHEIDX~ias$6 z!;_g}A}LHmIa_^@0T?pl7eqWcJ2)$qQ-4$5pu}pbtug-OEwcEOn|^z!wfOMtKr=L4 zw}f%DZ^3uV(Zx23l+Hpj(AzE&B^Yae9skJIT42qC%EORJP(c}!XhKgL`yq=dwr9PS z={J#kV7nRHmrzZa&OEQ-Oa0iSR7((NU-n{8&#WjU<+fYk;Qc|DvxL5m+2oAp{ocXD z*2X#4ti6`NQ!LATMA4{yd>FP@ft zn?+uWcx}N(;_~H}ygu|_Gj;e>LS-!rMmqAx7Vc6{gq{u^7Pjx(g}6Ed)!{It?fV`= zSx0Z+tUQ`Jj0gmX-w%vnw+<%;8X-tDe^0q~f75j}e%fLcDHPs>XVpUUw#iKbB?wD< z2vS#Ui>XP$GbjO1O&o{3d6TEM~Y%cYz{aUEUce%8o3m%heG-H7$x7ZWj$J4?4mI3N=*#&``Br3AwebOJ$^OBlcUhKe zlM5+4(-*fb!krLbE?7dz(qzL~XOR1})ND`U27J-9#1e8wIe*M4qKM_b>wCWN%mRMW zWMYFW!G{?%3YkYzbL#PM%I}rQZzgk0H)g-Cq#bBf#=k#Ipn}oaEUaIZDcKOep_`k7 z#OCiWgO*Xm1u?%3+JpQxMa3*uww)_biSF%1*0P>y>1-*oGTU;sxpjz4uSFcx;kka| z83FSAWUf0et)2AMsKbrwVp^$pm`UCj{h;I{%FNmL+xf#x_aAVK!3>pt)bCo-^M3h; zm>89W_3E7}f>!TCJWD^54nMTdwH}#JM6*l=2+!0chn2^xzLRmUs2?RP02qiAHal{L|(RyhQz)%y1_;pwqzoqpf-ewmr946j*fYZ9NVS)6JZqg%knalJo9H|BBF)tLxuH+ zXT+90(`HdyBz!^T=bMg~qUNLZ4&D13{EaWzXaA`G+48-O0ss@s+6*Q^2SF2PT>+Ap zsgpM)n6jnK&K_g&p9#^gkeE^^@op!ruV%u`?Jnl0xf0bUXCSf5dr&4LU4QM2{J4n2 z^Ox1%ij3I1%?1jcJw`>VzT7tmQC``2&s@H{-vL~a72{Gej1uBgB&T;(etk~@bTbkB z*3*k80T5rs)n68Cxq7 z#(%mFpH)RRa+AfM3mP5Y{=*k4{v=dU*rXcvU`ANfV6#dcp(42=3ud0*kKKF#95td9z&7LN!q{B8d5m z$BVFJQY2SDtkRq&%^jQ#S`}b!xSkbnt zv6xo?c1uBEY>m4;>GPG^Qj?aVSKQPtnXE4%cs}vl+SU77@=!Yt6{efG#&p5?t1$di zn+*D8-7i9?JKO-&`&FG6nSe(F8LQNDOSrB`XH-y*Nl?2weNekgi*f_l2@ua-0=;QB ztQ7VKK|lTgD-K%-ebe{K+DqPbzbvJs8pA+Dg%4Jc>n?sa0NUaT76l~u{8KT#Y@Pf zyByygzV8VEAv}<@t)GnHy#2&Wp9-5o-*r39cbtmBZRXDF!OnjECEJaaGj7L{LytNN zl~gye#5w*Mi{7%crP^&lyZOOm=%L*gvzumgI&OoACbHwX7OXuY&h~-r)#_a|OCh`ZbBC@{I7auY%9}BZ8;g#2x6T?j z=lcT7#=gTkJx3md1O)U%o{!nL4-eS7cb7`zKQjgDv7LwR)Ym@0W&x4W!3sxkZ@l*8 zF2U)|x9?CxZ+s#ePaqTW$a{`d8JFJkD0p7CQ<(13S01!m`wsT}4mQh_Tgl~T#Y#J7N^KMi7$l)b&q2}8k4(+qIEO=4avw(R4vHQt zV+-VK9V$_Su_7&^D%{xb;SSP3knYJ5zQNTkcK!%<@46rmuwjo^P8EepaERqelsC(? zIDS87qFQk2X7jHoeHS5Ix|bwIlGI?mq%_BCD?>C>u8hyLy4+2qx_v7$A~0 zacMfxgXTkXOa58gc(K8JxaaqHs`|03o5CKG**(J(IwetnegDfV)-Og=R|_RMI5% zA*GQymIjU*^A_%cha%FtUHw6ZX8mn z;-PVJkK=CILY100)7zjs$nsWn-(u3n(tw}e%!b>?5xVa--rql@q&Yqyt-bYE*I$R} z-mDCQ>&D&1$I@t>YBf8bHjhd+X@oX}*2#`5X;0D))3G~vV#5Y5IIF$Xi_feH%A9ZV zR3+ZYg7jfW!@o=_7)O5yWVs-xx#jB@s6MnxWv%Qz-p%B-Fd*XUw1-5A%?EGqmZOL) zSrR9(>mQpCoSi&#XNrpmSbuWs@)AuhyBVZOCM_0fmenp_3T;yp_kI+Mohy$Y$%Pq? zFMKyX3z<|WuLwlP&{YeN?~Xp&T^P?wD7JH5SM!e}C4s*6rQ_$_opYU1Okjri1^87% z|HE}x`8+tLZMatAdjLa@2l*7w*>ciQjWyG+^fhNa*EO{Z$+k8l(mC}nhTKmZpEX{5 z2l>iUu)0H!;!u}Pb()u&ThFuMMLuzM%T}cFYbOTEOUmLbp##U-FP7Nq*5CTuk%R~sHSw>M-Ho&yr_ig=vW{Z#K=S-_^SwpgJ-pne4); zm%4HfokM8BGs;{?3qxk|B)54=B)5`?A#TbuyYN*C!D|2z?8Z#%Ef#9B;45Z^rGnT` zzG_z}5q=lC2O7_!`;*~yjw;%wFkyAFsU`Lz71Hrr&Gp)8EG#xdd#ZsFgOvNFh7Hkc zFE4McC)i|dQ$R1Zt;pS2k{)_<)r`$s;3#WSVF`)rIF0oN*(?mMerj234p-wox(r1} z=}4u{IWqa~303RL##1bZPh00e9T5mK{#^c%=^)XJ(JtLn_Y4udBV7G`$qf7WcLYU; z&a@H2PF$L5y_UA~>#=xGe%fD_2khbbN=M+*J$yUE?G9l|vk3)P5?}r7x_h&Zck2EN zg5IsuvUwYJc<*h<-vSDMfay=AE{&B!d(L@%b=6&uTS;q5gKy~7;A0%#4r1%oQ{wNY z^w@eKvYKh6-a&r0kqaU-tS^RT)QTgMJrb38T)bJ@LiAD}{RDtJLg{{H^f%nkUk&^% zqE0%ZXiIX5>Yl0>NlssRL}uURc$7451cG7fqav2rUcK#xDp!x5^eMGDRAT3ce4ApwbnzVdQ;Xwj}p9pd`j2^dSqS}Qbpz%GNu3-L= zln*Ois9|LCTFI72Dttcf4VM}!;$sSNi{RjT#tWtGVzD+Prw%c> zuw}{Sx_|s&l|=|S+eOr4=h(LrjPubIjBzYy71~m^DM>a~Q^!ZxbYf6rfSxAFxz?V@ z__A)Khe$P<(yKJeM%#My*x$c#WAlYEdPG8gFHDmWtXwC!i-BgMh?qd zdn+rBv-AbtFX4X)W9r7({MG{j2@rV{IGs~WN<&~YT~jL)R)b@_h<}g>%WC#cbI7U} z4fxKRdN&bM6=YGxg|IGEA3L1vK7Pgj581kbg}LO{n)zCk2Y%?xg;GlzBq~bKF`DZ( zZ)dO%+UQOZbTidTk!z<{Yl+cEa{RWq;IRV*8$rF}*qhg4EHrIiURL8&+w<{jMkezivq?K~03T)5OXP2> zSvI3E-!i|bQKhn9rP+RTV_EAz5{g_^8RF`ag=%i2P0?0!;U=JKp-e`#f^5Z%JpK2L zX=W&6x#@9CIrj5AogLVTWdNQ&sI<(Vn%qo(@Mu~Wd_|bWwUT)B$;G6XMF*lbaxcSg z_9h0leM4nows@e?zG^p+lWQo-q5)yBmTy&B5sYZh9i^d`Y3u6#h%F>;f)UlzHsfx9 z-&5O=9@ zUWncrDOnq*wFIY(JJG1j4ZA+R3V~r2Fh#6I-N#o0A*5koG9k;|-CbHw@4JzaQDe1w z;>TC_mXT~@(zj%F!VIujTtkC&Q@GYl_lyVU49l2a5x^EmFFR}BODR?SOZlgt}^#f`4SYAzbL*C`kR7WAM{wVKPwaH8yV+wJLCB_oX zPY(!~3YcqC4HFDDx&6qR=}J>kwhPSd3 za6Lui#%ec@GO8GkR+=t8`JR&%y~1q0pKekir}j*;7Tfi+B1r{~_xt`awIHvaYB{1f zZ}C;7;m}_b-`uc9Z2w_m_>^_TpaftKkzw~vgexM*HXsTtb zbD1&l1l$R@1Tm-7nQTlN4-NSCq?Z{tvk3(ukYXFmugHHYfD4N+=)Tl;i1Km+op4E9 z2pZ0o?I69ie!3h6te&wtBIAmVZA5^Rqlh9n#RAj*e4=}=b(+~;0k@sxZYU(qn;kRv zo>Jpxu;*MnJDlJvE!~6?6CfzIE36fUHHSlH*#yqxNh;_>N{QuA7*sn6tf{otFt@d3 z{Z@merKj%($6qCDuJItSF;ufhd23*a9Y-vldw5;Vsttbc@*^iE7UnEnCq}1|jXk1& zI_lnWMF#!jfIde({nBNe$@|m`(v{qrmky%}{FGZI7HSAquTNiB)q_xKE~R6#%WU(3 zlbPL%Ucw!n*{V&o=USuUGzySMAd2vi6t18iG4-xUDr67U!+HXwbH-aTeuf;`} zH3+%G?X2J!Jt7A=MCU7g)6-(}#?aa1Fr(PduRzSlXky$%tPN&+-}JK-!l(w|_<^W- zUl95ku4^>hzE^rfWlJ#!vyxK?FU0AJ)sh2eKLE-(@(Y6+#N=!@+poz?Xh8`4AH>^V zgaI9H1-->W*X4SS9dok+@y`7^w4N9B*^l6eFNRDg8JW7l1l6nV#kqmt;mV$;<*WkW zeo;99fEhP<>@0=*VBZB5;~VZ+p!!nSOW$fWN{Cw&`PC;|?kwZGX}O0k1NIwBSBa5B z->OKz`7T1M&hBqyf$}23XNDd0Y9z_K8mgPFVf!BWymW}`0duk8@vj(gDAg74HN0n+ zWP+onzF-`Wsc!yYR&K@_8nOF0E2hnoh&-aQ`(+IDf#@VzNauLZck=-@)0uwz%MYqX z;MLeK+;R-yYp!*j167Gr6Ioq*+gZy|jT{9%ed%rWV4QA3LPu$>Grkbjt6PU4 z1%_m7ubJg2&azuAJW;l1x7{CGLM@uM`qgQfjapWv{da81-(mV^X~ujK?RWj*8M=*T zrz)jZ0Jv+s+WPlDzr>HAhnSxS_84%mji=_%AC{+g>XSO1(GkIS>$CTuGQmQM%~gFcCwk8x$n2j@f{39XmuS*%iVQ(=*h^9;PTlB4Hd~PVg^UhZ z6C_@E(Ntm}jQlS2+W&WSU#s0BXVY1JcwG%o6A+V;K7T-4UL;PPCxzkcVUJpO4H zB*_)YUCAyZtaY8Hek9w&Jy3U=`0X8KQ)m=H` zTB`i@Weg|kc151rOqh(|)D_=z%mo61u}9TtT+aqpu$9MrHdB?iG}|THb(|T3^U7)_l zg22~6x`IriTX$81XH1`*|GI+Tw}+Esb&HIZ8`2NXtrkNuJ1=iFtj8|Z$*#9l+H~Bh znv5_T-{<`!UM_fr5-5q}BlgBa$u2+)e&7ksxUXlJaNPN+2GUX4QlmelZS0FCBZvjui8Q&9IApR)j8fUY zgg0t!HET10Q?tsls_Z>^whygmoYRrN2pErqScG(8Zw!e5twamIrXQ;&0p9zvKf+D? z+<$+bs6vglwUSFP>Zzueark;Y-DQlR?3xI!qz1Ta^0SmLlvIQKQtU4GjQc)r3Uqqq z7>K;AQ%O7^frhg&!)6u*wRPDt@J;CQIb*}gfvX>mzxq;(vqo++yhv;%Nh?>wwK~^d zBa7h7G(8g*+5hsSfJ{`?fKA#SEm5IPCq!vu2^{p)K=J9~K@!KO zr9}tS8P(UB_Hpr?*hkw!y(m<}jdjezepj>t)LYp;5*eV2nqQks30 z8se=i69O_iimKuK1iV|edqSEY-stk{l@90$Jz8#&gGO{2N{G_acxCI!TSE{sHI8Tn z!zEyyLoo%c?OvY)CRIg7)vl_l@zW>d;j5=+^Av1;)y(eUAp&r;m3$A`vB;271gb&4 zkFW^=??g3gPjMb^Ewaswn{GsP40*o!alvIaPXsc|9MFzAbxV0@AeQ{>;y9Gl^LWpK zg;7}3I24!s);*4ylh~KJNnHE#+BzZMI#S0E&P?7z;0!5Deq5|DpUjjYMF@qU&Iog9 zoulHb#!Yi_jj-akBPDV2LzgY6MJCuyY~_;n^UrsXfrZYPqy+A(rLK9kPN@it;LoqtP8^7S zCL--*ViygF*dc$PR5~qw?Df)g?#Uqc!~iOD|K&M#dh&cQULU)+;7*A}bv~^WuePkv z10m~@JNIKf_8p()iO>w)assF|*RjF5?H>6Ig}Pma0<+4lG_>oE>`@F09sR=>2A9D9*XiOjaPmawt*1WIiYw1(r+*uhI;58 zif!#~tNUuRpXfW>CP$~Wj|vV$pCm#$STi0>n3Gx0HY*R?v}X0&F^viD+P6H|jM^fK z^8LpGu^&`12nFY5Aqm}XNnAI+Js97eV^-@uLvRTM{G+kP#~vJJ(l;vD@^D43xF(5z z-dI`OKv3r7?{|X{{&mJpjRn)+&CJtBURRt-16mobkR5t*Nt9k6IRGIaeZv=0U zb?9)FR!-M_>p}D4Tub>JePQe)(@>F$P<%m1>wFI^F~uVvh*4#b?f&MF2?m+qvJ$VF z7i@pC8Q@kPsHB08cbVMZE?mAC7jER+x5u&JKS#6Szr>Fi29a~^)JiG)cxwOj4}OaK z*&8fIuCmB4VoTCP85=-6f~|3gN(f6QU5zGph%e`=o2#i!53Q>!Vi&~AwUT>XN;-%Z z`_skzwzlBP*tjsNqoadg=&P^8!HlHW)0LX*&dy*$_EISsnB2laCfxP0Yv=64ni8cg zA<9q}yiU8VaR~cjVne}^dcjnvZlQn~vg*2VlDqFl_arp;<>E?ZgKf|7)A31)9WE?3 z<~9xJ5Hrk+E~fYG+6seGkg}OgSy*%B6vlLwAh{7aL&#W!ZU z^jyDpo-xGT8^;Wb1yLpBq!QvbAbGW$xs&dM$ctW&52V{8KRRZbOfdC-zayXyzksTV zB#O6h;TkUT_9~6Gj~H6^0!xa?&8rFkJYDgnVuc`$fiF8i#o=`b2D;8BobM#*4o*=> zQ!p`A9I!_+fZ5}PieMWlR(7stc?FGE+z^26?d_G&*Uu)WqEd^3u1NroCM3RrQp-Q) zRZag3CHtQYDS2C0HokzQ8=P|4QlK2J&B9`v0M60Og3m@h$)W zAZKM&?C$B=`58l2Z8}akFBDAipKd2%4ls}3xFFZlSCqU|M4AN;VP|D5!hju1Wnm&X zGBQ!K&Rp`-r$7R3+sej3;R&L)$)wD8TofKW*o$+y%0gWX<05QTU%R} zWI3Tbbgv>>5xrPP>6T#Oc=}?|+&XY{|f#w0ySsmoC7A)%)OP^Woa;*jB3u{FWZKNi|*1gm63osj*A} zIuxPH;O(6q9$?fkV~0$Cg!^YjMaAtHal(Y7Qp=#>4J4Jg&J|btJjpo9*z|PLD8ZpB z1rGO=vVS*M6~cm>hn)%sblu|Ej~3;6zr>F!!AS2a42a$VCyTtiysB@H8^}31RZYe- z$#gyM5|sM7hdI2vJj@mpu z-rnC2ajkom;e8;{;kb`4`*+vZDFJxVnbX*+7OXaFK(m#+s>kzTrt^bsnw{dW5)#24 zkAvdAlGjHXH9I90GU!&CR}`o?IMlixcTEtdYwn<7KI!$McIE}Uzet?rZ4@dH%&U}? zA8n3}a{yMyeU|XJ!6(ntL6=FA@^BSxWcB7tJ$EP^$lghWlkaSI%R(}Zv;8<57i=zscWk`S_!*ra#dfsrp0o*lTUU|!{&p6jx*L8qp2JTi}ofZ zhyCG%bcb_gHSSSu2$fL?4^7Af+|*C^*EP;f(t!%$zW5m~I8tZY;CcMXe_L;ZM?v-}C9?ks9{%e6j(V!tq zb(VLWyq7CUrh8DKW2+TG;L!0i=Rke=`321KL=UgYyXufsX#e$aIRrVmMGDVoT6jkI zAE8p?zY0~;AauLzB{Zo*_skz#mb>PRiRZG7g@RcGYQKBHm_?NiH)8>{+vhjA@mP5`LGll4O&<%KLP`)2vjQ07R zN-;z?GMWgkuf~Fxw0u-VmFXdR(%w?Ay|*`f+^|Xl1qEe%sDZDh?cH<5>hK%YvCl9{ zfKfbZ0o!X#aAsqN;(eC;2hl^W22DtH0${sDSeZdT9Qk=N?aztI3I!%{| z{xw5+R^_@C$6FZ%YAN5&ME@f!TOS{BIK~;iUq8JhHE&A9>JRAAWS5J6 z=!r%eVVZJP#NxeYOaP}mXV#zH?kQQYGO>wc7?@(6dVbJFsorn4#d@Aso->DZY58s) zN;m{b_iY^K@&mPgAS)0u36!`ljsg~|Ywmw?v5S&wIVrw?3O9V!5rZR>d5TI3~`_5JvnV~e8sm+=-p!4E;9$|EG6$7A{B7A+X_9)DRy_4~ z0Y{$WPjK`(hB-TaC~Im~A4xHfe&zfX*Bb+U2H?uQ18uG9>6f!se9xYUEN$*$;9WzL)eoQWX1Q z5Hx8&>lg{oxZ>w#nZhY!HTsMK1o2KrbAWO=uX%0CY!bPR2)Q6}wh7LLM9!LE zj4@uiEIL4&{vn$L>Udboh%#LKw_`>wU-n95_1!a=-QWx5``+{06|j~Uk@tRq1Yd_8 z4nw_RUPx&T(SJJwxU;Z5!QYBVI<~f!eGXsFFdYOZN->HQcVa!Q8f+ucn67gXcBFpV zdh*?3u{vNRndgHir~NMd-R_|ckdwPkj36W=XeUHauCWwo3g5j}=+t9ctkp1%U(l*X zF`^eey!%AF_WbQN0aOioH(eL0KJrmbs^l$Gq1NR+q_>UT3$bM5E@Pdu`D`>_9x+>6 zFBv;ZQXT8iRE49BmptCuRUWPL_|*LNEl5|>3K|PDynm{DBF1UC1#9bp=V-s>NYSyV z<%7)eY)}@sIQTzblOe1^2JV7D$P95rCuA^LxnEQR)7bNA<82Q|V*0#0zHBYihV-x9 zm{O02kYP>l!<&IAj0adQjT`&b#ye<0*P_8{^V8$zJ`2J$aLv!?7oFU`&UBkQm#bHf zWO&BEIUAISiw^5>Jj2(Q0sEbwC`1Xg zyHh;tZ~#qmn z1(He(`L_ALM7B4GNtE_P*lj_>mS#HNVAwD*g@lI2Ivp=d`avTpOX_-@<&R_-p6~vn z|K?wz%4=N@Qf;X#qk0o+`alSg5YB}%y2$q7@vP4q#BQBU+=Aa-2IyeS=*H8x%8S1LJu4XJjLqt=pm)Z(QvkO6!^AQPnjro z9A%*%WY6bBf_pp3qT*Uy_K#OIM>9e~Lm}(!7~$CBtal{1(6I)@4rrnaD}Kg|yXS^ zK8`9f!n{VrJ+?Ha>wN;7gG70y)%BNE^DkDzJoci`xY;26=BMlB?VG;IJwNC5mdLk`?oIU&6r8HMqw|c*SMPm&gT2XR6)%77C*I((= zmNmHZMRu(8Mf+(!F*`g=my;r>awT_mcX3cL_lW}B`UQM`?-(W3YGAf2$(Fva7Cu<6 zB&gCyc)w+p;CUg6&kwv=_s(*0b(fC7Q z4@C#}Q3SwU^e1It16eXEVpX4Hv-R#$%`J}HA7!?&Ilgz9W0p-F&upD*6|t_dh)1@j1P8B?Aj zq)L6-^kA`Pq#GiSY#1I=uRpprZyZcy`?d1+#fzL?&HosR5_FR-m}W=T#5F$BmXWyT=HgLcL5W)d())_#X>M!Un1t*C=;EJ>U7(J zvfWv5QC3e?{%|#wGj4xyQTK~T)$G9Hr6P&FeGfOG2^AMHW>9{7h^~?NsCk^3Vvzgs z_`yyu#iIIiV#CE!odsP*+jAt){44ld6x9DVTbawM=w?X<~Y5goIx0vXeSdVwYKV5il1 zpYO+$jP#lgDUru%_Hh4l3#g6k>({TatuTnBJUQ87$Ln?yMltsjl180* z9lwx>kY{VIA2>?~6x23JRqnW1dpM6wyFR`R_*ld?tyj+~#o2fPSL7TvIBX)Fyl7sK zi282+aT28Y>SepR#(^FG?pbHrul2_Lup$7gg?b*tZ7^T7Y;7;pE1=T9lYV44##{VS zb+a7PiaY8!j7iY>!KLuG)A`+B4q`OO;_waDbt1p`HHl*@GjL(c(m9m`4F+Jhz?z_O z5|K44G6MELiTSWo6o-~s)nrEM&c&l}Db%zb6Y}!%BtSYh3+@gnf}p7#1J})uN&tYG zsSta`n+0v&7nrkaglO~*Z;FZ9H)-Qkl4U4}_yrN-~sE<~9+ zWN4JZwp}asrZby^^&>0zp#olAbq5HFZLjSxqo$2Hx)!yEj8@R8OU{q;N^J(z=c>!dvOoWAy0wsoSyqK|JZE2Bhx0m5aZ>M1Wqh1OuU9%u>jmeAz zZ-ccTVFiAtkanA#h^Xk+-BDYj#l+%~YBQ$II_H1Ey*Nm2fvb+(NXFTy<(l$UCD`^DaG0I_?GG&E`2JyMSy|hH2<5DriHez`ARgPK>9&Bkk!yk&3h$MvW zOcZR5Ns`9i-+R$%cP;$;MZUlfA*)DtsNyzX&-slGus{}>t!7H&tBgKvOYvHsoSd9q z#`MTEyz+95+Mll*9HT5Ax)>%S%W28#H*K&q#|S~tj(>ilC+czjhvEV8T=~)e&wQm% zKpH>zGYL_p&%ZOpZ>tIJD(?$3<3DsPqW^8R#ppr7e; zTGVp$JA3!N+^p$;b?3+cBA2M&!XEU2*=29~6DIogT=giihxvyOUj|%5!!8ziooE;s zpjmfJcRl@5p{+`Q^;T&+*b^c7S!58x?ofScYzUL*8A#1HZEU*IKr}2O0yFg9`|ffO zFbIOwgi!y1+VcNrNy)5Yq$>Pn)}j2v@0Pyavk3#^E@lRVTQxDCs2`45#%C57bjBL~aIkiefwTG(&2wNE)Gr~#0y zdB$@9!Q9Tg)wF5qBmXZ_Lcm2JVz>ce6FuEN&?ERz@1>}}uNZmwl?Evt=* z=jfo?>axAj54fN%)JL~~4suSj;i%ukse}GEsKx-Ri>|(&Y?c|VZPzNUR(`Ka+s-k! zcIW=$???W{?1^Zc!0=h)&K=yu=vfd}K+a!`qlS)hmA%_8$c!DXz^Gn`C6O48KF&Xw zbnz8%cge#7Jz*=Y3#1ITnRZfvfEm9G_Nl`JvQTo5@bYQ%n^E3fxh%Yf57Mx)15RsA zewYD6|3KlXTl+(XFEUA}n_X2& zj~conw_g3vIiL^(@L+R0i@jF@RCSLtvSGQAY#8>8)Rl8oRCyA2vOp*QY5JR|RD`&A zyrYyj*Ae}Ju+~ua-_@(#JP*g-|)F^vjzJUsY^C!EoWVG8Q$>!eLf$Fe8JWI|wXoLb%)ipJFGfS;BJUlZypQWXw zq9BRPYFFRi2r0d@sctu$+zC06_-9vwJUzvLxJ5svEG$N%wXsYg1_$oW!Wamw zWWgw!h3_l)2SaLBWBSrHc8IUFbuOv4UA}GEXKz^G52teIk^vGj+yL8LS-*}ihJADHp+ir z?gG8fxk;&$5Q4q(ORDY)+uO@@L(hTN4v_xU8mC zOh=Q;U;?J+8k!K>bY<%yyQYKsh0Ptaj2M4^xW>meL~5Emk3iD|B4X6PnGK95I%^Co zvHc_*IJe`AJgrR__V3r5u|Xw1MUDe0T%ygwCwt(nS+>F4jadY2Or07rcOdzp+GiX90?hmBTJPya#o5^z}vz#{VvNRhWpLX;K=O%Y6C3r%)3*+dg z%kAdC{@t#PYC*w=f29|cz!x@B6m?xT8kL#qIg1lqzEMvY<&V^(mMVs$sPXud{#mto z8W-|ZftNf)`wDx<*7{uHhFQBShE-R-cUYZ3aNNAZC=+^d*WJ`sFX!EMvIUEAIRD~I z%&7+yt;{Z?JHEcrBz6LN_g60kNfSLFDcD`51WuX8=>moqye_s4>Vy}BKZNvzPaLG> z7nOJC|9@b9mY9-zndxv!^?5o?aZ)m;8I(uQSNS((KtMpA)l7M{`=BXrtP0;|%@D$W z0qg}&OZbNsY}Wkkbs9eX7zr!B-QzPTfVC)H*|2bbT)A6z*0}~8oHQ`3d}MEXk$`n* zW{y89)9EohyjYL)K>4c}&&#;LNgHNN;mXc5X706~;(T=GGrMOm z2A89NARF~!mJ|(tDb|5eAe6z4ivt@htM8=~X1OHp1sBkxp+T-~7MTF>DGhmO%~4EH z1R#Wg?aBU9x5Q@@c*@RanX=mHZE**Y*2tNT`A^(Ojiy(C2u<}xPmeSYcDoiMqB`@q zcVOe{{5!LVZkJ|VLq46XUP++)jY;(R?7zNS7mUCpa|qo$d^DFRi~kLq&;gF?~A$M!cDGkRO`iGvB0ZMv;Z z9A<~aCs$XA+1Zr1->$baeH)8qnE$EcenbF@lKn5cxAO)5(sO`Ps2@-aC16=W2PM`Y`wFy&CE4H7aY4JR90r@A8ZuA zfXARjNo5WI?M46NfW}s}1}x!Xbxj)Xd&g+4%-P4~u7r>IO&HLdT_1iLwLBoi041zJMtFmd_}$d z;~&NYr18Y{x&fjk|0}3t#hc4!=hS);0_5_j)WQ zeIAL4iV5`^&H1v4;afrY>9UKbvqs+s$EfPB?s&BzK&k;XkZ!={Ks2%_v(zkp5YGn$ zdN2$PR%w+su6F8n`%128?`j!~?sKiNT!5`~S)Dq9_AY%nMop2jw?qwDo0PO(u{%t8 z&K!dSw`oC6iP%0R!HX0KkLf+?h7onv6r83b3cik9=cA-|ZQhx2%^u5ntj*14Y|@|V z+Vm8e_Qv{1qdkuVN6%M$XZjnqd*?h~|3?!2pJ+_`w@=jj2PQ3h3V8de)dd%;FmI`~ zPxHmdhrQ|eA>U=Vdj7Hq=N_^U zkbVt+=*-StILBpdK_gA|CL3{dC;03#?#sh%J^R77UDTI(mI=HQlNXb=KW~h>s&aR` ztn~P;PX!J~7T_}#MPNWUN<9A1_z(hqx%B-l$&#u09RcUJ{n#hU!6mYBRiyuqFqLPb zg?zRe=H+|WzIAc)z%<=vCD)-$g>{18;Y2`LAnnSUma9E_xYP_i!})|t#@2QmbAQrD zcG;9SlqvfDZ0fjILk-Tj9YUaMd+F)=BCj+aZh_Lq%uoAqXA`}>5#rvC_N zAx6Fwns)>VvJmO+7HAKu$+s3>7HgcLoAV&_(n^Vif5XPaWr8U>(9rf>^LGz4>JS-E z#QD9maPwb9v0Ky<+2v98oR2E{B`b?$$c`j{TPd2^P7~6`MMLmw=r2dEKQ)oZwD&Fk zIzDM0cC>ZoUE1Ja;nx-6*^M_-`6Z{WYl|#{#?46JrL_|G$v&{Ww8LNfK!*wH)XJC~g-&>nX zG9o6Dh^MKv*{*TR)ZKNprnqITD|qntu=apTJ%IW;|M{g`Z?{6Bsi|qw6oUWHE7bt# z+!9GLa9J8KQ|g}Jd#(EYtCntEuSl6cel=0duW(i*S~FsjM%3e_Tq@Mx2nZeD&sKkZ z4Z_rt67%ntUk!8qTMK}4iI#!wCN~6_ijJG^=T_qOKaR2jUT~3JE+x+u0Bh;oGMO#$ zRJ|sKY8di8w6~r;`<}hKe2vVF%n}&$>&3XI-B%exK@|1wu^pVwo;qZ@ux~o!J$mLf8Zwux?E`kbY`e|YLqzW~LH^p9c;4ln>{K4Z zsJZGczhV@tNYGER`3_s5s_8OqXgHfDI0f;XjGiBS=$vm^P;Rl@l8olnL%46$eT-FY z5xc$iJH0sR-#h9i2duB>ozD0mZQsQ78i_ac94s9{PMV=gp}&^c#eBMwQ&N$)H!m-m zv0ljvj420Iw7c0CpcJJim599j~w!6>s@j-|Qa1x%KG`i3 z(bY&6qq2{-)tf_QBS5Tif1y~5AVd;$_aO(-_}YGNz5kD*k?|7W3>DA3)isX10IQ(x zpDZDtogSfTU*FxRb6b}bo_|)E8=#F%e9`U4*zi?oQCvpAHvPvz`c+dm>?YknzN+~9 zl(#P)C>~KxHOvO<7FH3)HW0rNqI7{^`^6qHFps9eKEd4Od#@{)!BSo$yK{@^;M&4} zFqT8$M&eHU7n<2^cH{2a{6)Y5^x}GUbK%^7aJi8W1iIbvQ#$s0CsSJHo+Vuzn(EO} z=X;{Ud$Ay6mXffWQg}O@2LhK6?c^(t7M{NdO07$bA-OH=4+_-}l?uKr1+-VEzp1VI zFBxJB1~7#0SJn6Xorw~+W<1@9^QQJ_b$$%e-ua@tmF<(F9m(ZqrKfA`LBVLT2o|kfc zHTm4pV1{;rOezQN$aCA*5{#O4gaxj(u1KmkAv$k+Ei_YEnfck`I0WNViO zzAcj?wtCGmi}04B%bB}A2D#HOxdJhn_VkG~|5rPz&1_j=33bFOFLF8)hTm7Q9 z;~s}-$nhJHa);2PoUK7>`3!+-z{5|#Ie($@>XPw#HQ*{wL+_qRG7!8!=(X6GZ+q9Yt4HI?SHQF` zSyJMPWkRd@>{t^cG`l9v3TC{h?(k-R&yAz9ZqA5gl*OqP8woQ^ZaQo=)mw;}^?bH7 zja#$PZDSNJw&iSey#E3cy!pPw=m0C~O}dHUrmO%7gC!a_cYr;RyxU%gX{X7CTF^W2 z!{%pE^tw3H{8f?h%@m{GC5+`6eAy#Bl-ylSe&55kKF5f*bOmF?KVhgrsQ8=EOAFsj zV!X>E1k8CRHJ_{z23GARIXqU-=49FXGaiFZw<3+AY-(ECfj9&3(UT zSg%SVAc|S}V#~-(`p6@JD!)|akDyxRU9iFtrjET!;9!nmRbV@n@MP9jjY>PkF3LMg zmOQ?Tkjy-pl!#3z18|!bS-Py>?WpSRu)fIq(4~gmZWEcNc_;kw8*?aQl?J?f8Tq9{ zFedM^z63P%!XI_!XPHZ1L>BZgqftgRMutsZiPguRZjrsgyZB7IVN?#V%UVX|IwxY%CG6fk{$F7umpPij-uKA=IrPc_PL@`-i!SkT;HE6-+{moehSIE`B z*q7APNzGmZk6-L}N9u^OhAGrpZ}s~XU|)S}Ant8HI8H>{JG;hPZCH-Y{D*32E^3}0 zqLTO=TQOg&TA|}%85{IaYtpC0p4*cGZfHrq+tOr;&NOgBFX3FBAMoVQx z+s^E7gO(cIMQwFjH*FQsk6!TXO9bK57sKyeTyr|!9NUK2M$v1qB}`*j7VLJ85P+5| znVfbq1s(5px40JcB?!>@3BKS{3+wSxs_ao%QpAx5uNj(;DghxK5q<6z#54m%BplQt zYPMg6>eG4#r7wH~OmcAxdvt41@L8Fiw!O&-IE^-&c2Z|Glj~H# z>bNzq^n+?7F8-~N1cORpRD`=t?GGLA+w*vD&nsRhriye0YQ(ob@+6abq6JTLcgyR7Y@nd%PMmyoT8nf@QDu=q$xzGR(w|@Pw z$jEqGo#tke>Gp@%gFMeGTi}%-W}903Zy}mozJ-l|xkr9$t=HSZSUKciik>a*8~R>+}5Qq98ipqI{goSOA&!pNoF^ z(=u%gTvQ4c`5GB&TGj8DX9tDqskxOMEwjGYuD2fF+F>0pp2{gMb};H2Y;xIa(P0^U zviSbzA3MNC^z>2EBgL7|$eyYcm%#B_XPUkWI4Vw73@&kjC02~8pFIz=Fd0S-daL;H z-FU}<0H=oIvhqh;I^F1S#CqBLdSO-l0d(e=@R50fSFea{U555|C@~lKL77m`1eNr?^F19 zx&H4$`uC0dzxX<*$dhw%smLUR5dzJ@wi$J@5>|6kG2s;u=-PZDqb^dD3pU5DMK*G zTn}k=x47ZN!|~!nLy^M5!(%53yrf>xm1GHEsC1Dz3pC9VSZ8ht9ed*^= zdZq5CS4_4`u|CgO{0@qa-<8>d#l8x^_dFeRG#FJ2nTiLt`mgnW%-xoDZC~{mHf~g+ zak3p7%MDqFB%1^U2KGll3S{T1O-x4ic~5)rnTD8!=m!DpM9Nm@bJRkZ`qHj%SMb$f zsJf(-wRNfU-n@?OQqz_iZ-t%)Xju-gfhO@eCU_`B_UV~yWq75D?fOuuUAZk~Vr7XDXX)4JOLtvFOrY+jwM2%JaJJ zCn;Z@P%;7dt`PEbadM~p(o^%uf;@>Z#LbdnRO7Da8)Sl#>0bfgnETnbOuk%luKMUQ z>j7!pUnL9$vL^(p7kgV=jI#I`8t3jBs&Tj&J+(wMU$#sJul_BGL#cGC25K{uCMfdI z(2<4<`s2Gyctw|pMXDp$nG;_4_!*;P3!3!aLv4KTtT&Pf`pZCcYq8PkbZBC9U^ta$ zqJ-hO9-FJNtk6x2m|77XQ3yDuo0D#F7l)S6X*rdpCmcVq@jo?^@A}ijz*!~(g3x{T z=0806YQQ>`%+@7$R~DAt6j@1UGf-k-ZQkLu(48qgqU@n08WqB!8%6J6=+27rZR%G6 z(flE$wyW7q4pC~cQcxD=G?&LsxzjKPh z_ASwioA9d!bG6+XHSa?oehl8Zs~iYas3*A3IpJqe+0LW-GTlH=CPN)Q;RDA?G!#}8Ry*ONqqe(mam=bp zKCl}zcc*n~JN2tfwiUS!1dXL8a4cP>b;Y6}KX{i?T3Y(iJ=oMW%0Jv+R-Rn+H}kmu zC5C?uxXRay$#c6s`f>vgJ4PlV`Nr#3TN6gxU^!LHfTIiXcqzE@;Lht2XV#eJZc5(} zK_3(-b=gTntzI+`!_6Uc(`cB75WQ6^nhLgE5N0OPEc&*J;J7vWypfcQ={Rdxr#yJG zr67B;#qBeRtK4j}YK$!i0`!V;YeO$q_qjDWgJht8WR$q{7{dA!BR z(Zb{I8&RMKxv7rSBe1nWGzx+|jW#7EcCZ5jCIJ_q_T{7b4F@!uDuh`NRyCB#v!V$P zok?bBu(#)%5yYidPqy6UZT47?N?T|f+2N#ZpxXxi*K_@?b3eixN;;)RNg34TZ# z|JeoxYmla+c=3_;B_&P#U4PxI5#v`XkPLv=t=mTk5a5Ia-Cg^0n8*wcX>zKTWi(hD zGl4|JbGmE%a1D;T*Z#l?(97#3^U$@HHsg>NZ80j-IKM(@`XIK!;IJ9D(LC zc*YPePcZY1HnC5iCzeeEp68lQQdA+r56H?dUA10i3&}l%S|?Uw&{Js z%BvT(s{#u3ojtLGeVn=tP@LWNwXcC!mCG^Zbj3A7BU)8ShNm5fMbA;cF>cD#Pr5Rx|!?PRgubXX*5hSmy~(bE&6X89bly%5+~^5UvLYC zB9<6127flzflP`E!RBrqZ*V7(S8ikx!O#n*s?RP|t*irB%4$#0kZQZxp&M__r6(lC z-?3WacP!EN#^fD^ay@ZOv$YFUwdrgBqh~Ehs6`tgNap52UX#E_)?s?i@vi<>FnEnO ztoSj?m`MfnlkY987Bt)Vk(G3^RbEttnr-AXD6nw5Uy&Wzd!3i z-0jg>%R?SG(y%+0Uw-Qyx`dwvU_BS?lBT0M$uCyu*bmE8W(o{ZUfy^<5Ta z)s-b#LkXHJQr7N`Br6#3G+8B|YZJWau$GK>=V+YCT#%hzcIHWsJEqHNF{Fv7n9Y-1&7KsZ%0Ij z-zlS7J=A4Hgi#5#!R~KJM`fE``9!^zMDJb@Jnub@MPaS^!DC#cMk#`(F+1}$&EuMEZo_pFHynhikp z?s*0*05EZ?1vR*FYG+hG7Ng(ApDaw{40=V;@Y@o#+Zg6Vor>1c zyAPv`P7tZhG;A5v3!BI-_4k8}l$iA_n*B?xvnIl@og5*u zVTCA4?(sV6pJA2yZCMvmyXyaT3L5x%jGxL~Z|vEy03LdAd-_@0_R%e^i{E zCwl90sm()k*ghK964~|HTi%t6Ln&Owc_Jgj-mC5*j!FPREEn{=>>u>L zYgFOF`&x{ak>whoui@S5Y@VnQV_USu%5saJ5jLEYDSPn(kjm`DG<&_ekh6JX`Y6ej z@1h=N@sO4xS4wv6=2&P@RwX5U8R461SgS&+r~+$ae4YAgTCl zNR+;fhDd3C&j{_Ayov)HzQU^yoK4qZxP(Rb9vpq{wp(hUT6heNeYMgz5WR%os%n$6 zbsMlM5=AD^hsoD3=;qVsak1D{f5Xc8 zW89u?S2_h0=QlgLMPIM~AltSf~3E+i=0|vi5bhOES7% zJK|HD<)bdKICN>KVv{ZIoCNIlU-(2b-B=Wxz`j5mi1spPRp#x0o4Y{}e91=%9M#bB zT`FM(JuPc|Bazf`E`aANBj8*B~^fnmL(y9j4D z!B^IumD?i;^19~lB*?$&OYnm*jRUlYF_Cz@+RT-Ue{%n=1-LNgbvRUToZtvFj>4Pb z8+d^`A1cnwVKjxw>%Q#cuP#4oKb&?`AIG3A5#)DtBf~(x|zkCRcV4zuPzDo@IBt{^Sec*;+2UipI+r} zhZ>F23*sd^c89G?ZlE|liLH^=En*@wfm#Pptg_5-Om8%TnX?>^(5#H*b1IKDeigkn z&A5FP?D=?jrCS5N3%WE2!9}f1YO#AW-cbq6(A_-`SJH6lmkXm;Ry|y7$->L$!hc`O zB0C~gB%uFQYbfAL2W$Cd>Q+gWYd&ej3(D7>Iv6i8UNPtDXA!p7jHCbozJho?;C6l) zmP+%N=o$)rhIGrk+PZtD8nrNQKQK9-^(AXklI0ji(K8Oy5yoS}Uuc~WSZ%!j3Z#a} zCt>M`VEcOVsMC({MlR*-WnAPo!A*m{Hr;vk9$`kc=q&j!SD}i&Oc|j8|KVrM%OYZY z2Ye+=prc!%hq9g11IP+5x7^W)kEBcjbFvqUKRU&=0{a_;M4}u}iw#f5SEl{Zw(o^x zUadPl;~7_dc`4_s^D^Fa#VsV3bQ|$~$~T88iorxWHH|cq=7DIw*7e2%Nt%~wvy^AP zpAOzBH);s(IJR@kL`JV#s^ztEl`ZOe!(iVlib$&CUXF1ek3r%F*AH=0O*$$rb&v#c zK`TBGnXP3RHXicU)p^)7(=MX8U6S)8pk#X>*GE8Q()0+pE)+bWq7lKqd@MV)%#GL# z+{@X0r+NsgtOZuNKr1}HtJEY)P3XRxXQ6?k(gS)@>rqU>Xru5F22c17XMPFoaMDSA z>20r@h{dGo!~6U#Y>nT^9gGa6(F%UoWx@|kb)p{kzGToWd24W*A{#9Wcb~?nkRceQ zS;b?*wfz{9HWEE2{a|n+|d=@iqcz*#%Mc$t^xwN=oiP(v!fTKJ!YRfBa ziN|<{(?NDf}RYxR<=feVk0Eo%e`e_0%cYjkUXN?UUmZ}2c$ zdv;_T;{JVcF}->xfXL*7c@`VHXE@~chzWGDTD|=}7@@v6z_c)(ZcFFQwR&cTayT>2Gs8=I$?)hQ*H>?~scgezRE#hV)hNB*Hzqv9_WSXaT4D>?sJ3o`RG|kJ2EDcN)EmSj zU-C?!+hk72%X{wEAX_vFkwilp0#mN!sqXQE?vmZX;P`^L{7i-YG;Yv3q`a!JJAz~J zPs%XMl;L6Qw^yb745+@I%C00UamD=+1RT&mu4nKaED`WCzv_<)Nh)(r?o6h^(_)Dd z!8+x=xMZ<|GdRZk1x4Ujb)EuPAOH%B#bs)@zd=}IkbHPZSrxIx5+IUz+3o}%_2DNH z^|Pte$mY&kzX6@jewE1rUg2Wg#fR{MfufaCHVG?00-QedX z&hMonUrarkl<2i*M7R$#R_mm98=0Z-t7t%B8$BWWFSq=dEVuloiQh?xh%eA|9ZoiV zl+VRJe@_%Nr70IY-F9!z2$|ut^C?veT!TSe{7;fUPwK3nkSd)U$hnRV&imA_AUxcS zEuW#zNH!>S?0-tDHR~brM~!_cN2*_A+6IL6T*U8uH}y-hc09_dH#18w+q|QZ1j7IC z{G3i-BPn;5NNmuD^@G7No`=ra*4Gqj?;f2c%BkPuyY$oN||2##&B1d>hw!_4Xt8(hA(=&rH@i;5A5!6*w zl)*Gk$b4tW1NR9{-NX-`Pt0>pyxn!TqT7fGpAut*HGQ9Paps`OlW`%DBT>wDy{alB^?8ok3@`S@l=|E7J9J`c5zock}i+>YoYS-&nrsdA3z{0gBoF7$U>dwNFv;f97K(9thh3@7|f;MI^Y}hB``q+Jh|KUdWIoihR=7r$gv4*(bPF{Lw*W8BFam$w{bc3q7BJgCQ zm>g!W6tiUk_Q4lOk5tH;1W-6Pkz=J0ZL|@@_tn4wAdg$7kSf)gNXX_)74ZEUP@1Wd z5i}kqDF)}%^WWZ|@UclPjenXl=}L5o)0XZnN!KBI73&yem5U8h#1h|(mn>uYSw359 z_php+Rvf!p2U{%BlfStZqfK4B zGN&k78oF%`S*mi7Cql%Pst5+2jTVkU8dW#CFf%aTrUW0<26_vBev>0Fdk4vlT^)B_ z1kVF3&&WQF-{l&zGy>Cb;uPcL{CfdD1nY-8zcb(?nwufqzD-DGv#{<|Hn?_3px8?I7$&Eqo%aP^FSPU8I2R?d|B%e4Hg+XRyWGu9C|!+TNMK z%@$YJ*8HBjT&k^qp6_=~nApczyExD&qGrrKYP?*$@N2Op~ifBkt*Ahbxh zX?$Mzw%RtG8!>{a>zh{>hL0*79@EbRu2cBk!CSK)fv)3DlME6{ghOAkPv(_yB?wGo zBeeRRHT8NWDvId9&^7x!*LUNyda=6o4mj0VAcpRwY1la)C{HFn2E1ICp?y5f8_1kR zKm!?B8Xa@`@hAXu2JnUYTzA>}wB$Uku6w184tfS>`O8f!w)A#}y*5D_Vc-4tWr0jK zr{`Ln@QJ!!?`vwlyrWxc$;tpXCw%rt&FVZ6{LscCMp9>X`|Q=Xk#i3|iRG5|O=a&~ zd*!{uk+sp^{Z)200W5!VUY8%N^x7)dt|-D=&9iSpRc3Y&@JFtB`S?v$uqaUt=~#@W z6^Phyv@zg}Ad)$!_CwDOF6m}&+1j-j3=9ma4Z0)Y&>wuWtx;H)*bTSVmXk|j zVgoyMFt;m}v|xM~GB^08+sxzGAc+u{y6SVBk(?D~HQwfPU`nL?!0|bRaSMjVG zZh({Odp#55)|AM2Y{e~QWY*0i!s+QILfB5zT=mHhCj+?&O9-7mlTmL3T&7XOd)Rf? zcMkW{*Ya46UhM?{mU4wPu=E_!)WLolP4+tpYeU-7BW!fnT1^WT=Z{`w^ihd3N}yRr9|!1a+>igLQa!Ym76uh1^NIBZelF5wOz335=Qm*3#)Qo@T(oyFSnAvG@mMwJ2NN_$wRnwGUw&402ipguD%J_SMOtrC7E)g1hTwZwzKX!_ zWyxC;N?Yq($L(yON^9oFL-H@NsLAIrkGj~;YL=cPsd}qykINXXb6{*B$)sIA1XZQ$ zUW$qo?jFd`G*o@#?)#1t9v_ZFg~-;M71GY#f5^+O&dZesnNQ)mPsP~EWLpMbxVp5K zn0$CI^1%@C7Tf}6>9yb-5ENF*-sM*2AKxua?0M|2t@+6{W06zSe#;4xz{;)Tvrl`` z$7hd@wk@ojqUn$NTz&NtdEw@gz0u9Q01#S>uy9erF@KANFBTWyO2kOi7%gLc(k{YF zn}g+h0fI>qW7~9f1VNUG&ENGs$qWH9=O^Yhnou32QlY%s&QBRpF(hR|6&C3)&T`Rm z5MTIF85$X>uIowS#a2YLTt{6T-&ZuN4M1M{Gcc+fxaY-lDMl1%F5{Ttr}*Bm_4Y#^@b z1^&QDmb$K0i7^%(8;tUqBbf#w51n?)ed~TL8yCE62c5@v!OhTqhL@(5%I)3^cle&Z z>e$%@%G=*!n9Nw?wYxPTLf~1VrL(b9lNdUAQq5-9^aui*w62~%Lc>ch%e^!jCC?@0 z<+O3t$JF$ERAhu7zNRc1fL$J#t`Fa|17!+nxcYl=Q1{_wxonAzDJtv3RWEa5apG+Y zeVJBW41ma&Q19~CrI3xWaS7yXenWAwdb9DFy4e0`w#V{B&f#OX=y0WHohMc2&%S}S zvh5e9Fq`025t+za&Ndj&TB$=EANY$E|4>GtF1QXYx8Nf?g6A31J;`COwA2e-<^a<^7BtnOTLsiR9NpZqf|Jme&N%%%1{-TTx)!I z{J@#tqU{5HRtJ(RoV)UaA=)zSK5AJDWmaHbK!D%8LIOEFQ?c(TEBT#-5L0H49MAj) zC?Cer%_BuuUx)#dKlTZHqT)UEf+K>rk<)hyg~-rEAYG%2(c)7!v|An;4{eb#$LmM) z$rSfQ6unqWV+;8-eG-E2+h-e<@h{R(;-7}_5^`JSifjqZwFOK}QQne+OrrXFqf=>7 z#N*0qLE-jlb-%5q3p4r=Ltd;V9nTJq=5cfRmh~#NC0^|B!o*{@CUQ~1- z*~mYj!qvH1)+{F%QQA;cBEzL!mk2J_1#($yYj9&wtW>4e-E4DO1V>!Wv?WnA{DO?@ zyeuY1yI$&Lw7IIrT0UHspk28uxk5|VP^Z~g7X zmKH>41cYEq)z0`CEzhmsHfJ}|lh?(n5OstO)E6P?p5zOv-}1R3GMjVD zn>hL!4HXeZVyV7Mq5YpRkV#qEJV1-tHkAJ(%eXM-kdeVHWR?co4gy4r>P?63q9ZGj zw~9*KSLR^cgq^40;u34U+2z}8i!Bev28D0Bi>vHxBLk~6r3AD{N&0wRKk5KdLF`@Y zZEaVf%KRMrJw34opXmC##}wjXywOTLt`1VN`&Far@5cwTV@sT1e-K}TD8M7{;@$fW zq@%~*yWV3Xe`n41iMSl`6dkd?R`izt6++3vR}A{ZuGfUas$H*g`g<^Saf*lTb4;f1 z<^6qD&HvZldBruge0y9FkfMM!ARrLw#E2*g0wSSEks=T}0W@?qF-q?U2nP&Bnn*xF zij;)jOK1W?Ls7aAI!ZS*LBRj!@6mG)=RV!1`*8BKA11S>tk0U+Gwb_X9dWx=gKHJ{ zG2=~4b||>N=A}OJx4PZP$C{Zesj`j?7vg~XUD5vXJgbe)TW;2Xm%WoHXmkc1UX$g& zr3z+bAv26u$n4hHU{>dvFI!Uz+WkWL2K^8I2$9UXQbZ3LSy;=!>mIjLwr^Z<-&|fa zP*g~wf12cz;V>!eSacL_f6yzcQ>{Fd(hovO$FL0opQf-3#sQ&gUXdOX`F zx3Q~?O2*D1=iWK+arCh{#)D(k9A_DMBP69XK#srv~CwgxTyHa~E z<-~vV9p1am&fm3p+(yeP0tOqEHf&_|Slql8Rh9AX;$yfEG%YZsZ8ON_UQE7M&_{t2 z=Y!`SWcHBh$XAFGp@Ei!uEockHlM53Kc_{grPR7Ah2l+Co`iGFeX8G5JvpcrHj8^C zQ@j{gZM!7K)Ddmr7-KI2!oO{svs~3!!?oe$>mJ?jv;=Ot)2|hUs6fp_^%bS5(;_dM z5^c>`2`(b#kt-~+D0Z#H;Yx3%ON7-cM(a_Uq3dk)6E9_1tM9OIn~l|80`@M0VQN}x z9!xZBO2y~68=7GUlCLoGy-#8~7(RS-zVdX?NrkE}Ab-!`v+ZUz{s8BKsF;0yDQz;M zoZ%VukMmfg-Bk&%zI+%rLPC|DmBVGnJXeDd*(+9@R$4)IkB8#Y+do)Dqc*qZP{`>a zhx4@MdbMdz1zbnU8s-Ozs^+;OW)v%Shljuwr_1wz%T!SlFjd_i>Ekpm+uZXY!COG6 zSNZ&6J%^Q1Pn7jkjaNq>9pWGfa!Lx@f2)z1uq&e^?TTW7uHhVd5gIO?LHeb}2CFj7 zv{lV%;T=P#*VbO0Vr%oG*@3e@hvHHUndEkQBtFr2AzK)7fNfO}IMsGwksxLy%ONxs zYKP5f=9f!j7fs|`$au|-s@}+cns8PoEM9X>WK=f6Hm1>c37vU3B#Lw}gOS?puv6|a zp0DAQ=G`$$Utl)^1PH9=G`5^rNdmT*Mk_q;BNMp&d8=&_M+-$Mw{P9=vVT%a%tHvO zm-SpYTt1V2NeitQX>qMmhyFQno0(yNfkEMY&C27@PL9oG?it%EjFk(Byc@$C>G3mc^P4#2y}i0nL4UQ`FS5 zdA|@DHwv=^OidOqO^4-q6{BGItzM>T%&vqKAE)`sh1aKC)0va+s7?03cK=oJa$VLz z+Kte=DjI{ip9g?Aw?k2D5?>XUS+x$2E`7UC)Uu^CT%mVIdpVpGhsOwMC1&vkcU=2! zKH}R^qqMwOTJal#nIcCVHBY&~njoEVNsc8Uw+U}z(EU*ADsvVhTfcgYz8#NPGD!cn$MXa}2_H+q&O!gARmf0Sm)qwx4~o#$6g#EkF| z-`&kjOh?9Mupi)y&b`G7*9yCh6w{ya){(f-qRr@nl?Zi38ZQ%J#z2I}vmoxp2Zrb| zQ&$p^EVV^*Z7=}eYxvG=-AiH@UxrQ7CJsy6z1B!&MR7xd(_@wUL&v!h0TcoNsK$bB z;k)5_YxP^6=o{qR?+C0{1rgs8EOa-&yy$`P4N;0`?lB7hs>pJs;0Mz5rk1;}bl^G_lu2jWK#nY1`BxPmHiAO{-{?YMugwZ z_qKMn$z1O}fv<@V5gO#)iHN>i=mE}?)kNSUO~@MUTIp>zWldxaxR?Rbls@m5HxC3k zF}obax1{Lt%kyaYy%#xaV|EeQ`cjBVy2^QBP|B_{diM0Zm<=gGB|q)8$#9S*4o6JzX() zE-a}5$)zwj7`VP((jI_}ao{wJdGJ!kDcN2CqoMF}*nhn>$RrW^~~F{{!KG~39kFan~;-l;zf3e4f3*2elMtp>0xK9dL+9g+Wb zq@e*z04;R}r=lZJc5A!p;(TVNL~4({NY{}whWITtwWY=KN-2@b7t1e-(OtAszAz+A zS>#dA9yONixfoTJp`&kCXDUB9xUiX$#tPjjLou zlT)=j$c{5+WmT*8`uTmKF*JbcpgsrUbJs=tR44lH<%i6Ej0GQZm2>0ZvwEA?UTcZQ zJgHD%#?x*nq?%u}CaW$xXD-#2AF482neK=M^eNj`%QA9`MHdGH`-Ii;;!bzHNC9_k%3 zDdS8s$i(!z=d|~$zcC^t&-WK7UHdWr5Cw0<-~{}TEz$=o0sd@razZDmIxIfwx)r-0 z3+5{Tgw-D!RMQ)WZ+Z@1uSgpAN>|uuvZMn=H$6d;Au_i~8c|QKX07;33s*i+w#!U~ zZ;4OnrE92Xl5vxy*mg;0(xtMQo4_Q7@9BnDf zg?5LuQSlMGUPIQqkfOmxj2h954$Z-F-;39~7&wmKOdnDZ8FA!o-^({jgF4y?J|Q$e zB=kf9H?dmbj4EmA!R)A@rINnC(BczMG8?|;?1vFOY}-iYAglHPF^5y7+^ZxjKrY#! zlkXK^JGy5NX-&@`4Um&=rHo;Lk005!)5H3ilaIxg;PtKJj&>|Vq#0;RR1rvrw0HME zp-7n$k<8HnM1#T^0|DJBo^#1xCRP*y>?@ou64ik_z`ILi_7~mb+Zx(XDRDk+txnkI z1iHSbEOvs{6pENPI~c)GHMR|+*6)68NuiD;ZA=0iGR2_40P(Eg7r6g+>^H|kId+{r zJ0XEX#;x-IbDC=*{4ZPQRhYpKu)`3^+Dal>R&~`46 z-6`{PW~BfE`&~2+ch%(-UDsc7Nq=_i`b$7%k2xmlw(P#@X|Vu_NIb&Hb@MMx>Aitu z7opoWMN8f5HDEa6p;9p4Q&a{plOnq1g}D9W%;$3s2z$p19v2EXYP{s$jobumQyRIy z7`wz?jVh|Dq{4_ejo`zad#2kxlU|DfP$NJ8_Tc?!?!c#!Tyl)%$s5)wq} zwztE$typ(T#u{M2&~!-EX=~M~=YHI#0hdk#Skg5&+&a-poq^^w|PD z=>|Oi{zsZRL11#kHRB*u;7(N#z*sTg*Y7?5_I8g>KV4u9@nMNMa`qR*`b&N2sh=ea zUJRboI@$KjzzlTG6rvdq0a8Q#n$cfVMr2=hs_)6Vb#_XdkxP2?B%nQUgXm|Yv~LF0 zhy;2gK!5eMa!y71B2j|D|d&CT@8My1d88)aZX7#q#s z9RQFJAat*-H+a33rzipPJ+IOL6Hwgwvh zM$crHVrB_NR0}YHpsm0*0ZFOd8Tlfk`HyCuKMpi&YO1!-ZJ#L|TNVXippo7$L@Al4TPVH zk9WLXu+L`*up7|9Al={R2l@vz&;SB@-c#*uIKc!W$(JRPU$nopm85?p`*T}$jnh0J zkgEz4)ELlsCf_tE4A1y1`TTO&z9oq9G%dSX8EV990Z>DU@BD(S5Bi}iYr)T1WekJh z+%m-NlbKwAOs>Ba%J&|KcWC1jlpy??=i*+o6%?hPv^!f~tN@ODdoL{Afg$P; ztQXl9C4YwIFUj4XBfQMZ|1$b?SQ7Km_CZ;2oeeY7=7r0b*VRaiJG|qBjT=XMCOQuQ NKXnytWjxF(;6KuOjd%b6 literal 0 HcmV?d00001 diff --git a/docs/public/spry.svg b/docs/public/spry.svg new file mode 100644 index 0000000..c2a44fd --- /dev/null +++ b/docs/public/spry.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/examples/simple-io/.gitignore b/examples/simple-io/.gitignore new file mode 100644 index 0000000..3a85790 --- /dev/null +++ b/examples/simple-io/.gitignore @@ -0,0 +1,3 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ diff --git a/examples/simple-io/analysis_options.yaml b/examples/simple-io/analysis_options.yaml new file mode 100644 index 0000000..dee8927 --- /dev/null +++ b/examples/simple-io/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/examples/simple-io/app.dart b/examples/simple-io/app.dart new file mode 100644 index 0000000..9417200 --- /dev/null +++ b/examples/simple-io/app.dart @@ -0,0 +1,23 @@ +import 'dart:io'; + +import 'package:spry/spry.dart'; +import 'package:spry/io.dart'; + +void main() async { + // Create an Spry app instance. + final app = Spry(); + + // Add a new route that matches GET requests to / path + app.get('/', (event) => "⚡️ Tadaa!"); + + // Creates a HttpServer listen on data handler. + final handler = const IOPlatform().createHandler(app); + + // Create an HttpServer. + final server = await HttpServer.bind('127.0.0.1', 3000); + + // Listen requests of handler. + server.listen(handler); + + print('🚀 HTTP server listen on http://127.0.0.1:3000'); +} diff --git a/examples/simple-io/pubspec.lock b/examples/simple-io/pubspec.lock new file mode 100644 index 0000000..0c7544e --- /dev/null +++ b/examples/simple-io/pubspec.lock @@ -0,0 +1,28 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + lints: + dependency: "direct dev" + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + routingkit: + dependency: transitive + description: + name: routingkit + sha256: cbd2b711f534cfc7596e938e68dae6dd22b4e5bddb4f8b5bee0e9f1ff9f54823 + url: "https://pub.dev" + source: hosted + version: "1.1.0" + spry: + dependency: "direct main" + description: + path: "../../packages/spry" + relative: true + source: path + version: "4.0.0-dev.2" +sdks: + dart: ">=3.4.3 <4.0.0" diff --git a/examples/simple-io/pubspec.yaml b/examples/simple-io/pubspec.yaml new file mode 100644 index 0000000..2f416cb --- /dev/null +++ b/examples/simple-io/pubspec.yaml @@ -0,0 +1,15 @@ +name: simple_io +description: Spry simple IO example app. +version: 0.0.0 +publish_to: none + +environment: + sdk: ^3.4.3 + +# Add regular dependencies here. +dependencies: + spry: + path: ../../packages/spry + +dev_dependencies: + lints: ^3.0.0 diff --git a/package.json b/package.json index d85aa42..df11859 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,6 @@ "docs:preview": "vitepress preview docs" }, "dependencies": { - "vitepress": "^1.1.4" + "vitepress": "^1.3.0" } } diff --git a/packages/spry/lib/src/http/response.dart b/packages/spry/lib/src/http/response.dart index ef7106a..1a0c965 100644 --- a/packages/spry/lib/src/http/response.dart +++ b/packages/spry/lib/src/http/response.dart @@ -26,12 +26,17 @@ abstract interface class Response implements HttpMessage { final Headers headers = const Headers(), final Encoding encoding = utf8, }) { + final bytes = switch (encoding.encode(body)) { + Uint8List bytes => bytes, + List bytes => Uint8List.fromList(bytes), + }; + return _ResponseImpl( - Stream.value(Uint8List.fromList(encoding.encode(body))), + Stream.value(bytes), status: status, statusText: statusText, headers: headers - .resetOf('content-length', body.length.toString()) + .resetOf('content-length', bytes.lengthInBytes.toString()) .resetOf('content-type', 'text/plain; charset=${encoding.name}'), ); } @@ -44,7 +49,10 @@ abstract interface class Response implements HttpMessage { final Headers headers = const Headers(), final Encoding encoding = utf8, }) { - final bytes = Uint8List.fromList(encoding.encode(json.encode(body))); + final bytes = switch (encoding.encode(json.encode(body))) { + Uint8List bytes => bytes, + List bytes => Uint8List.fromList(bytes), + }; return _ResponseImpl( Stream.value(bytes), From ff8637be23789ba7bc266944c8637b2a099538dc Mon Sep 17 00:00:00 2001 From: Seven Du Date: Wed, 10 Jul 2024 19:45:41 +0800 Subject: [PATCH 32/35] chore: WIP --- docs/.vitepress/config.mts | 4 ++++ docs/guide/app.md | 15 +++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 docs/guide/app.md diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index c6bfaca..0f481d9 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -41,6 +41,10 @@ export default defineConfig({ text: "Getting Started", link: "/guide/getting-started", }, + { + text: "Basics", + items: [], + }, ], }, socialLinks: [ diff --git a/docs/guide/app.md b/docs/guide/app.md new file mode 100644 index 0000000..9542ad8 --- /dev/null +++ b/docs/guide/app.md @@ -0,0 +1,15 @@ +--- +title: Guide → App instance +--- + +# App Instance + +Spry 的核心是 `app` 实例。它是处理传入请求的核心处理器。你可以使用应用程序实例来注册事件处理程序。 + +## 初始化应用程序 + +你可以使用 `Spry` 的默认工厂来创建一个新的 Spry 应用程序实例: + +```dart +final app = Spry(); +``` From c46e196519c85d1fb6d62ee8066a18eba884ffcf Mon Sep 17 00:00:00 2001 From: Seven Du Date: Thu, 11 Jul 2024 01:55:48 +0800 Subject: [PATCH 33/35] chore: WIP --- docs/.vitepress/config.mts | 68 +++- docs/advanced/cookies.md | 155 +++++++++ docs/{guide => }/getting-started.md | 4 +- docs/guide/app.md | 113 ++++++- docs/guide/event.md | 68 ++++ docs/guide/handler.md | 126 +++++++ docs/guide/routing.md | 337 +++++++++++++++++++ docs/guide/websocket/hooks.md | 60 ++++ docs/guide/websocket/introduction.md | 33 ++ docs/guide/websocket/message.md | 19 ++ docs/guide/websocket/peer.md | 39 +++ docs/guide/what-is-spry.md | 5 - docs/index.md | 6 +- docs/platforms/create.md | 44 +++ docs/platforms/index.md | 0 docs/platforms/io.md | 66 ++++ docs/platforms/plain.md | 83 +++++ docs/what-is-spry.md | 5 + examples/README.md | 2 + packages/spry/lib/io.dart | 8 +- packages/spry/lib/src/http/response.dart | 20 ++ packages/spry/lib/src/platform/platform.dart | 4 +- packages/spry/lib/src/spry.dart | 2 +- packages/spry/lib/src/utils/next.dart | 2 +- packages/spry/lib/websocket.dart | 6 +- packages/spry_cookie/README.md | 2 +- 26 files changed, 1233 insertions(+), 44 deletions(-) create mode 100644 docs/advanced/cookies.md rename docs/{guide => }/getting-started.md (95%) create mode 100644 docs/guide/event.md create mode 100644 docs/guide/handler.md create mode 100644 docs/guide/routing.md create mode 100644 docs/guide/websocket/hooks.md create mode 100644 docs/guide/websocket/introduction.md create mode 100644 docs/guide/websocket/message.md create mode 100644 docs/guide/websocket/peer.md delete mode 100644 docs/guide/what-is-spry.md create mode 100644 docs/platforms/create.md delete mode 100644 docs/platforms/index.md create mode 100644 docs/platforms/io.md create mode 100644 docs/platforms/plain.md create mode 100644 docs/what-is-spry.md diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 0f481d9..2a064a0 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -21,32 +21,66 @@ export default defineConfig({ nav: [ { text: "Guide", - link: "/guide/what-is-spry", - activeMatch: "^/guide/.*?", + items: [ + { text: "App", link: "/guide/app" }, + { text: "Routing", link: "/guide/routing" }, + { text: "Handler", link: "/guide/handler" }, + { text: "Event", link: "/guide/event" }, + { text: "WebSocket", link: "/guide/websocket/introduction" }, + ], }, { text: "Platforms", - link: "/platforms/", - activeMatch: "^/platforms/.*?", + items: [ + { text: "Plain", link: "/platforms/plain" }, + { text: "IO (dart:io)", link: "/platforms/io" }, + ], }, { text: "Examples", link: "https://github.com/medz/spry/tree/main/examples", }, ], - sidebar: { - "/guide": [ - { text: "What is Spry?", link: "/guide/what-is-spry" }, - { - text: "Getting Started", - link: "/guide/getting-started", - }, - { - text: "Basics", - items: [], - }, - ], - }, + sidebar: [ + { text: "What is Spry?", link: "/what-is-spry" }, + { + text: "Getting Started", + link: "/getting-started", + }, + { + text: "Basics", + items: [ + { text: "App", link: "/guide/app" }, + { text: "Routing", link: "/guide/routing" }, + { text: "Handler", link: "/guide/handler" }, + { text: "Event", link: "/guide/event" }, + ], + }, + { + text: "WebSocket", + items: [ + { text: "Introduction", link: "/guide/websocket/introduction" }, + { text: "Hooks", link: "/guide/websocket/hooks" }, + { text: "Peer", link: "/guide/websocket/peer" }, + { text: "Message", link: "/guide/websocket/message" }, + ], + }, + { + text: "Advanced", + items: [{ text: "Cookies", link: "/advanced/cookies" }], + }, + { + text: "Platforms", + items: [ + { + text: "Create a new platform", + link: "/platforms/create", + }, + { text: "Plain", link: "/platforms/plain" }, + { text: "IO (dart:io)", link: "/platforms/io" }, + ], + }, + ], socialLinks: [ { icon: "github", link: "https://github.com/medz/spry" }, { diff --git a/docs/advanced/cookies.md b/docs/advanced/cookies.md new file mode 100644 index 0000000..3a71493 --- /dev/null +++ b/docs/advanced/cookies.md @@ -0,0 +1,155 @@ +--- +title: Advanced → Cookies +--- + +# Cookies + +[![Pub Version](https://img.shields.io/pub/v/spry_cookie.svg)](https://pub.dev/packages/spry_cookie) + +An HTTP cookie is a small piece of data stored by the user's browser. Cookies were designed to be a reliable mechanism for websites to remember stateful information. When the user visits the website again, the cookie is automatically sent with the request. + +## Integration + +Spry is more focused on routing and APIs servers and does not have built-in support for Cookies. + +Install Spry cookies support package (`spry_cookie`): + +```bash +dart pub add spry_cookie +``` + +Or, update your `pubspec.yaml` file: + +```dart +dependencies: + spry_cookie: +``` + +## Usege + +Spry Cookies supports global and single handler mode. + +### Global Support Cookies + +```dart +import 'package:spry_cookie/spry_cookie.dart'; + +app.use(cookie()); +``` + +### Only single handler + +Wrap a closure handler with `cookieWith`: + +```dart +import 'package:spry_cookie/spry_cookie.dart'; + +app.get('/user', cookieWith((event) { + // ... +})); +``` + +## Sign/Unsign cookies + +Spray Cookie supports signed and unsigned cookies. You only need to configure the security key and hash algorithm: + +```dart +app.use(cookie( + secret: "Your cookie sign secret", +)); +``` + +By default, the `SHA-256` hash algorithm is used. If you want to customize it, please set `algorithm`: + +```dart +import 'package:crypto/crypto.dart'; + +app.use(cookie( + secret: "Your cookie sign secret", + algorithm: md5, // Set algorithm to MD5 +)); +``` + +## Automatic `secure` settings + +使用 `autoSecureSet` 选项,当 Handler 中未设置 `secure` 时候,会判断当前请求是否是 `https` 自动设置: + +```dart +app.use(cookie( + autoSecureSet: true +)); +``` + +## Cookie options + +### `domain` + +Specifies the value for the [Domain Set-Cookie attribute](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.3). By default, no domain is set, and most clients will consider the cookie to apply to only the current domain. + +### `expires` + +Specifies the `DateTime` to be the value for the [Expires Set-Cookie attribute](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.1). By default, no expiration is set, and most clients will consider this a "non-persistent cookie" and will delete it on a condition like exiting a web browser application. + +::: tip + +the [cookie storage model specification](https://datatracker.ietf.org/doc/html/rfc6265#section-5.3) states that if both `expires` and `maxAge` are set, then `maxAge` takes precedence, but it is possible not all clients by obey this, so if both are set, they should point to the same date and time. + +::: + +### `httpOnly` + +Specifies the boolean value for the [HttpOnly Set-Cookie attribute](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.6). When truthy, the `HttpOnly` attribute is set, otherwise it is not. By default, the `HttpOnly` attribute is not set. + +### `maxAge` + +Specifies the `int` (in seconds) to be the value for the [Max-Age Set-Cookie attribute](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2). The given number will be converted to an integer by rounding down. By default, no maximum age is set. + +### `partitioned` + +Specifies the `bool?` value for the [Partitioned Set-Cookie attribute](https://datatracker.ietf.org/doc/html/draft-cutler-httpbis-partitioned-cookies#section-2.1). When truthy, the `Partitioned` attribute is set, otherwise it is not. By default, the `Partitioned` attribute is not set. + +### `path` + +Specifies the value for the [Path Set-Cookie attribute](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.4). By default, the path is considered the ["default path"](https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4). + +### `secure` + +Specifies the boolean value for the [Secure Set-Cookie attribute](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.5). When truthy, the `Secure` attribute is set, otherwise it is not. By default, the `Secure` attribute is not set. + +### `sameSite` + +Specifies the `SameSite` enum to be the value for the [SameSite Set-Cookie attribute](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-09#section-5.4.7). + +* `SameSite.lax`: will set the `SameSite` attribute to `Lax` for lax same site enforcement. +* `SameSite.none`: will set the `SameSite` attribute to `None` for an explicit cross-site cookie. +* `SameSite.strict`: will set the `SameSite` attribute to `Strict` for strict same site enforcement. + +## Managing cookies + +We extend the `Event` object to add a `cookies` object to manage Cookies: + +```dart +app.use((event) { + print(event.cookie.get("user_id")); +}); +``` + +### `event.cookies.get` + +Gets a Request/Response cookie value. + +### `event.cookies.getAll` + +Gets all Request/Response cookies list. + +### `event.cookies.set` + +Sets a new cookie. + +### `event.cookies.delete` + +Deletes a cookie. + +### `event.cookies.serialize` + +Serialize a cookie. diff --git a/docs/guide/getting-started.md b/docs/getting-started.md similarity index 95% rename from docs/guide/getting-started.md rename to docs/getting-started.md index 11ffe4f..6a2dd40 100644 --- a/docs/guide/getting-started.md +++ b/docs/getting-started.md @@ -1,5 +1,5 @@ --- -title: Guide → Getting Started +title: Getting Started --- # Getting Started @@ -25,7 +25,7 @@ Creates a new file `app.dart`(or `main.dart` | `server.dart`): ::: code-group -<<< ../../examples/simple-io/app.dart +<<< ../examples/simple-io/app.dart ::: diff --git a/docs/guide/app.md b/docs/guide/app.md index 9542ad8..665f796 100644 --- a/docs/guide/app.md +++ b/docs/guide/app.md @@ -1,15 +1,118 @@ --- -title: Guide → App instance +title: Basics → App (Spry) --- -# App Instance +# App (Spry) -Spry 的核心是 `app` 实例。它是处理传入请求的核心处理器。你可以使用应用程序实例来注册事件处理程序。 +The heart of Spry is the `app` instance. It is the core handler for incoming requests. You can use the application instance to register event handlers. -## 初始化应用程序 +## Initialize the application -你可以使用 `Spry` 的默认工厂来创建一个新的 Spry 应用程序实例: +You can use the default factory of `Spry` to create a new Spry application instance: ```dart final app = Spry(); ``` + +There are some additional options supported when initializing the application: + +### `locals` + +`locals` is an initialization data with a `Map` type, which defaults to `null`. It exists to hold the instance of the entire App extension function. When your application has written extension functions locally and needs to be initialized for global sharing, setting it is very useful. + +When the initialization of App is completed, it will be converted to a [Locals](TOTO) object. Here is a simple demonstration of globally shared data: + +```dart +final app = Spry(locals: { + 'name': 'Seven', +}); + +final app.use((event) { + print(event.locals.get('nane')); // Seven + + return next(); +}); +``` + +### `router` + +Spry uses the Radix-Tree router implemented in [RoutingKit](https://pub.dev/packages/routingkit) by default. You can also implement your own Router for Spry to use. Just make it satisfy the `Router` signature: + +```dart +import 'package:routingkit/routingkit.dart'; + +class MyRouter implements Router { + ... +} + +final app = Spry(router: MyRouter()); +``` + +### `routerDriver` + +This is a custom Router implementation provided by [RoutingKit](https://pub.dev/packages/routingkit). When you pass it to Spry when using other RoutingKit directories, Spry will use this driver to create Router instances. + +### `caseSensitive` + +This option tells Router whether to distinguish between upper and lower case paths. The default is `false` (ignore case). If you use Spry for other scenarios, you may need it. + +```dart +final app = Spry(caseSensitive: false); +``` + +## Adding Routes + +`addRoute` is the basic method for the entire Core to implement and add routing handlers: + +```dart +app.addRoute(, , ); +``` + +::: tip +For more information, please see [Basics → Routing](/guide/routing) +::: + +## Adding Handlers + +Spry is capable of stack (Onion, growing from the inside out) processing. Each time `addHandler` is used, it will be added to the innermost layer of the nested layer. When calling, use them one by one in the order of addition. + +```dart +app.addHandler(ClosureHandler((event) => ...)); +``` + +Each added layer has the ability to return `Response` independently. This will result in that if a layer directly returns a `Response` object, the later added layer will not be called. +Of course, this is intentional, just like peeling an onion, we only need to peel specific layers, and there is no need to peel the onion layer by layer. + +If a layer wants to call the following layer, it needs to use `next` to implement it: + +```dart +MyHandler implements Handler { + Future handle(Event event) async { + print('Before'); + final res = await next(event); + print('After'); + + return res; + } +} +``` + +## `.use(...)` + +`addHandler` is a low API, it is not easy to use. So you can use `use` in Spry to quickly add a Handler without implementing the `Handler` interface: + +```dart +app.use((Event event) { + print('Hi'); + + return next(event); +}); +``` + +## Fallback + +After we run the Spry server, there is always a possibility of encountering a route that is not registered. Spry uses a `404` response by default. If you want to customize the implementation of a Handler that cannot find the path, please use `fallback`: + +```dart +app.fallback((event) => 404); +``` diff --git a/docs/guide/event.md b/docs/guide/event.md new file mode 100644 index 0000000..28edf0e --- /dev/null +++ b/docs/guide/event.md @@ -0,0 +1,68 @@ +--- +title: Basics → Event +--- + +# Event + +Every time a new HTTP request comes, Spry internally creates an Event object and passes it though event handlers until sending the response. + +An event is passed through all the lifecycle hooks and composable utils to use it as context. + +Example: + +```dart +app.use((event) { + console.log('Request: ${event.method} ${event.uri.toString()}'); + + return next(event); // Call next handler. +}); +``` + +## `event.app` + +Returns the Spry instance for the request event. + +## `event.locals` + +A container for passing values ​​in the Handlers stack. + +## `event.request` + +Spry abstract Request request object. + +## `event.uri` + +The requested URI for the request event. + +If the request URI is absolute (e.g. 'https://www.example.com/foo') then +it is returned as-is. Otherwise, the returned URI is reconstructed by +using the request URI path (e.g. '/foo') and HTTP header fields. + +To reconstruct the scheme, the 'X-Forwarded-Proto' header is used. + +To reconstruct the host, the 'X-Forwarded-Host' header is used. If it is +not present then the 'Host' header is used. If neither is present then +the host name of the server is used. + +## `event.getClientAddress()` + +Returns client address, value formated of `:port`. + +The returned value comes from the Platform implementation. +if the platform does not support it, an empty string will be returned. + +## `event.handlers` + +Access to the normalized request handlers. + +## `event.method` + +Access to the normalized (uppercase) request method. + +## `event.params` + +Returns the [Params](/guide/routing#params) of dynamic routing. + +## `event.route?` + +Return [Route], when the route has not yet started matching or has not been matched to return null, usually this situation is when the route is registered and has entered the fallback processor. diff --git a/docs/guide/handler.md b/docs/guide/handler.md new file mode 100644 index 0000000..26f7d7e --- /dev/null +++ b/docs/guide/handler.md @@ -0,0 +1,126 @@ +--- +title: Basics → Handler +--- + +# Handler + +After creating an app instance, you can start defining your application logic using event handlers. +An event handler is a function that receive an `Event` instance and returns a response. You can compare it to controllers in other frameworks. + +## Defining event handlers + +You can define event handlers using `Handler` interface. + +```dart +class MyHandler implements Handler { + Future handle(Event event) async { + return Response.text('Result string'); + } +} +``` + +But usually you don't need to do this. Because Spry's logic registration API provides a simpler closure type method: + +```dart +app.use((event) { + return 'Result string'; +}); +``` + +## Responsible + +Values returned from event handlers are automatically converted to responses. It can be: + +* JSON serializable value. If returning a JSON object or serializable value, it will be stringified and sent with default `application/json` content-type. +* `String`/`num`/`bool`: Sent as-is using default `text/plain` content-type. +* `Map`/`List`: Sent as-is using default `application/json` content-type. +* `null`/`void`: Spry with end response with `204 - No Content` status code. +* Any `Object` include `toJson()` method: Sent as-is using default `application/json` content-type. +* `Stream>` +* `Responsible` + +Any of above values could also be wrapped in a `Future`. This means that you can return a `Future` from your event handler and Spry will wait for it to resolve before sending the response. + +**Example**: Send text response: + +```dart +app.use((event) => 'Hello, Spry!'); +app.use((event) => 1); +app.use((event) => 2.1); +app.use((event) => true); +``` + +**Example**: Send JSON response: + +```dart +app.use((event) => {"url": event.uri.toString()}); +app.use((event) => [1, 2, 3]); + +class User { + late String name; + + toJson() => {'name': name}; +} +app.use((event) => User()..name = 'Bob'); +``` + +**Example**: Send a Future value: + +```dart +app.use((event) { + final completer = Completer(); + Timer(Duration(seconds: 1), () { + completer.complete('One second later'); + }); + + return completer.future; +}); +``` + +**Example**: Send a Stream: + +```dart +app.use((event) { + return File("foo.txt").openRead(); +}); +``` + +## Handlers stack + +In Spry, we abandoned the so-called middleware design and designed the concept of Handlers stack, where each layer interrupts the call by default. + +```dart +app.use((event) => 'value'); +app.use((event) => 'value 2'); // Will never be executed! +``` + +The app expects that each Handler call will return a value, so the Handler needs to actively tell the app to call the next Handler (using the `next` function): + +```dart +app.use((event) { + print(1); + + return next(event); +}); +app.use((event) { + print(2); +}); + +// console: 1, 2 +``` + +It can also perform functions similar to After-middleware: + +```dart +app.use((event) async { + final res = next(event); + print(1); + + return res; +}); +app.use((event) { + print(2); +}); + +// console: 2, 1 +``` diff --git a/docs/guide/routing.md b/docs/guide/routing.md new file mode 100644 index 0000000..7a1be65 --- /dev/null +++ b/docs/guide/routing.md @@ -0,0 +1,337 @@ +--- +title: Basics → Routing +--- + +# Routing + +Routing is the process of finding an appropriate request handler for an incoming request. The routing core of Spry is a high-performance Radix-Tree router based on [RoutingKit](https://pub.dev/packages/routingkit). + +[[toc]] + +## Overview + +To understand how routing works in Spry, first you should understand the basics about HTTP requests. Take a look at the sample request below: + +```http +GET /hello/spry HTTP/1.1 +host: spry.fun +content-length: 0 +``` + +This is a simple `GET` HTTP request to `/hello/spry`. If you enter the URL below into your browser's address bar, your browser will send this request. + +```http +http://spry.fun/hello/spry +``` + +### HTTP Method + +The first part of the request is the HTTP method. `GET` is the most common HTTP method, here are some other common HTTP methods: + +- `GET`: Get a resource +- `POST`: Create a resource +- `PUT`: Update a resource +- `DELETE`: Delete a resource +- `PATCH`: Update some properties of a resource + +### Request Path + +After the HTTP method is the request's URI. It consists of a path starting with `/` and an optional query string after `?`. The HTTP method and path are what Spry uses to route requests. + +### Router Methods + +Let's see how this request is handled in Spry: + +```dart +app.get('/hello/spry', (event) { + return 'Hello, Spry!'; +}); +``` + +All common HTTP methods can be used as methods of `Spry`. They accept a string representing the path, separated by `/`. +Note that `Spry` and `RoutesBuilder` do not build in all HTTP methods, you can use `on` to write manually: + +```dart +app.on("get", /hello/spry, (event) => "Hello, Spry!"); +``` + +After registering this route, the sample HTTP request above will get the following HTTP response: + +```http +HTTP/1.1 200 OK +content-length: 12 +content-type: text/plain; charset=utf-8 + +Hello, Spry! +``` + +### Route Parameters + +Now that we've successfully routed requests based on HTTP method and path, let me try to make the path dynamic. + +::: warning + +The `spry` name is hardcoded in both the path and the response. Let's make it dynamic so that you can access `/hello/` and get a response. + +::: + +```dart +app.get('/hello/:name', (event) { + final name = request.params.get("name"); + return 'Hello, $name!'; +}); +``` + +By using a path segment prefixed with `:` we indicate to the route that this is a dynamic path parameter. Now, any string provided here will match this route. We can then access the value of the string using `event.params`. + +If we run the sample request again, you will still get a response greeting `spry`. But now you can add any name after `/hello/` and see it in the response. Let's try `/hello/dart`. + +::: code-group + +```http [request] +GET /hello/dart HTTP/1.1 +content-length: 0 +``` + +```http [response] +HTTP/1.1 200 OK +content-length: 12 + +Hello, dart! +``` + +::: + +Now that you know the basics, check out the other sections to learn more. + +## Routes + +### Methods + +You can use a variety of HTTP method helpers to register routes directly to your Spry `Spry`: + +```dart +app.get("/foo/bar", (event) { + // ... +}); +``` + +Route handlers support you to return any `Responsible` content, including `String`, `Map`, `List`, `File`, `Stream`, etc. + +You can also specify the type of the return value of the route handler through the `T` type parameter: + +```dart +app.get("/foo", (event) { + return "bar"; +}); +``` + +This is a list of built-in HTTP methods: + +- `get` +- `post` +- `put` +- `patch` +- `delete` +- `head` + +Procesing HTTP method helpers, there is also an `on` function that accepts the HTTP method as an input parameter: + +```dart +app.on( + method: "get", + path: "/foo/bar", + (event) => { ... }, +); +``` + +### Path Segments + +Each route registration method accepts a string representation of `Segment`, and has the following four situations: + +- Constant (`foo`) +- Parameter (`:foo`) +- Anything (`*`) +- Catchall (`**`) + +#### Constant Segment + +This is a static Segment, only requests with an exact match string at this location are allowed. + +```dart +app.get("/foo/bar", (event) { + // ... +}); +``` + +#### Parameter Segment + +This is a parameter Segment, any string at this location will be allowed. Parameter Segment is specified with a `:` prefix, the string after `:` will be used as the parameter name. + +```dart +app.get("/foo/:bar", (event) { + // ... +}); +``` + +#### Anything Segment + +This is the same as the Parameter Segment, except that the parameter value is discarded. It is specified with a `*` prefix. + +```dart +app.get("/foo/*/baz", (event) { + // ... +}); +``` + +#### Catchall Segement + +This is a dynamic route component that matches one or more Segments, specified with `**`. Any string in the request will be allowed to match this location or after this location. + +```dart +// GET /foo/bar +// GET /foo/bar/baz +// ... +app.get("/foo/**", (event) { + // ... +}); +``` + +### Params + +When using parameter Segment (prefixed with `:`), the URI value for that location will be stored in `event.params`. You can access the value using the name in Path Sgements: + +```dart +app.get("/foo/:bar", (event) { + final bar = event.params("bar"); + // ... +}); +``` + +:: tip + +We can be sure that `event.params(...)` will never return `null` here because our path contains `:bar`. But if the parameter is processed in advance by middleware or other programs, we need to consider the `null` situation. + +::: + +Values matched via Catchall (`**`) or Anything (`*`) Segment will be stored in `request.params` as `Iterable`. You can access them using `request.params.catchall`: + +```dart +app.get("/foo/**", (event) { + final catchall = request.params.catchall; + // ... +}); +``` + +::: tip + +If your path contains multiple Parameter Segments, such as `/foo/:bar/:bar`, you use `event.params('bar')` only returns the first value, you can use `event.params.valuesOf('bar')` to get all values. + +::: + +### Body + +You can read the stream data of the request directly. + +```dart +app.post("/foo", (event) { + return event.request.body +}); +``` + +You can also send a `Stream` as the body of the response when the Handler returns `Stream`: + +```dart +app.post("/foo", (event) { + return File("foo.txt").openRead(); +}); +``` + +Of course, you can return any data without calling `write` or other methods of `request.response`. It supports `String`, `Map`, `List`, `File`, `Stream>`, etc. Of course, you can return an instance that implements `Responsible`. + +## Route Groups + +Route grouping allows you to create a group of routes with specific route prefixes or specific handlers. The grouping function supports both builder and closure syntax. + +All grouping methods return a `RoutesBuilder` instance, which means you can infinitely mix, match, and nest groups with other route building methods. + +::: tip + +Route groups can help you better organize your routes, but they are not required. + +::: + +### Path Prefix + +Path prefix routing groups allow you to add a prefix path before a routing group. + +```dart +final users = app.groupd(route: "/users"); + +// GET /users +users.get("/", (event) => ...); + +// POST /users +users.post("/", (event) => ...); + +// GET /users/:id +users.get("/:id", (event) => ...); +``` + +Any path component you can pass to helper methods such as `get`, `post` can be passed to `groupd`. +There is another syntax based on closures: + +```dart +app.group(route: "/users", (routes) { + // GET /users + routes.get("/", (event) => ...); + + // POST /users + routes.post("/", (event) => ...); + + // GET /users/:id + routes.get(":id", (event) => ...); +}); +``` + +Nested path prefixes allow you to define your CRUD API more concisely: + +```dart +app.group(route: "/users", (users) { + // GET /users + users.get('/', (event) => ...); + // POST /users + users.post('/', (event) => ...); + + users.group(path: ":id", (user) { + // GET /users/:id + user.get('/', (event) => ...); + // PUT /users/:id + user.put('/', (event) => ...); + // DELETE /users/:id + user.delete('/', (event) => ...); + }); +}); +``` + +### Handlers stack + +In addition to path prefixes, routing groups also allow you to add handlers to routing groups. + +```dart +app.get('fast-thing', (event) => ...); +app.group(uses: [SlowHandler()], (routes) { + routes.get('slow-thing', (event) => ...); +}); +``` + +This is particularly useful for protecting a subset of routes with different authentication handler. + +```dart +app.post('/login', (event) => ...); + +final auth = app.groupd(uses: [AuthHandler()]); + +auth.get('/profile', (event) => ...); +auth.get('/logout', (event) => ...); +``` diff --git a/docs/guide/websocket/hooks.md b/docs/guide/websocket/hooks.md new file mode 100644 index 0000000..95010e4 --- /dev/null +++ b/docs/guide/websocket/hooks.md @@ -0,0 +1,60 @@ +--- +title: WebSocket → Hooks +description: Using WebSocket hooks API, you can define a WebSocket server that works across runtimes with same synax. +--- + +# Hooks + +Using WebSocket hooks API, you can define a WebSocket server that works across runtimes with same synax. + +--- + +Spry WebSocket provides a cross-platform API to define WebSocket servers. An implementation with these hooks works across runtimes without needing you to go into details of any of them (while you always have the power to control low-level hooks). You can only define the life-cycle hooks that you only need and only those will be called on runtime. + +::: warning +Spry WebSocket API is still under development and can change. +::: + +```dart +import 'package:spry/websocket.dart'; + +class MyHooks implements Hooks { + @override + FutureOr fallback(Event event) { + // If the platform does not support WebSocket or the upgrade fails, + // it will be called. + print('[ws] Not support.'); + return const Response(null, status: 426); + } + + @override + FutureOr onClose(Peer peer, {int? code, String? reason}) { + // Received a hook from a connected client or actively closed the websocket + // call on the server side. + print('[ws] close'); + } + + @override + FutureOr onError(Peer peer, error) { + // Hook for errors from the server side. + print('[ws] error: ${Error.safeToString(error)}'); + } + + @override + FutureOr onMessage(Peer peer, Message message) { + // Hook when receiving messages from connected clients. + final text = message.text(); + print('[ws] message: $text'); + + if (text.contains('ping')) { + peer.sendText('pong'); + } + } + + @override + FutureOr onUpgrade(Event event) { + // Called when upgrading request to WebSocket + return CreatePeerOptions(...); + } +} +``` diff --git a/docs/guide/websocket/introduction.md b/docs/guide/websocket/introduction.md new file mode 100644 index 0000000..dce5e81 --- /dev/null +++ b/docs/guide/websocket/introduction.md @@ -0,0 +1,33 @@ +--- +title: WebSocket → Introduction +--- + +# Introduction + +Writing a real-time WebSocket server that works across different WebSocket runtimes is challenging because there is no single standard for WebSocket servers. You usually need to learn a lot of details about different API implementations, which also makes switching from one runtime to another expensive. Spry WebSocket is the solution to this problem! + +## Basic using... + +```dart +import 'package:spry/websocket.dart'; + +app.ws('/chat/rooms/:id', chatRoomHooks); +``` + +## Platform runtime + +`package:spry/websocket.dart` exports a mixin for `WebSocketPlatform` that only works on `Platform`. + +If the platform implementation you are using does not implement 'WebSocketPlatform', then your app will not support WebSockets. + +## `app.ws` + +This is a method for extending the Spry app instance to register WebSocket [Hooks](/guide/websocket/hooks): + +```dart +class ChatHooks implements Hooks { + ... +} + +app.ws('chat', ChatHooks()); +``` diff --git a/docs/guide/websocket/message.md b/docs/guide/websocket/message.md new file mode 100644 index 0000000..76645e4 --- /dev/null +++ b/docs/guide/websocket/message.md @@ -0,0 +1,19 @@ +--- +title: WebSocket → Peer +--- + +# Message + +On message [hook](/guide/websocket/hooks), you receive a message object containing an incoming message from the client. + +## `message.text()` + +Get stringified `String` version of the message. + +## `message.bytes()` + +Get stringified `Uint8List` version of the message. + +## `message.raw` + +Message raw data, Types: `Uint8List` or `String`. diff --git a/docs/guide/websocket/peer.md b/docs/guide/websocket/peer.md new file mode 100644 index 0000000..f37d5d3 --- /dev/null +++ b/docs/guide/websocket/peer.md @@ -0,0 +1,39 @@ +--- +title: WebSocket → Peer +--- + +# Peer + +Peer object allows easily interacting with connected clients. + +--- + +Websocket hooks accept a peer instance as their first argument. You can use peer object to get information about each connected client or send a message to them. + +## `peer.readyState` + +Client connection status (might be `-1`) + +::: tip +Read more is [readyState in MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState) +::: + +## `peer.protocol` + +Returns the websocket selected protocol. + +## `peer.extensions` + +Returns the websocket cliend-side request extensions. + +## `peer.send` + +Send a bytes message to the connected client. + +## `peer.sendText` + +Send a `String` message to the connected client. + +## `peer.close` + +Close websocket connect. diff --git a/docs/guide/what-is-spry.md b/docs/guide/what-is-spry.md deleted file mode 100644 index 7c9136c..0000000 --- a/docs/guide/what-is-spry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Guide → What is Spry? ---- - - diff --git a/docs/index.md b/docs/index.md index 98e9a27..e9a6387 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,10 +8,10 @@ hero: image: /code.png actions: - text: What is Spry? - link: /guide/what-is-spry + link: /what-is-spry - theme: alt text: Getting Started - link: /guide/getting-started + link: /getting-started - theme: alt text: View on GitHub link: https://github.com/medz/spry @@ -21,7 +21,7 @@ features: details: Your code is implemented through the platform and can be compiled into any runtime for your application. - icon: ✨ title: Small & Tree-shakable - details: Spry core is supper lightweight & tree-shakable, Only the extensions you use will be included in the final bundle. + details: Spry core is super lightweight & tree-shakable, Only the extensions you use will be included in the final bundle. - icon: 🧩 title: Composable details: Extend your application and add capabilities, Your codebase will scale with your project. diff --git a/docs/platforms/create.md b/docs/platforms/create.md new file mode 100644 index 0000000..3165a7b --- /dev/null +++ b/docs/platforms/create.md @@ -0,0 +1,44 @@ +--- +title: Platforms → Create a new platform +--- + +# Platform + +Run Spry everywhere using platforms. + +--- + +The app instance of Spry is lightweight without any logic about runtime it is going to run. Using Spry `Platform`, we can easily integrate server with each runtime. + +There are 2 base platforms: + +* [**Plain**](/platforms/plain) +* [**IO(`dart:io`)**](/platfrms/io) + +## Create a new platform + +To create a new platform support, we only need to implement the `Platform` interface: + +```dart +typedef Input = ; +typedef Output = ; + +class MyPlatform extends Platform { + ... +} +``` + +## WebSocket support + +In general, WebSocket support is optional. If your platform supports WebSocket, you only need to `with WebSocketPlatform` on your platform implementation: + +```dart +class MyPlatform + extends Platform + with WebSocketPlatform +{ + FutureOr websocket(Event event, Input request, Hooks hooks) { + // Your platform upgrading websocket logic. + } +} +``` diff --git a/docs/platforms/index.md b/docs/platforms/index.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/platforms/io.md b/docs/platforms/io.md new file mode 100644 index 0000000..95efb38 --- /dev/null +++ b/docs/platforms/io.md @@ -0,0 +1,66 @@ +--- +title: Platforms → IO (dart:io) +--- + +# IO + +Natively run Spry app with `dart:io` HTTP server. + +--- + +To listen to `HttpServer` and enable Spry app, convert Spry app to `dart:io` HTTP server listener using `IOPlatform` platform. + +## Usage + +First, create an Spry app: + +::: code-group +```dart [app.dart] +import 'package:spry/spry.dart'; + +final Spry app = () { + final app = Spry(); + app.use((event) => 'hello world!'); + + return app; +}(); +``` +::: + +Create HTTP server entry: + +::: code-group +```dart [server.dart] +import 'package:spry/spry.dart'; + +void main() async { + final server = await HttpServer.bind('127.0.0.1', 3000); + final handler = const IOPlatform().createHandler(app); + + server.listen(handler); + + print('🚀 HTTP server listen on http://127.0.0.1:3000'); +} +``` +::: + +Now, you can run you Spry app natively with `dart:io`: + +```bash +dart run server.dart +``` + +## Compile to executable program + +The IO platform allows you to compile to binary executable programs using the `dart compile exe` command: + +```bash +dart compile exe server.dart -o server +``` + +Start the server: + +```bash +./server +# console: 🚀 HTTP server listen on http://127.0.0.1:3000 +``` diff --git a/docs/platforms/plain.md b/docs/platforms/plain.md new file mode 100644 index 0000000..ef4db51 --- /dev/null +++ b/docs/platforms/plain.md @@ -0,0 +1,83 @@ +--- +title: Platforms → Plain +--- + +# Plain + +Run Spry app into any unknown runtime! + +--- + +Using plain adapter you can have an object input/output interface. + +::: tip +This can be also be particularly useful for testing your app or running inside lambda-like environments. +::: + +::: warning +Plain platform not support websocket. +::: + +## Usage + +First, create Spry app entry: + +::: code-group +```dart [app.dart] +import 'package:spry/spry.dart'; + +final Spry app = () { + final app = Spry(); + app.use((event) => 'hello world!'); + + return app; +}(); +``` +::: + +Create plain entry: + +::: code-group +```dart [plain.dart] +import 'package:spry/plain.dart'; +import 'app.dart'; + +final handler = const PlainPlatform().createHandler(app); +``` +::: + +## Local testing + +You can test platform using any runtime: + +::: code-group +```dart [plain_test.dart] +import 'package:spry/plain.dart'; +import 'package:test/test.dart'; +import 'plain.dart'; + +void main() { + test('Basic request', () async { + final request = PlainRequest(method: 'get', uri: Uri(path: '/')); + final response = await handler(request); + + expect(response.status, 200); + expect(response.headers.get('content-type'), contains('text/plain')); + expect(await response.text(), contains('hello world')); + }); +} +``` +::: + +The response example JSON (**This is not a real return, but just a visual demonstration of data**): + +```json +{ + status: 200, + statusText: "OK", + headers: { + "content-type": "text/plain; charset=utf8" + }, + body: "hello world!" +} +``` diff --git a/docs/what-is-spry.md b/docs/what-is-spry.md new file mode 100644 index 0000000..66528ac --- /dev/null +++ b/docs/what-is-spry.md @@ -0,0 +1,5 @@ +--- +title: What is Spry? +--- + + diff --git a/examples/README.md b/examples/README.md index 86e1eef..98e1028 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1 +1,3 @@ # Spry Examples + +* [`simple-io`](https://github.com/medz/spry/tree/main/examples/simple-io): Spry simple IO example app. diff --git a/packages/spry/lib/io.dart b/packages/spry/lib/io.dart index e002e01..51b4e80 100644 --- a/packages/spry/lib/io.dart +++ b/packages/spry/lib/io.dart @@ -11,10 +11,8 @@ import 'websocket.dart' hide CompressionOptions; const _kUpgradedWebSocket = #spry.io.upgraded.websocket; /// `dart:io` platform. -class IOPlatform - implements - Platform, - WebSocketPlatform { +class IOPlatform extends Platform + with WebSocketPlatform { const IOPlatform(); @override @@ -77,7 +75,7 @@ class IOPlatform } @override - websocket(Event event, HttpRequest request, Hooks hooks) async { + websocket(Event event, HttpRequest request, Hooks hooks) async { if (event.locals.getOrNull(_kUpgradedWebSocket) == true) { throw HttpException('The current request has been upgraded to WebSocket', uri: event.uri); diff --git a/packages/spry/lib/src/http/response.dart b/packages/spry/lib/src/http/response.dart index 1a0c965..db512ca 100644 --- a/packages/spry/lib/src/http/response.dart +++ b/packages/spry/lib/src/http/response.dart @@ -65,6 +65,26 @@ abstract interface class Response implements HttpMessage { ); } + /// Create redirect response. + factory Response.redirect( + Uri location, { + int status = 307, + String? statusText, + Headers headers = const Headers(), + }) { + const allowStatus = [300, 301, 302, 303, 304, 305, 306, 307, 308]; + if (!allowStatus.contains(status)) { + status = 307; + } + + return Response( + null, + status: status, + statusText: statusText ?? status.httpStatusReasonPhrase, + headers: headers, + ); + } + /// Response status. int get status; diff --git a/packages/spry/lib/src/platform/platform.dart b/packages/spry/lib/src/platform/platform.dart index 2553e74..24b512b 100644 --- a/packages/spry/lib/src/platform/platform.dart +++ b/packages/spry/lib/src/platform/platform.dart @@ -5,7 +5,9 @@ import '../http/headers/headers.dart'; import '../http/response.dart'; /// Spry platform interface. -abstract interface class Platform { +abstract class Platform { + const Platform(); + /// Gets a client address. /// /// If platform not support, returns a empty string. diff --git a/packages/spry/lib/src/spry.dart b/packages/spry/lib/src/spry.dart index 3c489f7..3ec54c8 100644 --- a/packages/spry/lib/src/spry.dart +++ b/packages/spry/lib/src/spry.dart @@ -8,7 +8,7 @@ import 'types.dart'; class Spry implements RoutesBuilder { const Spry._({required this.locals, required this.router}); - /// Creates a new Spry application/ + /// Creates a new Spry application. factory Spry({ final Map? locals, final Router? router, diff --git a/packages/spry/lib/src/utils/next.dart b/packages/spry/lib/src/utils/next.dart index 7717214..2a4dfdf 100644 --- a/packages/spry/lib/src/utils/next.dart +++ b/packages/spry/lib/src/utils/next.dart @@ -29,6 +29,6 @@ Future next(Event event) async { return switch (effect) { Future Function(Event) handle => handle(event), - _ => const Response(null), + _ => const Response(null, status: 204), }; } diff --git a/packages/spry/lib/websocket.dart b/packages/spry/lib/websocket.dart index ac1513b..b550568 100644 --- a/packages/spry/lib/websocket.dart +++ b/packages/spry/lib/websocket.dart @@ -154,18 +154,18 @@ class CreatePeerOptions { /// then you should use it. /// /// ```dart -/// class MyPlatform implements Platform, WebSocketPlatform { +/// class MyPlatform extends Platform with WebSocketPlatform { /// // ... /// } /// ``` -abstract interface class WebSocketPlatform { +mixin WebSocketPlatform on Platform { /// Upgrading websocket. /// /// The return value can be any data you need, for example, in the event /// of a failure, you can return a [Response] or the result of a fallback call. /// Due to the different contents returned by different platforms, the return /// value depends on your implementation. - FutureOr websocket(Event event, T request, Hooks hooks); + FutureOr websocket(Event event, T request, Hooks hooks); } /// WebSocket hooks interface. diff --git a/packages/spry_cookie/README.md b/packages/spry_cookie/README.md index 4e3e5c8..61d784d 100644 --- a/packages/spry_cookie/README.md +++ b/packages/spry_cookie/README.md @@ -1,7 +1,7 @@ # Spry cookie [![Test](https://github.com/medz/spry/actions/workflows/test.yml/badge.svg)](https://github.com/medz/spry/actions/workflows/test.yml) -[![Pub Version](https://img.shields.io/pub/v/spry.svg)](https://pub.dev/packages/spry) +[![Pub Version](https://img.shields.io/pub/v/spry_cookie.svg)](https://pub.dev/packages/spry_cookie) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/medz/spry/blob/main/LICENSE) [![X (twitter)](https://img.shields.io/badge/twitter-%40shiweidu-blue.svg)](https://twitter.com/shiweidu) [![Documentation](https://img.shields.io/badge/docs-spry.fun-brightgreen.svg)](https://spry.fun) From 8cc22317fa5410c839177d75d49775bc0dcc8695 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Thu, 11 Jul 2024 01:58:42 +0800 Subject: [PATCH 34/35] chore: WIP --- CHANGELOG.md | 14 ++++++-------- packages/spry/pubspec.yaml | 2 +- packages/spry_cookie/pubspec.yaml | 4 ++-- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80d14e2..ac7b5c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,22 +1,20 @@ -# Spry v4.0.0-dev.3 +# Spry v4.0.0-0 & Spry cookie v0.0.1 -To install Spry v4.0.0-dev.3 run this command: +To install Spry v4.0.0-0 & Spry cookie v0.0.1 run this command: ```bash -dart pub add spry:^4.0.0-dev.3 +dart pub add spry:^4.0.0 +dart pub add spry_cookie:^0.0.1 ``` Or update your `pubspec.yaml` file: ```yaml dependencies: - spry: ^4.0.0-dev.3 + spry: ^4.0.0 + spry_cookie: ^0.0,1 ``` -## What's Changed - -* fixed text response length calculation error. - # Spry v4.0.0-dev.2 To install Spry v4.0.0-dev.2 run this command: diff --git a/packages/spry/pubspec.yaml b/packages/spry/pubspec.yaml index c0fe135..693d2f5 100644 --- a/packages/spry/pubspec.yaml +++ b/packages/spry/pubspec.yaml @@ -1,6 +1,6 @@ name: spry description: Spry is a lightweight, composable Dart web framework designed to work collaboratively with various runtime platforms. -version: 4.0.0-dev.2 +version: 4.0.0 homepage: https://spry.fun repository: https://github.com/medz/spry diff --git a/packages/spry_cookie/pubspec.yaml b/packages/spry_cookie/pubspec.yaml index 3eaf03e..7435d40 100644 --- a/packages/spry_cookie/pubspec.yaml +++ b/packages/spry_cookie/pubspec.yaml @@ -1,6 +1,6 @@ name: spry_cookie description: Cookies Support for Spry. -version: 0.0.0-dev.0 +version: 0.0.1 homepage: https://spry.fun/cookies repository: https://github.com/medz/spry @@ -10,7 +10,7 @@ environment: dependencies: crypto: ^3.0.3 http_parser: ^4.1.0 - spry: ^4.0.0-dev + spry: ^4.0.0 dev_dependencies: lints: ^4.0.0 From dfccceae78c2ea0d408328180754d83bbf274186 Mon Sep 17 00:00:00 2001 From: Seven Du Date: Thu, 11 Jul 2024 02:02:42 +0800 Subject: [PATCH 35/35] docs: fix link --- README.md | 4 +--- docs/guide/app.md | 2 +- docs/platforms/create.md | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4908178..d7edd69 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,6 @@ Spry framework is an [MIT licensed](https://github.com/medz/spry/blob/main/LICEN ## Contributing -We welcome contributions! Please read our [contributing guide](CONTRIBUTING.md) to learn about our development process, how to propose bugfixes and improvements, and how to build and test your changes to Prisma. - -Thank you to all the people who already contributed to Odroe! +Thank you to all the people who already contributed to Spry! [![Contributors](https://contrib.rocks/image?repo=medz/spry)](https://github.com/odroe/prisma-dart/graphs/contributors) diff --git a/docs/guide/app.md b/docs/guide/app.md index 665f796..7cf8be0 100644 --- a/docs/guide/app.md +++ b/docs/guide/app.md @@ -20,7 +20,7 @@ There are some additional options supported when initializing the application: `locals` is an initialization data with a `Map` type, which defaults to `null`. It exists to hold the instance of the entire App extension function. When your application has written extension functions locally and needs to be initialized for global sharing, setting it is very useful. -When the initialization of App is completed, it will be converted to a [Locals](TOTO) object. Here is a simple demonstration of globally shared data: +When the initialization of App is completed, it will be converted to a `Locals` object. Here is a simple demonstration of globally shared data: ```dart final app = Spry(locals: { diff --git a/docs/platforms/create.md b/docs/platforms/create.md index 3165a7b..705739c 100644 --- a/docs/platforms/create.md +++ b/docs/platforms/create.md @@ -13,7 +13,7 @@ The app instance of Spry is lightweight without any logic about runtime it is go There are 2 base platforms: * [**Plain**](/platforms/plain) -* [**IO(`dart:io`)**](/platfrms/io) +* [**IO(`dart:io`)**](/platforms/io) ## Create a new platform