Module:Class

local p = {} local helpers = {} local util = {} local classMtFuncs = {} local staticMtFuncs = {} local protoMtFuncs = {} local classCount = 0

_G._DEBUG = _G.DEBUG or false local _DEBUG = _G._DEBUG local getmetatable, setmetatable, string, table, select, error, ipairs, pairs, tostring, type, rawget, rawset, next, unpack = getmetatable, setmetatable, string, table, select, error, ipairs, pairs, tostring, type, rawget, rawset, next, unpack local tinsert = table.insert local sformat = string.format local debug = debug

-- Currently missing features: -- Super indexing (`self.super[k]`, `self.super:method`) -- Mixins

local MESSAGES = { INVALID_SUPER_CALL = "Invalid super constructor call. The class must have a parent to call the super constructor", GETTER_ONLY_ASSIGNMENT = 'Cannot set class property %q{property} which has only a getter', GETTER_ONLY_STATIC_ASSIGNMENT = 'Cannot set static class property %q{property} which has only a getter', STATIC_GETTER_ONLY_ASSIGNMENT = 'Cannot set static class property %q{property} which has only a getter', MUST_CALL_SUPER = "Invalid class construction. The child class constructor must call `self:super(...)`", INVALID_CLASS_ACCESSOR = "Invalid class %{accessor} %q{property}: class %{accessor}s must be functions, recived a %{type} (%q{value}) instead", INVALID_STATIC_CLASS_ACCESSOR = "Invalid static class %{accessor} %q{property}: class %{accessor}s must be functions, recived a %{type} (%q{value}) instead", INVAID_SELF_SUPER = ":super was passed an invalid self object of type %{type} (%q{value}). Did you call :super with a '.' instead of a ':', i.e `.super(...)` instead of `:super(...)`", CLASS_FIELD_RESERVED = "Cannot set class property %q{property} because it is reserved", STATIC_CLASS_FIELD_RESERVED = "Cannot set static class property %q{property} because it is reserved", PROTO_FIELD_RESERVED = "Cannot set class prototype property %q{property} because it is reserved", PROPERTIES_TYPE = 'Class properties must be of type table, recived type %q{type} instead', STATIC_PROPERTIES_TYPE = 'Class static properties must be of type table, recived type %q{type} instead', INVALID_PARENT_CLASS = "Invalid parent class. The parent class must be a class created by makeClass", VALUE_NOT_CLASS = "bad argument #%{pos} to %q{name} (value of type %{type} (%q{value}) is not a class)", VALUE_NOT_INSTANCE = "bad argument #%{pos} to %q{name} (value of type %{type} (%q{value}) is not a class instance)", VALUE_NOT_CLASS_OR_INSTANCE = "bad argument #%{pos} to %q{name} (value of type %{type} (%q{value}) is not a class instance or class)", VALUE_NOT_CLASS_OR_INSTANCE_OR_PROTO = "Value of type %{type} (%q{value}) is not a class instance or class or a class prototype object", INVALID_SELF_OBJECT = "Class method :%{name} was passed an invalid self object of type %{type} (%q{value}). Did you mean to use ':' instead of '.' to call this method (`:%{name}(...)` instead of `.%{name}(...)`)", INVALID_STATIC_SELF_OBJECT = "Static class method :%{name} was passed an invalid self object of type %{type} (%q{value}). Did you mean to use ':' instead of '.' to call this method (`:%{name}(...)` instead of `.%{name}(...)`)", INVALID_PROTO_VALUE = "Cannot reassign the class prototype to a value whose type is not a table, recived a value of type %{type} (%q{value}) instead", INVALID_METHOD_ARG_TYPE = "bad argument #%{pos} to class method %q{name} (%{expected} expected, got %{type})", VALUE_EXPECTED = "bad argument #%{pos} to class method %q{name} (value expected)", INVALID_STATIC_METHOD_ARG_TYPE = "bad argument #%{pos} to static class method %q{name} (%{expected} expected, got %{type})", VALUE_EXPECTED_STATIC = "bad argument #%{pos} to static class method %q{name} (value expected)", INVAID_ARG_TYPE = "bad argument #%{pos} to %q{name} (%{expected} expected, got %{type})", INVALID_CONSTRUCTOR_VALUE = "Cannot set property 'constructor' to a value which is not a function", }

-- Global list of classes local classRegistry = setmetatable({}, { __mode="k" }) local instanceRegistry = setmetatable({}, { __mode="k" }) local prototypeRegistry = setmetatable({}, { __mode="k" }) - -- Helper functions - -- Pack values function helpers.pack(...) local n = select("#", ...)

return { n = n, ... }; end

local function noop end

function helpers.safetostring(v) local _, res = xpcall(function		return tostring(v)	end, function		if p.isClass(v) then			return "class"		elseif p.isInstance(v) then			return "class instance"		elseif p.isPrototype(v) then			return "class prototype"		else			return type(v)		end	end)

return res end

-- interpolate a formatted string, syntax is the same as `string.format(...)` except items are annotated with `% {}` -- ex: helpers.interpolate("%q{test}", { test="Test1" }) -> "\"Test1\"" function helpers.interpolate(s, substutions) local items = {} local i = 0

