Rules is a runtime type checker for Roblox, made to replace the old t library. It helps you validate values using rules anywhere in your code but mainly for RemoteEvents
and RemoteFunctions
.
If you’ve used 't'
before, Rules will feel familiar but with more features and support for the latest types.
Documentation
Basic
Rules.Void(...)
Passes only if no arguments are provided. Useful for validating empty input.
Rules.None(Value)
Passes only if the value is nil
.
Rules.Optional(Validator)
Allows a value to be nil
or pass the given validator.
Example: Rules.Optional(Rules.String)
Rules.Any(Value)
Passes if the value is not nil. Accepts any type.
Rules.Literal(Expected)
Passes only if the value exactly equals the expected value.
Example: Rules.Literal("Hello")
passes only if the input is "Hello"
Primitive Validators
Rules.String(Value)
Passes if the value is a string.
Rules.Number(Value)
Passes if the value is a number (including NaN and infinity).
Rules.RealNumber(Value)
Passes only if the value is a finite number (not NaN or infinity).
Rules.StrictNumber(Value)
Passes only if the value is a number and not NaN.
Rules.Boolean(Value)
Passes if the value is a boolean (true
or false
).
Rules.Table(Value)
Passes if the value is a table.
Rules.Thread(Value)
Passes if the value is a thread (i.e. a coroutine).
Rules.Userdata(Value)
Passes if the value is any userdata.
Rules.LuaUserdata(Value)
Passes only if the value is a Lua-native userdata (not Roblox instance userdata).
Rules.Buffer(Value)
Passes if the value is of type buffer
.
Rules.Vector(Value)
Passes if the value is of type vector
.
Rules.Callback(Value)
Passes if the value is a function (Lua or C).
Rules.CClosure(Value)
Passes only if the value is a C closure.
Fails if the function is defined in Lua.
Class Validators
Rules.Axes(Value)
Passes if the value is an instance of the Axes
class.
Rules.BrickColor(Value)
Passes if the value is an instance of the BrickColor
class.
Rules.CatalogSearchParams(Value)
Passes if the value is an instance of the CatalogSearchParams
class.
Rules.CFrame(Value)
Passes if the value is an instance of the CFrame
class.
Rules.Color3(Value)
Passes if the value is an instance of the Color3
class.
Rules.ColorSequence(Value)
Passes if the value is an instance of the ColorSequence
class.
Rules.ColorSequenceKeypoint(Value)
Passes if the value is an instance of the ColorSequenceKeypoint
class.
Rules.DateTime(Value)
Passes if the value is an instance of the DateTime
class.
Rules.DockWidgetPluginGuiInfo(Value)
Passes if the value is an instance of the DockWidgetPluginGuiInfo
class.
Rules.Faces(Value)
Passes if the value is an instance of the Faces
class.
Rules.FloatCurveKey(Value)
Passes if the value is an instance of the FloatCurveKey
class.
Rules.Font(Value)
Passes if the value is an instance of the Font
class.
Rules.Instance(Value)
Passes if the value is an instance of the Instance
class.
Rules.NumberRange(Value)
Passes if the value is an instance of the NumberRange
class.
Rules.NumberSequence(Value)
Passes if the value is an instance of the NumberSequence
class.
Rules.NumberSequenceKeypoint(Value)
Passes if the value is an instance of the NumberSequenceKeypoint
class.
Rules.OverlapParams(Value)
Passes if the value is an instance of the OverlapParams
class.
Rules.PathWaypoint(Value)
Passes if the value is an instance of the PathWaypoint
class.
Rules.PhysicalProperties(Value)
Passes if the value is an instance of the PhysicalProperties
class.
Rules.Random(Value)
Passes if the value is an instance of the Random
class.
Rules.Ray(Value)
Passes if the value is an instance of the Ray
class.
Rules.RaycastParams(Value)
Passes if the value is an instance of the RaycastParams
class.
Rules.RaycastResult(Value)
Passes if the value is an instance of the RaycastResult
class.
Rules.RBXScriptConnection(Value)
Passes if the value is an instance of the RBXScriptConnection
class.
Rules.RBXScriptSignal(Value)
Passes if the value is an instance of the RBXScriptSignal
class.
Rules.Rect(Value)
Passes if the value is an instance of the Rect
class.
Rules.Region3(Value)
Passes if the value is an instance of the Region3
class.
Rules.Region3int16(Value)
Passes if the value is an instance of the Region3int16
class.
Rules.TweenInfo(Value)
Passes if the value is an instance of the TweenInfo
class.
Rules.UDim(Value)
Passes if the value is an instance of the UDim
class.
Rules.UDim2(Value)
Passes if the value is an instance of the UDim2
class.
Rules.Vector2(Value)
Passes if the value is an instance of the Vector2
class.
Rules.Vector2int16(Value)
Passes if the value is an instance of the Vector2int16
class.
Rules.Vector3(Value)
Passes if the value is an instance of the Vector3
class.
Rules.Vector3int16(Value)
Passes if the value is an instance of the Vector3int16
class.
Type Variant Validator
Rules.Array(Value)
Passes if the value is an array, meaning it’s a table with sequential indices starting from 1
and without gaps or nil values in between.
Rules.StrictArray(Value)
Passes if the value is a strict array, meaning it has no fractional or negative indices, no gaps, and starts from index 1
. This also makes sure that ipairs
will fully loop through the array without any interruptions.
Rules.Frozen(Value)
Passes if the value is a frozen table. A frozen table cannot be modified after its creation.
Enums
Rules.Enum(Value)
Passes if the value is an Enum
type.
Rules.EnumItem(Value)
Passes if the value is an EnumItem
.
Rules.FromEnum(EnumType: Enum)(Value)
Passes if the value is an item from the specified EnumType
. This makes sure that the value belongs to a specific enum.
Values
Rules.ClassName(Value)
Passes if the value is an instance of the specified class.
Rules.ValidUTF8(Value)
Passes if the value is a valid UTF-8 string.
Rules.NaN(Value)
Passes if the value is NaN
.
Rules.Infinite(Value)
Passes if the value is infinite.
Rules.FiniteCFrame(Value)
Passes if the value is a valid CFrame
and all components (X, Y, Z) are within world bounds.
Rules.Values(Validator, TreatTablesAsValues?, Recursive?)
Passes if all values in the table pass the provided Validator
.
Rules.Keys(Validator, Recursive?)
Passes if all keys in the table pass the provided Validator
.
Rules.StringLength(MaxLength)(Value)
Passes if the string’s length is less than or equal to the specified MaxLength
.
Rules.TableLength(MaxLength, Array?)(Value)
Passes if the table’s length is less than or equal to the specified MaxLength
. If Array
is specified, it checks for array-like tables.
Rules.Struct(TableStructure, Strict?)(Value)
Passes if the table has the specified structure and optional strict rules are followed.
Example Usage:
local ValidateProfile = Rules.Struct({
Name = Rules.String,
Level = Rules.All(Rules.Number, function(Value) -- ⚠️ Custom Function⚠️
return Value >= 1, "Level must be at least 1"
end),
IsPremium = Rules.Boolean,
}, true) -- 'true' enables strict checking (no extra fields allowed)
local Profile = {
Name = "John Doe",
Level = 5,
IsPremium = false,
}
local Success, Message = ValidateProfile(Profile)
print(Success, Message) --> true, ""
Rules.Depth(MaxDepth)(Value)
Passes if the table’s depth does not exceed the specified MaxDepth
.
Rules.Match(Expression)(Value)
Passes if the value matches the specified regular expression Expression
.
Rules.Integer(Value)
Passes if the value is an integer (i.e., has no fractional part).
Rules.PositiveNumber(Value)
Passes if the value is a positive number.
Rules.NegativeNumber(Value)
Passes if the value is a negative number.
Rules.InRangeInclusive(Min, Max)(Value)
Passes if the value is between Min
and Max
, inclusive.
Rules.InRangeExclusive(Min, Max)(Value)
Passes if the value is between Min + 1
and Max - 1
, exclusive.
Rules.Minimum(Min)(Value)
Passes if the value is greater than or equal to the specified Min
.
Rules.Maximum(Max)(Value)
Passes if the value is less than or equal to the specified Max
.
Rules.Epsilon(Number, Epsilon)(Value)
Passes if the value is within Epsilon
of the specified Number
.
Rules.DescendantOf(Inst)(Value)
Passes if the value is a descendant of the specified Instance
.
API
Rules.Inverse(Rule: RuleValidator | string): RuleValidator
Description:
Creates a validator that inverts the result of a given rule. If the original rule succeeds, the inverse fails and vice versa.
Example Usage:
local NotANumber = Rules.Inverse("Number")
print(NotANumber("hello")) -- true, ""
print(NotANumber(42)) -- false, "Expected not a number"
Details:
- Accepts either a function or the string name of a rule.
- Only works for rules that have inverse messages defined in
InversedMessages
.
Rules.Set(...: RuleValidator): VarargRuleValidator
Description:
Validates a fixed list of arguments, each against a corresponding rule. Argument count must match exactly.
Example Usage:
local ValidatePlayerData = Rules.Set(
Rules.StringLength(20),
Rules.PositiveNumber
)
print(ValidatePlayerData("PlayerOne", 100)) -- true, ""
print(ValidatePlayerData("PlayerOne")) -- false, "Got less than 2 arguments"
print(ValidatePlayerData("PlayerOne", -50)) -- false, "#2: Value is not a positive number"
Rules.All(...: RuleValidator): RuleValidator
Description:
Returns a rule that only passes if all provided rules pass on the same value.
Example Usage:
local PositiveInteger = Rules.All(Rules.Integer, Rules.PositiveNumber)
print(PositiveInteger(10)) -- true, ""
print(PositiveInteger(-10)) -- false, "Value is not a positive number"
print(PositiveInteger(3.14)) -- false, "Value is not an integer"
Rules.Vararg(Validator: RuleValidator): VarargRuleValidator
Description:
Applies the same rule to every argument provided. All must pass.
Example Usage:
local AllPositive = Rules.Vararg(Rules.PositiveNumber)
print(AllPositive(1, 2, 3)) -- true, ""
print(AllPositive(5, -2)) -- false, "#2: Value is not a positive number"
Rules.Union(...: RuleValidator): RuleValidator
Description:
Returns a rule that passes if at least one rule passes. Fails only if none of the rules succeed.
Example Usage:
local StringOrNumber = Rules.Union(Rules..String), Rules.Number))
print(StringOrNumber("hello")) -- true, ""
print(StringOrNumber(42)) -- true, ""
print(StringOrNumber({})) -- false, "Expected type string or number"
Rules.Wrapped(Callback, Validator): Function
Description:
Wraps a function with validation logic. If validation fails, it throws an error instead of running the function.
Example Usage:
local Add = Rules.Wrapped(function(A, B)
return A + B
end, Rules.Set(Rules.Number, Rules.Number))
print(Add(3, 4)) -- 7
Add("A", 5) -- error: "#1: Value is not a number"
Rules.Custom(Name: string, Validator: RuleValidator, Message?: string, InversedMessage?: string)
Description:
Registers a new named rule. If messages are given, it becomes inversible.
Example Usage:
Rules.Custom("EvenNumber", function(Value: unknown): (boolean, string)
local IsNumber, Message = Rules.RealNumber(Value)
if not IsNumber then
return false, Message
end
if (Value :: number) % 2 ~= 0 then
return false, "Value is not even"
end
return true, ""
end, "Value is not even", "Value is even")
print(Rules.EvenNumber(4)) -- true, ""
print(Rules.EvenNumber(3)) -- false, "Value is not even"
Rules.Strict(Validator: RuleValidator): (Value: unknown) -> boolean
Description:
Wraps a validator and asserts success. If it fails, throws an error instead of returning false.
Example Usage:
local StrictString = Rules.Strict(Rules.String))
StrictString("text") -- true
StrictString(42) -- error: "Expected type string"
Rules.Clean(Validator: RuleValidator): (Value: unknown) -> (boolean, string)
Description:
Wraps a validator and guarantees a message is always returned. If validation succeeds, it returns an empty message.
Example Usage:
local CleanRule = Rules.Clean(Rules.PositiveNumber)
print(CleanRule(100)) -- true, ""
print(CleanRule(-5)) -- false, "Value is not a positive number"
Do’s:
- Cache Rules: Instead of creating new rules every time, cache frequently used rules. This helps stop memory leaks.
- Reuse Existing Rules: Use existing rules or create shared rule sets that can be reused across different parts of your code.
- Define Common Rules Early: Define general purpose rules (like type checks or range checks) once at the beginning and use them throughout your code.
- Limit Rule Creation: Try to create rules only when absolutely necessary, and group similar rules together.
Don’ts:
- Avoid Recreating Rules: Do not constantly recreate rules on the fly. It causes performance issues and possibly memory leaks.
- Avoid Overcomplicating Rules: Don’t make rules too complex by mixing too many checks into a single rule. Keep rules focused on one responsibility.
- Don’t Ignore Rule Maintenance: When a project grows, keep track of rules and update them to keep your sanity by making your rules easy to manage and update.
Download
Rules.rbxm (9.6 KB)
Code
--!strict
type RuleValidator = (Value: unknown) -> (boolean, string)
type VarargRuleValidator = (...unknown) -> (boolean, string)
local Rules = {}
local InversedMessages: {[string]: string} = {}
local CanBeInversed: {[RuleValidator]: boolean} = {}
local function InversibleRule(Rule: RuleValidator, Message: string, InversedMessage: string)
InversedMessages[Message] = InversedMessage
CanBeInversed[Rule] = true
return Rule
end
local function FromType(Type: string): RuleValidator
local Message = `Value isn't of type '{Type}'`
local Inversed = `Value cannot be of type '{Type}'`
return InversibleRule(function(Value: unknown)
return type(Value) == Type, Message
end, Message, Inversed)
end
local function FromClass(Type: string): RuleValidator
local Message = `Value isn't of type '{Type}'`
local Inversed = `Value cannot be of type '{Type}'`
return InversibleRule(function(Value: unknown)
return typeof(Value) == Type, Message
end, Message, Inversed)
end
do -- Basic
Rules.void = function(...: unknown)
local Success = select("#", ...) == 0
return Success, "Value isn't void"
end :: RuleValidator
Rules.Void = Rules.void
Rules.none = function(Value: unknown)
local Success = Value == nil
return Success, "Value isn't nil"
end :: RuleValidator
Rules.None = Rules.none
Rules.optional = function(Validator: RuleValidator): RuleValidator
return function(Value: unknown)
if Value == nil then
return true, ""
end
return Validator(Value)
end
end
Rules.Optional = Rules.optional
Rules.any = function(Value: unknown)
return Value ~= nil, "Value is nil"
end
Rules.Any = Rules.any
Rules.literal = function(Expected: any)
assert(Expected ~= nil, "Expected cannot be nil")
local Message = `Value is not {Expected}`
local Inversed = `Value is {Expected}`
return InversibleRule(function(Value: unknown)
local IsNone, TypeMessage = Rules.none(Value)
if IsNone then
return false, TypeMessage
end
return Value == Expected, `Value is not {Expected}`
end, Message, Inversed)
end
Rules.Literal = Rules.literal
end
do -- Primitive Validators
Rules.string = FromType("string")
Rules.String = Rules.string
Rules.number = FromType("number")
Rules.Number = Rules.number
Rules.real_number = function(Value: unknown)
local CorrectType, Message = Rules.number(Value)
if not CorrectType then
return false, Message
end
if Value ~= Value then
return false, "Value must not be NaN"
end
return math.abs(Value :: number) ~= math.huge, "Value must not be infinite"
end
Rules.realnumber = Rules.real_number
Rules.RealNumber = Rules.real_number
Rules.strict_number = function(Value: unknown)
local CorrectType, Message = Rules.number(Value)
if not CorrectType then
return false, Message
end
return Value == Value, "Value must not be NaN"
end
Rules.strictnumber = Rules.strict_number
Rules.StrictNumber = Rules.strict_number
Rules.table = FromType("table")
Rules.Table = Rules.table
Rules.boolean = FromType("boolean")
Rules.Boolean = Rules.boolean
Rules.thread = FromType("thread")
Rules.Thread = Rules.thread
Rules.userdata = FromType("userdata")
Rules.Userdata = Rules.userdata
Rules.buffer = FromType("buffer")
Rules.Buffer = Rules.buffer
Rules.lua_userdata = FromClass("userdata")
Rules.luauserdata = Rules.lua_userdata
Rules.LuaUserdata = Rules.lua_userdata
Rules.vector = FromType("vector")
Rules.Vector = Rules.vector
Rules.callback = FromType("function")
Rules.Callback = Rules.callback
Rules.cclosure = function(Value: unknown)
local CorrectType, TypeMessage = Rules.callback(Value)
if not CorrectType then
return false, TypeMessage
end
local Source = debug.info(Value :: () -> (), "s")
return Source == "[C]", "Function is not a CClosure"
end
Rules.CClosure = Rules.cclosure
end
do -- Class Validator
Rules.axes = FromClass("Axes")
Rules.Axes = Rules.axes
Rules.brick_color = FromClass("BrickColor")
Rules.brickcolor = Rules.brick_color
Rules.BrickColor = Rules.brick_color
Rules.catalog_search_params = FromClass("CatalogSearchParams")
Rules.catalogsearchparams = Rules.catalog_search_params
Rules.CatalogSearchParams = Rules.catalog_search_params
Rules.cframe = FromClass("CFrame")
Rules.CFrame = Rules.cframe
Rules.color3 = FromClass("Color3")
Rules.Color3 = Rules.color3
Rules.color_sequence = FromClass("ColorSequence")
Rules.colorsequence = Rules.color_sequence
Rules.ColorSequence = Rules.color_sequence
Rules.color_sequence_keypoint = FromClass("ColorSequenceKeypoint")
Rules.colorsequencekeypoint = Rules.color_sequence_keypoint
Rules.ColorSequenceKeypoint = Rules.color_sequence_keypoint
Rules.date_time = FromClass("DateTime")
Rules.datetime = Rules.date_time
Rules.DateTime = Rules.date_time
Rules.dock_widget_plugin_gui_info = FromClass("DockWidgetPluginGuiInfo")
Rules.dockwidgetpluginguiinfo = Rules.dock_widget_plugin_gui_info
Rules.DockWidgetPluginGuiInfo = Rules.dock_widget_plugin_gui_info
Rules.faces = FromClass("Faces")
Rules.Faces = Rules.faces
Rules.float_curve_key = FromClass("FloatCurveKey")
Rules.floatcurvekey = Rules.float_curve_key
Rules.FloatCurveKey = Rules.float_curve_key
Rules.font = FromClass("Font")
Rules.Font = Rules.font
Rules.instance = FromClass("Instance")
Rules.Instance = Rules.instance
Rules.number_range = FromClass("NumberRange")
Rules.numberrange = Rules.number_range
Rules.NumberRange = Rules.number_range
Rules.number_sequence = FromClass("NumberSequence")
Rules.numbersequence = Rules.number_sequence
Rules.NumberSequence = Rules.number_sequence
Rules.number_sequence_keypoint = FromClass("NumberSequenceKeypoint")
Rules.numbersequencekeypoint = Rules.number_sequence_keypoint
Rules.NumberSequenceKeypoint = Rules.number_sequence_keypoint
Rules.overlap_params = FromClass("OverlapParams")
Rules.overlapparams = Rules.overlap_params
Rules.OverlapParams = Rules.overlap_params
Rules.path_waypoint = FromClass("PathWaypoint")
Rules.pathwaypoint = Rules.path_waypoint
Rules.PathWaypoint = Rules.path_waypoint
Rules.physical_properties = FromClass("PhysicalProperties")
Rules.physicalproperties = Rules.physical_properties
Rules.PhysicalProperties = Rules.physical_properties
Rules.random = FromClass("Random")
Rules.Random = Rules.random
Rules.ray = FromClass("Ray")
Rules.Ray = Rules.ray
Rules.raycast_params = FromClass("RaycastParams")
Rules.raycastparams = Rules.raycast_params
Rules.RaycastParams = Rules.raycast_params
Rules.raycast_result = FromClass("RaycastResult")
Rules.raycastresult = Rules.raycast_result
Rules.RaycastResult = Rules.raycast_result
Rules.rbx_script_connection = FromClass("RBXScriptConnection")
Rules.rbxscriptconnection = Rules.rbx_script_connection
Rules.RBXScriptConnection = Rules.rbx_script_connection
Rules.rbx_script_signal = FromClass("RBXScriptSignal")
Rules.rbxscriptsignal = Rules.rbx_script_signal
Rules.RBXScriptSignal = Rules.rbx_script_signal
Rules.rect = FromClass("Rect")
Rules.Rect = Rules.rect
Rules.region3 = FromClass("Region3")
Rules.Region3 = Rules.region3
Rules.region3int16 = FromClass("Region3int16")
Rules.Region3int16 = Rules.region3int16
Rules.tween_info = FromClass("TweenInfo")
Rules.tweeninfo = Rules.tween_info
Rules.TweenInfo = Rules.tween_info
Rules.udim = FromClass("UDim")
Rules.UDim = Rules.udim
Rules.udim2 = FromClass("UDim2")
Rules.UDim2 = Rules.udim2
Rules.vector2 = FromClass("Vector2")
Rules.Vector2 = Rules.vector2
Rules.vector2int16 = FromClass("Vector2int16")
Rules.Vector2int16 = Rules.vector2int16
Rules.vector3 = FromClass("Vector3")
Rules.Vector3 = Rules.vector3
Rules.vector3int16 = FromClass("Vector3int16")
Rules.Vector3int16 = Rules.vector3int16
end
do -- Type Variant Validator
Rules.array = InversibleRule(function(Value: unknown)
local CorrectType, TypeMessage = Rules.table(Value)
if not CorrectType then
return false, TypeMessage
end
local Value = Value :: any
local Key = next(Value, table.maxn(Value))
local Success = Key == nil
return Success, "Value is not an array"
end, "Value is not an array", "Value cannot be an array")
Rules.Array = Rules.array
-- Ensures that ipairs will fully loop through the array
Rules.strict_array = function(Value: unknown)
local CorrectType, TypeMessage = Rules.table(Value)
if not CorrectType then
return false, TypeMessage
end
local Value = Value :: any
local Elements = table.maxn(Value)
if Elements == 0 then
if next(Value) ~= nil then
return false, "Value is not an array"
end
return true, ""
end
local Key = next(Value, Elements)
if Key ~= nil then
return false, "Value is not an array"
end
local CurrentKey: number?, KeyValue: any = next(Value)
local LastKey: number? = nil
while CurrentKey do
if CurrentKey % 1 ~= 0 then
return false, "Value must not have fractional indices"
end
if KeyValue == nil and CurrentKey ~= Elements then
return false, "Value must not have nil values inbetween or at the start"
end
if LastKey ~= nil and CurrentKey ~= LastKey + 1 then
return false, "Value must not have gaps in indices or negative indices"
end
LastKey = CurrentKey
CurrentKey, KeyValue = next(Value, CurrentKey)
end
local FirstKey = next(Value)
if FirstKey ~= 1 then
return false, "Value must not have nil values inbetween or at the start"
end
return true, ""
end :: RuleValidator
Rules.strictarray = Rules.strict_array
Rules.StrictArray = Rules.strict_array
Rules.frozen = InversibleRule(function(Value: unknown)
local CorrectType, TypeMessage = Rules.table(Value)
if not CorrectType then
return false, TypeMessage
end
return table.isfrozen(Value :: any), "Table is not frozen"
end, "Table is not frozen", "Table must be frozen")
Rules.Frozen = Rules.frozen
end
do -- Enums
Rules.enum = function(Value: unknown)
return typeof(Value) == "Enum", "Value is not an Enum"
end :: RuleValidator
Rules.Enum = Rules.enum
Rules.enum_item = function(Value: unknown)
return typeof(Value) == "EnumItem", "Value is not an EnumItem"
end :: RuleValidator
Rules.enumitem = Rules.enum_item
Rules.EnumItem = Rules.enum_item
Rules.from_enum = function(EnumType: Enum): RuleValidator
return function(Value: unknown)
local CorrectType, TypeMessage = Rules.enum_item(Value)
if not CorrectType then
return false, TypeMessage
end
local Success = (Value :: EnumItem).EnumType == EnumType
return Success, `Value is not of enum type '{EnumType}'`
end
end
Rules.fromenum = Rules.from_enum
Rules.FromEnum = Rules.from_enum
end
do -- Values
Rules.class_name = function(ClassName: string): RuleValidator
local Vowels = { a = true, e = true, i = true, o = true, u = true }
local Article = Vowels[ClassName:sub(1, 1):lower()] and "an" or "a"
return function(Value: unknown)
local CorrectType, TypeMessage = Rules.instance(Value)
if not CorrectType then
return false, TypeMessage
end
local Success = (Value :: Instance):IsA(ClassName)
return Success, `Value is not {Article} {ClassName}`
end
end
Rules.classname = Rules.class_name
Rules.ClassName = Rules.class_name
Rules.valid_utf8 = function(Value: unknown)
local CorrectType, TypeMessage = Rules.string(Value)
if not CorrectType then
return false, TypeMessage
end
local Success = (utf8.len(Value :: string)) ~= nil
return Success, "Value is not valid UTF8"
end :: RuleValidator
Rules.validutf8 = Rules.valid_utf8
Rules.ValidUTF8 = Rules.valid_utf8
Rules.nan = function(Value: unknown)
local CorrectType, TypeMessage = Rules.number(Value)
if not CorrectType then
return false, TypeMessage
end
local Success = Value ~= Value
return Success, "Value isn't NaN"
end :: RuleValidator
Rules.NaN = Rules.nan
Rules.infinite = function(Value: unknown)
local CorrectType, TypeMessage = Rules.number(Value)
if not CorrectType then
return false, TypeMessage
end
local Success = Value == math.huge
return Success, "Value is not infinite"
end :: RuleValidator
Rules.finite_cframe = function(Value: unknown)
local CorrectType, TypeMessage = Rules.cframe(Value)
if not CorrectType then
return false, TypeMessage
end
for _, Axis in { "X", "Y", "Z" } do
if not Rules.real_number((Value :: any)[Axis]) then
return false, "CFrame is out of world bounds"
end
end
local Value = Value :: CFrame
local RotationMatrix = table.pack(Value:GetComponents())
for Index = 1, RotationMatrix.n do
local AxisValue = RotationMatrix[Index]
if not Rules.real_number(AxisValue) then
return false, "CFrame is out of world bounds"
end
end
return true, ""
end
Rules.finitecframe = Rules.finite_cframe
Rules.FiniteCFrame = Rules.finite_cframe
Rules.values = function(Validator: RuleValidator, TreatTablesAsValues: boolean?, Recursive: boolean?): RuleValidator
return function(Value: unknown)
local CorrectType, TypeMessage = Rules.table(Value)
if not CorrectType then
return false, TypeMessage
end
local Seen = {}
setmetatable(Seen, { __mode = "k" })
local Value = Value :: {}
local Success: boolean, Message: string = true, ""
local function ValidateTable(Table: {}, Parent: {string})
for Key, TableValue in Table do
if not Success then
break
end
if type(TableValue) == "table" then
if Recursive == true and not Seen[TableValue] and TreatTablesAsValues ~= true then
Seen[TableValue] = true
local NewParent = table.clone(Parent)
table.insert(NewParent, Key :: string)
ValidateTable(TableValue :: {}, NewParent)
end
if not TreatTablesAsValues then
continue
end
end
if not Success then
break
end
local ThisSuccess, ThisMessage = Validator(TableValue)
if not ThisSuccess then
Success, Message = ThisSuccess, `{if #Parent > 0 then table.concat(Parent, ".") .. "." else ""}{Key}: {ThisMessage}`
break
end
end
end
ValidateTable(Value, {})
return Success, Message
end
end
Rules.Values = Rules.values
Rules.keys = function(Validator: RuleValidator, Recursive: boolean?): RuleValidator
return function(Value: unknown)
local CorrectType, TypeMessage = Rules.table(Value)
if not CorrectType then
return false, TypeMessage
end
local Seen = {}
setmetatable(Seen, { __mode = "k" })
local Value = Value :: {}
local Success: boolean, Message: string = true, ""
local function ValidateTable(Table: {}, Parent: {string})
for Key, TableValue in Table do
if not Success then
break
end
if type(TableValue) == "table" then
if Recursive == true and not Seen[TableValue] then
Seen[TableValue] = true
local NewParent = table.clone(Parent)
table.insert(NewParent, Key :: string)
ValidateTable(TableValue :: {}, NewParent)
end
end
if not Success then
break
end
local ThisSuccess, ThisMessage = Validator(Key)
if not ThisSuccess then
Success, Message = ThisSuccess, `{if #Parent > 0 then table.concat(Parent, ".") .. "." else ""}{Key}: {ThisMessage}`
break
end
end
end
ValidateTable(Value, {})
return Success, Message
end
end
Rules.Keys = Rules.keys
Rules.string_length = function(MaxLength: number): RuleValidator
local Message = `Value is longer than {MaxLength} characters`
local Inversed = `Value is shorter than {MaxLength} characters`
return InversibleRule(function(Value: unknown)
local CorrectType, TypeMessage = Rules.string(Value)
if not CorrectType then
return false, TypeMessage
end
local Success = string.len(Value :: string) <= MaxLength
return Success, `Value is longer than {MaxLength} characters`
end, Message, Inversed)
end
Rules.stringlength = Rules.string_length
Rules.StringLength = Rules.string_length
Rules.table_length = function(MaxLength: number, Array: boolean?) : RuleValidator
local Message = `Value has more than {MaxLength} elements`
local Inversed = `Value has less than {MaxLength} elements`
return InversibleRule(function(Value: unknown)
local CorrectType, TypeMessage = Rules.table(Value)
if not CorrectType then
return false, TypeMessage
end
if Array then
local Success = table.maxn(Value :: {}) <= MaxLength
return Success, Message
end
local Count = 0
for _ in Value :: {} do Count += 1 end
local Success = Count <= MaxLength
return Success, Message
end, Message, Inversed)
end
Rules.tablelength = Rules.table_length
Rules.TableLength = Rules.table_length
Rules.struct = function(TableStructure: {[any]: RuleValidator}, Strict: boolean?): RuleValidator
return function(Value: unknown)
local CorrectType, TypeMessage = Rules.table(Value)
if not CorrectType then
return false, TypeMessage
end
local Value = Value :: {}
if Strict == true then
for Key, TableValue in Value :: {} do
if TableStructure[Key] then
continue
end
return false, `Value has unexpected field '{Key}'`
end
end
for Key, RuleValidator in TableStructure do
local TableValue = Value[Key]
if not TableValue and Strict == true then
return false, `Value has missing field '{Key}'`
end
if type(RuleValidator) ~= "function" then
return false, `{Key}: Expected rule, got {typeof(RuleValidator)}`
end
local Success, Message = RuleValidator(TableValue)
if not Success then
return false, `{Key}: {Message}`
end
end
return true, ""
end
end
Rules.Struct = Rules.struct
Rules.depth = function(MaxDepth: number): RuleValidator
return function(Value: unknown)
local CorrectType, TypeMessage = Rules.table(Value)
if not CorrectType then
return false, TypeMessage
end
local MaxDepthRecorded = 1
local Seen = {}
setmetatable(Seen, { __mode = "k" })
local function Search(Table: {}, Depth: number)
if Seen[Table] then
return
end
if Depth > MaxDepthRecorded then
MaxDepthRecorded = Depth
end
for Key, Value in Table do
if type(Key) == "table" then
Search(Key :: {}, Depth + 1)
end
if type(Value) == "table" then
Search(Value :: {}, Depth + 1)
end
end
end
table.clear(Seen :: any)
Search(Value :: {}, MaxDepthRecorded)
local Success = MaxDepthRecorded <= MaxDepth
return Success, `Value exceeds max depth of {MaxDepth}`
end
end
Rules.Depth = Rules.depth
Rules.match = function(Expression: string)
local Message = `Value doesn't match the regex expression '{Expression}'`
local Inversed = `Value must not match the regex expression '{Expression}'`
return InversibleRule(function(Value: unknown)
local CorrectType, TypeMessage = Rules.string(Value)
if not CorrectType then
return false, TypeMessage
end
local Matches = string.match(Value :: string, Expression) ~= nil
return Matches, Message
end, Message, Inversed)
end
Rules.Match = Rules.match
Rules.integer = InversibleRule(function(Value: unknown)
local CorrectType, TypeMessage = Rules.strict_number(Value)
if not CorrectType then
return false, TypeMessage
end
if Rules.infinite(Value) then
return false, "Value is infinite"
end
local Integral, Fractional = math.modf(Value :: number)
local Success = Fractional == 0
return Success, "Value is not an integer"
end, "Value is not an integer", "Value cannot be an integer")
Rules.Integer = Rules.integer
Rules.positive_number = InversibleRule(function(Value: unknown)
local CorrectType, TypeMessage = Rules.strict_number(Value)
if not CorrectType then
return false, TypeMessage
end
local Success = (Value :: number) >= 0
return Success, "Value is not positive"
end, "Value is not positive", "Value cannot be positive")
Rules.positivenumber = Rules.positive_number
Rules.PositiveNumber = Rules.positive_number
Rules.negative_number = InversibleRule(function(Value: unknown)
local CorrectType, TypeMessage = Rules.strict_number(Value)
if not CorrectType then
return false, TypeMessage
end
local Success = (Value :: number) < 0
return Success, "Value is not negative"
end, "Value is not negative", "Value cannot be negative")
Rules.negativenumber = Rules.negative_number
Rules.NegativeNumber = Rules.negative_number
Rules.in_range_inclusive = function(Min: number, Max: number): RuleValidator
local Message = `Value is outside the range of {Min}..{Max}`
local Inversed = `Value must be outside the range of {Min}..{Max}`
return InversibleRule(function(Value: unknown)
local CorrectType, TypeMessage = Rules.strict_number(Value)
if not CorrectType then
return false, TypeMessage
end
local Success = (Value :: number) >= Min and (Value :: number) <= Max
return Success, Message
end, Message, Inversed)
end
Rules.inrangeinclusive = Rules.in_range_inclusive
Rules.InRangeInclusive = Rules.in_range_inclusive
Rules.in_range_exclusive = function(Min: number, Max: number)
local Message = `Value is outside the range of {Min + 1}..{Max - 1}`
local Inversed = `Value must be outside the range of {Min + 1}..{Max - 1}`
return InversibleRule(function(Value: unknown)
local CorrectType, TypeMessage = Rules.strict_number(Value)
if not CorrectType then
return false, TypeMessage
end
local Success = (Value :: number) > Min and (Value :: number) < Max
return Success, Message
end, Message, Inversed)
end
Rules.inrangeexclusive = Rules.in_range_exclusive
Rules.InRangeExclusive = Rules.in_range_exclusive
Rules.minimum = function(Min: number)
return Rules.in_range_inclusive(Min, math.huge)
end
Rules.Minimum = Rules.minimum
Rules.maximum = function(Max: number)
return Rules.in_range_inclusive(-math.huge, Max)
end
Rules.Maximum = Rules.maximum
Rules.epsilon = function(Number: number, Epsilon: number)
local Message = `Value is not within {Epsilon} of {Number}`
local Inversed = `Value cannot be within {Epsilon} of {Number}`
return InversibleRule(function(Value: unknown)
local CorrectType, TypeMessage = Rules.strict_number(Value)
if not CorrectType then
return false, TypeMessage
end
local Success = math.abs(Number - (Value :: number)) <= Epsilon
return Success, Message
end, Message, Inversed)
end
Rules.Epsilon = Rules.epsilon
Rules.descendant_of = function(Inst: Instance)
local Message = `Value is not a descendant of {Inst.Name}`
local Inversed = `Value cannot be a descendant of {Inst.Name}`
return InversibleRule(function(Value: unknown)
local CorrectType, TypeMessage = Rules.instance(Value)
if not CorrectType then
return false, TypeMessage
end
if not Inst or Inst.Parent == nil then
return false, "Validators::DescendantOf: Inst is nil or parented to nil!"
end
local Success = (Value :: Instance):IsDescendantOf(Inst)
return Success, Message
end, Message, Inversed)
end
Rules.descendantof = Rules.descendant_of
Rules.DescendantOf = Rules.descendant_of
end
Rules.inverse = function(Rule: RuleValidator | string): RuleValidator
if type(Rule) == "string" then
local Name = Rule
Rule = Rules[Name]
assert(Rule and Name ~= "inverse", `No rule called '{Name}'`)
assert(CanBeInversed[Rule :: any], `Rule '{Name}' can't be inversed`)
end
local Rule = Rule :: RuleValidator
-- Most rules don't benefit from this
assert(CanBeInversed[Rule], `Provided rule can't be inversed`)
return function(Value: unknown)
local Success, Message = (Rule :: RuleValidator)(Value)
local InversedMessage = InversedMessages[Message]
local ShouldInverse = InversedMessage ~= nil
return if ShouldInverse then not Success else Success, InversedMessage or Message
end
end
Rules.Inverse = Rules.inverse
Rules.set = function(...: RuleValidator): VarargRuleValidator
local RuleSet = table.pack(...)
local ExpectedArgumentCount = RuleSet.n
return function(...: unknown)
local Arguments = table.pack(...)
local Count = Arguments.n
if Count ~= ExpectedArgumentCount then
return false, if Count > ExpectedArgumentCount then `Got more than {ExpectedArgumentCount} arguments` else `Got less than {ExpectedArgumentCount} arguments`
end
for Index = 1, Count do
local Argument = Arguments[Index]
local ArgumentValidator = RuleSet[Index]
if type(ArgumentValidator) ~= "function" then
return false, `#{Index}: Expected rule, got {typeof(ArgumentValidator)}`
end
local Success, Message = ArgumentValidator(Argument)
if not Success then
return false, `#{Index}: {Message}`
end
end
return true, ""
end
end
Rules.Set = Rules.set
Rules.all = function(...: RuleValidator): RuleValidator
local RuleSet = { ... }
return function(Value: unknown)
for Index, RuleValidator in RuleSet do
if type(RuleValidator) ~= "function" then
return false, `#{Index}: Expected rule, got {typeof(RuleValidator)}`
end
local Success, Message = RuleValidator(Value)
if not Success then
return false, Message
end
end
return true, ""
end
end
Rules.All = Rules.all
Rules.var_arg = function(Validator: RuleValidator): VarargRuleValidator
assert(Rules.callback(Validator))
return function(...: unknown)
local Arguments = table.pack(...)
local Count = Arguments.n
for Index = 1, Count do
local Value = Arguments[Index]
local Success, Message = Validator(Value)
if not Success then
return false, `#{Index}: {Message}`
end
end
return true, ""
end
end
Rules.vararg = Rules.var_arg
Rules.Vararg = Rules.var_arg
Rules.union = function(...: RuleValidator): RuleValidator
local RuleSet = { ... }
return function(Value: unknown)
local FirstMessage: string
for Index, RuleValidator in RuleSet do
if type(RuleValidator) ~= "function" then
return false, `#{Index}: Provided validator is not a function`
end
local Success, Message = RuleValidator(Value)
if not Success then
if not FirstMessage then
FirstMessage = Message
end
else
return true, ""
end
end
return false, FirstMessage
end
end
Rules.Union = Rules.union
Rules.wrapped = function<Args..., Rets...>(Callback: (Args...) -> (Rets...), Validator: RuleValidator): (Args...) -> (Rets...)
return function(...: Args...)
local Success, Message = Validator(...)
if not Success then
assert(false, Message)
end
return Callback(...)
end
end
Rules.Wrapped = Rules.wrapped
Rules.custom = function(Name: string, CustomValidator: RuleValidator, Message: string?, Inversed: string?)
if Message and Inversed then
Rules[Name] = InversibleRule(CustomValidator, Message, Inversed)
return
end
Rules[Name] = CustomValidator
end
Rules.Custom = Rules.custom
Rules.strict = function(Validator: RuleValidator): (Value: unknown) -> boolean
return function(Value: unknown)
assert(Validator(Value))
return true
end
end
Rules.Strict = Rules.strict
Rules.clean = function(Validator: RuleValidator): (Value: unknown) -> boolean
return function(Value: unknown)
local Success, Message = Validator(Value)
return Success, if not Success then Message else ""
end
end
Rules.Clean = Rules.clean
return Rules