diff --git a/.gitignore b/.gitignore index 979f87c8..74215e2a 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 00000000..ac95ab58 --- /dev/null +++ b/AUTHORS.md @@ -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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 2079b327..8194a521 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,41 +1,78 @@ # 4.0.0 (NNBD) +* Published all packages with `angel3_` prefix * Changed Dart SDK requirements for all packages to ">=2.12.0 <3.0.0" to support NNBD. -* Updated pretty_logging to 2.0.0 -* Updated angel_http_exception to 2.0.0 -* Updated angel_cli to 3.0.0. (Rename not working) +* Migrated pretty_logging to 3.0.0 (0/0 tests passed) +* Migrated angel_http_exception to 3.0.0 (0/0 tests passed) +* Moved angel_cli to https://github.com/dukefirehawk/cli (Not migrated yet) +* Added code_buffer and migrated to 2.0.0 (16/16 tests passed) +* Added combinator and migrated to 2.0.0 (16/16 tests passed) +* Migrated angel_route to 5.0.0 (35/35 tests passed) +* Migrated angel_model to 3.0.0 (0/0 tests passed) +* Migrated angel_container to 3.0.0 (55/55 tests passed) +* Added merge_map and migrated to 2.0.0 (6/6 tests passed) +* Added mock_request and migrated to 2.0.0 (0/0 tests) +* Migrated angel_framework to 4.0.0 (146/150 tests passed) +* Migrated angel_auth to 4.0.0 (23/30 tests passed) +* Migrated angel_configuration to 4.0.0 (6/8 testspassed) +* Migrated angel_validate to 4.0.0 (6/7 tests passed) +* Migrated json_god to 4.0.0 (13/13 tests passed) +* Migrated angel_client to 4.0.0 (6/13 tests passed) +* Migrated angel_websocket to 4.0.0 (2/3 tests passed) +* Migrated angel_test to 4.0.0 (1/1 test passed) +* Added symbol_table and migrated to 2.0.0 (16/16 tests passed) +* Migrated jael to 4.0.0 (20/20 tests passed) +* Migrated jael_preprocessor to 3.0.0 (5/5 tests passed) +* Migrated angel_jael to 4.0.0 (1/1 test passed) +* Migrated pub_sub to 4.0.0 (16/16 tests passed) +* Migrated production to 3.0.0 (0/0 tests passed) +* Added html_builder and migrated to 2.0.0 (1/1 tests passed) +* Migrated hot to 4.0.0 (0/0 tests passed) +* Added range_header and migrated to 3.0.0 (12/12 tests passed) +* Migrated static to 4.0.0 (11/12 test passed) +* Created basic-sdk-2.12.x_nnbd template (1/1 test passed) <= Milestone 1 +* Migrated angel_serialize to 4.0.0 (0/0 test passed) +* Migrated angel_serialize_generator to 4.0.0 (33/33 tests passed) +* Migrated angel_orm to 3.0.0 (0/0 tests passed) +* Migrated angel_migration to 3.0.0 (0/0 tests passed) +* Added inflection2 and migrated to 1.0.0 (28/32 tests passed) +* Migrated angel_orm_generator to 4.0.0 (0/0 tests passed) +* Migrated angel_migration_runner to 3.0.0 (0/0 tests passed) +* Migrated angel_orm_test to 3.0.0 (0/0 tests passed) +* Migrated angel_orm_postgres to 3.0.0 (51/54 tests passed) +* Create orm-sdk-2.12.x boilerplate (in progress) <= Milestone 2 # 3.0.0 (Non NNBD) * Changed Dart SDK requirements for all packages to ">=2.10.0 <3.0.0" -* Updated pretty_logging to 2.0.0 -* Updated angel_http_exception to 2.0.0 +* Updated pretty_logging to 2.0.0 (0/0 tests passed) +* Updated angel_http_exception to 2.0.0 (0/0 tests passed) * Updated angel_cli to 3.0.0. (Rename not working) -* Updated angel_route to 4.0.0 -* Updated angel_model to 2.0.0 -* Updated angel_container to 2.0.0 -* Updated angel_framework to 3.0.0 -* Updated angel_auth to 3.0.0 -* Updated angel_configuration to 3.0.0 -* Updated jael to 3.0.0 -* Updated jael_preprocessor to 3.0.0 -* Updated validate to 3.0.0 -* Added and updated json_god to 3.0.0 -* Updated angel_client to 3.0.0 +* Updated angel_route to 4.0.0 (35/35 tests passed) +* Updated angel_model to 2.0.0 (0/0 tests passed) +* Updated angel_container to 2.0.0 (55/55 tests passed) +* Updated angel_framework to 3.0.0 (151/151 tests passed) +* Updated angel_auth to 3.0.0 (28/32 tests passed) +* Updated angel_configuration to 3.0.0 (6/8 tests passed) +* Updated angel_validate to 3.0.0 (7/7 tests passed) +* Added and updated json_god to 3.0.0 (7/7 tests passed) +* Updated angel_client to 3.0.0 (10/13 tests passed) * Updated angel_websocket to 3.0.0 (3/3 tests passed) -* Updated test to 3.0.0 -* Updated angel_jael to 3.0.0 (Issue with 2 dependencies) -* Added pub_sub and updated to 3.0.0 -* Updated production to 2.0.0 -* Updated hot to 3.0.0 -* Updated static to 3.0.0 -* Update basic-sdk-2.12.x boilerplate -* Updated angel_serialize to 3.0.0 -* Updated angel_serialize_generator to 3.0.0 -* Updated angel_orm to 3.0.0 -* Updated angel_migration to 3.0.0 -* Updated angel_orm_generator to 3.0.0 (use a fork of postgres) -* Updated angel_migration_runner to 3.0.0 -* Updated angel_orm_test to 1.0.0 -* Updated angel_orm_postgres to 2.0.0 +* Updated jael to 3.0.0 (20/20 tests passed) +* Updated jael_preprocessor to 3.0.0 (5/5 tests passed) +* Updated test to 3.0.0 (1/1 tests passed) +* Updated angel_jael to 3.0.0 (1/1 tests passed, Issue with 2 dependencies) +* Added pub_sub and updated to 3.0.0 (16/16 tests passed) +* Updated production to 2.0.0 (0/0 tests passed) +* Updated hot to 3.0.0 (0/0 tests passed) +* Updated static to 3.0.0 (12/12 tests passed) +* Update basic-sdk-2.12.x boilerplate (1/1 tests passed) +* Updated angel_serialize to 3.0.0 (0/0 tests passed) +* Updated angel_serialize_generator to 3.0.0 (33/33 tests passed) +* Updated angel_orm to 3.0.0 (0/0 tests passed) +* Updated angel_migration to 3.0.0 (0/0 tests passed) +* Updated angel_orm_generator to 3.0.0 (0/0 tests passed, use a fork of postgres) +* Updated angel_migration_runner to 3.0.0 (0/0 tests passed) +* Updated angel_orm_test to 1.0.0 (0/0 tests passed) +* Updated angel_orm_postgres to 2.0.0 (52/54 tests passed) * Update orm-sdk-2.12.x boilerplate * Updated angel_auth_oauth2 to 3.0.0 * Updated angel_auth_cache to 3.0.0 diff --git a/LICENSE b/LICENSE index 52a1469a..ee2937dd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 angel-dart +Copyright (c) 2021 dukefirehawk.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 54e52144..1cdf6b9c 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,53 @@ [![The Angel Framework](https://angel-dart.github.io/assets/images/logo.png)](https://angel-dart.dev) [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/angel_dart/discussion) -[![Pub](https://img.shields.io/pub/v/angel_framework.svg)](https://pub.dartlang.org/packages/angel_framework) -[![Build status](https://travis-ci.org/angel-dart/framework.svg?branch=master)](https://travis-ci.org/angel-dart/framework) -![License](https://img.shields.io/github/license/angel-dart/framework.svg) +[![version](https://img.shields.io/badge/pub-v4.0.1-brightgreen)](https://pub.dartlang.org/packages/framework) -**A polished, production-ready backend framework in Dart.** +[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/LICENSE) + + +**A polished, production-ready backend framework in Dart with NNBD support.** ----- ## About -Angel is a full-stack Web framework in Dart. It aims to -streamline development by providing many common features -out-of-the-box in a consistent manner. +Angel3 is a port of the original Angel framework to support NNBD in Dart SDK 2.12.x and above. +It is a full-stack Web framework in Dart that aims to streamline development by providing many common features out-of-the-box in a consistent manner. One of the main goal is to enable developers to build both frontend +and backend in the same language, Dart. Angel3 framework is designed as a collection of plugins that enable developers to pick and choose the parts needed for their projects. A series of starter templates are also provided for quick start and trial run with Angel3 framework. -With features like the following, Angel is the all-in-one framework you should choose to build your next project: -* GraphQL Support -* PostgreSQL ORM -* Dependency Injection +The availabe features in Angel3 are: * Static File Handling +* Basic Authentication +* PostgreSQL ORM * And much more... See all the packages in the `packages/` directory. ## IMPORTANT NOTES -This is a port of Angel Framework to work with Dart SDK 2.12.x and above. Dart SDK 2.12.x and below are not supported. +The migration of Angel Framework to Angel3 framework is still ongoing. About 35 out of 70++ packages have been migrated and tested to be stable and working as expected. Angel3 framework need more testing to get it to production quality. Hence, the Angel3 stable packages have been published with prefix `angel3_` on `pub.dev`for developers to try out. + +In order to acknowledge contributions, AUTHORS.md has been added to every Angel3 packages. This way no matter what the contributions are, be it code review, testing or submit PR, can all be recorded in this file. If you are the original author of the original Angel packages, feel free to send a PR to update that file. + Branch: master -- Same as sdk-2.12.x branch +- Stable version of `angel3` branch -Branch: sdk-2.12.x -- Required Dart SDK: ">=2.10.0 <3.0.0" -- NNBD Support: No -- Status: Beta release -- Notes: Not all packages are fully tested. Refer to WIKI page for details. The basic and ORM templates can be found at "https://github.com/dukefirehawk/boilerplates/tree/basic-sdk-2.12.x" and "https://github.com/dukefirehawk/boilerplates/tree/orm-sdk-2.12.x" respectively. +Branch: angel3 (Active development) +- Dart version : 2.12.x and above. Use sdk: ">=2.12.0 <3.0.0" +- Publish : Yes. See all packages with `angel3_` prefix on [pub.dev](https://pub.dev/publishers/dukefirehawk.com/packages). +- NNDB Support : Yes +- Status : Beta +- Notes : Basic and ORM templates are working with the key packages migration completed. Not all packages are fully tested. -Branch: sdk-2.12.x_nnbd -- Required Dart SDK: ">=2.12.0 <3.0.0" -- NNBD Support: Yes -- Status: Alpha release -- Notes: Heavy migration and code refactoring in progress. Refer to WIKI page for details. The basic template can be found at https://github.com/dukefirehawk/boilerplates/tree/basic-sdk-2.12.x_nnbd". +Branch: sdk-2.12.x-nnbd (Active development) +- Dart version : 2.12.x and above. Use sdk: ">=2.12.0 <3.0.0" +- Publish : No (Internal use only) +- NNDB Support : Yes +- Status : Beta +- Notes : Basic and ORM templates are working with key packages migration. Not all packages are fully tested. -Branch: sdk-2.10.x -- Required Dart SDK: ">=2.10.0 <2.12.0" -- NNBD support: No -- Status: Retired -- Notes: Not all packages are fully tested. This branch is the baseline used in migrating the framework to support Dart SDK 2.12.x and beyond. It may still work with Dart SDK 2.10.x but no longer maintained. Do not work with Dart SDK < 2.10.x. +For more details, checkout [Project Status](https://github.com/dukefirehawk/angel/wiki/Project-Status) -### Testing Angel Framework in NNBD mode -Creating new project -1. Clone `https://github.com/dukefirehawk/boilerplates/tree/basic-sdk-2.12.x_nnbd` project. - -Migrating an existing project to Angel NNBD -1. WARNING. Backup your existing code first as the following migration process cannot be reversed. -2. Run `dart pub outdated --mode=null-safety`. Make sure all the packages besides "angel_*" are upgradable. -3. Update all "angel_*" packages with dependencies in `https://github.com/dukefirehawk/boilerplates/tree/basic-sdk-2.12.x_nnbd/pubspec.yaml` file. Refer to WIKI on the migrated Angel NNBD packages. -4. Run `dart pub upgrade --null-safety`. -5. Run `dart migrate` to perform the migration. -6. Fix and resolve NNDB related warnings and errors. - -## Installation & Setup +## Installation & Setup Once you have [Dart](https://www.dartlang.org/) installed, bootstrapping a project is as simple as running a few shell commands: @@ -88,6 +77,10 @@ pub global activate --source path ./packages/cli Next, check out the [detailed documentation](https://docs.angel-dart.dev/v/2.x) to learn to flesh out your project. +### Migrating to Angel3 Framework + +Checkout [Migrating from Angel to Angel3](https://github.com/dukefirehawk/angel/wiki/Migrating-from-Angel-to-Angel3) + ## Examples and Documentation Visit the [documentation](https://docs.angel-dart.dev/v/2.x) for dozens of guides and resources, including video tutorials, diff --git a/TODO.md b/TODO.md index d1656546..8d0374c9 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,5 @@ -# Todo - +### angel_framework +* Migrate http_server to shelf ### Container/angel_container_generator * test/reflector_test.reflectab.dart - Changed ImplicitGetterMirrorImpl() from 5 to 3 parameters (revisit later) diff --git a/packages/.gitignore b/packages/.gitignore new file mode 100644 index 00000000..24d68312 --- /dev/null +++ b/packages/.gitignore @@ -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 diff --git a/packages/AUTHORS.md b/packages/AUTHORS.md new file mode 100644 index 00000000..ac95ab58 --- /dev/null +++ b/packages/AUTHORS.md @@ -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. diff --git a/packages/LICENSE b/packages/LICENSE new file mode 100644 index 00000000..b593ac86 --- /dev/null +++ b/packages/LICENSE @@ -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. diff --git a/packages/auth/.gitignore b/packages/auth/.gitignore index 356abd9d..24d68312 100644 --- a/packages/auth/.gitignore +++ b/packages/auth/.gitignore @@ -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 \ No newline at end of file diff --git a/packages/auth/AUTHORS.md b/packages/auth/AUTHORS.md new file mode 100644 index 00000000..ac95ab58 --- /dev/null +++ b/packages/auth/AUTHORS.md @@ -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. diff --git a/packages/auth/CHANGELOG.md b/packages/auth/CHANGELOG.md index 4d07992f..ff4829db 100644 --- a/packages/auth/CHANGELOG.md +++ b/packages/auth/CHANGELOG.md @@ -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. diff --git a/packages/auth/LICENSE b/packages/auth/LICENSE index eb4ce33e..b593ac86 100644 --- a/packages/auth/LICENSE +++ b/packages/auth/LICENSE @@ -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 diff --git a/packages/auth/README.md b/packages/auth/README.md index 4df0edd0..8a4cc876 100644 --- a/packages/auth/README.md +++ b/packages/auth/README.md @@ -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) { diff --git a/packages/auth/example/example.dart b/packages/auth/example/example.dart index 90e09f2b..89b8466e 100644 --- a/packages/auth/example/example.dart +++ b/packages/auth/example/example.dart @@ -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(); + var auth = AngelAuth(); - 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 fetchAUserByIdSomehow(id) async { diff --git a/packages/auth/lib/angel_auth.dart b/packages/auth/lib/angel3_auth.dart similarity index 92% rename from packages/auth/lib/angel_auth.dart rename to packages/auth/lib/angel3_auth.dart index f6476797..95d4fc9a 100644 --- a/packages/auth/lib/angel_auth.dart +++ b/packages/auth/lib/angel3_auth.dart @@ -1,4 +1,4 @@ -library angel_auth; +library angel3_auth; export 'src/middleware/require_auth.dart'; export 'src/strategies/strategies.dart'; diff --git a/packages/auth/lib/auth_token.dart b/packages/auth/lib/auth_token.dart index ff19d35e..b1db07a0 100644 --- a/packages/auth/lib/auth_token.dart +++ b/packages/auth/lib/auth_token.dart @@ -1,4 +1,4 @@ /// Stand-alone JWT library. -library angel_auth.auth_token; +library angel3_auth.auth_token; export 'src/auth_token.dart'; diff --git a/packages/auth/lib/src/auth_token.dart b/packages/auth/lib/src/auth_token.dart index 338303ef..3dddfac9 100644 --- a/packages/auth/lib/src/auth_token.dart +++ b/packages/auth/lib/src/auth_token.dart @@ -1,5 +1,5 @@ import 'dart:collection'; -import 'package:angel_framework/angel_framework.dart'; +import 'package:angel3_framework/angel3_framework.dart'; import 'dart:convert'; import 'package:crypto/crypto.dart'; @@ -26,10 +26,10 @@ String decodeBase64(String str) { class AuthToken { final SplayTreeMap _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 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? Function( + Map?, 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; + } } diff --git a/packages/auth/lib/src/configuration.dart b/packages/auth/lib/src/configuration.dart index 65cebb94..8d8b55ed 100644 --- a/packages/auth/lib/src/configuration.dart +++ b/packages/auth/lib/src/configuration.dart @@ -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 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 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 scopes}) { + Iterable 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; diff --git a/packages/auth/lib/src/middleware/require_auth.dart b/packages/auth/lib/src/middleware/require_auth.dart index 087ddb27..0c19c3fe 100644 --- a/packages/auth/lib/src/middleware/require_auth.dart +++ b/packages/auth/lib/src/middleware/require_auth.dart @@ -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({String realm}) { +RequestHandler forceBasicAuth({String? realm}) { return (RequestContext req, ResponseContext res) async { - if (req.container.has()) - return true; - else if (req.container.has>()) { - await req.container.makeAsync(); - return true; + if (req.container != null) { + var reqContainer = req.container!; + if (reqContainer.has()) { + return true; + } else if (reqContainer.has>()) { + await reqContainer.makeAsync(); + return true; + } } res.headers['www-authenticate'] = 'Basic realm="${realm ?? 'angel_auth'}"'; @@ -26,16 +29,23 @@ RequestHandler requireAuthentication() { if (throwError) { res.statusCode = 403; throw AngelHttpException.forbidden(); - } else + } else { return false; + } } - if (req.container.has() || req.method == 'OPTIONS') - return true; - else if (req.container.has>()) { - await req.container.makeAsync(); - return true; - } else + if (req.container != null) { + var reqContainer = req.container!; + if (reqContainer.has() || req.method == 'OPTIONS') { + return true; + } else if (reqContainer.has>()) { + await reqContainer.makeAsync(); + return true; + } else { + return _reject(res); + } + } else { return _reject(res); + } }; } diff --git a/packages/auth/lib/src/options.dart b/packages/auth/lib/src/options.dart index 6b9e9323..526aa1e2 100644 --- a/packages/auth/lib/src/options.dart +++ b/packages/auth/lib/src/options.dart @@ -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( +typedef AngelAuthTokenCallback = FutureOr Function( RequestContext req, ResponseContext res, AuthToken token, User user); class AngelAuthOptions { - AngelAuthCallback callback; - AngelAuthTokenCallback tokenCallback; - String successRedirect; - String failureRedirect; + AngelAuthCallback? callback; + AngelAuthTokenCallback? 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 { this.tokenCallback, this.canRespondWithJson = true, this.successRedirect, - String this.failureRedirect}); + this.failureRedirect}); } diff --git a/packages/auth/lib/src/plugin.dart b/packages/auth/lib/src/plugin.dart index 71b4b975..5103b300 100644 --- a/packages/auth/lib/src/plugin.dart +++ b/packages/auth/lib/src/plugin.dart @@ -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 { - Hmac _hs256; - int _jwtLifeSpan; + late Hmac _hs256; + late int _jwtLifeSpan; final StreamController _onLogin = StreamController(), _onLogout = StreamController(); - 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 { /// 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 { Map> 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 Function(Object) deserializer; + FutureOr Function(Object)? deserializer; /// Fires the result of [deserializer] whenever a user signs in to the application. Stream get onLogin => _onLogin.stream; @@ -65,25 +65,27 @@ class AngelAuth { String _randomString( {int length = 32, String validChars = - "ABCDEFHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"}) { + 'ABCDEFHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_'}) { var chars = []; - 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 { /// Configures an Angel server to decode and validate JSON Web tokens on demand, /// whenever an instance of [User] is injected. Future 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>()) { - app.container + if (!app.container!.has<_AuthResult>()) { + app.container! .registerLazySingleton>>((container) async { - var req = container.make(); - var res = container.make(); + var req = container.make()!; + var res = container.make()!; var result = await _decodeJwt(req, res); if (result != null) { return result; @@ -116,20 +121,19 @@ class AngelAuth { } }); - app.container.registerLazySingleton>((container) async { - var result = await container.makeAsync<_AuthResult>(); + app.container!.registerLazySingleton>((container) async { + var result = await container.makeAsync<_AuthResult>()!; return result.user; }); - app.container.registerLazySingleton>((container) async { - var result = await container.makeAsync<_AuthResult>(); + app.container! + .registerLazySingleton>((container) async { + var result = await container.makeAsync<_AuthResult>()!; 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 { } void _apply( - RequestContext req, ResponseContext res, AuthToken token, User user) { - if (!req.container.has()) { - req.container.registerSingleton(user); + RequestContext req, ResponseContext? res, AuthToken token, User user) { + if (!req.container!.has()) { + req.container!.registerSingleton(user); } - if (!req.container.has()) { - req.container.registerSingleton(token); + if (!req.container!.has()) { + req.container!.registerSingleton(token); } - if (allowCookie == true) { - _addProtectedCookie(res, 'token', token.serialize(_hs256)); + if (allowCookie) { + _addProtectedCookie(res!, 'token', token.serialize(_hs256)); } } @@ -174,7 +178,7 @@ class AngelAuth { /// ``` @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 { } } - Future<_AuthResult> _decodeJwt( + Future<_AuthResult?> _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 { } /// 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 { 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 { 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 { _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 { /// 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 options]) { + RequestHandler authenticate(type, [AngelAuthOptions? options]) { return (RequestContext req, ResponseContext res) async { - List names = []; + var names = []; 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 { 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(); + var hasExisting = req.container!.has(); var result = hasExisting - ? req.container.make() - : await strategy.authenticate(req, res, options); - if (result == true) + ? req.container!.make() + : 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 { var jwt = token.serialize(_hs256); if (options?.tokenCallback != null) { - if (!req.container.has()) { - req.container.registerSingleton(result); + if (!req.container!.has()) { + req.container!.registerSingleton(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 { } 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 { // 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 { /// 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 { /// 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 { } /// Log an authenticated user out. - RequestHandler logout([AngelAuthOptions options]) { + RequestHandler logout([AngelAuthOptions? options]) { return (RequestContext req, ResponseContext res) async { - if (req.container.has()) { - var user = req.container.make(); - _onLogout.add(user); + if (req.container?.has() == true) { + var user = req.container?.make(); + 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); } diff --git a/packages/auth/lib/src/popup_page.dart b/packages/auth/lib/src/popup_page.dart index 3ff336d4..cb6b3007 100644 --- a/packages/auth/lib/src/popup_page.dart +++ b/packages/auth/lib/src/popup_page.dart @@ -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'; diff --git a/packages/auth/lib/src/strategies/local.dart b/packages/auth/lib/src/strategies/local.dart index 21eeed2f..99cbf7da 100644 --- a/packages/auth/lib/src/strategies/local.dart +++ b/packages/auth/lib/src/strategies/local.dart @@ -1,18 +1,19 @@ import 'dart:async'; import 'dart:convert'; -import 'package:angel_framework/angel_framework.dart'; +import 'package:angel3_framework/angel3_framework.dart'; import '../options.dart'; import '../strategy.dart'; -bool _validateString(String str) => str != null && str.isNotEmpty; +bool _validateString(String? str) => str != null && str.isNotEmpty; /// Determines the validity of an incoming username and password. -typedef FutureOr LocalAuthVerifier( - String username, String password); +// typedef FutureOr LocalAuthVerifier(String? username, String? password); +typedef LocalAuthVerifier = FutureOr Function( + String? username, String? password); class LocalAuthStrategy extends AuthStrategy { - 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 verifier; String usernameField; @@ -23,35 +24,37 @@ class LocalAuthStrategy extends AuthStrategy { 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 authenticate(RequestContext req, ResponseContext res, - [AngelAuthOptions options_]) async { - AngelAuthOptions options = options_ ?? AngelAuthOptions(); - User verificationResult; + Future 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 extends AuthStrategy { .parseBody() .then((_) => req.bodyAsMap) .catchError((_) => {}); - 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(); diff --git a/packages/auth/lib/src/strategy.dart b/packages/auth/lib/src/strategy.dart index 72073686..d8fe6411 100644 --- a/packages/auth/lib/src/strategy.dart +++ b/packages/auth/lib/src/strategy.dart @@ -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 { /// Authenticates or rejects an incoming user. - FutureOr authenticate(RequestContext req, ResponseContext res, + FutureOr authenticate(RequestContext req, ResponseContext res, [AngelAuthOptions options]); } diff --git a/packages/auth/pubspec.yaml b/packages/auth/pubspec.yaml index ff3b8003..e217bbca 100644 --- a/packages/auth/pubspec.yaml +++ b/packages/auth/pubspec.yaml @@ -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 -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 diff --git a/packages/auth/test/auth_token_test.dart b/packages/auth/test/auth_token_test.dart index 7bdbf6b0..1171cac5 100644 --- a/packages/auth/test/auth_token_test.dart +++ b/packages/auth/test/auth_token_test.dart @@ -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, diff --git a/packages/auth/test/callback_test.dart b/packages/auth/test/callback_test.dart index 2506c12d..4c0de241 100644 --- a/packages/auth/test/callback_test.dart +++ b/packages/auth/test/callback_test.dart @@ -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 auth; - http.Client client; +void main() { + late Angel app; + late AngelHttp angelHttp; + AngelAuth 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(); - auth.serializer = (u) => u.id; + auth = AngelAuth(); + 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((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()) { - req.container.registerSingleton( + if (!req.container!.has()) { + req.container!.registerSingleton( 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}'); diff --git a/packages/auth/test/config_test.dart b/packages/auth/test/config_test.dart index e5b90ce8..cc13d30f 100644 --- a/packages/auth/test/config_test.dart +++ b/packages/auth/test/config_test.dart @@ -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()', () { diff --git a/packages/auth/test/local_test.dart b/packages/auth/test/local_test.dart index f0239e6e..ca73f1c0 100644 --- a/packages/auth/test/local_test.dart +++ b/packages/auth/test/local_test.dart @@ -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>( failureRedirect: '/failure', successRedirect: '/success'); Map sampleUser = {'hello': 'world'}; -Future> verifier(String username, String password) async { +Future> 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"')); }); } diff --git a/packages/auth/test/protect_cookie_test.dart b/packages/auth/test/protect_cookie_test.dart index 0d12f3d1..367fbf2e 100644 --- a/packages/auth/test/protect_cookie_test.dart +++ b/packages/auth/test/protect_cookie_test.dart @@ -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); }); diff --git a/packages/auth_twitter/example/main.dart b/packages/auth_twitter/example/main.dart index c4a5bce6..64f23d53 100644 --- a/packages/auth_twitter/example/main.dart +++ b/packages/auth_twitter/example/main.dart @@ -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); }, diff --git a/packages/auth_twitter/pubspec.yaml b/packages/auth_twitter/pubspec.yaml index 7831dc2f..ddd44c9f 100644 --- a/packages/auth_twitter/pubspec.yaml +++ b/packages/auth_twitter/pubspec.yaml @@ -1,11 +1,10 @@ name: "angel_auth_twitter" -#author: "Tobe O " +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: diff --git a/packages/cli/AUTHORS.md b/packages/cli/AUTHORS.md new file mode 100644 index 00000000..2a973de6 --- /dev/null +++ b/packages/cli/AUTHORS.md @@ -0,0 +1,2 @@ +Tobe O +Thomas Hii \ No newline at end of file diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 7e3bf35f..874c8d7f 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -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`. diff --git a/packages/cli/TODO.md b/packages/cli/TODO.md index 74835954..868ba85c 100644 --- a/packages/cli/TODO.md +++ b/packages/cli/TODO.md @@ -1,4 +1,5 @@ # Todo +* Migrate inflection2, mustache4dart2 and prompts packages to NNBD * `service` * Add tests diff --git a/packages/client/.gitignore b/packages/client/.gitignore index 7752ec67..24d68312 100644 --- a/packages/client/.gitignore +++ b/packages/client/.gitignore @@ -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 \ No newline at end of file +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties diff --git a/packages/client/AUTHORS.md b/packages/client/AUTHORS.md new file mode 100644 index 00000000..ac95ab58 --- /dev/null +++ b/packages/client/AUTHORS.md @@ -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. diff --git a/packages/client/CHANGELOG.md b/packages/client/CHANGELOG.md index 1d0f2aee..7fc15e9f 100644 --- a/packages/client/CHANGELOG.md +++ b/packages/client/CHANGELOG.md @@ -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` as body, not just `Map`. diff --git a/packages/client/LICENSE b/packages/client/LICENSE index eb4ce33e..b593ac86 100644 --- a/packages/client/LICENSE +++ b/packages/client/LICENSE @@ -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 diff --git a/packages/client/README.md b/packages/client/README.md index 6800b328..02087317 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -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, ); diff --git a/packages/client/example/main.dart b/packages/client/example/main.dart index d237c4bb..3768fd5f 100644 --- a/packages/client/example/main.dart +++ b/packages/client/example/main.dart @@ -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>('api/users') .map(User.fromMap, User.toMap); - var users = await userService.index(); + var users = await (userService.index() as FutureOr>); 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 toMap(User user) => {'name': user.name}; + static Map toMap(User user) => {'name': user.name}; } diff --git a/packages/client/lib/angel_client.dart b/packages/client/lib/angel3_client.dart similarity index 76% rename from packages/client/lib/angel_client.dart rename to packages/client/lib/angel3_client.dart index 9e4fb440..08731ce9 100644 --- a/packages/client/lib/angel_client.dart +++ b/packages/client/lib/angel3_client.dart @@ -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 AngelConfigurer(Angel app); +typedef AngelConfigurer = FutureOr 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(x); +typedef AngelDeserializer = 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 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 service(String path, - {@deprecated Type type, AngelDeserializer deserializer}); + {@deprecated Type? type, AngelDeserializer? deserializer}); //@override //Future delete(url, {Map headers}); @override - Future get(url, {Map headers}); + Future get(url, {Map? headers}); @override - Future head(url, {Map headers}); + Future head(url, {Map? headers}); @override Future patch(url, - {body, Map headers, Encoding encoding}); + {body, Map? headers, Encoding? encoding}); @override Future post(url, - {body, Map headers, Encoding encoding}); + {body, Map? headers, Encoding? encoding}); @override Future put(url, - {body, Map headers, Encoding encoding}); + {body, Map? headers, Encoding? encoding}); } /// Represents the result of authentication with an Angel server. class AngelAuthResult { - String _token; + String? _token; final Map data = {}; /// The JSON Web token that was sent with this response. - String get token => _token; + String? get token => _token; - AngelAuthResult({String token, Map data = const {}}) { + AngelAuthResult({String? token, Map 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) ?? {}); + if (data is Map) { + result.data.addAll((data['data'] as Map?) ?? {}); + } 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 toJson() { @@ -179,22 +180,22 @@ abstract class Service { Future close(); /// Retrieves all resources. - Future> index([Map params]); + Future?> index([Map? params]); /// Retrieves the desired resource. - Future read(Id id, [Map params]); + Future read(Id id, [Map? params]); /// Creates a resource. - Future create(Data data, [Map params]); + Future create(Data data, [Map? params]); /// Modifies a resource. - Future modify(Id id, Data data, [Map params]); + Future modify(Id id, Data data, [Map? params]); /// Overwrites a resource. - Future update(Id id, Data data, [Map params]); + Future update(Id id, Data data, [Map? params]); /// Removes the given resource. - Future remove(Id id, [Map params]); + Future remove(Id id, [Map? params]); /// Creates a [Service] that wraps over this one, and maps input and output using two converter functions. /// @@ -218,17 +219,17 @@ class _MappedService extends Service { Future close() => Future.value(); @override - Future create(U data, [Map params]) { + Future create(U data, [Map? params]) { return inner.create(decoder(data)).then(encoder); } @override - Future> index([Map params]) { - return inner.index(params).then((l) => l.map(encoder).toList()); + Future> index([Map? params]) { + return inner.index(params).then((l) => l!.map(encoder).toList()); } @override - Future modify(Id id, U data, [Map params]) { + Future modify(Id id, U data, [Map? params]) { return inner.modify(id, decoder(data), params).then(encoder); } @@ -252,17 +253,17 @@ class _MappedService extends Service { Stream get onUpdated => inner.onUpdated.map(encoder); @override - Future read(Id id, [Map params]) { + Future read(Id id, [Map? params]) { return inner.read(id, params).then(encoder); } @override - Future remove(Id id, [Map params]) { + Future remove(Id id, [Map? params]) { return inner.remove(id, params).then(encoder); } @override - Future update(Id id, U data, [Map params]) { + Future update(Id id, U data, [Map? params]) { return inner.update(id, decoder(data), params).then(encoder); } } @@ -275,9 +276,9 @@ class ServiceList extends DelegatingList { /// A function used to compare the ID's two items for equality. /// /// Defaults to comparing the [idField] of `Map` instances. - Equality get equality => _equality; + Equality? get equality => _equality; - Equality _equality; + Equality? _equality; final Service service; @@ -285,15 +286,16 @@ class ServiceList extends DelegatingList { final List _subs = []; - ServiceList(this.service, {this.idField = 'id', Equality equality}) + ServiceList(this.service, {this.idField = 'id', Equality? equality}) : super([]) { _equality = equality; - _equality ??= EqualityBy((map) { - if (map is Map) - return map[idField ?? 'id'] as Id; - else + _equality ??= EqualityBy((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 extends DelegatingList { })); // Modified/Updated - handleModified(Data item) { + void handleModified(Data item) { var indices = []; - 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 extends DelegatingList { // Removed _subs.add(service.onRemoved.where(_notNull).listen((item) { - removeWhere((x) => _equality.equals(item, x)); + removeWhere((x) => _equality!.equals(item, x)); _onChange.add(this); })); } diff --git a/packages/client/lib/base_angel_client.dart b/packages/client/lib/base_angel_client.dart index 09403a7c..81b7148b 100644 --- a/packages/client/lib/base_angel_client.dart +++ b/packages/client/lib/base_angel_client.dart @@ -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 _readHeaders = {'Accept': 'application/json'}; const Map _writeHeaders = { @@ -16,17 +16,15 @@ const Map _writeHeaders = { 'Content-Type': 'application/json' }; -Map _buildQuery(Map params) { +Map? _buildQuery(Map? 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 _onAuthenticated = StreamController(); final List _services = []; - final http.BaseClient client; + final http.BaseClient? client; @override Stream get onAuthenticated => _onAuthenticated.stream; @@ -61,7 +59,7 @@ abstract class BaseAngelClient extends Angel { @override Future 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 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 sendUnstreamed( - String method, url, Map headers, - [body, Encoding encoding]) async { + String method, url, Map? 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 service(String path, - {Type type, AngelDeserializer deserializer}) { + {Type? type, AngelDeserializer? deserializer}) { var url = baseUrl.replace(path: p.join(baseUrl.path, path)); var s = BaseAngelService(client, this, url, deserializer: deserializer); _services.add(s); - return s; + return s as Service; } Uri _join(url) { @@ -180,65 +176,65 @@ abstract class BaseAngelClient extends Angel { //} @override - Future get(url, {Map headers}) async { + Future get(url, {Map? headers}) async { return sendUnstreamed('GET', _join(url), headers); } @override - Future head(url, {Map headers}) async { + Future head(url, {Map? headers}) async { return sendUnstreamed('HEAD', _join(url), headers); } @override Future patch(url, - {body, Map headers, Encoding encoding}) async { + {body, Map? headers, Encoding? encoding}) async { return sendUnstreamed('PATCH', _join(url), headers, body, encoding); } @override Future post(url, - {body, Map headers, Encoding encoding}) async { + {body, Map? headers, Encoding? encoding}) async { return sendUnstreamed('POST', _join(url), headers, body, encoding); } @override Future put(url, - {body, Map headers, Encoding encoding}) async { + {body, Map? headers, Encoding? encoding}) async { return sendUnstreamed('PUT', _join(url), headers, body, encoding); } } -class BaseAngelService extends Service { +class BaseAngelService extends Service { @override final BaseAngelClient app; final Uri baseUrl; - final http.BaseClient client; - final AngelDeserializer deserializer; + final http.BaseClient? client; + final AngelDeserializer? deserializer; - final StreamController> _onIndexed = StreamController(); - final StreamController _onRead = StreamController(), + final StreamController> _onIndexed = StreamController(); + final StreamController _onRead = StreamController(), _onCreated = StreamController(), _onModified = StreamController(), _onUpdated = StreamController(), _onRemoved = StreamController(); @override - Stream> get onIndexed => _onIndexed.stream; + Stream> get onIndexed => _onIndexed.stream; @override - Stream get onRead => _onRead.stream; + Stream get onRead => _onRead.stream; @override - Stream get onCreated => _onCreated.stream; + Stream get onCreated => _onCreated.stream; @override - Stream get onModified => _onModified.stream; + Stream get onModified => _onModified.stream; @override - Stream get onUpdated => _onUpdated.stream; + Stream get onUpdated => _onUpdated.stream; @override - Stream get onRemoved => _onRemoved.stream; + Stream get onRemoved => _onRemoved.stream; @override Future close() async { @@ -251,14 +247,14 @@ class BaseAngelService extends Service { } 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 extends Service { } Future 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> index([Map params]) async { + Future?> index([Map? params]) async { var url = baseUrl.replace(queryParameters: _buildQuery(params)); var response = await app.sendUnstreamed('GET', url, _readHeaders); @@ -304,7 +300,7 @@ class BaseAngelService extends Service { } @override - Future read(id, [Map params]) async { + Future read(id, [Map? params]) async { var url = baseUrl.replace( path: p.join(baseUrl.path, id.toString()), queryParameters: _buildQuery(params)); @@ -313,54 +309,58 @@ class BaseAngelService extends Service { 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 create(data, [Map params]) async { + Future create(data, [Map? 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 modify(id, data, [Map params]) async { + Future modify(id, data, [Map? params]) async { var url = baseUrl.replace( path: p.join(baseUrl.path, id.toString()), queryParameters: _buildQuery(params)); @@ -370,27 +370,29 @@ class BaseAngelService extends Service { 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 update(id, data, [Map params]) async { + Future update(id, data, [Map? params]) async { var url = baseUrl.replace( path: p.join(baseUrl.path, id.toString()), queryParameters: _buildQuery(params)); @@ -400,27 +402,29 @@ class BaseAngelService extends Service { 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 remove(id, [Map params]) async { + Future remove(id, [Map? params]) async { var url = baseUrl.replace( path: p.join(baseUrl.path, id.toString()), queryParameters: _buildQuery(params)); @@ -429,20 +433,22 @@ class BaseAngelService extends Service { 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; diff --git a/packages/client/lib/browser.dart b/packages/client/lib/browser.dart index 5b25459a..1ae5336b 100644 --- a/packages/client/lib/browser.dart +++ b/packages/client/lib/browser.dart @@ -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 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 authenticateViaPopup(String url, - {String eventName = 'token', String errorMessage}) { - var ctrl = new StreamController(); + {String eventName = 'token', String? errorMessage}) { + var ctrl = StreamController(); 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(); } }); diff --git a/packages/client/lib/flutter.dart b/packages/client/lib/flutter.dart index 5e908fb5..0a2bc2cd 100644 --- a/packages/client/lib/flutter.dart +++ b/packages/client/lib/flutter.dart @@ -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 authenticateViaPopup(String url, {String eventName = 'token'}) { - throw new UnimplementedError( + throw UnimplementedError( 'Opening popup windows is not supported in the `flutter` client.'); } } diff --git a/packages/client/lib/io.dart b/packages/client/lib/io.dart index 30a24901..9c76bdd8 100644 --- a/packages/client/lib/io.dart +++ b/packages/client/lib/io.dart @@ -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 service(String path, - {Type type, AngelDeserializer deserializer}) { + {Type? type, AngelDeserializer? deserializer}) { var url = baseUrl.replace(path: p.join(baseUrl.path, path)); var s = RestService(client, this, url, type); _services.add(s); - return s; + return s as Service; } @override @@ -42,21 +42,21 @@ class Rest extends BaseAngelClient { /// Queries an Angel service via REST. class RestService extends BaseAngelService { - 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 diff --git a/packages/client/pubspec.yaml b/packages/client/pubspec.yaml index a8f3602e..77c76298 100644 --- a/packages/client/pubspec.yaml +++ b/packages/client/pubspec.yaml @@ -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 -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 diff --git a/packages/client/test/all_test.dart b/packages/client/test/all_test.dart index acc8dc99..ad3e8128 100644 --- a/packages/client/test/all_test.dart +++ b/packages/client/test/all_test.dart @@ -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: ''); - 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'}), ); }); diff --git a/packages/client/test/common.dart b/packages/client/test/common.dart index 8d7e2c90..429beeb9 100644 --- a/packages/client/test/common.dart +++ b/packages/client/test/common.dart @@ -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 read(Stream> 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 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 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.value(new http.StreamedResponse( - new Stream>.fromIterable([utf8.encode(json.encode(data))]), + return Future.value(http.StreamedResponse( + Stream>.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 headers; - final int contentLength; + final int? contentLength; Spec(this.request, this.method, this.path, this.headers, this.contentLength); diff --git a/packages/client/test/list_test.dart b/packages/client/test/list_test.dart index ba632a9e..8557fb3c 100644 --- a/packages/client/test/list_test.dart +++ b/packages/client/test/list_test.dart @@ -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 { diff --git a/packages/client/test/shared.dart b/packages/client/test/shared.dart index 11b6c660..e698aac2 100644 --- a/packages/client/test/shared.dart +++ b/packages/client/test/shared.dart @@ -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()); diff --git a/packages/client/web/main.dart b/packages/client/web/main.dart index 9e12ec08..da1cc68f 100644 --- a/packages/client/web/main.dart +++ b/packages/client/web/main.dart @@ -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()); } diff --git a/packages/code_buffer/.gitignore b/packages/code_buffer/.gitignore new file mode 100644 index 00000000..24d68312 --- /dev/null +++ b/packages/code_buffer/.gitignore @@ -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 diff --git a/packages/code_buffer/.travis.yml b/packages/code_buffer/.travis.yml new file mode 100644 index 00000000..de2210c9 --- /dev/null +++ b/packages/code_buffer/.travis.yml @@ -0,0 +1 @@ +language: dart \ No newline at end of file diff --git a/packages/code_buffer/AUTHORS.md b/packages/code_buffer/AUTHORS.md new file mode 100644 index 00000000..ac95ab58 --- /dev/null +++ b/packages/code_buffer/AUTHORS.md @@ -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. diff --git a/packages/code_buffer/CHANGELOG.md b/packages/code_buffer/CHANGELOG.md new file mode 100644 index 00000000..44b48088 --- /dev/null +++ b/packages/code_buffer/CHANGELOG.md @@ -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()`. \ No newline at end of file diff --git a/packages/code_buffer/LICENSE b/packages/code_buffer/LICENSE new file mode 100644 index 00000000..b593ac86 --- /dev/null +++ b/packages/code_buffer/LICENSE @@ -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. diff --git a/packages/code_buffer/README.md b/packages/code_buffer/README.md new file mode 100644 index 00000000..3fc1282c --- /dev/null +++ b/packages/code_buffer/README.md @@ -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); +} +``` \ No newline at end of file diff --git a/packages/code_buffer/analysis_options.yaml b/packages/code_buffer/analysis_options.yaml new file mode 100644 index 00000000..eae1e42a --- /dev/null +++ b/packages/code_buffer/analysis_options.yaml @@ -0,0 +1,3 @@ +analyzer: + strong-mode: + implicit-casts: false \ No newline at end of file diff --git a/packages/code_buffer/example/main.dart b/packages/code_buffer/example/main.dart new file mode 100644 index 00000000..89b1a531 --- /dev/null +++ b/packages/code_buffer/example/main.dart @@ -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); +} diff --git a/packages/code_buffer/lib/angel3_code_buffer.dart b/packages/code_buffer/lib/angel3_code_buffer.dart new file mode 100644 index 00000000..67954668 --- /dev/null +++ b/packages/code_buffer/lib/angel3_code_buffer.dart @@ -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 _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 get lines => List.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 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(); +} diff --git a/packages/code_buffer/pubspec.yaml b/packages/code_buffer/pubspec.yaml new file mode 100644 index 00000000..cdcffa04 --- /dev/null +++ b/packages/code_buffer/pubspec.yaml @@ -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 \ No newline at end of file diff --git a/packages/code_buffer/test/copy_test.dart b/packages/code_buffer/test/copy_test.dart new file mode 100644 index 00000000..01f60420 --- /dev/null +++ b/packages/code_buffer/test/copy_test.dart @@ -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); + }); +} diff --git a/packages/code_buffer/test/span_test.dart b/packages/code_buffer/test/span_test.dart new file mode 100644 index 00000000..61615ae8 --- /dev/null +++ b/packages/code_buffer/test/span_test.dart @@ -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); + }); +} diff --git a/packages/code_buffer/test/write_test.dart b/packages/code_buffer/test/write_test.dart new file mode 100644 index 00000000..509c44cd --- /dev/null +++ b/packages/code_buffer/test/write_test.dart @@ -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'); + }); + }); +} diff --git a/packages/combinator/.gitignore b/packages/combinator/.gitignore new file mode 100644 index 00000000..24d68312 --- /dev/null +++ b/packages/combinator/.gitignore @@ -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 diff --git a/packages/combinator/.travis.yml b/packages/combinator/.travis.yml new file mode 100644 index 00000000..2f22c5ce --- /dev/null +++ b/packages/combinator/.travis.yml @@ -0,0 +1,4 @@ +language: dart +dart: + - stable + - dev \ No newline at end of file diff --git a/packages/combinator/AUTHORS.md b/packages/combinator/AUTHORS.md new file mode 100644 index 00000000..ac95ab58 --- /dev/null +++ b/packages/combinator/AUTHORS.md @@ -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. diff --git a/packages/combinator/CHANGELOG.md b/packages/combinator/CHANGELOG.md new file mode 100644 index 00000000..e3710992 --- /dev/null +++ b/packages/combinator/CHANGELOG.md @@ -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. \ No newline at end of file diff --git a/packages/combinator/LICENSE b/packages/combinator/LICENSE new file mode 100644 index 00000000..b593ac86 --- /dev/null +++ b/packages/combinator/LICENSE @@ -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. diff --git a/packages/combinator/README.md b/packages/combinator/README.md new file mode 100644 index 00000000..bd61f1fd --- /dev/null +++ b/packages/combinator/README.md @@ -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`) \ No newline at end of file diff --git a/packages/combinator/analysis_options.yaml b/packages/combinator/analysis_options.yaml new file mode 100644 index 00000000..bbc55fe4 --- /dev/null +++ b/packages/combinator/analysis_options.yaml @@ -0,0 +1,4 @@ +analyzer: + strong-mode: + implicit-casts: false + #implicit-dynamic: false \ No newline at end of file diff --git a/packages/combinator/combinator.iml b/packages/combinator/combinator.iml new file mode 100644 index 00000000..75734c90 --- /dev/null +++ b/packages/combinator/combinator.iml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/combinator/example/basic_auth.dart b/packages/combinator/example/basic_auth.dart new file mode 100644 index 00000000..15128da6 --- /dev/null +++ b/packages/combinator/example/basic_auth.dart @@ -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 = + match(RegExp(r'[^:$]+'), errorMessage: 'Expected a string.') + .value((r) => r.span!.text); + +/// Transforms `{username}:{password}` to `{"username": username, "password": password}`. +final Parser> credentials = chain([ + string.opt(), + match(':'), + string.opt(), +]).map>( + (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?>( + 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('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); + } +} diff --git a/packages/combinator/example/calculator.dart b/packages/combinator/example/calculator.dart new file mode 100644 index 00000000..e0b46083 --- /dev/null +++ b/packages/combinator/example/calculator.dart @@ -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 calculatorGrammar() { + var expr = reference(); + + var number = match(RegExp(r'-?[0-9]+(\.[0-9]+)?')) + .value((r) => num.parse(r.span!.text)); + + var hex = match(RegExp(r'0x([A-Fa-f0-9]+)')) + .map((r) => int.parse(r.scanner.lastMatch![1]!, radix: 16)); + + var binary = match(RegExp(r'([0-1]+)b')) + .map((r) => int.parse(r.scanner.lastMatch![1]!, radix: 2)); + + var alternatives = >[]; + + void registerBinary(String op, num Function(num, num) f) { + alternatives.add( + chain([ + expr.space(), + match(op).space() as Parser, + 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); + } +} diff --git a/packages/combinator/example/delimiter.dart b/packages/combinator/example/delimiter.dart new file mode 100644 index 00000000..71008e47 --- /dev/null +++ b/packages/combinator/example/delimiter.dart @@ -0,0 +1,28 @@ +import 'dart:io'; +import 'package:angel3_combinator/angel3_combinator.dart'; +import 'package:string_scanner/string_scanner.dart'; + +final Parser id = + match(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); + } +} diff --git a/packages/combinator/example/json.dart b/packages/combinator/example/json.dart new file mode 100644 index 00000000..36eff7bf --- /dev/null +++ b/packages/combinator/example/json.dart @@ -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(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().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); + } +} diff --git a/packages/combinator/example/main.dart b/packages/combinator/example/main.dart new file mode 100644 index 00000000..2ed8d8b3 --- /dev/null +++ b/packages/combinator/example/main.dart @@ -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 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((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); + } +} diff --git a/packages/combinator/example/query_string.dart b/packages/combinator/example/query_string.dart new file mode 100644 index 00000000..4e7c3a6a --- /dev/null +++ b/packages/combinator/example/query_string.dart @@ -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 key = + match(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); + } +} diff --git a/packages/combinator/example/sexp.dart b/packages/combinator/example/sexp.dart new file mode 100644 index 00000000..541d3c35 --- /dev/null +++ b/packages/combinator/example/sexp.dart @@ -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 = {}; + + void registerFunction(String name, int nArgs, Function(List) 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); + } + } +} diff --git a/packages/combinator/example/tuple.dart b/packages/combinator/example/tuple.dart new file mode 100644 index 00000000..188ced83 --- /dev/null +++ b/packages/combinator/example/tuple.dart @@ -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 + var grammar = tuple3(pub, dart, lang); + + var scanner = SpanScanner('pub dart lang'); + print(grammar.parse(scanner).value); +} diff --git a/packages/combinator/lib/angel3_combinator.dart b/packages/combinator/lib/angel3_combinator.dart new file mode 100644 index 00000000..79e4c074 --- /dev/null +++ b/packages/combinator/lib/angel3_combinator.dart @@ -0,0 +1,2 @@ +export 'src/combinator/combinator.dart'; +export 'src/error.dart'; diff --git a/packages/combinator/lib/src/combinator/advance.dart b/packages/combinator/lib/src/combinator/advance.dart new file mode 100644 index 00000000..01506dc2 --- /dev/null +++ b/packages/combinator/lib/src/combinator/advance.dart @@ -0,0 +1,26 @@ +part of lex.src.combinator; + +class _Advance extends Parser { + final Parser parser; + final int amount; + + _Advance(this.parser, this.amount); + + @override + ParseResult __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(')'); + } +} diff --git a/packages/combinator/lib/src/combinator/any.dart b/packages/combinator/lib/src/combinator/any.dart new file mode 100644 index 00000000..616bf29d --- /dev/null +++ b/packages/combinator/lib/src/combinator/any.dart @@ -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 any(Iterable> parsers, + {bool backtrack: true, errorMessage, SyntaxErrorSeverity? severity}) { + return _Any(parsers, backtrack != false, errorMessage, + severity ?? SyntaxErrorSeverity.error); +} + +class _Any extends Parser { + final Iterable> parsers; + final bool backtrack; + final errorMessage; + final SyntaxErrorSeverity severity; + + _Any(this.parsers, this.backtrack, this.errorMessage, this.severity); + + @override + ParseResult _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 = []; + 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 __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(')'); + } +} diff --git a/packages/combinator/lib/src/combinator/cache.dart b/packages/combinator/lib/src/combinator/cache.dart new file mode 100644 index 00000000..b07b38e0 --- /dev/null +++ b/packages/combinator/lib/src/combinator/cache.dart @@ -0,0 +1,26 @@ +part of lex.src.combinator; + +class _Cache extends Parser { + final Map> _cache = {}; + final Parser parser; + + _Cache(this.parser); + + @override + ParseResult __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(')'); + } +} diff --git a/packages/combinator/lib/src/combinator/cast.dart b/packages/combinator/lib/src/combinator/cast.dart new file mode 100644 index 00000000..9b531d43 --- /dev/null +++ b/packages/combinator/lib/src/combinator/cast.dart @@ -0,0 +1,63 @@ +part of lex.src.combinator; + +class _Cast extends Parser { + final Parser parser; + + _Cast(this.parser); + + @override + ParseResult __parse(ParseArgs args) { + var result = parser._parse(args.increaseDepth()); + return ParseResult( + 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 extends Parser { + final Parser parser; + + _CastDynamic(this.parser); + + @override + ParseResult __parse(ParseArgs args) { + var result = parser._parse(args.increaseDepth()); + return ParseResult( + args.trampoline, + args.scanner, + this, + result.successful, + result.errors, + span: result.span, + value: result.value, + ); + } + + @override + void stringify(CodeBuffer buffer) { + buffer + ..writeln('cast (') + ..indent(); + parser.stringify(buffer); + buffer + ..outdent() + ..writeln(')'); + } +} diff --git a/packages/combinator/lib/src/combinator/chain.dart b/packages/combinator/lib/src/combinator/chain.dart new file mode 100644 index 00000000..39101df5 --- /dev/null +++ b/packages/combinator/lib/src/combinator/chain.dart @@ -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 chain(Iterable> parsers, + {bool failFast: true, SyntaxErrorSeverity? severity}) { + return _Chain( + parsers, failFast != false, severity ?? SyntaxErrorSeverity.error); +} + +class _Alt extends Parser { + final Parser parser; + final String? errorMessage; + final SyntaxErrorSeverity severity; + + _Alt(this.parser, this.errorMessage, this.severity); + + @override + ParseResult __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 extends ListParser { + final Iterable> parsers; + final bool failFast; + final SyntaxErrorSeverity severity; + + _Chain(this.parsers, this.failFast, this.severity); + + @override + ParseResult> __parse(ParseArgs args) { + var errors = []; + var results = []; + var spans = []; + 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>( + args.trampoline, + args.scanner, + this, + successful, + errors, + span: span, + value: List.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(')'); + } +} diff --git a/packages/combinator/lib/src/combinator/check.dart b/packages/combinator/lib/src/combinator/check.dart new file mode 100644 index 00000000..c15d7051 --- /dev/null +++ b/packages/combinator/lib/src/combinator/check.dart @@ -0,0 +1,41 @@ +part of lex.src.combinator; + +class _Check extends Parser { + final Parser parser; + final Matcher matcher; + final String? errorMessage; + final SyntaxErrorSeverity severity; + + _Check(this.parser, this.matcher, this.errorMessage, this.severity); + + @override + ParseResult __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(')'); + } +} diff --git a/packages/combinator/lib/src/combinator/combinator.dart b/packages/combinator/lib/src/combinator/combinator.dart new file mode 100644 index 00000000..3b2fcc7a --- /dev/null +++ b/packages/combinator/lib/src/combinator/combinator.dart @@ -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 { + ParseResult __parse(ParseArgs args); + + ParseResult _parse(ParseArgs args) { + var pos = args.scanner.position; + + if (args.trampoline.hasMemoized(this, pos)) + return args.trampoline.getMemoized(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 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 forward(int amount) => _Advance(this, amount); + + /// Moves backward a certain amount of steps after parsing, if it was successful. + Parser back(int amount) => _Advance(this, amount * -1); + + /// Casts this parser to produce [U] objects. + Parser cast() => _Cast(this); + + /// Casts this parser to produce [dynamic] objects. + Parser castDynamic() => _CastDynamic(this); + + // TODO: Type issue + /// Runs the given function, which changes the returned [ParseResult] into one relating to a [U] object. + Parser change(ParseResult Function(ParseResult) f) { + return _Change(this, f); + } + + /// Validates the parse result against a [Matcher]. + /// + /// You can provide a custom [errorMessage]. + Parser check(Matcher matcher, + {String? errorMessage, SyntaxErrorSeverity? severity}) => + _Check( + this, matcher, errorMessage, severity ?? SyntaxErrorSeverity.error); + + /// Binds an [errorMessage] to a copy of this parser. + Parser error({String? errorMessage, SyntaxErrorSeverity? severity}) => + _Alt(this, errorMessage, severity ?? SyntaxErrorSeverity.error); + + /// Removes multiple errors that occur in the same spot; this can reduce noise in parser output. + Parser foldErrors({bool equal(SyntaxError a, SyntaxError b)?}) { + equal ??= (b, e) => b.span?.start.offset == e.span?.start.offset; + return _FoldErrors(this, equal); + } + + /// Transforms the parse result using a unary function. + Parser map(U Function(ParseResult) f) { + return _Map(this, f); + } + + /// Prevents recursion past a certain [depth], preventing stack overflow errors. + Parser maxDepth(int depth) => _MaxDepth(this, depth); + + Parser operator ~() => negate(); + + /// Ensures this pattern is not matched. + /// + /// You can provide an [errorMessage]. + Parser negate( + {String errorMessage = 'Negate error', + SyntaxErrorSeverity severity = SyntaxErrorSeverity.error}) => + _Negate(this, errorMessage, severity); + + /// Caches the results of parse attempts at various locations within the source text. + /// + /// Use this to prevent excessive recursion. + Parser cache() => _Cache(this); + + Parser operator &(Parser other) => and(other); + + /// Consumes `this` and another parser, but only considers the result of `this` parser. + Parser and(Parser other) => then(other).change((r) { + return ParseResult( + r.trampoline, + r.scanner, + this, + r.successful, + r.errors, + span: r.span, + value: (r.value != null ? r.value![0] : r.value) as T?, + ); + }); + + Parser operator |(Parser other) => or(other); + + /// Shortcut for [or]-ing two parsers. + Parser or(Parser other) => any([this, other]); + + /// Parses this sequence one or more times. + ListParser 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 safe( + {bool backtrack: true, + String errorMessage = "error", + SyntaxErrorSeverity? severity}) => + _Safe( + this, backtrack, errorMessage, severity ?? SyntaxErrorSeverity.error); + + Parser> separatedByComma() => + separatedBy(match>(',').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> separatedBy(Parser other) { + var suffix = other.then(this).index(1).cast(); + 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.from(preceding); + if (v[1] != null && v[1] != "NULL") { + v[1].forEach((element) { + out.add(element as T); + }); + } + return out; + }); + } + + Parser surroundedByCurlyBraces({required T defaultValue}) => opt() + .surroundedBy(match('{').space(), match('}').space()) + .map((r) => r.value ?? defaultValue); + + Parser 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 surroundedBy(Parser left, [Parser? right]) { + return chain([ + left, + this, + right ?? left, + ]).index(1).castDynamic().cast(); + } + + /// Parses `this`, either as-is or wrapped in parentheses. + Parser maybeParenthesized() { + return any([parenthesized(), this]); + } + + /// Parses `this`, wrapped in parentheses. + Parser parenthesized() => + surroundedBy(match('(').space(), match(')').space()); + + /// Consumes any trailing whitespace. + Parser space() => trail(RegExp(r'[ \n\r\t]+')); + + /// Consumes 0 or more instance(s) of this parser. + ListParser star({bool backtrack: true}) => + times(1, exact: false, backtrack: backtrack).opt(); + + /// Shortcut for [chain]-ing two parsers together. + ListParser then(Parser other) => chain([this, other]); + + /// Casts this instance into a [ListParser]. + ListParser toList() => _ToList(this); + + /// Consumes and ignores any trailing occurrences of [pattern]. + Parser trail(Pattern pattern) => + then(match(pattern).opt()).first().cast(); + + /// 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 times(int count, + {bool exact: true, + String tooFew = 'Too few', + String tooMany = 'Too many', + bool backtrack: true, + SyntaxErrorSeverity? severity}) { + return _Repeat(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 opt({bool backtrack: true}) => _Opt(this, backtrack); + + /// Sets the value of the [ParseResult]. + Parser value(T Function(ParseResult) f) { + return _Value(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 extends Parser> { + /// Shortcut for calling [index] with `0`. + Parser first() => index(0); + + /// Modifies this parser to only return the value at the given index [i]. + Parser index(int i) => _Index(this, i); + + /// Shortcut for calling [index] with the greatest-possible index. + Parser last() => index(-1); + + /// Modifies this parser to call `List.reduce` on the parsed values. + Parser reduce(T Function(T, T) combine) => _Reduce(this, combine); + + /// Sorts the parsed values, using the given [Comparator]. + ListParser sort(Comparator compare) => _Compare(this, compare); + + @override + ListParser opt({bool backtrack: true}) => _ListOpt(this, backtrack); + + /// Modifies this parser, returning only the values that match a predicate. + Parser> where(bool Function(T) f) => + map>((r) => r.value?.where(f).toList() ?? []); + + /// Condenses a [ListParser] into having a value of the combined span's text. + Parser flatten() => map((r) => r.span?.text ?? ''); +} + +/// Prevents stack overflow in recursive parsers. +class Trampoline { + final Map> _active = {}; + final Map>> _memo = {}; + + bool hasMemoized(Parser parser, int position) { + var list = _memo[parser]; + return list?.any((t) => t.item1 == position) == true; + } + + ParseResult getMemoized(Parser parser, int position) { + return _memo[parser]?.firstWhere((t) => t.item1 == position).item2 + as ParseResult; + } + + 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 { + final Parser parser; + final bool successful; + final Iterable 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 change( + {Parser? parser, + bool? successful, + Iterable errors = const [], + FileSpan? span, + T? value}) { + return ParseResult( + trampoline, + scanner, + parser ?? this.parser, + successful ?? this.successful, + errors.isNotEmpty ? errors : this.errors, + span: span ?? this.span, + value: value ?? this.value, + ); + } + + ParseResult addErrors(Iterable errors) { + return change( + errors: List.from(this.errors)..addAll(errors), + ); + } +} diff --git a/packages/combinator/lib/src/combinator/compare.dart b/packages/combinator/lib/src/combinator/compare.dart new file mode 100644 index 00000000..4ee0b813 --- /dev/null +++ b/packages/combinator/lib/src/combinator/compare.dart @@ -0,0 +1,38 @@ +part of lex.src.combinator; + +class _Compare extends ListParser { + final ListParser parser; + final Comparator compare; + + _Compare(this.parser, this.compare); + + @override + ParseResult> __parse(ParseArgs args) { + ParseResult> 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.from(result.value!)); + return ParseResult>( + 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(')'); + } +} diff --git a/packages/combinator/lib/src/combinator/fold_errors.dart b/packages/combinator/lib/src/combinator/fold_errors.dart new file mode 100644 index 00000000..6d15c69a --- /dev/null +++ b/packages/combinator/lib/src/combinator/fold_errors.dart @@ -0,0 +1,29 @@ +part of lex.src.combinator; + +class _FoldErrors extends Parser { + final Parser parser; + final bool Function(SyntaxError, SyntaxError) equal; + + _FoldErrors(this.parser, this.equal); + + @override + ParseResult __parse(ParseArgs args) { + var result = parser._parse(args.increaseDepth()).change(parser: this); + var errors = result.errors.fold>([], (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(')'); + } +} diff --git a/packages/combinator/lib/src/combinator/index.dart b/packages/combinator/lib/src/combinator/index.dart new file mode 100644 index 00000000..e52860a7 --- /dev/null +++ b/packages/combinator/lib/src/combinator/index.dart @@ -0,0 +1,53 @@ +part of lex.src.combinator; + +class _Index extends Parser { + final ListParser parser; + final int index; + + _Index(this.parser, this.index); + + @override + ParseResult __parse(ParseArgs args) { + ParseResult> 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( + 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(')'); + } +} diff --git a/packages/combinator/lib/src/combinator/longest.dart b/packages/combinator/lib/src/combinator/longest.dart new file mode 100644 index 00000000..fa9124e7 --- /dev/null +++ b/packages/combinator/lib/src/combinator/longest.dart @@ -0,0 +1,114 @@ +part of lex.src.combinator; + +/// Matches any one of the given [parsers]. +/// +/// You can provide a custom [errorMessage]. +Parser longest(Iterable> parsers, + {Object? errorMessage, SyntaxErrorSeverity? severity}) { + return _Longest(parsers, errorMessage, severity ?? SyntaxErrorSeverity.error); +} + +class _Longest extends Parser { + final Iterable> parsers; + final Object? errorMessage; + final SyntaxErrorSeverity severity; + + _Longest(this.parsers, this.errorMessage, this.severity); + + @override + ParseResult _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 = []; + var results = >[]; + + 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 __parse(ParseArgs args) { + int replay = args.scanner.position; + var errors = []; + var results = >[]; + + 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(')'); + } +} diff --git a/packages/combinator/lib/src/combinator/map.dart b/packages/combinator/lib/src/combinator/map.dart new file mode 100644 index 00000000..4dfb326d --- /dev/null +++ b/packages/combinator/lib/src/combinator/map.dart @@ -0,0 +1,56 @@ +part of lex.src.combinator; + +class _Map extends Parser { + final Parser parser; + final U Function(ParseResult) f; + + _Map(this.parser, this.f); + + @override + ParseResult __parse(ParseArgs args) { + var result = parser._parse(args.increaseDepth()); + return ParseResult( + 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 extends Parser { + final Parser parser; + final ParseResult Function(ParseResult) f; + + _Change(this.parser, this.f); + + @override + ParseResult __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(')'); + } +} diff --git a/packages/combinator/lib/src/combinator/match.dart b/packages/combinator/lib/src/combinator/match.dart new file mode 100644 index 00000000..338faff6 --- /dev/null +++ b/packages/combinator/lib/src/combinator/match.dart @@ -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 match(Pattern pattern, + {String? errorMessage, SyntaxErrorSeverity? severity}) => + _Match(pattern, errorMessage, severity ?? SyntaxErrorSeverity.error); + +class _Match extends Parser { + final Pattern pattern; + final String? errorMessage; + final SyntaxErrorSeverity severity; + + _Match(this.pattern, this.errorMessage, this.severity); + + @override + ParseResult __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( + args.trampoline, + scanner, + this, + true, + [], + span: scanner.lastSpan, + ); + } + + @override + void stringify(CodeBuffer buffer) { + buffer.writeln('match($pattern)'); + } +} diff --git a/packages/combinator/lib/src/combinator/max_depth.dart b/packages/combinator/lib/src/combinator/max_depth.dart new file mode 100644 index 00000000..42befcff --- /dev/null +++ b/packages/combinator/lib/src/combinator/max_depth.dart @@ -0,0 +1,28 @@ +part of lex.src.combinator; + +class _MaxDepth extends Parser { + final Parser parser; + final int cap; + + _MaxDepth(this.parser, this.cap); + + @override + ParseResult __parse(ParseArgs args) { + if (args.depth > cap) { + return ParseResult(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(')'); + } +} diff --git a/packages/combinator/lib/src/combinator/negate.dart b/packages/combinator/lib/src/combinator/negate.dart new file mode 100644 index 00000000..b507bdcc --- /dev/null +++ b/packages/combinator/lib/src/combinator/negate.dart @@ -0,0 +1,51 @@ +part of lex.src.combinator; + +class _Negate extends Parser { + final Parser parser; + final String? errorMessage; + final SyntaxErrorSeverity severity; + + _Negate(this.parser, this.errorMessage, this.severity); + + @override + ParseResult __parse(ParseArgs args) { + var result = parser._parse(args.increaseDepth()).change(parser: this); + + if (!result.successful) { + return ParseResult( + 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(')'); + } +} diff --git a/packages/combinator/lib/src/combinator/opt.dart b/packages/combinator/lib/src/combinator/opt.dart new file mode 100644 index 00000000..12083ed7 --- /dev/null +++ b/packages/combinator/lib/src/combinator/opt.dart @@ -0,0 +1,57 @@ +part of lex.src.combinator; + +class _Opt extends Parser { + final Parser parser; + final bool backtrack; + + _Opt(this.parser, this.backtrack); + + @override + ParseResult __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 extends ListParser { + final ListParser parser; + final bool backtrack; + + _ListOpt(this.parser, this.backtrack); + + @override + ParseResult> __parse(ParseArgs args) { + var replay = args.scanner.position; + ParseResult> 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(')'); + } +} diff --git a/packages/combinator/lib/src/combinator/recursion.dart b/packages/combinator/lib/src/combinator/recursion.dart new file mode 100644 index 00000000..76516943 --- /dev/null +++ b/packages/combinator/lib/src/combinator/recursion.dart @@ -0,0 +1,142 @@ +part of lex.src.combinator; + +/* +/// Handles left recursion in a grammar using the Pratt algorithm. +class Recursion { + Iterable> prefix; + Map)> infix; + Map)> postfix; + + Recursion({this.prefix, this.infix, this.postfix}) { + prefix ??= []; + infix ??= {}; + postfix ??= {}; + } + + Parser 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 extends Parser { + final Recursion r; + final int precedence; + + _Precedence(this.r, this.precedence); + + @override + ParseResult __parse(ParseArgs args) { + int replay = args.scanner.position; + var errors = []; + 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(')'); + } +} +*/ diff --git a/packages/combinator/lib/src/combinator/reduce.dart b/packages/combinator/lib/src/combinator/reduce.dart new file mode 100644 index 00000000..29f1f1f3 --- /dev/null +++ b/packages/combinator/lib/src/combinator/reduce.dart @@ -0,0 +1,45 @@ +part of lex.src.combinator; + +class _Reduce extends Parser { + final ListParser parser; + final T Function(T, T) combine; + + _Reduce(this.parser, this.combine); + + @override + ParseResult __parse(ParseArgs args) { + ParseResult> result = parser._parse(args.increaseDepth()); + + if (!result.successful) + return ParseResult( + args.trampoline, + args.scanner, + this, + false, + result.errors, + ); + + result = result.change( + value: result.value?.isNotEmpty == true ? result.value : []); + return ParseResult( + args.trampoline, + args.scanner, + this, + result.successful, + [], + span: result.span, + value: result.value!.isEmpty ? null : result.value!.reduce(combine), + ); + } + + @override + void stringify(CodeBuffer buffer) { + buffer + ..writeln('reduce($combine) (') + ..indent(); + parser.stringify(buffer); + buffer + ..outdent() + ..writeln(')'); + } +} diff --git a/packages/combinator/lib/src/combinator/reference.dart b/packages/combinator/lib/src/combinator/reference.dart new file mode 100644 index 00000000..737b94d4 --- /dev/null +++ b/packages/combinator/lib/src/combinator/reference.dart @@ -0,0 +1,41 @@ +part of lex.src.combinator; + +Reference reference() => Reference._(); + +class Reference extends Parser { + Parser? _parser; + bool printed = false; + + Reference._(); + + void set parser(Parser value) { + if (_parser != null) + throw StateError('There is already a parser assigned to this reference.'); + _parser = value; + } + + @override + ParseResult __parse(ParseArgs args) { + if (_parser == null) + throw StateError('There is no parser assigned to this reference.'); + return _parser!._parse(args); + } + + @override + ParseResult _parse(ParseArgs args) { + if (_parser == null) + throw StateError('There is no parser assigned to this reference.'); + return _parser!._parse(args); + } + + @override + void stringify(CodeBuffer buffer) { + if (_parser == null) { + buffer.writeln('(undefined reference <$T>)'); + } else if (!printed) { + _parser!.stringify(buffer); + } + printed = true; + buffer.writeln('(previously printed reference)'); + } +} diff --git a/packages/combinator/lib/src/combinator/repeat.dart b/packages/combinator/lib/src/combinator/repeat.dart new file mode 100644 index 00000000..c8b9891d --- /dev/null +++ b/packages/combinator/lib/src/combinator/repeat.dart @@ -0,0 +1,88 @@ +part of lex.src.combinator; + +class _Repeat extends ListParser { + final Parser parser; + final int count; + final bool exact, backtrack; + final String tooFew; + final String tooMany; + final SyntaxErrorSeverity severity; + + _Repeat(this.parser, this.count, this.exact, this.tooFew, this.tooMany, + this.backtrack, this.severity); + + @override + ParseResult> __parse(ParseArgs args) { + var errors = []; + var results = []; + var spans = []; + int success = 0, replay = args.scanner.position; + ParseResult result; + + do { + result = parser._parse(args.increaseDepth()); + if (result.successful) { + success++; + if (result.value != null) { + results.add(result.value!); + } + replay = args.scanner.position; + } else if (backtrack) args.scanner.position = replay; + + if (result.span != null) { + spans.add(result.span!); + } + } while (result.successful); + + if (success < count) { + errors.addAll(result.errors); + errors.add( + SyntaxError( + severity, + tooFew, + result.span ?? args.scanner.emptySpan, + ), + ); + + if (backtrack) args.scanner.position = replay; + + return ParseResult>( + args.trampoline, args.scanner, this, false, errors); + } else if (success > count && exact) { + if (backtrack) args.scanner.position = replay; + + return ParseResult>(args.trampoline, args.scanner, this, false, [ + SyntaxError( + severity, + tooMany, + result.span ?? args.scanner.emptySpan, + ), + ]); + } + + var span = spans.reduce((a, b) => a.expand(b)); + return ParseResult>( + args.trampoline, + args.scanner, + this, + true, + [], + span: span, + value: results, + ); + } + + @override + void stringify(CodeBuffer buffer) { + var r = StringBuffer('{$count'); + if (!exact) r.write(','); + r.write('}'); + buffer + ..writeln('repeat($r) (') + ..indent(); + parser.stringify(buffer); + buffer + ..outdent() + ..writeln(')'); + } +} diff --git a/packages/combinator/lib/src/combinator/safe.dart b/packages/combinator/lib/src/combinator/safe.dart new file mode 100644 index 00000000..a2542295 --- /dev/null +++ b/packages/combinator/lib/src/combinator/safe.dart @@ -0,0 +1,47 @@ +part of lex.src.combinator; + +class _Safe extends Parser { + final Parser parser; + final bool backtrack; + final String errorMessage; + final SyntaxErrorSeverity severity; + bool _triggered = false; + + _Safe(this.parser, this.backtrack, this.errorMessage, this.severity); + + @override + ParseResult __parse(ParseArgs args) { + var replay = args.scanner.position; + + try { + if (_triggered) throw Exception(); + return parser._parse(args.increaseDepth()); + } catch (_) { + _triggered = true; + if (backtrack) args.scanner.position = replay; + var errors = []; + + errors.add( + SyntaxError( + severity, + errorMessage, + args.scanner.lastSpan ?? args.scanner.emptySpan, + ), + ); + + return ParseResult(args.trampoline, args.scanner, this, false, errors); + } + } + + @override + void stringify(CodeBuffer buffer) { + var t = _triggered ? 'triggered' : 'not triggered'; + buffer + ..writeln('safe($t) (') + ..indent(); + parser.stringify(buffer); + buffer + ..outdent() + ..writeln(')'); + } +} diff --git a/packages/combinator/lib/src/combinator/to_list.dart b/packages/combinator/lib/src/combinator/to_list.dart new file mode 100644 index 00000000..c2809975 --- /dev/null +++ b/packages/combinator/lib/src/combinator/to_list.dart @@ -0,0 +1,41 @@ +part of lex.src.combinator; + +class _ToList extends ListParser { + final Parser parser; + + _ToList(this.parser); + + @override + ParseResult> __parse(ParseArgs args) { + var result = parser._parse(args.increaseDepth()); + + if (result.value is List) { + return (result as ParseResult>).change(parser: this); + } + + List values = []; + if (result.value != null) { + values.add(result.value!); + } + return ParseResult( + args.trampoline, + args.scanner, + this, + result.successful, + result.errors, + span: result.span, + value: values, + ); + } + + @override + void stringify(CodeBuffer buffer) { + buffer + ..writeln('to list (') + ..indent(); + parser.stringify(buffer); + buffer + ..outdent() + ..writeln(')'); + } +} diff --git a/packages/combinator/lib/src/combinator/util.dart b/packages/combinator/lib/src/combinator/util.dart new file mode 100644 index 00000000..cb5dcdc7 --- /dev/null +++ b/packages/combinator/lib/src/combinator/util.dart @@ -0,0 +1,57 @@ +part of lex.src.combinator; + +/// A typed parser that parses a sequence of 2 values of different types. +Parser> tuple2(Parser a, Parser b) { + return chain([a, b]).map((r) { + return Tuple2(r.value?[0] as A, r.value?[1] as B); + }); +} + +/// A typed parser that parses a sequence of 3 values of different types. +Parser> tuple3(Parser a, Parser b, Parser c) { + return chain([a, b, c]).map((r) { + return Tuple3(r.value?[0] as A, r.value?[1] as B, r.value?[2] as C); + }); +} + +/// A typed parser that parses a sequence of 4 values of different types. +Parser> tuple4( + Parser a, Parser b, Parser c, Parser d) { + return chain([a, b, c, d]).map((r) { + return Tuple4( + r.value?[0] as A, r.value?[1] as B, r.value?[2] as C, r.value?[3] as D); + }); +} + +/// A typed parser that parses a sequence of 5 values of different types. +Parser> tuple5( + Parser a, Parser b, Parser c, Parser d, Parser e) { + return chain([a, b, c, d, e]).map((r) { + return Tuple5(r.value?[0] as A, r.value?[1] as B, r.value?[2] as C, + r.value?[3] as D, r.value?[4] as E); + }); +} + +/// A typed parser that parses a sequence of 6 values of different types. +Parser> tuple6(Parser a, + Parser b, Parser c, Parser d, Parser e, Parser f) { + return chain([a, b, c, d, e, f]).map((r) { + return Tuple6(r.value?[0] as A, r.value?[1] as B, r.value?[2] as C, + r.value?[3] as D, r.value?[4] as E, r.value?[5] as F); + }); +} + +/// A typed parser that parses a sequence of 7 values of different types. +Parser> tuple7( + Parser a, + Parser b, + Parser c, + Parser d, + Parser e, + Parser f, + Parser g) { + return chain([a, b, c, d, e, f, g]).map((r) { + return Tuple7(r.value?[0] as A, r.value?[1] as B, r.value?[2] as C, + r.value?[3] as D, r.value?[4] as E, r.value?[5] as F, r.value?[6] as G); + }); +} diff --git a/packages/combinator/lib/src/combinator/value.dart b/packages/combinator/lib/src/combinator/value.dart new file mode 100644 index 00000000..b0612073 --- /dev/null +++ b/packages/combinator/lib/src/combinator/value.dart @@ -0,0 +1,25 @@ +part of lex.src.combinator; + +class _Value extends Parser { + final Parser parser; + final T Function(ParseResult) f; + + _Value(this.parser, this.f); + + @override + ParseResult __parse(ParseArgs args) { + var result = parser._parse(args.increaseDepth()).change(parser: this); + return result.successful ? result.change(value: f(result)) : result; + } + + @override + void stringify(CodeBuffer buffer) { + buffer + ..writeln('set value($f) (') + ..indent(); + parser.stringify(buffer); + buffer + ..outdent() + ..writeln(')'); + } +} diff --git a/packages/combinator/lib/src/error.dart b/packages/combinator/lib/src/error.dart new file mode 100644 index 00000000..6c7b5f51 --- /dev/null +++ b/packages/combinator/lib/src/error.dart @@ -0,0 +1,23 @@ +import 'package:source_span/source_span.dart'; + +class SyntaxError implements Exception { + final SyntaxErrorSeverity severity; + final String? message; + final FileSpan? span; + String? _toolString; + + SyntaxError(this.severity, this.message, this.span); + + String? get toolString { + if (_toolString != null) return _toolString; + var type = severity == SyntaxErrorSeverity.warning ? 'warning' : 'error'; + return _toolString = '$type: ${span!.start.toolString}: $message'; + } +} + +enum SyntaxErrorSeverity { + warning, + error, + info, + hint, +} diff --git a/packages/combinator/pubspec.yaml b/packages/combinator/pubspec.yaml new file mode 100644 index 00000000..c937548f --- /dev/null +++ b/packages/combinator/pubspec.yaml @@ -0,0 +1,14 @@ +name: angel3_combinator +version: 2.0.1 +description: Packrat parser combinators that support static typing, generics, file spans, memoization, and more. +homepage: https://github.com/dukefirehawk/angel/tree/angel3/packages/combinator +environment: + sdk: '>=2.12.0 <3.0.0' +dependencies: + angel3_code_buffer: ^2.0.0 + matcher: ^0.12.10 + source_span: ^1.8.1 + string_scanner: ^1.1.0 + tuple: ^2.0.0 +dev_dependencies: + test: ^1.17.4 \ No newline at end of file diff --git a/packages/combinator/test/all.dart b/packages/combinator/test/all.dart new file mode 100644 index 00000000..9022d5fa --- /dev/null +++ b/packages/combinator/test/all.dart @@ -0,0 +1,12 @@ +import 'package:test/test.dart'; +import 'list_test.dart' as list; +import 'match_test.dart' as match; +import 'misc_test.dart' as misc; +import 'value_test.dart' as value; + +void main() { + group('list', list.main); + group('match', match.main); + group('value', value.main); + misc.main(); +} diff --git a/packages/combinator/test/common.dart b/packages/combinator/test/common.dart new file mode 100644 index 00000000..9ccc542b --- /dev/null +++ b/packages/combinator/test/common.dart @@ -0,0 +1,3 @@ +import 'package:string_scanner/string_scanner.dart'; + +SpanScanner scan(String text) => SpanScanner(text); diff --git a/packages/combinator/test/list_test.dart b/packages/combinator/test/list_test.dart new file mode 100644 index 00000000..c137eae0 --- /dev/null +++ b/packages/combinator/test/list_test.dart @@ -0,0 +1,22 @@ +import 'package:angel3_combinator/angel3_combinator.dart'; +import 'package:test/test.dart'; +import 'common.dart'; + +main() { + var number = chain([ + match(RegExp(r'[0-9]+')).value((r) => int.parse(r.span!.text)), + match(',').opt(), + ]).first().cast(); + + var numbers = number.plus(); + + test('sort', () { + var parser = numbers.sort((a, b) => a.compareTo(b)); + expect(parser.parse(scan('21,2,3,34,20')).value, [2, 3, 20, 21, 34]); + }); + test('reduce', () { + var parser = numbers.reduce((a, b) => a + b); + expect(parser.parse(scan('21,2,3,34,20')).value, 80); + expect(parser.parse(scan('not numbers')).value, isNull); + }); +} diff --git a/packages/combinator/test/match_test.dart b/packages/combinator/test/match_test.dart new file mode 100644 index 00000000..b2a4bcc4 --- /dev/null +++ b/packages/combinator/test/match_test.dart @@ -0,0 +1,16 @@ +import 'package:angel3_combinator/angel3_combinator.dart'; +import 'package:test/test.dart'; +import 'common.dart'; + +void main() { + test('match string', () { + expect(match('hello').parse(scan('hello world')).successful, isTrue); + }); + test('match start only', () { + expect(match('hello').parse(scan('goodbye hello')).successful, isFalse); + }); + + test('fail if no match', () { + expect(match('hello').parse(scan('world')).successful, isFalse); + }); +} diff --git a/packages/combinator/test/misc_test.dart b/packages/combinator/test/misc_test.dart new file mode 100644 index 00000000..aecdc1fb --- /dev/null +++ b/packages/combinator/test/misc_test.dart @@ -0,0 +1,66 @@ +import 'package:angel3_combinator/angel3_combinator.dart'; +import 'package:matcher/matcher.dart'; +import 'package:test/test.dart'; +import 'common.dart'; + +void main() { + test('advance', () { + var scanner = scan('hello world'); + + // Casted -> dynamic just for the sake of coverage. + var parser = match('he').forward(2).castDynamic(); + parser.parse(scanner); + expect(scanner.position, 4); + }); + + test('change', () { + var parser = match('hello').change((r) => r.change(value: 23)); + expect(parser.parse(scan('helloworld')).value, 23); + }); + + test('check', () { + var parser = match(RegExp(r'[A-Za-z]+')) + .value((r) => r.span!.length) + .check(greaterThan(3)); + expect(parser.parse(scan('helloworld')).successful, isTrue); + expect(parser.parse(scan('yo')).successful, isFalse); + }); + + test('map', () { + var parser = match(RegExp(r'[A-Za-z]+')).map((r) => r.span!.length); + expect(parser.parse(scan('hello')).value, 5); + }); + + test('negate', () { + var parser = match('hello').negate(errorMessage: 'world'); + expect(parser.parse(scan('goodbye world')).successful, isTrue); + expect(parser.parse(scan('hello world')).successful, isFalse); + expect(parser.parse(scan('hello world')).errors.first.message, 'world'); + }); + + group('opt', () { + var single = match('hello').opt(backtrack: true); + var list = match('hel').then(match('lo')).opt(); + + test('succeeds if present', () { + expect(single.parse(scan('hello')).successful, isTrue); + expect(list.parse(scan('hello')).successful, isTrue); + }); + + test('succeeds if not present', () { + expect(single.parse(scan('goodbye')).successful, isTrue); + expect(list.parse(scan('goodbye')).successful, isTrue); + }); + + test('backtracks if not present', () { + for (var parser in [single, list]) { + var scanner = scan('goodbye'); + var pos = scanner.position; + parser.parse(scanner); + expect(scanner.position, pos); + } + }); + }); + + test('safe', () {}); +} diff --git a/packages/combinator/test/recursion_test.dart b/packages/combinator/test/recursion_test.dart new file mode 100644 index 00000000..748883d8 --- /dev/null +++ b/packages/combinator/test/recursion_test.dart @@ -0,0 +1,57 @@ +import 'package:angel3_combinator/angel3_combinator.dart'; +import 'package:string_scanner/string_scanner.dart'; +import 'package:test/test.dart'; + +void main() {} + +/* +void main() { + var number = match( RegExp(r'-?[0-9]+(\.[0-9]+)?')) + .map((r) => num.parse(r.span.text)); + + var term = reference(); + + var r = Recursion(); + + r.prefix = [number]; + + r.infix.addAll({ + match('*'): (l, r, _) => l * r, + match('/'): (l, r, _) => l / r, + match('+'): (l, r, _) => l + r, + match('-'): (l, r, _) => l - r, + + + match('-'): (l, r, _) => l - r, + match('+'): (l, r, _) => l + r, + match('/'): (l, r, _) => l / r, + match('*'): (l, r, _) => l * r, + + }); + + term.parser = r.precedence(0); + + num parse(String text) { + var scanner = SpanScanner(text); + var result = term.parse(scanner); + print(result.span.highlight()); + return result.value; + } + + test('prefix', () { + expect(parse('24'), 24); + }); + + test('infix', () { + expect(parse('12/6'), 2); + expect(parse('24+23'), 47); + expect(parse('24-23'), 1); + expect(parse('4*3'), 12); + }); + + test('precedence', () { + expect(parse('2+3*5*2'), 15); + //expect(parse('2+3+5-2*2'), 15); + }); +} +*/ diff --git a/packages/combinator/test/value_test.dart b/packages/combinator/test/value_test.dart new file mode 100644 index 00000000..7f2cb102 --- /dev/null +++ b/packages/combinator/test/value_test.dart @@ -0,0 +1,15 @@ +import 'package:angel3_combinator/angel3_combinator.dart'; +import 'package:test/test.dart'; +import 'common.dart'; + +void main() { + var parser = match('hello').value((r) => 'world'); + + test('sets value', () { + expect(parser.parse(scan('hello world')).value, 'world'); + }); + + test('no value if no match', () { + expect(parser.parse(scan('goodbye world')).value, isNull); + }); +} diff --git a/packages/configuration/.gitignore b/packages/configuration/.gitignore index 5e6bb7e7..24d68312 100644 --- a/packages/configuration/.gitignore +++ b/packages/configuration/.gitignore @@ -1,63 +1,32 @@ -# Created by .ignore support plugin (hsz.mobi) -### JetBrains template -.idea -*.iml +# See https://www.dartlang.org/tools/private-files.html -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 +# Files and directories created by pub +.dart_tool +.packages +.pub/ +build/ -# User-specific stuff: -.idea/workspace.xml -.idea/tasks.xml -.idea/dictionaries -.idea/vcs.xml -.idea/jsLibraryMappings.xml +# If you're building an application, you may want to check-in your pubspec.lock +pubspec.lock -# Sensitive or high-churn files: -.idea/dataSources.ids -.idea/dataSources.xml -.idea/dataSources.local.xml -.idea/sqlDataSources.xml -.idea/dynamic.xml -.idea/uiDesigner.xml +# Directory created by dartdoc +# If you don't generate documentation locally you can remove this line. +doc/api/ -# 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 - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties ### 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 @@ -70,10 +39,33 @@ 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 -.dart_tool \ No newline at end of file +# 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 diff --git a/packages/configuration/AUTHORS.md b/packages/configuration/AUTHORS.md new file mode 100644 index 00000000..ac95ab58 --- /dev/null +++ b/packages/configuration/AUTHORS.md @@ -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. diff --git a/packages/configuration/CHANGELOG.md b/packages/configuration/CHANGELOG.md index 517932aa..d4292b2f 100644 --- a/packages/configuration/CHANGELOG.md +++ b/packages/configuration/CHANGELOG.md @@ -1,3 +1,9 @@ +# 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.2.0 * Allow including one configuration within another. * Badly-formatted `.env` files will no longer issue a warning, diff --git a/packages/configuration/LICENSE b/packages/configuration/LICENSE index eb4ce33e..b593ac86 100644 --- a/packages/configuration/LICENSE +++ b/packages/configuration/LICENSE @@ -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 diff --git a/packages/configuration/README.md b/packages/configuration/README.md index 7fa10919..2a709ecd 100644 --- a/packages/configuration/README.md +++ b/packages/configuration/README.md @@ -1,7 +1,10 @@ -# configuration +# angel3_configuration +[![version](https://img.shields.io/badge/pub-v4.0.0-brightgreen)](https://pub.dartlang.org/packages/angel3_configuration) +[![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/configuration/LICENSE) -[![Pub](https://img.shields.io/pub/v/angel_configuration.svg)](https://pub.dartlang.org/packages/angel_configuration) -[![build status](https://travis-ci.org/angel-dart/configuration.svg)](https://travis-ci.org/angel-dart/configuration) Automatic YAML configuration loader for Angel. @@ -17,7 +20,7 @@ In `pubspec.yaml`: ```yaml dependencies: - angel_configuration: ^2.0.0 + angel3_configuration: ^3.0.0 ``` # Usage diff --git a/packages/configuration/example/main.dart b/packages/configuration/example/main.dart index 1344cc1e..4a9e435b 100644 --- a/packages/configuration/example/main.dart +++ b/packages/configuration/example/main.dart @@ -1,7 +1,7 @@ import 'dart:async'; -import 'package:angel_configuration/angel_configuration.dart'; -import 'package:angel_framework/angel_framework.dart'; +import 'package:angel3_configuration/angel3_configuration.dart'; +import 'package:angel3_framework/angel3_framework.dart'; import 'package:file/local.dart'; Future main() async { diff --git a/packages/configuration/lib/angel_configuration.dart b/packages/configuration/lib/angel3_configuration.dart similarity index 87% rename from packages/configuration/lib/angel_configuration.dart rename to packages/configuration/lib/angel3_configuration.dart index 60b9f72a..41615436 100644 --- a/packages/configuration/lib/angel_configuration.dart +++ b/packages/configuration/lib/angel3_configuration.dart @@ -1,11 +1,11 @@ -library angel_configuration; +library angel3_configuration; import 'dart:async'; -import 'package:angel_framework/angel_framework.dart'; +import 'package:angel3_framework/angel3_framework.dart'; import 'package:dotenv/dotenv.dart' as dotenv; import 'package:file/file.dart'; -import 'package:merge_map/merge_map.dart'; +import 'package:angel3_merge_map/angel3_merge_map.dart'; import 'package:yaml/yaml.dart'; Future _loadYamlFile(Map map, File yamlFile, Map env, @@ -21,7 +21,7 @@ Future _loadYamlFile(Map map, File yamlFile, Map env, var out = {}; - var configMap = Map.of(config as Map); + var configMap = Map.of(config); // Check for _include if (configMap.containsKey('_include')) { @@ -44,8 +44,8 @@ Future _loadYamlFile(Map map, File yamlFile, Map env, } } - for (String key in configMap.keys) { - out[key] = _applyEnv(configMap[key], env ?? {}, warn); + for (var key in configMap.keys) { + out[key] = _applyEnv(configMap[key], env, warn); } map.addAll(mergeMap( @@ -58,7 +58,7 @@ Future _loadYamlFile(Map map, File yamlFile, Map env, } } -Object _applyEnv( +Object? _applyEnv( var v, Map env, void Function(String msg) warn) { if (v is String) { if (v.startsWith(r'$') && v.length > 1) { @@ -74,10 +74,10 @@ Object _applyEnv( return v; } } else if (v is Iterable) { - return v.map((x) => _applyEnv(x, env ?? {}, warn)).toList(); + return v.map((x) => _applyEnv(x, env, warn)).toList(); } else if (v is Map) { return v.keys - .fold({}, (out, k) => out..[k] = _applyEnv(v[k], env ?? {}, warn)); + .fold({}, (out, k) => out..[k] = _applyEnv(v[k], env, warn)); } else { return v; } @@ -88,9 +88,9 @@ Object _applyEnv( /// You can override [onWarning]; otherwise, configuration errors will throw. Future loadStandaloneConfiguration(FileSystem fileSystem, {String directoryPath = './config', - String overrideEnvironmentName, - String envPath, - void Function(String message) onWarning}) async { + String? overrideEnvironmentName, + String? envPath, + void Function(String message)? onWarning}) async { var sourceDirectory = fileSystem.directory(directoryPath); var env = dotenv.env; var envFile = sourceDirectory.childFile(envPath ?? '.env'); @@ -127,8 +127,8 @@ Future loadStandaloneConfiguration(FileSystem fileSystem, /// You can also specify a custom [envPath] to load system configuration from. AngelConfigurer configuration(FileSystem fileSystem, {String directoryPath = './config', - String overrideEnvironmentName, - String envPath}) { + String? overrideEnvironmentName, + String? envPath}) { return (Angel app) async { var config = await loadStandaloneConfiguration( fileSystem, diff --git a/packages/configuration/pubspec.yaml b/packages/configuration/pubspec.yaml index 0108b57e..7a92a6ed 100644 --- a/packages/configuration/pubspec.yaml +++ b/packages/configuration/pubspec.yaml @@ -1,24 +1,18 @@ -name: angel_configuration +name: angel3_configuration description: Automatic YAML application configuration loader for Angel, with .env support. -version: 3.0.0 -author: Tobe O -homepage: https://github.com/angel-dart/angel_configuration -publish_to: none +version: 4.0.0 +homepage: https://github.com/dukefirehawk/angel/tree/angel3/packages/configuration 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 - dotenv: ^3.0.0-nullsafety.0 -# file: ^5.0.0 - merge_map: ^1.0.0 + angel3_framework: ^4.0.0 + angel3_merge_map: ^2.0.0 + dotenv: ^3.0.0 + file: ^6.1.0 yaml: ^3.1.0 dev_dependencies: io: ^1.0.0 -# logging: ^0.11.0 - pedantic: ^1.0.0 -# pretty_logging: ^1.0.0 - test: ^1.15.7 + logging: ^1.0.1 + pedantic: ^1.11.0 + angel3_pretty_logging: ^3.0.0 + test: ^1.17.3 diff --git a/packages/configuration/test/all_test.dart b/packages/configuration/test/all_test.dart index d9839ef0..c7eea151 100644 --- a/packages/configuration/test/all_test.dart +++ b/packages/configuration/test/all_test.dart @@ -1,14 +1,15 @@ import 'dart:async'; -import 'package:angel_framework/angel_framework.dart'; -import 'package:angel_configuration/angel_configuration.dart'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_configuration/angel3_configuration.dart'; +import 'package:angel3_pretty_logging/angel3_pretty_logging.dart'; import 'package:file/local.dart'; import 'package:io/ansi.dart'; import 'package:logging/logging.dart'; import 'package:test/test.dart'; Future main() async { - //Logger.root.onRecord.listen(prettyLog); + Logger.root.onRecord.listen(prettyLog); // Note: Set ANGEL_ENV to 'development' var app = Angel(logger: Logger('angel_configuration')); diff --git a/packages/container/angel_container/.gitignore b/packages/container/angel_container/.gitignore index 7bf00e82..24d68312 100644 --- a/packages/container/angel_container/.gitignore +++ b/packages/container/angel_container/.gitignore @@ -1,13 +1,71 @@ -# See https://www.dartlang.org/guides/libraries/private-files +# See https://www.dartlang.org/tools/private-files.html # Files and directories created by pub -.dart_tool/ +.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 diff --git a/packages/container/angel_container/AUTHORS.md b/packages/container/angel_container/AUTHORS.md new file mode 100644 index 00000000..ac95ab58 --- /dev/null +++ b/packages/container/angel_container/AUTHORS.md @@ -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. diff --git a/packages/container/angel_container/CHANGELOG.md b/packages/container/angel_container/CHANGELOG.md index 63a7658b..d24bbd56 100644 --- a/packages/container/angel_container/CHANGELOG.md +++ b/packages/container/angel_container/CHANGELOG.md @@ -1,3 +1,12 @@ +# 3.0.1 +* Updated README + +# 3.0.0 +* Migrated to support Dart SDK 2.12.x NNBD + +# 2.0.0 +* Migrated to work with Dart SDK 2.12.x Non NNBD + # 1.1.0 * `pedantic` lints. * Add `ThrowingReflector`, which throws on all operations. diff --git a/packages/container/angel_container/LICENSE b/packages/container/angel_container/LICENSE index f8e6088a..b593ac86 100644 --- a/packages/container/angel_container/LICENSE +++ b/packages/container/angel_container/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 The Angel Framework +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 diff --git a/packages/container/angel_container/README.md b/packages/container/angel_container/README.md index 11f96719..8ed24cdd 100644 --- a/packages/container/angel_container/README.md +++ b/packages/container/angel_container/README.md @@ -1,2 +1,12 @@ -# container +# angel3_container +[![version](https://img.shields.io/badge/pub-v3.0.1-brightgreen)](https://pub.dartlang.org/packages/angel3_container) +[![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/container/angel_container/LICENSE) + A better IoC container for Angel, ultimately allowing Angel to be used without dart:mirrors. + +```dart +import 'package:angel3_container/angel3_container.dart'; +``` \ No newline at end of file diff --git a/packages/container/angel_container/example/main.dart b/packages/container/angel_container/example/main.dart index 3ee5106b..6676d35e 100644 --- a/packages/container/angel_container/example/main.dart +++ b/packages/container/angel_container/example/main.dart @@ -1,7 +1,7 @@ import 'dart:async'; -import 'package:angel_container/angel_container.dart'; -import 'package:angel_container/mirrors.dart'; +import 'package:angel3_container/angel3_container.dart'; +import 'package:angel3_container/mirrors.dart'; Future main() async { // Create a container instance. @@ -21,14 +21,14 @@ Future main() async { }); // Use `make` to create an instance. - var truck = container.make(); + var truck = container.make()!; // You can also resolve injections asynchronously. container.registerFactory>((_) async => 24); print(await container.makeAsync()); // Asynchronous resolution also works for plain objects. - await container.makeAsync().then((t) => t.drive()); + await container.makeAsync()!.then((t) => t.drive()); // Register a named singleton. container.registerNamedSingleton('the_truck', truck); @@ -37,7 +37,7 @@ Future main() async { truck.drive(); // Should print the same. - container.findByName('the_truck').drive(); + container.findByName('the_truck')!.drive(); // We can make a child container with its own factory. var childContainer = container.createChild(); @@ -47,10 +47,10 @@ Future main() async { }); // Make a truck with 5666 HP. - childContainer.make().drive(); + childContainer.make()!.drive(); // However, calling `make` will return the Engine singleton we created above. - print(childContainer.make().horsePower); + print(childContainer.make()!.horsePower); } abstract class Truck { @@ -64,12 +64,12 @@ class Engine { } class _TruckImpl implements Truck { - final Engine engine; + final Engine? engine; _TruckImpl(this.engine); @override void drive() { - print('Vroom! I have ${engine.horsePower} horsepower in my engine.'); + print('Vroom! I have ${engine!.horsePower} horsepower in my engine.'); } } diff --git a/packages/container/angel_container/example/throwing.dart b/packages/container/angel_container/example/throwing.dart index f6032417..836bc78c 100644 --- a/packages/container/angel_container/example/throwing.dart +++ b/packages/container/angel_container/example/throwing.dart @@ -1,4 +1,4 @@ -import 'package:angel_container/angel_container.dart'; +import 'package:angel3_container/angel3_container.dart'; void main() { var reflector = const ThrowingReflector(); diff --git a/packages/container/angel_container/lib/angel_container.dart b/packages/container/angel_container/lib/angel3_container.dart similarity index 87% rename from packages/container/angel_container/lib/angel_container.dart rename to packages/container/angel_container/lib/angel3_container.dart index e47500f6..18bce0fe 100644 --- a/packages/container/angel_container/lib/angel_container.dart +++ b/packages/container/angel_container/lib/angel3_container.dart @@ -1,4 +1,4 @@ -library angel_container; +library angel3_container; export 'src/container.dart'; export 'src/empty/empty.dart'; diff --git a/packages/container/angel_container/lib/src/container.dart b/packages/container/angel_container/lib/src/container.dart index 3ab78bde..398aeae4 100644 --- a/packages/container/angel_container/lib/src/container.dart +++ b/packages/container/angel_container/lib/src/container.dart @@ -7,11 +7,11 @@ class Container { final Map _singletons = {}; final Map _factories = {}; final Map _namedSingletons = {}; - final Container _parent; + final Container? _parent; Container(this.reflector) : _parent = null; - Container._child(this._parent) : reflector = _parent.reflector; + Container._child(Container this._parent) : reflector = _parent.reflector; bool get isRoot => _parent == null; @@ -23,8 +23,8 @@ class Container { } /// Determines if the container has an injection of the given type. - bool has([Type t]) { - var search = this; + bool has([Type? t]) { + Container? search = this; t ??= T == dynamic ? t : T; while (search != null) { @@ -42,7 +42,7 @@ class Container { /// Determines if the container has a named singleton with the given [name]. bool hasNamed(String name) { - var search = this; + Container? search = this; while (search != null) { if (search._namedSingletons.containsKey(name)) { @@ -59,9 +59,9 @@ class Container { /// /// It is similar to [make], but resolves an injection of either /// `Future` or `T`. - Future makeAsync([Type type]) { + Future? makeAsync([Type? type]) { type ??= T; - Type futureType; //.Future.value(null).runtimeType; + Type? futureType; //.Future.value(null).runtimeType; if (T == dynamic) { try { @@ -87,18 +87,18 @@ class Container { /// /// In contexts where a static generic type cannot be used, use /// the [type] argument, instead of [T]. - T make([Type type]) { + T? make([Type? type]) { type ??= T; - var search = this; + Container? search = this; while (search != null) { if (search._singletons.containsKey(type)) { // Find a singleton, if any. - return search._singletons[type] as T; + return search._singletons[type] as T?; } else if (search._factories.containsKey(type)) { // Find a factory, if any. - return search._factories[type](this) as T; + return search._factories[type]!(this) as T?; } else { search = search._parent; } @@ -115,8 +115,8 @@ class Container { var constructor = reflectedType.constructors.firstWhere( (c) => isDefault(c.name), - orElse: () => throw ReflectionException( - '${reflectedType.name} has no default constructor, and therefore cannot be instantiated.')); + orElse: (() => throw ReflectionException( + '${reflectedType.name} has no default constructor, and therefore cannot be instantiated.')) as ReflectedFunction Function()?); for (var param in constructor.parameters) { var value = make(param.type.reflectedType); @@ -131,7 +131,7 @@ class Container { return reflectedType.newInstance( isDefault(constructor.name) ? '' : constructor.name, positional, - named, []).reflectee as T; + named, []).reflectee as T?; } else { throw ReflectionException( '$type is not a class, and therefore cannot be instantiated.'); @@ -144,7 +144,7 @@ class Container { /// /// Returns [f]. T Function(Container) registerLazySingleton(T Function(Container) f, - {Type as}) { + {Type? as}) { return registerFactory( (container) { var r = f(container); @@ -159,7 +159,7 @@ class Container { /// type within *this* container will return the result of [f]. /// /// Returns [f]. - T Function(Container) registerFactory(T Function(Container) f, {Type as}) { + T Function(Container) registerFactory(T Function(Container) f, {Type? as}) { as ??= T; if (_factories.containsKey(as)) { @@ -174,7 +174,7 @@ class Container { /// type within *this* container will return [object]. /// /// Returns [object]. - T registerSingleton(T object, {Type as}) { + T registerSingleton(T object, {Type? as}) { as ??= T == dynamic ? as : T; if (_singletons.containsKey(as ?? object.runtimeType)) { @@ -192,11 +192,11 @@ class Container { /// /// [findByName] is best reserved for internal logic that end users of code should /// not see. - T findByName(String name) { + T? findByName(String name) { if (_namedSingletons.containsKey(name)) { - return _namedSingletons[name] as T; + return _namedSingletons[name] as T?; } else if (_parent != null) { - return _parent.findByName(name); + return _parent!.findByName(name); } else { throw StateError( 'This container does not have a singleton named "$name".'); diff --git a/packages/container/angel_container/lib/src/empty/empty.dart b/packages/container/angel_container/lib/src/empty/empty.dart index 9a8bb221..0cb08cd7 100644 --- a/packages/container/angel_container/lib/src/empty/empty.dart +++ b/packages/container/angel_container/lib/src/empty/empty.dart @@ -1,6 +1,6 @@ -import '../../angel_container.dart'; +import '../../angel3_container.dart'; -final Map _symbolNames = {}; +final Map _symbolNames = {}; /// A [Reflector] implementation that performs no actual reflection, /// instead returning empty objects on every invocation. @@ -13,9 +13,9 @@ class EmptyReflector extends Reflector { const EmptyReflector(); @override - String getName(Symbol symbol) { + String? getName(Symbol symbol) { return _symbolNames.putIfAbsent( - symbol, () => symbolRegex.firstMatch(symbol.toString()).group(1)); + symbol, () => symbolRegex.firstMatch(symbol.toString())!.group(1)); } @override @@ -52,13 +52,13 @@ class _EmptyReflectedClass extends ReflectedClass { @override ReflectedInstance newInstance( String constructorName, List positionalArguments, - [Map namedArguments, List typeArguments]) { + [Map? namedArguments, List? typeArguments]) { throw UnsupportedError( 'Classes reflected via an EmptyReflector cannot be instantiated.'); } @override - bool isAssignableTo(ReflectedType other) { + bool isAssignableTo(ReflectedType? other) { return other == this; } @@ -75,13 +75,13 @@ class _EmptyReflectedType extends ReflectedType { @override ReflectedInstance newInstance( String constructorName, List positionalArguments, - [Map namedArguments, List typeArguments]) { + [Map? namedArguments, List? typeArguments]) { throw UnsupportedError( 'Types reflected via an EmptyReflector cannot be instantiated.'); } @override - bool isAssignableTo(ReflectedType other) { + bool isAssignableTo(ReflectedType? other) { return other == this; } diff --git a/packages/container/angel_container/lib/src/mirrors/reflector.dart b/packages/container/angel_container/lib/src/mirrors/reflector.dart index 74639a68..d3d26835 100644 --- a/packages/container/angel_container/lib/src/mirrors/reflector.dart +++ b/packages/container/angel_container/lib/src/mirrors/reflector.dart @@ -85,7 +85,7 @@ class _ReflectedTypeMirror extends ReflectedType { ); @override - bool isAssignableTo(ReflectedType other) { + bool isAssignableTo(ReflectedType? other) { if (other is _ReflectedClassMirror) { return mirror.isAssignableTo(other.mirror); } else if (other is _ReflectedTypeMirror) { @@ -98,7 +98,7 @@ class _ReflectedTypeMirror extends ReflectedType { @override ReflectedInstance newInstance( String constructorName, List positionalArguments, - [Map namedArguments, List typeArguments]) { + [Map? namedArguments, List? typeArguments]) { throw ReflectionException( '$name is not a class, and therefore cannot be instantiated.'); } @@ -154,7 +154,7 @@ class _ReflectedClassMirror extends ReflectedClass { List get constructors => _constructorsOf(mirror); @override - bool isAssignableTo(ReflectedType other) { + bool isAssignableTo(ReflectedType? other) { if (other is _ReflectedClassMirror) { return mirror.isAssignableTo(other.mirror); } else if (other is _ReflectedTypeMirror) { @@ -167,7 +167,7 @@ class _ReflectedClassMirror extends ReflectedClass { @override ReflectedInstance newInstance( String constructorName, List positionalArguments, - [Map namedArguments, List typeArguments]) { + [Map? namedArguments, List? typeArguments]) { return _ReflectedInstanceMirror( mirror.newInstance(Symbol(constructorName), positionalArguments)); } @@ -207,7 +207,7 @@ class _ReflectedInstanceMirror extends ReflectedInstance { class _ReflectedMethodMirror extends ReflectedFunction { final dart.MethodMirror mirror; - final dart.ClosureMirror closureMirror; + final dart.ClosureMirror? closureMirror; _ReflectedMethodMirror(this.mirror, [this.closureMirror]) : super( @@ -242,7 +242,7 @@ class _ReflectedMethodMirror extends ReflectedFunction { 'This object was reflected without a ClosureMirror, and therefore cannot be directly invoked.'); } - return _ReflectedInstanceMirror(closureMirror.invoke(invocation.memberName, + return _ReflectedInstanceMirror(closureMirror!.invoke(invocation.memberName, invocation.positionalArguments, invocation.namedArguments)); } } diff --git a/packages/container/angel_container/lib/src/reflector.dart b/packages/container/angel_container/lib/src/reflector.dart index 1220a5ab..dfa24be8 100644 --- a/packages/container/angel_container/lib/src/reflector.dart +++ b/packages/container/angel_container/lib/src/reflector.dart @@ -4,15 +4,15 @@ import 'package:quiver/core.dart'; abstract class Reflector { const Reflector(); - String getName(Symbol symbol); + String? getName(Symbol symbol); - ReflectedClass reflectClass(Type clazz); + ReflectedClass? reflectClass(Type clazz); - ReflectedFunction reflectFunction(Function function); + ReflectedFunction? reflectFunction(Function function); - ReflectedType reflectType(Type type); + ReflectedType? reflectType(Type type); - ReflectedInstance reflectInstance(Object object); + ReflectedInstance? reflectInstance(Object object); ReflectedType reflectFutureOf(Type type) { throw UnsupportedError('`reflectFutureOf` requires `dart:mirrors`.'); @@ -22,7 +22,7 @@ abstract class Reflector { abstract class ReflectedInstance { final ReflectedType type; final ReflectedClass clazz; - final Object reflectee; + final Object? reflectee; const ReflectedInstance(this.type, this.clazz, this.reflectee); @@ -56,9 +56,9 @@ abstract class ReflectedType { ReflectedInstance newInstance( String constructorName, List positionalArguments, - [Map namedArguments, List typeArguments]); + [Map? namedArguments, List? typeArguments]); - bool isAssignableTo(ReflectedType other); + bool isAssignableTo(ReflectedType? other); } abstract class ReflectedClass extends ReflectedType { @@ -94,7 +94,7 @@ abstract class ReflectedClass extends ReflectedType { class ReflectedDeclaration { final String name; final bool isStatic; - final ReflectedFunction function; + final ReflectedFunction? function; const ReflectedDeclaration(this.name, this.isStatic, this.function); diff --git a/packages/container/angel_container/lib/src/static/static.dart b/packages/container/angel_container/lib/src/static/static.dart index 645a9fa5..0a915265 100644 --- a/packages/container/angel_container/lib/src/static/static.dart +++ b/packages/container/angel_container/lib/src/static/static.dart @@ -16,7 +16,7 @@ class StaticReflector extends Reflector { this.instances = const {}}); @override - String getName(Symbol symbol) { + String? getName(Symbol symbol) { if (!names.containsKey(symbol)) { throw ArgumentError( 'The value of $symbol is unknown - it was not generated.'); @@ -26,11 +26,11 @@ class StaticReflector extends Reflector { } @override - ReflectedClass reflectClass(Type clazz) => - reflectType(clazz) as ReflectedClass; + ReflectedClass? reflectClass(Type clazz) => + reflectType(clazz) as ReflectedClass?; @override - ReflectedFunction reflectFunction(Function function) { + ReflectedFunction? reflectFunction(Function function) { if (!functions.containsKey(function)) { throw ArgumentError( 'There is no reflection information available about $function.'); @@ -40,7 +40,7 @@ class StaticReflector extends Reflector { } @override - ReflectedInstance reflectInstance(Object object) { + ReflectedInstance? reflectInstance(Object object) { if (!instances.containsKey(object)) { throw ArgumentError( 'There is no reflection information available about $object.'); @@ -50,7 +50,7 @@ class StaticReflector extends Reflector { } @override - ReflectedType reflectType(Type type) { + ReflectedType? reflectType(Type type) { if (!types.containsKey(type)) { throw ArgumentError( 'There is no reflection information available about $type.'); diff --git a/packages/container/angel_container/lib/src/throwing.dart b/packages/container/angel_container/lib/src/throwing.dart index 05a2ea86..5b918fbd 100644 --- a/packages/container/angel_container/lib/src/throwing.dart +++ b/packages/container/angel_container/lib/src/throwing.dart @@ -17,7 +17,7 @@ class ThrowingReflector extends Reflector { const ThrowingReflector({this.errorMessage = defaultErrorMessage}); @override - String getName(Symbol symbol) => const EmptyReflector().getName(symbol); + String? getName(Symbol symbol) => const EmptyReflector().getName(symbol); UnsupportedError _error() => UnsupportedError(errorMessage); diff --git a/packages/container/angel_container/pubspec.yaml b/packages/container/angel_container/pubspec.yaml index d2b83669..0f8b6c60 100644 --- a/packages/container/angel_container/pubspec.yaml +++ b/packages/container/angel_container/pubspec.yaml @@ -1,13 +1,12 @@ -name: angel_container -version: 2.0.0 -author: Tobe O +name: angel3_container +version: 3.0.1 description: A hierarchical DI container, and pluggable backends for reflection. -homepage: https://github.com/angel-dart/container.git +homepage: https://github.com/dukefirehawk/angel/tree/angel3/packages/container/angel_container environment: - sdk: ">=2.10.0 <3.0.0" + sdk: '>=2.12.0 <3.0.0' dependencies: collection: ^1.15.0 - quiver: ^2.1.5 + quiver: ^3.0.1 dev_dependencies: pedantic: ^1.11.0 - test: ^1.16.5 \ No newline at end of file + test: ^1.17.4 \ No newline at end of file diff --git a/packages/container/angel_container/test/common.dart b/packages/container/angel_container/test/common.dart index b638ac9c..0afa75f5 100644 --- a/packages/container/angel_container/test/common.dart +++ b/packages/container/angel_container/test/common.dart @@ -1,13 +1,13 @@ import 'dart:async'; -import 'package:angel_container/angel_container.dart'; +import 'package:angel3_container/angel3_container.dart'; import 'package:test/test.dart'; void returnVoidFromAFunction(int x) {} void testReflector(Reflector reflector) { var blaziken = Pokemon('Blaziken', PokemonType.fire); - Container container; + late Container container; setUp(() { container = Container(reflector); @@ -16,7 +16,7 @@ void testReflector(Reflector reflector) { }); test('get field', () { - var blazikenMirror = reflector.reflectInstance(blaziken); + var blazikenMirror = reflector.reflectInstance(blaziken)!; expect(blazikenMirror.getField('type').reflectee, blaziken.type); }); @@ -24,19 +24,19 @@ void testReflector(Reflector reflector) { var mirror = reflector.reflectFunction(returnVoidFromAFunction); test('void return type returns dynamic', () { - expect(mirror.returnType, reflector.reflectType(dynamic)); + expect(mirror!.returnType, reflector.reflectType(dynamic)); }); test('counts parameters', () { - expect(mirror.parameters, hasLength(1)); + expect(mirror!.parameters, hasLength(1)); }); test('counts types parameters', () { - expect(mirror.typeParameters, isEmpty); + expect(mirror!.typeParameters, isEmpty); }); test('correctly reflects parameter types', () { - var p = mirror.parameters[0]; + var p = mirror!.parameters[0]; expect(p.name, 'x'); expect(p.isRequired, true); expect(p.isNamed, false); @@ -67,12 +67,12 @@ void testReflector(Reflector reflector) { }); test('constructor injects singleton', () { - var lower = container.make(); + var lower = container.make()!; expect(lower.lowercaseName, blaziken.name.toLowerCase()); }); test('newInstance works', () { - var type = container.reflector.reflectType(Pokemon); + var type = container.reflector.reflectType(Pokemon)!; var instance = type.newInstance('changeName', [blaziken, 'Charizard']).reflectee as Pokemon; @@ -83,7 +83,7 @@ void testReflector(Reflector reflector) { test('isAssignableTo', () { var pokemonType = container.reflector.reflectType(Pokemon); - var kantoPokemonType = container.reflector.reflectType(KantoPokemon); + var kantoPokemonType = container.reflector.reflectType(KantoPokemon)!; expect(kantoPokemonType.isAssignableTo(pokemonType), true); expect( diff --git a/packages/container/angel_container/test/empty_reflector_test.dart b/packages/container/angel_container/test/empty_reflector_test.dart index 593ef0f4..6ada3d63 100644 --- a/packages/container/angel_container/test/empty_reflector_test.dart +++ b/packages/container/angel_container/test/empty_reflector_test.dart @@ -1,4 +1,4 @@ -import 'package:angel_container/angel_container.dart'; +import 'package:angel3_container/angel3_container.dart'; import 'package:test/test.dart'; void main() { diff --git a/packages/container/angel_container/test/has_test.dart b/packages/container/angel_container/test/has_test.dart index bd5d21cb..c0d726a9 100644 --- a/packages/container/angel_container/test/has_test.dart +++ b/packages/container/angel_container/test/has_test.dart @@ -1,8 +1,8 @@ -import 'package:angel_container/angel_container.dart'; +import 'package:angel3_container/angel3_container.dart'; import 'package:test/test.dart'; void main() { - Container container; + late Container container; setUp(() { container = Container(const EmptyReflector()) @@ -37,14 +37,14 @@ void main() { } class Artist { - final String name; - final Song song; + final String? name; + final Song? song; Artist({this.name, this.song}); } class Song { - final String title; + final String? title; Song({this.title}); } diff --git a/packages/container/angel_container/test/lazy_test.dart b/packages/container/angel_container/test/lazy_test.dart index c5260702..56631343 100644 --- a/packages/container/angel_container/test/lazy_test.dart +++ b/packages/container/angel_container/test/lazy_test.dart @@ -1,4 +1,4 @@ -import 'package:angel_container/angel_container.dart'; +import 'package:angel3_container/angel3_container.dart'; import 'package:test/test.dart'; void main() { diff --git a/packages/container/angel_container/test/mirrors_test.dart b/packages/container/angel_container/test/mirrors_test.dart index e5f29b9c..86a5fd8e 100644 --- a/packages/container/angel_container/test/mirrors_test.dart +++ b/packages/container/angel_container/test/mirrors_test.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:angel_container/angel_container.dart'; -import 'package:angel_container/mirrors.dart'; +import 'package:angel3_container/angel3_container.dart'; +import 'package:angel3_container/mirrors.dart'; import 'package:test/test.dart'; import 'common.dart'; diff --git a/packages/container/angel_container/test/named_test.dart b/packages/container/angel_container/test/named_test.dart index e4c80079..a80d388b 100644 --- a/packages/container/angel_container/test/named_test.dart +++ b/packages/container/angel_container/test/named_test.dart @@ -1,8 +1,8 @@ -import 'package:angel_container/angel_container.dart'; +import 'package:angel3_container/angel3_container.dart'; import 'package:test/test.dart'; void main() { - Container container; + late Container container; setUp(() { container = Container(const EmptyReflector()); @@ -10,7 +10,7 @@ void main() { }); test('fetch by name', () { - expect(container.findByName('foo').bar, 'baz'); + expect(container.findByName('foo')!.bar, 'baz'); }); test('cannot redefine', () { @@ -28,7 +28,7 @@ void main() { } class Foo { - final String bar; + final String? bar; Foo({this.bar}); } diff --git a/packages/container/angel_container/test/throwing_reflector_test.dart b/packages/container/angel_container/test/throwing_reflector_test.dart index 3125f709..e41e0814 100644 --- a/packages/container/angel_container/test/throwing_reflector_test.dart +++ b/packages/container/angel_container/test/throwing_reflector_test.dart @@ -1,4 +1,4 @@ -import 'package:angel_container/angel_container.dart'; +import 'package:angel3_container/angel3_container.dart'; import 'package:test/test.dart'; void main() { diff --git a/packages/framework/.gitignore b/packages/framework/.gitignore index 0104b702..24d68312 100644 --- a/packages/framework/.gitignore +++ b/packages/framework/.gitignore @@ -1,41 +1,64 @@ -# 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 + +# 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 - -*.iml - -## Directory-based project format: -# if you remove the above rule, at least ignore the following: +# 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 -# Sensitive or high-churn files: -# .idea/dataSources.ids -# .idea/dataSources.xml -# .idea/sqlDataSources.xml -# .idea/dynamic.xml -# .idea/uiDesigner.xml - -# Gradle: -# .idea/gradle.xml -# .idea/libraries - -# Mongo Explorer plugin: -# .idea/mongoSettings.xml +## VsCode +.vscode/ ## File-based project format: -*.ipr *.iws ## Plugin-specific files: # IntelliJ +.idea/ /out/ - -# mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin @@ -45,22 +68,4 @@ atlassian-ide-plugin.xml com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties -### Dart template -# Don’t commit the following directories created by pub. -.buildlog -.pub/ -build/ -packages -.packages - -# Or the files created by dart2js. -*.dart.js -*.js_ -*.js.deps -*.js.map - -# Include when developing application packages. -pubspec.lock - -doc/api -.dart_tool \ No newline at end of file +fabric.properties diff --git a/packages/framework/.vscode/settings.json b/packages/framework/.vscode/settings.json index 20af2f68..b672a778 100644 --- a/packages/framework/.vscode/settings.json +++ b/packages/framework/.vscode/settings.json @@ -1,3 +1,6 @@ // Place your settings in this file to overwrite default and user settings. { + "files.watcherExclude": { + "**/target": true + } } \ No newline at end of file diff --git a/packages/framework/AUTHORS.md b/packages/framework/AUTHORS.md new file mode 100644 index 00000000..ac95ab58 --- /dev/null +++ b/packages/framework/AUTHORS.md @@ -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. diff --git a/packages/framework/CHANGELOG.md b/packages/framework/CHANGELOG.md index a3b932db..d25d881d 100644 --- a/packages/framework/CHANGELOG.md +++ b/packages/framework/CHANGELOG.md @@ -1,3 +1,15 @@ +# 4.0.2 +* Updated README + +# 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.1 * `AngelHttp.uri` now returns an empty `Uri` if the server is not listening. diff --git a/packages/framework/LICENSE b/packages/framework/LICENSE index 32e25c1c..b593ac86 100644 --- a/packages/framework/LICENSE +++ b/packages/framework/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2016 The Angel Framework +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 @@ -18,4 +18,4 @@ 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. \ No newline at end of file +SOFTWARE. diff --git a/packages/framework/README.md b/packages/framework/README.md index 7aecfc36..8cd577b0 100644 --- a/packages/framework/README.md +++ b/packages/framework/README.md @@ -1,16 +1,18 @@ -# angel_framework +# angel3_framework +[![version](https://img.shields.io/badge/pub-v4.0.2-brightgreen)](https://pub.dartlang.org/packages/angel3_framework) +[![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_framework.svg)](https://pub.dartlang.org/packages/angel_framework) -[![build status](https://travis-ci.org/angel-dart/framework.svg)](https://travis-ci.org/angel-dart/framework) +[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/angel3/packages/framework/LICENSE) A high-powered HTTP server with support for dependency injection, sophisticated routing and more. -This is the core of the [Angel](https://github.com/angel-dart/angel) framework. +This is the core of the [Angel](https://github.com/dukefirehawk/angel/tree/angel3) framework. To build real-world applications, please see the [homepage](https://angel-dart.dev). ```dart -import 'package:angel_container/mirrors.dart'; -import 'package:angel_framework/angel_framework.dart'; +import 'package:angel3_container/mirrors.dart'; +import 'package:angel3_framework/angel3_framework.dart'; main() async { var app = Angel(reflector: MirrorsReflector()); diff --git a/packages/framework/TODO.md b/packages/framework/TODO.md index 3381eeda..ab388e20 100644 --- a/packages/framework/TODO.md +++ b/packages/framework/TODO.md @@ -1,3 +1,4 @@ +* Migrate http_server to shelf * Support for [Trestle](https://github.com/dart-bridge/trestle), use this as default, set up migration system around this * Angel CLI * Angel bootstrap project diff --git a/packages/framework/analysis_options.yaml b/packages/framework/analysis_options.yaml index 42d44a85..4e27c95b 100644 --- a/packages/framework/analysis_options.yaml +++ b/packages/framework/analysis_options.yaml @@ -1,4 +1,5 @@ -# include: package:pedantic/analysis_options.yaml +include: package:pedantic/analysis_options.yaml + analyzer: errors: always_declare_return_types: ignore diff --git a/packages/framework/example/controller.dart b/packages/framework/example/controller.dart index 1e0da928..d4de54d6 100644 --- a/packages/framework/example/controller.dart +++ b/packages/framework/example/controller.dart @@ -1,9 +1,9 @@ -import 'package:angel_container/mirrors.dart'; -import 'package:angel_framework/angel_framework.dart'; -import 'package:angel_framework/http.dart'; +import 'package:angel3_container/mirrors.dart'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_framework/http.dart'; import 'package:logging/logging.dart'; -main() async { +void main() async { // Logging set up/boilerplate Logger.root.onRecord.listen(print); @@ -16,7 +16,7 @@ main() async { // Simple fallback to throw a 404 on unknown paths. app.fallback((req, res) { throw AngelHttpException.notFound( - message: 'Unknown path: "${req.uri.path}"', + message: 'Unknown path: "${req.uri!.path}"', ); }); @@ -40,7 +40,7 @@ class ArtistsController extends Controller { form(RequestContext req) async { // Deserialize the body into an artist. var artist = await req.deserializeBody((m) { - return Artist(name: m['name'] as String ?? '(unknown name)'); + return Artist(name: m!['name'] as String? ?? '(unknown name)'); }); // Return it (it will be serialized to JSON). @@ -49,7 +49,7 @@ class ArtistsController extends Controller { } class Artist { - final String name; + final String? name; Artist({this.name}); diff --git a/packages/framework/example/handle_error.dart b/packages/framework/example/handle_error.dart index e159a3ce..89da3fbc 100644 --- a/packages/framework/example/handle_error.dart +++ b/packages/framework/example/handle_error.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'dart:io'; -import 'package:angel_container/mirrors.dart'; -import 'package:angel_framework/angel_framework.dart'; -import 'package:angel_framework/http.dart'; +import 'package:angel3_container/mirrors.dart'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_framework/http.dart'; import 'package:logging/logging.dart'; -main() async { +void main() async { var app = Angel(reflector: MirrorsReflector()) ..logger = (Logger('angel') ..onRecord.listen((rec) { @@ -19,7 +19,7 @@ main() async { (req, res) => Future.error('Throwing just because I feel like!')); var http = AngelHttp(app); - var server = await http.startServer('127.0.0.1', 3000); + HttpServer? server = await http.startServer('127.0.0.1', 3000); var url = 'http://${server.address.address}:${server.port}'; print('Listening at $url'); } diff --git a/packages/framework/example/hostname.dart b/packages/framework/example/hostname.dart index 66003dd0..9f0a9a47 100644 --- a/packages/framework/example/hostname.dart +++ b/packages/framework/example/hostname.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:angel_framework/angel_framework.dart'; -import 'package:angel_framework/http.dart'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_framework/http.dart'; import 'package:logging/logging.dart'; Future apiConfigurer(Angel app) async { @@ -14,7 +14,7 @@ Future frontendConfigurer(Angel app) async { app.fallback((req, res) => '(usually an index page would be shown here.)'); } -main() async { +void main() async { // Logging set up/boilerplate hierarchicalLoggingEnabled = true; //Logger.root.onRecord.listen(prettyLog); @@ -33,7 +33,7 @@ main() async { }); app.errorHandler = (e, req, res) { - print(e.message ?? e.error ?? e); + print(e.message); print(e.stackTrace); return e.toJson(); }; diff --git a/packages/framework/example/http2/body_parsing.dart b/packages/framework/example/http2/body_parsing.dart index a0c27a9c..92bb9a25 100644 --- a/packages/framework/example/http2/body_parsing.dart +++ b/packages/framework/example/http2/body_parsing.dart @@ -1,11 +1,11 @@ import 'dart:io'; -import 'package:angel_framework/angel_framework.dart'; -import 'package:angel_framework/http.dart'; -import 'package:angel_framework/http2.dart'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_framework/http.dart'; +import 'package:angel3_framework/http2.dart'; import 'package:file/local.dart'; import 'package:logging/logging.dart'; -main() async { +void main() async { var app = Angel(); app.logger = Logger('angel') ..onRecord.listen((rec) { @@ -29,7 +29,7 @@ main() async { try { ctx.setAlpnProtocols(['h2'], true); } catch (e, st) { - app.logger.severe( + app.logger!.severe( 'Cannot set ALPN protocol on server to `h2`. The server will only serve HTTP/1.x.', e, st); @@ -41,6 +41,6 @@ main() async { // HTTP/1.x requests will fallback to `AngelHttp` http2.onHttp1.listen(http1.handleRequest); - var server = await http2.startServer('127.0.0.1', 3000); + SecureServerSocket server = await http2.startServer('127.0.0.1', 3000); print('Listening at https://${server.address.address}:${server.port}'); } diff --git a/packages/framework/example/http2/main.dart b/packages/framework/example/http2/main.dart index d8218ae2..d0a93e36 100644 --- a/packages/framework/example/http2/main.dart +++ b/packages/framework/example/http2/main.dart @@ -1,11 +1,11 @@ import 'dart:io'; -import 'package:angel_framework/angel_framework.dart'; -import 'package:angel_framework/http.dart'; -import 'package:angel_framework/http2.dart'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_framework/http.dart'; +import 'package:angel3_framework/http2.dart'; import 'package:logging/logging.dart'; import 'common.dart'; -main() async { +void main() async { var app = Angel() ..encoders.addAll({ 'gzip': gzip.encoder, @@ -25,7 +25,7 @@ main() async { try { ctx.setAlpnProtocols(['h2'], true); } catch (e, st) { - app.logger.severe( + app.logger!.severe( 'Cannot set ALPN protocol on server to `h2`. The server will only serve HTTP/1.x.', e, st, diff --git a/packages/framework/example/http2/server_push.dart b/packages/framework/example/http2/server_push.dart index 41a1170f..e73c862d 100644 --- a/packages/framework/example/http2/server_push.dart +++ b/packages/framework/example/http2/server_push.dart @@ -1,11 +1,11 @@ import 'dart:io'; -import 'package:angel_framework/angel_framework.dart'; -import 'package:angel_framework/http.dart'; -import 'package:angel_framework/http2.dart'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_framework/http.dart'; +import 'package:angel3_framework/http2.dart'; import 'package:file/local.dart'; import 'package:logging/logging.dart'; -main() async { +void main() async { var app = Angel(); app.logger = Logger('angel') ..onRecord.listen((rec) { @@ -45,7 +45,7 @@ main() async { try { ctx.setAlpnProtocols(['h2'], true); } catch (e, st) { - app.logger.severe( + app.logger!.severe( 'Cannot set ALPN protocol on server to `h2`. The server will only serve HTTP/1.x.', e, st); @@ -57,6 +57,6 @@ main() async { // HTTP/1.x requests will fallback to `AngelHttp` http2.onHttp1.listen(http1.handleRequest); - var server = await http2.startServer('127.0.0.1', 3000); + SecureServerSocket server = await http2.startServer('127.0.0.1', 3000); print('Listening at https://${server.address.address}:${server.port}'); } diff --git a/packages/framework/example/json.dart b/packages/framework/example/json.dart index f71a48dc..4a820273 100644 --- a/packages/framework/example/json.dart +++ b/packages/framework/example/json.dart @@ -1,10 +1,10 @@ import 'dart:async'; import 'dart:io'; import 'dart:isolate'; -import 'package:angel_framework/angel_framework.dart'; -import 'package:angel_framework/http.dart'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_framework/http.dart'; -main() async { +void main() async { int x = 0; var c = Completer(); var exit = ReceivePort(); @@ -44,10 +44,10 @@ serverMain(_) async { }); app.errorHandler = (e, req, res) { - print(e.message ?? e.error ?? e); + print(e.message); print(e.stackTrace); }; - var server = await http.startServer('127.0.0.1', 3000); + HttpServer server = await http.startServer('127.0.0.1', 3000); print('Listening at http://${server.address.address}:${server.port}'); } diff --git a/packages/framework/example/main.dart b/packages/framework/example/main.dart index ce99ce09..a2a382ef 100644 --- a/packages/framework/example/main.dart +++ b/packages/framework/example/main.dart @@ -1,9 +1,11 @@ -import 'package:angel_container/mirrors.dart'; -import 'package:angel_framework/angel_framework.dart'; -import 'package:angel_framework/http.dart'; +import 'dart:io'; + +import 'package:angel3_container/mirrors.dart'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_framework/http.dart'; import 'package:logging/logging.dart'; -main() async { +void main() async { // Logging set up/boilerplate //Logger.root.onRecord.listen(prettyLog); @@ -42,12 +44,12 @@ main() async { // Simple fallback to throw a 404 on unknown paths. app.fallback((req, res) { throw AngelHttpException.notFound( - message: 'Unknown path: "${req.uri.path}"', + message: 'Unknown path: "${req.uri!.path}"', ); }); var http = AngelHttp(app); - var server = await http.startServer('127.0.0.1', 3000); + HttpServer server = await http.startServer('127.0.0.1', 3000); var url = 'http://${server.address.address}:${server.port}'; print('Listening at $url'); print('Visit these pages to see Angel in action:'); diff --git a/packages/framework/example/map_service.dart b/packages/framework/example/map_service.dart index 4650dd09..60ea66c8 100644 --- a/packages/framework/example/map_service.dart +++ b/packages/framework/example/map_service.dart @@ -1,9 +1,9 @@ -import 'package:angel_container/mirrors.dart'; -import 'package:angel_framework/angel_framework.dart'; -import 'package:angel_framework/http.dart'; +import 'package:angel3_container/mirrors.dart'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_framework/http.dart'; import 'package:logging/logging.dart'; -main() async { +void main() async { // Logging set up/boilerplate Logger.root.onRecord.listen(print); diff --git a/packages/framework/example/status.dart b/packages/framework/example/status.dart index a434dd90..70fe35c0 100644 --- a/packages/framework/example/status.dart +++ b/packages/framework/example/status.dart @@ -1,7 +1,7 @@ -import 'package:angel_framework/angel_framework.dart'; -import 'package:angel_framework/http.dart'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_framework/http.dart'; -main() async { +void main() async { var app = Angel(); var http = AngelHttp(app); diff --git a/packages/framework/example/view.dart b/packages/framework/example/view.dart index c90fd3cd..8502c4b6 100644 --- a/packages/framework/example/view.dart +++ b/packages/framework/example/view.dart @@ -1,8 +1,10 @@ -import 'package:angel_container/mirrors.dart'; -import 'package:angel_framework/angel_framework.dart'; -import 'package:angel_framework/http.dart'; +import 'dart:io'; -main() async { +import 'package:angel3_container/mirrors.dart'; +import 'package:angel3_framework/angel3_framework.dart'; +import 'package:angel3_framework/http.dart'; + +void main() async { var app = Angel(reflector: MirrorsReflector()); app.viewGenerator = (name, [data]) async => @@ -12,7 +14,7 @@ main() async { app.get('/', (req, res) => res.render('index', {'foo': 'bar'})); var http = AngelHttp(app); - var server = await http.startServer('127.0.0.1', 3000); + HttpServer server = await http.startServer('127.0.0.1', 3000); var url = 'http://${server.address.address}:${server.port}'; print('Listening at $url'); } diff --git a/packages/framework/lib/angel3_framework.dart b/packages/framework/lib/angel3_framework.dart new file mode 100644 index 00000000..529046cb --- /dev/null +++ b/packages/framework/lib/angel3_framework.dart @@ -0,0 +1,7 @@ +/// An easily-extensible web server framework in Dart. +library angel3_framework; + +export 'package:angel3_http_exception/angel3_http_exception.dart'; +export 'package:angel3_model/angel3_model.dart'; +export 'package:angel3_route/angel3_route.dart'; +export 'src/core/core.dart'; diff --git a/packages/framework/lib/angel_framework.dart b/packages/framework/lib/angel_framework.dart deleted file mode 100644 index 60a495e6..00000000 --- a/packages/framework/lib/angel_framework.dart +++ /dev/null @@ -1,7 +0,0 @@ -/// An easily-extensible web server framework in Dart. -library angel_framework; - -export 'package:angel_http_exception/angel_http_exception.dart'; -export 'package:angel_model/angel_model.dart'; -export 'package:angel_route/angel_route.dart'; -export 'src/core/core.dart'; diff --git a/packages/framework/lib/src/core/anonymous_service.dart b/packages/framework/lib/src/core/anonymous_service.dart index f4d86f72..a4c9e471 100644 --- a/packages/framework/lib/src/core/anonymous_service.dart +++ b/packages/framework/lib/src/core/anonymous_service.dart @@ -7,19 +7,22 @@ import 'service.dart'; /// /// Well-suited for testing. class AnonymousService extends Service { - FutureOr> Function([Map]) _index; - FutureOr Function(Id, [Map]) _read, _remove; - FutureOr Function(Data, [Map]) _create; - FutureOr Function(Id, Data, [Map]) _modify, _update; + FutureOr> Function([Map?])? _index; + FutureOr Function(Id, [Map?])? _read, _remove; + FutureOr Function(Data, [Map?])? _create; + FutureOr Function(Id, Data, [Map?])? _modify, _update; AnonymousService( - {FutureOr> index([Map params]), - FutureOr read(Id id, [Map params]), - FutureOr create(Data data, [Map params]), - FutureOr modify(Id id, Data data, [Map params]), - FutureOr update(Id id, Data data, [Map params]), - FutureOr remove(Id id, [Map params]), - FutureOr Function(RequestContext, ResponseContext) readData}) + {FutureOr> Function([Map? params])? index, + FutureOr Function(Id id, [Map? params])? read, + FutureOr Function(Data data, [Map? params])? + create, + FutureOr Function(Id id, Data data, [Map? params])? + modify, + FutureOr Function(Id id, Data data, [Map? params])? + update, + FutureOr Function(Id id, [Map? params])? remove, + FutureOr Function(RequestContext, ResponseContext)? readData}) : super(readData: readData) { _index = index; _read = read; @@ -30,30 +33,30 @@ class AnonymousService extends Service { } @override - index([Map params]) => - Future.sync(() => _index != null ? _index(params) : super.index(params)); + index([Map? params]) => + Future.sync(() => _index != null ? _index!(params) : super.index(params)); @override - read(Id id, [Map params]) => Future.sync( - () => _read != null ? _read(id, params) : super.read(id, params)); + read(Id id, [Map? params]) => Future.sync( + () => _read != null ? _read!(id, params) : super.read(id, params)); @override - create(Data data, [Map params]) => Future.sync(() => - _create != null ? _create(data, params) : super.create(data, params)); + create(Data data, [Map? params]) => Future.sync(() => + _create != null ? _create!(data, params) : super.create(data, params)); @override - modify(Id id, Data data, [Map params]) => + modify(Id id, Data data, [Map? params]) => Future.sync(() => _modify != null - ? _modify(id, data, params) + ? _modify!(id, data, params) : super.modify(id, data, params)); @override - update(Id id, Data data, [Map params]) => + update(Id id, Data data, [Map? params]) => Future.sync(() => _update != null - ? _update(id, data, params) + ? _update!(id, data, params) : super.update(id, data, params)); @override - remove(Id id, [Map params]) => Future.sync( - () => _remove != null ? _remove(id, params) : super.remove(id, params)); + remove(Id id, [Map? params]) => Future.sync( + () => _remove != null ? _remove!(id, params) : super.remove(id, params)); } diff --git a/packages/framework/lib/src/core/controller.dart b/packages/framework/lib/src/core/controller.dart index e531b12b..fd29b0de 100644 --- a/packages/framework/lib/src/core/controller.dart +++ b/packages/framework/lib/src/core/controller.dart @@ -1,18 +1,18 @@ library angel_framework.http.controller; import 'dart:async'; -import 'package:angel_container/angel_container.dart'; -import 'package:angel_route/angel_route.dart'; +import 'package:angel3_container/angel3_container.dart'; +import 'package:angel3_route/angel3_route.dart'; import 'package:meta/meta.dart'; import 'package:recase/recase.dart'; import '../core/core.dart'; /// Supports grouping routes with shared functionality. class Controller { - Angel _app; + Angel? _app; /// The [Angel] application powering this controller. - Angel get app => _app; + Angel? get app => _app; /// If `true` (default), this class will inject itself as a singleton into the [app]'s container when bootstrapped. final bool injectSingleton; @@ -23,10 +23,10 @@ class Controller { /// A mapping of route paths to routes, produced from the [Expose] annotations on this class. Map routeMappings = {}; - SymlinkRoute _mountPoint; + SymlinkRoute? _mountPoint; /// The route at which this controller is mounted on the server. - SymlinkRoute get mountPoint => _mountPoint; + SymlinkRoute? get mountPoint => _mountPoint; Controller({this.injectSingleton = true}); @@ -36,12 +36,12 @@ class Controller { _app = app; if (injectSingleton != false) { - if (!app.container.has(runtimeType)) { - _app.container.registerSingleton(this, as: runtimeType); + if (!app.container!.has(runtimeType)) { + _app!.container!.registerSingleton(this, as: runtimeType); } } - var name = await applyRoutes(app, app.container.reflector); + var name = await applyRoutes(app, app.container!.reflector); app.controllers[name] = this; return null; } @@ -50,21 +50,22 @@ class Controller { Future applyRoutes( Router router, Reflector reflector) async { // Load global expose decl - var classMirror = reflector.reflectClass(this.runtimeType); - Expose exposeDecl = findExpose(reflector); + var classMirror = reflector.reflectClass(runtimeType)!; + Expose? exposeDecl = findExpose(reflector); if (exposeDecl == null) { throw Exception("All controllers must carry an @Expose() declaration."); } var routable = Routable(); - _mountPoint = router.mount(exposeDecl.path, routable); - var typeMirror = reflector.reflectType(this.runtimeType); + var m = router.mount(exposeDecl.path!, routable); + _mountPoint = m; + var typeMirror = reflector.reflectType(runtimeType); // Pre-reflect methods var instanceMirror = reflector.reflectInstance(this); final handlers = [] - ..addAll(exposeDecl.middleware) + ..addAll(exposeDecl.middleware!) ..addAll(middleware); final routeBuilder = _routeBuilder(reflector, instanceMirror, routable, handlers); @@ -72,12 +73,15 @@ class Controller { classMirror.declarations.forEach(routeBuilder); // Return the name. - return exposeDecl.as?.isNotEmpty == true ? exposeDecl.as : typeMirror.name; + var result = + exposeDecl.as?.isNotEmpty == true ? exposeDecl.as : typeMirror!.name; + + return Future.value(result); } void Function(ReflectedDeclaration) _routeBuilder( Reflector reflector, - ReflectedInstance instanceMirror, + ReflectedInstance? instanceMirror, Routable routable, Iterable handlers) { return (ReflectedDeclaration decl) { @@ -89,13 +93,13 @@ class Controller { methodName != 'call' && methodName != 'equals' && methodName != '==') { - var exposeDecl = decl.function.annotations + var exposeDecl = decl.function!.annotations .map((m) => m.reflectee) - .firstWhere((r) => r is Expose, orElse: () => null) as Expose; + .firstWhere((r) => r is Expose, orElse: () => null) as Expose?; if (exposeDecl == null) { // If this has a @noExpose, return null. - if (decl.function.annotations.any((m) => m.reflectee is NoExpose)) { + if (decl.function!.annotations.any((m) => m.reflectee is NoExpose)) { return; } else { // Otherwise, create an @Expose. @@ -104,37 +108,37 @@ class Controller { } var reflectedMethod = - instanceMirror.getField(methodName).reflectee as Function; + instanceMirror!.getField(methodName).reflectee as Function?; var middleware = [] ..addAll(handlers) - ..addAll(exposeDecl.middleware); - String name = + ..addAll(exposeDecl.middleware!); + String? name = exposeDecl.as?.isNotEmpty == true ? exposeDecl.as : methodName; // Check if normal - var method = decl.function; + var method = decl.function!; if (method.parameters.length == 2 && method.parameters[0].type.reflectedType == RequestContext && method.parameters[1].type.reflectedType == ResponseContext) { // Create a regular route - routeMappings[name] = routable - .addRoute(exposeDecl.method, exposeDecl.path, + routeMappings[name ?? ''] = routable + .addRoute(exposeDecl.method, exposeDecl.path ?? '', (RequestContext req, ResponseContext res) { - var result = reflectedMethod(req, res); + var result = reflectedMethod!(req, res); return result is RequestHandler ? result(req, res) : result; }, middleware: middleware); return; } - var injection = preInject(reflectedMethod, reflector); + var injection = preInject(reflectedMethod!, reflector); - if (exposeDecl?.allowNull?.isNotEmpty == true) { - injection.optional?.addAll(exposeDecl.allowNull); + if (exposeDecl.allowNull.isNotEmpty == true) { + injection.optional.addAll(exposeDecl.allowNull); } // If there is no path, reverse-engineer one. var path = exposeDecl.path; - var httpMethod = exposeDecl.method ?? 'GET'; + var httpMethod = exposeDecl.method; if (path == null) { // Try to build a route path by finding all potential // path segments, and then joining them. @@ -148,7 +152,7 @@ class Controller { var restPath = ReCase(rest.isEmpty ? 'index' : rest) .snakeCase .replaceAll(_rgxMultipleUnderscores, '_'); - httpMethod = methodMatch[1].toUpperCase(); + httpMethod = methodMatch[1]!.toUpperCase(); if (['index', 'by_id'].contains(restPath)) { parts.add('/'); @@ -190,7 +194,7 @@ class Controller { if (!path.startsWith('/')) path = '/$path'; } - routeMappings[name] = routable.addRoute( + routeMappings[name ?? ''] = routable.addRoute( httpMethod, path, handleContained(reflectedMethod, injection), middleware: middleware); } @@ -215,12 +219,12 @@ class Controller { /// /// If [concreteOnly] is `false`, then if there is no actual /// [Expose], one will be automatically created. - Expose findExpose(Reflector reflector, {bool concreteOnly = false}) { + Expose? findExpose(Reflector reflector, {bool concreteOnly = false}) { var existing = reflector - .reflectClass(runtimeType) + .reflectClass(runtimeType)! .annotations .map((m) => m.reflectee) - .firstWhere((r) => r is Expose, orElse: () => null) as Expose; + .firstWhere((r) => r is Expose, orElse: () => null) as Expose?; return existing ?? (concreteOnly ? null diff --git a/packages/framework/lib/src/core/driver.dart b/packages/framework/lib/src/core/driver.dart index b15df8e5..2ebd6836 100644 --- a/packages/framework/lib/src/core/driver.dart +++ b/packages/framework/lib/src/core/driver.dart @@ -1,9 +1,10 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io' show stderr, Cookie; -import 'package:angel_http_exception/angel_http_exception.dart'; -import 'package:angel_route/angel_route.dart'; -import 'package:combinator/combinator.dart'; +import 'package:angel3_http_exception/angel3_http_exception.dart'; +import 'package:angel3_route/angel3_route.dart'; +import 'package:angel3_combinator/angel3_combinator.dart'; +import 'package:logging/logging.dart'; import 'package:stack_trace/stack_trace.dart'; import 'package:tuple/tuple.dart'; import 'core.dart'; @@ -20,8 +21,13 @@ abstract class Driver< final Angel app; final bool useZone; bool _closed = false; - Server _server; - StreamSubscription _sub; + late Server _server; + + // TODO: Ugly fix + bool isServerInitialised = false; + + StreamSubscription? _sub; + final log = Logger('Driver'); /// The function used to bind this instance to a server.. final Future Function(dynamic, int) serverGenerator; @@ -32,36 +38,62 @@ abstract class Driver< Uri get uri; /// The native server running this instance. - Server get server => _server; + Server? get server { + // TODO: Ugly fix + if (isServerInitialised) { + return _server; + } else { + return null; + } + } Future generateServer(address, int port) => serverGenerator(address, port); /// Starts, and returns the server. - Future startServer([address, int port]) { + Future startServer([address, int port = 0]) { var host = address ?? '127.0.0.1'; - return generateServer(host, port ?? 0).then((server) { + return generateServer(host, port).then((server) { _server = server; + + // TODO: Ugly fix + isServerInitialised = true; + return Future.wait(app.startupHooks.map(app.configure)).then((_) { app.optimizeForProduction(); _sub = server.listen((request) { var stream = createResponseStreamFromRawRequest(request); stream.listen((response) { - return handleRawRequest(request, response); + // TODO: To be revisited + handleRawRequest(request, response); }); }); - return _server; + return Future.value(_server); }); + }).catchError((error) { + log.severe("Failed to create server", error); + throw ArgumentError("[Driver]Failed to create server"); }); } /// Shuts down the underlying server. - Future close() { - if (_closed) return Future.value(_server); + Future close() { + if (_closed) { + //return Future.value(_server); + return Future.value(); + } _closed = true; + _sub?.cancel(); + return app.close().then((_) => - Future.wait(app.shutdownHooks.map(app.configure)).then((_) => _server)); + Future.wait(app.shutdownHooks.map(app.configure)) + .then((_) => Future.value())); + /* + return app.close().then((_) => + Future.wait(app.shutdownHooks.map(app.configure)) + .then((_) => Future.value(_server))); + */ } Future createRequestContext( @@ -69,7 +101,7 @@ abstract class Driver< Future createResponseContext( Request request, Response response, - [RequestContextType correspondingRequest]); + [RequestContextType? correspondingRequest]); void setHeader(Response response, String key, String value); @@ -107,7 +139,8 @@ abstract class Driver< pipeline.handlers, resolved.fold>( {}, (out, r) => out..addAll(r.allParams)), - resolved.isEmpty ? null : resolved.first.parseResult, + //(resolved.isEmpty ? null : resolved.first.parseResult), + resolved.first.parseResult, pipeline, ); } @@ -122,17 +155,17 @@ abstract class Driver< req.params.addAll(tuple.item2); req.container - ..registerSingleton(req) + ?..registerSingleton(req) ..registerSingleton(res) ..registerSingleton(tuple.item4) ..registerSingleton>(line) ..registerSingleton(it) ..registerSingleton>(it) - ..registerSingleton>(tuple.item3) - ..registerSingleton(tuple.item3); + ..registerSingleton?>(tuple.item3) + ..registerSingleton(tuple.item3); - if (!app.environment.isProduction && app.logger != null) { - req.container.registerSingleton(Stopwatch()..start()); + if (app.environment.isProduction && app.logger != null) { + req.container?.registerSingleton(Stopwatch()..start()); } return runPipeline(it, req, res, app) @@ -157,32 +190,43 @@ abstract class Driver< stackTrace: st, statusCode: 500, message: e?.toString() ?? '500 Internal Server Error'); - }, test: (e) => e is! AngelHttpException).catchError( + }, test: (e) => e is AngelHttpException).catchError( (ee, StackTrace st) { - var e = ee as AngelHttpException; + //print(">>>> Framework error: $ee"); + //var t = (st).runtimeType; + //print(">>>> StackTrace: $t"); + AngelHttpException e; + if (ee is AngelHttpException) { + e = ee; + } else { + e = AngelHttpException(ee, + stackTrace: st, + statusCode: 500, + message: ee?.toString() ?? '500 Internal Server Error'); + } if (app.logger != null) { var error = e.error ?? e; - var trace = Trace.from(e.stackTrace ?? StackTrace.current).terse; - app.logger.severe(e.message ?? e.toString(), error, trace); + var trace = Trace.from(StackTrace.current).terse; + app.logger?.severe(e.message, error, trace); } - return handleAngelHttpException( - e, e.stackTrace ?? st, req, res, request, response); + return handleAngelHttpException(e, st, req, res, request, response); }); } else { var zoneSpec = ZoneSpecification( print: (self, parent, zone, line) { if (app.logger != null) { - app.logger.info(line); + app.logger?.info(line); } else { parent.print(zone, line); } }, handleUncaughtError: (self, parent, zone, error, stackTrace) { - var trace = Trace.from(stackTrace ?? StackTrace.current).terse; + var trace = Trace.from(stackTrace).terse; - return Future(() { + // TODO: To be revisited + Future(() { AngelHttpException e; if (error is FormatException) { @@ -191,24 +235,22 @@ abstract class Driver< e = error; } else { e = AngelHttpException(error, - stackTrace: stackTrace, - message: - error?.toString() ?? '500 Internal Server Error'); + stackTrace: stackTrace, message: error.toString()); } if (app.logger != null) { - app.logger.severe(e.message ?? e.toString(), error, trace); + app.logger?.severe(e.message, error, trace); } return handleAngelHttpException( e, trace, req, res, request, response); }).catchError((e, StackTrace st) { - var trace = Trace.from(st ?? StackTrace.current).terse; + var trace = Trace.from(st).terse; closeResponse(response); // Ideally, we won't be in a position where an absolutely fatal error occurs, // but if so, we'll need to log it. if (app.logger != null) { - app.logger.severe( + app.logger?.severe( 'Fatal error occurred when processing $uri.', e, trace); } else { stderr @@ -222,8 +264,8 @@ abstract class Driver< ); var zone = Zone.current.fork(specification: zoneSpec); - req.container.registerSingleton(zone); - req.container.registerSingleton(zoneSpec); + req.container!.registerSingleton(zone); + req.container!.registerSingleton(zoneSpec); // If a synchronous error is thrown, it's not caught by `zone.run`, // so use a try/catch, and recover when need be. @@ -243,8 +285,8 @@ abstract class Driver< Future handleAngelHttpException( AngelHttpException e, StackTrace st, - RequestContext req, - ResponseContext res, + RequestContext? req, + ResponseContext? res, Request request, Response response, {bool ignoreFinalizers = false}) { @@ -255,7 +297,7 @@ abstract class Driver< writeStringToResponse(response, '500 Internal Server Error'); closeResponse(response); } finally { - return null; + return Future.value(); } } @@ -280,11 +322,11 @@ abstract class Driver< ResponseContext res, {bool ignoreFinalizers = false}) { Future _cleanup(_) { - if (!app.environment.isProduction && + if (app.environment.isProduction && app.logger != null && - req.container.has()) { - var sw = req.container.make(); - app.logger.info( + req.container!.has()) { + var sw = req.container!.make(); + app.logger?.info( "${res.statusCode} ${req.method} ${req.uri} (${sw?.elapsedMilliseconds ?? 'unknown'} ms)"); } return req.close(); @@ -294,27 +336,27 @@ abstract class Driver< Future finalizers = ignoreFinalizers == true ? Future.value() - : Future.forEach(app.responseFinalizers, (f) => f(req, res)); + : Future.forEach(app.responseFinalizers, (dynamic f) => f(req, res)); return finalizers.then((_) { //if (res.isOpen) res.close(); for (var key in res.headers.keys) { - setHeader(response, key, res.headers[key]); + setHeader(response, key, res.headers[key] ?? ''); } - setContentLength(response, res.buffer.length); + setContentLength(response, res.buffer!.length); setChunkedEncoding(response, res.chunked ?? true); - List outputBuffer = res.buffer.toBytes(); + List outputBuffer = res.buffer!.toBytes(); if (res.encoders.isNotEmpty) { var allowedEncodings = req.headers - .value('accept-encoding') + ?.value('accept-encoding') ?.split(',') - ?.map((s) => s.trim()) - ?.where((s) => s.isNotEmpty) - ?.map((str) { + .map((s) => s.trim()) + .where((s) => s.isNotEmpty) + .map((str) { // Ignore quality specifications in accept-encoding // ex. gzip;q=0.8 if (!str.contains(';')) return str; @@ -323,7 +365,7 @@ abstract class Driver< if (allowedEncodings != null) { for (var encodingName in allowedEncodings) { - Converter, List> encoder; + Converter, List>? encoder; String key = encodingName; if (res.encoders.containsKey(encodingName)) { @@ -334,7 +376,7 @@ abstract class Driver< if (encoder != null) { setHeader(response, 'content-encoding', key); - outputBuffer = res.encoders[key].convert(outputBuffer); + outputBuffer = res.encoders[key]!.convert(outputBuffer); setContentLength(response, outputBuffer.length); break; } diff --git a/packages/framework/lib/src/core/env.dart b/packages/framework/lib/src/core/env.dart index a513c93a..39307b34 100644 --- a/packages/framework/lib/src/core/env.dart +++ b/packages/framework/lib/src/core/env.dart @@ -5,7 +5,7 @@ const AngelEnvironment angelEnv = AngelEnvironment(); /// Queries the environment's `ANGEL_ENV` value. class AngelEnvironment { - final String _customValue; + final String? _customValue; /// You can optionally provide a custom value, in order to override the system's /// value. diff --git a/packages/framework/lib/src/core/hooked_service.dart b/packages/framework/lib/src/core/hooked_service.dart index 91e6b99f..6e676de0 100644 --- a/packages/framework/lib/src/core/hooked_service.dart +++ b/packages/framework/lib/src/core/hooked_service.dart @@ -45,24 +45,24 @@ class HookedService> HookedService(this.inner) { // Clone app instance - if (inner.app != null) this.app = inner.app; + if (inner.app != null) app = inner.app; } @override - FutureOr Function(RequestContext, ResponseContext) get readData => + FutureOr Function(RequestContext, ResponseContext)? get readData => inner.readData; - RequestContext _getRequest(Map params) { + RequestContext? _getRequest(Map? params) { if (params == null) return null; - return params['__requestctx'] as RequestContext; + return params['__requestctx'] as RequestContext?; } - ResponseContext _getResponse(Map params) { + ResponseContext? _getResponse(Map? params) { if (params == null) return null; - return params['__responsectx'] as ResponseContext; + return params['__responsectx'] as ResponseContext?; } - Map _stripReq(Map params) { + Map? _stripReq(Map? params) { if (params == null) { return params; } else { @@ -95,7 +95,7 @@ class HookedService> /// Adds hooks to this instance. void addHooks(Angel app) { - var hooks = getAnnotation(inner, app.container.reflector); + var hooks = getAnnotation(inner, app.container!.reflector); List> before = [], after = []; if (hooks != null) { @@ -105,8 +105,8 @@ class HookedService> void applyListeners( Function fn, HookedServiceEventDispatcher dispatcher, - [bool isAfter]) { - Hooks hooks = getAnnotation(fn, app.container.reflector); + [bool? isAfter]) { + Hooks? hooks = getAnnotation(fn, app.container!.reflector); final listeners = >[] ..addAll(isAfter == true ? after : before); @@ -131,6 +131,7 @@ class HookedService> applyListeners(inner.remove, afterRemoved, true); } + @override List get bootstrappers => List.from(super.bootstrappers) ..add((RequestContext req, ResponseContext res) { @@ -140,7 +141,8 @@ class HookedService> return true; }); - void addRoutes([Service s]) { + @override + void addRoutes([Service? s]) { super.addRoutes(s ?? inner); } @@ -270,7 +272,7 @@ class HookedService> } @override - Future> index([Map _params]) { + Future> index([Map? _params]) { var params = _stripReq(_params); return beforeIndexed ._emit(HookedServiceEvent(false, _getRequest(_params), @@ -296,7 +298,7 @@ class HookedService> } @override - Future read(Id id, [Map _params]) { + Future read(Id id, [Map? _params]) { var params = _stripReq(_params); return beforeRead ._emit(HookedServiceEvent(false, _getRequest(_params), @@ -322,7 +324,7 @@ class HookedService> } @override - Future create(Data data, [Map _params]) { + Future create(Data data, [Map? _params]) { var params = _stripReq(_params); return beforeCreated ._emit(HookedServiceEvent(false, _getRequest(_params), @@ -337,7 +339,7 @@ class HookedService> .then((after) => after.result as Data); } - return inner.create(before.data, params).then((result) { + return inner.create(before.data!, params).then((result) { return afterCreated ._emit(HookedServiceEvent(true, _getRequest(_params), _getResponse(_params), inner, HookedServiceEvent.created, @@ -348,7 +350,7 @@ class HookedService> } @override - Future modify(Id id, Data data, [Map _params]) { + Future modify(Id id, Data data, [Map? _params]) { var params = _stripReq(_params); return beforeModified ._emit(HookedServiceEvent(false, _getRequest(_params), @@ -366,7 +368,7 @@ class HookedService> .then((after) => after.result as Data); } - return inner.modify(id, before.data, params).then((result) { + return inner.modify(id, before.data!, params).then((result) { return afterModified ._emit(HookedServiceEvent(true, _getRequest(_params), _getResponse(_params), inner, HookedServiceEvent.created, @@ -377,7 +379,7 @@ class HookedService> } @override - Future update(Id id, Data data, [Map _params]) { + Future update(Id id, Data data, [Map? _params]) { var params = _stripReq(_params); return beforeUpdated ._emit(HookedServiceEvent(false, _getRequest(_params), @@ -395,7 +397,7 @@ class HookedService> .then((after) => after.result as Data); } - return inner.update(id, before.data, params).then((result) { + return inner.update(id, before.data!, params).then((result) { return afterUpdated ._emit(HookedServiceEvent(true, _getRequest(_params), _getResponse(_params), inner, HookedServiceEvent.updated, @@ -406,7 +408,7 @@ class HookedService> } @override - Future remove(Id id, [Map _params]) { + Future remove(Id id, [Map? _params]) { var params = _stripReq(_params); return beforeRemoved ._emit(HookedServiceEvent(false, _getRequest(_params), @@ -434,7 +436,7 @@ class HookedService> /// Fires an `after` event. This will not be propagated to clients, /// but will be broadcasted to WebSockets, etc. Future> fire(String eventName, result, - [HookedServiceEventListener callback]) { + [HookedServiceEventListener? callback]) { HookedServiceEventDispatcher dispatcher; switch (eventName) { @@ -469,9 +471,9 @@ class HookedService> Future> fireEvent( HookedServiceEventDispatcher dispatcher, HookedServiceEvent event, - [HookedServiceEventListener callback]) { - Future f; - if (callback != null && event?._canceled != true) { + [HookedServiceEventListener? callback]) { + Future? f; + if (callback != null && event._canceled != true) { f = Future.sync(() => callback(event)); } f ??= Future.value(); @@ -506,46 +508,46 @@ class HookedServiceEvent> { /// Resolves a service from the application. /// /// Shorthand for `e.service.app.service(...)`. - Service getService(Pattern path) => service.app.findService(path); + Service? getService(Pattern path) => service.app!.findService(path); bool _canceled = false; - String _eventName; - Id _id; - bool _isAfter; - Data data; - Map _params; - RequestContext _request; - ResponseContext _response; + final String _eventName; + Id? _id; + final bool _isAfter; + Data? data; + Map? _params; + final RequestContext? _request; + final ResponseContext? _response; var result; String get eventName => _eventName; - Id get id => _id; + Id? get id => _id; bool get isAfter => _isAfter == true; bool get isBefore => !isAfter; - Map get params => _params; + Map? get params => _params; - RequestContext get request => _request; + RequestContext? get request => _request; - ResponseContext get response => _response; + ResponseContext? get response => _response; /// The inner service whose method was hooked. T service; HookedServiceEvent(this._isAfter, this._request, this._response, this.service, this._eventName, - {Id id, this.data, Map params, this.result}) { + {Id? id, this.data, Map? params, this.result}) { _id = id; _params = params ?? {}; } } /// Triggered on a hooked service event. -typedef FutureOr HookedServiceEventListener>(HookedServiceEvent event); +typedef HookedServiceEventListener> + = FutureOr Function(HookedServiceEvent event); /// Can be listened to, but events may be canceled. class HookedServiceEventDispatcher> { @@ -560,7 +562,7 @@ class HookedServiceEventDispatcher> { /// Fires an event, and returns it once it is either canceled, or all listeners have run. Future> _emit( HookedServiceEvent event) { - if (event?._canceled == true || event == null || listeners.isEmpty) { + if (event._canceled == true || listeners.isEmpty) { return Future.value(event); } diff --git a/packages/framework/lib/src/core/hostname_parser.dart b/packages/framework/lib/src/core/hostname_parser.dart index 5b3540fb..8394b671 100644 --- a/packages/framework/lib/src/core/hostname_parser.dart +++ b/packages/framework/lib/src/core/hostname_parser.dart @@ -4,7 +4,7 @@ import 'package:string_scanner/string_scanner.dart'; /// Parses a string into a [RegExp] that is matched against hostnames. class HostnameSyntaxParser { final SpanScanner _scanner; - var _safe = RegExp(r"[0-9a-zA-Z-_:]+"); + final _safe = RegExp(r"[0-9a-zA-Z-_:]+"); HostnameSyntaxParser(String hostname) : _scanner = SpanScanner(hostname, sourceUrl: hostname); @@ -25,7 +25,7 @@ class HostnameSyntaxParser { throw _formatExc('No hostname parts found before "|".'); } else { var next = _parseHostnamePart(); - if (next == null) { + if (next.isEmpty) { throw _formatExc('No hostname parts found after "|".'); } else { var prev = parts.removeLast(); @@ -33,11 +33,11 @@ class HostnameSyntaxParser { } } } else { - var part = _parseHostnamePart(); - if (part != null) { + String part = _parseHostnamePart(); + if (part.isNotEmpty) { if (_scanner.scan('.')) { var subPart = _parseHostnamePart(shouldThrow: false); - while (subPart != null) { + while (subPart.isNotEmpty) { part += '\\.' + subPart; if (_scanner.scan('.')) { subPart = _parseHostnamePart(shouldThrow: false); @@ -46,7 +46,6 @@ class HostnameSyntaxParser { } } } - parts.add(part); } } @@ -71,12 +70,12 @@ class HostnameSyntaxParser { } else if (_scanner.scan('+')) { return r'[^$]+'; } else if (_scanner.scan(_safe)) { - return _scanner.lastMatch[0]; + return _scanner.lastMatch?[0] ?? ""; } else if (!_scanner.isDone && shouldThrow) { - var s = String.fromCharCode(_scanner.peekChar()); + var s = String.fromCharCode(_scanner.peekChar()!); throw _formatExc('Unexpected character "$s".'); } else { - return null; + return ""; } } } diff --git a/packages/framework/lib/src/core/hostname_router.dart b/packages/framework/lib/src/core/hostname_router.dart index b896a895..09c4615e 100644 --- a/packages/framework/lib/src/core/hostname_router.dart +++ b/packages/framework/lib/src/core/hostname_router.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:angel_container/angel_container.dart'; -import 'package:angel_route/angel_route.dart'; +import 'package:angel3_container/angel3_container.dart'; +import 'package:angel3_route/angel3_route.dart'; import 'package:logging/logging.dart'; import 'env.dart'; import 'hostname_parser.dart'; @@ -45,8 +45,6 @@ class HostnameRouter { }); } - apps ??= {}; - creators ??= {}; apps = _parseMap(apps); creators = _parseMap(creators); var patterns = apps.keys.followedBy(creators.keys).toSet().toList(); @@ -60,10 +58,10 @@ class HostnameRouter { Map Function(Angel)> configurers, {Reflector reflector = const EmptyReflector(), AngelEnvironment environment = angelEnv, - Logger logger, + Logger? logger, bool allowMethodOverrides = true, - FutureOr Function(dynamic) serializer, - ViewGenerator viewGenerator}) { + FutureOr Function(dynamic)? serializer, + ViewGenerator? viewGenerator}) { var creators = configurers.map((p, c) { return MapEntry(p, () async { var app = Angel( @@ -86,30 +84,28 @@ class HostnameRouter { /// Also returns `true` if all of the sub-app's handlers returned /// `true`. Future handleRequest(RequestContext req, ResponseContext res) async { - if (req.hostname != null) { - for (var pattern in _patterns) { - // print('${req.hostname} vs $_creators'); - if (pattern.allMatches(req.hostname).isNotEmpty) { - // Resolve the entire pipeline within the context of the selected app. - var app = _apps[pattern] ??= (await _creators[pattern]()); - // print('App for ${req.hostname} = $app from $pattern'); - // app.dumpTree(); + for (var pattern in _patterns) { + // print('${req.hostname} vs $_creators'); + if (pattern.allMatches(req.hostname).isNotEmpty) { + // Resolve the entire pipeline within the context of the selected app. + var app = _apps[pattern] ??= (await _creators[pattern]!()); + // print('App for ${req.hostname} = $app from $pattern'); + // app.dumpTree(); - var r = app.optimizedRouter; - var resolved = r.resolveAbsolute(req.path, method: req.method); - var pipeline = MiddlewarePipeline(resolved); - // print('Pipeline: $pipeline'); - for (var handler in pipeline.handlers) { - // print(handler); - // Avoid stack overflow. - if (handler == handleRequest) { - continue; - } else if (!await app.executeHandler(handler, req, res)) { - // print('$handler TERMINATED'); - return false; - } else { - // print('$handler CONTINUED'); - } + var r = app.optimizedRouter; + var resolved = r.resolveAbsolute(req.path, method: req.method); + var pipeline = MiddlewarePipeline(resolved); + // print('Pipeline: $pipeline'); + for (var handler in pipeline.handlers) { + // print(handler); + // Avoid stack overflow. + if (handler == handleRequest) { + continue; + } else if (!await app.executeHandler(handler, req, res)) { + // print('$handler TERMINATED'); + return false; + } else { + // print('$handler CONTINUED'); } } } diff --git a/packages/framework/lib/src/core/injection.dart b/packages/framework/lib/src/core/injection.dart index 1f08de21..d5268a01 100644 --- a/packages/framework/lib/src/core/injection.dart +++ b/packages/framework/lib/src/core/injection.dart @@ -8,25 +8,27 @@ const List _primitiveTypes = [String, int, num, double, Null]; /// /// Calling [ioc] also auto-serializes the result of a [handler]. RequestHandler ioc(Function handler, {Iterable optional = const []}) { - InjectionRequest injection; - RequestHandler contained; - return (req, res) { - if (injection == null) { - injection = preInject(handler, req.app.container.reflector); - injection.optional.addAll(optional ?? []); + RequestHandler? contained; + + if (req.app?.container != null) { + InjectionRequest injection = + preInject(handler, req.app!.container!.reflector); + //if (injection != null) { + injection.optional.addAll(optional); contained = handleContained(handler, injection); + //} } - return req.app.executeHandler(contained, req, res); + return req.app!.executeHandler(contained, req, res); }; } resolveInjection(requirement, InjectionRequest injection, RequestContext req, ResponseContext res, bool throwOnUnresolved, - [Container container]) async { + [Container? container]) async { var propFromApp; - container ??= req?.container ?? res?.app?.container; + container ??= req.container ?? res.app!.container; if (requirement == RequestContext) { return req; @@ -34,17 +36,17 @@ resolveInjection(requirement, InjectionRequest injection, RequestContext req, return res; } else if (requirement is String && injection.parameters.containsKey(requirement)) { - var param = injection.parameters[requirement]; + var param = injection.parameters[requirement]!; var value = param.getValue(req); - if (value == null && param.required != false) throw param.error; + if (value == null && param.required != false) throw param.error as Object; return value; } else if (requirement is String) { - if (req.container.hasNamed(requirement)) { - return req.container.findByName(requirement); + if (req.container!.hasNamed(requirement)) { + return req.container!.findByName(requirement); } if (req.params.containsKey(requirement)) { return req.params[requirement]; - } else if ((propFromApp = req.app.findProperty(requirement)) != null) { + } else if ((propFromApp = req.app!.findProperty(requirement)) != null) { return propFromApp; } else if (injection.optional.contains(requirement)) { return null; @@ -59,7 +61,7 @@ resolveInjection(requirement, InjectionRequest injection, RequestContext req, var key = requirement.first; var type = requirement.last; if (req.params.containsKey(key) || - req.app.configuration.containsKey(key) || + req.app!.configuration.containsKey(key) || _primitiveTypes.contains(type)) { return await resolveInjection( key, injection, req, res, throwOnUnresolved, container); @@ -69,7 +71,7 @@ resolveInjection(requirement, InjectionRequest injection, RequestContext req, } } else if (requirement is Type && requirement != dynamic) { try { - var futureType = container.reflector.reflectFutureOf(requirement); + var futureType = container!.reflector.reflectFutureOf(requirement); if (container.has(futureType.reflectedType)) { return await container.make(futureType.reflectedType); } @@ -77,7 +79,7 @@ resolveInjection(requirement, InjectionRequest injection, RequestContext req, // Ignore. } - return await container.make(requirement); + return await container!.make(requirement); } else if (throwOnUnresolved) { throw ArgumentError( '$requirement cannot be injected into a request handler.'); @@ -96,7 +98,7 @@ bool suitableForInjection( /// Handles a request with a DI-enabled handler. RequestHandler handleContained(Function handler, InjectionRequest injection, - [Container container]) { + [Container? container]) { return (RequestContext req, ResponseContext res) async { if (injection.parameters.isNotEmpty && injection.parameters.values.any((p) => p.match != null) && @@ -157,7 +159,7 @@ class InjectionRequest { InjectionRequest preInject(Function handler, Reflector reflector) { var injection = InjectionRequest(); - var closureMirror = reflector.reflectFunction(handler); + var closureMirror = reflector.reflectFunction(handler)!; if (closureMirror.parameters.isEmpty) return injection; @@ -169,9 +171,8 @@ InjectionRequest preInject(Function handler, Reflector reflector) { var _Parameter = reflector.reflectType(Parameter); var p = parameter.annotations - .firstWhere((m) => m.type.isAssignableTo(_Parameter), - orElse: () => null) - ?.reflectee as Parameter; + .firstWhereOrNull((m) => m.type.isAssignableTo(_Parameter)) + ?.reflectee as Parameter?; //print(p); if (p != null) { injection.parameters[name] = Parameter( diff --git a/packages/framework/lib/src/core/map_service.dart b/packages/framework/lib/src/core/map_service.dart index a7b50915..d2335bb0 100644 --- a/packages/framework/lib/src/core/map_service.dart +++ b/packages/framework/lib/src/core/map_service.dart @@ -1,11 +1,11 @@ import 'dart:async'; -import 'package:angel_http_exception/angel_http_exception.dart'; +import 'package:angel3_http_exception/angel3_http_exception.dart'; import 'service.dart'; /// A basic service that manages an in-memory list of maps. -class MapService extends Service> { +class MapService extends Service> { /// If set to `true`, clients can remove all items by passing a `null` `id` to `remove`. /// /// `false` by default. @@ -48,14 +48,14 @@ class MapService extends Service> { } @override - Future>> index([Map params]) { + Future>> index([Map? params]) { if (allowQuery == false || params == null || params['query'] is! Map) { return Future.value(items); } else { - var query = params['query'] as Map; + var query = params['query'] as Map?; return Future.value(items.where((item) { - for (var key in query.keys) { + for (var key in query!.keys) { if (!item.containsKey(key)) { return false; } else if (item[key] != query[key]) return false; @@ -67,15 +67,16 @@ class MapService extends Service> { } @override - Future> read(String id, [Map params]) { + Future> read(String? id, + [Map? params]) { return Future.value(items.firstWhere(_matchesId(id), - orElse: () => throw AngelHttpException.notFound( - message: 'No record found for ID $id'))); + orElse: (() => throw AngelHttpException.notFound( + message: 'No record found for ID $id')))); } @override Future> create(Map data, - [Map params]) { + [Map? params]) { if (data is! Map) { throw AngelHttpException.badRequest( message: @@ -95,8 +96,8 @@ class MapService extends Service> { } @override - Future> modify(String id, Map data, - [Map params]) { + Future> modify(String? id, Map data, + [Map? params]) { if (data is! Map) { throw AngelHttpException.badRequest( message: @@ -110,17 +111,16 @@ class MapService extends Service> { var result = Map.from(item)..addAll(data); if (autoIdAndDateFields == true) { - result - ..[autoSnakeCaseNames == false ? 'updatedAt' : 'updated_at'] = - DateTime.now().toIso8601String(); + result[autoSnakeCaseNames == false ? 'updatedAt' : 'updated_at'] = + DateTime.now().toIso8601String(); } return Future.value(items[idx] = result); }); } @override - Future> update(String id, Map data, - [Map params]) { + Future> update(String? id, Map data, + [Map? params]) { if (data is! Map) { throw AngelHttpException.badRequest( message: @@ -149,8 +149,8 @@ class MapService extends Service> { } @override - Future> remove(String id, - [Map params]) { + Future> remove(String? id, + [Map? params]) { if (id == null || id == 'null') { // Remove everything... if (!(allowRemoveAll == true || diff --git a/packages/framework/lib/src/core/metadata.dart b/packages/framework/lib/src/core/metadata.dart index 0670f414..f2ee7b04 100644 --- a/packages/framework/lib/src/core/metadata.dart +++ b/packages/framework/lib/src/core/metadata.dart @@ -1,6 +1,6 @@ library angel_framework.http.metadata; -import 'package:angel_http_exception/angel_http_exception.dart'; +import 'package:angel3_http_exception/angel3_http_exception.dart'; import 'hooked_service.dart' show HookedServiceEventListener; import 'request_context.dart'; @@ -45,9 +45,9 @@ const NoExpose noExpose = NoExpose(); /// ``` class Expose { final String method; - final String path; - final Iterable middleware; - final String as; + final String? path; + final Iterable? middleware; + final String? as; final List allowNull; static const Expose get = Expose(null, method: 'GET'), @@ -71,16 +71,16 @@ class Expose { /// Used to apply special dependency injections or functionality to a function parameter. class Parameter { /// Inject the value of a request cookie. - final String cookie; + final String? cookie; /// Inject the value of a request header. - final String header; + final String? header; /// Inject the value of a key from the session. - final String session; + final String? session; /// Inject the value of a key from the query. - final String query; + final String? query; /// Only execute the handler if the value of this parameter matches the given value. final match; @@ -89,7 +89,7 @@ class Parameter { final defaultValue; /// If `true` (default), then an error will be thrown if this parameter is not present. - final bool required; + final bool? required; const Parameter( {this.cookie, @@ -122,17 +122,16 @@ class Parameter { /// Obtains a value for this parameter from a [RequestContext]. getValue(RequestContext req) { if (cookie?.isNotEmpty == true) { - return req.cookies.firstWhere((c) => c.name == cookie)?.value ?? - defaultValue; + return req.cookies.firstWhere((c) => c.name == cookie).value; } if (header?.isNotEmpty == true) { - return req.headers.value(header) ?? defaultValue; + return req.headers?.value(header ?? '') ?? defaultValue; } if (session?.isNotEmpty == true) { - return req.session[session] ?? defaultValue; + return req.session?[session] ?? defaultValue; } if (query?.isNotEmpty == true) { - return req.uri.queryParameters[query] ?? defaultValue; + return req.uri?.queryParameters[query] ?? defaultValue; } return defaultValue; } diff --git a/packages/framework/lib/src/core/request_context.dart b/packages/framework/lib/src/core/request_context.dart index 0d9c072b..bd0b3700 100644 --- a/packages/framework/lib/src/core/request_context.dart +++ b/packages/framework/lib/src/core/request_context.dart @@ -11,12 +11,14 @@ import 'dart:io' HttpSession, InternetAddress; -import 'package:angel_container/angel_container.dart'; +import 'package:angel3_container/angel3_container.dart'; import 'package:http_parser/http_parser.dart'; import 'package:http_server/http_server.dart'; import 'package:meta/meta.dart'; import 'package:mime/mime.dart'; import 'package:path/path.dart' as p; +import 'package:collection/collection.dart'; +import 'package:logging/logging.dart'; import 'metadata.dart'; import 'response_context.dart'; @@ -29,15 +31,19 @@ part 'injection.dart'; abstract class RequestContext { /// Similar to [Angel.shutdownHooks], allows for logic to be executed /// when a [RequestContext] is done being processed. + final log = Logger('RequestContext'); + final List Function()> shutdownHooks = []; - String _acceptHeaderCache, _extensionCache; - bool _acceptsAllCache, _hasParsedBody = false, _closed = false; - Map _bodyFields, _queryParameters; - List _bodyList; - Object _bodyObject; - List _uploadedFiles; - MediaType _contentType; + String? _acceptHeaderCache, _extensionCache; + bool? _acceptsAllCache; + Map? _queryParameters; + Object? _bodyObject; + bool _hasParsedBody = false, _closed = false; + Map _bodyFields = {}; + List _bodyList = []; + List _uploadedFiles = []; + MediaType _contentType = MediaType("text", "plain"); /// The underlying [RawRequest] provided by the driver. RawRequest get rawRequest; @@ -46,22 +52,22 @@ abstract class RequestContext { final Map serviceParams = {}; /// The [Angel] instance that is responding to this request. - Angel app; + Angel? app; /// Any cookies sent with this request. - List get cookies; + List get cookies => []; /// All HTTP headers sent with this request. - HttpHeaders get headers; + HttpHeaders? get headers; /// The requested hostname. - String get hostname; + String get hostname => 'localhost'; /// The IoC container that can be used to provide functionality to produce /// objects of a given type. /// /// This is a *child* of the container found in `app`. - Container get container; + Container? get container; /// The user's IP. String get ip => remoteAddress.address; @@ -69,24 +75,33 @@ abstract class RequestContext { /// This request's HTTP method. /// /// This may have been processed by an override. See [originalMethod] to get the real method. - String get method; + String get method => 'GET'; /// The original HTTP verb sent to the server. - String get originalMethod; + String get originalMethod => 'GET'; /// The content type of an incoming request. - MediaType get contentType => - _contentType ??= MediaType.parse(headers.contentType.toString()); + MediaType get contentType { + if (headers?.contentType != null) { + try { + _contentType = MediaType.parse(headers!.contentType.toString()); + } catch (e) { + log.warning( + 'Invalid media type [${headers!.contentType.toString()}]', e); + } + } + return _contentType; + } /// The URL parameters extracted from the request URI. Map params = {}; /// The requested path. - String get path; + String get path => ''; /// Is this an **XMLHttpRequest**? bool get isXhr { - return headers.value("X-Requested-With")?.trim()?.toLowerCase() == + return headers?.value("X-Requested-With")?.trim().toLowerCase() == 'xmlhttprequest'; } @@ -94,13 +109,13 @@ abstract class RequestContext { InternetAddress get remoteAddress; /// The user's HTTP session. - HttpSession get session; + HttpSession? get session; /// The [Uri] instance representing the path this request is responding to. - Uri get uri; + Uri? get uri; /// The [Stream] of incoming binary data sent from the client. - Stream> get body; + Stream>? get body; /// Returns `true` if [parseBody] has been called so far. bool get hasParsedBody => _hasParsedBody; @@ -111,9 +126,10 @@ abstract class RequestContext { Map get bodyAsMap { if (!hasParsedBody) { throw StateError('The request body has not been parsed yet.'); - } else if (_bodyFields == null) { - throw StateError('The request body, $_bodyObject, is not a Map.'); } + // else if (_bodyFields == null) { + // throw StateError('The request body, $_bodyObject, is not a Map.'); + //} return _bodyFields; } @@ -121,15 +137,17 @@ abstract class RequestContext { /// This setter allows you to explicitly set the request body **exactly once**. /// /// Use this if the format of the body is not natively parsed by Angel. - set bodyAsMap(Map value) => bodyAsObject = value; + set bodyAsMap(Map? value) => bodyAsObject = value; /// Returns a *mutable* [List] parsed from the request [body]. /// /// Note that [parseBody] must be called first. - List get bodyAsList { + List? get bodyAsList { if (!hasParsedBody) { throw StateError('The request body has not been parsed yet.'); - } else if (_bodyList == null) { + // TODO: Relook at this + //} else if (_bodyList == null) { + } else if (_bodyList.isEmpty) { throw StateError('The request body, $_bodyObject, is not a List.'); } @@ -139,12 +157,12 @@ abstract class RequestContext { /// This setter allows you to explicitly set the request body **exactly once**. /// /// Use this if the format of the body is not natively parsed by Angel. - set bodyAsList(List value) => bodyAsObject = value; + set bodyAsList(List? value) => bodyAsObject = value; /// Returns the parsed request body, whatever it may be (typically a [Map] or [List]). /// /// Note that [parseBody] must be called first. - Object get bodyAsObject { + Object? get bodyAsObject { if (!hasParsedBody) { throw StateError('The request body has not been parsed yet.'); } @@ -170,7 +188,7 @@ abstract class RequestContext { /// Returns a *mutable* map of the files parsed from the request [body]. /// /// Note that [parseBody] must be called first. - List get uploadedFiles { + List? get uploadedFiles { if (!hasParsedBody) { throw StateError('The request body has not been parsed yet.'); } @@ -179,13 +197,13 @@ abstract class RequestContext { } /// Returns a *mutable* map of the fields contained in the query. - Map get queryParameters => - _queryParameters ??= Map.from(uri.queryParameters); + Map get queryParameters => _queryParameters ??= + Map.from(uri?.queryParameters ?? {}); /// Returns the file extension of the requested path, if any. /// /// Includes the leading `.`, if there is one. - String get extension => _extensionCache ??= p.extension(uri.path); + String get extension => _extensionCache ??= p.extension(uri?.path ?? ''); /// Returns `true` if the client's `Accept` header indicates that the given [contentType] is considered a valid response. /// @@ -207,14 +225,14 @@ abstract class RequestContext { 'RequestContext.accepts expects the `contentType` parameter to NOT be null.'); } - _acceptHeaderCache ??= headers.value('accept'); + _acceptHeaderCache ??= headers?.value('accept'); if (_acceptHeaderCache == null) { return true; - } else if (strict != true && _acceptHeaderCache.contains('*/*')) { + } else if (strict != true && _acceptHeaderCache!.contains('*/*')) { return true; } else { - return _acceptHeaderCache.contains(contentTypeString); + return _acceptHeaderCache!.contains(contentTypeString); } } @@ -222,30 +240,34 @@ abstract class RequestContext { bool get acceptsAll => _acceptsAllCache ??= accepts('*/*'); /// Shorthand for deserializing [bodyAsMap], using some transformer function [f]. - Future deserializeBody(FutureOr Function(Map) f, + Future deserializeBody(FutureOr Function(Map?) f, {Encoding encoding = utf8}) async { await parseBody(encoding: encoding); return await f(bodyAsMap); } /// Shorthand for decoding [bodyAsMap], using some [codec]. - Future decodeBody(Codec codec, {Encoding encoding = utf8}) => + Future decodeBody(Codec codec, {Encoding encoding = utf8}) => deserializeBody(codec.decode, encoding: encoding); /// Manually parses the request body, if it has not already been parsed. Future parseBody({Encoding encoding = utf8}) async { - if (contentType == null) { - throw FormatException('Missing "content-type" header.'); - } + //if (contentType == null) { + // throw FormatException('Missing "content-type" header.'); + //} if (!_hasParsedBody) { _hasParsedBody = true; + var contentBody = body; + //TODO: Relook at this + contentBody ??= Stream.empty(); + if (contentType.type == 'application' && contentType.subtype == 'json') { _uploadedFiles = []; - var parsed = _bodyObject = - await encoding.decoder.bind(body).join().then(json.decode); + var parsed = (_bodyObject = + await encoding.decoder.bind(contentBody).join().then(json.decode)); if (parsed is Map) { _bodyFields = Map.from(parsed); @@ -256,16 +278,16 @@ abstract class RequestContext { contentType.subtype == 'x-www-form-urlencoded') { _uploadedFiles = []; var parsed = await encoding.decoder - .bind(body) + .bind(contentBody) .join() .then((s) => Uri.splitQueryString(s, encoding: encoding)); _bodyFields = Map.from(parsed); } else if (contentType.type == 'multipart' && contentType.subtype == 'form-data' && contentType.parameters.containsKey('boundary')) { - var boundary = contentType.parameters['boundary']; + var boundary = contentType.parameters['boundary'] ?? ''; var transformer = MimeMultipartTransformer(boundary); - var parts = transformer.bind(body).map((part) => + var parts = transformer.bind(contentBody).map((part) => HttpMultipartFormData.parse(part, defaultEncoding: encoding)); _bodyFields = {}; _uploadedFiles = []; @@ -277,8 +299,10 @@ abstract class RequestContext { part.contentDisposition.parameters.containsKey('name')) { // If there is no name, then don't parse it. var key = part.contentDisposition.parameters['name']; - var value = await part.join(); - _bodyFields[key] = value; + if (key != null) { + var value = await part.join(); + _bodyFields[key] = value; + } } } } else { @@ -297,7 +321,7 @@ abstract class RequestContext { _acceptHeaderCache = null; serviceParams.clear(); params.clear(); - await Future.forEach(shutdownHooks, (hook) => hook()); + await Future.forEach(shutdownHooks, (dynamic hook) => hook()); } } } @@ -306,8 +330,9 @@ abstract class RequestContext { class UploadedFile { /// The underlying `form-data` item. final HttpMultipartFormData formData; + final log = Logger('UploadedFile'); - MediaType _contentType; + MediaType _contentType = MediaType("multipart", "form-data"); UploadedFile(this.formData); @@ -316,22 +341,35 @@ class UploadedFile { /// The filename associated with the data on the user's system. /// Returns [:null:] if not present. - String get filename => formData.contentDisposition.parameters['filename']; + String? get filename => formData.contentDisposition.parameters['filename']; /// The name of the field associated with this data. /// Returns [:null:] if not present. - String get name => formData.contentDisposition.parameters['name']; + String? get name => formData.contentDisposition.parameters['name']; /// The parsed [:Content-Type:] header of the [:HttpMultipartFormData:]. /// Returns [:null:] if not present. - MediaType get contentType => _contentType ??= (formData.contentType == null - ? null - : MediaType.parse(formData.contentType.toString())); + //MediaType get contentType => _contentType ??= (formData.contentType == null + // ? null + // : MediaType.parse(formData.contentType.toString())); + + MediaType get contentType { + if (formData.contentType != null) { + try { + _contentType = MediaType.parse(formData.contentType.toString()); + } catch (e) { + log.warning( + 'Invalue media type [${formData.contentType.toString()}]', e); + } + } + + return _contentType; + } /// The parsed [:Content-Transfer-Encoding:] header of the /// [:HttpMultipartFormData:]. This field is used to determine how to decode /// the data. Returns [:null:] if not present. - HeaderValue get contentTransferEncoding => formData.contentTransferEncoding; + HeaderValue? get contentTransferEncoding => formData.contentTransferEncoding; /// Reads the contents of the file into a single linear buffer. /// diff --git a/packages/framework/lib/src/core/response_context.dart b/packages/framework/lib/src/core/response_context.dart index 2110dc0e..d67e71e2 100644 --- a/packages/framework/lib/src/core/response_context.dart +++ b/packages/framework/lib/src/core/response_context.dart @@ -6,9 +6,10 @@ import 'dart:convert' as c show json; import 'dart:io' show BytesBuilder, Cookie; import 'dart:typed_data'; -import 'package:angel_route/angel_route.dart'; +import 'package:angel3_route/angel3_route.dart'; import 'package:file/file.dart'; import 'package:http_parser/http_parser.dart'; +import 'package:logging/logging.dart'; import 'package:mime/mime.dart'; import 'controller.dart'; @@ -26,14 +27,16 @@ abstract class ResponseContext 'server': 'angel', }); - Completer _done; + final log = Logger('ResponseContext'); + + Completer? _done; int _statusCode = 200; /// The [Angel] instance that is sending a response. - Angel app; + Angel? app; /// Is `Transfer-Encoding` chunked? - bool chunked; + bool? chunked; /// Any and all cookies to be sent to the user. final List cookies = []; @@ -49,7 +52,7 @@ abstract class ResponseContext final Map renderParams = {}; /// Points to the [RequestContext] corresponding to this response. - RequestContext get correspondingRequest; + RequestContext? get correspondingRequest; @override Future get done => (_done ?? Completer()).future; @@ -71,7 +74,7 @@ abstract class ResponseContext /// ```dart /// app.injectSerializer(JSON.encode); /// ``` - FutureOr Function(dynamic) serializer = c.json.encode; + FutureOr Function(dynamic)? serializer = c.json.encode; /// This response's status code. int get statusCode => _statusCode; @@ -80,7 +83,7 @@ abstract class ResponseContext if (!isOpen) { throw closed(); } else { - _statusCode = value ?? 200; + _statusCode = value; // ?? 200; } } @@ -94,7 +97,7 @@ abstract class ResponseContext bool get isBuffered; /// A set of UTF-8 encoded bytes that will be written to the response. - BytesBuilder get buffer; + BytesBuilder? get buffer; /// The underlying [RawResponse] under this instance. RawResponse get rawResponse; @@ -108,14 +111,14 @@ abstract class ResponseContext /// Gets or sets the content length to send back to a client. /// /// Returns `null` if the header is invalidly formatted. - int get contentLength { - return int.tryParse(headers['content-length']); + int? get contentLength { + return int.tryParse(headers['content-length']!); } /// Gets or sets the content length to send back to a client. /// /// If [value] is `null`, then the header will be removed. - set contentLength(int value) { + set contentLength(int? value) { if (value == null) { headers.remove('content-length'); } else { @@ -126,7 +129,7 @@ abstract class ResponseContext /// Gets or sets the content type to send back to a client. MediaType get contentType { try { - return MediaType.parse(headers['content-type']); + return MediaType.parse(headers['content-type']!); } catch (_) { return MediaType('text', 'plain'); } @@ -140,29 +143,30 @@ abstract class ResponseContext static StateError closed() => StateError('Cannot modify a closed response.'); /// Sends a download as a response. - Future download(File file, {String filename}) async { + Future download(File file, {String? filename}) async { if (!isOpen) throw closed(); headers["Content-Disposition"] = 'attachment; filename="${filename ?? file.path}"'; - contentType = MediaType.parse(lookupMimeType(file.path)); + contentType = MediaType.parse(lookupMimeType(file.path)!); headers['content-length'] = file.lengthSync().toString(); if (!isBuffered) { await file.openRead().cast>().pipe(this); } else { - buffer.add(file.readAsBytesSync()); + buffer!.add(file.readAsBytesSync()); await close(); } } /// Prevents more data from being written to the response, and locks it entire from further editing. + @override Future close() { if (buffer is LockableBytesBuilder) { (buffer as LockableBytesBuilder).lock(); } - if (_done?.isCompleted == false) _done.complete(); + if (_done?.isCompleted == false) _done!.complete(); return Future.value(); } @@ -175,18 +179,18 @@ abstract class ResponseContext /// /// You can override the [contentType] sent; by default it is `application/javascript`. Future jsonp(value, - {String callbackName = "callback", MediaType contentType}) { + {String callbackName = "callback", MediaType? contentType}) { if (!isOpen) throw closed(); this.contentType = contentType ?? MediaType('application', 'javascript'); - write("$callbackName(${serializer(value)})"); + write("$callbackName(${serializer!(value)})"); return close(); } /// Renders a view to the response stream, and closes the response. - Future render(String view, [Map data]) { + Future render(String view, [Map? data]) { if (!isOpen) throw closed(); contentType = MediaType('text', 'html', {'charset': 'utf-8'}); - return Future.sync(() => app.viewGenerator( + return Future.sync(() => app!.viewGenerator!( view, Map.from(renderParams) ..addAll(data ?? {}))).then((content) { @@ -202,13 +206,13 @@ abstract class ResponseContext /// based on the provided params. /// /// See [Router]#navigate for more. :) - Future redirect(url, {bool absolute = true, int code = 302}) { + Future redirect(url, {bool absolute = true, int? code}) { if (!isOpen) throw closed(); headers ..['content-type'] = 'text/html' ..['location'] = (url is String || url is Uri) ? url.toString() - : app.navigate(url as Iterable, absolute: absolute); + : app!.navigate(url as Iterable, absolute: absolute); statusCode = code ?? 302; write(''' @@ -231,9 +235,9 @@ abstract class ResponseContext } /// Redirects to the given named [Route]. - Future redirectTo(String name, [Map params, int code]) async { + Future redirectTo(String name, [Map? params, int? code]) async { if (!isOpen) throw closed(); - Route _findRoute(Router r) { + Route? _findRoute(Router r) { for (Route route in r.routes) { if (route is SymlinkRoute) { final m = _findRoute(route.router); @@ -245,11 +249,11 @@ abstract class ResponseContext return null; } - Route matched = _findRoute(app); + Route? matched = _findRoute(app!); if (matched != null) { await redirect( - matched.makeUri(params.keys.fold>({}, (out, k) { + matched.makeUri(params!.keys.fold>({}, (out, k) { return out..[k.toString()] = params[k]; })), code: code); @@ -260,7 +264,7 @@ abstract class ResponseContext } /// Redirects to the given [Controller] action. - Future redirectToAction(String action, [Map params, int code]) { + Future redirectToAction(String action, [Map? params, int? code]) { if (!isOpen) throw closed(); // UserController@show List split = action.split("@"); @@ -270,14 +274,14 @@ abstract class ResponseContext "Controller redirects must take the form of 'Controller@action'. You gave: $action"); } - Controller controller = - app.controllers[split[0].replaceAll(_straySlashes, '')]; + Controller? controller = + app!.controllers[split[0].replaceAll(_straySlashes, '')]; if (controller == null) { throw Exception("Could not find a controller named '${split[0]}'"); } - Route matched = controller.routeMappings[split[1]]; + Route? matched = controller.routeMappings[split[1]]; if (matched == null) { throw Exception( @@ -285,24 +289,26 @@ abstract class ResponseContext } final head = controller - .findExpose(app.container.reflector) + .findExpose(app!.container!.reflector)! .path .toString() .replaceAll(_straySlashes, ''); - final tail = matched - .makeUri(params.keys.fold>({}, (out, k) { - return out..[k.toString()] = params[k]; - })) - .replaceAll(_straySlashes, ''); - + String tail = ""; + if (params != null) { + tail = matched + .makeUri(params.keys.fold>({}, (out, k) { + return out..[k.toString()] = params[k]; + })) + .replaceAll(_straySlashes, ''); + } return redirect('$head/$tail'.replaceAll(_straySlashes, ''), code: code); } /// Serializes data to the response. - Future serialize(value, {MediaType contentType}) async { + Future serialize(value, {MediaType? contentType}) async { if (!isOpen) throw closed(); this.contentType = contentType ?? MediaType('application', 'json'); - var text = await serializer(value); + var text = await serializer!(value); if (text.isEmpty) return true; write(text); await close(); @@ -314,16 +320,14 @@ abstract class ResponseContext /// `HEAD` responses will not actually write data. Future streamFile(File file) async { if (!isOpen) throw closed(); - var mimeType = app.mimeTypeResolver.lookup(file.path); + var mimeType = app!.mimeTypeResolver.lookup(file.path); contentLength = await file.length(); contentType = mimeType == null ? MediaType('application', 'octet-stream') : MediaType.parse(mimeType); - if (correspondingRequest.method != 'HEAD') { - return this - .addStream(file.openRead().cast>()) - .then((_) => this.close()); + if (correspondingRequest!.method != 'HEAD') { + return addStream(file.openRead().cast>()).then((_) => close()); } } @@ -338,16 +342,21 @@ abstract class ResponseContext Future addStream(Stream> stream); @override - void addError(Object error, [StackTrace stackTrace]) { + void addError(Object error, [StackTrace? stackTrace]) { if (_done?.isCompleted == false) { - _done.completeError(error, stackTrace); + _done!.completeError(error, stackTrace); } else if (_done == null) { - Zone.current.handleUncaughtError(error, stackTrace); + if (stackTrace != null) { + Zone.current.handleUncaughtError(error, stackTrace); + } else { + log.warning("[ResponseContext] stackTrace is null"); + } } } /// Writes data to the response. - void write(value, {Encoding encoding}) { + @override + void write(value, {Encoding? encoding}) { encoding ??= utf8; if (!isOpen && isBuffered) { @@ -355,7 +364,7 @@ abstract class ResponseContext } else if (!isBuffered) { add(encoding.encode(value.toString())); } else { - buffer.add(encoding.encode(value.toString())); + buffer!.add(encoding.encode(value.toString())); } } @@ -366,12 +375,12 @@ abstract class ResponseContext } else if (!isBuffered) { add([charCode]); } else { - buffer.addByte(charCode); + buffer!.addByte(charCode); } } @override - void writeln([Object obj = ""]) { + void writeln([Object? obj = ""]) { write(obj.toString()); write('\r\n'); } diff --git a/packages/framework/lib/src/core/routable.dart b/packages/framework/lib/src/core/routable.dart index a497c4d0..04a4591e 100644 --- a/packages/framework/lib/src/core/routable.dart +++ b/packages/framework/lib/src/core/routable.dart @@ -2,8 +2,8 @@ library angel_framework.http.routable; import 'dart:async'; -import 'package:angel_container/angel_container.dart'; -import 'package:angel_route/angel_route.dart'; +import 'package:angel3_container/angel3_container.dart'; +import 'package:angel3_route/angel3_route.dart'; import '../util.dart'; import 'hooked_service.dart'; @@ -15,24 +15,25 @@ import 'service.dart'; final RegExp _straySlashes = RegExp(r'(^/+)|(/+$)'); /// A function that receives an incoming [RequestContext] and responds to it. -typedef FutureOr RequestHandler(RequestContext req, ResponseContext res); +typedef RequestHandler = FutureOr Function( + RequestContext req, ResponseContext res); /// Sequentially runs a list of [handlers] of middleware, and returns early if any does not /// return `true`. Works well with [Router].chain. RequestHandler chain(Iterable handlers) { return (req, res) { - Future Function() runPipeline; + Future Function()? runPipeline; for (var handler in handlers) { - if (handler == null) break; + //if (handler == null) break; if (runPipeline == null) { runPipeline = () => Future.sync(() => handler(req, res)); } else { var current = runPipeline; - runPipeline = () => current().then((result) => !res.isOpen + runPipeline = () => current().then((result) => res.isOpen ? Future.value(result) - : req.app.executeHandler(handler, req, res)); + : req.app!.executeHandler(handler, req, res)); } } @@ -44,17 +45,17 @@ RequestHandler chain(Iterable handlers) { /// A routable server that can handle dynamic requests. class Routable extends Router { final Map _services = {}; - final Map _serviceLookups = {}; + final Map _serviceLookups = {}; final Map configuration = {}; - final Container _container; + final Container? _container; - Routable([Reflector reflector]) + Routable([Reflector? reflector]) : _container = reflector == null ? null : Container(reflector), super(); /// A [Container] used to inject dependencies. - Container get container => _container; + Container? get container => _container; void close() { _services.clear(); @@ -65,7 +66,8 @@ class Routable extends Router { /// A set of [Service] objects that have been mapped into routes. Map get services => _services; - StreamController _onService = StreamController.broadcast(); + final StreamController _onService = + StreamController.broadcast(); /// Fired whenever a service is added to this instance. /// @@ -73,34 +75,33 @@ class Routable extends Router { Stream get onService => _onService.stream; /// Retrieves the service assigned to the given path. - T findService(Pattern path) { + T? findService(Pattern path) { return _serviceLookups.putIfAbsent(path, () { return _services[path] ?? _services[path.toString().replaceAll(_straySlashes, '')]; - }) as T; + }) as T?; } /// Shorthand for finding a [Service] in a statically-typed manner. - Service findServiceOf(Pattern path) { + Service? findServiceOf(Pattern path) { return findService>(path); } /// Shorthand for finding a [HookedService] in a statically-typed manner. - HookedService findHookedService( + HookedService? findHookedService( Pattern path) { - return findService(path) as HookedService; + return findService(path) as HookedService?; } @override Route addRoute( String method, String path, RequestHandler handler, - {Iterable middleware}) { - middleware ??= []; + {Iterable middleware = const {}}) { final handlers = []; // Merge @Middleware declaration, if any var reflector = _container?.reflector; if (reflector != null && reflector is! ThrowingReflector) { - Middleware middlewareDeclaration = + Middleware? middlewareDeclaration = getAnnotation(handler, _container?.reflector); if (middlewareDeclaration != null) { handlers.addAll(middlewareDeclaration.handlers); @@ -108,7 +109,7 @@ class Routable extends Router { } final handlerSequence = []; - handlerSequence.addAll(middleware ?? []); + handlerSequence.addAll(middleware); handlerSequence.addAll(handlers); return super.addRoute(method, path.toString(), handler, diff --git a/packages/framework/lib/src/core/server.dart b/packages/framework/lib/src/core/server.dart index 6ddd2ed5..e8ce4bbc 100644 --- a/packages/framework/lib/src/core/server.dart +++ b/packages/framework/lib/src/core/server.dart @@ -3,10 +3,10 @@ library angel_framework.http.server; import 'dart:async'; import 'dart:collection' show HashMap; import 'dart:convert'; -import 'package:angel_container/angel_container.dart'; -import 'package:angel_http_exception/angel_http_exception.dart'; -import 'package:angel_route/angel_route.dart'; -import 'package:combinator/combinator.dart'; +import 'package:angel3_container/angel3_container.dart'; +import 'package:angel3_http_exception/angel3_http_exception.dart'; +import 'package:angel3_route/angel3_route.dart'; +import 'package:angel3_combinator/angel3_combinator.dart'; import 'package:http_parser/http_parser.dart'; import 'package:logging/logging.dart'; import 'package:mime/mime.dart'; @@ -22,16 +22,16 @@ import 'service.dart'; //final RegExp _straySlashes = RegExp(r'(^/+)|(/+$)'); /// A function that configures an [Angel] server in some way. -typedef FutureOr AngelConfigurer(Angel app); +typedef AngelConfigurer = FutureOr Function(Angel app); /// A function that asynchronously generates a view from the given path and data. -typedef FutureOr ViewGenerator(String path, - [Map data]); +typedef ViewGenerator = FutureOr Function(String path, + [Map? data]); /// A powerful real-time/REST/MVC server class. class Angel extends Routable { static ViewGenerator noViewEngineConfigured = - (String view, [Map data]) => 'No view engine has been configured yet.'; + (String view, [Map? data]) => 'No view engine has been configured yet.'; final List _children = []; final Map< @@ -39,8 +39,8 @@ class Angel extends Routable { Tuple4, ParseResult, MiddlewarePipeline>> handlerCache = HashMap(); - Router _flattened; - Angel _parent; + Router? _flattened; + Angel? _parent; /// A global Map of converters that can transform responses bodies. final Map, List>> encoders = {}; @@ -51,7 +51,7 @@ class Angel extends Routable { final MimeTypeResolver mimeTypeResolver = MimeTypeResolver(); /// A middleware to inject a serialize on every request. - FutureOr Function(dynamic) serializer; + FutureOr Function(dynamic)? serializer; /// A [Map] of dependency data obtained via reflection. /// @@ -91,10 +91,10 @@ class Angel extends Routable { final AngelEnvironment environment; /// Returns the parent instance of this application, if any. - Angel get parent => _parent; + Angel? get parent => _parent; /// Outputs diagnostics and debug messages. - Logger logger; + Logger? logger; /// Plug-ins to be called right before server startup. /// @@ -121,7 +121,7 @@ class Angel extends Routable { /// A function that renders views. /// /// Called by [ResponseContext]@`render`. - ViewGenerator viewGenerator = noViewEngineConfigured; + ViewGenerator? viewGenerator = noViewEngineConfigured; /// The handler currently configured to run on [AngelHttpException]s. Function(AngelHttpException e, RequestContext req, ResponseContext res) @@ -135,7 +135,7 @@ class Angel extends Routable { } res.contentType = MediaType('text', 'html', {'charset': 'utf8'}); - res.statusCode = e.statusCode; + res.statusCode = e.statusCode; // ?? 200; res.write("${e.message}"); res.write("

${e.message}