Module:Util/schema/validate: Difference between revisions
Jump to navigation
Jump to search
PhantomCaleb (talk | contribs) No edit summary |
PhantomCaleb (talk | contribs) No edit summary |
||
(3 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
-- I apologize for how big a mess this is. | |||
local utilsError = require("Module:UtilsError") | local utilsError = require("Module:UtilsError") | ||
Line 13: | Line 13: | ||
} | } | ||
local metaSchema = require("Module:Util/ | local metaSchema = require("Module:Util/schema/validate/Documentation/Spec").Schemas() | ||
local SYMBOLS = { | local SYMBOLS = { | ||
Line 42: | Line 42: | ||
end | end | ||
local function | local function walkSchema(fn, schemaNode, path) | ||
path = path or {} | path = path or {} | ||
local continue = fn(schemaNode, path) | local continue = fn(schemaNode, path) | ||
Line 96: | Line 49: | ||
end | end | ||
if schemaNode.items then | if schemaNode.items then | ||
walkSchema(fn, schemaNode.items, utilsTable.concat(path, "items")) | |||
end | end | ||
if schemaNode.keys then | if schemaNode.keys then | ||
walkSchema(fn, schemaNode.keys, utilsTable.concat(path, "keys")) | |||
end | end | ||
if schemaNode.values then | if schemaNode.values then | ||
walkSchema(fn, schemaNode.values, utilsTable.concat(path, "values")) | |||
end | end | ||
if schemaNode.properties then | if schemaNode.properties then | ||
for i, v in ipairs(schemaNode.properties) do | for i, v in ipairs(schemaNode.properties) do | ||
local keyPath = utilsTable.concat(path, "properties", v.name) | local keyPath = utilsTable.concat(path, "properties", v.name) | ||
walkSchema(fn, v, keyPath) | |||
end | end | ||
end | end | ||
if schemaNode.oneOf then | if schemaNode.oneOf then | ||
for k, v in pairs(schemaNode.oneOf) do | for k, v in pairs(schemaNode.oneOf) do | ||
walkSchema(fn, v, utilsTable.concat(path, "oneOf")) | |||
end | end | ||
end | end | ||
if schemaNode.allOf then | if schemaNode.allOf then | ||
for i, v in ipairs(schemaNode.allOf) do | for i, v in ipairs(schemaNode.allOf) do | ||
walkSchema(fn, v, utilsTable.concat(path, "allOf")) | |||
end | end | ||
end | end | ||
if schemaNode.definitions then | if schemaNode.definitions then | ||
for k, v in pairs(schemaNode.definitions) do | for k, v in pairs(schemaNode.definitions) do | ||
walkSchema(fn, v, utilsTable.concat(path, "definitions", k)) | |||
end | end | ||
end | end | ||
end | end | ||
local function collectIdReferences(schema) | |||
function | walkSchema(function(schemaNode) | ||
if schemaNode._id then | |||
references[schemaNode._id] = schemaNode | |||
if schemaNode. | |||
end | end | ||
end, schema) | end, schema) | ||
end | end | ||
function | local function collectReferences(schema) | ||
references = {} -- use of global variable here not ideal | |||
references["#"] = schema | |||
if schemaNode._ref | 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 | ||
end | end | ||
return schemaNode | |||
end | end | ||
function | local function luaType(schemaType) | ||
if schemaType == TYPES.array | |||
or schemaType == TYPES.record | |||
or schemaType == TYPES.map | |||
then | |||
return "table" | |||
end | 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, | |||
if | 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 | end | ||
for _, errMsg in ipairs(errorMessages) do | |||
table.insert(errorTbl, { | |||
path = dataName .. utilsTable.printPath(dataPath), | |||
msg = errMsg, | |||
}) | |||
end | 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 = { | |||
if | 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 | ||
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) | |||
for | |||
end | end | ||
schemaNode.allOfProps = commonProps | |||
end | end | ||
local err = doValidate(subschema, data, dataName, dataPath, schemaNode, true) | |||
if #err > 0 then | |||
errors[key] = err | |||
if | |||
else | else | ||
table.insert(valids, key) | |||
end | end | ||
end | end | ||
return errors, valids | |||
return | |||
end | end | ||
function | function doValidate(schemaNode, data, dataName, dataPath, parentSchema, quiet) | ||
dataPath = dataPath or {} | dataPath = dataPath or {} | ||
local value = extract(data, dataPath) | local value = extract(data, dataPath) | ||
local errPath = dataName .. utilsTable.printPath(dataPath) | local errPath = dataName .. utilsTable.printPath(dataPath) | ||
schemaNode = | schemaNode = resolveReference(schemaNode) | ||
local errors = | local errors = validatePrimitive(schemaNode, value, dataName, dataPath, quiet) | ||
if #errors > 0 then | if #errors > 0 then | ||
return errors | return errors | ||
Line 283: | Line 208: | ||
if schemaNode.allOf and value then | if schemaNode.allOf and value then | ||
local subschemaErrors = | 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 291: | Line 216: | ||
if not quiet then | if not quiet then | ||
utilsError.warn(msg, { traceBack = false }) | utilsError.warn(msg, { traceBack = false }) | ||
logSubschemaErrors(subschemaErrors) | |||
end | end | ||
table.insert(errors, { | table.insert(errors, { | ||
Line 301: | Line 226: | ||
end | end | ||
if schemaNode.oneOf and value then | if schemaNode.oneOf and value then | ||
local subschemaErrors, validSubschemas = | 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 307: | Line 232: | ||
if not quiet then | if not quiet then | ||
utilsError.warn(msg, { traceBack = false }) | utilsError.warn(msg, { traceBack = false }) | ||
logSubschemaErrors(subschemaErrors) | |||
end | end | ||
table.insert(errors, { | table.insert(errors, { | ||
Line 332: | 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, | 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 369: | 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, | errors = utilsTable.concat(errors, doValidate(schemaNode.items, data, dataName, itemPath, schemaNode, quiet)) | ||
end | end | ||
end | end | ||
Line 375: | 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, | errors = utilsTable.concat(errors, validatePrimitive(schemaNode.keys, k, dataName, dataPath, quiet, true)) | ||
errors = utilsTable.concat(errors, | errors = utilsTable.concat(errors, doValidate(schemaNode.values, data, dataName, keyPath, schemaNode, quiet)) | ||
end | end | ||
end | end | ||
Line 383: | Line 308: | ||
end | end | ||
function | 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 | 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 | ||
end | end | ||
return validate | return validate |
Latest revision as of 22:04, 13 May 2024
validate(schema, schemaName, data, dataName)
Returns
- An array of error paths and messages, or nil if there are none.
Examples
# | Input | Output | Status | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
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",
},
}
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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"]',
},
}
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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]",
},
},
},
},
}
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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",
},
},
},
},
}
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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",
},
}
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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",
},
},
},
},
}
|
-- 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/schema/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