Zelda Wiki

Want to contribute to this wiki?
Sign up for an account, and get started!

Come join the Zelda Wiki community Discord server!

READ MORE

Zelda Wiki
No edit summary
No edit summary
(44 intermediate revisions by the same user not shown)
Line 2: Line 2:
 
local h = {}
 
local h = {}
   
local utilsArg = require("Module:UtilsArg")
 
 
local utilsError = require("Module:UtilsError")
 
local utilsError = require("Module:UtilsError")
 
local utilsString = require("Module:UtilsString")
 
local utilsString = require("Module:UtilsString")
 
local utilsTable = require("Module:UtilsTable")
 
local utilsTable = require("Module:UtilsTable")
  +
local utilsValidate = require("Module:UtilsValidate")
   
 
local SYMBOLS = {
 
local SYMBOLS = {
Line 13: Line 13:
 
array = "{%s}",
 
array = "{%s}",
 
map = "<%s, %s>",
 
map = "<%s, %s>",
oneOf = "%s | %s",
+
oneOf = "%s|%s",
allOf = "%s & %s",
+
allOf = "%s&%s",
 
combinationGroup = "(%s)",
 
combinationGroup = "(%s)",
 
}
 
}
Line 25: Line 25:
 
}
 
}
   
  +
local function code(s)
function p.validate(schemas, schemaName, data, dataName)
 
  +
return string.format("<code>%s</code>", s)
  +
end
  +
  +
function p.validate(schema, schemaName, data, dataName)
 
-- first validate the schema itself
 
-- first validate the schema itself
 
h.collectReferences(p.Schemas["Schema"])
 
h.collectReferences(p.Schemas["Schema"])
local schemaErrors = h.validate(p.Schemas["Schema"], schemas[schemaName], schemaName)
+
local schemaErrors = h.validate(p.Schemas["Schema"], schema, schemaName)
 
if #schemaErrors > 0 then
 
if #schemaErrors > 0 then
 
utilsError.warn(string.format("Schema <code>%s</code> is invalid.", schemaName))
 
utilsError.warn(string.format("Schema <code>%s</code> is invalid.", schemaName))
 
end
 
end
 
-- then validate the data
 
-- then validate the data
h.collectReferences(schemas[schemaName])
+
h.collectReferences(schema)
local errors = h.validate(schemas[schemaName], data, dataName)
+
local errors = h.validate(schema, data, dataName)
 
if #errors > 0 then
 
if #errors > 0 then
 
utilsError.warn(string.format("<code>%s</code> is invalid according to schema <code>%s</code>", dataName, schemaName))
 
utilsError.warn(string.format("<code>%s</code> is invalid according to schema <code>%s</code>", dataName, schemaName))
return "Category:Modules with invalid data", errors
+
return errors
 
end
 
end
 
end
 
end
   
function p.getTypeDefinitions(schemas, schemaName, formattingFn)
+
function p.getTypeDefinitions(schema, schemaName, formattingFn)
local schema = schemas[schemaName]
 
 
h.collectReferences(schema)
 
h.collectReferences(schema)
 
h.minRefDepths(schema)
 
h.minRefDepths(schema)
Line 101: Line 104:
 
end
 
end
 
if schemaNode.oneOf then
 
if schemaNode.oneOf then
for i, v in ipairs(schemaNode.oneOf) do
+
for k, v in pairs(schemaNode.oneOf) do
 
h.walkSchema(fn, v, utilsTable.concat(path, "oneOf"))
 
h.walkSchema(fn, v, utilsTable.concat(path, "oneOf"))
 
end
 
end
Line 141: Line 144:
 
end
 
end
 
function h.showSubkeys(schemaNode)
 
function h.showSubkeys(schemaNode)
  +
local ref = schemaNode._ref
return not schemaNode._ref or h.minDepthNode[schemaNode._ref] == schemaNode or not h.hasRefs(h.references[schemaNode._ref])
 
  +
if schemaNode._hideSubkeys then
  +
return false
  +
elseif not ref then
  +
return true
  +
elseif not utilsString.startsWith(ref, "#/definitions") then
  +
return false
  +
else
  +
return h.minDepthNode[ref] == schemaNode or not h.hasRefs(h.references[ref])
  +
end
 
end
 
end
   
 
function h.getTypeDefs(schemaName, schema, formattingFn, parentSchema, isSubschema)
 
function h.getTypeDefs(schemaName, schema, formattingFn, parentSchema, isSubschema)
local typeLabel
+
local typeLabel = schema.typeLabel
 
local showSubkeys = true
 
local showSubkeys = true
if schema._ref and utilsString.startsWith("#/definitions", schema._ref) then
+
if schema._ref then
  +
typeLabel = typeLabel or string.gsub(schema._ref, "#/definitions/", "")
  +
typeLabel = typeLabel or string.gsub(typeLabel, "#", "")
 
showSubkeys = h.showSubkeys(schema)
 
showSubkeys = h.showSubkeys(schema)
typeLabel = string.gsub(schema._ref, "#/definitions/", "")
 
 
end
 
end
 
schema = h.resolveReference(schema)
 
schema = h.resolveReference(schema)
Line 181: Line 194:
 
subkeys.oneOf = subkeys.oneOf or {}
 
subkeys.oneOf = subkeys.oneOf or {}
 
local subtypes = {}
 
local subtypes = {}
  +
local i = 1
for i, subschema in ipairs(schema.oneOf) do
 
  +
for k, subschema in pairs(schema.oneOf) do
  +
if type(k) == "string" then
  +
subschema.typeLabel = k
  +
end
 
local keys, subtype = h.getTypeDefs(nil, subschema, formattingFn, schema, true)
 
local keys, subtype = h.getTypeDefs(nil, subschema, formattingFn, schema, true)
 
if string.find(subtype, "|") or string.find(subtype, "&") then
 
if string.find(subtype, "|") or string.find(subtype, "&") then
Line 188: Line 205:
 
table.insert(subtypes, subtype)
 
table.insert(subtypes, subtype)
 
subkeys.oneOf[i] = keys
 
subkeys.oneOf[i] = keys
  +
i = i + 1
 
end
 
end
 
symbolicType = subtypes[1]
 
symbolicType = subtypes[1]
Line 216: Line 234:
 
 
 
local parentType = parentSchema and parentSchema.type
 
local parentType = parentSchema and parentSchema.type
  +
if parentType == "array" and typeLabel then
  +
symbolicType = typeLabel
  +
end
 
 
 
local key = schemaName
 
local key = schemaName
 
if schema.default then
 
if schema.default then
key = key and string.format(SYMBOLS.default, schemaName, schema.default)
+
key = key and string.format(SYMBOLS.default, schemaName, tostring(schema.default))
 
end
 
end
if parentSchema and not parentSchema.allOf and not parentSchema.oneOf and parentType ~= p.TYPES.array and parentType ~= p.TYPES.map then -- otherwise leads to nonsense like [[{[string]}]|[[string]]], instead of [{string}|string]
+
if parentSchema == nil or not (parentSchema.allOf or parentSchema.oneOf or parentType == p.TYPES.array or parentType == p.TYPES.map) then -- otherwise leads to nonsense like [[{[string]}]|[[string]]], instead of [{string}|string]
 
if schema.required then
 
if schema.required then
 
symbolicType = string.format(SYMBOLS.required, symbolicType)
 
symbolicType = string.format(SYMBOLS.required, symbolicType)
Line 243: Line 264:
 
end
 
end
   
function h.validate(schemaNode, data, dataName, dataPath, quiet, isKey)
+
function h.validate(schemaNode, data, dataName, dataPath, parentSchema, quiet)
 
