Documentation for this module may be created at Module:UtilsArg/doc
local p = {} local h = {} local validators = {} local utilsString = require("Module:UtilsString") local utilsTable = require("Module:UtilsTable") local utilsVar = require("Module:UtilsVar") local CAT_DEPRECATED_PARAMS = "Category:"..require("Module:Constants/category/deprecatedParams") local CAT_INVALID_ARGS = "Category:"..require("Module: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 h.warn(errMessage) local utilsError = require("Module:UtilsError") utilsError.warn(errMessage) end function p.parse(frameArgs, templateSpec) local args = {} local unknownParams = utilsTable.clone(frameArgs) local repeatedParams = templateSpec.repeatedGroup and templateSpec.repeatedGroup.params or {} local repeatedAllowSingle = templateSpec.repeatedGroup and templateSpec.repeatedGroup.allowSingle 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] or repeatedAllowSingle 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.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) if arg[#arg] == "" then -- a trailing comma creates an extra blank entry at the end of the array - here we remove it table.remove(arg, #arg) end end if param.type == "boolean" then arg = arg and utilsString.trim(arg) if arg == nil then arg = nil elseif arg == "false" or arg == "no" or arg == "0" then arg = false else arg = true end elseif param.type == "number" then arg = arg and utilsString.trim(arg) local num = tonumber(arg) if arg == "" then arg = nil elseif num then arg = num end end if arg == nil then arg = param.default 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 = "Unknown", 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 booleans", snippet = "Boolean", expect = { { foo = true, bar = false, baz = true, }, nil } }, { desc = "Can parse numbers", snippet = "Number", expect = { { foo = 9000, bar = nil, }, 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. Trailing commas are supported", 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