Fixed binding with struct

This commit is contained in:
Leon Mika 2024-04-24 21:09:52 +10:00
parent 24a484ab77
commit 43098fa227
6 changed files with 165 additions and 10 deletions

View file

@ -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

View file

@ -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))

View file

@ -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
}

View file

@ -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())
})
}
}

View file

@ -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
}

View file

@ -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 {