mirror of
https://github.com/TomWright/dasel.git
synced 2022-05-22 02:32:45 +03:00
2
.github/workflows/test.yaml
vendored
2
.github/workflows/test.yaml
vendored
@@ -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)
|
||||
93
README.md
93
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:
|
||||
|
||||
```
|
||||
<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
|
||||
|
||||
41
condition.go
41
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)
|
||||
|
||||
46
condition_equal.go
Normal file
46
condition_equal.go
Normal 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
22
condition_key_equal.go
Normal 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
|
||||
}
|
||||
@@ -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},
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -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", `
|
||||
|
||||
@@ -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
61
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] {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
63
node_test.go
63
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) {
|
||||
|
||||
Reference in New Issue
Block a user