├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── index.d.ts ├── index.js ├── package.json ├── parameters.d.ts ├── test.js └── test.ts /.eslintrc: -------------------------------------------------------------------------------- 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 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 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"]} 85 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import type { MqttParameters, FillTopic, CleanTopic } from "./parameters"; 2 | import type { F } from "ts-toolbelt"; 3 | 4 | export type { MqttParameters } from './parameters' 5 | 6 | /** 7 | * Extract parameters from a topic based on the pattern provided 8 | * @param pattern Pattern to match against 9 | * @param topic Topic to extract parameters from 10 | */ 11 | export declare function exec( 12 | pattern: Pattern, 13 | topic: string 14 | ): MqttParameters | null; 15 | 16 | /** 17 | * Tests if a topic is valid for a certain pattern. 18 | * @param pattern Pattern to match against 19 | * @param topic Topic to test 20 | */ 21 | export declare function matches(pattern: string, topic: string): boolean; 22 | 23 | /** 24 | * Take a pattern and fill the parameters with the appropriate values 25 | * @param pattern Pattern to fill 26 | * @param params Parameters to insert into the pattern 27 | */ 28 | export declare function fill< 29 | Pattern extends string, 30 | Params extends MqttParameters 31 | >( 32 | pattern: F.Narrow, 33 | params: F.Narrow 34 | ): FillTopic; 35 | 36 | /** 37 | * Extract parameter values from a topic using a pattern. 38 | * 39 | * This function doesn't check if the pattern matches before attempting 40 | * extraction, @see exec if you want to extract values, with assurances that the 41 | * topic actually matches the pattern.. 42 | * 43 | * @deprecated use exec for safety on topic matching the pattern 44 | * @param pattern Pattern to extract using 45 | * @param topic Topic to extract values from 46 | */ 47 | export declare function extract( 48 | pattern: Pattern, 49 | topic: string 50 | ): MqttParameters; 51 | 52 | /** 53 | * Clean a pattern (remove any property names from wildcard segments) 54 | * 55 | * @param pattern Pattern to clean 56 | * @returns cleaned pattern safe for usage in subscriptions etc. 57 | */ 58 | export declare function clean( 59 | pattern: Pattern 60 | ): CleanTopic; 61 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 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 | } 122 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mqtt-pattern", 3 | "version": "2.1.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 | }, 31 | "dependencies": { 32 | "mqtt-match": "^1.0.2", 33 | "ts-toolbelt": "^9.6.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /parameters.d.ts: -------------------------------------------------------------------------------- 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 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 extends `${infer PartA}/${infer PartB}` 14 | ? IsParameter | FilteredTopicSplit 15 | : IsParameter; 16 | 17 | /** Type to get Parameter Value */ 18 | type ParameterValue = Parameter extends `#${string}` 19 | ? string[] 20 | : string; 21 | 22 | /** Type to remove # prefix from parameter */ 23 | type StripParameterHash = Parameter extends `#${infer Name}` 24 | ? Name 25 | : Parameter; 26 | 27 | /** Parameter Type */ 28 | export type MqttParameters = { 29 | [key in FilteredTopicSplit as StripParameterHash]: ParameterValue; 30 | }; 31 | //#endregion 32 | 33 | //#region Topic Cleaning 34 | 35 | /** Clean a segment of a Topic */ 36 | type CleanTopicSegment = 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 extends `${infer PartA}/${infer PartB}` 44 | ? `${CleanTopicSegment}/${CleanTopic}` 45 | : CleanTopicSegment; 46 | 47 | //#endregion 48 | 49 | //#region Topic Filling 50 | type JoinPath = Parts extends [infer v, ...infer rest] 51 | ? rest extends [] 52 | ? v 53 | : v extends string 54 | ? rest extends string[] 55 | ? `${v}/${JoinPath}` 56 | : "" 57 | : "" 58 | : ""; 59 | 60 | type FillParam< 61 | Section extends string, 62 | Parameters extends MqttParameters, 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 74 | : unknown 75 | : unknown 76 | : Section; 77 | 78 | export type FillTopic< 79 | Pattern extends string, 80 | Parameters extends MqttParameters, 81 | FullPattern extends string = Pattern 82 | > = Pattern extends `${infer Left}/${infer Right}` 83 | ? `${FillParam}/${FillTopic< 84 | Right, 85 | Parameters, 86 | FullPattern 87 | >}` 88 | : FillParam; 89 | 90 | //#endregion 91 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 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 | }); 168 | -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | import { clean, exec, extract, fill, matches, MqttParameters } 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(), 12 | check | null, Test.Pass>(), 13 | check(), 14 | ]); 15 | 16 | // Matches 17 | const matchv = matches("foo/bar/+baz", "foo/bar/test"); 18 | check(); 19 | 20 | // Fill 21 | const fillv = fill("foo/bar/+baz/#test", { 22 | baz: "test", 23 | test: ["v1", "v2"], 24 | }); 25 | check(); 26 | 27 | // Extract 28 | const extractv = extract("foo/bar/+baz", "foo/bar/test"); 29 | checks([ 30 | check(), 31 | check(), 32 | ]); 33 | 34 | // Clean 35 | const cleanv = clean("foo/bar/+baz"); 36 | check(); 37 | --------------------------------------------------------------------------------