Skip to content

Commit

Permalink
Add initial branch support
Browse files Browse the repository at this point in the history
  • Loading branch information
TomWright committed Oct 7, 2024
1 parent 8f1ea53 commit f8a5e27
Show file tree
Hide file tree
Showing 22 changed files with 293 additions and 123 deletions.
29 changes: 27 additions & 2 deletions api.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
// Package dasel contains everything you'll need to use dasel from a go application.
package dasel

func Select(data any, selector string) any {
panic("not implemented")
import (
"github.com/tomwright/dasel/v3/execution"
"github.com/tomwright/dasel/v3/model"
)

func Select(data any, selector string) (any, error) {
val := model.NewValue(data)
res, err := execution.ExecuteSelector(selector, val)
if err != nil {
return nil, err
}
return res.Interface(), nil
}

func Modify(data any, selector string, newValue any) error {
val := model.NewValue(data)
newVal := model.NewValue(newValue)
res, err := execution.ExecuteSelector(selector, val)
if err != nil {
return err
}

if err := res.Set(newVal); err != nil {
return err
}

return nil
}
32 changes: 32 additions & 0 deletions api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package dasel_test

import (
"github.com/google/go-cmp/cmp"
"github.com/tomwright/dasel/v3"
"testing"
)

type modifyTestCase struct {
selector string
in any
value any
exp any
}

func (tc modifyTestCase) run(t *testing.T) {
if err := dasel.Modify(&tc.in, "[1]", 4); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !cmp.Equal(tc.exp, tc.in) {
t.Errorf("unexpected result: %s", cmp.Diff(tc.exp, tc.in))
}
}

func TestModify(t *testing.T) {
t.Run("int over int", modifyTestCase{
selector: "[1]",
in: []int{1, 2, 3},
value: 4,
exp: []int{1, 4, 3},
}.run)
}
25 changes: 22 additions & 3 deletions execution/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,26 @@ func ExecuteAST(expr ast.Expr, value *model.Value) (*model.Value, error) {
if err != nil {
return nil, fmt.Errorf("error evaluating expression: %w", err)
}
res, err := executor(value)
if err != nil {
return nil, fmt.Errorf("execution error: %w", err)

if !value.IsBranch() {
res, err := executor(value)
if err != nil {
return nil, fmt.Errorf("execution error: %w", err)
}
return res, nil
}

res := model.NewSliceValue()
res.MarkAsBranch()

if err := value.RangeSlice(func(i int, value *model.Value) error {
r, err := executor(value)
if err != nil {
return err
}
return res.Append(r)
}); err != nil {
return nil, fmt.Errorf("branch execution error: %w", err)
}

return res, nil
Expand Down Expand Up @@ -78,6 +95,8 @@ func exprExecutor(expr ast.Expr) (expressionExecutor, error) {
return mapExprExecutor(e)
case ast.ConditionalExpr:
return conditionalExprExecutor(e)
case ast.BranchExpr:
return branchExprExecutor(e)
default:
return nil, fmt.Errorf("unhandled expression type: %T", e)
}
Expand Down
28 changes: 28 additions & 0 deletions execution/execute_branch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package execution

import (
"fmt"

"github.com/tomwright/dasel/v3/model"
"github.com/tomwright/dasel/v3/selector/ast"
)

func branchExprExecutor(e ast.BranchExpr) (expressionExecutor, error) {
return func(data *model.Value) (*model.Value, error) {
res := model.NewSliceValue()

for _, expr := range e.Exprs {
r, err := ExecuteAST(expr, data)
if err != nil {
return nil, fmt.Errorf("failed to execute branch expr: %w", err)
}
if err := res.Append(r); err != nil {
return nil, fmt.Errorf("failed to append branch result: %w", err)
}
}

res.MarkAsBranch()

return res, nil
}, nil
}
40 changes: 40 additions & 0 deletions execution/execute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ func TestExecuteSelector_HappyPath(t *testing.T) {
if !equal {
t.Errorf("unexpected output: %v", cmp.Diff(exp.Interface(), res.Interface()))
}

expMeta := exp.Metadata
gotMeta := res.Metadata
if !cmp.Equal(expMeta, gotMeta) {
t.Errorf("unexpected output metadata: %v", cmp.Diff(expMeta, gotMeta))
}
}
}

Expand Down Expand Up @@ -575,4 +581,38 @@ func TestExecuteSelector_HappyPath(t *testing.T) {
out: model.NewStringValue("nope"),
}))
})

