Comparison

teal-src/util/datamapper.tl @ 12133:11060c8919b6

util.datamapper: Add support for $ref pointers Allows reuse of repetitive definitions in schemas.
author Kim Alvefur <zash@zash.se>
date Wed, 29 Dec 2021 17:57:09 +0100
parent 11481:b2f9782497dd
child 12580:a9dbf657c894
comparison
equal deleted inserted replaced
12132:4ff0d33dfb2b 12133:11060c8919b6
19 -- TODO s/number/integer/ once we have appropriate math.type() compat 19 -- TODO s/number/integer/ once we have appropriate math.type() compat
20 -- 20 --
21 21
22 local st = require "util.stanza"; 22 local st = require "util.stanza";
23 local json = require"util.json" 23 local json = require"util.json"
24 local pointer = require"util.jsonpointer";
24 25
25 local json_type_name = json.json_type_name; 26 local json_type_name = json.json_type_name;
26 local json_schema_object = require "util.jsonschema" 27 local json_schema_object = require "util.jsonschema"
27 local type schema_t = boolean | json_type_name | json_schema_object 28 local type schema_t = boolean | json_type_name | json_schema_object
28 29
53 "in_text_tag" 54 "in_text_tag"
54 "in_attribute" 55 "in_attribute"
55 "in_single_attribute" 56 "in_single_attribute"
56 "in_children" 57 "in_children"
57 "in_wrapper" 58 "in_wrapper"
59 end
60
61 local function resolve_schema(schema : schema_t, root : json_schema_object) : schema_t
62 if schema is json_schema_object and schema["$ref"] and schema["$ref"]:sub(1, 1) == "#" then
63 local referenced = pointer.resolve(root as table, schema["$ref"]:sub(2)) as schema_t;
64 if referenced ~= nil then
65 return referenced
66 end
67 end
68 return schema;
58 end 69 end
59 70
60 local function unpack_propschema( propschema : schema_t, propname : string, current_ns : string ) 71 local function unpack_propschema( propschema : schema_t, propname : string, current_ns : string )
61 : json_type_name, value_goes, string, string, string, string, { any } 72 : json_type_name, value_goes, string, string, string, string, { any }
62 local proptype : json_type_name = "string" 73 local proptype : json_type_name = "string"
110 end 121 end
111 122
112 return proptype, value_where, name, namespace, prefix, single_attribute, enums 123 return proptype, value_where, name, namespace, prefix, single_attribute, enums
113 end 124 end
114 125
115 local parse_object : function (schema : schema_t, s : st.stanza_t) : { string : any } 126 local parse_object : function (schema : schema_t, s : st.stanza_t, root : json_schema_object) : { string : any }
116 local parse_array : function (schema : schema_t, s : st.stanza_t) : { any } 127 local parse_array : function (schema : schema_t, s : st.stanza_t, root : json_schema_object) : { any }
117 128
118 local function extract_value (s : st.stanza_t, value_where : value_goes, proptype : json.json_type_name, name : string, namespace : string, prefix : string, single_attribute : string, enums : { any }) : string 129 local function extract_value (s : st.stanza_t, value_where : value_goes, proptype : json.json_type_name, name : string, namespace : string, prefix : string, single_attribute : string, enums : { any }) : string
119 if value_where == "in_tag_name" then 130 if value_where == "in_tag_name" then
120 local c : st.stanza_t 131 local c : st.stanza_t
121 if proptype == "boolean" then 132 if proptype == "boolean" then
152 elseif value_where == "in_text_tag" then 163 elseif value_where == "in_text_tag" then
153 return s:get_child_text(name, namespace) 164 return s:get_child_text(name, namespace)
154 end 165 end
155 end 166 end
156 167
157 function parse_object (schema : schema_t, s : st.stanza_t) : { string : any } 168 function parse_object (schema : schema_t, s : st.stanza_t, root : json_schema_object) : { string : any }
158 local out : { string : any } = {} 169 local out : { string : any } = {}
170 schema = resolve_schema(schema, root)
159 if schema is json_schema_object and schema.properties then 171 if schema is json_schema_object and schema.properties then
160 for prop, propschema in pairs(schema.properties) do 172 for prop, propschema in pairs(schema.properties) do
173 propschema = resolve_schema(propschema, root)
161 174
162 local proptype, value_where, name, namespace, prefix, single_attribute, enums = unpack_propschema(propschema, prop, s.attr.xmlns) 175 local proptype, value_where, name, namespace, prefix, single_attribute, enums = unpack_propschema(propschema, prop, s.attr.xmlns)
163 176
164 if value_where == "in_children" and propschema is json_schema_object then 177 if value_where == "in_children" and propschema is json_schema_object then
165 if proptype == "object" then 178 if proptype == "object" then
166 local c = s:get_child(name, namespace) 179 local c = s:get_child(name, namespace)
167 if c then 180 if c then
168 out[prop] = parse_object(propschema, c); 181 out[prop] = parse_object(propschema, c, root);
169 end 182 end
170 elseif proptype == "array" then 183 elseif proptype == "array" then
171 local a = parse_array(propschema, s); 184 local a = parse_array(propschema, s, root);
172 if a and a[1] ~= nil then 185 if a and a[1] ~= nil then
173 out[prop] = a; 186 out[prop] = a;
174 end 187 end
175 else 188 else
176 error "unreachable" 189 error "unreachable"
177 end 190 end
178 elseif value_where == "in_wrapper" and propschema is json_schema_object and proptype == "array" then 191 elseif value_where == "in_wrapper" and propschema is json_schema_object and proptype == "array" then
179 local wrapper = s:get_child(name, namespace); 192 local wrapper = s:get_child(name, namespace);
180 if wrapper then 193 if wrapper then
181 out[prop] = parse_array(propschema, wrapper); 194 out[prop] = parse_array(propschema, wrapper, root);
182 end 195 end
183 else 196 else
184 local value : string = extract_value (s, value_where, proptype, name, namespace, prefix, single_attribute, enums) 197 local value : string = extract_value (s, value_where, proptype, name, namespace, prefix, single_attribute, enums)
185 198
186 out[prop] = totype(proptype, value) 199 out[prop] = totype(proptype, value)
189 end 202 end
190 203
191 return out 204 return out
192 end 205 end
193 206
194 function parse_array (schema : json_schema_object, s : st.stanza_t) : { any } 207 function parse_array (schema : json_schema_object, s : st.stanza_t, root : json_schema_object) : { any }
195 local itemschema : schema_t = schema.items; 208 local itemschema : schema_t = resolve_schema(schema.items, root);
196 local proptype, value_where, child_name, namespace, prefix, single_attribute, enums = unpack_propschema(itemschema, nil, s.attr.xmlns) 209 local proptype, value_where, child_name, namespace, prefix, single_attribute, enums = unpack_propschema(itemschema, nil, s.attr.xmlns)
197 local attr_name : string 210 local attr_name : string
198 if value_where == "in_single_attribute" then -- FIXME this shouldn't be needed 211 if value_where == "in_single_attribute" then -- FIXME this shouldn't be needed
199 value_where = "in_attribute"; 212 value_where = "in_attribute";
200 attr_name = single_attribute; 213 attr_name = single_attribute;
202 local out : { any } = {} 215 local out : { any } = {}
203 216
204 if proptype == "object" then 217 if proptype == "object" then
205 if itemschema is json_schema_object then 218 if itemschema is json_schema_object then
206 for c in s:childtags(child_name, namespace) do 219 for c in s:childtags(child_name, namespace) do
207 table.insert(out, parse_object(itemschema, c)); 220 table.insert(out, parse_object(itemschema, c, root));
208 end 221 end
209 else 222 else
210 error "array items must be schema object" 223 error "array items must be schema object"
211 end 224 end
212 elseif proptype == "array" then 225 elseif proptype == "array" then
213 if itemschema is json_schema_object then 226 if itemschema is json_schema_object then
214 for c in s:childtags(child_name, namespace) do 227 for c in s:childtags(child_name, namespace) do
215 table.insert(out, parse_array(itemschema, c)); 228 table.insert(out, parse_array(itemschema, c, root));
216 end 229 end
217 end 230 end
218 else 231 else
219 for c in s:childtags(child_name, namespace) do 232 for c in s:childtags(child_name, namespace) do
220 local value : string = extract_value (c, value_where, proptype, attr_name or child_name, namespace, prefix, single_attribute, enums) 233 local value : string = extract_value (c, value_where, proptype, attr_name or child_name, namespace, prefix, single_attribute, enums)
225 return out; 238 return out;
226 end 239 end
227 240
228 local function parse (schema : json_schema_object, s : st.stanza_t) : table 241 local function parse (schema : json_schema_object, s : st.stanza_t) : table
229 if schema.type == "object" then 242 if schema.type == "object" then
230 return parse_object(schema, s) 243 return parse_object(schema, s, schema)
231 elseif schema.type == "array" then 244 elseif schema.type == "array" then
232 return parse_array(schema, s) 245 return parse_array(schema, s, schema)
233 else 246 else
234 error "top-level scalars unsupported" 247 error "top-level scalars unsupported"
235 end 248 end
236 end 249 end
237 250
245 elseif proptype == "boolean" then 258 elseif proptype == "boolean" then
246 return v and "1" or "0" 259 return v and "1" or "0"
247 end 260 end
248 end 261 end
249 262
250 local unparse : function (json_schema_object, table, string, string, st.stanza_t) : st.stanza_t 263 local unparse : function (json_schema_object, table, string, string, st.stanza_t, json_schema_object) : st.stanza_t
251 264
252 local function unparse_property(out : st.stanza_t, v : any, proptype : json_type_name, propschema : schema_t, value_where : value_goes, name : string, namespace : string, current_ns : string, prefix : string, single_attribute : string) 265 local function unparse_property(out : st.stanza_t, v : any, proptype : json_type_name, propschema : schema_t, value_where : value_goes, name : string, namespace : string, current_ns : string, prefix : string, single_attribute : string, root : json_schema_object)
266
253 if value_where == "in_attribute" then 267 if value_where == "in_attribute" then
254 local attr = name 268 local attr = name
255 if prefix then 269 if prefix then
256 attr = prefix .. ':' .. name 270 attr = prefix .. ':' .. name
257 elseif namespace and namespace ~= current_ns then 271 elseif namespace and namespace ~= current_ns then
282 out:tag(v, propattr):up(); 296 out:tag(v, propattr):up();
283 elseif proptype == "boolean" and v == true then 297 elseif proptype == "boolean" and v == true then
284 out:tag(name, propattr):up(); 298 out:tag(name, propattr):up();
285 end 299 end
286 elseif proptype == "object" and propschema is json_schema_object and v is table then 300 elseif proptype == "object" and propschema is json_schema_object and v is table then
287 local c = unparse(propschema, v, name, namespace); 301 local c = unparse(propschema, v, name, namespace, nil, root);
288 if c then 302 if c then
289 out:add_direct_child(c); 303 out:add_direct_child(c);
290 end 304 end
291 elseif proptype == "array" and propschema is json_schema_object and v is table then 305 elseif proptype == "array" and propschema is json_schema_object and v is table then
292 if value_where == "in_wrapper" then 306 if value_where == "in_wrapper" then
293 local c = unparse(propschema, v, name, namespace); 307 local c = unparse(propschema, v, name, namespace, nil, root);
294 if c then 308 if c then
295 out:add_direct_child(c); 309 out:add_direct_child(c);
296 end 310 end
297 else 311 else
298 unparse(propschema, v, name, namespace, out); 312 unparse(propschema, v, name, namespace, out, root);
299 end 313 end
300 else 314 else
301 out:text_tag(name, toxmlstring(proptype, v), propattr) 315 out:text_tag(name, toxmlstring(proptype, v), propattr)
302 end 316 end
303 end 317 end
304 end 318 end
305 319
306 function unparse ( schema : json_schema_object, t : table, current_name : string, current_ns : string, ctx : st.stanza_t ) : st.stanza_t 320 function unparse ( schema : json_schema_object, t : table, current_name : string, current_ns : string, ctx : st.stanza_t, root : json_schema_object ) : st.stanza_t
321
322 if root == nil then root = schema end
307 323
308 if schema.xml then 324 if schema.xml then
309 if schema.xml.name then 325 if schema.xml.name then
310 current_name = schema.xml.name 326 current_name = schema.xml.name
311 end 327 end
318 local out = ctx or st.stanza(current_name, { xmlns = current_ns }) 334 local out = ctx or st.stanza(current_name, { xmlns = current_ns })
319 335
320 if schema.type == "object" then 336 if schema.type == "object" then
321 337
322 for prop, propschema in pairs(schema.properties) do 338 for prop, propschema in pairs(schema.properties) do
339 propschema = resolve_schema(propschema, root)
323 local v = t[prop] 340 local v = t[prop]
324 341
325 if v ~= nil then 342 if v ~= nil then
326 local proptype, value_where, name, namespace, prefix, single_attribute = unpack_propschema(propschema, prop, current_ns) 343 local proptype, value_where, name, namespace, prefix, single_attribute = unpack_propschema(propschema, prop, current_ns)
327 unparse_property(out, v, proptype, propschema, value_where, name, namespace, current_ns, prefix, single_attribute) 344 unparse_property(out, v, proptype, propschema, value_where, name, namespace, current_ns, prefix, single_attribute, root)
328 end 345 end
329 end 346 end
330 return out; 347 return out;
331 348
332 elseif schema.type == "array" then 349 elseif schema.type == "array" then
333 local proptype, value_where, name, namespace, prefix, single_attribute = unpack_propschema(schema.items, current_name, current_ns) 350 local itemschema = resolve_schema(schema.items, root)
351 local proptype, value_where, name, namespace, prefix, single_attribute = unpack_propschema(itemschema, current_name, current_ns)
334 for _, item in ipairs(t as { string }) do 352 for _, item in ipairs(t as { string }) do
335 unparse_property(out, item, proptype, schema.items, value_where, name, namespace, current_ns, prefix, single_attribute) 353 unparse_property(out, item, proptype, itemschema, value_where, name, namespace, current_ns, prefix, single_attribute, root)
336 end 354 end
337 return out; 355 return out;
338 end 356 end
339 end 357 end
340 358