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--