From 789add45bb91f4bc526fd8aadc8898aa8665dc45 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Fri, 10 May 2024 05:38:39 +0000 Subject: [PATCH] Added 'add', 'filter' and 'reduce' builtins --- ucl/builtins.go | 129 ++++++++++++++++++++++++++++++++++++++- ucl/inst.go | 4 ++ ucl/testbuiltins_test.go | 57 +++++++++++++++++ 3 files changed, 188 insertions(+), 2 deletions(-) diff --git a/ucl/builtins.go b/ucl/builtins.go index b14f3a4..04009a5 100644 --- a/ucl/builtins.go +++ b/ucl/builtins.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strconv" "strings" ) @@ -28,6 +29,30 @@ func echoBuiltin(ctx context.Context, args invocationArgs) (object, error) { return nil, nil } +func addBuiltin(ctx context.Context, args invocationArgs) (object, error) { + if len(args.args) == 0 { + return intObject(0), nil + } + + n := 0 + for i, a := range args.args { + switch t := a.(type) { + case intObject: + n += int(t) + case strObject: + v, err := strconv.Atoi(string(t)) + if err != nil { + return nil, fmt.Errorf("arg %v of 'add' not convertable to an int", i) + } + n += v + default: + return nil, fmt.Errorf("arg %v of 'add' not convertable to an int", i) + } + } + + return intObject(n), nil +} + func setBuiltin(ctx context.Context, args invocationArgs) (object, error) { if err := args.expectArgn(2); err != nil { return nil, err @@ -181,8 +206,6 @@ func keysBuiltin(ctx context.Context, args invocationArgs) (object, error) { return nil, err } return keys, nil - default: - return nil, nil } return nil, nil @@ -215,6 +238,108 @@ func mapBuiltin(ctx context.Context, args invocationArgs) (object, error) { return nil, errors.New("expected listable") } +func filterBuiltin(ctx context.Context, args invocationArgs) (object, error) { + if err := args.expectArgn(2); err != nil { + return nil, err + } + + inv, err := args.invokableArg(1) + if err != nil { + return nil, err + } + + switch t := args.args[0].(type) { + case listable: + l := t.Len() + newList := listObject{} + for i := 0; i < l; i++ { + v := t.Index(i) + m, err := inv.invoke(ctx, args.fork([]object{v})) + if err != nil { + return nil, err + } else if m.Truthy() { + newList = append(newList, v) + } + } + 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{strObject(k), v})); err != nil { + return err + } else if m.Truthy() { + newHash[k] = v + } + return nil + }); err != nil { + return nil, err + } + return newHash, nil + } + return nil, errors.New("expected listable") +} + +func reduceBuiltin(ctx context.Context, args invocationArgs) (object, error) { + var err error + if err = args.expectArgn(2); err != nil { + return nil, err + } + + var ( + accum object + setFirst bool + block invokable + ) + if len(args.args) == 3 { + accum = args.args[1] + block, err = args.invokableArg(2) + if err != nil { + return nil, err + } + } else { + setFirst = true + block, err = args.invokableArg(1) + if err != nil { + return nil, err + } + } + + switch t := args.args[0].(type) { + case listable: + l := t.Len() + for i := 0; i < l; i++ { + v := t.Index(i) + if setFirst { + accum = v + setFirst = false + continue + } + + newAccum, err := block.invoke(ctx, args.fork([]object{v, accum})) + if err != nil { + return nil, err + } + + accum = newAccum + } + return accum, nil + case hashable: + // TODO: should raise error? + if err := t.Each(func(k string, v object) error { + newAccum, err := block.invoke(ctx, args.fork([]object{strObject(k), v, accum})) + if err != nil { + return err + } + accum = newAccum + return nil + }); err != nil { + return nil, err + } + return accum, nil + } + return nil, errors.New("expected listable") +} + func firstBuiltin(ctx context.Context, args invocationArgs) (object, error) { if err := args.expectArgn(1); err != nil { return nil, err diff --git a/ucl/inst.go b/ucl/inst.go index b0d7ae7..7fa1f56 100644 --- a/ucl/inst.go +++ b/ucl/inst.go @@ -55,9 +55,13 @@ func New(opts ...InstOption) *Inst { rootEC.addCmd("call", invokableFunc(callBuiltin)) rootEC.addCmd("map", invokableFunc(mapBuiltin)) + rootEC.addCmd("filter", invokableFunc(filterBuiltin)) rootEC.addCmd("head", invokableFunc(firstBuiltin)) + rootEC.addCmd("reduce", invokableFunc(reduceBuiltin)) rootEC.addCmd("eq", invokableFunc(eqBuiltin)) + rootEC.addCmd("add", invokableFunc(addBuiltin)) + rootEC.addCmd("cat", invokableFunc(concatBuiltin)) rootEC.addCmd("break", invokableFunc(breakBuiltin)) rootEC.addCmd("continue", invokableFunc(continueBuiltin)) diff --git a/ucl/testbuiltins_test.go b/ucl/testbuiltins_test.go index fe1c71a..06d393c 100644 --- a/ucl/testbuiltins_test.go +++ b/ucl/testbuiltins_test.go @@ -697,3 +697,60 @@ func TestBuiltins_Keys(t *testing.T) { }) } } + +func TestBuiltins_Filter(t *testing.T) { + tests := []struct { + desc string + expr string + want any + }{ + {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 map 1", expr: `filter [alpha:"hello" bravo:"world"] { |k v| eq $k "alpha" }`, want: map[string]any{ + "alpha": "hello", + }}, + {desc: "filter map 2", expr: `filter [alpha:"hello" bravo:"world"] { |k v| eq $v "world" }`, want: map[string]any{ + "bravo": "world", + }}, + {desc: "filter map 3", expr: `filter [alpha:"hello" bravo:"world"] { |k v| eq $v "alpha" }`, want: map[string]any{}}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + ctx := context.Background() + outW := bytes.NewBuffer(nil) + + inst := New(WithOut(outW), WithTestBuiltin()) + + res, err := inst.Eval(ctx, tt.expr) + assert.NoError(t, err) + assert.Equal(t, tt.want, res) + }) + } +} + +func TestBuiltins_Reduce(t *testing.T) { + tests := []struct { + desc string + expr string + want any + }{ + {desc: "reduce list 1", expr: `reduce [1 1 1] { |x a| add $x $a }`, want: 3}, + {desc: "reduce list 2", expr: `reduce [1 1 1] 20 { |x a| add $x $a }`, want: 23}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + ctx := context.Background() + outW := bytes.NewBuffer(nil) + + inst := New(WithOut(outW), WithTestBuiltin()) + + res, err := inst.Eval(ctx, tt.expr) + assert.NoError(t, err) + assert.Equal(t, tt.want, res) + }) + } +}