Module:MakeClass

From EarthMC
Jump to navigation Jump to search

Documentation for this module may be created at Module:MakeClass/doc

---------------------------------------------------------------------------------
--						[Deprecated] Module:MakeClass
--
-- This module is used to implement makeClass. Code was written by Thundercraft5.
-- Deprecated: Please use Module:Class instead.
--
----------------[ CONTENTS ]-----------------
-- The following list is all the functions this module houses.
--
-- * function: isStaticClass(t: table)
-- * function: isClass(t: table)
-- * function: makeClass(constructor?: function, methods: table, parentClass?: table, options?: table)
--
------[ CURRENT REMAINING DEPENDENCIES ]------
-- As this method of use is deprecated, here lists the remaining dependencies:
--
-- * Module:Loader
-- * Module:String
-- * Module:Constructor
-- * Module:Sword upgrades/Stats
--
---------------------------------------------------------------------------------

local table, lu = require('Module:Table'), require('Module:LibU')
local p = {}

---------------------------------------------------------------------------------
-- function: isStaticClass(t: table)
-- 
-- Checks if a class is static.
---------------------------------------------------------------------------------
function p.isStaticClass(t)
	local mt = getmetatable(t)
	return type(t) == 'table' and mt and mt.__isClass and not mt.__constructorCalled
end

---------------------------------------------------------------------------------
-- function: isClass(t: table)
--
-- Determines if the table is a class.
---------------------------------------------------------------------------------
function p.isClass(t)
	return type(t) == 'table' and (getmetatable(t) or {}).__isClass or false
end

---------------------------------------------------------------------------------
-- Base helper methods for .makeClass()
---------------------------------------------------------------------------------
local baseMethods = {}
local baseStaticMethods = {}

function baseMethods:instanceof(class)
	lu.checkType('instanceof', 1, class, { 'table' })
	lu.assertTrue(p.isClass(class), 'class expected', 2)
	lu.assertTrue(p.isStaticClass(class), 'the class must be static', 2)
	
	if not self.parent then
		return false
	end
	
	local tmp = self
	while tmp.parent do
		if tmp.parent.static == class then
			return true
		else
			tmp = tmp.parent
		end
	end
	
	return false
end

function baseStaticMethods:checkSelf(toCheck)
	if type(toCheck) ~= 'table' or toCheck.static.constructor ~= self.constructor then
		local method = getStackName()
		
		error(string.format(
			'%sinvalid self object. Did you call .%s() with a dot instead of a colon, i.e. ' ..
			'%s%s(...) instead of %s%s(...), or did you call the method directly from the prototype?',
			self.name and self.name .. ': ' or '', method, self.name and self.name .. '.' or '.', method, self.name and self.name .. ':' or ':', method
		 ), 3)
	end
end

function baseStaticMethods:extendPrototype(methods)
	lu.checkType('extendPrototype', 1, methods, { 'table' })
	
	table.merge(self.prototype, methods)
	return self
end

