Module:UtilsArg

From Zelda Wiki, the Zelda encyclopedia
Jump to navigation Jump to search

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.svg
nil
Green check.svg
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.svg
nil
Green check.svg
... 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.svg
nil
Green check.svg
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.svg
{
  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.",
      },
    },
  },
}
Green check.svg
Arguments may have aliases when a high-usage parameter needs to be renamed.
5
local args = { oldName = "foo" }
return utilsArg.parse(args, {
  params = {
    newName = {
      aliases = {"oldName"}  
    },
  }
})
{
  newName = "foo",
  oldName = "foo",
}
Green check.svg
nil
Green check.svg
Validation of deprecated parameters.
6
local args = {
  oldArg = "foo",
  oldArg2 = "bar",
}
return utilsArg.parse(args, {
  params = {
    oldArg = {
      deprecated = true
    },
    oldArg2 = {
      deprecated = "Category:Custom Deprecation Category",
    },
  }
})
{
  oldArg = "foo",
  oldArg2 = "bar",
}
Green check.svg
{
  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>.",
      },
    },
  },
}
Green check.svg
Using an unknown parameter counts as an error.
7
local args = {
  foo = "bar"
}
return utilsArg.parse(args, {
  params = {} -- template has no args defined and yet "foo" is used as one
})
{}
Green check.svg
{
  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",
      },
    },
  },
}
Green check.svg
Can parse booleans
8
local args = {
  foo = "anything",
  bar = "false",
  baz = " ",
}
return utilsArg.parse(args, {
  params = {
    foo = {
      type = "boolean"
    },
    bar = {
      type = "boolean",
    },
    baz = {
      type = "boolean",
    },
  }
})
{
  foo = true,
  bar = false,
  baz = true,
}
Green check.svg
nil
Green check.svg
Can parse numbers
9
local args = {
  foo = "9000",
  bar = " ",
}
return utilsArg.parse(args, {
  params = {
    foo = {
      type = "number"
    },
    bar = {
      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.
10
local args = {
  foo = "notANumber",
}
return utilsArg.parse(args, {
  params = {
    foo = {
      type = "number"
    }
  }
})
{ foo = "notANumber" }
Green check.svg
{
  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>',
      },
    },
  },
}
Green check.svg
Default values
11
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.
12
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.
13
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. Trailing commas are supported
14
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
15
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.
16
local args = {""}
return utilsArg.parse(args, {
  params = {
    [1] = {
      name = "game",
      nilIfEmpty = true,
      required = true,
      desc = "A game",
    },
  }
})
{}
Green check.svg
{
  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.",
      },
    },
  },
}
Green check.svg
If trim, nilIfEmpty, and required are set, then the argument is invalid if it is a blank string.
17
local args = {"  \n"}
return utilsArg.parse(args, {
  params = {
    [1] = {
      name = "game",
      desc = "A game",
      trim = true,
      nilIfEmpty = true,
      required = true,
    },
  }
})
{}
Green check.svg
{
  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.",
      },
    },
  },
}
Green check.svg
enum validation.
18
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
{
  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>',
      },
    },
  },
}
Green check.svg
split is used to parse comma-separated strings as arrays. Each array item can be validated against an enum.
19
local args = {
  games = "OoT, fakeGame, BotW",
}
return utilsArg.parse(args, {
  params = {
    games = {
      split = true,
      enum = Franchise.enum(),
    },
  },
})
{
  games = {"OoT", "fakeGame", "BotW"},
}
Green check.svg
{
  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]].",
      },
    },
  },
}
Green check.svg
sortAndRemoveDuplicates can be used alongside split and enum. Entries are sorted to match the sort order of the enum.
20
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
{
  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>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.
21
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
{
  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>',
      },
    },
  },
}
Green check.svg
If enumDependsOn refers to a required parameter, then enum is not evaluated when that parameter is nil.
22
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
{
  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.",
      },
    },
  },
}
Green check.svg
Altogether now
23
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
24
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
{
  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 = {
    plural = {
      {
        category = "Category:Articles using deprecated parameters in template calls",
        msg = "<code>plural</code> is deprecated but has value <code>true</code>.",
      },
    },
    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]].",
      },
    },
    term = {
      {
        category = "Category:Articles using invalid arguments in template calls",
        msg = "<code>term</code> parameter is required.",
      },
    },
  },
}
Green check.svg
trim, nilIfEmpty, and validators such as enum are applied to individual trailing arguments
25
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
{
  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]].",
      },
    },
  },
}
Green check.svg
repeatedGroup
26
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.svg
{
  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.",
      },
    },
  },
}
Green check.svg

enum

enum(enum)

This function validates that a value (or each value in an list) is contained within an enum list.

Returns

  • List of error messages as well as an error category, or nil if there are no errors. Any error messages are logged using mw.addWarning.

Examples

#InputOutputResultStatus
27
enum({"a", "b", "c"}, "a", "letter")
nil
Green check.svg
28
enum({"a", "b", "c"}, "d", "letter")
{
  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>',
  },
}
table
Green check.svg
29
enum({"a", "b", "c"}, {"c", "d", "e"}, "letter")
{
  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>',
  },
}
table
Green check.svg
30
enum(
  {
    "Wisdom",
    "Power",
    "Courage",
    reference = "[[Triforce]]",
  },
  "foo",
  "triforcePiece"
)
{
  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]].",
  },
}
table
Green check.svg

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