diff --git a/cmdlang/builtins.go b/cmdlang/builtins.go index 6b137fc..8b8e6fa 100644 --- a/cmdlang/builtins.go +++ b/cmdlang/builtins.go @@ -113,6 +113,23 @@ func callBuiltin(ctx context.Context, args invocationArgs) (object, error) { return inv.invoke(ctx, args.shift(1)) } +func lenBuiltin(ctx context.Context, args invocationArgs) (object, error) { + if err := args.expectArgn(1); err != nil { + return nil, err + } + + switch v := args.args[0].(type) { + case strObject: + return intObject(len(string(v))), nil + case listable: + return intObject(v.Len()), nil + case hashable: + return intObject(v.Len()), nil + } + + return intObject(0), nil +} + func indexBuiltin(ctx context.Context, args invocationArgs) (object, error) { if err := args.expectArgn(1); err != nil { return nil, err diff --git a/cmdlang/inst.go b/cmdlang/inst.go index 97f2332..53e35e2 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("len", invokableFunc(lenBuiltin)) rootEC.addCmd("index", invokableFunc(indexBuiltin)) rootEC.addCmd("call", invokableFunc(callBuiltin)) diff --git a/cmdlang/objs.go b/cmdlang/objs.go index 33a3d4d..2f5d010 100644 --- a/cmdlang/objs.go +++ b/cmdlang/objs.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "github.com/lmika/gopkgs/fp/slices" "reflect" "strconv" ) @@ -160,7 +161,7 @@ func fromGoValue(v any) (object, error) { case reflect.Slice: return listableProxyObject{resVal}, nil case reflect.Struct: - return structProxyObject{resVal}, nil + return newStructProxyObject(resVal), nil } return proxyObject{v}, nil @@ -392,7 +393,15 @@ func (p listableProxyObject) Index(i int) object { } type structProxyObject struct { - v reflect.Value + v reflect.Value + vf []reflect.StructField +} + +func newStructProxyObject(v reflect.Value) structProxyObject { + return structProxyObject{ + v: v, + vf: slices.Filter(reflect.VisibleFields(v.Type()), func(t reflect.StructField) bool { return t.IsExported() }), + } } func (s structProxyObject) String() string { @@ -404,7 +413,7 @@ func (s structProxyObject) Truthy() bool { } func (s structProxyObject) Len() int { - return s.v.Type().NumField() + return len(s.vf) } func (s structProxyObject) Value(k string) object { @@ -416,14 +425,13 @@ func (s structProxyObject) Value(k string) object { } 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()) + for _, f := range s.vf { + v, err := fromGoValue(s.v.FieldByName(f.Name).Interface()) if err != nil { v = nil } - if err := fn(f, v); err != nil { + if err := fn(f.Name, v); err != nil { return err } } diff --git a/cmdlang/testbuiltins_test.go b/cmdlang/testbuiltins_test.go index fca1ff5..6b5fe84 100644 --- a/cmdlang/testbuiltins_test.go +++ b/cmdlang/testbuiltins_test.go @@ -386,3 +386,62 @@ func TestBuiltins_Index(t *testing.T) { }) } } + +func TestBuiltins_Len(t *testing.T) { + tests := []struct { + desc string + expr string + want string + }{ + {desc: "len of list 1", expr: `len ["alpha" "beta" "gamma"]`, want: "3\n"}, + {desc: "len of list 2", expr: `len ["alpha"]`, want: "1\n"}, + {desc: "len of list 3", expr: `len []`, want: "0\n"}, + + {desc: "len of hash 1", expr: `len ["first":"alpha" "second":"beta" "third":"gamma"]`, want: "3\n"}, + {desc: "len of hash 2", expr: `len ["first":"alpha" "second":"beta"]`, want: "2\n"}, + {desc: "len of hash 3", expr: `len ["first":"alpha"]`, want: "1\n"}, + {desc: "len of hash 4", expr: `len [:]`, want: "0\n"}, + + {desc: "len of string 1", expr: `len "Hello, world"`, want: "12\n"}, + {desc: "len of string 2", expr: `len "chair"`, want: "5\n"}, + {desc: "len of string 3", expr: `len ""`, want: "0\n"}, + + {desc: "len of int", expr: `len 1232`, want: "0\n"}, + {desc: "len of nil", expr: `len ()`, want: "0\n"}, + + {desc: "go list 1", expr: `goInt | len`, want: "3\n"}, + {desc: "go struct 1", expr: `goStruct | len`, want: "3\n"}, + {desc: "go struct 2", expr: `index (goStruct) Gamma | len`, want: "2\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 + hidden string + missing string + }{ + Alpha: "foo", + Beta: "bar", + Gamma: []int{22, 33}, + hidden: "hidden", + missing: "missing", + }, nil + }) + err := inst.EvalAndDisplay(ctx, tt.expr) + + assert.NoError(t, err) + assert.Equal(t, tt.want, outW.String()) + }) + } +}