Module:Lua class: Difference between revisions
Appearance
Content deleted Content added
m Fixed bug where "type" calls below _G.type used the local (original) function |
Turned overridden libraryUtil local, but still accessible to other modules. Added warnings. Made error handling go brrr. Implemented hashing. Completed error levels. Optimized closures. Returned cls_private from "class". And a little more. |
||
Line 1: | Line 1: | ||
libraryUtil = require('libraryUtil') -- overridden for new types and exceptions |
local libraryUtil = require('libraryUtil') -- overridden for new types and exceptions |
||
local warn = require('Module:Warning') |
|||
local classes, instances = {}, {} -- registry of all complete/internal class and instance objects |
local classes, instances = {}, {} -- registry of all complete/internal class and instance objects and their pointers |
||
local inst_private_mts, inst_public_mts = {}, {} -- for each class since they are immutable |
local inst_private_mts, inst_public_mts = {}, {} -- for each class since they are mostly immutable |
||
local una_metamethods = {__ipairs=1, __pairs=1, __tostring=1, __unm=1} |
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 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 oth_metamethods = {__call=1, __index=1, __newindex=1, __init=1} |
||
local not_metamethods = {__name=1, __bases=1, __methods=1, |
local not_metamethods = {__name=1, __bases=1, __methods=1, __classmethods=1, __staticmethods=1, __normalmethods=1, __slots=1, __protected=1} |
||
-- __class and __hash |
|||
local function |
local function objtostr(obj) |
||
local copy = {} |
|||
if not not_metamethods[key] then |
|||
for key, val in pairs(obj) do |
|||
return instances[self_private][key] -- instance should be clean of misbahaved keys so that __index(cls_private, key) handles it |
|||
copy[key] = type(val) == 'function' and 'function' or val |
|||
end |
end |
||
return mw.text.jsonEncode(copy, mw.text.JSON_PRETTY) |
|||
error(('AttributeError: unauthorized read attempt of internal "%s"'):format(key), 2) |
|||
end |
end |
||
local inst_mt = { |
|||
local function private_read_custom(self_private, key) |
|||
__index = function (self, key) |
|||
if not not_metamethods[key] then |
|||
if tonumber(key) or key == 'hash' or key == '__hash' then -- don't search numeric keys in classes and hash isn't inheritable |
|||
local self = instances[self_private] |
|||
return nil |
|||
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 |
end |
||
return self.__class[key] -- key could be invalid here without issues as __index(cls_private, key) would handle it |
|||
return value |
|||
end, |
|||
__tostring = objtostr-- |
|||
} |
|||
local function private_read(self_private, key) |
|||
return instances[self_private][key] -- instance should be clean of invalid keys so that __index(cls_private, key) handles them |
|||
end |
|||
local function private_read_custom(self_private, key) |
|||
if not_metamethods[key] then |
|||
error(("AttributeError: unauthorized read attempt of internal '%s'"):format(key), 2) |
|||
end |
|||
local self = instances[self_private] |
|||
local value = self.__class.__index(self_private, key) -- custom __index can handle invalid keys |
|||
if value == nil then |
|||
return self[key] -- same reason of private_read for not checking key validity |
|||
end |
end |
||
return value |
|||
error(('AttributeError: unauthorized read attempt of internal "%s"'):format(key), 2) |
|||
end |
end |
||
Line 32: | Line 49: | ||
libraryUtil.checkTypeMultiForIndex(key, {'string', 'number'}) |
libraryUtil.checkTypeMultiForIndex(key, {'string', 'number'}) |
||
local self = instances[self_private] |
local self = instances[self_private] |
||
if type(key) == 'string' then |
|||
if tonumber(key) or not classes[self.__class].__methods[key] and key:sub(1,2) ~= '__' then |
|||
local cls = classes[self.__class] |
|||
self[key] = value |
|||
if cls.__normalmethods[key] or key:sub(1,2) == '__' and key ~= '__hash' then |
|||
else |
|||
error(( |
error(("AttributeError: forbidden write attempt {%s: %s} to immutable method or invalid key"):format(key, tostring(value)), 2) |
||
elseif key:find('[^_%w]') or key:find('^%d') then |
|||
error(("AttributeError: invalid attribute name '%s'"):format(key), 2) |
|||
elseif key == '__hash' and self.__hash ~= nil then |
|||
error("AttributeError: forbidden update attempt to immutable __hash", 2) |
|||
end |
|||
end |
end |
||
self[key] = value |
|||
end |
end |
||
local function private_write_custom(self_private, key, value) |
local function private_write_custom(self_private, key, value) |
||
local self = instances[self_private] |
local self = instances[self_private] |
||
local cls = classes[self.__class] |
|||
local keyType = type(key) |
|||
if not self.__class.__newindex(self_private, key, value) then -- custom __newindex can handle misbehaved keys |
|||
if keyType == 'string' and (cls.__normalmethods[key] or key:sub(1,2) == '__' and key ~= '__hash') then |
|||
libraryUtil.checkTypeMultiForIndex(key, {'string', 'number'}) |
|||
error(("AttributeError: forbidden write attempt {%s: %s} to immutable method or invalid key"):format(key, tostring(value)), 2) |
|||
self[key] = value |
|||
end |
|||
if cls.__newindex(self_private, key, value) == false then -- custom __newindex can handle invalid keys |
|||
libraryUtil.checkTypeMultiForIndex(key, {'string', 'number'}) |
|||
if keyType == 'string' then |
|||
if key:find('[^_%w]') or key:find('^%d') then |
|||
error(("AttributeError: invalid attribute name '%s'"):format(key), 2) |
|||
elseif key == '__hash' and self.__hash ~= nil then |
|||
error("AttributeError: forbidden update attempt to immutable __hash", 2) |
|||
end |
|||
end |
end |
||
self[key] = value |
|||
else |
|||
error(('AttributeError: forbidden write attempt {%s: %s} to immutable instance method'):format(key, tostring(value)), 2) |
|||
end |
end |
||
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) |
local function public_read(self_public, key) |
||
if type(key) |
if type(key) == 'string' and key:sub(1,1) == '_' then |
||
error(("AttributeError: unauthorized read attempt of nonpublic '%s'"):format(key), 2) |
|||
return instances[instances[self_public]][key] -- same reason of private_read... |
|||
end |
end |
||
return instances[instances[self_public]][key] -- same reason of private_read... |
|||
error(('AttributeError: unauthorized read attempt of nonpublic "%s"'):format(key), 2) |
|||
end |
end |
||
local function public_read_custom(self_public, key) |
local function public_read_custom(self_public, key) |
||
if type(key) |
if type(key) == 'string' and key:sub(1,1) == '_' then |
||
error(("AttributeError: unauthorized read attempt of nonpublic '%s'"):format(key), 2) |
|||
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 |
end |
||
local self = instances[instances[self_public]] |
|||
error(('AttributeError: unauthorized read attempt of nonpublic "%s"'):format(key), 2) |
|||
local value = self.__class.__index(instances[self_public], key) |
|||
if value == nil then |
|||
return self[key] -- same reason of private_read... |
|||
end |
|||
return value |
|||
end |
end |
||
local function public_write(self_public, key, value) |
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 self = instances[instances[self_public]] |
||
local cls = classes[self.__class] |
local cls = classes[self.__class] |
||
if |
if type(key) == 'string' then |
||
if key:sub(1,1) == '_' then |
|||
error(('AttributeError: forbidden write attempt {%s: %s} to immutable instance method'):format(key, tostring(value)), 2) |
|||
error(("AttributeError: unauthorized write attempt of nonpublic {%s: %s}"):format(key, tostring(value)), 2) |
|||
elseif cls.__normalmethods[key] then |
|||
error(("AttributeError: forbidden write attempt {%s: %s} to immutable method"):format(key, tostring(value)), 2) |
|||
end |
|||
end |
end |
||
if self[key] == nil and not cls.__slots[key] then -- if instance and __slots are valid, no danger of creating |
if self[key] == nil and not cls.__slots[key] then -- if instance and __slots are valid, no danger of creating invalid attributes |
||
libraryUtil.checkTypeMultiForIndex(key, {'string', 'number'}) -- otherwise error message would not make sense |
libraryUtil.checkTypeMultiForIndex(key, {'string', 'number'}) -- otherwise error message would not make sense |
||
error(( |
error(("AttributeError: public attribute creation attempt {%s: %s} not expected by __slots"):format(key, tostring(value)), 2) |
||
end |
end |
||
self[key] = value |
self[key] = value |
||
Line 105: | Line 119: | ||
local function public_write_custom(self_public, key, value) |
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 self = instances[instances[self_public]] |
||
local cls = classes[self.__class] |
local cls = classes[self.__class] |
||
if |
if type(key) == 'string' then |
||
if key:sub(1,1) == '_' then |
|||
error(('AttributeError: forbidden write attempt {%s: %s} to immutable instance method'):format(key, tostring(value)), 2) |
|||
error(("AttributeError: unauthorized write attempt of nonpublic {%s: %s}"):format(key, tostring(value)), 2) |
|||
elseif cls.__normalmethods[key] then |
|||
error(("AttributeError: forbidden write attempt {%s: %s} to immutable method"):format(key, tostring(value)), 2) |
|||
end |
|||
end |
end |
||
if |
if cls.__newindex(instances[self_public], key, value) == false then |
||
if self[key] == nil and not cls.__slots[key] then |
if self[key] == nil and not cls.__slots[key] then |
||
libraryUtil.checkTypeMultiForIndex(key, {'string', 'number'}) -- otherwise error message... |
libraryUtil.checkTypeMultiForIndex(key, {'string', 'number'}) -- otherwise error message... |
||
error(( |
error(("AttributeError: public attribute creation attempt {%s: %s} not expected by __slots"):format(key, tostring(value)), 2) |
||
end |
end |
||
self[key] = value |
self[key] = value |
||
Line 124: | Line 139: | ||
local function constructor(wrapper, ...) |
local function constructor(wrapper, ...) |
||
if select('#', ...) ~= 1 or type(...) ~= 'table' then |
if select('#', ...) ~= 1 or type(...) ~= 'table' then |
||
error( |
error("SyntaxError: incorrect instance constructor syntax, should be: Class{arg1, arg2..., kw1=kwarg1, kw2=kwarg2...}", 2) |
||
end |
end |
||
local self = {} -- __new |
local self = {} -- __new |
||
local cls_private = classes[classes[wrapper]] and classes[wrapper] or wrapper |
local cls_private = classes[classes[wrapper]] and classes[wrapper] or wrapper |
||
self.__class = cls_private |
self.__class = cls_private |
||
setmetatable(self, inst_mt) |
|||
local self_private = {} -- wrapper |
local self_private = {} -- wrapper |
||
Line 142: | Line 158: | ||
end |
end |
||
mt.__call = cls.__call |
mt.__call = cls.__call |
||
mt.__metatable = |
mt.__metatable = "unauthorized access attempt of wrapper object metatable" |
||
inst_private_mts[cls] = mt |
inst_private_mts[cls] = mt |
||
Line 152: | Line 168: | ||
local __init = cls.__init |
local __init = cls.__init |
||
if __init and __init(self_private, ...) then |
if __init and __init(self_private, ...) then |
||
error( |
error("TypeError: __init must not return a var-list") |
||
end |
end |
||
for key in pairs(cls.__methods) do |
for key in pairs(cls.__methods) do |
||
local func = cls[key] -- index once to save time in future calls |
|||
self[key] = function (...) return cls[key](self_private, ...) end |
|||
self[key] = function (...) return func(self_private, ...) end |
|||
end |
end |
||
if cls._hash then -- not inheritable |
|||
setmetatable(self, inst_mt) |
|||
self.hash = function () return cls._hash(self_private) end |
|||
self.hash() -- construction of self is finalized at this point, so immutable hash can be safely set |
|||
end |
|||
local self_public = {} |
local self_public = {} |
||
Line 170: | Line 190: | ||
for key in pairs(una_metamethods) do |
for key in pairs(una_metamethods) do |
||
if cls[key] then |
if cls[key] then |
||
mt[key] = function (a) return |
local func = cls[key] |
||
mt[key] = function (a) return func(instances[a]) end |
|||
end |
end |
||
end |
end |
||
for key in pairs(bin_metamethods) do |
for key in pairs(bin_metamethods) do |
||
if cls[key] then |
if cls[key] then |
||
mt[key] = function (a, b) return |
local func = cls[key] |
||
mt[key] = function (a, b) return func(instances[a], instances[b]) end |
|||
end |
end |
||
end |
end |
||
if cls.__call then |
|||
mt.__call = function (self_public, ...) return cls.__call(instances[self_public], ...) end |
|||
local func = cls.__call |
|||
mt.__metatable = 'unauthorized access attempt of wrapper object metatable' |
|||
mt.__call = function (self_public, ...) return func(instances[self_public], ...) end |
|||
end |
|||
mt.__metatable = "unauthorized access attempt of wrapper object metatable" |
|||
inst_public_mts[cls] = mt |
inst_public_mts[cls] = mt |
||
Line 192: | Line 217: | ||
local function multi_inheritance(cls, key) |
local function multi_inheritance(cls, key) |
||
for _, base in ipairs(cls.__bases) do |
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 |
if key:sub(1,1) ~= '_' or base.__protected[key] or key:sub(1,2) == '__' and key ~= '__name' and key ~= '__hash' then |
||
local value = base[key] |
local value = base[key] |
||
if value ~= nil then |
if value ~= nil then |
||
Line 209: | Line 234: | ||
__call = constructor, |
__call = constructor, |
||
__index = function (cls_private, key) |
__index = function (cls_private, key) |
||
if |
if type(key) ~= 'string' then |
||
warn(("AttributeWarning: index '%s' type should be string, %s given"):format(tostring(key), type(key)), 2) |
|||
libraryUtil.checkTypeMultiForIndex(key, {'string'}) |
|||
elseif not_metamethods[key] then |
|||
local cls = classes[cls_private] |
|||
error(("AttributeError: unauthorized read attempt of internal '%s'"):format(key), 2) |
|||
local value = cls[key] |
|||
elseif key:find('[^_%w]') or key:find('^%d') then |
|||
warn(("AttributeWarning: index '%s' should be a valid Lua name"):format(key), 2) |
|||
end |
|||
local cls = classes[cls_private] |
|||
local value = cls[key] |
|||
if not cls.__slots[key] then |
|||
local valueType = type(value) |
|||
if valueType == 'table' then |
|||
return mw.clone(value) -- because class attributes are immutable by default |
return mw.clone(value) -- because class attributes are immutable by default |
||
elseif valueType == 'set' then --should list be clone or deep copy? |
|||
return value.copy() |
|||
end |
end |
||
return value |
|||
end |
end |
||
return value |
|||
error(('AttributeError: unauthorized read attempt of internal "%s"'):format(key), 2) |
|||
end, |
end, |
||
__newindex = function (cls_private, key, value) |
__newindex = function (cls_private, key, value) |
||
local cls = classes[cls_private] |
local cls = classes[cls_private] |
||
if cls.__slots[key] then -- __slots should be valid, so no need to check key |
if not cls.__slots[key] and key ~= '__hash' then -- __slots should be valid, so no need to check key validity before |
||
cls[key] = value |
|||
else |
|||
libraryUtil.checkTypeMultiForIndex(key, {'string'}) -- otherwise error message would not make sense |
libraryUtil.checkTypeMultiForIndex(key, {'string'}) -- otherwise error message would not make sense |
||
error(( |
error(("AttributeError: write attempt {%s: %s} not expected by __slots"):format(key, tostring(value)), 2) |
||
elseif key == '__hash' and cls.__hash ~= nil then |
|||
error("AttributeError: forbidden update attempt to immutable __hash", 2) |
|||
end |
end |
||
cls[key] = value |
|||
end, |
end, |
||
__metatable = |
__metatable = "unauthorized access attempt of wrapper object metatable" |
||
} |
} |
||
Line 235: | Line 269: | ||
__call = constructor, |
__call = constructor, |
||
__index = function (cls_public, key) |
__index = function (cls_public, key) |
||
if type(key) ~= 'string' then |
|||
warn(("AttributeWarning: index '%s' type should be string, %s given"):format(tostring(key), type(key)), 2) |
|||
if key:sub(1,1) ~= '_' then |
|||
elseif key:sub(1,1) == '_' then |
|||
local value = classes[classes[cls_public]][key] |
|||
error(("AttributeError: unauthorized read attempt of nonpublic '%s'"):format(key), 2) |
|||
if type(value) == 'table' then |
|||
elseif key:find('[^_%w]') or key:find('^%d') then |
|||
return mw.clone(value) -- all class attributes are immutable in the public scope |
|||
warn(("AttributeWarning: index '%s' should be a valid Lua name"):format(key), 2) |
|||
end |
|||
return value |
|||
end |
end |
||
local value = classes[classes[cls_public]][key] |
|||
error(('AttributeError: unauthorized read attempt of nonpublic "%s"'):format(key), 2) |
|||
local valueType = type(value) |
|||
if valueType == 'table' then |
|||
return mw.clone(value) -- all class attributes are immutable in the public scope |
|||
elseif valueType == 'set' then |
|||
return value.copy() |
|||
end |
|||
return value |
|||
end, |
end, |
||
__newindex = function (cls_public, key, value) |
__newindex = function (cls_public, key, value) |
||
libraryUtil.checkTypeMultiForIndex(key, {'string'}) -- otherwise error message... |
libraryUtil.checkTypeMultiForIndex(key, {'string'}) -- otherwise error message... |
||
error(( |
error(("AttributeError: forbidden write attempt of {%s: %s}; a class is immutable in public scope"):format(key, tostring(value)), 2) |
||
end, |
end, |
||
__metatable = |
__metatable = "unauthorized access attempt of wrapper object metatable" |
||
} |
} |
||
math.randomseed(os.time()) |
|||
local function hash(obj_private) |
|||
if not obj_private.__hash then -- not inheritable |
|||
obj_private.__hash = math.floor(math.random() * 2^32) -- due to math.random bug in Lua 5.1 |
|||
end |
|||
return obj_private.__hash |
|||
end |
|||
function class(...) |
function class(...) |
||
Line 258: | Line 307: | ||
local idx |
local idx |
||
if type(args[1]) == 'string' then |
if type(args[1]) == 'string' then |
||
local __name = args[1] |
|||
if __name:find('%W') or __name:find('^%d') then |
|||
error(("ValueError: class '%s' must be a valid Lua name without '_'s"):format(__name), 2) |
|||
end |
|||
cls.__name = __name |
|||
idx = 2 |
idx = 2 |
||
else |
else |
||
Line 269: | Line 322: | ||
cls.__bases[#cls.__bases+1] = classes[classes[args[i]]] |
cls.__bases[#cls.__bases+1] = classes[classes[args[i]]] |
||
end |
end |
||
setmetatable(cls, cls_mt) |
|||
local kwargs = args[#args] |
local kwargs = args[#args] |
||
libraryUtil.checkType('class', #args, kwargs, 'table') |
libraryUtil.checkType('class', #args, kwargs, 'table') |
||
if kwargs.__name or kwargs.__bases then |
if kwargs.__name ~= nil or kwargs.__bases ~= nil then |
||
error( |
error("ValueError: __name and unpacked __bases must be passed as optional first args to 'class'", 2) |
||
end |
end |
||
cls.__slots = {} |
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 = { |
local mt = { |
||
__index = function (__slots, key) -- multi_inheritance |
__index = function (__slots, key) -- multi_inheritance |
||
for _, base in ipairs(cls.__bases) do |
for _, base in ipairs(cls.__bases) do |
||
if key:sub(1,1) ~= '_' or base.__protected[key] then |
if (key:sub(1,1) ~= '_' or base.__protected[key]) and base.__slots[key] then |
||
return true |
|||
if base.__slots[key] then |
|||
return true |
|||
end |
|||
end |
end |
||
end |
end |
||
Line 299: | Line 341: | ||
} |
} |
||
setmetatable(cls.__slots, mt) |
setmetatable(cls.__slots, mt) |
||
if kwargs.__slots ~= nil then |
|||
libraryUtil.checkTypeForNamedArg('class', '__slots', kwargs.__slots, 'table') |
|||
cls.__protected = {} |
|||
for i, slot in ipairs(kwargs.__slots) do |
|||
if kwargs.__protected then |
|||
libraryUtil.checkType('__slots', i, slot, 'string') |
|||
for _, key in ipairs(kwargs.__protected) do |
|||
if |
if slot:find('[^_%w]') or slot:find('^%d') then |
||
error(("ValueError: invalid slot name '%s'"):format(slot), 2) |
|||
cls.__protected[key] = true |
|||
elseif slot:sub(1,2) == '__' then |
|||
else |
|||
error(( |
error(("ValueError: slot '%s' has forbidden namespace"):format(slot), 2) |
||
elseif rawget(cls.__slots, slot) then |
|||
warn(("ValueWarning: duplicated slot '%s'"):format(slot), 2) |
|||
elseif kwargs[slot] ~= nil or cls.__slots[slot] then |
|||
error(("ValueError: slot '%s' is predefined in class or allocated in __slots of bases"):format(slot), 2) |
|||
end |
end |
||
cls.__slots[slot] = true |
|||
end |
end |
||
kwargs. |
kwargs.__slots = nil |
||
end |
end |
||
cls.__protected = {} |
|||
mt = { |
mt = { |
||
__index = function (__protected, key) |
__index = function (__protected, key) |
||
Line 321: | Line 370: | ||
} |
} |
||
setmetatable(cls.__protected, mt) |
setmetatable(cls.__protected, mt) |
||
if kwargs.__protected ~= nil then |
|||
libraryUtil.checkTypeForNamedArg('class', '__protected', kwargs.__protected, 'table') |
|||
for i, key in ipairs(kwargs.__protected) do |
|||
libraryUtil.checkType('__protected', i, key, 'string') |
|||
if key:sub(1,1) ~= '_' or key:sub(2,2) == '_' then |
|||
error(("ValueError: the namespace of '%s' is not manually protectable"):format(key), 2) |
|||
elseif key == '_hash' then |
|||
error("ValueError: forbidden attempt to protect _hash which is not inheritable", 2) |
|||
elseif rawget(cls.__protected, key) then |
|||
warn(("ValueWarning: duplicated '%s' in __protected"):format(key), 2) |
|||
elseif cls.__protected[key] then |
|||
error(("ValueError: '%s' is already allocated in __protected of bases"):format(key), 2) |
|||
elseif kwargs[key] == nil then -- key validity will be checked further ahead |
|||
error(("ValueError: attempt to protect undefined '%s'"):format(key), 2) |
|||
end |
|||
cls.__protected[key] = true |
|||
end |
|||
kwargs.__protected = nil |
|||
end |
|||
if kwargs.__methods then |
if kwargs.__methods ~= nil then |
||
error( |
error("ValueError: __classmethods and __staticmethods should be passed as optional attributes instead of __methods", 2) |
||
elseif kwargs.hash ~= nil or kwargs.__hash ~= nil then |
|||
error("ValueError: forbidden attempt to define hash or __hash which are set internally", 2) |
|||
end |
end |
||
cls.__normalmethods = {} -- used in instance write methods |
|||
mt = { |
|||
__index = function (__normalmethods, key) |
|||
return cls.__methods[key] or cls.__classmethods[key] or cls.__staticmethods[key] |
|||
end |
|||
} |
|||
setmetatable(cls.__normalmethods, mt) |
|||
local cls_private = {} -- wrapper |
local cls_private = {} -- wrapper |
||
Line 330: | Line 408: | ||
classes[cls_private] = cls |
classes[cls_private] = cls |
||
cls.__classmethods = {} |
|||
mt = { |
|||
for _, key in ipairs(kwargs.__classmethods) do |
|||
__index = function (__classmethods, key) |
|||
for _, base in ipairs(cls.__bases) do |
|||
if (key:sub(1,1) ~= '_' or base.__protected[key]) and base.__classmethods[key] then |
|||
return true |
|||
end |
|||
end |
|||
end |
|||
} |
|||
setmetatable(cls.__classmethods, mt) |
|||
if kwargs.__classmethods ~= nil then |
|||
libraryUtil.checkTypeForNamedArg('class', '__classmethods', kwargs.__classmethods, 'table') |
|||
for i, key in ipairs(kwargs.__classmethods) do |
|||
libraryUtil.checkType('__classmethods', i, key, 'string') |
|||
if key:find('[^_%w]') or key:find('^%d') then |
|||
error(("ValueError: invalid classmethod name '%s'"):format(key), 2) |
|||
elseif key:sub(1,2) == '__' then |
|||
error(("ValueError: classmethod '%s' has forbidden namespace"):format(key), 2) |
|||
elseif key == '_hash' then |
|||
error("ValueError: invalid classmethod _hash, classes have their own hash classmethod", 2) |
|||
elseif rawget(cls.__classmethods, key) then |
|||
error(("ValueError: duplicated '%s' in __classmethods"):format(key), 2) |
|||
elseif not cls.__classmethods[key] and cls[key] ~= nil then |
|||
error(("ValueError: forbidden attempt to convert '%s' non-classmethod to classmethod"):format(key), 2) |
|||
end |
|||
libraryUtil.checkTypeForNamedArg('class', key, kwargs[key], 'function') |
|||
cls.__classmethods[key] = true |
|||
local func = kwargs[key] |
local func = kwargs[key] |
||
cls[key] = function (...) return func(cls_private, ...) end |
cls[key] = function (...) return func(cls_private, ...) end |
||
Line 339: | Line 444: | ||
end |
end |
||
cls.__normalmethods.hash = true |
|||
local staticmethods = {} |
|||
cls.hash = function () return hash(cls_private) end -- classes are always hashable so this is independent from _hash |
|||
if kwargs.__staticmethods then |
|||
cls.hash() |
|||
for _, key in ipairs(kwargs.__staticmethods) do |
|||
staticmethods[key] = true |
|||
if kwargs.__eq == nil and kwargs._hash == nil then |
|||
kwargs._hash = hash |
|||
end |
|||
if kwargs._hash ~= nil then |
|||
libraryUtil.checkTypeForNamedArg('class', '_hash', kwargs._hash, 'function') |
|||
cls.__normalmethods._hash = true |
|||
cls._hash = kwargs._hash |
|||
kwargs._hash = nil |
|||
end |
|||
cls.__staticmethods = {} |
|||
mt = { |
|||
__index = function (__staticmethods, key) |
|||
for _, base in ipairs(cls.__bases) do |
|||
if (key:sub(1,1) ~= '_' or base.__protected[key]) and base.__staticmethods[key] then |
|||
return true |
|||
end |
|||
end |
|||
end |
|||
} |
|||
setmetatable(cls.__staticmethods, mt) |
|||
if kwargs.__staticmethods ~= nil then |
|||
libraryUtil.checkTypeForNamedArg('class', '__staticmethods', kwargs.__staticmethods, 'table') |
|||
for i, key in ipairs(kwargs.__staticmethods) do |
|||
libraryUtil.checkType('__staticmethods', i, key, 'string') |
|||
if key:sub(1,2) == '__' then |
|||
error(("ValueError: staticmethod '%s' has forbidden namespace"):format(key), 2) |
|||
elseif rawget(cls.__staticmethods, key) then |
|||
warn(("ValueWarning: duplicated staticmethod '%s'"):format(key), 2) |
|||
elseif not cls.__staticmethods[key] and cls[key] ~= nil then |
|||
error(("ValueError: forbidden attempt to convert '%s' non-staticmethod to staticmethod"):format(key), 2) |
|||
end |
|||
libraryUtil.checkTypeForNamedArg('class', key, kwargs[key], 'function') |
|||
cls.__staticmethods[key] = true |
|||
end |
end |
||
kwargs.__staticmethods = nil |
kwargs.__staticmethods = nil |
||
Line 358: | Line 497: | ||
local valid = false |
local valid = false |
||
for key, val in pairs(kwargs) do |
for key, val in pairs(kwargs) do |
||
if type(key) ~= 'string' then |
|||
if key:sub(1,2) == '__' and not una_metamethods[key] and not bin_metamethods[key] and not oth_metamethods[key] then |
|||
error(( |
error(("TypeError: invalid attribute name '%s' (string expected, got %s)"):format(tostring(key), type(key)), 2) |
||
elseif key:find('[^_%w]') or key:find('^%d') then |
|||
error(("ValueError: invalid attribute name '%s'"):format(key), 2) |
|||
elseif 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)), 2) |
|||
end |
end |
||
cls[key] = val |
cls[key] = val |
||
if type(val) == 'function' then |
if type(val) == 'function' then |
||
if not |
if not cls.__staticmethods[key] and key:sub(1,2) ~= '__' then -- classmethods and _hash were already removed from kwargs |
||
cls.__methods[key] = true |
cls.__methods[key] = true |
||
end |
end |
||
Line 371: | Line 514: | ||
end |
end |
||
end |
end |
||
if not valid then |
|||
assert(valid, 'AssertionError: a (sub)class must have at least one functional method') |
|||
error("ValueError: a (sub)class must have at least one functional method", 2) |
|||
end |
|||
setmetatable(cls, cls_mt) |
|||
local cls_public = {} |
local cls_public = {} |
||
setmetatable(cls_public, cls_public_mt) |
setmetatable(cls_public, cls_public_mt) |
||
classes[cls_public] = cls_private |
classes[cls_public] = cls_private |
||
return cls_public, cls_private -- cls_private should not be accesible outside the module which defined it! |
|||
return cls_public |
|||
end --make it a proxy to try to enforce behavior? |
|||
end |
|||
Line 517: | Line 660: | ||
local try_clause = args[1] |
local try_clause = args[1] |
||
if args[2] ~= 'except' then |
|||
error("SyntaxError: missing required except clause", 3) |
|||
end |
|||
local except_clauses = {} |
local except_clauses = {} |
||
local i = 3 |
local i = 3 |
||
local argType, exceptionTypes = nil, {string=1, table=1} |
|||
repeat |
repeat |
||
libraryUtil.checkTypeMulti('try', i, args[i], {'string', 'table', 'function'}, 4) |
libraryUtil.checkTypeMulti('try', i, args[i], {'string', 'table', 'function'}, 4) |
||
argType = type(args[i]) |
|||
if exceptionTypes[argType] then |
|||
libraryUtil.checkType('try', i+1, args[i+1], 'function', nil, 4) |
libraryUtil.checkType('try', i+1, args[i+1], 'function', nil, 4) |
||
except_clauses[#except_clauses+1] = {exceptions={}, handler=args[i+1]} |
except_clauses[#except_clauses+1] = {exceptions={}, handler=args[i+1]} |
||
if |
if argType == 'string' then |
||
except_clauses[#except_clauses].exceptions[args[i]] = true |
except_clauses[#except_clauses].exceptions[args[i]] = true |
||
else |
else |
||
for _, exception in ipairs(args[i]) do |
for _, exception in ipairs(args[i]) do |
||
if type(exception) ~= 'string' then |
if type(exception) ~= 'string' then |
||
error(( |
error(("TypeError: invalid exception type in except (string expected, got %s)"):format(type(exception)), 3) |
||
end |
end |
||
except_clauses[#except_clauses].exceptions[exception] = true |
except_clauses[#except_clauses].exceptions[exception] = true |
||
Line 545: | Line 692: | ||
local else_clause, finally_clause |
local else_clause, finally_clause |
||
if args[i-1] == 'except' then |
if args[i-1] == 'except' then |
||
error( |
error("SyntaxError: except after except clause without specific exceptions, which should be the last", 3) |
||
elseif args[i-1] == 'else' then |
elseif args[i-1] == 'else' then |
||
libraryUtil.checkType('try', i, args[i], 'function', nil, 4) |
libraryUtil.checkType('try', i, args[i], 'function', nil, 4) |
||
Line 558: | Line 705: | ||
if args[i-1] ~= nil then |
if args[i-1] ~= nil then |
||
error(( |
error(("SyntaxError: unexpected arguments #%d–#%d to 'try'"):format(i-1, #args), 3) |
||
end |
end |
||
return try_clause, except_clauses, else_clause, finally_clause |
return try_clause, except_clauses, else_clause, finally_clause |
||
Line 571: | Line 718: | ||
for _, except in ipairs(except_clauses) do |
for _, except in ipairs(except_clauses) do |
||
if except.exceptions[errtype] or #except.exceptions == 0 then |
if except.exceptions[errtype] or #except.exceptions == 0 then |
||
handled, message = pcall(except.handler |
handled, message = pcall(except.handler) |
||
break |
break |
||
end |
end |
||
Line 588: | Line 735: | ||
end |
end |
||
if not success and message then |
if not success and message then |
||
error(message) |
error(message, 0) -- what should be the level? |
||
end |
end |
||
end |
end |
Revision as of 22:48, 21 July 2021
![]() | 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.
local libraryUtil = require('libraryUtil') -- overridden for new types and exceptions
local warn = require('Module:Warning')
local classes, instances = {}, {} -- registry of all complete/internal class and instance objects and their pointers
local inst_private_mts, inst_public_mts = {}, {} -- for each class since they are mostly 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, __classmethods=1, __staticmethods=1, __normalmethods=1, __slots=1, __protected=1}
-- __class and __hash
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) or key == 'hash' or key == '__hash' then -- don't search numeric keys in classes and hash isn't inheritable
return nil
end
return self.__class[key] -- key could be invalid here without issues as __index(cls_private, key) would handle it
end,
__tostring = objtostr--
}
local function private_read(self_private, key)
return instances[self_private][key] -- instance should be clean of invalid keys so that __index(cls_private, key) handles them
end
local function private_read_custom(self_private, key)
if not_metamethods[key] then
error(("AttributeError: unauthorized read attempt of internal '%s'"):format(key), 2)
end
local self = instances[self_private]
local value = self.__class.__index(self_private, key) -- custom __index can handle invalid keys
if value == nil then
return self[key] -- same reason of private_read for not checking key validity
end
return value
end
local function private_write(self_private, key, value)
libraryUtil.checkTypeMultiForIndex(key, {'string', 'number'})
local self = instances[self_private]
if type(key) == 'string' then
local cls = classes[self.__class]
if cls.__normalmethods[key] or key:sub(1,2) == '__' and key ~= '__hash' then
error(("AttributeError: forbidden write attempt {%s: %s} to immutable method or invalid key"):format(key, tostring(value)), 2)
elseif key:find('[^_%w]') or key:find('^%d') then
error(("AttributeError: invalid attribute name '%s'"):format(key), 2)
elseif key == '__hash' and self.__hash ~= nil then
error("AttributeError: forbidden update attempt to immutable __hash", 2)
end
end
self[key] = value
end
local function private_write_custom(self_private, key, value)
local self = instances[self_private]
local cls = classes[self.__class]
local keyType = type(key)
if keyType == 'string' and (cls.__normalmethods[key] or key:sub(1,2) == '__' and key ~= '__hash') then
error(("AttributeError: forbidden write attempt {%s: %s} to immutable method or invalid key"):format(key, tostring(value)), 2)
end
if cls.__newindex(self_private, key, value) == false then -- custom __newindex can handle invalid keys
libraryUtil.checkTypeMultiForIndex(key, {'string', 'number'})
if keyType == 'string' then
if key:find('[^_%w]') or key:find('^%d') then
error(("AttributeError: invalid attribute name '%s'"):format(key), 2)
elseif key == '__hash' and self.__hash ~= nil then
error("AttributeError: forbidden update attempt to immutable __hash", 2)
end
end
self[key] = value
end
end
local function public_read(self_public, key)
if type(key) == 'string' and key:sub(1,1) == '_' then
error(("AttributeError: unauthorized read attempt of nonpublic '%s'"):format(key), 2)
end
return instances[instances[self_public]][key] -- same reason of private_read...
end
local function public_read_custom(self_public, key)
if type(key) == 'string' and key:sub(1,1) == '_' then
error(("AttributeError: unauthorized read attempt of nonpublic '%s'"):format(key), 2)
end
local self = instances[instances[self_public]]
local value = self.__class.__index(instances[self_public], key)
if value == nil then
return self[key] -- same reason of private_read...
end
return value
end
local function public_write(self_public, key, value)
local self = instances[instances[self_public]]
local cls = classes[self.__class]
if type(key) == 'string' then
if key:sub(1,1) == '_' then
error(("AttributeError: unauthorized write attempt of nonpublic {%s: %s}"):format(key, tostring(value)), 2)
elseif cls.__normalmethods[key] then
error(("AttributeError: forbidden write attempt {%s: %s} to immutable method"):format(key, tostring(value)), 2)
end
end
if self[key] == nil and not cls.__slots[key] then -- if instance and __slots are valid, no danger of creating invalid 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)
local self = instances[instances[self_public]]
local cls = classes[self.__class]
if type(key) == 'string' then
if key:sub(1,1) == '_' then
error(("AttributeError: unauthorized write attempt of nonpublic {%s: %s}"):format(key, tostring(value)), 2)
elseif cls.__normalmethods[key] then
error(("AttributeError: forbidden write attempt {%s: %s} to immutable method"):format(key, tostring(value)), 2)
end
end
if cls.__newindex(instances[self_public], key, value) == false 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
setmetatable(self, inst_mt)
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
local func = cls[key] -- index once to save time in future calls
self[key] = function (...) return func(self_private, ...) end
end
if cls._hash then -- not inheritable
self.hash = function () return cls._hash(self_private) end
self.hash() -- construction of self is finalized at this point, so immutable hash can be safely set
end
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
local func = cls[key]
mt[key] = function (a) return func(instances[a]) end
end
end
for key in pairs(bin_metamethods) do
if cls[key] then
local func = cls[key]
mt[key] = function (a, b) return func(instances[a], instances[b]) end
end
end
if cls.__call then
local func = cls.__call
mt.__call = function (self_public, ...) return func(instances[self_public], ...) end
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' and key ~= '__hash' 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 type(key) ~= 'string' then
warn(("AttributeWarning: index '%s' type should be string, %s given"):format(tostring(key), type(key)), 2)
elseif not_metamethods[key] then
error(("AttributeError: unauthorized read attempt of internal '%s'"):format(key), 2)
elseif key:find('[^_%w]') or key:find('^%d') then
warn(("AttributeWarning: index '%s' should be a valid Lua name"):format(key), 2)
end
local cls = classes[cls_private]
local value = cls[key]
if not cls.__slots[key] then
local valueType = type(value)
if valueType == 'table' then
return mw.clone(value) -- because class attributes are immutable by default
elseif valueType == 'set' then --should list be clone or deep copy?
return value.copy()
end
end
return value
end,
__newindex = function (cls_private, key, value)
local cls = classes[cls_private]
if not cls.__slots[key] and key ~= '__hash' then -- __slots should be valid, so no need to check key validity before
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)
elseif key == '__hash' and cls.__hash ~= nil then
error("AttributeError: forbidden update attempt to immutable __hash", 2)
end
cls[key] = value
end,
__metatable = "unauthorized access attempt of wrapper object metatable"
}
local cls_public_mt = {
__call = constructor,
__index = function (cls_public, key)
if type(key) ~= 'string' then
warn(("AttributeWarning: index '%s' type should be string, %s given"):format(tostring(key), type(key)), 2)
elseif key:sub(1,1) == '_' then
error(("AttributeError: unauthorized read attempt of nonpublic '%s'"):format(key), 2)
elseif key:find('[^_%w]') or key:find('^%d') then
warn(("AttributeWarning: index '%s' should be a valid Lua name"):format(key), 2)
end
local value = classes[classes[cls_public]][key]
local valueType = type(value)
if valueType == 'table' then
return mw.clone(value) -- all class attributes are immutable in the public scope
elseif valueType == 'set' then
return value.copy()
end
return value
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"
}
math.randomseed(os.time())
local function hash(obj_private)
if not obj_private.__hash then -- not inheritable
obj_private.__hash = math.floor(math.random() * 2^32) -- due to math.random bug in Lua 5.1
end
return obj_private.__hash
end
function class(...)
local args = {...}
local cls = {} -- internal
local idx
if type(args[1]) == 'string' then
local __name = args[1]
if __name:find('%W') or __name:find('^%d') then
error(("ValueError: class '%s' must be a valid Lua name without '_'s"):format(__name), 2)
end
cls.__name = __name
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
setmetatable(cls, cls_mt)
local kwargs = args[#args]
libraryUtil.checkType('class', #args, kwargs, 'table')
if kwargs.__name ~= nil or kwargs.__bases ~= nil then
error("ValueError: __name and unpacked __bases must be passed as optional first args to 'class'", 2)
end
cls.__slots = {}
local mt = {
__index = function (__slots, key) -- multi_inheritance
for _, base in ipairs(cls.__bases) do
if (key:sub(1,1) ~= '_' or base.__protected[key]) and base.__slots[key] then
return true
end
end
end
}
setmetatable(cls.__slots, mt)
if kwargs.__slots ~= nil then
libraryUtil.checkTypeForNamedArg('class', '__slots', kwargs.__slots, 'table')
for i, slot in ipairs(kwargs.__slots) do
libraryUtil.checkType('__slots', i, slot, 'string')
if slot:find('[^_%w]') or slot:find('^%d') then
error(("ValueError: invalid slot name '%s'"):format(slot), 2)
elseif slot:sub(1,2) == '__' then
error(("ValueError: slot '%s' has forbidden namespace"):format(slot), 2)
elseif rawget(cls.__slots, slot) then
warn(("ValueWarning: duplicated slot '%s'"):format(slot), 2)
elseif kwargs[slot] ~= nil or cls.__slots[slot] then
error(("ValueError: slot '%s' is predefined in class or allocated in __slots of bases"):format(slot), 2)
end
cls.__slots[slot] = true
end
kwargs.__slots = nil
end
cls.__protected = {}
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.__protected ~= nil then
libraryUtil.checkTypeForNamedArg('class', '__protected', kwargs.__protected, 'table')
for i, key in ipairs(kwargs.__protected) do
libraryUtil.checkType('__protected', i, key, 'string')
if key:sub(1,1) ~= '_' or key:sub(2,2) == '_' then
error(("ValueError: the namespace of '%s' is not manually protectable"):format(key), 2)
elseif key == '_hash' then
error("ValueError: forbidden attempt to protect _hash which is not inheritable", 2)
elseif rawget(cls.__protected, key) then
warn(("ValueWarning: duplicated '%s' in __protected"):format(key), 2)
elseif cls.__protected[key] then
error(("ValueError: '%s' is already allocated in __protected of bases"):format(key), 2)
elseif kwargs[key] == nil then -- key validity will be checked further ahead
error(("ValueError: attempt to protect undefined '%s'"):format(key), 2)
end
cls.__protected[key] = true
end
kwargs.__protected = nil
end
if kwargs.__methods ~= nil then
error("ValueError: __classmethods and __staticmethods should be passed as optional attributes instead of __methods", 2)
elseif kwargs.hash ~= nil or kwargs.__hash ~= nil then
error("ValueError: forbidden attempt to define hash or __hash which are set internally", 2)
end
cls.__normalmethods = {} -- used in instance write methods
mt = {
__index = function (__normalmethods, key)
return cls.__methods[key] or cls.__classmethods[key] or cls.__staticmethods[key]
end
}
setmetatable(cls.__normalmethods, mt)
local cls_private = {} -- wrapper
setmetatable(cls_private, cls_private_mt)
classes[cls_private] = cls
cls.__classmethods = {}
mt = {
__index = function (__classmethods, key)
for _, base in ipairs(cls.__bases) do
if (key:sub(1,1) ~= '_' or base.__protected[key]) and base.__classmethods[key] then
return true
end
end
end
}
setmetatable(cls.__classmethods, mt)
if kwargs.__classmethods ~= nil then
libraryUtil.checkTypeForNamedArg('class', '__classmethods', kwargs.__classmethods, 'table')
for i, key in ipairs(kwargs.__classmethods) do
libraryUtil.checkType('__classmethods', i, key, 'string')
if key:find('[^_%w]') or key:find('^%d') then
error(("ValueError: invalid classmethod name '%s'"):format(key), 2)
elseif key:sub(1,2) == '__' then
error(("ValueError: classmethod '%s' has forbidden namespace"):format(key), 2)
elseif key == '_hash' then
error("ValueError: invalid classmethod _hash, classes have their own hash classmethod", 2)
elseif rawget(cls.__classmethods, key) then
error(("ValueError: duplicated '%s' in __classmethods"):format(key), 2)
elseif not cls.__classmethods[key] and cls[key] ~= nil then
error(("ValueError: forbidden attempt to convert '%s' non-classmethod to classmethod"):format(key), 2)
end
libraryUtil.checkTypeForNamedArg('class', key, kwargs[key], 'function')
cls.__classmethods[key] = true
local func = kwargs[key]
cls[key] = function (...) return func(cls_private, ...) end
kwargs[key] = nil
end
kwargs.__classmethods = nil
end
cls.__normalmethods.hash = true
cls.hash = function () return hash(cls_private) end -- classes are always hashable so this is independent from _hash
cls.hash()
if kwargs.__eq == nil and kwargs._hash == nil then
kwargs._hash = hash
end
if kwargs._hash ~= nil then
libraryUtil.checkTypeForNamedArg('class', '_hash', kwargs._hash, 'function')
cls.__normalmethods._hash = true
cls._hash = kwargs._hash
kwargs._hash = nil
end
cls.__staticmethods = {}
mt = {
__index = function (__staticmethods, key)
for _, base in ipairs(cls.__bases) do
if (key:sub(1,1) ~= '_' or base.__protected[key]) and base.__staticmethods[key] then
return true
end
end
end
}
setmetatable(cls.__staticmethods, mt)
if kwargs.__staticmethods ~= nil then
libraryUtil.checkTypeForNamedArg('class', '__staticmethods', kwargs.__staticmethods, 'table')
for i, key in ipairs(kwargs.__staticmethods) do
libraryUtil.checkType('__staticmethods', i, key, 'string')
if key:sub(1,2) == '__' then
error(("ValueError: staticmethod '%s' has forbidden namespace"):format(key), 2)
elseif rawget(cls.__staticmethods, key) then
warn(("ValueWarning: duplicated staticmethod '%s'"):format(key), 2)
elseif not cls.__staticmethods[key] and cls[key] ~= nil then
error(("ValueError: forbidden attempt to convert '%s' non-staticmethod to staticmethod"):format(key), 2)
end
libraryUtil.checkTypeForNamedArg('class', key, kwargs[key], 'function')
cls.__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 type(key) ~= 'string' then
error(("TypeError: invalid attribute name '%s' (string expected, got %s)"):format(tostring(key), type(key)), 2)
elseif key:find('[^_%w]') or key:find('^%d') then
error(("ValueError: invalid attribute name '%s'"):format(key), 2)
elseif 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)), 2)
end
cls[key] = val
if type(val) == 'function' then
if not cls.__staticmethods[key] and key:sub(1,2) ~= '__' then -- classmethods and _hash were already removed from kwargs
cls.__methods[key] = true
end
if key ~= '__init' then -- __init does not qualify to a functional/proper class
valid = true
end
end
end
if not valid then
error("ValueError: a (sub)class must have at least one functional method", 2)
end
local cls_public = {}
setmetatable(cls_public, cls_public_mt)
classes[cls_public] = cls_private
return cls_public, cls_private -- cls_private should not be accesible outside the module which defined it!
end --make it a proxy to try to enforce behavior?
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]
if args[2] ~= 'except' then
error("SyntaxError: missing required except clause", 3)
end
local except_clauses = {}
local i = 3
local argType, exceptionTypes = nil, {string=1, table=1}
repeat
libraryUtil.checkTypeMulti('try', i, args[i], {'string', 'table', 'function'}, 4)
argType = type(args[i])
if exceptionTypes[argType] then
libraryUtil.checkType('try', i+1, args[i+1], 'function', nil, 4)
except_clauses[#except_clauses+1] = {exceptions={}, handler=args[i+1]}
if argType == '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)), 3)
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", 3)
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, 0) -- what should be the level?
end
end
return classes, instances--