Repository: diegoholiveira/jsonlogic
Pull request #107 (2025-02-26 22:57:38)

create the `typing` package with types conversion helpers


Patch:
diff --git a/arrays.go b/arrays.go
index 30c6feb..1c2f7ac 100644
--- a/arrays.go
+++ b/arrays.go
@@ -2,6 +2,8 @@ package jsonlogic
 
 import (
 	"fmt"
+
+	"github.com/diegoholiveira/jsonlogic/v3/internal/typing"
 )
 
 type ErrReduceDataType struct {
@@ -17,11 +19,11 @@ func filter(values, data any) any {
 
 	var subject any
 
-	if isSlice(parsed[0]) {
+	if typing.IsSlice(parsed[0]) {
 		subject = parsed[0]
 	}
 
-	if isMap(parsed[0]) {
+	if typing.IsMap(parsed[0]) {
 		subject = apply(parsed[0], data)
 	}
 
@@ -36,7 +38,7 @@ func filter(values, data any) any {
 	for _, value := range subject.([]any) {
 		v := parseValues(logic, value)
 
-		if isTrue(v) {
+		if typing.IsTrue(v) {
 			result = append(result, value)
 		}
 	}
@@ -49,11 +51,11 @@ func _map(values, data any) any {
 
 	var subject any
 
-	if isSlice(parsed[0]) {
+	if typing.IsSlice(parsed[0]) {
 		subject = parsed[0]
 	}
 
-	if isMap(parsed[0]) {
+	if typing.IsMap(parsed[0]) {
 		subject = apply(parsed[0], data)
 	}
 
@@ -68,7 +70,7 @@ func _map(values, data any) any {
 	for _, value := range subject.([]any) {
 		v := parseValues(logic, value)
 
-		if isTrue(v) || isNumber(v) || isBool(v) {
+		if typing.IsTrue(v) || typing.IsNumber(v) || typing.IsBool(v) {
 			result = append(result, v)
 		}
 	}
@@ -81,11 +83,11 @@ func reduce(values, data any) any {
 
 	var subject any
 
-	if isSlice(parsed[0]) {
+	if typing.IsSlice(parsed[0]) {
 		subject = parsed[0]
 	}
 
-	if isMap(parsed[0]) {
+	if typing.IsMap(parsed[0]) {
 		subject = apply(parsed[0], data)
 	}
 
@@ -100,18 +102,18 @@ func reduce(values, data any) any {
 
 	{
 		initialValue := parsed[2]
-		if isMap(initialValue) {
+		if typing.IsMap(initialValue) {
 			initialValue = apply(initialValue, data)
 		}
 
-		if isBool(initialValue) {
-			accumulator = isTrue(initialValue)
+		if typing.IsBool(initialValue) {
+			accumulator = typing.IsTrue(initialValue)
 			valueType = "bool"
-		} else if isNumber(initialValue) {
-			accumulator = toNumber(initialValue)
+		} else if typing.IsNumber(initialValue) {
+			accumulator = typing.ToNumber(initialValue)
 			valueType = "number"
-		} else if isString(initialValue) {
-			accumulator = toString(initialValue)
+		} else if typing.IsString(initialValue) {
+			accumulator = typing.ToString(initialValue)
 			valueType = "string"
 		} else {
 			panic(ErrReduceDataType{
@@ -137,11 +139,11 @@ func reduce(values, data any) any {
 
 		switch context["valueType"] {
 		case "bool":
-			context["accumulator"] = isTrue(v)
+			context["accumulator"] = typing.IsTrue(v)
 		case "number":
-			context["accumulator"] = toNumber(v)
+			context["accumulator"] = typing.ToNumber(v)
 		case "string":
-			context["accumulator"] = toString(v)
+			context["accumulator"] = typing.ToString(v)
 		}
 	}
 
diff --git a/comp.go b/comp.go
index 842b03c..04e57f9 100644
--- a/comp.go
+++ b/comp.go
@@ -4,6 +4,7 @@ import (
 	"reflect"
 
 	"github.com/diegoholiveira/jsonlogic/v3/internal/javascript"
+	"github.com/diegoholiveira/jsonlogic/v3/internal/typing"
 )
 
 // less reference javascript implementation
@@ -11,8 +12,8 @@ import (
 func less(a, b any) bool {
 	// If both values are strings, they are compared as strings,
 	// based on the values of the Unicode code points they contain.
-	if isString(a) && isString(b) {
-		return toString(b) > toString(a)
+	if typing.IsString(a) && typing.IsString(b) {
+		return typing.ToString(b) > typing.ToString(a)
 	}
 
 	// Otherwise the values are compared as numeric values.
@@ -37,7 +38,7 @@ func equals(a, b any) bool {
 		return a == b
 	}
 
-	if isString(a) && isString(b) {
+	if typing.IsString(a) && typing.IsString(b) {
 		return a == b
 	}
 
diff --git a/helpers.go b/helpers.go
deleted file mode 100644
index 09dc834..0000000
--- a/helpers.go
+++ /dev/null
@@ -1,101 +0,0 @@
-package jsonlogic
-
-import (
-	"reflect"
-	"strconv"
-)
-
-func is(obj any, kind reflect.Kind) bool {
-	return obj != nil && reflect.TypeOf(obj).Kind() == kind
-}
-
-func isBool(obj any) bool {
-	return is(obj, reflect.Bool)
-}
-
-func isString(obj any) bool {
-	return is(obj, reflect.String)
-}
-
-func isNumber(obj any) bool {
-	switch obj.(type) {
-	case int, float64:
-		return true
-	default:
-		return false
-	}
-}
-
-func isPrimitive(obj any) bool {
-	return isBool(obj) || isString(obj) || isNumber(obj)
-}
-
-func isMap(obj any) bool {
-	return is(obj, reflect.Map)
-}
-
-func isSlice(obj any) bool {
-	return is(obj, reflect.Slice)
-}
-
-func isEmptySlice(obj any) bool {
-	if !isSlice(obj) {
-		return false
-	}
-
-	for _, v := range obj.([]any) {
-		if isTrue(v) {
-			return false
-		}
-	}
-
-	return true
-}
-
-func isTrue(obj any) bool {
-	if isBool(obj) {
-		return obj.(bool)
-	}
-
-	if isNumber(obj) {
-		return toNumber(obj) != 0
-	}
-
-	if isString(obj) || isSlice(obj) || isMap(obj) {
-		return reflect.ValueOf(obj).Len() > 0
-	}
-
-	return false
-}
-
-func toNumber(value any) float64 {
-	if isString(value) {
-		w, _ := strconv.ParseFloat(value.(string), 64)
-
-		return w
-	}
-
-	switch value := value.(type) {
-	case int:
-		return float64(value)
-	default:
-		return value.(float64)
-	}
-}
-
-func toString(value any) string {
-	if isNumber(value) {
-		switch value := value.(type) {
-		case int:
-			return strconv.FormatInt(int64(value), 10)
-		default:
-			return strconv.FormatFloat(value.(float64), 'f', -1, 64)
-		}
-	}
-
-	if value == nil {
-		return ""
-	}
-
-	return value.(string)
-}
diff --git a/internal/javascript/javascript.go b/internal/javascript/javascript.go
index 5a2fd1e..a42fdf9 100644
--- a/internal/javascript/javascript.go
+++ b/internal/javascript/javascript.go
@@ -1,3 +1,4 @@
+// Package javascript provides utilities for working with JavaScript code and runtime integration.
 package javascript
 
 import (
diff --git a/internal/typing/typing.go b/internal/typing/typing.go
new file mode 100644
index 0000000..37bd818
--- /dev/null
+++ b/internal/typing/typing.go
@@ -0,0 +1,209 @@
+// Package typing provides type checking and conversion utilities for JSON data types.
+package typing
+
+import (
+	"reflect"
+	"strconv"
+)
+
+func is(obj any, kind reflect.Kind) bool {
+	return obj != nil && reflect.TypeOf(obj).Kind() == kind
+}
+
+// IsBool checks if the provided value is a boolean type.
+// Returns false if the value is nil.
+//
+// Example:
+//
+//	IsBool(true)   // Returns: true
+//	IsBool(false)  // Returns: true
+//	IsBool("true") // Returns: false
+//	IsBool(nil)    // Returns: false
+func IsBool(obj any) bool {
+	return is(obj, reflect.Bool)
+}
+
+// IsString checks if the provided value is a string type.
+// Returns false if the value is nil.
+//
+// Example:
+//
+//	IsString("test")  // Returns: true
+//	IsString("")      // Returns: true
+//	IsString(42)      // Returns: false
+//	IsString(nil)     // Returns: false
+func IsString(obj any) bool {
+	return is(obj, reflect.String)
+}
+
+// IsNumber checks if the provided value is a numeric type (int or float64).
+// Returns false for any other type including nil.
+//
+// Example:
+//
+//	IsNumber(42)       // Returns: true
+//	IsNumber(3.14)     // Returns: true
+//	IsNumber("42")     // Returns: false
+//	IsNumber(nil)      // Returns: false
+func IsNumber(obj any) bool {
+	switch obj.(type) {
+	case int, float64:
+		return true
+	default:
+		return false
+	}
+}
+
+// IsPrimitive checks if the provided value is a primitive type (boolean, string, or number).
+// Returns false if the value is nil or any other type.
+//
+// Example:
+//
+//	IsPrimitive(42)      // Returns: true
+//	IsPrimitive("test")  // Returns: true
+//	IsPrimitive(true)    // Returns: true
+//	IsPrimitive([])      // Returns: false
+//	IsPrimitive(nil)     // Returns: false
+func IsPrimitive(obj any) bool {
+	return IsBool(obj) || IsString(obj) || IsNumber(obj)
+}
+
+// IsMap checks if the provided value is a map type.
+// Returns false if the value is nil.
+//
+// Example:
+//
+//	IsMap(map[string]int{"a": 1})  // Returns: true
+//	IsMap(map[string]any{})        // Returns: true
+//	IsMap([]int{1, 2, 3})          // Returns: false
+//	IsMap(nil)                     // Returns: false
+func IsMap(obj any) bool {
+	return is(obj, reflect.Map)
+}
+
+// IsSlice checks if the provided value is a slice type.
+// Returns false if the value is nil.
+//
+// Example:
+//
+//	IsSlice([]int{1, 2, 3})  // Returns: true
+//	IsSlice([]any{})         // Returns: true
+//	IsSlice("test")          // Returns: false
+//	IsSlice(nil)             // Returns: false
+func IsSlice(obj any) bool {
+	return is(obj, reflect.Slice)
+}
+
+// IsEmptySlice checks if the provided value is a slice and all its elements are falsy.
+// Returns false if the value is not a slice or if all elements in the slice are falsy.
+// A falsy value is: false, 0, "", empty array, or empty map.
+//
+// Example:
+//
+//	IsEmptySlice([]any{})             // Returns: true
+//	IsEmptySlice([]any{0, "", false}) // Returns: true
+//	IsEmptySlice([]any{1, 2, 3})      // Returns: false
+//	IsEmptySlice("test")              // Returns: false
+func IsEmptySlice(obj any) bool {
+	if !IsSlice(obj) {
+		return false
+	}
+
+	for _, v := range obj.([]any) {
+		if IsTrue(v) {
+			return false
+		}
+	}
+
+	return true
+}
+
+// IsTrue checks if the provided value is considered truthy in JavaScript logic.
+// For booleans: true is truthy
+// For numbers: non-zero is truthy
+// For strings: non-empty string is truthy
+// For slices/maps: non-empty slice/map is truthy
+// Returns false for nil or any other type.
+//
+// Example:
+//
+//	IsTrue(true)                      // Returns: true
+//	IsTrue(42)                        // Returns: true
+//	IsTrue("test")                    // Returns: true
+//	IsTrue([]any{1, 2, 3})            // Returns: true
+//	IsTrue(false)                     // Returns: false
+//	IsTrue(0)                         // Returns: false
+//	IsTrue("")                        // Returns: false
+//	IsTrue([]any{})                   // Returns: false
+//	IsTrue(nil)                       // Returns: false
+func IsTrue(obj any) bool {
+	if IsBool(obj) {
+		return obj.(bool)
+	}
+
+	if IsNumber(obj) {
+		return ToNumber(obj) != 0
+	}
+
+	if IsString(obj) || IsSlice(obj) || IsMap(obj) {
+		return reflect.ValueOf(obj).Len() > 0
+	}
+
+	return false
+}
+
+// ToNumber converts the provided value to a float64.
+// If the value is a string, it attempts to parse it as a float64.
+// If the value is an int, it converts it to float64.
+// If the value is already a float64, it returns it as is.
+// For all other types, it attempts a type assertion to float64.
+//
+// Example:
+//
+//	ToNumber(42)                 // Returns: 42.0
+//	ToNumber(3.14)               // Returns: 3.14
+//	ToNumber("42")               // Returns: 42.0
+//	ToNumber("3.14")             // Returns: 3.14
+//	ToNumber("invalid")          // Returns: 0.0
+func ToNumber(value any) float64 {
+	if IsString(value) {
+		w, _ := strconv.ParseFloat(value.(string), 64)
+
+		return w
+	}
+
+	switch value := value.(type) {
+	case int:
+		return float64(value)
+	default:
+		return value.(float64)
+	}
+}
+
+// ToString converts the provided value to a string.
+// For numbers: converts to string representation
+// For nil: returns an empty string
+// For other types: performs a direct type assertion to string
+//
+// Example:
+//
+//	ToString(42)        // Returns: "42"
+//	ToString(3.14)      // Returns: "3.14"
+//	ToString("test")    // Returns: "test"
+//	ToString(nil)       // Returns: ""
+func ToString(value any) string {
+	if IsNumber(value) {
+		switch value := value.(type) {
+		case int:
+			return strconv.FormatInt(int64(value), 10)
+		default:
+			return strconv.FormatFloat(value.(float64), 'f', -1, 64)
+		}
+	}
+
+	if value == nil {
+		return ""
+	}
+
+	return value.(string)
+}
diff --git a/jsonlogic.go b/jsonlogic.go
index b4fb0d3..75812e5 100644
--- a/jsonlogic.go
+++ b/jsonlogic.go
@@ -7,6 +7,8 @@ import (
 	"strings"
 
 	"github.com/barkimedes/go-deepcopy"
+
+	"github.com/diegoholiveira/jsonlogic/v3/internal/typing"
 )
 
 type ErrInvalidOperator struct {
@@ -39,11 +41,11 @@ func between(operator string, values []any, data any) any {
 
 func unary(operator string, value any) any {
 	if operator == "+" || operator == "*" || operator == "/" {
-		return toNumber(value)
+		return typing.ToNumber(value)
 	}
 
 	if operator == "-" {
-		return -1 * toNumber(value)
+		return -1 * typing.ToNumber(value)
 	}
 
 	if operator == "!!" {
@@ -54,7 +56,7 @@ func unary(operator string, value any) any {
 		return abs(value)
 	}
 
-	b := isTrue(value)
+	b := typing.IsTrue(value)
 
 	if operator == "!" {
 		return !b
@@ -65,31 +67,32 @@ func unary(operator string, value any) any {
 
 func _and(values, data any) any {
 	values = getValuesWithoutParsing(values, data)
+
 	var v float64
 
 	isBoolExpression := true
 
 	for _, value := range values.([]any) {
 		value = parseValues(value, data)
-		if isSlice(value) {
+		if typing.IsSlice(value) {
 			return value
 		}
 
-		if isBool(value) && !value.(bool) {
+		if typing.IsBool(value) && !value.(bool) {
 			return false
 		}
 
-		if isString(value) && toString(value) == "" {
+		if typing.IsString(value) && typing.ToString(value) == "" {
 			return value
 		}
 
-		if !isNumber(value) {
+		if !typing.IsNumber(value) {
 			continue
 		}
 
 		isBoolExpression = false
 
-		_value := toNumber(value)
+		_value := typing.ToNumber(value)
 
 		if _value > v {
 			v = _value
@@ -108,7 +111,7 @@ func _or(values, data any) any {
 
 	for _, value := range values.([]any) {
 		parsed := parseValues(value, data)
-		if isTrue(parsed) {
+		if typing.IsTrue(parsed) {
 			return parsed
 		}
 	}
@@ -122,11 +125,11 @@ func _inRange(value any, values any) bool {
 	i := v[0]
 	j := v[1]
 
-	if isNumber(value) {
-		return toNumber(value) >= toNumber(i) && toNumber(j) >= toNumber(value)
+	if typing.IsNumber(value) {
+		return typing.ToNumber(value) >= typing.ToNumber(i) && typing.ToNumber(j) >= typing.ToNumber(value)
 	}
 
-	return toString(value) >= toString(i) && toString(j) >= toString(value)
+	return typing.ToString(value) >= typing.ToString(i) && typing.ToString(j) >= typing.ToString(value)
 }
 
 func _in(value any, values any) bool {
@@ -134,16 +137,16 @@ func _in(value any, values any) bool {
 		return false
 	}
 
-	if isString(values) {
+	if typing.IsString(values) {
 		return strings.Contains(values.(string), value.(string))
 	}
 
-	if !isSlice(values) {
+	if !typing.IsSlice(values) {
 		return false
 	}
 
 	for _, element := range values.([]any) {
-		if isSlice(element) {
+		if typing.IsSlice(element) {
 			if _inRange(value, element) {
 				return true
 			}
@@ -151,8 +154,8 @@ func _in(value any, values any) bool {
 			continue
 		}
 
-		if isNumber(value) {
-			if toNumber(element) == value {
+		if typing.IsNumber(value) {
+			if typing.ToNumber(element) == value {
 				return true
 			}
 
@@ -174,10 +177,10 @@ func max(values any) any {
 		return nil
 	}
 
-	bigger := toNumber(converted[0])
+	bigger := typing.ToNumber(converted[0])
 
 	for i := 1; i < size; i++ {
-		_n := toNumber(converted[i])
+		_n := typing.ToNumber(converted[i])
 		if _n > bigger {
 			bigger = _n
 		}
@@ -193,10 +196,10 @@ func min(values any) any {
 		return nil
 	}
 
-	smallest := toNumber(converted[0])
+	smallest := typing.ToNumber(converted[0])
 
 	for i := 1; i < size; i++ {
-		_n := toNumber(converted[i])
+		_n := typing.ToNumber(converted[i])
 		if smallest > _n {
 			smallest = _n
 		}
@@ -208,11 +211,11 @@ func min(values any) any {
 func merge(values any, level int8) any {
 	result := make([]any, 0)
 
-	if isPrimitive(values) || level > 1 {
+	if typing.IsPrimitive(values) || level > 1 {
 		return append(result, values)
 	}
 
-	if isSlice(values) {
+	if typing.IsSlice(values) {
 		for _, value := range values.([]any) {
 			_values := merge(value, level+1).([]any)
 
@@ -224,7 +227,7 @@ func merge(values any, level int8) any {
 }
 
 func conditional(values, data any) any {
-	if isPrimitive(values) {
+	if typing.IsPrimitive(values) {
 		return values
 	}
 
@@ -238,11 +241,11 @@ func conditional(values, data any) any {
 
 	for i := 0; i < length-1; i = i + 2 {
 		v := parsed[i]
-		if isMap(v) {
+		if typing.IsMap(v) {
 			v = getVar(parsed[i], data)
 		}
 
-		if isTrue(v) {
+		if typing.IsTrue(v) {
 			return parseValues(parsed[i+1], data)
 		}
 	}
@@ -259,7 +262,7 @@ func setProperty(value, data any) any {
 
 	object := _value[0]
 
-	if !isMap(object) {
+	if !typing.IsMap(object) {
 		return object
 	}
 
@@ -276,7 +279,7 @@ func setProperty(value, data any) any {
 }
 
 func missing(values, data any) any {
-	if isString(values) {
+	if typing.IsString(values) {
 		values = []any{values}
 	}
 
@@ -295,7 +298,7 @@ func missing(values, data any) any {
 
 func missingSome(values, data any) any {
 	parsed := values.([]any)
-	number := int(toNumber(parsed[0]))
+	number := int(typing.ToNumber(parsed[0]))
 	vars := parsed[1]
 
 	missing := make([]any, 0)
@@ -323,15 +326,15 @@ func all(values, data any) any {
 
 	var subject any
 
-	if isMap(parsed[0]) {
+	if typing.IsMap(parsed[0]) {
 		subject = apply(parsed[0], data)
 	}
 
-	if isSlice(parsed[0]) {
+	if typing.IsSlice(parsed[0]) {
 		subject = parsed[0]
 	}
 
-	if !isTrue(subject) {
+	if !typing.IsTrue(subject) {
 		return false
 	}
 
@@ -339,7 +342,7 @@ func all(values, data any) any {
 		conditions := solveVars(parsed[1], value)
 		v := apply(conditions, value)
 
-		if !isTrue(v) {
+		if !typing.IsTrue(v) {
 			return false
 		}
 	}
@@ -352,15 +355,15 @@ func none(values, data any) any {
 
 	var subject any
 
-	if isMap(parsed[0]) {
+	if typing.IsMap(parsed[0]) {
 		subject = apply(parsed[0], data)
 	}
 
-	if isSlice(parsed[0]) {
+	if typing.IsSlice(parsed[0]) {
 		subject = parsed[0]
 	}
 
-	if !isTrue(subject) {
+	if !typing.IsTrue(subject) {
 		return true
 	}
 
@@ -369,7 +372,7 @@ func none(values, data any) any {
 	for _, value := range subject.([]any) {
 		v := apply(conditions, value)
 
-		if isTrue(v) {
+		if typing.IsTrue(v) {
 			return false
 		}
 	}
@@ -382,15 +385,15 @@ func some(values, data any) any {
 
 	var subject any
 
-	if isMap(parsed[0]) {
+	if typing.IsMap(parsed[0]) {
 		subject = apply(parsed[0], data)
 	}
 
-	if isSlice(parsed[0]) {
+	if typing.IsSlice(parsed[0]) {
 		subject = parsed[0]
 	}
 
-	if !isTrue(subject) {
+	if !typing.IsTrue(subject) {
 		return false
 	}
 
@@ -403,7 +406,7 @@ func some(values, data any) any {
 			value,
 		)
 
-		if isTrue(v) {
+		if typing.IsTrue(v) {
 			return true
 		}
 	}
@@ -412,18 +415,18 @@ func some(values, data any) any {
 }
 
 func parseValues(values, data any) any {
-	if values == nil || isPrimitive(values) {
+	if values == nil || typing.IsPrimitive(values) {
 		return values
 	}
 
-	if isMap(values) {
+	if typing.IsMap(values) {
 		return apply(values, data)
 	}
 
 	parsed := make([]any, 0)
 
 	for _, value := range values.([]any) {
-		if isMap(value) {
+		if typing.IsMap(value) {
 			parsed = append(parsed, apply(value, data))
 		} else {
 			parsed = append(parsed, value)
@@ -438,11 +441,11 @@ func parseValues(values, data any) any {
 // of JSONLogic.
 // Used in lazy evaluation of "AND" and "OR" operators
 func getValuesWithoutParsing(values, data any) any {
-	if values == nil || isPrimitive(values) {
+	if values == nil || typing.IsPrimitive(values) {
 		return values
 	}
 
-	if isMap(values) {
+	if typing.IsMap(values) {
 		return apply(values, data)
 	}
 
@@ -580,11 +583,11 @@ func ApplyInterface(rule, data any) (output any, err error) {
 		}
 	}()
 
-	if isMap(rule) {
+	if typing.IsMap(rule) {
 		return apply(rule, data), err
 	}
 
-	if isSlice(rule) {
+	if typing.IsSlice(rule) {
 		var parsed []any
 
 		for _, value := range rule.([]any) {
diff --git a/math.go b/math.go
index 281965f..f835705 100644
--- a/math.go
+++ b/math.go
@@ -1,16 +1,20 @@
 package jsonlogic
 
-import "math"
+import (
+	"math"
+
+	"github.com/diegoholiveira/jsonlogic/v3/internal/typing"
+)
 
 func mod(a any, b any) any {
-	_a := toNumber(a)
-	_b := toNumber(b)
+	_a := typing.ToNumber(a)
+	_b := typing.ToNumber(b)
 
 	return math.Mod(_a, _b)
 }
 
 func abs(a any) any {
-	_a := toNumber(a)
+	_a := typing.ToNumber(a)
 
 	return math.Abs(_a)
 }
@@ -19,7 +23,7 @@ func sum(values any) any {
 	sum := float64(0)
 
 	for _, n := range values.([]any) {
-		sum += toNumber(n)
+		sum += typing.ToNumber(n)
 	}
 
 	return sum
@@ -32,9 +36,9 @@ func minus(values any) any {
 		return 0
 	}
 
-	sum := toNumber(_values[0])
+	sum := typing.ToNumber(_values[0])
 	for i := 1; len(_values) > i; i++ {
-		sum -= toNumber(_values[i])
+		sum -= typing.ToNumber(_values[i])
 	}
 
 	return sum
@@ -44,7 +48,7 @@ func mult(values any) any {
 	sum := float64(1)
 
 	for _, n := range values.([]any) {
-		sum *= toNumber(n)
+		sum *= typing.ToNumber(n)
 	}
 
 	return sum
@@ -57,9 +61,9 @@ func div(values any) any {
 		return 0
 	}
 
-	sum := toNumber(_values[0])
+	sum := typing.ToNumber(_values[0])
 	for i := 1; len(_values) > i; i++ {
-		sum = sum / toNumber(_values[i])
+		sum = sum / typing.ToNumber(_values[i])
 	}
 
 	return sum
diff --git a/operation.go b/operation.go
index 2a16c15..4ac31c4 100644
--- a/operation.go
+++ b/operation.go
@@ -2,6 +2,7 @@ package jsonlogic
 
 import (
 	"github.com/diegoholiveira/jsonlogic/v3/internal/javascript"
+	"github.com/diegoholiveira/jsonlogic/v3/internal/typing"
 )
 
 // customOperators holds custom operators
@@ -64,7 +65,7 @@ func operation(operator string, values, data any) any {
 		return conditional(values, data)
 	}
 
-	if isPrimitive(values) {
+	if typing.IsPrimitive(values) {
 		return unary(operator, values)
 	}
 
diff --git a/strings.go b/strings.go
index aa759fb..6425550 100644
--- a/strings.go
+++ b/strings.go
@@ -3,14 +3,16 @@ package jsonlogic
 import (
 	"bytes"
 	"strings"
+
+	"github.com/diegoholiveira/jsonlogic/v3/internal/typing"
 )
 
 func substr(values any) any {
 	parsed := values.([]any)
 
-	runes := []rune(toString(parsed[0]))
+	runes := []rune(typing.ToString(parsed[0]))
 
-	from := int(toNumber(parsed[1]))
+	from := int(typing.ToNumber(parsed[1]))
 	length := len(runes)
 
 	if from < 0 {
@@ -23,7 +25,7 @@ func substr(values any) any {
 	}
 
 	if len(parsed) == 3 {
-		length = int(toNumber(parsed[2]))
+		length = int(typing.ToNumber(parsed[2]))
 	}
 
 	var to int
@@ -42,13 +44,13 @@ func substr(values any) any {
 }
 
 func concat(values any) any {
-	if isString(values) {
+	if typing.IsString(values) {
 		return values
 	}
 
 	var s bytes.Buffer
 	for _, text := range values.([]any) {
-		s.WriteString(toString(text))
+		s.WriteString(typing.ToString(text))
 	}
 
 	return strings.TrimSpace(s.String())
diff --git a/validator.go b/validator.go
index e63f780..44d11a4 100644
--- a/validator.go
+++ b/validator.go
@@ -3,6 +3,8 @@ package jsonlogic
 import (
 	"encoding/json"
 	"io"
+
+	"github.com/diegoholiveira/jsonlogic/v3/internal/typing"
 )
 
 var operators = map[string]bool{
@@ -62,7 +64,7 @@ func ValidateJsonLogic(rules any) bool {
 		return true
 	}
 
-	if isMap(rules) {
+	if typing.IsMap(rules) {
 		for operator, value := range rules.(map[string]any) {
 			if !isOperator(operator) {
 				return false
@@ -72,9 +74,9 @@ func ValidateJsonLogic(rules any) bool {
 		}
 	}
 
-	if isSlice(rules) {
+	if typing.IsSlice(rules) {
 		for _, value := range rules.([]any) {
-			if isSlice(value) || isMap(value) {
+			if typing.IsSlice(value) || typing.IsMap(value) {
 				if ValidateJsonLogic(value) {
 					continue
 				}
@@ -82,7 +84,7 @@ func ValidateJsonLogic(rules any) bool {
 				return false
 			}
 
-			if isVar(value) || isPrimitive(value) {
+			if isVar(value) || typing.IsPrimitive(value) {
 				continue
 			}
 		}
@@ -90,7 +92,7 @@ func ValidateJsonLogic(rules any) bool {
 		return true
 	}
 
-	return isPrimitive(rules)
+	return typing.IsPrimitive(rules)
 }
 
 func isOperator(op string) bool {
@@ -104,7 +106,7 @@ func isOperator(op string) bool {
 }
 
 func isVar(value any) bool {
-	if !isMap(value) {
+	if !typing.IsMap(value) {
 		return false
 	}
 
@@ -113,5 +115,5 @@ func isVar(value any) bool {
 		return false
 	}
 
-	return isString(_var) || isNumber(_var) || _var == nil
+	return typing.IsString(_var) || typing.IsNumber(_var) || _var == nil
 }
diff --git a/vars.go b/vars.go
index 429d720..c24e07d 100644
--- a/vars.go
+++ b/vars.go
@@ -4,20 +4,22 @@ import (
 	"encoding/json"
 	"strconv"
 	"strings"
+
+	"github.com/diegoholiveira/jsonlogic/v3/internal/typing"
 )
 
 func solveVars(values, data any) any {
-	if isMap(values) {
+	if typing.IsMap(values) {
 		logic := map[string]any{}
 
 		for key, value := range values.(map[string]any) {
 			if key == "var" {
-				if isString(value) && (value == "" || strings.HasPrefix(value.(string), ".")) {
+				if typing.IsString(value) && (value == "" || strings.HasPrefix(value.(string), ".")) {
 					logic["var"] = value
 					continue
 				}
 
-				if isEmptySlice(value) {
+				if typing.IsEmptySlice(value) {
 					logic["var"] = ""
 					continue
 				}
@@ -36,7 +38,7 @@ func solveVars(values, data any) any {
 		return any(logic)
 	}
 
-	if isSlice(values) {
+	if typing.IsSlice(values) {
 		logic := []any{}
 
 		for _, value := range values.([]any) {
@@ -51,23 +53,23 @@ func solveVars(values, data any) any {
 
 func getVar(value, data any) any {
 	if value == nil {
-		if !isPrimitive(data) {
+		if !typing.IsPrimitive(data) {
 			return nil
 		}
 		return data
 	}
 
-	if isString(value) && toString(value) == "" {
+	if typing.IsString(value) && typing.ToString(value) == "" {
 		return data
 	}
 
-	if isNumber(value) {
-		value = toString(value)
+	if typing.IsNumber(value) {
+		value = typing.ToString(value)
 	}
 
 	var _default any
 
-	if isSlice(value) { // syntax sugar
+	if typing.IsSlice(value) { // syntax sugar
 		v := value.([]any)
 
 		if len(v) == 0 {
@@ -94,12 +96,12 @@ func getVar(value, data any) any {
 			continue
 		}
 
-		if isMap(data) {
+		if typing.IsMap(data) {
 			_value = data.(map[string]any)[part]
 		}
 
-		if isSlice(data) {
-			pos := int(toNumber(part))
+		if typing.IsSlice(data) {
+			pos := int(typing.ToNumber(part))
 			container := data.([]any)
 			if pos >= len(container) {
 				return _default
@@ -111,7 +113,7 @@ func getVar(value, data any) any {
 			return _default
 		}
 
-		if isPrimitive(_value) {
+		if typing.IsPrimitive(_value) {
 			continue
 		}
 


Test patch:
diff --git a/internal/typing/typing_test.go b/internal/typing/typing_test.go
new file mode 100644
index 0000000..80066ff
--- /dev/null
+++ b/internal/typing/typing_test.go
@@ -0,0 +1,262 @@
+package typing
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestIsBool(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    any
+		expected bool
+	}{
+		{"true value", true, true},
+		{"false value", false, true},
+		{"nil value", nil, false},
+		{"string value", "true", false},
+		{"int value", 1, false},
+		{"float value", 1.5, false},
+		{"slice value", []any{}, false},
+		{"map value", map[string]any{}, false},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := IsBool(tt.input)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestIsString(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    any
+		expected bool
+	}{
+		{"empty string", "", true},
+		{"non-empty string", "hello", true},
+		{"nil value", nil, false},
+		{"boolean value", true, false},
+		{"int value", 1, false},
+		{"float value", 1.5, false},
+		{"slice value", []any{}, false},
+		{"map value", map[string]any{}, false},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := IsString(tt.input)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestIsNumber(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    any
+		expected bool
+	}{
+		{"int zero", 0, true},
+		{"int positive", 42, true},
+		{"int negative", -10, true},
+		{"float zero", 0.0, true},
+		{"float positive", 3.14, true},
+		{"float negative", -2.5, true},
+		{"nil value", nil, false},
+		{"boolean value", true, false},
+		{"string value", "123", false},
+		{"slice value", []any{}, false},
+		{"map value", map[string]any{}, false},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := IsNumber(tt.input)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestIsPrimitive(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    any
+		expected bool
+	}{
+		{"boolean", true, true},
+		{"string", "hello", true},
+		{"int", 42, true},
+		{"float", 3.14, true},
+		{"nil value", nil, false},
+		{"slice value", []any{}, false},
+		{"map value", map[string]any{}, false},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := IsPrimitive(tt.input)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestIsMap(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    any
+		expected bool
+	}{
+		{"empty map", map[string]any{}, true},
+		{"non-empty map", map[string]any{"key": "value"}, true},
+		{"nil value", nil, false},
+		{"boolean value", true, false},
+		{"int value", 1, false},
+		{"float value", 1.5, false},
+		{"string value", "hello", false},
+		{"slice value", []any{}, false},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := IsMap(tt.input)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestIsSlice(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    any
+		expected bool
+	}{
+		{"empty slice", []any{}, true},
+		{"non-empty slice", []any{1, 2, 3}, true},
+		{"nil value", nil, false},
+		{"boolean value", true, false},
+		{"int value", 1, false},
+		{"float value", 1.5, false},
+		{"string value", "hello", false},
+		{"map value", map[string]any{}, false},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := IsSlice(tt.input)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestIsEmptySlice(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    any
+		expected bool
+	}{
+		{"empty slice", []any{}, true},
+		{"slice with zeros", []any{0, 0, 0}, true},
+		{"slice with empty strings", []any{"", ""}, true},
+		{"slice with false values", []any{false, false}, true},
+		{"slice with mixed falsy values", []any{0, "", false, []any{}}, true},
+		{"non-empty slice with truthy value", []any{0, 1, 0}, false},
+		{"non-empty slice with true", []any{false, true}, false},
+		{"nil value", nil, false},
+		{"boolean value", true, false},
+		{"int value", 1, false},
+		{"float value", 1.5, false},
+		{"string value", "hello", false},
+		{"map value", map[string]any{}, false},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := IsEmptySlice(tt.input)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestIsTrue(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    any
+		expected bool
+	}{
+		{"true boolean", true, true},
+		{"false boolean", false, false},
+		{"positive number", 42, true},
+		{"negative number", -10, true},
+		{"zero number", 0, false},
+		{"non-empty string", "hello", true},
+		{"empty string", "", false},
+		{"non-empty slice", []any{1, 2, 3}, true},
+		{"empty slice", []any{}, false},
+		{"non-empty map", map[string]any{"key": "value"}, true},
+		{"empty map", map[string]any{}, false},
+		{"nil value", nil, false},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := IsTrue(tt.input)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestToNumber(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    any
+		expected float64
+	}{
+		{"int zero", 0, 0.0},
+		{"int positive", 42, 42.0},
+		{"int negative", -10, -10.0},
+		{"float zero", 0.0, 0.0},
+		{"float positive", 3.14, 3.14},
+		{"float negative", -2.5, -2.5},
+		{"string number integer", "42", 42.0},
+		{"string number float", "3.14", 3.14},
+		{"string number negative", "-10", -10.0},
+		{"string empty", "", 0.0},
+		{"string non-number", "hello", 0.0},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := ToNumber(tt.input)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestToString(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    any
+		expected string
+	}{
+		{"int zero", 0, "0"},
+		{"int positive", 42, "42"},
+		{"int negative", -10, "-10"},
+		{"float zero", 0.0, "0"},
+		{"float positive", 3.14, "3.14"},
+		{"float negative", -2.5, "-2.5"},
+		{"string", "hello", "hello"},
+		{"empty string", "", ""},
+		{"nil value", nil, ""},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := ToString(tt.input)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
\ No newline at end of file