Jump to content

Module:Sandbox/Grufo

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Uzume (talk | contribs) at 13:15, 18 July 2025 (improve parameter validation). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.

-- Example module to demonstrate how it can detect how it is loaded and why such can be useful
require[[strict]]
local currentFrame = mw.getCurrentFrame()
local parentFrame = currentFrame:getParent()
local function explist(...)
	-- Capture varargs as simple reproducible expression list; XXX: [[MapReduce]] would be cool
	return {['n']=select('#', ...), ...} -- Use 'n' /a la/ Lua 5.0 "setn", etc.
end
local pkgTitle = ... or currentFrame:getTitle()
local pkgAPI = {}

--[=[ Debug console tests: basic "smoke" tests to just ensure it is not entirely broken
=p.invoke(false, 'string', 'sub', {'xyzzy', '2', '4'})
=p.invoke(true, 'string', 'sub', {'xyzzy', '2', '4'})
=p.invoke(false, 'string', 'sub', {'xyzzy', '0', '0'})
=p.invoke(true, 'string', 'sub', {'xyzzy', '0', '0'})
=p.invoke(false, 'string', 'sub', {'xyzzy', '0', '0', no_category='1'})
=p.invoke(true, 'string', 'sub', {'xyzzy', '0', '0', no_category='1'})
]=]
function pkgAPI.invoke(emu, modname, fnname, modargs)
	local baseFrame = parentFrame or currentFrame -- No need to pass a frame with no parent
	--[=[ Input validation:
	Scribunto does a good job of its own input validation and throws errors when we pass these on
	but we spoof "modargs" to the target *raw*, so do some validation so the target gets as few
	surprises as possible.
	XXX: Copy 'modargs' and convert to proper format? Does 'callParserfunction' do that?
	]=]
	assert('string' == type(modname), 'Module name must a string')
	assert('string' == type(fnname), 'Function name must a string')
	for k, v in pairs(modargs) do
		local ktype = type(k)
		if 'number' == ktype then
			assert(math.floor(k) == k, 'Numeric parameter keys must be integers')
		elseif 'string' == ktype then
			k = tonumber(k)
			assert(nil == k or math.floor(k) ~= k, 'Integer parameter keys must be numbers')
		else
			error('Parameter keys must be strings or integers', 0)
		end
		assert('string' == type(v), 'Argument values must be strings')
	end

	if not emu then
		-- Copy "modargs" so we don't suddenly change them for the caller
		local parserfnargs = {}
		for k, v in pairs(modargs) do
			parserfnargs[k] = v
		end
		table.insert(parserfnargs, 1, fnname)
		table.insert(parserfnargs, 1, modname)
		return baseFrame:callParserFunction('#invoke', parserfnargs)
	end

	--[=[
	Emulate a "#invoke" call with "require", a frameSpoof and a monkey patched "getCurrentFrame"
	providing a spoofed "args", "getParent" and "getTitle"
	falling back to creating an on demand "newChild" frame for other fields
	]=]
	--XXX: mw.title.new(828, modname).prefixedText
	--XXX: mw.site.namespaces.Module.canonicalName .. ':' .. modname
	local modtitle = 'Module:' .. modname

	--[=[ Spoof a "#invoke" frame for the target:
	Scribunto frame objects are just regular Lua tables with method functions, however, those
	functions are also lexical closures that depends on their upvalues and require one to pass
	in the correct frame reference, so we cannot copy those function references into our
	"frameSpoof" but we can build redirector stub functions that punt calls to those, so long
	as we ensure we always call them with the right frame reference. We want the target module
	to never use these stubs as creating a frame via "newFrame" is expensive, so we go one
	further and instead create delay stub functions that only do that on demand when the target
	module tries to access any fields we do not end up directly spoofing. The delay stubs also
	have to punt to newly created frame but only after it has created it. Creating the new frame
	should only happen once so even though we create many delay stubs, only one will actually
	create it. To ensure that only one winning delay stub is ever called, each delay stub updates
	all fields of the "frameSpoof" to always punting stubs instead, thereby effectively caching
	the frame creation. So we start by filling all frame fields of "frameSpoof" with delay stubs
	that: first create a new frame, then cache it by update all frame fields with always punting
	stubs, and finally punt themselves. Of course the "args" fields is not a function so it we
	must handle it different but we at first ignore that and later ensure to overwrite it in
	both cases: when writing always punting stubs we just write the actual frame "args" value and
	when writing delay stubs we just write in our local "args" spoofer. Of course, we overwrite
	the delay stubs with a few more local spoofs that are easy to create and commonly accessed
	by the target: "getParent" and "getTitle". We might be able to improve performance by
	spoofing more fields but many call back into the PHP parser so they would be hard to emulate
	adequately without creating an actual parser frame and the ones we do spoof cover those
	most frequently accessed by any target module: the "args" and the parent "args".
	]=]
	local frameSpoof = {}
	-- Punt to "newChild" frame on demand for all function fields; Spoofed fields are overwritten later
	for name in pairs(baseFrame) do
		frameSpoof[name] = function(self, ...)
			local frameDelayed = baseFrame:newChild{title = modtitle, args = modargs}
			-- Overwrite all fields on demand effectively caching this so it will only happen once
			for name in pairs(frameDelayed) do
				frameSpoof[name] = function(self, ...)
					if self == frameSpoof then
						self = frameDelayed -- Only redirect correct value and let error propagate
					end
					-- All future accesses will punt since we made an actual frame we lose nothing
					return frameDelayed[name](self, ...)
				end
			end
			-- Overwrite "args" again because it is not a function
			frameSpoof.args = frameDelayed.args
			if self == frameSpoof then
				self = frameDelayed -- Only redirect correct value and let error propagate
			end
			-- First punt after updating all fields to punt in the future
			return frameDelayed[name](self, ...)
		end
	end
	-- Spoof "getParent"
	function frameSpoof:getParent(...)
		if self ~= frameSpoof then
			return baseFrame.getParent(self, ...) -- Let a real frame throw the error
		end
		return baseFrame
	end
	-- Spoof "getTitle"
	function frameSpoof:getTitle(...)
		if self ~= frameSpoof then
			return baseFrame.getTitle(self, ...) -- Let a real frame throw the error
		end
		return modtitle
	end
	-- Spoof "args" including recreating a metatable for read-only access to upvalues
	local args_mt = {}
	frameSpoof.args = setmetatable({}, args_mt)
	args_mt.__index = modargs
	function args_mt:__pairs()
		return pairs(modargs)
	end
	function args_mt:__ipairs()
		return ipairs(modargs)
	end

	-- Emulate the "#invoke" call with "require" and our "frameSpoof"
	local mpkey = 'getCurrentFrame'
	local getCurrentFrame = rawget(mw, mpkey)
	-- Monkey patch "getCurrentFrame" to return our "frameSpoof"
	rawset(mw, mpkey, function() return frameSpoof end)
	local oldmod = rawget(package.loaded, modtitle)
	rawset(package.loaded, modtitle, nil) -- Force "require" to load from scratch
	-- "require" here so the target initialization block sees our monkey patched "getCurrentFrame"
	local fn = require(modtitle)[fnname]
	rawset(package.loaded, modtitle, oldmod) -- Pretend "require" was never called here
	local ret
	if 'function' == type(fn) then --XXX: We don't support callable tables; neither does "#invoke"?
		ret = explist(fn(frameSpoof)) -- Attempt to support multiple return values
	end
	rawset(mw, mpkey, getCurrentFrame) -- Revert earlier monkey patch
	--TODO: if "ret" is "nil", throw an error about "'fnname' is not a function in 'modname'"
	return table.concat(ret, '', 1, ret.n)
