From 43098fa227409628510097e0b3fec7f3a5f19b05 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Wed, 24 Apr 2024 21:09:52 +1000 Subject: [PATCH] Fixed binding with struct --- cmdlang/builtins.go | 32 ++++++++++++++++++++ cmdlang/inst.go | 1 + cmdlang/objs.go | 53 ++++++++++++++++++++++++++++++++- cmdlang/testbuiltins_test.go | 57 ++++++++++++++++++++++++++++++++++++ cmdlang/userbuiltin.go | 31 +++++++++++++++----- cmdlang/userbuiltin_test.go | 1 - 6 files changed, 165 insertions(+), 10 deletions(-) diff --git a/cmdlang/builtins.go b/cmdlang/builtins.go index 8320edd..6b137fc 100644 --- a/cmdlang/builtins.go +++ b/cmdlang/builtins.go @@ -113,6 +113,38 @@ func callBuiltin(ctx context.Context, args invocationArgs) (object, error) { return inv.invoke(ctx, args.shift(1)) } +func indexBuiltin(ctx context.Context, args invocationArgs) (object, error) { + if err := args.expectArgn(1); err != nil { + return nil, err + } + + val := args.args[0] + for _, idx := range args.args[1:] { + switch v := val.(type) { + case listable: + intIdx, ok := idx.(intObject) + if !ok { + return nil, errors.New("expected int for listable") + } + if int(intIdx) >= 0 && int(intIdx) < v.Len() { + val = v.Index(int(intIdx)) + } else { + val = nil + } + case hashable: + strIdx, ok := idx.(strObject) + if !ok { + return nil, errors.New("expected string for hashable") + } + val = v.Value(string(strIdx)) + default: + return val, nil + } + } + + return val, nil +} + func mapBuiltin(ctx context.Context, args invocationArgs) (object, error) { if err := args.expectArgn(2); err != nil { return nil, err diff --git a/cmdlang/inst.go b/cmdlang/inst.go index e5453b8..97f2332 100644 --- a/cmdlang/inst.go +++ b/cmdlang/inst.go @@ -31,6 +31,7 @@ func New(opts ...InstOption) *Inst { rootEC.addCmd("set", invokableFunc(setBuiltin)) rootEC.addCmd("toUpper", invokableFunc(toUpperBuiltin)) //rootEC.addCmd("cat", invokableFunc(catBuiltin)) + rootEC.addCmd("index", invokableFunc(indexBuiltin)) rootEC.addCmd("call", invokableFunc(callBuiltin)) rootEC.addCmd("map", invokableFunc(mapBuiltin)) diff --git a/cmdlang/objs.go b/cmdlang/objs.go index b439a74..33a3d4d 100644 --- a/cmdlang/objs.go +++ b/cmdlang/objs.go @@ -20,6 +20,7 @@ type listable interface { type hashable interface { Len() int + Value(k string) object Each(func(k string, v object) error) error } @@ -59,6 +60,10 @@ func (s hashObject) Len() int { return len(s) } +func (s hashObject) Value(k string) object { + return s[k] +} + func (s hashObject) Each(fn func(k string, v object) error) error { for k, v := range s { if err := fn(k, v); err != nil { @@ -133,6 +138,8 @@ func toGoValue(obj object) (interface{}, bool) { return v.p, true case listableProxyObject: return v.v.Interface(), true + case structProxyObject: + return v.v.Interface(), true } return nil, false @@ -144,11 +151,16 @@ func fromGoValue(v any) (object, error) { return nil, nil case string: return strObject(t), nil + case int: + return intObject(t), nil } resVal := reflect.ValueOf(v) - if resVal.Type().Kind() == reflect.Slice { + switch resVal.Kind() { + case reflect.Slice: return listableProxyObject{resVal}, nil + case reflect.Struct: + return structProxyObject{resVal}, nil } return proxyObject{v}, nil @@ -378,3 +390,42 @@ func (p listableProxyObject) Index(i int) object { } return e } + +type structProxyObject struct { + v reflect.Value +} + +func (s structProxyObject) String() string { + return fmt.Sprintf("structProxyObject{%v}", s.v.Type()) +} + +func (s structProxyObject) Truthy() bool { + return true +} + +func (s structProxyObject) Len() int { + return s.v.Type().NumField() +} + +func (s structProxyObject) Value(k string) object { + e, err := fromGoValue(s.v.FieldByName(k).Interface()) + if err != nil { + return nil + } + return e +} + +func (s structProxyObject) Each(fn func(k string, v object) error) error { + for i := 0; i < s.v.Type().NumField(); i++ { + f := s.v.Type().Field(i).Name + v, err := fromGoValue(s.v.Field(i).Interface()) + if err != nil { + v = nil + } + + if err := fn(f, v); err != nil { + return err + } + } + return nil +} diff --git a/cmdlang/testbuiltins_test.go b/cmdlang/testbuiltins_test.go index 30fc5ca..fca1ff5 100644 --- a/cmdlang/testbuiltins_test.go +++ b/cmdlang/testbuiltins_test.go @@ -188,6 +188,7 @@ func TestBuiltins_ForEach(t *testing.T) { // TODO: hash is not sorted, so need to find a way to sort it {desc: "iterate over map", expr: ` foreach [a:"1"] { |k v| echo $k "=" $v }`, want: "a=1\n(nil)\n"}, + {desc: "iterate via pipe", expr: `["2" "4" "6"] | foreach { |x| echo $x }`, want: "2\n4\n6\n(nil)\n"}, } for _, tt := range tests { @@ -329,3 +330,59 @@ func TestBuiltins_Map(t *testing.T) { }) } } + +func TestBuiltins_Index(t *testing.T) { + tests := []struct { + desc string + expr string + want string + }{ + {desc: "index from list 1", expr: `index ["alpha" "beta" "gamma"] 0`, want: "alpha\n"}, + {desc: "index from list 2", expr: `index ["alpha" "beta" "gamma"] 1`, want: "beta\n"}, + {desc: "index from list 3", expr: `index ["alpha" "beta" "gamma"] 2`, want: "gamma\n"}, + {desc: "index from list 4", expr: `index ["alpha" "beta" "gamma"] 3`, want: "(nil)\n"}, + + {desc: "index from hash 1", expr: `index ["first":"alpha" "second":"beta" "third":"gamma"] "first"`, want: "alpha\n"}, + {desc: "index from hash 2", expr: `index ["first":"alpha" "second":"beta" "third":"gamma"] "second"`, want: "beta\n"}, + {desc: "index from hash 3", expr: `index ["first":"alpha" "second":"beta" "third":"gamma"] "third"`, want: "gamma\n"}, + {desc: "index from hash 4", expr: `index ["first":"alpha" "second":"beta" "third":"gamma"] "missing"`, want: "(nil)\n"}, + + {desc: "multi-list 1", expr: `index [[1 2] [3 4]] 0 1`, want: "2\n"}, + {desc: "multi-list 2", expr: `index [[1 2] [3 4]] 1 0`, want: "3\n"}, + {desc: "list of hash 1", expr: `index [["id":"abc"] ["id":"123"]] 0 id`, want: "abc\n"}, + {desc: "list of hash 2", expr: `index [["id":"abc"] ["id":"123"]] 1 id`, want: "123\n"}, + + {desc: "go list 1", expr: `goInt | index 1`, want: "5\n"}, + {desc: "go list 2", expr: `goInt | index 2`, want: "4\n"}, + {desc: "go struct 1", expr: `goStruct | index Alpha`, want: "foo\n"}, + {desc: "go struct 2", expr: `goStruct | index Beta`, want: "bar\n"}, + {desc: "go struct 3", expr: `goStruct | index Gamma 1`, want: "33\n"}, + } + + 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 []int + }{ + Alpha: "foo", + Beta: "bar", + Gamma: []int{22, 33}, + }, nil + }) + err := inst.EvalAndDisplay(ctx, tt.expr) + + assert.NoError(t, err) + assert.Equal(t, tt.want, outW.String()) + }) + } +} diff --git a/cmdlang/userbuiltin.go b/cmdlang/userbuiltin.go index 7e825f3..ab099d1 100644 --- a/cmdlang/userbuiltin.go +++ b/cmdlang/userbuiltin.go @@ -68,18 +68,33 @@ func bindArg(v interface{}, arg object) error { *t = arg.String() } - // Check for proxy object - if po, ok := arg.(proxyObject); ok { - poValue := reflect.ValueOf(po.p) - argValue := reflect.ValueOf(v) + switch t := arg.(type) { + case proxyObject: + return bindProxyObject(v, reflect.ValueOf(t.p)) + case listableProxyObject: + return bindProxyObject(v, t.v) + case structProxyObject: + return bindProxyObject(v, t.v) + } - if argValue.Type().Kind() != reflect.Pointer { + return nil +} + +func bindProxyObject(v interface{}, r reflect.Value) error { + argValue := reflect.ValueOf(v) + if argValue.Kind() != reflect.Ptr { + return errors.New("v must be a pointer to a struct") + } + + for { + if r.Type().AssignableTo(argValue.Elem().Type()) { + argValue.Elem().Set(r) return nil - } else if !poValue.Type().AssignableTo(argValue.Elem().Type()) { + } + if r.Type().Kind() != reflect.Pointer { return nil } - argValue.Elem().Set(poValue) + r = r.Elem() } - return nil } diff --git a/cmdlang/userbuiltin_test.go b/cmdlang/userbuiltin_test.go index 9fa6c30..bb6cbee 100644 --- a/cmdlang/userbuiltin_test.go +++ b/cmdlang/userbuiltin_test.go @@ -143,7 +143,6 @@ func TestInst_SetBuiltin(t *testing.T) { }{ {descr: "return as is", expr: `countTo3`, want: []string{"1", "2", "3"}}, {descr: "iterate over", expr: `foreach (countTo3) { |x| echo $x }`, wantOut: "1\n2\n3\n"}, - {descr: "iterate via foreach", expr: `["2" "4" "6"] | foreach { |x| echo $x }`, wantOut: "2\n4\n6\n"}, } for _, tt := range tests {