---------------------------------------------------------------------------------
-- function: makeClass(constructor?: function, methods: table, parentClass?: table, options?: table)
--
-- Creates a class table.
---------------------------------------------------------------------------------
function p.makeClass(constructor, methods, parentClass, options)
	lu.checkType('makeClass', 1, constructor, { 'function', 'table', 'nil' })
	lu.checkType('makeClass', 2, methods, { 'table', 'nil' })
	lu.checkType('makeClass', 3, parentClass, { 'table', 'nil' })
	lu.checkType('makeClass', 4, options, 'table', true)
	
	local options = options or {}
	local ignoreTableRet = unpack{
			options.ignoreTableRet or options.ignoreTable or options.ignore,
		}
	local origConstructor = constructor
		
	function tmpFunc(t1)
		return t1
	end
	
	function tmpChildFunc(t1, ...)
		t1.super(...)
		return t1
	end
	
	local get, set = 'get', 'set'
	local methodIsClass = p.isClass(methods)
	local constructorIsClass = p.isClass(constructor)
	local tpConstructor = type(constructor)
	
	if tpConstructor == 'table' and (methodIsClass or (methods == nil and parentClass == nil and table.length(options) == 0)) and not constructorIsClass then
		parentClass = methods
		methods = constructor
		origConstructor, constructor = nil, nil
	elseif constructorIsClass then
		parentClass = constructor
		origConstructor, constructor = nil, nil
	elseif tpConstructor == 'table' and type(methods) == 'table' then
		options = methods
		methods = nil
		parentClass = nil
	elseif tpConstructor == 'function' and methodIsClass then
		parentClass = methods
		methods = nil
	end
	methods = methods ~= nil and methods or {}
	
	if methods.constructor then
		constructor = methods.constructor
	elseif not methods.constructor and type(origConstructor) == 'table' then
		constructor = parentClass and tmpChildFunc or tmpFunc
	end
	
	local util = {}
	local t2 = {}
	local staticReservedNames = table.sequenceToSet{
		'parent',
		'methods',
		'super',
		'__constructorCalled',
		'__isClass',
		'prototype',
		'name',
	}
	
	local reservedNames = table.sequenceToSet{
		'parent',
		'methods',
		'static',
		'__proto__',
		'super',
		'__constructorCalled',
		'__isClass',
		'name',
	}
	
	local allowedTypes = table.sequenceToSet{
		'static',
	}
	
	if constructor == nil then
		constructor = parentClass and tmpChildFunc or tmpFunc
	end
	
	function util:methodType(f, searchType)
		if searchType then
			searchType = searchType:lower()
		end
		
		if type(f) == 'function' then return 'function' end
		if type(f) ~= 'table' then return false end
		if type(f[2]) ~= 'function' then return false end
		
		local f1 = f[1]
		local tpF1 = type(f1)
		
		if tpF1 == 'table' then
			if #f1 == 1 then
				if searchType then
					return f1[1] == searchType
					and searchType
					or false
				else
					return f1[1]
				end
			else
				if searchType then
					return f1[1] == searchType
					and searchType
					or f1[2] == searchType
					and searchType
					or false
				else
					return unpack(f1)
				end
			end
		elseif tpF1 == 'string' and searchType then
			return f1 == searchType
			and searchType 
			or false
		else
			return f1
		end
	end
	
	function util:filterMethods(t, _type, invert)
		local ret = {}
		for k, v in pairs(t) do
			local tmp = util:methodType(v, _type) == _type
			
			local compare
			if invert then
				compare = not tmp
			else
				compare = tmp
			end
			
			if compare then
				ret[k] = v
			end
		end
		return ret
	end
	
	function util:new(...)
		local methods = util:filterMethods(methods, 'static', true)
		
		local mt = {
			methods = methods,
			parent = parentClass,
			static = t2,
			name = options.name,
			__isClass = true,
		}
		
		for k, v in pairs(t2.prototype) do
			methods[k] = v
		end
		
		local t1 = {}
		local metatableMethods = {}
		
		local nonFuncMethods = util:filterMethods(methods, false)
		for k, v in pairs(methods) do
			if not reservedNames[k] then
				t1[k] = nonFuncMethods[k]
			end
		end
			
		if parentClass and (getmetatable(parentClass) or {}).__constructorCalled == true then
			for k, v in pairs(parentClass) do
				if not reservedNames[k] then
					t1[k] = v
				end
			end
		end
		
		mt.__proto__ = mt.methods
		mt.super = function(...)
			if mt.parent == nil then
				error('the class must have a parent when calling the super constructor', 2)
			end
			for k, v in pairs(mt.parent) do
				if not reservedNames[k] then
					t1[k] = v
				end
			end
			
			mt.parent = mt.parent:constructor(...)
			return mt.parent
		end
		
		mt.methods.getmetatable = getmetatable
		table.merge(mt.methods, baseMethods)
		
		mt.__index = function(_, k)
			local value
			
			if reservedNames[k] then
				return mt[k]
			elseif reservedNames[k] and (k == '__isClass' or k == '__constructorCalled') then
				return nil
			end
			
			if parentClass then
				value = mt.methods[k] or (mt.parent.methods or {})[k]
			else
				value = mt.methods[k]
			end
			
			return value
		end
		
		mt.__newindex = function(_, k, v)
			local mType, mType2 = util:methodType(v)
			
			if metatableMethods[k] then
				rawset(mt.methods, k, metatableMethods[k])
				rawset(mt, k, metatableMethods[k])
				return mt.methods[k]
			elseif reservedNames[k] then
				formattedError('class property %q is readonly', 2, k)
			elseif mType == 'static' or mType2 == 'static' then
				return rawset(mt.static, k, v[2])
			else
				return rawset(t1, k, v)
			end
		end
		
		local function methodExists(index)
			local ind = mt.methods[index] or (mt.parent and mt.parent.methods or {})[index]
			if ind then
				return true
			end
		end
		
		metatableMethods.__tostring = function() return t1:__tostring(t1) end
		metatableMethods.__concat = function(a, b) return t1:__concat(a, b) end
		metatableMethods.__call = t1.__call
		metatableMethods.__pairs = function() return t1:__pairs(t1, t1.methods, t1.static) end
		metatableMethods.__ipairs = function() return t1:__ipairs(t1, t1.methods, t1.static) end
		metatableMethods.__add = function(a, b) return t1:__add(a, b) end
		metatableMethods.__sub = function(a, b) return t1:__sub(a, b) end
		metatableMethods.__mul = function(a, b) return t1:__mul(a, b) end
		metatableMethods.__div = function(a, b) return t1:__div(a, b) end
		metatableMethods.__mod = function(a, b) return t1:__mod(a, b) end
		metatableMethods.__pow = function(a, b) return t1:__pow(a, b) end
		metatableMethods.__unm = function(a, b) return t1:__unm(a, b) end
		
		-- Custom metamethods
		metatableMethods.__tonumber = function(a) return t1:__tonumber(a) end
		metatableMethods.__type = function(a, b) return t1:__type(a, b) end
		metatableMethods.__unpack = function(a, b) return t1:__type(a, b) end
		metatableMethods.__toboolean = function(a) return t1:__toboolean(a, b) end
		metatableMethods.__totable = function(a, b, c, d) return t1:__totable(a, b, c, d) end
		metatableMethods.__tconcat = function(a, b, c, d) return t1:__tconcat(a, b, c, d) end
		
		for k, v in pairs(metatableMethods) do
			if methodExists(k) then
				mt[k] = v
			elseif k == '__tostring' and t2.name then
				mt[k] = function(self)
					return '[class ' .. name .. ']'
				end
			end
		end
		
		mt.__metatable = mt 
		setmetatable(t1, mt)
		
		local constructorValue = constructor(t1, ...) 
		
		if t1.parent and p.isStaticClass(t1.parent) then
			error('makeClass(): parent class constructor must be called if the child constructor was called', 2)
		end
		mt.__constructorCalled = true
		
		local ret
		
		if ignoreTableRet or type(constuctorValue) == 'table' then
			ret = constructorValue
		else
			ret = t1
		end
		
		if ret == nil then
			ret = t1
		end
		
		return ret
	end
	
	local metatableMethods, mt = {}, {}
	mt.parent = (parentClass or {}).static
	mt.name = options.name
	mt.methods = util:filterMethods(methods, 'static') or {}
	mt.prototype = table.merge(util:filterMethods(methods, 'static', true), (parentClass or {}).prototype or {}) or {}
	mt.methods.constructor, mt.prototype.constructor = util.new, util.new
	
	table.merge(mt.methods, baseStaticMethods)
	
	mt.__isClass = true
	mt.__constructorCalled = false
	
	mt.__call = util.new
	mt.__index = function(_, k)
		local value
		if mt.parent ~= nil then
			value = mt.methods[k] or mt.parent[k] or (mt.parent.methods or {})[k]
		else
			value = mt.methods[k]
		end
		
		local mType, mType2 = util:methodType(value)
		
		if staticReservedNames[k] then
			return mt[k]
		elseif mType == 'static' or mType == 'static' then
			return value[2]
		else
			return value
		end
	end
	
	mt.__newindex = function(_, k, v)
		local function mType(type)
			return util:methodType(v, type)
		end
		
		if metatableMethods[k] then
			rawset(mt.methods, k, v)
			rawset(mt, k, metatableMethods[k])
		elseif staticReservedNames[k] then
			formattedError('class property %q is readonly', 2, k)
		elseif type(v) == 'function' then
			rawset(mt.methods, k, v)
		elseif util:methodType(v, 'static') then
			rawset(mt, k, v[2])
		else
			rawset(t2, k, v)
		end
		
		return t2[k]
	end
	
	local function methodExists(index)
		local ind = (mt.methods or {})[index] or ((mt.parent or {}).methods or {})[index]
		if ind then
			return true
		end
	end
	
	metatableMethods.__tostring = function() return t2:__tostring(t2) end
	metatableMethods.__concat = function(a, b) return t2:__concat(a, b) end
	metatableMethods.__pairs = function() return t2:__pairs(t2, t2.methods) end
	metatableMethods.__ipairs = function() return t2:__ipairs(t2, t2.methods) end
	metatableMethods.__add = function(a, b) return t2:__add(a, b) end
	metatableMethods.__sub = function(a, b) return t2:__sub(a, b) end
	metatableMethods.__mul = function(a, b) return t2:__mul(a, b) end
	metatableMethods.__div = function(a, b) return t2:__div(a, b) end
	metatableMethods.__mod = function(a, b) return t2:__mod(a, b) end
	metatableMethods.__pow = function(a, b) return t2:__pow(a, b) end
	metatableMethods.__unm = function(a, b) return t2:__unm(a, b) end
	
	-- Custom metamethods
	metatableMethods.__tonumber = function(a) return t2:__tonumber(a) end
	metatableMethods.__type = function(a, b) return t2:__type(a, b) end
	metatableMethods.__unpack = function(a, b) return t2:__type(a, b) end
	metatableMethods.__toboolean = function(a) return t2:__toboolean(a, b) end
	metatableMethods.__totable = function(a, b, c, d) return t2:__totable(a, b, c, d) end
	metatableMethods.__tconcat = function(a, b, c, d) return t2:__tconcat(a, b, c, d) end
	
	for k, v in pairs(metatableMethods) do
		if methodExists(k) then
			mt[k] = v
		end
	end
	
	mt.__metatable = {
		__isClass = true,
		__constructorCalled = false,
	}
	
	return setmetatable(t2, mt)
end

function p.test(...)
	local Class = p.makeClass{ '$', constructor = function() error() end }
	
	Class.prototype.test = function()
		return '$'
	end
	
	Class.prototype.__tostring = function()
		error()
	end
	
	return Class
end

return p