FDK - Class and Package based system

FDK is a package management class-based system for better code organisation. FDK is nearly to the first release and we wanted to see if there’s any changes we should make to improve FDK before we release it. We’re working on a dependency system, plugin for installing, and a logging package that is nearly finished. Any ideas or improvements would be helpful! (https://github.com/TheFlamingBlaster/FDK)
(@froghopperjacob (me) and @TheFlamingBlaster)
BaseClass:

--[[
	BaseClass module, used as a root for all other classes. Internally indexed as BaseClass
	Licenced under the terms at: https://www.apache.org/licenses/LICENSE-2.0.txt
--]]

local BaseClass = { }
local internal = { }

internal.classes = { }


--[[
	Function: Checks the object against any types given. cleans code up
	Arguments: object - check against, ... - types
]]
local function checkTypes(object, ...)
	for _, check in pairs({ ... }) do
		if (typeof(object) == check) then
			return true
		end
	end

	return false
end

--[[
	Function: Tries to get the metamethod of a metatable
	Arguments: object - class or any table /shrug, try - what is being tried
]]
local function tryGetMetatable(object, try)
	local _, got = pcall(function()
		return getmetatable(object)[try]
	end)

	return got
end

--[[
	Function: Creates a proxy interface for locked classes
	Arguments: object - class or any table /shrug
]]
local function createProxyInterface(object)
	local cachedObject = { } -- should cached be a weak table to prevent garbage collection?

	return setmetatable({ }, {
		__index = function(self, index)
			if (typeof(object[index]) == "table") then
				if (cachedObject[index] == nil) then
					cachedObject[index] = createProxyInterface(object[index])
				end

				return cachedObject[index]
			end

			return object[index]
		end,

		__newindex = function(self, index, value)
			return error("[BaseClass - LOCKED] This table is locked")
		end,

		__call = function(self, ...)
			return object(...):lock()
		end,

		__tostring = function(self)
			return tostring(object)
		end,

		--Class Metamethods
		__concact = tryGetMetatable(object, "__concat"),
		__unm = tryGetMetatable(object, "__unm"),
		__add = tryGetMetatable(object, "__add"),
		__sub = tryGetMetatable(object, "__sub"),
		__mul = tryGetMetatable(object, "__mul"),
		__div = tryGetMetatable(object, "__div"),
		__mod = tryGetMetatable(object, "__mod"),
		__pow = tryGetMetatable(object, "__pow"),
		__eq = tryGetMetatable(object, "__eq"),
		__lt = tryGetMetatable(object, "__lt"),
		__le = tryGetMetatable(object, "__le"),
		__gc = tryGetMetatable(object, "__gc"),
		__len = tryGetMetatable(object, "__len"),
		__mode = tryGetMetatable(object, "__mode"),
		--Class Metamethods

		__metatable = "B-Locked."
	})
end

--[[
	Function: Checks if the classes are the exact same
	Arguments: self - the class, value - the other class
]]
internal.__eq = function(self, value)
	return internal.classes[value] and self:isA(value["className"]) or false
end

--[[
	Function: When the init function is called this is called and returns an extended class
	Arguments: self - the class, ... - values supplied by the user
]]
internal.__call = function(self, ...)
	local className = self.className
	local initFunction = self[className]

	if (initFunction and checkTypes(initFunction, "function")) then
		return initFunction(self:Extend(className), ...)
	end

	return error("[BaseClass - CONSTRUCTOR]: No constructor function found for class " .. className)
end

local external = createProxyInterface(BaseClass)

--[[
	Function: Creates a new class from the class name
	Arguments: self - the module, className - the class name
]]
BaseClass.new = function(self, className)
	if (not checkTypes(className, "string") or string.len(className) == 0) then
		return error("[FDK - CLASS INITIALISATION]: Expected string, got " .. className .. ".")
	end

	local newClass, classProperties = { }, {
		["className"] = className,
		["inherits"] = {
			["BaseClass"] = external
		}
	}

	local externalClassProperties, classString =
		createProxyInterface(classProperties),
		className .. ": " ..tostring(newClass):sub(7)

	internal.classes[newClass] = classProperties
	newClass.__properties = externalClassProperties

	return setmetatable(newClass, {
		__index = function(self, index)
			if (rawget(newClass, index)) then
				return rawget(newClass, index)
			end

			if (classProperties[index]) then
				return externalClassProperties[index]
			end

			if (BaseClass[index]) then
				return BaseClass[index]
			end
		end,

		__newindex = function(self, index, value)
			if (string.sub(index, 1, string.len("__")) == "__" and checkTypes(value, "function", "string")) then
				getmetatable(self)[index] = value
			end

			return rawset(self, index, value)
		end,


		__tostring = function(self)
			return classString
		end,

		__eq = internal.__eq,

		__call = internal.__call
	})
end

--[[
	Function: Creates a new class and allows access to the inherited class
	Arguments: extender - the original class, className - the new class name
]]
BaseClass.extend = function(extender, className)
	local newClass, properties =
		BaseClass:New(className), internal.classes[extender]

	internal.classes[newClass].inherits = properties.inherits
	internal.classes[newClass].inherits[properties.className] = extender

	getmetatable(newClass).__index = function(self, k)
		if (rawget(newClass, k)) then
			return rawget(newClass, k)
		end

		if (extender[k]) then
			return extender[k]
		end

		if (BaseClass[k]) then
			return BaseClass[k]
		end
	end

	return newClass
end

--[[
	Function: Returns a locked class
	Arguments: self - the class
]]
BaseClass.lock = function(self)
	return createProxyInterface(self)
end

--[[
	Function: Tells if a class is inheritied by antoher
	Arguments: self - the class, className - the other class
]]
BaseClass.isA = function(self, className)
	return internal.classes[self].inherits[className] ~= nil
end

--[[
	Function: Tells if a class is registered in internals
	Arguments: self - this class, class - checking another class
	Explain: If you provide the argument class it will check that instead of this class, self
]]
BaseClass.registered = function(self, class)
	return internal.classes[class ~= nil and class or self] ~= nil
end

--Legacy Support
BaseClass.Registered = BaseClass.registered
BaseClass.IsA = BaseClass.isA
BaseClass.Lock = BaseClass.lock
BaseClass.Extend = BaseClass.extend
BaseClass.New = BaseClass.new
--Legacy Support

return external

FDK:

--[[
	Main module for FDK package management.
	Licenced under the terms at: https://www.apache.org/licenses/LICENSE-2.0.txt
--]]
local BaseClass
local packages

-- This is here checking if its Lemur and where the packages/baseclass are
if (__LEMUR__ == nil) then
	packages = (game:GetService("RunService"):IsClient() and
		game:GetService("ReplicatedStorage"):FindFirstChild("ClientPackages") or
		game:GetService("ServerScriptService"):FindFirstChild("ServerPackages"))

	BaseClass = require(script.BaseClass)
else
	packages = script.Parent.Parent.tests
	BaseClass = require(script.Parent.BaseClass)
end

local FDK = BaseClass:new("Flaming Development Toolkit")
local external = FDK:lock()

--[[
	Function: Checks the object against any types given. cleans code up
	Arguments: object - check against, ... - types
]]
local function checkTypes(object, ...)
	local typeOfObject = typeof(object)

	for _, check in pairs({ ... }) do
		if (typeOfObject == check) then
			return true
		end
	end

	return false
end

--[[
	Function: Creates a new class from the class name
	Arguments: self - FDK, importString - what is wanted to be imported
]]
FDK.import = function(self, importString)
	if (not checkTypes(importString, "string") or string.len(importString) == 0) then
		return error("[FDK - PACKAGE MANAGER] Expected string, got "..typeof(importString))
	end

	if (importString == "FDK") then
		return external
	elseif (importString == "BaseClass" or importString == "Class") then
		return BaseClass
	end

	local currentIndex, splitImportString, toRequire =
		packages, { }, nil

	for directory in string.gmatch(importString, "%a+") do
		table.insert(splitImportString, directory)
	end

	for _, directory in pairs(splitImportString) do
		if (currentIndex:FindFirstChild(directory)) then
			currentIndex = currentIndex[directory]
		else
			return error("[FDK - PACKAGE MANAGER] Package " .. directory .. " does not exist.")
		end
	end

	if (currentIndex:IsA("NumberValue") or currentIndex:IsA("IntValue")) then
		toRequire = currentIndex.Value
	elseif (currentIndex:IsA("ModuleScript")) then
		toRequire = currentIndex
	end

	if (toRequire == nil) then
		return error("[FDK - PACKAGE MANAGER] Package does not exist.")
	end

	local class = require(toRequire)

	if (not checkTypes(class, "table", "function")) then
		return error("[FDK - PACKAGE MANAGER] Expected function or table, got "
			.. typeof(class) .. " while initalizing class module.")
	end

	self:wrapEnvironment(class)

	return class(), currentIndex.Name -- Should we be calling the class?
end

--[[
	Function: Wraps the provided enviroment with useful FDK functions
	Arguments: self - FDK, value - table/function given for the enviroment
]]
FDK.wrapEnvironment = function(self, value)
	local useValue = (value == nil and self or value)

	local functionEnviorment

	if (checkTypes(useValue, "function")) then
		functionEnviorment = getfenv(useValue)
	elseif (checkTypes(useValue, "table")) then
		functionEnviorment = useValue
	end

	if (functionEnviorment == nil) then
		return error("[FDK - PACKAGE MANAGER] Expected function or table, got " .. typeof(functionEnviorment) .. ".")
	end

	functionEnviorment.import = function(importString)
		local class, name = self:import(importString)

		return class, name
	end

	functionEnviorment.BaseClass = BaseClass
	functionEnviorment.new = FDK.new

	--Legacy Support
	functionEnviorment.Class = BaseClass
	functionEnviorment.New = FDK.new
	--Legacy Support
end

--Legacy Support
FDK.WrapEnv = FDK.wrapEnvironment
FDK.Import = FDK.import
--Legacy Support

_G.FDK = external

return external

EDIT: Add plugin

1 Like

Seems good. Make sure it’s clean of bugs by properly testing it!