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
# User-specific stuff:
.idea/workspace.xml
.idea/tasks.xml
.idea/dictionaries
.idea/vcs.xml
.idea/jsLibraryMappings.xml
# Sensitive or high-churn files:
.idea/dataSources.ids
.idea/dataSources.xml
.idea/dataSources.local.xml
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml
## VsCode
.vscode/
#.vscode/*
#!.vscode/settings.json
#!.vscode/tasks.json
#!.vscode/launch.json
#!.vscode/extensions.json
# IntelliJ
.idea/
/out/
.idea_modules/
# Gradle:
.idea/gradle.xml
.idea/libraries
# Mongo Explorer plugin:
.idea/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
@ -79,13 +71,7 @@ crashlytics.properties
crashlytics-build.properties
fabric.properties
### VSCode template
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# Others
logs/
*.pem
.DS_Store

12
AUTHORS.md Normal file
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)
* Published all packages with `angel3_` prefix
* Changed Dart SDK requirements for all packages to ">=2.12.0 <3.0.0" to support NNBD.
* Updated pretty_logging to 2.0.0
* Updated angel_http_exception to 2.0.0
* Updated angel_cli to 3.0.0. (Rename not working)
* Migrated pretty_logging to 3.0.0 (0/0 tests passed)
* Migrated angel_http_exception to 3.0.0 (0/0 tests passed)
* Moved angel_cli to https://github.com/dukefirehawk/cli (Not migrated yet)
* Added code_buffer and migrated to 2.0.0 (16/16 tests passed)
* Added combinator and migrated to 2.0.0 (16/16 tests passed)
* Migrated angel_route to 5.0.0 (35/35 tests passed)
* Migrated angel_model to 3.0.0 (0/0 tests passed)
* Migrated angel_container to 3.0.0 (55/55 tests passed)
* Added merge_map and migrated to 2.0.0 (6/6 tests passed)
* Added mock_request and migrated to 2.0.0 (0/0 tests)
* Migrated angel_framework to 4.0.0 (146/150 tests passed)
* Migrated angel_auth to 4.0.0 (23/30 tests passed)
* Migrated angel_configuration to 4.0.0 (6/8 testspassed)
* Migrated angel_validate to 4.0.0 (6/7 tests passed)
* Migrated json_god to 4.0.0 (13/13 tests passed)
* Migrated angel_client to 4.0.0 (6/13 tests passed)
* Migrated angel_websocket to 4.0.0 (2/3 tests passed)
* Migrated angel_test to 4.0.0 (1/1 test passed)
* Added symbol_table and migrated to 2.0.0 (16/16 tests passed)
* Migrated jael to 4.0.0 (20/20 tests passed)
* Migrated jael_preprocessor to 3.0.0 (5/5 tests passed)
* Migrated angel_jael to 4.0.0 (1/1 test passed)
* Migrated pub_sub to 4.0.0 (16/16 tests passed)
* Migrated production to 3.0.0 (0/0 tests passed)
* Added html_builder and migrated to 2.0.0 (1/1 tests passed)
* Migrated hot to 4.0.0 (0/0 tests passed)
* Added range_header and migrated to 3.0.0 (12/12 tests passed)
* Migrated static to 4.0.0 (11/12 test passed)
* Created basic-sdk-2.12.x_nnbd template (1/1 test passed) <= Milestone 1
* Migrated angel_serialize to 4.0.0 (0/0 test passed)
* Migrated angel_serialize_generator to 4.0.0 (33/33 tests passed)
* Migrated angel_orm to 3.0.0 (0/0 tests passed)
* Migrated angel_migration to 3.0.0 (0/0 tests passed)
* Added inflection2 and migrated to 1.0.0 (28/32 tests passed)
* Migrated angel_orm_generator to 4.0.0 (0/0 tests passed)
* Migrated angel_migration_runner to 3.0.0 (0/0 tests passed)
* Migrated angel_orm_test to 3.0.0 (0/0 tests passed)
* Migrated angel_orm_postgres to 3.0.0 (51/54 tests passed)
* Create orm-sdk-2.12.x boilerplate (in progress) <= Milestone 2
# 3.0.0 (Non NNBD)
* Changed Dart SDK requirements for all packages to ">=2.10.0 <3.0.0"
* Updated pretty_logging to 2.0.0
* Updated angel_http_exception to 2.0.0
* Updated pretty_logging to 2.0.0 (0/0 tests passed)
* Updated angel_http_exception to 2.0.0 (0/0 tests passed)
* Updated angel_cli to 3.0.0. (Rename not working)
* Updated angel_route to 4.0.0
* Updated angel_model to 2.0.0
* Updated angel_container to 2.0.0
* Updated angel_framework to 3.0.0
* Updated angel_auth to 3.0.0
* Updated angel_configuration to 3.0.0
* Updated jael to 3.0.0
* Updated jael_preprocessor to 3.0.0
* Updated validate to 3.0.0
* Added and updated json_god to 3.0.0
* Updated angel_client to 3.0.0
* Updated angel_route to 4.0.0 (35/35 tests passed)
* Updated angel_model to 2.0.0 (0/0 tests passed)
* Updated angel_container to 2.0.0 (55/55 tests passed)
* Updated angel_framework to 3.0.0 (151/151 tests passed)
* Updated angel_auth to 3.0.0 (28/32 tests passed)
* Updated angel_configuration to 3.0.0 (6/8 tests passed)
* Updated angel_validate to 3.0.0 (7/7 tests passed)
* Added and updated json_god to 3.0.0 (7/7 tests passed)
* Updated angel_client to 3.0.0 (10/13 tests passed)
* Updated angel_websocket to 3.0.0 (3/3 tests passed)
* Updated test to 3.0.0
* Updated angel_jael to 3.0.0 (Issue with 2 dependencies)
* Added pub_sub and updated to 3.0.0
* Updated production to 2.0.0
* Updated hot to 3.0.0
* Updated static to 3.0.0
* Update basic-sdk-2.12.x boilerplate
* Updated angel_serialize to 3.0.0
* Updated angel_serialize_generator to 3.0.0
* Updated angel_orm to 3.0.0
* Updated angel_migration to 3.0.0
* Updated angel_orm_generator to 3.0.0 (use a fork of postgres)
* Updated angel_migration_runner to 3.0.0
* Updated angel_orm_test to 1.0.0
* Updated angel_orm_postgres to 2.0.0
* Updated jael to 3.0.0 (20/20 tests passed)
* Updated jael_preprocessor to 3.0.0 (5/5 tests passed)
* Updated test to 3.0.0 (1/1 tests passed)
* Updated angel_jael to 3.0.0 (1/1 tests passed, Issue with 2 dependencies)
* Added pub_sub and updated to 3.0.0 (16/16 tests passed)
* Updated production to 2.0.0 (0/0 tests passed)
* Updated hot to 3.0.0 (0/0 tests passed)
* Updated static to 3.0.0 (12/12 tests passed)
* Update basic-sdk-2.12.x boilerplate (1/1 tests passed)
* Updated angel_serialize to 3.0.0 (0/0 tests passed)
* Updated angel_serialize_generator to 3.0.0 (33/33 tests passed)
* Updated angel_orm to 3.0.0 (0/0 tests passed)
* Updated angel_migration to 3.0.0 (0/0 tests passed)
* Updated angel_orm_generator to 3.0.0 (0/0 tests passed, use a fork of postgres)
* Updated angel_migration_runner to 3.0.0 (0/0 tests passed)
* Updated angel_orm_test to 1.0.0 (0/0 tests passed)
* Updated angel_orm_postgres to 2.0.0 (52/54 tests passed)
* Update orm-sdk-2.12.x boilerplate
* Updated angel_auth_oauth2 to 3.0.0
* Updated angel_auth_cache to 3.0.0

