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 all commits
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
10 changes: 10 additions & 0 deletions .busted
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",
},
}
15 changes: 15 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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
indent_size = 4
186 changes: 186 additions & 0 deletions mqtt/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,192 @@ function mqtt.run_sync(cl)
end
end


--- Validates a topic with wildcards.
-- @tparam string t wildcard topic to validate
-- @return topic, or false+error
xHasKx marked this conversation as resolved.
Show resolved Hide resolved
-- @usage
-- local t = "invalid/#/subscribe/#/topic"
-- local topic = assert(mqtt.validate_subscribe_topic(t))
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, "bad subscribe-topic; expected topic to be a string, got: "..type(t)
end
if #t < 1 then
return false, "bad subscribe-topic; expected minimum topic length of 1"
end
do
local _, count = t:gsub("#", "")
if count > 1 then
return false, "bad subscribe-topic; wildcard '#' may only appear once, got: '"..t.."'"
end
if count == 1 then
if t ~= "#" and not t:find("/#$") then
return false, "bad subscribe-topic; wildcard '#' must be the last character, and " ..
"be prefixed with '/' (unless the topic is '#'), got: '"..t.."'"
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, "bad subscribe-topic; wildcard '+' must be enclosed between '/' " ..
"(except at start/end), got: '"..t.."'"
end
i = i + 1
end
end
end
return t
end

--- Validates a topic without wildcards.
-- @tparam string t topic to validate
-- @return topic, or false+error
-- @usage
-- local t = "invalid/#/publish/+/topic"
-- local topic = assert(mqtt.validate_publish_topic(t))
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, "bad publish-topic; expected topic to be a string, got: "..type(t)
end
if #t < 1 then
return false, "bad publish-topic; expected minimum topic length of 1"
end
if t:find("+", nil, true) or t:find("#", nil, true) then
return false, "bad publish-topic; wildcards '#', and '+' are not allowed when publishing, got: '"..t.."'"
end
return t
end

do
local MATCH_ALL = "(.+)" -- matches anything at least 1 character long
local MATCH_HASH = "(.-)" -- match anything, can be empty
local MATCH_PLUS = "([^/]-)" -- match anything between '/', can be empty

--- 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
-- @tparam string t the wildcard topic
-- @return Lua-pattern (string) or throws error on invalid input
-- @usage
-- local patt = compile_topic_pattern("homes/+/+/#")
--
-- local incoming_topic = "homes/myhome/living/mainlights/brightness"
-- local homeid, roomid, varargs = incoming_topic:match(patt)
function mqtt.compile_topic_pattern(t)
t = assert(mqtt.validate_subscribe_topic(t))
if t == "#" then
t = MATCH_ALL
else
t = t:gsub("#", MATCH_HASH)
t = t:gsub("%+", MATCH_PLUS)
end
return "^"..t.."$"
end
Comment on lines +161 to +170
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated to return the number of fields and presence of varargs;

Suggested change
function mqtt.compile_topic_pattern(t)
t = assert(mqtt.validate_subscribe_topic(t))
if t == "#" then
t = MATCH_ALL
else
t = t:gsub("#", MATCH_HASH)
t = t:gsub("%+", MATCH_PLUS)
end
return "^"..t.."$"
end
function mqtt.compile_topic_pattern(t)
t = assert(mqtt.validate_subscribe_topic(t))
if t == "#" then
return MATCH_ALL, 0, true
end
local fields, vararg
t, vararg = t:gsub("#", MATCH_HASH)
t, fields = t:gsub("%+", MATCH_PLUS)
return "^"..t.."$", fields, vararg ~= 0
end

end

do
local HAS_VARARG_PATTERN = "%(%.[%-%+]%)%$$" -- matches patterns that have a vararg matcher

--- 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.
-- @tparam string topic incoming topic string (required)
-- @tparam table opts options table(required)
-- @return fields (table) + varargs (table or nil), or false+err if the match failed,
-- or throws an error on invalid input.
-- @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
error("expected topic to be a string, got: "..type(topic))
end
if type(opts) ~= "table" then
error("expected options to be a table, got: "..type(opts))
end
local pattern = opts.pattern
if not pattern then
local ptopic = assert(opts.topic, "either 'opts.topic' or 'opts.pattern' must set")
pattern = assert(mqtt.compile_topic_pattern(ptopic))
-- 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
error("expected 'opts.keys' to be a table (array), got: "..type(keys))
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(HAS_VARARG_PATTERN) 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
end


-- export module table
return mqtt

Expand Down
Loading