Zelda Wiki

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

Come join the Zelda Wiki community Discord server!

READ MORE

Zelda Wiki
(+defaults)
No edit summary
 
(31 intermediate revisions by 2 users not shown)
Line 1: Line 1:
 
local p = {}
 
local p = {}
 
local h = {}
 
local h = {}
  +
local validators = {}
   
local i18n = require("Module:I18n")
+
local Constants = mw.loadData("Module:Constants/Data")
  +
local utilsPackage = require("Module:UtilsPackage")
local s = i18n.getString
 
local utilsError = require("Module:UtilsError")
 
local utilsSchema = require("Module:UtilsSchema")
 
 
local utilsString = require("Module:UtilsString")
 
local utilsString = require("Module:UtilsString")
 
local utilsTable = require("Module:UtilsTable")
 
local utilsTable = require("Module:UtilsTable")
local utilsValidate = require("Module:UtilsValidate")
+
local utilsVar = require("Module:UtilsVar")
   
  +
-- lazy-load so it only gets linked when needed - most pages won't have errors
local VALIDATORS = {"required", "enum", "deprecated", "type"}
 
  +
local _utilsError = utilsPackage.lazyLoad("Module:UtilsError")
local NOT_A_NUMBER = "NaN"
 
  +
  +
local CAT_DEPRECATED_PARAMS = "Category:"..Constants.category.deprecatedParams
  +
local CAT_INVALID_ARGS = "Category:"..Constants.category.invalidArgs
  +
  +
local parentFrame = mw.getCurrentFrame() and mw.getCurrentFrame():getParent()
  +
if parentFrame then
  +
h.templatePage = parentFrame:getTitle()
  +
h.instanceCounter = utilsVar.counter("instanceCounter"..h.templatePage) -- [[Module:UtilsError]] uses this too
  +
h.instanceCounter.increment()
  +
end
   
 
function p.parse(frameArgs, templateSpec)
 
function p.parse(frameArgs, templateSpec)
Line 25: Line 34:
 
categories = {},
 
categories = {},
 
}
 
}
  +
 
-- Parse ordinary args
 
-- Parse ordinary args
 
for k, v in pairs(templateSpec.params) do
 
for k, v in pairs(templateSpec.params) do
Line 30: Line 40:
 
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
  +
for i, alias in ipairs(v.aliases or {}) do
  +
args[alias] = h.parseArg(frameArgs[alias], v)
  +
args[v.name or k] = args[v.name or k] or args[alias] -- if both the parameter and its alias is used, the alias should not overrid
  +
unknownParams[alias] = nil
  +
end
 
end
 
end
 
end
 
end
Line 41: Line 56:
 
local paramSpec = templateSpec.params[param]
 
local paramSpec = templateSpec.params[param]
 
args[repeated] = args[repeated] or {}
 
args[repeated] = args[repeated] or {}
  +
local arg = h.parseArg(v, paramSpec)
args[repeated][index] = args[repeated][index] or {}
 
  +
if arg then
utilsTable.merge(args[repeated][index], {
 
  +
args[repeated][index] = args[repeated][index] or {}
[param] = h.parseArg(v, paramSpec)
 
  +
args[repeated][index][param] = arg
})
 
  +
end
 
unknownParams[k] = nil
 
unknownParams[k] = nil
 
end
 
end
 
end
 
end
args[repeated] = utilsTable.compact(args[repeated])
+
args[repeated] = args[repeated] and utilsTable.compact(args[repeated]) or {} -- in case a number is accidentally skipped for a whole "row"
 
end
 
end
 
 
Line 54: Line 70:
 
local variadicParam = templateSpec.params["..."]
 
local variadicParam = templateSpec.params["..."]
 
if variadicParam then
 
if variadicParam then
args[variadicParam.name] = {}
 
 
local i = #templateSpec.params + 1
 
local i = #templateSpec.params + 1
 
while frameArgs[i] do
 
while frameArgs[i] do
 
local varArg = h.parseArg(frameArgs[i], variadicParam)
 
local varArg = h.parseArg(frameArgs[i], variadicParam)
 
if varArg then
 
if varArg then
  +
args[variadicParam.name] = args[variadicParam.name] or {}
 
table.insert(args[variadicParam.name], varArg)
 
table.insert(args[variadicParam.name], varArg)
 
end
 
end
Line 67: Line 83:
 
 
 
-- Validate
 
-- Validate
for k, v in pairs(templateSpec.params) do
+
for paramKey, paramSpec in pairs(templateSpec.params) do
  +
if not repeatedParamsMap[paramKey] then -- repeated args are an edge case for validation and need to be handled separately
local argErrors, errorCategories = h.validate(args[v.name or k], v, v.name or k, args)
 
  +
local paramName = paramSpec.name or paramKey
if #argErrors > 0 then
 
  +
local paramValue = args[paramName]
err.args[v.name or k] = utilsTable.concat(err.args[v.name or k] or {}, argErrors)
 
  +
h.validateAndAddErrors(err, args, paramSpec, paramName, paramValue)
  +
else
  +
for i in ipairs(args[repeated] or {}) do
  +
local paramName = paramKey..i -- no need to check paramSpec.name as repeated params should always be named arguments
  +
local paramValue = args[repeated][i][paramKey]
  +
h.validateAndAddErrors(err, args, paramSpec, paramName, paramValue)
  +
end
 
end
 
end
err.categories = utilsTable.concat(err.categories, errorCategories)
 
 
end
 
end
 
 
Line 84: Line 106:
 
 
 
-- Handle any args left that don't have corresponding params defined
 
-- Handle any args left that don't have corresponding params defined
for k in pairs(unknownParams) do
+
for k, v in pairs(unknownParams) do
  +
-- value is not strictly necessary but can make it easier to search for the invalid usage when the template is used several times on a page
local errMsg = s("msg.unknownParam", { param = k })
 
  +
local errMsg = string.format("No such parameter <code>%s</code> is defined for this template. Value: <code>%s</code>", k, v)
utilsError.warn(errMsg)
 
  +
h.warn(errMsg)
 
err.args[k] = {{
 
err.args[k] = {{
category = s("cat.unknownParams"),
+
category = "Category:Articles Using Unknown Parameters in Template Calls",
 
message = errMsg
 
message = errMsg
 
}}
 
}}
err.categories = utilsTable.concat(err.categories, s("cat.unknownParams"))
+
err.categories = utilsTable.concat(err.categories, "Category:Articles Using Unknown Parameters in Template Calls")
 
end
 
end
 
if #err.categories == 0 then
 
if #err.categories == 0 then
 
err = nil
 
err = nil
  +
end
  +
if err and mw.title.getCurrentTitle().nsText == "User" then
  +
err.categories = {}
  +
end
  +
if err then
  +
local categoryText = ""
  +
local categories = utilsTable.unique(err.categories)
  +
for i, cat in ipairs(categories) do
  +
categoryText = categoryText.."[["..cat.."]]"
  +
end
  +
err.categoryText = categoryText
 
end
 
end
 
return args, err
 
return args, err
 
end
 
end
   
function p.schemaValidate(schema, schemaName, arg, name)
+
function p.enum(enum, value, name)
local err = utilsSchema.validate(schema, schemaName, arg, name)
+
local err, category = validators.enum(enum, value, name)
if err then
+
if not err or #err == 0 then
return s("cat.invalidArgs"), err
+
return nil
  +
else
  +
return {
  +
messages = err,
  +
category = category,
  +
}
 
end
 
end
  +
end
  +
  +
function h.warn(errMessage)
  +
_utilsError().warn(errMessage)
 
end
 
end
   
Line 127: Line 170:
 
if arg and param.trim then
 
if arg and param.trim then
 
arg = utilsString.trim(arg)
 
arg = utilsString.trim(arg)
  +
end
  +
if arg and param.nilIfEmpty then
  +
arg = utilsString.nilIfEmpty(arg)
 
end
 
end
 
if arg and param.split then
 
if arg and param.split then
Line 132: Line 178:
 
arg = utilsString.split(arg, pattern)
 
arg = utilsString.split(arg, pattern)
 
end
 
end
if param.nilIfEmpty then
+
arg = arg or param.default
arg = utilsString.nilIfEmpty(arg)
 
end
 
 
if param.type == "number" then
 
if param.type == "number" then
arg = tonumber(arg) or param.default or NOT_A_NUMBER
+
local num = tonumber(arg)
  +
if num then
  +
arg = num
  +
end
 
end
 
end
arg = arg or param.default
 
 
return arg
 
return arg
  +
end
  +
  +
function h.validateAndAddErrors(err, args, paramSpec, paramName, paramValue)
  +
local argErrors, errorCategories = h.validate(paramValue, paramSpec, paramName, args)
  +
if #argErrors > 0 then
  +
err.args[paramName] = utilsTable.concat(err.args[paramName] or {}, argErrors)
  +
end
  +
err.categories = utilsTable.concat(err.categories, errorCategories)
 
end
 
end
   
Line 145: Line 199:
 
local errors = {}
 
local errors = {}
 
local categories = {}
 
local categories = {}
  +
local validatorNames = {"required", "deprecated", "enum", "type"} -- do validation in this order
for i, validator in ipairs(VALIDATORS) do
 
  +
for i, validatorName in ipairs(validatorNames) do
local validatorData = param[validator]
 
  +
local validatorData = param[validatorName]
if validator == "enum" and param.enum then
 
  +
if validatorName == "enum" and param.enum then
 
local enum = param.enum
 
local enum = param.enum
 
if type(param.enum) == "function" then
 
if type(param.enum) == "function" then
Line 154: Line 209:
 
end
 
end
 
validatorData = enum
 
