Module:Util/schema/validate: Difference between revisions

From Zelda Wiki, the Zelda encyclopedia
Jump to navigation Jump to search
No edit summary
No edit summary
Line 1: Line 1:
local h = {}
-- I apologize for how big a mess this is.


local utilsError = require("Module:UtilsError")
local utilsError = require("Module:UtilsError")
Line 111: Line 111:
end
end


local function validate(schema, schemaName, data, dataName)
local function luaType(schemaType)
-- first validate the schema itself
if schemaType == TYPES.array
collectReferences(metaSchema)
or schemaType == TYPES.record
local schemaErrors = h.validate(metaSchema.values, schema, schemaName)
or schemaType == TYPES.map
if #schemaErrors > 0 then
then
utilsError.warn(string.format("Schema <code>%s</code> is invalid.", schemaName))
return "table"
end
return schemaType
end
local function errorCollector(errorTbl, value, dataName, dataPath, quiet, isKey)
return function(validator)
local errorMessages = validator(value, dataName, dataPath, isKey, {
quiet = quiet,
stackTrace = false,
})
if type(errorMessages) ~= "table" then --errMsg can either be a single message, or an array of messages (as is the case with enum)
errorMessages = {errorMessages}
end
for _, errMsg in ipairs(errorMessages) do
table.insert(errorTbl, {
path = dataName .. utilsTable.printPath(dataPath),
msg = errMsg,
})
end
end
end
local function validatePrimitive(schemaNode, value, dataName, dataPath, quiet, isKey)
local errors = {}
local addIfError = errorCollector(errors, value, dataName, dataPath, quiet, isKey)
local validatorOptions = {
quiet = quiet,
stackTrace = false,
}
if schemaNode.type and schemaNode.type ~= "any" then
local expectedType = luaType(schemaNode.type)
addIfError(utilsValidate.type(expectedType))
end
if schemaNode.required then
addIfError(utilsValidate.required)
end
if schemaNode.deprecated then
addIfError(utilsValidate.deprecated)
end
if schemaNode.enum then
addIfError(utilsValidate.enum(schemaNode.enum))
end
return errors
end
 
