Module:Lua class
Appearance
![]() | This module is rated as beta, and is ready for widespread use. It is still new and should be used with some caution to ensure the results are as expected. |
![]() | This module is subject to page protection. It is a highly visible module in use by a very large number of pages, or is substituted very frequently. Because vandalism or mistakes would affect many pages, and even trivial editing might cause substantial load on the servers, it is protected from editing. |
![]() | This module depends on the following other modules: |
This module provides utilities for declaring classes in Lua code. It creates global variables, so must be called before require('strict')
if that is used.
libraryUtil = require('libraryUtil') -- overridden for new types and exceptions
local classes, instances = {}, {} -- registry of all complete/internal class and instance objects (with some exceptions)
local inst_private_mts, inst_public_mts = {}, {} -- for each class since they are immutable
local una_metamethods = {__ipairs=1, __pairs=1, __tostring=1, __unm=1}
local bin_metamethods = {__add=1, __concat=1, __div=1, __eq=1, __le=1, __lt=1, __mod=1, __mul=1, __pow=1, __sub=1}
local oth_metamethods = {__call=1, __index=1, __newindex=1, __init=1}
local not_metamethods = {__name=1, __bases=1, __methods=1, __slots=1, __protected=1} -- and __class
local function private_read(self_private, key)
if not not_metamethods[key] then
return instances[self_private][key] -- instance should be clean of misbahaved keys so that __index(cls_private, key) handles it
end
error(('AttributeError: unauthorized read attempt of internal "%s"'):format(key), 2)
end
local function private_read_custom(self_private, key)
if not not_metamethods[key] then
local self = instances[self_private]
local value = self.__class.__index(self_private, key) -- custom __index can handle misbehaved keys
if value == nil then
value = self[key] -- same reason of private_read for not checking key type
end
return value
end
error(('AttributeError: unauthorized read attempt of internal "%s"'):format(key), 2)
end
local function private_write(self_private, key, value)
libraryUtil.checkTypeMultiForIndex(key, {'string', 'number'})
local self = instances[self_private]
if tonumber(key) or not classes[self.__class].__methods[key] and key:sub(1,2) ~= '__' then
self[key] = value
else
error(('AttributeError: forbidden write attempt {%s: %s} to immutable instance method'):format(key, tostring(value)), 2)
end
end
local function private_write_custom(self_private, key, value)
local self = instances[self_private]
if type(key) ~= 'string' or not classes[self.__class].__methods[key] and key:sub(1,2) ~= '__' then
if not self.__class.__newindex(self_private, key, value) then -- custom __newindex can handle misbehaved keys
libraryUtil.checkTypeMultiForIndex(key, {'string', 'number'})
self[key] = value
end
else
error(('AttributeError: forbidden write attempt {%s: %s} to immutable instance method'):format(key, tostring(value)), 2)
end
end
local function objtostr(obj)
local copy = {}
for key, val in pairs(obj) do
copy[key] = type(val) == 'function' and 'function' or val
end
return mw.text.jsonEncode(copy, mw.text.JSON_PRETTY)
end
local inst_mt = {
__index = function (self, key)
return self.__class[key] -- key could be misbehaved here without issues as __index(cls_private, key) would handle it
end,
__tostring = objtostr--
}
local function public_read(self_public, key)
if type(key) ~= 'string' or key:sub(1,1) ~= '_' then
return instances[instances[self_public]][key] -- same reason of private_read...
end
error(('AttributeError: unauthorized read attempt of nonpublic "%s"'):format(key), 2)
end
local function public_read_custom(self_public, key)
if type(key) ~= 'string' or key:sub(1,1) ~= '_' then
local self = instances[instances[self_public]]
local value = self.__class.__index(instances[self_public], key)
if value == nil then
value = self[key] -- same reason of private_read...
end
return value
end
error(('AttributeError: unauthorized read attempt of nonpublic "%s"'):format(key), 2)
end
local function public_write(self_public, key, value)
if type(key) == 'string' and key:sub(1,1) == '_' then
error(('AttributeError: unauthorized write attempt of nonpublic {%s: %s}'):format(key, tostring(value)), 2)
end
local self = instances[instances[self_public]]
local cls = classes[self.__class]
if cls.__methods[key] then
error(('AttributeError: forbidden write attempt {%s: %s} to immutable instance method'):format(key, tostring(value)), 2)
end
if self[key] == nil and not cls.__slots[key] then -- if instance and __slots are valid, no danger of creating misbehaved attributes
libraryUtil.checkTypeMultiForIndex(key, {'string', 'number'}) -- otherwise error message would not make sense
error(('AttributeError: public attribute creation attempt {%s: %s} not expected by __slots'):format(key, tostring(value)), 2)
end
self[key] = value
end
local function public_write_custom(self_public, key, value)
if type(key) == 'string' and key:sub(1,1) == '_' then
error(('AttributeError: unauthorized write attempt of nonpublic {%s: %s}'):format(key, tostring(value)), 2)
end
local self = instances[instances[self_public]]
local cls = classes[self.__class]
if cls.__methods[key] then
error(('AttributeError: forbidden write attempt {%s: %s} to immutable instance method'):format(key, tostring(value)), 2)
end
if not cls.__newindex(instances[self_public], key, value) then
if self[key] == nil and not cls.__slots[key] then
libraryUtil.checkTypeMultiForIndex(key, {'string', 'number'}) -- otherwise error message...
error(('AttributeError: public attribute creation attempt {%s: %s} not expected by __slots'):format(key, tostring(value)), 2)
end
self[key] = value
end
end
local function constructor(wrapper, ...)
if select('#', ...) ~= 1 or type(...) ~= 'table' then
error('SyntaxError: incorrect instance constructor syntax, should be: Class{arg1, arg2..., kw1=kwarg1, kw2=kwarg2...}', 2)
end
local self = {} -- __new
local cls_private = classes[classes[wrapper]] and classes[wrapper] or wrapper
self.__class = cls_private
local self_private = {} -- wrapper
local cls = classes[cls_private]
local mt = inst_private_mts[cls]
if not mt then
mt = {}
mt.__index = cls.__index and private_read_custom or private_read
mt.__newindex = cls.__newindex and private_write_custom or private_write
for key in pairs(una_metamethods) do
mt[key] = cls[key]
end
mt.__call = cls.__call
mt.__metatable = 'unauthorized access attempt of wrapper object metatable'
inst_private_mts[cls] = mt
end
setmetatable(self_private, mt)
instances[self_private] = self
local __init = cls.__init
if __init and __init(self_private, ...) then
error('TypeError: __init must not return a var-list')
end
for key in pairs(cls.__methods) do
self[key] = function (...) return cls[key](self_private, ...) end
end
setmetatable(self, inst_mt)
local self_public = {}
mt = inst_public_mts[cls]
if not mt then
mt = {}
mt.__index = cls.__index and public_read_custom or public_read
mt.__newindex = cls.__newindex and public_write_custom or public_write
for key in pairs(una_metamethods) do
if cls[key] then
mt[key] = function (a) return cls[key](instances[a]) end
end
end
for key in pairs(bin_metamethods) do
if cls[key] then
mt[key] = function (a, b) return cls[key](instances[a], instances[b]) end
end
end
mt.__call = function (self_public, ...) return cls.__call(instances[self_public], ...) end
mt.__metatable = 'unauthorized access attempt of wrapper object metatable'
inst_public_mts[cls] = mt
end
setmetatable(self_public, mt)
instances[self_public] = self_private
return self_public
end
local function multi_inheritance(cls, key)
for _, base in ipairs(cls.__bases) do
if key:sub(1,1) ~= '_' or base.__protected[key] or key:sub(1,2) == '__' and key ~= '__name' then
local value = base[key]
if value ~= nil then
return value
end
end
end
end
local cls_mt = {
__index = multi_inheritance,
__tostring = objtostr--
}
local cls_private_mt = {
__call = constructor,
__index = function (cls_private, key)
if not not_metamethods[key] then
libraryUtil.checkTypeMultiForIndex(key, {'string'})
local cls = classes[cls_private]
local value = cls[key]
if type(value) == 'table' and not cls.__slots[key] then
return mw.clone(value) -- because class attributes are immutable by default
end
return value
end
error(('AttributeError: unauthorized read attempt of internal "%s"'):format(key), 2)
end,
__newindex = function (cls_private, key, value)
local cls = classes[cls_private]
if cls.__slots[key] then -- __slots should be valid, so no need to check key type before
cls[key] = value
else
libraryUtil.checkTypeMultiForIndex(key, {'string'}) -- otherwise error message would not make sense
error(('AttributeError: write attempt {%s: %s} not expected by __slots'):format(key, tostring(value)), 2)
end
end,
__metatable = 'unauthorized access attempt of wrapper object metatable'
}
local cls_public_mt = {
__call = constructor,
__index = function (cls_public, key)
libraryUtil.checkTypeMultiForIndex(key, {'string'})
if key:sub(1,1) ~= '_' then
local value = classes[classes[cls_public]][key]
if type(value) == 'table' then
return mw.clone(value) -- all class attributes are immutable in the public scope
end
return value
end
error(('AttributeError: unauthorized read attempt of nonpublic "%s"'):format(key), 2)
end,
__newindex = function (cls_public, key, value)
libraryUtil.checkTypeMultiForIndex(key, {'string'}) -- otherwise error message...
error(('AttributeError: forbidden write attempt of {%s: %s}; a class is immutable in public scope'):format(key, tostring(value)), 2)
end,
__metatable = 'unauthorized access attempt of wrapper object metatable'
}
function class(...)
local args = {...}
local cls = {} -- internal
local idx
if type(args[1]) == 'string' then
cls.__name = args[1]
idx = 2
else
idx = 1
end
cls.__bases = {}
for i = idx, #args-1 do
libraryUtil.checkType('class', i, args[i], 'class')
cls.__bases[#cls.__bases+1] = classes[classes[args[i]]]
end
local kwargs = args[#args]
libraryUtil.checkType('class', #args, kwargs, 'table')
if kwargs.__name or kwargs.__bases then
error('ValueError: __name and unpacked __bases must be passed as optional first args to "class"')
end
cls.__slots = {}
if kwargs.__slots then
for _, slot in ipairs(kwargs.__slots) do
if slot:sub(1,2) ~= '__' then
cls.__slots[slot] = true
else
error(('ValueError: slot "%s" has forbidden namespace'):format(slot))
end
end
kwargs.__slots = nil
end
local mt = {
__index = function (__slots, key) -- multi_inheritance
for _, base in ipairs(cls.__bases) do
if key:sub(1,1) ~= '_' or base.__protected[key] then
if base.__slots[key] then
return true
end
end
end
end
}
setmetatable(cls.__slots, mt)
cls.__protected = {}
if kwargs.__protected then
for _, key in ipairs(kwargs.__protected) do
if key:sub(1,1) == '_' and key:sub(2,2) ~= '_' then
cls.__protected[key] = true
else
error(('ValueError: the namespace of "%s" is not manually protectable'):format(key))
end
end
kwargs.__protected = nil
end
mt = {
__index = function (__protected, key)
for _, base in ipairs(cls.__bases) do
if base.__protected[key] then
return true
end
end
end
}
setmetatable(cls.__protected, mt)
if kwargs.__methods then
error('ValueError: __classmethods and __staticmethods should be passed as optional attributes instead of __methods')
end
local cls_private = {} -- wrapper
setmetatable(cls_private, cls_private_mt)
classes[cls_private] = cls
if kwargs.__classmethods then
for _, key in ipairs(kwargs.__classmethods) do
local func = kwargs[key]
cls[key] = function (...) return func(cls_private, ...) end
kwargs[key] = nil
end
kwargs.__classmethods = nil
end
local staticmethods = {}
if kwargs.__staticmethods then
for _, key in ipairs(kwargs.__staticmethods) do
staticmethods[key] = true
end
kwargs.__staticmethods = nil
end
cls.__methods = {}
for _, base in ipairs(cls.__bases) do
for key in pairs(base.__methods) do
if key:sub(1,1) ~= '_' or base.__protected[key] then
cls.__methods[key] = true
end
end
end
local valid = false
for key, val in pairs(kwargs) do
if key:sub(1,2) == '__' and not una_metamethods[key] and not bin_metamethods[key] and not oth_metamethods[key] then
error(('ValueError: unrecognized metamethod or unauthorized internal attribute {%s: %s}'):format(key, tostring(val)))
end
cls[key] = val
if type(val) == 'function' then
if not staticmethods[key] and key:sub(1,2) ~= '__' then
cls.__methods[key] = true
end
if key ~= '__init' then -- __init does not qualify to a functional/proper class
valid = true
end
end
end
assert(valid, 'AssertionError: a (sub)class must have at least one functional method')
setmetatable(cls, cls_mt)
local cls_public = {}
setmetatable(cls_public, cls_public_mt)
classes[cls_public] = cls_private
return cls_public
end
local function rissubclass2(class, classinfo)
if class == classinfo then
return true
end
for _, base in ipairs(class.__bases) do
if rissubclass2(base, classinfo) then
return true
end
end
return false
end
local function rissubclass1(class, classinfo, parent, level)
libraryUtil.checkTypeMulti(parent, 2, classinfo, {'class', 'table'}, level)
if classes[classinfo] then
return rissubclass2(class, classes[classes[classinfo]])
end
for i = 1, #classinfo do
if rissubclass1(class, classinfo[i], parent, level+1) then
return true
end
end
return false
end
function issubclass(class, classinfo)
libraryUtil.checkType('issubclass', 1, class, 'class')
class = classes[class]
return rissubclass1(classes[class] or class, classinfo, 'issubclass', 4)
end
function isinstance(instance, classinfo)
instance = instances[instance]
if instance then -- because named (ClassName) instances would fail with checkType
return rissubclass1(classes[instance.__class], classinfo, 'isinstance', 4)
end
error(("TypeError: bad argument #1 to 'isinstance' (instance expected, got %s)"):format(type(instance)), 2)
end
libraryUtil.checkType = function (name, argIdx, arg, expectType, nilOk, level)
if arg == nil and nilOk then
return
end
if type(arg) ~= expectType then
error(("TypeError: bad argument #%d to '%s' (%s expected, got %s)"):format(argIdx, name, expectType, type(arg)), level or 3)
end
end
libraryUtil.checkTypeMulti = function (name, argIdx, arg, expectTypes, level)
local argType = type(arg)
for _, expectType in ipairs(expectTypes) do
if argType == expectType then
return
end
end
local n = #expectTypes
local typeList
if n > 1 then
typeList = table.concat(expectTypes, ', ', 1, n-1) .. ' or ' .. expectTypes[n]
else
typeList = expectTypes[1]
end
error(("TypeError: bad argument #%d to '%s' (%s expected, got %s)"):format(argIdx, name, typeList, type(arg)), level or 3)
end
libraryUtil.checkTypeForIndex = function (index, value, expectType, level)
if type(value) ~= expectType then
error(("TypeError: value for index '%s' must be %s, %s given"):format(index, expectType, type(value)), level or 3)
end
end
libraryUtil.checkTypeMultiForIndex = function (index, expectTypes, level)
local indexType = type(index)
for _, expectType in ipairs(expectTypes) do
if indexType == expectType then
return
end
end
local n = #expectTypes
local typeList
if n > 1 then
typeList = table.concat(expectTypes, ', ', 1, n-1) .. ' or ' .. expectTypes[n]
else
typeList = expectTypes[1]
end
error(("TypeError: index '%s' must be %s, %s given"):format(index, typeList, type(index)), level or 3)
end
libraryUtil.checkTypeForNamedArg = function (name, argName, arg, expectType, nilOk, level)
if arg == nil and nilOk then
return
end
if type(arg) ~= expectType then
error(("TypeError: bad named argument %s to '%s' (%s expected, got %s)"):format(argName, name, expectType, type(arg)), level or 3)
end
end
libraryUtil.checkTypeMultiForNamedArg = function (name, argName, arg, expectTypes, level)
local argType = type(arg)
for _, expectType in ipairs(expectTypes) do
if argType == expectType then
return
end
end
local n = #expectTypes
local typeList
if n > 1 then
typeList = table.concat(expectTypes, ', ', 1, n-1) .. ' or ' .. expectTypes[n]
else
typeList = expectTypes[1]
end
error(("TypeError: bad named argument %s to '%s' (%s expected, got %s)"):format(argName, name, typeList, type(arg)), level or 3)
end
local type = type
_G.type = function (value)
local t = type(value)
if t == 'table' then
if classes[value] then
return 'class'
elseif instances[value] then
return classes[instances[value].__class].__name or 'instance' -- should __name be directly readable instead?
end
end
return t
end
local function try_parser(...)
local args = {...}
libraryUtil.checkType('try', 1, args[1], 'function', nil, 4)
local try_clause = args[1]
assert(args[2] == 'except', 'AssertionError: missing required except clause')
local except_clauses = {}
local i = 3
repeat
libraryUtil.checkTypeMulti('try', i, args[i], {'string', 'table', 'function'}, 4)
if ({string=1, table=1})[type(args[i])] then
libraryUtil.checkType('try', i+1, args[i+1], 'function', nil, 4)
except_clauses[#except_clauses+1] = {exceptions={}, handler=args[i+1]}
if type(args[i]) == 'string' then
except_clauses[#except_clauses].exceptions[args[i]] = true
else
for _, exception in ipairs(args[i]) do
if type(exception) ~= 'string' then
error(('TypeError: invalid exception type in except (string expected, got %s)'):format(type(exception)))
end
except_clauses[#except_clauses].exceptions[exception] = true
end
end
i = i + 3
else
except_clauses[#except_clauses+1] = {exceptions={}, handler=args[i]}
i = i + 2
break
end
until args[i-1] ~= 'except'
local else_clause, finally_clause
if args[i-1] == 'except' then
error('SyntaxError: except after except clause without specific exceptions, which should be the last')
elseif args[i-1] == 'else' then
libraryUtil.checkType('try', i, args[i], 'function', nil, 4)
else_clause = args[i]
i = i + 2
end
if args[i-1] == 'finally' then
libraryUtil.checkType('try', i, args[i], 'function', nil, 4)
finally_clause = args[i]
i = i + 2
end
if args[i-1] ~= nil then
error(('SyntaxError: unexpected arguments #%d–#%d to "try"'):format(i-1, #args), 3)
end
return try_clause, except_clauses, else_clause, finally_clause
end
function try(...)
local try_clause, except_clauses, else_clause, finally_clause = try_parser(...)
local function errhandler(message)
local errtype = mw.text.split(message, ':')[1]
local handled = false
for _, except in ipairs(except_clauses) do
if except.exceptions[errtype] or #except.exceptions == 0 then
handled, message = pcall(except.handler())
break
end
end
if not handled then
return message
end
end
local success, message = xpcall(try_clause, errhandler)
if else_clause and success then
success, message = pcall(else_clause)
end
if finally_clause then
finally_clause()
end
if not success and message then
error(message)
end
end
return classes, instances--