-
Notifications
You must be signed in to change notification settings - Fork 42
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
base: master
Are you sure you want to change the base?
Conversation
not sure where this should go, it's now in possibly every subscription could be parsed by the client, and the parsed results passed along in the event handler. thoughts? |
5ba0510
to
525351a
Compare
init.lua is ok as you've added a whole-module functions.
Actually I'm not sure these validation functions is required at all. IMO, only the last method to check topic and mask match can be frequently used in the client code - in the incoming message handler. There might be added a new client method like this:
|
agreed they are not, and I would not implement them in the client itself. They are just convenience functions for user-code that wants to validate user-input. Btw; the parser function needs to compile, and the compile function needs to validate, so 3 out of the 4 are needed. So I guess the last one (to validate non-wildcarded topics) is then just for completeness sake. |
yeah, I was thinking along the same lines. But in that case, who will be responsible to call the ACK on the received message? |
This is not related. I think, like in usual message handler, ack should be done explicitly like this: https://github.com/xHasKx/luamqtt/blob/master/examples/simple.lua#L38 |
exactly, but then you'd always need 2 handlers, a generic one that does the ACK, and another one matching a specific topic mask. Still learning on the MQTT stuff; why doesn't the client send the ACK by default? why relying on user-code to do that? |
I think user can use subscribe handlers in 3 ways:
For now I think it will be better to leave user always responsible for sending ack. And if he will use last case with handlers set in both ways and send ack twice accidentally - this will not be a MQTT protocol violation (as I remember). Maybe some brokers will close connection in this case but I think it's uncommon.
For example, MQTT can be used to distribute some tasks between workers, and if worker can't handle some task - it may not send ack to indicate failure (in the other words, worker will not send ack untill task is successfully done). Thus the task will stay in a queue and thus can be handled by some different worker. This scenario requires a persistent mqtt session (clean=false) I suppose there might be a client option for __init() method enabling auto-ack for all messages, but I think it should be disabled by default. |
For example, user can use topic-specific handler to execute it's logic and do ack, and at the same time also use generic client:on() message handler for logging without ack. |
That makes sense, thx for the explanation |
@xHasKx any chance we can move this forward? anything required from me? I rebased the PR on |
-- @param t (string) wildcard topic to validate | ||
-- @return topic, or false+error | ||
function mqtt.validate_subscribe_topic(t) | ||
if type(t) ~= "string" then |
There was a problem hiding this comment.
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:
Line 10 in e3fa62c
* passing invalid arguments (like number instead of string) to function in this library will raise exception |
There was a problem hiding this comment.
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.
-- @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 comment
The reason will be displayed to describe this comment to others. Learn more.
please follow such errors checking behavior:
Line 10 in e3fa62c
* passing invalid arguments (like number instead of string) to function in this library will raise exception |
There was a problem hiding this comment.
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
mqtt/init.lua
Outdated
local pattern = opts.pattern | ||
if not pattern then | ||
local ptopic = opts.topic | ||
if not ptopic then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mqtt/init.lua
Outdated
end | ||
end | ||
end | ||
if not pattern:find("%(%.[%-%+]%)%$$") then -- pattern for "#" as last char |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
And I'm still concerned about mqtt.topic_match function. It's arguments are complicated and it's doing two tasks at the same time:
IMO, we can break that into two functions, first one is just compile_topic_pattern+string.match call, and the second one can be just a helper to split a particular topic string with What do you think? |
Actually string.match will return the words at the wildcards of the topic, so your topic_match function is just adding keys to them. Does it actually required? |
Pushed a commit with the review comments (probably easiest to review that commit without whitespace changes).
|
No, it is not required. It is just convenience to get named arguments/parameters, so user code can be more readable. I have a number of 'id' type fields, which I hate to have in code as array indices, using names is so much more convenient for that. That's the whole purpose I guess (and caching the compiled pattern).
compile + string_match would not work, since there would be no structure where the compiled pattern can be stored (cached) for future use. So that would have to be implemented in user code, and then this function would be reduced to only a That said, I do have doubts about the return values, or the entire interface. Currently I do not like the 2 return values, makes it cumbersome to handle. something like; local topic_matcher = mqtt.create_topic_matcher {
topic = "homes/+/+/#",
"homeid",
"roomid",
"varargs",
}
local fields, err = topic_matcher("homes/myhome/living/mainlights/brightness")
print(fields.homeid) --> "myhome"
print(fields.roomid) --> "living"
print(fields.varargs[1]) --> "mainlights"
print(fields.varargs[2]) --> "brightness" This would enable adding a new incoming message callback per topic-pattern, where the |
@Tieske, I wrote some comments and continue review tomorrow |
@Tieske, what do you think about that form: local topic_matcher = mqtt.topic_matcher { -- without "create_" it looks like a class constructor calling
topic = "homes/+/+/#",
"homeid",
"roomid",
}
local fields, err = topic_matcher("homes/myhome/living/mainlights/brightness")
print(fields.homeid) --> "myhome"
print(fields.roomid) --> "living"
print(fields[1]) --> "myhome"
print(fields[2]) --> "living"
print(fields[3]) --> "mainlights"
print(fields[4]) --> "brightness" It will simplify Another variant is to explicitly provide a key to store array of all unnamed topic words like that: local topic_matcher = mqtt.topic_matcher { -- without "create_" it looks like a class constructor calling
topic = "homes/+/+/#",
"homeid",
"roomid",
rest = "varargs",
}
local fields, err = topic_matcher("homes/myhome/living/mainlights/brightness")
print(fields.homeid) --> "myhome"
print(fields.roomid) --> "living"
print(fields[1]) --> "myhome"
print(fields[2]) --> "living"
print(fields.varargs[1]) --> "mainlights"
print(fields.varargs[2]) --> "brightness" |
if we adjust This option: print(fields.homeid) --> "myhome"
print(fields.roomid) --> "living"
print(fields[1]) --> "myhome"
print(fields[2]) --> "living"
print(fields[3]) --> "mainlights"
print(fields[4]) --> "brightness" has as a drawback that you can't immediately tell where the varargs start in the array part. Hence I would prefer your second option; print(fields.homeid) --> "myhome"
print(fields.roomid) --> "living"
print(fields[1]) --> "myhome"
print(fields[2]) --> "living"
print(fields.varargs[1]) --> "mainlights"
print(fields.varargs[2]) --> "brightness" But here the user needs an extra field name for the varargs, to prevent namecollisions between the field names and the varargs field. Leading me to: print(fields.homeid) --> "myhome"
print(fields.roomid) --> "living"
print(fields[1]) --> "mainlights"
print(fields[2]) --> "brightness" as the simplest version, clearly separating the known fields from the varargs, whilst making varargs size and traversal easy as the array part. And no extra user specified name required. |
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 |
There was a problem hiding this comment.
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;
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 |
I suggested local topic_matcher = mqtt.topic_matcher {
topic = "homes/+/+/#",
"homeid",
-- "roomid", -- note only one name for +
...
} Where the second And we have to find a proper form for the case when user don't have names for
I suggested local topic_matcher = mqtt.topic_matcher {
topic = "homes/+/+/#",
"homeid",
-- "roomid", -- note only one name for +
...
} Where the second And we have to find a proper form for the case when user don't have names for We can define some default name for the Or accept that it's ok to return two values from the matcher function - first table with Or accept that it's ok to return two values from the matcher function - first table with |
is that expected to be a common case? I’d expect "all fields named" to be the common case. We can just throw an error if there aren’t enough field names? Or we could inject numbered names |
I would say, we can't predict which case will be more common. The only thing I'm sure is that the best interface is the one that hard to use in the wrong way unintentionally. We can do one step back and ask how that I mean, user can call some new method callback(publish_message, topic_fields, topic_varargs_fields) In this case for the the In the case of two tables in resulf of the (and of course users can create and use the What do you think? |
see tests for parsing examples.