t.Run("branch", func(t *testing.T) {
t.Run("single branch", runTest(testCase{
s: "branch(1)",
outFn: func() *model.Value {
r := model.NewSliceValue()
r.MarkAsBranch()
if err := r.Append(model.NewIntValue(1)); err != nil {
t.Fatalf("unexpected error: %v", err)
}
return r
},
}))
t.Run("many branches", runTest(testCase{
s: "branch(1, 1+1, 3/1, 123)",
outFn: func() *model.Value {
r := model.NewSliceValue()
r.MarkAsBranch()
if err := r.Append(model.NewIntValue(1)); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := r.Append(model.NewIntValue(2)); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := r.Append(model.NewIntValue(3)); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := r.Append(model.NewIntValue(123)); err != nil {
t.Fatalf("unexpected error: %v", err)
}
return r
},
}))
})
}
16 changes: 8 additions & 8 deletions internal/cli/generic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func TestCrossFormatHappyPath(t *testing.T) {
"stringFalse": "false",
"stringTrue": "true",
"sliceOfNumbers": [1, 2, 3, 4, 5],
"map": {
"mapData": {
"oneTwoThree": 123,
"oneTwoDotThree": 12.3,
"hello": "world",
Expand All @@ -75,7 +75,7 @@ func TestCrossFormatHappyPath(t *testing.T) {
"stringFalse": "false",
"stringTrue": "true",
"sliceOfNumbers": [1, 2, 3, 4, 5],
"map": {
"mapData": {
"oneTwoThree": 123,
"oneTwoDotThree": 12.3,
"hello": "world",
Expand All @@ -100,7 +100,7 @@ sliceOfNumbers:
- 3
- 4
- 5
map:
mapData:
oneTwoThree: 123
oneTwoDotThree: 12.3
hello: world
Expand All @@ -114,7 +114,7 @@ map:
- 3
- 4
- 5
map:
mapData:
oneTwoThree: 123
oneTwoDotThree: 12.3
hello: world
Expand All @@ -140,7 +140,7 @@ stringFalse = 'false'
stringTrue = 'true'
sliceOfNumbers = [1, 2, 3, 4, 5]
[map]
[mapData]
oneTwoThree = 123
oneTwoDotThree = 12.3
hello = "world"
Expand All @@ -150,7 +150,7 @@ stringFalse = "false"
stringTrue = "true"
sliceOfNumbers = [1, 2, 3, 4, 5]
[map.map]
[mapData.mapData]
oneTwoThree = 123
oneTwoDotThree = 12.3
hello = "world"
Expand Down Expand Up @@ -261,7 +261,7 @@ sliceOfNumbers = [1, 2, 3, 4, 5]
}

t.Run("root", newTestsWithPrefix(""))
t.Run("nested once", newTestsWithPrefix("map."))
t.Run("nested twice", newTestsWithPrefix("map.map."))
t.Run("nested once", newTestsWithPrefix("mapData."))
t.Run("nested twice", newTestsWithPrefix("mapData.mapData."))
})
}
2 changes: 1 addition & 1 deletion model/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func NewValue(v any) *Value {
}
}

func (v *Value) Interface() interface{} {
func (v *Value) Interface() any {
return v.Value.Interface()
}

Expand Down
18 changes: 18 additions & 0 deletions model/value_metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,21 @@ func (v *Value) IsSpread() bool {
func (v *Value) MarkAsSpread() {
v.SetMetadataValue("spread", true)
}

// IsBranch returns true if the value is a branched value.
func (v *Value) IsBranch() bool {
val, ok := v.Metadata["spread"]
if !ok {
return false
}
spread, ok := val.(bool)
if !ok {
return false
}
return spread
}

// MarkAsBranch marks the value as a branch value.
func (v *Value) MarkAsBranch() {
v.SetMetadataValue("branch", true)
}
21 changes: 21 additions & 0 deletions model/value_set.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package model

import (
"reflect"
)

func (v *Value) Set(newValue *Value) error {
a := v.UnpackKinds(reflect.Ptr, reflect.Interface)
b := newValue.UnpackKinds(reflect.Ptr, reflect.Interface)

if a.Kind() == b.Kind() {
a.Value.Set(b.Value)
return nil
}

x := newPtr()
x.Elem().Set(b.Value)
v.Value.Set(x)

return nil
}
12 changes: 12 additions & 0 deletions selector/ast/expression_complex.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,15 @@ type ConditionalExpr struct {
}

func (ConditionalExpr) expr() {}

type BranchExpr struct {
Exprs []Expr
}

func (BranchExpr) expr() {}

func BranchExprs(exprs ...Expr) Expr {
return BranchExpr{
Exprs: exprs,
}
}
5 changes: 4 additions & 1 deletion selector/lexer/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ const (
If
Else
ElseIf
Branch
Map
Filter
)

type Tokens []Token
Expand Down Expand Up @@ -108,5 +111,5 @@ type UnexpectedTokenError struct {
}

func (e *UnexpectedTokenError) Error() string {
return fmt.Sprintf("unexpected token: %s at position %d.", string(e.Token), e.Pos)
return fmt.Sprintf("failed to tokenize: unexpected token: %s at position %d.", string(e.Token), e.Pos)
}
31 changes: 23 additions & 8 deletions selector/lexer/tokenize.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ func (p *Tokenizer) peekRuneMatches(i int, fn func(rune) bool) bool {
}

func (p *Tokenizer) parseCurRune() (Token, error) {
// Skip over whitespace
for p.i < p.srcLen && unicode.IsSpace(rune(p.src[p.i])) {
p.i++
}

switch p.src[p.i] {
case '.':
if p.peekRuneEqual(p.i+1, '.') && p.peekRuneEqual(p.i+2, '.') {
Expand Down Expand Up @@ -180,10 +185,16 @@ func (p *Tokenizer) parseCurRune() (Token, error) {
return nil
}
other := p.src[pos : pos+l]
if m == other || caseInsensitive && strings.EqualFold(m, other) {
return ptr.To(NewToken(kind, other, pos, l))
if m != other && !(caseInsensitive && strings.EqualFold(m, other)) {
return nil
}
return nil

if pos+(l) < p.srcLen && (unicode.IsLetter(rune(p.src[pos+l])) || unicode.IsDigit(rune(p.src[pos+l]))) {
// There is a follow letter or digit.
return nil
}

return ptr.To(NewToken(kind, other, pos, l))
}

if t := matchStr(pos, "null", true, Null); t != nil {
Expand All @@ -204,6 +215,15 @@ func (p *Tokenizer) parseCurRune() (Token, error) {
if t := matchStr(pos, "else", false, Else); t != nil {
return *t, nil
}
if t := matchStr(pos, "branch", false, Branch); t != nil {
return *t, nil
}
if t := matchStr(pos, "map", false, Map); t != nil {
return *t, nil
}
if t := matchStr(pos, "filter", false, Filter); t != nil {
return *t, nil
}

if unicode.IsDigit(rune(p.src[pos])) {
// Handle whole numbers
Expand Down Expand Up @@ -239,11 +259,6 @@ func (p *Tokenizer) Next() (Token, error) {
return NewToken(EOF, "", p.i, 0), nil
}

// Skip over whitespace
for p.i < p.srcLen && unicode.IsSpace(rune(p.src[p.i])) {
p.i++
}

t, err := p.parseCurRune()
if err != nil {
return Token{}, err
Expand Down
Loading

0 comments on commit f8a5e27

Please sign in to comment.