TradeSkillMaster/External/LibTSMClass/LibTSMClass.lua

376 lines
11 KiB
Lua
Raw Permalink Normal View History

2020-11-13 14:13:12 -05:00
--- 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