PhantomCaleb (talk | contribs) mNo edit summary |
PhantomCaleb (talk | contribs) No edit summary |
||
(21 intermediate revisions by the same user not shown) | |||
Line 104: | Line 104: | ||
end |
end |
||
if schemaNode.oneOf then |
if schemaNode.oneOf then |
||
− | for |
+ | 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 149: | Line 149: | ||
elseif not ref then |
elseif not ref then |
||
return true |
return true |
||
− | elseif not utilsString.startsWith("#/definitions" |
+ | elseif not utilsString.startsWith(ref, "#/definitions") then |
return false |
return false |
||
else |
else |
||
Line 157: | Line 157: | ||
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 then |
if schema._ref then |
||
− | typeLabel = string.gsub(schema._ref, "#/definitions/", "") |
+ | typeLabel = typeLabel or string.gsub(schema._ref, "#/definitions/", "") |
− | typeLabel = string.gsub(typeLabel, "#", "") |
+ | typeLabel = typeLabel or string.gsub(typeLabel, "#", "") |
showSubkeys = h.showSubkeys(schema) |
showSubkeys = h.showSubkeys(schema) |
||
end |
end |
||
Line 194: | Line 194: | ||
subkeys.oneOf = subkeys.oneOf or {} |
subkeys.oneOf = subkeys.oneOf or {} |
||
local subtypes = {} |
local subtypes = {} |
||
+ | local i = 1 |
||
− | for |
+ | for k, subschema in pairs(schema.oneOf) do |
⚫ | |||
+ | 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 201: | 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 235: | Line 240: | ||
local key = schemaName |
local key = schemaName |
||
if schema.default then |
if schema.default then |
||
− | key = key and string.format(SYMBOLS.default, schemaName, |
+ | key = key and string.format(SYMBOLS.default, schemaName, tostring(schema.default)) |
end |
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 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] |
||
Line 261: | Line 266: | ||
function h.validate(schemaNode, data, dataName, dataPath, parentSchema, quiet) |
function h.validate(schemaNode, data, dataName, dataPath, parentSchema, quiet) |
||
dataPath = dataPath or {} |
dataPath = dataPath or {} |
||
− | local value = utilsTable.property |
+ | local value = utilsTable.property(data, dataPath) |
− | local errPath = dataName .. utilsTable. |
+ | local errPath = dataName .. utilsTable.printPath(dataPath) |
schemaNode = h.resolveReference(schemaNode) |
schemaNode = h.resolveReference(schemaNode) |
||
Line 274: | Line 279: | ||
local invalidSubschemas = utilsTable.keys(subschemaErrors) |
local invalidSubschemas = utilsTable.keys(subschemaErrors) |
||
if #invalidSubschemas > 0 then |
if #invalidSubschemas > 0 then |
||
− | invalidSubschemas = utilsTable.map |
+ | 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 289: | 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, schemaNode) |
+ | 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 304: | Line 309: | ||
end |
end |
||
if #validSubschemas > 1 then |
if #validSubschemas > 1 then |
||
− | validSubschemas = utilsTable.map |
+ | 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 323: | Line 328: | ||
end |
end |
||
if not schemaNode.additionalProperties and not (parentSchema and parentSchema.allOf) then |
if not schemaNode.additionalProperties and not (parentSchema and parentSchema.allOf) then |
||
− | local schemaProps = |
+ | 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 |
+ | local undefinedProps = utilsTable.difference(dataProps, schemaProps) |
if #undefinedProps > 0 then |
if #undefinedProps > 0 then |
||
− | undefinedProps = mw.text.listToText(utilsTable.map |
+ | 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 340: | Line 349: | ||
end |
end |
||
if schemaNode.items and value then |
if schemaNode.items and value then |
||
− | local props = utilsTable. |
+ | local props = utilsTable.stringKeys(value) |
if #props > 0 and not (parentSchema and parentSchema.allOf) then |
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)) |
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 |
if not quiet then |
||
− | utilsError.warn(msg, false) |
+ | utilsError.warn(msg, { traceBack = false }) |
end |
end |
||
table.insert(errors, { |
table.insert(errors, { |
||
Line 358: | Line 367: | ||
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, schemaNode, quiet)) |
errors = utilsTable.concat(errors, h.validate(schemaNode.values, data, dataName, keyPath, schemaNode, quiet)) |
||
Line 367: | Line 376: | ||
end |
end |
||
− | function h.validateSubschemas(subschemas, data, dataName, dataPath, schemaNode) |
+ | function h.validateSubschemas(subschemas, data, dataName, dataPath, schemaNode, parentSchema) |
local errors = {} |
local errors = {} |
||
local valids = {} |
local valids = {} |
||
− | for |
+ | for k, subschema in pairs(subschemas) do |
− | local key = subschema._ref and string.gsub(subschema._ref, "#/definitions/", "") or |
+ | 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) |
local err = h.validate(subschema, data, dataName, dataPath, schemaNode, true) |
||
if #err > 0 then |
if #err > 0 then |
||
Line 381: | 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 .. '"]') |
||
+ | local indent = string.rep(":", #subpath) |
||
for _, err in pairs(schemaErrors) do |
for _, err in pairs(schemaErrors) do |
||
+ | local msg = string.format("%s: %s", code(utilsTable.printPath(subpath)), err.msg) |
||
− | utilsError.warn( |
+ | utilsError.warn(indent .. msg, { traceBack = false }) |
⚫ | |||
+ | h.logSubschemaErrors(err.errors, subpath) |
||
+ | end |
||
end |
end |
||
end |
end |
||
Line 414: | 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 |
+ | 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) |
||
⚫ | |||
+ | errorMessages = {errorMessages} |
||
+ | end |
||
+ | for _, errMsg in ipairs(errorMessages) do |
||
table.insert(errorTbl, { |
table.insert(errorTbl, { |
||
− | path = dataName .. utilsTable. |
+ | path = dataName .. utilsTable.printPath(dataPath), |
msg = errMsg, |
msg = errMsg, |
||
}) |
}) |
||
Line 510: | Line 535: | ||
{ |
{ |
||
name = "enum", |
name = "enum", |
||
⚫ | |||
⚫ | |||
− | + | allOf = { |
|
+ | { |
||
⚫ | |||
⚫ | |||
⚫ | |||
+ | }, |
||
+ | { |
||
+ | type = "record", |
||
+ | properties = { |
||
+ | { |
||
+ | name = "reference", |
||
+ | type = "string", |
||
+ | desc = "A link to a page that lists the accepted values." |
||
+ | } |
||
+ | } |
||
+ | } |
||
+ | } |
||
}, |
}, |
||
}, |
}, |
||
Line 626: | Line 665: | ||
name = "oneOf", |
name = "oneOf", |
||
required = true, |
required = true, |
||
⚫ | |||
⚫ | |||
+ | oneOf = { |
||
⚫ | |||
+ | { |
||
⚫ | |||
+ | type = "array", |
||
+ | items = { _ref = "#/definitions/Schema" }, |
||
+ | }, |
||
+ | { |
||
+ | type = "map", |
||
+ | keys = { type = "string" }, |
||
+ | values = { _ref = "#/definitions/Schema" }, |
||
+ | } |
||
+ | }, |
||
} |
} |
||
}, |
}, |
||
Line 677: | Line 725: | ||
p.Documentation = { |
p.Documentation = { |
||
+ | validate = { |
||
− | { |
||
− | name = "validate", |
||
params = {"schema", "schemaName", "data", "dataName"}, |
params = {"schema", "schemaName", "data", "dataName"}, |
||
returns = "An array of error paths and messages, or nil if there are none.", |
returns = "An array of error paths and messages, or nil if there are none.", |
||
Line 716: | Line 763: | ||
{ |
{ |
||
cost = "50 Rupees", |
cost = "50 Rupees", |
||
− | games = { " |
+ | games = { "YY", "ZZ" }, |
nonsenseField = "foo", |
nonsenseField = "foo", |
||
deprecatedField = "bar", |
deprecatedField = "bar", |
||
Line 724: | Line 771: | ||
expect = { |
expect = { |
||
{ |
{ |
||
− | path = 'itemVariable |
+ | path = 'itemVariable.name', |
− | msg = '<code>itemVariable |
+ | 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 |
+ | path = 'itemVariable.games[2]', |
− | msg = '<code>itemVariable |
+ | msg = '<code>itemVariable.games[2]</code> has unexpected value <code>ZZ</code>. The accepted values are: <code>{"TLoZ", "TAoL", "ALttP"}</code>', |
}, |
}, |
||
{ |
{ |
||
− | path = 'itemVariable |
+ | path = 'itemVariable.cost', |
− | msg = '<code>itemVariable |
+ | msg = '<code>itemVariable.cost</code> is type <code>string</code> but type <code>number</code> was expected.', |
}, |
}, |
||
{ |
{ |
||
− | msg = '<code>itemVariable |
+ | msg = '<code>itemVariable.deprecatedField</code> is deprecated but has value <code>bar</code>.', |
− | path = 'itemVariable |
+ | path = 'itemVariable.deprecatedField', |
}, |
}, |
||
{ |
{ |
||
Line 854: | Line 905: | ||
functions = { |
functions = { |
||
{ |
{ |
||
− | msg = '<code>doc</code> is supposed to be an array only, but it has string keys: { |
+ | msg = '<code>doc</code> is supposed to be an array only, but it has string keys: {"sections"}', |
path = "doc", |
path = "doc", |
||
}, |
}, |
||
Line 916: | Line 967: | ||
{ |
{ |
||
{ |
{ |
||
− | path = |
+ | path = "arg.foo", |
− | msg = '<code>arg |
+ | msg = '<code>arg.foo</code> is type <code>number</code> but type <code>string</code> was expected.', |
}, |
}, |
||
}, |
}, |
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
# | 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",
},
},
},
},
}
|
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