Add 'framework/' from commit '64d6729def5ffcf225a2c8f74fdf115218df1c56'

git-subtree-dir: framework
git-subtree-mainline: 6f6510ab27
git-subtree-split: 64d6729def
This commit is contained in:
Tobe O 2020-02-15 18:12:48 -05:00
commit 609d06f66b
136 changed files with 12340 additions and 0 deletions

66
framework/.gitignore vendored Normal file
View file

@ -0,0 +1,66 @@
# Created by .ignore support plugin (hsz.mobi)
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
*.iml
## Directory-based project format:
# if you remove the above rule, at least ignore the following:
# User-specific stuff:
# .idea/workspace.xml
# .idea/tasks.xml
# .idea/dictionaries
# Sensitive or high-churn files:
# .idea/dataSources.ids
# .idea/dataSources.xml
# .idea/sqlDataSources.xml
# .idea/dynamic.xml
# .idea/uiDesigner.xml
# Gradle:
# .idea/gradle.xml
# .idea/libraries
# Mongo Explorer plugin:
# .idea/mongoSettings.xml
## File-based project format:
*.ipr
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.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
### Dart template
# Dont commit the following directories created by pub.
.buildlog
.pub/
build/
packages
.packages
# Or the files created by dart2js.
*.dart.js
*.js_
*.js.deps
*.js.map
# Include when developing application packages.
pubspec.lock
doc/api
.dart_tool

View file

@ -0,0 +1,451 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DBNavigator.Project.DataEditorManager">
<record-view-column-sorting-type value="BY_INDEX" />
<value-preview-text-wrapping value="true" />
<value-preview-pinned value="false" />
</component>
<component name="DBNavigator.Project.DataExportManager">
<export-instructions>
<create-header value="true" />
<quote-values-containing-separator value="true" />
<quote-all-values value="false" />
<value-separator value="" />
<file-name value="" />
<file-location value="" />
<scope value="GLOBAL" />
<destination value="FILE" />
<format value="EXCEL" />
<charset value="windows-1252" />
</export-instructions>
</component>
<component name="DBNavigator.Project.DatabaseBrowserManager">
<autoscroll-to-editor value="false" />
<autoscroll-from-editor value="true" />
<show-object-properties value="true" />
<loaded-nodes />
</component>
<component name="DBNavigator.Project.EditorStateManager">
<last-used-providers />
</component>
<component name="DBNavigator.Project.MethodExecutionManager">
<method-browser />
<execution-history>
<group-entries value="true" />
<execution-inputs />
</execution-history>
<argument-values-cache />
</component>
<component name="DBNavigator.Project.ObjectDependencyManager">
<last-used-dependency-type value="INCOMING" />
</component>
<component name="DBNavigator.Project.ObjectQuickFilterManager">
<last-used-operator value="EQUAL" />
<filters />
</component>
<component name="DBNavigator.Project.ScriptExecutionManager" clear-outputs="true">
<recently-used-interfaces />
</component>
<component name="DBNavigator.Project.Settings">
<connections />
<browser-settings>
<general>
<display-mode value="TABBED" />
<navigation-history-size value="100" />
<show-object-details value="false" />
</general>
<filters>
<object-type-filter>
<object-type name="SCHEMA" enabled="true" />
<object-type name="USER" enabled="true" />
<object-type name="ROLE" enabled="true" />
<object-type name="PRIVILEGE" enabled="true" />
<object-type name="CHARSET" enabled="true" />
<object-type name="TABLE" enabled="true" />
<object-type name="VIEW" enabled="true" />
<object-type name="MATERIALIZED_VIEW" enabled="true" />
<object-type name="NESTED_TABLE" enabled="true" />
<object-type name="COLUMN" enabled="true" />
<object-type name="INDEX" enabled="true" />
<object-type name="CONSTRAINT" enabled="true" />
<object-type name="DATASET_TRIGGER" enabled="true" />
<object-type name="DATABASE_TRIGGER" enabled="true" />
<object-type name="SYNONYM" enabled="true" />
<object-type name="SEQUENCE" enabled="true" />
<object-type name="PROCEDURE" enabled="true" />
<object-type name="FUNCTION" enabled="true" />
<object-type name="PACKAGE" enabled="true" />
<object-type name="TYPE" enabled="true" />
<object-type name="TYPE_ATTRIBUTE" enabled="true" />
<object-type name="ARGUMENT" enabled="true" />
<object-type name="DIMENSION" enabled="true" />
<object-type name="CLUSTER" enabled="true" />
<object-type name="DBLINK" enabled="true" />
</object-type-filter>
</filters>
<sorting>
<object-type name="COLUMN" sorting-type="NAME" />
<object-type name="FUNCTION" sorting-type="NAME" />
<object-type name="PROCEDURE" sorting-type="NAME" />
<object-type name="ARGUMENT" sorting-type="POSITION" />
</sorting>
<default-editors>
<object-type name="VIEW" editor-type="SELECTION" />
<object-type name="PACKAGE" editor-type="SELECTION" />
<object-type name="TYPE" editor-type="SELECTION" />
</default-editors>
</browser-settings>
<navigation-settings>
<lookup-filters>
<lookup-objects>
<object-type name="SCHEMA" enabled="true" />
<object-type name="USER" enabled="false" />
<object-type name="ROLE" enabled="false" />
<object-type name="PRIVILEGE" enabled="false" />
<object-type name="CHARSET" enabled="false" />
<object-type name="TABLE" enabled="true" />
<object-type name="VIEW" enabled="true" />
<object-type name="MATERIALIZED VIEW" enabled="true" />
<object-type name="NESTED TABLE" enabled="false" />
<object-type name="COLUMN" enabled="false" />
<object-type name="INDEX" enabled="true" />
<object-type name="CONSTRAINT" enabled="true" />
<object-type name="DATASET TRIGGER" enabled="true" />
<object-type name="DATABASE TRIGGER" enabled="true" />
<object-type name="SYNONYM" enabled="false" />
<object-type name="SEQUENCE" enabled="true" />
<object-type name="PROCEDURE" enabled="true" />
<object-type name="FUNCTION" enabled="true" />
<object-type name="PACKAGE" enabled="true" />
<object-type name="TYPE" enabled="true" />
<object-type name="TYPE ATTRIBUTE" enabled="false" />
<object-type name="ARGUMENT" enabled="false" />
<object-type name="DIMENSION" enabled="false" />
<object-type name="CLUSTER" enabled="false" />
<object-type name="DBLINK" enabled="true" />
</lookup-objects>
<force-database-load value="false" />
<prompt-connection-selection value="true" />
<prompt-schema-selection value="true" />
</lookup-filters>
</navigation-settings>
<dataset-grid-settings>
<general>
<enable-zooming value="true" />
</general>
<sorting>
<nulls-first value="true" />
<max-sorting-columns value="4" />
</sorting>
<tracking-columns>
<columnNames value="" />
<visible value="true" />
<editable value="false" />
</tracking-columns>
</dataset-grid-settings>
<dataset-editor-settings>
<text-editor-popup>
<active value="false" />
<active-if-empty value="false" />
<data-length-threshold value="100" />
<popup-delay value="1000" />
</text-editor-popup>
<values-list-popup>
<show-popup-button value="true" />
<element-count-threshold value="1000" />
<data-length-threshold value="250" />
</values-list-popup>
<general>
<fetch-block-size value="100" />
<fetch-timeout value="30" />
<trim-whitespaces value="true" />
<convert-empty-strings-to-null value="true" />
<select-content-on-cell-edit value="true" />
<large-value-preview-active value="true" />
</general>
<filters>
<prompt-filter-dialog value="true" />
<default-filter-type value="BASIC" />
</filters>
<qualified-text-editor text-length-threshold="300">
<content-types>
<content-type name="Text" enabled="true" />
<content-type name="XML" enabled="true" />
<content-type name="DTD" enabled="true" />
<content-type name="HTML" enabled="true" />
<content-type name="XHTML" enabled="true" />
<content-type name="CSS" enabled="true" />
<content-type name="SQL" enabled="true" />
<content-type name="PL/SQL" enabled="true" />
<content-type name="JavaScript" enabled="true" />
<content-type name="JSP" enabled="true" />
<content-type name="JSPx" enabled="true" />
<content-type name="ASP" enabled="true" />
<content-type name="YAML" enabled="true" />
<content-type name="Bash" enabled="true" />
</content-types>
</qualified-text-editor>
<record-navigation>
<navigation-target value="VIEWER" />
</record-navigation>
</dataset-editor-settings>
<code-editor-settings>
<general>
<show-object-navigation-gutter value="false" />
<show-spec-declaration-navigation-gutter value="true" />
</general>
<confirmations>
<save-changes value="false" />
<revert-changes value="true" />
</confirmations>
</code-editor-settings>
<code-completion-settings>
<filters>
<basic-filter>
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
<filter-element type="RESERVED_WORD" id="function" selected="true" />
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
<filter-element type="OBJECT" id="schema" selected="true" />
<filter-element type="OBJECT" id="role" selected="true" />
<filter-element type="OBJECT" id="user" selected="true" />
<filter-element type="OBJECT" id="privilege" selected="true" />
<user-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="false" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</user-schema>
<public-schema>
<filter-element type="OBJECT" id="table" selected="false" />
<filter-element type="OBJECT" id="view" selected="false" />
<filter-element type="OBJECT" id="materialized view" selected="false" />
<filter-element type="OBJECT" id="index" selected="false" />
<filter-element type="OBJECT" id="constraint" selected="false" />
<filter-element type="OBJECT" id="trigger" selected="false" />
<filter-element type="OBJECT" id="synonym" selected="false" />
<filter-element type="OBJECT" id="sequence" selected="false" />
<filter-element type="OBJECT" id="procedure" selected="false" />
<filter-element type="OBJECT" id="function" selected="false" />
<filter-element type="OBJECT" id="package" selected="false" />
<filter-element type="OBJECT" id="type" selected="false" />
<filter-element type="OBJECT" id="dimension" selected="false" />
<filter-element type="OBJECT" id="cluster" selected="false" />
<filter-element type="OBJECT" id="dblink" selected="false" />
</public-schema>
<any-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</any-schema>
</basic-filter>
<extended-filter>
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
<filter-element type="RESERVED_WORD" id="function" selected="true" />
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
<filter-element type="OBJECT" id="schema" selected="true" />
<filter-element type="OBJECT" id="user" selected="true" />
<filter-element type="OBJECT" id="role" selected="true" />
<filter-element type="OBJECT" id="privilege" selected="true" />
<user-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</user-schema>
<public-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</public-schema>
<any-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</any-schema>
</extended-filter>
</filters>
<sorting enabled="true">
<sorting-element type="RESERVED_WORD" id="keyword" />
<sorting-element type="RESERVED_WORD" id="datatype" />
<sorting-element type="OBJECT" id="column" />
<sorting-element type="OBJECT" id="table" />
<sorting-element type="OBJECT" id="view" />
<sorting-element type="OBJECT" id="materialized view" />
<sorting-element type="OBJECT" id="index" />
<sorting-element type="OBJECT" id="constraint" />
<sorting-element type="OBJECT" id="trigger" />
<sorting-element type="OBJECT" id="synonym" />
<sorting-element type="OBJECT" id="sequence" />
<sorting-element type="OBJECT" id="procedure" />
<sorting-element type="OBJECT" id="function" />
<sorting-element type="OBJECT" id="package" />
<sorting-element type="OBJECT" id="type" />
<sorting-element type="OBJECT" id="dimension" />
<sorting-element type="OBJECT" id="cluster" />
<sorting-element type="OBJECT" id="dblink" />
<sorting-element type="OBJECT" id="schema" />
<sorting-element type="OBJECT" id="role" />
<sorting-element type="OBJECT" id="user" />
<sorting-element type="RESERVED_WORD" id="function" />
<sorting-element type="RESERVED_WORD" id="parameter" />
</sorting>
<format>
<enforce-code-style-case value="true" />
</format>
</code-completion-settings>
<execution-engine-settings>
<statement-execution>
<fetch-block-size value="100" />
<execution-timeout value="20" />
<debug-execution-timeout value="600" />
<focus-result value="false" />
<prompt-execution value="false" />
</statement-execution>
<script-execution>
<command-line-interfaces />
<execution-timeout value="300" />
</script-execution>
<method-execution>
<execution-timeout value="30" />
<debug-execution-timeout value="600" />
<parameter-history-size value="10" />
</method-execution>
</execution-engine-settings>
<operation-settings>
<transactions>
<uncommitted-changes>
<on-project-close value="ASK" />
<on-disconnect value="ASK" />
<on-autocommit-toggle value="ASK" />
</uncommitted-changes>
<multiple-uncommitted-changes>
<on-commit value="ASK" />
<on-rollback value="ASK" />
</multiple-uncommitted-changes>
</transactions>
<session-browser>
<disconnect-session value="ASK" />
<kill-session value="ASK" />
<reload-on-filter-change value="false" />
</session-browser>
<compiler>
<compile-type value="KEEP" />
<compile-dependencies value="ASK" />
<always-show-controls value="false" />
</compiler>
<debugger>
<debugger-type value="JDBC" />
<use-generic-runners value="true" />
</debugger>
</operation-settings>
<ddl-file-settings>
<extensions>
<mapping file-type-id="VIEW" extensions="vw" />
<mapping file-type-id="TRIGGER" extensions="trg" />
<mapping file-type-id="PROCEDURE" extensions="prc" />
<mapping file-type-id="FUNCTION" extensions="fnc" />
<mapping file-type-id="PACKAGE" extensions="pkg" />
<mapping file-type-id="PACKAGE_SPEC" extensions="pks" />
<mapping file-type-id="PACKAGE_BODY" extensions="pkb" />
<mapping file-type-id="TYPE" extensions="tpe" />
<mapping file-type-id="TYPE_SPEC" extensions="tps" />
<mapping file-type-id="TYPE_BODY" extensions="tpb" />
</extensions>
<general>
<lookup-ddl-files value="true" />
<create-ddl-files value="false" />
<synchronize-ddl-files value="true" />
<use-qualified-names value="false" />
<make-scripts-rerunnable value="true" />
</general>
</ddl-file-settings>
<general-settings>
<regional-settings>
<date-format value="MEDIUM" />
<number-format value="UNGROUPED" />
<locale value="SYSTEM_DEFAULT" />
<use-custom-formats value="false" />
</regional-settings>
<environment>
<environment-types>
<environment-type id="development" name="Development" description="Development environment" color="-2430209/-12296320" readonly-code="false" readonly-data="false" />
<environment-type id="integration" name="Integration" description="Integration environment" color="-2621494/-12163514" readonly-code="true" readonly-data="false" />
<environment-type id="production" name="Production" description="Productive environment" color="-11574/-10271420" readonly-code="true" readonly-data="true" />
<environment-type id="other" name="Other" description="" color="-1576/-10724543" readonly-code="false" readonly-data="false" />
</environment-types>
<visibility-settings>
<connection-tabs value="true" />
<dialog-headers value="true" />
<object-editor-tabs value="true" />
<script-editor-tabs value="false" />
<execution-result-tabs value="true" />
</visibility-settings>
</environment>
</general-settings>
</component>
<component name="DBNavigator.Project.StatementExecutionManager">
<execution-variables />
</component>
</project>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="PROJECT" charset="UTF-8" />
</component>
</project>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<includedPredefinedLibrary name="ECMAScript 6" />
<includedPredefinedLibrary name="Node.js Core" />
</component>
</project>

