From 9eb8a5ab6e6f77ec2796210405b8996f077e37e3 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Mon, 16 Nov 2020 22:44:28 +0000 Subject: [PATCH] Search selector (#43) Implement search selector --- .github/workflows/test.yaml | 2 +- README.md | 93 ++++++++++++ condition.go | 41 ------ condition_equal.go | 46 ++++++ condition_key_equal.go | 22 +++ condition_test.go | 20 +++ internal/command/put_internal_test.go | 12 ++ internal/command/root_put_test.go | 197 ++++++++++++++++++++++++++ internal/command/root_select_test.go | 75 +++++++++- node.go | 61 ++++++++ node_put.go | 5 +- node_query_multiple.go | 110 ++++++++++++-- node_test.go | 63 ++++++++ 13 files changed, 693 insertions(+), 54 deletions(-) create mode 100644 condition_equal.go create mode 100644 condition_key_equal.go diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 808a1c4..e4ffa41 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -27,4 +27,4 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos file: ./coverage.txt # optional flags: unittests # optional - fail_ci_if_error: true # optional (default = false) \ No newline at end of file + fail_ci_if_error: false # optional (default = false) \ No newline at end of file diff --git a/README.md b/README.md index 2aaadea..06a3eb3 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ Comparable to [jq](https://github.com/stedolan/jq) / [yq](https://github.com/kis * [Any index](#any-index) * [Dynamic](#dynamic) * [Using queries in dynamic selectors](#using-queries-in-dynamic-selectors) + * [Search](#search) * [Examples](#examples) * [General](#general) * [Filter JSON API results](#filter-json-api-results) @@ -421,6 +422,40 @@ Once decoded, you can access them using any of the standard selectors provided b ``` Using [github.com/clbanning/mxj](https://github.com/clbanning/mxj). +#### XML Documents + +XML documents within dasel are stored as a map of values. + +This is just how dasel stores data and is required for the general functionality to work. An example of a simple documents representation is as follows: + +``` + + Tom + 27 + +``` + +``` +map[ + Person:map[ + -active:true + Age:27 + Name:map[ + #text:Tom + -main:true + ] + ] +] +``` + +In general this won't affect you, but on the odd occasion in specific instances it could lead to unexpected output. + +If you are struggling with this please raise an issue for support. This will also help me know when the docs aren't sufficient. + +##### Debugging + +You can run select commands with the `--plain` flag to see the raw data that is stored within dasel. This can help you figure out the exact properties you may need to target when it isn't immediately obvious. + #### Arrays/Lists Due to the way that XML is decoded, dasel can only detect something as a list if there are at least 2 items. @@ -565,6 +600,64 @@ The resolution of that query looks something like this: .users.[0].name.first ``` +### Search + +Search selectors recursively search all the data below the current node and returns all the results - this means they can only be used in multi select/put commands. + +The syntax is as follows: +``` +.(?:key=value) +``` + +If `key` is: +- `.` or `value` - dasel checks if the current nodes value is `value`. +- `-` or `keyValue` - dasel checks if the current nodes key/name/index value is `value`. +- Else dasel uses the `key` as a selector itself and compares the result against `value`. + +#### Search Example + +``` +{ + "users": [ + { + "primary": true, + "name": { + "first": "Tom", + "last": "Wright" + } + }, + { + "primary": false, + "extra": { + "name": { + "first": "Joe", + "last": "Blogs" + } + }, + "name": { + "first": "Jim", + "last": "Wright" + } + } + ] +} +``` + +Search for all objects with a key of `name` and output the first name of each: +``` +dasel -p json -m '.(?:-=name).first' +"Tom" +"Joe" +"Jim" +``` + +Search for all objects with a last name of `Wright` and output the first name of each: +``` +dasel -p json -m '.(?:name.last=Wright).name.first' +"Tom" +"Jim" +``` + ## Examples ### General diff --git a/condition.go b/condition.go index 6cffa96..305451d 100644 --- a/condition.go +++ b/condition.go @@ -1,50 +1,9 @@ package dasel import ( - "errors" - "fmt" "reflect" ) -// EqualCondition lets you check for an exact match. -type EqualCondition struct { - // Key is the key of the value to check against. - Key string - // Value is the value we are looking for. - Value string -} - -// Check checks to see if other contains the required key value pair. -func (c EqualCondition) Check(other reflect.Value) (bool, error) { - if !other.IsValid() { - return false, &UnhandledCheckType{Value: nil} - } - - value := unwrapValue(other) - - if c.Key == "value" || c.Key == "." { - return fmt.Sprint(value.Interface()) == c.Value, nil - } - - switch value.Kind() { - case reflect.Map, reflect.Slice: - subRootNode := New(value.Interface()) - foundNode, err := subRootNode.Query(c.Key) - if err != nil { - var valueNotFound = &ValueNotFound{} - if errors.As(err, &valueNotFound) { - return false, nil - } - - return false, fmt.Errorf("subquery failed: %w", err) - } - - return fmt.Sprint(foundNode.InterfaceValue()) == c.Value, nil - } - - return false, &UnhandledCheckType{Value: value.Kind().String()} -} - // Condition defines a Check we can use within dynamic selectors. type Condition interface { Check(other reflect.Value) (bool, error) diff --git a/condition_equal.go b/condition_equal.go new file mode 100644 index 0000000..9c7826f --- /dev/null +++ b/condition_equal.go @@ -0,0 +1,46 @@ +package dasel + +import ( + "errors" + "fmt" + "reflect" +) + +// EqualCondition lets you check for an exact match. +type EqualCondition struct { + // Key is the key of the value to check against. + Key string + // Value is the value we are looking for. + Value string +} + +// Check checks to see if other contains the required key value pair. +func (c EqualCondition) Check(other reflect.Value) (bool, error) { + if !other.IsValid() { + return false, &UnhandledCheckType{Value: nil} + } + + value := unwrapValue(other) + + if c.Key == "value" || c.Key == "." { + return fmt.Sprint(value.Interface()) == c.Value, nil + } + + switch value.Kind() { + case reflect.Map, reflect.Slice: + subRootNode := New(value.Interface()) + foundNode, err := subRootNode.Query(c.Key) + if err != nil { + var valueNotFound = &ValueNotFound{} + if errors.As(err, &valueNotFound) { + return false, nil + } + + return false, fmt.Errorf("subquery failed: %w", err) + } + + return fmt.Sprint(foundNode.InterfaceValue()) == c.Value, nil + } + + return false, &UnhandledCheckType{Value: value.Kind().String()} +} diff --git a/condition_key_equal.go b/condition_key_equal.go new file mode 100644 index 0000000..65de611 --- /dev/null +++ b/condition_key_equal.go @@ -0,0 +1,22 @@ +package dasel + +import ( + "reflect" +) + +// KeyEqualCondition lets you check for an exact match. +type KeyEqualCondition struct { + // Value is the value we are looking for. + Value string +} + +// Check checks to see if other contains the required key value pair. +func (c KeyEqualCondition) Check(other reflect.Value) (bool, error) { + if !other.IsValid() { + return false, &UnhandledCheckType{Value: nil} + } + + value := unwrapValue(other) + + return c.Value == value.String(), nil +} diff --git a/condition_test.go b/condition_test.go index 8310826..66fd806 100644 --- a/condition_test.go +++ b/condition_test.go @@ -73,3 +73,23 @@ func TestEqualCondition_Check(t *testing.T) { false, &dasel.UnhandledCheckType{Value: ""}, )) } + +func TestKeyEqualCondition_Check(t *testing.T) { + c := &dasel.KeyEqualCondition{Value: "name"} + + t.Run("MatchStringValue", conditionTest( + c, + "name", + true, nil, + )) + t.Run("NoMatchMissingKey", conditionTest( + c, + "asd", + false, nil, + )) + t.Run("Nil", conditionTest( + c, + nil, + false, &dasel.UnhandledCheckType{Value: nil}, + )) +} diff --git a/internal/command/put_internal_test.go b/internal/command/put_internal_test.go index 31b2c19..1a30e55 100644 --- a/internal/command/put_internal_test.go +++ b/internal/command/put_internal_test.go @@ -189,6 +189,18 @@ func TestPut(t *testing.T) { t.Errorf("unexpected error: %v", err) } }) + t.Run("InvalidVarType", func(t *testing.T) { + err := runGenericPutCommand(genericPutOptions{Parser: "yaml", ValueType: "int", Value: "asd", Reader: bytes.NewBuffer([]byte{})}, nil) + if err == nil || err.Error() != "could not parse int [asd]: strconv.ParseInt: parsing \"asd\": invalid syntax" { + t.Errorf("unexpected error: %v", err) + } + }) + t.Run("FailedWrite", func(t *testing.T) { + err := runGenericPutCommand(genericPutOptions{Parser: "yaml", ValueType: "string", Selector: ".name", Value: "asd", Reader: bytes.NewBuffer([]byte{}), Writer: &failingWriter{}}, nil) + if err == nil || err.Error() != "could not write output: could not write to output file: could not write data: i am meant to fail at writing" { + t.Errorf("unexpected error: %v", err) + } + }) t.Run("ObjectMissingParserFlag", func(t *testing.T) { err := runPutObjectCommand(putObjectOpts{}, nil) if err == nil || err.Error() != "parser flag required when reading from stdin" { diff --git a/internal/command/root_put_test.go b/internal/command/root_put_test.go index df38bc0..6cdd58b 100644 --- a/internal/command/root_put_test.go +++ b/internal/command/root_put_test.go @@ -4,6 +4,7 @@ import ( "bytes" "github.com/tomwright/dasel/internal/command" "io/ioutil" + "os" "strings" "testing" ) @@ -101,6 +102,51 @@ func putTest(in string, varType string, parser string, selector string, value st } } +func putFileTest(in string, varType string, parser string, selector string, value string, out string, outFile string, expErr error, additionalArgs ...string) func(t *testing.T) { + return func(t *testing.T) { + defer func() { + _ = os.Remove(outFile) + }() + cmd := command.NewRootCMD() + + args := []string{ + "put", varType, + } + args = append(args, additionalArgs...) + args = append(args, "-p", parser, "-o", outFile, selector, value) + + cmd.SetIn(strings.NewReader(in)) + cmd.SetArgs(args) + + err := cmd.Execute() + + if expErr == nil && err != nil { + t.Errorf("expected err %v, got %v", expErr, err) + return + } + if expErr != nil && err == nil { + t.Errorf("expected err %v, got %v", expErr, err) + return + } + if expErr != nil && err != nil && err.Error() != expErr.Error() { + t.Errorf("expected err %v, got %v", expErr, err) + return + } + + output, err := ioutil.ReadFile(outFile) + if err != nil { + t.Errorf("could not read output file: %s", err) + return + } + + out = strings.TrimSpace(out) + outputStr := strings.TrimSpace(string(output)) + if out != outputStr { + t.Errorf("expected result %v, got %v", out, outputStr) + } + } +} + func TestRootCMD_Put_JSON(t *testing.T) { t.Run("String", putStringTest(`{ "id": "x" @@ -268,6 +314,150 @@ func TestRootCMD_Put_JSON(t *testing.T) { "value": "X" } ]`, nil, "-m")) + + t.Run("KeySearch", putStringTest(`{ + "users": [ + { + "primary": true, + "name": { + "first": "Tom", + "last": "Wright" + } + }, + { + "primary": false, + "extra": { + "name": { + "first": "Joe", + "last": "Blogs" + } + }, + "name": { + "first": "Jim", + "last": "Wright" + } + } + ] +}`, "json", ".(?:-=name).first", "Bobby", `{ + "users": [ + { + "name": { + "first": "Bobby", + "last": "Wright" + }, + "primary": true + }, + { + "extra": { + "name": { + "first": "Bobby", + "last": "Blogs" + } + }, + "name": { + "first": "Bobby", + "last": "Wright" + }, + "primary": false + } + ] +}`, nil, "-m")) + + t.Run("ValueSearch", putStringTest(`{ + "users": [ + { + "primary": true, + "name": { + "first": "Tom", + "last": "Wright" + } + }, + { + "primary": false, + "extra": { + "name": { + "first": "Joe", + "last": "Blogs" + } + }, + "name": { + "first": "Jim", + "last": "Wright" + } + } + ] +}`, "json", ".(?:.=Wright)", "Wrighto", `{ + "users": [ + { + "name": { + "first": "Tom", + "last": "Wrighto" + }, + "primary": true + }, + { + "extra": { + "name": { + "first": "Joe", + "last": "Blogs" + } + }, + "name": { + "first": "Jim", + "last": "Wrighto" + }, + "primary": false + } + ] +}`, nil, "-m")) + + t.Run("KeyValueSearch", putStringTest(`{ + "users": [ + { + "primary": true, + "name": { + "first": "Tom", + "last": "Wright" + } + }, + { + "primary": false, + "extra": { + "name": { + "first": "Joe", + "last": "Blogs" + } + }, + "name": { + "first": "Jim", + "last": "Wright" + } + } + ] +}`, "json", ".(?:.last=Wright).first", "Fred", `{ + "users": [ + { + "name": { + "first": "Fred", + "last": "Wright" + }, + "primary": true + }, + { + "extra": { + "name": { + "first": "Joe", + "last": "Blogs" + } + }, + "name": { + "first": "Fred", + "last": "Wright" + }, + "primary": false + } + ] +}`, nil, "-m")) } func TestRootCMD_Put_YAML(t *testing.T) { @@ -278,6 +468,13 @@ name: "Tom" id: "y" name: Tom `, nil)) + t.Run("StringInFile", putFileTest(` +id: "x" +name: "Tom" +`, "string", "yaml", "id", "y", ` +id: "y" +name: Tom +`, "TestRootCMD_Put_YAML_out.yaml", nil)) t.Run("Int", putIntTest(` id: 123 `, "yaml", "id", "456", ` diff --git a/internal/command/root_select_test.go b/internal/command/root_select_test.go index 07e3e20..5c9bf0b 100644 --- a/internal/command/root_select_test.go +++ b/internal/command/root_select_test.go @@ -2,6 +2,7 @@ package command_test import ( "bytes" + "fmt" "github.com/tomwright/dasel/internal/command" "io/ioutil" "strings" @@ -134,7 +135,35 @@ func TestRootCMD_Select(t *testing.T) { )) } -func selectTest(in string, parser string, selector string, out string, expErr error, additionalArgs ...string) func(t *testing.T) { +func selectTest(in string, parser string, selector string, output string, expErr error, additionalArgs ...string) func(t *testing.T) { + return selectTestCheck(in, parser, selector, func(out string) error { + if out != output { + return fmt.Errorf("expected %v, got %v", output, out) + } + return nil + }, expErr, additionalArgs...) +} + +func selectTestContainsLines(in string, parser string, selector string, output []string, expErr error, additionalArgs ...string) func(t *testing.T) { + return selectTestCheck(in, parser, selector, func(out string) error { + splitOut := strings.Split(out, "\n") + for _, s := range output { + found := false + for _, got := range splitOut { + if s == got { + found = true + break + } + } + if !found { + return fmt.Errorf("required value not found: %s", s) + } + } + return nil + }, expErr, additionalArgs...) +} + +func selectTestCheck(in string, parser string, selector string, checkFn func(out string) error, expErr error, additionalArgs ...string) func(t *testing.T) { return func(t *testing.T) { cmd := command.NewRootCMD() outputBuffer := bytes.NewBuffer([]byte{}) @@ -172,8 +201,8 @@ func selectTest(in string, parser string, selector string, out string, expErr er return } - if out != string(output) { - t.Errorf("expected result %v, got %v", out, string(output)) + if err := checkFn(string(output)); err != nil { + t.Errorf("unexpected output: %s", err) } } } @@ -296,6 +325,31 @@ func TestRootCmd_Select_JSON(t *testing.T) { } ] }`, "json", ".users.(.addresses.(.primary=true).number=123)(.name.last=Wright).name.first", newline(`"Tom"`), nil)) + + t.Run("KeySearch", selectTestContainsLines(`{ + "users": [ + { + "primary": true, + "name": { + "first": "Tom", + "last": "Wright" + } + }, + { + "primary": false, + "extra": { + "name": { + "first": "Joe", + "last": "Blogs" + } + }, + "name": { + "first": "Jim", + "last": "Wright" + } + } + ] +}`, "json", ".(?:-=name).first", []string{`"Tom"`, `"Joe"`, `"Jim"`}, nil, "-m")) } func TestRootCmd_Select_YAML(t *testing.T) { @@ -413,6 +467,21 @@ func TestRootCMD_Select_XML(t *testing.T) { t.Run("DynamicString", selectTest(xmlData, "xml", ".data.details.addresses.(postcode=XXX XXX).street", "101 Some Street\n", nil)) t.Run("DynamicString", selectTest(xmlData, "xml", ".data.details.addresses.(postcode=YYY YYY).street", "34 Another Street\n", nil)) t.Run("Attribute", selectTest(xmlData, "xml", ".data.details.addresses.(-primary=true).street", "101 Some Street\n", nil)) + + t.Run("KeySearch", selectTestContainsLines(` + + + + + + + + + + + + +`, "xml", ".food.(?:keyValue=apple).-color", []string{"yellow", "red", "green"}, nil, "-m")) } func TestRootCMD_Select_CSV(t *testing.T) { diff --git a/node.go b/node.go index d06bddb..7b24c52 100644 --- a/node.go +++ b/node.go @@ -27,6 +27,19 @@ type Selector struct { Conditions []Condition `json:"conditions,omitempty"` } +// Copy returns a copy of the selector. +func (s Selector) Copy() Selector { + return Selector{ + Raw: s.Raw, + Current: s.Current, + Remaining: s.Remaining, + Type: s.Type, + Property: s.Property, + Index: s.Index, + Conditions: s.Conditions, + } +} + // Node represents a single node in the chain of nodes for a selector. type Node struct { // Previous is the previous node in the chain. @@ -112,6 +125,51 @@ func ParseSelector(selector string) (Selector, error) { nextSel := strings.TrimPrefix(sel.Current, ".") switch { + case strings.HasPrefix(nextSel, "(?:") && strings.HasSuffix(nextSel, ")"): + sel.Type = "SEARCH" + + dynamicGroups, err := DynamicSelectorToGroups(nextSel) + if err != nil { + return sel, err + } + if len(dynamicGroups) != 1 { + return sel, fmt.Errorf("require exactly 1 group in search selector") + } + + for _, g := range dynamicGroups { + m := dynamicSelectorRegexp.FindStringSubmatch(g) + if m == nil { + return sel, fmt.Errorf("invalid search format") + } + + m[1] = strings.TrimPrefix(m[1], "?:") + + var cond Condition + switch m[1] { + case "-", "keyValue": + switch m[2] { + case "=": + cond = &KeyEqualCondition{ + Value: m[3], + } + default: + return sel, &UnknownComparisonOperatorErr{Operator: m[2]} + } + default: + switch m[2] { + case "=": + cond = &EqualCondition{ + Key: strings.TrimPrefix(m[1], "?:"), + Value: m[3], + } + default: + return sel, &UnknownComparisonOperatorErr{Operator: m[2]} + } + } + + sel.Conditions = append(sel.Conditions, cond) + } + case strings.HasPrefix(nextSel, "(") && strings.HasSuffix(nextSel, ")"): sel.Type = "DYNAMIC" dynamicGroups, err := DynamicSelectorToGroups(nextSel) @@ -121,6 +179,9 @@ func ParseSelector(selector string) (Selector, error) { for _, g := range dynamicGroups { m := dynamicSelectorRegexp.FindStringSubmatch(g) + if m == nil { + return sel, fmt.Errorf("invalid search format") + } var cond Condition switch m[2] { diff --git a/node_put.go b/node_put.go index c365b3a..368f0a0 100644 --- a/node_put.go +++ b/node_put.go @@ -99,7 +99,10 @@ func buildPutMultipleChain(n *Node) error { for _, next := range n.NextMultiple { // Add the back reference - next.Previous = n + if next.Previous == nil { + // This can already be set in some cases - SEARCH. + next.Previous = n + } if err := buildPutMultipleChain(next); err != nil { return err diff --git a/node_query_multiple.go b/node_query_multiple.go index c947787..7032b76 100644 --- a/node_query_multiple.go +++ b/node_query_multiple.go @@ -51,7 +51,10 @@ func buildFindMultipleChain(n *Node) error { for _, next := range n.NextMultiple { // Add the back reference - next.Previous = n + if next.Previous == nil { + // This can already be set in some cases - SEARCH. + next.Previous = n + } if err := buildFindMultipleChain(next); err != nil { return err @@ -197,6 +200,101 @@ func findNodesDynamic(selector Selector, previousValue reflect.Value, createIfNo return nil, &UnsupportedTypeForSelector{Selector: selector, Value: value.Kind()} } +func findNodesSearchRecursiveSubNode(selector Selector, subNode *Node, key string, createIfNotExists bool) ([]*Node, error) { + subResults, err := findNodesSearchRecursive(selector, subNode, createIfNotExists, false) + if err != nil { + return nil, fmt.Errorf("could not find nodes search recursive: %w", err) + } + + // Loop through each condition. + allConditionsMatched := true +sliceConditionLoop: + for _, c := range selector.Conditions { + found := false + var err error + + switch cond := c.(type) { + case *KeyEqualCondition: + found, err = cond.Check(reflect.ValueOf(key)) + default: + found, err = cond.Check(subNode.Value) + } + if err != nil || !found { + allConditionsMatched = false + break sliceConditionLoop + } + } + + results := make([]*Node, 0) + + if allConditionsMatched { + results = append(results, subNode) + } + if len(subResults) > 0 { + results = append(results, subResults...) + } + + return results, nil +} + +// findNodesSearchRecursive iterates through the value of the previous node and creates a new node for each element. +// If any of those nodes match the checks they are returned. +func findNodesSearchRecursive(selector Selector, previousNode *Node, createIfNotExists bool, firstNode bool) ([]*Node, error) { + if !isValid(previousNode.Value) { + return nil, &UnexpectedPreviousNilValue{Selector: selector.Raw} + } + value := unwrapValue(previousNode.Value) + + results := make([]*Node, 0) + + switch value.Kind() { + case reflect.Slice: + for i := 0; i < value.Len(); i++ { + object := value.Index(i) + + subNode := &Node{ + Previous: previousNode, + Value: object, + Selector: selector.Copy(), + } + subNode.Selector.Type = "INDEX" + subNode.Selector.Index = i + + if newResults, err := findNodesSearchRecursiveSubNode(selector, subNode, fmt.Sprint(subNode.Selector.Index), createIfNotExists); err != nil { + return nil, err + } else { + results = append(results, newResults...) + } + } + + case reflect.Map: + for _, key := range value.MapKeys() { + object := value.MapIndex(key) + + subNode := &Node{ + Previous: previousNode, + Value: object, + Selector: selector.Copy(), + } + subNode.Selector.Type = "PROPERTY" + subNode.Selector.Property = fmt.Sprint(key.Interface()) + + if newResults, err := findNodesSearchRecursiveSubNode(selector, subNode, fmt.Sprint(subNode.Selector.Property), createIfNotExists); err != nil { + return nil, err + } else { + results = append(results, newResults...) + } + } + } + + return results, nil +} + +// findNodesSearch finds all available nodes by recursively searching the previous value. +func findNodesSearch(selector Selector, previousNode *Node, createIfNotExists bool) ([]*Node, error) { + return findNodesSearchRecursive(selector, previousNode, createIfNotExists, true) +} + // findNodesAnyIndex returns a node for every value in the previous value list. func findNodesAnyIndex(selector Selector, previousValue reflect.Value) ([]*Node, error) { if !isValid(previousValue) { @@ -228,13 +326,7 @@ func initialiseEmptyValue(selector Selector, previousValue reflect.Value) reflec switch selector.Type { case "PROPERTY": return reflect.ValueOf(map[interface{}]interface{}{}) - case "INDEX": - return reflect.ValueOf([]interface{}{}) - case "NEXT_AVAILABLE_INDEX": - return reflect.ValueOf([]interface{}{}) - case "INDEX_ANY": - return reflect.ValueOf([]interface{}{}) - case "DYNAMIC": + case "INDEX", "NEXT_AVAILABLE_INDEX", "INDEX_ANY", "DYNAMIC": return reflect.ValueOf([]interface{}{}) } return previousValue @@ -260,6 +352,8 @@ func findNodes(selector Selector, previousNode *Node, createIfNotExists bool) ([ res, err = findNodesAnyIndex(selector, previousNode.Value) case "DYNAMIC": res, err = findNodesDynamic(selector, previousNode.Value, createIfNotExists) + case "SEARCH": + res, err = findNodesSearch(selector, previousNode, createIfNotExists) default: err = &UnsupportedSelector{Selector: selector.Raw} } diff --git a/node_test.go b/node_test.go index f01e3ed..ed909c3 100644 --- a/node_test.go +++ b/node_test.go @@ -34,6 +34,18 @@ var ( } ) +func testParseSelector(in string, exp dasel.Selector) func(t *testing.T) { + return func(t *testing.T) { + got, err := dasel.ParseSelector(in) + if err != nil { + t.Errorf("unexpected error: %s", err) + } + if !reflect.DeepEqual(exp, got) { + t.Errorf("expected %v, got %v", exp, got) + } + } +} + func TestParseSelector(t *testing.T) { t.Run("NonIntIndex", func(t *testing.T) { _, err := dasel.ParseSelector(".[a]") @@ -56,6 +68,57 @@ func TestParseSelector(t *testing.T) { t.Errorf("expected error %v, got %v", exp, err) } }) + t.Run("MultipleSearchGroups", func(t *testing.T) { + _, err := dasel.ParseSelector(".(?:a=b)(a=b)") + exp := "require exactly 1 group in search selector" + if err == nil || err.Error() != exp { + t.Errorf("expected error %v, got %v", exp, err) + } + }) + t.Run("UnknownComparisonOperator", func(t *testing.T) { + _, err := dasel.ParseSelector(".(a>b)") + exp := "unknown comparison operator: >" + if err == nil || err.Error() != exp { + t.Errorf("expected error %v, got %v", exp, err) + } + }) + t.Run("UnknownSearchComparisonOperator", func(t *testing.T) { + _, err := dasel.ParseSelector(".(?:a>b)") + exp := "unknown comparison operator: >" + if err == nil || err.Error() != exp { + t.Errorf("expected error %v, got %v", exp, err) + } + }) + t.Run("UnknownSearchKeyComparisonOperator", func(t *testing.T) { + _, err := dasel.ParseSelector(".(?:->b)") + exp := "unknown comparison operator: >" + if err == nil || err.Error() != exp { + t.Errorf("expected error %v, got %v", exp, err) + } + }) + t.Run("Search", testParseSelector(".(?:name=asd)", dasel.Selector{ + Raw: ".(?:name=asd)", + Current: ".(?:name=asd)", + Remaining: "", + Type: "SEARCH", + Conditions: []dasel.Condition{ + &dasel.EqualCondition{ + Key: "name", + Value: "asd", + }, + }, + })) + t.Run("SearchKey", testParseSelector(".(?:-=asd)", dasel.Selector{ + Raw: ".(?:-=asd)", + Current: ".(?:-=asd)", + Remaining: "", + Type: "SEARCH", + Conditions: []dasel.Condition{ + &dasel.KeyEqualCondition{ + Value: "asd", + }, + }, + })) } func TestNode_QueryMultiple(t *testing.T) {