July 25, 2020

👭 Knight Challenge #11 👬

Want to try your hand at these challenges? There's a couple of things you can do!
From writing, to research, to images, find your preferred way to contribute with our eleventh theme: Couples!

Latest Announcements

Difference between revisions of "Module:UtilsArg"

From Zelda Wiki, the Zelda encyclopedia
Jump to: navigation, search
m
 
Line 140: Line 140:
 
arg = NOT_A_NUMBER
 
arg = NOT_A_NUMBER
 
else
 
else
arg = num or param.default
+
arg = num
 
end
 
end
 
end
 
end

Latest revision as of 23:26, 26 September 2020

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.

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