Zelda Wiki

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

Come join the Zelda Wiki community Discord server!

READ MORE

Zelda Wiki
Advertisement

This module takes care of parsing and validating template input, allowing module developers to focus on core logic. It aims to achieve the same goal as Wikipedia's Module:Arguments but with a different approach:

  • Validation is based on a schema that is also used to auto-generate documentation. This guarantees that the documentation is up to date with the validation code.
  • No implicit behaviour. If you want to trim a parameter, you specify trim = true. If you want to treat empty strings as nil, you specify nilIfEmpty = true. Module:Arguments does several things "automagically" by default and lacks clarity as a result.
  • No frame handling. It's up to the caller to consolidate frame.args and frame:getParent().args if needed. This can be done with utilsTable.merge. Most modules typically need only one frame anyway.
This module exports the following functions.

parse

parse(frameArgs, templateSpec)

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

Parameters

Returns

  • A table of arguments parsed from the template input.
  • A table of validation errors, or nil if there are none. The error messages are also logged using mw.addWarning.

Examples

#InputOutputStatus
Positional arguments are assigned to their names.
1
local args = {"OoT", page = "Boss Key"}
return utilsArg.parse(args, {
  params = {
    [1] = {
      name = "game",
      desc = "A game",
    },
    page = {
      desc = "A page on the wiki",
    },  
  }
})
{
  page = "Boss Key",
  game = "OoT",
}
Green check
nil
Green check
Special parameter ... is used to parse an array of trailing template arguments
2
local args = {"OoT", "MM", "TWW", "TP"}
return utilsArg.parse(args, {
  params = {
    ["..."] = {
      name = "games",
    },
  },
})
{
  games = {"OoT", "MM", "TWW", "TP"},
}
Green check
nil
Green check
... used with other positional args
3
local args = {"foo", "bar", "OoT", "MM", "TWW", "TP"}
return utilsArg.parse(args, {
  params = {
    [1] = {
      name = "foo"
    },
    [2] = {
      name = "bar",
    },
    ["..."] = {
      name = "games",
    },
  },
})
{
  foo = "foo",
  bar = "bar",
  games = {"OoT", "MM", "TWW", "TP"},
}
Green check
nil
Green check
Validation of required arguments.
4
local args = {nil, nil, "Baz"}
return utilsArg.parse(args, {
  params = {
    [1] = {
      name = "foo",
      required = true,
    },
    [2] = {
      name = "bar",
      required = "Category:Custom Category Name",
    },
    [3] = {
      name = "baz",
      required = true,
    },
  }
})
{ baz = "Baz" }
Green check
{
  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
Validation of deprecated arguments.
5
local args = {
  oldArg = "foo",
  oldArg2 = "bar",
}
return utilsArg.parse(args, {
  params = {
    oldArg = {
      deprecated = true
    },
    oldArg2 = {
      deprecated = "Category:Custom Deprecation Category",
    },
  }
})
Expected
{ oldArg = "foo" }
Actual
{
  oldArg = "foo",
  oldArg2 = "bar",
}
TFH Red Link desperate
Expected
{
  categories = {"Category:Pages with Deprecated Arguments"},
  args = {
    oldArg = {
      {
        msg = "<code>oldArg</code> is deprecated but has value <code>foo</code>.",
        category = "Category:Pages with Deprecated Arguments",
      },
    },
  },
}
Actual
{
  categories = {
    "Category:Pages with Deprecated Arguments",
    "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>.",
      },
    },
    oldArg2 = {
      {
        category = "Category:Pages with Deprecated Arguments",
        msg = "<code>oldArg2</code> is deprecated but has value <code>bar</code>.",
      },
    },
  },
}
TFH Red Link desperate
Using an unknown parameter counts as an error.
6
local args = {
  foo = "bar"
}
return utilsArg.parse(args, {
  params = {} -- template has no args defined and yet "foo" is used as one
})
{}
Green check
{
  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
Can parse numbers
7
local args = {
  foo = "9000",
}
return utilsArg.parse(args, {
  params = {
    foo = {
      type = "number"
    }
  }
})
{ foo = 9000 }
Green check
nil
Green check
Returns an error if a non-number is passed when a number is expected.
8
local args = {
  foo = "notANumber",
}
return utilsArg.parse(args, {
  params = {
    foo = {
      type = "number"
    }
  }
})
{ foo = "NaN" }
Green check
{
  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
trim can be set on a parameter so that utilsString.trim is called for the argument.
9
local args = {" foo   \n"}
return utilsArg.parse(args, {
  params = {
    [1] = {
      name = "someParam",
      trim = true,
    },
  }
})
{ someParam = "foo" }
Green check
nil
Green check
nilIfEmpty can be set on a parameter so that utilsString.nilIfEmpty is called for the argument.
10
local args = {
  foo = ""
}
return utilsArg.parse(args, {
  params = {
    foo = {
      nilIfEmpty = true,
    },
  }
})
{}
Green check
nil
Green check
split can be set on a parameter so that utilsString.split is called for the argument.
11
local args = {
  foo = "  a, b  , c"
}
return utilsArg.parse(args, {
  params = {
    foo = {
      trim = true,
      split = true,
    }
  }
})
{
  foo = {"a", "b", "c"},
}
Green check
nil
Green check
split using a custom splitting pattern
12
local args = {
  foo = "abc"
}
return utilsArg.parse(args, {
  params = {
    foo = {
      split = "",
    }
  }
})
{
  foo = {"a", "b", "c"},
}
Green check
nil
Green check
If nilIfEmpty and required are set, then the argument is invalid if it is an empty string.
13
local args = {""}
return utilsArg.parse(args, {
  params = {
    [1] = {
      name = "game",
      nilIfEmpty = true,
      required = true,
      desc = "A game",
    },
  }
})
{}
Green check
{
  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
If trim, nilIfEmpty, and required are set, then the argument is invalid if it is a blank string.
14
local args = {"  \n"}
return utilsArg.parse(args, {
  params = {
    [1] = {
      name = "game",
      desc = "A game",
      trim = true,
      nilIfEmpty = true,
      required = true,
    },
  }
})
{}
Green check
{
  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
enum validation.
15
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
{
  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
split is used to parse comma-separated strings as arrays. Each array item can be validated against an enum.
16
local args = {
  games = "OoT, fakeGame, BotW",
}
return utilsArg.parse(args, {
  params = {
    games = {
      split = true,
      enum = Franchise.enum(),
    },
  },
})
{
  games = {"OoT", "fakeGame", "BotW"},
}
Green check
{
  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
enum can be written as a function, when the list of acceptable values depends on the value of another argument.
17
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
{
  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
If enumDependsOn refers to a required parameter, then enum is not evaluated when that parameter is nil.
18
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
{
  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
Altogether now
19
local args = {"Dinolfos", games = "OoT, MM", plural = nil}
return utilsArg.parse(args, {
  params = {
    [1] = {
      name = "term",
      nilIfEmpty = true,
      required = true,
    },
    games = {
      split = true,
      enum = Franchise.enum(),
    },
    plural = {
      deprecated = true,
    }
  }
})
{
  term = "Dinolfos",
  games = {"OoT", "MM"},
}
Green check
nil
Green check
20
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
{
  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
trim, nilIfEmpty, and validators such as enum are applied to individual trailing arguments
21
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
{
  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
repeatedGroup
22
local args = {
  tab1 = "Tab 1",
  content1 = "Content 1",
  
  tab2= "Tab 2",
  content2 = "Content 2",
  
  -- missing tab3, content3
  
  tab4 = "Tab 4",
  -- missing content4
  
  --missing tab5
  content5 = "Content 5",
}
return utilsArg.parse(args, {
  params = {
    ["tab"] = { required = true },
    ["content"] = { required = true },
  },
  repeatedGroup = {
    name = "tabs",
    params = {"tab", "content"},
  },
})
{
  tabs = {
    {
      tab = "Tab 1",
      content = "Content 1",
    },
    {
      tab = "Tab 2",
      content = "Content 2",
    },
    { tab = "Tab 4" },
    { content = "Content 5" },
  },
}
Green check
{
  categories = {
    "Category:Pages with Invalid Arguments",
    "Category:Pages with Invalid Arguments",
  },
  args = {
    tab = {
      {
        category = "Category:Pages with Invalid Arguments",
        msg = "<code>tab</code> is required but is <code>nil</code>.",
      },
    },
    content = {
      {
        category = "Category:Pages with Invalid Arguments",
        msg = "<code>content</code> is required but is <code>nil</code>.",
      },
    },
  },
}

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.
23
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
{
  {
    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
Passes schema validation.
24
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
nil
Green check

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 = {},
	}
	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
	-- Process any repeatedGroup params
	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
	
	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
	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
	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
		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 = "<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 = "<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>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
Advertisement