mqtt-pattern 1.1.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintrc CHANGED
@@ -1,30 +1,30 @@
1
- {
2
- "extends": [
3
- "eslint:recommended",
4
- "plugin:node/recommended"
5
- ],
6
- "env": {},
7
- "globals": {
8
- "window": true
9
- },
10
- "plugins": [
11
- "node"
12
- ],
13
- "rules": {
14
- "no-undef": 2,
15
- "no-unused-vars": 2,
16
- "strict": [2, "global"],
17
- "new-cap": 0,
18
- "indent": ["error", "tab", {"MemberExpression": 1, "ArrayExpression": 1, "ObjectExpression": 1 }],
19
- "no-console": 0,
20
- "no-invalid-this": 0,
21
- "node/no-missing-require": 1,
22
- "no-trailing-spaces": 2,
23
- "semi": ["error", "always"],
24
- "key-spacing": ["warn", { "beforeColon": false, "afterColon": true }],
25
- "space-infix-ops": ["warn"],
26
- "object-curly-newline": ["warn", {"minProperties": 1}],
27
- "no-shadow": ["error"],
28
- "no-undef-init": ["error"]
29
- }
30
- }
1
+ {
2
+ "extends": [
3
+ "eslint:recommended",
4
+ "plugin:node/recommended"
5
+ ],
6
+ "env": {},
7
+ "globals": {
8
+ "window": true
9
+ },
10
+ "plugins": [
11
+ "node"
12
+ ],
13
+ "rules": {
14
+ "no-undef": 2,
15
+ "no-unused-vars": 2,
16
+ "strict": [2, "global"],
17
+ "new-cap": 0,
18
+ "indent": ["error", "tab", {"MemberExpression": 1, "ArrayExpression": 1, "ObjectExpression": 1 }],
19
+ "no-console": 0,
20
+ "no-invalid-this": 0,
21
+ "node/no-missing-require": 1,
22
+ "no-trailing-spaces": 2,
23
+ "semi": ["error", "always"],
24
+ "key-spacing": ["warn", { "beforeColon": false, "afterColon": true }],
25
+ "space-infix-ops": ["warn"],
26
+ "object-curly-newline": ["warn", {"minProperties": 1}],
27
+ "no-shadow": ["error"],
28
+ "no-undef-init": ["error"]
29
+ }
30
+ }
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2017
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.
1
+ MIT License
2
+
3
+ Copyright (c) 2017
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.
package/README.md CHANGED
@@ -1,78 +1,84 @@
1
- # mqtt-pattern
2
- Fast library for matching MQTT patterns with named wildcards to extract data from topics
3
-
4
- Successor to [mqtt-regex](./mqtt-regex)
5
-
6
- ## Example:
7
-
8
- ``` javascript
9
- var MQTTPattern = require("mqtt-pattern");
10
-
11
- // Wildcards in patterns don't need names
12
- var pattern = "device/+id/+/#data";
13
-
14
- var topic = "device/fitbit/heartrate/rate/bpm";
15
-
16
- var params = MQTTPattern.exec(pattern, topic);
17
-
18
- // params will be
19
- {
20
- id: "fitbit",
21
- data: ["rate", "bmp"]
22
- }
23
-
24
- var filled = MQTTPattern.fill(pattern, params);
25
- // filled will be
26
- "device/fitbit/undefined/rate/bmp"
27
-
28
- ```
29
-
30
- ## Installing
31
-
32
- With NPM:
33
-
34
- ```bash
35
- npm install --save mqtt-pattern
36
- ```
37
-
38
- ## API
39
-
40
- ### `exec(pattern : String, topic : String) : Object | null`
41
- Validates that `topic` fits the `pattern` and parses out any parameters.
42
- If the topic doesn't match, it returns `null`
43
-
44
- ### `matches(pattern : String, topic : String) : Boolean`
45
- Validates whether `topic` fits the `pattern`. Ignores parameters.
46
-
47
- ### `extract(pattern : String, topic : String) : Object`
48
- Traverses the `pattern` and attempts to fetch parameters from the `topic`.
49
- Useful if you know in advance that your `topic` will be valid and want to extract data.
50
- If the `topic` doesn't match, or the `pattern` doesn't contain named wildcards, returns an empty object.
51
- Do not use this for validation.
52
-
53
- ### `fill(pattern : String, params: Object) : String`
54
- Reverse of `extract`, traverse the `pattern` and fill in params with keys in an object. Missing keys for `+` params are set to `undefined`. Missing keys for `#` params yeid empty strings.
55
-
56
- ## How params work
57
-
58
- 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 (`#`).
59
- Note that the `#` wildcard must only be used if it's at the end of the topic.
60
- This library was inspired by the syntax in the routers for web frameworks.
61
-
62
- ### Examples of topic patterns:
63
-
64
- #### user/+id/#path
65
- This would match paths that start with `user/`, and then extract the next section as the user `id`.
66
- Then it would get the following paths and turn them into an array for the `path` param.
67
- Here is some input/output that you can expect:
68
-
69
- user/bob/status/mood: {id: "bob", path:["status","mood"]
70
- user/bob: {id:"bob", path: []}
71
- user/bob/ishungry: {id: "bob", path: ["ishungry"]
72
-
73
- #### device/+/+/component/+type/#path
74
- Not all wildcards need to be associated with a parameter, and it could be useful to use plain MQTT topics.
75
- 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.
76
- Here are some examples of what this might be used with:
77
-
78
- device/deviceversion/deviceidhere/component/infrared/status/active: {type:"infrared",path: ["status","active"]}
1
+ # mqtt-pattern
2
+ Fast library for matching MQTT patterns with named wildcards to extract data from topics
3
+
4
+ Successor to [mqtt-regex](./mqtt-regex)
5
+
6
+ ## Example:
7
+
8
+ ``` javascript
9
+ var MQTTPattern = require("mqtt-pattern");
10
+
11
+ // Wildcards in patterns don't need names
12
+ var pattern = "device/+id/+/#data";
13
+
14
+ var topic = "device/fitbit/heartrate/rate/bpm";
15
+
16
+ var params = MQTTPattern.exec(pattern, topic);
17
+
18
+ // params will be
19
+ {
20
+ id: "fitbit",
21
+ data: ["rate", "bmp"]
22
+ }
23
+
24
+ var filled = MQTTPattern.fill(pattern, params);
25
+ // filled will be
26
+ "device/fitbit/undefined/rate/bmp"
27
+
28
+ MQTTPattern.clean("hello/+param1/world/#param2");
29
+ // hello/+/world/#
30
+
31
+ ```
32
+
33
+ ## Installing
34
+
35
+ With NPM:
36
+
37
+ ```bash
38
+ npm install --save mqtt-pattern
39
+ ```
40
+
41
+ ## API
42
+
43
+ ### `exec(pattern : String, topic : String) : Object | null`
44
+ Validates that `topic` fits the `pattern` and parses out any parameters.
45
+ If the topic doesn't match, it returns `null`
46
+
47
+ ### `matches(pattern : String, topic : String) : Boolean`
48
+ Validates whether `topic` fits the `pattern`. Ignores parameters.
49
+
50
+ ### `extract(pattern : String, topic : String) : Object`
51
+ Traverses the `pattern` and attempts to fetch parameters from the `topic`.
52
+ Useful if you know in advance that your `topic` will be valid and want to extract data.
53
+ If the `topic` doesn't match, or the `pattern` doesn't contain named wildcards, returns an empty object.
54
+ Do not use this for validation.
55
+
56
+ ### `fill(pattern : String, params: Object) : String`
57
+ Reverse of `extract`, traverse the `pattern` and fill in params with keys in an object. Missing keys for `+` params are set to `undefined`. Missing keys for `#` params yeid empty strings.
58
+
59
+ ### `clean(pattern : String) : String`
60
+ Removes the parameter names from a pattern.
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 must only be used if it's at the end of the topic.
66
+ This library was inspired by the syntax in the routers for web frameworks.
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 the following 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 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"]}
package/index.d.ts ADDED
@@ -0,0 +1,58 @@
1
+ import type { MqttParameters, FillTopic, CleanTopic } from "./parameters";
2
+ import type { F } from "ts-toolbelt";
3
+
4
+ /**
5
+ * Extract parameters from a topic based on the pattern provided
6
+ * @param pattern Pattern to match against
7
+ * @param topic Topic to extract parameters from
8
+ */
9
+ export declare function exec<Pattern extends string>(
10
+ pattern: Pattern,
11
+ topic: string
12
+ ): MqttParameters<Pattern> | null;
13
+
14
+ /**
15
+ * Tests if a topic is valid for a certain pattern.
16
+ * @param pattern Pattern to match against
17
+ * @param topic Topic to test
18
+ */
19
+ export declare function matches(pattern: string, topic: string): boolean;
20
+
21
+ /**
22
+ * Take a pattern and fill the parameters with the appropriate values
23
+ * @param pattern Pattern to fill
24
+ * @param params Parameters to insert into the pattern
25
+ */
26
+ export declare function fill<
27
+ Pattern extends string,
28
+ Params extends MqttParameters<Pattern>
29
+ >(
30
+ pattern: F.Narrow<Pattern>,
31
+ params: F.Narrow<Params>
32
+ ): FillTopic<Pattern, Params>;
33
+
34
+ /**
35
+ * Extract parameter values from a topic using a pattern.
36
+ *
37
+ * This function doesn't check if the pattern matches before attempting
38
+ * extraction, @see exec if you want to extract values, with assurances that the
39
+ * topic actually matches the pattern..
40
+ *
41
+ * @deprecated use exec for safety on topic matching the pattern
42
+ * @param pattern Pattern to extract using
43
+ * @param topic Topic to extract values from
44
+ */
45
+ export declare function extract<Pattern extends string>(
46
+ pattern: Pattern,
47
+ topic: string
48
+ ): MqttParameters<Pattern>;
49
+
50
+ /**
51
+ * Clean a pattern (remove any property names from wildcard segments)
52
+ *
53
+ * @param pattern Pattern to clean
54
+ * @returns cleaned pattern safe for usage in subscriptions etc.
55
+ */
56
+ export declare function clean<Pattern extends string>(
57
+ pattern: Pattern
58
+ ): CleanTopic<Pattern>;
package/index.js CHANGED
@@ -1,97 +1,121 @@
1
- "use strict";
2
- var SEPARATOR = "/";
3
- var SINGLE = "+";
4
- var ALL = "#";
5
-
6
- module.exports = {
7
- matches: matches,
8
- extract: extract,
9
- exec: exec,
10
- fill: fill,
11
- };
12
-
13
- function exec(pattern, topic) {
14
- return matches(pattern, topic) ? extract(pattern, topic) : null;
15
- }
16
-
17
- function matches(pattern, topic) {
18
- var patternSegments = pattern.split(SEPARATOR);
19
- var topicSegments = topic.split(SEPARATOR);
20
-
21
- var patternLength = patternSegments.length;
22
- var topicLength = topicSegments.length;
23
- var lastIndex = patternLength - 1;
24
-
25
- for(var i = 0; i < patternLength; i++){
26
- var currentPattern = patternSegments[i];
27
- var patternChar = currentPattern[0];
28
- var currentTopic = topicSegments[i];
29
-
30
- if(!currentTopic && !currentPattern)
31
- continue;
32
-
33
- if(!currentTopic && currentPattern !== ALL) return false;
34
-
35
- // Only allow # at end
36
- if(patternChar === ALL)
37
- return i === lastIndex;
38
- if(patternChar !== SINGLE && currentPattern !== currentTopic)
39
- return false;
40
- }
41
-
42
- return patternLength === topicLength;
43
- }
44
-
45
- function fill(pattern, params){
46
- var patternSegments = pattern.split(SEPARATOR);
47
- var patternLength = patternSegments.length;
48
-
49
- var result = [];
50
-
51
- for (var i = 0; i < patternLength; i++) {
52
- var currentPattern = patternSegments[i];
53
- var patternChar = currentPattern[0];
54
- var patternParam = currentPattern.slice(1);
55
- var paramValue = params[patternParam];
56
-
57
- if(patternChar === ALL){
58
- // Check that it isn't undefined
59
- if(paramValue !== void 0)
60
- result.push([].concat(paramValue).join(SEPARATOR)); // Ensure it's an array
61
-
62
- // Since # wildcards are always at the end, break out of the loop
63
- break;
64
- } else if (patternChar === SINGLE)
65
- // Coerce param into a string, missing params will be undefined
66
- result.push("" + paramValue);
67
- else result.push(currentPattern);
68
- }
69
-
70
- return result.join(SEPARATOR);
71
- }
72
-
73
-
74
- function extract(pattern, topic) {
75
- var params = {};
76
- var patternSegments = pattern.split(SEPARATOR);
77
- var topicSegments = topic.split(SEPARATOR);
78
-
79
- var patternLength = patternSegments.length;
80
-
81
- for(var i = 0; i < patternLength; i++){
82
- var currentPattern = patternSegments[i];
83
- var patternChar = currentPattern[0];
84
-
85
- if(currentPattern.length === 1)
86
- continue;
87
-
88
- if(patternChar === ALL){
89
- params[currentPattern.slice(1)] = topicSegments.slice(i);
90
- break;
91
- } else if(patternChar === SINGLE){
92
- params[currentPattern.slice(1)] = topicSegments[i];
93
- }
94
- }
95
-
96
- return params;
97
- }
1
+ "use strict";
2
+ var SEPARATOR = "/";
3
+ var SINGLE = "+";
4
+ var ALL = "#";
5
+
6
+ module.exports = {
7
+ matches: matches,
8
+ extract: extract,
9
+ exec: exec,
10
+ fill: fill,
11
+ clean: clean
12
+ };
13
+
14
+ function exec(pattern, topic) {
15
+ return matches(pattern, topic) ? extract(pattern, topic) : null;
16
+ }
17
+
18
+ function matches(pattern, topic) {
19
+ var patternSegments = pattern.split(SEPARATOR);
20
+ var topicSegments = topic.split(SEPARATOR);
21
+
22
+ var patternLength = patternSegments.length;
23
+ var topicLength = topicSegments.length;
24
+ var lastIndex = patternLength - 1;
25
+
26
+ for(var i = 0; i < patternLength; i++){
27
+ var currentPattern = patternSegments[i];
28
+ var patternChar = currentPattern[0];
29
+ var currentTopic = topicSegments[i];
30
+
31
+ if(!currentTopic && !currentPattern)
32
+ continue;
33
+
34
+ if(!currentTopic && currentPattern !== ALL) return false;
35
+
36
+ // Only allow # at end
37
+ if(patternChar === ALL)
38
+ return i === lastIndex;
39
+ if(patternChar !== SINGLE && currentPattern !== currentTopic)
40
+ return false;
41
+ }
42
+
43
+ return patternLength === topicLength;
44
+ }
45
+
46
+ function fill(pattern, params){
47
+ var patternSegments = pattern.split(SEPARATOR);
48
+ var patternLength = patternSegments.length;
49
+
50
+ var result = [];
51
+
52
+ for (var i = 0; i < patternLength; i++) {
53
+ var currentPattern = patternSegments[i];
54
+ var patternChar = currentPattern[0];
55
+ var patternParam = currentPattern.slice(1);
56
+ var paramValue = params[patternParam];
57
+
58
+ if(patternChar === ALL){
59
+ // Check that it isn't undefined
60
+ if(paramValue !== void 0)
61
+ result.push([].concat(paramValue).join(SEPARATOR)); // Ensure it's an array
62
+
63
+ // Since # wildcards are always at the end, break out of the loop
64
+ break;
65
+ } else if (patternChar === SINGLE)
66
+ // Coerce param into a string, missing params will be undefined
67
+ result.push("" + paramValue);
68
+ else result.push(currentPattern);
69
+ }
70
+
71
+ return result.join(SEPARATOR);
72
+ }
73
+
74
+
75
+ function extract(pattern, topic) {
76
+ var params = {};
77
+ var patternSegments = pattern.split(SEPARATOR);
78
+ var topicSegments = topic.split(SEPARATOR);
79
+
80
+ var patternLength = patternSegments.length;
81
+
82
+ for(var i = 0; i < patternLength; i++){
83
+ var currentPattern = patternSegments[i];
84
+ var patternChar = currentPattern[0];
85
+
86
+ if(currentPattern.length === 1)
87
+ continue;
88
+
89
+ if(patternChar === ALL){
90
+ params[currentPattern.slice(1)] = topicSegments.slice(i);
91
+ break;
92
+ } else if(patternChar === SINGLE){
93
+ params[currentPattern.slice(1)] = topicSegments[i];
94
+ }
95
+ }
96
+
97
+ return params;
98
+ }
99
+
100
+
101
+ function clean(pattern) {
102
+ var patternSegments = pattern.split(SEPARATOR);
103
+ var patternLength = patternSegments.length;
104
+
105
+ var cleanedSegments = [];
106
+
107
+ for(var i = 0; i < patternLength; i++){
108
+ var currentPattern = patternSegments[i];
109
+ var patternChar = currentPattern[0];
110
+
111
+ if(patternChar === ALL){
112
+ cleanedSegments.push(ALL);
113
+ } else if(patternChar === SINGLE){
114
+ cleanedSegments.push(SINGLE);
115
+ } else {
116
+ cleanedSegments.push(currentPattern);
117
+ }
118
+ }
119
+
120
+ return cleanedSegments.join('/');
121
+ }
package/package.json CHANGED
@@ -1,33 +1,35 @@
1
- {
2
- "name": "mqtt-pattern",
3
- "version": "1.1.3",
4
- "description": "Fast library for matching MQTT patterns with named wildcards",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "node test"
8
- },
9
- "repository": {
10
- "type": "git",
11
- "url": "git+https://github.com/RangerMauve/mqtt-pattern.git"
12
- },
13
- "keywords": [
14
- "mqtt",
15
- "pattern",
16
- "match",
17
- "topic"
18
- ],
19
- "author": "rangermauve",
20
- "license": "MIT",
21
- "bugs": {
22
- "url": "https://github.com/RangerMauve/mqtt-pattern/issues"
23
- },
24
- "homepage": "https://github.com/RangerMauve/mqtt-pattern#readme",
25
- "devDependencies": {
26
- "eslint": "^3.19.0",
27
- "eslint-plugin-node": "^4.2.2",
28
- "tape": "^4.6.3"
29
- },
30
- "dependencies": {
31
- "mqtt-match": "^1.0.2"
32
- }
33
- }
1
+ {
2
+ "name": "mqtt-pattern",
3
+ "version": "2.0.0",
4
+ "description": "Fast library for matching MQTT patterns with named wildcards",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "scripts": {
8
+ "test": "node test"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/RangerMauve/mqtt-pattern.git"
13
+ },
14
+ "keywords": [
15
+ "mqtt",
16
+ "pattern",
17
+ "match",
18
+ "topic"
19
+ ],
20
+ "author": "rangermauve",
21
+ "license": "MIT",
22
+ "bugs": {
23
+ "url": "https://github.com/RangerMauve/mqtt-pattern/issues"
24
+ },
25
+ "homepage": "https://github.com/RangerMauve/mqtt-pattern#readme",
26
+ "devDependencies": {
27
+ "eslint": "^3.19.0",
28
+ "eslint-plugin-node": "^4.2.2",
29
+ "tape": "^4.6.3",
30
+ "ts-toolbelt": "^9.6.0"
31
+ },
32
+ "dependencies": {
33
+ "mqtt-match": "^1.0.2"
34
+ }
35
+ }
@@ -0,0 +1,90 @@
1
+ /** This code is copied from the typings in @erichardson-lee/mqtt-router */
2
+
3
+ //#region Topic Parameter name & type inference
4
+
5
+ /** Check if a string is a parameter in a type */
6
+ type IsParameter<Parameter> = Parameter extends `+${infer ParamName}`
7
+ ? ParamName
8
+ : never | Parameter extends `#${string}`
9
+ ? Parameter
10
+ : never;
11
+
12
+ /** Type To split by / and extract parameters */
13
+ type FilteredTopicSplit<Topic> = Topic extends `${infer PartA}/${infer PartB}`
14
+ ? IsParameter<PartA> | FilteredTopicSplit<PartB>
15
+ : IsParameter<Topic>;
16
+
17
+ /** Type to get Parameter Value */
18
+ type ParameterValue<Parameter> = Parameter extends `#${string}`
19
+ ? string[]
20
+ : string;
21
+
22
+ /** Type to remove # prefix from parameter */
23
+ type StripParameterHash<Parameter> = Parameter extends `#${infer Name}`
24
+ ? Name
25
+ : Parameter;
26
+
27
+ /** Parameter Type */
28
+ export type MqttParameters<Topic> = {
29
+ [key in FilteredTopicSplit<Topic> as StripParameterHash<key>]: ParameterValue<key>;
30
+ };
31
+ //#endregion
32
+
33
+ //#region Topic Cleaning
34
+
35
+ /** Clean a segment of a Topic */
36
+ type CleanTopicSegment<Segment> = Segment extends `+${string}`
37
+ ? "+"
38
+ : Segment extends `#${string}`
39
+ ? "#"
40
+ : Segment;
41
+
42
+ /** Clean a Topic (replace #value or +value with # or + respectively) */
43
+ export type CleanTopic<Topic> = Topic extends `${infer PartA}/${infer PartB}`
44
+ ? `${CleanTopicSegment<PartA>}/${CleanTopic<PartB>}`
45
+ : CleanTopicSegment<Topic>;
46
+
47
+ //#endregion
48
+
49
+ //#region Topic Filling
50
+ type JoinPath<Parts extends string[]> = Parts extends [infer v, ...infer rest]
51
+ ? rest extends []
52
+ ? v
53
+ : v extends string
54
+ ? rest extends string[]
55
+ ? `${v}/${JoinPath<rest>}`
56
+ : ""
57
+ : ""
58
+ : "";
59
+
60
+ type FillParam<
61
+ Section extends string,
62
+ Parameters extends MqttParameters<FullPattern>,
63
+ FullPattern extends string = Section
64
+ > = Section extends `+${infer PName}`
65
+ ? PName extends keyof Parameters
66
+ ? Parameters[PName] extends string
67
+ ? Parameters[PName]
68
+ : unknown
69
+ : unknown
70
+ : Section extends `#${infer PName}`
71
+ ? PName extends keyof Parameters
72
+ ? Parameters[PName] extends string[]
73
+ ? JoinPath<Parameters[PName]>
74
+ : unknown
75
+ : unknown
76
+ : Section;
77
+
78
+ export type FillTopic<
79
+ Pattern extends string,
80
+ Parameters extends MqttParameters<FullPattern>,
81
+ FullPattern extends string = Pattern
82
+ > = Pattern extends `${infer Left}/${infer Right}`
83
+ ? `${FillParam<Left, Parameters, FullPattern>}/${FillTopic<
84
+ Right,
85
+ Parameters,
86
+ FullPattern
87
+ >}`
88
+ : FillParam<Pattern, Parameters, FullPattern>;
89
+
90
+ //#endregion
package/test.js CHANGED
@@ -1,157 +1,167 @@
1
- "use strict";
2
-
3
- var test = require("tape");
4
-
5
- var MQTTPattern = require("./");
6
-
7
- test("matches() supports patterns with no wildcards", function (t) {
8
- t.plan(1);
9
- t.ok(MQTTPattern.matches("foo/bar/baz", "foo/bar/baz"), "Matched topic");
10
- });
11
-
12
- test("matches() doesn't match different topics", function (t) {
13
- t.plan(1);
14
- t.notOk(MQTTPattern.matches("foo/bar/baz", "baz/bar/foo"), "Didn't match topic");
15
- });
16
-
17
- test("matches() supports patterns with # at the beginning", function (t) {
18
- t.plan(1);
19
- t.ok(MQTTPattern.matches("#", "foo/bar/baz"), "Matched topic");
20
- });
21
-
22
- test("matches() supports patterns with # at the end", function (t) {
23
- t.plan(1);
24
- t.ok(MQTTPattern.matches("foo/#", "foo/bar/baz"), "Matched topic");
25
- });
26
-
27
- test("matches() supports patterns with # at the end and topic has no children", function (t) {
28
- t.plan(1);
29
- t.ok(MQTTPattern.matches("foo/bar/#", "foo/bar"), "Matched childless topic");
30
- });
31
-
32
- test("matches() doesn't support # wildcards with more after them", function (t) {
33
- t.plan(1);
34
- t.notOk(MQTTPattern.matches("#/bar/baz", "foo/bar/baz"), "Didn't match topic");
35
- });
36
-
37
- test("matches() supports patterns with + at the beginning", function (t) {
38
- t.plan(1);
39
- t.ok(MQTTPattern.matches("+/bar/baz", "foo/bar/baz"), "Matched topic");
40
- });
41
-
42
- test("matches() supports patterns with + at the end", function (t) {
43
- t.plan(1);
44
- t.ok(MQTTPattern.matches("foo/bar/+", "foo/bar/baz"), "Matched topic");
45
- });
46
-
47
- test("matches() supports patterns with + in the middle", function (t) {
48
- t.plan(1);
49
- t.ok(MQTTPattern.matches("foo/+/baz", "foo/bar/baz"), "Matched topic");
50
- });
51
-
52
- test("matches() supports patterns multiple wildcards", function (t) {
53
- t.plan(1);
54
- t.ok(MQTTPattern.matches("foo/+/#", "foo/bar/baz"), "Matched topic");
55
- });
56
-
57
- test("matches() supports named wildcards", function (t) {
58
- t.plan(1);
59
- t.ok(MQTTPattern.matches("foo/+something/#else", "foo/bar/baz"), "Matched topic");
60
- });
61
-
62
- test("matches() supports leading slashes", function (t){
63
- t.plan(2);
64
- t.ok(MQTTPattern.matches("/foo/bar", "/foo/bar"), "Matched topic");
65
- t.notok(MQTTPattern.matches("/foo/bar", "/bar/foo"), "Didn't match invalid topic");
66
- });
67
-
68
- test("extract() returns empty object of there's nothing to extract", function (t) {
69
- t.plan(1);
70
- t.deepEqual(MQTTPattern.extract("foo/bar/baz", "foo/bar/baz"), {}, "Extracted empty object");
71
- });
72
-
73
- test("extract() returns empty object if wildcards don't have label", function (t) {
74
- t.plan(1);
75
- t.deepEqual(MQTTPattern.extract("foo/+/#", "foo/bar/baz"), {}, "Extracted empty object");
76
- });
77
-
78
- test("extract() returns object with an array for # wildcard", function (t) {
79
- t.plan(1);
80
- t.deepEqual(MQTTPattern.extract("foo/#something", "foo/bar/baz"), {
81
- something: ["bar", "baz"]
82
- }, "Extracted param");
83
- });
84
-
85
- test("extract() returns object with a string for + wildcard", function (t) {
86
- t.plan(1);
87
- t.deepEqual(MQTTPattern.extract("foo/+hello/+world", "foo/bar/baz"), {
88
- hello: "bar",
89
- world: "baz"
90
- }, "Extracted params");
91
- });
92
-
93
- test("extract() parses params from all wildcards", function (t) {
94
- t.plan(1);
95
- t.deepEqual(MQTTPattern.extract("+hello/+world/#wow", "foo/bar/baz/fizz"), {
96
- hello: "foo",
97
- world: "bar",
98
- wow: ["baz", "fizz"]
99
- }, "Extracted params");
100
- });
101
-
102
- test("exec() returns null if it doesn't match", function (t) {
103
- t.plan(1);
104
- t.equal(MQTTPattern.exec("hello/world", "foo/bar/baz"), null, "Got null");
105
- });
106
-
107
- test("exec() returns params if they can be parsed", function(t){
108
- t.plan(1);
109
- t.deepEqual(MQTTPattern.exec("foo/+hello/#world", "foo/bar/baz"), {
110
- hello: "bar",
111
- world: ["baz"]
112
- }, "Extracted params");
113
- });
114
-
115
- test("fill() fills in pattern with both types of wildcards", function(t){
116
- t.plan(1);
117
- t.deepEqual(MQTTPattern.fill("foo/+hello/#world", {
118
- hello: "Hello",
119
- world: ["the", "world", "wow"],
120
- }), "foo/Hello/the/world/wow", "Filled in params");
121
- });
122
-
123
- test("fill() fills missing + params with undefined", function(t){
124
- t.plan(1);
125
- t.deepEqual(
126
- MQTTPattern.fill("foo/+hello", {}),
127
- "foo/undefined",
128
- "Filled in params"
129
- );
130
- });
131
-
132
- test("fill() ignores empty # params", function(t){
133
- t.plan(1);
134
- t.deepEqual(
135
- MQTTPattern.fill("foo/#hello", {}),
136
- "foo",
137
- "Filled in params"
138
- );
139
- });
140
-
141
- test("fill() ignores non-named # params", function (t) {
142
- t.plan(1);
143
- t.deepEqual(
144
- MQTTPattern.fill("foo/#", {}),
145
- "foo",
146
- "Filled in params"
147
- );
148
- });
149
-
150
- test("fill() uses `undefined` for non-named + params", function(t){
151
- t.plan(1);
152
- t.deepEqual(
153
- MQTTPattern.fill("foo/+", {}),
154
- "foo/undefined",
155
- "Filled in params"
156
- );
157
- });
1
+ "use strict";
2
+
3
+ var test = require("tape");
4
+
5
+ var MQTTPattern = require("./");
6
+
7
+ test("matches() supports patterns with no wildcards", function (t) {
8
+ t.plan(1);
9
+ t.ok(MQTTPattern.matches("foo/bar/baz", "foo/bar/baz"), "Matched topic");
10
+ });
11
+
12
+ test("matches() doesn't match different topics", function (t) {
13
+ t.plan(1);
14
+ t.notOk(MQTTPattern.matches("foo/bar/baz", "baz/bar/foo"), "Didn't match topic");
15
+ });
16
+
17
+ test("matches() supports patterns with # at the beginning", function (t) {
18
+ t.plan(1);
19
+ t.ok(MQTTPattern.matches("#", "foo/bar/baz"), "Matched topic");
20
+ });
21
+
22
+ test("matches() supports patterns with # at the end", function (t) {
23
+ t.plan(1);
24
+ t.ok(MQTTPattern.matches("foo/#", "foo/bar/baz"), "Matched topic");
25
+ });
26
+
27
+ test("matches() supports patterns with # at the end and topic has no children", function (t) {
28
+ t.plan(1);
29
+ t.ok(MQTTPattern.matches("foo/bar/#", "foo/bar"), "Matched childless topic");
30
+ });
31
+
32
+ test("matches() doesn't support # wildcards with more after them", function (t) {
33
+ t.plan(1);
34
+ t.notOk(MQTTPattern.matches("#/bar/baz", "foo/bar/baz"), "Didn't match topic");
35
+ });
36
+
37
+ test("matches() supports patterns with + at the beginning", function (t) {
38
+ t.plan(1);
39
+ t.ok(MQTTPattern.matches("+/bar/baz", "foo/bar/baz"), "Matched topic");
40
+ });
41
+
42
+ test("matches() supports patterns with + at the end", function (t) {
43
+ t.plan(1);
44
+ t.ok(MQTTPattern.matches("foo/bar/+", "foo/bar/baz"), "Matched topic");
45
+ });
46
+
47
+ test("matches() supports patterns with + in the middle", function (t) {
48
+ t.plan(1);
49
+ t.ok(MQTTPattern.matches("foo/+/baz", "foo/bar/baz"), "Matched topic");
50
+ });
51
+
52
+ test("matches() supports patterns multiple wildcards", function (t) {
53
+ t.plan(1);
54
+ t.ok(MQTTPattern.matches("foo/+/#", "foo/bar/baz"), "Matched topic");
55
+ });
56
+
57
+ test("matches() supports named wildcards", function (t) {
58
+ t.plan(1);
59
+ t.ok(MQTTPattern.matches("foo/+something/#else", "foo/bar/baz"), "Matched topic");
60
+ });
61
+
62
+ test("matches() supports leading slashes", function (t){
63
+ t.plan(2);
64
+ t.ok(MQTTPattern.matches("/foo/bar", "/foo/bar"), "Matched topic");
65
+ t.notok(MQTTPattern.matches("/foo/bar", "/bar/foo"), "Didn't match invalid topic");
66
+ });
67
+
68
+ test("extract() returns empty object of there's nothing to extract", function (t) {
69
+ t.plan(1);
70
+ t.deepEqual(MQTTPattern.extract("foo/bar/baz", "foo/bar/baz"), {}, "Extracted empty object");
71
+ });
72
+
73
+ test("extract() returns empty object if wildcards don't have label", function (t) {
74
+ t.plan(1);
75
+ t.deepEqual(MQTTPattern.extract("foo/+/#", "foo/bar/baz"), {}, "Extracted empty object");
76
+ });
77
+
78
+ test("extract() returns object with an array for # wildcard", function (t) {
79
+ t.plan(1);
80
+ t.deepEqual(MQTTPattern.extract("foo/#something", "foo/bar/baz"), {
81
+ something: ["bar", "baz"]
82
+ }, "Extracted param");
83
+ });
84
+
85
+ test("extract() returns object with a string for + wildcard", function (t) {
86
+ t.plan(1);
87
+ t.deepEqual(MQTTPattern.extract("foo/+hello/+world", "foo/bar/baz"), {
88
+ hello: "bar",
89
+ world: "baz"
90
+ }, "Extracted params");
91
+ });
92
+
93
+ test("extract() parses params from all wildcards", function (t) {
94
+ t.plan(1);
95
+ t.deepEqual(MQTTPattern.extract("+hello/+world/#wow", "foo/bar/baz/fizz"), {
96
+ hello: "foo",
97
+ world: "bar",
98
+ wow: ["baz", "fizz"]
99
+ }, "Extracted params");
100
+ });
101
+
102
+ test("exec() returns null if it doesn't match", function (t) {
103
+ t.plan(1);
104
+ t.equal(MQTTPattern.exec("hello/world", "foo/bar/baz"), null, "Got null");
105
+ });
106
+
107
+ test("exec() returns params if they can be parsed", function(t){
108
+ t.plan(1);
109
+ t.deepEqual(MQTTPattern.exec("foo/+hello/#world", "foo/bar/baz"), {
110
+ hello: "bar",
111
+ world: ["baz"]
112
+ }, "Extracted params");
113
+ });
114
+
115
+ test("fill() fills in pattern with both types of wildcards", function(t){
116
+ t.plan(1);
117
+ t.deepEqual(MQTTPattern.fill("foo/+hello/#world", {
118
+ hello: "Hello",
119
+ world: ["the", "world", "wow"],
120
+ }), "foo/Hello/the/world/wow", "Filled in params");
121
+ });
122
+
123
+ test("fill() fills missing + params with undefined", function(t){
124
+ t.plan(1);
125
+ t.deepEqual(
126
+ MQTTPattern.fill("foo/+hello", {}),
127
+ "foo/undefined",
128
+ "Filled in params"
129
+ );
130
+ });
131
+
132
+ test("fill() ignores empty # params", function(t){
133
+ t.plan(1);
134
+ t.deepEqual(
135
+ MQTTPattern.fill("foo/#hello", {}),
136
+ "foo",
137
+ "Filled in params"
138
+ );
139
+ });
140
+
141
+ test("fill() ignores non-named # params", function (t) {
142
+ t.plan(1);
143
+ t.deepEqual(
144
+ MQTTPattern.fill("foo/#", {}),
145
+ "foo",
146
+ "Filled in params"
147
+ );
148
+ });
149
+
150
+ test("fill() uses `undefined` for non-named + params", function(t){
151
+ t.plan(1);
152
+ t.deepEqual(
153
+ MQTTPattern.fill("foo/+", {}),
154
+ "foo/undefined",
155
+ "Filled in params"
156
+ );
157
+ });
158
+
159
+ test("clean() removes parameter names", function(t){
160
+ t.plan(1);
161
+ t.equal(MQTTPattern.clean("hello/+param1/world/#param2"), "hello/+/world/#", "Got hello/+/world/#");
162
+ });
163
+
164
+ test("clean() works when there aren't any parameter names", function(t){
165
+ t.plan(1);
166
+ t.equal(MQTTPattern.clean("hello/+/world/#"), "hello/+/world/#", "Got hello/+/world/#");
167
+ });
package/test.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { clean, exec, extract, fill, matches } from "./index";
2
+ import { Test } from "ts-toolbelt";
3
+ import test from "node:test";
4
+ // Type Testing
5
+
6
+ const { check, checks } = Test;
7
+
8
+ // Exec
9
+ const execv = exec("foo/bar/+baz", "foo/bar/test");
10
+ checks([
11
+ check<typeof execv, { baz: string } | null, Test.Pass>(),
12
+ check<typeof execv, { baz: string[] } | null, Test.Fail>(),
13
+ ]);
14
+
15
+ // Matches
16
+ const matchv = matches("foo/bar/+baz", "foo/bar/test");
17
+ check<typeof matchv, boolean, Test.Pass>();
18
+
19
+ // Fill
20
+ const fillv = fill("foo/bar/+baz/#test", {
21
+ baz: "test",
22
+ test: ["v1", "v2"],
23
+ });
24
+ check<typeof fillv, "foo/bar/test/v1/v2", Test.Pass>();
25
+
26
+ // Extract
27
+ const extractv = extract("foo/bar/+baz", "foo/bar/test");
28
+ checks([
29
+ check<typeof extractv, { baz: string }, Test.Pass>(),
30
+ check<typeof extractv, { baz: string[] }, Test.Fail>(),
31
+ ]);
32
+
33
+ // Clean
34
+ const cleanv = clean("foo/bar/+baz");
35
+ check<typeof cleanv, "foo/bar/+", Test.Pass>();