@@ -8,6 +8,59 @@ import (
88 "github.com/stretchr/testify/require"
99)
1010
11+ // ── normalizeJSONNumbers unit tests ──────────────────────────────────────────
12+
13+ func TestNormalizeJSONNumbers_ExponentFloat (t * testing.T ) {
14+ result := normalizeJSONNumbers (json .Number ("1e5" ))
15+ assert .Equal (t , float64 (100000 ), result )
16+ }
17+
18+ func TestNormalizeJSONNumbers_NegativeFloat (t * testing.T ) {
19+ result := normalizeJSONNumbers (json .Number ("-3.14" ))
20+ assert .Equal (t , float64 (- 3.14 ), result )
21+ }
22+
23+ func TestNormalizeJSONNumbers_IntegerMaxInt64 (t * testing.T ) {
24+ // 9223372036854775807 is exactly math.MaxInt64 — must come back as int64.
25+ result := normalizeJSONNumbers (json .Number ("9223372036854775807" ))
26+ assert .Equal (t , int64 (9223372036854775807 ), result )
27+ }
28+
29+ func TestNormalizeJSONNumbers_OverflowInt64PreservedAsString (t * testing.T ) {
30+ // One past MaxInt64 — cannot fit in int64; must preserve as string.
31+ result := normalizeJSONNumbers (json .Number ("9223372036854775808" ))
32+ assert .Equal (t , "9223372036854775808" , result )
33+ }
34+
35+ func TestNormalizeJSONNumbers_NestedMap (t * testing.T ) {
36+ input := map [string ]any {
37+ "count" : json .Number ("42" ),
38+ "ratio" : json .Number ("0.5" ),
39+ "label" : "hello" ,
40+ }
41+ result := normalizeJSONNumbers (input )
42+ m := result .(map [string ]any )
43+ assert .Equal (t , int64 (42 ), m ["count" ])
44+ assert .Equal (t , float64 (0.5 ), m ["ratio" ])
45+ assert .Equal (t , "hello" , m ["label" ])
46+ }
47+
48+ func TestNormalizeJSONNumbers_NestedArray (t * testing.T ) {
49+ input := []any {json .Number ("1" ), json .Number ("2.5" ), "text" }
50+ result := normalizeJSONNumbers (input )
51+ arr := result .([]any )
52+ assert .Equal (t , int64 (1 ), arr [0 ])
53+ assert .Equal (t , float64 (2.5 ), arr [1 ])
54+ assert .Equal (t , "text" , arr [2 ])
55+ }
56+
57+ func TestNormalizeJSONNumbers_PassthroughTypes (t * testing.T ) {
58+ // Non-number types should be returned unchanged.
59+ assert .Equal (t , true , normalizeJSONNumbers (true ))
60+ assert .Equal (t , "hello" , normalizeJSONNumbers ("hello" ))
61+ assert .Nil (t , normalizeJSONNumbers (nil ))
62+ }
63+
1164func TestFunc_ToLower (t * testing.T ) {
1265 eval , err := NewEvaluator ()
1366 require .NoError (t , err )
@@ -460,6 +513,16 @@ func TestFunc_FormatTimestamp(t *testing.T) {
460513 expr : `formatTimestamp(parseTimestamp("2024-01-15T14:30:00Z"), "15:04")` ,
461514 want : "14:30" ,
462515 },
516+ {
517+ name : "named month format" ,
518+ expr : `formatTimestamp(parseTimestamp("2024-01-15T00:00:00Z"), "Jan 2, 2006")` ,
519+ want : "Jan 15, 2024" ,
520+ },
521+ {
522+ name : "rfc3339 roundtrip" ,
523+ expr : `formatTimestamp(parseTimestamp("2026-03-24T00:00:00Z"), "2006-01-02T15:04:05Z07:00")` ,
524+ want : "2026-03-24T00:00:00Z" ,
525+ },
463526 }
464527 for _ , tt := range tests {
465528 t .Run (tt .name , func (t * testing.T ) {
@@ -469,3 +532,224 @@ func TestFunc_FormatTimestamp(t *testing.T) {
469532 })
470533 }
471534}
535+
536+ // ── Additional boundary and regression tests ──────────────────────────────────
537+
538+ func TestFunc_Obj_MaxArity (t * testing.T ) {
539+ // obj() supports up to 5 key-value pairs (10 args). Verify all registered
540+ // overloads (2, 4, 6, 8, 10 args) produce the correct maps.
541+ eval , err := NewEvaluator ()
542+ require .NoError (t , err )
543+
544+ t .Run ("three pairs (6 args)" , func (t * testing.T ) {
545+ result , err := eval .Eval (`obj("a", 1, "b", 2, "c", 3)` , newTestContext ())
546+ require .NoError (t , err )
547+ assert .Equal (t , map [string ]any {"a" : int64 (1 ), "b" : int64 (2 ), "c" : int64 (3 )}, result )
548+ })
549+
550+ t .Run ("four pairs (8 args)" , func (t * testing.T ) {
551+ result , err := eval .Eval (`obj("a", 1, "b", 2, "c", 3, "d", 4)` , newTestContext ())
552+ require .NoError (t , err )
553+ assert .Equal (t , map [string ]any {"a" : int64 (1 ), "b" : int64 (2 ), "c" : int64 (3 ), "d" : int64 (4 )}, result )
554+ })
555+
556+ t .Run ("five pairs (10 args — max arity)" , func (t * testing.T ) {
557+ result , err := eval .Eval (`obj("a", 1, "b", 2, "c", 3, "d", 4, "e", 5)` , newTestContext ())
558+ require .NoError (t , err )
559+ assert .Equal (t , map [string ]any {
560+ "a" : int64 (1 ), "b" : int64 (2 ), "c" : int64 (3 ),
561+ "d" : int64 (4 ), "e" : int64 (5 ),
562+ }, result )
563+ })
564+ }
565+
566+ func TestFunc_Flatten_MixedScalarAndSublist (t * testing.T ) {
567+ // The flatten implementation passes non-list elements through unchanged.
568+ // A list like [1, [2, 3]] should produce [1, 2, 3].
569+ eval , err := NewEvaluator ()
570+ require .NoError (t , err )
571+
572+ ctx := newTestContext ()
573+ ctx .Inputs ["mixed" ] = []any {int64 (1 ), []any {int64 (2 ), int64 (3 )}}
574+ result , err := eval .Eval (`flatten(inputs.mixed)` , ctx )
575+ require .NoError (t , err )
576+ assert .Equal (t , []any {int64 (1 ), int64 (2 ), int64 (3 )}, result )
577+ }
578+
579+ func TestFunc_Default_FalsyButNonNull (t * testing.T ) {
580+ // false and 0 are falsy but are NOT null — default() must return them unchanged.
581+ eval , err := NewEvaluator ()
582+ require .NoError (t , err )
583+
584+ t .Run ("false returns false not fallback" , func (t * testing.T ) {
585+ result , err := eval .Eval (`default(false, true)` , newTestContext ())
586+ require .NoError (t , err )
587+ assert .Equal (t , false , result )
588+ })
589+
590+ t .Run ("zero returns zero not fallback" , func (t * testing.T ) {
591+ result , err := eval .Eval (`default(0, 99)` , newTestContext ())
592+ require .NoError (t , err )
593+ assert .Equal (t , int64 (0 ), result )
594+ })
595+
596+ t .Run ("empty string returns empty string not fallback" , func (t * testing.T ) {
597+ result , err := eval .Eval (`default("", "fallback")` , newTestContext ())
598+ require .NoError (t , err )
599+ assert .Equal (t , "" , result )
600+ })
601+ }
602+
603+ func TestFunc_Split_EmptySeparator (t * testing.T ) {
604+ // strings.Split("abc", "") returns ["a", "b", "c"] — each character.
605+ eval , err := NewEvaluator ()
606+ require .NoError (t , err )
607+
608+ result , err := eval .Eval (`"abc".split("")` , newTestContext ())
609+ require .NoError (t , err )
610+ assert .Equal (t , []any {"a" , "b" , "c" }, result )
611+ }
612+
613+ func TestFunc_Replace_EmptyOldString (t * testing.T ) {
614+ // strings.ReplaceAll("ab", "", "X") inserts X between and around each char.
615+ eval , err := NewEvaluator ()
616+ require .NoError (t , err )
617+
618+ result , err := eval .Eval (`"ab".replace("", "X")` , newTestContext ())
619+ require .NoError (t , err )
620+ assert .Equal (t , "XaXbX" , result )
621+ }
622+
623+ func TestFunc_ParseInt_WhitespaceIsInvalid (t * testing.T ) {
624+ // strconv.ParseInt is strict — " 42" (leading space) must fail.
625+ eval , err := NewEvaluator ()
626+ require .NoError (t , err )
627+
628+ _ , err = eval .Eval (`parseInt(" 42")` , newTestContext ())
629+ require .Error (t , err )
630+ }
631+
632+ func TestFunc_JsonDecode_FloatValue (t * testing.T ) {
633+ eval , err := NewEvaluator ()
634+ require .NoError (t , err )
635+
636+ result , err := eval .Eval (`jsonDecode("3.14")` , newTestContext ())
637+ require .NoError (t , err )
638+ assert .Equal (t , float64 (3.14 ), result )
639+ }
640+
641+ func TestFunc_JsonDecode_BoolValue (t * testing.T ) {
642+ eval , err := NewEvaluator ()
643+ require .NoError (t , err )
644+
645+ result , err := eval .Eval (`jsonDecode("true")` , newTestContext ())
646+ require .NoError (t , err )
647+ assert .Equal (t , true , result )
648+ }
649+
650+ func TestFunc_JsonDecode_ArrayOfIntegers (t * testing.T ) {
651+ eval , err := NewEvaluator ()
652+ require .NoError (t , err )
653+
654+ result , err := eval .Eval (`jsonDecode("[1, 2, 3]")` , newTestContext ())
655+ require .NoError (t , err )
656+
657+ arr , ok := result .([]any )
658+ require .True (t , ok , "expected []any, got %T" , result )
659+ assert .Equal (t , []any {int64 (1 ), int64 (2 ), int64 (3 )}, arr )
660+ }
661+
662+ func TestFunc_JsonDecode_NestedObject (t * testing.T ) {
663+ eval , err := NewEvaluator ()
664+ require .NoError (t , err )
665+
666+ result , err := eval .Eval (`jsonDecode("{\"a\":{\"b\":42}}")` , newTestContext ())
667+ require .NoError (t , err )
668+
669+ m , ok := result .(map [string ]any )
670+ require .True (t , ok )
671+ inner , ok := m ["a" ].(map [string ]any )
672+ require .True (t , ok , "expected inner map" )
673+ assert .Equal (t , int64 (42 ), inner ["b" ])
674+ }
675+
676+ func TestFunc_JsonEncode_List (t * testing.T ) {
677+ eval , err := NewEvaluator ()
678+ require .NoError (t , err )
679+
680+ result , err := eval .Eval (`jsonEncode(["a", "b", "c"])` , newTestContext ())
681+ require .NoError (t , err )
682+
683+ s , ok := result .(string )
684+ require .True (t , ok )
685+ var arr []string
686+ require .NoError (t , json .Unmarshal ([]byte (s ), & arr ))
687+ assert .Equal (t , []string {"a" , "b" , "c" }, arr )
688+ }
689+
690+ func TestFunc_JsonEncode_Primitive (t * testing.T ) {
691+ eval , err := NewEvaluator ()
692+ require .NoError (t , err )
693+
694+ t .Run ("integer" , func (t * testing.T ) {
695+ result , err := eval .Eval (`jsonEncode(42)` , newTestContext ())
696+ require .NoError (t , err )
697+ assert .Equal (t , "42" , result )
698+ })
699+
700+ t .Run ("boolean" , func (t * testing.T ) {
701+ result , err := eval .Eval (`jsonEncode(true)` , newTestContext ())
702+ require .NoError (t , err )
703+ assert .Equal (t , "true" , result )
704+ })
705+
706+ t .Run ("string" , func (t * testing.T ) {
707+ result , err := eval .Eval (`jsonEncode("hello")` , newTestContext ())
708+ require .NoError (t , err )
709+ assert .Equal (t , `"hello"` , result )
710+ })
711+ }
712+
713+ func TestFunc_ParseTimestamp_NamedMonthFormat (t * testing.T ) {
714+ // "Jan 2, 2006" is one of the supported layouts in parseTimestamp.
715+ eval , err := NewEvaluator ()
716+ require .NoError (t , err )
717+
718+ result , err := eval .Eval (`formatTimestamp(parseTimestamp("Mar 15, 2025"), "2006-01-02")` , newTestContext ())
719+ require .NoError (t , err )
720+ assert .Equal (t , "2025-03-15" , result )
721+ }
722+
723+ func TestFunc_StringChaining (t * testing.T ) {
724+ // Verify that string methods can be chained.
725+ eval , err := NewEvaluator ()
726+ require .NoError (t , err )
727+
728+ result , err := eval .Eval (`" HELLO WORLD ".trim().toLower()` , newTestContext ())
729+ require .NoError (t , err )
730+ assert .Equal (t , "hello world" , result )
731+ }
732+
733+ func TestFunc_ResolveString_WithCustomFunction (t * testing.T ) {
734+ // Verify that custom functions work when embedded in {{ }} template strings.
735+ eval , err := NewEvaluator ()
736+ require .NoError (t , err )
737+
738+ result , err := eval .ResolveString (`tag:{{ "PRODUCTION".toLower() }}` , newTestContext ())
739+ require .NoError (t , err )
740+ assert .Equal (t , "tag:production" , result )
741+ }
742+
743+ func TestFunc_JsonRoundtrip (t * testing.T ) {
744+ // jsonEncode followed by jsonDecode must preserve the original value.
745+ eval , err := NewEvaluator ()
746+ require .NoError (t , err )
747+
748+ result , err := eval .Eval (`jsonDecode(jsonEncode(obj("x", 1, "y", "hello")))` , newTestContext ())
749+ require .NoError (t , err )
750+
751+ m , ok := result .(map [string ]any )
752+ require .True (t , ok )
753+ assert .Equal (t , int64 (1 ), m ["x" ])
754+ assert .Equal (t , "hello" , m ["y" ])
755+ }
0 commit comments