1
0
mirror of https://github.com/TomWright/dasel.git synced 2022-05-22 02:32:45 +03:00

Add multi value selector. (#34)

Select/Put multiple values in one query
This commit is contained in:
Tom Wright
2020-11-08 23:36:47 +00:00
committed by GitHub
parent fc1640717e
commit d140860fea
22 changed files with 1345 additions and 104 deletions

View File

@@ -49,6 +49,7 @@ Comparable to [jq](https://github.com/stedolan/jq) / [yq](https://github.com/kis
* [Child](#child-elements)
* [Index](#index)
* [Next available index](#next-available-index)
* [Any index](#any-index)
* [Dynamic](#dynamic)
* [Using queries in dynamic selectors](#using-queries-in-dynamic-selectors)
* [Examples](#examples)
@@ -141,7 +142,7 @@ An important note is that if no sub-command is given, dasel will default to `sel
### Select
```bash
dasel select -f <file> -p <parser> <selector>
dasel select -f <file> -p <parser> -m <selector>
```
#### Arguments
@@ -160,6 +161,22 @@ This is required if you are piping in data, otherwise dasel will use the given f
See [supported file types](#supported-file-types).
##### `-m`, `--multiple`
Tells dasel to select multiple items.
This causes the [dynamic](#dynamic) selector to return all matching results rather than the first, and enables the [any index](#any-index) selector.
All matches will be output on a new line.
E.g.
```
echo '[{"name": "Tom"}, {"name": "Jim"}]' | dasel -p json -m '.[*].name'
"Tom"
"Jim"
```
##### `-s`, `--selector`, `<selector>`
Specify the selector to use. See [Selectors](#selectors) for more information.
@@ -190,7 +207,7 @@ cat deployment.yaml | dasel select -p yaml "spec.template.spec.containers.(name=
### Put
```bash
dasel put <type> -f <file> -o <out> -p <parser> <selector> <value>
dasel put <type> -f <file> -o <out> -p <parser> -m <selector> <value>
```
```bash
@@ -230,6 +247,26 @@ This is required if you are piping in data, otherwise dasel will use the given f
See [supported file types](#supported-file-types).
##### `-m`, `--multiple`
Tells dasel to put multiple items.
This causes the [dynamic](#dynamic) selector to return all matching results rather than the first, and enables the [any index](#any-index) selector.
E.g.
```
echo '[{"name": "Tom"}, {"name": "Jim"}]' | dasel put string -p json -m '.[*].name' Frank
[
{
"name": "Frank"
},
{
"name": "Frank"
}
]
```
##### `-s`, `--selector`, `<selector>`
Specify the selector to use. See [Selectors](#selectors) for more information.
@@ -251,7 +288,7 @@ This is required.
Putting objects works slightly differently to a standard put, but the same principles apply.
```bash
dasel put object -f <file> -o <out> -p <parser> -t <type> <selector> <values>
dasel put object -f <file> -o <out> -p <parser> -m -t <type> <selector> <values>
```
#### Arguments
@@ -287,6 +324,26 @@ This is required if you are piping in data, otherwise dasel will use the given f
See [supported file types](#supported-file-types).
##### `-m`, `--multiple`
Tells dasel to put multiple items.
This causes the [dynamic](#dynamic) selector to return all matching results rather than the first, and enables the [any index](#any-index) selector.
E.g.
```
echo '[{"name": "Tom"}, {"name": "Jim"}]' | dasel put object -p json -m -t string '.[*]' 'name=Frank'
[
{
"name": "Frank"
},
{
"name": "Frank"
}
]
```
##### `-s`, `--selector`, `<selector>`
Specify the selector to use. See [Selectors](#selectors) for more information.
@@ -424,6 +481,12 @@ green
The next available index selector is used when adding to a list of items. It allows you to append to a list.
- `colours.[]`
#### Any Index
The any index selector is used to select *all* items of a list.
- `colours[*]`
This must be used in conjunction with `-m`,`--multiple`.
#### Dynamic
Dynamic selectors are used with lists when you don't know the index of the item, but instead want to find the index based on some other criteria.

View File

@@ -3,6 +3,7 @@ package dasel
import (
"errors"
"fmt"
"reflect"
)
// ErrMissingPreviousNode is returned when findValue doesn't have access to the previous node.
@@ -51,17 +52,13 @@ func (e UnsupportedTypeForSelector) Error() string {
// ValueNotFound is returned when a selector string cannot be fully resolved.
type ValueNotFound struct {
Selector string
Node *Node
Selector string
PreviousValue reflect.Value
}
// Error returns the error message.
func (e ValueNotFound) Error() string {
var previousValue interface{}
if e.Node != nil && e.Node.Previous != nil {
previousValue = e.Node.Previous.Value
}
return fmt.Sprintf("no value found for selector: %s: %v", e.Selector, previousValue)
return fmt.Sprintf("no value found for selector: %s: %v", e.Selector, e.PreviousValue)
}
// UnexpectedPreviousNilValue is returned when the previous node contains a nil value.

View File

@@ -27,22 +27,10 @@ func TestErrorMessages(t *testing.T) {
}, Out: "selector [INDEX] does not support value: map[string]interface {}: map[]"},
{In: &dasel.ValueNotFound{
Selector: ".name",
Node: &dasel.Node{
Selector: dasel.Selector{
Current: ".name",
},
},
}, Out: "no value found for selector: .name: <nil>"},
}, Out: "no value found for selector: .name: <invalid reflect.Value>"},
{In: &dasel.ValueNotFound{
Selector: ".name",
Node: &dasel.Node{
Previous: &dasel.Node{
Value: reflect.ValueOf(map[string]interface{}{}),
},
Selector: dasel.Selector{
Current: ".name",
},
},
Selector: ".name",
PreviousValue: reflect.ValueOf(map[string]interface{}{}),
}, Out: "no value found for selector: .name: map[]"},
{In: &dasel.UnexpectedPreviousNilValue{Selector: ".name"}, Out: "previous value is nil: .name"},
{In: &dasel.UnhandledCheckType{Value: ""}, Out: "unhandled check type: string"},

View File

@@ -1,6 +1,7 @@
package command
import (
"bytes"
"fmt"
"github.com/spf13/cobra"
"github.com/tomwright/dasel"
@@ -140,8 +141,70 @@ func writeNodeToOutput(opts writeNodeToOutputOpts, cmd *cobra.Command) error {
return nil
}
type writeNodesToOutputOpts struct {
Nodes []*dasel.Node
Parser storage.Parser
File string
Out string
Writer io.Writer
Plain bool
}
func writeNodesToOutput(opts writeNodesToOutputOpts, cmd *cobra.Command) error {
if opts.Writer == nil {
switch {
case opts.Out == "" && shouldReadFromStdin(opts.File):
// No out flag and we read from stdin.
opts.Writer = cmd.OutOrStdout()
case opts.Out == "stdout", opts.Out == "-":
// Out flag wants to write to stdout.
opts.Writer = cmd.OutOrStdout()
case opts.Out == "":
// No out flag... write to the file we read from.
f, err := os.Create(opts.File)
if err != nil {
return fmt.Errorf("could not open output file: %w", err)
}
defer f.Close()
opts.Writer = f
case opts.Out != "":
// Out flag was set.
f, err := os.Create(opts.Out)
if err != nil {
return fmt.Errorf("could not open output file: %w", err)
}
defer f.Close()
opts.Writer = f
}
}
buf := new(bytes.Buffer)
for i, n := range opts.Nodes {
subOpts := writeNodeToOutputOpts{
Node: n,
Parser: opts.Parser,
Writer: buf,
Plain: opts.Plain,
}
if err := writeNodeToOutput(subOpts, cmd); err != nil {
return fmt.Errorf("could not write node %d to output: %w", i, err)
}
}
if _, err := io.Copy(opts.Writer, buf); err != nil {
return fmt.Errorf("could not copy buffer to real output: %w", err)
}
return nil
}
func putCommand() *cobra.Command {
var fileFlag, selectorFlag, parserFlag, outFlag string
var multiFlag bool
cmd := &cobra.Command{
Use: "put -f <file> -s <selector>",
@@ -159,6 +222,7 @@ func putCommand() *cobra.Command {
cmd.PersistentFlags().StringVarP(&selectorFlag, "selector", "s", "", "The selector to use when querying the data structure.")
cmd.PersistentFlags().StringVarP(&parserFlag, "parser", "p", "", "The parser to use with the given file.")
cmd.PersistentFlags().StringVarP(&outFlag, "out", "o", "", "Output destination.")
cmd.PersistentFlags().BoolVarP(&multiFlag, "multiple", "m", false, "Select multiple results.")
_ = cmd.MarkPersistentFlagFilename("file")
@@ -175,6 +239,7 @@ type genericPutOptions struct {
Init func(genericPutOptions) genericPutOptions
Reader io.Reader
Writer io.Writer
Multi bool
}
func getGenericInit(cmd *cobra.Command, args []string) func(options genericPutOptions) genericPutOptions {
@@ -183,6 +248,7 @@ func getGenericInit(cmd *cobra.Command, args []string) func(options genericPutOp
opts.Out = cmd.Flag("out").Value.String()
opts.Parser = cmd.Flag("parser").Value.String()
opts.Selector = cmd.Flag("selector").Value.String()
opts.Multi, _ = cmd.Flags().GetBool("multiple")
if opts.Selector == "" && len(args) > 0 {
opts.Selector = args[0]
@@ -219,8 +285,14 @@ func runGenericPutCommand(opts genericPutOptions, cmd *cobra.Command) error {
return err
}
if err := rootNode.Put(opts.Selector, updateValue); err != nil {
return fmt.Errorf("could not put value: %w", err)
if opts.Multi {
if err := rootNode.PutMultiple(opts.Selector, updateValue); err != nil {
return fmt.Errorf("could not put multi value: %w", err)
}
} else {
if err := rootNode.Put(opts.Selector, updateValue); err != nil {
return fmt.Errorf("could not put value: %w", err)
}
}
if err := writeNodeToOutput(writeNodeToOutputOpts{

View File

@@ -17,6 +17,7 @@ type putObjectOpts struct {
InputValues []string
Reader io.Reader
Writer io.Writer
Multi bool
}
func getMapFromTypesValues(inputTypes []string, inputValues []string) (map[string]interface{}, error) {
@@ -59,8 +60,14 @@ func runPutObjectCommand(opts putObjectOpts, cmd *cobra.Command) error {
return err
}
if err := rootNode.Put(opts.Selector, updateValue); err != nil {
return fmt.Errorf("could not put value: %w", err)
if opts.Multi {
if err := rootNode.PutMultiple(opts.Selector, updateValue); err != nil {
return fmt.Errorf("could not put object multi value: %w", err)
}
} else {
if err := rootNode.Put(opts.Selector, updateValue); err != nil {
return fmt.Errorf("could not put object value: %w", err)
}
}
if err := writeNodeToOutput(writeNodeToOutputOpts{
@@ -92,6 +99,8 @@ func putObjectCommand() *cobra.Command {
InputTypes: typeList.Strings,
InputValues: args,
}
opts.Multi, _ = cmd.Flags().GetBool("multiple")
if opts.Selector == "" && len(opts.InputValues) > 0 {
opts.Selector = opts.InputValues[0]
opts.InputValues = opts.InputValues[1:]

View File

@@ -1,10 +1,46 @@
package command
import (
"errors"
"reflect"
"strings"
"testing"
)
func TestPut_Object(t *testing.T) {
t.Run("SingleFailingWriter", func(t *testing.T) {
err := runPutObjectCommand(putObjectOpts{
Parser: "json",
Selector: ".[0]",
Reader: strings.NewReader(`[{"name": "Tom"}]`),
InputValues: []string{"name=Frank"},
InputTypes: []string{"string"},
Writer: &failingWriter{},
}, nil)
if err == nil || !errors.Is(err, errFailingWriterErr) {
t.Errorf("expected error %v, got %v", errFailingWriterErr, err)
return
}
})
t.Run("MultiFailingWriter", func(t *testing.T) {
err := runPutObjectCommand(putObjectOpts{
Parser: "json",
Selector: ".[*]",
Reader: strings.NewReader(`[{"name": "Tom"}]`),
InputValues: []string{"name=Frank"},
InputTypes: []string{"string"},
Writer: &failingWriter{},
Multi: true,
}, nil)
if err == nil || !errors.Is(err, errFailingWriterErr) {
t.Errorf("expected error %v, got %v", errFailingWriterErr, err)
return
}
})
}
func TestGetMapFromTypes(t *testing.T) {
t.Run("Valid", func(t *testing.T) {
exp := map[string]interface{}{

View File

@@ -33,6 +33,28 @@ func TestRootCMD_Put(t *testing.T) {
}
`,
))
t.Run("InvalidSingleSelector", expectErrFromInput(
`{"name": "Tom"}`,
[]string{"put", "string", "-f", "stdin", "-o", "stdout", "-p", "json", "-s", "[-]", "Frank"},
"selector is not supported here: [-]",
))
t.Run("InvalidMultiSelector", expectErrFromInput(
`{"name": "Tom"}`,
[]string{"put", "string", "-f", "stdin", "-o", "stdout", "-p", "json", "-m", "-s", "[-]", "Frank"},
"selector is not supported here: [-]",
))
t.Run("InvalidObjectSingleSelector", expectErrFromInput(
`{"name": "Tom"}`,
[]string{"put", "object", "-f", "stdin", "-o", "stdout", "-p", "json", "-t", "string", "-s", "[-]", "Frank"},
"selector is not supported here: [-]",
))
t.Run("InvalidMultiSelector", expectErrFromInput(
`{"name": "Tom"}`,
[]string{"put", "object", "-f", "stdin", "-o", "stdout", "-p", "json", "-m", "-t", "string", "-s", "[-]", "Frank"},
"selector is not supported here: [-]",
))
}
func putTest(in string, varType string, parser string, selector string, value string, out string, expErr error, additionalArgs ...string) func(t *testing.T) {
@@ -126,6 +148,126 @@ func TestRootCMD_Put_JSON(t *testing.T) {
}
]
}`, nil))
t.Run("MultipleObject", putObjectTest(`{
"numbers": [
{
"rank": 1,
"number": "one"
},
{
"rank": 2,
"number": "two"
},
{
"rank": 3,
"number": "three"
}
]
}`, "json", ".numbers.[*]", []string{"number=five", "rank=5"}, []string{"string", "int"}, `{
"numbers": [
{
"number": "five",
"rank": 5
},
{
"number": "five",
"rank": 5
},
{
"number": "five",
"rank": 5
}
]
}`, nil, "-m"))
t.Run("AppendObject", putObjectTest(`{
"numbers": [
{
"rank": 1,
"number": "one"
},
{
"rank": 2,
"number": "two"
},
{
"rank": 3,
"number": "three"
}
]
}`, "json", ".numbers.[]", []string{"rank=4", "number=four"}, []string{"int", "string"}, `{
"numbers": [
{
"number": "one",
"rank": 1
},
{
"number": "two",
"rank": 2
},
{
"number": "three",
"rank": 3
},
{
"number": "four",
"rank": 4
}
]
}`, nil))
t.Run("AppendObjectMulti", putObjectTest(`{
"numbers": [
{
"rank": 1,
"number": "one"
},
{
"rank": 2,
"number": "two"
},
{
"rank": 3,
"number": "three"
}
]
}`, "json", ".numbers.[]", []string{"rank=4", "number=four"}, []string{"int", "string"}, `{
"numbers": [
{
"number": "one",
"rank": 1
},
{
"number": "two",
"rank": 2
},
{
"number": "three",
"rank": 3
},
{
"number": "four",
"rank": 4
}
]
}`, nil, "-m"))
t.Run("MultipleString", putStringTest(`[
{"value": "A"},
{"value": "B"},
{"value": "C"}
]`, "json", "[*].value", "X", `[
{
"value": "X"
},
{
"value": "X"
},
{
"value": "X"
}
]`, nil, "-m"))
}
func TestRootCMD_Put_YAML(t *testing.T) {
@@ -270,17 +412,19 @@ func TestRootCMD_Put_CSV(t *testing.T) {
`, nil))
}
func putObjectTest(in string, parser string, selector string, values []string, types []string, out string, expErr error) func(t *testing.T) {
func putObjectTest(in string, parser string, selector string, values []string, types []string, out string, expErr error, additionalArgs ...string) func(t *testing.T) {
return func(t *testing.T) {
cmd := command.NewRootCMD()
outputBuffer := bytes.NewBuffer([]byte{})
args := []string{
"put", "object", "-p", parser, "-o", "stdout", selector,
"put", "object", "-p", parser, "-o", "stdout",
}
for _, t := range types {
args = append(args, "-t", t)
}
args = append(args, additionalArgs...)
args = append(args, selector)
args = append(args, values...)
cmd.SetOut(outputBuffer)
@@ -316,14 +460,14 @@ func putObjectTest(in string, parser string, selector string, values []string, t
}
}
func putStringTest(in string, parser string, selector string, value string, out string, expErr error) func(t *testing.T) {
return putTest(in, "string", parser, selector, value, out, expErr)
func putStringTest(in string, parser string, selector string, value string, out string, expErr error, additionalArgs ...string) func(t *testing.T) {
return putTest(in, "string", parser, selector, value, out, expErr, additionalArgs...)
}
func putIntTest(in string, parser string, selector string, value string, out string, expErr error) func(t *testing.T) {
return putTest(in, "int", parser, selector, value, out, expErr)
func putIntTest(in string, parser string, selector string, value string, out string, expErr error, additionalArgs ...string) func(t *testing.T) {
return putTest(in, "int", parser, selector, value, out, expErr, additionalArgs...)
}
func putBoolTest(in string, parser string, selector string, value string, out string, expErr error) func(t *testing.T) {
return putTest(in, "bool", parser, selector, value, out, expErr)
func putBoolTest(in string, parser string, selector string, value string, out string, expErr error, additionalArgs ...string) func(t *testing.T) {
return putTest(in, "bool", parser, selector, value, out, expErr, additionalArgs...)
}

View File

@@ -121,16 +121,31 @@ func TestRootCMD_Select(t *testing.T) {
`"Tom"
`,
))
t.Run("InvalidSingleSelector", expectErrFromInput(
`{"name": "Tom"}`,
[]string{"select", "-p", "json", "-s", "[-]"},
"selector is not supported here: [-]",
))
t.Run("InvalidMultiSelector", expectErrFromInput(
`{"name": "Tom"}`,
[]string{"select", "-p", "json", "-m", "-s", "[-]"},
"selector is not supported here: [-]",
))
}
func selectTest(in string, parser string, selector string, out string, expErr error) func(t *testing.T) {
func selectTest(in string, parser string, selector string, out string, expErr error, additionalArgs ...string) func(t *testing.T) {
return func(t *testing.T) {
cmd := command.NewRootCMD()
outputBuffer := bytes.NewBuffer([]byte{})
args := []string{
"select", "-p", parser, selector,
"select", "-p", parser,
}
if additionalArgs != nil {
args = append(args, additionalArgs...)
}
args = append(args, selector)
cmd.SetOut(outputBuffer)
cmd.SetIn(strings.NewReader(in))
@@ -214,6 +229,13 @@ func TestRootCmd_Select_JSON(t *testing.T) {
t.Run("DynamicString", selectTest(jsonData, "json", ".details.addresses.(postcode=YYY YYY).street", newline(`"34 Another Street"`), nil))
t.Run("QueryFromFile", selectTestFromFile("./../../tests/assets/example.json", ".preferences.favouriteColour", newline(`"red"`), nil))
t.Run("MultiProperty", selectTest(jsonData, "json", ".details.addresses[*].street", newline(`"101 Some Street"
"34 Another Street"`), nil, "-m"))
t.Run("MultiRoot", selectTest(jsonDataSingle, "json", ".", newline(`{
"x": "asd"
}`), nil, "-m"))
t.Run("SubSelector", selectTest(`{
"users": [
{

View File

@@ -15,12 +15,12 @@ func TestChangeDefaultCommand(t *testing.T) {
os.Args = cachedArgs
}()
testArgs := func(in []string, exp []string) func(t *testing.T) {
testArgs := func(in []string, exp []string, blacklistedArgs ...string) func(t *testing.T) {
return func(t *testing.T) {
os.Args = in
cmd := command.NewRootCMD()
command.ChangeDefaultCommand(cmd, "select")
command.ChangeDefaultCommand(cmd, "select", blacklistedArgs...)
got := os.Args
if !reflect.DeepEqual(exp, got) {
@@ -43,6 +43,18 @@ func TestChangeDefaultCommand(t *testing.T) {
[]string{"dasel", "put", "-p", "json", "-t", "string", "name=Tom"},
[]string{"dasel", "put", "-p", "json", "-t", "string", "name=Tom"},
))
t.Run("IgnoreBlacklisted", testArgs(
[]string{"dasel", "-v"},
[]string{"dasel", "-v"},
"-v",
))
t.Run("IgnoreBlacklisted", testArgs(
[]string{"dasel", "select", "-v"},
[]string{"dasel", "select", "-v"},
"-v",
))
}
func expectErr(args []string, expErr string) func(t *testing.T) {
@@ -62,6 +74,24 @@ func expectErr(args []string, expErr string) func(t *testing.T) {
}
}
func expectErrFromInput(in string, args []string, expErr string) func(t *testing.T) {
return func(t *testing.T) {
cmd := command.NewRootCMD()
outputBuffer := bytes.NewBuffer([]byte{})
cmd.SetIn(bytes.NewReader([]byte(in)))
cmd.SetOut(outputBuffer)
cmd.SetArgs(args)
err := cmd.Execute()
if err == nil || !strings.Contains(err.Error(), expErr) {
t.Errorf("unexpected error: %v", err)
return
}
}
}
func expectOutput(in string, args []string, exp string) func(t *testing.T) {
return func(t *testing.T) {
cmd := command.NewRootCMD()

View File

@@ -14,6 +14,7 @@ type selectOptions struct {
Reader io.Reader
Writer io.Writer
Plain bool
Multi bool
}
func runSelectCommand(opts selectOptions, cmd *cobra.Command) error {
@@ -30,7 +31,34 @@ func runSelectCommand(opts selectOptions, cmd *cobra.Command) error {
return err
}
if opts.Writer == nil {
opts.Writer = cmd.OutOrStdout()
}
if opts.Multi {
var results []*dasel.Node
if opts.Selector == "." {
results = []*dasel.Node{rootNode}
} else {
results, err = rootNode.QueryMultiple(opts.Selector)
if err != nil {
return fmt.Errorf("could not query multiple node: %w", err)
}
}
if err := writeNodesToOutput(writeNodesToOutputOpts{
Nodes: results,
Parser: parser,
Writer: opts.Writer,
Plain: opts.Plain,
}, cmd); err != nil {
return fmt.Errorf("could not write output: %w", err)
}
return nil
}
var res *dasel.Node
if opts.Selector == "." {
res = rootNode
} else {
@@ -40,10 +68,6 @@ func runSelectCommand(opts selectOptions, cmd *cobra.Command) error {
}
}
if opts.Writer == nil {
opts.Writer = cmd.OutOrStdout()
}
if err := writeNodeToOutput(writeNodeToOutputOpts{
Node: res,
Parser: parser,
@@ -58,7 +82,7 @@ func runSelectCommand(opts selectOptions, cmd *cobra.Command) error {
func selectCommand() *cobra.Command {
var fileFlag, selectorFlag, parserFlag string
var plainFlag bool
var plainFlag, multiFlag bool
cmd := &cobra.Command{
Use: "select -f <file> -p <json,yaml> -s <selector>",
@@ -73,6 +97,7 @@ func selectCommand() *cobra.Command {
Parser: parserFlag,
Selector: selectorFlag,
Plain: plainFlag,
Multi: multiFlag,
}, cmd)
},
}
@@ -81,6 +106,7 @@ func selectCommand() *cobra.Command {
cmd.Flags().StringVarP(&selectorFlag, "selector", "s", "", "The selector to use when querying the data structure.")
cmd.Flags().StringVarP(&parserFlag, "parser", "p", "", "The parser to use with the given file.")
cmd.Flags().BoolVar(&plainFlag, "plain", false, "Do not format output to the output data format.")
cmd.Flags().BoolVarP(&multiFlag, "multiple", "m", false, "Select multiple results.")
_ = cmd.MarkFlagFilename("file")

View File

@@ -2,6 +2,7 @@ package command
import (
"bytes"
"errors"
"io/ioutil"
"strings"
"testing"
@@ -64,6 +65,15 @@ var tomlData = `id = "1111"
postcode = "YYY YYY"
`
var errFailingWriterErr = errors.New("i am meant to fail at writing")
type failingWriter struct {
}
func (fp *failingWriter) Write(_ []byte) (int, error) {
return 0, errFailingWriterErr
}
func selectTest(in string, parser string, selector string, out string, expErr error) func(t *testing.T) {
return func(t *testing.T) {
outputBuffer := bytes.NewBuffer([]byte{})
@@ -104,6 +114,36 @@ func newline(x string) string {
return x + "\n"
}
func TestSelect(t *testing.T) {
t.Run("SingleFailingWriter", func(t *testing.T) {
err := runSelectCommand(selectOptions{
Parser: "json",
Selector: ".",
Reader: strings.NewReader(`{"name": "Tom"}`),
Writer: &failingWriter{},
}, nil)
if err == nil || !errors.Is(err, errFailingWriterErr) {
t.Errorf("expected error %v, got %v", errFailingWriterErr, err)
return
}
})
t.Run("MultiFailingWriter", func(t *testing.T) {
err := runSelectCommand(selectOptions{
Parser: "json",
Selector: ".",
Reader: strings.NewReader(`{"name": "Tom"}`),
Writer: &failingWriter{},
Multi: true,
}, nil)
if err == nil || !errors.Is(err, errFailingWriterErr) {
t.Errorf("expected error %v, got %v", errFailingWriterErr, err)
return
}
})
}
func TestSelect_JSON(t *testing.T) {
t.Run("SingleProperty", selectTest(jsonData, "json", ".id", newline(`"1111"`), nil))
t.Run("ObjectProperty", selectTest(jsonData, "json", ".details.name", newline(`"Tom"`), nil))

View File

@@ -10,7 +10,7 @@ import (
type CSVParser struct {
}
// YAMLSingleDocument represents a CSV file.
// CSVDocument represents a CSV file.
// This is required to keep headers in the expected order.
type CSVDocument struct {
originalRequired

View File

@@ -61,6 +61,7 @@ func TestNewParserFromFilename(t *testing.T) {
{In: "a.yml", Out: &storage.YAMLParser{}},
{In: "a.toml", Out: &storage.TOMLParser{}},
{In: "a.xml", Out: &storage.XMLParser{}},
{In: "a.csv", Out: &storage.CSVParser{}},
{In: "a.txt", Out: nil, Err: &storage.UnknownParserErr{Parser: ".txt"}},
}

View File

@@ -1,7 +1,6 @@
package storage_test
import (
"fmt"
"github.com/tomwright/dasel/internal/storage"
"reflect"
"testing"
@@ -17,21 +16,6 @@ var xmlMap = map[string]interface{}{
},
}
func TestXMLParser_FromBytes2(t *testing.T) {
got, err := (&storage.XMLParser{}).FromBytes([]byte(`<data>
<names>
<name>Tom</name>
<name>Jim</name>
</names>
</data>`))
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
fmt.Println(got)
}
func TestXMLParser_FromBytes(t *testing.T) {
got, err := (&storage.XMLParser{}).FromBytes(xmlBytes)
if err != nil {

View File

@@ -1,7 +1,6 @@
package storage_test
import (
"fmt"
"github.com/tomwright/dasel/internal/storage"
"reflect"
"strings"
@@ -21,7 +20,6 @@ func TestYAMLParser_FromBytes(t *testing.T) {
t.Errorf("unexpected error: %s", err)
return
}
fmt.Printf("%T", got)
exp := &storage.YAMLSingleDocument{Value: yamlMap}
if !reflect.DeepEqual(exp, got) {
t.Errorf("expected %v, got %v", exp, got)
@@ -51,13 +49,22 @@ name: Jim
}
})
t.Run("Invalid", func(t *testing.T) {
d, err := (&storage.YAMLParser{}).FromBytes([]byte(`{1:asd`))
fmt.Println(d)
_, err := (&storage.YAMLParser{}).FromBytes([]byte(`{1:asd`))
if err == nil || !strings.Contains(err.Error(), "could not unmarshal config data") {
t.Errorf("unexpected error: %v", err)
return
}
})
t.Run("Empty", func(t *testing.T) {
got, err := (&storage.YAMLParser{}).FromBytes([]byte(``))
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
if !reflect.DeepEqual(nil, got) {
t.Errorf("expected %v, got %v", nil, got)
}
})
}
func TestYAMLParser_ToBytes(t *testing.T) {

17
node.go
View File

@@ -31,8 +31,13 @@ type Selector struct {
type Node struct {
// Previous is the previous node in the chain.
Previous *Node `json:"-"`
// Next is the next node in the chain.
// Next contains the next node in the chain.
// This is used with Query and Put requests.
Next *Node `json:"next,omitempty"`
// NextMultiple contains the next nodes in the chain.
// This is used with QueryMultiple and PutMultiple requests.
// When a major version change occurs this will completely replace Next.
NextMultiple []*Node `json:"nextMultiple,omitempty"`
// OriginalValue is the value returned from the parser.
// In most cases this is the same as Value, but is different for thr YAML parser
// as it contains information on the original document.
@@ -52,7 +57,7 @@ func (n *Node) InterfaceValue() interface{} {
const (
propertySelector = `(?P<property>[a-zA-Z\-_]+)`
indexSelector = `\[(?P<index>[0-9a-zA-Z]*?)\]`
indexSelector = `\[(?P<index>[0-9a-zA-Z\*]*?)\]`
dynamicSelector = `(?P<name>.+)(?P<comparison>=|<|>)(?P<value>.+)`
)
@@ -105,9 +110,12 @@ func ParseSelector(selector string) (Selector, error) {
sel.Property = match[1]
} else if match := indexRegexp.FindStringSubmatch(selector); len(match) != 0 {
sel.Current = match[0]
if match[1] == "" {
switch match[1] {
case "":
sel.Type = "NEXT_AVAILABLE_INDEX"
} else {
case "*":
sel.Type = "INDEX_ANY"
default:
sel.Type = "INDEX"
var err error
index, err := strconv.ParseInt(match[1], 10, 32)
@@ -161,6 +169,7 @@ func New(value interface{}) *Node {
rootNode := &Node{
Previous: nil,
Next: nil,
NextMultiple: nil,
OriginalValue: value,
Value: baseValue,
Selector: Selector{

View File

@@ -25,6 +25,29 @@ func (n *Node) Put(selector string, newValue interface{}) error {
return nil
}
// PutMultiple all applicable nodes for the given selector and updates all of their values to the given value.
// It then attempts to propagate the value back up the chain to the root element.
func (n *Node) PutMultiple(selector string, newValue interface{}) error {
n.Selector.Remaining = selector
if err := buildPutMultipleChain(n); err != nil {
return err
}
final := lastNodes(n)
val := reflect.ValueOf(newValue)
for _, n := range final {
n.Value = val
if err := propagate(n); err != nil {
return err
}
}
return nil
}
func buildPutChain(n *Node) error {
if n.Selector.Remaining == "" {
// We've reached the end
@@ -47,8 +70,41 @@ func buildPutChain(n *Node) error {
// Populate the value for the new node.
nextNode.Value, err = findValue(nextNode, true)
if err != nil {
return fmt.Errorf("could not find value: %w", err)
return fmt.Errorf("could not find put value: %w", err)
}
return buildPutChain(nextNode)
}
func buildPutMultipleChain(n *Node) error {
if n.Selector.Remaining == "" {
// We've reached the end
return nil
}
var err error
// Parse the selector.
nextSelector, err := ParseSelector(n.Selector.Remaining)
if err != nil {
return fmt.Errorf("failed to parse selector: %w", err)
}
// Populate the value for the new node.
n.NextMultiple, err = findNodes(nextSelector, n, true)
if err != nil {
return fmt.Errorf("could not find put multiple value: %w", err)
}
for _, next := range n.NextMultiple {
// Add the back reference
next.Previous = n
if err := buildPutMultipleChain(next); err != nil {
return err
}
}
return nil
}

View File

@@ -17,12 +17,15 @@ func (n *Node) Query(selector string) (*Node, error) {
return lastNode(rootNode), nil
}
// lastNode returns the last node in the chain.
// If a node contains multiple next nodes, the first node is taken.
func lastNode(n *Node) *Node {
node := n
for {
if n.Next == nil {
return n
if node.Next == nil {
return node
}
n = n.Next
node = node.Next
}
}
@@ -72,7 +75,7 @@ func findValueProperty(n *Node, createIfNotExists bool) (reflect.Value, error) {
if createIfNotExists {
return nilValue(), nil
}
return nilValue(), &ValueNotFound{Selector: n.Selector.Current, Node: n}
return nilValue(), &ValueNotFound{Selector: n.Selector.Current, PreviousValue: n.Previous.Value}
}
return nilValue(), &UnsupportedTypeForSelector{Selector: n.Selector, Value: n.Previous.Value.Type().Kind()}
@@ -95,7 +98,7 @@ func findValueIndex(n *Node, createIfNotExists bool) (reflect.Value, error) {
if createIfNotExists {
return nilValue(), nil
}
return nilValue(), &ValueNotFound{Selector: n.Selector.Current, Node: n}
return nilValue(), &ValueNotFound{Selector: n.Selector.Current, PreviousValue: n.Previous.Value}
}
return nilValue(), &UnsupportedTypeForSelector{Selector: n.Selector, Value: value.Kind()}
@@ -106,7 +109,7 @@ func findValueIndex(n *Node, createIfNotExists bool) (reflect.Value, error) {
func findNextAvailableIndex(n *Node, createIfNotExists bool) (reflect.Value, error) {
if !createIfNotExists {
// Next available index isn't supported unless it's creating the item.
return nilValue(), &ValueNotFound{Selector: n.Selector.Current, Node: n}
return nilValue(), &ValueNotFound{Selector: n.Selector.Current, PreviousValue: n.Previous.Value}
}
return nilValue(), nil
}
@@ -158,29 +161,12 @@ func findValueDynamic(n *Node, createIfNotExists bool) (reflect.Value, error) {
n.Selector.Type = "NEXT_AVAILABLE_INDEX"
return nilValue(), nil
}
return nilValue(), &ValueNotFound{Selector: n.Selector.Current, Node: n}
return nilValue(), &ValueNotFound{Selector: n.Selector.Current, PreviousValue: n.Previous.Value}
}
return nilValue(), &UnsupportedTypeForSelector{Selector: n.Selector, Value: value.Kind()}
}
func initialiseEmptyValue(n *Node) {
switch n.Selector.Type {
case "PROPERTY":
n.wasInitialised = true
n.Previous.Value = reflect.ValueOf(map[interface{}]interface{}{})
case "INDEX":
n.wasInitialised = true
n.Previous.Value = reflect.ValueOf([]interface{}{})
case "NEXT_AVAILABLE_INDEX":
n.wasInitialised = true
n.Previous.Value = reflect.ValueOf([]interface{}{})
case "DYNAMIC":
n.wasInitialised = true
n.Previous.Value = reflect.ValueOf([]interface{}{})
}
}
// findValue finds the value for the given node.
// The value is essentially pulled from the previous node, using the (already parsed) selector
// information stored on the current node.
@@ -191,7 +177,7 @@ func findValue(n *Node, createIfNotExists bool) (reflect.Value, error) {
}
if createIfNotExists && !isValid(n.Previous.Value) {
initialiseEmptyValue(n)
n.Previous.Value, n.wasInitialised = initialiseEmptyValue(n.Selector, n.Previous.Value)
}
switch n.Selector.Type {
@@ -204,6 +190,6 @@ func findValue(n *Node, createIfNotExists bool) (reflect.Value, error) {
case "DYNAMIC":
return findValueDynamic(n, createIfNotExists)
default:
return nilValue(), &UnsupportedSelector{Selector: n.Selector.Type}
return nilValue(), &UnsupportedSelector{Selector: n.Selector.Raw}
}
}

View File

@@ -52,7 +52,7 @@ func TestFindValueProperty(t *testing.T) {
n := getNodeWithValue(map[string]interface{}{})
n.Selector.Current = "x"
got, err := findValueProperty(n, false)
assertQueryResult(t, nilValue(), &ValueNotFound{Selector: n.Selector.Current, Node: n}, got, err)
assertQueryResult(t, nilValue(), &ValueNotFound{Selector: n.Selector.Current, PreviousValue: n.Previous.Value}, got, err)
})
}
@@ -68,7 +68,7 @@ func TestFindValueIndex(t *testing.T) {
n.Selector.Current = "[0]"
n.Selector.Index = 0
got, err := findValueIndex(n, false)
assertQueryResult(t, nilValue(), &ValueNotFound{Selector: n.Selector.Current, Node: n}, got, err)
assertQueryResult(t, nilValue(), &ValueNotFound{Selector: n.Selector.Current, PreviousValue: n.Previous.Value}, got, err)
})
t.Run("UnsupportedType", func(t *testing.T) {
val := map[string]interface{}{}
@@ -86,7 +86,7 @@ func TestFindValueNextAvailableIndex(t *testing.T) {
n.Selector.Current = "[0]"
n.Selector.Index = 0
got, err := findNextAvailableIndex(n, false)
assertQueryResult(t, nilValue(), &ValueNotFound{Selector: n.Selector.Current, Node: n}, got, err)
assertQueryResult(t, nilValue(), &ValueNotFound{Selector: n.Selector.Current, PreviousValue: n.Previous.Value}, got, err)
})
}
@@ -104,7 +104,7 @@ func TestFindValueDynamic(t *testing.T) {
&EqualCondition{Key: "name", Value: "x"},
}
got, err := findValueDynamic(n, false)
assertQueryResult(t, nilValue(), &ValueNotFound{Selector: n.Selector.Current, Node: n}, got, err)
assertQueryResult(t, nilValue(), &ValueNotFound{Selector: n.Selector.Current, PreviousValue: n.Previous.Value}, got, err)
})
t.Run("NotFoundWithCreate", func(t *testing.T) {
n := getNodeWithValue([]interface{}{})
@@ -154,7 +154,7 @@ func TestFindValue(t *testing.T) {
})
t.Run("UnsupportedSelector", func(t *testing.T) {
n := getNodeWithValue([]interface{}{})
n.Selector.Type = "BAD"
n.Selector.Raw = "BAD"
got, err := findValue(n, false)
assertQueryResult(t, nilValue(), &UnsupportedSelector{Selector: "BAD"}, got, err)
})

279
node_query_multiple.go Normal file
View File

@@ -0,0 +1,279 @@
package dasel
import (
"fmt"
"reflect"
)
// QueryMultiple uses the given selector to query the current node for every match
// possible and returns all of the end nodes.
func (n *Node) QueryMultiple(selector string) ([]*Node, error) {
n.Selector.Remaining = selector
if err := buildFindMultipleChain(n); err != nil {
return nil, err
}
return lastNodes(n), nil
}
// lastNodes returns a list of all of the last nodes.
func lastNodes(n *Node) []*Node {
if len(n.NextMultiple) == 0 {
return []*Node{n}
}
var res []*Node
for _, nextNode := range n.NextMultiple {
res = append(res, lastNodes(nextNode)...)
}
return res
}
func buildFindMultipleChain(n *Node) error {
if n.Selector.Remaining == "" {
// We've reached the end
return nil
}
var err error
// Parse the selector.
nextSelector, err := ParseSelector(n.Selector.Remaining)
if err != nil {
return fmt.Errorf("failed to parse selector: %w", err)
}
// Populate the value for the new node.
n.NextMultiple, err = findNodes(nextSelector, n, false)
if err != nil {
return fmt.Errorf("could not find multiple value: %w", err)
}
for _, next := range n.NextMultiple {
// Add the back reference
next.Previous = n
if err := buildFindMultipleChain(next); err != nil {
return err
}
}
return nil
}
// findNodesProperty finds the value for the given node using the property selector
// information.
func findNodesProperty(selector Selector, previousValue reflect.Value, createIfNotExists bool) ([]*Node, error) {
if !isValid(previousValue) {
return nil, &UnexpectedPreviousNilValue{Selector: selector.Raw}
}
value := unwrapValue(previousValue)
if value.Kind() == reflect.Map {
node := &Node{
Value: nilValue(),
Selector: selector,
}
for _, key := range value.MapKeys() {
if fmt.Sprint(key.Interface()) == selector.Property {
node.Value = value.MapIndex(key)
return []*Node{node}, nil
}
}
if createIfNotExists {
return []*Node{node}, nil
}
return nil, &ValueNotFound{Selector: selector.Current, PreviousValue: previousValue}
}
return nil, &UnsupportedTypeForSelector{Selector: selector, Value: previousValue.Type().Kind()}
}
// findNodesIndex finds the value for the given node using the index selector
// information.
func findNodesIndex(selector Selector, previousValue reflect.Value, createIfNotExists bool) ([]*Node, error) {
if !isValid(previousValue) {
return nil, &UnexpectedPreviousNilValue{Selector: selector.Raw}
}
value := unwrapValue(previousValue)
if value.Kind() == reflect.Slice {
node := &Node{
Value: nilValue(),
Selector: selector,
}
valueLen := value.Len()
if selector.Index >= 0 && selector.Index < valueLen {
node.Value = value.Index(selector.Index)
return []*Node{node}, nil
}
if createIfNotExists {
return []*Node{node}, nil
}
return nil, &ValueNotFound{Selector: selector.Current, PreviousValue: previousValue}
}
return nil, &UnsupportedTypeForSelector{Selector: selector, Value: value.Kind()}
}
// findNextAvailableIndexNodes finds the value for the given node using the index selector
// information.
func findNextAvailableIndexNodes(selector Selector, previousValue reflect.Value, createIfNotExists bool) ([]*Node, error) {
if !createIfNotExists {
// Next available index isn't supported unless it's creating the item.
return nil, &ValueNotFound{Selector: selector.Current, PreviousValue: previousValue}
}
return []*Node{
{
Value: nilValue(),
Selector: selector,
},
}, nil
}
// processFindDynamicItems is used by findNodesDynamic.
func processFindDynamicItems(selector Selector, object reflect.Value) (bool, error) {
// Loop through each condition.
allConditionsMatched := true
for _, c := range selector.Conditions {
// If the object doesn't match any checks, return a ValueNotFound.
found, err := c.Check(object)
if err != nil {
return false, err
}
if !found {
allConditionsMatched = false
break
}
}
if allConditionsMatched {
return true, nil
}
return false, nil
}
// findNodesDynamic finds the value for the given node using the dynamic selector
// information.
func findNodesDynamic(selector Selector, previousValue reflect.Value, createIfNotExists bool) ([]*Node, error) {
if !isValid(previousValue) {
return nil, &UnexpectedPreviousNilValue{Selector: selector.Raw}
}
value := unwrapValue(previousValue)
if value.Kind() == reflect.Slice {
results := make([]*Node, 0)
for i := 0; i < value.Len(); i++ {
object := value.Index(i)
found, err := processFindDynamicItems(selector, object)
if err != nil {
return nil, err
}
if found {
selector.Type = "INDEX"
selector.Index = i
results = append(results, &Node{
Value: object,
Selector: selector,
})
}
}
if len(results) > 0 {
return results, nil
}
if createIfNotExists {
selector.Type = "NEXT_AVAILABLE_INDEX"
return []*Node{
{
Value: nilValue(),
Selector: selector,
},
}, nil
}
return nil, &ValueNotFound{Selector: selector.Current, PreviousValue: previousValue}
}
return nil, &UnsupportedTypeForSelector{Selector: selector, Value: value.Kind()}
}
// findNodesAnyIndex returns a node for every value in the previous value list.
func findNodesAnyIndex(selector Selector, previousValue reflect.Value) ([]*Node, error) {
if !isValid(previousValue) {
return nil, &UnexpectedPreviousNilValue{Selector: selector.Raw}
}
value := unwrapValue(previousValue)
if value.Kind() == reflect.Slice {
results := make([]*Node, 0)
for i := 0; i < value.Len(); i++ {
object := value.Index(i)
selector.Type = "INDEX"
selector.Index = i
results = append(results, &Node{
Value: object,
Selector: selector,
})
}
if len(results) > 0 {
return results, nil
}
return nil, &ValueNotFound{Selector: selector.Current, PreviousValue: previousValue}
}
return nil, &UnsupportedTypeForSelector{Selector: selector, Value: value.Kind()}
}
func initialiseEmptyValue(selector Selector, previousValue reflect.Value) (reflect.Value, bool) {
switch selector.Type {
case "PROPERTY":
return reflect.ValueOf(map[interface{}]interface{}{}), true
case "INDEX":
return reflect.ValueOf([]interface{}{}), true
case "NEXT_AVAILABLE_INDEX":
return reflect.ValueOf([]interface{}{}), true
case "INDEX_ANY":
return reflect.ValueOf([]interface{}{}), true
case "DYNAMIC":
return reflect.ValueOf([]interface{}{}), true
}
return previousValue, false
}
// findNodes returns all of the nodes from the previous value that match the given selector.
func findNodes(selector Selector, previousNode *Node, createIfNotExists bool) ([]*Node, error) {
initialised := false
if createIfNotExists && !isValid(previousNode.Value) {
previousNode.Value, initialised = initialiseEmptyValue(selector, previousNode.Value)
}
var res []*Node
var err error
switch selector.Type {
case "PROPERTY":
res, err = findNodesProperty(selector, previousNode.Value, createIfNotExists)
case "INDEX":
res, err = findNodesIndex(selector, previousNode.Value, createIfNotExists)
case "NEXT_AVAILABLE_INDEX":
res, err = findNextAvailableIndexNodes(selector, previousNode.Value, createIfNotExists)
case "INDEX_ANY":
res, err = findNodesAnyIndex(selector, previousNode.Value)
case "DYNAMIC":
res, err = findNodesDynamic(selector, previousNode.Value, createIfNotExists)
default:
err = &UnsupportedSelector{Selector: selector.Raw}
}
if err != nil {
return nil, err
}
if initialised && res != nil && len(res) > 0 {
for _, n := range res {
n.wasInitialised = initialised
}
}
return res, nil
}

View File

@@ -0,0 +1,137 @@
package dasel
import (
"reflect"
"testing"
)
func assertQueryMultipleResult(t *testing.T, exp []reflect.Value, expErr error, got []*Node, gotErr error) bool {
if !assertErrResult(t, expErr, gotErr) {
return false
}
gotVals := make([]reflect.Value, len(got))
if len(got) > 0 {
for i, n := range got {
gotVals[i] = n.Value
}
}
if !reflect.DeepEqual(exp, gotVals) {
t.Errorf("expected result %v, got %v", exp, gotVals)
return false
}
return true
}
func TestFindNodesProperty(t *testing.T) {
t.Run("NilValue", func(t *testing.T) {
selector := Selector{Current: ".", Raw: "."}
got, err := findNodesProperty(selector, nilValue(), false)
assertQueryMultipleResult(t, []reflect.Value{}, &UnexpectedPreviousNilValue{Selector: "."}, got, err)
})
t.Run("NotFound", func(t *testing.T) {
previousValue := reflect.ValueOf(map[string]interface{}{})
selector := Selector{Current: "x"}
got, err := findNodesProperty(selector, previousValue, false)
assertQueryMultipleResult(t, []reflect.Value{}, &ValueNotFound{Selector: selector.Current, PreviousValue: previousValue}, got, err)
})
}
func TestFindNodesIndex(t *testing.T) {
t.Run("NilValue", func(t *testing.T) {
selector := Selector{Current: ".", Raw: "."}
got, err := findNodesIndex(selector, nilValue(), false)
assertQueryMultipleResult(t, []reflect.Value{}, &UnexpectedPreviousNilValue{Selector: "."}, got, err)
})
t.Run("NotFound", func(t *testing.T) {
selector := Selector{Current: "[0]", Index: 0, Raw: ".[0]"}
previousValue := reflect.ValueOf([]interface{}{})
got, err := findNodesIndex(selector, previousValue, false)
assertQueryMultipleResult(t, []reflect.Value{}, &ValueNotFound{Selector: "[0]", PreviousValue: previousValue}, got, err)
})
t.Run("UnsupportedType", func(t *testing.T) {
selector := Selector{Current: "[0]", Index: 0, Raw: ".[0]"}
previousValue := reflect.ValueOf(map[string]interface{}{})
got, err := findNodesIndex(selector, previousValue, false)
assertQueryMultipleResult(t, []reflect.Value{}, &UnsupportedTypeForSelector{Selector: selector, Value: previousValue.Kind()}, got, err)
})
}
func TestFindNextAvailableIndexNodes(t *testing.T) {
t.Run("NotFound", func(t *testing.T) {
previousValue := reflect.ValueOf([]interface{}{})
selector := Selector{Current: "[0]", Index: 0}
got, err := findNextAvailableIndexNodes(selector, previousValue, false)
assertQueryMultipleResult(t, []reflect.Value{}, &ValueNotFound{Selector: selector.Current, PreviousValue: previousValue}, got, err)
})
}
func TestFindNodesDynamic(t *testing.T) {
t.Run("NilValue", func(t *testing.T) {
previousValue := reflect.ValueOf(nil)
selector := Selector{Raw: "."}
got, err := findNodesDynamic(selector, previousValue, false)
assertQueryMultipleResult(t, []reflect.Value{}, &UnexpectedPreviousNilValue{Selector: "."}, got, err)
})
t.Run("NotFound", func(t *testing.T) {
previousValue := reflect.ValueOf([]interface{}{})
selector := Selector{
Current: "(name=x)",
Conditions: []Condition{
&EqualCondition{Key: "name", Value: "x"},
},
}
got, err := findNodesDynamic(selector, previousValue, false)
assertQueryMultipleResult(t, []reflect.Value{}, &ValueNotFound{Selector: selector.Current, PreviousValue: previousValue}, got, err)
})
t.Run("NotFoundWithCreate", func(t *testing.T) {
previousValue := reflect.ValueOf([]interface{}{})
selector := Selector{
Type: "NEXT_AVAILABLE_INDEX",
Current: "(name=x)",
Conditions: []Condition{
&EqualCondition{Key: "name", Value: "x"},
},
}
got, err := findNodesDynamic(selector, previousValue, true)
if !assertQueryMultipleResult(t, []reflect.Value{nilValue()}, nil, got, err) {
return
}
if exp, got := "NEXT_AVAILABLE_INDEX", selector.Type; exp != got {
t.Errorf("expected type of %s, got %s", exp, got)
return
}
})
t.Run("UnsupportedCheckType", func(t *testing.T) {
previousValue := reflect.ValueOf([]interface{}{
1,
})
selector := Selector{
Current: "(name=x)",
Conditions: []Condition{
&EqualCondition{Key: "name", Value: "x"},
},
}
got, err := findNodesDynamic(selector, previousValue, false)
assertQueryMultipleResult(t, []reflect.Value{}, &UnhandledCheckType{Value: previousValue.Kind().String()}, got, err)
})
t.Run("UnsupportedType", func(t *testing.T) {
previousValue := reflect.ValueOf(map[string]interface{}{})
selector := Selector{
Current: "(name=x)",
Conditions: []Condition{
&EqualCondition{Key: "name", Value: "x"},
},
}
got, err := findNodesDynamic(selector, previousValue, false)
assertQueryMultipleResult(t, []reflect.Value{}, &UnsupportedTypeForSelector{Selector: selector, Value: previousValue.Kind()}, got, err)
})
}
func TestFindNodes(t *testing.T) {
t.Run("UnsupportedSelector", func(t *testing.T) {
previousNode := &Node{Value: reflect.ValueOf([]interface{}{})}
selector := Selector{Raw: "BAD"}
got, err := findNodes(selector, previousNode, false)
assertQueryMultipleResult(t, []reflect.Value{}, &UnsupportedSelector{Selector: "BAD"}, got, err)
})
}

View File

@@ -1,6 +1,7 @@
package dasel_test
import (
"errors"
"fmt"
"github.com/tomwright/dasel"
"github.com/tomwright/dasel/internal/storage"
@@ -41,6 +42,13 @@ func TestParseSelector(t *testing.T) {
t.Errorf("expected error %v, got %v", exp, err)
}
})
t.Run("InvalidDynamicBracketCount", func(t *testing.T) {
_, err := dasel.ParseSelector(".((name=x)")
exp := dasel.ErrDynamicSelectorBracketMismatch
if err == nil || !errors.Is(err, exp) {
t.Errorf("expected error %v, got %v", exp, err)
}
})
t.Run("InvalidDynamicComparison", func(t *testing.T) {
_, err := dasel.ParseSelector(".(x<2)")
exp := &dasel.UnknownComparisonOperatorErr{Operator: "<"}
@@ -50,6 +58,260 @@ func TestParseSelector(t *testing.T) {
})
}
func TestNode_QueryMultiple(t *testing.T) {
value := []map[string]interface{}{
{
"name": "Tom",
"age": "27",
},
{
"name": "Jim",
"age": "27",
},
{
"name": "Amelia",
"age": "25",
},
}
extractValues := func(nodes []*dasel.Node) []interface{} {
gotValues := make([]interface{}, len(nodes))
for i, n := range nodes {
gotValues[i] = n.InterfaceValue()
}
return gotValues
}
t.Run("SingleResult", func(t *testing.T) {
nodes, err := dasel.New(value).QueryMultiple(".[0].name")
if err != nil {
t.Errorf("unexpected query error: %s", err)
return
}
gotValues := extractValues(nodes)
expValues := []interface{}{
"Tom",
}
if !reflect.DeepEqual(expValues, gotValues) {
t.Errorf("expected %v, got %v", expValues, gotValues)
}
})
t.Run("SingleResultDynamic", func(t *testing.T) {
nodes, err := dasel.New(value).QueryMultiple(".(age=25).name")
if err != nil {
t.Errorf("unexpected query error: %s", err)
return
}
gotValues := extractValues(nodes)
expValues := []interface{}{
"Amelia",
}
if !reflect.DeepEqual(expValues, gotValues) {
t.Errorf("expected %v, got %v", expValues, gotValues)
}
})
t.Run("MultipleResultDynamic", func(t *testing.T) {
nodes, err := dasel.New(value).QueryMultiple(".(age=27).name")
if err != nil {
t.Errorf("unexpected query error: %s", err)
return
}
gotValues := extractValues(nodes)
expValues := []interface{}{
"Tom",
"Jim",
}
if !reflect.DeepEqual(expValues, gotValues) {
t.Errorf("expected %v, got %v", expValues, gotValues)
}
})
t.Run("MultipleResultAnyIndex", func(t *testing.T) {
nodes, err := dasel.New(value).QueryMultiple(".[*].name")
if err != nil {
t.Errorf("unexpected query error: %s", err)
return
}
gotValues := extractValues(nodes)
expValues := []interface{}{
"Tom",
"Jim",
"Amelia",
}
if !reflect.DeepEqual(expValues, gotValues) {
t.Errorf("expected %v, got %v", expValues, gotValues)
}
})
}
func TestNode_PutMultiple(t *testing.T) {
t.Run("SingleResult", func(t *testing.T) {
value := []map[string]interface{}{
{
"name": "Tom",
"age": "27",
},
{
"name": "Jim",
"age": "27",
},
{
"name": "Amelia",
"age": "25",
},
}
err := dasel.New(value).PutMultiple(".[0].name", "Frank")
if err != nil {
t.Errorf("unexpected query error: %s", err)
return
}
exp := []map[string]interface{}{
{
"name": "Frank",
"age": "27",
},
{
"name": "Jim",
"age": "27",
},
{
"name": "Amelia",
"age": "25",
},
}
if !reflect.DeepEqual(exp, value) {
t.Errorf("expected %v, got %v", exp, value)
}
})
t.Run("SingleResultDynamic", func(t *testing.T) {
value := []map[string]interface{}{
{
"name": "Frank",
"age": "27",
},
{
"name": "Jim",
"age": "27",
},
{
"name": "Amelia",
"age": "25",
},
}
err := dasel.New(value).PutMultiple(".(age=25).name", "Frank")
if err != nil {
t.Errorf("unexpected query error: %s", err)
return
}
exp := []map[string]interface{}{
{
"name": "Frank",
"age": "27",
},
{
"name": "Jim",
"age": "27",
},
{
"name": "Frank",
"age": "25",
},
}
if !reflect.DeepEqual(exp, value) {
t.Errorf("expected %v, got %v", exp, value)
}
})
t.Run("MultipleResultDynamic", func(t *testing.T) {
value := []map[string]interface{}{
{
"name": "Tom",
"age": "27",
},
{
"name": "Jim",
"age": "27",
},
{
"name": "Amelia",
"age": "25",
},
}
err := dasel.New(value).PutMultiple(".(age=27).name", "Frank")
if err != nil {
t.Errorf("unexpected query error: %s", err)
return
}
exp := []map[string]interface{}{
{
"name": "Frank",
"age": "27",
},
{
"name": "Frank",
"age": "27",
},
{
"name": "Amelia",
"age": "25",
},
}
if !reflect.DeepEqual(exp, value) {
t.Errorf("expected %v, got %v", exp, value)
}
})
t.Run("MultipleResultAnyIndex", func(t *testing.T) {
value := []map[string]interface{}{
{
"name": "Tom",
"age": "27",
},
{
"name": "Jim",
"age": "27",
},
{
"name": "Amelia",
"age": "25",
},
}
err := dasel.New(value).PutMultiple(".[*].name", "Frank")
if err != nil {
t.Errorf("unexpected query error: %s", err)
return
}
exp := []map[string]interface{}{
{
"name": "Frank",
"age": "27",
},
{
"name": "Frank",
"age": "27",
},
{
"name": "Frank",
"age": "25",
},
}
if !reflect.DeepEqual(exp, value) {
t.Errorf("expected %v, got %v", exp, value)
}
})
}
func TestNode_Query(t *testing.T) {
parser, err := storage.NewParserFromFilename("./tests/assets/example.json")
if err != nil {
@@ -231,7 +493,29 @@ func putQueryTest(rootNode *dasel.Node, putSelector string, newValue interface{}
}
}
func TestNode_Put(t *testing.T) {
func putQueryMultipleTest(rootNode *dasel.Node, putSelector string, newValue interface{}, querySelector string) func(t *testing.T) {
return func(t *testing.T) {
err := rootNode.PutMultiple(putSelector, newValue)
if err != nil {
t.Errorf("unexpected put error: %v", err)
return
}
got, err := rootNode.QueryMultiple(querySelector)
if err != nil {
t.Errorf("unexpected query error: %v", err)
return
}
for _, n := range got {
if !reflect.DeepEqual(newValue, n.InterfaceValue()) {
t.Errorf("expected %v, got %v", newValue, n.InterfaceValue())
}
}
}
}
func TestNode_Put_Query(t *testing.T) {
data := map[string]interface{}{
"id": "123",
"people": []map[string]interface{}{
@@ -301,3 +585,74 @@ func TestNode_Put(t *testing.T) {
t.Run("NilChainToListNextAvailableIndex", putQueryTest(dasel.New(nil), "my.favourite.people.[]", "Tom", "my.favourite.people.[0]"))
t.Run("NilChainToDynamic", putQueryTest(dasel.New(nil), "(name=Jim).name", "Tom", "[0].name"))
}
func TestNode_PutMultiple_Query(t *testing.T) {
data := map[string]interface{}{
"id": "123",
"people": []map[string]interface{}{
{
"id": 1,
"name": "Tom",
},
{
"id": 2,
"name": "Jim",
},
},
"names": []string{
"Tom",
"Jim",
},
}
rootNode := dasel.New(data)
t.Run("InvalidSelector", func(t *testing.T) {
err := rootNode.PutMultiple("people.[a].name", "Thomas")
expErr := fmt.Errorf("failed to parse selector: %w", &dasel.InvalidIndexErr{Index: "a"})
if err == nil {
t.Errorf("expected err %v, got %v", expErr, err)
return
}
if err.Error() != expErr.Error() {
t.Errorf("expected err %v, got %v", expErr, err)
return
}
})
t.Run("ExistingSingleString", putQueryMultipleTest(rootNode, "id", "456", "id"))
t.Run("ExistingStringValue", putQueryMultipleTest(rootNode, "people.[0].name", "Thomas", "people.(id=1).name"))
t.Run("ExistingIntValue", putQueryMultipleTest(rootNode, "people.(id=1).id", 3, "people.(id=3).id"))
t.Run("NewPropertyOnExistingObject", putQueryMultipleTest(rootNode, "people.(id=3).age", 27, "people.[0].age"))
t.Run("AppendObjectToList", func(t *testing.T) {
err := rootNode.PutMultiple("people.[]", map[string]interface{}{
"id": 1,
"name": "Bob",
})
if err != nil {
t.Errorf("unexpected put error: %v", err)
return
}
got, err := rootNode.Query("people.[2].id")
if err != nil {
t.Errorf("unexpected query [1] error: %v", err)
return
}
if exp, got := 1, got.InterfaceValue().(int); exp != got {
t.Errorf("expected %d, got %d", exp, got)
}
got, err = rootNode.Query("people.[2].name")
if err != nil {
t.Errorf("unexpected query [2] error: %v", err)
return
}
if exp, got := "Bob", got.InterfaceValue().(string); exp != got {
t.Errorf("expected %s, got %s", exp, got)
}
})
t.Run("AppendStringToList", putQueryMultipleTest(rootNode, "names.[]", "Bob", "names.[2]"))
t.Run("NilRootNode", putQueryMultipleTest(dasel.New(nil), "name", "Thomas", "name"))
t.Run("NilChain", putQueryMultipleTest(dasel.New(nil), "my.name", "Thomas", "my.name"))
t.Run("NilChainToListIndex", putQueryMultipleTest(dasel.New(nil), "my.favourite.people.[0]", "Tom", "my.favourite.people.[0]"))
t.Run("NilChainToListNextAvailableIndex", putQueryMultipleTest(dasel.New(nil), "my.favourite.people.[]", "Tom", "my.favourite.people.[0]"))
t.Run("NilChainToDynamic", putQueryMultipleTest(dasel.New(nil), "(name=Jim).name", "Tom", "[0].name"))
}