├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── Bug_report.md │ ├── Documentation.md │ └── Feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── main.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmrc ├── .prettierrc ├── .vscode ├── extensions.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── _config.yml ├── index.md └── modules │ ├── formatter.ts.md │ ├── helpers.ts.md │ ├── index.md │ ├── index.ts.md │ ├── matcher.ts.md │ ├── parser.ts.md │ └── route.ts.md ├── dtslint ├── index.d.ts ├── index.ts └── tsconfig.json ├── jest.config.ts ├── package-lock.json ├── package.json ├── src ├── formatter.ts ├── helpers.ts ├── index.ts ├── matcher.ts ├── parser.ts └── route.ts ├── test ├── formatter.test.ts ├── index.test.ts ├── matcher.spec.ts ├── parser.test.ts └── route.test.ts ├── tsconfig.build-es6.json ├── tsconfig.build.json └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | es6 4 | coverage 5 | dtslint 6 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | 4 | "parser": "@typescript-eslint/parser", 5 | "parserOptions": { 6 | "project": "./tsconfig.json" 7 | }, 8 | 9 | "plugins": ["@typescript-eslint", "deprecation", "import", "simple-import-sort"], 10 | 11 | "extends": [ 12 | "eslint:recommended", 13 | "plugin:@typescript-eslint/eslint-recommended", 14 | "plugin:@typescript-eslint/recommended", 15 | "prettier" 16 | ], 17 | 18 | "rules": { 19 | "@typescript-eslint/array-type": ["warn", { "default": "generic", "readonly": "generic" }], 20 | "@typescript-eslint/ban-ts-comment": "off", 21 | "@typescript-eslint/ban-types": "off", 22 | "@typescript-eslint/member-delimiter-style": 0, 23 | "@typescript-eslint/no-empty-interface": "off", 24 | "@typescript-eslint/no-explicit-any": "off", 25 | "@typescript-eslint/no-inferrable-types": ["error", { "ignoreParameters": true }], 26 | "@typescript-eslint/no-non-null-assertion": "off", 27 | "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], 28 | "@typescript-eslint/prefer-as-const": "off", 29 | "@typescript-eslint/prefer-readonly": "warn", 30 | // "deprecation/deprecation": "off", 31 | "import/first": "error", 32 | "import/newline-after-import": "error", 33 | "import/no-cycle": "error", 34 | "import/no-duplicates": "error", 35 | "import/no-unresolved": "off", 36 | "import/order": "off", 37 | "no-unused-vars": "off", 38 | "prefer-rest-params": "off", 39 | "prefer-spread": "off", 40 | "simple-import-sort/imports": "error" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: Create a report to help make fp-ts-routing better 4 | --- 5 | 6 | ## 🐛 Bug report 7 | 8 | ### Current Behavior 9 | 10 | 11 | 12 | ### Expected behavior 13 | 14 | 15 | 16 | ### Reproducible example 17 | 18 | ### Suggested solution(s) 19 | 20 | 21 | 22 | ### Additional context 23 | 24 | 25 | 26 | ### Your environment 27 | 28 | Which versions of fp-ts-routing are affected by this issue? Did this work in previous versions of fp-ts-routing? 29 | 30 | 31 | 32 | | Software | Version(s) | 33 | | ------------- | ---------- | 34 | | fp-ts-routing | | 35 | | fp-ts | | 36 | | io-ts | | 37 | | TypeScript | | 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Documentation" 3 | about: Improvements or suggestions of fp-ts-routing documentation 4 | --- 5 | 6 | ## 📖 Documentation 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680Feature request" 3 | about: Suggest an idea for fp-ts-routing 4 | --- 5 | 6 | ## 🚀 Feature request 7 | 8 | ### Current Behavior 9 | 10 | 11 | 12 | ### Desired Behavior 13 | 14 | 15 | 16 | ### Suggested Solution 17 | 18 | 19 | 20 | 21 | 22 | ### Who does this impact? Who is this for? 23 | 24 | 25 | 26 | ### Describe alternatives you've considered 27 | 28 | 29 | 30 | ### Additional context 31 | 32 | 33 | 34 | ### Your environment 35 | 36 | 37 | 38 | | Software | Version(s) | 39 | | ------------- | ---------- | 40 | | fp-ts-routing | | 41 | | fp-ts | | 42 | | io-ts | | 43 | | TypeScript | | 44 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Before submitting a pull request,** please make sure the following is done: 2 | 3 | - Fork [the repository](https://github.com/gcanti/fp-ts-routing) and create your branch from `master`. 4 | - Run `npm install` in the repository root. 5 | - If you've fixed a bug or added code that should be tested, add tests! 6 | - Ensure the test suite passes (`npm test`). 7 | 8 | **Note**. If you've fixed a bug please link the related issue or, if missing, open an issue before sending a PR. 9 | 10 | **Note**. If you find a typo in the **documentation**, make sure to modify the corresponding source (docs are generated). 11 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [16.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | cache: 'npm' 27 | - run: npm ci 28 | - run: npm test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules 3 | lib 4 | es6 5 | dev 6 | coverage 7 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run docs:update && npx pretty-quick --staged 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | git-tag-version = false 2 | tag-version-prefix = 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "printWidth": 120, 5 | "trailingComma": "none" 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[javascript]": { 3 | "editor.formatOnSave": true 4 | }, 5 | "[javascriptreact]": { 6 | "editor.formatOnSave": true 7 | }, 8 | "[json]": { 9 | "editor.formatOnSave": true 10 | }, 11 | "[markdown]": { 12 | "editor.wordWrap": "wordWrapColumn", 13 | "editor.quickSuggestions": { 14 | "comments": "on", 15 | "strings": "on", 16 | "other": "on" 17 | }, 18 | "editor.formatOnSave": true 19 | }, 20 | "[typescript]": { 21 | "editor.formatOnSave": true 22 | }, 23 | "[typescriptreact]": { 24 | "editor.formatOnSave": true 25 | }, 26 | "editor.codeActionsOnSave": { 27 | "source.fixAll.eslint": true 28 | }, 29 | "eslint.validate": ["javascript", "javascriptreact", "html", "typescript", "typescriptreact"], 30 | "typescript.reportStyleChecksAsWarnings": false, 31 | "typescript.tsdk": "node_modules/typescript/lib", 32 | "typescript.validate.enable": true 33 | } 34 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "check", 7 | "problemMatcher": ["$tsc"], 8 | "presentation": { 9 | "clear": true, 10 | "revealProblems": "onProblem" 11 | } 12 | }, 13 | { 14 | "type": "npm", 15 | "script": "lint", 16 | "problemMatcher": ["$eslint-stylish"], 17 | "presentation": { 18 | "clear": true, 19 | "revealProblems": "onProblem" 20 | } 21 | }, 22 | { 23 | "type": "npm", 24 | "script": "prettier:fix", 25 | "problemMatcher": [], 26 | "presentation": { 27 | "clear": true, 28 | "revealProblems": "onProblem" 29 | } 30 | }, 31 | { 32 | "type": "npm", 33 | "script": "dtslint", 34 | "problemMatcher": [], 35 | "presentation": { 36 | "clear": true, 37 | "revealProblems": "onProblem" 38 | } 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | > **Tags:** 4 | > 5 | > - [New Feature] 6 | > - [Bug Fix] 7 | > - [Breaking Change] 8 | > - [Documentation] 9 | > - [Internal] 10 | > - [Polish] 11 | > - [Experimental] 12 | 13 | **Note**: Gaps between patch versions are faulty/broken releases. **Note**: A feature tagged as Experimental is in a 14 | high state of flux, you're at risk of it changing without notice. 15 | 16 | # 0.7.0 17 | 18 | - **Polish** 19 | - Type `Route#toString` as `/${string}` 20 | 21 | # 0.6.0 22 | 23 | - **New Feature** 24 | - Pipeable methods for parsers and formatters, #63 (@StefanoMagrassi) 25 | - **Bug Fix** 26 | - Leading slashes, #65 (@StefanoMagrassi) 27 | - **Polish** 28 | - query fail with array, #60 (@StefanoMagrassi) 29 | - **Internal** 30 | - Node.js querystring and url dependencies, #58 (@StefanoMagrassi) 31 | - Remove deprecation, #72 (@StefanoMagrassi) 32 | - Drop TSLint in favor of ESLint, #73 (@StefanoMagrassi) 33 | 34 | # 0.5.4 35 | 36 | - **Polish** 37 | - make `query` work with partial codecs, #54 (@anilanar) 38 | 39 | # 0.5.3 40 | 41 | - **Bug Fix** 42 | - don't set `target: es6` in `tsconfig.build-es6.json` (@gcanti) 43 | 44 | # 0.5.2 45 | 46 | - **Bug Fix** 47 | - rewrite es6 imports (@gcanti) 48 | 49 | # 0.5.1 50 | 51 | - **New Feature** 52 | - add `Parser` monoid instance (@mlegenhausen) 53 | - add `Parser` monad, alternative instance and related top-level data-last functions (@gcanti) 54 | - add `Formatter` contravariant instance and related top-level data-last functions (@gcanti) 55 | - `Match` 56 | - add top-level data-last function `imap` (@gcanti) 57 | - add top-level data-last function `then` (@gcanti) 58 | 59 | # 0.5.0 60 | 61 | - **Breaking Change** 62 | - upgrade to `fp-ts@2.0.1` and `io-ts@2.0.0` (@gcanti) 63 | - move `fp-ts@2.0.1` and `io-ts@2.0.0` to `peerDependencies` (@gcanti) 64 | 65 | # 0.4.4 66 | 67 | - **Bug Fix** 68 | - remove `fp-ts@2` from dependencies (@gcanti) 69 | 70 | # 0.4.3 71 | 72 | - **Bug Fix** 73 | - move `fp-ts` back to dependencies (@gcanti) 74 | 75 | # 0.4.2 76 | 77 | - **New Feature** 78 | - make `fp-ts-routing` compatible with both `fp-ts@1.x` and `fp-ts@2.x` (@gcanti) 79 | 80 | # 0.4.1 81 | 82 | - **Polish** 83 | - better workaround for #37 (@gcanti) 84 | 85 | # 0.4.0 86 | 87 | - **Breaking Change** 88 | - remove `null` from `Query` (@Eldow) 89 | - discard `undefined` parameters from route params (@Eldow) 90 | 91 | # 0.3.9 92 | 93 | Make `fp-ts-routing` compatible with `typescript@3.2.0-rc`, closes #34 (@sledorze) 94 | 95 | # 0.3.8 96 | 97 | - **New Feature** 98 | - `Query` now accepts `null`s, closes #32 (@sledorze) 99 | 100 | # 0.3.7 101 | 102 | Make `fp-ts-routing` compatible with `io-ts-types@0.4.0+`, closes #30 (@gcanti) 103 | 104 | # 0.3.6 105 | 106 | - **New Feature** 107 | - add `succeed` primitive (@sledorze) 108 | 109 | # 0.3.5 110 | 111 | - **Internal** 112 | - upgrade to latest `io-ts-types` (@gcanti) 113 | 114 | # 0.3.4 115 | 116 | - **New Feature** 117 | - add option to disable URI encoding/decoding, closes #18 (@gcanti) 118 | 119 | # 0.3.3 120 | 121 | - **New Feature** 122 | - add `format` function (@gcanti) 123 | - **Bug Fix** 124 | - decode/encode pathname parts, fix #12 (@gcanti) 125 | - **Inernal** 126 | - remove `Route.prototype.inspect` (@gcanti) 127 | 128 | # 0.3.2 129 | 130 | - **Internal** 131 | - upgrade to latest versions, fix #8 (@gcanti) 132 | - make type parameters more strict with `extends object` (@gcanti) 133 | 134 | # 0.3.1 135 | 136 | - **New Feature** 137 | - statically avoid overlapping keys (@gcanti) 138 | 139 | # 0.3.0 140 | 141 | - **Breaking Change** 142 | - remove `format` argument from `type` (@gcanti) 143 | - **Bug Fix** 144 | - serialize query in `query` (@gcanti) 145 | - **Internal** 146 | - upgrade to latest versions (@gcanti) 147 | 148 | # 0.2.1 149 | 150 | - **Internal** 151 | - pin `io-ts`, `io-ts-types` versions (@gcanti) 152 | 153 | # 0.2.0 154 | 155 | - **Breaking Change** 156 | - upgrade to io-ts 0.9.x (@gcanti) 157 | - do not re-export `IntegerFromString` (@gcanti) 158 | 159 | # 0.1.0 160 | 161 | - **Breaking Change** 162 | - upgrade to fp-ts 0.6 and io-ts 0.8 (@gcanti) 163 | 164 | # 0.0.1 165 | 166 | Initial release 167 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Giulio Canti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | To install the stable version: 4 | 5 | ```sh 6 | npm i fp-ts-routing 7 | ``` 8 | 9 | # TypeScript compatibility 10 | 11 | | `fp-ts-routing` version | required `typescript` version | 12 | | ----------------------- | -------------------------------------------------------------- | 13 | | 0.7.0+ | 4.1+ | 14 | | <= 0.6.0 | tested against TypeScript 3.5.2 but should run with 3.2.2+ too | 15 | 16 | # Usage 17 | 18 | ```ts 19 | // 20 | // locations 21 | // 22 | 23 | interface Home { 24 | readonly _tag: 'Home' 25 | } 26 | 27 | interface User { 28 | readonly _tag: 'User' 29 | readonly id: number 30 | } 31 | 32 | interface Invoice { 33 | readonly _tag: 'Invoice' 34 | readonly userId: number 35 | readonly invoiceId: number 36 | } 37 | 38 | interface NotFound { 39 | readonly _tag: 'NotFound' 40 | } 41 | 42 | type Location = Home | User | Invoice | NotFound 43 | 44 | const home: Location = { _tag: 'Home' } 45 | 46 | const user = (id: number): Location => ({ _tag: 'User', id }) 47 | 48 | const invoice = (userId: number, invoiceId: number): Location => ({ _tag: 'Invoice', userId, invoiceId }) 49 | 50 | const notFound: Location = { _tag: 'NotFound' } 51 | 52 | // matches 53 | const defaults = end 54 | const homeMatch = lit('home').then(end) 55 | const userIdMatch = lit('users').then(int('userId')) 56 | const userMatch = userIdMatch.then(end) 57 | const invoiceMatch = userIdMatch.then(lit('invoice')).then(int('invoiceId')).then(end) 58 | 59 | // router 60 | const router = zero() 61 | .alt(defaults.parser.map(() => home)) 62 | .alt(homeMatch.parser.map(() => home)) 63 | .alt(userMatch.parser.map(({ userId }) => user(userId))) 64 | .alt(invoiceMatch.parser.map(({ userId, invoiceId }) => invoice(userId, invoiceId))) 65 | 66 | // helper 67 | const parseLocation = (s: string): Location => parse(router, Route.parse(s), notFound) 68 | 69 | import * as assert from 'assert' 70 | 71 | // 72 | // parsers 73 | // 74 | 75 | assert.strictEqual(parseLocation('/'), home) 76 | assert.strictEqual(parseLocation('/home'), home) 77 | assert.deepEqual(parseLocation('/users/1'), user(1)) 78 | assert.deepEqual(parseLocation('/users/1/invoice/2'), invoice(1, 2)) 79 | assert.strictEqual(parseLocation('/foo'), notFound) 80 | 81 | // 82 | // formatters 83 | // 84 | 85 | assert.strictEqual(format(userMatch.formatter, { userId: 1 }), '/users/1') 86 | assert.strictEqual(format(invoiceMatch.formatter, { userId: 1, invoiceId: 2 }), '/users/1/invoice/2') 87 | ``` 88 | 89 | # Documentation 90 | 91 | - [API Reference](https://gcanti.github.io/fp-ts-routing) 92 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | remote_theme: pmarsceill/just-the-docs 2 | 3 | # Enable or disable the site search 4 | search_enabled: true 5 | 6 | # Aux links for the upper right navigation 7 | aux_links: 8 | 'fp-ts-routing on GitHub': 9 | - 'https://github.com/gcanti/fp-ts-routing' 10 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Home 3 | nav_order: 1 4 | --- 5 | 6 | # TypeScript compatibility 7 | 8 | The stable version is tested against TypeScript 3.2.2 9 | 10 | # Usage 11 | 12 | ```ts 13 | // 14 | // locations 15 | // 16 | 17 | interface Home { 18 | readonly _tag: 'Home' 19 | } 20 | 21 | interface User { 22 | readonly _tag: 'User' 23 | readonly id: number 24 | } 25 | 26 | interface Invoice { 27 | readonly _tag: 'Invoice' 28 | readonly userId: number 29 | readonly invoiceId: number 30 | } 31 | 32 | interface NotFound { 33 | readonly _tag: 'NotFound' 34 | } 35 | 36 | type Location = Home | User | Invoice | NotFound 37 | 38 | const home: Location = { _tag: 'Home' } 39 | 40 | const user = (id: number): Location => ({ _tag: 'User', id }) 41 | 42 | const invoice = (userId: number, invoiceId: number): Location => ({ _tag: 'Invoice', userId, invoiceId }) 43 | 44 | const notFound: Location = { _tag: 'NotFound' } 45 | 46 | // matches 47 | const defaults = end 48 | const homeMatch = lit('home').then(end) 49 | const userIdMatch = lit('users').then(int('userId')) 50 | const userMatch = userIdMatch.then(end) 51 | const invoiceMatch = userIdMatch.then(lit('invoice')).then(int('invoiceId')).then(end) 52 | 53 | // router 54 | const router = zero() 55 | .alt(defaults.parser.map(() => home)) 56 | .alt(homeMatch.parser.map(() => home)) 57 | .alt(userMatch.parser.map(({ userId }) => user(userId))) 58 | .alt(invoiceMatch.parser.map(({ userId, invoiceId }) => invoice(userId, invoiceId))) 59 | 60 | // helper 61 | const parseLocation = (s: string): Location => parse(router, Route.parse(s), notFound) 62 | 63 | import * as assert from 'assert' 64 | 65 | // 66 | // parsers 67 | // 68 | 69 | assert.strictEqual(parseLocation('/'), home) 70 | assert.strictEqual(parseLocation('/home'), home) 71 | assert.deepEqual(parseLocation('/users/1'), user(1)) 72 | assert.deepEqual(parseLocation('/users/1/invoice/2'), invoice(1, 2)) 73 | assert.strictEqual(parseLocation('/foo'), notFound) 74 | 75 | // 76 | // formatters 77 | // 78 | 79 | assert.strictEqual(format(userMatch.formatter, { userId: 1 }), '/users/1') 80 | assert.strictEqual(format(invoiceMatch.formatter, { userId: 1, invoiceId: 2 }), '/users/1/invoice/2') 81 | ``` 82 | 83 | # Defining new matches via `io-ts` types 84 | 85 | The function `type` allows to define a new `Match` from a [io-ts](https://github.com/gcanti/io-ts) runtime type 86 | 87 | ```ts 88 | type(k: K, type: t.Type): Match<{ [_ in K]: A }> 89 | ``` 90 | -------------------------------------------------------------------------------- /docs/modules/formatter.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: formatter.ts 3 | nav_order: 1 4 | parent: Modules 5 | --- 6 | 7 | ## formatter overview 8 | 9 | Added in v0.6.0 10 | 11 | --- 12 | 13 |

Table of contents

14 | 15 | - [formatters](#formatters) 16 | - [Formatter (class)](#formatter-class) 17 | - [contramap (method)](#contramap-method) 18 | - [then (method)](#then-method) 19 | - [\_A (property)](#_a-property) 20 | - [contramap](#contramap) 21 | - [format](#format) 22 | - [formatter](#formatter) 23 | - [then](#then) 24 | 25 | --- 26 | 27 | # formatters 28 | 29 | ## Formatter (class) 30 | 31 | **Signature** 32 | 33 | ```ts 34 | export declare class Formatter
{ 35 | constructor(readonly run: (r: Route, a: A) => Route) 36 | } 37 | ``` 38 | 39 | Added in v0.4.0 40 | 41 | ### contramap (method) 42 | 43 | **Signature** 44 | 45 | ```ts 46 | contramap(f: (b: B) => A): Formatter 47 | ``` 48 | 49 | Added in v0.4.0 50 | 51 | ### then (method) 52 | 53 | **Signature** 54 | 55 | ```ts 56 | then(that: Formatter & Formatter>): Formatter 57 | ``` 58 | 59 | Added in v0.4.0 60 | 61 | ### \_A (property) 62 | 63 | **Signature** 64 | 65 | ```ts 66 | readonly _A: A 67 | ``` 68 | 69 | Added in v0.4.0 70 | 71 | ## contramap 72 | 73 | **Signature** 74 | 75 | ```ts 76 | export declare const contramap: (f: (b: B) => A) => (fa: Formatter) => Formatter 77 | ``` 78 | 79 | Added in v0.5.1 80 | 81 | ## format 82 | 83 | **Signature** 84 | 85 | ```ts 86 | export declare const format: (formatter: Formatter, a: A, encode?: boolean) => string 87 | ``` 88 | 89 | Added in v0.4.0 90 | 91 | ## formatter 92 | 93 | **Signature** 94 | 95 | ```ts 96 | export declare const formatter: Contravariant1<'fp-ts-routing/Formatter'> 97 | ``` 98 | 99 | Added in v0.5.1 100 | 101 | ## then 102 | 103 | **Signature** 104 | 105 | ```ts 106 | export declare const then: ( 107 | fb: Formatter 108 | ) => (fa: Formatter & Formatter>) => Formatter 109 | ``` 110 | 111 | Added in v0.6.0 112 | -------------------------------------------------------------------------------- /docs/modules/helpers.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: helpers.ts 3 | nav_order: 2 4 | parent: Modules 5 | --- 6 | 7 | ## helpers overview 8 | 9 | Added in v0.6.0 10 | 11 | --- 12 | 13 |

Table of contents

14 | 15 | - [helpers](#helpers) 16 | - [RowLacks (type alias)](#rowlacks-type-alias) 17 | 18 | --- 19 | 20 | # helpers 21 | 22 | ## RowLacks (type alias) 23 | 24 | Encodes the constraint that a given object `O` 25 | does not contain specific keys `K` 26 | 27 | **Signature** 28 | 29 | ```ts 30 | export type RowLacks = O & Record, never> 31 | ``` 32 | 33 | Added in v0.4.0 34 | -------------------------------------------------------------------------------- /docs/modules/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Modules 3 | has_children: true 4 | permalink: /docs/modules 5 | nav_order: 2 6 | --- 7 | -------------------------------------------------------------------------------- /docs/modules/index.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: index.ts 3 | nav_order: 3 4 | parent: Modules 5 | --- 6 | 7 | ## index overview 8 | 9 | Added in v0.4.0 10 | 11 | --- 12 | 13 |

Table of contents

14 | 15 | - [formatters](#formatters) 16 | - [Formatter](#formatter) 17 | - [contramap](#contramap) 18 | - [format](#format) 19 | - [formatter](#formatter) 20 | - [helpers](#helpers) 21 | - [RowLacks](#rowlacks) 22 | - [matchers](#matchers) 23 | - [IntegerFromString](#integerfromstring) 24 | - [Match](#match) 25 | - [end](#end) 26 | - [imap](#imap) 27 | - [int](#int) 28 | - [lit](#lit) 29 | - [query](#query) 30 | - [str](#str) 31 | - [succeed](#succeed) 32 | - [then](#then) 33 | - [type](#type) 34 | - [parsers](#parsers) 35 | - [Parser](#parser) 36 | - [alt](#alt) 37 | - [ap](#ap) 38 | - [apFirst](#apfirst) 39 | - [apSecond](#apsecond) 40 | - [chain](#chain) 41 | - [chainFirst](#chainfirst) 42 | - [flatten](#flatten) 43 | - [getParserMonoid](#getparsermonoid) 44 | - [map](#map) 45 | - [parse](#parse) 46 | - [parser](#parser) 47 | - [zero](#zero) 48 | 49 | --- 50 | 51 | # formatters 52 | 53 | ## Formatter 54 | 55 | **Signature** 56 | 57 | ```ts 58 | export declare const Formatter: typeof Formatter 59 | ``` 60 | 61 | Added in v0.4.0 62 | 63 | ## contramap 64 | 65 | **Signature** 66 | 67 | ```ts 68 | export declare const contramap: (f: (b: B) => A) => (fa: Formatter
) => Formatter 69 | ``` 70 | 71 | Added in v0.5.1 72 | 73 | ## format 74 | 75 | **Signature** 76 | 77 | ```ts 78 | export declare const format: (formatter: Formatter, a: A, encode?: boolean) => string 79 | ``` 80 | 81 | Added in v0.4.0 82 | 83 | ## formatter 84 | 85 | **Signature** 86 | 87 | ```ts 88 | export declare const formatter: Contravariant1<'fp-ts-routing/Formatter'> 89 | ``` 90 | 91 | Added in v0.5.1 92 | 93 | # helpers 94 | 95 | ## RowLacks 96 | 97 | **Signature** 98 | 99 | ```ts 100 | export declare const RowLacks: any 101 | ``` 102 | 103 | Added in v0.4.0 104 | 105 | # matchers 106 | 107 | ## IntegerFromString 108 | 109 | **Signature** 110 | 111 | ```ts 112 | export declare const IntegerFromString: Type 113 | ``` 114 | 115 | Added in v0.4.2 116 | 117 | ## Match 118 | 119 | **Signature** 120 | 121 | ```ts 122 | export declare const Match: typeof Match 123 | ``` 124 | 125 | Added in v0.4.0 126 | 127 | ## end 128 | 129 | **Signature** 130 | 131 | ```ts 132 | export declare const end: Match<{}> 133 | ``` 134 | 135 | Added in v0.4.0 136 | 137 | ## imap 138 | 139 | **Signature** 140 | 141 | ```ts 142 | export declare const imap: (f: (a: A) => B, g: (b: B) => A) => (ma: Match) => Match 143 | ``` 144 | 145 | Added in v0.5.1 146 | 147 | ## int 148 | 149 | **Signature** 150 | 151 | ```ts 152 | export declare const int: (k: K) => Match<{ [_ in K]: number }> 153 | ``` 154 | 155 | Added in v0.4.0 156 | 157 | ## lit 158 | 159 | **Signature** 160 | 161 | ```ts 162 | export declare const lit: (literal: string) => Match<{}> 163 | ``` 164 | 165 | Added in v0.4.0 166 | 167 | ## query 168 | 169 | **Signature** 170 | 171 | ```ts 172 | export declare const query: (type: Type, unknown>) => Match 173 | ``` 174 | 175 | Added in v0.4.0 176 | 177 | ## str 178 | 179 | **Signature** 180 | 181 | ```ts 182 | export declare const str: (k: K) => Match<{ [_ in K]: string }> 183 | ``` 184 | 185 | Added in v0.4.0 186 | 187 | ## succeed 188 | 189 | **Signature** 190 | 191 | ```ts 192 | export declare const succeed: (a: A) => Match 193 | ``` 194 | 195 | Added in v0.4.0 196 | 197 | ## then 198 | 199 | **Signature** 200 | 201 | ```ts 202 | export declare const then: (mb: Match) => (ma: Match & Match>) => Match 203 | ``` 204 | 205 | Added in v0.5.1 206 | 207 | ## type 208 | 209 | **Signature** 210 | 211 | ```ts 212 | export declare const type: (k: K, type: Type) => Match<{ [_ in K]: A }> 213 | ``` 214 | 215 | Added in v0.4.0 216 | 217 | # parsers 218 | 219 | ## Parser 220 | 221 | **Signature** 222 | 223 | ```ts 224 | export declare const Parser: typeof Parser 225 | ``` 226 | 227 | Added in v0.4.0 228 | 229 | ## alt 230 | 231 | **Signature** 232 | 233 | ```ts 234 | export declare const alt: (that: Lazy>) => (fa: Parser) => Parser 235 | ``` 236 | 237 | Added in v0.5.1 238 | 239 | ## ap 240 | 241 | **Signature** 242 | 243 | ```ts 244 | export declare const ap: (fa: Parser) => (fab: Parser<(a: A) => B>) => Parser 245 | ``` 246 | 247 | Added in v0.5.1 248 | 249 | ## apFirst 250 | 251 | **Signature** 252 | 253 | ```ts 254 | export declare const apFirst: (fb: Parser) => (fa: Parser) => Parser 255 | ``` 256 | 257 | Added in v0.5.1 258 | 259 | ## apSecond 260 | 261 | **Signature** 262 | 263 | ```ts 264 | export declare const apSecond: (fb: Parser) => (fa: Parser) => Parser 265 | ``` 266 | 267 | Added in v0.5.1 268 | 269 | ## chain 270 | 271 | **Signature** 272 | 273 | ```ts 274 | export declare const chain: (f: (a: A) => Parser) => (ma: Parser) => Parser 275 | ``` 276 | 277 | Added in v0.5.1 278 | 279 | ## chainFirst 280 | 281 | **Signature** 282 | 283 | ```ts 284 | export declare const chainFirst: (f: (a: A) => Parser) => (ma: Parser) => Parser 285 | ``` 286 | 287 | Added in v0.5.1 288 | 289 | ## flatten 290 | 291 | **Signature** 292 | 293 | ```ts 294 | export declare const flatten: (mma: Parser>) => Parser 295 | ``` 296 | 297 | Added in v0.5.1 298 | 299 | ## getParserMonoid 300 | 301 | **Signature** 302 | 303 | ```ts 304 | export declare const getParserMonoid: () => Monoid> 305 | ``` 306 | 307 | Added in v0.5.1 308 | 309 | ## map 310 | 311 | **Signature** 312 | 313 | ```ts 314 | export declare const map: (f: (a: A) => B) => (fa: Parser) => Parser 315 | ``` 316 | 317 | Added in v0.5.1 318 | 319 | ## parse 320 | 321 | **Signature** 322 | 323 | ```ts 324 | export declare const parse: (parser: Parser, r: Route, a: A) => A 325 | ``` 326 | 327 | Added in v0.4.0 328 | 329 | ## parser 330 | 331 | **Signature** 332 | 333 | ```ts 334 | export declare const parser: Monad1<'fp-ts-routing/Parser'> & Alternative1<'fp-ts-routing/Parser'> 335 | ``` 336 | 337 | Added in v0.5.1 338 | 339 | ## zero 340 | 341 | **Signature** 342 | 343 | ```ts 344 | export declare const zero: () => Parser 345 | ``` 346 | 347 | Added in v0.4.0 348 | -------------------------------------------------------------------------------- /docs/modules/matcher.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: matcher.ts 3 | nav_order: 4 4 | parent: Modules 5 | --- 6 | 7 | ## matcher overview 8 | 9 | Added in v0.6.0 10 | 11 | --- 12 | 13 |

Table of contents

14 | 15 | - [matchers](#matchers) 16 | - [IntegerFromString](#integerfromstring) 17 | - [Match (class)](#match-class) 18 | - [imap (method)](#imap-method) 19 | - [then (method)](#then-method) 20 | - [\_A (property)](#_a-property) 21 | - [end](#end) 22 | - [imap](#imap) 23 | - [int](#int) 24 | - [lit](#lit) 25 | - [query](#query) 26 | - [str](#str) 27 | - [succeed](#succeed) 28 | - [then](#then) 29 | - [type](#type) 30 | 31 | --- 32 | 33 | # matchers 34 | 35 | ## IntegerFromString 36 | 37 | **Signature** 38 | 39 | ```ts 40 | export declare const IntegerFromString: Type 41 | ``` 42 | 43 | Added in v0.4.2 44 | 45 | ## Match (class) 46 | 47 | **Signature** 48 | 49 | ```ts 50 | export declare class Match
{ 51 | constructor(readonly parser: Parser, readonly formatter: Formatter) 52 | } 53 | ``` 54 | 55 | Added in v0.4.0 56 | 57 | ### imap (method) 58 | 59 | **Signature** 60 | 61 | ```ts 62 | imap(f: (a: A) => B, g: (b: B) => A): Match 63 | ``` 64 | 65 | Added in v0.4.0 66 | 67 | ### then (method) 68 | 69 | **Signature** 70 | 71 | ```ts 72 | then(that: Match & Match>): Match 73 | ``` 74 | 75 | Added in v0.4.0 76 | 77 | ### \_A (property) 78 | 79 | **Signature** 80 | 81 | ```ts 82 | readonly _A: A 83 | ``` 84 | 85 | Added in v0.4.0 86 | 87 | ## end 88 | 89 | `end` matches the end of a route 90 | 91 | **Signature** 92 | 93 | ```ts 94 | export declare const end: Match<{}> 95 | ``` 96 | 97 | Added in v0.4.0 98 | 99 | ## imap 100 | 101 | **Signature** 102 | 103 | ```ts 104 | export declare const imap: (f: (a: A) => B, g: (b: B) => A) => (ma: Match) => Match 105 | ``` 106 | 107 | Added in v0.5.1 108 | 109 | ## int 110 | 111 | `int` matches any integer path component 112 | 113 | **Signature** 114 | 115 | ```ts 116 | export declare const int: (k: K) => Match<{ [_ in K]: number }> 117 | ``` 118 | 119 | **Example** 120 | 121 | ```ts 122 | import { int, Route } from 'fp-ts-routing' 123 | import { some, none } from 'fp-ts/lib/Option' 124 | 125 | assert.deepStrictEqual(int('id').parser.run(Route.parse('/1')), some([{ id: 1 }, new Route([], {})])) 126 | assert.deepStrictEqual(int('id').parser.run(Route.parse('/a')), none) 127 | ``` 128 | 129 | Added in v0.4.0 130 | 131 | ## lit 132 | 133 | `lit(x)` will match exactly the path component `x` 134 | 135 | **Signature** 136 | 137 | ```ts 138 | export declare const lit: (literal: string) => Match<{}> 139 | ``` 140 | 141 | **Example** 142 | 143 | ```ts 144 | import { lit, Route } from 'fp-ts-routing' 145 | import { some, none } from 'fp-ts/lib/Option' 146 | 147 | assert.deepStrictEqual(lit('subview').parser.run(Route.parse('/subview/')), some([{}, new Route([], {})])) 148 | assert.deepStrictEqual(lit('subview').parser.run(Route.parse('/')), none) 149 | ``` 150 | 151 | Added in v0.4.0 152 | 153 | ## query 154 | 155 | Will match a querystring. 156 | 157 | **Note**. Use `io-ts`'s `strict` instead of `type` otherwise excess properties won't be removed. 158 | 159 | **Signature** 160 | 161 | ```ts 162 | export declare const query: (type: Type, unknown>) => Match 163 | ``` 164 | 165 | **Example** 166 | 167 | ```ts 168 | import * as t from 'io-ts' 169 | import { lit, str, query, Route } from 'fp-ts-routing' 170 | 171 | const route = lit('accounts') 172 | .then(str('accountId')) 173 | .then(lit('files')) 174 | .then(query(t.strict({ pathparam: t.string }))) 175 | .formatter.run(Route.empty, { accountId: 'testId', pathparam: '123' }) 176 | .toString() 177 | 178 | assert.strictEqual(route, '/accounts/testId/files?pathparam=123') 179 | ``` 180 | 181 | Added in v0.4.0 182 | 183 | ## str 184 | 185 | `str` matches any string path component 186 | 187 | **Signature** 188 | 189 | ```ts 190 | export declare const str: (k: K) => Match<{ [_ in K]: string }> 191 | ``` 192 | 193 | **Example** 194 | 195 | ```ts 196 | import { str, Route } from 'fp-ts-routing' 197 | import { some, none } from 'fp-ts/lib/Option' 198 | 199 | assert.deepStrictEqual(str('id').parser.run(Route.parse('/abc')), some([{ id: 'abc' }, new Route([], {})])) 200 | assert.deepStrictEqual(str('id').parser.run(Route.parse('/')), none) 201 | ``` 202 | 203 | Added in v0.4.0 204 | 205 | ## succeed 206 | 207 | `succeed` matches everything but consumes nothing 208 | 209 | **Signature** 210 | 211 | ```ts 212 | export declare const succeed: (a: A) => Match 213 | ``` 214 | 215 | Added in v0.4.0 216 | 217 | ## then 218 | 219 | **Signature** 220 | 221 | ```ts 222 | export declare const then: (mb: Match) => (ma: Match & Match>) => Match 223 | ``` 224 | 225 | Added in v0.5.1 226 | 227 | ## type 228 | 229 | `type` matches any io-ts type path component 230 | 231 | **Signature** 232 | 233 | ```ts 234 | export declare const type: (k: K, type: Type) => Match<{ [_ in K]: A }> 235 | ``` 236 | 237 | **Example** 238 | 239 | ```ts 240 | import * as t from 'io-ts' 241 | import { lit, type, Route } from 'fp-ts-routing' 242 | import { some, none } from 'fp-ts/lib/Option' 243 | 244 | const T = t.keyof({ 245 | a: null, 246 | b: null 247 | }) 248 | 249 | const match = lit('search').then(type('topic', T)) 250 | 251 | assert.deepStrictEqual(match.parser.run(Route.parse('/search/a')), some([{ topic: 'a' }, Route.empty])) 252 | assert.deepStrictEqual(match.parser.run(Route.parse('/search/b')), some([{ topic: 'b' }, Route.empty])) 253 | assert.deepStrictEqual(match.parser.run(Route.parse('/search/')), none) 254 | ``` 255 | 256 | Added in v0.4.0 257 | -------------------------------------------------------------------------------- /docs/modules/parser.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: parser.ts 3 | nav_order: 5 4 | parent: Modules 5 | --- 6 | 7 | ## parser overview 8 | 9 | Added in v0.6.0 10 | 11 | --- 12 | 13 |

Table of contents

14 | 15 | - [parsers](#parsers) 16 | - [Parser (class)](#parser-class) 17 | - [of (static method)](#of-static-method) 18 | - [map (method)](#map-method) 19 | - [ap (method)](#ap-method) 20 | - [chain (method)](#chain-method) 21 | - [alt (method)](#alt-method) 22 | - [then (method)](#then-method) 23 | - [\_A (property)](#_a-property) 24 | - [alt](#alt) 25 | - [ap](#ap) 26 | - [apFirst](#apfirst) 27 | - [apSecond](#apsecond) 28 | - [chain](#chain) 29 | - [chainFirst](#chainfirst) 30 | - [flatten](#flatten) 31 | - [getParserMonoid](#getparsermonoid) 32 | - [map](#map) 33 | - [parse](#parse) 34 | - [parser](#parser) 35 | - [then](#then) 36 | - [zero](#zero) 37 | 38 | --- 39 | 40 | # parsers 41 | 42 | ## Parser (class) 43 | 44 | **Signature** 45 | 46 | ```ts 47 | export declare class Parser
{ 48 | constructor(readonly run: (r: Route) => O.Option<[A, Route]>) 49 | } 50 | ``` 51 | 52 | Added in v0.4.0 53 | 54 | ### of (static method) 55 | 56 | **Signature** 57 | 58 | ```ts 59 | static of(a: A): Parser 60 | ``` 61 | 62 | Added in v0.4.0 63 | 64 | ### map (method) 65 | 66 | **Signature** 67 | 68 | ```ts 69 | map(f: (a: A) => B): Parser 70 | ``` 71 | 72 | Added in v0.4.0 73 | 74 | ### ap (method) 75 | 76 | **Signature** 77 | 78 | ```ts 79 | ap(fab: Parser<(a: A) => B>): Parser 80 | ``` 81 | 82 | Added in v0.4.0 83 | 84 | ### chain (method) 85 | 86 | **Signature** 87 | 88 | ```ts 89 | chain(f: (a: A) => Parser): Parser 90 | ``` 91 | 92 | Added in v0.4.0 93 | 94 | ### alt (method) 95 | 96 | **Signature** 97 | 98 | ```ts 99 | alt(that: Parser): Parser 100 | ``` 101 | 102 | Added in v0.4.0 103 | 104 | ### then (method) 105 | 106 | **Signature** 107 | 108 | ```ts 109 | then(that: Parser>): Parser 110 | ``` 111 | 112 | Added in v0.4.0 113 | 114 | ### \_A (property) 115 | 116 | **Signature** 117 | 118 | ```ts 119 | readonly _A: A 120 | ``` 121 | 122 | Added in v0.4.0 123 | 124 | ## alt 125 | 126 | **Signature** 127 | 128 | ```ts 129 | export declare const alt: (that: Lazy>) => (fa: Parser) => Parser 130 | ``` 131 | 132 | Added in v0.5.1 133 | 134 | ## ap 135 | 136 | **Signature** 137 | 138 | ```ts 139 | export declare const ap: (fa: Parser) => (fab: Parser<(a: A) => B>) => Parser 140 | ``` 141 | 142 | Added in v0.5.1 143 | 144 | ## apFirst 145 | 146 | **Signature** 147 | 148 | ```ts 149 | export declare const apFirst: (fb: Parser) => (fa: Parser) => Parser 150 | ``` 151 | 152 | Added in v0.5.1 153 | 154 | ## apSecond 155 | 156 | **Signature** 157 | 158 | ```ts 159 | export declare const apSecond: (fb: Parser) => (fa: Parser) => Parser 160 | ``` 161 | 162 | Added in v0.5.1 163 | 164 | ## chain 165 | 166 | **Signature** 167 | 168 | ```ts 169 | export declare const chain: (f: (a: A) => Parser) => (ma: Parser) => Parser 170 | ``` 171 | 172 | Added in v0.5.1 173 | 174 | ## chainFirst 175 | 176 | **Signature** 177 | 178 | ```ts 179 | export declare const chainFirst: (f: (a: A) => Parser) => (ma: Parser) => Parser 180 | ``` 181 | 182 | Added in v0.5.1 183 | 184 | ## flatten 185 | 186 | **Signature** 187 | 188 | ```ts 189 | export declare const flatten: (mma: Parser>) => Parser 190 | ``` 191 | 192 | Added in v0.5.1 193 | 194 | ## getParserMonoid 195 | 196 | **Signature** 197 | 198 | ```ts 199 | export declare const getParserMonoid: () => Monoid> 200 | ``` 201 | 202 | Added in v0.5.1 203 | 204 | ## map 205 | 206 | **Signature** 207 | 208 | ```ts 209 | export declare const map: (f: (a: A) => B) => (fa: Parser) => Parser 210 | ``` 211 | 212 | Added in v0.5.1 213 | 214 | ## parse 215 | 216 | **Signature** 217 | 218 | ```ts 219 | export declare const parse: (parser: Parser, r: Route, a: A) => A 220 | ``` 221 | 222 | Added in v0.4.0 223 | 224 | ## parser 225 | 226 | **Signature** 227 | 228 | ```ts 229 | export declare const parser: Monad1<'fp-ts-routing/Parser'> & Alternative1<'fp-ts-routing/Parser'> 230 | ``` 231 | 232 | Added in v0.5.1 233 | 234 | ## then 235 | 236 | **Signature** 237 | 238 | ```ts 239 | export declare const then: (fb: Parser) => (fa: Parser & Parser>) => Parser 240 | ``` 241 | 242 | Added in v0.6.0 243 | 244 | ## zero 245 | 246 | **Signature** 247 | 248 | ```ts 249 | export declare const zero: () => Parser 250 | ``` 251 | 252 | Added in v0.4.0 253 | -------------------------------------------------------------------------------- /docs/modules/route.ts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: route.ts 3 | nav_order: 6 4 | parent: Modules 5 | --- 6 | 7 | ## route overview 8 | 9 | Added in v0.6.0 10 | 11 | --- 12 | 13 |

Table of contents

14 | 15 | - [routes](#routes) 16 | - [Query (interface)](#query-interface) 17 | - [QueryValues (type alias)](#queryvalues-type-alias) 18 | - [Route (class)](#route-class) 19 | - [isEmpty (static method)](#isempty-static-method) 20 | - [parse (static method)](#parse-static-method) 21 | - [toString (method)](#tostring-method) 22 | 23 | --- 24 | 25 | # routes 26 | 27 | ## Query (interface) 28 | 29 | **Signature** 30 | 31 | ```ts 32 | export interface Query { 33 | [key: string]: QueryValues 34 | } 35 | ``` 36 | 37 | Added in v0.4.0 38 | 39 | ## QueryValues (type alias) 40 | 41 | **Signature** 42 | 43 | ```ts 44 | export type QueryValues = string | Array | undefined 45 | ``` 46 | 47 | Added in v0.4.0 48 | 49 | ## Route (class) 50 | 51 | **Signature** 52 | 53 | ```ts 54 | export declare class Route { 55 | constructor(readonly parts: Array, readonly query: Query) 56 | } 57 | ``` 58 | 59 | Added in v0.4.0 60 | 61 | ### isEmpty (static method) 62 | 63 | **Signature** 64 | 65 | ```ts 66 | static isEmpty(r: Route): boolean 67 | ``` 68 | 69 | Added in v0.4.0 70 | 71 | ### parse (static method) 72 | 73 | **Signature** 74 | 75 | ```ts 76 | static parse(s: string, decode: boolean = true): Route 77 | ``` 78 | 79 | Added in v0.4.0 80 | 81 | ### toString (method) 82 | 83 | **Signature** 84 | 85 | ```ts 86 | toString(encode: boolean = true): `/${string}` 87 | ``` 88 | 89 | Added in v0.4.0 90 | -------------------------------------------------------------------------------- /dtslint/index.d.ts: -------------------------------------------------------------------------------- 1 | // Minimum TypeScript Version: 3.8 2 | -------------------------------------------------------------------------------- /dtslint/index.ts: -------------------------------------------------------------------------------- 1 | import * as R from '../src' 2 | import * as t from 'io-ts' 3 | import { pipe } from 'fp-ts/lib/pipeable' 4 | 5 | // shouldn't type-check when using a duplicate key 6 | // @ts-expect-error 7 | R.str('a').then(R.str('a')) 8 | pipe( 9 | R.str('a'), 10 | // @ts-expect-error 11 | R.then(R.str('a')) 12 | ) 13 | 14 | declare const BadQuery: t.Type<{ a: string; b: number }, { a: string } & { b: number }> 15 | // @ts-expect-error 16 | R.query(BadQuery) 17 | 18 | const PartialQuery = t.partial({ a: t.string }) 19 | R.query(PartialQuery) 20 | 21 | const ExactPartialQuery = t.exact(t.partial({ a: t.string })) 22 | R.query(ExactPartialQuery) 23 | -------------------------------------------------------------------------------- /dtslint/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "target": "es5", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "lib": ["es2015", "dom"], 8 | "strict": true, 9 | "noImplicitReturns": false, 10 | "noUnusedLocals": false, 11 | "noUnusedParameters": false, 12 | "noFallthroughCasesInSwitch": true, 13 | "skipLibCheck": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { JestConfigWithTsJest } from 'ts-jest' 2 | 3 | const config: JestConfigWithTsJest = { 4 | preset: 'ts-jest', 5 | 6 | collectCoverage: true, 7 | coverageThreshold: { 8 | global: { 9 | branches: 100, 10 | functions: 100, 11 | lines: 100, 12 | statements: 100 13 | } 14 | }, 15 | moduleFileExtensions: ['ts', 'js'], 16 | roots: ['/test'], 17 | testEnvironment: 'node', 18 | transform: { 19 | '^.+\\.tsx?$': 'ts-jest' 20 | } 21 | } 22 | 23 | export default config 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fp-ts-routing", 3 | "version": "0.7.0", 4 | "description": "A type-safe routing library for TypeScript", 5 | "files": [ 6 | "lib", 7 | "es6" 8 | ], 9 | "main": "lib/index.js", 10 | "module": "es6/index.js", 11 | "typings": "lib/index.d.ts", 12 | "sideEffects": false, 13 | "engines": { 14 | "npm": ">= 6.14" 15 | }, 16 | "scripts": { 17 | "prepare": "husky install", 18 | "clean": "rm -rf lib/* es6/*", 19 | "prettier:fix": "prettier --write \"{src,test}/**/*.ts\"", 20 | "check": "tsc", 21 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx", 22 | "dtslint": "dtslint --expectOnly dtslint", 23 | "pretest": "npm run check && npm run lint && npm run dtslint", 24 | "test": "jest --ci", 25 | "mocha": "mocha -r ts-node/register test/*.ts", 26 | "prebuild": "npm run clean", 27 | "build": "tsc -p ./tsconfig.build.json && tsc -p ./tsconfig.build-es6.json", 28 | "postbuild": "import-path-rewrite", 29 | "prepublishOnly": "npm run build", 30 | "docs": "docs-ts", 31 | "docs:update": "npm run docs && git add docs" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/gcanti/fp-ts-routing.git" 36 | }, 37 | "author": "Giulio Canti ", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/gcanti/fp-ts-routing/issues" 41 | }, 42 | "homepage": "https://github.com/gcanti/fp-ts-routing", 43 | "peerDependencies": { 44 | "fp-ts": "^2.0.1", 45 | "io-ts": "^2.0.0" 46 | }, 47 | "devDependencies": { 48 | "@definitelytyped/dtslint": "^0.0.163", 49 | "@types/jest": "^29.5.2", 50 | "@types/node": "^16.18.36", 51 | "@typescript-eslint/eslint-plugin": "^5.59.11", 52 | "@typescript-eslint/parser": "^5.59.11", 53 | "docs-ts": "^0.8.0", 54 | "eslint": "^8.43.0", 55 | "eslint-config-prettier": "^8.8.0", 56 | "eslint-plugin-deprecation": "^1.4.1", 57 | "eslint-plugin-import": "^2.27.5", 58 | "eslint-plugin-simple-import-sort": "^10.0.0", 59 | "fp-ts": "^2.0.1", 60 | "husky": "^8.0.3", 61 | "import-path-rewrite": "github:gcanti/import-path-rewrite", 62 | "io-ts": "^2.0.0", 63 | "jest": "^29.5.0", 64 | "mocha": "^10.2.0", 65 | "prettier": "^2.8.8", 66 | "pretty-quick": "^3.1.3", 67 | "ts-jest": "^29.1.0", 68 | "ts-node": "^10.9.1", 69 | "typescript": "^5.1.3" 70 | }, 71 | "tags": [ 72 | "typescript", 73 | "functional-programming", 74 | "routing", 75 | "applicative" 76 | ], 77 | "keywords": [ 78 | "typescript", 79 | "functional-programming", 80 | "routing", 81 | "applicative" 82 | ] 83 | } 84 | -------------------------------------------------------------------------------- /src/formatter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 0.6.0 3 | */ 4 | import { Contravariant1 } from 'fp-ts/lib/Contravariant' 5 | 6 | import { RowLacks } from './helpers' 7 | import { Route } from './route' 8 | 9 | /** 10 | * @category formatters 11 | * @since 0.4.0 12 | */ 13 | export class Formatter
{ 14 | /** 15 | * @since 0.4.0 16 | */ 17 | readonly _A!: A 18 | constructor(readonly run: (r: Route, a: A) => Route) {} 19 | /** 20 | * @since 0.4.0 21 | */ 22 | contramap(f: (b: B) => A): Formatter { 23 | return new Formatter((r, b) => this.run(r, f(b))) 24 | } 25 | /** 26 | * @since 0.4.0 27 | */ 28 | then(that: Formatter & Formatter>): Formatter { 29 | return new Formatter((r, ab) => that.run(this.run(r, ab), ab)) 30 | } 31 | } 32 | 33 | declare module 'fp-ts/lib/HKT' { 34 | interface URItoKind { 35 | 'fp-ts-routing/Formatter': Formatter 36 | } 37 | } 38 | 39 | const FORMATTER_URI = 'fp-ts-routing/Formatter' 40 | 41 | type FORMATTER_URI = typeof FORMATTER_URI 42 | 43 | /** 44 | * @category formatters 45 | * @since 0.5.1 46 | */ 47 | export const formatter: Contravariant1 = { 48 | URI: FORMATTER_URI, 49 | contramap: (fa, f) => fa.contramap(f) 50 | } 51 | 52 | /** 53 | * @category formatters 54 | * @since 0.5.1 55 | */ 56 | export const contramap = 57 | (f: (b: B) => A) => 58 | (fa: Formatter): Formatter => 59 | formatter.contramap(fa, f) 60 | 61 | /** 62 | * @category formatters 63 | * @since 0.6.0 64 | */ 65 | export const then = 66 | (fb: Formatter) => 67 | (fa: Formatter & Formatter>): Formatter => 68 | fa.then(fb as any) 69 | 70 | /** 71 | * @category formatters 72 | * @since 0.4.0 73 | */ 74 | export const format = (formatter: Formatter, a: A, encode: boolean = true): string => 75 | formatter.run(Route.empty, a).toString(encode) 76 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 0.6.0 3 | */ 4 | 5 | /** 6 | * Encodes the constraint that a given object `O` 7 | * does not contain specific keys `K` 8 | * 9 | * @category helpers 10 | * @since 0.4.0 11 | */ 12 | export type RowLacks = O & Record, never> 13 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 0.4.0 3 | */ 4 | 5 | /* istanbul ignore file */ 6 | // Istanbul has some issues with re-exported symbols, so we temporarly ignore coverage only for this file 7 | 8 | import { contramap, format, Formatter, formatter } from './formatter' 9 | import { RowLacks } from './helpers' 10 | import { end, imap, int, IntegerFromString, lit, Match, query, str, succeed, then, type } from './matcher' 11 | import { 12 | alt, 13 | ap, 14 | apFirst, 15 | apSecond, 16 | chain, 17 | chainFirst, 18 | flatten, 19 | getParserMonoid, 20 | map, 21 | parse, 22 | Parser, 23 | parser, 24 | zero 25 | } from './parser' 26 | 27 | // --- Re-exports 28 | export * from './route' 29 | 30 | export { 31 | /** 32 | * @category helpers 33 | * @since 0.4.0 34 | */ 35 | RowLacks, 36 | /** 37 | * @category parsers 38 | * @since 0.4.0 39 | */ 40 | Parser, 41 | /** 42 | * @category parsers 43 | * @since 0.4.0 44 | */ 45 | zero, 46 | /** 47 | * @category parsers 48 | * @since 0.4.0 49 | */ 50 | parse, 51 | /** 52 | * @category parsers 53 | * @since 0.5.1 54 | */ 55 | getParserMonoid, 56 | /** 57 | * @category parsers 58 | * @since 0.5.1 59 | */ 60 | parser, 61 | /** 62 | * @category parsers 63 | * @since 0.5.1 64 | */ 65 | alt, 66 | /** 67 | * @category parsers 68 | * @since 0.5.1 69 | */ 70 | ap, 71 | /** 72 | * @category parsers 73 | * @since 0.5.1 74 | */ 75 | apFirst, 76 | /** 77 | * @category parsers 78 | * @since 0.5.1 79 | */ 80 | apSecond, 81 | /** 82 | * @category parsers 83 | * @since 0.5.1 84 | */ 85 | chain, 86 | /** 87 | * @category parsers 88 | * @since 0.5.1 89 | */ 90 | chainFirst, 91 | /** 92 | * @category parsers 93 | * @since 0.5.1 94 | */ 95 | flatten, 96 | /** 97 | * @category parsers 98 | * @since 0.5.1 99 | */ 100 | map, 101 | /** 102 | * @category formatters 103 | * @since 0.4.0 104 | */ 105 | Formatter, 106 | /** 107 | * @category formatters 108 | * @since 0.4.0 109 | */ 110 | format, 111 | /** 112 | * @category formatters 113 | * @since 0.5.1 114 | */ 115 | formatter, 116 | /** 117 | * @category formatters 118 | * @since 0.5.1 119 | */ 120 | contramap, 121 | /** 122 | * @category matchers 123 | * @since 0.4.0 124 | */ 125 | Match, 126 | /** 127 | * @category matchers 128 | * @since 0.5.1 129 | */ 130 | imap, 131 | /** 132 | * @category matchers 133 | * @since 0.5.1 134 | */ 135 | then, 136 | /** 137 | * @category matchers 138 | * @since 0.4.0 139 | */ 140 | succeed, 141 | /** 142 | * @category matchers 143 | * @since 0.4.0 144 | */ 145 | end, 146 | /** 147 | * @category matchers 148 | * @since 0.4.0 149 | */ 150 | type, 151 | /** 152 | * @category matchers 153 | * @since 0.4.0 154 | */ 155 | str, 156 | /** 157 | * @category matchers 158 | * @since 0.4.2 159 | */ 160 | IntegerFromString, 161 | /** 162 | * @category matchers 163 | * @since 0.4.0 164 | */ 165 | int, 166 | /** 167 | * @category matchers 168 | * @since 0.4.0 169 | */ 170 | lit, 171 | /** 172 | * @category matchers 173 | * @since 0.4.0 174 | */ 175 | query 176 | } 177 | -------------------------------------------------------------------------------- /src/matcher.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 0.6.0 3 | */ 4 | import * as E from 'fp-ts/lib/Either' 5 | import { identity } from 'fp-ts/lib/function' 6 | import * as O from 'fp-ts/lib/Option' 7 | // This `pipe` version is deprecated, but provided by `fp-ts` v2.0.1 and higher. 8 | import { pipe } from 'fp-ts/lib/pipeable' 9 | import { failure, Int, string, success, Type } from 'io-ts' 10 | 11 | import { Formatter } from './formatter' 12 | import { RowLacks } from './helpers' 13 | import { Parser } from './parser' 14 | import { QueryValues, Route } from './route' 15 | 16 | /** 17 | * @category matchers 18 | * @since 0.4.0 19 | */ 20 | export class Match { 21 | /** 22 | * @since 0.4.0 23 | */ 24 | readonly _A!: A 25 | constructor(readonly parser: Parser, readonly formatter: Formatter) {} 26 | /** 27 | * @since 0.4.0 28 | */ 29 | imap(f: (a: A) => B, g: (b: B) => A): Match { 30 | return new Match(this.parser.map(f), this.formatter.contramap(g)) 31 | } 32 | /** 33 | * @since 0.4.0 34 | */ 35 | then(that: Match & Match>): Match { 36 | return new Match(this.parser.then(that.parser), this.formatter.then(that.formatter)) 37 | } 38 | } 39 | 40 | /** 41 | * @category matchers 42 | * @since 0.5.1 43 | */ 44 | export const imap = 45 | (f: (a: A) => B, g: (b: B) => A) => 46 | (ma: Match): Match => 47 | ma.imap(f, g) 48 | 49 | /** 50 | * @category matchers 51 | * @since 0.5.1 52 | */ 53 | export const then = 54 | (mb: Match) => 55 | (ma: Match & Match>): Match => 56 | ma.then(mb as any) 57 | 58 | const singleton = (k: K, v: V): { [_ in K]: V } => ({ [k as any]: v } as any) 59 | 60 | /** 61 | * `succeed` matches everything but consumes nothing 62 | * 63 | * @category matchers 64 | * @since 0.4.0 65 | */ 66 | export const succeed = (a: A): Match => new Match(new Parser((r) => O.some([a, r])), new Formatter(identity)) 67 | 68 | /** 69 | * `end` matches the end of a route 70 | * 71 | * @category matchers 72 | * @since 0.4.0 73 | */ 74 | export const end: Match<{}> = new Match( 75 | new Parser((r) => (Route.isEmpty(r) ? O.some([{}, r]) : O.none)), 76 | new Formatter(identity) 77 | ) 78 | 79 | /** 80 | * `type` matches any io-ts type path component 81 | * 82 | * @example 83 | * import * as t from 'io-ts' 84 | * import { lit, type, Route } from 'fp-ts-routing' 85 | * import { some, none } from 'fp-ts/lib/Option' 86 | * 87 | * const T = t.keyof({ 88 | * a: null, 89 | * b: null 90 | * }) 91 | * 92 | * const match = lit('search').then(type('topic', T)) 93 | * 94 | * assert.deepStrictEqual(match.parser.run(Route.parse('/search/a')), some([{ topic: 'a' }, Route.empty])) 95 | * assert.deepStrictEqual(match.parser.run(Route.parse('/search/b')), some([{ topic: 'b' }, Route.empty])) 96 | * assert.deepStrictEqual(match.parser.run(Route.parse('/search/')), none) 97 | * 98 | * @category matchers 99 | * @since 0.4.0 100 | */ 101 | export const type = (k: K, type: Type): Match<{ [_ in K]: A }> => 102 | new Match( 103 | new Parser((r) => { 104 | if (r.parts.length === 0) { 105 | return O.none 106 | } 107 | 108 | return pipe( 109 | type.decode(r.parts[0]), 110 | O.fromEither, 111 | O.map((a) => [singleton(k, a), new Route(r.parts.slice(1), r.query)]) 112 | ) 113 | }), 114 | new Formatter((r, o) => new Route(r.parts.concat(type.encode(o[k])), r.query)) 115 | ) 116 | 117 | /** 118 | * `str` matches any string path component 119 | * 120 | * @example 121 | * import { str, Route } from 'fp-ts-routing' 122 | * import { some, none } from 'fp-ts/lib/Option' 123 | * 124 | * assert.deepStrictEqual(str('id').parser.run(Route.parse('/abc')), some([{ id: 'abc' }, new Route([], {})])) 125 | * assert.deepStrictEqual(str('id').parser.run(Route.parse('/')), none) 126 | * 127 | * @category matchers 128 | * @since 0.4.0 129 | */ 130 | export const str = (k: K): Match<{ [_ in K]: string }> => type(k, string) 131 | 132 | /** 133 | * @category matchers 134 | * @since 0.4.2 135 | */ 136 | export const IntegerFromString = new Type( 137 | 'IntegerFromString', 138 | (u): u is number => Int.is(u), 139 | (u, c) => 140 | pipe( 141 | string.validate(u, c), 142 | E.chain((s) => { 143 | const n = +s 144 | return isNaN(n) || !Number.isInteger(n) ? failure(s, c) : success(n) 145 | }) 146 | ), 147 | String 148 | ) 149 | 150 | /** 151 | * `int` matches any integer path component 152 | * 153 | * @example 154 | * import { int, Route } from 'fp-ts-routing' 155 | * import { some, none } from 'fp-ts/lib/Option' 156 | * 157 | * assert.deepStrictEqual(int('id').parser.run(Route.parse('/1')), some([{ id: 1 }, new Route([], {})])) 158 | * assert.deepStrictEqual(int('id').parser.run(Route.parse('/a')), none) 159 | * 160 | * @category matchers 161 | * @since 0.4.0 162 | */ 163 | export const int = (k: K): Match<{ [_ in K]: number }> => type(k, IntegerFromString) 164 | 165 | /** 166 | * `lit(x)` will match exactly the path component `x` 167 | * 168 | * @example 169 | * import { lit, Route } from 'fp-ts-routing' 170 | * import { some, none } from 'fp-ts/lib/Option' 171 | * 172 | * assert.deepStrictEqual(lit('subview').parser.run(Route.parse('/subview/')), some([{}, new Route([], {})])) 173 | * assert.deepStrictEqual(lit('subview').parser.run(Route.parse('/')), none) 174 | * 175 | * @category matchers 176 | * @since 0.4.0 177 | */ 178 | export const lit = (literal: string): Match<{}> => 179 | new Match( 180 | new Parser((r) => { 181 | if (r.parts.length === 0) { 182 | return O.none 183 | } 184 | 185 | return r.parts[0] === literal ? O.some([{}, new Route(r.parts.slice(1), r.query)]) : O.none 186 | }), 187 | new Formatter((r) => new Route(r.parts.concat(literal), r.query)) 188 | ) 189 | 190 | /** 191 | * Will match a querystring. 192 | * 193 | * 194 | * **Note**. Use `io-ts`'s `strict` instead of `type` otherwise excess properties won't be removed. 195 | * 196 | * @example 197 | * import * as t from 'io-ts' 198 | * import { lit, str, query, Route } from 'fp-ts-routing' 199 | * 200 | * const route = lit('accounts') 201 | * .then(str('accountId')) 202 | * .then(lit('files')) 203 | * .then(query(t.strict({ pathparam: t.string }))) 204 | * .formatter.run(Route.empty, { accountId: 'testId', pathparam: '123' }) 205 | * .toString() 206 | * 207 | * assert.strictEqual(route, '/accounts/testId/files?pathparam=123') 208 | * 209 | * @category matchers 210 | * @since 0.4.0 211 | */ 212 | export const query = (type: Type>): Match => 213 | new Match( 214 | new Parser((r) => 215 | pipe( 216 | type.decode(r.query), 217 | O.fromEither, 218 | O.map((query) => [query, new Route(r.parts, {})]) 219 | ) 220 | ), 221 | new Formatter((r, query) => new Route(r.parts, type.encode(query))) 222 | ) 223 | -------------------------------------------------------------------------------- /src/parser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 0.6.0 3 | */ 4 | import { Alternative1 } from 'fp-ts/lib/Alternative' 5 | import { identity, Lazy } from 'fp-ts/lib/function' 6 | import { Monad1 } from 'fp-ts/lib/Monad' 7 | import { Monoid } from 'fp-ts/lib/Monoid' 8 | import * as O from 'fp-ts/lib/Option' 9 | // This `pipe` version is deprecated, but provided by `fp-ts` v2.0.1 and higher. 10 | import { pipe } from 'fp-ts/lib/pipeable' 11 | 12 | import { RowLacks } from './helpers' 13 | import { Route } from './route' 14 | 15 | declare module 'fp-ts/lib/HKT' { 16 | interface URItoKind { 17 | 'fp-ts-routing/Parser': Parser 18 | } 19 | } 20 | 21 | const PARSER_URI = 'fp-ts-routing/Parser' 22 | 23 | type PARSER_URI = typeof PARSER_URI 24 | 25 | /** 26 | * @category parsers 27 | * @since 0.4.0 28 | */ 29 | export class Parser { 30 | /** 31 | * @since 0.4.0 32 | */ 33 | readonly _A!: A 34 | constructor(readonly run: (r: Route) => O.Option<[A, Route]>) {} 35 | /** 36 | * @since 0.4.0 37 | */ 38 | static of(a: A): Parser { 39 | return new Parser((s) => O.some([a, s])) 40 | } 41 | /** 42 | * @since 0.4.0 43 | */ 44 | map(f: (a: A) => B): Parser { 45 | return this.chain((a) => Parser.of(f(a))) // <= derived 46 | } 47 | /** 48 | * @since 0.4.0 49 | */ 50 | ap(fab: Parser<(a: A) => B>): Parser { 51 | return fab.chain((f) => this.map(f)) // <= derived 52 | } 53 | /** 54 | * @since 0.4.0 55 | */ 56 | chain(f: (a: A) => Parser): Parser { 57 | return new Parser((r) => 58 | pipe( 59 | this.run(r), 60 | O.chain(([a, r2]) => f(a).run(r2)) 61 | ) 62 | ) 63 | } 64 | /** 65 | * @since 0.4.0 66 | */ 67 | alt(that: Parser): Parser { 68 | return new Parser((r) => 69 | pipe( 70 | this.run(r), 71 | O.alt(() => that.run(r)) 72 | ) 73 | ) 74 | } 75 | /** 76 | * @since 0.4.0 77 | */ 78 | then(that: Parser>): Parser { 79 | return that.ap(this.map(assign as (a: A) => (b: B) => A & B)) 80 | } 81 | } 82 | 83 | /** 84 | * @category parsers 85 | * @since 0.4.0 86 | */ 87 | export const zero = (): Parser => new Parser(() => O.none) 88 | 89 | /** 90 | * @category parsers 91 | * @since 0.4.0 92 | */ 93 | export const parse = (parser: Parser, r: Route, a: A): A => 94 | pipe( 95 | parser.run(r), 96 | O.fold( 97 | () => a, 98 | ([x]) => x 99 | ) 100 | ) 101 | 102 | /** 103 | * @category parsers 104 | * @since 0.5.1 105 | */ 106 | export const getParserMonoid = (): Monoid> => ({ 107 | concat: (x, y) => x.alt(y), 108 | empty: zero() 109 | }) 110 | 111 | /** 112 | * @category parsers 113 | * @since 0.5.1 114 | */ 115 | export const parser: Monad1 & Alternative1 = { 116 | URI: PARSER_URI, 117 | map: (ma, f) => ma.map(f), 118 | of: Parser.of, 119 | ap: (mab, ma) => ma.ap(mab), 120 | chain: (ma, f) => ma.chain(f), 121 | alt: (fx, f) => 122 | new Parser((r) => 123 | pipe( 124 | fx.run(r), 125 | O.alt(() => f().run(r)) 126 | ) 127 | ), 128 | zero 129 | } 130 | 131 | /** 132 | * @category parsers 133 | * @since 0.5.1 134 | */ 135 | export const alt = 136 | (that: Lazy>) => 137 | (fa: Parser): Parser => 138 | parser.alt(fa, that) 139 | 140 | /** 141 | * @category parsers 142 | * @since 0.5.1 143 | */ 144 | export const ap = 145 | (fa: Parser) => 146 | (fab: Parser<(a: A) => B>): Parser => 147 | parser.ap(fab, fa) 148 | 149 | // taken from fp-ts 2.0.1 https://github.com/gcanti/fp-ts/blob/2.0.1/src/pipeable.ts#L1028 150 | /** 151 | * @category parsers 152 | * @since 0.5.1 153 | */ 154 | export const apFirst = 155 | (fb: Parser) => 156 | (fa: Parser): Parser => 157 | parser.ap( 158 | parser.map(fa, (a) => () => a), 159 | fb 160 | ) 161 | 162 | // taken from fp-ts 2.0.1 https://github.com/gcanti/fp-ts/blob/2.0.1/src/pipeable.ts#L1031 163 | /** 164 | * @category parsers 165 | * @since 0.5.1 166 | */ 167 | export const apSecond = 168 | (fb: Parser) => 169 | (fa: Parser): Parser => 170 | parser.ap( 171 | parser.map(fa, () => (b: B) => b), 172 | fb 173 | ) 174 | 175 | /** 176 | * @category parsers 177 | * @since 0.5.1 178 | */ 179 | export const chain = 180 | (f: (a: A) => Parser) => 181 | (ma: Parser): Parser => 182 | parser.chain(ma, f) 183 | 184 | /** 185 | * @category parsers 186 | * @since 0.5.1 187 | */ 188 | export const chainFirst = 189 | (f: (a: A) => Parser) => 190 | (ma: Parser): Parser => 191 | parser.chain(ma, (a) => parser.map(f(a), () => a)) 192 | 193 | /** 194 | * @category parsers 195 | * @since 0.5.1 196 | */ 197 | export const flatten = (mma: Parser>): Parser => parser.chain(mma, identity) 198 | 199 | /** 200 | * @category parsers 201 | * @since 0.5.1 202 | */ 203 | export const map = 204 | (f: (a: A) => B) => 205 | (fa: Parser): Parser => 206 | parser.map(fa, f) 207 | 208 | /** 209 | * @category parsers 210 | * @since 0.6.0 211 | */ 212 | export const then = 213 | (fb: Parser) => 214 | (fa: Parser & Parser>): Parser => 215 | fa.then(fb as any) 216 | 217 | // --- Helpers 218 | const assign = 219 | (a: A) => 220 | (b: B): A & B => 221 | Object.assign({}, a, b) 222 | -------------------------------------------------------------------------------- /src/route.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 0.6.0 3 | */ 4 | import { isEmpty } from 'fp-ts/lib/Record' 5 | 6 | /** 7 | * @category routes 8 | * @since 0.4.0 9 | */ 10 | export type QueryValues = string | Array | undefined 11 | 12 | /** 13 | * @category routes 14 | * @since 0.4.0 15 | */ 16 | export interface Query { 17 | [key: string]: QueryValues 18 | } 19 | 20 | /** 21 | * @category routes 22 | * @since 0.4.0 23 | */ 24 | export class Route { 25 | /** 26 | * @since 0.4.0 27 | */ 28 | static empty = new Route([], {}) 29 | constructor(readonly parts: Array, readonly query: Query) {} 30 | /** 31 | * @since 0.4.0 32 | */ 33 | static isEmpty(r: Route): boolean { 34 | return r.parts.length === 0 && isEmpty(r.query) 35 | } 36 | /** 37 | * @since 0.4.0 38 | */ 39 | static parse(s: string, decode: boolean = true): Route { 40 | const { pathname, searchParams } = new URL(s, 'http://localhost') // `base` is needed when `path` is relative 41 | 42 | const segments = pathname.split('/').filter(Boolean) 43 | const parts = decode ? segments.map(decodeURIComponent) : segments 44 | 45 | return new Route(parts, toQuery(searchParams)) 46 | } 47 | /** 48 | * @since 0.4.0 49 | */ 50 | toString(encode: boolean = true): `/${string}` { 51 | const qs = fromQuery(this.query).toString() 52 | const parts = encode ? this.parts.map(encodeURIComponent) : this.parts 53 | return `/${parts.join('/')}${qs ? '?' + qs : ''}` 54 | } 55 | } 56 | 57 | const fromQuery = (query: Query): URLSearchParams => { 58 | const qs = new URLSearchParams() 59 | 60 | Object.entries(query).forEach(([k, v]) => { 61 | if (typeof v === 'undefined') { 62 | return 63 | } 64 | 65 | return Array.isArray(v) ? v.forEach((x) => qs.append(k, x)) : qs.set(k, v) 66 | }) 67 | 68 | return qs 69 | } 70 | 71 | const toQuery = (params: URLSearchParams): Query => { 72 | const q: Query = {} 73 | 74 | params.forEach((v, k) => { 75 | const current = q[k] 76 | 77 | if (current) { 78 | q[k] = Array.isArray(current) ? [...current, v] : [current, v] 79 | } else { 80 | q[k] = v 81 | } 82 | }) 83 | 84 | return q 85 | } 86 | -------------------------------------------------------------------------------- /test/formatter.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import { pipe } from 'fp-ts/lib/function' 3 | import * as t from 'io-ts' 4 | 5 | import * as F from '../src/formatter' 6 | import { Route } from '../src/route' 7 | 8 | const FORMATTER = (k: string) => 9 | new F.Formatter((r, o) => new Route(r.parts.concat(t.string.encode(o[k])), r.query)) 10 | 11 | describe('format', () => { 12 | it('encode = true', () => { 13 | const x = FORMATTER('username') 14 | assert.strictEqual(F.format(x, { username: '@giulio' }), '/%40giulio') 15 | }) 16 | 17 | it('encode = false', () => { 18 | const x = FORMATTER('username') 19 | assert.strictEqual(F.format(x, { username: '@giulio' }, false), '/@giulio') 20 | }) 21 | }) 22 | 23 | describe('Formatter', () => { 24 | it('then', () => { 25 | const x = FORMATTER('username').then(FORMATTER('foo')) 26 | const y = pipe(FORMATTER('username'), F.then(FORMATTER('foo'))) 27 | 28 | assert.strictEqual(F.format(x, { username: 'test', foo: 'bar' }), '/test/bar') 29 | assert.strictEqual(F.format(y, { username: 'test', foo: 'bar' }), '/test/bar') 30 | }) 31 | 32 | it('contramap', () => { 33 | const x = new F.Formatter((r, a: { foo: number }) => new Route(r.parts.concat(String(a.foo)), r.query)) 34 | 35 | assert.strictEqual( 36 | F.format( 37 | F.formatter.contramap(x, (b: { bar: string }) => ({ foo: b.bar.length })), 38 | { bar: 'baz' } 39 | ), 40 | '/3' 41 | ) 42 | 43 | assert.strictEqual(F.format(F.contramap((b: { bar: string }) => ({ foo: b.bar.length }))(x), { bar: 'baz' }), '/3') 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import { end, format, int, lit, parse, Route, zero } from '../src' 4 | 5 | describe('Usage example', () => { 6 | // locations 7 | interface Home { 8 | readonly _tag: 'Home' 9 | } 10 | 11 | const Home = (): Home => ({ _tag: 'Home' }) 12 | 13 | interface User { 14 | readonly _tag: 'User' 15 | readonly id: number 16 | } 17 | 18 | const User = (id: number): User => ({ _tag: 'User', id }) 19 | 20 | interface Invoice { 21 | readonly _tag: 'Invoice' 22 | readonly userId: number 23 | readonly invoiceId: number 24 | } 25 | 26 | const Invoice = (userId: number, invoiceId: number): Invoice => ({ _tag: 'Invoice', userId, invoiceId }) 27 | 28 | interface NotFound { 29 | readonly _tag: 'NotFound' 30 | } 31 | 32 | const NotFound = (): NotFound => ({ _tag: 'NotFound' }) 33 | 34 | type Location = Home | User | Invoice | NotFound 35 | 36 | // matches 37 | const defaults = end 38 | const home = lit('home').then(end) 39 | const userId = lit('users').then(int('userId')) 40 | const user = userId.then(end) 41 | const invoice = userId.then(lit('invoice')).then(int('invoiceId')).then(end) 42 | 43 | // router 44 | const router = zero() 45 | .alt(defaults.parser.map(() => Home())) 46 | .alt(home.parser.map(() => Home())) 47 | .alt(user.parser.map(({ userId }) => User(userId))) 48 | .alt(invoice.parser.map(({ userId, invoiceId }) => Invoice(userId, invoiceId))) 49 | 50 | // helpers 51 | const parseLocation = (s: string): Location => parse(router, Route.parse(s), NotFound()) 52 | 53 | it('should match a location', () => { 54 | assert.deepStrictEqual(parseLocation('/'), Home()) 55 | assert.deepStrictEqual(parseLocation('/home'), Home()) 56 | assert.deepStrictEqual(parseLocation('/users/1'), User(1)) 57 | assert.deepStrictEqual(parseLocation('/users/1/invoice/2'), Invoice(1, 2)) 58 | assert.deepStrictEqual(parseLocation('/foo'), NotFound()) 59 | }) 60 | 61 | it('should format a location', () => { 62 | assert.strictEqual(format(user.formatter, { userId: 1 }), '/users/1') 63 | assert.strictEqual(format(invoice.formatter, { userId: 1, invoiceId: 2 }), '/users/1/invoice/2') 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /test/matcher.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import * as Arr from 'fp-ts/lib/Array' 3 | import * as E from 'fp-ts/lib/Either' 4 | import { pipe } from 'fp-ts/lib/function' 5 | import * as O from 'fp-ts/lib/Option' 6 | import * as S from 'fp-ts/lib/string' 7 | import * as t from 'io-ts' 8 | 9 | import { format } from '../src/formatter' 10 | import * as M from '../src/matcher' 11 | import { parse } from '../src/parser' 12 | import { Route } from '../src/route' 13 | 14 | const arrEquals = Arr.getEq(S.Eq) 15 | 16 | describe('IntegerFromString', () => { 17 | it('is', () => { 18 | assert.strictEqual(M.IntegerFromString.is('a'), false) 19 | assert.strictEqual(M.IntegerFromString.is(1.2), false) 20 | assert.strictEqual(M.IntegerFromString.is(1), true) 21 | }) 22 | }) 23 | 24 | describe('Match', () => { 25 | it('imap', () => { 26 | const match = pipe( 27 | M.str('id'), 28 | M.imap( 29 | ({ id }) => ({ userId: id }), 30 | ({ userId }) => ({ id: userId }) 31 | ) 32 | ) 33 | 34 | assert.deepStrictEqual(parse(match.parser, Route.parse('/1'), { userId: '0' }), { 35 | userId: '1' 36 | }) 37 | 38 | assert.strictEqual(format(match.formatter, { userId: '1' }), '/1') 39 | }) 40 | 41 | it('type', () => { 42 | const T = t.keyof({ 43 | a: null, 44 | b: null 45 | }) 46 | const match = pipe(M.lit('search'), M.then(M.type('topic', T))) 47 | 48 | assert.deepStrictEqual(match.parser.run(Route.parse('/search/a')), O.some([{ topic: 'a' }, Route.empty])) 49 | assert.deepStrictEqual(match.parser.run(Route.parse('/search/b')), O.some([{ topic: 'b' }, Route.empty])) 50 | assert.deepStrictEqual(match.parser.run(Route.parse('/search/')), O.none) 51 | }) 52 | 53 | it('str', () => { 54 | const match = M.str('id') 55 | 56 | assert.deepStrictEqual(match.parser.run(Route.parse('/abc')), O.some([{ id: 'abc' }, Route.empty])) 57 | assert.deepStrictEqual(match.parser.run(Route.parse('/')), O.none) 58 | }) 59 | 60 | it('int', () => { 61 | const match = M.int('id') 62 | 63 | assert.deepStrictEqual(match.parser.run(Route.parse('/1')), O.some([{ id: 1 }, Route.empty])) 64 | assert.deepStrictEqual(match.parser.run(Route.parse('/a')), O.none) 65 | assert.deepStrictEqual(match.parser.run(Route.parse('/1a')), O.none) 66 | assert.deepStrictEqual(match.parser.run(Route.parse('/1.2')), O.none) 67 | }) 68 | 69 | it('query', () => { 70 | const DateFromISOString = new t.Type( 71 | 'DateFromISOString', 72 | (u): u is Date => u instanceof Date, 73 | (u, c) => { 74 | const validation = t.string.validate(u, c) 75 | if (E.isLeft(validation)) { 76 | return validation as any 77 | } else { 78 | const s = validation.right 79 | const d = new Date(s) 80 | return isNaN(d.getTime()) ? t.failure(s, c) : t.success(d) 81 | } 82 | }, 83 | (a) => a.toISOString() 84 | ) 85 | 86 | assert.strictEqual( 87 | pipe( 88 | M.query(t.type({ a: t.string, b: M.IntegerFromString })).parser.run(Route.parse('/foo/bar/?a=baz&b=1')), 89 | O.exists(([{ a, b }]) => a === 'baz' && b === 1) 90 | ), 91 | true 92 | ) 93 | const date = '2018-01-18T14:51:47.912Z' 94 | 95 | assert.deepStrictEqual( 96 | M.query(t.type({ a: DateFromISOString })).formatter.run(Route.empty, { 97 | a: new Date(date) 98 | }), 99 | new Route([], { a: date }) 100 | ) 101 | 102 | const route = M.lit('accounts') 103 | .then(M.str('accountId')) 104 | .then(M.lit('files')) 105 | .then(M.query(t.strict({ pathparam: t.string }))) 106 | .formatter.run(Route.empty, { accountId: 'testId', pathparam: '123' }) 107 | .toString() 108 | 109 | assert.strictEqual(route, '/accounts/testId/files?pathparam=123') 110 | }) 111 | 112 | it('query accept undefined ', () => { 113 | const Q = t.type({ a: t.union([t.undefined, t.string]) }) 114 | 115 | assert.strictEqual( 116 | pipe( 117 | M.query(Q).parser.run(Route.parse('/foo/bar/?a=baz')), 118 | O.exists(([{ a }]) => a === 'baz') 119 | ), 120 | true 121 | ) 122 | assert.strictEqual(O.isSome(M.query(Q).parser.run(Route.parse('/foo/bar/?b=1'))), true) 123 | assert.deepStrictEqual(M.query(Q).formatter.run(Route.empty, { a: undefined }), new Route([], { a: undefined })) 124 | assert.deepStrictEqual(M.query(Q).formatter.run(Route.empty, { a: 'baz' }), new Route([], { a: 'baz' })) 125 | }) 126 | 127 | it('query works with partial codecs', () => { 128 | type StringOrArray = t.TypeOf 129 | const stringOrArray = t.union([t.string, t.array(t.string)]) 130 | const normalize = (v: StringOrArray): Array => (Array.isArray(v) ? v : [v]) 131 | 132 | const arrayParam = new t.Type, Array>( 133 | 'ArrayParameter', 134 | (u): u is StringOrArray => stringOrArray.is(u), 135 | (u, c) => pipe(stringOrArray.validate(u, c), E.map(normalize)), 136 | normalize 137 | ) 138 | 139 | const Q = t.partial({ a: t.string, b: t.string, c: arrayParam }) 140 | 141 | assert.strictEqual( 142 | pipe( 143 | M.query(Q).parser.run(Route.parse('/foo/bar')), 144 | O.exists(([{ a, b }]) => a === undefined && b === undefined) 145 | ), 146 | true 147 | ) 148 | 149 | assert.strictEqual( 150 | pipe( 151 | M.query(Q).parser.run(Route.parse('/foo/bar?a=baz')), 152 | O.exists(([{ a, b }]) => a === 'baz' && b === undefined) 153 | ), 154 | true 155 | ) 156 | 157 | assert.strictEqual( 158 | pipe( 159 | M.query(Q).parser.run(Route.parse('/foo/bar?a=baz&b=quu')), 160 | O.exists(([{ a, b }]) => a === 'baz' && b === 'quu') 161 | ), 162 | true 163 | ) 164 | 165 | assert.strictEqual( 166 | pipe( 167 | M.query(Q).parser.run(Route.parse('/foo/bar?a=baz&c=quu')), 168 | O.exists(([{ a, c }]) => a === 'baz' && Array.isArray(c) && arrEquals.equals(c, ['quu'])) 169 | ), 170 | true 171 | ) 172 | 173 | assert.strictEqual( 174 | pipe( 175 | M.query(Q).parser.run(Route.parse('/foo/bar?a=baz&c=1&c=2&c=3')), 176 | O.exists(([{ a, c }]) => a === 'baz' && Array.isArray(c) && arrEquals.equals(c, ['1', '2', '3'])) 177 | ), 178 | true 179 | ) 180 | 181 | assert.deepStrictEqual(M.query(Q).formatter.run(Route.empty, {}), Route.empty) 182 | assert.deepStrictEqual(M.query(Q).formatter.run(Route.empty, { a: 'baz' }), new Route([], { a: 'baz' })) 183 | assert.deepStrictEqual( 184 | M.query(Q).formatter.run(Route.empty, { a: 'baz', b: 'quu' }), 185 | new Route([], { a: 'baz', b: 'quu' }) 186 | ) 187 | }) 188 | 189 | it('query works with array partial', () => { 190 | const Q = t.partial({ a: t.array(t.string) }) 191 | 192 | assert.strictEqual( 193 | pipe( 194 | M.query(Q).parser.run(Route.parse('/foo/bar?a=baz&a=bar')), 195 | O.exists(([{ a }]) => Array.isArray(a) && a[0] === 'baz') 196 | ), 197 | true 198 | ) 199 | 200 | assert.deepStrictEqual(M.query(Q).formatter.run(Route.empty, { a: ['baz'] }), new Route([], { a: ['baz'] })) 201 | }) 202 | 203 | it('query deletes extranous params for exact partial codecs', () => { 204 | const Q = t.exact(t.partial({ a: t.string })) 205 | 206 | assert.strictEqual( 207 | pipe( 208 | M.query(Q).parser.run(Route.parse('/foo/bar?b=baz')), 209 | O.exists(([q]) => (q as any)['b'] === undefined) 210 | ), 211 | true 212 | ) 213 | }) 214 | 215 | it('succeed', () => { 216 | const match = M.succeed({}) 217 | 218 | assert.deepStrictEqual(match.parser.run(Route.parse('/')), O.some([{}, Route.empty])) 219 | assert.deepStrictEqual(match.parser.run(Route.parse('/a')), O.some([{}, new Route(['a'], {})])) 220 | assert.deepStrictEqual( 221 | M.succeed({ meaning: 42 }).parser.run(Route.parse('/a')), 222 | O.some([{ meaning: 42 }, new Route(['a'], {})]) 223 | ) 224 | }) 225 | 226 | it('end', () => { 227 | const match = M.end 228 | 229 | assert.deepStrictEqual(match.parser.run(Route.parse('/')), O.some([{}, Route.empty])) 230 | assert.deepStrictEqual(match.parser.run(Route.parse('/a')), O.none) 231 | }) 232 | 233 | it('lit', () => { 234 | const match = M.lit('subview') 235 | 236 | assert.deepStrictEqual(match.parser.run(Route.parse('/subview/')), O.some([{}, Route.empty])) 237 | assert.deepStrictEqual(match.parser.run(Route.parse('/sdfsdf')), O.none) 238 | assert.deepStrictEqual(match.parser.run(Route.parse('/')), O.none) 239 | }) 240 | }) 241 | -------------------------------------------------------------------------------- /test/parser.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import { pipe } from 'fp-ts/lib/function' 3 | import * as O from 'fp-ts/lib/Option' 4 | 5 | import { Route } from '../src' 6 | import * as P from '../src/parser' 7 | 8 | const ROUTE = new Route(['aaa'], {}) 9 | 10 | const lit = (literal: string) => 11 | new P.Parser((r) => { 12 | if (r.parts.length === 0) { 13 | return O.none 14 | } 15 | 16 | return r.parts[0] === literal ? O.some([{}, new Route(r.parts.slice(1), r.query)]) : O.none 17 | }) 18 | 19 | type Data = (typeof PARSER)['_A'] 20 | const PARSER = P.parser.of({ s: 'aaa' }) 21 | const LIT_A = lit('a') 22 | const LIT_B = lit('b') 23 | 24 | describe('Parser', () => { 25 | it('map', () => { 26 | const fn = (a: Data) => a.s.length 27 | const result = O.some([3, ROUTE]) 28 | 29 | assert.deepStrictEqual(P.parser.map(PARSER, fn).run(ROUTE), result) 30 | 31 | assert.deepStrictEqual(pipe(PARSER, P.map(fn)).run(ROUTE), result) 32 | }) 33 | 34 | it('ap', () => { 35 | const mab = P.parser.of((n: number) => n * 2) 36 | const ma = P.parser.of(1) 37 | const result = O.some([2, ROUTE]) 38 | 39 | assert.deepStrictEqual(P.parser.ap(mab, ma).run(ROUTE), result) 40 | 41 | assert.deepStrictEqual(P.ap(ma)(mab).run(ROUTE), result) 42 | }) 43 | 44 | it('apFirst', () => { 45 | const first = P.parser.of(1) 46 | const second = P.parser.of(2) 47 | 48 | assert.deepStrictEqual(P.apFirst(second)(first).run(ROUTE), O.some([1, ROUTE])) 49 | }) 50 | 51 | it('apSecond', () => { 52 | const first = P.parser.of(1) 53 | const second = P.parser.of(2) 54 | 55 | assert.deepStrictEqual(P.apSecond(second)(first).run(ROUTE), O.some([2, ROUTE])) 56 | }) 57 | 58 | it('chain', () => { 59 | const fn = (a: Data) => P.parser.of(a.s.length) 60 | const result = O.some([3, ROUTE]) 61 | 62 | assert.deepStrictEqual(P.parser.chain(PARSER, fn).run(ROUTE), result) 63 | 64 | assert.deepStrictEqual(pipe(PARSER, P.chain(fn)).run(ROUTE), result) 65 | }) 66 | 67 | it('chainFirst', () => { 68 | assert.deepStrictEqual( 69 | pipe( 70 | PARSER, 71 | P.chainFirst((a) => P.parser.of(a.s.length)) 72 | ).run(ROUTE), 73 | O.some([{ s: 'aaa' }, ROUTE]) 74 | ) 75 | }) 76 | 77 | it('alt', () => { 78 | const x = P.parser.alt(LIT_A, () => LIT_B) 79 | assert.deepStrictEqual(x.run(Route.parse('/a')), O.some([{}, Route.empty])) 80 | assert.deepStrictEqual(x.run(Route.parse('/b')), O.some([{}, Route.empty])) 81 | assert.deepStrictEqual(x.run(Route.parse('/c')), O.none) 82 | 83 | const y = P.alt(() => LIT_A)(LIT_B) 84 | assert.deepStrictEqual(y.run(Route.parse('/a')), O.some([{}, Route.empty])) 85 | assert.deepStrictEqual(y.run(Route.parse('/b')), O.some([{}, Route.empty])) 86 | assert.deepStrictEqual(y.run(Route.parse('/c')), O.none) 87 | }) 88 | 89 | it('then', () => { 90 | const x = LIT_A.then(LIT_B) 91 | const y = pipe(LIT_A, P.then(LIT_B)) 92 | 93 | assert.deepStrictEqual(x.run(Route.parse('/a/b')), O.some([{}, Route.empty])) 94 | assert.deepStrictEqual(x.run(Route.parse('/a/c')), O.none) 95 | 96 | assert.deepStrictEqual(y.run(Route.parse('/a/b')), O.some([{}, Route.empty])) 97 | assert.deepStrictEqual(y.run(Route.parse('/a/c')), O.none) 98 | }) 99 | 100 | it('flatten', () => { 101 | const inside = PARSER 102 | const outside = P.parser.of(inside) 103 | 104 | assert.deepStrictEqual(P.flatten(outside).run(ROUTE), O.some([{ s: 'aaa' }, ROUTE])) 105 | }) 106 | 107 | it('zero', () => { 108 | assert.deepStrictEqual(P.zero().run(ROUTE), O.none) 109 | }) 110 | 111 | it('parse', () => { 112 | const p = new P.Parser((r) => (r.parts[0] === 'a' ? O.some(['aaa', Route.empty]) : O.none)) 113 | 114 | assert.deepStrictEqual(P.parse(p, Route.parse('/a'), 'bbb'), 'aaa') 115 | assert.deepStrictEqual(P.parse(p, Route.empty, 'bbb'), 'bbb') 116 | }) 117 | 118 | it('getParserMonoid', () => { 119 | const monoid = P.getParserMonoid<{ v: string }>() 120 | const parser = monoid.concat( 121 | LIT_A.map(() => ({ v: 'a' })), 122 | LIT_B.map(() => ({ v: 'b' })) 123 | ) 124 | assert.deepStrictEqual(parser.run(Route.parse('/a')), O.some([{ v: 'a' }, Route.empty])) 125 | assert.deepStrictEqual(parser.run(Route.parse('/b')), O.some([{ v: 'b' }, Route.empty])) 126 | assert.deepStrictEqual(parser.run(Route.parse('/c')), O.none) 127 | }) 128 | }) 129 | -------------------------------------------------------------------------------- /test/route.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import { Route } from '../src/route' 4 | 5 | describe('Route', () => { 6 | it('parse', () => { 7 | assert.deepStrictEqual(Route.parse(''), Route.empty) 8 | assert.deepStrictEqual(Route.parse('/'), Route.empty) 9 | assert.deepStrictEqual(Route.parse('/foo'), new Route(['foo'], {})) 10 | assert.deepStrictEqual(Route.parse('/foo/bar'), new Route(['foo', 'bar'], {})) 11 | assert.deepStrictEqual(Route.parse('/foo/bar/'), new Route(['foo', 'bar'], {})) 12 | assert.deepStrictEqual(Route.parse('/foo/bar?a=1'), new Route(['foo', 'bar'], { a: '1' })) 13 | assert.deepStrictEqual(Route.parse('/foo/bar/?a=1'), new Route(['foo', 'bar'], { a: '1' })) 14 | assert.deepStrictEqual(Route.parse('/foo/bar?a=1&a=2&a=3'), new Route(['foo', 'bar'], { a: ['1', '2', '3'] })) 15 | assert.deepStrictEqual(Route.parse('/a%20b'), new Route(['a b'], {})) 16 | assert.deepStrictEqual(Route.parse('/foo?a=b%20c'), new Route(['foo'], { a: 'b c' })) 17 | assert.deepStrictEqual(Route.parse('/@a'), new Route(['@a'], {})) 18 | assert.deepStrictEqual(Route.parse('/%40a'), new Route(['@a'], {})) 19 | assert.deepStrictEqual(Route.parse('/?a=@b'), new Route([], { a: '@b' })) 20 | assert.deepStrictEqual(Route.parse('/?@a=b'), new Route([], { '@a': 'b' })) 21 | }) 22 | 23 | it('parse (decode = false)', () => { 24 | assert.deepStrictEqual(Route.parse('/%40a', false), new Route(['%40a'], {})) 25 | }) 26 | 27 | it('toString', () => { 28 | assert.strictEqual(new Route([], {}).toString(), '/') 29 | assert.strictEqual(new Route(['a'], {}).toString(), '/a') 30 | assert.strictEqual(new Route(['a'], { b: 'b' }).toString(), '/a?b=b') 31 | assert.strictEqual(new Route(['a'], { b: 'b c' }).toString(), '/a?b=b+c') 32 | assert.strictEqual(new Route(['a'], { b: ['1', '2', '3'] }).toString(), '/a?b=1&b=2&b=3') 33 | assert.strictEqual(new Route(['a'], { b: undefined }).toString(), '/a') 34 | assert.strictEqual(new Route(['a c'], { b: 'b' }).toString(), '/a%20c?b=b') 35 | assert.strictEqual(new Route(['@a'], {}).toString(), '/%40a') 36 | assert.strictEqual(new Route(['a&b'], {}).toString(), '/a%26b') 37 | assert.strictEqual(new Route([], { a: '@b' }).toString(), '/?a=%40b') 38 | assert.strictEqual(new Route([], { '@a': 'b' }).toString(), '/?%40a=b') 39 | }) 40 | 41 | it('toString (encode = false)', () => { 42 | assert.strictEqual(new Route(['@a'], {}).toString(false), '/@a') 43 | }) 44 | 45 | it('parse and toString should be inverse functions', () => { 46 | const path = '/a%20c?b=b+c' 47 | assert.strictEqual(Route.parse(path).toString(), path) 48 | }) 49 | 50 | it('isEmpty', () => { 51 | assert.strictEqual(Route.isEmpty(new Route([], {})), true) 52 | assert.strictEqual(Route.isEmpty(new Route(['a'], {})), false) 53 | assert.strictEqual(Route.isEmpty(new Route([], { a: 'a' })), false) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /tsconfig.build-es6.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "outDir": "./es6", 5 | "module": "es6" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false 5 | }, 6 | "include": ["./src"] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "outDir": "./lib", 5 | "target": "es5", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "lib": ["es6", "dom"], 9 | "sourceMap": true, 10 | "declaration": true, 11 | "strict": true, 12 | "noImplicitReturns": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "skipLibCheck": true 18 | }, 19 | "include": ["./src", "./test", "./scripts", "./jest.config.ts"] 20 | } 21 | --------------------------------------------------------------------------------