PhantomCaleb (talk | contribs) (undefined params) |
PhantomCaleb (talk | contribs) (repeatedGroup) |
||
Line 15: | Line 15: | ||
local args = {} |
local args = {} |
||
local unknownParams = utilsTable.clone(frameArgs) |
local unknownParams = utilsTable.clone(frameArgs) |
||
+ | |||
+ | local repeatedParams = templateSpec.repeatedGroup and templateSpec.repeatedGroup.params or {} |
||
+ | local repeatedParamsMap = utilsTable.invert(repeatedParams) |
||
+ | local isRepeated = h.isRepeated(repeatedParamsMap) |
||
+ | |||
local err = { |
local err = { |
||
args = {}, |
args = {}, |
||
Line 20: | Line 25: | ||
} |
} |
||
for k, v in pairs(templateSpec.params) do |
for k, v in pairs(templateSpec.params) do |
||
+ | if not repeatedParamsMap[k] then |
||
− | args[v.name or k] = h.parseArg(frameArgs[k], v) |
+ | args[v.name or k] = h.parseArg(frameArgs[k], v) |
− | unknownParams[k] = nil |
+ | unknownParams[k] = nil |
+ | end |
||
+ | end |
||
+ | -- Process any repeatedGroup params |
||
+ | local repeated = templateSpec.repeatedGroup and templateSpec.repeatedGroup.name |
||
+ | if repeated then |
||
+ | for k, v in pairs(unknownParams) do |
||
+ | local isRepeated, index, param = isRepeated(k) |
||
+ | if isRepeated then |
||
+ | local paramSpec = templateSpec.params[param] |
||
+ | args[repeated] = args[repeated] or {} |
||
+ | args[repeated][index] = args[repeated][index] or {} |
||
+ | utilsTable.merge(args[repeated][index], { |
||
+ | [param] = h.parseArg(v, paramSpec) |
||
+ | }) |
||
+ | unknownParams[k] = nil |
||
+ | end |
||
+ | end |
||
+ | if templateSpec.repeatedGroup.compact then |
||
+ | args[repeated] = utilsTable.compact(args[repeated]) |
||
+ | end |
||
end |
end |
||
+ | |||
local variadicParam = templateSpec.params["..."] |
local variadicParam = templateSpec.params["..."] |
||
if variadicParam then |
if variadicParam then |
||
Line 62: | Line 89: | ||
if err then |
if err then |
||
return s("cat.invalidArgs"), err |
return s("cat.invalidArgs"), err |
||
+ | end |
||
+ | end |
||
+ | |||
+ | function h.isRepeated(repeatedParamsMap) |
||
+ | -- @param param a template parameter e.g. "tab1" |
||
+ | -- @return boolean indicating whether parameter is part of a repeated group |
||
+ | -- @return number index of the repition, e.g. 1 for "tab1", 2 for "tab2", etc. |
||
+ | -- @return name of the parameter without the index, e.g. "tab" for "tab1", "tab2", etc. |
||
+ | return function(param) |
||
+ | if type(param) == "number" then |
||
+ | return false |
||
+ | end |
||
+ | local name = utilsString.trim(param, "0-9") |
||
+ | local index = tonumber(string.sub(param, #name + 1)) |
||
+ | if not repeatedParamsMap[name] or not index then |
||
+ | return false |
||
+ | end |
||
+ | return true, index, name |
||
end |
end |
||
end |
end |
||
Line 490: | Line 535: | ||
}, |
}, |
||
}, |
}, |
||
+ | { |
||
+ | desc = "repeatedGroup", |
||
+ | snippet = "RepeatedGroup", |
||
+ | expect = { |
||
+ | { |
||
+ | tabs = { |
||
+ | { |
||
+ | tab = "Tab 1", |
||
+ | content = "Content 1", |
||
+ | }, |
||
+ | { |
||
+ | tab = "Tab 2", |
||
+ | content = "Content 2", |
||
+ | }, |
||
+ | [4] = { tab = "Tab 4" }, |
||
+ | [5] = { content = "Content 5" }, |
||
+ | } |
||
+ | }, |
||
+ | nil |
||
+ | } |
||
+ | }, |
||
+ | { |
||
+ | desc = "repeatedGroup - if <code>compact</code> is set to true, [[Module:UtilsTable#compact|utilsTable.compact]] is applied to the resulting array.", |
||
+ | snippet = "RepeatedGroupCompact", |
||
+ | expect = { |
||
+ | { |
||
+ | tabs = { |
||
+ | { |
||
+ | tab = "Tab 1", |
||
+ | content = "Content 1", |
||
+ | }, |
||
+ | { |
||
+ | tab = "Tab 3", |
||
+ | content = "Content 3", |
||
+ | }, |
||
+ | }, |
||
+ | }, |
||
+ | nil |
||
+ | } |
||
+ | } |
||
}, |
}, |
||
}, |
}, |
Revision as of 11:53, 27 June 2020
This module takes care of parsing and validating template input, allowing module developers to focus on core logic. It aims to achieve the same goal as Wikipedia's Module:Arguments but with a different approach:
- Validation is based on a schema that is also used to auto-generate documentation. This guarantees that the documentation is up to date with the validation code.
- No implicit behaviour. If you want to trim a parameter, you specify
trim = true
. If you want to treat empty strings as nil, you specifynilIfEmpty = true
. Module:Arguments does several things "automagically" by default and lacks clarity as a result. - No frame handling. It's up to the caller to consolidate
frame.args
andframe:getParent().args
if needed. This can be done with utilsTable.merge. Most modules typically need only one frame anyway.
Lua error in Module:Documentation/Module at line 785: bad argument #1 to 'unpack' (table expected, got nil).
local p = {}
local h = {}
local i18n = require("Module:I18n")
local s = i18n.getString
local utilsError = require("Module:UtilsError")
local utilsSchema = require("Module:UtilsSchema")
local utilsString = require("Module:UtilsString")
local utilsTable = require("Module:UtilsTable")
local utilsValidate = require("Module:UtilsValidate")
local VALIDATORS = {"required", "enum", "deprecated"}
function p.parse(frameArgs, templateSpec)
local args = {}
local unknownParams = utilsTable.clone(frameArgs)
local repeatedParams = templateSpec.repeatedGroup and templateSpec.repeatedGroup.params or {}
local repeatedParamsMap = utilsTable.invert(repeatedParams)
local isRepeated = h.isRepeated(repeatedParamsMap)
local err = {
args = {},
categories = {},
}
for k, v in pairs(templateSpec.params) do
if not repeatedParamsMap[k] then
args[v.name or k] = h.parseArg(frameArgs[k], v)
unknownParams[k] = nil
end
end
-- Process any repeatedGroup params
local repeated = templateSpec.repeatedGroup and templateSpec.repeatedGroup.name
if repeated then
for k, v in pairs(unknownParams) do
local isRepeated, index, param = isRepeated(k)
if isRepeated then
local paramSpec = templateSpec.params[param]
args[repeated] = args[repeated] or {}
args[repeated][index] = args[repeated][index] or {}
utilsTable.merge(args[repeated][index], {
[param] = h.parseArg(v, paramSpec)
})
unknownParams[k] = nil
end
end
if templateSpec.repeatedGroup.compact then
args[repeated] = utilsTable.compact(args[repeated])
end
end
local variadicParam = templateSpec.params["..."]
if variadicParam then
args[variadicParam.name] = {}
local i = #templateSpec.params + 1
while frameArgs[i] do
local varArg = h.parseArg(frameArgs[i], variadicParam)
if varArg then
table.insert(args[variadicParam.name], varArg)
end
unknownParams[i] = nil
i = i + 1
end
end
for k, v in pairs(templateSpec.params) do
local argErrors, errorCategories = h.validate(args[v.name or k], v, v.name or k, args)
if #argErrors > 0 then
err.args[v.name or k] = utilsTable.concat(err.args[v.name or k] or {}, argErrors)
end
err.categories = utilsTable.concat(err.categories, errorCategories)
end
for k in pairs(unknownParams) do
local errMsg = s("msg.unknownParam", { param = k })
utilsError.warn(errMsg)
err.args[k] = {{
category = s("cat.unknownParams"),
message = errMsg
}}
err.categories = utilsTable.concat(err.categories, s("cat.unknownParams"))
end
if #err.categories == 0 then
err = nil
end
return args, err
end
function p.schemaValidate(schema, schemaName, arg, name)
local err = utilsSchema.validate(schema, schemaName, arg, name)
if err then
return s("cat.invalidArgs"), err
end
end
function h.isRepeated(repeatedParamsMap)
-- @param param a template parameter e.g. "tab1"
-- @return boolean indicating whether parameter is part of a repeated group
-- @return number index of the repition, e.g. 1 for "tab1", 2 for "tab2", etc.
-- @return name of the parameter without the index, e.g. "tab" for "tab1", "tab2", etc.
return function(param)
if type(param) == "number" then
return false
end
local name = utilsString.trim(param, "0-9")
local index = tonumber(string.sub(param, #name + 1))
if not repeatedParamsMap[name] or not index then
return false
end
return true, index, name
end
end
function h.parseArg(arg, param)
if arg and param.trim then
arg = utilsString.trim(arg)
end
if arg and param.split then
local pattern = type(param.split) == "string" and param.split or nil
arg = utilsString.split(arg, pattern)
end
if param.nilIfEmpty then
arg = utilsString.nilIfEmpty(arg)
end
return arg
end
function h.validate(arg, param, paramName, args)
local errors = {}
local categories = {}
for i, validator in ipairs(VALIDATORS) do
local validatorData = param[validator]
if validator == "enum" and param.enum then
local enum = param.enum
if type(param.enum) == "function" then
local dependency = args[param.enumDependsOn]
enum = dependency and enum(dependency)
end
validatorData = enum
end
local errorMessages, cat
if validatorData ~= nil then
errorMessages, cat = h[validator](validatorData, arg, paramName)
end
for _, err in ipairs(errorMessages or {}) do
table.insert(errors, {
msg = err,
category = cat
})
table.insert(categories, cat)
end
end
return errors, categories
end
function h.required(required, value, name)
if not required then
return
end
local err = utilsValidate.required(value, name)
if err then
return {err}, s("cat.invalidArgs")
end
end
function h.enum(enum, value, name)
if not enum then
return
end
local err = utilsValidate._enum(enum)(value, name)
if err then
return err, s("cat.invalidArgs")
end
end
function h.deprecated(deprecated, value, name)
if not deprecated then
return
end
local err = utilsValidate.deprecated(value, name)
if err then
return {err}, s("cat.deprecatedArgs")
end
end
i18n.loadStrings({
en = {
cat = {
invalidArgs = "Category:Pages with Invalid Arguments",
deprecatedArgs = "Category:Pages with Deprecated Arguments",
unknownParams = "Category:Pages using Unknown Parameters",
},
msg = {
unknownParam = "No such parameter <code>${param}</code> is defined for this template."
}
},
})
p.Schemas = {
parse = {
frameArgs = {
type = "any",
required = true,
desc = "Table of arguments obtained from {{Scribunto Manual|lib=Frame object|frame object}}.",
},
templateSpec = {
type = "any",
required = true,
desc = "[[Module:Documentation#Templates|Template documentation object]].",
}
}
}
p.Documentation = {
parse = {
desc = "This function validates template input and parses it into a table for use in the rest of the module.",
params = {"frameArgs", "templateSpec"},
returns = {
"A table of arguments parsed from the template input.",
"A table of validation errors, or nil if there are none. The error messages are also logged using {{Scribunto Manual|lib=mw.addWarning}}.",
},
cases = {
outputOnly = true,
{
desc = "Positional arguments are assigned to their names.",
snippet = "PositionalAndNamedArgs",
expect = {
{
game = "OoT",
page = "Boss Key",
},
nil
}
},
{
desc = "Special parameter <code>...</code> is used to parse an array of trailing template arguments",
snippet = "TrailingArgs",
expect = {
{
games = {"OoT", "MM", "TWW", "TP"}
},
nil
}
},
{
desc = "<code>...</code> used with other positional args",
snippet = "TrailingArgsWithPositionalArgs",
expect = {
{
foo = "foo",
bar = "bar",
games = {"OoT", "MM", "TWW", "TP"},
},
nil
}
},
{
desc = "Validation of required arguments.",
snippet = "RequiredArgs",
expect = {
{
page = "Boss Key"
},
{
categories = {"Category:Pages with Invalid Arguments"},
args = {
game = {
{
category = "Category:Pages with Invalid Arguments",
msg = "<code>game</code> is required but is <code>nil</code>.",
},
},
},
},
}
},
{
desc = "Validation of deprecated arguments.",
snippet = "Deprecated",
expect = {
{ oldArg = "foo" },
{
categories = {"Category:Pages with Deprecated Arguments"},
args = {
oldArg = {
{
category = "Category:Pages with Deprecated Arguments",
msg = "<code>oldArg</code> is deprecated but has value <code>foo</code>.",
},
},
},
},
}
},
{
desc = "Using an unknown parameter counts as an error.",
snippet = "Unkown",
expect = {
{},
{
categories = {"Category:Pages using Unknown Parameters"},
args = {
foo = {
{
message = "No such parameter <code>foo</code> is defined for this template.",
category = "Category:Pages using Unknown Parameters",
},
},
},
}
},
},
{
desc = "<code>trim</code> can be set on a parameter so that [[Module:UtilsString#trim|utilsString.trim]] is called for the argument.",
snippet = "Trim",
expect = {{ someParam = "foo" }}
},
{
desc = "<code>nilIfEmpty</code> can be set on a parameter so that [[Module:UtilsString#nilIfEmpty|utilsString.nilIfEmpty]] is called for the argument.",
snippet = "NilIfEmpty",
expect = {{}, nil}
},
{
desc = "<code>split</code> can be set on a parameter so that [[Module:UtilsString#split|utilsString.split]] is called for the argument.",
snippet = "Split",
expect = {
{ foo = {"a", "b", "c"} },
},
},
{
desc = "<code>split</code> using a custom splitting pattern",
snippet = "SplitPattern",
expect = {
{ foo = {"a", "b", "c"} },
},
},
{
desc = "If <code>nilIfEmpty</code> and <code>required</code> are set, then the argument is invalid if it is an empty string.",
snippet = "NilIfEmptyWithRequiredArgs",
expect = {
{},
{
categories = {"Category:Pages with Invalid Arguments"},
args = {
game = {
{
category = "Category:Pages with Invalid Arguments",
msg = "<code>game</code> is required but is <code>nil</code>.",
},
},
},
},
},
},
{
desc = "If <code>trim</code>, <code>nilIfEmpty</code>, and <code>required</code> are set, then the argument is invalid if it is a blank string.",
snippet = "TrimNilIfEmptyRequired",
expect = {
{},
{
categories = {"Category:Pages with Invalid Arguments"},
args = {
game = {
{
category = "Category:Pages with Invalid Arguments",
msg = "<code>game</code> is required but is <code>nil</code>.",
},
},
},
},
},
},
{
desc = "<code>enum</code> validation.",
snippet = "Enum",
expect = {
{
triforce2 = "Limpah",
game = "ALttZ",
triforce1 = "Kooloo",
},
{
categories = {
"Category:Pages with Invalid Arguments",
"Category:Pages with Invalid Arguments",
"Category:Pages with Invalid Arguments",
},
args = {
triforce2 = {
{
category = "Category:Pages with Invalid Arguments",
msg = "<code>triforce2</code> has unexpected value <code>Limpah</code>. For a list of accepted values, refer to [[Triforce]].",
},
},
game = {
{
category = "Category:Pages with Invalid Arguments",
msg = "<code>game</code> has unexpected value <code>ALttZ</code>. For a list of accepted values, refer to [[Data:Franchise]].",
},
},
triforce1 = {
{
category = "Category:Pages with Invalid Arguments",
msg = '<code>triforce1</code> has unexpected value <code>Kooloo</code>. The accepted values are: <code>{"Courage", "Power", "Wisdom"}</code>',
},
},
},
},
},
},
{
desc = "<code>split</code> is used to parse comma-separated strings as arrays. Each array item can be validated against an <code>enum</code>.",
snippet = "SplitEnum",
expect = {
{
games = {"OoT", "fakeGame", "BotW"},
},
{
categories = {"Category:Pages with Invalid Arguments"},
args = {
games = {
{
category = "Category:Pages with Invalid Arguments",
msg = "<code>games[2]</code> has unexpected value <code>fakeGame</code>. For a list of accepted values, refer to [[Data:Franchise]].",
}
}
}
}
}
},
{
desc = "<code>enum</code> can be written as a function, when the list of acceptable values depends on the value of another argument.",
snippet = "EnumDependsOn",
expect = {
{
term = "Dinolfos",
game = "TP"
},
{
categories = {"Category:Pages with Invalid Arguments"},
args = {
term = {
{
category = "Category:Pages with Invalid Arguments",
msg = '<code>term</code> has unexpected value <code>Dinolfos</code>. The accepted values are: <code>{"Dynalfos"}</code>',
},
},
},
},
},
},
{
desc = "If <code>enumDependsOn</code> refers to a required parameter, then <code>enum</code> is not evaluated when that parameter is nil.",
snippet = "EnumDependsOnNil",
expect = {
{ term = "Dinolfos" },
{
categories = {"Category:Pages with Invalid Arguments"},
args = {
game = {
{
category = "Category:Pages with Invalid Arguments",
msg = "<code>game</code> is required but is <code>nil</code>.",
}
},
},
},
},
},
{
desc = "Altogether now",
snippet = "TermStorePass",
expect = {
{
term = "Dinolfos",
games = {"OoT", "MM"},
},
nil
}
},
{
snippet = "TermStoreFail",
expect = {
{
plural = "true",
games = {"YY", "ZZ"},
},
{
categories = {
"Category:Pages with Invalid Arguments",
"Category:Pages with Deprecated Arguments",
"Category:Pages with Invalid Arguments",
"Category:Pages with Invalid Arguments",
},
args = {
term = {
{
category = "Category:Pages with Invalid Arguments",
msg = "<code>term</code> is required but is <code>nil</code>.",
},
},
games = {
{
category = "Category:Pages with Invalid Arguments",
msg = "<code>games[1]</code> has unexpected value <code>YY</code>. For a list of accepted values, refer to [[Data:Franchise]]."
},
{
category = "Category:Pages with Invalid Arguments",
msg = "<code>games[2]</code> has unexpected value <code>ZZ</code>. For a list of accepted values, refer to [[Data:Franchise]]."
},
},
plural = {
{
category = "Category:Pages with Deprecated Arguments",
msg = "<code>plural</code> is deprecated but has value <code>true</code>.",
},
},
},
},
},
},
{
desc = "<code>trim</code>, <code>nilIfEmpty</code>, and validators such as <code>enum</code> are applied to individual trailing arguments",
snippet = "TrailingArgsStringTrimNilIfEmptyEnum",
expect = {
{
games = {"OoT", "MM", "ALttZ"},
},
{
categories = {"Category:Pages with Invalid Arguments"},
args = {
games = {
{
category = "Category:Pages with Invalid Arguments",
msg = "<code>games[3]</code> has unexpected value <code>ALttZ</code>. For a list of accepted values, refer to [[Data:Franchise]].",
}
},
},
},
},
},
{
desc = "repeatedGroup",
snippet = "RepeatedGroup",
expect = {
{
tabs = {
{
tab = "Tab 1",
content = "Content 1",
},
{
tab = "Tab 2",
content = "Content 2",
},
[4] = { tab = "Tab 4" },
[5] = { content = "Content 5" },
}
},
nil
}
},
{
desc = "repeatedGroup - if <code>compact</code> is set to true, [[Module:UtilsTable#compact|utilsTable.compact]] is applied to the resulting array.",
snippet = "RepeatedGroupCompact",
expect = {
{
tabs = {
{
tab = "Tab 1",
content = "Content 1",
},
{
tab = "Tab 3",
content = "Content 3",
},
},
},
nil
}
}
},
},
schemaValidate = {
notoc = true,
desc = "This function validates an input argument against a [[Module:Schema|schema]]. Currently, this function is not performant enough to be used in actual articles. It exists mainly to assist with building [[Module:Documentation|documentation]] and possibly as a debugging tool.",
params = {"schema", "schemaName", "arg", "argName"},
returns = {
"If argument is invalid against the schema, returns the name of an [[:Category:Pages with Invalid Arguments|error category]], else returns <code>nil</code>.",
"A table of the validation errors logged using {{Scribunto Manual|lib=mw.addWarning}}, or <code>nil</code> if there were none."
},
cases = {
outputOnly = true,
{
desc = "Fails schema validation.",
snippet = "Fails",
expect = {
s("cat.invalidArgs"),
{
{
path = "magicWords",
msg = "<code>magicWords</code> does not match any <code>oneOf</code> sub-schemas.",
errors = {
{
{
msg = "<code>magicWords</code> is type <code>table</code> but type <code>string</code> was expected.",
path = "magicWords",
},
},
{
{
msg = '<code>magicWords[1]</code> has unexpected value <code>Alakazam</code>. The accepted values are: <code>{"Kooloo", "Limpah"}</code>',
path = "magicWords[1]",
},
},
},
},
},
},
},
{
desc = "Passes schema validation.",
snippet = "Passes",
expect = {nil, nil},
},
}
},
}
return p