View file

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2016 angel-dart
Copyright (c) 2021 dukefirehawk.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -1,64 +1,53 @@
[![The Angel Framework](https://angel-dart.github.io/assets/images/logo.png)](https://angel-dart.dev)
[![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/angel_dart/discussion)
[![Pub](https://img.shields.io/pub/v/angel_framework.svg)](https://pub.dartlang.org/packages/angel_framework)
[![Build status](https://travis-ci.org/angel-dart/framework.svg?branch=master)](https://travis-ci.org/angel-dart/framework)
![License](https://img.shields.io/github/license/angel-dart/framework.svg)
[![version](https://img.shields.io/badge/pub-v4.0.1-brightgreen)](https://pub.dartlang.org/packages/framework)
**A polished, production-ready backend framework in Dart.**
[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/LICENSE)
**A polished, production-ready backend framework in Dart with NNBD support.**
-----
## About
Angel is a full-stack Web framework in Dart. It aims to
streamline development by providing many common features
out-of-the-box in a consistent manner.
Angel3 is a port of the original Angel framework to support NNBD in Dart SDK 2.12.x and above.
It is a full-stack Web framework in Dart that aims to streamline development by providing many common features out-of-the-box in a consistent manner. One of the main goal is to enable developers to build both frontend
and backend in the same language, Dart. Angel3 framework is designed as a collection of plugins that enable developers to pick and choose the parts needed for their projects. A series of starter templates are also provided for quick start and trial run with Angel3 framework.
With features like the following, Angel is the all-in-one framework you should choose to build your next project:
* GraphQL Support
* PostgreSQL ORM
* Dependency Injection
The availabe features in Angel3 are:
* Static File Handling
* Basic Authentication
* PostgreSQL ORM
* And much more...
See all the packages in the `packages/` directory.
## IMPORTANT NOTES
This is a port of Angel Framework to work with Dart SDK 2.12.x and above. Dart SDK 2.12.x and below are not supported.
The migration of Angel Framework to Angel3 framework is still ongoing. About 35 out of 70++ packages have been migrated and tested to be stable and working as expected. Angel3 framework need more testing to get it to production quality. Hence, the Angel3 stable packages have been published with prefix `angel3_` on `pub.dev`for developers to try out.
In order to acknowledge contributions, AUTHORS.md has been added to every Angel3 packages. This way no matter what the contributions are, be it code review, testing or submit PR, can all be recorded in this file. If you are the original author of the original Angel packages, feel free to send a PR to update that file.
Branch: master
- Same as sdk-2.12.x branch
- Stable version of `angel3` branch
Branch: sdk-2.12.x
- Required Dart SDK: ">=2.10.0 <3.0.0"
- NNBD Support: No
- Status: Beta release
- Notes: Not all packages are fully tested. Refer to WIKI page for details. The basic and ORM templates can be found at "https://github.com/dukefirehawk/boilerplates/tree/basic-sdk-2.12.x" and "https://github.com/dukefirehawk/boilerplates/tree/orm-sdk-2.12.x" respectively.
Branch: angel3 (Active development)
- Dart version : 2.12.x and above. Use sdk: ">=2.12.0 <3.0.0"
- Publish : Yes. See all packages with `angel3_` prefix on [pub.dev](https://pub.dev/publishers/dukefirehawk.com/packages).
- NNDB Support : Yes
- Status : Beta
- Notes : Basic and ORM templates are working with the key packages migration completed. Not all packages are fully tested.
Branch: sdk-2.12.x_nnbd
- Required Dart SDK: ">=2.12.0 <3.0.0"
- NNBD Support: Yes
- Status: Alpha release
- Notes: Heavy migration and code refactoring in progress. Refer to WIKI page for details. The basic template can be found at https://github.com/dukefirehawk/boilerplates/tree/basic-sdk-2.12.x_nnbd".
Branch: sdk-2.12.x-nnbd (Active development)
- Dart version : 2.12.x and above. Use sdk: ">=2.12.0 <3.0.0"
- Publish : No (Internal use only)
- NNDB Support : Yes
- Status : Beta
- Notes : Basic and ORM templates are working with key packages migration. Not all packages are fully tested.
Branch: sdk-2.10.x
- Required Dart SDK: ">=2.10.0 <2.12.0"
- NNBD support: No
- Status: Retired
- Notes: Not all packages are fully tested. This branch is the baseline used in migrating the framework to support Dart SDK 2.12.x and beyond. It may still work with Dart SDK 2.10.x but no longer maintained. Do not work with Dart SDK < 2.10.x.
For more details, checkout [Project Status](https://github.com/dukefirehawk/angel/wiki/Project-Status)
### Testing Angel Framework in NNBD mode
Creating new project
1. Clone `https://github.com/dukefirehawk/boilerplates/tree/basic-sdk-2.12.x_nnbd` project.
Migrating an existing project to Angel NNBD
1. WARNING. Backup your existing code first as the following migration process cannot be reversed.
2. Run `dart pub outdated --mode=null-safety`. Make sure all the packages besides "angel_*" are upgradable.
3. Update all "angel_*" packages with dependencies in `https://github.com/dukefirehawk/boilerplates/tree/basic-sdk-2.12.x_nnbd/pubspec.yaml` file. Refer to WIKI on the migrated Angel NNBD packages.
4. Run `dart pub upgrade --null-safety`.
5. Run `dart migrate` to perform the migration.
6. Fix and resolve NNDB related warnings and errors.
## Installation & Setup
## Installation & Setup
Once you have [Dart](https://www.dartlang.org/) installed, bootstrapping a project is as simple as running a few shell commands:
@ -88,6 +77,10 @@ pub global activate --source path ./packages/cli
Next, check out the [detailed documentation](https://docs.angel-dart.dev/v/2.x) to learn to flesh out your project.
### Migrating to Angel3 Framework
Checkout [Migrating from Angel to Angel3](https://github.com/dukefirehawk/angel/wiki/Migrating-from-Angel-to-Angel3)
## Examples and Documentation
Visit the [documentation](https://docs.angel-dart.dev/v/2.x)
for dozens of guides and resources, including video tutorials,

View file

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

71
packages/.gitignore vendored Normal file
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
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
.buildlog
.packages
# SDK 1.20 and later (no longer creates packages directories)
# Older SDK versions
# (Include if the minimum SDK version specified in pubsepc.yaml is earlier than 1.20)
.project
.pub/
build/
.buildlog
**/packages/
# Files created by dart2js
# (Most Dart developers will use pub build to compile Dart, use/modify these
# rules if you intend to use dart2js directly
@ -22,36 +39,17 @@ build/
*.info.json
# Directory created by dartdoc
doc/api/
# Don't commit pubspec lock file
# (Library packages only! Remove pattern if developing an application package)
pubspec.lock
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/workspace.xml
.idea/tasks.xml
.idea/dictionaries
.idea/vcs.xml
.idea/jsLibraryMappings.xml
# Sensitive or high-churn files:
.idea/dataSources.ids
.idea/dataSources.xml
.idea/dataSources.local.xml
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml
# Gradle:
.idea/gradle.xml
.idea/libraries
# Mongo Explorer plugin:
.idea/mongoSettings.xml
## VsCode
.vscode/
## File-based project format:
*.iws
@ -59,9 +57,8 @@ pubspec.lock
## Plugin-specific files:
# IntelliJ
.idea/
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
@ -72,5 +69,3 @@ com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
.dart_tool

12
packages/auth/AUTHORS.md Normal file
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
* 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
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)
[![build status](https://travis-ci.org/angel-dart/auth.svg?branch=master)](https://travis-ci.org/angel-dart/auth)
[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/angel3/packages/auth/LICENSE)
A complete authentication plugin for Angel. Inspired by Passport.
@ -76,7 +78,7 @@ configureServer(Angel app) async {
```
This renders a simple HTML page that fires the user's JWT as a `token` event in `window.opener`.
`angel_client` [exposes this as a Stream](https://github.com/angel-dart/client#authentication):
`angel_client` [exposes this as a Stream](https://github.com/dukefirehawk/angel/tree/angel3/packages/client#authentication):
```dart
app.authenticateViaPopup('/auth/google').listen((jwt) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import 'dart:convert';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel3_framework/angel3_framework.dart';
import 'package:http_parser/http_parser.dart';
import 'options.dart';

View file

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

View file

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

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

View file

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

View file

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

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';
void main() {
@ -70,6 +70,7 @@ void main() {
);
});
/* Deprecated as clientId and clientSecret cannot be null
test('ensures id not null', () {
expect(
() => ExternalAuthOptions(
@ -89,6 +90,7 @@ void main() {
throwsArgumentError,
);
});
*/
});
group('fromMap()', () {

View file

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

View file

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

View file

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

View file

@ -1,11 +1,10 @@
name: "angel_auth_twitter"
#author: "Tobe O <thosakwe@gmail.com>"
version: 3.0.0
description: "package:angel_auth strategy for Twitter login. Auto-signs requests."
homepage: "https://github.com/angel-dart/auth_twitter.git"
publish_to: none
environment:
sdk: ">=2.10.0 <3.0.0"
homepage: "https://github.com/angel-dart/auth_twitter.git"
version: 3.0.0
publish_to: none
dependencies:
angel_auth:
git:

2
packages/cli/AUTHORS.md Normal file
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
* Fix a bug where new directories were not being created in
`init`.

View file

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

View file

@ -1,15 +1,34 @@
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
.buildlog
.dart_tool
.packages
.project
.pub/
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/
### Dart template
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
# SDK 1.20 and later (no longer creates packages directories)
# Older SDK versions
# (Include if the minimum SDK version specified in pubsepc.yaml is earlier than 1.20)
.project
.buildlog
**/packages/
# Files created by dart2js
# (Most Dart developers will use pub build to compile Dart, use/modify these
# (Most Dart developers will use pub build to compile Dart, use/modify these
# rules if you intend to use dart2js directly
# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
# differentiate from explicit Javascript files)
@ -20,62 +39,33 @@ build/
*.info.json
# Directory created by dartdoc
doc/api/
# Don't commit pubspec lock file
# Don't commit pubspec lock file
# (Library packages only! Remove pattern if developing an application package)
pubspec.lock
.idea
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
lib/angel_client.js
*.sum
# User-specific stuff:
# Logs
logs
*.log
npm-debug.log*
## VsCode
.vscode/
# Runtime data
pids
*.pid
*.seed
*.pid.lock
## File-based project format:
*.iws
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
## Plugin-specific files:
# Coverage directory used by tools like istanbul
coverage
# IntelliJ
.idea/
/out/
.idea_modules/
# nyc test coverage
.nyc_output
# JIRA plugin
atlassian-ide-plugin.xml
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules
jspm_packages
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
.dart_tool
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

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
* `_join` previously discarded quer parameters, etc.
* 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
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)
[![build status](https://travis-ci.org/angel-dart/client.svg)](https://travis-ci.org/angel-dart/client)
Client library for the Angel framework.
This library provides virtually the same API as an Angel server.
The client can run in the browser, in Flutter, or on the command-line.
In addition, the client supports `angel_auth` authentication.
[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/angel3/packages/client/LICENSE)
# Usage
```dart
// Choose one or the other, depending on platform
import 'package:angel_client/io.dart';
import 'package:angel_client/browser.dart';
import 'package:angel_client/flutter.dart';
import 'package:angel3_client/io.dart';
import 'package:angel3_client/browser.dart';
import 'package:angel3_client/flutter.dart';
main() async {
Angel app = new Rest("http://localhost:3000");
Angel app = Rest("http://localhost:3000");
}
```
@ -33,7 +30,7 @@ foo() async {
}
```
The CLI client also supports reflection via `json_god`. There is no need to work with Maps;
The CLI client also supports reflection via `angel3_json_god`. There is no need to work with Maps;
you can use the same class on the client and the server.
```dart
@ -96,9 +93,9 @@ Use `ServiceList` for this case:
```dart
build(BuildContext context) async {
var list = new ServiceList(app.service('api/todos'));
var list = ServiceList(app.service('api/todos'));
return new StreamBuilder(
return StreamBuilder(
stream: list.onChange,
builder: _yourBuildFunction,
);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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