From de29bd21c03565f43b4fdccb5ca6f9d1883e8be7 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Tue, 29 Oct 2024 18:22:01 +0000 Subject: [PATCH] Add interactive mode, fixes and improvements --- execution/execute.go | 27 +-- execution/execute_binary.go | 9 +- execution/execute_branch.go | 1 + execution/execute_branch_test.go | 20 ++ execution/execute_map.go | 1 - execution/execute_map_test.go | 13 +- execution/execute_test.go | 2 +- go.mod | 23 +++ go.sum | 47 +++++ internal/cli/command.go | 6 +- internal/cli/interactive.go | 316 +++++++++++++++++++++++++++++++ internal/cli/query.go | 99 +++------- internal/cli/read_write_flag.go | 59 ++++++ internal/cli/run.go | 99 ++++++++++ internal/cli/variable.go | 15 +- model/value.go | 61 ++++++ model/value_slice.go | 8 + parsing/d/reader.go | 2 +- parsing/format.go | 4 +- parsing/json/json.go | 2 +- parsing/json/json_test.go | 2 +- parsing/reader.go | 13 +- parsing/toml/toml.go | 2 +- parsing/writer.go | 2 + parsing/xml/xml.go | 84 +++++++- parsing/xml/xml_test.go | 4 +- parsing/yaml/yaml.go | 2 +- parsing/yaml/yaml_test.go | 4 +- selector/parser/parse_array.go | 11 +- selector/parser/parse_object.go | 11 +- 30 files changed, 817 insertions(+), 132 deletions(-) create mode 100644 internal/cli/interactive.go create mode 100644 internal/cli/read_write_flag.go create mode 100644 internal/cli/run.go diff --git a/execution/execute.go b/execution/execute.go index 5b763fb..bf3ef77 100644 --- a/execution/execute.go +++ b/execution/execute.go @@ -135,30 +135,9 @@ func exprExecutor(opts *Options, expr ast.Expr) (expressionExecutor, error) { func chainedExprExecutor(options *Options, e ast.ChainedExpr) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { for _, expr := range e.Exprs { - - if !data.IsBranch() { - res, err := ExecuteAST(expr, data, options) - if err != nil { - return nil, fmt.Errorf("error executing expression: %w", err) - } - data = res - continue - } - - res := model.NewSliceValue() - res.MarkAsBranch() - if err := data.RangeSlice(func(i int, value *model.Value) error { - r, err := ExecuteAST(expr, value, options) - if err != nil { - return fmt.Errorf("error executing expression: %w", err) - } - - if err := res.Append(r); err != nil { - return err - } - return nil - }); err != nil { - return nil, err + res, err := ExecuteAST(expr, data, options) + if err != nil { + return nil, fmt.Errorf("error executing expression: %w", err) } data = res } diff --git a/execution/execute_binary.go b/execution/execute_binary.go index c8e466d..1ea37c4 100644 --- a/execution/execute_binary.go +++ b/execution/execute_binary.go @@ -23,7 +23,11 @@ func basicBinaryExpressionExecutorFn(handler func(left *model.Value, right *mode if err != nil { return nil, fmt.Errorf("error evaluating right expression: %w", err) } - return handler(left, right, expr) + res, err := handler(left, right, expr) + if err != nil { + return nil, err + } + return res, nil } res := model.NewSliceValue() @@ -52,6 +56,9 @@ var binaryExpressionExecutors = map[lexer.TokenKind]binaryExpressionExecutorFn{} func binaryExprExecutor(opts *Options, e ast.BinaryExpr) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { + if e.Left == nil || e.Right == nil { + return nil, fmt.Errorf("left and right expressions must be provided") + } exec, ok := binaryExpressionExecutors[e.Operator.Kind] if !ok { diff --git a/execution/execute_branch.go b/execution/execute_branch.go index 95dad60..95d5a06 100644 --- a/execution/execute_branch.go +++ b/execution/execute_branch.go @@ -13,6 +13,7 @@ func branchExprExecutor(opts *Options, e ast.BranchExpr) (expressionExecutor, er res.MarkAsBranch() if len(e.Exprs) == 0 { + // No expressions given. We'll branch on the input data. if err := data.RangeSlice(func(_ int, value *model.Value) error { if err := res.Append(value); err != nil { return fmt.Errorf("failed to append branch result: %w", err) diff --git a/execution/execute_branch_test.go b/execution/execute_branch_test.go index 5b09f06..eb70160 100644 --- a/execution/execute_branch_test.go +++ b/execution/execute_branch_test.go @@ -125,4 +125,24 @@ func TestBranch(t *testing.T) { execution.WithUnstable(), }, }.run) + t.Run("map on branch", testCase{ + s: `branch([1], [2], [3]).map($this * 2).branch()`, + outFn: func() *model.Value { + r := model.NewSliceValue() + r.MarkAsBranch() + if err := r.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := r.Append(model.NewIntValue(4)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := r.Append(model.NewIntValue(6)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + return r + }, + opts: []execution.ExecuteOptionFn{ + execution.WithUnstable(), + }, + }.run) } diff --git a/execution/execute_map.go b/execution/execute_map.go index d5efcd8..6059063 100644 --- a/execution/execute_map.go +++ b/execution/execute_map.go @@ -26,7 +26,6 @@ func mapExprExecutor(opts *Options, e ast.MapExpr) (expressionExecutor, error) { }); err != nil { return nil, fmt.Errorf("error ranging over slice: %w", err) } - return res, nil }, nil } diff --git a/execution/execute_map_test.go b/execution/execute_map_test.go index 58fd318..c03fc97 100644 --- a/execution/execute_map_test.go +++ b/execution/execute_map_test.go @@ -3,7 +3,6 @@ package execution_test import ( "testing" - "github.com/tomwright/dasel/v3/internal/ptr" "github.com/tomwright/dasel/v3/model" "github.com/tomwright/dasel/v3/model/orderedmap" ) @@ -38,7 +37,17 @@ func TestMap(t *testing.T) { ) .map ( total )`, outFn: func() *model.Value { - return model.NewValue([]any{ptr.To(int64(6)), ptr.To(int64(8)), ptr.To(int64(10))}) + res := model.NewSliceValue() + if err := res.Append(model.NewValue(6)); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewValue(8)); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewValue(10)); err != nil { + t.Fatal(err) + } + return res }, }.run) } diff --git a/execution/execute_test.go b/execution/execute_test.go index 7711698..62189ed 100644 --- a/execution/execute_test.go +++ b/execution/execute_test.go @@ -45,7 +45,7 @@ func (tc testCase) run(t *testing.T) { t.Fatal(err) } if !equal { - t.Errorf("unexpected output: %v", cmp.Diff(exp.Interface(), res.Interface())) + t.Errorf("unexpected output: %v\nexp: %s\ngot: %s", cmp.Diff(exp.Interface(), res.Interface()), exp.String(), res.String()) } expMeta := exp.Metadata diff --git a/go.mod b/go.mod index 9fe5a7c..437f883 100644 --- a/go.mod +++ b/go.mod @@ -8,3 +8,26 @@ require ( github.com/pelletier/go-toml/v2 v2.2.2 gopkg.in/yaml.v3 v3.0.1 ) + +require ( + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/bubbles v0.20.0 // indirect + github.com/charmbracelet/bubbletea v1.1.2 // indirect + github.com/charmbracelet/lipgloss v0.13.1 // indirect + github.com/charmbracelet/x/ansi v0.4.0 // indirect + github.com/charmbracelet/x/term v0.2.0 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.3.8 // indirect +) diff --git a/go.sum b/go.sum index fbd7fce..b4d4975 100644 --- a/go.sum +++ b/go.sum @@ -4,17 +4,56 @@ github.com/alecthomas/kong v1.2.1 h1:E8jH4Tsgv6wCRX2nGrdPyHDUCSG83WH2qE4XLACD33Q github.com/alecthomas/kong v1.2.1/go.mod h1:rKTSFhbdp3Ryefn8x5MOEprnRFQ7nlmMC01GKhehhBM= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v1.1.2 h1:naQXF2laRxyLyil/i7fxdpiz1/k06IKquhm4vBfHsIc= +github.com/charmbracelet/bubbletea v1.1.2/go.mod h1:9HIU/hBV24qKjlehyj8z1r/tR9TYTQEag+cWZnuXo8E= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/lipgloss v0.13.1 h1:Oik/oqDTMVA01GetT4JdEC033dNzWoQHdWnHnQmXE2A= +github.com/charmbracelet/lipgloss v0.13.1/go.mod h1:zaYVJ2xKSKEnTEEbX6uAHabh2d975RJ+0yfkFpRBz5U= +github.com/charmbracelet/x/ansi v0.4.0 h1:NqwHA4B23VwsDn4H3VcNX1W1tOmgnvY1NDx5tOXdnOU= +github.com/charmbracelet/x/ansi v0.4.0/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= +github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= 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/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 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/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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= @@ -24,6 +63,14 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/cli/command.go b/internal/cli/command.go index 7ba2fb1..2b74f12 100644 --- a/internal/cli/command.go +++ b/internal/cli/command.go @@ -17,8 +17,9 @@ type Globals struct { type CLI struct { Globals - Query QueryCmd `cmd:"" default:"withargs" help:"[default] Execute a query"` - Version VersionCmd `cmd:"" help:"Print the version"` + Query QueryCmd `cmd:"" default:"withargs" help:"[default] Execute a query"` + Version VersionCmd `cmd:"" help:"Print the version"` + Interactive InteractiveCmd `cmd:"" help:"Start an interactive session"` } func MustRun(stdin io.Reader, stdout, stderr io.Writer) { @@ -46,6 +47,7 @@ func Run(stdin io.Reader, stdout, stderr io.Writer) (*kong.Context, error) { }, kong.Bind(&cli.Globals), kong.TypeMapper(reflect.TypeFor[*[]variable](), &variableMapper{}), + kong.TypeMapper(reflect.TypeFor[*[]extReadWriteFlag](), &extReadWriteFlagMapper{}), kong.OptionFunc(func(k *kong.Kong) error { k.Stdout = cli.Stdout k.Stderr = cli.Stderr diff --git a/internal/cli/interactive.go b/internal/cli/interactive.go new file mode 100644 index 0000000..a84ef9a --- /dev/null +++ b/internal/cli/interactive.go @@ -0,0 +1,316 @@ +package cli + +import ( + "bytes" + "fmt" + "io" + "strings" + + "github.com/charmbracelet/bubbles/textarea" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/tomwright/dasel/v3/internal" +) + +const ( + useHighPerformanceRenderer = false +) + +var ( + titleStyle = func() lipgloss.Style { + b := lipgloss.HiddenBorder() + return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1) + }() + + resultTitleStyle = func() lipgloss.Style { + b := lipgloss.HiddenBorder() + return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1) + }() + + viewportStyle = func() lipgloss.Style { + b := lipgloss.RoundedBorder() + return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1) + }() + + commandInputStyle = func() lipgloss.Style { + return lipgloss.NewStyle() + }() +) + +func NewInteractiveCmd(queryCmd *QueryCmd) *InteractiveCmd { + return &InteractiveCmd{ + Vars: queryCmd.Vars, + ExtReadFlags: queryCmd.ExtReadFlags, + ExtWriteFlags: queryCmd.ExtWriteFlags, + InFormat: queryCmd.InFormat, + OutFormat: queryCmd.OutFormat, + + Query: queryCmd.Query, + } +} + +type InteractiveCmd struct { + Vars *[]variable `flag:"" name:"var" help:"Variables to pass to the query. E.g. --var foo=\"bar\" --var baz=json:file:./some/file.json"` + ExtReadFlags *[]extReadWriteFlag `flag:"" name:"read-flag" help:"Reader flag to customise parsing. E.g. --read-flag xml-mode=structured"` + ExtWriteFlags *[]extReadWriteFlag `flag:"" name:"write-flag" help:"Writer flag to customise output"` + InFormat string `flag:"" name:"in" short:"i" help:"The format of the input data."` + OutFormat string `flag:"" name:"out" short:"o" help:"The format of the output data."` + + Query string `arg:"" help:"The query to execute." optional:"" default:""` +} + +func (c *InteractiveCmd) Run(ctx *Globals) error { + var err error + var stdInBytes []byte = nil + + if ctx.Stdin != nil { + stdInBytes, err = io.ReadAll(ctx.Stdin) + if err != nil { + return err + } + } + + m := initialModel(c, c.Query, stdInBytes) + + p := tea.NewProgram(m) + + _, err = p.Run() + return err +} + +func initialModel(it *InteractiveCmd, defaultCommand string, stdInBytes []byte) interactiveTeaModel { + ti := textarea.New() + ti.Placeholder = "Enter a query..." + ti.SetValue(defaultCommand) + ti.Focus() + ti.SetHeight(5) + ti.ShowLineNumbers = false + + return interactiveTeaModel{ + it: it, + commandInput: ti, + err: nil, + stdInBytes: stdInBytes, + output: "Loading...", + firstUpdate: true, + } +} + +type interactiveTeaModel struct { + it *InteractiveCmd + err error + originalErr error + commandInput textarea.Model + currentCommand string + output string + outputViewport viewport.Model + originalOutput string + originalOutputViewport viewport.Model + outputReady bool + previousCommand string + stdInBytes []byte + firstUpdate bool +} + +func (m interactiveTeaModel) Init() tea.Cmd { + return textarea.Blink +} + +func (m interactiveTeaModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + var cmd tea.Cmd + + m.previousCommand = m.currentCommand + + m.currentCommand = m.commandInput.Value() + if m.firstUpdate || m.currentCommand != m.previousCommand { + m.firstUpdate = false + // If the command has changed, we need to execute it + + { + out, err := m.execDasel(m.currentCommand, true) + if err != nil { + m.originalErr = err + } else { + m.originalErr = nil + m.originalOutput = out + m.originalOutputViewport.SetContent(m.originalOutput) + } + if m.originalErr != nil { + m.originalOutput = m.originalErr.Error() + m.originalOutputViewport.SetContent(m.originalOutput) + } + } + + out, err := m.execDasel(m.currentCommand, false) + if err != nil { + m.err = err + } else { + m.err = nil + m.output = out + m.outputViewport.SetContent(m.output) + } + if m.err != nil { + m.output = m.err.Error() + m.outputViewport.SetContent(m.output) + } + } + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.Type { + case tea.KeyEsc: + return m, tea.Quit + //if m.commandInput.Focused() { + // m.commandInput.Blur() + //} + case tea.KeyCtrlC: + return m, tea.Quit + default: + if !m.commandInput.Focused() { + cmd = m.commandInput.Focus() + cmds = append(cmds, cmd) + } + } + + case tea.WindowSizeMsg: + headerHeight := lipgloss.Height(m.headerView()) + commandInputHeight := lipgloss.Height(m.commandInputView()) + resultHeaderHeight := lipgloss.Height(m.resultHeaderView()) + verticalMarginHeight := headerHeight + 2 + commandInputHeight + resultHeaderHeight + + viewportHeight := msg.Height - verticalMarginHeight + viewportWidth := (msg.Width / 2) - 4 + + if !m.outputReady { + m.outputReady = true + + m.commandInput.SetWidth(msg.Width) + + m.outputViewport = viewport.New(viewportWidth, viewportHeight) + //m.outputViewport.YPosition = verticalMarginHeight + m.outputViewport.HighPerformanceRendering = useHighPerformanceRenderer + m.outputViewport.SetContent(m.output) + m.commandInput.SetWidth(msg.Width) + if useHighPerformanceRenderer { + m.outputViewport.YPosition = headerHeight + 1 + } + + m.originalOutputViewport = viewport.New(viewportWidth, viewportHeight) + m.originalOutputViewport.YPosition = verticalMarginHeight + m.originalOutputViewport.HighPerformanceRendering = useHighPerformanceRenderer + m.originalOutputViewport.SetContent(m.output) + + if useHighPerformanceRenderer { + m.originalOutputViewport.YPosition = headerHeight + 1 + } + } else { + m.commandInput.SetWidth(msg.Width) + + m.outputViewport.Width = viewportWidth + m.outputViewport.Height = viewportHeight + m.outputViewport.SetContent(m.output) + + m.originalOutputViewport.Width = viewportWidth + m.originalOutputViewport.Height = viewportHeight + m.outputViewport.SetContent(m.originalOutput) + } + + if useHighPerformanceRenderer { + // Render (or re-render) the whole viewport. Necessary both to + // initialize the viewport and when the window is resized. + // + // This is needed for high-performance rendering only. + cmds = append(cmds, viewport.Sync(m.outputViewport)) + cmds = append(cmds, viewport.Sync(m.originalOutputViewport)) + } + + // We handle errors just like any other message + case error: + m.err = msg + return m, nil + } + + m.commandInput, cmd = m.commandInput.Update(msg) + cmds = append(cmds, cmd) + + m.outputViewport, cmd = m.outputViewport.Update(msg) + cmds = append(cmds, cmd) + + m.originalOutputViewport, cmd = m.originalOutputViewport.Update(msg) + cmds = append(cmds, cmd) + + return m, tea.Batch(cmds...) +} + +func (m interactiveTeaModel) execDasel(selector string, root bool) (res string, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic: %v", r) + } + }() + var stdIn *bytes.Reader = nil + if m.stdInBytes != nil { + stdIn = bytes.NewReader(m.stdInBytes) + } else { + stdIn = bytes.NewReader([]byte{}) + } + + o := runOpts{ + Vars: m.it.Vars, + ExtReadFlags: m.it.ExtReadFlags, + ExtWriteFlags: m.it.ExtWriteFlags, + InFormat: m.it.InFormat, + OutFormat: m.it.OutFormat, + ReturnRoot: root, + Unstable: true, + Query: selector, + + Stdin: stdIn, + } + + outBytes, err := run(o) + return string(outBytes), err +} + +func (m interactiveTeaModel) headerView() string { + return titleStyle.Render("Dasel Interactive Mode - " + internal.Version + " - ctrl+c or esc to exit") +} + +func (m interactiveTeaModel) commandInputView() string { + return commandInputStyle.Render(m.commandInput.View()) +} + +func (m interactiveTeaModel) originalHeaderView() string { + return resultTitleStyle.Render("Root") +} + +func (m interactiveTeaModel) resultHeaderView() string { + return resultTitleStyle.Render("Result") +} + +func (m interactiveTeaModel) viewportView() string { + return viewportStyle.Render(m.outputViewport.View()) +} + +func (m interactiveTeaModel) originalViewportView() string { + return viewportStyle.Render(m.originalOutputViewport.View()) +} + +func (m interactiveTeaModel) View() string { + res := []string{ + m.headerView(), + m.commandInputView(), + } + + var left, right []string + + left = append(left, m.originalHeaderView(), m.originalViewportView()) + right = append(right, m.resultHeaderView(), m.viewportView()) + + viewports := lipgloss.JoinHorizontal(lipgloss.Bottom, strings.Join(left, "\n"), strings.Join(right, "\n")) + res = append(res, viewports) + + return strings.Join(res, "\n") +} diff --git a/internal/cli/query.go b/internal/cli/query.go index c43bd8d..f6e458a 100644 --- a/internal/cli/query.go +++ b/internal/cli/query.go @@ -1,96 +1,43 @@ package cli -import ( - "fmt" - "io" - - "github.com/tomwright/dasel/v3/execution" - "github.com/tomwright/dasel/v3/model" - "github.com/tomwright/dasel/v3/parsing" -) +import "fmt" type QueryCmd struct { - Vars *[]variable `flag:"" name:"var" help:"Variables to pass to the query. E.g. --var foo=\"bar\" --var baz=json:file:./some/file.json"` - InFormat string `flag:"" name:"in" short:"i" help:"The format of the input data."` - OutFormat string `flag:"" name:"out" short:"o" help:"The format of the output data."` - ReturnRoot bool `flag:"" name:"root" help:"Return the root value."` - Unstable bool `flag:"" name:"unstable" help:"Allow access to potentially unstable features."` + Vars variables `flag:"" name:"var" help:"Variables to pass to the query. E.g. --var foo=\"bar\" --var baz=json:file:./some/file.json"` + ExtReadFlags extReadWriteFlags `flag:"" name:"read-flag" help:"Reader flag to customise parsing. E.g. --read-flag xml-mode=structured"` + ExtWriteFlags extReadWriteFlags `flag:"" name:"write-flag" help:"Writer flag to customise output"` + InFormat string `flag:"" name:"in" short:"i" help:"The format of the input data."` + OutFormat string `flag:"" name:"out" short:"o" help:"The format of the output data."` + ReturnRoot bool `flag:"" name:"root" help:"Return the root value."` + Unstable bool `flag:"" name:"unstable" help:"Allow access to potentially unstable features."` + Interactive bool `flag:"" name:"it" help:"Run in interactive mode."` Query string `arg:"" help:"The query to execute." optional:"" default:""` } func (c *QueryCmd) Run(ctx *Globals) error { - var opts []execution.ExecuteOptionFn - - if c.OutFormat == "" { - c.OutFormat = c.InFormat - } - - var reader parsing.Reader - var err error - if len(c.InFormat) > 0 { - reader, err = parsing.Format(c.InFormat).NewReader() - if err != nil { - return fmt.Errorf("failed to get input reader: %w", err) - } - } - - writerOptions := parsing.DefaultWriterOptions() - - writer, err := parsing.Format(c.OutFormat).NewWriter(writerOptions) - if err != nil { - return fmt.Errorf("failed to get output writer: %w", err) - } - - if c.Vars != nil { - for _, v := range *c.Vars { - opts = append(opts, execution.WithVariable(v.Name, v.Value)) - } - } - - // Default to null. If stdin is being read then this will be overwritten. - inputData := model.NewNullValue() - - var inputBytes []byte - if ctx.Stdin != nil { - inputBytes, err = io.ReadAll(ctx.Stdin) - if err != nil { - return fmt.Errorf("error reading stdin: %w", err) - } - } - - if len(inputBytes) > 0 { - if reader == nil { - return fmt.Errorf("input format is required when reading stdin") - } - inputData, err = reader.Read(inputBytes) - if err != nil { - return fmt.Errorf("error reading input: %w", err) - } + if c.Interactive { + return NewInteractiveCmd(c).Run(ctx) } - opts = append(opts, execution.WithVariable("root", inputData)) + o := runOpts{ + Vars: c.Vars, + ExtReadFlags: c.ExtReadFlags, + ExtWriteFlags: c.ExtWriteFlags, + InFormat: c.InFormat, + OutFormat: c.OutFormat, + ReturnRoot: c.ReturnRoot, + Unstable: c.Unstable, + Query: c.Query, - if c.Unstable { - opts = append(opts, execution.WithUnstable()) + Stdin: ctx.Stdin, } - - options := execution.NewOptions(opts...) - out, err := execution.ExecuteSelector(c.Query, inputData, options) + outBytes, err := run(o) if err != nil { return err } - if c.ReturnRoot { - out = inputData - } - - outputBytes, err := writer.Write(out) - if err != nil { - return fmt.Errorf("error writing output: %w", err) - } - - _, err = ctx.Stdout.Write(outputBytes) + _, err = ctx.Stdout.Write(outBytes) if err != nil { return fmt.Errorf("error writing output: %w", err) } diff --git a/internal/cli/read_write_flag.go b/internal/cli/read_write_flag.go new file mode 100644 index 0000000..d38e7c8 --- /dev/null +++ b/internal/cli/read_write_flag.go @@ -0,0 +1,59 @@ +package cli + +import ( + "fmt" + "reflect" + "strings" + + "github.com/alecthomas/kong" + "github.com/tomwright/dasel/v3/parsing" +) + +type extReadWriteFlag struct { + Name string + Value string +} + +type extReadWriteFlags *[]extReadWriteFlag + +func applyReaderFlags(readerOptions *parsing.ReaderOptions, f extReadWriteFlags) { + if f != nil { + for _, flag := range *f { + readerOptions.Ext[flag.Name] = flag.Value + } + } +} + +func applyWriterFlags(writerOptions *parsing.WriterOptions, f extReadWriteFlags) { + if f != nil { + for _, flag := range *f { + writerOptions.Ext[flag.Name] = flag.Value + } + } +} + +type extReadWriteFlagMapper struct { +} + +func (vm *extReadWriteFlagMapper) Decode(ctx *kong.DecodeContext, target reflect.Value) error { + t := ctx.Scan.Pop() + + strVal, ok := t.Value.(string) + if !ok { + return fmt.Errorf("expected string value for variable") + } + + nameValueSplit := strings.SplitN(strVal, "=", 2) + if len(nameValueSplit) != 2 { + return fmt.Errorf("invalid read/write flag format, expect foo=bar") + } + + res := extReadWriteFlag{ + Name: nameValueSplit[0], + Value: nameValueSplit[1], + } + + target.Elem().Set(reflect.Append(target.Elem(), reflect.ValueOf(res))) + + return nil +} diff --git a/internal/cli/run.go b/internal/cli/run.go new file mode 100644 index 0000000..baffccc --- /dev/null +++ b/internal/cli/run.go @@ -0,0 +1,99 @@ +package cli + +import ( + "fmt" + "io" + + "github.com/tomwright/dasel/v3/execution" + "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/parsing" +) + +type runOpts struct { + Vars variables + ExtReadFlags extReadWriteFlags + ExtWriteFlags extReadWriteFlags + InFormat string + OutFormat string + ReturnRoot bool + Unstable bool + Query string + + Stdin io.Reader +} + +func run(o runOpts) ([]byte, error) { + var opts []execution.ExecuteOptionFn + + if o.OutFormat == "" && o.InFormat != "" { + o.OutFormat = o.InFormat + } else if o.OutFormat != "" && o.InFormat == "" { + o.InFormat = o.OutFormat + } + + readerOptions := parsing.DefaultReaderOptions() + applyReaderFlags(&readerOptions, o.ExtReadFlags) + + var reader parsing.Reader + var err error + if len(o.InFormat) > 0 { + reader, err = parsing.Format(o.InFormat).NewReader(readerOptions) + if err != nil { + return nil, fmt.Errorf("failed to get input reader: %w", err) + } + } + + writerOptions := parsing.DefaultWriterOptions() + applyWriterFlags(&writerOptions, o.ExtWriteFlags) + + writer, err := parsing.Format(o.OutFormat).NewWriter(writerOptions) + if err != nil { + return nil, fmt.Errorf("failed to get output writer: %w", err) + } + + opts = append(opts, variableOptions(o.Vars)...) + + // Default to null. If stdin is being read then this will be overwritten. + inputData := model.NewNullValue() + + var inputBytes []byte + if o.Stdin != nil { + inputBytes, err = io.ReadAll(o.Stdin) + if err != nil { + return nil, fmt.Errorf("error reading stdin: %w", err) + } + } + + if len(inputBytes) > 0 { + if reader == nil { + return nil, fmt.Errorf("input format is required when reading stdin") + } + inputData, err = reader.Read(inputBytes) + if err != nil { + return nil, fmt.Errorf("error reading input: %w", err) + } + } + + opts = append(opts, execution.WithVariable("root", inputData)) + + if o.Unstable { + opts = append(opts, execution.WithUnstable()) + } + + options := execution.NewOptions(opts...) + out, err := execution.ExecuteSelector(o.Query, inputData, options) + if err != nil { + return nil, err + } + + if o.ReturnRoot { + out = inputData + } + + outputBytes, err := writer.Write(out) + if err != nil { + return nil, fmt.Errorf("error writing output: %w", err) + } + + return outputBytes, nil +} diff --git a/internal/cli/variable.go b/internal/cli/variable.go index 8253623..124ee7c 100644 --- a/internal/cli/variable.go +++ b/internal/cli/variable.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/alecthomas/kong" + "github.com/tomwright/dasel/v3/execution" "github.com/tomwright/dasel/v3/model" "github.com/tomwright/dasel/v3/parsing" ) @@ -17,6 +18,18 @@ type variable struct { Value *model.Value } +type variables *[]variable + +func variableOptions(vars variables) []execution.ExecuteOptionFn { + var opts []execution.ExecuteOptionFn + if vars != nil { + for _, v := range *vars { + opts = append(opts, execution.WithVariable(v.Name, v.Value)) + } + } + return opts +} + type variableMapper struct { } @@ -65,7 +78,7 @@ func (vm *variableMapper) Decode(ctx *kong.DecodeContext, target reflect.Value) valueRaw = string(contents) } - reader, err := parsing.Format(format).NewReader() + reader, err := parsing.Format(format).NewReader(parsing.DefaultReaderOptions()) if err != nil { return fmt.Errorf("failed to create reader: %w", err) } diff --git a/model/value.go b/model/value.go index c01653f..1a1ec25 100644 --- a/model/value.go +++ b/model/value.go @@ -4,6 +4,7 @@ import ( "fmt" "reflect" "slices" + "strings" ) type Type string @@ -50,6 +51,66 @@ type Value struct { setFn func(*Value) error } +func (v *Value) String() string { + switch v.Type() { + case TypeString: + val, err := v.StringValue() + if err != nil { + panic(err) + } + return fmt.Sprintf("string{%s}", val) + case TypeInt: + val, err := v.IntValue() + if err != nil { + panic(err) + } + return fmt.Sprintf("int{%d}", val) + case TypeFloat: + val, err := v.FloatValue() + if err != nil { + panic(err) + } + return fmt.Sprintf("float(%g)", val) + case TypeBool: + val, err := v.BoolValue() + if err != nil { + panic(err) + } + return fmt.Sprintf("bool{%t}", val) + case TypeMap: + res := fmt.Sprintf("map[%d]{", len(v.Metadata)) + if err := v.RangeMap(func(k string, v *Value) error { + res += fmt.Sprintf("%s: %s, ", k, v.String()) + return nil + }); err != nil { + panic(err) + } + res = strings.TrimSuffix(res, ", ") + return res + "}" + case TypeSlice: + md := "" + if v.IsSpread() { + md = "spread, " + } + if v.IsBranch() { + md += "branch, " + } + res := fmt.Sprintf("array[%s]{", strings.TrimSuffix(md, ", ")) + if err := v.RangeSlice(func(k int, v *Value) error { + res += fmt.Sprintf("%d: %s, ", k, v.String()) + return nil + }); err != nil { + panic(err) + } + res = strings.TrimSuffix(res, ", ") + return res + "}" + case TypeNull: + return "null" + default: + return fmt.Sprintf("unknown[%s]", v.Interface()) + } +} + // NewValue creates a new value. func NewValue(v any) *Value { switch val := v.(type) { diff --git a/model/value_slice.go b/model/value_slice.go index 59cfc5a..52ce98a 100644 --- a/model/value_slice.go +++ b/model/value_slice.go @@ -26,6 +26,14 @@ func (v *Value) isSlice() bool { // Append appends a value to the slice. func (v *Value) Append(val *Value) error { + // Branches behave differently when appending to a slice. + // We expect each item in a branch to be its own value. + if val.IsBranch() { + return val.RangeSlice(func(_ int, item *Value) error { + return v.Append(item) + }) + } + unpacked := v.UnpackKinds(reflect.Interface, reflect.Ptr) if !unpacked.isSlice() { return ErrUnexpectedType{ diff --git a/parsing/d/reader.go b/parsing/d/reader.go index 4e1c920..0e7b544 100644 --- a/parsing/d/reader.go +++ b/parsing/d/reader.go @@ -19,7 +19,7 @@ func init() { parsing.RegisterReader(Dasel, newDaselReader) } -func newDaselReader() (parsing.Reader, error) { +func newDaselReader(options parsing.ReaderOptions) (parsing.Reader, error) { return &daselReader{}, nil } diff --git a/parsing/format.go b/parsing/format.go index 73a6a0a..096c5fc 100644 --- a/parsing/format.go +++ b/parsing/format.go @@ -8,12 +8,12 @@ import ( type Format string // NewReader creates a new reader for the format. -func (f Format) NewReader() (Reader, error) { +func (f Format) NewReader(options ReaderOptions) (Reader, error) { fn, ok := readers[f] if !ok { return nil, fmt.Errorf("unsupported reader file format: %s", f) } - return fn() + return fn(options) } // NewWriter creates a new writer for the format. diff --git a/parsing/json/json.go b/parsing/json/json.go index 43e3d8e..5486d19 100644 --- a/parsing/json/json.go +++ b/parsing/json/json.go @@ -29,7 +29,7 @@ func init() { parsing.RegisterWriter(JSON, newJSONWriter) } -func newJSONReader() (parsing.Reader, error) { +func newJSONReader(options parsing.ReaderOptions) (parsing.Reader, error) { return &jsonReader{}, nil } diff --git a/parsing/json/json_test.go b/parsing/json/json_test.go index 02a2fab..8c10424 100644 --- a/parsing/json/json_test.go +++ b/parsing/json/json_test.go @@ -25,7 +25,7 @@ func TestJson(t *testing.T) { } } `) - reader, err := json.JSON.NewReader() + reader, err := json.JSON.NewReader(parsing.DefaultReaderOptions()) if err != nil { t.Fatal(err) } diff --git a/parsing/reader.go b/parsing/reader.go index 1d66eaf..c8e4902 100644 --- a/parsing/reader.go +++ b/parsing/reader.go @@ -4,6 +4,17 @@ import "github.com/tomwright/dasel/v3/model" var readers = map[Format]NewReaderFn{} +type ReaderOptions struct { + Ext map[string]string +} + +// DefaultReaderOptions returns the default reader options. +func DefaultReaderOptions() ReaderOptions { + return ReaderOptions{ + Ext: make(map[string]string), + } +} + // Reader reads a value from a byte slice. type Reader interface { // Read reads a value from a byte slice. @@ -11,7 +22,7 @@ type Reader interface { } // NewReaderFn is a function that creates a new reader. -type NewReaderFn func() (Reader, error) +type NewReaderFn func(options ReaderOptions) (Reader, error) // RegisterReader registers a new reader for the format. func RegisterReader(format Format, fn NewReaderFn) { diff --git a/parsing/toml/toml.go b/parsing/toml/toml.go index 3695d5f..feef845 100644 --- a/parsing/toml/toml.go +++ b/parsing/toml/toml.go @@ -19,7 +19,7 @@ func init() { parsing.RegisterWriter(TOML, newTOMLWriter) } -func newTOMLReader() (parsing.Reader, error) { +func newTOMLReader(options parsing.ReaderOptions) (parsing.Reader, error) { return &tomlReader{}, nil } diff --git a/parsing/writer.go b/parsing/writer.go index 97c4f23..27165b6 100644 --- a/parsing/writer.go +++ b/parsing/writer.go @@ -7,6 +7,7 @@ var writers = map[Format]NewWriterFn{} type WriterOptions struct { Compact bool Indent string + Ext map[string]string } // DefaultWriterOptions returns the default writer options. @@ -14,6 +15,7 @@ func DefaultWriterOptions() WriterOptions { return WriterOptions{ Compact: false, Indent: " ", + Ext: make(map[string]string), } } diff --git a/parsing/xml/xml.go b/parsing/xml/xml.go index b7302b4..c97d033 100644 --- a/parsing/xml/xml.go +++ b/parsing/xml/xml.go @@ -20,15 +20,15 @@ const ( var _ parsing.Reader = (*xmlReader)(nil) var _ parsing.Writer = (*xmlWriter)(nil) -//var _ parsing.Writer = (*xmlWriter)(nil) - func init() { parsing.RegisterReader(XML, newXMLReader) parsing.RegisterWriter(XML, newXMLWriter) } -func newXMLReader() (parsing.Reader, error) { - return &xmlReader{}, nil +func newXMLReader(options parsing.ReaderOptions) (parsing.Reader, error) { + return &xmlReader{ + structured: options.Ext["xml-mode"] == "structured", + }, nil } // NewXMLWriter creates a new XML writer. @@ -38,7 +38,9 @@ func newXMLWriter(options parsing.WriterOptions) (parsing.Writer, error) { }, nil } -type xmlReader struct{} +type xmlReader struct { + structured bool +} // Read reads a value from a byte slice. func (j *xmlReader) Read(data []byte) (*model.Value, error) { @@ -54,7 +56,10 @@ func (j *xmlReader) Read(data []byte) (*model.Value, error) { return nil, err } - return el.toModel() + if j.structured { + return el.toStructuredModel() + } + return el.toFriendlyModel() } type xmlAttr struct { @@ -69,7 +74,7 @@ type xmlElement struct { Content string } -func (e *xmlElement) toModel() (*model.Value, error) { +func (e *xmlElement) toStructuredModel() (*model.Value, error) { attrs := model.NewMapValue() for _, attr := range e.Attrs { if err := attrs.SetMapKey(attr.Name, model.NewStringValue(attr.Value)); err != nil { @@ -89,7 +94,7 @@ func (e *xmlElement) toModel() (*model.Value, error) { } children := model.NewSliceValue() for _, child := range e.Children { - childModel, err := child.toModel() + childModel, err := child.toStructuredModel() if err != nil { return nil, err } @@ -103,6 +108,69 @@ func (e *xmlElement) toModel() (*model.Value, error) { return res, nil } +func (e *xmlElement) toFriendlyModel() (*model.Value, error) { + if len(e.Attrs) == 0 && len(e.Children) == 0 { + return model.NewStringValue(e.Content), nil + } + + res := model.NewMapValue() + for _, attr := range e.Attrs { + if err := res.SetMapKey("-"+attr.Name, model.NewStringValue(attr.Value)); err != nil { + return nil, err + } + } + + if len(e.Content) > 0 { + if err := res.SetMapKey("#text", model.NewStringValue(e.Content)); err != nil { + return nil, err + } + } + + if len(e.Children) > 0 { + childElementKeys := make([]string, 0) + childElements := make(map[string][]*xmlElement) + + for _, child := range e.Children { + if _, ok := childElements[child.Name]; !ok { + childElementKeys = append(childElementKeys, child.Name) + } + childElements[child.Name] = append(childElements[child.Name], child) + } + + for _, key := range childElementKeys { + cs := childElements[key] + switch len(cs) { + case 0: + continue + case 1: + childModel, err := cs[0].toFriendlyModel() + if err != nil { + return nil, err + } + if err := res.SetMapKey(key, childModel); err != nil { + return nil, err + } + default: + children := model.NewSliceValue() + for _, child := range cs { + childModel, err := child.toFriendlyModel() + if err != nil { + return nil, err + } + if err := children.Append(childModel); err != nil { + return nil, err + } + } + if err := res.SetMapKey(key, children); err != nil { + return nil, err + } + } + } + } + + return res, nil +} + func (j *xmlReader) parseElement(decoder *xml.Decoder, element xml.StartElement) (*xmlElement, error) { el := &xmlElement{ Name: element.Name.Local, diff --git a/parsing/xml/xml_test.go b/parsing/xml/xml_test.go index 6d69387..a72629d 100644 --- a/parsing/xml/xml_test.go +++ b/parsing/xml/xml_test.go @@ -16,7 +16,7 @@ type testCase struct { } func (tc testCase) run(t *testing.T) { - r, err := xml.XML.NewReader() + r, err := xml.XML.NewReader(parsing.DefaultReaderOptions()) if err != nil { t.Fatalf("unexpected error: %s", err) } @@ -37,7 +37,7 @@ func (tc rwTestCase) run(t *testing.T) { if tc.out == "" { tc.out = tc.in } - r, err := xml.XML.NewReader() + r, err := xml.XML.NewReader(parsing.DefaultReaderOptions()) if err != nil { t.Fatalf("unexpected error: %s", err) } diff --git a/parsing/yaml/yaml.go b/parsing/yaml/yaml.go index ce539d8..cc9aae4 100644 --- a/parsing/yaml/yaml.go +++ b/parsing/yaml/yaml.go @@ -22,7 +22,7 @@ func init() { parsing.RegisterWriter(YAML, newYAMLWriter) } -func newYAMLReader() (parsing.Reader, error) { +func newYAMLReader(options parsing.ReaderOptions) (parsing.Reader, error) { return &yamlReader{}, nil } diff --git a/parsing/yaml/yaml_test.go b/parsing/yaml/yaml_test.go index 264e8ab..12c4da4 100644 --- a/parsing/yaml/yaml_test.go +++ b/parsing/yaml/yaml_test.go @@ -16,7 +16,7 @@ type testCase struct { } func (tc testCase) run(t *testing.T) { - r, err := yaml.YAML.NewReader() + r, err := yaml.YAML.NewReader(parsing.DefaultReaderOptions()) if err != nil { t.Fatalf("unexpected error: %s", err) } @@ -37,7 +37,7 @@ func (tc rwTestCase) run(t *testing.T) { if tc.out == "" { tc.out = tc.in } - r, err := yaml.YAML.NewReader() + r, err := yaml.YAML.NewReader(parsing.DefaultReaderOptions()) if err != nil { t.Fatalf("unexpected error: %s", err) } diff --git a/selector/parser/parse_array.go b/selector/parser/parse_array.go index 0e3e645..8f3cadb 100644 --- a/selector/parser/parse_array.go +++ b/selector/parser/parse_array.go @@ -22,9 +22,16 @@ func parseArray(p *Parser) (ast.Expr, error) { return nil, err } - return ast.ArrayExpr{ + arr := ast.ArrayExpr{ Exprs: elements, - }, nil + } + + res, err := parseFollowingSymbol(p, arr) + if err != nil { + return nil, err + } + + return res, nil } func parseIndex(p *Parser) (ast.Expr, error) { diff --git a/selector/parser/parse_object.go b/selector/parser/parse_object.go index 40566c7..d43e954 100644 --- a/selector/parser/parse_object.go +++ b/selector/parser/parse_object.go @@ -95,7 +95,14 @@ func parseObject(p *Parser) (ast.Expr, error) { } p.advance() - return ast.ObjectExpr{ + obj := ast.ObjectExpr{ Pairs: pairs, - }, nil + } + + res, err := parseFollowingSymbol(p, obj) + if err != nil { + return nil, err + } + + return res, nil }