diff --git a/ucl/builtins.go b/ucl/builtins.go index e984786..4841a3a 100644 --- a/ucl/builtins.go +++ b/ucl/builtins.go @@ -88,17 +88,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 a4d1128..66c323b 100644 --- a/ucl/inst.go +++ b/ucl/inst.go @@ -61,6 +61,8 @@ func New(opts ...InstOption) *Inst { rootEC.addCmd("reduce", invokableFunc(reduceBuiltin)) rootEC.addCmd("eq", invokableFunc(eqBuiltin)) + rootEC.addCmd("ne", invokableFunc(neBuiltin)) + rootEC.addCmd("add", invokableFunc(addBuiltin)) rootEC.addCmd("cat", invokableFunc(concatBuiltin)) @@ -88,6 +90,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 5527c85..6580c9c 100644 --- a/ucl/objs.go +++ b/ucl/objs.go @@ -118,6 +118,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 { @@ -159,6 +161,8 @@ func fromGoValue(v any) (object, error) { return strObject(t), nil case int: return intObject(t), nil + case bool: + return boolObject(t), nil } return fromGoReflectValue(reflect.ValueOf(v)) diff --git a/ucl/testbuiltins_test.go b/ucl/testbuiltins_test.go index 34e0220..d9133d6 100644 --- a/ucl/testbuiltins_test.go +++ b/ucl/testbuiltins_test.go @@ -229,16 +229,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| @@ -246,11 +246,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 { @@ -891,3 +891,72 @@ func TestBuiltins_Reduce(t *testing.T) { }) } } + +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()) + inst.SetVar("hello", Opaque(testProxyObject{v: "hello"})) + inst.SetVar("world", Opaque(testProxyObject{v: "world"})) + + 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) + }) + } +}