Compare commits
6 commits
1b2983d02b
...
3b83e34dcc
Author | SHA1 | Date | |
---|---|---|---|
|
3b83e34dcc | ||
|
e12b15f8c8 | ||
|
d884105ded | ||
|
9148934aa6 | ||
|
78e6f36d2c | ||
|
be65590d2e |
203 changed files with 30742 additions and 0 deletions
71
packages/container/container/.gitignore
vendored
Normal file
71
packages/container/container/.gitignore
vendored
Normal file
|
@ -0,0 +1,71 @@
|
|||
# See https://www.dartlang.org/tools/private-files.html
|
||||
|
||||
# Files and directories created by pub
|
||||
.dart_tool
|
||||
.packages
|
||||
.pub/
|
||||
build/
|
||||
|
||||
# If you're building an application, you may want to check-in your pubspec.lock
|
||||
pubspec.lock
|
||||
|
||||
# Directory created by dartdoc
|
||||
# If you don't generate documentation locally you can remove this line.
|
||||
doc/api/
|
||||
|
||||
### Dart template
|
||||
# See https://www.dartlang.org/tools/private-files.html
|
||||
|
||||
# Files and directories created by pub
|
||||
|
||||
# SDK 1.20 and later (no longer creates packages directories)
|
||||
|
||||
# Older SDK versions
|
||||
# (Include if the minimum SDK version specified in pubsepc.yaml is earlier than 1.20)
|
||||
.project
|
||||
.buildlog
|
||||
**/packages/
|
||||
|
||||
|
||||
# Files created by dart2js
|
||||
# (Most Dart developers will use pub build to compile Dart, use/modify these
|
||||
# rules if you intend to use dart2js directly
|
||||
# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
|
||||
# differentiate from explicit Javascript files)
|
||||
*.dart.js
|
||||
*.part.js
|
||||
*.js.deps
|
||||
*.js.map
|
||||
*.info.json
|
||||
|
||||
# Directory created by dartdoc
|
||||
|
||||
# Don't commit pubspec lock file
|
||||
# (Library packages only! Remove pattern if developing an application package)
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff:
|
||||
|
||||
## VsCode
|
||||
.vscode/
|
||||
|
||||
## File-based project format:
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
.idea/
|
||||
/out/
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
12
packages/container/container/AUTHORS.md
Normal file
12
packages/container/container/AUTHORS.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
Primary Authors
|
||||
===============
|
||||
|
||||
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
|
||||
|
||||
Thomas is the current maintainer of the code base. He has refactored and migrated the
|
||||
code base to support NNBD.
|
||||
|
||||
* __[Tobe O](thosakwe@gmail.com)__
|
||||
|
||||
Tobe has written much of the original code prior to NNBD migration. He has moved on and
|
||||
is no longer involved with the project.
|
151
packages/container/container/CHANGELOG.md
Normal file
151
packages/container/container/CHANGELOG.md
Normal file
|
@ -0,0 +1,151 @@
|
|||
# Change Log
|
||||
|
||||
## 8.1.1
|
||||
|
||||
* Updated repository link
|
||||
|
||||
## 8.1.0
|
||||
|
||||
* Updated `lints` to 3.0.0
|
||||
* Fixed analyser warnings
|
||||
|
||||
## 8.0.0
|
||||
|
||||
* Require Dart >= 3.0
|
||||
|
||||
## 7.1.0-beta.2
|
||||
|
||||
* Require Dart >= 2.19
|
||||
* Refactored `EmptyReflector`
|
||||
|
||||
## 7.1.0-beta.1
|
||||
|
||||
* Require Dart >= 2.18
|
||||
* Moved `defaultErrorMessage` to `ContainerConst` class to resolve reflectatable issue.
|
||||
* Added `hashCode`
|
||||
|
||||
## 7.0.0
|
||||
|
||||
* Require Dart >= 2.17
|
||||
|
||||
## 6.0.0
|
||||
|
||||
* Require Dart >= 2.16
|
||||
* Removed `error`
|
||||
|
||||
## 5.0.0
|
||||
|
||||
* Skipped release
|
||||
|
||||
## 4.0.0
|
||||
|
||||
* Skipped release
|
||||
|
||||
## 3.1.1
|
||||
|
||||
* Updated `_ReflectedMethodMirror` to have optional `returnType` parameter
|
||||
* Updated `Container` to handle non nullable type
|
||||
|
||||
## 3.1.0
|
||||
|
||||
* Updated linter to `package:lints`
|
||||
|
||||
## 3.0.2
|
||||
|
||||
* Resolved static analysis warnings
|
||||
|
||||
## 3.0.1
|
||||
|
||||
* Updated README
|
||||
|
||||
## 3.0.0
|
||||
|
||||
* Migrated to support Dart >= 2.12 NNBD
|
||||
|
||||
## 2.0.0
|
||||
|
||||
* Migrated to work with Dart >= 2.12 Non NNBD
|
||||
|
||||
## 1.1.0
|
||||
|
||||
* `pedantic` lints.
|
||||
* Add `ThrowingReflector`, which throws on all operations.
|
||||
* `EmptyReflector` uses `Object` instead of `dynamic` as its returned
|
||||
type, as the `dynamic` type is (apparently?) no longer a valid constant value.
|
||||
* `registerSingleton` now returns the provided `object`.
|
||||
* `registerFactory` and `registerLazySingleton` now return the provided function `f`.
|
||||
|
||||
## 1.0.4
|
||||
|
||||
* Slight patch to prevent annoying segfault.
|
||||
|
||||
## 1.0.3
|
||||
|
||||
* Added `Future` support to `Reflector`.
|
||||
|
||||
## 1.0.2
|
||||
|
||||
* Added `makeAsync<T>`.
|
||||
|
||||
## 1.0.1
|
||||
|
||||
* Added `hasNamed`.
|
||||
|
||||
## 1.0.0
|
||||
|
||||
* Removed `@GenerateReflector`.
|
||||
|
||||
## 1.0.0-alpha.12
|
||||
|
||||
* `StaticReflector` now defaults to empty arguments.
|
||||
|
||||
## 1.0.0-alpha.11
|
||||
|
||||
* Added `StaticReflector`.
|
||||
|
||||
## 1.0.0-alpha.10
|
||||
|
||||
* Added `Container.registerLazySingleton<T>`.
|
||||
* Added named singleton support.
|
||||
|
||||
## 1.0.0-alpha.9
|
||||
|
||||
* Added `Container.has<T>`.
|
||||
|
||||
## 1.0.0-alpha.8
|
||||
|
||||
* Fixed a bug where `_ReflectedTypeInstance.isAssignableTo` always failed.
|
||||
* Added `@GenerateReflector` annotation.
|
||||
|
||||
## 1.0.0-alpha.7
|
||||
|
||||
* Add `EmptyReflector`.
|
||||
* `ReflectedType.newInstance` now returns a `ReflectedInstance`.
|
||||
* Moved `ReflectedInstance.invoke` to `ReflectedFunction.invoke`.
|
||||
|
||||
## 1.0.0-alpha.6
|
||||
|
||||
* Add `getField` to `ReflectedInstance`.
|
||||
|
||||
## 1.0.0-alpha.5
|
||||
|
||||
* Remove concrete type from `ReflectedTypeParameter`.
|
||||
|
||||
## 1.0.0-alpha.4
|
||||
|
||||
* Safely handle `void` return types of methods.
|
||||
|
||||
## 1.0.0-alpha.3
|
||||
|
||||
* Reflecting `void` in `MirrorsReflector` now forwards to `dynamic`.
|
||||
|
||||
## 1.0.0-alpha.2
|
||||
|
||||
* Added `ReflectedInstance.reflectee`.
|
||||
|
||||
## 1.0.0-alpha.1
|
||||
|
||||
* Allow omission of the first argument of `Container.make`, to use
|
||||
a generic type argument instead.
|
||||
* `singleton` -> `registerSingleton`
|
||||
* Add `createChild`, and support hierarchical containers.
|
29
packages/container/container/LICENSE
Normal file
29
packages/container/container/LICENSE
Normal file
|
@ -0,0 +1,29 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2021, dukefirehawk.com
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
45
packages/container/container/README.md
Normal file
45
packages/container/container/README.md
Normal file
|
@ -0,0 +1,45 @@
|
|||
# Angel3 Container
|
||||
|
||||
![Pub Version (including pre-releases)](https://img.shields.io/pub/v/angel3_container?include_prereleases)
|
||||
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
|
||||
[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion)
|
||||
[![License](https://img.shields.io/github/license/dart-backend/angel)](https://github.com/dart-backend/angel/tree/master/packages/container/angel_container/LICENSE)
|
||||
|
||||
A better IoC container for Angel3, ultimately allowing Angel3 to be used with or without `dart:mirrors` package.
|
||||
|
||||
```dart
|
||||
import 'package:angel3_container/mirrors.dart';
|
||||
import 'package:angel3_framework/angel3_framework.dart';
|
||||
import 'package:angel3_framework/http.dart';
|
||||
|
||||
@Expose('/sales', middleware: [process1])
|
||||
class SalesController extends Controller {
|
||||
@Expose('/', middleware: [process2])
|
||||
Future<String> route1(RequestContext req, ResponseContext res) async {
|
||||
return "Sales route";
|
||||
}
|
||||
}
|
||||
|
||||
bool process1(RequestContext req, ResponseContext res) {
|
||||
res.write('Hello, ');
|
||||
return true;
|
||||
}
|
||||
|
||||
bool process2(RequestContext req, ResponseContext res) {
|
||||
res.write('From Sales, ');
|
||||
return true;
|
||||
}
|
||||
|
||||
void main() async {
|
||||
// Using Mirror Reflector
|
||||
var app = Angel(reflector: MirrorsReflector());
|
||||
|
||||
// Sales Controller
|
||||
app.container.registerSingleton<SalesController>(SalesController());
|
||||
await app.mountController<SalesController>();
|
||||
|
||||
var http = AngelHttp(app);
|
||||
var server = await http.startServer('localhost', 3000);
|
||||
print("Angel3 server listening at ${http.uri}");
|
||||
}
|
||||
```
|
1
packages/container/container/analysis_options.yaml
Normal file
1
packages/container/container/analysis_options.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
include: package:lints/recommended.yaml
|
75
packages/container/container/example/main.dart
Normal file
75
packages/container/container/example/main.dart
Normal file
|
@ -0,0 +1,75 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:platform_container/platform_container.dart';
|
||||
import 'package:platform_container/mirrors.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
// Create a container instance.
|
||||
var container = Container(const MirrorsReflector());
|
||||
|
||||
// Register a singleton.
|
||||
container.registerSingleton<Engine>(Engine(40));
|
||||
|
||||
// You can also omit the type annotation, in which the object's runtime type will be used.
|
||||
// If you're injecting an abstract class, prefer the type annotation.
|
||||
//
|
||||
// container.registerSingleton(Engine(40));
|
||||
|
||||
// Register a factory that creates a truck.
|
||||
container.registerFactory<Truck>((container) {
|
||||
return _TruckImpl(container.make<Engine>());
|
||||
});
|
||||
|
||||
// Use `make` to create an instance.
|
||||
var truck = container.make<Truck>();
|
||||
|
||||
// You can also resolve injections asynchronously.
|
||||
container.registerFactory<Future<int>>((_) async => 24);
|
||||
print(await container.makeAsync<int>());
|
||||
|
||||
// Asynchronous resolution also works for plain objects.
|
||||
await container.makeAsync<Truck>().then((t) => t.drive());
|
||||
|
||||
// Register a named singleton.
|
||||
container.registerNamedSingleton('the_truck', truck);
|
||||
|
||||
// Should print: 'Vroom! I have 40 horsepower in my engine.'
|
||||
truck.drive();
|
||||
|
||||
// Should print the same.
|
||||
container.findByName<Truck>('the_truck').drive();
|
||||
|
||||
// We can make a child container with its own factory.
|
||||
var childContainer = container.createChild();
|
||||
|
||||
childContainer.registerFactory<Truck>((container) {
|
||||
return _TruckImpl(Engine(5666));
|
||||
});
|
||||
|
||||
// Make a truck with 5666 HP.
|
||||
childContainer.make<Truck>().drive();
|
||||
|
||||
// However, calling `make<Engine>` will return the Engine singleton we created above.
|
||||
print(childContainer.make<Engine>().horsePower);
|
||||
}
|
||||
|
||||
abstract class Truck {
|
||||
void drive();
|
||||
}
|
||||
|
||||
class Engine {
|
||||
final int horsePower;
|
||||
|
||||
Engine(this.horsePower);
|
||||
}
|
||||
|
||||
class _TruckImpl implements Truck {
|
||||
final Engine engine;
|
||||
|
||||
_TruckImpl(this.engine);
|
||||
|
||||
@override
|
||||
void drive() {
|
||||
print('Vroom! I have ${engine.horsePower} horsepower in my engine.');
|
||||
}
|
||||
}
|
6
packages/container/container/example/throwing.dart
Normal file
6
packages/container/container/example/throwing.dart
Normal file
|
@ -0,0 +1,6 @@
|
|||
import 'package:platform_container/platform_container.dart';
|
||||
|
||||
void main() {
|
||||
var reflector = const ThrowingReflector();
|
||||
reflector.reflectClass(StringBuffer);
|
||||
}
|
1
packages/container/container/lib/mirrors.dart
Normal file
1
packages/container/container/lib/mirrors.dart
Normal file
|
@ -0,0 +1 @@
|
|||
export 'src/mirrors/mirrors.dart';
|
9
packages/container/container/lib/platform_container.dart
Normal file
9
packages/container/container/lib/platform_container.dart
Normal file
|
@ -0,0 +1,9 @@
|
|||
library angel3_container;
|
||||
|
||||
export 'src/container.dart';
|
||||
export 'src/empty/empty.dart';
|
||||
export 'src/static/static.dart';
|
||||
export 'src/exception.dart';
|
||||
export 'src/reflector.dart';
|
||||
export 'src/throwing.dart';
|
||||
export 'src/container_const.dart';
|
239
packages/container/container/lib/src/container.dart
Normal file
239
packages/container/container/lib/src/container.dart
Normal file
|
@ -0,0 +1,239 @@
|
|||
import 'dart:async';
|
||||
import 'exception.dart';
|
||||
import 'reflector.dart';
|
||||
|
||||
class Container {
|
||||
final Reflector reflector;
|
||||
final Map<Type, dynamic> _singletons = {};
|
||||
final Map<Type, dynamic Function(Container)> _factories = {};
|
||||
final Map<String, dynamic> _namedSingletons = {};
|
||||
final Container? _parent;
|
||||
|
||||
Container(this.reflector) : _parent = null;
|
||||
|
||||
Container._child(Container this._parent) : reflector = _parent.reflector;
|
||||
|
||||
bool get isRoot => _parent == null;
|
||||
|
||||
/// Creates a child [Container] that can define its own singletons and factories.
|
||||
///
|
||||
/// Use this to create children of a global "scope."
|
||||
Container createChild() {
|
||||
return Container._child(this);
|
||||
}
|
||||
|
||||
/// Determines if the container has an injection of the given type.
|
||||
bool has<T>([Type? t]) {
|
||||
var t2 = T;
|
||||
if (t != null) {
|
||||
t2 = t;
|
||||
} else if (T == dynamic && t == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Container? search = this;
|
||||
while (search != null) {
|
||||
if (search._singletons.containsKey(t2)) {
|
||||
return true;
|
||||
} else if (search._factories.containsKey(t2)) {
|
||||
return true;
|
||||
} else {
|
||||
search = search._parent;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Determines if the container has a named singleton with the given [name].
|
||||
bool hasNamed(String name) {
|
||||
Container? search = this;
|
||||
|
||||
while (search != null) {
|
||||
if (search._namedSingletons.containsKey(name)) {
|
||||
return true;
|
||||
} else {
|
||||
search = search._parent;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Instantiates an instance of [T], asynchronously.
|
||||
///
|
||||
/// It is similar to [make], but resolves an injection of either
|
||||
/// `Future<T>` or `T`.
|
||||
Future<T> makeAsync<T>([Type? type]) {
|
||||
var t2 = T;
|
||||
if (type != null) {
|
||||
t2 = type;
|
||||
}
|
||||
|
||||
Type? futureType; //.Future<T>.value(null).runtimeType;
|
||||
|
||||
if (T == dynamic) {
|
||||
try {
|
||||
futureType = reflector.reflectFutureOf(t2).reflectedType;
|
||||
} on UnsupportedError {
|
||||
// Ignore this.
|
||||
}
|
||||
}
|
||||
|
||||
if (has<T>(t2)) {
|
||||
return Future<T>.value(make(t2));
|
||||
} else if (has<Future<T>>()) {
|
||||
return make<Future<T>>();
|
||||
} else if (futureType != null) {
|
||||
return make(futureType);
|
||||
} else {
|
||||
throw ReflectionException(
|
||||
'No injection for Future<$t2> or $t2 was found.');
|
||||
}
|
||||
}
|
||||
|
||||
/// Instantiates an instance of [T].
|
||||
///
|
||||
/// In contexts where a static generic type cannot be used, use
|
||||
/// the [type] argument, instead of [T].
|
||||
T make<T>([Type? type]) {
|
||||
Type t2 = T;
|
||||
if (type != null) {
|
||||
t2 = type;
|
||||
}
|
||||
|
||||
Container? search = this;
|
||||
|
||||
while (search != null) {
|
||||
if (search._singletons.containsKey(t2)) {
|
||||
// Find a singleton, if any.
|
||||
return search._singletons[t2] as T;
|
||||
} else if (search._factories.containsKey(t2)) {
|
||||
// Find a factory, if any.
|
||||
return search._factories[t2]!(this) as T;
|
||||
} else {
|
||||
search = search._parent;
|
||||
}
|
||||
}
|
||||
|
||||
var reflectedType = reflector.reflectType(t2);
|
||||
var positional = [];
|
||||
var named = <String, Object>{};
|
||||
|
||||
if (reflectedType is ReflectedClass) {
|
||||
bool isDefault(String name) {
|
||||
return name.isEmpty || name == reflectedType.name;
|
||||
}
|
||||
|
||||
var constructor = reflectedType.constructors.firstWhere(
|
||||
(c) => isDefault(c.name),
|
||||
orElse: (() => throw ReflectionException(
|
||||
'${reflectedType.name} has no default constructor, and therefore cannot be instantiated.')));
|
||||
|
||||
for (var param in constructor.parameters) {
|
||||
var value = make(param.type.reflectedType);
|
||||
|
||||
if (param.isNamed) {
|
||||
named[param.name] = value;
|
||||
} else {
|
||||
positional.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
return reflectedType.newInstance(
|
||||
isDefault(constructor.name) ? '' : constructor.name,
|
||||
positional,
|
||||
named, []).reflectee as T;
|
||||
} else {
|
||||
throw ReflectionException(
|
||||
'$t2 is not a class, and therefore cannot be instantiated.');
|
||||
}
|
||||
}
|
||||
|
||||
/// Shorthand for registering a factory that injects a singleton when it runs.
|
||||
///
|
||||
/// In many cases, you might prefer this to [registerFactory].
|
||||
///
|
||||
/// Returns [f].
|
||||
T Function(Container) registerLazySingleton<T>(T Function(Container) f,
|
||||
{Type? as}) {
|
||||
return registerFactory<T>(
|
||||
(container) {
|
||||
var r = f(container);
|
||||
container.registerSingleton<T>(r, as: as);
|
||||
return r;
|
||||
},
|
||||
as: as,
|
||||
);
|
||||
}
|
||||
|
||||
/// Registers a factory. Any attempt to resolve the
|
||||
/// type within *this* container will return the result of [f].
|
||||
///
|
||||
/// Returns [f].
|
||||
T Function(Container) registerFactory<T>(T Function(Container) f,
|
||||
{Type? as}) {
|
||||
Type t2 = T;
|
||||
if (as != null) {
|
||||
t2 = as;
|
||||
}
|
||||
|
||||
if (_factories.containsKey(t2)) {
|
||||
throw StateError('This container already has a factory for $t2.');
|
||||
}
|
||||
|
||||
_factories[t2] = f;
|
||||
return f;
|
||||
}
|
||||
|
||||
/// Registers a singleton. Any attempt to resolve the
|
||||
/// type within *this* container will return [object].
|
||||
///
|
||||
/// Returns [object].
|
||||
T registerSingleton<T>(T object, {Type? as}) {
|
||||
Type t2 = T;
|
||||
if (as != null) {
|
||||
t2 = as;
|
||||
} else if (T == dynamic) {
|
||||
t2 = as ?? object.runtimeType;
|
||||
}
|
||||
//as ??= T == dynamic ? as : T;
|
||||
|
||||
if (_singletons.containsKey(t2)) {
|
||||
throw StateError('This container already has a singleton for $t2.');
|
||||
}
|
||||
|
||||
_singletons[t2] = object;
|
||||
return object;
|
||||
}
|
||||
|
||||
/// Finds a named singleton.
|
||||
///
|
||||
/// In general, prefer using [registerSingleton] and [registerFactory].
|
||||
///
|
||||
/// [findByName] is best reserved for internal logic that end users of code should
|
||||
/// not see.
|
||||
T findByName<T>(String name) {
|
||||
if (_namedSingletons.containsKey(name)) {
|
||||
return _namedSingletons[name] as T;
|
||||
} else if (_parent != null) {
|
||||
return _parent.findByName<T>(name);
|
||||
} else {
|
||||
throw StateError(
|
||||
'This container does not have a singleton named "$name".');
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers a *named* singleton.
|
||||
///
|
||||
/// Note that this is not related to type-based injections, and exists as a mechanism
|
||||
/// to enable injecting multiple instances of a type within the same container hierarchy.
|
||||
T registerNamedSingleton<T>(String name, T object) {
|
||||
if (_namedSingletons.containsKey(name)) {
|
||||
throw StateError('This container already has a singleton named "$name".');
|
||||
}
|
||||
|
||||
_namedSingletons[name] = object;
|
||||
return object;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
class ContainerConst {
|
||||
static const String defaultErrorMessage =
|
||||
'You attempted to perform a reflective action, but you are using `ThrowingReflector`, '
|
||||
'a class which disables reflection. Consider using the `MirrorsReflector` '
|
||||
'class if you need reflection.';
|
||||
|
||||
ContainerConst._();
|
||||
}
|
112
packages/container/container/lib/src/empty/empty.dart
Normal file
112
packages/container/container/lib/src/empty/empty.dart
Normal file
|
@ -0,0 +1,112 @@
|
|||
import '../../platform_container.dart';
|
||||
|
||||
final Map<Symbol, String?> _symbolNames = <Symbol, String?>{};
|
||||
|
||||
/// A [Reflector] implementation that performs no actual reflection,
|
||||
/// instead returning empty objects on every invocation.
|
||||
///
|
||||
/// Use this in contexts where you know you won't need any reflective capabilities.
|
||||
class EmptyReflector extends Reflector {
|
||||
/// A [RegExp] that can be used to extract the name of a symbol without reflection.
|
||||
static final RegExp symbolRegex = RegExp(r'Symbol\("([^"]+)"\)');
|
||||
|
||||
const EmptyReflector();
|
||||
|
||||
@override
|
||||
String? getName(Symbol symbol) {
|
||||
return _symbolNames.putIfAbsent(
|
||||
symbol, () => symbolRegex.firstMatch(symbol.toString())?.group(1));
|
||||
}
|
||||
|
||||
@override
|
||||
ReflectedClass reflectClass(Type clazz) {
|
||||
return const _EmptyReflectedClass();
|
||||
}
|
||||
|
||||
@override
|
||||
ReflectedInstance reflectInstance(Object object) {
|
||||
return const _EmptyReflectedInstance();
|
||||
}
|
||||
|
||||
@override
|
||||
ReflectedType reflectType(Type type) {
|
||||
return const _EmptyReflectedType();
|
||||
}
|
||||
|
||||
@override
|
||||
ReflectedFunction reflectFunction(Function function) {
|
||||
return const _EmptyReflectedFunction();
|
||||
}
|
||||
}
|
||||
|
||||
class _EmptyReflectedClass extends ReflectedClass {
|
||||
const _EmptyReflectedClass()
|
||||
: super(
|
||||
'(empty)',
|
||||
const <ReflectedTypeParameter>[],
|
||||
const <ReflectedInstance>[],
|
||||
const <ReflectedFunction>[],
|
||||
const <ReflectedDeclaration>[],
|
||||
Object);
|
||||
|
||||
@override
|
||||
ReflectedInstance newInstance(
|
||||
String constructorName, List positionalArguments,
|
||||
[Map<String, dynamic>? namedArguments, List<Type>? typeArguments]) {
|
||||
throw UnsupportedError(
|
||||
'Classes reflected via an EmptyReflector cannot be instantiated.');
|
||||
}
|
||||
|
||||
@override
|
||||
bool isAssignableTo(ReflectedType? other) {
|
||||
return other == this;
|
||||
}
|
||||
}
|
||||
|
||||
class _EmptyReflectedType extends ReflectedType {
|
||||
const _EmptyReflectedType()
|
||||
: super('(empty)', const <ReflectedTypeParameter>[], Object);
|
||||
|
||||
@override
|
||||
ReflectedInstance newInstance(
|
||||
String constructorName, List positionalArguments,
|
||||
[Map<String, dynamic> namedArguments = const {},
|
||||
List<Type> typeArguments = const []]) {
|
||||
throw UnsupportedError(
|
||||
'Types reflected via an EmptyReflector cannot be instantiated.');
|
||||
}
|
||||
|
||||
@override
|
||||
bool isAssignableTo(ReflectedType? other) {
|
||||
return other == this;
|
||||
}
|
||||
}
|
||||
|
||||
class _EmptyReflectedInstance extends ReflectedInstance {
|
||||
const _EmptyReflectedInstance()
|
||||
: super(const _EmptyReflectedType(), const _EmptyReflectedClass(), null);
|
||||
|
||||
@override
|
||||
ReflectedInstance getField(String name) {
|
||||
throw UnsupportedError(
|
||||
'Instances reflected via an EmptyReflector cannot call getField().');
|
||||
}
|
||||
}
|
||||
|
||||
class _EmptyReflectedFunction extends ReflectedFunction {
|
||||
const _EmptyReflectedFunction()
|
||||
: super(
|
||||
'(empty)',
|
||||
const <ReflectedTypeParameter>[],
|
||||
const <ReflectedInstance>[],
|
||||
const <ReflectedParameter>[],
|
||||
false,
|
||||
false,
|
||||
returnType: const _EmptyReflectedType());
|
||||
|
||||
@override
|
||||
ReflectedInstance invoke(Invocation invocation) {
|
||||
throw UnsupportedError(
|
||||
'Instances reflected via an EmptyReflector cannot call invoke().');
|
||||
}
|
||||
}
|
8
packages/container/container/lib/src/exception.dart
Normal file
8
packages/container/container/lib/src/exception.dart
Normal file
|
@ -0,0 +1,8 @@
|
|||
class ReflectionException implements Exception {
|
||||
final String message;
|
||||
|
||||
ReflectionException(this.message);
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export 'reflector.dart';
|
252
packages/container/container/lib/src/mirrors/reflector.dart
Normal file
252
packages/container/container/lib/src/mirrors/reflector.dart
Normal file
|
@ -0,0 +1,252 @@
|
|||
import 'dart:async';
|
||||
import 'dart:mirrors' as dart;
|
||||
|
||||
import '../exception.dart';
|
||||
import '../reflector.dart';
|
||||
|
||||
import 'package:quiver/core.dart';
|
||||
|
||||
/// A [Reflector] implementation that forwards to `dart:mirrors`.
|
||||
///
|
||||
/// Useful on the server, where reflection is supported.
|
||||
class MirrorsReflector extends Reflector {
|
||||
const MirrorsReflector();
|
||||
|
||||
@override
|
||||
String getName(Symbol symbol) => dart.MirrorSystem.getName(symbol);
|
||||
|
||||
@override
|
||||
ReflectedClass reflectClass(Type clazz) {
|
||||
var mirror = dart.reflectType(clazz);
|
||||
|
||||
if (mirror is dart.ClassMirror) {
|
||||
return _ReflectedClassMirror(mirror);
|
||||
} else {
|
||||
throw ArgumentError('$clazz is not a class.');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
ReflectedFunction reflectFunction(Function function) {
|
||||
var closure = dart.reflect(function) as dart.ClosureMirror;
|
||||
return _ReflectedMethodMirror(closure.function, closure);
|
||||
}
|
||||
|
||||
@override
|
||||
ReflectedType reflectType(Type type) {
|
||||
var mirror = dart.reflectType(type);
|
||||
|
||||
if (!mirror.hasReflectedType) {
|
||||
return reflectType(dynamic);
|
||||
} else {
|
||||
if (mirror is dart.ClassMirror) {
|
||||
return _ReflectedClassMirror(mirror);
|
||||
} else {
|
||||
return _ReflectedTypeMirror(mirror);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
ReflectedType reflectFutureOf(Type type) {
|
||||
var inner = reflectType(type);
|
||||
dart.TypeMirror localMirror;
|
||||
if (inner is _ReflectedClassMirror) {
|
||||
localMirror = inner.mirror;
|
||||
} else if (inner is _ReflectedTypeMirror) {
|
||||
localMirror = inner.mirror;
|
||||
} else {
|
||||
throw ArgumentError('$type is not a class or type.');
|
||||
}
|
||||
|
||||
var future = dart.reflectType(Future, [localMirror.reflectedType]);
|
||||
return _ReflectedClassMirror(future as dart.ClassMirror);
|
||||
}
|
||||
|
||||
@override
|
||||
ReflectedInstance reflectInstance(Object object) {
|
||||
return _ReflectedInstanceMirror(dart.reflect(object));
|
||||
}
|
||||
}
|
||||
|
||||
class _ReflectedTypeParameter extends ReflectedTypeParameter {
|
||||
final dart.TypeVariableMirror mirror;
|
||||
|
||||
_ReflectedTypeParameter(this.mirror)
|
||||
: super(dart.MirrorSystem.getName(mirror.simpleName));
|
||||
}
|
||||
|
||||
class _ReflectedTypeMirror extends ReflectedType {
|
||||
final dart.TypeMirror mirror;
|
||||
|
||||
_ReflectedTypeMirror(this.mirror)
|
||||
: super(
|
||||
dart.MirrorSystem.getName(mirror.simpleName),
|
||||
mirror.typeVariables.map((m) => _ReflectedTypeParameter(m)).toList(),
|
||||
mirror.reflectedType,
|
||||
);
|
||||
|
||||
@override
|
||||
bool isAssignableTo(ReflectedType? other) {
|
||||
if (other is _ReflectedClassMirror) {
|
||||
return mirror.isAssignableTo(other.mirror);
|
||||
} else if (other is _ReflectedTypeMirror) {
|
||||
return mirror.isAssignableTo(other.mirror);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
ReflectedInstance newInstance(
|
||||
String constructorName, List positionalArguments,
|
||||
[Map<String, dynamic>? namedArguments, List<Type>? typeArguments]) {
|
||||
throw ReflectionException(
|
||||
'$name is not a class, and therefore cannot be instantiated.');
|
||||
}
|
||||
}
|
||||
|
||||
class _ReflectedClassMirror extends ReflectedClass {
|
||||
final dart.ClassMirror mirror;
|
||||
|
||||
_ReflectedClassMirror(this.mirror)
|
||||
: super(
|
||||
dart.MirrorSystem.getName(mirror.simpleName),
|
||||
mirror.typeVariables.map((m) => _ReflectedTypeParameter(m)).toList(),
|
||||
[],
|
||||
[],
|
||||
_declarationsOf(mirror),
|
||||
mirror.reflectedType,
|
||||
);
|
||||
|
||||
static List<ReflectedFunction> _constructorsOf(dart.ClassMirror mirror) {
|
||||
var out = <ReflectedFunction>[];
|
||||
|
||||
for (var key in mirror.declarations.keys) {
|
||||
var value = mirror.declarations[key];
|
||||
|
||||
if (value is dart.MethodMirror && value.isConstructor) {
|
||||
out.add(_ReflectedMethodMirror(value));
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
static List<ReflectedDeclaration> _declarationsOf(dart.ClassMirror mirror) {
|
||||
var out = <ReflectedDeclaration>[];
|
||||
|
||||
for (var key in mirror.declarations.keys) {
|
||||
var value = mirror.declarations[key];
|
||||
|
||||
if (value is dart.MethodMirror && !value.isConstructor) {
|
||||
out.add(
|
||||
_ReflectedDeclarationMirror(dart.MirrorSystem.getName(key), value));
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@override
|
||||
List<ReflectedInstance> get annotations =>
|
||||
mirror.metadata.map((m) => _ReflectedInstanceMirror(m)).toList();
|
||||
|
||||
@override
|
||||
List<ReflectedFunction> get constructors => _constructorsOf(mirror);
|
||||
|
||||
@override
|
||||
bool isAssignableTo(ReflectedType? other) {
|
||||
if (other is _ReflectedClassMirror) {
|
||||
return mirror.isAssignableTo(other.mirror);
|
||||
} else if (other is _ReflectedTypeMirror) {
|
||||
return mirror.isAssignableTo(other.mirror);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
ReflectedInstance newInstance(
|
||||
String constructorName, List positionalArguments,
|
||||
[Map<String, dynamic>? namedArguments, List<Type>? typeArguments]) {
|
||||
return _ReflectedInstanceMirror(
|
||||
mirror.newInstance(Symbol(constructorName), positionalArguments));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
return other is _ReflectedClassMirror && other.mirror == mirror;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hash2(mirror, " ");
|
||||
}
|
||||
|
||||
class _ReflectedDeclarationMirror extends ReflectedDeclaration {
|
||||
final dart.MethodMirror mirror;
|
||||
|
||||
_ReflectedDeclarationMirror(String name, this.mirror)
|
||||
: super(name, mirror.isStatic, null);
|
||||
|
||||
@override
|
||||
bool get isStatic => mirror.isStatic;
|
||||
|
||||
@override
|
||||
ReflectedFunction get function => _ReflectedMethodMirror(mirror);
|
||||
}
|
||||
|
||||
class _ReflectedInstanceMirror extends ReflectedInstance {
|
||||
final dart.InstanceMirror mirror;
|
||||
|
||||
_ReflectedInstanceMirror(this.mirror)
|
||||
: super(_ReflectedClassMirror(mirror.type),
|
||||
_ReflectedClassMirror(mirror.type), mirror.reflectee);
|
||||
|
||||
@override
|
||||
ReflectedInstance getField(String name) {
|
||||
return _ReflectedInstanceMirror(mirror.getField(Symbol(name)));
|
||||
}
|
||||
}
|
||||
|
||||
class _ReflectedMethodMirror extends ReflectedFunction {
|
||||
final dart.MethodMirror mirror;
|
||||
final dart.ClosureMirror? closureMirror;
|
||||
|
||||
_ReflectedMethodMirror(this.mirror, [this.closureMirror])
|
||||
: super(
|
||||
dart.MirrorSystem.getName(mirror.simpleName),
|
||||
<ReflectedTypeParameter>[],
|
||||
mirror.metadata
|
||||
.map((mirror) => _ReflectedInstanceMirror(mirror))
|
||||
.toList(),
|
||||
mirror.parameters.map(_reflectParameter).toList(),
|
||||
mirror.isGetter,
|
||||
mirror.isSetter,
|
||||
returnType: !mirror.returnType.hasReflectedType
|
||||
? const MirrorsReflector().reflectType(dynamic)
|
||||
: const MirrorsReflector()
|
||||
.reflectType(mirror.returnType.reflectedType));
|
||||
|
||||
static ReflectedParameter _reflectParameter(dart.ParameterMirror mirror) {
|
||||
return ReflectedParameter(
|
||||
dart.MirrorSystem.getName(mirror.simpleName),
|
||||
mirror.metadata
|
||||
.map((mirror) => _ReflectedInstanceMirror(mirror))
|
||||
.toList(),
|
||||
const MirrorsReflector().reflectType(mirror.type.reflectedType),
|
||||
!mirror.isOptional,
|
||||
mirror.isNamed);
|
||||
}
|
||||
|
||||
@override
|
||||
ReflectedInstance invoke(Invocation invocation) {
|
||||
if (closureMirror == null) {
|
||||
throw StateError(
|
||||
'This object was reflected without a ClosureMirror, and therefore cannot be directly invoked.');
|
||||
}
|
||||
|
||||
return _ReflectedInstanceMirror(closureMirror!.invoke(invocation.memberName,
|
||||
invocation.positionalArguments, invocation.namedArguments));
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
|
189
packages/container/container/lib/src/reflector.dart
Normal file
189
packages/container/container/lib/src/reflector.dart
Normal file
|
@ -0,0 +1,189 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:quiver/core.dart';
|
||||
|
||||
abstract class Reflector {
|
||||
const Reflector();
|
||||
|
||||
String? getName(Symbol symbol);
|
||||
|
||||
ReflectedClass? reflectClass(Type clazz);
|
||||
|
||||
ReflectedFunction? reflectFunction(Function function);
|
||||
|
||||
ReflectedType? reflectType(Type type);
|
||||
|
||||
ReflectedInstance? reflectInstance(Object object);
|
||||
|
||||
ReflectedType reflectFutureOf(Type type) {
|
||||
throw UnsupportedError('`reflectFutureOf` requires `dart:mirrors`.');
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ReflectedInstance {
|
||||
final ReflectedType type;
|
||||
final ReflectedClass clazz;
|
||||
final Object? reflectee;
|
||||
|
||||
const ReflectedInstance(this.type, this.clazz, this.reflectee);
|
||||
|
||||
@override
|
||||
int get hashCode => hash2(type, clazz);
|
||||
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
other is ReflectedInstance && other.type == type && other.clazz == clazz;
|
||||
|
||||
ReflectedInstance getField(String name);
|
||||
}
|
||||
|
||||
abstract class ReflectedType {
|
||||
final String name;
|
||||
final List<ReflectedTypeParameter> typeParameters;
|
||||
final Type reflectedType;
|
||||
|
||||
const ReflectedType(this.name, this.typeParameters, this.reflectedType);
|
||||
|
||||
@override
|
||||
int get hashCode => hash3(name, typeParameters, reflectedType);
|
||||
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
other is ReflectedType &&
|
||||
other.name == name &&
|
||||
const ListEquality<ReflectedTypeParameter>()
|
||||
.equals(other.typeParameters, typeParameters) &&
|
||||
other.reflectedType == reflectedType;
|
||||
|
||||
ReflectedInstance newInstance(
|
||||
String constructorName, List positionalArguments,
|
||||
[Map<String, dynamic> namedArguments = const {},
|
||||
List<Type> typeArguments = const []]);
|
||||
|
||||
bool isAssignableTo(ReflectedType? other);
|
||||
}
|
||||
|
||||
abstract class ReflectedClass extends ReflectedType {
|
||||
final List<ReflectedInstance> annotations;
|
||||
final List<ReflectedFunction> constructors;
|
||||
final List<ReflectedDeclaration> declarations;
|
||||
|
||||
const ReflectedClass(
|
||||
String name,
|
||||
List<ReflectedTypeParameter> typeParameters,
|
||||
this.annotations,
|
||||
this.constructors,
|
||||
this.declarations,
|
||||
Type reflectedType)
|
||||
: super(name, typeParameters, reflectedType);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
hash4(super.hashCode, annotations, constructors, declarations);
|
||||
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
other is ReflectedClass &&
|
||||
super == other &&
|
||||
const ListEquality<ReflectedInstance>()
|
||||
.equals(other.annotations, annotations) &&
|
||||
const ListEquality<ReflectedFunction>()
|
||||
.equals(other.constructors, constructors) &&
|
||||
const ListEquality<ReflectedDeclaration>()
|
||||
.equals(other.declarations, declarations);
|
||||
}
|
||||
|
||||
class ReflectedDeclaration {
|
||||
final String name;
|
||||
final bool isStatic;
|
||||
final ReflectedFunction? function;
|
||||
|
||||
const ReflectedDeclaration(this.name, this.isStatic, this.function);
|
||||
|
||||
@override
|
||||
int get hashCode => hash3(name, isStatic, function);
|
||||
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
other is ReflectedDeclaration &&
|
||||
other.name == name &&
|
||||
other.isStatic == isStatic &&
|
||||
other.function == function;
|
||||
}
|
||||
|
||||
abstract class ReflectedFunction {
|
||||
final String name;
|
||||
final List<ReflectedTypeParameter> typeParameters;
|
||||
final List<ReflectedInstance> annotations;
|
||||
final ReflectedType? returnType;
|
||||
final List<ReflectedParameter> parameters;
|
||||
final bool isGetter, isSetter;
|
||||
|
||||
const ReflectedFunction(this.name, this.typeParameters, this.annotations,
|
||||
this.parameters, this.isGetter, this.isSetter,
|
||||
{this.returnType});
|
||||
|
||||
@override
|
||||
int get hashCode => hashObjects([
|
||||
name,
|
||||
typeParameters,
|
||||
annotations,
|
||||
returnType,
|
||||
parameters,
|
||||
isGetter,
|
||||
isSetter
|
||||
]);
|
||||
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
other is ReflectedFunction &&
|
||||
other.name == name &&
|
||||
const ListEquality<ReflectedTypeParameter>()
|
||||
.equals(other.typeParameters, typeParameters) &&
|
||||
const ListEquality<ReflectedInstance>()
|
||||
.equals(other.annotations, annotations) &&
|
||||
other.returnType == returnType &&
|
||||
const ListEquality<ReflectedParameter>()
|
||||
.equals(other.parameters, other.parameters) &&
|
||||
other.isGetter == isGetter &&
|
||||
other.isSetter == isSetter;
|
||||
|
||||
ReflectedInstance invoke(Invocation invocation);
|
||||
}
|
||||
|
||||
class ReflectedParameter {
|
||||
final String name;
|
||||
final List<ReflectedInstance> annotations;
|
||||
final ReflectedType type;
|
||||
final bool isRequired;
|
||||
final bool isNamed;
|
||||
|
||||
const ReflectedParameter(
|
||||
this.name, this.annotations, this.type, this.isRequired, this.isNamed);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
hashObjects([name, annotations, type, isRequired, isNamed]);
|
||||
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
other is ReflectedParameter &&
|
||||
other.name == name &&
|
||||
const ListEquality<ReflectedInstance>()
|
||||
.equals(other.annotations, annotations) &&
|
||||
other.type == type &&
|
||||
other.isRequired == isRequired &&
|
||||
other.isNamed == isNamed;
|
||||
}
|
||||
|
||||
class ReflectedTypeParameter {
|
||||
final String name;
|
||||
|
||||
const ReflectedTypeParameter(this.name);
|
||||
|
||||
@override
|
||||
int get hashCode => hashObjects([name]);
|
||||
|
||||
@override
|
||||
bool operator ==(other) =>
|
||||
other is ReflectedTypeParameter && other.name == name;
|
||||
}
|
61
packages/container/container/lib/src/static/static.dart
Normal file
61
packages/container/container/lib/src/static/static.dart
Normal file
|
@ -0,0 +1,61 @@
|
|||
import '../reflector.dart';
|
||||
|
||||
/// A [Reflector] implementation that performs simple [Map] lookups.
|
||||
///
|
||||
/// `package:angel_container_generator` uses this to create reflectors from analysis metadata.
|
||||
class StaticReflector extends Reflector {
|
||||
final Map<Symbol, String> names;
|
||||
final Map<Type, ReflectedType> types;
|
||||
final Map<Function, ReflectedFunction> functions;
|
||||
final Map<Object, ReflectedInstance> instances;
|
||||
|
||||
const StaticReflector(
|
||||
{this.names = const {},
|
||||
this.types = const {},
|
||||
this.functions = const {},
|
||||
this.instances = const {}});
|
||||
|
||||
@override
|
||||
String? getName(Symbol symbol) {
|
||||
if (!names.containsKey(symbol)) {
|
||||
throw ArgumentError(
|
||||
'The value of $symbol is unknown - it was not generated.');
|
||||
}
|
||||
|
||||
return names[symbol];
|
||||
}
|
||||
|
||||
@override
|
||||
ReflectedClass? reflectClass(Type clazz) =>
|
||||
reflectType(clazz) as ReflectedClass?;
|
||||
|
||||
@override
|
||||
ReflectedFunction? reflectFunction(Function function) {
|
||||
if (!functions.containsKey(function)) {
|
||||
throw ArgumentError(
|
||||
'There is no reflection information available about $function.');
|
||||
}
|
||||
|
||||
return functions[function];
|
||||
}
|
||||
|
||||
@override
|
||||
ReflectedInstance? reflectInstance(Object object) {
|
||||
if (!instances.containsKey(object)) {
|
||||
throw ArgumentError(
|
||||
'There is no reflection information available about $object.');
|
||||
}
|
||||
|
||||
return instances[object];
|
||||
}
|
||||
|
||||
@override
|
||||
ReflectedType? reflectType(Type type) {
|
||||
if (!types.containsKey(type)) {
|
||||
throw ArgumentError(
|
||||
'There is no reflection information available about $type.');
|
||||
}
|
||||
|
||||
return types[type];
|
||||
}
|
||||
}
|
40
packages/container/container/lib/src/throwing.dart
Normal file
40
packages/container/container/lib/src/throwing.dart
Normal file
|
@ -0,0 +1,40 @@
|
|||
import 'package:platform_container/src/container_const.dart';
|
||||
|
||||
import 'empty/empty.dart';
|
||||
import 'reflector.dart';
|
||||
|
||||
/// A [Reflector] implementation that throws exceptions on all attempts
|
||||
/// to perform reflection.
|
||||
///
|
||||
/// Use this in contexts where you know you won't need any reflective capabilities.
|
||||
class ThrowingReflector extends Reflector {
|
||||
/// The error message to give the end user when an [UnsupportedError] is thrown.
|
||||
final String errorMessage;
|
||||
|
||||
/*
|
||||
static const String defaultErrorMessage =
|
||||
'You attempted to perform a reflective action, but you are using `ThrowingReflector`, '
|
||||
'a class which disables reflection. Consider using the `MirrorsReflector` '
|
||||
'class if you need reflection.';
|
||||
*/
|
||||
|
||||
const ThrowingReflector(
|
||||
{this.errorMessage = ContainerConst.defaultErrorMessage});
|
||||
|
||||
@override
|
||||
String? getName(Symbol symbol) => const EmptyReflector().getName(symbol);
|
||||
|
||||
UnsupportedError _error() => UnsupportedError(errorMessage);
|
||||
|
||||
@override
|
||||
ReflectedClass reflectClass(Type clazz) => throw _error();
|
||||
|
||||
@override
|
||||
ReflectedInstance reflectInstance(Object object) => throw _error();
|
||||
|
||||
@override
|
||||
ReflectedType reflectType(Type type) => throw _error();
|
||||
|
||||
@override
|
||||
ReflectedFunction reflectFunction(Function function) => throw _error();
|
||||
}
|
13
packages/container/container/pubspec.yaml
Normal file
13
packages/container/container/pubspec.yaml
Normal file
|
@ -0,0 +1,13 @@
|
|||
name: platform_container
|
||||
version: 9.0.0
|
||||
description: Protevus Platform hierarchical DI container, and pluggable backends for reflection.
|
||||
homepage: https://angel3-framework.web.app/
|
||||
repository: https://github.com/dart-backend/angel/tree/master/packages/container/angel_container
|
||||
environment:
|
||||
sdk: '>=3.3.0 <4.0.0'
|
||||
dependencies:
|
||||
collection: ^1.17.0
|
||||
quiver: ^3.2.0
|
||||
dev_dependencies:
|
||||
test: ^1.24.0
|
||||
lints: ^4.0.0
|
122
packages/container/container/test/common.dart
Normal file
122
packages/container/container/test/common.dart
Normal file
|
@ -0,0 +1,122 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:platform_container/platform_container.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void returnVoidFromAFunction(int x) {}
|
||||
|
||||
void testReflector(Reflector reflector) {
|
||||
var blaziken = Pokemon('Blaziken', PokemonType.fire);
|
||||
late Container container;
|
||||
|
||||
setUp(() {
|
||||
container = Container(reflector);
|
||||
container.registerSingleton(blaziken);
|
||||
container.registerFactory<Future<int>>((_) async => 46);
|
||||
});
|
||||
|
||||
test('get field', () {
|
||||
var blazikenMirror = reflector.reflectInstance(blaziken)!;
|
||||
expect(blazikenMirror.getField('type').reflectee, blaziken.type);
|
||||
});
|
||||
|
||||
group('reflectFunction', () {
|
||||
var mirror = reflector.reflectFunction(returnVoidFromAFunction);
|
||||
|
||||
test('void return type returns dynamic', () {
|
||||
expect(mirror!.returnType, reflector.reflectType(dynamic));
|
||||
});
|
||||
|
||||
test('counts parameters', () {
|
||||
expect(mirror!.parameters, hasLength(1));
|
||||
});
|
||||
|
||||
test('counts types parameters', () {
|
||||
expect(mirror!.typeParameters, isEmpty);
|
||||
});
|
||||
|
||||
test('correctly reflects parameter types', () {
|
||||
var p = mirror!.parameters[0];
|
||||
expect(p.name, 'x');
|
||||
expect(p.isRequired, true);
|
||||
expect(p.isNamed, false);
|
||||
expect(p.annotations, isEmpty);
|
||||
expect(p.type, reflector.reflectType(int));
|
||||
});
|
||||
});
|
||||
|
||||
test('make on singleton type returns singleton', () {
|
||||
expect(container.make(Pokemon), blaziken);
|
||||
});
|
||||
|
||||
test('make with generic returns same as make with explicit type', () {
|
||||
expect(container.make<Pokemon>(), blaziken);
|
||||
});
|
||||
|
||||
test('make async returns async object', () async {
|
||||
expect(container.makeAsync<int>(), completion(46));
|
||||
});
|
||||
|
||||
test('make async returns sync object', () async {
|
||||
expect(container.makeAsync<Pokemon>(), completion(blaziken));
|
||||
});
|
||||
|
||||
test('make on aliased singleton returns singleton', () {
|
||||
container.registerSingleton(blaziken, as: StateError);
|
||||
expect(container.make(StateError), blaziken);
|
||||
});
|
||||
|
||||
test('constructor injects singleton', () {
|
||||
var lower = container.make<LowerPokemon>();
|
||||
expect(lower.lowercaseName, blaziken.name.toLowerCase());
|
||||
});
|
||||
|
||||
test('newInstance works', () {
|
||||
var type = container.reflector.reflectType(Pokemon)!;
|
||||
var instance =
|
||||
type.newInstance('changeName', [blaziken, 'Charizard']).reflectee
|
||||
as Pokemon;
|
||||
print(instance);
|
||||
expect(instance.name, 'Charizard');
|
||||
expect(instance.type, PokemonType.fire);
|
||||
});
|
||||
|
||||
test('isAssignableTo', () {
|
||||
var pokemonType = container.reflector.reflectType(Pokemon);
|
||||
var kantoPokemonType = container.reflector.reflectType(KantoPokemon)!;
|
||||
|
||||
expect(kantoPokemonType.isAssignableTo(pokemonType), true);
|
||||
expect(
|
||||
kantoPokemonType
|
||||
.isAssignableTo(container.reflector.reflectType(String)),
|
||||
false);
|
||||
});
|
||||
}
|
||||
|
||||
class LowerPokemon {
|
||||
final Pokemon pokemon;
|
||||
|
||||
LowerPokemon(this.pokemon);
|
||||
|
||||
String get lowercaseName => pokemon.name.toLowerCase();
|
||||
}
|
||||
|
||||
class Pokemon {
|
||||
final String name;
|
||||
final PokemonType type;
|
||||
|
||||
Pokemon(this.name, this.type);
|
||||
|
||||
factory Pokemon.changeName(Pokemon other, String name) {
|
||||
return Pokemon(name, other.type);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'NAME: $name, TYPE: $type';
|
||||
}
|
||||
|
||||
class KantoPokemon extends Pokemon {
|
||||
KantoPokemon(super.name, super.type);
|
||||
}
|
||||
|
||||
enum PokemonType { water, fire, grass, ice, poison, flying }
|
138
packages/container/container/test/empty_reflector_test.dart
Normal file
138
packages/container/container/test/empty_reflector_test.dart
Normal file
|
@ -0,0 +1,138 @@
|
|||
import 'package:platform_container/platform_container.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
var reflector = const EmptyReflector();
|
||||
|
||||
test('getName', () {
|
||||
expect(reflector.getName(#foo), 'foo');
|
||||
expect(reflector.getName(#==), '==');
|
||||
});
|
||||
|
||||
group('reflectClass', () {
|
||||
var mirror = reflector.reflectClass(Truck);
|
||||
|
||||
test('name returns empty', () {
|
||||
expect(mirror.name, '(empty)');
|
||||
});
|
||||
|
||||
test('annotations returns empty', () {
|
||||
expect(mirror.annotations, isEmpty);
|
||||
});
|
||||
|
||||
test('typeParameters returns empty', () {
|
||||
expect(mirror.typeParameters, isEmpty);
|
||||
});
|
||||
|
||||
test('declarations returns empty', () {
|
||||
expect(mirror.declarations, isEmpty);
|
||||
});
|
||||
|
||||
test('constructors returns empty', () {
|
||||
expect(mirror.constructors, isEmpty);
|
||||
});
|
||||
|
||||
test('reflectedType returns Object', () {
|
||||
expect(mirror.reflectedType, Object);
|
||||
});
|
||||
|
||||
test('cannot call newInstance', () {
|
||||
expect(() => mirror.newInstance('', []), throwsUnsupportedError);
|
||||
});
|
||||
|
||||
test('isAssignableTo self', () {
|
||||
expect(mirror.isAssignableTo(mirror), true);
|
||||
});
|
||||
});
|
||||
|
||||
group('reflectType', () {
|
||||
var mirror = reflector.reflectType(Truck);
|
||||
|
||||
test('name returns empty', () {
|
||||
expect(mirror.name, '(empty)');
|
||||
});
|
||||
|
||||
test('typeParameters returns empty', () {
|
||||
expect(mirror.typeParameters, isEmpty);
|
||||
});
|
||||
|
||||
test('reflectedType returns Object', () {
|
||||
expect(mirror.reflectedType, Object);
|
||||
});
|
||||
|
||||
test('cannot call newInstance', () {
|
||||
expect(() => mirror.newInstance('', []), throwsUnsupportedError);
|
||||
});
|
||||
|
||||
test('isAssignableTo self', () {
|
||||
expect(mirror.isAssignableTo(mirror), true);
|
||||
});
|
||||
});
|
||||
|
||||
group('reflectFunction', () {
|
||||
void doIt(int x) {}
|
||||
|
||||
var mirror = reflector.reflectFunction(doIt);
|
||||
|
||||
test('name returns empty', () {
|
||||
expect(mirror.name, '(empty)');
|
||||
});
|
||||
|
||||
test('annotations returns empty', () {
|
||||
expect(mirror.annotations, isEmpty);
|
||||
});
|
||||
|
||||
test('typeParameters returns empty', () {
|
||||
expect(mirror.typeParameters, isEmpty);
|
||||
});
|
||||
|
||||
test('parameters returns empty', () {
|
||||
expect(mirror.parameters, isEmpty);
|
||||
});
|
||||
|
||||
test('return type is dynamic', () {
|
||||
expect(mirror.returnType, reflector.reflectType(dynamic));
|
||||
});
|
||||
|
||||
test('isGetter returns false', () {
|
||||
expect(mirror.isGetter, false);
|
||||
});
|
||||
|
||||
test('isSetter returns false', () {
|
||||
expect(mirror.isSetter, false);
|
||||
});
|
||||
|
||||
test('cannot invoke', () {
|
||||
var invocation = Invocation.method(#drive, []);
|
||||
expect(() => mirror.invoke(invocation), throwsUnsupportedError);
|
||||
});
|
||||
});
|
||||
|
||||
group('reflectInstance', () {
|
||||
var mirror = reflector.reflectInstance(Truck());
|
||||
|
||||
test('reflectee returns null', () {
|
||||
expect(mirror.reflectee, null);
|
||||
});
|
||||
|
||||
test('type returns empty', () {
|
||||
expect(mirror.type.name, '(empty)');
|
||||
});
|
||||
|
||||
test('clazz returns empty', () {
|
||||
expect(mirror.clazz.name, '(empty)');
|
||||
});
|
||||
|
||||
test('cannot getField', () {
|
||||
expect(() => mirror.getField('wheelCount'), throwsUnsupportedError);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class Truck {
|
||||
int get wheelCount => 4;
|
||||
|
||||
void drive() {
|
||||
print('Vroom!!!');
|
||||
}
|
||||
}
|
51
packages/container/container/test/has_test.dart
Normal file
51
packages/container/container/test/has_test.dart
Normal file
|
@ -0,0 +1,51 @@
|
|||
import 'package:platform_container/platform_container.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
late Container container;
|
||||
|
||||
setUp(() {
|
||||
container = Container(const EmptyReflector())
|
||||
..registerSingleton<Song>(Song(title: 'I Wish'))
|
||||
..registerNamedSingleton('foo', 1)
|
||||
..registerFactory<Artist>((container) {
|
||||
return Artist(
|
||||
name: 'Stevie Wonder',
|
||||
song: container.make<Song>(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('hasNamed', () {
|
||||
var child = container.createChild()..registerNamedSingleton('bar', 2);
|
||||
expect(child.hasNamed('foo'), true);
|
||||
expect(child.hasNamed('bar'), true);
|
||||
expect(child.hasNamed('baz'), false);
|
||||
});
|
||||
|
||||
test('has on singleton', () {
|
||||
var result = container.has<Song>();
|
||||
expect(result, true);
|
||||
});
|
||||
|
||||
test('has on factory', () {
|
||||
expect(container.has<Artist>(), true);
|
||||
});
|
||||
|
||||
test('false if neither', () {
|
||||
expect(container.has<bool>(), false);
|
||||
});
|
||||
}
|
||||
|
||||
class Artist {
|
||||
final String? name;
|
||||
final Song? song;
|
||||
|
||||
Artist({this.name, this.song});
|
||||
}
|
||||
|
||||
class Song {
|
||||
final String? title;
|
||||
|
||||
Song({this.title});
|
||||
}
|
18
packages/container/container/test/lazy_test.dart
Normal file
18
packages/container/container/test/lazy_test.dart
Normal file
|
@ -0,0 +1,18 @@
|
|||
import 'package:platform_container/platform_container.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
test('returns the same instance', () {
|
||||
var container = Container(const EmptyReflector())
|
||||
..registerLazySingleton<Dummy>((_) => Dummy('a'));
|
||||
|
||||
var first = container.make<Dummy>();
|
||||
expect(container.make<Dummy>(), first);
|
||||
});
|
||||
}
|
||||
|
||||
class Dummy {
|
||||
final String s;
|
||||
|
||||
Dummy(this.s);
|
||||
}
|
26
packages/container/container/test/mirrors_test.dart
Normal file
26
packages/container/container/test/mirrors_test.dart
Normal file
|
@ -0,0 +1,26 @@
|
|||
import 'dart:async';
|
||||
import 'package:platform_container/platform_container.dart';
|
||||
import 'package:platform_container/mirrors.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'common.dart';
|
||||
|
||||
void main() {
|
||||
testReflector(const MirrorsReflector());
|
||||
|
||||
test('futureOf', () {
|
||||
var r = MirrorsReflector();
|
||||
var fStr = r.reflectFutureOf(String);
|
||||
expect(fStr.reflectedType.toString(), 'Future<String>');
|
||||
// expect(fStr.reflectedType, Future<String>.value(null).runtimeType);
|
||||
});
|
||||
|
||||
test('concrete future make', () async {
|
||||
var c = Container(MirrorsReflector());
|
||||
c.registerFactory<Future<String>>((_) async => 'hey');
|
||||
var fStr = c.reflector.reflectFutureOf(String);
|
||||
var s1 = await c.make(fStr.reflectedType);
|
||||
var s2 = await c.makeAsync(String);
|
||||
print([s1, s2]);
|
||||
expect(s1, s2);
|
||||
});
|
||||
}
|
34
packages/container/container/test/named_test.dart
Normal file
34
packages/container/container/test/named_test.dart
Normal file
|
@ -0,0 +1,34 @@
|
|||
import 'package:platform_container/platform_container.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
late Container container;
|
||||
|
||||
setUp(() {
|
||||
container = Container(const EmptyReflector());
|
||||
container.registerNamedSingleton('foo', Foo(bar: 'baz'));
|
||||
});
|
||||
|
||||
test('fetch by name', () {
|
||||
expect(container.findByName<Foo>('foo').bar, 'baz');
|
||||
});
|
||||
|
||||
test('cannot redefine', () {
|
||||
expect(() => container.registerNamedSingleton('foo', Foo(bar: 'quux')),
|
||||
throwsStateError);
|
||||
});
|
||||
|
||||
test('throws on unknown name', () {
|
||||
expect(() => container.findByName('bar'), throwsStateError);
|
||||
});
|
||||
|
||||
test('throws on incorrect type', () {
|
||||
expect(() => container.findByName<List<String>>('foo'), throwsA(anything));
|
||||
});
|
||||
}
|
||||
|
||||
class Foo {
|
||||
final String? bar;
|
||||
|
||||
Foo({this.bar});
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import 'package:platform_container/platform_container.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
var reflector = const ThrowingReflector();
|
||||
|
||||
test('getName', () {
|
||||
expect(reflector.getName(#foo), 'foo');
|
||||
expect(reflector.getName(#==), '==');
|
||||
});
|
||||
|
||||
test('reflectClass fails', () {
|
||||
expect(() => reflector.reflectClass(Truck), throwsUnsupportedError);
|
||||
});
|
||||
|
||||
test('reflectType fails', () {
|
||||
expect(() => reflector.reflectType(Truck), throwsUnsupportedError);
|
||||
});
|
||||
|
||||
test('reflectFunction throws', () {
|
||||
void doIt(int x) {}
|
||||
expect(() => reflector.reflectFunction(doIt), throwsUnsupportedError);
|
||||
});
|
||||
|
||||
test('reflectInstance throws', () {
|
||||
expect(() => reflector.reflectInstance(Truck()), throwsUnsupportedError);
|
||||
});
|
||||
}
|
||||
|
||||
class Truck {
|
||||
int get wheelCount => 4;
|
||||
|
||||
void drive() {
|
||||
print('Vroom!!!');
|
||||
}
|
||||
}
|
13
packages/container/container_generator/.gitignore
vendored
Normal file
13
packages/container/container_generator/.gitignore
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
# See https://www.dartlang.org/guides/libraries/private-files
|
||||
|
||||
# Files and directories created by pub
|
||||
.dart_tool/
|
||||
.packages
|
||||
.pub/
|
||||
build/
|
||||
# If you're building an application, you may want to check-in your pubspec.lock
|
||||
pubspec.lock
|
||||
|
||||
# Directory created by dartdoc
|
||||
# If you don't generate documentation locally you can remove this line.
|
||||
doc/api/
|
58
packages/container/container_generator/CHANGELOG.md
Normal file
58
packages/container/container_generator/CHANGELOG.md
Normal file
|
@ -0,0 +1,58 @@
|
|||
# Change Log
|
||||
|
||||
## 8.1.1
|
||||
|
||||
* Updated repository link
|
||||
|
||||
## 8.1.0
|
||||
|
||||
* Updated `lints` to 3.0.0
|
||||
* Fixed analyser warnings
|
||||
|
||||
## 8.0.0
|
||||
|
||||
* Require Dart >= 3.0
|
||||
|
||||
## 7.1.0-beta.1
|
||||
|
||||
* Require Dart >= 2.19
|
||||
* Upgraded `relectable` to 4.x.x
|
||||
|
||||
## 7.0.0
|
||||
|
||||
* Require Dart >= 2.17
|
||||
|
||||
## 6.0.0
|
||||
|
||||
* Require Dart >= 2.16
|
||||
|
||||
## 5.0.0
|
||||
|
||||
* Skipped release
|
||||
|
||||
## 4.0.0
|
||||
|
||||
* Skipped release
|
||||
|
||||
## 3.0.1
|
||||
|
||||
* Updated `package:angel3_container`
|
||||
|
||||
## 3.0.0
|
||||
|
||||
* Fixed NNBD issues
|
||||
* All 9 test cases passed
|
||||
|
||||
## 3.0.0-beta.1
|
||||
|
||||
* Migrated to support Dart >= 2.12 NNBD
|
||||
* Updated linter to `package:lints`
|
||||
* Updated to use `angel3_` packages
|
||||
|
||||
## 2.0.0
|
||||
|
||||
* Migrated to work with Dart >= 2.12 Non NNBD
|
||||
|
||||
## 1.0.1
|
||||
|
||||
* Update for `pkg:angel_container@1.0.3`.
|
29
packages/container/container_generator/LICENSE
Normal file
29
packages/container/container_generator/LICENSE
Normal file
|
@ -0,0 +1,29 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2021, dukefirehawk.com
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
32
packages/container/container_generator/README.md
Normal file
32
packages/container/container_generator/README.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
# Angel3 Container Generator
|
||||
|
||||
![Pub Version (including pre-releases)](https://img.shields.io/pub/v/angel3_container_generator?include_prereleases)
|
||||
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
|
||||
[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion)
|
||||
[![License](https://img.shields.io/github/license/dart-backend/angel)](https://github.com/dart-backend/angel/tree/master/packages/container/angel3_container_generator/LICENSE)
|
||||
|
||||
An alternative container for Angel3 that uses `reflectable` package instead of `dart:mirrors` for reflection. However, `reflectable` has more limited relfection capabilities when compared to `dart:mirrors`.
|
||||
|
||||
## Usage
|
||||
|
||||
* Annotable the class with `@contained`.
|
||||
* Run `dart run build_runner build <Your class directory>`
|
||||
* Alternatively create a `build.xml` file with the following content
|
||||
|
||||
```yaml
|
||||
targets:
|
||||
$default:
|
||||
builders:
|
||||
reflectable:
|
||||
generate_for:
|
||||
- bin/**_controller.dart
|
||||
options:
|
||||
formatted: true
|
||||
```
|
||||
|
||||
## Known limitation
|
||||
|
||||
* `analyser` 6.x is not supported due to `reflectable`
|
||||
* Reflection on functions/closures is not supported
|
||||
* Reflection on private declarations is not supported
|
||||
* Reflection on generic type is not supported
|
|
@ -0,0 +1 @@
|
|||
include: package:lints/recommended.yaml
|
75
packages/container/container_generator/example/main.dart
Normal file
75
packages/container/container_generator/example/main.dart
Normal file
|
@ -0,0 +1,75 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:platform_container/platform_container.dart';
|
||||
import 'package:platform_container_generator/angel3_container_generator.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
// Create a container instance.
|
||||
Container container = Container(GeneratedReflector());
|
||||
|
||||
// Register a singleton.
|
||||
container.registerSingleton<Engine>(Engine(40));
|
||||
|
||||
// You can also omit the type annotation, in which the object's runtime type will be used.
|
||||
// If you're injecting an abstract class, prefer the type annotation.
|
||||
//
|
||||
// container.registerSingleton(Engine(40));
|
||||
|
||||
// Register a factory that creates a truck.
|
||||
container.registerFactory<Truck>((container) {
|
||||
return _TruckImpl(container.make<Engine>());
|
||||
});
|
||||
|
||||
// Use `make` to create an instance.
|
||||
var truck = container.make<Truck>();
|
||||
|
||||
// You can also resolve injections asynchronously.
|
||||
container.registerFactory<Future<int>>((_) async => 24);
|
||||
print(await container.makeAsync<int>());
|
||||
|
||||
// Asynchronous resolution also works for plain objects.
|
||||
await container.makeAsync<Truck>().then((t) => t.drive());
|
||||
|
||||
// Register a named singleton.
|
||||
container.registerNamedSingleton('the_truck', truck);
|
||||
|
||||
// Should print: 'Vroom! I have 40 horsepower in my engine.'
|
||||
truck.drive();
|
||||
|
||||
// Should print the same.
|
||||
container.findByName<Truck>('the_truck').drive();
|
||||
|
||||
// We can make a child container with its own factory.
|
||||
var childContainer = container.createChild();
|
||||
|
||||
childContainer.registerFactory<Truck>((container) {
|
||||
return _TruckImpl(Engine(5666));
|
||||
});
|
||||
|
||||
// Make a truck with 5666 HP.
|
||||
childContainer.make<Truck>().drive();
|
||||
|
||||
// However, calling `make<Engine>` will return the Engine singleton we created above.
|
||||
print(childContainer.make<Engine>().horsePower);
|
||||
}
|
||||
|
||||
abstract class Truck {
|
||||
void drive();
|
||||
}
|
||||
|
||||
class Engine {
|
||||
final int horsePower;
|
||||
|
||||
Engine(this.horsePower);
|
||||
}
|
||||
|
||||
class _TruckImpl implements Truck {
|
||||
final Engine engine;
|
||||
|
||||
_TruckImpl(this.engine);
|
||||
|
||||
@override
|
||||
void drive() {
|
||||
print('Vroom! I have ${engine.horsePower} horsepower in my engine.');
|
||||
}
|
||||
}
|
6520
packages/container/container_generator/example/main.reflectable.dart
Normal file
6520
packages/container/container_generator/example/main.reflectable.dart
Normal file
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,255 @@
|
|||
import 'package:platform_container/platform_container.dart';
|
||||
import 'package:reflectable/reflectable.dart';
|
||||
|
||||
/// A [Reflectable] instance that can be used as an annotation on types to generate metadata for them.
|
||||
const Reflectable contained = ContainedReflectable();
|
||||
|
||||
@contained
|
||||
class ContainedReflectable extends Reflectable {
|
||||
const ContainedReflectable()
|
||||
: super(
|
||||
topLevelInvokeCapability,
|
||||
typeAnnotationQuantifyCapability,
|
||||
superclassQuantifyCapability,
|
||||
libraryCapability,
|
||||
invokingCapability,
|
||||
metadataCapability,
|
||||
reflectedTypeCapability,
|
||||
typeCapability,
|
||||
typingCapability);
|
||||
}
|
||||
|
||||
/// A [Reflector] instance that uses a [Reflectable] to reflect upon data.
|
||||
class GeneratedReflector extends Reflector {
|
||||
final Reflectable reflectable;
|
||||
|
||||
const GeneratedReflector([this.reflectable = contained]);
|
||||
|
||||
@override
|
||||
String getName(Symbol symbol) {
|
||||
return symbol.toString().substring(7);
|
||||
}
|
||||
|
||||
@override
|
||||
ReflectedClass reflectClass(Type clazz) {
|
||||
return reflectType(clazz) as ReflectedClass;
|
||||
}
|
||||
|
||||
@override
|
||||
ReflectedFunction reflectFunction(Function function) {
|
||||
if (!reflectable.canReflect(function)) {
|
||||
throw UnsupportedError('Cannot reflect $function.');
|
||||
}
|
||||
|
||||
var mirror = reflectable.reflect(function);
|
||||
|
||||
if (mirror is ClosureMirror) {
|
||||
return _GeneratedReflectedFunction(mirror.function, this, mirror);
|
||||
} else {
|
||||
throw ArgumentError('$function is not a Function.');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
ReflectedInstance reflectInstance(Object object) {
|
||||
if (!reflectable.canReflect(object)) {
|
||||
throw UnsupportedError('Cannot reflect $object.');
|
||||
} else {
|
||||
var mirror = reflectable.reflect(object);
|
||||
return _GeneratedReflectedInstance(mirror, this);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
ReflectedType reflectType(Type type) {
|
||||
if (!reflectable.canReflectType(type)) {
|
||||
throw UnsupportedError('Cannot reflect $type.');
|
||||
} else {
|
||||
var mirror = reflectable.reflectType(type);
|
||||
return mirror is ClassMirror
|
||||
? _GeneratedReflectedClass(mirror, this)
|
||||
: _GeneratedReflectedType(mirror);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _GeneratedReflectedInstance extends ReflectedInstance {
|
||||
final InstanceMirror mirror;
|
||||
final GeneratedReflector reflector;
|
||||
|
||||
_GeneratedReflectedInstance(this.mirror, this.reflector)
|
||||
: super(_GeneratedReflectedType(mirror.type),
|
||||
_GeneratedReflectedClass(mirror.type, reflector), mirror.reflectee);
|
||||
|
||||
@override
|
||||
ReflectedType get type => clazz;
|
||||
|
||||
@override
|
||||
ReflectedInstance getField(String name) {
|
||||
var result = mirror.invokeGetter(name)!;
|
||||
var instance = reflector.reflectable.reflect(result);
|
||||
return _GeneratedReflectedInstance(instance, reflector);
|
||||
}
|
||||
}
|
||||
|
||||
class _GeneratedReflectedClass extends ReflectedClass {
|
||||
final ClassMirror mirror;
|
||||
final Reflector reflector;
|
||||
|
||||
_GeneratedReflectedClass(this.mirror, this.reflector)
|
||||
: super(mirror.simpleName, [], [], [], [], mirror.reflectedType);
|
||||
|
||||
@override
|
||||
List<ReflectedTypeParameter> get typeParameters =>
|
||||
mirror.typeVariables.map(_convertTypeVariable).toList();
|
||||
|
||||
@override
|
||||
List<ReflectedFunction> get constructors =>
|
||||
_constructorsOf(mirror.declarations, reflector);
|
||||
|
||||
@override
|
||||
List<ReflectedDeclaration> get declarations =>
|
||||
_declarationsOf(mirror.declarations, reflector);
|
||||
|
||||
@override
|
||||
List<ReflectedInstance> get annotations => mirror.metadata
|
||||
.map(reflector.reflectInstance)
|
||||
.whereType<ReflectedInstance>()
|
||||
.toList();
|
||||
|
||||
@override
|
||||
bool isAssignableTo(ReflectedType? other) {
|
||||
if (other is _GeneratedReflectedClass) {
|
||||
return mirror.isAssignableTo(other.mirror);
|
||||
} else if (other is _GeneratedReflectedType) {
|
||||
return mirror.isAssignableTo(other.mirror);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
ReflectedInstance newInstance(
|
||||
String constructorName, List positionalArguments,
|
||||
[Map<String, dynamic>? namedArguments, List<Type>? typeArguments]) {
|
||||
namedArguments ??= {};
|
||||
var result = mirror.newInstance(constructorName, positionalArguments,
|
||||
namedArguments.map((k, v) => MapEntry(Symbol(k), v)));
|
||||
return reflector.reflectInstance(result)!;
|
||||
}
|
||||
}
|
||||
|
||||
class _GeneratedReflectedType extends ReflectedType {
|
||||
final TypeMirror mirror;
|
||||
|
||||
_GeneratedReflectedType(this.mirror)
|
||||
: super(mirror.simpleName, [], mirror.reflectedType);
|
||||
|
||||
@override
|
||||
List<ReflectedTypeParameter> get typeParameters =>
|
||||
mirror.typeVariables.map(_convertTypeVariable).toList();
|
||||
|
||||
@override
|
||||
bool isAssignableTo(ReflectedType? other) {
|
||||
if (other is _GeneratedReflectedClass) {
|
||||
return mirror.isAssignableTo(other.mirror);
|
||||
} else if (other is _GeneratedReflectedType) {
|
||||
return mirror.isAssignableTo(other.mirror);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
ReflectedInstance newInstance(
|
||||
String constructorName, List positionalArguments,
|
||||
[Map<String, dynamic> namedArguments = const {},
|
||||
List<Type> typeArguments = const []]) {
|
||||
throw UnsupportedError('Cannot create a new instance of $reflectedType.');
|
||||
}
|
||||
}
|
||||
|
||||
class _GeneratedReflectedFunction extends ReflectedFunction {
|
||||
final MethodMirror mirror;
|
||||
final Reflector reflector;
|
||||
final ClosureMirror? closure;
|
||||
|
||||
_GeneratedReflectedFunction(this.mirror, this.reflector, [this.closure])
|
||||
: super(
|
||||
mirror.simpleName,
|
||||
[],
|
||||
[],
|
||||
mirror.parameters
|
||||
.map((p) => _convertParameter(p, reflector))
|
||||
.toList(),
|
||||
mirror.isGetter,
|
||||
mirror.isSetter,
|
||||
returnType: !mirror.isRegularMethod
|
||||
? null
|
||||
: _GeneratedReflectedType(mirror.returnType));
|
||||
|
||||
@override
|
||||
List<ReflectedInstance> get annotations => mirror.metadata
|
||||
.map(reflector.reflectInstance)
|
||||
.whereType<ReflectedInstance>()
|
||||
.toList();
|
||||
|
||||
@override
|
||||
ReflectedInstance invoke(Invocation invocation) {
|
||||
if (closure != null) {
|
||||
throw UnsupportedError('Only closures can be invoked directly.');
|
||||
} else {
|
||||
var result = closure!.delegate(invocation)!;
|
||||
return reflector.reflectInstance(result)!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<ReflectedFunction> _constructorsOf(
|
||||
Map<String, DeclarationMirror> map, Reflector reflector) {
|
||||
return map.entries.fold<List<ReflectedFunction>>([], (out, entry) {
|
||||
var v = entry.value;
|
||||
|
||||
if (v is MethodMirror && v.isConstructor) {
|
||||
return out..add(_GeneratedReflectedFunction(v, reflector));
|
||||
} else {
|
||||
return out;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
List<ReflectedDeclaration> _declarationsOf(
|
||||
Map<String, DeclarationMirror> map, Reflector reflector) {
|
||||
return map.entries.fold<List<ReflectedDeclaration>>([], (out, entry) {
|
||||
var v = entry.value;
|
||||
|
||||
if (v is VariableMirror) {
|
||||
var decl = ReflectedDeclaration(v.simpleName, v.isStatic, null);
|
||||
return out..add(decl);
|
||||
}
|
||||
if (v is MethodMirror) {
|
||||
var decl = ReflectedDeclaration(
|
||||
v.simpleName, v.isStatic, _GeneratedReflectedFunction(v, reflector));
|
||||
return out..add(decl);
|
||||
} else {
|
||||
return out;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ReflectedTypeParameter _convertTypeVariable(TypeVariableMirror mirror) {
|
||||
return ReflectedTypeParameter(mirror.simpleName);
|
||||
}
|
||||
|
||||
ReflectedParameter _convertParameter(
|
||||
ParameterMirror mirror, Reflector reflector) {
|
||||
return ReflectedParameter(
|
||||
mirror.simpleName,
|
||||
mirror.metadata
|
||||
.map(reflector.reflectInstance)
|
||||
.whereType<ReflectedInstance>()
|
||||
.toList(),
|
||||
reflector.reflectType(mirror.type.reflectedType)!,
|
||||
!mirror.isOptional,
|
||||
mirror.isNamed);
|
||||
}
|
18
packages/container/container_generator/pubspec.yaml
Normal file
18
packages/container/container_generator/pubspec.yaml
Normal file
|
@ -0,0 +1,18 @@
|
|||
name: platform_container_generator
|
||||
version: 9.0.0
|
||||
description: Codegen support for using pkg:reflectable with pkg:angel3_container.
|
||||
homepage: https://angel3-framework.web.app/
|
||||
repository: https://github.com/dart-backend/angel/tree/master/packages/container/angel_container_generator
|
||||
environment:
|
||||
sdk: '>=3.3.0 <4.0.0'
|
||||
dependencies:
|
||||
platform_container: ^9.0.0
|
||||
reflectable: ^4.0.0
|
||||
dev_dependencies:
|
||||
build_runner: ^2.4.0
|
||||
build_test: ^2.1.0
|
||||
test: ^1.24.0
|
||||
lints: ^4.0.0
|
||||
# dependency_overrides:
|
||||
# angel3_container:
|
||||
# path: ../angel_container
|
179
packages/container/container_generator/test/reflector_test.dart
Normal file
179
packages/container/container_generator/test/reflector_test.dart
Normal file
|
@ -0,0 +1,179 @@
|
|||
import 'package:platform_container/platform_container.dart';
|
||||
import 'package:platform_container_generator/angel3_container_generator.dart';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'reflector_test.reflectable.dart';
|
||||
|
||||
void main() {
|
||||
initializeReflectable();
|
||||
|
||||
var reflector = const GeneratedReflector();
|
||||
late Container container;
|
||||
|
||||
setUp(() {
|
||||
container = Container(reflector);
|
||||
container.registerSingleton(Artist(name: 'Stevie Wonder'));
|
||||
});
|
||||
|
||||
group('reflectClass', () {
|
||||
var mirror = reflector.reflectClass(Artist);
|
||||
|
||||
test('name', () {
|
||||
expect(mirror.name, 'Artist');
|
||||
});
|
||||
});
|
||||
|
||||
test('inject constructor parameters', () {
|
||||
var album = container.make<Album>();
|
||||
print(album.title);
|
||||
expect(album.title, 'flowers by stevie wonder');
|
||||
});
|
||||
|
||||
// Skip as pkg:reflectable cannot reflect on closures at all (yet)
|
||||
//testReflector(reflector);
|
||||
}
|
||||
|
||||
@contained
|
||||
void returnVoidFromAFunction(int x) {}
|
||||
|
||||
void testReflector(Reflector reflector) {
|
||||
var blaziken = Pokemon('Blaziken', PokemonType.fire);
|
||||
late Container container;
|
||||
|
||||
setUp(() {
|
||||
container = Container(reflector);
|
||||
container.registerSingleton(blaziken);
|
||||
});
|
||||
|
||||
test('get field', () {
|
||||
var blazikenMirror = reflector.reflectInstance(blaziken)!;
|
||||
expect(blazikenMirror.getField('type').reflectee, blaziken.type);
|
||||
});
|
||||
|
||||
group('reflectFunction', () {
|
||||
var mirror = reflector.reflectFunction(returnVoidFromAFunction);
|
||||
|
||||
test('void return type returns dynamic', () {
|
||||
expect(mirror?.returnType, reflector.reflectType(dynamic));
|
||||
});
|
||||
|
||||
test('counts parameters', () {
|
||||
expect(mirror?.parameters, hasLength(1));
|
||||
});
|
||||
|
||||
test('counts types parameters', () {
|
||||
expect(mirror?.typeParameters, isEmpty);
|
||||
});
|
||||
|
||||
test('correctly reflects parameter types', () {
|
||||
var p = mirror?.parameters[0];
|
||||
expect(p?.name, 'x');
|
||||
expect(p?.isRequired, true);
|
||||
expect(p?.isNamed, false);
|
||||
expect(p?.annotations, isEmpty);
|
||||
expect(p?.type, reflector.reflectType(int));
|
||||
});
|
||||
}, skip: 'pkg:reflectable cannot reflect on closures at all (yet)');
|
||||
|
||||
test('make on singleton type returns singleton', () {
|
||||
expect(container.make(Pokemon), blaziken);
|
||||
});
|
||||
|
||||
test('make with generic returns same as make with explicit type', () {
|
||||
expect(container.make<Pokemon>(), blaziken);
|
||||
});
|
||||
|
||||
test('make on aliased singleton returns singleton', () {
|
||||
container.registerSingleton(blaziken, as: StateError);
|
||||
expect(container.make(StateError), blaziken);
|
||||
});
|
||||
|
||||
test('constructor injects singleton', () {
|
||||
var lower = container.make<LowerPokemon>();
|
||||
expect(lower.lowercaseName, blaziken.name.toLowerCase());
|
||||
});
|
||||
|
||||
test('newInstance works', () {
|
||||
var type = container.reflector.reflectType(Pokemon)!;
|
||||
var instance =
|
||||
type.newInstance('changeName', [blaziken, 'Charizard']).reflectee
|
||||
as Pokemon;
|
||||
print(instance);
|
||||
expect(instance.name, 'Charizard');
|
||||
expect(instance.type, PokemonType.fire);
|
||||
});
|
||||
|
||||
test('isAssignableTo', () {
|
||||
var pokemonType = container.reflector.reflectType(Pokemon);
|
||||
var kantoPokemonType = container.reflector.reflectType(KantoPokemon)!;
|
||||
|
||||
expect(kantoPokemonType.isAssignableTo(pokemonType), true);
|
||||
|
||||
expect(
|
||||
kantoPokemonType
|
||||
.isAssignableTo(container.reflector.reflectType(String)),
|
||||
false);
|
||||
});
|
||||
}
|
||||
|
||||
@contained
|
||||
class LowerPokemon {
|
||||
final Pokemon pokemon;
|
||||
|
||||
LowerPokemon(this.pokemon);
|
||||
|
||||
String get lowercaseName => pokemon.name.toLowerCase();
|
||||
}
|
||||
|
||||
@contained
|
||||
class Pokemon {
|
||||
final String name;
|
||||
final PokemonType type;
|
||||
|
||||
Pokemon(this.name, this.type);
|
||||
|
||||
factory Pokemon.changeName(Pokemon other, String name) {
|
||||
return Pokemon(name, other.type);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'NAME: $name, TYPE: $type';
|
||||
}
|
||||
|
||||
@contained
|
||||
class KantoPokemon extends Pokemon {
|
||||
KantoPokemon(super.name, super.type);
|
||||
}
|
||||
|
||||
@contained
|
||||
enum PokemonType { water, fire, grass, ice, poison, flying }
|
||||
|
||||
@contained
|
||||
class Artist {
|
||||
final String name;
|
||||
|
||||
Artist({required this.name});
|
||||
|
||||
String get lowerName {
|
||||
return name.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
@contained
|
||||
class Album {
|
||||
final Artist artist;
|
||||
|
||||
Album(this.artist);
|
||||
|
||||
String get title => 'flowers by ${artist.lowerName}';
|
||||
}
|
||||
|
||||
@contained
|
||||
class AlbumLength {
|
||||
final Artist artist;
|
||||
final Album album;
|
||||
|
||||
AlbumLength(this.artist, this.album);
|
||||
|
||||
int get totalLength => artist.name.length + album.title.length;
|
||||
}
|
File diff suppressed because it is too large
Load diff
71
packages/exceptions/.gitignore
vendored
Normal file
71
packages/exceptions/.gitignore
vendored
Normal file
|
@ -0,0 +1,71 @@
|
|||
# See https://www.dartlang.org/tools/private-files.html
|
||||
|
||||
# Files and directories created by pub
|
||||
.dart_tool
|
||||
.packages
|
||||
.pub/
|
||||
build/
|
||||
|
||||
# If you're building an application, you may want to check-in your pubspec.lock
|
||||
pubspec.lock
|
||||
|
||||
# Directory created by dartdoc
|
||||
# If you don't generate documentation locally you can remove this line.
|
||||
doc/api/
|
||||
|
||||
### Dart template
|
||||
# See https://www.dartlang.org/tools/private-files.html
|
||||
|
||||
# Files and directories created by pub
|
||||
|
||||
# SDK 1.20 and later (no longer creates packages directories)
|
||||
|
||||
# Older SDK versions
|
||||
# (Include if the minimum SDK version specified in pubsepc.yaml is earlier than 1.20)
|
||||
.project
|
||||
.buildlog
|
||||
**/packages/
|
||||
|
||||
|
||||
# Files created by dart2js
|
||||
# (Most Dart developers will use pub build to compile Dart, use/modify these
|
||||
# rules if you intend to use dart2js directly
|
||||
# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
|
||||
# differentiate from explicit Javascript files)
|
||||
*.dart.js
|
||||
*.part.js
|
||||
*.js.deps
|
||||
*.js.map
|
||||
*.info.json
|
||||
|
||||
# Directory created by dartdoc
|
||||
|
||||
# Don't commit pubspec lock file
|
||||
# (Library packages only! Remove pattern if developing an application package)
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff:
|
||||
|
||||
## VsCode
|
||||
.vscode/
|
||||
|
||||
## File-based project format:
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
.idea/
|
||||
/out/
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
12
packages/exceptions/AUTHORS.md
Normal file
12
packages/exceptions/AUTHORS.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
Primary Authors
|
||||
===============
|
||||
|
||||
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
|
||||
|
||||
Thomas is the current maintainer of the code base. He has refactored and migrated the
|
||||
code base to support NNBD.
|
||||
|
||||
* __[Tobe O](thosakwe@gmail.com)__
|
||||
|
||||
Tobe has written much of the original code prior to NNBD migration. He has moved on and
|
||||
is no longer involved with the project.
|
72
packages/exceptions/CHANGELOG.md
Normal file
72
packages/exceptions/CHANGELOG.md
Normal file
|
@ -0,0 +1,72 @@
|
|||
|
||||
# Change Log
|
||||
|
||||
## 8.1.1
|
||||
|
||||
* Updated repository link
|
||||
|
||||
## 8.1.0
|
||||
|
||||
* Updated `lints` to 3.0.0
|
||||
|
||||
## 8.0.0
|
||||
|
||||
* Require Dart >= 3.0
|
||||
|
||||
## 7.0.0
|
||||
|
||||
* Require Dart >= 2.17
|
||||
|
||||
## 6.0.1
|
||||
|
||||
* Updated README
|
||||
|
||||
## 6.0.0
|
||||
|
||||
* Require Dart >= 2.16
|
||||
* [**Breaking**] `error` for `AngelHttpException` is no longer mandatory
|
||||
|
||||
## 5.0.0
|
||||
|
||||
* Skipped release
|
||||
|
||||
## 4.0.0
|
||||
|
||||
* Skipped release
|
||||
|
||||
## 3.1.0
|
||||
|
||||
* Upgraded to `lints` linter
|
||||
|
||||
## 3.0.2
|
||||
|
||||
* Updated LICENSE link
|
||||
|
||||
## 3.0.1
|
||||
|
||||
* Updated README
|
||||
|
||||
## 3.0.0
|
||||
|
||||
* Migrated to support Dart >= 2.12 NNBD
|
||||
|
||||
## 2.0.0
|
||||
|
||||
* Migrated to work with Dart >= 2.12 Non NNBD
|
||||
|
||||
## 1.1.0
|
||||
|
||||
* Emit `is_error` and `status_code` in `toJson()`.
|
||||
* No more `camelCase` at all.
|
||||
|
||||
## 1.0.0+3
|
||||
|
||||
* Slightly relax the deserialization of `errors`.
|
||||
|
||||
## 1.0.0+2
|
||||
|
||||
* Added a backwards-compatible way to cast the `errors` List.
|
||||
|
||||
## 1.0.0+1
|
||||
|
||||
* Dart 2 updates.
|
29
packages/exceptions/LICENSE
Normal file
29
packages/exceptions/LICENSE
Normal file
|
@ -0,0 +1,29 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2021, dukefirehawk.com
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
8
packages/exceptions/README.md
Normal file
8
packages/exceptions/README.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# Angel3 Http Exception
|
||||
|
||||
![Pub Version (including pre-releases)](https://img.shields.io/pub/v/angel3_http_exception?include_prereleases)
|
||||
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
|
||||
[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion)
|
||||
[![License](https://img.shields.io/github/license/dart-backend/angel)](https://github.com/dart-backend/angel/tree/master/packages/http_exception/LICENSE)
|
||||
|
||||
Exception class that can be serialized to JSON and serialized to clients. Angel3's HTTP exception class.
|
1
packages/exceptions/analysis_options.yaml
Normal file
1
packages/exceptions/analysis_options.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
include: package:lints/recommended.yaml
|
3
packages/exceptions/example/main.dart
Normal file
3
packages/exceptions/example/main.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
import 'package:platform_exceptions/http_exception.dart';
|
||||
|
||||
void main() => throw HttpException.notFound(message: "Can't find that page!");
|
121
packages/exceptions/lib/http_exception.dart
Normal file
121
packages/exceptions/lib/http_exception.dart
Normal file
|
@ -0,0 +1,121 @@
|
|||
library http_exception;
|
||||
|
||||
//import 'package:dart2_constant/convert.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
/// Exception class that can be serialized to JSON and serialized to clients.
|
||||
/// Carries HTTP-specific metadata, like [statusCode].
|
||||
///
|
||||
/// Originally inspired by
|
||||
/// [feathers-errors](https://github.com/feathersjs/feathers-errors).
|
||||
class HttpException implements Exception {
|
||||
/// A list of errors that occurred when this exception was thrown.
|
||||
final List<String> errors = [];
|
||||
|
||||
/// The error throw by exception.
|
||||
dynamic error;
|
||||
|
||||
/// The cause of this exception.
|
||||
String message;
|
||||
|
||||
/// The [StackTrace] associated with this error.
|
||||
StackTrace? stackTrace;
|
||||
|
||||
/// An HTTP status code this exception will throw.
|
||||
int statusCode;
|
||||
|
||||
HttpException(
|
||||
{this.message = '500 Internal Server Error',
|
||||
this.stackTrace,
|
||||
this.statusCode = 500,
|
||||
this.error,
|
||||
List<String> errors = const []}) {
|
||||
this.errors.addAll(errors);
|
||||
}
|
||||
|
||||
Map toJson() {
|
||||
return {
|
||||
'is_error': true,
|
||||
'status_code': statusCode,
|
||||
'message': message,
|
||||
'errors': errors
|
||||
};
|
||||
}
|
||||
|
||||
Map toMap() => toJson();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '$statusCode: $message';
|
||||
}
|
||||
|
||||
factory HttpException.fromMap(Map data) {
|
||||
return HttpException(
|
||||
statusCode: (data['status_code'] ?? data['statusCode'] ?? 500) as int,
|
||||
message: data['message']?.toString() ?? 'Internal Server Error',
|
||||
errors: data['errors'] is Iterable
|
||||
? ((data['errors'] as Iterable).map((x) => x.toString()).toList())
|
||||
: <String>[],
|
||||
);
|
||||
}
|
||||
|
||||
factory HttpException.fromJson(String str) =>
|
||||
HttpException.fromMap(json.decode(str) as Map);
|
||||
|
||||
/// Throws a 400 Bad Request error, including an optional arrray of (validation?)
|
||||
/// errors you specify.
|
||||
factory HttpException.badRequest(
|
||||
{String message = '400 Bad Request',
|
||||
List<String> errors = const []}) =>
|
||||
HttpException(message: message, errors: errors, statusCode: 400);
|
||||
|
||||
/// Throws a 401 Not Authenticated error.
|
||||
factory HttpException.notAuthenticated(
|
||||
{String message = '401 Not Authenticated'}) =>
|
||||
HttpException(message: message, statusCode: 401);
|
||||
|
||||
/// Throws a 402 Payment Required error.
|
||||
factory HttpException.paymentRequired(
|
||||
{String message = '402 Payment Required'}) =>
|
||||
HttpException(message: message, statusCode: 402);
|
||||
|
||||
/// Throws a 403 Forbidden error.
|
||||
factory HttpException.forbidden({String message = '403 Forbidden'}) =>
|
||||
HttpException(message: message, statusCode: 403);
|
||||
|
||||
/// Throws a 404 Not Found error.
|
||||
factory HttpException.notFound({String message = '404 Not Found'}) =>
|
||||
HttpException(message: message, statusCode: 404);
|
||||
|
||||
/// Throws a 405 Method Not Allowed error.
|
||||
factory HttpException.methodNotAllowed(
|
||||
{String message = '405 Method Not Allowed'}) =>
|
||||
HttpException(message: message, statusCode: 405);
|
||||
|
||||
/// Throws a 406 Not Acceptable error.
|
||||
factory HttpException.notAcceptable(
|
||||
{String message = '406 Not Acceptable'}) =>
|
||||
HttpException(message: message, statusCode: 406);
|
||||
|
||||
/// Throws a 408 Timeout error.
|
||||
factory HttpException.methodTimeout({String message = '408 Timeout'}) =>
|
||||
HttpException(message: message, statusCode: 408);
|
||||
|
||||
/// Throws a 409 Conflict error.
|
||||
factory HttpException.conflict({String message = '409 Conflict'}) =>
|
||||
HttpException(message: message, statusCode: 409);
|
||||
|
||||
/// Throws a 422 Not Processable error.
|
||||
factory HttpException.notProcessable(
|
||||
{String message = '422 Not Processable'}) =>
|
||||
HttpException(message: message, statusCode: 422);
|
||||
|
||||
/// Throws a 501 Not Implemented error.
|
||||
factory HttpException.notImplemented(
|
||||
{String message = '501 Not Implemented'}) =>
|
||||
HttpException(message: message, statusCode: 501);
|
||||
|
||||
/// Throws a 503 Unavailable error.
|
||||
factory HttpException.unavailable({String message = '503 Unavailable'}) =>
|
||||
HttpException(message: message, statusCode: 503);
|
||||
}
|
9
packages/exceptions/pubspec.yaml
Normal file
9
packages/exceptions/pubspec.yaml
Normal file
|
@ -0,0 +1,9 @@
|
|||
name: platform_exceptions
|
||||
version: 9.0.0
|
||||
description: Exception pakcage
|
||||
homepage: https://angel3-framework.web.app/
|
||||
repository: https://github.com/dart-backend/angel/tree/master/packages/http_exception
|
||||
environment:
|
||||
sdk: '>=3.3.0 <4.0.0'
|
||||
dev_dependencies:
|
||||
lints: ^4.0.0
|
12
packages/framework/AUTHORS.md
Normal file
12
packages/framework/AUTHORS.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
Primary Authors
|
||||
===============
|
||||
|
||||
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
|
||||
|
||||
Thomas is the current maintainer of the code base. He has refactored and migrated the
|
||||
code base to support NNBD.
|
||||
|
||||
* __[Tobe O](thosakwe@gmail.com)__
|
||||
|
||||
Tobe has written much of the original code prior to NNBD migration. He has moved on and
|
||||
is no longer involved with the project.
|
452
packages/framework/CHANGELOG.md
Normal file
452
packages/framework/CHANGELOG.md
Normal file
|
@ -0,0 +1,452 @@
|
|||
# Change Log
|
||||
|
||||
## 8.4.0
|
||||
|
||||
* Require Dart >= 3.3
|
||||
* Updated `lints` to 4.0.0
|
||||
|
||||
## 8.3.2
|
||||
|
||||
* Updated README
|
||||
|
||||
## 8.3.1
|
||||
|
||||
* Updated repository link
|
||||
|
||||
## 8.3.0
|
||||
|
||||
* Updated `lints` to 3.0.0
|
||||
* Fixed linter warnings
|
||||
|
||||
## 8.2.0
|
||||
|
||||
* Add `addResponseHeader` to `AngelHttp` to add headers to HTTP default response
|
||||
* Add `removeResponseHeader` to `AngelHttp` to remove headers from HTTP default response
|
||||
|
||||
## 8.1.1
|
||||
|
||||
* Updated broken image on README
|
||||
|
||||
## 8.1.0
|
||||
|
||||
* Updated `uuid` to 4.0.0
|
||||
|
||||
## 8.0.0
|
||||
|
||||
* Require Dart >= 3.0
|
||||
* Updated `http` to 1.0.0
|
||||
|
||||
## 7.0.4
|
||||
|
||||
* Updated `Expose` fields to non-nullable
|
||||
* Updated `Controller` to use non-nullable field
|
||||
|
||||
## 7.0.3
|
||||
|
||||
* Fixed issue #83. Allow Http request to return null headers instead of throwing an exception.
|
||||
|
||||
## 7.0.2
|
||||
|
||||
* Added performance benchmark to README
|
||||
|
||||
## 7.0.1
|
||||
|
||||
* Fixed `BytesBuilder` warnings
|
||||
|
||||
## 7.0.0
|
||||
|
||||
* Require Dart >= 2.17
|
||||
|
||||
## 6.0.0
|
||||
|
||||
* Require Dart >= 2.16
|
||||
* Updated `container` to non nullable
|
||||
* Updated `angel` to non nullable
|
||||
* Updated `logger` to non nullable
|
||||
* Refactored error handler
|
||||
|
||||
## 5.0.0
|
||||
|
||||
* Skipped release
|
||||
|
||||
## 4.2.4
|
||||
|
||||
* Fixed issue 48. Log not working in development
|
||||
|
||||
## 4.2.3
|
||||
|
||||
* Fixed `res.json()` throwing bad state exception
|
||||
|
||||
## 4.2.2
|
||||
|
||||
* Added `Date` to response header
|
||||
* Updated `Server: Angel3` response header
|
||||
|
||||
## 4.2.1
|
||||
|
||||
* Updated `package:angel3_container`
|
||||
|
||||
## 4.2.0
|
||||
|
||||
* Updated to `package:belatuk_combinator`
|
||||
* Updated to `package:belatuk_merge_map`
|
||||
* Updated linter to `package:lints`
|
||||
|
||||
## 4.1.3
|
||||
|
||||
* Updated README
|
||||
|
||||
## 4.1.2
|
||||
|
||||
* Updated README
|
||||
* Fixed NNBD issues
|
||||
|
||||
## 4.1.1
|
||||
|
||||
* Updated link to `Angel3` home page
|
||||
* Fixed pedantic warnings
|
||||
|
||||
## 4.1.0
|
||||
|
||||
* Replaced `http_server` with `belatuk_http_server`
|
||||
|
||||
## 4.0.4
|
||||
|
||||
* Fixed response returning incorrect status code
|
||||
|
||||
## 4.0.3
|
||||
|
||||
* Fixed "Primitive after parsed param injection" test case
|
||||
* Fixed "Cannot remove all unless explicitly set" test case
|
||||
* Fixed "null" test case
|
||||
|
||||
## 4.0.2
|
||||
|
||||
* Updated README
|
||||
|
||||
## 4.0.1
|
||||
|
||||
* Updated README
|
||||
|
||||
## 4.0.0
|
||||
|
||||
* Migrated to support Dart >= 2.12 NNBD
|
||||
|
||||
## 3.0.0
|
||||
|
||||
* Migrated to work with Dart >= 2.12 Non NNBD
|
||||
|
||||
## 2.1.1
|
||||
|
||||
* `AngelHttp.uri` now returns an empty `Uri` if the server is not listening.
|
||||
|
||||
## 2.1.0
|
||||
|
||||
* This release was originally planned to be `2.0.5`, but it adds several features, and has
|
||||
therefore been bumped to `2.1.0`.
|
||||
* Fix a new (did not appear before 2.6/2.7) type error causing compilation to fail.
|
||||
<https://github.com/angel-dart/framework/issues/249>
|
||||
|
||||
## 2.0.5-beta
|
||||
|
||||
* Make `@Expose()` in `Controller` optional. <https://github.com/angel-dart/angel/issues/107>
|
||||
* Add `allowHttp1` to `AngelHttp2` constructors. <https://github.com/angel-dart/angel/issues/108>
|
||||
* Add `deserializeBody` and `decodeBody` to `RequestContext`. <https://github.com/angel-dart/angel/issues/109>
|
||||
* Add `HostnameRouter`, which allows for routing based on hostname. <https://github.com/angel-dart/angel/issues/110>
|
||||
* Default to using `ThrowingReflector`, instead of `EmptyReflector`. This will give a more descriptive
|
||||
error when trying to use controllers, etc. without reflection enabled.
|
||||
* `mountController` returns the mounted controller.
|
||||
|
||||
## 2.0.4+1
|
||||
|
||||
* Run `Controller.configureRoutes` before mounting `@Expose` routes.
|
||||
* Make `Controller.configureServer` always return a `Future`.
|
||||
|
||||
## 2.0.4
|
||||
|
||||
* Prepare for Dart SDK change to `Stream<List<int>>` that are now
|
||||
`Stream<Uint8List>`.
|
||||
* Accept any content type if accept header is missing. See
|
||||
[this PR](https://github.com/angel-dart/framework/pull/239).
|
||||
|
||||
## 2.0.3
|
||||
|
||||
* Patch up a bug caused by an upstream change to Dart's stream semantics.
|
||||
See more: <https://github.com/angel-dart/angel/issues/106#issuecomment-499564485>
|
||||
|
||||
## 2.0.2+1
|
||||
|
||||
* Fix a bug in the implementation of `Controller.applyRoutes`.
|
||||
|
||||
## 2.0.2
|
||||
|
||||
* Make `ResponseContext` *explicitly* implement `StreamConsumer` (though technically it already did???)
|
||||
* Split `Controller.configureServer` to create `Controller.applyRoutes`.
|
||||
|
||||
## 2.0.1
|
||||
|
||||
* Tracked down a bug in `Driver.runPipeline` that allowed fallback
|
||||
handlers to run, even after the response was closed.
|
||||
* Add `RequestContext.shutdownHooks`.
|
||||
* Call `RequestContext.close` in `Driver.sendResponse`.
|
||||
* AngelConfigurer is now `FutureOr<void>`, instead of just `FutureOr`.
|
||||
* Use a `Container.has<Stopwatch>` check in `Driver.sendResponse`.
|
||||
* Remove unnecessary `new` and `const`.
|
||||
|
||||
## 2.0.0
|
||||
|
||||
* Angel 2! :angel: :rocket:
|
||||
|
||||
## 2.0.0-rc.10
|
||||
|
||||
* Fix an error that prevented `AngelHttp2.custom` from working properly.
|
||||
* Add `startSharedHttp2`.
|
||||
|
||||
## 2.0.0-rc.9
|
||||
|
||||
* Fix some bugs in the `HookedService` implementation that skipped
|
||||
the outputs of `before` events.
|
||||
|
||||
## 2.0.0-rc.8
|
||||
|
||||
* Fix `MapService` flaw where clients could remove all records, even if `allowRemoveAll` were `false`.
|
||||
|
||||
## 2.0.0-rc.7
|
||||
|
||||
* `AnonymousService` can override `readData`.
|
||||
* `Service.map` now overrides `readData`.
|
||||
* `HookedService.readData` forwards to `inner`.
|
||||
|
||||
## 2.0.0-rc.6
|
||||
|
||||
* Make `redirect` and `download` methods asynchronous.
|
||||
|
||||
## 2.0.0-rc.5
|
||||
|
||||
* Make `serializer` `FutureOr<String> Function(Object)`.
|
||||
* Make `ResponseContext.serialize` return `Future<bool>`.
|
||||
|
||||
## 2.0.0-rc.4
|
||||
|
||||
* Support resolution of asynchronous injections in controllers and `ioc`.
|
||||
* Inject `RequestContext` and `ResponseContext` into requests.
|
||||
|
||||
## 2.0.0-rc.3
|
||||
|
||||
* `MapService.modify` was not actually modifying items.
|
||||
|
||||
## 2.0.0-rc.2
|
||||
|
||||
* Fixes Pub analyzer lints (see `angel_route@3.0.6`)
|
||||
|
||||
## 2.0.0-rc.1
|
||||
|
||||
* Fix logic error that allowed content to be written to streaming responses after `close` was closed.
|
||||
|
||||
## 2.0.0-rc.0
|
||||
|
||||
* Log a warning when no `reflector` is provided.
|
||||
* Add `AngelEnvironment` class.
|
||||
* Add `Angel.environment`.
|
||||
* Deprecated `app.isProduction` in favor of `app.environment.isProduction`.
|
||||
* Allow setting of `bodyAsObject`, `bodyAsMap`, or `bodyAsList` **exactly once**.
|
||||
* Resolve named singletons in `resolveInjection`.
|
||||
* Fix a bug where `Service.parseId<double>` would attempt to parse an `int`.
|
||||
* Replace as Data cast in Service.dart with a method that throws a 400 on error.
|
||||
|
||||
## 2.0.0-alpha.24
|
||||
|
||||
* Add `AngelEnv` class to `core`.
|
||||
* Deprecate `Angel.isProduction`, in favor of `AngelEnv`.
|
||||
|
||||
## 2.0.0-alpha.23
|
||||
|
||||
* `ResponseContext.render` sets `charset` to `utf8` in `contentType`.
|
||||
|
||||
## 2.0.0-alpha.22
|
||||
|
||||
* Update pipeline handling mechanism, and inject a `MiddlewarePipelineIterator`.
|
||||
* This allows routes to know where in the resolution process they exist, at runtime.
|
||||
|
||||
## 2.0.0-alpha.21
|
||||
|
||||
* Update for `angel_route@3.0.4` compatibility.
|
||||
* Add `readAsBytes` and `readAsString` to `UploadedFile`.
|
||||
* URI-decode path components in HTTP2.
|
||||
|
||||
## 2.0.0-alpha.20
|
||||
|
||||
* Inject the `MiddlewarePipeline` into requests.
|
||||
|
||||
## 2.0.0-alpha.19
|
||||
|
||||
* `parseBody` checks for null content type, and throws a `400` if none was given.
|
||||
* Add `ResponseContext.contentLength`.
|
||||
* Update `streamFile` to set content length, and also to work on `HEAD` requests.
|
||||
|
||||
## 2.0.0-alpha.18
|
||||
|
||||
* Upgrade `http2` dependency.
|
||||
* Upgrade `uuid` dependency.
|
||||
* Fixed a bug that prevented body parsing from ever completing with `http2`.
|
||||
* Add `Providers.hashCode`.
|
||||
|
||||
## 2.0.0-alpha.17
|
||||
|
||||
* Revert the migration to `lumberjack` for now. In the future, when it's more
|
||||
stable, there'll be a conversion, perhaps.
|
||||
|
||||
## 2.0.0-alpha.16
|
||||
|
||||
* Use `package:lumberjack` for logging.
|
||||
|
||||
## 2.0.0-alpha.15
|
||||
|
||||
* Remove dependency on `body_parser`.
|
||||
* `RequestContext` now exposes a `Stream<List<int>> get body` getter.
|
||||
* Calling `RequestContext.parseBody()` parses its contents.
|
||||
* Added `bodyAsMap`, `bodyAsList`, `bodyAsObject`, and `uploadedFiles` to `RequestContext`.
|
||||
* Removed `Angel.keepRawRequestBuffers` and anything that had to do with buffering request bodies.
|
||||
|
||||
## 2.0.0-alpha.14
|
||||
|
||||
* Patch `HttpResponseContext._openStream` to send content-length.
|
||||
|
||||
## 2.0.0-alpha.13
|
||||
|
||||
* Fixed a logic error in `HttpResponseContext` that prevented status codes from being sent.
|
||||
|
||||
## 2.0.0-alpha.12
|
||||
|
||||
* Remove `ResponseContext.sendFile`.
|
||||
* Add `Angel.mimeTypeResolver`.
|
||||
* Fix a bug where an unknown MIME type on `streamFile` would return a 500.
|
||||
|
||||
## 2.0.0-alpha.11
|
||||
|
||||
* Add `readMany` to `Service`.
|
||||
* Allow `ResponseContext.redirect` to take a `Uri`.
|
||||
* Add `Angel.mountController`.
|
||||
* Add `Angel.findServiceOf`.
|
||||
* Roll in HTTP/2. See `pkg:angel_framework/http2.dart`.
|
||||
|
||||
## 2.0.0-alpha.10
|
||||
|
||||
* All calls to `Service.parseId` are now affixed with the `<Id>` argument.
|
||||
* Added `uri` getter to `AngelHttp`.
|
||||
* The default for `parseQuery` now wraps query parameters in `Map<String, dynamic>.from`.
|
||||
This resolves a bug in `package:angel_validate`.
|
||||
|
||||
## 2.0.0-alpha.9
|
||||
|
||||
* Add `Service.map`.
|
||||
|
||||
## 2.0.0-alpha.8
|
||||
|
||||
* No longer export HTTP-specific code from `angel_framework.dart`.
|
||||
An import of `import 'package:angel_framework/http.dart';` will be necessary in most cases now.
|
||||
|
||||
## 2.0.0-alpha.7
|
||||
|
||||
* Force a tigher contract on services. They now must return `Data` on all
|
||||
methods except for `index`, which returns a `List<Data>`.
|
||||
|
||||
## 2.0.0-alpha.6
|
||||
|
||||
* Allow passing a custom `Container` to `handleContained` and co.
|
||||
|
||||
## 2.0.0-alpha.5
|
||||
|
||||
* `MapService` methods now explicitly return `Map<String, dynamic>`.
|
||||
|
||||
## 2.0.0-alpha.4
|
||||
|
||||
* Renamed `waterfall` to `chain`.
|
||||
* Renamed `Routable.service` to `Routable.findService`.
|
||||
* Also `Routable.findHookedService`.
|
||||
|
||||
## 2.0.0-alpha.3
|
||||
|
||||
* Added `<Id, Data>` type parameters to `Service`.
|
||||
* `HookedService` now follows suit, and takes a third parameter, pointing to the inner service.
|
||||
* `Routable.use` now uses the generic parameters added to `Service`.
|
||||
* Added generic usage to `HookedServiceListener`, etc.
|
||||
* All service methods take `Map<String, dynamic>` as `params` now.
|
||||
|
||||
## 2.0.0-alpha.2
|
||||
|
||||
* Added `ResponseContext.detach`.
|
||||
|
||||
## 2.0.0-alpha.1
|
||||
|
||||
* Removed `Angel.injectEncoders`.
|
||||
* Added `Providers.toJson`.
|
||||
* Moved `Providers.graphql` to `Providers.graphQL`.
|
||||
* `Angel.optimizeForProduction` no longer calls `preInject`,
|
||||
as it does not need to.
|
||||
* Rename `ResponseContext.enableBuffer` to `ResponseContext.useBuffer`.
|
||||
|
||||
## 2.0.0-alpha
|
||||
|
||||
* Removed `random_string` dependency.
|
||||
* Moved reflection to `package:angel_container`.
|
||||
* Upgraded `package:file` to `5.0.0`.
|
||||
* `ResponseContext.sendFile` now uses `package:file`.
|
||||
* Abandon `ContentType` in favor of `MediaType`.
|
||||
* Changed view engine to use `Map<String, dynamic>`.
|
||||
* Remove dependency on `package:json_god` by default.
|
||||
* Remove dependency on `package:dart2_constant`.
|
||||
* Moved `lib/hooks.dart` into `package:angel_hooks`.
|
||||
* Moved `TypedService` into `package:angel_typed_service`.
|
||||
* Completely removed the `AngelBase` class.
|
||||
* Removed all `@deprecated` symbols.
|
||||
* `Service.toId` was renamed to `Service.parseId`; it also now uses its
|
||||
single type argument to determine how to parse a value. \* In addition, this method was also made `static`.
|
||||
* `RequestContext` and `ResponseContext` are now generic, and take a
|
||||
single type argument pointing to the underlying request/response type,
|
||||
respectively.
|
||||
* `RequestContext.io` and `ResponseContext.io` are now permanently
|
||||
gone.
|
||||
* `HttpRequestContextImpl` and `HttpResponseContextImpl` were renamed to
|
||||
`HttpRequestContext` and `HttpResponseContext`.
|
||||
* Lazy-parsing request bodies is now the default; `Angel.lazyParseBodies` was replaced
|
||||
with `Angel.eagerParseRequestBodies`.
|
||||
* `Angel.storeOriginalBuffer` -> `Angel.storeRawRequestBuffers`.
|
||||
* The methods `lazyBody`, `lazyFiles`, and `lazyOriginalBuffer` on `ResponseContext` were all
|
||||
replaced with `parseBody`, `parseUploadedFiles`, and `parseRawRequestBuffer`, respectively.
|
||||
* Removed the synchronous equivalents of the above methods (`body`, `files`, and `originalBuffer`),
|
||||
as well as `query`.
|
||||
* Removed `Angel.injections` and `RequestContext.injections`.
|
||||
* Removed `Angel.inject` and `RequestContext.inject`.
|
||||
* Removed a dependency on `package:pool`, which also meant removing `AngelHttp.throttle`.
|
||||
* Remove the `RequestMiddleware` typedef; from now on, one should use `ResponseContext.end`
|
||||
exclusively to close responses.
|
||||
* `waterfall` will now only accept `RequestHandler`.
|
||||
* `Routable`, and all of its subclasses, now extend `Router<RequestHandler>`, and therefore only
|
||||
take routes in the form of `FutureOr myFunc(RequestContext, ResponseContext res)`.
|
||||
* `@Middleware` now takes an `Iterable` of `RequestHandler`s.
|
||||
* `@Expose.path` now *must* be a `String`, not just any `Pattern`.
|
||||
* `@Expose.middleware` now takes `Iterable<RequestHandler>`, instead of just `List`.
|
||||
* `createDynamicHandler` was renamed to `ioc`, and is now used to run IoC-aware handlers in a
|
||||
type-safe manner.
|
||||
* `RequestContext.params` is now a `Map<String, dynamic>`, rather than just a `Map`.
|
||||
* Removed `RequestContext.grab`.
|
||||
* Removed `RequestContext.properties`.
|
||||
* Removed the defunct `debug` property where it still existed.
|
||||
* `Routable.use` now only accepts a `Service`.
|
||||
* Removed `Angel.createZoneForRequest`.
|
||||
* Removed `Angel.defaultZoneCreator`.
|
||||
* Added all flags to the `Angel` constructor, ex. `Angel.eagerParseBodies`.
|
||||
* Fix a bug where synchronous errors in `handleRequest` would not be caught.
|
||||
* `AngelHttp.useZone` now defaults to `false`.
|
||||
* `ResponseContext` now starts in streaming mode by default; the response buffer is opt-in,
|
||||
as in many cases it is unnecessary and slows down response time.
|
||||
* `ResponseContext.streaming` was replaced by `ResponseContext.isBuffered`.
|
||||
* Made `LockableBytesBuilder` public.
|
||||
* Removed the now-obsolete `ResponseContext.willCloseItself`.
|
||||
* Removed `ResponseContext.dispose`.
|
||||
* Removed the now-obsolete `ResponseContext.end`.
|
||||
* Removed the now-obsolete `ResponseContext.releaseCorrespondingRequest`.
|
||||
* `preInject` now takes a `Reflector` as its second argument.
|
||||
* `Angel.reflector` defaults to `const EmptyReflector()`, disabling
|
||||
reflection out-of-the-box.
|
29
packages/framework/LICENSE
Normal file
29
packages/framework/LICENSE
Normal file
|
@ -0,0 +1,29 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2021, dukefirehawk.com
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
86
packages/framework/README.md
Normal file
86
packages/framework/README.md
Normal file
|
@ -0,0 +1,86 @@
|
|||
# Angel3 Framework
|
||||
|
||||
[![Angel3 Framework](../../angel3_logo.png)](https://github.com/dart-backend/angel)
|
||||
|
||||
![Pub Version (including pre-releases)](https://img.shields.io/pub/v/angel3_framework?include_prereleases)
|
||||
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
|
||||
[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion)
|
||||
[![License](https://img.shields.io/github/license/dart-backend/angel)](https://github.com/dart-backend/angel/tree/master/packages/framework/LICENSE)
|
||||
[![melos](https://img.shields.io/badge/maintained%20with-melos-f700ff.svg?style=flat-square)](https://github.com/invertase/melos)
|
||||
|
||||
Angel3 framework is a high-powered HTTP server with support for dependency injection, sophisticated routing, authentication, ORM, graphql etc. It is designed to keep the core minimal but extensible through a series of plugin packages. It won't dictate which features, databases or web templating engine to use. This flexibility enable Angel3 framework to grow with your application as new features can be added to handle the new use cases.
|
||||
|
||||
This package is the core package of [Angel3](https://github.com/dart-backend/angel). For more information, visit us at [Angel3 Website](https://angel3-framework.web.app).
|
||||
|
||||
## Installation and Setup
|
||||
|
||||
### (Option 1) Create a new project by cloning from boilerplate templates
|
||||
|
||||
1. Download and install [Dart](https://dart.dev/get-dart)
|
||||
|
||||
2. Clone one of the following starter projects:
|
||||
* [Angel3 Basic Template](https://github.com/dukefirehawk/boilerplates/tree/v7/angel3-basic)
|
||||
* [Angel3 ORM Template](https://github.com/dukefirehawk/boilerplates/tree/v7/angel3-orm)
|
||||
* [Angel3 ORM MySQL Template](https://github.com/dukefirehawk/boilerplates/tree/v7/angel3-orm-mysql)
|
||||
* [Angel3 Graphql Template](https://github.com/dukefirehawk/boilerplates/tree/v7/angel3-graphql)
|
||||
|
||||
3. Run the project in development mode (*hot-reloaded* is enabled on file changes).
|
||||
|
||||
```bash
|
||||
dart --observe bin/dev.dart
|
||||
```
|
||||
|
||||
4. Run the project in production mode (*hot-reloaded* is disabled).
|
||||
|
||||
```bash
|
||||
dart bin/prod.dart
|
||||
```
|
||||
|
||||
5. Run as docker. Edit and build the image with the provided `Dockerfile` file.
|
||||
|
||||
### (Option 2) Create a new project with Angel3 CLI
|
||||
|
||||
1. Download and install [Dart](https://dart.dev/get-dart)
|
||||
|
||||
2. Install the [Angel3 CLI](https://pub.dev/packages/angel3_cli):
|
||||
|
||||
```bash
|
||||
dart pub global activate angel3_cli
|
||||
```
|
||||
|
||||
3. On terminal, create a new project:
|
||||
|
||||
```bash
|
||||
angel3 init hello
|
||||
```
|
||||
|
||||
4. Run the project in development mode (*hot-reloaded* is enabled on file changes).
|
||||
|
||||
```bash
|
||||
dart --observe bin/dev.dart
|
||||
```
|
||||
|
||||
5. Run the project in production mode (*hot-reloaded* is disabled).
|
||||
|
||||
```bash
|
||||
dart bin/prod.dart
|
||||
```
|
||||
|
||||
6. Run as docker. Edit and build the image with the provided `Dockerfile` file.
|
||||
|
||||
## Performance Benchmark
|
||||
|
||||
The performance benchmark can be found at
|
||||
|
||||
[TechEmpower Framework Benchmarks Round 21](https://www.techempower.com/benchmarks/#section=data-r21&test=composite)
|
||||
|
||||
### Migrating from Angel to Angel3
|
||||
|
||||
Check out [Migrating to Angel3](https://angel3-docs.dukefirehawk.com/migration/angel-2.x.x-to-angel3/migration-guide-3)
|
||||
|
||||
## Donation & Support
|
||||
|
||||
If you like this project and interested in supporting its development, you can make a donation using the following services:
|
||||
|
||||
* [![GitHub](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/dukefirehawk)
|
||||
* [paypal](https://paypal.me/dukefirehawk?country.x=MY&locale.x=en_US) service
|
1
packages/framework/analysis_options.yaml
Normal file
1
packages/framework/analysis_options.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
include: package:lints/recommended.yaml
|
29
packages/framework/dev.key
Normal file
29
packages/framework/dev.key
Normal file
|
@ -0,0 +1,29 @@
|
|||
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||
MIIE5DAcBgoqhkiG9w0BDAEBMA4ECL7L6rj6uEHGAgIIAASCBMLbucyfqAkgCbhP
|
||||
xNSHYllPMAv/dsIjtnsBwepCXPGkCBCuOAw/2FaCHjN9hBqL5V7fkrKeaemhm2YE
|
||||
ycPtlHJYPDf3kEkyMjdZ9rIY6kePGfQizs2uJPcXj4YPyQ4HsfVXpOicKfQrouf5
|
||||
Mze9bGzeMN065q3iP4dYUMwHAyZYteXCsanQNHlqvsWli0W+H8St8fdsXefZhnv1
|
||||
qVatKWdNdWQ9t5MuljgNU2Vv56sHKEYXI0yLxk2QUMk8KlJfnmt8foYUsnPUXHmc
|
||||
gIjLKwwVkpdololnEHSNu0cEOUPowjgJru+uMpn7vdNl7TPEQ9jbEgdNg4JwoYzU
|
||||
0nao8WzjaSp7kzvZz0VFwKnk5AjstGvvuAWckADdq23QElbn/mF7AG1m/TBpYxzF
|
||||
gTt37UdndS/AcvVznWVVrRP5iTSIawdIwvqI4s7rqsoE0GCcak+RhchgAz2gWKkS
|
||||
oODUo0JL6pPVbJ3l4ebbaO6c99nDVc8dViPtc1EkStJEJ2O4kI4xgLSCr4Y9ahKn
|
||||
oAaoSkX7Xxq3aQm+BzqSpLjdGL8atsqR/YVOIHYIl3gThvP0NfZGx1xHyvO5mCdZ
|
||||
kHxSA7tKWxauZ3eQ2clbnzeRsl4El0WMHy/5K1ovene4v7sunmoXVtghBC8hK6eh
|
||||
zMO9orex2PNQ/VQC7HCvtytunOVx1lkSBoNo7hR70igg6rW9H7UyoAoBOwMpT1xa
|
||||
J6V62nqruTKOqFNfur7aHJGpHGtDb5/ickHeYCyPTvmGp67u4wChzKReeg02oECe
|
||||
d1E5FKAcIa8s9TVOB6Z+HvTRNQZu2PsI6TJnjQRowvY9DAHiWTlJZBBY/pko3hxX
|
||||
TsIeybpvRdEHpDWv86/iqtw1hv9CUxS/8ZTWUgBo+osShHW79FeDASr9FC4/Zn76
|
||||
ZDERTgV4YWlW/klVWcG2lFo7jix+OPXAB+ZQavLhlN1xdWBcIz1AUWjAM4hdPylW
|
||||
HCX4PB9CQIPl2E7F+Y2p6nMcMWSJVBi5UIH7E9LfaBguXSzMmTk2Fw5p1aOQ6wfN
|
||||
goVAMVwi8ppAVs741PfHdZ295xMmK/1LCxz5DeAdD/tsA/SYfT753GotioDuC7im
|
||||
EyJ5JyvTr5I6RFFBuqt3NlUb3Hp16wP3B2x9DZiB6jxr0l341/NHgsyeBXkuIy9j
|
||||
ON2mvpBPCJhS8kgWo3G0UyyKnx64tcgpGuSvZhGwPz843B6AbYyE6pMRfSWRMkMS
|
||||
YZYa+VNKhR4ixdj07ocFZEWLVjCH7kxkE8JZXKt8jKYmkWd0lS1QVjgaKlO6lRa3
|
||||
q6SPJkhW6pvqobvcqVNXwi1XuzpZeEbuh0B7OTekFTTxx5g9XeDl56M8SVQ1KEhT
|
||||
Q1t7H2Nba18WCB7cf+6PN0F0K0Jz1Kq7ZWaqEI/grX1m4RQuvNF5807sB/QKMO/Z
|
||||
Gz3NXvHg5xTJRd/567lxPGkor0cE7qD1EZfmJ2HrBYXQ91bhgA7LToBuMZo6ZRXH
|
||||
QfsanjbP4FPLMiGdQigLjj3A35L/f4sQOOVac/sRaFnm7pzcxsMvyVU/YtvGcjYE
|
||||
xaOOVnamg661Wo0wksXoDjeSz/JIyyKO3Gwp1FSm2wGLjjy/Ehmqcqy8rvHuf07w
|
||||
AUukhVtTNn4=
|
||||
-----END ENCRYPTED PRIVATE KEY-----
|
57
packages/framework/dev.pem
Normal file
57
packages/framework/dev.pem
Normal file
|
@ -0,0 +1,57 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDKTCCAhGgAwIBAgIJAOWmjTS+OnTEMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNV
|
||||
BAMMDGludGVybWVkaWF0ZTAeFw0xNTA1MTgwOTAwNDBaFw0yMzA4MDQwOTAwNDBa
|
||||
MBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
|
||||
AQoCggEBALlcwQJuzd+xH8QFgfJSn5tRlvhkldSX98cE7NiA602NBbnAVyUrkRXq
|
||||
Ni75lgt0kwjYfA9z674m8WSVbgpLPintPCla9CYky1TH0keIs8Rz6cGWHryWEHiu
|
||||
EDuljQynu2b3sAFuHu9nfWurbJwZnFakBKpdQ9m4EyOZCHC/jHYY7HacKSXg1Cki
|
||||
we2ca0BWDrcqy8kLy0dZ5oC6IZG8O8drAK8f3f44CRYw59D3sOKBrKXaabpvyEcb
|
||||
N7Wk2HDBVwHpUJo1reVwtbM8dhqQayYSD8oXnGpP3RQNu/e2rzlXRyq/BfcDY1JI
|
||||
7TbC4t/7/N4EcPSpGsTcSOC9A7FpzvECAwEAAaN7MHkwCQYDVR0TBAIwADAsBglg
|
||||
hkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0O
|
||||
BBYEFCnwiEMMFZh7NhCr+qA8K0w4Q+AOMB8GA1UdIwQYMBaAFB0h1Evsaw2vfrmS
|
||||
YuoCTmC4EE6ZMA0GCSqGSIb3DQEBCwUAA4IBAQAcFmHMaXRxyoNaeOowQ6iQWoZd
|
||||
AUbvG7SHr7I6Pi2aqdqofsKWts7Ytm5WsS0M2nN+sW504houu0iCPeJJX8RQw2q4
|
||||
CCcNOs9IXk+2uMzlpocHpv+yYoUiD5DxgWh7eghQMLyMpf8FX3Gy4VazeuXznHOM
|
||||
4gE4L417xkDzYOzqVTp0FTyAPUv6G2euhNCD6TMru9REcRhYul+K9kocjA5tt2KG
|
||||
MH6y28LXbLyq4YJUxSUU9gY/xlnbbZS48KDqEcdYC9zjW9nQ0qS+XQuQuFIcwjJ5
|
||||
V4kAUYxDu6FoTpyQjgsrmBbZlKNxH7Nj4NDlcdJhp/zeSKHqWa5hSWjjKIxp
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDAjCCAeqgAwIBAgIJAOWmjTS+OnTDMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV
|
||||
BAMMDXJvb3RhdXRob3JpdHkwHhcNMTUwNTE4MDkwMDQwWhcNMjMwODA0MDkwMDQw
|
||||
WjAXMRUwEwYDVQQDDAxpbnRlcm1lZGlhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IB
|
||||
DwAwggEKAoIBAQDSrAO1CoPvUllgLOzDm5nG0skDF7vh1DUgAIDVGz0ecD0JFbQx
|
||||
EF79pju/6MbtpTW2FYvRp11t/G7rGtX923ybOHY/1MNFQrdIvPlO1VV7IGKjoMwP
|
||||
DNeb0fIGjHoE9QxaDxR8NX8xQbItpsw+TUtRfc9SLkR+jaYJfVRoM21BOncZbSHE
|
||||
YKiZlEbpecB/+EtwVpgvl+8mPD5U07Fi4fp/lza3WXInXQPyiTVllIEJCt4PKmlu
|
||||
MocNaJOW38bysL7i0PzDpVZtOxLHOTaW68yF3FckIHNCaA7k1ABEEEegjFMmIao7
|
||||
B9w7A0jvr4jZVvNmui5Djjn+oJxwEVVgyf8LAgMBAAGjUDBOMB0GA1UdDgQWBBQd
|
||||
IdRL7GsNr365kmLqAk5guBBOmTAfBgNVHSMEGDAWgBRk81s9d0ZbiZhh44KckwPb
|
||||
oTc0XzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBZQTK0plfdB5PC
|
||||
cC5icut4EmrByJa1RbU7ayuEE70e7hla6KVmVjVdCBGltI4jBYwfhKbRItHiAJ/8
|
||||
x+XZKBG8DLPFuDb7lAa1ObhAYF7YThUFPQYaBhfzKcWrdmWDBFpvNv6E0Mm364dZ
|
||||
e7Yxmbe5S4agkYPoxEzgEYmcUk9jbjdR6eTbs8laG169ljrECXfEU9RiAcqz5iSX
|
||||
NLSewqB47hn3B9qgKcQn+PsgO2j7M+rfklhNgeGJeWmy7j6clSOuCsIjWHU0RLQ4
|
||||
0W3SB/rpEAJ7fgQbYUPTIUNALSOWi/o1tDX2mXPRjBoxqAv7I+vYk1lZPmSzkyRh
|
||||
FKvRDxsW
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDAzCCAeugAwIBAgIJAJ0MomS4Ck+8MA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV
|
||||
BAMMDXJvb3RhdXRob3JpdHkwHhcNMTUwNTE4MDkwMDQwWhcNMjMwODA0MDkwMDQw
|
||||
WjAYMRYwFAYDVQQDDA1yb290YXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOC
|
||||
AQ8AMIIBCgKCAQEAts1ijtBV92S2cOvpUMOSTp9c6A34nIGr0T5Nhz6XiqRVT+gv
|
||||
dQgmkdKJQjbvR60y6jzltYFsI2MpGVXY8h/oAL81D/k7PDB2aREgyBfTPAhBHyGw
|
||||
siR+2xYt5b/Zs99q5RdRqQNzNpLPJriIKvUsRyQWy1UiG2s7pRXQeA8qB0XtJdCj
|
||||
kFIi+G2bDsaffspGeDOCqt7t+yqvRXfSES0c/l7DIHaiMbbp4//ZNML3RNgAjPz2
|
||||
hCezZ+wOYajOIyoSPK8IgICrhYFYxvgWxwbLDBEfC5B3jOQsySe10GoRAKZz1gBV
|
||||
DmgReu81tYJmdgkc9zknnQtIFdA0ex+GvZlfWQIDAQABo1AwTjAdBgNVHQ4EFgQU
|
||||
ZPNbPXdGW4mYYeOCnJMD26E3NF8wHwYDVR0jBBgwFoAUZPNbPXdGW4mYYeOCnJMD
|
||||
26E3NF8wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEATzkZ97K777uZ
|
||||
lQcduNX3ey4IbCiEzFA2zO5Blj+ilfIwNbZXNOgm/lqNvVGDYs6J1apJJe30vL3X
|
||||
J+t2zsZWzzQzb9uIU37zYemt6m0fHrSrx/iy5lGNqt3HMfqEcOqSCOIK3PCTMz2/
|
||||
uyGe1iw33PVeWsm1JUybQ9IrU/huJjbgOHU4wab+8SJCM49ipArp68Fr6j4lcEaE
|
||||
4rfRg1ZsvxiOyUB3qPn6wyL/JB8kOJ+QCBe498376eaem8AEFk0kQRh6hDaWtq/k
|
||||
t6IIXQLjx+EBDVP/veK0UnVhKRP8YTOoV8ZiG1NcdlJmX/Uk7iAfevP7CkBfSN8W
|
||||
r6AL284qtw==
|
||||
-----END CERTIFICATE-----
|
59
packages/framework/example/controller.dart
Normal file
59
packages/framework/example/controller.dart
Normal file
|
@ -0,0 +1,59 @@
|
|||
import 'package:platform_container/mirrors.dart';
|
||||
import 'package:platform_framework/platform_framework.dart';
|
||||
import 'package:platform_framework/http.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
void main() async {
|
||||
// Logging set up/boilerplate
|
||||
Logger.root.onRecord.listen(print);
|
||||
|
||||
// Create our server.
|
||||
var app = Angel(logger: Logger('angel'), reflector: MirrorsReflector());
|
||||
var http = AngelHttp(app);
|
||||
|
||||
await app.mountController<ArtistsController>();
|
||||
|
||||
// Simple fallback to throw a 404 on unknown paths.
|
||||
app.fallback((req, res) {
|
||||
throw HttpException.notFound(
|
||||
message: 'Unknown path: "${req.uri!.path}"',
|
||||
);
|
||||
});
|
||||
|
||||
app.errorHandler = (e, req, res) => e.toJson();
|
||||
|
||||
await http.startServer('127.0.0.1', 3000);
|
||||
print('Listening at ${http.uri}');
|
||||
app.dumpTree();
|
||||
}
|
||||
|
||||
class ArtistsController extends Controller {
|
||||
List index() {
|
||||
return ['Elvis', 'Stevie', 'Van Gogh'];
|
||||
}
|
||||
|
||||
String getById(int id, RequestContext req) {
|
||||
return 'You fetched ID: $id from IP: ${req.ip}';
|
||||
}
|
||||
|
||||
@Expose.post
|
||||
Future<Artist> form(RequestContext req) async {
|
||||
// Deserialize the body into an artist.
|
||||
var artist = await req.deserializeBody((m) {
|
||||
return Artist(name: m!['name'] as String? ?? '(unknown name)');
|
||||
});
|
||||
|
||||
// Return it (it will be serialized to JSON).
|
||||
return artist;
|
||||
}
|
||||
}
|
||||
|
||||
class Artist {
|
||||
final String? name;
|
||||
|
||||
Artist({this.name});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'name': name};
|
||||
}
|
||||
}
|
25
packages/framework/example/handle_error.dart
Normal file
25
packages/framework/example/handle_error.dart
Normal file
|
@ -0,0 +1,25 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:platform_container/mirrors.dart';
|
||||
import 'package:platform_framework/platform_framework.dart';
|
||||
import 'package:platform_framework/http.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
void main() async {
|
||||
var app = Angel(reflector: MirrorsReflector())
|
||||
..logger = (Logger('angel')
|
||||
..onRecord.listen((rec) {
|
||||
print(rec);
|
||||
if (rec.error != null) print(rec.error);
|
||||
if (rec.stackTrace != null) print(rec.stackTrace);
|
||||
}))
|
||||
..encoders.addAll({'gzip': gzip.encoder});
|
||||
|
||||
app.fallback(
|
||||
(req, res) => Future.error('Throwing just because I feel like!'));
|
||||
|
||||
var http = AngelHttp(app);
|
||||
HttpServer? server = await http.startServer('127.0.0.1', 3000);
|
||||
var url = 'http://${server.address.address}:${server.port}';
|
||||
print('Listening at $url');
|
||||
}
|
46
packages/framework/example/hostname.dart
Normal file
46
packages/framework/example/hostname.dart
Normal file
|
@ -0,0 +1,46 @@
|
|||
import 'dart:async';
|
||||
import 'package:platform_framework/platform_framework.dart';
|
||||
import 'package:platform_framework/http.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
Future<void> apiConfigurer(Angel app) async {
|
||||
app.get('/', (req, res) => 'Hello, API!');
|
||||
app.fallback((req, res) {
|
||||
return 'fallback on ${req.uri} (within the API)';
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> frontendConfigurer(Angel app) async {
|
||||
app.fallback((req, res) => '(usually an index page would be shown here.)');
|
||||
}
|
||||
|
||||
void main() async {
|
||||
// Logging set up/boilerplate
|
||||
hierarchicalLoggingEnabled = true;
|
||||
//Logger.root.onRecord.listen(prettyLog);
|
||||
|
||||
var app = Angel(logger: Logger('angel'));
|
||||
var http = AngelHttp(app);
|
||||
var multiHost = HostnameRouter.configure({
|
||||
'api.localhost:3000': apiConfigurer,
|
||||
'localhost:3000': frontendConfigurer,
|
||||
});
|
||||
|
||||
app
|
||||
..fallback(multiHost.handleRequest)
|
||||
..fallback((req, res) {
|
||||
res.write('Uncaught hostname: ${req.hostname}');
|
||||
});
|
||||
|
||||
app.errorHandler = (e, req, res) {
|
||||
print(e.message);
|
||||
print(e.stackTrace);
|
||||
return e.toJson();
|
||||
};
|
||||
|
||||
await http.startServer('127.0.0.1', 3000);
|
||||
print('Listening at ${http.uri}');
|
||||
print('See what happens when you visit http://localhost:3000 instead '
|
||||
'of http://127.0.0.1:3000. Then, try '
|
||||
'http://api.localhost:3000.');
|
||||
}
|
46
packages/framework/example/http2/body_parsing.dart
Normal file
46
packages/framework/example/http2/body_parsing.dart
Normal file
|
@ -0,0 +1,46 @@
|
|||
import 'dart:io';
|
||||
import 'package:platform_framework/platform_framework.dart';
|
||||
import 'package:platform_framework/http.dart';
|
||||
import 'package:platform_framework/http2.dart';
|
||||
import 'package:file/local.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
void main() async {
|
||||
var app = Angel();
|
||||
app.logger = Logger('angel')
|
||||
..onRecord.listen((rec) {
|
||||
print(rec);
|
||||
if (rec.error != null) print(rec.error);
|
||||
if (rec.stackTrace != null) print(rec.stackTrace);
|
||||
});
|
||||
|
||||
var publicDir = Directory('example/public');
|
||||
var indexHtml =
|
||||
const LocalFileSystem().file(publicDir.uri.resolve('body_parsing.html'));
|
||||
|
||||
app.get('/', (req, res) => res.streamFile(indexHtml));
|
||||
|
||||
app.post('/', (req, res) => req.parseBody().then((_) => req.bodyAsMap));
|
||||
|
||||
var ctx = SecurityContext()
|
||||
..useCertificateChain('dev.pem')
|
||||
..usePrivateKey('dev.key', password: 'dartdart');
|
||||
|
||||
try {
|
||||
ctx.setAlpnProtocols(['h2'], true);
|
||||
} catch (e, st) {
|
||||
app.logger.severe(
|
||||
'Cannot set ALPN protocol on server to `h2`. The server will only serve HTTP/1.x.',
|
||||
e,
|
||||
st);
|
||||
}
|
||||
|
||||
var http1 = AngelHttp(app);
|
||||
var http2 = AngelHttp2(app, ctx);
|
||||
|
||||
// HTTP/1.x requests will fallback to `AngelHttp`
|
||||
http2.onHttp1.listen(http1.handleRequest);
|
||||
|
||||
var server = await http2.startServer('127.0.0.1', 3000);
|
||||
print('Listening at https://${server.address.address}:${server.port}');
|
||||
}
|
7
packages/framework/example/http2/common.dart
Normal file
7
packages/framework/example/http2/common.dart
Normal file
|
@ -0,0 +1,7 @@
|
|||
import 'package:logging/logging.dart';
|
||||
|
||||
void dumpError(LogRecord rec) {
|
||||
print(rec);
|
||||
if (rec.error != null) print(rec.error);
|
||||
if (rec.stackTrace != null) print(rec.stackTrace);
|
||||
}
|
29
packages/framework/example/http2/dev.key
Normal file
29
packages/framework/example/http2/dev.key
Normal file
|
@ -0,0 +1,29 @@
|
|||
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||
MIIE5DAcBgoqhkiG9w0BDAEBMA4ECL7L6rj6uEHGAgIIAASCBMLbucyfqAkgCbhP
|
||||
xNSHYllPMAv/dsIjtnsBwepCXPGkCBCuOAw/2FaCHjN9hBqL5V7fkrKeaemhm2YE
|
||||
ycPtlHJYPDf3kEkyMjdZ9rIY6kePGfQizs2uJPcXj4YPyQ4HsfVXpOicKfQrouf5
|
||||
Mze9bGzeMN065q3iP4dYUMwHAyZYteXCsanQNHlqvsWli0W+H8St8fdsXefZhnv1
|
||||
qVatKWdNdWQ9t5MuljgNU2Vv56sHKEYXI0yLxk2QUMk8KlJfnmt8foYUsnPUXHmc
|
||||
gIjLKwwVkpdololnEHSNu0cEOUPowjgJru+uMpn7vdNl7TPEQ9jbEgdNg4JwoYzU
|
||||
0nao8WzjaSp7kzvZz0VFwKnk5AjstGvvuAWckADdq23QElbn/mF7AG1m/TBpYxzF
|
||||
gTt37UdndS/AcvVznWVVrRP5iTSIawdIwvqI4s7rqsoE0GCcak+RhchgAz2gWKkS
|
||||
oODUo0JL6pPVbJ3l4ebbaO6c99nDVc8dViPtc1EkStJEJ2O4kI4xgLSCr4Y9ahKn
|
||||
oAaoSkX7Xxq3aQm+BzqSpLjdGL8atsqR/YVOIHYIl3gThvP0NfZGx1xHyvO5mCdZ
|
||||
kHxSA7tKWxauZ3eQ2clbnzeRsl4El0WMHy/5K1ovene4v7sunmoXVtghBC8hK6eh
|
||||
zMO9orex2PNQ/VQC7HCvtytunOVx1lkSBoNo7hR70igg6rW9H7UyoAoBOwMpT1xa
|
||||
J6V62nqruTKOqFNfur7aHJGpHGtDb5/ickHeYCyPTvmGp67u4wChzKReeg02oECe
|
||||
d1E5FKAcIa8s9TVOB6Z+HvTRNQZu2PsI6TJnjQRowvY9DAHiWTlJZBBY/pko3hxX
|
||||
TsIeybpvRdEHpDWv86/iqtw1hv9CUxS/8ZTWUgBo+osShHW79FeDASr9FC4/Zn76
|
||||
ZDERTgV4YWlW/klVWcG2lFo7jix+OPXAB+ZQavLhlN1xdWBcIz1AUWjAM4hdPylW
|
||||
HCX4PB9CQIPl2E7F+Y2p6nMcMWSJVBi5UIH7E9LfaBguXSzMmTk2Fw5p1aOQ6wfN
|
||||
goVAMVwi8ppAVs741PfHdZ295xMmK/1LCxz5DeAdD/tsA/SYfT753GotioDuC7im
|
||||
EyJ5JyvTr5I6RFFBuqt3NlUb3Hp16wP3B2x9DZiB6jxr0l341/NHgsyeBXkuIy9j
|
||||
ON2mvpBPCJhS8kgWo3G0UyyKnx64tcgpGuSvZhGwPz843B6AbYyE6pMRfSWRMkMS
|
||||
YZYa+VNKhR4ixdj07ocFZEWLVjCH7kxkE8JZXKt8jKYmkWd0lS1QVjgaKlO6lRa3
|
||||
q6SPJkhW6pvqobvcqVNXwi1XuzpZeEbuh0B7OTekFTTxx5g9XeDl56M8SVQ1KEhT
|
||||
Q1t7H2Nba18WCB7cf+6PN0F0K0Jz1Kq7ZWaqEI/grX1m4RQuvNF5807sB/QKMO/Z
|
||||
Gz3NXvHg5xTJRd/567lxPGkor0cE7qD1EZfmJ2HrBYXQ91bhgA7LToBuMZo6ZRXH
|
||||
QfsanjbP4FPLMiGdQigLjj3A35L/f4sQOOVac/sRaFnm7pzcxsMvyVU/YtvGcjYE
|
||||
xaOOVnamg661Wo0wksXoDjeSz/JIyyKO3Gwp1FSm2wGLjjy/Ehmqcqy8rvHuf07w
|
||||
AUukhVtTNn4=
|
||||
-----END ENCRYPTED PRIVATE KEY-----
|
57
packages/framework/example/http2/dev.pem
Normal file
57
packages/framework/example/http2/dev.pem
Normal file
|
@ -0,0 +1,57 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDKTCCAhGgAwIBAgIJAOWmjTS+OnTEMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNV
|
||||
BAMMDGludGVybWVkaWF0ZTAeFw0xNTA1MTgwOTAwNDBaFw0yMzA4MDQwOTAwNDBa
|
||||
MBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
|
||||
AQoCggEBALlcwQJuzd+xH8QFgfJSn5tRlvhkldSX98cE7NiA602NBbnAVyUrkRXq
|
||||
Ni75lgt0kwjYfA9z674m8WSVbgpLPintPCla9CYky1TH0keIs8Rz6cGWHryWEHiu
|
||||
EDuljQynu2b3sAFuHu9nfWurbJwZnFakBKpdQ9m4EyOZCHC/jHYY7HacKSXg1Cki
|
||||
we2ca0BWDrcqy8kLy0dZ5oC6IZG8O8drAK8f3f44CRYw59D3sOKBrKXaabpvyEcb
|
||||
N7Wk2HDBVwHpUJo1reVwtbM8dhqQayYSD8oXnGpP3RQNu/e2rzlXRyq/BfcDY1JI
|
||||
7TbC4t/7/N4EcPSpGsTcSOC9A7FpzvECAwEAAaN7MHkwCQYDVR0TBAIwADAsBglg
|
||||
hkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0O
|
||||
BBYEFCnwiEMMFZh7NhCr+qA8K0w4Q+AOMB8GA1UdIwQYMBaAFB0h1Evsaw2vfrmS
|
||||
YuoCTmC4EE6ZMA0GCSqGSIb3DQEBCwUAA4IBAQAcFmHMaXRxyoNaeOowQ6iQWoZd
|
||||
AUbvG7SHr7I6Pi2aqdqofsKWts7Ytm5WsS0M2nN+sW504houu0iCPeJJX8RQw2q4
|
||||
CCcNOs9IXk+2uMzlpocHpv+yYoUiD5DxgWh7eghQMLyMpf8FX3Gy4VazeuXznHOM
|
||||
4gE4L417xkDzYOzqVTp0FTyAPUv6G2euhNCD6TMru9REcRhYul+K9kocjA5tt2KG
|
||||
MH6y28LXbLyq4YJUxSUU9gY/xlnbbZS48KDqEcdYC9zjW9nQ0qS+XQuQuFIcwjJ5
|
||||
V4kAUYxDu6FoTpyQjgsrmBbZlKNxH7Nj4NDlcdJhp/zeSKHqWa5hSWjjKIxp
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDAjCCAeqgAwIBAgIJAOWmjTS+OnTDMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV
|
||||
BAMMDXJvb3RhdXRob3JpdHkwHhcNMTUwNTE4MDkwMDQwWhcNMjMwODA0MDkwMDQw
|
||||
WjAXMRUwEwYDVQQDDAxpbnRlcm1lZGlhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IB
|
||||
DwAwggEKAoIBAQDSrAO1CoPvUllgLOzDm5nG0skDF7vh1DUgAIDVGz0ecD0JFbQx
|
||||
EF79pju/6MbtpTW2FYvRp11t/G7rGtX923ybOHY/1MNFQrdIvPlO1VV7IGKjoMwP
|
||||
DNeb0fIGjHoE9QxaDxR8NX8xQbItpsw+TUtRfc9SLkR+jaYJfVRoM21BOncZbSHE
|
||||
YKiZlEbpecB/+EtwVpgvl+8mPD5U07Fi4fp/lza3WXInXQPyiTVllIEJCt4PKmlu
|
||||
MocNaJOW38bysL7i0PzDpVZtOxLHOTaW68yF3FckIHNCaA7k1ABEEEegjFMmIao7
|
||||
B9w7A0jvr4jZVvNmui5Djjn+oJxwEVVgyf8LAgMBAAGjUDBOMB0GA1UdDgQWBBQd
|
||||
IdRL7GsNr365kmLqAk5guBBOmTAfBgNVHSMEGDAWgBRk81s9d0ZbiZhh44KckwPb
|
||||
oTc0XzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBZQTK0plfdB5PC
|
||||
cC5icut4EmrByJa1RbU7ayuEE70e7hla6KVmVjVdCBGltI4jBYwfhKbRItHiAJ/8
|
||||
x+XZKBG8DLPFuDb7lAa1ObhAYF7YThUFPQYaBhfzKcWrdmWDBFpvNv6E0Mm364dZ
|
||||
e7Yxmbe5S4agkYPoxEzgEYmcUk9jbjdR6eTbs8laG169ljrECXfEU9RiAcqz5iSX
|
||||
NLSewqB47hn3B9qgKcQn+PsgO2j7M+rfklhNgeGJeWmy7j6clSOuCsIjWHU0RLQ4
|
||||
0W3SB/rpEAJ7fgQbYUPTIUNALSOWi/o1tDX2mXPRjBoxqAv7I+vYk1lZPmSzkyRh
|
||||
FKvRDxsW
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDAzCCAeugAwIBAgIJAJ0MomS4Ck+8MA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV
|
||||
BAMMDXJvb3RhdXRob3JpdHkwHhcNMTUwNTE4MDkwMDQwWhcNMjMwODA0MDkwMDQw
|
||||
WjAYMRYwFAYDVQQDDA1yb290YXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOC
|
||||
AQ8AMIIBCgKCAQEAts1ijtBV92S2cOvpUMOSTp9c6A34nIGr0T5Nhz6XiqRVT+gv
|
||||
dQgmkdKJQjbvR60y6jzltYFsI2MpGVXY8h/oAL81D/k7PDB2aREgyBfTPAhBHyGw
|
||||
siR+2xYt5b/Zs99q5RdRqQNzNpLPJriIKvUsRyQWy1UiG2s7pRXQeA8qB0XtJdCj
|
||||
kFIi+G2bDsaffspGeDOCqt7t+yqvRXfSES0c/l7DIHaiMbbp4//ZNML3RNgAjPz2
|
||||
hCezZ+wOYajOIyoSPK8IgICrhYFYxvgWxwbLDBEfC5B3jOQsySe10GoRAKZz1gBV
|
||||
DmgReu81tYJmdgkc9zknnQtIFdA0ex+GvZlfWQIDAQABo1AwTjAdBgNVHQ4EFgQU
|
||||
ZPNbPXdGW4mYYeOCnJMD26E3NF8wHwYDVR0jBBgwFoAUZPNbPXdGW4mYYeOCnJMD
|
||||
26E3NF8wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEATzkZ97K777uZ
|
||||
lQcduNX3ey4IbCiEzFA2zO5Blj+ilfIwNbZXNOgm/lqNvVGDYs6J1apJJe30vL3X
|
||||
J+t2zsZWzzQzb9uIU37zYemt6m0fHrSrx/iy5lGNqt3HMfqEcOqSCOIK3PCTMz2/
|
||||
uyGe1iw33PVeWsm1JUybQ9IrU/huJjbgOHU4wab+8SJCM49ipArp68Fr6j4lcEaE
|
||||
4rfRg1ZsvxiOyUB3qPn6wyL/JB8kOJ+QCBe498376eaem8AEFk0kQRh6hDaWtq/k
|
||||
t6IIXQLjx+EBDVP/veK0UnVhKRP8YTOoV8ZiG1NcdlJmX/Uk7iAfevP7CkBfSN8W
|
||||
r6AL284qtw==
|
||||
-----END CERTIFICATE-----
|
43
packages/framework/example/http2/main.dart
Normal file
43
packages/framework/example/http2/main.dart
Normal file
|
@ -0,0 +1,43 @@
|
|||
import 'dart:io';
|
||||
import 'package:platform_framework/platform_framework.dart';
|
||||
import 'package:platform_framework/http.dart';
|
||||
import 'package:platform_framework/http2.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'common.dart';
|
||||
|
||||
void main() async {
|
||||
var app = Angel()
|
||||
..encoders.addAll({
|
||||
'gzip': gzip.encoder,
|
||||
'deflate': zlib.encoder,
|
||||
});
|
||||
app.logger = Logger('angel')..onRecord.listen(dumpError);
|
||||
|
||||
app.get('/', (req, res) => 'Hello HTTP/2!!!');
|
||||
|
||||
app.fallback((req, res) =>
|
||||
throw HttpException.notFound(message: 'No file exists at ${req.uri}'));
|
||||
|
||||
var ctx = SecurityContext()
|
||||
..useCertificateChain('dev.pem')
|
||||
..usePrivateKey('dev.key', password: 'dartdart');
|
||||
|
||||
try {
|
||||
ctx.setAlpnProtocols(['h2'], true);
|
||||
} catch (e, st) {
|
||||
app.logger.severe(
|
||||
'Cannot set ALPN protocol on server to `h2`. The server will only serve HTTP/1.x.',
|
||||
e,
|
||||
st,
|
||||
);
|
||||
}
|
||||
|
||||
var http1 = AngelHttp(app);
|
||||
var http2 = AngelHttp2(app, ctx);
|
||||
|
||||
// HTTP/1.x requests will fallback to `AngelHttp`
|
||||
http2.onHttp1.listen(http1.handleRequest);
|
||||
|
||||
await http2.startServer('127.0.0.1', 3000);
|
||||
print('Listening at ${http2.uri}');
|
||||
}
|
27
packages/framework/example/http2/public/app.js
Normal file
27
packages/framework/example/http2/public/app.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
window.onload = function() {
|
||||
var $app = document.getElementById('app');
|
||||
var $loading = document.getElementById('loading');
|
||||
$app.removeChild($loading);
|
||||
var $button = document.createElement('button');
|
||||
var $h1 = document.createElement('h1');
|
||||
$app.appendChild($h1);
|
||||
$app.appendChild($button);
|
||||
|
||||
$h1.textContent = '~Angel HTTP/2 server push~';
|
||||
|
||||
$button.textContent = 'Change color';
|
||||
$button.onclick = function() {
|
||||
var color = Math.floor(Math.random() * 0xffffff);
|
||||
$h1.style.color = '#' + color.toString(16);
|
||||
};
|
||||
|
||||
$button.onclick();
|
||||
|
||||
window.setInterval($button.onclick, 2000);
|
||||
|
||||
var rotation = 0;
|
||||
window.setInterval(function() {
|
||||
rotation += .6;
|
||||
$button.style.transform = 'rotate(' + rotation + 'deg)';
|
||||
}, 10);
|
||||
};
|
21
packages/framework/example/http2/public/body_parsing.html
Normal file
21
packages/framework/example/http2/public/body_parsing.html
Normal file
|
@ -0,0 +1,21 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Angel HTTP/2</title>
|
||||
<style>
|
||||
input:not([type="submit"]) {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<form action="/" method="post">
|
||||
<input name="name" placeholder="Your Name" type="text">
|
||||
<input name="password" placeholder="Secret Field" type="password">
|
||||
<input name="age" placeholder="Your Age" type="number">
|
||||
<input name="birthday" placeholder="Your Birthday" type="datetime-local">
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
12
packages/framework/example/http2/public/index.html
Normal file
12
packages/framework/example/http2/public/index.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Angel HTTP/2</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"><span id="loading">Loading...</span></div>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
20
packages/framework/example/http2/public/style.css
Normal file
20
packages/framework/example/http2/public/style.css
Normal file
|
@ -0,0 +1,20 @@
|
|||
button {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
html, body {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
#app {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#app h1 {
|
||||
font-style: italic;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#loading {
|
||||
color: red;
|
||||
}
|
62
packages/framework/example/http2/server_push.dart
Normal file
62
packages/framework/example/http2/server_push.dart
Normal file
|
@ -0,0 +1,62 @@
|
|||
import 'dart:io';
|
||||
import 'package:platform_framework/platform_framework.dart';
|
||||
import 'package:platform_framework/http.dart';
|
||||
import 'package:platform_framework/http2.dart';
|
||||
import 'package:file/local.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
void main() async {
|
||||
var app = Angel();
|
||||
app.logger = Logger('angel')
|
||||
..onRecord.listen((rec) {
|
||||
print(rec);
|
||||
if (rec.error != null) print(rec.error);
|
||||
if (rec.stackTrace != null) print(rec.stackTrace);
|
||||
});
|
||||
|
||||
var publicDir = Directory('example/http2/public');
|
||||
var indexHtml =
|
||||
const LocalFileSystem().file(publicDir.uri.resolve('index.html'));
|
||||
var styleCss =
|
||||
const LocalFileSystem().file(publicDir.uri.resolve('style.css'));
|
||||
var appJs = const LocalFileSystem().file(publicDir.uri.resolve('app.js'));
|
||||
|
||||
// Send files when requested
|
||||
app
|
||||
..get('/style.css', (req, res) => res.streamFile(styleCss))
|
||||
..get('/app.js', (req, res) => res.streamFile(appJs));
|
||||
|
||||
app.get('/', (req, res) async {
|
||||
// Regardless of whether we pushed other resources, let's still send /index.html.
|
||||
await res.streamFile(indexHtml);
|
||||
|
||||
// If the client is HTTP/2 and supports server push, let's
|
||||
// send down /style.css and /app.js as well, to improve initial load time.
|
||||
if (res is Http2ResponseContext && res.canPush) {
|
||||
await res.push('/style.css').streamFile(styleCss);
|
||||
await res.push('/app.js').streamFile(appJs);
|
||||
}
|
||||
});
|
||||
|
||||
var ctx = SecurityContext()
|
||||
..useCertificateChain('dev.pem')
|
||||
..usePrivateKey('dev.key', password: 'dartdart');
|
||||
|
||||
try {
|
||||
ctx.setAlpnProtocols(['h2'], true);
|
||||
} catch (e, st) {
|
||||
app.logger.severe(
|
||||
'Cannot set ALPN protocol on server to `h2`. The server will only serve HTTP/1.x.',
|
||||
e,
|
||||
st);
|
||||
}
|
||||
|
||||
var http1 = AngelHttp(app);
|
||||
var http2 = AngelHttp2(app, ctx);
|
||||
|
||||
// HTTP/1.x requests will fallback to `AngelHttp`
|
||||
http2.onHttp1.listen(http1.handleRequest);
|
||||
|
||||
var server = await http2.startServer('127.0.0.1', 3000);
|
||||
print('Listening at https://${server.address.address}:${server.port}');
|
||||
}
|
53
packages/framework/example/json.dart
Normal file
53
packages/framework/example/json.dart
Normal file
|
@ -0,0 +1,53 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'package:platform_framework/platform_framework.dart';
|
||||
import 'package:platform_framework/http.dart';
|
||||
|
||||
void main() async {
|
||||
var x = 0;
|
||||
var c = Completer();
|
||||
var exit = ReceivePort();
|
||||
var isolates = <Isolate>[];
|
||||
|
||||
exit.listen((_) {
|
||||
if (++x >= 50) {
|
||||
c.complete();
|
||||
}
|
||||
});
|
||||
|
||||
for (var i = 1; i < Platform.numberOfProcessors; i++) {
|
||||
var isolate = await Isolate.spawn(serverMain, null);
|
||||
isolates.add(isolate);
|
||||
print('Spawned isolate #${i + 1}...');
|
||||
|
||||
isolate.addOnExitListener(exit.sendPort);
|
||||
}
|
||||
|
||||
serverMain(null);
|
||||
|
||||
print('Angel listening at http://localhost:3000');
|
||||
await c.future;
|
||||
}
|
||||
|
||||
void serverMain(_) async {
|
||||
var app = Angel();
|
||||
var http =
|
||||
AngelHttp.custom(app, startShared, useZone: false); // Run a cluster
|
||||
|
||||
app.get('/', (req, res) {
|
||||
return res.serialize({
|
||||
'foo': 'bar',
|
||||
'one': [2, 'three'],
|
||||
'bar': {'baz': 'quux'}
|
||||
});
|
||||
});
|
||||
|
||||
app.errorHandler = (e, req, res) {
|
||||
print(e.message);
|
||||
print(e.stackTrace);
|
||||
};
|
||||
|
||||
var server = await http.startServer('127.0.0.1', 3000);
|
||||
print('Listening at http://${server.address.address}:${server.port}');
|
||||
}
|
58
packages/framework/example/main.dart
Normal file
58
packages/framework/example/main.dart
Normal file
|
@ -0,0 +1,58 @@
|
|||
import 'package:platform_container/mirrors.dart';
|
||||
import 'package:platform_framework/platform_framework.dart';
|
||||
import 'package:platform_framework/http.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
void main() async {
|
||||
// Logging set up/boilerplate
|
||||
//Logger.root.onRecord.listen(prettyLog);
|
||||
|
||||
// Create our server.
|
||||
var app = Angel(
|
||||
logger: Logger('angel'),
|
||||
reflector: MirrorsReflector(),
|
||||
);
|
||||
|
||||
// Index route. Returns JSON.
|
||||
app.get('/', (req, res) => 'Welcome to Angel!');
|
||||
|
||||
// Accepts a URL like /greet/foo or /greet/bob.
|
||||
app.get(
|
||||
'/greet/:name',
|
||||
(req, res) {
|
||||
var name = req.params['name'];
|
||||
res
|
||||
..write('Hello, $name!')
|
||||
..close();
|
||||
},
|
||||
);
|
||||
|
||||
// Pattern matching - only call this handler if the query value of `name` equals 'emoji'.
|
||||
app.get(
|
||||
'/greet',
|
||||
ioc((@Query('name', match: 'emoji') String name) => '😇🔥🔥🔥'),
|
||||
);
|
||||
|
||||
// Handle any other query value of `name`.
|
||||
app.get(
|
||||
'/greet',
|
||||
ioc((@Query('name') String name) => 'Hello, $name!'),
|
||||
);
|
||||
|
||||
// Simple fallback to throw a 404 on unknown paths.
|
||||
app.fallback((req, res) {
|
||||
throw HttpException.notFound(
|
||||
message: 'Unknown path: "${req.uri!.path}"',
|
||||
);
|
||||
});
|
||||
|
||||
var http = AngelHttp(app);
|
||||
var server = await http.startServer('127.0.0.1', 3000);
|
||||
var url = 'http://${server.address.address}:${server.port}';
|
||||
print('Listening at $url');
|
||||
print('Visit these pages to see Angel in action:');
|
||||
print('* $url/greet/bob');
|
||||
print('* $url/greet/?name=emoji');
|
||||
print('* $url/greet/?name=jack');
|
||||
print('* $url/nonexistent_page');
|
||||
}
|
22
packages/framework/example/map_service.dart
Normal file
22
packages/framework/example/map_service.dart
Normal file
|
@ -0,0 +1,22 @@
|
|||
import 'package:platform_container/mirrors.dart';
|
||||
import 'package:platform_framework/platform_framework.dart';
|
||||
import 'package:platform_framework/http.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
void main() async {
|
||||
// Logging set up/boilerplate
|
||||
Logger.root.onRecord.listen(print);
|
||||
|
||||
// Create our server.
|
||||
var app = Angel(
|
||||
logger: Logger('angel'),
|
||||
reflector: MirrorsReflector(),
|
||||
);
|
||||
|
||||
// Create a RESTful service that manages an in-memory collection.
|
||||
app.use('/api/todos', MapService());
|
||||
|
||||
var http = AngelHttp(app);
|
||||
await http.startServer('127.0.0.1', 0);
|
||||
print('Listening at ${http.uri}');
|
||||
}
|
14
packages/framework/example/status.dart
Normal file
14
packages/framework/example/status.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
import 'package:platform_framework/platform_framework.dart';
|
||||
import 'package:platform_framework/http.dart';
|
||||
|
||||
void main() async {
|
||||
var app = Angel();
|
||||
var http = AngelHttp(app);
|
||||
|
||||
app.fallback((req, res) {
|
||||
res.statusCode = 304;
|
||||
});
|
||||
|
||||
await http.startServer('127.0.0.1', 3000);
|
||||
print('Listening at ${http.uri}');
|
||||
}
|
18
packages/framework/example/view.dart
Normal file
18
packages/framework/example/view.dart
Normal file
|
@ -0,0 +1,18 @@
|
|||
import 'package:platform_container/mirrors.dart';
|
||||
import 'package:platform_framework/platform_framework.dart';
|
||||
import 'package:platform_framework/http.dart';
|
||||
|
||||
void main() async {
|
||||
var app = Angel(reflector: MirrorsReflector());
|
||||
|
||||
app.viewGenerator = (name, [data]) async =>
|
||||
'View generator invoked with name $name and data: $data';
|
||||
|
||||
// Index route. Returns JSON.
|
||||
app.get('/', (req, res) => res.render('index', {'foo': 'bar'}));
|
||||
|
||||
var http = AngelHttp(app);
|
||||
var server = await http.startServer('127.0.0.1', 3000);
|
||||
var url = 'http://${server.address.address}:${server.port}';
|
||||
print('Listening at $url');
|
||||
}
|
9
packages/framework/example/views/index.jl
Normal file
9
packages/framework/example/views/index.jl
Normal file
|
@ -0,0 +1,9 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello!</h1>
|
||||
</body>
|
||||
</html>
|
1
packages/framework/lib/http.dart
Normal file
1
packages/framework/lib/http.dart
Normal file
|
@ -0,0 +1 @@
|
|||
export 'src/http/http.dart';
|
3
packages/framework/lib/http2.dart
Normal file
3
packages/framework/lib/http2.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
export 'src/http2/angel_http2.dart';
|
||||
export 'src/http2/http2_request_context.dart';
|
||||
export 'src/http2/http2_response_context.dart';
|
7
packages/framework/lib/platform_framework.dart
Normal file
7
packages/framework/lib/platform_framework.dart
Normal file
|
@ -0,0 +1,7 @@
|
|||
/// An easily-extensible web server framework in Dart.
|
||||
library angel3_framework;
|
||||
|
||||
export 'package:platform_exceptions/http_exception.dart';
|
||||
export 'package:platform_model/platform_model.dart';
|
||||
export 'package:platform_route/platform_route.dart';
|
||||
export 'src/core/core.dart';
|
61
packages/framework/lib/src/core/anonymous_service.dart
Normal file
61
packages/framework/lib/src/core/anonymous_service.dart
Normal file
|
@ -0,0 +1,61 @@
|
|||
import 'dart:async';
|
||||
import 'service.dart';
|
||||
|
||||
/// An easy helper class to create one-off services without having to create an entire class.
|
||||
///
|
||||
/// Well-suited for testing.
|
||||
class AnonymousService<Id, Data> extends Service<Id, Data> {
|
||||
FutureOr<List<Data>> Function([Map<String, dynamic>?])? _index;
|
||||
FutureOr<Data> Function(Id, [Map<String, dynamic>?])? _read, _remove;
|
||||
FutureOr<Data> Function(Data, [Map<String, dynamic>?])? _create;
|
||||
FutureOr<Data> Function(Id, Data, [Map<String, dynamic>?])? _modify, _update;
|
||||
|
||||
AnonymousService(
|
||||
{FutureOr<List<Data>> Function([Map<String, dynamic>? params])? index,
|
||||
FutureOr<Data> Function(Id id, [Map<String, dynamic>? params])? read,
|
||||
FutureOr<Data> Function(Data data, [Map<String, dynamic>? params])?
|
||||
create,
|
||||
FutureOr<Data> Function(Id id, Data data, [Map<String, dynamic>? params])?
|
||||
modify,
|
||||
FutureOr<Data> Function(Id id, Data data, [Map<String, dynamic>? params])?
|
||||
update,
|
||||
FutureOr<Data> Function(Id id, [Map<String, dynamic>? params])? remove,
|
||||
super.readData}) {
|
||||
_index = index;
|
||||
_read = read;
|
||||
_create = create;
|
||||
_modify = modify;
|
||||
_update = update;
|
||||
_remove = remove;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Data>> index([Map<String, dynamic>? params]) =>
|
||||
Future.sync(() => _index != null ? _index!(params) : super.index(params));
|
||||
|
||||
@override
|
||||
Future<Data> read(Id id, [Map<String, dynamic>? params]) => Future.sync(
|
||||
() => _read != null ? _read!(id, params) : super.read(id, params));
|
||||
|
||||
@override
|
||||
Future<Data> create(Data data, [Map<String, dynamic>? params]) =>
|
||||
Future.sync(() => _create != null
|
||||
? _create!(data, params)
|
||||
: super.create(data, params));
|
||||
|
||||
@override
|
||||
Future<Data> modify(Id id, Data data, [Map<String, dynamic>? params]) =>
|
||||
Future.sync(() => _modify != null
|
||||
? _modify!(id, data, params)
|
||||
: super.modify(id, data, params));
|
||||
|
||||
@override
|
||||
Future<Data> update(Id id, Data data, [Map<String, dynamic>? params]) =>
|
||||
Future.sync(() => _update != null
|
||||
? _update!(id, data, params)
|
||||
: super.update(id, data, params));
|
||||
|
||||
@override
|
||||
Future<Data> remove(Id id, [Map<String, dynamic>? params]) => Future.sync(
|
||||
() => _remove != null ? _remove!(id, params) : super.remove(id, params));
|
||||
}
|
242
packages/framework/lib/src/core/controller.dart
Normal file
242
packages/framework/lib/src/core/controller.dart
Normal file
|
@ -0,0 +1,242 @@
|
|||
library platform_framework.http.controller;
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:platform_container/platform_container.dart';
|
||||
import 'package:platform_route/platform_route.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
import '../core/core.dart';
|
||||
|
||||
/// Supports grouping routes with shared functionality.
|
||||
class Controller {
|
||||
Angel? _app;
|
||||
|
||||
/// The [Angel] application powering this controller.
|
||||
Angel get app {
|
||||
if (_app == null) {
|
||||
throw ArgumentError("Angel is not instantiated.");
|
||||
}
|
||||
|
||||
return _app!;
|
||||
}
|
||||
|
||||
/// If `true` (default), this class will inject itself as a singleton into the [app]'s container when bootstrapped.
|
||||
final bool injectSingleton;
|
||||
|
||||
/// Middleware to run before all handlers in this class.
|
||||
List<RequestHandler> middleware = [];
|
||||
|
||||
/// A mapping of route paths to routes, produced from the [Expose] annotations on this class.
|
||||
Map<String, Route> routeMappings = {};
|
||||
|
||||
SymlinkRoute<RequestHandler>? _mountPoint;
|
||||
|
||||
/// The route at which this controller is mounted on the server.
|
||||
SymlinkRoute<RequestHandler>? get mountPoint => _mountPoint;
|
||||
|
||||
Controller({this.injectSingleton = true});
|
||||
|
||||
/// Applies routes, DI, and other configuration to an [app].
|
||||
@mustCallSuper
|
||||
Future<void> configureServer(Angel app) async {
|
||||
_app = app;
|
||||
|
||||
if (injectSingleton != false) {
|
||||
if (!app.container.has(runtimeType)) {
|
||||
_app!.container.registerSingleton(this, as: runtimeType);
|
||||
}
|
||||
}
|
||||
|
||||
var name = await applyRoutes(app, app.container.reflector);
|
||||
app.controllers[name] = this;
|
||||
//return null;
|
||||
}
|
||||
|
||||
/// Applies the routes from this [Controller] to some [router].
|
||||
Future<String> applyRoutes(
|
||||
Router<RequestHandler> router, Reflector reflector) async {
|
||||
// Load global expose decl
|
||||
var classMirror = reflector.reflectClass(runtimeType)!;
|
||||
var exposeDecl = findExpose(reflector);
|
||||
|
||||
if (exposeDecl == null) {
|
||||
throw Exception('All controllers must carry an @Expose() declaration.');
|
||||
}
|
||||
|
||||
var routable = Routable();
|
||||
_mountPoint = router.mount(exposeDecl.path, routable);
|
||||
//_mountPoint = m;
|
||||
var typeMirror = reflector.reflectType(runtimeType);
|
||||
|
||||
// Pre-reflect methods
|
||||
var instanceMirror = reflector.reflectInstance(this);
|
||||
final handlers = <RequestHandler>[...exposeDecl.middleware, ...middleware];
|
||||
final routeBuilder =
|
||||
_routeBuilder(reflector, instanceMirror, routable, handlers);
|
||||
await configureRoutes(routable);
|
||||
classMirror.declarations.forEach(routeBuilder);
|
||||
|
||||
// Return the name.
|
||||
var result =
|
||||
exposeDecl.as?.isNotEmpty == true ? exposeDecl.as : typeMirror!.name;
|
||||
|
||||
return Future.value(result);
|
||||
}
|
||||
|
||||
void Function(ReflectedDeclaration) _routeBuilder(
|
||||
Reflector reflector,
|
||||
ReflectedInstance? instanceMirror,
|
||||
Routable routable,
|
||||
Iterable<RequestHandler> handlers) {
|
||||
return (ReflectedDeclaration decl) {
|
||||
var methodName = decl.name;
|
||||
|
||||
// Ignore built-in methods.
|
||||
if (methodName != 'toString' &&
|
||||
methodName != 'noSuchMethod' &&
|
||||
methodName != 'call' &&
|
||||
methodName != 'equals' &&
|
||||
methodName != '==') {
|
||||
var exposeDecl = decl.function!.annotations
|
||||
.map((m) => m.reflectee)
|
||||
.firstWhere((r) => r is Expose, orElse: () => null) as Expose?;
|
||||
|
||||
if (exposeDecl == null) {
|
||||
// If this has a @noExpose, return null.
|
||||
if (decl.function!.annotations.any((m) => m.reflectee is NoExpose)) {
|
||||
return;
|
||||
} else {
|
||||
// Otherwise, create an @Expose.
|
||||
exposeDecl = Expose('');
|
||||
}
|
||||
}
|
||||
|
||||
var reflectedMethod =
|
||||
instanceMirror!.getField(methodName).reflectee as Function?;
|
||||
var middleware = <RequestHandler>[
|
||||
...handlers,
|
||||
...exposeDecl.middleware
|
||||
];
|
||||
var name =
|
||||
exposeDecl.as?.isNotEmpty == true ? exposeDecl.as : methodName;
|
||||
|
||||
// Check if normal
|
||||
var method = decl.function!;
|
||||
if (method.parameters.length == 2 &&
|
||||
method.parameters[0].type.reflectedType == RequestContext &&
|
||||
method.parameters[1].type.reflectedType == ResponseContext) {
|
||||
// Create a regular route
|
||||
routeMappings[name ?? ''] = routable
|
||||
.addRoute(exposeDecl.method, exposeDecl.path,
|
||||
(RequestContext req, ResponseContext res) {
|
||||
var result = reflectedMethod!(req, res);
|
||||
return result is RequestHandler ? result(req, res) : result;
|
||||
}, middleware: middleware);
|
||||
return;
|
||||
}
|
||||
|
||||
var injection = preInject(reflectedMethod!, reflector);
|
||||
|
||||
if (exposeDecl.allowNull.isNotEmpty == true) {
|
||||
injection.optional.addAll(exposeDecl.allowNull);
|
||||
}
|
||||
|
||||
// If there is no path, reverse-engineer one.
|
||||
var path = exposeDecl.path;
|
||||
var httpMethod = exposeDecl.method;
|
||||
if (path == '') {
|
||||
// Try to build a route path by finding all potential
|
||||
// path segments, and then joining them.
|
||||
var parts = <String>[];
|
||||
|
||||
// If the name starts with get/post/patch, etc., then that
|
||||
// should be the path.
|
||||
var methodMatch = _methods.firstMatch(method.name);
|
||||
if (methodMatch != null) {
|
||||
var rest = method.name.replaceAll(_methods, '');
|
||||
var restPath = ReCase(rest.isEmpty ? 'index' : rest)
|
||||
.snakeCase
|
||||
.replaceAll(_rgxMultipleUnderscores, '_');
|
||||
httpMethod = methodMatch[1]!.toUpperCase();
|
||||
|
||||
if (['index', 'by_id'].contains(restPath)) {
|
||||
parts.add('/');
|
||||
} else {
|
||||
parts.add(restPath);
|
||||
}
|
||||
}
|
||||
// If the name does NOT start with get/post/patch, etc. then
|
||||
// snake_case-ify the name, and add it to the list of segments.
|
||||
// If the name is index, though, add "/".
|
||||
else {
|
||||
if (method.name == 'index') {
|
||||
parts.add('/');
|
||||
} else {
|
||||
parts.add(ReCase(method.name)
|
||||
.snakeCase
|
||||
.replaceAll(_rgxMultipleUnderscores, '_'));
|
||||
}
|
||||
}
|
||||
|
||||
// Try to infer String, int, or double. We called
|
||||
// preInject() earlier, so we can figure out the types
|
||||
// of required parameters, and add those to the path.
|
||||
for (var p in injection.required) {
|
||||
if (p is List && p.length == 2 && p[0] is String && p[1] is Type) {
|
||||
var name = p[0] as String;
|
||||
var type = p[1] as Type;
|
||||
if (type == String) {
|
||||
parts.add(':$name');
|
||||
} else if (type == int) {
|
||||
parts.add('int:$name');
|
||||
} else if (type == double) {
|
||||
parts.add('double:$name');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
path = parts.join('/');
|
||||
if (!path.startsWith('/')) path = '/$path';
|
||||
}
|
||||
|
||||
routeMappings[name ?? ''] = routable.addRoute(
|
||||
httpMethod, path, handleContained(reflectedMethod, injection),
|
||||
middleware: middleware);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Used to add additional routes or middlewares to the router from within
|
||||
/// a [Controller].
|
||||
///
|
||||
/// ```dart
|
||||
/// @override
|
||||
/// FutureOr<void> configureRoutes(Routable routable) {
|
||||
/// routable.all('*', myMiddleware);
|
||||
/// }
|
||||
/// ```
|
||||
FutureOr<void> configureRoutes(Routable routable) {}
|
||||
|
||||
static final RegExp _methods = RegExp(r'^(get|post|patch|delete)');
|
||||
static final RegExp _rgxMultipleUnderscores = RegExp(r'__+');
|
||||
|
||||
/// Finds the [Expose] declaration for this class.
|
||||
///
|
||||
/// If [concreteOnly] is `false`, then if there is no actual
|
||||
/// [Expose], one will be automatically created.
|
||||
Expose? findExpose(Reflector reflector, {bool concreteOnly = false}) {
|
||||
var existing = reflector
|
||||
.reflectClass(runtimeType)!
|
||||
.annotations
|
||||
.map((m) => m.reflectee)
|
||||
.firstWhere((r) => r is Expose, orElse: () => null) as Expose?;
|
||||
return existing ??
|
||||
(concreteOnly
|
||||
? null
|
||||
: Expose(ReCase(runtimeType.toString())
|
||||
.snakeCase
|
||||
.replaceAll('_controller', '')
|
||||
.replaceAll('_ctrl', '')
|
||||
.replaceAll(_rgxMultipleUnderscores, '_')));
|
||||
}
|
||||
}
|
14
packages/framework/lib/src/core/core.dart
Normal file
14
packages/framework/lib/src/core/core.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
export 'anonymous_service.dart';
|
||||
export 'controller.dart';
|
||||
export 'driver.dart';
|
||||
export 'env.dart';
|
||||
export 'hooked_service.dart';
|
||||
export 'hostname_parser.dart';
|
||||
export 'hostname_router.dart';
|
||||
export 'map_service.dart';
|
||||
export 'metadata.dart';
|
||||
export 'request_context.dart';
|
||||
export 'response_context.dart';
|
||||
export 'routable.dart';
|
||||
export 'server.dart';
|
||||
export 'service.dart';
|
400
packages/framework/lib/src/core/driver.dart
Normal file
400
packages/framework/lib/src/core/driver.dart
Normal file
|
@ -0,0 +1,400 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io' show Cookie;
|
||||
import 'package:platform_exceptions/http_exception.dart';
|
||||
import 'package:platform_route/platform_route.dart';
|
||||
import 'package:belatuk_combinator/belatuk_combinator.dart';
|
||||
import 'package:stack_trace/stack_trace.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
import 'core.dart';
|
||||
|
||||
/// Base driver class for Angel implementations.
|
||||
///
|
||||
/// Powers both AngelHttp and AngelHttp2.
|
||||
abstract class Driver<
|
||||
Request,
|
||||
Response,
|
||||
Server extends Stream<Request>,
|
||||
RequestContextType extends RequestContext,
|
||||
ResponseContextType extends ResponseContext> {
|
||||
final Angel app;
|
||||
final bool useZone;
|
||||
bool _closed = false;
|
||||
|
||||
StreamSubscription<Request>? _sub;
|
||||
//final log = Logger('Driver');
|
||||
|
||||
/// The function used to bind this instance to a server..
|
||||
final Future<Server> Function(dynamic, int) serverGenerator;
|
||||
|
||||
Driver(this.app, this.serverGenerator, {this.useZone = true});
|
||||
|
||||
/// The path at which this server is listening for requests.
|
||||
Uri get uri;
|
||||
|
||||
/// The native server running this instance.
|
||||
Server? server;
|
||||
|
||||
Future<Server> generateServer(address, int port) =>
|
||||
serverGenerator(address, port);
|
||||
|
||||
/// Starts, and returns the server.
|
||||
Future<Server> startServer([address, int port = 0]) {
|
||||
var host = address ?? '127.0.0.1';
|
||||
return generateServer(host, port).then((server) {
|
||||
this.server = server;
|
||||
|
||||
return Future.wait(app.startupHooks.map(app.configure)).then((_) {
|
||||
app.optimizeForProduction();
|
||||
_sub = this.server?.listen((request) {
|
||||
var stream = createResponseStreamFromRawRequest(request);
|
||||
stream.listen((response) {
|
||||
// TODO: To be revisited
|
||||
handleRawRequest(request, response);
|
||||
});
|
||||
});
|
||||
return Future.value(this.server!);
|
||||
});
|
||||
}).catchError((error) {
|
||||
app.logger.severe('Failed to create server', error);
|
||||
throw ArgumentError('[Driver]Failed to create server');
|
||||
});
|
||||
}
|
||||
|
||||
/// Shuts down the underlying server.
|
||||
Future<void> close() {
|
||||
if (_closed) {
|
||||
//return Future.value(_server);
|
||||
return Future.value();
|
||||
}
|
||||
_closed = true;
|
||||
|
||||
_sub?.cancel();
|
||||
|
||||
return app.close().then((_) =>
|
||||
Future.wait(app.shutdownHooks.map(app.configure))
|
||||
.then((_) => Future.value()));
|
||||
/*
|
||||
return app.close().then((_) =>
|
||||
Future.wait(app.shutdownHooks.map(app.configure))
|
||||
.then((_) => Future.value(_server)));
|
||||
*/
|
||||
}
|
||||
|
||||
Future<RequestContextType> createRequestContext(
|
||||
Request request, Response response);
|
||||
|
||||
Future<ResponseContextType> createResponseContext(
|
||||
Request request, Response response,
|
||||
[RequestContextType? correspondingRequest]);
|
||||
|
||||
void setHeader(Response response, String key, String value);
|
||||
|
||||
void setContentLength(Response response, int length);
|
||||
|
||||
void setChunkedEncoding(Response response, bool value);
|
||||
|
||||
void setStatusCode(Response response, int value);
|
||||
|
||||
void addCookies(Response response, Iterable<Cookie> cookies);
|
||||
|
||||
void writeStringToResponse(Response response, String value);
|
||||
|
||||
void writeToResponse(Response response, List<int> data);
|
||||
|
||||
Future closeResponse(Response response);
|
||||
|
||||
Stream<Response> createResponseStreamFromRawRequest(Request request);
|
||||
|
||||
/// Handles a single request.
|
||||
Future handleRawRequest(Request request, Response response) {
|
||||
app.logger.info('[Server] Called handleRawRequest');
|
||||
|
||||
return createRequestContext(request, response).then((req) {
|
||||
return createResponseContext(request, response, req).then((res) {
|
||||
Future handle() {
|
||||
var path = req.path;
|
||||
if (path == '/') path = '';
|
||||
|
||||
Tuple4<List, Map<String, dynamic>, ParseResult<RouteResult>,
|
||||
MiddlewarePipeline> resolveTuple() {
|
||||
var r = app.optimizedRouter;
|
||||
var resolved =
|
||||
r.resolveAbsolute(path, method: req.method, strip: false);
|
||||
var pipeline = MiddlewarePipeline<RequestHandler>(resolved);
|
||||
return Tuple4(
|
||||
pipeline.handlers,
|
||||
resolved.fold<Map<String, dynamic>>(
|
||||
<String, dynamic>{}, (out, r) => out..addAll(r.allParams)),
|
||||
//(resolved.isEmpty ? null : resolved.first.parseResult),
|
||||
resolved.first.parseResult,
|
||||
pipeline,
|
||||
);
|
||||
}
|
||||
|
||||
var cacheKey = req.method + path;
|
||||
var tuple = app.environment.isProduction
|
||||
? app.handlerCache.putIfAbsent(cacheKey, resolveTuple)
|
||||
: resolveTuple();
|
||||
var line = tuple.item4 as MiddlewarePipeline<RequestHandler>;
|
||||
var it = MiddlewarePipelineIterator<RequestHandler>(line);
|
||||
|
||||
req.params.addAll(tuple.item2);
|
||||
|
||||
req.container
|
||||
?..registerSingleton<RequestContext>(req)
|
||||
..registerSingleton<ResponseContext>(res)
|
||||
..registerSingleton<MiddlewarePipeline>(tuple.item4)
|
||||
..registerSingleton<MiddlewarePipeline<RequestHandler>>(line)
|
||||
..registerSingleton<MiddlewarePipelineIterator>(it)
|
||||
..registerSingleton<MiddlewarePipelineIterator<RequestHandler>>(it)
|
||||
..registerSingleton<ParseResult<RouteResult>?>(tuple.item3)
|
||||
..registerSingleton<ParseResult?>(tuple.item3);
|
||||
|
||||
if (!app.environment.isProduction) {
|
||||
req.container?.registerSingleton<Stopwatch>(Stopwatch()..start());
|
||||
}
|
||||
|
||||
return runPipeline(it, req, res, app)
|
||||
.then((_) => sendResponse(request, response, req, res));
|
||||
}
|
||||
|
||||
if (useZone == false) {
|
||||
Future f;
|
||||
|
||||
try {
|
||||
f = handle();
|
||||
} catch (e, st) {
|
||||
f = Future.error(e, st);
|
||||
}
|
||||
|
||||
return f.catchError((e, StackTrace st) {
|
||||
if (e is FormatException) {
|
||||
throw HttpException.badRequest(message: e.message)
|
||||
..stackTrace = st;
|
||||
}
|
||||
throw HttpException(
|
||||
stackTrace: st,
|
||||
statusCode: (e is HttpException) ? e.statusCode : 500,
|
||||
message: e?.toString() ?? '500 Internal Server Error');
|
||||
}, test: (e) => e is HttpException).catchError((ee, StackTrace st) {
|
||||
//print(">>>> Framework error: $ee");
|
||||
//var t = (st).runtimeType;
|
||||
//print(">>>> StackTrace: $t");
|
||||
HttpException e;
|
||||
if (ee is HttpException) {
|
||||
e = ee;
|
||||
} else {
|
||||
e = HttpException(
|
||||
stackTrace: st,
|
||||
statusCode: 500,
|
||||
message: ee?.toString() ?? '500 Internal Server Error');
|
||||
}
|
||||
|
||||
var error = e.error ?? e;
|
||||
var trace = Trace.from(StackTrace.current).terse;
|
||||
app.logger.severe(e.message, error, trace);
|
||||
|
||||
return handleHttpException(e, st, req, res, request, response);
|
||||
});
|
||||
} else {
|
||||
var zoneSpec = ZoneSpecification(
|
||||
print: (self, parent, zone, line) {
|
||||
app.logger.info(line);
|
||||
},
|
||||
handleUncaughtError: (self, parent, zone, error, stackTrace) {
|
||||
var trace = Trace.from(stackTrace).terse;
|
||||
|
||||
// TODO: To be revisited
|
||||
Future(() {
|
||||
HttpException e;
|
||||
|
||||
if (error is FormatException) {
|
||||
e = HttpException.badRequest(message: error.message);
|
||||
} else if (error is HttpException) {
|
||||
e = error;
|
||||
} else {
|
||||
e = HttpException(
|
||||
stackTrace: stackTrace, message: error.toString());
|
||||
}
|
||||
|
||||
app.logger.severe(e.message, error, trace);
|
||||
|
||||
return handleHttpException(
|
||||
e, trace, req, res, request, response);
|
||||
}).catchError((e, StackTrace st) {
|
||||
var trace = Trace.from(st).terse;
|
||||
closeResponse(response);
|
||||
// Ideally, we won't be in a position where an absolutely fatal error occurs,
|
||||
// but if so, we'll need to log it.
|
||||
app.logger.severe(
|
||||
'Fatal error occurred when processing $uri.', e, trace);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
var zone = Zone.current.fork(specification: zoneSpec);
|
||||
req.container?.registerSingleton<Zone>(zone);
|
||||
req.container?.registerSingleton<ZoneSpecification>(zoneSpec);
|
||||
|
||||
// If a synchronous error is thrown, it's not caught by `zone.run`,
|
||||
// so use a try/catch, and recover when need be.
|
||||
|
||||
try {
|
||||
return zone.run(handle);
|
||||
} catch (e, st) {
|
||||
zone.handleUncaughtError(e, st);
|
||||
return Future.value();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Handles an [HttpException].
|
||||
Future handleHttpException(
|
||||
HttpException e,
|
||||
StackTrace st,
|
||||
RequestContext? req,
|
||||
ResponseContext? res,
|
||||
Request request,
|
||||
Response response,
|
||||
{bool ignoreFinalizers = false}) {
|
||||
if (req == null || res == null) {
|
||||
try {
|
||||
app.logger.severe('500 Internal Server Error', e, st);
|
||||
setStatusCode(response, 500);
|
||||
writeStringToResponse(response, '500 Internal Server Error');
|
||||
closeResponse(response);
|
||||
} catch (e) {
|
||||
app.logger.severe('500 Internal Server Error', e);
|
||||
}
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
Future handleError;
|
||||
|
||||
if (!res.isOpen) {
|
||||
handleError = Future.value();
|
||||
} else {
|
||||
res.statusCode = e.statusCode;
|
||||
handleError =
|
||||
Future.sync(() => app.errorHandler(e, req, res)).then((result) {
|
||||
return app.executeHandler(result, req, res).then((_) => res.close());
|
||||
});
|
||||
}
|
||||
|
||||
return handleError.then((_) => sendResponse(request, response, req, res,
|
||||
ignoreFinalizers: ignoreFinalizers == true));
|
||||
}
|
||||
|
||||
/// Sends a response.
|
||||
Future sendResponse(Request request, Response response, RequestContext req,
|
||||
ResponseContext res,
|
||||
{bool ignoreFinalizers = false}) {
|
||||
//app.logger.fine("Calling SendResponse");
|
||||
Future<void> cleanup(_) {
|
||||
if (!app.environment.isProduction && req.container!.has<Stopwatch>()) {
|
||||
var sw = req.container!.make<Stopwatch>();
|
||||
app.logger.fine(
|
||||
"${res.statusCode} ${req.method} ${req.uri} (${sw.elapsedMilliseconds} ms)");
|
||||
}
|
||||
return req.close();
|
||||
}
|
||||
|
||||
// TODO: Debugging header
|
||||
/*
|
||||
for (var key in res.headers.keys) {
|
||||
app.logger.fine("Response header key: $key");
|
||||
}
|
||||
*/
|
||||
|
||||
if (!res.isBuffered) {
|
||||
//if (res.isOpen) {
|
||||
return res.close().then(cleanup);
|
||||
//}
|
||||
//return Future.value();
|
||||
}
|
||||
|
||||
//app.logger.fine("Calling finalizers");
|
||||
|
||||
var finalizers = ignoreFinalizers == true
|
||||
? Future.value()
|
||||
: Future.forEach(app.responseFinalizers, (dynamic f) => f(req, res));
|
||||
|
||||
return finalizers.then((_) {
|
||||
//if (res.isOpen) res.close();
|
||||
|
||||
for (var key in res.headers.keys) {
|
||||
app.logger.fine("Response header key: $key");
|
||||
setHeader(response, key, res.headers[key] ?? '');
|
||||
}
|
||||
|
||||
setContentLength(response, res.buffer?.length ?? 0);
|
||||
setChunkedEncoding(response, res.chunked ?? true);
|
||||
|
||||
var outputBuffer = res.buffer?.toBytes() ?? <int>[];
|
||||
|
||||
if (res.encoders.isNotEmpty) {
|
||||
var allowedEncodings = req.headers
|
||||
?.value('accept-encoding')
|
||||
?.split(',')
|
||||
.map((s) => s.trim())
|
||||
.where((s) => s.isNotEmpty)
|
||||
.map((str) {
|
||||
// Ignore quality specifications in accept-encoding
|
||||
// ex. gzip;q=0.8
|
||||
if (!str.contains(';')) return str;
|
||||
return str.split(';')[0];
|
||||
});
|
||||
|
||||
if (allowedEncodings != null) {
|
||||
for (var encodingName in allowedEncodings) {
|
||||
var key = encodingName;
|
||||
|
||||
Converter<List<int>, List<int>>? encoder;
|
||||
if (res.encoders.containsKey(encodingName)) {
|
||||
encoder = res.encoders[encodingName];
|
||||
} else if (encodingName == '*') {
|
||||
encoder = res.encoders[key = res.encoders.keys.first];
|
||||
}
|
||||
|
||||
if (encoder != null) {
|
||||
setHeader(response, 'content-encoding', key);
|
||||
outputBuffer =
|
||||
res.encoders[key]?.convert(outputBuffer) ?? <int>[];
|
||||
setContentLength(response, outputBuffer.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setStatusCode(response, res.statusCode);
|
||||
addCookies(response, res.cookies);
|
||||
writeToResponse(response, outputBuffer);
|
||||
return closeResponse(response).then(cleanup);
|
||||
});
|
||||
}
|
||||
|
||||
/// Runs a [MiddlewarePipeline].
|
||||
static Future<void> runPipeline<RequestContextType extends RequestContext,
|
||||
ResponseContextType extends ResponseContext>(
|
||||
MiddlewarePipelineIterator<RequestHandler> it,
|
||||
RequestContextType req,
|
||||
ResponseContextType res,
|
||||
Angel app) async {
|
||||
var broken = false;
|
||||
while (it.moveNext()) {
|
||||
var current = it.current.handlers.iterator;
|
||||
|
||||
while (!broken && current.moveNext()) {
|
||||
var result = await app.executeHandler(current.current, req, res);
|
||||
if (result != true) {
|
||||
broken = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
27
packages/framework/lib/src/core/env.dart
Normal file
27
packages/framework/lib/src/core/env.dart
Normal file
|
@ -0,0 +1,27 @@
|
|||
import 'dart:io';
|
||||
|
||||
/// A constant instance of [AngelEnv].
|
||||
const AngelEnvironment angelEnv = AngelEnvironment();
|
||||
|
||||
/// Queries the environment's `ANGEL_ENV` value.
|
||||
class AngelEnvironment {
|
||||
final String? _customValue;
|
||||
|
||||
/// You can optionally provide a custom value, in order to override the system's
|
||||
/// value.
|
||||
const AngelEnvironment([this._customValue]);
|
||||
|
||||
/// Returns the value of the `ANGEL_ENV` variable; defaults to `'development'`.
|
||||
String get value =>
|
||||
(_customValue ?? Platform.environment['ANGEL_ENV'] ?? 'development')
|
||||
.toLowerCase();
|
||||
|
||||
/// Returns whether the [value] is `'development'`.
|
||||
bool get isDevelopment => value == 'development';
|
||||
|
||||
/// Returns whether the [value] is `'production'`.
|
||||
bool get isProduction => value == 'production';
|
||||
|
||||
/// Returns whether the [value] is `'staging'`.
|
||||
bool get isStaging => value == 'staging';
|
||||
}
|
605
packages/framework/lib/src/core/hooked_service.dart
Normal file
605
packages/framework/lib/src/core/hooked_service.dart
Normal file
|
@ -0,0 +1,605 @@
|
|||
library platform_framework.core.hooked_service;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import '../util.dart';
|
||||
import 'metadata.dart';
|
||||
import 'request_context.dart';
|
||||
import 'response_context.dart';
|
||||
import 'routable.dart';
|
||||
import 'server.dart';
|
||||
import 'service.dart';
|
||||
|
||||
/// Wraps another service in a service that broadcasts events on actions.
|
||||
class HookedService<Id, Data, T extends Service<Id, Data>>
|
||||
extends Service<Id, Data> {
|
||||
final List<StreamController<HookedServiceEvent>> _ctrl = [];
|
||||
|
||||
/// Tbe service that is proxied by this hooked one.
|
||||
final T inner;
|
||||
|
||||
final HookedServiceEventDispatcher<Id, Data, T> beforeIndexed =
|
||||
HookedServiceEventDispatcher<Id, Data, T>();
|
||||
final HookedServiceEventDispatcher<Id, Data, T> beforeRead =
|
||||
HookedServiceEventDispatcher<Id, Data, T>();
|
||||
final HookedServiceEventDispatcher<Id, Data, T> beforeCreated =
|
||||
HookedServiceEventDispatcher<Id, Data, T>();
|
||||
final HookedServiceEventDispatcher<Id, Data, T> beforeModified =
|
||||
HookedServiceEventDispatcher<Id, Data, T>();
|
||||
final HookedServiceEventDispatcher<Id, Data, T> beforeUpdated =
|
||||
HookedServiceEventDispatcher<Id, Data, T>();
|
||||
final HookedServiceEventDispatcher<Id, Data, T> beforeRemoved =
|
||||
HookedServiceEventDispatcher<Id, Data, T>();
|
||||
final HookedServiceEventDispatcher<Id, Data, T> afterIndexed =
|
||||
HookedServiceEventDispatcher<Id, Data, T>();
|
||||
final HookedServiceEventDispatcher<Id, Data, T> afterRead =
|
||||
HookedServiceEventDispatcher<Id, Data, T>();
|
||||
final HookedServiceEventDispatcher<Id, Data, T> afterCreated =
|
||||
HookedServiceEventDispatcher<Id, Data, T>();
|
||||
final HookedServiceEventDispatcher<Id, Data, T> afterModified =
|
||||
HookedServiceEventDispatcher<Id, Data, T>();
|
||||
final HookedServiceEventDispatcher<Id, Data, T> afterUpdated =
|
||||
HookedServiceEventDispatcher<Id, Data, T>();
|
||||
final HookedServiceEventDispatcher<Id, Data, T> afterRemoved =
|
||||
HookedServiceEventDispatcher<Id, Data, T>();
|
||||
|
||||
HookedService(this.inner) {
|
||||
// Clone app instance
|
||||
if (inner.isAppActive) {
|
||||
app = inner.app;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<Data> Function(RequestContext, ResponseContext)? get readData =>
|
||||
inner.readData;
|
||||
|
||||
RequestContext? _getRequest(Map? params) {
|
||||
if (params == null) return null;
|
||||
return params['__requestctx'] as RequestContext?;
|
||||
}
|
||||
|
||||
ResponseContext? _getResponse(Map? params) {
|
||||
if (params == null) return null;
|
||||
return params['__responsectx'] as ResponseContext?;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _stripReq(Map<String, dynamic>? params) {
|
||||
if (params == null) {
|
||||
return {};
|
||||
} else {
|
||||
return params.keys
|
||||
.where((key) => key != '__requestctx' && key != '__responsectx')
|
||||
.fold<Map<String, dynamic>>(
|
||||
{}, (map, key) => map..[key] = params[key]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Closes any open [StreamController]s on this instance. **Internal use only**.
|
||||
@override
|
||||
Future close() {
|
||||
for (var c in _ctrl) {
|
||||
c.close();
|
||||
}
|
||||
beforeIndexed._close();
|
||||
beforeRead._close();
|
||||
beforeCreated._close();
|
||||
beforeModified._close();
|
||||
beforeUpdated._close();
|
||||
beforeRemoved._close();
|
||||
afterIndexed._close();
|
||||
afterRead._close();
|
||||
afterCreated._close();
|
||||
afterModified._close();
|
||||
afterUpdated._close();
|
||||
afterRemoved._close();
|
||||
inner.close();
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
/// Adds hooks to this instance.
|
||||
void addHooks(Angel app) {
|
||||
var hooks = getAnnotation<Hooks>(inner, app.container.reflector);
|
||||
var before = <HookedServiceEventListener<Id, Data, T>>[];
|
||||
var after = <HookedServiceEventListener<Id, Data, T>>[];
|
||||
|
||||
if (hooks != null) {
|
||||
before.addAll(hooks.before.cast());
|
||||
after.addAll(hooks.after.cast());
|
||||
}
|
||||
|
||||
void applyListeners(
|
||||
Function fn, HookedServiceEventDispatcher<Id, Data, T> dispatcher,
|
||||
[bool? isAfter]) {
|
||||
var hooks = getAnnotation<Hooks>(fn, app.container.reflector);
|
||||
final listeners = <HookedServiceEventListener<Id, Data, T>>[
|
||||
...isAfter == true ? after : before
|
||||
];
|
||||
|
||||
if (hooks != null) {
|
||||
listeners.addAll((isAfter == true ? hooks.after : hooks.before).cast());
|
||||
}
|
||||
|
||||
listeners.forEach(dispatcher.listen);
|
||||
}
|
||||
|
||||
applyListeners(inner.index, beforeIndexed);
|
||||
applyListeners(inner.read, beforeRead);
|
||||
applyListeners(inner.create, beforeCreated);
|
||||
applyListeners(inner.modify, beforeModified);
|
||||
applyListeners(inner.update, beforeUpdated);
|
||||
applyListeners(inner.remove, beforeRemoved);
|
||||
applyListeners(inner.index, afterIndexed, true);
|
||||
applyListeners(inner.read, afterRead, true);
|
||||
applyListeners(inner.create, afterCreated, true);
|
||||
applyListeners(inner.modify, afterModified, true);
|
||||
applyListeners(inner.update, afterUpdated, true);
|
||||
applyListeners(inner.remove, afterRemoved, true);
|
||||
}
|
||||
|
||||
@override
|
||||
List<RequestHandler> get bootstrappers =>
|
||||
List<RequestHandler>.from(super.bootstrappers)
|
||||
..add((RequestContext req, ResponseContext res) {
|
||||
req.serviceParams
|
||||
..['__requestctx'] = req
|
||||
..['__responsectx'] = res;
|
||||
return true;
|
||||
});
|
||||
|
||||
@override
|
||||
void addRoutes([Service? service]) {
|
||||
super.addRoutes(service ?? inner);
|
||||
}
|
||||
|
||||
/// Runs the [listener] before every service method specified.
|
||||
void before(Iterable<String> eventNames,
|
||||
HookedServiceEventListener<Id, Data, T> listener) {
|
||||
eventNames.map((name) {
|
||||
switch (name) {
|
||||
case HookedServiceEvent.indexed:
|
||||
return beforeIndexed;
|
||||
case HookedServiceEvent.read:
|
||||
return beforeRead;
|
||||
case HookedServiceEvent.created:
|
||||
return beforeCreated;
|
||||
case HookedServiceEvent.modified:
|
||||
return beforeModified;
|
||||
case HookedServiceEvent.updated:
|
||||
return beforeUpdated;
|
||||
case HookedServiceEvent.removed:
|
||||
return beforeRemoved;
|
||||
default:
|
||||
throw ArgumentError('Invalid service method: $name');
|
||||
}
|
||||
}).forEach((HookedServiceEventDispatcher<Id, Data, T> dispatcher) =>
|
||||
dispatcher.listen(listener));
|
||||
}
|
||||
|
||||
/// Runs the [listener] after every service method specified.
|
||||
void after(Iterable<String> eventNames,
|
||||
HookedServiceEventListener<Id, Data, T> listener) {
|
||||
eventNames.map((name) {
|
||||
switch (name) {
|
||||
case HookedServiceEvent.indexed:
|
||||
return afterIndexed;
|
||||
case HookedServiceEvent.read:
|
||||
return afterRead;
|
||||
case HookedServiceEvent.created:
|
||||
return afterCreated;
|
||||
case HookedServiceEvent.modified:
|
||||
return afterModified;
|
||||
case HookedServiceEvent.updated:
|
||||
return afterUpdated;
|
||||
case HookedServiceEvent.removed:
|
||||
return afterRemoved;
|
||||
default:
|
||||
throw ArgumentError('Invalid service method: $name');
|
||||
}
|
||||
}).forEach((HookedServiceEventDispatcher<Id, Data, T> dispatcher) =>
|
||||
dispatcher.listen(listener));
|
||||
}
|
||||
|
||||
/// Runs the [listener] before every service method.
|
||||
void beforeAll(HookedServiceEventListener<Id, Data, T> listener) {
|
||||
beforeIndexed.listen(listener);
|
||||
beforeRead.listen(listener);
|
||||
beforeCreated.listen(listener);
|
||||
beforeModified.listen(listener);
|
||||
beforeUpdated.listen(listener);
|
||||
beforeRemoved.listen(listener);
|
||||
}
|
||||
|
||||
/// Runs the [listener] after every service method.
|
||||
void afterAll(HookedServiceEventListener<Id, Data, T> listener) {
|
||||
afterIndexed.listen(listener);
|
||||
afterRead.listen(listener);
|
||||
afterCreated.listen(listener);
|
||||
afterModified.listen(listener);
|
||||
afterUpdated.listen(listener);
|
||||
afterRemoved.listen(listener);
|
||||
}
|
||||
|
||||
/// Returns a [Stream] of all events fired before every service method.
|
||||
///
|
||||
/// *NOTE*: Only use this if you do not plan to modify events. There is no guarantee
|
||||
/// that events coming out of this [Stream] will see changes you make within the [Stream]
|
||||
/// callback.
|
||||
Stream<HookedServiceEvent<Id, Data, T>> beforeAllStream() {
|
||||
var ctrl = StreamController<HookedServiceEvent<Id, Data, T>>();
|
||||
_ctrl.add(ctrl);
|
||||
before(HookedServiceEvent.all, ctrl.add);
|
||||
return ctrl.stream;
|
||||
}
|
||||
|
||||
/// Returns a [Stream] of all events fired after every service method.
|
||||
///
|
||||
/// *NOTE*: Only use this if you do not plan to modify events. There is no guarantee
|
||||
/// that events coming out of this [Stream] will see changes you make within the [Stream]
|
||||
/// callback.
|
||||
Stream<HookedServiceEvent<Id, Data, T>> afterAllStream() {
|
||||
var ctrl = StreamController<HookedServiceEvent<Id, Data, T>>();
|
||||
_ctrl.add(ctrl);
|
||||
before(HookedServiceEvent.all, ctrl.add);
|
||||
return ctrl.stream;
|
||||
}
|
||||
|
||||
/// Returns a [Stream] of all events fired before every service method specified.
|
||||
///
|
||||
/// *NOTE*: Only use this if you do not plan to modify events. There is no guarantee
|
||||
/// that events coming out of this [Stream] will see changes you make within the [Stream]
|
||||
/// callback.
|
||||
Stream<HookedServiceEvent<Id, Data, T>> beforeStream(
|
||||
Iterable<String> eventNames) {
|
||||
var ctrl = StreamController<HookedServiceEvent<Id, Data, T>>();
|
||||
_ctrl.add(ctrl);
|
||||
before(eventNames, ctrl.add);
|
||||
return ctrl.stream;
|
||||
}
|
||||
|
||||
/// Returns a [Stream] of all events fired AFTER every service method specified.
|
||||
///
|
||||
/// *NOTE*: Only use this if you do not plan to modify events. There is no guarantee
|
||||
/// that events coming out of this [Stream] will see changes you make within the [Stream]
|
||||
/// callback.
|
||||
Stream<HookedServiceEvent<Id, Data, T>> afterStream(
|
||||
Iterable<String> eventNames) {
|
||||
var ctrl = StreamController<HookedServiceEvent<Id, Data, T>>();
|
||||
_ctrl.add(ctrl);
|
||||
after(eventNames, ctrl.add);
|
||||
return ctrl.stream;
|
||||
}
|
||||
|
||||
/// Runs the [listener] before [create], [modify] and [update].
|
||||
void beforeModify(HookedServiceEventListener<Id, Data, T> listener) {
|
||||
beforeCreated.listen(listener);
|
||||
beforeModified.listen(listener);
|
||||
beforeUpdated.listen(listener);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Data>> index([Map<String, dynamic>? params]) {
|
||||
var localParams = _stripReq(params);
|
||||
return beforeIndexed
|
||||
._emit(HookedServiceEvent(false, _getRequest(params),
|
||||
_getResponse(params), inner, HookedServiceEvent.indexed,
|
||||
params: localParams))
|
||||
.then((before) {
|
||||
if (before._canceled) {
|
||||
return afterIndexed
|
||||
._emit(HookedServiceEvent(true, _getRequest(params),
|
||||
_getResponse(params), inner, HookedServiceEvent.indexed,
|
||||
params: localParams, result: before.result))
|
||||
.then((after) => after.result as List<Data>);
|
||||
}
|
||||
|
||||
return inner.index(localParams).then((result) {
|
||||
return afterIndexed
|
||||
._emit(HookedServiceEvent(true, _getRequest(params),
|
||||
_getResponse(params), inner, HookedServiceEvent.indexed,
|
||||
params: localParams, result: result))
|
||||
.then((after) => after.result as List<Data>);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Data> read(Id id, [Map<String, dynamic>? params]) {
|
||||
var localParams = _stripReq(params);
|
||||
return beforeRead
|
||||
._emit(HookedServiceEvent(false, _getRequest(params),
|
||||
_getResponse(params), inner, HookedServiceEvent.read,
|
||||
id: id, params: localParams))
|
||||
.then((before) {
|
||||
if (before._canceled) {
|
||||
return afterRead
|
||||
._emit(HookedServiceEvent(true, _getRequest(params),
|
||||
_getResponse(params), inner, HookedServiceEvent.read,
|
||||
id: id, params: localParams, result: before.result))
|
||||
.then((after) => after.result as Data);
|
||||
}
|
||||
|
||||
return inner.read(id, localParams).then((result) {
|
||||
return afterRead
|
||||
._emit(HookedServiceEvent(true, _getRequest(params),
|
||||
_getResponse(params), inner, HookedServiceEvent.read,
|
||||
id: id, params: localParams, result: result))
|
||||
.then((after) => after.result as Data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Data> create(Data data, [Map<String, dynamic>? params]) {
|
||||
var localParams = _stripReq(params);
|
||||
return beforeCreated
|
||||
._emit(HookedServiceEvent(false, _getRequest(params),
|
||||
_getResponse(params), inner, HookedServiceEvent.created,
|
||||
data: data, params: localParams))
|
||||
.then((before) {
|
||||
if (before._canceled) {
|
||||
return afterCreated
|
||||
._emit(HookedServiceEvent(true, _getRequest(params),
|
||||
_getResponse(params), inner, HookedServiceEvent.created,
|
||||
data: before.data, params: localParams, result: before.result))
|
||||
.then((after) => after.result as Data);
|
||||
}
|
||||
|
||||
return inner.create(before.data as Data, localParams).then((result) {
|
||||
return afterCreated
|
||||
._emit(HookedServiceEvent(true, _getRequest(params),
|
||||
_getResponse(params), inner, HookedServiceEvent.created,
|
||||
data: before.data, params: localParams, result: result))
|
||||
.then((after) => after.result as Data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Data> modify(Id id, Data data, [Map<String, dynamic>? params]) {
|
||||
var localParams = _stripReq(params);
|
||||
return beforeModified
|
||||
._emit(HookedServiceEvent(false, _getRequest(params),
|
||||
_getResponse(params), inner, HookedServiceEvent.modified,
|
||||
id: id, data: data, params: localParams))
|
||||
.then((before) {
|
||||
if (before._canceled) {
|
||||
return afterModified
|
||||
._emit(HookedServiceEvent(true, _getRequest(params),
|
||||
_getResponse(params), inner, HookedServiceEvent.modified,
|
||||
id: id,
|
||||
data: before.data,
|
||||
params: localParams,
|
||||
result: before.result))
|
||||
.then((after) => after.result as Data);
|
||||
}
|
||||
|
||||
return inner.modify(id, before.data as Data, localParams).then((result) {
|
||||
return afterModified
|
||||
._emit(HookedServiceEvent(true, _getRequest(params),
|
||||
_getResponse(params), inner, HookedServiceEvent.created,
|
||||
id: id, data: before.data, params: localParams, result: result))
|
||||
.then((after) => after.result as Data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Data> update(Id id, Data data, [Map<String, dynamic>? params]) {
|
||||
var localParams = _stripReq(params);
|
||||
return beforeUpdated
|
||||
._emit(HookedServiceEvent(false, _getRequest(params),
|
||||
_getResponse(params), inner, HookedServiceEvent.updated,
|
||||
id: id, data: data, params: localParams))
|
||||
.then((before) {
|
||||
if (before._canceled) {
|
||||
return afterUpdated
|
||||
._emit(HookedServiceEvent(true, _getRequest(params),
|
||||
_getResponse(params), inner, HookedServiceEvent.updated,
|
||||
id: id,
|
||||
data: before.data,
|
||||
params: localParams,
|
||||
result: before.result))
|
||||
.then((after) => after.result as Data);
|
||||
}
|
||||
|
||||
return inner.update(id, before.data as Data, localParams).then((result) {
|
||||
return afterUpdated
|
||||
._emit(HookedServiceEvent(true, _getRequest(params),
|
||||
_getResponse(params), inner, HookedServiceEvent.updated,
|
||||
id: id, data: before.data, params: localParams, result: result))
|
||||
.then((after) => after.result as Data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Data> remove(Id id, [Map<String, dynamic>? params]) {
|
||||
var localParams = _stripReq(params);
|
||||
return beforeRemoved
|
||||
._emit(HookedServiceEvent(false, _getRequest(params),
|
||||
_getResponse(params), inner, HookedServiceEvent.removed,
|
||||
id: id, params: localParams))
|
||||
.then((before) {
|
||||
if (before._canceled) {
|
||||
return afterRemoved
|
||||
._emit(HookedServiceEvent(true, _getRequest(params),
|
||||
_getResponse(params), inner, HookedServiceEvent.removed,
|
||||
id: id, params: localParams, result: before.result))
|
||||
.then((after) => after.result) as Data;
|
||||
}
|
||||
|
||||
return inner.remove(id, localParams).then((result) {
|
||||
return afterRemoved
|
||||
._emit(HookedServiceEvent(true, _getRequest(params),
|
||||
_getResponse(params), inner, HookedServiceEvent.removed,
|
||||
id: id, params: localParams, result: result))
|
||||
.then((after) => after.result as Data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Fires an `after` event. This will not be propagated to clients,
|
||||
/// but will be broadcasted to WebSockets, etc.
|
||||
Future<HookedServiceEvent<Id, Data, T>> fire(String eventName, result,
|
||||
[HookedServiceEventListener<Id, Data, T>? callback]) {
|
||||
HookedServiceEventDispatcher<Id, Data, T> dispatcher;
|
||||
|
||||
switch (eventName) {
|
||||
case HookedServiceEvent.indexed:
|
||||
dispatcher = afterIndexed;
|
||||
break;
|
||||
case HookedServiceEvent.read:
|
||||
dispatcher = afterRead;
|
||||
break;
|
||||
case HookedServiceEvent.created:
|
||||
dispatcher = afterCreated;
|
||||
break;
|
||||
case HookedServiceEvent.modified:
|
||||
dispatcher = afterModified;
|
||||
break;
|
||||
case HookedServiceEvent.updated:
|
||||
dispatcher = afterUpdated;
|
||||
break;
|
||||
case HookedServiceEvent.removed:
|
||||
dispatcher = afterRemoved;
|
||||
break;
|
||||
default:
|
||||
throw ArgumentError("Invalid service event name: '$eventName'");
|
||||
}
|
||||
|
||||
var ev =
|
||||
HookedServiceEvent<Id, Data, T>(true, null, null, inner, eventName);
|
||||
return fireEvent(dispatcher, ev, callback);
|
||||
}
|
||||
|
||||
/// Sends an arbitrary event down the hook chain.
|
||||
Future<HookedServiceEvent<Id, Data, T>> fireEvent(
|
||||
HookedServiceEventDispatcher<Id, Data, T> dispatcher,
|
||||
HookedServiceEvent<Id, Data, T> event,
|
||||
[HookedServiceEventListener<Id, Data, T>? callback]) {
|
||||
Future? f;
|
||||
if (callback != null && event._canceled != true) {
|
||||
f = Future.sync(() => callback(event));
|
||||
}
|
||||
f ??= Future.value();
|
||||
return f.then((_) => dispatcher._emit(event));
|
||||
}
|
||||
}
|
||||
|
||||
/// Fired when a hooked service is invoked.
|
||||
class HookedServiceEvent<Id, Data, T extends Service<Id, Data>> {
|
||||
static const String indexed = 'indexed';
|
||||
static const String read = 'read';
|
||||
static const String created = 'created';
|
||||
static const String modified = 'modified';
|
||||
static const String updated = 'updated';
|
||||
static const String removed = 'removed';
|
||||
|
||||
static const List<String> all = [
|
||||
indexed,
|
||||
read,
|
||||
created,
|
||||
modified,
|
||||
updated,
|
||||
removed
|
||||
];
|
||||
|
||||
/// Use this to end processing of an event.
|
||||
void cancel([result]) {
|
||||
_canceled = true;
|
||||
this.result = result ?? this.result;
|
||||
}
|
||||
|
||||
/// Resolves a service from the application.
|
||||
///
|
||||
/// Shorthand for `e.service.app.service(...)`.
|
||||
Service? getService(Pattern path) => service.app.findService(path);
|
||||
|
||||
bool _canceled = false;
|
||||
final String _eventName;
|
||||
Id? _id;
|
||||
final bool _isAfter;
|
||||
Data? data;
|
||||
Map<String, dynamic>? _params;
|
||||
final RequestContext? _request;
|
||||
final ResponseContext? _response;
|
||||
dynamic result;
|
||||
|
||||
String get eventName => _eventName;
|
||||
|
||||
Id? get id => _id;
|
||||
|
||||
bool get isAfter => _isAfter == true;
|
||||
|
||||
bool get isBefore => !isAfter;
|
||||
|
||||
Map get params => _params ?? {};
|
||||
|
||||
RequestContext? get request => _request;
|
||||
|
||||
ResponseContext? get response => _response;
|
||||
|
||||
/// The inner service whose method was hooked.
|
||||
T service;
|
||||
|
||||
HookedServiceEvent(this._isAfter, this._request, this._response, this.service,
|
||||
this._eventName,
|
||||
{Id? id, this.data, Map<String, dynamic>? params, this.result}) {
|
||||
//_data = data;
|
||||
_id = id;
|
||||
_params = params ?? {};
|
||||
}
|
||||
}
|
||||
|
||||
/// Triggered on a hooked service event.
|
||||
typedef HookedServiceEventListener<Id, Data, T extends Service<Id, Data>>
|
||||
= FutureOr<dynamic> Function(HookedServiceEvent<Id, Data, T> event);
|
||||
|
||||
/// Can be listened to, but events may be canceled.
|
||||
class HookedServiceEventDispatcher<Id, Data, T extends Service<Id, Data>> {
|
||||
final List<StreamController<HookedServiceEvent<Id, Data, T>>> _ctrl = [];
|
||||
final List<HookedServiceEventListener<Id, Data, T>> listeners = [];
|
||||
|
||||
void _close() {
|
||||
for (var c in _ctrl) {
|
||||
c.close();
|
||||
}
|
||||
listeners.clear();
|
||||
}
|
||||
|
||||
/// Fires an event, and returns it once it is either canceled, or all listeners have run.
|
||||
Future<HookedServiceEvent<Id, Data, T>> _emit(
|
||||
HookedServiceEvent<Id, Data, T> event) {
|
||||
if (event._canceled == true || listeners.isEmpty) {
|
||||
return Future.value(event);
|
||||
}
|
||||
|
||||
var f = Future<HookedServiceEvent<Id, Data, T>>.value(event);
|
||||
|
||||
for (var listener in listeners) {
|
||||
f = f.then((event) {
|
||||
if (event._canceled) return event;
|
||||
return Future.sync(() => listener(event)).then((_) => event);
|
||||
});
|
||||
}
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
/// Returns a [Stream] containing all events fired by this dispatcher.
|
||||
///
|
||||
/// *NOTE*: Callbacks on the returned [Stream] cannot be guaranteed to run before other [listeners].
|
||||
/// Use this only if you need a read-only stream of events.
|
||||
Stream<HookedServiceEvent<Id, Data, T>> asStream() {
|
||||
var ctrl = StreamController<HookedServiceEvent<Id, Data, T>>();
|
||||
_ctrl.add(ctrl);
|
||||
listen(ctrl.add);
|
||||
return ctrl.stream;
|
||||
}
|
||||
|
||||
/// Registers the listener to be called whenever an event is triggered.
|
||||
void listen(HookedServiceEventListener<Id, Data, T> listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
}
|
81
packages/framework/lib/src/core/hostname_parser.dart
Normal file
81
packages/framework/lib/src/core/hostname_parser.dart
Normal file
|
@ -0,0 +1,81 @@
|
|||
import 'dart:collection';
|
||||
import 'package:string_scanner/string_scanner.dart';
|
||||
|
||||
/// Parses a string into a [RegExp] that is matched against hostnames.
|
||||
class HostnameSyntaxParser {
|
||||
final SpanScanner _scanner;
|
||||
final _safe = RegExp(r'[0-9a-zA-Z-_:]+');
|
||||
|
||||
HostnameSyntaxParser(String hostname)
|
||||
: _scanner = SpanScanner(hostname, sourceUrl: hostname);
|
||||
|
||||
FormatException _formatExc(String message) {
|
||||
var span = _scanner.lastSpan ?? _scanner.emptySpan;
|
||||
return FormatException(
|
||||
'${span.start.toolString}: $message\n${span.highlight(color: true)}');
|
||||
}
|
||||
|
||||
RegExp parse() {
|
||||
var b = StringBuffer();
|
||||
var parts = Queue<String>();
|
||||
|
||||
while (!_scanner.isDone) {
|
||||
if (_scanner.scan('|')) {
|
||||
if (parts.isEmpty) {
|
||||
throw _formatExc('No hostname parts found before "|".');
|
||||
} else {
|
||||
var next = _parseHostnamePart();
|
||||
if (next.isEmpty) {
|
||||
throw _formatExc('No hostname parts found after "|".');
|
||||
} else {
|
||||
var prev = parts.removeLast();
|
||||
parts.addLast('(($prev)|($next))');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var part = _parseHostnamePart();
|
||||
if (part.isNotEmpty) {
|
||||
if (_scanner.scan('.')) {
|
||||
var subPart = _parseHostnamePart(shouldThrow: false);
|
||||
while (subPart.isNotEmpty) {
|
||||
part += '\\.$subPart';
|
||||
if (_scanner.scan('.')) {
|
||||
subPart = _parseHostnamePart(shouldThrow: false);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
parts.add(part);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (parts.isNotEmpty) {
|
||||
b.write(parts.removeFirst());
|
||||
}
|
||||
|
||||
if (b.isEmpty) {
|
||||
throw _formatExc('Invalid or empty hostname.');
|
||||
} else {
|
||||
return RegExp('^$b\$', caseSensitive: false);
|
||||
}
|
||||
}
|
||||
|
||||
String _parseHostnamePart({bool shouldThrow = true}) {
|
||||
if (_scanner.scan('*.')) {
|
||||
return r'([^$.]+\.)?';
|
||||
} else if (_scanner.scan('*')) {
|
||||
return r'[^$]*';
|
||||
} else if (_scanner.scan('+')) {
|
||||
return r'[^$]+';
|
||||
} else if (_scanner.scan(_safe)) {
|
||||
return _scanner.lastMatch?[0] ?? '';
|
||||
} else if (!_scanner.isDone && shouldThrow) {
|
||||
var s = String.fromCharCode(_scanner.peekChar()!);
|
||||
throw _formatExc('Unexpected character "$s".');
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
117
packages/framework/lib/src/core/hostname_router.dart
Normal file
117
packages/framework/lib/src/core/hostname_router.dart
Normal file
|
@ -0,0 +1,117 @@
|
|||
import 'dart:async';
|
||||
import 'package:platform_container/platform_container.dart';
|
||||
import 'package:platform_route/platform_route.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'env.dart';
|
||||
import 'hostname_parser.dart';
|
||||
import 'request_context.dart';
|
||||
import 'response_context.dart';
|
||||
import 'routable.dart';
|
||||
import 'server.dart';
|
||||
|
||||
/// A utility that allows requests to be handled based on their
|
||||
/// origin's hostname.
|
||||
///
|
||||
/// For example, an application could handle example.com and api.example.com
|
||||
/// separately.
|
||||
///
|
||||
/// The provided patterns can be any `Pattern`. If a `String` is provided, a simple
|
||||
/// grammar (see [HostnameSyntaxParser]) is used to create [RegExp].
|
||||
///
|
||||
/// For example:
|
||||
/// * `example.com` -> `/example\.com/`
|
||||
/// * `*.example.com` -> `/([^$.]\.)?example\.com/`
|
||||
/// * `example.*` -> `/example\./[^$]*`
|
||||
/// * `example.+` -> `/example\./[^$]+`
|
||||
class HostnameRouter {
|
||||
final Map<Pattern, Angel> _apps = {};
|
||||
final Map<Pattern, FutureOr<Angel> Function()> _creators = {};
|
||||
final List<Pattern> _patterns = [];
|
||||
|
||||
HostnameRouter(
|
||||
{Map<Pattern, Angel> apps = const {},
|
||||
Map<Pattern, FutureOr<Angel> Function()> creators = const {}}) {
|
||||
Map<Pattern, V> parseMap<V>(Map<Pattern, V> map) {
|
||||
return map.map((p, c) {
|
||||
Pattern pp;
|
||||
|
||||
if (p is String) {
|
||||
pp = HostnameSyntaxParser(p).parse();
|
||||
} else {
|
||||
pp = p;
|
||||
}
|
||||
|
||||
return MapEntry(pp, c);
|
||||
});
|
||||
}
|
||||
|
||||
apps = parseMap(apps);
|
||||
creators = parseMap(creators);
|
||||
var patterns = apps.keys.followedBy(creators.keys).toSet().toList();
|
||||
_apps.addAll(apps);
|
||||
_creators.addAll(creators);
|
||||
_patterns.addAll(patterns);
|
||||
// print(_creators);
|
||||
}
|
||||
|
||||
factory HostnameRouter.configure(
|
||||
Map<Pattern, FutureOr<void> Function(Angel)> configurers,
|
||||
{Reflector reflector = const EmptyReflector(),
|
||||
AngelEnvironment environment = angelEnv,
|
||||
Logger? logger,
|
||||
bool allowMethodOverrides = true,
|
||||
FutureOr<String> Function(dynamic)? serializer,
|
||||
ViewGenerator? viewGenerator}) {
|
||||
var creators = configurers.map((p, c) {
|
||||
return MapEntry(p, () async {
|
||||
var app = Angel(
|
||||
reflector: reflector,
|
||||
environment: environment,
|
||||
logger: logger,
|
||||
allowMethodOverrides: allowMethodOverrides,
|
||||
serializer: serializer,
|
||||
viewGenerator: viewGenerator);
|
||||
await app.configure(c);
|
||||
return app;
|
||||
});
|
||||
});
|
||||
return HostnameRouter(creators: creators);
|
||||
}
|
||||
|
||||
/// Attempts to handle a request, according to its hostname.
|
||||
///
|
||||
/// If none is matched, then `true` is returned.
|
||||
/// Also returns `true` if all of the sub-app's handlers returned
|
||||
/// `true`.
|
||||
Future<bool> handleRequest(RequestContext req, ResponseContext res) async {
|
||||
for (var pattern in _patterns) {
|
||||
// print('${req.hostname} vs $_creators');
|
||||
if (pattern.allMatches(req.hostname).isNotEmpty) {
|
||||
// Resolve the entire pipeline within the context of the selected app.
|
||||
var app = _apps[pattern] ??= (await _creators[pattern]!());
|
||||
// print('App for ${req.hostname} = $app from $pattern');
|
||||
// app.dumpTree();
|
||||
|
||||
var r = app.optimizedRouter;
|
||||
var resolved = r.resolveAbsolute(req.path, method: req.method);
|
||||
var pipeline = MiddlewarePipeline<RequestHandler>(resolved);
|
||||
// print('Pipeline: $pipeline');
|
||||
for (var handler in pipeline.handlers) {
|
||||
// print(handler);
|
||||
// Avoid stack overflow.
|
||||
if (handler == handleRequest) {
|
||||
continue;
|
||||
} else if (!await app.executeHandler(handler, req, res)) {
|
||||
// print('$handler TERMINATED');
|
||||
return false;
|
||||
} else {
|
||||
// print('$handler CONTINUED');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, return true.
|
||||
return true;
|
||||
}
|
||||
}
|
208
packages/framework/lib/src/core/injection.dart
Normal file
208
packages/framework/lib/src/core/injection.dart
Normal file
|
@ -0,0 +1,208 @@
|
|||
part of 'request_context.dart';
|
||||
|
||||
const List<Type> _primitiveTypes = [String, int, num, double, Null];
|
||||
|
||||
/// Shortcut for calling [preInject], and then [handleContained].
|
||||
///
|
||||
/// Use this to instantly create a request handler for a DI-enabled method.
|
||||
///
|
||||
/// Calling [ioc] also auto-serializes the result of a [handler].
|
||||
RequestHandler ioc(Function handler, {Iterable<String> optional = const []}) {
|
||||
return (req, res) {
|
||||
RequestHandler? contained;
|
||||
|
||||
if (req.app?.container != null) {
|
||||
var injection = preInject(handler, req.app!.container.reflector);
|
||||
//if (injection != null) {
|
||||
injection.optional.addAll(optional);
|
||||
contained = handleContained(handler, injection);
|
||||
//}
|
||||
}
|
||||
|
||||
return req.app!.executeHandler(contained, req, res);
|
||||
};
|
||||
}
|
||||
|
||||
Future resolveInjection(requirement, InjectionRequest injection,
|
||||
RequestContext req, ResponseContext res, bool throwOnUnresolved,
|
||||
[Container? container]) async {
|
||||
dynamic propFromApp;
|
||||
container ??= req.container ?? res.app!.container;
|
||||
|
||||
if (requirement == RequestContext) {
|
||||
return req;
|
||||
} else if (requirement == ResponseContext) {
|
||||
return res;
|
||||
} else if (requirement is String &&
|
||||
injection.parameters.containsKey(requirement)) {
|
||||
var param = injection.parameters[requirement]!;
|
||||
var value = param.getValue(req);
|
||||
if (value == null && param.required != false) throw param.error as Object;
|
||||
return value;
|
||||
} else if (requirement is String) {
|
||||
if (req.container!.hasNamed(requirement)) {
|
||||
return req.container!.findByName(requirement);
|
||||
}
|
||||
if (req.params.containsKey(requirement)) {
|
||||
return req.params[requirement];
|
||||
} else if ((propFromApp = req.app!.findProperty(requirement)) != null) {
|
||||
return propFromApp;
|
||||
} else if (injection.optional.contains(requirement)) {
|
||||
return null;
|
||||
} else if (throwOnUnresolved) {
|
||||
throw ArgumentError(
|
||||
"Cannot resolve parameter '$requirement' within handler.");
|
||||
}
|
||||
} else if (requirement is List &&
|
||||
requirement.length == 2 &&
|
||||
requirement.first is String &&
|
||||
requirement.last is Type) {
|
||||
var key = requirement.first;
|
||||
var type = requirement.last;
|
||||
if (req.params.containsKey(key) ||
|
||||
req.app!.configuration.containsKey(key) ||
|
||||
_primitiveTypes.contains(type)) {
|
||||
return await resolveInjection(
|
||||
key, injection, req, res, throwOnUnresolved, container);
|
||||
} else {
|
||||
return await resolveInjection(
|
||||
type, injection, req, res, throwOnUnresolved, container);
|
||||
}
|
||||
} else if (requirement is Type && requirement != dynamic) {
|
||||
try {
|
||||
var futureType = container.reflector.reflectFutureOf(requirement);
|
||||
if (container.has(futureType.reflectedType)) {
|
||||
return await container.make(futureType.reflectedType);
|
||||
}
|
||||
} on UnsupportedError {
|
||||
// Ignore.
|
||||
}
|
||||
|
||||
return await container.make(requirement);
|
||||
} else if (throwOnUnresolved) {
|
||||
throw ArgumentError(
|
||||
'$requirement cannot be injected into a request handler.');
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if an [InjectionRequest] can be sufficiently executed within the current request/response context.
|
||||
bool suitableForInjection(
|
||||
RequestContext req, ResponseContext res, InjectionRequest injection) {
|
||||
return injection.parameters.values.any((p) {
|
||||
if (p.match == null) return false;
|
||||
var value = p.getValue(req);
|
||||
return value == p.match;
|
||||
});
|
||||
}
|
||||
|
||||
/// Handles a request with a DI-enabled handler.
|
||||
RequestHandler handleContained(Function handler, InjectionRequest injection,
|
||||
[Container? container]) {
|
||||
return (RequestContext req, ResponseContext res) async {
|
||||
if (injection.parameters.isNotEmpty &&
|
||||
injection.parameters.values.any((p) => p.match != null) &&
|
||||
!suitableForInjection(req, res, injection)) {
|
||||
return Future.value(true);
|
||||
}
|
||||
|
||||
var args = [];
|
||||
|
||||
var named = <Symbol, dynamic>{};
|
||||
|
||||
for (var r in injection.required) {
|
||||
args.add(await resolveInjection(r, injection, req, res, true, container));
|
||||
}
|
||||
|
||||
for (var entry in injection.named.entries) {
|
||||
var name = Symbol(entry.key);
|
||||
named[name] = await resolveInjection(
|
||||
[entry.key, entry.value], injection, req, res, false, container);
|
||||
}
|
||||
|
||||
return Function.apply(handler, args, named);
|
||||
};
|
||||
}
|
||||
|
||||
/// Contains a list of the data required for a DI-enabled method to run.
|
||||
///
|
||||
/// This improves performance by removing the necessity to reflect a method
|
||||
/// every time it is requested.
|
||||
///
|
||||
/// Regular request handlers can also skip DI entirely, lowering response time
|
||||
/// and memory use.
|
||||
class InjectionRequest {
|
||||
/// Optional, typed data that can be passed to a DI-enabled method.
|
||||
final Map<String, Type> named;
|
||||
|
||||
/// A list of the arguments required for a DI-enabled method to run.
|
||||
final List required;
|
||||
|
||||
/// A list of the arguments that can be null in a DI-enabled method.
|
||||
final List<String> optional;
|
||||
|
||||
/// Extended parameter definitions.
|
||||
final Map<String, Parameter> parameters;
|
||||
|
||||
const InjectionRequest.constant(
|
||||
{this.named = const {},
|
||||
this.required = const [],
|
||||
this.optional = const [],
|
||||
this.parameters = const {}});
|
||||
|
||||
InjectionRequest()
|
||||
: named = {},
|
||||
required = [],
|
||||
optional = [],
|
||||
parameters = {};
|
||||
}
|
||||
|
||||
/// Predetermines what needs to be injected for a handler to run.
|
||||
InjectionRequest preInject(Function handler, Reflector reflector) {
|
||||
var injection = InjectionRequest();
|
||||
|
||||
var closureMirror = reflector.reflectFunction(handler)!;
|
||||
|
||||
if (closureMirror.parameters.isEmpty) return injection;
|
||||
|
||||
// Load parameters
|
||||
for (var parameter in closureMirror.parameters) {
|
||||
var name = parameter.name;
|
||||
var type = parameter.type.reflectedType;
|
||||
|
||||
var p = parameter.annotations
|
||||
.firstWhereOrNull(
|
||||
(m) => m.type.isAssignableTo(reflector.reflectType(Parameter)))
|
||||
?.reflectee as Parameter?;
|
||||
//print(p);
|
||||
if (p != null) {
|
||||
injection.parameters[name] = Parameter(
|
||||
cookie: p.cookie,
|
||||
header: p.header,
|
||||
query: p.query,
|
||||
session: p.session,
|
||||
match: p.match,
|
||||
defaultValue: p.defaultValue,
|
||||
required: parameter.isNamed ? false : p.required != false,
|
||||
);
|
||||
}
|
||||
|
||||
if (!parameter.isNamed) {
|
||||
if (!parameter.isRequired) injection.optional.add(name);
|
||||
|
||||
if (type == RequestContext || type == ResponseContext) {
|
||||
injection.required.add(type);
|
||||
} else if (name == 'req') {
|
||||
injection.required.add(RequestContext);
|
||||
} else if (name == 'res') {
|
||||
injection.required.add(ResponseContext);
|
||||
} else if (type == dynamic) {
|
||||
injection.required.add(name);
|
||||
} else {
|
||||
injection.required.add([name, type]);
|
||||
}
|
||||
} else {
|
||||
injection.named[name] = type;
|
||||
}
|
||||
}
|
||||
return injection;
|
||||
}
|
170
packages/framework/lib/src/core/map_service.dart
Normal file
170
packages/framework/lib/src/core/map_service.dart
Normal file
|
@ -0,0 +1,170 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:platform_exceptions/http_exception.dart';
|
||||
|
||||
import 'service.dart';
|
||||
|
||||
/// A basic service that manages an in-memory list of maps.
|
||||
class MapService extends Service<String?, Map<String, dynamic>> {
|
||||
/// If set to `true`, clients can remove all items by passing a `null` `id` to `remove`.
|
||||
///
|
||||
/// `false` by default.
|
||||
final bool allowRemoveAll;
|
||||
|
||||
/// If set to `true`, parameters in `req.query` are applied to the database query.
|
||||
final bool allowQuery;
|
||||
|
||||
/// If set to `true` (default), then the service will manage an `id` string and `createdAt` and `updatedAt` fields.
|
||||
final bool autoIdAndDateFields;
|
||||
|
||||
/// If set to `true` (default), then the keys `created_at` and `updated_at` will automatically be snake_cased.
|
||||
final bool autoSnakeCaseNames;
|
||||
|
||||
final List<Map<String, dynamic>> items = [];
|
||||
|
||||
MapService(
|
||||
{this.allowRemoveAll = false,
|
||||
this.allowQuery = true,
|
||||
this.autoIdAndDateFields = true,
|
||||
this.autoSnakeCaseNames = true})
|
||||
: super();
|
||||
|
||||
String get createdAtKey =>
|
||||
autoSnakeCaseNames == false ? 'createdAt' : 'created_at';
|
||||
|
||||
String get updatedAtKey =>
|
||||
autoSnakeCaseNames == false ? 'updatedAt' : 'updated_at';
|
||||
|
||||
bool Function(Map<String, dynamic>) _matchesId(id) {
|
||||
return (Map<String, dynamic> item) {
|
||||
if (item['id'] == null) {
|
||||
return false;
|
||||
} else if (autoIdAndDateFields != false) {
|
||||
return item['id'] == id?.toString();
|
||||
} else {
|
||||
return item['id'] == id;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Map<String, dynamic>>> index([Map<String, dynamic>? params]) {
|
||||
if (allowQuery == false || params == null || params['query'] is! Map) {
|
||||
return Future.value(items);
|
||||
} else {
|
||||
var query = params['query'] as Map?;
|
||||
|
||||
return Future.value(items.where((item) {
|
||||
for (var key in query!.keys) {
|
||||
if (!item.containsKey(key)) {
|
||||
return false;
|
||||
} else if (item[key] != query[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}).toList());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> read(String? id,
|
||||
[Map<String, dynamic>? params]) {
|
||||
return Future.value(items.firstWhere(_matchesId(id),
|
||||
orElse: (() => throw HttpException.notFound(
|
||||
message: 'No record found for ID $id'))));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> create(Map<String, dynamic> data,
|
||||
[Map<String, dynamic>? params]) {
|
||||
var now = DateTime.now().toIso8601String();
|
||||
var result = Map<String, dynamic>.from(data);
|
||||
|
||||
if (autoIdAndDateFields == true) {
|
||||
result
|
||||
..['id'] = items.length.toString()
|
||||
..[autoSnakeCaseNames == false ? 'createdAt' : 'created_at'] = now
|
||||
..[autoSnakeCaseNames == false ? 'updatedAt' : 'updated_at'] = now;
|
||||
}
|
||||
items.add(result);
|
||||
return Future.value(result);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> modify(String? id, Map<String, dynamic> data,
|
||||
[Map<String, dynamic>? params]) {
|
||||
//if (data is! Map) {
|
||||
// throw HttpException.badRequest(
|
||||
// message:
|
||||
// 'MapService does not support `modify` with ${data.runtimeType}.');
|
||||
//}
|
||||
if (!items.any(_matchesId(id))) return create(data, params);
|
||||
|
||||
return read(id).then((item) {
|
||||
var idx = items.indexOf(item);
|
||||
if (idx < 0) return create(data, params);
|
||||
var result = Map<String, dynamic>.from(item)..addAll(data);
|
||||
|
||||
if (autoIdAndDateFields == true) {
|
||||
result[autoSnakeCaseNames == false ? 'updatedAt' : 'updated_at'] =
|
||||
DateTime.now().toIso8601String();
|
||||
}
|
||||
return Future.value(items[idx] = result);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> update(String? id, Map<String, dynamic> data,
|
||||
[Map<String, dynamic>? params]) {
|
||||
//if (data is! Map) {
|
||||
// throw HttpException.badRequest(
|
||||
// message:
|
||||
// 'MapService does not support `update` with ${data.runtimeType}.');
|
||||
//}
|
||||
if (!items.any(_matchesId(id))) return create(data, params);
|
||||
|
||||
return read(id).then((old) {
|
||||
if (!items.remove(old)) {
|
||||
throw HttpException.notFound(message: 'No record found for ID $id');
|
||||
}
|
||||
|
||||
var result = Map<String, dynamic>.from(data);
|
||||
if (autoIdAndDateFields == true) {
|
||||
result
|
||||
..['id'] = id?.toString()
|
||||
..[autoSnakeCaseNames == false ? 'createdAt' : 'created_at'] =
|
||||
old[autoSnakeCaseNames == false ? 'createdAt' : 'created_at']
|
||||
..[autoSnakeCaseNames == false ? 'updatedAt' : 'updated_at'] =
|
||||
DateTime.now().toIso8601String();
|
||||
}
|
||||
items.add(result);
|
||||
return Future.value(result);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> remove(String? id,
|
||||
[Map<String, dynamic>? params]) {
|
||||
if (id == null || id == 'null') {
|
||||
// Remove everything...
|
||||
if (!(allowRemoveAll == true ||
|
||||
params?.containsKey('provider') != true)) {
|
||||
throw HttpException.forbidden(
|
||||
message: 'Clients are not allowed to delete all items.');
|
||||
} else {
|
||||
items.clear();
|
||||
return Future.value({});
|
||||
}
|
||||
}
|
||||
|
||||
return read(id, params).then((result) {
|
||||
if (items.remove(result)) {
|
||||
return result;
|
||||
} else {
|
||||
throw HttpException.notFound(message: 'No record found for ID $id');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
166
packages/framework/lib/src/core/metadata.dart
Normal file
166
packages/framework/lib/src/core/metadata.dart
Normal file
|
@ -0,0 +1,166 @@
|
|||
library angel_framework.http.metadata;
|
||||
|
||||
import 'package:platform_exceptions/http_exception.dart';
|
||||
|
||||
import 'hooked_service.dart' show HookedServiceEventListener;
|
||||
import 'request_context.dart';
|
||||
import 'routable.dart';
|
||||
|
||||
/// Annotation to map middleware onto a handler.
|
||||
class Middleware {
|
||||
final Iterable<RequestHandler> handlers;
|
||||
|
||||
const Middleware(this.handlers);
|
||||
}
|
||||
|
||||
/// Attaches hooks to a [HookedService].
|
||||
class Hooks {
|
||||
final List<HookedServiceEventListener> before;
|
||||
final List<HookedServiceEventListener> after;
|
||||
|
||||
const Hooks({this.before = const [], this.after = const []});
|
||||
}
|
||||
|
||||
/// Specifies to NOT expose a method to the Internet.
|
||||
class NoExpose {
|
||||
const NoExpose();
|
||||
}
|
||||
|
||||
const NoExpose noExpose = NoExpose();
|
||||
|
||||
/// Exposes a [Controller] or a [Controller] method to the Internet.
|
||||
/// Example:
|
||||
///
|
||||
/// ```dart
|
||||
/// @Expose('/elements')
|
||||
/// class ElementController extends Controller {
|
||||
///
|
||||
/// @Expose('/')
|
||||
/// List<Element> getList() => someComputationHere();
|
||||
///
|
||||
/// @Expose('/int:elementId')
|
||||
/// getElement(int elementId) => someOtherComputation();
|
||||
///
|
||||
/// }
|
||||
/// ```
|
||||
class Expose {
|
||||
final String method;
|
||||
final String path;
|
||||
final Iterable<RequestHandler> middleware;
|
||||
final String? as;
|
||||
final List<String> allowNull;
|
||||
|
||||
static const Expose get = Expose('', method: 'GET'),
|
||||
post = Expose('', method: 'POST'),
|
||||
patch = Expose('', method: 'PATCH'),
|
||||
put = Expose('', method: 'PUT'),
|
||||
delete = Expose('', method: 'DELETE'),
|
||||
head = Expose('', method: 'HEAD');
|
||||
|
||||
const Expose(this.path,
|
||||
{this.method = 'GET',
|
||||
this.middleware = const [],
|
||||
this.as,
|
||||
this.allowNull = const []});
|
||||
|
||||
const Expose.method(this.method,
|
||||
{this.middleware = const [], this.as, this.allowNull = const []})
|
||||
: path = '';
|
||||
}
|
||||
|
||||
/// Used to apply special dependency injections or functionality to a function parameter.
|
||||
class Parameter {
|
||||
/// Inject the value of a request cookie.
|
||||
final String? cookie;
|
||||
|
||||
/// Inject the value of a request header.
|
||||
final String? header;
|
||||
|
||||
/// Inject the value of a key from the session.
|
||||
final String? session;
|
||||
|
||||
/// Inject the value of a key from the query.
|
||||
final String? query;
|
||||
|
||||
/// Only execute the handler if the value of this parameter matches the given value.
|
||||
final dynamic match;
|
||||
|
||||
/// Specify a default value.
|
||||
final dynamic defaultValue;
|
||||
|
||||
/// If `true` (default), then an error will be thrown if this parameter is not present.
|
||||
final bool required;
|
||||
|
||||
const Parameter(
|
||||
{this.cookie,
|
||||
this.query,
|
||||
this.header,
|
||||
this.session,
|
||||
this.match,
|
||||
this.defaultValue,
|
||||
this.required = true});
|
||||
|
||||
/// Returns an error that can be thrown when the parameter is not present.
|
||||
Object? get error {
|
||||
if (cookie?.isNotEmpty == true) {
|
||||
return HttpException.badRequest(
|
||||
message: 'Missing required cookie "$cookie".');
|
||||
}
|
||||
if (header?.isNotEmpty == true) {
|
||||
return HttpException.badRequest(
|
||||
message: 'Missing required header "$header".');
|
||||
}
|
||||
if (query?.isNotEmpty == true) {
|
||||
return HttpException.badRequest(
|
||||
message: 'Missing required query parameter "$query".');
|
||||
}
|
||||
if (session?.isNotEmpty == true) {
|
||||
return StateError('Session does not contain required key "$session".');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Obtains a value for this parameter from a [RequestContext].
|
||||
dynamic getValue(RequestContext req) {
|
||||
if (cookie?.isNotEmpty == true) {
|
||||
return req.cookies.firstWhere((c) => c.name == cookie).value;
|
||||
}
|
||||
if (header?.isNotEmpty == true) {
|
||||
return req.headers?.value(header ?? '') ?? defaultValue;
|
||||
}
|
||||
if (session?.isNotEmpty == true) {
|
||||
return req.session?[session] ?? defaultValue;
|
||||
}
|
||||
if (query?.isNotEmpty == true) {
|
||||
return req.uri?.queryParameters[query] ?? defaultValue;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// Shortcut for declaring a request header [Parameter].
|
||||
class Header extends Parameter {
|
||||
const Header(String header, {super.match, super.defaultValue, super.required})
|
||||
: super(header: header);
|
||||
}
|
||||
|
||||
/// Shortcut for declaring a request session [Parameter].
|
||||
class Session extends Parameter {
|
||||
const Session(String session,
|
||||
{super.match, super.defaultValue, super.required})
|
||||
: super(session: session);
|
||||
}
|
||||
|
||||
/// Shortcut for declaring a request query [Parameter].
|
||||
class Query extends Parameter {
|
||||
const Query(String query, {super.match, super.defaultValue, super.required})
|
||||
: super(query: query);
|
||||
}
|
||||
|
||||
/// Shortcut for declaring a request cookie [Parameter].
|
||||
class CookieValue extends Parameter {
|
||||
const CookieValue(String cookie,
|
||||
{super.match, super.defaultValue, super.required})
|
||||
: super(cookie: cookie);
|
||||
}
|
382
packages/framework/lib/src/core/request_context.dart
Normal file
382
packages/framework/lib/src/core/request_context.dart
Normal file
|
@ -0,0 +1,382 @@
|
|||
library angel_framework.http.request_context;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data' show BytesBuilder;
|
||||
import 'dart:io'
|
||||
show Cookie, HeaderValue, HttpHeaders, HttpSession, InternetAddress;
|
||||
|
||||
import 'package:platform_container/platform_container.dart';
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:belatuk_http_server/belatuk_http_server.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import 'metadata.dart';
|
||||
import 'response_context.dart';
|
||||
import 'routable.dart';
|
||||
import 'server.dart' show Angel;
|
||||
|
||||
part 'injection.dart';
|
||||
|
||||
/// A convenience wrapper around an incoming [RawRequest].
|
||||
abstract class RequestContext<RawRequest> {
|
||||
/// Similar to [Angel.shutdownHooks], allows for logic to be executed
|
||||
/// when a [RequestContext] is done being processed.
|
||||
final _log = Logger('RequestContext');
|
||||
|
||||
final List<FutureOr<void> Function()> shutdownHooks = [];
|
||||
|
||||
String? _acceptHeaderCache, _extensionCache;
|
||||
bool? _acceptsAllCache;
|
||||
Map<String, dynamic>? _queryParameters;
|
||||
Object? _bodyObject;
|
||||
bool _hasParsedBody = false, _closed = false;
|
||||
Map<String, dynamic> _bodyFields = {};
|
||||
List _bodyList = [];
|
||||
List<UploadedFile> _uploadedFiles = <UploadedFile>[];
|
||||
MediaType _contentType = MediaType('text', 'plain');
|
||||
|
||||
/// The underlying [RawRequest] provided by the driver.
|
||||
RawRequest get rawRequest;
|
||||
|
||||
/// Additional params to be passed to services.
|
||||
final Map<String, dynamic> serviceParams = {};
|
||||
|
||||
/// The [Angel] instance that is responding to this request.
|
||||
Angel? app;
|
||||
|
||||
/// Any cookies sent with this request.
|
||||
List<Cookie> get cookies => <Cookie>[];
|
||||
|
||||
/// All HTTP headers sent with this request.
|
||||
HttpHeaders? get headers;
|
||||
|
||||
/// The requested hostname.
|
||||
String get hostname => 'localhost';
|
||||
|
||||
/// The IoC container that can be used to provide functionality to produce
|
||||
/// objects of a given type.
|
||||
///
|
||||
/// This is a *child* of the container found in `app`.
|
||||
Container? get container;
|
||||
|
||||
/// The user's IP.
|
||||
String get ip => remoteAddress.address;
|
||||
|
||||
/// This request's HTTP method.
|
||||
///
|
||||
/// This may have been processed by an override. See [originalMethod] to get the real method.
|
||||
String get method => 'GET';
|
||||
|
||||
/// The original HTTP verb sent to the server.
|
||||
String get originalMethod => 'GET';
|
||||
|
||||
/// The content type of an incoming request.
|
||||
MediaType get contentType {
|
||||
if (headers?.contentType != null) {
|
||||
try {
|
||||
_contentType = MediaType.parse(headers!.contentType.toString());
|
||||
} catch (e) {
|
||||
_log.warning(
|
||||
'Invalid media type [${headers!.contentType.toString()}]', e);
|
||||
}
|
||||
}
|
||||
return _contentType;
|
||||
}
|
||||
|
||||
/// The URL parameters extracted from the request URI.
|
||||
Map<String, dynamic> params = <String, dynamic>{};
|
||||
|
||||
/// The requested path.
|
||||
String get path => '';
|
||||
|
||||
/// Is this an **XMLHttpRequest**?
|
||||
bool get isXhr {
|
||||
return headers?.value('X-Requested-With')?.trim().toLowerCase() ==
|
||||
'xmlhttprequest';
|
||||
}
|
||||
|
||||
/// The remote address requesting this resource.
|
||||
InternetAddress get remoteAddress;
|
||||
|
||||
/// The user's HTTP session.
|
||||
HttpSession? get session;
|
||||
|
||||
/// The [Uri] instance representing the path this request is responding to.
|
||||
Uri? get uri;
|
||||
|
||||
/// The [Stream] of incoming binary data sent from the client.
|
||||
Stream<List<int>>? get body;
|
||||
|
||||
/// Returns `true` if [parseBody] has been called so far.
|
||||
bool get hasParsedBody => _hasParsedBody;
|
||||
|
||||
/// Returns a *mutable* [Map] of the fields parsed from the request [body].
|
||||
///
|
||||
/// Note that [parseBody] must be called first.
|
||||
Map<String, dynamic> get bodyAsMap {
|
||||
if (!hasParsedBody) {
|
||||
throw StateError('The request body has not been parsed yet.');
|
||||
}
|
||||
// else if (_bodyFields == null) {
|
||||
// throw StateError('The request body, $_bodyObject, is not a Map.');
|
||||
//}
|
||||
|
||||
return _bodyFields;
|
||||
}
|
||||
|
||||
/// This setter allows you to explicitly set the request body **exactly once**.
|
||||
///
|
||||
/// Use this if the format of the body is not natively parsed by Angel.
|
||||
set bodyAsMap(Map<String, dynamic>? value) => bodyAsObject = value;
|
||||
|
||||
/// Returns a *mutable* [List] parsed from the request [body].
|
||||
///
|
||||
/// Note that [parseBody] must be called first.
|
||||
List? get bodyAsList {
|
||||
if (!hasParsedBody) {
|
||||
throw StateError('The request body has not been parsed yet.');
|
||||
// TODO: Relook at this
|
||||
//} else if (_bodyList == null) {
|
||||
} else if (_bodyList.isEmpty) {
|
||||
throw StateError('The request body, $_bodyObject, is not a List.');
|
||||
}
|
||||
|
||||
return _bodyList;
|
||||
}
|
||||
|
||||
/// This setter allows you to explicitly set the request body **exactly once**.
|
||||
///
|
||||
/// Use this if the format of the body is not natively parsed by Angel.
|
||||
set bodyAsList(List? value) => bodyAsObject = value;
|
||||
|
||||
/// Returns the parsed request body, whatever it may be (typically a [Map] or [List]).
|
||||
///
|
||||
/// Note that [parseBody] must be called first.
|
||||
Object? get bodyAsObject {
|
||||
if (!hasParsedBody) {
|
||||
throw StateError('The request body has not been parsed yet.');
|
||||
}
|
||||
|
||||
return _bodyObject;
|
||||
}
|
||||
|
||||
/// This setter allows you to explicitly set the request body **exactly once**.
|
||||
///
|
||||
/// Use this if the format of the body is not natively parsed by Angel.
|
||||
set bodyAsObject(value) {
|
||||
if (_bodyObject != null) {
|
||||
throw StateError(
|
||||
'The request body has already been parsed/set, and cannot be overwritten.');
|
||||
} else {
|
||||
if (value is List) _bodyList = value;
|
||||
if (value is Map<String, dynamic>) _bodyFields = value;
|
||||
_bodyObject = value;
|
||||
_hasParsedBody = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a *mutable* map of the files parsed from the request [body].
|
||||
///
|
||||
/// Note that [parseBody] must be called first.
|
||||
List<UploadedFile>? get uploadedFiles {
|
||||
if (!hasParsedBody) {
|
||||
throw StateError('The request body has not been parsed yet.');
|
||||
}
|
||||
|
||||
return _uploadedFiles;
|
||||
}
|
||||
|
||||
/// Returns a *mutable* map of the fields contained in the query.
|
||||
Map<String, dynamic> get queryParameters => _queryParameters ??=
|
||||
Map<String, dynamic>.from(uri?.queryParameters ?? {});
|
||||
|
||||
/// Returns the file extension of the requested path, if any.
|
||||
///
|
||||
/// Includes the leading `.`, if there is one.
|
||||
String get extension => _extensionCache ??= p.extension(uri?.path ?? '');
|
||||
|
||||
/// Returns `true` if the client's `Accept` header indicates that the given [contentType] is considered a valid response.
|
||||
///
|
||||
/// You cannot provide a `null` [contentType].
|
||||
/// If the `Accept` header's value is `*/*`, this method will always return `true`.
|
||||
/// To ignore the wildcard (`*/*`), pass [strict] as `true`.
|
||||
///
|
||||
/// [contentType] can be either of the following:
|
||||
/// * A [ContentType], in which case the `Accept` header will be compared against its `mimeType` property.
|
||||
/// * Any other Dart value, in which case the `Accept` header will be compared against the result of a `toString()` call.
|
||||
bool accepts(contentType, {bool strict = false}) {
|
||||
var contentTypeString = contentType is MediaType
|
||||
? contentType.mimeType
|
||||
: contentType?.toString();
|
||||
|
||||
// Change to assert
|
||||
if (contentTypeString == null) {
|
||||
_log.severe('RequestContext.accepts is null');
|
||||
throw ArgumentError(
|
||||
'RequestContext.accepts expects the `contentType` parameter to NOT be null.');
|
||||
}
|
||||
|
||||
_acceptHeaderCache ??= headers?.value('accept');
|
||||
|
||||
if (_acceptHeaderCache == null) {
|
||||
return true;
|
||||
} else if (strict != true && _acceptHeaderCache!.contains('*/*')) {
|
||||
return true;
|
||||
} else {
|
||||
return _acceptHeaderCache!.contains(contentTypeString);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns as `true` if the client's `Accept` header indicates that it will accept any response content type.
|
||||
bool get acceptsAll => _acceptsAllCache ??= accepts('*/*');
|
||||
|
||||
/// Shorthand for deserializing [bodyAsMap], using some transformer function [f].
|
||||
Future<T> deserializeBody<T>(FutureOr<T> Function(Map?) f,
|
||||
{Encoding encoding = utf8}) async {
|
||||
await parseBody(encoding: encoding);
|
||||
return await f(bodyAsMap);
|
||||
}
|
||||
|
||||
/// Shorthand for decoding [bodyAsMap], using some [codec].
|
||||
Future<T> decodeBody<T>(Codec<T, Map?> codec, {Encoding encoding = utf8}) =>
|
||||
deserializeBody(codec.decode, encoding: encoding);
|
||||
|
||||
/// Manually parses the request body, if it has not already been parsed.
|
||||
Future<void> parseBody({Encoding encoding = utf8}) async {
|
||||
//if (contentType == null) {
|
||||
// throw FormatException('Missing "content-type" header.');
|
||||
//}
|
||||
|
||||
if (!_hasParsedBody) {
|
||||
_hasParsedBody = true;
|
||||
|
||||
var contentBody = body ?? Stream.empty();
|
||||
|
||||
if (contentType.type == 'application' && contentType.subtype == 'json') {
|
||||
_uploadedFiles = [];
|
||||
|
||||
var parsed = (_bodyObject =
|
||||
await encoding.decoder.bind(contentBody).join().then(json.decode));
|
||||
|
||||
if (parsed is Map) {
|
||||
_bodyFields = Map<String, dynamic>.from(parsed);
|
||||
} else if (parsed is List) {
|
||||
_bodyList = parsed;
|
||||
}
|
||||
} else if (contentType.type == 'application' &&
|
||||
contentType.subtype == 'x-www-form-urlencoded') {
|
||||
_uploadedFiles = [];
|
||||
var parsed = await encoding.decoder
|
||||
.bind(contentBody)
|
||||
.join()
|
||||
.then((s) => Uri.splitQueryString(s, encoding: encoding));
|
||||
_bodyFields = Map<String, dynamic>.from(parsed);
|
||||
} else if (contentType.type == 'multipart' &&
|
||||
contentType.subtype == 'form-data' &&
|
||||
contentType.parameters.containsKey('boundary')) {
|
||||
var boundary = contentType.parameters['boundary'] ?? '';
|
||||
var transformer = MimeMultipartTransformer(boundary);
|
||||
var parts = transformer.bind(contentBody).map((part) =>
|
||||
HttpMultipartFormData.parse(part, defaultEncoding: encoding));
|
||||
_bodyFields = {};
|
||||
_uploadedFiles = [];
|
||||
|
||||
await for (var part in parts) {
|
||||
if (part.isBinary) {
|
||||
_uploadedFiles.add(UploadedFile(part));
|
||||
} else if (part.isText &&
|
||||
part.contentDisposition.parameters.containsKey('name')) {
|
||||
// If there is no name, then don't parse it.
|
||||
var key = part.contentDisposition.parameters['name'];
|
||||
if (key != null) {
|
||||
var value = await part.join();
|
||||
_bodyFields[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_bodyFields = {};
|
||||
_uploadedFiles = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Disposes of all resources.
|
||||
@mustCallSuper
|
||||
Future<void> close() async {
|
||||
if (!_closed) {
|
||||
_closed = true;
|
||||
_acceptsAllCache = null;
|
||||
_acceptHeaderCache = null;
|
||||
serviceParams.clear();
|
||||
params.clear();
|
||||
await Future.forEach(shutdownHooks, (dynamic hook) => hook());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads information about a binary chunk uploaded to the server.
|
||||
class UploadedFile {
|
||||
/// The underlying `form-data` item.
|
||||
final HttpMultipartFormData formData;
|
||||
final log = Logger('UploadedFile');
|
||||
|
||||
MediaType _contentType = MediaType('multipart', 'form-data');
|
||||
|
||||
UploadedFile(this.formData);
|
||||
|
||||
/// Returns the binary stream from [formData].
|
||||
Stream<List<int>> get data => formData.cast<List<int>>();
|
||||
|
||||
/// The filename associated with the data on the user's system.
|
||||
/// Returns [:null:] if not present.
|
||||
String? get filename => formData.contentDisposition.parameters['filename'];
|
||||
|
||||
/// The name of the field associated with this data.
|
||||
/// Returns [:null:] if not present.
|
||||
String? get name => formData.contentDisposition.parameters['name'];
|
||||
|
||||
/// The parsed [:Content-Type:] header of the [:HttpMultipartFormData:].
|
||||
/// Returns [:null:] if not present.
|
||||
//MediaType get contentType => _contentType ??= (formData.contentType == null
|
||||
// ? null
|
||||
// : MediaType.parse(formData.contentType.toString()));
|
||||
|
||||
MediaType get contentType {
|
||||
if (formData.contentType != null) {
|
||||
try {
|
||||
_contentType = MediaType.parse(formData.contentType.toString());
|
||||
} catch (e) {
|
||||
log.warning(
|
||||
'Invalue media type [${formData.contentType.toString()}]', e);
|
||||
}
|
||||
}
|
||||
|
||||
return _contentType;
|
||||
}
|
||||
|
||||
/// The parsed [:Content-Transfer-Encoding:] header of the
|
||||
/// [:HttpMultipartFormData:]. This field is used to determine how to decode
|
||||
/// the data. Returns [:null:] if not present.
|
||||
HeaderValue? get contentTransferEncoding => formData.contentTransferEncoding;
|
||||
|
||||
/// Reads the contents of the file into a single linear buffer.
|
||||
///
|
||||
/// Note that this leads to holding the whole file in memory, which might
|
||||
/// not be ideal for large files.w
|
||||
Future<List<int>> readAsBytes() {
|
||||
return data
|
||||
.fold<BytesBuilder>(BytesBuilder(), (bb, out) => bb..add(out))
|
||||
.then((bb) => bb.takeBytes());
|
||||
}
|
||||
|
||||
/// Reads the contents of the file as [String], using the given [encoding].
|
||||
Future<String> readAsString({Encoding encoding = utf8}) {
|
||||
return encoding.decoder.bind(data).join();
|
||||
}
|
||||
}
|
456
packages/framework/lib/src/core/response_context.dart
Normal file
456
packages/framework/lib/src/core/response_context.dart
Normal file
|
@ -0,0 +1,456 @@
|
|||
library platform_framework.http.response_context;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:convert' as c show json;
|
||||
import 'dart:io' show BytesBuilder, Cookie;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:platform_route/platform_route.dart';
|
||||
import 'package:file/file.dart';
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
|
||||
import 'controller.dart';
|
||||
import 'request_context.dart';
|
||||
import 'server.dart' show Angel;
|
||||
|
||||
final RegExp _straySlashes = RegExp(r'(^/+)|(/+$)');
|
||||
|
||||
/// A convenience wrapper around an outgoing HTTP request.
|
||||
abstract class ResponseContext<RawResponse>
|
||||
implements StreamConsumer<List<int>>, StreamSink<List<int>>, StringSink {
|
||||
final Map properties = {};
|
||||
final CaseInsensitiveMap<String> _headers = CaseInsensitiveMap<String>.from(
|
||||
{'content-type': 'text/plain', 'server': 'Angel3'});
|
||||
|
||||
//final log = Logger('ResponseContext');
|
||||
|
||||
Completer? _done;
|
||||
int _statusCode = 200;
|
||||
|
||||
/// The [Angel] instance that is sending a response.
|
||||
Angel? app;
|
||||
|
||||
/// Is `Transfer-Encoding` chunked?
|
||||
bool? chunked;
|
||||
|
||||
/// Any and all cookies to be sent to the user.
|
||||
final List<Cookie> cookies = [];
|
||||
|
||||
/// A set of [Converter] objects that can be used to encode response data.
|
||||
///
|
||||
/// At most one encoder will ever be used to convert data.
|
||||
final Map<String, Converter<List<int>, List<int>>> encoders = {};
|
||||
|
||||
/// A [Map] of data to inject when `res.render` is called.
|
||||
///
|
||||
/// This can be used to reduce boilerplate when using templating engines.
|
||||
final Map<String, dynamic> renderParams = {};
|
||||
|
||||
/// Points to the [RequestContext] corresponding to this response.
|
||||
RequestContext? get correspondingRequest;
|
||||
|
||||
@override
|
||||
Future get done => (_done ?? Completer()).future;
|
||||
|
||||
/// Headers that will be sent to the user.
|
||||
///
|
||||
/// Note that if you have already started writing to the underlying stream, headers will not persist.
|
||||
CaseInsensitiveMap<String> get headers => _headers;
|
||||
|
||||
/// Serializes response data into a String.
|
||||
///
|
||||
/// The default is conversion into JSON via `json.encode`.
|
||||
///
|
||||
/// If you are 100% sure that your response handlers will only
|
||||
/// be JSON-encodable objects (i.e. primitives, `List`s and `Map`s),
|
||||
/// then consider setting [serializer] to `JSON.encode`.
|
||||
///
|
||||
/// To set it globally for the whole [app], use the following helper:
|
||||
/// ```dart
|
||||
/// app.injectSerializer(JSON.encode);
|
||||
/// ```
|
||||
FutureOr<String> Function(dynamic) serializer = c.json.encode;
|
||||
|
||||
/// This response's status code.
|
||||
int get statusCode => _statusCode;
|
||||
|
||||
set statusCode(int value) {
|
||||
if (!isOpen) {
|
||||
throw closed();
|
||||
} else {
|
||||
_statusCode = value; // ?? 200;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the response is still available for processing by Angel.
|
||||
///
|
||||
/// If it is `false`, then Angel will stop executing handlers, and will only run
|
||||
/// response finalizers if the response [isBuffered].
|
||||
bool get isOpen;
|
||||
|
||||
/// Returns `true` if response data is being written to a buffer, rather than to the underlying stream.
|
||||
bool get isBuffered;
|
||||
|
||||
/// A set of UTF-8 encoded bytes that will be written to the response.
|
||||
BytesBuilder? get buffer;
|
||||
|
||||
/// The underlying [RawResponse] under this instance.
|
||||
RawResponse get rawResponse;
|
||||
|
||||
/// Signals Angel that the response is being held alive deliberately, and that the framework should not automatically close it.
|
||||
///
|
||||
/// This is mostly used in situations like WebSocket handlers, where the connection should remain
|
||||
/// open indefinitely.
|
||||
FutureOr<RawResponse> detach();
|
||||
|
||||
/// Gets or sets the content length to send back to a client.
|
||||
///
|
||||
/// Returns `null` if the header is invalidly formatted.
|
||||
int? get contentLength {
|
||||
return int.tryParse(headers['content-length'] ?? '-1');
|
||||
}
|
||||
|
||||
/// Gets or sets the content length to send back to a client.
|
||||
///
|
||||
/// If [value] is `null`, then the header will be removed.
|
||||
set contentLength(int? value) {
|
||||
if (value == null || value == -1) {
|
||||
headers.remove('content-length');
|
||||
} else {
|
||||
headers['content-length'] = value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets or sets the content type to send back to a client.
|
||||
MediaType get contentType {
|
||||
try {
|
||||
return MediaType.parse(headers['content-type']!);
|
||||
} catch (_) {
|
||||
return MediaType('text', 'plain');
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets or sets the content type to send back to a client.
|
||||
set contentType(MediaType value) {
|
||||
headers['content-type'] = value.toString();
|
||||
}
|
||||
|
||||
static StateError closed() => StateError('Cannot modify a closed response.');
|
||||
|
||||
/// Sends a download as a response.
|
||||
Future<void> download(File file, {String? filename}) async {
|
||||
if (!isOpen) throw closed();
|
||||
|
||||
headers['Content-Disposition'] =
|
||||
'attachment; filename="${filename ?? file.path}"';
|
||||
contentType = MediaType.parse(lookupMimeType(file.path)!);
|
||||
headers['content-length'] = file.lengthSync().toString();
|
||||
|
||||
if (!isBuffered) {
|
||||
await file.openRead().cast<List<int>>().pipe(this);
|
||||
} else {
|
||||
buffer!.add(file.readAsBytesSync());
|
||||
await close();
|
||||
}
|
||||
}
|
||||
|
||||
/// Prevents more data from being written to the response, and locks it entire from further editing.
|
||||
@override
|
||||
Future<void> close() {
|
||||
if (buffer is LockableBytesBuilder) {
|
||||
(buffer as LockableBytesBuilder).lock();
|
||||
}
|
||||
|
||||
if (_done?.isCompleted == false) _done!.complete();
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
/// Serializes JSON to the response.
|
||||
Future<bool> json(value) =>
|
||||
serialize(value, contentType: MediaType('application', 'json'));
|
||||
|
||||
/// Returns a JSONP response.
|
||||
///
|
||||
/// You can override the [contentType] sent; by default it is `application/javascript`.
|
||||
Future<void> jsonp(value,
|
||||
{String callbackName = 'callback', MediaType? contentType}) {
|
||||
if (!isOpen) throw closed();
|
||||
this.contentType = contentType ?? MediaType('application', 'javascript');
|
||||
write('$callbackName(${serializer(value)})');
|
||||
return close();
|
||||
}
|
||||
|
||||
/// Renders a view to the response stream, and closes the response.
|
||||
Future<void> render(String view, [Map<String, dynamic>? data]) {
|
||||
if (!isOpen) throw closed();
|
||||
contentType = MediaType('text', 'html', {'charset': 'utf-8'});
|
||||
return Future<String>.sync(() => app!.viewGenerator!(
|
||||
view,
|
||||
Map<String, dynamic>.from(renderParams)
|
||||
..addAll(data ?? <String, dynamic>{}))).then((content) {
|
||||
write(content);
|
||||
return close();
|
||||
});
|
||||
}
|
||||
|
||||
/// Redirects to user to the given URL.
|
||||
///
|
||||
/// [url] can be a `String`, or a `List`.
|
||||
/// If it is a `List`, a URI will be constructed
|
||||
/// based on the provided params.
|
||||
///
|
||||
/// See [Router]#navigate for more. :)
|
||||
Future<void> redirect(url, {bool absolute = true, int? code}) {
|
||||
if (!isOpen) throw closed();
|
||||
headers
|
||||
..['content-type'] = 'text/html'
|
||||
..['location'] = (url is String || url is Uri)
|
||||
? url.toString()
|
||||
: app!.navigate(url as Iterable, absolute: absolute);
|
||||
statusCode = code ?? 302;
|
||||
write('''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Redirecting...</title>
|
||||
<meta http-equiv="refresh" content="0; url=$url">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Currently redirecting you...</h1>
|
||||
<br />
|
||||
Click <a href="$url">here</a> if you are not automatically redirected...
|
||||
<script>
|
||||
window.location = "$url";
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
''');
|
||||
return close();
|
||||
}
|
||||
|
||||
/// Redirects to the given named [Route].
|
||||
Future<void> redirectTo(String name, [Map? params, int? code]) async {
|
||||
if (!isOpen) throw closed();
|
||||
Route? findRoute(Router r) {
|
||||
for (var route in r.routes) {
|
||||
if (route is SymlinkRoute) {
|
||||
final m = findRoute(route.router);
|
||||
|
||||
if (m != null) return m;
|
||||
} else if (route.name == name) {
|
||||
return route;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
var matched = findRoute(app!);
|
||||
|
||||
if (matched != null) {
|
||||
await redirect(
|
||||
matched.makeUri(params!.keys.fold<Map<String, dynamic>>({}, (out, k) {
|
||||
return out..[k.toString()] = params[k];
|
||||
})),
|
||||
code: code);
|
||||
return;
|
||||
}
|
||||
|
||||
throw ArgumentError.notNull('Route to redirect to ($name)');
|
||||
}
|
||||
|
||||
/// Redirects to the given [Controller] action.
|
||||
Future<void> redirectToAction(String action, [Map? params, int? code]) {
|
||||
if (!isOpen) throw closed();
|
||||
// UserController@show
|
||||
var split = action.split('@');
|
||||
|
||||
if (split.length < 2) {
|
||||
throw Exception(
|
||||
"Controller redirects must take the form of 'Controller@action'. You gave: $action");
|
||||
}
|
||||
|
||||
var controller = app!.controllers[split[0].replaceAll(_straySlashes, '')];
|
||||
|
||||
if (controller == null) {
|
||||
throw Exception("Could not find a controller named '${split[0]}'");
|
||||
}
|
||||
|
||||
var matched = controller.routeMappings[split[1]];
|
||||
|
||||
if (matched == null) {
|
||||
throw Exception(
|
||||
"Controller '${split[0]}' does not contain any action named '${split[1]}'");
|
||||
}
|
||||
|
||||
final head = controller
|
||||
.findExpose(app!.container.reflector)!
|
||||
.path
|
||||
.toString()
|
||||
.replaceAll(_straySlashes, '');
|
||||
var tail = '';
|
||||
if (params != null) {
|
||||
tail = matched
|
||||
.makeUri(params.keys.fold<Map<String, dynamic>>({}, (out, k) {
|
||||
return out..[k.toString()] = params[k];
|
||||
}))
|
||||
.replaceAll(_straySlashes, '');
|
||||
}
|
||||
return redirect('$head/$tail'.replaceAll(_straySlashes, ''), code: code);
|
||||
}
|
||||
|
||||
/// Serializes data to the response.
|
||||
Future<bool> serialize(value, {MediaType? contentType}) async {
|
||||
if (!isOpen) {
|
||||
throw closed();
|
||||
}
|
||||
this.contentType = contentType ?? MediaType('application', 'json');
|
||||
var text = await serializer(value);
|
||||
if (text.isEmpty) return true;
|
||||
write(text);
|
||||
await close();
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Streams a file to this response.
|
||||
///
|
||||
/// `HEAD` responses will not actually write data.
|
||||
Future streamFile(File file) async {
|
||||
if (!isOpen) {
|
||||
throw closed();
|
||||
}
|
||||
var mimeType = app!.mimeTypeResolver.lookup(file.path);
|
||||
contentLength = await file.length();
|
||||
contentType = mimeType == null
|
||||
? MediaType('application', 'octet-stream')
|
||||
: MediaType.parse(mimeType);
|
||||
|
||||
if (correspondingRequest!.method != 'HEAD') {
|
||||
return addStream(file.openRead().cast<List<int>>()).then((_) => close());
|
||||
}
|
||||
}
|
||||
|
||||
/// Configure the response to write to an intermediate response buffer, rather than to the stream directly.
|
||||
void useBuffer();
|
||||
|
||||
/// Adds a stream directly the underlying response.
|
||||
///
|
||||
/// If this instance has access to a [correspondingRequest], then it will attempt to transform
|
||||
/// the content using at most one of the response [encoders].
|
||||
@override
|
||||
Future addStream(Stream<List<int>> stream);
|
||||
|
||||
@override
|
||||
void addError(Object error, [StackTrace? stackTrace]) {
|
||||
if (_done?.isCompleted == false) {
|
||||
_done!.completeError(error, stackTrace);
|
||||
} else if (_done == null) {
|
||||
if (stackTrace != null) {
|
||||
Zone.current.handleUncaughtError(error, stackTrace);
|
||||
} else {
|
||||
app?.logger.warning('[ResponseContext] stackTrace is null');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes data to the response.
|
||||
@override
|
||||
void write(value, {Encoding? encoding}) {
|
||||
encoding ??= utf8;
|
||||
|
||||
if (!isOpen && isBuffered) {
|
||||
throw closed();
|
||||
} else if (!isBuffered) {
|
||||
add(encoding.encode(value.toString()));
|
||||
} else {
|
||||
buffer!.add(encoding.encode(value.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void writeCharCode(int charCode) {
|
||||
if (!isOpen && isBuffered) {
|
||||
throw closed();
|
||||
} else if (!isBuffered) {
|
||||
add([charCode]);
|
||||
} else {
|
||||
buffer!.addByte(charCode);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void writeln([Object? obj = '']) {
|
||||
write(obj.toString());
|
||||
write('\r\n');
|
||||
}
|
||||
|
||||
@override
|
||||
void writeAll(Iterable objects, [String separator = '']) {
|
||||
write(objects.join(separator));
|
||||
}
|
||||
}
|
||||
|
||||
abstract class LockableBytesBuilder implements BytesBuilder {
|
||||
factory LockableBytesBuilder() {
|
||||
return _LockableBytesBuilderImpl();
|
||||
}
|
||||
|
||||
void lock();
|
||||
}
|
||||
|
||||
class _LockableBytesBuilderImpl implements LockableBytesBuilder {
|
||||
final BytesBuilder _buf = BytesBuilder(copy: false);
|
||||
bool _closed = false;
|
||||
|
||||
StateError _deny() =>
|
||||
StateError('Cannot modified a closed response\'s buffer.');
|
||||
|
||||
@override
|
||||
void lock() {
|
||||
_closed = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void add(List<int> bytes) {
|
||||
if (_closed) {
|
||||
throw _deny();
|
||||
} else {
|
||||
_buf.add(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void addByte(int byte) {
|
||||
if (_closed) {
|
||||
throw _deny();
|
||||
} else {
|
||||
_buf.addByte(byte);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void clear() {
|
||||
_buf.clear();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isEmpty => _buf.isEmpty;
|
||||
|
||||
@override
|
||||
bool get isNotEmpty => _buf.isNotEmpty;
|
||||
|
||||
@override
|
||||
int get length => _buf.length;
|
||||
|
||||
@override
|
||||
Uint8List takeBytes() {
|
||||
return _buf.takeBytes();
|
||||
}
|
||||
|
||||
@override
|
||||
Uint8List toBytes() {
|
||||
return _buf.toBytes();
|
||||
}
|
||||
}
|
140
packages/framework/lib/src/core/routable.dart
Normal file
140
packages/framework/lib/src/core/routable.dart
Normal file
|
@ -0,0 +1,140 @@
|
|||
library angel_framework.http.routable;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:platform_container/platform_container.dart';
|
||||
import 'package:platform_route/platform_route.dart';
|
||||
|
||||
import '../util.dart';
|
||||
import 'hooked_service.dart';
|
||||
import 'metadata.dart';
|
||||
import 'request_context.dart';
|
||||
import 'response_context.dart';
|
||||
import 'service.dart';
|
||||
|
||||
final RegExp _straySlashes = RegExp(r'(^/+)|(/+$)');
|
||||
|
||||
/// A function that receives an incoming [RequestContext] and responds to it.
|
||||
typedef RequestHandler = FutureOr<dynamic> Function(
|
||||
RequestContext<dynamic> req, ResponseContext<dynamic> res);
|
||||
|
||||
/// Sequentially runs a list of [handlers] of middleware, and returns early if any does not
|
||||
/// return `true`. Works well with [Router].chain.
|
||||
RequestHandler chain(Iterable<RequestHandler> handlers) {
|
||||
return (req, res) {
|
||||
Future Function()? runPipeline;
|
||||
|
||||
for (var handler in handlers) {
|
||||
//if (handler == null) break;
|
||||
|
||||
if (runPipeline == null) {
|
||||
runPipeline = () => Future.sync(() => handler(req, res));
|
||||
} else {
|
||||
var current = runPipeline;
|
||||
runPipeline = () => current().then((result) => !res.isOpen
|
||||
? Future.value(result)
|
||||
: req.app!.executeHandler(handler, req, res));
|
||||
}
|
||||
}
|
||||
|
||||
runPipeline ??= () => Future.value();
|
||||
return runPipeline();
|
||||
};
|
||||
}
|
||||
|
||||
/// A routable server that can handle dynamic requests.
|
||||
class Routable extends Router<RequestHandler> {
|
||||
final Map<Pattern, Service> _services = {};
|
||||
final Map<Pattern, Service?> _serviceLookups = {};
|
||||
|
||||
/// A [Map] of application-specific data that can be accessed.
|
||||
///
|
||||
/// Packages like `package:angel3_configuration` populate this map
|
||||
/// for you.
|
||||
final Map configuration = {};
|
||||
|
||||
final Container _container;
|
||||
|
||||
Routable([Reflector? reflector])
|
||||
// : _container = reflector == null ? null : Container(reflector),
|
||||
: _container = Container(reflector ?? ThrowingReflector()),
|
||||
super();
|
||||
|
||||
/// A [Container] used to inject dependencies.
|
||||
Container get container => _container;
|
||||
|
||||
void close() {
|
||||
_services.clear();
|
||||
configuration.clear();
|
||||
_onService.close();
|
||||
}
|
||||
|
||||
/// A set of [Service] objects that have been mapped into routes.
|
||||
Map<Pattern, Service> get services => _services;
|
||||
|
||||
final StreamController<Service> _onService =
|
||||
StreamController<Service>.broadcast();
|
||||
|
||||
/// Fired whenever a service is added to this instance.
|
||||
///
|
||||
/// **NOTE**: This is a broadcast stream.
|
||||
Stream<Service> get onService => _onService.stream;
|
||||
|
||||
/// Retrieves the service assigned to the given path.
|
||||
T? findService<T extends Service>(Pattern path) {
|
||||
return _serviceLookups.putIfAbsent(path, () {
|
||||
return _services[path] ??
|
||||
_services[path.toString().replaceAll(_straySlashes, '')];
|
||||
}) as T?;
|
||||
}
|
||||
|
||||
/// Shorthand for finding a [Service] in a statically-typed manner.
|
||||
Service<Id, Data>? findServiceOf<Id, Data>(Pattern path) {
|
||||
return findService<Service<Id, Data>>(path);
|
||||
}
|
||||
|
||||
/// Shorthand for finding a [HookedService] in a statically-typed manner.
|
||||
HookedService<dynamic, dynamic, T>? findHookedService<T extends Service>(
|
||||
Pattern path) {
|
||||
return findService(path) as HookedService<dynamic, dynamic, T>?;
|
||||
}
|
||||
|
||||
@override
|
||||
Route<RequestHandler> addRoute(
|
||||
String method, String path, RequestHandler handler,
|
||||
{Iterable<RequestHandler> middleware = const {}}) {
|
||||
final handlers = <RequestHandler>[];
|
||||
// Merge @Middleware declaration, if any
|
||||
var reflector = _container.reflector;
|
||||
if (reflector is! ThrowingReflector) {
|
||||
var middlewareDeclaration =
|
||||
getAnnotation<Middleware>(handler, _container.reflector);
|
||||
if (middlewareDeclaration != null) {
|
||||
handlers.addAll(middlewareDeclaration.handlers);
|
||||
}
|
||||
}
|
||||
|
||||
final handlerSequence = <RequestHandler>[];
|
||||
handlerSequence.addAll(middleware);
|
||||
handlerSequence.addAll(handlers);
|
||||
|
||||
return super.addRoute(method, path.toString(), handler,
|
||||
middleware: handlerSequence);
|
||||
}
|
||||
|
||||
/// Mounts a [service] at the given [path].
|
||||
///
|
||||
/// Returns a [HookedService] that can be used to hook into
|
||||
/// events dispatched by this service.
|
||||
HookedService<Id, Data, T> use<Id, Data, T extends Service<Id, Data>>(
|
||||
String path, T service) {
|
||||
var hooked = HookedService<Id, Data, T>(service);
|
||||
_services[path.toString().trim().replaceAll(RegExp(r'(^/+)|(/+$)'), '')] =
|
||||
hooked;
|
||||
hooked.addRoutes();
|
||||
mount(path.toString(), hooked);
|
||||
service.onHooked(hooked);
|
||||
_onService.add(hooked);
|
||||
return hooked;
|
||||
}
|
||||
}
|
423
packages/framework/lib/src/core/server.dart
Normal file
423
packages/framework/lib/src/core/server.dart
Normal file
|
@ -0,0 +1,423 @@
|
|||
library platform_framework.http.server;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:collection' show HashMap;
|
||||
import 'dart:convert';
|
||||
import 'package:platform_container/platform_container.dart';
|
||||
import 'package:platform_exceptions/http_exception.dart';
|
||||
import 'package:platform_route/platform_route.dart';
|
||||
import 'package:belatuk_combinator/belatuk_combinator.dart';
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
import 'controller.dart';
|
||||
import 'env.dart';
|
||||
import 'hooked_service.dart';
|
||||
import 'request_context.dart';
|
||||
import 'response_context.dart';
|
||||
import 'routable.dart';
|
||||
import 'service.dart';
|
||||
|
||||
//final RegExp _straySlashes = RegExp(r'(^/+)|(/+$)');
|
||||
|
||||
/// A function that configures an [Angel] server.
|
||||
typedef AngelConfigurer = FutureOr<void> Function(Angel app);
|
||||
|
||||
/// A function that asynchronously generates a view from the given path and data.
|
||||
typedef ViewGenerator = FutureOr<String> Function(String path,
|
||||
[Map<String, dynamic>? data]);
|
||||
|
||||
/// A function that handles error
|
||||
typedef AngelErrorHandler = dynamic Function(
|
||||
HttpException e, RequestContext req, ResponseContext res);
|
||||
|
||||
/// The default error handler for [Angel] server
|
||||
Future<bool> _defaultErrorHandler(
|
||||
HttpException e, RequestContext req, ResponseContext res) async {
|
||||
if (!req.accepts('text/html', strict: true) &&
|
||||
(req.accepts('application/json') ||
|
||||
req.accepts('application/javascript'))) {
|
||||
await res.json(e.toJson());
|
||||
return Future.value(false);
|
||||
} else {
|
||||
res.contentType = MediaType('text', 'html', {'charset': 'utf8'});
|
||||
res.statusCode = e.statusCode;
|
||||
res.write('<!DOCTYPE html><html><head><title>${e.message}</title>');
|
||||
res.write('</head><body><h1>${e.message}</h1><ul>');
|
||||
|
||||
for (var error in e.errors) {
|
||||
res.write('<li>$error</li>');
|
||||
}
|
||||
|
||||
res.write('</ul></body></html>');
|
||||
return Future.value(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Default ROOT level logger
|
||||
Logger _defaultLogger() {
|
||||
Logger logger = Logger('ROOT')
|
||||
..onRecord.listen((rec) {
|
||||
if (rec.error == null) {
|
||||
print(rec.message);
|
||||
}
|
||||
|
||||
if (rec.error != null) {
|
||||
var err = rec.error;
|
||||
if (err is HttpException && err.statusCode != 500) return;
|
||||
print('${rec.message} \n');
|
||||
print(rec.error);
|
||||
if (rec.stackTrace != null) {
|
||||
print(rec.stackTrace);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return logger;
|
||||
}
|
||||
|
||||
/// A powerful real-time/REST/MVC server class.
|
||||
class Angel extends Routable {
|
||||
static Future<String> _noViewEngineConfigured(String view, [Map? data]) =>
|
||||
Future.value('No view engine has been configured yet.');
|
||||
|
||||
final List<Angel> _children = [];
|
||||
final Map<
|
||||
String,
|
||||
Tuple4<List, Map<String, dynamic>, ParseResult<RouteResult>,
|
||||
MiddlewarePipeline>> handlerCache = HashMap();
|
||||
|
||||
Router<RequestHandler>? _flattened;
|
||||
Angel? _parent;
|
||||
|
||||
/// A global Map of converters that can transform responses bodies.
|
||||
final Map<String, Converter<List<int>, List<int>>> encoders = {};
|
||||
|
||||
final Map<dynamic, InjectionRequest> _preContained = {};
|
||||
|
||||
/// A [MimeTypeResolver] that can be used to specify the MIME types of files not known by `package:mime`.
|
||||
final MimeTypeResolver mimeTypeResolver = MimeTypeResolver();
|
||||
|
||||
/// A middleware to inject a serialize on every request.
|
||||
FutureOr<String> Function(dynamic)? serializer;
|
||||
|
||||
/// A [Map] of dependency data obtained via reflection.
|
||||
///
|
||||
/// You may modify this [Map] yourself if you intend to avoid reflection entirely.
|
||||
Map<dynamic, InjectionRequest> get preContained => _preContained;
|
||||
|
||||
/// Returns the [flatten]ed version of this router in production.
|
||||
Router<RequestHandler> get optimizedRouter => _flattened ?? this;
|
||||
|
||||
/// Determines whether to allow HTTP request method overrides.
|
||||
bool allowMethodOverrides = true;
|
||||
|
||||
/// All child application mounted on this instance.
|
||||
List<Angel> get children => List<Angel>.unmodifiable(_children);
|
||||
|
||||
final Map<Pattern, Controller> _controllers = {};
|
||||
|
||||
/// A set of [Controller] objects that have been loaded into the application.
|
||||
Map<Pattern, Controller> get controllers => _controllers;
|
||||
|
||||
/// The [AngelEnvironment] in which the application is running.
|
||||
///
|
||||
/// By default, it is automatically inferred.
|
||||
final AngelEnvironment environment;
|
||||
|
||||
/// Returns the parent instance of this application, if any.
|
||||
Angel? get parent => _parent;
|
||||
|
||||
/// Outputs diagnostics and debug messages.
|
||||
Logger _logger = _defaultLogger();
|
||||
|
||||
Logger get logger => _logger;
|
||||
|
||||
/// Assign a custom logger.
|
||||
/// Passing null will reset to default logger
|
||||
set logger(Logger? log) {
|
||||
_logger.clearListeners();
|
||||
|
||||
_logger = log ?? _defaultLogger();
|
||||
}
|
||||
|
||||
/// Plug-ins to be called right before server startup.
|
||||
///
|
||||
/// If the server is never started, they will never be called.
|
||||
final List<AngelConfigurer> startupHooks = [];
|
||||
|
||||
/// Plug-ins to be called right before server shutdown.
|
||||
///
|
||||
/// If the server is never [close]d, they will never be called.
|
||||
final List<AngelConfigurer> shutdownHooks = [];
|
||||
|
||||
/// Always run before responses are sent.
|
||||
///
|
||||
/// These will only not run if a response's `willCloseItself` is set to `true`.
|
||||
final List<RequestHandler> responseFinalizers = [];
|
||||
|
||||
/// A function that renders views.
|
||||
///
|
||||
/// Called by [ResponseContext]@`render`.
|
||||
ViewGenerator? viewGenerator = _noViewEngineConfigured;
|
||||
|
||||
/// The handler currently configured to run on [HttpException]s.
|
||||
AngelErrorHandler errorHandler = _defaultErrorHandler;
|
||||
|
||||
@override
|
||||
Route<RequestHandler> addRoute(
|
||||
String method, String path, RequestHandler handler,
|
||||
{Iterable<RequestHandler> middleware = const []}) {
|
||||
if (_flattened != null) {
|
||||
logger.warning(
|
||||
'WARNING: You added a route ($method $path) to the router, after it had been optimized.');
|
||||
logger.warning(
|
||||
'This route will be ignored, and no requests will ever reach it.');
|
||||
}
|
||||
|
||||
return super.addRoute(method, path, handler, middleware: middleware);
|
||||
}
|
||||
|
||||
@override
|
||||
SymlinkRoute<RequestHandler> mount(
|
||||
String path, Router<RequestHandler> router) {
|
||||
if (_flattened != null) {
|
||||
logger.warning(
|
||||
'WARNING: You added mounted a child router ($path) on the router, after it had been optimized.');
|
||||
logger.warning(
|
||||
'This route will be ignored, and no requests will ever reach it.');
|
||||
}
|
||||
|
||||
if (router is Angel) {
|
||||
router._parent = this;
|
||||
_children.add(router);
|
||||
}
|
||||
|
||||
return super.mount(path.toString(), router);
|
||||
}
|
||||
|
||||
/// Loads some base dependencies into the service container.
|
||||
void bootstrapContainer() {
|
||||
if (runtimeType != Angel) {
|
||||
container.registerSingleton(this);
|
||||
}
|
||||
|
||||
container.registerSingleton<Angel>(this);
|
||||
container.registerSingleton<Routable>(this);
|
||||
container.registerSingleton<Router>(this);
|
||||
}
|
||||
|
||||
/// Shuts down the server, and closes any open [StreamController]s.
|
||||
///
|
||||
/// The server will be **COMPLETELY DEFUNCT** after this operation!
|
||||
@override
|
||||
Future<void> close() {
|
||||
Future.forEach(services.values, (Service service) {
|
||||
service.close();
|
||||
});
|
||||
|
||||
super.close();
|
||||
viewGenerator = _noViewEngineConfigured;
|
||||
_preContained.clear();
|
||||
handlerCache.clear();
|
||||
encoders.clear();
|
||||
_children.clear();
|
||||
//_parent = null;
|
||||
//logger = null;
|
||||
//_flattened = null;
|
||||
startupHooks.clear();
|
||||
shutdownHooks.clear();
|
||||
responseFinalizers.clear();
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
@override
|
||||
void dumpTree(
|
||||
{Function(String tree)? callback,
|
||||
String header = 'Dumping route tree:',
|
||||
String tab = ' ',
|
||||
bool showMatchers = false}) {
|
||||
if (environment.isProduction) {
|
||||
_flattened ??= flatten(this);
|
||||
|
||||
_flattened!.dumpTree(
|
||||
callback: callback,
|
||||
header: header.isNotEmpty == true
|
||||
? header
|
||||
: (environment.isProduction
|
||||
? 'Dumping flattened route tree:'
|
||||
: 'Dumping route tree:'),
|
||||
tab: tab);
|
||||
} else {
|
||||
super.dumpTree(
|
||||
callback: callback,
|
||||
header: header.isNotEmpty == true
|
||||
? header
|
||||
: (environment.isProduction
|
||||
? 'Dumping flattened route tree:'
|
||||
: 'Dumping route tree:'),
|
||||
tab: tab);
|
||||
}
|
||||
}
|
||||
|
||||
Future getHandlerResult(handler, RequestContext req, ResponseContext res) {
|
||||
if (handler is RequestHandler) {
|
||||
var result = handler(req, res);
|
||||
return getHandlerResult(result, req, res);
|
||||
}
|
||||
|
||||
if (handler is Future) {
|
||||
return handler.then((result) => getHandlerResult(result, req, res));
|
||||
}
|
||||
|
||||
if (handler is Function) {
|
||||
var result = runContained(handler, req, res);
|
||||
return getHandlerResult(result, req, res);
|
||||
}
|
||||
|
||||
if (handler is Stream) {
|
||||
return getHandlerResult(handler.toList(), req, res);
|
||||
}
|
||||
|
||||
return Future.value(handler);
|
||||
}
|
||||
|
||||
/// Runs some [handler]. Returns `true` if request execution should continue.
|
||||
Future<bool> executeHandler(
|
||||
handler, RequestContext req, ResponseContext res) {
|
||||
return getHandlerResult(handler, req, res).then((result) {
|
||||
if (result == null) {
|
||||
return false;
|
||||
} else if (result is bool) {
|
||||
return result;
|
||||
} else if (result != null) {
|
||||
return res.serialize(result);
|
||||
} else {
|
||||
return res.isOpen;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Attempts to find a property by the given name within this application.
|
||||
dynamic findProperty(key) {
|
||||
if (configuration.containsKey(key)) return configuration[key];
|
||||
|
||||
//return parent != null ? parent?.findProperty(key) : null;
|
||||
if (parent != null) {
|
||||
return parent?.findProperty(key);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Runs several optimizations, *if* [angelEnv.isProduction] is `true`.
|
||||
///
|
||||
/// * Preprocesses all dependency injection, and eliminates the burden of reflecting handlers
|
||||
/// at run-time.
|
||||
/// * [flatten]s the route tree into a linear one.
|
||||
///
|
||||
/// You may [force] the optimization to run, if you are not running in production.
|
||||
void optimizeForProduction({bool force = false}) {
|
||||
if (environment.isProduction || force == true) {
|
||||
_flattened ??= flatten(this);
|
||||
logger.info('Angel is running in production mode.');
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a function after injecting from service container.
|
||||
/// If this function has been reflected before, then
|
||||
/// the execution will be faster, as the injection requirements were stored beforehand.
|
||||
Future runContained(Function handler, RequestContext req, ResponseContext res,
|
||||
[Container? container]) {
|
||||
container ??= Container(EmptyReflector());
|
||||
return Future.sync(() {
|
||||
if (_preContained.containsKey(handler)) {
|
||||
return handleContained(handler, _preContained[handler]!, container)(
|
||||
req, res);
|
||||
}
|
||||
|
||||
return runReflected(handler, req, res, container);
|
||||
});
|
||||
}
|
||||
|
||||
/// Runs with DI, and *always* reflects. Prefer [runContained].
|
||||
Future runReflected(Function handler, RequestContext req, ResponseContext res,
|
||||
[Container? container]) {
|
||||
container ??=
|
||||
req.container ?? res.app?.container ?? Container(EmptyReflector());
|
||||
|
||||
if (container.reflector is EmptyReflector) {
|
||||
throw ArgumentError("No `reflector` passed");
|
||||
}
|
||||
var h = handleContained(
|
||||
handler,
|
||||
_preContained[handler] = preInject(handler, container.reflector),
|
||||
container);
|
||||
return Future.sync(() => h(req, res));
|
||||
// return closureMirror.apply(args).reflectee;
|
||||
}
|
||||
|
||||
/// Applies an [AngelConfigurer] to this instance.
|
||||
Future configure(AngelConfigurer configurer) {
|
||||
return Future.sync(() => configurer(this));
|
||||
}
|
||||
|
||||
/// Shorthand for using the [container] to instantiate, and then mount a [Controller].
|
||||
/// Returns the created controller.
|
||||
///
|
||||
/// Just like [Container].make, in contexts without properly-reified generics (dev releases of Dart 2),
|
||||
/// provide a [type] argument as well.
|
||||
///
|
||||
/// If you are on `Dart >=2.0.0`, simply call `mountController<T>()`.
|
||||
Future<T> mountController<T extends Controller>([Type? type]) {
|
||||
var controller = container.make<T>(type);
|
||||
return configure(controller.configureServer).then((_) => controller);
|
||||
}
|
||||
|
||||
/// Shorthand for calling `all('*', handler)`.
|
||||
Route<RequestHandler?> fallback(RequestHandler handler) {
|
||||
return all('*', handler);
|
||||
}
|
||||
|
||||
@override
|
||||
HookedService<Id, Data, T> use<Id, Data, T extends Service<Id, Data>>(
|
||||
String path, T service) {
|
||||
service.app = this;
|
||||
return super.use(path, service)..app = this;
|
||||
}
|
||||
|
||||
static const String _reflectionErrorMessage =
|
||||
'${ContainerConst.defaultErrorMessage} $_reflectionInfo';
|
||||
|
||||
static const String _reflectionInfo =
|
||||
'Features like controllers, constructor dependency injection, and `ioc` require reflection, '
|
||||
'and will not work without it.\n\n'
|
||||
'For more, see the documentation:\n'
|
||||
'https://docs.angel-dart.dev/guides/dependency-injection#enabling-dart-mirrors-or-other-reflection';
|
||||
|
||||
Angel(
|
||||
{Reflector reflector =
|
||||
const ThrowingReflector(errorMessage: _reflectionErrorMessage),
|
||||
this.environment = angelEnv,
|
||||
Logger? logger,
|
||||
this.allowMethodOverrides = true,
|
||||
this.serializer,
|
||||
this.viewGenerator})
|
||||
: super(reflector) {
|
||||
// Override default logger
|
||||
if (logger != null) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
if (reflector is EmptyReflector || reflector is ThrowingReflector) {
|
||||
var msg =
|
||||
'No `reflector` was passed to the Angel constructor, so reflection will not be available.\n$_reflectionInfo';
|
||||
this.logger.warning(msg);
|
||||
}
|
||||
|
||||
bootstrapContainer();
|
||||
viewGenerator ??= _noViewEngineConfigured;
|
||||
serializer ??= json.encode;
|
||||
}
|
||||
}
|
380
packages/framework/lib/src/core/service.dart
Normal file
380
packages/framework/lib/src/core/service.dart
Normal file
|
@ -0,0 +1,380 @@
|
|||
library platform_framework.http.service;
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:platform_exceptions/http_exception.dart';
|
||||
import 'package:belatuk_merge_map/belatuk_merge_map.dart';
|
||||
import 'package:quiver/core.dart';
|
||||
import '../util.dart';
|
||||
import 'anonymous_service.dart';
|
||||
import 'hooked_service.dart' show HookedService;
|
||||
import 'metadata.dart';
|
||||
import 'request_context.dart';
|
||||
import 'response_context.dart';
|
||||
import 'routable.dart';
|
||||
import 'server.dart';
|
||||
|
||||
/// Indicates how the service was accessed.
|
||||
///
|
||||
/// This will be passed to the `params` object in a service method.
|
||||
/// When requested on the server side, this will be null.
|
||||
class Providers {
|
||||
/// The transport through which the client is accessing this service.
|
||||
final String via;
|
||||
|
||||
const Providers(this.via);
|
||||
|
||||
static const String viaRest = 'rest';
|
||||
static const String viaWebsocket = 'websocket';
|
||||
static const String viaGraphQL = 'graphql';
|
||||
|
||||
/// Represents a request via REST.
|
||||
static const Providers rest = Providers(viaRest);
|
||||
|
||||
/// Represents a request over WebSockets.
|
||||
static const Providers websocket = Providers(viaWebsocket);
|
||||
|
||||
/// Represents a request parsed from GraphQL.
|
||||
static const Providers graphQL = Providers(viaGraphQL);
|
||||
|
||||
@override
|
||||
int get hashCode => hashObjects([via]);
|
||||
|
||||
@override
|
||||
bool operator ==(other) => other is Providers && other.via == via;
|
||||
|
||||
Map<String, String> toJson() {
|
||||
return {'via': via};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'via:$via';
|
||||
}
|
||||
}
|
||||
|
||||
/// A front-facing interface that can present data to and operate on data on behalf of the user.
|
||||
///
|
||||
/// Heavily inspired by FeathersJS. <3
|
||||
class Service<Id, Data> extends Routable {
|
||||
/// A [List] of keys that services should ignore, should they see them in the query.
|
||||
static const List<String> specialQueryKeys = <String>[
|
||||
r'$limit',
|
||||
r'$sort',
|
||||
'page',
|
||||
'token'
|
||||
];
|
||||
|
||||
/// Handlers that must run to ensure this service's functionality.
|
||||
List<RequestHandler> get bootstrappers => [];
|
||||
|
||||
/// The [Angel] app powering this service.
|
||||
Angel? _app;
|
||||
|
||||
Angel get app {
|
||||
if (_app == null) {
|
||||
throw ArgumentError("Angel is not initialized");
|
||||
}
|
||||
return _app!;
|
||||
}
|
||||
|
||||
set app(Angel angel) {
|
||||
_app = angel;
|
||||
}
|
||||
|
||||
bool get isAppActive => _app != null;
|
||||
|
||||
/// Closes this service, including any database connections or stream controllers.
|
||||
@override
|
||||
void close() {}
|
||||
|
||||
/// An optional [readData] function can be passed to handle non-map/non-json bodies.
|
||||
Service(
|
||||
{FutureOr<Data> Function(RequestContext, ResponseContext)? readData}) {
|
||||
_readData = readData;
|
||||
|
||||
_readData ??= (req, res) {
|
||||
if (req.bodyAsObject is! Data) {
|
||||
throw HttpException.badRequest(
|
||||
message:
|
||||
'Invalid request body. Expected $Data; found ${req.bodyAsObject} instead.');
|
||||
} else {
|
||||
return req.bodyAsObject as Data;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
FutureOr<Data> Function(RequestContext, ResponseContext)? _readData;
|
||||
|
||||
/// A [Function] that reads the request body and converts it into [Data].
|
||||
FutureOr<Data> Function(RequestContext, ResponseContext)? get readData =>
|
||||
_readData;
|
||||
|
||||
/// Retrieves the first object from the result of calling [index] with the given [params].
|
||||
///
|
||||
/// If the result of [index] is `null`, OR an empty [Iterable], a 404 `HttpException` will be thrown.
|
||||
///
|
||||
/// If the result is both non-null and NOT an [Iterable], it will be returned as-is.
|
||||
///
|
||||
/// If the result is a non-empty [Iterable], [findOne] will return `it.first`, where `it` is the aforementioned [Iterable].
|
||||
///
|
||||
/// A custom [errorMessage] may be provided.
|
||||
Future<Data> findOne(
|
||||
[Map<String, dynamic>? params,
|
||||
String errorMessage = 'No record was found matching the given query.']) {
|
||||
return index(params).then((result) {
|
||||
if (result.isEmpty) {
|
||||
throw HttpException.notFound(message: errorMessage);
|
||||
} else {
|
||||
return result.first;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Retrieves all resources.
|
||||
Future<List<Data>> index([Map<String, dynamic>? params]) {
|
||||
throw HttpException.methodNotAllowed();
|
||||
}
|
||||
|
||||
/// Retrieves the desired resource.
|
||||
Future<Data> read(Id id, [Map<String, dynamic>? params]) {
|
||||
throw HttpException.methodNotAllowed();
|
||||
}
|
||||
|
||||
/// Reads multiple resources at once.
|
||||
///
|
||||
/// Service implementations should override this to ensure data is fetched within a
|
||||
/// single round trip.
|
||||
Future<List<Data>> readMany(List<Id> ids, [Map<String, dynamic>? params]) {
|
||||
return Future.wait(ids.map((id) => read(id, params)));
|
||||
}
|
||||
|
||||
/// Creates a resource.
|
||||
Future<Data> create(Data data, [Map<String, dynamic>? params]) {
|
||||
throw HttpException.methodNotAllowed();
|
||||
}
|
||||
|
||||
/// Modifies a resource.
|
||||
Future<Data> modify(Id id, Data data, [Map<String, dynamic>? params]) {
|
||||
throw HttpException.methodNotAllowed();
|
||||
}
|
||||
|
||||
/// Overwrites a resource.
|
||||
Future<Data> update(Id id, Data data, [Map<String, dynamic>? params]) {
|
||||
throw HttpException.methodNotAllowed();
|
||||
}
|
||||
|
||||
/// Removes the given resource.
|
||||
Future<Data> remove(Id id, [Map<String, dynamic>? params]) {
|
||||
throw HttpException.methodNotAllowed();
|
||||
}
|
||||
|
||||
/// Creates an [AnonymousService] that wraps over this one, and maps input and output
|
||||
/// using two converter functions.
|
||||
///
|
||||
/// Handy utility for handling data in a type-safe manner.
|
||||
Service<Id, U> map<U>(U Function(Data) encoder, Data Function(U) decoder,
|
||||
{FutureOr<U> Function(RequestContext, ResponseContext)? readData}) {
|
||||
readData ??= (req, res) async {
|
||||
var inner = await this.readData!(req, res)!;
|
||||
return encoder(inner);
|
||||
};
|
||||
|
||||
return AnonymousService<Id, U>(
|
||||
readData: readData,
|
||||
index: ([params]) {
|
||||
return index(params).then((it) => it.map(encoder).toList());
|
||||
},
|
||||
read: (id, [params]) {
|
||||
return read(id, params).then(encoder);
|
||||
},
|
||||
create: (data, [params]) {
|
||||
return create(decoder(data), params).then(encoder);
|
||||
},
|
||||
modify: (id, data, [params]) {
|
||||
return modify(id, decoder(data), params).then(encoder);
|
||||
},
|
||||
update: (id, data, [params]) {
|
||||
return update(id, decoder(data), params).then(encoder);
|
||||
},
|
||||
remove: (id, [params]) {
|
||||
return remove(id, params).then(encoder);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Transforms an [id] (whether it is a String, num, etc.) into one acceptable by a service.
|
||||
///
|
||||
/// The single type argument, [T], is used to determine how to parse the [id].
|
||||
///
|
||||
/// For example, `parseId<bool>` attempts to parse the value as a [bool].
|
||||
static T parseId<T>(id) {
|
||||
if (id == null || id == 'null') {
|
||||
return 'null' as T;
|
||||
//throw ArgumentError("[Service] Null is not supported");
|
||||
} else if (T == String) {
|
||||
return id.toString() as T;
|
||||
} else if (T == int) {
|
||||
return int.parse(id.toString()) as T;
|
||||
} else if (T == bool) {
|
||||
return (id == true || id.toString() == 'true') as T;
|
||||
} else if (T == double) {
|
||||
return double.parse(id.toString()) as T;
|
||||
} else if (T == num) {
|
||||
return num.parse(id.toString()) as T;
|
||||
} else {
|
||||
return id as T;
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates RESTful routes pointing to this class's methods.
|
||||
void addRoutes([Service? service]) {
|
||||
_addRoutesInner(service ?? this, bootstrappers);
|
||||
}
|
||||
|
||||
void _addRoutesInner(Service service, Iterable<RequestHandler> handlerss) {
|
||||
var restProvider = {'provider': Providers.rest};
|
||||
var handlers = List<RequestHandler>.from(handlerss);
|
||||
|
||||
// Add global middleware if declared on the instance itself
|
||||
var before = getAnnotation<Middleware>(service, app.container.reflector);
|
||||
|
||||
if (before != null) handlers.addAll(before.handlers);
|
||||
|
||||
var indexMiddleware =
|
||||
getAnnotation<Middleware>(service.index, app.container.reflector);
|
||||
get('/', (req, res) {
|
||||
return index(mergeMap([
|
||||
{'query': req.queryParameters},
|
||||
restProvider,
|
||||
req.serviceParams
|
||||
]));
|
||||
}, middleware: [
|
||||
...handlers,
|
||||
...(indexMiddleware == null) ? [] : indexMiddleware.handlers.toList()
|
||||
]);
|
||||
|
||||
var createMiddleware =
|
||||
getAnnotation<Middleware>(service.create, app.container.reflector);
|
||||
post('/', (req, ResponseContext res) {
|
||||
return req.parseBody().then((_) async {
|
||||
return await create(
|
||||
(await readData!(req, res))!,
|
||||
mergeMap([
|
||||
{'query': req.queryParameters},
|
||||
restProvider,
|
||||
req.serviceParams
|
||||
])).then((r) {
|
||||
res.statusCode = 201;
|
||||
return r;
|
||||
});
|
||||
});
|
||||
}, middleware: [
|
||||
...handlers,
|
||||
...(createMiddleware == null) ? [] : createMiddleware.handlers.toList()
|
||||
]);
|
||||
|
||||
var readMiddleware =
|
||||
getAnnotation<Middleware>(service.read, app.container.reflector);
|
||||
|
||||
get('/:id', (req, res) {
|
||||
return read(
|
||||
parseId<Id>(req.params['id']),
|
||||
mergeMap([
|
||||
{'query': req.queryParameters},
|
||||
restProvider,
|
||||
req.serviceParams
|
||||
]));
|
||||
}, middleware: [
|
||||
...handlers,
|
||||
...(readMiddleware == null) ? [] : readMiddleware.handlers.toList()
|
||||
]);
|
||||
|
||||
var modifyMiddleware =
|
||||
getAnnotation<Middleware>(service.modify, app.container.reflector);
|
||||
|
||||
patch('/:id', (req, res) {
|
||||
return req.parseBody().then((_) async {
|
||||
return await modify(
|
||||
parseId<Id>(req.params['id']),
|
||||
(await readData!(req, res))!,
|
||||
mergeMap([
|
||||
{'query': req.queryParameters},
|
||||
restProvider,
|
||||
req.serviceParams
|
||||
]));
|
||||
});
|
||||
}, middleware: [
|
||||
...handlers,
|
||||
...(modifyMiddleware == null) ? [] : modifyMiddleware.handlers.toList()
|
||||
]);
|
||||
|
||||
var updateMiddleware =
|
||||
getAnnotation<Middleware>(service.update, app.container.reflector);
|
||||
post('/:id', (req, res) {
|
||||
return req.parseBody().then((_) async {
|
||||
return await update(
|
||||
parseId<Id>(req.params['id']),
|
||||
(await readData!(req, res))!,
|
||||
mergeMap([
|
||||
{'query': req.queryParameters},
|
||||
restProvider,
|
||||
req.serviceParams
|
||||
]));
|
||||
});
|
||||
}, middleware: [
|
||||
...handlers,
|
||||
...(updateMiddleware == null) ? [] : updateMiddleware.handlers.toList()
|
||||
]);
|
||||
|
||||
put('/:id', (req, res) {
|
||||
return req.parseBody().then((_) async {
|
||||
return await update(
|
||||
parseId<Id>(req.params['id']),
|
||||
(await readData!(req, res))!,
|
||||
mergeMap([
|
||||
{'query': req.queryParameters},
|
||||
restProvider,
|
||||
req.serviceParams
|
||||
]));
|
||||
});
|
||||
}, middleware: [
|
||||
...handlers,
|
||||
...(updateMiddleware == null) ? [] : updateMiddleware.handlers.toList()
|
||||
]);
|
||||
|
||||
var removeMiddleware =
|
||||
getAnnotation<Middleware>(service.remove, app.container.reflector);
|
||||
delete('/', (req, res) {
|
||||
return remove(
|
||||
'' as Id,
|
||||
mergeMap([
|
||||
{'query': req.queryParameters},
|
||||
restProvider,
|
||||
req.serviceParams
|
||||
]));
|
||||
}, middleware: [
|
||||
...handlers,
|
||||
...(removeMiddleware == null) ? [] : removeMiddleware.handlers.toList()
|
||||
]);
|
||||
|
||||
delete('/:id', (req, res) {
|
||||
return remove(
|
||||
parseId<Id>(req.params['id']),
|
||||
mergeMap([
|
||||
{'query': req.queryParameters},
|
||||
restProvider,
|
||||
req.serviceParams
|
||||
]));
|
||||
}, middleware: [
|
||||
...handlers,
|
||||
...(removeMiddleware == null) ? [] : removeMiddleware.handlers.toList()
|
||||
]);
|
||||
|
||||
// REST compliance
|
||||
put('/', (req, res) => throw HttpException.notFound());
|
||||
patch('/', (req, res) => throw HttpException.notFound());
|
||||
}
|
||||
|
||||
/// Invoked when this service is wrapped within a [HookedService].
|
||||
void onHooked(HookedService hookedService) {}
|
||||
}
|
10
packages/framework/lib/src/fast_name_from_symbol.dart
Normal file
10
packages/framework/lib/src/fast_name_from_symbol.dart
Normal file
|
@ -0,0 +1,10 @@
|
|||
final Map<Symbol, String> _cache = {};
|
||||
|
||||
String fastNameFromSymbol(Symbol s) {
|
||||
return _cache.putIfAbsent(s, () {
|
||||
var str = s.toString();
|
||||
var open = str.indexOf('"');
|
||||
var close = str.lastIndexOf('"');
|
||||
return str.substring(open + 1, close);
|
||||
});
|
||||
}
|
142
packages/framework/lib/src/http/angel_http.dart
Normal file
142
packages/framework/lib/src/http/angel_http.dart
Normal file
|
@ -0,0 +1,142 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io'
|
||||
show
|
||||
Cookie,
|
||||
HttpRequest,
|
||||
HttpResponse,
|
||||
HttpServer,
|
||||
Platform,
|
||||
SecurityContext;
|
||||
import 'package:platform_framework/platform_framework.dart';
|
||||
import '../core/core.dart';
|
||||
import 'http_request_context.dart';
|
||||
import 'http_response_context.dart';
|
||||
|
||||
final RegExp _straySlashes = RegExp(r'(^/+)|(/+$)');
|
||||
|
||||
typedef ServerGeneratorType = Future<HttpServer> Function(dynamic, int);
|
||||
|
||||
/// Adapts `dart:io`'s [HttpServer] to serve Angel.
|
||||
class AngelHttp extends Driver<HttpRequest, HttpResponse, HttpServer,
|
||||
HttpRequestContext, HttpResponseContext> {
|
||||
@override
|
||||
Uri get uri {
|
||||
return Uri(
|
||||
scheme: 'http', host: server?.address.address, port: server?.port);
|
||||
}
|
||||
|
||||
AngelHttp._(super.app, super.serverGenerator, bool useZone)
|
||||
: super(useZone: useZone);
|
||||
|
||||
factory AngelHttp(Angel app, {bool useZone = true}) {
|
||||
return AngelHttp._(app, HttpServer.bind, useZone);
|
||||
}
|
||||
|
||||
/// An instance mounted on a server started by the [serverGenerator].
|
||||
factory AngelHttp.custom(Angel app, ServerGeneratorType serverGenerator,
|
||||
{bool useZone = true, Map<String, String> headers = const {}}) {
|
||||
return AngelHttp._(app, serverGenerator, useZone);
|
||||
}
|
||||
|
||||
factory AngelHttp.fromSecurityContext(Angel app, SecurityContext context,
|
||||
{bool useZone = true}) {
|
||||
return AngelHttp._(app, (address, int port) {
|
||||
return HttpServer.bindSecure(address, port, context);
|
||||
}, useZone);
|
||||
}
|
||||
|
||||
/// Creates an HTTPS server.
|
||||
///
|
||||
/// Provide paths to a certificate chain and server key (both .pem).
|
||||
/// If no password is provided, a random one will be generated upon running
|
||||
/// the server.
|
||||
factory AngelHttp.secure(
|
||||
Angel app, String certificateChainPath, String serverKeyPath,
|
||||
{String? password, bool useZone = true}) {
|
||||
var certificateChain =
|
||||
Platform.script.resolve(certificateChainPath).toFilePath();
|
||||
var serverKey = Platform.script.resolve(serverKeyPath).toFilePath();
|
||||
var serverContext = SecurityContext();
|
||||
serverContext.useCertificateChain(certificateChain, password: password);
|
||||
serverContext.usePrivateKey(serverKey, password: password);
|
||||
|
||||
return AngelHttp.fromSecurityContext(app, serverContext, useZone: useZone);
|
||||
}
|
||||
|
||||
Future handleRequest(HttpRequest request) =>
|
||||
handleRawRequest(request, request.response);
|
||||
|
||||
@override
|
||||
void addCookies(HttpResponse response, Iterable<Cookie> cookies) =>
|
||||
response.cookies.addAll(cookies);
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
return await super.close();
|
||||
}
|
||||
|
||||
/// Remove headers from HTTP Response
|
||||
void removeResponseHeader(Map<String, Object> headers) {
|
||||
headers.forEach((key, value) {
|
||||
server?.defaultResponseHeaders.remove(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
/// Add headers to HTTP Response
|
||||
void addResponseHeader(Map<String, Object> headers) {
|
||||
headers.forEach((key, value) {
|
||||
server?.defaultResponseHeaders.add(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future closeResponse(HttpResponse response) => response.close();
|
||||
|
||||
@override
|
||||
Future<HttpRequestContext> createRequestContext(
|
||||
HttpRequest request, HttpResponse response) {
|
||||
var path = request.uri.path.replaceAll(_straySlashes, '');
|
||||
if (path.isEmpty) path = '/';
|
||||
return HttpRequestContext.from(request, app, path);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<HttpResponseContext> createResponseContext(
|
||||
HttpRequest request, HttpResponse response,
|
||||
[HttpRequestContext? correspondingRequest]) {
|
||||
var context = HttpResponseContext(response, app, correspondingRequest);
|
||||
context.serializer = (app.serializer ?? json.encode);
|
||||
context.encoders.addAll(app.encoders);
|
||||
return Future<HttpResponseContext>.value(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<HttpResponse> createResponseStreamFromRawRequest(
|
||||
HttpRequest request) =>
|
||||
Stream.fromIterable([request.response]);
|
||||
|
||||
@override
|
||||
void setChunkedEncoding(HttpResponse response, bool value) =>
|
||||
response.headers.chunkedTransferEncoding = value;
|
||||
|
||||
@override
|
||||
void setContentLength(HttpResponse response, int length) =>
|
||||
response.headers.contentLength = length;
|
||||
|
||||
@override
|
||||
void setHeader(HttpResponse response, String key, String value) =>
|
||||
response.headers.set(key, value);
|
||||
|
||||
@override
|
||||
void setStatusCode(HttpResponse response, int value) =>
|
||||
response.statusCode = value;
|
||||
|
||||
@override
|
||||
void writeStringToResponse(HttpResponse response, String value) =>
|
||||
response.write(value);
|
||||
|
||||
@override
|
||||
void writeToResponse(HttpResponse response, List<int> data) =>
|
||||
response.add(data);
|
||||
}
|
19
packages/framework/lib/src/http/http.dart
Normal file
19
packages/framework/lib/src/http/http.dart
Normal file
|
@ -0,0 +1,19 @@
|
|||
/// Various libraries useful for creating highly-extensible servers.
|
||||
library angel_framework.http;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
export 'angel_http.dart';
|
||||
export 'http_request_context.dart';
|
||||
export 'http_response_context.dart';
|
||||
|
||||
/// Boots a shared server instance. Use this if launching multiple isolates.
|
||||
Future<HttpServer> startShared(address, int port) =>
|
||||
HttpServer.bind(address ?? '127.0.0.1', port, shared: true);
|
||||
|
||||
Future<HttpServer> Function(dynamic, int) startSharedSecure(
|
||||
SecurityContext securityContext) {
|
||||
return (address, int port) => HttpServer.bindSecure(
|
||||
address ?? '127.0.0.1', port, securityContext,
|
||||
shared: true);
|
||||
}
|
110
packages/framework/lib/src/http/http_request_context.dart
Normal file
110
packages/framework/lib/src/http/http_request_context.dart
Normal file
|
@ -0,0 +1,110 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:platform_container/platform_container.dart';
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
|
||||
import '../core/core.dart';
|
||||
|
||||
/// An implementation of [RequestContext] that wraps a [HttpRequest].
|
||||
class HttpRequestContext extends RequestContext<HttpRequest?> {
|
||||
Container? _container;
|
||||
MediaType _contentType = MediaType('text', 'plain');
|
||||
HttpRequest? _io;
|
||||
String? _override;
|
||||
String _path = '';
|
||||
|
||||
@override
|
||||
Container? get container => _container;
|
||||
|
||||
@override
|
||||
MediaType get contentType {
|
||||
return _contentType;
|
||||
}
|
||||
|
||||
@override
|
||||
List<Cookie> get cookies {
|
||||
return rawRequest?.cookies ?? [];
|
||||
}
|
||||
|
||||
@override
|
||||
HttpHeaders? get headers {
|
||||
return rawRequest?.headers;
|
||||
}
|
||||
|
||||
@override
|
||||
String get hostname {
|
||||
return rawRequest?.headers.value('host') ?? 'localhost';
|
||||
}
|
||||
|
||||
/// The underlying [HttpRequest] instance underneath this context.
|
||||
@override
|
||||
HttpRequest? get rawRequest => _io;
|
||||
|
||||
@override
|
||||
Stream<List<int>>? get body => _io;
|
||||
|
||||
@override
|
||||
String get method {
|
||||
return _override ?? originalMethod;
|
||||
}
|
||||
|
||||
@override
|
||||
String get originalMethod {
|
||||
return rawRequest?.method ?? '';
|
||||
}
|
||||
|
||||
@override
|
||||
String get path {
|
||||
return _path;
|
||||
}
|
||||
|
||||
@override
|
||||
InternetAddress get remoteAddress {
|
||||
return rawRequest?.connectionInfo?.remoteAddress ??
|
||||
InternetAddress("127.0.0.1");
|
||||
}
|
||||
|
||||
@override
|
||||
HttpSession? get session {
|
||||
return rawRequest?.session;
|
||||
}
|
||||
|
||||
@override
|
||||
Uri get uri {
|
||||
return rawRequest?.uri ?? Uri();
|
||||
}
|
||||
|
||||
/// Magically transforms an [HttpRequest] into a [RequestContext].
|
||||
static Future<HttpRequestContext> from(
|
||||
HttpRequest request, Angel app, String path) {
|
||||
var ctx = HttpRequestContext().._container = app.container.createChild();
|
||||
|
||||
var override = request.method;
|
||||
|
||||
if (app.allowMethodOverrides == true) {
|
||||
override =
|
||||
request.headers.value('x-http-method-override')?.toUpperCase() ??
|
||||
request.method;
|
||||
}
|
||||
|
||||
ctx.app = app;
|
||||
ctx._contentType = request.headers.contentType == null
|
||||
? MediaType('text', 'plain')
|
||||
: MediaType.parse(request.headers.contentType.toString());
|
||||
ctx._override = override;
|
||||
ctx._path = path;
|
||||
ctx._io = request;
|
||||
|
||||
return Future.value(ctx);
|
||||
}
|
||||
|
||||
@override
|
||||
Future close() {
|
||||
//_contentType = null;
|
||||
_io = null;
|
||||
_override = null;
|
||||
//_path = null;
|
||||
return super.close();
|
||||
}
|
||||
}
|
220
packages/framework/lib/src/http/http_response_context.dart
Normal file
220
packages/framework/lib/src/http/http_response_context.dart
Normal file
|
@ -0,0 +1,220 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io' hide BytesBuilder;
|
||||
import 'dart:typed_data' show BytesBuilder;
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
|
||||
import '../core/core.dart';
|
||||
import 'http_request_context.dart';
|
||||
|
||||
/// An implementation of [ResponseContext] that abstracts over an [HttpResponse].
|
||||
class HttpResponseContext extends ResponseContext<HttpResponse> {
|
||||
/// The underlying [HttpResponse] under this instance.
|
||||
@override
|
||||
final HttpResponse rawResponse;
|
||||
|
||||
LockableBytesBuilder? _buffer;
|
||||
|
||||
final HttpRequestContext? _correspondingRequest;
|
||||
bool _isDetached = false, _isClosed = false, _streamInitialized = false;
|
||||
|
||||
HttpResponseContext(this.rawResponse, Angel? app,
|
||||
[this._correspondingRequest]) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
@override
|
||||
HttpResponse detach() {
|
||||
_isDetached = true;
|
||||
return rawResponse;
|
||||
}
|
||||
|
||||
@override
|
||||
RequestContext? get correspondingRequest {
|
||||
return _correspondingRequest;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isOpen {
|
||||
return !_isClosed && !_isDetached;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isBuffered => _buffer != null;
|
||||
|
||||
@override
|
||||
BytesBuilder? get buffer => _buffer;
|
||||
|
||||
@override
|
||||
void addError(Object error, [StackTrace? stackTrace]) {
|
||||
rawResponse.addError(error, stackTrace);
|
||||
super.addError(error, stackTrace);
|
||||
}
|
||||
|
||||
@override
|
||||
void useBuffer() {
|
||||
_buffer = LockableBytesBuilder();
|
||||
}
|
||||
|
||||
Iterable<String>? __allowedEncodings;
|
||||
|
||||
Iterable<String>? get _allowedEncodings {
|
||||
return __allowedEncodings ??= correspondingRequest?.headers
|
||||
?.value('accept-encoding')
|
||||
?.split(',')
|
||||
.map((s) => s.trim())
|
||||
.where((s) => s.isNotEmpty)
|
||||
.map((str) {
|
||||
// Ignore quality specifications in accept-encoding
|
||||
// ex. gzip;q=0.8
|
||||
if (!str.contains(';')) return str;
|
||||
return str.split(';')[0];
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
set contentType(MediaType value) {
|
||||
super.contentType = value;
|
||||
if (!_streamInitialized) {
|
||||
rawResponse.headers.contentType =
|
||||
ContentType(value.type, value.subtype, parameters: value.parameters);
|
||||
}
|
||||
}
|
||||
|
||||
bool _openStream() {
|
||||
if (!_streamInitialized) {
|
||||
// If this is the first stream added to this response,
|
||||
// then add headers, status code, etc.
|
||||
rawResponse
|
||||
..statusCode = statusCode
|
||||
..cookies.addAll(cookies);
|
||||
headers.forEach(rawResponse.headers.set);
|
||||
|
||||
rawResponse.headers.date = DateTime.now();
|
||||
|
||||
if (headers.containsKey('content-length')) {
|
||||
rawResponse.contentLength = int.tryParse(headers['content-length']!) ??
|
||||
rawResponse.contentLength;
|
||||
}
|
||||
|
||||
rawResponse.headers.contentType = ContentType(
|
||||
contentType.type, contentType.subtype,
|
||||
charset: contentType.parameters['charset'],
|
||||
parameters: contentType.parameters);
|
||||
|
||||
if (encoders.isNotEmpty && correspondingRequest != null) {
|
||||
if (_allowedEncodings != null) {
|
||||
for (var encodingName in _allowedEncodings!) {
|
||||
Converter<List<int>, List<int>>? encoder;
|
||||
var key = encodingName;
|
||||
|
||||
if (encoders.containsKey(encodingName)) {
|
||||
encoder = encoders[encodingName];
|
||||
} else if (encodingName == '*') {
|
||||
encoder = encoders[key = encoders.keys.first];
|
||||
}
|
||||
|
||||
if (encoder != null) {
|
||||
rawResponse.headers.set('content-encoding', key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//_isClosed = true;
|
||||
return _streamInitialized = true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future addStream(Stream<List<int>> stream) {
|
||||
if (_isClosed && isBuffered) throw ResponseContext.closed();
|
||||
_openStream();
|
||||
|
||||
var output = stream;
|
||||
|
||||
if (encoders.isNotEmpty && correspondingRequest != null) {
|
||||
if (_allowedEncodings != null) {
|
||||
for (var encodingName in _allowedEncodings!) {
|
||||
Converter<List<int>, List<int>>? encoder;
|
||||
var key = encodingName;
|
||||
|
||||
if (encoders.containsKey(encodingName)) {
|
||||
encoder = encoders[encodingName];
|
||||
} else if (encodingName == '*') {
|
||||
encoder = encoders[key = encoders.keys.first];
|
||||
}
|
||||
|
||||
if (encoder != null) {
|
||||
output = encoders[key]!.bind(output);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rawResponse.addStream(output);
|
||||
}
|
||||
|
||||
@override
|
||||
void add(List<int> data) {
|
||||
if (_isClosed && isBuffered) {
|
||||
throw ResponseContext.closed();
|
||||
} else if (!isBuffered) {
|
||||
if (!_isClosed) {
|
||||
_openStream();
|
||||
|
||||
if (encoders.isNotEmpty && correspondingRequest != null) {
|
||||
if (_allowedEncodings != null) {
|
||||
for (var encodingName in _allowedEncodings!) {
|
||||
Converter<List<int>, List<int>>? encoder;
|
||||
var key = encodingName;
|
||||
|
||||
if (encoders.containsKey(encodingName)) {
|
||||
encoder = encoders[encodingName];
|
||||
} else if (encodingName == '*') {
|
||||
encoder = encoders[key = encoders.keys.first];
|
||||
}
|
||||
|
||||
if (encoder != null) {
|
||||
data = encoders[key]!.convert(data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rawResponse.add(data);
|
||||
}
|
||||
} else {
|
||||
buffer!.add(data);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future close() {
|
||||
if (!_isDetached) {
|
||||
if (!_isClosed) {
|
||||
if (!isBuffered) {
|
||||
try {
|
||||
_openStream();
|
||||
rawResponse.close();
|
||||
} catch (_) {
|
||||
// This only seems to occur on `MockHttpRequest`, but
|
||||
// this try/catch prevents a crash.
|
||||
}
|
||||
} else {
|
||||
_buffer!.lock();
|
||||
}
|
||||
|
||||
_isClosed = true;
|
||||
}
|
||||
|
||||
super.close();
|
||||
}
|
||||
return Future.value();
|
||||
}
|
||||
}
|
243
packages/framework/lib/src/http2/angel_http2.dart
Normal file
243
packages/framework/lib/src/http2/angel_http2.dart
Normal file
|
@ -0,0 +1,243 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:platform_framework/platform_framework.dart' hide Header;
|
||||
import 'package:platform_framework/http.dart';
|
||||
import 'package:http2/transport.dart';
|
||||
import 'package:platform_mock_request/platform_mock_request.dart';
|
||||
import 'http2_request_context.dart';
|
||||
import 'http2_response_context.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
/// Boots a shared server instance. Use this if launching multiple isolates.
|
||||
Future<SecureServerSocket> startSharedHttp2(
|
||||
address, int port, SecurityContext ctx) {
|
||||
return SecureServerSocket.bind(address, port, ctx, shared: true);
|
||||
}
|
||||
|
||||
/// Adapts `package:http2`'s [ServerTransportConnection] to serve Angel.
|
||||
class AngelHttp2 extends Driver<Socket, ServerTransportStream,
|
||||
SecureServerSocket, Http2RequestContext, Http2ResponseContext> {
|
||||
final ServerSettings? settings;
|
||||
late AngelHttp _http;
|
||||
final StreamController<HttpRequest> _onHttp1 = StreamController();
|
||||
final Map<String, MockHttpSession> _sessions = {};
|
||||
final Uuid _uuid = Uuid();
|
||||
_AngelHttp2ServerSocket? _artificial;
|
||||
|
||||
SecureServerSocket? get socket => _artificial;
|
||||
|
||||
AngelHttp2._(
|
||||
Angel app,
|
||||
Future<SecureServerSocket> Function(dynamic, int) serverGenerator,
|
||||
bool useZone,
|
||||
bool allowHttp1,
|
||||
this.settings)
|
||||
: super(
|
||||
app,
|
||||
serverGenerator,
|
||||
useZone: useZone,
|
||||
) {
|
||||
if (allowHttp1) {
|
||||
_http = AngelHttp(app, useZone: useZone);
|
||||
onHttp1.listen(_http.handleRequest);
|
||||
}
|
||||
}
|
||||
|
||||
factory AngelHttp2(Angel app, SecurityContext securityContext,
|
||||
{bool useZone = true,
|
||||
bool allowHttp1 = false,
|
||||
ServerSettings? settings}) {
|
||||
return AngelHttp2.custom(app, securityContext, SecureServerSocket.bind,
|
||||
allowHttp1: allowHttp1, settings: settings);
|
||||
}
|
||||
|
||||
factory AngelHttp2.custom(
|
||||
Angel app,
|
||||
SecurityContext ctx,
|
||||
Future<SecureServerSocket> Function(
|
||||
InternetAddress? address, int port, SecurityContext ctx)
|
||||
serverGenerator,
|
||||
{bool useZone = true,
|
||||
bool allowHttp1 = false,
|
||||
ServerSettings? settings}) {
|
||||
return AngelHttp2._(app, (address, port) {
|
||||
var addr = address is InternetAddress
|
||||
? address
|
||||
: InternetAddress(address.toString());
|
||||
return Future.sync(() => serverGenerator(addr, port, ctx));
|
||||
}, useZone, allowHttp1, settings);
|
||||
}
|
||||
|
||||
/// Fires when an HTTP/1.x request is received.
|
||||
Stream<HttpRequest> get onHttp1 => _onHttp1.stream;
|
||||
|
||||
@override
|
||||
Future<SecureServerSocket> generateServer([address, int? port]) async {
|
||||
var s = await serverGenerator(address ?? '127.0.0.1', port ?? 0);
|
||||
return _artificial = _AngelHttp2ServerSocket(s, this);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _artificial?.close();
|
||||
await _http.close();
|
||||
return await super.close();
|
||||
}
|
||||
|
||||
@override
|
||||
void addCookies(ServerTransportStream response, Iterable<Cookie> cookies) {
|
||||
var headers =
|
||||
cookies.map((cookie) => Header.ascii('set-cookie', cookie.toString()));
|
||||
response.sendHeaders(headers.toList());
|
||||
}
|
||||
|
||||
@override
|
||||
Future closeResponse(ServerTransportStream response) {
|
||||
response.terminate();
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Http2RequestContext> createRequestContext(
|
||||
Socket request, ServerTransportStream response) {
|
||||
return Http2RequestContext.from(response, request, app, _sessions, _uuid);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Http2ResponseContext> createResponseContext(
|
||||
Socket request, ServerTransportStream response,
|
||||
[Http2RequestContext? correspondingRequest]) async {
|
||||
return Http2ResponseContext(app, response, correspondingRequest)
|
||||
..encoders.addAll(app.encoders);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<ServerTransportStream> createResponseStreamFromRawRequest(
|
||||
Socket request) {
|
||||
var connection =
|
||||
ServerTransportConnection.viaSocket(request, settings: settings);
|
||||
return connection.incomingStreams;
|
||||
}
|
||||
|
||||
@override
|
||||
void setChunkedEncoding(ServerTransportStream response, bool value) {
|
||||
// Do nothing in HTTP/2
|
||||
}
|
||||
|
||||
@override
|
||||
void setContentLength(ServerTransportStream response, int length) {
|
||||
setHeader(response, 'content-length', length.toString());
|
||||
}
|
||||
|
||||
@override
|
||||
void setHeader(ServerTransportStream response, String key, String? value) {
|
||||
response.sendHeaders([Header.ascii(key, value!)]);
|
||||
}
|
||||
|
||||
@override
|
||||
void setStatusCode(ServerTransportStream response, int value) {
|
||||
response.sendHeaders([Header.ascii(':status', value.toString())]);
|
||||
}
|
||||
|
||||
@override
|
||||
Uri get uri => Uri(
|
||||
scheme: 'https',
|
||||
host: server?.address.address,
|
||||
port: server?.port != 443 ? server?.port : null);
|
||||
|
||||
@override
|
||||
void writeStringToResponse(ServerTransportStream response, String value) {
|
||||
writeToResponse(response, utf8.encode(value));
|
||||
}
|
||||
|
||||
@override
|
||||
void writeToResponse(ServerTransportStream response, List<int> data) {
|
||||
response.sendData(data);
|
||||
}
|
||||
}
|
||||
|
||||
class _FakeServerSocket extends Stream<Socket> implements ServerSocket {
|
||||
final _AngelHttp2ServerSocket angel;
|
||||
final _ctrl = StreamController<Socket>();
|
||||
|
||||
_FakeServerSocket(this.angel);
|
||||
|
||||
@override
|
||||
InternetAddress get address => angel.address;
|
||||
|
||||
@override
|
||||
Future<ServerSocket> close() async {
|
||||
await (_ctrl.close());
|
||||
return this;
|
||||
}
|
||||
|
||||
@override
|
||||
int get port => angel.port;
|
||||
|
||||
@override
|
||||
StreamSubscription<Socket> listen(void Function(Socket event)? onData,
|
||||
{Function? onError, void Function()? onDone, bool? cancelOnError}) {
|
||||
return _ctrl.stream.listen(onData,
|
||||
cancelOnError: cancelOnError, onError: onError, onDone: onDone);
|
||||
}
|
||||
}
|
||||
|
||||
class _AngelHttp2ServerSocket extends Stream<SecureSocket>
|
||||
implements SecureServerSocket {
|
||||
final SecureServerSocket socket;
|
||||
final AngelHttp2 driver;
|
||||
final _ctrl = StreamController<SecureSocket>();
|
||||
late _FakeServerSocket _fake;
|
||||
StreamSubscription? _sub;
|
||||
|
||||
_AngelHttp2ServerSocket(this.socket, this.driver) {
|
||||
_fake = _FakeServerSocket(this);
|
||||
HttpServer.listenOn(_fake).pipe(driver._onHttp1);
|
||||
_sub = socket.listen(
|
||||
(socket) {
|
||||
if (socket.selectedProtocol == null ||
|
||||
socket.selectedProtocol == 'http/1.0' ||
|
||||
socket.selectedProtocol == 'http/1.1') {
|
||||
_fake._ctrl.add(socket);
|
||||
} else if (socket.selectedProtocol == 'h2' ||
|
||||
socket.selectedProtocol == 'h2-14') {
|
||||
_ctrl.add(socket);
|
||||
} else {
|
||||
socket.destroy();
|
||||
throw Exception(
|
||||
'AngelHttp2 does not support ${socket.selectedProtocol} as an ALPN protocol.');
|
||||
}
|
||||
},
|
||||
onDone: _ctrl.close,
|
||||
onError: (e, st) {
|
||||
driver.app.logger.warning(
|
||||
'HTTP/2 incoming connection failure: ', e, st as StackTrace);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
InternetAddress get address => socket.address;
|
||||
|
||||
@override
|
||||
int get port => socket.port;
|
||||
|
||||
@override
|
||||
Future<SecureServerSocket> close() {
|
||||
_sub?.cancel();
|
||||
_fake.close();
|
||||
_ctrl.close();
|
||||
return socket.close();
|
||||
}
|
||||
|
||||
@override
|
||||
StreamSubscription<SecureSocket> listen(
|
||||
void Function(SecureSocket event)? onData,
|
||||
{Function? onError,
|
||||
void Function()? onDone,
|
||||
bool? cancelOnError}) {
|
||||
return _ctrl.stream.listen(onData,
|
||||
cancelOnError: cancelOnError, onError: onError, onDone: onDone);
|
||||
}
|
||||
}
|
186
packages/framework/lib/src/http2/http2_request_context.dart
Normal file
186
packages/framework/lib/src/http2/http2_request_context.dart
Normal file
|
@ -0,0 +1,186 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:platform_container/platform_container.dart';
|
||||
import 'package:platform_framework/platform_framework.dart';
|
||||
import 'package:collection/collection.dart' show IterableExtension;
|
||||
import 'package:http2/transport.dart';
|
||||
import 'package:platform_mock_request/platform_mock_request.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
final RegExp _comma = RegExp(r',\s*');
|
||||
final RegExp _straySlashes = RegExp(r'(^/+)|(/+$)');
|
||||
|
||||
class Http2RequestContext extends RequestContext<ServerTransportStream?> {
|
||||
final StreamController<List<int>> _body = StreamController();
|
||||
@override
|
||||
final Container container;
|
||||
List<Cookie> _cookies = <Cookie>[];
|
||||
HttpHeaders? _headers;
|
||||
String? _method, _override, _path;
|
||||
late Socket _socket;
|
||||
ServerTransportStream? _stream;
|
||||
Uri? _uri;
|
||||
HttpSession? _session;
|
||||
|
||||
Http2RequestContext._(this.container);
|
||||
|
||||
@override
|
||||
Stream<List<int>> get body => _body.stream;
|
||||
|
||||
static Future<Http2RequestContext> from(
|
||||
ServerTransportStream stream,
|
||||
Socket socket,
|
||||
Angel app,
|
||||
Map<String, MockHttpSession> sessions,
|
||||
Uuid uuid) {
|
||||
var c = Completer<Http2RequestContext>();
|
||||
var req = Http2RequestContext._(app.container.createChild())
|
||||
..app = app
|
||||
.._socket = socket
|
||||
.._stream = stream;
|
||||
|
||||
var headers = req._headers = MockHttpHeaders();
|
||||
// String scheme = 'https', host = socket.address.address, path = '';
|
||||
var uri =
|
||||
Uri(scheme: 'https', host: socket.address.address, port: socket.port);
|
||||
var cookies = <Cookie>[];
|
||||
|
||||
void finalize() {
|
||||
req
|
||||
.._cookies = List.unmodifiable(cookies)
|
||||
.._uri = uri;
|
||||
if (!c.isCompleted) c.complete(req);
|
||||
}
|
||||
|
||||
void parseHost(String value) {
|
||||
var inUri = Uri.tryParse(value);
|
||||
if (inUri == null) return;
|
||||
// if (uri == null || uri.scheme == 'localhost') return;
|
||||
|
||||
if (inUri.hasScheme) uri = uri.replace(scheme: inUri.scheme);
|
||||
|
||||
if (inUri.hasAuthority) {
|
||||
uri = uri.replace(host: inUri.host, userInfo: inUri.userInfo);
|
||||
}
|
||||
|
||||
if (inUri.hasPort) uri = uri.replace(port: inUri.port);
|
||||
}
|
||||
|
||||
stream.incomingMessages.listen((msg) {
|
||||
if (msg is DataStreamMessage) {
|
||||
finalize();
|
||||
req._body.add(msg.bytes);
|
||||
} else if (msg is HeadersStreamMessage) {
|
||||
for (var header in msg.headers) {
|
||||
var name = ascii.decode(header.name).toLowerCase();
|
||||
var value = Uri.decodeComponent(ascii.decode(header.value));
|
||||
|
||||
switch (name) {
|
||||
case ':method':
|
||||
req._method = value;
|
||||
break;
|
||||
case ':path':
|
||||
var inUri = Uri.parse(value);
|
||||
uri = uri.replace(path: inUri.path);
|
||||
if (inUri.hasQuery) uri = uri.replace(query: inUri.query);
|
||||
var path = uri.path.replaceAll(_straySlashes, '');
|
||||
req._path = path;
|
||||
if (path.isEmpty) req._path = '/';
|
||||
break;
|
||||
case ':scheme':
|
||||
uri = uri.replace(scheme: value);
|
||||
break;
|
||||
case ':authority':
|
||||
parseHost(value);
|
||||
break;
|
||||
case 'cookie':
|
||||
var cookieStrings = value.split(';').map((s) => s.trim());
|
||||
|
||||
for (var cookieString in cookieStrings) {
|
||||
try {
|
||||
cookies.add(Cookie.fromSetCookieValue(cookieString));
|
||||
} catch (_) {
|
||||
// Ignore malformed cookies, and just don't add them to the container.
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
var name = ascii.decode(header.name).toLowerCase();
|
||||
|
||||
if (name == 'host') {
|
||||
parseHost(value);
|
||||
}
|
||||
|
||||
headers.add(name, value.split(_comma));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.endStream) finalize();
|
||||
}
|
||||
}, onDone: () {
|
||||
finalize();
|
||||
req._body.close();
|
||||
}, cancelOnError: true, onError: c.completeError);
|
||||
|
||||
// Apply session
|
||||
var dartSessId = cookies.firstWhereOrNull((c) => c.name == 'DARTSESSID');
|
||||
|
||||
dartSessId ??= Cookie('DARTSESSID', uuid.v4());
|
||||
|
||||
req._session = sessions.putIfAbsent(
|
||||
dartSessId.value,
|
||||
() => MockHttpSession(id: dartSessId!.value),
|
||||
);
|
||||
|
||||
return c.future;
|
||||
}
|
||||
|
||||
@override
|
||||
List<Cookie> get cookies => _cookies;
|
||||
|
||||
/// The underlying HTTP/2 [ServerTransportStream].
|
||||
ServerTransportStream? get stream => _stream;
|
||||
|
||||
@override
|
||||
Uri? get uri => _uri;
|
||||
|
||||
@override
|
||||
HttpSession? get session {
|
||||
return _session;
|
||||
}
|
||||
|
||||
@override
|
||||
InternetAddress get remoteAddress => _socket.remoteAddress;
|
||||
|
||||
@override
|
||||
String get path {
|
||||
return _path ?? '';
|
||||
}
|
||||
|
||||
@override
|
||||
String get originalMethod {
|
||||
return _method ?? 'GET';
|
||||
}
|
||||
|
||||
@override
|
||||
String get method {
|
||||
return _override ?? _method ?? 'GET';
|
||||
}
|
||||
|
||||
@override
|
||||
String get hostname => _headers?.value('host') ?? 'localhost';
|
||||
|
||||
@override
|
||||
HttpHeaders? get headers => _headers;
|
||||
|
||||
@override
|
||||
Future close() {
|
||||
_body.close();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@override
|
||||
ServerTransportStream? get rawRequest => _stream;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue