Module:LibraryUtil

-- From MediaWiki source file

- -- Find line/module name via `debug.stacktrace` for debugging - local function getCodeLocation(level) local trace = debug.traceback('', (level or 1) + 2) return ((trace:gsub('\nstack traceback:\n', ):match('^\t([^\n<>]+):')..':'):gsub('\t?%(tail call%): ?%?', )) end

-- Override default `mw.log` to add a code location if not _G.mwLogFuncChanged then local oldLog = mw.log mw.oldLog = oldLog local function allToString(...) local t = {...} for i = 1, select('#', ...) do			t[i] = tostring(t[i]) end return table.concat(t, ' ') end function mw.log(...) return oldLog(getCodeLocation, allToString(...)) end function mw.logObject(...) return oldLog(getCodeLocation, mw.dumpObject(...)) end _G.mwLogFuncChanged = true end

- -- Create an argument number/name - local function makeArgNumber(val) return table.concat{ type(val) == 'number' and '#' or '\, val, type(val) ~= "number" and '\ or '' } end

- -- Get the namae of a function based off of a stack level using `debug.stracktrace` - local function getParentName(level) level = (level or 0) + 3 local stack = ((debug.traceback(, level) or )		:gsub('\nstack traceback:\n', )		:match('in function ([^\n]+)') or ) :gsub('^\'(.-)\'$', '%1') stack = (stack:match('[<>:]') or stack == '') and '?' or stack return stack end

- -- Utility function to find the index of a value in a table - local function indexOf(t, value) if type(t) ~= 'table' then return -1 end local i = 1 while t[i] do		if t[i] == value then return i end i = i+1 end return -1 end

- -- Helper function for `checkType` to check of a set of given types matches a value - local function typeMatches(val, types, nilOk, numberOk) local valType = type(val) local tpTypes = type(types) local tn = tonumber(val) local isStringNumber = not not (tn and (types == 'string' or indexOf(types, 'string') ~= -1)) if tpTypes == 'table' and #types == 1 then types = types[1]; tpTypes = 'string' end if valType == 'nil' and nilOk then return true elseif val == nil and types == nil then return false end if tpTypes == 'string' then return (valType == types or (types == 'number' and not not tn)) or (isStringNumber and numberOk) end for i = 1, #types, 1 do		local v = types[i] if v == valType or (v == 'number' and tn) or isStringNumber then return true end end return false end

- -- Format a list of types in an error message for `checkType` and it's related functions - local function generateTypes(types) if type(types) == "string" then return types end local n = #types

if n == 1 then return types[1] end

return table.concat(types, '/') end

- -- Format an error message for `checkType` and related functions - local function generateMsg(name, index, msg, types) local msg = string.format("bad argument %s to '%s' (%s)",		makeArgNumber(index),		name,		types and generateTypes(types)..' expected, got '..msg or msg  ); return msg end

- -- Data for `checkType` - _G.typeMatches = typeMatches _G.getParentName = getParentName _G.makeArgNumber = makeArgNumber _G.generateMsg = generateMsg

local validTypes = { ['string']=true, ['number']=true, ['nil']=true, ['table']=true, ['boolean']=true, ['function']=true, ['any']=true, } - -- Check if a set of given types is valid - function validateTypes(types) local tp = type(types) local len = tp == 'table' and #types if not types or (tp ~= "table" and tp ~= "string") or types == "" or (tp == 'table' and len > 7) then return false end if (tp == 'string' or tp == 'table') and len == 0 then return true end if len == 1 and tp == 'table' then types = types[1]; tp = 'string' end if tp == "string" then return not not validTypes[types] end for i = 1, len, 1 do		if not validTypes[types[i]] then return false end end return true end

- -- function: checkType -- -- modes: --  type checker: checkType(name?: string, pos: number, value: any, types: string|table, nilOk?: boolean) --  no value error: checkType(name: 'no value', pos: number, types?: string|table, level?: number)

-- Options to types: --  *name: Changes the function to the value provided --  *base: Makes the value provided have the correct number base - function checkType(name, argIdx, arg, expectTypes, nilOk) local isConstructor, tpName if name == true then name = nil isConstructor = true end tpName = type(name) -- Argument overloads if (validateTypes(arg) and tpName == "number" and not validateTypes(expectTypes)) or (name == nil and argIdx == nil) then nilOk = expectTypes expectTypes = arg or 'string' arg = argIdx or nil argIdx = name or 1 name = nil end name = name or expectTypes.name local t = {} local tpArg = type(arg) t.checkType = error -- Check if types match and if there is an error tpName = type(name) local level = isConstructor and 1 or (tpName == "number" and name or 0) local matches = name ~= 'no value' and typeMatches(arg, expectTypes, nilOk, (expectTypes and expectTypes.numberOk)) local isError = (tpName == "string" and name ~= 'no value') or not matches local fName -- Optimize as much as possible if isError then fName = (name ~= 'no value' and tpName ~= 'number') and name or getParentName(name == 'no value' and expectTypes or level) end if name == 'no value' then t.checkType(generateMsg(fName, argIdx, arg and 'no value' or 'value expected', arg), (expectTypes or level)+3) end -- Check if `base` is valid if expectTypes.base and (expectTypes.base < 2 or expectTypes.base > 36) then error('the option "base" must be between 2 and 36', 2) end local numArg = tonumber(arg, expectTypes.base) -- Return if `numArg` is valid if expectTypes and #expectTypes == 0 then if tpArg == 'string' and numArg then return numArg end return arg end -- Check if types are valid if not validateTypes(expectTypes) then error('invalid types given', 2) end -- Check if argument is convertable to a number if expectTypes.base and not numArg then local msg = generateMsg(fName, argIdx, 'value is not convertable to a base '..expectTypes.base..' number') t.checkType(msg, level+3) elseif expectTypes.base and numArg then return numArg end -- Error if types did not match if not matches then local msg = generateMsg(fName, argIdx, tpArg, expectTypes) t.checkType(msg, level+3) end return arg end

