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
parents 25a3ad36ef3e
children 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