--- Provides a reuseable and convenient framework for creating classes in Lua. -- Two possible notations: -- -- B = class(A) -- class.B(A) -- -- The latter form creates a named class within the current environment. Note -- that this implicitly brings in `pl.utils` as a dependency. -- -- See the Guide for further @{01-introduction.md.Simplifying_Object_Oriented_Programming_in_Lua|discussion} -- @module pl.class local error, getmetatable, io, pairs, rawget, rawset, setmetatable, tostring, type = _G.error, _G.getmetatable, _G.io, _G.pairs, _G.rawget, _G.rawset, _G.setmetatable, _G.tostring, _G.type local compat -- this trickery is necessary to prevent the inheritance of 'super' and -- the resulting recursive call problems. local function call_ctor(c, obj, ...) local init = rawget(c, '_init') local parent_with_init = rawget(c, '_parent_with_init') if parent_with_init then if not init then -- inheriting an init init = rawget(parent_with_init, '_init') parent_with_init = rawget(parent_with_init, '_parent_with_init') end if parent_with_init then -- super() points to one above whereever _init came from rawset(obj, 'super', function(loc_obj, ...) call_ctor(parent_with_init, loc_obj, ...) end) end else -- Without this, calling super() where none exists will sometimes loop and stack overflow rawset(obj, 'super', nil) end local res = init(obj, ...) if parent_with_init then -- If this execution of call_ctor set a super, unset it rawset(obj, 'super', nil) end return res end --- initializes an __instance__ upon creation. -- @function class:_init -- @param ... parameters passed to the constructor -- @usage local Cat = class() -- function Cat:_init(name) -- --self:super(name) -- call the ancestor initializer if needed -- self.name = name -- end -- -- local pussycat = Cat("pussycat") -- print(pussycat.name) --> pussycat --- checks whether an __instance__ is derived from some class. -- Works the other way around as `class_of`. It has two ways of using; -- 1) call with a class to check against, 2) call without params. -- @function instance:is_a -- @param some_class class to check against, or `nil` to return the class -- @return `true` if `instance` is derived from `some_class`, or if `some_class == nil` then -- it returns the class table of the instance -- @usage local pussycat = Lion() -- assuming Lion derives from Cat -- if pussycat:is_a(Cat) then -- -- it's true, it is a Lion, but also a Cat -- end -- -- if pussycat:is_a() == Lion then -- -- It's true -- end local function is_a(self, klass) if klass == nil then -- no class provided, so return the class this instance is derived from return getmetatable(self) end local m = getmetatable(self) if not m then return false end --*can't be an object! while m do if m == klass then return true end m = rawget(m, '_base') end return false end --- checks whether an __instance__ is derived from some class. -- Works the other way around as `is_a`. -- @function some_class:class_of -- @param some_instance instance to check against -- @return `true` if `some_instance` is derived from `some_class` -- @usage local pussycat = Lion() -- assuming Lion derives from Cat -- if Cat:class_of(pussycat) then -- -- it's true -- end local function class_of(klass, obj) if type(klass) ~= 'table' or not rawget(klass, 'is_a') then return false end return klass.is_a(obj, klass) end --- cast an object to another class. -- It is not clever (or safe!) so use carefully. -- @param some_instance the object to be changed -- @function some_class:cast local function cast(klass, obj) return setmetatable(obj, klass) end local function _class_tostring(obj) local mt = obj._class local name = rawget(mt, '_name') setmetatable(obj, nil) local str = tostring(obj) setmetatable(obj, mt) if name then str = name .. str:gsub('table', '') end return str end local function tupdate(td, ts, dont_override) for k, v in pairs(ts) do if not dont_override or td[k] == nil then td[k] = v end end end local function _class(base, c_arg, c) -- the class `c` will be the metatable for all its objects, -- and they will look up their methods in it. local mt = {} -- a metatable for the class to support __call and _handler -- can define class by passing it a plain table of methods local plain = type(base) == 'table' and not getmetatable(base) if plain then c = base base = c._base else c = c or {} end if type(base) == 'table' then -- our new class is a shallow copy of the base class! -- but be careful not to wipe out any methods we have been given at this point! tupdate(c, base, plain) c._base = base -- inherit the 'not found' handler, if present if rawget(c, '_handler') then mt.__index = c._handler end elseif base ~= nil then error('must derive from a table type', 3) end c.__index = c setmetatable(c, mt) if not plain then if base and rawget(base, '_init') then c._parent_with_init = base end -- For super and inherited init c._init = nil end if base and rawget(base, '_class_init') then base._class_init(c, c_arg) end -- expose a ctor which can be called by () mt.__call = function(_class_tbl, ...) local obj if rawget(c, '_create') then obj = c._create(...) end if not obj then obj = {} end setmetatable(obj, c) if rawget(c, '_init') or rawget(c, '_parent_with_init') then -- constructor exists local res = call_ctor(c, obj, ...) if res then -- _if_ a ctor returns a value, it becomes the object... obj = res setmetatable(obj, c) end end if base and rawget(base, '_post_init') then base._post_init(obj) end return obj end -- Call Class.catch to set a handler for methods/properties not found in the class! c.catch = function(self, handler) if type(self) == 'function' then -- called using . instead of : handler = self end c._handler = handler mt.__index = handler end c.is_a = is_a c.class_of = class_of c.cast = cast c._class = c if not rawget(c, '__tostring') then c.__tostring = _class_tostring end return c end --- create a new class, derived from a given base class. -- Supporting two class creation syntaxes: -- either `Name = class(base)` or `class.Name(base)`. -- The first form returns the class directly and does not set its `_name`. -- The second form creates a variable `Name` in the current environment set -- to the class, and also sets `_name`. -- @function class -- @param base optional base class -- @param c_arg optional parameter to class constructor -- @param c optional table to be used as class local class class = setmetatable({}, { __call = function(_fun, ...) return _class(...) end, __index = function(_tbl, key) if key == 'class' then io.stderr:write('require("pl.class").class is deprecated. Use require("pl.class")\n') return class end compat = compat or require('pl.compat') local env = compat.getfenv(2) return function(...) local c = _class(...) c._name = key rawset(env, key, c) return c end end, }) class.properties = class() function class.properties._class_init(klass) klass.__index = function(t, key) -- normal class lookup! local v = klass[key] if v then return v end -- is it a getter? v = rawget(klass, 'get_' .. key) if v then return v(t) end -- is it a field? return rawget(t, '_' .. key) end klass.__newindex = function(t, key, value) -- if there's a setter, use that, otherwise directly set table local p = 'set_' .. key local setter = klass[p] if setter then setter(t, value) else rawset(t, key, value) end end end return class