util.datamapper: Improve handling of schemas with non-obvious "type"

The JSON Schema specification says that schemas are objects or booleans,
and that the 'type' property is optional and can be an array.

This module previously allowed bare type names as schemas and did not
really handle booleans.

It now handles missing 'type' properties and boolean 'true' as a schema.
Objects and arrays are guessed based on the presence of 'properties' or
'items' field.
This commit is contained in:
Kim Alvefur 2022-07-08 17:32:48 +02:00
parent e700edc50f
commit 89359b70dc
3 changed files with 63 additions and 25 deletions

View file

@ -25,7 +25,7 @@ describe("util.datamapper", function()
from = attr(); from = attr();
type = attr(); type = attr();
id = attr(); id = attr();
body = "string"; body = true; -- should be assumed to be a string
lang = {type = "string"; xml = {attribute = true; prefix = "xml"}}; lang = {type = "string"; xml = {attribute = true; prefix = "xml"}};
delay = { delay = {
type = "object"; type = "object";
@ -56,7 +56,8 @@ describe("util.datamapper", function()
xml = {namespace = "urn:xmpp:reactions:0"; name = "reactions"}; xml = {namespace = "urn:xmpp:reactions:0"; name = "reactions"};
properties = { properties = {
to = {type = "string"; xml = {attribute = true; name = "id"}}; to = {type = "string"; xml = {attribute = true; name = "id"}};
reactions = {type = "array"; items = {type = "string"; xml = {name = "reaction"}}}; -- should be assumed to be array since it has 'items'
reactions = { items = { xml = { name = "reaction" } } };
}; };
}; };
stanza_ids = { stanza_ids = {
@ -190,7 +191,8 @@ describe("util.datamapper", function()
version = { version = {
type = "object"; type = "object";
xml = {name = "query"; namespace = "jabber:iq:version"}; xml = {name = "query"; namespace = "jabber:iq:version"};
properties = {name = "string"; version = "string"; os = "string"}; -- properties should be assumed to be strings
properties = {name = true; version = {}; os = {}};
}; };
}; };
}; };

View file

@ -25,7 +25,7 @@ local pointer = require"util.jsonpointer";
local json_type_name = json.json_type_name; local json_type_name = json.json_type_name;
local json_schema_object = require "util.jsonschema" local json_schema_object = require "util.jsonschema"
local type schema_t = boolean | json_type_name | json_schema_object local type schema_t = boolean | json_schema_object
local function toboolean ( s : string ) : boolean local function toboolean ( s : string ) : boolean
if s == "true" or s == "1" then if s == "true" or s == "1" then
@ -59,15 +59,28 @@ local enum value_goes
end end
local function resolve_schema(schema : schema_t, root : json_schema_object) : schema_t local function resolve_schema(schema : schema_t, root : json_schema_object) : schema_t
if schema is json_schema_object and schema["$ref"] and schema["$ref"]:sub(1, 1) == "#" then if schema is json_schema_object then
local referenced = pointer.resolve(root as table, schema["$ref"]:sub(2)) as schema_t; if schema["$ref"] and schema["$ref"]:sub(1, 1) == "#" then
if referenced ~= nil then return pointer.resolve(root as table, schema["$ref"]:sub(2)) as schema_t;
return referenced
end end
end end
return schema; return schema;
end end
local function guess_schema_type(schema : json_schema_object) : json_type_name
local schema_types = schema.type
if schema_types is json_type_name then
return schema_types
elseif schema_types ~= nil then
error "schema has unsupported 'type' property"
elseif schema.properties then
return "object"
elseif schema.items then
return "array"
end
return "string" -- default assumption
end
local function unpack_propschema( propschema : schema_t, propname : string, current_ns : string ) local function unpack_propschema( propschema : schema_t, propname : string, current_ns : string )
: json_type_name, value_goes, string, string, string, string, { any } : json_type_name, value_goes, string, string, string, string, { any }
local proptype : json_type_name = "string" local proptype : json_type_name = "string"
@ -79,9 +92,9 @@ local function unpack_propschema( propschema : schema_t, propname : string, curr
local enums : { any } local enums : { any }
if propschema is json_schema_object then if propschema is json_schema_object then
proptype = propschema.type proptype = guess_schema_type(propschema);
elseif propschema is json_type_name then elseif propschema is string then -- Teal says this can never be a string, but it could before so best be sure
proptype = propschema error("schema as string is not supported: "..propschema.." {"..current_ns.."}"..propname)
end end
if proptype == "object" or proptype == "array" then if proptype == "object" or proptype == "array" then
@ -120,6 +133,10 @@ local function unpack_propschema( propschema : schema_t, propname : string, curr
end end
end end
if current_ns == "urn:xmpp:reactions:0" and name == "reactions" then
assert(proptype=="array")
end
return proptype, value_where, name, namespace, prefix, single_attribute, enums return proptype, value_where, name, namespace, prefix, single_attribute, enums
end end
@ -239,9 +256,10 @@ function parse_array (schema : json_schema_object, s : st.stanza_t, root : json_
end end
local function parse (schema : json_schema_object, s : st.stanza_t) : table local function parse (schema : json_schema_object, s : st.stanza_t) : table
if schema.type == "object" then local s_type = guess_schema_type(schema)
if s_type == "object" then
return parse_object(schema, s, schema) return parse_object(schema, s, schema)
elseif schema.type == "array" then elseif s_type == "array" then
return parse_array(schema, s, schema) return parse_array(schema, s, schema)
else else
error "top-level scalars unsupported" error "top-level scalars unsupported"
@ -333,7 +351,8 @@ function unparse ( schema : json_schema_object, t : table, current_name : string
local out = ctx or st.stanza(current_name, { xmlns = current_ns }) local out = ctx or st.stanza(current_name, { xmlns = current_ns })
if schema.type == "object" then local s_type = guess_schema_type(schema)
if s_type == "object" then
for prop, propschema in pairs(schema.properties) do for prop, propschema in pairs(schema.properties) do
propschema = resolve_schema(propschema, root) propschema = resolve_schema(propschema, root)
@ -346,7 +365,7 @@ function unparse ( schema : json_schema_object, t : table, current_name : string
end end
return out; return out;
elseif schema.type == "array" then elseif s_type == "array" then
local itemschema = resolve_schema(schema.items, root) local itemschema = resolve_schema(schema.items, root)
local proptype, value_where, name, namespace, prefix, single_attribute = unpack_propschema(itemschema, current_name, current_ns) local proptype, value_where, name, namespace, prefix, single_attribute = unpack_propschema(itemschema, current_name, current_ns)
for _, item in ipairs(t as { string }) do for _, item in ipairs(t as { string }) do

View file

@ -1,3 +1,5 @@
-- This file is generated from teal-src/util/datamapper.lua
local st = require("util.stanza"); local st = require("util.stanza");
local pointer = require("util.jsonpointer"); local pointer = require("util.jsonpointer");
@ -29,15 +31,28 @@ end
local value_goes = {} local value_goes = {}
local function resolve_schema(schema, root) local function resolve_schema(schema, root)
if type(schema) == "table" and schema["$ref"] and schema["$ref"]:sub(1, 1) == "#" then if type(schema) == "table" then
local referenced = pointer.resolve(root, schema["$ref"]:sub(2)); if schema["$ref"] and schema["$ref"]:sub(1, 1) == "#" then
if referenced ~= nil then return pointer.resolve(root, schema["$ref"]:sub(2))
return referenced
end end
end end
return schema return schema
end end
local function guess_schema_type(schema)
local schema_types = schema.type
if type(schema_types) == "string" then
return schema_types
elseif schema_types ~= nil then
error("schema has unsupported 'type' property")
elseif schema.properties then
return "object"
elseif schema.items then
return "array"
end
return "string"
end
local function unpack_propschema(propschema, propname, current_ns) local function unpack_propschema(propschema, propname, current_ns)
local proptype = "string" local proptype = "string"
@ -49,9 +64,9 @@ local function unpack_propschema(propschema, propname, current_ns)
local enums local enums
if type(propschema) == "table" then if type(propschema) == "table" then
proptype = propschema.type proptype = guess_schema_type(propschema);
elseif type(propschema) == "string" then elseif type(propschema) == "string" then
proptype = propschema error("schema as string is not supported: " .. propschema .. " {" .. current_ns .. "}" .. propname)
end end
if proptype == "object" or proptype == "array" then if proptype == "object" or proptype == "array" then
@ -209,9 +224,10 @@ function parse_array(schema, s, root)
end end
local function parse(schema, s) local function parse(schema, s)
if schema.type == "object" then local s_type = guess_schema_type(schema)
if s_type == "object" then
return parse_object(schema, s, schema) return parse_object(schema, s, schema)
elseif schema.type == "array" then elseif s_type == "array" then
return parse_array(schema, s, schema) return parse_array(schema, s, schema)
else else
error("top-level scalars unsupported") error("top-level scalars unsupported")
@ -306,7 +322,8 @@ function unparse(schema, t, current_name, current_ns, ctx, root)
local out = ctx or st.stanza(current_name, {xmlns = current_ns}) local out = ctx or st.stanza(current_name, {xmlns = current_ns})
if schema.type == "object" then local s_type = guess_schema_type(schema)
if s_type == "object" then
for prop, propschema in pairs(schema.properties) do for prop, propschema in pairs(schema.properties) do
propschema = resolve_schema(propschema, root) propschema = resolve_schema(propschema, root)
@ -319,7 +336,7 @@ function unparse(schema, t, current_name, current_ns, ctx, root)
end end
return out return out
elseif schema.type == "array" then elseif s_type == "array" then
local itemschema = resolve_schema(schema.items, root) local itemschema = resolve_schema(schema.items, root)
local proptype, value_where, name, namespace, prefix, single_attribute = unpack_propschema(itemschema, current_name, current_ns) local proptype, value_where, name, namespace, prefix, single_attribute = unpack_propschema(itemschema, current_name, current_ns)
for _, item in ipairs(t) do for _, item in ipairs(t) do