-
Notifications
You must be signed in to change notification settings - Fork 45
generic topic validation and parsing #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| return { | ||
| default = { | ||
| ROOT = { "tests/spec" }, | ||
| pattern = "%.lua", | ||
| lpath = "./?.lua;./?/?.lua;./?/init.lua", | ||
| verbose = true, | ||
| coverage = false, | ||
| output = "gtest", | ||
| }, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| root = true | ||
|
|
||
| [*] | ||
| end_of_line = lf | ||
| insert_final_newline = true | ||
| trim_trailing_whitespace = true | ||
| charset = utf-8 | ||
|
|
||
| [*.lua] | ||
| indent_style = tab | ||
| indent_size = 4 | ||
|
|
||
| [Makefile] | ||
| indent_style = tab | ||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -81,6 +81,183 @@ function mqtt.run_sync(cl) | |||
| end | ||||
| end | ||||
|
|
||||
|
|
||||
| --- Validates a topic with wildcards. | ||||
| -- @param t (string) wildcard topic to validate | ||||
Tieske marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||
| -- @return topic, or false+error | ||||
xHasKx marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
| function mqtt.validate_subscribe_topic(t) | ||||
| if type(t) ~= "string" then | ||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please follow such errors checking behavior: Line 10 in e3fa62c
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since it is validation, the output of a validation function should be usable as input for an assertion. Hence this function does not throw errors itself, and returns the input value if valid. This is exactly what I implemented for the other functions, as they should throw errors, as documented. |
||||
| return false, "not a string" | ||||
| end | ||||
| if #t < 1 then | ||||
| return false, "minimum topic length is 1" | ||||
| end | ||||
| do | ||||
| local _, count = t:gsub("#", "") | ||||
| if count > 1 then | ||||
| return false, "wildcard '#' may only appear once" | ||||
| end | ||||
| if count == 1 then | ||||
| if t ~= "#" and not t:find("/#$") then | ||||
| return false, "wildcard '#' must be the last character, and be prefixed with '/' (unless the topic is '#')" | ||||
| end | ||||
| end | ||||
| end | ||||
| do | ||||
| local t1 = "/"..t.."/" | ||||
| local i = 1 | ||||
| while i do | ||||
| i = t1:find("+", i) | ||||
| if i then | ||||
| if t1:sub(i-1, i+1) ~= "/+/" then | ||||
| return false, "wildcard '+' must be enclosed between '/' (except at start/end)" | ||||
| end | ||||
| i = i + 1 | ||||
| end | ||||
| end | ||||
| end | ||||
| return t | ||||
| end | ||||
|
|
||||
| --- Validates a topic without wildcards. | ||||
| -- @param t (string) topic to validate | ||||
| -- @return topic, or false+error | ||||
| function mqtt.validate_publish_topic(t) | ||||
| if type(t) ~= "string" then | ||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please follow such errors checking behavior: Line 10 in e3fa62c
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same; result is input for assertions |
||||
| return false, "not a string" | ||||
| end | ||||
| if #t < 1 then | ||||
| return false, "minimum topic length is 1" | ||||
| end | ||||
| if t:find("+", nil, true) or t:find("#", nil, true) then | ||||
| return false, "wildcards '#', and '+' are not allowed when publishing" | ||||
| end | ||||
| return t | ||||
| end | ||||
|
|
||||
| --- Returns a Lua pattern from topic. | ||||
| -- Takes a wildcarded-topic and returns a Lua pattern that can be used | ||||
| -- to validate if a received topic matches the wildcard-topic | ||||
| -- @param t (string) the wildcard topic | ||||
| -- @return Lua-pattern (string) or false+err | ||||
| -- @usage | ||||
| -- local patt = compile_topic_pattern("homes/+/+/#") | ||||
| -- | ||||
| -- local topic = "homes/myhome/living/mainlights/brightness" | ||||
| -- local homeid, roomid, varargs = topic:match(patt) | ||||
| function mqtt.compile_topic_pattern(t) | ||||
| local ok, err = mqtt.validate_subscribe_topic(t) | ||||
| if not ok then | ||||
| return ok, err | ||||
| end | ||||
| if t == "#" then | ||||
| t = "(.+)" -- matches anything at least 1 character long | ||||
| else | ||||
| t = t:gsub("#","(.-)") -- match anything, can be empty | ||||
| t = t:gsub("%+","([^/]-)") -- match anything between '/', can be empty | ||||
| end | ||||
| return "^"..t.."$" | ||||
| end | ||||
|
|
||||
| --- Parses wildcards in a topic into a table. | ||||
| -- Options include: | ||||
| -- | ||||
| -- - `opts.topic`: the wild-carded topic to match against (optional if `opts.pattern` is given) | ||||
| -- | ||||
| -- - `opts.pattern`: the compiled pattern for the wild-carded topic (optional if `opts.topic` | ||||
| -- is given). If not given then topic will be compiled and the result will be | ||||
| -- stored in this field for future use (cache). | ||||
| -- | ||||
| -- - `opts.keys`: (optional) array of field names. The order must be the same as the | ||||
| -- order of the wildcards in `topic` | ||||
| -- | ||||
| -- Returned tables: | ||||
| -- | ||||
| -- - `fields` table: the array part will have the values of the wildcards, in | ||||
| -- the order they appeared. The hash part, will have the field names provided | ||||
| -- in `opts.keys`, with the values of the corresponding wildcard. If a `#` | ||||
| -- wildcard was used, that one will be the last in the table. | ||||
| -- | ||||
| -- - `varargs` table: will only be returned if the wildcard topic contained the | ||||
| -- `#` wildcard. The returned table is an array, with all segments that were | ||||
| -- matched by the `#` wildcard. | ||||
| -- @param topic (string) incoming topic string (required) | ||||
| -- @param opts (table) with options (required) | ||||
| -- @return fields (table) + varargs (table or nil), or false+err on error. | ||||
| -- @usage | ||||
| -- local opts = { | ||||
| -- topic = "homes/+/+/#", | ||||
| -- keys = { "homeid", "roomid", "varargs"}, | ||||
| -- } | ||||
| -- local fields, varargs = topic_match("homes/myhome/living/mainlights/brightness", opts) | ||||
| -- | ||||
| -- print(fields[1], fields.homeid) -- "myhome myhome" | ||||
| -- print(fields[2], fields.roomid) -- "living living" | ||||
| -- print(fields[3], fields.varargs) -- "mainlights/brightness mainlights/brightness" | ||||
| -- | ||||
| -- print(varargs[1]) -- "mainlights" | ||||
| -- print(varargs[2]) -- "brightness" | ||||
| function mqtt.topic_match(topic, opts) | ||||
| if type(topic) ~= "string" then | ||||
| return false, "expected topic to be a string" | ||||
| end | ||||
| if type(opts) ~= "table" then | ||||
| return false, "expected optionss to be a table" | ||||
| end | ||||
| local pattern = opts.pattern | ||||
| if not pattern then | ||||
| local ptopic = opts.topic | ||||
| if not ptopic then | ||||
|
||||
| return false, "either 'opts.topic' or 'opts.pattern' must set" | ||||
| end | ||||
| local err | ||||
| pattern, err = mqtt.compile_topic_pattern(ptopic) | ||||
| if not pattern then | ||||
| return false, "failed to compile 'opts.topic' into pattern: "..tostring(err) | ||||
| end | ||||
| -- store/cache compiled pattern for next time | ||||
| opts.pattern = pattern | ||||
| end | ||||
| local values = { topic:match(pattern) } | ||||
| if values[1] == nil then | ||||
| return false, "topic does not match wildcard pattern" | ||||
| end | ||||
| local keys = opts.keys | ||||
| if keys ~= nil then | ||||
| if type(keys) ~= "table" then | ||||
| return false, "expected 'opts.keys' to be a table (array)" | ||||
| end | ||||
| -- we have a table with keys, copy values to fields | ||||
| for i, value in ipairs(values) do | ||||
| local key = keys[i] | ||||
| if key ~= nil then | ||||
| values[key] = value | ||||
| end | ||||
| end | ||||
| end | ||||
| if not pattern:find("%(%.[%-%+]%)%$$") then -- pattern for "#" as last char | ||||
|
||||
| -- we're done | ||||
| return values | ||||
| end | ||||
| -- we have a '#' wildcard | ||||
| local vararg = values[#values] | ||||
| local varargs = {} | ||||
| local i = 0 | ||||
| local ni = 0 | ||||
| while ni do | ||||
| ni = vararg:find("/", i, true) | ||||
| if ni then | ||||
| varargs[#varargs + 1] = vararg:sub(i, ni-1) | ||||
| i = ni + 1 | ||||
| else | ||||
| varargs[#varargs + 1] = vararg:sub(i, -1) | ||||
| end | ||||
| end | ||||
|
|
||||
| return values, varargs | ||||
| end | ||||
|
|
||||
|
|
||||
| -- export module table | ||||
| return mqtt | ||||
|
|
||||
|
|
||||
Uh oh!
There was an error while loading. Please reload this page.