Add 'framework/' from commit '64d6729def5ffcf225a2c8f74fdf115218df1c56'
git-subtree-dir: framework git-subtree-mainline:6f6510ab27
git-subtree-split:64d6729def
This commit is contained in:
commit
609d06f66b
136 changed files with 12340 additions and 0 deletions
66
framework/.gitignore
vendored
Normal file
66
framework/.gitignore
vendored
Normal 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
|
||||||
|
# Don’t 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
|
451
framework/.idea/dbnavigator.xml
Normal file
451
framework/.idea/dbnavigator.xml
Normal 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>
|
6
framework/.idea/encodings.xml
Normal file
6
framework/.idea/encodings.xml
Normal 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>
|
7
framework/.idea/jsLibraryMappings.xml
Normal file
7
framework/.idea/jsLibraryMappings.xml
Normal 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>
|
525
framework/.idea/libraries/Dart_Packages.xml
Normal file
525
framework/.idea/libraries/Dart_Packages.xml
Normal 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>
|
27
framework/.idea/libraries/Dart_SDK.xml
Normal file
27
framework/.idea/libraries/Dart_SDK.xml
Normal 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
4
framework/.idea/misc.xml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="PhpWorkspaceProjectConfiguration" backward_compatibility_performed="true" />
|
||||||
|
</project>
|
8
framework/.idea/modules.xml
Normal file
8
framework/.idea/modules.xml
Normal 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>
|
8
framework/.idea/runConfigurations/All_Tests.xml
Normal file
8
framework/.idea/runConfigurations/All_Tests.xml
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
6
framework/.idea/runConfigurations/Controller_Tests.xml
Normal file
6
framework/.idea/runConfigurations/Controller_Tests.xml
Normal 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>
|
6
framework/.idea/runConfigurations/DI_Tests.xml
Normal file
6
framework/.idea/runConfigurations/DI_Tests.xml
Normal 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>
|
6
framework/.idea/runConfigurations/Hooked_Tests.xml
Normal file
6
framework/.idea/runConfigurations/Hooked_Tests.xml
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
6
framework/.idea/runConfigurations/Routing_Tests.xml
Normal file
6
framework/.idea/runConfigurations/Routing_Tests.xml
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
7
framework/.idea/runConfigurations/handle_error_dart.xml
Normal file
7
framework/.idea/runConfigurations/handle_error_dart.xml
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
7
framework/.idea/runConfigurations/json_dart.xml
Normal file
7
framework/.idea/runConfigurations/json_dart.xml
Normal 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>
|
7
framework/.idea/runConfigurations/main_dart.xml
Normal file
7
framework/.idea/runConfigurations/main_dart.xml
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
8
framework/.idea/runConfigurations/tests_in_framework.xml
Normal file
8
framework/.idea/runConfigurations/tests_in_framework.xml
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
7
framework/.idea/runConfigurations/view_dart.xml
Normal file
7
framework/.idea/runConfigurations/view_dart.xml
Normal 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
6
framework/.idea/vcs.xml
Normal 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>
|
1616
framework/.idea/workspace.xml
Normal file
1616
framework/.idea/workspace.xml
Normal file
File diff suppressed because it is too large
Load diff
6
framework/.travis.yml
Normal file
6
framework/.travis.yml
Normal 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
3
framework/.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// Place your settings in this file to overwrite default and user settings.
|
||||||
|
{
|
||||||
|
}
|
282
framework/CHANGELOG.md
Normal file
282
framework/CHANGELOG.md
Normal 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
21
framework/LICENSE
Normal 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
61
framework/README.md
Normal 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
7
framework/TODO.md
Normal 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
|
15
framework/analysis_options.yaml
Normal file
15
framework/analysis_options.yaml
Normal 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
29
framework/dev.key
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||||
|
MIIE5DAcBgoqhkiG9w0BDAEBMA4ECL7L6rj6uEHGAgIIAASCBMLbucyfqAkgCbhP
|
||||||
|
xNSHYllPMAv/dsIjtnsBwepCXPGkCBCuOAw/2FaCHjN9hBqL5V7fkrKeaemhm2YE
|
||||||
|
ycPtlHJYPDf3kEkyMjdZ9rIY6kePGfQizs2uJPcXj4YPyQ4HsfVXpOicKfQrouf5
|
||||||
|
Mze9bGzeMN065q3iP4dYUMwHAyZYteXCsanQNHlqvsWli0W+H8St8fdsXefZhnv1
|
||||||
|
qVatKWdNdWQ9t5MuljgNU2Vv56sHKEYXI0yLxk2QUMk8KlJfnmt8foYUsnPUXHmc
|
||||||
|
gIjLKwwVkpdololnEHSNu0cEOUPowjgJru+uMpn7vdNl7TPEQ9jbEgdNg4JwoYzU
|
||||||
|
0nao8WzjaSp7kzvZz0VFwKnk5AjstGvvuAWckADdq23QElbn/mF7AG1m/TBpYxzF
|
||||||
|
gTt37UdndS/AcvVznWVVrRP5iTSIawdIwvqI4s7rqsoE0GCcak+RhchgAz2gWKkS
|
||||||
|
oODUo0JL6pPVbJ3l4ebbaO6c99nDVc8dViPtc1EkStJEJ2O4kI4xgLSCr4Y9ahKn
|
||||||
|
oAaoSkX7Xxq3aQm+BzqSpLjdGL8atsqR/YVOIHYIl3gThvP0NfZGx1xHyvO5mCdZ
|
||||||
|
kHxSA7tKWxauZ3eQ2clbnzeRsl4El0WMHy/5K1ovene4v7sunmoXVtghBC8hK6eh
|
||||||
|
zMO9orex2PNQ/VQC7HCvtytunOVx1lkSBoNo7hR70igg6rW9H7UyoAoBOwMpT1xa
|
||||||
|
J6V62nqruTKOqFNfur7aHJGpHGtDb5/ickHeYCyPTvmGp67u4wChzKReeg02oECe
|
||||||
|
d1E5FKAcIa8s9TVOB6Z+HvTRNQZu2PsI6TJnjQRowvY9DAHiWTlJZBBY/pko3hxX
|
||||||
|
TsIeybpvRdEHpDWv86/iqtw1hv9CUxS/8ZTWUgBo+osShHW79FeDASr9FC4/Zn76
|
||||||
|
ZDERTgV4YWlW/klVWcG2lFo7jix+OPXAB+ZQavLhlN1xdWBcIz1AUWjAM4hdPylW
|
||||||
|
HCX4PB9CQIPl2E7F+Y2p6nMcMWSJVBi5UIH7E9LfaBguXSzMmTk2Fw5p1aOQ6wfN
|
||||||
|
goVAMVwi8ppAVs741PfHdZ295xMmK/1LCxz5DeAdD/tsA/SYfT753GotioDuC7im
|
||||||
|
EyJ5JyvTr5I6RFFBuqt3NlUb3Hp16wP3B2x9DZiB6jxr0l341/NHgsyeBXkuIy9j
|
||||||
|
ON2mvpBPCJhS8kgWo3G0UyyKnx64tcgpGuSvZhGwPz843B6AbYyE6pMRfSWRMkMS
|
||||||
|
YZYa+VNKhR4ixdj07ocFZEWLVjCH7kxkE8JZXKt8jKYmkWd0lS1QVjgaKlO6lRa3
|
||||||
|
q6SPJkhW6pvqobvcqVNXwi1XuzpZeEbuh0B7OTekFTTxx5g9XeDl56M8SVQ1KEhT
|
||||||
|
Q1t7H2Nba18WCB7cf+6PN0F0K0Jz1Kq7ZWaqEI/grX1m4RQuvNF5807sB/QKMO/Z
|
||||||
|
Gz3NXvHg5xTJRd/567lxPGkor0cE7qD1EZfmJ2HrBYXQ91bhgA7LToBuMZo6ZRXH
|
||||||
|
QfsanjbP4FPLMiGdQigLjj3A35L/f4sQOOVac/sRaFnm7pzcxsMvyVU/YtvGcjYE
|
||||||
|
xaOOVnamg661Wo0wksXoDjeSz/JIyyKO3Gwp1FSm2wGLjjy/Ehmqcqy8rvHuf07w
|
||||||
|
AUukhVtTNn4=
|
||||||
|
-----END ENCRYPTED PRIVATE KEY-----
|
57
framework/dev.pem
Normal file
57
framework/dev.pem
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDKTCCAhGgAwIBAgIJAOWmjTS+OnTEMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNV
|
||||||
|
BAMMDGludGVybWVkaWF0ZTAeFw0xNTA1MTgwOTAwNDBaFw0yMzA4MDQwOTAwNDBa
|
||||||
|
MBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
|
||||||
|
AQoCggEBALlcwQJuzd+xH8QFgfJSn5tRlvhkldSX98cE7NiA602NBbnAVyUrkRXq
|
||||||
|
Ni75lgt0kwjYfA9z674m8WSVbgpLPintPCla9CYky1TH0keIs8Rz6cGWHryWEHiu
|
||||||
|
EDuljQynu2b3sAFuHu9nfWurbJwZnFakBKpdQ9m4EyOZCHC/jHYY7HacKSXg1Cki
|
||||||
|
we2ca0BWDrcqy8kLy0dZ5oC6IZG8O8drAK8f3f44CRYw59D3sOKBrKXaabpvyEcb
|
||||||
|
N7Wk2HDBVwHpUJo1reVwtbM8dhqQayYSD8oXnGpP3RQNu/e2rzlXRyq/BfcDY1JI
|
||||||
|
7TbC4t/7/N4EcPSpGsTcSOC9A7FpzvECAwEAAaN7MHkwCQYDVR0TBAIwADAsBglg
|
||||||
|
hkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0O
|
||||||
|
BBYEFCnwiEMMFZh7NhCr+qA8K0w4Q+AOMB8GA1UdIwQYMBaAFB0h1Evsaw2vfrmS
|
||||||
|
YuoCTmC4EE6ZMA0GCSqGSIb3DQEBCwUAA4IBAQAcFmHMaXRxyoNaeOowQ6iQWoZd
|
||||||
|
AUbvG7SHr7I6Pi2aqdqofsKWts7Ytm5WsS0M2nN+sW504houu0iCPeJJX8RQw2q4
|
||||||
|
CCcNOs9IXk+2uMzlpocHpv+yYoUiD5DxgWh7eghQMLyMpf8FX3Gy4VazeuXznHOM
|
||||||
|
4gE4L417xkDzYOzqVTp0FTyAPUv6G2euhNCD6TMru9REcRhYul+K9kocjA5tt2KG
|
||||||
|
MH6y28LXbLyq4YJUxSUU9gY/xlnbbZS48KDqEcdYC9zjW9nQ0qS+XQuQuFIcwjJ5
|
||||||
|
V4kAUYxDu6FoTpyQjgsrmBbZlKNxH7Nj4NDlcdJhp/zeSKHqWa5hSWjjKIxp
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDAjCCAeqgAwIBAgIJAOWmjTS+OnTDMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV
|
||||||
|
BAMMDXJvb3RhdXRob3JpdHkwHhcNMTUwNTE4MDkwMDQwWhcNMjMwODA0MDkwMDQw
|
||||||
|
WjAXMRUwEwYDVQQDDAxpbnRlcm1lZGlhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IB
|
||||||
|
DwAwggEKAoIBAQDSrAO1CoPvUllgLOzDm5nG0skDF7vh1DUgAIDVGz0ecD0JFbQx
|
||||||
|
EF79pju/6MbtpTW2FYvRp11t/G7rGtX923ybOHY/1MNFQrdIvPlO1VV7IGKjoMwP
|
||||||
|
DNeb0fIGjHoE9QxaDxR8NX8xQbItpsw+TUtRfc9SLkR+jaYJfVRoM21BOncZbSHE
|
||||||
|
YKiZlEbpecB/+EtwVpgvl+8mPD5U07Fi4fp/lza3WXInXQPyiTVllIEJCt4PKmlu
|
||||||
|
MocNaJOW38bysL7i0PzDpVZtOxLHOTaW68yF3FckIHNCaA7k1ABEEEegjFMmIao7
|
||||||
|
B9w7A0jvr4jZVvNmui5Djjn+oJxwEVVgyf8LAgMBAAGjUDBOMB0GA1UdDgQWBBQd
|
||||||
|
IdRL7GsNr365kmLqAk5guBBOmTAfBgNVHSMEGDAWgBRk81s9d0ZbiZhh44KckwPb
|
||||||
|
oTc0XzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBZQTK0plfdB5PC
|
||||||
|
cC5icut4EmrByJa1RbU7ayuEE70e7hla6KVmVjVdCBGltI4jBYwfhKbRItHiAJ/8
|
||||||
|
x+XZKBG8DLPFuDb7lAa1ObhAYF7YThUFPQYaBhfzKcWrdmWDBFpvNv6E0Mm364dZ
|
||||||
|
e7Yxmbe5S4agkYPoxEzgEYmcUk9jbjdR6eTbs8laG169ljrECXfEU9RiAcqz5iSX
|
||||||
|
NLSewqB47hn3B9qgKcQn+PsgO2j7M+rfklhNgeGJeWmy7j6clSOuCsIjWHU0RLQ4
|
||||||
|
0W3SB/rpEAJ7fgQbYUPTIUNALSOWi/o1tDX2mXPRjBoxqAv7I+vYk1lZPmSzkyRh
|
||||||
|
FKvRDxsW
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDAzCCAeugAwIBAgIJAJ0MomS4Ck+8MA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV
|
||||||
|
BAMMDXJvb3RhdXRob3JpdHkwHhcNMTUwNTE4MDkwMDQwWhcNMjMwODA0MDkwMDQw
|
||||||
|
WjAYMRYwFAYDVQQDDA1yb290YXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOC
|
||||||
|
AQ8AMIIBCgKCAQEAts1ijtBV92S2cOvpUMOSTp9c6A34nIGr0T5Nhz6XiqRVT+gv
|
||||||
|
dQgmkdKJQjbvR60y6jzltYFsI2MpGVXY8h/oAL81D/k7PDB2aREgyBfTPAhBHyGw
|
||||||
|
siR+2xYt5b/Zs99q5RdRqQNzNpLPJriIKvUsRyQWy1UiG2s7pRXQeA8qB0XtJdCj
|
||||||
|
kFIi+G2bDsaffspGeDOCqt7t+yqvRXfSES0c/l7DIHaiMbbp4//ZNML3RNgAjPz2
|
||||||
|
hCezZ+wOYajOIyoSPK8IgICrhYFYxvgWxwbLDBEfC5B3jOQsySe10GoRAKZz1gBV
|
||||||
|
DmgReu81tYJmdgkc9zknnQtIFdA0ex+GvZlfWQIDAQABo1AwTjAdBgNVHQ4EFgQU
|
||||||
|
ZPNbPXdGW4mYYeOCnJMD26E3NF8wHwYDVR0jBBgwFoAUZPNbPXdGW4mYYeOCnJMD
|
||||||
|
26E3NF8wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEATzkZ97K777uZ
|
||||||
|
lQcduNX3ey4IbCiEzFA2zO5Blj+ilfIwNbZXNOgm/lqNvVGDYs6J1apJJe30vL3X
|
||||||
|
J+t2zsZWzzQzb9uIU37zYemt6m0fHrSrx/iy5lGNqt3HMfqEcOqSCOIK3PCTMz2/
|
||||||
|
uyGe1iw33PVeWsm1JUybQ9IrU/huJjbgOHU4wab+8SJCM49ipArp68Fr6j4lcEaE
|
||||||
|
4rfRg1ZsvxiOyUB3qPn6wyL/JB8kOJ+QCBe498376eaem8AEFk0kQRh6hDaWtq/k
|
||||||
|
t6IIXQLjx+EBDVP/veK0UnVhKRP8YTOoV8ZiG1NcdlJmX/Uk7iAfevP7CkBfSN8W
|
||||||
|
r6AL284qtw==
|
||||||
|
-----END CERTIFICATE-----
|
59
framework/example/controller.dart
Normal file
59
framework/example/controller.dart
Normal 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};
|
||||||
|
}
|
||||||
|
}
|
25
framework/example/handle_error.dart
Normal file
25
framework/example/handle_error.dart
Normal 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');
|
||||||
|
}
|
47
framework/example/hostname.dart
Normal file
47
framework/example/hostname.dart
Normal 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.');
|
||||||
|
}
|
46
framework/example/http2/body_parsing.dart
Normal file
46
framework/example/http2/body_parsing.dart
Normal 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}');
|
||||||
|
}
|
7
framework/example/http2/common.dart
Normal file
7
framework/example/http2/common.dart
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
void dumpError(LogRecord rec) {
|
||||||
|
print(rec);
|
||||||
|
if (rec.error != null) print(rec.error);
|
||||||
|
if (rec.stackTrace != null) print(rec.stackTrace);
|
||||||
|
}
|
29
framework/example/http2/dev.key
Normal file
29
framework/example/http2/dev.key
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||||
|
MIIE5DAcBgoqhkiG9w0BDAEBMA4ECL7L6rj6uEHGAgIIAASCBMLbucyfqAkgCbhP
|
||||||
|
xNSHYllPMAv/dsIjtnsBwepCXPGkCBCuOAw/2FaCHjN9hBqL5V7fkrKeaemhm2YE
|
||||||
|
ycPtlHJYPDf3kEkyMjdZ9rIY6kePGfQizs2uJPcXj4YPyQ4HsfVXpOicKfQrouf5
|
||||||
|
Mze9bGzeMN065q3iP4dYUMwHAyZYteXCsanQNHlqvsWli0W+H8St8fdsXefZhnv1
|
||||||
|
qVatKWdNdWQ9t5MuljgNU2Vv56sHKEYXI0yLxk2QUMk8KlJfnmt8foYUsnPUXHmc
|
||||||
|
gIjLKwwVkpdololnEHSNu0cEOUPowjgJru+uMpn7vdNl7TPEQ9jbEgdNg4JwoYzU
|
||||||
|
0nao8WzjaSp7kzvZz0VFwKnk5AjstGvvuAWckADdq23QElbn/mF7AG1m/TBpYxzF
|
||||||
|
gTt37UdndS/AcvVznWVVrRP5iTSIawdIwvqI4s7rqsoE0GCcak+RhchgAz2gWKkS
|
||||||
|
oODUo0JL6pPVbJ3l4ebbaO6c99nDVc8dViPtc1EkStJEJ2O4kI4xgLSCr4Y9ahKn
|
||||||
|
oAaoSkX7Xxq3aQm+BzqSpLjdGL8atsqR/YVOIHYIl3gThvP0NfZGx1xHyvO5mCdZ
|
||||||
|
kHxSA7tKWxauZ3eQ2clbnzeRsl4El0WMHy/5K1ovene4v7sunmoXVtghBC8hK6eh
|
||||||
|
zMO9orex2PNQ/VQC7HCvtytunOVx1lkSBoNo7hR70igg6rW9H7UyoAoBOwMpT1xa
|
||||||
|
J6V62nqruTKOqFNfur7aHJGpHGtDb5/ickHeYCyPTvmGp67u4wChzKReeg02oECe
|
||||||
|
d1E5FKAcIa8s9TVOB6Z+HvTRNQZu2PsI6TJnjQRowvY9DAHiWTlJZBBY/pko3hxX
|
||||||
|
TsIeybpvRdEHpDWv86/iqtw1hv9CUxS/8ZTWUgBo+osShHW79FeDASr9FC4/Zn76
|
||||||
|
ZDERTgV4YWlW/klVWcG2lFo7jix+OPXAB+ZQavLhlN1xdWBcIz1AUWjAM4hdPylW
|
||||||
|
HCX4PB9CQIPl2E7F+Y2p6nMcMWSJVBi5UIH7E9LfaBguXSzMmTk2Fw5p1aOQ6wfN
|
||||||
|
goVAMVwi8ppAVs741PfHdZ295xMmK/1LCxz5DeAdD/tsA/SYfT753GotioDuC7im
|
||||||
|
EyJ5JyvTr5I6RFFBuqt3NlUb3Hp16wP3B2x9DZiB6jxr0l341/NHgsyeBXkuIy9j
|
||||||
|
ON2mvpBPCJhS8kgWo3G0UyyKnx64tcgpGuSvZhGwPz843B6AbYyE6pMRfSWRMkMS
|
||||||
|
YZYa+VNKhR4ixdj07ocFZEWLVjCH7kxkE8JZXKt8jKYmkWd0lS1QVjgaKlO6lRa3
|
||||||
|
q6SPJkhW6pvqobvcqVNXwi1XuzpZeEbuh0B7OTekFTTxx5g9XeDl56M8SVQ1KEhT
|
||||||
|
Q1t7H2Nba18WCB7cf+6PN0F0K0Jz1Kq7ZWaqEI/grX1m4RQuvNF5807sB/QKMO/Z
|
||||||
|
Gz3NXvHg5xTJRd/567lxPGkor0cE7qD1EZfmJ2HrBYXQ91bhgA7LToBuMZo6ZRXH
|
||||||
|
QfsanjbP4FPLMiGdQigLjj3A35L/f4sQOOVac/sRaFnm7pzcxsMvyVU/YtvGcjYE
|
||||||
|
xaOOVnamg661Wo0wksXoDjeSz/JIyyKO3Gwp1FSm2wGLjjy/Ehmqcqy8rvHuf07w
|
||||||
|
AUukhVtTNn4=
|
||||||
|
-----END ENCRYPTED PRIVATE KEY-----
|
57
framework/example/http2/dev.pem
Normal file
57
framework/example/http2/dev.pem
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDKTCCAhGgAwIBAgIJAOWmjTS+OnTEMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNV
|
||||||
|
BAMMDGludGVybWVkaWF0ZTAeFw0xNTA1MTgwOTAwNDBaFw0yMzA4MDQwOTAwNDBa
|
||||||
|
MBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
|
||||||
|
AQoCggEBALlcwQJuzd+xH8QFgfJSn5tRlvhkldSX98cE7NiA602NBbnAVyUrkRXq
|
||||||
|
Ni75lgt0kwjYfA9z674m8WSVbgpLPintPCla9CYky1TH0keIs8Rz6cGWHryWEHiu
|
||||||
|
EDuljQynu2b3sAFuHu9nfWurbJwZnFakBKpdQ9m4EyOZCHC/jHYY7HacKSXg1Cki
|
||||||
|
we2ca0BWDrcqy8kLy0dZ5oC6IZG8O8drAK8f3f44CRYw59D3sOKBrKXaabpvyEcb
|
||||||
|
N7Wk2HDBVwHpUJo1reVwtbM8dhqQayYSD8oXnGpP3RQNu/e2rzlXRyq/BfcDY1JI
|
||||||
|
7TbC4t/7/N4EcPSpGsTcSOC9A7FpzvECAwEAAaN7MHkwCQYDVR0TBAIwADAsBglg
|
||||||
|
hkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0O
|
||||||
|
BBYEFCnwiEMMFZh7NhCr+qA8K0w4Q+AOMB8GA1UdIwQYMBaAFB0h1Evsaw2vfrmS
|
||||||
|
YuoCTmC4EE6ZMA0GCSqGSIb3DQEBCwUAA4IBAQAcFmHMaXRxyoNaeOowQ6iQWoZd
|
||||||
|
AUbvG7SHr7I6Pi2aqdqofsKWts7Ytm5WsS0M2nN+sW504houu0iCPeJJX8RQw2q4
|
||||||
|
CCcNOs9IXk+2uMzlpocHpv+yYoUiD5DxgWh7eghQMLyMpf8FX3Gy4VazeuXznHOM
|
||||||
|
4gE4L417xkDzYOzqVTp0FTyAPUv6G2euhNCD6TMru9REcRhYul+K9kocjA5tt2KG
|
||||||
|
MH6y28LXbLyq4YJUxSUU9gY/xlnbbZS48KDqEcdYC9zjW9nQ0qS+XQuQuFIcwjJ5
|
||||||
|
V4kAUYxDu6FoTpyQjgsrmBbZlKNxH7Nj4NDlcdJhp/zeSKHqWa5hSWjjKIxp
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDAjCCAeqgAwIBAgIJAOWmjTS+OnTDMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV
|
||||||
|
BAMMDXJvb3RhdXRob3JpdHkwHhcNMTUwNTE4MDkwMDQwWhcNMjMwODA0MDkwMDQw
|
||||||
|
WjAXMRUwEwYDVQQDDAxpbnRlcm1lZGlhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IB
|
||||||
|
DwAwggEKAoIBAQDSrAO1CoPvUllgLOzDm5nG0skDF7vh1DUgAIDVGz0ecD0JFbQx
|
||||||
|
EF79pju/6MbtpTW2FYvRp11t/G7rGtX923ybOHY/1MNFQrdIvPlO1VV7IGKjoMwP
|
||||||
|
DNeb0fIGjHoE9QxaDxR8NX8xQbItpsw+TUtRfc9SLkR+jaYJfVRoM21BOncZbSHE
|
||||||
|
YKiZlEbpecB/+EtwVpgvl+8mPD5U07Fi4fp/lza3WXInXQPyiTVllIEJCt4PKmlu
|
||||||
|
MocNaJOW38bysL7i0PzDpVZtOxLHOTaW68yF3FckIHNCaA7k1ABEEEegjFMmIao7
|
||||||
|
B9w7A0jvr4jZVvNmui5Djjn+oJxwEVVgyf8LAgMBAAGjUDBOMB0GA1UdDgQWBBQd
|
||||||
|
IdRL7GsNr365kmLqAk5guBBOmTAfBgNVHSMEGDAWgBRk81s9d0ZbiZhh44KckwPb
|
||||||
|
oTc0XzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBZQTK0plfdB5PC
|
||||||
|
cC5icut4EmrByJa1RbU7ayuEE70e7hla6KVmVjVdCBGltI4jBYwfhKbRItHiAJ/8
|
||||||
|
x+XZKBG8DLPFuDb7lAa1ObhAYF7YThUFPQYaBhfzKcWrdmWDBFpvNv6E0Mm364dZ
|
||||||
|
e7Yxmbe5S4agkYPoxEzgEYmcUk9jbjdR6eTbs8laG169ljrECXfEU9RiAcqz5iSX
|
||||||
|
NLSewqB47hn3B9qgKcQn+PsgO2j7M+rfklhNgeGJeWmy7j6clSOuCsIjWHU0RLQ4
|
||||||
|
0W3SB/rpEAJ7fgQbYUPTIUNALSOWi/o1tDX2mXPRjBoxqAv7I+vYk1lZPmSzkyRh
|
||||||
|
FKvRDxsW
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDAzCCAeugAwIBAgIJAJ0MomS4Ck+8MA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV
|
||||||
|
BAMMDXJvb3RhdXRob3JpdHkwHhcNMTUwNTE4MDkwMDQwWhcNMjMwODA0MDkwMDQw
|
||||||
|
WjAYMRYwFAYDVQQDDA1yb290YXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOC
|
||||||
|
AQ8AMIIBCgKCAQEAts1ijtBV92S2cOvpUMOSTp9c6A34nIGr0T5Nhz6XiqRVT+gv
|
||||||
|
dQgmkdKJQjbvR60y6jzltYFsI2MpGVXY8h/oAL81D/k7PDB2aREgyBfTPAhBHyGw
|
||||||
|
siR+2xYt5b/Zs99q5RdRqQNzNpLPJriIKvUsRyQWy1UiG2s7pRXQeA8qB0XtJdCj
|
||||||
|
kFIi+G2bDsaffspGeDOCqt7t+yqvRXfSES0c/l7DIHaiMbbp4//ZNML3RNgAjPz2
|
||||||
|
hCezZ+wOYajOIyoSPK8IgICrhYFYxvgWxwbLDBEfC5B3jOQsySe10GoRAKZz1gBV
|
||||||
|
DmgReu81tYJmdgkc9zknnQtIFdA0ex+GvZlfWQIDAQABo1AwTjAdBgNVHQ4EFgQU
|
||||||
|
ZPNbPXdGW4mYYeOCnJMD26E3NF8wHwYDVR0jBBgwFoAUZPNbPXdGW4mYYeOCnJMD
|
||||||
|
26E3NF8wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEATzkZ97K777uZ
|
||||||
|
lQcduNX3ey4IbCiEzFA2zO5Blj+ilfIwNbZXNOgm/lqNvVGDYs6J1apJJe30vL3X
|
||||||
|
J+t2zsZWzzQzb9uIU37zYemt6m0fHrSrx/iy5lGNqt3HMfqEcOqSCOIK3PCTMz2/
|
||||||
|
uyGe1iw33PVeWsm1JUybQ9IrU/huJjbgOHU4wab+8SJCM49ipArp68Fr6j4lcEaE
|
||||||
|
4rfRg1ZsvxiOyUB3qPn6wyL/JB8kOJ+QCBe498376eaem8AEFk0kQRh6hDaWtq/k
|
||||||
|
t6IIXQLjx+EBDVP/veK0UnVhKRP8YTOoV8ZiG1NcdlJmX/Uk7iAfevP7CkBfSN8W
|
||||||
|
r6AL284qtw==
|
||||||
|
-----END CERTIFICATE-----
|
43
framework/example/http2/main.dart
Normal file
43
framework/example/http2/main.dart
Normal 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}');
|
||||||
|
}
|
9
framework/example/http2/pretty_logging.dart
Normal file
9
framework/example/http2/pretty_logging.dart
Normal 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());
|
||||||
|
}
|
27
framework/example/http2/public/app.js
Normal file
27
framework/example/http2/public/app.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
window.onload = function() {
|
||||||
|
var $app = document.getElementById('app');
|
||||||
|
var $loading = document.getElementById('loading');
|
||||||
|
$app.removeChild($loading);
|
||||||
|
var $button = document.createElement('button');
|
||||||
|
var $h1 = document.createElement('h1');
|
||||||
|
$app.appendChild($h1);
|
||||||
|
$app.appendChild($button);
|
||||||
|
|
||||||
|
$h1.textContent = '~Angel HTTP/2 server push~';
|
||||||
|
|
||||||
|
$button.textContent = 'Change color';
|
||||||
|
$button.onclick = function() {
|
||||||
|
var color = Math.floor(Math.random() * 0xffffff);
|
||||||
|
$h1.style.color = '#' + color.toString(16);
|
||||||
|
};
|
||||||
|
|
||||||
|
$button.onclick();
|
||||||
|
|
||||||
|
window.setInterval($button.onclick, 2000);
|
||||||
|
|
||||||
|
var rotation = 0;
|
||||||
|
window.setInterval(function() {
|
||||||
|
rotation += .6;
|
||||||
|
$button.style.transform = 'rotate(' + rotation + 'deg)';
|
||||||
|
}, 10);
|
||||||
|
};
|
21
framework/example/http2/public/body_parsing.html
Normal file
21
framework/example/http2/public/body_parsing.html
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Angel HTTP/2</title>
|
||||||
|
<style>
|
||||||
|
input:not([type="submit"]) {
|
||||||
|
margin-bottom: 2em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<form action="/" method="post">
|
||||||
|
<input name="name" placeholder="Your Name" type="text">
|
||||||
|
<input name="password" placeholder="Secret Field" type="password">
|
||||||
|
<input name="age" placeholder="Your Age" type="number">
|
||||||
|
<input name="birthday" placeholder="Your Birthday" type="datetime-local">
|
||||||
|
<input type="submit" value="Submit">
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
12
framework/example/http2/public/index.html
Normal file
12
framework/example/http2/public/index.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Angel HTTP/2</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"><span id="loading">Loading...</span></div>
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
20
framework/example/http2/public/style.css
Normal file
20
framework/example/http2/public/style.css
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
button {
|
||||||
|
margin-top: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
background-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app h1 {
|
||||||
|
font-style: italic;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading {
|
||||||
|
color: red;
|
||||||
|
}
|
62
framework/example/http2/server_push.dart
Normal file
62
framework/example/http2/server_push.dart
Normal 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}');
|
||||||
|
}
|
53
framework/example/json.dart
Normal file
53
framework/example/json.dart
Normal 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}');
|
||||||
|
}
|
59
framework/example/main.dart
Normal file
59
framework/example/main.dart
Normal 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');
|
||||||
|
}
|
22
framework/example/map_service.dart
Normal file
22
framework/example/map_service.dart
Normal 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}');
|
||||||
|
}
|
14
framework/example/status.dart
Normal file
14
framework/example/status.dart
Normal 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}');
|
||||||
|
}
|
18
framework/example/view.dart
Normal file
18
framework/example/view.dart
Normal 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');
|
||||||
|
}
|
9
framework/example/views/index.jl
Normal file
9
framework/example/views/index.jl
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Title</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello!</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
7
framework/lib/angel_framework.dart
Normal file
7
framework/lib/angel_framework.dart
Normal 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
1
framework/lib/http.dart
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export 'src/http/http.dart';
|
3
framework/lib/http2.dart
Normal file
3
framework/lib/http2.dart
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export 'src/http2/angel_http2.dart';
|
||||||
|
export 'src/http2/http2_request_context.dart';
|
||||||
|
export 'src/http2/http2_response_context.dart';
|
59
framework/lib/src/core/anonymous_service.dart
Normal file
59
framework/lib/src/core/anonymous_service.dart
Normal 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));
|
||||||
|
}
|
233
framework/lib/src/core/controller.dart
Normal file
233
framework/lib/src/core/controller.dart
Normal 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, '_')));
|
||||||
|
}
|
||||||
|
}
|
14
framework/lib/src/core/core.dart
Normal file
14
framework/lib/src/core/core.dart
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
export 'anonymous_service.dart';
|
||||||
|
export 'controller.dart';
|
||||||
|
export 'driver.dart';
|
||||||
|
export 'env.dart';
|
||||||
|
export 'hooked_service.dart';
|
||||||
|
export 'hostname_parser.dart';
|
||||||
|
export 'hostname_router.dart';
|
||||||
|
export 'map_service.dart';
|
||||||
|
export 'metadata.dart';
|
||||||
|
export 'request_context.dart';
|
||||||
|
export 'response_context.dart';
|
||||||
|
export 'routable.dart';
|
||||||
|
export 'server.dart';
|
||||||
|
export 'service.dart';
|
372
framework/lib/src/core/driver.dart
Normal file
372
framework/lib/src/core/driver.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
framework/lib/src/core/env.dart
Normal file
27
framework/lib/src/core/env.dart
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
/// A constant instance of [AngelEnv].
|
||||||
|
const AngelEnvironment angelEnv = AngelEnvironment();
|
||||||
|
|
||||||
|
/// Queries the environment's `ANGEL_ENV` value.
|
||||||
|
class AngelEnvironment {
|
||||||
|
final String _customValue;
|
||||||
|
|
||||||
|
/// You can optionally provide a custom value, in order to override the system's
|
||||||
|
/// value.
|
||||||
|
const AngelEnvironment([this._customValue]);
|
||||||
|
|
||||||
|
/// Returns the value of the `ANGEL_ENV` variable; defaults to `'development'`.
|
||||||
|
String get value =>
|
||||||
|
(_customValue ?? Platform.environment['ANGEL_ENV'] ?? 'development')
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
/// Returns whether the [value] is `'development'`.
|
||||||
|
bool get isDevelopment => value == 'development';
|
||||||
|
|
||||||
|
/// Returns whether the [value] is `'production'`.
|
||||||
|
bool get isProduction => value == 'production';
|
||||||
|
|
||||||
|
/// Returns whether the [value] is `'staging'`.
|
||||||
|
bool get isStaging => value == 'staging';
|
||||||
|
}
|
594
framework/lib/src/core/hooked_service.dart
Normal file
594
framework/lib/src/core/hooked_service.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
82
framework/lib/src/core/hostname_parser.dart
Normal file
82
framework/lib/src/core/hostname_parser.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
121
framework/lib/src/core/hostname_router.dart
Normal file
121
framework/lib/src/core/hostname_router.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
207
framework/lib/src/core/injection.dart
Normal file
207
framework/lib/src/core/injection.dart
Normal 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;
|
||||||
|
}
|
175
framework/lib/src/core/map_service.dart
Normal file
175
framework/lib/src/core/map_service.dart
Normal 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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
179
framework/lib/src/core/metadata.dart
Normal file
179
framework/lib/src/core/metadata.dart
Normal 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);
|
||||||
|
}
|
350
framework/lib/src/core/request_context.dart
Normal file
350
framework/lib/src/core/request_context.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
446
framework/lib/src/core/response_context.dart
Normal file
446
framework/lib/src/core/response_context.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
133
framework/lib/src/core/routable.dart
Normal file
133
framework/lib/src/core/routable.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
389
framework/lib/src/core/server.dart
Normal file
389
framework/lib/src/core/server.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
373
framework/lib/src/core/service.dart
Normal file
373
framework/lib/src/core/service.dart
Normal 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) {}
|
||||||
|
}
|
10
framework/lib/src/fast_name_from_symbol.dart
Normal file
10
framework/lib/src/fast_name_from_symbol.dart
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
final Map<Symbol, String> _cache = {};
|
||||||
|
|
||||||
|
String fastNameFromSymbol(Symbol s) {
|
||||||
|
return _cache.putIfAbsent(s, () {
|
||||||
|
String str = s.toString();
|
||||||
|
int open = str.indexOf('"');
|
||||||
|
int close = str.lastIndexOf('"');
|
||||||
|
return str.substring(open + 1, close);
|
||||||
|
});
|
||||||
|
}
|
131
framework/lib/src/http/angel_http.dart
Normal file
131
framework/lib/src/http/angel_http.dart
Normal 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);
|
||||||
|
}
|
19
framework/lib/src/http/http.dart
Normal file
19
framework/lib/src/http/http.dart
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
/// Various libraries useful for creating highly-extensible servers.
|
||||||
|
library angel_framework.http;
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
export 'angel_http.dart';
|
||||||
|
export 'http_request_context.dart';
|
||||||
|
export 'http_response_context.dart';
|
||||||
|
|
||||||
|
/// Boots a shared server instance. Use this if launching multiple isolates.
|
||||||
|
Future<HttpServer> startShared(address, int port) =>
|
||||||
|
HttpServer.bind(address ?? '127.0.0.1', port ?? 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);
|
||||||
|
}
|
136
framework/lib/src/http/http_request_context.dart
Normal file
136
framework/lib/src/http/http_request_context.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
216
framework/lib/src/http/http_response_context.dart
Normal file
216
framework/lib/src/http/http_response_context.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
237
framework/lib/src/http2/angel_http2.dart
Normal file
237
framework/lib/src/http2/angel_http2.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
187
framework/lib/src/http2/http2_request_context.dart
Normal file
187
framework/lib/src/http2/http2_request_context.dart
Normal 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;
|
||||||
|
}
|
231
framework/lib/src/http2/http2_response_context.dart
Normal file
231
framework/lib/src/http2/http2_response_context.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
123
framework/lib/src/safe_stream_controller.dart
Normal file
123
framework/lib/src/safe_stream_controller.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
framework/lib/src/util.dart
Normal file
27
framework/lib/src/util.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
62
framework/performance/hello/angel.md
Normal file
62
framework/performance/hello/angel.md
Normal 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$
|
||||||
|
```
|
23
framework/performance/hello/main.dart
Normal file
23
framework/performance/hello/main.dart
Normal 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}');
|
||||||
|
}
|
18
framework/performance/hello/raw.dart
Normal file
18
framework/performance/hello/raw.dart
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
60
framework/performance/hello/raw.md
Normal file
60
framework/performance/hello/raw.md
Normal 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
Loading…
Reference in a new issue