Skip to content

Commit fce641d

Browse files
committed
Add recursive descent operator
1 parent bf5c39e commit fce641d

File tree

12 files changed

+338
-12
lines changed

12 files changed

+338
-12
lines changed

execution/execute.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ func exprExecutor(opts *Options, expr ast.Expr) (expressionExecutor, error) {
117117
return filterExprExecutor(opts, e)
118118
case ast.SearchExpr:
119119
return searchExprExecutor(opts, e)
120+
case ast.RecursiveDescentExpr:
121+
return recursiveDescentExprExecutor(opts, e)
120122
case ast.ConditionalExpr:
121123
return conditionalExprExecutor(opts, e)
122124
case ast.BranchExpr:
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package execution
2+
3+
import (
4+
"fmt"
5+
"github.com/tomwright/dasel/v3/model"
6+
"github.com/tomwright/dasel/v3/selector/ast"
7+
)
8+
9+
func doesValueMatchRecursiveDescentKey(opts *Options, data *model.Value, e ast.RecursiveDescentExpr) (*model.Value, error) {
10+
if e.IsWildcard {
11+
if data.IsScalar() {
12+
return data, nil
13+
}
14+
return nil, nil
15+
}
16+
17+
var key *model.Value
18+
19+
var expr ast.Expr
20+
21+
switch exprT := e.Expr.(type) {
22+
case ast.PropertyExpr:
23+
expr = exprT.Property
24+
case ast.IndexExpr:
25+
expr = exprT.Index
26+
default:
27+
expr = e.Expr
28+
}
29+
30+
key, err := ExecuteAST(expr, data, opts)
31+
if err != nil {
32+
return nil, err
33+
}
34+
35+
switch key.Type() {
36+
case model.TypeString:
37+
keyStr, err := key.StringValue()
38+
if err != nil {
39+
return nil, err
40+
}
41+
if !data.IsMap() {
42+
return nil, nil
43+
}
44+
exists, err := data.MapKeyExists(keyStr)
45+
if err != nil {
46+
return nil, err
47+
}
48+
if !exists {
49+
return nil, nil
50+
}
51+
return data.GetMapKey(keyStr)
52+
53+
case model.TypeInt:
54+
keyInt, err := key.IntValue()
55+
if err != nil {
56+
return nil, err
57+
}
58+
if !data.IsSlice() {
59+
return nil, nil
60+
}
61+
sliceSize, err := data.SliceLen()
62+
if err != nil {
63+
return nil, err
64+
}
65+
if keyInt >= 0 && keyInt < int64(sliceSize) {
66+
res, err := data.GetSliceIndex(int(keyInt))
67+
return res, err
68+
}
69+
return nil, nil
70+
default:
71+
// TODO : Do we need to handle variable lookup?
72+
return nil, fmt.Errorf("unexpected recursive descent key type: %v", key.Type())
73+
}
74+
}
75+
76+
func recursiveDescentExprExecutor(opts *Options, e ast.RecursiveDescentExpr) (expressionExecutor, error) {
77+
var recurseTree func(data *model.Value) ([]*model.Value, error)
78+
79+
recurseTree = func(data *model.Value) ([]*model.Value, error) {
80+
res := make([]*model.Value, 0)
81+
82+
switch data.Type() {
83+
case model.TypeMap:
84+
if err := data.RangeMap(func(key string, v *model.Value) error {
85+
appendValue, err := doesValueMatchRecursiveDescentKey(opts, v, e)
86+
if err != nil {
87+
return err
88+
}
89+
if appendValue != nil {
90+
res = append(res, appendValue)
91+
}
92+
93+
gotNext, err := recurseTree(v)
94+
if err != nil {
95+
return err
96+
}
97+
res = append(res, gotNext...)
98+
99+
return nil
100+
}); err != nil {
101+
return nil, err
102+
}
103+
case model.TypeSlice:
104+
if err := data.RangeSlice(func(i int, v *model.Value) error {
105+
106+
appendValue, err := doesValueMatchRecursiveDescentKey(opts, v, e)
107+
if err != nil {
108+
return err
109+
}
110+
if appendValue != nil {
111+
res = append(res, appendValue)
112+
}
113+
114+
gotNext, err := recurseTree(v)
115+
if err != nil {
116+
return err
117+
}
118+
res = append(res, gotNext...)
119+
120+
return nil
121+
}); err != nil {
122+
return nil, err
123+
}
124+
}
125+
126+
return res, nil
127+
}
128+
129+
return func(data *model.Value) (*model.Value, error) {
130+
matches := model.NewSliceValue()
131+
132+
found, err := recurseTree(data)
133+
if err != nil {
134+
return nil, err
135+
}
136+
137+
for _, f := range found {
138+
if err := matches.Append(f); err != nil {
139+
return nil, err
140+
}
141+
}
142+
143+
return matches, nil
144+
}, nil
145+
}

internal/cli/command_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,81 @@ func TestRun(t *testing.T) {
113113
stdout: []byte(`{
114114
"name": "Tom"
115115
}
116+
`),
117+
stderr: nil,
118+
err: nil,
119+
}))
120+
})
121+
t.Run("recursive descent", func(t *testing.T) {
122+
t.Run("wildcard", runTest(testCase{
123+
args: []string{"-i", "json", `..*`},
124+
in: []byte(`{
125+
"user": {
126+
"name": "Alice",
127+
"roles": ["admin", "editor"],
128+
"meta": {
129+
"active": true,
130+
"score": 42
131+
}
132+
},
133+
"tags": ["x", "y"],
134+
"count": 10
135+
}`),
136+
stdout: []byte(`[
137+
"Alice",
138+
"admin",
139+
"editor",
140+
true,
141+
42,
142+
"x",
143+
"y",
144+
10
145+
]
146+
`),
147+
stderr: nil,
148+
err: nil,
149+
}))
150+
151+
t.Run("property", runTest(testCase{
152+
args: []string{"-i", "json", `..name`},
153+
in: []byte(`{
154+
"user": {
155+
"name": "Alice",
156+
"roles": ["admin", "editor"],
157+
"meta": {
158+
"active": true,
159+
"score": 42
160+
}
161+
},
162+
"tags": ["x", "y"],
163+
"count": 10
164+
}`),
165+
stdout: []byte(`[
166+
"Alice"
167+
]
168+
`),
169+
stderr: nil,
170+
err: nil,
171+
}))
172+
173+
t.Run("index", runTest(testCase{
174+
args: []string{"-i", "json", `..[0]`},
175+
in: []byte(`{
176+
"user": {
177+
"name": "Alice",
178+
"roles": ["admin", "editor"],
179+
"meta": {
180+
"active": true,
181+
"score": 42
182+
}
183+
},
184+
"tags": ["x", "y"],
185+
"count": 10
186+
}`),
187+
stdout: []byte(`[
188+
"admin",
189+
"x"
190+
]
116191
`),
117192
stderr: nil,
118193
err: nil,

model/value.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,24 @@ func (v *Value) Type() Type {
249249
}
250250
}
251251

252+
// IsScalar returns true if the type is scalar.
253+
func (v *Value) IsScalar() bool {
254+
switch {
255+
case v.IsString():
256+
return true
257+
case v.IsInt():
258+
return true
259+
case v.IsFloat():
260+
return true
261+
case v.IsBool():
262+
return true
263+
case v.IsNull():
264+
return true
265+
default:
266+
return false
267+
}
268+
}
269+
252270
// Len returns the length of the value.
253271
func (v *Value) Len() (int, error) {
254272
var l int

selector/ast/expression_complex.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,13 @@ type SearchExpr struct {
103103

104104
func (SearchExpr) expr() {}
105105

106+
type RecursiveDescentExpr struct {
107+
IsWildcard bool
108+
Expr Expr
109+
}
110+
111+
func (RecursiveDescentExpr) expr() {}
112+
106113
type SortByExpr struct {
107114
Expr Expr
108115
Descending bool

selector/lexer/token.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ const (
2727
NotEqual // !=
2828
And
2929
Or
30-
Like
31-
NotLike
30+
Like // =~
31+
NotLike // !~
3232
String
3333
Number
3434
Bool
@@ -42,7 +42,8 @@ const (
4242
Slash
4343
Percent
4444
Dot
45-
Spread
45+
Spread // ...
46+
RecursiveDescent // ..
4647
Dollar
4748
Variable
4849
GreaterThan

selector/lexer/tokenize.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ func (p *Tokenizer) parseCurRune() (Token, error) {
6161
if p.peekRuneEqual(p.i+1, '.') && p.peekRuneEqual(p.i+2, '.') {
6262
return NewToken(Spread, "...", p.i, 3), nil
6363
}
64+
if p.peekRuneEqual(p.i+1, '.') {
65+
return NewToken(RecursiveDescent, "..", p.i, 2), nil
66+
}
6467
return NewToken(Dot, ".", p.i, 1), nil
6568
case ',':
6669
return NewToken(Comma, ",", p.i, 1), nil

selector/lexer/tokenize_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,32 @@ func TestTokenizer_Parse(t *testing.T) {
108108
},
109109
}.run)
110110

111+
t.Run("recursive descent", func(t *testing.T) {
112+
t.Run("key", testCase{
113+
in: `..foo`,
114+
out: []lexer.TokenKind{
115+
lexer.RecursiveDescent,
116+
lexer.Symbol,
117+
},
118+
}.run)
119+
t.Run("index", testCase{
120+
in: `..[1]`,
121+
out: []lexer.TokenKind{
122+
lexer.RecursiveDescent,
123+
lexer.OpenBracket,
124+
lexer.Number,
125+
lexer.CloseBracket,
126+
},
127+
}.run)
128+
t.Run("wildcard", testCase{
129+
in: `..*`,
130+
out: []lexer.TokenKind{
131+
lexer.RecursiveDescent,
132+
lexer.Star,
133+
},
134+
}.run)
135+
})
136+
111137
t.Run("everything", testCase{
112138
in: "foo.bar.baz[1] != 42.123 || foo.b_a_r.baz['hello'] == 42 && x == 'a\\'b' + false true . .... asd... $name null",
113139
out: []lexer.TokenKind{

selector/parser/parse_array.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func parseIndex(p *Parser) (ast.Expr, error) {
4444
token := p.current()
4545
p.advance()
4646

47-
idx, err := parseIndexSquareBrackets(p)
47+
idx, err := parseIndexSquareBrackets(p, false)
4848
if err != nil {
4949
return nil, err
5050
}
@@ -72,7 +72,7 @@ func parseIndex(p *Parser) (ast.Expr, error) {
7272

7373
// parseIndexSquareBrackets parses square bracket array access.
7474
// E.g. [0], [0:1], [0:], [:2]
75-
func parseIndexSquareBrackets(p *Parser) (ast.Expr, error) {
75+
func parseIndexSquareBrackets(p *Parser, expectIndex bool) (ast.Expr, error) {
7676
// Handle index (from bracket)
7777
if err := p.expect(lexer.OpenBracket); err != nil {
7878
return nil, err
@@ -121,6 +121,11 @@ func parseIndexSquareBrackets(p *Parser) (ast.Expr, error) {
121121
Index: start,
122122
}, nil
123123
}
124+
if expectIndex {
125+
if err := p.expect(lexer.CloseBracket); err != nil {
126+
return nil, err
127+
}
128+
}
124129

125130
if err := p.expect(lexer.Colon); err != nil {
126131
return nil, err

0 commit comments

Comments
 (0)