--- LibTSMClass Library -- Allows for OOP in lua through the implementation of classes. Many features of proper classes are supported including -- inhertiance, polymorphism, and virtual methods. -- @author TradeSkillMaster Team (admin@tradeskillmaster.com) -- @license MIT -- @module LibTSMClass local Lib = {} local private = { classInfo = {}, instInfo = {}, constructTbl = nil } -- Set the keys as weak so that instances of classes can be GC'd (classes are never GC'd) setmetatable(private.instInfo, { __mode = "k" }) local SPECIAL_PROPERTIES = { __init = true, __tostring = true, __dump = true, __class = true, __isa = true, __super = true, __name = true, __as = true, } local RESERVED_KEYS = { __super = true, __isa = true, __class = true, __name = true, __as = true, } local DEFAULT_INST_FIELDS = { __init = function(self) -- do nothing end, __tostring = function(self) return private.instInfo[self].str end, __dump = function(self) return private.InstDump(self) end, } -- ============================================================================ -- Public Library Functions -- ============================================================================ function Lib.DefineClass(name, superclass, ...) if type(name) ~= "string" then error("Invalid class name: "..tostring(name), 2) end local abstract = false for i = 1, select('#', ...) do local modifier = select(i, ...) if modifier == "ABSTRACT" then abstract = true else error("Invalid modifier: "..tostring(modifier), 2) end end local class = setmetatable({}, private.CLASS_MT) private.classInfo[class] = { name = name, static = {}, superStatic = {}, superclass = superclass, abstract = abstract, isStaticReference = false, } while superclass do for key, value in pairs(private.classInfo[superclass].static) do if not private.classInfo[class].superStatic[key] then private.classInfo[class].superStatic[key] = { class = superclass, value = value } end end private.classInfo[superclass].subclassed = true superclass = superclass.__super end return class end function Lib.ConstructWithTable(tbl, class, ...) private.constructTbl = tbl local inst = class(...) assert(not private.constructTbl and inst == tbl, "Internal error!") return inst end -- ============================================================================ -- Instance Metatable -- ============================================================================ private.INST_MT = { __newindex = function(self, key, value) if RESERVED_KEYS[key] then error("Can't set reserved key: "..tostring(key), 2) end if private.classInfo[self.__class].static[key] ~= nil then private.classInfo[self.__class].static[key] = value elseif not private.instInfo[self].hasSuperclass then -- we just set this directly on the instance table for better performance rawset(self, key, value) else private.instInfo[self].fields[key] = value end end, __index = function(self, key) -- This method is super optimized since it's used for every class instance access, meaning function calls and -- table lookup is kept to an absolute minimum, at the expense of readability and code reuse. local instInfo = private.instInfo[self] -- check if this key is an instance field first, since this is the most common case local res = instInfo.fields[key] if res ~= nil then instInfo.currentClass = nil return res end -- check if it's the special __super field or __as method if key == "__super" then if not instInfo.hasSuperclass then error("The class of this instance has no superclass.", 2) end -- The class of the current class method we are in, or nil if we're not in a class method. local methodClass = instInfo.methodClass -- We can only access the superclass within a class method and will use the class which defined that method -- as the base class to jump to the superclass of, regardless of what class the instance actually is. if not methodClass then error("The superclass can only be referenced within a class method.", 2) end return private.InstAs(self, private.classInfo[instInfo.currentClass or methodClass].superclass) elseif key == "__as" then return private.InstAs end -- reset the current class since we're not continuing the __super chain local class = instInfo.currentClass or instInfo.class instInfo.currentClass = nil -- check if this is a static key local classInfo = private.classInfo[class] res = classInfo.static[key] if res ~= nil then return res end -- check if it's a static field in the superclass local superStaticRes = classInfo.superStatic[key] if superStaticRes then res = superStaticRes.value return res end -- check if this field has a default value res = DEFAULT_INST_FIELDS[key] if res ~= nil then return res end return nil end, __tostring = function(self) return self:__tostring() end, __metatable = false, } -- ============================================================================ -- Class Metatable -- ============================================================================ private.CLASS_MT = { __newindex = function(self, key, value) local classInfo = private.classInfo[self] if classInfo.subclassed then error("Can't modify classes after they are subclassed", 2) end if classInfo.static[key] then error("Can't modify or override static members", 2) end if RESERVED_KEYS[key] then error("Reserved word: "..tostring(key), 2) end local isMethod = type(value) == "function" if classInfo.isStaticReference then -- we are defining a static class function, not a class method assert(isMethod) classInfo.isStaticReference = false isMethod = false end if isMethod then -- We wrap class methods so that within them, the instance appears to be of the defining class classInfo.static[key] = function(inst, ...) local instInfo = private.instInfo[inst] if not instInfo.isClassLookup[self] then error(format("Attempt to call class method on non-object (%s)!", tostring(inst)), 2) end if not instInfo.hasSuperclass then -- don't need to worry about methodClass so just call the function directly return value(inst, ...) else local prevMethodClass = instInfo.methodClass instInfo.methodClass = self return private.InstMethodReturnHelper(prevMethodClass, instInfo, value(inst, ...)) end end else classInfo.static[key] = value end end, __index = function(self, key) local classInfo = private.classInfo[self] assert(not classInfo.isStaticReference) -- check if it's the special __isa method which all classes implicitly have if key == "__isa" then return private.ClassIsA elseif key == "__name" then return classInfo.name elseif key == "__super" then return classInfo.superclass elseif key == "__static" then classInfo.isStaticReference = true return self elseif classInfo.static[key] ~= nil then return classInfo.static[key] end error(format("Invalid static class key (%s)", tostring(key)), 2) end, __tostring = function(self) return "class:"..private.classInfo[self].name end, __call = function(self, ...) if private.classInfo[self].abstract then error("Attempting to instantiate an abstract class!", 2) end -- Create a new instance of this class local inst = private.constructTbl or {} local instStr = strmatch(tostring(inst), "table:[^0-9a-fA-F]*([0-9a-fA-F]+)") setmetatable(inst, private.INST_MT) local hasSuperclass = private.classInfo[self].superclass and true or false private.instInfo[inst] = { class = self, fields = { __class = self, __isa = private.InstIsA, }, str = private.classInfo[self].name..":"..instStr, isClassLookup = {}, hasSuperclass = hasSuperclass, currentClass = nil, } if not hasSuperclass then -- set the static members directly on this object for better performance for key, value in pairs(private.classInfo[self].static) do if not SPECIAL_PROPERTIES[key] then rawset(inst, key, value) end end end local c = self while c do private.instInfo[inst].isClassLookup[c] = true c = private.classInfo[c].superclass end if private.constructTbl then -- re-set all the object attributes through the proper metamethod for k, v in pairs(inst) do rawset(inst, k, nil) inst[k] = v end private.constructTbl = nil end if select("#", inst:__init(...)) > 0 then error("__init must not return any values", 2) end return inst end, __metatable = false, } -- ============================================================================ -- Helper Functions -- ============================================================================ function private.InstMethodReturnHelper(class, instInfo, ...) -- reset methodClass now that the function returned instInfo.methodClass = class return ... end function private.InstIsA(inst, targetClass) return private.instInfo[inst].isClassLookup[targetClass] end function private.InstAs(inst, targetClass) local instInfo = private.instInfo[inst] instInfo.currentClass = targetClass if not targetClass then error(format("Requested class does not exist!"), 2) elseif not instInfo.isClassLookup[targetClass] then error(format("Object is not an instance of the requested class (%s)!", tostring(targetClass)), 2) end -- For classes with no superclass, we don't go through the __index metamethod, so can't use __as if not instInfo.hasSuperclass then error("The class of this instance has no superclass.", 2) end -- We can only access the superclass within a class method. if not instInfo.methodClass then error("The superclass can only be referenced within a class method.", 2) end return inst end function private.ClassIsA(class, targetClass) while class do if class == targetClass then return true end class = class.__super end end function private.InstDump(inst) local instInfo = private.instInfo[inst] local tbl = instInfo.hasSuperclass and instInfo.fields or inst print(instInfo.str.." {") for key, value in pairs(tbl) do local valueStr = nil if type(value) == "table" then if private.classInfo[value] or private.instInfo[value] then -- this is a class or instance of a class valueStr = tostring(value) elseif next(value) then valueStr = "{ ... }" else valueStr = "{}" end elseif type(value) == "string" or type(value) == "number" or type(value) == "boolean" then valueStr = tostring(value) end if valueStr then print(format(" |cff88ccff%s|r=%s", tostring(key), valueStr)) end end print("}") end -- ============================================================================ -- Initialization Code -- ============================================================================ do -- register with LibStub local libStubTbl = LibStub:NewLibrary("LibTSMClass", 1) if libStubTbl then for k, v in pairs(Lib) do libStubTbl[k] = v end end -- register with TSM local addonName, addonTable = ... if addonName == "TradeSkillMaster" then local tsmModuleTbl = addonTable.Init("LibTSMClass") for k, v in pairs(Lib) do tsmModuleTbl[k] = v end end end