Capil ka isi

Modul:type guard

Matan Wiktionary

Dokumentasi untuk modul ini dapat dibuat di Modul:type guard/doc

-- Imported from enwiktionary
-- Source: https://en.wiktionary.org/wiki/Module:type guard
-- License: CC BY-SA

local export = {}

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

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

local types = {
	["nil"]		 = true,
	["string"]   = true,
	["number"]   = true,
	["table"]	 = true,
	["function"] = true,
	["boolean"]  = true,
}

local errors = {
	already_exists	 = "type '%s' already exists",
	bad_argument	 = "bad argument #%d (expected '%s', found '%s')",
	bad_property	 = "bad property '%s' (expected '%s', found '%s')",
	nonexistent_prop = "property '%s' does not exist on class '%s'",
	nonexistent_type = "attempt to use nonexistent %s '%s'",
}

-- 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 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 or enum in the type store.
]==]
function export.type(value)
	local lua_type = type(value)

	if lua_type == "table" then
		local class = value._typing_class
		if class then
			if types[class] then
				return class
			else
				error(format(errors.nonexistent_type, "class", class))
			end
		end

		local enum = value._typing_enum
		if enum then
			if types[enum] then
				return enum
			else
				error(format(errors.nonexistent_type, "enum", enum))
			end
		end
	end

	return lua_type
end

local special_type = export.type

--[==[
Return a type guard for a function `fun`. `arg_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 guard = require("Module:type guard").guard

local function say_hi(to_whom)
	mw.log("Hello, " .. to_whom .. "!")
end

say_hi = guard(say_hi, {"string"})

say_hi(1)		--> bad argument #1 (expected 'string', found 'number')
say_hi("John")	--> Hello, John!
```
]==]
function export.guard(fun, arg_types)
	if not arg_types then
		return fun
	end

	return function(...)
		local args = { ... }

		for i, a in ipairs(args) do
			local expected = arg_types[i]
			if not expected then
				break -- bail out, could be inside varargs
			end

			local eval = eval_type(expected)
			local actual = special_type(a)

			if not eval[actual] then
				error(format(errors.bad_argument, i, expected, actual))
			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 in {__call} will be automatically generated based on
the given properties, which can then be used like this:

```
local class = require("Module:type guard").class

local Person = class "Person" {
	name = "string",
	age = "number",
}

local instance = Person {
	name = "John",
	age = 20,
}
```
]==]
function export.class(name)
	return function(properties)
		if types[name] then
			error(format(errors.already_exists, name))
		end

		local mt = {}
		local ret = {}
		local prop_types = {}

		local i = 1

		for k, t in pairs(properties) do
			prop_types[k] = eval_type(t)
			i = i + 1
		end

		ret._typing_class = name

		mt.__call = function(self, args)
			local instance = {}

			for k, def in pairs(prop_types) do
				local val = args[k]

				local actual = special_type(val)
				if not def[actual] then
					error(format(errors.bad_property, k, properties[k], actual))
				end

				instance[k] = val
			end

			return setmetatable(instance, {
				__index = self,
				__newindex = function(self, k, v)
					-- allow setting method without having to predefine each one
					if type(v) ~= "function" and not properties[k] then
						error(format(errors.nonexistent_prop, k, name))
					end
					rawset(self, k, v)
				end,
			})
		end

		setmetatable(ret, mt)

		types[name] = true

		return ret
	end
end

--[==[
Create a plain enum and save it to the type store. Example usage:

```
local enum = require("Module:type guard").enum

local Greeting = enum "Greeting" {
	"Hello",
	"Goodbye",
}
```

Calling the enum object with an instance of the enum will return the key
of that instance. Using the example above:

```
local instance = Greeting.Hello
local key = Greeting(instance)
mw.log(key) --> Hello
```
]==]
function export.enum(name)
	return function(keys)
		if types[name] then
			error(format(errors.already_exists, name))
		end

		local ret = {}

		for i, k in ipairs(keys) do
			local item = {
				_typing_enum = name,
				_item = i,
			}

--[==[
Get the index of an enum item. Example usage:
```
local enum = require("Module:type guard").enum

local Greeting = enum "Greeting" {
	"Hello",
	"Goodbye",
}

mw.log(Greeting.Hello:index())		--> 1
mw.log(Greeting.Goodbye:index())	--> 2
```
]==]
			function item:index()
				return self._item
			end

			ret[k] = item
		end

		types[name] = true

		return setmetatable(ret, {
			__call = function(self, i)
				for k, v in pairs(self) do
					if v._item == i then
						return self[k]
					end
				end
			end,
		})
	end
end

return export