OoT Navi.png

Hey! Listen!

This wiki contains spoilers! Read at your own risk!

Module:UtilsArg

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:

  • Parameter validation is based on the same data as the documentation. This ensures that the documentation is always accurate.
  • 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. This is trivial to do—most of the time a module only needs one or the other, and utilsTable.merge can be used when this is not the case.
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.
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.svg
nil
Green check.svg
Special parameter ... is used to parse an array of trailing template arguments
local args = {"OoT", "MM", "TWW", "TP"}
return utilsArg.parse(args, {
  params = {
    ["..."] = {
      name = "games",
    },
  },
})
{
  games = {"OoT", "MM", "TWW", "TP"},
}
Green check.svg
nil
Green check.svg
... used with other positional args
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.svg
nil
Green check.svg
Validation of required arguments.
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.svg
{
  categories = {
    "Category:Pages with Invalid Arguments",
    "Category:Custom Category Name",
  },
  args = {
    bar = {
      {
        category = "Category:Custom Category Name",
        msg = "<code>bar</code> is required but is <code>nil</code>.",
      },
    },
    foo = {
      {
        category = "Category:Pages with Invalid Arguments",
        msg = "<code>foo</code> is required but is <code>nil</code>.",
      },
    },
  },
}
Green check.svg
Validation of deprecated arguments.
local args = {
  oldArg = "foo"
}
return utilsArg.parse(args, {
  params = {
    oldArg = {
      deprecated = true
    }
  }
})
{ oldArg = "foo" }
Green check.svg
{
  categories = {"Category:Pages with Deprecated Arguments"},
  args = {
    oldArg = {
      {
        category = "Category:Pages with Deprecated Arguments",
        msg = "<code>oldArg</code> is deprecated but has value <code>foo</code>.",
      },
    },
  },
}
Green check.svg
Using an unknown parameter counts as an error.
local args = {
  foo = "bar"
}
return utilsArg.parse(args, {
  params = {} -- template has no args defined and yet "foo" is used as one
})
{}
Green check.svg
{
  categories = {"Category:Pages using Unknown Parameters"},
  args = {
    foo = {
      {
        message = "No such parameter <code>foo</code> is defined for this template.",
        category = "Category:Pages using Unknown Parameters",
      },
    },
  },
}
Green check.svg
Can parse numbers
local args = {
  foo = "9000",
}
return utilsArg.parse(args, {
  params = {
    foo = {
      type = "number"
    }
  }
})
{ foo = 9000 }
Green check.svg
nil
Green check.svg
Returns an error if a non-number is passed when a number is expected.
local args = {
  foo = "notANumber",
}
return utilsArg.parse(args, {
  params = {
    foo = {
      type = "number"
    }
  }
})
{ foo = "NaN" }
Green check.svg
{
  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>',
      },
    },
  },
}
Green check.svg
Default values
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.svg
nil
Green check.svg
trim can be set on a parameter so that utilsString.trim is called for the argument.
local args = {" foo   \n"}
return utilsArg.parse(args, {
  params = {
    [1] = {
      name = "someParam",
      trim = true,
    },
  }
})
{ someParam = "foo" }
Green check.svg
nil
Green check.svg
nilIfEmpty can be set on a parameter so that utilsString.nilIfEmpty is called for the argument.
local args = {
  foo = ""
}
return utilsArg.parse(args, {
  params = {
    foo = {
      nilIfEmpty = true,
    },
  }
})
{}
Green check.svg
nil
Green check.svg
split can be set on a parameter so that utilsString.split is called for the argument.
local args = {
  foo = "  a, b  , c"
}
return utilsArg.parse(args, {
  params = {
    foo = {
      trim = true,
      split = true,
    }
  }
})
{
  foo = {"a", "b", "c"},
}
Green check.svg
nil
Green check.svg
split using a custom splitting pattern
local args = {
  foo = "abc"
}
return utilsArg.parse(args, {
  params = {
    foo = {
      split = "",
    }
  }
})
{
  foo = {"a", "b", "c"},
}
Green check.svg
nil
Green check.svg
If nilIfEmpty and required are set, then the argument is invalid if it is an empty string.
local args = {""}
return utilsArg.parse(args, {
  params = {
    [1] = {
      name = "game",
      nilIfEmpty = true,
      required = true,
      desc = "A game",
    },
  }
})
{}
Green check.svg
{
  categories = {"Category:Pages with Invalid Arguments"},
  args = {
    game = {
      {
        category = "Category:Pages with Invalid Arguments",
        msg = "<code>game</code> is required but is <code>nil</code>.",
      },
    },
  },
}
Green check.svg
If trim, nilIfEmpty, and required are set, then the argument is invalid if it is a blank string.
local args = {"  \n"}
return utilsArg.parse(args, {
  params = {
    [1] = {
      name = "game",
      desc = "A game",
      trim = true,
      nilIfEmpty = true,
      required = true,
    },
  }
})
{}
Green check.svg
{
  categories = {"Category:Pages with Invalid Arguments"},
  args = {
    game = {
      {
        category = "Category:Pages with Invalid Arguments",
        msg = "<code>game</code> is required but is <code>nil</code>.",
      },
    },
  },
}
Green check.svg
enum validation.
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.svg
{
  categories = {
    "Category:Pages with Invalid Arguments",
    "Category:Pages with Invalid Arguments",
    "Category:Pages with Invalid Arguments",
  },
  args = {
    triforce2 = {
      {
        category = "Category:Pages with Invalid Arguments",
        msg = "<code>triforce2</code> has unexpected value <code>Limpah</code>. For a list of accepted values, refer to [[Triforce]].",
      },
    },
    game = {
      {
        category = "Category:Pages with Invalid Arguments",
        msg = "<code>game</code> has unexpected value <code>ALttZ</code>. For a list of accepted values, refer to [[Data:Franchise]].",
      },
    },
    triforce1 = {
      {
        category = "Category:Pages with Invalid Arguments",
        msg = '<code>triforce1</code> has unexpected value <code>Kooloo</code>. The accepted values are: <code>{"Courage", "Power", "Wisdom"}</code>',
      },
    },
  },
}
Green check.svg
split is used to parse comma-separated strings as arrays. Each array item can be validated against an enum.
local args = {
  games = "OoT, fakeGame, BotW",
}
return utilsArg.parse(args, {
  params = {
    games = {
      split = true,
      enum = Franchise.enum(),
    },
  },
})
{
  games = {"OoT", "fakeGame", "BotW"},
}
Green check.svg
{
  categories = {"Category:Pages with Invalid Arguments"},
  args = {
    games = {
      {
        category = "Category:Pages with Invalid Arguments",
        msg = "<code>games[2]</code> has unexpected value <code>fakeGame</code>. For a list of accepted values, refer to [[Data:Franchise]].",
      },
    },
  },
}
Green check.svg
sortAndRemoveDuplicates can be used alongside split and enum
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.svg
{
  categories = {"Category:Pages with Invalid Arguments"},
  args = {
    games = {
      {
        category = "Category:Pages with Invalid Arguments",
        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.
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.svg
{
  categories = {"Category:Pages with Invalid Arguments"},
  args = {
    term = {
      {
        category = "Category:Pages with Invalid Arguments",
        msg = '<code>term</code> has unexpected value <code>Dinolfos</code>. The accepted values are: <code>{"Dynalfos"}</code>',
      },
    },
  },
}
Green check.svg
If enumDependsOn refers to a required parameter, then enum is not evaluated when that parameter is nil.
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.svg
{
  categories = {"Category:Pages with Invalid Arguments"},
  args = {
    game = {
      {
        category = "Category:Pages with Invalid Arguments",
        msg = "<code>game</code> is required but is <code>nil</code>.",
      },
    },
  },
}
Green check.svg
Altogether now
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.svg
nil
Green check.svg
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.svg
{
  categories = {
    "Category:Pages with Invalid Arguments",
    "Category:Pages with Deprecated Arguments",
    "Category:Pages with Invalid Arguments",
    "Category:Pages with Invalid Arguments",
  },
  args = {
    plural = {
      {
        category = "Category:Pages with Deprecated Arguments",
        msg = "<code>plural</code> is deprecated but has value <code>true</code>.",
      },
    },
    games = {
      {
        category = "Category:Pages with Invalid Arguments",
        msg = "<code>games[1]</code> has unexpected value <code>YY</code>. For a list of accepted values, refer to [[Data:Franchise]].",
      },
      {
        category = "Category:Pages with Invalid Arguments",
        msg = "<code>games[2]</code> has unexpected value <code>ZZ</code>. For a list of accepted values, refer to [[Data:Franchise]].",
      },
    },
    term = {
      {
        category = "Category:Pages with Invalid Arguments",
        msg = "<code>term</code> is required but is <code>nil</code>.",
      },
    },
  },
}
Green check.svg
trim, nilIfEmpty, and validators such as enum are applied to individual trailing arguments
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.svg
{
  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]].",
      },
    },
  },
}
Green check.svg
repeatedGroup
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"] = {},
    ["content"] = {}
  },
  repeatedGroup = {
    name = "tabs",
    params = {"tab", "content"},
    count = 3,
  },
})
{
  tabs = {
    {
      tab = "Tab 1",
      content = "Content 1",
    },
    {
      tab = "Tab 2",
      content = "Content 2",
    },
    { tab = "Tab 4" },
    { content = "Content 5" },
  },
}
Green check.svg
nil
Green check.svg

