├── .gitignore ├── package.json ├── README.md ├── index.js └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | .DS_Store -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "open-path", 3 | "version": "1.0.6", 4 | "description": "Url path parser for OpenAPI.", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+ssh://git@github.com/Spikef/open-path.git" 9 | }, 10 | "keywords": [ 11 | "OpenAPI", 12 | "path", 13 | "parser" 14 | ], 15 | "scripts": { 16 | "test": "mocha test --recursive" 17 | }, 18 | "author": "Spikef", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/Spikef/open-path/issues" 22 | }, 23 | "homepage": "https://github.com/Spikef/open-path#readme", 24 | "devDependencies": { 25 | "mocha": "^4.1.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # open-path 2 | 3 | > Url path parser for OpenAPI. 4 | 5 | ## Install 6 | 7 | ```bash 8 | $ npm i open-path 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```javascript 14 | var Parse = require('open-path'); 15 | var parse = new Parse('/user/{category}/today/{tag}/detail'); 16 | // or 17 | // var parse = new Parse('/user/:category/today/:tag/detail'); 18 | 19 | console.log(parse.path); 20 | // prints: /user/{category}/today/{tag}/detail 21 | 22 | // match 23 | parse.match(ctx.path); // returns params if matched or null 24 | 25 | // build 26 | parse.build({ 27 | category: 'tech', 28 | tag: 'mobile' 29 | }); 30 | // returns: /user/tech/today/mobile/detail 31 | ``` 32 | 33 | ## Parameters 34 | 35 | The path argument is used to define parameters. 36 | 37 | ### Named Parameters 38 | 39 | Named parameters are defined by prefixing a colon to the parameter name (:foo) or surround with braces ({foo}). By default, the parameter will match until the following path segment. 40 | 41 | ### Unnamed Parameters 42 | 43 | Unnamed parameters are defined by regexp and naming by index. 44 | 45 | ### Parameter Modifiers 46 | 47 | #### Optional 48 | 49 | Parameters can be suffixed with a question mark (?) to make the parameter optional. 50 | 51 | ```javascript 52 | var parse = new Parse('/:foo/:bar?'); 53 | ``` 54 | 55 | #### Zero or more 56 | 57 | Parameters can be suffixed with an asterisk (*) to denote a zero or more parameter matches. The prefix is taken into account for each match. 58 | 59 | ```javascript 60 | var parse = new Parse('/:foo*'); 61 | ``` 62 | 63 | #### One or more 64 | 65 | Parameters can be suffixed with a plus sign (+) to denote a one or more parameter matches. The prefix is taken into account for each match. 66 | 67 | ```javascript 68 | var parse = new Parse('/:foo+'); 69 | ``` 70 | 71 | ## Path 72 | 73 | Path can be suffixed with an asterisk (*) or plus sign (+) to allow more path matches. 74 | 75 | ### Zero or more 76 | 77 | ```javascript 78 | var parse = new Parse('/api*'); 79 | ``` 80 | 81 | ### One or more 82 | 83 | ```javascript 84 | var parse = new Parse('/api+'); 85 | ``` 86 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var TYPES = { 2 | STRING: 1, 3 | REGEXP: 2 4 | }; 5 | 6 | /** 7 | * create path parse 8 | * @param {String|RegExp} path 9 | * @constructor 10 | */ 11 | function Parse(path) { 12 | if (!this instanceof Parse) throw new Error('Parse is a constructor, use new.'); 13 | 14 | if (typeof path === 'string') { 15 | this.type = TYPES.STRING; 16 | } else if (path instanceof RegExp) { 17 | this.type = TYPES.REGEXP; 18 | } else { 19 | throw new Error('Argument path must be String or RegExp.'); 20 | } 21 | 22 | var tokens = []; 23 | var params = []; 24 | var regexp = ''; 25 | 26 | if (this.type === TYPES.STRING) { 27 | var end = '(?:/?)$'; 28 | path.split(/(?=\/)/).forEach(function(p) { 29 | if (/^(?:(\/?)(?::([^*+?]+)|{(.+)})([*+?]?))$/.test(p)) { 30 | var p1 = RegExp.$1; 31 | var p2 = RegExp.$2 || RegExp.$3; 32 | var p3 = RegExp.$4; 33 | 34 | params.push(p2); 35 | tokens.push({ 36 | name: p2, 37 | prefix: p1, 38 | suffix: p3, 39 | required: !p3 || p3 === '+' 40 | }); 41 | 42 | if (p3 === '+' || p3 === '*') { 43 | regexp += '(?:' + p1 + '((?:[^/]+)' + p3 + '(?:/[^/]+)*))' + p3 + '?'; 44 | } else { 45 | regexp += '(?:' + p1 + '([^/]+))' + p3; 46 | } 47 | } else if (/[*+]$/.test(p)) { 48 | var r = p.substr(0, p.length - 1); 49 | var e = p.substr(p.length - 1); 50 | tokens.push(r); 51 | regexp += p; 52 | end = '(?:/[^/]+)' + e +'$'; 53 | } else { 54 | tokens.push(p); 55 | regexp += p; 56 | } 57 | }); 58 | 59 | this.path = path.replace(/:([^/]+)/g, '{$1}').replace(/[?*+]/g, ''); 60 | this.regexp = new RegExp('^' + regexp + end, 'i'); 61 | } else { 62 | this.path = path; 63 | this.regexp = path; 64 | } 65 | 66 | this.params = params; 67 | this.tokens = tokens; 68 | } 69 | 70 | Parse.prototype.match = function(path) { 71 | var matches; 72 | if (matches = path.match(this.regexp)) { 73 | var params = {}; 74 | 75 | if (this.type === TYPES.STRING) { 76 | this.params.forEach(function(key, i) { 77 | var value = matches[i + 1]; 78 | if (value !== undefined) { 79 | params[key] = safeDecodeURIComponent(value); 80 | } else { 81 | params[key] = value; 82 | } 83 | }); 84 | } else if (this.type === TYPES.REGEXP) { 85 | matches.forEach(function(m, i) { 86 | params[i] = matches[i] 87 | }); 88 | } 89 | 90 | return params; 91 | } else { 92 | return null; 93 | } 94 | }; 95 | 96 | Parse.prototype.build = function(params) { 97 | return this.tokens.map(function(token) { 98 | if (typeof token === 'string') { 99 | return token; 100 | } else if (params[token.name] !== undefined) { 101 | return token.prefix + String(params[token.name]); 102 | } else if (token.required) { 103 | return token.prefix + 'undefined'; 104 | } else { 105 | return ''; 106 | } 107 | }).join(''); 108 | }; 109 | 110 | function safeDecodeURIComponent(input) { 111 | try { 112 | return decodeURIComponent(input); 113 | } catch (e) { 114 | return input; 115 | } 116 | } 117 | 118 | module.exports = Parse; 119 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | var Parse = require('../index'); 4 | var parse1 = new Parse('/user/:category/today/{tag}?/detail'); 5 | var parse2 = new Parse('/page/:id?'); 6 | var parse3 = new Parse('/html/:filename*'); 7 | var parse4 = new Parse('/file/:filename+'); 8 | var parse5 = new Parse(/^\/article\/(\d+)(?:\/?)$/i); 9 | var parse6 = new Parse('/api*'); 10 | var parse7 = new Parse('/api+'); 11 | 12 | describe('Path resolve', function () { 13 | it('path should be [/user/{category}/today/{tag}/detail]', function () { 14 | assert.deepEqual('/user/{category}/today/{tag}/detail', parse1.path); 15 | }); 16 | }); 17 | 18 | describe('Path match', function () { 19 | it('[/user/tech/today/mobile/detail] should matched', function () { 20 | assert.deepEqual({ 21 | category: 'tech', 22 | tag: 'mobile' 23 | }, parse1.match('/user/tech/today/mobile/detail')); 24 | }); 25 | 26 | it('[/user/tech/today/detail/detail] should matched', function () { 27 | assert.deepEqual({ 28 | category: 'tech', 29 | tag: 'detail' 30 | }, parse1.match('/user/tech/today/detail/detail')); 31 | }); 32 | 33 | it('[/user/tech/today/detail] should matched', function () { 34 | assert.deepEqual({ 35 | category: 'tech', 36 | tag: undefined 37 | }, parse1.match('/user/tech/today/detail')); 38 | }); 39 | 40 | it('[/user/tech/today] should not matched', function () { 41 | assert.deepEqual(null, parse1.match('/user/tech')); 42 | }); 43 | }); 44 | 45 | describe('Path build', function () { 46 | it('path should be [/user/tech/today/mobile/detail]', function () { 47 | assert.deepEqual('/user/tech/today/mobile/detail', parse1.build({ 48 | category: 'tech', 49 | tag: 'mobile' 50 | })); 51 | }); 52 | 53 | it('path should be [/user/tech/today/mobile/detail]', function () { 54 | assert.deepEqual('/user/undefined/today/detail', parse1.build({})); 55 | }); 56 | }); 57 | 58 | describe('Path match with ?', function () { 59 | it('[/page] should matched', function () { 60 | assert.deepEqual({ 61 | id: undefined 62 | }, parse2.match('/page')); 63 | }); 64 | 65 | it('[/page/1] should matched', function () { 66 | assert.deepEqual({ 67 | id: 1 68 | }, parse2.match('/page/1')); 69 | }); 70 | 71 | it('[/page/1/2] should not matched', function () { 72 | assert.deepEqual(null, parse2.match('/page/1/2')); 73 | }); 74 | }); 75 | 76 | describe('Path match with *', function () { 77 | it('[/html] should matched', function () { 78 | assert.deepEqual({ 79 | filename: undefined 80 | }, parse3.match('/html')); 81 | }); 82 | 83 | it('[/html/] should matched', function () { 84 | assert.deepEqual({ 85 | filename: undefined 86 | }, parse3.match('/html/')); 87 | }); 88 | 89 | it('[/html/lib/] should matched', function () { 90 | assert.deepEqual({ 91 | filename: 'lib' 92 | }, parse3.match('/html/lib/')); 93 | }); 94 | 95 | it('[/html/index.js] should matched', function () { 96 | assert.deepEqual({ 97 | filename: 'index.js' 98 | }, parse3.match('/html/index.js')); 99 | }); 100 | 101 | it('[/html/lib/index.js] should matched', function () { 102 | assert.deepEqual({ 103 | filename: 'lib/index.js' 104 | }, parse3.match('/html/lib/index.js')); 105 | }); 106 | 107 | it('[/html/lib/images/avatar.png] should matched', function () { 108 | assert.deepEqual({ 109 | filename: 'lib/images/avatar.png' 110 | }, parse3.match('/html/lib/images/avatar.png')); 111 | }); 112 | }); 113 | 114 | describe('Path match with +', function () { 115 | it('[/file] should not matched', function () { 116 | assert.deepEqual(null, parse4.match('/file')); 117 | }); 118 | 119 | it('[/file/] should not matched', function () { 120 | assert.deepEqual(null, parse4.match('/file/')); 121 | }); 122 | 123 | it('[/file/images/] should matched', function () { 124 | assert.deepEqual({ 125 | filename: 'images' 126 | }, parse4.match('/file/images/')); 127 | }); 128 | 129 | it('[/file/avatar.png] should matched', function () { 130 | assert.deepEqual({ 131 | filename: 'avatar.png' 132 | }, parse4.match('/file/avatar.png')); 133 | }); 134 | 135 | it('[/file/images/avatar.png] should matched', function () { 136 | assert.deepEqual({ 137 | filename: 'images/avatar.png' 138 | }, parse4.match('/file/images/avatar.png')); 139 | }); 140 | }); 141 | 142 | describe('Path regexp', function () { 143 | it('[/article/1] should matched', function () { 144 | assert.deepEqual({0: '/article/1', 1: '1'}, parse5.match('/article/1')); 145 | }); 146 | 147 | it('[/article/1/] should matched', function () { 148 | assert.deepEqual(null, parse5.match('/new/article/1/')); 149 | }); 150 | 151 | it('[/new/article/1] should not matched', function () { 152 | assert.deepEqual(null, parse5.match('/new/article/1')); 153 | }); 154 | }); 155 | 156 | describe('Path match ends with *', function () { 157 | it('[/api] should matched', function () { 158 | assert.deepEqual({}, parse6.match('/api')); 159 | }); 160 | 161 | it('[/api/post] should matched', function () { 162 | assert.deepEqual({}, parse6.match('/api/post')); 163 | }); 164 | 165 | it('[/api-post] should not matched', function () { 166 | assert.deepEqual(null, parse6.match('/api-post')); 167 | }); 168 | }); 169 | 170 | describe('Path match ends with +', function () { 171 | it('[/api] should not matched', function () { 172 | assert.deepEqual(null, parse7.match('/api')); 173 | }); 174 | 175 | it('[/api/post] should matched', function () { 176 | assert.deepEqual({}, parse7.match('/api/post')); 177 | }); 178 | 179 | it('[/api-post] should not matched', function () { 180 | assert.deepEqual(null, parse7.match('/api-post')); 181 | }); 182 | }); 183 | --------------------------------------------------------------------------------