dataPath = dataPath or {}
 
dataPath = dataPath or {}
local value = utilsTable.property(dataPath)(data)
+
local value = utilsTable.property(data, dataPath)
local errPath = dataName .. utilsTable.path(dataPath)
+
local errPath = dataName .. utilsTable.printPath(dataPath)
 
schemaNode = h.resolveReference(schemaNode)
 
schemaNode = h.resolveReference(schemaNode)
 
 
local errors = h.validatePrimitive(schemaNode, value, dataName, dataPath, quiet, isKey)
+
local errors = h.validatePrimitive(schemaNode, value, dataName, dataPath, quiet)
 
if #errors > 0 then
 
if #errors > 0 then
 
return errors
 
return errors
Line 255: Line 276:
 
 
 
if schemaNode.allOf and value then
 
if schemaNode.allOf and value then
local subschemaErrors = h.validateSubschemas(schemaNode.allOf, data, dataName, dataPath)
+
local subschemaErrors = h.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
invalidSubschemas = utilsTable.map(code)(invalidSubschemas)
+
invalidSubschemas = utilsTable.map(invalidSubschemas, code)
 
invalidSubschemas = mw.text.listToText(invalidSubschemas)
 
invalidSubschemas = mw.text.listToText(invalidSubschemas)
 
local msg = string.format("%s does not match <code>allOf</code> sub-schemas %s", code(errPath), invalidSubschemas)
 
local msg = string.format("%s does not match <code>allOf</code> sub-schemas %s", code(errPath), invalidSubschemas)
 
if not quiet then
 
if not quiet then
utilsError.warn(msg, false)
+
utilsError.warn(msg, { traceBack = false })
 
h.logSubschemaErrors(subschemaErrors)
 
h.logSubschemaErrors(subschemaErrors)
 
end
 
end
Line 273: Line 294:
 
end
 
end
 
if schemaNode.oneOf and value then
 
if schemaNode.oneOf and value then
local subschemaErrors, validSubschemas = h.validateSubschemas(schemaNode.oneOf, data, dataName, dataPath)
+
local subschemaErrors, validSubschemas = h.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
 
local msg = string.format("%s does not match any <code>oneOf</code> sub-schemas.", code(errPath))
 
local msg = string.format("%s does not match any <code>oneOf</code> sub-schemas.", code(errPath))
 
if not quiet then
 
if not quiet then
utilsError.warn(msg, false)
+
utilsError.warn(msg, { traceBack = false })
 
h.logSubschemaErrors(subschemaErrors)
 
h.logSubschemaErrors(subschemaErrors)
 
end
 
end
Line 288: Line 309:
 
end
 
end
 
if #validSubschemas > 1 then
 
if #validSubschemas > 1 then
validSubschemas = utilsTable.map(code)(validSubschemas)
+
validSubschemas = utilsTable.map(validSubschemas, code)
 
validSubschemas = mw.text.listToText(validSubschemas)
 
validSubschemas = mw.text.listToText(validSubschemas)
 
local msg = string.format("%s matches <code>oneOf</code> sub-schemas %s, but must match only one.", code(errPath), validSubschemas)
 
local msg = string.format("%s matches <code>oneOf</code> sub-schemas %s, but must match only one.", code(errPath), validSubschemas)
 
if not quiet then
 
if not quiet then
utilsError.warn(msg, false)
+
utilsError.warn(msg, { traceBack = false })
 
end
 
end
 
table.insert(errors, {
 
table.insert(errors, {
Line 304: Line 325:
 
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, quiet))
+
errors = utilsTable.concat(errors, h.validate(propSchema, data, dataName, keyPath, schemaNode, quiet))
 
end
 
end
if not schemaNode.additionalProperties then
+
if not schemaNode.additionalProperties and not (parentSchema and parentSchema.allOf) then
local schemaProps = utilsTable.map("name")(schemaNode.properties)
+
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 dataProps = utilsTable.keys(value)
local undefinedProps = utilsTable.difference(dataProps)(schemaProps)
+
local undefinedProps = utilsTable.difference(dataProps, schemaProps)
 
if #undefinedProps > 0 then
 
if #undefinedProps > 0 then
undefinedProps = mw.text.listToText(utilsTable.map(code)(undefinedProps))
+
undefinedProps = mw.text.listToText(utilsTable.map(undefinedProps, code))
 
local msg = string.format("Record %s has undefined properties: %s", code(errPath), undefinedProps)
 
local msg = string.format("Record %s has undefined properties: %s", code(errPath), undefinedProps)
 
if not quiet then
 
if not quiet then
utilsError.warn(msg, false)
+
utilsError.warn(msg, { traceBack = false })
 
end
 
end
 
table.insert(errors, {
 
table.insert(errors, {
Line 324: Line 349:
 
end
 
end
 
if schemaNode.items and value then
 
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("%s is supposed to be an array only, but it has string keys: %s", code(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
 
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, quiet))
+
errors = utilsTable.concat(errors, h.validate(schemaNode.items, data, dataName, itemPath, schemaNode, quiet))
 
end
 
end
 
end
 
end
 
if schemaNode.keys and schemaNode.values and value then
 
if schemaNode.keys and schemaNode.values and value then
 
for k, v in pairs(value) do
 
for k, v in pairs(value) do
local keyPath = utilsTable.concat(dataPath, 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, h.validatePrimitive(schemaNode.keys, k, dataName, dataPath, quiet, true))
errors = utilsTable.concat(errors, h.validate(schemaNode.values, data, dataName, keyPath, quiet))
+
errors = utilsTable.concat(errors, h.validate(schemaNode.values, data, dataName, keyPath, schemaNode, quiet))
 
end
 
end
 
end
 
end
Line 340: Line 376:
 
end
 
end
   
function h.validateSubschemas(subschemas, data, dataName, dataPath)
+
function h.validateSubschemas(subschemas, data, dataName, dataPath, schemaNode, parentSchema)
 
local errors = {}
 
local errors = {}
 
local valids = {}
 
local valids = {}
for i, subschema in ipairs(subschemas) do
+
for k, subschema in pairs(subschemas) do
local key = subschema._ref and string.gsub(subschema._ref, "#/definitions/", "") or i
+
local key = subschema._ref and string.gsub(subschema._ref, "#/definitions/", "") or k
  +
if parentSchema and parentSchema.allOf then
local err = h.validate(subschema, data, dataName, dataPath, true)
 
  +
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
 
if #err > 0 then
 
errors[key] = err
 
errors[key] = err
Line 354: Line 397:
 
return errors, valids
 
return errors, valids
 
end
 
end
function h.logSubschemaErrors(subschemaErrors)
+
function h.logSubschemaErrors(subschemaErrors, path)
 
for schemaKey, schemaErrors in pairs(subschemaErrors) do
 
for schemaKey, schemaErrors in pairs(subschemaErrors) do
  +
local subpath = utilsTable.concat(path or {}, '["' .. schemaKey .. '"]')
for _, err in pairs(utilsTable.flatten(subschemaErrors)) do
 
  +
local indent = string.rep(":", #subpath)
utilsError.warn(code(schemaKey) .. ": " .. err.msg, false)
 
  +
for _, err in pairs(schemaErrors) do
  +
local msg = string.format("%s: %s", code(utilsTable.printPath(subpath)), err.msg)
  +
utilsError.warn(indent .. msg, { traceBack = false })
  +
if err.errors then
  +
h.logSubschemaErrors(err.errors, subpath)
  +
end
 
end
 
end
 
end
 
end
Line 371: Line 420:
 
if schemaNode.type and schemaNode.type ~= "any" then
 
if schemaNode.type and schemaNode.type ~= "any" then
 
local expectedType = h.getLuaType(schemaNode.type)
 
local expectedType = h.getLuaType(schemaNode.type)
addIfError(utilsArg.type(expectedType))
+
addIfError(utilsValidate.type(expectedType))
 
end
 
end
 
if schemaNode.required then
 
if schemaNode.required then
addIfError(utilsArg.required)
+
addIfError(utilsValidate.required)
  +
end
  +
if schemaNode.deprecated then
  +
addIfError(utilsValidate.deprecated)
 
end
 
end
 
if schemaNode.enum then
 
if schemaNode.enum then
addIfError(utilsArg.enum(schemaNode.enum))
+
addIfError(utilsValidate.enum(schemaNode.enum))
 
end
 
end
 
return errors
 
return errors
Line 384: Line 436:
 
function h.errorCollector(errorTbl, value, dataName, dataPath, quiet, isKey)
 
function h.errorCollector(errorTbl, value, dataName, dataPath, quiet, isKey)
 
return function(validator)
 
return function(validator)
local err, msg = validator(value, dataName, dataPath, isKey, {
+
local errorMessages = validator(value, dataName, dataPath, isKey, {
 
quiet = quiet,
 
quiet = quiet,
 
stackTrace = false,
 
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)
if err then
 
  +
errorMessages = {errorMessages}
  +
end
  +
for _, errMsg in ipairs(errorMessages) do
 
table.insert(errorTbl, {
 
table.insert(errorTbl, {
path = dataName .. utilsTable.path(dataPath),
+
path = dataName .. utilsTable.printPath(dataPath),
msg = msg,
+
msg = errMsg,
 
})
 
})
 
end
 
end
Line 421: Line 476:
 
}
 
}
 
},
 
},
additionalProperties = true,
 
 
},
 
},
 
{
 
{
Line 453: Line 507:
 
type = "boolean",
 
type = "boolean",
 
desc = "If <code>true</code>, the value cannot be <code>nil</code>."
 
desc = "If <code>true</code>, the value cannot be <code>nil</code>."
  +
},
  +
{
  +
name = "deprecated",
  +
type = "boolean",
  +
desc = "If <code>true</code>, validation fails when this value is present. A deprecation warning is logged."
 
},
 
},
 
{
 
{
Line 460: Line 519:
 
},
 
},
 
},
 
},
additionalProperties = true,
 
 
},
 
},
 