schemaValidate

schemaValidate(schema, schemaName, arg, argName)

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.

Returns
  • If argument is invalid against the schema, returns the name of an error category, else returns nil.
  • A table of the validation errors logged using mw.addWarning, or nil if there were none.
Examples
InputOutputStatus
Fails schema validation.
local mySchema = {
  oneOf = {
    {
      type = "string"
    },
    {
      type = "array",
      items = {
        type = "string",
        enum = {"Kooloo", "Limpah"}
      }
    }
  }
}
local magicWords = {"Alakazam"}
return utilsArg.schemaValidate(mySchema, "mySchema", magicWords, "magicWords")
"Category:Pages with Invalid Arguments"
Green check.svg
{
  {
    path = "magicWords",
    msg = "<code>magicWords</code> does not match any <code>oneOf</code> sub-schemas.",
    errors = {
      {
        {
          msg = "<code>magicWords</code> is type <code>table</code> but type <code>string</code> was expected.",
          path = "magicWords",
        },
      },
      {
        {
          msg = '<code>magicWords[1]</code> has unexpected value <code>Alakazam</code>. The accepted values are: <code>{"Kooloo", "Limpah"}</code>',
          path = "magicWords[1]",
        },
      },
    },
  },
}
Green check.svg
Passes schema validation.
local mySchema = {
  oneOf = {
    {
      type = "string"
    },
    {
      type = "array",
      items = {
        type = "string",
        enum = {"Kooloo", "Limpah"},
      }
    }
  }
}
local magicWords = "Alakazam"
return utilsArg.schemaValidate(mySchema, "mySchema", magicWords, "magicWords")
nil
Green check.svg
nil
Green check.svg

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] = {{
			category = s("cat.unknownParams"),
			message = errMsg
		}}
		err.categories = utilsTable.concat(err.categories, s("cat.unknownParams"))
	end
	if #err.categories == 0 then
		err = nil
	end
	return args, err