View file

@ -0,0 +1,525 @@
<component name="libraryTable">
<library name="Dart Packages" type="DartPackagesLibraryType">
<properties>
<option name="packageNameToDirsMap">
<entry key="analyzer">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/analyzer-0.32.4/lib" />
</list>
</value>
</entry>
<entry key="angel_container">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/angel_container-1.0.0-alpha.8/lib" />
</list>
</value>
</entry>
<entry key="angel_http_exception">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/angel_http_exception-1.0.0+3/lib" />
</list>
</value>
</entry>
<entry key="angel_model">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/angel_model-1.0.0+1/lib" />
</list>
</value>
</entry>
<entry key="angel_route">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/angel_route-3.0.0/lib" />
</list>
</value>
</entry>
<entry key="args">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/args-1.5.0/lib" />
</list>
</value>
</entry>
<entry key="async">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/async-2.0.8/lib" />
</list>
</value>
</entry>
<entry key="body_parser">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/body_parser-1.1.1/lib" />
</list>
</value>
</entry>
<entry key="boolean_selector">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/boolean_selector-1.0.4/lib" />
</list>
</value>
</entry>
<entry key="charcode">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/charcode-1.1.2/lib" />
</list>
</value>
</entry>
<entry key="code_buffer">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/code_buffer-1.0.1/lib" />
</list>
</value>
</entry>
<entry key="collection">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/collection-1.14.11/lib" />
</list>
</value>
</entry>
<entry key="combinator">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/combinator-1.0.0+3/lib" />
</list>
</value>
</entry>
<entry key="convert">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/convert-2.0.2/lib" />
</list>
</value>
</entry>
<entry key="crypto">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/crypto-2.0.6/lib" />
</list>
</value>
</entry>
<entry key="csslib">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/csslib-0.14.5/lib" />
</list>
</value>
</entry>
<entry key="dart2_constant">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/dart2_constant-1.0.2+dart2/lib" />
</list>
</value>
</entry>
<entry key="file">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/file-5.0.4/lib" />
</list>
</value>
</entry>
<entry key="front_end">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/front_end-0.1.4/lib" />
</list>
</value>
</entry>
<entry key="glob">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/glob-1.1.7/lib" />
</list>
</value>
</entry>
<entry key="html">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/html-0.13.3+3/lib" />
</list>
</value>
</entry>
<entry key="http">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/http-0.11.3+17/lib" />
</list>
</value>
</entry>
<entry key="http_multi_server">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/http_multi_server-2.0.5/lib" />
</list>
</value>
</entry>
<entry key="http_parser">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/http_parser-3.1.3/lib" />
</list>
</value>
</entry>
<entry key="http_server">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/http_server-0.9.8/lib" />
</list>
</value>
</entry>
<entry key="intl">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/intl-0.15.7/lib" />
</list>
</value>
</entry>
<entry key="io">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/io-0.3.3/lib" />
</list>
</value>
</entry>
<entry key="js">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/js-0.6.1+1/lib" />
</list>
</value>
</entry>
<entry key="json_rpc_2">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/json_rpc_2-2.0.9/lib" />
</list>
</value>
</entry>
<entry key="kernel">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/kernel-0.3.4/lib" />
</list>
</value>
</entry>
<entry key="logging">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/logging-0.11.3+2/lib" />
</list>
</value>
</entry>
<entry key="matcher">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/matcher-0.12.3+1/lib" />
</list>
</value>
</entry>
<entry key="merge_map">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/merge_map-1.0.1/lib" />
</list>
</value>
</entry>
<entry key="meta">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/meta-1.1.6/lib" />
</list>
</value>
</entry>
<entry key="mime">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/mime-0.9.6+2/lib" />
</list>
</value>
</entry>
<entry key="mock_request">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/mock_request-1.0.3/lib" />
</list>
</value>
</entry>
<entry key="multi_server_socket">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/multi_server_socket-1.0.2/lib" />
</list>
</value>
</entry>
<entry key="node_preamble">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/node_preamble-1.4.4/lib" />
</list>
</value>
</entry>
<entry key="package_config">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/package_config-1.0.5/lib" />
</list>
</value>
</entry>
<entry key="package_resolver">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/package_resolver-1.0.4/lib" />
</list>
</value>
</entry>
<entry key="path">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/path-1.6.2/lib" />
</list>
</value>
</entry>
<entry key="plugin">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/plugin-0.2.0+3/lib" />
</list>
</value>
</entry>
<entry key="pool">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/pool-1.3.6/lib" />
</list>
</value>
</entry>
<entry key="pub_semver">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/pub_semver-1.4.2/lib" />
</list>
</value>
</entry>
<entry key="quiver">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/quiver-2.0.0+1/lib" />
</list>
</value>
</entry>
<entry key="shelf">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/shelf-0.7.3+3/lib" />
</list>
</value>
</entry>
<entry key="shelf_packages_handler">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/shelf_packages_handler-1.0.4/lib" />
</list>
</value>
</entry>
<entry key="shelf_static">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/shelf_static-0.2.8/lib" />
</list>
</value>
</entry>
<entry key="shelf_web_socket">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/shelf_web_socket-0.2.2+4/lib" />
</list>
</value>
</entry>
<entry key="source_map_stack_trace">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/source_map_stack_trace-1.1.5/lib" />
</list>
</value>
</entry>
<entry key="source_maps">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/source_maps-0.10.7/lib" />
</list>
</value>
</entry>
<entry key="source_span">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/source_span-1.4.1/lib" />
</list>
</value>
</entry>
<entry key="stack_trace">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/stack_trace-1.9.3/lib" />
</list>
</value>
</entry>
<entry key="stream_channel">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/stream_channel-1.6.8/lib" />
</list>
</value>
</entry>
<entry key="string_scanner">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/string_scanner-1.0.4/lib" />
</list>
</value>
</entry>
<entry key="term_glyph">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/term_glyph-1.0.1/lib" />
</list>
</value>
</entry>
<entry key="test">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/test-1.3.0/lib" />
</list>
</value>
</entry>
<entry key="tuple">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/tuple-1.0.2/lib" />
</list>
</value>
</entry>
<entry key="typed_data">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/typed_data-1.1.6/lib" />
</list>
</value>
</entry>
<entry key="utf">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/utf-0.9.0+5/lib" />
</list>
</value>
</entry>
<entry key="vm_service_client">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/vm_service_client-0.2.6/lib" />
</list>
</value>
</entry>
<entry key="watcher">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/watcher-0.9.7+10/lib" />
</list>
</value>
</entry>
<entry key="web_socket_channel">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/web_socket_channel-1.0.9/lib" />
</list>
</value>
</entry>
<entry key="yaml">
<value>
<list>
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/yaml-2.1.15/lib" />
</list>
</value>
</entry>
</option>
</properties>
<CLASSES>
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/analyzer-0.32.4/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/angel_container-1.0.0-alpha.8/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/angel_http_exception-1.0.0+3/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/angel_model-1.0.0+1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/angel_route-3.0.0/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/args-1.5.0/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/async-2.0.8/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/body_parser-1.1.1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/boolean_selector-1.0.4/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/charcode-1.1.2/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/code_buffer-1.0.1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/collection-1.14.11/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/combinator-1.0.0+3/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/convert-2.0.2/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/crypto-2.0.6/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/csslib-0.14.5/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/dart2_constant-1.0.2+dart2/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/file-5.0.4/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/front_end-0.1.4/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/glob-1.1.7/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/html-0.13.3+3/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/http-0.11.3+17/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/http_multi_server-2.0.5/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/http_parser-3.1.3/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/http_server-0.9.8/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/intl-0.15.7/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/io-0.3.3/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/js-0.6.1+1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/json_rpc_2-2.0.9/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/kernel-0.3.4/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/logging-0.11.3+2/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/matcher-0.12.3+1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/merge_map-1.0.1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/meta-1.1.6/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/mime-0.9.6+2/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/mock_request-1.0.3/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/multi_server_socket-1.0.2/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/node_preamble-1.4.4/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/package_config-1.0.5/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/package_resolver-1.0.4/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/path-1.6.2/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/plugin-0.2.0+3/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/pool-1.3.6/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/pub_semver-1.4.2/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/quiver-2.0.0+1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/shelf-0.7.3+3/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/shelf_packages_handler-1.0.4/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/shelf_static-0.2.8/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/shelf_web_socket-0.2.2+4/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/source_map_stack_trace-1.1.5/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/source_maps-0.10.7/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/source_span-1.4.1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/stack_trace-1.9.3/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/stream_channel-1.6.8/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/string_scanner-1.0.4/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/term_glyph-1.0.1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/test-1.3.0/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/tuple-1.0.2/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/typed_data-1.1.6/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/utf-0.9.0+5/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/vm_service_client-0.2.6/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/watcher-0.9.7+10/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/web_socket_channel-1.0.9/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/yaml-2.1.15/lib" />
</CLASSES>
<JAVADOC />
<LIBRARY_FILE />
<SOURCES />
</library>
</component>

