├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yaml │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── test.yaml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── all_lint_rules.yaml ├── melos.yaml ├── packages ├── pharaoh │ ├── .gitignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── SHELF_INTEROP.md │ ├── analysis_options.yaml │ ├── example │ │ └── README.md │ ├── lib │ │ ├── pharaoh.dart │ │ ├── pharaoh_next.dart │ │ └── src │ │ │ ├── _next │ │ │ ├── _core │ │ │ │ ├── config.dart │ │ │ │ ├── container.dart │ │ │ │ ├── core_impl.dart │ │ │ │ └── reflector.dart │ │ │ ├── _router │ │ │ │ ├── definition.dart │ │ │ │ ├── meta.dart │ │ │ │ └── utils.dart │ │ │ ├── _validation │ │ │ │ ├── dto.dart │ │ │ │ └── meta.dart │ │ │ ├── core.dart │ │ │ ├── http.dart │ │ │ ├── router.dart │ │ │ └── validation.dart │ │ │ ├── http │ │ │ ├── cookie.dart │ │ │ ├── message.dart │ │ │ ├── request.dart │ │ │ ├── request_impl.dart │ │ │ ├── response.dart │ │ │ ├── response_impl.dart │ │ │ ├── router.dart │ │ │ ├── router │ │ │ │ ├── router_contract.dart │ │ │ │ └── router_handler.dart │ │ │ └── session.dart │ │ │ ├── middleware │ │ │ ├── body_parser.dart │ │ │ ├── cookie_parser.dart │ │ │ ├── request_logger.dart │ │ │ └── session_mw.dart │ │ │ ├── shelf_interop │ │ │ ├── adapter.dart │ │ │ └── shelf.dart │ │ │ ├── utils │ │ │ ├── exceptions.dart │ │ │ └── utils.dart │ │ │ └── view │ │ │ └── view.dart │ ├── pubspec.yaml │ └── test │ │ ├── acceptance │ │ └── request_handling_test.dart │ │ ├── core_test.dart │ │ ├── http │ │ ├── req.query_test.dart │ │ ├── res.cookie_test.dart │ │ ├── res.format_test.dart │ │ ├── res.json_test.dart │ │ ├── res.redirect_test.dart │ │ ├── res.render_test.dart │ │ ├── res.send_test.dart │ │ ├── res.set_text.dart │ │ ├── res.status_test.dart │ │ ├── res.type_test.dart │ │ └── session_test.dart │ │ ├── issue_route_not_found_test.dart │ │ ├── middleware │ │ ├── body_parser_test.dart │ │ ├── cookie_parser_test.dart │ │ └── session_mw_test.dart │ │ ├── pharaoh_next │ │ ├── config │ │ │ └── config_test.dart │ │ ├── core │ │ │ ├── application_factory_test.dart │ │ │ └── core_test.dart │ │ ├── http │ │ │ └── meta_test.dart │ │ ├── router_test.dart │ │ └── validation │ │ │ └── validation_test.dart │ │ └── router │ │ ├── handler_test.dart │ │ └── router_group_test.dart ├── pharaoh_basic_auth │ ├── .gitignore │ ├── .pubignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── example │ │ └── pharaoh_basic_auth_example.dart │ ├── lib │ │ ├── pharaoh_basic_auth.dart │ │ └── src │ │ │ └── basic_auth.dart │ ├── pubspec.yaml │ └── test │ │ └── basic_auth_test.dart ├── pharaoh_jwt_auth │ ├── .gitignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── example │ │ └── pharaoh_jwt_auth_example.dart │ ├── lib │ │ ├── pharaoh_jwt_auth.dart │ │ └── src │ │ │ └── pharaoh_jwt_auth_base.dart │ ├── pubspec.yaml │ └── test │ │ └── pharaoh_jwt_auth_test.dart ├── spanner │ ├── .gitignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── analysis_options.yaml │ ├── example │ │ └── spanner_example.dart │ ├── lib │ │ ├── spanner.dart │ │ └── src │ │ │ ├── parametric │ │ │ ├── definition.dart │ │ │ └── utils.dart │ │ │ ├── route │ │ │ └── action.dart │ │ │ └── tree │ │ │ ├── node.dart │ │ │ ├── tree.dart │ │ │ └── utils.dart │ ├── pubspec.yaml │ └── test │ │ ├── case_insensitive_test.dart │ │ ├── helpers │ │ └── test_utils.dart │ │ ├── issue_127_test.dart │ │ ├── middleware_test.dart │ │ ├── parametric_test.dart │ │ └── wildcard_test.dart └── spookie │ ├── .gitignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── example │ └── spookie_example.dart │ ├── lib │ ├── spookie.dart │ └── src │ │ ├── expectation.dart │ │ └── http_expectation.dart │ ├── pubspec.yaml │ └── test │ └── spookie_test.dart ├── pharaoh_examples ├── .gitignore ├── README.md ├── analysis_options.yaml ├── lib │ ├── api_service │ │ └── index.dart │ ├── middleware │ │ └── index.dart │ ├── route_groups │ │ └── index.dart │ ├── serve_files_1 │ │ └── index.dart │ ├── serve_files_2 │ │ └── index.dart │ └── shelf_middleware │ │ ├── cors.dart │ │ └── helmet.dart ├── public │ ├── web_demo_1 │ │ ├── .pubignore │ │ ├── files │ │ │ ├── CCTV大赛上海分赛区.txt │ │ │ ├── amazing.txt │ │ │ └── notes │ │ │ │ └── groceries.txt │ │ └── index.html │ └── web_demo_2 │ │ ├── dart.png │ │ ├── favicon.ico │ │ └── index.html ├── pubspec.yaml └── test │ └── api_service_test.dart └── pubspec.yaml /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | title: "fix: " 5 | labels: bug 6 | --- 7 | 8 | **Description** 9 | 10 | A clear and concise description of what the bug is. 11 | 12 | **Code Snippet** 13 | 14 | ```dart 15 | import 'package:pharaoh/pharaoh.dart'; 16 | 17 | void main() { 18 | 19 | /// your code goes here 20 | 21 | } 22 | ``` 23 | 24 | **Expected Behavior** 25 | 26 | A clear and concise description of what you expected to happen. 27 | 28 | **Additional Context** 29 | 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yaml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: A new feature to be added to the project 4 | title: "feat: " 5 | labels: feature 6 | --- 7 | 8 | **Description** 9 | 10 | Clearly describe what you are looking to add. The more context the better. 11 | 12 | **Requirements** 13 | 14 | - [ ] Checklist of requirements to be fulfilled 15 | 16 | **Additional Context** 17 | 18 | Add any other context or screenshots about the feature request go here. 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ## Description 10 | 11 | 12 | 13 | ## Type of Change 14 | 15 | 16 | 17 | - [ ] ✨ New feature (non-breaking change which adds functionality) 18 | - [ ] 🛠️ Bug fix (non-breaking change which fixes an issue) 19 | - [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change) 20 | - [ ] 🧹 Code refactor 21 | - [ ] ✅ Build configuration change 22 | - [ ] 📝 Documentation 23 | - [ ] 🗑️ Chore 24 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: Test Pipeline 7 | 8 | on: 9 | push: 10 | branches: ["main"] 11 | pull_request: 12 | branches: ["main"] 13 | 14 | jobs: 15 | analyze: 16 | name: Analyze Code 17 | runs-on: macos-14 18 | steps: 19 | - name: Checkout Repository 20 | uses: actions/checkout@v3 21 | 22 | - uses: dart-lang/setup-dart@v1.3 23 | - uses: bluefireteam/melos-action@v3 24 | 25 | - name: Bootstrap 26 | run: | 27 | dart pub global activate melos 28 | melos bootstrap 29 | 30 | - name: Check formatting 31 | run: melos format -- --set-exit-if-changed 32 | 33 | - name: Check linting 34 | run: | 35 | cd packages/pharaoh && dart run build_runner build --delete-conflicting-outputs 36 | melos analyze 37 | 38 | test: 39 | name: Test Packages 40 | runs-on: macos-14 41 | steps: 42 | - name: Checkout Repository 43 | uses: actions/checkout@v3 44 | 45 | - uses: dart-lang/setup-dart@v1.3 46 | - uses: bluefireteam/melos-action@v3 47 | 48 | - name: Bootstrap 49 | run: | 50 | dart pub global activate melos 51 | melos bootstrap 52 | cd packages/pharaoh && dart run build_runner build --delete-conflicting-outputs 53 | 54 | - name: Run Unit tests 55 | run: melos tests:ci 56 | 57 | - name: Upload Coverage 58 | uses: codecov/codecov-action@v3 59 | env: 60 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 61 | with: 62 | files: coverage/*_lcov.info 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/pubspec_overrides.yaml 2 | .idea 3 | .dart_tool 4 | .vscode 5 | melos_pharaoh.iml 6 | **/pubspec.lock 7 | 8 | **/melos_** 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Pharaoh 2 | 3 | Pharaoh is a backend framework, inspired by the likes of ExpressJS, to empower developers in building comprehensive server-side applications using Dart. The driving force behind Pharaoh's creation is a strong belief in the potential of Dart to serve as the primary language for developing the entire architecture of a company's product. Just as the JavaScript ecosystem has evolved, Pharaoh aims to contribute to the Dart ecosystem, providing a foundation for building scalable and feature-rich server-side applications. 4 | 5 | ## Table of contents 6 | 7 | - [Get Started!](#get-started) 8 | - [Coding Guidelines](#coding-guidelines) 9 | - [Reporting an Issue](#reporting-an-issue) 10 | - [PR and Code Contributions](#PRs-and-Code-contributions) 11 | 12 | ## Get Started! 13 | 14 | ready to contribute ... 👋🏽 Let's go 🚀 15 | 16 | ### Steps for contributing 17 | 1. [Open an issue](https://github.com/Pharaoh-Framework/pharaoh/issues/new/choose) for the bug you want to fix or the feature that you want to add. 18 | 19 | 2. Fork the repo to your GitHub Account, then clone the code to your local machine. If you are not sure how to do this, GitHub's [Fork a repo](https://docs.github.com/en/get-started/quickstart/fork-a-repo) documentation has a great step by step guide for that. 20 | 21 | 3. Set up the workspace by running the following commands 22 | 23 | ```shell 24 | dart pub global activate melos 25 | ``` 26 | 27 | and this 28 | 29 | ``` 30 | melos bootstrap 31 | ``` 32 | 33 | ## Coding Guidelines 34 | 35 | It's good practice to create a branch for each new issue you work on, although not compulsory. 36 | - All dart analysis checks must pass before you submit your code. Ensure your code is linted by running 37 | ``` 38 | melos run analyze 39 | ``` 40 | 41 | - You must write tests for your bug-fix/feature and all tests must pass. You can verify by running 42 | ``` 43 | run melos run all tests. 44 | ``` 45 | 46 | 47 | 48 | If the tests pass, you can commit your changes to your fork and then create 49 | a pull request from there. Make sure to reference your issue from the pull request comments by including the issue number e.g. Resolves: #123. 50 | 51 | ### Branches 52 | Use the main branch for bug fixes or minor work that is intended for the 53 | current release stream. 54 | 55 | Use the correspondingly named branch, e.g. 2.0, for anything intended for 56 | a future release of Pharaoh. 57 | 58 | ## Reporting an Issue 59 | 60 | We will typically close any vague issues or questions that are specific to some 61 | app you are writing. Please double check the docs and other references before reporting an issue or posting a question. 62 | 63 | Things that will help get your issue looked at: 64 | 65 | - Full and runnable Dart code. 66 | 67 | - Clear description of the problem or unexpected behavior. 68 | 69 | - Clear description of the expected result. 70 | 71 | - Steps you have taken to debug it yourself. 72 | 73 | - If you post a question and do not outline the above items or make it easy for us to understand and reproduce your issue, it will be closed. 74 | 75 | ## PRs and Code contributions 76 | When you've got your contribution working, all test and lint style passed, and committed to your branch it's time to create a Pull Request (PR). If you are unsure how to do this GitHub's [Creating a pull request from a fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork) documentation will help you with that. Once you create your PR you will be presented with a template in the PR's description that looks like this: 77 | 78 | ```md 79 | 86 | 87 | ## Description 88 | 90 | 91 | ## Type of Change 92 | 93 | 94 | 95 | - [ ] ✨ New feature (non-breaking change which adds functionality) 96 | - [ ] 🛠️ Bug fix (non-breaking change which fixes an issue) 97 | - [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change) 98 | - [ ] 🧹 Code refactor 99 | - [ ] ✅ Build configuration change 100 | - [ ] 📝 Documentation 101 | - [ ] 🗑️ Chore 102 | 103 | All you need to do is fill in the information as requested by the template. Please do not remove this as it helps both you and the reviewers confirm that the various tasks have been completed. 104 | ``` 105 | 106 | Here is an examples of good PR descriptions: 107 | 108 | - 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2023 Chima Precious 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pharaoh 🏇 2 | 3 | [![Dart](https://github.com/codekeyz/pharaoh/workflows/Dart/badge.svg)](https://github.com/codekeyz/pharaoh/actions/workflows/test.yml) 4 | [![codecov](https://codecov.io/gh/codekeyz/pharaoh/graph/badge.svg?token=4CJTGP1U2M)](https://codecov.io/gh/codekeyz/pharaoh) 5 | [![Pub Version](https://img.shields.io/pub/v/pharaoh?color=green)](https://pub.dev/packages/pharaoh) 6 | [![popularity](https://img.shields.io/pub/popularity/pharaoh?logo=dart)](https://pub.dev/packages/pharaoh/score) 7 | [![likes](https://img.shields.io/pub/likes/pharaoh?logo=dart)](https://pub.dev/packages/pharaoh/score) 8 | [![melos](https://img.shields.io/badge/maintained%20with-melos-f700ff.svg?style=flat-square)](https://github.com/invertase/melos) 9 | 10 | ## Features 11 | 12 | - Robust routing 13 | - Focus on high performance 14 | - Super-high test coverage 15 | - HTTP helpers (just like ExpressJS) 16 | - Interoperability with Shelf Middlewares [See here](./packages/pharaoh/SHELF_INTEROP.md) 17 | 18 | ## Installing: 19 | 20 | In your pubspec.yaml 21 | 22 | ```yaml 23 | dependencies: 24 | pharaoh: ^0.0.6 # requires Dart => ^3.0.0 25 | ``` 26 | 27 | ## Basic Usage: 28 | 29 | ```dart 30 | import 'package:pharaoh/pharaoh.dart'; 31 | 32 | void main() async { 33 | 34 | final guestRouter = Pharaoh.router 35 | ..get('/user', (req, res) => res.ok("Hello World")) 36 | ..post('/post', (req, res) => res.json({"mee": "moo"})) 37 | ..put('/put', (req, res) => res.json({"pookey": "reyrey"})); 38 | 39 | final app = Pharaoh() 40 | ..use((req, res, next) => next()); 41 | ..get('/foo', (req, res) => res.ok("bar")) 42 | ..group('/guest', guestRouter); 43 | 44 | await app.listen(); 45 | } 46 | ``` 47 | 48 | See the [Pharaoh Examples](./pharaoh_examples/lib/) directory for more practical use-cases. 49 | 50 | ## Philosophy 51 | 52 | Pharaoh emerges as a backend framework, inspired by the likes of ExpressJS, to empower developers in building comprehensive server-side applications using Dart. The driving force behind Pharaoh's creation is a strong belief in the potential of Dart to serve as the primary language for developing the entire architecture of a company's product. Just as the JavaScript ecosystem has evolved, Pharaoh aims to contribute to the Dart ecosystem, providing a foundation for building scalable and feature-rich server-side applications. 53 | 54 | ## Contributors ✨ 55 | 56 | The Pharaoh project welcomes all constructive contributions. Contributions take many forms, 57 | from code for bug fixes and enhancements, to additions and fixes to documentation, additional 58 | tests, triaging incoming pull requests and issues, and more! 59 | 60 | ### Contributing Code To Pharaoh 🛠 61 | 62 | To setup and contribute to Pharaoh, Install [`Melos`](https://melos.invertase.dev/~melos-latest) as a global package via [`pub.dev`](https://pub.dev/packages/melos); 63 | 64 | ```console 65 | $ dart pub global activate melos 66 | ``` 67 | 68 | then initialize the workspace using the command below 69 | 70 | ```console 71 | $ melos bootstrap 72 | ``` 73 | 74 | ### Running Tests 75 | 76 | To run the tests, you can either run `dart test` in the package you're working on or use the command below to run the full test suite: 77 | 78 | ```console 79 | $ melos run tests 80 | ``` 81 | 82 | ## People 83 | 84 | [List of all contributors](https://github.com/codekeyz/pharaoh/graphs/contributors) 85 | 86 | ## License 87 | 88 | [MIT](LICENSE) 89 | -------------------------------------------------------------------------------- /melos.yaml: -------------------------------------------------------------------------------- 1 | name: pharaohdev 2 | 3 | packages: 4 | - "packages/**" 5 | - pharaoh_examples 6 | 7 | command: 8 | version: 9 | branch: master 10 | workspaceChangelog: true 11 | 12 | bootstrap: 13 | runPubGetInParallel: false 14 | hooks: 15 | pre: dart pub global activate coverage 16 | 17 | scripts: 18 | tests: 19 | run: | 20 | melos exec -c 1 -- "dart test" --fail-fast 21 | 22 | tests:ci: 23 | run: | 24 | melos exec -c 1 -- "dart test --coverage=coverage" --fail-fast 25 | melos exec -- "dart pub global run coverage:format_coverage --check-ignore --report-on=lib --lcov -o "$MELOS_ROOT_PATH/coverage/$(echo "\$MELOS_PACKAGE_NAME")_lcov.info" -i ./coverage" 26 | find $MELOS_ROOT_PATH/coverage -type f -empty -print -delete 27 | 28 | format: melos exec -- "dart format ." 29 | 30 | analyze: melos exec -- "dart analyze . --fatal-infos" 31 | -------------------------------------------------------------------------------- /packages/pharaoh/.gitignore: -------------------------------------------------------------------------------- 1 | # https://dart.dev/guides/libraries/private-files 2 | # Created by `dart pub` 3 | .dart_tool/ 4 | 5 | # Avoid committing pubspec.lock for library packages; see 6 | # https://dart.dev/guides/libraries/private-files#pubspeclock. 7 | pubspec.lock 8 | .idea/ 9 | **/pubspec_overrides.yaml 10 | **.reflectable.dart 11 | -------------------------------------------------------------------------------- /packages/pharaoh/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.0.8 2 | 3 | - perf: Use new improvements in spanner version 4 | 5 | # 0.0.7 6 | 7 | - chore: Perf improvements by @codekeyz in https://github.com/codekeyz/pharaoh/pull/123 8 | - chore: Bump package versions by @codekeyz in https://github.com/codekeyz/pharaoh/pull/124 9 | 10 | # 0.0.6+2 11 | 12 | - chore: Pass response to onError cb by @codekeyz in https://github.com/codekeyz/pharaoh/pull/105 13 | - chore: Pass stack-trace along with Exception by @codekeyz in https://github.com/codekeyz/pharaoh/pull/108 14 | - chore: Add actual type for pharaoh error by @codekeyz in https://github.com/codekeyz/pharaoh/pull/109 15 | - feat: Allow passing uri directly to Spookie by @codekeyz in https://github.com/codekeyz/pharaoh/pull/111 16 | - bugfix: pass path & query correctly from Spookie by @codekeyz in https://github.com/codekeyz/pharaoh/pull/112 17 | 18 | # 0.0.6 19 | 20 | - bugfix: wildcard search in node by @codekeyz in https://github.com/codekeyz/pharaoh/pull/98 21 | - chore: make request response extensible by @codekeyz in https://github.com/codekeyz/pharaoh/pull/99 22 | - bugfix: fix json-encode in cookieParser by @codekeyz in https://github.com/codekeyz/pharaoh/pull/100 23 | - bugfix: fix response redirects by @codekeyz in https://github.com/codekeyz/pharaoh/pull/103 24 | 25 | # 0.0.5 26 | 27 | - Added new router implementation 28 | - Added cookie-parser & session management 29 | - Added content negotiation -> `res.format` 30 | - Separated query parameters from route parameters 31 | 32 | # 0.0.4 33 | 34 | - Make `supertest` tests composable by @codekeyz in https://github.com/codekeyz/pharaoh/pull/40 35 | - downgrade minimum dart version to 3.0.0 by @iamEtornam in https://github.com/codekeyz/pharaoh/pull/42 36 | 37 | ## New Contributors 38 | 39 | - @iamEtornam made their first contribution in https://github.com/codekeyz/pharaoh/pull/42 40 | 41 | **Full Changelog**: https://github.com/codekeyz/pharaoh/compare/0.0.3...0.0.4 42 | 43 | # 0.0.3 44 | 45 | - Fix middleware match by @codekeyz in https://github.com/codekeyz/pharaoh/pull/24 46 | - Feature/improve shelf middleware adapter by @codekeyz in https://github.com/codekeyz/pharaoh/pull/27 47 | - Feature/pharaoh basic auth by @codekeyz in https://github.com/codekeyz/pharaoh/pull/34 48 | - Port shelf-static to pharaoh by @codekeyz in https://github.com/codekeyz/pharaoh/pull/29 49 | - Fix/only allow get and head by @codekeyz in https://github.com/codekeyz/pharaoh/pull/35 50 | 51 | **Full Changelog**: https://github.com/codekeyz/pharaoh/commits/0.0.3 52 | 53 | # 0.0.2 54 | 55 | - Made fixes to Middlewares in Route Group 56 | - Improve adapter for Shelf Middlewares 57 | - Add more examples & fix pub.dev scores 58 | 59 | I'm actively looking for contributors. Do checkout the project and leave a star if you find this useful. 👋 60 | 61 | # 0.0.1 62 | 63 | - Initial version 64 | -------------------------------------------------------------------------------- /packages/pharaoh/LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2023 Chima Precious 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /packages/pharaoh/README.md: -------------------------------------------------------------------------------- 1 | # Pharaoh 🏇 2 | 3 | [![Dart CI](https://github.com/codekeyz/pharaoh/workflows/Dart/badge.svg)](https://github.com/codekeyz/pharaoh/actions/workflows/dart.yml) 4 | [![Pub Version](https://img.shields.io/pub/v/pharaoh?color=green)](https://pub.dev/packages/pharaoh) 5 | [![popularity](https://img.shields.io/pub/popularity/pharaoh?logo=dart)](https://pub.dev/packages/pharaoh/score) 6 | [![likes](https://img.shields.io/pub/likes/pharaoh?logo=dart)](https://pub.dev/packages/pharaoh/score) 7 | [![melos](https://img.shields.io/badge/maintained%20with-melos-f700ff.svg?style=flat-square)](https://github.com/invertase/melos) 8 | 9 | ## Features 10 | 11 | - Robust routing 12 | - Focus on high performance 13 | - Super-high test coverage 14 | - HTTP helpers (just like ExpressJS) 15 | - Interoperability with Shelf Middlewares [See here](https://github.com/Pharaoh-Framework/pharaoh/blob/main/packages/pharaoh/SHELF_INTEROP.md) 16 | 17 | ## Installing: 18 | 19 | In your pubspec.yaml 20 | 21 | ```yaml 22 | dependencies: 23 | pharaoh: ^0.0.6 # requires Dart => ^3.0.0 24 | ``` 25 | 26 | ## Basic Usage: 27 | 28 | ```dart 29 | import 'package:pharaoh/pharaoh.dart'; 30 | 31 | final app = Pharaoh(); 32 | 33 | void main() async { 34 | 35 | app.use((req, res, next) { 36 | 37 | /// do something here 38 | 39 | next(); 40 | 41 | }); 42 | 43 | app.get('/foo', (req, res) => res.ok("bar")); 44 | 45 | final guestRouter = Pharaoh.router 46 | ..get('/user', (req, res) => res.ok("Hello World")) 47 | ..post('/post', (req, res) => res.json({"mee": "moo"})) 48 | ..put('/put', (req, res) => res.json({"pookey": "reyrey"})); 49 | 50 | app.group('/guest', guestRouter); 51 | 52 | await app.listen(); // port => 3000 53 | } 54 | 55 | ``` 56 | 57 | See the [Pharaoh Examples](https://github.com/codekeyz/pharaoh/tree/main/pharaoh_examples) directory for more practical use-cases. 58 | 59 | ## Philosophy 60 | 61 | Pharaoh emerges as a backend framework, inspired by the likes of ExpressJS, to empower developers in building comprehensive server-side applications using Dart. The driving force behind Pharaoh's creation is a strong belief in the potential of Dart to serve as the primary language for developing the entire architecture of a company's product. Just as the JavaScript ecosystem has evolved, Pharaoh aims to contribute to the Dart ecosystem, providing a foundation for building scalable and feature-rich server-side applications. 62 | 63 | ## Contributors ✨ 64 | 65 | The Pharaoh project welcomes all constructive contributions. Contributions take many forms, 66 | from code for bug fixes and enhancements, to additions and fixes to documentation, additional 67 | tests, triaging incoming pull requests and issues, and more! 68 | 69 | ### Contributing Code To Pharaoh 🛠 70 | 71 | To setup and contribute to Pharaoh, Install [`Melos`](https://melos.invertase.dev/~melos-latest) as a global package via [`pub.dev`](https://pub.dev/packages/melos); 72 | 73 | ```console 74 | $ dart pub global activate melos 75 | ``` 76 | 77 | then initialize the workspace using the command below 78 | 79 | ```console 80 | $ melos bootstrap 81 | ``` 82 | 83 | ### Running Tests 84 | 85 | To run the tests, you can either run `dart test` in the package you're working on or use the command below to run the full test suite: 86 | 87 | ```console 88 | $ melos run tests 89 | ``` 90 | 91 | ## People 92 | 93 | [List of all contributors](https://github.com/codekeyz/pharaoh/graphs/contributors) 94 | 95 | ## License 96 | 97 | [MIT](LICENSE) 98 | -------------------------------------------------------------------------------- /packages/pharaoh/SHELF_INTEROP.md: -------------------------------------------------------------------------------- 1 | # Shelf Interoperability 2 | 3 | Before jumping into building Pharaoh, I realized there was already a large pool of `shelf` middlewares that could do most of the things I thought I'd have needed to write so i wrote an adapter to allow you use them directly with `Pharaoh`. 4 | 5 | Here's a list of libraries that I personally have tested and confirmed to work with `Pharaoh` through the `useShelfMiddleware` hook. 6 | 7 | - [Shelf Cors Headers](https://pub.dev/packages/shelf_cors_headers) -> [see example](https://github.com/codekeyz/pharaoh/tree/main/pharaoh_examples/lib/shelf_middleware/cors.dart) 8 | 9 | - [Shelf Static](https://pub.dev/packages/shelf_static) -> [see example](https://github.com/codekeyz/pharaoh/tree/main/pharaoh_examples/lib/serve_files_2/index.dart) 10 | 11 | - [Shelf Helmet](https://pub.dev/packages/shelf_helmet) -> [see example](https://github.com/codekeyz/pharaoh/tree/main/pharaoh_examples/lib/shelf_middleware/helmet.dart) 12 | 13 | ## Contributors ✨ 14 | 15 | You can try out other shelf libraries and verify that they work and send in a PR to update this doc. If you do find any that doesn't work too, please raise an issue and I might look into that. 16 | -------------------------------------------------------------------------------- /packages/pharaoh/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/core.yaml 2 | -------------------------------------------------------------------------------- /packages/pharaoh/example/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ### API Service 4 | 5 | API service with a database access, secured with `:api-key` using a Middleware on PATH: `/api` and 3 has routes. 6 | 7 | - GET: `/api/users` 8 | - GET: `/api/repos` 9 | - GET: `/api/user/:name/repos` 10 | 11 | [Jump to Source](https://github.com/codekeyz/pharaoh/blob/main/pharaoh_examples/lib/api_service/index.dart) 12 | 13 | ### Route Groups 14 | 15 | API service with two route groups `/guest` and `/admin`. 16 | 17 | - Group: `/admin` 18 | - Group: `/guest` 19 | 20 | [Jump to Source](https://github.com/codekeyz/pharaoh/tree/main/pharaoh_examples/lib/route_groups/index.dart) 21 | 22 | ### Middleware 23 | 24 | API service with Logger Middleware that logs every request that hits our server. 25 | 26 | [Jump to Source](https://github.com/codekeyz/pharaoh/blob/main/pharaoh_examples/lib/middleware/index.dart) 27 | 28 | ### CORS with Shelf Middleware 29 | 30 | Add CORS to our Pharaoah server using [shelf_cors_headers](https://pub.dev/packages/shelf_cors_headers) 31 | 32 | [Jump to Source](https://github.com/codekeyz/pharaoh/blob/main/pharaoh_examples/lib/shelf_middleware/cors.dart) 33 | 34 | ### Helmet with Pharaoh (Shelf Middleware) 35 | 36 | Use Helmet with Pharaoah [shelf_helmet](https://pub.dev/packages/shelf_helmet) 37 | 38 | [Jump to Source](https://github.com/codekeyz/pharaoh/blob/main/pharaoh_examples/lib/shelf_middleware/helmet.dart) 39 | 40 | ### Serve Webpages and Files 1 41 | 42 | Serve a Webpage, and files using Pharaoh 43 | 44 | [Jump to Source](https://github.com/codekeyz/pharaoh/blob/main/pharaoh_examples/lib/serve_files_1/index.dart) 45 | 46 | ### Serve Webpages and Files 2 47 | 48 | Serve a Webpage, favicon and Image using [shelf_static](https://pub.dev/packages/shelf_static) 49 | 50 | [Jump to Source](https://github.com/codekeyz/pharaoh/blob/main/pharaoh_examples/lib/serve_files_2/index.dart) 51 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/pharaoh.dart: -------------------------------------------------------------------------------- 1 | library; 2 | 3 | export 'src/view/view.dart'; 4 | export 'src/http/cookie.dart'; 5 | export 'src/http/request.dart'; 6 | export 'src/http/response.dart'; 7 | export 'src/http/router.dart'; 8 | 9 | export 'src/shelf_interop/adapter.dart'; 10 | export 'src/shelf_interop/shelf.dart' show ShelfBody; 11 | 12 | export 'src/utils/utils.dart'; 13 | export 'src/utils/exceptions.dart'; 14 | 15 | export 'src/middleware/session_mw.dart'; 16 | export 'src/middleware/body_parser.dart'; 17 | export 'src/middleware/cookie_parser.dart'; 18 | export 'src/middleware/request_logger.dart'; 19 | 20 | export 'package:spanner/spanner.dart' show HTTPMethod; 21 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/pharaoh_next.dart: -------------------------------------------------------------------------------- 1 | export 'pharaoh.dart'; 2 | export 'src/_next/core.dart'; 3 | export 'src/_next/http.dart'; 4 | export 'src/_next/router.dart'; 5 | export 'src/_next/validation.dart'; 6 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/_next/_core/config.dart: -------------------------------------------------------------------------------- 1 | part of '../core.dart'; 2 | 3 | DotEnv? _env; 4 | 5 | T env(String name, T defaultValue) { 6 | _env ??= DotEnv(quiet: true, includePlatformEnvironment: true)..load(); 7 | final strVal = _env![name]; 8 | if (strVal == null) return defaultValue; 9 | 10 | final parsedVal = switch (T) { 11 | const (String) => strVal, 12 | const (int) => int.parse(strVal), 13 | const (num) => num.parse(strVal), 14 | const (bool) => bool.parse(strVal), 15 | const (double) => double.parse(strVal), 16 | const (List) => jsonDecode(strVal), 17 | _ => throw ArgumentError.value( 18 | T, null, 'Unsupported Type used in `env` call.'), 19 | }; 20 | return parsedVal as T; 21 | } 22 | 23 | extension ConfigExtension on Map { 24 | T getValue(String name, {T? defaultValue, bool allowEmpty = false}) { 25 | final value = this[name] ?? defaultValue; 26 | if (value is! T) { 27 | throw ArgumentError.value( 28 | value, null, 'Invalid value provided for $name'); 29 | } 30 | if (value != null && value.toString().trim().isEmpty && !allowEmpty) { 31 | throw ArgumentError.value( 32 | value, null, 'Empty value not allowed for $name'); 33 | } 34 | return value; 35 | } 36 | } 37 | 38 | class AppConfig { 39 | final String name; 40 | final String environment; 41 | final bool isDebug; 42 | final String timezone; 43 | final String locale; 44 | final String key; 45 | final int? _port; 46 | final String? _url; 47 | 48 | Uri get _uri { 49 | final uri = Uri.parse(_url!); 50 | return _port == null ? uri : uri.replace(port: _port); 51 | } 52 | 53 | int get port => _uri.port; 54 | 55 | String get url => _uri.toString(); 56 | 57 | const AppConfig({ 58 | required this.name, 59 | required this.environment, 60 | required this.isDebug, 61 | required this.key, 62 | this.timezone = 'UTC', 63 | this.locale = 'en', 64 | int? port, 65 | String? url, 66 | }) : _port = port, 67 | _url = url; 68 | } 69 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/_next/_core/container.dart: -------------------------------------------------------------------------------- 1 | part of '../core.dart'; 2 | 3 | final GetIt _getIt = GetIt.instance; 4 | 5 | T instanceFromRegistry({Type? type}) { 6 | type ??= T; 7 | try { 8 | return _getIt.get(type: type) as T; 9 | } catch (_) { 10 | throw Exception('Dependency not found in registry: $type'); 11 | } 12 | } 13 | 14 | T registerSingleton(T instance) { 15 | return _getIt.registerSingleton(instance); 16 | } 17 | 18 | bool isRegistered() => _getIt.isRegistered(); 19 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/_next/_core/core_impl.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: avoid_function_literals_in_foreach_calls 2 | 3 | part of '../core.dart'; 4 | 5 | class _PharaohNextImpl implements Application { 6 | late final AppConfig _appConfig; 7 | late final Spanner _spanner; 8 | 9 | ViewEngine? _viewEngine; 10 | 11 | _PharaohNextImpl(this._appConfig, this._spanner); 12 | 13 | @override 14 | T singleton(T instance) => registerSingleton(instance); 15 | 16 | @override 17 | T instanceOf() => instanceFromRegistry(); 18 | 19 | @override 20 | void useRoutes(RoutesResolver routeResolver) { 21 | final routes = routeResolver.call(); 22 | routes.forEach((route) => route.commit(_spanner)); 23 | } 24 | 25 | @override 26 | void useViewEngine(ViewEngine viewEngine) => _viewEngine = viewEngine; 27 | 28 | @override 29 | AppConfig get config => _appConfig; 30 | 31 | @override 32 | String get name => config.name; 33 | 34 | @override 35 | String get url => config.url; 36 | 37 | @override 38 | int get port => config.port; 39 | 40 | Pharaoh _createPharaohInstance({OnErrorCallback? onException}) { 41 | final pharaoh = Pharaoh()..useSpanner(_spanner); 42 | Pharaoh.viewEngine = _viewEngine; 43 | 44 | if (onException != null) pharaoh.onError(onException); 45 | return pharaoh; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/_next/_core/reflector.dart: -------------------------------------------------------------------------------- 1 | part of '../core.dart'; 2 | 3 | class Injectable extends r.Reflectable { 4 | const Injectable() 5 | : super( 6 | r.invokingCapability, 7 | r.metadataCapability, 8 | r.newInstanceCapability, 9 | r.declarationsCapability, 10 | r.reflectedTypeCapability, 11 | r.typeRelationsCapability, 12 | const r.InstanceInvokeCapability('^[^_]'), 13 | r.subtypeQuantifyCapability, 14 | ); 15 | } 16 | 17 | const unnamedConstructor = ''; 18 | 19 | const inject = Injectable(); 20 | 21 | List filteredDeclarationsOf( 22 | r.ClassMirror cm, predicate) { 23 | var result = []; 24 | cm.declarations.forEach((k, v) { 25 | if (predicate(v)) result.add(v as X); 26 | }); 27 | return result; 28 | } 29 | 30 | r.ClassMirror reflectType(Type type) { 31 | try { 32 | return inject.reflectType(type) as r.ClassMirror; 33 | } catch (e) { 34 | throw UnsupportedError( 35 | 'Unable to reflect on $type. Re-run your build command'); 36 | } 37 | } 38 | 39 | extension ClassMirrorExtensions on r.ClassMirror { 40 | List get variables { 41 | return filteredDeclarationsOf(this, (v) => v is r.VariableMirror); 42 | } 43 | 44 | List get getters { 45 | return filteredDeclarationsOf( 46 | this, (v) => v is r.MethodMirror && v.isGetter); 47 | } 48 | 49 | List get setters { 50 | return filteredDeclarationsOf( 51 | this, (v) => v is r.MethodMirror && v.isSetter); 52 | } 53 | 54 | List get methods { 55 | return filteredDeclarationsOf( 56 | this, (v) => v is r.MethodMirror && v.isRegularMethod); 57 | } 58 | } 59 | 60 | T createNewInstance(Type classType) { 61 | final classMirror = reflectType(classType); 62 | final constructorMethod = classMirror.declarations.entries 63 | .firstWhereOrNull((e) => e.key == '$classType') 64 | ?.value as r.MethodMirror?; 65 | final constructorParameters = constructorMethod?.parameters ?? []; 66 | if (constructorParameters.isEmpty) { 67 | return classMirror.newInstance(unnamedConstructor, const []) as T; 68 | } 69 | 70 | final namedDeps = constructorParameters 71 | .where((e) => e.isNamed) 72 | .map((e) => ( 73 | name: e.simpleName, 74 | instance: instanceFromRegistry(type: e.reflectedType) 75 | )) 76 | .fold>( 77 | {}, (prev, e) => prev..[Symbol(e.name)] = e.instance); 78 | 79 | final dependencies = constructorParameters 80 | .where((e) => !e.isNamed) 81 | .map((e) => instanceFromRegistry(type: e.reflectedType)) 82 | .toList(); 83 | 84 | return classMirror.newInstance(unnamedConstructor, dependencies, namedDeps) 85 | as T; 86 | } 87 | 88 | ControllerMethod parseControllerMethod(ControllerMethodDefinition defn) { 89 | final type = defn.$1; 90 | final method = defn.$2; 91 | 92 | final ctrlMirror = inject.reflectType(type) as r.ClassMirror; 93 | if (ctrlMirror.superclass?.reflectedType != HTTPController) { 94 | throw ArgumentError('$type must extend BaseController'); 95 | } 96 | 97 | final methods = ctrlMirror.instanceMembers.values.whereType(); 98 | final actualMethod = 99 | methods.firstWhereOrNull((e) => e.simpleName == symbolToString(method)); 100 | if (actualMethod == null) { 101 | throw ArgumentError( 102 | '$type does not have method #${symbolToString(method)}'); 103 | } 104 | 105 | final parameters = actualMethod.parameters; 106 | if (parameters.isEmpty) return ControllerMethod(defn); 107 | 108 | if (parameters.any((e) => e.metadata.length > 1)) { 109 | throw ArgumentError( 110 | 'Multiple annotations using on $type #${symbolToString(method)} parameter'); 111 | } 112 | 113 | final params = parameters.map((e) { 114 | final meta = e.metadata.first; 115 | if (meta is! RequestAnnotation) { 116 | throw ArgumentError( 117 | 'Invalid annotation $meta used on $type #${symbolToString(method)} parameter', 118 | ); 119 | } 120 | 121 | final paramType = e.reflectedType; 122 | final maybeDto = _tryResolveDtoInstance(paramType); 123 | 124 | return ControllerMethodParam( 125 | e.simpleName, 126 | paramType, 127 | defaultValue: e.defaultValue, 128 | optional: e.isOptional, 129 | meta: meta, 130 | dto: maybeDto, 131 | ); 132 | }); 133 | 134 | return ControllerMethod(defn, params); 135 | } 136 | 137 | BaseDTO? _tryResolveDtoInstance(Type type) { 138 | try { 139 | final mirror = dtoReflector.reflectType(type) as r.ClassMirror; 140 | return mirror.newInstance(unnamedConstructor, []) as BaseDTO; 141 | } on r.NoSuchCapabilityError catch (_) { 142 | return null; 143 | } 144 | } 145 | 146 | void ensureIsSubTypeOf(Type objectType) { 147 | try { 148 | final type = reflectType(objectType); 149 | if (type.superclass!.reflectedType != Parent) throw Exception(); 150 | } catch (e) { 151 | throw ArgumentError.value(objectType, 'Invalid Type provided', 152 | 'Ensure your class extends `$Parent` class'); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/_next/_router/meta.dart: -------------------------------------------------------------------------------- 1 | part of '../router.dart'; 2 | 3 | abstract class RequestAnnotation { 4 | final String? name; 5 | 6 | const RequestAnnotation([this.name]); 7 | 8 | T process(Request request, ControllerMethodParam methodParam); 9 | } 10 | 11 | enum ValidationErrorLocation { param, query, body, header } 12 | 13 | class RequestValidationError extends Error { 14 | final String message; 15 | final Map? errors; 16 | final ValidationErrorLocation location; 17 | 18 | RequestValidationError.param(this.message) 19 | : location = ValidationErrorLocation.param, 20 | errors = null; 21 | 22 | RequestValidationError.header(this.message) 23 | : location = ValidationErrorLocation.header, 24 | errors = null; 25 | 26 | RequestValidationError.query(this.message) 27 | : location = ValidationErrorLocation.query, 28 | errors = null; 29 | 30 | RequestValidationError.body(this.message) 31 | : location = ValidationErrorLocation.body, 32 | errors = null; 33 | 34 | RequestValidationError.errors(this.location, this.errors) : message = ''; 35 | 36 | Map get errorBody => { 37 | 'location': location.name, 38 | if (errors != null) 39 | 'errors': errors!.entries.map((e) => '${e.key}: ${e.value}').toList(), 40 | if (message.isNotEmpty) 'errors': [message], 41 | }; 42 | 43 | @override 44 | String toString() => errorBody.toString(); 45 | } 46 | 47 | /// Use this to annotate a parameter in a controller method 48 | /// which will be resolved to the request body. 49 | /// 50 | /// Example: create(@Body() user) {} 51 | class Body extends RequestAnnotation { 52 | const Body(); 53 | 54 | @override 55 | process(Request request, ControllerMethodParam methodParam) { 56 | final body = request.body; 57 | if (body == null) { 58 | if (methodParam.optional) return null; 59 | throw RequestValidationError.body( 60 | EzValidator.globalLocale.required('body')); 61 | } 62 | 63 | final dtoInstance = methodParam.dto; 64 | if (dtoInstance != null) return dtoInstance..make(request); 65 | 66 | final type = methodParam.type; 67 | if (type != dynamic && body.runtimeType != type) { 68 | throw RequestValidationError.body( 69 | EzValidator.globalLocale.isTypeOf('${methodParam.type}', 'body')); 70 | } 71 | 72 | return body; 73 | } 74 | } 75 | 76 | /// Use this to annotate a parameter in a controller method 77 | /// which will be resolved to a parameter in the request path. 78 | /// 79 | /// `/users//details` Example: getUser(@Param() String userId) {} 80 | class Param extends RequestAnnotation { 81 | const Param([super.name]); 82 | 83 | @override 84 | process(Request request, ControllerMethodParam methodParam) { 85 | final paramName = name ?? methodParam.name; 86 | final value = request.params[paramName] ?? methodParam.defaultValue; 87 | final parsedValue = _parseValue(value, methodParam.type); 88 | if (parsedValue == null) { 89 | throw RequestValidationError.param( 90 | EzValidator.globalLocale.isTypeOf('${methodParam.type}', paramName)); 91 | } 92 | return parsedValue; 93 | } 94 | } 95 | 96 | /// Use this to annotate a parameter in a controller method 97 | /// which will be resolved to a parameter in the request query params. 98 | /// 99 | /// `/users?name=Chima` Example: searchUsers(@Query() String name) {} 100 | class Query extends RequestAnnotation { 101 | const Query([super.name]); 102 | 103 | @override 104 | process(Request request, ControllerMethodParam methodParam) { 105 | final paramName = name ?? methodParam.name; 106 | final value = request.query[paramName] ?? methodParam.defaultValue; 107 | if (!methodParam.optional && value == null) { 108 | throw RequestValidationError.query( 109 | EzValidator.globalLocale.required(paramName)); 110 | } 111 | 112 | final parsedValue = _parseValue(value, methodParam.type); 113 | if (parsedValue == null) { 114 | throw RequestValidationError.query( 115 | EzValidator.globalLocale.isTypeOf('${methodParam.type}', paramName)); 116 | } 117 | return parsedValue; 118 | } 119 | } 120 | 121 | class Header extends RequestAnnotation { 122 | const Header([super.name]); 123 | 124 | @override 125 | process(Request request, ControllerMethodParam methodParam) { 126 | final paramName = name ?? methodParam.name; 127 | final value = request.headers[paramName] ?? methodParam.defaultValue; 128 | if (!methodParam.optional && value == null) { 129 | throw RequestValidationError.header( 130 | EzValidator.globalLocale.required(paramName)); 131 | } 132 | 133 | final parsedValue = _parseValue(value, methodParam.type); 134 | if (parsedValue == null) { 135 | throw RequestValidationError.header( 136 | EzValidator.globalLocale.isTypeOf('${methodParam.type}', paramName)); 137 | } 138 | return parsedValue; 139 | } 140 | } 141 | 142 | _parseValue(dynamic value, Type type) { 143 | if (value.runtimeType == type) return value; 144 | value = value.toString(); 145 | return switch (type) { 146 | const (int) => int.tryParse(value), 147 | const (double) => double.tryParse(value), 148 | const (bool) => value == 'true', 149 | const (List) || const (Map) => jsonDecode(value), 150 | _ => value, 151 | }; 152 | } 153 | 154 | const param = Param(); 155 | const query = Query(); 156 | const body = Body(); 157 | const header = Header(); 158 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/_next/_router/utils.dart: -------------------------------------------------------------------------------- 1 | part of '../router.dart'; 2 | 3 | String cleanRoute(String route) { 4 | final result = 5 | route.replaceAll(RegExp(r'/+'), '/').replaceAll(RegExp(r'/$'), ''); 6 | return result.isEmpty ? '/' : result; 7 | } 8 | 9 | String symbolToString(Symbol symbol) { 10 | final str = symbol.toString(); 11 | return str.substring(8, str.length - 2); 12 | } 13 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/_next/_validation/dto.dart: -------------------------------------------------------------------------------- 1 | part of '../validation.dart'; 2 | 3 | const _instanceInvoke = r.InstanceInvokeCapability('^[^_]'); 4 | 5 | class DtoReflector extends r.Reflectable { 6 | const DtoReflector() 7 | : super( 8 | r.typeCapability, 9 | r.metadataCapability, 10 | r.newInstanceCapability, 11 | r.declarationsCapability, 12 | r.reflectedTypeCapability, 13 | _instanceInvoke, 14 | r.subtypeQuantifyCapability); 15 | } 16 | 17 | @protected 18 | const dtoReflector = DtoReflector(); 19 | 20 | abstract interface class _BaseDTOImpl { 21 | late Map data; 22 | 23 | void make(Request request) { 24 | data = const {}; 25 | final (result, errors) = schema.validateSync(request.body ?? {}); 26 | if (errors.isNotEmpty) { 27 | throw RequestValidationError.errors(ValidationErrorLocation.body, errors); 28 | } 29 | data = Map.from(result); 30 | } 31 | 32 | EzSchema? _schemaCache; 33 | 34 | EzSchema get schema { 35 | if (_schemaCache != null) return _schemaCache!; 36 | 37 | final mirror = dtoReflector.reflectType(runtimeType) as r.ClassMirror; 38 | final properties = mirror.getters.where((e) => e.isAbstract); 39 | 40 | final entries = properties.map((prop) { 41 | final returnType = prop.reflectedReturnType; 42 | final meta = 43 | prop.metadata.whereType().firstOrNull ?? 44 | ezRequired(returnType); 45 | 46 | if (meta.propertyType != returnType) { 47 | throw ArgumentError( 48 | 'Type Mismatch between ${meta.runtimeType}(${meta.propertyType}) & $runtimeType class property ${prop.simpleName}->($returnType)'); 49 | } 50 | 51 | return MapEntry(meta.name ?? prop.simpleName, meta.validator); 52 | }); 53 | 54 | final entriesToMap = entries.fold>>( 55 | {}, (prev, curr) => prev..[curr.key] = curr.value); 56 | return _schemaCache = EzSchema.shape(entriesToMap); 57 | } 58 | } 59 | 60 | @dtoReflector 61 | abstract class BaseDTO extends _BaseDTOImpl { 62 | @override 63 | noSuchMethod(Invocation invocation) { 64 | final property = symbolToString(invocation.memberName); 65 | return data[property]; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/_next/_validation/meta.dart: -------------------------------------------------------------------------------- 1 | part of '../validation.dart'; 2 | 3 | abstract class ClassPropertyValidator { 4 | final String? name; 5 | 6 | /// TODO: we need to be able to infer nullability also from the type 7 | /// we'll need reflection for that, tho currently, the reason i'm not 8 | /// doing it is because of the amount of code the library (reflectable) 9 | /// generates just to enable this capability 10 | final bool optional; 11 | 12 | final T? defaultVal; 13 | 14 | Type get propertyType => T; 15 | 16 | const ClassPropertyValidator({ 17 | this.name, 18 | this.defaultVal, 19 | this.optional = false, 20 | }); 21 | 22 | EzValidator get validator { 23 | final base = EzValidator(defaultValue: defaultVal, optional: optional); 24 | return optional 25 | ? base.isType(propertyType) 26 | : base.required().isType(propertyType); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/_next/http.dart: -------------------------------------------------------------------------------- 1 | library; 2 | 3 | import 'dart:io'; 4 | 5 | import '../http/request.dart'; 6 | import '../http/response.dart'; 7 | import '../middleware/session_mw.dart'; 8 | import '../http/router.dart'; 9 | import 'core.dart'; 10 | 11 | @inject 12 | abstract class ClassMiddleware with AppInstance { 13 | handle(Request req, Response res, NextFunction next) { 14 | next(); 15 | } 16 | 17 | Middleware? get handler => null; 18 | } 19 | 20 | @inject 21 | abstract class ServiceProvider with AppInstance { 22 | static List get defaultProviders => []; 23 | 24 | void boot() {} 25 | 26 | void register() {} 27 | } 28 | 29 | @inject 30 | abstract class HTTPController with AppInstance { 31 | late final Request request; 32 | 33 | late final Response response; 34 | 35 | Map get params => request.params; 36 | 37 | Map get queryParams => request.query; 38 | 39 | Map get headers => request.headers; 40 | 41 | Session? get session => request.session; 42 | 43 | get requestBody => request.body; 44 | 45 | bool get expectsJson { 46 | final headerValue = 47 | request.headers[HttpHeaders.acceptEncodingHeader]?.toString(); 48 | return headerValue != null && headerValue.contains('application/json'); 49 | } 50 | 51 | Response badRequest([String? message]) { 52 | const status = 422; 53 | if (message == null) return response.status(status); 54 | return response.json({'error': message}, statusCode: status); 55 | } 56 | 57 | Response notFound([String? message]) { 58 | const status = 404; 59 | if (message == null) return response.status(status); 60 | return response.json({'error': message}, statusCode: status); 61 | } 62 | 63 | Response jsonResponse(data, {int statusCode = 200}) { 64 | return response.json(data, statusCode: statusCode); 65 | } 66 | 67 | Response redirectTo(String url, {int statusCode = 302}) { 68 | return response.redirect(url, statusCode); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/_next/router.dart: -------------------------------------------------------------------------------- 1 | library router; 2 | 3 | import 'dart:convert'; 4 | 5 | import 'package:spanner/spanner.dart'; 6 | import 'package:spanner/src/tree/tree.dart' show BASE_PATH; 7 | import 'package:ez_validator_dart/ez_validator.dart'; 8 | import 'package:grammer/grammer.dart'; 9 | import 'package:meta/meta.dart'; 10 | import '../http/request.dart'; 11 | import '../http/response.dart'; 12 | import '../http/router.dart'; 13 | import 'validation.dart'; 14 | import 'core.dart'; 15 | 16 | part '_router/definition.dart'; 17 | part '_router/meta.dart'; 18 | part '_router/utils.dart'; 19 | 20 | abstract interface class Route { 21 | static UseAliasedMiddleware middleware(String name) => 22 | UseAliasedMiddleware(name); 23 | 24 | static ControllerRouteMethodDefinition get( 25 | String path, ControllerMethodDefinition defn) => 26 | ControllerRouteMethodDefinition( 27 | defn, RouteMapping([HTTPMethod.GET], path)); 28 | 29 | static ControllerRouteMethodDefinition head( 30 | String path, ControllerMethodDefinition defn) => 31 | ControllerRouteMethodDefinition( 32 | defn, RouteMapping([HTTPMethod.HEAD], path)); 33 | 34 | static ControllerRouteMethodDefinition post( 35 | String path, ControllerMethodDefinition defn) => 36 | ControllerRouteMethodDefinition( 37 | defn, RouteMapping([HTTPMethod.POST], path)); 38 | 39 | static ControllerRouteMethodDefinition put( 40 | String path, ControllerMethodDefinition defn) => 41 | ControllerRouteMethodDefinition( 42 | defn, RouteMapping([HTTPMethod.PUT], path)); 43 | 44 | static ControllerRouteMethodDefinition delete( 45 | String path, ControllerMethodDefinition defn) => 46 | ControllerRouteMethodDefinition( 47 | defn, RouteMapping([HTTPMethod.DELETE], path)); 48 | 49 | static ControllerRouteMethodDefinition patch( 50 | String path, ControllerMethodDefinition defn) => 51 | ControllerRouteMethodDefinition( 52 | defn, RouteMapping([HTTPMethod.PATCH], path)); 53 | 54 | static ControllerRouteMethodDefinition options( 55 | String path, ControllerMethodDefinition defn) => 56 | ControllerRouteMethodDefinition( 57 | defn, RouteMapping([HTTPMethod.OPTIONS], path)); 58 | 59 | static ControllerRouteMethodDefinition trace( 60 | String path, ControllerMethodDefinition defn) => 61 | ControllerRouteMethodDefinition( 62 | defn, RouteMapping([HTTPMethod.TRACE], path)); 63 | 64 | static ControllerRouteMethodDefinition mapping( 65 | List methods, 66 | String path, 67 | ControllerMethodDefinition defn, 68 | ) { 69 | var mapping = RouteMapping(methods, path); 70 | if (methods.contains(HTTPMethod.ALL)) { 71 | mapping = RouteMapping([HTTPMethod.ALL], path); 72 | } 73 | return ControllerRouteMethodDefinition(defn, mapping); 74 | } 75 | 76 | static RouteGroupDefinition group(String name, List routes, 77 | {String? prefix}) => 78 | RouteGroupDefinition._(name, definitions: routes, prefix: prefix); 79 | 80 | static RouteGroupDefinition resource(String resource, Type controller, 81 | {String? parameterName}) { 82 | resource = resource.toLowerCase(); 83 | 84 | final resourceId = 85 | '${(parameterName ?? resource).toSingular().toLowerCase()}Id'; 86 | 87 | return Route.group(resource, [ 88 | Route.get('/', (controller, #index)), 89 | Route.get('/<$resourceId>', (controller, #show)), 90 | Route.post('/', (controller, #create)), 91 | Route.put('/<$resourceId>', (controller, #update)), 92 | Route.patch('/<$resourceId>', (controller, #update)), 93 | Route.delete('/<$resourceId>', (controller, #delete)) 94 | ]); 95 | } 96 | 97 | static FunctionalRouteDefinition route( 98 | HTTPMethod method, String path, RequestHandler handler) => 99 | FunctionalRouteDefinition.route(method, path, handler); 100 | 101 | static FunctionalRouteDefinition notFound(RequestHandler handler, 102 | [HTTPMethod method = HTTPMethod.ALL]) => 103 | Route.route(method, '/*', handler); 104 | } 105 | 106 | Middleware useAliasedMiddleware(String alias) => 107 | ApplicationFactory.resolveMiddlewareForGroup(alias) 108 | .reduce((val, e) => val.chain(e)); 109 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/_next/validation.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: camel_case_types 2 | 3 | import 'package:collection/collection.dart'; 4 | import 'package:ez_validator_dart/ez_validator.dart'; 5 | import 'package:meta/meta.dart'; 6 | import 'package:pharaoh/src/_next/core.dart'; 7 | import 'package:reflectable/reflectable.dart' as r; 8 | 9 | import '../http/request.dart'; 10 | import 'router.dart'; 11 | 12 | part '_validation/dto.dart'; 13 | part '_validation/meta.dart'; 14 | 15 | class ezEmail extends ClassPropertyValidator { 16 | final String? message; 17 | 18 | const ezEmail({super.name, super.defaultVal, super.optional, this.message}); 19 | 20 | @override 21 | EzValidator get validator => super.validator.email(message); 22 | } 23 | 24 | class ezDateTime extends ClassPropertyValidator { 25 | final String? message; 26 | 27 | final DateTime? minDate, maxDate; 28 | 29 | const ezDateTime( 30 | {super.name, 31 | super.defaultVal, 32 | super.optional, 33 | this.message, 34 | this.maxDate, 35 | this.minDate}); 36 | 37 | @override 38 | EzValidator get validator { 39 | final base = super.validator.date(message); 40 | if (minDate != null) return base.minDate(minDate!); 41 | if (maxDate != null) return base.maxDate(maxDate!); 42 | return base; 43 | } 44 | } 45 | 46 | class ezMinLength extends ClassPropertyValidator { 47 | final int value; 48 | 49 | const ezMinLength(this.value); 50 | 51 | @override 52 | EzValidator get validator => super.validator.minLength(value); 53 | } 54 | 55 | class ezMaxLength extends ClassPropertyValidator { 56 | final int value; 57 | 58 | const ezMaxLength(this.value); 59 | 60 | @override 61 | EzValidator get validator => super.validator.maxLength(value); 62 | } 63 | 64 | class ezRequired extends ClassPropertyValidator { 65 | final Type? type; 66 | 67 | const ezRequired([this.type]); 68 | 69 | @override 70 | Type get propertyType => type ?? T; 71 | } 72 | 73 | class ezOptional extends ClassPropertyValidator { 74 | final Type type; 75 | final Object? defaultValue; 76 | 77 | const ezOptional(this.type, {this.defaultValue}) 78 | : super(defaultVal: defaultValue); 79 | 80 | @override 81 | Type get propertyType => type; 82 | 83 | @override 84 | bool get optional => true; 85 | } 86 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/http/cookie.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import '../utils/exceptions.dart'; 5 | import '../utils/utils.dart'; 6 | 7 | /// [expires] The time at which the cookie expires. 8 | /// 9 | /// By default the value of `httpOnly` will be set to `true`. 10 | class CookieOpts { 11 | final String? domain; 12 | final String? secret; 13 | final DateTime? expires; 14 | final Duration? maxAge; 15 | final String path; 16 | final bool secure; 17 | final bool signed; 18 | final bool httpOnly; 19 | 20 | const CookieOpts({ 21 | this.domain, 22 | this.expires, 23 | this.maxAge, 24 | this.secret, 25 | this.httpOnly = false, 26 | this.signed = false, 27 | this.secure = false, 28 | this.path = '/', 29 | }); 30 | 31 | CookieOpts copyWith({ 32 | String? domain, 33 | String? secret, 34 | DateTime? expires, 35 | Duration? maxAge, 36 | String? path, 37 | bool? secure, 38 | bool? signed, 39 | bool? httpOnly, 40 | }) { 41 | return CookieOpts( 42 | domain: domain ?? this.domain, 43 | secret: secret ?? this.secret, 44 | expires: expires ?? this.expires, 45 | maxAge: maxAge ?? this.maxAge, 46 | path: path ?? this.path, 47 | secure: secure ?? this.secure, 48 | signed: signed ?? this.signed, 49 | httpOnly: httpOnly ?? this.httpOnly, 50 | ); 51 | } 52 | 53 | void validate() { 54 | if (signed && secret == null) { 55 | throw PharaohException.value( 56 | 'CookieOpts("secret") required for signed cookies'); 57 | } 58 | } 59 | } 60 | 61 | extension CookieExtension on Cookie { 62 | void setMaxAge(Duration? duration) { 63 | if (duration == null) { 64 | expires = null; 65 | maxAge = null; 66 | return; 67 | } 68 | 69 | expires = DateTime.now().add(duration); 70 | maxAge = duration.inSeconds; 71 | } 72 | 73 | String get decodedValue => Uri.decodeComponent(value); 74 | 75 | bool get signed => decodedValue.startsWith('s:'); 76 | 77 | String get actualStr => signed 78 | ? decodedValue.substring(2) 79 | : decodedValue; // s:foo-bar-baz --> foo-bar-bar 80 | 81 | bool get jsonEncoded => actualStr.startsWith('j:'); 82 | 83 | dynamic get actualObj { 84 | if (!jsonEncoded) return actualStr; 85 | final value = actualStr.substring(2); // j:foo-bar-baz --> foo-bar-bar 86 | return jsonDecode(value); 87 | } 88 | } 89 | 90 | Cookie bakeCookie(String name, Object? value, CookieOpts opts) { 91 | opts.validate(); 92 | if (value is! String) value = 'j:${jsonEncode(value)}'; 93 | if (opts.signed) value = 's:${signValue(value, opts.secret!)}'; 94 | 95 | return Cookie(name, Uri.encodeComponent(value)) 96 | ..httpOnly = opts.httpOnly 97 | ..domain = opts.domain 98 | ..path = opts.path 99 | ..secure = opts.secure 100 | ..setMaxAge(opts.maxAge); 101 | } 102 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/http/message.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:http_parser/http_parser.dart'; 5 | 6 | import '../shelf_interop/shelf.dart'; 7 | 8 | abstract class Message { 9 | final Map headers; 10 | 11 | T? body; 12 | 13 | MediaType? _contentTypeCache; 14 | 15 | Message(this.body, {required this.headers}); 16 | 17 | /// This is parsed from the Content-Type header in [headers]. It contains only 18 | /// the MIME type, without any Content-Type parameters. 19 | /// 20 | /// If [headers] doesn't have a Content-Type header, this will be `null`. 21 | MediaType? get mediaType { 22 | if (_contentTypeCache != null) return _contentTypeCache; 23 | var type = headers[HttpHeaders.contentTypeHeader]; 24 | if (type == null) return null; 25 | if (type is Iterable) type = type.join(';'); 26 | return _contentTypeCache = MediaType.parse(type); 27 | } 28 | 29 | String? get mimeType => mediaType?.mimeType; 30 | 31 | /// The encoding of the message body. 32 | /// 33 | /// This is parsed from the "charset" parameter of the Content-Type header in 34 | /// [headers]. 35 | /// 36 | /// If [headers] doesn't have a Content-Type header or it specifies an 37 | /// encoding that `dart:convert` doesn't support, this will be `null`. 38 | Encoding? get encoding { 39 | var ctype = mediaType; 40 | if (ctype == null) return null; 41 | if (!ctype.parameters.containsKey('charset')) return null; 42 | return Encoding.getByName(ctype.parameters['charset']); 43 | } 44 | 45 | int? get contentLength { 46 | final content = body; 47 | return content is ShelfBody ? content.contentLength : null; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/http/request.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:http_parser/http_parser.dart'; 4 | import 'package:pharaoh/pharaoh.dart'; 5 | 6 | import 'message.dart'; 7 | 8 | part 'request_impl.dart'; 9 | 10 | class RequestContext { 11 | static const String phar = 'phar'; 12 | static const String auth = '$phar.auth'; 13 | 14 | /// cookies & session 15 | static const String cookies = '$phar.cookies'; 16 | static const String signedCookies = '$phar.signedcookies'; 17 | static const String session = '$phar.session'; 18 | } 19 | 20 | HTTPMethod getHttpMethod(HttpRequest req) => switch (req.method) { 21 | 'GET' => HTTPMethod.GET, 22 | 'HEAD' => HTTPMethod.HEAD, 23 | 'POST' => HTTPMethod.POST, 24 | 'PUT' => HTTPMethod.PUT, 25 | 'DELETE' => HTTPMethod.DELETE, 26 | 'PATCH' => HTTPMethod.PATCH, 27 | 'OPTIONS' => HTTPMethod.OPTIONS, 28 | 'TRACE' => HTTPMethod.TRACE, 29 | _ => throw PharaohException('Method ${req.method} not yet supported') 30 | }; 31 | 32 | sealed class Request extends Message { 33 | factory Request.from(HttpRequest request) = _$RequestImpl._; 34 | 35 | late final HttpRequest actual; 36 | 37 | Request(super.body, {super.headers = const {}}); 38 | 39 | Uri get uri; 40 | 41 | String get path; 42 | 43 | Map get query; 44 | 45 | String get ipAddr; 46 | 47 | String? get hostname; 48 | 49 | String get protocol; 50 | 51 | String get protocolVersion; 52 | 53 | dynamic auth; 54 | 55 | HTTPMethod get method; 56 | 57 | Map get params; 58 | 59 | Map get headers; 60 | 61 | List get cookies; 62 | 63 | List get signedCookies; 64 | 65 | String? get sessionId; 66 | 67 | Session? get session; 68 | 69 | dynamic get body; 70 | 71 | DateTime? get ifModifiedSince; 72 | 73 | Object? operator [](String name); 74 | 75 | void operator []=(String name, dynamic value); 76 | 77 | void setParams(String key, String value); 78 | } 79 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/http/request_impl.dart: -------------------------------------------------------------------------------- 1 | part of 'request.dart'; 2 | 3 | class _$RequestImpl extends Request { 4 | final Map _params = {}; 5 | final Map _context = {}; 6 | 7 | _$RequestImpl._(HttpRequest _req) : super(_req, headers: {}) { 8 | actual = _req; 9 | actual.headers.forEach((name, values) => headers[name] = values); 10 | headers.remove(HttpHeaders.transferEncodingHeader); 11 | } 12 | 13 | @override 14 | void setParams(String key, String value) => _params[key] = value; 15 | 16 | /// If this is non-`null` and the requested resource hasn't been modified 17 | /// since this date and time, the server should return a 304 Not Modified 18 | /// response. 19 | /// 20 | /// This is parsed from the If-Modified-Since header in [headers]. If 21 | /// [headers] doesn't have an If-Modified-Since header, this will be `null`. 22 | /// 23 | /// Throws [FormatException], if incoming HTTP request has an invalid 24 | /// If-Modified-Since header. 25 | @override 26 | DateTime? get ifModifiedSince { 27 | if (_ifModifiedSinceCache != null) return _ifModifiedSinceCache; 28 | if (!headers.containsKey('if-modified-since')) return null; 29 | _ifModifiedSinceCache = parseHttpDate(headers['if-modified-since']!); 30 | return _ifModifiedSinceCache; 31 | } 32 | 33 | DateTime? _ifModifiedSinceCache; 34 | 35 | @override 36 | Uri get uri => actual.uri; 37 | 38 | @override 39 | String get path => actual.uri.path; 40 | 41 | @override 42 | String get ipAddr => 43 | actual.connectionInfo?.remoteAddress.address ?? 'Unknown'; 44 | 45 | @override 46 | HTTPMethod get method => getHttpMethod(actual); 47 | 48 | @override 49 | Map get params => _params; 50 | 51 | @override 52 | Map get query => actual.uri.queryParameters; 53 | 54 | @override 55 | String? get hostname => actual.headers.host; 56 | 57 | @override 58 | String get protocol => actual.requestedUri.scheme; 59 | 60 | @override 61 | String get protocolVersion => actual.protocolVersion; 62 | 63 | @override 64 | List get cookies => _context[RequestContext.cookies] ?? []; 65 | 66 | @override 67 | List get signedCookies => 68 | _context[RequestContext.signedCookies] ?? []; 69 | 70 | @override 71 | Session? get session => _context[RequestContext.session]; 72 | 73 | @override 74 | String? get sessionId => session?.id; 75 | 76 | @override 77 | Object? operator [](String name) => _context[name]; 78 | 79 | @override 80 | void operator []=(String name, dynamic value) { 81 | _context[name] = value; 82 | } 83 | 84 | @override 85 | dynamic get auth => _context[RequestContext.auth]; 86 | 87 | set auth(dynamic value) => _context[RequestContext.auth] = value; 88 | } 89 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/http/response.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:pharaoh/pharaoh.dart'; 5 | import 'package:http_parser/http_parser.dart'; 6 | 7 | import '../shelf_interop/shelf.dart' as shelf; 8 | 9 | import 'message.dart'; 10 | 11 | part 'response_impl.dart'; 12 | 13 | sealed class Response extends Message { 14 | Response(super.body, {super.headers = const {}}); 15 | 16 | /// Constructs an HTTP Response 17 | factory Response.create({ 18 | int? statusCode, 19 | Object? body, 20 | Encoding? encoding, 21 | Map? headers, 22 | }) => 23 | _$ResponseImpl._( 24 | body: body == null ? null : ShelfBody(body), 25 | ended: false, 26 | statusCode: statusCode, 27 | headers: headers ?? {}, 28 | ); 29 | 30 | Response header(String headerKey, String headerValue); 31 | 32 | /// Creates a new cookie setting the name and value. 33 | /// 34 | /// [name] and [value] must be composed of valid characters according to RFC 35 | /// 6265. 36 | Response cookie( 37 | String name, 38 | Object? value, [ 39 | CookieOpts opts = const CookieOpts(), 40 | ]); 41 | 42 | Response withCookie(Cookie cookie); 43 | 44 | Response type(ContentType type); 45 | 46 | Response status(int code); 47 | 48 | Response withBody(Object object); 49 | 50 | /// [data] should be json-encodable 51 | Response json(Object? data, {int? statusCode}); 52 | 53 | Response ok([String? data]); 54 | 55 | Response send(Object data); 56 | 57 | Response notModified({Map? headers}); 58 | 59 | Response format(Request request, Map data); 60 | 61 | Response notFound([String? message]); 62 | 63 | Response unauthorized({Object? data}); 64 | 65 | Response redirect(String url, [int statusCode = HttpStatus.found]); 66 | 67 | Response movedPermanently(String url); 68 | 69 | Response internalServerError([String? message]); 70 | 71 | Response render(String name, [Map data = const {}]); 72 | 73 | Response end(); 74 | 75 | bool get ended; 76 | 77 | int get statusCode; 78 | 79 | ViewRenderData? get viewToRender; 80 | } 81 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/http/router/router_contract.dart: -------------------------------------------------------------------------------- 1 | part of '../router.dart'; 2 | 3 | sealed class RouterContract { 4 | void get(String path, RequestHandler hdler); 5 | 6 | void post(String path, RequestHandler hdler); 7 | 8 | void put(String path, RequestHandler hdler); 9 | 10 | void delete(String path, RequestHandler hdler); 11 | 12 | void head(String path, RequestHandler hdler); 13 | 14 | void patch(String path, RequestHandler hdler); 15 | 16 | void options(String path, RequestHandler hdler); 17 | 18 | void trace(String path, RequestHandler hdler); 19 | 20 | void use(Middleware middleware); 21 | 22 | void on(String path, Middleware hdler, {HTTPMethod method = HTTPMethod.ALL}); 23 | } 24 | 25 | mixin RouteDefinitionMixin on RouterContract { 26 | late Spanner spanner; 27 | 28 | void useSpanner(Spanner router) { 29 | spanner = router; 30 | } 31 | 32 | @override 33 | void delete(String path, RequestHandler hdler) { 34 | spanner.addRoute(HTTPMethod.DELETE, path, useRequestHandler(hdler)); 35 | } 36 | 37 | @override 38 | void get(String path, RequestHandler hdler) { 39 | spanner.addRoute(HTTPMethod.GET, path, useRequestHandler(hdler)); 40 | } 41 | 42 | @override 43 | void head(String path, RequestHandler hdler) { 44 | spanner.addRoute(HTTPMethod.HEAD, path, useRequestHandler(hdler)); 45 | } 46 | 47 | @override 48 | void options(String path, RequestHandler hdler) { 49 | spanner.addRoute(HTTPMethod.OPTIONS, path, useRequestHandler(hdler)); 50 | } 51 | 52 | @override 53 | void patch(String path, RequestHandler hdler) { 54 | spanner.addRoute(HTTPMethod.PATCH, path, useRequestHandler(hdler)); 55 | } 56 | 57 | @override 58 | void post(String path, RequestHandler hdler) { 59 | spanner.addRoute(HTTPMethod.POST, path, useRequestHandler(hdler)); 60 | } 61 | 62 | @override 63 | void put(String path, RequestHandler hdler) { 64 | spanner.addRoute(HTTPMethod.PUT, path, useRequestHandler(hdler)); 65 | } 66 | 67 | @override 68 | void trace(String path, RequestHandler hdler) { 69 | spanner.addRoute(HTTPMethod.TRACE, path, useRequestHandler(hdler)); 70 | } 71 | 72 | @override 73 | void use(Middleware middleware) { 74 | spanner.addMiddleware(BASE_PATH, middleware); 75 | } 76 | 77 | @override 78 | void on( 79 | String path, 80 | Middleware middleware, { 81 | HTTPMethod method = HTTPMethod.ALL, 82 | }) { 83 | if (method == HTTPMethod.ALL) { 84 | spanner.addMiddleware(path, middleware); 85 | } else { 86 | spanner.addRoute(method, path, middleware); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/http/router/router_handler.dart: -------------------------------------------------------------------------------- 1 | part of '../router.dart'; 2 | 3 | typedef ReqRes = ({Request req, Response res}); 4 | 5 | final class RequestHook { 6 | final FutureOr Function(Request req, Response res)? onBefore; 7 | final FutureOr Function(Request req, Response res)? onAfter; 8 | const RequestHook({this.onAfter, this.onBefore}); 9 | } 10 | 11 | typedef NextFunction = dynamic Function([dynamic result, Next? chain]); 12 | 13 | typedef RequestHandler = FutureOr Function(Request req, Response res); 14 | 15 | typedef Middleware = FutureOr Function( 16 | Request req, 17 | Response res, 18 | NextFunction next, 19 | ); 20 | 21 | extension ReqResExtension on ReqRes { 22 | ReqRes merge(dynamic val) => switch (val) { 23 | ReqRes() => val, 24 | Response() => (req: this.req, res: val), 25 | Request() => (req: val, res: this.res), 26 | null => this, 27 | _ => throw PharaohException.value('Invalid Type used on merge', val) 28 | }; 29 | } 30 | 31 | extension MiddlewareChainExtension on Middleware { 32 | /// Chains the current middleware with a new one. 33 | Middleware chain(Middleware mdw) => (req, res, done) => this( 34 | req, 35 | res, 36 | ([nr, chain]) { 37 | // Use the existing chain if available, otherwise use the new chain 38 | Middleware nextMdw = chain ?? mdw; 39 | 40 | // If there's an existing chain, recursively chain the new handler 41 | if (chain != null) { 42 | nextMdw = nextMdw.chain(mdw); 43 | } 44 | 45 | done(nr, nextMdw); 46 | }, 47 | ); 48 | } 49 | 50 | Middleware useRequestHandler(RequestHandler handler) => 51 | (req, res, next_) async { 52 | final result = await handler(req, res); 53 | next_(result); 54 | }; 55 | 56 | Future executeHandlers( 57 | Iterable handlers, 58 | ReqRes reqRes, 59 | ) async { 60 | var result = reqRes; 61 | final iterator = handlers.iterator; 62 | 63 | Future handleChain([dynamic nr_, Middleware? mdw]) async { 64 | result = result.merge(nr_); 65 | if (mdw == null || result.res.ended) return; 66 | 67 | return await mdw.call( 68 | result.req, 69 | result.res, 70 | ([nr_, chain]) => handleChain(nr_, chain), 71 | ); 72 | } 73 | 74 | while (iterator.moveNext()) { 75 | await handleChain(null, iterator.current); 76 | if (result.res.ended) break; 77 | } 78 | 79 | return result; 80 | } 81 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/http/session.dart: -------------------------------------------------------------------------------- 1 | part of '../middleware/session_mw.dart'; 2 | 3 | class Session { 4 | static const String name = 'pharaoh.sid'; 5 | 6 | final String id; 7 | late final Map _dataBag; 8 | late final String hash; 9 | 10 | bool _resave = false; 11 | bool _saveUninitialized = false; 12 | 13 | bool get resave => _resave; 14 | bool get saveUninitialized => _saveUninitialized; 15 | 16 | SessionStore? _store; 17 | Cookie? cookie; 18 | 19 | Session( 20 | this.id, { 21 | Map data = const {}, 22 | }) : _dataBag = {...data} { 23 | hash = hashData(_dataBag); 24 | } 25 | 26 | void _withStore(SessionStore store) { 27 | _store = store; 28 | } 29 | 30 | void _withConfig({bool? resave, bool? saveUninitialized}) { 31 | this._resave = resave ?? false; 32 | this._saveUninitialized = saveUninitialized ?? false; 33 | } 34 | 35 | void resetMaxAge() { 36 | final cookie_ = cookie; 37 | if (cookie_ == null || cookie_.maxAge == null) return; 38 | final expires = DateTime.now().add(Duration(seconds: cookie_.maxAge!)); 39 | cookie = cookie_..expires = expires; 40 | } 41 | 42 | Map toJson() => { 43 | 'id': id, 44 | 'data': _dataBag, 45 | 'cookie': cookie?.toString(), 46 | }; 47 | 48 | void operator []=(String name, dynamic value) { 49 | _dataBag[name] = value; 50 | } 51 | 52 | dynamic operator [](String key) => _dataBag[key]; 53 | 54 | @override 55 | String toString() => jsonEncode(toJson()); 56 | 57 | FutureOr save() => _store!.set(id, this); 58 | 59 | FutureOr destroy() => _store!.destroy(id); 60 | 61 | bool get valid { 62 | final exp = expiry; 63 | if (exp == null) return true; 64 | return exp.isAfter(DateTime.now()); 65 | } 66 | 67 | bool get modified => hash != hashData(_dataBag); 68 | 69 | DateTime? get expiry => cookie?.expires; 70 | } 71 | 72 | abstract class SessionStore { 73 | FutureOr> get sessions; 74 | 75 | FutureOr clear(); 76 | 77 | FutureOr destroy(String sessionId); 78 | 79 | FutureOr set(String sessionId, Session value); 80 | 81 | FutureOr get(String sessionId); 82 | } 83 | 84 | class InMemoryStore extends SessionStore { 85 | final Map _sessionsMap = {}; 86 | 87 | @override 88 | void clear() => _sessionsMap.clear(); 89 | 90 | @override 91 | void destroy(String sessionId) => _sessionsMap.remove(sessionId); 92 | 93 | @override 94 | List get sessions => _sessionsMap.values.toList(); 95 | 96 | @override 97 | void set(String sessionId, Session value) => _sessionsMap[sessionId] = value; 98 | 99 | @override 100 | Session? get(String sessionId) => _sessionsMap[sessionId]; 101 | } 102 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/middleware/body_parser.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:mime/mime.dart'; 5 | 6 | import '../http/request.dart'; 7 | import '../http/response.dart'; 8 | import '../http/router.dart'; 9 | 10 | sealed class MimeType { 11 | static const String multiPartForm = 'multipart/form-data'; 12 | static const String applicationFormUrlEncoded = 13 | 'application/x-www-form-urlencoded'; 14 | static const String applicationJson = 'application/json'; 15 | static const String textPlain = 'text/plain'; 16 | } 17 | 18 | bodyParser(Request req, Response res, NextFunction next) async { 19 | final mimeType = req.mediaType?.mimeType; 20 | if (mimeType == null || req.actual.contentLength == 0) { 21 | return next(req..body = null); 22 | } 23 | 24 | if (mimeType == MimeType.multiPartForm) { 25 | final boundary = req.mediaType!.parameters['boundary']; 26 | if (boundary == null) return next(req..body = null); 27 | 28 | final parts = MimeMultipartTransformer(boundary).bind(req.actual); 29 | final dataBag = {}; 30 | 31 | await for (final part in parts) { 32 | final header = HeaderValue.parse(part.headers['content-disposition']!); 33 | final name = header.parameters['name']!; 34 | final filename = header.parameters['filename']; 35 | if (filename != null) break; 36 | dataBag[name] = await utf8.decodeStream(part); 37 | } 38 | 39 | return next(req..body = dataBag); 40 | } 41 | 42 | final buffer = StringBuffer(); 43 | await for (final chunk in utf8.decoder.bind(req.actual)) { 44 | if (chunk.isEmpty) return next(req..body = null); 45 | buffer.write(chunk); 46 | } 47 | final body = buffer.toString(); 48 | 49 | switch (mimeType) { 50 | case MimeType.applicationFormUrlEncoded: 51 | req.body = Uri.splitQueryString(Uri.decodeFull(body)); 52 | break; 53 | case MimeType.applicationJson: 54 | if (body.isNotEmpty) req.body = json.decode(body); 55 | break; 56 | case MimeType.textPlain: 57 | req.body = body; 58 | break; 59 | } 60 | 61 | next(req); 62 | } 63 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/middleware/cookie_parser.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:pharaoh/src/utils/utils.dart'; 4 | 5 | import '../http/cookie.dart'; 6 | import '../http/request.dart'; 7 | import '../http/router.dart'; 8 | 9 | Middleware cookieParser({CookieOpts opts = const CookieOpts()}) { 10 | opts.validate(); 11 | 12 | return (req, res, next) { 13 | final rawcookies = req.actual.cookies; 14 | if (rawcookies.isEmpty) return next(); 15 | 16 | final unSignedCookies = rawcookies.where((e) => !e.signed).toList(); 17 | var signedCookies = rawcookies.where((e) => e.signed).toList(); 18 | 19 | final secret = opts.secret; 20 | if (secret != null && signedCookies.isNotEmpty) { 21 | final verifiedCookies = []; 22 | 23 | for (final cookie in signedCookies) { 24 | var realValue = unsignValue(cookie.actualStr, secret); 25 | if (realValue != null) { 26 | verifiedCookies.add(cookie..value = Uri.encodeComponent(realValue)); 27 | } 28 | } 29 | signedCookies = verifiedCookies; 30 | } 31 | 32 | req[RequestContext.cookies] = unSignedCookies; 33 | req[RequestContext.signedCookies] = signedCookies; 34 | 35 | next(req); 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/middleware/request_logger.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import '../http/router.dart'; 4 | 5 | final logRequestHook = RequestHook( 6 | onBefore: (req, res) async { 7 | req['startTime'] = DateTime.now(); 8 | return (req: req, res: res); 9 | }, 10 | onAfter: (req, res) async { 11 | final startTime = req['startTime'] as DateTime; 12 | final elapsedTime = DateTime.now().difference(startTime).inMilliseconds; 13 | 14 | final logLines = """ 15 | Request: ${req.method.name} ${req.path} 16 | Content-Type: ${req.mimeType} 17 | Status Code: ${res.statusCode} 18 | Elapsed Time: ${elapsedTime} ms 19 | ------------------------------------------------------- 20 | """; 21 | stdout.writeln(logLines); 22 | 23 | return (req: req, res: res); 24 | }, 25 | ); 26 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/middleware/session_mw.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | 5 | import 'package:collection/collection.dart'; 6 | import 'package:uuid/uuid.dart'; 7 | 8 | import '../http/cookie.dart'; 9 | import '../http/request.dart'; 10 | import '../http/router.dart'; 11 | import '../utils/utils.dart'; 12 | 13 | part '../http/session.dart'; 14 | 15 | typedef GenSessionIdFunc = FutureOr Function(Request request); 16 | 17 | /// - [name] The name of the session ID cookie to set in the response 18 | /// (and read from in the request). 19 | /// The default value is `pharaoh.sid`. 20 | /// 21 | /// - [saveUninitialized] Forces a session that is "uninitialized" to 22 | /// be saved to the store. A session is uninitialized when it is new 23 | /// but not modified. 24 | /// Default value is `true` 25 | /// 26 | /// - [rolling] The expiration is reset to the original maxAge, 27 | /// resetting the expiration countdown and forces 28 | /// the session identifier cookie to be set on every response. 29 | /// 30 | /// - [resave] Forces the session to be saved back to the session store, 31 | /// even if the session was never modified during the request. 32 | /// Default value is `false` 33 | /// 34 | /// - [genId] Function to call to generate a new session ID. Provide a 35 | /// function that returns a string that will be used as a session ID. 36 | /// Session ID is generated from [uuid]-[v4] by default. 37 | /// 38 | /// - [store] The session store instance, defaults to a new MemoryStore 39 | /// instance. 40 | /// 41 | /// - [cookie] Settings object for the session ID cookie. The default 42 | /// value is `{ path: '/', httpOnly: true, secure: false, maxAge: null }`. 43 | /// 44 | /// - [secret] This is the secret used to sign the session ID cookie. 45 | /// You can also provide it in [cookie.secret] options. But [secret] will 46 | /// will be used if both are set. 47 | Middleware sessionMdw({ 48 | String name = Session.name, 49 | String? secret, 50 | bool saveUninitialized = true, 51 | bool rolling = false, 52 | bool resave = false, 53 | GenSessionIdFunc? genId, 54 | SessionStore? store, 55 | CookieOpts cookie = const CookieOpts(httpOnly: true, secure: false), 56 | }) { 57 | if (secret != null) cookie = cookie.copyWith(secret: secret); 58 | final opts = cookie.copyWith(signed: true)..validate(); 59 | final sessionStore = store ??= InMemoryStore(); 60 | final uuid = Uuid(); 61 | 62 | return (req, res, next) async { 63 | nextWithSession(Session session) { 64 | req[RequestContext.session] = session.._withStore(sessionStore); 65 | return next((req: req, res: res)); 66 | } 67 | 68 | if (!req.path.startsWith(opts.path)) return next(); 69 | if (req.session?.valid ?? false) return next(); 70 | 71 | final reqSid = 72 | req.signedCookies.firstWhereOrNull((e) => e.name == name)?.value; 73 | if (reqSid != null) { 74 | var result = await sessionStore.get(reqSid); 75 | if (result != null && result.valid) { 76 | if (rolling) result = result..resetMaxAge(); 77 | return nextWithSession(result); 78 | } 79 | 80 | await sessionStore.destroy(reqSid); 81 | } 82 | 83 | final sessionId = await genId?.call(req) ?? uuid.v4(); 84 | final session = Session(sessionId); 85 | final cookie = bakeCookie(name, sessionId, opts); 86 | session 87 | ..cookie = cookie 88 | .._withConfig(saveUninitialized: saveUninitialized, resave: resave); 89 | 90 | return nextWithSession(session); 91 | }; 92 | } 93 | 94 | final sessionPreResponseHook = RequestHook( 95 | onAfter: (req, res) async { 96 | final session = req.session; 97 | final reqRes = (req: req, res: res); 98 | if (session == null) return reqRes; 99 | 100 | if (session.saveUninitialized || session.resave || session.modified) { 101 | await session.save(); 102 | res = res.withCookie(session.cookie!); 103 | } 104 | 105 | return (req: req, res: res); 106 | }, 107 | ); 108 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/shelf_interop/adapter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import '../http/request.dart'; 4 | import '../http/response.dart'; 5 | import '../http/router.dart'; 6 | import '../utils/exceptions.dart'; 7 | import 'shelf.dart' as shelf; 8 | 9 | typedef ShelfMiddlewareType2 = FutureOr Function(shelf.Request); 10 | 11 | /// Use this hook to transform any shelf 12 | /// middleware into a [Middleware] that Pharaoh 13 | /// can use. 14 | /// 15 | /// This will also throw an Exception if you use a Middleware 16 | /// that has a [Type] signature different from either [shelf.Middleware] 17 | /// or [ShelfMiddlewareType2] tho in most cases, it should work. 18 | Middleware useShelfMiddleware(dynamic middleware) { 19 | if (middleware is shelf.Middleware) { 20 | return (req, res, next) async { 21 | final shelfResponse = await middleware( 22 | (req) => shelf.Response.ok(req.read()))(_toShelfRequest(req)); 23 | res = _fromShelfResponse((req: req, res: res), shelfResponse); 24 | 25 | next(res); 26 | }; 27 | } 28 | 29 | if (middleware is ShelfMiddlewareType2) { 30 | return (req, res, next) async { 31 | final shelfResponse = await middleware(_toShelfRequest(req)); 32 | res = _fromShelfResponse((req: req, res: res), shelfResponse); 33 | 34 | /// TODO(codekeyz) find out how to end or let the request continue 35 | /// based off the shelf response 36 | next(res.end()); 37 | }; 38 | } 39 | 40 | throw PharaohException.value('Unknown Shelf Middleware Type', middleware); 41 | } 42 | 43 | shelf.Request _toShelfRequest(Request req) { 44 | final httpReq = req.actual; 45 | 46 | var headers = >{}; 47 | httpReq.headers.forEach((k, v) { 48 | headers[k] = v; 49 | }); 50 | 51 | return shelf.Request( 52 | httpReq.method, 53 | httpReq.requestedUri, 54 | protocolVersion: httpReq.protocolVersion, 55 | headers: headers, 56 | body: httpReq, 57 | context: {'shelf.io.connection_info': httpReq.connectionInfo!}, 58 | ); 59 | } 60 | 61 | Response _fromShelfResponse(ReqRes reqRes, shelf.Response response) { 62 | Map headers = reqRes.res.headers; 63 | response.headers.forEach((key, value) => headers[key] = value); 64 | 65 | return Response.create( 66 | body: response.read(), 67 | encoding: response.encoding, 68 | headers: headers, 69 | statusCode: response.statusCode, 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/shelf_interop/shelf.dart: -------------------------------------------------------------------------------- 1 | import 'package:shelf/src/body.dart'; 2 | export 'package:shelf/src/request.dart'; 3 | export 'package:shelf/src/response.dart'; 4 | export 'package:shelf/src/middleware.dart'; 5 | 6 | typedef ShelfBody = Body; 7 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/utils/exceptions.dart: -------------------------------------------------------------------------------- 1 | import 'package:spanner/spanner.dart'; 2 | 3 | class PharaohException extends Error { 4 | /// Whether value was provided. 5 | final bool _hasValue; 6 | 7 | /// The invalid value. 8 | final dynamic invalidValue; 9 | 10 | /// Message describing the problem. 11 | final dynamic message; 12 | 13 | @pragma("vm:entry-point") 14 | PharaohException(this.message) 15 | : invalidValue = null, 16 | _hasValue = false; 17 | 18 | @pragma("vm:entry-point") 19 | PharaohException.value(this.message, [value]) 20 | : invalidValue = value, 21 | _hasValue = true; 22 | 23 | String get _errorName => "$parentName${!_hasValue ? "(s)" : ""}"; 24 | 25 | String get parentName => "Pharaoh Error"; 26 | 27 | @override 28 | String toString() { 29 | Object? message = this.message; 30 | var messageString = (message == null) ? "" : ": $message"; 31 | String prefix = "$_errorName$messageString"; 32 | if (!_hasValue) return prefix; 33 | // If we know the invalid value, we can try to describe the problem. 34 | String errorValue = Error.safeToString(invalidValue); 35 | return "$prefix ---> $errorValue"; 36 | } 37 | } 38 | 39 | class PharaohErrorBody { 40 | final String path; 41 | final HTTPMethod method; 42 | final String message; 43 | 44 | const PharaohErrorBody( 45 | this.message, 46 | this.path, { 47 | required this.method, 48 | }); 49 | 50 | Map toJson() => { 51 | "path": path, 52 | "method": method.name, 53 | "message": message, 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/utils/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | import 'dart:convert'; 4 | import 'package:crypto/crypto.dart'; 5 | 6 | String contentTypeToString(ContentType type, {String charset = 'utf-8'}) { 7 | return '${type.value}; charset=${type.charset ?? charset}'; 8 | } 9 | 10 | /// Run [callback] and capture any errors that would otherwise be top-leveled. 11 | /// 12 | /// If `this` is called in a non-root error zone, it will just run [callback] 13 | /// and return the result. Otherwise, it will capture any errors using 14 | /// [runZoned] and pass them to [onError]. 15 | void catchTopLevelErrors(void Function() callback, 16 | void Function(dynamic error, StackTrace) onError) { 17 | if (Zone.current.inSameErrorZone(Zone.root)) { 18 | return runZonedGuarded(callback, onError); 19 | } else { 20 | return callback(); 21 | } 22 | } 23 | 24 | bool safeCompare(String a, String b) { 25 | if (a.length != b.length) return false; 26 | var result = 0; 27 | for (var i = 0; i < a.length; i++) { 28 | result |= a.codeUnitAt(i) ^ b.codeUnitAt(i); 29 | } 30 | return result == 0; 31 | } 32 | 33 | /// Sign the given [value] with [secret]. 34 | String signValue(String value, String secret) { 35 | final hmac = Hmac(sha256, utf8.encode(secret)); 36 | final bytes = utf8.encode(value); 37 | final digest = hmac.convert(bytes); 38 | return '$value.${base64.encode(digest.bytes).replaceAll(RegExp('=+\$'), '')}'; 39 | } 40 | 41 | /// Unsign and decode the given [input] with [secret], 42 | /// returning `null` if the signature is invalid. 43 | String? unsignValue(String input, String secret) { 44 | var tentativeValue = input.substring(0, input.lastIndexOf('.')); 45 | var expectedInput = signValue(tentativeValue, secret); 46 | final valid = safeCompare(expectedInput, input); 47 | return valid ? tentativeValue : null; 48 | } 49 | 50 | String hashData(dynamic sess) { 51 | if (sess is! String) sess = jsonEncode(sess); 52 | return sha1.convert(utf8.encode(sess)).toString(); 53 | } 54 | -------------------------------------------------------------------------------- /packages/pharaoh/lib/src/view/view.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:isolate'; 3 | 4 | import '../http/router.dart'; 5 | import '../utils/exceptions.dart'; 6 | import '../shelf_interop/shelf.dart' as shelf; 7 | 8 | abstract class ViewEngine { 9 | String get name; 10 | 11 | FutureOr render(String template, Map data); 12 | } 13 | 14 | class ViewRenderData { 15 | final String name; 16 | final Map data; 17 | const ViewRenderData(this.name, this.data); 18 | } 19 | 20 | final viewRenderHook = RequestHook( 21 | onAfter: (req, res) async { 22 | final viewData = res.viewToRender; 23 | final reqRes = (req: req, res: res); 24 | if (viewData == null) return reqRes; 25 | 26 | final viewEngine = Pharaoh.viewEngine; 27 | if (viewEngine == null) throw PharaohException('No view engine found'); 28 | 29 | try { 30 | final result = await Isolate.run( 31 | () => viewEngine.render(viewData.name, viewData.data), 32 | ); 33 | res = res.end()..body = shelf.ShelfBody(result); 34 | } catch (e) { 35 | throw PharaohException.value('Failed to render view ${viewData.name}', e); 36 | } 37 | 38 | return reqRes.merge(res); 39 | }, 40 | ); 41 | -------------------------------------------------------------------------------- /packages/pharaoh/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: pharaoh 2 | description: Minimalist web-server library for Dart 3 | version: 0.0.8+3 4 | repository: https://github.com/codekeyz/pharaoh/tree/main/packages/pharaoh 5 | 6 | environment: 7 | sdk: ^3.0.0 8 | 9 | dependencies: 10 | meta: ^1.15.0 11 | spanner: ^1.0.5 12 | mime: ^1.0.4 13 | collection: ^1.18.0 14 | http_parser: ^4.0.2 15 | crypto: ^3.0.3 16 | uuid: ^4.2.1 17 | 18 | # We only need this to implement inter-operability for shelf 19 | shelf: ^1.4.1 20 | 21 | # framework 22 | reflectable: ^4.0.12 23 | get_it: ^8.0.2 24 | grammer: ^1.0.3 25 | dotenv: ^4.2.0 26 | ez_validator_dart: ^0.3.1 27 | spookie: ^1.0.2+3 28 | 29 | dev_dependencies: 30 | lints: ^3.0.0 31 | -------------------------------------------------------------------------------- /packages/pharaoh/test/acceptance/request_handling_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:pharaoh/pharaoh.dart'; 2 | import 'package:spookie/spookie.dart'; 3 | 4 | void main() { 5 | test('should execute request', () async { 6 | final app = Pharaoh() 7 | ..get('/users/', (req, res) => res.json(req.params)) 8 | ..post('/users/', (req, res) => res.json(req.params)) 9 | ..get('/home/chima', (req, res) => res.ok('Okay 🚀')) 10 | ..delete('/home/chima', (req, res) => res.ok('Item deleted')) 11 | ..post('/home/strange', (req, res) => res.ok('Post something 🚀')) 12 | ..get('/chima/', (req, res) => res.json(req.params)); 13 | 14 | final tester = await request(app); 15 | 16 | await tester 17 | .get('/home/chima') 18 | .expectStatus(200) 19 | .expectBody('Okay 🚀') 20 | .test(); 21 | 22 | await tester 23 | .post('/home/strange', {}) 24 | .expectStatus(200) 25 | .expectBody('Post something 🚀') 26 | .test(); 27 | 28 | await tester 29 | .get('/users/204') 30 | .expectStatus(200) 31 | .expectBody({'userId': '204'}).test(); 32 | 33 | await tester 34 | .post('/users/204398938948374797', {}) 35 | .expectStatus(200) 36 | .expectBody({'userId': '204398938948374797'}) 37 | .test(); 38 | 39 | await tester.get('/something-new-is-here').expectStatus(404).expectBody( 40 | {"error": "Route not found: /something-new-is-here"}).test(); 41 | 42 | await tester 43 | .delete('/home/chima') 44 | .expectStatus(200) 45 | .expectBody('Item deleted') 46 | .test(); 47 | 48 | await tester 49 | .get('/chima/asbc') 50 | .expectStatus(200) 51 | .expectJsonBody({'userId': 'asbc'}).test(); 52 | }); 53 | 54 | group('execute middleware and request', () { 55 | test('on base path /', () async { 56 | final app = Pharaoh() 57 | ..use((req, res, next) => next(req..setParams('foo', 'bar'))) 58 | ..get('/', 59 | (req, res) => res.json({...req.params, "name": 'Hello World'})); 60 | 61 | await (await request(app)) 62 | .get('/') 63 | .expectStatus(200) 64 | .expectBody({'foo': 'bar', 'name': 'Hello World'}).test(); 65 | }); 66 | 67 | test('of level 1', () async { 68 | final app = Pharaoh() 69 | ..use((req, res, next) => next(req..setParams('name', 'Chima'))) 70 | ..get( 71 | '/foo/bar', (req, res) => res.ok('Name: ${req.params['name']} 🚀')); 72 | 73 | await (await request(app)) 74 | .get('/foo/bar') 75 | .expectStatus(200) 76 | .expectBody('Name: Chima 🚀') 77 | .test(); 78 | }); 79 | 80 | test('of level 2', () async { 81 | final app = Pharaoh() 82 | ..use((req, res, next) => next(req..setParams('name', 'Chima'))) 83 | ..use((req, res, next) => next(req..setParams('age', '14'))) 84 | ..get('/foo/bar', (req, res) => res.json(req.params)); 85 | 86 | await (await request(app)) 87 | .get('/foo/bar') 88 | .expectStatus(200) 89 | .expectBody({'name': 'Chima', 'age': '14'}).test(); 90 | }); 91 | 92 | test('of level 3', () async { 93 | final app = Pharaoh() 94 | ..use((req, res, next) => next(req..setParams('points', '4000'))) 95 | ..use((req, res, next) => next(req..setParams('name', 'Chima'))) 96 | ..use((req, res, next) => next(req..setParams('age', '14'))) 97 | ..get('/foo/bar', (req, res) => res.json(req.params)); 98 | 99 | await (await request(app)) 100 | .get('/foo/bar') 101 | .expectStatus(200) 102 | .expectBody({'points': '4000', 'name': 'Chima', 'age': '14'}).test(); 103 | }); 104 | 105 | test('in right order', () async { 106 | final app = Pharaoh() 107 | ..use((req, res, next) => next(req..setParams('name', 'Chima'))) 108 | ..use((req, res, next) => next(req..setParams('points', '4000'))) 109 | ..use((req, res, next) => next(req..setParams('age', '14'))) 110 | ..get('/foo/bar', (req, res) => res.json(req.params)); 111 | 112 | await (await request(app)) 113 | .get('/foo/bar') 114 | .expectStatus(200) 115 | .expectBody({'name': 'Chima', 'points': '4000', 'age': '14'}).test(); 116 | }); 117 | 118 | test('if only request not ended', () async { 119 | final app = Pharaoh() 120 | ..use((req, res, next) => next(req..setParams('name', 'Chima'))) 121 | ..use((req, res, next) => next(res.ok('Say hello'))) 122 | ..get('/foo/bar', (req, res) => res.json(req.params)); 123 | 124 | await (await request(app)) 125 | .get('/foo/bar') 126 | .expectStatus(200) 127 | .expectBody('Say hello') 128 | .test(); 129 | }); 130 | 131 | test('should execute route groups', () async { 132 | final app = Pharaoh() 133 | ..get('/users/', (req, res) => res.json(req.params)); 134 | 135 | final router = Pharaoh.router 136 | ..use((req, res, next) => next(res.header('admin', '1'))) 137 | ..get('/', (req, res) => res.ok('Group working')) 138 | ..delete('/say-hello', (req, res) => res.ok('Hello World')); 139 | 140 | app.group('/api/v1', router); 141 | 142 | await (await request(app)) 143 | .get('/users/chima') 144 | .expectStatus(200) 145 | .expectHeader('admin', isNull) 146 | .expectBody({'userId': 'chima'}).test(); 147 | 148 | await (await request(app)) 149 | .get('/api/v1') 150 | .expectStatus(200) 151 | .expectHeader('admin', '1') 152 | .expectBody('Group working') 153 | .test(); 154 | 155 | await (await request(app)) 156 | .delete('/api/v1/say-hello') 157 | .expectStatus(200) 158 | .expectHeader('admin', '1') 159 | .expectBody('Hello World') 160 | .test(); 161 | }); 162 | }); 163 | } 164 | -------------------------------------------------------------------------------- /packages/pharaoh/test/core_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:pharaoh/pharaoh.dart'; 2 | import 'package:spookie/spookie.dart'; 3 | 4 | void main() { 5 | group('pharaoh_core', () { 6 | test('should initialize without onError callback', () async { 7 | final app = Pharaoh() 8 | ..get('/', (req, res) => throw ArgumentError('Some weird error')); 9 | 10 | await (await request(app)) 11 | .get('/') 12 | .expectStatus(500) 13 | .expectJsonBody(allOf( 14 | containsPair('error', 'Invalid argument(s): Some weird error'), 15 | contains('trace'), 16 | )) 17 | .test(); 18 | }); 19 | 20 | test('should use onError callback if provided', () async { 21 | final app = Pharaoh() 22 | ..use((req, res, next) => next(res.header('foo', 'bar'))) 23 | ..onError((_, req, res) => 24 | res.status(500).withBody('An error occurred just now')) 25 | ..get('/', (req, res) => throw ArgumentError('Some weird error')); 26 | 27 | await (await request(app)) 28 | .get('/') 29 | .expectStatus(500) 30 | .expectBody('An error occurred just now') 31 | .expectHeader('foo', 'bar') 32 | .test(); 33 | }); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /packages/pharaoh/test/http/req.query_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:pharaoh/pharaoh.dart'; 2 | import 'package:spookie/spookie.dart'; 3 | 4 | void main() { 5 | group('req.query && req.params', () { 6 | test('should pass a query', () async { 7 | final app = Pharaoh()..get('/', (req, res) => res.json(req.query)); 8 | 9 | await (await request(app)) 10 | .get('/?value1=1&value2=2') 11 | .expectStatus(200) 12 | .expectJsonBody({"value1": "1", "value2": "2"}).test(); 13 | }); 14 | }); 15 | 16 | test('should pass a param', () async { 17 | final app = Pharaoh() 18 | ..get('/', (req, res) => res.json(req.params)); 19 | 20 | await (await request(app)) 21 | .get('/heyOnuoha') 22 | .expectStatus(200) 23 | .expectJsonBody({"username": "heyOnuoha"}).test(); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /packages/pharaoh/test/http/res.cookie_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:pharaoh/pharaoh.dart'; 4 | import 'package:spookie/spookie.dart'; 5 | 6 | void main() { 7 | group('res.cookie(name, string)', () { 8 | test('should set a cookie', () async { 9 | final app = Pharaoh() 10 | ..get('/', (req, res) => res.cookie('name', 'chima').end()); 11 | 12 | await (await request(app)) 13 | .get('/') 14 | .expectStatus(200) 15 | .expectHeader(HttpHeaders.setCookieHeader, 'name=chima; Path=/') 16 | .test(); 17 | }); 18 | 19 | test('should allow multiple calls', () async { 20 | final app = Pharaoh() 21 | ..get( 22 | '/', 23 | (req, res) => res 24 | .cookie('name', 'chima') 25 | .cookie('age', '1') 26 | .cookie('gender', '?') 27 | .end()); 28 | 29 | await (await request(app)) 30 | .get('/') 31 | .expectStatus(200) 32 | .expectHeader(HttpHeaders.setCookieHeader, 33 | 'name=chima; Path=/,age=1; Path=/,gender=%3F; Path=/') 34 | .test(); 35 | }); 36 | }); 37 | 38 | group('res.cookie(name, string, {...options})', () { 39 | test('should set :httpOnly or :secure', () async { 40 | final app = Pharaoh() 41 | ..get('/', (req, res) { 42 | return res 43 | .cookie('name', 'chima', CookieOpts(httpOnly: true, secure: true)) 44 | .end(); 45 | }); 46 | 47 | await (await request(app)) 48 | .get('/') 49 | .expectStatus(200) 50 | .expectHeader(HttpHeaders.setCookieHeader, 51 | 'name=chima; Path=/; Secure; HttpOnly') 52 | .test(); 53 | }); 54 | 55 | test('should set :maxAge', () async { 56 | final app = Pharaoh() 57 | ..get('/', (req, res) { 58 | return res 59 | .cookie('name', 'chima', 60 | CookieOpts(maxAge: const Duration(seconds: 5))) 61 | .end(); 62 | }); 63 | 64 | await (await request(app)) 65 | .get('/') 66 | .expectStatus(200) 67 | .expectHeader(HttpHeaders.setCookieHeader, contains('Max-Age=5;')) 68 | .expectHeader(HttpHeaders.setCookieHeader, contains('Expires=')) 69 | .test(); 70 | }); 71 | 72 | test('should set :signed', () async { 73 | final app = Pharaoh() 74 | ..get('/', (req, res) { 75 | return res 76 | .cookie('user', {"name": 'tobi'}, 77 | CookieOpts(signed: true, secret: 'foo bar baz')) 78 | .end(); 79 | }); 80 | 81 | await (await request(app)) 82 | .get('/') 83 | .expectStatus(200) 84 | .expectHeader(HttpHeaders.setCookieHeader, 85 | 'user=s%3Aj%3A%7B%22name%22%3A%22tobi%22%7D.K20xcwmDS%2BPb1rsD95o5Jm5SqWs1KteqdnynnB7jkTE; Path=/') 86 | .test(); 87 | }); 88 | 89 | test('should reject when :signed without :secret', () async { 90 | final app = Pharaoh() 91 | ..get( 92 | '/', 93 | (req, res) => res 94 | .cookie('user', {"name": 'tobi'}, CookieOpts(signed: true)) 95 | .end()); 96 | 97 | await (await request(app)) 98 | .get('/') 99 | .expectStatus(500) 100 | .expectBody( 101 | contains('CookieOpts(\\"secret\\") required for signed cookies')) 102 | .test(); 103 | }); 104 | }); 105 | } 106 | -------------------------------------------------------------------------------- /packages/pharaoh/test/http/res.format_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:pharaoh/pharaoh.dart'; 4 | import 'package:spookie/spookie.dart'; 5 | 6 | void main() { 7 | group('res.format(Map options)', () { 8 | test('should respond using :accept provided', () async { 9 | final app = Pharaoh() 10 | ..get('/', (req, res) { 11 | return res.format(req, { 12 | 'text/plain; charset=utf-8': (res) => res.ok('Hello World'), 13 | 'text/html; charset=utf-8': (res) => 14 | res.type(ContentType.html).send('

