Merge branch 'angel3' into master

This commit is contained in:
thomashii 2021-05-16 16:20:23 +08:00
commit 57b45decd1
693 changed files with 19315 additions and 7471 deletions

40
.gitignore vendored
View file

@ -38,37 +38,29 @@ pubspec.lock
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff: # User-specific stuff:
.idea/workspace.xml
.idea/tasks.xml
.idea/dictionaries
.idea/vcs.xml
.idea/jsLibraryMappings.xml
# Sensitive or high-churn files: ## VsCode
.idea/dataSources.ids .vscode/
.idea/dataSources.xml #.vscode/*
.idea/dataSources.local.xml #!.vscode/settings.json
.idea/sqlDataSources.xml #!.vscode/tasks.json
.idea/dynamic.xml #!.vscode/launch.json
.idea/uiDesigner.xml #!.vscode/extensions.json
# IntelliJ
.idea/
/out/
.idea_modules/
# Gradle: # Gradle:
.idea/gradle.xml .idea/gradle.xml
.idea/libraries .idea/libraries
# Mongo Explorer plugin:
.idea/mongoSettings.xml
## File-based project format: ## File-based project format:
*.iws *.iws
## Plugin-specific files: ## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin # JIRA plugin
atlassian-ide-plugin.xml atlassian-ide-plugin.xml
@ -79,13 +71,7 @@ crashlytics.properties
crashlytics-build.properties crashlytics-build.properties
fabric.properties fabric.properties
### VSCode template # Others
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
logs/ logs/
*.pem *.pem
.DS_Store .DS_Store

12
AUTHORS.md Normal file
View 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.

View file

@ -1,41 +1,78 @@
# 4.0.0 (NNBD) # 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. * Changed Dart SDK requirements for all packages to ">=2.12.0 <3.0.0" to support NNBD.
* Updated pretty_logging to 2.0.0 * Migrated pretty_logging to 3.0.0 (0/0 tests passed)
* Updated angel_http_exception to 2.0.0 * Migrated angel_http_exception to 3.0.0 (0/0 tests passed)
* Updated angel_cli to 3.0.0. (Rename not working) * 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) # 3.0.0 (Non NNBD)
* Changed Dart SDK requirements for all packages to ">=2.10.0 <3.0.0" * Changed Dart SDK requirements for all packages to ">=2.10.0 <3.0.0"
* Updated pretty_logging to 2.0.0 * Updated pretty_logging to 2.0.0 (0/0 tests passed)
* Updated angel_http_exception to 2.0.0 * Updated angel_http_exception to 2.0.0 (0/0 tests passed)
* Updated angel_cli to 3.0.0. (Rename not working) * Updated angel_cli to 3.0.0. (Rename not working)
* Updated angel_route to 4.0.0 * Updated angel_route to 4.0.0 (35/35 tests passed)
* Updated angel_model to 2.0.0 * Updated angel_model to 2.0.0 (0/0 tests passed)
* Updated angel_container to 2.0.0 * Updated angel_container to 2.0.0 (55/55 tests passed)
* Updated angel_framework to 3.0.0 * Updated angel_framework to 3.0.0 (151/151 tests passed)
* Updated angel_auth to 3.0.0 * Updated angel_auth to 3.0.0 (28/32 tests passed)
* Updated angel_configuration to 3.0.0 * Updated angel_configuration to 3.0.0 (6/8 tests passed)
* Updated jael to 3.0.0 * Updated angel_validate to 3.0.0 (7/7 tests passed)
* Updated jael_preprocessor to 3.0.0 * Added and updated json_god to 3.0.0 (7/7 tests passed)
* Updated validate to 3.0.0 * Updated angel_client to 3.0.0 (10/13 tests passed)
* Added and updated json_god to 3.0.0
* Updated angel_client to 3.0.0
* Updated angel_websocket to 3.0.0 (3/3 tests passed) * Updated angel_websocket to 3.0.0 (3/3 tests passed)
* Updated test to 3.0.0 * Updated jael to 3.0.0 (20/20 tests passed)
* Updated angel_jael to 3.0.0 (Issue with 2 dependencies) * Updated jael_preprocessor to 3.0.0 (5/5 tests passed)
* Added pub_sub and updated to 3.0.0 * Updated test to 3.0.0 (1/1 tests passed)
* Updated production to 2.0.0 * Updated angel_jael to 3.0.0 (1/1 tests passed, Issue with 2 dependencies)
* Updated hot to 3.0.0 * Added pub_sub and updated to 3.0.0 (16/16 tests passed)
* Updated static to 3.0.0 * Updated production to 2.0.0 (0/0 tests passed)
* Update basic-sdk-2.12.x boilerplate * Updated hot to 3.0.0 (0/0 tests passed)
* Updated angel_serialize to 3.0.0 * Updated static to 3.0.0 (12/12 tests passed)
* Updated angel_serialize_generator to 3.0.0 * Update basic-sdk-2.12.x boilerplate (1/1 tests passed)
* Updated angel_orm to 3.0.0 * Updated angel_serialize to 3.0.0 (0/0 tests passed)
* Updated angel_migration to 3.0.0 * Updated angel_serialize_generator to 3.0.0 (33/33 tests passed)
* Updated angel_orm_generator to 3.0.0 (use a fork of postgres) * Updated angel_orm to 3.0.0 (0/0 tests passed)
* Updated angel_migration_runner to 3.0.0 * Updated angel_migration to 3.0.0 (0/0 tests passed)
* Updated angel_orm_test to 1.0.0 * Updated angel_orm_generator to 3.0.0 (0/0 tests passed, use a fork of postgres)
* Updated angel_orm_postgres to 2.0.0 * 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 * Update orm-sdk-2.12.x boilerplate
* Updated angel_auth_oauth2 to 3.0.0 * Updated angel_auth_oauth2 to 3.0.0
* Updated angel_auth_cache to 3.0.0 * Updated angel_auth_cache to 3.0.0

View file

