diff --git a/api.go b/api.go index 78ae5cf..e34cae1 100644 --- a/api.go +++ b/api.go @@ -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 } diff --git a/api_test.go b/api_test.go new file mode 100644 index 0000000..f2c9550 --- /dev/null +++ b/api_test.go @@ -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) +} diff --git a/execution/execute.go b/execution/execute.go index 8047c80..a238d05 100644 --- a/execution/execute.go +++ b/execution/execute.go @@ -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 @@ -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) } diff --git a/execution/execute_branch.go b/execution/execute_branch.go new file mode 100644 index 0000000..d64e61a --- /dev/null +++ b/execution/execute_branch.go @@ -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 +} diff --git a/execution/execute_test.go b/execution/execute_test.go index 28a3c1f..57b2265 100644 --- a/execution/execute_test.go +++ b/execution/execute_test.go @@ -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)) + } } } @@ -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 + }, + })) + }) } diff --git a/internal/cli/generic_test.go b/internal/cli/generic_test.go index cf3fb4d..cfc2622 100644 --- a/internal/cli/generic_test.go +++ b/internal/cli/generic_test.go @@ -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", @@ -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", @@ -100,7 +100,7 @@ sliceOfNumbers: - 3 - 4 - 5 -map: +mapData: oneTwoThree: 123 oneTwoDotThree: 12.3 hello: world @@ -114,7 +114,7 @@ map: - 3 - 4 - 5 - map: + mapData: oneTwoThree: 123 oneTwoDotThree: 12.3 hello: world @@ -140,7 +140,7 @@ stringFalse = 'false' stringTrue = 'true' sliceOfNumbers = [1, 2, 3, 4, 5] -[map] +[mapData] oneTwoThree = 123 oneTwoDotThree = 12.3 hello = "world" @@ -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" @@ -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.")) }) } diff --git a/model/value.go b/model/value.go index 76edb7f..b86259d 100644 --- a/model/value.go +++ b/model/value.go @@ -53,7 +53,7 @@ func NewValue(v any) *Value { } } -func (v *Value) Interface() interface{} { +func (v *Value) Interface() any { return v.Value.Interface() } diff --git a/model/value_metadata.go b/model/value_metadata.go index a939c58..bed68b8 100644 --- a/model/value_metadata.go +++ b/model/value_metadata.go @@ -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) +} diff --git a/model/value_set.go b/model/value_set.go new file mode 100644 index 0000000..5cfe3d9 --- /dev/null +++ b/model/value_set.go @@ -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 +} diff --git a/selector/ast/expression_complex.go b/selector/ast/expression_complex.go index 4c3440e..009c8d0 100644 --- a/selector/ast/expression_complex.go +++ b/selector/ast/expression_complex.go @@ -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, + } +} diff --git a/selector/lexer/token.go b/selector/lexer/token.go index 4ce78e8..66379ae 100644 --- a/selector/lexer/token.go +++ b/selector/lexer/token.go @@ -50,6 +50,9 @@ const ( If Else ElseIf + Branch + Map + Filter ) type Tokens []Token @@ -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) } diff --git a/selector/lexer/tokenize.go b/selector/lexer/tokenize.go index e4ef790..fd60150 100644 --- a/selector/lexer/tokenize.go +++ b/selector/lexer/tokenize.go @@ -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, '.') { @@ -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 { @@ -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 @@ -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 diff --git a/selector/parser/error.go b/selector/parser/error.go index 8990394..e3611b3 100644 --- a/selector/parser/error.go +++ b/selector/parser/error.go @@ -20,5 +20,5 @@ type UnexpectedTokenError struct { } func (e *UnexpectedTokenError) Error() string { - return fmt.Sprintf("unexpected token %v %q at position %d.", e.Token.Kind, e.Token.Value, e.Token.Pos) + return fmt.Sprintf("failed to parse: unexpected token %v %q at position %d.", e.Token.Kind, e.Token.Value, e.Token.Pos) } diff --git a/selector/parser/parse_array.go b/selector/parser/parse_array.go index 1403bd1..cd018e4 100644 --- a/selector/parser/parse_array.go +++ b/selector/parser/parse_array.go @@ -44,8 +44,6 @@ func parseArray(p *Parser) (ast.Expr, error) { // parseSquareBrackets parses square bracket array access. // E.g. [0], [0:1], [0:], [:2] func parseSquareBrackets(p *Parser) (ast.Expr, error) { - p.pushScope(scopeArray) - defer p.popScope() // Handle index (from bracket) if err := p.expect(lexer.OpenBracket); err != nil { return nil, err @@ -63,9 +61,6 @@ func parseSquareBrackets(p *Parser) (ast.Expr, error) { return ast.SpreadExpr{}, nil } - p.pushScope(scopeArray) - defer p.popScope() - var ( start ast.Expr end ast.Expr diff --git a/selector/parser/parse_func.go b/selector/parser/parse_func.go index 59b48e9..a8fa34b 100644 --- a/selector/parser/parse_func.go +++ b/selector/parser/parse_func.go @@ -6,9 +6,6 @@ import ( ) func parseFunc(p *Parser) (ast.Expr, error) { - p.pushScope(scopeFuncArgs) - defer p.popScope() - if err := p.expect(lexer.Symbol); err != nil { return nil, err } diff --git a/selector/parser/parse_group.go b/selector/parser/parse_group.go index 9181f3e..923ee83 100644 --- a/selector/parser/parse_group.go +++ b/selector/parser/parse_group.go @@ -6,8 +6,6 @@ import ( ) func parseGroup(p *Parser) (ast.Expr, error) { - p.pushScope(scopeGroup) - defer p.popScope() if err := p.expect(lexer.OpenParen); err != nil { return nil, err } diff --git a/selector/parser/parse_if.go b/selector/parser/parse_if.go index 41b7a60..cfc2c31 100644 --- a/selector/parser/parse_if.go +++ b/selector/parser/parse_if.go @@ -14,9 +14,6 @@ func parseIfCondition(p *Parser) (ast.Expr, error) { } func parseIf(p *Parser) (ast.Expr, error) { - p.pushScope(scopeIf) - defer p.popScope() - if err := p.expect(lexer.If); err != nil { return nil, err } diff --git a/selector/parser/parse_map.go b/selector/parser/parse_map.go index b8e5062..76334bf 100644 --- a/selector/parser/parse_map.go +++ b/selector/parser/parse_map.go @@ -1,22 +1,14 @@ package parser import ( - "fmt" - "github.com/tomwright/dasel/v3/selector/ast" "github.com/tomwright/dasel/v3/selector/lexer" ) func parseMap(p *Parser) (ast.Expr, error) { - p.pushScope(scopeMap) - defer p.popScope() - - if err := p.expect(lexer.Symbol); err != nil { + if err := p.expect(lexer.Map); err != nil { return nil, err } - if p.current().Value != "map" { - return nil, fmt.Errorf("expected map but got %q", p.current().Value) - } p.advance() if err := p.expect(lexer.OpenParen); err != nil { @@ -38,3 +30,29 @@ func parseMap(p *Parser) (ast.Expr, error) { Exprs: expressions, }, nil } + +func parseBranch(p *Parser) (ast.Expr, error) { + if err := p.expect(lexer.Branch); err != nil { + return nil, err + } + + p.advance() + if err := p.expect(lexer.OpenParen); err != nil { + return nil, err + } + p.advance() + + expressions, err := p.parseExpressionsAsSlice( + []lexer.TokenKind{lexer.CloseParen}, + []lexer.TokenKind{lexer.Comma}, + true, + bpDefault, + ) + if err != nil { + return nil, err + } + + return ast.BranchExpr{ + Exprs: expressions, + }, nil +} diff --git a/selector/parser/parse_object.go b/selector/parser/parse_object.go index 9b47cae..03f78f5 100644 --- a/selector/parser/parse_object.go +++ b/selector/parser/parse_object.go @@ -6,9 +6,6 @@ import ( ) func parseObject(p *Parser) (ast.Expr, error) { - p.pushScope(scopeObject) - defer p.popScope() - if err := p.expect(lexer.OpenCurly); err != nil { return nil, err } diff --git a/selector/parser/parse_symbol.go b/selector/parser/parse_symbol.go index 74ad4cb..dc56cc1 100644 --- a/selector/parser/parse_symbol.go +++ b/selector/parser/parse_symbol.go @@ -10,10 +10,6 @@ func parseSymbol(p *Parser) (ast.Expr, error) { next := p.peek() - if token.Value == "map" && next.IsKind(lexer.OpenParen) { - return parseMap(p) - } - // Handle functions if next.IsKind(lexer.OpenParen) { return parseFunc(p) diff --git a/selector/parser/parser.go b/selector/parser/parser.go index 7e6c3a2..7524dab 100644 --- a/selector/parser/parser.go +++ b/selector/parser/parser.go @@ -1,79 +1,15 @@ package parser import ( - "fmt" "slices" "github.com/tomwright/dasel/v3/selector/ast" "github.com/tomwright/dasel/v3/selector/lexer" ) -type scope string - -const ( - scopeRoot scope = "root" - scopeFuncArgs scope = "funcArgs" - scopeArray scope = "array" - scopeObject scope = "object" - scopeMap scope = "map" - scopeGroup scope = "group" - scopeIf scope = "if" -) - type Parser struct { tokens lexer.Tokens i int - scopes []scope -} - -func (p *Parser) pushScope(s scope) { - p.scopes = append(p.scopes, s) -} - -func (p *Parser) popScope() { - p.scopes = p.scopes[:len(p.scopes)-1] -} - -func (p *Parser) currentScope() scope { - if len(p.scopes) == 0 { - return scopeRoot - } - return p.scopes[len(p.scopes)-1] -} - -func (p *Parser) endOfExpressionTokens() []lexer.TokenKind { - allowLeftDonation := true - var tokens []lexer.TokenKind - switch p.currentScope() { - case scopeRoot: - tokens = append(tokens, lexer.EOF, lexer.Dot) - case scopeFuncArgs: - tokens = append(tokens, lexer.Comma, lexer.CloseParen) - case scopeMap: - tokens = append(tokens, lexer.Comma, lexer.CloseParen, lexer.Dot, lexer.Spread) - case scopeArray: - tokens = append(tokens, lexer.CloseBracket, lexer.Colon, lexer.Number, lexer.Symbol, lexer.Spread) - case scopeObject: - tokens = append(tokens, lexer.CloseCurly, lexer.Equals, lexer.Number, lexer.Symbol, lexer.Comma) - case scopeGroup: - tokens = append(tokens, lexer.CloseParen, lexer.Dot) - default: - allowLeftDonation = false - } - - if allowLeftDonation { - tokens = append(tokens, leftDenotationTokens...) - } - - return tokens -} - -func (p *Parser) expectEndOfExpression() error { - tokens := p.endOfExpressionTokens() - if len(tokens) == 0 { - return fmt.Errorf("no end of scope tokens found: %q", p.currentScope()) - } - return p.expect(tokens...) } func NewParser(tokens lexer.Tokens) *Parser { @@ -145,12 +81,6 @@ func (p *Parser) Parse() (ast.Expr, error) { } func (p *Parser) parseExpression(bp bindingPower) (left ast.Expr, err error) { - //defer func() { - // if err == nil { - // err = p.expectEndOfExpression() - // } - //}() - switch p.current().Kind { case lexer.String: left, err = parseStringLiteral(p) @@ -172,6 +102,12 @@ func (p *Parser) parseExpression(bp bindingPower) (left ast.Expr, err error) { left, err = parseGroup(p) case lexer.If: left, err = parseIf(p) + case lexer.Branch: + left, err = parseBranch(p) + case lexer.Map: + left, err = parseMap(p) + //case lexer.Filter: + // left, err = parseFilter(p) default: return nil, &UnexpectedTokenError{ Token: p.current(), diff --git a/selector/parser/parser_test.go b/selector/parser/parser_test.go index e9fef23..f1ec6cc 100644 --- a/selector/parser/parser_test.go +++ b/selector/parser/parser_test.go @@ -31,6 +31,29 @@ func TestParser_Parse_HappyPath(t *testing.T) { } } + t.Run("branching", func(t *testing.T) { + t.Run("two branches", run(t, testCase{ + input: `branch("hello", len("world"))`, + expected: ast.BranchExprs( + ast.StringExpr{Value: "hello"}, + ast.ChainExprs( + ast.CallExpr{ + Function: "len", + Args: ast.Expressions{ast.StringExpr{Value: "world"}}, + }, + ), + ), + })) + t.Run("three branches", run(t, testCase{ + input: `branch("foo", "bar", "baz")`, + expected: ast.BranchExprs( + ast.StringExpr{Value: "foo"}, + ast.StringExpr{Value: "bar"}, + ast.StringExpr{Value: "baz"}, + ), + })) + }) + t.Run("literal access", func(t *testing.T) { t.Run("string", run(t, testCase{ input: `"hello world"`,