s = s:gsub('(%%([%d%.%#+%-]*)(%w?)%b{})', function(w, options, substutionType)		i = i + 1		local start, stop = 3 + (#(options .. substutionType)), -2		tinsert(items, substutions[w:sub(start, stop)] or error("Missing interpolation parameter '" .. w:sub(start, stop) .. "'", 4))

return "%" .. (substutionType ~= "" and substutionType or "s") end)

return sformat(s, unpack(items, 1, i)) end

-- Bind a `self` value to a function -- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks if _DEBUG then function helpers.proxy(t, func) return function(...) local res = helpers.pack(({ [" "] = func })[" "](t, ...))

if t then return unpack(res, 1, res.n) end end end else function helpers.proxy(t, func) return function(...) return func(t, ...) end end end

-- Get table keys function helpers.tableKeys(t) local ret = {}

for k in pairs(t) do		tinsert(ret, k)	end

return ret end

-- Merge keys into table function helpers.merge(t1, ...) if select('#', ...) > 1 then for _, t in ipairs{ ... } do			for k, v in pairs(t) do				t1[k] = v			end end else for k, v in pairs(...) do			t1[k] = v		end end

return t1 end

function helpers.clearTable(t) for k in pairs(t) do		t[k] = nil end

return t end

function helpers.getMethodName(level) local methodName = mw.text.split(debug.traceback, "\n")[(level or 0) + 3]:match("in function [\'\"](.-)[\'\"]")

return methodName end

function helpers.getName(value) local s

if p.isInstance(value) then s = 'instance of class "' .. debug.getNameOf(value) .. '"' elseif p.isPrototype(value) then s = 'prototype of class "' .. debug.getNameOf(value) .. '"' elseif p.isClass(value) then s = 'class "' .. debug.getNameOf(value) .. '"' else s = type(value) .. ' ("' .. helpers.safetostring(value) .. '")' end return s end

-- Function to create a child class form a given parent function helpers.createChildClass(parent, classData) helpers.assertStaticSelf(parent) classData = classData or {} if type(classData) == 'string' then return function(data) data = data or {} data.parent = parent data.__metadata = data.__metadata or {} data.__metadata.name = classData return p.makeClass(data) end else classData.parent = parent return p.makeClass(classData) end end - -- Assertion utilities - function helpers.assertIsClass(v, pos) if not p.isClass(v) then error(helpers.interpolate(MESSAGES.VALUE_NOT_CLASS, { value = v ~= nil and helpers.safetostring(v) or "nil", type = type(v), pos = pos or 1, name = helpers.getMethodName(1) }), 3) end

return v end

function helpers.assertIsInstance(v, pos) if not p.isInstance(v) then error(helpers.interpolate(MESSAGES.VALUE_NOT_INSTANCE, { value = v ~= nil and helpers.safetostring(v) or "nil", type = type(v), pos = pos or 1, name = helpers.getMethodName(1) }), 3) end

return v end

function helpers.assertIsInstanceOrClass(v, pos) if not p.isInstance(v) and not p.isClass(v) then error(helpers.interpolate(MESSAGES.VALUE_NOT_CLASS_OR_INSTANCE, { value = v ~= nil and helpers.safetostring(v) or "nil", type = type(v), pos = pos or 1, name = helpers.getMethodName(1) }), 3) end

return v end

function helpers.assertIsInstanceOrClassOrProto(v, pos) if not p.isInstance(v) and not p.isClass(v) and not p.isPrototype(v) then error(helpers.interpolate(MESSAGES.VALUE_NOT_CLASS_OR_INSTANCE_OR_PROTO, { value = v ~= nil and helpers.safetostring(v) or "nil", type = type(v), pos = pos or 1, name = helpers.getMethodName(1) }), 3) end

return v end

function helpers.assertStaticSelf(v) if not p.isClass(v) then error(helpers.interpolate(MESSAGES.INVALID_STATIC_SELF_OBJECT, { name = helpers.getMethodName(1), value = helpers.safetostring(v), type = type(v) }), 3) end

return v end

- -- functions used inside of interals - function helpers.defaultConstructor(self) return self end

function helpers.defaultInheritedConstructor(self, ...) self:super(...)

return self end

function helpers.invalidSuperCall error(MESSAGES.INVALID_SUPER_CALL, 2) end

function helpers.ipairsFunc(t, i)	i = i + 1

local v = t[i] if v ~= nil then return i, v end end

local oldLog = mw.log - -- Overwrite mw.dumpObject to accept classes - function mw.dumpObject(object) local doneTable = {} local doneObj = {} local ct = {} local function sorter(a, b)		local ta, tb = type(a), type(b) if ta ~= tb then return ta < tb		end if ta == 'string' or ta == 'number' then return a < b		end if ta == 'boolean' then return tostring(a) < tostring(b) end return false -- Incomparable end local function _dumpObject(object, indent, expandTable) local tp = type(object) if tp == 'number' or tp == 'nil' or tp == 'boolean' then return tostring(object) elseif tp == 'string' then return string.format("%q", object) elseif tp == 'table' then local s = helpers.safetostring(object)

if not doneObj[object] then if s == 'table' or classRegistry[object] or instanceRegistry[object] or prototypeRegistry[object] then if classRegistry[object] then s = 'class(' .. object.__metadata__.name .. ')' elseif instanceRegistry[object] then s = 'class instance(' .. object.__class__.__metadata__.name .. ')' elseif prototypeRegistry[object] then s = 'class prototype(' .. object.__holdingClass__.__metadata__.name .. ')' end

ct[tp] = (ct[tp] or 0) + 1 doneObj[object] = s .. '#' .. ct[tp] else doneObj[object] = s					doneTable[object] = true end end if doneTable[object] or not expandTable then return doneObj[object] end doneTable[object] = true

local ret = { doneObj[object], ' {\n' } local mt = getmetatable(object) if mt then ret[#ret + 1] = string.rep(" ", indent + 2) ret[#ret + 1] = 'metatable = ' ret[#ret + 1] = _dumpObject(mt, indent + 2, false) ret[#ret + 1] = "\n" end

local doneKeys = {} for key, value in ipairs(object) do				doneKeys[key] = true ret[#ret + 1] = string.rep(" ", indent + 2) ret[#ret + 1] = _dumpObject(value, indent + 2, true) ret[#ret + 1] = ',\n' end local keys = {} for key in pairs(object) do				if not doneKeys[key] then keys[#keys + 1] = key end end table.sort(keys, sorter) for i = 1, #keys do				local key = keys[i] ret[#ret + 1] = string.rep(" ", indent + 2) ret[#ret + 1] = '[' ret[#ret + 1] = _dumpObject(key, indent + 3, false) ret[#ret + 1] = '] = ' ret[#ret + 1] = _dumpObject(object[key], indent + 2, true) ret[#ret + 1] = ",\n" end

if p.isClass(object) or p.isPrototype(object) or p.isInstance(object) then local proto = object.__proto__

if proto then ret[#ret + 1] = string.rep(" ", indent + 2) ret[#ret + 1] = '(Prototype) = ' ret[#ret + 1] = _dumpObject(object.__proto__, indent + 2, true) ret[#ret + 1] = ",\n" end end

ret[#ret + 1] = string.rep(" ", indent) ret[#ret + 1] = '}'

return table.concat(ret) else if not doneObj[object] then ct[tp] = (ct[tp] or 0) + 1 doneObj[object] = tostring(object) .. '#' .. ct[tp] end return doneObj[object] end end return _dumpObject(object, 0, true) end

function mw.logObject(v, prefix) mw.log((prefix and prefix .. " " or "") .. mw.dumpObject(v)) end p.dump = mw.dumpObject p.dumpObject = mw.dumpObject p.log = mw.logObject p.logObject = mw.logObject

- -- Internal Data - local metaProperties = { ['__index'] = 1, ['__newindex'] = 1, ['__mode'] = 1, ['__tostring'] = 1, ['__concat'] = 1, ['__metatable'] = 1, ['__ipairs'] = 1, ['__pairs'] = 1, ['__pow'] = 1, ['__add'] = 1, ['__sub'] = 1, ['__div'] = 1, ['__mul'] = 1, ['__unm'] = 1, ['__eq'] = 1, ['__lt'] = 1, ['__le'] = 1, }

-- TODO: Maybe add custom metamethods? local customStaticMetaProperties = { ['__getter'] = 1, ['__setter'] = 1, }

local customMetaProperties = { ['__getter'] = 1, ['__setter'] = 1, ['__protoIndex'] = 1, }

-- Properties that maybe added directly to the metatable without need for further modificiation local contextSafeProperties = { ['__tostring'] = 1, ['__pairs'] = 1, ['__ipairs'] = 1, ["__unm"] = 1, }

-- Relational operators local relationalOperators = { ['__add'] = 1, ['__sub'] = 1, ['__div'] = 1, ['__mul'] = 1, ['__pow'] = 1, ['__le'] = 1, ['__lt'] = 1, ['__concat'] = 1, }

local reservedProps = { ["super"] = 1, ["__proto__"] = 1, ['__getters__'] = 1, ['__setters__'] = 1, ['__parent__'] = 1, ['__class__'] = 1, ['__static__'] = 1, ['__metadata__'] = 1, }

local reservedProtoProps = { ['__proto__'] = 1, ['__holdingClass__'] = 1, ['__getters__'] = 1, ['__setters__'] = 1, ['__parent__'] = 1, }

local reservedStaticProps = { ['__parent__'] = 1, ['__proto__'] = 1, ['__setters__'] = 1, ['__getters__'] = 1, ['__children__'] = 1, ['__parents__'] = 1, ['__metadata__'] = 1, ['constructor'] = 1, ['super'] = 1, ['childClass'] = 1, }

- -- Class structure -- -- Static class -- * Static metatable --	* Static Parent metatable -- * Class prototype --	* Class prototype metatable -- * Class instance -- * Class instance metatable -- * Parent class (repeat above) - function helpers:overwriteProto(t, isOverwriting) if isOverwriting and #helpers.tableKeys(self.__prototype) > 0 then helpers.clearTable(self.__prototype) helpers.clearTable(self.__classGetters) helpers.clearTable(self.__classSetters) helpers.clearTable(self.__classMetamethods) end

for k, v in pairs(t) do		if k ~= 'constructor' then self.__protoMt.__fakeTable[k] = v		elseif k == 'constructor' then if type(v) == 'function' then self.__origConstructor = v			else error(helpers.interpolate(MESSAGES.INVALID_CONSTRUCTOR_VALUE), 2) end end end end

function helpers.parseAccessorTable(k, v, setters, getters, len, static) static = static ~= false

len = len or #helpers.tableKeys(v) local get = v.get local set = v.set if len > 0 and len <= 2 and (set or get) then if set ~= nil then if type(set) == "function" then setters[k] = set else return helpers.interpolate(					static and MESSAGES.INVALID_STATIC_CLASS_ACCESSOR or MESSAGES.INVALID_CLASS_ACCESSOR,					{ accessor = "setter", property = k, type = type(set), value = set }				) end end if get ~= nil then if type(get) == "function" then getters[k] = get else return helpers.interpolate(					static and MESSAGES.INVALID_STATIC_CLASS_ACCESSOR or MESSAGES.INVALID_CLASS_ACCESSOR,					{ accessor = "getter", property = k, type = type(get), value = get }				) end end end end

- -- Class metatable methods - function classMtFuncs.__index(t, k)	local self = instanceRegistry[t] local staticMt = self.__staticMt

if k == 'super' then return self.__super end if reservedProps[k] then if k == '__getters__' then return staticMt.__classGetters elseif k == '__setters__' then return staticMt.__classSetters elseif k == '__static__' or k == '__class__' then return staticMt.__class elseif k == '__proto__' then return staticMt.__protoMt.__fakeTable elseif k == '__parent__' then return staticMt.__parentStaticMt.__class end end local __index = staticMt.__classMetamethods.__index local protoProp = staticMt.__prototype[k] local getter = staticMt.__classGetters[k]

-- If requested value does not exist in current prototype, repeat same step on parent prototypes if staticMt.__hasParent and protoProp == nil and getter == nil then local cur = staticMt.__parentStaticMt local curGetters = cur.__classGetters local curProto = cur.__prototype

while cur do			local value = curProto[k] local foundGetter = curGetters[k]

if foundGetter ~= nil then getter = foundGetter break elseif value ~= nil then protoProp = value break end

if not cur.__parentStaticMt then break end

cur = cur.__parentStaticMt curProto = cur.__prototype curGetters = cur.__classGetters end end

if getter ~= nil then -- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks if _DEBUG then local res = helpers.pack(({ [" "] = getter })[" "](t))

if getter then return unpack(res, 1, res.n) end else return getter(t) end end

if protoProp ~= nil then return protoProp else if __index then -- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stack if _DEBUG then local res = helpers.pack(__index(t, k)) if self then return unpack(res, 1, res.n) end else return __index(t, k)			end end

return nil end end

function classMtFuncs.__newindex(t, k, v)	if reservedProps[k] then error(helpers.interpolate(MESSAGES.CLASS_FIELD_RESERVED, { property = k }), 2) end

local self = instanceRegistry[t] local staticMt = self.__staticMt local setter = staticMt.__classSetters[k] local getter = staticMt.__classGetters[k] local __newindex = staticMt.__classMetamethods.__newindex

-- Search for setter on parent classes if staticMt.__hasParent and setter == nil then local cur = staticMt.__parentStaticMt local curSetters = cur.__classSetters local curGetters = cur.__classGetters local curProto = cur.__prototype

while cur do			local foundSetter = curSetters[k] getter = curGetters[k]

if foundSetter ~= nil then setter = foundSetter break end

curProto = cur.__prototype curSetters = cur.__classSetters cur = cur.__parentStaticMt end end if getter and not setter then error(helpers.interpolate(MESSAGES.GETTER_ONLY_ASSIGNMENT, { property = k }), 3) elseif setter ~= nil then -- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks if _DEBUG then local res = helpers.pack(({ [" "] = setter })[" "](t, v))

if setter then return unpack(res, 1, res.n) end else return setter(t, v)		end else if __newindex then -- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks if _DEBUG then local res = helpers.pack(__newindex(t, k, v)) if self then return unpack(res, 1, res.n) end else return __newindex(t, k, v)			end end

return rawset(t, k, v)	end end

-- Override default pairs, invoke class getters in the process function classMtFuncs.__pairs(t) local self = instanceRegistry[t] local staticMt = self.__staticMt local getters = helpers.tableKeys(staticMt.__classGetters) local i = 0 local onGetters = false local __pairs = staticMt.__classMetamethods.__pairs

if __pairs then -- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks if _DEBUG then local res = helpers.pack(__pairs(t, k, v))

if self then return unpack(res, 1, res.n) end else return __pairs(t, k, v)		end end

return function(t, k1) local k, v

if not onGetters then k, v = next(t, k1) end

-- Insert getters into results if k == nil then onGetters = true i = i + 1

k, v = getters[i], t[getters[i]] end

if k == nil then return nil, nil else return k, v		end end, t, nil end

function classMtFuncs.__ipairs(t) local self = instanceRegistry[t] local __ipairs = self.__staticMt.__classMetamethods.__ipairs

if __ipairs then -- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks if _DEBUG then local res = helpers.pack(__ipairs(t, k, v))

if self then return unpack(res, 1, res.n) end else return __ipairs(t, k, v)		end end

return function(t, i)		i = i + 1

local v = t[i] if v ~= nil then return i, v end end, t, 0 end

function classMtFuncs.__tostring(t) local self = instanceRegistry[t] local __tostring = self.__staticMt.__classMetamethods.__tostring

if __tostring then -- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks if _DEBUG then local res = helpers.pack(__tostring(t, k, v))

if self then return unpack(res, 1, res.n) end else return __tostring(t, k, v)		end end

return "class instance" end

- -- Static class metamethods - -- Construct class metadatatable function helpers.createClassMt(staticMt) local classMt = { __staticMt = staticMt, }

classMt.__metatable = { "instance metatable" }

-- Set metamethods for k, v in pairs(classMtFuncs) do		classMt[k] = v	end

return classMt end

-- Called when actually constructing the class function staticMtFuncs.__call(t, ...) local created = {} local self = classRegistry[t] or (instanceRegistry[t] and instanceRegistry[t].__staticMt) or error(helpers.interpolate(MESSAGES.INVALID_SELF_OBJECT, { name = helpers.getMethodName, value = helpers.safetostring(t), type = type(t) }), 2) local classMt = helpers.createClassMt(self) local superCalled = false local done = false

setmetatable(created, classMt)

instanceRegistry[created] = classMt

-- Set class properties for k, v in pairs(self.__initProps) do		rawset(created, k, v)	end

-- set class metamethods for k, v in pairs(self.__classMetamethods) do		if contextSafeProperties[k] or k == '__eq' or k == '__metatable' then classMt[k] = v		elseif relationalOperators[k] then classMt[k] = v		end end

local i = 0 local len = #self.__parents

classMt.__constructorArgs = helpers.pack(...)

-- TODO: Add super indexing support if self.__parentStaticMt then local curParent = self.__parents[1]

local function superFunc(...) i = i + 1 local passedSelf = ({ ... })[1] if not instanceRegistry[passedSelf] then error(MESSAGES.INVAID_SELF_SUPER, 2) end

curParent = classRegistry[self.__parents[i]] or error(MESSAGES.INVALID_SUPER_CALL, 2) local constructor = curParent.__origConstructor local res = helpers.pack(constructor(...)); if (res.n == 0 or res.n == 1) and res[1] ~= passedSelf then return passedSelf elseif res.n > 1 then if res[1] == nil then res[1] = passedSelf end return unpack(res, 1, res.n)			else return passedSelf end end

-- .super is returned via `__index` classMt.__super = superFunc else classMt.__super = helpers.invalidSuperCall end local constructor = self.__origConstructor assert(constructor ~= staticMtFuncs.__call) local ret = helpers.pack(constructor(created, ...))

if self.__parentStaticMt and i ~= len then error(MESSAGES.MUST_CALL_SUPER, 3); end

if (ret.n == 0 or ret.n == 1) and ret[1] ~= created then return created elseif ret.n > 1 then if ret[1] ~= created then ret[1] = created end return unpack(ret, 1, ret.n)	else return created end end

function staticMtFuncs.__newindex(t, k, v)	local self = classRegistry[t] if reservedStaticProps[k] then error(helpers.interpolate(MESSAGES.STATIC_CLASS_FIELD_RESERVED, { property = k }), 2) end

-- If key is to Overwrite the prototype, clear any residual prototype keys and reset from new table if k == 'prototype' then if type(v) == 'table' then helpers.overwriteProto(self, v, true)

return self.__protoMt.__fakeTable else error(helpers.interpolate(MESSAGES.INVALID_PROTO_VALUE, { type = type(v), value = helpers.safetostring(v) }), 2) end end

local setter = self.__staticSetters[k] local __newindex = self.__staticMetamethods.__newindex

-- Search for setter in parent classes if self.__hasParent and setter == nil then local cur = self.__parentStaticMt local curSetters = cur.__staticSetters local curGetters = cur.__staticGetters local curProto = cur.__prototype

while cur do			local foundSetter = curSetters[k] local foundGetter = curGetters[k]

if foundSetter ~= nil or foundGetter ~= nil then setter = foundSetter getter = foundGetter break end

cur = cur.__parentStaticMt

if not cur then break end curProto = cur.__prototype curSetters = cur.__staticSetters end end

-- If getter but not setter exists raise exception if getter and not setter then error(helpers.interpolate(MESSAGES.GETTER_ONLY_STATIC_ASSIGNMENT, { property = k }), 3) elseif setter ~= nil then -- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks if _DEBUG then local res = helpers.pack(({ [" "] = setter })[" "](t, v)) if setter then return unpack(res, 1, res.n) end else return setter(t, v)		end else if __newindex then -- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks if _DEBUG then local res = helpers.pack(__newindex(t)) if self then return unpack(res, 1, res.n) end else return __newindex(t) end end

return rawset(t, k, v)	end end

function staticMtFuncs.__index(t, k)	if util[k] then return util[k] end

local self = classRegistry[t]

if self.__indexDefaults[k] then return self.__indexDefaults[k] end

local getter = self.__staticGetters[k] local value = rawget(t, k)	local __index = self.__staticMetamethods.__index

-- Search for getter/prototype property on parents if self.__hasParent and getter == nil and value == nil then local cur = self.__parentStaticMt local curGetters = cur.__staticGetters local curProto = cur.__prototype

while cur do			local foundGetter = curGetters[k] local foundValue = rawget(cur.__class, k)			if foundValue ~= nil then value = foundValue break elseif foundGetter ~= nil then getter = foundGetter break end

cur = cur.__parentStaticMt

if not cur then break end curProto = cur.__prototype curGetters = cur.__staticGetters end end

if getter ~= nil then -- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks if _DEBUG then local res = helpers.pack(({ [" "] = getter })[" "](t)) if getter then return unpack(res, 1, res.n) end else return getter(t) end else if __index then -- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks if _DEBUG then local res = helpers.pack(__index(t)) if self then return unpack(res, 1, res.n) end else return __index(t) end end

return value end end

function staticMtFuncs.__pairs(t) local self = classRegistry[t] local __pairs = self.__staticMetamethods.__pairs

if __pairs then -- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks if _DEBUG then local res = helpers.pack(__pairs(t)) if self then return unpack(res, 1, res.n) end else return __pairs(t) end end

local getters = helpers.tableKeys(self.__staticGetters) local i = 0 local onGetters = false local protoDone = false

return function(t, k1) local k, v

-- Insert prototype into results if k1 == 'prototype' and not protoDone then protoDone = true return 'prototype', self.__protoMt.__fakeTable end

if not onGetters then k, v = next(t, k1 ~= 'prototype' and k1 or nil) end

-- Insert getters into results if k == nil then onGetters = true i = i + 1

k, v = getters[i], t[getters[i]] end

if k == nil then return nil, nil else return k, v		end end, t, 'prototype' end

function staticMtFuncs.__ipairs(t) local self = classRegistry[t] local __ipairs = self.__staticMetamethods.__ipairs

if __ipairs then -- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks if _DEBUG then local res = helpers.pack(__ipairs(t)) if self then return unpack(res, 1, res.n) end else return __ipairs(t) end end

return helpers.ipairsFunc, t, 0 end

function staticMtFuncs.__tostring(t) local self = classRegistry[t] local __tostring = self.__staticMetamethods.__tostring

if __tostring then -- If in Debug mode, forbid tail call optimizations as that will obfuscate the function name in error stacks if _DEBUG then local res = helpers.pack(__tostring(t)) if self then return unpack(res, 1, res.n) end else return __tostring(t) end end

return "class" end

- -- Prototype metamethods - function protoMtFuncs.__pairs(t) local self = prototypeRegistry[t]

return function(t, k)		local nextKey, value = next(self.__prototype, k);

return nextKey, value end, self.__fakeTable, nil end

function protoMtFuncs.__ipairs(t) return helpers.ipairsFunc, prototypeRegistry[t], 0 end

function protoMtFuncs.__newindex(t, k, v)	local self = prototypeRegistry[t] local tp = type(v)

if reservedProtoProps[k] then error(helpers.interpolate(MESSAGES.PROTO_FIELD_RESERVED, { property = k })) end if k == 'constructor' and tp == 'function' then -- Overwrite constructor local oldConstructor = self.__staticMt.__origConstructor local newConstructor = v

self.__staticMt.__origConstructor = newConstructor or oldConstructor

return self.__fakeTable elseif k == 'constructor' and tp ~= 'function' then error(helpers.interpolate(MESSAGES.INVALID_CONSTRUCTOR_VALUE)) elseif tp == 'table' and next(v) ~= nil and k ~= '__metatable' then local err = helpers.parseAccessorTable(k, v, self.__staticMt.__classSetters, self.__staticMt.__classGetters, nil, false)

if err then error(err, 2) end return else if metaProperties[k] then self.__staticMt.__classMetamethods[k] = v		end end self.__prototype[k] = v	return self.__fakeTable end

function protoMtFuncs.__index(t, k)	local self = prototypeRegistry[t] if self.__indexDefaults[k] then return self.__indexDefaults[k] end

local value = self.__prototype[k] if value == nil then local parent = self.__parentMt while parent do			local foundValue = parent.__prototype[k]

if foundValue ~= nil then value = foundValue break end parent = parent.__parentMt end end return value end

function protoMtFuncs.__tostring return "class prototype" end

- -- Utilies, exposed on the static class - function util:checkSelf(value, name) if not classRegistry[self] then error(helpers.interpolate(MESSAGES.INVALID_STATIC_SELF_OBJECT, { name = helpers.getMethodName(-1), value = helpers.safetostring(self), type = type(self) }), 2) end

if not self:instanceof(value) then local methodName = name or helpers.getMethodName(1)

error(helpers.interpolate(MESSAGES.INVALID_SELF_OBJECT, { name = methodName, value = helpers.safetostring(value), type = type(value) }), 3) end

return self end

function util:checkSelfStatic(value, name) if not classRegistry[self] then error(helpers.interpolate(MESSAGES.INVALID_STATIC_SELF_OBJECT, { name = helpers.getMethodName(-1), value = helpers.safetostring(self), type = type(self) }), 2) end

if not self:isChildOrEqual(value) then local methodName = name or mw.text.split(debug.traceback, "\n")[3]:match("in function [\'\"](.-)[\'\"]")

error(helpers.interpolate(MESSAGES.INVALID_STATIC_SELF_OBJECT, { name = methodName, value = helpers.safetostring(value), type = type(value) }), 3) end

return self end

function util:isChildOrEqual(value) if not classRegistry[self] then error(helpers.interpolate(MESSAGES.INVALID_STATIC_SELF_OBJECT, { name = helpers.getMethodName(-1), value = helpers.safetostring(self), type = type(self) }), 2) end

local cr = classRegistry[value] if not cr then return false end if value == self then return true end local i = 1 local parents = cr.__parents local class = parents[i]

while class do		if class == self then return true end

i = i + 1 class = parents[i] end return false end

-- Check if value is an instance of class function util:instanceof(value) if not classRegistry[self] then error(helpers.interpolate(MESSAGES.INVALID_STATIC_SELF_OBJECT, { name = helpers.getMethodName(-1), value = helpers.safetostring(self), type = type(self) }), 2) end if not instanceRegistry[value] then return false end

local class = value.__class__ local parents = classRegistry[class].__parents

if class == self then return true end

local i = 1 local parent = parents[i]

while parent do		if parent == self then return true end

i = i + 1 parent = parents[i] end

return false end

-- Check types of method arguments function util:checkTypes(types, ...) if not classRegistry[self] then error(helpers.interpolate(MESSAGES.INVALID_STATIC_SELF_OBJECT, { name = helpers.getMethodName(-1), value = helpers.safetostring(self), type = type(self) }), 2) end

local max = #types local values = helpers.pack(...) local i = 1 local value = values[1] -- Rename error function to get proper name in stack local checkTypes = error

while i <= max do		local valueType = type(value) local targetType = types[i]

if i > values.n then checkTypes(helpers.interpolate(MESSAGES.INVALID_METHOD_ARG_TYPE, { name = helpers.getMethodName(1), type = "no value", pos = i, expected = helpers.getName(targetType) }), 3) end

if p.isClass(targetType) then if not targetType:instanceof(value) then checkTypes(helpers.interpolate(MESSAGES.INVALID_METHOD_ARG_TYPE, { name = helpers.getMethodName(1), pos = i, 					value = helpers.safetostring(value), type = valueType, expected = helpers.getName(targetType) }), 3)			end else if valueType ~= targetType then checkTypes(helpers.interpolate(MESSAGES.INVALID_METHOD_ARG_TYPE, { name = helpers.getMethodName(1), pos = i,					value = helpers.safetostring(value), type = helpers.getName(value), expected = targetType }), 3)			end end

i = i + 1 value = values[i] end

return ... end

- -- Package exports - function p.isClass(v) return classRegistry[v] ~= nil end

function p.isInstance(v) return instanceRegistry[v] ~= nil end

function p.isPrototype(v) return prototypeRegistry[v] ~= nil end

function p.makeClass(...) local args = { ... }	local name, data

if type(args[1]) == 'string' then data = args[2] name = args[1] else data = args[1] end

data = data or {}

if type(data) ~= 'table' then error(string.format('Argument #1 must be a table or a string (class name) or nil, recived %q instead', type(data)), 2) end if type(data) ~= 'table' then error(string.format('Argument #2 must be a table or nil, recived %q instead', type(data)), 2) end

local parentClass = data.parentClass or data.parent local constructor = data.constructor or (parentClass and helpers.defaultInheritedConstructor or helpers.defaultConstructor) local staticData = data.static or {} local metadata = data.__metadata or {} local classFields = data.class or {}

if type(staticData) ~= 'table' then error(helpers.interpolate(MESSAGES.STATIC_PROPERTIES_TYPE, { type = type(staticData) }), 2) end if type(classFields) ~= 'table' then error(helpers.interpolate(MESSAGES.PROPERTIES_TYPE, { type = type(classFields) }), 2) end

local staticMt = {}

local Class = {} local prototype = data.prototype or {} local initProps = {}

local setters, getters = {}, {}

classCount = classCount + 1

if not classRegistry[parentClass] and parentClass ~= nil then error(MESSAGES.INVALID_PARENT_CLASS, 2) end

-- Setup metadata metadata.name = name or metadata.name or "unnamed class " .. classCount metadata.id = classCount

-	-- Set up static metatable -	helpers.merge(staticMt, {		__prototype = prototype,		__class = Class,		__initProps = initProps,		__origConstructor = constructor,

__classSetters = setters, __classGetters = getters, __staticSetters = {}, __staticGetters = {}, __staticMetamethods = {},

__parentClass = parentClass, __parentStaticMt = parentClass and classRegistry[parentClass],

__isInstance = false, __hasParent = not not parentClass, __classMetamethods = {}, __parents = setmetatable({}, { __mode = "v" }), __childClasses = setmetatable({}, { __mode = "v" }), });

helpers.merge(staticMt, staticMtFuncs)

-	-- Set static/class methods -	for k, v in pairs(data) do		if k ~= 'constructor' and k ~= 'parent' and k ~= 'class' and k ~= 'prototype' and k ~= '__metadata' and k ~= 'static' then local tp = type(v) if tp == 'function' then rawset(prototype, k, v)				if metaProperties[k] then staticMt.__classMetamethods[k] = v				end elseif tp == 'table' then local len = #helpers.tableKeys(v)

if (len <= 2 and len > 0) and (v.set or v.get) then local err = helpers.parseAccessorTable(k, v, staticMt.__classSetters, staticMt.__classGetters, len, false) if err then error(err, 2) end else rawset(initProps, k, v)				end else rawset(initProps, k, v)			end end end

if next(classFields) ~= nil then for k, v in pairs(classFields) do			initProps[k] = v		end end

if next(staticData) ~= nil then for k, v in pairs(staticData) do			if reservedStaticProps[k] then error(helpers.interpolate(MESSAGES.STATIC_CLASS_FIELD_RESERVED, { property = k }), 2) end local tp = type(v)

if tp == 'table' then local len = #helpers.tableKeys(v) if (len > 0 and len <= 2) and (v.get or v.set) then local err = helpers.parseAccessorTable(k, v, staticMt.__staticSetters, staticMt.__staticGetters, len, true) if err then error(err, 2) end else rawset(Class, k, v)				end else rawset(Class, k, v)			end

if metaProperties[k] and tp == 'function' then if contextSafeProperties[k] or relationalOperators[k] or k == '__eq' or k == '__metatable' then staticMt[k] = v				end

staticMt.__staticMetamethods[k] = v			-- TODO: Add metatable property verification -- elseif metaProperties[k] then -- 	error("Metatable properties must be functions.") end end end

-	-- Set up prototype metatable -	local protoMt = {} local fakeProto = {}

helpers.merge(protoMt, {		__staticMt = staticMt,		__class = Class,		__prototype = staticMt.__prototype,		__parentMt = staticMt.__hasParent and staticMt.__parentStaticMt.__protoMt or nil,		__fakeTable = fakeProto,		__indexDefaults = setmetatable({ ['constructor'] = staticMtFuncs.__call, ['__setters__'] = staticMt.__classSetters, ['__getters__'] = staticMt.__classGetters, ['__proto__'] = staticMt.__hasParent and staticMt.__parentStaticMt.__protoMt.__fakeTable or nil, ['__holdingClass__'] = staticMt.__class, ['__parent__'] = staticMt.__hasParent and staticMt.__parentStaticMt.__class, }, { __mode = "v" }),	});

staticMt.__protoMt = protoMt staticMt.__metatable = { __isClass = true, __isInstance = false, __hasParent = staticMt.__hasParent, __class = Class, __parent = staticMt.__parentClass, }	helpers.overwriteProto(staticMt, prototype, false)

-	-- Set up final metadata for static metatable -	staticMt.__customMetadata = metadata staticMt.__indexDefaults = setmetatable({		['__parent__'] = staticMt.__parentClass,		['__proto__'] = staticMt.__parentClass,		['prototype'] = protoMt.__fakeTable,		['__setters__'] = staticMt.__staticSetters,		['__getters__'] = staticMt.__staticGetters,		['__children__'] = staticMt.__childClasses,		['__parents__'] = staticMt.__parents,		['__metadata__'] = staticMt.__customMetadata,		['constructor'] = staticMtFuncs.__call,		-- Utility methods		['childClass'] = helpers.createChildClass,	}, { __mode = "v" })

-- Set prototype table metamethods for k, v in pairs(protoMtFuncs) do		protoMt[k] = v	end prototypeRegistry[fakeProto] = protoMt

-- Merge parent initProps into target class and add child class to parent class list if parentClass then local parentMt = staticMt.__parentStaticMt

table.insert(classRegistry[parentClass].__childClasses, Class)

while parentMt do			tinsert(staticMt.__parents, parentMt.__class) helpers.merge(initProps, staticMt.__initProps)

parentMt = parentMt.__parentStaticMt end end

-- Finalize setup setmetatable(fakeProto, protoMt)

classRegistry[Class] = staticMt

setmetatable(Class, staticMt) return Class end - -- Debug functions (Only use for debugging!) - - -- Class instance debug functions - function debug.instanceExists(value) return not not instanceRegistry[helpers.assertIsInstance(value)] end

function debug.getInstanceMetatable(instance) return instanceRegistry[helpers.assertIsInstance(instance)] end

function debug.hasInstanceMetamethod(instance, method) return not not debug.getInstanceMetamethod(helpers.assertIsInstance(instance), method) end

function debug.getInstanceMetamethod(instance, method) return debug.getInstanceMetatable(helpers.assertIsInstance(instance)).__staticMt.__classMetamethods[method] end

function debug.getInstanceSetters(instance) return helpers.assertIsInstance(instance).__setters__ end

function debug.getInstanceGetters(instance) return helpers.assertIsInstance(instance).__getters__ end

- -- Class debug functions - function debug.classExists(value) return not not classRegistry[helpers.assertIsClass(value)] end

function debug.getClassMetatable(class) return classRegistry[helpers.assertIsClass(class)] end

function debug.hasClassMetamethod(class, method) return not not debug.getClassMetamethod(helpers.assertIsClass(class), method) end

function debug.getClassMetamethod(class, method) return debug.getClassMetatable(helpers.assertIsClass(class)).__staticMetamethods[method] end

function debug.getClassSetters(class) return helpers.assertIsClass(class).__setters__ end

function debug.getClassGetters(class) return helpers.assertIsClass(class).__getters__ end

function debug.getClassParents(value) return helpers.assertIsClass(value).__parent__ end

function debug.getClassName(class) return classRegistry[helpers.assertIsClass(class)].__customMetadata.name end

function debug.getClassId(class) return classRegistry[helpers.assertIsClass(class)].__customMetadata.id end

function debug.getClassByName(name) if type(name) ~= 'string' then error("Argument #1 must be a string", 2) end

for k, v in pairs(classRegistry) do		if v.__customMetadata.name == name then return k end end return nil end

- -- Class prototype functions - function debug.getClassOfPrototype(proto) return helpers.assertIsPrototype(proto).__holdingClass__ end

function debug.getPrototypeMetatable(proto) return prototypeRegistry[helpers.assertIsPrototype(proto)] end

function debug.getPrototypeTable(proto) return prototypeRegistry[helpers.assertIsPrototype(proto)].__prototype end

function debug.getPrototypeConstructor(proto) return prototypeRegistry[helpers.assertIsPrototype(proto)].__prototype.constructor end

- -- Class/Class instance functions - function debug.getParentOf(value) return helpers.assertIsInstanceOrClassOrProto(value).__parent__ end

function debug.getPrototypeOf(value) return helpers.assertIsInstanceOrClassOrProto(value).__proto__ end

function debug.getNameOf(value) helpers.assertIsInstanceOrClassOrProto(value) if p.isClass(value) then return classRegistry[value].__customMetadata.name elseif p.isPrototype(value) then return prototypeRegistry[value].__staticMt.__customMetadata.name elseif p.isInstance(value) then return instanceRegistry[value].__staticMt.__customMetadata.name end error("Invalid program state entered") end

return setmetatable(p, {	__call = function(_, ...) return p.makeClass(...) end, })