View file

@ -0,0 +1,27 @@
<component name="libraryTable">
<library name="Dart SDK">
<CLASSES>
<root url="file:///usr/local/opt/dart/libexec/lib/async" />
<root url="file:///usr/local/opt/dart/libexec/lib/collection" />
<root url="file:///usr/local/opt/dart/libexec/lib/convert" />
<root url="file:///usr/local/opt/dart/libexec/lib/core" />
<root url="file:///usr/local/opt/dart/libexec/lib/developer" />
<root url="file:///usr/local/opt/dart/libexec/lib/html" />
<root url="file:///usr/local/opt/dart/libexec/lib/indexed_db" />
<root url="file:///usr/local/opt/dart/libexec/lib/io" />
<root url="file:///usr/local/opt/dart/libexec/lib/isolate" />
<root url="file:///usr/local/opt/dart/libexec/lib/js" />
<root url="file:///usr/local/opt/dart/libexec/lib/js_util" />
<root url="file:///usr/local/opt/dart/libexec/lib/math" />
<root url="file:///usr/local/opt/dart/libexec/lib/mirrors" />
<root url="file:///usr/local/opt/dart/libexec/lib/svg" />
<root url="file:///usr/local/opt/dart/libexec/lib/typed_data" />
<root url="file:///usr/local/opt/dart/libexec/lib/web_audio" />
<root url="file:///usr/local/opt/dart/libexec/lib/web_gl" />
<root url="file:///usr/local/opt/dart/libexec/lib/web_sql" />
</CLASSES>
<JAVADOC />
<LIBRARY_FILE />
<SOURCES />
</library>
</component>

4
framework/.idea/misc.xml Normal file
View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PhpWorkspaceProjectConfiguration" backward_compatibility_performed="true" />
</project>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/framework.iml" filepath="$PROJECT_DIR$/framework.iml" />
</modules>
</component>
</project>

View file

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test" />
<option name="scope" value="FOLDER" />
<option name="testRunnerOptions" value="-j 4" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,9 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All Tests (PRODUCTION)" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true">
<option name="envs">
<entry key="ANGEL_ENV" value="production" />
</option>
<option name="filePath" value="$PROJECT_DIR$/test/all.dart" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All Tests (for coverage)" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test/all.dart" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Controller Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test/controller_test.dart" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="DI Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test/di_test.dart" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Hooked Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test/hooked_test.dart" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Match routes, even with query params in routing_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/test/routing_test.dart" />
<option name="scope" value="GROUP_OR_TEST_BY_NAME" />
<option name="testName" value="Match routes, even with query params" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,9 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Middleware via metadata in routing_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/test/routing_test.dart" />
<option name="scope" value="GROUP_OR_TEST_BY_NAME" />
<option name="testName" value="Middleware via metadata" />
<option name="testRunnerOptions" value="-j 4" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Routing Tests" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test/routing_test.dart" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,9 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="can fetch data in services_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/test/services_test.dart" />
<option name="scope" value="GROUP_OR_TEST_BY_NAME" />
<option name="testName" value="can fetch data" />
<option name="testRunnerOptions" value="-j 4" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="cannot write after close in streaming_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/test/streaming_test.dart" />
<option name="scope" value="GROUP_OR_TEST_BY_NAME" />
<option name="testName" value="cannot write after close" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,9 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="encoding in encoders_buffer_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/test/encoders_buffer_test.dart" />
<option name="scope" value="GROUP_OR_TEST_BY_NAME" />
<option name="testName" value="encoding" />
<option name="testRunnerOptions" value="-j 4" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,9 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="encoding in streaming_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/test/streaming_test.dart" />
<option name="scope" value="GROUP_OR_TEST_BY_NAME" />
<option name="testName" value="encoding" />
<option name="testRunnerOptions" value="-j 4" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="handle_error.dart" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/example/handle_error.dart" />
<option name="workingDirectory" value="$PROJECT_DIR$" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="injects header or throws in parameter_meta_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/test/parameter_meta_test.dart" />
<option name="scope" value="GROUP_OR_TEST_BY_NAME" />
<option name="testName" value="injects header or throws" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="injects session or throws in parameter_meta_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/test/parameter_meta_test.dart" />
<option name="scope" value="GROUP_OR_TEST_BY_NAME" />
<option name="testName" value="injects session or throws" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="json.dart" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/example/json.dart" />
<option name="workingDirectory" value="$PROJECT_DIR$" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="main.dart" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/example/main.dart" />
<option name="workingDirectory" value="$PROJECT_DIR$" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,9 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="metadata in hooked_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/test/hooked_test.dart" />
<option name="scope" value="GROUP_OR_TEST_BY_NAME" />
<option name="testName" value="metadata" />
<option name="testRunnerOptions" value="-j 4" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="only match route with matching method in routing_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="envs">
<entry key="ANGEL_ENV" value="production" />
</option>
<option name="filePath" value="$PROJECT_DIR$/test/routing_test.dart" />
<option name="scope" value="GROUP_OR_TEST_BY_NAME" />
<option name="testName" value="only match route with matching method" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="performance::hello (DEV)" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/performance/hello/main.dart" />
<option name="workingDirectory" value="$PROJECT_DIR$" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="performance::hello (PRODUCTION)" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true">
<option name="checkedMode" value="false" />
<option name="envs">
<entry key="ANGEL_ENV" value="production" />
</option>
<option name="filePath" value="$PROJECT_DIR$/performance/hello/main.dart" />
<option name="workingDirectory" value="$PROJECT_DIR$" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="performance::hello::raw" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true">
<option name="checkedMode" value="false" />
<option name="filePath" value="$PROJECT_DIR$/performance/hello/raw.dart" />
<option name="workingDirectory" value="$PROJECT_DIR$" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="tests in find_one_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/test/find_one_test.dart" />
<option name="testRunnerOptions" value="-j4" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="tests in framework" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$" />
<option name="scope" value="FOLDER" />
<option name="testRunnerOptions" value="-j 4" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="tests in framework (PRODUCTION)" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">
<option name="envs">
<entry key="ANGEL_ENV" value="production" />
</option>
<option name="filePath" value="$PROJECT_DIR$/test" />
<option name="scope" value="FOLDER" />
<option name="testRunnerOptions" value="-j 4" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,9 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="tests in server_test.dart (PRODUCTION)" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true">
<option name="envs">
<entry key="ANGEL_ENV" value="production" />
</option>
<option name="filePath" value="$PROJECT_DIR$/test/server_test.dart" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="view.dart" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/example/view.dart" />
<option name="workingDirectory" value="$PROJECT_DIR$" />
<method />
</configuration>
</component>

6
framework/.idea/vcs.xml Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

File diff suppressed because it is too large Load diff

6
framework/.travis.yml Normal file
View file

@ -0,0 +1,6 @@
language: dart
dart:
- dev
- stable
before_script: chmod +x ./tool/travis.sh
script: ./tool/travis.sh