end

function p.schemaValidate(schema, schemaName, arg, name)
	local err = utilsSchema.validate(schema, schemaName, arg, name)
	if err then
		return s("cat.invalidArgs"), err
	end
end

function h.isRepeated(repeatedParamsMap)
	-- @param param a template parameter e.g. "tab1"
	-- @return boolean indicating whether parameter is part of a repeated group
	-- @return number index of the repition, e.g. 1 for "tab1", 2 for "tab2", etc.
	-- @return name of the parameter without the index, e.g. "tab" for "tab1", "tab2", etc.
	return function(param)
		if type(param) == "number" then
			return false
		end
		local name = utilsString.trim(param, "0-9")
		local index = tonumber(string.sub(param, #name + 1))
		if not repeatedParamsMap[name] or not index then
			return false
		end
		return true, index, name
	end
end

function h.parseArg(arg, param)
	if arg and param.trim then
		arg = utilsString.trim(arg)
	end
	if arg and param.split then
		local pattern = type(param.split) == "string" and param.split or nil
		arg = utilsString.split(arg, pattern)
	end
	if param.nilIfEmpty then
		arg = utilsString.nilIfEmpty(arg)
	end
	if param.type == "number" then
		local num = tonumber(arg)
		if arg and not num and not param.default then
			arg = NOT_A_NUMBER
		else
			arg = num
		end
	end
	arg = arg or param.default
	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 = "<code>" .. name .. "</code> is expected to be a number but was: <code>" .. utilsTable.print(value) .. "</code>"
		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 <code>${param}</code> is defined for this template."
		}
	},
})

p.Schemas = {
	parse = {
		frameArgs = {
			type = "any",
			required = true,
			desc = "Table of arguments obtained from {{Scribunto Manual|lib=Frame object|frame object}}.",
		},
		templateSpec = {
			type = "any",
			required = true,
			desc = "[[Module:Documentation#Templates|Template documentation object]].",
		}
	}
}