["Primitive Schema"] = {
 
["Primitive Schema"] = {
Line 477: Line 535:
 
{
 
{
 
name = "enum",
 
name = "enum",
  +
desc = "An array of values that are considered acceptable, plus an optional <code>reference</code> key.",
type = "array",
 
items = { type = "any" },
+
allOf = {
  +
{
desc = "An array of values that are considered acceptable.",
 
  +
type = "array",
  +
items = { type = "any" },
  +
},
  +
{
  +
type = "record",
  +
properties = {
  +
{
  +
name = "reference",
  +
type = "string",
  +
desc = "A link to a page that lists the accepted values."
  +
}
  +
}
  +
}
  +
}
 
},
 
},
 
},
 
},
additionalProperties = true,
 
 
}
 
}
 
}
 
}
Line 506: Line 577:
 
},
 
},
 
},
 
},
additionalProperties = true,
 
 
}
 
}
 
}
 
}
Line 520: Line 590:
 
},
 
},
 
},
 
},
additionalProperties = true,
 
 
},
 
},
 
["Record Schema"] = {
 
["Record Schema"] = {
Line 551: Line 620:
 
name = "additionalProperties",
 
name = "additionalProperties",
 
type = "boolean",
 
type = "boolean",
desc = "If true, the record is considered valid when it has additional properties other than the ones specified in <code>properties</code>."
+
desc = "If true, the record is considered valid when it has additional properties other than the ones specified in <code>properties</code>. True by default for <code>allOf</code> subschemas; false by default otherwise."
 
},
 
},
additionalProperties = true,
 
 
},
 
},
 
},
 
},
Line 585: Line 653:
 
},
 
},
 
},
 
},
additionalProperties = true,
 
 
}
 
}
 
}
 
}
Line 598: Line 665:
 
name = "oneOf",
 
name = "oneOf",
 
required = true,
 
required = true,
  +
desc = "A table of subschemas. The data must be valid against '''exactly one''' of them.",
type = "array",
 
  +
oneOf = {
desc = "An array of subschemas. The data must be valid against '''exactly one''' of them.",
 
  +
{
items = { _ref = "#/definitions/Schema" },
 
  +
type = "array",
  +
items = { _ref = "#/definitions/Schema" },
  +
},
  +
{
  +
type = "map",
  +
keys = { type = "string" },
  +
values = { _ref = "#/definitions/Schema" },
  +
}
  +
},
 
}
 
}
 
},
 
},
additionalProperties = true,
 
 
}
 
}
 
}
 
}
Line 621: Line 696:
 
},
 
},
 
},
 
},
additionalProperties = true,
 
 
}
 
}
 
}
 
}
Line 637: Line 711:
 
desc = "A [[Module:Schema#References|reference]] to another part of the schema.",
 