3
framework/.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
// Place your settings in this file to overwrite default and user settings.
{
}

282
framework/CHANGELOG.md Normal file
View file

@ -0,0 +1,282 @@
# 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.

21
framework/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 The Angel Framework
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

61
framework/README.md Normal file
View file

@ -0,0 +1,61 @@
# angel_framework
[![Pub](https://img.shields.io/pub/v/angel_framework.svg)](https://pub.dartlang.org/packages/angel_framework)
[![build status](https://travis-ci.org/angel-dart/framework.svg)](https://travis-ci.org/angel-dart/framework)
A high-powered HTTP server with support for dependency injection, sophisticated routing and more.
This is the core of the [Angel](https://github.com/angel-dart/angel) framework.
To build real-world applications, please see the [homepage](https://angel-dart.dev).
```dart
import 'package:angel_container/mirrors.dart';
import 'package:angel_framework/angel_framework.dart';
main() async {
var app = Angel(reflector: MirrorsReflector());
// Index route. Returns JSON.
app.get('/', (req, res) => res.write('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 AngelHttpException.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');
}
```

7
framework/TODO.md Normal file
View file

@ -0,0 +1,7 @@
* Support for [Trestle](https://github.com/dart-bridge/trestle), use this as default, set up migration system around this
* Angel CLI
* Angel bootstrap project
* More docs
* Make tutorials, videos
* Launch!
* Get a nice launch process, so we can pre-compile things before running. Also support a sort of hot-reload

View file

@ -0,0 +1,15 @@
# include: package:pedantic/analysis_options.yaml
analyzer:
errors:
always_declare_return_types: ignore
omit_local_variable_types: ignore
prefer_single_quotes: ignore
prefer_spread_collections: ignore
strong-mode:
implicit-casts: false
linter:
rules:
- avoid_slow_async_io
- curly_braces_in_flow_control_structures
- unnecessary_const
- unnecessary_new

29
framework/dev.key Normal file
View 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
framework/dev.pem Normal file
View 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-----

View file

@ -0,0 +1,59 @@
import 'package:angel_container/mirrors.dart';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:logging/logging.dart';
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 AngelHttpException.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
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};
}
}

View file

@ -0,0 +1,25 @@
import 'dart:async';
import 'dart:io';
import 'package:angel_container/mirrors.dart';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:logging/logging.dart';
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);
var server = await http.startServer('127.0.0.1', 3000);
var url = 'http://${server.address.address}:${server.port}';
print('Listening at $url');
}

View file

@ -0,0 +1,47 @@
import 'dart:async';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:logging/logging.dart';
import 'package:pretty_logging/pretty_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.)');
}
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 ?? e.error ?? e);
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.');
}

View file

@ -0,0 +1,46 @@
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:angel_framework/http2.dart';
import 'package:file/local.dart';
import 'package:logging/logging.dart';
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}');
}

View 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);
}

View 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-----

View 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-----

View file

@ -0,0 +1,43 @@
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:angel_framework/http2.dart';
import 'package:logging/logging.dart';
import 'common.dart';
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 AngelHttpException.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}');
}

View file

@ -0,0 +1,9 @@
import 'package:logging/logging.dart';
/// Prints the contents of a [LogRecord] with pretty colors.
void prettyLog(LogRecord record) {
print(record.toString());
if (record.error != null) print(record.error.toString());
if (record.stackTrace != null) print(record.stackTrace.toString());
}

View 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);
};

View 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>

View 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>

View 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;
}

View file

@ -0,0 +1,62 @@
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:angel_framework/http2.dart';
import 'package:file/local.dart';
import 'package:logging/logging.dart';
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}');
}

View file

@ -0,0 +1,53 @@
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
main() async {
int x = 0;
var c = Completer();
var exit = ReceivePort();
List<Isolate> isolates = [];
exit.listen((_) {
if (++x >= 50) {
c.complete();
}
});
for (int 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;
}
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 ?? e.error ?? e);
print(e.stackTrace);
};
var server = await http.startServer('127.0.0.1', 3000);
print('Listening at http://${server.address.address}:${server.port}');
}

View file

@ -0,0 +1,59 @@
import 'package:angel_container/mirrors.dart';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:logging/logging.dart';
import 'package:pretty_logging/pretty_logging.dart';
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 AngelHttpException.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');
}

View file

@ -0,0 +1,22 @@
import 'package:angel_container/mirrors.dart';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:logging/logging.dart';
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}');
}

View file

@ -0,0 +1,14 @@
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
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}');
}

View file

@ -0,0 +1,18 @@
import 'package:angel_container/mirrors.dart';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
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');
}

View file

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>Hello!</h1>
</body>
</html>

View file

@ -0,0 +1,7 @@
/// An easily-extensible web server framework in Dart.
library angel_framework;
export 'package:angel_http_exception/angel_http_exception.dart';
export 'package:angel_model/angel_model.dart';
export 'package:angel_route/angel_route.dart';
export 'src/core/core.dart';

1
framework/lib/http.dart Normal file
View file

@ -0,0 +1 @@
export 'src/http/http.dart';

3
framework/lib/http2.dart Normal file
View 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';

View file

@ -0,0 +1,59 @@
import 'dart:async';
import 'request_context.dart';
import 'response_context.dart';
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>> index([Map<String, dynamic> params]),
FutureOr<Data> read(Id id, [Map<String, dynamic> params]),
FutureOr<Data> create(Data data, [Map<String, dynamic> params]),
FutureOr<Data> modify(Id id, Data data, [Map<String, dynamic> params]),
FutureOr<Data> update(Id id, Data data, [Map<String, dynamic> params]),
FutureOr<Data> remove(Id id, [Map<String, dynamic> params]),
FutureOr<Data> Function(RequestContext, ResponseContext) readData})
: super(readData: readData) {
_index = index;
_read = read;
_create = create;
_modify = modify;
_update = update;
_remove = remove;
}
@override
index([Map<String, dynamic> params]) =>
Future.sync(() => _index != null ? _index(params) : super.index(params));
@override
read(Id id, [Map<String, dynamic> params]) => Future.sync(
() => _read != null ? _read(id, params) : super.read(id, params));
@override
create(Data data, [Map<String, dynamic> params]) => Future.sync(() =>
_create != null ? _create(data, params) : super.create(data, params));
@override
modify(Id id, Data data, [Map<String, dynamic> params]) =>
Future.sync(() => _modify != null
? _modify(id, data, params)
: super.modify(id, data, params));
@override
update(Id id, Data data, [Map<String, dynamic> params]) =>
Future.sync(() => _update != null
? _update(id, data, params)
: super.update(id, data, params));
@override
remove(Id id, [Map<String, dynamic> params]) => Future.sync(
() => _remove != null ? _remove(id, params) : super.remove(id, params));
}

View file

@ -0,0 +1,233 @@
library angel_framework.http.controller;
import 'dart:async';
import 'package:angel_container/angel_container.dart';
import 'package:angel_route/angel_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 => _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(this.runtimeType);
Expose exposeDecl = findExpose(reflector);
if (exposeDecl == null) {
throw Exception("All controllers must carry an @Expose() declaration.");
}
var routable = Routable();
_mountPoint = router.mount(exposeDecl.path, routable);
var typeMirror = reflector.reflectType(this.runtimeType);
// Pre-reflect methods
var instanceMirror = reflector.reflectInstance(this);
final handlers = <RequestHandler>[]
..addAll(exposeDecl.middleware)
..addAll(middleware);
final routeBuilder =
_routeBuilder(reflector, instanceMirror, routable, handlers);
await configureRoutes(routable);
classMirror.declarations.forEach(routeBuilder);
// Return the name.
return exposeDecl.as?.isNotEmpty == true ? exposeDecl.as : typeMirror.name;
}
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(null);
}
}
var reflectedMethod =
instanceMirror.getField(methodName).reflectee as Function;
var middleware = <RequestHandler>[]
..addAll(handlers)
..addAll(exposeDecl.middleware);
String 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 ?? 'GET';
if (path == null) {
// 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, '_')));
}
}

View 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';

View file