@ -1,6 +1,6 @@
The MIT License (MIT) 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -1,62 +1,51 @@
[![The Angel Framework](https://angel-dart.github.io/assets/images/logo.png)](https://angel-dart.dev) [![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) [![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) [![version](https://img.shields.io/badge/pub-v4.0.1-brightgreen)](https://pub.dartlang.org/packages/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)
**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 ## About
Angel is a full-stack Web framework in Dart. It aims to Angel3 is a port of the original Angel framework to support NNBD in Dart SDK 2.12.x and above.
streamline development by providing many common features 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
out-of-the-box in a consistent manner. 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: The availabe features in Angel3 are:
* GraphQL Support
* PostgreSQL ORM
* Dependency Injection
* Static File Handling * Static File Handling
* Basic Authentication
* PostgreSQL ORM
* And much more... * And much more...
See all the packages in the `packages/` directory. See all the packages in the `packages/` directory.
## IMPORTANT NOTES ## 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 Branch: master
- Same as sdk-2.12.x branch - Stable version of `angel3` branch
Branch: sdk-2.12.x Branch: angel3 (Active development)
- Required Dart SDK: ">=2.10.0 <3.0.0" - Dart version : 2.12.x and above. Use sdk: ">=2.12.0 <3.0.0"
- NNBD Support: No - Publish : Yes. See all packages with `angel3_` prefix on [pub.dev](https://pub.dev/publishers/dukefirehawk.com/packages).
- Status: Beta release - NNDB Support : Yes
- 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. - 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 Branch: sdk-2.12.x-nnbd (Active development)
- Required Dart SDK: ">=2.12.0 <3.0.0" - Dart version : 2.12.x and above. Use sdk: ">=2.12.0 <3.0.0"
- NNBD Support: Yes - Publish : No (Internal use only)
- Status: Alpha release - NNDB Support : Yes
- 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". - Status : Beta
- Notes : Basic and ORM templates are working with key packages migration. Not all packages are fully tested.
Branch: sdk-2.10.x For more details, checkout [Project Status](https://github.com/dukefirehawk/angel/wiki/Project-Status)
- 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.
### 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
@ -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. 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 ## Examples and Documentation
Visit the [documentation](https://docs.angel-dart.dev/v/2.x) Visit the [documentation](https://docs.angel-dart.dev/v/2.x)
for dozens of guides and resources, including video tutorials, for dozens of guides and resources, including video tutorials,

View file

@ -1,5 +1,5 @@
# Todo ### angel_framework
* Migrate http_server to shelf
### Container/angel_container_generator ### Container/angel_container_generator
* test/reflector_test.reflectab.dart - Changed ImplicitGetterMirrorImpl() from 5 to 3 parameters (revisit later) * test/reflector_test.reflectab.dart - Changed ImplicitGetterMirrorImpl() from 5 to 3 parameters (revisit later)

71
packages/.gitignore vendored Normal file
View 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
View 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
View 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.

View file

@ -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 ### Dart template
# See https://www.dartlang.org/tools/private-files.html # See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub # 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 .project
.pub/ .buildlog
build/
**/packages/ **/packages/
# Files created by dart2js # 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 # rules if you intend to use dart2js directly
@ -22,36 +39,17 @@ build/
*.info.json *.info.json
# Directory created by dartdoc # 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) # (Library packages only! Remove pattern if developing an application package)
pubspec.lock
### JetBrains template ### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff: # User-specific stuff:
.idea/workspace.xml
.idea/tasks.xml
.idea/dictionaries
.idea/vcs.xml
.idea/jsLibraryMappings.xml
# Sensitive or high-churn files: ## VsCode
.idea/dataSources.ids .vscode/
.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
## File-based project format: ## File-based project format:
*.iws *.iws
@ -59,9 +57,8 @@ pubspec.lock
## Plugin-specific files: ## Plugin-specific files:
# IntelliJ # IntelliJ
.idea/
/out/ /out/
# mpeltonen/sbt-idea plugin
.idea_modules/ .idea_modules/
# JIRA plugin # JIRA plugin
@ -72,5 +69,3 @@ com_crashlytics_export_strings.xml
crashlytics.properties crashlytics.properties
crashlytics-build.properties crashlytics-build.properties
fabric.properties fabric.properties
.dart_tool

12
packages/auth/AUTHORS.md Normal file
View 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.

View file

@ -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 # 2.1.5+1
* Fix error in popup page. * Fix error in popup page.

View file

@ -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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -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) [![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/angel3/packages/auth/LICENSE)
[![build status](https://travis-ci.org/angel-dart/auth.svg?branch=master)](https://travis-ci.org/angel-dart/auth)
A complete authentication plugin for Angel. Inspired by Passport. 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`. 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 ```dart
app.authenticateViaPopup('/auth/google').listen((jwt) { app.authenticateViaPopup('/auth/google').listen((jwt) {

View file

@ -1,13 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'package:angel_auth/angel_auth.dart'; import 'package:angel3_auth/angel3_auth.dart';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel3_framework/angel3_framework.dart';
import 'package:angel_framework/http.dart'; import 'package:angel3_framework/http.dart';
main() async { void main() async {
var app = Angel(); 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); auth.deserializer = (id) => fetchAUserByIdSomehow(id);
@ -30,7 +30,7 @@ main() async {
} }
class User { class User {
String id, username, password; String? id, username, password;
} }
Future<User> fetchAUserByIdSomehow(id) async { Future<User> fetchAUserByIdSomehow(id) async {

View file

@ -1,4 +1,4 @@
library angel_auth; library angel3_auth;
export 'src/middleware/require_auth.dart'; export 'src/middleware/require_auth.dart';
export 'src/strategies/strategies.dart'; export 'src/strategies/strategies.dart';

View file

@ -1,4 +1,4 @@
/// Stand-alone JWT library. /// Stand-alone JWT library.
library angel_auth.auth_token; library angel3_auth.auth_token;
export 'src/auth_token.dart'; export 'src/auth_token.dart';

View file

@ -1,5 +1,5 @@
import 'dart:collection'; import 'dart:collection';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel3_framework/angel3_framework.dart';
import 'dart:convert'; import 'dart:convert';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
@ -26,10 +26,10 @@ String decodeBase64(String str) {
class AuthToken { class AuthToken {
final SplayTreeMap<String, String> _header = final SplayTreeMap<String, String> _header =
SplayTreeMap.from({"alg": "HS256", "typ": "JWT"}); SplayTreeMap.from({'alg': 'HS256', 'typ': 'JWT'});
String ipAddress; String? ipAddress;
DateTime issuedAt; late DateTime issuedAt;
num lifeSpan; num lifeSpan;
var userId; var userId;
Map<String, dynamic> payload = {}; Map<String, dynamic> payload = {};
@ -38,12 +38,20 @@ class AuthToken {
{this.ipAddress, {this.ipAddress,
this.lifeSpan = -1, this.lifeSpan = -1,
this.userId, this.userId,
DateTime issuedAt, DateTime? issuedAt,
Map payload = const {}}) { Map payload = const {}}) {
this.issuedAt = issuedAt ?? DateTime.now(); this.issuedAt = issuedAt ?? DateTime.now();
this.payload.addAll( this.payload.addAll(payload.keys
payload?.keys?.fold({}, (out, k) => out..[k.toString()] = payload[k]) ?? .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) => factory AuthToken.fromJson(String jsons) =>
@ -51,37 +59,40 @@ class AuthToken {
factory AuthToken.fromMap(Map data) { factory AuthToken.fromMap(Map data) {
return AuthToken( return AuthToken(
ipAddress: data["aud"].toString(), ipAddress: data['aud'].toString(),
lifeSpan: data["exp"] as num, lifeSpan: data['exp'] as num,
issuedAt: DateTime.parse(data["iat"].toString()), issuedAt: DateTime.parse(data['iat'].toString()),
userId: data["sub"], userId: data['sub'],
payload: data["pld"] as Map ?? {}); payload: data['pld'] as Map);
} }
factory AuthToken.parse(String jwt) { factory AuthToken.parse(String jwt) {
var split = jwt.split("."); var split = jwt.split('.');
if (split.length != 3) if (split.length != 3) {
throw AngelHttpException.notAuthenticated(message: "Invalid JWT."); throw AngelHttpException.notAuthenticated(message: 'Invalid JWT.');
}
var payloadString = decodeBase64(split[1]); var payloadString = decodeBase64(split[1]);
return AuthToken.fromMap(json.decode(payloadString) as Map); return AuthToken.fromMap(json.decode(payloadString) as Map);
} }
factory AuthToken.validate(String jwt, Hmac hmac) { factory AuthToken.validate(String jwt, Hmac hmac) {
var split = jwt.split("."); var split = jwt.split('.');
if (split.length != 3) if (split.length != 3) {
throw AngelHttpException.notAuthenticated(message: "Invalid JWT."); throw AngelHttpException.notAuthenticated(message: 'Invalid JWT.');
}
// var headerString = decodeBase64(split[0]); // var headerString = decodeBase64(split[0]);
var payloadString = decodeBase64(split[1]); 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); var signature = base64Url.encode(hmac.convert(data.codeUnits).bytes);
if (signature != split[2]) if (signature != split[2]) {
throw AngelHttpException.notAuthenticated( 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); return AuthToken.fromMap(json.decode(payloadString) as Map);
} }
@ -89,9 +100,9 @@ class AuthToken {
String serialize(Hmac hmac) { String serialize(Hmac hmac) {
var headerString = base64Url.encode(json.encode(_header).codeUnits); var headerString = base64Url.encode(json.encode(_header).codeUnits);
var payloadString = base64Url.encode(json.encode(toJson()).codeUnits); var payloadString = base64Url.encode(json.encode(toJson()).codeUnits);
var data = headerString + "." + payloadString; var data = headerString + '.' + payloadString;
var signature = hmac.convert(data.codeUnits).bytes; var signature = hmac.convert(data.codeUnits).bytes;
return data + "." + base64Url.encode(signature); return data + '.' + base64Url.encode(signature);
} }
Map toJson() { Map toJson() {
@ -114,11 +125,12 @@ SplayTreeMap _splayify(Map map) {
return SplayTreeMap.from(data); return SplayTreeMap.from(data);
} }
_splay(value) { dynamic _splay(value) {
if (value is Iterable) { if (value is Iterable) {
return value.map(_splay).toList(); return value.map(_splay).toList();
} else if (value is Map) } else if (value is Map) {
return _splayify(value); return _splayify(value);
else } else {
return value; return value;
}
} }

View file

@ -1,7 +1,6 @@
import 'package:charcode/ascii.dart'; import 'package:charcode/ascii.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:meta/meta.dart'; import 'package:quiver/core.dart';
import 'package:quiver_hashcode/hashcode.dart';
/// A common class containing parsing and validation logic for third-party authentication configuration. /// A common class containing parsing and validation logic for third-party authentication configuration.
class ExternalAuthOptions { class ExternalAuthOptions {
@ -18,18 +17,12 @@ class ExternalAuthOptions {
final Set<String> scopes; final Set<String> scopes;
ExternalAuthOptions._( ExternalAuthOptions._(
this.clientId, this.clientSecret, this.redirectUri, this.scopes) { this.clientId, this.clientSecret, this.redirectUri, this.scopes);
if (clientId == null) {
throw ArgumentError.notNull('clientId');
} else if (clientSecret == null) {
throw ArgumentError.notNull('clientSecret');
}
}
factory ExternalAuthOptions( factory ExternalAuthOptions(
{@required String clientId, {required String clientId,
@required String clientSecret, required String clientSecret,
@required redirectUri, required redirectUri,
Iterable<String> scopes = const []}) { Iterable<String> scopes = const []}) {
if (redirectUri is String) { if (redirectUri is String) {
return ExternalAuthOptions._( return ExternalAuthOptions._(
@ -50,9 +43,15 @@ class ExternalAuthOptions {
/// * `client_secret` /// * `client_secret`
/// * `redirect_uri` /// * `redirect_uri`
factory ExternalAuthOptions.fromMap(Map map) { 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( return ExternalAuthOptions(
clientId: map['client_id'] as String, clientId: clientId as String,
clientSecret: map['client_secret'] as String, clientSecret: clientSecret as String,
redirectUri: map['redirect_uri'], redirectUri: map['redirect_uri'],
scopes: map['scopes'] is Iterable scopes: map['scopes'] is Iterable
? ((map['scopes'] as Iterable).map((x) => x.toString())) ? ((map['scopes'] as Iterable).map((x) => x.toString()))
@ -73,15 +72,15 @@ class ExternalAuthOptions {
/// Creates a copy of this object, with the specified changes. /// Creates a copy of this object, with the specified changes.
ExternalAuthOptions copyWith( ExternalAuthOptions copyWith(
{String clientId, {String? clientId,
String clientSecret, String? clientSecret,
redirectUri, redirectUri,
Iterable<String> scopes}) { Iterable<String> scopes = const []}) {
return ExternalAuthOptions( return ExternalAuthOptions(
clientId: clientId ?? this.clientId, clientId: clientId ?? this.clientId,
clientSecret: clientSecret ?? this.clientSecret, clientSecret: clientSecret ?? this.clientSecret,
redirectUri: redirectUri ?? this.redirectUri, 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 /// If no [asteriskCount] is given, then the number of asterisks will equal the length of
/// the actual [clientSecret]. /// the actual [clientSecret].
@override @override
String toString({bool obscureSecret = true, int asteriskCount}) { String toString({bool obscureSecret = true, int? asteriskCount}) {
String secret; String? secret;
if (!obscureSecret) { if (!obscureSecret) {
secret = clientSecret; secret = clientSecret;

View file

@ -1,16 +1,19 @@
import 'dart:async'; 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. /// Forces Basic authentication over the requested resource, with the given [realm] name, if no JWT is present.
/// ///
/// [realm] defaults to `'angel_auth'`. /// [realm] defaults to `'angel_auth'`.
RequestHandler forceBasicAuth<User>({String realm}) { RequestHandler forceBasicAuth<User>({String? realm}) {
return (RequestContext req, ResponseContext res) async { return (RequestContext req, ResponseContext res) async {
if (req.container.has<User>()) if (req.container != null) {
return true; var reqContainer = req.container!;
else if (req.container.has<Future<User>>()) { if (reqContainer.has<User>()) {
await req.container.makeAsync<User>(); return true;
return true; } else if (reqContainer.has<Future<User>>()) {
await reqContainer.makeAsync<User>();
return true;
}
} }
res.headers['www-authenticate'] = 'Basic realm="${realm ?? 'angel_auth'}"'; res.headers['www-authenticate'] = 'Basic realm="${realm ?? 'angel_auth'}"';
@ -26,16 +29,23 @@ RequestHandler requireAuthentication<User>() {
if (throwError) { if (throwError) {
res.statusCode = 403; res.statusCode = 403;
throw AngelHttpException.forbidden(); throw AngelHttpException.forbidden();
} else } else {
return false; return false;
}
} }
if (req.container.has<User>() || req.method == 'OPTIONS') if (req.container != null) {
return true; var reqContainer = req.container!;
else if (req.container.has<Future<User>>()) { if (reqContainer.has<User>() || req.method == 'OPTIONS') {
await req.container.makeAsync<User>(); return true;
return true; } else if (reqContainer.has<Future<User>>()) {
} else await reqContainer.makeAsync<User>();
return true;
} else {
return _reject(res);
}
} else {
return _reject(res); return _reject(res);
}
}; };
} }

View file

@ -1,19 +1,19 @@
import 'dart:async'; import 'dart:async';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel3_framework/angel3_framework.dart';
import 'auth_token.dart'; import 'auth_token.dart';
typedef FutureOr AngelAuthCallback( typedef AngelAuthCallback = FutureOr Function(
RequestContext req, ResponseContext res, String token); RequestContext req, ResponseContext res, String token);
typedef FutureOr AngelAuthTokenCallback<User>( typedef AngelAuthTokenCallback<User> = FutureOr Function(
RequestContext req, ResponseContext res, AuthToken token, User user); RequestContext req, ResponseContext res, AuthToken token, User user);
class AngelAuthOptions<User> { class AngelAuthOptions<User> {
AngelAuthCallback callback; AngelAuthCallback? callback;
AngelAuthTokenCallback<User> tokenCallback; AngelAuthTokenCallback<User>? tokenCallback;
String successRedirect; String? successRedirect;
String failureRedirect; String? failureRedirect;
/// If `false` (default: `true`), then successful authentication will return `true` and allow the /// If `false` (default: `true`), then successful authentication will return `true` and allow the
/// execution of subsequent handlers, just like any other middleware. /// execution of subsequent handlers, just like any other middleware.
@ -26,5 +26,5 @@ class AngelAuthOptions<User> {
this.tokenCallback, this.tokenCallback,
this.canRespondWithJson = true, this.canRespondWithJson = true,
this.successRedirect, this.successRedirect,
String this.failureRedirect}); this.failureRedirect});
} }

View file

@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math' as Math; import 'dart:math';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel3_framework/angel3_framework.dart';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'auth_token.dart'; import 'auth_token.dart';
import 'options.dart'; import 'options.dart';
@ -9,12 +9,12 @@ import 'strategy.dart';
/// Handles authentication within an Angel application. /// Handles authentication within an Angel application.
class AngelAuth<User> { class AngelAuth<User> {
Hmac _hs256; late Hmac _hs256;
int _jwtLifeSpan; late int _jwtLifeSpan;
final StreamController<User> _onLogin = StreamController<User>(), final StreamController<User> _onLogin = StreamController<User>(),
_onLogout = StreamController<User>(); _onLogout = StreamController<User>();
Math.Random _random = Math.Random.secure(); final Random _random = Random.secure();
final RegExp _rgxBearer = RegExp(r"^Bearer"); final RegExp _rgxBearer = RegExp(r'^Bearer');
/// If `true` (default), then JWT's will be stored and retrieved from a `token` cookie. /// If `true` (default), then JWT's will be stored and retrieved from a `token` cookie.
final bool allowCookie; final bool allowCookie;
@ -29,7 +29,7 @@ class AngelAuth<User> {
/// A domain to restrict emitted cookies to. /// A domain to restrict emitted cookies to.
/// ///
/// Only applies if [allowCookie] is `true`. /// Only applies if [allowCookie] is `true`.
final String cookieDomain; final String? cookieDomain;
/// A path to restrict emitted cookies to. /// A path to restrict emitted cookies to.
/// ///
@ -48,10 +48,10 @@ class AngelAuth<User> {
Map<String, AuthStrategy<User>> strategies = {}; Map<String, AuthStrategy<User>> strategies = {};
/// Serializes a user into a unique identifier associated only with one identity. /// 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. /// 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. /// Fires the result of [deserializer] whenever a user signs in to the application.
Stream<User> get onLogin => _onLogin.stream; Stream<User> get onLogin => _onLogin.stream;
@ -65,25 +65,27 @@ class AngelAuth<User> {
String _randomString( String _randomString(
{int length = 32, {int length = 32,
String validChars = String validChars =
"ABCDEFHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"}) { 'ABCDEFHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_'}) {
var chars = <int>[]; 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); return String.fromCharCodes(chars);
} }
/// `jwtLifeSpan` - should be in *milliseconds*. /// `jwtLifeSpan` - should be in *milliseconds*.
AngelAuth( AngelAuth(
{String jwtKey, {String? jwtKey,
this.serializer, this.serializer,
this.deserializer, this.deserializer,
num jwtLifeSpan, num? jwtLifeSpan,
this.allowCookie = true, this.allowCookie = true,
this.allowTokenInQuery = true, this.allowTokenInQuery = true,
this.enforceIp = true, this.enforceIp = true,
this.cookieDomain, this.cookieDomain,
this.cookiePath = '/', this.cookiePath = '/',
this.secureCookies = true, this.secureCookies = true,
this.reviveTokenEndpoint = "/auth/token"}) this.reviveTokenEndpoint = '/auth/token'})
: super() { : super() {
_hs256 = Hmac(sha256, (jwtKey ?? _randomString()).codeUnits); _hs256 = Hmac(sha256, (jwtKey ?? _randomString()).codeUnits);
_jwtLifeSpan = jwtLifeSpan?.toInt() ?? -1; _jwtLifeSpan = jwtLifeSpan?.toInt() ?? -1;
@ -92,22 +94,25 @@ class AngelAuth<User> {
/// Configures an Angel server to decode and validate JSON Web tokens on demand, /// Configures an Angel server to decode and validate JSON Web tokens on demand,
/// whenever an instance of [User] is injected. /// whenever an instance of [User] is injected.
Future<void> configureServer(Angel app) async { Future<void> configureServer(Angel app) async {
if (serializer == null) if (serializer == null) {
throw StateError( throw StateError(
'An `AngelAuth` plug-in was called without its `serializer` being set. All authentication will fail.'); 'An `AngelAuth` plug-in was called without its `serializer` being set. All authentication will fail.');
if (deserializer == null) }
if (deserializer == null) {
throw StateError( throw StateError(
'An `AngelAuth` plug-in was called without its `deserializer` being set. All authentication will fail.'); 'An `AngelAuth` plug-in was called without its `deserializer` being set. All authentication will fail.');
}
app.container.registerSingleton(this); app.container!.registerSingleton(this);
if (runtimeType != AngelAuth) if (runtimeType != AngelAuth) {
app.container.registerSingleton(this, as: AngelAuth); app.container!.registerSingleton(this, as: AngelAuth);
}
if (!app.container.has<_AuthResult<User>>()) { if (!app.container!.has<_AuthResult<User>>()) {
app.container app.container!
.registerLazySingleton<Future<_AuthResult<User>>>((container) async { .registerLazySingleton<Future<_AuthResult<User>>>((container) async {
var req = container.make<RequestContext>(); var req = container.make<RequestContext>()!;
var res = container.make<ResponseContext>(); var res = container.make<ResponseContext>()!;
var result = await _decodeJwt(req, res); var result = await _decodeJwt(req, res);
if (result != null) { if (result != null) {
return result; return result;
@ -116,20 +121,19 @@ class AngelAuth<User> {
} }
}); });
app.container.registerLazySingleton<Future<User>>((container) async { app.container!.registerLazySingleton<Future<User>>((container) async {
var result = await container.makeAsync<_AuthResult<User>>(); var result = await container.makeAsync<_AuthResult<User>>()!;
return result.user; return result.user;
}); });
app.container.registerLazySingleton<Future<AuthToken>>((container) async { app.container!
var result = await container.makeAsync<_AuthResult<User>>(); .registerLazySingleton<Future<AuthToken>>((container) async {
var result = await container.makeAsync<_AuthResult<User>>()!;
return result.token; return result.token;
}); });
} }
if (reviveTokenEndpoint != null) { app.post(reviveTokenEndpoint, reviveJwt);
app.post(reviveTokenEndpoint, reviveJwt);
}
app.shutdownHooks.add((_) { app.shutdownHooks.add((_) {
_onLogin.close(); _onLogin.close();
@ -137,17 +141,17 @@ class AngelAuth<User> {
} }
void _apply( void _apply(
RequestContext req, ResponseContext res, AuthToken token, User user) { RequestContext req, ResponseContext? res, AuthToken token, User user) {
if (!req.container.has<User>()) { if (!req.container!.has<User>()) {
req.container.registerSingleton<User>(user); req.container!.registerSingleton<User>(user);
} }
if (!req.container.has<AuthToken>()) { if (!req.container!.has<AuthToken>()) {
req.container.registerSingleton<AuthToken>(token); req.container!.registerSingleton<AuthToken>(token);
} }
if (allowCookie == true) { if (allowCookie) {
_addProtectedCookie(res, 'token', token.serialize(_hs256)); _addProtectedCookie(res!, 'token', token.serialize(_hs256));
} }
} }
@ -174,7 +178,7 @@ class AngelAuth<User> {
/// ``` /// ```
@deprecated @deprecated
Future decodeJwt(RequestContext req, ResponseContext res) async { 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); return await reviveJwt(req, res);
} else { } else {
await _decodeJwt(req, res); await _decodeJwt(req, res);
@ -182,28 +186,30 @@ class AngelAuth<User> {
} }
} }
Future<_AuthResult<User>> _decodeJwt( Future<_AuthResult<User>?> _decodeJwt(
RequestContext req, ResponseContext res) async { RequestContext req, ResponseContext res) async {
String jwt = getJwt(req); var jwt = getJwt(req);
if (jwt != null) { if (jwt != null) {
var token = AuthToken.validate(jwt, _hs256); var token = AuthToken.validate(jwt, _hs256);
if (enforceIp) { if (enforceIp) {
if (req.ip != null && req.ip != token.ipAddress) if (req.ip != token.ipAddress) {
throw AngelHttpException.forbidden( 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) { if (token.lifeSpan > -1) {
var expiry = var expiry =
token.issuedAt.add(Duration(milliseconds: token.lifeSpan.toInt())); token.issuedAt.add(Duration(milliseconds: token.lifeSpan.toInt()));
if (!expiry.isAfter(DateTime.now())) if (!expiry.isAfter(DateTime.now())) {
throw AngelHttpException.forbidden(message: "Expired JWT."); 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); _apply(req, res, token, user);
return _AuthResult(user, token); return _AuthResult(user, token);
} }
@ -212,19 +218,20 @@ class AngelAuth<User> {
} }
/// Retrieves a JWT from a request, if any was sent at all. /// Retrieves a JWT from a request, if any was sent at all.
String getJwt(RequestContext req) { String? getJwt(RequestContext req) {
if (req.headers.value("Authorization") != null) { if (req.headers?.value('Authorization') != null) {
final authHeader = req.headers.value("Authorization"); final authHeader = req.headers!.value('Authorization')!;
// Allow Basic auth to fall through // Allow Basic auth to fall through
if (_rgxBearer.hasMatch(authHeader)) if (_rgxBearer.hasMatch(authHeader)) {
return authHeader.replaceAll(_rgxBearer, "").trim(); return authHeader.replaceAll(_rgxBearer, '').trim();
}
} else if (allowCookie && } else if (allowCookie &&
req.cookies.any((cookie) => cookie.name == "token")) { req.cookies.any((cookie) => cookie.name == 'token')) {
return req.cookies.firstWhere((cookie) => cookie.name == "token").value; return req.cookies.firstWhere((cookie) => cookie.name == 'token').value;
} else if (allowTokenInQuery && } else if (allowTokenInQuery &&
req.uri.queryParameters['token'] is String) { req.uri?.queryParameters['token'] is String) {
return req.uri.queryParameters['token']?.toString(); return req.uri!.queryParameters['token']?.toString();
} }
return null; return null;
@ -243,10 +250,10 @@ class AngelAuth<User> {
cookie.secure = true; cookie.secure = true;
} }
if (_jwtLifeSpan > 0) { var lifeSpan = _jwtLifeSpan;
cookie.maxAge ??= _jwtLifeSpan < 0 ? -1 : _jwtLifeSpan ~/ 1000; if (lifeSpan > 0) {
cookie.expires ??= cookie.maxAge ??= lifeSpan < 0 ? -1 : lifeSpan ~/ 1000;
DateTime.now().add(Duration(milliseconds: _jwtLifeSpan)); cookie.expires ??= DateTime.now().add(Duration(milliseconds: lifeSpan));
} }
cookie.domain ??= cookieDomain; cookie.domain ??= cookieDomain;
@ -265,13 +272,14 @@ class AngelAuth<User> {
jwt = body['token']?.toString(); jwt = body['token']?.toString();
} }
if (jwt == null) { if (jwt == null) {
throw AngelHttpException.forbidden(message: "No JWT provided"); throw AngelHttpException.forbidden(message: 'No JWT provided');
} else { } else {
var token = AuthToken.validate(jwt, _hs256); var token = AuthToken.validate(jwt, _hs256);
if (enforceIp) { if (enforceIp) {
if (req.ip != token.ipAddress) if (req.ip != token.ipAddress) {
throw AngelHttpException.forbidden( 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) { if (token.lifeSpan > -1) {
@ -290,12 +298,12 @@ class AngelAuth<User> {
_addProtectedCookie(res, 'token', token.serialize(_hs256)); _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)}; return {'data': data, 'token': token.serialize(_hs256)};
} }
} catch (e) { } catch (e) {
if (e is AngelHttpException) rethrow; 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. /// 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. /// 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 { return (RequestContext req, ResponseContext res) async {
List<String> names = []; var names = <String>[];
var arr = type is Iterable var arr = type is Iterable
? type.map((x) => x.toString()).toList() ? type.map((x) => x.toString()).toList()
: [type.toString()]; : [type.toString()];
for (String t in arr) { for (var t in arr) {
var n = t var n = t
.split(',') .split(',')
.map((s) => s.trim()) .map((s) => s.trim())
@ -323,20 +331,20 @@ class AngelAuth<User> {
names.addAll(n); names.addAll(n);
} }
for (int i = 0; i < names.length; i++) { for (var i = 0; i < names.length; i++) {
var name = names[i]; var name = names[i];
var strategy = strategies[name] ??= var strategy = strategies[name] ??=
throw ArgumentError('No strategy "$name" found.'); throw ArgumentError('No strategy "$name" found.');
var hasExisting = req.container.has<User>(); var hasExisting = req.container!.has<User>();
var result = hasExisting var result = hasExisting
? req.container.make<User>() ? req.container!.make<User>()
: await strategy.authenticate(req, res, options); : await strategy.authenticate(req, res, options!);
if (result == true) if (result == true) {
return result; return result;
else if (result != false && result != null) { } else if (result != false && result != null) {
var userId = await serializer(result); var userId = await serializer!(result);
// Create JWT // Create JWT
var token = AuthToken( var token = AuthToken(
@ -344,11 +352,11 @@ class AngelAuth<User> {
var jwt = token.serialize(_hs256); var jwt = token.serialize(_hs256);
if (options?.tokenCallback != null) { if (options?.tokenCallback != null) {
if (!req.container.has<User>()) { if (!req.container!.has<User>()) {
req.container.registerSingleton<User>(result); 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; if (r != null) return r;
jwt = token.serialize(_hs256); jwt = token.serialize(_hs256);
} }
@ -360,17 +368,17 @@ class AngelAuth<User> {
} }
if (options?.callback != null) { if (options?.callback != null) {
return await options.callback(req, res, jwt); return await options!.callback!(req, res, jwt);
} }
if (options?.successRedirect?.isNotEmpty == true) { if (options?.successRedirect?.isNotEmpty == true) {
await res.redirect(options.successRedirect); await res.redirect(options!.successRedirect);
return false; return false;
} else if (options?.canRespondWithJson != false && } else if (options?.canRespondWithJson != false &&
req.accepts('application/json')) { req.accepts('application/json')) {
var user = hasExisting var user = hasExisting
? result ? result
: await deserializer(await serializer(result)); : await deserializer!((await serializer!(result)) as Object);
_onLogin.add(user); _onLogin.add(user);
return {"data": user, "token": jwt}; return {"data": user, "token": jwt};
} }
@ -381,13 +389,14 @@ class AngelAuth<User> {
// Check if not redirect // Check if not redirect
if (res.statusCode == 301 || if (res.statusCode == 301 ||
res.statusCode == 302 || res.statusCode == 302 ||
res.headers.containsKey('location')) res.headers.containsKey('location')) {
return false; return false;
else if (options?.failureRedirect != null) { } else if (options?.failureRedirect != null) {
await res.redirect(options.failureRedirect); await res.redirect(options!.failureRedirect);
return false; return false;
} else } else {
throw AngelHttpException.notAuthenticated(); throw AngelHttpException.notAuthenticated();
}
} }
} }
}; };
@ -395,7 +404,7 @@ class AngelAuth<User> {
/// Log a user in on-demand. /// Log a user in on-demand.
Future login(AuthToken token, RequestContext req, ResponseContext res) async { 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); _apply(req, res, token, user);
_onLogin.add(user); _onLogin.add(user);
@ -406,7 +415,7 @@ class AngelAuth<User> {
/// Log a user in on-demand. /// Log a user in on-demand.
Future loginById(userId, RequestContext req, ResponseContext res) async { Future loginById(userId, RequestContext req, ResponseContext res) async {
var user = await deserializer(userId); var user = await deserializer!(userId as Object);
var token = var token =
AuthToken(userId: userId, lifeSpan: _jwtLifeSpan, ipAddress: req.ip); AuthToken(userId: userId, lifeSpan: _jwtLifeSpan, ipAddress: req.ip);
_apply(req, res, token, user); _apply(req, res, token, user);
@ -418,21 +427,23 @@ class AngelAuth<User> {
} }
/// Log an authenticated user out. /// Log an authenticated user out.
RequestHandler logout([AngelAuthOptions<User> options]) { RequestHandler logout([AngelAuthOptions<User>? options]) {
return (RequestContext req, ResponseContext res) async { return (RequestContext req, ResponseContext res) async {
if (req.container.has<User>()) { if (req.container?.has<User>() == true) {
var user = req.container.make<User>(); var user = req.container?.make<User>();
_onLogout.add(user); if (user != null) {
_onLogout.add(user);
}
} }
if (allowCookie == true) { if (allowCookie == true) {
res.cookies.removeWhere((cookie) => cookie.name == "token"); res.cookies.removeWhere((cookie) => cookie.name == 'token');
_addProtectedCookie(res, 'token', '""'); _addProtectedCookie(res, 'token', '""');
} }
if (options != null && if (options != null &&
options.successRedirect != null && options.successRedirect != null &&
options.successRedirect.isNotEmpty) { options.successRedirect!.isNotEmpty) {
await res.redirect(options.successRedirect); await res.redirect(options.successRedirect);
} }

View file

@ -1,5 +1,5 @@
import 'dart:convert'; 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 'package:http_parser/http_parser.dart';
import 'options.dart'; import 'options.dart';

View file

@ -1,18 +1,19 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel3_framework/angel3_framework.dart';
import '../options.dart'; import '../options.dart';
import '../strategy.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. /// Determines the validity of an incoming username and password.
typedef FutureOr<User> LocalAuthVerifier<User>( // typedef FutureOr<User> LocalAuthVerifier<User>(String? username, String? password);
String username, String password); typedef LocalAuthVerifier<User> = FutureOr<User> Function(
String? username, String? password);
class LocalAuthStrategy<User> extends AuthStrategy<User> { class LocalAuthStrategy<User> extends AuthStrategy<User> {
RegExp _rgxBasic = RegExp(r'^Basic (.+)$', caseSensitive: false); final RegExp _rgxBasic = RegExp(r'^Basic (.+)$', caseSensitive: false);
RegExp _rgxUsrPass = RegExp(r'^([^:]+):(.+)$'); final RegExp _rgxUsrPass = RegExp(r'^([^:]+):(.+)$');
LocalAuthVerifier<User> verifier; LocalAuthVerifier<User> verifier;
String usernameField; String usernameField;
@ -23,35 +24,37 @@ class LocalAuthStrategy<User> extends AuthStrategy<User> {
String realm; String realm;
LocalAuthStrategy(this.verifier, LocalAuthStrategy(this.verifier,
{String this.usernameField = 'username', {this.usernameField = 'username',
String this.passwordField = 'password', this.passwordField = 'password',
String this.invalidMessage = this.invalidMessage = 'Please provide a valid username and password.',
'Please provide a valid username and password.', this.allowBasic = true,
bool this.allowBasic = true, this.forceBasic = false,
bool this.forceBasic = false, this.realm = 'Authentication is required.'});
String this.realm = 'Authentication is required.'});
@override @override
Future<User> authenticate(RequestContext req, ResponseContext res, Future<User?> authenticate(RequestContext req, ResponseContext res,
[AngelAuthOptions options_]) async { [AngelAuthOptions? options_]) async {
AngelAuthOptions options = options_ ?? AngelAuthOptions(); var options = options_ ?? AngelAuthOptions();
User verificationResult; User? verificationResult;
if (allowBasic) { if (allowBasic) {
String authHeader = req.headers.value('authorization') ?? ""; var authHeader = req.headers?.value('authorization') ?? '';
if (_rgxBasic.hasMatch(authHeader)) { if (_rgxBasic.hasMatch(authHeader)) {
String base64AuthString = _rgxBasic.firstMatch(authHeader).group(1); var base64AuthString = _rgxBasic.firstMatch(authHeader)?.group(1);
String authString = if (base64AuthString == null) {
String.fromCharCodes(base64.decode(base64AuthString)); return null;
}
var authString = String.fromCharCodes(base64.decode(base64AuthString));
if (_rgxUsrPass.hasMatch(authString)) { if (_rgxUsrPass.hasMatch(authString)) {
Match usrPassMatch = _rgxUsrPass.firstMatch(authString); Match usrPassMatch = _rgxUsrPass.firstMatch(authString)!;
verificationResult = verificationResult =
await verifier(usrPassMatch.group(1), usrPassMatch.group(2)); await verifier(usrPassMatch.group(1), usrPassMatch.group(2));
} else } else {
throw AngelHttpException.badRequest(errors: [invalidMessage]); throw AngelHttpException.badRequest(errors: [invalidMessage]);
}
if (verificationResult == false || verificationResult == null) { if (verificationResult == null) {
res res
..statusCode = 401 ..statusCode = 401
..headers['www-authenticate'] = 'Basic realm="$realm"'; ..headers['www-authenticate'] = 'Basic realm="$realm"';
@ -68,27 +71,29 @@ class LocalAuthStrategy<User> extends AuthStrategy<User> {
.parseBody() .parseBody()
.then((_) => req.bodyAsMap) .then((_) => req.bodyAsMap)
.catchError((_) => <String, dynamic>{}); .catchError((_) => <String, dynamic>{});
if (_validateString(body[usernameField]?.toString()) && //if (body != null) {
_validateString(body[passwordField]?.toString())) { if (_validateString(body[usernameField].toString()) &&
_validateString(body[passwordField].toString())) {
verificationResult = await verifier( 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 && if (options.failureRedirect != null &&
options.failureRedirect.isNotEmpty) { options.failureRedirect!.isNotEmpty) {
await res.redirect(options.failureRedirect, code: 401); await res.redirect(options.failureRedirect, code: 401);
return null; return null;
} }
if (forceBasic) { if (forceBasic) {
res.headers['www-authenticate'] = 'Basic realm="$realm"'; res.headers['www-authenticate'] = 'Basic realm="$realm"';
throw AngelHttpException.notAuthenticated(); return null;
} }
return null; return null;
} else if (verificationResult != null && verificationResult != false) { } else if (verificationResult != false) {
return verificationResult; return verificationResult;
} else { } else {
throw AngelHttpException.notAuthenticated(); throw AngelHttpException.notAuthenticated();

View file

@ -1,10 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel3_framework/angel3_framework.dart';
import 'options.dart'; import 'options.dart';
/// A function that handles login and signup for an Angel application. /// A function that handles login and signup for an Angel application.
abstract class AuthStrategy<User> { abstract class AuthStrategy<User> {
/// Authenticates or rejects an incoming user. /// Authenticates or rejects an incoming user.
FutureOr<User> authenticate(RequestContext req, ResponseContext res, FutureOr<User?> authenticate(RequestContext req, ResponseContext res,
[AngelAuthOptions<User> options]); [AngelAuthOptions<User> options]);
} }

View file

@ -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. description: A complete authentication plugin for Angel. Includes support for stateless JWT tokens, Basic Auth, and more.
version: 3.0.0 version: 4.0.1
author: Tobe O <thosakwe@gmail.com> homepage: https://github.com/dukefirehawk/angel/tree/angel3/packages/auth
homepage: https://github.com/angel-dart/angel_auth
publish_to: none
environment: environment:
sdk: ">=2.10.0 <3.0.0" sdk: '>=2.12.0 <3.0.0'
dependencies: dependencies:
angel_framework: angel3_framework: ^4.0.0
git: charcode: ^1.2.0
url: https://github.com/dukefirehawk/angel.git collection: ^1.15.0
ref: sdk-2.12.x
path: packages/framework
charcode: ^1.0.0
collection: ^1.0.0
crypto: ^3.0.0 crypto: ^3.0.0
http_parser: ^4.0.0 http_parser: ^4.0.0
meta: ^1.0.0 meta: ^1.3.0
quiver_hashcode: ^2.0.0 quiver: ^3.0.0
dev_dependencies: dev_dependencies:
http: ^0.13.0 http: ^0.13.1
io: ^1.0.0 io: ^1.0.0
logging: ^1.0.0 logging: ^1.0.0
pedantic: ^1.0.0 pedantic: ^1.11.0
test: ^1.15.7 test: ^1.17.4

View file

@ -1,12 +1,12 @@
import "package:angel_auth/src/auth_token.dart"; import 'package:angel3_auth/src/auth_token.dart';
import "package:crypto/crypto.dart"; import 'package:crypto/crypto.dart';
import "package:test/test.dart"; import 'package:test/test.dart';
main() async { void main() async {
final Hmac hmac = Hmac(sha256, "angel_auth".codeUnits); final hmac = Hmac(sha256, 'angel_auth'.codeUnits);
test("sample serialization", () { test('sample serialization', () {
var token = AuthToken(ipAddress: "localhost", userId: "thosakwe"); var token = AuthToken(ipAddress: 'localhost', userId: 'thosakwe');
var jwt = token.serialize(hmac); var jwt = token.serialize(hmac);
print(jwt); print(jwt);
@ -17,7 +17,7 @@ main() async {
}); });
test('custom payload', () { test('custom payload', () {
var token = AuthToken(ipAddress: "localhost", userId: "thosakwe", payload: { var token = AuthToken(ipAddress: 'localhost', userId: 'thosakwe', payload: {
"foo": "bar", "foo": "bar",
"baz": { "baz": {
"one": 1, "one": 1,

View file

@ -1,22 +1,24 @@
import 'dart:io'; import 'dart:io';
import 'package:angel_auth/angel_auth.dart'; import 'package:angel3_auth/angel3_auth.dart';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel3_framework/angel3_framework.dart';
import 'package:angel_framework/http.dart'; import 'package:angel3_framework/http.dart';
import 'dart:convert'; import 'dart:convert';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:io/ansi.dart'; import 'package:io/ansi.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:collection/collection.dart';
class User extends Model { class User extends Model {
String username, password; String? username, password;
User({this.username, this.password}); User({this.username, this.password});
static User parse(Map map) { static User parse(Map map) {
return User( return User(
username: map['username'] as String, username: map['username'] as String?,
password: map['password'] as String, password: map['password'] as String?,
); );
} }
@ -31,13 +33,13 @@ class User extends Model {
} }
} }
main() { void main() {
Angel app; late Angel app;
AngelHttp angelHttp; late AngelHttp angelHttp;
AngelAuth<User> auth; AngelAuth<User?> auth;
http.Client client; http.Client? client;
HttpServer server; HttpServer server;
String url; String? url;
setUp(() async { setUp(() async {
hierarchicalLoggingEnabled = true; hierarchicalLoggingEnabled = true;
@ -47,7 +49,7 @@ main() {
var oldErrorHandler = app.errorHandler; var oldErrorHandler = app.errorHandler;
app.errorHandler = (e, req, res) { 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); return oldErrorHandler(e, req, res);
}; };
@ -66,24 +68,26 @@ main() {
}); });
await app await app
.findService('users') .findService('users')!
.create({'username': 'jdoe1', 'password': 'password'}); .create({'username': 'jdoe1', 'password': 'password'});
auth = AngelAuth<User>(); auth = AngelAuth<User?>();
auth.serializer = (u) => u.id; auth.serializer = (u) => u!.id;
auth.deserializer = 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); await app.configure(auth.configureServer);
auth.strategies['local'] = LocalAuthStrategy((username, password) async { auth.strategies['local'] = LocalAuthStrategy((username, password) async {
var users = await app var users = await app
.findService('users') .findService('users')!
.index() .index()
.then((it) => it.map<User>((m) => User.parse(m as Map)).toList()); .then((it) => it.map<User>((m) => User.parse(m as Map)).toList());
return users.firstWhere(
(user) => user.username == username && user.password == password, var result = users.firstWhereOrNull(
orElse: () => null); (user) => user.username == username && user.password == password);
return Future.value(result);
}); });
app.post( app.post(
@ -97,8 +101,8 @@ main() {
app.chain([ app.chain([
(req, res) { (req, res) {
if (!req.container.has<User>()) { if (!req.container!.has<User>()) {
req.container.registerSingleton<User>( req.container!.registerSingleton<User>(
User(username: req.params['name']?.toString())); User(username: req.params['name']?.toString()));
} }
return true; return true;
@ -114,15 +118,15 @@ main() {
}); });
tearDown(() async { tearDown(() async {
client.close(); client!.close();
await angelHttp.close(); await angelHttp.close();
app = null; //app = null;
client = null; client = null;
url = null; url = null;
}); });
test('login', () async { 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'}); body: {'username': 'jdoe1', 'password': 'password'});
print('Response: ${response.body}'); print('Response: ${response.body}');
expect(response.body, equals('Hello!')); expect(response.body, equals('Hello!'));
@ -132,7 +136,7 @@ main() {
: null); : null);
test('preserve existing user', () async { 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'}, body: {'username': 'jdoe1', 'password': 'password'},
headers: {'accept': 'application/json'}); headers: {'accept': 'application/json'});
print('Response: ${response.body}'); print('Response: ${response.body}');

View file

@ -1,4 +1,4 @@
import 'package:angel_auth/angel_auth.dart'; import 'package:angel3_auth/angel3_auth.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
void main() { void main() {
@ -70,6 +70,7 @@ void main() {
); );
}); });
/* Deprecated as clientId and clientSecret cannot be null
test('ensures id not null', () { test('ensures id not null', () {
expect( expect(
() => ExternalAuthOptions( () => ExternalAuthOptions(
@ -89,6 +90,7 @@ void main() {
throwsArgumentError, throwsArgumentError,
); );
}); });
*/
}); });
group('fromMap()', () { group('fromMap()', () {

View file

@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:angel_auth/angel_auth.dart'; import 'package:angel3_auth/angel3_auth.dart';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel3_framework/angel3_framework.dart';
import 'package:angel_framework/http.dart'; import 'package:angel3_framework/http.dart';
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@ -13,11 +13,11 @@ var localOpts = AngelAuthOptions<Map<String, String>>(
failureRedirect: '/failure', successRedirect: '/success'); failureRedirect: '/failure', successRedirect: '/success');
Map<String, String> sampleUser = {'hello': 'world'}; 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') { if (username == 'username' && password == 'password') {
return sampleUser; return sampleUser;
} else { } else {
return null; throw ArgumentError('Unexpected type for data');
} }
} }
@ -31,10 +31,10 @@ Future wireAuth(Angel app) async {
void main() async { void main() async {
Angel app; Angel app;
AngelHttp angelHttp; late AngelHttp angelHttp;
http.Client client; http.Client? client;
String url; String? url;
String basicAuthUrl; String? basicAuthUrl;
setUp(() async { setUp(() async {
client = http.Client(); client = http.Client();
@ -72,7 +72,7 @@ void main() async {
}); });
test('can use "auth" as middleware', () 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'}); headers: {'Accept': 'application/json'});
print(response.body); print(response.body);
expect(response.statusCode, equals(403)); expect(response.statusCode, equals(403));
@ -80,7 +80,7 @@ void main() async {
test('successRedirect', () async { test('successRedirect', () async {
var postData = {'username': 'username', 'password': 'password'}; 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), body: json.encode(postData),
headers: {'content-type': 'application/json'}); headers: {'content-type': 'application/json'});
expect(response.statusCode, equals(302)); expect(response.statusCode, equals(302));
@ -89,7 +89,7 @@ void main() async {
test('failureRedirect', () async { test('failureRedirect', () async {
var postData = {'username': 'password', 'password': 'username'}; 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), body: json.encode(postData),
headers: {'content-type': 'application/json'}); headers: {'content-type': 'application/json'});
print('Login response: ${response.body}'); print('Login response: ${response.body}');
@ -99,13 +99,13 @@ void main() async {
test('allow basic', () async { test('allow basic', () async {
var authString = base64.encode('username:password'.runes.toList()); 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'}); headers: {'authorization': 'Basic $authString'});
expect(response.body, equals('"Woo auth"')); expect(response.body, equals('"Woo auth"'));
}); });
test('allow basic via URL encoding', () async { 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"')); expect(response.body, equals('"Woo auth"'));
}); });
@ -113,12 +113,13 @@ void main() async {
auth.strategies.clear(); auth.strategies.clear();
auth.strategies['local'] = auth.strategies['local'] =
LocalAuthStrategy(verifier, forceBasic: true, realm: 'test'); 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', 'accept': 'application/json',
'content-type': 'application/json' 'content-type': 'application/json'
}); });
print(response.headers); print('Header = ${response?.headers}');
print('Body <${response.body}>'); print('Body <${response?.body}>');
expect(response.headers['www-authenticate'], equals('Basic realm="test"')); var head = response?.headers['www-authenticate'];
expect(head, equals('Basic realm="test"'));
}); });
} }

View file

@ -1,12 +1,12 @@
import 'dart:io'; import 'dart:io';
import 'package:angel_auth/angel_auth.dart'; import 'package:angel3_auth/angel3_auth.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
const Duration threeDays = const Duration(days: 3); const Duration threeDays = Duration(days: 3);
void main() { void main() {
Cookie defaultCookie; late Cookie defaultCookie;
var auth = AngelAuth( var auth = AngelAuth(
secureCookies: true, secureCookies: true,
cookieDomain: 'SECURE', cookieDomain: 'SECURE',
@ -21,7 +21,7 @@ void main() {
test('sets expires', () { test('sets expires', () {
var now = DateTime.now(); var now = DateTime.now();
var expiry = auth.protectCookie(defaultCookie).expires; var expiry = auth.protectCookie(defaultCookie).expires!;
var diff = expiry.difference(now); var diff = expiry.difference(now);
expect(diff.inSeconds, threeDays.inSeconds); expect(diff.inSeconds, threeDays.inSeconds);
}); });

View file

@ -38,8 +38,8 @@ main() async {
'http://localhost:3000/auth/twitter/callback', 'http://localhost:3000/auth/twitter/callback',
), ),
(twit, req, res) async { (twit, req, res) async {
var response = await twit.twitterClient var response = await twit.twitterClient.get(Uri.parse(
.get('https://api.twitter.com/1.1/account/verify_credentials.json'); 'https://api.twitter.com/1.1/account/verify_credentials.json'));
var userData = json.decode(response.body) as Map; var userData = json.decode(response.body) as Map;
return _User(userData['screen_name'] as String); return _User(userData['screen_name'] as String);
}, },

View file

@ -1,11 +1,10 @@
name: "angel_auth_twitter" 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." description: "package:angel_auth strategy for Twitter login. Auto-signs requests."
homepage: "https://github.com/angel-dart/auth_twitter.git"
publish_to: none
environment: environment:
sdk: ">=2.10.0 <3.0.0" sdk: ">=2.10.0 <3.0.0"
homepage: "https://github.com/angel-dart/auth_twitter.git"
version: 3.0.0
publish_to: none
dependencies: dependencies:
angel_auth: angel_auth:
git: git:

2
packages/cli/AUTHORS.md Normal file
View file

@ -0,0 +1,2 @@
Tobe O <thosakwe@gmail.com>
Thomas Hii <thomashii@dukefirehawk.com>

View file

@ -1,3 +1,6 @@
# 3.0.0
* Migrated to work with Dart SDK 2.12.x Non NNBD
# 2.1.7+1 # 2.1.7+1
* Fix a bug where new directories were not being created in * Fix a bug where new directories were not being created in
`init`. `init`.

View file

@ -1,4 +1,5 @@
# Todo # Todo
* Migrate inflection2, mustache4dart2 and prompts packages to NNBD
* `service` * `service`
* Add tests * Add tests

View file

@ -1,13 +1,32 @@
# See https://www.dartlang.org/tools/private-files.html # See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub # Files and directories created by pub
.buildlog .dart_tool
.packages .packages
.project
.pub/ .pub/
build/ 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/ **/packages/
# Files created by dart2js # 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 # rules if you intend to use dart2js directly
@ -20,62 +39,33 @@ build/
*.info.json *.info.json
# Directory created by dartdoc # 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) # (Library packages only! Remove pattern if developing an application package)
pubspec.lock ### JetBrains template
.idea # 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 # User-specific stuff:
*.sum
# Logs ## VsCode
logs .vscode/
*.log
npm-debug.log*
# Runtime data ## File-based project format:
pids *.iws
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover ## Plugin-specific files:
lib-cov
# Coverage directory used by tools like istanbul # IntelliJ
coverage .idea/
/out/
.idea_modules/
# nyc test coverage # JIRA plugin
.nyc_output atlassian-ide-plugin.xml
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) # Crashlytics plugin (for Android Studio and IntelliJ)
.grunt com_crashlytics_export_strings.xml
crashlytics.properties
# node-waf configuration crashlytics-build.properties
.lock-wscript fabric.properties
# 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

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

View file

@ -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 # 2.0.2
* `_join` previously discarded quer parameters, etc. * `_join` previously discarded quer parameters, etc.
* Allow any `Map<String, dynamic>` as body, not just `Map<String, String>`. * Allow any `Map<String, dynamic>` as body, not just `Map<String, String>`.

View file

@ -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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -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) [![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/angel3/packages/client/LICENSE)
[![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.
# Usage # Usage
```dart ```dart
// Choose one or the other, depending on platform // Choose one or the other, depending on platform
import 'package:angel_client/io.dart'; import 'package:angel3_client/io.dart';
import 'package:angel_client/browser.dart'; import 'package:angel3_client/browser.dart';
import 'package:angel_client/flutter.dart'; import 'package:angel3_client/flutter.dart';
main() async { 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. you can use the same class on the client and the server.
```dart ```dart
@ -96,9 +93,9 @@ Use `ServiceList` for this case:
```dart ```dart
build(BuildContext context) async { 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, stream: list.onChange,
builder: _yourBuildFunction, builder: _yourBuildFunction,
); );

View file

@ -1,21 +1,21 @@
import 'dart:async'; import 'dart:async';
import 'package:angel_client/angel_client.dart'; import 'package:angel3_client/angel3_client.dart';
Future doSomething(Angel app) async { Future doSomething(Angel app) async {
var userService = app var userService = app
.service<String, Map<String, dynamic>>('api/users') .service<String, Map<String, dynamic>>('api/users')
.map(User.fromMap, User.toMap); .map(User.fromMap, User.toMap);
var users = await userService.index(); var users = await (userService.index() as FutureOr<List<User>>);
print('Name: ${users.first.name}'); print('Name: ${users.first.name}');
} }
class User { class User {
final String name; final String? name;
User({this.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};
} }

View file

@ -1,21 +1,20 @@
/// Client library for the Angel framework. /// Client library for the Angel framework.
library angel_client; library angel3_client;
import 'dart:async'; import 'dart:async';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
export 'package:angel_http_exception/angel_http_exception.dart'; export 'package:angel3_http_exception/angel3_http_exception.dart';
import 'package:meta/meta.dart';
/// A function that configures an [Angel] client in some way. /// 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. /// A function that deserializes data received from the server.
/// ///
/// This is only really necessary in the browser, where `json_god` /// This is only really necessary in the browser, where `json_god`
/// doesn't work. /// doesn't work.
typedef T AngelDeserializer<T>(x); typedef AngelDeserializer<T> = T? Function(dynamic x);
/// Represents an Angel server that we are querying. /// Represents an Angel server that we are querying.
abstract class Angel extends http.BaseClient { abstract class Angel extends http.BaseClient {
@ -23,13 +22,13 @@ abstract class Angel extends http.BaseClient {
/// that is automatically attached to every request sent. /// that is automatically attached to every request sent.
/// ///
/// This is designed with `package:angel_auth` in mind. /// This is designed with `package:angel_auth` in mind.
String authToken; String? authToken;
/// The root URL at which the target server. /// The root URL at which the target server.
final Uri baseUrl; final Uri baseUrl;
Angel(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. /// Prefer to use [baseUrl] instead.
@deprecated @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. /// The given [credentials] are sent to server as-is; the request body is sent as JSON.
Future<AngelAuthResult> authenticate( Future<AngelAuthResult> authenticate(
{@required String type, {required String type,
credentials, credentials,
String authEndpoint = '/auth', String authEndpoint = '/auth',
@deprecated String reviveEndpoint = '/auth/token'}); @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 /// You can pass a custom [deserializer], which is typically necessary in cases where
/// `dart:mirrors` does not exist. /// `dart:mirrors` does not exist.
Service<Id, Data> service<Id, Data>(String path, Service<Id, Data> service<Id, Data>(String path,
{@deprecated Type type, AngelDeserializer<Data> deserializer}); {@deprecated Type? type, AngelDeserializer<Data>? deserializer});
//@override //@override
//Future<http.Response> delete(url, {Map<String, String> headers}); //Future<http.Response> delete(url, {Map<String, String> headers});
@override @override
Future<http.Response> get(url, {Map<String, String> headers}); Future<http.Response> get(url, {Map<String, String>? headers});
@override @override
Future<http.Response> head(url, {Map<String, String> headers}); Future<http.Response> head(url, {Map<String, String>? headers});
@override @override
Future<http.Response> patch(url, Future<http.Response> patch(url,
{body, Map<String, String> headers, Encoding encoding}); {body, Map<String, String>? headers, Encoding? encoding});
@override @override
Future<http.Response> post(url, Future<http.Response> post(url,
{body, Map<String, String> headers, Encoding encoding}); {body, Map<String, String>? headers, Encoding? encoding});
@override @override
Future<http.Response> put(url, 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. /// Represents the result of authentication with an Angel server.
class AngelAuthResult { class AngelAuthResult {
String _token; String? _token;
final Map<String, dynamic> data = {}; final Map<String, dynamic> data = {};
/// The JSON Web token that was sent with this response. /// 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; _token = token;
this.data.addAll(data ?? {}); this.data.addAll(data);
} }
/// Attempts to deserialize a response from a [Map]. /// Attempts to deserialize a response from a [Map].
factory AngelAuthResult.fromMap(Map data) { factory AngelAuthResult.fromMap(Map? data) {
final result = AngelAuthResult(); 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(); result._token = data['token'].toString();
}
if (data is Map) if (data is Map) {
result.data.addAll((data['data'] as Map<String, dynamic>) ?? {}); result.data.addAll((data['data'] as Map<String, dynamic>?) ?? {});
}
if (result.token == null) { if (result.token == null) {
throw FormatException( throw FormatException(
'The required "token" field was not present in the given data.'); '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( throw FormatException(
'The required "data" field in the given data was not a map; instead, it was ${data['data']}.'); '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]. /// Attempts to deserialize a response from a [String].
factory AngelAuthResult.fromJson(String s) => 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. /// Converts this instance into a JSON-friendly representation.
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@ -179,22 +180,22 @@ abstract class Service<Id, Data> {
Future close(); Future close();
/// Retrieves all resources. /// Retrieves all resources.
Future<List<Data>> index([Map<String, dynamic> params]); Future<List<Data>?> index([Map<String, dynamic>? params]);
/// Retrieves the desired resource. /// 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. /// Creates a resource.
Future<Data> create(Data data, [Map<String, dynamic> params]); Future<Data> create(Data data, [Map<String, dynamic>? params]);
/// Modifies a resource. /// 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. /// 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. /// 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. /// 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(); Future close() => Future.value();
@override @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); return inner.create(decoder(data)).then(encoder);
} }
@override @override
Future<List<U>> index([Map<String, dynamic> params]) { Future<List<U>> index([Map<String, dynamic>? params]) {
return inner.index(params).then((l) => l.map(encoder).toList()); return inner.index(params).then((l) => l!.map(encoder).toList());
} }
@override @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); 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); Stream<U> get onUpdated => inner.onUpdated.map(encoder);
@override @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); return inner.read(id, params).then(encoder);
} }
@override @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); return inner.remove(id, params).then(encoder);
} }
@override @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); 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. /// A function used to compare the ID's two items for equality.
/// ///
/// Defaults to comparing the [idField] of `Map` instances. /// 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; final Service<Id, Data> service;
@ -285,15 +286,16 @@ class ServiceList<Id, Data> extends DelegatingList<Data> {
final List<StreamSubscription> _subs = []; final List<StreamSubscription> _subs = [];
ServiceList(this.service, {this.idField = 'id', Equality<Data> equality}) ServiceList(this.service, {this.idField = 'id', Equality<Data>? equality})
: super([]) { : super([]) {
_equality = equality; _equality = equality;
_equality ??= EqualityBy<Data, Id>((map) { _equality ??= EqualityBy<Data, Id?>((map) {
if (map is Map) if (map is Map) {
return map[idField ?? 'id'] as Id; return map[idField] as Id?;
else } else {
throw UnsupportedError( throw UnsupportedError(
'ServiceList only knows how to find the id from a Map object. Provide a custom `Equality` in your call to the constructor.'); 'ServiceList only knows how to find the id from a Map object. Provide a custom `Equality` in your call to the constructor.');
}
}); });
// Index // Index
_subs.add(service.onIndexed.where(_notNull).listen((data) { _subs.add(service.onIndexed.where(_notNull).listen((data) {
@ -310,15 +312,17 @@ class ServiceList<Id, Data> extends DelegatingList<Data> {
})); }));
// Modified/Updated // Modified/Updated
handleModified(Data item) { void handleModified(Data item) {
var indices = <int>[]; var indices = <int>[];
for (int i = 0; i < length; i++) { for (var i = 0; i < length; i++) {
if (_equality.equals(item, this[i])) indices.add(i); if (_equality!.equals(item, this[i])) indices.add(i);
} }
if (indices.isNotEmpty) { if (indices.isNotEmpty) {
for (var i in indices) this[i] = item; for (var i in indices) {
this[i] = item;
}
_onChange.add(this); _onChange.add(this);
} }
@ -331,7 +335,7 @@ class ServiceList<Id, Data> extends DelegatingList<Data> {
// Removed // Removed
_subs.add(service.onRemoved.where(_notNull).listen((item) { _subs.add(service.onRemoved.where(_notNull).listen((item) {
removeWhere((x) => _equality.equals(item, x)); removeWhere((x) => _equality!.equals(item, x));
_onChange.add(this); _onChange.add(this);
})); }));
} }

View file

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert' show Encoding; 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 'dart:convert';
import 'package:http/src/base_client.dart' as http; import 'package:http/src/base_client.dart' as http;
import 'package:http/src/base_request.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/response.dart' as http;
import 'package:http/src/streamed_response.dart' as http; import 'package:http/src/streamed_response.dart' as http;
import 'package:path/path.dart' as p; 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> _readHeaders = {'Accept': 'application/json'};
const Map<String, String> _writeHeaders = { const Map<String, String> _writeHeaders = {
@ -16,17 +16,15 @@ const Map<String, String> _writeHeaders = {
'Content-Type': 'application/json' '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())); return params?.map((k, v) => MapEntry(k, v.toString()));
} }
bool _invalid(http.Response response) => 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, AngelHttpException failure(http.Response response,
{error, String message, StackTrace stack}) { {error, String? message, StackTrace? stack}) {
try { try {
var v = json.decode(response.body); var v = json.decode(response.body);
@ -52,7 +50,7 @@ abstract class BaseAngelClient extends Angel {
final StreamController<AngelAuthResult> _onAuthenticated = final StreamController<AngelAuthResult> _onAuthenticated =
StreamController<AngelAuthResult>(); StreamController<AngelAuthResult>();
final List<Service> _services = []; final List<Service> _services = [];
final http.BaseClient client; final http.BaseClient? client;
@override @override
Stream<AngelAuthResult> get onAuthenticated => _onAuthenticated.stream; Stream<AngelAuthResult> get onAuthenticated => _onAuthenticated.stream;
@ -61,7 +59,7 @@ abstract class BaseAngelClient extends Angel {
@override @override
Future<AngelAuthResult> authenticate( Future<AngelAuthResult> authenticate(
{String type, {String? type,
credentials, credentials,
String authEndpoint = '/auth', String authEndpoint = '/auth',
@deprecated String reviveEndpoint = '/auth/token'}) async { @deprecated String reviveEndpoint = '/auth/token'}) async {
@ -92,14 +90,12 @@ abstract class BaseAngelClient extends Angel {
//var v = json.decode(response.body); //var v = json.decode(response.body);
var v = jsonDecode(response.body); var v = jsonDecode(response.body);
if (v is! Map || if (v is! Map || !v.containsKey('data') || !v.containsKey('token')) {
!(v as Map).containsKey('data') ||
!(v as Map).containsKey('token')) {
throw AngelHttpException.notAuthenticated( throw AngelHttpException.notAuthenticated(
message: "Auth endpoint '$url' did not return a proper response."); 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); _onAuthenticated.add(r);
return r; return r;
} on AngelHttpException { } on AngelHttpException {
@ -111,7 +107,7 @@ abstract class BaseAngelClient extends Angel {
@override @override
Future<void> close() async { Future<void> close() async {
client.close(); client!.close();
await _onAuthenticated.close(); await _onAuthenticated.close();
await Future.wait(_services.map((s) => s.close())).then((_) { await Future.wait(_services.map((s) => s.close())).then((_) {
_services.clear(); _services.clear();
@ -128,13 +124,13 @@ abstract class BaseAngelClient extends Angel {
if (authToken?.isNotEmpty == true) { if (authToken?.isNotEmpty == true) {
request.headers['authorization'] ??= 'Bearer $authToken'; request.headers['authorization'] ??= 'Bearer $authToken';
} }
return client.send(request); return client!.send(request);
} }
/// Sends a non-streaming [Request] and returns a non-streaming [Response]. /// Sends a non-streaming [Request] and returns a non-streaming [Response].
Future<http.Response> sendUnstreamed( Future<http.Response> sendUnstreamed(
String method, url, Map<String, String> headers, String method, url, Map<String, String>? headers,
[body, Encoding encoding]) async { [body, Encoding? encoding]) async {
var request = var request =
http.Request(method, url is Uri ? url : Uri.parse(url.toString())); http.Request(method, url is Uri ? url : Uri.parse(url.toString()));
@ -160,12 +156,12 @@ abstract class BaseAngelClient extends Angel {
@override @override
Service<Id, Data> service<Id, Data>(String path, 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 url = baseUrl.replace(path: p.join(baseUrl.path, path));
var s = BaseAngelService<Id, Data>(client, this, url, var s = BaseAngelService<Id, Data>(client, this, url,
deserializer: deserializer); deserializer: deserializer);
_services.add(s); _services.add(s);
return s; return s as Service<Id, Data>;
} }
Uri _join(url) { Uri _join(url) {
@ -180,65 +176,65 @@ abstract class BaseAngelClient extends Angel {
//} //}
@override @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); return sendUnstreamed('GET', _join(url), headers);
} }
@override @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); return sendUnstreamed('HEAD', _join(url), headers);
} }
@override @override
Future<http.Response> patch(url, 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); return sendUnstreamed('PATCH', _join(url), headers, body, encoding);
} }
@override @override
Future<http.Response> post(url, 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); return sendUnstreamed('POST', _join(url), headers, body, encoding);
} }
@override @override
Future<http.Response> put(url, 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); 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 @override
final BaseAngelClient app; final BaseAngelClient app;
final Uri baseUrl; final Uri baseUrl;
final http.BaseClient client; final http.BaseClient? client;
final AngelDeserializer<Data> deserializer; final AngelDeserializer<Data>? deserializer;
final StreamController<List<Data>> _onIndexed = StreamController(); final StreamController<List<Data?>> _onIndexed = StreamController();
final StreamController<Data> _onRead = StreamController(), final StreamController<Data?> _onRead = StreamController(),
_onCreated = StreamController(), _onCreated = StreamController(),
_onModified = StreamController(), _onModified = StreamController(),
_onUpdated = StreamController(), _onUpdated = StreamController(),
_onRemoved = StreamController(); _onRemoved = StreamController();
@override @override
Stream<List<Data>> get onIndexed => _onIndexed.stream; Stream<List<Data?>> get onIndexed => _onIndexed.stream;
@override @override
Stream<Data> get onRead => _onRead.stream; Stream<Data?> get onRead => _onRead.stream;
@override @override
Stream<Data> get onCreated => _onCreated.stream; Stream<Data?> get onCreated => _onCreated.stream;
@override @override
Stream<Data> get onModified => _onModified.stream; Stream<Data?> get onModified => _onModified.stream;
@override @override
Stream<Data> get onUpdated => _onUpdated.stream; Stream<Data?> get onUpdated => _onUpdated.stream;
@override @override
Stream<Data> get onRemoved => _onRemoved.stream; Stream<Data?> get onRemoved => _onRemoved.stream;
@override @override
Future close() async { Future close() async {
@ -251,14 +247,14 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
} }
BaseAngelService(this.client, this.app, baseUrl, {this.deserializer}) 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. /// Use [baseUrl] instead.
@deprecated @deprecated
String get basePath => baseUrl.toString(); String get basePath => baseUrl.toString();
Data deserialize(x) { Data? deserialize(x) {
return deserializer != null ? deserializer(x) : x as Data; return deserializer != null ? deserializer!(x) : x as Data?;
} }
String makeBody(x) { String makeBody(x) {
@ -267,15 +263,15 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
} }
Future<http.StreamedResponse> send(http.BaseRequest request) { 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}'; request.headers['Authorization'] = 'Bearer ${app.authToken}';
} }
return client.send(request); return client!.send(request);
} }
@override @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 url = baseUrl.replace(queryParameters: _buildQuery(params));
var response = await app.sendUnstreamed('GET', url, _readHeaders); var response = await app.sendUnstreamed('GET', url, _readHeaders);
@ -304,7 +300,7 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
} }
@override @override
Future<Data> read(id, [Map<String, dynamic> params]) async { Future<Data?> read(id, [Map<String, dynamic>? params]) async {
var url = baseUrl.replace( var url = baseUrl.replace(
path: p.join(baseUrl.path, id.toString()), path: p.join(baseUrl.path, id.toString()),
queryParameters: _buildQuery(params)); queryParameters: _buildQuery(params));
@ -313,54 +309,58 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
try { try {
if (_invalid(response)) { if (_invalid(response)) {
if (_onRead.hasListener) if (_onRead.hasListener) {
_onRead.addError(failure(response)); _onRead.addError(failure(response));
else } else {
throw failure(response); throw failure(response);
}
} }
var r = deserialize(json.decode(response.body)); var r = deserialize(json.decode(response.body));
_onRead.add(r); _onRead.add(r);
return r; return r;
} catch (e, st) { } catch (e, st) {
if (_onRead.hasListener) if (_onRead.hasListener) {
_onRead.addError(e, st); _onRead.addError(e, st);
else } else {
throw failure(response, error: e, stack: st); throw failure(response, error: e, stack: st);
}
} }
return null; return null;
} }
@override @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 url = baseUrl.replace(queryParameters: _buildQuery(params));
var response = var response =
await app.sendUnstreamed('POST', url, _writeHeaders, makeBody(data)); await app.sendUnstreamed('POST', url, _writeHeaders, makeBody(data));
try { try {
if (_invalid(response)) { if (_invalid(response)) {
if (_onCreated.hasListener) if (_onCreated.hasListener) {
_onCreated.addError(failure(response)); _onCreated.addError(failure(response));
else } else {
throw failure(response); throw failure(response);
}
} }
var r = deserialize(json.decode(response.body)); var r = deserialize(json.decode(response.body));
_onCreated.add(r); _onCreated.add(r);
return r; return r;
} catch (e, st) { } catch (e, st) {
if (_onCreated.hasListener) if (_onCreated.hasListener) {
_onCreated.addError(e, st); _onCreated.addError(e, st);
else } else {
throw failure(response, error: e, stack: st); throw failure(response, error: e, stack: st);
}
} }
return null; return null;
} }
@override @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( var url = baseUrl.replace(
path: p.join(baseUrl.path, id.toString()), path: p.join(baseUrl.path, id.toString()),
queryParameters: _buildQuery(params)); queryParameters: _buildQuery(params));
@ -370,27 +370,29 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
try { try {
if (_invalid(response)) { if (_invalid(response)) {
if (_onModified.hasListener) if (_onModified.hasListener) {
_onModified.addError(failure(response)); _onModified.addError(failure(response));
else } else {
throw failure(response); throw failure(response);
}
} }
var r = deserialize(json.decode(response.body)); var r = deserialize(json.decode(response.body));
_onModified.add(r); _onModified.add(r);
return r; return r;
} catch (e, st) { } catch (e, st) {
if (_onModified.hasListener) if (_onModified.hasListener) {
_onModified.addError(e, st); _onModified.addError(e, st);
else } else {
throw failure(response, error: e, stack: st); throw failure(response, error: e, stack: st);
}
} }
return null; return null;
} }
@override @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( var url = baseUrl.replace(
path: p.join(baseUrl.path, id.toString()), path: p.join(baseUrl.path, id.toString()),
queryParameters: _buildQuery(params)); queryParameters: _buildQuery(params));
@ -400,27 +402,29 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
try { try {
if (_invalid(response)) { if (_invalid(response)) {
if (_onUpdated.hasListener) if (_onUpdated.hasListener) {
_onUpdated.addError(failure(response)); _onUpdated.addError(failure(response));
else } else {
throw failure(response); throw failure(response);
}
} }
var r = deserialize(json.decode(response.body)); var r = deserialize(json.decode(response.body));
_onUpdated.add(r); _onUpdated.add(r);
return r; return r;
} catch (e, st) { } catch (e, st) {
if (_onUpdated.hasListener) if (_onUpdated.hasListener) {
_onUpdated.addError(e, st); _onUpdated.addError(e, st);
else } else {
throw failure(response, error: e, stack: st); throw failure(response, error: e, stack: st);
}
} }
return null; return null;
} }
@override @override
Future<Data> remove(id, [Map<String, dynamic> params]) async { Future<Data?> remove(id, [Map<String, dynamic>? params]) async {
var url = baseUrl.replace( var url = baseUrl.replace(
path: p.join(baseUrl.path, id.toString()), path: p.join(baseUrl.path, id.toString()),
queryParameters: _buildQuery(params)); queryParameters: _buildQuery(params));
@ -429,20 +433,22 @@ class BaseAngelService<Id, Data> extends Service<Id, Data> {
try { try {
if (_invalid(response)) { if (_invalid(response)) {
if (_onRemoved.hasListener) if (_onRemoved.hasListener) {
_onRemoved.addError(failure(response)); _onRemoved.addError(failure(response));
else } else {
throw failure(response); throw failure(response);
}
} }
var r = deserialize(json.decode(response.body)); var r = deserialize(json.decode(response.body));
_onRemoved.add(r); _onRemoved.add(r);
return r; return r;
} catch (e, st) { } catch (e, st) {
if (_onRemoved.hasListener) if (_onRemoved.hasListener) {
_onRemoved.addError(e, st); _onRemoved.addError(e, st);
else } else {
throw failure(response, error: e, stack: st); throw failure(response, error: e, stack: st);
}
} }
return null; return null;

View file

@ -6,27 +6,28 @@ import 'dart:async'
import 'dart:html' show CustomEvent, Event, window; import 'dart:html' show CustomEvent, Event, window;
import 'dart:convert'; import 'dart:convert';
import 'package:http/browser_client.dart' as http; import 'package:http/browser_client.dart' as http;
import 'angel_client.dart'; import 'angel3_client.dart';
// import 'auth_types.dart' as auth_types; // import 'auth_types.dart' as auth_types;
import 'base_angel_client.dart'; import 'base_angel_client.dart';
export 'angel_client.dart'; export 'angel3_client.dart';
/// Queries an Angel server via REST. /// Queries an Angel server via REST.
class Rest extends BaseAngelClient { class Rest extends BaseAngelClient {
Rest(String basePath) : super(new http.BrowserClient(), basePath); Rest(String basePath) : super(http.BrowserClient(), basePath);
@override
Future<AngelAuthResult> authenticate( Future<AngelAuthResult> authenticate(
{String type, {String? type,
credentials, credentials,
String authEndpoint = '/auth', String authEndpoint = '/auth',
@deprecated String reviveEndpoint = '/auth/token'}) async { @deprecated String reviveEndpoint = '/auth/token'}) async {
if (type == null || type == 'token') { if (type == null || type == 'token') {
if (!window.localStorage.containsKey('token')) { if (!window.localStorage.containsKey('token')) {
throw new Exception( throw Exception(
'Cannot revive token from localStorage - there is none.'); '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}; credentials ??= {'token': token};
} }
@ -39,33 +40,34 @@ class Rest extends BaseAngelClient {
@override @override
Stream<String> authenticateViaPopup(String url, Stream<String> authenticateViaPopup(String url,
{String eventName = 'token', String errorMessage}) { {String eventName = 'token', String? errorMessage}) {
var ctrl = new StreamController<String>(); var ctrl = StreamController<String>();
var wnd = window.open(url, 'angel_client_auth_popup'); var wnd = window.open(url, 'angel_client_auth_popup');
Timer t; Timer t;
StreamSubscription sub; StreamSubscription? sub;
t = new Timer.periodic(new Duration(milliseconds: 500), (timer) { t = Timer.periodic(Duration(milliseconds: 500), (timer) {
if (!ctrl.isClosed) { if (!ctrl.isClosed) {
if (wnd.closed) { if (wnd.closed!) {
ctrl.addError(new AngelHttpException.notAuthenticated( ctrl.addError(AngelHttpException.notAuthenticated(
message: message:
errorMessage ?? 'Authentication via popup window failed.')); errorMessage ?? 'Authentication via popup window failed.'));
ctrl.close(); ctrl.close();
timer.cancel(); timer.cancel();
sub?.cancel(); sub?.cancel();
} }
} else } else {
timer.cancel(); timer.cancel();
}
}); });
sub = window.on[eventName ?? 'token'].listen((Event ev) { sub = window.on[eventName].listen((Event ev) {
var e = ev as CustomEvent; var e = ev as CustomEvent;
if (!ctrl.isClosed) { if (!ctrl.isClosed) {
ctrl.add(e.detail.toString()); ctrl.add(e.detail.toString());
t.cancel(); t.cancel();
ctrl.close(); ctrl.close();
sub.cancel(); sub!.cancel();
} }
}); });

View file

@ -4,16 +4,16 @@ library angel_client.flutter;
import 'dart:async'; import 'dart:async';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'base_angel_client.dart'; import 'base_angel_client.dart';
export 'angel_client.dart'; export 'angel3_client.dart';
/// Queries an Angel server via REST. /// Queries an Angel server via REST.
class Rest extends BaseAngelClient { 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 @override
Stream<String> authenticateViaPopup(String url, Stream<String> authenticateViaPopup(String url,
{String eventName = 'token'}) { {String eventName = 'token'}) {
throw new UnimplementedError( throw UnimplementedError(
'Opening popup windows is not supported in the `flutter` client.'); 'Opening popup windows is not supported in the `flutter` client.');
} }
} }

View file

@ -3,11 +3,11 @@ library angel_client.cli;
import 'dart:async'; import 'dart:async';
import 'package:http/http.dart' as http; 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 'package:path/path.dart' as p;
import 'angel_client.dart'; import 'angel3_client.dart';
import 'base_angel_client.dart'; import 'base_angel_client.dart';
export 'angel_client.dart'; export 'angel3_client.dart';
/// Queries an Angel server via REST. /// Queries an Angel server via REST.
class Rest extends BaseAngelClient { class Rest extends BaseAngelClient {
@ -17,11 +17,11 @@ class Rest extends BaseAngelClient {
@override @override
Service<Id, Data> service<Id, Data>(String path, 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 url = baseUrl.replace(path: p.join(baseUrl.path, path));
var s = RestService<Id, Data>(client, this, url, type); var s = RestService<Id, Data>(client, this, url, type);
_services.add(s); _services.add(s);
return s; return s as Service<Id, Data>;
} }
@override @override
@ -42,21 +42,21 @@ class Rest extends BaseAngelClient {
/// Queries an Angel service via REST. /// Queries an Angel service via REST.
class RestService<Id, Data> extends BaseAngelService<Id, Data> { 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); : super(client, app, url);
@override @override
Data deserialize(x) { Data? deserialize(x) {
print(x); print(x);
if (type != null) { if (type != null) {
return x.runtimeType == type return x.runtimeType == type
? x as Data ? x as Data?
: god.deserializeDatum(x, outputType: type) as Data; : god.deserializeDatum(x, outputType: type) as Data?;
} }
return x as Data; return x as Data?;
} }
@override @override

View file

@ -1,41 +1,23 @@
name: angel_client name: angel3_client
version: 3.0.0 version: 4.0.0
description: Support for querying Angel servers in the browser, Flutter, and command-line. description: Support for querying Angel servers in the browser, Flutter, and command-line.
author: Tobe O <thosakwe@gmail.com> homepage: https://github.com/dukefirehawk/angel/tree/angel3/packages/client
homepage: https://github.com/angel-dart/angel_client
publish_to: none
environment: environment:
sdk: ">=2.10.0 <3.0.0" sdk: '>=2.12.0 <3.0.0'
dependencies: dependencies:
angel_http_exception: angel3_http_exception: ^3.0.0
git: angel3_json_god: ^4.0.0
url: https://github.com/dukefirehawk/angel.git collection: ^1.15.0
ref: sdk-2.12.x http: ^0.13.1
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
#dart_json_mapper: ^1.7.0 #dart_json_mapper: ^1.7.0
meta: ^1.0.0 meta: ^1.3.0
path: ^1.0.0 path: ^1.8.0
dev_dependencies: dev_dependencies:
angel_framework: angel3_framework: ^4.0.0
git: angel3_model: ^3.0.0
url: https://github.com/dukefirehawk/angel.git angel3_mock_request: ^2.0.0
ref: sdk-2.12.x async: ^2.6.1
path: packages/framework build_runner: ^1.12.2
angel_model: build_web_compilers: ^2.16.5
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
pedantic: ^1.11.0 pedantic: ^1.11.0
test: ^1.16.5 test: ^1.17.4

View file

@ -9,75 +9,75 @@ void main() {
test('sets method,body,headers,path', () async { test('sets method,body,headers,path', () async {
await app.post(Uri.parse('/post'), await app.post(Uri.parse('/post'),
headers: {'method': 'post'}, body: 'post'); headers: {'method': 'post'}, body: 'post');
expect(app.client.spec.method, 'POST'); expect(app.client.spec!.method, 'POST');
expect(app.client.spec.path, '/post'); expect(app.client.spec!.path, '/post');
expect(app.client.spec.headers['method'], 'post'); expect(app.client.spec!.headers['method'], 'post');
expect(await read(app.client.spec.request.finalize()), 'post'); expect(await read(app.client.spec!.request.finalize()), 'post');
}); });
group('service methods', () { group('service methods', () {
test('index', () async { test('index', () async {
await todoService.index(); await todoService.index();
expect(app.client.spec.method, 'GET'); expect(app.client.spec!.method, 'GET');
expect(app.client.spec.path, '/api/todos'); expect(app.client.spec!.path, '/api/todos');
}); });
test('read', () async { test('read', () async {
await todoService.read('sleep'); await todoService.read('sleep');
expect(app.client.spec.method, 'GET'); expect(app.client.spec!.method, 'GET');
expect(app.client.spec.path, '/api/todos/sleep'); expect(app.client.spec!.path, '/api/todos/sleep');
}); });
test('create', () async { test('create', () async {
await todoService.create({}); await todoService.create({});
expect(app.client.spec.method, 'POST'); expect(app.client.spec!.method, 'POST');
expect(app.client.spec.headers['content-type'], expect(app.client.spec!.headers['content-type'],
startsWith('application/json')); startsWith('application/json'));
expect(app.client.spec.path, '/api/todos'); expect(app.client.spec!.path, '/api/todos');
expect(await read(app.client.spec.request.finalize()), '{}'); expect(await read(app.client.spec!.request.finalize()), '{}');
}); });
test('modify', () async { test('modify', () async {
await todoService.modify('sleep', {}); await todoService.modify('sleep', {});
expect(app.client.spec.method, 'PATCH'); expect(app.client.spec!.method, 'PATCH');
expect(app.client.spec.headers['content-type'], expect(app.client.spec!.headers['content-type'],
startsWith('application/json')); startsWith('application/json'));
expect(app.client.spec.path, '/api/todos/sleep'); expect(app.client.spec!.path, '/api/todos/sleep');
expect(await read(app.client.spec.request.finalize()), '{}'); expect(await read(app.client.spec!.request.finalize()), '{}');
}); });
test('update', () async { test('update', () async {
await todoService.update('sleep', {}); await todoService.update('sleep', {});
expect(app.client.spec.method, 'POST'); expect(app.client.spec!.method, 'POST');
expect(app.client.spec.headers['content-type'], expect(app.client.spec!.headers['content-type'],
startsWith('application/json')); startsWith('application/json'));
expect(app.client.spec.path, '/api/todos/sleep'); expect(app.client.spec!.path, '/api/todos/sleep');
expect(await read(app.client.spec.request.finalize()), '{}'); expect(await read(app.client.spec!.request.finalize()), '{}');
}); });
test('remove', () async { test('remove', () async {
await todoService.remove('sleep'); await todoService.remove('sleep');
expect(app.client.spec.method, 'DELETE'); expect(app.client.spec!.method, 'DELETE');
expect(app.client.spec.path, '/api/todos/sleep'); expect(app.client.spec!.path, '/api/todos/sleep');
}); });
}); });
group('authentication', () { group('authentication', () {
test('no type defaults to token', () async { test('no type defaults to token', () async {
await app.authenticate(credentials: '<jwt>'); await app.authenticate(credentials: '<jwt>');
expect(app.client.spec.path, '/auth/token'); expect(app.client.spec!.path, '/auth/token');
}); });
test('sets type', () async { test('sets type', () async {
await app.authenticate(type: 'local'); 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 { test('credentials send right body', () async {
await app await app
.authenticate(type: 'local', credentials: {'username': 'password'}); .authenticate(type: 'local', credentials: {'username': 'password'});
expect( expect(
await read(app.client.spec.request.finalize()), await read(app.client.spec!.request.finalize()),
json.encode({'username': 'password'}), json.encode({'username': 'password'}),
); );
}); });

View file

@ -1,5 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:angel_client/base_angel_client.dart'; import 'package:angel3_client/base_angel_client.dart';
import 'dart:convert'; import 'dart:convert';
import 'package:http/src/base_client.dart' as http; import 'package:http/src/base_client.dart' as http;
import 'package:http/src/base_request.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 { class MockAngel extends BaseAngelClient {
@override @override
final SpecClient client = new SpecClient(); final SpecClient client = SpecClient();
MockAngel() : super(null, 'http://localhost:3000'); MockAngel() : super(null, 'http://localhost:3000');
@override @override
authenticateViaPopup(String url, {String eventName = 'token'}) { Stream<String> authenticateViaPopup(String url,
throw new UnsupportedError('Nope'); {String eventName = 'token'}) {
throw UnsupportedError('Nope');
} }
} }
class SpecClient extends http.BaseClient { class SpecClient extends http.BaseClient {
Spec _spec; Spec? _spec;
Spec get spec => _spec; Spec? get spec => _spec;
@override @override
send(http.BaseRequest request) { Future<http.StreamedResponse> send(http.BaseRequest request) {
_spec = new Spec(request, request.method, request.url.path, request.headers, _spec = Spec(request, request.method, request.url.path, request.headers,
request.contentLength); request.contentLength);
dynamic data = {'text': 'Clean your room!', 'completed': true}; dynamic data = {'text': 'Clean your room!', 'completed': true};
@ -40,8 +41,8 @@ class SpecClient extends http.BaseClient {
data = [data]; data = [data];
} }
return new Future<http.StreamedResponse>.value(new http.StreamedResponse( return Future<http.StreamedResponse>.value(http.StreamedResponse(
new Stream<List<int>>.fromIterable([utf8.encode(json.encode(data))]), Stream<List<int>>.fromIterable([utf8.encode(json.encode(data))]),
200, 200,
headers: { headers: {
'content-type': 'application/json', 'content-type': 'application/json',
@ -54,7 +55,7 @@ class Spec {
final http.BaseRequest request; final http.BaseRequest request;
final String method, path; final String method, path;
final Map<String, String> headers; final Map<String, String> headers;
final int contentLength; final int? contentLength;
Spec(this.request, this.method, this.path, this.headers, this.contentLength); Spec(this.request, this.method, this.path, this.headers, this.contentLength);

View file

@ -1,27 +1,27 @@
import 'package:async/async.dart'; import 'package:async/async.dart';
import 'dart:io'; import 'dart:io';
import 'package:angel_client/io.dart' as c; import 'package:angel3_client/io.dart' as c;
import 'package:angel_framework/angel_framework.dart' as s; import 'package:angel3_framework/angel3_framework.dart' as s;
import 'package:angel_framework/http.dart' as s; import 'package:angel3_framework/http.dart' as s;
import 'package:pedantic/pedantic.dart'; import 'package:pedantic/pedantic.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
main() { void main() {
HttpServer server; late HttpServer server;
c.Angel app; late c.Angel app;
c.ServiceList list; late c.ServiceList list;
StreamQueue queue; late StreamQueue queue;
setUp(() async { setUp(() async {
var serverApp = new s.Angel(); var serverApp = s.Angel();
var http = new s.AngelHttp(serverApp); var http = s.AngelHttp(serverApp);
serverApp.use('/api/todos', new s.MapService(autoIdAndDateFields: false)); serverApp.use('/api/todos', s.MapService(autoIdAndDateFields: false));
server = await http.startServer(); server = await http.startServer();
var uri = 'http://${server.address.address}:${server.port}'; var uri = 'http://${server.address.address}:${server.port}';
app = new c.Rest(uri); app = c.Rest(uri);
list = new c.ServiceList(app.service('api/todos')); list = c.ServiceList(app.service('api/todos'));
queue = new StreamQueue(list.onChange); queue = StreamQueue(list.onChange);
}); });
tearDown(() async { tearDown(() async {

View file

@ -1,14 +1,14 @@
import 'package:angel_model/angel_model.dart'; import 'package:angel3_model/angel3_model.dart';
class Postcard extends Model { class Postcard extends Model {
String location; String? location;
String message; String? message;
Postcard({String id, this.location, this.message}) { Postcard({String? id, this.location, this.message}) {
this.id = id; this.id = id;
} }
factory Postcard.fromJson(Map data) => new Postcard( factory Postcard.fromJson(Map data) => Postcard(
id: data['id'].toString(), id: data['id'].toString(),
location: data['location'].toString(), location: data['location'].toString(),
message: data['message'].toString()); message: data['message'].toString());

View file

@ -1,8 +1,8 @@
import 'dart:html'; import 'dart:html';
import 'package:angel_client/browser.dart'; import 'package:angel3_client/browser.dart';
/// Dummy app to ensure client works with DDC. /// Dummy app to ensure client works with DDC.
main() { void main() {
var app = new Rest(window.location.origin); var app = Rest(window.location.origin);
window.alert(app.baseUrl.toString()); window.alert(app.baseUrl.toString());
} }

71
packages/code_buffer/.gitignore vendored Normal file
View 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

View file

@ -0,0 +1 @@
language: dart

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

View 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()`.

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

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

View file

@ -0,0 +1,3 @@
analyzer:
strong-mode:
implicit-casts: false

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

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

View 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

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

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

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

View file

@ -0,0 +1,4 @@
language: dart
dart:
- stable
- dev

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

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

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

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

View file

@ -0,0 +1,4 @@
analyzer:
strong-mode:
implicit-casts: false
#implicit-dynamic: false

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

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

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

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

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

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

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

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

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

View file

@ -0,0 +1,2 @@
export 'src/combinator/combinator.dart';
export 'src/error.dart';

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View 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