desc = "A [[Module:Schema#References|reference]] to another part of the schema.",
 
},
 
},
  +
{
  +
name = "_hideSubkeys",
  +
type = "boolean",
  +
desc = "Ignore reference when generating documentation. Use sparingly as there is already a mechanism for determining whether to show referenced schema fragments."
  +
}
 
},
 
},
additionalProperties = true,
 
 
}
 
}
 
}
 
}
Line 647: Line 725:
   
 
p.Documentation = {
 
p.Documentation = {
  +
validate = {
{
 
  +
params = {"schema", "schemaName", "data", "dataName"},
outputOnly = true,
 
  +
returns = "An array of error paths and messages, or nil if there are none.",
name = "validate",
 
params = {
 
{
 
name = "schemas",
 
description = "Collection of related schemas to validate against.",
 
},
 
{
 
name = "schemaName",
 
description = "A key in the above table—the particular schema to validate against, which may refernce the other schemas provided.",
 
},
 
{
 
name = "data",
 
description = "The data to validate against the given schema.",
 
},
 
{
 
name = "dataName",
 
description = "A string used in warning messages to represent the data as a variable."
 
},
 
},
 
returns = {
 
"The name of an error category if there are validation errors, nil otherwise",
 
"An array of paths and messages.",
 
},
 
 
cases = {
 
cases = {
  +
outputOnly = true,
 
{
 
{
description = "Basic record with three fields.",
+
desc = "Basic record with four fields.",
 
args = {
 
args = {
 
{
 
{
gameItem = {
+
type = "record",
type = "record",
+
properties = {
properties = {
+
{
{
+
name = "name",
name = "name",
+
type = "string",
  +
required = true,
  +
},
  +
{
  +
name = "games",
  +
type = "array",
  +
items = {
 
type = "string",
 
type = "string",
required = true,
+
enum = { "TLoZ", "TAoL", "ALttP"},
 
},
 
},
{
+
},
name = "games",
+
{
type = "array",
+
name = "cost",
items = {
+
type = "number",
type = "string",
+
},
  +
{
enum = { "TLoZ", "TAoL", "ALttP", "other" },
 
},
+
name = "deprecatedField",
},
+
type = "string",
{
+
deprecated = true,
name = "cost",
 
type = "number",
 
}
 
 
}
 
}
},
+
},
 
},
 
},
 
"gameItem",
 
"gameItem",
 
{
 
{
 
cost = "50 Rupees",
 
cost = "50 Rupees",
games = { "💩" },
+
games = { "YY", "ZZ" },
 
nonsenseField = "foo",
 
nonsenseField = "foo",
  +
deprecatedField = "bar",
 
},
 
},
 
"itemVariable"
 
"itemVariable"
 
},
 
},
expected = {
+
expect = {
"Category:Modules with invalid data",
 
 
{
 
{
  +
path = 'itemVariable.name',
{
 
path = 'itemVariable["name"]',
+
msg = '<code>itemVariable.name</code> is required but is <code>nil</code>.',
  +
},
msg = '<code>itemVariable["name"]</code> is required but is <code>nil</code>.',
 
},
+
{
  +
path = 'itemVariable.games[1]',
{
 
path = 'itemVariable["games"][1]',
+
msg = '<code>itemVariable.games[1]</code> has unexpected value <code>YY</code>. The accepted values are: <code>{"TLoZ", "TAoL", "ALttP"}</code>',
  +
},
msg = '<code>itemVariable["games"][1]</code> has unexpected value <code>💩</code>. The accepted values are: <code>{ "TLoZ", "TAoL", "ALttP", "other" }</code>',
 
},
+
{
  +
path = 'itemVariable.games[2]',
{
 
  +
msg = '<code>itemVariable.games[2]</code> has unexpected value <code>ZZ</code>. The accepted values are: <code>{"TLoZ", "TAoL", "ALttP"}</code>',
path = 'itemVariable["cost"]',
 
  +
},
msg = '<code>itemVariable["cost"]</code> is type <code>string</code> but type <code>number</code> was expected.',
 
},
+
{
  +
path = 'itemVariable.cost',
{
 
  +
msg = '<code>itemVariable.cost</code> is type <code>string</code> but type <code>number</code> was expected.',
path = "itemVariable",
 
  +
},
msg = "Record <code>itemVariable</code> has undefined properties: <code>nonsenseField</code>",
 
},
+
{
  +
msg = '<code>itemVariable.deprecatedField</code> is deprecated but has value <code>bar</code>.',
  +
path = 'itemVariable.deprecatedField',
  +
},
  +
{
  +
path = "itemVariable",
  +
msg = "Record <code>itemVariable</code> has undefined properties: <code>nonsenseField</code>",
 
},
 
},
 
},
 
},
 
},
 
},
 
{
 
{
description = "Map validation.",
+
desc = "Map validation.",
 
args = {
 
args = {
 
{
 
{
Games = {
+
type = "map",
type = "map",
+
keys = { type = "string" },
keys = { type = "string" },
+
values = { type = "string" },
values = { type = "string" },
 
},
 
 
},
 
},
 
"Games",
 
"Games",
Line 748: Line 812:
 
"games"
 
"games"
 
},
 
},
expected = {
+
expect = {
"Category:Modules with invalid data",
 
 
{
 
{
{
+
path = "games",
  +
msg = "<code>games</code> key <code>3</code> is type <code>number</code> but type <code>string</code> was expected.",
path = "games",
 
  +
},
msg = "<code>games</code> key <code>3</code> is type <code>number</code> but type <code>string</code> was expected.",
 
},
+
{
{
+
path = 'games["OoT"]',
path = 'games["OoT"]',
+
msg = '<code>games["OoT"]</code> is type <code>number</code> but type <code>string</code> was expected.',
  +
},
msg = '<code>games["OoT"]</code> is type <code>number</code> but type <code>string</code> was expected.',
 
},
 
}
 
 
}
 
}
 
},
 
},
 
{
 
{
description = "A schema using <code>oneOf</code> and an ID-based reference.",
+
desc = "A schema using <code>oneOf</code> and an ID-based reference.",
 
args = {
 
args = {
 
{
 
{
numberOrArray = {
+
oneOf = {
oneOf = {
+
{
{
+
_id = "#num",
_id = "#num",
+
type = "number",
type = "number",
 
},
 
{
 
type = "array",
 
items = { _ref = "#num"},
 
},
 
 
},
 
},
},
+
{
  +
type = "array",
  +
items = { _ref = "#num"},
  +
},
  +
}
 
},
 
},
 
"numberOrArray",
 
"numberOrArray",
Line 783: Line 842:
 
"arg",
 
"arg",
 
},
 
},
expected = {
+
expect = {
"Category:Modules with invalid data",
 
 
{
 
{
{
 
 
msg = "<code>arg</code> does not match any <code>oneOf</code> sub-schemas.",
 
msg = "<code>arg</code> does not match any <code>oneOf</code> sub-schemas.",
path = "arg",
+
path = "arg",
errors = {
+
errors = {
  +
{
 
{
 
{
{
+
path = "arg",
  +
msg = "<code>arg</code> is type <code>table</code> but type <code>number</code> was expected.",
path = "arg",
 
msg = "<code>arg</code> is type <code>table</code> but type <code>number</code> was expected.",
 
},
 
 
},
 
},
  +
},
  +
{
 
{
 
{
{
+
path = "arg[1]",
  +
msg = "<code>arg[1]</code> is type <code>string</code> but type <code>number</code> was expected.",
path = "arg[1]",
 
msg = "<code>arg[1]</code> is type <code>string</code> but type <code>number</code> was expected.",
 
},
 
 
},
 
},
 
},
 
},
Line 808: Line 864:
 
},
 
},
 
{
 
{
description = "A schema using <code>oneOf</code> and <code>definitions</code> references. A simplification of [[Module:Documentation]]'s schema.",
+
desc = "Schema using <code>oneOf</code> and <code>definitions</code> references. A simplification of [[Module:Documentation]]'s schema.",
 
args = {
 
args = {
 
{
 
{
Documentation = {
+
oneOf = {
oneOf = {
+
{ _ref = "#/definitions/functions", },
{ _ref = "#/definitions/functions", },
+
{ _ref = "#/definitions/sections", },
  +
},
{ _ref = "#/definitions/sections", },
 
  +
definitions = {
  +
functions = {
  +
type = "array",
  +
items = { type = "string" },
 
},
 
},
definitions = {
+
sections = {
functions = {
+
type = "record",
type = "array",
+
properties = {
items = { type = "string" },
+
{
},
+
name = "sections",
sections = {
+
type = "array",
type = "record",
+
items = { _ref = "#/definitions/functions" }
properties = {
 
{
 
name = "sections",
 
type = "array",
 
items = { _ref = "#/definitions/functions" }
 
},
 
 
},
 
},
additionalProperties = true,
 
 
},
 
},
}
+
},
},
+
}
 
},
 
},
 
"Documentation",
 
"Documentation",
Line 845: Line 898:
 
"doc",
 
"doc",
 
},
 
},
expected = {
+
expect = {
"Category:Modules with invalid data",
 
 
{
 
{
  +
msg = "<code>doc</code> does not match any <code>oneOf</code> sub-schemas.",
{
 
path = "doc",
+
path = "doc",
  +
errors = {
msg = "<code>doc</code> matches <code>oneOf</code> sub-schemas <code>functions</code> and <code>sections</code>, but must match only one.",
 
  +
functions = {
  +
{
  +
msg = '<code>doc</code> is supposed to be an array only, but it has string keys: {"sections"}',
  +
path = "doc",
  +
},
  +
},
  +
sections = {
  +
{
  +
path = "doc",
  +
msg = "Record <code>doc</code> has undefined properties: <code>1</code>",
  +
},
  +
},
 
},
 
},
}
+
},
 
},
 
},
 
},
 
},
 
{
 
{
description = "A schema using <code>allOf</code>",
+
desc = "Data is invalid if it matches more than one <code>oneOf</code>.",
 
args = {
 
args = {
 
{
 
{
  +
oneOf = {{ type = "string" }, { type = "string" }}
Schema = {
 
allOf = {
+
},
{
+
"Schema",
type = "number",
+
"Fooloo Limpah",
required = true,
+
"magicWords",
  +
},
  +
expect = {
  +
{
  +
path = "magicWords",
  +
msg = "<code>magicWords</code> matches <code>oneOf</code> sub-schemas <code>1</code> and <code>2</code>, but must match only one.",
  +
},
  +
},
  +
},
  +
{
  +
desc = "A schema using <code>allOf</code>.",
  +
args = {
  +
{
  +
allOf = {
  +
{
  +
type = "record",
  +
properties = {
  +
{
  +
name = "foo",
  +
type = "string",
  +
},
 
},
 
},
{
 
type = "number",
 
enum = { 0 },
 
}
 
 
},
 
},
  +
{
  +
type = "array",
  +
items = { type = "number" },
  +
}
 
},
 
},
 
},
 
},
 
"Schema",
 
"Schema",
"foo",
+
{1, 2, 3, foo = 4},
 
"arg",
 
"arg",
 
},
 
},
expected = {
+
expect = {
"Category:Modules with invalid data",
 
 
{
 
{
  +
msg = "<code>arg</code> does not match <code>allOf</code> sub-schemas <code>1</code>",
{
 
  +
path = "arg",
msg = "<code>arg</code> does not match <code>allOf</code> sub-schemas <code>1</code> and <code>2</code>",
 
path = "arg",
+
errors = {
errors = {
+
{
 
{
 
{
{
+
path = "arg.foo",
  +
msg = '<code>arg.foo</code> is type <code>number</code> but type <code>string</code> was expected.',
path = "arg",
 
msg = "<code>arg</code> is type <code>string</code> but type <code>number</code> was expected.",
 
},
 
},
 
{
 
{
 
path = "arg",
 
msg = "<code>arg</code> is type <code>string</code> but type <code>number</code> was expected.",
 
},
 
{
 
path = "arg",
 
msg = "<code>arg</code> has unexpected value <code>foo</code>. The accepted values are: <code>{ 0 }</code>",
 
},
 
 
},
 
},
 
},
 
},
},
+
}
}
+
},
 
}
 
}
 
},
 
},

Revision as of 13:02, 13 June 2020

This module is a bastardized JSON Schema implementation for Lua tables. It is a submodule for Module:Documentation.

This module exports the following functions.

validate

validate(schema, schemaName, data, dataName)

Returns

  • An array of error paths and messages, or nil if there are none.

Examples

#InputOutputStatus
Basic record with four fields.
1
validate(
  {
    type = "record",
    properties = {
      {
        type = "string",
        name = "name",
        required = true,
      },
      {
        name = "games",
        type = "array",
        items = {
          type = "string",
          enum = {"TLoZ", "TAoL", "ALttP"},
        },
      },
      {
        name = "cost",
        type = "number",
      },
      {
        deprecated = true,
        type = "string",
        name = "deprecatedField",
      },
    },
  },
  "gameItem",
  {
    cost = "50 Rupees",
    nonsenseField = "foo",
    deprecatedField = "bar",
    games = {"YY", "ZZ"},
  },
  "itemVariable"
)
{
  {
    msg = "<code>itemVariable.name</code> is required but is <code>nil</code>.",
    path = "itemVariable.name",
  },
  {
    msg = '<code>itemVariable.games[1]</code> has unexpected value <code>YY</code>. The accepted values are: <code>{"TLoZ", "TAoL", "ALttP"}</code>',
    path = "itemVariable.games[1]",
  },
  {
    msg = '<code>itemVariable.games[2]</code> has unexpected value <code>ZZ</code>. The accepted values are: <code>{"TLoZ", "TAoL", "ALttP"}</code>',
    path = "itemVariable.games[2]",
  },
  {
    msg = "<code>itemVariable.cost</code> is type <code>string</code> but type <code>number</code> was expected.",
    path = "itemVariable.cost",
  },
  {
    msg = "<code>itemVariable.deprecatedField</code> is deprecated but has value <code>bar</code>.",
    path = "itemVariable.deprecatedField",
  },
  {
    msg = "Record <code>itemVariable</code> has undefined properties: <code>nonsenseField</code>",
    path = "itemVariable",
  },
}
Green check
Map validation.
2
validate(
  {
    type = "map",
    keys = { type = "string" },
    values = { type = "string" },
  },
  "Games",
  {
    [3] = "A Link to the Past",
    OoA = "Oracle of Ages",
    OoT = 5,
  },
  "games"
)
{
  {
    msg = "<code>games</code> key <code>3</code> is type <code>number</code> but type <code>string</code> was expected.",
    path = "games",
  },
  {
    msg = '<code>games["OoT"]</code> is type <code>number</code> but type <code>string</code> was expected.',
    path = 'games["OoT"]',
  },
}
Green check
A schema using oneOf and an ID-based reference.
3
validate(
  {
    oneOf = {
      {
        type = "number",
        _id = "#num",
      },
      {
        type = "array",
        items = { _ref = "#num" },
      },
    },
  },
  "numberOrArray",
  {"foo"},
  "arg"
)
{
  {
    path = "arg",
    msg = "<code>arg</code> does not match any <code>oneOf</code> sub-schemas.",
    errors = {
      {
        {
          msg = "<code>arg</code> is type <code>table</code> but type <code>number</code> was expected.",
          path = "arg",
        },
      },
      {
        {
          msg = "<code>arg[1]</code> is type <code>string</code> but type <code>number</code> was expected.",
          path = "arg[1]",
        },
      },
    },
  },
}
Green check
Schema using oneOf and definitions references. A simplification of Module:Documentation's schema.
4
validate(
  {
    oneOf = {
      { _ref = "#/definitions/functions" },
      { _ref = "#/definitions/sections" },
    },
    definitions = {
      sections = {
        type = "record",
        properties = {
          {
            name = "sections",
            type = "array",
            items = { _ref = "#/definitions/functions" },
          },
        },
      },
      functions = {
        type = "array",
        items = { type = "string" },
      },
    },
  },
  "Documentation",
  {
    "foo",
    sections = {
      {"bar", "baz"},
      {"quux"},
    },
  },
  "doc"
)
{
  {
    path = "doc",
    msg = "<code>doc</code> does not match any <code>oneOf</code> sub-schemas.",
    errors = {
      sections = {
        {
          msg = "Record <code>doc</code> has undefined properties: <code>1</code>",
          path = "doc",
        },
      },
      functions = {
        {
          msg = '<code>doc</code> is supposed to be an array only, but it has string keys: {"sections"}',
          path = "doc",
        },
      },
    },
  },
}
Green check
Data is invalid if it matches more than one oneOf.
5
validate(
  {
    oneOf = {
      { type = "string" },
      { type = "string" },
    },
  },
  "Schema",
  "Fooloo Limpah",
  "magicWords"
)
{
  {
    msg = "<code>magicWords</code> matches <code>oneOf</code> sub-schemas <code>1</code> and <code>2</code>, but must match only one.",
    path = "magicWords",
  },
}
Green check
A schema using allOf.
6
validate(
  {
    allOf = {
      {
        type = "record",
        properties = {
          {
            name = "foo",
            type = "string",
          },
        },
      },
      {
        type = "array",
        items = { type = "number" },
      },
    },
  },
  "Schema",
  {1, 2, 3, foo = 4},
  "arg"
)
{
  {
    path = "arg",
    msg = "<code>arg</code> does not match <code>allOf</code> sub-schemas <code>1</code>",
    errors = {
      {
        {
          msg = "<code>arg.foo</code> is type <code>number</code> but type <code>string</code> was expected.",
          path = "arg.foo",
        },
      },
    },
  },
}
Green check

local p = {}
local h = {}

local utilsError = require("Module:UtilsError")
local utilsString = require("Module:UtilsString")
local utilsTable = require("Module:UtilsTable")
local utilsValidate = require("Module:UtilsValidate")

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

p.TYPES = {
	oneOf = "oneOf",
	array = "array",
	record = "record",
	map = "map"
}

local function code(s)
	return string.format("<code>%s</code>", s)
end

function p.validate(schema, schemaName, data, dataName)
	-- first validate the schema itself
	h.collectReferences(p.Schemas["Schema"])
	local schemaErrors = h.validate(p.Schemas["Schema"], schema, schemaName)
	if #schemaErrors > 0 then
		utilsError.warn(string.format("Schema <code>%s</code> is invalid.", schemaName))
	end
	-- then validate the data
	h.collectReferences(schema)
	local errors = h.validate(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

function p.getTypeDefinitions(schema, schemaName, formattingFn)
	h.collectReferences(schema)
	h.minRefDepths(schema)
	return h.getTypeDefs(schemaName, schema, formattingFn)
end

function h.collectReferences(schema)
	h.references = {}
	h.references["#"] = schema
	for k, v in pairs(schema.definitions or {}) do
		h.references["#/definitions/" .. k] = v
		h.collectIdReferences(v)
	end
	h.collectIdReferences(schema)
end
function h.collectIdReferences(schema)
	h.walkSchema(function(schemaNode)
		if schemaNode._id then
			h.references[schemaNode._id] = schemaNode
		end
	end, schema)
end

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

function h.walkSchema(fn, schemaNode, path)
	path = path or {}
	local continue = fn(schemaNode, path)
	if continue == false then
		return
	end
	if schemaNode.items then
		h.walkSchema(fn, schemaNode.items, utilsTable.concat(path, "items"))
	end
	if schemaNode.keys then
		h.walkSchema(fn, schemaNode.keys, utilsTable.concat(path, "keys"))
	end
	if schemaNode.values then
		h.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)
			h.walkSchema(fn, v, keyPath)
		end
	end
	if schemaNode.oneOf then
		for k, v in pairs(schemaNode.oneOf) do
			h.walkSchema(fn, v, utilsTable.concat(path, "oneOf"))
		end
	end
	if schemaNode.allOf then
		for i, v in ipairs(schemaNode.allOf) do
			h.walkSchema(fn, v, utilsTable.concat(path, "allOf"))
		end
	end
	if schemaNode.definitions then
		for k, v in pairs(schemaNode.definitions) do
			h.walkSchema(fn, v, utilsTable.concat(path, "definitions", k))
		end
	end
end

-- This is to ensure that the documentation for recursive refs is shown only once.
function h.minRefDepths(schema)
	h.minDepthNode = {}
	local minDepths = {}
	h.walkSchema(function(schemaNode, path)
		if schemaNode._ref then
			minDepths[schemaNode._ref] = math.min(minDepths[schemaNode._ref] or 9000, #path)
			if #path == minDepths[schemaNode._ref] then
				h.minDepthNode[schemaNode._ref] = schemaNode
			end
		end
	end, schema)
end
function h.hasRefs(schema)
	local hasRefs = false
	h.walkSchema(function(schemaNode)
		if schemaNode._ref then
			hasRefs = true
			return false
		end
	end, schema)
	return hasRefs
end
function h.showSubkeys(schemaNode)
	local ref = schemaNode._ref
	if schemaNode._hideSubkeys then
		return false
	elseif not ref then
		return true
	elseif not utilsString.startsWith(ref, "#/definitions") then
		return false
	else
		return h.minDepthNode[ref] == schemaNode or not h.hasRefs(h.references[ref])
	end
end

function h.getTypeDefs(schemaName, schema, formattingFn, parentSchema, isSubschema)
	local typeLabel = schema.typeLabel
	local showSubkeys = true
	if schema._ref then
		typeLabel = typeLabel or string.gsub(schema._ref, "#/definitions/", "")
		typeLabel = typeLabel or string.gsub(typeLabel, "#", "")
		showSubkeys = h.showSubkeys(schema)
	end
	schema = h.resolveReference(schema)
	
	local rawType = schema.type
	local symbolicType
	local subkeys
	if showSubkeys then
		if schema.type == p.TYPES.record then
			for _, prop in ipairs(schema.properties) do
				subkeys = subkeys or {}
				local propDef = h.getTypeDefs(prop.name, prop, formattingFn, schema)
				table.insert(subkeys, propDef)
			end
		end
		if schema.type == p.TYPES.array then
			local subtypeKeys, subtype = h.getTypeDefs(nil, schema.items, formattingFn, schema)
			if #subtypeKeys > 0 then
				subkeys = subtypeKeys
			end
			symbolicType = string.format(SYMBOLS.array, subtype)
		end
		if schema.type == p.TYPES.map then
			local _, keyType = h.getTypeDefs(nil, schema.keys, formattingFn, schema)
			local valueDef, valueType = h.getTypeDefs(nil, schema.values, formattingFn, schema)
			symbolicType = string.format(SYMBOLS.map, keyType, valueType)
			subkeys = valueDef
		end
		if schema.oneOf then
			subkeys = subkeys or {}
			subkeys.oneOf = subkeys.oneOf or {}
			local subtypes = {}
			local i = 1
			for k, subschema in pairs(schema.oneOf) do
				if type(k) == "string" then
					subschema.typeLabel = k
				end
				local keys, subtype = h.getTypeDefs(nil, subschema, formattingFn, schema, true)
				if string.find(subtype, "|") or string.find(subtype, "&") then
					subtype = string.format(SYMBOLS.combinationGroup, subtype)
				end
				table.insert(subtypes, subtype)
				subkeys.oneOf[i] = keys
				i = i + 1
			end
			symbolicType = subtypes[1]
			for i, subtype in ipairs(utilsTable.tail(subtypes)) do
				symbolicType = string.format(SYMBOLS.oneOf, symbolicType, subtype)
			end
		end
		if schema.allOf then
			subkeys = subkeys or {}
			subkeys.allOf = {}
			local subtypes = {}
			for i, subschema in ipairs(schema.allOf) do
				local keys, subtype = h.getTypeDefs(nil, subschema, formattingFn, schema)
				if string.find(subtype, "|") or string.find(subtype, "&") then
					subtype = string.format(SYMBOLS.combinationGroup, subtype)
				end
				table.insert(subtypes, subtype)
				subkeys.allOf[i] = keys
			end
			subtypes = utilsTable.unique(subtypes)
			symbolicType = subtypes[1]
			for i, subtype in ipairs(utilsTable.tail(subtypes)) do
				symbolicType = string.format(SYMBOLS.allOf, symbolicType, subtype)
			end
		end
	end
	symbolicType = symbolicType or rawType or typeLabel
	
	local parentType = parentSchema and parentSchema.type
	if parentType == "array" and typeLabel then
		symbolicType = typeLabel
	end
	
	local key = schemaName 
	if schema.default then
		key = key and string.format(SYMBOLS.default, schemaName, tostring(schema.default))
	end
	if parentSchema == nil or not (parentSchema.allOf or parentSchema.oneOf or parentType == p.TYPES.array or parentType == p.TYPES.map) then  -- otherwise leads to nonsense like [[{[string]}]|[[string]]], instead of [{string}|string]
		if schema.required then
			symbolicType = string.format(SYMBOLS.required, symbolicType)
		else
			symbolicType = string.format(SYMBOLS.optional, symbolicType)
			key = key and string.format(SYMBOLS.optional, key)
		end
	end
	
	local formattedDef = formattingFn({
		key = key,
		subkeys = subkeys,
		rawType = rawType,
		typeLabel = typeLabel,
		symbolicType = symbolicType,
		desc = schema.desc,
		parentType = parentType,
		isSubschema = isSubschema,
	})
	return formattedDef, symbolicType
end

function h.validate(schemaNode, data, dataName, dataPath, parentSchema, quiet)
	dataPath = dataPath or {}
	local value = utilsTable.property(data, dataPath)
	local errPath = dataName .. utilsTable.printPath(dataPath)
	schemaNode = h.resolveReference(schemaNode)
	
	local errors = h.validatePrimitive(schemaNode, value, dataName, dataPath, quiet)
	if #errors > 0 then
		return errors
	end
	
	if schemaNode.allOf and value then
		local subschemaErrors = h.validateSubschemas(schemaNode.allOf, data, dataName, dataPath, schemaNode)
		local invalidSubschemas = utilsTable.keys(subschemaErrors)
		if #invalidSubschemas > 0 then
			invalidSubschemas = utilsTable.map(invalidSubschemas, code)
			invalidSubschemas = mw.text.listToText(invalidSubschemas)
			local msg = string.format("%s does not match <code>allOf</code> sub-schemas %s", code(errPath), invalidSubschemas)
			if not quiet then
				utilsError.warn(msg, { traceBack = false })
				h.logSubschemaErrors(subschemaErrors)
			end
			table.insert(errors, {
				path = errPath,
				msg = msg,
				errors = subschemaErrors
			})
		end
	end
	if schemaNode.oneOf and value then
		local subschemaErrors, validSubschemas = h.validateSubschemas(schemaNode.oneOf, data, dataName, dataPath, schemaNode, parentSchema)
		local invalidSubschemas = utilsTable.keys(subschemaErrors)
		if #validSubschemas == 0 then
			local msg = string.format("%s does not match any <code>oneOf</code> sub-schemas.", code(errPath))
			if not quiet then
				utilsError.warn(msg, { traceBack = false })
				h.logSubschemaErrors(subschemaErrors)
			end
			table.insert(errors, {
				path = errPath,
				msg = msg,
				errors = subschemaErrors
			})
		end
		if #validSubschemas > 1 then
			validSubschemas = utilsTable.map(validSubschemas, code)
			validSubschemas = mw.text.listToText(validSubschemas)
			local msg = string.format("%s matches <code>oneOf</code> sub-schemas %s, but must match only one.", code(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, h.validate(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, code))
				local msg = string.format("Record %s has undefined properties: %s", code(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("%s is supposed to be an array only, but it has string keys: %s", code(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, h.validate(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, h.validatePrimitive(schemaNode.keys, k, dataName, dataPath, quiet, true))
			errors = utilsTable.concat(errors, h.validate(schemaNode.values, data, dataName, keyPath, schemaNode, quiet))
		end
	end
	
	return errors
end

function h.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 = h.validate(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 h.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("%s: %s", code(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
	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 == p.TYPES.array 
	or schemaType == p.TYPES.record 
	or schemaType == p.TYPES.map 
	then
		return "table"
	end
	return schemaType
end

p.Schemas = {
	Schema = {
		allOf = {
			{
				type = "record",
				properties = {
					{
						name = "definitions",
						desc = "Schema fragments for [[Module:Schema#References|referencing]].",
						type = "map",
						keys = { type = "string" },
						values = { _ref = "#/definitions/Schema" },
					}
				},
			},
			{
				_ref = "#/definitions/Schema",
			},
		},
	
		definitions = {
			Schema = {
				required = true,
				oneOf = {
					{ _ref = "#/definitions/Primitive Schema" },
					{ _ref = "#/definitions/Array Schema" },
					{ _ref = "#/definitions/Record Schema" },
					{ _ref = "#/definitions/Map Schema" },
					{ _ref = "#/definitions/oneOf" },
					{ _ref = "#/definitions/allOf" },
					{ _ref = "#/definitions/Reference" },
				}
			},
			base = {
				type = "record",
				properties = {
					{
						name = "_id",
						type = "string",
						desc = "An ID to use for [[Module:Schema#References|referencing]].",
					},
					{
						name = "required",
						type = "boolean",
						desc = "If <code>true</code>, the value cannot be <code>nil</code>."
					},
					{
						name = "deprecated",
						type = "boolean",
						desc = "If <code>true</code>, validation fails when this value is present. A deprecation warning is logged."
					},
					{
						name = "desc",
						type = "string",
						desc = "Description of the schema, for [[Module:Schema#Documentation|documentation]]."
					},
				},
			},
			["Primitive Schema"] = { 
				allOf = { 
					{ _ref = "#/definitions/base" },
					{
						type = "record",
						properties = {
							{
								name = "type",
								type = "string",
								required = true,
								enum = { "string", "number" , "boolean", "any" },
								desc = '<code>"string"</code>, <code>"number"</code>, <code>"boolean"</code>, or <code>"any"</code>'
							},
							{
								name = "enum",
								desc = "An array of values that are considered acceptable, plus an optional <code>reference</code> key.",
								allOf = {
									{
										type = "array",
										items = { type = "any" },
									},
									{
										type = "record",
										properties = {
											{
												name = "reference",
												type = "string",
												desc = "A link to a page that lists the accepted values."
											}
										}
									}
								}
							},
						},
					}
				}
			},
			["Array Schema"] = {
				allOf = {
					{ _ref = "#/definitions/base" },
					{
						type = "record",
						properties = {
							{
								name = "type",
								type = "string",
								required = true,
								enum = { "array" },
								desc = 'The string <code>"array"</code>.'
							},
							{
								name = "items",
								required = true,
								_ref = "#/definitions/Schema",
								desc = "A schema that all items in the array must adhere to.",
							},
						},
					}
				}
			},
			name = {
				type = "record",
				properties = {
					{
						name = "name",
						required = true,
						type = "string",
						desc = "The key for the record entry."
					},
				},
			},
			["Record Schema"] = {
				allOf = {
					{ _ref = "#/definitions/base" },
					{
						type = "record",
						properties = {
							{
								name = "type",
								type = "string",
								required = true,
								enum = { "record" },
								desc = 'The string <code>"record"</code>.'
							},
							{
								name = "properties",
								required = true,
								type = "array",
								desc = "An array of schemas for each record entry, plus an additional field <code>name</code> for the name of the record entry.",
								items = {
									allOf = {
										{ _ref = "#/definitions/name" },
										{ _ref = "#/definitions/Schema" },
									}
								},
							},
						},
						{
							name = "additionalProperties",
							type = "boolean",
							desc = "If true, the record is considered valid when it has additional properties other than the ones specified in <code>properties</code>. True by default for <code>allOf</code> subschemas; false by default otherwise."
						},
					},
				},
			},
			["Map Schema"] = {
				allOf = {
					{ _ref = "#/definitions/base" },
					{
						type = "record",
						properties = {
							{
								name = "type",
								type = "string",
								required = true,
								enum = { "map" },
								desc = 'The string <code>"map"</code>.'
							},
							{
								name = "keys",
								type = "record",
								required = true,
								_ref = "#/definitions/Schema",
								desc = "The schema for the keys of the map."
							},
							{
								name = "values",
								type = "record",
								required = true,
								_ref = "#/definitions/Schema",
								desc = "The schema for the values of the map.",
							},
						},
					}
				}
			},
			["oneOf"] = {
				allOf = {
					{ _ref = "#/definitions/base" },
					{
						type = "record",
						properties = {
							{
								name = "oneOf",
								required = true,
								desc = "A table of subschemas. The data must be valid against '''exactly one''' of them.",
								oneOf = {
									{
										type = "array",
										items = { _ref = "#/definitions/Schema" },
									},
									{
										type = "map",
										keys = { type = "string" },
										values = { _ref = "#/definitions/Schema" },
									}
								},
							}
						},
					}
				}
			},
			["allOf"] = {
				allOf = {
					{ _ref = "#/definitions/base" },
					{
						type = "record",
						properties = {
							{
								name = "allOf",
								required = true,
								type = "array",
								desc = "An array of subschemas. The data must be valid against '''all''' of them.",
								items = { _ref = "#/definitions/Schema" },
							},
						},
					}
				}
			},
			["Reference"] = {
				allOf = {
					{ _ref = "#/definitions/base" },
					{
						type = "record",
						properties = {
							{
								name = "_ref",
								required = true,
								type = "string",
								desc = "A [[Module:Schema#References|reference]] to another part of the schema.",
							},
							{
								name = "_hideSubkeys",
								type = "boolean",
								desc = "Ignore reference when generating documentation. Use sparingly as there is already a mechanism for determining whether to show referenced schema fragments."
							}
						},
					}
				}
			},
		},
	}
}

p.Documentation = {
	validate = {
		params = {"schema", "schemaName", "data", "dataName"},
		returns = "An array of error paths and messages, or nil if there are none.",
		cases = {
			outputOnly = true,
			{
				desc = "Basic record with four fields.",
				args = {
					{
						type = "record",
						properties = {
							{
								name = "name",
								type = "string",
								required = true,
							},
							{
								name = "games",
								type = "array",
								items = {
									type = "string",
									enum = { "TLoZ", "TAoL", "ALttP"},
								},
							},
							{
								name = "cost",
								type = "number",
							},
							{
								name = "deprecatedField",
								type = "string",
								deprecated = true,
							}
						},						
					},
					"gameItem",
					{
						cost = "50 Rupees",
						games = { "YY", "ZZ" },
						nonsenseField = "foo",
						deprecatedField = "bar",
					},
					"itemVariable"
				},
				expect = {
					{
						path = 'itemVariable.name',
						msg = '<code>itemVariable.name</code> is required but is <code>nil</code>.',
					},
					{
						path = 'itemVariable.games[1]',
						msg = '<code>itemVariable.games[1]</code> has unexpected value <code>YY</code>. The accepted values are: <code>{"TLoZ", "TAoL", "ALttP"}</code>',
					},
					{
						path = 'itemVariable.games[2]',
						msg = '<code>itemVariable.games[2]</code> has unexpected value <code>ZZ</code>. The accepted values are: <code>{"TLoZ", "TAoL", "ALttP"}</code>',
					},
					{
						path = 'itemVariable.cost',
						msg = '<code>itemVariable.cost</code> is type <code>string</code> but type <code>number</code> was expected.',
					},
					{
						msg = '<code>itemVariable.deprecatedField</code> is deprecated but has value <code>bar</code>.',
						path = 'itemVariable.deprecatedField',
					},
					{
						path = "itemVariable",
						msg = "Record <code>itemVariable</code> has undefined properties: <code>nonsenseField</code>",
					},
				},
			},
			{
				desc = "Map validation.",
				args = {
					{
						type = "map",
						keys = { type = "string" },
						values = { type = "string" },
					},
					"Games",
					{
						OoA = "Oracle of Ages",
						OoT = 5,
						[3] = "A Link to the Past",
					},
					"games"
				},
				expect = {
					{
						path = "games",
						msg = "<code>games</code> key <code>3</code> is type <code>number</code> but type <code>string</code> was expected.",
					},
					{
						path = 'games["OoT"]',
						msg = '<code>games["OoT"]</code> is type <code>number</code> but type <code>string</code> was expected.',
					},
				}
			},
			{
				desc = "A schema using <code>oneOf</code> and an ID-based reference.",
				args = {
					{
						oneOf = {
							{
								_id = "#num",
								type = "number",
							},
							{
								type = "array",
								items = { _ref = "#num"},
							},
						}
					},
					"numberOrArray",
					{ "foo" },
					"arg",
				},
				expect = {
					{
						msg = "<code>arg</code> does not match any <code>oneOf</code> sub-schemas.",
						path = "arg",
						errors = {
							{
								{
									path = "arg",
									msg = "<code>arg</code> is type <code>table</code> but type <code>number</code> was expected.",
								},
							},
							{
								{
									path = "arg[1]",
									msg = "<code>arg[1]</code> is type <code>string</code> but type <code>number</code> was expected.",
								},
							},
						},
					},
				},
			},
			{
				desc = "Schema using <code>oneOf</code> and <code>definitions</code> references. A simplification of [[Module:Documentation]]'s schema.",
				args = {
					{
						oneOf = {
							{ _ref = "#/definitions/functions", },
							{ _ref = "#/definitions/sections", },
						},
						definitions = {
							functions = {
								type = "array",
								items = { type = "string" },
							},
							sections = {
								type = "record",
								properties = {
									{
										name = "sections",
										type = "array",
										items = { _ref = "#/definitions/functions" }
									},
								},
							},
						}
					},
					"Documentation",
					{
						"foo",
						sections = { 
							{"bar", "baz"},
							{ "quux" },
						},
					},
					"doc",
				},
				expect = {
					{
						msg = "<code>doc</code> does not match any <code>oneOf</code> sub-schemas.",
						path = "doc",
						errors = {
							functions = {
								{
									msg = '<code>doc</code> is supposed to be an array only, but it has string keys: {"sections"}',
									path = "doc",
								},
							},
							sections = {
								{
									path = "doc",
									msg = "Record <code>doc</code> has undefined properties: <code>1</code>",
								},
							},
						},
					},
				},
			},
			{
				desc = "Data is invalid if it matches more than one <code>oneOf</code>.",
				args = {
					{
						oneOf = {{ type = "string" }, { type = "string" }}
					},
					"Schema",
					"Fooloo Limpah",
					"magicWords",
				},
				expect = {
					{
						path = "magicWords",
						msg = "<code>magicWords</code> matches <code>oneOf</code> sub-schemas <code>1</code> and <code>2</code>, but must match only one.",
					},
				},
			},
			{
				desc = "A schema using <code>allOf</code>.",
				args = {
					{
						allOf = {
							{ 
								type = "record",
								properties = {
									{
										name = "foo",
										type = "string",
									},
								},
							},
							{ 
								type = "array",
								items = { type = "number" },
							}
						},
					},
					"Schema",
					{1, 2, 3, foo = 4},
					"arg",
				},
				expect = {
					{
						msg = "<code>arg</code> does not match <code>allOf</code> sub-schemas <code>1</code>",
						path = "arg",
						errors = {
							{
								{
									path = "arg.foo",
									msg = '<code>arg.foo</code> is type <code>number</code> but type <code>string</code> was expected.',
								},
							},
						}
					},
				}
			},
		},
	},
}

return p