Hello World

'), 15 | }); 16 | }); 17 | 18 | await (await request(app)) 19 | .get( 20 | '/', 21 | headers: {HttpHeaders.acceptHeader: ContentType.text.toString()}, 22 | ) 23 | .expectStatus(200) 24 | .expectContentType('text/plain; charset=utf-8') 25 | .expectBody('Hello World') 26 | .test(); 27 | 28 | await (await request(app)) 29 | .get( 30 | '/', 31 | headers: {HttpHeaders.acceptHeader: ContentType.html.toString()}, 32 | ) 33 | .expectStatus(200) 34 | .expectContentType('text/html; charset=utf-8') 35 | .expectBody('

Hello World

') 36 | .test(); 37 | }); 38 | 39 | test('should respond using default when :accept not provided', () async { 40 | final app = Pharaoh() 41 | ..get( 42 | '/', 43 | (req, res) => res.format(req, { 44 | ContentType.text.toString(): (res) => res.ok('Hello World'), 45 | ContentType.html.toString(): (res) => 46 | res.send('

Hello World

'), 47 | '_': (res) => res.json({'message': 'Hello World'}) 48 | }), 49 | ); 50 | 51 | await (await request(app)) 52 | .get('/') 53 | .expectStatus(200) 54 | .expectContentType('application/json; charset=utf-8') 55 | .expectBody('{"message":"Hello World"}') 56 | .test(); 57 | }); 58 | 59 | test('should send error when :accept not supported', () async { 60 | final app = Pharaoh() 61 | ..get( 62 | '/', 63 | (req, res) => res.format(req, { 64 | ContentType.text.toString(): (res) => res.ok('Hello World'), 65 | ContentType.html.toString(): (res) => 66 | res.send('