- -- function: checkArgs(types: table, ...arguments?: any) -- -- Checks a given set of arguments against a list of types in a compact and streamlined manner - function checkArgs(types, ...) checkType(1, types, { 'string', 'table' }) if type(types) == 'string' then types = { types } end local t = {} -- Number of arguments local n = select('#', ...) t.checkTypeArgs = error local level = type(types.level) == "number" and (types.level >= -1 and types.level or -types.level) or 0 local len = #types -- Valid types local validTypes = { ['string']=true, ['nil']=true, ['number']=true, ['table']=true, ['boolean']=true, ['function']=true, ['any']=true, ['stringable']=true, }	local parentName = getParentName(-1)

local toIter if types.strict then toIter = (len >= n and len or n)	else toIter = len end local fName = getParentName(level) for i = 1, (len >= n and len or n) do		local any -- Variables local curTypes = type(types[i]) ~= "table" and { types[i] } or types[i] local emptyOk = curTypes.emptyOk local nilOk = curTypes.nilOk local val = ({ ... })[i] local argIndex = select('#', ...)+1 if i > #types and types.strict then t.checkTypeArgs(('bad argument #%d to \'%s\' (no value was expected)'):format(i, fName), level+3) end -- Case for nilOk if nilOk and emptyOk == nil then emptyOk = true end -- Special Case if nilOk is false and emptyOk is false if nilOk and emptyOk == false then table.insert(curTypes, 'nil') end -- Special case for the 'any' type if indexOf(curTypes, 'any') ~= -1 or (emptyOk == false and #curTypes == 0) then any = true curTypes = { nilOk = nilOk, emptyOk = emptyOk, }		end -- Sort types table.sort(curTypes) -- Error message #1 local valueExpected = ('bad argument #%d to \'%s\' (value expected)'):format(i, fName) local tpVal = type(val) -- If argument is nil, but not empty, and the expected type is 'any' and nilOk is false, error if nilOk == false and any and not (n n) and tpVal == 'nil' then t.checkTypeArgs(valueExpected, level+3) end -- If argument is empty if n n and (not emptyOk or tpVal == 'nil') then if any then -- Value expected error if argument is empty t.checkTypeArgs(valueExpected, level+3) elseif not nilOk or (nilOk and emptyOk == false) then -- No value error t.checkTypeArgs(generateMsg(fName, argIndex, 'no value', generateTypes(curTypes)), level+3) end else curTypes.name = fName -- Else check its type as normal checkType((level ~= 0 and level+1 or 1), i, val, curTypes, nilOk) end end return ... end

checkTypeArgs = checkArgs

- -- Alias for `checkType` - function checkTypeMulti(t, types) checkType(1, t, 'table') checkType(2, types, 'table') t.checkTypeMulti = error for i = 1, #types do		checkType(true, i, t[i], types[i]) end end

- -- function: alertDeprecation(name: string, useInstead?: string, level?: number) -- -- Throws a derecation error message with an optional substitute for the deprecated function - function alertDeprecation(...) local name, useInstead, level = checkTypeArgs({ 		'string', { 'string', nilOk=true }, { 'number', nilOk=true } 	}, ...) local t = {} t.alertDeprecation = error if type(name) == "table" then name, useInstead = unpack{ name.name or name[1], name.useInstead or name.use or name[2], }	end t.alertDeprecation(string.format( 'function %q is deprecated%s', name or getParentName, useInstead and string.format(', use the function %q instead', useInstead) or '' ), (level or 0)+3) end

- -- function: forEachArgs(types: string, ...arguments?: string) -- -- Returns an iterator which iterates over the list of given arguments, and asserts the type of each argument. -- Useful in variable argument functions. - function forEachArgs(types, ...) checkType(1, types, { 'string', 'table' }) local startIndex = types.startIndex and types.startIndex-1 or 0 local required = types.required or 0 local i = 0+(startIndex or 0) local args = { ... }	local lim = select('#', ...) local ind = indexOf(types, 'any') local t = {} t.checkType = error return function i = i + 1 if lim lim then if ind ~= -1 then t.checkType(generateMsg(getParentName, i, 'value expected'), 3) elseif ind == -1 then checkType('no value', i, types, 1) end end if i <= lim then if ind == -1 then checkType(1, i, args[i], types, types.nilOk) end return i, args[i], args else return nil, nil end end, ... end

