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

Search selector (#43)

Implement search selector
This commit is contained in:
Tom Wright
2020-11-16 22:44:28 +00:00
committed by GitHub
parent 1dc4163c67
commit 9eb8a5ab6e
13 changed files with 693 additions and 54 deletions

View File

@@ -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)
fail_ci_if_error: false # optional (default = false)

View File

@@ -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:
```
<Person active="true">
<Name main="yes">Tom</Name>
<Age>27</Age>
</Person>
```
```
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

View File

@@ -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)

46
condition_equal.go Normal file
View File

@@ -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()}
}

22
condition_key_equal.go Normal file
View File

@@ -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
}

View File

@@ -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},
))
}

View File

@@ -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" {

View File

@@ -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", `

View File

@@ -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(`
<food>
<tart>
<apple color="yellow"/>
</tart>
<pie>
<crust quality="flaky"/>
<filling>
<apple color="red"/>
</filling>
</pie>
<apple color="green"/>
</food>
`, "xml", ".food.(?:keyValue=apple).-color", []string{"yellow", "red", "green"}, nil, "-m"))
}
func TestRootCMD_Select_CSV(t *testing.T) {

61
node.go
View File

@@ -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] {

View File

@@ -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

View File

@@ -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}
}

View File

@@ -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) {