Hello World

'), 67 | }), 68 | ); 69 | 70 | await (await request(app)) 71 | .get( 72 | '/', 73 | headers: {HttpHeaders.acceptHeader: ContentType.binary.toString()}, 74 | ) 75 | .expectStatus(406) 76 | .expectContentType('application/json; charset=utf-8') 77 | .expectBody({"error": "Not Acceptable"}) 78 | .test(); 79 | }); 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /packages/pharaoh/test/http/res.json_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:pharaoh/pharaoh.dart'; 4 | import 'package:spookie/spookie.dart'; 5 | 6 | void main() { 7 | group('.json(Object)', () { 8 | test('should not override previous Content-Types', () async { 9 | final app = Pharaoh() 10 | ..get('/', (req, res) { 11 | return res 12 | .type(ContentType.parse('application/vnd.example+json')) 13 | .json({"hello": "world"}); 14 | }); 15 | 16 | await (await request(app)) 17 | .get('/') 18 | .expectStatus(200) 19 | .expectContentType('application/vnd.example+json') 20 | .expectBody('{"hello":"world"}') 21 | .test(); 22 | }); 23 | 24 | test('should catch object serialization errors', () async { 25 | final app = Pharaoh()..get('/', (req, res) => res.json(Never)); 26 | 27 | await (await request(app)) 28 | .get('/') 29 | .expectStatus(500) 30 | .expectBody({ 31 | 'error': "Converting object to an encodable object failed: Never" 32 | }) 33 | .expectContentType('application/json; charset=utf-8') 34 | .test(); 35 | }); 36 | 37 | group('when given primitives', () { 38 | test('should respond with json for ', () async { 39 | final app = Pharaoh()..use((req, res, next) => next(res.json(null))); 40 | 41 | await (await request(app)) 42 | .get('/') 43 | .expectStatus(200) 44 | .expectBody('null') 45 | .expectContentType('application/json; charset=utf-8') 46 | .test(); 47 | }); 48 | 49 | test('should respond with json for ', () async { 50 | final app = Pharaoh()..use((req, res, next) => next(res.json(300))); 51 | 52 | await (await request(app)) 53 | .get('/') 54 | .expectStatus(200) 55 | .expectBody('300') 56 | .expectContentType('application/json; charset=utf-8') 57 | .test(); 58 | }); 59 | 60 | test('should respond with json for ', () async { 61 | final app = Pharaoh()..use((req, res, next) => next(res.json(300.34))); 62 | 63 | await (await request(app)) 64 | .get('/') 65 | .expectStatus(200) 66 | .expectBody('300.34') 67 | .expectContentType('application/json; charset=utf-8') 68 | .test(); 69 | }); 70 | 71 | test('should respond with json for ', () async { 72 | final app = Pharaoh()..use((req, res, next) => next(res.json("str"))); 73 | 74 | await (await request(app)) 75 | .get('/') 76 | .expectStatus(200) 77 | .expectBody('"str"') 78 | .expectContentType('application/json; charset=utf-8') 79 | .test(); 80 | }); 81 | 82 | test('should respond with json for ', () async { 83 | final app = Pharaoh()..use((req, res, next) => next(res.json(true))); 84 | 85 | await (await request(app)) 86 | .get('/') 87 | .expectStatus(200) 88 | .expectBody('true') 89 | .expectContentType('application/json; charset=utf-8') 90 | .test(); 91 | }); 92 | }); 93 | 94 | group('when given a collection type', () { 95 | test(' should respond with json', () async { 96 | final app = Pharaoh() 97 | ..use((req, res, next) => next(res.json(["foo", "bar", "baz"]))); 98 | 99 | await (await request(app)) 100 | .get('/') 101 | .expectStatus(200) 102 | .expectBody('["foo","bar","baz"]') 103 | .expectContentType('application/json; charset=utf-8') 104 | .test(); 105 | }); 106 | 107 | test(' should respond with json', () async { 108 | final app = Pharaoh() 109 | ..use((req, res, next) => 110 | next(res.json({"name": "Foo bar", "age": 23.45}))); 111 | 112 | await (await request(app)) 113 | .get('/') 114 | .expectStatus(200) 115 | .expectBody('{"name":"Foo bar","age":23.45}') 116 | .expectContentType('application/json; charset=utf-8') 117 | .test(); 118 | }); 119 | 120 | test(' should respond with json', () async { 121 | final app = Pharaoh() 122 | ..use((req, res, next) => next(res.json({"Chima", "Foo", "Bar"}))); 123 | 124 | await (await request(app)) 125 | .get('/') 126 | .expectStatus(200) 127 | .expectBody('["Chima","Foo","Bar"]') 128 | .expectContentType('application/json; charset=utf-8') 129 | .test(); 130 | }); 131 | }); 132 | }); 133 | } 134 | -------------------------------------------------------------------------------- /packages/pharaoh/test/http/res.redirect_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:pharaoh/pharaoh.dart'; 4 | import 'package:spookie/spookie.dart'; 5 | 6 | void main() { 7 | group('res.redirect', () { 8 | test('should redirect to paths on the api', () async { 9 | final app = Pharaoh() 10 | ..get('/bar', (_, res) => res.ok('Finally here!')) 11 | ..get('/foo', (_, res) => res.redirect('/bar', 301)); 12 | 13 | await (await request(app)) 14 | .get('/foo') 15 | .expectStatus(HttpStatus.ok) 16 | .expectBody("Finally here!") 17 | .test(); 18 | }); 19 | 20 | test('should redirect to remote paths', () async { 21 | final app = Pharaoh() 22 | ..get('/foo', (_, res) => res.redirect('https://example.com', 301)); 23 | 24 | await (await request(app)) 25 | .get('/foo') 26 | .expectStatus(HttpStatus.ok) 27 | .expectBody(contains('Example Domain')) 28 | .test(); 29 | }); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /packages/pharaoh/test/http/res.render_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:pharaoh/pharaoh.dart'; 4 | import 'package:spookie/spookie.dart'; 5 | 6 | class TestViewEngine extends ViewEngine { 7 | final knownTemplates = ['welcome']; 8 | 9 | @override 10 | String get name => 'FoobarViewEngine'; 11 | 12 | @override 13 | FutureOr render(String template, Map data) { 14 | if (!knownTemplates.contains(template)) { 15 | throw Exception('Not found'); 16 | } 17 | 18 | return data.isEmpty 19 | ? 'Hello World' 20 | : data.entries.map((e) => '${e.key}:${e.value}').join('\n'); 21 | } 22 | } 23 | 24 | void main() { 25 | late Pharaoh app; 26 | 27 | setUp(() { 28 | Pharaoh.viewEngine = TestViewEngine(); 29 | app = Pharaoh(); 30 | }); 31 | 32 | group('res.render', () { 33 | test('should render template', () async { 34 | app = app..get('/', (req, res) => res.render('welcome')); 35 | 36 | await (await request(app)) 37 | .get('/') 38 | .expectBody('Hello World') 39 | .expectStatus(200) 40 | .expectContentType('text/html; charset=utf-8') 41 | .test(); 42 | }); 43 | 44 | test('should render template with variables', () async { 45 | app = app 46 | ..get( 47 | '/', 48 | (req, res) => res.render('welcome', {'username': 'Spookie'}), 49 | ); 50 | 51 | await (await request(app)) 52 | .get('/') 53 | .expectStatus(200) 54 | .expectBody('username:Spookie') 55 | .expectContentType('text/html; charset=utf-8') 56 | .test(); 57 | }); 58 | 59 | test('should err when template not found', () async { 60 | app = app..get('/', (req, res) => res.render('products')); 61 | 62 | await (await request(app)) 63 | .get('/') 64 | .expectStatus(500) 65 | .expectContentType('application/json; charset=utf-8') 66 | .expectJsonBody(containsPair( 67 | 'error', 68 | "Pharaoh Error: Failed to render view products ---> Instance of \'_Exception\'", 69 | )) 70 | .test(); 71 | }); 72 | 73 | test('should err when no view engine', () async { 74 | Pharaoh.viewEngine = null; 75 | app = app..get('/', (req, res) => res.render('products')); 76 | 77 | await (await request(app)) 78 | .get('/') 79 | .expectStatus(500) 80 | .expectJsonBody(containsPair( 81 | 'error', 82 | 'Pharaoh Error(s): No view engine found', 83 | )) 84 | .test(); 85 | }); 86 | }); 87 | } 88 | -------------------------------------------------------------------------------- /packages/pharaoh/test/http/res.send_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'dart:typed_data'; 4 | 5 | import 'package:pharaoh/pharaoh.dart'; 6 | import 'package:spookie/spookie.dart'; 7 | 8 | void main() { 9 | group('.send(Object)', () { 10 | test('should default content-Type to octet-stream', () async { 11 | final app = Pharaoh(); 12 | 13 | app.use((req, res, next) { 14 | final buffer = Uint8List.fromList(utf8.encode("Hello World")); 15 | next(res.send(buffer)); 16 | }); 17 | 18 | await (await request(app)) 19 | .get('/') 20 | .expectStatus(200) 21 | .expectBody('Hello World') 22 | .expectContentType('application/octet-stream') 23 | .test(); 24 | }); 25 | 26 | test('should not override previous Content-Types', () async { 27 | final app = Pharaoh() 28 | ..get('/html', (req, res) { 29 | return res.type(ContentType.html).send("

