diff --git a/ucl/builtins.go b/ucl/builtins.go index 734663e..a26ccb4 100644 --- a/ucl/builtins.go +++ b/ucl/builtins.go @@ -517,7 +517,7 @@ func keysBuiltin(ctx context.Context, args invocationArgs) (Object, error) { }); err != nil { return nil, err } - return keys, nil + return &keys, nil } return nil, nil @@ -545,7 +545,7 @@ func mapBuiltin(ctx context.Context, args invocationArgs) (Object, error) { } newList = append(newList, m) } - return newList, nil + return &newList, nil } return nil, errors.New("expected listable") } @@ -569,17 +569,17 @@ func filterBuiltin(ctx context.Context, args invocationArgs) (Object, error) { m, err := inv.invoke(ctx, args.fork([]Object{v})) if err != nil { return nil, err - } else if m.Truthy() { + } else if m != nil && m.Truthy() { newList = append(newList, v) } } - return newList, nil + return &newList, nil case hashable: newHash := hashObject{} if err := t.Each(func(k string, v Object) error { if m, err := inv.invoke(ctx, args.fork([]Object{StringObject(k), v})); err != nil { return err - } else if m.Truthy() { + } else if m != nil && m.Truthy() { newHash[k] = v } return nil diff --git a/ucl/builtins/csv.go b/ucl/builtins/csv.go index 3b00cf4..95b30d5 100644 --- a/ucl/builtins/csv.go +++ b/ucl/builtins/csv.go @@ -6,6 +6,7 @@ import ( "errors" "io" "io/fs" + "os" "strings" "ucl.lmika.dev/ucl" ) @@ -25,6 +26,13 @@ func CSV(fs fs.FS) ucl.Module { } } +func (h csvHandlers) openFile(name string) (fs.File, error) { + if h.fs == nil { + return os.Open(name) + } + return h.fs.Open(name) +} + func (h csvHandlers) eachRecord(ctx context.Context, args ucl.CallArgs) (any, error) { var ( filename string @@ -35,7 +43,7 @@ func (h csvHandlers) eachRecord(ctx context.Context, args ucl.CallArgs) (any, er return nil, err } - f, err := h.fs.Open(filename) + f, err := h.openFile(filename) if err != nil { return nil, err } diff --git a/ucl/eval.go b/ucl/eval.go index 4f96ce3..506cb12 100644 --- a/ucl/eval.go +++ b/ucl/eval.go @@ -203,7 +203,7 @@ func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (Objec func (e evaluator) evalListOrHash(ctx context.Context, ec *evalCtx, loh *astListOrHash) (Object, error) { if loh.EmptyList { - return listObject{}, nil + return &listObject{}, nil } else if loh.EmptyHash { return hashObject{}, nil } @@ -241,7 +241,7 @@ func (e evaluator) evalListOrHash(ctx context.Context, ec *evalCtx, loh *astList } l = append(l, v) } - return l, nil + return &l, nil } func (e evaluator) evalLiteral(ctx context.Context, ec *evalCtx, n *astLiteral) (Object, error) { diff --git a/ucl/objs.go b/ucl/objs.go index 59ff80b..934b7f2 100644 --- a/ucl/objs.go +++ b/ucl/objs.go @@ -22,6 +22,16 @@ type Listable interface { Index(i int) Object } +type ModListable interface { + Listable + + // Insert adds a new item to the list. idx can be a positive + // number from 0 to len(), in which case the object will be inserted + // at that position. If idx is negative, then the item will be inserted + // at that position from the right. + Insert(idx int, obj Object) error +} + type hashable interface { Len() int Value(k string) Object @@ -34,20 +44,32 @@ func (lo *listObject) Append(o Object) { *lo = append(*lo, o) } -func (s listObject) String() string { - return fmt.Sprintf("%v", []Object(s)) +func (lo *listObject) Insert(idx int, obj Object) error { + if idx != -1 { + return errors.New("not supported") + } + *lo = append(*lo, obj) + return nil } -func (s listObject) Truthy() bool { - return len(s) > 0 +func (s *listObject) String() string { + return fmt.Sprintf("%v", []Object(*s)) } -func (s listObject) Len() int { - return len(s) +func (s *listObject) Truthy() bool { + return len(*s) > 0 } -func (s listObject) Index(i int) Object { - return s[i] +func (s *listObject) Len() int { + return len(*s) +} + +func (s *listObject) Index(i int) Object { + return (*s)[i] +} + +func (s *listObject) Add(o Object) { + *s = append(*s, o) } type hashObject map[string]Object @@ -149,9 +171,9 @@ func toGoValue(obj Object) (interface{}, bool) { return bool(v), true case timeObject: return time.Time(v), true - case listObject: - xs := make([]interface{}, 0, len(v)) - for _, va := range v { + case *listObject: + xs := make([]interface{}, 0, len(*v)) + for _, va := range *v { x, ok := toGoValue(va) if !ok { continue diff --git a/ucl/testbuiltins_test.go b/ucl/testbuiltins_test.go index d8895c9..3727a77 100644 --- a/ucl/testbuiltins_test.go +++ b/ucl/testbuiltins_test.go @@ -38,7 +38,9 @@ func WithTestBuiltin() InstOption { })) i.rootEC.addCmd("list", invokableFunc(func(ctx context.Context, args invocationArgs) (Object, error) { - return listObject(args.args), nil + var a listObject = make([]Object, len(args.args)) + copy(a, args.args) + return &a, nil })) i.rootEC.addCmd("error", invokableFunc(func(ctx context.Context, args invocationArgs) (Object, error) { @@ -1170,6 +1172,8 @@ func TestBuiltins_Filter(t *testing.T) { {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 list 4", expr: `filter [() () ()] { |x| $x }`, want: []any{}}, + {desc: "filter list 5", expr: `filter [] { |x| $x }`, want: []any{}}, {desc: "filter map 1", expr: `filter [alpha:"hello" bravo:"world"] { |k v| eq $k "alpha" }`, want: map[string]any{ "alpha": "hello", diff --git a/ucl/userbuiltin.go b/ucl/userbuiltin.go index 1294e47..ce26e40 100644 --- a/ucl/userbuiltin.go +++ b/ucl/userbuiltin.go @@ -121,6 +121,12 @@ func (ca CallArgs) bindArg(v interface{}, arg Object) error { } *t = i return nil + case *ModListable: + if i, ok := arg.(ModListable); ok { + *t = i + return nil + } + return errors.New("exepected listable") case *string: if arg != nil { *t = arg.String()