Module:UtilsArg

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

local utilsString = require("Module:UtilsString") local utilsTable = require("Module:UtilsTable") local utilsVar = require("Module:UtilsVar")

h.templatePage = mw.getCurrentFrame:getParent:getTitle h.instanceCounter = utilsVar.counter("instanceCounter"..h.templatePage) h.instanceCounter.increment

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]) -- in case a number is accidentally skipped for a whole "row" 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 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]) 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  is defined for this template. Value:  ", k, v)		h.warn(errMsg) err.args[k] = err.categories = utilsTable.concat(err.categories, "Category:Pages 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 return args, err end

function h.warn(errMessage) local fullMessage = string.format("Misuse of %s at instance %s: %s", h.templatePage, h.instanceCounter.value, errMessage) mw.addWarning(fullMessage) -- We also log the message as mw.addWarning currently doesn't work if the editor has enabled the preference "Show previews without reloading the page". -- Unfortunately neither mw.addWarning nor mw.log work with the visual editor (even in source mode). -- An alternative would be to throw an actual script error as most wikis do. -- However, full errors degrade the reader experience. -- Many of our validation errors are not so critical that readers should know that they even exist. mw.log(fullMessage) 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 arg and param.nilIfEmpty then arg = utilsString.nilIfEmpty(arg) 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(" parameter is required.", name) h.warn(err) return {err}, "Category:Pages using invalid arguments in template calls" 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(" has unexpected value  . For a list of accepted values, refer to %s.", path, v, enum.reference) else local acceptedValues = utilsTable.print(enum, true) msg = string.format(" has unexpected value  . The accepted values are:  ", path, v, acceptedValues) end table.insert(errors, msg) h.warn(msg) end end return errors, "Category:Pages using invalid arguments in template calls" end function validators.deprecated(deprecated, value, name) if not deprecated then return end if value ~= nil then local err = string.format(" is deprecated but has value  .", name, value) h.warn(err) return {err}, "Category:Pages using deprecated parameters in template calls" end end function validators.type(expectedType, value, name) if expectedType == "number" and tonumber(value) == nil then local msg = " is expected to be a number but was:  " h.warn(msg) return {msg}, "Category:Pages using invalid arguments in template calls" end end

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 using invalid arguments in template calls", "Category:Custom Category Name", },						args = { bar = { {									category = "Category:Custom Category Name", msg = " parameter is required.", },							},							foo = { {									category = "Category:Pages using invalid arguments in template calls", msg = " parameter is required.", },							},						},					},				},			},			{				desc = "Validation of deprecated parameters.", snippet = "Deprecated", expect = { { oldArg = "foo", oldArg2 = "bar" }, {						categories = {"Category:Custom Deprecation Category", "Category:Pages using deprecated parameters in template calls"}, args = { oldArg = { {									category = "Category:Pages using deprecated parameters in template calls", msg = " is deprecated but has value  .", },							},							oldArg2 = { {									category = "Category:Custom Deprecation Category", msg = " is deprecated but has value  .", },							},						},					},				}			},			{				desc = "Using an unknown parameter counts as an error.", snippet = "Unkown", expect = { {}, 					{						categories = {"Category:Pages using unknown parameters in template calls"}, args = { foo = { {									message = "No such parameter  is defined for this template. Value:  ", category = "Category:Pages 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" }, {						categories = {"Category:Pages using invalid arguments in template calls"}, args = { foo = { {									category = "Category:Pages using invalid arguments in template calls", msg = ' is expected to be a number but was:  ', },							},						},					},				},			},			{				desc = "Default values", snippet = "Default", expect = { { 						someParamWithDefault = "foo", someParamWithDefaultNumber = 1, param3 = "bar", param4 = "" }				}			},			{				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 using invalid arguments in template calls"}, args = { game = { {									category = "Category:Pages using invalid arguments in template calls", msg = " parameter is required.", },							},						},					},				},			},			{				desc = "If,  , and   are set, then the argument is invalid if it is a blank string.", snippet = "TrimNilIfEmptyRequired", expect = { {},					{						categories = {"Category:Pages using invalid arguments in template calls"}, args = { game = { {									category = "Category:Pages using invalid arguments in template calls", msg = " parameter is required.", },							},						},					},				},			},			{				desc = " validation.", snippet = "Enum", expect = { {						triforce2 = "Limpah", game = "ALttZ", triforce1 = "Kooloo", },					{						categories = { "Category:Pages using invalid arguments in template calls", "Category:Pages using invalid arguments in template calls", "Category:Pages using invalid arguments in template calls", },						args = { triforce2 = { {									category = "Category:Pages using invalid arguments in template calls", msg = " has unexpected value  . For a list of accepted values, refer to Triforce.", },							},							game = { {									category = "Category:Pages using invalid arguments in template calls", msg = " has unexpected value  . For a list of accepted values, refer to Data:Franchise.", },							},							triforce1 = { {									category = "Category:Pages using invalid arguments in template calls", 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 using invalid arguments in template calls"}, args = { games = { {									category = "Category:Pages using invalid arguments in template calls", msg = " has unexpected value  . For a list of accepted values, refer to Data:Franchise.", }							}						}					}				}			},			{				desc = " can be used alongside   and  . Entries are sorted to match the sort order of the enum.", 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 using invalid arguments in template calls"}, args = { term = { {									category = "Category:Pages using invalid arguments in template calls", 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 using invalid arguments in template calls"}, args = { game = { {									category = "Category:Pages using invalid arguments in template calls", msg = " parameter is required.", }							},						},					},				},			},			{				desc = "Altogether now", snippet = "TermStorePass", expect = { {						term = "Dinolfos", games = {"OoT", "MM"}, },					nil }			},			{				snippet = "TermStoreFail", expect = { {						plural = "true", games = {"YY", "ZZ"}, },					{						categories = { "Category:Pages using invalid arguments in template calls", "Category:Pages using deprecated parameters in template calls", "Category:Pages using invalid arguments in template calls", "Category:Pages using invalid arguments in template calls", },						args = { term = { {									category = "Category:Pages using invalid arguments in template calls", msg = " parameter is required.", },							},							games = { {									category = "Category:Pages using invalid arguments in template calls", msg = " has unexpected value  . For a list of accepted values, refer to Data:Franchise." },								{									category = "Category:Pages using invalid arguments in template calls", msg = " has unexpected value  . For a list of accepted values, refer to Data:Franchise." },							},							plural = { {									category = "Category:Pages using deprecated parameters in template calls", 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 using invalid arguments in template calls"}, args = { games = { {								category = "Category:Pages using invalid arguments in template calls", 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" }, }					},					{						categories = { "Category:Pages using invalid arguments in template calls", "Category:Pages using invalid arguments in template calls", },						args = { tab4 = { {									category = "Category:Pages using invalid arguments in template calls", msg = " parameter is required.", },							},							content3 = { {									category = "Category:Pages using invalid arguments in template calls", msg = " parameter is required.", },							},						},					},				},			},		},	} }

return p