Hey

"); 30 | }) 31 | ..get('/text', (req, res) { 32 | return res.type(ContentType.text).send("Hey"); 33 | }); 34 | 35 | final tester = await request(app); 36 | 37 | await tester 38 | .get('/html') 39 | .expectContentType('text/html; charset=utf-8') 40 | .test(); 41 | 42 | await tester 43 | .get('/text') 44 | .expectContentType('text/plain; charset=utf-8') 45 | .test(); 46 | }); 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /packages/pharaoh/test/http/res.set_text.dart: -------------------------------------------------------------------------------- 1 | import 'package:pharaoh/pharaoh.dart'; 2 | import 'package:spookie/spookie.dart'; 3 | 4 | void main() { 5 | group('res.header(String headerKey, String headerValue)', () { 6 | test('should set the response header field', () async { 7 | final app = Pharaoh() 8 | ..use((req, res, next) { 9 | res = res.header("content-type", 'text/x-foo; charset=utf-8').end(); 10 | next(res); 11 | }); 12 | 13 | await (await request(app)) 14 | .get('/') 15 | .expectContentType('text/x-foo; charset=utf-8') 16 | .expectStatus(200) 17 | .test(); 18 | }); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /packages/pharaoh/test/http/res.status_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:pharaoh/pharaoh.dart'; 2 | import 'package:spookie/spookie.dart'; 3 | 4 | void main() { 5 | group('res.status(code)', () { 6 | group('should set the response status code', () { 7 | test('when "code" is 201 to 201', () async { 8 | final app = Pharaoh() 9 | ..use( 10 | (req, res, next) => next(res.status(201).end()), 11 | ); 12 | 13 | await (await request(app)).get('/').expectStatus(201).test(); 14 | }); 15 | 16 | test('when "code" is 400 to 400', () async { 17 | final app = Pharaoh() 18 | ..use((req, res, next) { 19 | res = res.status(400).end(); 20 | next(res); 21 | }); 22 | 23 | await (await request(app)).get('/').expectStatus(400).test(); 24 | }); 25 | 26 | test('when "code" is 500 to 500', () async { 27 | final app = Pharaoh() 28 | ..use((req, res, next) { 29 | res = res.status(500).end(); 30 | next(res); 31 | }); 32 | 33 | await (await request(app)).get('/').expectStatus(500).test(); 34 | }); 35 | }); 36 | 37 | group("should throw error", () { 38 | test('when "code" is 302 without location', () async { 39 | final app = Pharaoh() 40 | ..use((req, res, next) { 41 | res = res.status(302).end(); 42 | next(res); 43 | }); 44 | 45 | try { 46 | await (await request(app)).get('/').test(); 47 | } catch (e) { 48 | expect( 49 | (e as dynamic).message, 50 | 'Server response has no Location header for redirect', 51 | ); 52 | } 53 | }); 54 | }); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /packages/pharaoh/test/http/res.type_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:pharaoh/pharaoh.dart'; 4 | import 'package:spookie/spookie.dart'; 5 | 6 | void main() { 7 | group('res.type(ContentType)', () { 8 | test('should set the Content-Type with type/subtype', () async { 9 | final app = Pharaoh() 10 | ..get('/', (req, res) { 11 | final cType = 12 | ContentType('application', 'vnd.amazon.ebook', charset: 'utf-8'); 13 | 14 | return res.type(cType).send('var name = "tj";'); 15 | }); 16 | 17 | await (await request(app)) 18 | .get('/') 19 | .expectStatus(200) 20 | .expectBody('var name = "tj";') 21 | .expectContentType('application/vnd.amazon.ebook; charset=utf-8') 22 | .test(); 23 | }); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /packages/pharaoh/test/http/session_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:pharaoh/pharaoh.dart'; 2 | import 'package:spookie/spookie.dart'; 3 | 4 | void main() { 5 | group('session', () { 6 | group('InMemoryStore', () { 7 | late SessionStore store; 8 | final sessionId = 'some-session-id'; 9 | late Session session; 10 | 11 | setUpAll(() { 12 | store = InMemoryStore(); 13 | session = Session(sessionId); 14 | }); 15 | 16 | test('should have empty sessions when initialized', () async { 17 | final sessions = await store.sessions; 18 | expect(sessions, isEmpty); 19 | }); 20 | 21 | test('should store session value', () async { 22 | await store.set(sessionId, session); 23 | expect(store.sessions, hasLength(1)); 24 | expect(store.sessions, [session]); 25 | }); 26 | 27 | test('should return stored session', () async { 28 | var result = await store.get(sessionId); 29 | expect(result, session); 30 | 31 | result = await store.get('non-existent-sessionId'); 32 | expect(result, isNull); 33 | }); 34 | 35 | test('should destroy session', () async { 36 | await store.destroy(sessionId); 37 | final sessions = await store.sessions; 38 | expect(sessions, isEmpty); 39 | }); 40 | 41 | test('should clear sessions', () async { 42 | await store.clear(); 43 | final sessions = await store.sessions; 44 | expect(sessions, isEmpty); 45 | }); 46 | 47 | test('should have .modified :true if session data modified', () async { 48 | final session = Session('some-new-session'); 49 | expect(session.modified, false); 50 | 51 | session['name'] = 'Chima'; 52 | session['tag'] = '@codekeyz'; 53 | session['dogs'] = 2; 54 | 55 | expect(session.modified, true); 56 | }); 57 | }); 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /packages/pharaoh/test/issue_route_not_found_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:pharaoh/pharaoh.dart'; 2 | import 'package:spookie/spookie.dart'; 3 | 4 | void main() { 5 | test('should error on route not found', () async { 6 | final app = Pharaoh()..get('/', (req, res) => res.ok('Hello')); 7 | 8 | final tester = await request(app); 9 | 10 | await tester.get('/').expectStatus(200).expectBody('Hello').test(); 11 | 12 | await tester 13 | .get('/come') 14 | .expectStatus(404) 15 | .expectJsonBody({"error": "Route not found: /come"}).test(); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /packages/pharaoh/test/middleware/body_parser_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:pharaoh/pharaoh.dart'; 2 | import 'package:spookie/spookie.dart'; 3 | 4 | void main() { 5 | group('body_parser', () { 6 | group('should parse request body ', () { 7 | test('when content-type not specified', () async { 8 | final app = Pharaoh()..post('/', (req, res) => res.json(req.body)); 9 | 10 | await (await request(app)) 11 | .post('/', {'name': 'Chima', 'age': '24'}) 12 | .expectStatus(200) 13 | .expectBody({'name': 'Chima', 'age': '24'}) 14 | .test(); 15 | }); 16 | 17 | test('when content-type is `application/json`', () async { 18 | final app = Pharaoh()..post('/', (req, res) => res.json(req.body)); 19 | 20 | await (await request(app)) 21 | .post('/', '{"name":"Chima","age":24}', 22 | headers: {'Content-Type': 'application/json'}) 23 | .expectStatus(200) 24 | .expectBody({'name': 'Chima', 'age': 24}) 25 | .test(); 26 | }); 27 | 28 | test('when content-type is `application/x-www-form-urlencoded`', 29 | () async { 30 | final app = Pharaoh()..post('/', (req, res) => res.json(req.body)); 31 | 32 | await (await request(app)) 33 | .post('/', 'name%3DChima%26age%3D24', 34 | headers: {'Content-Type': 'application/x-www-form-urlencoded'}) 35 | .expectStatus(200) 36 | .expectBody({'name': 'Chima', 'age': '24'}) 37 | .test(); 38 | }); 39 | }); 40 | 41 | group('should not parse request body', () { 42 | test('when request body is null', () async { 43 | final app = Pharaoh()..post('/', (req, res) => res.json(req.body)); 44 | 45 | await (await request(app)) 46 | .post('/', null) 47 | .expectStatus(200) 48 | .expectBody('null') 49 | .test(); 50 | }); 51 | 52 | test('when request body is empty', () async { 53 | final app = Pharaoh()..post('/', (req, res) => res.json(req.body)); 54 | 55 | await (await request(app)) 56 | .post('/', '') 57 | .expectStatus(200) 58 | .expectBody('null') 59 | .test(); 60 | }); 61 | }); 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /packages/pharaoh/test/middleware/cookie_parser_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:pharaoh/pharaoh.dart'; 4 | import 'package:spookie/spookie.dart'; 5 | 6 | void main() { 7 | group('cookieParser', () { 8 | group('when cookies are sent', () { 9 | test('should populate req.cookies', () async { 10 | final app = Pharaoh() 11 | ..use(cookieParser()) 12 | ..get('/', (req, res) { 13 | final str = req.cookies.toString(); 14 | return res.ok(str); 15 | }); 16 | 17 | await (await request(app)) 18 | .get('/', headers: {HttpHeaders.cookieHeader: 'foo=bar; bar=baz'}) 19 | .expectStatus(200) 20 | .expectBody('[foo=bar; HttpOnly, bar=baz; HttpOnly]') 21 | .test(); 22 | }); 23 | 24 | test('should populate req.signedCookies', () async { 25 | const opts = CookieOpts(secret: 'foo bar baz', signed: true); 26 | final app = Pharaoh() 27 | ..use(cookieParser(opts: opts)) 28 | ..get('/', (req, res) { 29 | final str = req.signedCookies.toString(); 30 | return res.ok(str); 31 | }); 32 | 33 | await (await request(app)) 34 | .get('/', headers: { 35 | HttpHeaders.cookieHeader: 36 | 'name=s%3Achima.4ytL9j25i8e59B6eCUUZdrWHdGLK3Cua%2BG1oGyurzXY' 37 | }) 38 | .expectStatus(200) 39 | .expectBody('[name=chima; HttpOnly]') 40 | .test(); 41 | }); 42 | 43 | test('should remove tampered signed cookies', () async { 44 | const opts = CookieOpts(secret: 'foo bar baz', signed: true); 45 | final app = Pharaoh() 46 | ..use(cookieParser(opts: opts)) 47 | ..get('/', (req, res) { 48 | final str = req.signedCookies.toString(); 49 | return res.ok(str); 50 | }); 51 | 52 | await (await request(app)) 53 | .get('/', headers: { 54 | HttpHeaders.cookieHeader: 55 | 'name=s%3Achimaxyz.4ytL9j25i8e59B6eCUUZdrWHdGLK3Cua%2BG1oGyurzXY; Path=/' 56 | }) 57 | .expectStatus(200) 58 | .expectBody('[]') 59 | .test(); 60 | }); 61 | 62 | test('should leave unsigned cookies as they are', () async { 63 | const opts = CookieOpts(secret: 'foo bar baz', signed: true); 64 | final app = Pharaoh() 65 | ..use(cookieParser(opts: opts)) 66 | ..get('/', (req, res) { 67 | final str = req.cookies.toString(); 68 | return res.ok(str); 69 | }); 70 | 71 | await (await request(app)) 72 | .get('/', headers: {HttpHeaders.cookieHeader: 'name=chima; Path=/'}) 73 | .expectStatus(200) 74 | .expectBody('[name=chima; HttpOnly, Path=/; HttpOnly]') 75 | .test(); 76 | }); 77 | }); 78 | 79 | group('when no cookies are sent', () { 80 | test('should default req.cookies to []', () async { 81 | final app = Pharaoh() 82 | ..use(cookieParser()) 83 | ..get('/', (req, res) { 84 | final str = req.cookies.toString(); 85 | return res.ok(str); 86 | }); 87 | 88 | await (await request(app)) 89 | .get('/') 90 | .expectStatus(200) 91 | .expectBody('[]') 92 | .test(); 93 | }); 94 | 95 | test('should default req.signedCookies to []', () async { 96 | final app = Pharaoh() 97 | ..use(cookieParser()) 98 | ..get('/', (req, res) { 99 | final str = req.signedCookies.toString(); 100 | return res.ok(str); 101 | }); 102 | 103 | await (await request(app)) 104 | .get('/') 105 | .expectStatus(200) 106 | .expectBody('[]') 107 | .test(); 108 | }); 109 | }); 110 | 111 | group('when json-encoded value', () { 112 | test('should parse when signed', () async { 113 | final cookieOpts = CookieOpts(signed: true, secret: 'foo-bar-mee-moo'); 114 | final cookie = 115 | bakeCookie('user', {'foo': 'bar', 'mee': 'mee'}, cookieOpts); 116 | 117 | expect(cookie.toString(), 118 | 'user=s%3Aj%3A%7B%22foo%22%3A%22bar%22%2C%22mee%22%3A%22mee%22%7D.sxYOqZyRsCeSGNGzAR5UG3Hv%2BW%2BiXl9TQPlbbdBLMF0; Path=/'); 119 | 120 | expect(cookie.signed, isTrue); 121 | 122 | expect(cookie.jsonEncoded, isTrue); 123 | 124 | final app = Pharaoh() 125 | ..use(cookieParser(opts: cookieOpts)) 126 | ..get('/', (req, res) => res.json(req.signedCookies.first.actualObj)); 127 | 128 | await (await request(app)) 129 | .get('/', headers: {HttpHeaders.cookieHeader: cookie.toString()}) 130 | .expectStatus(200) 131 | .expectJsonBody({'foo': 'bar', 'mee': 'mee'}) 132 | .test(); 133 | }); 134 | 135 | test('should parse when un-signed', () async { 136 | final cookieOpts = CookieOpts(signed: false); 137 | final cookie = 138 | bakeCookie('user', {'foo': 'bar', 'mee': 'mee'}, cookieOpts); 139 | 140 | expect(cookie.toString(), 141 | 'user=j%3A%7B%22foo%22%3A%22bar%22%2C%22mee%22%3A%22mee%22%7D; Path=/'); 142 | 143 | expect(cookie.signed, isFalse); 144 | 145 | expect(cookie.jsonEncoded, isTrue); 146 | 147 | final app = Pharaoh() 148 | ..use(cookieParser(opts: cookieOpts)) 149 | ..get('/', (req, res) => res.json(req.cookies.first.actualObj)); 150 | 151 | await (await request(app)) 152 | .get('/', headers: {HttpHeaders.cookieHeader: cookie.toString()}) 153 | .expectStatus(200) 154 | .expectJsonBody({'foo': 'bar', 'mee': 'mee'}) 155 | .test(); 156 | }); 157 | }); 158 | }); 159 | } 160 | -------------------------------------------------------------------------------- /packages/pharaoh/test/pharaoh_next/config/config_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:pharaoh/pharaoh_next.dart'; 2 | import 'package:spookie/spookie.dart'; 3 | 4 | import '../core/core_test.dart'; 5 | import './config_test.reflectable.dart' as r; 6 | 7 | Matcher throwsArgumentErrorWithMessage(String message) => 8 | throwsA(isA().having((p0) => p0.message, '', message)); 9 | 10 | class AppServiceProvider extends ServiceProvider {} 11 | 12 | void main() { 13 | setUpAll(() => r.initializeReflectable()); 14 | 15 | group('App Config Test', () { 16 | test('should return AppConfig instance', () async { 17 | final testApp = TestKidsApp( 18 | middlewares: [TestMiddleware], providers: [AppServiceProvider]); 19 | expect(testApp, isNotNull); 20 | }); 21 | 22 | test('should use prioritize `port` over port in `url`', () { 23 | const config = AppConfig( 24 | name: 'Foo Bar', 25 | environment: 'debug', 26 | isDebug: true, 27 | key: 'asdfajkl', 28 | url: 'http://localhost:3000', 29 | port: 4000, 30 | ); 31 | 32 | expect(config.url, 'http://localhost:4000'); 33 | expect(config.port, 4000); 34 | }); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /packages/pharaoh/test/pharaoh_next/core/application_factory_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:pharaoh/pharaoh_next.dart'; 2 | import 'package:spookie/spookie.dart'; 3 | 4 | import 'application_factory_test.reflectable.dart'; 5 | 6 | class TestHttpController extends HTTPController { 7 | Future index() async { 8 | return response.ok('Hello World'); 9 | } 10 | 11 | Future show(@query int userId) async { 12 | return response.ok('User $userId'); 13 | } 14 | } 15 | 16 | void main() { 17 | initializeReflectable(); 18 | 19 | group('ApplicationFactory', () { 20 | group('.buildControllerMethod', () { 21 | group('should return request handler', () { 22 | test('for method with no args', () async { 23 | final indexMethod = ControllerMethod((TestHttpController, #index)); 24 | final handler = ApplicationFactory.buildControllerMethod(indexMethod); 25 | 26 | expect(handler, isA()); 27 | 28 | await (await request(Pharaoh()..get('/', handler))) 29 | .get('/') 30 | .expectStatus(200) 31 | .expectBody('Hello World') 32 | .test(); 33 | }); 34 | 35 | test('for method with args', () async { 36 | final showMethod = ControllerMethod( 37 | (TestHttpController, #show), 38 | [ControllerMethodParam('userId', int, meta: query)], 39 | ); 40 | 41 | final handler = ApplicationFactory.buildControllerMethod(showMethod); 42 | expect(handler, isA()); 43 | 44 | await (await request(Pharaoh()..get('/test', handler))) 45 | .get('/test?userId=2345') 46 | .expectStatus(200) 47 | .expectBody('User 2345') 48 | .test(); 49 | }); 50 | }); 51 | }); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /packages/pharaoh/test/pharaoh_next/core/core_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:pharaoh/pharaoh_next.dart'; 2 | import 'package:spookie/spookie.dart'; 3 | 4 | import '../config/config_test.dart'; 5 | import 'core_test.reflectable.dart'; 6 | 7 | const appConfig = AppConfig( 8 | name: 'Test App', 9 | environment: 'production', 10 | isDebug: false, 11 | url: 'http://localhost', 12 | port: 3000, 13 | key: 'askdfjal;ksdjkajl;j', 14 | ); 15 | 16 | class TestMiddleware extends ClassMiddleware {} 17 | 18 | class FoobarMiddleware extends ClassMiddleware { 19 | @override 20 | Middleware get handler => (req, res, next) => next(); 21 | } 22 | 23 | class TestKidsApp extends ApplicationFactory { 24 | final AppConfig? config; 25 | 26 | TestKidsApp({ 27 | this.providers = const [], 28 | this.middlewares = const [], 29 | this.config, 30 | }) : super(config ?? appConfig); 31 | 32 | @override 33 | final List providers; 34 | 35 | @override 36 | final List middlewares; 37 | 38 | @override 39 | Map> get middlewareGroups => { 40 | 'api': [FoobarMiddleware], 41 | 'web': [String] 42 | }; 43 | } 44 | 45 | void main() { 46 | initializeReflectable(); 47 | 48 | group('Core', () { 49 | final testApp = TestKidsApp(middlewares: [TestMiddleware]); 50 | 51 | group('should error', () { 52 | test('when invalid provider type passed', () { 53 | expect( 54 | () => 55 | TestKidsApp(middlewares: [TestMiddleware], providers: [String]), 56 | throwsArgumentErrorWithMessage( 57 | 'Ensure your class extends `ServiceProvider` class')); 58 | }); 59 | 60 | test('when invalid middleware type passed middlewares is not valid', () { 61 | expect( 62 | () => TestKidsApp( 63 | middlewares: [String], providers: [AppServiceProvider]), 64 | throwsArgumentErrorWithMessage( 65 | 'Ensure your class extends `ClassMiddleware` class')); 66 | }); 67 | }); 68 | 69 | test('should resolve global middleware', () { 70 | expect(testApp.globalMiddleware, isA()); 71 | }); 72 | 73 | group('when middleware group', () { 74 | test('should resolve', () { 75 | final group = Route.middleware('api').group('Users', [ 76 | Route.route(HTTPMethod.GET, '/', (req, res) => null), 77 | ]); 78 | 79 | expect(group.paths, ['[ALL]: /users', '[GET]: /users']); 80 | }); 81 | 82 | test('should error when not exist', () { 83 | expect( 84 | () => Route.middleware('foo').group('Users', [ 85 | Route.route(HTTPMethod.GET, '/', (req, res) => null), 86 | ]), 87 | throwsA( 88 | isA().having((p0) => p0.message, 'message', 89 | 'Middleware group `foo` does not exist'), 90 | ), 91 | ); 92 | }); 93 | }); 94 | 95 | test('should throw if type is not subtype of Middleware', () { 96 | final middlewares = ApplicationFactory.resolveMiddlewareForGroup('api'); 97 | expect(middlewares, isA>()); 98 | 99 | expect(middlewares.length, 1); 100 | 101 | expect(() => ApplicationFactory.resolveMiddlewareForGroup('web'), 102 | throwsA(isA())); 103 | }); 104 | 105 | test('should return tester', () async { 106 | await testApp.bootstrap(listen: false); 107 | 108 | expect(await testApp.tester, isA()); 109 | }); 110 | }); 111 | } 112 | -------------------------------------------------------------------------------- /packages/pharaoh/test/router/handler_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:pharaoh/pharaoh.dart'; 2 | import 'package:spookie/spookie.dart'; 3 | 4 | void main() { 5 | group('route_handler', () { 6 | test('should deliver :req', () async { 7 | final app = Pharaoh() 8 | ..use((req, res, next) { 9 | req[RequestContext.auth] = 'some-token'; 10 | return next(req); 11 | }) 12 | ..get('/', (req, res) => res.send(req.auth)); 13 | 14 | await (await request(app)) 15 | .get('/') 16 | .expectStatus(200) 17 | .expectBody('some-token') 18 | .test(); 19 | }); 20 | 21 | test('should deliver res', () async { 22 | final app = Pharaoh() 23 | ..use((req, res, next) { 24 | req[RequestContext.auth] = 'some-token'; 25 | return next(res.ok('Hello World')); 26 | }); 27 | 28 | await (await request(app)) 29 | .get('/') 30 | .expectStatus(200) 31 | .expectBody('Hello World') 32 | .test(); 33 | }); 34 | 35 | test('should deliver both :req and :res', () async { 36 | final app = Pharaoh() 37 | ..use((req, res, next) { 38 | req[RequestContext.auth] = 'World'; 39 | return next((req: req, res: res.cookie('name', 'tobi'))); 40 | }) 41 | ..get('/', (req, res) => res.ok('Hello ${req.auth}')); 42 | 43 | await (await request(app)) 44 | .get('/') 45 | .expectHeader('set-cookie', 'name=tobi; Path=/') 46 | .expectStatus(200) 47 | .expectBody('Hello World') 48 | .test(); 49 | }); 50 | 51 | test('should chain middlewares in the right order', () async { 52 | final listResultList = []; 53 | 54 | final Middleware mdw1 = (req, res, next) { 55 | listResultList.add(1); 56 | next(); 57 | }; 58 | 59 | final Middleware mdw2 = (req, res, next) { 60 | listResultList.add(2); 61 | next(); 62 | }; 63 | 64 | final Middleware mdw3 = (req, res, next) { 65 | listResultList.add(3); 66 | next(); 67 | }; 68 | 69 | Pharaoh getApp(Middleware chain) { 70 | final app = Pharaoh(); 71 | return app 72 | ..use(chain) 73 | ..get('/test', (req, res) => res.ok()); 74 | } 75 | 76 | final testChain1 = mdw1.chain(mdw2).chain(mdw3); 77 | await (await request(getApp(testChain1))).get('/test').test(); 78 | expect(listResultList, [1, 2, 3]); 79 | 80 | listResultList.clear(); 81 | 82 | final testChain2 = mdw2.chain(mdw1).chain(mdw3); 83 | await (await request(getApp(testChain2))).get('/test').test(); 84 | expect(listResultList, [2, 1, 3]); 85 | 86 | listResultList.clear(); 87 | 88 | final testChain3 = mdw3.chain(mdw1.chain(mdw3)).chain(mdw2.chain(mdw1)); 89 | await (await request(getApp(testChain3))).get('/test').test(); 90 | expect(listResultList, [3, 1, 3, 2, 1]); 91 | 92 | listResultList.clear(); 93 | 94 | final complexChain = testChain3.chain(testChain1).chain(testChain2); 95 | await (await request(getApp(complexChain))).get('/test').test(); 96 | expect(listResultList, [3, 1, 3, 2, 1, 1, 2, 3, 2, 1, 3]); 97 | 98 | listResultList.clear(); 99 | 100 | final shortLivedChain = testChain3 101 | .chain((req, res, next) => next(res.end())) 102 | .chain(testChain2); 103 | 104 | await (await request(getApp(shortLivedChain))).get('/test').test(); 105 | expect(listResultList, [3, 1, 3, 2, 1]); 106 | }); 107 | }); 108 | } 109 | -------------------------------------------------------------------------------- /packages/pharaoh/test/router/router_group_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:pharaoh/pharaoh.dart'; 2 | import 'package:spookie/spookie.dart'; 3 | 4 | void main() { 5 | group('router', () { 6 | test('should execute middlewares in group', () async { 7 | final app = Pharaoh()..post('/', (req, res) => res.json(req.body)); 8 | 9 | final adminRouter = Pharaoh.router 10 | ..get('/', (req, res) => res.ok('Holy Moly 🚀')) 11 | ..post('/hello', (req, res) => res.json(req.body)); 12 | app.group('/admin', adminRouter); 13 | 14 | final appTester = await request(app); 15 | 16 | await appTester 17 | .post('/', {'_': 'Hello World 🚀'}) 18 | .expectBody({"_": "Hello World 🚀"}) 19 | .expectStatus(200) 20 | .test(); 21 | 22 | await appTester 23 | .post('/admin/hello', {'_': 'Hello World 🚀'}) 24 | .expectBody({"_": "Hello World 🚀"}) 25 | .expectStatus(200) 26 | .test(); 27 | 28 | await appTester 29 | .get('/admin') 30 | .expectBody('Holy Moly 🚀') 31 | .expectStatus(200) 32 | .test(); 33 | }); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /packages/pharaoh_basic_auth/.gitignore: -------------------------------------------------------------------------------- 1 | # https://dart.dev/guides/libraries/private-files 2 | # Created by `dart pub` 3 | .dart_tool/ 4 | 5 | # Avoid committing pubspec.lock for library packages; see 6 | # https://dart.dev/guides/libraries/private-files#pubspeclock. 7 | pubspec.lock 8 | -------------------------------------------------------------------------------- /packages/pharaoh_basic_auth/.pubignore: -------------------------------------------------------------------------------- 1 | cliff.toml 2 | dart_test.yml 3 | pubspec_overrides.yaml -------------------------------------------------------------------------------- /packages/pharaoh_basic_auth/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | 3 | - Initial version. 4 | -------------------------------------------------------------------------------- /packages/pharaoh_basic_auth/LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2023 Chima Precious 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /packages/pharaoh_basic_auth/README.md: -------------------------------------------------------------------------------- 1 | # pharaoh_basic_auth 🏴 2 | 3 | Simple plug & play HTTP basic auth middleware for Pharaoh. 4 | 5 | ## Installing: 6 | 7 | In your pubspec.yaml 8 | 9 | ```yaml 10 | dependencies: 11 | pharaoh: ^0.0.5+6 12 | pharaoh_basic_auth: 13 | ``` 14 | 15 | ## Basic Usage: 16 | 17 | ```dart 18 | import 'package:pharaoh/pharaoh.dart'; 19 | import 'package:pharaoh_basic_auth/src/basic_auth.dart'; 20 | 21 | void main() async { 22 | final app = Pharaoh(); 23 | 24 | app.use(basicAuth(users: {"admin": "supersecret"})); 25 | } 26 | ``` 27 | 28 | The middleware will now check incoming requests to match the credentials 29 | `admin:supersecret`. 30 | 31 | The middleware will check incoming requests for a basic auth (`Authorization`) 32 | header, parse it and check if the credentials are legit. If there are any 33 | credentials, the `auth` property on the `request` will contain the `user` and `password` properties. 34 | 35 | **If a request is found to not be authorized**, it will respond with HTTP 401 36 | and a configurable body (default `Unauthorized`). 37 | 38 | ### Static Users 39 | 40 | If you simply want to check basic auth against one or multiple static credentials, 41 | you can pass those credentials in the `users` option: 42 | 43 | ```dart 44 | app.use(basicAuth( 45 | users: { 46 | "admin": "supersecret", 47 | "adam": "password1234", 48 | "eve": "asdfghjkl", 49 | }, 50 | )); 51 | ``` 52 | 53 | The middleware will check incoming requests to have a basic auth header matching 54 | one of the three passed credentials. 55 | 56 | ### Custom authorization 57 | 58 | Alternatively, you can pass your own `authorizer` function, to check the credentials 59 | however you want. It will be called with a username and password and is expected to 60 | return `true` or `false` to indicate that the credentials were approved or not. 61 | 62 | When using your own `authorizer`, make sure **not to use standard string comparison (`==`)** 63 | when comparing user input with secret credentials, as that would make you vulnerable against 64 | [timing attacks](https://en.wikipedia.org/wiki/Timing_attack). Use the provided `safeCompare` 65 | function instead - always provide the user input as its first argument. 66 | 67 | ```dart 68 | bool myAuthorizer(username, password) => 69 | safeCompare(username, 'customuser') && 70 | safeCompare(password, 'custompassword'); 71 | 72 | app.use(basicAuth(authorizer: myAuthorizer )); 73 | ``` 74 | 75 | This will authorize all requests with the credentials `customuser:custompassword`. 76 | In an actual application you would likely look up some data instead ;-) You can do whatever you 77 | want in custom authorizers, just return `true` or `false` in the end and stay aware of timing 78 | attacks. 79 | 80 | ## Tests 81 | 82 | The cases in the `basic_auth_test.dart` are also used for automated testing. So if you want 83 | to contribute or just make sure that the package still works, simply run: 84 | 85 | ```shell 86 | dart test 87 | ``` 88 | -------------------------------------------------------------------------------- /packages/pharaoh_basic_auth/example/pharaoh_basic_auth_example.dart: -------------------------------------------------------------------------------- 1 | import 'package:pharaoh/pharaoh.dart'; 2 | import 'package:pharaoh_basic_auth/src/basic_auth.dart'; 3 | 4 | void main() async { 5 | final app = Pharaoh(); 6 | 7 | app.use(basicAuth(users: {"foo": "foo-bar-pass"})); 8 | 9 | app.get('/', (req, res) => res.ok('Hurray 🔥')); 10 | 11 | await app.listen(); 12 | } 13 | -------------------------------------------------------------------------------- /packages/pharaoh_basic_auth/lib/pharaoh_basic_auth.dart: -------------------------------------------------------------------------------- 1 | library; 2 | 3 | export 'src/basic_auth.dart'; 4 | -------------------------------------------------------------------------------- /packages/pharaoh_basic_auth/lib/src/basic_auth.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | 5 | import 'package:pharaoh/pharaoh.dart'; 6 | 7 | typedef Authorizer = FutureOr Function(String username, String password); 8 | 9 | typedef UnAuthorizedResponse = String Function(Request req); 10 | 11 | typedef GetRealm = String Function(Request req); 12 | 13 | /// - [authorizer] You can pass your own [Authorizer] function, to check the credentials however you want. 14 | /// 15 | /// - [unauthorizedResponse] You can either pass a static response or a function that gets 16 | /// passed the pharaoh request object and is expected to return an error message [String]. 17 | /// 18 | /// - [users] If you simply want to check basic auth against one or multiple static credentials, 19 | /// you can pass those credentials in the users option: 20 | /// 21 | /// - [challenge] Per default the middleware will not add a WWW-Authenticate challenge header to responses of unauthorized requests. 22 | /// You can enable that by adding challenge: true to the options object. This will cause most browsers to show a popup to enter credentials 23 | /// on unauthorized responses. 24 | /// You can set the realm (the realm identifies the system to authenticate against and can be used by clients to save credentials) of the challenge 25 | /// by passing a static string or a function that gets passed the request object and is expected to return the challenge 26 | Middleware basicAuth({ 27 | final Authorizer? authorizer, 28 | final UnAuthorizedResponse? unauthorizedResponse, 29 | final Map? users, 30 | final bool challenge = false, 31 | final GetRealm? realm, 32 | }) { 33 | return (req, res, next) async { 34 | void bounceRequest() { 35 | if (challenge) { 36 | String challengeString = 'Basic'; 37 | var realmName = realm?.call(req); 38 | if (realmName != null) challengeString += ' realm="$realmName"'; 39 | res.header(HttpHeaders.wwwAuthenticateHeader, challengeString); 40 | } 41 | final errorMsg = unauthorizedResponse?.call(req); 42 | next(res.unauthorized(data: errorMsg)); 43 | } 44 | 45 | final authHeader = req.headers[HttpHeaders.authorizationHeader]; 46 | if (authHeader == null || authHeader is! Iterable || authHeader.isEmpty) { 47 | return bounceRequest(); 48 | } 49 | 50 | var authParts = (authHeader.last as String).split(' '); 51 | if (authParts[0].toLowerCase() != 'basic') return bounceRequest(); 52 | authParts = String.fromCharCodes(base64.decode(authParts.last)).split(':'); 53 | 54 | final username = authParts.first, userpass = authParts.last; 55 | final secret = users?[username]; 56 | 57 | final a = authorizer != null && await authorizer.call(username, userpass); 58 | final b = secret != null && safeCompare(userpass, secret); 59 | if (a || b) { 60 | req.auth = {"user": username, "pass": userpass}; 61 | return next(req); 62 | } 63 | 64 | bounceRequest(); 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /packages/pharaoh_basic_auth/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: pharaoh_basic_auth 2 | description: Simple plug & play HTTP basic authentication middleware for Pharaoh. 3 | version: 1.0.0+2 4 | repository: https://github.com/codekeyz/pharaoh/tree/main/packages/pharaoh_basic_auth 5 | 6 | environment: 7 | sdk: ^3.0.0 8 | 9 | dependencies: 10 | pharaoh: ^0.0.5+6 11 | 12 | dev_dependencies: 13 | spookie: 14 | -------------------------------------------------------------------------------- /packages/pharaoh_basic_auth/test/basic_auth_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:pharaoh/pharaoh.dart'; 2 | import 'package:pharaoh_basic_auth/src/basic_auth.dart'; 3 | import 'package:spookie/spookie.dart'; 4 | 5 | final app = Pharaoh().get('/', (req, res) => res); 6 | 7 | void main() { 8 | group('pharaoh_basic_auth', () { 9 | group('safe compare', () { 10 | test('should return false on different inputs', () { 11 | expect(safeCompare('asdf', 'rftghe'), false); 12 | }); 13 | 14 | test('should return false on prefix inputs', () { 15 | expect(safeCompare('some', 'something'), false); 16 | }); 17 | 18 | test('should return true on same inputs', () { 19 | expect(safeCompare('anothersecret', 'anothersecret'), true); 20 | }); 21 | }); 22 | 23 | group('static users', () { 24 | late Pharaoh app; 25 | const endpoint = '/static'; 26 | 27 | setUpAll(() { 28 | // requires basic auth with username 'Admin' and password 'secret1234' 29 | final staticUserAuth = basicAuth( 30 | users: {"Admin": "secret1234"}, 31 | challenge: false, 32 | unauthorizedResponse: (_) => 'Username & password is required!', 33 | ); 34 | app = Pharaoh() 35 | ..use(staticUserAuth) 36 | ..get(endpoint, (req, res) => res.send('You passed')); 37 | }); 38 | 39 | test( 40 | 'should reject on missing header', 41 | () async => (await request(app)) 42 | .get(endpoint) 43 | .expectStatus(401) 44 | .expectBody('"Username & password is required!"') 45 | .test(), 46 | ); 47 | 48 | test( 49 | 'should reject on wrong credentials', 50 | () async => (await request(app)) 51 | .auth('dude', 'stuff') 52 | .get(endpoint) 53 | .expectStatus(401) 54 | .test(), 55 | ); 56 | 57 | test( 58 | 'should reject on shorter prefix', 59 | () async => (await request(app)) 60 | .auth('Admin', 'secret') 61 | .get(endpoint) 62 | .expectStatus(401) 63 | .test(), 64 | ); 65 | 66 | test( 67 | 'should reject without challenge', 68 | () async => (await request(app)) 69 | .auth('dude', 'stuff') 70 | .get(endpoint) 71 | .expectStatus(401) 72 | .expectHeader('WWW-Authenticate', isNull) 73 | .test(), 74 | ); 75 | 76 | test( 77 | 'should accept correct credentials', 78 | () async => await (await request(app)) 79 | .auth('Admin', 'secret1234') 80 | .get(endpoint) 81 | .expectStatus(200) 82 | .test(), 83 | ); 84 | }); 85 | 86 | group('custom authorizer', () { 87 | late Pharaoh app; 88 | const endpoint = '/custom'; 89 | 90 | setUpAll(() { 91 | // Custom authorizer checking if the username starts with 'A' and the password with 'secret' 92 | bool myAuthorizer( 93 | String username, 94 | String password, 95 | ) => 96 | username.startsWith('A') && password.startsWith('secret'); 97 | 98 | final customAuthorizerAuth = basicAuth( 99 | authorizer: myAuthorizer, 100 | unauthorizedResponse: (_) => 'Ohmygod, credentials is required!', 101 | ); 102 | app = Pharaoh() 103 | ..use(customAuthorizerAuth) 104 | ..get(endpoint, (req, res) => res.send('You passed')); 105 | }); 106 | 107 | test( 108 | 'should reject on missing header', 109 | () async => (await request( 110 | app, 111 | )) 112 | .get(endpoint) 113 | .expectStatus(401) 114 | .test(), 115 | ); 116 | 117 | test( 118 | 'should reject on wrong credentials', 119 | () async => (await request(app)) 120 | .auth('dude', 'stuff') 121 | .get(endpoint) 122 | .expectStatus(401) 123 | .expectBody('"Ohmygod, credentials is required!"') 124 | .test(), 125 | ); 126 | 127 | test( 128 | 'should accept fitting credentials', 129 | () async => (await request(app)) 130 | .auth('Aloha', 'secretverymuch') 131 | .get(endpoint) 132 | .expectStatus(200) 133 | .expectBody('You passed') 134 | .test(), 135 | ); 136 | 137 | group('with safe compare', () { 138 | const endpoint = '/custom-compare'; 139 | 140 | setUp(() { 141 | bool myComparingAuthorizer(username, password) => 142 | safeCompare(username, 'Testeroni') && 143 | safeCompare(password, 'testsecret'); 144 | 145 | final customAuth = basicAuth(authorizer: myComparingAuthorizer); 146 | app = Pharaoh() 147 | ..use(customAuth) 148 | ..get(endpoint, (req, res) => res.send('You passed')); 149 | }); 150 | 151 | test( 152 | 'should reject wrong credentials', 153 | () async => (await request(app)) 154 | .auth('bla', 'blub') 155 | .get(endpoint) 156 | .expectStatus(401) 157 | .test(), 158 | ); 159 | 160 | test( 161 | 'should accept fitting credentials', 162 | () async => (await request(app)) 163 | .auth('Testeroni', 'testsecret') 164 | .get(endpoint) 165 | .expectStatus(200) 166 | .expectBody('You passed') 167 | .test(), 168 | ); 169 | }); 170 | }); 171 | }); 172 | } 173 | -------------------------------------------------------------------------------- /packages/pharaoh_jwt_auth/.gitignore: -------------------------------------------------------------------------------- 1 | # https://dart.dev/guides/libraries/private-files 2 | # Created by `dart pub` 3 | .dart_tool/ 4 | 5 | # Avoid committing pubspec.lock for library packages; see 6 | # https://dart.dev/guides/libraries/private-files#pubspeclock. 7 | pubspec.lock 8 | -------------------------------------------------------------------------------- /packages/pharaoh_jwt_auth/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | 3 | - Initial version. 4 | -------------------------------------------------------------------------------- /packages/pharaoh_jwt_auth/LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2023 Chima Precious 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /packages/pharaoh_jwt_auth/README.md: -------------------------------------------------------------------------------- 1 | # pharaoh_jwt_auth 🪭 2 | 3 | This module provides Pharaoh middleware for validating JWTs (JSON Web Tokens) through the [dart_jsonwebtoken](https://pub.dev/packages/dart_jsonwebtoken) 4 | package. 5 | 6 | The decoded JWT payload is available on the request object via `req.auth`. 7 | 8 | ## Installing: 9 | 10 | In your pubspec.yaml 11 | 12 | ```yaml 13 | dependencies: 14 | pharaoh: ^0.0.5+6 15 | pharaoh_jwt_auth: 16 | ``` 17 | 18 | ## Basic Usage: 19 | 20 | ```dart 21 | import 'package:pharaoh/pharaoh.dart'; 22 | import 'package:pharaoh_jwt_auth/pharaoh_jwt_auth.dart'; 23 | 24 | void main() async { 25 | final app = Pharaoh(); 26 | 27 | app.use(jwtAuth(secret: () => SecretKey('some-secret-key'))); 28 | 29 | app.get('/', (req, res) => res.ok('Hello World')); 30 | 31 | await app.listen(); 32 | } 33 | ``` 34 | 35 | The package also exports the [dart_jsonwebtoken](https://pub.dev/packages/dart_jsonwebtoken) package for your usage outside of this library. 36 | 37 | ## Tests 38 | 39 | The cases in the `pharaoh_jwt_auth_test.dart` are also used for automated testing. So if you want 40 | to contribute or just make sure that the package still works, simply run: 41 | 42 | ```shell 43 | dart test 44 | ``` 45 | -------------------------------------------------------------------------------- /packages/pharaoh_jwt_auth/example/pharaoh_jwt_auth_example.dart: -------------------------------------------------------------------------------- 1 | import 'package:pharaoh/pharaoh.dart'; 2 | import 'package:pharaoh_jwt_auth/pharaoh_jwt_auth.dart'; 3 | 4 | void main() async { 5 | final app = Pharaoh(); 6 | 7 | app.use(jwtAuth(secret: () => SecretKey('some-secret-key'))); 8 | 9 | app.get('/', (req, res) => res.ok('Hello World')); 10 | 11 | await app.listen(); 12 | } 13 | -------------------------------------------------------------------------------- /packages/pharaoh_jwt_auth/lib/pharaoh_jwt_auth.dart: -------------------------------------------------------------------------------- 1 | /// Support for doing something awesome. 2 | /// 3 | /// More dartdocs go here. 4 | library; 5 | 6 | export 'src/pharaoh_jwt_auth_base.dart'; 7 | 8 | export 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; 9 | -------------------------------------------------------------------------------- /packages/pharaoh_jwt_auth/lib/src/pharaoh_jwt_auth_base.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:pharaoh/pharaoh.dart'; 5 | import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; 6 | 7 | const _tokenMalformed = 'Format is Authorization: Bearer [token]'; 8 | const _tokenNotFound = 'No authorization token was found'; 9 | 10 | Middleware jwtAuth({required FutureOr Function() secret}) { 11 | return (req, res, next) async { 12 | void reject(String message) { 13 | next(res.unauthorized(data: message)); 14 | } 15 | 16 | final _ = req.headers[HttpHeaders.authorizationHeader]; 17 | if (_ is! Iterable) return reject(_tokenNotFound); 18 | final tokenParts = _.first.toString().split(' '); 19 | if (tokenParts.first != 'Bearer') return reject(_tokenMalformed); 20 | final token = tokenParts.last; 21 | if (JWT.tryDecode(token) == null) return reject(_tokenMalformed); 22 | 23 | final key = await Future.sync(secret); 24 | try { 25 | final result = JWT.verify(token, key); 26 | req.auth = result.payload; 27 | } on JWTException catch (e) { 28 | return reject(e.message); 29 | } catch (error) { 30 | return reject(_tokenMalformed); 31 | } 32 | 33 | return next(req); 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /packages/pharaoh_jwt_auth/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: pharaoh_jwt_auth 2 | description: Pharaoh middleware that validates a JsonWebToken (JWT) and set the req.auth with the attributes 3 | version: 1.0.0+2 4 | repository: https://github.com/codekeyz/pharaoh/tree/main/packages/pharaoh_jwt_auth 5 | 6 | environment: 7 | sdk: ^3.0.0 8 | 9 | dependencies: 10 | pharaoh: ^0.0.5+6 11 | dart_jsonwebtoken: 12 | 13 | dev_dependencies: 14 | spookie: 15 | -------------------------------------------------------------------------------- /packages/pharaoh_jwt_auth/test/pharaoh_jwt_auth_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:pharaoh/pharaoh.dart'; 4 | import 'package:pharaoh_jwt_auth/pharaoh_jwt_auth.dart'; 5 | import 'package:spookie/spookie.dart'; 6 | 7 | void main() { 8 | group('pharaoh_jwt_auth', () { 9 | late Pharaoh app; 10 | final secretKey = SecretKey('hello-new-secret'); 11 | 12 | setUpAll(() { 13 | app = Pharaoh() 14 | ..use(jwtAuth(secret: () => secretKey)) 15 | ..get('/users/me', (req, res) => res.json(req.auth)); 16 | }); 17 | 18 | test( 19 | 'should reject on no authorization header', 20 | () async => (await request(app)) 21 | .get('/users/me') 22 | .expectStatus(401) 23 | .expectContentType('application/json; charset=utf-8') 24 | .expectBody('"No authorization token was found"') 25 | .test(), 26 | ); 27 | 28 | test( 29 | 'should reject on malformed token', 30 | () async => (await request(app)) 31 | .token('some-random-token') 32 | .get('/users/me') 33 | .expectStatus(401) 34 | .expectBody('"Format is Authorization: Bearer [token]"') 35 | .test(), 36 | ); 37 | 38 | test( 39 | 'should reject on expired token', 40 | () async { 41 | final jwt = JWT({ 42 | 'id': 34345, 43 | 'user': {"name": 'Foo', 'lastname': 'Bar'} 44 | }, issuer: 'https://github.com/jonasroussel/dart_jsonwebtoken'); 45 | 46 | final token = jwt.sign(secretKey, 47 | algorithm: JWTAlgorithm.HS256, expiresIn: Duration(seconds: 1)); 48 | 49 | await Future.delayed(const Duration(seconds: 1)); 50 | 51 | await (await request(app)) 52 | .token(token) 53 | .get('/users/me') 54 | .expectStatus(401) 55 | .expectBody('"jwt expired"') 56 | .test(); 57 | }, 58 | ); 59 | 60 | test( 61 | 'should accept on valid authorization header', 62 | () async { 63 | final jwt = JWT({ 64 | 'id': 123, 65 | 'server': {'id': '3e4fc296', 'loc': 'euw-2'} 66 | }, issuer: 'https://github.com/jonasroussel/dart_jsonwebtoken'); 67 | 68 | final token = jwt.sign(secretKey); 69 | 70 | await (await request(app)) 71 | .token(token) 72 | .get('/users/me') 73 | .expectBodyCustom((body) => jsonDecode(body)['id'], 123) 74 | .expectStatus(200) 75 | .test(); 76 | }, 77 | ); 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /packages/spanner/.gitignore: -------------------------------------------------------------------------------- 1 | # https://dart.dev/guides/libraries/private-files 2 | # Created by `dart pub` 3 | .dart_tool/ 4 | 5 | # Avoid committing pubspec.lock for library packages; see 6 | # https://dart.dev/guides/libraries/private-files#pubspeclock. 7 | pubspec.lock 8 | -------------------------------------------------------------------------------- /packages/spanner/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.3 2 | 3 | - (perf): Improve single-param definition matching. 4 | - (perf): Improve lookup & insert speed by 18% 5 | 6 | ## 1.0.2 7 | 8 | - (perf): Make route debug logs optional. 9 | - (bug): Fallback to `HTTPMethod.ALL` when exact route method not found. 10 | 11 | ## 1.0.1 12 | 13 | - De-coupled spanner from pharaoh. 14 | - Updated documentation 15 | 16 | ## 1.0.0 17 | 18 | - Initial version. 19 | -------------------------------------------------------------------------------- /packages/spanner/LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2023 Chima Precious 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /packages/spanner/README.md: -------------------------------------------------------------------------------- 1 | # spanner 🎢 2 | 3 | Generic HTTP Router implementation, internally uses a Radix Tree (aka compact Prefix Tree), supports route params, wildcards. 4 | 5 | ```dart 6 | import 'package:spanner/spanner.dart'; 7 | import 'package:test/test.dart'; 8 | 9 | void main() { 10 | test('spanner sample test', () { 11 | routeHandler() async {} 12 | 13 | final router = Spanner() 14 | ..addMiddleware('/user', #userMiddleware) 15 | ..addRoute(HTTPMethod.GET, '/user', #currentUser) 16 | ..addRoute(HTTPMethod.GET, '/user/', 123) 17 | ..addRoute(HTTPMethod.GET, '/user/.png/download', null) 18 | ..addRoute(HTTPMethod.GET, '/user/.png//hello', null) 19 | ..addRoute(HTTPMethod.GET, '/a/-static', routeHandler); 20 | 21 | var result = router.lookup(HTTPMethod.GET, '/user'); 22 | expect(result!.values, [#userMiddleware, #currentUser]); 23 | 24 | result = router.lookup(HTTPMethod.GET, '/user/24'); 25 | expect(result?.params, {'userId': '24'}); 26 | expect(result?.values, [#userMiddleware, 123]); 27 | 28 | result = router.lookup(HTTPMethod.GET, '/user/aws-image.png/download'); 29 | expect(result?.params, {'file': 'aws-image'}); 30 | 31 | result = router.lookup(HTTPMethod.GET, '/user/aws-image.png/A29384/hello'); 32 | expect(result?.params, {'file': 'aws-image', 'user2': 'A29384'}); 33 | 34 | result = router.lookup(HTTPMethod.GET, '/a/chima-static'); 35 | expect(result?.values, [routeHandler]); 36 | expect(result?.params, {'userId': 'chima'}); 37 | }); 38 | } 39 | 40 | ``` 41 | -------------------------------------------------------------------------------- /packages/spanner/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the static analysis results for your project (errors, 2 | # warnings, and lints). 3 | # 4 | # This enables the 'recommended' set of lints from `package:lints`. 5 | # This set helps identify many issues that may lead to problems when running 6 | # or consuming Dart code, and enforces writing Dart using a single, idiomatic 7 | # style and format. 8 | # 9 | # If you want a smaller set of lints you can change this to specify 10 | # 'package:lints/core.yaml'. These are just the most critical lints 11 | # (the recommended set includes the core lints). 12 | # The core lints are also what is used by pub.dev for scoring packages. 13 | 14 | include: package:lints/recommended.yaml 15 | 16 | # Uncomment the following section to specify additional rules. 17 | 18 | # linter: 19 | # rules: 20 | # - camel_case_types 21 | 22 | # analyzer: 23 | # exclude: 24 | # - path/to/excluded/files/** 25 | 26 | # For more information about the core and recommended set of lints, see 27 | # https://dart.dev/go/core-lints 28 | 29 | # For additional information about configuring this file, see 30 | # https://dart.dev/guides/language/analysis-options 31 | -------------------------------------------------------------------------------- /packages/spanner/example/spanner_example.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:spanner/spanner.dart'; 5 | 6 | typedef Handler = String Function(Map params); 7 | 8 | void main() async { 9 | final spanner = Spanner(); 10 | 11 | getUsers(Map params) => jsonEncode(['Foo', 'Bar']); 12 | 13 | getUser(Map params) => 'Hello ${params['userId']}'; 14 | 15 | spanner 16 | ..addRoute(HTTPMethod.GET, '/', getUsers) 17 | ..addRoute(HTTPMethod.GET, '/', getUser); 18 | 19 | final server = await HttpServer.bind('localhost', 0); 20 | 21 | print('Server Started on port: ${server.port}'); 22 | 23 | server.listen((request) { 24 | if (request.method != 'GET') { 25 | request.response 26 | ..write('Request not supported') 27 | ..close(); 28 | return; 29 | } 30 | 31 | final result = spanner.lookup(HTTPMethod.GET, request.uri.path); 32 | if (result == null) { 33 | request.response 34 | ..write('Request not supported') 35 | ..close(); 36 | return; 37 | } 38 | 39 | final params = result.params; // Map 40 | 41 | /// your handler will be in this list. 42 | /// 43 | /// If any middlewares where resolved along the route to this handler 44 | /// they'll be present in the list 45 | /// 46 | /// The list is ordered in the exact way you registed your middlewares 47 | /// and handlers 48 | final resolvedHandler = result.values; // List 49 | 50 | final handlerResult = (resolvedHandler.first as Handler).call(params); 51 | request.response 52 | ..write(handlerResult) 53 | ..close(); 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /packages/spanner/lib/spanner.dart: -------------------------------------------------------------------------------- 1 | library; 2 | 3 | export 'src/tree/tree.dart' show RouterConfig, RouteResult, HTTPMethod, Spanner; 4 | -------------------------------------------------------------------------------- /packages/spanner/lib/src/parametric/definition.dart: -------------------------------------------------------------------------------- 1 | import '../tree/node.dart'; 2 | import '../tree/tree.dart'; 3 | import 'utils.dart'; 4 | 5 | typedef ParamAndValue = ({String name, String? value}); 6 | 7 | SingleParameterDefn _singleParamDefn(RegExpMatch m) => SingleParameterDefn._( 8 | m.group(2)!, 9 | prefix: m.group(1)?.nullIfEmpty, 10 | suffix: m.group(3)?.nullIfEmpty, 11 | ); 12 | 13 | ParameterDefinition buildParamDefinition(String part) { 14 | if (closeDoorParametricRegex.hasMatch(part)) { 15 | throw ArgumentError.value( 16 | part, null, 'Parameter definition is invalid. Close door neighbors'); 17 | } 18 | 19 | final matches = parametricDefnsRegex.allMatches(part); 20 | if (matches.isEmpty) { 21 | throw ArgumentError.value(part, null, 'Parameter definition is invalid'); 22 | } 23 | 24 | if (matches.length == 1) { 25 | return _singleParamDefn(matches.first); 26 | } 27 | 28 | return CompositeParameterDefinition._(matches.map(_singleParamDefn)); 29 | } 30 | 31 | abstract class ParameterDefinition implements HandlerStore { 32 | String get name; 33 | 34 | String get templateStr; 35 | 36 | String get key; 37 | 38 | bool get terminal; 39 | 40 | bool matches(String route, {bool caseSensitive = false}); 41 | 42 | void resolveParams( 43 | String pattern, 44 | List collector, { 45 | bool caseSentive = false, 46 | }); 47 | } 48 | 49 | class SingleParameterDefn extends ParameterDefinition with HandlerStoreMixin { 50 | @override 51 | final String name; 52 | 53 | final String? prefix; 54 | final String? suffix; 55 | 56 | @override 57 | final String templateStr; 58 | 59 | @override 60 | String get key => 'prefix=$prefix&suffix=$suffix'; 61 | 62 | bool _terminal; 63 | 64 | @override 65 | bool get terminal => _terminal; 66 | 67 | @override 68 | bool matches(String route, {bool caseSensitive = false}) { 69 | final match = matchPattern( 70 | route, 71 | prefix ?? '', 72 | suffix ?? '', 73 | caseSensitive, 74 | ); 75 | return match != null; 76 | } 77 | 78 | SingleParameterDefn._( 79 | this.name, { 80 | this.prefix, 81 | this.suffix, 82 | }) : templateStr = 83 | buildTemplateString(name: name, prefix: prefix, suffix: suffix), 84 | _terminal = false; 85 | 86 | @override 87 | void resolveParams( 88 | final String pattern, 89 | List collector, { 90 | bool caseSentive = false, 91 | }) { 92 | collector.add(( 93 | name: name, 94 | value: matchPattern(pattern, prefix ?? "", suffix ?? "", caseSentive) 95 | )); 96 | } 97 | 98 | @override 99 | void addRoute(HTTPMethod method, IndexedValue handler) { 100 | super.addRoute(method, handler); 101 | _terminal = true; 102 | } 103 | } 104 | 105 | class CompositeParameterDefinition extends ParameterDefinition 106 | implements HandlerStore { 107 | final Iterable parts; 108 | final SingleParameterDefn _maybeTerminalPart; 109 | 110 | CompositeParameterDefinition._(this.parts) : _maybeTerminalPart = parts.last; 111 | 112 | @override 113 | String get templateStr => parts.map((e) => e.templateStr).join(); 114 | 115 | @override 116 | String get name => parts.map((e) => e.name).join('|'); 117 | 118 | @override 119 | String get key => parts.map((e) => e.key).join('|'); 120 | 121 | RegExp get _template => buildRegexFromTemplate(templateStr); 122 | 123 | @override 124 | bool get terminal => _maybeTerminalPart.terminal; 125 | 126 | @override 127 | bool matches(String route, {bool caseSensitive = false}) => 128 | _template.hasMatch(route); 129 | 130 | @override 131 | void resolveParams( 132 | String pattern, 133 | List collector, { 134 | bool caseSentive = false, 135 | }) { 136 | final match = _template.firstMatch(pattern); 137 | if (match == null) return; 138 | 139 | for (final key in match.groupNames) { 140 | collector.add((name: key, value: match.namedGroup(key))); 141 | } 142 | } 143 | 144 | @override 145 | void addMiddleware(IndexedValue handler) { 146 | _maybeTerminalPart.addMiddleware(handler); 147 | } 148 | 149 | @override 150 | void addRoute(HTTPMethod method, IndexedValue handler) { 151 | _maybeTerminalPart.addRoute(method, handler); 152 | } 153 | 154 | @override 155 | void offsetIndex(int index) => _maybeTerminalPart.offsetIndex(index); 156 | 157 | @override 158 | IndexedValue? getHandler(HTTPMethod method) { 159 | return _maybeTerminalPart.getHandler(method); 160 | } 161 | 162 | @override 163 | bool hasMethod(HTTPMethod method) => _maybeTerminalPart.hasMethod(method); 164 | 165 | @override 166 | Iterable get methods => _maybeTerminalPart.methods; 167 | } 168 | -------------------------------------------------------------------------------- /packages/spanner/lib/src/route/action.dart: -------------------------------------------------------------------------------- 1 | part of '../tree/node.dart'; 2 | 3 | typedef Indexed = ({int index, T value}); 4 | 5 | typedef IndexedValue = Indexed; 6 | 7 | abstract interface class HandlerStore { 8 | IndexedValue? getHandler(HTTPMethod method); 9 | 10 | Iterable get methods; 11 | 12 | void offsetIndex(int index); 13 | 14 | bool hasMethod(HTTPMethod method); 15 | 16 | void addRoute(HTTPMethod method, IndexedValue handler); 17 | void addMiddleware(IndexedValue handler); 18 | } 19 | 20 | mixin HandlerStoreMixin implements HandlerStore { 21 | final List middlewares = []; 22 | final requestHandlers = List.filled( 23 | HTTPMethod.values.length, 24 | null, 25 | ); 26 | 27 | @override 28 | void offsetIndex(int index) { 29 | for (final middleware in middlewares.indexed) { 30 | middlewares[middleware.$1] = ( 31 | index: middleware.$2.index + index, 32 | value: middleware.$2.value, 33 | ); 34 | } 35 | 36 | // Offset the indices of request handlers 37 | for (int i = 0; i < requestHandlers.length; i++) { 38 | if (requestHandlers[i] != null) { 39 | requestHandlers[i] = ( 40 | index: requestHandlers[i]!.index + index, 41 | value: requestHandlers[i]!.value, 42 | ); 43 | } 44 | } 45 | } 46 | 47 | @override 48 | Iterable get methods => HTTPMethod.values.where(hasMethod); 49 | 50 | @override 51 | bool hasMethod(HTTPMethod method) => requestHandlers[method.index] != null; 52 | 53 | @override 54 | IndexedValue? getHandler(HTTPMethod method) => 55 | requestHandlers[method.index] ?? requestHandlers[HTTPMethod.ALL.index]; 56 | 57 | @override 58 | void addRoute(HTTPMethod method, IndexedValue handler) { 59 | if (hasMethod(method)) { 60 | final route = (this as Node).route; 61 | throw ArgumentError.value( 62 | '${method.name}: $route', null, 'Route entry already exists'); 63 | } 64 | requestHandlers[method.index] = handler; 65 | } 66 | 67 | @override 68 | void addMiddleware(IndexedValue handler) => middlewares.add(handler); 69 | } 70 | -------------------------------------------------------------------------------- /packages/spanner/lib/src/tree/utils.dart: -------------------------------------------------------------------------------- 1 | // --> username 2 | String? getParameter(String pattern, {int start = 0}) { 3 | if (start != 0) pattern = pattern.substring(start); 4 | // < 5 | if (pattern.codeUnitAt(start) != 60) return null; 6 | 7 | final sb = StringBuffer(); 8 | for (int i = 1; i < pattern.length; i++) { 9 | // > 10 | if (pattern.codeUnitAt(i) == 62) break; 11 | sb.write(pattern[i]); 12 | } 13 | return sb.toString(); 14 | } 15 | 16 | bool isRegexeric(String pattern, {int at = 0}) { 17 | if (at > (pattern.length - 1)) return false; 18 | return pattern.codeUnitAt(at) == 40; 19 | } 20 | -------------------------------------------------------------------------------- /packages/spanner/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: spanner 2 | description: Generic HTTP Router implementation, internally uses a Radix Tree (aka compact Prefix Tree), supports route params, wildcards. 3 | version: 1.0.5 4 | repository: https://github.com/Pharaoh-Framework/pharaoh/tree/main/packages/spanner 5 | 6 | environment: 7 | sdk: ^3.0.0 8 | 9 | dependencies: 10 | collection: ^1.18.0 11 | meta: 12 | 13 | dev_dependencies: 14 | lints: ^2.1.0 15 | test: ^1.24.0 16 | -------------------------------------------------------------------------------- /packages/spanner/test/helpers/test_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:spanner/spanner.dart'; 2 | import 'package:spanner/src/tree/node.dart'; 3 | import 'package:test/expect.dart'; 4 | 5 | Matcher havingParameters(Map params) { 6 | return isA() 7 | .having((p0) => p0.actual, 'with actual', isA()) 8 | .having((p0) => p0.params, 'with parameters', params); 9 | } 10 | 11 | Matcher isStaticNode(String name) { 12 | return isA().having( 13 | (p0) => p0.actual, 14 | 'has actual', 15 | isA().having((p0) => p0.route, 'has name', name), 16 | ); 17 | } 18 | 19 | Matcher hasValues(List result) { 20 | return isA().having( 21 | (p0) => p0.values, 22 | 'has values', 23 | result, 24 | ); 25 | } 26 | 27 | T runSyncAndReturnException(Function call) { 28 | dynamic result; 29 | try { 30 | call.call(); 31 | } catch (e) { 32 | result = e; 33 | } 34 | 35 | expect(result, isA()); 36 | return result as T; 37 | } 38 | -------------------------------------------------------------------------------- /packages/spanner/test/issue_127_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:spanner/spanner.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test("ALL as fallback for specific method", () { 6 | final router = Spanner() 7 | ..addRoute(HTTPMethod.GET, '/api/auth/login', 4) 8 | ..addRoute(HTTPMethod.ALL, '/api/auth/login', 5); 9 | 10 | var result = router.lookup(HTTPMethod.GET, '/api/auth/login'); 11 | expect(result?.values, [4]); 12 | 13 | result = router.lookup(HTTPMethod.POST, '/api/auth/login'); 14 | expect(result?.values, [5]); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /packages/spanner/test/middleware_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:spanner/spanner.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('addMiddleware', () { 6 | test('should return middlewares', () { 7 | router() => Spanner() 8 | ..addMiddleware('/', 24) 9 | ..addRoute(HTTPMethod.GET, '/user', 44); 10 | 11 | final result = router().lookup(HTTPMethod.GET, '/user'); 12 | expect(result?.values, [24, 44]); 13 | }); 14 | 15 | test('should return Wildcard', () { 16 | router() => Spanner() 17 | ..addMiddleware('/', 24) 18 | ..addRoute(HTTPMethod.GET, '/user', 44) 19 | ..addRoute(HTTPMethod.ALL, '/*', 100); 20 | 21 | final result = router().lookup(HTTPMethod.GET, '/some-unknown-route'); 22 | expect(result?.values, [24, 100]); 23 | }); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /packages/spanner/test/parametric_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:spanner/src/parametric/definition.dart'; 2 | import 'package:spanner/src/tree/node.dart'; 3 | import 'package:spanner/src/tree/tree.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | import 'helpers/test_utils.dart'; 7 | 8 | void main() { 9 | group('parametric route', () { 10 | group('should reject', () { 11 | test('inconsistent parameter definitions', () { 12 | router() => Spanner() 13 | ..addRoute(HTTPMethod.GET, '/user/.png/download', null) 14 | ..addRoute(HTTPMethod.POST, '/user/.png', null) 15 | ..addRoute(HTTPMethod.GET, '/user/.png//hello', null); 16 | 17 | final exception = runSyncAndReturnException(router); 18 | expect(exception.message, 19 | contains('Route has inconsistent naming in parameter definition')); 20 | expect(exception.message, contains('.png')); 21 | expect(exception.message, contains('.png')); 22 | }); 23 | 24 | test('close door parameter definitions', () { 25 | router() => 26 | Spanner()..addRoute(HTTPMethod.GET, '/user/', null); 27 | 28 | final exception = runSyncAndReturnException(router); 29 | expect(exception.message, 30 | contains('Parameter definition is invalid. Close door neighbors')); 31 | expect(exception.invalidValue, ''); 32 | }); 33 | 34 | test('invalid parameter definition', () { 35 | router() => Spanner() 36 | ..addRoute(HTTPMethod.GET, '/user/>#>', null); 37 | 38 | final exception = runSyncAndReturnException(router); 39 | expect(exception.message, contains('Parameter definition is invalid')); 40 | expect(exception.invalidValue, '>#>'); 41 | }); 42 | 43 | test('duplicate routes', () { 44 | router() => Spanner() 45 | ..addRoute(HTTPMethod.GET, '/user', null) 46 | ..addRoute(HTTPMethod.GET, '/user', null); 47 | 48 | router2() => Spanner() 49 | ..addRoute(HTTPMethod.GET, '//item/', null) 50 | ..addRoute(HTTPMethod.GET, '//item/', null); 51 | 52 | router3() => Spanner() 53 | ..addRoute(HTTPMethod.GET, '//item/chimahello', null) 54 | ..addRoute(HTTPMethod.GET, '//item/chimahello', null); 55 | 56 | var exception = runSyncAndReturnException(router); 57 | expect(exception.message, contains('Route entry already exists')); 58 | 59 | exception = runSyncAndReturnException(router2); 60 | expect(exception.message, contains('Route entry already exists')); 61 | 62 | exception = runSyncAndReturnException(router3); 63 | expect(exception.message, contains('Route entry already exists')); 64 | }); 65 | }); 66 | 67 | test('with request.url contains dash', () { 68 | final router = Spanner()..addRoute(HTTPMethod.GET, '/a//b', null); 69 | 70 | final result = router.lookup(HTTPMethod.GET, '/a/foo-bar/b'); 71 | expect(result, havingParameters({'param': 'foo-bar'})); 72 | }); 73 | 74 | test('with fixed suffix', () async { 75 | final router = Spanner() 76 | ..addRoute(HTTPMethod.GET, '/user', null) 77 | ..addRoute(HTTPMethod.GET, '/user/', null) 78 | ..addRoute(HTTPMethod.GET, '/user//details', null) 79 | ..addRoute(HTTPMethod.GET, '/user/.png/download', null) 80 | ..addRoute(HTTPMethod.GET, '/user/.png//hello', null) 81 | ..addRoute(HTTPMethod.GET, '/a/-static', null) 82 | ..addRoute(HTTPMethod.GET, '/b/.static', null); 83 | 84 | var node = router.lookup(HTTPMethod.GET, '/user'); 85 | expect(node, isStaticNode('user')); 86 | 87 | node = router.lookup(HTTPMethod.GET, '/user/24'); 88 | expect(node, havingParameters({'userId': '24'})); 89 | 90 | node = router.lookup(HTTPMethod.GET, '/user/3948/details'); 91 | expect(node, havingParameters({'userId': '3948'})); 92 | 93 | node = router.lookup(HTTPMethod.GET, '/user/aws-image.png/download'); 94 | expect(node, havingParameters({'file': 'aws-image'})); 95 | 96 | node = router.lookup(HTTPMethod.GET, '/user/aws-image.png/A29384/hello'); 97 | expect( 98 | node, 99 | havingParameters( 100 | {'file': 'aws-image', 'user2': 'A29384'})); 101 | 102 | node = router.lookup(HTTPMethod.GET, '/a/param-static'); 103 | expect(node, havingParameters({'userId': 'param'})); 104 | 105 | node = router.lookup(HTTPMethod.GET, '/b/param.static'); 106 | expect(node, havingParameters({'userId': 'param'})); 107 | }); 108 | 109 | test('contain param and wildcard together', () { 110 | final router = Spanner() 111 | ..addRoute(HTTPMethod.GET, '//item/', null) 112 | ..addRoute(HTTPMethod.GET, '//item/*', #wild); 113 | 114 | expect( 115 | router.lookup(HTTPMethod.GET, '/fr/item/12345'), 116 | havingParameters({'lang': 'fr', 'id': '12345'}), 117 | ); 118 | 119 | final result = router.lookup(HTTPMethod.GET, '/fr/item/12345/edit'); 120 | expect(result?.values, [#wild]); 121 | expect(result?.params, {'lang': 'fr', 'id': '12345'}); 122 | }); 123 | }); 124 | } 125 | -------------------------------------------------------------------------------- /packages/spanner/test/wildcard_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:spanner/spanner.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('wildcard_test', () { 6 | test('*', () { 7 | final router = Spanner() 8 | ..addMiddleware('/api', 3) 9 | ..addRoute(HTTPMethod.GET, '/api/auth/login', 4) 10 | ..addRoute(HTTPMethod.GET, '/api/users/', 5) 11 | ..addRoute(HTTPMethod.ALL, '/*', 'mee-moo'); 12 | 13 | expect( 14 | router.lookup(HTTPMethod.GET, '/api/users/hello')?.values, 15 | [3, 5], 16 | ); 17 | 18 | var result = router.lookup(HTTPMethod.GET, '/api'); 19 | expect(result?.values, [3]); 20 | 21 | result = router.lookup(HTTPMethod.GET, '/api/users'); 22 | expect(result?.values, [3]); 23 | 24 | result = router.lookup(HTTPMethod.GET, '/api/users/chima/details/home'); 25 | expect(result?.values, [3, 'mee-moo']); 26 | }); 27 | 28 | test('when wildcard with HTTPMethod.ALL', () { 29 | final router = Spanner()..addRoute(HTTPMethod.ALL, '/*', 'mee-moo'); 30 | 31 | var result = router.lookup(HTTPMethod.GET, '/hello/boy'); 32 | expect(result!.values, ['mee-moo']); 33 | 34 | result = router.lookup(HTTPMethod.DELETE, '/hello'); 35 | expect(result?.values, ['mee-moo']); 36 | 37 | result = router.lookup(HTTPMethod.POST, '/hello'); 38 | expect(result?.values, ['mee-moo']); 39 | }); 40 | 41 | test('when wildcard with specific HTTPMethod', () { 42 | final router = Spanner()..addRoute(HTTPMethod.GET, '/*', 'mee-moo'); 43 | 44 | var result = router.lookup(HTTPMethod.GET, '/hello-world'); 45 | expect(result!.values, ['mee-moo']); 46 | 47 | result = router.lookup(HTTPMethod.POST, '/hello'); 48 | expect(result?.values, const []); 49 | }); 50 | 51 | test('static route and wildcard on same method', () { 52 | final router = Spanner() 53 | ..addRoute(HTTPMethod.GET, '/hello-world', 'foo-bar') 54 | ..addRoute(HTTPMethod.GET, '/*', 'mee-moo'); 55 | 56 | var result = router.lookup(HTTPMethod.GET, '/hello-world'); 57 | expect(result!.values, ['foo-bar']); 58 | 59 | result = router.lookup(HTTPMethod.GET, '/hello'); 60 | expect(result?.values, ['mee-moo']); 61 | }); 62 | 63 | test( 64 | 'static route and wildcard on same method with additional HTTPMETHOD.ALL', 65 | () { 66 | final router = Spanner() 67 | ..addRoute(HTTPMethod.GET, '/hello-world', 'foo-bar') 68 | ..addRoute(HTTPMethod.GET, '/*', 'mee-moo') 69 | ..addRoute(HTTPMethod.ALL, '/*', 'ange-lina'); 70 | 71 | var result = router.lookup(HTTPMethod.GET, '/hello-world'); 72 | expect(result!.values, ['foo-bar']); 73 | 74 | result = router.lookup(HTTPMethod.GET, '/hello'); 75 | expect(result?.values, ['mee-moo']); 76 | 77 | result = router.lookup(HTTPMethod.POST, '/hello'); 78 | expect(result?.values, ['ange-lina']); 79 | }); 80 | 81 | test('issue', () { 82 | final router = Spanner() 83 | ..addRoute(HTTPMethod.GET, '/hello/chima', 'haah') 84 | ..addRoute(HTTPMethod.GET, '/hello/*', 'bar') 85 | ..addRoute(HTTPMethod.GET, '//hello', 'var') 86 | ..addRoute(HTTPMethod.GET, '/*', 'mee'); 87 | 88 | var result = router.lookup(HTTPMethod.GET, '/hello/chima'); 89 | expect(result?.values, ['haah']); 90 | 91 | result = router.lookup(HTTPMethod.GET, '/hello/come'); 92 | expect(result?.values, ['bar']); 93 | 94 | result = router.lookup(HTTPMethod.GET, '/holycrap'); 95 | expect(result?.values, ['mee']); 96 | 97 | result = router.lookup(HTTPMethod.GET, '/holycrap/hello'); 98 | expect(result?.values, ['var']); 99 | 100 | result = router.lookup(HTTPMethod.GET, '/holycrap/hello/goat'); 101 | expect(result?.values, ['mee']); 102 | }); 103 | 104 | test('contain param and wildcard together', () { 105 | final router = Spanner() 106 | ..addRoute(HTTPMethod.GET, '//item/', 'id-man') 107 | ..addRoute(HTTPMethod.GET, '/*', 'wildcard'); 108 | 109 | var result = router.lookup(HTTPMethod.GET, '/fr/item/12345'); 110 | expect(result?.values, ['id-man']); 111 | 112 | result = router.lookup(HTTPMethod.GET, '/fr/item/ajsdkflajsdfj/how'); 113 | expect(result?.values, ['wildcard']); 114 | }); 115 | }); 116 | } 117 | -------------------------------------------------------------------------------- /packages/spookie/.gitignore: -------------------------------------------------------------------------------- 1 | # https://dart.dev/guides/libraries/private-files 2 | # Created by `dart pub` 3 | .dart_tool/ 4 | -------------------------------------------------------------------------------- /packages/spookie/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.2 2 | 3 | - Encode encode Map type on .post & set headers to application/json. 4 | 5 | ## 1.0.1 6 | 7 | - Renamed expection methods -> make them self explanatory. 8 | 9 | ## 1.0.0 10 | 11 | - Initial version. 12 | -------------------------------------------------------------------------------- /packages/spookie/LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2023 Chima Precious 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /packages/spookie/README.md: -------------------------------------------------------------------------------- 1 | # spookie 🎌 2 | 3 | Easy & composable tests for your API's. I wrote this to work just like https://www.npmjs.com/package/supertest 4 | 5 | ## Installing: 6 | 7 | In your pubspec.yaml 8 | 9 | ```yaml 10 | dev_dependencies: 11 | spookie: 12 | ``` 13 | 14 | ## Basic Usage: 15 | 16 | ```dart 17 | import 'package:pharaoh/pharaoh.dart'; 18 | import 'package:spookie/spookie.dart'; 19 | 20 | void main() async { 21 | 22 | final app = Pharaoh().get('/', (req, res) { 23 | return res 24 | .type(ContentType.parse('application/vnd.example+json')) 25 | .json({"hello": "world"}); 26 | }); 27 | 28 | await app.listen(port: 5000); 29 | 30 | 31 | test('should not override previous Content-Types', () async { 32 | 33 | await Spookie.uri(Uri.parse('http://localhost:5000')).get('/') 34 | .expectStatus(200) 35 | .expectContentType('application/vnd.example+json') 36 | .expectBody('{"hello":"world"}') 37 | .test(); 38 | 39 | }); 40 | } 41 | 42 | ``` 43 | 44 | ## Tests 45 | 46 | The cases in the `spookie_test.dart` are also used for automated testing. So if you want 47 | to contribute or just make sure that the package still works, simply run: 48 | 49 | ```shell 50 | dart test 51 | ``` 52 | -------------------------------------------------------------------------------- /packages/spookie/example/spookie_example.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:pharaoh/pharaoh.dart'; 4 | import 'package:spookie/spookie.dart'; 5 | 6 | void main() async { 7 | final app = Pharaoh(); 8 | 9 | app.get('/', (req, res) { 10 | return res 11 | .type(ContentType.parse('application/vnd.example+json')) 12 | .json({"hello": "world"}); 13 | }); 14 | 15 | test('should not override previous Content-Types', () async { 16 | await (await request(app)) 17 | .get('/') 18 | .expectStatus(200) 19 | .expectContentType('application/vnd.example+json') 20 | .expectBody('{"hello":"world"}') 21 | .test(); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /packages/spookie/lib/spookie.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'package:http/http.dart' as http; 4 | import 'src/http_expectation.dart'; 5 | 6 | export 'package:test/test.dart'; 7 | 8 | typedef HttpRequestHandler = Function(HttpRequest req); 9 | 10 | abstract interface class Spookie { 11 | factory Spookie.server(HttpServer server) => 12 | _$SpookieImpl(getServerUri(server)); 13 | 14 | factory Spookie.uri(Uri uri) => _$SpookieImpl(uri); 15 | 16 | Spookie auth(String user, String pass); 17 | 18 | Spookie token(String token); 19 | 20 | HttpResponseExpection post( 21 | String path, 22 | Object? body, { 23 | Map? headers, 24 | }); 25 | 26 | HttpResponseExpection put( 27 | String path, { 28 | Map? headers, 29 | Object? body, 30 | }); 31 | 32 | HttpResponseExpection patch( 33 | String path, { 34 | Map? headers, 35 | Object? body, 36 | }); 37 | 38 | HttpResponseExpection delete( 39 | String path, { 40 | Map? headers, 41 | Object? body, 42 | }); 43 | 44 | HttpResponseExpection get( 45 | String path, { 46 | Map? headers, 47 | }); 48 | } 49 | 50 | class _$SpookieImpl implements Spookie { 51 | final Uri baseUri; 52 | 53 | _$SpookieImpl(this.baseUri); 54 | 55 | Uri getUri(String p) { 56 | final uri = Uri.parse(p); 57 | return baseUri.replace(path: uri.path, query: uri.query); 58 | } 59 | 60 | final Map _headers = {}; 61 | 62 | Map mergeHeaders(Map headers) { 63 | final map = Map.from(_headers); 64 | for (final key in headers.keys) { 65 | final value = headers[key]; 66 | if (value != null) map[key] = value; 67 | } 68 | return map; 69 | } 70 | 71 | @override 72 | HttpResponseExpection post( 73 | String path, 74 | Object? body, { 75 | Map? headers, 76 | }) { 77 | headers = mergeHeaders(headers ?? {}); 78 | 79 | if (body is Map && !headers.containsKey(HttpHeaders.contentTypeHeader)) { 80 | headers[HttpHeaders.contentTypeHeader] = 'application/json'; 81 | body = jsonEncode(body); 82 | } 83 | 84 | return expectHttp(http.post(getUri(path), headers: headers, body: body)); 85 | } 86 | 87 | @override 88 | HttpResponseExpection get( 89 | String path, { 90 | Map? headers, 91 | }) => 92 | expectHttp( 93 | http.get(getUri(path), headers: mergeHeaders(headers ?? {})), 94 | ); 95 | 96 | @override 97 | HttpResponseExpection delete( 98 | String path, { 99 | Map? headers, 100 | Object? body, 101 | }) => 102 | expectHttp( 103 | http.delete(getUri(path), 104 | headers: mergeHeaders(headers ?? {}), body: body), 105 | ); 106 | 107 | @override 108 | HttpResponseExpection patch( 109 | String path, { 110 | Map? headers, 111 | Object? body, 112 | }) => 113 | expectHttp( 114 | http.patch(getUri(path), 115 | headers: mergeHeaders(headers ?? {}), body: body), 116 | ); 117 | 118 | @override 119 | HttpResponseExpection put( 120 | String path, { 121 | Map? headers, 122 | Object? body, 123 | }) => 124 | expectHttp( 125 | http.put(getUri(path), 126 | headers: mergeHeaders(headers ?? {}), body: body), 127 | ); 128 | 129 | @override 130 | Spookie auth(String user, String pass) { 131 | final basicAuth = base64Encode(utf8.encode('$user:$pass')); 132 | _headers[HttpHeaders.authorizationHeader] = 'Basic $basicAuth'; 133 | return this; 134 | } 135 | 136 | @override 137 | Spookie token(String token) { 138 | _headers[HttpHeaders.authorizationHeader] = 'Bearer $token'; 139 | return this; 140 | } 141 | } 142 | 143 | class SpookieAgent { 144 | static Spookie? _instance; 145 | static HttpServer? _server; 146 | 147 | static Future create(HttpRequestHandler app) async { 148 | if (_instance != null) { 149 | await _server?.close(); 150 | _server = null; 151 | } 152 | 153 | _server = await HttpServer.bind(InternetAddress.anyIPv4, 0) 154 | ..listen(app); 155 | return _instance = Spookie.server(_server!); 156 | } 157 | } 158 | 159 | Uri getServerUri(HttpServer server) { 160 | if (server.address.isLoopback) { 161 | return Uri(scheme: 'http', host: 'localhost', port: server.port); 162 | } 163 | // IPv6 addresses in URLs need to be enclosed in square brackets to avoid 164 | // URL ambiguity with the ":" in the address. 165 | if (server.address.type == InternetAddressType.IPv6) { 166 | return Uri( 167 | scheme: 'http', 168 | host: '[${server.address.address}]', 169 | port: server.port, 170 | ); 171 | } 172 | 173 | return Uri(scheme: 'http', host: server.address.address, port: server.port); 174 | } 175 | 176 | Future request(T app) async { 177 | return SpookieAgent.create((app as dynamic).handleRequest); 178 | } 179 | -------------------------------------------------------------------------------- /packages/spookie/lib/src/expectation.dart: -------------------------------------------------------------------------------- 1 | abstract class ExpectationBase { 2 | ExpectationBase(this.actual); 3 | 4 | /// The value that is tested 5 | final Actual actual; 6 | 7 | Future test(); 8 | } 9 | -------------------------------------------------------------------------------- /packages/spookie/lib/src/http_expectation.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:test/test.dart'; 5 | import 'package:http/http.dart' as http; 6 | 7 | import 'expectation.dart'; 8 | 9 | typedef HttpFutureResponse = Future; 10 | 11 | HttpResponseExpection expectHttp(HttpFutureResponse value) => 12 | HttpResponseExpection(value); 13 | 14 | typedef GetValueFromResponse = T Function(http.Response response); 15 | 16 | typedef MatchCase = ({GetValueFromResponse value, dynamic matcher}); 17 | 18 | class HttpResponseExpection 19 | extends ExpectationBase { 20 | HttpResponseExpection(super.value); 21 | 22 | final List _matchcases = []; 23 | 24 | HttpResponseExpection expectHeader(String header, dynamic matcher) { 25 | final MatchCase test = 26 | (value: (resp) => resp.headers[header], matcher: matcher); 27 | _matchcases.add(test); 28 | return this; 29 | } 30 | 31 | HttpResponseExpection expectContentType(dynamic matcher) { 32 | final MatchCase test = ( 33 | value: (resp) => resp.headers[HttpHeaders.contentTypeHeader], 34 | matcher: matcher 35 | ); 36 | _matchcases.add(test); 37 | return this; 38 | } 39 | 40 | HttpResponseExpection expectHeaders(dynamic matcher) { 41 | final MatchCase test = (value: (resp) => resp.headers, matcher: matcher); 42 | _matchcases.add(test); 43 | return this; 44 | } 45 | 46 | HttpResponseExpection expectStatus(dynamic matcher) { 47 | final MatchCase test = (value: (resp) => resp.statusCode, matcher: matcher); 48 | _matchcases.add(test); 49 | return this; 50 | } 51 | 52 | HttpResponseExpection expectBody(dynamic matcher) { 53 | if (matcher is! Matcher && matcher is! String) { 54 | matcher = jsonEncode(matcher); 55 | } 56 | 57 | final MatchCase value = (value: (resp) => resp.body, matcher: matcher); 58 | _matchcases.add(value); 59 | return this; 60 | } 61 | 62 | HttpResponseExpection expectJsonBody(dynamic matcher) { 63 | _matchcases.add(( 64 | value: (res) => res.headers, 65 | matcher: containsPair( 66 | HttpHeaders.contentTypeHeader, 'application/json; charset=utf-8') 67 | )); 68 | 69 | final MatchCase value = 70 | (value: (resp) => jsonDecode(resp.body), matcher: matcher); 71 | _matchcases.add(value); 72 | return this; 73 | } 74 | 75 | HttpResponseExpection expectBodyCustom( 76 | T Function(String body) getValue, 77 | dynamic matcher, 78 | ) { 79 | final MatchCase value = ( 80 | value: (resp) => getValue(resp.body), 81 | matcher: matcher, 82 | ); 83 | _matchcases.add(value); 84 | return this; 85 | } 86 | 87 | HttpResponseExpection custom(GetValueFromResponse check, Matcher matcher) { 88 | final MatchCase value = (value: check, matcher: matcher); 89 | _matchcases.add(value); 90 | return this; 91 | } 92 | 93 | @override 94 | Future test() async { 95 | final response = await actual; 96 | 97 | // ignore: no_leading_underscores_for_local_identifiers 98 | for (final _case in _matchcases) { 99 | expect(_case.value(response), _case.matcher); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /packages/spookie/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: spookie 2 | description: SuperAgent driven library for testing Dart HTTP servers. 3 | version: 1.0.2+3 4 | repository: https://github.com/codekeyz/pharaoh/tree/main/packages/spookie 5 | 6 | environment: 7 | sdk: ^3.0.0 8 | 9 | dependencies: 10 | http: any 11 | test: any 12 | 13 | dev_dependencies: 14 | pharaoh: 15 | -------------------------------------------------------------------------------- /pharaoh_examples/.gitignore: -------------------------------------------------------------------------------- 1 | # https://dart.dev/guides/libraries/private-files 2 | # Created by `dart pub` 3 | .dart_tool/ 4 | -------------------------------------------------------------------------------- /pharaoh_examples/README.md: -------------------------------------------------------------------------------- 1 | # pharaoh_examples 2 | 3 | [See Examples](../packages/pharaoh/example/README.md) 4 | -------------------------------------------------------------------------------- /pharaoh_examples/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the static analysis results for your project (errors, 2 | # warnings, and lints). 3 | # 4 | # This enables the 'recommended' set of lints from `package:lints`. 5 | # This set helps identify many issues that may lead to problems when running 6 | # or consuming Dart code, and enforces writing Dart using a single, idiomatic 7 | # style and format. 8 | # 9 | # If you want a smaller set of lints you can change this to specify 10 | # 'package:lints/core.yaml'. These are just the most critical lints 11 | # (the recommended set includes the core lints). 12 | # The core lints are also what is used by pub.dev for scoring packages. 13 | 14 | include: package:lints/recommended.yaml 15 | 16 | # Uncomment the following section to specify additional rules. 17 | 18 | # linter: 19 | # rules: 20 | # - camel_case_types 21 | 22 | # analyzer: 23 | # exclude: 24 | # - path/to/excluded/files/** 25 | 26 | # For more information about the core and recommended set of lints, see 27 | # https://dart.dev/go/core-lints 28 | 29 | # For additional information about configuring this file, see 30 | # https://dart.dev/guides/language/analysis-options 31 | -------------------------------------------------------------------------------- /pharaoh_examples/lib/api_service/index.dart: -------------------------------------------------------------------------------- 1 | import 'package:pharaoh/pharaoh.dart'; 2 | 3 | /// map of valid api keys, typically mapped to 4 | /// account info with some sort of database like redis. 5 | /// api keys do _not_ serve as authentication, merely to 6 | /// track API usage or help prevent malicious behavior etc. 7 | var apiKeys = ['foo', 'bar', 'baz']; 8 | 9 | /// these two objects will serve as our faux database 10 | var repos = [ 11 | {"name": 'express', "url": 'https://github.com/expressjs/express'}, 12 | {"name": 'stylus', "url": 'https://github.com/learnboost/stylus'}, 13 | {"name": 'cluster', "url": 'https://github.com/learnboost/cluster'} 14 | ]; 15 | 16 | var users = [ 17 | {"name": 'tobi'}, 18 | {"name": 'loki'}, 19 | {"name": 'jane'} 20 | ]; 21 | 22 | var userRepos = { 23 | "tobi": [repos[0], repos[1]], 24 | "loki": [repos[1]], 25 | "jane": [repos[2]] 26 | }; 27 | 28 | final app = Pharaoh(); 29 | 30 | void main([args]) async { 31 | final port = List.from(args).isEmpty ? 3000 : args[0]; 32 | 33 | /// if we wanted to supply more than JSON, we could 34 | /// use something similar to the content-negotiation 35 | /// example. 36 | /// here we validate the API key, 37 | /// by mounting this middleware to /api 38 | /// meaning only paths prefixed with "/api" 39 | /// will cause this middleware to be invoked 40 | app.on('/api', (req, res, next) { 41 | var key = req.query['api-key']; 42 | 43 | /// key isn't present 44 | if (key == null) { 45 | next(res.json('API key required', statusCode: 400)); 46 | return; 47 | } 48 | 49 | /// key is invalid 50 | if (!apiKeys.contains(key)) { 51 | next(res.json('Invalid API key', statusCode: 401)); 52 | return; 53 | } 54 | 55 | req['key'] = key; 56 | 57 | next(req); 58 | }); 59 | 60 | /// we now can assume the api key is valid, 61 | /// and simply expose the data 62 | /// example: http://localhost:3000/api/users/?api-key=foo 63 | app.get('/api/users', (req, res) => res.json(users)); 64 | 65 | /// example: http://localhost:3000/api/repos/?api-key=foo 66 | app.get('/api/repos', (req, res) => res.json(repos)); 67 | 68 | /// example: http://localhost:3000/api/user/tobi/repos/?api-key=foo 69 | app.get('/api/user//repos', (req, res) { 70 | var name = req.params['name']; 71 | var user = userRepos[name]; 72 | 73 | if (user != null) { 74 | return res.json(user); 75 | } 76 | 77 | return res.notFound(); 78 | }); 79 | 80 | await app.listen(port: port); 81 | } 82 | -------------------------------------------------------------------------------- /pharaoh_examples/lib/middleware/index.dart: -------------------------------------------------------------------------------- 1 | import 'package:pharaoh/pharaoh.dart'; 2 | 3 | final app = Pharaoh(); 4 | 5 | void main() async { 6 | app.useRequestHook(logRequestHook); 7 | 8 | app.get('/', (req, res) => res.ok("Hurray 🚀")); 9 | 10 | await app.listen(); 11 | } 12 | -------------------------------------------------------------------------------- /pharaoh_examples/lib/route_groups/index.dart: -------------------------------------------------------------------------------- 1 | import 'package:pharaoh/pharaoh.dart'; 2 | 3 | final app = Pharaoh()..useRequestHook(logRequestHook); 4 | 5 | void main() async { 6 | final guestRouter = Pharaoh.router 7 | ..get('/foo', (req, res) => res.ok("Hello World")) 8 | ..post('/bar', (req, res) => res.json({"mee": "moo"})) 9 | ..put('/yoo', (req, res) => res.json({"pookey": "reyrey"})); 10 | 11 | final adminRouter = Pharaoh.router 12 | ..get('/user', (req, res) => res.json({"chima": "happy"})) 13 | ..put('/hello', (req, res) => res.json({"name": "chima"})) 14 | ..post('/say-hello', (req, res) => res.notFound()) 15 | ..delete('/delete', (req, res) => res.json(req.body)); 16 | 17 | app.group('/admin', adminRouter); 18 | 19 | app.group('/guest', guestRouter); 20 | 21 | await app.listen(); 22 | } 23 | -------------------------------------------------------------------------------- /pharaoh_examples/lib/serve_files_1/index.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:pharaoh/pharaoh.dart'; 4 | 5 | final app = Pharaoh(); 6 | 7 | void main() async { 8 | /// path to where the files are stored on disk 9 | final publicDir = '${Directory.current.path}/public/web_demo_1'; 10 | 11 | app.get('/', (req, res) async { 12 | final file = File('$publicDir/index.html'); 13 | final exists = await file.exists(); 14 | if (!exists) { 15 | return res.status(404).send('"Cant find that file, sorry!"'); 16 | } 17 | return res.type(ContentType.html).send(file.openRead()); 18 | }); 19 | 20 | /// /files/* is accessed via req.params[*] 21 | /// but here we name it 22 | app.get('/files/', (req, res) async { 23 | final pathToFile = req.params['file']; 24 | final file = File('$publicDir/files/$pathToFile'); 25 | final exists = await file.exists(); 26 | if (!exists) { 27 | return res.status(404).send('"Cant find that file, sorry!"'); 28 | } 29 | 30 | return res.send(file.openRead()); 31 | }); 32 | 33 | await app.listen(port: 3000); 34 | } 35 | -------------------------------------------------------------------------------- /pharaoh_examples/lib/serve_files_2/index.dart: -------------------------------------------------------------------------------- 1 | import 'package:pharaoh/pharaoh.dart'; 2 | import 'package:shelf_static/shelf_static.dart'; 3 | import 'package:shelf_cors_headers/shelf_cors_headers.dart'; 4 | 5 | final app = Pharaoh(); 6 | 7 | final serveStatic = createStaticHandler( 8 | 'public/web_demo_2', 9 | defaultDocument: 'index.html', 10 | ); 11 | 12 | final cors = corsHeaders(); 13 | 14 | void main() async { 15 | app.useRequestHook(logRequestHook); 16 | 17 | app.use(useShelfMiddleware(cors)); 18 | 19 | app.use(useShelfMiddleware(serveStatic)); 20 | 21 | await app.listen(); 22 | } 23 | -------------------------------------------------------------------------------- /pharaoh_examples/lib/shelf_middleware/cors.dart: -------------------------------------------------------------------------------- 1 | import 'package:pharaoh/pharaoh.dart'; 2 | import 'package:shelf_cors_headers/shelf_cors_headers.dart'; 3 | 4 | final app = Pharaoh(); 5 | 6 | void main() async { 7 | /// Using shelf_cors_header with Pharaoh 8 | app.use(useShelfMiddleware(corsHeaders())); 9 | 10 | app.get('/', (req, res) => res.json(req.headers)); 11 | 12 | await app.listen(); 13 | } 14 | -------------------------------------------------------------------------------- /pharaoh_examples/lib/shelf_middleware/helmet.dart: -------------------------------------------------------------------------------- 1 | import 'package:pharaoh/pharaoh.dart'; 2 | import 'package:shelf_helmet/shelf_helmet.dart'; 3 | 4 | final app = Pharaoh(); 5 | 6 | void main() async { 7 | /// Using shelf_helmet with Pharaoh 8 | app.use(useShelfMiddleware(helmet())); 9 | 10 | app.get('/', (req, res) => res.json(req.headers)); 11 | 12 | await app.listen(); 13 | } 14 | -------------------------------------------------------------------------------- /pharaoh_examples/public/web_demo_1/.pubignore: -------------------------------------------------------------------------------- 1 | files/ -------------------------------------------------------------------------------- /pharaoh_examples/public/web_demo_1/files/CCTV大赛上海分赛区.txt: -------------------------------------------------------------------------------- 1 | Only for test. 2 | The file name is faked. -------------------------------------------------------------------------------- /pharaoh_examples/public/web_demo_1/files/amazing.txt: -------------------------------------------------------------------------------- 1 | what an amazing download -------------------------------------------------------------------------------- /pharaoh_examples/public/web_demo_1/files/notes/groceries.txt: -------------------------------------------------------------------------------- 1 | * milk 2 | * eggs 3 | * bread 4 | -------------------------------------------------------------------------------- /pharaoh_examples/public/web_demo_1/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pharaoh_examples/public/web_demo_2/dart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekeyz/pharaoh/fd75d39fe0c70f949d088d60c8d2168d5101f0df/pharaoh_examples/public/web_demo_2/dart.png -------------------------------------------------------------------------------- /pharaoh_examples/public/web_demo_2/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekeyz/pharaoh/fd75d39fe0c70f949d088d60c8d2168d5101f0df/pharaoh_examples/public/web_demo_2/favicon.ico -------------------------------------------------------------------------------- /pharaoh_examples/public/web_demo_2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | shelf_static 7 | 8 | 9 |

