teal-src/prosody/util/jsonschema.tl
changeset 12983 fbbf4f0db8f0
parent 12786 8815d3090928
child 12992 8592770be63a
equal deleted inserted replaced
12982:088d278c75b5 12983:fbbf4f0db8f0
       
     1 -- Copyright (C) 2021 Kim Alvefur
       
     2 --
       
     3 -- This project is MIT/X11 licensed. Please see the
       
     4 -- COPYING file in the source package for more information.
       
     5 --
       
     6 -- Based on
       
     7 -- https://json-schema.org/draft/2020-12/json-schema-core.html
       
     8 -- https://json-schema.org/draft/2020-12/json-schema-validation.html
       
     9 --
       
    10 
       
    11 if not math.type then require "prosody.util.mathcompat" end
       
    12 
       
    13 local json = require "prosody.util.json"
       
    14 local null = json.null;
       
    15 
       
    16 local pointer = require "prosody.util.jsonpointer"
       
    17 
       
    18 local type json_type_name = json.json_type_name
       
    19 
       
    20 -- json_type_name here is non-standard
       
    21 local type schema_t = boolean | json_schema_object
       
    22 
       
    23 local record json_schema_object
       
    24 	type json_type_name = json.json_type_name
       
    25 	type schema_object = json_schema_object
       
    26 
       
    27 	type : json_type_name | { json_type_name }
       
    28 	enum : { any }
       
    29 	const : any
       
    30 
       
    31 	allOf : { schema_t }
       
    32 	anyOf : { schema_t }
       
    33 	oneOf : { schema_t }
       
    34 
       
    35 	["not"] : schema_t
       
    36 	["if"] : schema_t
       
    37 	["then"] : schema_t
       
    38 	["else"] : schema_t
       
    39 
       
    40 	["$ref"] : string
       
    41 
       
    42 	-- numbers
       
    43 	multipleOf : number
       
    44 	maximum : number
       
    45 	exclusiveMaximum : number
       
    46 	minimum : number
       
    47 	exclusiveMinimum : number
       
    48 
       
    49 	-- strings
       
    50 	maxLength : integer
       
    51 	minLength : integer
       
    52 	pattern : string -- NYI
       
    53 	format : string
       
    54 
       
    55 	-- arrays
       
    56 	prefixItems : { schema_t }
       
    57 	items : schema_t
       
    58 	contains : schema_t
       
    59 	maxItems : integer
       
    60 	minItems : integer
       
    61 	uniqueItems : boolean
       
    62 	maxContains : integer -- NYI
       
    63 	minContains : integer -- NYI
       
    64 
       
    65 	-- objects
       
    66 	properties : { string : schema_t }
       
    67 	maxProperties : integer -- NYI
       
    68 	minProperties : integer -- NYI
       
    69 	required : { string }
       
    70 	dependentRequired : { string : { string } }
       
    71 	additionalProperties: schema_t
       
    72 	patternProperties: schema_t -- NYI
       
    73 	propertyNames : schema_t
       
    74 
       
    75 	-- xml
       
    76 	record xml_t
       
    77 		name : string
       
    78 		namespace : string
       
    79 		prefix : string
       
    80 		attribute : boolean
       
    81 		wrapped : boolean
       
    82 
       
    83 		-- nonstantard, maybe in the future
       
    84 		text : boolean
       
    85 		x_name_is_value : boolean
       
    86 		x_single_attribute : string
       
    87 	end
       
    88 
       
    89 	xml : xml_t
       
    90 
       
    91 	-- descriptive
       
    92 	title : string
       
    93 	description : string
       
    94 	deprecated : boolean
       
    95 	readOnly : boolean
       
    96 	writeOnly : boolean
       
    97 
       
    98 	-- methods
       
    99 	validate : function ( schema_t, any, json_schema_object ) : boolean
       
   100 end
       
   101 
       
   102 -- TODO validator function per schema property
       
   103 
       
   104 local function simple_validate(schema : json_type_name | { json_type_name }, data : any) : boolean
       
   105 	if schema == nil then
       
   106 		return true
       
   107 	elseif schema == "object" and data is table then
       
   108 		return type(data) == "table" and (next(data)==nil or type((next(data, nil))) == "string")
       
   109 	elseif schema == "array" and data is table then
       
   110 		return type(data) == "table" and (next(data)==nil or type((next(data, nil))) == "number")
       
   111 	elseif schema == "integer" then
       
   112 		return math.type(data) == schema
       
   113 	elseif schema == "null" then
       
   114 		return data == null
       
   115 	elseif schema is { json_type_name } then
       
   116 		for _, one in ipairs(schema as { json_type_name }) do
       
   117 			if simple_validate(one, data) then
       
   118 				return true
       
   119 			end
       
   120 		end
       
   121 		return false
       
   122 	else
       
   123 		return type(data) == schema
       
   124 	end
       
   125 end
       
   126 
       
   127 local complex_validate : function ( json_schema_object, any, json_schema_object ) : boolean
       
   128 
       
   129 local function validate (schema : schema_t, data : any, root : json_schema_object) : boolean
       
   130 	if schema is boolean then
       
   131 		return schema
       
   132 	else
       
   133 		return complex_validate(schema, data, root)
       
   134 	end
       
   135 end
       
   136 
       
   137 function complex_validate (schema : json_schema_object, data : any, root : json_schema_object) : boolean
       
   138 
       
   139 	if root == nil then
       
   140 		root = schema
       
   141 	end
       
   142 
       
   143 	if schema["$ref"] and schema["$ref"]:sub(1,1) == "#" then
       
   144 		local referenced = pointer.resolve(root as table, schema["$ref"]:sub(2)) as schema_t
       
   145 		if referenced ~= nil and referenced ~= root and referenced ~= schema then
       
   146 			if not validate(referenced, data, root) then
       
   147 				return false;
       
   148 			end
       
   149 		end
       
   150 	end
       
   151 
       
   152 	if not simple_validate(schema.type, data) then
       
   153 		return false;
       
   154 	end
       
   155 
       
   156 	if schema.type == "object" then
       
   157 		if data is table then
       
   158 			-- just check that there the keys are all strings
       
   159 			for k in pairs(data) do
       
   160 				if not k is string then
       
   161 					return false
       
   162 				end
       
   163 			end
       
   164 		end
       
   165 	end
       
   166 
       
   167 	if schema.type == "array" then
       
   168 		if data is table then
       
   169 			-- just check that there the keys are all numbers
       
   170 			for i in pairs(data) do
       
   171 				if not i is integer then
       
   172 					return false
       
   173 				end
       
   174 			end
       
   175 		end
       
   176 	end
       
   177 
       
   178 	if schema["enum"] ~= nil then
       
   179 		local match = false
       
   180 		for _, v in ipairs(schema["enum"]) do
       
   181 			if v == data then
       
   182 				-- FIXME supposed to do deep-compare
       
   183 				match = true
       
   184 				break
       
   185 			end
       
   186 		end
       
   187 		if not match then
       
   188 			return false
       
   189 		end
       
   190 	end
       
   191 
       
   192 	-- XXX this is measured in byte, while JSON measures in ... bork
       
   193 	-- TODO use utf8.len?
       
   194 	if data is string then
       
   195 		if schema.maxLength and #data > schema.maxLength then
       
   196 			return false
       
   197 		end
       
   198 		if schema.minLength and #data < schema.minLength then
       
   199 			return false
       
   200 		end
       
   201 	end
       
   202 
       
   203 	if data is number then
       
   204 		if schema.multipleOf and (data == 0 or data % schema.multipleOf ~= 0) then
       
   205 			return false
       
   206 		end
       
   207 
       
   208 		if schema.maximum and not ( data <= schema.maximum ) then
       
   209 			return false
       
   210 		end
       
   211 
       
   212 		if schema.exclusiveMaximum and not ( data < schema.exclusiveMaximum ) then
       
   213 			return false
       
   214 		end
       
   215 
       
   216 		if schema.minimum and not ( data >= schema.minimum ) then
       
   217 			return false
       
   218 		end
       
   219 
       
   220 		if schema.exclusiveMinimum and not ( data > schema.exclusiveMinimum ) then
       
   221 			return false
       
   222 		end
       
   223 	end
       
   224 
       
   225 	if schema.allOf then
       
   226 		for _, sub in ipairs(schema.allOf) do
       
   227 			if not validate(sub, data, root) then
       
   228 				return false
       
   229 			end
       
   230 		end
       
   231 	end
       
   232 
       
   233 	if schema.oneOf then
       
   234 		local valid = 0
       
   235 		for _, sub in ipairs(schema.oneOf) do
       
   236 			if validate(sub, data, root) then
       
   237 				valid = valid + 1
       
   238 			end
       
   239 		end
       
   240 		if valid ~= 1 then
       
   241 			return false
       
   242 		end
       
   243 	end
       
   244 
       
   245 	if schema.anyOf then
       
   246 		local match = false
       
   247 		for _, sub in ipairs(schema.anyOf) do
       
   248 			if validate(sub, data, root) then
       
   249 				match = true
       
   250 				break
       
   251 			end
       
   252 		end
       
   253 		if not match then
       
   254 			return false
       
   255 		end
       
   256 	end
       
   257 
       
   258 	if schema["not"] then
       
   259 		if validate(schema["not"], data, root) then
       
   260 			return false
       
   261 		end
       
   262 	end
       
   263 
       
   264 	if schema["if"] ~= nil then
       
   265 		if validate(schema["if"], data, root) then
       
   266 			if schema["then"] then
       
   267 				return validate(schema["then"], data, root)
       
   268 			end
       
   269 		else
       
   270 			if schema["else"] then
       
   271 				return validate(schema["else"], data, root)
       
   272 			end
       
   273 		end
       
   274 	end
       
   275 
       
   276 	if schema.const ~= nil and schema.const ~= data then
       
   277 		return false
       
   278 	end
       
   279 
       
   280 	if data is table then
       
   281 
       
   282 		if schema.maxItems and #data > schema.maxItems then
       
   283 			return false
       
   284 		end
       
   285 
       
   286 		if schema.minItems and #data < schema.minItems then
       
   287 			return false
       
   288 		end
       
   289 
       
   290 		if schema.required then
       
   291 			for _, k in ipairs(schema.required) do
       
   292 				if data[k] == nil then
       
   293 					return false
       
   294 				end
       
   295 			end
       
   296 		end
       
   297 
       
   298 		if schema.propertyNames ~= nil then
       
   299 			for k in pairs(data) do
       
   300 				if not validate(schema.propertyNames, k, root) then
       
   301 					return false
       
   302 				end
       
   303 			end
       
   304 		end
       
   305 
       
   306 		if schema.properties then
       
   307 			for k, sub in pairs(schema.properties) do
       
   308 				if data[k] ~= nil and not validate(sub, data[k], root) then
       
   309 					return false
       
   310 				end
       
   311 			end
       
   312 		end
       
   313 
       
   314 		if schema.additionalProperties ~= nil then
       
   315 			for k, v in pairs(data) do
       
   316 				if schema.properties == nil or schema.properties[k as string] == nil then
       
   317 					if not validate(schema.additionalProperties, v, root) then
       
   318 						return false
       
   319 					end
       
   320 				end
       
   321 			end
       
   322 		end
       
   323 
       
   324 		if schema.uniqueItems then
       
   325 			-- only works for scalars, would need to deep-compare for objects/arrays/tables
       
   326 			local values : { any : boolean } = {}
       
   327 			for _, v in pairs(data) do
       
   328 				if values[v] then
       
   329 					return false
       
   330 				end
       
   331 				values[v] = true
       
   332 			end
       
   333 		end
       
   334 
       
   335 		local p = 0
       
   336 		if schema.prefixItems ~= nil then
       
   337 			for i, s in ipairs(schema.prefixItems) do
       
   338 				if data[i] == nil then
       
   339 					break
       
   340 				elseif validate(s, data[i], root) then
       
   341 					p = i
       
   342 				else
       
   343 					return false
       
   344 				end
       
   345 			end
       
   346 		end
       
   347 
       
   348 		if schema.items ~= nil then
       
   349 			for i = p+1, #data do
       
   350 				if not validate(schema.items, data[i], root) then
       
   351 					return false
       
   352 				end
       
   353 			end
       
   354 		end
       
   355 
       
   356 		if schema.contains ~= nil then
       
   357 			local found = false
       
   358 			for i = 1, #data do
       
   359 				if validate(schema.contains, data[i], root) then
       
   360 					found = true
       
   361 					break
       
   362 				end
       
   363 			end
       
   364 			if not found then
       
   365 				return false
       
   366 			end
       
   367 		end
       
   368 	end
       
   369 
       
   370 	return true;
       
   371 end
       
   372 
       
   373 
       
   374 json_schema_object.validate = validate;
       
   375 
       
   376 return json_schema_object;