end
function pkgAPI.main(args, key)
	local numparamsn, numparams = 0, {}
	local strparamsn, strparams = 0, {}
	for param in pairs(args) do
		local paramtype = type(param)
		if 'number' == paramtype then
			numparamsn = 1 + numparamsn
			numparams[numparamsn] = param
		elseif 'string' == paramtype then
			strparamsn = 1 + strparamsn
			strparams[strparamsn] = param
		end
	end
	table.sort(numparams)
	table.sort(strparams)
	local ret = pkgTitle .. ' {{key|' .. key .. '}} {{nums'
	for i, numparam in ipairs(numparams) do
		ret = ret .. '|' .. numparam .. '=' .. args[numparam]
	end
	ret = ret .. '}} {{strs'
	for i, strparam in ipairs(strparams) do
		ret = ret .. '|' .. strparam .. '=' .. args[strparam]
	end
	ret = ret .. '}}'
	return ret
end

if ... then
	-- 'require()' and 'mw.loadData()'
	if not parentFrame then
		--[=[ 'mw.loadData()'
		This is where data that is static during a page rendering should be made available to modules
		]=]
		return {pkgTitle, 'loadData'}
	end
	--[=[ 'require()'
	This is where "main" functionality should be made available to modules
	]=]
	return pkgAPI
else
	-- '#invoke' and console
	if not parentFrame then
		--[=[ console
		This is where debug functionality should be made available to the console as 'p'
		]=]
		return pkgAPI
	end
	--[=[ '#invoke'
	This is where "wrapper" functionality should be made available to wikitext templates, etc.
	]=]
	local mt = {}
	function mt:__index(key)
		return function(frame) --[=[
			This is the function from '{{#invoke:key|...}}', where 'key' is any valid string argument
			'#invoke' parses this first "function" argument differently (e.g., "=" is valid, etc.)
			and sends the rest to the frame parser
			Notice: '{{#invoke:main|...}}' is valid despite
			"main" being an API function accessible from require()
			]=]
			assert(frame == currentFrame)
			return pkgAPI.main(currentFrame.args, key)
		end
	end
	return setmetatable({}, mt)
end