├── .gitignore ├── LICENSE ├── README.md ├── example ├── example1.js └── example2.js ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 RangerMauve 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 | mqtt-regex 2 | ========== 3 | 4 | Converts an MQTT topic with parameters into a regular expression. 5 | 6 | _Unless you need regex support, you should use [mqtt-pattern](https://www.npmjs.com/package/mqtt-pattern) which is faster_ 7 | 8 | ## Example 9 | 10 | ``` javascript 11 | var mqtt_regex = require("mqtt-regex"); 12 | 13 | var pattern = "chat/+id/+user/#path"; 14 | 15 | var room_message_info = mqtt_regex(pattern).exec; 16 | 17 | var topic = "chat/lobby/bob/text"; 18 | 19 | var message_content = "Hello, World!"; 20 | 21 | var params = room_message_info(topic); 22 | 23 | if(params && (params.path.indexOf("text") !== -1)) { 24 | chat.getRoom(params.id).sendFrom(params.user, message_content) 25 | } 26 | ``` 27 | 28 | ## Installing 29 | 30 | With npm: 31 | 32 | $ npm install --save mqtt-regex 33 | 34 | To use it in the browser, either compile a package with `node run build` or 35 | use [Browserify](http://browserify.org). 36 | 37 | ## API 38 | The API is super simple and should be easy to integrate with any project 39 | 40 | ### mqtt_regex(topic_pattern) 41 | Takes an MQTT topic pattern, and generates a RegExp object along with a function for parsing params from the result. The results also have an `exec` function that does both. 42 | The return looks like 43 | ``` javascript 44 | { 45 | regex: "RegExp object for matching" 46 | getParams: function(results){ 47 | // Processes results from RegExp.prototype.exec 48 | // Returns an object containing the values for each param 49 | }, 50 | exec: function(topic){ 51 | // Performs regex.exec on topic 52 | // If there was a match, parses parameters and returns result 53 | }, 54 | topic: "Regular MQTT topic with the pattern stuff stripped out", 55 | format: function(params){ 56 | // Generate a string with the params filled into the pattern 57 | return "formatted/pattern/here 58 | } 59 | } 60 | ``` 61 | 62 | ## How params work 63 | 64 | MQTT defines two types of "wildcards", one for matching a single section of the path (`+`), and one for zero or more sections of the path (`#`). 65 | Note that the `#` wildcard can only be used if it's at the end of the topic. 66 | This library was inspired by the syntax in the routers for Express and Sinatra, and an attempt was made to have this just as simple to use. 67 | 68 | ### Examples of topic patterns: 69 | 70 | #### user/+id/#path 71 | This would match paths that start with `user/`, and then extract the next section as the user `id`. 72 | Then it would get all subsequent paths and turn them into an array for the `path` param. 73 | Here is some input/output that you can expect: 74 | 75 | user/bob/status/mood: {id: "bob", path:["status","mood"] 76 | user/bob: {id:"bob", path: []} 77 | user/bob/ishungry: {id: "bob", path: ["ishungry"] 78 | 79 | #### device/+/+/component/+type/#path 80 | Not all wildcards need to be associated with a parameter, and it could be useful to just use plain MQTT topics. 81 | In this example you might only care about the status of some part of a device, and are willing to ignore a part of the path. 82 | Here are some examples of what this might be used with: 83 | 84 | device/deviceversion/deviceidhere/component/infrared/status/active: {type:"infrared",path: ["status","active"]} 85 | -------------------------------------------------------------------------------- /example/example1.js: -------------------------------------------------------------------------------- 1 | var mqtt_regex = require("../index.js"); 2 | 3 | var tests = { 4 | basic: { 5 | pattern: "foo/bar/baz", 6 | tests: ["foo/bar/baz", "foo/bar"] 7 | }, 8 | single_1: { 9 | pattern: "foo/+bar/baz", 10 | tests: ["foo/bar/baz", "foo/bar"] 11 | }, 12 | single_2: { 13 | pattern: "foo/bar/+baz", 14 | tests: ["foo/bar/baz", "foo/bar"] 15 | }, 16 | multi_1: { 17 | pattern: "foo/#bar", 18 | tests: ["foo/bar/baz", "foo/bar", "foo"] 19 | }, 20 | multi_2: { 21 | pattern: "foo/bar/#baz", 22 | tests: ["foo/bar/baz", "foo/bar/fizz/baz", "foo/baz"] 23 | }, 24 | complex_1: { 25 | pattern: "foo/+baz/#bar", 26 | tests: ["foo/bar/baz", "foo/bar/baz/fizz", "foo/bar"] 27 | } 28 | } 29 | 30 | Object.keys(tests).forEach(function(name) { 31 | var test = tests[name]; 32 | var pattern = test.pattern; 33 | var cases = test.tests; 34 | console.log("Processing test", name, "\n"); 35 | var compiled = mqtt_regex(pattern); 36 | console.log("Compiled", pattern, "to:", compiled.regex, "\n"); 37 | console.log("Running test cases:"); 38 | cases.forEach(function(topic, index) { 39 | console.log("Running #" + index + ":", topic); 40 | 41 | var matches = compiled.regex.exec(topic); 42 | if (matches) console.log("\tMatched:\t", matches.slice(1)); 43 | else return console.log("\tFailed\n"); 44 | 45 | var params = compiled.getParams(matches); 46 | console.log("\tGot params:\t", params); 47 | console.log(); 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /example/example2.js: -------------------------------------------------------------------------------- 1 | const mqtt_regex = require("../index.js") 2 | 3 | const pattern = '+foo/bar/#baz' 4 | const expected = '1/bar/2/3' 5 | 6 | const formatter = mqtt_regex(pattern).format 7 | const formatted = formatter({ 8 | foo: 1, baz: [2,3] 9 | }) 10 | 11 | console.log({ 12 | pattern, 13 | formatted, 14 | expected 15 | }) 16 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2014 RangerMauve 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | var escapeRegex = require('escape-string-regexp'); 26 | 27 | module.exports = parse; 28 | 29 | /** 30 | * Parses topic string with parameters 31 | * @param topic Topic string with optional params 32 | * @returns {Object} Compiles a regex for matching topics, getParams for getting params, and exec for doing both 33 | */ 34 | function parse(topic) { 35 | var tokens = tokenize(topic).map(process_token); 36 | var result = { 37 | regex: make_regex(tokens), 38 | getParams: make_pram_getter(tokens), 39 | topic: make_clean_topic(tokens), 40 | format: make_formatter(topic) 41 | }; 42 | result.exec = exec.bind(result); 43 | return result; 44 | }; 45 | 46 | function make_formatter(topic) { 47 | function formatter(params) { 48 | var tokens = tokenize(topic) 49 | return tokens.map(function(token) { 50 | if (token[0] == '+') { 51 | return params[token.slice(1)] 52 | } else if(token[0] == '#') { 53 | // If the param is an array, turn it into a path 54 | return [].concat(params[token.slice(1)] || []).join('/') 55 | } else { 56 | return token 57 | } 58 | }).flat().join('/') 59 | } 60 | return formatter 61 | 62 | } 63 | 64 | 65 | /** 66 | * Matches regex against topic, returns params if successful 67 | * @param topic Topic to match 68 | */ 69 | function exec(topic) { 70 | var regex = this.regex; 71 | var getParams = this.getParams; 72 | var match = regex.exec(topic); 73 | if (match) return getParams(match); 74 | } 75 | 76 | // Split the topic into consumable tokens 77 | function tokenize(topic) { 78 | return topic.split("/"); 79 | } 80 | 81 | // Processes token and determines if it's a `single`, `multi` or `raw` token 82 | // Each token contains the type, an optional parameter name, and a piece of the regex 83 | // The piece can have a different syntax for when it is last 84 | function process_token(token, index, tokens) { 85 | var last = (index === (tokens.length - 1)); 86 | if (token[0] === "+") return process_single(token, last); 87 | else if (token[0] === "#") return process_multi(token, last); 88 | else return process_raw(token, last); 89 | } 90 | 91 | // Processes a token for single paths (prefixed with a +) 92 | function process_single(token) { 93 | var name = token.slice(1); 94 | return { 95 | type: "single", 96 | name: name, 97 | piece: "([^/#+]+/)", 98 | last: "([^/#+]+/?)" 99 | }; 100 | } 101 | 102 | // Processes a token for multiple paths (prefixed with a #) 103 | function process_multi(token, last) { 104 | if (!last) throw new Error("# wildcard must be at the end of the pattern"); 105 | var name = token.slice(1); 106 | return { 107 | type: "multi", 108 | name: name, 109 | piece: "((?:[^/#+]+/)*)", 110 | last: "((?:[^/#+]+/?)*)" 111 | } 112 | } 113 | 114 | // Processes a raw string for the path, no special logic is expected 115 | function process_raw(token) { 116 | var token = escapeRegex(token); 117 | return { 118 | type: "raw", 119 | piece: token + "/", 120 | last: token + "/?" 121 | }; 122 | } 123 | 124 | // Turn a topic pattern into a regular MQTT topic 125 | function make_clean_topic(tokens) { 126 | return tokens.map(function (token) { 127 | if (token.type === "raw") return token.piece.slice(0, -1); 128 | else if (token.type === "single") return "+"; 129 | else if (token.type === "multi") return "#"; 130 | else return ""; // Wat 131 | }).join("/"); 132 | } 133 | 134 | // Generates the RegExp object from the tokens 135 | function make_regex(tokens) { 136 | var str = tokens.reduce(function (res, token, index) { 137 | var is_last = (index == (tokens.length - 1)); 138 | var before_multi = (index === (tokens.length - 2)) && (last(tokens).type == "multi"); 139 | return res + ((is_last || before_multi) ? token.last : token.piece); 140 | }, 141 | ""); 142 | return new RegExp("^" + str + "$"); 143 | } 144 | 145 | // Generates the function for getting the params object from the regex results 146 | function make_pram_getter(tokens) { 147 | return function (results) { 148 | // Get only the capturing tokens 149 | var capture_tokens = remove_raw(tokens); 150 | var res = {}; 151 | 152 | // If the regex didn't actually match, just return an empty object 153 | if (!results) return res; 154 | 155 | // Remove the first item and iterate through the capture groups 156 | results.slice(1).forEach(function (capture, index) { 157 | // Retreive the token description for the capture group 158 | var token = capture_tokens[index]; 159 | var param = capture; 160 | // If the token doesn't have a name, continue to next group 161 | if (!token.name) return; 162 | 163 | // If the token is `multi`, split the capture along `/`, remove empty items 164 | if (token.type === "multi") { 165 | param = capture.split("/"); 166 | if (!last(param)) 167 | param = remove_last(param); 168 | // Otherwise, remove any trailing `/` 169 | } else if (last(capture) === "/") 170 | param = remove_last(capture); 171 | // Set the param on the result object 172 | res[token.name] = param; 173 | }); 174 | return res; 175 | } 176 | } 177 | 178 | // Removes any tokens of type `raw` 179 | function remove_raw(tokens) { 180 | return tokens.filter(function (token) { 181 | return (token.type !== "raw"); 182 | }) 183 | } 184 | 185 | // Gets the last item or character 186 | function last(items) { 187 | return items[items.length - 1]; 188 | } 189 | 190 | // Returns everything but the last item or character 191 | function remove_last(items) { 192 | return items.slice(0, items.length - 1); 193 | } 194 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mqtt-regex", 3 | "version": "1.1.0", 4 | "description": "Converts an MQTT topic with parameters into a regular expression.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "browserify index.js --standalone mqtt_regex > build/build.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/RangerMauve/mqtt-regex.git" 13 | }, 14 | "keywords": [ 15 | "mqtt", 16 | "regex", 17 | "router" 18 | ], 19 | "author": "RangerMauve", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/RangerMauve/mqtt-regex/issues" 23 | }, 24 | "homepage": "https://github.com/RangerMauve/mqtt-regex", 25 | "devDependencies": { 26 | "browserify": "^5.12.1" 27 | }, 28 | "dependencies": { 29 | "escape-string-regexp": "^1.0.2" 30 | } 31 | } 32 | --------------------------------------------------------------------------------