-- t: a runtime typechecker for Roblox local t = {} function t.type(typeName) return function(value) local valueType = type(value) if valueType == typeName then return true else return false, string.format("%s expected, got %s", typeName, valueType) end end end function t.typeof(typeName) return function(value) local valueType = typeof(value) if valueType == typeName then return true else return false, string.format("%s expected, got %s", typeName, valueType) end end end --[[** matches any type except nil @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] function t.any(value) if value ~= nil then return true else return false, "any expected, got nil" end end --Lua primitives --[[** ensures Lua primitive boolean type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.boolean = t.typeof("boolean") --[[** ensures Lua primitive buffer type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.buffer = t.typeof("buffer") --[[** ensures Lua primitive thread type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.thread = t.typeof("thread") --[[** ensures Lua primitive callback type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.callback = t.typeof("function") t["function"] = t.callback --[[** ensures Lua primitive none type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.none = t.typeof("nil") t["nil"] = t.none --[[** ensures Lua primitive string type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.string = t.typeof("string") --[[** ensures Lua primitive table type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.table = t.typeof("table") --[[** ensures Lua primitive userdata type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.userdata = t.type("userdata") --[[** ensures Lua primitive vector type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.vector = t.type("vector") --[[** ensures value is a number and non-NaN @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] function t.number(value) local valueType = typeof(value) if valueType == "number" then if value == value then return true else return false, "unexpected NaN value" end else return false, string.format("number expected, got %s", valueType) end end --[[** ensures value is NaN @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] function t.nan(value) local valueType = typeof(value) if valueType == "number" then if value ~= value then return true else return false, "unexpected non-NaN value" end else return false, string.format("number expected, got %s", valueType) end end -- roblox types --[[** ensures Roblox Axes type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.Axes = t.typeof("Axes") --[[** ensures Roblox BrickColor type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.BrickColor = t.typeof("BrickColor") --[[** ensures Roblox CatalogSearchParams type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.CatalogSearchParams = t.typeof("CatalogSearchParams") --[[** ensures Roblox CFrame type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.CFrame = t.typeof("CFrame") --[[** ensures Roblox Color3 type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.Color3 = t.typeof("Color3") --[[** ensures Roblox ColorSequence type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.ColorSequence = t.typeof("ColorSequence") --[[** ensures Roblox ColorSequenceKeypoint type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.ColorSequenceKeypoint = t.typeof("ColorSequenceKeypoint") --[[** ensures Roblox DateTime type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.DateTime = t.typeof("DateTime") --[[** ensures Roblox DockWidgetPluginGuiInfo type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.DockWidgetPluginGuiInfo = t.typeof("DockWidgetPluginGuiInfo") --[[** ensures Roblox Enum type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.Enum = t.typeof("Enum") --[[** ensures Roblox EnumItem type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.EnumItem = t.typeof("EnumItem") --[[** ensures Roblox Enums type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.Enums = t.typeof("Enums") --[[** ensures Roblox Faces type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.Faces = t.typeof("Faces") --[[** ensures Roblox FloatCurveKey type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.FloatCurveKey = t.typeof("FloatCurveKey") --[[** ensures Roblox Font type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.Font = t.typeof("Font") --[[** ensures Roblox Instance type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.Instance = t.typeof("Instance") --[[** ensures Roblox NumberRange type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.NumberRange = t.typeof("NumberRange") --[[** ensures Roblox NumberSequence type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.NumberSequence = t.typeof("NumberSequence") --[[** ensures Roblox NumberSequenceKeypoint type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.NumberSequenceKeypoint = t.typeof("NumberSequenceKeypoint") --[[** ensures Roblox OverlapParams type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.OverlapParams = t.typeof("OverlapParams") --[[** ensures Roblox PathWaypoint type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.PathWaypoint = t.typeof("PathWaypoint") --[[** ensures Roblox PhysicalProperties type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.PhysicalProperties = t.typeof("PhysicalProperties") --[[** ensures Roblox Random type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.Random = t.typeof("Random") --[[** ensures Roblox Ray type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.Ray = t.typeof("Ray") --[[** ensures Roblox RaycastParams type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.RaycastParams = t.typeof("RaycastParams") --[[** ensures Roblox RaycastResult type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.RaycastResult = t.typeof("RaycastResult") --[[** ensures Roblox RBXScriptConnection type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.RBXScriptConnection = t.typeof("RBXScriptConnection") --[[** ensures Roblox RBXScriptSignal type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.RBXScriptSignal = t.typeof("RBXScriptSignal") --[[** ensures Roblox Rect type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.Rect = t.typeof("Rect") --[[** ensures Roblox Region3 type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.Region3 = t.typeof("Region3") --[[** ensures Roblox Region3int16 type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.Region3int16 = t.typeof("Region3int16") --[[** ensures Roblox TweenInfo type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.TweenInfo = t.typeof("TweenInfo") --[[** ensures Roblox UDim type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.UDim = t.typeof("UDim") --[[** ensures Roblox UDim2 type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.UDim2 = t.typeof("UDim2") --[[** ensures Roblox Vector2 type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.Vector2 = t.typeof("Vector2") --[[** ensures Roblox Vector2int16 type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.Vector2int16 = t.typeof("Vector2int16") --[[** ensures Roblox Vector3 type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.Vector3 = t.typeof("Vector3") --[[** ensures Roblox Vector3int16 type @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] t.Vector3int16 = t.typeof("Vector3int16") --[[** ensures value is a given literal value @param literal The literal to use @returns A function that will return true iff the condition is passed **--]] function t.literal(...) local size = select("#", ...) if size == 1 then local literal = ... return function(value) if value ~= literal then return false, string.format("expected %s, got %s", tostring(literal), tostring(value)) end return true end else local literals = {} for i = 1, size do local value = select(i, ...) literals[i] = t.literal(value) end return t.union(table.unpack(literals, 1, size)) end end --[[** DEPRECATED Please use t.literal **--]] t.exactly = t.literal --[[** Returns a t.union of each key in the table as a t.literal @param keyTable The table to get keys from @returns True iff the condition is satisfied, false otherwise **--]] function t.keyOf(keyTable) local keys = {} local length = 0 for key in pairs(keyTable) do length = length + 1 keys[length] = key end return t.literal(table.unpack(keys, 1, length)) end --[[** Returns a t.union of each value in the table as a t.literal @param valueTable The table to get values from @returns True iff the condition is satisfied, false otherwise **--]] function t.valueOf(valueTable) local values = {} local length = 0 for _, value in pairs(valueTable) do length = length + 1 values[length] = value end return t.literal(table.unpack(values, 1, length)) end --[[** ensures value is an integer @param value The value to check against @returns True iff the condition is satisfied, false otherwise **--]] function t.integer(value) local success, errMsg = t.number(value) if not success then return false, errMsg or "" end if value % 1 == 0 then return true else return false, string.format("integer expected, got %s", value) end end --[[** ensures value is a number where min <= value @param min The minimum to use @returns A function that will return true iff the condition is passed **--]] function t.numberMin(min) return function(value) local success, errMsg = t.number(value) if not success then return false, errMsg or "" end if value >= min then return true else return false, string.format("number >= %s expected, got %s", min, value) end end end --[[** ensures value is a number where value <= max @param max The maximum to use @returns A function that will return true iff the condition is passed **--]] function t.numberMax(max) return function(value) local success, errMsg = t.number(value) if not success then return false, errMsg end if value <= max then return true else return false, string.format("number <= %s expected, got %s", max, value) end end end --[[** ensures value is a number where min < value @param min The minimum to use @returns A function that will return true iff the condition is passed **--]] function t.numberMinExclusive(min) return function(value) local success, errMsg = t.number(value) if not success then return false, errMsg or "" end if min < value then return true else return false, string.format("number > %s expected, got %s", min, value) end end end --[[** ensures value is a number where value < max @param max The maximum to use @returns A function that will return true iff the condition is passed **--]] function t.numberMaxExclusive(max) return function(value) local success, errMsg = t.number(value) if not success then return false, errMsg or "" end if value < max then return true else return false, string.format("number < %s expected, got %s", max, value) end end end --[[** ensures value is a number where value > 0 @returns A function that will return true iff the condition is passed **--]] t.numberPositive = t.numberMinExclusive(0) --[[** ensures value is a number where value < 0 @returns A function that will return true iff the condition is passed **--]] t.numberNegative = t.numberMaxExclusive(0) --[[** ensures value is a number where min <= value <= max @param min The minimum to use @param max The maximum to use @returns A function that will return true iff the condition is passed **--]] function t.numberConstrained(min, max) assert(t.number(min)) assert(t.number(max)) local minCheck = t.numberMin(min) local maxCheck = t.numberMax(max) return function(value) local minSuccess, minErrMsg = minCheck(value) if not minSuccess then return false, minErrMsg or "" end local maxSuccess, maxErrMsg = maxCheck(value) if not maxSuccess then return false, maxErrMsg or "" end return true end end --[[** ensures value is a number where min < value < max @param min The minimum to use @param max The maximum to use @returns A function that will return true iff the condition is passed **--]] function t.numberConstrainedExclusive(min, max) assert(t.number(min)) assert(t.number(max)) local minCheck = t.numberMinExclusive(min) local maxCheck = t.numberMaxExclusive(max) return function(value) local minSuccess, minErrMsg = minCheck(value) if not minSuccess then return false, minErrMsg or "" end local maxSuccess, maxErrMsg = maxCheck(value) if not maxSuccess then return false, maxErrMsg or "" end return true end end --[[** ensures value matches string pattern @param string pattern to check against @returns A function that will return true iff the condition is passed **--]] function t.match(pattern) assert(t.string(pattern)) return function(value) local stringSuccess, stringErrMsg = t.string(value) if not stringSuccess then return false, stringErrMsg end if string.match(value, pattern) == nil then return false, string.format("%q failed to match pattern %q", value, pattern) end return true end end --[[** ensures value is either nil or passes check @param check The check to use @returns A function that will return true iff the condition is passed **--]] function t.optional(check) assert(t.callback(check)) return function(value) if value == nil then return true end local success, errMsg = check(value) if success then return true else return false, string.format("(optional) %s", errMsg or "") end end end --[[** matches given tuple against tuple type definition @param ... The type definition for the tuples @returns A function that will return true iff the condition is passed **--]] function t.tuple(...) local checks = { ... } return function(...) local args = { ... } for i, check in ipairs(checks) do local success, errMsg = check(args[i]) if success == false then return false, string.format("Bad tuple index #%s:\n\t%s", i, errMsg or "") end end return true end end --[[** ensures all keys in given table pass check @param check The function to use to check the keys @returns A function that will return true iff the condition is passed **--]] function t.keys(check) assert(t.callback(check)) return function(value) local tableSuccess, tableErrMsg = t.table(value) if tableSuccess == false then return false, tableErrMsg or "" end for key in pairs(value) do local success, errMsg = check(key) if success == false then return false, string.format("bad key %s:\n\t%s", tostring(key), errMsg or "") end end return true end end --[[** ensures all values in given table pass check @param check The function to use to check the values @returns A function that will return true iff the condition is passed **--]] function t.values(check) assert(t.callback(check)) return function(value) local tableSuccess, tableErrMsg = t.table(value) if tableSuccess == false then return false, tableErrMsg or "" end for key, val in pairs(value) do local success, errMsg = check(val) if success == false then return false, string.format("bad value for key %s:\n\t%s", tostring(key), errMsg or "") end end return true end end --[[** ensures value is a table and all keys pass keyCheck and all values pass valueCheck @param keyCheck The function to use to check the keys @param valueCheck The function to use to check the values @returns A function that will return true iff the condition is passed **--]] function t.map(keyCheck, valueCheck) assert(t.callback(keyCheck)) assert(t.callback(valueCheck)) local keyChecker = t.keys(keyCheck) local valueChecker = t.values(valueCheck) return function(value) local keySuccess, keyErr = keyChecker(value) if not keySuccess then return false, keyErr or "" end local valueSuccess, valueErr = valueChecker(value) if not valueSuccess then return false, valueErr or "" end return true end end --[[** ensures value is a table and all keys pass valueCheck and all values are true @param valueCheck The function to use to check the values @returns A function that will return true iff the condition is passed **--]] function t.set(valueCheck) return t.map(valueCheck, t.literal(true)) end do local arrayKeysCheck = t.keys(t.integer) --[[** ensures value is an array and all values of the array match check @param check The check to compare all values with @returns A function that will return true iff the condition is passed **--]] function t.array(check) assert(t.callback(check)) local valuesCheck = t.values(check) return function(value) local keySuccess, keyErrMsg = arrayKeysCheck(value) if keySuccess == false then return false, string.format("[array] %s", keyErrMsg or "") end -- # is unreliable for sparse arrays -- Count upwards using ipairs to avoid false positives from the behavior of # local arraySize = 0 for _ in ipairs(value) do arraySize = arraySize + 1 end for key in pairs(value) do if key < 1 or key > arraySize then return false, string.format("[array] key %s must be sequential", tostring(key)) end end local valueSuccess, valueErrMsg = valuesCheck(value) if not valueSuccess then return false, string.format("[array] %s", valueErrMsg or "") end return true end end --[[** ensures value is an array of a strict makeup and size @param check The check to compare all values with @returns A function that will return true iff the condition is passed **--]] function t.strictArray(...) local valueTypes = { ... } assert(t.array(t.callback)(valueTypes)) return function(value) local keySuccess, keyErrMsg = arrayKeysCheck(value) if keySuccess == false then return false, string.format("[strictArray] %s", keyErrMsg or "") end -- If there's more than the set array size, disallow if #valueTypes < #value then return false, string.format("[strictArray] Array size exceeds limit of %d", #valueTypes) end for idx, typeFn in pairs(valueTypes) do local typeSuccess, typeErrMsg = typeFn(value[idx]) if not typeSuccess then return false, string.format("[strictArray] Array index #%d - %s", idx, typeErrMsg) end end return true end end end do local callbackArray = t.array(t.callback) --[[** creates a union type @param ... The checks to union @returns A function that will return true iff the condition is passed **--]] function t.union(...) local checks = { ... } assert(callbackArray(checks)) return function(value) for _, check in ipairs(checks) do if check(value) then return true end end return false, "bad type for union" end end --[[** Alias for t.union **--]] t.some = t.union --[[** creates an intersection type @param ... The checks to intersect @returns A function that will return true iff the condition is passed **--]] function t.intersection(...) local checks = { ... } assert(callbackArray(checks)) return function(value) for _, check in ipairs(checks) do local success, errMsg = check(value) if not success then return false, errMsg or "" end end return true end end --[[** Alias for t.intersection **--]] t.every = t.intersection end do local checkInterface = t.map(t.any, t.callback) --[[** ensures value matches given interface definition @param checkTable The interface definition @returns A function that will return true iff the condition is passed **--]] function t.interface(checkTable) assert(checkInterface(checkTable)) return function(value) local tableSuccess, tableErrMsg = t.table(value) if tableSuccess == false then return false, tableErrMsg or "" end for key, check in pairs(checkTable) do local success, errMsg = check(value[key]) if success == false then return false, string.format("[interface] bad value for %s:\n\t%s", tostring(key), errMsg or "") end end return true end end --[[** ensures value matches given interface definition strictly @param checkTable The interface definition @returns A function that will return true iff the condition is passed **--]] function t.strictInterface(checkTable) assert(checkInterface(checkTable)) return function(value) local tableSuccess, tableErrMsg = t.table(value) if tableSuccess == false then return false, tableErrMsg or "" end for key, check in pairs(checkTable) do local success, errMsg = check(value[key]) if success == false then return false, string.format("[interface] bad value for %s:\n\t%s", tostring(key), errMsg or "") end end for key in pairs(value) do if not checkTable[key] then return false, string.format("[interface] unexpected field %q", tostring(key)) end end return true end end end --[[** ensure value is an Instance and it's ClassName matches the given ClassName @param className The class name to check for @returns A function that will return true iff the condition is passed **--]] function t.instanceOf(className, childTable) assert(t.string(className)) local childrenCheck if childTable ~= nil then childrenCheck = t.children(childTable) end return function(value) local instanceSuccess, instanceErrMsg = t.Instance(value) if not instanceSuccess then return false, instanceErrMsg or "" end if value.ClassName ~= className then return false, string.format("%s expected, got %s", className, value.ClassName) end if childrenCheck then local childrenSuccess, childrenErrMsg = childrenCheck(value) if not childrenSuccess then return false, childrenErrMsg end end return true end end t.instance = t.instanceOf --[[** ensure value is an Instance and it's ClassName matches the given ClassName by an IsA comparison @param className The class name to check for @returns A function that will return true iff the condition is passed **--]] function t.instanceIsA(className, childTable) assert(t.string(className)) local childrenCheck if childTable ~= nil then childrenCheck = t.children(childTable) end return function(value) local instanceSuccess, instanceErrMsg = t.Instance(value) if not instanceSuccess then return false, instanceErrMsg or "" end if not value:IsA(className) then return false, string.format("%s expected, got %s", className, value.ClassName) end if childrenCheck then local childrenSuccess, childrenErrMsg = childrenCheck(value) if not childrenSuccess then return false, childrenErrMsg end end return true end end --[[** ensures value is an enum of the correct type @param enum The enum to check @returns A function that will return true iff the condition is passed **--]] function t.enum(enum) assert(t.Enum(enum)) return function(value) local enumItemSuccess, enumItemErrMsg = t.EnumItem(value) if not enumItemSuccess then return false, enumItemErrMsg end if value.EnumType == enum then return true else return false, string.format("enum of %s expected, got enum of %s", tostring(enum), tostring(value.EnumType)) end end end do local checkWrap = t.tuple(t.callback, t.callback) --[[** wraps a callback in an assert with checkArgs @param callback The function to wrap @param checkArgs The function to use to check arguments in the assert @returns A function that first asserts using checkArgs and then calls callback **--]] function t.wrap(callback, checkArgs) assert(checkWrap(callback, checkArgs)) return function(...) assert(checkArgs(...)) return callback(...) end end end --[[** asserts a given check @param check The function to wrap with an assert @returns A function that simply wraps the given check in an assert **--]] function t.strict(check) return function(...) assert(check(...)) end end do local checkChildren = t.map(t.string, t.callback) --[[** Takes a table where keys are child names and values are functions to check the children against. Pass an instance tree into the function. If at least one child passes each check, the overall check passes. Warning! If you pass in a tree with more than one child of the same name, this function will always return false @param checkTable The table to check against @returns A function that checks an instance tree **--]] function t.children(checkTable) assert(checkChildren(checkTable)) return function(value) local instanceSuccess, instanceErrMsg = t.Instance(value) if not instanceSuccess then return false, instanceErrMsg or "" end local childrenByName = {} for _, child in ipairs(value:GetChildren()) do local name = child.Name if checkTable[name] then if childrenByName[name] then return false, string.format("Cannot process multiple children with the same name %q", name) end childrenByName[name] = child end end for name, check in pairs(checkTable) do local success, errMsg = check(childrenByName[name]) if not success then return false, string.format("[%s.%s] %s", value:GetFullName(), name, errMsg or "") end end return true end end end return t