- -- function: makeCheckSelfFunction(libraryName: string, varName: string, selfObj?: table, selfObjDesc?: string) -- -- Creates a function which asserts that the given object is an instance of another. - function makeCheckSelfFunction(libraryName, varName, selfObj, selfObjDesc) if type(varName) == 'table' then varName = libraryName selfObjDesc = selfObj end if type(libraryName) == 'table' then selfObj = libraryName libraryName = varName end return function(self, method) if self ~= selfObj then method = method or getParentName; ({ checkSelf=error })['checkSelf'](string.format( "%s: invalid %s. Did you call .%s with a dot instead of a colon, i.e. " .. "%s.%s(...) instead of %s:%s(...)?", libraryName, selfObjDesc or 'self object', method, varName, method, varName, method ), 3)		end end end

- -- function: formattedError(formatStr?: string, level?: number, ...substitions?: string | number) -- -- Throws an error but with an option to use `string.format`. - function formattedError(formatStr, level, ...) checkType(1, formatStr, { 'string' }, true) checkType(2, level, { 'number' }, true)

local t = { ... }

if #t > 0 then for i, v in ipairs(t) do			t[i] = tostring(v) end end local formatStr = type(formatStr) == 'string' and formatStr or 'unknown error' return ({ formattedError=error })['formattedError'](string.format(formatStr, unpack(t)), tonumber(level) == 0 and level or (level or 1)+1) end

- -- function: formattedAssert(v?: any, formatStr?: string, level?: number, ...substitions?: string | number) -- -- Works like the native `assert` but allows an option for using `formattedError`. - function formattedAssert(v, formatStr, level, ...) checkType(2, formatStr, { 'string' }, true) checkType(3, level, { 'number' }, true) local formatStr = type(formatStr) == 'string' and formatStr or 'assertion failed!' if not v then formattedError(formatStr, level == 0 and level or (level or 1)+1, ...) else return v	end end

- -- Inverse alias for `formattedAssert` - function assertFalse(v, formatStr, level, ...) checkType(2, formatStr, { 'string' }, true) checkType(3, level, { 'number' }, true) local formatStr = type(formatStr) == 'string' and formatStr or 'assertion failed!' if v then formattedError(formatStr, level == 0 and level or (level or 1)+1, ...) else return v	end end

assertTrue = formattedAssert

- -- function: pipeline(...) -- -- Creates a function pipeline, where each argument is stored then passed to a function -- when it is found in the arguments list, then it is invoked. - function pipeline(...) local value = checkArgs('any', ...) local prependArgs = { value } local t, functionFound for k, v in forEachArgs({ 'any', startIndex=2 }, ...) do		if type(v) ~= 'function' then if #prependArgs == 0 then prependArgs[#prependArgs+1] = value end prependArgs[#prependArgs+1] = v		else t = { pcall(v, unpack(prependArgs)) } functionFound = true if not t[1] then t[2] = tostring(t[2]) -- Error message if invocation failed assertTrue(					t[1], 					'Exception in calling function%s%s at que posistion #%d: %s', 					2, 					t[2]:match('^(.-):(.-):') and ' in module "'..t[2]:match('^(.-):(.-):')..'"' or ,					t[2]:match('^(.-):(.-):') and ' at line '..({ t[2]:match('^(.-):(.-):') })[2]..' called' or ,					k,					t[2]:gsub('^(.-):(.-):', '')				) end table.remove(t or {}, 1) prependArgs = t		end end if functionFound then return unpack(t) else return ... end end

- -- deprecated - function customArgError(cond, name, index, msg, ...) local tmp = { ... }	local t = {} t.assertTrue = error if #tmp == 1 and type(tmp[1]) == "table" then params = tmp[1] else params = tmp end -- mw.log('[WARN] [Deprecation] usage of `customArgError` is deprecated, use `assertTrue` instead.') if cond then t.assertTrue(string.format('bad argument to %s to \'%s\' (%s)', makeArgNumber(index), name, params and string.format(msg, unpack(params)) or msg ), 3) end end

- -- function: bind(targetFn: function, ...items?: any) -- -- Wraps `targetFn` in a new function which serves as a proxy prepending any arguments -- given when the new function is called - function bind(targetFn, ...) local mt = getmetatable(targetFn) if not (mt and (mt.__call or mt.__isClass)) then checkType(1, targetFn, 'function') end local args = { ... }	return function(...) for _, v in ipairs{ ... } do			table.insert(args, v)		end local values = { targetFn(unpack(args)) } return unpack(values) end end

_G.getStackName = getParentName return _G