From d8460f69bcc092fa9f657a3a91ffd67d0e90625c Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sun, 25 May 2025 09:50:25 +1000 Subject: [PATCH 01/22] Added separate Eval mode with option to isolate the environment This allows for keeping variables isolated --- repl/evaldisplay.go | 2 +- ucl/ast.go | 2 +- ucl/builtins/csv_test.go | 2 +- ucl/builtins/fs_test.go | 2 +- ucl/builtins/itrs_test.go | 2 +- ucl/builtins/lists_test.go | 2 +- ucl/builtins/log_test.go | 2 +- ucl/builtins/os_test.go | 2 +- ucl/builtins/strs_test.go | 12 ++--- ucl/builtins/time_test.go | 4 +- ucl/env.go | 3 ++ ucl/inst.go | 42 +++++++++++++++--- ucl/inst_test.go | 89 +++++++++++++++++++++++++++++++++++++- ucl/testbuiltins_test.go | 28 ++++++------ ucl/userbuiltin_test.go | 40 ++++++++--------- 15 files changed, 176 insertions(+), 58 deletions(-) diff --git a/repl/evaldisplay.go b/repl/evaldisplay.go index a37e74f..083e4e3 100644 --- a/repl/evaldisplay.go +++ b/repl/evaldisplay.go @@ -14,7 +14,7 @@ import ( type NoResults struct{} func (r *REPL) EvalAndDisplay(ctx context.Context, expr string) error { - res, err := r.inst.Eval(ctx, expr) + res, err := r.inst.EvalString(ctx, expr) if err != nil { if errors.Is(err, ucl.ErrNotConvertable) { return nil diff --git a/ucl/ast.go b/ucl/ast.go index ab2e2bc..3b567d9 100644 --- a/ucl/ast.go +++ b/ucl/ast.go @@ -172,6 +172,6 @@ var scanner = lexer.MustStateful(lexer.Rules{ var parser = participle.MustBuild[astScript](participle.Lexer(scanner), participle.Elide("Whitespace", "Comment")) -func parse(r io.Reader) (*astScript, error) { +func parse(fname string, r io.Reader) (*astScript, error) { return parser.Parse("test", r) } diff --git a/ucl/builtins/csv_test.go b/ucl/builtins/csv_test.go index 5b60834..56953af 100644 --- a/ucl/builtins/csv_test.go +++ b/ucl/builtins/csv_test.go @@ -45,7 +45,7 @@ func TestCSV_ReadRecord(t *testing.T) { ucl.WithOut(&bfr), ) - _, err := inst.Eval(context.Background(), tt.eval) + _, err := inst.EvalString(context.Background(), tt.eval) assert.NoError(t, err) assert.Equal(t, tt.wantOut, bfr.String()) }) diff --git a/ucl/builtins/fs_test.go b/ucl/builtins/fs_test.go index f7f0e41..818cf11 100644 --- a/ucl/builtins/fs_test.go +++ b/ucl/builtins/fs_test.go @@ -29,7 +29,7 @@ func TestFS_Cat(t *testing.T) { inst := ucl.New( ucl.WithModule(builtins.FS(testFS)), ) - res, err := inst.Eval(context.Background(), tt.eval) + res, err := inst.EvalString(context.Background(), tt.eval) assert.NoError(t, err) assert.Equal(t, tt.want, res) }) diff --git a/ucl/builtins/itrs_test.go b/ucl/builtins/itrs_test.go index e852209..f27ba68 100644 --- a/ucl/builtins/itrs_test.go +++ b/ucl/builtins/itrs_test.go @@ -26,7 +26,7 @@ func TestItrs_ToList(t *testing.T) { inst := ucl.New( ucl.WithModule(builtins.Itrs()), ) - res, err := inst.Eval(context.Background(), tt.eval) + res, err := inst.EvalString(context.Background(), tt.eval) if tt.wantErr { assert.Error(t, err) } else { diff --git a/ucl/builtins/lists_test.go b/ucl/builtins/lists_test.go index ca86731..526176e 100644 --- a/ucl/builtins/lists_test.go +++ b/ucl/builtins/lists_test.go @@ -34,7 +34,7 @@ func TestLists_First(t *testing.T) { ucl.WithModule(builtins.Itrs()), ucl.WithModule(builtins.Lists()), ) - res, err := inst.Eval(context.Background(), tt.eval) + res, err := inst.EvalString(context.Background(), tt.eval) if tt.wantErr { assert.Error(t, err) } else { diff --git a/ucl/builtins/log_test.go b/ucl/builtins/log_test.go index 9071c02..f4b8e95 100644 --- a/ucl/builtins/log_test.go +++ b/ucl/builtins/log_test.go @@ -34,7 +34,7 @@ func TestLog_Puts(t *testing.T) { })), ) - res, err := inst.Eval(context.Background(), tt.eval) + res, err := inst.EvalString(context.Background(), tt.eval) if tt.wantErr { assert.Error(t, err) } else { diff --git a/ucl/builtins/os_test.go b/ucl/builtins/os_test.go index a246892..cbefcbf 100644 --- a/ucl/builtins/os_test.go +++ b/ucl/builtins/os_test.go @@ -28,7 +28,7 @@ func TestOS_Env(t *testing.T) { inst := ucl.New( ucl.WithModule(builtins.OS()), ) - res, err := inst.Eval(context.Background(), tt.eval) + res, err := inst.EvalString(context.Background(), tt.eval) assert.NoError(t, err) assert.Equal(t, tt.want, res) }) diff --git a/ucl/builtins/strs_test.go b/ucl/builtins/strs_test.go index 3fc4cf6..c38065b 100644 --- a/ucl/builtins/strs_test.go +++ b/ucl/builtins/strs_test.go @@ -28,7 +28,7 @@ func TestStrs_ToUpper(t *testing.T) { inst := ucl.New( ucl.WithModule(builtins.Strs()), ) - res, err := inst.Eval(context.Background(), tt.eval) + res, err := inst.EvalString(context.Background(), tt.eval) if tt.wantErr { assert.Error(t, err) } else { @@ -59,7 +59,7 @@ func TestStrs_ToLower(t *testing.T) { inst := ucl.New( ucl.WithModule(builtins.Strs()), ) - res, err := inst.Eval(context.Background(), tt.eval) + res, err := inst.EvalString(context.Background(), tt.eval) if tt.wantErr { assert.Error(t, err) } else { @@ -90,7 +90,7 @@ func TestStrs_Trim(t *testing.T) { inst := ucl.New( ucl.WithModule(builtins.Strs()), ) - res, err := inst.Eval(context.Background(), tt.eval) + res, err := inst.EvalString(context.Background(), tt.eval) if tt.wantErr { assert.Error(t, err) } else { @@ -122,7 +122,7 @@ func TestStrs_HasPrefix(t *testing.T) { inst := ucl.New( ucl.WithModule(builtins.Strs()), ) - res, err := inst.Eval(context.Background(), tt.eval) + res, err := inst.EvalString(context.Background(), tt.eval) if tt.wantErr { assert.Error(t, err) } else { @@ -160,7 +160,7 @@ func TestStrs_Split(t *testing.T) { inst := ucl.New( ucl.WithModule(builtins.Strs()), ) - res, err := inst.Eval(context.Background(), tt.eval) + res, err := inst.EvalString(context.Background(), tt.eval) if tt.wantErr { assert.Error(t, err) } else { @@ -191,7 +191,7 @@ func TestStrs_Join(t *testing.T) { ucl.WithModule(builtins.Itrs()), ucl.WithModule(builtins.Strs()), ) - res, err := inst.Eval(context.Background(), tt.eval) + res, err := inst.EvalString(context.Background(), tt.eval) if tt.wantErr { assert.Error(t, err) } else { diff --git a/ucl/builtins/time_test.go b/ucl/builtins/time_test.go index 4c95b5a..a0087fa 100644 --- a/ucl/builtins/time_test.go +++ b/ucl/builtins/time_test.go @@ -25,7 +25,7 @@ func TestTime_FromUnix(t *testing.T) { inst := ucl.New( ucl.WithModule(builtins.Time()), ) - res, err := inst.Eval(context.Background(), tt.eval) + res, err := inst.EvalString(context.Background(), tt.eval) if tt.wantErr { assert.Error(t, err) } else { @@ -47,7 +47,7 @@ func TestTime_Sleep(t *testing.T) { ucl.WithModule(builtins.Time()), ) - _, err := inst.Eval(ctx, `time:sleep 1`) + _, err := inst.EvalString(ctx, `time:sleep 1`) assert.Error(t, err) assert.Equal(t, "context canceled", err.Error()) assert.True(t, time.Now().Sub(st) < time.Second) diff --git a/ucl/env.go b/ucl/env.go index 3d217ca..6d92ffb 100644 --- a/ucl/env.go +++ b/ucl/env.go @@ -61,6 +61,9 @@ func (ec *evalCtx) setOrDefineVar(name string, val Object) { func (ec *evalCtx) getVar(name string) (Object, bool) { if ec.vars == nil { + if ec.parent == nil { + return nil, false + } return ec.parent.getVar(name) } diff --git a/ucl/inst.go b/ucl/inst.go index 585bea3..791b202 100644 --- a/ucl/inst.go +++ b/ucl/inst.go @@ -141,8 +141,25 @@ func (inst *Inst) Out() io.Writer { return inst.out } -func (inst *Inst) Eval(ctx context.Context, expr string) (any, error) { - res, err := inst.eval(ctx, expr) +type EvalOption func(*evalOptions) + +func WithSubEnv() EvalOption { + return func(opts *evalOptions) { + opts.forkEnv = true + } +} + +func (inst *Inst) Eval(ctx context.Context, r io.Reader, options ...EvalOption) (any, error) { + opts := evalOptions{ + filename: "unnamed", + forkEnv: false, + } + + for _, opt := range options { + opt(&opts) + } + + res, err := inst.eval(ctx, r, opts) if err != nil { if errors.Is(err, ErrHalt) { return nil, nil @@ -158,16 +175,29 @@ func (inst *Inst) Eval(ctx context.Context, expr string) (any, error) { return goRes, nil } -func (inst *Inst) eval(ctx context.Context, expr string) (Object, error) { - ast, err := parse(strings.NewReader(expr)) +func (inst *Inst) EvalString(ctx context.Context, expr string) (any, error) { + return inst.Eval(ctx, strings.NewReader(expr)) +} + +type evalOptions struct { + filename string + forkEnv bool +} + +func (inst *Inst) eval(ctx context.Context, r io.Reader, opts evalOptions) (Object, error) { + ast, err := parse(opts.filename, r) if err != nil { return nil, err } eval := evaluator{inst: inst} - // TODO: this should be a separate forkAndIsolate() session - return eval.evalScript(ctx, inst.rootEC, ast) + env := inst.rootEC + if opts.forkEnv { + env = env.fork() + } + + return eval.evalScript(ctx, env, ast) } type PseudoVarHandler interface { diff --git a/ucl/inst_test.go b/ucl/inst_test.go index 479c845..184a3b0 100644 --- a/ucl/inst_test.go +++ b/ucl/inst_test.go @@ -3,6 +3,7 @@ package ucl_test import ( "bytes" "context" + "strings" "ucl.lmika.dev/ucl" "github.com/stretchr/testify/assert" @@ -113,7 +114,7 @@ func TestInst_Eval(t *testing.T) { outW := bytes.NewBuffer(nil) inst := ucl.New(ucl.WithOut(outW), ucl.WithTestBuiltin()) - res, err := inst.Eval(ctx, tt.expr) + res, err := inst.EvalString(ctx, tt.expr) if tt.wantErr != nil { assert.ErrorIs(t, err, tt.wantErr) @@ -129,6 +130,90 @@ func TestInst_Eval(t *testing.T) { } } +func TestInst_Eval_WithSubEnv(t *testing.T) { + t.Run("global symbols should not leak across environments", func(t *testing.T) { + ctx := t.Context() + + inst := ucl.New() + + res, err := inst.Eval(ctx, strings.NewReader(`$a = "hello" ; $a`), ucl.WithSubEnv()) + assert.NoError(t, err) + assert.Equal(t, "hello", res) + + res, err = inst.Eval(ctx, strings.NewReader(`$a`), ucl.WithSubEnv()) + assert.NoError(t, err) + assert.Nil(t, res) + }) + + t.Run("when using hooks, reading vars are not permitted to leak across environment", func(t *testing.T) { + ctx := t.Context() + + hooks := make([]ucl.Invokable, 0) + + inst := ucl.New() + inst.SetBuiltin("hook", func(ctx context.Context, args ucl.CallArgs) (any, error) { + var hookProc ucl.Invokable + + if err := args.Bind(&hookProc); err != nil { + return nil, err + } + hooks = append(hooks, hookProc) + + return nil, nil + }) + + res, err := inst.Eval(ctx, strings.NewReader(`$a = "hello" ; hook { $a }`), ucl.WithSubEnv()) + assert.NoError(t, err) + assert.Nil(t, res) + + res, err = inst.Eval(ctx, strings.NewReader(`$a = "world" ; hook { $a }`), ucl.WithSubEnv()) + assert.NoError(t, err) + assert.Nil(t, res) + + h1, err := hooks[0].Invoke(ctx, ucl.CallArgs{}) + assert.NoError(t, err) + assert.Equal(t, "hello", h1) + + h2, err := hooks[1].Invoke(ctx, ucl.CallArgs{}) + assert.NoError(t, err) + assert.Equal(t, "world", h2) + }) + + t.Run("when using hooks, writing vars are not permitted to leak across environment", func(t *testing.T) { + ctx := t.Context() + + hooks := make([]ucl.Invokable, 0) + + inst := ucl.New() + inst.SetBuiltin("hook", func(ctx context.Context, args ucl.CallArgs) (any, error) { + var hookProc ucl.Invokable + + if err := args.Bind(&hookProc); err != nil { + return nil, err + } + hooks = append(hooks, hookProc) + + return nil, nil + }) + + res, err := inst.Eval(ctx, strings.NewReader(`$a = "hello" ; hook { $a = "new value" ; $a }`), ucl.WithSubEnv()) + assert.NoError(t, err) + assert.Nil(t, res) + + res, err = inst.Eval(ctx, strings.NewReader(`$a = "world" ; hook { $a }`), ucl.WithSubEnv()) + assert.NoError(t, err) + assert.Nil(t, res) + + h1, err := hooks[0].Invoke(ctx, ucl.CallArgs{}) + assert.NoError(t, err) + assert.Equal(t, "new value", h1) + + h2, err := hooks[1].Invoke(ctx, ucl.CallArgs{}) + assert.NoError(t, err) + assert.Equal(t, "world", h2) + }) +} + func TestInst_SetPseudoVar(t *testing.T) { tests := []struct { desc string @@ -155,7 +240,7 @@ func TestInst_SetPseudoVar(t *testing.T) { inst.SetPseudoVar("bar", bar) inst.SetMissingPseudoVarHandler(missingPseudoVarType{}) - res, err := inst.Eval(t.Context(), tt.expr) + res, err := inst.EvalString(t.Context(), tt.expr) if tt.wantErr { assert.Error(t, err) diff --git a/ucl/testbuiltins_test.go b/ucl/testbuiltins_test.go index 096e446..9ac1d61 100644 --- a/ucl/testbuiltins_test.go +++ b/ucl/testbuiltins_test.go @@ -151,7 +151,7 @@ func TestBuiltins_Echo(t *testing.T) { outW := bytes.NewBuffer(nil) inst := New(WithOut(outW), WithTestBuiltin()) - res, err := inst.Eval(ctx, tt.expr) + res, err := inst.EvalString(ctx, tt.expr) assert.NoError(t, err) assert.Nil(t, res) @@ -1309,7 +1309,7 @@ func TestBuiltins_Keys(t *testing.T) { }, nil }) - res, err := inst.Eval(ctx, tt.expr) + res, err := inst.EvalString(ctx, tt.expr) assert.NoError(t, err) assert.Len(t, res, len(tt.wantItems)) for _, i := range tt.wantItems { @@ -1349,7 +1349,7 @@ func TestBuiltins_Filter(t *testing.T) { inst := New(WithOut(outW), WithTestBuiltin()) - res, err := inst.Eval(ctx, tt.expr) + res, err := inst.EvalString(ctx, tt.expr) assert.NoError(t, err) assert.Equal(t, tt.want, res) }) @@ -1375,7 +1375,7 @@ func TestBuiltins_Reduce(t *testing.T) { inst := New(WithOut(outW), WithTestBuiltin()) - res, err := inst.Eval(ctx, tt.expr) + res, err := inst.EvalString(ctx, tt.expr) assert.NoError(t, err) assert.Equal(t, tt.want, res) }) @@ -1404,7 +1404,7 @@ func TestBuiltins_Head(t *testing.T) { inst := New(WithOut(outW), WithTestBuiltin()) - res, err := inst.Eval(ctx, tt.expr) + res, err := inst.EvalString(ctx, tt.expr) assert.NoError(t, err) assert.Equal(t, tt.want, res) }) @@ -1459,7 +1459,7 @@ func TestBuiltins_LtLeGtLe(t *testing.T) { inst.SetVar("true", true) inst.SetVar("false", false) - eqRes, err := inst.Eval(ctx, tt.expr) + eqRes, err := inst.EvalString(ctx, tt.expr) if tt.wantErr { assert.Error(t, err) @@ -1527,11 +1527,11 @@ func TestBuiltins_EqNe(t *testing.T) { inst.SetVar("true", true) inst.SetVar("false", false) - eqRes, err := inst.Eval(ctx, tt.expr) + eqRes, err := inst.EvalString(ctx, tt.expr) assert.NoError(t, err) assert.Equal(t, tt.want, eqRes) - neRes, err := inst.Eval(ctx, strings.ReplaceAll(tt.expr, "eq", "ne")) + neRes, err := inst.EvalString(ctx, strings.ReplaceAll(tt.expr, "eq", "ne")) assert.NoError(t, err) assert.Equal(t, !tt.want, neRes) }) @@ -1562,7 +1562,7 @@ func TestBuiltins_Str(t *testing.T) { inst := New(WithOut(outW), WithTestBuiltin()) - eqRes, err := inst.Eval(ctx, tt.expr) + eqRes, err := inst.EvalString(ctx, tt.expr) assert.NoError(t, err) assert.Equal(t, tt.want, eqRes) }) @@ -1598,7 +1598,7 @@ func TestBuiltins_Int(t *testing.T) { inst := New(WithOut(outW), WithTestBuiltin()) - eqRes, err := inst.Eval(ctx, tt.expr) + eqRes, err := inst.EvalString(ctx, tt.expr) if tt.wantErr { assert.Error(t, err) } else { @@ -1654,7 +1654,7 @@ func TestBuiltins_AddSubMupDivMod(t *testing.T) { inst := New(WithOut(outW), WithTestBuiltin()) - eqRes, err := inst.Eval(ctx, tt.expr) + eqRes, err := inst.EvalString(ctx, tt.expr) if tt.wantErr { assert.Error(t, err) } else { @@ -1705,7 +1705,7 @@ func TestBuiltins_AndOrNot(t *testing.T) { inst.SetVar("true", true) inst.SetVar("false", false) - eqRes, err := inst.Eval(ctx, tt.expr) + eqRes, err := inst.EvalString(ctx, tt.expr) if tt.wantErr { assert.Error(t, err) } else { @@ -1742,7 +1742,7 @@ func TestBuiltins_Cat(t *testing.T) { inst.SetVar("true", true) inst.SetVar("false", false) - eqRes, err := inst.Eval(ctx, tt.expr) + eqRes, err := inst.EvalString(ctx, tt.expr) assert.NoError(t, err) assert.Equal(t, tt.want, eqRes) }) @@ -1750,7 +1750,7 @@ func TestBuiltins_Cat(t *testing.T) { } func evalAndDisplay(ctx context.Context, inst *Inst, expr string) error { - res, err := inst.eval(ctx, expr) + res, err := inst.eval(ctx, strings.NewReader(expr), evalOptions{}) if err != nil { return err } diff --git a/ucl/userbuiltin_test.go b/ucl/userbuiltin_test.go index 8b1951f..58bf5ab 100644 --- a/ucl/userbuiltin_test.go +++ b/ucl/userbuiltin_test.go @@ -24,7 +24,7 @@ func TestInst_SetBuiltin(t *testing.T) { return x + y, nil }) - res, err := inst.Eval(context.Background(), `add2 "Hello, " "World"`) + res, err := inst.EvalString(context.Background(), `add2 "Hello, " "World"`) assert.NoError(t, err) assert.Equal(t, "Hello, World", res) }) @@ -44,7 +44,7 @@ func TestInst_SetBuiltin(t *testing.T) { return x + y, nil }) - res, err := inst.Eval(context.Background(), `add2 "Hello, " "World"`) + res, err := inst.EvalString(context.Background(), `add2 "Hello, " "World"`) assert.NoError(t, err) assert.Equal(t, "Hello, World", res) }) @@ -85,7 +85,7 @@ func TestInst_SetBuiltin(t *testing.T) { for _, tt := range tests { t.Run(tt.descr, func(t *testing.T) { - res, err := inst.Eval(context.Background(), tt.expr) + res, err := inst.EvalString(context.Background(), tt.expr) assert.NoError(t, err) assert.Equal(t, tt.want, res) }) @@ -108,7 +108,7 @@ func TestInst_SetBuiltin(t *testing.T) { return pair{x, y}, nil }) - res, err := inst.Eval(context.Background(), `add2 "Hello" "World"`) + res, err := inst.EvalString(context.Background(), `add2 "Hello" "World"`) assert.NoError(t, err) assert.Equal(t, pair{"Hello", "World"}, res) }) @@ -149,7 +149,7 @@ func TestInst_SetBuiltin(t *testing.T) { return x.x + ":" + x.y, nil }) - res, err := inst.Eval(context.Background(), tt.expr) + res, err := inst.EvalString(context.Background(), tt.expr) assert.NoError(t, err) assert.Equal(t, tt.want, res) }) @@ -176,7 +176,7 @@ func TestInst_SetBuiltin(t *testing.T) { return []string{"1", "2", "3"}, nil }) - res, err := inst.Eval(context.Background(), tt.expr) + res, err := inst.EvalString(context.Background(), tt.expr) assert.NoError(t, err) assert.Equal(t, tt.want, res) assert.Equal(t, tt.wantOut, outW.String()) @@ -206,11 +206,11 @@ func TestCallArgs_Bind(t *testing.T) { return ds.DoString(), nil }) - va, err := inst.Eval(ctx, `dostr (sa)`) + va, err := inst.EvalString(ctx, `dostr (sa)`) assert.NoError(t, err) assert.Equal(t, "do string A: a val", va) - vb, err := inst.Eval(ctx, `dostr (sb)`) + vb, err := inst.EvalString(ctx, `dostr (sb)`) assert.NoError(t, err) assert.Equal(t, "do string B: foo bar", vb) }) @@ -240,7 +240,7 @@ func TestCallArgs_Bind(t *testing.T) { return fmt.Sprintf("[%v]", v), nil }) - res, err := inst.Eval(ctx, tt.eval) + res, err := inst.EvalString(ctx, tt.eval) assert.NoError(t, err) assert.Equal(t, tt.want, res) }) @@ -295,7 +295,7 @@ func TestCallArgs_CanBind(t *testing.T) { return nil, nil }) - _, err := inst.Eval(ctx, tt.eval) + _, err := inst.EvalString(ctx, tt.eval) assert.NoError(t, err) assert.Equal(t, tt.want, res) }) @@ -332,7 +332,7 @@ func TestCallArgs_CanBind(t *testing.T) { return h.Value(k), nil }) - res, err := inst.Eval(ctx, tt.eval) + res, err := inst.EvalString(ctx, tt.eval) if tt.wantErr { assert.Error(t, err) assert.Nil(t, res) @@ -370,7 +370,7 @@ func TestCallArgs_CanBind(t *testing.T) { ctx := context.Background() - res, err := inst.Eval(ctx, `wrap { |x| toUpper $x }`) + res, err := inst.EvalString(ctx, `wrap { |x| toUpper $x }`) assert.NoError(t, err) assert.Equal(t, "[[HELLO]]", res) }) @@ -401,7 +401,7 @@ func TestCallArgs_CanBind(t *testing.T) { assert.NoError(t, err) assert.Nil(t, before) - res, err := inst.Eval(ctx, `wrap { |x| toUpper $x }`) + res, err := inst.EvalString(ctx, `wrap { |x| toUpper $x }`) assert.NoError(t, err) assert.Nil(t, res) @@ -437,7 +437,7 @@ func TestCallArgs_MissingCommandHandler(t *testing.T) { return fmt.Sprintf("was %v", name), nil })) - res, err := inst.Eval(ctx, tt.eval) + res, err := inst.EvalString(ctx, tt.eval) assert.NoError(t, err) assert.Equal(t, tt.want, res) }) @@ -460,27 +460,27 @@ func TestCallArgs_IsTopLevel(t *testing.T) { return nil, nil }) - _, err := inst.Eval(ctx, `lvl "one"`) + _, err := inst.EvalString(ctx, `lvl "one"`) assert.NoError(t, err) assert.True(t, res["one"]) - _, err = inst.Eval(ctx, `echo (lvl "two")`) + _, err = inst.EvalString(ctx, `echo (lvl "two")`) assert.NoError(t, err) assert.True(t, res["two"]) - _, err = inst.Eval(ctx, `proc doLvl { |n| lvl $n } ; doLvl "three"`) + _, err = inst.EvalString(ctx, `proc doLvl { |n| lvl $n } ; doLvl "three"`) assert.NoError(t, err) assert.False(t, res["three"]) - _, err = inst.Eval(ctx, `doLvl "four"`) + _, err = inst.EvalString(ctx, `doLvl "four"`) assert.NoError(t, err) assert.False(t, res["four"]) - _, err = inst.Eval(ctx, `["a"] | map { |x| doLvl "five" ; $x }`) + _, err = inst.EvalString(ctx, `["a"] | map { |x| doLvl "five" ; $x }`) assert.NoError(t, err) assert.False(t, res["five"]) - _, err = inst.Eval(ctx, `if 1 { lvl "six" }`) + _, err = inst.EvalString(ctx, `if 1 { lvl "six" }`) assert.NoError(t, err) assert.True(t, res["six"]) }) From ba6d42acbb7fd7af5d9adf250d4599600079067a Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sun, 25 May 2025 10:08:56 +1000 Subject: [PATCH 02/22] Fixed bug which was not isolating procs --- ucl/builtins.go | 2 +- ucl/env.go | 36 +++++++++++---- ucl/inst.go | 6 ++- ucl/inst_test.go | 114 +++++++++++++++++++++++------------------------ 4 files changed, 89 insertions(+), 69 deletions(-) diff --git a/ucl/builtins.go b/ucl/builtins.go index da7ddc4..71bad9b 100644 --- a/ucl/builtins.go +++ b/ucl/builtins.go @@ -1161,7 +1161,7 @@ func procBuiltin(ctx context.Context, args macroArgs) (Object, error) { obj := procObject{args.eval, args.ec, blockObj.block} if procName != "" { - args.ec.addCmd(procName, obj) + args.ec.addUserCmd(procName, obj) } return obj, nil } diff --git a/ucl/env.go b/ucl/env.go index 6d92ffb..7a58cc1 100644 --- a/ucl/env.go +++ b/ucl/env.go @@ -1,18 +1,17 @@ package ucl type evalCtx struct { - root *evalCtx - parent *evalCtx - commands map[string]invokable - macros map[string]macroable - vars map[string]Object - pseudoVars map[string]pseudoVar + root *evalCtx + parent *evalCtx + commands map[string]invokable + macros map[string]macroable + vars map[string]Object + pseudoVars map[string]pseudoVar + userCommandFrame bool // Frame to use for user-defined commands } func (ec *evalCtx) forkAndIsolate() *evalCtx { - newEc := &evalCtx{parent: ec} - newEc.root = newEc - return newEc + return &evalCtx{parent: ec, root: ec.root, userCommandFrame: true} } func (ec *evalCtx) fork() *evalCtx { @@ -27,6 +26,25 @@ func (ec *evalCtx) addCmd(name string, inv invokable) { ec.root.commands[name] = inv } +func (ec *evalCtx) addUserCmd(name string, inv invokable) { + frame := ec + for frame != nil { + if frame.userCommandFrame { + break + } + frame = frame.parent + } + if frame == nil { + panic("no user command frame found") + } + + if frame.commands == nil { + frame.commands = make(map[string]invokable) + } + + frame.commands[name] = inv +} + func (ec *evalCtx) addMacro(name string, inv macroable) { if ec.root.macros == nil { ec.root.macros = make(map[string]macroable) diff --git a/ucl/inst.go b/ucl/inst.go index 791b202..074433e 100644 --- a/ucl/inst.go +++ b/ucl/inst.go @@ -53,7 +53,9 @@ type Module struct { } func New(opts ...InstOption) *Inst { - rootEC := &evalCtx{} + rootEC := &evalCtx{ + userCommandFrame: true, + } rootEC.root = rootEC rootEC.addCmd("echo", invokableFunc(echoBuiltin)) @@ -194,7 +196,7 @@ func (inst *Inst) eval(ctx context.Context, r io.Reader, opts evalOptions) (Obje env := inst.rootEC if opts.forkEnv { - env = env.fork() + env = env.forkAndIsolate() } return eval.evalScript(ctx, env, ast) diff --git a/ucl/inst_test.go b/ucl/inst_test.go index 184a3b0..06c60a0 100644 --- a/ucl/inst_test.go +++ b/ucl/inst_test.go @@ -145,72 +145,72 @@ func TestInst_Eval_WithSubEnv(t *testing.T) { assert.Nil(t, res) }) - t.Run("when using hooks, reading vars are not permitted to leak across environment", func(t *testing.T) { - ctx := t.Context() + t.Run("environments should not leak when using hooks", func(t *testing.T) { + tests := []struct { + descr string + eval1 string + eval2 string + want1 any + want2 any + }{ + { + descr: "reading vars", + eval1: `$a = "hello" ; hook { $a }`, + eval2: `$a = "world" ; hook { $a }`, + want1: "hello", + want2: "world", + }, + { + descr: "modifying vars", + eval1: `$a = "hello" ; hook { $a = "new value" ; $a }`, + eval2: `$a = "world" ; hook { $a }`, + want1: "new value", + want2: "world", + }, + { + descr: "defining procs", + eval1: `proc say_hello { "hello" } ; hook { say_hello }`, + eval2: `proc say_hello { "world" } ; hook { say_hello }`, + want1: "hello", + want2: "world", + }, + } - hooks := make([]ucl.Invokable, 0) + for _, tt := range tests { + t.Run(tt.descr, func(t *testing.T) { + ctx := t.Context() - inst := ucl.New() - inst.SetBuiltin("hook", func(ctx context.Context, args ucl.CallArgs) (any, error) { - var hookProc ucl.Invokable + hooks := make([]ucl.Invokable, 0) - if err := args.Bind(&hookProc); err != nil { - return nil, err - } - hooks = append(hooks, hookProc) + inst := ucl.New() + inst.SetBuiltin("hook", func(ctx context.Context, args ucl.CallArgs) (any, error) { + var hookProc ucl.Invokable - return nil, nil - }) + if err := args.Bind(&hookProc); err != nil { + return nil, err + } + hooks = append(hooks, hookProc) - res, err := inst.Eval(ctx, strings.NewReader(`$a = "hello" ; hook { $a }`), ucl.WithSubEnv()) - assert.NoError(t, err) - assert.Nil(t, res) + return nil, nil + }) - res, err = inst.Eval(ctx, strings.NewReader(`$a = "world" ; hook { $a }`), ucl.WithSubEnv()) - assert.NoError(t, err) - assert.Nil(t, res) + res, err := inst.Eval(ctx, strings.NewReader(tt.eval1), ucl.WithSubEnv()) + assert.NoError(t, err) + assert.Nil(t, res) - h1, err := hooks[0].Invoke(ctx, ucl.CallArgs{}) - assert.NoError(t, err) - assert.Equal(t, "hello", h1) + res, err = inst.Eval(ctx, strings.NewReader(tt.eval2), ucl.WithSubEnv()) + assert.NoError(t, err) + assert.Nil(t, res) - h2, err := hooks[1].Invoke(ctx, ucl.CallArgs{}) - assert.NoError(t, err) - assert.Equal(t, "world", h2) - }) + h1, err := hooks[0].Invoke(ctx, ucl.CallArgs{}) + assert.NoError(t, err) + assert.Equal(t, tt.want1, h1) - t.Run("when using hooks, writing vars are not permitted to leak across environment", func(t *testing.T) { - ctx := t.Context() - - hooks := make([]ucl.Invokable, 0) - - inst := ucl.New() - inst.SetBuiltin("hook", func(ctx context.Context, args ucl.CallArgs) (any, error) { - var hookProc ucl.Invokable - - if err := args.Bind(&hookProc); err != nil { - return nil, err - } - hooks = append(hooks, hookProc) - - return nil, nil - }) - - res, err := inst.Eval(ctx, strings.NewReader(`$a = "hello" ; hook { $a = "new value" ; $a }`), ucl.WithSubEnv()) - assert.NoError(t, err) - assert.Nil(t, res) - - res, err = inst.Eval(ctx, strings.NewReader(`$a = "world" ; hook { $a }`), ucl.WithSubEnv()) - assert.NoError(t, err) - assert.Nil(t, res) - - h1, err := hooks[0].Invoke(ctx, ucl.CallArgs{}) - assert.NoError(t, err) - assert.Equal(t, "new value", h1) - - h2, err := hooks[1].Invoke(ctx, ucl.CallArgs{}) - assert.NoError(t, err) - assert.Equal(t, "world", h2) + h2, err := hooks[1].Invoke(ctx, ucl.CallArgs{}) + assert.NoError(t, err) + assert.Equal(t, tt.want2, h2) + }) + } }) } From f1cdf6dd938fb956469c548c8286b9b71e06c63b Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sun, 25 May 2025 12:36:27 +1000 Subject: [PATCH 03/22] Added facility for defining top-level commands from invokables --- ucl/inst_test.go | 27 +++++++++++++++++++++++++++ ucl/userbuiltin.go | 4 ++++ 2 files changed, 31 insertions(+) diff --git a/ucl/inst_test.go b/ucl/inst_test.go index 06c60a0..7a9149a 100644 --- a/ucl/inst_test.go +++ b/ucl/inst_test.go @@ -174,6 +174,20 @@ func TestInst_Eval_WithSubEnv(t *testing.T) { want1: "hello", want2: "world", }, + { + descr: "exporting procs 1", + eval1: `export say_hello { "hello" } ; hook { say_hello }`, + eval2: `hook { say_hello }`, + want1: "hello", + want2: "hello", + }, + { + descr: "exporting procs 2", + eval1: `$a = "hello" ; export say_hello { $a = "world"; $a } ; hook { say_hello }`, + eval2: `$a = "other" ; hook { say_hello }`, + want1: "world", + want2: "world", + }, } for _, tt := range tests { @@ -193,6 +207,19 @@ func TestInst_Eval_WithSubEnv(t *testing.T) { return nil, nil }) + inst.SetBuiltin("export", func(ctx context.Context, args ucl.CallArgs) (any, error) { + var ( + name string + hookProc ucl.Invokable + ) + + if err := args.Bind(&name, &hookProc); err != nil { + return nil, err + } + inst.SetBuiltinInvokable(name, hookProc) + + return nil, nil + }) res, err := inst.Eval(ctx, strings.NewReader(tt.eval1), ucl.WithSubEnv()) assert.NoError(t, err) diff --git a/ucl/userbuiltin.go b/ucl/userbuiltin.go index 7058b0e..83d72d6 100644 --- a/ucl/userbuiltin.go +++ b/ucl/userbuiltin.go @@ -81,6 +81,10 @@ func (inst *Inst) SetBuiltin(name string, fn BuiltinHandler) { inst.rootEC.addCmd(name, userBuiltin{fn: fn}) } +func (inst *Inst) SetBuiltinInvokable(name string, fn Invokable) { + inst.rootEC.addCmd(name, fn.inv) +} + type userBuiltin struct { fn func(ctx context.Context, args CallArgs) (any, error) } From e869e6c9bd4d9830d729c0c80203d77190b04493 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Tue, 27 May 2025 21:09:48 +1000 Subject: [PATCH 04/22] Added HashObject --- ucl/objs.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/ucl/objs.go b/ucl/objs.go index f5894f1..0ec03b1 100644 --- a/ucl/objs.go +++ b/ucl/objs.go @@ -98,6 +98,33 @@ func (ss StringListObject) Index(i int) Object { return StringObject(ss[i]) } +type HashObject map[string]Object + +func (ho HashObject) String() string { + return fmt.Sprintf("%v", ho) +} + +func (ho HashObject) Truthy() bool { + return len(ho) > 0 +} + +func (ho HashObject) Len() int { + return len(ho) +} + +func (ho HashObject) Value(k string) Object { + return ho[k] +} + +func (ho HashObject) Each(fn func(k string, v Object) error) error { + for k, v := range ho { + if err := fn(k, v); err != nil { + return err + } + } + return nil +} + type iteratorObject struct { Iterable } From 03e6878524a11ec77b656b20406b646e91b35bf5 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Tue, 27 May 2025 21:21:10 +1000 Subject: [PATCH 05/22] Fixed nil panic --- ucl/eval.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ucl/eval.go b/ucl/eval.go index 89172f8..ce10b11 100644 --- a/ucl/eval.go +++ b/ucl/eval.go @@ -241,7 +241,7 @@ func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (Objec return mph.get(ctx, *n.PseudoVar) } - return nil, errors.New("unknown pseudo-variable: " + *n.Var) + return nil, errors.New("unknown pseudo-variable: " + *n.PseudoVar) case n.MaybeSub != nil: sub := n.MaybeSub.Sub if sub == nil { From 41b4fdb0038280dfe631d78e84d80dea035cd46c Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Tue, 27 May 2025 21:42:13 +1000 Subject: [PATCH 06/22] Switch to exposing the existing HashObject --- ucl/builtins.go | 2 +- ucl/eval.go | 4 ++-- ucl/objs.go | 41 +++++++---------------------------------- 3 files changed, 10 insertions(+), 37 deletions(-) diff --git a/ucl/builtins.go b/ucl/builtins.go index 71bad9b..ddb065a 100644 --- a/ucl/builtins.go +++ b/ucl/builtins.go @@ -690,7 +690,7 @@ func filterBuiltin(ctx context.Context, args invocationArgs) (Object, error) { } return &newList, nil case Hashable: - newHash := hashObject{} + 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 diff --git a/ucl/eval.go b/ucl/eval.go index ce10b11..b2edca2 100644 --- a/ucl/eval.go +++ b/ucl/eval.go @@ -294,11 +294,11 @@ func (e evaluator) evalListOrHash(ctx context.Context, ec *evalCtx, loh *astList if loh.EmptyList { return &ListObject{}, nil } else if loh.EmptyHash { - return hashObject{}, nil + return HashObject{}, nil } if firstIsHash := loh.Elements[0].Right != nil; firstIsHash { - h := hashObject{} + h := HashObject{} for _, el := range loh.Elements { if el.Right == nil { return nil, errors.New("miss-match of lists and hash") diff --git a/ucl/objs.go b/ucl/objs.go index 0ec03b1..816bbaf 100644 --- a/ucl/objs.go +++ b/ucl/objs.go @@ -98,33 +98,6 @@ func (ss StringListObject) Index(i int) Object { return StringObject(ss[i]) } -type HashObject map[string]Object - -func (ho HashObject) String() string { - return fmt.Sprintf("%v", ho) -} - -func (ho HashObject) Truthy() bool { - return len(ho) > 0 -} - -func (ho HashObject) Len() int { - return len(ho) -} - -func (ho HashObject) Value(k string) Object { - return ho[k] -} - -func (ho HashObject) Each(fn func(k string, v Object) error) error { - for k, v := range ho { - if err := fn(k, v); err != nil { - return err - } - } - return nil -} - type iteratorObject struct { Iterable } @@ -137,9 +110,9 @@ func (i iteratorObject) Truthy() bool { return i.Iterable.HasNext() } -type hashObject map[string]Object +type HashObject map[string]Object -func (s hashObject) String() string { +func (s HashObject) String() string { if len(s) == 0 { return "[:]" } @@ -158,19 +131,19 @@ func (s hashObject) String() string { return sb.String() } -func (s hashObject) Truthy() bool { +func (s HashObject) Truthy() bool { return len(s) > 0 } -func (s hashObject) Len() int { +func (s HashObject) Len() int { return len(s) } -func (s hashObject) Value(k string) Object { +func (s HashObject) Value(k string) Object { return s[k] } -func (s hashObject) Each(fn func(k string, v Object) error) error { +func (s HashObject) Each(fn func(k string, v Object) error) error { for k, v := range s { if err := fn(k, v); err != nil { return err @@ -246,7 +219,7 @@ func toGoValue(obj Object) (interface{}, bool) { xs = append(xs, x) } return xs, true - case hashObject: + case HashObject: xs := make(map[string]interface{}) for k, va := range v { x, ok := toGoValue(va) From e71699d5a78add731e45d73a13ff3f4487885417 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Wed, 28 May 2025 21:39:03 +1000 Subject: [PATCH 07/22] Added list:uniq and the !nil builtin --- ucl/ast.go | 2 +- ucl/builtins.go | 26 +++++++++++---- ucl/builtins/lists.go | 67 ++++++++++++++++++++++++++++++++++++++ ucl/builtins/lists_test.go | 31 ++++++++++++++++++ ucl/builtins/strs.go | 4 +-- ucl/builtins/strs_test.go | 1 + ucl/inst.go | 2 ++ ucl/inst_test.go | 2 +- ucl/objs.go | 7 ++++ ucl/testbuiltins_test.go | 38 +++++++++++++++++++++ 10 files changed, 169 insertions(+), 11 deletions(-) diff --git a/ucl/ast.go b/ucl/ast.go index 3b567d9..54aaa64 100644 --- a/ucl/ast.go +++ b/ucl/ast.go @@ -148,7 +148,7 @@ var scanner = lexer.MustStateful(lexer.Rules{ {"NL", `[;\n][; \n\t]*`, nil}, {"PIPE", `\|`, nil}, {"EQ", `=`, nil}, - {"Ident", `[-]*[a-zA-Z_][\w-!?]*`, nil}, + {"Ident", `[-!?]*[a-zA-Z_!?-][\w-!?]*`, nil}, }, "String": { {"Escaped", `\\.`, nil}, diff --git a/ucl/builtins.go b/ucl/builtins.go index ddb065a..cc946b3 100644 --- a/ucl/builtins.go +++ b/ucl/builtins.go @@ -188,7 +188,7 @@ func eqBuiltin(ctx context.Context, args invocationArgs) (Object, error) { l := args.args[0] r := args.args[1] - return BoolObject(objectsEqual(l, r)), nil + return BoolObject(ObjectsEqual(l, r)), nil } func neBuiltin(ctx context.Context, args invocationArgs) (Object, error) { @@ -199,7 +199,7 @@ func neBuiltin(ctx context.Context, args invocationArgs) (Object, error) { l := args.args[0] r := args.args[1] - return BoolObject(!objectsEqual(l, r)), nil + return BoolObject(!ObjectsEqual(l, r)), nil } func ltBuiltin(ctx context.Context, args invocationArgs) (Object, error) { @@ -223,7 +223,7 @@ func leBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if err != nil { return nil, err } - return BoolObject(isLess || objectsEqual(args.args[0], args.args[1])), nil + return BoolObject(isLess || ObjectsEqual(args.args[0], args.args[1])), nil } func gtBuiltin(ctx context.Context, args invocationArgs) (Object, error) { @@ -247,7 +247,7 @@ func geBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if err != nil { return nil, err } - return BoolObject(isGreater || objectsEqual(args.args[0], args.args[1])), nil + return BoolObject(isGreater || ObjectsEqual(args.args[0], args.args[1])), nil } func andBuiltin(ctx context.Context, args invocationArgs) (Object, error) { @@ -286,7 +286,7 @@ func notBuiltin(ctx context.Context, args invocationArgs) (Object, error) { var errObjectsNotEqual = errors.New("objects not equal") -func objectsEqual(l, r Object) bool { +func ObjectsEqual(l, r Object) bool { if l == nil || r == nil { return l == nil && r == nil } @@ -314,7 +314,7 @@ func objectsEqual(l, r Object) bool { return false } for i := 0; i < lv.Len(); i++ { - if !objectsEqual(lv.Index(i), rv.Index(i)) { + if !ObjectsEqual(lv.Index(i), rv.Index(i)) { return false } } @@ -332,7 +332,7 @@ func objectsEqual(l, r Object) bool { rkv := rv.Value(k) if rkv == nil { return errObjectsNotEqual - } else if !objectsEqual(lkv, rkv) { + } else if !ObjectsEqual(lkv, rkv) { return errObjectsNotEqual } return nil @@ -370,6 +370,18 @@ func strBuiltin(ctx context.Context, args invocationArgs) (Object, error) { return StringObject(args.args[0].String()), nil } +func notNilBuiltin(ctx context.Context, args invocationArgs) (Object, error) { + if err := args.expectArgn(1); err != nil { + return nil, err + } + + if args.args[0] == nil { + return BoolObject(false), nil + } + + return BoolObject(true), nil +} + func intBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if err := args.expectArgn(1); err != nil { return nil, err diff --git a/ucl/builtins/lists.go b/ucl/builtins/lists.go index f81f3e2..4e70095 100644 --- a/ucl/builtins/lists.go +++ b/ucl/builtins/lists.go @@ -11,6 +11,7 @@ func Lists() ucl.Module { Name: "lists", Builtins: map[string]ucl.BuiltinHandler{ "first": listFirst, + "uniq": listUniq, }, } } @@ -58,3 +59,69 @@ func listFirst(ctx context.Context, args ucl.CallArgs) (any, error) { return newList, nil } + +func eachListOrIterItem(ctx context.Context, o ucl.Object, f func(int, ucl.Object) error) error { + switch t := o.(type) { + case ucl.Listable: + for i := 0; i < t.Len(); i++ { + if err := f(i, t.Index(i)); err != nil { + return err + } + } + return nil + case ucl.Iterable: + idx := 0 + for t.HasNext() { + v, err := t.Next(ctx) + if err != nil { + return err + } + if err := f(idx, v); err != nil { + return err + } + idx++ + } + } + return errors.New("expected listable") +} + +type uniqKey struct { + sVal string + iVal int +} + +func listUniq(ctx context.Context, args ucl.CallArgs) (any, error) { + var ( + what ucl.Object + ) + + if err := args.Bind(&what); err != nil { + return nil, err + } + + seen := make(map[uniqKey]bool) + found := ucl.NewListObject() + + if err := eachListOrIterItem(ctx, what, func(idx int, v ucl.Object) error { + var key uniqKey + switch v := v.(type) { + case ucl.StringObject: + key = uniqKey{sVal: string(v)} + case ucl.IntObject: + key = uniqKey{iVal: int(v)} + default: + return errors.New("expected string or int") + } + + if !seen[key] { + seen[key] = true + found.Append(v) + } + + return nil + }); err != nil { + return nil, err + } + + return found, nil +} diff --git a/ucl/builtins/lists_test.go b/ucl/builtins/lists_test.go index 526176e..72ccf65 100644 --- a/ucl/builtins/lists_test.go +++ b/ucl/builtins/lists_test.go @@ -45,6 +45,37 @@ func TestLists_First(t *testing.T) { } } +func TestLists_Uniq(t *testing.T) { + tests := []struct { + desc string + eval string + want any + wantErr bool + }{ + {desc: "uniq 1", eval: `lists:uniq [a a a a b b b c c c]`, want: []any{"a", "b", "c"}}, + {desc: "uniq 2", eval: `lists:uniq [1 2 1 3 2 4 2 5 3]`, want: []any{1, 2, 3, 4, 5}}, + {desc: "uniq 3", eval: `lists:uniq [1 a 2 b 3 b 2 a 1]5`, want: []any{1, "a", 2, "b", 3}}, + + {desc: "uniq err 1", eval: `lists:uniq [[1 2 3] [a:2] ()]`, 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 uclListOf(args ...any) *ucl.ListObject { newList := ucl.NewListObject() for _, arg := range args { diff --git a/ucl/builtins/strs.go b/ucl/builtins/strs.go index 03801d0..281dd4c 100644 --- a/ucl/builtins/strs.go +++ b/ucl/builtins/strs.go @@ -103,7 +103,7 @@ func join(ctx context.Context, args ucl.CallArgs) (any, error) { if i > 0 { sb.WriteString(tok) } - sb.WriteString(t.Index(i).String()) + sb.WriteString(ucl.ObjectToString(t.Index(i))) } return sb.String(), nil case ucl.Iterable: @@ -120,7 +120,7 @@ func join(ctx context.Context, args ucl.CallArgs) (any, error) { } else { first = false } - sb.WriteString(v.String()) + sb.WriteString(ucl.ObjectToString(v)) } return sb.String(), nil diff --git a/ucl/builtins/strs_test.go b/ucl/builtins/strs_test.go index c38065b..6dc5436 100644 --- a/ucl/builtins/strs_test.go +++ b/ucl/builtins/strs_test.go @@ -183,6 +183,7 @@ func TestStrs_Join(t *testing.T) { {desc: "join 3", eval: `strs:join [a b c] ""`, want: "abc"}, {desc: "join 4", eval: `strs:join [a b c]`, want: "abc"}, {desc: "join 5", eval: `strs:join (itrs:from [a b c]) ","`, want: "a,b,c"}, + {desc: "join 6", eval: `strs:join [a () c () e]`, want: "ace"}, } for _, tt := range tests { diff --git a/ucl/inst.go b/ucl/inst.go index 074433e..de58436 100644 --- a/ucl/inst.go +++ b/ucl/inst.go @@ -81,6 +81,8 @@ func New(opts ...InstOption) *Inst { rootEC.addCmd("str", invokableFunc(strBuiltin)) rootEC.addCmd("int", invokableFunc(intBuiltin)) + rootEC.addCmd("!nil", invokableFunc(notNilBuiltin)) + rootEC.addCmd("add", invokableFunc(addBuiltin)) rootEC.addCmd("sub", invokableFunc(subBuiltin)) rootEC.addCmd("mup", invokableFunc(mupBuiltin)) diff --git a/ucl/inst_test.go b/ucl/inst_test.go index 7a9149a..e2dbf60 100644 --- a/ucl/inst_test.go +++ b/ucl/inst_test.go @@ -21,7 +21,7 @@ func TestInst_Eval(t *testing.T) { {desc: "simple string", expr: `firstarg "hello"`, want: "hello"}, {desc: "simple int 1", expr: `firstarg 123`, want: 123}, {desc: "simple int 2", expr: `firstarg -234`, want: -234}, - {desc: "simple ident", expr: `firstarg a-test`, want: "a-test"}, + {desc: "simple ident 1", expr: `firstarg a-test`, want: "a-test"}, // String interpolation {desc: "interpolate string 1", expr: `$what = "world" ; firstarg "hello $what"`, want: "hello world"}, diff --git a/ucl/objs.go b/ucl/objs.go index 816bbaf..981f7ab 100644 --- a/ucl/objs.go +++ b/ucl/objs.go @@ -680,3 +680,10 @@ func isBreakErr(err error) bool { return errors.As(err, &errBreak{}) || errors.As(err, &errReturn{}) || errors.Is(err, ErrHalt) } + +func ObjectToString(obj Object) string { + if obj == nil { + return "" + } + return obj.String() +} diff --git a/ucl/testbuiltins_test.go b/ucl/testbuiltins_test.go index 9ac1d61..9361ae4 100644 --- a/ucl/testbuiltins_test.go +++ b/ucl/testbuiltins_test.go @@ -1749,6 +1749,44 @@ func TestBuiltins_Cat(t *testing.T) { } } +func TestBuiltins_NotNil(t *testing.T) { + tests := []struct { + desc string + expr string + want any + }{ + {desc: "not nil 1", expr: `!nil "hello"`, want: true}, + {desc: "not nil 2", expr: `!nil ""`, want: true}, + {desc: "not nil 3", expr: `!nil 4`, want: true}, + {desc: "not nil 4", expr: `!nil 0`, want: true}, + {desc: "not nil 5", expr: `!nil $true`, want: true}, + {desc: "not nil 6", expr: `!nil $false`, want: true}, + {desc: "not nil 7", expr: `!nil [1 2 3]`, want: true}, + {desc: "not nil 8", expr: `!nil []`, want: true}, + {desc: "not nil 9", expr: `!nil [a:1 b:21]`, want: true}, + {desc: "not nil 10", expr: `!nil [:]`, want: true}, + + {desc: "not nil 11", expr: `!nil ()`, want: false}, + + {desc: "not nil 12", expr: `[1 () 2 () 3] | filter !nil | reduce "" { |x a| "$a $x" }`, want: " 1 2 3"}, + } + + 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.SetVar("true", true) + inst.SetVar("false", false) + + eqRes, err := inst.EvalString(ctx, tt.expr) + assert.NoError(t, err) + assert.Equal(t, tt.want, eqRes) + }) + } +} + func evalAndDisplay(ctx context.Context, inst *Inst, expr string) error { res, err := inst.eval(ctx, strings.NewReader(expr), evalOptions{}) if err != nil { From 3a88c0c777d8af1aa60e4c6c003e3d60ba75ccfe Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Wed, 28 May 2025 21:39:31 +1000 Subject: [PATCH 08/22] Renambed builtins_test --- ucl/{testbuiltins_test.go => builtins_test.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename ucl/{testbuiltins_test.go => builtins_test.go} (100%) diff --git a/ucl/testbuiltins_test.go b/ucl/builtins_test.go similarity index 100% rename from ucl/testbuiltins_test.go rename to ucl/builtins_test.go From eda791d7144cc2a993ce326d58f5891222293508 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Thu, 12 Jun 2025 13:28:44 +0200 Subject: [PATCH 09/22] Made indexing a little more strict Also added support for var setting with or without the dollar sign. --- ucl/ast.go | 1 + ucl/builtins.go | 13 +++++++++--- ucl/builtins_test.go | 49 +++++++++++++++++++++++++++++--------------- ucl/errors.go | 2 ++ ucl/eval.go | 12 +++++++---- ucl/inst_test.go | 26 ++++++++++++++++------- 6 files changed, 73 insertions(+), 30 deletions(-) diff --git a/ucl/ast.go b/ucl/ast.go index 54aaa64..719f381 100644 --- a/ucl/ast.go +++ b/ucl/ast.go @@ -98,6 +98,7 @@ type astDotSuffix struct { } type astDot struct { + Pos lexer.Position Arg astCmdArg `parser:"@@"` DotSuffix []astDotSuffix `parser:"( DOT @@ )*"` } diff --git a/ucl/builtins.go b/ucl/builtins.go index cc946b3..0c84824 100644 --- a/ucl/builtins.go +++ b/ucl/builtins.go @@ -6,6 +6,8 @@ import ( "fmt" "strconv" "strings" + + "github.com/alecthomas/participle/v2/lexer" ) func echoBuiltin(ctx context.Context, args invocationArgs) (Object, error) { @@ -509,7 +511,10 @@ func lenBuiltin(ctx context.Context, args invocationArgs) (Object, error) { return IntObject(0), nil } -func indexLookup(ctx context.Context, obj, elem Object) (Object, error) { +func indexLookup(ctx context.Context, obj, elem Object, pos lexer.Position) (Object, error) { + if obj == nil { + return nil, nil + } switch v := obj.(type) { case Listable: intIdx, ok := elem.(IntObject) @@ -525,9 +530,11 @@ func indexLookup(ctx context.Context, obj, elem Object) (Object, error) { case Hashable: strIdx, ok := elem.(StringObject) if !ok { - return nil, errors.New("expected string for Hashable") + return nil, nil } return v.Value(string(strIdx)), nil + default: + return nil, notIndexableError(pos) } return nil, nil } @@ -539,7 +546,7 @@ func indexBuiltin(ctx context.Context, args invocationArgs) (Object, error) { val := args.args[0] for _, idx := range args.args[1:] { - newVal, err := indexLookup(ctx, val, idx) + newVal, err := indexLookup(ctx, val, idx, lexer.Position{}) if err != nil { return nil, err } diff --git a/ucl/builtins_test.go b/ucl/builtins_test.go index 9361ae4..effb183 100644 --- a/ucl/builtins_test.go +++ b/ucl/builtins_test.go @@ -5,9 +5,10 @@ import ( "context" "errors" "fmt" - "github.com/stretchr/testify/assert" "strings" "testing" + + "github.com/stretchr/testify/assert" ) type testIterator struct { @@ -57,6 +58,19 @@ func WithTestBuiltin() InstOption { return &a, nil })) + i.rootEC.addCmd("rearrange", invokableFunc(func(ctx context.Context, args invocationArgs) (Object, error) { + var as ListObject = make([]Object, 0) + + for _, a := range args.args { + vs, ok := args.kwargs[a.String()] + if ok { + as = append(as, vs.Index(0)) + } + } + + return &as, nil + })) + i.rootEC.addCmd("error", invokableFunc(func(ctx context.Context, args invocationArgs) (Object, error) { if len(args.args) == 0 { return nil, errors.New("an error occurred") @@ -131,10 +145,10 @@ func TestBuiltins_Echo(t *testing.T) { echo "world" # command after this ; `, want: "Hello\nworld\n"}, - {desc: "interpolated string 1", expr: `$what = "world" ; echo "Hello, $what"`, want: "Hello, world\n"}, - {desc: "interpolated string 2", expr: `$what = "world" ; echo "Hello, \$what"`, want: "Hello, $what\n"}, + {desc: "interpolated string 1", expr: `what = "world" ; echo "Hello, $what"`, want: "Hello, world\n"}, + {desc: "interpolated string 2", expr: `what = "world" ; echo "Hello, \$what"`, want: "Hello, $what\n"}, {desc: "interpolated string 3", expr: `echo "separate\nlines\n\tand tabs"`, want: "separate\nlines\n\tand tabs\n"}, - {desc: "interpolated string 4", expr: `$what = "Hello" ; $where = "world" ; echo "$what, $where"`, want: "Hello, world\n"}, + {desc: "interpolated string 4", expr: `what = "Hello" ; where = "world" ; echo "$what, $where"`, want: "Hello, world\n"}, {desc: "interpolated string 5", expr: ` for [123 "foo" true ()] { |x| echo "[[$x]]" @@ -167,19 +181,19 @@ func TestBuiltins_If(t *testing.T) { want string }{ {desc: "single then", expr: ` - $x = "Hello" + x = "Hello" if $x { echo "true" }`, want: "true\n(nil)\n"}, {desc: "single then and else", expr: ` - $x = "Hello" + x = "Hello" if $x { echo "true" } else { echo "false" }`, want: "true\n(nil)\n"}, {desc: "single then, elif and else", expr: ` - $x = "Hello" + x = "Hello" if $y { echo "y is true" } elif $x { @@ -188,14 +202,14 @@ func TestBuiltins_If(t *testing.T) { echo "nothings x" }`, want: "x is true\n(nil)\n"}, {desc: "single then and elif, no else", expr: ` - $x = "Hello" + x = "Hello" if $y { echo "y is true" } elif $x { echo "x is true" }`, want: "x is true\n(nil)\n"}, {desc: "single then, two elif, and else", expr: ` - $x = "Hello" + x = "Hello" if $z { echo "z is true" } elif $y { @@ -213,15 +227,15 @@ func TestBuiltins_If(t *testing.T) { } else { echo "none is true" }`, want: "none is true\n(nil)\n"}, - {desc: "compressed then", expr: `$x = "Hello" ; if $x { echo "true" }`, want: "true\n(nil)\n"}, + {desc: "compressed then", expr: `x = "Hello" ; if $x { echo "true" }`, want: "true\n(nil)\n"}, {desc: "compressed else", expr: `if $x { echo "true" } else { echo "false" }`, want: "false\n(nil)\n"}, {desc: "compressed if", expr: `if $x { echo "x" } elif $y { echo "y" } else { echo "false" }`, want: "false\n(nil)\n"}, - {desc: "if of itr 1", expr: `$i = itr ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"}, - {desc: "if of itr 2", expr: `$i = itr ; for (seq 1) { head $i } ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"}, - {desc: "if of itr 3", expr: `$i = itr ; for (seq 3) { head $i } ; if $i { echo "more" } else { echo "none" }`, want: "none\n(nil)\n"}, - {desc: "if of itr 4", expr: `$i = (itr | map { |x| add 2 $x }) ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"}, - {desc: "if of itr 5", expr: `$i = (itr | filter { |x| () }) ; if $i { echo "more" } else { echo "none" }`, want: "none\n(nil)\n"}, - {desc: "if of itr 6", expr: `$i = (itr | filter { |x| 1 }) ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"}, + {desc: "if of itr 1", expr: `i = itr ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"}, + {desc: "if of itr 2", expr: `i = itr ; for (seq 1) { head $i } ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"}, + {desc: "if of itr 3", expr: `i = itr ; for (seq 3) { head $i } ; if $i { echo "more" } else { echo "none" }`, want: "none\n(nil)\n"}, + {desc: "if of itr 4", expr: `i = (itr | map { |x| add 2 $x }) ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"}, + {desc: "if of itr 5", expr: `i = (itr | filter { |x| () }) ; if $i { echo "more" } else { echo "none" }`, want: "none\n(nil)\n"}, + {desc: "if of itr 6", expr: `i = (itr | filter { |x| 1 }) ; if $i { echo "more" } else { echo "none" }`, want: "more\n(nil)\n"}, } for _, tt := range tests { @@ -508,6 +522,9 @@ func TestBuiltins_Procs(t *testing.T) { echo (call $er ["xxx"]) echo (call $er ["yyy"]) `, want: "Xxxx\nXxxxyyy\n(nil)\n"}, + {desc: "calling with kwargs", expr: ` + echo (call rearrange [b a] [a:"ey" b:"bee"]) + `, want: "[bee ey]\n(nil)\n"}, } for _, tt := range tests { diff --git a/ucl/errors.go b/ucl/errors.go index a948b03..f7f80e9 100644 --- a/ucl/errors.go +++ b/ucl/errors.go @@ -3,6 +3,7 @@ package ucl import ( "errors" "fmt" + "github.com/alecthomas/participle/v2/lexer" ) @@ -12,6 +13,7 @@ var ( var ( tooManyFinallyBlocksError = newBadUsage("try needs at most 1 finally") + notIndexableError = newBadUsage("index only support on lists and hashes") ) type errorWithPos struct { diff --git a/ucl/eval.go b/ucl/eval.go index b2edca2..82313df 100644 --- a/ucl/eval.go +++ b/ucl/eval.go @@ -5,6 +5,8 @@ import ( "errors" "fmt" "strings" + + "github.com/alecthomas/participle/v2/lexer" ) type evaluator struct { @@ -205,7 +207,7 @@ func (e evaluator) evalDot(ctx context.Context, ec *evalCtx, n astDot) (Object, } } - res, err = indexLookup(ctx, res, idx) + res, err = indexLookup(ctx, res, idx, n.Pos) if err != nil { return nil, err } @@ -258,9 +260,11 @@ func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (Objec func (e evaluator) assignArg(ctx context.Context, ec *evalCtx, n astCmdArg, toVal Object) (Object, error) { switch { + case n.Ident != nil: + ec.setOrDefineVar(n.Ident.String(), toVal) + return toVal, nil case n.Literal != nil: - // We may use this for variable setting? - return nil, errors.New("cannot assign to a literal") + return nil, errors.New("cannot assign to a literal value") case n.Var != nil: ec.setOrDefineVar(*n.Var, toVal) return toVal, nil @@ -428,7 +432,7 @@ func (e evaluator) interpolateLongIdent(ctx context.Context, ec *evalCtx, n *ast } } - res, err = indexLookup(ctx, res, idx) + res, err = indexLookup(ctx, res, idx, lexer.Position{}) if err != nil { return "", err } diff --git a/ucl/inst_test.go b/ucl/inst_test.go index e2dbf60..718d0b7 100644 --- a/ucl/inst_test.go +++ b/ucl/inst_test.go @@ -4,19 +4,22 @@ import ( "bytes" "context" "strings" + "ucl.lmika.dev/ucl" - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestInst_Eval(t *testing.T) { tests := []struct { - desc string - expr string - want any - wantObj bool - wantErr error + desc string + expr string + want any + wantObj bool + wantAnErr bool + wantErr error }{ {desc: "simple string", expr: `firstarg "hello"`, want: "hello"}, {desc: "simple int 1", expr: `firstarg 123`, want: 123}, @@ -100,7 +103,14 @@ func TestInst_Eval(t *testing.T) { {desc: "dot idents 6", expr: `$x = [alpha:"hello" bravo:"world"] ; $x.("charlie")`, want: nil}, {desc: "dot idents 7", expr: `$x = [MORE:"stuff"] ; $x.("more" | toUpper)`, want: "stuff"}, {desc: "dot idents 8", expr: `$x = [MORE:"stuff"] ; $x.(toUpper ("more"))`, want: "stuff"}, - {desc: "dot idents 9", expr: `$x = [MORE:"stuff"] ; x.y`, want: nil}, + {desc: "dot idents 9", expr: `$x = [MORE:"stuff"] ; $x.y`, want: nil}, + + {desc: "dot err 1", expr: `$x = [1 2 3] ; $x.Hello`, want: nil}, + {desc: "dot err 2", expr: `$x = [1 2 3] ; $x.("world")`, want: nil}, + {desc: "dot err 4", expr: `$x = [a:1 b:2] ; $x.(5)`, want: nil}, + {desc: "dot err 3", expr: `$x = [a:1 b:2] ; $x.(0)`, want: nil}, + {desc: "dot err 5", expr: `$x = 123 ; $x.(5)`, wantAnErr: true}, + {desc: "dot err 6", expr: `$x = 123 ; $x.Five`, wantAnErr: true}, {desc: "parse comments 1", expr: parseComments1, wantObj: true, wantErr: nil}, {desc: "parse comments 2", expr: parseComments2, wantObj: true, wantErr: nil}, @@ -118,6 +128,8 @@ func TestInst_Eval(t *testing.T) { if tt.wantErr != nil { assert.ErrorIs(t, err, tt.wantErr) + } else if tt.wantAnErr { + assert.Error(t, err) } else if tt.wantObj { assert.NoError(t, err) _, isObj := res.(ucl.Object) From 2172fb86d858496c2431d2a05e6e2983da2cae79 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sat, 14 Jun 2025 02:24:43 +0200 Subject: [PATCH 10/22] Reverted back to no dollar sign for assignments --- ucl/builtins_test.go | 88 +++++++++++++++++++-------------------- ucl/eval.go | 3 +- ucl/inst_test.go | 92 ++++++++++++++++++++--------------------- ucl/userbuiltin_test.go | 2 +- 4 files changed, 92 insertions(+), 93 deletions(-) diff --git a/ucl/builtins_test.go b/ucl/builtins_test.go index effb183..4a099f9 100644 --- a/ucl/builtins_test.go +++ b/ucl/builtins_test.go @@ -295,53 +295,53 @@ func TestBuiltins_While(t *testing.T) { want string }{ {desc: "iterate while true 1", expr: ` - $x = 0 + x = 0 while (lt $x 5) { echo $x - $x = (add $x 1) + x = add $x 1 } echo "done"`, want: "0\n1\n2\n3\n4\ndone\n(nil)\n"}, {desc: "iterate while true 2", expr: ` - $x = 20 + x = 20 while (lt $x 5) { echo $x - $x = (add $x 1) + x = (add $x 1) } echo "done"`, want: "done\n(nil)\n"}, {desc: "iterate while true with pipeline", expr: ` - $x = 0 + x = 0 while (lt $x 5) { echo $x - $x = (add $x 1) + x = (add $x 1) if (ge $x 3) { break "Ahh" } } | echo " was the break" echo "done"`, want: "0\n1\n2\nAhh was the break\ndone\n(nil)\n"}, {desc: "iterate for ever with break 1", expr: ` - $x = 0 + x = 0 while { echo $x - $x = (add $x 1) + x = add $x 1 if (ge $x 5) { break } } echo "done"`, want: "0\n1\n2\n3\n4\ndone\n(nil)\n"}, {desc: "iterate for ever with break 2", expr: ` - $x = 0 + x = 0 echo (while { echo $x - $x = add $x 1 + x = add $x 1 if (ge $x 5) { break $x } }) `, want: "0\n1\n2\n3\n4\n5\n(nil)\n"}, {desc: "iterate for ever with continue", expr: ` - $x = 0 + x = 0 while { - $x = (add $x 1) + x = (add $x 1) if (or (eq $x 2) (eq $x 4)) { echo "quack" continue @@ -501,10 +501,10 @@ func TestBuiltins_Procs(t *testing.T) { } } - $helloGreater = makeGreeter "Hello" + helloGreater = makeGreeter "Hello" $helloGreater "world" - $goodbye = makeGreeter "Goodbye cruel" + goodbye = makeGreeter "Goodbye cruel" $goodbye "world" call (makeGreeter "Quick") ["call me"] @@ -512,13 +512,13 @@ func TestBuiltins_Procs(t *testing.T) { `, want: "Hello, world\nGoodbye cruel, world\nQuick, call me\n(nil)\n"}, {desc: "modifying closed over variables", expr: ` proc makeSetter { - $bla = "X" + bla = "X" proc appendToBla { |x| - $bla = cat $bla $x + bla = cat $bla $x } } - $er = makeSetter + er = makeSetter echo (call $er ["xxx"]) echo (call $er ["yyy"]) `, want: "Xxxx\nXxxxyyy\n(nil)\n"}, @@ -623,7 +623,7 @@ func TestBuiltins_Return(t *testing.T) { echo "world" } proc greet { - $what = (greetWhat) + what = (greetWhat) echo "Hello, " $what } @@ -687,7 +687,7 @@ func TestBuiltins_Return(t *testing.T) { proc test-thing { for [1 2 3] { |x| - $myClosure = proc { echo $x } + myClosure = proc { echo $x } do-thing $myClosure } } @@ -714,12 +714,12 @@ func TestBuiltins_Return(t *testing.T) { proc test-thing { [1 2 3] | map { |x| - $myProc = proc { echo $x } + myProc = proc { echo $x } proc { do-thing $myProc } } } - $hello = "xx" + hello = "xx" for (test-thing) { |y| call $y ; echo $hello } `, want: "1\nxx\n2\nxx\n3\nxx\n(nil)\n"}, {desc: "check closure 7", expr: ` @@ -728,15 +728,15 @@ func TestBuiltins_Return(t *testing.T) { } proc test-thing { - $f = 0 + f = 0 [1 2 3] | map { |x| - $myProc = proc { echo $f } - $f = (add $f 1) + myProc = proc { echo $f } + f = (add $f 1) proc { do-thing $myProc } } } - $hello = "xx" + hello = "xx" for (test-thing) { |y| call $y ; echo $hello } `, want: "3\nxx\n3\nxx\n3\nxx\n(nil)\n"}, {desc: "check closure 7", expr: ` @@ -745,16 +745,16 @@ func TestBuiltins_Return(t *testing.T) { } proc test-thing { - $f = 1 + f = 1 [1 2 3] | map { |x| - $g = $f - $myProc = (proc { echo $g }) - $f = (add $f 1) + g = $f + myProc = (proc { echo $g }) + f = (add $f 1) proc { do-thing $myProc } } } - $hello = "xx" + hello = "xx" for (test-thing) { |y| call $y ; echo $hello } `, want: "1\nxx\n2\nxx\n3\nxx\n(nil)\n"}, } @@ -917,11 +917,11 @@ func TestBuiltins_Try(t *testing.T) { } `, want: "Hello\nCatch me: bang\nAlways\n", wantErr: "boom"}, {desc: "try 12", expr: ` - $a = try { "e" } catch { "f" } + a = try { "e" } catch { "f" } echo $a `, want: "e\n(nil)\n"}, {desc: "try 13", expr: ` - $a = try { error "bang" } catch { "f" } + a = try { error "bang" } catch { "f" } echo $a `, want: "f\n(nil)\n"}, {desc: "try 14", expr: ` @@ -1081,12 +1081,12 @@ func TestBuiltins_Map(t *testing.T) { map ["a" "b" "c"] (proc { |x| makeUpper $x }) `, want: "A\nB\nC\n"}, {desc: "map list 2", expr: ` - $makeUpper = proc { |x| $x | toUpper } + makeUpper = proc { |x| $x | toUpper } map ["a" "b" "c"] $makeUpper `, want: "A\nB\nC\n"}, {desc: "map list with pipe", expr: ` - $makeUpper = proc { |x| $x | toUpper } + makeUpper = proc { |x| $x | toUpper } ["a" "b" "c"] | map $makeUpper `, want: "A\nB\nC\n"}, @@ -1094,15 +1094,15 @@ func TestBuiltins_Map(t *testing.T) { map ["a" "b" "c"] { |x| toUpper $x } `, want: "A\nB\nC\n"}, {desc: "map list with stream", expr: ` - $makeUpper = proc { |x| toUpper $x } + makeUpper = proc { |x| toUpper $x } - $l = ["a" "b" "c"] | map $makeUpper + l = ["a" "b" "c"] | map $makeUpper echo $l `, want: "[A B C]\n(nil)\n"}, {desc: "map itr stream", expr: ` - $add2 = proc { |x| add $x 2 } + add2 = proc { |x| add $x 2 } - $l = itr | map $add2 + l = itr | map $add2 for $l { |x| echo $x } `, want: "3\n4\n5\n(nil)\n"}, } @@ -1356,7 +1356,7 @@ func TestBuiltins_Filter(t *testing.T) { }}, {desc: "filter map 3", expr: `filter [alpha:"hello" bravo:"world"] { |k v| eq $v "alpha" }`, want: map[string]any{}}, - {desc: "filter itr 1", expr: `$s = "" ; itr | filter { |x| ne $x 2 } | for { |x| $s = "$s $x" }; $s`, want: " 1 3"}, + {desc: "filter itr 1", expr: `s = "" ; itr | filter { |x| ne $x 2 } | for { |x| s = "$s $x" }; $s`, want: " 1 3"}, } for _, tt := range tests { @@ -1408,10 +1408,10 @@ func TestBuiltins_Head(t *testing.T) { {desc: "head list 1", expr: `head [1 2 3]`, want: 1}, {desc: "head itr 1", expr: `head (itr)`, want: 1}, - {desc: "head itr 2", expr: `$h = (itr) ; head $h`, want: 1}, - {desc: "head itr 3", expr: `$h = (itr) ; head $h ; head $h`, want: 2}, - {desc: "head itr 4", expr: `$h = (itr) ; head $h ; head $h ; head $h`, want: 3}, - {desc: "head itr 5", expr: `$h = (itr) ; head $h ; head $h ; head $h ; head $h`, want: nil}, + {desc: "head itr 2", expr: `h = (itr) ; head $h`, want: 1}, + {desc: "head itr 3", expr: `h = (itr) ; head $h ; head $h`, want: 2}, + {desc: "head itr 4", expr: `h = (itr) ; head $h ; head $h ; head $h`, want: 3}, + {desc: "head itr 5", expr: `h = (itr) ; head $h ; head $h ; head $h ; head $h`, want: nil}, } for _, tt := range tests { @@ -1747,7 +1747,7 @@ func TestBuiltins_Cat(t *testing.T) { {desc: "cat 6", expr: `cat "array = " [1 3 2 4]`, want: "array = [1 3 2 4]"}, {desc: "cat 7", expr: `cat 1 $true 3 [4]`, want: "1true3[4]"}, {desc: "cat 8", expr: `cat`, want: ""}, - {desc: "cat 9", expr: `$x = ["a" "b" "c"] ; cat "array = " [1 $x.(0) $x.(2) $x.(1)]`, want: "array = [1 a c b]"}, + {desc: "cat 9", expr: `x = ["a" "b" "c"] ; cat "array = " [1 $x.(0) $x.(2) $x.(1)]`, want: "array = [1 a c b]"}, } for _, tt := range tests { diff --git a/ucl/eval.go b/ucl/eval.go index 82313df..71635b3 100644 --- a/ucl/eval.go +++ b/ucl/eval.go @@ -266,8 +266,7 @@ func (e evaluator) assignArg(ctx context.Context, ec *evalCtx, n astCmdArg, toVa case n.Literal != nil: return nil, errors.New("cannot assign to a literal value") case n.Var != nil: - ec.setOrDefineVar(*n.Var, toVal) - return toVal, nil + return nil, errors.New("cannot assign to a dereferenced variable") case n.PseudoVar != nil: pvar, ok := ec.getPseudoVar(*n.PseudoVar) if ok { diff --git a/ucl/inst_test.go b/ucl/inst_test.go index 718d0b7..52fdf3e 100644 --- a/ucl/inst_test.go +++ b/ucl/inst_test.go @@ -27,15 +27,15 @@ func TestInst_Eval(t *testing.T) { {desc: "simple ident 1", expr: `firstarg a-test`, want: "a-test"}, // String interpolation - {desc: "interpolate string 1", expr: `$what = "world" ; firstarg "hello $what"`, want: "hello world"}, - {desc: "interpolate string 2", expr: `$what = "world" ; $when = "now" ; firstarg "$when, hello $what"`, want: "now, hello world"}, - {desc: "interpolate string 3", expr: `$what = "world" ; $when = "now" ; firstarg "${when}, hello ${what}"`, want: "now, hello world"}, - {desc: "interpolate string 4", expr: `$crazy = [far: "unknown"] ; firstarg "hello ${crazy.far}"`, want: "hello unknown"}, - {desc: "interpolate string 5", expr: `$oldWords = ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(1)}"`, want: "hello thither"}, - {desc: "interpolate string 6", expr: `$oldWords = ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(add 1 1)}"`, want: "hello yonder"}, - {desc: "interpolate string 7", expr: `$oldWords = ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(add 2 | sub (sub 2 1) | sub 1)}"`, want: "hello hither"}, - {desc: "interpolate string 8", expr: `$words = ["old": ["hither" "thither" "yonder"] "new": ["near" "far"]] ; firstarg "hello ${words.old.(2)}"`, want: "hello yonder"}, - {desc: "interpolate string 9", expr: `$what = "world" ; firstarg "hello $($what)"`, want: "hello world"}, + {desc: "interpolate string 1", expr: `what = "world" ; firstarg "hello $what"`, want: "hello world"}, + {desc: "interpolate string 2", expr: `what = "world" ; when = "now" ; firstarg "$when, hello $what"`, want: "now, hello world"}, + {desc: "interpolate string 3", expr: `what = "world" ; when = "now" ; firstarg "${when}, hello ${what}"`, want: "now, hello world"}, + {desc: "interpolate string 4", expr: `crazy = [far: "unknown"] ; firstarg "hello ${crazy.far}"`, want: "hello unknown"}, + {desc: "interpolate string 5", expr: `oldWords = ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(1)}"`, want: "hello thither"}, + {desc: "interpolate string 6", expr: `oldWords = ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(add 1 1)}"`, want: "hello yonder"}, + {desc: "interpolate string 7", expr: `oldWords = ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(add 2 | sub (sub 2 1) | sub 1)}"`, want: "hello hither"}, + {desc: "interpolate string 8", expr: `words = ["old": ["hither" "thither" "yonder"] "new": ["near" "far"]] ; firstarg "hello ${words.old.(2)}"`, want: "hello yonder"}, + {desc: "interpolate string 9", expr: `what = "world" ; firstarg "hello $($what)"`, want: "hello world"}, {desc: "interpolate string 10", expr: `firstarg "hello $([1 2 3] | len)"`, want: "hello 3"}, {desc: "interpolate string 11", expr: `firstarg "hello $(add (add 1 2) 3)"`, want: "hello 6"}, {desc: "interpolate string 12", expr: `firstarg ("$(add 2 (add 1 1)) + $([1 2 3].(1) | cat ("$("")")) = $(("$(add 2 (4))"))")`, want: "4 + 2 = 6"}, @@ -62,55 +62,55 @@ func TestInst_Eval(t *testing.T) { // Multi-statements {desc: "multi 1", expr: `firstarg "hello" ; firstarg "world"`, want: "world"}, {desc: "multi 2", expr: `list "hello" | toUpper ; firstarg "world"`, want: "world"}, - {desc: "multi 3", expr: `$new = "this is new" ; firstarg $new`, want: "this is new"}, + {desc: "multi 3", expr: `new = "this is new" ; firstarg $new`, want: "this is new"}, // Lists {desc: "list 1", expr: `firstarg ["1" "2" "3"]`, want: []any{"1", "2", "3"}}, - {desc: "list 2", expr: `$one = "one" ; firstarg [$one (list "two" | map { |x| toUpper $x } | head) "three"]`, want: []any{"one", "TWO", "three"}}, + {desc: "list 2", expr: `one = "one" ; firstarg [$one (list "two" | map { |x| toUpper $x } | head) "three"]`, want: []any{"one", "TWO", "three"}}, {desc: "list 3", expr: `firstarg []`, want: []any{}}, - {desc: "list 4", expr: `$x = ["a" "b" "c"] ; firstarg [$x.(2) $x.(1) $x.(0)]`, want: []any{"c", "b", "a"}}, + {desc: "list 4", expr: `x = ["a" "b" "c"] ; firstarg [$x.(2) $x.(1) $x.(0)]`, want: []any{"c", "b", "a"}}, // Maps {desc: "map 1", expr: `firstarg [one:"1" two:"2" three:"3"]`, want: map[string]any{"one": "1", "two": "2", "three": "3"}}, {desc: "map 2", expr: `firstarg ["one":"1" "two":"2" "three":"3"]`, want: map[string]any{"one": "1", "two": "2", "three": "3"}}, {desc: "map 3", expr: ` - $one = "one" ; $n1 = "1" + one = "one" ; n1 = "1" firstarg [ $one:$n1 (list "two" | map { |x| toUpper $x } | head):(list "2" | map { |x| toUpper $x } | head) three:"3" ]`, want: map[string]any{"one": "1", "TWO": "2", "three": "3"}}, {desc: "map 4", expr: `firstarg [:]`, want: map[string]any{}}, - {desc: "map 5", expr: `$x = ["a" "b" "c"] ; firstarg ["one":$x.(2) "two":$x.(1) "three":$x.(0)]`, want: map[string]any{"one": "c", "two": "b", "three": "a"}}, - {desc: "map 6", expr: `$x = [a:"A" b:"B" c:"C"] ; firstarg ["one":$x.c "two":$x.b "three":$x.a]`, want: map[string]any{"one": "C", "two": "B", "three": "A"}}, + {desc: "map 5", expr: `x = ["a" "b" "c"] ; firstarg ["one":$x.(2) "two":$x.(1) "three":$x.(0)]`, want: map[string]any{"one": "c", "two": "b", "three": "a"}}, + {desc: "map 6", expr: `x = [a:"A" b:"B" c:"C"] ; firstarg ["one":$x.c "two":$x.b "three":$x.a]`, want: map[string]any{"one": "C", "two": "B", "three": "A"}}, // Dots - {desc: "dot expr 1", expr: `$x = [1 2 3] ; $x.(0)`, want: 1}, - {desc: "dot expr 2", expr: `$x = [1 2 3] ; $x.(1)`, want: 2}, - {desc: "dot expr 3", expr: `$x = [1 2 3] ; $x.(2)`, want: 3}, - {desc: "dot expr 4", expr: `$x = [1 2 3] ; $x.(3)`, want: nil}, - {desc: "dot expr 5", expr: `$x = [1 2 3] ; $x.(add 1 1)`, want: 3}, - {desc: "dot expr 6", expr: `$x = [1 2 3] ; $x.(-1)`, want: 3}, - {desc: "dot expr 7", expr: `$x = [1 2 3] ; $x.(-2)`, want: 2}, - {desc: "dot expr 8", expr: `$x = [1 2 3] ; $x.(-3)`, want: 1}, - {desc: "dot expr 9", expr: `$x = [1 2 3] ; $x.(-4)`, want: nil}, + {desc: "dot expr 1", expr: `x = [1 2 3] ; $x.(0)`, want: 1}, + {desc: "dot expr 2", expr: `x = [1 2 3] ; $x.(1)`, want: 2}, + {desc: "dot expr 3", expr: `x = [1 2 3] ; $x.(2)`, want: 3}, + {desc: "dot expr 4", expr: `x = [1 2 3] ; $x.(3)`, want: nil}, + {desc: "dot expr 5", expr: `x = [1 2 3] ; $x.(add 1 1)`, want: 3}, + {desc: "dot expr 6", expr: `x = [1 2 3] ; $x.(-1)`, want: 3}, + {desc: "dot expr 7", expr: `x = [1 2 3] ; $x.(-2)`, want: 2}, + {desc: "dot expr 8", expr: `x = [1 2 3] ; $x.(-3)`, want: 1}, + {desc: "dot expr 9", expr: `x = [1 2 3] ; $x.(-4)`, want: nil}, - {desc: "dot idents 1", expr: `$x = [alpha:"hello" bravo:"world"] ; $x.alpha`, want: "hello"}, - {desc: "dot idents 2", expr: `$x = [alpha:"hello" bravo:"world"] ; $x.bravo`, want: "world"}, - {desc: "dot idents 3", expr: `$x = [alpha:"hello" bravo:"world"] ; $x.charlie`, want: nil}, - {desc: "dot idents 4", expr: `$x = [alpha:"hello" bravo:"world"] ; $x.("alpha")`, want: "hello"}, - {desc: "dot idents 5", expr: `$x = [alpha:"hello" bravo:"world"] ; $x.("bravo")`, want: "world"}, - {desc: "dot idents 6", expr: `$x = [alpha:"hello" bravo:"world"] ; $x.("charlie")`, want: nil}, - {desc: "dot idents 7", expr: `$x = [MORE:"stuff"] ; $x.("more" | toUpper)`, want: "stuff"}, - {desc: "dot idents 8", expr: `$x = [MORE:"stuff"] ; $x.(toUpper ("more"))`, want: "stuff"}, - {desc: "dot idents 9", expr: `$x = [MORE:"stuff"] ; $x.y`, want: nil}, + {desc: "dot idents 1", expr: `x = [alpha:"hello" bravo:"world"] ; $x.alpha`, want: "hello"}, + {desc: "dot idents 2", expr: `x = [alpha:"hello" bravo:"world"] ; $x.bravo`, want: "world"}, + {desc: "dot idents 3", expr: `x = [alpha:"hello" bravo:"world"] ; $x.charlie`, want: nil}, + {desc: "dot idents 4", expr: `x = [alpha:"hello" bravo:"world"] ; $x.("alpha")`, want: "hello"}, + {desc: "dot idents 5", expr: `x = [alpha:"hello" bravo:"world"] ; $x.("bravo")`, want: "world"}, + {desc: "dot idents 6", expr: `x = [alpha:"hello" bravo:"world"] ; $x.("charlie")`, want: nil}, + {desc: "dot idents 7", expr: `x = [MORE:"stuff"] ; $x.("more" | toUpper)`, want: "stuff"}, + {desc: "dot idents 8", expr: `x = [MORE:"stuff"] ; $x.(toUpper ("more"))`, want: "stuff"}, + {desc: "dot idents 9", expr: `x = [MORE:"stuff"] ; $x.y`, want: nil}, - {desc: "dot err 1", expr: `$x = [1 2 3] ; $x.Hello`, want: nil}, - {desc: "dot err 2", expr: `$x = [1 2 3] ; $x.("world")`, want: nil}, - {desc: "dot err 4", expr: `$x = [a:1 b:2] ; $x.(5)`, want: nil}, - {desc: "dot err 3", expr: `$x = [a:1 b:2] ; $x.(0)`, want: nil}, - {desc: "dot err 5", expr: `$x = 123 ; $x.(5)`, wantAnErr: true}, - {desc: "dot err 6", expr: `$x = 123 ; $x.Five`, wantAnErr: true}, + {desc: "dot err 1", expr: `x = [1 2 3] ; $x.Hello`, want: nil}, + {desc: "dot err 2", expr: `x = [1 2 3] ; $x.("world")`, want: nil}, + {desc: "dot err 4", expr: `x = [a:1 b:2] ; $x.(5)`, want: nil}, + {desc: "dot err 3", expr: `x = [a:1 b:2] ; $x.(0)`, want: nil}, + {desc: "dot err 5", expr: `x = 123 ; $x.(5)`, wantAnErr: true}, + {desc: "dot err 6", expr: `x = 123 ; $x.Five`, wantAnErr: true}, {desc: "parse comments 1", expr: parseComments1, wantObj: true, wantErr: nil}, {desc: "parse comments 2", expr: parseComments2, wantObj: true, wantErr: nil}, @@ -148,7 +148,7 @@ func TestInst_Eval_WithSubEnv(t *testing.T) { inst := ucl.New() - res, err := inst.Eval(ctx, strings.NewReader(`$a = "hello" ; $a`), ucl.WithSubEnv()) + res, err := inst.Eval(ctx, strings.NewReader(`a = "hello" ; $a`), ucl.WithSubEnv()) assert.NoError(t, err) assert.Equal(t, "hello", res) @@ -167,15 +167,15 @@ func TestInst_Eval_WithSubEnv(t *testing.T) { }{ { descr: "reading vars", - eval1: `$a = "hello" ; hook { $a }`, - eval2: `$a = "world" ; hook { $a }`, + eval1: `a = "hello" ; hook { $a }`, + eval2: `a = "world" ; hook { $a }`, want1: "hello", want2: "world", }, { descr: "modifying vars", - eval1: `$a = "hello" ; hook { $a = "new value" ; $a }`, - eval2: `$a = "world" ; hook { $a }`, + eval1: `a = "hello" ; hook { a = "new value" ; $a }`, + eval2: `a = "world" ; hook { $a }`, want1: "new value", want2: "world", }, @@ -195,8 +195,8 @@ func TestInst_Eval_WithSubEnv(t *testing.T) { }, { descr: "exporting procs 2", - eval1: `$a = "hello" ; export say_hello { $a = "world"; $a } ; hook { say_hello }`, - eval2: `$a = "other" ; hook { say_hello }`, + eval1: `a = "hello" ; export say_hello { a = "world"; $a } ; hook { say_hello }`, + eval2: `a = "other" ; hook { say_hello }`, want1: "world", want2: "world", }, diff --git a/ucl/userbuiltin_test.go b/ucl/userbuiltin_test.go index 58bf5ab..5e3fe30 100644 --- a/ucl/userbuiltin_test.go +++ b/ucl/userbuiltin_test.go @@ -124,7 +124,7 @@ func TestInst_SetBuiltin(t *testing.T) { want string }{ {descr: "pass via args", expr: `join (add2 "left" "right")`, want: "left:right"}, - {descr: "pass via vars", expr: `$x = (add2 "blue" "green") ; join $x`, want: "blue:green"}, + {descr: "pass via vars", expr: `x = (add2 "blue" "green") ; join $x`, want: "blue:green"}, } for _, tt := range tests { From e2f471c608b0d7ba946f399204f634a5bb95dbc3 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sat, 14 Jun 2025 02:52:03 +0200 Subject: [PATCH 11/22] Added setting of index values --- ucl/builtins.go | 33 +++++++++++++++++++++++++++++++++ ucl/errors.go | 2 ++ ucl/eval.go | 46 +++++++++++++++++++++++++++++++++++++++++++++- ucl/inst_test.go | 11 +++++++++++ ucl/objs.go | 32 ++++++++++++++++++++++++++++++-- 5 files changed, 121 insertions(+), 3 deletions(-) diff --git a/ucl/builtins.go b/ucl/builtins.go index 0c84824..e1edbcc 100644 --- a/ucl/builtins.go +++ b/ucl/builtins.go @@ -539,6 +539,39 @@ func indexLookup(ctx context.Context, obj, elem Object, pos lexer.Position) (Obj return nil, nil } +func indexAssign(ctx context.Context, obj, elem, toVal Object, pos lexer.Position) (_ Object, err error) { + if obj == nil { + return nil, assignToNilIndex(pos) + } + switch v := obj.(type) { + case ModListable: + intIdx, ok := elem.(IntObject) + if !ok { + return nil, nil + } + if int(intIdx) >= 0 && int(intIdx) < v.Len() { + err = v.SetIndex(int(intIdx), toVal) + } else if int(intIdx) < 0 && int(intIdx) >= -v.Len() { + err = v.SetIndex(v.Len()+int(intIdx), toVal) + } + if err != nil { + return nil, err + } + return toVal, nil + case ModHashable: + strIdx, ok := elem.(StringObject) + if !ok { + return nil, nil + } + err = v.SetValue(string(strIdx), toVal) + if err != nil { + return nil, err + } + return toVal, nil + } + return nil, notModIndexableError(pos) +} + func indexBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if err := args.expectArgn(1); err != nil { return nil, err diff --git a/ucl/errors.go b/ucl/errors.go index f7f80e9..8e20adb 100644 --- a/ucl/errors.go +++ b/ucl/errors.go @@ -14,6 +14,8 @@ var ( var ( tooManyFinallyBlocksError = newBadUsage("try needs at most 1 finally") notIndexableError = newBadUsage("index only support on lists and hashes") + notModIndexableError = newBadUsage("list or hash cannot be modified") + assignToNilIndex = newBadUsage("assigning to nil index value") ) type errorWithPos struct { diff --git a/ucl/eval.go b/ucl/eval.go index 71635b3..c6469ae 100644 --- a/ucl/eval.go +++ b/ucl/eval.go @@ -220,7 +220,51 @@ func (e evaluator) assignDot(ctx context.Context, ec *evalCtx, n astDot, toVal O return e.assignArg(ctx, ec, n.Arg, toVal) } - return nil, errors.New("TODO") + val, err := e.evalArgForDotAssign(ctx, ec, n.Arg) + if err != nil { + return nil, err + } + + for i, dot := range n.DotSuffix { + isLast := i == len(n.DotSuffix)-1 + + var idx Object + if dot.KeyIdent != nil { + idx = StringObject(dot.KeyIdent.String()) + } else { + idx, err = e.evalPipeline(ctx, ec, dot.Pipeline) + if err != nil { + return nil, err + } + } + + if isLast { + val, err = indexAssign(ctx, val, idx, toVal, n.Pos) + } else { + val, err = indexLookup(ctx, val, idx, n.Pos) + } + if err != nil { + return nil, err + } + } + + return val, nil +} + +func (e evaluator) evalArgForDotAssign(ctx context.Context, ec *evalCtx, n astCmdArg) (Object, error) { + // Special case for dot assigns of 'a.b = c' where a is actually a var deref (i.e. $a) + // which is unnecessary for assignments. Likewise, having '$a.b = c' should be dissallowed + + switch { + case n.Ident != nil: + if v, ok := ec.getVar(n.Ident.String()); ok { + return v, nil + } + return nil, nil + case n.Var != nil: + return nil, errors.New("cannot assign to a dereferenced variable") + } + return e.evalArg(ctx, ec, n) } func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (Object, error) { diff --git a/ucl/inst_test.go b/ucl/inst_test.go index 52fdf3e..041c367 100644 --- a/ucl/inst_test.go +++ b/ucl/inst_test.go @@ -116,6 +116,17 @@ func TestInst_Eval(t *testing.T) { {desc: "parse comments 2", expr: parseComments2, wantObj: true, wantErr: nil}, {desc: "parse comments 3", expr: parseComments3, wantObj: true, wantErr: nil}, {desc: "parse comments 4", expr: parseComments4, wantObj: true, wantErr: nil}, + + // Assign dots + {desc: "assign dot 1", expr: `x = [1 2 3] ; x.(0) = 4 ; "$x"`, want: "[4 2 3]"}, + {desc: "assign dot 2", expr: `x = [1 2 3] ; x.(1) = 5 ; "$x"`, want: "[1 5 3]"}, + {desc: "assign dot 3", expr: `x = [1 2 3] ; x.(-1) = 6 ; "$x"`, want: "[1 2 6]"}, + {desc: "assign dot 4", expr: `y = [a:1 b:2] ; y.a = "hello" ; "$y"`, want: `[a:hello b:2]`}, + {desc: "assign dot 5", expr: `y = [a:1 b:2] ; y.b = "world" ; "$y"`, want: `[a:1 b:world]`}, + {desc: "assign dot 6", expr: `y = [a:"b" b:2] ; y.($y.a) = "world" ; "$y"`, want: `[a:b b:world]`}, + {desc: "assign dot 7", expr: `z = [a:[1 2] b:[3 3]] ; z.a.(1) = 3 ; "$z"`, want: `[a:[1 3] b:[3 3]]`}, + {desc: "assign dot 8", expr: `z = [[1 2] [3 4]] ; z.(1).(0) = 5 ; "$z"`, want: `[[1 2] [5 4]]`}, + {desc: "assign dot 7", expr: `z = [[a:1 b:2] [c:3 d:4]] ; z.(1).a = 5 ; "$z"`, want: `[[a:1 b:2] [a:5 c:3 d:4]]`}, } for _, tt := range tests { diff --git a/ucl/objs.go b/ucl/objs.go index 981f7ab..2ecde0f 100644 --- a/ucl/objs.go +++ b/ucl/objs.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "reflect" + "sort" "strconv" "strings" "time" @@ -35,9 +36,13 @@ type ModListable interface { // 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, shifting all other elements to the right. + // If idx is negative, then the item will be inserted // at that position from the right. Insert(idx int, obj Object) error + + // SetIndex replaces the item at index position idx with obj. + SetIndex(idx int, obj Object) error } type Hashable interface { @@ -46,6 +51,11 @@ type Hashable interface { Each(func(k string, v Object) error) error } +type ModHashable interface { + Hashable + SetValue(k string, val Object) error +} + type ListObject []Object func NewListObject() *ListObject { @@ -80,6 +90,11 @@ func (s *ListObject) Index(i int) Object { return (*s)[i] } +func (s *ListObject) SetIndex(i int, toVal Object) error { + (*s)[i] = toVal + return nil +} + type StringListObject []string func (ss StringListObject) String() string { @@ -117,9 +132,17 @@ func (s HashObject) String() string { return "[:]" } + // Return the keys in sorted order + keys := make([]string, 0, len(s)) + for k := range s { + keys = append(keys, k) + } + sort.Strings(keys) + sb := strings.Builder{} sb.WriteString("[") - for k, v := range s { + for _, k := range keys { + v := s[k] if sb.Len() != 1 { sb.WriteString(" ") } @@ -152,6 +175,11 @@ func (s HashObject) Each(fn func(k string, v Object) error) error { return nil } +func (s HashObject) SetValue(k string, val Object) error { + s[k] = val + return nil +} + type StringObject string func (s StringObject) String() string { From 58195738ba283a12ae64fb91ec6509be52f730c1 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Tue, 17 Jun 2025 13:37:52 +0200 Subject: [PATCH 12/22] Fix bugs with binding to Opaque objects and pointers --- ucl/objs.go | 2 + ucl/userbuiltin.go | 10 ++-- ucl/userbuiltin_test.go | 101 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 107 insertions(+), 6 deletions(-) diff --git a/ucl/objs.go b/ucl/objs.go index 2ecde0f..b2ddc6a 100644 --- a/ucl/objs.go +++ b/ucl/objs.go @@ -263,6 +263,8 @@ func toGoValue(obj Object) (interface{}, bool) { return v, true case proxyObject: return v.p, true + case OpaqueObject: + return v.v, true case listableProxyObject: return v.orig.Interface(), true case structProxyObject: diff --git a/ucl/userbuiltin.go b/ucl/userbuiltin.go index 83d72d6..d915eea 100644 --- a/ucl/userbuiltin.go +++ b/ucl/userbuiltin.go @@ -163,10 +163,12 @@ func (ca CallArgs) bindArg(v interface{}, arg Object) error { switch t := arg.(type) { case proxyObject: return bindProxyObject(v, reflect.ValueOf(t.p)) + case OpaqueObject: + return bindProxyObject(v, reflect.ValueOf(t.v)) case listableProxyObject: - return bindProxyObject(v, t.v) + return bindProxyObject(v, t.orig) case structProxyObject: - return bindProxyObject(v, t.v) + return bindProxyObject(v, t.orig) } return bindProxyObject(v, reflect.ValueOf(arg)) @@ -185,9 +187,9 @@ func canBindArg(v interface{}, arg Object) bool { case proxyObject: return canBindProxyObject(v, reflect.ValueOf(t.p)) case listableProxyObject: - return canBindProxyObject(v, t.v) + return canBindProxyObject(v, t.orig) case structProxyObject: - return canBindProxyObject(v, t.v) + return canBindProxyObject(v, t.orig) } return true diff --git a/ucl/userbuiltin_test.go b/ucl/userbuiltin_test.go index 5e3fe30..2e2436d 100644 --- a/ucl/userbuiltin_test.go +++ b/ucl/userbuiltin_test.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" "testing" + "ucl.lmika.dev/ucl" "github.com/stretchr/testify/assert" @@ -29,6 +30,59 @@ func TestInst_SetBuiltin(t *testing.T) { assert.Equal(t, "Hello, World", res) }) + t.Run("simple builtin accepting and returning pointers", func(t *testing.T) { + type point struct { + x, y int + } + + tests := []struct { + descr string + expr string + want string + }{ + {descr: "pass via args", expr: `vec 1 2 | vadd`, want: "3"}, + {descr: "pass via vars", expr: `x = (vec 2 3) ; vadd $x`, want: "5"}, + {descr: "pass twice", expr: `vadd (vadd2 (vec 1 2) (vec 3 4))`, want: "10"}, + } + + for _, tt := range tests { + t.Run(tt.descr, func(t *testing.T) { + inst := ucl.New() + inst.SetBuiltin("vec", func(ctx context.Context, args ucl.CallArgs) (any, error) { + var x, y int + + if err := args.Bind(&x, &y); err != nil { + return nil, err + } + + return &point{x, y}, nil + }) + inst.SetBuiltin("vadd", func(ctx context.Context, args ucl.CallArgs) (any, error) { + var v *point + + if err := args.Bind(&v); err != nil { + return nil, err + } + + return v.x + v.y, nil + }) + inst.SetBuiltin("vadd2", func(ctx context.Context, args ucl.CallArgs) (any, error) { + var v, u *point + + if err := args.Bind(&v, &u); err != nil { + return nil, err + } + + return &point{v.x + u.x, v.y + u.y}, nil + }) + + res, err := inst.EvalString(context.Background(), tt.expr) + assert.NoError(t, err) + assert.Equal(t, tt.want, fmt.Sprint(res)) + }) + } + }) + t.Run("bind shift arguments", func(t *testing.T) { inst := ucl.New() inst.SetBuiltin("add2", func(ctx context.Context, args ucl.CallArgs) (any, error) { @@ -105,7 +159,7 @@ func TestInst_SetBuiltin(t *testing.T) { return nil, err } - return pair{x, y}, nil + return ucl.Opaque(pair{x, y}), nil }) res, err := inst.EvalString(context.Background(), `add2 "Hello" "World"`) @@ -137,7 +191,7 @@ func TestInst_SetBuiltin(t *testing.T) { return nil, err } - return pair{x, y}, nil + return ucl.Opaque(pair{x, y}), nil }) inst.SetBuiltin("join", func(ctx context.Context, args ucl.CallArgs) (any, error) { var x pair @@ -156,6 +210,49 @@ func TestInst_SetBuiltin(t *testing.T) { } }) + t.Run("builtin operating on and returning proxy object for pointers", func(t *testing.T) { + type pair struct { + x, y string + } + + tests := []struct { + descr string + expr string + want string + }{ + {descr: "pass via args", expr: `join (add2 "left" "right")`, want: "left:right"}, + {descr: "pass via vars", expr: `x = (add2 "blue" "green") ; join $x`, want: "blue:green"}, + } + + for _, tt := range tests { + t.Run(tt.descr, func(t *testing.T) { + inst := ucl.New() + inst.SetBuiltin("add2", func(ctx context.Context, args ucl.CallArgs) (any, error) { + var x, y string + + if err := args.Bind(&x, &y); err != nil { + return nil, err + } + + return ucl.Opaque(&pair{x, y}), nil + }) + inst.SetBuiltin("join", func(ctx context.Context, args ucl.CallArgs) (any, error) { + var x *pair + + if err := args.Bind(&x); err != nil { + return nil, err + } + + return x.x + ":" + x.y, nil + }) + + res, err := inst.EvalString(context.Background(), tt.expr) + assert.NoError(t, err) + assert.Equal(t, tt.want, res) + }) + } + }) + t.Run("slices returned by commands treated as lists", func(t *testing.T) { tests := []struct { descr string From 4ab94410b7fd8b98e55aedb3d235641b8831d4b8 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Wed, 18 Jun 2025 23:57:14 +0200 Subject: [PATCH 13/22] Fixed nil panic in not --- ucl/builtins.go | 2 +- ucl/builtins_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/ucl/builtins.go b/ucl/builtins.go index e1edbcc..689e09e 100644 --- a/ucl/builtins.go +++ b/ucl/builtins.go @@ -283,7 +283,7 @@ func notBuiltin(ctx context.Context, args invocationArgs) (Object, error) { return nil, err } - return BoolObject(!args.args[0].Truthy()), nil + return BoolObject(!isTruthy(args.args[0])), nil } var errObjectsNotEqual = errors.New("objects not equal") diff --git a/ucl/builtins_test.go b/ucl/builtins_test.go index 4a099f9..8dea035 100644 --- a/ucl/builtins_test.go +++ b/ucl/builtins_test.go @@ -1766,6 +1766,35 @@ func TestBuiltins_Cat(t *testing.T) { } } +func TestBuiltins_Not(t *testing.T) { + tests := []struct { + desc string + expr string + want any + }{ + {desc: "not 1", expr: `not 1`, want: false}, + {desc: "not 2", expr: `not 0`, want: true}, + {desc: "not 3", expr: `not ()`, want: true}, + {desc: "not 4", expr: `not $true`, want: false}, + {desc: "not 5", expr: `not $false`, want: true}, + } + + 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.SetVar("true", true) + inst.SetVar("false", false) + + eqRes, err := inst.EvalString(ctx, tt.expr) + assert.NoError(t, err) + assert.Equal(t, tt.want, eqRes) + }) + } +} + func TestBuiltins_NotNil(t *testing.T) { tests := []struct { desc string From 7a2b0128333eca174dd659e763b8670ba31fa7c8 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Thu, 17 Jul 2025 01:21:42 +0200 Subject: [PATCH 14/22] Added the 'in' builtin and fns:uni --- cmd/cmsh/main.go | 1 + go.mod | 3 +- go.sum | 3 ++ ucl/builtins.go | 46 ++++++++++++++++++++++++++++ ucl/builtins/fns.go | 64 +++++++++++++++++++++++++++++++++++++++ ucl/builtins_test.go | 71 ++++++++++++++++++++++++++++++++++++++++++++ ucl/inst.go | 1 + ucl/objs.go | 19 ++++++++++++ ucl/userbuiltin.go | 35 ++++++++++++++++------ 9 files changed, 233 insertions(+), 10 deletions(-) create mode 100644 ucl/builtins/fns.go diff --git a/cmd/cmsh/main.go b/cmd/cmsh/main.go index 44f1921..cc5d919 100644 --- a/cmd/cmsh/main.go +++ b/cmd/cmsh/main.go @@ -26,6 +26,7 @@ func main() { ucl.WithModule(builtins.Strs()), ucl.WithModule(builtins.Lists()), ucl.WithModule(builtins.Time()), + ucl.WithModule(builtins.Fns()), ) ctx := context.Background() diff --git a/go.mod b/go.mod index cbf4d2d..b65ae1b 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/alecthomas/participle/v2 v2.1.1 github.com/chzyer/readline v1.5.1 github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 ) require ( @@ -17,4 +17,5 @@ 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 f9cdee7..a9528c9 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/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= go.abhg.dev/goldmark/frontmatter v0.2.0 h1:P8kPG0YkL12+aYk2yU3xHv4tcXzeVnN+gU0tJ5JnxRw= @@ -33,3 +34,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +lmika.dev/pkg/modash v0.0.0-20250619112300-0be0b6b35b1b h1:Oymcj66pgyJ2CtGk9lPh06P4FOekllE1iPehDwaL0vw= +lmika.dev/pkg/modash v0.0.0-20250619112300-0be0b6b35b1b/go.mod h1:8NDl/yR1eCCEhip9FJlVuMNXIeaztQ0Ks/tizExFcTI= diff --git a/ucl/builtins.go b/ucl/builtins.go index 689e09e..59edecd 100644 --- a/ucl/builtins.go +++ b/ucl/builtins.go @@ -636,6 +636,52 @@ func (mi mappedIter) Next(ctx context.Context) (Object, error) { return mi.inv.invoke(ctx, mi.args.fork([]Object{v})) } +func inBuiltin(ctx context.Context, args invocationArgs) (Object, error) { + if err := args.expectArgn(2); err != nil { + return nil, err + } + + r := args.args[1] + + if args.args[0] == nil { + return BoolObject(false), nil + } + + switch t := args.args[0].(type) { + case StringObject: + var rs string + if r != nil { + rs = r.String() + } + return BoolObject(strings.Contains(t.String(), rs)), nil + case Listable: + l := t.Len() + for i := 0; i < l; i++ { + v := t.Index(i) + + if ObjectsEqual(v, r) { + return BoolObject(true), nil + } + } + return BoolObject(false), nil + case Hashable: + v := t.Value(r.String()) + return BoolObject(v != nil), nil + case Iterable: + for t.HasNext() { + v, err := t.Next(ctx) + if err != nil { + return nil, err + } + if ObjectsEqual(v, r) { + return BoolObject(true), nil + } + } + return BoolObject(false), nil + } + return nil, errors.New("expected listable") +} + func mapBuiltin(ctx context.Context, args invocationArgs) (Object, error) { if err := args.expectArgn(2); err != nil { return nil, err diff --git a/ucl/builtins/fns.go b/ucl/builtins/fns.go new file mode 100644 index 0000000..4cc31b0 --- /dev/null +++ b/ucl/builtins/fns.go @@ -0,0 +1,64 @@ +package builtins + +import ( + "context" + + "lmika.dev/pkg/modash/moslice" + "ucl.lmika.dev/ucl" +) + +func Fns() ucl.Module { + return ucl.Module{ + Name: "fns", + Builtins: map[string]ucl.BuiltinHandler{ + "ident": fnsIdent, + "uni": fnsUni, + }, + } +} + +func fnsIdent(ctx context.Context, args ucl.CallArgs) (any, error) { + var inv ucl.Invokable + + if err := args.Bind(&inv); err != nil { + return nil, err + } + + return ucl.GoFunction(func(ctx context.Context, args ucl.CallArgs) (any, error) { + if args.NArgs() == 0 { + return nil, nil + } + + var x ucl.Object + if err := args.Bind(&x); err != nil { + return nil, err + } + + return inv.Invoke(ctx, x) + }), nil +} + +func fnsUni(ctx context.Context, args ucl.CallArgs) (any, error) { + var inv ucl.Invokable + + if err := args.Bind(&inv); err != nil { + return nil, err + } + + restArgs := moslice.Map(args.RestAsObjects(), func(o ucl.Object) any { return o }) + + return ucl.GoFunction(func(ctx context.Context, args ucl.CallArgs) (any, error) { + fwdArgs := make([]any, len(restArgs)+1) + + var o ucl.Object + if args.NArgs() != 0 { + if err := args.Bind(&o); err != nil { + return nil, err + } + } + fwdArgs[0] = o + copy(fwdArgs[1:], restArgs) + + return inv.Invoke(ctx, fwdArgs...) + }), nil +} diff --git a/ucl/builtins_test.go b/ucl/builtins_test.go index 8dea035..281fd82 100644 --- a/ucl/builtins_test.go +++ b/ucl/builtins_test.go @@ -1069,6 +1069,77 @@ func TestBuiltins_Seq(t *testing.T) { } +func TestBuiltins_Call(t *testing.T) { + tests := []struct { + desc string + expr string + want string + }{ + {desc: "call simple", expr: ` + call add [1 2] + `, want: "3\n"}, + {desc: "call with proc", expr: ` + call (proc { |x y| add $x $y }) [3 4] + `, want: "7\n"}, + {desc: "meta call", expr: ` + call "call" ["add" [1 3]] + `, want: "4\n"}, + {desc: "curry proc", expr: ` + proc curry { |name b| + proc { |a| call $name [$a $b] } + } + add2 = curry add 2 + map [1 2 3] $add2 | cat + `, want: "[3 4 5]\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()) + err := evalAndDisplay(ctx, inst, tt.expr) + + assert.NoError(t, err) + assert.Equal(t, tt.want, outW.String()) + }) + } +} + +func TestBuiltins_In(t *testing.T) { + tests := []struct { + desc string + expr string + want string + }{ + {desc: "in str 1", expr: `in "absolute" "sol"`, want: "true\n"}, + {desc: "in str 2", expr: `in "absolute" "not here"`, want: "false\n"}, + {desc: "in list 1", expr: `in [1 2 3] 2`, want: "true\n"}, + {desc: "in list 2", expr: `in [1 2 3] 4`, want: "false\n"}, + {desc: "in map as key 1", expr: `in [a:1 b:2 c:3] a`, want: "true\n"}, + {desc: "in map as key 2", expr: `in [a:1 b:2 c:3] gad`, want: "false\n"}, + {desc: "in itr 1", expr: `in (itr) 2`, want: "true\n"}, + {desc: "in itr 2", expr: `in (itr) 8`, want: "false\n"}, + {desc: "in itr 3", expr: `itr = itr ; in $itr 2 ; head $itr`, want: "3\n"}, + {desc: "in itr 4", expr: `itr = itr ; in $itr 8 ; head $itr`, want: "(nil)\n"}, + {desc: "in nil", expr: `in () 4`, want: "false\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()) + err := evalAndDisplay(ctx, inst, tt.expr) + + assert.NoError(t, err) + assert.Equal(t, tt.want, outW.String()) + }) + } +} + func TestBuiltins_Map(t *testing.T) { tests := []struct { desc string diff --git a/ucl/inst.go b/ucl/inst.go index de58436..bdc4cb7 100644 --- a/ucl/inst.go +++ b/ucl/inst.go @@ -68,6 +68,7 @@ func New(opts ...InstOption) *Inst { rootEC.addCmd("filter", invokableFunc(filterBuiltin)) rootEC.addCmd("reduce", invokableFunc(reduceBuiltin)) rootEC.addCmd("head", invokableFunc(firstBuiltin)) + rootEC.addCmd("in", invokableFunc(inBuiltin)) rootEC.addCmd("keys", invokableFunc(keysBuiltin)) diff --git a/ucl/objs.go b/ucl/objs.go index b2ddc6a..cf98303 100644 --- a/ucl/objs.go +++ b/ucl/objs.go @@ -523,6 +523,25 @@ func (i invokableFunc) invoke(ctx context.Context, args invocationArgs) (Object, return i(ctx, args) } +type GoFunction func(ctx context.Context, args CallArgs) (any, error) + +func (gf GoFunction) String() string { + return "(proc)" +} + +func (gf GoFunction) Truthy() bool { + return gf != nil +} + +func (gf GoFunction) invoke(ctx context.Context, args invocationArgs) (Object, error) { + v, err := gf(ctx, CallArgs{args: args}) + if err != nil { + return nil, err + } + + return fromGoValue(v) +} + type blockObject struct { block *astBlock closedEC *evalCtx diff --git a/ucl/userbuiltin.go b/ucl/userbuiltin.go index d915eea..4bf40b9 100644 --- a/ucl/userbuiltin.go +++ b/ucl/userbuiltin.go @@ -34,6 +34,10 @@ func (ca *CallArgs) Bind(vars ...interface{}) error { return nil } +func (ca *CallArgs) RestAsObjects() []Object { + return ca.args.args +} + func (ca *CallArgs) CanBind(vars ...interface{}) bool { if len(ca.args.args) < len(vars) { return false @@ -107,17 +111,30 @@ func (ca CallArgs) bindArg(v interface{}, arg Object) error { *t, _ = toGoValue(arg) return nil case *Invokable: - i, ok := arg.(invokable) - if !ok { + switch ait := arg.(type) { + case invokable: + *t = Invokable{ + inv: ait, + eval: ca.args.eval, + inst: ca.args.inst, + ec: ca.args.ec, + } + return nil + case StringObject: + iv := ca.args.ec.lookupInvokable(string(ait)) + if iv == nil { + return errors.New("'" + string(ait) + "' is not invokable") + } + *t = Invokable{ + inv: iv, + eval: ca.args.eval, + inst: ca.args.inst, + ec: ca.args.ec, + } + return nil + default: return errors.New("exepected invokable") } - *t = Invokable{ - inv: i, - eval: ca.args.eval, - inst: ca.args.inst, - ec: ca.args.ec, - } - return nil case *Listable: i, ok := arg.(Listable) if !ok { From 7c76e61b08e4c6fb662599eab849d7ceba4dda3b Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Fri, 18 Jul 2025 22:13:58 +1000 Subject: [PATCH 15/22] Added list:append --- go.mod | 2 +- go.sum | 1 + ucl/builtins/fns_test.go | 31 ++++++++++++++++ ucl/builtins/lists.go | 68 +++++++++++++++++++++++++++++++++-- ucl/builtins/lists_test.go | 74 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 ucl/builtins/fns_test.go 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 From dd516feed4fde9376ac62a19450118e0a64cac32 Mon Sep 17 00:00:00 2001 From: lmika Date: Fri, 24 Oct 2025 06:09:45 +0000 Subject: [PATCH 16/22] Added os:exec builtin (#2) Co-authored-by: Leon Mika Reviewed-on: https://lmika.dev/lmika/ucl/pulls/2 --- ucl/builtins/os.go | 57 ++++++++++++++++++++++++++++++++++++++--- ucl/builtins/os_test.go | 26 ++++++++++++++++++- 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/ucl/builtins/os.go b/ucl/builtins/os.go index e51f62f..9274bc7 100644 --- a/ucl/builtins/os.go +++ b/ucl/builtins/os.go @@ -2,20 +2,32 @@ package builtins import ( "context" + "errors" "os" + "os/exec" + "ucl.lmika.dev/ucl" ) +type OSProvider interface { + LookupEnv(string) (string, bool) + Exec(ctx context.Context, cmd string, args ...string) (*exec.Cmd, error) +} + type osHandlers struct { + provider OSProvider } func OS() ucl.Module { - osh := osHandlers{} + osh := osHandlers{ + provider: builtinOSProvider{}, + } return ucl.Module{ Name: "os", Builtins: map[string]ucl.BuiltinHandler{ - "env": osh.env, + "env": osh.env, + "exec": osh.exec, }, } } @@ -26,7 +38,7 @@ func (oh osHandlers) env(ctx context.Context, args ucl.CallArgs) (any, error) { return nil, err } - val, ok := os.LookupEnv(envName) + val, ok := oh.provider.LookupEnv(envName) if ok { return val, nil } @@ -38,3 +50,42 @@ func (oh osHandlers) env(ctx context.Context, args ucl.CallArgs) (any, error) { return "", nil } + +func (oh osHandlers) exec(ctx context.Context, args ucl.CallArgs) (any, error) { + var cmdArgs []string + + for args.NArgs() > 0 { + var s string + if err := args.Bind(&s); err != nil { + return nil, err + } + + cmdArgs = append(cmdArgs, s) + } + + if len(cmdArgs) == 0 { + return nil, errors.New("expected command") + } + + cmd, err := oh.provider.Exec(ctx, cmdArgs[0], cmdArgs[1:]...) + if err != nil { + return nil, err + } + + res, err := cmd.Output() + if err != nil { + return nil, err + } + + return string(res), nil +} + +type builtinOSProvider struct{} + +func (builtinOSProvider) LookupEnv(key string) (string, bool) { + return os.LookupEnv(key) +} + +func (builtinOSProvider) Exec(ctx context.Context, name string, args ...string) (*exec.Cmd, error) { + return exec.CommandContext(ctx, name, args...), nil +} diff --git a/ucl/builtins/os_test.go b/ucl/builtins/os_test.go index cbefcbf..73dc1cb 100644 --- a/ucl/builtins/os_test.go +++ b/ucl/builtins/os_test.go @@ -2,8 +2,10 @@ package builtins_test import ( "context" - "github.com/stretchr/testify/assert" "testing" + "time" + + "github.com/stretchr/testify/assert" "ucl.lmika.dev/ucl" "ucl.lmika.dev/ucl/builtins" ) @@ -34,3 +36,25 @@ func TestOS_Env(t *testing.T) { }) } } + +func TestOS_Exec(t *testing.T) { + tests := []struct { + descr string + eval string + want any + }{ + {descr: "run command 1", eval: `os:exec "echo" "hello, world"`, want: "hello, world\n"}, + {descr: "run command 1", eval: `os:exec "date" "+%Y%m%d"`, want: time.Now().Format("20060102") + "\n"}, + } + + for _, tt := range tests { + t.Run(tt.descr, func(t *testing.T) { + inst := ucl.New( + ucl.WithModule(builtins.OS()), + ) + res, err := inst.EvalString(context.Background(), tt.eval) + assert.NoError(t, err) + assert.Equal(t, tt.want, res) + }) + } +} From 1dcfede41764799a923c9130bc93beb2af1c16df Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sun, 26 Oct 2025 10:47:07 +1100 Subject: [PATCH 17/22] Replaced gopkg with modash --- .forgejo/workflows/build.yaml | 2 +- .forgejo/workflows/test.yaml | 2 +- go.mod | 9 ++++----- go.sum | 11 +++-------- repl/docs.go | 4 ++-- ucl/objs.go | 4 ++-- ucl/userbuiltin.go | 4 ++-- 7 files changed, 15 insertions(+), 21 deletions(-) diff --git a/.forgejo/workflows/build.yaml b/.forgejo/workflows/build.yaml index 6b08303..754af40 100644 --- a/.forgejo/workflows/build.yaml +++ b/.forgejo/workflows/build.yaml @@ -14,7 +14,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v2 with: - go-version: 1.24 + go-version: 1.25 - uses: actions/setup-node@v4 with: node-version: 21.1 diff --git a/.forgejo/workflows/test.yaml b/.forgejo/workflows/test.yaml index 8441947..b58a727 100644 --- a/.forgejo/workflows/test.yaml +++ b/.forgejo/workflows/test.yaml @@ -14,7 +14,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v2 with: - go-version: 1.22.4 + go-version: 1.25 - name: Build run: | make test \ No newline at end of file diff --git a/go.mod b/go.mod index b7b4cb6..b397a0c 100644 --- a/go.mod +++ b/go.mod @@ -1,21 +1,20 @@ module ucl.lmika.dev -go 1.24 +go 1.25 require ( github.com/alecthomas/participle/v2 v2.1.1 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 + github.com/yuin/goldmark v1.7.8 + go.abhg.dev/goldmark/frontmatter v0.2.0 + lmika.dev/pkg/modash v0.1.0 ) require ( github.com/BurntSushi/toml v1.2.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/yuin/goldmark v1.7.8 // indirect - 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 ) diff --git a/go.sum b/go.sum index 99a7730..98d9414 100644 --- a/go.sum +++ b/go.sum @@ -16,24 +16,19 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f h1:tz68Lhc1oR15HVz69IGbtdukdH0x70kBDEvvj5pTXyE= -github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f/go.mod h1:zHQvhjGXRro/Xp2C9dbC+ZUpE0gL4GYW75x1lk7hwzI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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= go.abhg.dev/goldmark/frontmatter v0.2.0 h1:P8kPG0YkL12+aYk2yU3xHv4tcXzeVnN+gU0tJ5JnxRw= go.abhg.dev/goldmark/frontmatter v0.2.0/go.mod h1:XqrEkZuM57djk7zrlRUB02x8I5J0px76YjkOzhB4YlU= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -lmika.dev/pkg/modash v0.0.0-20250619112300-0be0b6b35b1b h1:Oymcj66pgyJ2CtGk9lPh06P4FOekllE1iPehDwaL0vw= -lmika.dev/pkg/modash v0.0.0-20250619112300-0be0b6b35b1b/go.mod h1:8NDl/yR1eCCEhip9FJlVuMNXIeaztQ0Ks/tizExFcTI= +lmika.dev/pkg/modash v0.1.0 h1:fltroSvP0nKj9K0E6G+S9LULvB9Qhj47+SZ2b9v/v/c= +lmika.dev/pkg/modash v0.1.0/go.mod h1:8NDl/yR1eCCEhip9FJlVuMNXIeaztQ0Ks/tizExFcTI= diff --git a/repl/docs.go b/repl/docs.go index 40e3485..86b68d9 100644 --- a/repl/docs.go +++ b/repl/docs.go @@ -4,7 +4,7 @@ import ( "context" "errors" "fmt" - "github.com/lmika/gopkgs/fp/maps" + "lmika.dev/pkg/modash/momap" "os" "slices" "sort" @@ -33,7 +33,7 @@ func (d Doc) config(cmdName string, r *REPL) { func (r *REPL) helpBuiltin(ctx context.Context, args ucl.CallArgs) (any, error) { switch { case args.NArgs() == 0: - names := maps.Keys(r.commandDocs) + names := momap.Keys(r.commandDocs) sort.Strings(names) tabWriter := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0) diff --git a/ucl/objs.go b/ucl/objs.go index cf98303..76ea074 100644 --- a/ucl/objs.go +++ b/ucl/objs.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "github.com/lmika/gopkgs/fp/slices" + "lmika.dev/pkg/modash/moslice" ) type Object interface { @@ -626,7 +626,7 @@ func newStructProxyObject(v reflect.Value, orig reflect.Value) structProxyObject return structProxyObject{ v: v, orig: orig, - vf: slices.Filter(reflect.VisibleFields(v.Type()), func(t reflect.StructField) bool { return t.IsExported() }), + vf: moslice.Filter(reflect.VisibleFields(v.Type()), func(t reflect.StructField) bool { return t.IsExported() }), } } diff --git a/ucl/userbuiltin.go b/ucl/userbuiltin.go index 4bf40b9..87046f3 100644 --- a/ucl/userbuiltin.go +++ b/ucl/userbuiltin.go @@ -5,7 +5,7 @@ import ( "errors" "reflect" - "github.com/lmika/gopkgs/fp/slices" + "lmika.dev/pkg/modash/moslice" ) type BuiltinHandler func(ctx context.Context, args CallArgs) (any, error) @@ -291,7 +291,7 @@ func (i Invokable) Invoke(ctx context.Context, args ...any) (any, error) { inst: i.inst, } - invArgs.args, err = slices.MapWithError(args, func(a any) (Object, error) { + invArgs.args, err = moslice.MapWithError(args, func(a any) (Object, error) { return fromGoValue(a) }) if err != nil { From ebd8c6195630cea73988bbffc3c7420b510ede24 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Mon, 27 Oct 2025 22:05:14 +1100 Subject: [PATCH 18/22] Added additional builtins for strings and lists --- ucl/builtins/lists.go | 59 +++++++++++- ucl/builtins/lists_test.go | 44 ++++++++- ucl/builtins/os.go | 9 ++ ucl/builtins/os_test.go | 3 +- ucl/builtins/strs.go | 117 ++++++++++++++++++++--- ucl/builtins/strs_test.go | 189 ++++++++++++++++++++++++++++++++++++- ucl/inst.go | 1 + ucl/objs.go | 5 + 8 files changed, 404 insertions(+), 23 deletions(-) diff --git a/ucl/builtins/lists.go b/ucl/builtins/lists.go index 20df5b2..e62ed77 100644 --- a/ucl/builtins/lists.go +++ b/ucl/builtins/lists.go @@ -3,6 +3,7 @@ package builtins import ( "context" "errors" + "ucl.lmika.dev/ucl" ) @@ -10,10 +11,11 @@ func Lists() ucl.Module { return ucl.Module{ Name: "lists", Builtins: map[string]ucl.BuiltinHandler{ - "append": listAppend, - "first": listFirst, - "batch": listBatch, - "uniq": listUniq, + "append": listAppend, + "first": listFirst, + "batch": listBatch, + "uniq": listUniq, + "sublist": listSublist, }, } } @@ -189,3 +191,52 @@ func listBatch(ctx context.Context, args ucl.CallArgs) (any, error) { return groups, nil } + +func listSublist(ctx context.Context, args ucl.CallArgs) (any, error) { + var ( + l ucl.Listable + from, fromIdx int + to, toIdx int + ) + + if err := args.Bind(&l, &from); err != nil { + return nil, err + } + + if l == nil { + return nil, nil + } + + if args.NArgs() >= 1 { + if err := args.Bind(&to); err != nil { + return nil, err + } + + fromIdx, toIdx = listPos(l, from), listPos(l, to) + } else { + if from < 0 { + fromIdx, toIdx = listPos(l, from), l.Len() + } else { + fromIdx, toIdx = 0, listPos(l, from) + } + } + + if fromIdx > toIdx { + return ucl.NewListObject(), nil + } + + newList := ucl.NewListObjectOfLength(toIdx - fromIdx) + for i := fromIdx; i < toIdx; i++ { + if err := newList.SetIndex(i-fromIdx, l.Index(i)); err != nil { + return nil, err + } + } + return newList, nil +} + +func listPos(l ucl.Listable, pos int) int { + if pos < 0 { + return max(l.Len()+pos, 0) + } + return min(pos, l.Len()) +} diff --git a/ucl/builtins/lists_test.go b/ucl/builtins/lists_test.go index cf65a2c..b3cd919 100644 --- a/ucl/builtins/lists_test.go +++ b/ucl/builtins/lists_test.go @@ -2,8 +2,9 @@ package builtins_test import ( "context" - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" "ucl.lmika.dev/ucl" "ucl.lmika.dev/ucl/builtins" ) @@ -150,6 +151,47 @@ func TestLists_Uniq(t *testing.T) { } } +func TestStrs_Sublist(t *testing.T) { + tests := []struct { + desc string + eval string + want any + wantErr bool + }{ + {desc: "sublist 1", eval: `lists:sublist [h e l l o ',' ' ' w o r l d] 5`, want: []any{"h", "e", "l", "l", "o"}}, + {desc: "sublist 2", eval: `lists:sublist [h e l l o ',' ' ' w o r l d] 5000`, want: []any{"h", "e", "l", "l", "o", ",", " ", "w", "o", "r", "l", "d"}}, + {desc: "sublist 3", eval: `lists:sublist [h e l l o ',' ' ' w o r l d] -5`, want: []any{"w", "o", "r", "l", "d"}}, + {desc: "sublist 4", eval: `lists:sublist [h e l l o ',' ' ' w o r l d] -5000`, want: []any{"h", "e", "l", "l", "o", ",", " ", "w", "o", "r", "l", "d"}}, + {desc: "sublist 5", eval: `lists:sublist [h e l l o ',' ' ' w o r l d] 0 5`, want: []any{"h", "e", "l", "l", "o"}}, + {desc: "sublist 6", eval: `lists:sublist [h e l l o ',' ' ' w o r l d] 3 10`, want: []any{"l", "o", ",", " ", "w", "o", "r"}}, + {desc: "sublist 7", eval: `lists:sublist [h e l l o ',' ' ' w o r l d] 3 10000`, want: []any{"l", "o", ",", " ", "w", "o", "r", "l", "d"}}, + {desc: "sublist 8", eval: `lists:sublist [h e l l o ',' ' ' w o r l d] 3 -5`, want: []any{"l", "o", ",", " "}}, + {desc: "sublist 9", eval: `lists:sublist [h e l l o ',' ' ' w o r l d] -9 -5`, want: []any{"l", "o", ",", " "}}, + {desc: "sublist 10", eval: `lists:sublist [h e l l o ',' ' ' w o r l d] 8 5`, want: []any{}}, + {desc: "sublist 11", eval: `lists:sublist [] 8 5`, want: []any{}}, + + {desc: "err 1", eval: `lists:sublist`, wantErr: true}, + {desc: "err 2", eval: `lists:sublist "asd"`, wantErr: true}, + {desc: "err 3", eval: `lists:sublist ["asd"]`, wantErr: true}, + {desc: "err 4", eval: `lists:sublist () 8 5`, wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + inst := ucl.New( + 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 uclListOf(args ...any) *ucl.ListObject { newList := ucl.NewListObject() for _, arg := range args { diff --git a/ucl/builtins/os.go b/ucl/builtins/os.go index 9274bc7..e19c19f 100644 --- a/ucl/builtins/os.go +++ b/ucl/builtins/os.go @@ -5,6 +5,7 @@ import ( "errors" "os" "os/exec" + "strings" "ucl.lmika.dev/ucl" ) @@ -72,6 +73,14 @@ func (oh osHandlers) exec(ctx context.Context, args ucl.CallArgs) (any, error) { return nil, err } + if args.HasSwitch("in") { + var inVal string + if err := args.BindSwitch("in", &inVal); err != nil { + return nil, err + } + cmd.Stdin = strings.NewReader(inVal) + } + res, err := cmd.Output() if err != nil { return nil, err diff --git a/ucl/builtins/os_test.go b/ucl/builtins/os_test.go index 73dc1cb..b54b59f 100644 --- a/ucl/builtins/os_test.go +++ b/ucl/builtins/os_test.go @@ -44,7 +44,8 @@ func TestOS_Exec(t *testing.T) { want any }{ {descr: "run command 1", eval: `os:exec "echo" "hello, world"`, want: "hello, world\n"}, - {descr: "run command 1", eval: `os:exec "date" "+%Y%m%d"`, want: time.Now().Format("20060102") + "\n"}, + {descr: "run command 2", eval: `os:exec "date" "+%Y%m%d"`, want: time.Now().Format("20060102") + "\n"}, + {descr: "run command 3", eval: `os:exec "tr" "[a-z]" "[A-Z]" -in "hello"`, want: "HELLO"}, } for _, tt := range tests { diff --git a/ucl/builtins/strs.go b/ucl/builtins/strs.go index 281dd4c..c27ded5 100644 --- a/ucl/builtins/strs.go +++ b/ucl/builtins/strs.go @@ -4,6 +4,7 @@ import ( "context" "errors" "strings" + "ucl.lmika.dev/ucl" ) @@ -11,16 +12,74 @@ func Strs() ucl.Module { return ucl.Module{ Name: "strs", Builtins: map[string]ucl.BuiltinHandler{ - "to-upper": toUpper, - "to-lower": toLower, - "trim": trim, - "split": split, - "join": join, - "has-prefix": hasPrefix, + "to-upper": toUpper, + "to-lower": toLower, + "trim": trim, + "split": split, + "join": join, + "has-prefix": hasPrefix, + "has-suffix": hasSuffix, + "trim-prefix": trimPrefix, + "trim-suffix": trimSuffix, + "substr": strsSubstr, + "replace": strsReplace, }, } } +func strsReplace(ctx context.Context, args ucl.CallArgs) (any, error) { + var ( + s string + from string + to string + ) + + if err := args.Bind(&s, &from, &to); err != nil { + return nil, err + } + + var count = -1 + if args.HasSwitch("n") { + if err := args.BindSwitch("n", &count); err != nil { + return nil, err + } + } + + return strings.Replace(s, from, to, count), nil +} + +func strsSubstr(ctx context.Context, args ucl.CallArgs) (any, error) { + var ( + s string + from int + to int + ) + + if err := args.Bind(&s, &from); err != nil { + return nil, err + } + + if args.NArgs() >= 1 { + if err := args.Bind(&to); err != nil { + return nil, err + } + + ffr := strPos(s, from) + tfr := strPos(s, to) + if ffr > tfr { + return "", nil + } + return s[ffr:tfr], nil + + } else { + if from < 0 { + return s[strPos(s, from):], nil + } else { + return s[:strPos(s, from)], nil + } + } +} + func toUpper(ctx context.Context, args ucl.CallArgs) (any, error) { var s string if err := args.Bind(&s); err != nil { @@ -57,17 +116,40 @@ func hasPrefix(ctx context.Context, args ucl.CallArgs) (any, error) { return strings.HasPrefix(s, prefix), nil } -func split(ctx context.Context, args ucl.CallArgs) (any, error) { - var s string - if err := args.Bind(&s); err != nil { +func hasSuffix(ctx context.Context, args ucl.CallArgs) (any, error) { + var s, suffix string + if err := args.Bind(&s, &suffix); err != nil { return nil, err } - sep := "" - if args.NArgs() > 0 { - if err := args.Bind(&sep); err != nil { - return nil, err - } + return strings.HasSuffix(s, suffix), nil +} + +func trimPrefix(ctx context.Context, args ucl.CallArgs) (any, error) { + var s, prefix string + if err := args.Bind(&s, &prefix); err != nil { + return nil, err + } + + return strings.TrimPrefix(s, prefix), nil +} + +func trimSuffix(ctx context.Context, args ucl.CallArgs) (any, error) { + var s, suffix string + if err := args.Bind(&s, &suffix); err != nil { + return nil, err + } + + return strings.TrimSuffix(s, suffix), nil +} + +func split(ctx context.Context, args ucl.CallArgs) (any, error) { + var ( + s string + sep string + ) + if err := args.Bind(&s, &sep); err != nil { + return nil, err } n := -1 @@ -128,3 +210,10 @@ func join(ctx context.Context, args ucl.CallArgs) (any, error) { return nil, errors.New("expected listable or iterable as arg 1") } + +func strPos(s string, pos int) int { + if pos < 0 { + return max(len(s)+pos, 0) + } + return min(pos, len(s)) +} diff --git a/ucl/builtins/strs_test.go b/ucl/builtins/strs_test.go index 6dc5436..3335c92 100644 --- a/ucl/builtins/strs_test.go +++ b/ucl/builtins/strs_test.go @@ -2,8 +2,9 @@ package builtins_test import ( "context" - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" "ucl.lmika.dev/ucl" "ucl.lmika.dev/ucl/builtins" ) @@ -145,14 +146,15 @@ func TestStrs_Split(t *testing.T) { {desc: "split 3", eval: `strs:split "" ";"`, want: []string{""}}, {desc: "split 4", eval: `strs:split " " ";"`, want: []string{" "}}, - {desc: "split by char 1", eval: `strs:split "123"`, want: []string{"1", "2", "3"}}, + {desc: "split by char 1", eval: `strs:split "123" ""`, want: []string{"1", "2", "3"}}, {desc: "split max 1", eval: `strs:split "1,2,3" "," -max 2`, want: []string{"1", "2,3"}}, {desc: "split max 2", eval: `strs:split "1,2,3" "," -max 5`, want: []string{"1", "2", "3"}}, - {desc: "split by char max 1", eval: `strs:split "12345" -max 3`, want: []string{"1", "2", "345"}}, + {desc: "split by char max 1", eval: `strs:split "12345" "" -max 3`, want: []string{"1", "2", "345"}}, {desc: "err 1", eval: `strs:split "1,2,3" -max []`, wantErr: true}, + {desc: "err 1", eval: `strs:split "1,2,3"`, wantErr: true}, } for _, tt := range tests { @@ -202,3 +204,184 @@ func TestStrs_Join(t *testing.T) { }) } } + +func TestStrs_HasSuffix(t *testing.T) { + tests := []struct { + desc string + eval string + want any + wantErr bool + }{ + {desc: "has suffix 1", eval: `strs:has-suffix "hello, world" "world"`, want: true}, + {desc: "has suffix 2", eval: `strs:has-suffix "hello, world" "hello"`, want: false}, + {desc: "has suffix 3", eval: `strs:has-suffix "" "world"`, want: false}, + {desc: "has suffix 4", eval: `strs:has-suffix "hello" ""`, want: true}, + {desc: "has suffix 5", eval: `strs:has-suffix "test.txt" ".txt"`, want: true}, + {desc: "has suffix 6", eval: `strs:has-suffix "test.txt" ".pdf"`, want: false}, + + {desc: "err 1", eval: `strs:has-suffix`, wantErr: true}, + {desc: "err 2", eval: `strs:has-suffix "asd"`, wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + inst := ucl.New( + ucl.WithModule(builtins.Strs()), + ) + 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 TestStrs_TrimPrefix(t *testing.T) { + tests := []struct { + desc string + eval string + want any + wantErr bool + }{ + {desc: "trim prefix 1", eval: `strs:trim-prefix "hello, world" "hello, "`, want: "world"}, + {desc: "trim prefix 2", eval: `strs:trim-prefix "goodbye, world" "hello"`, want: "goodbye, world"}, + {desc: "trim prefix 3", eval: `strs:trim-prefix "" "world"`, want: ""}, + {desc: "trim prefix 4", eval: `strs:trim-prefix "hello" ""`, want: "hello"}, + {desc: "trim prefix 5", eval: `strs:trim-prefix "test.txt" "test"`, want: ".txt"}, + {desc: "trim prefix 6", eval: `strs:trim-prefix "/path/to/file" "/path/"`, want: "to/file"}, + + {desc: "err 1", eval: `strs:trim-prefix`, wantErr: true}, + {desc: "err 2", eval: `strs:trim-prefix "asd"`, wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + inst := ucl.New( + ucl.WithModule(builtins.Strs()), + ) + 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 TestStrs_TrimSuffix(t *testing.T) { + tests := []struct { + desc string + eval string + want any + wantErr bool + }{ + {desc: "trim suffix 1", eval: `strs:trim-suffix "hello, world" ", world"`, want: "hello"}, + {desc: "trim suffix 2", eval: `strs:trim-suffix "hello, world" "goodbye"`, want: "hello, world"}, + {desc: "trim suffix 3", eval: `strs:trim-suffix "" "world"`, want: ""}, + {desc: "trim suffix 4", eval: `strs:trim-suffix "hello" ""`, want: "hello"}, + {desc: "trim suffix 5", eval: `strs:trim-suffix "test.txt" ".txt"`, want: "test"}, + {desc: "trim suffix 6", eval: `strs:trim-suffix "file.backup" ".backup"`, want: "file"}, + + {desc: "err 1", eval: `strs:trim-suffix`, wantErr: true}, + {desc: "err 2", eval: `strs:trim-suffix "asd"`, wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + inst := ucl.New( + ucl.WithModule(builtins.Strs()), + ) + 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 TestStrs_Substr(t *testing.T) { + tests := []struct { + desc string + eval string + want any + wantErr bool + }{ + {desc: "substr 1", eval: `strs:substr "hello, world" 5`, want: "hello"}, + {desc: "substr 2", eval: `strs:substr "hello, world" 5000`, want: "hello, world"}, + {desc: "substr 3", eval: `strs:substr "hello, world" -5`, want: "world"}, + {desc: "substr 4", eval: `strs:substr "hello, world" -5000`, want: "hello, world"}, + {desc: "substr 5", eval: `strs:substr "hello, world" 0 5`, want: "hello"}, + {desc: "substr 6", eval: `strs:substr "hello, world" 3 10`, want: "lo, wor"}, + {desc: "substr 7", eval: `strs:substr "hello, world" 3 10000`, want: "lo, world"}, + {desc: "substr 8", eval: `strs:substr "hello, world" 3 -5`, want: "lo, "}, + {desc: "substr 9", eval: `strs:substr "hello, world" -9 -5`, want: "lo, "}, + {desc: "substr 10", eval: `strs:substr "hello, world" 8 5`, want: ""}, + {desc: "substr 11", eval: `strs:substr "" 8 5`, want: ""}, + {desc: "substr 12", eval: `strs:substr () 8 5`, want: ""}, + + {desc: "err 1", eval: `strs:substr`, wantErr: true}, + {desc: "err 2", eval: `strs:substr "asd"`, wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + inst := ucl.New( + ucl.WithModule(builtins.Strs()), + ) + 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 TestStrs_Replace(t *testing.T) { + tests := []struct { + desc string + eval string + want any + wantErr bool + }{ + {desc: "replace 1", eval: `strs:replace "hello hello hello" "hello" "world"`, want: "world world world"}, + {desc: "replace 2", eval: `strs:replace "hello hi hello hi" "hello" "lo"`, want: "lo hi lo hi"}, + {desc: "replace 3", eval: `strs:replace "hello hello hello" "hello" "world" -n 1`, want: "world hello hello"}, + {desc: "replace 4", eval: `strs:replace "hello hello hello" "hello" "world" -n 2`, want: "world world hello"}, + {desc: "replace 5", eval: `strs:replace "hello hello hello" "hello" "world" -n 0`, want: "hello hello hello"}, + {desc: "replace 6", eval: `strs:replace "each one" "" "|"`, want: "|e|a|c|h| |o|n|e|"}, + {desc: "replace 7", eval: `strs:replace "hello hello hello" "hello" ""`, want: " "}, + {desc: "replace 8", eval: `strs:replace "nothing to replace here" "what" "why"`, want: "nothing to replace here"}, + {desc: "replace 9", eval: `strs:replace "" "what" "why"`, want: ""}, + {desc: "replace 10", eval: `strs:replace "" "hello" "world" -n 0`, want: ""}, + + {desc: "err 1", eval: `strs:replace`, wantErr: true}, + {desc: "err 2", eval: `strs:replace "asd"`, wantErr: true}, + {desc: "err 3", eval: `strs:replace "asd" "asd"`, wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + inst := ucl.New( + ucl.WithModule(builtins.Strs()), + ) + 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) + } + }) + } +} diff --git a/ucl/inst.go b/ucl/inst.go index bdc4cb7..5b671c3 100644 --- a/ucl/inst.go +++ b/ucl/inst.go @@ -82,6 +82,7 @@ func New(opts ...InstOption) *Inst { rootEC.addCmd("str", invokableFunc(strBuiltin)) rootEC.addCmd("int", invokableFunc(intBuiltin)) + rootEC.addCmd("nil?", invokableFunc(notNilBuiltin)) rootEC.addCmd("!nil", invokableFunc(notNilBuiltin)) rootEC.addCmd("add", invokableFunc(addBuiltin)) diff --git a/ucl/objs.go b/ucl/objs.go index 76ea074..fedb25f 100644 --- a/ucl/objs.go +++ b/ucl/objs.go @@ -62,6 +62,11 @@ func NewListObject() *ListObject { return &ListObject{} } +func NewListObjectOfLength(l int) *ListObject { + o := make(ListObject, l) + return &o +} + func (lo *ListObject) Append(o Object) { *lo = append(*lo, o) } From 0d5e406f9008b717a11364a92c1f31f0aebc7922 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Mon, 26 Jan 2026 10:58:17 +1100 Subject: [PATCH 19/22] Started working on urls package --- cmd/cmsh/main.go | 4 ++- ucl/builtins/urls.go | 67 +++++++++++++++++++++++++++++++++++++++ ucl/builtins/urls_test.go | 37 +++++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 ucl/builtins/urls.go create mode 100644 ucl/builtins/urls_test.go diff --git a/cmd/cmsh/main.go b/cmd/cmsh/main.go index cc5d919..bfcf4cd 100644 --- a/cmd/cmsh/main.go +++ b/cmd/cmsh/main.go @@ -3,8 +3,9 @@ package main import ( "context" "fmt" - "github.com/chzyer/readline" "log" + + "github.com/chzyer/readline" "ucl.lmika.dev/repl" "ucl.lmika.dev/ucl" "ucl.lmika.dev/ucl/builtins" @@ -27,6 +28,7 @@ func main() { ucl.WithModule(builtins.Lists()), ucl.WithModule(builtins.Time()), ucl.WithModule(builtins.Fns()), + ucl.WithModule(builtins.URLs()), ) ctx := context.Background() diff --git a/ucl/builtins/urls.go b/ucl/builtins/urls.go new file mode 100644 index 0000000..f21f391 --- /dev/null +++ b/ucl/builtins/urls.go @@ -0,0 +1,67 @@ +package builtins + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "strings" + + "ucl.lmika.dev/ucl" +) + +func URLs() ucl.Module { + return ucl.Module{ + Name: "urls", + Builtins: map[string]ucl.BuiltinHandler{ + "fetch": urlsFetch, + }, + } +} + +func urlsFetch(ctx context.Context, args ucl.CallArgs) (any, error) { + var url string + + if err := args.Bind(&url); err != nil { + return nil, err + } + + if strings.HasPrefix(url, "http:") || strings.HasPrefix(url, "https:") { + return urlFetchHTTP(ctx, url, args) + } + + return nil, errors.New("unsupported URL scheme") +} + +func urlFetchHTTP(ctx context.Context, url string, args ucl.CallArgs) (any, error) { + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("non-200 status code: %v", resp.StatusCode) + } + + // Do content negotiation + contentType := resp.Header.Get("Content-Type") + switch { + case strings.HasPrefix(contentType, "text/"): + bts, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + // TODO: honour encoding + return string(bts), nil + } + + return nil, errors.New("unsupported content type: " + contentType) +} diff --git a/ucl/builtins/urls_test.go b/ucl/builtins/urls_test.go new file mode 100644 index 0000000..47d46a9 --- /dev/null +++ b/ucl/builtins/urls_test.go @@ -0,0 +1,37 @@ +package builtins_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "ucl.lmika.dev/ucl" + "ucl.lmika.dev/ucl/builtins" +) + +func TestURLs_Fetch_http(t *testing.T) { + tests := []struct { + desc string + eval string + want any + wantErr bool + }{ + {desc: "fetch 1", eval: `in (urls:fetch "https://www.example.com") "Example Domain"`, want: true}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + inst := ucl.New( + ucl.WithModule(builtins.Strs()), + ucl.WithModule(builtins.URLs()), + ) + 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) + } + }) + } +} From 852ea7c0f7f0dd3e824c87e15ed5af288d805cb0 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sun, 15 Feb 2026 15:29:24 +1100 Subject: [PATCH 20/22] Started writing module documentation --- _docs/mods.tmpl | 14 + cmd/doc/main.go | 17 + go.mod | 65 +++- go.sum | 729 +++++++++++++++++++++++++++++++++++++++ ucl/builtins/csv.go | 21 ++ ucl/builtins/fns.go | 22 +- ucl/builtins/fns_test.go | 4 +- 7 files changed, 867 insertions(+), 5 deletions(-) create mode 100644 _docs/mods.tmpl create mode 100644 cmd/doc/main.go diff --git a/_docs/mods.tmpl b/_docs/mods.tmpl new file mode 100644 index 0000000..bce7149 --- /dev/null +++ b/_docs/mods.tmpl @@ -0,0 +1,14 @@ +# Module {{ (.FirstPartWithMeta "module").FirstMeta "module" }} + +{{ (.FirstPartWithMeta "module").Body }} + +{{ range .PartsWithMeta "fn" }} + +## {{ .FirstMeta "fn" }} + +``` +{{ .FirstMeta "syntax" }} +``` +{{ .Body }} + +{{ end }} diff --git a/cmd/doc/main.go b/cmd/doc/main.go new file mode 100644 index 0000000..d4da430 --- /dev/null +++ b/cmd/doc/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "log" + + "lmika.dev/pkg/progdoc" +) + +func main() { + if err := progdoc.Site( + progdoc.Meta(progdoc.SiteMeta{Title: "UCL"}), + progdoc.Path("/").File("_docs/index.md"), + progdoc.Path("/mods").GoFiles("ucl/builtins", "_docs/mods.tmpl"), + ).Generate(); err != nil { + log.Fatal(err) + } +} diff --git a/go.mod b/go.mod index b397a0c..bd5461c 100644 --- a/go.mod +++ b/go.mod @@ -5,16 +5,75 @@ go 1.25 require ( github.com/alecthomas/participle/v2 v2.1.1 github.com/chzyer/readline v1.5.1 - github.com/stretchr/testify v1.10.0 - github.com/yuin/goldmark v1.7.8 + github.com/stretchr/testify v1.11.1 + github.com/yuin/goldmark v1.7.16 go.abhg.dev/goldmark/frontmatter v0.2.0 lmika.dev/pkg/modash v0.1.0 ) require ( + github.com/Azure/go-autorest v14.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest v0.11.30 // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.24 // indirect + github.com/Azure/go-autorest/autorest/date v0.3.1 // indirect + github.com/Azure/go-autorest/logger v0.2.2 // indirect + github.com/Azure/go-autorest/tracing v0.6.1 // indirect github.com/BurntSushi/toml v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.2.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/analysis v0.24.2 // indirect + github.com/go-openapi/errors v0.22.6 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/loads v0.23.2 // indirect + github.com/go-openapi/runtime v0.29.2 // indirect + github.com/go-openapi/spec v0.22.3 // indirect + github.com/go-openapi/strfmt v0.25.0 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect + github.com/go-openapi/validate v0.25.1 // indirect + github.com/go-stack/stack v1.8.1 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.9.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/netlify/open-api/v2 v2.48.0 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect + github.com/rsc/goversion v1.2.0 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect + go.mongodb.org/mongo-driver v1.17.9 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + lmika.dev/pkg/progdoc v0.0.0-20260207235039-984257fb414b // indirect ) diff --git a/go.sum b/go.sum index 98d9414..8cf84bd 100644 --- a/go.sum +++ b/go.sum @@ -1,34 +1,763 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest v0.10.1/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630= +github.com/Azure/go-autorest/autorest v0.11.30 h1:iaZ1RGz/ALZtN5eq4Nr1SOFSlf2E4pDI3Tcsl+dZPVE= +github.com/Azure/go-autorest/autorest v0.11.30/go.mod h1:t1kpPIOpIVX7annvothKvb0stsrXa37i7b+xpmBW8Fs= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= +github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk= +github.com/Azure/go-autorest/autorest/adal v0.9.24 h1:BHZfgGsGwdkHDyZdtQRQk1WeUdW0m2WPAwuHZwUi5i4= +github.com/Azure/go-autorest/autorest/adal v0.9.24/go.mod h1:7T1+g0PYFmACYW5LlG2fcoPiPlFHjClyRGL7dRlP5c8= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/date v0.3.1 h1:o9Z8Jyt+VJJTCZ/UORishuHOusBwolhjokt9s5k8I4w= +github.com/Azure/go-autorest/autorest/date v0.3.1/go.mod h1:Dz/RDmXlfiFFS/eW+b/xMUSFs1tboPVy6UjgADToWDM= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/logger v0.2.2 h1:hYqBsEBywrrOSW24kkOCXRcKfKhK76OzLTfF+MYDE2o= +github.com/Azure/go-autorest/logger v0.2.2/go.mod h1:I5fg9K52o+iuydlWfa9T5K6WFos9XYr9dYTFzpqgibw= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-autorest/tracing v0.6.1 h1:YUMSrC/CeD1ZnnXcNYU4a/fzsO35u2Fsful9L/2nyR0= +github.com/Azure/go-autorest/tracing v0.6.1/go.mod h1:/3EgjbsjraOqiicERAeu3m7/z0x1TzjQGAwDrJrXGkc= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28= +github.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/alecthomas/assert/v2 v2.3.0 h1:mAsH2wmvjsuvyBvAmCtm7zFsBlb8mIHx5ySLVdDZXL0= github.com/alecthomas/assert/v2 v2.3.0/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff/v4 v4.0.2/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/corbym/gocrest v1.0.3/go.mod h1:maVFL5lbdS2PgfOQgGRWDYTeunSWQeiEgoNdTABShCs= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= +github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/analysis v0.19.4/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= +github.com/go-openapi/analysis v0.19.10/go.mod h1:qmhS3VNFxBlquFJ0RGoDtylO9y4pgTAUNE9AEEMdlJQ= +github.com/go-openapi/analysis v0.19.16/go.mod h1:GLInF007N83Ad3m8a/CbQ5TPzdnGT7workfHwuVjNVk= +github.com/go-openapi/analysis v0.24.2 h1:6p7WXEuKy1llDgOH8FooVeO+Uq2za9qoAOq4ZN08B50= +github.com/go-openapi/analysis v0.24.2/go.mod h1:x27OOHKANE0lutg2ml4kzYLoHGMKgRm1Cj2ijVOjJuE= +github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/errors v0.19.3/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/errors v0.19.6/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.19.7/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.22.6 h1:eDxcf89O8odEnohIXwEjY1IB4ph5vmbUsBMsFNwXWPo= +github.com/go-openapi/errors v0.22.6/go.mod h1:z9S8ASTUqx7+CP1Q8dD8ewGH/1JWFFLX/2PmAYNQLgk= +github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= +github.com/go-openapi/loads v0.19.3/go.mod h1:YVfqhUCdahYwR3f3iiwQLhicVRvLlU/WO5WPaZvcvSI= +github.com/go-openapi/loads v0.19.5/go.mod h1:dswLCAdonkRufe/gSUC3gN8nTSaB9uaS2es0x5/IbjY= +github.com/go-openapi/loads v0.19.6/go.mod h1:brCsvE6j8mnbmGBh103PT/QLHfbyDxA4hsKvYBNEGVc= +github.com/go-openapi/loads v0.19.7/go.mod h1:brCsvE6j8mnbmGBh103PT/QLHfbyDxA4hsKvYBNEGVc= +github.com/go-openapi/loads v0.20.0/go.mod h1:2LhKquiE513rN5xC6Aan6lYOSddlL8Mp20AW9kpviM4= +github.com/go-openapi/loads v0.23.2 h1:rJXAcP7g1+lWyBHC7iTY+WAF0rprtM+pm8Jxv1uQJp4= +github.com/go-openapi/loads v0.23.2/go.mod h1:IEVw1GfRt/P2Pplkelxzj9BYFajiWOtY2nHZNj4UnWY= +github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= +github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= +github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= +github.com/go-openapi/runtime v0.19.15/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo= +github.com/go-openapi/runtime v0.19.16/go.mod h1:5P9104EJgYcizotuXhEuUrzVc+j1RiSjahULvYmlv98= +github.com/go-openapi/runtime v0.19.24/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk= +github.com/go-openapi/runtime v0.29.2 h1:UmwSGWNmWQqKm1c2MGgXVpC2FTGwPDQeUsBMufc5Yj0= +github.com/go-openapi/runtime v0.29.2/go.mod h1:biq5kJXRJKBJxTDJXAa00DOTa/anflQPhT0/wmjuy+0= +github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.19.6/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +github.com/go-openapi/spec v0.19.8/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +github.com/go-openapi/spec v0.19.15/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU= +github.com/go-openapi/spec v0.20.0/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU= +github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc= +github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs= +github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= +github.com/go-openapi/strfmt v0.19.2/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/strfmt v0.19.4/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= +github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= +github.com/go-openapi/strfmt v0.19.11/go.mod h1:UukAYgTaQfqJuAFlNxxMWNvMYiwiXtLsF2VwmoFtbtc= +github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ= +github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.7/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= +github.com/go-openapi/swag v0.19.9/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= +github.com/go-openapi/swag v0.19.12/go.mod h1:eFdyEBkTdoAf/9RXBvj4cr1nH7GD8Kzo5HTt47gr72M= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= +github.com/go-openapi/validate v0.19.3/go.mod h1:90Vh6jjkTn+OT1Eefm0ZixWNFjhtOH7vS9k0lo6zwJo= +github.com/go-openapi/validate v0.19.10/go.mod h1:RKEZTUWDkxKQxN2jDT7ZnZi2bhZlbNMAuKvKB+IaGx8= +github.com/go-openapi/validate v0.19.12/go.mod h1:Rzou8hA/CBw8donlS6WNEUQupNvUZ0waH08tGe6kAQ4= +github.com/go-openapi/validate v0.19.15/go.mod h1:tbn/fdOwYHgrhPBzidZfJC2MIVvs9GA7monOmWBbeCI= +github.com/go-openapi/validate v0.20.0/go.mod h1:b60iJT+xNNLfaQJUqLI7946tYiFEOuE9E4k54HpKcJ0= +github.com/go-openapi/validate v0.25.1 h1:sSACUI6Jcnbo5IWqbYHgjibrhhmt3vR6lCzKZnmAgBw= +github.com/go-openapi/validate v0.25.1/go.mod h1:RMVyVFYte0gbSTaZ0N4KmTn6u/kClvAFp+mAVfS/DQc= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= +github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= +github.com/go-swagger/go-swagger v0.24.0/go.mod h1:c9aQFJez+LeV1heNtxEclIL2XIbeS+CTKt+Bm1kuz4E= +github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013/go.mod h1:b65mBPzqzZWxOZGxSWrqs4GInLIn+u99Q9q7p+GKni0= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= +github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= +github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= +github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= +github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= +github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= +github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= +github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= +github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= +github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= +github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= +github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= +github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= +github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= +github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= +github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= +github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= +github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/netlify/open-api/v2 v2.48.0 h1:sTZ0Nuqj3JIQwEnaQuT+obF0NHQ3vkNr6FdO2xPRPUI= +github.com/netlify/open-api/v2 v2.48.0/go.mod h1:g4zTkS20wcAzQCJuBfaIw4pUeTPpaKTDMIT/9yiNoNs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= +github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rsc/goversion v1.2.0 h1:zVF4y5ciA/rw779S62bEAq4Yif1cBc/UwRkXJ2xZyT4= +github.com/rsc/goversion v1.2.0/go.mod h1:Tf/O0TQyfRvp7NelXAyfXYRKUO+LX3KNgXc8ALRUv4k= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM= +github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= +github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= +github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= go.abhg.dev/goldmark/frontmatter v0.2.0 h1:P8kPG0YkL12+aYk2yU3xHv4tcXzeVnN+gU0tJ5JnxRw= go.abhg.dev/goldmark/frontmatter v0.2.0/go.mod h1:XqrEkZuM57djk7zrlRUB02x8I5J0px76YjkOzhB4YlU= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.3.0/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE= +go.mongodb.org/mongo-driver v1.3.4/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE= +go.mongodb.org/mongo-driver v1.4.3/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc= +go.mongodb.org/mongo-driver v1.4.4/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc= +go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= +go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200610111108-226ff32320da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200612220849-54c614fe050c/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= lmika.dev/pkg/modash v0.1.0 h1:fltroSvP0nKj9K0E6G+S9LULvB9Qhj47+SZ2b9v/v/c= lmika.dev/pkg/modash v0.1.0/go.mod h1:8NDl/yR1eCCEhip9FJlVuMNXIeaztQ0Ks/tizExFcTI= +lmika.dev/pkg/progdoc v0.0.0-20260207232002-20d5d26e2049 h1:QRtV2JQ7wCa+YzKDnYyy9VcsN31CRknyh+wNu18mFEA= +lmika.dev/pkg/progdoc v0.0.0-20260207232002-20d5d26e2049/go.mod h1:GefQ+wxW6L0p2TfB4fzoP7ihOgEt5Lb/1qgXhruVHos= +lmika.dev/pkg/progdoc v0.0.0-20260207232417-30f42db6a6cf h1:U4G6uRgOtQvfCsF/4UDEZHIYJo5ueHc4hcLFVW64IhI= +lmika.dev/pkg/progdoc v0.0.0-20260207232417-30f42db6a6cf/go.mod h1:GefQ+wxW6L0p2TfB4fzoP7ihOgEt5Lb/1qgXhruVHos= +lmika.dev/pkg/progdoc v0.0.0-20260207233637-1d52b0784c9c h1:SqyLDQRCBsN+z+sFqxJh6ysUEjG1Yox54fEoQzEpZ80= +lmika.dev/pkg/progdoc v0.0.0-20260207233637-1d52b0784c9c/go.mod h1:GefQ+wxW6L0p2TfB4fzoP7ihOgEt5Lb/1qgXhruVHos= +lmika.dev/pkg/progdoc v0.0.0-20260207234435-b84425251a34 h1:O95sVmffXYmRA7DehyxysWZqagHn9pFAhfF77tFtvMg= +lmika.dev/pkg/progdoc v0.0.0-20260207234435-b84425251a34/go.mod h1:GefQ+wxW6L0p2TfB4fzoP7ihOgEt5Lb/1qgXhruVHos= +lmika.dev/pkg/progdoc v0.0.0-20260207235039-984257fb414b h1:uarBEkpjAnpzO98btsA6ZDw9oGnJX8zSb8gZnCFl+6Y= +lmika.dev/pkg/progdoc v0.0.0-20260207235039-984257fb414b/go.mod h1:GefQ+wxW6L0p2TfB4fzoP7ihOgEt5Lb/1qgXhruVHos= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/ucl/builtins/csv.go b/ucl/builtins/csv.go index be54667..c13bab9 100644 --- a/ucl/builtins/csv.go +++ b/ucl/builtins/csv.go @@ -7,6 +7,7 @@ import ( "io" "io/fs" "strings" + "ucl.lmika.dev/ucl" ) @@ -14,6 +15,10 @@ type csvHandlers struct { fs fs.FS } +/// :module csv +// +// Performs operations over CSV files. + func CSV(fs fs.FS) ucl.Module { fsh := csvHandlers{fs: fs} @@ -25,6 +30,22 @@ func CSV(fs fs.FS) ucl.Module { } } +/// :fn each-record +// +// :syntax each-record FILENAME BLOCK +// +// Invokes the given block for each record in the CSV file. +// +// The block is invoked for every row of the parsed CSV file, excluding the first which is assumed to be the header +// row. The block receives two arguments: the parsed row of the column-separated values, as a list of strings; +// and a header which is a dictionary from header names to column indices. +// +// Example: +// +// csv:each-record "data.csv" { |row header| +// echo $row.($header.("My Header Name")) +// } + func (h csvHandlers) eachRecord(ctx context.Context, args ucl.CallArgs) (any, error) { var ( filename string diff --git a/ucl/builtins/fns.go b/ucl/builtins/fns.go index 4cc31b0..5bbfe82 100644 --- a/ucl/builtins/fns.go +++ b/ucl/builtins/fns.go @@ -7,12 +7,16 @@ import ( "ucl.lmika.dev/ucl" ) +/// :module fns +// +// Performs operations over functions, usually returning new functions as a result. + func Fns() ucl.Module { return ucl.Module{ Name: "fns", Builtins: map[string]ucl.BuiltinHandler{ "ident": fnsIdent, - "uni": fnsUni, + "uni": fnsUni, }, } } @@ -38,6 +42,22 @@ func fnsIdent(ctx context.Context, args ucl.CallArgs) (any, error) { }), nil } +/// :fn uni +// +// :syntax fns:uni FUNC ARGS... +// +// Returns a function with a single arg which invokes FUNC with the first argument bound to the argument of the returned +// function, and all arguments bound to the remaining args to `fns:uni`. The result of `fns:uni` is therefore a single +// argument function that will effectively pass the first argument to FUNC along with the remaining arguments to `fns:uni`. +// +// The goal of this is to make functions that can be easily invokable within pipelines or higher-order functions. +// +// Example: +// +// s = fns:uni add 2 +// map [1 2 3] $s +// --> [3 4 5] + func fnsUni(ctx context.Context, args ucl.CallArgs) (any, error) { var inv ucl.Invokable diff --git a/ucl/builtins/fns_test.go b/ucl/builtins/fns_test.go index a7fe418..060575e 100644 --- a/ucl/builtins/fns_test.go +++ b/ucl/builtins/fns_test.go @@ -2,8 +2,9 @@ package builtins_test import ( "context" - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" "ucl.lmika.dev/ucl" "ucl.lmika.dev/ucl/builtins" ) @@ -16,6 +17,7 @@ func TestFns_Uni(t *testing.T) { }{ {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}, + {descr: "uni 3", eval: `s = fns:uni add 2 ; map [1 2 3] $s`, want: []any{3, 4, 5}}, } for _, tt := range tests { From 33cf23b221b10bdc3f021becb60096c1859d8d13 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sun, 15 Feb 2026 16:03:45 +1100 Subject: [PATCH 21/22] Fixed some bugs discovered by Claude Code --- CLAUDE.md | 57 +++++++++++++++++++++++++++++++++++++++++++ repl/evaldisplay.go | 9 +++++-- ucl/ast.go | 2 +- ucl/builtins.go | 14 ++++++++--- ucl/builtins/lists.go | 1 + ucl/builtins/log.go | 6 +++-- ucl/env.go | 4 +-- ucl/eval.go | 2 +- ucl/objs.go | 11 --------- ucl/userbuiltin.go | 1 - 10 files changed, 84 insertions(+), 23 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..92950bf --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,57 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +UCL is an embeddable command-based scripting language implemented in Go, with Tcl/shell-like syntax. It is a **library** — there is no root `main.go`. All executables live under `cmd/`. + +## Build & Test Commands + +- **Run all tests:** `make test` (or `go test ./ucl/...`) +- **Run a single test:** `go test ./ucl/... -run TestName` +- **Run builtin module tests:** `go test ./ucl/builtins/...` +- **Build site + WASM playground:** `make site` +- **Clean build artifacts:** `make clean` + +## Architecture + +### Core Package (`ucl/`) + +The language engine follows a **parse → tree-walk evaluate** pipeline: + +- **`ast.go`** — AST node types and parser (using `participle` library with struct-tag grammar) +- **`eval.go`** — Tree-walk evaluator: scripts, statements, commands, pipelines, arguments, dot-access, string interpolation +- **`objs.go`** — Object type system: `StringObject`, `IntObject`, `BoolObject`, `ListObject`, `HashObject`, `TimeObject`, plus proxy types for Go interop via reflection +- **`inst.go`** — `Inst` (interpreter instance): the public API for creating and configuring the evaluator +- **`builtins.go`** — Built-in functions and macros (control flow, math, collections) +- **`env.go`** — `evalCtx`: scoped evaluation context (linked-list chain for variable/command lookup) +- **`userbuiltin.go`** — Public embedding API (`BuiltinHandler`, `CallArgs` with reflection-based argument binding) + +### Two Command Dispatch Kinds + +- **Invokables** (`invokable` interface): receive fully-evaluated arguments. Used by all regular functions, Go builtins, user `proc`s, and blocks. +- **Macros** (`macroable` interface): receive unevaluated AST + evaluator. Used by `if`, `for`, `while`, `proc`, `try` for lazy evaluation control. + +### Optional Builtin Modules (`ucl/builtins/`) + +Modules (`strs`, `lists`, `itrs`, `fns`, `os`, `fs`, `log`, `csv`, `time`, `urls`) are opt-in via `ucl.WithModule(builtins.Strs())` etc. Each is a self-contained file. + +### Other Packages + +- **`repl/`** — REPL wrapper with `EvalAndDisplay()`, help system, and `AddTypePrinter[T]` generic +- **`cmd/cmsh/`** — Interactive shell using `readline` +- **`cmd/playwasm/`** — WASM playground (builds with `GOOS=js GOARCH=wasm`) +- **`cmd/gendocs/`** — Markdown-to-HTML doc site generator (goldmark + frontmatter) + +## Testing Conventions + +- Tests use `testify/assert` with **table-driven** patterns (`[]struct{ desc, expr string; want any }`) +- Test packages use the `_test` suffix for black-box testing +- Shared test helpers (e.g., `WithTestBuiltin()`) are in `ucl/builtins_test.go` and provide test-only commands (`firstarg`, `toUpper`, `sjoin`, etc.) + +## CI + +Forgejo workflows in `.forgejo/workflows/`: +- `test.yaml` — runs `make test` on `feature/*` branches +- `build.yaml` — runs tests + `make site-deploy` on `main` diff --git a/repl/evaldisplay.go b/repl/evaldisplay.go index 083e4e3..4fc841d 100644 --- a/repl/evaldisplay.go +++ b/repl/evaldisplay.go @@ -8,6 +8,7 @@ import ( "os" "reflect" "strings" + "ucl.lmika.dev/ucl" ) @@ -44,7 +45,7 @@ func (r *REPL) echoPrinter(ctx context.Context, w io.Writer, args []any) (err er } _, err = fmt.Fprintln(w, res) - return nil + return err } func (r *REPL) displayResult(ctx context.Context, w io.Writer, res any, concise bool) (err error) { @@ -56,7 +57,7 @@ func (r *REPL) displayResult(ctx context.Context, w io.Writer, res any, concise switch v := res.(type) { case nil: - if _, err = fmt.Fprintln(os.Stdout, "(nil)"); err != nil { + if _, err = fmt.Fprintln(w, "(nil)"); err != nil { return err } case ucl.Listable: @@ -92,6 +93,10 @@ func (r *REPL) displayResult(ctx context.Context, w io.Writer, res any, concise } fmt.Fprintf(w, "]") } else { + if v == nil { + fmt.Fprintf(w, "(nil)") + return nil + } // In the off-chance that this is actually a slice of printables vt := reflect.SliceOf(reflect.TypeOf(v[0])) if tp, ok := r.typePrinters[vt]; ok { diff --git a/ucl/ast.go b/ucl/ast.go index 719f381..e27ec01 100644 --- a/ucl/ast.go +++ b/ucl/ast.go @@ -174,5 +174,5 @@ var parser = participle.MustBuild[astScript](participle.Lexer(scanner), participle.Elide("Whitespace", "Comment")) func parse(fname string, r io.Reader) (*astScript, error) { - return parser.Parse("test", r) + return parser.Parse(fname, r) } diff --git a/ucl/builtins.go b/ucl/builtins.go index 59edecd..c984394 100644 --- a/ucl/builtins.go +++ b/ucl/builtins.go @@ -455,7 +455,6 @@ func callBuiltin(ctx context.Context, args invocationArgs) (Object, error) { for i := 0; i < argList.Len(); i++ { calledArgs[i] = argList.Index(i) } - args.shift(1) } invArgs := args.fork(calledArgs) @@ -932,7 +931,7 @@ func (s seqObject) Len() int { func (s seqObject) Index(i int) Object { l := s.Len() - if i < 0 || i > l { + if i < 0 || i >= l { return nil } if s.from > s.to { @@ -1028,6 +1027,9 @@ func tryBuiltin(ctx context.Context, args macroArgs) (res Object, err error) { finallyBlocks = append(finallyBlocks, i+1) } } + if len(finallyBlocks) > 1 { + return nil, tooManyFinallyBlocksError(args.ast.Pos) + } defer func() { if isBreakErr(err) { @@ -1122,7 +1124,13 @@ func foreachBuiltin(ctx context.Context, args macroArgs) (Object, error) { case Hashable: err := t.Each(func(k string, v Object) error { last, err = args.evalBlock(ctx, blockIdx, []Object{StringObject(k), v}, false) - return err + if err != nil { + if errors.As(err, &breakErr) && breakErr.isCont { + return nil + } + return err + } + return nil }) if errors.As(err, &breakErr) { if !breakErr.isCont { diff --git a/ucl/builtins/lists.go b/ucl/builtins/lists.go index e62ed77..0aa8264 100644 --- a/ucl/builtins/lists.go +++ b/ucl/builtins/lists.go @@ -116,6 +116,7 @@ func eachListOrIterItem(ctx context.Context, o ucl.Object, f func(int, ucl.Objec } idx++ } + return nil } return errors.New("expected listable") } diff --git a/ucl/builtins/log.go b/ucl/builtins/log.go index c336833..0f2458f 100644 --- a/ucl/builtins/log.go +++ b/ucl/builtins/log.go @@ -4,6 +4,7 @@ import ( "context" "log" "strings" + "ucl.lmika.dev/ucl" ) @@ -36,9 +37,10 @@ func (lh logHandler) logMessage(ctx context.Context, args ucl.CallArgs) (any, er var s ucl.Object for args.NArgs() > 0 { - if err := args.Bind(&s); err == nil { - sb.WriteString(s.String()) + if err := args.Bind(&s); err != nil { + return nil, err } + sb.WriteString(s.String()) } return nil, lh.handler(sb.String()) } diff --git a/ucl/env.go b/ucl/env.go index 7a58cc1..307a372 100644 --- a/ucl/env.go +++ b/ucl/env.go @@ -113,7 +113,7 @@ func (ec *evalCtx) lookupInvokable(name string) invokable { } } - return ec.parent.lookupInvokable(name) + return nil } func (ec *evalCtx) lookupMacro(name string) macroable { @@ -127,5 +127,5 @@ func (ec *evalCtx) lookupMacro(name string) macroable { } } - return ec.parent.lookupMacro(name) + return nil } diff --git a/ucl/eval.go b/ucl/eval.go index c6469ae..3d8c14e 100644 --- a/ucl/eval.go +++ b/ucl/eval.go @@ -326,7 +326,7 @@ func (e evaluator) assignArg(ctx context.Context, ec *evalCtx, n astCmdArg, toVa } return toVal, nil } - return nil, errors.New("unknown pseudo-variable: " + *n.Var) + return nil, fmt.Errorf("unknown pseudo-variable: %v", n.PseudoVar) case n.MaybeSub != nil: return nil, errors.New("cannot assign to a subexpression") case n.ListOrHash != nil: diff --git a/ucl/objs.go b/ucl/objs.go index fedb25f..70f7cd8 100644 --- a/ucl/objs.go +++ b/ucl/objs.go @@ -444,17 +444,6 @@ func (ia invocationArgs) expectArgn(x int) error { return nil } -func (ia invocationArgs) stringArg(i int) (string, error) { - if len(ia.args) < i { - return "", errors.New("expected at least " + strconv.Itoa(i) + " args") - } - s, ok := ia.args[i].(fmt.Stringer) - if !ok { - return "", errors.New("expected a string arg") - } - return s.String(), nil -} - func (ia invocationArgs) intArg(i int) (int, error) { if len(ia.args) < i { return 0, errors.New("expected at least " + strconv.Itoa(i) + " args") diff --git a/ucl/userbuiltin.go b/ucl/userbuiltin.go index 87046f3..1222d17 100644 --- a/ucl/userbuiltin.go +++ b/ucl/userbuiltin.go @@ -239,7 +239,6 @@ func canBindProxyObject(v interface{}, r reflect.Value) bool { for { if r.Type().AssignableTo(argValue.Elem().Type()) { - argValue.Elem().Set(r) return true } if r.Type().Kind() != reflect.Pointer { From 23f730fb2fbf1e1473a8f8fe3b0b121a1e9feae5 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sun, 15 Mar 2026 10:06:00 +1100 Subject: [PATCH 22/22] Added single dots --- .beads/issues.jsonl | 0 ucl/ast.go | 20 +++-- ucl/builtins/time.go | 35 ++++++++ ucl/builtins/time_test.go | 54 +++++++++++++ ucl/eval.go | 164 ++++++++++++++++++++++++++++++++------ ucl/inst_test.go | 57 +++++++++++++ ucl/objs.go | 81 +++++++++++++++++-- 7 files changed, 376 insertions(+), 35 deletions(-) create mode 100644 .beads/issues.jsonl diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl new file mode 100644 index 0000000..e69de29 diff --git a/ucl/ast.go b/ucl/ast.go index e27ec01..d794ef8 100644 --- a/ucl/ast.go +++ b/ucl/ast.go @@ -63,8 +63,8 @@ func (ai *astIdentNames) String() string { } type astElementPair struct { - Left astDot `parser:"@@"` - Right *astDot `parser:"( COLON @@ )? NL?"` + Left astArgOrJustDot `parser:"@@"` + Right *astArgOrJustDot `parser:"( COLON @@ )? NL?"` } type astListOrHash struct { @@ -97,16 +97,26 @@ type astDotSuffix struct { Pipeline *astPipeline `parser:"| LP @@ RP"` } -type astDot struct { +type astArgOrJustDot struct { + Arg *astArgDotSuffix `parser:"@@"` + Dot *astJustDotSuffix `parser:"| @@"` +} + +type astArgDotSuffix struct { Pos lexer.Position Arg astCmdArg `parser:"@@"` DotSuffix []astDotSuffix `parser:"( DOT @@ )*"` } +type astJustDotSuffix struct { + Pos lexer.Position + DotSuffix []astDotSuffix `parser:" DOT ( @@ ( DOT @@ )* )?"` +} + type astCmd struct { Pos lexer.Position - Name astDot `parser:"@@"` - InvokeArgs []astDot `parser:"@@*"` + Name astArgOrJustDot `parser:"@@"` + InvokeArgs []astArgOrJustDot `parser:"@@*"` } type astPipeline struct { diff --git a/ucl/builtins/time.go b/ucl/builtins/time.go index fcc804d..583c270 100644 --- a/ucl/builtins/time.go +++ b/ucl/builtins/time.go @@ -2,7 +2,9 @@ package builtins import ( "context" + "errors" "time" + "ucl.lmika.dev/ucl" ) @@ -11,6 +13,8 @@ func Time() ucl.Module { Name: "time", Builtins: map[string]ucl.BuiltinHandler{ "from-unix": timeFromUnix, + "to-unix": timeToUnix, + "now": timeNow, "sleep": timeSleep, }, } @@ -26,6 +30,23 @@ func timeFromUnix(ctx context.Context, args ucl.CallArgs) (any, error) { return time.Unix(int64(ux), 0).UTC(), nil } +func timeToUnix(ctx context.Context, args ucl.CallArgs) (any, error) { + tval, err := getTimeArg(&args) + if err != nil { + return nil, err + } + + return int(tval.Unix()), nil +} + +func timeNow(ctx context.Context, args ucl.CallArgs) (any, error) { + if err := args.Bind(); err != nil { + return nil, err + } + + return time.Now().UTC(), nil +} + func timeSleep(ctx context.Context, args ucl.CallArgs) (any, error) { var secs int @@ -40,3 +61,17 @@ func timeSleep(ctx context.Context, args ucl.CallArgs) (any, error) { return nil, ctx.Err() } } + +func getTimeArg(args *ucl.CallArgs) (time.Time, error) { + var t any + if err := args.Bind(&t); err != nil { + return time.Time{}, err + } + + tval, ok := t.(time.Time) + if !ok { + return time.Time{}, errors.New("expected time.Time") + } + + return tval, nil +} diff --git a/ucl/builtins/time_test.go b/ucl/builtins/time_test.go index a0087fa..72921c6 100644 --- a/ucl/builtins/time_test.go +++ b/ucl/builtins/time_test.go @@ -36,6 +36,60 @@ func TestTime_FromUnix(t *testing.T) { } } +func TestTime_Now(t *testing.T) { + tests := []struct { + desc string + eval string + wantErr bool + }{ + {desc: "now returns time", eval: `time:now`, wantErr: false}, + {desc: "now with to-unix and from-unix roundtrip", eval: `time:to-unix (time:now) | time:from-unix`, wantErr: false}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + inst := ucl.New( + ucl.WithModule(builtins.Time()), + ) + res, err := inst.EvalString(context.Background(), tt.eval) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.NotNil(t, res) + } + }) + } +} + +func TestTime_ToUnix(t *testing.T) { + tests := []struct { + desc string + eval string + want any + wantErr bool + }{ + {desc: "to-unix from epoch", eval: `time:to-unix (time:from-unix 0)`, want: 0}, + {desc: "to-unix from specific time", eval: `time:to-unix (time:from-unix 1234567890)`, want: 1234567890}, + {desc: "roundtrip", eval: `time:to-unix (time:from-unix 999999999)`, want: 999999999}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + inst := ucl.New( + ucl.WithModule(builtins.Time()), + ) + 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 TestTime_Sleep(t *testing.T) { t.Run("should terminate on cancelled context", func(t *testing.T) { st := time.Now() diff --git a/ucl/eval.go b/ucl/eval.go index 3d8c14e..fbb35f8 100644 --- a/ucl/eval.go +++ b/ucl/eval.go @@ -103,10 +103,7 @@ func (e evaluator) evalPipeline(ctx context.Context, ec *evalCtx, n *astPipeline } func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, currentPipe Object, ast *astCmd) (Object, error) { - switch { - case (ast.Name.Arg.Ident != nil) && len(ast.Name.DotSuffix) == 0: - name := ast.Name.Arg.Ident.String() - + if name, ok := e.isArgOrDotAnIdent(ast.Name); ok { // Regular command if cmd := ec.lookupInvokable(name); cmd != nil { return e.evalInvokable(ctx, ec, currentPipe, ast, cmd) @@ -117,8 +114,8 @@ func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, currentPipe Object, } else { return nil, errors.New("unknown command: " + name) } - case len(ast.InvokeArgs) > 0: - nameElem, err := e.evalDot(ctx, ec, ast.Name) + } else if len(ast.InvokeArgs) > 0 { + nameElem, err := e.evalArgOrDot(ctx, ec, ast.Name) if err != nil { return nil, err } @@ -131,7 +128,7 @@ func (e evaluator) evalCmd(ctx context.Context, ec *evalCtx, currentPipe Object, return e.evalInvokable(ctx, ec, currentPipe, ast, inv) } - nameElem, err := e.evalDot(ctx, ec, ast.Name) + nameElem, err := e.evalArgOrDot(ctx, ec, ast.Name) if err != nil { return nil, err } @@ -142,7 +139,25 @@ func (e evaluator) assignCmd(ctx context.Context, ec *evalCtx, ast *astCmd, toVa if len(ast.InvokeArgs) != 0 { return nil, errors.New("cannot assign to multiple values") } - return e.assignDot(ctx, ec, ast.Name, toVal) + return e.assignArgOrDot(ctx, ec, ast.Name, toVal) +} + +func (e evaluator) isArgOrDotAnIdent(argOrDot astArgOrJustDot) (string, bool) { + nonDotArg := argOrDot.Arg + if nonDotArg == nil { + return "", false + } + + if len(nonDotArg.DotSuffix) != 0 { + return "", false + } + + ident := nonDotArg.Arg.Ident + if ident == nil { + return "", false + } + + return ident.String(), true } func (e evaluator) evalInvokable(ctx context.Context, ec *evalCtx, currentPipe Object, ast *astCmd, cmd invokable) (Object, error) { @@ -157,16 +172,16 @@ func (e evaluator) evalInvokable(ctx context.Context, ec *evalCtx, currentPipe O argsPtr.Append(currentPipe) } for _, arg := range ast.InvokeArgs { - if ident := arg.Arg.Ident; len(arg.DotSuffix) == 0 && ident != nil && ident.String()[0] == '-' { + if ident, ok := e.isArgOrDotAnIdent(arg); ok && ident[0] == '-' { // Arg switch if kwargs == nil { kwargs = make(map[string]*ListObject) } argsPtr = &ListObject{} - kwargs[ident.String()[1:]] = argsPtr + kwargs[ident[1:]] = argsPtr } else { - ae, err := e.evalDot(ctx, ec, arg) + ae, err := e.evalArgOrDot(ctx, ec, arg) if err != nil { return nil, err } @@ -188,7 +203,55 @@ func (e evaluator) evalMacro(ctx context.Context, ec *evalCtx, hasPipe bool, pip }) } -func (e evaluator) evalDot(ctx context.Context, ec *evalCtx, n astDot) (Object, error) { +func (e evaluator) evalArgOrDot(ctx context.Context, ec *evalCtx, n astArgOrJustDot) (Object, error) { + if n.Arg != nil { + return e.evalArgWithDot(ctx, ec, *n.Arg) + } else if n.Dot != nil { + return e.evalJustDot(ctx, ec, *n.Dot) + } + return nil, errors.New("unhandled arg or dot type") +} + +func (e evaluator) evalPseudoVar(ctx context.Context, ec *evalCtx, name string) (Object, error) { + if v, ok := ec.getPseudoVar(name); ok { + return v.get(ctx, name) + } + + if mph := e.inst.missingPseudoVarHandler; mph != nil { + return mph.get(ctx, name) + } + + return nil, fmt.Errorf("unknown pseudo-variable: '%v'", name) +} + +func (e evaluator) evalJustDot(ctx context.Context, ec *evalCtx, n astJustDotSuffix) (Object, error) { + res, err := e.evalPseudoVar(ctx, ec, ".") + if err != nil { + return nil, err + } else if len(n.DotSuffix) == 0 { + return res, nil + } + + for _, dot := range n.DotSuffix { + var idx Object + if dot.KeyIdent != nil { + idx = StringObject(dot.KeyIdent.String()) + } else { + idx, err = e.evalPipeline(ctx, ec, dot.Pipeline) + if err != nil { + return nil, err + } + } + + res, err = indexLookup(ctx, res, idx, n.Pos) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (e evaluator) evalArgWithDot(ctx context.Context, ec *evalCtx, n astArgDotSuffix) (Object, error) { res, err := e.evalArg(ctx, ec, n.Arg) if err != nil { return nil, err @@ -215,7 +278,16 @@ func (e evaluator) evalDot(ctx context.Context, ec *evalCtx, n astDot) (Object, return res, nil } -func (e evaluator) assignDot(ctx context.Context, ec *evalCtx, n astDot, toVal Object) (Object, error) { +func (e evaluator) assignArgOrDot(ctx context.Context, ec *evalCtx, n astArgOrJustDot, toVal Object) (Object, error) { + if n.Arg != nil { + return e.assignArgWithDot(ctx, ec, *n.Arg, toVal) + } else if n.Dot != nil { + return e.assignJustDot(ctx, ec, *n.Dot, toVal) + } + return nil, errors.New("unhandled arg or dot type") +} + +func (e evaluator) assignArgWithDot(ctx context.Context, ec *evalCtx, n astArgDotSuffix, toVal Object) (Object, error) { if len(n.DotSuffix) == 0 { return e.assignArg(ctx, ec, n.Arg, toVal) } @@ -251,6 +323,56 @@ func (e evaluator) assignDot(ctx context.Context, ec *evalCtx, n astDot, toVal O return val, nil } +func (e evaluator) assignJustDot(ctx context.Context, ec *evalCtx, n astJustDotSuffix, toVal Object) (Object, error) { + if len(n.DotSuffix) == 0 { + pvar, ok := ec.getPseudoVar(".") + if ok { + if err := pvar.set(ctx, ".", toVal); err != nil { + return nil, err + } + return toVal, nil + } + + if pvar := e.inst.missingPseudoVarHandler; pvar != nil { + if err := pvar.set(ctx, ".", toVal); err != nil { + return nil, err + } + return toVal, nil + } + return nil, fmt.Errorf("unknown pseudo-variable: .") + } + + val, err := e.evalPseudoVar(ctx, ec, ".") + if err != nil { + return nil, err + } + + for i, dot := range n.DotSuffix { + isLast := i == len(n.DotSuffix)-1 + + var idx Object + if dot.KeyIdent != nil { + idx = StringObject(dot.KeyIdent.String()) + } else { + idx, err = e.evalPipeline(ctx, ec, dot.Pipeline) + if err != nil { + return nil, err + } + } + + if isLast { + val, err = indexAssign(ctx, val, idx, toVal, n.Pos) + } else { + val, err = indexLookup(ctx, val, idx, n.Pos) + } + if err != nil { + return nil, err + } + } + + return val, nil +} + func (e evaluator) evalArgForDotAssign(ctx context.Context, ec *evalCtx, n astCmdArg) (Object, error) { // Special case for dot assigns of 'a.b = c' where a is actually a var deref (i.e. $a) // which is unnecessary for assignments. Likewise, having '$a.b = c' should be dissallowed @@ -279,15 +401,7 @@ func (e evaluator) evalArg(ctx context.Context, ec *evalCtx, n astCmdArg) (Objec } return nil, nil case n.PseudoVar != nil: - if v, ok := ec.getPseudoVar(*n.PseudoVar); ok { - return v.get(ctx, *n.PseudoVar) - } - - if mph := e.inst.missingPseudoVarHandler; mph != nil { - return mph.get(ctx, *n.PseudoVar) - } - - return nil, errors.New("unknown pseudo-variable: " + *n.PseudoVar) + return e.evalPseudoVar(ctx, ec, *n.PseudoVar) case n.MaybeSub != nil: sub := n.MaybeSub.Sub if sub == nil { @@ -351,12 +465,12 @@ func (e evaluator) evalListOrHash(ctx context.Context, ec *evalCtx, loh *astList return nil, errors.New("miss-match of lists and hash") } - n, err := e.evalDot(ctx, ec, el.Left) + n, err := e.evalArgOrDot(ctx, ec, el.Left) if err != nil { return nil, err } - v, err := e.evalDot(ctx, ec, *el.Right) + v, err := e.evalArgOrDot(ctx, ec, *el.Right) if err != nil { return nil, err } @@ -371,7 +485,7 @@ func (e evaluator) evalListOrHash(ctx context.Context, ec *evalCtx, loh *astList if el.Right != nil { return nil, errors.New("miss-match of lists and hash") } - v, err := e.evalDot(ctx, ec, el.Left) + v, err := e.evalArgOrDot(ctx, ec, el.Left) if err != nil { return nil, err } diff --git a/ucl/inst_test.go b/ucl/inst_test.go index 041c367..9735dcc 100644 --- a/ucl/inst_test.go +++ b/ucl/inst_test.go @@ -307,6 +307,50 @@ func TestInst_SetPseudoVar(t *testing.T) { } } +func TestInst_ParseDot(t *testing.T) { + strPVal := &stringPseudoVar{str: "this is dot"} + slicePVal := &anyPseudoVar{val: []string{"item1", "item2"}} + mapPVal := &anyPseudoVar{val: map[string]string{"key1": "value1", "key2": "value2"}} + + tests := []struct { + desc string + pvalHandler ucl.PseudoVarHandler + expr string + wantRes any + wantBarVar string + wantErr bool + }{ + {desc: "read dot 1", pvalHandler: strPVal, expr: `.`, wantRes: "this is dot"}, + {desc: "read dot 2", pvalHandler: strPVal, expr: `toUpper .`, wantRes: "THIS IS DOT"}, + {desc: "read dot 3", pvalHandler: strPVal, expr: `. | toUpper`, wantRes: "THIS IS DOT"}, + {desc: "read dot 4", pvalHandler: slicePVal, expr: `.(0)`, wantRes: "item1"}, + {desc: "read dot 5", pvalHandler: slicePVal, expr: `.(1)`, wantRes: "item2"}, + {desc: "read dot 6", pvalHandler: slicePVal, expr: `. | len`, wantRes: 2}, + {desc: "read dot 7", pvalHandler: mapPVal, expr: `.key1`, wantRes: "value1"}, + {desc: "read dot 8", pvalHandler: mapPVal, expr: `.key2`, wantRes: "value2"}, + + // Always keep last as this will modify the pvals + {desc: "write dot 1", pvalHandler: strPVal, expr: `. = "hello" ; .`, wantRes: "hello"}, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + inst := ucl.New(ucl.WithTestBuiltin()) + inst.SetPseudoVar(".", tt.pvalHandler) + + res, err := inst.EvalString(t.Context(), tt.expr) + + if tt.wantErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.wantRes, res) + }) + } +} + var parseComments1 = ` proc lookup { |file| foreach { |toks| @@ -359,6 +403,19 @@ func (s *stringPseudoVar) Set(ctx context.Context, v any) error { return nil } +type anyPseudoVar struct { + val any +} + +func (s *anyPseudoVar) Get(ctx context.Context) (any, error) { + return s.val, nil +} + +func (s *anyPseudoVar) Set(ctx context.Context, v any) error { + s.val = v + return nil +} + type missingPseudoVarType struct{} func (missingPseudoVarType) Get(ctx context.Context, name string) (any, error) { diff --git a/ucl/objs.go b/ucl/objs.go index 70f7cd8..d77f13f 100644 --- a/ucl/objs.go +++ b/ucl/objs.go @@ -308,6 +308,11 @@ func fromGoReflectValue(resVal reflect.Value) (Object, error) { switch resVal.Kind() { case reflect.Slice: return listableProxyObject{v: resVal, orig: resVal}, nil + case reflect.Map: + if resVal.Type().Key().Kind() == reflect.String { + return mapProxyObject{v: resVal, orig: resVal}, nil + } + return nil, errors.New("map keys must be strings") case reflect.Struct: return newStructProxyObject(resVal, resVal), nil case reflect.Pointer: @@ -346,11 +351,16 @@ func (ma macroArgs) identIs(ctx context.Context, n int, expectedIdent string) bo return false } - if len(ma.ast.InvokeArgs[ma.argShift+n].DotSuffix) != 0 { + arg := ma.ast.InvokeArgs[ma.argShift+n].Arg + if arg == nil { return false } - lit := ma.ast.InvokeArgs[ma.argShift+n].Arg.Ident + if len(arg.DotSuffix) != 0 { + return false + } + + lit := arg.Arg.Ident if lit == nil { return false } @@ -363,11 +373,16 @@ func (ma *macroArgs) shiftIdent(ctx context.Context) (string, bool) { return "", false } - if len(ma.ast.InvokeArgs[ma.argShift].DotSuffix) != 0 { + arg := ma.ast.InvokeArgs[ma.argShift].Arg + if arg == nil { return "", false } - lit := ma.ast.InvokeArgs[ma.argShift].Arg.Ident + if len(arg.DotSuffix) != 0 { + return "", false + } + + lit := arg.Arg.Ident if lit != nil { ma.argShift += 1 return lit.String(), true @@ -380,7 +395,7 @@ func (ma macroArgs) evalArg(ctx context.Context, n int) (Object, error) { return nil, errors.New("not enough arguments") // FIX } - return ma.eval.evalDot(ctx, ma.ec, ma.ast.InvokeArgs[ma.argShift+n]) + return ma.eval.evalArgOrDot(ctx, ma.ec, ma.ast.InvokeArgs[ma.argShift+n]) } func (ma macroArgs) evalBlock(ctx context.Context, n int, args []Object, pushScope bool) (Object, error) { @@ -670,6 +685,62 @@ func (s structProxyObject) Each(fn func(k string, v Object) error) error { return nil } +type mapProxyObject struct { + v reflect.Value + orig reflect.Value +} + +func newMapProxyObject(v reflect.Value, orig reflect.Value) structProxyObject { + return structProxyObject{ + v: v, + orig: orig, + } +} + +func (s mapProxyObject) String() string { + return fmt.Sprintf("mapProxyObject{%v}", s.v.Type()) +} + +func (p mapProxyObject) Truthy() bool { + return p.v.Len() > 0 +} + +func (p mapProxyObject) Len() int { + return p.v.Len() +} + +func (p mapProxyObject) Value(k string) Object { + val := p.v.MapIndex(reflect.ValueOf(k)) + if !val.IsValid() || val.IsZero() { + return nil + } + + e, err := fromGoValue(val.Interface()) + if err != nil { + return nil + } + return e +} + +func (s mapProxyObject) Each(fn func(k string, v Object) error) error { + for _, k := range s.v.MapKeys() { + val := k.MapIndex(reflect.ValueOf(k)) + if !val.IsValid() || val.IsZero() { + continue + } + + v, err := fromGoValue(val.Interface()) + if err != nil { + v = nil + } + + if err := fn(k.String(), v); err != nil { + return err + } + } + return nil +} + type OpaqueObject struct { v any }