Skip to content
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

generic topic validation and parsing #36

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .editorconfig
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
Tieske marked this conversation as resolved.
Show resolved Hide resolved

[Makefile]
indent_style = tab
177 changes: 177 additions & 0 deletions mqtt/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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
Copy link
Owner

@xHasKx xHasKx Aug 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please follow such errors checking behavior:

* passing invalid arguments (like number instead of string) to function in this library will raise exception

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link
Owner

@xHasKx xHasKx Aug 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please follow such errors checking behavior:

* passing invalid arguments (like number instead of string) to function in this library will raise exception

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link
Owner

@xHasKx xHasKx Aug 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please follow such errors checking behavior:

* passing invalid arguments (like number instead of string) to function in this library will raise exception

example:
assert(value_type == "string", "expecting uri to be a string")

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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please move that pattern string (and its related strings from compile_topic_pattern function) into the local variables in the init.lua file to look like constants as they are related very implicitly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. moved them into narrow scoped module locals.

-- 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

Expand Down
Loading