local function logSubschemaErrors(subschemaErrors, path)
for schemaKey, schemaErrors in pairs(subschemaErrors) do
local subpath = utilsTable.concat(path or {}, '["' .. schemaKey .. '"]')
local indent = string.rep(":", #subpath)
for _, err in pairs(schemaErrors) do
local msg = string.format("<code>%s</code>: %s", utilsTable.printPath(subpath), err.msg)
utilsError.warn(indent .. msg, { traceBack = false })
if err.errors then
logSubschemaErrors(err.errors, subpath)
end
end
end
end
-- then validate the data
end
collectReferences(schema)
 
local errors = h.validate(schema, data, dataName)
local function validateSubschemas(subschemas, data, dataName, dataPath, schemaNode, parentSchema)
if #errors > 0 then
local errors = {}
utilsError.warn(string.format("<code>%s</code> is invalid according to schema <code>%s</code>", dataName, schemaName))
local valids = {}
return errors
for k, subschema in pairs(subschemas) do
local key = subschema._ref and string.gsub(subschema._ref, "#/definitions/", "") or k
if parentSchema and parentSchema.allOf then
local commonProps = {}
for _, v in ipairs(parentSchema.allOf) do
commonProps = utilsTable.concat(commonProps, v.properties)
end
schemaNode.allOfProps = commonProps
end
local err = doValidate(subschema, data, dataName, dataPath, schemaNode, true)
if #err > 0 then
errors[key] = err
else
table.insert(valids, key)
end
end
end
return errors, valids
end
end


function h.validate(schemaNode, data, dataName, dataPath, parentSchema, quiet)
function doValidate(schemaNode, data, dataName, dataPath, parentSchema, quiet)
dataPath = dataPath or {}
dataPath = dataPath or {}
local value = extract(data, dataPath)
local value = extract(data, dataPath)
Line 133: Line 202:
schemaNode = resolveReference(schemaNode)
schemaNode = resolveReference(schemaNode)
local errors = h.validatePrimitive(schemaNode, value, dataName, dataPath, quiet)
local errors = validatePrimitive(schemaNode, value, dataName, dataPath, quiet)
if #errors > 0 then
if #errors > 0 then
return errors
return errors
Line 139: Line 208:
if schemaNode.allOf and value then
if schemaNode.allOf and value then
local subschemaErrors = h.validateSubschemas(schemaNode.allOf, data, dataName, dataPath, schemaNode)
local subschemaErrors = validateSubschemas(schemaNode.allOf, data, dataName, dataPath, schemaNode)
local invalidSubschemas = utilsTable.keys(subschemaErrors)
local invalidSubschemas = utilsTable.keys(subschemaErrors)
if #invalidSubschemas > 0 then
if #invalidSubschemas > 0 then
Line 147: Line 216:
if not quiet then
if not quiet then
utilsError.warn(msg, { traceBack = false })
utilsError.warn(msg, { traceBack = false })
h.logSubschemaErrors(subschemaErrors)
logSubschemaErrors(subschemaErrors)
end
end
table.insert(errors, {
table.insert(errors, {
Line 157: Line 226:
end
end
if schemaNode.oneOf and value then
if schemaNode.oneOf and value then
local subschemaErrors, validSubschemas = h.validateSubschemas(schemaNode.oneOf, data, dataName, dataPath, schemaNode, parentSchema)
local subschemaErrors, validSubschemas = validateSubschemas(schemaNode.oneOf, data, dataName, dataPath, schemaNode, parentSchema)
local invalidSubschemas = utilsTable.keys(subschemaErrors)
local invalidSubschemas = utilsTable.keys(subschemaErrors)
if #validSubschemas == 0 then
if #validSubschemas == 0 then
Line 163: Line 232:
if not quiet then
if not quiet then
utilsError.warn(msg, { traceBack = false })
utilsError.warn(msg, { traceBack = false })
h.logSubschemaErrors(subschemaErrors)
logSubschemaErrors(subschemaErrors)
end
end
table.insert(errors, {
table.insert(errors, {
Line 188: Line 257:
for _, propSchema in pairs(schemaNode.properties) do
for _, propSchema in pairs(schemaNode.properties) do
local keyPath = utilsTable.concat(dataPath, propSchema.name)
local keyPath = utilsTable.concat(dataPath, propSchema.name)
errors = utilsTable.concat(errors, h.validate(propSchema, data, dataName, keyPath, schemaNode, quiet))
errors = utilsTable.concat(errors, doValidate(propSchema, data, dataName, keyPath, schemaNode, quiet))
end
end
if not schemaNode.additionalProperties and not (parentSchema and parentSchema.allOf) then
if not schemaNode.additionalProperties and not (parentSchema and parentSchema.allOf) then
Line 225: Line 294:
for i, item in ipairs(value) do
for i, item in ipairs(value) do
local itemPath = utilsTable.concat(dataPath, i)
local itemPath = utilsTable.concat(dataPath, i)
errors = utilsTable.concat(errors, h.validate(schemaNode.items, data, dataName, itemPath, schemaNode, quiet))
errors = utilsTable.concat(errors, doValidate(schemaNode.items, data, dataName, itemPath, schemaNode, quiet))
end
end
end
end
Line 231: Line 300:
for k, v in pairs(value) do
for k, v in pairs(value) do
local keyPath = utilsTable.concat(dataPath, string.format('["%s"]', k))
local keyPath = utilsTable.concat(dataPath, string.format('["%s"]', k))
errors = utilsTable.concat(errors, h.validatePrimitive(schemaNode.keys, k, dataName, dataPath, quiet, true))
errors = utilsTable.concat(errors, validatePrimitive(schemaNode.keys, k, dataName, dataPath, quiet, true))
errors = utilsTable.concat(errors, h.validate(schemaNode.values, data, dataName, keyPath, schemaNode, quiet))
errors = utilsTable.concat(errors, doValidate(schemaNode.values, data, dataName, keyPath, schemaNode, quiet))
end
end
end
end
Line 239: Line 308:
end
end


function h.validateSubschemas(subschemas, data, dataName, dataPath, schemaNode, parentSchema)
local function validate(schema, schemaName, data, dataName)
local errors = {}
-- first validate the schema itself
local valids = {}
collectReferences(metaSchema)
for k, subschema in pairs(subschemas) do
local schemaErrors = doValidate(metaSchema.values, schema, schemaName)
local key = subschema._ref and string.gsub(subschema._ref, "#/definitions/", "") or k
if #schemaErrors > 0 then
if parentSchema and parentSchema.allOf then
utilsError.warn(string.format("Schema <code>%s</code> is invalid.", schemaName))
local commonProps = {}
for _, v in ipairs(parentSchema.allOf) do
commonProps = utilsTable.concat(commonProps, v.properties)
end
schemaNode.allOfProps = commonProps
end
local err = h.validate(subschema, data, dataName, dataPath, schemaNode, true)
if #err > 0 then
errors[key] = err
else
table.insert(valids, key)
end
end
end
return errors, valids
-- then validate the data
end
collectReferences(schema)
function h.logSubschemaErrors(subschemaErrors, path)
local errors = doValidate(schema, data, dataName)
for schemaKey, schemaErrors in pairs(subschemaErrors) do
if #errors > 0 then
local subpath = utilsTable.concat(path or {}, '["' .. schemaKey .. '"]')
utilsError.warn(string.format("<code>%s</code> is invalid according to schema <code>%s</code>", dataName, schemaName))
local indent = string.rep(":", #subpath)
return errors
for _, err in pairs(schemaErrors) do
local msg = string.format("<code>%s</code>: %s", utilsTable.printPath(subpath), err.msg)
utilsError.warn(indent .. msg, { traceBack = false })
if err.errors then
h.logSubschemaErrors(err.errors, subpath)
end
end
end
end
 
function h.validatePrimitive(schemaNode, value, dataName, dataPath, quiet, isKey)
local errors = {}
local addIfError = h.errorCollector(errors, value, dataName, dataPath, quiet, isKey)
local validatorOptions = {
quiet = quiet,
stackTrace = false,
}
if schemaNode.type and schemaNode.type ~= "any" then
local expectedType = h.getLuaType(schemaNode.type)
addIfError(utilsValidate.type(expectedType))
end
end
if schemaNode.required then
addIfError(utilsValidate.required)
end
if schemaNode.deprecated then
addIfError(utilsValidate.deprecated)
end
if schemaNode.enum then
addIfError(utilsValidate.enum(schemaNode.enum))
end
return errors
end
function h.errorCollector(errorTbl, value, dataName, dataPath, quiet, isKey)
return function(validator)
local errorMessages = validator(value, dataName, dataPath, isKey, {
quiet = quiet,
stackTrace = false,
})
if type(errorMessages) ~= "table" then --errMsg can either be a single message, or an array of messages (as is the case with enum)
errorMessages = {errorMessages}
end
for _, errMsg in ipairs(errorMessages) do
table.insert(errorTbl, {
path = dataName .. utilsTable.printPath(dataPath),
msg = errMsg,
})
end
end
end
function h.getLuaType(schemaType)
if schemaType == TYPES.array
or schemaType == TYPES.record
or schemaType == TYPES.map
then
return "table"
end
return schemaType
end
end


return validate
return validate

Revision as of 20:47, 12 May 2024

Lua error in package.lua at line 80: module 'Module:Util/schemas/validate/Documentation/Spec' not found.


-- I apologize for how big a mess this is.

local utilsError = require("Module:UtilsError")
local utilsTable = require("Module:UtilsTable")
local utilsValidate = require("Module:UtilsValidate")
local util = {
	markup = {
		code = require("Module:Util/markup/code")
	},
	strings = {
		trim = require("Module:Util/strings/trim")
	}
}

local metaSchema = require("Module:Util/schemas/validate/Documentation/Spec").Schemas()

local SYMBOLS = {
	optional = "[%s]",
	required = "%s!",
	default = "%s=%s",
	array = "{%s}",
	map = "<%s, %s>",
	oneOf = "%s|%s",
	allOf = "%s&%s",
	combinationGroup = "(%s)",
}

local TYPES = {
	oneOf = "oneOf",
	array = "array",
	record = "record",
	map = "map"
}

-- Extracts a value from nested tables given a path to the value as an array of successive keys
local function extract(tbl, path)
	local result = tbl
	for i, key in ipairs(path) do
		result = result[key] or result[util.strings.trim(key, '%[%]"')]
	end
	return result
end

local function walkSchema(fn, schemaNode, path)
	path = path or {}
	local continue = fn(schemaNode, path)
	if continue == false then
		return
	end
	if schemaNode.items then
		walkSchema(fn, schemaNode.items, utilsTable.concat(path, "items"))
	end
	if schemaNode.keys then
		walkSchema(fn, schemaNode.keys, utilsTable.concat(path, "keys"))
	end
	if schemaNode.values then
		walkSchema(fn, schemaNode.values, utilsTable.concat(path, "values"))
	end
	if schemaNode.properties then
		for i, v in ipairs(schemaNode.properties) do
			local keyPath = utilsTable.concat(path, "properties", v.name)
			walkSchema(fn, v, keyPath)
		end
	end
	if schemaNode.oneOf then
		for k, v in pairs(schemaNode.oneOf) do
			walkSchema(fn, v, utilsTable.concat(path, "oneOf"))
		end
	end
	if schemaNode.allOf then
		for i, v in ipairs(schemaNode.allOf) do
			walkSchema(fn, v, utilsTable.concat(path, "allOf"))
		end
	end
	if schemaNode.definitions then
		for k, v in pairs(schemaNode.definitions) do
			walkSchema(fn, v, utilsTable.concat(path, "definitions", k))
		end
	end
end

local function collectIdReferences(schema)
	walkSchema(function(schemaNode)
		if schemaNode._id then
			references[schemaNode._id] = schemaNode
		end
	end, schema)
end
local function collectReferences(schema)
	references = {} -- use of global variable here not ideal
	references["#"] = schema
	for k, v in pairs(schema.definitions or {}) do
		references["#/definitions/" .. k] = v
		collectIdReferences(v)
	end
	collectIdReferences(schema)
end

function resolveReference(schemaNode)
	if schemaNode._ref then
		local referenceNode = references[schemaNode._ref]
		if not referenceNode then
			mw.addWarning(string.format("<code>%s</code> not found", mw.text.nowiki(schemaNode._ref)))
		else
			local resolvedSchema = utilsTable.merge({}, references[schemaNode._ref], schemaNode)
			schemaNode = utilsTable.merge({}, schemaNode, resolvedSchema)
			schemaNode._ref = nil
		end
	end
	return schemaNode
end

local function luaType(schemaType)
	if schemaType == TYPES.array 
	or schemaType == TYPES.record 
	or schemaType == TYPES.map 
	then
		return "table"
	end
	return schemaType
end
local function errorCollector(errorTbl, value, dataName, dataPath, quiet, isKey) 
	return function(validator)
		local errorMessages = validator(value, dataName, dataPath, isKey, {
			quiet = quiet,
			stackTrace = false,
		})
		if type(errorMessages) ~= "table" then --errMsg can either be a single message, or an array of messages (as is the case with enum)
			errorMessages = {errorMessages}
		end
		for _, errMsg in ipairs(errorMessages) do
			table.insert(errorTbl, {
				path = dataName .. utilsTable.printPath(dataPath),
				msg = errMsg,
			})
		end
	end
end
local function validatePrimitive(schemaNode, value, dataName, dataPath, quiet, isKey)
	local errors = {}
	local addIfError = errorCollector(errors, value, dataName, dataPath, quiet, isKey)
	local validatorOptions = {
		quiet = quiet,
		stackTrace = false,
	}
	if schemaNode.type and schemaNode.type ~= "any" then
		local expectedType = luaType(schemaNode.type)
		addIfError(utilsValidate.type(expectedType))
	end
	if schemaNode.required then
		addIfError(utilsValidate.required)
	end
	if schemaNode.deprecated then
		addIfError(utilsValidate.deprecated)
	end
	if schemaNode.enum then
		addIfError(utilsValidate.enum(schemaNode.enum))
	end
	return errors
end

local function logSubschemaErrors(subschemaErrors, path)
	for schemaKey, schemaErrors in pairs(subschemaErrors) do
		local subpath = utilsTable.concat(path or {}, '["' .. schemaKey .. '"]')
		local indent = string.rep(":", #subpath)
		for _, err in pairs(schemaErrors) do
			local msg = string.format("<code>%s</code>: %s", utilsTable.printPath(subpath), err.msg)
			utilsError.warn(indent .. msg, { traceBack = false })
			if err.errors then
				logSubschemaErrors(err.errors, subpath)
			end
		end
	end
end

local function validateSubschemas(subschemas, data, dataName, dataPath, schemaNode, parentSchema)
	local errors = {}
	local valids = {}
	for k, subschema in pairs(subschemas) do
		local key = subschema._ref and string.gsub(subschema._ref, "#/definitions/", "") or k
		if parentSchema and parentSchema.allOf then
			local commonProps = {}
			for _, v in ipairs(parentSchema.allOf) do
				commonProps = utilsTable.concat(commonProps, v.properties)
			end
			schemaNode.allOfProps = commonProps
		end
		local err = doValidate(subschema, data, dataName, dataPath, schemaNode, true)
		if #err > 0 then
			errors[key] = err
		else
			table.insert(valids, key)
		end
	end
	return errors, valids
end

function doValidate(schemaNode, data, dataName, dataPath, parentSchema, quiet)
	dataPath = dataPath or {}
	local value = extract(data, dataPath)
	local errPath = dataName .. utilsTable.printPath(dataPath)
	schemaNode = resolveReference(schemaNode)
	
	local errors = validatePrimitive(schemaNode, value, dataName, dataPath, quiet)
	if #errors > 0 then
		return errors
	end
	
	if schemaNode.allOf and value then
		local subschemaErrors = validateSubschemas(schemaNode.allOf, data, dataName, dataPath, schemaNode)
		local invalidSubschemas = utilsTable.keys(subschemaErrors)
		if #invalidSubschemas > 0 then
			invalidSubschemas = utilsTable.map(invalidSubschemas, util.markup.code)
			invalidSubschemas = mw.text.listToText(invalidSubschemas)
			local msg = string.format("<code>%s</code> does not match <code>allOf</code> sub-schemas %s", errPath, invalidSubschemas)
			if not quiet then
				utilsError.warn(msg, { traceBack = false })
				logSubschemaErrors(subschemaErrors)
			end
			table.insert(errors, {
				path = errPath,
				msg = msg,
				errors = subschemaErrors
			})
		end
	end
	if schemaNode.oneOf and value then
		local subschemaErrors, validSubschemas = validateSubschemas(schemaNode.oneOf, data, dataName, dataPath, schemaNode, parentSchema)
		local invalidSubschemas = utilsTable.keys(subschemaErrors)
		if #validSubschemas == 0 then
			local msg = string.format("<code>%s</code> does not match any <code>oneOf</code> sub-schemas.", errPath)
			if not quiet then
				utilsError.warn(msg, { traceBack = false })
				logSubschemaErrors(subschemaErrors)
			end
			table.insert(errors, {
				path = errPath,
				msg = msg,
				errors = subschemaErrors
			})
		end
		if #validSubschemas > 1 then
			validSubschemas = utilsTable.map(validSubschemas, util.markup.code)
			validSubschemas = mw.text.listToText(validSubschemas)
			local msg = string.format("<code>%s</code> matches <code>oneOf</code> sub-schemas %s, but must match only one.", errPath, validSubschemas)
			if not quiet then
				utilsError.warn(msg, { traceBack = false })
			end
			table.insert(errors, {
				path = errPath,
				msg = msg,
			})
		end
	end
	
	if schemaNode.properties and value then
		for _, propSchema in pairs(schemaNode.properties) do
			local keyPath = utilsTable.concat(dataPath, propSchema.name)
			errors = utilsTable.concat(errors, doValidate(propSchema, data, dataName, keyPath, schemaNode, quiet))
		end
		if not schemaNode.additionalProperties and not (parentSchema and parentSchema.allOf) then
			local schemaProps = schemaNode.properties
			if parentSchema and parentSchema.allOfProps then
				schemaProps = utilsTable.concat(schemaProps, parentSchema.allOfProps)
			end
			local schemaProps = utilsTable.map(schemaProps, "name")
			local dataProps = utilsTable.keys(value)
			local undefinedProps = utilsTable.difference(dataProps, schemaProps)
			if #undefinedProps > 0 then
				undefinedProps = mw.text.listToText(utilsTable.map(undefinedProps, util.markup.code))
				local msg = string.format("Record <code>%s</code> has undefined properties: %s", errPath, undefinedProps)
				if not quiet then
					utilsError.warn(msg, { traceBack = false })
				end
				table.insert(errors, {
					path = errPath,
					msg = msg,
				})
			end
		end
	end
	if schemaNode.items and value then
		local props = utilsTable.stringKeys(value)
		if #props > 0 and not (parentSchema and parentSchema.allOf) then
			local msg = string.format("<code>%s</code> is supposed to be an array only, but it has string keys: %s", errPath, utilsTable.print(props))
			if not quiet then
				utilsError.warn(msg, { traceBack = false })
			end
			table.insert(errors, {
				path = errPath,
				msg = msg,
			})
		end
		for i, item in ipairs(value) do
			local itemPath = utilsTable.concat(dataPath, i)
			errors = utilsTable.concat(errors, doValidate(schemaNode.items, data, dataName, itemPath, schemaNode, quiet))
		end
	end
	if schemaNode.keys and schemaNode.values and value then
		for k, v in pairs(value) do
			local keyPath = utilsTable.concat(dataPath, string.format('["%s"]', k))
			errors = utilsTable.concat(errors, validatePrimitive(schemaNode.keys, k, dataName, dataPath, quiet, true))
			errors = utilsTable.concat(errors, doValidate(schemaNode.values, data, dataName, keyPath, schemaNode, quiet))
		end
	end
	
	return errors
end

local function validate(schema, schemaName, data, dataName)
	-- first validate the schema itself
	collectReferences(metaSchema)
	local schemaErrors = doValidate(metaSchema.values, schema, schemaName)
	if #schemaErrors > 0 then
		utilsError.warn(string.format("Schema <code>%s</code> is invalid.", schemaName))
	end
	-- then validate the data
	collectReferences(schema)
	local errors = doValidate(schema, data, dataName)
	if #errors > 0 then
		utilsError.warn(string.format("<code>%s</code> is invalid according to schema <code>%s</code>", dataName, schemaName))
		return errors
	end
end

return validate