@@ -81,6 +81,165 @@ function mqtt.run_sync(cl)
8181 end
8282end
8383
84+
85+ --- Validates a topic with wildcards.
86+ -- @param t (string) wildcard topic to validate
87+ -- @return topic, or false+error
88+ function mqtt .validate_subscribe_topic (t )
89+ if type (t ) ~= " string" then
90+ return false , " not a string"
91+ end
92+ if # t < 1 then
93+ return false , " minimum topic length is 1"
94+ end
95+ do
96+ local _ , count = t :gsub (" #" , " " )
97+ if count > 1 then
98+ return false , " wildcard '#' may only appear once"
99+ end
100+ if count == 1 then
101+ if t ~= " #" and not t :find (" /#$" ) then
102+ return false , " wildcard '#' must be the last character, and be prefixed with '/' (unless the topic is '#')"
103+ end
104+ end
105+ end
106+ do
107+ local t1 = " /" .. t .. " /"
108+ local i = 1
109+ while i do
110+ i = t1 :find (" +" , i )
111+ if i then
112+ if t1 :sub (i - 1 , i + 1 ) ~= " /+/" then
113+ return false , " wildcard '+' must be enclosed between '/' (except at start/end)"
114+ end
115+ i = i + 1
116+ end
117+ end
118+ end
119+ return t
120+ end
121+
122+ --- Validates a topic without wildcards.
123+ -- @param t (string) topic to validate
124+ -- @return topic, or false+error
125+ function mqtt .validate_publish_topic (t )
126+ if type (t ) ~= " string" then
127+ return false , " not a string"
128+ end
129+ if # t < 1 then
130+ return false , " minimum topic length is 1"
131+ end
132+ if t :find (" +" , nil , true ) or t :find (" #" , nil , true ) then
133+ return false , " wildcards '#', and '+' are not allowed when publishing"
134+ end
135+ return t
136+ end
137+
138+ --- Returns a Lua pattern from topic.
139+ -- Takes a wildcarded-topic and returns a Lua pattern that can be used
140+ -- to validate if a received topic matches the wildcard-topic
141+ -- @param t (string) the wildcard topic
142+ -- @return Lua-pattern (string) or false+err
143+ function mqtt .compile_topic_pattern (t )
144+ local ok , err = mqtt .validate_subscribe_topic (t )
145+ if not ok then
146+ return ok , err
147+ end
148+ if t == " #" then
149+ t = " (.+)" -- matches anything at least 1 character long
150+ else
151+ t = t :gsub (" #" ," (.-)" ) -- match anything, can be empty
152+ t = t :gsub (" %+" ," ([^/]-)" ) -- match anything between '/', can be empty
153+ end
154+ return " ^" .. t .. " $"
155+ end
156+
157+ --- Parses wildcards in a topic into a table.
158+ -- Options include:
159+ --
160+ -- - `opts.topic`: the wild-carded topic to match against (optional if `pattern` is given)
161+ --
162+ -- - `opts.pattern`: the compiled pattern for the wild-carded topic (optional if `topic`
163+ -- is given). If not given then topic will be compiled and the result will be
164+ -- stored in this field for future use (cache).
165+ --
166+ -- - `opts.keys`: (optional) array of field names. The order must be the same as the
167+ -- order of the wildcards in `topic`
168+ --
169+ -- Returned tables:
170+ --
171+ -- - `fields` table: the array part will have the values of the wildcards, in
172+ -- the order they appeared. The hash part, will have the field names provided
173+ -- in `opts.keys`, with the values of the corresponding wildcard. If a `#`
174+ -- wildcard was used, that one will be the last in the table.
175+ --
176+ -- - `varargs` table: will only be returned if the wildcard topic contained the
177+ -- `#` wildcard. The returned table is an array, with all segments that were
178+ -- matched by the `#` wildcard.
179+ -- @param topic (string) incoming topic string (required)
180+ -- @param opts (table) with options (required)
181+ -- @return fields (table) + varargs (table or nil), or false+err on error.
182+ function mqtt .topic_match (topic , opts )
183+ if type (topic ) ~= " string" then
184+ return false , " expected topic to be a string"
185+ end
186+ if type (opts ) ~= " table" then
187+ return false , " expected optionss to be a table"
188+ end
189+ local pattern = opts .pattern
190+ if not pattern then
191+ local ptopic = opts .topic
192+ if not ptopic then
193+ return false , " either 'opts.topic' or 'opts.pattern' must set"
194+ end
195+ local err
196+ pattern , err = mqtt .compile_topic_pattern (ptopic )
197+ if not pattern then
198+ return false , " failed to compile 'opts.topic' into pattern: " .. tostring (err )
199+ end
200+ -- store/cache compiled pattern for next time
201+ opts .pattern = pattern
202+ end
203+ local values = { topic :match (pattern ) }
204+ if values [1 ] == nil then
205+ return false , " topic does not match wildcard pattern"
206+ end
207+ local keys = opts .keys
208+ if keys ~= nil then
209+ if type (keys ) ~= " table" then
210+ return false , " expected 'opts.keys' to be a table (array)"
211+ end
212+ -- we have a table with keys, copy values to fields
213+ for i , value in ipairs (values ) do
214+ local key = keys [i ]
215+ if key ~= nil then
216+ values [key ] = value
217+ end
218+ end
219+ end
220+ if not pattern :find (" %(%.[%-%+]%)%$$" ) then -- pattern for "#" as last char
221+ -- we're done
222+ return values
223+ end
224+ -- we have a '#' wildcard
225+ local vararg = values [# values ]
226+ local varargs = {}
227+ local i = 0
228+ local ni = 0
229+ while ni do
230+ ni = vararg :find (" /" , i , true )
231+ if ni then
232+ varargs [# varargs + 1 ] = vararg :sub (i , ni - 1 )
233+ i = ni + 1
234+ else
235+ varargs [# varargs + 1 ] = vararg :sub (i , - 1 )
236+ end
237+ end
238+
239+ return values , varargs
240+ end
241+
242+
84243-- export module table
85244return mqtt
86245
0 commit comments