Hello, shelf_static!

10 | Dart logo 11 | 12 | 13 | -------------------------------------------------------------------------------- /pharaoh_examples/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: pharaoh_examples 2 | description: A set of examples showing effective use of Pharaoh. 3 | version: 1.0.0 4 | publish_to: none 5 | 6 | environment: 7 | sdk: ^3.0.0 8 | 9 | dependencies: 10 | pharaoh: 11 | shelf_static: ^1.1.2 12 | shelf_helmet: ^2.1.1 13 | shelf_cors_headers: ^0.1.5 14 | 15 | dev_dependencies: 16 | lints: ^3.0.0 17 | test: ^1.24.9 18 | spookie: 19 | -------------------------------------------------------------------------------- /pharaoh_examples/test/api_service_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:pharaoh_examples/api_service/index.dart' as apisvc; 2 | import 'package:spookie/spookie.dart'; 3 | 4 | void main() async { 5 | group('api_service_example', () { 6 | late Spookie appTester; 7 | 8 | setUpAll(() async { 9 | apisvc.main([0]); 10 | appTester = await request(apisvc.app); 11 | }); 12 | 13 | tearDownAll(() => apisvc.app.shutdown()); 14 | 15 | group('should return json error message', () { 16 | test('when `api-key` not provided', () async { 17 | await appTester 18 | .get('/api/users') 19 | .expectStatus(400) 20 | .expectJsonBody('API key required') 21 | .test(); 22 | }); 23 | 24 | test('when `api-key` is invalid', () async { 25 | await appTester 26 | .get('/api/users?api-key=asfas') 27 | .expectStatus(401) 28 | .expectJsonBody('Invalid API key') 29 | .test(); 30 | }); 31 | }); 32 | 33 | group('when `api-key` is provided and valid', () { 34 | test('should return users', () async { 35 | final result = [ 36 | {'name': 'tobi'}, 37 | {'name': 'loki'}, 38 | {'name': 'jane'} 39 | ]; 40 | 41 | await appTester 42 | .get('/api/users?api-key=foo') 43 | .expectStatus(200) 44 | .expectJsonBody(result) 45 | .test(); 46 | }); 47 | 48 | group('and route is get repos for :name', () { 49 | test('should return repos when found', () async { 50 | const result = [ 51 | {'name': 'express', 'url': "https://github.com/expressjs/express"}, 52 | {'name': 'stylus', 'url': "https://github.com/learnboost/stylus"}, 53 | ]; 54 | 55 | await appTester 56 | .get('/api/user/tobi/repos?api-key=foo') 57 | .expectStatus(200) 58 | .expectJsonBody(result) 59 | .test(); 60 | }); 61 | 62 | test('should return notFound when :name not found', () async { 63 | await appTester 64 | .get('/api/user/chima/repos?api-key=foo') 65 | .expectStatus(404) 66 | .expectJsonBody({"error": "Not found"}).test(); 67 | }); 68 | }); 69 | 70 | test('should return repos', () async { 71 | const result = [ 72 | {'name': 'express', 'url': "https://github.com/expressjs/express"}, 73 | {'name': 'stylus', 'url': "https://github.com/learnboost/stylus"}, 74 | {'name': 'cluster', 'url': "https://github.com/learnboost/cluster"}, 75 | ]; 76 | 77 | await appTester 78 | .get('/api/repos?api-key=foo') 79 | .expectStatus(200) 80 | .expectJsonBody(result) 81 | .test(); 82 | }); 83 | }); 84 | }); 85 | } 86 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: pharaoh_workspace 2 | repository: https://github.com/codekeyz/pharaoh 3 | 4 | environment: 5 | sdk: ^3.0.0 6 | 7 | dev_dependencies: 8 | melos: ^3.2.0 9 | --------------------------------------------------------------------------------