├── .gitignore ├── LICENSE ├── README.md ├── examples ├── route_specific.js └── top_level.js ├── index.js ├── lib ├── parsers │ └── default-matrix-parser.js └── query.js ├── package.json └── test └── matrix-parser.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2016 Raghav Dua 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | matrix-parser is a Node.js Middleware for parsing [Matrix URIs](https://www.w3.org/DesignIssues/MatrixURIs.html) 2 | 3 | #Install 4 | ```bash 5 | npm install matrix-parser 6 | ``` 7 | 8 | #Run Tests 9 | traverse to the root directory 'matrix-parser' and: 10 | ```bash 11 | npm install 12 | npm test 13 | ``` 14 | 15 | #API 16 | ```javascript 17 | var matrixParser = require ('matrix-parser'); 18 | ``` 19 | 20 | The ```matrixParser``` object exposes a single function - the Middleware to parse matrix-style URIs. When used, it creates the matrix Array inside the request object, which can be accessed like ```req.matrix``` 21 | 22 | ##Options 23 | You can pass an ```options``` object when calling matrixParser (). The object currently supports the ```maxKeys``` field, used to specify **the maximum number of keys allowed per segment**. Keys are added into the matrix object from left to right. 24 | 25 | For example, using ```matrixParser ({ maxKeys: 2 })``` and a URI ```http://example.com/home;k1=v1;k2=v2;k3=v3``` will simply neglect all key-value pairs after k2. 26 | 27 | **NOTE:** The middleware currently doesn't support Matrix URIs to be used in combination with query strings. 28 | You could use matrix parameters **before** the '?', followed by query parameters, but mixing is not currently supported. 29 | 30 | #Rules 31 | Here are the rules matrix-parser follows to parse Matrix URIs. Check out [this thread](https://github.com/medialize/URI.js/issues/181) to understand in more detail. 32 | 33 | 1. The semicolon ';' is used to delimit key-value pairs (unlike the ampersand '&' in query strings) 34 | eg- /index;a=b;c=d 35 | 36 | 2. '=' is used to delimit the keys and values 37 | 38 | 3. The string between / and the first ; is the segment name. 39 | eg- /index;hello=world 40 | here, segment = "index" 41 | 42 | 4. All keys are unique. For compatibility, the key's last declaration is used to assign its value 43 | eg- /index;key1=value1;key2=v2;key1=helloworld 44 | here, the value of key1 = 'helloworld' (NOT value1) 45 | 46 | 5. Comma ',' is used to assign multiple values to a key 47 | eg- /index;list=1,2,3,4 48 | here, list = [1,2,3,4] (array containing the comma-separated values) 49 | 50 | 6. Spaces are encoded as %20 instead of + 51 | eg- /index;msg=hello%20world 52 | 53 | 7. If a key is not followed by the delimiter '=' and value, it is discarded 54 | eg- /index;a=b;noValueKey;c=d 55 | here, noValueKey will be discarded and will not be present in the matrix 56 | 57 | 8. If a key is followed by the delimiter '=' but not a value, its value is set to '' 58 | eg- /index;key=;a=b 59 | here, key = '' 60 | 61 | 9. If no segment name is given, it defaults to '' 62 | eg- http://example.com/;key=value 63 | here, segment = '' and key = 'value' 64 | 65 | 10. If the path name is empty, req.matrix defaults an array of single object with segment = '' and matrix = {} 66 | eg- http://example.com/ 67 | here, req.matrix = [ {segment: "", matrix: {}} ] 68 | 69 | #Format 70 | A typical Matrix URI looks like: 71 | ``` 72 | http://:/;=;=/;= 73 | ``` 74 | 75 | The constructed req.matrix looks like: 76 | ``` 77 | [ 78 | { 79 | segment: , 80 | matrix: { 81 | : , 82 | : 83 | } 84 | }, 85 | { 86 | segment: , 87 | matrix: { 88 | : 89 | } 90 | } 91 | ] 92 | ``` 93 | 94 | #Examples 95 | 96 | ###Express: Top-level generic 97 | ```javascript 98 | var app = require ('express') (), 99 | matrixParser = require ('matrix-parser'); 100 | 101 | app 102 | .use (matrixParser ()) 103 | .get ('*', function (req, res, next) { 104 | res.header ('Content-Type', 'text/plain'); 105 | res.write ('You posted: '); 106 | res.end (JSON.stringify (req.matrix, null, 2)); 107 | }) 108 | .listen (8080, function () { 109 | console.log ('listening on port 8080'); 110 | }); 111 | ``` 112 | 113 | ###Test: 114 | ```bash 115 | curl 'http://localhost:8080/index;name=RAGHAV%20DUA;house=targaryen/profile;age=20;email=duaraghav8%40gmail.com' 116 | ``` 117 | 118 | NOTICE THE %20 USED TO INDICATE SPACE INSTEAD OF + 119 | 120 | Output Construct of ```req.matrix```: 121 | ```javascript 122 | [ 123 | { 124 | segment: 'index', 125 | matrix: { 126 | name: 'RAGHAV DUA', 127 | house: 'targaryen' 128 | } 129 | }, 130 | { 131 | segment: 'profile', 132 | matrix: { 133 | age: 20, 134 | email: 'duaraghav8@gmail.com' 135 | } 136 | } 137 | ] 138 | ``` 139 | 140 | **NOTE:** If you want to allow Matrix on a specific route (like '/index'), then you have to append '*' to it like 141 | ```javascript 142 | app.get ('/index*', function (req, res) { 143 | /* 144 | this means that the first segment of the matrix is always "index" 145 | and only URIs starting with /index are valid, like: 146 | http://example.com/index;hello=world (VALID) 147 | http://example.com/home;hello=world (INVALID) 148 | */ 149 | assert (req.matrix [0].segment === "index"); //no assertion errors =) 150 | }); 151 | ``` 152 | 153 | ###Express: Route-specific 154 | ```javascript 155 | var app = require ('express') (), 156 | matrixParser = require ('matrix-parser'); 157 | 158 | var colorCodes = { 159 | "white": "#FFFFFF", 160 | "black": "#000000" 161 | }; 162 | 163 | var mpMiddleware = matrixParser ({ maxKeys: 1 }); //notice the use of the maxKeys option 164 | 165 | app 166 | .get ('/index*', mpMiddleware, function (req, res, next) { 167 | var color = req.matrix [0].matrix.color; //req.matrix [0] refers to parameters provided in the /index segment 168 | res.send ('Color code for ' + color + ' is ' + colorCodes [color]); 169 | }) 170 | .listen (8080, function () { 171 | console.log ('listening on port 8080'); 172 | }); 173 | ``` 174 | 175 | ###Test 1: 176 | ```bash 177 | curl 'http://localhost:8080/index;color=white' 178 | ``` 179 | 180 | Output: ```Color code for white is #FFFFFF``` 181 | 182 | ###Test 2: 183 | ```bash 184 | curl 'http://localhost:8080/index;color=indigo' 185 | ``` 186 | 187 | Output: ```Color code for white is undefined``` 188 | (because color code for indigo is not defined in colorCodes object in our script) 189 | 190 | #License 191 | MIT 192 | -------------------------------------------------------------------------------- /examples/route_specific.js: -------------------------------------------------------------------------------- 1 | var app = require ('express') (), 2 | matrixParser = require ('..'); 3 | 4 | var colorCodes = { 5 | "white": "#FFFFFF", 6 | "black": "#000000" 7 | }; 8 | 9 | var mpMiddleware = matrixParser (); 10 | 11 | app 12 | .get ('/index*', mpMiddleware, function (req, res, next) { 13 | var color = req.matrix [0].matrix.color; //req.matrix [0] refers to parameters provided in the /index segment 14 | res.send ('Color code for ' + color + ' is ' + colorCodes [color]); 15 | }) 16 | 17 | .listen (8080, function () { 18 | console.log ('listening on port 8080'); 19 | }); -------------------------------------------------------------------------------- /examples/top_level.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var app = require ('express') (), 4 | //matrixParser = require ('matrix-parser'); 5 | matrixParser = require ('..'); 6 | 7 | app 8 | //.use (matrixParser ({ maxKeys: 2 })) 9 | .use (matrixParser ()) 10 | 11 | .get ('*', function (req, res, next) { 12 | res.header ('Content-Type', 'text/plain'); 13 | res.write ('You posted: '); 14 | res.end (JSON.stringify (req.matrix, null, 2)); 15 | }) 16 | 17 | .listen (8080, function () { 18 | console.log ('listening on port 8080'); 19 | }); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * matrix-parser 3 | * Copyright(c) 2016 Raghav Dua 4 | * MIT Licensed 5 | */ 6 | 7 | 'use strict'; 8 | 9 | module.exports = require ('./lib/query'); 10 | -------------------------------------------------------------------------------- /lib/parsers/default-matrix-parser.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * matrix-parser 3 | * Copyright(c) 2016 Raghav Dua 4 | * MIT Licensed 5 | */ 6 | 7 | 'use strict'; 8 | 9 | var queryParser = require ('querystring').parse; 10 | 11 | var normalizer = { 12 | useLastDeclared: function (m) { 13 | Object.keys (m).forEach (function (k) { 14 | var value = m [k]; 15 | m [k] = (value.constructor === Array) ? (value [value.length - 1]) : value; 16 | }); 17 | }, 18 | 19 | commaSeparate: function (m) { 20 | Object.keys (m).forEach (function (k) { 21 | if (m [k].indexOf (',') !== -1) m [k] = m [k].split (','); 22 | }); 23 | return m; 24 | }, 25 | 26 | removeTrailingSemicolons: function (seg) { 27 | var result = ''; 28 | for (var i = 0; i < seg.length; i++) { 29 | if (seg [i] === ';') { 30 | if (i === seg.length-1 || seg [i+1] === ';') continue; 31 | } 32 | result += seg [i]; 33 | } 34 | return result; 35 | }, 36 | 37 | removeDanglingKeys: function (seg, m) { 38 | var reg = /(?:;)[^;=]+(?=;)/g; 39 | var matches = (seg+';').match (reg); 40 | 41 | if (matches) { 42 | matches.forEach (function (k) { 43 | var key = k.slice (1); 44 | !m [key] && delete m [key]; 45 | }); 46 | } 47 | } 48 | }; 49 | 50 | function createMatrix (uri, opts) { 51 | var matrix = []; 52 | 53 | uri.split ('/').slice (1).forEach (function (segment) { 54 | segment = normalizer.removeTrailingSemicolons (segment); 55 | 56 | var firstSep = segment.indexOf (';'); 57 | if (firstSep === -1) firstSep = segment.length; 58 | 59 | var segObject = { segment: segment.slice (0, firstSep) }; 60 | var m = queryParser (segment.slice (firstSep + 1), ';', '=', opts); 61 | 62 | normalizer.useLastDeclared (m); 63 | segObject.matrix = normalizer.commaSeparate (m); 64 | normalizer.removeDanglingKeys (segment, segObject.matrix); 65 | 66 | matrix.push (segObject); 67 | }); 68 | 69 | return matrix; 70 | } 71 | 72 | function defaultMatrixParser (val, opts) { 73 | if (typeof val !== 'string' || !val.length) return []; 74 | return createMatrix (val, opts); 75 | }; 76 | 77 | module.exports = defaultMatrixParser; -------------------------------------------------------------------------------- /lib/query.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * matrix-parser 3 | * Copyright(c) 2016 Raghav Dua 4 | * MIT Licensed 5 | */ 6 | 7 | 'use strict'; 8 | 9 | var urlParser = require ('url').parse; 10 | var defaultMatrixParser = require ('./parsers/default-matrix-parser'); 11 | 12 | module.exports = function (options) { 13 | var opts = Object.create (options || null); 14 | var matrixParser = defaultMatrixParser; 15 | 16 | if (typeof options === 'function') { 17 | opts = undefined; 18 | matrixParser = options; 19 | } 20 | 21 | return function (req, res, next) { 22 | if (!req.matrix) { 23 | req.matrix = matrixParser (urlParser (req.url).pathname, opts); 24 | } 25 | next (); 26 | }; 27 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "matrix-parser", 3 | "version": "1.2.2", 4 | "description": "Node.js middleware for parsing Matrix URIs", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "./node_modules/.bin/mocha --reporter spec" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/duaraghav8/matrix-parser" 12 | }, 13 | "author": "Raghav Dua ", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/duaraghav8/matrix-parser/issues" 17 | }, 18 | "devDependencies": { 19 | "mocha": "~2.5.3", 20 | "supertest": "~1.2.0", 21 | "should": "~9.0.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/matrix-parser.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright(c) 2016 Raghav Dua 3 | * MIT Licensed 4 | */ 5 | 6 | 'use strict'; 7 | 8 | var express = require ('express'), 9 | should = require ('should'), 10 | request = require ('supertest'); 11 | 12 | var matrixParser = require ('..'); 13 | 14 | describe ('matrixParser ()', function () { 15 | var server; 16 | before (function () { 17 | server = createServer (); 18 | }); 19 | 20 | it ('should default to [ {segment: "", matrix: {}} ] when url = \'/\'', function (done) { 21 | request (server) 22 | .get ('/') 23 | .end (function (err, res) { 24 | res.body.length.should.equal (1); 25 | res.body [0].segment.should.equal (''); 26 | res.body [0].matrix.should.be.empty (); 27 | done (); 28 | }); 29 | }); 30 | 31 | it ('should default matrix sub-object to {} when no key-value pairs provided', function (done) { 32 | request (server) 33 | .get ('/home') 34 | .end (function (err, res) { 35 | res.body.length.should.equal (1); 36 | res.body [0].segment.should.equal ('home'); 37 | res.body [0].should.have.property ('matrix'); 38 | res.body [0].matrix.should.be.empty (); 39 | done (); 40 | }); 41 | }); 42 | 43 | it ('should ignore trailing semicolon(s)', function (done) { 44 | request (server) 45 | .get ('/home;name=raghav;') 46 | .end (function (err, res) { 47 | res.body.length.should.equal (1); 48 | res.body [0].matrix.should.have.property ('name', 'raghav'); 49 | res.body [0].matrix.should.not.have.property (''); 50 | done (); 51 | }); 52 | }); 53 | 54 | it ('should interpret %20 as space', function (done) { 55 | request (server) 56 | .get ('/index;name=raghav%20dua') 57 | .end (function (err, res) { 58 | res.body [0].matrix.should.have.property ('name', 'raghav dua'); 59 | done (); 60 | }); 61 | }); 62 | 63 | it ('should assign a key\'s last declaration as its value and discard its previous declarations', function (done) { 64 | request (server) 65 | .get ('/index;name=raghav;name=tyrion;name=lannister') 66 | .end (function (err, res) { 67 | res.body [0].matrix.name.should.equal ('lannister'); 68 | done (); 69 | }); 70 | }); 71 | 72 | it ('should ignore all keys without values and =', function (done) { 73 | request (server) 74 | .get ('/index;a;b=alphabet;c') 75 | .end (function (err, res) { 76 | res.body [0].matrix.should.have.property ('b', 'alphabet'); 77 | res.body [0].matrix.should.not.have.property ('a'); 78 | res.body [0].matrix.should.not.have.property ('c'); 79 | done (); 80 | }); 81 | }); 82 | 83 | it ('should set a key\'s value to \'\' (empty string) if its value is not provided after =', function (done) { 84 | request (server) 85 | .get ('/index;name=;age=20') 86 | .end (function (err, res) { 87 | res.body [0].matrix.should.have.property ('name', ''); 88 | res.body [0].matrix.should.have.property ('age', '20'); 89 | done (); 90 | }); 91 | }); 92 | 93 | it ('should set key to \'\' (empty string) when key is not specified before =', function (done) { 94 | request (server) 95 | .get ('/index;=value') 96 | .end (function (err, res) { 97 | res.body [0].matrix.should.have.property ('', 'value'); 98 | done (); 99 | }); 100 | }); 101 | 102 | it ('should set both key and value to \'\' (empty string) when they aren\'t specified', function (done) { 103 | request (server) 104 | .get ('/;=') 105 | .end (function (err, res) { 106 | res.body [0].matrix.should.have.property ('', ''); 107 | done (); 108 | }); 109 | }); 110 | 111 | it ('should parse multi-segment URIs', function (done) { 112 | request (server) 113 | .get ('/club;name=lakers;address=downtown/members;role=guest') 114 | .end (function (err, res) { 115 | res.body.length.should.equal (2); 116 | 117 | res.body [0].segment.should.equal ('club'); 118 | res.body [0].matrix.should.have.property ('name', 'lakers'); 119 | res.body [0].matrix.should.have.property ('address', 'downtown'); 120 | 121 | res.body [1].segment.should.equal ('members'); 122 | res.body [1].matrix.should.have.property ('role', 'guest'); 123 | 124 | done (); 125 | }); 126 | }); 127 | 128 | it ('should ignore query strings, i.e., EVERYTHING after \'?\'', function (done) { 129 | request (server) 130 | .get ('/team;name=lakers?city=LA') 131 | .end (function (err, res) { 132 | res.body.length.should.equal (1); 133 | res.body [0].segment.should.equal ('team'); 134 | res.body [0].matrix.should.have.property ('name', 'lakers'); 135 | done (); 136 | }); 137 | }); 138 | 139 | it ('should interpret comma-separated values as multiple values (array) for the (single) key', function (done) { 140 | request (server) 141 | .get ('/home;list=1,2,3,4') 142 | .end (function (err, res) { 143 | res.body [0].matrix.should.have.property ('list'); 144 | res.body [0].matrix.list.should.be.an.Array (); 145 | res.body [0].matrix.list.length.should.equal (4); 146 | 147 | for (var i = 1; i <= 4; i++) { 148 | res.body [0].matrix.list [i-1].should.equal (i.toString ()); 149 | } 150 | 151 | done (); 152 | }); 153 | }); 154 | 155 | it ('should interpret \'\' (empty string) if nothing exists between 2 commas: /index;list=a,b,,,', function (done) { 156 | request (server) 157 | .get ('/index;list=a,b,,,') 158 | .end (function (err, res) { 159 | res.body [0].matrix.should.have.property ('list'); 160 | res.body [0].matrix.list.should.be.an.Array (); 161 | res.body [0].matrix.list.length.should.equal (5); //['a','b','','',''] 162 | 163 | res.body [0].matrix.list [0].should.equal ('a'); 164 | res.body [0].matrix.list [1].should.equal ('b'); 165 | res.body [0].matrix.list [2].should.equal (''); 166 | res.body [0].matrix.list [3].should.equal (''); 167 | res.body [0].matrix.list [4].should.equal (''); 168 | 169 | done (); 170 | }); 171 | }); 172 | 173 | it ('should set segment to \'\' (empty string) when it encounters: /;key=value', function (done) { 174 | request (server) 175 | .get ('/;key=value/;green=house') 176 | .end (function (err, res) { 177 | res.body.length.should.equal (2); 178 | 179 | res.body [0].segment.should.equal (''); 180 | res.body [0].matrix.should.have.property ('key', 'value'); 181 | 182 | res.body [1].segment.should.equal (''); 183 | res.body [1].matrix.should.have.property ('green', 'house'); 184 | 185 | done (); 186 | }); 187 | }); 188 | }); 189 | 190 | function createServer () { 191 | var app = express (); 192 | 193 | //middleware 194 | app.use (matrixParser ()); 195 | 196 | //routes 197 | app.use (function (req, res) { 198 | res 199 | .status (200) 200 | .send (req.matrix); 201 | }); 202 | 203 | return app; 204 | } --------------------------------------------------------------------------------