require("Dependencies")
Cool = TypeProtect(
{
a = "number|string",
b = "number|string",
c = "boolean?"
}, "any...", 0,
function(Def, ...)
if Def.c then
return Def.a - Def.b, ...
else
return Def.a + Def.b, ...
end
end
)
print(Cool({
a = 1,
b = 3,
c = true
}))
print(Col3(1, 0, 1):ToHSV())
--[[
print(Cool({
a = 1,
b = 2,
c = false
}, "hi", true, 5))
print(Cool({
a = 1,
b = false,
c = false
}, "hello"))
]]
print(unpack({"hi", "hello", 3}, 1, -1))
require("Util")
require("TypeProtect")
require("Class")
require("Col3")
function string.split(inputstr, sep)
sep = sep or '%s'
sep = sep ~= "" and "([^"..sep.."]*)("..sep.."?)" or "."
local t = {}
for field, s in string.gmatch(inputstr, sep) do
table.insert(t, field)
if s == "" then
return t
end
end
return t
end
function table.print(t, Indent)
if not Indent then
print(string.rep(" ", Indent or 0).."{")
end
local CallWith = {}
for i, v in next, t do
if type(v) == "table" then
CallWith[i] = v
else
print(string.rep(" ", Indent or 0), "["..i.."] =", v)
end
end
for i, v in next, CallWith do
local Indent = Indent and Indent + 1 or 1
print(string.rep(" ", Indent).."["..i.."] = {")
RecursePrint(v, Indent)
end
print(string.rep(" ", Indent and Indent or 0).."}")
end
function typeof(v)
local Type = type(v)
return Type == "table" and v.__Type or Type
end
local function ParseTypeTable(t, ErrorDepth, Path)
-- iterating over and modifying a table at the same time can lead to unpredictable behaviour once in a blue moon
local Modifications = {} -- so we'll store the modifications here to add them to the table later on
for i, v in next, t do
if type(v) == "table" then
ParseTypeTable(v, ErrorDepth + 1, (Path and Path.."." or "#")..i)
elseif type(v) == "string" then
local IsInfInput
repeat
local Replaced = 0
local Got
v, Got = v:gsub("%.%.%.", "")
Replaced = Replaced + Got
IsInfInput = IsInfInput or Got ~= 0
v, Got = v:gsub("%?$", "|nil")
Replaced = Replaced + Got
v, Got = v:gsub("^%?", "nil|")
Replaced = Replaced + Got
until Replaced == 0
local Seperated = v:split("|")
Seperated.__TypeBundle = true
for i, v in ipairs(Seperated) do
if v == "any" then
Seperated = {__Any = true, __TypeBundle = true}
break
end
end
Modifications[i] = Seperated
if IsInfInput then
Modifications.__InfInput = Seperated
end
else
error(string.format("Expected string or table, got %s at %s%s.", typeof(v), Path and Path.."." or "", i), ErrorDepth)
end
end
for i, v in next, Modifications do
t[i] = v
end
end
local function VerifyTableTypes(t, Types, ErrorDepth, Path)
local InfInput = Types.__InfInput
if not InfInput or not InfInput.__Any then
for i, v in next, t do
if not Types[i] then
if InfInput then
local Type = typeof(v)
for i, v in ipairs(InfInput) do
if Type == v then
goto continue
end
end
local Types
if #InfInput > 1 then
Types = table.concat(InfInput, ", ", 1, #InfInput - 1).." or "..InfInput[#InfInput]
else
Types = InfInput[1]
end
error(string.format("Expected %s, got %s at #%s%s.", Types, Type, Path and Path.."." or "", i), ErrorDepth)
::continue::
else
error(string.format("Expected nil, got %s at %s%s.", typeof(v), Path and Path.."." or "", i), ErrorDepth)
end
end
end
end
local Debug = Types
for i, v in next, Types do
if i == "__TypeBundle" or i == "__InfInput" or i == "__Any" then
goto continue
end
local Type = typeof(t[i])
if v.__TypeBundle then
if v.__Any then
goto continue
end
for i, v in ipairs(v) do
if Type == v then
goto continue
end
end
local Types
if #v > 1 then
Types = table.concat(v, ", ", 1, #v - 1).." or "..v[#v]
else
Types = v[1]
end
table.print(Debug)
error(string.format("Expected %s, got %s at #%s%s.", Types, Type, Path and Path.."." or "", i), ErrorDepth)
elseif not t[i] then
error(string.format("Unexpected table structure at at #%s%s.", Path and Path.."." or "", i), ErrorDepth)
else
if type(t[i]) ~= "table" then
print(Path.."."..i)
end
VerifyTableTypes(t[i], v, ErrorDepth + 1, Path and Path.."."..i or i)
end
::continue::
end
end
function TypeProtect(...)
local Types = {...}
local Func = table.remove(Types)
local ErrorDepth = table.remove(Types)
do -- initial error checking/processing
if type(Func) ~= "function" or type(ErrorDepth) ~= "number" then
error(string.format("Expected a number and a function as the last 2 parameters, got %s and %s.", typeof(Func), typeof(ErrorDepth)), 2)
end
ParseTypeTable(Types, 3 + ErrorDepth)
end
ErrorDepth = math.max(0, ErrorDepth)
return function(...)
local Got = {...}
VerifyTableTypes(Got, Types, 3 + ErrorDepth)
return Func(...)
end
end
local ClassMeta = {}
function ClassMeta:__Call(...)
return self:New(...)
end
function ClassMeta:__index(Key)
if ClassMeta[Key] then
return ClassMeta[Key]
elseif self.__Constructors[Key] then
return function(...)
local Object = setmetatable({}, self.__Methods)
self.__Constructors[Key](Object, ...)
return Object
end
end
end
ClassMeta.__newindex = TypeProtect("Class", "string|number", "function", 0, function(self, Index, Function)
if not self.__PromiseState.Types then
error("Attempted to add a method/constructor without making a promise first.", 3)
elseif Index ~= self.__PromiseState.PromiseName then
error("Attempted to add a method/constructor with a name that doesn't match the last given promise.", 3)
end
local ProtectArgs = self.__PromiseState.Types
table.insert(ProtectArgs, 0)
table.insert(ProtectArgs, Function)
local Protected = TypeProtect(unpack(ProtectArgs))
local PromiseType = self.__PromiseState.PromiseType
if PromiseType == ":" then
self.__Methods[Index] = Protected
elseif PromiseType == "+" then
if not self.__Constructors.__Default then
self.__Constructors.__Default = Protected
end
self.__Constructors[Index] = Protected
end
self.__PromiseState.PromiseType = nil
self.__PromiseState.PromiseName = nil
self.__PromiseState.Types = nil
end)
ClassMeta.AddPromise = TypeProtect("Class", "string", "string...", 0, function(self, PromiseName, ...)
local PromiseType = PromiseName:sub(1, 1)
if PromiseType ~= ":" and PromiseType ~= "+" then
error("Please use + for promising a constructor and : for promising a method as a prefix before the promised name.\nExamples: \":MyMethod\" \"+fromHSV\"", 3)
end
self.__PromiseState.PromiseType = PromiseType
self.__PromiseState.PromiseName = PromiseName:sub(2, -1)
self.__PromiseState.Types = {...}
end)
function ClassMeta:__call(...)
return self.__Default(...)
end
Class = TypeProtect("string", 0, function(Type)
local Class = {
__Type = "Class",
__Methods = {
__Type = Type
},
__Constructors = {},
__PromiseState = {}
}
Class.__Methods.__index = Class.__Methods
return setmetatable(Class, ClassMeta)
end)
Col3 = Class("Col3")
Col3:AddPromise("+New", "Col3", "number?", "number?", "number?")
function Col3:New(R, G, B)
self.R = R or 0
self.G = G or 0
self.B = B or 0
end
-- from https://www.rapidtables.com/convert/color/hsv-to-rgb.html
Col3:AddPromise("+FromHSV", "Col3", "number?", "number?", "number?")
function Col3:FromHSV(H, S, V)
H = (H % 1) * 360
S = math.clamp(S, 0, 1)
V = math.clamp(V, 0, 1)
local C = V * S
local X = C * (1 - math.abs(((H / 60) % 2) - 1))
local M = V - C
if H < 60 then
self.R = C
self.G = X
self.B = 0
elseif H < 120 then
self.R = X
self.G = C
self.B = 0
elseif H < 180 then
self.R = 0
self.G = C
self.B = X
elseif H < 240 then
self.R = 0
self.G = X
self.B = C
elseif H < 300 then
self.R = X
self.G = 0
self.B = C
else
self.R = C
self.G = 0
self.B = X
end
self.R = self.R + M
self.G = self.G + M
self.B = self.B + M
end
-- from https://www.rapidtables.com/convert/color/rgb-to-hsv.html
Col3:AddPromise(":ToHSV", "Col3")
function Col3:ToHSV()
local R, G, B = self.R, self.G, self.B
local Cmax = math.max(R, G, B)
local Cmin = math.min(R, G, B)
local Delta = Cmax - Cmin
local H
if Delta == 0 then
H = 0
elseif Cmax == R then
H = 60 * (((G - B) / Delta) % 6)
elseif Cmax == G then
H = 60 * (((B - R) / Delta + 2) % 6)
elseif Cmax == B then
H = 60 * (((R - G) / Delta + 4) % 6)
end
local S
if Cmax == 0 then
S = 0
else
S = Delta / Cmax
end
V = Cmax
return H / 360, S, V
end
Col3:AddPromise(":__tostring", "Col3")
function Col3:__tostring()
return string.format("Col3(%d, %d, %d)", self.R, self.G, self.B)
end