validatorData = enum
  +
end
  +
if validatorName == "type" and validatorData and arg == nil then
  +
validatorData = nil -- we shouldn't do type validation if arg is nil and not required
 
end
 
end
 
local errorMessages, defaultCat
 
local errorMessages, defaultCat
 
if validatorData ~= nil then
 
if validatorData ~= nil then
errorMessages, defaultCat = h[validator](validatorData, arg, paramName)
+
errorMessages, defaultCat = validators[validatorName](validatorData, arg, paramName)
 
end
 
end
  +
-- Here we allow custom categories so that editors can do targeted maintenance on a specific template parameter
  +
-- For example, we deprecate a parameter and use a custom category to clean up all the pages that use that parameter
 
local cat = defaultCat
 
local cat = defaultCat
if validator == "required" and type(validatorData) == "string" then
+
if (validatorName == "required" or validatorName == "deprecated") and type(validatorData) == "string" then
 
cat = validatorData
 
cat = validatorData
 
end
 
end
for _, err in ipairs(errorMessages or {}) do
+
if errorMessages then
  +
for _, err in ipairs(errorMessages) do
table.insert(errors, {
 
  +
table.insert(errors, {
msg = err,
 
category = cat
+
msg = err,
  +
category = cat
})
 
  +
})
table.insert(categories, cat)
 
  +
table.insert(categories, cat)
  +
end
  +
break -- run only one validator for now as there isn't yet any situtation where it makes sense to run several
 
end
 
end
 
end
 
end
 
return errors, categories
 
return errors, categories
 
end
 
end
  +
function h.required(required, value, name)
 
  +
function validators.required(required, value, name)
 
if not required then
 
if not required then
 
return
 
return
 
end
 
end
  +
if value == nil then
local err = utilsValidate.required(value, name)
 
  +
local err = string.format("<code>%s</code> parameter is required.", name)
if err then
 
  +
h.warn(err)
return {err}, s("cat.invalidArgs")
 
  +
return {err}, CAT_INVALID_ARGS
 
end
 
end
 
end
 
end
function h.enum(enum, value, name)
+
function validators.enum(enum, value, name)
 
if not enum then
 
if not enum then
 
return
 
return
 
end
 
end
  +
-- Sometimes `value` is an "array", sometimes it's just a primitive value
local err = utilsValidate._enum(enum)(value, name)
 
  +
-- We can simplify the code by folding the latter case into the former
if err then
 
  +
-- i.e. making `value` always an array
return err, s("cat.invalidArgs")
 
  +
local isMultiValue = type(value) == "table"
  +
if not isMultiValue then
  +
value = { value }
  +
end
  +
  +
local errors = {}
  +
for k, v in ipairs(value) do
  +
if not utilsTable.keyOf(enum, v) then
  +
local path = name
  +
if isMultiValue then
  +
path = path .. string.format("[%s]", k)
  +
end
  +
local msg
  +
if enum.reference then
  +
msg = string.format("<code>%s</code> has unexpected value <code>%s</code>. For a list of accepted values, refer to %s.", path, v, enum.reference)
  +
else
  +
local acceptedValues = utilsTable.print(enum, true)
  +
msg = string.format("<code>%s</code> has unexpected value <code>%s</code>. The accepted values are: <code>%s</code>", path, v, acceptedValues)
  +
end
  +
table.insert(errors, msg)
  +
h.warn(msg)
  +
end
 
end
 
end
  +
return errors, CAT_INVALID_ARGS
 
end
 
end
function h.deprecated(deprecated, value, name)
+
function validators.deprecated(deprecated, value, name)
 
if not deprecated then
 
if not deprecated then
 
return
 
return
 
end
 
end
  +
if value ~= nil then
local err = utilsValidate.deprecated(value, name)
 
if err then
+
if type(value) == "table" then
  +
value = utilsTable.print(value, true)
return {err}, s("cat.deprecatedArgs")
 
  +
end
  +
local err = string.format("<code>%s</code> is deprecated but has value <code>%s</code>.", name, value)
  +
h.warn(err)
  +
return {err}, CAT_DEPRECATED_PARAMS
 
end
 
end
 
end
 
end
function h.type(expectedType, value, name)
+
function validators.type(expectedType, value, name)
if expectedType == "number" and value == NOT_A_NUMBER then
+
if expectedType == "number" and tonumber(value) == nil then
 
local msg = "<code>" .. name .. "</code> is expected to be a number but was: <code>" .. utilsTable.print(value) .. "</code>"
 
local msg = "<code>" .. name .. "</code> is expected to be a number but was: <code>" .. utilsTable.print(value) .. "</code>"
utilsError.warn(msg)
+
h.warn(msg)
return {msg}, s("cat.invalidArgs")
+
return {msg}, CAT_INVALID_ARGS
 
end
 
end
 
end
 
end
   
  +
