Pergi ke kandungan

Modul:type guard

Daripada Wikikamus

local export = {}

--[==[ intro:
This module provides utilities for type guarding functions and creating classes
and enums.
]==]

local error = error
local ipairs = ipairs
local pairs = pairs
local setmetatable = setmetatable
local sub = string.sub
local type = type
local unpack = unpack or table.unpack

local types = {}

-- Defaults for types.
local type_defaults = {
	["string"]		= "",
	["number"]		= 0,
	["table"]		= {},
	["function"]	= function() end,
	["boolean"]		= false,
}

-- Built-in types.
local builtins = {}
builtins["nil"] = true
for k, _ in pairs(type_defaults) do
	builtins[k] = true
end

-- Parse user-defined type into table of possible types.
local function eval_type(t)
	local ret = {}
	
	if sub(t, -1) == "?" then
		ret["nil"] = true
		t = sub(t, 1, -2)
	end
	
	for v in string.gmatch(t, "([^|]+)") do
		ret[v] = true
	end
	
	return ret
end

--[==[
Same as the built-in {type} function, except checks to see if the value matches
an existing class in the type store.
]==]
function export.type(value)
	local luatype = type(value)
	
	if luatype == "table" then
		local class = value._typing_class
		if class then
			if types[class] then
				return class
			else
				error("Attempt to use nonexistent class " .. class .. ".")
			end
		end
		
		local enum = value._typing_enum
		if enum then
			if types[enum] then
				return enum
			else
				error("Attempt to use nonexistent enum " .. enum .. ".")
			end
		end
	end
	
	return luatype
end

local special_type = export.type

--[==[
Return a type guard for a function `fun`. `types` should be an array of the
expected types to be returned by {export.type}, in the same order of the
arguments of the function. Example usage:

```
local m_type = require("Module:type guard")
local function say_hi(to_whom)
	mw.log("Hello, " .. to_whom .. "!")
end
say_hi = m_type.guard(say_hi, {"string"})
say_hi(1)		--> Expected argument 1 to be of type `string`, but received `number`.
say_hi("John")	--> Hello, John!
```
]==]
function export.guard(fun, types)
	if not types then
		return fun
	end
	
	return function(...)
		local args = {...}
		
		for i, a in ipairs(args) do
			local expected = types[i]
			local eval = eval_type(expected)
			local actual = special_type(a)
			
			if not eval[actual] then
				error("Expected argument " .. i .. " to be of type `" ..
					expected .. "`, but received `" ..
					actual .. "`.")
			elseif actual == "table" and a._typing_enum then
				args[i] = a._item
			end
		end
		
		return fun(unpack(args))
	end
end

--[==[
Instantiate a new class with the given name and properties and save it to the
type store. A constructor will be automatically generated based on the given
properties, which can then be optionally wrapped in a method to handle more
complex initialisation logic. The default {__call} method can be used like this:

```
local m_type = require("Module:type guard")
local Person = m_type.class("Person", { name = "string", age = "number" })
local instance = Person {
	name = "John",
	age = 20,
}
mw.logObject(instance)
mw.logObject(getmetatable(instance))
```

The expected output is:
```
table#1 {
    metatable = table#2,
    ["age"] = 20,
    ["name"] = "John",
}
table#1 {
    ["__index"] = table#2 {
        ["_typing_class"] = "Person",
        ["age"] = 0,
        ["name"] = "",
    },
}
```
]==]
function export.class(name, properties)
	if builtins[name] then
		error("Class " .. name .. " has the same name as a built-in type.")
	end
	
	local mt = {}
	local ret = {}
	local prop_types = {}
	
	local i = 1
	
	for k, t in pairs(properties) do
		prop_types[k] = eval_type(t)
		ret[k] = type_defaults[t] or nil
		i = i + 1
	end
	
	ret._typing_class = name
	
	mt.__call = function(self, args)
		local r = {}
		
		for k, def in pairs(prop_types) do
			local val = args[k]
			
			if val == nil then
				if not def["nil"] then
					error("Missing required property `" .. k .. "`.")
				end
			else
				local actual_t = special_type(val)
				if not def[actual_t] then
					error("Expected property `" .. k .. "` to be of type `"
						.. properties[k] .. "`, but received `"
						.. actual_t .. "`.")
				end
			end
			
			r[k] = val
		end
		
		return setmetatable(r, {
			__index = self,
		})
	end
	
	setmetatable(ret, mt)
	
	types[name] = true
	
	return ret
end

--[==[
Create a plain enum with the keys `keys` and save it to the type store with name
`name`.

Note that the enum items are actually tables containing the number and
typing information; if your function is just expecting the number, you should
type guard it with {export.guard} before passing your enum in.
]==]
function export.enum(name, keys)
	if builtins[name] then
		error("Class " .. name .. " has the same name as a built-in type.")
	end
	
	local ret = {}
	local mt = {
		__eq = function(a, b)
			return a._item == b
		end,
	}
	
	for i, k in ipairs(keys) do
		local item = {
			_typing_enum = name,
			_item = i,
		}
		setmetatable(item, mt)
		ret[k] = item
	end
	
	types[name] = true
	return ret
end

return export