diff --git a/go.mod b/go.mod index b65ae1b..b7b4cb6 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/chzyer/readline v1.5.1 github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f github.com/stretchr/testify v1.10.0 + lmika.dev/pkg/modash v0.0.0-20250619112300-0be0b6b35b1b ) require ( @@ -17,5 +18,4 @@ require ( go.abhg.dev/goldmark/frontmatter v0.2.0 // indirect golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - lmika.dev/pkg/modash v0.0.0-20250619112300-0be0b6b35b1b // indirect ) diff --git a/go.sum b/go.sum index a9528c9..99a7730 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= diff --git a/ucl/builtins/fns_test.go b/ucl/builtins/fns_test.go new file mode 100644 index 0000000..a7fe418 --- /dev/null +++ b/ucl/builtins/fns_test.go @@ -0,0 +1,31 @@ +package builtins_test + +import ( + "context" + "github.com/stretchr/testify/assert" + "testing" + "ucl.lmika.dev/ucl" + "ucl.lmika.dev/ucl/builtins" +) + +func TestFns_Uni(t *testing.T) { + tests := []struct { + descr string + eval string + want any + }{ + {descr: "uni 1", eval: `s = fns:uni add 2 ; $s 3`, want: 5}, + {descr: "uni 2", eval: `s = fns:uni (proc { |x| add $x 1 }) ; $s 3`, want: 4}, + } + + for _, tt := range tests { + t.Run(tt.descr, func(t *testing.T) { + inst := ucl.New( + ucl.WithModule(builtins.Fns()), + ) + res, err := inst.EvalString(context.Background(), tt.eval) + assert.NoError(t, err) + assert.Equal(t, tt.want, res) + }) + } +} diff --git a/ucl/builtins/lists.go b/ucl/builtins/lists.go index 4e70095..20df5b2 100644 --- a/ucl/builtins/lists.go +++ b/ucl/builtins/lists.go @@ -10,12 +10,45 @@ func Lists() ucl.Module { return ucl.Module{ Name: "lists", Builtins: map[string]ucl.BuiltinHandler{ - "first": listFirst, - "uniq": listUniq, + "append": listAppend, + "first": listFirst, + "batch": listBatch, + "uniq": listUniq, }, } } +func listAppend(ctx context.Context, args ucl.CallArgs) (any, error) { + var ( + what ucl.Object + item ucl.Object + ) + + if err := args.Bind(&what); err != nil { + return nil, err + } + + if what == nil { + what = ucl.NewListObject() + } + + t, ok := what.(ucl.ModListable) + if !ok { + return nil, errors.New("expected mutable list") + } + + for args.NArgs() > 0 { + if err := args.Bind(&item); err != nil { + return nil, err + } + if err := t.Insert(-1, item); err != nil { + return nil, err + } + } + + return t, nil +} + func listFirst(ctx context.Context, args ucl.CallArgs) (any, error) { var ( what ucl.Object @@ -125,3 +158,34 @@ func listUniq(ctx context.Context, args ucl.CallArgs) (any, error) { return found, nil } + +func listBatch(ctx context.Context, args ucl.CallArgs) (any, error) { + var ( + what ucl.Object + groupSize int + ) + + if err := args.Bind(&what, &groupSize); err != nil { + return nil, err + } + + if groupSize <= 0 { + return nil, errors.New("group size must be > 0") + } + + groups := ucl.NewListObject() + var thisGroup *ucl.ListObject + + if err := eachListOrIterItem(ctx, what, func(idx int, v ucl.Object) error { + if thisGroup == nil || thisGroup.Len() == groupSize { + thisGroup = ucl.NewListObject() + groups.Append(thisGroup) + } + thisGroup.Append(v) + return nil + }); err != nil { + return nil, err + } + + return groups, nil +} diff --git a/ucl/builtins/lists_test.go b/ucl/builtins/lists_test.go index 72ccf65..cf65a2c 100644 --- a/ucl/builtins/lists_test.go +++ b/ucl/builtins/lists_test.go @@ -8,6 +8,44 @@ import ( "ucl.lmika.dev/ucl/builtins" ) +func TestLists_Append(t *testing.T) { + tests := []struct { + desc string + eval string + want any + wantErr bool + }{ + {desc: "append 1", eval: `lists:append [1 2 3] 4`, want: []any{1, 2, 3, 4}}, + {desc: "append 2", eval: `lists:append [1 2 3] 4 5 6`, want: []any{1, 2, 3, 4, 5, 6}}, + {desc: "append 3", eval: `lists:append [] 1 2 3`, want: []any{1, 2, 3}}, + {desc: "append 4", eval: `lists:append () 1 2 3`, want: []any{1, 2, 3}}, + {desc: "append 5", eval: `lists:append [1 2 3]`, want: []any{1, 2, 3}}, + {desc: "append 6", eval: `l = [] ; lists:append $l 1 2 3 ; $l`, want: []any{1, 2, 3}}, + {desc: "append 7", eval: `l = [1 2 3] ; lists:append $l [4 5 6] ; $l`, want: []any{1, 2, 3, []any{4, 5, 6}}}, + {desc: "append 8", eval: `lists:append (seq 3 | itrs:from | itrs:to-list) 4 5 6`, want: []any{0, 1, 2, 4, 5, 6}}, + + {desc: "err 1", eval: `lists:append "asa" 1`, wantErr: true}, + {desc: "err 2", eval: `lists:append 123 1`, wantErr: true}, + {desc: "err 3", eval: `lists:append [:] 1`, wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + inst := ucl.New( + ucl.WithModule(builtins.Itrs()), + ucl.WithModule(builtins.Lists()), + ) + res, err := inst.EvalString(context.Background(), tt.eval) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, res) + } + }) + } +} + func TestLists_First(t *testing.T) { tests := []struct { desc string @@ -45,6 +83,42 @@ func TestLists_First(t *testing.T) { } } +func TestLists_Batch(t *testing.T) { + tests := []struct { + desc string + eval string + want any + wantErr bool + }{ + {desc: "batch 1", eval: `lists:batch [1 2 3 4 5] 2`, want: []any{[]any{1, 2}, []any{3, 4}, []any{5}}}, + {desc: "batch 2", eval: `lists:batch [1 2 3 4] 2`, want: []any{[]any{1, 2}, []any{3, 4}}}, + {desc: "batch 3", eval: `lists:batch [1 2 3 4 5] 3`, want: []any{[]any{1, 2, 3}, []any{4, 5}}}, + {desc: "batch 4", eval: `lists:batch [1 2 3 4 5] 12`, want: []any{[]any{1, 2, 3, 4, 5}}}, + {desc: "batch 5", eval: `lists:batch [1 2 3 4 5] 1`, want: []any{[]any{1}, []any{2}, []any{3}, []any{4}, []any{5}}}, + {desc: "batch 6", eval: `lists:batch [1] 12`, want: []any{[]any{1}}}, + {desc: "batch 7", eval: `lists:batch [] 12`, want: []any{}}, + + {desc: "err 1", eval: `lists:batch [1 2 3] -3`, wantErr: true}, + {desc: "err 2", eval: `lists:batch [1 2 3] 0`, wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + inst := ucl.New( + ucl.WithModule(builtins.Itrs()), + ucl.WithModule(builtins.Lists()), + ) + res, err := inst.EvalString(context.Background(), tt.eval) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, res) + } + }) + } +} + func TestLists_Uniq(t *testing.T) { tests := []struct { desc string