-- See [[Module:Arguments#store]]
i18n.loadStrings({
 
  +
function p.store(transclusion)
en = {
 
  +
local frame = mw.getCurrentFrame()
cat = {
 
  +
local isValid = transclusion.isValid
invalidArgs = "Category:Pages with Invalid Arguments",
 
  +
if isValid ~= nil then
deprecatedArgs = "Category:Pages with Deprecated Arguments",
 
  +
isValid = isValid and "1" or "0"
unknownParams = "Category:Pages using Unknown Parameters",
 
  +
end
},
 
  +
for k, v in pairs(transclusion.args) do
msg = {
 
  +
frame:expandTemplate({
unknownParam = "No such parameter <code>${param}</code> is defined for this template."
 
  +
title = "Arguments/Store",
}
 
  +
args = {
},
 
  +
module = transclusion.module or frame:getTitle(),
})
 
  +
template = frame:getParent():getTitle(),
  +
pageInstance = h.instanceCounter and h.instanceCounter.value(),
  +
parameter = tostring(k),
  +
argument = tostring(v),
  +
isValid = isValid,
  +
}
  +
})
  +
end
  +
end
   
p.Schemas = {
+
function p.Schemas()
parse = {
+
return {
frameArgs = {
+
parse = {
type = "any",
+
frameArgs = {
required = true,
+
type = "any",
  +
required = true,
desc = "Table of arguments obtained from {{Scribunto Manual|lib=Frame object|frame object}}.",
 
  +
desc = "Table of arguments obtained from {{Scribunto Manual|lib=Frame object|frame object}}.",
},
 
  +
},
templateSpec = {
 
type = "any",
+
templateSpec = {
required = true,
+
type = "any",
  +
required = true,
desc = "[[Module:Documentation#Templates|Template documentation object]].",
 
  +
desc = "[[Module:Documentation#Templates|Template documentation object]].",
  +
}
 
}
 
}
 
}
 
}
  +
end
}
 
   
p.Documentation = {
+
function p.Documentation()
parse = {
+
return {
  +
parse = {
desc = "This function validates template input and parses it into a table for use in the rest of the module.",
 
  +
desc = "This function validates template input and parses it into a table for use in the rest of the module.",
params = {"frameArgs", "templateSpec"},
 
  +
params = {"frameArgs", "templateSpec"},
returns = {
 
  +
returns = {
"A table of arguments parsed from the template input.",
 
  +
"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}}.",
 
  +
"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
 
}
 
 
},
 
},
{
+
cases = {
  +
outputOnly = true,
desc = "Special parameter <code>...</code> is used to parse an array of trailing template arguments",
 
  +
{
snippet = "TrailingArgs",
 
  +
desc = "Positional arguments are assigned to their names.",
expect = {
 
  +
snippet = "PositionalAndNamedArgs",
{
 
games = {"OoT", "MM", "TWW", "TP"}
+
expect = {
},
+
{
nil
+
game = "OoT",
  +
page = "Boss Key",
}
 
},
 
{
 
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 = {
 
{
 
baz = "Baz"
 
},
 
{
 
categories = {
 
"Category:Pages with Invalid Arguments",
 
"Category:Custom Category Name",
 
 
},
 
},
args = {
+
nil
bar = {
+
}
{
+
},
  +
{
category = "Category:Custom Category Name",
 
msg = "<code>bar</code> is required but is <code>nil</code>.",
+
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 = {
  +
{
  +
baz = "Baz"
  +
},
  +
{
  +
categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]][[Category:Custom Category Name]]",
  +
categories = {
  +
"Category:Articles Using Invalid Arguments in Template Calls",
  +
"Category:Custom Category Name",
 
},
 
},
foo = {
+
args = {
{
+
bar = {
  +
{
category = "Category:Pages with Invalid Arguments",
 
msg = "<code>foo</code> is required but is <code>nil</code>.",
+
category = "Category:Custom Category Name",
  +
msg = "<code>bar</code> parameter is required.",
  +
},
  +
},
  +
foo = {
  +
{
  +
category = "Category:Articles Using Invalid Arguments in Template Calls",
  +
msg = "<code>foo</code> parameter is required.",
  +
},
 
},
 
},
 
},
 
},
Line 307: Line 413:
 
},
 
},
 
},
 
},
},
+
{
  +
desc = "Arguments may have aliases when a high-usage parameter needs to be renamed.",
{
 
  +
snippet = "RequiredArgsWithAliases",
desc = "Validation of deprecated arguments.",
 
snippet = "Deprecated",
+
expect = {
expect = {
+
{
{ oldArg = "foo" },
+
newName = "foo",
{
+
oldName = "foo",
  +
},
categories = {"Category:Pages with Deprecated Arguments"},
 
args = {
+
nil
oldArg = {
+
}
{
+
},
  +
{
category = "Category:Pages with Deprecated Arguments",
 
msg = "<code>oldArg</code> is deprecated but has value <code>foo</code>.",
+
desc = "Validation of deprecated parameters.",
  +
snippet = "Deprecated",
  +
expect = {
  +
{ oldArg = "foo", oldArg2 = "bar" },
  +
{
  +
categoryText = "[[Category:Custom Deprecation Category]][[Category:Articles Using Deprecated Parameters in Template Calls]]",
  +
categories = {"Category:Custom Deprecation Category", "Category:Articles Using Deprecated Parameters in Template Calls"},
  +
args = {
  +
oldArg = {
  +
{
  +
category = "Category:Articles Using Deprecated Parameters in Template Calls",
  +
msg = "<code>oldArg</code> is deprecated but has value <code>foo</code>.",
  +
},
  +
},
  +
oldArg2 = {
  +
{
  +
category = "Category:Custom Deprecation Category",
  +
msg = "<code>oldArg2</code> is deprecated but has value <code>bar</code>.",
  +
},
 
},
 
},
 
},
 
},
 
},
 
},
},
+
}
}
+
},
},
+
{
  +
desc = "Using an unknown parameter counts as an error.",
{
 
  +
snippet = "Unkown",
desc = "Using an unknown parameter counts as an error.",
 
snippet = "Unkown",
+
expect = {
expect = {
+
{},
{},
+
{
  +
categoryText = "[[Category:Articles Using Unknown Parameters in Template Calls]]",
{
 
categories = {"Category:Pages using Unknown Parameters"},
+
categories = {"Category:Articles Using Unknown Parameters in Template Calls"},
args = {
+
args = {
foo = {
+
foo = {
{
+
{
message = "No such parameter <code>foo</code> is defined for this template.",
+
message = "No such parameter <code>foo</code> is defined for this template. Value: <code>bar</code>",
category = "Category:Pages using Unknown Parameters",
+
category = "Category:Articles Using Unknown Parameters in Template Calls",
  +
},
 
},
 
},
 
},
 
},
},
+
}
  +
},
  +
},
  +
{
  +
desc = "Can parse numbers",
  +
snippet = "Number",
  +
expect = {
  +
{ foo = 9000 },
  +
nil
 
}
 
}
 
},
 
},
},
+
{
  +
desc = "Returns an error if a non-number is passed when a number is expected.",
{
 
desc = "Can parse numbers",
+
snippet = "InvalidNumber",
snippet = "Number",
+
expect = {
expect = {
+
{ foo = "notANumber" },
{ foo = 9000 },
+
{
  +
categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
nil
 
  +
categories = {"Category:Articles Using Invalid Arguments in Template Calls"},
}
 
  +
args = {
},
 
{
+
foo = {
  +
{
desc = "Returns an error if a non-number is passed when a number is expected.",
 
  +
category = "Category:Articles Using Invalid Arguments in Template Calls",
snippet = "InvalidNumber",
 
  +
msg = '<code>foo</code> is expected to be a number but was: <code>"notANumber"</code>',
expect = {
 
{ foo = "NaN" },
+
},
{
 
categories = {"Category:Pages with Invalid Arguments"},
 
args = {
 
foo = {
 
{
 
category = "Category:Pages with Invalid Arguments",
 
msg = '<code>foo</code> is expected to be a number but was: <code>"NaN"</code>',
 
 
},
 
},
 
},
 
},
Line 369: Line 495:
 
},
 
},
 
},
 
},
},
+
{
  +
desc = "Default values",
{
 
desc = "Default values",
+
snippet = "Default",
snippet = "Default",
+
expect = {
expect = {
+
{
  +
someParamWithDefault = "foo",
{
 
someParamWithDefault = "foo",
+
someParamWithDefaultNumber = 1,
someParamWithDefaultNumber = 1,
+
param3 = "bar",
param3 = "bar",
+
param4 = ""
param4 = ""
+
}
 
}
 
}
}
 
},
 
{
 
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>trim</code> can be set on a parameter so that [[Module:UtilsString#trim|utilsString.trim]] is called for the argument.",
{
 
  +
snippet = "Trim",
desc = "<code>split</code> using a custom splitting pattern",
 
  +
expect = {{ someParam = "foo" }}
snippet = "SplitPattern",
 
expect = {
 
{ foo = {"a", "b", "c"} },
 
 
},
 
},
},
+
{
  +
desc = "<code>nilIfEmpty</code> can be set on a parameter so that [[Module:UtilsString#nilIfEmpty|utilsString.nilIfEmpty]] is called for the argument.",
{
 
  +
snippet = "NilIfEmpty",
desc = "If <code>nilIfEmpty</code> and <code>required</code> are set, then the argument is invalid if it is an empty string.",
 
  +
expect = {{}, nil}
snippet = "NilIfEmptyWithRequiredArgs",
 
expect = {
+
},
{},
+
{
  +
desc = "<code>split</code> can be set on a parameter so that [[Module:UtilsString#split|utilsString.split]] is called for the argument.",
{
 
  +
snippet = "Split",
categories = {"Category:Pages with Invalid Arguments"},
 
args = {
+
expect = {
game = {
+
{ foo = {"a", "b", "c"} },
{
+
},
  +
},
category = "Category:Pages with Invalid Arguments",
 
  +
{
msg = "<code>game</code> is required but is <code>nil</code>.",
 
  +
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 = {
  +
{},
  +
{
  +
categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
  +
categories = {"Category:Articles Using Invalid Arguments in Template Calls"},
  +
args = {
  +
game = {
  +
{
  +
category = "Category:Articles Using Invalid Arguments in Template Calls",
  +
msg = "<code>game</code> parameter is required.",
  +
},
 
},
 
},
 
},
 
},
Line 423: Line 550:
 
},
 
},
 
},
 
},
},
+
{
  +
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",
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.",
 
  +
expect = {
snippet = "TrimNilIfEmptyRequired",
 
expect = {
+
{},
{},
+
{
  +
categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
{
 
categories = {"Category:Pages with Invalid Arguments"},
+
categories = {"Category:Articles Using Invalid Arguments in Template Calls"},
args = {
+
args = {
game = {
+
game = {
{
+
{
category = "Category:Pages with Invalid Arguments",
+
category = "Category:Articles Using Invalid Arguments in Template Calls",
msg = "<code>game</code> is required but is <code>nil</code>.",
+
msg = "<code>game</code> parameter is required.",
  +
},
 
},
 
},
 
},
 
},
Line 441: Line 569:
 
},
 
},
 
},
 
},
},
+
{
  +
desc = "<code>enum</code> validation.",
{
 
desc = "<code>enum</code> validation.",
+
snippet = "Enum",
snippet = "Enum",
+
expect = {
expect = {
+
{
  +
triforce2 = "Limpah",
{
 
triforce2 = "Limpah",
+
game = "ALttZ",
game = "ALttZ",
+
triforce1 = "Kooloo",
triforce1 = "Kooloo",
 
},
 
{
 
categories = {
 
"Category:Pages with Invalid Arguments",
 
"Category:Pages with Invalid Arguments",
 
"Category:Pages with Invalid Arguments",
 
 
},
 
},
args = {
+
{
  +
categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
triforce2 = {
 
{
+
categories = {
category = "Category:Pages with Invalid Arguments",
+
"Category:Articles Using Invalid Arguments in Template Calls",
  +
"Category:Articles Using Invalid Arguments in Template Calls",
msg = "<code>triforce2</code> has unexpected value <code>Limpah</code>. For a list of accepted values, refer to [[Triforce]].",
 
  +
"Category:Articles Using Invalid Arguments in Template Calls",
},
 
 
},
 
},
game = {
+
args = {
{
+
triforce2 = {
  +
{
category = "Category:Pages with Invalid Arguments",
 
  +
category = "Category:Articles Using Invalid Arguments in Template Calls",
msg = "<code>game</code> has unexpected value <code>ALttZ</code>. For a list of accepted values, refer to [[Data:Franchise]].",
 
  +
msg = "<code>triforce2</code> has unexpected value <code>Limpah</code>. For a list of accepted values, refer to [[Triforce]].",
  +
},
 
},
 
},
},
+
game = {
triforce1 = {
+
{
  +
category = "Category:Articles Using Invalid Arguments in Template Calls",
{
 
  +
msg = "<code>game</code> has unexpected value <code>ALttZ</code>. For a list of accepted values, refer to [[Data:Franchise]].",
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>',
 
  +
},
  +
triforce1 = {
  +
{
  +
category = "Category:Articles Using Invalid Arguments in Template Calls",
  +
msg = '<code>triforce1</code> has unexpected value <code>Kooloo</code>. The accepted values are: <code>{"Courage", "Power", "Wisdom"}</code>',
  +
},
 
},
 
},
 
},
 
},
Line 479: Line 608:
 
},
 
},
 
},
 
},
},
+
{
  +
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",
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 = {
expect = {
+
{
  +
games = {"OoT", "fakeGame", "BotW"},
{
 
  +
},
games = {"OoT", "fakeGame", "BotW"},
 
},
+
{
  +
categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
{
 
categories = {"Category:Pages with Invalid Arguments"},
+
categories = {"Category:Articles Using Invalid Arguments in Template Calls"},
args = {
+
args = {
games = {
+
games = {
{
+
{
category = "Category:Pages with Invalid Arguments",
+
category = "Category:Articles Using Invalid Arguments in Template Calls",
msg = "<code>games[2]</code> has unexpected value <code>fakeGame</code>. For a list of accepted values, refer to [[Data:Franchise]].",
+
msg = "<code>games[2]</code> has unexpected value <code>fakeGame</code>. For a list of accepted values, refer to [[Data:Franchise]].",
  +
}
 
}
 
}
 
}
 
}
 
}
 
}
 
}
 
}
}
 
},
 
{
 
desc = "<code>sortAndRemoveDuplicates</code> can be used alongside <code>split</code> and <code>enum</code>",
 
snippet = "SplitEnumSortAndRemoveDuplicates",
 
expect = {
 
{ games = {"OoT", "BotW"} },
 
nil
 
 
},
 
},
},
+
{
  +
desc = "<code>sortAndRemoveDuplicates</code> can be used alongside <code>split</code> and <code>enum</code>. Entries are sorted to match the sort order of the enum.",
{
 
  +
snippet = "SplitEnumSortAndRemoveDuplicates",
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 = {
expect = {
+
{ games = {"OoT", "BotW"} },
{
+
nil
term = "Dinolfos",
 
game = "TP"
 
 
},
 
},
{
+
},
  +
{
categories = {"Category:Pages with Invalid Arguments"},
 
  +
desc = "<code>enum</code> can be written as a function, when the list of acceptable values depends on the value of another argument.",
args = {
 
term = {
+
snippet = "EnumDependsOn",
{
+
expect = {
  +
{
category = "Category:Pages with Invalid Arguments",
 
  +
term = "Dinolfos",
msg = '<code>term</code> has unexpected value <code>Dinolfos</code>. The accepted values are: <code>{"Dynalfos"}</code>',
 
  +
game = "TP"
  +
},
  +
{
  +
categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
  +
categories = {"Category:Articles Using Invalid Arguments in Template Calls"},
  +
args = {
  +
term = {
  +
{
  +
category = "Category:Articles Using Invalid Arguments in Template Calls",
  +
msg = '<code>term</code> has unexpected value <code>Dinolfos</code>. The accepted values are: <code>{"Dynalfos"}</code>',
  +
},
 
},
 
},
 
},
 
},
Line 528: Line 659:
 
},
 
},
 
},
 
},
},
+
{
  +
desc = "If <code>enumDependsOn</code> refers to a required parameter, then <code>enum</code> is not evaluated when that parameter is nil.",
{
 
  +
snippet = "EnumDependsOnNil",
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 = {
expect = {
+
{ term = "Dinolfos" },
{ term = "Dinolfos" },
+
{
  +
categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
{
 
categories = {"Category:Pages with Invalid Arguments"},
+
categories = {"Category:Articles Using Invalid Arguments in Template Calls"},
args = {
+
args = {
game = {
+
game = {
{
+
{
category = "Category:Pages with Invalid Arguments",
+
category = "Category:Articles Using Invalid Arguments in Template Calls",
msg = "<code>game</code> is required but is <code>nil</code>.",
+
msg = "<code>game</code> parameter is required.",
}
+
}
  +
},
 
},
 
},
 
},
 
},
 
},
 
},
 
},
 
},
},
+
{
  +
desc = "Altogether now",
{
 
desc = "Altogether now",
+
snippet = "TermStorePass",
snippet = "TermStorePass",
+
expect = {
expect = {
+
{
  +
term = "Dinolfos",
{
 
term = "Dinolfos",
+
games = {"OoT", "MM"},
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 = {
+
nil
term = {
+
}
{
+
},
  +
{
category = "Category:Pages with Invalid Arguments",
 
  +
snippet = "TermStoreFail",
msg = "<code>term</code> is required but is <code>nil</code>.",
 
},
+
expect = {
  +
{
  +
plural = "true",
  +
games = {"YY", "ZZ"},
  +
},
  +
{
  +
categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]][[Category:Articles Using Deprecated Parameters in Template Calls]]",
  +
categories = {
  +
"Category:Articles Using Invalid Arguments in Template Calls",
  +
"Category:Articles Using Deprecated Parameters in Template Calls",
  +
"Category:Articles Using Invalid Arguments in Template Calls",
  +
"Category:Articles Using Invalid Arguments in Template Calls",
 
},
 
},
games = {
+
args = {
{
+
term = {
  +
{
category = "Category:Pages with Invalid Arguments",
 
  +
category = "Category:Articles Using Invalid Arguments in Template Calls",
msg = "<code>games[1]</code> has unexpected value <code>YY</code>. For a list of accepted values, refer to [[Data:Franchise]]."
 
  +
msg = "<code>term</code> parameter is required.",
  +
},
 
},
 
},
{
+
games = {
  +
{
category = "Category:Pages with Invalid Arguments",
 
  +
category = "Category:Articles Using Invalid Arguments in Template Calls",
msg = "<code>games[2]</code> has unexpected value <code>ZZ</code>. For a list of accepted values, refer to [[Data:Franchise]]."
 
  +
msg = "<code>games[1]</code> has unexpected value <code>YY</code>. For a list of accepted values, refer to [[Data:Franchise]]."
  +
},
  +
{
  +
category = "Category:Articles Using Invalid Arguments in Template Calls",
  +
msg = "<code>games[2]</code> has unexpected value <code>ZZ</code>. For a list of accepted values, refer to [[Data:Franchise]]."
  +
},
 
},
 
},
},
+
plural = {
plural = {
+
{
  +
category = "Category:Articles Using Deprecated Parameters in Template Calls",
{
 
  +
msg = "<code>plural</code> is deprecated but has value <code>true</code>.",
category = "Category:Pages with Deprecated Arguments",
 
  +
},
msg = "<code>plural</code> is deprecated but has value <code>true</code>.",
 
 
},
 
},
 
},
 
},
Line 598: Line 731:
 
},
 
},
 
},
 
},
},
+
{
  +
desc = "<code>trim</code>, <code>nilIfEmpty</code>, and validators such as <code>enum</code> are applied to individual trailing arguments",
{
 
  +
snippet = "TrailingArgsStringTrimNilIfEmptyEnum",
desc = "<code>trim</code>, <code>nilIfEmpty</code>, and validators such as <code>enum</code> are applied to individual trailing arguments",
 
  +
expect = {
snippet = "TrailingArgsStringTrimNilIfEmptyEnum",
 
expect = {
+
{
  +
games = {"OoT", "MM", "ALttZ"},
{
 
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]].",
 
}
 
 
},
 
},
},
+
{
  +
categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
},
 
  +
categories = {"Category:Articles Using Invalid Arguments in Template Calls"},
},
 
  +
args = {
},
 
{
+
games = {
  +
{
desc = "repeatedGroup",
 
  +
category = "Category:Articles Using Invalid Arguments in Template Calls",
snippet = "RepeatedGroup",
 
  +
msg = "<code>games[3]</code> has unexpected value <code>ALttZ</code>. For a list of accepted values, refer to [[Data:Franchise]].",
expect = {
 
{
+
}
tabs = {
 
{
 
tab = "Tab 1",
 
content = "Content 1",
 
 
},
 
},
{
 
tab = "Tab 2",
 
content = "Content 2",
 
 
},
 
},
{ tab = "Tab 4" },
+
},
{ content = "Content 5" },
 
}
 
 
},
 
},
nil
+
},
}
+
{
  +
desc = "repeatedGroup",
},
 
  +
snippet = "RepeatedGroup",
},
 
  +
expect = {
},
 
schemaValidate = {
 
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",
+
tabs = {
msg = "<code>magicWords</code> does not match any <code>oneOf</code> sub-schemas.",
 
errors = {
 
 
{
 
{
  +
tab = "Tab 1",
  +
content = "Content 1",
  +
},
  +
{
  +
tab = "Tab 2",
  +
content = "Content 2",
  +
},
  +
{ tab = "Tab 4" },
  +
{ content = "Content 5" },
  +
}
  +
},
  +
{
  +
categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
  +
categories = {
  +
"Category:Articles Using Invalid Arguments in Template Calls",
  +
"Category:Articles Using Invalid Arguments in Template Calls",
  +
},
  +
args = {
  +
tab4 = {
 
{
 
{
  +
category = "Category:Articles Using Invalid Arguments in Template Calls",
msg = "<code>magicWords</code> is type <code>table</code> but type <code>string</code> was expected.",
 
path = "magicWords",
+
msg = "<code>tab4</code> parameter is required.",
 
},
 
},
 
},
 
},
{
+
content3 = {
 
{
 
{
  +
category = "Category:Articles Using Invalid Arguments in Template Calls",
msg = '<code>magicWords[1]</code> has unexpected value <code>Alakazam</code>. The accepted values are: <code>{"Kooloo", "Limpah"}</code>',
 
path = "magicWords[1]",
+
msg = "<code>content3</code> parameter is required.",
 
},
 
},
 
},
 
},
Line 678: Line 794:
 
},
 
},
 
},
 
},
{
+
},
  +
enum = {
desc = "Passes schema validation.",
 
  +
desc = "This function validates that a value (or each value in an list) is contained within an <code>enum</code> list.",
snippet = "Passes",
 
expect = {nil, nil},
+
params = {"enum"},
  +
returns = "List of error messages as well as an error category, or <code>nil</code> if there are no errors. Any error messages are logged using {{Scribunto Manual|lib=mw.addWarning}}.",
  +
cases = {
  +
{
  +
args = { {"a", "b", "c"}, "a", "letter" },
  +
expect = nil,
  +
},
  +
{
  +
args = { {"a", "b", "c"}, "d", "letter" },
  +
expect = {
  +
category = "Category:Articles Using Invalid Arguments in Template Calls",
  +
messages = {
  +
'<code>letter</code> has unexpected value <code>d</code>. The accepted values are: <code>{"a", "b", "c"}</code>',
  +
},
  +
},
  +
},
  +
{
  +
args = { {"a", "b", "c"}, {"c", "d", "e"}, "letter" },
  +
expect = {
  +
category = "Category:Articles Using Invalid Arguments in Template Calls",
  +
messages = {
  +
'<code>letter[2]</code> has unexpected value <code>d</code>. The accepted values are: <code>{"a", "b", "c"}</code>',
  +
'<code>letter[3]</code> has unexpected value <code>e</code>. The accepted values are: <code>{"a", "b", "c"}</code>',
  +
},
  +
},
  +
},
  +
{
  +
args = { {"Wisdom", "Power", "Courage", reference = "[[Triforce]]"}, "foo", "triforcePiece" },
  +
expect = {
  +
category = "Category:Articles Using Invalid Arguments in Template Calls",
  +
messages = {
  +
"<code>triforcePiece</code> has unexpected value <code>foo</code>. For a list of accepted values, refer to [[Triforce]].",
  +
},
  +
}
  +
},
 
},
 
},
 
}
 
}
},
+
}
  +
end
}
 
   
 
return p
 
return p

Latest revision as of 03:34, 17 October 2022

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 specify nilIfEmpty = 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 and frame:getParent().args if needed. This can be done with utilsTable.merge. Most modules typically need only one frame anyway.
This module exports the following functions.

parse

parse(frameArgs, templateSpec)

This function validates template input and parses it into a table for use in the rest of the module.

Parameters

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 mw.addWarning.

Examples

#InputOutputStatus
Positional arguments are assigned to their names.
1
local args = {"OoT", page = "Boss Key"}
return utilsArg.parse(args, {
  params = {
    [1] = {
      name = "game",
      desc = "A game",
    },
    page = {
      desc = "A page on the wiki",
    },  
  }
})
{
  page = "Boss Key",
  game = "OoT",
}
Green check
nil
Green check
Special parameter ... is used to parse an array of trailing template arguments
2
local args = {"OoT", "MM", "TWW", "TP"}
return utilsArg.parse(args, {
  params = {
    ["..."] = {
      name = "games",
    },
  },
})
{
  games = {"OoT", "MM", "TWW", "TP"},
}
Green check
nil
Green check
... used with other positional args
3
local args = {"foo", "bar", "OoT", "MM", "TWW", "TP"}
return utilsArg.parse(args, {
  params = {
    [1] = {
      name = "foo"
    },
    [2] = {
      name = "bar",
    },
    ["..."] = {
      name = "games",
    },
  },
})
{
  foo = "foo",
  bar = "bar",
  games = {"OoT", "MM", "TWW", "TP"},
}
Green check
nil
Green check
Validation of required arguments.
4
local args = {nil, nil, "Baz"}
return utilsArg.parse(args, {
  params = {
    [1] = {
      name = "foo",
      required = true,
    },
    [2] = {
      name = "bar",
      required = "Category:Custom Category Name",
    },
    [3] = {
      name = "baz",
      required = true,
    },
  }
})
{ baz = "Baz" }
Green check
{
  categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]][[Category:Custom Category Name]]",
  categories = {
    "Category:Articles Using Invalid Arguments in Template Calls",
    "Category:Custom Category Name",
  },
  args = {
    bar = {
      {
        category = "Category:Custom Category Name",
        msg = "<code>bar</code> parameter is required.",
      },
    },
    foo = {
      {
        category = "Category:Articles Using Invalid Arguments in Template Calls",
        msg = "<code>foo</code> parameter is required.",
      },
    },
  },
}
Green check
Arguments may have aliases when a high-usage parameter needs to be renamed.
5
local args = { oldName = "foo" }
return utilsArg.parse(args, {
  params = {
    newName = {
      aliases = {"oldName"}  
    },
  }
})
{
  newName = "foo",
  oldName = "foo",
}
Green check
nil
Green check
Validation of deprecated parameters.
6
local args = {
  oldArg = "foo",
  oldArg2 = "bar",
}
return utilsArg.parse(args, {
  params = {
    oldArg = {
      deprecated = true
    },
    oldArg2 = {
      deprecated = "Category:Custom Deprecation Category",
    },
  }
})
{
  oldArg = "foo",
  oldArg2 = "bar",
}
Green check
{
  categoryText = "[[Category:Custom Deprecation Category]][[Category:Articles Using Deprecated Parameters in Template Calls]]",
  categories = {
    "Category:Custom Deprecation Category",
    "Category:Articles Using Deprecated Parameters in Template Calls",
  },
  args = {
    oldArg = {
      {
        category = "Category:Articles Using Deprecated Parameters in Template Calls",
        msg = "<code>oldArg</code> is deprecated but has value <code>foo</code>.",
      },
    },
    oldArg2 = {
      {
        category = "Category:Custom Deprecation Category",
        msg = "<code>oldArg2</code> is deprecated but has value <code>bar</code>.",
      },
    },
  },
}
Green check
Using an unknown parameter counts as an error.
7
local args = {
  foo = "bar"
}
return utilsArg.parse(args, {
  params = {} -- template has no args defined and yet "foo" is used as one
})
{}
Green check
{
  categoryText = "[[Category:Articles Using Unknown Parameters in Template Calls]]",
  categories = {
    "Category:Articles Using Unknown Parameters in Template Calls",
  },
  args = {
    foo = {
      {
        message = "No such parameter <code>foo</code> is defined for this template. Value: <code>bar</code>",
        category = "Category:Articles Using Unknown Parameters in Template Calls",
      },
    },
  },
}
Green check
Can parse numbers
8
local args = {
  foo = "9000",
}
return utilsArg.parse(args, {
  params = {
    foo = {
      type = "number"
    }
  }
})
{ foo = 9000 }
Green check
nil
Green check
Returns an error if a non-number is passed when a number is expected.
9
local args = {
  foo = "notANumber",
}
return utilsArg.parse(args, {
  params = {
    foo = {
      type = "number"
    }
  }
})
{ foo = "notANumber" }
Green check
{
  categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
  categories = {
    "Category:Articles Using Invalid Arguments in Template Calls",
  },
  args = {
    foo = {
      {
        category = "Category:Articles Using Invalid Arguments in Template Calls",
        msg = '<code>foo</code> is expected to be a number but was: <code>"notANumber"</code>',
      },
    },
  },
}
Green check
Default values
10
local args = {[3] = "", [4] = ""}
return utilsArg.parse(args, {
  params = {
    [1] = {
      name = "someParamWithDefault",
      type = "string",
      default = "foo",
    },
    [2] = {
      name = "someParamWithDefaultNumber",
      type = "number",
      default = 1,
    },
    [3] = {
      name = "param3",
      type = "string",
      default = "bar",
      nilIfEmpty = true,
    },
    [4] = {
      name = "param4",
      type = "string",
      default = "baz",
    }
  }
})
{
  param4 = "",
  someParamWithDefaultNumber = 1,
  someParamWithDefault = "foo",
  param3 = "bar",
}
Green check
nil
Green check
trim can be set on a parameter so that utilsString.trim is called for the argument.
11
local args = {" foo   \n"}
return utilsArg.parse(args, {
  params = {
    [1] = {
      name = "someParam",
      trim = true,
    },
  }
})
{ someParam = "foo" }
Green check
nil
Green check
nilIfEmpty can be set on a parameter so that utilsString.nilIfEmpty is called for the argument.
12
local args = {
  foo = ""
}
return utilsArg.parse(args, {
  params = {
    foo = {
      nilIfEmpty = true,
    },
  }
})
{}
Green check
nil
Green check
split can be set on a parameter so that utilsString.split is called for the argument.
13
local args = {
  foo = "  a, b  , c"
}
return utilsArg.parse(args, {
  params = {
    foo = {
      trim = true,
      split = true,
    }
  }
})
{
  foo = {"a", "b", "c"},
}
Green check
nil
Green check
split using a custom splitting pattern
14
local args = {
  foo = "abc"
}
return utilsArg.parse(args, {
  params = {
    foo = {
      split = "",
    }
  }
})
{
  foo = {"a", "b", "c"},
}
Green check
nil
Green check
If nilIfEmpty and required are set, then the argument is invalid if it is an empty string.
15
local args = {""}
return utilsArg.parse(args, {
  params = {
    [1] = {
      name = "game",
      nilIfEmpty = true,
      required = true,
      desc = "A game",
    },
  }
})
{}
Green check
{
  categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
  categories = {
    "Category:Articles Using Invalid Arguments in Template Calls",
  },
  args = {
    game = {
      {
        category = "Category:Articles Using Invalid Arguments in Template Calls",
        msg = "<code>game</code> parameter is required.",
      },
    },
  },
}
Green check
If trim, nilIfEmpty, and required are set, then the argument is invalid if it is a blank string.
16
local args = {"  \n"}
return utilsArg.parse(args, {
  params = {
    [1] = {
      name = "game",
      desc = "A game",
      trim = true,
      nilIfEmpty = true,
      required = true,
    },
  }
})
{}
Green check
{
  categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
  categories = {
    "Category:Articles Using Invalid Arguments in Template Calls",
  },
  args = {
    game = {
      {
        category = "Category:Articles Using Invalid Arguments in Template Calls",
        msg = "<code>game</code> parameter is required.",
      },
    },
  },
}
Green check
enum validation.
17
local args = {"Kooloo", "Limpah", "ALttZ"}
return utilsArg.parse(args, {
  params = {
    [1] = {
      name = "triforce1",
      enum = {"Courage", "Power", "Wisdom"}
    },
    [2] = {
      name = "triforce2",
      enum = {"Courage", "Power", "Wisdom", reference = "[[Triforce]]"},
    },
    [3] = {
      name = "game",
      enum = Franchise.enum(), -- from [[Module:Franchise]]
    }
  }
})
{
  triforce2 = "Limpah",
  game = "ALttZ",
  triforce1 = "Kooloo",
}
Green check
{
  categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
  categories = {
    "Category:Articles Using Invalid Arguments in Template Calls",
    "Category:Articles Using Invalid Arguments in Template Calls",
    "Category:Articles Using Invalid Arguments in Template Calls",
  },
  args = {
    triforce2 = {
      {
        category = "Category:Articles Using Invalid Arguments in Template Calls",
        msg = "<code>triforce2</code> has unexpected value <code>Limpah</code>. For a list of accepted values, refer to [[Triforce]].",
      },
    },
    game = {
      {
        category = "Category:Articles Using Invalid Arguments in Template Calls",
        msg = "<code>game</code> has unexpected value <code>ALttZ</code>. For a list of accepted values, refer to [[Data:Franchise]].",
      },
    },
    triforce1 = {
      {
        category = "Category:Articles Using Invalid Arguments in Template Calls",
        msg = '<code>triforce1</code> has unexpected value <code>Kooloo</code>. The accepted values are: <code>{"Courage", "Power", "Wisdom"}</code>',
      },
    },
  },
}
Green check
split is used to parse comma-separated strings as arrays. Each array item can be validated against an enum.
18
local args = {
  games = "OoT, fakeGame, BotW",
}
return utilsArg.parse(args, {
  params = {
    games = {
      split = true,
      enum = Franchise.enum(),
    },
  },
})
{
  games = {"OoT", "fakeGame", "BotW"},
}
Green check
{
  categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
  categories = {
    "Category:Articles Using Invalid Arguments in Template Calls",
  },
  args = {
    games = {
      {
        category = "Category:Articles Using Invalid Arguments in Template Calls",
        msg = "<code>games[2]</code> has unexpected value <code>fakeGame</code>. For a list of accepted values, refer to [[Data:Franchise]].",
      },
    },
  },
}
Green check
sortAndRemoveDuplicates can be used alongside split and enum. Entries are sorted to match the sort order of the enum.
19
local args = {
  games = "BotW, BotW, fakeGame, OoT, OoT",
}
return utilsArg.parse(args, {
  params = {
    games = {
      split = true,
      enum = Franchise.enum(),
      sortAndRemoveDuplicates = true,
    },
  },
})
{
  games = {"OoT", "BotW"},
}
Green check
{
  categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
  categories = {
    "Category:Articles Using Invalid Arguments in Template Calls",
  },
  args = {
    games = {
      {
        category = "Category:Articles Using Invalid Arguments in Template Calls",
        msg = "<code>games[3]</code> has unexpected value <code>fakeGame</code>. For a list of accepted values, refer to [[Data:Franchise]].",
      },
    },
  },
}
enum can be written as a function, when the list of acceptable values depends on the value of another argument.
20
local validTermsForGame = {
  OoT = {"Dinolfos"},
  TP = {"Dynalfos"},
}
local args = {"TP", "Dinolfos"}
return utilsArg.parse(args, {
  params = {
    [1] = {
      name = "game",
      required = true,
    },
    [2] = {
      name = "term",
      enumDependsOn = "game",
      enum = function(game)
        return validTermsForGame[game]
      end,
    }
  }
})
{
  term = "Dinolfos",
  game = "TP",
}
Green check
{
  categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
  categories = {
    "Category:Articles Using Invalid Arguments in Template Calls",
  },
  args = {
    term = {
      {
        category = "Category:Articles Using Invalid Arguments in Template Calls",
        msg = '<code>term</code> has unexpected value <code>Dinolfos</code>. The accepted values are: <code>{"Dynalfos"}</code>',
      },
    },
  },
}
Green check
If enumDependsOn refers to a required parameter, then enum is not evaluated when that parameter is nil.
21
local validTermsForGame = {
  OoT = {"Dinolfos"},
  TP = {"Dynalfos"},
}
local args = {nil, "Dinolfos"}
return utilsArg.parse(args, {
  params = {
    [1] = {
      name = "game",
      required = true,
    },
    [2] = {
      name = "term",
      enumDependsOn = "game",
      enum = function(game)
        return validTermsForGame[game]
      end,
    }
  }
})
{ term = "Dinolfos" }
Green check
{
  categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
  categories = {
    "Category:Articles Using Invalid Arguments in Template Calls",
  },
  args = {
    game = {
      {
        category = "Category:Articles Using Invalid Arguments in Template Calls",
        msg = "<code>game</code> parameter is required.",
      },
    },
  },
}
Green check
Altogether now
22
local args = {"Dinolfos", games = "OoT, MM", plural = nil}
return utilsArg.parse(args, {
  params = {
    [1] = {
      name = "term",
      nilIfEmpty = true,
      required = true,
    },
    games = {
      split = true,
      enum = Franchise.enum(),
    },
    plural = {
      deprecated = true,
    }
  }
})
{
  term = "Dinolfos",
  games = {"OoT", "MM"},
}
Green check
nil
Green check
23
local args = {"", games = "YY, ZZ", plural = "true"}
return utilsArg.parse(args, {
  params = {
    [1] = {
      name = "term",
      nilIfEmpty = true,
      required = true,
    },
    games = {
      split = true,
      enum = Franchise.enum(),
    },
    plural = {
      deprecated = true,
    },
  }
})
{
  plural = "true",
  games = {"YY", "ZZ"},
}
Green check
{
  categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]][[Category:Articles Using Deprecated Parameters in Template Calls]]",
  categories = {
    "Category:Articles Using Invalid Arguments in Template Calls",
    "Category:Articles Using Deprecated Parameters in Template Calls",
    "Category:Articles Using Invalid Arguments in Template Calls",
    "Category:Articles Using Invalid Arguments in Template Calls",
  },
  args = {
    plural = {
      {
        category = "Category:Articles Using Deprecated Parameters in Template Calls",
        msg = "<code>plural</code> is deprecated but has value <code>true</code>.",
      },
    },
    games = {
      {
        category = "Category:Articles Using Invalid Arguments in Template Calls",
        msg = "<code>games[1]</code> has unexpected value <code>YY</code>. For a list of accepted values, refer to [[Data:Franchise]].",
      },
      {
        category = "Category:Articles Using Invalid Arguments in Template Calls",
        msg = "<code>games[2]</code> has unexpected value <code>ZZ</code>. For a list of accepted values, refer to [[Data:Franchise]].",
      },
    },
    term = {
      {
        category = "Category:Articles Using Invalid Arguments in Template Calls",
        msg = "<code>term</code> parameter is required.",
      },
    },
  },
}
Green check
trim, nilIfEmpty, and validators such as enum are applied to individual trailing arguments
24
local args = {"\n  OoT", "", "MM ", "ALttZ"}
return utilsArg.parse(args, {
  params = {
    ["..."] = {
      name = "games",
      trim = true,
      nilIfEmpty = true,
      enum = Franchise.enum()
    },
  }
})
{
  games = {"OoT", "MM", "ALttZ"},
}
Green check
{
  categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
  categories = {
    "Category:Articles Using Invalid Arguments in Template Calls",
  },
  args = {
    games = {
      {
        category = "Category:Articles Using Invalid Arguments in Template Calls",
        msg = "<code>games[3]</code> has unexpected value <code>ALttZ</code>. For a list of accepted values, refer to [[Data:Franchise]].",
      },
    },
  },
}
Green check
repeatedGroup
25
local args = {
  tab1 = "Tab 1",
  content1 = "Content 1",
  
  tab2= "Tab 2",
  content2 = "Content 2",
  
  -- missing tab3, content3
  
  tab4 = "Tab 4",
  -- missing content4
  
  --missing tab5
  content5 = "Content 5",
}
return utilsArg.parse(args, {
  params = {
    ["tab"] = { required = true },
    ["content"] = { required = true },
  },
  repeatedGroup = {
    name = "tabs",
    params = {"tab", "content"},
  },
})
{
  tabs = {
    {
      tab = "Tab 1",
      content = "Content 1",
    },
    {
      tab = "Tab 2",
      content = "Content 2",
    },
    { tab = "Tab 4" },
    { content = "Content 5" },
  },
}
Green check
{
  categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
  categories = {
    "Category:Articles Using Invalid Arguments in Template Calls",
    "Category:Articles Using Invalid Arguments in Template Calls",
  },
  args = {
    tab4 = {
      {
        category = "Category:Articles Using Invalid Arguments in Template Calls",
        msg = "<code>tab4</code> parameter is required.",
      },
    },
    content3 = {
      {
        category = "Category:Articles Using Invalid Arguments in Template Calls",
        msg = "<code>content3</code> parameter is required.",
      },
    },
  },
}
Green check

enum

enum(enum)

This function validates that a value (or each value in an list) is contained within an enum list.

Returns

  • List of error messages as well as an error category, or nil if there are no errors. Any error messages are logged using mw.addWarning.

Examples

#InputOutputResultStatus
26
enum({"a", "b", "c"}, "a", "letter")
nil
Green check
27
enum({"a", "b", "c"}, "d", "letter")
{
  category = "Category:Articles Using Invalid Arguments in Template Calls",
  messages = {
    '<code>letter</code> has unexpected value <code>d</code>. The accepted values are: <code>{"a", "b", "c"}</code>',
  },
}
Green check
28
enum({"a", "b", "c"}, {"c", "d", "e"}, "letter")
{
  category = "Category:Articles Using Invalid Arguments in Template Calls",
  messages = {
    '<code>letter[2]</code> has unexpected value <code>d</code>. The accepted values are: <code>{"a", "b", "c"}</code>',
    '<code>letter[3]</code> has unexpected value <code>e</code>. The accepted values are: <code>{"a", "b", "c"}</code>',
  },
}
Green check
29
enum(
  {
    "Wisdom",
    "Power",
    "Courage",
    reference = "[[Triforce]]",
  },
  "foo",
  "triforcePiece"
)
{
  category = "Category:Articles Using Invalid Arguments in Template Calls",
  messages = {
    "<code>triforcePiece</code> has unexpected value <code>foo</code>. For a list of accepted values, refer to [[Triforce]].",
  },
}
Green check

local p = {}
local h = {}
local validators = {}

local Constants = mw.loadData("Module:Constants/Data")
local utilsPackage = require("Module:UtilsPackage")
local utilsString = require("Module:UtilsString")
local utilsTable = require("Module:UtilsTable")
local utilsVar = require("Module:UtilsVar")

-- lazy-load so it only gets linked when needed - most pages won't have errors 
local _utilsError = utilsPackage.lazyLoad("Module:UtilsError")

local CAT_DEPRECATED_PARAMS = "Category:"..Constants.category.deprecatedParams
local CAT_INVALID_ARGS = "Category:"..Constants.category.invalidArgs

local parentFrame = mw.getCurrentFrame() and mw.getCurrentFrame():getParent()
if parentFrame then
	h.templatePage = parentFrame:getTitle()
	h.instanceCounter = utilsVar.counter("instanceCounter"..h.templatePage) -- [[Module:UtilsError]] uses this too
	h.instanceCounter.increment()
end

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 = {},
	}
	
	-- Parse ordinary args
	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
			for i, alias in ipairs(v.aliases or {}) do
				args[alias] = h.parseArg(frameArgs[alias], v)
				args[v.name or k] = args[v.name or k] or args[alias] -- if both the parameter and its alias is used, the alias should not overrid
				unknownParams[alias] = nil
			end
		end
	end
	
	-- Parse repeatedGroup args
	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 {}
				local arg = h.parseArg(v, paramSpec)
				if arg then
					args[repeated][index] = args[repeated][index] or {}
					args[repeated][index][param] = arg
				end
				unknownParams[k] = nil
			end
		end
		args[repeated] = args[repeated] and utilsTable.compact(args[repeated]) or {} -- in case a number is accidentally skipped for a whole "row"
	end
	
	-- Parse variadic args
	local variadicParam = templateSpec.params["..."]
	if variadicParam then
		local i = #templateSpec.params + 1
		while frameArgs[i] do
			local varArg = h.parseArg(frameArgs[i], variadicParam)
			if varArg then
				args[variadicParam.name] = args[variadicParam.name] or {}
				table.insert(args[variadicParam.name], varArg)
			end
			unknownParams[i] = nil
			i = i + 1
		end
	end
	
	-- Validate
	for paramKey, paramSpec in pairs(templateSpec.params) do
		if not repeatedParamsMap[paramKey] then -- repeated args are an edge case for validation and need to be handled separately
			local paramName = paramSpec.name or paramKey
			local paramValue = args[paramName]
			h.validateAndAddErrors(err, args, paramSpec, paramName, paramValue)
		else
			for i in ipairs(args[repeated] or {}) do
				local paramName = paramKey..i -- no need to check paramSpec.name as repeated params should always be named arguments
				local paramValue = args[repeated][i][paramKey]
				h.validateAndAddErrors(err, args, paramSpec, paramName, paramValue)
			end
		end
	end
	
	-- Do any post processing such as sorting and removing duplicates
	for k, v in pairs(templateSpec.params) do
		local arg = args[v.name or k]
		if arg and v.enum and v.split and v.sortAndRemoveDuplicates then
			args[v.name or k] = utilsTable.intersection(v.enum, arg)
		end
	end
	
	-- Handle any args left that don't have corresponding params defined
	for k, v in pairs(unknownParams) do
		-- value is not strictly necessary but can make it easier to search for the invalid usage when the template is used several times on a page
		local errMsg = string.format("No such parameter <code>%s</code> is defined for this template. Value: <code>%s</code>", k, v)
		h.warn(errMsg)
		err.args[k] = {{
			category = "Category:Articles Using Unknown Parameters in Template Calls",
			message = errMsg
		}}
		err.categories = utilsTable.concat(err.categories, "Category:Articles Using Unknown Parameters in Template Calls")
	end
	if #err.categories == 0 then
		err = nil
	end
	if err and mw.title.getCurrentTitle().nsText == "User" then
		err.categories = {}
	end
	if err then
		local categoryText = ""
		local categories = utilsTable.unique(err.categories)
		for i, cat in ipairs(categories) do
			categoryText = categoryText.."[["..cat.."]]"
		end
		err.categoryText = categoryText
	end
	return args, err
end

function p.enum(enum, value, name)
	local err, category = validators.enum(enum, value, name)
	if not err or #err == 0 then
		return nil
	else
		return {
			messages = err,
			category = category,
		}
	end
end

function h.warn(errMessage)
	_utilsError().warn(errMessage)
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.nilIfEmpty then
		arg = utilsString.nilIfEmpty(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
	arg = arg or param.default
	if param.type == "number" then
		local num = tonumber(arg)
		if num then
			arg = num
		end
	end
	return arg
end

function h.validateAndAddErrors(err, args, paramSpec, paramName, paramValue)
	local argErrors, errorCategories = h.validate(paramValue, paramSpec, paramName, args)
	if #argErrors > 0 then
		err.args[paramName] = utilsTable.concat(err.args[paramName] or {}, argErrors)
	end
	err.categories = utilsTable.concat(err.categories, errorCategories)
end

function h.validate(arg, param, paramName, args)
	local errors = {}
	local categories = {}
	local validatorNames = {"required", "deprecated", "enum", "type"} -- do validation in this order
	for i, validatorName in ipairs(validatorNames) do
		local validatorData = param[validatorName]
		if validatorName == "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
		if validatorName == "type" and validatorData and arg == nil then
			validatorData = nil -- we shouldn't do type validation if arg is nil and not required
		end
		local errorMessages, defaultCat
		if validatorData ~= nil then 
			errorMessages, defaultCat = validators[validatorName](validatorData, arg, paramName)
		end
		-- Here we allow custom categories so that editors can do targeted maintenance on a specific template parameter
		-- For example, we deprecate a parameter and use a custom category to clean up all the pages that use that parameter
		local cat = defaultCat
		if (validatorName == "required" or validatorName == "deprecated") and type(validatorData) == "string" then
			cat = validatorData
		end
		if errorMessages then
			for _, err in ipairs(errorMessages) do
				table.insert(errors, {
					msg = err,
					category = cat
				})
				table.insert(categories, cat)
			end
			break -- run only one validator for now as there isn't yet any situtation where it makes sense to run several
		end
	end
	return errors, categories
end

function validators.required(required, value, name)
	if not required then
		return
	end
	if value == nil then
		local err = string.format("<code>%s</code> parameter is required.", name)
		h.warn(err)
		return {err}, CAT_INVALID_ARGS
	end
end
function validators.enum(enum, value, name)
	if not enum then
		return
	end
	-- Sometimes `value` is an "array", sometimes it's just a primitive value
	-- We can simplify the code by folding the latter case into the former
	-- i.e. making `value` always an array
	local isMultiValue = type(value) == "table"
	if not isMultiValue then
		value = { value }
	end
	
	local errors = {}
	for k, v in ipairs(value) do
		if not utilsTable.keyOf(enum, v) then
			local path = name
			if isMultiValue then
				path = path .. string.format("[%s]", k)
			end
			local msg
			if enum.reference then
				msg = string.format("<code>%s</code> has unexpected value <code>%s</code>. For a list of accepted values, refer to %s.", path, v, enum.reference)
			else
				local acceptedValues = utilsTable.print(enum, true)
				msg = string.format("<code>%s</code> has unexpected value <code>%s</code>. The accepted values are: <code>%s</code>", path, v, acceptedValues)
			end
			table.insert(errors, msg)
			h.warn(msg)
		end
	end
	return errors, CAT_INVALID_ARGS
end
function validators.deprecated(deprecated, value, name)
	if not deprecated then
		return
	end
	if value ~= nil then
		if type(value) == "table" then
			value = utilsTable.print(value, true)
		end
		local err = string.format("<code>%s</code> is deprecated but has value <code>%s</code>.", name, value)
		h.warn(err)
		return {err}, CAT_DEPRECATED_PARAMS
	end
end
function validators.type(expectedType, value, name)
	if expectedType == "number" and tonumber(value) == nil then
		local msg = "<code>" .. name .. "</code> is expected to be a number but was: <code>" .. utilsTable.print(value) .. "</code>"
		h.warn(msg)
		return {msg}, CAT_INVALID_ARGS
	end
end

-- See [[Module:Arguments#store]]
function p.store(transclusion)
	local frame = mw.getCurrentFrame()
	local isValid = transclusion.isValid
	if isValid ~= nil then
		isValid = isValid and "1" or "0"
	end
	for k, v in pairs(transclusion.args) do
		frame:expandTemplate({
			title = "Arguments/Store",
			args = {
				module = transclusion.module or frame:getTitle(),
				template = frame:getParent():getTitle(),
				pageInstance = h.instanceCounter and h.instanceCounter.value(),
				parameter = tostring(k),
				argument = tostring(v),
				isValid = isValid,
			}
		})
	end
end

function p.Schemas()
	return {
		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]].",
			}
		}
	}
end

function p.Documentation()
	return {
		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 = {
						{
							baz = "Baz"
						},
						{
							categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]][[Category:Custom Category Name]]",
							categories = {
								"Category:Articles Using Invalid Arguments in Template Calls",
								"Category:Custom Category Name",
							},
							args = {
								bar = {
									{
										category = "Category:Custom Category Name",
										msg = "<code>bar</code> parameter is required.",
									},
								},
								foo = {
									{
										category = "Category:Articles Using Invalid Arguments in Template Calls",
										msg = "<code>foo</code> parameter is required.",
									},
								},
							},
						},
					},
				},
				{
					desc = "Arguments may have aliases when a high-usage parameter needs to be renamed.",
					snippet = "RequiredArgsWithAliases",
					expect = {
						{
							newName = "foo",
							oldName = "foo",
						},
						nil
					}
				},
				{
					desc = "Validation of deprecated parameters.",
					snippet = "Deprecated",
					expect = {
						{ oldArg = "foo", oldArg2 = "bar" },
						{
							categoryText = "[[Category:Custom Deprecation Category]][[Category:Articles Using Deprecated Parameters in Template Calls]]",
							categories = {"Category:Custom Deprecation Category", "Category:Articles Using Deprecated Parameters in Template Calls"},
							args = {
								oldArg = {
									{
										category = "Category:Articles Using Deprecated Parameters in Template Calls",
										msg = "<code>oldArg</code> is deprecated but has value <code>foo</code>.",
									},
								},
								oldArg2 = {
									{
										category = "Category:Custom Deprecation Category",
										msg = "<code>oldArg2</code> is deprecated but has value <code>bar</code>.",
									},
								},
							},
						},
					}
				},
				{
					desc = "Using an unknown parameter counts as an error.",
					snippet = "Unkown",
					expect = {
						{}, 
						{
							categoryText = "[[Category:Articles Using Unknown Parameters in Template Calls]]",
							categories = {"Category:Articles Using Unknown Parameters in Template Calls"},
							args = {
								foo = {
									{
										message = "No such parameter <code>foo</code> is defined for this template. Value: <code>bar</code>",
										category = "Category:Articles Using Unknown Parameters in Template Calls",
									},
								},
							},
						}
					},
				},
				{
					desc = "Can parse numbers",
					snippet = "Number",
					expect = {
						{ foo = 9000 },
						nil
					}
				},
				{
					desc = "Returns an error if a non-number is passed when a number is expected.",
					snippet = "InvalidNumber",
					expect = {
						{ foo = "notANumber" },
						{
							categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
							categories = {"Category:Articles Using Invalid Arguments in Template Calls"},
							args = {
								foo = {
									{
										category = "Category:Articles Using Invalid Arguments in Template Calls",
										msg = '<code>foo</code> is expected to be a number but was: <code>"notANumber"</code>',
									},
								},
							},
						},
					},
				},
				{
					desc = "Default values",
					snippet = "Default",
					expect = {
						{ 
							someParamWithDefault = "foo", 
							someParamWithDefaultNumber = 1, 
							param3 = "bar", 
							param4 = ""
						}
					}
				},
				{
					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 = {
						{},
						{
							categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
							categories = {"Category:Articles Using Invalid Arguments in Template Calls"},
							args = {
								game = {
									{
										category = "Category:Articles Using Invalid Arguments in Template Calls",
										msg = "<code>game</code> parameter is required.",
									},
								},
							},
						},
					},
				},
				{
					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 = {
						{},
						{
							categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
							categories = {"Category:Articles Using Invalid Arguments in Template Calls"},
							args = {
								game = {
									{
										category = "Category:Articles Using Invalid Arguments in Template Calls",
										msg = "<code>game</code> parameter is required.",
									},
								},
							},
						},
					},
				},
				{
					desc = "<code>enum</code> validation.",
					snippet = "Enum",
					expect = {
						{
							triforce2 = "Limpah",
							game = "ALttZ",
							triforce1 = "Kooloo",
						},
						{
							categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
							categories = {
								"Category:Articles Using Invalid Arguments in Template Calls",
								"Category:Articles Using Invalid Arguments in Template Calls",
								"Category:Articles Using Invalid Arguments in Template Calls",
							},
							args = {
								triforce2 = {
									{
										category = "Category:Articles Using Invalid Arguments in Template Calls",
										msg = "<code>triforce2</code> has unexpected value <code>Limpah</code>. For a list of accepted values, refer to [[Triforce]].",
									},
								},
								game = {
									{
										category = "Category:Articles Using Invalid Arguments in Template Calls",
										msg = "<code>game</code> has unexpected value <code>ALttZ</code>. For a list of accepted values, refer to [[Data:Franchise]].",
									},
								},
								triforce1 = {
									{
										category = "Category:Articles Using Invalid Arguments in Template Calls",
										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"},
						},
						{
							categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
							categories = {"Category:Articles Using Invalid Arguments in Template Calls"},
							args = {
								games = {
									{
										category = "Category:Articles Using Invalid Arguments in Template Calls",
										msg = "<code>games[2]</code> has unexpected value <code>fakeGame</code>. For a list of accepted values, refer to [[Data:Franchise]].",
									}
								}
							}
						}
					}
				},
				{
					desc = "<code>sortAndRemoveDuplicates</code> can be used alongside <code>split</code> and <code>enum</code>. Entries are sorted to match the sort order of the enum.",
					snippet = "SplitEnumSortAndRemoveDuplicates",
					expect = {
						{ games = {"OoT", "BotW"} },
						nil
					},
				},
				{
					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"
						},
						{
							categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
							categories = {"Category:Articles Using Invalid Arguments in Template Calls"},
							args = {
								term = {
									{
										category = "Category:Articles Using Invalid Arguments in Template Calls",
										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" },
						{
							categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
							categories = {"Category:Articles Using Invalid Arguments in Template Calls"},
							args = {
								game = {
									{
										category = "Category:Articles Using Invalid Arguments in Template Calls",
							    		msg = "<code>game</code> parameter is required.",
									}
								},
							},
						},
					},
				},
				{
					desc = "Altogether now",
					snippet = "TermStorePass",
					expect = {
						{
							term = "Dinolfos",
							games = {"OoT", "MM"},
						},
						nil
					}
				},
				{
					snippet = "TermStoreFail",
					expect = {
						{
							plural = "true",
							games = {"YY", "ZZ"},
						},
						{
							categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]][[Category:Articles Using Deprecated Parameters in Template Calls]]",
							categories = {
								"Category:Articles Using Invalid Arguments in Template Calls",
								"Category:Articles Using Deprecated Parameters in Template Calls",
								"Category:Articles Using Invalid Arguments in Template Calls",
								"Category:Articles Using Invalid Arguments in Template Calls",
							},
							args = {
								term = {
									{
										category = "Category:Articles Using Invalid Arguments in Template Calls",
										msg = "<code>term</code> parameter is required.",
									},
								},
								games = {
									{
										category = "Category:Articles Using Invalid Arguments in Template Calls",
										msg = "<code>games[1]</code> has unexpected value <code>YY</code>. For a list of accepted values, refer to [[Data:Franchise]]."
									},
									{
										category = "Category:Articles Using Invalid Arguments in Template Calls",
										msg = "<code>games[2]</code> has unexpected value <code>ZZ</code>. For a list of accepted values, refer to [[Data:Franchise]]."
									},
								},
								plural = {
									{
										category = "Category:Articles Using Deprecated Parameters in Template Calls",
										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"},
						},
						{
							categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
							categories = {"Category:Articles Using Invalid Arguments in Template Calls"},
							args = {
								games = {
								{
									category = "Category:Articles Using Invalid Arguments in Template Calls",
									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",
								},
								{ tab = "Tab 4" },
								{ content = "Content 5" },
							}
						},
						{
							categoryText = "[[Category:Articles Using Invalid Arguments in Template Calls]]",
							categories = {
								"Category:Articles Using Invalid Arguments in Template Calls",
								"Category:Articles Using Invalid Arguments in Template Calls",
							},
							args = {
								tab4 = {
									{
										category = "Category:Articles Using Invalid Arguments in Template Calls",
										msg = "<code>tab4</code> parameter is required.",
									},
								},
								content3 = {
									{
										category = "Category:Articles Using Invalid Arguments in Template Calls",
										msg = "<code>content3</code> parameter is required.",
									},
								},
							},
						},
					},
				},
			},
		},
		enum = {
			desc = "This function validates that a value (or each value in an list) is contained within an <code>enum</code> list.",
			params = {"enum"},
			returns = "List of error messages as well as an error category, or <code>nil</code> if there are no errors. Any error messages are logged using {{Scribunto Manual|lib=mw.addWarning}}.",
			cases = {
				{
					args = { {"a", "b", "c"}, "a", "letter" },
					expect = nil,
				},
				{
					args = { {"a", "b", "c"}, "d", "letter" },
					expect = {
					category = "Category:Articles Using Invalid Arguments in Template Calls",
						messages = {
							'<code>letter</code> has unexpected value <code>d</code>. The accepted values are: <code>{"a", "b", "c"}</code>',
						},
					},
				},
				{
					args = { {"a", "b", "c"}, {"c", "d", "e"}, "letter" },
					expect = {
					category = "Category:Articles Using Invalid Arguments in Template Calls",
						messages = {
							'<code>letter[2]</code> has unexpected value <code>d</code>. The accepted values are: <code>{"a", "b", "c"}</code>',
							'<code>letter[3]</code> has unexpected value <code>e</code>. The accepted values are: <code>{"a", "b", "c"}</code>',
						},
					},
				},
				{
					args = { {"Wisdom", "Power", "Courage", reference = "[[Triforce]]"}, "foo", "triforcePiece" },
					expect = {
					category = "Category:Articles Using Invalid Arguments in Template Calls",
						messages = {
							"<code>triforcePiece</code> has unexpected value <code>foo</code>. For a list of accepted values, refer to [[Triforce]].",
						},
					}
				},
			},
		}
	}
end

return p