@ -0,0 +1,372 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io' show stderr, Cookie;
import 'package:angel_http_exception/angel_http_exception.dart';
import 'package:angel_route/angel_route.dart';
import 'package:combinator/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;
Server _server;
StreamSubscription<Request> _sub;
/// 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 get server => _server;
Future<Server> generateServer(address, int port) =>
serverGenerator(address, port);
/// Starts, and returns the server.
Future<Server> startServer([address, int port]) {
var host = address ?? '127.0.0.1';
return generateServer(host, port ?? 0).then((server) {
_server = server;
return Future.wait(app.startupHooks.map(app.configure)).then((_) {
app.optimizeForProduction();
_sub = server.listen((request) {
var stream = createResponseStreamFromRawRequest(request);
stream.listen((response) {
return handleRawRequest(request, response);
});
});
return _server;
});
});
}
/// Shuts down the underlying server.
Future<Server> close() {
if (_closed) return Future.value(_server);
_closed = true;
_sub?.cancel();
return app.close().then((_) =>
Future.wait(app.shutdownHooks.map(app.configure)).then((_) => _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) {
return createRequestContext(request, response).then((req) {
return createResponseContext(request, response, req).then((res) {
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,
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 && app.logger != null) {
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 AngelHttpException.badRequest(message: e.message)
..stackTrace = st;
}
throw AngelHttpException(e,
stackTrace: st,
statusCode: 500,
message: e?.toString() ?? '500 Internal Server Error');
}, test: (e) => e is! AngelHttpException).catchError(
(ee, StackTrace st) {
var e = ee as AngelHttpException;
if (app.logger != null) {
var error = e.error ?? e;
var trace = Trace.from(e.stackTrace ?? StackTrace.current).terse;
app.logger.severe(e.message ?? e.toString(), error, trace);
}
return handleAngelHttpException(
e, e.stackTrace ?? st, req, res, request, response);
});
} else {
var zoneSpec = ZoneSpecification(
print: (self, parent, zone, line) {
if (app.logger != null) {
app.logger.info(line);
} else {
parent.print(zone, line);
}
},
handleUncaughtError: (self, parent, zone, error, stackTrace) {
var trace = Trace.from(stackTrace ?? StackTrace.current).terse;
return Future(() {
AngelHttpException e;
if (error is FormatException) {
e = AngelHttpException.badRequest(message: error.message);
} else if (error is AngelHttpException) {
e = error;
} else {
e = AngelHttpException(error,
stackTrace: stackTrace,
message:
error?.toString() ?? '500 Internal Server Error');
}
if (app.logger != null) {
app.logger.severe(e.message ?? e.toString(), error, trace);
}
return handleAngelHttpException(
e, trace, req, res, request, response);
}).catchError((e, StackTrace st) {
var trace = Trace.from(st ?? StackTrace.current).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.
if (app.logger != null) {
app.logger.severe(
'Fatal error occurred when processing $uri.', e, trace);
} else {
stderr
..writeln('Fatal error occurred when processing '
'${req.uri}:')
..writeln(e)
..writeln(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 [AngelHttpException].
Future handleAngelHttpException(
AngelHttpException e,
StackTrace st,
RequestContext req,
ResponseContext res,
Request request,
Response response,
{bool ignoreFinalizers = false}) {
if (req == null || res == null) {
try {
app.logger?.severe(null, e, st);
setStatusCode(response, 500);
writeStringToResponse(response, '500 Internal Server Error');
closeResponse(response);
} finally {
return null;
}
}
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}) {
Future<void> _cleanup(_) {
if (!app.environment.isProduction &&
app.logger != null &&
req.container.has<Stopwatch>()) {
var sw = req.container.make<Stopwatch>();
app.logger.info(
"${res.statusCode} ${req.method} ${req.uri} (${sw?.elapsedMilliseconds ?? 'unknown'} ms)");
}
return req.close();
}
if (!res.isBuffered) return res.close().then(_cleanup);
Future finalizers = ignoreFinalizers == true
? Future.value()
: Future.forEach(app.responseFinalizers, (f) => f(req, res));
return finalizers.then((_) {
//if (res.isOpen) res.close();
for (var key in res.headers.keys) {
setHeader(response, key, res.headers[key]);
}
setContentLength(response, res.buffer.length);
setChunkedEncoding(response, res.chunked ?? true);
List<int> outputBuffer = res.buffer.toBytes();
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) {
Converter<List<int>, List<int>> encoder;
String key = encodingName;
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);
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;
}
}
}
}
}

View 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';
}

View file

@ -0,0 +1,594 @@
library angel_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.app != null) this.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 params;
} 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() {
_ctrl.forEach((c) => 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);
List<HookedServiceEventListener<Id, Data, T>> before = [], after = [];
if (hooks != null) {
before.addAll(hooks.before.cast());
after.addAll(hooks.after.cast());
}
void applyListeners(
Function fn, HookedServiceEventDispatcher<Id, Data, T> dispatcher,
[bool isAfter]) {
Hooks hooks = getAnnotation<Hooks>(fn, app.container.reflector);
final listeners = <HookedServiceEventListener<Id, Data, T>>[]
..addAll(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);
}
List<RequestHandler> get bootstrappers =>
List<RequestHandler>.from(super.bootstrappers)
..add((RequestContext req, ResponseContext res) {
req.serviceParams
..['__requestctx'] = req
..['__responsectx'] = res;
return true;
});
void addRoutes([Service s]) {
super.addRoutes(s ?? 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 params = _stripReq(_params);
return beforeIndexed
._emit(HookedServiceEvent(false, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.indexed,
params: params))
.then((before) {
if (before._canceled) {
return afterIndexed
._emit(HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.indexed,
params: params, result: before.result))
.then((after) => after.result as List<Data>);
}
return inner.index(params).then((result) {
return afterIndexed
._emit(HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.indexed,
params: params, result: result))
.then((after) => after.result as List<Data>);
});
});
}
@override
Future<Data> read(Id id, [Map<String, dynamic> _params]) {
var params = _stripReq(_params);
return beforeRead
._emit(HookedServiceEvent(false, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.read,
id: id, params: params))
.then((before) {
if (before._canceled) {
return afterRead
._emit(HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.read,
id: id, params: params, result: before.result))
.then((after) => after.result as Data);
}
return inner.read(id, params).then((result) {
return afterRead
._emit(HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.read,
id: id, params: params, result: result))
.then((after) => after.result as Data);
});
});
}
@override
Future<Data> create(Data data, [Map<String, dynamic> _params]) {
var params = _stripReq(_params);
return beforeCreated
._emit(HookedServiceEvent(false, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.created,
data: data, params: params))
.then((before) {
if (before._canceled) {
return afterCreated
._emit(HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.created,
data: before.data, params: params, result: before.result))
.then((after) => after.result as Data);
}
return inner.create(before.data, params).then((result) {
return afterCreated
._emit(HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.created,
data: before.data, params: params, result: result))
.then((after) => after.result as Data);
});
});
}
@override
Future<Data> modify(Id id, Data data, [Map<String, dynamic> _params]) {
var params = _stripReq(_params);
return beforeModified
._emit(HookedServiceEvent(false, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.modified,
id: id, data: data, params: params))
.then((before) {
if (before._canceled) {
return afterModified
._emit(HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.modified,
id: id,
data: before.data,
params: params,
result: before.result))
.then((after) => after.result as Data);
}
return inner.modify(id, before.data, params).then((result) {
return afterModified
._emit(HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.created,
id: id, data: before.data, params: params, result: result))
.then((after) => after.result as Data);
});
});
}
@override
Future<Data> update(Id id, Data data, [Map<String, dynamic> _params]) {
var params = _stripReq(_params);
return beforeUpdated
._emit(HookedServiceEvent(false, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.updated,
id: id, data: data, params: params))
.then((before) {
if (before._canceled) {
return afterUpdated
._emit(HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.updated,
id: id,
data: before.data,
params: params,
result: before.result))
.then((after) => after.result as Data);
}
return inner.update(id, before.data, params).then((result) {
return afterUpdated
._emit(HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.updated,
id: id, data: before.data, params: params, result: result))
.then((after) => after.result as Data);
});
});
}
@override
Future<Data> remove(Id id, [Map<String, dynamic> _params]) {
var params = _stripReq(_params);
return beforeRemoved
._emit(HookedServiceEvent(false, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.removed,
id: id, params: params))
.then((before) {
if (before._canceled) {
return afterRemoved
._emit(HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.removed,
id: id, params: params, result: before.result))
.then((after) => after.result) as Data;
}
return inner.remove(id, params).then((result) {
return afterRemoved
._emit(HookedServiceEvent(true, _getRequest(_params),
_getResponse(_params), inner, HookedServiceEvent.removed,
id: id, params: params, 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;
String _eventName;
Id _id;
bool _isAfter;
Data data;
Map<String, dynamic> _params;
RequestContext _request;
ResponseContext _response;
var 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}) {
_id = id;
_params = params ?? {};
}
}
/// Triggered on a hooked service event.
typedef FutureOr HookedServiceEventListener<Id, Data,
T extends Service<Id, Data>>(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() {
_ctrl.forEach((c) => 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 || event == null || 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);
}
}

View file

@ -0,0 +1,82 @@
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;
var _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 == null) {
throw _formatExc('No hostname parts found after "|".');
} else {
var prev = parts.removeLast();
parts.addLast('(($prev)|($next))');
}
}
} else {
var part = _parseHostnamePart();
if (part != null) {
if (_scanner.scan('.')) {
var subPart = _parseHostnamePart(shouldThrow: false);
while (subPart != null) {
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 null;
}
}
}

View file

@ -0,0 +1,121 @@
import 'dart:async';
import 'package:angel_container/angel_container.dart';
import 'package:angel_route/angel_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 ??= {};
creators ??= {};
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 {
if (req.hostname != null) {
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;
}
}

View file

@ -0,0 +1,207 @@
part of angel_framework.http.request_context;
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 []}) {
InjectionRequest injection;
RequestHandler contained;
return (req, res) {
if (injection == null) {
injection = preInject(handler, req.app.container.reflector);
injection.optional.addAll(optional ?? []);
contained = handleContained(handler, injection);
}
return req.app.executeHandler(contained, req, res);
};
}
resolveInjection(requirement, InjectionRequest injection, RequestContext req,
ResponseContext res, bool throwOnUnresolved,
[Container container]) async {
var 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;
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);
List args = [];
Map<Symbol, dynamic> named = {};
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 _Parameter = reflector.reflectType(Parameter);
var p = parameter.annotations
.firstWhere((m) => m.type.isAssignableTo(_Parameter),
orElse: () => null)
?.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;
}

View file

@ -0,0 +1,175 @@
import 'dart:async';
import 'package:angel_http_exception/angel_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 AngelHttpException.notFound(
message: 'No record found for ID $id')));
}
@override
Future<Map<String, dynamic>> create(Map<String, dynamic> data,
[Map<String, dynamic> params]) {
if (data is! Map) {
throw AngelHttpException.badRequest(
message:
'MapService does not support `create` with ${data.runtimeType}.');
}
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 AngelHttpException.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 AngelHttpException.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 AngelHttpException.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 AngelHttpException.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 AngelHttpException.notFound(
message: 'No record found for ID $id');
}
});
}
}

View file

@ -0,0 +1,179 @@
library angel_framework.http.metadata;
import 'package:angel_http_exception/angel_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(null, method: 'GET'),
post = Expose(null, method: 'POST'),
patch = Expose(null, method: 'PATCH'),
put = Expose(null, method: 'PUT'),
delete = Expose(null, method: 'DELETE'),
head = Expose(null, method: 'HEAD');
const Expose(this.path,
{this.method = "GET",
this.middleware = const [],
this.as,
this.allowNull = const []});
const Expose.method(this.method,
{this.middleware, this.as, this.allowNull = const []})
: path = null;
}
/// 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 match;
/// Specify a default value.
final 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});
/// Returns an error that can be thrown when the parameter is not present.
get error {
if (cookie?.isNotEmpty == true) {
return AngelHttpException.badRequest(
message: 'Missing required cookie "$cookie".');
}
if (header?.isNotEmpty == true) {
return AngelHttpException.badRequest(
message: 'Missing required header "$header".');
}
if (query?.isNotEmpty == true) {
return AngelHttpException.badRequest(
message: 'Missing required query parameter "$query".');
}
if (session?.isNotEmpty == true) {
return StateError('Session does not contain required key "$session".');
}
}
/// Obtains a value for this parameter from a [RequestContext].
getValue(RequestContext req) {
if (cookie?.isNotEmpty == true) {
return req.cookies.firstWhere((c) => c.name == cookie)?.value ??
defaultValue;
}
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, {match, defaultValue, bool required = true})
: super(
header: header,
match: match,
defaultValue: defaultValue,
required: required);
}
/// Shortcut for declaring a request session [Parameter].
class Session extends Parameter {
const Session(String session, {match, defaultValue, bool required = true})
: super(
session: session,
match: match,
defaultValue: defaultValue,
required: required);
}
/// Shortcut for declaring a request query [Parameter].
class Query extends Parameter {
const Query(String query, {match, defaultValue, bool required = true})
: super(
query: query,
match: match,
defaultValue: defaultValue,
required: required);
}
/// Shortcut for declaring a request cookie [Parameter].
class CookieValue extends Parameter {
const CookieValue(String cookie, {match, defaultValue, bool required = true})
: super(
cookie: cookie,
match: match,
defaultValue: defaultValue,
required: required);
}

