Rules - A Runtime Type Checker

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:

  1. Cache Rules: Instead of creating new rules every time, cache frequently used rules. This helps stop memory leaks.
  2. Reuse Existing Rules: Use existing rules or create shared rule sets that can be reused across different parts of your code.
  3. Define Common Rules Early: Define general purpose rules (like type checks or range checks) once at the beginning and use them throughout your code.
  4. Limit Rule Creation: Try to create rules only when absolutely necessary, and group similar rules together.

Don’ts:

  1. Avoid Recreating Rules: Do not constantly recreate rules on the fly. It causes performance issues and possibly memory leaks.
  2. Avoid Overcomplicating Rules: Don’t make rules too complex by mixing too many checks into a single rule. Keep rules focused on one responsibility.
  3. 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
4 Likes