p.Documentation = {
	parse = {
		desc = "This function validates template input and parses it into a table for use in the rest of the module.",
		params = {"frameArgs", "templateSpec"},
		returns = {
			"A table of arguments parsed from the template input.",
			"A table of validation errors, or nil if there are none. The error messages are also logged using {{Scribunto Manual|lib=mw.addWarning}}.",
		},
		cases = {
			outputOnly = true,
			{
				desc = "Positional arguments are assigned to their names.",
				snippet = "PositionalAndNamedArgs",
				expect = {
					{
						game = "OoT",
						page = "Boss Key",
					},
					nil
				}
			},
			{
				desc = "Special parameter <code>...</code> is used to parse an array of trailing template arguments",
				snippet = "TrailingArgs",
				expect = {
					{
						games = {"OoT", "MM", "TWW", "TP"}
					},
					nil
				}
			},
			{
				desc = "<code>...</code> used with other positional args",
				snippet = "TrailingArgsWithPositionalArgs",
				expect = {
					{
						foo = "foo",
						bar = "bar",
						games = {"OoT", "MM", "TWW", "TP"},
					},
					nil
				}
			},
			{
				desc = "Validation of required arguments.",
				snippet = "RequiredArgs",
				expect = {
					{
						baz = "Baz"
					},
					{
						categories = {
							"Category:Pages with Invalid Arguments",
							"Category:Custom Category Name",
						},
						args = {
							bar = {
								{
									category = "Category:Custom Category Name",
									msg = "<code>bar</code> is required but is <code>nil</code>.",
								},
							},
							foo = {
								{
									category = "Category:Pages with Invalid Arguments",
									msg = "<code>foo</code> is required but is <code>nil</code>.",
								},
							},
						},
					},
				},
			},
			{
				desc = "Validation of deprecated arguments.",
				snippet = "Deprecated",
				expect = {
					{ oldArg = "foo" },
					{
						categories = {"Category:Pages with Deprecated Arguments"},
						args = {
							oldArg = {
								{
									category = "Category:Pages with Deprecated Arguments",
									msg = "<code>oldArg</code> is deprecated but has value <code>foo</code>.",
								},
							},
						},
					},
				}
			},
			{
				desc = "Using an unknown parameter counts as an error.",
				snippet = "Unkown",
				expect = {
					{}, 
					{
						categories = {"Category:Pages using Unknown Parameters"},
						args = {
							foo = {
								{
									message = "No such parameter <code>foo</code> is defined for this template.",
									category = "Category:Pages using Unknown Parameters",
								},
							},
						},
					}
				},
			},
			{
				desc = "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 = '<code>foo</code> is expected to be a number but was: <code>"NaN"</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 = {
					{},
					{
						categories = {"Category:Pages with Invalid Arguments"},
						args = {
							game = {
								{
									category = "Category:Pages with Invalid Arguments",
									msg = "<code>game</code> is required but is <code>nil</code>.",
								},
							},
						},
					},
				},
			},
			{
				desc = "If <code>trim</code>, <code>nilIfEmpty</code>, and <code>required</code> are set, then the argument is invalid if it is a blank string.",
				snippet = "TrimNilIfEmptyRequired",
				expect = {
					{},
					{
						categories = {"Category:Pages with Invalid Arguments"},
						args = {
							game = {
								{
									category = "Category:Pages with Invalid Arguments",
									msg = "<code>game</code> is required but is <code>nil</code>.",
								},
							},
						},
					},
				},
			},
			{
				desc = "<code>enum</code> validation.",
				snippet = "Enum",
				expect = {
					{
						triforce2 = "Limpah",
						game = "ALttZ",
						triforce1 = "Kooloo",
					},
					{
						categories = {
							"Category:Pages with Invalid Arguments",
							"Category:Pages with Invalid Arguments",
							"Category:Pages with Invalid Arguments",
						},
						args = {
							triforce2 = {
								{
									category = "Category:Pages with Invalid Arguments",
									msg = "<code>triforce2</code> has unexpected value <code>Limpah</code>. For a list of accepted values, refer to [[Triforce]].",
								},
							},
							game = {
								{
									category = "Category:Pages with Invalid Arguments",
									msg = "<code>game</code> has unexpected value <code>ALttZ</code>. For a list of accepted values, refer to [[Data:Franchise]].",
								},
							},
							triforce1 = {
								{
									category = "Category:Pages with Invalid Arguments",
									msg = '<code>triforce1</code> has unexpected value <code>Kooloo</code>. The accepted values are: <code>{"Courage", "Power", "Wisdom"}</code>',
								},
							},
						},
					},
				},
			},
			{
				desc = "<code>split</code> is used to parse comma-separated strings as arrays. Each array item can be validated against an <code>enum</code>.",
				snippet = "SplitEnum",
				expect = {
					{
						games = {"OoT", "fakeGame", "BotW"},
					},
					{
						categories = {"Category:Pages with Invalid Arguments"},
						args = {
							games = {
								{
									category = "Category:Pages with Invalid Arguments",
									msg = "<code>games[2]</code> has unexpected value <code>fakeGame</code>. For a list of accepted values, refer to [[Data:Franchise]].",
								}
							}
						}
					}
				}
			},
			{
				desc = "<code>sortAndRemoveDuplicates</code> can be used alongside <code>split</code> and <code>enum</code>",
				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"
					},
					{
						categories = {"Category:Pages with Invalid Arguments"},
						args = {
							term = {
								{
									category = "Category:Pages with Invalid Arguments",
									msg = '<code>term</code> has unexpected value <code>Dinolfos</code>. The accepted values are: <code>{"Dynalfos"}</code>',
								},
							},
						},
					},
				},
			},
			{
				desc = "If <code>enumDependsOn</code> refers to a required parameter, then <code>enum</code> is not evaluated when that parameter is nil.",
				snippet = "EnumDependsOnNil",
				expect = {
					{ term = "Dinolfos" },
					{
						categories = {"Category:Pages with Invalid Arguments"},
						args = {
							game = {
								{
									category = "Category:Pages with Invalid Arguments",
						    		msg = "<code>game</code> is required but is <code>nil</code>.",
								}
							},
						},
					},
				},
			},
			{
				desc = "Altogether now",
				snippet = "TermStorePass",
				expect = {
					{
						term = "Dinolfos",
						games = {"OoT", "MM"},
					},
					nil
				}
			},
			{
				snippet = "TermStoreFail",
				expect = {
					{
						plural = "true",
						games = {"YY", "ZZ"},
					},
					{
						categories = {
							"Category:Pages with Invalid Arguments",
							"Category:Pages with Deprecated Arguments",
							"Category:Pages with Invalid Arguments",
							"Category:Pages with Invalid Arguments",
						},
						args = {
							term = {
								{
									category = "Category:Pages with Invalid Arguments",
									msg = "<code>term</code> is required but is <code>nil</code>.",
								},
							},
							games = {
								{
									category = "Category:Pages with Invalid Arguments",
									msg = "<code>games[1]</code> has unexpected value <code>YY</code>. For a list of accepted values, refer to [[Data:Franchise]]."
								},
								{
									category = "Category:Pages with Invalid Arguments",
									msg = "<code>games[2]</code> has unexpected value <code>ZZ</code>. For a list of accepted values, refer to [[Data:Franchise]]."
								},
							},
							plural = {
								{
									category = "Category:Pages with Deprecated Arguments",
									msg = "<code>plural</code> is deprecated but has value <code>true</code>.",
								},
							},
						},
					},
				},
			},
			{
				desc = "<code>trim</code>, <code>nilIfEmpty</code>, and validators such as <code>enum</code> are applied to individual trailing arguments",
				snippet = "TrailingArgsStringTrimNilIfEmptyEnum",
				expect = {
					{
						games = {"OoT", "MM", "ALttZ"},
					},
					{
						categories = {"Category:Pages with Invalid Arguments"},
						args = {
							games = {
							{
								category = "Category:Pages with Invalid Arguments",
								msg = "<code>games[3]</code> has unexpected value <code>ALttZ</code>. For a list of accepted values, refer to [[Data:Franchise]].",
							}
						},
						},
					},
				},
			},
			{
				desc = "repeatedGroup",
				snippet = "RepeatedGroup",
				expect = {
					{
						tabs = {
							{
								tab = "Tab 1",
								content = "Content 1",
							},
							{
								tab = "Tab 2",
								content = "Content 2",
							},
							{ tab = "Tab 4" },
							{ content = "Content 5" },
						}
					},
					nil
				}
			},
		},
	},
	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",
							msg = "<code>magicWords</code> does not match any <code>oneOf</code> sub-schemas.",
							errors = {
								{
									{
										msg = "<code>magicWords</code> is type <code>table</code> but type <code>string</code> was expected.",
										path = "magicWords",
									},
								},
								{
									{
										msg = '<code>magicWords[1]</code> has unexpected value <code>Alakazam</code>. The accepted values are: <code>{"Kooloo", "Limpah"}</code>',
										path = "magicWords[1]",
									},
								},
							},
						},
					},
				},
			},
			{
				desc = "Passes schema validation.",
				snippet = "Passes",
				expect = {nil, nil},
			},
		}
	},
}

return p