View file

@ -0,0 +1,350 @@
library angel_framework.http.request_context;
import 'dart:async';
import 'dart:convert';
import 'dart:io'
show
BytesBuilder,
Cookie,
HeaderValue,
HttpHeaders,
HttpSession,
InternetAddress;
import 'package:angel_container/angel_container.dart';
import 'package:http_parser/http_parser.dart';
import 'package:http_server/http_server.dart';
import 'package:meta/meta.dart';
import 'package:mime/mime.dart';
import 'package:path/path.dart' as p;
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 List<FutureOr<void> Function()> shutdownHooks = [];
String _acceptHeaderCache, _extensionCache;
bool _acceptsAllCache, _hasParsedBody = false, _closed = false;
Map<String, dynamic> _bodyFields, _queryParameters;
List _bodyList;
Object _bodyObject;
List<UploadedFile> _uploadedFiles;
MediaType _contentType;
/// 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;
/// All HTTP headers sent with this request.
HttpHeaders get headers;
/// The requested hostname.
String get hostname;
/// 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;
/// The original HTTP verb sent to the server.
String get originalMethod;
/// The content type of an incoming request.
MediaType get contentType =>
_contentType ??= MediaType.parse(headers.contentType.toString());
/// 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.');
} else if (_bodyList == null) {
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) {
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;
if (contentType.type == 'application' && contentType.subtype == 'json') {
_uploadedFiles = [];
var parsed = _bodyObject =
await encoding.decoder.bind(body).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(body)
.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(body).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'];
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, (hook) => hook());
}
}
}
/// Reads information about a binary chunk uploaded to the server.
class UploadedFile {
/// The underlying `form-data` item.
final HttpMultipartFormData formData;
MediaType _contentType;
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()));
/// 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();
}
}

View file

@ -0,0 +1,446 @@
library angel_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:angel_route/angel_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': 'angel',
});
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']);
}
/// 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) {
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.
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.
void json(value) => this
..contentType = MediaType('application', 'json')
..serialize(value);
/// 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 = 302}) {
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 (Route 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;
}
Route 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
List<String> split = action.split("@");
if (split.length < 2) {
throw Exception(
"Controller redirects must take the form of 'Controller@action'. You gave: $action");
}
Controller controller =
app.controllers[split[0].replaceAll(_straySlashes, '')];
if (controller == null) {
throw Exception("Could not find a controller named '${split[0]}'");
}
Route 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, '');
final 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 this
.addStream(file.openRead().cast<List<int>>())
.then((_) => this.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) {
Zone.current.handleUncaughtError(error, stackTrace);
}
}
/// Writes data to the response.
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 extends 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();
}
}

View file

@ -0,0 +1,133 @@
library angel_framework.http.routable;
import 'dart:async';
import 'package:angel_container/angel_container.dart';
import 'package:angel_route/angel_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 FutureOr RequestHandler(RequestContext req, ResponseContext 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 = {};
final Map configuration = {};
final Container _container;
Routable([Reflector reflector])
: _container = reflector == null ? null : Container(reflector),
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;
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}) {
middleware ??= [];
final handlers = <RequestHandler>[];
// Merge @Middleware declaration, if any
var reflector = _container?.reflector;
if (reflector != null && reflector is! ThrowingReflector) {
Middleware 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;
}
}

View file

@ -0,0 +1,389 @@
library angel_framework.http.server;
import 'dart:async';
import 'dart:collection' show HashMap;
import 'dart:convert';
import 'package:angel_container/angel_container.dart';
import 'package:angel_http_exception/angel_http_exception.dart';
import 'package:angel_route/angel_route.dart';
import 'package:combinator/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 in some way.
typedef FutureOr<void> AngelConfigurer(Angel app);
/// A function that asynchronously generates a view from the given path and data.
typedef FutureOr<String> ViewGenerator(String path,
[Map<String, dynamic> data]);
/// A powerful real-time/REST/MVC server class.
class Angel extends Routable {
static ViewGenerator noViewEngineConfigured =
(String view, [Map data]) => '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;
/// Now *deprecated*, in favor of [AngelEnv] and [angelEnv]. Use `app.environment.isProduction`
/// instead.
///
/// Indicates whether the application is running in a production environment.
///
/// The criteria for this is the `ANGEL_ENV` environment variable being set to
/// `'production'`.
///
/// This value is memoized the first time you call it, so do not change environment
/// configuration at runtime!
@deprecated
bool get isProduction => environment.isProduction;
/// 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;
/// 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 [Map] of application-specific data that can be accessed by any
/// piece of code that can see this [Angel] instance.
///
/// Packages like `package:angel_configuration` populate this map
/// for you.
final Map configuration = {};
/// A function that renders views.
///
/// Called by [ResponseContext]@`render`.
ViewGenerator viewGenerator = noViewEngineConfigured;
/// The handler currently configured to run on [AngelHttpException]s.
Function(AngelHttpException e, RequestContext req, ResponseContext res)
errorHandler =
(AngelHttpException e, RequestContext req, ResponseContext res) {
if (!req.accepts('text/html', strict: true) &&
(req.accepts('application/json') ||
req.accepts('application/javascript'))) {
res.json(e.toJson());
return;
}
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 (String error in e.errors) {
res.write("<li>$error</li>");
}
res.write("</ul></body></html>");
res.close();
};
@override
Route<RequestHandler> addRoute(
String method, String path, RequestHandler handler,
{Iterable<RequestHandler> middleware}) {
middleware ??= [];
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
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!
Future close() {
Future.forEach(services.values, (Service service) {
service.close();
});
super.close();
viewGenerator = noViewEngineConfigured;
_preContained.clear();
handlerCache.clear();
encoders.clear();
//_serializer = json.encode;
_children.clear();
_parent = null;
logger = null;
startupHooks.clear();
shutdownHooks.clear();
responseFinalizers.clear();
_flattened = null;
return Future.value();
}
@override
void dumpTree(
{callback(String tree),
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.
findProperty(key) {
if (configuration.containsKey(key)) return configuration[key];
return parent != null ? parent.findProperty(key) : 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]) {
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;
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 =
ThrowingReflector.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,
this.logger,
this.allowMethodOverrides = true,
this.serializer,
this.viewGenerator})
: super(reflector) {
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;
logger?.warning(msg);
}
bootstrapContainer();
viewGenerator ??= noViewEngineConfigured;
serializer ??= json.encode;
}
}

View file

@ -0,0 +1,373 @@
library angel_framework.http.service;
import 'dart:async';
import 'package:angel_http_exception/angel_http_exception.dart';
import 'package:merge_map/merge_map.dart';
import 'package:quiver_hashcode/hashcode.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;
/// Closes this service, including any database connections or stream controllers.
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 ??
(req, res) {
if (req.bodyAsObject is! Data) {
throw AngelHttpException.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 `AngelHttpException` 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 == null) {
throw AngelHttpException.notFound(message: errorMessage);
} else {
if (result.isEmpty) {
throw AngelHttpException.notFound(message: errorMessage);
} else {
return result.first;
}
}
});
}
/// Retrieves all resources.
Future<List<Data>> index([Map<String, dynamic> params]) {
throw AngelHttpException.methodNotAllowed();
}
/// Retrieves the desired resource.
Future<Data> read(Id id, [Map<String, dynamic> params]) {
throw AngelHttpException.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 AngelHttpException.methodNotAllowed();
}
/// Modifies a resource.
Future<Data> modify(Id id, Data data, [Map<String, dynamic> params]) {
throw AngelHttpException.methodNotAllowed();
}
/// Overwrites a resource.
Future<Data> update(Id id, Data data, [Map<String, dynamic> params]) {
throw AngelHttpException.methodNotAllowed();
}
/// Removes the given resource.
Future<Data> remove(Id id, [Map<String, dynamic> params]) {
throw AngelHttpException.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;
} 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
Middleware before =
getAnnotation<Middleware>(service, app.container.reflector);
if (before != null) handlers.addAll(before.handlers);
Middleware indexMiddleware =
getAnnotation<Middleware>(service.index, app.container.reflector);
get('/', (req, res) {
return this.index(mergeMap([
{'query': req.queryParameters},
restProvider,
req.serviceParams
]));
},
middleware: <RequestHandler>[]
..addAll(handlers)
..addAll((indexMiddleware == null) ? [] : indexMiddleware.handlers));
Middleware createMiddleware =
getAnnotation<Middleware>(service.create, app.container.reflector);
post('/', (req, ResponseContext res) {
return req.parseBody().then((_) async {
return await this
.create(
await readData(req, res),
mergeMap([
{'query': req.queryParameters},
restProvider,
req.serviceParams
]))
.then((r) {
res.statusCode = 201;
return r;
});
});
},
middleware: []
..addAll(handlers)
..addAll(
(createMiddleware == null) ? [] : createMiddleware.handlers));
Middleware readMiddleware =
getAnnotation<Middleware>(service.read, app.container.reflector);
get('/:id', (req, res) {
return this.read(
parseId<Id>(req.params['id']),
mergeMap([
{'query': req.queryParameters},
restProvider,
req.serviceParams
]));
},
middleware: []
..addAll(handlers)
..addAll((readMiddleware == null) ? [] : readMiddleware.handlers));
Middleware modifyMiddleware =
getAnnotation<Middleware>(service.modify, app.container.reflector);
patch('/:id', (req, res) {
return req.parseBody().then((_) async {
return await this.modify(
parseId<Id>(req.params['id']),
await readData(req, res),
mergeMap([
{'query': req.queryParameters},
restProvider,
req.serviceParams
]));
});
},
middleware: []
..addAll(handlers)
..addAll(
(modifyMiddleware == null) ? [] : modifyMiddleware.handlers));
Middleware updateMiddleware =
getAnnotation<Middleware>(service.update, app.container.reflector);
post('/:id', (req, res) {
return req.parseBody().then((_) async {
return await this.update(
parseId<Id>(req.params['id']),
await readData(req, res),
mergeMap([
{'query': req.queryParameters},
restProvider,
req.serviceParams
]));
});
},
middleware: []
..addAll(handlers)
..addAll(
(updateMiddleware == null) ? [] : updateMiddleware.handlers));
put('/:id', (req, res) {
return req.parseBody().then((_) async {
return await this.update(
parseId<Id>(req.params['id']),
await readData(req, res),
mergeMap([
{'query': req.queryParameters},
restProvider,
req.serviceParams
]));
});
},
middleware: []
..addAll(handlers)
..addAll(
(updateMiddleware == null) ? [] : updateMiddleware.handlers));
Middleware removeMiddleware =
getAnnotation<Middleware>(service.remove, app.container.reflector);
delete('/', (req, res) {
return this.remove(
null,
mergeMap([
{'query': req.queryParameters},
restProvider,
req.serviceParams
]));
},
middleware: []
..addAll(handlers)
..addAll(
(removeMiddleware == null) ? [] : removeMiddleware.handlers));
delete('/:id', (req, res) {
return this.remove(
parseId<Id>(req.params['id']),
mergeMap([
{'query': req.queryParameters},
restProvider,
req.serviceParams
]));
},
middleware: []
..addAll(handlers)
..addAll(
(removeMiddleware == null) ? [] : removeMiddleware.handlers));
// REST compliance
put('/', (req, res) => throw AngelHttpException.notFound());
patch('/', (req, res) => throw AngelHttpException.notFound());
}
/// Invoked when this service is wrapped within a [HookedService].
void onHooked(HookedService hookedService) {}
}

