util.datamapper: Improve handling of schemas with non-obvious "type" 0.12
authorKim Alvefur <zash@zash.se>
Fri, 08 Jul 2022 17:32:48 +0200
branch0.12
changeset 12584 a9dbf657c894
parent 12583 ca6a43fe0231
child 12585 6ee9071c0a1f
child 12586 e72bb50456c4
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.
spec/util_datamapper_spec.lua
teal-src/util/datamapper.tl
util/datamapper.lua
--- a/spec/util_datamapper_spec.lua	Fri Jul 08 14:38:23 2022 +0200
+++ b/spec/util_datamapper_spec.lua	Fri Jul 08 17:32:48 2022 +0200
@@ -25,7 +25,7 @@
 				from = attr();
 				type = attr();
 				id = attr();
-				body = "string";
+				body = true; -- should be assumed to be a string
 				lang = {type = "string"; xml = {attribute = true; prefix = "xml"}};
 				delay = {
 					type = "object";
@@ -56,7 +56,8 @@
 					xml = {namespace = "urn:xmpp:reactions:0"; name = "reactions"};
 					properties = {
 						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 = {
@@ -190,7 +191,8 @@
 					version = {
 						type = "object";
 						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 = {}};
 					};
 				};
 			};
--- a/teal-src/util/datamapper.tl	Fri Jul 08 14:38:23 2022 +0200
+++ b/teal-src/util/datamapper.tl	Fri Jul 08 17:32:48 2022 +0200
@@ -25,7 +25,7 @@
 
 local json_type_name = json.json_type_name;
 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
 	if s == "true" or s == "1" then
@@ -59,15 +59,28 @@
 end
 
 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
-		local referenced = pointer.resolve(root as table, schema["$ref"]:sub(2)) as schema_t;
-		if referenced ~= nil then
-			return referenced
+	if schema is json_schema_object then
+		if schema["$ref"] and schema["$ref"]:sub(1, 1) == "#" then
+			return pointer.resolve(root as table, schema["$ref"]:sub(2)) as schema_t;
 		end
 	end
 	return schema;
 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 )
 		: json_type_name, value_goes, string, string, string, string, { any }
 	local proptype : json_type_name = "string"
@@ -79,9 +92,9 @@
 	local enums : { any }
 
 	if propschema is json_schema_object then
-		proptype = propschema.type
-	elseif propschema is json_type_name then
-		proptype = propschema
+		proptype = guess_schema_type(propschema);
+	elseif propschema is string then -- Teal says this can never be a string, but it could before so best be sure
+		error("schema as string is not supported: "..propschema.." {"..current_ns.."}"..propname)
 	end
 
 	if proptype == "object" or proptype == "array" then
@@ -120,6 +133,10 @@
 		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
 end
 
@@ -239,9 +256,10 @@
 end
 
 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)
-	elseif schema.type == "array" then
+	elseif s_type == "array" then
 		return parse_array(schema, s, schema)
 	else
 		error "top-level scalars unsupported"
@@ -333,7 +351,8 @@
 
 	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
 			propschema = resolve_schema(propschema, root)
@@ -346,7 +365,7 @@
 		end
 		return out;
 
-	elseif schema.type == "array" then
+	elseif s_type == "array" then
 		local itemschema = resolve_schema(schema.items, root)
 		local proptype, value_where, name, namespace, prefix, single_attribute = unpack_propschema(itemschema, current_name, current_ns)
 		for _, item in ipairs(t as { string }) do
--- a/util/datamapper.lua	Fri Jul 08 14:38:23 2022 +0200
+++ b/util/datamapper.lua	Fri Jul 08 17:32:48 2022 +0200
@@ -1,3 +1,5 @@
+-- This file is generated from teal-src/util/datamapper.lua
+
 local st = require("util.stanza");
 local pointer = require("util.jsonpointer");
 
@@ -29,15 +31,28 @@
 local value_goes = {}
 
 local function resolve_schema(schema, root)
-	if type(schema) == "table" and schema["$ref"] and schema["$ref"]:sub(1, 1) == "#" then
-		local referenced = pointer.resolve(root, schema["$ref"]:sub(2));
-		if referenced ~= nil then
-			return referenced
+	if type(schema) == "table" then
+		if schema["$ref"] and schema["$ref"]:sub(1, 1) == "#" then
+			return pointer.resolve(root, schema["$ref"]:sub(2))
 		end
 	end
 	return schema
 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 proptype = "string"
@@ -49,9 +64,9 @@
 	local enums
 
 	if type(propschema) == "table" then
-		proptype = propschema.type
+		proptype = guess_schema_type(propschema);
 	elseif type(propschema) == "string" then
-		proptype = propschema
+		error("schema as string is not supported: " .. propschema .. " {" .. current_ns .. "}" .. propname)
 	end
 
 	if proptype == "object" or proptype == "array" then
@@ -209,9 +224,10 @@
 end
 
 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)
-	elseif schema.type == "array" then
+	elseif s_type == "array" then
 		return parse_array(schema, s, schema)
 	else
 		error("top-level scalars unsupported")
@@ -306,7 +322,8 @@
 
 	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
 			propschema = resolve_schema(propschema, root)
@@ -319,7 +336,7 @@
 		end
 		return out
 
-	elseif schema.type == "array" then
+	elseif s_type == "array" then
 		local itemschema = resolve_schema(schema.items, root)
 		local proptype, value_where, name, namespace, prefix, single_attribute = unpack_propschema(itemschema, current_name, current_ns)
 		for _, item in ipairs(t) do