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)
if tonumber(key) then
return nil -- don't search numeric keys in classes
end
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
local _type = type
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
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 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--