From 1c3346947b6a54c6bf13b8171310df92d63f3e99 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Thu, 5 Sep 2024 21:57:39 +1000 Subject: [PATCH] Completed 'eq' cases and added 'ne' --- ucl/builtins.go | 74 ++++++++++- ucl/inst.go | 10 ++ ucl/objs.go | 4 + ucl/testbuiltins_test.go | 271 ++++++++++++++++++++++++++++++++++++--- 4 files changed, 341 insertions(+), 18 deletions(-) diff --git a/ucl/builtins.go b/ucl/builtins.go index b584d95..199c909 100644 --- a/ucl/builtins.go +++ b/ucl/builtins.go @@ -63,17 +63,85 @@ func eqBuiltin(ctx context.Context, args invocationArgs) (object, error) { l := args.args[0] r := args.args[1] + return boolObject(objectsEqual(l, r)), nil +} + +func neBuiltin(ctx context.Context, args invocationArgs) (object, error) { + if err := args.expectArgn(2); err != nil { + return nil, err + } + + l := args.args[0] + r := args.args[1] + + return boolObject(!objectsEqual(l, r)), nil +} + +var errObjectsNotEqual = errors.New("objects not equal") + +func objectsEqual(l, r object) bool { + if l == nil || r == nil { + return l == nil && r == nil + } + switch lv := l.(type) { case strObject: if rv, ok := r.(strObject); ok { - return boolObject(lv == rv), nil + return lv == rv } case intObject: if rv, ok := r.(intObject); ok { - return boolObject(lv == rv), nil + return lv == rv } + case boolObject: + if rv, ok := r.(boolObject); ok { + return lv == rv + } + case listable: + rv, ok := r.(listable) + if !ok { + return false + } + + if lv.Len() != rv.Len() { + return false + } + for i := 0; i < lv.Len(); i++ { + if !objectsEqual(lv.Index(i), rv.Index(i)) { + return false + } + } + return true + case hashable: + rv, ok := r.(hashable) + if !ok { + return false + } + + if lv.Len() != rv.Len() { + return false + } + if err := lv.Each(func(k string, lkv object) error { + rkv := rv.Value(k) + if rkv == nil { + return errObjectsNotEqual + } else if !objectsEqual(lkv, rkv) { + return errObjectsNotEqual + } + return nil + }); err != nil { + return false + } + return true + case OpaqueObject: + rv, ok := r.(OpaqueObject) + if !ok { + return false + } + + return lv.v == rv.v } - return boolObject(false), nil + return false } func concatBuiltin(ctx context.Context, args invocationArgs) (object, error) { diff --git a/ucl/inst.go b/ucl/inst.go index 4d002cf..17792d1 100644 --- a/ucl/inst.go +++ b/ucl/inst.go @@ -58,6 +58,8 @@ func New(opts ...InstOption) *Inst { rootEC.addCmd("head", invokableFunc(firstBuiltin)) rootEC.addCmd("eq", invokableFunc(eqBuiltin)) + rootEC.addCmd("ne", invokableFunc(neBuiltin)) + rootEC.addCmd("cat", invokableFunc(concatBuiltin)) rootEC.addCmd("break", invokableFunc(breakBuiltin)) rootEC.addCmd("continue", invokableFunc(continueBuiltin)) @@ -83,6 +85,14 @@ func New(opts ...InstOption) *Inst { return inst } +func (inst *Inst) SetVar(name string, value any) { + obj, err := fromGoValue(value) + if err != nil { + return + } + inst.rootEC.setOrDefineVar(name, obj) +} + func (inst *Inst) Out() io.Writer { if inst.out == nil { return os.Stdout diff --git a/ucl/objs.go b/ucl/objs.go index 1c1aa36..e2fe7d4 100644 --- a/ucl/objs.go +++ b/ucl/objs.go @@ -107,6 +107,8 @@ func toGoValue(obj object) (interface{}, bool) { return string(v), true case intObject: return int(v), true + case boolObject: + return bool(v), true case listObject: xs := make([]interface{}, 0, len(v)) for _, va := range v { @@ -146,6 +148,8 @@ func fromGoValue(v any) (object, error) { return strObject(t), nil case int: return intObject(t), nil + case bool: + return boolObject(t), nil } resVal := reflect.ValueOf(v) diff --git a/ucl/testbuiltins_test.go b/ucl/testbuiltins_test.go index bcf0931..db9877b 100644 --- a/ucl/testbuiltins_test.go +++ b/ucl/testbuiltins_test.go @@ -228,16 +228,16 @@ func TestBuiltins_Break(t *testing.T) { expr string want string }{ - //{desc: "break unconditionally returning nothing", expr: ` - // foreach ["1" "2" "3"] { |v| - // break - // echo $v - // }`, want: "(nil)\n"}, - //{desc: "break conditionally returning nothing", expr: ` - // foreach ["1" "2" "3"] { |v| - // echo $v - // if (eq $v "2") { break } - // }`, want: "1\n2\n(nil)\n"}, + {desc: "break unconditionally returning nothing", expr: ` + foreach ["1" "2" "3"] { |v| + break + echo $v + }`, want: "(nil)\n"}, + {desc: "break conditionally returning nothing", expr: ` + foreach ["1" "2" "3"] { |v| + echo $v + if (eq $v "2") { break } + }`, want: "1\n2\n(nil)\n"}, {desc: "break inner loop only returning nothing", expr: ` foreach ["a" "b"] { |u| foreach ["1" "2" "3"] { |v| @@ -245,11 +245,11 @@ func TestBuiltins_Break(t *testing.T) { if (eq $v "2") { break } } }`, want: "a1\na2\nb1\nb2\n(nil)\n"}, - //{desc: "break returning value", expr: ` - // echo (foreach ["1" "2" "3"] { |v| - // echo $v - // if (eq $v "2") { break "hello" } - // })`, want: "1\n2\nhello\n(nil)\n"}, + {desc: "break returning value", expr: ` + echo (foreach ["1" "2" "3"] { |v| + echo $v + if (eq $v "2") { break "hello" } + })`, want: "1\n2\nhello\n(nil)\n"}, } for _, tt := range tests { @@ -393,6 +393,36 @@ func TestBuiltins_Return(t *testing.T) { expr string want string }{ + // syntax tests + {desc: "empty proc 1", expr: ` + proc greet {} + greet + `, want: "(nil)\n"}, + {desc: "empty proc 2", expr: ` + proc greet { + } + + greet + `, want: "(nil)\n"}, + {desc: "empty proc 3", expr: ` + proc greet { + + + } + + greet + `, want: "(nil)\n"}, + {desc: "empty proc 4", expr: ` + proc greet { + # bla + + # di + # bla! + } + + greet + `, want: "(nil)\n"}, + {desc: "nil return", expr: ` proc greet { echo "Hello" @@ -402,6 +432,27 @@ func TestBuiltins_Return(t *testing.T) { greet `, want: "Hello\n(nil)\n"}, + + {desc: "simple arg 1", expr: ` + proc greet { |x| + return (cat "Hello, " $x) + } + + greet "person" + `, want: "Hello, person\n"}, + {desc: "simple arg 2", expr: ` + proc greet { + # This will greet someone + # here are the args: + |x| + + # And here is the code + return (cat "Hello, " $x) + } + + greet "person" + `, want: "Hello, person\n"}, + {desc: "simple return", expr: ` proc greet { return "Hello, world" @@ -410,6 +461,7 @@ func TestBuiltins_Return(t *testing.T) { greet `, want: "Hello, world\n"}, + {desc: "only return current frame", expr: ` proc greetWhat { echo "Greet the" @@ -618,6 +670,16 @@ func TestBuiltins_Index(t *testing.T) { inst.SetBuiltin("goInt", func(ctx context.Context, args CallArgs) (any, error) { return []int{6, 5, 4}, nil }) + inst.SetBuiltin("goList", func(ctx context.Context, args CallArgs) (any, error) { + type nest struct { + This string + } + return []*nest{ + {This: "thing 1"}, + {This: "thing 2"}, + nil, + }, nil + }) inst.SetBuiltin("goStruct", func(ctx context.Context, args CallArgs) (any, error) { type nested struct { This string @@ -710,3 +772,182 @@ func TestBuiltins_Len(t *testing.T) { }) } } + +func TestBuiltins_Keys(t *testing.T) { + type testNested struct { + Nested string + Type string + } + + tests := []struct { + desc string + expr string + wantItems []string + }{ + {desc: "keys of map", expr: `keys [alpha: "hello" bravo: "world"]`, wantItems: []string{"alpha", "bravo"}}, + {desc: "keys of go struct 1", expr: `goStruct | keys`, wantItems: []string{"Alpha", "Beta", "Gamma"}}, + {desc: "keys of go struct 2", expr: `index (goStruct) Gamma | keys`, wantItems: []string{"Nested", "Type"}}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + ctx := context.Background() + outW := bytes.NewBuffer(nil) + + inst := New(WithOut(outW), WithTestBuiltin()) + inst.SetBuiltin("goInt", func(ctx context.Context, args CallArgs) (any, error) { + return []int{6, 5, 4}, nil + }) + inst.SetBuiltin("goStruct", func(ctx context.Context, args CallArgs) (any, error) { + return struct { + Alpha string + Beta string + Gamma testNested + hidden string + missing string + }{ + Alpha: "foo", + Beta: "bar", + Gamma: testNested{ + Nested: "ads", + Type: "asd", + }, + hidden: "hidden", + missing: "missing", + }, nil + }) + + res, err := inst.Eval(ctx, tt.expr) + assert.NoError(t, err) + assert.Len(t, res, len(tt.wantItems)) + for _, i := range tt.wantItems { + assert.Contains(t, res, i) + } + }) + } +} + +func TestBuiltins_Filter(t *testing.T) { + tests := []struct { + desc string + expr string + want any + }{ + {desc: "filter list 1", expr: `filter [1 2 3] { |x| eq $x 2 }`, want: []any{2}}, + {desc: "filter list 2", expr: `filter ["flim" "flam" "fla"] { |x| eq $x "flam" }`, want: []any{"flam"}}, + {desc: "filter list 3", expr: `filter ["flim" "flam" "fla"] { |x| eq $x "bogie" }`, want: []any{}}, + + {desc: "filter map 1", expr: `filter [alpha:"hello" bravo:"world"] { |k v| eq $k "alpha" }`, want: map[string]any{ + "alpha": "hello", + }}, + {desc: "filter map 2", expr: `filter [alpha:"hello" bravo:"world"] { |k v| eq $v "world" }`, want: map[string]any{ + "bravo": "world", + }}, + {desc: "filter map 3", expr: `filter [alpha:"hello" bravo:"world"] { |k v| eq $v "alpha" }`, want: map[string]any{}}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + ctx := context.Background() + outW := bytes.NewBuffer(nil) + + inst := New(WithOut(outW), WithTestBuiltin()) + + res, err := inst.Eval(ctx, tt.expr) + assert.NoError(t, err) + assert.Equal(t, tt.want, res) + }) + } +} + +func TestBuiltins_Reduce(t *testing.T) { + tests := []struct { + desc string + expr string + want any + }{ + {desc: "reduce list 1", expr: `reduce [1 1 1] { |x a| add $x $a }`, want: 3}, + {desc: "reduce list 2", expr: `reduce [1 1 1] 20 { |x a| add $x $a }`, want: 23}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + ctx := context.Background() + outW := bytes.NewBuffer(nil) + + inst := New(WithOut(outW), WithTestBuiltin()) + + res, err := inst.Eval(ctx, tt.expr) + assert.NoError(t, err) + assert.Equal(t, tt.want, res) + }) + } +} + +func TestBuiltins_EqNe(t *testing.T) { + tests := []struct { + desc string + expr string + want bool + }{ + {desc: "equal strs 1", expr: `eq "hello" "hello"`, want: true}, + {desc: "equal strs 2", expr: `eq "bla" "bla"`, want: true}, + {desc: "equal strs 3", expr: `eq "" ""`, want: true}, + {desc: "equal ints 1", expr: `eq 123 123`, want: true}, + {desc: "equal ints 2", expr: `eq -21 -21`, want: true}, + {desc: "equal ints 3", expr: `eq 0 0`, want: true}, + {desc: "equal lists 1", expr: `eq [1 2 3] [1 2 3]`, want: true}, + {desc: "equal lists 2", expr: `eq ["foo" "bar"] ["foo" "bar"]`, want: true}, + {desc: "equal lists 3", expr: `eq [] []`, want: true}, + {desc: "equal hashes 1", expr: `eq ["this":1 "that":"thing"] ["that":"thing" "this":1]`, want: true}, + {desc: "equal hashes 2", expr: `eq ["foo":"bar"] ["foo":"bar"]`, want: true}, + {desc: "equal bools 1", expr: `eq true true`, want: true}, + {desc: "equal bools 2", expr: `eq false false`, want: true}, + {desc: "equal nil 1", expr: `eq () ()`, want: true}, + {desc: "equal opaque 1", expr: `eq $hello $hello`, want: true}, + {desc: "equal opaque 2", expr: `eq $world $world`, want: true}, + + {desc: "not equal strs 1", expr: `eq "hello" "world"`, want: false}, + {desc: "not equal strs 2", expr: `eq "bla" "BLA"`, want: false}, + {desc: "not equal int 1", expr: `eq 131 313`, want: false}, + {desc: "not equal int 2", expr: `eq -2 2`, want: false}, + {desc: "not equal lists 1", expr: `eq [1 2 3] [1 2]`, want: false}, + {desc: "not equal lists 2", expr: `eq ["123" "234"] [123 234]`, want: false}, + {desc: "not equal hashes 1", expr: `eq ["this":1 "that":"thing"] ["that":"thing"]`, want: false}, + {desc: "not equal hashes 2", expr: `eq ["this":1 "that":"thing"] ["this":1 "that":"thing" "other":"thing"]`, want: false}, + {desc: "not equal hashes 3", expr: `eq ["this":1 "that":"thing"] ["this":"1" "that":"other"]`, want: false}, + {desc: "not equal opaque 1", expr: `eq $hello $world`, want: false}, + {desc: "not equal opaque 2", expr: `eq $hello "hello"`, want: false}, + + {desc: "not equal types 1", expr: `eq "123" 123`, want: false}, + {desc: "not equal types 2", expr: `eq 0 ""`, want: false}, + {desc: "not equal types 3", expr: `eq [] [:]`, want: false}, + {desc: "not equal types 4", expr: `eq ["23"] "23"`, want: false}, + {desc: "not equal types 5", expr: `eq true ()`, want: false}, + {desc: "not equal types 6", expr: `eq () false`, want: false}, + {desc: "not equal types 7", expr: `eq () "yes"`, want: false}, + {desc: "not equal types 8", expr: `eq () $world`, want: false}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + type testProxyObject struct { + v string + } + + ctx := context.Background() + outW := bytes.NewBuffer(nil) + + inst := New(WithOut(outW), WithTestBuiltin()) + // Removed code I don't have the rights to + + eqRes, err := inst.Eval(ctx, tt.expr) + assert.NoError(t, err) + assert.Equal(t, tt.want, eqRes) + + neRes, err := inst.Eval(ctx, strings.ReplaceAll(tt.expr, "eq", "ne")) + assert.NoError(t, err) + assert.Equal(t, !tt.want, neRes) + }) + } +}