Merge branch 'angel3' into master
This commit is contained in:
commit
57b45decd1
693 changed files with 19315 additions and 7471 deletions
40
.gitignore
vendored
40
.gitignore
vendored
|
@ -38,37 +38,29 @@ pubspec.lock
|
|||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff:
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/dictionaries
|
||||
.idea/vcs.xml
|
||||
.idea/jsLibraryMappings.xml
|
||||
|
||||
# Sensitive or high-churn files:
|
||||
.idea/dataSources.ids
|
||||
.idea/dataSources.xml
|
||||
.idea/dataSources.local.xml
|
||||
.idea/sqlDataSources.xml
|
||||
.idea/dynamic.xml
|
||||
.idea/uiDesigner.xml
|
||||
## VsCode
|
||||
.vscode/
|
||||
#.vscode/*
|
||||
#!.vscode/settings.json
|
||||
#!.vscode/tasks.json
|
||||
#!.vscode/launch.json
|
||||
#!.vscode/extensions.json
|
||||
|
||||
# IntelliJ
|
||||
.idea/
|
||||
/out/
|
||||
.idea_modules/
|
||||
|
||||
# Gradle:
|
||||
.idea/gradle.xml
|
||||
.idea/libraries
|
||||
|
||||
# Mongo Explorer plugin:
|
||||
.idea/mongoSettings.xml
|
||||
|
||||
## File-based project format:
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
/out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
@ -79,13 +71,7 @@ crashlytics.properties
|
|||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
### VSCode template
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# Others
|
||||
logs/
|
||||
*.pem
|
||||
.DS_Store
|
||||
|
|
12
AUTHORS.md
Normal file
12
AUTHORS.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
Primary Authors
|
||||
===============
|
||||
|
||||
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
|
||||
|
||||
Thomas is the current maintainer of the code base. He has refactored and migrated the
|
||||
code base to support NNBD.
|
||||
|
||||
* __[Tobe O](thosakwe@gmail.com)__
|
||||
|
||||
Tobe has written much of the original code prior to NNBD migration. He has moved on and
|
||||
is no longer involved with the project.
|
99
CHANGELOG.md
99
CHANGELOG.md
|
@ -1,41 +1,78 @@
|
|||
# 4.0.0 (NNBD)
|
||||
* Published all packages with `angel3_` prefix
|
||||
* Changed Dart SDK requirements for all packages to ">=2.12.0 <3.0.0" to support NNBD.
|
||||
* Updated pretty_logging to 2.0.0
|
||||
* Updated angel_http_exception to 2.0.0
|
||||
* Updated angel_cli to 3.0.0. (Rename not working)
|
||||
* Migrated pretty_logging to 3.0.0 (0/0 tests passed)
|
||||
* Migrated angel_http_exception to 3.0.0 (0/0 tests passed)
|
||||
* Moved angel_cli to https://github.com/dukefirehawk/cli (Not migrated yet)
|
||||
* Added code_buffer and migrated to 2.0.0 (16/16 tests passed)
|
||||
* Added combinator and migrated to 2.0.0 (16/16 tests passed)
|
||||
* Migrated angel_route to 5.0.0 (35/35 tests passed)
|
||||
* Migrated angel_model to 3.0.0 (0/0 tests passed)
|
||||
* Migrated angel_container to 3.0.0 (55/55 tests passed)
|
||||
* Added merge_map and migrated to 2.0.0 (6/6 tests passed)
|
||||
* Added mock_request and migrated to 2.0.0 (0/0 tests)
|
||||
* Migrated angel_framework to 4.0.0 (146/150 tests passed)
|
||||
* Migrated angel_auth to 4.0.0 (23/30 tests passed)
|
||||
* Migrated angel_configuration to 4.0.0 (6/8 testspassed)
|
||||
* Migrated angel_validate to 4.0.0 (6/7 tests passed)
|
||||
* Migrated json_god to 4.0.0 (13/13 tests passed)
|
||||
* Migrated angel_client to 4.0.0 (6/13 tests passed)
|
||||
* Migrated angel_websocket to 4.0.0 (2/3 tests passed)
|
||||
* Migrated angel_test to 4.0.0 (1/1 test passed)
|
||||
* Added symbol_table and migrated to 2.0.0 (16/16 tests passed)
|
||||
* Migrated jael to 4.0.0 (20/20 tests passed)
|
||||
* Migrated jael_preprocessor to 3.0.0 (5/5 tests passed)
|
||||
* Migrated angel_jael to 4.0.0 (1/1 test passed)
|
||||
* Migrated pub_sub to 4.0.0 (16/16 tests passed)
|
||||
* Migrated production to 3.0.0 (0/0 tests passed)
|
||||
* Added html_builder and migrated to 2.0.0 (1/1 tests passed)
|
||||
* Migrated hot to 4.0.0 (0/0 tests passed)
|
||||
* Added range_header and migrated to 3.0.0 (12/12 tests passed)
|
||||
* Migrated static to 4.0.0 (11/12 test passed)
|
||||
* Created basic-sdk-2.12.x_nnbd template (1/1 test passed) <= Milestone 1
|
||||
* Migrated angel_serialize to 4.0.0 (0/0 test passed)
|
||||
* Migrated angel_serialize_generator to 4.0.0 (33/33 tests passed)
|
||||
* Migrated angel_orm to 3.0.0 (0/0 tests passed)
|
||||
* Migrated angel_migration to 3.0.0 (0/0 tests passed)
|
||||
* Added inflection2 and migrated to 1.0.0 (28/32 tests passed)
|
||||
* Migrated angel_orm_generator to 4.0.0 (0/0 tests passed)
|
||||
* Migrated angel_migration_runner to 3.0.0 (0/0 tests passed)
|
||||
* Migrated angel_orm_test to 3.0.0 (0/0 tests passed)
|
||||
* Migrated angel_orm_postgres to 3.0.0 (51/54 tests passed)
|
||||
* Create orm-sdk-2.12.x boilerplate (in progress) <= Milestone 2
|
||||
|
||||
# 3.0.0 (Non NNBD)
|
||||
* Changed Dart SDK requirements for all packages to ">=2.10.0 <3.0.0"
|
||||
* Updated pretty_logging to 2.0.0
|
||||
* Updated angel_http_exception to 2.0.0
|
||||
* Updated pretty_logging to 2.0.0 (0/0 tests passed)
|
||||
* Updated angel_http_exception to 2.0.0 (0/0 tests passed)
|
||||
* Updated angel_cli to 3.0.0. (Rename not working)
|
||||
* Updated angel_route to 4.0.0
|
||||
* Updated angel_model to 2.0.0
|
||||
* Updated angel_container to 2.0.0
|
||||
* Updated angel_framework to 3.0.0
|
||||
* Updated angel_auth to 3.0.0
|
||||
* Updated angel_configuration to 3.0.0
|
||||
* Updated jael to 3.0.0
|
||||
* Updated jael_preprocessor to 3.0.0
|
||||
* Updated validate to 3.0.0
|
||||
* Added and updated json_god to 3.0.0
|
||||
* Updated angel_client to 3.0.0
|
||||
* Updated angel_route to 4.0.0 (35/35 tests passed)
|
||||
* Updated angel_model to 2.0.0 (0/0 tests passed)
|
||||
* Updated angel_container to 2.0.0 (55/55 tests passed)
|
||||
* Updated angel_framework to 3.0.0 (151/151 tests passed)
|
||||
* Updated angel_auth to 3.0.0 (28/32 tests passed)
|
||||
* Updated angel_configuration to 3.0.0 (6/8 tests passed)
|
||||
* Updated angel_validate to 3.0.0 (7/7 tests passed)
|
||||
* Added and updated json_god to 3.0.0 (7/7 tests passed)
|
||||
* Updated angel_client to 3.0.0 (10/13 tests passed)
|
||||
* Updated angel_websocket to 3.0.0 (3/3 tests passed)
|
||||
* Updated test to 3.0.0
|
||||
* Updated angel_jael to 3.0.0 (Issue with 2 dependencies)
|
||||
* Added pub_sub and updated to 3.0.0
|
||||
* Updated production to 2.0.0
|
||||
* Updated hot to 3.0.0
|
||||
* Updated static to 3.0.0
|
||||
* Update basic-sdk-2.12.x boilerplate
|
||||
* Updated angel_serialize to 3.0.0
|
||||
* Updated angel_serialize_generator to 3.0.0
|
||||
* Updated angel_orm to 3.0.0
|
||||
* Updated angel_migration to 3.0.0
|
||||
* Updated angel_orm_generator to 3.0.0 (use a fork of postgres)
|
||||
* Updated angel_migration_runner to 3.0.0
|
||||
* Updated angel_orm_test to 1.0.0
|
||||
* Updated angel_orm_postgres to 2.0.0
|
||||
* Updated jael to 3.0.0 (20/20 tests passed)
|
||||
* Updated jael_preprocessor to 3.0.0 (5/5 tests passed)
|
||||
* Updated test to 3.0.0 (1/1 tests passed)
|
||||
* Updated angel_jael to 3.0.0 (1/1 tests passed, Issue with 2 dependencies)
|
||||
* Added pub_sub and updated to 3.0.0 (16/16 tests passed)
|
||||
* Updated production to 2.0.0 (0/0 tests passed)
|
||||
* Updated hot to 3.0.0 (0/0 tests passed)
|
||||
* Updated static to 3.0.0 (12/12 tests passed)
|
||||
* Update basic-sdk-2.12.x boilerplate (1/1 tests passed)
|
||||
* Updated angel_serialize to 3.0.0 (0/0 tests passed)
|
||||
* Updated angel_serialize_generator to 3.0.0 (33/33 tests passed)
|
||||
* Updated angel_orm to 3.0.0 (0/0 tests passed)
|
||||
* Updated angel_migration to 3.0.0 (0/0 tests passed)
|
||||
* Updated angel_orm_generator to 3.0.0 (0/0 tests passed, use a fork of postgres)
|
||||
* Updated angel_migration_runner to 3.0.0 (0/0 tests passed)
|
||||
* Updated angel_orm_test to 1.0.0 (0/0 tests passed)
|
||||
* Updated angel_orm_postgres to 2.0.0 (52/54 tests passed)
|
||||
* Update orm-sdk-2.12.x boilerplate
|
||||
* Updated angel_auth_oauth2 to 3.0.0
|
||||
* Updated angel_auth_cache to 3.0.0
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 angel-dart
|
||||
Copyright (c) 2021 dukefirehawk.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
75
README.md
75
README.md
|
@ -1,64 +1,53 @@
|
|||
[![The Angel Framework](https://angel-dart.github.io/assets/images/logo.png)](https://angel-dart.dev)
|
||||
|
||||
[![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/angel_dart/discussion)
|
||||
[![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?branch=master)](https://travis-ci.org/angel-dart/framework)
|
||||
![License](https://img.shields.io/github/license/angel-dart/framework.svg)
|
||||
[![version](https://img.shields.io/badge/pub-v4.0.1-brightgreen)](https://pub.dartlang.org/packages/framework)
|
||||
|
||||
**A polished, production-ready backend framework in Dart.**
|
||||
[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/LICENSE)
|
||||
|
||||
|
||||
**A polished, production-ready backend framework in Dart with NNBD support.**
|
||||
|
||||
-----
|
||||
## About
|
||||
Angel is a full-stack Web framework in Dart. It aims to
|
||||
streamline development by providing many common features
|
||||
out-of-the-box in a consistent manner.
|
||||
Angel3 is a port of the original Angel framework to support NNBD in Dart SDK 2.12.x and above.
|
||||
It is a full-stack Web framework in Dart that aims to streamline development by providing many common features out-of-the-box in a consistent manner. One of the main goal is to enable developers to build both frontend
|
||||
and backend in the same language, Dart. Angel3 framework is designed as a collection of plugins that enable developers to pick and choose the parts needed for their projects. A series of starter templates are also provided for quick start and trial run with Angel3 framework.
|
||||
|
||||
With features like the following, Angel is the all-in-one framework you should choose to build your next project:
|
||||
* GraphQL Support
|
||||
* PostgreSQL ORM
|
||||
* Dependency Injection
|
||||
The availabe features in Angel3 are:
|
||||
* Static File Handling
|
||||
* Basic Authentication
|
||||
* PostgreSQL ORM
|
||||
* And much more...
|
||||
|
||||
See all the packages in the `packages/` directory.
|
||||
|
||||
## IMPORTANT NOTES
|
||||
This is a port of Angel Framework to work with Dart SDK 2.12.x and above. Dart SDK 2.12.x and below are not supported.
|
||||
The migration of Angel Framework to Angel3 framework is still ongoing. About 35 out of 70++ packages have been migrated and tested to be stable and working as expected. Angel3 framework need more testing to get it to production quality. Hence, the Angel3 stable packages have been published with prefix `angel3_` on `pub.dev`for developers to try out.
|
||||
|
||||
In order to acknowledge contributions, AUTHORS.md has been added to every Angel3 packages. This way no matter what the contributions are, be it code review, testing or submit PR, can all be recorded in this file. If you are the original author of the original Angel packages, feel free to send a PR to update that file.
|
||||
|
||||
|
||||
Branch: master
|
||||
- Same as sdk-2.12.x branch
|
||||
- Stable version of `angel3` branch
|
||||
|
||||
Branch: sdk-2.12.x
|
||||
- Required Dart SDK: ">=2.10.0 <3.0.0"
|
||||
- NNBD Support: No
|
||||
- Status: Beta release
|
||||
- Notes: Not all packages are fully tested. Refer to WIKI page for details. The basic and ORM templates can be found at "https://github.com/dukefirehawk/boilerplates/tree/basic-sdk-2.12.x" and "https://github.com/dukefirehawk/boilerplates/tree/orm-sdk-2.12.x" respectively.
|
||||
Branch: angel3 (Active development)
|
||||
- Dart version : 2.12.x and above. Use sdk: ">=2.12.0 <3.0.0"
|
||||
- Publish : Yes. See all packages with `angel3_` prefix on [pub.dev](https://pub.dev/publishers/dukefirehawk.com/packages).
|
||||
- NNDB Support : Yes
|
||||
- Status : Beta
|
||||
- Notes : Basic and ORM templates are working with the key packages migration completed. Not all packages are fully tested.
|
||||
|
||||
Branch: sdk-2.12.x_nnbd
|
||||
- Required Dart SDK: ">=2.12.0 <3.0.0"
|
||||
- NNBD Support: Yes
|
||||
- Status: Alpha release
|
||||
- Notes: Heavy migration and code refactoring in progress. Refer to WIKI page for details. The basic template can be found at https://github.com/dukefirehawk/boilerplates/tree/basic-sdk-2.12.x_nnbd".
|
||||
Branch: sdk-2.12.x-nnbd (Active development)
|
||||
- Dart version : 2.12.x and above. Use sdk: ">=2.12.0 <3.0.0"
|
||||
- Publish : No (Internal use only)
|
||||
- NNDB Support : Yes
|
||||
- Status : Beta
|
||||
- Notes : Basic and ORM templates are working with key packages migration. Not all packages are fully tested.
|
||||
|
||||
Branch: sdk-2.10.x
|
||||
- Required Dart SDK: ">=2.10.0 <2.12.0"
|
||||
- NNBD support: No
|
||||
- Status: Retired
|
||||
- Notes: Not all packages are fully tested. This branch is the baseline used in migrating the framework to support Dart SDK 2.12.x and beyond. It may still work with Dart SDK 2.10.x but no longer maintained. Do not work with Dart SDK < 2.10.x.
|
||||
For more details, checkout [Project Status](https://github.com/dukefirehawk/angel/wiki/Project-Status)
|
||||
|
||||
### Testing Angel Framework in NNBD mode
|
||||
Creating new project
|
||||
1. Clone `https://github.com/dukefirehawk/boilerplates/tree/basic-sdk-2.12.x_nnbd` project.
|
||||
|
||||
Migrating an existing project to Angel NNBD
|
||||
1. WARNING. Backup your existing code first as the following migration process cannot be reversed.
|
||||
2. Run `dart pub outdated --mode=null-safety`. Make sure all the packages besides "angel_*" are upgradable.
|
||||
3. Update all "angel_*" packages with dependencies in `https://github.com/dukefirehawk/boilerplates/tree/basic-sdk-2.12.x_nnbd/pubspec.yaml` file. Refer to WIKI on the migrated Angel NNBD packages.
|
||||
4. Run `dart pub upgrade --null-safety`.
|
||||
5. Run `dart migrate` to perform the migration.
|
||||
6. Fix and resolve NNDB related warnings and errors.
|
||||
|
||||
## Installation & Setup
|
||||
## Installation & Setup
|
||||
|
||||
Once you have [Dart](https://www.dartlang.org/) installed, bootstrapping a project is as simple as running a few shell commands:
|
||||
|
||||
|
@ -88,6 +77,10 @@ pub global activate --source path ./packages/cli
|
|||
|
||||
Next, check out the [detailed documentation](https://docs.angel-dart.dev/v/2.x) to learn to flesh out your project.
|
||||
|
||||
### Migrating to Angel3 Framework
|
||||
|
||||
Checkout [Migrating from Angel to Angel3](https://github.com/dukefirehawk/angel/wiki/Migrating-from-Angel-to-Angel3)
|
||||
|
||||
## Examples and Documentation
|
||||
Visit the [documentation](https://docs.angel-dart.dev/v/2.x)
|
||||
for dozens of guides and resources, including video tutorials,
|
||||
|
|
4
TODO.md
4
TODO.md
|
@ -1,5 +1,5 @@
|
|||
# Todo
|
||||
|
||||
### angel_framework
|
||||
* Migrate http_server to shelf
|
||||
### Container/angel_container_generator
|
||||
|
||||
* test/reflector_test.reflectab.dart - Changed ImplicitGetterMirrorImpl() from 5 to 3 parameters (revisit later)
|
||||
|
|
71
packages/.gitignore
vendored
Normal file
71
packages/.gitignore
vendored
Normal file
|
@ -0,0 +1,71 @@
|
|||
# See https://www.dartlang.org/tools/private-files.html
|
||||
|
||||
# Files and directories created by pub
|
||||
.dart_tool
|
||||
.packages
|
||||
.pub/
|
||||
build/
|
||||
|
||||
# If you're building an application, you may want to check-in your pubspec.lock
|
||||
pubspec.lock
|
||||
|
||||
# Directory created by dartdoc
|
||||
# If you don't generate documentation locally you can remove this line.
|
||||
doc/api/
|
||||
|
||||
### Dart template
|
||||
# See https://www.dartlang.org/tools/private-files.html
|
||||
|
||||
# Files and directories created by pub
|
||||
|
||||
# SDK 1.20 and later (no longer creates packages directories)
|
||||
|
||||
# Older SDK versions
|
||||
# (Include if the minimum SDK version specified in pubsepc.yaml is earlier than 1.20)
|
||||
.project
|
||||
.buildlog
|
||||
**/packages/
|
||||
|
||||
|
||||
# Files created by dart2js
|
||||
# (Most Dart developers will use pub build to compile Dart, use/modify these
|
||||
# rules if you intend to use dart2js directly
|
||||
# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
|
||||
# differentiate from explicit Javascript files)
|
||||
*.dart.js
|
||||
*.part.js
|
||||
*.js.deps
|
||||
*.js.map
|
||||
*.info.json
|
||||
|
||||
# Directory created by dartdoc
|
||||
|
||||
# Don't commit pubspec lock file
|
||||
# (Library packages only! Remove pattern if developing an application package)
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff:
|
||||
|
||||
## VsCode
|
||||
.vscode/
|
||||
|
||||
## File-based project format:
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
.idea/
|
||||
/out/
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
12
packages/AUTHORS.md
Normal file
12
packages/AUTHORS.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
Primary Authors
|
||||
===============
|
||||
|
||||
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
|
||||
|
||||
Thomas is the current maintainer of the code base. He has refactored and migrated the
|
||||
code base to support NNBD.
|
||||
|
||||
* __[Tobe O](thosakwe@gmail.com)__
|
||||
|
||||
Tobe has written much of the original code prior to NNBD migration. He has moved on and
|
||||
is no longer involved with the project.
|
21
packages/LICENSE
Normal file
21
packages/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 dukefirehawk.com
|
||||
|
||||
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.
|
55
packages/auth/.gitignore
vendored
55
packages/auth/.gitignore
vendored
|
@ -1,15 +1,32 @@
|
|||
# Created by .ignore support plugin (hsz.mobi)
|
||||
# See https://www.dartlang.org/tools/private-files.html
|
||||
|
||||
# Files and directories created by pub
|
||||
.dart_tool
|
||||
.packages
|
||||
.pub/
|
||||
build/
|
||||
|
||||
# If you're building an application, you may want to check-in your pubspec.lock
|
||||
pubspec.lock
|
||||
|
||||
# Directory created by dartdoc
|
||||
# If you don't generate documentation locally you can remove this line.
|
||||
doc/api/
|
||||
|
||||
### Dart template
|
||||
# See https://www.dartlang.org/tools/private-files.html
|
||||
|
||||
# Files and directories created by pub
|
||||
.buildlog
|
||||
.packages
|
||||
|
||||
# SDK 1.20 and later (no longer creates packages directories)
|
||||
|
||||
# Older SDK versions
|
||||
# (Include if the minimum SDK version specified in pubsepc.yaml is earlier than 1.20)
|
||||
.project
|
||||
.pub/
|
||||
build/
|
||||
.buildlog
|
||||
**/packages/
|
||||
|
||||
|
||||
# Files created by dart2js
|
||||
# (Most Dart developers will use pub build to compile Dart, use/modify these
|
||||
# rules if you intend to use dart2js directly
|
||||
|
@ -22,36 +39,17 @@ build/
|
|||
*.info.json
|
||||
|
||||
# Directory created by dartdoc
|
||||
doc/api/
|
||||
|
||||
# Don't commit pubspec lock file
|
||||
# (Library packages only! Remove pattern if developing an application package)
|
||||
pubspec.lock
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff:
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/dictionaries
|
||||
.idea/vcs.xml
|
||||
.idea/jsLibraryMappings.xml
|
||||
|
||||
# Sensitive or high-churn files:
|
||||
.idea/dataSources.ids
|
||||
.idea/dataSources.xml
|
||||
.idea/dataSources.local.xml
|
||||
.idea/sqlDataSources.xml
|
||||
.idea/dynamic.xml
|
||||
.idea/uiDesigner.xml
|
||||
|
||||
# Gradle:
|
||||
.idea/gradle.xml
|
||||
.idea/libraries
|
||||
|
||||
# Mongo Explorer plugin:
|
||||
.idea/mongoSettings.xml
|
||||
## VsCode
|
||||
.vscode/
|
||||
|
||||
## File-based project format:
|
||||
*.iws
|
||||
|
@ -59,9 +57,8 @@ pubspec.lock
|
|||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
.idea/
|
||||
/out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
|
@ -72,5 +69,3 @@ com_crashlytics_export_strings.xml
|
|||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
.dart_tool
|
12
packages/auth/AUTHORS.md
Normal file
12
packages/auth/AUTHORS.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
Primary Authors
|
||||
===============
|
||||
|
||||
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
|
||||
|
||||
Thomas is the current maintainer of the code base. He has refactored and migrated the
|
||||
code base to support NNBD.
|
||||
|
||||
* __[Tobe O](thosakwe@gmail.com)__
|
||||
|
||||
Tobe has written much of the original code prior to NNBD migration. He has moved on and
|
||||
is no longer involved with the project.
|
|
@ -1,3 +1,12 @@
|
|||
# 4.0.1
|
||||
* Updated README
|
||||
|
||||
# 4.0.0
|
||||
* Migrated to support Dart SDK 2.12.x NNBD
|
||||
|
||||
# 3.0.0
|
||||
* Migrated to work with Dart SDK 2.12.x Non NNBD
|
||||
|
||||
# 2.1.5+1
|
||||
* Fix error in popup page.
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 angel-dart
|
||||
Copyright (c) 2021 dukefirehawk.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
# angel_auth
|
||||
# angel3_auth
|
||||
[![version](https://img.shields.io/badge/pub-v4.0.1-brightgreen)](https://pub.dartlang.org/packages/angel3_auth)
|
||||
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
|
||||
[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion)
|
||||
|
||||
[![Pub](https://img.shields.io/pub/v/angel_auth.svg)](https://pub.dartlang.org/packages/angel_auth)
|
||||
[![build status](https://travis-ci.org/angel-dart/auth.svg?branch=master)](https://travis-ci.org/angel-dart/auth)
|
||||
[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/angel3/packages/auth/LICENSE)
|
||||
|
||||
A complete authentication plugin for Angel. Inspired by Passport.
|
||||
|
||||
|
@ -76,7 +78,7 @@ configureServer(Angel app) async {
|
|||
```
|
||||
|
||||
This renders a simple HTML page that fires the user's JWT as a `token` event in `window.opener`.
|
||||
`angel_client` [exposes this as a Stream](https://github.com/angel-dart/client#authentication):
|
||||
`angel_client` [exposes this as a Stream](https://github.com/dukefirehawk/angel/tree/angel3/packages/client#authentication):
|
||||
|
||||
```dart
|
||||
app.authenticateViaPopup('/auth/google').listen((jwt) {
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import 'dart:async';
|
||||
import 'package:angel_auth/angel_auth.dart';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_framework/http.dart';
|
||||
import 'package:angel3_auth/angel3_auth.dart';
|
||||
import 'package:angel3_framework/angel3_framework.dart';
|
||||
import 'package:angel3_framework/http.dart';
|
||||
|
||||
main() async {
|
||||
void main() async {
|
||||
var app = Angel();
|
||||
var auth = AngelAuth<User>();
|
||||
var auth = AngelAuth<User?>();
|
||||
|
||||
auth.serializer = (user) => user.id;
|
||||
auth.serializer = (user) => user!.id;
|
||||
|
||||
auth.deserializer = (id) => fetchAUserByIdSomehow(id);
|
||||
|
||||
|
@ -30,7 +30,7 @@ main() async {
|
|||
}
|
||||
|
||||
class User {
|
||||
String id, username, password;
|
||||
String? id, username, password;
|
||||
}
|
||||
|
||||
Future<User> fetchAUserByIdSomehow(id) async {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
library angel_auth;
|
||||
library angel3_auth;
|
||||
|
||||
export 'src/middleware/require_auth.dart';
|
||||
export 'src/strategies/strategies.dart';
|
|
@ -1,4 +1,4 @@
|
|||
/// Stand-alone JWT library.
|
||||
library angel_auth.auth_token;
|
||||
library angel3_auth.auth_token;
|
||||
|
||||
export 'src/auth_token.dart';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'dart:collection';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel3_framework/angel3_framework.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:crypto/crypto.dart';
|
||||
|
||||
|
@ -26,10 +26,10 @@ String decodeBase64(String str) {
|
|||
|
||||
class AuthToken {
|
||||
final SplayTreeMap<String, String> _header =
|
||||
SplayTreeMap.from({"alg": "HS256", "typ": "JWT"});
|
||||
SplayTreeMap.from({'alg': 'HS256', 'typ': 'JWT'});
|
||||
|
||||
String ipAddress;
|
||||
DateTime issuedAt;
|
||||
String? ipAddress;
|
||||
late DateTime issuedAt;
|
||||
num lifeSpan;
|
||||
var userId;
|
||||
Map<String, dynamic> payload = {};
|
||||
|
@ -38,12 +38,20 @@ class AuthToken {
|
|||
{this.ipAddress,
|
||||
this.lifeSpan = -1,
|
||||
this.userId,
|
||||
DateTime issuedAt,
|
||||
DateTime? issuedAt,
|
||||
Map payload = const {}}) {
|
||||
this.issuedAt = issuedAt ?? DateTime.now();
|
||||
this.payload.addAll(
|
||||
payload?.keys?.fold({}, (out, k) => out..[k.toString()] = payload[k]) ??
|
||||
{});
|
||||
this.payload.addAll(payload.keys
|
||||
.fold({}, ((out, k) => out?..[k.toString()] = payload[k])) ??
|
||||
{});
|
||||
/*
|
||||
this.payload.addAll(payload.keys.fold(
|
||||
{},
|
||||
((out, k) => out..[k.toString()] = payload[k])
|
||||
as Map<String, dynamic>? Function(
|
||||
Map<String, dynamic>?, dynamic)) ??
|
||||
{});
|
||||
*/
|
||||
}
|
||||
|
||||
factory AuthToken.fromJson(String jsons) =>
|
||||
|
@ -51,37 +59,40 @@ class AuthToken {
|
|||
|
||||
factory AuthToken.fromMap(Map data) {
|
||||
return AuthToken(
|
||||
ipAddress: data["aud"].toString(),
|
||||
lifeSpan: data["exp"] as num,
|
||||
issuedAt: DateTime.parse(data["iat"].toString()),
|
||||
userId: data["sub"],
|
||||
payload: data["pld"] as Map ?? {});
|
||||
ipAddress: data['aud'].toString(),
|
||||
lifeSpan: data['exp'] as num,
|
||||
issuedAt: DateTime.parse(data['iat'].toString()),
|
||||
userId: data['sub'],
|
||||
payload: data['pld'] as Map);
|
||||
}
|
||||
|
||||
factory AuthToken.parse(String jwt) {
|
||||
var split = jwt.split(".");
|
||||
var split = jwt.split('.');
|
||||
|
||||
if (split.length != 3)
|
||||
throw AngelHttpException.notAuthenticated(message: "Invalid JWT.");
|
||||
if (split.length != 3) {
|
||||
throw AngelHttpException.notAuthenticated(message: 'Invalid JWT.');
|
||||
}
|
||||
|
||||
var payloadString = decodeBase64(split[1]);
|
||||
return AuthToken.fromMap(json.decode(payloadString) as Map);
|
||||
}
|
||||
|
||||
factory AuthToken.validate(String jwt, Hmac hmac) {
|
||||
var split = jwt.split(".");
|
||||
var split = jwt.split('.');
|
||||
|
||||
if (split.length != 3)
|
||||
throw AngelHttpException.notAuthenticated(message: "Invalid JWT.");
|
||||
if (split.length != 3) {
|
||||
throw AngelHttpException.notAuthenticated(message: 'Invalid JWT.');
|
||||
}
|
||||
|
||||
// var headerString = decodeBase64(split[0]);
|
||||
var payloadString = decodeBase64(split[1]);
|
||||
var data = split[0] + "." + split[1];
|
||||
var data = split[0] + '.' + split[1];
|
||||
var signature = base64Url.encode(hmac.convert(data.codeUnits).bytes);
|
||||
|
||||
if (signature != split[2])
|
||||
if (signature != split[2]) {
|
||||
throw AngelHttpException.notAuthenticated(
|
||||
message: "JWT payload does not match hashed version.");
|
||||
message: 'JWT payload does not match hashed version.');
|
||||
}
|
||||
|
||||
return AuthToken.fromMap(json.decode(payloadString) as Map);
|
||||
}
|
||||
|
@ -89,9 +100,9 @@ class AuthToken {
|
|||
String serialize(Hmac hmac) {
|
||||
var headerString = base64Url.encode(json.encode(_header).codeUnits);
|
||||
var payloadString = base64Url.encode(json.encode(toJson()).codeUnits);
|
||||
var data = headerString + "." + payloadString;
|
||||
var data = headerString + '.' + payloadString;
|
||||
var signature = hmac.convert(data.codeUnits).bytes;
|
||||
return data + "." + base64Url.encode(signature);
|
||||
return data + '.' + base64Url.encode(signature);
|
||||
}
|
||||
|
||||
Map toJson() {
|
||||
|
@ -114,11 +125,12 @@ SplayTreeMap _splayify(Map map) {
|
|||
return SplayTreeMap.from(data);
|
||||
}
|
||||
|
||||
_splay(value) {
|
||||
dynamic _splay(value) {
|
||||
if (value is Iterable) {
|
||||
return value.map(_splay).toList();
|
||||
} else if (value is Map)
|
||||
} else if (value is Map) {
|
||||
return _splayify(value);
|
||||
else
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:charcode/ascii.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:quiver_hashcode/hashcode.dart';
|
||||
import 'package:quiver/core.dart';
|
||||
|
||||
/// A common class containing parsing and validation logic for third-party authentication configuration.
|
||||
class ExternalAuthOptions {
|
||||
|
@ -18,18 +17,12 @@ class ExternalAuthOptions {
|
|||
final Set<String> scopes;
|
||||
|
||||
ExternalAuthOptions._(
|
||||
this.clientId, this.clientSecret, this.redirectUri, this.scopes) {
|
||||
if (clientId == null) {
|
||||
throw ArgumentError.notNull('clientId');
|
||||
} else if (clientSecret == null) {
|
||||
throw ArgumentError.notNull('clientSecret');
|
||||
}
|
||||
}
|
||||
this.clientId, this.clientSecret, this.redirectUri, this.scopes);
|
||||
|
||||
factory ExternalAuthOptions(
|
||||
{@required String clientId,
|
||||
@required String clientSecret,
|
||||
@required redirectUri,
|
||||
{required String clientId,
|
||||
required String clientSecret,
|
||||
required redirectUri,
|
||||
Iterable<String> scopes = const []}) {
|
||||
if (redirectUri is String) {
|
||||
return ExternalAuthOptions._(
|
||||
|
@ -50,9 +43,15 @@ class ExternalAuthOptions {
|
|||
/// * `client_secret`
|
||||
/// * `redirect_uri`
|
||||
factory ExternalAuthOptions.fromMap(Map map) {
|
||||
var clientId = map['client_id'];
|
||||
var clientSecret = map['client_secret'];
|
||||
if (clientId == null || clientSecret == null) {
|
||||
throw ArgumentError('Invalid clientId and/or clientSecret');
|
||||
}
|
||||
|
||||
return ExternalAuthOptions(
|
||||
clientId: map['client_id'] as String,
|
||||
clientSecret: map['client_secret'] as String,
|
||||
clientId: clientId as String,
|
||||
clientSecret: clientSecret as String,
|
||||
redirectUri: map['redirect_uri'],
|
||||
scopes: map['scopes'] is Iterable
|
||||
? ((map['scopes'] as Iterable).map((x) => x.toString()))
|
||||
|
@ -73,15 +72,15 @@ class ExternalAuthOptions {
|
|||
|
||||
/// Creates a copy of this object, with the specified changes.
|
||||
ExternalAuthOptions copyWith(
|
||||
{String clientId,
|
||||
String clientSecret,
|
||||
{String? clientId,
|
||||
String? clientSecret,
|
||||
redirectUri,
|
||||
Iterable<String> scopes}) {
|
||||
Iterable<String> scopes = const []}) {
|
||||
return ExternalAuthOptions(
|
||||
clientId: clientId ?? this.clientId,
|
||||
clientSecret: clientSecret ?? this.clientSecret,
|
||||
redirectUri: redirectUri ?? this.redirectUri,
|
||||
scopes: (scopes ??= []).followedBy(this.scopes),
|
||||
scopes: (scopes).followedBy(this.scopes),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -111,8 +110,8 @@ class ExternalAuthOptions {
|
|||
/// If no [asteriskCount] is given, then the number of asterisks will equal the length of
|
||||
/// the actual [clientSecret].
|
||||
@override
|
||||
String toString({bool obscureSecret = true, int asteriskCount}) {
|
||||
String secret;
|
||||
String toString({bool obscureSecret = true, int? asteriskCount}) {
|
||||
String? secret;
|
||||
|
||||
if (!obscureSecret) {
|
||||
secret = clientSecret;
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
import 'dart:async';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel3_framework/angel3_framework.dart';
|
||||
|
||||
/// Forces Basic authentication over the requested resource, with the given [realm] name, if no JWT is present.
|
||||
///
|
||||
/// [realm] defaults to `'angel_auth'`.
|
||||
RequestHandler forceBasicAuth<User>({String realm}) {
|
||||
RequestHandler forceBasicAuth<User>({String? realm}) {
|
||||
return (RequestContext req, ResponseContext res) async {
|
||||
if (req.container.has<User>())
|
||||
return true;
|
||||
else if (req.container.has<Future<User>>()) {
|
||||
await req.container.makeAsync<User>();
|
||||
return true;
|
||||
if (req.container != null) {
|
||||
var reqContainer = req.container!;
|
||||
if (reqContainer.has<User>()) {
|
||||
return true;
|
||||
} else if (reqContainer.has<Future<User>>()) {
|
||||
await reqContainer.makeAsync<User>();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
res.headers['www-authenticate'] = 'Basic realm="${realm ?? 'angel_auth'}"';
|
||||
|
@ -26,16 +29,23 @@ RequestHandler requireAuthentication<User>() {
|
|||
if (throwError) {
|
||||
res.statusCode = 403;
|
||||
throw AngelHttpException.forbidden();
|
||||
} else
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (req.container.has<User>() || req.method == 'OPTIONS')
|
||||
return true;
|
||||
else if (req.container.has<Future<User>>()) {
|
||||
await req.container.makeAsync<User>();
|
||||
return true;
|
||||
} else
|
||||
if (req.container != null) {
|
||||
var reqContainer = req.container!;
|
||||
if (reqContainer.has<User>() || req.method == 'OPTIONS') {
|
||||
return true;
|
||||
} else if (reqContainer.has<Future<User>>()) {
|
||||
await reqContainer.makeAsync<User>();
|
||||
return true;
|
||||
} else {
|
||||
return _reject(res);
|
||||
}
|
||||
} else {
|
||||
return _reject(res);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel3_framework/angel3_framework.dart';
|
||||
import 'auth_token.dart';
|
||||
|
||||
typedef FutureOr AngelAuthCallback(
|
||||
typedef AngelAuthCallback = FutureOr Function(
|
||||
RequestContext req, ResponseContext res, String token);
|
||||
|
||||
typedef FutureOr AngelAuthTokenCallback<User>(
|
||||
typedef AngelAuthTokenCallback<User> = FutureOr Function(
|
||||
RequestContext req, ResponseContext res, AuthToken token, User user);
|
||||
|
||||
class AngelAuthOptions<User> {
|
||||
AngelAuthCallback callback;
|
||||
AngelAuthTokenCallback<User> tokenCallback;
|
||||
String successRedirect;
|
||||
String failureRedirect;
|
||||
AngelAuthCallback? callback;
|
||||
AngelAuthTokenCallback<User>? tokenCallback;
|
||||
String? successRedirect;
|
||||
String? failureRedirect;
|
||||
|
||||
/// If `false` (default: `true`), then successful authentication will return `true` and allow the
|
||||
/// execution of subsequent handlers, just like any other middleware.
|
||||
|
@ -26,5 +26,5 @@ class AngelAuthOptions<User> {
|
|||
this.tokenCallback,
|
||||
this.canRespondWithJson = true,
|
||||
this.successRedirect,
|
||||
String this.failureRedirect});
|
||||
this.failureRedirect});
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math' as Math;
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'dart:math';
|
||||
import 'package:angel3_framework/angel3_framework.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'auth_token.dart';
|
||||
import 'options.dart';
|
||||
|
@ -9,12 +9,12 @@ import 'strategy.dart';
|
|||
|
||||
/// Handles authentication within an Angel application.
|
||||
class AngelAuth<User> {
|
||||
Hmac _hs256;
|
||||
int _jwtLifeSpan;
|
||||
late Hmac _hs256;
|
||||
late int _jwtLifeSpan;
|
||||
final StreamController<User> _onLogin = StreamController<User>(),
|
||||
_onLogout = StreamController<User>();
|
||||
Math.Random _random = Math.Random.secure();
|
||||
final RegExp _rgxBearer = RegExp(r"^Bearer");
|
||||
final Random _random = Random.secure();
|
||||
final RegExp _rgxBearer = RegExp(r'^Bearer');
|
||||
|
||||
/// If `true` (default), then JWT's will be stored and retrieved from a `token` cookie.
|
||||
final bool allowCookie;
|
||||
|
@ -29,7 +29,7 @@ class AngelAuth<User> {
|
|||
/// A domain to restrict emitted cookies to.
|
||||
///
|
||||
/// Only applies if [allowCookie] is `true`.
|
||||
final String cookieDomain;
|
||||
final String? cookieDomain;
|
||||
|
||||
/// A path to restrict emitted cookies to.
|
||||
///
|
||||
|
@ -48,10 +48,10 @@ class AngelAuth<User> {
|
|||
Map<String, AuthStrategy<User>> strategies = {};
|
||||
|
||||
/// Serializes a user into a unique identifier associated only with one identity.
|
||||
FutureOr Function(User) serializer;
|
||||
FutureOr Function(User)? serializer;
|
||||
|
||||
/// Deserializes a unique identifier into its associated identity. In most cases, this is a user object or model instance.
|
||||
FutureOr<User> Function(Object) deserializer;
|
||||
FutureOr<User> Function(Object)? deserializer;
|
||||
|
||||
/// Fires the result of [deserializer] whenever a user signs in to the application.
|
||||
Stream<User> get onLogin => _onLogin.stream;
|
||||
|
@ -65,25 +65,27 @@ class AngelAuth<User> {
|
|||
String _randomString(
|
||||
{int length = 32,
|
||||
String validChars =
|
||||
"ABCDEFHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"}) {
|
||||
'ABCDEFHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_'}) {
|
||||
var chars = <int>[];
|
||||
while (chars.length < length) chars.add(_random.nextInt(validChars.length));
|
||||
while (chars.length < length) {
|
||||
chars.add(_random.nextInt(validChars.length));
|
||||
}
|
||||
return String.fromCharCodes(chars);
|
||||
}
|
||||
|
||||
/// `jwtLifeSpan` - should be in *milliseconds*.
|
||||
AngelAuth(
|
||||
{String jwtKey,
|
||||
{String? jwtKey,
|
||||
this.serializer,
|
||||
this.deserializer,
|
||||
num jwtLifeSpan,
|
||||
num? jwtLifeSpan,
|
||||
this.allowCookie = true,
|
||||
this.allowTokenInQuery = true,
|
||||
this.enforceIp = true,
|
||||
this.cookieDomain,
|
||||
this.cookiePath = '/',
|
||||
this.secureCookies = true,
|
||||
this.reviveTokenEndpoint = "/auth/token"})
|
||||
this.reviveTokenEndpoint = '/auth/token'})
|
||||
: super() {
|
||||
_hs256 = Hmac(sha256, (jwtKey ?? _randomString()).codeUnits);
|
||||
_jwtLifeSpan = jwtLifeSpan?.toInt() ?? -1;
|
||||
|
@ -92,22 +94,25 @@ class AngelAuth<User> {
|
|||
/// Configures an Angel server to decode and validate JSON Web tokens on demand,
|
||||
/// whenever an instance of [User] is injected.
|
||||
Future<void> configureServer(Angel app) async {
|
||||
if (serializer == null)
|
||||
if (serializer == null) {
|
||||
throw StateError(
|
||||
'An `AngelAuth` plug-in was called without its `serializer` being set. All authentication will fail.');
|
||||
if (deserializer == null)
|
||||
}
|
||||
if (deserializer == null) {
|
||||
throw StateError(
|
||||
'An `AngelAuth` plug-in was called without its `deserializer` being set. All authentication will fail.');
|
||||
}
|
||||
|
||||
app.container.registerSingleton(this);
|
||||
if (runtimeType != AngelAuth)
|
||||
app.container.registerSingleton(this, as: AngelAuth);
|
||||
app.container!.registerSingleton(this);
|
||||
if (runtimeType != AngelAuth) {
|
||||
app.container!.registerSingleton(this, as: AngelAuth);
|
||||
}
|
||||
|
||||
if (!app.container.has<_AuthResult<User>>()) {
|
||||
app.container
|
||||
if (!app.container!.has<_AuthResult<User>>()) {
|
||||
app.container!
|
||||
.registerLazySingleton<Future<_AuthResult<User>>>((container) async {
|
||||
var req = container.make<RequestContext>();
|
||||
var res = container.make<ResponseContext>();
|
||||
var req = container.make<RequestContext>()!;
|
||||
var res = container.make<ResponseContext>()!;
|
||||
var result = await _decodeJwt(req, res);
|
||||
if (result != null) {
|
||||
return result;
|
||||
|
@ -116,20 +121,19 @@ class AngelAuth<User> {
|
|||
}
|
||||
});
|
||||
|
||||
app.container.registerLazySingleton<Future<User>>((container) async {
|
||||
var result = await container.makeAsync<_AuthResult<User>>();
|
||||
app.container!.registerLazySingleton<Future<User>>((container) async {
|
||||
var result = await container.makeAsync<_AuthResult<User>>()!;
|
||||
return result.user;
|
||||
});
|
||||
|
||||
app.container.registerLazySingleton<Future<AuthToken>>((container) async {
|
||||
var result = await container.makeAsync<_AuthResult<User>>();
|
||||
app.container!
|
||||
.registerLazySingleton<Future<AuthToken>>((container) async {
|
||||
var result = await container.makeAsync<_AuthResult<User>>()!;
|
||||
return result.token;
|
||||
});
|
||||
}
|
||||
|
||||
if (reviveTokenEndpoint != null) {
|
||||
app.post(reviveTokenEndpoint, reviveJwt);
|
||||
}
|
||||
app.post(reviveTokenEndpoint, reviveJwt);
|
||||
|
||||
app.shutdownHooks.add((_) {
|
||||
_onLogin.close();
|
||||
|
@ -137,17 +141,17 @@ class AngelAuth<User> {
|
|||
}
|
||||
|
||||
void _apply(
|
||||
RequestContext req, ResponseContext res, AuthToken token, User user) {
|
||||
if (!req.container.has<User>()) {
|
||||
req.container.registerSingleton<User>(user);
|
||||
RequestContext req, ResponseContext? res, AuthToken token, User user) {
|
||||
if (!req.container!.has<User>()) {
|
||||
req.container!.registerSingleton<User>(user);
|
||||
}
|
||||
|
||||
if (!req.container.has<AuthToken>()) {
|
||||
req.container.registerSingleton<AuthToken>(token);
|
||||
if (!req.container!.has<AuthToken>()) {
|
||||
req.container!.registerSingleton<AuthToken>(token);
|
||||
}
|
||||
|
||||
if (allowCookie == true) {
|
||||
_addProtectedCookie(res, 'token', token.serialize(_hs256));
|
||||
if (allowCookie) {
|
||||
_addProtectedCookie(res!, 'token', token.serialize(_hs256));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -174,7 +178,7 @@ class AngelAuth<User> {
|
|||
/// ```
|
||||
@deprecated
|
||||
Future decodeJwt(RequestContext req, ResponseContext res) async {
|
||||
if (req.method == "POST" && req.path == reviveTokenEndpoint) {
|
||||
if (req.method == 'POST' && req.path == reviveTokenEndpoint) {
|
||||
return await reviveJwt(req, res);
|
||||
} else {
|
||||
await _decodeJwt(req, res);
|
||||
|
@ -182,28 +186,30 @@ class AngelAuth<User> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<_AuthResult<User>> _decodeJwt(
|
||||
Future<_AuthResult<User>?> _decodeJwt(
|
||||
RequestContext req, ResponseContext res) async {
|
||||
String jwt = getJwt(req);
|
||||
var jwt = getJwt(req);
|
||||
|
||||
if (jwt != null) {
|
||||
var token = AuthToken.validate(jwt, _hs256);
|
||||
|
||||
if (enforceIp) {
|
||||
if (req.ip != null && req.ip != token.ipAddress)
|
||||
if (req.ip != token.ipAddress) {
|
||||
throw AngelHttpException.forbidden(
|
||||
message: "JWT cannot be accessed from this IP address.");
|
||||
message: 'JWT cannot be accessed from this IP address.');
|
||||
}
|
||||
}
|
||||
|
||||
if (token.lifeSpan > -1) {
|
||||
var expiry =
|
||||
token.issuedAt.add(Duration(milliseconds: token.lifeSpan.toInt()));
|
||||
|
||||
if (!expiry.isAfter(DateTime.now()))
|
||||
throw AngelHttpException.forbidden(message: "Expired JWT.");
|
||||
if (!expiry.isAfter(DateTime.now())) {
|
||||
throw AngelHttpException.forbidden(message: 'Expired JWT.');
|
||||
}
|
||||
}
|
||||
|
||||
var user = await deserializer(token.userId);
|
||||
var user = await deserializer!(token.userId as Object);
|
||||
_apply(req, res, token, user);
|
||||
return _AuthResult(user, token);
|
||||
}
|
||||
|
@ -212,19 +218,20 @@ class AngelAuth<User> {
|
|||
}
|
||||
|
||||
/// Retrieves a JWT from a request, if any was sent at all.
|
||||
String getJwt(RequestContext req) {
|
||||
if (req.headers.value("Authorization") != null) {
|
||||
final authHeader = req.headers.value("Authorization");
|
||||
String? getJwt(RequestContext req) {
|
||||
if (req.headers?.value('Authorization') != null) {
|
||||
final authHeader = req.headers!.value('Authorization')!;
|
||||
|
||||
// Allow Basic auth to fall through
|
||||
if (_rgxBearer.hasMatch(authHeader))
|
||||
return authHeader.replaceAll(_rgxBearer, "").trim();
|
||||
if (_rgxBearer.hasMatch(authHeader)) {
|
||||
return authHeader.replaceAll(_rgxBearer, '').trim();
|
||||
}
|
||||
} else if (allowCookie &&
|
||||
req.cookies.any((cookie) => cookie.name == "token")) {
|
||||
return req.cookies.firstWhere((cookie) => cookie.name == "token").value;
|
||||
req.cookies.any((cookie) => cookie.name == 'token')) {
|
||||
return req.cookies.firstWhere((cookie) => cookie.name == 'token').value;
|
||||
} else if (allowTokenInQuery &&
|
||||
req.uri.queryParameters['token'] is String) {
|
||||
return req.uri.queryParameters['token']?.toString();
|
||||
req.uri?.queryParameters['token'] is String) {
|
||||
return req.uri!.queryParameters['token']?.toString();
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -243,10 +250,10 @@ class AngelAuth<User> {
|
|||
cookie.secure = true;
|
||||
}
|
||||
|
||||
if (_jwtLifeSpan > 0) {
|
||||
cookie.maxAge ??= _jwtLifeSpan < 0 ? -1 : _jwtLifeSpan ~/ 1000;
|
||||
cookie.expires ??=
|
||||
DateTime.now().add(Duration(milliseconds: _jwtLifeSpan));
|
||||
var lifeSpan = _jwtLifeSpan;
|
||||
if (lifeSpan > 0) {
|
||||
cookie.maxAge ??= lifeSpan < 0 ? -1 : lifeSpan ~/ 1000;
|
||||
cookie.expires ??= DateTime.now().add(Duration(milliseconds: lifeSpan));
|
||||
}
|
||||
|
||||
cookie.domain ??= cookieDomain;
|
||||
|
@ -265,13 +272,14 @@ class AngelAuth<User> {
|
|||
jwt = body['token']?.toString();
|
||||
}
|
||||
if (jwt == null) {
|
||||
throw AngelHttpException.forbidden(message: "No JWT provided");
|
||||
throw AngelHttpException.forbidden(message: 'No JWT provided');
|
||||
} else {
|
||||
var token = AuthToken.validate(jwt, _hs256);
|
||||
if (enforceIp) {
|
||||
if (req.ip != token.ipAddress)
|
||||
if (req.ip != token.ipAddress) {
|
||||
throw AngelHttpException.forbidden(
|
||||
message: "JWT cannot be accessed from this IP address.");
|
||||
message: 'JWT cannot be accessed from this IP address.');
|
||||
}
|
||||
}
|
||||
|
||||
if (token.lifeSpan > -1) {
|
||||
|
@ -290,12 +298,12 @@ class AngelAuth<User> {
|
|||
_addProtectedCookie(res, 'token', token.serialize(_hs256));
|
||||
}
|
||||
|
||||
final data = await deserializer(token.userId);
|
||||
final data = await deserializer!(token.userId as Object);
|
||||
return {'data': data, 'token': token.serialize(_hs256)};
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is AngelHttpException) rethrow;
|
||||
throw AngelHttpException.badRequest(message: "Malformed JWT");
|
||||
throw AngelHttpException.badRequest(message: 'Malformed JWT');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -307,14 +315,14 @@ class AngelAuth<User> {
|
|||
/// or a `401 Not Authenticated` is thrown, if it is the last one.
|
||||
///
|
||||
/// Any other result is considered an authenticated user, and terminates the loop.
|
||||
RequestHandler authenticate(type, [AngelAuthOptions<User> options]) {
|
||||
RequestHandler authenticate(type, [AngelAuthOptions<User>? options]) {
|
||||
return (RequestContext req, ResponseContext res) async {
|
||||
List<String> names = [];
|
||||
var names = <String>[];
|
||||
var arr = type is Iterable
|
||||
? type.map((x) => x.toString()).toList()
|
||||
: [type.toString()];
|
||||
|
||||
for (String t in arr) {
|
||||
for (var t in arr) {
|
||||
var n = t
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
|
@ -323,20 +331,20 @@ class AngelAuth<User> {
|
|||
names.addAll(n);
|
||||
}
|
||||
|
||||
for (int i = 0; i < names.length; i++) {
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
var name = names[i];
|
||||
|
||||
var strategy = strategies[name] ??=
|
||||
throw ArgumentError('No strategy "$name" found.');
|
||||
|
||||
var hasExisting = req.container.has<User>();
|
||||
var hasExisting = req.container!.has<User>();
|
||||
var result = hasExisting
|
||||
? req.container.make<User>()
|
||||
: await strategy.authenticate(req, res, options);
|
||||
if (result == true)
|
||||
? req.container!.make<User>()
|
||||
: await strategy.authenticate(req, res, options!);
|
||||
if (result == true) {
|
||||
return result;
|
||||
else if (result != false && result != null) {
|
||||
var userId = await serializer(result);
|
||||
} else if (result != false && result != null) {
|
||||
var userId = await serializer!(result);
|
||||
|
||||
// Create JWT
|
||||
var token = AuthToken(
|
||||
|
@ -344,11 +352,11 @@ class AngelAuth<User> {
|
|||
var jwt = token.serialize(_hs256);
|
||||
|
||||
if (options?.tokenCallback != null) {
|
||||
if (!req.container.has<User>()) {
|
||||
req.container.registerSingleton<User>(result);
|
||||
if (!req.container!.has<User>()) {
|
||||
req.container!.registerSingleton<User>(result);
|
||||
}
|
||||
|
||||
var r = await options.tokenCallback(req, res, token, result);
|
||||
var r = await options!.tokenCallback!(req, res, token, result);
|
||||
if (r != null) return r;
|
||||
jwt = token.serialize(_hs256);
|
||||
}
|
||||
|
@ -360,17 +368,17 @@ class AngelAuth<User> {
|
|||
}
|
||||
|
||||
if (options?.callback != null) {
|
||||
return await options.callback(req, res, jwt);
|
||||
return await options!.callback!(req, res, jwt);
|
||||
}
|
||||
|
||||
if (options?.successRedirect?.isNotEmpty == true) {
|
||||
await res.redirect(options.successRedirect);
|
||||
await res.redirect(options!.successRedirect);
|
||||
return false;
|
||||
} else if (options?.canRespondWithJson != false &&
|
||||
req.accepts('application/json')) {
|
||||
var user = hasExisting
|
||||
? result
|
||||
: await deserializer(await serializer(result));
|
||||
: await deserializer!((await serializer!(result)) as Object);
|
||||
_onLogin.add(user);
|
||||
return {"data": user, "token": jwt};
|
||||
}
|
||||
|
@ -381,13 +389,14 @@ class AngelAuth<User> {
|
|||
// Check if not redirect
|
||||
if (res.statusCode == 301 ||
|
||||
res.statusCode == 302 ||
|
||||
res.headers.containsKey('location'))
|
||||
res.headers.containsKey('location')) {
|
||||
return false;
|
||||
else if (options?.failureRedirect != null) {
|
||||
await res.redirect(options.failureRedirect);
|
||||
} else if (options?.failureRedirect != null) {
|
||||
await res.redirect(options!.failureRedirect);
|
||||
return false;
|
||||
} else
|
||||
} else {
|
||||
throw AngelHttpException.notAuthenticated();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -395,7 +404,7 @@ class AngelAuth<User> {
|
|||
|
||||
/// Log a user in on-demand.
|
||||
Future login(AuthToken token, RequestContext req, ResponseContext res) async {
|
||||
var user = await deserializer(token.userId);
|
||||
var user = await deserializer!(token.userId as Object);
|
||||
_apply(req, res, token, user);
|
||||
_onLogin.add(user);
|
||||
|
||||
|
@ -406,7 +415,7 @@ class AngelAuth<User> {
|
|||
|
||||
/// Log a user in on-demand.
|
||||
Future loginById(userId, RequestContext req, ResponseContext res) async {
|
||||
var user = await deserializer(userId);
|
||||
var user = await deserializer!(userId as Object);
|
||||
var token =
|
||||
AuthToken(userId: userId, lifeSpan: _jwtLifeSpan, ipAddress: req.ip);
|
||||
_apply(req, res, token, user);
|
||||
|
@ -418,21 +427,23 @@ class AngelAuth<User> {
|
|||
}
|
||||
|
||||
/// Log an authenticated user out.
|
||||
RequestHandler logout([AngelAuthOptions<User> options]) {
|
||||
RequestHandler logout([AngelAuthOptions<User>? options]) {
|
||||
return (RequestContext req, ResponseContext res) async {
|
||||
if (req.container.has<User>()) {
|
||||
var user = req.container.make<User>();
|
||||
_onLogout.add(user);
|
||||
if (req.container?.has<User>() == true) {
|
||||
var user = req.container?.make<User>();
|
||||
if (user != null) {
|
||||
_onLogout.add(user);
|
||||
}
|
||||
}
|
||||
|
||||
if (allowCookie == true) {
|
||||
res.cookies.removeWhere((cookie) => cookie.name == "token");
|
||||
res.cookies.removeWhere((cookie) => cookie.name == 'token');
|
||||
_addProtectedCookie(res, 'token', '""');
|
||||
}
|
||||
|
||||
if (options != null &&
|
||||
options.successRedirect != null &&
|
||||
options.successRedirect.isNotEmpty) {
|
||||
options.successRedirect!.isNotEmpty) {
|
||||
await res.redirect(options.successRedirect);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'dart:convert';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel3_framework/angel3_framework.dart';
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'options.dart';
|
||||
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel3_framework/angel3_framework.dart';
|
||||
import '../options.dart';
|
||||
import '../strategy.dart';
|
||||
|
||||
bool _validateString(String str) => str != null && str.isNotEmpty;
|
||||
bool _validateString(String? str) => str != null && str.isNotEmpty;
|
||||
|
||||
/// Determines the validity of an incoming username and password.
|
||||
typedef FutureOr<User> LocalAuthVerifier<User>(
|
||||
String username, String password);
|
||||
// typedef FutureOr<User> LocalAuthVerifier<User>(String? username, String? password);
|
||||
typedef LocalAuthVerifier<User> = FutureOr<User> Function(
|
||||
String? username, String? password);
|
||||
|
||||
class LocalAuthStrategy<User> extends AuthStrategy<User> {
|
||||
RegExp _rgxBasic = RegExp(r'^Basic (.+)$', caseSensitive: false);
|
||||
RegExp _rgxUsrPass = RegExp(r'^([^:]+):(.+)$');
|
||||
final RegExp _rgxBasic = RegExp(r'^Basic (.+)$', caseSensitive: false);
|
||||
final RegExp _rgxUsrPass = RegExp(r'^([^:]+):(.+)$');
|
||||
|
||||
LocalAuthVerifier<User> verifier;
|
||||
String usernameField;
|
||||
|
@ -23,35 +24,37 @@ class LocalAuthStrategy<User> extends AuthStrategy<User> {
|
|||
String realm;
|
||||
|
||||
LocalAuthStrategy(this.verifier,
|
||||
{String this.usernameField = 'username',
|
||||
String this.passwordField = 'password',
|
||||
String this.invalidMessage =
|
||||
'Please provide a valid username and password.',
|
||||
bool this.allowBasic = true,
|
||||
bool this.forceBasic = false,
|
||||
String this.realm = 'Authentication is required.'});
|
||||
{this.usernameField = 'username',
|
||||
this.passwordField = 'password',
|
||||
this.invalidMessage = 'Please provide a valid username and password.',
|
||||
this.allowBasic = true,
|
||||
this.forceBasic = false,
|
||||
this.realm = 'Authentication is required.'});
|
||||
|
||||
@override
|
||||
Future<User> authenticate(RequestContext req, ResponseContext res,
|
||||
[AngelAuthOptions options_]) async {
|
||||
AngelAuthOptions options = options_ ?? AngelAuthOptions();
|
||||
User verificationResult;
|
||||
Future<User?> authenticate(RequestContext req, ResponseContext res,
|
||||
[AngelAuthOptions? options_]) async {
|
||||
var options = options_ ?? AngelAuthOptions();
|
||||
User? verificationResult;
|
||||
|
||||
if (allowBasic) {
|
||||
String authHeader = req.headers.value('authorization') ?? "";
|
||||
var authHeader = req.headers?.value('authorization') ?? '';
|
||||
|
||||
if (_rgxBasic.hasMatch(authHeader)) {
|
||||
String base64AuthString = _rgxBasic.firstMatch(authHeader).group(1);
|
||||
String authString =
|
||||
String.fromCharCodes(base64.decode(base64AuthString));
|
||||
var base64AuthString = _rgxBasic.firstMatch(authHeader)?.group(1);
|
||||
if (base64AuthString == null) {
|
||||
return null;
|
||||
}
|
||||
var authString = String.fromCharCodes(base64.decode(base64AuthString));
|
||||
if (_rgxUsrPass.hasMatch(authString)) {
|
||||
Match usrPassMatch = _rgxUsrPass.firstMatch(authString);
|
||||
Match usrPassMatch = _rgxUsrPass.firstMatch(authString)!;
|
||||
verificationResult =
|
||||
await verifier(usrPassMatch.group(1), usrPassMatch.group(2));
|
||||
} else
|
||||
} else {
|
||||
throw AngelHttpException.badRequest(errors: [invalidMessage]);
|
||||
}
|
||||
|
||||
if (verificationResult == false || verificationResult == null) {
|
||||
if (verificationResult == null) {
|
||||
res
|
||||
..statusCode = 401
|
||||
..headers['www-authenticate'] = 'Basic realm="$realm"';
|
||||
|
@ -68,27 +71,29 @@ class LocalAuthStrategy<User> extends AuthStrategy<User> {
|
|||
.parseBody()
|
||||
.then((_) => req.bodyAsMap)
|
||||
.catchError((_) => <String, dynamic>{});
|
||||
if (_validateString(body[usernameField]?.toString()) &&
|
||||
_validateString(body[passwordField]?.toString())) {
|
||||
//if (body != null) {
|
||||
if (_validateString(body[usernameField].toString()) &&
|
||||
_validateString(body[passwordField].toString())) {
|
||||
verificationResult = await verifier(
|
||||
body[usernameField]?.toString(), body[passwordField]?.toString());
|
||||
body[usernameField].toString(), body[passwordField].toString());
|
||||
}
|
||||
//}
|
||||
}
|
||||
|
||||
if (verificationResult == false || verificationResult == null) {
|
||||
if (verificationResult == null) {
|
||||
if (options.failureRedirect != null &&
|
||||
options.failureRedirect.isNotEmpty) {
|
||||
options.failureRedirect!.isNotEmpty) {
|
||||
await res.redirect(options.failureRedirect, code: 401);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (forceBasic) {
|
||||
res.headers['www-authenticate'] = 'Basic realm="$realm"';
|
||||
throw AngelHttpException.notAuthenticated();
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
} else if (verificationResult != null && verificationResult != false) {
|
||||
} else if (verificationResult != false) {
|
||||
return verificationResult;
|
||||
} else {
|
||||
throw AngelHttpException.notAuthenticated();
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import 'dart:async';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel3_framework/angel3_framework.dart';
|
||||
import 'options.dart';
|
||||
|
||||
/// A function that handles login and signup for an Angel application.
|
||||
abstract class AuthStrategy<User> {
|
||||
/// Authenticates or rejects an incoming user.
|
||||
FutureOr<User> authenticate(RequestContext req, ResponseContext res,
|
||||
FutureOr<User?> authenticate(RequestContext req, ResponseContext res,
|
||||
[AngelAuthOptions<User> options]);
|
||||
}
|
||||
|
|
|
@ -1,26 +1,20 @@
|
|||
name: angel_auth
|
||||
name: angel3_auth
|
||||
description: A complete authentication plugin for Angel. Includes support for stateless JWT tokens, Basic Auth, and more.
|
||||
version: 3.0.0
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
homepage: https://github.com/angel-dart/angel_auth
|
||||
publish_to: none
|
||||
version: 4.0.1
|
||||
homepage: https://github.com/dukefirehawk/angel/tree/angel3/packages/auth
|
||||
environment:
|
||||
sdk: ">=2.10.0 <3.0.0"
|
||||
sdk: '>=2.12.0 <3.0.0'
|
||||
dependencies:
|
||||
angel_framework:
|
||||
git:
|
||||
url: https://github.com/dukefirehawk/angel.git
|
||||
ref: sdk-2.12.x
|
||||
path: packages/framework
|
||||
charcode: ^1.0.0
|
||||
collection: ^1.0.0
|
||||
angel3_framework: ^4.0.0
|
||||
charcode: ^1.2.0
|
||||
collection: ^1.15.0
|
||||
crypto: ^3.0.0
|
||||
http_parser: ^4.0.0
|
||||
meta: ^1.0.0
|
||||
quiver_hashcode: ^2.0.0
|
||||
meta: ^1.3.0
|
||||
quiver: ^3.0.0
|
||||
dev_dependencies:
|
||||
http: ^0.13.0
|
||||
http: ^0.13.1
|
||||
io: ^1.0.0
|
||||
logging: ^1.0.0
|
||||
pedantic: ^1.0.0
|
||||
test: ^1.15.7
|
||||
pedantic: ^1.11.0
|
||||
test: ^1.17.4
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import "package:angel_auth/src/auth_token.dart";
|
||||
import "package:crypto/crypto.dart";
|
||||
import "package:test/test.dart";
|
||||
import 'package:angel3_auth/src/auth_token.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
main() async {
|
||||
final Hmac hmac = Hmac(sha256, "angel_auth".codeUnits);
|
||||
void main() async {
|
||||
final hmac = Hmac(sha256, 'angel_auth'.codeUnits);
|
||||
|
||||
test("sample serialization", () {
|
||||
var token = AuthToken(ipAddress: "localhost", userId: "thosakwe");
|
||||
test('sample serialization', () {
|
||||
var token = AuthToken(ipAddress: 'localhost', userId: 'thosakwe');
|
||||
var jwt = token.serialize(hmac);
|
||||
print(jwt);
|
||||
|
||||
|
@ -17,7 +17,7 @@ main() async {
|
|||
});
|
||||
|
||||
test('custom payload', () {
|
||||
var token = AuthToken(ipAddress: "localhost", userId: "thosakwe", payload: {
|
||||
var token = AuthToken(ipAddress: 'localhost', userId: 'thosakwe', payload: {
|
||||
"foo": "bar",
|
||||
"baz": {
|
||||
"one": 1,
|
||||
|
|
|
@ -1,22 +1,24 @@
|
|||
import 'dart:io';
|
||||
import 'package:angel_auth/angel_auth.dart';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_framework/http.dart';
|
||||
import 'package:angel3_auth/angel3_auth.dart';
|
||||
import 'package:angel3_framework/angel3_framework.dart';
|
||||
import 'package:angel3_framework/http.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:collection/collection.dart' show IterableExtension;
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:io/ansi.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
class User extends Model {
|
||||
String username, password;
|
||||
String? username, password;
|
||||
|
||||
User({this.username, this.password});
|
||||
|
||||
static User parse(Map map) {
|
||||
return User(
|
||||
username: map['username'] as String,
|
||||
password: map['password'] as String,
|
||||
username: map['username'] as String?,
|
||||
password: map['password'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -31,13 +33,13 @@ class User extends Model {
|
|||
}
|
||||
}
|
||||
|
||||
main() {
|
||||
Angel app;
|
||||
AngelHttp angelHttp;
|
||||
AngelAuth<User> auth;
|
||||
http.Client client;
|
||||
void main() {
|
||||
late Angel app;
|
||||
late AngelHttp angelHttp;
|
||||
AngelAuth<User?> auth;
|
||||
http.Client? client;
|
||||
HttpServer server;
|
||||
String url;
|
||||
String? url;
|
||||
|
||||
setUp(() async {
|
||||
hierarchicalLoggingEnabled = true;
|
||||
|
@ -47,7 +49,7 @@ main() {
|
|||
|
||||
var oldErrorHandler = app.errorHandler;
|
||||
app.errorHandler = (e, req, res) {
|
||||
app.logger.severe(e.message, e, e.stackTrace ?? StackTrace.current);
|
||||
app.logger!.severe(e.message, e, e.stackTrace ?? StackTrace.current);
|
||||
return oldErrorHandler(e, req, res);
|
||||
};
|
||||
|
||||
|
@ -66,24 +68,26 @@ main() {
|
|||
});
|
||||
|
||||
await app
|
||||
.findService('users')
|
||||
.findService('users')!
|
||||
.create({'username': 'jdoe1', 'password': 'password'});
|
||||
|
||||
auth = AngelAuth<User>();
|
||||
auth.serializer = (u) => u.id;
|
||||
auth = AngelAuth<User?>();
|
||||
auth.serializer = (u) => u!.id;
|
||||
auth.deserializer =
|
||||
(id) async => await app.findService('users').read(id) as User;
|
||||
(id) async => await app.findService('users')!.read(id) as User;
|
||||
|
||||
await app.configure(auth.configureServer);
|
||||
|
||||
auth.strategies['local'] = LocalAuthStrategy((username, password) async {
|
||||
var users = await app
|
||||
.findService('users')
|
||||
.findService('users')!
|
||||
.index()
|
||||
.then((it) => it.map<User>((m) => User.parse(m as Map)).toList());
|
||||
return users.firstWhere(
|
||||
(user) => user.username == username && user.password == password,
|
||||
orElse: () => null);
|
||||
|
||||
var result = users.firstWhereOrNull(
|
||||
(user) => user.username == username && user.password == password);
|
||||
|
||||
return Future.value(result);
|
||||
});
|
||||
|
||||
app.post(
|
||||
|
@ -97,8 +101,8 @@ main() {
|
|||
|
||||
app.chain([
|
||||
(req, res) {
|
||||
if (!req.container.has<User>()) {
|
||||
req.container.registerSingleton<User>(
|
||||
if (!req.container!.has<User>()) {
|
||||
req.container!.registerSingleton<User>(
|
||||
User(username: req.params['name']?.toString()));
|
||||
}
|
||||
return true;
|
||||
|
@ -114,15 +118,15 @@ main() {
|
|||
});
|
||||
|
||||
tearDown(() async {
|
||||
client.close();
|
||||
client!.close();
|
||||
await angelHttp.close();
|
||||
app = null;
|
||||
//app = null;
|
||||
client = null;
|
||||
url = null;
|
||||
});
|
||||
|
||||
test('login', () async {
|
||||
final response = await client.post(Uri.parse('$url/login'),
|
||||
final response = await client!.post(Uri.parse('$url/login'),
|
||||
body: {'username': 'jdoe1', 'password': 'password'});
|
||||
print('Response: ${response.body}');
|
||||
expect(response.body, equals('Hello!'));
|
||||
|
@ -132,7 +136,7 @@ main() {
|
|||
: null);
|
||||
|
||||
test('preserve existing user', () async {
|
||||
final response = await client.post(Uri.parse('$url/existing/foo'),
|
||||
final response = await client!.post(Uri.parse('$url/existing/foo'),
|
||||
body: {'username': 'jdoe1', 'password': 'password'},
|
||||
headers: {'accept': 'application/json'});
|
||||
print('Response: ${response.body}');
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:angel_auth/angel_auth.dart';
|
||||
import 'package:angel3_auth/angel3_auth.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
|
@ -70,6 +70,7 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
/* Deprecated as clientId and clientSecret cannot be null
|
||||
test('ensures id not null', () {
|
||||
expect(
|
||||
() => ExternalAuthOptions(
|
||||
|
@ -89,6 +90,7 @@ void main() {
|
|||
throwsArgumentError,
|
||||
);
|
||||
});
|
||||
*/
|
||||
});
|
||||
|
||||
group('fromMap()', () {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'package:angel_auth/angel_auth.dart';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_framework/http.dart';
|
||||
import 'package:angel3_auth/angel3_auth.dart';
|
||||
import 'package:angel3_framework/angel3_framework.dart';
|
||||
import 'package:angel3_framework/http.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:logging/logging.dart';
|
||||
|
@ -13,11 +13,11 @@ var localOpts = AngelAuthOptions<Map<String, String>>(
|
|||
failureRedirect: '/failure', successRedirect: '/success');
|
||||
Map<String, String> sampleUser = {'hello': 'world'};
|
||||
|
||||
Future<Map<String, String>> verifier(String username, String password) async {
|
||||
Future<Map<String, String>> verifier(String? username, String? password) async {
|
||||
if (username == 'username' && password == 'password') {
|
||||
return sampleUser;
|
||||
} else {
|
||||
return null;
|
||||
throw ArgumentError('Unexpected type for data');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,10 +31,10 @@ Future wireAuth(Angel app) async {
|
|||
|
||||
void main() async {
|
||||
Angel app;
|
||||
AngelHttp angelHttp;
|
||||
http.Client client;
|
||||
String url;
|
||||
String basicAuthUrl;
|
||||
late AngelHttp angelHttp;
|
||||
http.Client? client;
|
||||
String? url;
|
||||
String? basicAuthUrl;
|
||||
|
||||
setUp(() async {
|
||||
client = http.Client();
|
||||
|
@ -72,7 +72,7 @@ void main() async {
|
|||
});
|
||||
|
||||
test('can use "auth" as middleware', () async {
|
||||
var response = await client.get(Uri.parse('$url/success'),
|
||||
var response = await client!.get(Uri.parse('$url/success'),
|
||||
headers: {'Accept': 'application/json'});
|
||||
print(response.body);
|
||||
expect(response.statusCode, equals(403));
|
||||
|
@ -80,7 +80,7 @@ void main() async {
|
|||
|
||||
test('successRedirect', () async {
|
||||
var postData = {'username': 'username', 'password': 'password'};
|
||||
var response = await client.post(Uri.parse('$url/login'),
|
||||
var response = await client!.post(Uri.parse('$url/login'),
|
||||
body: json.encode(postData),
|
||||
headers: {'content-type': 'application/json'});
|
||||
expect(response.statusCode, equals(302));
|
||||
|
@ -89,7 +89,7 @@ void main() async {
|
|||
|
||||
test('failureRedirect', () async {
|
||||
var postData = {'username': 'password', 'password': 'username'};
|
||||
var response = await client.post(Uri.parse('$url/login'),
|
||||
var response = await client!.post(Uri.parse('$url/login'),
|
||||
body: json.encode(postData),
|
||||
headers: {'content-type': 'application/json'});
|
||||
print('Login response: ${response.body}');
|
||||
|
@ -99,13 +99,13 @@ void main() async {
|
|||
|
||||
test('allow basic', () async {
|
||||
var authString = base64.encode('username:password'.runes.toList());
|
||||
var response = await client.get(Uri.parse('$url/hello'),
|
||||
var response = await client!.get(Uri.parse('$url/hello'),
|
||||
headers: {'authorization': 'Basic $authString'});
|
||||
expect(response.body, equals('"Woo auth"'));
|
||||
});
|
||||
|
||||
test('allow basic via URL encoding', () async {
|
||||
var response = await client.get(Uri.parse('$basicAuthUrl/hello'));
|
||||
var response = await client!.get(Uri.parse('$basicAuthUrl/hello'));
|
||||
expect(response.body, equals('"Woo auth"'));
|
||||
});
|
||||
|
||||
|
@ -113,12 +113,13 @@ void main() async {
|
|||
auth.strategies.clear();
|
||||
auth.strategies['local'] =
|
||||
LocalAuthStrategy(verifier, forceBasic: true, realm: 'test');
|
||||
var response = await client.get(Uri.parse('$url/hello'), headers: {
|
||||
var response = await client?.get(Uri.parse('$url/hello'), headers: {
|
||||
'accept': 'application/json',
|
||||
'content-type': 'application/json'
|
||||
});
|
||||
print(response.headers);
|
||||
print('Body <${response.body}>');
|
||||
expect(response.headers['www-authenticate'], equals('Basic realm="test"'));
|
||||
print('Header = ${response?.headers}');
|
||||
print('Body <${response?.body}>');
|
||||
var head = response?.headers['www-authenticate'];
|
||||
expect(head, equals('Basic realm="test"'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:angel_auth/angel_auth.dart';
|
||||
import 'package:angel3_auth/angel3_auth.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
const Duration threeDays = const Duration(days: 3);
|
||||
const Duration threeDays = Duration(days: 3);
|
||||
|
||||
void main() {
|
||||
Cookie defaultCookie;
|
||||
late Cookie defaultCookie;
|
||||
var auth = AngelAuth(
|
||||
secureCookies: true,
|
||||
cookieDomain: 'SECURE',
|
||||
|
@ -21,7 +21,7 @@ void main() {
|
|||
|
||||
test('sets expires', () {
|
||||
var now = DateTime.now();
|
||||
var expiry = auth.protectCookie(defaultCookie).expires;
|
||||
var expiry = auth.protectCookie(defaultCookie).expires!;
|
||||
var diff = expiry.difference(now);
|
||||
expect(diff.inSeconds, threeDays.inSeconds);
|
||||
});
|
||||
|
|
|
@ -38,8 +38,8 @@ main() async {
|
|||
'http://localhost:3000/auth/twitter/callback',
|
||||
),
|
||||
(twit, req, res) async {
|
||||
var response = await twit.twitterClient
|
||||
.get('https://api.twitter.com/1.1/account/verify_credentials.json');
|
||||
var response = await twit.twitterClient.get(Uri.parse(
|
||||
'https://api.twitter.com/1.1/account/verify_credentials.json'));
|
||||
var userData = json.decode(response.body) as Map;
|
||||
return _User(userData['screen_name'] as String);
|
||||
},
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
name: "angel_auth_twitter"
|
||||
#author: "Tobe O <thosakwe@gmail.com>"
|
||||
version: 3.0.0
|
||||
description: "package:angel_auth strategy for Twitter login. Auto-signs requests."
|
||||
homepage: "https://github.com/angel-dart/auth_twitter.git"
|
||||
publish_to: none
|
||||
environment:
|
||||
sdk: ">=2.10.0 <3.0.0"
|
||||
homepage: "https://github.com/angel-dart/auth_twitter.git"
|
||||
version: 3.0.0
|
||||
publish_to: none
|
||||
dependencies:
|
||||
angel_auth:
|
||||
git:
|
||||
|
|
2
packages/cli/AUTHORS.md
Normal file
2
packages/cli/AUTHORS.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
Tobe O <thosakwe@gmail.com>
|
||||
Thomas Hii <thomashii@dukefirehawk.com>
|
|
@ -1,3 +1,6 @@
|
|||
# 3.0.0
|
||||
* Migrated to work with Dart SDK 2.12.x Non NNBD
|
||||
|
||||
# 2.1.7+1
|
||||
* Fix a bug where new directories were not being created in
|
||||
`init`.
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
# Todo
|
||||
* Migrate inflection2, mustache4dart2 and prompts packages to NNBD
|
||||
|
||||
* `service`
|
||||
* Add tests
|
||||
|
|
96
packages/client/.gitignore
vendored
96
packages/client/.gitignore
vendored
|
@ -1,15 +1,34 @@
|
|||
# See https://www.dartlang.org/tools/private-files.html
|
||||
|
||||
# Files and directories created by pub
|
||||
.buildlog
|
||||
.dart_tool
|
||||
.packages
|
||||
.project
|
||||
.pub/
|
||||
build/
|
||||
|
||||
# If you're building an application, you may want to check-in your pubspec.lock
|
||||
pubspec.lock
|
||||
|
||||
# Directory created by dartdoc
|
||||
# If you don't generate documentation locally you can remove this line.
|
||||
doc/api/
|
||||
|
||||
### Dart template
|
||||
# See https://www.dartlang.org/tools/private-files.html
|
||||
|
||||
# Files and directories created by pub
|
||||
|
||||
# SDK 1.20 and later (no longer creates packages directories)
|
||||
|
||||
# Older SDK versions
|
||||
# (Include if the minimum SDK version specified in pubsepc.yaml is earlier than 1.20)
|
||||
.project
|
||||
.buildlog
|
||||
**/packages/
|
||||
|
||||
|
||||
# Files created by dart2js
|
||||
# (Most Dart developers will use pub build to compile Dart, use/modify these
|
||||
# (Most Dart developers will use pub build to compile Dart, use/modify these
|
||||
# rules if you intend to use dart2js directly
|
||||
# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
|
||||
# differentiate from explicit Javascript files)
|
||||
|
@ -20,62 +39,33 @@ build/
|
|||
*.info.json
|
||||
|
||||
# Directory created by dartdoc
|
||||
doc/api/
|
||||
|
||||
# Don't commit pubspec lock file
|
||||
# Don't commit pubspec lock file
|
||||
# (Library packages only! Remove pattern if developing an application package)
|
||||
pubspec.lock
|
||||
.idea
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
lib/angel_client.js
|
||||
*.sum
|
||||
# User-specific stuff:
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
## VsCode
|
||||
.vscode/
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
## File-based project format:
|
||||
*.iws
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
## Plugin-specific files:
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
# IntelliJ
|
||||
.idea/
|
||||
/out/
|
||||
.idea_modules/
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
jspm_packages
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
.dart_tool
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
|
12
packages/client/AUTHORS.md
Normal file
12
packages/client/AUTHORS.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
Primary Authors
|
||||
===============
|
||||
|
||||
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
|
||||
|
||||
Thomas is the current maintainer of the code base. He has refactored and migrated the
|
||||
code base to support NNBD.
|
||||
|
||||
* __[Tobe O](thosakwe@gmail.com)__
|
||||
|
||||
Tobe has written much of the original code prior to NNBD migration. He has moved on and
|
||||
is no longer involved with the project.
|
|
@ -1,3 +1,11 @@
|
|||
|
||||
|
||||
# 4.0.0
|
||||
* Migrated to support Dart SDK 2.12.x NNBD
|
||||
|
||||
# 3.0.0
|
||||
* Migrated to work with Dart SDK 2.12.x Non NNBD
|
||||
|
||||
# 2.0.2
|
||||
* `_join` previously discarded quer parameters, etc.
|
||||
* Allow any `Map<String, dynamic>` as body, not just `Map<String, String>`.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 angel-dart
|
||||
Copyright (c) 2021 dukefirehawk.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
@ -1,23 +1,20 @@
|
|||
# angel_client
|
||||
# angel3_client
|
||||
[![version](https://img.shields.io/badge/pub-v4.0.0-brightgreen)](https://pub.dartlang.org/packages/angel3_client)
|
||||
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
|
||||
[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion)
|
||||
|
||||
[![Pub](https://img.shields.io/pub/v/angel_client.svg)](https://pub.dartlang.org/packages/angel_client)
|
||||
[![build status](https://travis-ci.org/angel-dart/client.svg)](https://travis-ci.org/angel-dart/client)
|
||||
|
||||
Client library for the Angel framework.
|
||||
This library provides virtually the same API as an Angel server.
|
||||
The client can run in the browser, in Flutter, or on the command-line.
|
||||
In addition, the client supports `angel_auth` authentication.
|
||||
[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/angel3/packages/client/LICENSE)
|
||||
|
||||
# Usage
|
||||
|
||||
```dart
|
||||
// Choose one or the other, depending on platform
|
||||
import 'package:angel_client/io.dart';
|
||||
import 'package:angel_client/browser.dart';
|
||||
import 'package:angel_client/flutter.dart';
|
||||
import 'package:angel3_client/io.dart';
|
||||
import 'package:angel3_client/browser.dart';
|
||||
import 'package:angel3_client/flutter.dart';
|
||||
|
||||
main() async {
|
||||
Angel app = new Rest("http://localhost:3000");
|
||||
Angel app = Rest("http://localhost:3000");
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -33,7 +30,7 @@ foo() async {
|
|||
}
|
||||
```
|
||||
|
||||
The CLI client also supports reflection via `json_god`. There is no need to work with Maps;
|
||||
The CLI client also supports reflection via `angel3_json_god`. There is no need to work with Maps;
|
||||
you can use the same class on the client and the server.
|
||||
|
||||
```dart
|
||||
|
@ -96,9 +93,9 @@ Use `ServiceList` for this case:
|
|||
|
||||
```dart
|
||||
build(BuildContext context) async {
|
||||
var list = new ServiceList(app.service('api/todos'));
|
||||
var list = ServiceList(app.service('api/todos'));
|
||||
|
||||
return new StreamBuilder(
|
||||
return StreamBuilder(
|
||||
stream: list.onChange,
|
||||
builder: _yourBuildFunction,
|
||||
);
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
import 'dart:async';
|
||||
import 'package:angel_client/angel_client.dart';
|
||||
import 'package:angel3_client/angel3_client.dart';
|
||||
|
||||
Future doSomething(Angel app) async {
|
||||
var userService = app
|
||||
.service<String, Map<String, dynamic>>('api/users')
|
||||
.map(User.fromMap, User.toMap);
|
||||
|
||||
var users = await userService.index();
|
||||
var users = await (userService.index() as FutureOr<List<User>>);
|
||||
print('Name: ${users.first.name}');
|
||||
}
|
||||
|
||||
class User {
|
||||
final String name;
|
||||
final String? name;
|
||||
|
||||
User({this.name});
|
||||
|
||||
static User fromMap(Map data) => User(name: data['name'] as String);
|
||||
static User fromMap(Map data) => User(name: data['name'] as String?);
|
||||
|
||||
static Map<String, String> toMap(User user) => {'name': user.name};
|
||||
static Map<String, String?> toMap(User user) => {'name': user.name};
|
||||
}
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
/// Client library for the Angel framework.
|
||||
library angel_client;
|
||||
library angel3_client;
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
export 'package:angel_http_exception/angel_http_exception.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
export 'package:angel3_http_exception/angel3_http_exception.dart';
|
||||
|
||||
/// A function that configures an [Angel] client in some way.
|
||||
typedef FutureOr<void> AngelConfigurer(Angel app);
|
||||
typedef AngelConfigurer = FutureOr<void> Function(Angel app);
|
||||
|
||||
/// A function that deserializes data received from the server.
|
||||
///
|
||||
/// This is only really necessary in the browser, where `json_god`
|
||||
/// doesn't work.
|
||||
typedef T AngelDeserializer<T>(x);
|
||||
typedef AngelDeserializer<T> = T? Function(dynamic x);
|
||||
|
||||
/// Represents an Angel server that we are querying.
|
||||
abstract class Angel extends http.BaseClient {
|
||||
|
@ -23,13 +22,13 @@ abstract class Angel extends http.BaseClient {
|
|||
/// that is automatically attached to every request sent.
|
||||
///
|
||||
/// This is designed with `package:angel_auth` in mind.
|
||||
String authToken;
|
||||
String? authToken;
|
||||
|
||||
/// The root URL at which the target server.
|
||||
final Uri baseUrl;
|
||||
|
||||
Angel(baseUrl)
|
||||
: this.baseUrl = baseUrl is Uri ? baseUrl : Uri.parse(baseUrl.toString());
|
||||
: baseUrl = baseUrl is Uri ? baseUrl : Uri.parse(baseUrl.toString());
|
||||
|
||||
/// Prefer to use [baseUrl] instead.
|
||||
@deprecated
|
||||
|
@ -46,7 +45,7 @@ abstract class Angel extends http.BaseClient {
|
|||
///
|
||||
/// The given [credentials] are sent to server as-is; the request body is sent as JSON.
|
||||
Future<AngelAuthResult> authenticate(
|
||||
{@required String type,
|
||||
{required String type,
|
||||
credentials,
|
||||
String authEndpoint = '/auth',
|
||||
@deprecated String reviveEndpoint = '/auth/token'});
|
||||
|
@ -85,57 +84,59 @@ abstract class Angel extends http.BaseClient {
|
|||
/// You can pass a custom [deserializer], which is typically necessary in cases where
|
||||
/// `dart:mirrors` does not exist.
|
||||
Service<Id, Data> service<Id, Data>(String path,
|
||||
{@deprecated Type type, AngelDeserializer<Data> deserializer});
|
||||
{@deprecated Type? type, AngelDeserializer<Data>? deserializer});
|
||||
|
||||
//@override
|
||||
//Future<http.Response> delete(url, {Map<String, String> headers});
|
||||
|
||||
@override
|
||||
Future<http.Response> get(url, {Map<String, String> headers});
|
||||
Future<http.Response> get(url, {Map<String, String>? headers});
|
||||
|
||||
@override
|
||||
Future<http.Response> head(url, {Map<String, String> headers});
|
||||
Future<http.Response> head(url, {Map<String, String>? headers});
|
||||
|
||||
@override
|
||||
Future<http.Response> patch(url,
|
||||
{body, Map<String, String> headers, Encoding encoding});
|
||||
{body, Map<String, String>? headers, Encoding? encoding});
|
||||
|
||||
@override
|
||||
Future<http.Response> post(url,
|
||||
{body, Map<String, String> headers, Encoding encoding});
|
||||
{body, Map<String, String>? headers, Encoding? encoding});
|
||||
|
||||
@override
|
||||
Future<http.Response> put(url,
|
||||
{body, Map<String, String> headers, Encoding encoding});
|
||||
{body, Map<String, String>? headers, Encoding? encoding});
|
||||
}
|
||||
|
||||
/// Represents the result of authentication with an Angel server.
|
||||
class AngelAuthResult {
|
||||
String _token;
|
||||
String? _token;
|
||||
final Map<String, dynamic> data = {};
|
||||
|
||||
/// The JSON Web token that was sent with this response.
|
||||
String get token => _token;
|
||||
String? get token => _token;
|
||||
|
||||
AngelAuthResult({String token, Map<String, dynamic> data = const {}}) {
|
||||
AngelAuthResult({String? token, Map<String, dynamic> data = const {}}) {
|
||||
_token = token;
|
||||
this.data.addAll(data ?? {});
|
||||
this.data.addAll(data);
|
||||
}
|
||||
|
||||
/// Attempts to deserialize a response from a [Map].
|
||||
factory AngelAuthResult.fromMap(Map data) {
|
||||
factory AngelAuthResult.fromMap(Map? data) {
|
||||
final result = AngelAuthResult();
|
||||
|
||||
if (data is Map && data.containsKey('token') && data['token'] is String)
|
||||
if (data is Map && data.containsKey('token') && data['token'] is String) {
|
||||
result._token = data['token'].toString();
|
||||
}
|
||||
|
||||
if (data is Map)
|
||||
result.data.addAll((data['data'] as Map<String, dynamic>) ?? {});
|
||||
if (data is Map) {
|
||||
result.data.addAll((data['data'] as Map<String, dynamic>?) ?? {});
|
||||
}
|
||||
|
||||
if (result.token == null) {
|
||||
throw FormatException(
|
||||
'The required "token" field was not present in the given data.');
|
||||
} else if (data['data'] is! Map) {
|
||||
} else if (data!['data'] is! Map) {
|
||||
throw FormatException(
|
||||
'The required "data" field in the given data was not a map; instead, it was ${data['data']}.');
|
||||
}
|
||||
|
@ -145,7 +146,7 @@ class AngelAuthResult {
|
|||
|
||||
/// Attempts to deserialize a response from a [String].
|
||||
factory AngelAuthResult.fromJson(String s) =>
|
||||
AngelAuthResult.fromMap(json.decode(s) as Map);
|
||||
AngelAuthResult.fromMap(json.decode(s) as Map?);
|
||||
|
||||
/// Converts this instance into a JSON-friendly representation.
|
||||
Map<String, dynamic> toJson() {
|
||||
|
@ -179,22 +180,22 @@ abstract class Service<Id, Data> {
|
|||
Future close();
|
||||
|
||||
/// Retrieves all resources.
|
||||
Future<List<Data>> index([Map<String, dynamic> params]);
|
||||
Future<List<Data>?> index([Map<String, dynamic>? params]);
|
||||
|
||||
/// Retrieves the desired resource.
|
||||
Future<Data> read(Id id, [Map<String, dynamic> params]);
|
||||
Future<Data> read(Id id, [Map<String, dynamic>? params]);
|
||||
|
||||
/// Creates a resource.
|
||||
Future<Data> create(Data data, [Map<String, dynamic> params]);
|
||||
Future<Data> create(Data data, [Map<String, dynamic>? params]);
|
||||
|
||||
/// Modifies a resource.
|
||||
Future<Data> modify(Id id, Data data, [Map<String, dynamic> params]);
|
||||
Future<Data> modify(Id id, Data data, [Map<String, dynamic>? params]);
|
||||
|
||||
/// Overwrites a resource.
|
||||
Future<Data> update(Id id, Data data, [Map<String, dynamic> params]);
|
||||
Future<Data> update(Id id, Data data, [Map<String, dynamic>? params]);
|
||||
|
||||
/// Removes the given resource.
|
||||
Future<Data> remove(Id id, [Map<String, dynamic> params]);
|
||||
Future<Data> remove(Id id, [Map<String, dynamic>? params]);
|
||||
|
||||
/// Creates a [Service] that wraps over this one, and maps input and output using two converter functions.
|
||||
///
|
||||
|
@ -218,17 +219,17 @@ class _MappedService<Id, Data, U> extends Service<Id, U> {
|
|||
Future close() => Future.value();
|
||||
|
||||
@override
|
||||
Future<U> create(U data, [Map<String, dynamic> params]) {
|
||||
Future<U> create(U data, [Map<String, dynamic>? params]) {
|
||||
return inner.create(decoder(data)).then(encoder);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<U>> index([Map<String, dynamic> params]) {
|
||||
return inner.index(params).then((l) => l.map(encoder).toList());
|
||||
Future<List<U>> index([Map<String, dynamic>? params]) {
|
||||
return inner.index(params).then((l) => l!.map(encoder).toList());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<U> modify(Id id, U data, [Map<String, dynamic> params]) {
|
||||
Future<U> modify(Id id, U data, [Map<String, dynamic>? params]) {
|
||||
return inner.modify(id, decoder(data), params).then(encoder);
|
||||
}
|
||||
|
||||
|
@ -252,17 +253,17 @@ class _MappedService<Id, Data, U> extends Service<Id, U> {
|
|||
Stream<U> get onUpdated => inner.onUpdated.map(encoder);
|
||||
|
||||
@override
|
||||
Future<U> read(Id id, [Map<String, dynamic> params]) {
|
||||
Future<U> read(Id id, [Map<String, dynamic>? params]) {
|
||||
return inner.read(id, params).then(encoder);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<U> remove(Id id, [Map<String, dynamic> params]) {
|
||||
Future<U> remove(Id id, [Map<String, dynamic>? params]) {
|
||||
return inner.remove(id, params).then(encoder);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<U> update(Id id, U data, [Map<String, dynamic> params]) {
|
||||
Future<U> update(Id id, U data, [Map<String, dynamic>? params]) {
|
||||
return inner.update(id, decoder(data), params).then(encoder);
|
||||
}
|
||||
}
|
||||
|
@ -275,9 +276,9 @@ class ServiceList<Id, Data> extends DelegatingList<Data> {
|
|||
/// A function used to compare the ID's two items for equality.
|
||||
///
|
||||
/// Defaults to comparing the [idField] of `Map` instances.
|
||||
Equality<Data> get equality => _equality;
|
||||
Equality<Data>? get equality => _equality;
|
||||
|
||||
Equality<Data> _equality;
|
||||
Equality<Data>? _equality;
|
||||
|
||||
final Service<Id, Data> service;
|
||||
|
||||
|
@ -285,15 +286,16 @@ class ServiceList<Id, Data> extends DelegatingList<Data> {
|
|||
|
||||
final List<StreamSubscription> _subs = [];
|
||||
|
||||
ServiceList(this.service, {this.idField = 'id', Equality<Data> equality})
|
||||
ServiceList(this.service, {this.idField = 'id', Equality<Data>? equality})
|
||||
: super([]) {
|
||||
_equality = equality;
|
||||
_equality ??= EqualityBy<Data, Id>((map) {
|
||||
if (map is Map)
|
||||
return map[idField ?? 'id'] as Id;
|
||||
else
|
||||
_equality ??= EqualityBy<Data, Id?>((map) {
|
||||
if (map is Map) {
|
||||
return map[idField] as Id?;
|
||||
} else {
|
||||
throw UnsupportedError(
|
||||
'ServiceList only knows how to find the id from a Map object. Provide a custom `Equality` in your call to the constructor.');
|
||||
}
|
||||
});
|
||||
// Index
|
||||
_subs.add(service.onIndexed.where(_notNull).listen((data) {
|
||||
|
@ -310,15 +312,17 @@ class ServiceList<Id, Data> extends DelegatingList<Data> {
|
|||
}));
|
||||
|
||||
// Modified/Updated
|
||||
handleModified(Data item) {
|
||||
void handleModified(Data item) {
|
||||
var indices = <int>[];
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (_equality.equals(item, this[i])) indices.add(i);
|
||||
for (var i = 0; i < length; i++) {
|
||||
if (_equality!.equals(item, this[i])) indices.add(i);
|
||||
}
|
||||
|
||||
if (indices.isNotEmpty) {
|
||||
for (var i in indices) this[i] = item;
|
||||
for (var i in indices) {
|
||||
this[i] = item;
|
||||
}
|
||||
|
||||
_onChange.add(this);
|
||||
}
|
||||
|
@ -331,7 +335,7 @@ class ServiceList<Id, Data> extends DelegatingList<Data> {
|
|||
|
||||
// Removed
|
||||
_subs.add(service.onRemoved.where(_notNull).listen((item) {
|
||||
removeWhere((x) => _equality.equals(item, x));
|
||||
removeWhere((x) => _equality!.equals(item, x));
|
||||
_onChange.add(this);
|
||||
}));
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert' show Encoding;
|
||||
import 'package:angel_http_exception/angel_http_exception.dart';
|
||||
import 'package:angel3_http_exception/angel3_http_exception.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:http/src/base_client.dart' as http;
|
||||
import 'package:http/src/base_request.dart' as http;
|
||||
|
@ -8,7 +8,7 @@ import 'package:http/src/request.dart' as http;
|
|||
import 'package:http/src/response.dart' as http;
|
||||
import 'package:http/src/streamed_response.dart' as http;
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'angel_client.dart';
|
||||
import 'angel3_client.dart';
|
||||
|
||||
const Map<String, String> _readHeaders = {'Accept': 'application/json'};
|
||||
const Map<String, String> _writeHeaders = {
|
||||
|
@ -16,17 +16,15 @@ const Map<String, String> _writeHeaders = {
|
|||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
Map<String, String> _buildQuery(Map<String, dynamic> params) {
|
||||
Map<String, String>? _buildQuery(Map<String, dynamic>? params) {
|
||||
return params?.map((k, v) => MapEntry(k, v.toString()));
|
||||
}
|
||||
|
||||
bool _invalid(http.Response response) =>
|
||||
response.statusCode == null ||
|
||||
response.statusCode < 200 ||
|
||||
response.statusCode >= 300;
|
||||
response.statusCode < 200 || response.statusCode >= 300;
|
||||
|
||||
AngelHttpException failure(http.Response response,
|
||||
{error, String message, StackTrace stack}) {
|
||||
{error, String? message, StackTrace? stack}) {
|
||||
try {
|
||||
var v = json.decode(response.body);
|
||||
|
||||
|
@ -52,7 +50,7 @@ abstract class BaseAngelClient extends Angel {
|
|||
final StreamController<AngelAuthResult> _onAuthenticated =
|
||||
StreamController<AngelAuthResult>();
|
||||
final List<Service> _services = [];
|
||||
final http.BaseClient client;
|
||||
final http.BaseClient? client;
|
||||
|
||||
@override
|
||||
Stream<AngelAuthResult> get onAuthenticated => _onAuthenticated.stream;
|
||||
|
@ -61,7 +59,7 @@ abstract class BaseAngelClient extends Angel {
|
|||
|
||||
@override
|
||||
Future<AngelAuthResult> authenticate(
|
||||
{String type,
|
||||
{String? type,
|
||||
credentials,
|
||||
String authEndpoint = '/auth',
|
||||
@deprecated String reviveEndpoint = '/auth/token'}) async {
|
||||
|
@ -92,14 +90,12 @@ abstract class BaseAngelClient extends Angel {
|
|||
//var v = json.decode(response.body);
|
||||
var v = jsonDecode(response.body);
|
||||
|
||||
if (v is! Map ||
|
||||
!(v as Map).containsKey('data') ||
|
||||
!(v as Map).containsKey('token')) {
|
||||
if (v is! Map || !v.containsKey('data') || !v.containsKey('token')) {
|
||||
throw AngelHttpException.notAuthenticated(
|
||||
message: "Auth endpoint '$url' did not return a proper response.");
|
||||
}
|
||||
|
||||
var r = AngelAuthResult.fromMap(v as Map);
|
||||
var r = AngelAuthResult.fromMap(v);
|
||||
_onAuthenticated.add(r);
|
||||
return r;
|
||||
} on AngelHttpException {
|
||||
|
@ -111,7 +107,7 @@ abstract class BaseAngelClient extends Angel {
|
|||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
client.close();
|
||||
client!.close();
|
||||
await _onAuthenticated.close();
|
||||
await Future.wait(_services.map((s) => s.close())).then((_) {
|
||||
_services.clear();
|
||||
|
@ -128,13 +124,13 @@ abstract class BaseAngelClient extends Angel {
|
|||
if (authToken?.isNotEmpty == true) {
|
||||
request.headers['authorization'] ??= 'Bearer $authToken';
|
||||
}
|
||||
return client.send(request);
|
||||
return client!.send(request);
|
||||
}
|
||||
|
||||
/// Sends a non-streaming [Request] and returns a non-streaming [Response].
|
||||
Future<http.Response> sendUnstreamed(
|
||||
String method, url, Map<String, String> headers,
|
||||
[body, Encoding encoding]) async {
|
||||
String method, url, Map<String, String>? headers,
|
||||
[body, Encoding? encoding]) async {
|
||||
var request =
|
||||
http.Request(method, url is Uri ? url : Uri.parse(url.toString()));
|
||||
|
||||
|
@ -160,12 +156,12 @@ abstract class BaseAngelClient extends Angel {
|
|||
|
||||
@override
|
||||
Service<Id, Data> service<Id, Data>(String path,
|
||||
{Type type, AngelDeserializer<Data> deserializer}) {
|
||||
{Type? type, AngelDeserializer<Data>? deserializer}) {
|
||||
var url = baseUrl.replace(path: p.join(baseUrl.path, path));
|
||||
var s = BaseAngelService<Id, Data>(client, this, url,
|
||||
deserializer: deserializer);
|
||||
_services.add(s);
|
||||
return s;
|
||||
return s as Service<Id, Data>;
|
||||
}
|
||||
|
||||
Uri _join(url) {
|
||||
|
@ -180,65 +176,65 @@ abstract class BaseAngelClient extends Angel {
|
|||
//}
|
||||
|
||||
@override
|
||||
Future<http.Response> get(url, {Map<String, String> headers}) async {
|
||||
Future<http.Response> get(url, {Map<String, String>? headers}) async {
|
||||
return sendUnstreamed('GET', _join(url), headers);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<http.Response> head(url, {Map<String, String> headers}) async {
|
||||
Future<http.Response> head(url, {Map<String, String>? headers}) async {
|
||||
return sendUnstreamed('HEAD', _join(url), headers);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<http.Response> patch(url,
|
||||
{body, Map<String, String> headers, Encoding encoding}) async {
|
||||
{body, Map<String, String>? headers, Encoding? encoding}) async {
|
||||
return sendUnstreamed('PATCH', _join(url), headers, body, encoding);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<http.Response> post(url,
|
||||
{body, Map<String, String> headers, Encoding encoding}) async {
|
||||
{body, Map<String, String>? headers, Encoding? encoding}) async {
|
||||
return sendUnstreamed('POST', _join(url), headers, body, encoding);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<http.Response> put(url,
|
||||
{body, Map<String, String> headers, Encoding encoding}) async {
|
||||
{body, Map<String, String>? headers, Encoding? encoding}) async {
|
||||
return sendUnstreamed('PUT', _join(url), headers, body, encoding);
|
||||
}
|
||||
}
|
||||
|
||||
class BaseAngelService<Id, Data> extends Service<Id, Data> {
|
||||
class BaseAngelService<Id, Data> extends Service<Id, Data?> {
|
||||
@override
|
||||
final BaseAngelClient app;
|
||||
final Uri baseUrl;
|
||||
final http.BaseClient client;
|
||||
final AngelDeserializer<Data> deserializer;
|
||||
final http.BaseClient? client;
|
||||
final AngelDeserializer<Data>? deserializer;
|
||||
|
||||
final StreamController<List<Data>> _onIndexed = StreamController();
|
||||
final StreamController<Data> _onRead = StreamController(),
|
||||
final StreamController<List<Data?>> _onIndexed = StreamController();
|
||||
final StreamController<Data?> _onRead = StreamController(),
|
||||
_onCreated = StreamController(),
|
||||
_onModified = StreamController(),
|
||||
_onUpdated = StreamController(),
|
||||
_onRemoved = StreamController();
|
||||
|
||||
@override
|
||||
Stream<List<Data>> get onIndexed => _onIndexed.stream;
|
||||
Stream<List<Data?>> get onIndexed => _onIndexed.stream;
|
||||
|
||||
@override
|
||||
Stream<Data> get onRead => _onRead.stream;
|
||||
Stream<Data?> get onRead => _onRead.stream;
|
||||
|
||||
@override
|
||||
Stream<Data> get onCreated => _onCreated.stream;
|
||||
Stream<Data?> get onCreated => _onCreated.stream;
|
||||
|
||||
@override
|
||||
Stream<Data> get onModified => _onModified.stream;
|
||||
Stream<Data?> get onModified => _onModified.stream;
|
||||
|
||||
@override
|
||||
Stream<Data> get onUpdated => _onUpdated.stream;
|
||||
Stream<Data?> get onUpdated => _onUpdated.stream;
|
||||
|
||||
@override
|
||||
Stream<Data> get onRemoved => _onRemoved.stream;
|
||||
Stream<Data?> get onRemoved => _onRemoved.stream;
|
||||
|
||||
@override
|
||||
Future close() async {
|
||||
|
@ -251,14 +247,14 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
|
|||
}
|
||||
|
||||
BaseAngelService(this.client, this.app, baseUrl, {this.deserializer})
|
||||
: this.baseUrl = baseUrl is Uri ? baseUrl : Uri.parse(baseUrl.toString());
|
||||
: baseUrl = baseUrl is Uri ? baseUrl : Uri.parse(baseUrl.toString());
|
||||
|
||||
/// Use [baseUrl] instead.
|
||||
@deprecated
|
||||
String get basePath => baseUrl.toString();
|
||||
|
||||
Data deserialize(x) {
|
||||
return deserializer != null ? deserializer(x) : x as Data;
|
||||
Data? deserialize(x) {
|
||||
return deserializer != null ? deserializer!(x) : x as Data?;
|
||||
}
|
||||
|
||||
String makeBody(x) {
|
||||
|
@ -267,15 +263,15 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
|
|||
}
|
||||
|
||||
Future<http.StreamedResponse> send(http.BaseRequest request) {
|
||||
if (app.authToken != null && app.authToken.isNotEmpty) {
|
||||
if (app.authToken != null && app.authToken!.isNotEmpty) {
|
||||
request.headers['Authorization'] = 'Bearer ${app.authToken}';
|
||||
}
|
||||
|
||||
return client.send(request);
|
||||
return client!.send(request);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Data>> index([Map<String, dynamic> params]) async {
|
||||
Future<List<Data?>?> index([Map<String, dynamic>? params]) async {
|
||||
var url = baseUrl.replace(queryParameters: _buildQuery(params));
|
||||
var response = await app.sendUnstreamed('GET', url, _readHeaders);
|
||||
|
||||
|
@ -304,7 +300,7 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<Data> read(id, [Map<String, dynamic> params]) async {
|
||||
Future<Data?> read(id, [Map<String, dynamic>? params]) async {
|
||||
var url = baseUrl.replace(
|
||||
path: p.join(baseUrl.path, id.toString()),
|
||||
queryParameters: _buildQuery(params));
|
||||
|
@ -313,54 +309,58 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
|
|||
|
||||
try {
|
||||
if (_invalid(response)) {
|
||||
if (_onRead.hasListener)
|
||||
if (_onRead.hasListener) {
|
||||
_onRead.addError(failure(response));
|
||||
else
|
||||
} else {
|
||||
throw failure(response);
|
||||
}
|
||||
}
|
||||
|
||||
var r = deserialize(json.decode(response.body));
|
||||
_onRead.add(r);
|
||||
return r;
|
||||
} catch (e, st) {
|
||||
if (_onRead.hasListener)
|
||||
if (_onRead.hasListener) {
|
||||
_onRead.addError(e, st);
|
||||
else
|
||||
} else {
|
||||
throw failure(response, error: e, stack: st);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Data> create(data, [Map<String, dynamic> params]) async {
|
||||
Future<Data?> create(data, [Map<String, dynamic>? params]) async {
|
||||
var url = baseUrl.replace(queryParameters: _buildQuery(params));
|
||||
var response =
|
||||
await app.sendUnstreamed('POST', url, _writeHeaders, makeBody(data));
|
||||
|
||||
try {
|
||||
if (_invalid(response)) {
|
||||
if (_onCreated.hasListener)
|
||||
if (_onCreated.hasListener) {
|
||||
_onCreated.addError(failure(response));
|
||||
else
|
||||
} else {
|
||||
throw failure(response);
|
||||
}
|
||||
}
|
||||
|
||||
var r = deserialize(json.decode(response.body));
|
||||
_onCreated.add(r);
|
||||
return r;
|
||||
} catch (e, st) {
|
||||
if (_onCreated.hasListener)
|
||||
if (_onCreated.hasListener) {
|
||||
_onCreated.addError(e, st);
|
||||
else
|
||||
} else {
|
||||
throw failure(response, error: e, stack: st);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Data> modify(id, data, [Map<String, dynamic> params]) async {
|
||||
Future<Data?> modify(id, data, [Map<String, dynamic>? params]) async {
|
||||
var url = baseUrl.replace(
|
||||
path: p.join(baseUrl.path, id.toString()),
|
||||
queryParameters: _buildQuery(params));
|
||||
|
@ -370,27 +370,29 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
|
|||
|
||||
try {
|
||||
if (_invalid(response)) {
|
||||
if (_onModified.hasListener)
|
||||
if (_onModified.hasListener) {
|
||||
_onModified.addError(failure(response));
|
||||
else
|
||||
} else {
|
||||
throw failure(response);
|
||||
}
|
||||
}
|
||||
|
||||
var r = deserialize(json.decode(response.body));
|
||||
_onModified.add(r);
|
||||
return r;
|
||||
} catch (e, st) {
|
||||
if (_onModified.hasListener)
|
||||
if (_onModified.hasListener) {
|
||||
_onModified.addError(e, st);
|
||||
else
|
||||
} else {
|
||||
throw failure(response, error: e, stack: st);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Data> update(id, data, [Map<String, dynamic> params]) async {
|
||||
Future<Data?> update(id, data, [Map<String, dynamic>? params]) async {
|
||||
var url = baseUrl.replace(
|
||||
path: p.join(baseUrl.path, id.toString()),
|
||||
queryParameters: _buildQuery(params));
|
||||
|
@ -400,27 +402,29 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
|
|||
|
||||
try {
|
||||
if (_invalid(response)) {
|
||||
if (_onUpdated.hasListener)
|
||||
if (_onUpdated.hasListener) {
|
||||
_onUpdated.addError(failure(response));
|
||||
else
|
||||
} else {
|
||||
throw failure(response);
|
||||
}
|
||||
}
|
||||
|
||||
var r = deserialize(json.decode(response.body));
|
||||
_onUpdated.add(r);
|
||||
return r;
|
||||
} catch (e, st) {
|
||||
if (_onUpdated.hasListener)
|
||||
if (_onUpdated.hasListener) {
|
||||
_onUpdated.addError(e, st);
|
||||
else
|
||||
} else {
|
||||
throw failure(response, error: e, stack: st);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Data> remove(id, [Map<String, dynamic> params]) async {
|
||||
Future<Data?> remove(id, [Map<String, dynamic>? params]) async {
|
||||
var url = baseUrl.replace(
|
||||
path: p.join(baseUrl.path, id.toString()),
|
||||
queryParameters: _buildQuery(params));
|
||||
|
@ -429,20 +433,22 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
|
|||
|
||||
try {
|
||||
if (_invalid(response)) {
|
||||
if (_onRemoved.hasListener)
|
||||
if (_onRemoved.hasListener) {
|
||||
_onRemoved.addError(failure(response));
|
||||
else
|
||||
} else {
|
||||
throw failure(response);
|
||||
}
|
||||
}
|
||||
|
||||
var r = deserialize(json.decode(response.body));
|
||||
_onRemoved.add(r);
|
||||
return r;
|
||||
} catch (e, st) {
|
||||
if (_onRemoved.hasListener)
|
||||
if (_onRemoved.hasListener) {
|
||||
_onRemoved.addError(e, st);
|
||||
else
|
||||
} else {
|
||||
throw failure(response, error: e, stack: st);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
@ -6,27 +6,28 @@ import 'dart:async'
|
|||
import 'dart:html' show CustomEvent, Event, window;
|
||||
import 'dart:convert';
|
||||
import 'package:http/browser_client.dart' as http;
|
||||
import 'angel_client.dart';
|
||||
import 'angel3_client.dart';
|
||||
// import 'auth_types.dart' as auth_types;
|
||||
import 'base_angel_client.dart';
|
||||
export 'angel_client.dart';
|
||||
export 'angel3_client.dart';
|
||||
|
||||
/// Queries an Angel server via REST.
|
||||
class Rest extends BaseAngelClient {
|
||||
Rest(String basePath) : super(new http.BrowserClient(), basePath);
|
||||
Rest(String basePath) : super(http.BrowserClient(), basePath);
|
||||
|
||||
@override
|
||||
Future<AngelAuthResult> authenticate(
|
||||
{String type,
|
||||
{String? type,
|
||||
credentials,
|
||||
String authEndpoint = '/auth',
|
||||
@deprecated String reviveEndpoint = '/auth/token'}) async {
|
||||
if (type == null || type == 'token') {
|
||||
if (!window.localStorage.containsKey('token')) {
|
||||
throw new Exception(
|
||||
throw Exception(
|
||||
'Cannot revive token from localStorage - there is none.');
|
||||
}
|
||||
|
||||
var token = json.decode(window.localStorage['token']);
|
||||
var token = json.decode(window.localStorage['token']!);
|
||||
credentials ??= {'token': token};
|
||||
}
|
||||
|
||||
|
@ -39,33 +40,34 @@ class Rest extends BaseAngelClient {
|
|||
|
||||
@override
|
||||
Stream<String> authenticateViaPopup(String url,
|
||||
{String eventName = 'token', String errorMessage}) {
|
||||
var ctrl = new StreamController<String>();
|
||||
{String eventName = 'token', String? errorMessage}) {
|
||||
var ctrl = StreamController<String>();
|
||||
var wnd = window.open(url, 'angel_client_auth_popup');
|
||||
|
||||
Timer t;
|
||||
StreamSubscription sub;
|
||||
t = new Timer.periodic(new Duration(milliseconds: 500), (timer) {
|
||||
StreamSubscription? sub;
|
||||
t = Timer.periodic(Duration(milliseconds: 500), (timer) {
|
||||
if (!ctrl.isClosed) {
|
||||
if (wnd.closed) {
|
||||
ctrl.addError(new AngelHttpException.notAuthenticated(
|
||||
if (wnd.closed!) {
|
||||
ctrl.addError(AngelHttpException.notAuthenticated(
|
||||
message:
|
||||
errorMessage ?? 'Authentication via popup window failed.'));
|
||||
ctrl.close();
|
||||
timer.cancel();
|
||||
sub?.cancel();
|
||||
}
|
||||
} else
|
||||
} else {
|
||||
timer.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
sub = window.on[eventName ?? 'token'].listen((Event ev) {
|
||||
sub = window.on[eventName].listen((Event ev) {
|
||||
var e = ev as CustomEvent;
|
||||
if (!ctrl.isClosed) {
|
||||
ctrl.add(e.detail.toString());
|
||||
t.cancel();
|
||||
ctrl.close();
|
||||
sub.cancel();
|
||||
sub!.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -4,16 +4,16 @@ library angel_client.flutter;
|
|||
import 'dart:async';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'base_angel_client.dart';
|
||||
export 'angel_client.dart';
|
||||
export 'angel3_client.dart';
|
||||
|
||||
/// Queries an Angel server via REST.
|
||||
class Rest extends BaseAngelClient {
|
||||
Rest(String basePath) : super(new http.Client() as http.BaseClient, basePath);
|
||||
Rest(String basePath) : super(http.Client() as http.BaseClient, basePath);
|
||||
|
||||
@override
|
||||
Stream<String> authenticateViaPopup(String url,
|
||||
{String eventName = 'token'}) {
|
||||
throw new UnimplementedError(
|
||||
throw UnimplementedError(
|
||||
'Opening popup windows is not supported in the `flutter` client.');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,11 +3,11 @@ library angel_client.cli;
|
|||
|
||||
import 'dart:async';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:json_god/json_god.dart' as god;
|
||||
import 'package:angel3_json_god/angel3_json_god.dart' as god;
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'angel_client.dart';
|
||||
import 'angel3_client.dart';
|
||||
import 'base_angel_client.dart';
|
||||
export 'angel_client.dart';
|
||||
export 'angel3_client.dart';
|
||||
|
||||
/// Queries an Angel server via REST.
|
||||
class Rest extends BaseAngelClient {
|
||||
|
@ -17,11 +17,11 @@ class Rest extends BaseAngelClient {
|
|||
|
||||
@override
|
||||
Service<Id, Data> service<Id, Data>(String path,
|
||||
{Type type, AngelDeserializer deserializer}) {
|
||||
{Type? type, AngelDeserializer? deserializer}) {
|
||||
var url = baseUrl.replace(path: p.join(baseUrl.path, path));
|
||||
var s = RestService<Id, Data>(client, this, url, type);
|
||||
_services.add(s);
|
||||
return s;
|
||||
return s as Service<Id, Data>;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -42,21 +42,21 @@ class Rest extends BaseAngelClient {
|
|||
|
||||
/// Queries an Angel service via REST.
|
||||
class RestService<Id, Data> extends BaseAngelService<Id, Data> {
|
||||
final Type type;
|
||||
final Type? type;
|
||||
|
||||
RestService(http.BaseClient client, BaseAngelClient app, url, this.type)
|
||||
RestService(http.BaseClient? client, BaseAngelClient app, url, this.type)
|
||||
: super(client, app, url);
|
||||
|
||||
@override
|
||||
Data deserialize(x) {
|
||||
Data? deserialize(x) {
|
||||
print(x);
|
||||
if (type != null) {
|
||||
return x.runtimeType == type
|
||||
? x as Data
|
||||
: god.deserializeDatum(x, outputType: type) as Data;
|
||||
? x as Data?
|
||||
: god.deserializeDatum(x, outputType: type) as Data?;
|
||||
}
|
||||
|
||||
return x as Data;
|
||||
return x as Data?;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -1,41 +1,23 @@
|
|||
name: angel_client
|
||||
version: 3.0.0
|
||||
name: angel3_client
|
||||
version: 4.0.0
|
||||
description: Support for querying Angel servers in the browser, Flutter, and command-line.
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
homepage: https://github.com/angel-dart/angel_client
|
||||
publish_to: none
|
||||
homepage: https://github.com/dukefirehawk/angel/tree/angel3/packages/client
|
||||
environment:
|
||||
sdk: ">=2.10.0 <3.0.0"
|
||||
sdk: '>=2.12.0 <3.0.0'
|
||||
dependencies:
|
||||
angel_http_exception:
|
||||
git:
|
||||
url: https://github.com/dukefirehawk/angel.git
|
||||
ref: sdk-2.12.x
|
||||
path: packages/http_exception
|
||||
collection: ^1.0.0
|
||||
http: ^0.13.0
|
||||
json_god:
|
||||
git:
|
||||
url: https://github.com/dukefirehawk/angel.git
|
||||
ref: sdk-2.12.x
|
||||
path: packages/json_god
|
||||
angel3_http_exception: ^3.0.0
|
||||
angel3_json_god: ^4.0.0
|
||||
collection: ^1.15.0
|
||||
http: ^0.13.1
|
||||
#dart_json_mapper: ^1.7.0
|
||||
meta: ^1.0.0
|
||||
path: ^1.0.0
|
||||
meta: ^1.3.0
|
||||
path: ^1.8.0
|
||||
dev_dependencies:
|
||||
angel_framework:
|
||||
git:
|
||||
url: https://github.com/dukefirehawk/angel.git
|
||||
ref: sdk-2.12.x
|
||||
path: packages/framework
|
||||
angel_model:
|
||||
git:
|
||||
url: https://github.com/dukefirehawk/angel.git
|
||||
ref: sdk-2.12.x
|
||||
path: packages/model
|
||||
async: ^2.0.0
|
||||
build_runner: ^1.0.0
|
||||
build_web_compilers: ^2.12.2
|
||||
mock_request: ^1.0.0
|
||||
angel3_framework: ^4.0.0
|
||||
angel3_model: ^3.0.0
|
||||
angel3_mock_request: ^2.0.0
|
||||
async: ^2.6.1
|
||||
build_runner: ^1.12.2
|
||||
build_web_compilers: ^2.16.5
|
||||
pedantic: ^1.11.0
|
||||
test: ^1.16.5
|
||||
test: ^1.17.4
|
||||
|
|
|
@ -9,75 +9,75 @@ void main() {
|
|||
test('sets method,body,headers,path', () async {
|
||||
await app.post(Uri.parse('/post'),
|
||||
headers: {'method': 'post'}, body: 'post');
|
||||
expect(app.client.spec.method, 'POST');
|
||||
expect(app.client.spec.path, '/post');
|
||||
expect(app.client.spec.headers['method'], 'post');
|
||||
expect(await read(app.client.spec.request.finalize()), 'post');
|
||||
expect(app.client.spec!.method, 'POST');
|
||||
expect(app.client.spec!.path, '/post');
|
||||
expect(app.client.spec!.headers['method'], 'post');
|
||||
expect(await read(app.client.spec!.request.finalize()), 'post');
|
||||
});
|
||||
|
||||
group('service methods', () {
|
||||
test('index', () async {
|
||||
await todoService.index();
|
||||
expect(app.client.spec.method, 'GET');
|
||||
expect(app.client.spec.path, '/api/todos');
|
||||
expect(app.client.spec!.method, 'GET');
|
||||
expect(app.client.spec!.path, '/api/todos');
|
||||
});
|
||||
|
||||
test('read', () async {
|
||||
await todoService.read('sleep');
|
||||
expect(app.client.spec.method, 'GET');
|
||||
expect(app.client.spec.path, '/api/todos/sleep');
|
||||
expect(app.client.spec!.method, 'GET');
|
||||
expect(app.client.spec!.path, '/api/todos/sleep');
|
||||
});
|
||||
|
||||
test('create', () async {
|
||||
await todoService.create({});
|
||||
expect(app.client.spec.method, 'POST');
|
||||
expect(app.client.spec.headers['content-type'],
|
||||
expect(app.client.spec!.method, 'POST');
|
||||
expect(app.client.spec!.headers['content-type'],
|
||||
startsWith('application/json'));
|
||||
expect(app.client.spec.path, '/api/todos');
|
||||
expect(await read(app.client.spec.request.finalize()), '{}');
|
||||
expect(app.client.spec!.path, '/api/todos');
|
||||
expect(await read(app.client.spec!.request.finalize()), '{}');
|
||||
});
|
||||
|
||||
test('modify', () async {
|
||||
await todoService.modify('sleep', {});
|
||||
expect(app.client.spec.method, 'PATCH');
|
||||
expect(app.client.spec.headers['content-type'],
|
||||
expect(app.client.spec!.method, 'PATCH');
|
||||
expect(app.client.spec!.headers['content-type'],
|
||||
startsWith('application/json'));
|
||||
expect(app.client.spec.path, '/api/todos/sleep');
|
||||
expect(await read(app.client.spec.request.finalize()), '{}');
|
||||
expect(app.client.spec!.path, '/api/todos/sleep');
|
||||
expect(await read(app.client.spec!.request.finalize()), '{}');
|
||||
});
|
||||
|
||||
test('update', () async {
|
||||
await todoService.update('sleep', {});
|
||||
expect(app.client.spec.method, 'POST');
|
||||
expect(app.client.spec.headers['content-type'],
|
||||
expect(app.client.spec!.method, 'POST');
|
||||
expect(app.client.spec!.headers['content-type'],
|
||||
startsWith('application/json'));
|
||||
expect(app.client.spec.path, '/api/todos/sleep');
|
||||
expect(await read(app.client.spec.request.finalize()), '{}');
|
||||
expect(app.client.spec!.path, '/api/todos/sleep');
|
||||
expect(await read(app.client.spec!.request.finalize()), '{}');
|
||||
});
|
||||
|
||||
test('remove', () async {
|
||||
await todoService.remove('sleep');
|
||||
expect(app.client.spec.method, 'DELETE');
|
||||
expect(app.client.spec.path, '/api/todos/sleep');
|
||||
expect(app.client.spec!.method, 'DELETE');
|
||||
expect(app.client.spec!.path, '/api/todos/sleep');
|
||||
});
|
||||
});
|
||||
|
||||
group('authentication', () {
|
||||
test('no type defaults to token', () async {
|
||||
await app.authenticate(credentials: '<jwt>');
|
||||
expect(app.client.spec.path, '/auth/token');
|
||||
expect(app.client.spec!.path, '/auth/token');
|
||||
});
|
||||
|
||||
test('sets type', () async {
|
||||
await app.authenticate(type: 'local');
|
||||
expect(app.client.spec.path, '/auth/local');
|
||||
expect(app.client.spec!.path, '/auth/local');
|
||||
});
|
||||
|
||||
test('credentials send right body', () async {
|
||||
await app
|
||||
.authenticate(type: 'local', credentials: {'username': 'password'});
|
||||
expect(
|
||||
await read(app.client.spec.request.finalize()),
|
||||
await read(app.client.spec!.request.finalize()),
|
||||
json.encode({'username': 'password'}),
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'package:angel_client/base_angel_client.dart';
|
||||
import 'package:angel3_client/base_angel_client.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:http/src/base_client.dart' as http;
|
||||
import 'package:http/src/base_request.dart' as http;
|
||||
|
@ -10,24 +10,25 @@ Future<String> read(Stream<List<int>> stream) =>
|
|||
|
||||
class MockAngel extends BaseAngelClient {
|
||||
@override
|
||||
final SpecClient client = new SpecClient();
|
||||
final SpecClient client = SpecClient();
|
||||
|
||||
MockAngel() : super(null, 'http://localhost:3000');
|
||||
|
||||
@override
|
||||
authenticateViaPopup(String url, {String eventName = 'token'}) {
|
||||
throw new UnsupportedError('Nope');
|
||||
Stream<String> authenticateViaPopup(String url,
|
||||
{String eventName = 'token'}) {
|
||||
throw UnsupportedError('Nope');
|
||||
}
|
||||
}
|
||||
|
||||
class SpecClient extends http.BaseClient {
|
||||
Spec _spec;
|
||||
Spec? _spec;
|
||||
|
||||
Spec get spec => _spec;
|
||||
Spec? get spec => _spec;
|
||||
|
||||
@override
|
||||
send(http.BaseRequest request) {
|
||||
_spec = new Spec(request, request.method, request.url.path, request.headers,
|
||||
Future<http.StreamedResponse> send(http.BaseRequest request) {
|
||||
_spec = Spec(request, request.method, request.url.path, request.headers,
|
||||
request.contentLength);
|
||||
dynamic data = {'text': 'Clean your room!', 'completed': true};
|
||||
|
||||
|
@ -40,8 +41,8 @@ class SpecClient extends http.BaseClient {
|
|||
data = [data];
|
||||
}
|
||||
|
||||
return new Future<http.StreamedResponse>.value(new http.StreamedResponse(
|
||||
new Stream<List<int>>.fromIterable([utf8.encode(json.encode(data))]),
|
||||
return Future<http.StreamedResponse>.value(http.StreamedResponse(
|
||||
Stream<List<int>>.fromIterable([utf8.encode(json.encode(data))]),
|
||||
200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
|
@ -54,7 +55,7 @@ class Spec {
|
|||
final http.BaseRequest request;
|
||||
final String method, path;
|
||||
final Map<String, String> headers;
|
||||
final int contentLength;
|
||||
final int? contentLength;
|
||||
|
||||
Spec(this.request, this.method, this.path, this.headers, this.contentLength);
|
||||
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
import 'package:async/async.dart';
|
||||
import 'dart:io';
|
||||
import 'package:angel_client/io.dart' as c;
|
||||
import 'package:angel_framework/angel_framework.dart' as s;
|
||||
import 'package:angel_framework/http.dart' as s;
|
||||
import 'package:angel3_client/io.dart' as c;
|
||||
import 'package:angel3_framework/angel3_framework.dart' as s;
|
||||
import 'package:angel3_framework/http.dart' as s;
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
main() {
|
||||
HttpServer server;
|
||||
c.Angel app;
|
||||
c.ServiceList list;
|
||||
StreamQueue queue;
|
||||
void main() {
|
||||
late HttpServer server;
|
||||
late c.Angel app;
|
||||
late c.ServiceList list;
|
||||
late StreamQueue queue;
|
||||
|
||||
setUp(() async {
|
||||
var serverApp = new s.Angel();
|
||||
var http = new s.AngelHttp(serverApp);
|
||||
serverApp.use('/api/todos', new s.MapService(autoIdAndDateFields: false));
|
||||
var serverApp = s.Angel();
|
||||
var http = s.AngelHttp(serverApp);
|
||||
serverApp.use('/api/todos', s.MapService(autoIdAndDateFields: false));
|
||||
|
||||
server = await http.startServer();
|
||||
var uri = 'http://${server.address.address}:${server.port}';
|
||||
app = new c.Rest(uri);
|
||||
list = new c.ServiceList(app.service('api/todos'));
|
||||
queue = new StreamQueue(list.onChange);
|
||||
app = c.Rest(uri);
|
||||
list = c.ServiceList(app.service('api/todos'));
|
||||
queue = StreamQueue(list.onChange);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import 'package:angel_model/angel_model.dart';
|
||||
import 'package:angel3_model/angel3_model.dart';
|
||||
|
||||
class Postcard extends Model {
|
||||
String location;
|
||||
String message;
|
||||
String? location;
|
||||
String? message;
|
||||
|
||||
Postcard({String id, this.location, this.message}) {
|
||||
Postcard({String? id, this.location, this.message}) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
factory Postcard.fromJson(Map data) => new Postcard(
|
||||
factory Postcard.fromJson(Map data) => Postcard(
|
||||
id: data['id'].toString(),
|
||||
location: data['location'].toString(),
|
||||
message: data['message'].toString());
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import 'dart:html';
|
||||
import 'package:angel_client/browser.dart';
|
||||
import 'package:angel3_client/browser.dart';
|
||||
|
||||
/// Dummy app to ensure client works with DDC.
|
||||
main() {
|
||||
var app = new Rest(window.location.origin);
|
||||
void main() {
|
||||
var app = Rest(window.location.origin);
|
||||
window.alert(app.baseUrl.toString());
|
||||
}
|
||||
|
|
71
packages/code_buffer/.gitignore
vendored
Normal file
71
packages/code_buffer/.gitignore
vendored
Normal file
|
@ -0,0 +1,71 @@
|
|||
# See https://www.dartlang.org/tools/private-files.html
|
||||
|
||||
# Files and directories created by pub
|
||||
.dart_tool
|
||||
.packages
|
||||
.pub/
|
||||
build/
|
||||
|
||||
# If you're building an application, you may want to check-in your pubspec.lock
|
||||
pubspec.lock
|
||||
|
||||
# Directory created by dartdoc
|
||||
# If you don't generate documentation locally you can remove this line.
|
||||
doc/api/
|
||||
|
||||
### Dart template
|
||||
# See https://www.dartlang.org/tools/private-files.html
|
||||
|
||||
# Files and directories created by pub
|
||||
|
||||
# SDK 1.20 and later (no longer creates packages directories)
|
||||
|
||||
# Older SDK versions
|
||||
# (Include if the minimum SDK version specified in pubsepc.yaml is earlier than 1.20)
|
||||
.project
|
||||
.buildlog
|
||||
**/packages/
|
||||
|
||||
|
||||
# Files created by dart2js
|
||||
# (Most Dart developers will use pub build to compile Dart, use/modify these
|
||||
# rules if you intend to use dart2js directly
|
||||
# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
|
||||
# differentiate from explicit Javascript files)
|
||||
*.dart.js
|
||||
*.part.js
|
||||
*.js.deps
|
||||
*.js.map
|
||||
*.info.json
|
||||
|
||||
# Directory created by dartdoc
|
||||
|
||||
# Don't commit pubspec lock file
|
||||
# (Library packages only! Remove pattern if developing an application package)
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff:
|
||||
|
||||
## VsCode
|
||||
.vscode/
|
||||
|
||||
## File-based project format:
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
.idea/
|
||||
/out/
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
1
packages/code_buffer/.travis.yml
Normal file
1
packages/code_buffer/.travis.yml
Normal file
|
@ -0,0 +1 @@
|
|||
language: dart
|
12
packages/code_buffer/AUTHORS.md
Normal file
12
packages/code_buffer/AUTHORS.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
Primary Authors
|
||||
===============
|
||||
|
||||
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
|
||||
|
||||
Thomas is the current maintainer of the code base. He has refactored and migrated the
|
||||
code base to support NNBD.
|
||||
|
||||
* __[Tobe O](thosakwe@gmail.com)__
|
||||
|
||||
Tobe has written much of the original code prior to NNBD migration. He has moved on and
|
||||
is no longer involved with the project.
|
8
packages/code_buffer/CHANGELOG.md
Normal file
8
packages/code_buffer/CHANGELOG.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# 2.0.2
|
||||
* Updated README
|
||||
# 2.0.1
|
||||
* Fixed invalid homepage url in pubspec.yaml
|
||||
# 2.0.0
|
||||
* Migrated to support Dart SDK 2.12.x NNBD
|
||||
# 1.0.1
|
||||
* Added `CodeBuffer.noWhitespace()`.
|
21
packages/code_buffer/LICENSE
Normal file
21
packages/code_buffer/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 dukefirehawk.com
|
||||
|
||||
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.
|
66
packages/code_buffer/README.md
Normal file
66
packages/code_buffer/README.md
Normal file
|
@ -0,0 +1,66 @@
|
|||
# angel3_code_buffer
|
||||
[![version](https://img.shields.io/badge/pub-v2.0.2-brightgreen)](https://pub.dartlang.org/packages/angel3_code_buffer)
|
||||
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
|
||||
[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion)
|
||||
|
||||
[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/angel3/packages/code_buffer/LICENSE)
|
||||
|
||||
An advanced StringBuffer geared toward generating code, and source maps.
|
||||
|
||||
# Installation
|
||||
In your `pubspec.yaml`:
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
angel3_code_buffer: ^2.0.0
|
||||
```
|
||||
|
||||
# Usage
|
||||
Use a `CodeBuffer` just like any regular `StringBuffer`:
|
||||
|
||||
```dart
|
||||
String someFunc() {
|
||||
var buf = CodeBuffer();
|
||||
buf
|
||||
..write('hello ')
|
||||
..writeln('world!');
|
||||
return buf.toString();
|
||||
}
|
||||
```
|
||||
|
||||
However, a `CodeBuffer` supports indentation.
|
||||
|
||||
```dart
|
||||
void someOtherFunc() {
|
||||
var buf = CodeBuffer();
|
||||
// Custom options...
|
||||
var buf = CodeBuffer(newline: '\r\n', space: '\t', trailingNewline: true);
|
||||
|
||||
// Any following lines will have an incremented indentation level...
|
||||
buf.indent();
|
||||
|
||||
// And vice-versa:
|
||||
buf.outdent();
|
||||
}
|
||||
```
|
||||
|
||||
`CodeBuffer` instances keep track of every `SourceSpan` they create.
|
||||
This makes them useful for codegen tools, or to-JS compilers.
|
||||
|
||||
```dart
|
||||
void someFunc(CodeBuffer buf) {
|
||||
buf.write('hello');
|
||||
expect(buf.lastLine.text, 'hello');
|
||||
|
||||
buf.writeln('world');
|
||||
expect(buf.lastLine.lastSpan.start.column, 5);
|
||||
}
|
||||
```
|
||||
|
||||
You can copy a `CodeBuffer` into another, heeding indentation rules:
|
||||
|
||||
```dart
|
||||
void yetAnotherFunc(CodeBuffer a, CodeBuffer b) {
|
||||
b.copyInto(a);
|
||||
}
|
||||
```
|
3
packages/code_buffer/analysis_options.yaml
Normal file
3
packages/code_buffer/analysis_options.yaml
Normal file
|
@ -0,0 +1,3 @@
|
|||
analyzer:
|
||||
strong-mode:
|
||||
implicit-casts: false
|
46
packages/code_buffer/example/main.dart
Normal file
46
packages/code_buffer/example/main.dart
Normal file
|
@ -0,0 +1,46 @@
|
|||
import 'package:angel3_code_buffer/angel3_code_buffer.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
/// Use a `CodeBuffer` just like any regular `StringBuffer`:
|
||||
String someFunc() {
|
||||
var buf = new CodeBuffer();
|
||||
buf
|
||||
..write('hello ')
|
||||
..writeln('world!');
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
/// However, a `CodeBuffer` supports indentation.
|
||||
void someOtherFunc() {
|
||||
var buf = new CodeBuffer();
|
||||
|
||||
// Custom options...
|
||||
// ignore: unused_local_variable
|
||||
var customBuf =
|
||||
new CodeBuffer(newline: '\r\n', space: '\t', trailingNewline: true);
|
||||
|
||||
// Without whitespace..
|
||||
// ignore: unused_local_variable
|
||||
var minifyingBuf = new CodeBuffer.noWhitespace();
|
||||
|
||||
// Any following lines will have an incremented indentation level...
|
||||
buf.indent();
|
||||
|
||||
// And vice-versa:
|
||||
buf.outdent();
|
||||
}
|
||||
|
||||
/// `CodeBuffer` instances keep track of every `SourceSpan` they create.
|
||||
//This makes them useful for codegen tools, or to-JS compilers.
|
||||
void yetAnotherOtherFunc(CodeBuffer buf) {
|
||||
buf.write('hello');
|
||||
expect(buf.lastLine!.text, 'hello');
|
||||
|
||||
buf.writeln('world');
|
||||
expect(buf.lastLine!.lastSpan!.start.column, 5);
|
||||
}
|
||||
|
||||
/// You can copy a `CodeBuffer` into another, heeding indentation rules:
|
||||
void yetEvenAnotherFunc(CodeBuffer a, CodeBuffer b) {
|
||||
b.copyInto(a);
|
||||
}
|
229
packages/code_buffer/lib/angel3_code_buffer.dart
Normal file
229
packages/code_buffer/lib/angel3_code_buffer.dart
Normal file
|
@ -0,0 +1,229 @@
|
|||
import 'package:source_span/source_span.dart';
|
||||
|
||||
/// An advanced StringBuffer geared toward generating code, and source maps.
|
||||
class CodeBuffer implements StringBuffer {
|
||||
/// The character sequence used to represent a line break.
|
||||
final String newline;
|
||||
|
||||
/// The character sequence used to represent a space/tab.
|
||||
final String space;
|
||||
|
||||
/// The source URL to be applied to all generated [SourceSpan] instances.
|
||||
final sourceUrl;
|
||||
|
||||
/// If `true` (default: `false`), then an additional [newline] will be inserted at the end of the generated string.
|
||||
final bool trailingNewline;
|
||||
|
||||
final List<CodeBufferLine> _lines = [];
|
||||
CodeBufferLine? _currentLine, _lastLine;
|
||||
int _indentationLevel = 0;
|
||||
int _length = 0;
|
||||
|
||||
CodeBuffer(
|
||||
{this.space = ' ',
|
||||
this.newline = '\n',
|
||||
this.trailingNewline = false,
|
||||
this.sourceUrl});
|
||||
|
||||
/// Creates a [CodeBuffer] that does not emit additional whitespace.
|
||||
factory CodeBuffer.noWhitespace({sourceUrl}) => CodeBuffer(
|
||||
space: '', newline: '', trailingNewline: false, sourceUrl: sourceUrl);
|
||||
|
||||
/// The last line created within this buffer.
|
||||
CodeBufferLine? get lastLine => _lastLine;
|
||||
|
||||
/// Returns an immutable collection of the [CodeBufferLine]s within this instance.
|
||||
List<CodeBufferLine> get lines => List<CodeBufferLine>.unmodifiable(_lines);
|
||||
|
||||
@override
|
||||
bool get isEmpty => _lines.isEmpty;
|
||||
|
||||
@override
|
||||
bool get isNotEmpty => _lines.isNotEmpty;
|
||||
|
||||
@override
|
||||
int get length => _length;
|
||||
|
||||
CodeBufferLine _createLine() {
|
||||
var start = SourceLocation(
|
||||
_length,
|
||||
sourceUrl: sourceUrl,
|
||||
line: _lines.length,
|
||||
column: _indentationLevel * space.length,
|
||||
);
|
||||
var line = CodeBufferLine._(_indentationLevel, start).._end = start;
|
||||
_lines.add(_lastLine = line);
|
||||
return line;
|
||||
}
|
||||
|
||||
/// Increments the indentation level.
|
||||
void indent() {
|
||||
_indentationLevel++;
|
||||
}
|
||||
|
||||
/// Decrements the indentation level, if it is greater than `0`.
|
||||
void outdent() {
|
||||
if (_indentationLevel > 0) _indentationLevel--;
|
||||
}
|
||||
|
||||
/// Copies the contents of this [CodeBuffer] into another, preserving indentation and source mapping information.
|
||||
void copyInto(CodeBuffer other) {
|
||||
if (_lines.isEmpty) return;
|
||||
int i = 0;
|
||||
|
||||
for (var line in _lines) {
|
||||
// To compute offset:
|
||||
// 1. Find current length of other
|
||||
// 2. Add length of its newline
|
||||
// 3. Add indentation
|
||||
var column = (other._indentationLevel + line.indentationLevel) *
|
||||
other.space.length;
|
||||
var offset = other._length + other.newline.length + column;
|
||||
|
||||
// Re-compute start + end
|
||||
var start = SourceLocation(
|
||||
offset,
|
||||
sourceUrl: other.sourceUrl,
|
||||
line: other._lines.length + i,
|
||||
column: column,
|
||||
);
|
||||
|
||||
var end = SourceLocation(
|
||||
offset + line.span.length,
|
||||
sourceUrl: other.sourceUrl,
|
||||
line: start.line,
|
||||
column: column + line._buf.length,
|
||||
);
|
||||
|
||||
var clone = CodeBufferLine._(
|
||||
line.indentationLevel + other._indentationLevel, start)
|
||||
.._end = end
|
||||
.._buf.write(line._buf.toString());
|
||||
|
||||
// Adjust lastSpan
|
||||
if (line._lastSpan != null) {
|
||||
var s = line._lastSpan!.start;
|
||||
var lastSpanColumn =
|
||||
((line.indentationLevel + other._indentationLevel) *
|
||||
other.space.length) +
|
||||
line.text.indexOf(line._lastSpan!.text);
|
||||
clone._lastSpan = SourceSpan(
|
||||
SourceLocation(
|
||||
offset + s.offset,
|
||||
sourceUrl: other.sourceUrl,
|
||||
line: clone.span.start.line,
|
||||
column: lastSpanColumn,
|
||||
),
|
||||
SourceLocation(
|
||||
offset + s.offset + line._lastSpan!.length,
|
||||
sourceUrl: other.sourceUrl,
|
||||
line: clone.span.end.line,
|
||||
column: lastSpanColumn + line._lastSpan!.length,
|
||||
),
|
||||
line._lastSpan!.text,
|
||||
);
|
||||
}
|
||||
|
||||
other._lines.add(other._currentLine = other._lastLine = clone);
|
||||
|
||||
// Adjust length accordingly...
|
||||
other._length = offset + clone.span.length;
|
||||
i++;
|
||||
}
|
||||
|
||||
other.writeln();
|
||||
}
|
||||
|
||||
@override
|
||||
void clear() {
|
||||
_lines.clear();
|
||||
_length = _indentationLevel = 0;
|
||||
_currentLine = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void writeCharCode(int charCode) {
|
||||
_currentLine ??= _createLine();
|
||||
|
||||
_currentLine!._buf.writeCharCode(charCode);
|
||||
var end = _currentLine!._end;
|
||||
_currentLine!._end = SourceLocation(
|
||||
end.offset + 1,
|
||||
sourceUrl: end.sourceUrl,
|
||||
line: end.line,
|
||||
column: end.column + 1,
|
||||
);
|
||||
_length++;
|
||||
_currentLine!._lastSpan =
|
||||
SourceSpan(end, _currentLine!._end, String.fromCharCode(charCode));
|
||||
}
|
||||
|
||||
@override
|
||||
void write(Object? obj) {
|
||||
var msg = obj.toString();
|
||||
_currentLine ??= _createLine();
|
||||
_currentLine!._buf.write(msg);
|
||||
var end = _currentLine!._end;
|
||||
_currentLine!._end = SourceLocation(
|
||||
end.offset + msg.length,
|
||||
sourceUrl: end.sourceUrl,
|
||||
line: end.line,
|
||||
column: end.column + msg.length,
|
||||
);
|
||||
_length += msg.length;
|
||||
_currentLine!._lastSpan = SourceSpan(end, _currentLine!._end, msg);
|
||||
}
|
||||
|
||||
@override
|
||||
void writeln([Object? obj = ""]) {
|
||||
if (obj != null && obj != '') write(obj);
|
||||
_currentLine = null;
|
||||
_length++;
|
||||
}
|
||||
|
||||
@override
|
||||
void writeAll(Iterable objects, [String separator = ""]) {
|
||||
write(objects.join(separator));
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
var buf = StringBuffer();
|
||||
int i = 0;
|
||||
|
||||
for (var line in lines) {
|
||||
if (i++ > 0) buf.write(newline);
|
||||
for (int j = 0; j < line.indentationLevel; j++) buf.write(space);
|
||||
buf.write(line._buf.toString());
|
||||
}
|
||||
|
||||
if (trailingNewline == true) buf.write(newline);
|
||||
|
||||
return buf.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a line of text within a [CodeBuffer].
|
||||
class CodeBufferLine {
|
||||
/// Mappings from one [SourceSpan] to another, to aid with generating dynamic source maps.
|
||||
final Map<SourceSpan, SourceSpan> sourceMappings = {};
|
||||
|
||||
/// The level of indentation preceding this line.
|
||||
final int indentationLevel;
|
||||
|
||||
final SourceLocation _start;
|
||||
final StringBuffer _buf = StringBuffer();
|
||||
late SourceLocation _end;
|
||||
SourceSpan? _lastSpan;
|
||||
|
||||
CodeBufferLine._(this.indentationLevel, this._start);
|
||||
|
||||
/// The [SourceSpan] corresponding to the last text written to this line.
|
||||
SourceSpan? get lastSpan => _lastSpan;
|
||||
|
||||
/// The [SourceSpan] corresponding to this entire line.
|
||||
SourceSpan get span => SourceSpan(_start, _end, _buf.toString());
|
||||
|
||||
/// The text within this line.
|
||||
String get text => _buf.toString();
|
||||
}
|
11
packages/code_buffer/pubspec.yaml
Normal file
11
packages/code_buffer/pubspec.yaml
Normal file
|
@ -0,0 +1,11 @@
|
|||
name: angel3_code_buffer
|
||||
version: 2.0.2
|
||||
description: An advanced StringBuffer geared toward generating code, and source maps.
|
||||
homepage: https://github.com/dukefirehawk/angel/tree/angel3/packages/code_buffer
|
||||
environment:
|
||||
sdk: '>=2.12.0 <3.0.0'
|
||||
dependencies:
|
||||
charcode: ^1.2.0
|
||||
source_span: ^1.8.1
|
||||
dev_dependencies:
|
||||
test: ^1.17.3
|
45
packages/code_buffer/test/copy_test.dart
Normal file
45
packages/code_buffer/test/copy_test.dart
Normal file
|
@ -0,0 +1,45 @@
|
|||
import 'package:angel3_code_buffer/angel3_code_buffer.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
var a = CodeBuffer(), b = CodeBuffer();
|
||||
|
||||
setUp(() {
|
||||
a.writeln('outer block 1');
|
||||
b..writeln('inner block 1')..writeln('inner block 2');
|
||||
b.copyInto(a..indent());
|
||||
a
|
||||
..outdent()
|
||||
..writeln('outer block 2');
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
a.clear();
|
||||
b.clear();
|
||||
});
|
||||
|
||||
test('sets correct text', () {
|
||||
expect(
|
||||
a.toString(),
|
||||
[
|
||||
'outer block 1',
|
||||
' inner block 1',
|
||||
' inner block 2',
|
||||
'outer block 2',
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
test('sets lastLine+lastSpan', () {
|
||||
var c = CodeBuffer()
|
||||
..indent()
|
||||
..write('>')
|
||||
..writeln('innermost');
|
||||
c.copyInto(a);
|
||||
expect(a.lastLine!.text, '>innermost');
|
||||
expect(a.lastLine!.span.start.column, 2);
|
||||
expect(a.lastLine!.lastSpan!.start.line, 4);
|
||||
expect(a.lastLine!.lastSpan!.start.column, 3);
|
||||
expect(a.lastLine!.lastSpan!.end.line, 4);
|
||||
expect(a.lastLine!.lastSpan!.end.column, 12);
|
||||
});
|
||||
}
|
44
packages/code_buffer/test/span_test.dart
Normal file
44
packages/code_buffer/test/span_test.dart
Normal file
|
@ -0,0 +1,44 @@
|
|||
import 'package:charcode/charcode.dart';
|
||||
import 'package:angel3_code_buffer/angel3_code_buffer.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
var buf = CodeBuffer();
|
||||
tearDown(buf.clear);
|
||||
|
||||
test('writeCharCode', () {
|
||||
buf.writeCharCode($x);
|
||||
expect(buf.lastLine!.lastSpan!.start.column, 0);
|
||||
expect(buf.lastLine!.lastSpan!.start.line, 0);
|
||||
expect(buf.lastLine!.lastSpan!.end.column, 1);
|
||||
expect(buf.lastLine!.lastSpan!.end.line, 0);
|
||||
});
|
||||
|
||||
test('write', () {
|
||||
buf.write('foo');
|
||||
expect(buf.lastLine!.lastSpan!.start.column, 0);
|
||||
expect(buf.lastLine!.lastSpan!.start.line, 0);
|
||||
expect(buf.lastLine!.lastSpan!.end.column, 3);
|
||||
expect(buf.lastLine!.lastSpan!.end.line, 0);
|
||||
});
|
||||
|
||||
test('multiple writes in one line', () {
|
||||
buf..write('foo')..write('baz');
|
||||
expect(buf.lastLine!.lastSpan!.start.column, 3);
|
||||
expect(buf.lastLine!.lastSpan!.start.line, 0);
|
||||
expect(buf.lastLine!.lastSpan!.end.column, 6);
|
||||
expect(buf.lastLine!.lastSpan!.end.line, 0);
|
||||
});
|
||||
|
||||
test('multiple lines', () {
|
||||
buf
|
||||
..writeln('foo')
|
||||
..write('bar')
|
||||
..write('+')
|
||||
..writeln('baz');
|
||||
expect(buf.lastLine!.lastSpan!.start.column, 4);
|
||||
expect(buf.lastLine!.lastSpan!.start.line, 1);
|
||||
expect(buf.lastLine!.lastSpan!.end.column, 7);
|
||||
expect(buf.lastLine!.lastSpan!.end.line, 1);
|
||||
});
|
||||
}
|
87
packages/code_buffer/test/write_test.dart
Normal file
87
packages/code_buffer/test/write_test.dart
Normal file
|
@ -0,0 +1,87 @@
|
|||
import 'package:charcode/charcode.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:angel3_code_buffer/angel3_code_buffer.dart';
|
||||
|
||||
main() {
|
||||
var buf = CodeBuffer();
|
||||
tearDown(buf.clear);
|
||||
|
||||
test('writeCharCode', () {
|
||||
buf.writeCharCode($x);
|
||||
expect(buf.toString(), 'x');
|
||||
});
|
||||
|
||||
test('write', () {
|
||||
buf.write('hello world');
|
||||
expect(buf.toString(), 'hello world');
|
||||
});
|
||||
|
||||
test('custom space', () {
|
||||
var b = CodeBuffer(space: '+')
|
||||
..writeln('foo')
|
||||
..indent()
|
||||
..writeln('baz');
|
||||
expect(b.toString(), 'foo\n+baz');
|
||||
});
|
||||
|
||||
test('custom newline', () {
|
||||
var b = CodeBuffer(newline: 'N')
|
||||
..writeln('foo')
|
||||
..indent()
|
||||
..writeln('baz');
|
||||
expect(b.toString(), 'fooN baz');
|
||||
});
|
||||
|
||||
test('trailing newline', () {
|
||||
var b = CodeBuffer(trailingNewline: true)..writeln('foo');
|
||||
expect(b.toString(), 'foo\n');
|
||||
});
|
||||
|
||||
group('multiple lines', () {
|
||||
setUp(() {
|
||||
buf..writeln('foo')..writeln('bar')..writeln('baz');
|
||||
expect(buf.lines, hasLength(3));
|
||||
expect(buf.lines[0].text, 'foo');
|
||||
expect(buf.lines[1].text, 'bar');
|
||||
expect(buf.lines[2].text, 'baz');
|
||||
});
|
||||
});
|
||||
|
||||
test('indent', () {
|
||||
buf
|
||||
..writeln('foo')
|
||||
..indent()
|
||||
..writeln('bar')
|
||||
..indent()
|
||||
..writeln('baz')
|
||||
..outdent()
|
||||
..writeln('quux')
|
||||
..outdent()
|
||||
..writeln('end');
|
||||
expect(buf.toString(), 'foo\n bar\n baz\n quux\nend');
|
||||
});
|
||||
|
||||
group('sets lastLine text', () {
|
||||
test('writeCharCode', () {
|
||||
buf.writeCharCode($x);
|
||||
expect(buf.lastLine!.text, 'x');
|
||||
});
|
||||
|
||||
test('write', () {
|
||||
buf.write('hello world');
|
||||
expect(buf.lastLine!.text, 'hello world');
|
||||
});
|
||||
});
|
||||
|
||||
group('sets lastLine lastSpan', () {
|
||||
test('writeCharCode', () {
|
||||
buf.writeCharCode($x);
|
||||
expect(buf.lastLine!.lastSpan!.text, 'x');
|
||||
});
|
||||
|
||||
test('write', () {
|
||||
buf.write('hello world');
|
||||
expect(buf.lastLine!.lastSpan!.text, 'hello world');
|
||||
});
|
||||
});
|
||||
}
|
71
packages/combinator/.gitignore
vendored
Normal file
71
packages/combinator/.gitignore
vendored
Normal file
|
@ -0,0 +1,71 @@
|
|||
# See https://www.dartlang.org/tools/private-files.html
|
||||
|
||||
# Files and directories created by pub
|
||||
.dart_tool
|
||||
.packages
|
||||
.pub/
|
||||
build/
|
||||
|
||||
# If you're building an application, you may want to check-in your pubspec.lock
|
||||
pubspec.lock
|
||||
|
||||
# Directory created by dartdoc
|
||||
# If you don't generate documentation locally you can remove this line.
|
||||
doc/api/
|
||||
|
||||
### Dart template
|
||||
# See https://www.dartlang.org/tools/private-files.html
|
||||
|
||||
# Files and directories created by pub
|
||||
|
||||
# SDK 1.20 and later (no longer creates packages directories)
|
||||
|
||||
# Older SDK versions
|
||||
# (Include if the minimum SDK version specified in pubsepc.yaml is earlier than 1.20)
|
||||
.project
|
||||
.buildlog
|
||||
**/packages/
|
||||
|
||||
|
||||
# Files created by dart2js
|
||||
# (Most Dart developers will use pub build to compile Dart, use/modify these
|
||||
# rules if you intend to use dart2js directly
|
||||
# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
|
||||
# differentiate from explicit Javascript files)
|
||||
*.dart.js
|
||||
*.part.js
|
||||
*.js.deps
|
||||
*.js.map
|
||||
*.info.json
|
||||
|
||||
# Directory created by dartdoc
|
||||
|
||||
# Don't commit pubspec lock file
|
||||
# (Library packages only! Remove pattern if developing an application package)
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff:
|
||||
|
||||
## VsCode
|
||||
.vscode/
|
||||
|
||||
## File-based project format:
|
||||
*.iws
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# IntelliJ
|
||||
.idea/
|
||||
/out/
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
4
packages/combinator/.travis.yml
Normal file
4
packages/combinator/.travis.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
language: dart
|
||||
dart:
|
||||
- stable
|
||||
- dev
|
12
packages/combinator/AUTHORS.md
Normal file
12
packages/combinator/AUTHORS.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
Primary Authors
|
||||
===============
|
||||
|
||||
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
|
||||
|
||||
Thomas is the current maintainer of the code base. He has refactored and migrated the
|
||||
code base to support NNBD.
|
||||
|
||||
* __[Tobe O](thosakwe@gmail.com)__
|
||||
|
||||
Tobe has written much of the original code prior to NNBD migration. He has moved on and
|
||||
is no longer involved with the project.
|
17
packages/combinator/CHANGELOG.md
Normal file
17
packages/combinator/CHANGELOG.md
Normal file
|
@ -0,0 +1,17 @@
|
|||
# 2.0.1
|
||||
* Updated README
|
||||
|
||||
# 2.0.0
|
||||
* Migrated to support Dart SDK 2.12.x NNBD
|
||||
|
||||
# 1.1.0
|
||||
* Add `tupleX` parsers. Hooray for strong typing!
|
||||
|
||||
# 1.0.0+3
|
||||
* `then` now *always* returns `dynamic`.
|
||||
|
||||
# 1.0.0+2
|
||||
* `star` now includes with a call to `opt`.
|
||||
* Added comments.
|
||||
* Enforce generics on `separatedBy`.
|
||||
* Enforce Dart 2 semantics.
|
21
packages/combinator/LICENSE
Normal file
21
packages/combinator/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 dukefirehawk.com
|
||||
|
||||
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.
|
123
packages/combinator/README.md
Normal file
123
packages/combinator/README.md
Normal file
|
@ -0,0 +1,123 @@
|
|||
# angel3_combinator
|
||||
[![version](https://img.shields.io/badge/pub-v2.0.1-brightgreen)](https://pub.dartlang.org/packages/angel3_combinator)
|
||||
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
|
||||
[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion)
|
||||
|
||||
[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/angel3/packages/combinator/LICENSE)
|
||||
|
||||
Packrat parser combinators that support static typing, generics, file spans, memoization, and more.
|
||||
|
||||
**RECOMMENDED:**
|
||||
Check `example/` for examples.
|
||||
The examples contain examples of using:
|
||||
* Generic typing
|
||||
* Reading `FileSpan` from `ParseResult`
|
||||
* More...
|
||||
|
||||
## Basic Usage
|
||||
```dart
|
||||
void main() {
|
||||
// Parse a Pattern (usually String or RegExp).
|
||||
var foo = match('foo');
|
||||
var number = match(RegExp(r'[0-9]+'), errorMessage: 'Expected a number.');
|
||||
|
||||
// Set a value.
|
||||
var numWithValue = number.map((r) => int.parse(r.span.text));
|
||||
|
||||
// Expect a pattern, or nothing.
|
||||
var optional = numWithValue.opt();
|
||||
|
||||
// Expect a pattern zero or more times.
|
||||
var star = optional.star();
|
||||
|
||||
// Expect one or more times.
|
||||
var plus = optional.plus();
|
||||
|
||||
// Expect an arbitrary number of times.
|
||||
var threeTimes = optional.times(3);
|
||||
|
||||
// Expect a sequence of patterns.
|
||||
var doraTheExplorer = chain([
|
||||
match('Dora').space(),
|
||||
match('the').space(),
|
||||
match('Explorer').space(),
|
||||
]);
|
||||
|
||||
// Choose exactly one of a set of patterns, whichever
|
||||
// appears first.
|
||||
var alt = any([
|
||||
match('1'),
|
||||
match('11'),
|
||||
match('111'),
|
||||
]);
|
||||
|
||||
// Choose the *longest* match for any of the given alternatives.
|
||||
var alt2 = longest([
|
||||
match('1'),
|
||||
match('11'),
|
||||
match('111'),
|
||||
]);
|
||||
|
||||
// Friendly operators
|
||||
var fooOrNumber = foo | number;
|
||||
var fooAndNumber = foo & number;
|
||||
var notFoo = ~foo;
|
||||
}
|
||||
```
|
||||
|
||||
## Error Messages
|
||||
Parsers without descriptive error messages can lead to frustrating dead-ends
|
||||
for end-users. Fortunately, `angel3_combinator` is built with error handling in mind.
|
||||
|
||||
```dart
|
||||
void main(Parser parser) {
|
||||
// Append an arbitrary error message to a parser if it is not matched.
|
||||
var withError = parser.error(errorMessage: 'Hey!!! Wrong!!!');
|
||||
|
||||
// You can also set the severity of an error.
|
||||
var asHint = parser.error(severity: SyntaxErrorSeverity.hint);
|
||||
|
||||
// Constructs like `any`, `chain`, and `longest` support this as well.
|
||||
var foo = longest([
|
||||
parser.error(errorMessage: 'foo'),
|
||||
parser.error(errorMessage: 'bar')
|
||||
], errorMessage: 'Expected a "foo" or a "bar"');
|
||||
|
||||
// If multiple errors are present at one location,
|
||||
// it can create a lot of noise.
|
||||
//
|
||||
// Use `foldErrors` to only take one error at a given location.
|
||||
var lessNoise = parser.foldErrors();
|
||||
}
|
||||
```
|
||||
|
||||
## Whitespaces
|
||||
Handling optional whitespace is dead-easy:
|
||||
|
||||
```dart
|
||||
void main(Parser parser) {
|
||||
var optionalSpace = parser.space();
|
||||
}
|
||||
```
|
||||
|
||||
## For Programming Languages
|
||||
`angel3_combinator` was conceived to make writing parsers for complex grammars easier,
|
||||
namely programming languages. Thus, there are functions built-in to make common constructs
|
||||
easier:
|
||||
|
||||
```dart
|
||||
void main(Parser parser) {
|
||||
var array = parser
|
||||
.separatedByComma()
|
||||
.surroundedBySquareBrackets(defaultValue: []);
|
||||
|
||||
var braces = parser.surroundedByCurlyBraces();
|
||||
|
||||
var sep = parser.separatedBy(match('!').space());
|
||||
}
|
||||
```
|
||||
|
||||
## Differences between this and Petitparser
|
||||
* `angel3_combinator` makes extensive use of Dart's dynamic typing
|
||||
* `angel3_combinator` supports detailed error messages (with configurable severity)
|
||||
* `angel3_combinator` keeps track of locations (ex. `line 1: 3`)
|
4
packages/combinator/analysis_options.yaml
Normal file
4
packages/combinator/analysis_options.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
analyzer:
|
||||
strong-mode:
|
||||
implicit-casts: false
|
||||
#implicit-dynamic: false
|
14
packages/combinator/combinator.iml
Normal file
14
packages/combinator/combinator.iml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build" />
|
||||
</content>
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="Dart SDK" level="project" />
|
||||
<orderEntry type="library" name="Dart Packages" level="project" />
|
||||
</component>
|
||||
</module>
|
55
packages/combinator/example/basic_auth.dart
Normal file
55
packages/combinator/example/basic_auth.dart
Normal file
|
@ -0,0 +1,55 @@
|
|||
// Run this with "Basic QWxhZGRpbjpPcGVuU2VzYW1l"
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:angel3_combinator/angel3_combinator.dart';
|
||||
import 'package:string_scanner/string_scanner.dart';
|
||||
|
||||
/// Parse a part of a decoded Basic auth string.
|
||||
///
|
||||
/// Namely, the `username` or `password` in `{username}:{password}`.
|
||||
final Parser<String> string =
|
||||
match<String>(RegExp(r'[^:$]+'), errorMessage: 'Expected a string.')
|
||||
.value((r) => r.span!.text);
|
||||
|
||||
/// Transforms `{username}:{password}` to `{"username": username, "password": password}`.
|
||||
final Parser<Map<String, String>> credentials = chain<String>([
|
||||
string.opt(),
|
||||
match<String>(':'),
|
||||
string.opt(),
|
||||
]).map<Map<String, String>>(
|
||||
(r) => {'username': r.value![0], 'password': r.value![2]});
|
||||
|
||||
/// We can actually embed a parser within another parser.
|
||||
///
|
||||
/// This is used here to BASE64URL-decode a string, and then
|
||||
/// parse the decoded string.
|
||||
final Parser credentialString = match<Map<String, String>?>(
|
||||
RegExp(r'([^\n$]+)'),
|
||||
errorMessage: 'Expected a credential string.')
|
||||
.value((r) {
|
||||
var decoded = utf8.decode(base64Url.decode(r.span!.text));
|
||||
var scanner = SpanScanner(decoded);
|
||||
return credentials.parse(scanner).value;
|
||||
});
|
||||
|
||||
final Parser basic = match<Null>('Basic').space();
|
||||
|
||||
final Parser basicAuth = basic.then(credentialString).index(1);
|
||||
|
||||
void main() {
|
||||
while (true) {
|
||||
stdout.write('Enter a basic auth value: ');
|
||||
var line = stdin.readLineSync()!;
|
||||
var scanner = SpanScanner(line, sourceUrl: 'stdin');
|
||||
var result = basicAuth.parse(scanner);
|
||||
|
||||
if (!result.successful) {
|
||||
for (var error in result.errors) {
|
||||
print(error.toolString);
|
||||
print(error.span!.highlight(color: true));
|
||||
}
|
||||
} else
|
||||
print(result.value);
|
||||
}
|
||||
}
|
70
packages/combinator/example/calculator.dart
Normal file
70
packages/combinator/example/calculator.dart
Normal file
|
@ -0,0 +1,70 @@
|
|||
import 'dart:math';
|
||||
import 'dart:io';
|
||||
import 'package:angel3_combinator/angel3_combinator.dart';
|
||||
import 'package:string_scanner/string_scanner.dart';
|
||||
|
||||
/// Note: This grammar does not handle precedence, for the sake of simplicity.
|
||||
Parser<num> calculatorGrammar() {
|
||||
var expr = reference<num>();
|
||||
|
||||
var number = match<num>(RegExp(r'-?[0-9]+(\.[0-9]+)?'))
|
||||
.value((r) => num.parse(r.span!.text));
|
||||
|
||||
var hex = match<int>(RegExp(r'0x([A-Fa-f0-9]+)'))
|
||||
.map((r) => int.parse(r.scanner.lastMatch![1]!, radix: 16));
|
||||
|
||||
var binary = match<int>(RegExp(r'([0-1]+)b'))
|
||||
.map((r) => int.parse(r.scanner.lastMatch![1]!, radix: 2));
|
||||
|
||||
var alternatives = <Parser<num>>[];
|
||||
|
||||
void registerBinary(String op, num Function(num, num) f) {
|
||||
alternatives.add(
|
||||
chain<num>([
|
||||
expr.space(),
|
||||
match<Null>(op).space() as Parser<num>,
|
||||
expr.space(),
|
||||
]).map((r) => f(r.value![0], r.value![2])),
|
||||
);
|
||||
}
|
||||
|
||||
registerBinary('**', (a, b) => pow(a, b));
|
||||
registerBinary('*', (a, b) => a * b);
|
||||
registerBinary('/', (a, b) => a / b);
|
||||
registerBinary('%', (a, b) => a % b);
|
||||
registerBinary('+', (a, b) => a + b);
|
||||
registerBinary('-', (a, b) => a - b);
|
||||
registerBinary('^', (a, b) => a.toInt() ^ b.toInt());
|
||||
registerBinary('&', (a, b) => a.toInt() & b.toInt());
|
||||
registerBinary('|', (a, b) => a.toInt() | b.toInt());
|
||||
|
||||
alternatives.addAll([
|
||||
number,
|
||||
hex,
|
||||
binary,
|
||||
expr.parenthesized(),
|
||||
]);
|
||||
|
||||
expr.parser = longest(alternatives);
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
void main() {
|
||||
var calculator = calculatorGrammar();
|
||||
|
||||
while (true) {
|
||||
stdout.write('Enter an expression: ');
|
||||
var line = stdin.readLineSync()!;
|
||||
var scanner = SpanScanner(line, sourceUrl: 'stdin');
|
||||
var result = calculator.parse(scanner);
|
||||
|
||||
if (!result.successful) {
|
||||
for (var error in result.errors) {
|
||||
stderr.writeln(error.toolString);
|
||||
stderr.writeln(error.span!.highlight(color: true));
|
||||
}
|
||||
} else
|
||||
print(result.value);
|
||||
}
|
||||
}
|
28
packages/combinator/example/delimiter.dart
Normal file
28
packages/combinator/example/delimiter.dart
Normal file
|
@ -0,0 +1,28 @@
|
|||
import 'dart:io';
|
||||
import 'package:angel3_combinator/angel3_combinator.dart';
|
||||
import 'package:string_scanner/string_scanner.dart';
|
||||
|
||||
final Parser<String> id =
|
||||
match<String>(RegExp(r'[A-Za-z]+')).value((r) => r.span!.text);
|
||||
|
||||
// We can use `separatedBy` to easily construct parser
|
||||
// that can be matched multiple times, separated by another
|
||||
// pattern.
|
||||
//
|
||||
// This is useful for parsing arrays or map literals.
|
||||
main() {
|
||||
while (true) {
|
||||
stdout.write('Enter a string (ex "a,b,c"): ');
|
||||
var line = stdin.readLineSync()!;
|
||||
var scanner = SpanScanner(line, sourceUrl: 'stdin');
|
||||
var result = id.separatedBy(match(',').space()).parse(scanner);
|
||||
|
||||
if (!result.successful) {
|
||||
for (var error in result.errors) {
|
||||
print(error.toolString);
|
||||
print(error.span!.highlight(color: true));
|
||||
}
|
||||
} else
|
||||
print(result.value);
|
||||
}
|
||||
}
|
70
packages/combinator/example/json.dart
Normal file
70
packages/combinator/example/json.dart
Normal file
|
@ -0,0 +1,70 @@
|
|||
import 'dart:io';
|
||||
import 'package:angel3_combinator/angel3_combinator.dart';
|
||||
import 'package:string_scanner/string_scanner.dart';
|
||||
|
||||
Parser jsonGrammar() {
|
||||
var expr = reference();
|
||||
|
||||
// Parse a number
|
||||
var number = match<num>(RegExp(r'-?[0-9]+(\.[0-9]+)?'),
|
||||
errorMessage: 'Expected a number.')
|
||||
.value(
|
||||
(r) => num.parse(r.span!.text),
|
||||
);
|
||||
|
||||
// Parse a string (no escapes supported, because lazy).
|
||||
var string =
|
||||
match(RegExp(r'"[^"]*"'), errorMessage: 'Expected a string.').value(
|
||||
(r) => r.span!.text.substring(1, r.span!.text.length - 1),
|
||||
);
|
||||
|
||||
// Parse an array
|
||||
var array = expr
|
||||
.space()
|
||||
.separatedByComma()
|
||||
.surroundedBySquareBrackets(defaultValue: []);
|
||||
|
||||
// KV pair
|
||||
var keyValuePair = chain([
|
||||
string.space(),
|
||||
match(':').space(),
|
||||
expr.error(errorMessage: 'Missing expression.'),
|
||||
]).castDynamic().cast<Map>().value((r) => {r.value![0]: r.value![2]});
|
||||
|
||||
// Parse an object.
|
||||
var object = keyValuePair
|
||||
.separatedByComma()
|
||||
.castDynamic()
|
||||
.surroundedByCurlyBraces(defaultValue: {});
|
||||
|
||||
expr.parser = longest(
|
||||
[
|
||||
array,
|
||||
number,
|
||||
string,
|
||||
object.error(),
|
||||
],
|
||||
errorMessage: 'Expected an expression.',
|
||||
).space();
|
||||
|
||||
return expr.foldErrors();
|
||||
}
|
||||
|
||||
main() {
|
||||
var JSON = jsonGrammar();
|
||||
|
||||
while (true) {
|
||||
stdout.write('Enter some JSON: ');
|
||||
var line = stdin.readLineSync()!;
|
||||
var scanner = SpanScanner(line, sourceUrl: 'stdin');
|
||||
var result = JSON.parse(scanner);
|
||||
|
||||
if (!result.successful) {
|
||||
for (var error in result.errors) {
|
||||
print(error.toolString);
|
||||
print(error.span!.highlight(color: true));
|
||||
}
|
||||
} else
|
||||
print(result.value);
|
||||
}
|
||||
}
|
37
packages/combinator/example/main.dart
Normal file
37
packages/combinator/example/main.dart
Normal file
|
@ -0,0 +1,37 @@
|
|||
import 'dart:io';
|
||||
import 'package:angel3_combinator/angel3_combinator.dart';
|
||||
import 'package:string_scanner/string_scanner.dart';
|
||||
|
||||
final Parser minus = match('-');
|
||||
|
||||
final Parser<int> digit =
|
||||
match(RegExp(r'[0-9]'), errorMessage: 'Expected a number');
|
||||
|
||||
final Parser digits = digit.plus();
|
||||
|
||||
final Parser dot = match('.');
|
||||
|
||||
final Parser decimal = ( // digits, (dot, digits)?
|
||||
digits & (dot & digits).opt() //
|
||||
);
|
||||
|
||||
final Parser number = //
|
||||
(minus.opt() & decimal) // minus?, decimal
|
||||
.map<num>((r) => num.parse(r.span!.text));
|
||||
|
||||
main() {
|
||||
while (true) {
|
||||
stdout.write('Enter a number: ');
|
||||
var line = stdin.readLineSync()!;
|
||||
var scanner = SpanScanner(line, sourceUrl: 'stdin');
|
||||
var result = number.parse(scanner);
|
||||
|
||||
if (!result.successful) {
|
||||
for (var error in result.errors) {
|
||||
stderr.writeln(error.toolString);
|
||||
stderr.writeln(error.span!.highlight(color: true));
|
||||
}
|
||||
} else
|
||||
print(result.value);
|
||||
}
|
||||
}
|
44
packages/combinator/example/query_string.dart
Normal file
44
packages/combinator/example/query_string.dart
Normal file
|
@ -0,0 +1,44 @@
|
|||
// For some reason, this cannot be run in checked mode???
|
||||
|
||||
import 'dart:io';
|
||||
import 'package:angel3_combinator/angel3_combinator.dart';
|
||||
import 'package:string_scanner/string_scanner.dart';
|
||||
|
||||
final Parser<String> key =
|
||||
match<String>(RegExp(r'[^=&\n]+'), errorMessage: 'Missing k/v')
|
||||
.value((r) => r.span!.text);
|
||||
|
||||
final Parser value = key.map((r) => Uri.decodeQueryComponent(r.value!));
|
||||
|
||||
final Parser pair = chain([
|
||||
key,
|
||||
match('='),
|
||||
value,
|
||||
]).map((r) {
|
||||
return {
|
||||
r.value![0]: r.value![2],
|
||||
};
|
||||
});
|
||||
|
||||
final Parser pairs = pair
|
||||
.separatedBy(match(r'&'))
|
||||
.map((r) => r.value!.reduce((a, b) => a..addAll(b)));
|
||||
|
||||
final Parser queryString = pairs.opt();
|
||||
|
||||
main() {
|
||||
while (true) {
|
||||
stdout.write('Enter a query string: ');
|
||||
var line = stdin.readLineSync()!;
|
||||
var scanner = SpanScanner(line, sourceUrl: 'stdin');
|
||||
var result = pairs.parse(scanner);
|
||||
|
||||
if (!result.successful) {
|
||||
for (var error in result.errors) {
|
||||
print(error.toolString);
|
||||
print(error.span!.highlight(color: true));
|
||||
}
|
||||
} else
|
||||
print(result.value);
|
||||
}
|
||||
}
|
84
packages/combinator/example/sexp.dart
Normal file
84
packages/combinator/example/sexp.dart
Normal file
|
@ -0,0 +1,84 @@
|
|||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:angel3_combinator/angel3_combinator.dart';
|
||||
import 'package:string_scanner/string_scanner.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
void main() {
|
||||
var expr = reference();
|
||||
var symbols = <String, dynamic>{};
|
||||
|
||||
void registerFunction(String name, int nArgs, Function(List<num>) f) {
|
||||
symbols[name] = Tuple2(nArgs, f);
|
||||
}
|
||||
|
||||
registerFunction('**', 2, (args) => pow(args[0], args[1]));
|
||||
registerFunction('*', 2, (args) => args[0] * args[1]);
|
||||
registerFunction('/', 2, (args) => args[0] / args[1]);
|
||||
registerFunction('%', 2, (args) => args[0] % args[1]);
|
||||
registerFunction('+', 2, (args) => args[0] + args[1]);
|
||||
registerFunction('-', 2, (args) => args[0] - args[1]);
|
||||
registerFunction('.', 1, (args) => args[0].toDouble());
|
||||
registerFunction('print', 1, (args) {
|
||||
print(args[0]);
|
||||
return args[0];
|
||||
});
|
||||
|
||||
var number =
|
||||
match(RegExp(r'[0-9]+(\.[0-9]+)?'), errorMessage: 'Expected a number.')
|
||||
.map((r) => num.parse(r.span!.text));
|
||||
|
||||
var id = match(
|
||||
RegExp(
|
||||
r'[A-Za-z_!\\$",\\+-\\./:;\\?<>%&\\*@\[\]\\{\}\\|`\\^~][A-Za-z0-9_!\\$",\\+-\\./:;\\?<>%&\*@\[\]\\{\}\\|`\\^~]*'),
|
||||
errorMessage: 'Expected an ID')
|
||||
.map((r) => symbols[r.span!.text] ??=
|
||||
throw "Undefined symbol: '${r.span!.text}'");
|
||||
|
||||
var atom = number.castDynamic().or(id);
|
||||
|
||||
var list = expr.space().times(2, exact: false).map((r) {
|
||||
try {
|
||||
var out = [];
|
||||
var q = Queue.from(r.value!.reversed);
|
||||
|
||||
while (q.isNotEmpty) {
|
||||
var current = q.removeFirst();
|
||||
if (current is! Tuple2)
|
||||
out.insert(0, current);
|
||||
else {
|
||||
var args = [];
|
||||
for (int i = 0; i < (current.item1 as num); i++)
|
||||
args.add(out.removeLast());
|
||||
out.add(current.item2(args));
|
||||
}
|
||||
}
|
||||
|
||||
return out.length == 1 ? out.first : out;
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
expr.parser = longest([
|
||||
list,
|
||||
atom,
|
||||
expr.parenthesized(),
|
||||
]); //list | atom | expr.parenthesized();
|
||||
|
||||
while (true) {
|
||||
stdout.write('> ');
|
||||
var line = stdin.readLineSync()!;
|
||||
var result = expr.parse(SpanScanner(line));
|
||||
|
||||
if (result.errors.isNotEmpty) {
|
||||
for (var error in result.errors) {
|
||||
print(error.toolString);
|
||||
print(error.message);
|
||||
}
|
||||
} else {
|
||||
print(result.value);
|
||||
}
|
||||
}
|
||||
}
|
14
packages/combinator/example/tuple.dart
Normal file
14
packages/combinator/example/tuple.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
import 'package:angel3_combinator/angel3_combinator.dart';
|
||||
import 'package:string_scanner/string_scanner.dart';
|
||||
|
||||
void main() {
|
||||
var pub = match('pub').map((r) => r.span!.text).space();
|
||||
var dart = match('dart').map((r) => 24).space();
|
||||
var lang = match('lang').map((r) => true).space();
|
||||
|
||||
// Parses a Tuple3<String, int, bool>
|
||||
var grammar = tuple3(pub, dart, lang);
|
||||
|
||||
var scanner = SpanScanner('pub dart lang');
|
||||
print(grammar.parse(scanner).value);
|
||||
}
|
2
packages/combinator/lib/angel3_combinator.dart
Normal file
2
packages/combinator/lib/angel3_combinator.dart
Normal file
|
@ -0,0 +1,2 @@
|
|||
export 'src/combinator/combinator.dart';
|
||||
export 'src/error.dart';
|
26
packages/combinator/lib/src/combinator/advance.dart
Normal file
26
packages/combinator/lib/src/combinator/advance.dart
Normal file
|
@ -0,0 +1,26 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
class _Advance<T> extends Parser<T> {
|
||||
final Parser<T> parser;
|
||||
final int amount;
|
||||
|
||||
_Advance(this.parser, this.amount);
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
var result = parser._parse(args.increaseDepth()).change(parser: this);
|
||||
if (result.successful) args.scanner.position += amount;
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('advance($amount) (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
85
packages/combinator/lib/src/combinator/any.dart
Normal file
85
packages/combinator/lib/src/combinator/any.dart
Normal file
|
@ -0,0 +1,85 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
/// Matches any one of the given [parsers].
|
||||
///
|
||||
/// If [backtrack] is `true` (default), a failed parse will not modify the scanner state.
|
||||
///
|
||||
/// You can provide a custom [errorMessage]. You can set it to `false` to not
|
||||
/// generate any error at all.
|
||||
Parser<T> any<T>(Iterable<Parser<T>> parsers,
|
||||
{bool backtrack: true, errorMessage, SyntaxErrorSeverity? severity}) {
|
||||
return _Any(parsers, backtrack != false, errorMessage,
|
||||
severity ?? SyntaxErrorSeverity.error);
|
||||
}
|
||||
|
||||
class _Any<T> extends Parser<T> {
|
||||
final Iterable<Parser<T>> parsers;
|
||||
final bool backtrack;
|
||||
final errorMessage;
|
||||
final SyntaxErrorSeverity severity;
|
||||
|
||||
_Any(this.parsers, this.backtrack, this.errorMessage, this.severity);
|
||||
|
||||
@override
|
||||
ParseResult<T> _parse(ParseArgs args) {
|
||||
var inactive = parsers
|
||||
.where((p) => !args.trampoline.isActive(p, args.scanner.position));
|
||||
|
||||
if (inactive.isEmpty) {
|
||||
return ParseResult(args.trampoline, args.scanner, this, false, []);
|
||||
}
|
||||
|
||||
var errors = <SyntaxError>[];
|
||||
int replay = args.scanner.position;
|
||||
|
||||
for (var parser in inactive) {
|
||||
var result = parser._parse(args.increaseDepth());
|
||||
|
||||
if (result.successful)
|
||||
return result;
|
||||
else {
|
||||
if (backtrack) args.scanner.position = replay;
|
||||
if (parser is _Alt) errors.addAll(result.errors);
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessage != false) {
|
||||
errors.add(
|
||||
SyntaxError(
|
||||
severity,
|
||||
errorMessage?.toString() ??
|
||||
'No match found for ${parsers.length} alternative(s)',
|
||||
args.scanner.emptySpan,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ParseResult(args.trampoline, args.scanner, this, false, errors);
|
||||
}
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
// Never called
|
||||
throw ArgumentError("[Combinator] Invalid method call");
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('any(${parsers.length}) (')
|
||||
..indent();
|
||||
int i = 1;
|
||||
|
||||
for (var parser in parsers) {
|
||||
buffer
|
||||
..writeln('#${i++}:')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer.outdent();
|
||||
}
|
||||
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
26
packages/combinator/lib/src/combinator/cache.dart
Normal file
26
packages/combinator/lib/src/combinator/cache.dart
Normal file
|
@ -0,0 +1,26 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
class _Cache<T> extends Parser<T> {
|
||||
final Map<int, ParseResult<T>> _cache = {};
|
||||
final Parser<T> parser;
|
||||
|
||||
_Cache(this.parser);
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
return _cache.putIfAbsent(args.scanner.position, () {
|
||||
return parser._parse(args.increaseDepth());
|
||||
}).change(parser: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('cache(${_cache.length}) (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
63
packages/combinator/lib/src/combinator/cast.dart
Normal file
63
packages/combinator/lib/src/combinator/cast.dart
Normal file
|
@ -0,0 +1,63 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
class _Cast<T, U extends T> extends Parser<U> {
|
||||
final Parser<T> parser;
|
||||
|
||||
_Cast(this.parser);
|
||||
|
||||
@override
|
||||
ParseResult<U> __parse(ParseArgs args) {
|
||||
var result = parser._parse(args.increaseDepth());
|
||||
return ParseResult<U>(
|
||||
args.trampoline,
|
||||
args.scanner,
|
||||
this,
|
||||
result.successful,
|
||||
result.errors,
|
||||
span: result.span,
|
||||
value: result.value as U?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('cast<$U> (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
||||
|
||||
class _CastDynamic<T> extends Parser<dynamic> {
|
||||
final Parser<T> parser;
|
||||
|
||||
_CastDynamic(this.parser);
|
||||
|
||||
@override
|
||||
ParseResult<dynamic> __parse(ParseArgs args) {
|
||||
var result = parser._parse(args.increaseDepth());
|
||||
return ParseResult<dynamic>(
|
||||
args.trampoline,
|
||||
args.scanner,
|
||||
this,
|
||||
result.successful,
|
||||
result.errors,
|
||||
span: result.span,
|
||||
value: result.value,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('cast<dynamic> (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
111
packages/combinator/lib/src/combinator/chain.dart
Normal file
111
packages/combinator/lib/src/combinator/chain.dart
Normal file
|
@ -0,0 +1,111 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
/// Expects to parse a sequence of [parsers].
|
||||
///
|
||||
/// If [failFast] is `true` (default), then the first failure to parse will abort the parse.
|
||||
ListParser<T> chain<T>(Iterable<Parser<T>> parsers,
|
||||
{bool failFast: true, SyntaxErrorSeverity? severity}) {
|
||||
return _Chain<T>(
|
||||
parsers, failFast != false, severity ?? SyntaxErrorSeverity.error);
|
||||
}
|
||||
|
||||
class _Alt<T> extends Parser<T> {
|
||||
final Parser<T> parser;
|
||||
final String? errorMessage;
|
||||
final SyntaxErrorSeverity severity;
|
||||
|
||||
_Alt(this.parser, this.errorMessage, this.severity);
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
var result = parser._parse(args.increaseDepth());
|
||||
return result.successful
|
||||
? result
|
||||
: result.addErrors([
|
||||
SyntaxError(
|
||||
severity, errorMessage, result.span ?? args.scanner.emptySpan),
|
||||
]);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
parser.stringify(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
class _Chain<T> extends ListParser<T> {
|
||||
final Iterable<Parser<T>> parsers;
|
||||
final bool failFast;
|
||||
final SyntaxErrorSeverity severity;
|
||||
|
||||
_Chain(this.parsers, this.failFast, this.severity);
|
||||
|
||||
@override
|
||||
ParseResult<List<T>> __parse(ParseArgs args) {
|
||||
var errors = <SyntaxError>[];
|
||||
var results = <T>[];
|
||||
var spans = <FileSpan>[];
|
||||
bool successful = true;
|
||||
|
||||
for (var parser in parsers) {
|
||||
var result = parser._parse(args.increaseDepth());
|
||||
|
||||
if (!result.successful) {
|
||||
if (parser is _Alt) errors.addAll(result.errors);
|
||||
|
||||
if (failFast) {
|
||||
return ParseResult(
|
||||
args.trampoline, args.scanner, this, false, result.errors);
|
||||
}
|
||||
|
||||
successful = false;
|
||||
}
|
||||
|
||||
if (result.value != null) {
|
||||
results.add(result.value!);
|
||||
} else {
|
||||
results.add("NULL" as T);
|
||||
}
|
||||
|
||||
if (result.span != null) {
|
||||
spans.add(result.span!);
|
||||
}
|
||||
}
|
||||
|
||||
FileSpan? span;
|
||||
|
||||
if (spans.isNotEmpty) {
|
||||
span = spans.reduce((a, b) => a.expand(b));
|
||||
}
|
||||
|
||||
return ParseResult<List<T>>(
|
||||
args.trampoline,
|
||||
args.scanner,
|
||||
this,
|
||||
successful,
|
||||
errors,
|
||||
span: span,
|
||||
value: List<T>.unmodifiable(results),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('chain(${parsers.length}) (')
|
||||
..indent();
|
||||
int i = 1;
|
||||
|
||||
for (var parser in parsers) {
|
||||
buffer
|
||||
..writeln('#${i++}:')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer.outdent();
|
||||
}
|
||||
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
41
packages/combinator/lib/src/combinator/check.dart
Normal file
41
packages/combinator/lib/src/combinator/check.dart
Normal file
|
@ -0,0 +1,41 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
class _Check<T> extends Parser<T> {
|
||||
final Parser<T> parser;
|
||||
final Matcher matcher;
|
||||
final String? errorMessage;
|
||||
final SyntaxErrorSeverity severity;
|
||||
|
||||
_Check(this.parser, this.matcher, this.errorMessage, this.severity);
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
var matchState = {};
|
||||
var result = parser._parse(args.increaseDepth()).change(parser: this);
|
||||
if (!result.successful)
|
||||
return result;
|
||||
else if (!matcher.matches(result.value, matchState)) {
|
||||
return result.change(successful: false).addErrors([
|
||||
SyntaxError(
|
||||
severity,
|
||||
errorMessage ??
|
||||
matcher.describe(StringDescription('Expected ')).toString() + '.',
|
||||
result.span,
|
||||
),
|
||||
]);
|
||||
} else
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
var d = matcher.describe(StringDescription());
|
||||
buffer
|
||||
..writeln('check($d) (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
393
packages/combinator/lib/src/combinator/combinator.dart
Normal file
393
packages/combinator/lib/src/combinator/combinator.dart
Normal file
|
@ -0,0 +1,393 @@
|
|||
library lex.src.combinator;
|
||||
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:angel3_code_buffer/angel3_code_buffer.dart';
|
||||
import 'package:matcher/matcher.dart';
|
||||
import 'package:source_span/source_span.dart';
|
||||
import 'package:string_scanner/string_scanner.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
import '../error.dart';
|
||||
|
||||
part 'any.dart';
|
||||
|
||||
part 'advance.dart';
|
||||
|
||||
part 'cache.dart';
|
||||
|
||||
part 'cast.dart';
|
||||
|
||||
part 'chain.dart';
|
||||
|
||||
part 'check.dart';
|
||||
|
||||
part 'compare.dart';
|
||||
|
||||
part 'fold_errors.dart';
|
||||
|
||||
part 'index.dart';
|
||||
|
||||
part 'longest.dart';
|
||||
|
||||
part 'map.dart';
|
||||
|
||||
part 'match.dart';
|
||||
|
||||
part 'max_depth.dart';
|
||||
|
||||
part 'negate.dart';
|
||||
|
||||
part 'opt.dart';
|
||||
|
||||
part 'recursion.dart';
|
||||
|
||||
part 'reduce.dart';
|
||||
|
||||
part 'reference.dart';
|
||||
|
||||
part 'repeat.dart';
|
||||
|
||||
part 'safe.dart';
|
||||
|
||||
part 'to_list.dart';
|
||||
|
||||
part 'util.dart';
|
||||
|
||||
part 'value.dart';
|
||||
|
||||
class ParseArgs {
|
||||
final Trampoline trampoline;
|
||||
final SpanScanner scanner;
|
||||
final int depth;
|
||||
|
||||
ParseArgs(this.trampoline, this.scanner, this.depth);
|
||||
|
||||
ParseArgs increaseDepth() => ParseArgs(trampoline, scanner, depth + 1);
|
||||
}
|
||||
|
||||
/// A parser combinator, which can parse very complicated grammars in a manageable manner.
|
||||
abstract class Parser<T> {
|
||||
ParseResult<T> __parse(ParseArgs args);
|
||||
|
||||
ParseResult<T> _parse(ParseArgs args) {
|
||||
var pos = args.scanner.position;
|
||||
|
||||
if (args.trampoline.hasMemoized(this, pos))
|
||||
return args.trampoline.getMemoized<T>(this, pos);
|
||||
|
||||
if (args.trampoline.isActive(this, pos))
|
||||
return ParseResult(args.trampoline, args.scanner, this, false, []);
|
||||
|
||||
args.trampoline.enter(this, pos);
|
||||
var result = __parse(args);
|
||||
args.trampoline.memoize(this, pos, result);
|
||||
args.trampoline.exit(this);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Parses text from a [SpanScanner].
|
||||
ParseResult<T> parse(SpanScanner scanner, [int depth = 1]) {
|
||||
var args = ParseArgs(Trampoline(), scanner, depth);
|
||||
return _parse(args);
|
||||
}
|
||||
|
||||
/// Skips forward a certain amount of steps after parsing, if it was successful.
|
||||
Parser<T> forward(int amount) => _Advance<T>(this, amount);
|
||||
|
||||
/// Moves backward a certain amount of steps after parsing, if it was successful.
|
||||
Parser<T> back(int amount) => _Advance<T>(this, amount * -1);
|
||||
|
||||
/// Casts this parser to produce [U] objects.
|
||||
Parser<U> cast<U extends T>() => _Cast<T, U>(this);
|
||||
|
||||
/// Casts this parser to produce [dynamic] objects.
|
||||
Parser<dynamic> castDynamic() => _CastDynamic<T>(this);
|
||||
|
||||
// TODO: Type issue
|
||||
/// Runs the given function, which changes the returned [ParseResult] into one relating to a [U] object.
|
||||
Parser<U> change<U>(ParseResult<U> Function(ParseResult<T>) f) {
|
||||
return _Change<T, U>(this, f);
|
||||
}
|
||||
|
||||
/// Validates the parse result against a [Matcher].
|
||||
///
|
||||
/// You can provide a custom [errorMessage].
|
||||
Parser<T> check(Matcher matcher,
|
||||
{String? errorMessage, SyntaxErrorSeverity? severity}) =>
|
||||
_Check<T>(
|
||||
this, matcher, errorMessage, severity ?? SyntaxErrorSeverity.error);
|
||||
|
||||
/// Binds an [errorMessage] to a copy of this parser.
|
||||
Parser<T> error({String? errorMessage, SyntaxErrorSeverity? severity}) =>
|
||||
_Alt<T>(this, errorMessage, severity ?? SyntaxErrorSeverity.error);
|
||||
|
||||
/// Removes multiple errors that occur in the same spot; this can reduce noise in parser output.
|
||||
Parser<T> foldErrors({bool equal(SyntaxError a, SyntaxError b)?}) {
|
||||
equal ??= (b, e) => b.span?.start.offset == e.span?.start.offset;
|
||||
return _FoldErrors<T>(this, equal);
|
||||
}
|
||||
|
||||
/// Transforms the parse result using a unary function.
|
||||
Parser<U> map<U>(U Function(ParseResult<T>) f) {
|
||||
return _Map<T, U>(this, f);
|
||||
}
|
||||
|
||||
/// Prevents recursion past a certain [depth], preventing stack overflow errors.
|
||||
Parser<T> maxDepth(int depth) => _MaxDepth<T>(this, depth);
|
||||
|
||||
Parser<T> operator ~() => negate();
|
||||
|
||||
/// Ensures this pattern is not matched.
|
||||
///
|
||||
/// You can provide an [errorMessage].
|
||||
Parser<T> negate(
|
||||
{String errorMessage = 'Negate error',
|
||||
SyntaxErrorSeverity severity = SyntaxErrorSeverity.error}) =>
|
||||
_Negate<T>(this, errorMessage, severity);
|
||||
|
||||
/// Caches the results of parse attempts at various locations within the source text.
|
||||
///
|
||||
/// Use this to prevent excessive recursion.
|
||||
Parser<T> cache() => _Cache<T>(this);
|
||||
|
||||
Parser<T> operator &(Parser<T> other) => and(other);
|
||||
|
||||
/// Consumes `this` and another parser, but only considers the result of `this` parser.
|
||||
Parser<T> and(Parser other) => then(other).change<T>((r) {
|
||||
return ParseResult<T>(
|
||||
r.trampoline,
|
||||
r.scanner,
|
||||
this,
|
||||
r.successful,
|
||||
r.errors,
|
||||
span: r.span,
|
||||
value: (r.value != null ? r.value![0] : r.value) as T?,
|
||||
);
|
||||
});
|
||||
|
||||
Parser<T> operator |(Parser<T> other) => or(other);
|
||||
|
||||
/// Shortcut for [or]-ing two parsers.
|
||||
Parser<T> or<U>(Parser<T> other) => any<T>([this, other]);
|
||||
|
||||
/// Parses this sequence one or more times.
|
||||
ListParser<T> plus() => times(1, exact: false);
|
||||
|
||||
/// Safely escapes this parser when an error occurs.
|
||||
///
|
||||
/// The generated parser only runs once; repeated uses always exit eagerly.
|
||||
Parser<T> safe(
|
||||
{bool backtrack: true,
|
||||
String errorMessage = "error",
|
||||
SyntaxErrorSeverity? severity}) =>
|
||||
_Safe<T>(
|
||||
this, backtrack, errorMessage, severity ?? SyntaxErrorSeverity.error);
|
||||
|
||||
Parser<List<T>> separatedByComma() =>
|
||||
separatedBy(match<List<T>>(',').space());
|
||||
|
||||
/// Expects to see an infinite amounts of the pattern, separated by the [other] pattern.
|
||||
///
|
||||
/// Use this as a shortcut to parse arrays, parameter lists, etc.
|
||||
Parser<List<T>> separatedBy(Parser other) {
|
||||
var suffix = other.then(this).index(1).cast<T>();
|
||||
return this.then(suffix.star()).map((r) {
|
||||
var v = r.value;
|
||||
if (v == null || v.length < 2) {
|
||||
return [];
|
||||
}
|
||||
var preceding = v.isEmpty ? [] : (v[0] == null ? [] : [v[0]]);
|
||||
var out = List<T>.from(preceding);
|
||||
if (v[1] != null && v[1] != "NULL") {
|
||||
v[1].forEach((element) {
|
||||
out.add(element as T);
|
||||
});
|
||||
}
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
Parser<T> surroundedByCurlyBraces({required T defaultValue}) => opt()
|
||||
.surroundedBy(match('{').space(), match('}').space())
|
||||
.map((r) => r.value ?? defaultValue);
|
||||
|
||||
Parser<T> surroundedBySquareBrackets({required T defaultValue}) => opt()
|
||||
.surroundedBy(match('[').space(), match(']').space())
|
||||
.map((r) => r.value ?? defaultValue);
|
||||
|
||||
/// Expects to see the pattern, surrounded by the others.
|
||||
///
|
||||
/// If no [right] is provided, it expects to see the same pattern on both sides.
|
||||
/// Use this parse things like parenthesized expressions, arrays, etc.
|
||||
Parser<T> surroundedBy(Parser left, [Parser? right]) {
|
||||
return chain([
|
||||
left,
|
||||
this,
|
||||
right ?? left,
|
||||
]).index(1).castDynamic().cast<T>();
|
||||
}
|
||||
|
||||
/// Parses `this`, either as-is or wrapped in parentheses.
|
||||
Parser<T> maybeParenthesized() {
|
||||
return any([parenthesized(), this]);
|
||||
}
|
||||
|
||||
/// Parses `this`, wrapped in parentheses.
|
||||
Parser<T> parenthesized() =>
|
||||
surroundedBy(match('(').space(), match(')').space());
|
||||
|
||||
/// Consumes any trailing whitespace.
|
||||
Parser<T> space() => trail(RegExp(r'[ \n\r\t]+'));
|
||||
|
||||
/// Consumes 0 or more instance(s) of this parser.
|
||||
ListParser<T> star({bool backtrack: true}) =>
|
||||
times(1, exact: false, backtrack: backtrack).opt();
|
||||
|
||||
/// Shortcut for [chain]-ing two parsers together.
|
||||
ListParser<dynamic> then(Parser other) => chain<dynamic>([this, other]);
|
||||
|
||||
/// Casts this instance into a [ListParser].
|
||||
ListParser<T> toList() => _ToList<T>(this);
|
||||
|
||||
/// Consumes and ignores any trailing occurrences of [pattern].
|
||||
Parser<T> trail(Pattern pattern) =>
|
||||
then(match(pattern).opt()).first().cast<T>();
|
||||
|
||||
/// Expect this pattern a certain number of times.
|
||||
///
|
||||
/// If [exact] is `false` (default: `true`), then the generated parser will accept
|
||||
/// an infinite amount of occurrences after the specified [count].
|
||||
///
|
||||
/// You can provide custom error messages for when there are [tooFew] or [tooMany] occurrences.
|
||||
ListParser<T> times(int count,
|
||||
{bool exact: true,
|
||||
String tooFew = 'Too few',
|
||||
String tooMany = 'Too many',
|
||||
bool backtrack: true,
|
||||
SyntaxErrorSeverity? severity}) {
|
||||
return _Repeat<T>(this, count, exact, tooFew, tooMany, backtrack,
|
||||
severity ?? SyntaxErrorSeverity.error);
|
||||
}
|
||||
|
||||
/// Produces an optional copy of this parser.
|
||||
///
|
||||
/// If [backtrack] is `true` (default), then a failed parse will not
|
||||
/// modify the scanner state.
|
||||
Parser<T> opt({bool backtrack: true}) => _Opt(this, backtrack);
|
||||
|
||||
/// Sets the value of the [ParseResult].
|
||||
Parser<T> value(T Function(ParseResult<T?>) f) {
|
||||
return _Value<T>(this, f);
|
||||
}
|
||||
|
||||
/// Prints a representation of this parser, ideally without causing a stack overflow.
|
||||
void stringify(CodeBuffer buffer);
|
||||
}
|
||||
|
||||
/// A [Parser] that produces [List]s of a type [T].
|
||||
abstract class ListParser<T> extends Parser<List<T>> {
|
||||
/// Shortcut for calling [index] with `0`.
|
||||
Parser<T> first() => index(0);
|
||||
|
||||
/// Modifies this parser to only return the value at the given index [i].
|
||||
Parser<T> index(int i) => _Index<T>(this, i);
|
||||
|
||||
/// Shortcut for calling [index] with the greatest-possible index.
|
||||
Parser<T> last() => index(-1);
|
||||
|
||||
/// Modifies this parser to call `List.reduce` on the parsed values.
|
||||
Parser<T> reduce(T Function(T, T) combine) => _Reduce<T>(this, combine);
|
||||
|
||||
/// Sorts the parsed values, using the given [Comparator].
|
||||
ListParser<T> sort(Comparator<T> compare) => _Compare(this, compare);
|
||||
|
||||
@override
|
||||
ListParser<T> opt({bool backtrack: true}) => _ListOpt(this, backtrack);
|
||||
|
||||
/// Modifies this parser, returning only the values that match a predicate.
|
||||
Parser<List<T>> where(bool Function(T) f) =>
|
||||
map<List<T>>((r) => r.value?.where(f).toList() ?? []);
|
||||
|
||||
/// Condenses a [ListParser] into having a value of the combined span's text.
|
||||
Parser<String> flatten() => map<String>((r) => r.span?.text ?? '');
|
||||
}
|
||||
|
||||
/// Prevents stack overflow in recursive parsers.
|
||||
class Trampoline {
|
||||
final Map<Parser, Queue<int>> _active = {};
|
||||
final Map<Parser, List<Tuple2<int, ParseResult>>> _memo = {};
|
||||
|
||||
bool hasMemoized(Parser parser, int position) {
|
||||
var list = _memo[parser];
|
||||
return list?.any((t) => t.item1 == position) == true;
|
||||
}
|
||||
|
||||
ParseResult<T> getMemoized<T>(Parser parser, int position) {
|
||||
return _memo[parser]?.firstWhere((t) => t.item1 == position).item2
|
||||
as ParseResult<T>;
|
||||
}
|
||||
|
||||
void memoize(Parser parser, int position, ParseResult? result) {
|
||||
if (result != null) {
|
||||
var list = _memo.putIfAbsent(parser, () => []);
|
||||
var tuple = Tuple2(position, result);
|
||||
if (!list.contains(tuple)) list.add(tuple);
|
||||
}
|
||||
}
|
||||
|
||||
bool isActive(Parser parser, int position) {
|
||||
if (!_active.containsKey(parser)) {
|
||||
return false;
|
||||
}
|
||||
var q = _active[parser]!;
|
||||
if (q.isEmpty) return false;
|
||||
//return q.contains(position);
|
||||
return q.first == position;
|
||||
}
|
||||
|
||||
void enter(Parser parser, int position) {
|
||||
_active.putIfAbsent(parser, () => Queue()).addFirst(position);
|
||||
}
|
||||
|
||||
void exit(Parser parser) {
|
||||
if (_active.containsKey(parser)) _active[parser]?.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
/// The result generated by a [Parser].
|
||||
class ParseResult<T> {
|
||||
final Parser<T> parser;
|
||||
final bool successful;
|
||||
final Iterable<SyntaxError> errors;
|
||||
final FileSpan? span;
|
||||
final T? value;
|
||||
final SpanScanner scanner;
|
||||
final Trampoline trampoline;
|
||||
|
||||
ParseResult(
|
||||
this.trampoline, this.scanner, this.parser, this.successful, this.errors,
|
||||
{this.span, this.value});
|
||||
|
||||
ParseResult<T> change(
|
||||
{Parser<T>? parser,
|
||||
bool? successful,
|
||||
Iterable<SyntaxError> errors = const [],
|
||||
FileSpan? span,
|
||||
T? value}) {
|
||||
return ParseResult<T>(
|
||||
trampoline,
|
||||
scanner,
|
||||
parser ?? this.parser,
|
||||
successful ?? this.successful,
|
||||
errors.isNotEmpty ? errors : this.errors,
|
||||
span: span ?? this.span,
|
||||
value: value ?? this.value,
|
||||
);
|
||||
}
|
||||
|
||||
ParseResult<T> addErrors(Iterable<SyntaxError> errors) {
|
||||
return change(
|
||||
errors: List<SyntaxError>.from(this.errors)..addAll(errors),
|
||||
);
|
||||
}
|
||||
}
|
38
packages/combinator/lib/src/combinator/compare.dart
Normal file
38
packages/combinator/lib/src/combinator/compare.dart
Normal file
|
@ -0,0 +1,38 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
class _Compare<T> extends ListParser<T> {
|
||||
final ListParser<T> parser;
|
||||
final Comparator<T> compare;
|
||||
|
||||
_Compare(this.parser, this.compare);
|
||||
|
||||
@override
|
||||
ParseResult<List<T>> __parse(ParseArgs args) {
|
||||
ParseResult<List<T>> result = parser._parse(args.increaseDepth());
|
||||
if (!result.successful) return result;
|
||||
|
||||
result = result.change(
|
||||
value: result.value?.isNotEmpty == true ? result.value : []);
|
||||
result = result.change(value: List<T>.from(result.value!));
|
||||
return ParseResult<List<T>>(
|
||||
args.trampoline,
|
||||
args.scanner,
|
||||
this,
|
||||
true,
|
||||
[],
|
||||
span: result.span,
|
||||
value: result.value?..sort(compare),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('sort($compare) (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
29
packages/combinator/lib/src/combinator/fold_errors.dart
Normal file
29
packages/combinator/lib/src/combinator/fold_errors.dart
Normal file
|
@ -0,0 +1,29 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
class _FoldErrors<T> extends Parser<T> {
|
||||
final Parser<T> parser;
|
||||
final bool Function(SyntaxError, SyntaxError) equal;
|
||||
|
||||
_FoldErrors(this.parser, this.equal);
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
var result = parser._parse(args.increaseDepth()).change(parser: this);
|
||||
var errors = result.errors.fold<List<SyntaxError>>([], (out, e) {
|
||||
if (!out.any((b) => equal(e, b))) out.add(e);
|
||||
return out;
|
||||
});
|
||||
return result.change(errors: errors);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('fold errors (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
53
packages/combinator/lib/src/combinator/index.dart
Normal file
53
packages/combinator/lib/src/combinator/index.dart
Normal file
|
@ -0,0 +1,53 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
class _Index<T> extends Parser<T> {
|
||||
final ListParser<T> parser;
|
||||
final int index;
|
||||
|
||||
_Index(this.parser, this.index);
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
ParseResult<List<T>> result = parser._parse(args.increaseDepth());
|
||||
Object? value;
|
||||
|
||||
if (result.successful) {
|
||||
var vList = result.value;
|
||||
if (vList == null) {
|
||||
throw ArgumentError("ParseResult is null");
|
||||
}
|
||||
if (index == -1) {
|
||||
value = vList.last;
|
||||
} else {
|
||||
if (index < vList.length) {
|
||||
//TODO: Look at this
|
||||
// print(">>>>Index: $index, Size: ${vList.length}");
|
||||
// value =
|
||||
// index == -1 ? result.value!.last : result.value!.elementAt(index);
|
||||
value = result.value!.elementAt(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ParseResult<T>(
|
||||
args.trampoline,
|
||||
args.scanner,
|
||||
this,
|
||||
result.successful,
|
||||
result.errors,
|
||||
span: result.span,
|
||||
value: value as T?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('index($index) (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
114
packages/combinator/lib/src/combinator/longest.dart
Normal file
114
packages/combinator/lib/src/combinator/longest.dart
Normal file
|
@ -0,0 +1,114 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
/// Matches any one of the given [parsers].
|
||||
///
|
||||
/// You can provide a custom [errorMessage].
|
||||
Parser<T> longest<T>(Iterable<Parser<T>> parsers,
|
||||
{Object? errorMessage, SyntaxErrorSeverity? severity}) {
|
||||
return _Longest(parsers, errorMessage, severity ?? SyntaxErrorSeverity.error);
|
||||
}
|
||||
|
||||
class _Longest<T> extends Parser<T> {
|
||||
final Iterable<Parser<T>> parsers;
|
||||
final Object? errorMessage;
|
||||
final SyntaxErrorSeverity severity;
|
||||
|
||||
_Longest(this.parsers, this.errorMessage, this.severity);
|
||||
|
||||
@override
|
||||
ParseResult<T> _parse(ParseArgs args) {
|
||||
var inactive = parsers
|
||||
.toList()
|
||||
.where((p) => !args.trampoline.isActive(p, args.scanner.position));
|
||||
|
||||
if (inactive.isEmpty) {
|
||||
return ParseResult(args.trampoline, args.scanner, this, false, []);
|
||||
}
|
||||
|
||||
int replay = args.scanner.position;
|
||||
var errors = <SyntaxError>[];
|
||||
var results = <ParseResult<T>>[];
|
||||
|
||||
for (var parser in inactive) {
|
||||
var result = parser._parse(args.increaseDepth());
|
||||
|
||||
if (result.successful && result.span != null)
|
||||
results.add(result);
|
||||
else if (parser is _Alt) errors.addAll(result.errors);
|
||||
|
||||
args.scanner.position = replay;
|
||||
}
|
||||
|
||||
if (results.isNotEmpty) {
|
||||
results.sort((a, b) => b.span!.length.compareTo(a.span!.length));
|
||||
args.scanner.scan(results.first.span!.text);
|
||||
return results.first;
|
||||
}
|
||||
|
||||
if (errorMessage != false)
|
||||
errors.add(
|
||||
SyntaxError(
|
||||
severity,
|
||||
errorMessage?.toString() ??
|
||||
'No match found for ${parsers.length} alternative(s)',
|
||||
args.scanner.emptySpan,
|
||||
),
|
||||
);
|
||||
|
||||
return ParseResult(args.trampoline, args.scanner, this, false, errors);
|
||||
}
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
int replay = args.scanner.position;
|
||||
var errors = <SyntaxError>[];
|
||||
var results = <ParseResult<T>>[];
|
||||
|
||||
for (var parser in parsers) {
|
||||
var result = parser._parse(args.increaseDepth());
|
||||
|
||||
if (result.successful)
|
||||
results.add(result);
|
||||
else if (parser is _Alt) errors.addAll(result.errors);
|
||||
|
||||
args.scanner.position = replay;
|
||||
}
|
||||
|
||||
if (results.isNotEmpty) {
|
||||
results.sort((a, b) => b.span!.length.compareTo(a.span!.length));
|
||||
args.scanner.scan(results.first.span!.text);
|
||||
return results.first;
|
||||
}
|
||||
|
||||
errors.add(
|
||||
SyntaxError(
|
||||
severity,
|
||||
errorMessage?.toString() ??
|
||||
'No match found for ${parsers.length} alternative(s)',
|
||||
args.scanner.emptySpan,
|
||||
),
|
||||
);
|
||||
|
||||
return ParseResult(args.trampoline, args.scanner, this, false, errors);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('longest(${parsers.length}) (')
|
||||
..indent();
|
||||
int i = 1;
|
||||
|
||||
for (var parser in parsers) {
|
||||
buffer
|
||||
..writeln('#${i++}:')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer.outdent();
|
||||
}
|
||||
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
56
packages/combinator/lib/src/combinator/map.dart
Normal file
56
packages/combinator/lib/src/combinator/map.dart
Normal file
|
@ -0,0 +1,56 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
class _Map<T, U> extends Parser<U> {
|
||||
final Parser<T> parser;
|
||||
final U Function(ParseResult<T>) f;
|
||||
|
||||
_Map(this.parser, this.f);
|
||||
|
||||
@override
|
||||
ParseResult<U> __parse(ParseArgs args) {
|
||||
var result = parser._parse(args.increaseDepth());
|
||||
return ParseResult<U>(
|
||||
args.trampoline,
|
||||
args.scanner,
|
||||
this,
|
||||
result.successful,
|
||||
result.errors,
|
||||
span: result.span,
|
||||
value: result.successful ? f(result) : null,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('map<$U> (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
||||
|
||||
class _Change<T, U> extends Parser<U> {
|
||||
final Parser<T> parser;
|
||||
final ParseResult<U> Function(ParseResult<T>) f;
|
||||
|
||||
_Change(this.parser, this.f);
|
||||
|
||||
@override
|
||||
ParseResult<U> __parse(ParseArgs args) {
|
||||
return f(parser._parse(args.increaseDepth())).change(parser: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('change($f) (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
40
packages/combinator/lib/src/combinator/match.dart
Normal file
40
packages/combinator/lib/src/combinator/match.dart
Normal file
|
@ -0,0 +1,40 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
/// Expects to match a given [pattern]. If it is not matched, you can provide a custom [errorMessage].
|
||||
Parser<T> match<T>(Pattern pattern,
|
||||
{String? errorMessage, SyntaxErrorSeverity? severity}) =>
|
||||
_Match<T>(pattern, errorMessage, severity ?? SyntaxErrorSeverity.error);
|
||||
|
||||
class _Match<T> extends Parser<T> {
|
||||
final Pattern pattern;
|
||||
final String? errorMessage;
|
||||
final SyntaxErrorSeverity severity;
|
||||
|
||||
_Match(this.pattern, this.errorMessage, this.severity);
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
var scanner = args.scanner;
|
||||
if (!scanner.scan(pattern))
|
||||
return ParseResult(args.trampoline, scanner, this, false, [
|
||||
SyntaxError(
|
||||
severity,
|
||||
errorMessage ?? 'Expected "$pattern".',
|
||||
scanner.emptySpan,
|
||||
),
|
||||
]);
|
||||
return ParseResult<T>(
|
||||
args.trampoline,
|
||||
scanner,
|
||||
this,
|
||||
true,
|
||||
[],
|
||||
span: scanner.lastSpan,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer.writeln('match($pattern)');
|
||||
}
|
||||
}
|
28
packages/combinator/lib/src/combinator/max_depth.dart
Normal file
28
packages/combinator/lib/src/combinator/max_depth.dart
Normal file
|
@ -0,0 +1,28 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
class _MaxDepth<T> extends Parser<T> {
|
||||
final Parser<T> parser;
|
||||
final int cap;
|
||||
|
||||
_MaxDepth(this.parser, this.cap);
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
if (args.depth > cap) {
|
||||
return ParseResult<T>(args.trampoline, args.scanner, this, false, []);
|
||||
}
|
||||
|
||||
return parser._parse(args.increaseDepth());
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('max depth($cap) (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
51
packages/combinator/lib/src/combinator/negate.dart
Normal file
51
packages/combinator/lib/src/combinator/negate.dart
Normal file
|
@ -0,0 +1,51 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
class _Negate<T> extends Parser<T> {
|
||||
final Parser<T> parser;
|
||||
final String? errorMessage;
|
||||
final SyntaxErrorSeverity severity;
|
||||
|
||||
_Negate(this.parser, this.errorMessage, this.severity);
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
var result = parser._parse(args.increaseDepth()).change(parser: this);
|
||||
|
||||
if (!result.successful) {
|
||||
return ParseResult<T>(
|
||||
args.trampoline,
|
||||
args.scanner,
|
||||
this,
|
||||
true,
|
||||
[],
|
||||
span: result.span ?? args.scanner.lastSpan ?? args.scanner.emptySpan,
|
||||
value: result.value,
|
||||
);
|
||||
}
|
||||
|
||||
result = result.change(successful: false);
|
||||
|
||||
if (errorMessage != null) {
|
||||
result = result.addErrors([
|
||||
SyntaxError(
|
||||
severity,
|
||||
errorMessage,
|
||||
result.span,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('negate (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
57
packages/combinator/lib/src/combinator/opt.dart
Normal file
57
packages/combinator/lib/src/combinator/opt.dart
Normal file
|
@ -0,0 +1,57 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
class _Opt<T> extends Parser<T> {
|
||||
final Parser<T> parser;
|
||||
final bool backtrack;
|
||||
|
||||
_Opt(this.parser, this.backtrack);
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
var replay = args.scanner.position;
|
||||
var result = parser._parse(args.increaseDepth());
|
||||
|
||||
if (!result.successful) args.scanner.position = replay;
|
||||
|
||||
return result.change(parser: this, successful: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('optional (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
||||
|
||||
class _ListOpt<T> extends ListParser<T> {
|
||||
final ListParser<T> parser;
|
||||
final bool backtrack;
|
||||
|
||||
_ListOpt(this.parser, this.backtrack);
|
||||
|
||||
@override
|
||||
ParseResult<List<T>> __parse(ParseArgs args) {
|
||||
var replay = args.scanner.position;
|
||||
ParseResult<List<T>> result = parser._parse(args.increaseDepth());
|
||||
|
||||
if (!result.successful) args.scanner.position = replay;
|
||||
|
||||
return result.change(parser: this, successful: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('optional (')
|
||||
..indent();
|
||||
parser.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
142
packages/combinator/lib/src/combinator/recursion.dart
Normal file
142
packages/combinator/lib/src/combinator/recursion.dart
Normal file
|
@ -0,0 +1,142 @@
|
|||
part of lex.src.combinator;
|
||||
|
||||
/*
|
||||
/// Handles left recursion in a grammar using the Pratt algorithm.
|
||||
class Recursion<T> {
|
||||
Iterable<Parser<T>> prefix;
|
||||
Map<Parser, T Function(T, T, ParseResult<T>)> infix;
|
||||
Map<Parser, T Function(T, T, ParseResult<T>)> postfix;
|
||||
|
||||
Recursion({this.prefix, this.infix, this.postfix}) {
|
||||
prefix ??= [];
|
||||
infix ??= {};
|
||||
postfix ??= {};
|
||||
}
|
||||
|
||||
Parser<T> precedence(int p) => _Precedence(this, p);
|
||||
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('recursion (')
|
||||
..indent()
|
||||
..writeln('prefix(${prefix.length}')
|
||||
..writeln('infix(${infix.length}')
|
||||
..writeln('postfix(${postfix.length}')
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
||||
|
||||
class _Precedence<T> extends Parser<T> {
|
||||
final Recursion r;
|
||||
final int precedence;
|
||||
|
||||
_Precedence(this.r, this.precedence);
|
||||
|
||||
@override
|
||||
ParseResult<T> __parse(ParseArgs args) {
|
||||
int replay = args.scanner.position;
|
||||
var errors = <SyntaxError>[];
|
||||
var start = args.scanner.state;
|
||||
var reversedKeys = r.infix.keys.toList().reversed;
|
||||
|
||||
for (var pre in r.prefix) {
|
||||
var result = pre._parse(args.increaseDepth()), originalResult = result;
|
||||
|
||||
if (!result.successful) {
|
||||
if (pre is _Alt) errors.addAll(result.errors);
|
||||
args.scanner.position = replay;
|
||||
} else {
|
||||
var left = result.value;
|
||||
replay = args.scanner.position;
|
||||
//print('${result.span.text}:\n' + scanner.emptySpan.highlight());
|
||||
|
||||
while (true) {
|
||||
bool matched = false;
|
||||
|
||||
//for (int i = 0; i < r.infix.length; i++) {
|
||||
for (int i = r.infix.length - 1; i >= 0; i--) {
|
||||
//var fix = r.infix.keys.elementAt(r.infix.length - i - 1);
|
||||
var fix = reversedKeys.elementAt(i);
|
||||
|
||||
if (i < precedence) continue;
|
||||
|
||||
var result = fix._parse(args.increaseDepth());
|
||||
|
||||
if (!result.successful) {
|
||||
if (fix is _Alt) errors.addAll(result.errors);
|
||||
// If this is the last alternative and it failed, don't continue looping.
|
||||
//if (true || i + 1 < r.infix.length)
|
||||
args.scanner.position = replay;
|
||||
} else {
|
||||
//print('FOUND $fix when left was $left');
|
||||
//print('$i vs $precedence\n${originalResult.span.highlight()}');
|
||||
result = r.precedence(i)._parse(args.increaseDepth());
|
||||
|
||||
if (!result.successful) {
|
||||
} else {
|
||||
matched = false;
|
||||
var old = left;
|
||||
left = r.infix[fix](left, result.value, result);
|
||||
print(
|
||||
'$old $fix ${result.value} = $left\n${result.span.highlight()}');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched) break;
|
||||
}
|
||||
|
||||
replay = args.scanner.position;
|
||||
//print('f ${result.span.text}');
|
||||
|
||||
for (var post in r.postfix.keys) {
|
||||
var result = pre._parse(args.increaseDepth());
|
||||
|
||||
if (!result.successful) {
|
||||
if (post is _Alt) errors.addAll(result.errors);
|
||||
args.scanner.position = replay;
|
||||
} else {
|
||||
left = r.infix[post](left, originalResult.value, result);
|
||||
}
|
||||
}
|
||||
|
||||
if (!args.scanner.isDone) {
|
||||
// If we're not done scanning, then we need some sort of guard to ensure the
|
||||
// that this exact parser does not run again in the exact position.
|
||||
}
|
||||
return ParseResult(
|
||||
args.trampoline,
|
||||
args.scanner,
|
||||
this,
|
||||
true,
|
||||
errors,
|
||||
value: left,
|
||||
span: args.scanner.spanFrom(start),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return ParseResult(
|
||||
args.trampoline,
|
||||
args.scanner,
|
||||
this,
|
||||
false,
|
||||
errors,
|
||||
span: args.scanner.spanFrom(start),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void stringify(CodeBuffer buffer) {
|
||||
buffer
|
||||
..writeln('precedence($precedence) (')
|
||||
..indent();
|
||||
r.stringify(buffer);
|
||||
buffer
|
||||
..outdent()
|
||||
..writeln(')');
|
||||
}
|
||||
}
|
||||
*/
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue