Software /
code /
prosody-modules
Comparison
mod_rest/jsonmap.lib.lua @ 3896:987b203bb091
mod_rest: Restructure JSON / Stanza mapping definitions
It was a pain to remember which index had the mapping function for which
direction. This is more readable.
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Sat, 22 Feb 2020 14:08:19 +0100 |
parent | 3895:25a3ad36ef3e |
child | 3906:dbebc9226597 |
comparison
equal
deleted
inserted
replaced
3895:25a3ad36ef3e | 3896:987b203bb091 |
---|---|
5 local xml = require "util.xml"; | 5 local xml = require "util.xml"; |
6 | 6 |
7 -- Reused in many XEPs so declared up here | 7 -- Reused in many XEPs so declared up here |
8 local dataform = { | 8 local dataform = { |
9 -- Generic and complete dataforms mapping | 9 -- Generic and complete dataforms mapping |
10 "func", "jabber:x:data", "x", | 10 type = "func", xmlns = "jabber:x:data", tagname = "x", |
11 function (s) | 11 st2json = function (s) |
12 local fields = array(); | 12 local fields = array(); |
13 local form = { | 13 local form = { |
14 type = s.attr.type; | 14 type = s.attr.type; |
15 title = s:get_child_text("title"); | 15 title = s:get_child_text("title"); |
16 instructions = s:get_child_text("instructions"); | 16 instructions = s:get_child_text("instructions"); |
45 end | 45 end |
46 fields:push(i); | 46 fields:push(i); |
47 end | 47 end |
48 return form; | 48 return form; |
49 end; | 49 end; |
50 function (x) | 50 json2st = function (x) |
51 if type(x) == "table" and x ~= json.null then | 51 if type(x) == "table" and x ~= json.null then |
52 local form = st.stanza("x", { xmlns = "jabber:x:data", type = x.type }); | 52 local form = st.stanza("x", { xmlns = "jabber:x:data", type = x.type }); |
53 if x.title then | 53 if x.title then |
54 form:text_tag("title", x.title); | 54 form:text_tag("title", x.title); |
55 end | 55 end |
88 return form; | 88 return form; |
89 end | 89 end |
90 end; | 90 end; |
91 }; | 91 }; |
92 | 92 |
93 local function formdata(s,t) | 93 local function formdata(s, t) |
94 local form = st.stanza("x", { xmlns = "jabber:x:data", type = t }); | 94 local form = st.stanza("x", { xmlns = "jabber:x:data", type = t }); |
95 for k,v in pairs(s) do | 95 for k, v in pairs(s) do |
96 form:tag("field", { var = k }); | 96 form:tag("field", { var = k }); |
97 if type(v) == "string" then | 97 if type(v) == "string" then |
98 form:text_tag("value", v); | 98 form:text_tag("value", v); |
99 elseif type(v) == "table" then | 99 elseif type(v) == "table" then |
100 for _, v_ in ipairs(v) do | 100 for _, v_ in ipairs(v) do |
122 -- basic presence | 122 -- basic presence |
123 show = "text_tag", | 123 show = "text_tag", |
124 status = "text_tag", | 124 status = "text_tag", |
125 priority = "text_tag", | 125 priority = "text_tag", |
126 | 126 |
127 state = {"name", "http://jabber.org/protocol/chatstates"}, | 127 state = { type = "name", xmlns = "http://jabber.org/protocol/chatstates" }, |
128 nick = {"text_tag", "http://jabber.org/protocol/nick", "nick"}, | 128 nick = { type = "text_tag", xmlns = "http://jabber.org/protocol/nick", tagname = "nick" }, |
129 delay = {"attr", "urn:xmpp:delay", "delay", "stamp"}, | 129 delay = { type = "attr", xmlns = "urn:xmpp:delay", tagname = "delay", attr = "stamp" }, |
130 replace = {"attr", "urn:xmpp:message-correct:0", "replace", "id"}, | 130 replace = { type = "attr", xmlns = "urn:xmpp:message-correct:0", tagname = "replace", attr = "id" }, |
131 | 131 |
132 -- XEP-0045 MUC | 132 -- XEP-0045 MUC |
133 -- TODO history, password, ??? | 133 -- TODO history, password, ??? |
134 join = {"bool_tag", "http://jabber.org/protocol/muc", "x"}, | 134 join = { type = "bool_tag", xmlns = "http://jabber.org/protocol/muc", tagname = "x" }, |
135 | 135 |
136 -- XEP-0071 | 136 -- XEP-0071 |
137 html = { | 137 html = { |
138 "func", "http://jabber.org/protocol/xhtml-im", "html", | 138 type = "func", xmlns = "http://jabber.org/protocol/xhtml-im", tagname = "html", |
139 function (s) --> json string | 139 st2json = function (s) --> json string |
140 return (tostring(s:get_child("body", "http://www.w3.org/1999/xhtml")):gsub(" xmlns='[^']*'","", 1)); | 140 return (tostring(s:get_child("body", "http://www.w3.org/1999/xhtml")):gsub(" xmlns='[^']*'", "", 1)); |
141 end; | 141 end; |
142 function (s) --> xml | 142 json2st = function (s) --> xml |
143 if type(s) == "string" then | 143 if type(s) == "string" then |
144 return assert(xml.parse([[<x:html xmlns:x='http://jabber.org/protocol/xhtml-im' xmlns='http://www.w3.org/1999/xhtml'>]]..s..[[</x:html>]])); | 144 return assert(xml.parse("<x:html xmlns:x='http://jabber.org/protocol/xhtml-im' xmlns='http://www.w3.org/1999/xhtml'>" .. s .. "</x:html>")); |
145 end | 145 end |
146 end; | 146 end; |
147 }; | 147 }; |
148 | 148 |
149 -- XEP-0199: XMPP Ping | 149 -- XEP-0199: XMPP Ping |
150 ping = {"bool_tag", "urn:xmpp:ping", "ping"}, | 150 ping = { type = "bool_tag", xmlns = "urn:xmpp:ping", tagname = "ping" }, |
151 | 151 |
152 -- XEP-0092: Software Version | 152 -- XEP-0092: Software Version |
153 version = {"func", "jabber:iq:version", "query", | 153 version = { type = "func", xmlns = "jabber:iq:version", tagname = "query", |
154 function (s) | 154 st2json = function (s) |
155 return { | 155 return { |
156 name = s:get_child_text("name"); | 156 name = s:get_child_text("name"); |
157 version = s:get_child_text("version"); | 157 version = s:get_child_text("version"); |
158 os = s:get_child_text("os"); | 158 os = s:get_child_text("os"); |
159 } | 159 } |
160 end, | 160 end, |
161 function (s) | 161 json2st = function (s) |
162 local v = st.stanza("query", { xmlns = "jabber:iq:version" }); | 162 local v = st.stanza("query", { xmlns = "jabber:iq:version" }); |
163 if type(s) == "table" then | 163 if type(s) == "table" then |
164 v:text_tag("name", s.name); | 164 v:text_tag("name", s.name); |
165 v:text_tag("version", s.version); | 165 v:text_tag("version", s.version); |
166 if s.os then | 166 if s.os then |
171 end | 171 end |
172 }; | 172 }; |
173 | 173 |
174 -- XEP-0030 | 174 -- XEP-0030 |
175 disco = { | 175 disco = { |
176 "func", "http://jabber.org/protocol/disco#info", "query", | 176 type = "func", xmlns = "http://jabber.org/protocol/disco#info", tagname = "query", |
177 function (s) --> array of features | 177 st2json = function (s) --> array of features |
178 local identities, features = array(), array(); | 178 local identities, features = array(), array(); |
179 for tag in s:childtags() do | 179 for tag in s:childtags() do |
180 if tag.name == "identity" and tag.attr.category and tag.attr.type then | 180 if tag.name == "identity" and tag.attr.category and tag.attr.type then |
181 identities:push({ category = tag.attr.category, type = tag.attr.type, name = tag.attr.name }); | 181 identities:push({ category = tag.attr.category, type = tag.attr.type, name = tag.attr.name }); |
182 elseif tag.name == "feature" and tag.attr.var then | 182 elseif tag.name == "feature" and tag.attr.var then |
183 features:push(tag.attr.var); | 183 features:push(tag.attr.var); |
184 end | 184 end |
185 end | 185 end |
186 return { node = s.attr.node, identities = identities, features = features, }; | 186 return { node = s.attr.node, identities = identities, features = features, }; |
187 end; | 187 end; |
188 function (s) | 188 json2st = function (s) |
189 if type(s) == "table" and s ~= json.null then | 189 if type(s) == "table" and s ~= json.null then |
190 local disco = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info", node = s.node }); | 190 local disco = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info", node = s.node }); |
191 if s.identities then | 191 if s.identities then |
192 for _, identity in ipairs(s.identities) do | 192 for _, identity in ipairs(s.identities) do |
193 disco:tag("identity", { category = identity.category, type = identity.type, name = identity.name }):up(); | 193 disco:tag("identity", { category = identity.category, type = identity.type, name = identity.name }):up(); |
204 end | 204 end |
205 end; | 205 end; |
206 }; | 206 }; |
207 | 207 |
208 items = { | 208 items = { |
209 "func", "http://jabber.org/protocol/disco#items", "query", | 209 type = "func", xmlns = "http://jabber.org/protocol/disco#items", tagname = "query", |
210 function (s) --> array of features | map with node | 210 st2json = function (s) --> array of features | map with node |
211 if s.attr.node and s.tags[1] == nil then | 211 if s.attr.node and s.tags[1] == nil then |
212 return { node = s.attr. node }; | 212 return { node = s.attr.node }; |
213 end | 213 end |
214 | 214 |
215 local items = array(); | 215 local items = array(); |
216 for item in s:childtags("item") do | 216 for item in s:childtags("item") do |
217 items:push({ jid = item.attr.jid, node = item.attr.node, name = item.attr.name }); | 217 items:push({ jid = item.attr.jid, node = item.attr.node, name = item.attr.name }); |
218 end | 218 end |
219 return items; | 219 return items; |
220 end; | 220 end; |
221 function (s) | 221 json2st = function (s) |
222 if type(s) == "table" and s ~= json.null then | 222 if type(s) == "table" and s ~= json.null then |
223 local disco = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#items", node = s.node }); | 223 local disco = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#items", node = s.node }); |
224 for _, item in ipairs(s) do | 224 for _, item in ipairs(s) do |
225 if type(item) == "string" then | 225 if type(item) == "string" then |
226 disco:tag("item", { jid = item }); | 226 disco:tag("item", { jid = item }); |
234 end | 234 end |
235 end; | 235 end; |
236 }; | 236 }; |
237 | 237 |
238 -- XEP-0050: Ad-Hoc Commands | 238 -- XEP-0050: Ad-Hoc Commands |
239 command = {"func", "http://jabber.org/protocol/commands", "command", | 239 command = { type = "func", xmlns = "http://jabber.org/protocol/commands", tagname = "command", |
240 function (s) | 240 st2json = function (s) |
241 local cmd = { | 241 local cmd = { |
242 action = s.attr.action, | 242 action = s.attr.action, |
243 node = s.attr.node, | 243 node = s.attr.node, |
244 sessionid = s.attr.sessionid, | 244 sessionid = s.attr.sessionid, |
245 status = s.attr.status, | 245 status = s.attr.status, |
259 type = note.attr.type; | 259 type = note.attr.type; |
260 text = note:get_text(); | 260 text = note:get_text(); |
261 }; | 261 }; |
262 end | 262 end |
263 if form then | 263 if form then |
264 cmd.form = dataform[4](form); | 264 cmd.form = dataform[4] (form); |
265 end | 265 end |
266 return cmd; | 266 return cmd; |
267 end; | 267 end; |
268 function (s) | 268 json2st = function (s) |
269 if type(s) == "table" and s ~= json.null then | 269 if type(s) == "table" and s ~= json.null then |
270 local cmd = st.stanza("command", { | 270 local cmd = st.stanza("command", { |
271 xmlns = "http://jabber.org/protocol/commands", | 271 xmlns = "http://jabber.org/protocol/commands", |
272 action = s.action, | 272 action = s.action, |
273 node = s.node, | 273 node = s.node, |
274 sessionid = s.sessionid, | 274 sessionid = s.sessionid, |
275 status = s.status, | 275 status = s.status, |
276 }); | 276 }); |
277 if type(s.actions) == "table" then | 277 if type(s.actions) == "table" then |
278 cmd:tag("actions", { execute = s.actions.execute }); | 278 cmd:tag("actions", { execute = s.actions.execute }); |
279 do | 279 do |
280 if s.actions.next == true then | 280 if s.actions.next == true then |
281 cmd:tag("next"):up(); | 281 cmd:tag("next"):up(); |
290 cmd:up(); | 290 cmd:up(); |
291 elseif type(s.note) == "table" then | 291 elseif type(s.note) == "table" then |
292 cmd:text_tag("note", s.note.text, { type = s.note.type }); | 292 cmd:text_tag("note", s.note.text, { type = s.note.type }); |
293 end | 293 end |
294 if s.form then | 294 if s.form then |
295 cmd:add_child(dataform[5](s.form)); | 295 cmd:add_child(dataform[5] (s.form)); |
296 elseif s.data then | 296 elseif s.data then |
297 cmd:add_child(formdata(s.data)); | 297 cmd:add_child(formdata(s.data)); |
298 end | 298 end |
299 return cmd; | 299 return cmd; |
300 elseif type(s) == "string" then -- assume node | 300 elseif type(s) == "string" then -- assume node |
301 return st.stanza("command", { xmlns = "http://jabber.org/protocol/commands", node = s }); | 301 return st.stanza("command", { xmlns = "http://jabber.org/protocol/commands", node = s }); |
302 end | 302 end |
303 -- else .. missing required attribute | 303 -- else .. missing required attribute |
304 end; | 304 end; |
305 }; | 305 }; |
306 | 306 |
307 -- XEP-0066: Out of Band Data | 307 -- XEP-0066: Out of Band Data |
308 oob_url = {"func", "jabber:iq:oob", "query", | 308 oob_url = { type = "func", xmlns = "jabber:iq:oob", tagname = "query", |
309 function (s) | 309 st2json = function (s) |
310 return s:get_child_text("url"); | 310 return s:get_child_text("url"); |
311 end; | 311 end; |
312 function (s) | 312 json2st = function (s) |
313 if type(s) == "string" then | 313 if type(s) == "string" then |
314 return st.stanza("query", { xmlns = "jabber:iq:oob" }):text_tag("url", s); | 314 return st.stanza("query", { xmlns = "jabber:iq:oob" }):text_tag("url", s); |
315 end | 315 end |
316 end; | 316 end; |
317 }; | 317 }; |
318 | 318 |
319 -- XEP-XXXX: User-defined Data Transfer | 319 -- XEP-XXXX: User-defined Data Transfer |
320 payload = {"func", "urn:xmpp:udt:0", "payload", | 320 payload = { type = "func", xmlns = "urn:xmpp:udt:0", tagname = "payload", |
321 function (s) | 321 st2json = function (s) |
322 local rawjson = s:get_child_text("json", "urn:xmpp:json:0"); | 322 local rawjson = s:get_child_text("json", "urn:xmpp:json:0"); |
323 if not rawjson then return nil, "missing-json-payload"; end | 323 if not rawjson then return nil, "missing-json-payload"; end |
324 local parsed, err = json.decode(rawjson); | 324 local parsed, err = json.decode(rawjson); |
325 if not parsed then return nil, err; end | 325 if not parsed then return nil, err; end |
326 return { | 326 return { |
327 datatype = s.attr.datatype; | 327 datatype = s.attr.datatype; |
328 data = parsed; | 328 data = parsed; |
329 }; | 329 }; |
330 end; | 330 end; |
331 function (s) | 331 json2st = function (s) |
332 if type(s) == "table" then | 332 if type(s) == "table" then |
333 return st.stanza("payload", { xmlns = "urn:xmpp:udt:0", datatype = s.datatype }) | 333 return st.stanza("payload", { xmlns = "urn:xmpp:udt:0", datatype = s.datatype }) |
334 :tag("json", { xmlns = "urn:xmpp:json:0" }):text(json.encode(s.data)); | 334 :tag("json", { xmlns = "urn:xmpp:json:0" }):text(json.encode(s.data)); |
335 end; | 335 end; |
336 end | 336 end |
337 }; | 337 }; |
338 | 338 |
339 -- XEP-0004: Data Forms | 339 -- XEP-0004: Data Forms |
340 dataform = dataform; | 340 dataform = dataform; |
341 | 341 |
342 -- Simpler mapping from JSON map | 342 -- Simpler mapping from JSON map |
343 formdata = {"func", "jabber:x:data", "", | 343 formdata = { type = "func", xmlns = "jabber:x:data", tagname = "", |
344 function () | 344 st2json = function () |
345 -- Tricky to do in a generic way without each form layout | 345 -- Tricky to do in a generic way without each form layout |
346 -- In the future, some well-known layouts might be understood | 346 -- In the future, some well-known layouts might be understood |
347 return nil, "not-implemented"; | 347 return nil, "not-implemented"; |
348 end, | 348 end, |
349 formdata, | 349 json2st = formdata, |
350 }; | 350 }; |
351 }; | 351 }; |
352 | 352 |
353 local implied_kinds = { | 353 local implied_kinds = { |
354 disco = "iq", | 354 disco = "iq", |
412 end | 412 end |
413 | 413 |
414 for k, mapping in pairs(field_mappings) do | 414 for k, mapping in pairs(field_mappings) do |
415 if mapping == "text_tag" then | 415 if mapping == "text_tag" then |
416 t[k] = s:get_child_text(k); | 416 t[k] = s:get_child_text(k); |
417 elseif mapping[1] == "text_tag" then | 417 elseif mapping.type == "text_tag" then |
418 t[k] = s:get_child_text(mapping[3], mapping[2]); | 418 t[k] = s:get_child_text(mapping.tagname, mapping.xmlns); |
419 elseif mapping[1] == "name" then | 419 elseif mapping.type == "name" then |
420 local child = s:get_child(nil, mapping[2]); | 420 local child = s:get_child(nil, mapping.xmlns); |
421 if child then | 421 if child then |
422 t[k] = child.name; | 422 t[k] = child.name; |
423 end | 423 end |
424 elseif mapping[1] == "attr" then | 424 elseif mapping.type == "attr" then |
425 local child = s:get_child(mapping[3], mapping[2]) | 425 local child = s:get_child(mapping.tagname, mapping.xmlns); |
426 if child then | 426 if child then |
427 t[k] = child.attr[mapping[4]]; | 427 t[k] = child.attr[mapping.attr]; |
428 end | 428 end |
429 elseif mapping[1] == "bool_tag" then | 429 elseif mapping.type == "bool_tag" then |
430 if s:get_child(mapping[3], mapping[2]) then | 430 if s:get_child(mapping.tagname, mapping.xmlns) then |
431 t[k] = true; | 431 t[k] = true; |
432 end | 432 end |
433 elseif mapping[1] == "func" then | 433 elseif mapping.type == "func" and mapping.st2json then |
434 local child = s:get_child(mapping[3], mapping[2] or k); | 434 local child = s:get_child(mapping.tagname, mapping.xmlns or k); |
435 -- TODO handle err | 435 -- TODO handle err |
436 if child then | 436 if child then |
437 t[k] = mapping[4](child); | 437 t[k] = mapping.st2json(child); |
438 end | 438 end |
439 end | 439 end |
440 end | 440 end |
441 | 441 |
442 return t; | 442 return t; |
491 if mapping then | 491 if mapping then |
492 if mapping == "text_tag" then | 492 if mapping == "text_tag" then |
493 s:text_tag(k, v); | 493 s:text_tag(k, v); |
494 elseif mapping == "attr" then -- luacheck: ignore 542 | 494 elseif mapping == "attr" then -- luacheck: ignore 542 |
495 -- handled already | 495 -- handled already |
496 elseif mapping[1] == "text_tag" then | 496 elseif mapping.type == "text_tag" then |
497 s:text_tag(mapping[3] or k, v, mapping[2] and { xmlns = mapping[2] }); | 497 s:text_tag(mapping.tagname or k, v, mapping.xmlns and { xmlns = mapping.xmlns }); |
498 elseif mapping[1] == "name" then | 498 elseif mapping.type == "name" then |
499 s:tag(v, { xmlns = mapping[2] }):up(); | 499 s:tag(v, { xmlns = mapping.xmlns }):up(); |
500 elseif mapping[1] == "attr" then | 500 elseif mapping.type == "attr" then |
501 s:tag(mapping[3] or k, { xmlns = mapping[2], [ mapping[4] or k ] = v }):up(); | 501 s:tag(mapping.tagname or k, { xmlns = mapping.xmlns, [mapping.attr or k] = v }):up(); |
502 elseif mapping[1] == "bool_tag" then | 502 elseif mapping.type == "bool_tag" then |
503 s:tag(mapping[3] or k, { xmlns = mapping[2] }):up(); | 503 s:tag(mapping.tagname or k, { xmlns = mapping.xmlns }):up(); |
504 elseif mapping[1] == "func" then | 504 elseif mapping.type == "func" and mapping.json2st then |
505 s:add_child(mapping[5](v)):up(); | 505 s:add_child(mapping.json2st(v)):up(); |
506 end | 506 end |
507 else | 507 else |
508 return nil, "unknown-field"; | 508 return nil, "unknown-field"; |
509 end | 509 end |
510 end | 510 end |