Module:UtilsArg

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", "type"} local NOT_A_NUMBER = "NaN"

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 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 {} args[repeated][index] = args[repeated][index] or {} utilsTable.merge(args[repeated][index], {					[param] = h.parseArg(v, paramSpec)				}) unknownParams[k] = nil end end args[repeated] = utilsTable.compact(args[repeated]) end -- Parse variadic args 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 -- Validate 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 -- 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 in pairs(unknownParams) do		local errMsg = s("msg.unknownParam", { param = k }) utilsError.warn(errMsg) err.args[k] = 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 if param.type == "number" then arg = tonumber(arg) or NOT_A_NUMBER 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, defaultCat if validatorData ~= nil then errorMessages, defaultCat = h[validator](validatorData, arg, paramName) end local cat = defaultCat if validator == "required" and type(validatorData) == "string" then cat = validatorData 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 function h.type(expectedType, value, name) if expectedType == "number" and value == NOT_A_NUMBER then local msg = " is expected to be a number but was:  " utilsError.warn(msg) return {msg}, s("cat.invalidArgs") 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  is defined for this template."		}	}, })

p.Schemas = { parse = { frameArgs = { type = "any", required = true, desc = "Table of arguments obtained from .", },		templateSpec = { type = "any", required = true, desc = "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 .", },		cases = { outputOnly = true, {				desc = "Positional arguments are assigned to their names.", snippet = "PositionalAndNamedArgs", expect = { {						game = "OoT", page = "Boss Key", },					nil }			},			{				desc = "Special parameter  is used to parse an array of trailing template arguments", snippet = "TrailingArgs", expect = { {						games = {"OoT", "MM", "TWW", "TP"} },					nil }			},			{				desc = " 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 = { bar = { {									category = "Category:Custom Category Name", msg = " is required but is  .", },							},							foo = { {									category = "Category:Pages with Invalid Arguments", msg = " is required but is  .", },							},						},					},				},			},			{				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 = " is deprecated but has value  .", },							},						},					},				}			},			{				desc = "Using an unknown parameter counts as an error.", snippet = "Unkown", expect = { {}, 					{						categories = {"Category:Pages using Unknown Parameters"}, args = { foo = { {									message = "No such parameter  is defined for this template.", category = "Category:Pages using Unknown Parameters", },							},						},					}				},			},			{				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 = "NaN" }, {						categories = {"Category:Pages with Invalid Arguments"}, args = { foo = { {									category = "Category:Pages with Invalid Arguments", msg = ' is expected to be a number but was:  ', },							},						},					},				},			},			{				desc = " can be set on a parameter so that utilsString.trim is called for the argument.", snippet = "Trim", expect = },			{				desc = " can be set on a parameter so that utilsString.nilIfEmpty is called for the argument.", snippet = "NilIfEmpty", expect = {{}, nil} },			{				desc = " can be set on a parameter so that utilsString.split is called for the argument.", snippet = "Split", expect = { { foo = {"a", "b", "c"} }, },			},			{				desc = " using a custom splitting pattern", snippet = "SplitPattern", expect = { { foo = {"a", "b", "c"} }, },			},			{				desc = "If  and   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 = " is required but is  .", },							},						},					},				},			},			{				desc = "If,  , and   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 = " is required but is  .", },							},						},					},				},			},			{				desc = " 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 = " has unexpected value  . For a list of accepted values, refer to Triforce.", },							},							game = { {									category = "Category:Pages with Invalid Arguments", msg = " has unexpected value  . For a list of accepted values, refer to Data:Franchise.", },							},							triforce1 = { {									category = "Category:Pages with Invalid Arguments", msg = ' has unexpected value. The accepted values are: ', },							},						},					},				},			},			{				desc = " is used to parse comma-separated strings as arrays. Each array item can be validated against an  .", snippet = "SplitEnum", expect = { {						games = {"OoT", "fakeGame", "BotW"}, },					{						categories = {"Category:Pages with Invalid Arguments"}, args = { games = { {									category = "Category:Pages with Invalid Arguments", msg = " has unexpected value  . For a list of accepted values, refer to Data:Franchise.", }							}						}					}				}			},			{				desc = " can be used alongside   and  ", snippet = "SplitEnumSortAndRemoveDuplicates", expect = { { games = {"OoT", "BotW"} }, nil },			},			{				desc = " 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 = ' has unexpected value. The accepted values are: ', },							},						},					},				},			},			{				desc = "If  refers to a required parameter, then   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 = " is required but is  .", }							},						},					},				},			},			{				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 = " is required but is  .", },							},							games = { {									category = "Category:Pages with Invalid Arguments", msg = " has unexpected value  . For a list of accepted values, refer to Data:Franchise." },								{									category = "Category:Pages with Invalid Arguments", msg = " has unexpected value  . For a list of accepted values, refer to Data:Franchise." },							},							plural = { {									category = "Category:Pages with Deprecated Arguments", msg = " is deprecated but has value  .", },							},						},					},				},			},			{				desc = ", , and validators such as   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 = " has unexpected value  . 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" }, }					},					nil }			},		},	},	schemaValidate = { desc = "This function validates an input argument against a schema. Currently, this function is not performant enough to be used in actual articles. It exists mainly to assist with building 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 error category, else returns .", "A table of the validation errors logged using, or  if there were none." },		cases = { outputOnly = true, {				desc = "Fails schema validation.", snippet = "Fails", expect = { s("cat.invalidArgs"), {						{							path = "magicWords", msg = " does not match any   sub-schemas.", errors = { {									{										msg = " is type   but type   was expected.", path = "magicWords", },								},								{									{										msg = ' has unexpected value. The accepted values are: ', path = "magicWords[1]", },								},							},						},					},				},			},			{				desc = "Passes schema validation.", snippet = "Passes", expect = {nil, nil}, },		}	}, }

return p