View file

@ -0,0 +1,10 @@
final Map<Symbol, String> _cache = {};
String fastNameFromSymbol(Symbol s) {
return _cache.putIfAbsent(s, () {
String str = s.toString();
int open = str.indexOf('"');
int close = str.lastIndexOf('"');
return str.substring(open + 1, close);
});
}

View file

@ -0,0 +1,131 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io'
show
Cookie,
HttpRequest,
HttpResponse,
HttpServer,
Platform,
SecurityContext;
import 'package:angel_framework/angel_framework.dart';
import '../core/core.dart';
import 'http_request_context.dart';
import 'http_response_context.dart';
final RegExp _straySlashes = RegExp(r'(^/+)|(/+$)');
/// Adapts `dart:io`'s [HttpServer] to serve Angel.
class AngelHttp extends Driver<HttpRequest, HttpResponse, HttpServer,
HttpRequestContext, HttpResponseContext> {
@override
Uri get uri => server == null
? Uri()
: Uri(scheme: 'http', host: server.address.address, port: server.port);
AngelHttp._(Angel app,
Future<HttpServer> Function(dynamic, int) serverGenerator, bool useZone)
: super(app, serverGenerator, 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, Future<HttpServer> Function(dynamic, int) serverGenerator,
{bool useZone = true}) {
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);
}
/// Use [server] instead.
@deprecated
HttpServer get httpServer => server;
Future handleRequest(HttpRequest request) =>
handleRawRequest(request, request.response);
@override
void addCookies(HttpResponse response, Iterable<Cookie> cookies) =>
response.cookies.addAll(cookies);
@override
Future<HttpServer> close() async {
await server?.close();
return await super.close();
}
@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]) {
return Future<HttpResponseContext>.value(
HttpResponseContext(response, app, correspondingRequest)
..serializer = (app.serializer ?? json.encode)
..encoders.addAll(app.encoders ?? {}));
}
@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);
}

View 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 ?? 0, shared: true);
Future<HttpServer> Function(dynamic, int) startSharedSecure(
SecurityContext securityContext) {
return (address, int port) => HttpServer.bindSecure(
address ?? '127.0.0.1', port ?? 0, securityContext,
shared: true);
}

View file

@ -0,0 +1,136 @@
import 'dart:async';
import 'dart:io';
import 'package:angel_container/angel_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;
HttpRequest _io;
String _override, _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');
}
/// The underlying [HttpRequest] instance underneath this context.
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;
}
@override
HttpSession get session {
return rawRequest.session;
}
@override
Uri get uri {
return rawRequest.uri;
}
/// Magically transforms an [HttpRequest] into a [RequestContext].
static Future<HttpRequestContext> from(
HttpRequest request, Angel app, String path) {
HttpRequestContext ctx = HttpRequestContext()
.._container = app.container.createChild();
String 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
? null
: MediaType.parse(request.headers.contentType.toString());
ctx._override = override;
/*
// Faster way to get path
List<int> _path = [];
// Go up until we reach a ?
for (int ch in request.uri.toString().codeUnits) {
if (ch != $question)
_path.add(ch);
else
break;
}
// Remove trailing slashes
int lastSlash = -1;
for (int i = _path.length - 1; i >= 0; i--) {
if (_path[i] == $slash)
lastSlash = i;
else
break;
}
if (lastSlash > -1)
ctx._path = String.fromCharCodes(_path.take(lastSlash));
else
ctx._path = String.fromCharCodes(_path);
*/
ctx._path = path;
ctx._io = request;
return Future.value(ctx);
}
@override
Future close() {
_contentType = null;
_io = null;
_override = _path = null;
return super.close();
}
}

View file

@ -0,0 +1,216 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
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;
Angel app;
LockableBytesBuilder _buffer;
final HttpRequestContext _correspondingRequest;
bool _isDetached = false, _isClosed = false, _streamInitialized = false;
HttpResponseContext(this.rawResponse, this.app, [this._correspondingRequest]);
@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);
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;
String 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();
Stream<List<int>> output = stream;
if (encoders.isNotEmpty && correspondingRequest != null) {
if (_allowedEncodings != null) {
for (var encodingName in _allowedEncodings) {
Converter<List<int>, List<int>> encoder;
String 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;
String 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();
}
}

View file

@ -0,0 +1,237 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:angel_framework/angel_framework.dart' hide Header;
import 'package:angel_framework/http.dart';
import 'package:http2/transport.dart';
import 'package:mock_request/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;
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> serverGenerator(
address, int port, SecurityContext ctx),
{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<SecureServerSocket> 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 {
(_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>();
_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);
},
);
}
InternetAddress get address => socket.address;
int get port => socket.port;
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);
}
}

View file

@ -0,0 +1,187 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:angel_container/src/container.dart';
import 'package:angel_framework/angel_framework.dart';
import 'package:http2/transport.dart';
import 'package:mock_request/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();
final Container container;
List<Cookie> _cookies;
HttpHeaders _headers;
String _method, _override, _path;
HttpSession _session;
Socket _socket;
ServerTransportStream _stream;
Uri _uri;
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.firstWhere((c) => c.name == 'DARTSESSID', orElse: () => null);
if (dartSessId == null) {
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;
}
@override
String get method {
return _override ?? _method;
}
@override
String get hostname => _headers.value('host');
@override
HttpHeaders get headers => _headers;
@override
Future close() {
_body.close();
return super.close();
}
@override
ServerTransportStream get rawRequest => _stream;
}

View file

