├── .editorconfig ├── .eslintrc.js ├── .github ├── FUNDING.yml └── workflows │ └── npm-publish.yml ├── .gitignore ├── .prettierrc.js ├── .vscode └── tasks.json ├── LICENSE.md ├── README.md ├── package-lock.json ├── package.json ├── src ├── index.js └── utilities │ ├── buildExpression.js │ ├── escapeExpression.js │ ├── getMatches.js │ ├── getTokens.js │ ├── index.js │ └── zipObject.js └── test ├── match.js ├── test.js └── utilities ├── buildExpression.js ├── escapePattern.js ├── getMatches.js ├── getTokens.js └── zipObject.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | jest: true, 5 | node: true, 6 | browser: true 7 | }, 8 | 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:prettier/recommended' 12 | ], 13 | 14 | plugins: [ 15 | 'prettier' 16 | ], 17 | 18 | parserOptions: { 19 | ecmaVersion: 6, 20 | sourceType: 'module' 21 | }, 22 | 23 | rules: { 24 | 'prettier/prettier': 'error' 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: 2 | - https://buymeacoffee.com/nblackburn 3 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Package 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: 16 19 | - run: npm ci 20 | - run: npm run test 21 | 22 | publish-npm: 23 | needs: build 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v3 27 | - uses: actions/setup-node@v3 28 | with: 29 | node-version: 16 30 | registry-url: https://registry.npmjs.org/ 31 | - run: npm publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | coverage/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | tabWidth: 4, 4 | singleQuote: true, 5 | trailingComma: 'es5' 6 | }; -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "test", 7 | "problemMatcher": [] 8 | }, 9 | { 10 | "type": "npm", 11 | "script": "coverage", 12 | "problemMatcher": [] 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright © 2019 Nathaniel Blackburn 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 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # micro match 2 | 3 | A simple url matching utility for micro. 4 | 5 | ## Installation 6 | 7 | To install micro match, simply run the following command in your terminal of choice. 8 | 9 | ```bash 10 | npm install -g micro-match 11 | ``` 12 | 13 | Once you have installed micro match, simply include it in your project like so... 14 | 15 | ```javascript 16 | const { match, test } = require('micro-match'); 17 | ``` 18 | 19 | ## Usage 20 | 21 | ### Match 22 | 23 | The match method allows you to match a route binding to it's real world counterpart and return it's bindings. 24 | 25 | ```javascript 26 | const {id} = match('/users/:id', '/users/1'); 27 | ``` 28 | Parameters are defined as a comma followed by an alias to bind it's value to. 29 | 30 | You can also create optional parameters by attaching a `?` suffix. This will allow both `/users` and `/users/1` to be matched. 31 | 32 | Now you will be able to access the `id` parameter with the value of `1` and handle it however ever you like. 33 | 34 | ### Test 35 | 36 | The test method allows you check if the pattern matches the given url. 37 | 38 | ```javascript 39 | test('/users/:id', '/users/1'); 40 | ``` 41 | 42 | ## Changes 43 | 44 | Details for each release are documented in the [release notes](CHANGELOG.md). 45 | 46 | ## License 47 | 48 | This utility is licensed under [MIT](http://opensource.org/licenses/mit), see [LICENSE.md](LICENSE.md) for details. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "micro-match", 3 | "description": "A simple url matching utility for micro.", 4 | "version": "1.0.3", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "lint": "eslint src/**/*", 9 | "coverage": "jest --coverage", 10 | "lint:fix": "eslint src/**/* --fix", 11 | "format": "prettier src/**/* --check", 12 | "format:fix": "prettier src/**/* --write" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/nblackburn/micro-match.git" 17 | }, 18 | "keywords": [ 19 | "url", 20 | "micro", 21 | "match", 22 | "utility" 23 | ], 24 | "devDependencies": { 25 | "eslint": "^6.4.0", 26 | "eslint-config-prettier": "^6.3.0", 27 | "eslint-plugin-prettier": "^3.1.1", 28 | "husky": "^3.0.5", 29 | "jest": "^29.0.0", 30 | "prettier": "^1.18.2" 31 | }, 32 | "jest": { 33 | "verbose": true, 34 | "testEnvironment": "node", 35 | "testRegex": "test/(.*).js$", 36 | "testPathIgnorePatterns": [ 37 | "/test/fixtures/", 38 | "/node_modules/" 39 | ], 40 | "moduleNameMapper": { 41 | "^@/(.*)$": "/src/$1" 42 | } 43 | }, 44 | "husky": { 45 | "hooks": { 46 | "pre-push": "npm run test", 47 | "pre-commit": "npm run lint:fix && npm run format:fix" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const { zipObject, getTokens, buildExpression } = require('./utilities'); 2 | 3 | /** 4 | * Match the route bindings from a url. 5 | * 6 | * @param {string} pattern 7 | * @param {string} url 8 | * @param {?boolean} unsafe 9 | * 10 | * @return {object} 11 | */ 12 | const match = (pattern, url, unsafe) => { 13 | // Avoid ReDoS, see example: https://goo.gl/RxSRXD 14 | if (url.length > 10000 && unsafe !== true) { 15 | throw new Error('URL too large, aborting!'); 16 | } 17 | 18 | let tokens = getTokens(pattern); 19 | let expression = buildExpression(pattern); 20 | let matches = url.match(expression); 21 | 22 | if (matches) { 23 | matches = matches.splice(1, tokens.length); 24 | } 25 | 26 | return zipObject(tokens, matches); 27 | }; 28 | 29 | /** 30 | * Test a url matches the given pattern. 31 | * 32 | * @param {string} pattern 33 | * @param {string} url 34 | * 35 | * @return {boolean} 36 | */ 37 | const test = (pattern, url) => { 38 | let expression = buildExpression(pattern); 39 | 40 | return expression.test(url); 41 | }; 42 | 43 | module.exports = { 44 | test, 45 | match, 46 | }; 47 | -------------------------------------------------------------------------------- /src/utilities/buildExpression.js: -------------------------------------------------------------------------------- 1 | const escapeExpression = require('./escapeExpression'); 2 | 3 | /** 4 | * Build the regular expression needed to match the pattern. 5 | * 6 | * @param {string} pattern 7 | * 8 | * @return {regexp} 9 | */ 10 | module.exports = pattern => { 11 | let escaped = escapeExpression(pattern); 12 | 13 | let expression = escaped 14 | .replace(/:[^/?]+\?/g, '(?:/)?([^/]+)?') 15 | .replace(/:[^/]+/g, '([^/]+)'); 16 | 17 | return new RegExp(`^${expression}$`); 18 | }; 19 | -------------------------------------------------------------------------------- /src/utilities/escapeExpression.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Escape special characters from a regular expression. 3 | * 4 | * @param {string} pattern 5 | * 6 | * @return {string} 7 | */ 8 | module.exports = pattern => { 9 | if (typeof pattern !== 'string') { 10 | throw new Error('pattern must of type string.'); 11 | } 12 | 13 | return pattern.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'); 14 | }; 15 | -------------------------------------------------------------------------------- /src/utilities/getMatches.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get matches from a regular expression. 3 | * 4 | * @param {regexp} expression 5 | * @param {string} pattern 6 | * 7 | * @return {array} 8 | */ 9 | module.exports = (expression, pattern) => { 10 | let match; 11 | let matches = []; 12 | 13 | while ((match = expression.exec(pattern)) !== null) { 14 | matches.push(match[1]); 15 | } 16 | 17 | return matches; 18 | }; 19 | -------------------------------------------------------------------------------- /src/utilities/getTokens.js: -------------------------------------------------------------------------------- 1 | const getMatches = require('./getMatches'); 2 | /** 3 | * Get the tokens. 4 | * 5 | * @param {string} pattern 6 | * 7 | * @return {array} 8 | */ 9 | module.exports = pattern => { 10 | let expression = /:([^/\\?]+)/g; 11 | let matches = getMatches(expression, pattern); 12 | 13 | return matches; 14 | }; 15 | -------------------------------------------------------------------------------- /src/utilities/index.js: -------------------------------------------------------------------------------- 1 | const zipObject = require('./zipObject'); 2 | const getTokens = require('./getTokens'); 3 | const getMatches = require('./getMatches'); 4 | const buildExpression = require('./buildExpression'); 5 | const escapeExpression = require('./escapeExpression'); 6 | 7 | module.exports = { 8 | zipObject, 9 | getTokens, 10 | getMatches, 11 | buildExpression, 12 | escapeExpression, 13 | }; 14 | -------------------------------------------------------------------------------- /src/utilities/zipObject.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Take a key and value array and convert them into key/value pairs. 3 | * 4 | * @param {array} keys 5 | * @param {array} values 6 | * 7 | * @return {object} 8 | */ 9 | module.exports = (keys, values) => { 10 | keys = keys || []; 11 | values = values || []; 12 | 13 | let zipped = {}; 14 | 15 | // Ensure keys and values are of equal length. 16 | if (keys.length !== values.length) { 17 | keys = keys.slice(0, values.length); 18 | values = values.slice(0, keys.length); 19 | } 20 | 21 | for (let index = 0; index < keys.length; index++) { 22 | if (keys[index] && values[index]) { 23 | zipped[keys[index]] = values[index]; 24 | } 25 | } 26 | 27 | return zipped; 28 | }; 29 | -------------------------------------------------------------------------------- /test/match.js: -------------------------------------------------------------------------------- 1 | const { match } = require('@/index'); 2 | 3 | describe('match', () => { 4 | test('get required parameter', () => { 5 | const route = '/api/v1/users/1'; 6 | const { id } = match('/api/:version?/users/:id', route); 7 | 8 | expect(id).toBe('1'); 9 | }); 10 | 11 | test('get optional parameter', () => { 12 | const route = '/api/v1/users/1'; 13 | const { version } = match('/api/:version?/users/:id', route); 14 | 15 | expect(version).toBe('v1'); 16 | }); 17 | 18 | test('do not allow large urls', () => { 19 | let route = ''; 20 | 21 | for (let i = 0; i < 10001; i++) { 22 | route += String(i % 10); 23 | } 24 | 25 | let matcher = () => { 26 | return match('/api/:version?/users/:id', route); 27 | }; 28 | 29 | expect(matcher).toThrow(); 30 | }); 31 | 32 | test('throws for non-strings', () => { 33 | let route = ''; 34 | 35 | let matcher = () => { 36 | return match(0, route); 37 | }; 38 | 39 | expect(matcher).toThrow(); 40 | }); 41 | 42 | test('allow large urls', () => { 43 | let route = ''; 44 | 45 | for (let i = 0; i < 10001; i++) { 46 | route += String(i % 10); 47 | } 48 | 49 | let matcher = jest.fn(() => { 50 | return match('/api/:version?/users/:id', route, true); 51 | }); 52 | 53 | matcher(); 54 | 55 | expect(matcher).toHaveReturned(); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const { test: valid } = require('@/index'); 2 | 3 | describe('test', () => { 4 | test('test that a matching url returns true', () => { 5 | const route = '/api/v1/users/1'; 6 | const matches = valid('/api/:version?/users/:id', route); 7 | 8 | expect(matches).toBe(true); 9 | }); 10 | 11 | test('test that a unmatching url returns false', () => { 12 | const route = '/api/v1/films/1'; 13 | const matches = valid('/api/:version?/users/:id', route); 14 | 15 | expect(matches).toBe(false); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/utilities/buildExpression.js: -------------------------------------------------------------------------------- 1 | const { buildExpression } = require('@/utilities'); 2 | 3 | describe('utilities: buildExpression', () => { 4 | test('returns a regular expression', () => { 5 | let expression = buildExpression('/api/:version?/users/:id'); 6 | 7 | expect(expression).toBeInstanceOf(RegExp); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/utilities/escapePattern.js: -------------------------------------------------------------------------------- 1 | const { escapeExpression } = require('@/utilities'); 2 | 3 | describe('utilities: escapeExpression', () => { 4 | test('escapes special characters from the pattern', () => { 5 | let escaped = escapeExpression('/api/:version?/users/:id'); 6 | 7 | expect(escaped).toBe('/api/:version\\?/users/:id'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/utilities/getMatches.js: -------------------------------------------------------------------------------- 1 | const { getMatches } = require('@/utilities'); 2 | 3 | describe('utilities: getMatches', () => { 4 | test('get the matches from a pattern', () => { 5 | let matches = getMatches(/:([^/\\?]+)/g, '/api/:version?/users/:id'); 6 | 7 | expect(matches).toStrictEqual(['version', 'id']); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/utilities/getTokens.js: -------------------------------------------------------------------------------- 1 | const { getTokens } = require('@/utilities'); 2 | 3 | describe('utilities: getTokens', () => { 4 | test('get the tokens from a pattern', () => { 5 | let tokens = getTokens('/api/:version?/users/:id'); 6 | 7 | expect(tokens).toStrictEqual(['version', 'id']); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/utilities/zipObject.js: -------------------------------------------------------------------------------- 1 | const { zipObject } = require('@/utilities'); 2 | 3 | describe('utilities: zipObject', () => { 4 | test('returns an object', () => { 5 | let values = ['v1']; 6 | let keys = ['version']; 7 | let zipped = zipObject(keys, values); 8 | 9 | expect(typeof zipped).toBe('object'); 10 | }); 11 | 12 | test('keys and values of differing lengths is normalized', () => { 13 | let keys = ['version']; 14 | let values = ['v1', 'users']; 15 | let zipped = zipObject(keys, values); 16 | 17 | expect(Object.keys(zipped)).toHaveLength(1); 18 | }); 19 | 20 | test('not passing any keys returns an empty object', () => { 21 | let values = ['v1', 'users']; 22 | let zipped = zipObject(null, values); 23 | 24 | expect(Object.keys(zipped)).toHaveLength(0); 25 | }); 26 | 27 | test('not passing any values returns an empty object', () => { 28 | let keys = ['version']; 29 | let zipped = zipObject(keys, null); 30 | 31 | expect(Object.keys(zipped)).toHaveLength(0); 32 | }); 33 | }); 34 | --------------------------------------------------------------------------------