@ -0,0 +1,231 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:angel_framework/angel_framework.dart' hide Header;
import 'package:http2/transport.dart';
import 'http2_request_context.dart';
class Http2ResponseContext extends ResponseContext<ServerTransportStream> {
final Angel app;
final ServerTransportStream stream;
ServerTransportStream get rawResponse => stream;
LockableBytesBuilder _buffer;
final Http2RequestContext _req;
bool _isDetached = false,
_isClosed = false,
_streamInitialized = false,
_isPush = false;
Uri _targetUri;
Http2ResponseContext(this.app, this.stream, this._req) {
_targetUri = _req.uri;
}
final List<Http2ResponseContext> _pushes = [];
/// Returns `true` if an attempt to [push] a resource will succeed.
///
/// See [ServerTransportStream].`push`.
bool get canPush => stream.canPush;
/// Returns a [List] of all resources that have [push]ed to the client.
List<Http2ResponseContext> get pushes => List.unmodifiable(_pushes);
@override
ServerTransportStream detach() {
_isDetached = true;
return stream;
}
@override
RequestContext get correspondingRequest => _req;
Uri get targetUri => _targetUri;
@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]) {
super.addError(error, stackTrace);
}
@override
void useBuffer() {
_buffer = LockableBytesBuilder();
}
/// Write headers, status, etc. to the underlying [stream].
bool _openStream() {
if (_isPush || _streamInitialized) return false;
var headers = <Header>[
Header.ascii(':status', statusCode.toString()),
];
if (encoders.isNotEmpty && correspondingRequest != null) {
if (_allowedEncodings != null) {
for (var encodingName in _allowedEncodings) {
Converter<List<int>, List<int>> encoder;
String key = encodingName;
if (encoders.containsKey(encodingName)) {
encoder = encoders[encodingName];
} else if (encodingName == '*') {
encoder = encoders[key = encoders.keys.first];
}
if (encoder != null) {
this.headers['content-encoding'] = key;
break;
}
}
}
}
// Add all normal headers
for (var key in this.headers.keys) {
headers.add(Header.ascii(key.toLowerCase(), this.headers[key]));
}
// Persist session ID
cookies.add(Cookie('DARTSESSID', _req.session.id));
// Send all cookies
for (var cookie in cookies) {
headers.add(Header.ascii('set-cookie', cookie.toString()));
}
stream.sendHeaders(headers);
return _streamInitialized = true;
}
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
Future addStream(Stream<List<int>> stream) {
if (!isOpen && isBuffered) throw ResponseContext.closed();
_openStream();
Stream<List<int>> output = stream;
if (encoders.isNotEmpty && correspondingRequest != null) {
if (_allowedEncodings != null) {
for (var encodingName in _allowedEncodings) {
Converter<List<int>, List<int>> encoder;
String 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 output.forEach(this.stream.sendData);
}
@override
void add(List<int> data) {
if (!isOpen && isBuffered) {
throw ResponseContext.closed();
} else if (!isBuffered) {
_openStream();
if (!_isClosed) {
if (encoders.isNotEmpty && correspondingRequest != null) {
if (_allowedEncodings != null) {
for (var encodingName in _allowedEncodings) {
Converter<List<int>, List<int>> encoder;
String 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;
}
}
}
}
stream.sendData(data);
}
} else {
buffer.add(data);
}
}
@override
Future close() async {
if (!_isDetached && !_isClosed && !isBuffered) {
_openStream();
await stream.outgoingMessages.close();
}
_isClosed = true;
await super.close();
}
/// Pushes a resource to the client.
Http2ResponseContext push(String path,
{Map<String, String> headers = const {}, String method = 'GET'}) {
var targetUri = _req.uri.replace(path: path);
var h = <Header>[
Header.ascii(':authority', targetUri.authority),
Header.ascii(':method', method),
Header.ascii(':path', targetUri.path),
Header.ascii(':scheme', targetUri.scheme),
];
for (var key in headers.keys) {
h.add(Header.ascii(key, headers[key]));
}
var s = stream.push(h);
var r = Http2ResponseContext(app, s, _req)
.._isPush = true
.._targetUri = targetUri;
_pushes.add(r);
return r;
}
}

View file

@ -0,0 +1,123 @@
import 'dart:async';
typedef void _InitCallback();
/// A [StreamController] boilerplate that prevents memory leaks.
abstract class SafeCtrl<T> {
factory SafeCtrl() => _SingleSafeCtrl();
factory SafeCtrl.broadcast() => _BroadcastSafeCtrl();
Stream<T> get stream;
void add(T event);
void addError(error, [StackTrace stackTrace]);
Future close();
void whenInitialized(void callback());
}
class _SingleSafeCtrl<T> implements SafeCtrl<T> {
StreamController<T> _stream;
bool _hasListener = false, _initialized = false;
_InitCallback _initializer;
_SingleSafeCtrl() {
_stream = StreamController<T>(onListen: () {
_hasListener = true;
if (!_initialized && _initializer != null) {
_initializer();
_initialized = true;
}
}, onPause: () {
_hasListener = false;
}, onResume: () {
_hasListener = true;
}, onCancel: () {
_hasListener = false;
});
}
@override
Stream<T> get stream => _stream.stream;
@override
void add(T event) {
if (_hasListener) _stream.add(event);
}
@override
void addError(error, [StackTrace stackTrace]) {
if (_hasListener) _stream.addError(error, stackTrace);
}
@override
Future close() {
return _stream.close();
}
@override
void whenInitialized(void callback()) {
if (!_initialized) {
if (!_hasListener) {
_initializer = callback;
} else {
_initializer();
_initialized = true;
}
}
}
}
class _BroadcastSafeCtrl<T> implements SafeCtrl<T> {
StreamController<T> _stream;
int _listeners = 0;
bool _initialized = false;
_InitCallback _initializer;
_BroadcastSafeCtrl() {
_stream = StreamController<T>.broadcast(onListen: () {
_listeners++;
if (!_initialized && _initializer != null) {
_initializer();
_initialized = true;
}
}, onCancel: () {
_listeners--;
});
}
@override
Stream<T> get stream => _stream.stream;
@override
void add(T event) {
if (_listeners > 0) _stream.add(event);
}
@override
void addError(error, [StackTrace stackTrace]) {
if (_listeners > 0) _stream.addError(error, stackTrace);
}
@override
Future close() {
return _stream.close();
}
@override
void whenInitialized(void callback()) {
if (!_initialized) {
if (_listeners <= 0) {
_initializer = callback;
} else {
_initializer();
_initialized = true;
}
}
}
}

View file

@ -0,0 +1,27 @@
import 'package:angel_container/angel_container.dart';
final RegExp straySlashes = RegExp(r'(^/+)|(/+$)');
T matchingAnnotation<T>(List<ReflectedInstance> metadata) {
for (ReflectedInstance metaDatum in metadata) {
if (metaDatum.type.reflectedType == T) {
return metaDatum.reflectee as T;
}
}
return null;
}
T getAnnotation<T>(obj, Reflector reflector) {
if (reflector == null) {
return null;
} else {
if (obj is Function) {
var methodMirror = reflector.reflectFunction(obj);
return matchingAnnotation<T>(methodMirror.annotations);
} else {
var classMirror = reflector.reflectClass(obj.runtimeType as Type);
return matchingAnnotation<T>(classMirror.annotations);
}
}
}

View file

@ -0,0 +1,62 @@
# Angel Results
5 consecutive trials run on a Windows 10 box with 4GB RAM, and several programs open in the background.
Setup:
* Angel framework `1.0.8`
* Running `wrk` 4.0.2.2
* 2 threads
* 256 connections
* 30 seconds
Average:
* `11070.18` req/sec
* `11.86` ms latency
```
tobe@LAPTOP-VBHCSVRH:/mnt/c/Users/thosa$ wrk -c 256 -d 30 -t 2 http://localhost:3000
Running 30s test @ http://localhost:3000
2 threads and 256 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 12.23ms 7.56ms 206.05ms 93.09%
Req/Sec 5.48k 761.94 7.18k 87.50%
324822 requests in 30.06s, 62.88MB read
Requests/sec: 10806.24
Transfer/sec: 2.09MB
tobe@LAPTOP-VBHCSVRH:/mnt/c/Users/thosa$ wrk -c 256 -d 30 -t 2 http://localhost:3000
Running 30s test @ http://localhost:3000
2 threads and 256 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 11.06ms 4.88ms 134.86ms 78.68%
Req/Sec 5.98k 539.40 7.50k 91.40%
356355 requests in 30.11s, 68.99MB read
Requests/sec: 11836.11
Transfer/sec: 2.29MB
tobe@LAPTOP-VBHCSVRH:/mnt/c/Users/thosa$ wrk -c 256 -d 30 -t 2 http://localhost:3000
Running 30s test @ http://localhost:3000
2 threads and 256 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 12.03ms 6.18ms 159.93ms 87.89%
Req/Sec 5.52k 0.88k 7.32k 90.31%
327749 requests in 30.06s, 63.45MB read
Requests/sec: 10901.35
Transfer/sec: 2.11MB
tobe@LAPTOP-VBHCSVRH:/mnt/c/Users/thosa$ wrk -c 256 -d 30 -t 2 http://localhost:3000
Running 30s test @ http://localhost:3000
2 threads and 256 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 12.92ms 7.06ms 189.00ms 82.48%
Req/Sec 5.12k 1.00k 6.42k 75.59%
302273 requests in 30.05s, 58.52MB read
Requests/sec: 10059.96
Transfer/sec: 1.95MB
tobe@LAPTOP-VBHCSVRH:/mnt/c/Users/thosa$ wrk -c 256 -d 30 -t 2 http://localhost:3000
Running 30s test @ http://localhost:3000
2 threads and 256 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 11.05ms 4.92ms 104.90ms 69.57%
Req/Sec 5.95k 0.87k 7.65k 76.80%
352798 requests in 30.03s, 68.30MB read
Requests/sec: 11747.23
Transfer/sec: 2.27MB
tobe@LAPTOP-VBHCSVRH:/mnt/c/Users/thosa$
```

View file

@ -0,0 +1,23 @@
/// A basic server that prints "Hello, world!"
library performance.hello;
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
main() async {
var app = Angel();
var http = AngelHttp.custom(app, startShared, useZone: false);
app.get('/', (req, res) => res.write('Hello, world!'));
app.optimizeForProduction(force: true);
var oldHandler = app.errorHandler;
app.errorHandler = (e, req, res) {
print('Oops: ${e.error ?? e}');
print(e.stackTrace);
return oldHandler(e, req, res);
};
await http.startServer('127.0.0.1', 3000);
print('Listening at ${http.uri}');
}

View file

@ -0,0 +1,18 @@
/// A basic server that prints "Hello, world!"
library performance.hello;
import 'dart:io';
main() {
return HttpServer.bind('127.0.0.1', 3000, shared: true).then((server) {
print('Listening at http://${server.address.address}:${server.port}');
server.listen((request) {
if (request.uri.path == '/') {
request.response.write('Hello, world!');
}
request.response.close();
});
});
}

View file

@ -0,0 +1,60 @@
# `dart:io` Results
5 consecutive trials run on a Windows 10 box with 4GB RAM, and several programs open in the background.
Setup:
* Running `wrk` 4.0.2.2
* 2 threads
* 256 connections
* 30 seconds
Average:
* `14598.16` req/sec
* `8.88` ms latency
```
tobe@LAPTOP-VBHCSVRH:/mnt/c/Users/thosa$ wrk -c 256 -d 30 -t 2 http://localhost:3000
Running 30s test @ http://localhost:3000
2 threads and 256 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 9.67ms 8.19ms 202.28ms 96.17%
Req/Sec 7.15k 1.47k 9.97k 73.76%
417716 requests in 30.07s, 82.06MB read
Requests/sec: 13892.50
Transfer/sec: 2.73MB
tobe@LAPTOP-VBHCSVRH:/mnt/c/Users/thosa$ wrk -c 256 -d 30 -t 2 http://localhost:3000
Running 30s test @ http://localhost:3000
2 threads and 256 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 8.47ms 3.14ms 100.77ms 65.40%
Req/Sec 7.61k 670.47 8.85k 73.88%
453301 requests in 30.07s, 89.05MB read
Requests/sec: 15077.15
Transfer/sec: 2.96MB
tobe@LAPTOP-VBHCSVRH:/mnt/c/Users/thosa$ wrk -c 256 -d 30 -t 2 http://localhost:3000
Running 30s test @ http://localhost:3000
2 threads and 256 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 8.62ms 3.51ms 73.34ms 63.74%
Req/Sec 7.52k 650.22 8.91k 79.17%
448445 requests in 30.07s, 88.10MB read
Requests/sec: 14911.53
Transfer/sec: 2.93MB
tobe@LAPTOP-VBHCSVRH:/mnt/c/Users/thosa$ wrk -c 256 -d 30 -t 2 http://localhost:3000
Running 30s test @ http://localhost:3000
2 threads and 256 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 8.75ms 3.51ms 70.50ms 64.53%
Req/Sec 7.41k 825.50 10.23k 72.24%
441338 requests in 30.09s, 86.70MB read
Requests/sec: 14665.62
Transfer/sec: 2.88MB
tobe@LAPTOP-VBHCSVRH:/mnt/c/Users/thosa$ wrk -c 256 -d 30 -t 2 http://localhost:3000
Running 30s test @ http://localhost:3000
2 threads and 256 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 8.90ms 3.62ms 78.36ms 66.71%
Req/Sec 7.31k 742.11 10.79k 77.84%
434674 requests in 30.09s, 85.39MB read
Requests/sec: 14443.98
Transfer/sec: 2.84MB
```

Some files were not shown because too many files have changed in this diff Show more