From c9f0b5cb4e42a1d519a4ec6c033b37a9cc1a80e5 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Tue, 24 Sep 2024 23:40:01 +0100 Subject: [PATCH 01/56] Initial start on selector parser rewrite --- dencoding/yaml_decoder.go | 9 +- selector/ast/ast.go | 7 ++ selector/ast/expression_complex.go | 24 ++++ selector/ast/expression_literal.go | 31 +++++ selector/lexer/token.go | 98 +++++++++++++++ selector/lexer/tokenize.go | 188 +++++++++++++++++++++++++++++ selector/lexer/tokenize_test.go | 29 +++++ selector/parser.go | 16 +++ selector/parser/error.go | 24 ++++ selector/parser/parse_literal.go | 45 +++++++ selector/parser/parse_symbol.go | 93 ++++++++++++++ selector/parser/parser.go | 107 ++++++++++++++++ selector/parser/parser_test.go | 110 +++++++++++++++++ storage/toml_test.go | 7 +- 14 files changed, 781 insertions(+), 7 deletions(-) create mode 100644 selector/ast/ast.go create mode 100644 selector/ast/expression_complex.go create mode 100644 selector/ast/expression_literal.go create mode 100644 selector/lexer/token.go create mode 100644 selector/lexer/tokenize.go create mode 100644 selector/lexer/tokenize_test.go create mode 100644 selector/parser.go create mode 100644 selector/parser/error.go create mode 100644 selector/parser/parse_literal.go create mode 100644 selector/parser/parse_symbol.go create mode 100644 selector/parser/parser.go create mode 100644 selector/parser/parser_test.go diff --git a/dencoding/yaml_decoder.go b/dencoding/yaml_decoder.go index ce1975f7..a4d258cb 100644 --- a/dencoding/yaml_decoder.go +++ b/dencoding/yaml_decoder.go @@ -2,12 +2,13 @@ package dencoding import ( "fmt" - "github.com/tomwright/dasel/v2/util" - "gopkg.in/yaml.v3" "io" "reflect" "strconv" "time" + + "github.com/tomwright/dasel/v2/util" + "gopkg.in/yaml.v3" ) // YAMLDecoder wraps a standard yaml encoder to implement custom ordering logic. @@ -159,7 +160,7 @@ var allowedTimestampFormats = []string{ "2006-1-2t15:4:5.999999999Z07:00", // RFC3339Nano with short date fields and lower-case "t". "2006-1-2 15:4:5.999999999", // space separated with no time zone "2006-1-2", // date only - // Notable exception: time.Parse cannot handle: "2001-12-14 21:59:43.10 -5" + // Notable exception: time.Tokenize cannot handle: "2001-12-14 21:59:43.10 -5" // from the set of examples. } @@ -169,7 +170,7 @@ var allowedTimestampFormats = []string{ // Copied from yaml.v3. func parseTimestamp(s string) (time.Time, bool) { // TODO write code to check all the formats supported by - // http://yaml.org/type/timestamp.html instead of using time.Parse. + // http://yaml.org/type/timestamp.html instead of using time.Tokenize. // Quick check: all date formats start with YYYY-. i := 0 diff --git a/selector/ast/ast.go b/selector/ast/ast.go new file mode 100644 index 00000000..923346e2 --- /dev/null +++ b/selector/ast/ast.go @@ -0,0 +1,7 @@ +package ast + +type Expressions []Expr + +type Expr interface { + expr() +} diff --git a/selector/ast/expression_complex.go b/selector/ast/expression_complex.go new file mode 100644 index 00000000..be144761 --- /dev/null +++ b/selector/ast/expression_complex.go @@ -0,0 +1,24 @@ +package ast + +import "github.com/tomwright/dasel/v2/selector/lexer" + +type BinaryExpr struct { + Left Expr + Operator lexer.Token + Right Expr +} + +func (BinaryExpr) expr() {} + +type CallExpr struct { + Function string + Args Expressions +} + +func (CallExpr) expr() {} + +type ChainedExpr struct { + Exprs Expressions +} + +func (ChainedExpr) expr() {} diff --git a/selector/ast/expression_literal.go b/selector/ast/expression_literal.go new file mode 100644 index 00000000..10050829 --- /dev/null +++ b/selector/ast/expression_literal.go @@ -0,0 +1,31 @@ +package ast + +type NumberFloatExpr struct { + Value float64 +} + +func (NumberFloatExpr) expr() {} + +type NumberIntExpr struct { + Value int64 +} + +func (NumberIntExpr) expr() {} + +type StringExpr struct { + Value string +} + +func (StringExpr) expr() {} + +type BoolExpr struct { + Value bool +} + +func (BoolExpr) expr() {} + +type SymbolExpr struct { + Value string +} + +func (SymbolExpr) expr() {} diff --git a/selector/lexer/token.go b/selector/lexer/token.go new file mode 100644 index 00000000..b5c6130e --- /dev/null +++ b/selector/lexer/token.go @@ -0,0 +1,98 @@ +package lexer + +import ( + "fmt" + "slices" +) + +type TokenKind int + +const ( + EOF TokenKind = iota + Symbol + Comma + Colon + OpenBracket + CloseBracket + OpenParen + CloseParen + Equal + NotEqual + Not + And + Or + Like + NotLike + String + Number + Bool + Add + Increment + IncrementBy + Subtract + Decrement + DecrementBy + Multiply + Divide + Modulus + Dot +) + +type Tokens []Token + +func (tt Tokens) Split(kind TokenKind) []Tokens { + var res []Tokens + var cur Tokens + for _, t := range tt { + if t.Kind == kind { + if len(cur) > 0 { + res = append(res, cur) + } + cur = nil + continue + } + cur = append(cur, t) + } + if len(cur) > 0 { + res = append(res, cur) + } + return res +} + +type Token struct { + Kind TokenKind + Value string + Pos int + Len int +} + +func NewToken(kind TokenKind, value string, pos int, len int) Token { + return Token{ + Kind: kind, + Value: value, + Pos: pos, + Len: len, + } +} + +func (t Token) IsKind(kind ...TokenKind) bool { + return slices.Contains(kind, t.Kind) +} + +type PositionalError struct { + Pos int + Err error +} + +func (e *PositionalError) Error() string { + return fmt.Sprintf("%v. Position %d.", e.Pos, e.Err) +} + +type UnexpectedTokenError struct { + Pos int + Token rune +} + +func (e *UnexpectedTokenError) Error() string { + return fmt.Sprintf("unexpected token: %s at position %d.", string(e.Token), e.Pos) +} diff --git a/selector/lexer/tokenize.go b/selector/lexer/tokenize.go new file mode 100644 index 00000000..e6819605 --- /dev/null +++ b/selector/lexer/tokenize.go @@ -0,0 +1,188 @@ +package lexer + +import ( + "strings" + "unicode" +) + +type Tokenizer struct { + i int + src string + srcLen int +} + +func NewTokenizer(src string) *Tokenizer { + return &Tokenizer{ + i: 0, + src: src, + srcLen: len([]rune(src)), + } +} + +func (p *Tokenizer) Tokenize() (Tokens, error) { + var tokens Tokens + for { + tok, err := p.Next() + if err != nil { + return nil, err + } + if tok.Kind == EOF { + break + } + tokens = append(tokens, tok) + } + return tokens, nil +} + +func (p *Tokenizer) peekRuneEqual(i int, to rune) bool { + if i >= p.srcLen { + return false + } + return rune(p.src[i]) == to +} + +func (p *Tokenizer) parseCurRune() (Token, error) { + switch p.src[p.i] { + case '.': + return NewToken(Dot, ".", p.i, 1), nil + case ',': + return NewToken(Comma, ",", p.i, 1), nil + case ':': + return NewToken(Colon, ":", p.i, 1), nil + case '[': + return NewToken(OpenBracket, "[", p.i, 1), nil + case ']': + return NewToken(CloseBracket, "]", p.i, 1), nil + case '(': + return NewToken(OpenParen, "(", p.i, 1), nil + case ')': + return NewToken(CloseParen, ")", p.i, 1), nil + case '=': + if p.peekRuneEqual(p.i+1, '=') { + return NewToken(Equal, "==", p.i, 2), nil + } + if p.peekRuneEqual(p.i+1, '~') { + return NewToken(Like, "=~", p.i, 2), nil + } + return NewToken(Equal, "=", p.i, 1), nil + case '+': + if p.peekRuneEqual(p.i+1, '=') { + return NewToken(IncrementBy, "+=", p.i, 2), nil + } + if p.peekRuneEqual(p.i+1, '+') { + return NewToken(Increment, "++", p.i, 2), nil + } + return NewToken(Add, "+", p.i, 1), nil + case '-': + if p.peekRuneEqual(p.i+1, '=') { + return NewToken(DecrementBy, "-=", p.i, 2), nil + } + if p.peekRuneEqual(p.i+1, '-') { + return NewToken(Decrement, "--", p.i, 2), nil + } + return NewToken(Subtract, "-", p.i, 1), nil + case '!': + if p.peekRuneEqual(p.i+1, '=') { + return NewToken(NotEqual, "!=", p.i, 2), nil + } + if p.peekRuneEqual(p.i+1, '~') { + return NewToken(NotLike, "!~", p.i, 2), nil + } + return NewToken(Not, "!", p.i, 1), nil + case '&': + if p.peekRuneEqual(p.i+1, '&') { + return NewToken(And, "&&", p.i, 2), nil + } + return Token{}, &UnexpectedTokenError{ + Pos: p.i, + Token: rune(p.src[p.i]), + } + case '|': + if p.peekRuneEqual(p.i+1, '|') { + return NewToken(Or, "||", p.i, 2), nil + } + return Token{}, &UnexpectedTokenError{ + Pos: p.i, + Token: rune(p.src[p.i]), + } + case '"', '\'': + pos := p.i + buf := make([]rune, 0) + pos++ + var escaped bool + for pos < p.srcLen { + if p.src[pos] == p.src[p.i] && !escaped { + break + } + if escaped { + escaped = false + buf = append(buf, rune(p.src[pos])) + pos++ + continue + } + if p.src[pos] == '\\' { + pos++ + escaped = true + continue + } + buf = append(buf, rune(p.src[pos])) + pos++ + } + res := NewToken(String, string(buf), p.i, pos+1-p.i) + return res, nil + default: + pos := p.i + + if pos+3 < p.srcLen && strings.EqualFold(p.src[pos:pos+4], "true") { + return NewToken(Bool, p.src[pos:pos+4], p.i, 4), nil + } + if pos+4 < p.srcLen && strings.EqualFold(p.src[pos:pos+5], "false") { + return NewToken(Bool, p.src[pos:pos+5], p.i, 5), nil + } + + if unicode.IsDigit(rune(p.src[pos])) { + // Handle whole numbers + for pos < p.srcLen && unicode.IsDigit(rune(p.src[pos])) { + pos++ + } + // Handle floats + if pos < p.srcLen && p.src[pos] == '.' && pos+1 < p.srcLen && unicode.IsDigit(rune(p.src[pos+1])) { + pos++ + for pos < p.srcLen && unicode.IsDigit(rune(p.src[pos])) { + pos++ + } + } + return NewToken(Number, p.src[p.i:pos], p.i, pos-p.i), nil + } + + if unicode.IsLetter(rune(p.src[pos])) { + for pos < p.srcLen && (unicode.IsLetter(rune(p.src[pos])) || unicode.IsDigit(rune(p.src[pos]))) { + pos++ + } + return NewToken(Symbol, p.src[p.i:pos], p.i, pos-p.i), nil + } + + return Token{}, &UnexpectedTokenError{ + Pos: p.i, + Token: rune(p.src[p.i]), + } + } +} + +func (p *Tokenizer) Next() (Token, error) { + if p.i >= len(p.src) { + 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 + } + p.i += t.Len + return t, nil +} diff --git a/selector/lexer/tokenize_test.go b/selector/lexer/tokenize_test.go new file mode 100644 index 00000000..330ce2cd --- /dev/null +++ b/selector/lexer/tokenize_test.go @@ -0,0 +1,29 @@ +package lexer + +import "testing" + +func TestTokenizer_Parse(t *testing.T) { + tok := NewTokenizer("foo.bar.baz[1] != 42.123 || foo.bar.baz['hello'] == 42 && x == 'a\\'b' + false") + tokens, err := tok.Tokenize() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + exp := []TokenKind{ + Symbol, Dot, Symbol, Dot, Symbol, OpenBracket, Number, CloseBracket, NotEqual, Number, + Or, + Symbol, Dot, Symbol, Dot, Symbol, OpenBracket, String, CloseBracket, Equal, Number, + And, + Symbol, Equal, String, + Add, Bool, + } + if len(tokens) != len(exp) { + t.Fatalf("unexpected number of tokens: %d", len(tokens)) + } + + for i := range tokens { + if tokens[i].Kind != exp[i] { + t.Errorf("unexpected token kind at position %d: exp %v, got %v", i, exp[i], tokens[i].Kind) + return + } + } +} diff --git a/selector/parser.go b/selector/parser.go new file mode 100644 index 00000000..cc063eea --- /dev/null +++ b/selector/parser.go @@ -0,0 +1,16 @@ +package selector + +import ( + "github.com/tomwright/dasel/v2/selector/ast" + "github.com/tomwright/dasel/v2/selector/lexer" + "github.com/tomwright/dasel/v2/selector/parser" +) + +func Parse(selector string) (ast.Expr, error) { + tokens, err := lexer.NewTokenizer(selector).Tokenize() + if err != nil { + return nil, err + } + + return parser.NewParser(tokens).Parse() +} diff --git a/selector/parser/error.go b/selector/parser/error.go new file mode 100644 index 00000000..e69e9746 --- /dev/null +++ b/selector/parser/error.go @@ -0,0 +1,24 @@ +package parser + +import ( + "fmt" + + "github.com/tomwright/dasel/v2/selector/lexer" +) + +type PositionalError struct { + Position int + Err error +} + +func (e *PositionalError) Error() string { + return fmt.Sprintf("%v. Position %d.", e.Err, e.Position) +} + +type UnexpectedTokenError struct { + Token lexer.Token +} + +func (e *UnexpectedTokenError) Error() string { + return fmt.Sprintf("unexpected token: %s at position %d.", e.Token.Value, e.Token.Pos) +} diff --git a/selector/parser/parse_literal.go b/selector/parser/parse_literal.go new file mode 100644 index 00000000..152fc969 --- /dev/null +++ b/selector/parser/parse_literal.go @@ -0,0 +1,45 @@ +package parser + +import ( + "strconv" + "strings" + + "github.com/tomwright/dasel/v2/selector/ast" +) + +func parseStringLiteral(p *Parser) (ast.Expr, error) { + token := p.current() + p.advance() + return &ast.StringExpr{ + Value: token.Value, + }, nil +} + +func parseBoolLiteral(p *Parser) (ast.Expr, error) { + token := p.current() + p.advance() + return &ast.BoolExpr{ + Value: strings.EqualFold(token.Value, "true"), + }, nil +} + +func parseNumberLiteral(p *Parser) (ast.Expr, error) { + token := p.current() + p.advance() + if strings.Contains(token.Value, ".") { + value, err := strconv.ParseFloat(token.Value, 64) + if err != nil { + return nil, err + } + return &ast.NumberFloatExpr{ + Value: value, + }, nil + } + value, err := strconv.ParseInt(token.Value, 10, 64) + if err != nil { + return nil, err + } + return &ast.NumberIntExpr{ + Value: value, + }, nil +} diff --git a/selector/parser/parse_symbol.go b/selector/parser/parse_symbol.go new file mode 100644 index 00000000..9f082f22 --- /dev/null +++ b/selector/parser/parse_symbol.go @@ -0,0 +1,93 @@ +package parser + +import ( + "github.com/tomwright/dasel/v2/selector/ast" + "github.com/tomwright/dasel/v2/selector/lexer" +) + +func parseArray(p *Parser) (ast.Expr, error) { + // Handle index (from bracket) + p.advance() + + // todo : handle spread operator + + if !p.current().IsKind(lexer.Number) { + return nil, &UnexpectedTokenError{ + Token: p.current(), + } + } + index, err := p.parseExpression() + if err != nil { + return nil, err + } + if err := p.expect(p.current(), lexer.CloseBracket); err != nil { + return nil, err + } + p.advance() + + return &ast.CallExpr{ + Function: "index", + Args: ast.Expressions{index}, + }, nil +} + +func parseSymbol(p *Parser) (ast.Expr, error) { + token := p.current() + + next := p.peek() + + // Handle functions + if next.IsKind(lexer.OpenParen) { + p.advanceN(2) + args, err := parseArgs(p) + if err != nil { + return nil, err + } + return &ast.CallExpr{ + Function: token.Value, + Args: args, + }, nil + } + + // Handle index (before bracket) + if next.IsKind(lexer.OpenBracket) { + p.advance() + return &ast.CallExpr{ + Function: "property", + Args: ast.Expressions{&ast.StringExpr{Value: token.Value}}, + }, nil + } + + if next.IsKind(lexer.Dot, lexer.EOF, lexer.Comma) { + p.advance() + return &ast.CallExpr{ + Function: "property", + Args: []ast.Expr{&ast.StringExpr{Value: token.Value}}, + }, nil + } + + return nil, &UnexpectedTokenError{ + Token: next, + } +} + +func parseArgs(p *Parser) ([]ast.Expr, error) { + args := make([]ast.Expr, 0) + for p.hasToken() { + if p.current().IsKind(lexer.CloseParen) { + p.advance() + break + } + + arg, err := p.parseExpression() + if err != nil { + return nil, err + } + args = append(args, arg) + + if p.current().IsKind(lexer.Comma) { + p.advance() + } + } + return args, nil +} diff --git a/selector/parser/parser.go b/selector/parser/parser.go new file mode 100644 index 00000000..8c9b64a2 --- /dev/null +++ b/selector/parser/parser.go @@ -0,0 +1,107 @@ +package parser + +import ( + "fmt" + + "github.com/tomwright/dasel/v2/selector/ast" + "github.com/tomwright/dasel/v2/selector/lexer" +) + +type Parser struct { + tokens lexer.Tokens + i int +} + +func NewParser(tokens lexer.Tokens) *Parser { + return &Parser{ + tokens: tokens, + } +} + +func (p *Parser) Parse() (ast.Expr, error) { + var expressions ast.Expressions + for p.hasToken() { + if p.current().IsKind(lexer.EOF) { + break + } + if p.current().IsKind(lexer.Dot) { + p.advance() + continue + } + expr, err := p.parseExpression() + if err != nil { + return nil, err + } + expressions = append(expressions, expr) + } + if len(expressions) == 1 { + return expressions[0], nil + } + return &ast.ChainedExpr{Exprs: expressions}, nil +} + +func (p *Parser) parseExpression() (ast.Expr, error) { + switch p.current().Kind { + case lexer.String: + return parseStringLiteral(p) + case lexer.Number: + return parseNumberLiteral(p) + case lexer.Symbol: + return parseSymbol(p) + case lexer.OpenBracket: + return parseArray(p) + case lexer.Bool: + return parseBoolLiteral(p) + default: + return nil, &PositionalError{ + Position: p.current().Pos, + Err: fmt.Errorf("unhandled token: %v", p.current().Kind), + } + } +} + +func (p *Parser) hasToken() bool { + return p.i < len(p.tokens) && !p.tokens[p.i].IsKind(lexer.EOF) +} + +func (p *Parser) hasTokenN(n int) bool { + return p.i+n < len(p.tokens) && !p.tokens[p.i+n].IsKind(lexer.EOF) +} + +func (p *Parser) current() lexer.Token { + if p.hasToken() { + return p.tokens[p.i] + } + return lexer.Token{Kind: lexer.EOF} +} + +func (p *Parser) advance() lexer.Token { + p.i++ + return p.current() +} + +func (p *Parser) advanceN(n int) lexer.Token { + p.i += n + return p.current() +} + +func (p *Parser) peek() lexer.Token { + return p.peekN(1) +} + +func (p *Parser) peekN(n int) lexer.Token { + if p.i+n >= len(p.tokens) { + return lexer.Token{Kind: lexer.EOF} + } + return p.tokens[p.i+n] +} + +func (p *Parser) expect(t lexer.Token, kind ...lexer.TokenKind) error { + if t.IsKind(kind...) { + return nil + } + return &PositionalError{ + Position: t.Pos, + Err: fmt.Errorf("unexpected token: %v", t.Value), + } +} diff --git a/selector/parser/parser_test.go b/selector/parser/parser_test.go new file mode 100644 index 00000000..697c440f --- /dev/null +++ b/selector/parser/parser_test.go @@ -0,0 +1,110 @@ +package parser_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/tomwright/dasel/v2/selector/ast" + "github.com/tomwright/dasel/v2/selector/lexer" + "github.com/tomwright/dasel/v2/selector/parser" +) + +func TestParser_Parse(t *testing.T) { + testCases := []struct { + name string + input string + expected ast.Expr + }{ + { + name: "single property", + input: "foo", + expected: &ast.CallExpr{ + Function: "property", + Args: ast.Expressions{&ast.StringExpr{Value: "foo"}}, + }, + }, + { + name: "chained properties", + input: "foo.bar", + expected: ast.ChainedExpr{ + Exprs: ast.Expressions{ + &ast.CallExpr{ + Function: "property", + Args: ast.Expressions{&ast.StringExpr{Value: "foo"}}, + }, + &ast.CallExpr{ + Function: "property", + Args: ast.Expressions{&ast.StringExpr{Value: "bar"}}, + }, + }, + }, + }, + { + name: "single function with no args", + input: "all()", + expected: &ast.CallExpr{ + Function: "all", + Args: ast.Expressions{}, + }, + }, + { + name: "single function with various args", + input: "all(\"foo\", 'bar', false, TRUE, 123, 12.3, hello, funcOne(), funcTwo(1, 2, 3), asd[5])", + expected: &ast.CallExpr{ + Function: "all", + Args: ast.Expressions{ + &ast.StringExpr{Value: "foo"}, + &ast.StringExpr{Value: "bar"}, + &ast.BoolExpr{Value: false}, + &ast.BoolExpr{Value: true}, + &ast.NumberIntExpr{Value: 123}, + &ast.NumberFloatExpr{Value: 12.3}, + &ast.CallExpr{ + Function: "property", + Args: ast.Expressions{&ast.StringExpr{Value: "hello"}}, + }, + &ast.CallExpr{ + Function: "funcOne", + Args: ast.Expressions{}, + }, + &ast.CallExpr{ + Function: "funcTwo", + Args: ast.Expressions{ + &ast.NumberIntExpr{Value: 1}, + &ast.NumberIntExpr{Value: 2}, + &ast.NumberIntExpr{Value: 3}, + }, + }, + &ast.CallExpr{ + Function: "property", + Args: ast.Expressions{&ast.StringExpr{Value: "asd"}}, + }, + &ast.CallExpr{ + Function: "index", + Args: ast.Expressions{ + &ast.NumberIntExpr{Value: 5}, + }, + }, + }, + }, + }, + } + + for _, testCase := range testCases { + tc := testCase + t.Run(tc.name, func(t *testing.T) { + tokens, err := lexer.NewTokenizer(tc.input).Tokenize() + if err != nil { + t.Fatal(err) + } + got, err := parser.NewParser(tokens).Parse() + if err != nil { + t.Fatal(err) + } + + if !cmp.Equal(tc.expected, got) { + t.Fatalf("unexpected result: %s", cmp.Diff(tc.expected, got)) + } + }) + } +} diff --git a/storage/toml_test.go b/storage/toml_test.go index 6687f7b1..ef6637cb 100644 --- a/storage/toml_test.go +++ b/storage/toml_test.go @@ -1,11 +1,12 @@ package storage_test import ( - "github.com/tomwright/dasel/v2" - "github.com/tomwright/dasel/v2/storage" "reflect" "strings" "testing" + + "github.com/tomwright/dasel/v2" + "github.com/tomwright/dasel/v2/storage" ) var tomlBytes = []byte(`names = ['John', 'Frank'] @@ -140,7 +141,7 @@ func TestTOMLParser_ToBytes(t *testing.T) { } }) // t.Run("time.Time", func(t *testing.T) { - // v, _ := time.Parse(time.RFC3339, "2022-01-02T12:34:56Z") + // v, _ := time.Tokenize(time.RFC3339, "2022-01-02T12:34:56Z") // got, err := (&storage.TOMLParser{}).ToBytes(dasel.ValueOf(v)) // if err != nil { // t.Errorf("unexpected error: %s", err) From 20f73abc2059ec82fb35d92dc83903b0f314b59b Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Wed, 25 Sep 2024 00:56:51 +0100 Subject: [PATCH 02/56] Add spread selectors --- selector/lexer/token.go | 2 + selector/lexer/tokenize.go | 4 ++ selector/parser/error.go | 2 +- selector/parser/parse_symbol.go | 85 +++++++++++++++++++++++++++++++-- selector/parser/parser.go | 7 ++- selector/parser/parser_test.go | 43 ++++++++++++++++- 6 files changed, 132 insertions(+), 11 deletions(-) diff --git a/selector/lexer/token.go b/selector/lexer/token.go index b5c6130e..7f8bd091 100644 --- a/selector/lexer/token.go +++ b/selector/lexer/token.go @@ -14,6 +14,8 @@ const ( Colon OpenBracket CloseBracket + OpenCurly + CloseCurly OpenParen CloseParen Equal diff --git a/selector/lexer/tokenize.go b/selector/lexer/tokenize.go index e6819605..46eae87a 100644 --- a/selector/lexer/tokenize.go +++ b/selector/lexer/tokenize.go @@ -57,6 +57,10 @@ func (p *Tokenizer) parseCurRune() (Token, error) { return NewToken(OpenParen, "(", p.i, 1), nil case ')': return NewToken(CloseParen, ")", p.i, 1), nil + case '{': + return NewToken(OpenCurly, "{", p.i, 1), nil + case '}': + return NewToken(CloseCurly, "}", p.i, 1), nil case '=': if p.peekRuneEqual(p.i+1, '=') { return NewToken(Equal, "==", p.i, 2), nil diff --git a/selector/parser/error.go b/selector/parser/error.go index e69e9746..0f03e3a4 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: %s at position %d.", e.Token.Value, e.Token.Pos) + return fmt.Sprintf("unexpected token %v %q at position %d.", e.Token.Kind, e.Token.Value, e.Token.Pos) } diff --git a/selector/parser/parse_symbol.go b/selector/parser/parse_symbol.go index 9f082f22..db75d7d6 100644 --- a/selector/parser/parse_symbol.go +++ b/selector/parser/parse_symbol.go @@ -5,16 +5,91 @@ import ( "github.com/tomwright/dasel/v2/selector/lexer" ) -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) { // Handle index (from bracket) p.advance() - // todo : handle spread operator + // Spread [...] + if p.current().IsKind(lexer.Dot) && p.peekN(1).IsKind(lexer.Dot) && p.peekN(2).IsKind(lexer.Dot) { + p.advanceN(3) + if err := p.expect(p.current(), lexer.CloseBracket); err != nil { + return nil, err + } + p.advance() + return &ast.CallExpr{ + Function: "all", + Args: ast.Expressions{}, + }, nil + } + + // Range [1:2] + if p.current().IsKind(lexer.Number) && p.peekN(1).IsKind(lexer.Colon) && p.peekN(2).IsKind(lexer.Number) { + from, err := p.parseExpression() + if err != nil { + return nil, err + } + p.advance() + to, err := p.parseExpression() + if err != nil { + return nil, err + } + if err := p.expect(p.current(), lexer.CloseBracket); err != nil { + return nil, err + } + p.advance() + return &ast.CallExpr{ + Function: "range", + Args: ast.Expressions{ + from, to, + }, + }, nil + } - if !p.current().IsKind(lexer.Number) { - return nil, &UnexpectedTokenError{ - Token: p.current(), + // Range [:2] + if p.current().IsKind(lexer.Colon) && p.peekN(1).IsKind(lexer.Number) { + from := &ast.NumberIntExpr{Value: -1} + p.advanceN(1) + to, err := p.parseExpression() + if err != nil { + return nil, err + } + if err := p.expect(p.current(), lexer.CloseBracket); err != nil { + return nil, err + } + p.advance() + return &ast.CallExpr{ + Function: "range", + Args: ast.Expressions{ + from, to, + }, + }, nil + } + + // Range [1:] + if p.current().IsKind(lexer.Number) && p.peekN(1).IsKind(lexer.Colon) { + from, err := p.parseExpression() + if err != nil { + return nil, err } + p.advanceN(1) + to := &ast.NumberIntExpr{Value: -1} + if err := p.expect(p.current(), lexer.CloseBracket); err != nil { + return nil, err + } + p.advance() + return &ast.CallExpr{ + Function: "range", + Args: ast.Expressions{ + from, to, + }, + }, nil + } + + // Array index [1] + if err := p.expect(p.current(), lexer.Number); err != nil { + return nil, err } index, err := p.parseExpression() if err != nil { diff --git a/selector/parser/parser.go b/selector/parser/parser.go index 8c9b64a2..f25272d4 100644 --- a/selector/parser/parser.go +++ b/selector/parser/parser.go @@ -49,13 +49,12 @@ func (p *Parser) parseExpression() (ast.Expr, error) { case lexer.Symbol: return parseSymbol(p) case lexer.OpenBracket: - return parseArray(p) + return parseSquareBrackets(p) case lexer.Bool: return parseBoolLiteral(p) default: - return nil, &PositionalError{ - Position: p.current().Pos, - Err: fmt.Errorf("unhandled token: %v", p.current().Kind), + return nil, &UnexpectedTokenError{ + Token: p.current(), } } } diff --git a/selector/parser/parser_test.go b/selector/parser/parser_test.go index 697c440f..4e3c6781 100644 --- a/selector/parser/parser_test.go +++ b/selector/parser/parser_test.go @@ -49,7 +49,7 @@ func TestParser_Parse(t *testing.T) { }, { name: "single function with various args", - input: "all(\"foo\", 'bar', false, TRUE, 123, 12.3, hello, funcOne(), funcTwo(1, 2, 3), asd[5])", + input: "all(\"foo\", 'bar', false, TRUE, 123, 12.3, hello, funcOne(), funcTwo(1, 2, 3), asd[5], asd[...], asd[0:1], asd[2:], asd[:2])", expected: &ast.CallExpr{ Function: "all", Args: ast.Expressions{ @@ -85,6 +85,47 @@ func TestParser_Parse(t *testing.T) { &ast.NumberIntExpr{Value: 5}, }, }, + &ast.CallExpr{ + Function: "property", + Args: ast.Expressions{&ast.StringExpr{Value: "asd"}}, + }, + &ast.CallExpr{ + Function: "all", + Args: ast.Expressions{}, + }, + &ast.CallExpr{ + Function: "property", + Args: ast.Expressions{&ast.StringExpr{Value: "asd"}}, + }, + &ast.CallExpr{ + Function: "range", + Args: ast.Expressions{ + &ast.NumberIntExpr{Value: 0}, + &ast.NumberIntExpr{Value: 1}, + }, + }, + &ast.CallExpr{ + Function: "property", + Args: ast.Expressions{&ast.StringExpr{Value: "asd"}}, + }, + &ast.CallExpr{ + Function: "range", + Args: ast.Expressions{ + &ast.NumberIntExpr{Value: 2}, + &ast.NumberIntExpr{Value: -1}, + }, + }, + &ast.CallExpr{ + Function: "property", + Args: ast.Expressions{&ast.StringExpr{Value: "asd"}}, + }, + &ast.CallExpr{ + Function: "range", + Args: ast.Expressions{ + &ast.NumberIntExpr{Value: -1}, + &ast.NumberIntExpr{Value: 2}, + }, + }, }, }, }, From e09907d45c4dbde4ac94701b994628f17dc2dc38 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Mon, 30 Sep 2024 19:41:05 +0100 Subject: [PATCH 03/56] Start getting v3 core functionality working --- .github/workflows/build-dev.yaml | 2 +- .github/workflows/build-test.yaml | 2 +- .github/workflows/build.yaml | 2 +- CHANGELOG.md | 2 +- README.md | 8 +- cmd/dasel/main.go | 3 +- dencoding/json_decoder_test.go | 3 +- dencoding/json_encoder_test.go | 3 +- dencoding/toml_decoder_test.go | 3 +- dencoding/toml_encoder_test.go | 3 +- dencoding/yaml_decoder.go | 2 +- dencoding/yaml_decoder_test.go | 2 +- dencoding/yaml_encoder.go | 5 +- dencoding/yaml_encoder_test.go | 3 +- error_test.go | 2 +- execution/execute.go | 157 ++++++++++++ execution/execute_func.go | 64 +++++ execution/execute_literal.go | 30 +++ execution/execute_map.go | 37 +++ execution/execute_object.go | 52 ++++ execution/execute_test.go | 212 +++++++++++++++ execution/func.go | 55 ++++ func_all.go | 3 +- func_equal.go | 3 +- func_join.go | 3 +- func_join_test.go | 3 +- func_keys.go | 3 +- func_keys_test.go | 3 +- func_less_than.go | 3 +- func_more_than.go | 3 +- func_string.go | 2 +- go.mod | 2 +- internal/command/delete.go | 2 +- internal/command/options.go | 6 +- internal/command/put.go | 7 +- internal/command/root.go | 2 +- internal/command/select.go | 2 +- model/error.go | 11 + model/generic.go | 1 + model/value.go | 70 +++++ model/value_literal.go | 94 +++++++ model/value_map.go | 50 ++++ model/value_slice.go | 62 +++++ model/value_slice_test.go | 28 ++ model/values.go | 3 + ptr/to.go | 5 + selector/ast/expression_complex.go | 56 +++- selector/ast/expression_literal.go | 6 - selector/lexer/token.go | 10 +- selector/lexer/tokenize.go | 13 +- selector/lexer/tokenize_test.go | 4 +- selector/parser.go | 6 +- selector/parser/error.go | 2 +- selector/parser/parse_array.go | 111 ++++++++ selector/parser/parse_func.go | 51 ++++ selector/parser/parse_literal.go | 45 +++- selector/parser/parse_map.go | 53 ++++ selector/parser/parse_object.go | 91 +++++++ selector/parser/parse_symbol.go | 164 ++---------- selector/parser/parser.go | 92 ++++++- selector/parser/parser_test.go | 397 ++++++++++++++++++++--------- storage/csv.go | 7 +- storage/csv_test.go | 7 +- storage/json.go | 5 +- storage/json_test.go | 7 +- storage/parser.go | 2 +- storage/parser_test.go | 6 +- storage/plain.go | 3 +- storage/plain_test.go | 5 +- storage/toml.go | 5 +- storage/toml_test.go | 4 +- storage/xml.go | 5 +- storage/xml_test.go | 5 +- storage/yaml.go | 7 +- storage/yaml_test.go | 7 +- value.go | 4 +- 76 files changed, 1818 insertions(+), 385 deletions(-) create mode 100644 execution/execute.go create mode 100644 execution/execute_func.go create mode 100644 execution/execute_literal.go create mode 100644 execution/execute_map.go create mode 100644 execution/execute_object.go create mode 100644 execution/execute_test.go create mode 100644 execution/func.go create mode 100644 model/error.go create mode 100644 model/generic.go create mode 100644 model/value.go create mode 100644 model/value_literal.go create mode 100644 model/value_map.go create mode 100644 model/value_slice.go create mode 100644 model/value_slice_test.go create mode 100644 model/values.go create mode 100644 ptr/to.go create mode 100644 selector/parser/parse_array.go create mode 100644 selector/parser/parse_func.go create mode 100644 selector/parser/parse_map.go create mode 100644 selector/parser/parse_object.go diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index a955362d..d6d7baa1 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -69,7 +69,7 @@ jobs: - name: Set env run: echo RELEASE_VERSION=development >> $GITHUB_ENV - name: Build - run: GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} CGO_ENABLED=0 go build -o target/release/${{ matrix.artifact_name }} -ldflags="-X 'github.com/tomwright/dasel/v2/internal.Version=${{ env.RELEASE_VERSION }}'" ./cmd/dasel + run: GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} CGO_ENABLED=0 go build -o target/release/${{ matrix.artifact_name }} -ldflags="-X 'github.com/tomwright/dasel/v3/internal.Version=${{ env.RELEASE_VERSION }}'" ./cmd/dasel - name: Test version if: matrix.test_version == true run: ./target/release/${{ matrix.artifact_name }} --version diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 0accb8e0..2c09d6db 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -81,7 +81,7 @@ jobs: - name: Set env run: echo RELEASE_VERSION=development >> $GITHUB_ENV - name: Build - run: GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} CGO_ENABLED=0 go build -o target/release/${{ matrix.artifact_name }} -ldflags="-X 'github.com/tomwright/dasel/v2/internal.Version=${{ env.RELEASE_VERSION }}'" ./cmd/dasel + run: GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} CGO_ENABLED=0 go build -o target/release/${{ matrix.artifact_name }} -ldflags="-X 'github.com/tomwright/dasel/v3/internal.Version=${{ env.RELEASE_VERSION }}'" ./cmd/dasel - name: Test version if: matrix.test_version == true run: ./target/release/${{ matrix.artifact_name }} --version diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 78832c17..01a2fb5b 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -77,7 +77,7 @@ jobs: - name: Set env run: echo RELEASE_VERSION=${GITHUB_REF:10} >> $GITHUB_ENV - name: Build - run: GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} CGO_ENABLED=0 go build -o target/release/${{ matrix.artifact_name }} -ldflags="-X 'github.com/tomwright/dasel/v2/internal.Version=${{ env.RELEASE_VERSION }}'" ./cmd/dasel + run: GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} CGO_ENABLED=0 go build -o target/release/${{ matrix.artifact_name }} -ldflags="-X 'github.com/tomwright/dasel/v3/internal.Version=${{ env.RELEASE_VERSION }}'" ./cmd/dasel - name: Test version if: matrix.test_version == true run: ./target/release/${{ matrix.artifact_name }} --version diff --git a/CHANGELOG.md b/CHANGELOG.md index ea05913d..55d951fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -154,7 +154,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Changed go module to `github.com/tomwright/dasel/v2` to ensure it works correctly with go modules. +- Changed go module to `github.com/tomwright/dasel/v3` to ensure it works correctly with go modules. ## [v2.1.0] - 2023-01-11 diff --git a/README.md b/README.md index e970e997..6e2e8796 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # dasel [![Gitbook](https://badges.aleen42.com/src/gitbook_1.svg)](https://daseldocs.tomwright.me) -[![Go Report Card](https://goreportcard.com/badge/github.com/TomWright/dasel/v2)](https://goreportcard.com/report/github.com/TomWright/dasel/v2) -[![PkgGoDev](https://pkg.go.dev/badge/github.com/tomwright/dasel)](https://pkg.go.dev/github.com/tomwright/dasel/v2) +[![Go Report Card](https://goreportcard.com/badge/github.com/tomwright/dasel/v3)](https://goreportcard.com/report/github.com/tomwright/dasel/v3) +[![PkgGoDev](https://pkg.go.dev/badge/github.com/tomwright/dasel)](https://pkg.go.dev/github.com/tomwright/dasel/v3) ![Test](https://github.com/TomWright/dasel/workflows/Test/badge.svg) ![Build](https://github.com/TomWright/dasel/workflows/Build/badge.svg) [![codecov](https://codecov.io/gh/TomWright/dasel/branch/master/graph/badge.svg)](https://codecov.io/gh/TomWright/dasel) @@ -75,7 +75,7 @@ brew install dasel You can also install a [development version](https://daseldocs.tomwright.me/installation#development-version) with: ```bash -go install github.com/tomwright/dasel/v2/cmd/dasel@master +go install github.com/tomwright/dasel/v3/cmd/dasel@master ``` For more information see the [installation documentation](https://daseldocs.tomwright.me/installation). @@ -181,7 +181,7 @@ Please [open a discussion](https://github.com/TomWright/dasel/discussions) if: - Uses a [standard query/selector syntax](https://daseldocs.tomwright.me/functions/selector-overview) across all data formats. - Zero runtime dependencies. - [Available on Linux, Mac and Windows](https://daseldocs.tomwright.me/installation). -- Available to [import and use in your own projects](https://pkg.go.dev/github.com/tomwright/dasel/v2). +- Available to [import and use in your own projects](https://pkg.go.dev/github.com/tomwright/dasel/v3). - [Run via Docker](https://daseldocs.tomwright.me/installation#docker). - [Faster than jq/yq](#benchmarks). - [Pre-commit hooks](#pre-commit). diff --git a/cmd/dasel/main.go b/cmd/dasel/main.go index 8066eceb..6921e1c5 100644 --- a/cmd/dasel/main.go +++ b/cmd/dasel/main.go @@ -1,8 +1,9 @@ package main import ( - "github.com/tomwright/dasel/v2/internal/command" "os" + + "github.com/tomwright/dasel/v3/internal/command" ) func main() { diff --git a/dencoding/json_decoder_test.go b/dencoding/json_decoder_test.go index 2aca6aca..2d95e146 100644 --- a/dencoding/json_decoder_test.go +++ b/dencoding/json_decoder_test.go @@ -2,10 +2,11 @@ package dencoding_test import ( "bytes" - "github.com/tomwright/dasel/v2/dencoding" "io" "reflect" "testing" + + "github.com/tomwright/dasel/v3/dencoding" ) func TestJSONDecoder_Decode(t *testing.T) { diff --git a/dencoding/json_encoder_test.go b/dencoding/json_encoder_test.go index 513e1837..7bb557f8 100644 --- a/dencoding/json_encoder_test.go +++ b/dencoding/json_encoder_test.go @@ -2,8 +2,9 @@ package dencoding_test import ( "bytes" - "github.com/tomwright/dasel/v2/dencoding" "testing" + + "github.com/tomwright/dasel/v3/dencoding" ) func TestJSONEncoder_Encode(t *testing.T) { diff --git a/dencoding/toml_decoder_test.go b/dencoding/toml_decoder_test.go index 6ade2976..8718662e 100644 --- a/dencoding/toml_decoder_test.go +++ b/dencoding/toml_decoder_test.go @@ -2,10 +2,11 @@ package dencoding_test import ( "bytes" - "github.com/tomwright/dasel/v2/dencoding" "io" "reflect" "testing" + + "github.com/tomwright/dasel/v3/dencoding" ) func TestTOMLDecoder_Decode(t *testing.T) { diff --git a/dencoding/toml_encoder_test.go b/dencoding/toml_encoder_test.go index eb66f741..36ac7fb3 100644 --- a/dencoding/toml_encoder_test.go +++ b/dencoding/toml_encoder_test.go @@ -2,8 +2,9 @@ package dencoding_test import ( "bytes" - "github.com/tomwright/dasel/v2/dencoding" "testing" + + "github.com/tomwright/dasel/v3/dencoding" ) func TestTOMLEncoder_Encode(t *testing.T) { diff --git a/dencoding/yaml_decoder.go b/dencoding/yaml_decoder.go index a4d258cb..a4be60d1 100644 --- a/dencoding/yaml_decoder.go +++ b/dencoding/yaml_decoder.go @@ -7,7 +7,7 @@ import ( "strconv" "time" - "github.com/tomwright/dasel/v2/util" + "github.com/tomwright/dasel/v3/util" "gopkg.in/yaml.v3" ) diff --git a/dencoding/yaml_decoder_test.go b/dencoding/yaml_decoder_test.go index 172bc53c..bb62e415 100644 --- a/dencoding/yaml_decoder_test.go +++ b/dencoding/yaml_decoder_test.go @@ -6,7 +6,7 @@ import ( "reflect" "testing" - "github.com/tomwright/dasel/v2/dencoding" + "github.com/tomwright/dasel/v3/dencoding" ) func TestYAMLDecoder_Decode(t *testing.T) { diff --git a/dencoding/yaml_encoder.go b/dencoding/yaml_encoder.go index cb0d718a..a25c2f74 100644 --- a/dencoding/yaml_encoder.go +++ b/dencoding/yaml_encoder.go @@ -1,10 +1,11 @@ package dencoding import ( - "github.com/tomwright/dasel/v2/util" - "gopkg.in/yaml.v3" "io" "strconv" + + "github.com/tomwright/dasel/v3/util" + "gopkg.in/yaml.v3" ) // YAMLEncoder wraps a standard yaml encoder to implement custom ordering logic. diff --git a/dencoding/yaml_encoder_test.go b/dencoding/yaml_encoder_test.go index 66061b0b..a52ea0d4 100644 --- a/dencoding/yaml_encoder_test.go +++ b/dencoding/yaml_encoder_test.go @@ -2,8 +2,9 @@ package dencoding_test import ( "bytes" - "github.com/tomwright/dasel/v2/dencoding" "testing" + + "github.com/tomwright/dasel/v3/dencoding" ) func TestYAMLEncoder_Encode(t *testing.T) { diff --git a/error_test.go b/error_test.go index d5118507..8e999127 100644 --- a/error_test.go +++ b/error_test.go @@ -6,7 +6,7 @@ import ( "reflect" "testing" - "github.com/tomwright/dasel/v2" + "github.com/tomwright/dasel/v3" ) func TestErrorMessages(t *testing.T) { diff --git a/execution/execute.go b/execution/execute.go new file mode 100644 index 00000000..8681cb1a --- /dev/null +++ b/execution/execute.go @@ -0,0 +1,157 @@ +package execution + +import ( + "fmt" + + "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/selector/ast" + "github.com/tomwright/dasel/v3/selector/lexer" + "github.com/tomwright/dasel/v3/selector/parser" +) + +func ExecuteSelector(selector string, value *model.Value) (*model.Value, error) { + tokens, err := lexer.NewTokenizer(selector).Tokenize() + if err != nil { + return nil, fmt.Errorf("error tokenizing selector: %w", err) + } + + expr, err := parser.NewParser(tokens).Parse() + if err != nil { + return nil, fmt.Errorf("error parsing selector: %w", err) + } + + res, err := ExecuteAST(expr, value) + if err != nil { + return nil, fmt.Errorf("error executing selector: %w", err) + } + + return res, nil +} + +type expressionExecutor func(data *model.Value) (*model.Value, error) + +func ExecuteAST(expr ast.Expr, value *model.Value) (*model.Value, error) { + executor, err := exprExecutor(expr) + 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) + } + + return res, nil +} + +func exprExecutor(expr ast.Expr) (expressionExecutor, error) { + switch e := expr.(type) { + case ast.BinaryExpr: + return binaryExprExecutor(e) + case ast.CallExpr: + return callExprExecutor(e) + case ast.ChainedExpr: + return chainedExprExecutor(e) + case ast.SpreadExpr: + return spreadExprExecutor() + case ast.RangeExpr: + return rangeExprExecutor(e) + case ast.IndexExpr: + return indexExprExecutor(e) + case ast.PropertyExpr: + return propertyExprExecutor(e) + case ast.NumberIntExpr: + return numberIntExprExecutor(e) + case ast.NumberFloatExpr: + return numberFloatExprExecutor(e) + case ast.StringExpr: + return stringExprExecutor(e) + case ast.BoolExpr: + return boolExprExecutor(e) + case ast.ObjectExpr: + return objectExprExecutor(e) + case ast.MapExpr: + return mapExprExecutor(e) + default: + return nil, fmt.Errorf("unhandled expression type: %T", e) + } +} + +func binaryExprExecutor(e ast.BinaryExpr) (expressionExecutor, error) { + return func(data *model.Value) (*model.Value, error) { + panic("not implemented") + }, nil +} + +func chainedExprExecutor(e ast.ChainedExpr) (expressionExecutor, error) { + return func(data *model.Value) (*model.Value, error) { + for _, expr := range e.Exprs { + res, err := ExecuteAST(expr, data) + if err != nil { + return nil, fmt.Errorf("error executing expression: %w", err) + } + data = res + } + return data, nil + }, nil +} + +func spreadExprExecutor() (expressionExecutor, error) { + return func(data *model.Value) (*model.Value, error) { + s := model.NewSliceValue() + + switch { + case data.IsSlice(): + v, err := data.SliceValue() + if err != nil { + return nil, fmt.Errorf("error getting slice value: %w", err) + } + for _, sv := range v { + s.Append(model.NewValue(sv)) + } + case data.IsMap(): + v, err := data.MapValue() + if err != nil { + return nil, fmt.Errorf("error getting map value: %w", err) + } + for _, kv := range v.KeyValues() { + s.Append(model.NewValue(kv.Value)) + } + default: + return nil, fmt.Errorf("cannot spread on type %s", data.Type()) + } + + return s, nil + }, nil +} + +func rangeExprExecutor(e ast.RangeExpr) (expressionExecutor, error) { + return func(data *model.Value) (*model.Value, error) { + panic("not implemented") + }, nil +} + +func indexExprExecutor(e ast.IndexExpr) (expressionExecutor, error) { + return func(data *model.Value) (*model.Value, error) { + panic("not implemented") + }, nil +} + +func propertyExprExecutor(e ast.PropertyExpr) (expressionExecutor, error) { + return func(data *model.Value) (*model.Value, error) { + if !data.IsMap() { + return nil, fmt.Errorf("expected map, got %s", data.Type()) + } + key, err := ExecuteAST(e.Property, data) + if err != nil { + return nil, fmt.Errorf("error evaluating property: %w", err) + } + if !key.IsString() { + return nil, fmt.Errorf("expected property to resolve to string, got %s", key.Type()) + } + keyStr, err := key.StringValue() + if err != nil { + return nil, fmt.Errorf("error getting string value: %w", err) + } + return data.GetMapKey(keyStr) + }, nil +} diff --git a/execution/execute_func.go b/execution/execute_func.go new file mode 100644 index 00000000..83415bbd --- /dev/null +++ b/execution/execute_func.go @@ -0,0 +1,64 @@ +package execution + +import ( + "fmt" + + "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/selector/ast" +) + +func prepareArgs(data *model.Value, argsE ast.Expressions) (model.Values, error) { + args := make(model.Values, 0) + for i, arg := range argsE { + res, err := ExecuteAST(arg, data) + if err != nil { + return nil, fmt.Errorf("error evaluating argument %d: %w", i, err) + } + args = append(args, res) + } + return args, nil +} + +func callSingleExecutor(f singleResponseFunc, argsE ast.Expressions) (expressionExecutor, error) { + return func(data *model.Value) (*model.Value, error) { + args, err := prepareArgs(data, argsE) + if err != nil { + return nil, fmt.Errorf("error preparing arguments: %w", err) + } + + res, err := f(data, args) + if err != nil { + return nil, fmt.Errorf("error executing function: %w", err) + } + + return res, nil + }, nil +} + +func callMultiExecutor(f multiResponseFunc, argsE ast.Expressions) (expressionExecutor, error) { + return func(data *model.Value) (*model.Value, error) { + panic("multi response functions are not supported") + //args, err := prepareArgs(data, argsE) + //if err != nil { + // return nil, fmt.Errorf("error preparing arguments: %w", err) + //} + + //res, err := f(data, args) + //if err != nil { + // return nil, fmt.Errorf("error executing function: %w", err) + //} + + //return res, nil + }, nil +} + +func callExprExecutor(e ast.CallExpr) (expressionExecutor, error) { + if f, ok := singleResponseFuncLookup[e.Function]; ok { + return callSingleExecutor(f, e.Args) + } + if f, ok := multiResponseFuncLookup[e.Function]; ok { + return callMultiExecutor(f, e.Args) + } + + return nil, fmt.Errorf("unknown function: %q", e.Function) +} diff --git a/execution/execute_literal.go b/execution/execute_literal.go new file mode 100644 index 00000000..97e5ea12 --- /dev/null +++ b/execution/execute_literal.go @@ -0,0 +1,30 @@ +package execution + +import ( + "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/selector/ast" +) + +func numberIntExprExecutor(e ast.NumberIntExpr) (expressionExecutor, error) { + return func(data *model.Value) (*model.Value, error) { + return model.NewIntValue(e.Value), nil + }, nil +} + +func numberFloatExprExecutor(e ast.NumberFloatExpr) (expressionExecutor, error) { + return func(data *model.Value) (*model.Value, error) { + return model.NewFloatValue(e.Value), nil + }, nil +} + +func stringExprExecutor(e ast.StringExpr) (expressionExecutor, error) { + return func(data *model.Value) (*model.Value, error) { + return model.NewStringValue(e.Value), nil + }, nil +} + +func boolExprExecutor(e ast.BoolExpr) (expressionExecutor, error) { + return func(data *model.Value) (*model.Value, error) { + return model.NewBoolValue(e.Value), nil + }, nil +} diff --git a/execution/execute_map.go b/execution/execute_map.go new file mode 100644 index 00000000..1bd0a441 --- /dev/null +++ b/execution/execute_map.go @@ -0,0 +1,37 @@ +package execution + +import ( + "fmt" + + "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/selector/ast" +) + +func mapExprExecutor(e ast.MapExpr) (expressionExecutor, error) { + return func(data *model.Value) (*model.Value, error) { + if !data.IsSlice() { + return nil, fmt.Errorf("cannot map over non-array") + } + sliceLen, err := data.SliceLen() + if err != nil { + return nil, fmt.Errorf("error getting slice length: %w", err) + } + res := model.NewSliceValue() + + for i := 0; i < sliceLen; i++ { + item, err := data.GetSliceIndex(i) + if err != nil { + return nil, fmt.Errorf("error getting slice index: %w", err) + } + for _, expr := range e.Exprs { + item, err = ExecuteAST(expr, item) + if err != nil { + return nil, err + } + } + res.Append(item) + } + + return res, nil + }, nil +} diff --git a/execution/execute_object.go b/execution/execute_object.go new file mode 100644 index 00000000..ed350ae8 --- /dev/null +++ b/execution/execute_object.go @@ -0,0 +1,52 @@ +package execution + +import ( + "fmt" + + "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/selector/ast" +) + +func objectExprExecutor(e ast.ObjectExpr) (expressionExecutor, error) { + return func(data *model.Value) (*model.Value, error) { + obj := model.NewMapValue() + for _, p := range e.Pairs { + if ast.IsSpreadExpr(p.Key) && ast.IsSpreadExpr(p.Value) { + if !data.IsMap() { + return nil, fmt.Errorf("cannot spread non-object into object") + } + m, err := data.MapValue() + if err != nil { + return nil, fmt.Errorf("error getting map value: %w", err) + } + for _, kv := range m.KeyValues() { + if err := obj.SetMapKey(kv.Key, model.NewValue(kv.Value)); err != nil { + return nil, fmt.Errorf("error setting map key: %w", err) + } + } + continue + } + + if ast.IsSpreadExpr(p.Key) { + return nil, fmt.Errorf("cannot spread object key name") + } + + key, err := ExecuteAST(p.Key, data) + if err != nil { + return nil, fmt.Errorf("error evaluating key: %w", err) + } + if !key.IsString() { + return nil, fmt.Errorf("expected key to resolve to string, got %s", key.Type()) + } + val, err := ExecuteAST(p.Value, data) + if err != nil { + return nil, fmt.Errorf("error evaluating value: %w", err) + } + keyStr, err := key.StringValue() + if err := obj.SetMapKey(keyStr, val); err != nil { + return nil, fmt.Errorf("error setting map key: %w", err) + } + } + return obj, nil + }, nil +} diff --git a/execution/execute_test.go b/execution/execute_test.go new file mode 100644 index 00000000..959edb2c --- /dev/null +++ b/execution/execute_test.go @@ -0,0 +1,212 @@ +package execution_test + +import ( + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/tomwright/dasel/v3/dencoding" + "github.com/tomwright/dasel/v3/execution" + "github.com/tomwright/dasel/v3/model" +) + +func TestExecuteSelector_HappyPath(t *testing.T) { + type testCase struct { + in *model.Value + inFn func() *model.Value + s string + out *model.Value + outFn func() *model.Value + } + + runTest := func(tc testCase) func(t *testing.T) { + return func(t *testing.T) { + in := tc.in + if tc.inFn != nil { + in = tc.inFn() + } + exp := tc.out + if tc.outFn != nil { + exp = tc.outFn() + } + res, err := execution.ExecuteSelector(tc.s, in) + if err != nil { + t.Fatal(err) + } + + toInterface := func(v *model.Value) interface{} { + if v == nil { + return nil + } + if v.IsMap() { + m, _ := v.MapValue() + return m.KeyValues() + } + return v.UnpackKinds(reflect.Ptr).Interface() + } + expV, gotV := toInterface(exp), toInterface(res) + + if !cmp.Equal(expV, gotV, cmpopts.IgnoreUnexported(dencoding.Map{})) { + t.Errorf("unexpected result: %v", cmp.Diff(expV, gotV)) + } + } + } + + t.Run("literal", func(t *testing.T) { + t.Run("string", runTest(testCase{ + in: model.NewValue(nil), + s: `"hello"`, + out: model.NewStringValue("hello"), + })) + t.Run("int", runTest(testCase{ + in: model.NewValue(nil), + s: `123`, + out: model.NewIntValue(123), + })) + t.Run("float", runTest(testCase{ + in: model.NewValue(nil), + s: `123.4`, + out: model.NewFloatValue(123.4), + })) + t.Run("true", runTest(testCase{ + in: model.NewValue(nil), + s: `true`, + out: model.NewBoolValue(true), + })) + t.Run("false", runTest(testCase{ + in: model.NewValue(nil), + s: `false`, + out: model.NewBoolValue(false), + })) + }) + + t.Run("function", func(t *testing.T) { + t.Run("add", func(t *testing.T) { + t.Run("int", runTest(testCase{ + in: model.NewValue(nil), + s: `add(1, 2, 3)`, + out: model.NewIntValue(6), + })) + t.Run("float", runTest(testCase{ + in: model.NewValue(nil), + s: `add(1f, 2.5, 3.5)`, + out: model.NewFloatValue(7), + })) + t.Run("mixed", runTest(testCase{ + in: model.NewValue(nil), + s: `add(1, 2f)`, + out: model.NewFloatValue(3), + })) + }) + }) + + t.Run("get", func(t *testing.T) { + inputMap := func() *model.Value { + return model.NewValue( + dencoding.NewMap(). + Set("title", "Mr"). + Set("name", dencoding.NewMap(). + Set("first", "Tom"). + Set("last", "Wright")), + ) + } + t.Run("property", runTest(testCase{ + in: inputMap(), + s: `title`, + out: model.NewStringValue("Mr"), + })) + t.Run("nested property", runTest(testCase{ + in: inputMap(), + s: `name.first`, + out: model.NewStringValue("Tom"), + })) + }) + + t.Run("object", func(t *testing.T) { + inputMap := func() *model.Value { + return model.NewValue(dencoding.NewMap(). + Set("title", "Mr"). + Set("age", int64(30)). + Set("name", dencoding.NewMap(). + Set("first", "Tom"). + Set("last", "Wright"))) + } + t.Run("get", runTest(testCase{ + in: inputMap(), + s: `{title}`, + outFn: func() *model.Value { + return model.NewValue(dencoding.NewMap().Set("title", "Mr")) + //res := model.NewMapValue() + //_ = res.SetMapKey("title", model.NewStringValue("Mr")) + //return res + }, + })) + t.Run("get multiple", runTest(testCase{ + in: inputMap(), + s: `{title, age}`, + outFn: func() *model.Value { + return model.NewValue(dencoding.NewMap().Set("title", "Mr").Set("age", int64(30))) + //res := model.NewMapValue() + //_ = res.SetMapKey("title", model.NewStringValue("Mr")) + //_ = res.SetMapKey("age", model.NewIntValue(30)) + //return res + }, + })) + t.Run("get with spread", runTest(testCase{ + in: inputMap(), + s: `{...}`, + outFn: func() *model.Value { + res := inputMap() + return res + }, + })) + t.Run("set", runTest(testCase{ + in: inputMap(), + s: `{title="Mrs"}`, + outFn: func() *model.Value { + res := model.NewMapValue() + _ = res.SetMapKey("title", model.NewStringValue("Mrs")) + return res + }, + })) + t.Run("set with spread", runTest(testCase{ + in: inputMap(), + s: `{..., title="Mrs"}`, + outFn: func() *model.Value { + res := inputMap() + _ = res.SetMapKey("title", model.NewStringValue("Mrs")) + return res + }, + })) + }) + + t.Run("map", func(t *testing.T) { + t.Run("property from slice of maps", runTest(testCase{ + inFn: func() *model.Value { + r := model.NewSliceValue() + + m1 := model.NewMapValue() + _ = m1.SetMapKey("number", model.NewIntValue(1)) + m2 := model.NewMapValue() + _ = m2.SetMapKey("number", model.NewIntValue(2)) + m3 := model.NewMapValue() + _ = m3.SetMapKey("number", model.NewIntValue(3)) + + _ = r.Append(m1) + _ = r.Append(m2) + _ = r.Append(m3) + + return r + }, + s: `map(number)`, + outFn: func() *model.Value { + r := model.NewSliceValue() + _ = r.Append(model.NewIntValue(1)) + _ = r.Append(model.NewIntValue(2)) + _ = r.Append(model.NewIntValue(3)) + return r + }, + })) + }) +} diff --git a/execution/func.go b/execution/func.go new file mode 100644 index 00000000..3d4343be --- /dev/null +++ b/execution/func.go @@ -0,0 +1,55 @@ +package execution + +import ( + "fmt" + + "github.com/tomwright/dasel/v3/model" +) + +type singleResponseFunc func(data *model.Value, args model.Values) (*model.Value, error) + +type multiResponseFunc func(data *model.Value, args model.Values) (model.Values, error) + +var singleResponseFuncLookup = map[string]singleResponseFunc{} +var multiResponseFuncLookup = map[string]multiResponseFunc{} + +func registerFunc(name string, fn singleResponseFunc) { + singleResponseFuncLookup[name] = fn +} + +func registerMultiResponseFunc(name string, fn multiResponseFunc) { + multiResponseFuncLookup[name] = fn +} + +func init() { + registerFunc("add", func(_ *model.Value, args model.Values) (*model.Value, error) { + var foundInts, foundFloats int + var intRes int64 + var floatRes float64 + for _, arg := range args { + if arg.IsFloat() { + foundFloats++ + v, err := arg.FloatValue() + if err != nil { + return nil, fmt.Errorf("error getting float value: %w", err) + } + floatRes += v + continue + } + if arg.IsInt() { + foundInts++ + v, err := arg.IntValue() + if err != nil { + return nil, fmt.Errorf("error getting int value: %w", err) + } + intRes += v + continue + } + return nil, fmt.Errorf("expected int or float, got %s", arg.Type()) + } + if foundFloats > 0 { + return model.NewFloatValue(floatRes + float64(intRes)), nil + } + return model.NewIntValue(intRes), nil + }) +} diff --git a/func_all.go b/func_all.go index 7220a3cf..b8a263e7 100644 --- a/func_all.go +++ b/func_all.go @@ -2,8 +2,9 @@ package dasel import ( "fmt" - "github.com/tomwright/dasel/v2/dencoding" "reflect" + + "github.com/tomwright/dasel/v3/dencoding" ) var AllFunc = BasicFunction{ diff --git a/func_equal.go b/func_equal.go index beec504f..eac2340c 100644 --- a/func_equal.go +++ b/func_equal.go @@ -2,8 +2,9 @@ package dasel import ( "fmt" - "github.com/tomwright/dasel/v2/util" "reflect" + + "github.com/tomwright/dasel/v3/util" ) var EqualFunc = BasicFunction{ diff --git a/func_join.go b/func_join.go index cdc56e19..af9a8f52 100644 --- a/func_join.go +++ b/func_join.go @@ -1,8 +1,9 @@ package dasel import ( - "github.com/tomwright/dasel/v2/util" "strings" + + "github.com/tomwright/dasel/v3/util" ) var JoinFunc = BasicFunction{ diff --git a/func_join_test.go b/func_join_test.go index f2b3d3c9..f9e5e362 100644 --- a/func_join_test.go +++ b/func_join_test.go @@ -1,9 +1,10 @@ package dasel import ( - "github.com/tomwright/dasel/v2/dencoding" "strings" "testing" + + "github.com/tomwright/dasel/v3/dencoding" ) func TestJoinFunc(t *testing.T) { diff --git a/func_keys.go b/func_keys.go index 6579b77e..12ff8f6d 100644 --- a/func_keys.go +++ b/func_keys.go @@ -2,10 +2,11 @@ package dasel import ( "fmt" - "github.com/tomwright/dasel/v2/dencoding" "reflect" "sort" "strings" + + "github.com/tomwright/dasel/v3/dencoding" ) type ErrInvalidType struct { diff --git a/func_keys_test.go b/func_keys_test.go index 43cf2c1d..9c55b637 100644 --- a/func_keys_test.go +++ b/func_keys_test.go @@ -1,8 +1,9 @@ package dasel import ( - "github.com/tomwright/dasel/v2/dencoding" "testing" + + "github.com/tomwright/dasel/v3/dencoding" ) func TestKeysFunc(t *testing.T) { diff --git a/func_less_than.go b/func_less_than.go index 7000dcf5..e204dc1d 100644 --- a/func_less_than.go +++ b/func_less_than.go @@ -2,9 +2,10 @@ package dasel import ( "fmt" - "github.com/tomwright/dasel/v2/util" "reflect" "sort" + + "github.com/tomwright/dasel/v3/util" ) var LessThanFunc = BasicFunction{ diff --git a/func_more_than.go b/func_more_than.go index 73ff0172..9f2c6a89 100644 --- a/func_more_than.go +++ b/func_more_than.go @@ -2,9 +2,10 @@ package dasel import ( "fmt" - "github.com/tomwright/dasel/v2/util" "reflect" "sort" + + "github.com/tomwright/dasel/v3/util" ) var MoreThanFunc = BasicFunction{ diff --git a/func_string.go b/func_string.go index 295071bd..f64f78f7 100644 --- a/func_string.go +++ b/func_string.go @@ -1,6 +1,6 @@ package dasel -import "github.com/tomwright/dasel/v2/util" +import "github.com/tomwright/dasel/v3/util" var StringFunc = BasicFunction{ name: "string", diff --git a/go.mod b/go.mod index 9f12bf43..0566f3cc 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/tomwright/dasel/v2 +module github.com/tomwright/dasel/v3 go 1.21 diff --git a/internal/command/delete.go b/internal/command/delete.go index 06833fbe..e4443109 100644 --- a/internal/command/delete.go +++ b/internal/command/delete.go @@ -2,7 +2,7 @@ package command import ( "github.com/spf13/cobra" - "github.com/tomwright/dasel/v2" + "github.com/tomwright/dasel/v3" ) func deleteCommand() *cobra.Command { diff --git a/internal/command/options.go b/internal/command/options.go index 910b161a..93988236 100644 --- a/internal/command/options.go +++ b/internal/command/options.go @@ -7,9 +7,9 @@ import ( "strings" "github.com/spf13/cobra" - "github.com/tomwright/dasel/v2" - "github.com/tomwright/dasel/v2/dencoding" - "github.com/tomwright/dasel/v2/storage" + "github.com/tomwright/dasel/v3" + "github.com/tomwright/dasel/v3/dencoding" + "github.com/tomwright/dasel/v3/storage" ) type readOptions struct { diff --git a/internal/command/put.go b/internal/command/put.go index 6ec1b7a2..00e5bd7e 100644 --- a/internal/command/put.go +++ b/internal/command/put.go @@ -2,10 +2,11 @@ package command import ( "fmt" - "github.com/spf13/cobra" - "github.com/tomwright/dasel/v2" - "github.com/tomwright/dasel/v2/storage" "strconv" + + "github.com/spf13/cobra" + "github.com/tomwright/dasel/v3" + "github.com/tomwright/dasel/v3/storage" ) func putCommand() *cobra.Command { diff --git a/internal/command/root.go b/internal/command/root.go index 7e56f749..b08cd791 100644 --- a/internal/command/root.go +++ b/internal/command/root.go @@ -2,7 +2,7 @@ package command import ( "github.com/spf13/cobra" - "github.com/tomwright/dasel/v2/internal" + "github.com/tomwright/dasel/v3/internal" ) // NewRootCMD returns the root command for use with cobra. diff --git a/internal/command/select.go b/internal/command/select.go index 0e6816b9..4367e727 100644 --- a/internal/command/select.go +++ b/internal/command/select.go @@ -2,7 +2,7 @@ package command import ( "github.com/spf13/cobra" - "github.com/tomwright/dasel/v2" + "github.com/tomwright/dasel/v3" ) func selectCommand() *cobra.Command { diff --git a/model/error.go b/model/error.go new file mode 100644 index 00000000..3977c68c --- /dev/null +++ b/model/error.go @@ -0,0 +1,11 @@ +package model + +import "fmt" + +type MapKeyNotFound struct { + Key string +} + +func (e *MapKeyNotFound) Error() string { + return fmt.Sprintf("map key not found: %q", e.Key) +} diff --git a/model/generic.go b/model/generic.go new file mode 100644 index 00000000..8b537907 --- /dev/null +++ b/model/generic.go @@ -0,0 +1 @@ +package model diff --git a/model/value.go b/model/value.go new file mode 100644 index 00000000..5891dbba --- /dev/null +++ b/model/value.go @@ -0,0 +1,70 @@ +package model + +import ( + "reflect" + "slices" +) + +type Type string + +func (t Type) String() string { + return string(t) +} + +const ( + TypeString Type = "string" + TypeInt Type = "int" + TypeFloat Type = "float" + TypeBool Type = "bool" + TypeMap Type = "map" + TypeSlice Type = "array" + TypeUnknown Type = "unknown" +) + +type Value struct { + Value reflect.Value +} + +func NewValue(v any) *Value { + if rv, ok := v.(reflect.Value); ok { + return &Value{ + Value: rv, + } + } + return &Value{ + Value: reflect.ValueOf(v), + } +} + +func (v *Value) Interface() interface{} { + return v.Value.Interface() +} + +func (v *Value) UnpackKinds(kinds ...reflect.Kind) *Value { + res := v.Value + for { + if !slices.Contains(kinds, res.Kind()) { + return NewValue(res) + } + res = res.Elem() + } +} + +func (v *Value) Type() Type { + switch { + case v.IsString(): + return TypeString + case v.IsInt(): + return TypeInt + case v.IsFloat(): + return TypeFloat + case v.IsBool(): + return TypeBool + case v.IsMap(): + return TypeMap + case v.IsSlice(): + return TypeSlice + default: + return TypeUnknown + } +} diff --git a/model/value_literal.go b/model/value_literal.go new file mode 100644 index 00000000..1879a0e3 --- /dev/null +++ b/model/value_literal.go @@ -0,0 +1,94 @@ +package model + +import ( + "fmt" + "reflect" +) + +func NewStringValue(x string) *Value { + res := reflect.New(reflect.TypeFor[string]()) + res.Elem().Set(reflect.ValueOf(x)) + return NewValue(res) +} + +func (v *Value) IsString() bool { + return v.UnpackKinds(reflect.Ptr, reflect.Interface).isString() +} + +func (v *Value) isString() bool { + return v.Value.Kind() == reflect.String +} + +func (v *Value) StringValue() (string, error) { + unpacked := v.UnpackKinds(reflect.Ptr, reflect.Interface) + if !unpacked.isString() { + return "", fmt.Errorf("expected string, got %s", unpacked.Type()) + } + return unpacked.Value.String(), nil +} + +func NewIntValue(x int64) *Value { + res := reflect.New(reflect.TypeFor[int64]()) + res.Elem().Set(reflect.ValueOf(x)) + return NewValue(res) +} + +func (v *Value) IsInt() bool { + return v.UnpackKinds(reflect.Ptr, reflect.Interface).isInt() +} + +func (v *Value) isInt() bool { + return v.Value.Kind() == reflect.Int64 +} + +func (v *Value) IntValue() (int64, error) { + unpacked := v.UnpackKinds(reflect.Ptr, reflect.Interface) + if !unpacked.isInt() { + return 0, fmt.Errorf("expected int, got %s", unpacked.Type()) + } + return unpacked.Value.Int(), nil +} + +func NewFloatValue(x float64) *Value { + res := reflect.New(reflect.TypeFor[float64]()) + res.Elem().Set(reflect.ValueOf(x)) + return NewValue(res) +} + +func (v *Value) IsFloat() bool { + return v.UnpackKinds(reflect.Ptr, reflect.Interface).isFloat() +} + +func (v *Value) isFloat() bool { + return v.Value.Kind() == reflect.Float64 +} + +func (v *Value) FloatValue() (float64, error) { + unpacked := v.UnpackKinds(reflect.Ptr, reflect.Interface) + if !unpacked.IsFloat() { + return 0, fmt.Errorf("expected float, got %s", unpacked.Type()) + } + return unpacked.Value.Float(), nil +} + +func NewBoolValue(x bool) *Value { + res := reflect.New(reflect.TypeFor[bool]()) + res.Elem().Set(reflect.ValueOf(x)) + return NewValue(res) +} + +func (v *Value) IsBool() bool { + return v.UnpackKinds(reflect.Ptr, reflect.Interface).isBool() +} + +func (v *Value) isBool() bool { + return v.Value.Kind() == reflect.Bool +} + +func (v *Value) BoolValue() (bool, error) { + unpacked := v.UnpackKinds(reflect.Ptr, reflect.Interface) + if !unpacked.IsBool() { + return false, fmt.Errorf("expected bool, got %s", unpacked.Type()) + } + return unpacked.Value.Bool(), nil +} diff --git a/model/value_map.go b/model/value_map.go new file mode 100644 index 00000000..9ef38695 --- /dev/null +++ b/model/value_map.go @@ -0,0 +1,50 @@ +package model + +import ( + "fmt" + "reflect" + + "github.com/tomwright/dasel/v3/dencoding" +) + +func NewMapValue() *Value { + return NewValue(dencoding.NewMap()) +} + +func (v *Value) MapValue() (*dencoding.Map, error) { + if !v.IsMap() { + return nil, fmt.Errorf("value is not a map") + } + return v.Value.Interface().(*dencoding.Map), nil +} + +func (v *Value) IsMap() bool { + return v.UnpackKinds(reflect.Interface).isMap() +} + +func (v *Value) isMap() bool { + return v.Value.Type() == reflect.TypeFor[*dencoding.Map]() +} + +func (v *Value) SetMapKey(key string, value *Value) error { + m, err := v.MapValue() + if err != nil { + return fmt.Errorf("error getting map: %w", err) + } + m.Set(key, value.Value.Interface()) + return nil +} + +func (v *Value) GetMapKey(key string) (*Value, error) { + m, err := v.MapValue() + if err != nil { + return nil, fmt.Errorf("error getting map: %w", err) + } + val, ok := m.Get(key) + if !ok { + return nil, &MapKeyNotFound{Key: key} + } + return &Value{ + Value: reflect.ValueOf(val), + }, nil +} diff --git a/model/value_slice.go b/model/value_slice.go new file mode 100644 index 00000000..74268cb9 --- /dev/null +++ b/model/value_slice.go @@ -0,0 +1,62 @@ +package model + +import ( + "fmt" + "reflect" +) + +func NewSliceValue() *Value { + s := reflect.MakeSlice(reflect.SliceOf(reflect.TypeFor[any]()), 0, 0) + ptr := reflect.New(reflect.SliceOf(reflect.TypeFor[any]())) + ptr.Elem().Set(s) + return NewValue(ptr) +} + +func (v *Value) SliceValue() ([]any, error) { + unpacked := v.UnpackKinds(reflect.Interface, reflect.Ptr) + if !unpacked.IsSlice() { + return nil, fmt.Errorf("expected slice, got %s", v.Type()) + } + res, ok := unpacked.Interface().([]any) + if !ok { + return nil, fmt.Errorf("could not convert slice to []interface{}") + } + return res, nil +} + +func (v *Value) IsSlice() bool { + return v.UnpackKinds(reflect.Interface, reflect.Ptr).isSlice() +} + +func (v *Value) isSlice() bool { + return v.Value.Kind() == reflect.Slice +} + +func (v *Value) Append(val *Value) error { + unpacked := v.UnpackKinds(reflect.Interface, reflect.Ptr) + if !unpacked.isSlice() { + return fmt.Errorf("expected slice, got %s", v.Type()) + } + newVal := reflect.Append(unpacked.Value, val.Value) + unpacked.Value.Set(newVal) + return nil +} + +func (v *Value) SliceLen() (int, error) { + unpacked := v.UnpackKinds(reflect.Interface, reflect.Ptr) + if !unpacked.isSlice() { + return 0, fmt.Errorf("expected slice, got %s", v.Type()) + } + return unpacked.Value.Len(), nil +} + +func (v *Value) GetSliceIndex(i int) (*Value, error) { + unpacked := v.UnpackKinds(reflect.Interface, reflect.Ptr) + if !unpacked.isSlice() { + return nil, fmt.Errorf("expected slice, got %s", v.Type()) + } + if i < 0 || i >= unpacked.Value.Len() { + return nil, fmt.Errorf("index out of range: %d", i) + } + return NewValue(unpacked.Value.Index(i)), nil +} diff --git a/model/value_slice_test.go b/model/value_slice_test.go new file mode 100644 index 00000000..b6f00bb6 --- /dev/null +++ b/model/value_slice_test.go @@ -0,0 +1,28 @@ +package model_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/ptr" +) + +func TestNewSliceValue(t *testing.T) { + x := model.NewSliceValue() + if err := x.Append(model.NewStringValue("hello")); err != nil { + t.Errorf("unexpected error: %s", err) + } + if err := x.Append(model.NewStringValue("world")); err != nil { + t.Errorf("unexpected error: %s", err) + } + + got, err := x.SliceValue() + if err != nil { + t.Errorf("unexpected error: %s", err) + } + exp := []any{ptr.To("hello"), ptr.To("world")} + if !cmp.Equal(exp, got) { + t.Errorf("unexpected result: %s", cmp.Diff(exp, got)) + } +} diff --git a/model/values.go b/model/values.go new file mode 100644 index 00000000..2c485577 --- /dev/null +++ b/model/values.go @@ -0,0 +1,3 @@ +package model + +type Values []*Value diff --git a/ptr/to.go b/ptr/to.go new file mode 100644 index 00000000..6c3ee9bd --- /dev/null +++ b/ptr/to.go @@ -0,0 +1,5 @@ +package ptr + +func To[T any](v T) *T { + return &v +} diff --git a/selector/ast/expression_complex.go b/selector/ast/expression_complex.go index be144761..c616253a 100644 --- a/selector/ast/expression_complex.go +++ b/selector/ast/expression_complex.go @@ -1,6 +1,6 @@ package ast -import "github.com/tomwright/dasel/v2/selector/lexer" +import "github.com/tomwright/dasel/v3/selector/lexer" type BinaryExpr struct { Left Expr @@ -21,4 +21,58 @@ type ChainedExpr struct { Exprs Expressions } +func ChainExprs(exprs ...Expr) Expr { + if len(exprs) == 1 { + return exprs[0] + } + return ChainedExpr{ + Exprs: exprs, + } +} + func (ChainedExpr) expr() {} + +type SpreadExpr struct{} + +func (SpreadExpr) expr() {} + +func IsSpreadExpr(e Expr) bool { + _, ok := e.(SpreadExpr) + return ok +} + +type RangeExpr struct { + Start Expr + End Expr +} + +func (RangeExpr) expr() {} + +type IndexExpr struct { + Index Expr +} + +func (IndexExpr) expr() {} + +type PropertyExpr struct { + Property Expr +} + +func (PropertyExpr) expr() {} + +type KeyValue struct { + Key Expr + Value Expr +} + +type ObjectExpr struct { + Pairs []KeyValue +} + +func (ObjectExpr) expr() {} + +type MapExpr struct { + Exprs Expressions +} + +func (MapExpr) expr() {} diff --git a/selector/ast/expression_literal.go b/selector/ast/expression_literal.go index 10050829..b369b9fe 100644 --- a/selector/ast/expression_literal.go +++ b/selector/ast/expression_literal.go @@ -23,9 +23,3 @@ type BoolExpr struct { } func (BoolExpr) expr() {} - -type SymbolExpr struct { - Value string -} - -func (SymbolExpr) expr() {} diff --git a/selector/lexer/token.go b/selector/lexer/token.go index 7f8bd091..4d832984 100644 --- a/selector/lexer/token.go +++ b/selector/lexer/token.go @@ -19,6 +19,7 @@ const ( OpenParen CloseParen Equal + Equals NotEqual Not And @@ -31,13 +32,14 @@ const ( Add Increment IncrementBy - Subtract + Dash Decrement DecrementBy - Multiply - Divide - Modulus + Star + Slash + Percent Dot + Spread ) type Tokens []Token diff --git a/selector/lexer/tokenize.go b/selector/lexer/tokenize.go index 46eae87a..bd2e73cc 100644 --- a/selector/lexer/tokenize.go +++ b/selector/lexer/tokenize.go @@ -44,6 +44,9 @@ func (p *Tokenizer) peekRuneEqual(i int, to rune) bool { func (p *Tokenizer) parseCurRune() (Token, error) { switch p.src[p.i] { case '.': + if p.peekRuneEqual(p.i+1, '.') && p.peekRuneEqual(p.i+2, '.') { + return NewToken(Spread, "...", p.i, 3), nil + } return NewToken(Dot, ".", p.i, 1), nil case ',': return NewToken(Comma, ",", p.i, 1), nil @@ -61,6 +64,12 @@ func (p *Tokenizer) parseCurRune() (Token, error) { return NewToken(OpenCurly, "{", p.i, 1), nil case '}': return NewToken(CloseCurly, "}", p.i, 1), nil + case '*': + return NewToken(Star, "*", p.i, 1), nil + case '/': + return NewToken(Slash, "/", p.i, 1), nil + case '%': + return NewToken(Percent, "%", p.i, 1), nil case '=': if p.peekRuneEqual(p.i+1, '=') { return NewToken(Equal, "==", p.i, 2), nil @@ -68,7 +77,7 @@ func (p *Tokenizer) parseCurRune() (Token, error) { if p.peekRuneEqual(p.i+1, '~') { return NewToken(Like, "=~", p.i, 2), nil } - return NewToken(Equal, "=", p.i, 1), nil + return NewToken(Equals, "=", p.i, 1), nil case '+': if p.peekRuneEqual(p.i+1, '=') { return NewToken(IncrementBy, "+=", p.i, 2), nil @@ -84,7 +93,7 @@ func (p *Tokenizer) parseCurRune() (Token, error) { if p.peekRuneEqual(p.i+1, '-') { return NewToken(Decrement, "--", p.i, 2), nil } - return NewToken(Subtract, "-", p.i, 1), nil + return NewToken(Dash, "-", p.i, 1), nil case '!': if p.peekRuneEqual(p.i+1, '=') { return NewToken(NotEqual, "!=", p.i, 2), nil diff --git a/selector/lexer/tokenize_test.go b/selector/lexer/tokenize_test.go index 330ce2cd..ab9685ff 100644 --- a/selector/lexer/tokenize_test.go +++ b/selector/lexer/tokenize_test.go @@ -3,7 +3,7 @@ package lexer import "testing" func TestTokenizer_Parse(t *testing.T) { - tok := NewTokenizer("foo.bar.baz[1] != 42.123 || foo.bar.baz['hello'] == 42 && x == 'a\\'b' + false") + tok := NewTokenizer("foo.bar.baz[1] != 42.123 || foo.bar.baz['hello'] == 42 && x == 'a\\'b' + false . .... asd...") tokens, err := tok.Tokenize() if err != nil { t.Fatalf("unexpected error: %v", err) @@ -15,6 +15,8 @@ func TestTokenizer_Parse(t *testing.T) { And, Symbol, Equal, String, Add, Bool, + Dot, Spread, Dot, + Symbol, Spread, } if len(tokens) != len(exp) { t.Fatalf("unexpected number of tokens: %d", len(tokens)) diff --git a/selector/parser.go b/selector/parser.go index cc063eea..b54fa1fe 100644 --- a/selector/parser.go +++ b/selector/parser.go @@ -1,9 +1,9 @@ package selector import ( - "github.com/tomwright/dasel/v2/selector/ast" - "github.com/tomwright/dasel/v2/selector/lexer" - "github.com/tomwright/dasel/v2/selector/parser" + "github.com/tomwright/dasel/v3/selector/ast" + "github.com/tomwright/dasel/v3/selector/lexer" + "github.com/tomwright/dasel/v3/selector/parser" ) func Parse(selector string) (ast.Expr, error) { diff --git a/selector/parser/error.go b/selector/parser/error.go index 0f03e3a4..89903945 100644 --- a/selector/parser/error.go +++ b/selector/parser/error.go @@ -3,7 +3,7 @@ package parser import ( "fmt" - "github.com/tomwright/dasel/v2/selector/lexer" + "github.com/tomwright/dasel/v3/selector/lexer" ) type PositionalError struct { diff --git a/selector/parser/parse_array.go b/selector/parser/parse_array.go new file mode 100644 index 00000000..c376861c --- /dev/null +++ b/selector/parser/parse_array.go @@ -0,0 +1,111 @@ +package parser + +import ( + "github.com/tomwright/dasel/v3/selector/ast" + "github.com/tomwright/dasel/v3/selector/lexer" +) + +func parseArray(p *Parser) (ast.Expr, error) { + if err := p.expect(lexer.Symbol); err != nil { + return nil, err + } + if err := p.expectN(1, lexer.OpenBracket); err != nil { + return nil, err + } + token := p.current() + p.advance() + + idx, err := parseSquareBrackets(p) + if err != nil { + return nil, err + } + return ast.ChainExprs( + ast.PropertyExpr{ + Property: ast.StringExpr{Value: token.Value}, + }, + idx, + ), nil +} + +// 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 + } + + p.advance() + + // Spread [...] + if p.current().IsKind(lexer.Spread) { + p.advance() + if err := p.expect(lexer.CloseBracket); err != nil { + return nil, err + } + p.advance() + return ast.SpreadExpr{}, nil + } + + p.pushScope(scopeArray) + defer p.popScope() + + var ( + start ast.Expr + end ast.Expr + err error + ) + + if p.current().IsKind(lexer.Colon) { + p.advance() + // We have no start index + end, err = p.parseExpression() + if err != nil { + return nil, err + } + p.advance() + return ast.RangeExpr{ + End: end, + }, nil + } + + start, err = p.parseExpression() + if err != nil { + return nil, err + } + + if p.current().IsKind(lexer.CloseBracket) { + // This is an index + p.advance() + return ast.IndexExpr{ + Index: start, + }, nil + } + + if err := p.expect(lexer.Colon); err != nil { + return nil, err + } + + p.advance() + + if p.current().IsKind(lexer.CloseBracket) { + // There is no end + p.advance() + return ast.RangeExpr{ + Start: start, + }, nil + } + + end, err = p.parseExpression() + if err != nil { + return nil, err + } + + p.advance() + return ast.RangeExpr{ + Start: start, + End: end, + }, nil +} diff --git a/selector/parser/parse_func.go b/selector/parser/parse_func.go new file mode 100644 index 00000000..8a8475ba --- /dev/null +++ b/selector/parser/parse_func.go @@ -0,0 +1,51 @@ +package parser + +import ( + "github.com/tomwright/dasel/v3/selector/ast" + "github.com/tomwright/dasel/v3/selector/lexer" +) + +func parseFunc(p *Parser) (ast.Expr, error) { + p.pushScope(scopeFuncArgs) + defer p.popScope() + + if err := p.expect(lexer.Symbol); err != nil { + return nil, err + } + if err := p.expectN(1, lexer.OpenParen); err != nil { + return nil, err + } + + token := p.current() + + p.advanceN(2) + args, err := parseArgs(p) + if err != nil { + return nil, err + } + return ast.CallExpr{ + Function: token.Value, + Args: args, + }, nil +} + +func parseArgs(p *Parser) ([]ast.Expr, error) { + args := make([]ast.Expr, 0) + for p.hasToken() { + if p.current().IsKind(lexer.CloseParen) { + p.advance() + break + } + + arg, err := p.parseExpression() + if err != nil { + return nil, err + } + args = append(args, arg) + + if p.current().IsKind(lexer.Comma) { + p.advance() + } + } + return args, nil +} diff --git a/selector/parser/parse_literal.go b/selector/parser/parse_literal.go index 152fc969..870390e1 100644 --- a/selector/parser/parse_literal.go +++ b/selector/parser/parse_literal.go @@ -4,13 +4,14 @@ import ( "strconv" "strings" - "github.com/tomwright/dasel/v2/selector/ast" + "github.com/tomwright/dasel/v3/selector/ast" + "github.com/tomwright/dasel/v3/selector/lexer" ) func parseStringLiteral(p *Parser) (ast.Expr, error) { token := p.current() p.advance() - return &ast.StringExpr{ + return ast.StringExpr{ Value: token.Value, }, nil } @@ -18,28 +19,46 @@ func parseStringLiteral(p *Parser) (ast.Expr, error) { func parseBoolLiteral(p *Parser) (ast.Expr, error) { token := p.current() p.advance() - return &ast.BoolExpr{ + return ast.BoolExpr{ Value: strings.EqualFold(token.Value, "true"), }, nil } +func parseSpread(p *Parser) (ast.Expr, error) { + p.advance() + return ast.SpreadExpr{}, nil +} + func parseNumberLiteral(p *Parser) (ast.Expr, error) { token := p.current() - p.advance() - if strings.Contains(token.Value, ".") { + next := p.advance() + switch { + case next.IsKind(lexer.Symbol) && strings.EqualFold(next.Value, "f"): value, err := strconv.ParseFloat(token.Value, 64) if err != nil { return nil, err } - return &ast.NumberFloatExpr{ + p.advance() + return ast.NumberFloatExpr{ + Value: value, + }, nil + + case strings.Contains(token.Value, "."): + value, err := strconv.ParseFloat(token.Value, 64) + if err != nil { + return nil, err + } + return ast.NumberFloatExpr{ + Value: value, + }, nil + + default: + value, err := strconv.ParseInt(token.Value, 10, 64) + if err != nil { + return nil, err + } + return ast.NumberIntExpr{ Value: value, }, nil } - value, err := strconv.ParseInt(token.Value, 10, 64) - if err != nil { - return nil, err - } - return &ast.NumberIntExpr{ - Value: value, - }, nil } diff --git a/selector/parser/parse_map.go b/selector/parser/parse_map.go new file mode 100644 index 00000000..7796a350 --- /dev/null +++ b/selector/parser/parse_map.go @@ -0,0 +1,53 @@ +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 { + 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 { + return nil, err + } + p.advance() + + expressions := make([]ast.Expr, 0) + + for { + if p.current().IsKind(lexer.CloseParen) { + if len(expressions) == 0 { + return nil, fmt.Errorf("expected at least one expression in map") + } + p.advance() + break + } + + if p.current().IsKind(lexer.Dot) { + p.advance() + continue + } + + expr, err := p.parseExpression() + if err != nil { + return nil, err + } + expressions = append(expressions, expr) + } + + return ast.MapExpr{ + Exprs: expressions, + }, nil +} diff --git a/selector/parser/parse_object.go b/selector/parser/parse_object.go new file mode 100644 index 00000000..b8ecb204 --- /dev/null +++ b/selector/parser/parse_object.go @@ -0,0 +1,91 @@ +package parser + +import ( + "github.com/tomwright/dasel/v3/selector/ast" + "github.com/tomwright/dasel/v3/selector/lexer" +) + +func parseObject(p *Parser) (ast.Expr, error) { + p.pushScope(scopeObject) + defer p.popScope() + + if err := p.expect(lexer.OpenCurly); err != nil { + return nil, err + } + p.advance() + + pairs := make([]ast.KeyValue, 0) + + for { + if p.current().IsKind(lexer.CloseCurly) { + break + } + + if p.current().IsKind(lexer.Comma) { + p.advance() + continue + } + + if p.current().IsKind(lexer.Spread) { + p.advance() + pairs = append(pairs, ast.KeyValue{ + Key: ast.SpreadExpr{}, + Value: ast.SpreadExpr{}, + }) + if err := p.expect(lexer.Comma, lexer.CloseCurly); err != nil { + return nil, err + } + continue + } + + if p.current().IsKind(lexer.Symbol) && p.peek().IsKind(lexer.Comma, lexer.CloseCurly) { + // if the next token is a comma or close curly, then it is a shorthand property + pairs = append(pairs, ast.KeyValue{ + Key: ast.StringExpr{Value: p.current().Value}, + Value: ast.PropertyExpr{Property: ast.StringExpr{Value: p.current().Value}}, + }) + p.advance() + if err := p.expect(lexer.Comma, lexer.CloseCurly); err != nil { + return nil, err + } + continue + } + + key, err := p.parseExpression() + if err != nil { + return nil, err + } + + // Attempt to simplify the key to a string expression. + if prop, ok := key.(ast.PropertyExpr); ok { + key = prop.Property + } + + if err := p.expect(lexer.Equals); err != nil { + return nil, err + } + p.advance() + + val, err := p.parseExpression() + if err != nil { + return nil, err + } + + pairs = append(pairs, ast.KeyValue{ + Key: key, + Value: val, + }) + if err := p.expect(lexer.Comma, lexer.CloseCurly); err != nil { + return nil, err + } + } + + if err := p.expect(lexer.CloseCurly); err != nil { + return nil, err + } + p.advance() + + return ast.ObjectExpr{ + Pairs: pairs, + }, nil +} diff --git a/selector/parser/parse_symbol.go b/selector/parser/parse_symbol.go index db75d7d6..74ad4cbb 100644 --- a/selector/parser/parse_symbol.go +++ b/selector/parser/parse_symbol.go @@ -1,168 +1,40 @@ package parser import ( - "github.com/tomwright/dasel/v2/selector/ast" - "github.com/tomwright/dasel/v2/selector/lexer" + "github.com/tomwright/dasel/v3/selector/ast" + "github.com/tomwright/dasel/v3/selector/lexer" ) -// parseSquareBrackets parses square bracket array access. -// E.g. [0], [0:1], [0:], [:2]. [...] -func parseSquareBrackets(p *Parser) (ast.Expr, error) { - // Handle index (from bracket) - p.advance() - - // Spread [...] - if p.current().IsKind(lexer.Dot) && p.peekN(1).IsKind(lexer.Dot) && p.peekN(2).IsKind(lexer.Dot) { - p.advanceN(3) - if err := p.expect(p.current(), lexer.CloseBracket); err != nil { - return nil, err - } - p.advance() - return &ast.CallExpr{ - Function: "all", - Args: ast.Expressions{}, - }, nil - } - - // Range [1:2] - if p.current().IsKind(lexer.Number) && p.peekN(1).IsKind(lexer.Colon) && p.peekN(2).IsKind(lexer.Number) { - from, err := p.parseExpression() - if err != nil { - return nil, err - } - p.advance() - to, err := p.parseExpression() - if err != nil { - return nil, err - } - if err := p.expect(p.current(), lexer.CloseBracket); err != nil { - return nil, err - } - p.advance() - return &ast.CallExpr{ - Function: "range", - Args: ast.Expressions{ - from, to, - }, - }, nil - } - - // Range [:2] - if p.current().IsKind(lexer.Colon) && p.peekN(1).IsKind(lexer.Number) { - from := &ast.NumberIntExpr{Value: -1} - p.advanceN(1) - to, err := p.parseExpression() - if err != nil { - return nil, err - } - if err := p.expect(p.current(), lexer.CloseBracket); err != nil { - return nil, err - } - p.advance() - return &ast.CallExpr{ - Function: "range", - Args: ast.Expressions{ - from, to, - }, - }, nil - } - - // Range [1:] - if p.current().IsKind(lexer.Number) && p.peekN(1).IsKind(lexer.Colon) { - from, err := p.parseExpression() - if err != nil { - return nil, err - } - p.advanceN(1) - to := &ast.NumberIntExpr{Value: -1} - if err := p.expect(p.current(), lexer.CloseBracket); err != nil { - return nil, err - } - p.advance() - return &ast.CallExpr{ - Function: "range", - Args: ast.Expressions{ - from, to, - }, - }, nil - } - - // Array index [1] - if err := p.expect(p.current(), lexer.Number); err != nil { - return nil, err - } - index, err := p.parseExpression() - if err != nil { - return nil, err - } - if err := p.expect(p.current(), lexer.CloseBracket); err != nil { - return nil, err - } - p.advance() - - return &ast.CallExpr{ - Function: "index", - Args: ast.Expressions{index}, - }, nil -} - func parseSymbol(p *Parser) (ast.Expr, error) { token := p.current() next := p.peek() + if token.Value == "map" && next.IsKind(lexer.OpenParen) { + return parseMap(p) + } + // Handle functions if next.IsKind(lexer.OpenParen) { - p.advanceN(2) - args, err := parseArgs(p) - if err != nil { - return nil, err - } - return &ast.CallExpr{ - Function: token.Value, - Args: args, - }, nil + return parseFunc(p) } - // Handle index (before bracket) if next.IsKind(lexer.OpenBracket) { - p.advance() - return &ast.CallExpr{ - Function: "property", - Args: ast.Expressions{&ast.StringExpr{Value: token.Value}}, - }, nil + return parseArray(p) } - if next.IsKind(lexer.Dot, lexer.EOF, lexer.Comma) { - p.advance() - return &ast.CallExpr{ - Function: "property", - Args: []ast.Expr{&ast.StringExpr{Value: token.Value}}, - }, nil + prop := ast.PropertyExpr{ + Property: ast.StringExpr{Value: token.Value}, } - return nil, &UnexpectedTokenError{ - Token: next, + if next.IsKind(lexer.Spread) { + p.advanceN(2) + return ast.ChainExprs( + prop, + ast.SpreadExpr{}, + ), nil } -} - -func parseArgs(p *Parser) ([]ast.Expr, error) { - args := make([]ast.Expr, 0) - for p.hasToken() { - if p.current().IsKind(lexer.CloseParen) { - p.advance() - break - } - arg, err := p.parseExpression() - if err != nil { - return nil, err - } - args = append(args, arg) - - if p.current().IsKind(lexer.Comma) { - p.advance() - } - } - return args, nil + p.advance() + return prop, nil } diff --git a/selector/parser/parser.go b/selector/parser/parser.go index f25272d4..9025c6cf 100644 --- a/selector/parser/parser.go +++ b/selector/parser/parser.go @@ -3,13 +3,64 @@ package parser import ( "fmt" - "github.com/tomwright/dasel/v2/selector/ast" - "github.com/tomwright/dasel/v2/selector/lexer" + "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" ) 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 { + switch p.currentScope() { + case scopeRoot: + return []lexer.TokenKind{lexer.EOF, lexer.Dot} + case scopeFuncArgs: + return []lexer.TokenKind{lexer.Comma, lexer.CloseParen} + case scopeMap: + return []lexer.TokenKind{lexer.Comma, lexer.CloseParen, lexer.Dot} + case scopeArray: + return []lexer.TokenKind{lexer.CloseBracket, lexer.Colon, lexer.Number, lexer.Symbol} + case scopeObject: + return []lexer.TokenKind{lexer.CloseCurly, lexer.Equals, lexer.Number, lexer.Symbol, lexer.Comma} + default: + return nil + } +} + +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 { @@ -34,13 +85,22 @@ func (p *Parser) Parse() (ast.Expr, error) { } expressions = append(expressions, expr) } - if len(expressions) == 1 { + switch len(expressions) { + case 0: + return nil, nil + case 1: return expressions[0], nil + default: + return ast.ChainExprs(expressions...), nil } - return &ast.ChainedExpr{Exprs: expressions}, nil } -func (p *Parser) parseExpression() (ast.Expr, error) { +func (p *Parser) parseExpression() (res ast.Expr, err error) { + defer func() { + if err == nil { + err = p.expectEndOfExpression() + } + }() switch p.current().Kind { case lexer.String: return parseStringLiteral(p) @@ -50,8 +110,12 @@ func (p *Parser) parseExpression() (ast.Expr, error) { return parseSymbol(p) case lexer.OpenBracket: return parseSquareBrackets(p) + case lexer.OpenCurly: + return parseObject(p) case lexer.Bool: return parseBoolLiteral(p) + case lexer.Spread: + return parseSpread(p) default: return nil, &UnexpectedTokenError{ Token: p.current(), @@ -95,12 +159,22 @@ func (p *Parser) peekN(n int) lexer.Token { return p.tokens[p.i+n] } -func (p *Parser) expect(t lexer.Token, kind ...lexer.TokenKind) error { +func (p *Parser) expect(kind ...lexer.TokenKind) error { + t := p.current() + if p.current().IsKind(kind...) { + return nil + } + return &UnexpectedTokenError{ + Token: t, + } +} + +func (p *Parser) expectN(n int, kind ...lexer.TokenKind) error { + t := p.peekN(n) if t.IsKind(kind...) { return nil } - return &PositionalError{ - Position: t.Pos, - Err: fmt.Errorf("unexpected token: %v", t.Value), + return &UnexpectedTokenError{ + Token: t, } } diff --git a/selector/parser/parser_test.go b/selector/parser/parser_test.go index 4e3c6781..494130e1 100644 --- a/selector/parser/parser_test.go +++ b/selector/parser/parser_test.go @@ -4,136 +4,19 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/tomwright/dasel/v2/selector/ast" - "github.com/tomwright/dasel/v2/selector/lexer" - "github.com/tomwright/dasel/v2/selector/parser" + "github.com/tomwright/dasel/v3/selector/ast" + "github.com/tomwright/dasel/v3/selector/lexer" + "github.com/tomwright/dasel/v3/selector/parser" ) -func TestParser_Parse(t *testing.T) { - testCases := []struct { - name string +func TestParser_Parse_HappyPath(t *testing.T) { + type testCase struct { input string expected ast.Expr - }{ - { - name: "single property", - input: "foo", - expected: &ast.CallExpr{ - Function: "property", - Args: ast.Expressions{&ast.StringExpr{Value: "foo"}}, - }, - }, - { - name: "chained properties", - input: "foo.bar", - expected: ast.ChainedExpr{ - Exprs: ast.Expressions{ - &ast.CallExpr{ - Function: "property", - Args: ast.Expressions{&ast.StringExpr{Value: "foo"}}, - }, - &ast.CallExpr{ - Function: "property", - Args: ast.Expressions{&ast.StringExpr{Value: "bar"}}, - }, - }, - }, - }, - { - name: "single function with no args", - input: "all()", - expected: &ast.CallExpr{ - Function: "all", - Args: ast.Expressions{}, - }, - }, - { - name: "single function with various args", - input: "all(\"foo\", 'bar', false, TRUE, 123, 12.3, hello, funcOne(), funcTwo(1, 2, 3), asd[5], asd[...], asd[0:1], asd[2:], asd[:2])", - expected: &ast.CallExpr{ - Function: "all", - Args: ast.Expressions{ - &ast.StringExpr{Value: "foo"}, - &ast.StringExpr{Value: "bar"}, - &ast.BoolExpr{Value: false}, - &ast.BoolExpr{Value: true}, - &ast.NumberIntExpr{Value: 123}, - &ast.NumberFloatExpr{Value: 12.3}, - &ast.CallExpr{ - Function: "property", - Args: ast.Expressions{&ast.StringExpr{Value: "hello"}}, - }, - &ast.CallExpr{ - Function: "funcOne", - Args: ast.Expressions{}, - }, - &ast.CallExpr{ - Function: "funcTwo", - Args: ast.Expressions{ - &ast.NumberIntExpr{Value: 1}, - &ast.NumberIntExpr{Value: 2}, - &ast.NumberIntExpr{Value: 3}, - }, - }, - &ast.CallExpr{ - Function: "property", - Args: ast.Expressions{&ast.StringExpr{Value: "asd"}}, - }, - &ast.CallExpr{ - Function: "index", - Args: ast.Expressions{ - &ast.NumberIntExpr{Value: 5}, - }, - }, - &ast.CallExpr{ - Function: "property", - Args: ast.Expressions{&ast.StringExpr{Value: "asd"}}, - }, - &ast.CallExpr{ - Function: "all", - Args: ast.Expressions{}, - }, - &ast.CallExpr{ - Function: "property", - Args: ast.Expressions{&ast.StringExpr{Value: "asd"}}, - }, - &ast.CallExpr{ - Function: "range", - Args: ast.Expressions{ - &ast.NumberIntExpr{Value: 0}, - &ast.NumberIntExpr{Value: 1}, - }, - }, - &ast.CallExpr{ - Function: "property", - Args: ast.Expressions{&ast.StringExpr{Value: "asd"}}, - }, - &ast.CallExpr{ - Function: "range", - Args: ast.Expressions{ - &ast.NumberIntExpr{Value: 2}, - &ast.NumberIntExpr{Value: -1}, - }, - }, - &ast.CallExpr{ - Function: "property", - Args: ast.Expressions{&ast.StringExpr{Value: "asd"}}, - }, - &ast.CallExpr{ - Function: "range", - Args: ast.Expressions{ - &ast.NumberIntExpr{Value: -1}, - &ast.NumberIntExpr{Value: 2}, - }, - }, - }, - }, - }, } - for _, testCase := range testCases { - tc := testCase - t.Run(tc.name, func(t *testing.T) { + run := func(t *testing.T, tc testCase) func(*testing.T) { + return func(t *testing.T) { tokens, err := lexer.NewTokenizer(tc.input).Tokenize() if err != nil { t.Fatal(err) @@ -142,10 +25,270 @@ func TestParser_Parse(t *testing.T) { if err != nil { t.Fatal(err) } - if !cmp.Equal(tc.expected, got) { - t.Fatalf("unexpected result: %s", cmp.Diff(tc.expected, got)) + t.Errorf("unexpected result: %s", cmp.Diff(tc.expected, got)) } - }) + } } + + t.Run("literal access", func(t *testing.T) { + t.Run("string", run(t, testCase{ + input: `"hello world"`, + expected: ast.StringExpr{Value: "hello world"}, + })) + t.Run("int", run(t, testCase{ + input: "42", + expected: ast.NumberIntExpr{Value: 42}, + })) + t.Run("float", run(t, testCase{ + input: "42.1", + expected: ast.NumberFloatExpr{Value: 42.1}, + })) + t.Run("whole number float", run(t, testCase{ + input: "42f", + expected: ast.NumberFloatExpr{Value: 42}, + })) + t.Run("bool true lowercase", run(t, testCase{ + input: "true", + expected: ast.BoolExpr{Value: true}, + })) + t.Run("bool true uppercase", run(t, testCase{ + input: "TRUE", + expected: ast.BoolExpr{Value: true}, + })) + t.Run("bool true mixed case", run(t, testCase{ + input: "TrUe", + expected: ast.BoolExpr{Value: true}, + })) + t.Run("bool false lowercase", run(t, testCase{ + input: "false", + expected: ast.BoolExpr{Value: false}, + })) + t.Run("bool false uppercase", run(t, testCase{ + input: "FALSE", + expected: ast.BoolExpr{Value: false}, + })) + t.Run("bool false mixed case", run(t, testCase{ + input: "FaLsE", + expected: ast.BoolExpr{Value: false}, + })) + }) + + t.Run("property access", func(t *testing.T) { + t.Run("single property access", run(t, testCase{ + input: "foo", + expected: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, + })) + t.Run("chained property access", run(t, testCase{ + input: "foo.bar", + expected: ast.ChainExprs( + ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, + ast.PropertyExpr{Property: ast.StringExpr{Value: "bar"}}, + ), + })) + t.Run("property access spread", run(t, testCase{ + input: "foo...", + expected: ast.ChainExprs( + ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, + ast.SpreadExpr{}, + ), + })) + t.Run("property access spread into property access", run(t, testCase{ + input: "foo....bar", + expected: ast.ChainExprs( + ast.ChainExprs( + ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, + ast.SpreadExpr{}, + ), + ast.PropertyExpr{Property: ast.StringExpr{Value: "bar"}}, + ), + })) + }) + + t.Run("array access", func(t *testing.T) { + t.Run("root array", func(t *testing.T) { + t.Run("index", run(t, testCase{ + input: "[1]", + expected: ast.IndexExpr{Index: ast.NumberIntExpr{Value: 1}}, + })) + t.Run("range", func(t *testing.T) { + t.Run("start and end funcs", run(t, testCase{ + input: "[calcStart(1):calcEnd()]", + expected: ast.RangeExpr{ + Start: ast.CallExpr{ + Function: "calcStart", + Args: ast.Expressions{ + ast.NumberIntExpr{Value: 1}, + }, + }, + End: ast.CallExpr{ + Function: "calcEnd", + Args: ast.Expressions{}, + }, + }, + })) + t.Run("start and end", run(t, testCase{ + input: "[5:10]", + expected: ast.RangeExpr{Start: ast.NumberIntExpr{Value: 5}, End: ast.NumberIntExpr{Value: 10}}, + })) + t.Run("start", run(t, testCase{ + input: "[5:]", + expected: ast.RangeExpr{Start: ast.NumberIntExpr{Value: 5}}, + })) + t.Run("end", run(t, testCase{ + input: "[:10]", + expected: ast.RangeExpr{End: ast.NumberIntExpr{Value: 10}}, + })) + }) + t.Run("spread", func(t *testing.T) { + t.Run("standard", run(t, testCase{ + input: "...", + expected: ast.SpreadExpr{}, + })) + t.Run("brackets", run(t, testCase{ + input: "[...]", + expected: ast.SpreadExpr{}, + })) + }) + }) + t.Run("property array", func(t *testing.T) { + t.Run("index", run(t, testCase{ + input: "foo[1]", + expected: ast.ChainExprs( + ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, + ast.IndexExpr{Index: ast.NumberIntExpr{Value: 1}}, + ), + })) + t.Run("range", func(t *testing.T) { + t.Run("start and end funcs", run(t, testCase{ + input: "foo[calcStart(1):calcEnd()]", + expected: ast.ChainExprs( + ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, + ast.RangeExpr{ + Start: ast.CallExpr{ + Function: "calcStart", + Args: ast.Expressions{ + ast.NumberIntExpr{Value: 1}, + }, + }, + End: ast.CallExpr{ + Function: "calcEnd", + Args: ast.Expressions{}, + }, + }, + ), + })) + t.Run("start and end", run(t, testCase{ + input: "foo[5:10]", + expected: ast.ChainExprs( + ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, + ast.RangeExpr{Start: ast.NumberIntExpr{Value: 5}, End: ast.NumberIntExpr{Value: 10}}, + ), + })) + t.Run("start", run(t, testCase{ + input: "foo[5:]", + expected: ast.ChainExprs( + ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, + ast.RangeExpr{Start: ast.NumberIntExpr{Value: 5}}, + ), + })) + t.Run("end", run(t, testCase{ + input: "foo[:10]", + expected: ast.ChainExprs( + ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, + ast.RangeExpr{End: ast.NumberIntExpr{Value: 10}}, + ), + })) + }) + t.Run("spread", func(t *testing.T) { + t.Run("standard", run(t, testCase{ + input: "foo...", + expected: ast.ChainExprs( + ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, + ast.SpreadExpr{}, + ), + })) + t.Run("brackets", run(t, testCase{ + input: "foo[...]", + expected: ast.ChainExprs( + ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, + ast.SpreadExpr{}, + ), + })) + }) + }) + }) + + t.Run("map", func(t *testing.T) { + t.Run("single property", run(t, testCase{ + input: "foo.map(x)", + expected: ast.ChainExprs( + ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, + ast.MapExpr{ + Exprs: ast.Expressions{ + ast.PropertyExpr{Property: ast.StringExpr{Value: "x"}}, + }, + }, + ), + })) + t.Run("nested property", run(t, testCase{ + input: "foo.map(x.y)", + expected: ast.ChainExprs( + ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, + ast.MapExpr{ + Exprs: ast.Expressions{ + ast.PropertyExpr{Property: ast.StringExpr{Value: "x"}}, + ast.PropertyExpr{Property: ast.StringExpr{Value: "y"}}, + }, + }, + ), + })) + }) + + t.Run("object", func(t *testing.T) { + t.Run("get single property", run(t, testCase{ + input: "{foo}", + expected: ast.ObjectExpr{Pairs: []ast.KeyValue{ + {Key: ast.StringExpr{Value: "foo"}, Value: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}}, + }}, + })) + t.Run("get multiple properties", run(t, testCase{ + input: "{foo, bar, baz}", + expected: ast.ObjectExpr{Pairs: []ast.KeyValue{ + {Key: ast.StringExpr{Value: "foo"}, Value: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}}, + {Key: ast.StringExpr{Value: "bar"}, Value: ast.PropertyExpr{Property: ast.StringExpr{Value: "bar"}}}, + {Key: ast.StringExpr{Value: "baz"}, Value: ast.PropertyExpr{Property: ast.StringExpr{Value: "baz"}}}, + }}, + })) + t.Run("set single property", run(t, testCase{ + input: "{foo=1}", + expected: ast.ObjectExpr{Pairs: []ast.KeyValue{ + {Key: ast.StringExpr{Value: "foo"}, Value: ast.NumberIntExpr{Value: 1}}, + }}, + })) + t.Run("set multiple properties", run(t, testCase{ + input: "{foo=1, bar=2, baz=3}", + expected: ast.ObjectExpr{Pairs: []ast.KeyValue{ + {Key: ast.StringExpr{Value: "foo"}, Value: ast.NumberIntExpr{Value: 1}}, + {Key: ast.StringExpr{Value: "bar"}, Value: ast.NumberIntExpr{Value: 2}}, + {Key: ast.StringExpr{Value: "baz"}, Value: ast.NumberIntExpr{Value: 3}}, + }}, + })) + t.Run("combine get set", run(t, testCase{ + input: `{ + ..., + foo, + bar=2, + baz=evalSomething(), + "Name"="Tom", + }`, + expected: ast.ObjectExpr{Pairs: []ast.KeyValue{ + {Key: ast.SpreadExpr{}, Value: ast.SpreadExpr{}}, + {Key: ast.StringExpr{Value: "foo"}, Value: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}}, + {Key: ast.StringExpr{Value: "bar"}, Value: ast.NumberIntExpr{Value: 2}}, + {Key: ast.StringExpr{Value: "baz"}, Value: ast.CallExpr{Function: "evalSomething", Args: ast.Expressions{}}}, + {Key: ast.StringExpr{Value: "Name"}, Value: ast.StringExpr{Value: "Tom"}}, + }}, + })) + }) } diff --git a/storage/csv.go b/storage/csv.go index 7265a001..c805832b 100644 --- a/storage/csv.go +++ b/storage/csv.go @@ -4,10 +4,11 @@ import ( "bytes" "encoding/csv" "fmt" - "github.com/tomwright/dasel/v2" - "github.com/tomwright/dasel/v2/dencoding" - "github.com/tomwright/dasel/v2/util" "sort" + + "github.com/tomwright/dasel/v3" + "github.com/tomwright/dasel/v3/dencoding" + "github.com/tomwright/dasel/v3/util" ) func init() { diff --git a/storage/csv_test.go b/storage/csv_test.go index a8eed7f0..919068f0 100644 --- a/storage/csv_test.go +++ b/storage/csv_test.go @@ -1,11 +1,12 @@ package storage_test import ( - "github.com/tomwright/dasel/v2" - "github.com/tomwright/dasel/v2/dencoding" - "github.com/tomwright/dasel/v2/storage" "reflect" "testing" + + "github.com/tomwright/dasel/v3" + "github.com/tomwright/dasel/v3/dencoding" + "github.com/tomwright/dasel/v3/storage" ) var csvBytes = []byte(`id,name diff --git a/storage/json.go b/storage/json.go index 115a8851..cb40264e 100644 --- a/storage/json.go +++ b/storage/json.go @@ -3,9 +3,10 @@ package storage import ( "bytes" "fmt" - "github.com/tomwright/dasel/v2" - "github.com/tomwright/dasel/v2/dencoding" "io" + + "github.com/tomwright/dasel/v3" + "github.com/tomwright/dasel/v3/dencoding" ) func init() { diff --git a/storage/json_test.go b/storage/json_test.go index 95473153..2f869cc0 100644 --- a/storage/json_test.go +++ b/storage/json_test.go @@ -1,11 +1,12 @@ package storage_test import ( - "github.com/tomwright/dasel/v2" - "github.com/tomwright/dasel/v2/dencoding" - "github.com/tomwright/dasel/v2/storage" "reflect" "testing" + + "github.com/tomwright/dasel/v3" + "github.com/tomwright/dasel/v3/dencoding" + "github.com/tomwright/dasel/v3/storage" ) var jsonBytes = []byte(`{ diff --git a/storage/parser.go b/storage/parser.go index 4aea06e8..3b1b7f68 100644 --- a/storage/parser.go +++ b/storage/parser.go @@ -7,7 +7,7 @@ import ( "path/filepath" "strings" - "github.com/tomwright/dasel/v2" + "github.com/tomwright/dasel/v3" ) var readParsersByExtension = map[string]ReadParser{} diff --git a/storage/parser_test.go b/storage/parser_test.go index 0a29ea51..307ccb31 100644 --- a/storage/parser_test.go +++ b/storage/parser_test.go @@ -7,9 +7,9 @@ import ( "strings" "testing" - "github.com/tomwright/dasel/v2" - "github.com/tomwright/dasel/v2/dencoding" - "github.com/tomwright/dasel/v2/storage" + "github.com/tomwright/dasel/v3" + "github.com/tomwright/dasel/v3/dencoding" + "github.com/tomwright/dasel/v3/storage" ) func TestUnknownParserErr_Error(t *testing.T) { diff --git a/storage/plain.go b/storage/plain.go index a0d1f333..491aa9e0 100644 --- a/storage/plain.go +++ b/storage/plain.go @@ -3,7 +3,8 @@ package storage import ( "bytes" "fmt" - "github.com/tomwright/dasel/v2" + + "github.com/tomwright/dasel/v3" ) func init() { diff --git a/storage/plain_test.go b/storage/plain_test.go index 93e50c4f..a6804731 100644 --- a/storage/plain_test.go +++ b/storage/plain_test.go @@ -2,9 +2,10 @@ package storage_test import ( "errors" - "github.com/tomwright/dasel/v2" - "github.com/tomwright/dasel/v2/storage" "testing" + + "github.com/tomwright/dasel/v3" + "github.com/tomwright/dasel/v3/storage" ) func TestPlainParser_FromBytes(t *testing.T) { diff --git a/storage/toml.go b/storage/toml.go index 14266b47..68eb9646 100644 --- a/storage/toml.go +++ b/storage/toml.go @@ -3,9 +3,10 @@ package storage import ( "bytes" "fmt" - "github.com/tomwright/dasel/v2" - "github.com/tomwright/dasel/v2/dencoding" "io" + + "github.com/tomwright/dasel/v3" + "github.com/tomwright/dasel/v3/dencoding" ) func init() { diff --git a/storage/toml_test.go b/storage/toml_test.go index ef6637cb..0788cac4 100644 --- a/storage/toml_test.go +++ b/storage/toml_test.go @@ -5,8 +5,8 @@ import ( "strings" "testing" - "github.com/tomwright/dasel/v2" - "github.com/tomwright/dasel/v2/storage" + "github.com/tomwright/dasel/v3" + "github.com/tomwright/dasel/v3/storage" ) var tomlBytes = []byte(`names = ['John', 'Frank'] diff --git a/storage/xml.go b/storage/xml.go index 34b98491..390cbac5 100644 --- a/storage/xml.go +++ b/storage/xml.go @@ -3,10 +3,11 @@ package storage import ( "bytes" "fmt" - "github.com/tomwright/dasel/v2" - "github.com/tomwright/dasel/v2/dencoding" "strings" + "github.com/tomwright/dasel/v3" + "github.com/tomwright/dasel/v3/dencoding" + "github.com/clbanning/mxj/v2" "golang.org/x/net/html/charset" ) diff --git a/storage/xml_test.go b/storage/xml_test.go index 00aceac1..6cfe3c61 100644 --- a/storage/xml_test.go +++ b/storage/xml_test.go @@ -3,12 +3,13 @@ package storage_test import ( "bytes" "fmt" - "github.com/tomwright/dasel/v2" - "github.com/tomwright/dasel/v2/storage" "io" "reflect" "testing" + "github.com/tomwright/dasel/v3" + "github.com/tomwright/dasel/v3/storage" + "golang.org/x/text/encoding/charmap" "golang.org/x/text/encoding/unicode" ) diff --git a/storage/yaml.go b/storage/yaml.go index 0c8c9bc2..38c06094 100644 --- a/storage/yaml.go +++ b/storage/yaml.go @@ -3,10 +3,11 @@ package storage import ( "bytes" "fmt" - "github.com/tomwright/dasel/v2" - "github.com/tomwright/dasel/v2/dencoding" - "github.com/tomwright/dasel/v2/util" "io" + + "github.com/tomwright/dasel/v3" + "github.com/tomwright/dasel/v3/dencoding" + "github.com/tomwright/dasel/v3/util" ) func init() { diff --git a/storage/yaml_test.go b/storage/yaml_test.go index 53d8f8f2..c12652d4 100644 --- a/storage/yaml_test.go +++ b/storage/yaml_test.go @@ -1,12 +1,13 @@ package storage_test import ( - "github.com/tomwright/dasel/v2" - "github.com/tomwright/dasel/v2/dencoding" - "github.com/tomwright/dasel/v2/storage" "reflect" "strings" "testing" + + "github.com/tomwright/dasel/v3" + "github.com/tomwright/dasel/v3/dencoding" + "github.com/tomwright/dasel/v3/storage" ) var yamlBytes = []byte(`name: Tom diff --git a/value.go b/value.go index e809bb64..2dee6994 100644 --- a/value.go +++ b/value.go @@ -3,8 +3,8 @@ package dasel import ( "reflect" - "github.com/tomwright/dasel/v2/dencoding" - "github.com/tomwright/dasel/v2/util" + "github.com/tomwright/dasel/v3/dencoding" + "github.com/tomwright/dasel/v3/util" ) // Value is a wrapper around reflect.Value that adds some handy helper funcs. From f4427b6a33fbaaf612fe1ffa6ce476625806480e Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Mon, 30 Sep 2024 19:53:40 +0100 Subject: [PATCH 04/56] Fix issues --- execution/execute_test.go | 45 ++++++++++++++++++++++----------------- model/value_literal.go | 5 +++-- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/execution/execute_test.go b/execution/execute_test.go index 959edb2c..75cc22f9 100644 --- a/execution/execute_test.go +++ b/execution/execute_test.go @@ -9,6 +9,7 @@ import ( "github.com/tomwright/dasel/v3/dencoding" "github.com/tomwright/dasel/v3/execution" "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/ptr" ) func TestExecuteSelector_HappyPath(t *testing.T) { @@ -184,28 +185,34 @@ func TestExecuteSelector_HappyPath(t *testing.T) { t.Run("map", func(t *testing.T) { t.Run("property from slice of maps", runTest(testCase{ inFn: func() *model.Value { - r := model.NewSliceValue() - - m1 := model.NewMapValue() - _ = m1.SetMapKey("number", model.NewIntValue(1)) - m2 := model.NewMapValue() - _ = m2.SetMapKey("number", model.NewIntValue(2)) - m3 := model.NewMapValue() - _ = m3.SetMapKey("number", model.NewIntValue(3)) - - _ = r.Append(m1) - _ = r.Append(m2) - _ = r.Append(m3) - - return r + return model.NewValue([]any{ + dencoding.NewMap().Set("number", 1), + dencoding.NewMap().Set("number", 2), + dencoding.NewMap().Set("number", 3), + }) }, s: `map(number)`, outFn: func() *model.Value { - r := model.NewSliceValue() - _ = r.Append(model.NewIntValue(1)) - _ = r.Append(model.NewIntValue(2)) - _ = r.Append(model.NewIntValue(3)) - return r + return model.NewValue([]any{1, 2, 3}) + }, + })) + t.Run("with chain of selectors", runTest(testCase{ + inFn: func() *model.Value { + return model.NewValue([]any{ + dencoding.NewMap().Set("foo", 1).Set("bar", 4), + dencoding.NewMap().Set("foo", 2).Set("bar", 5), + dencoding.NewMap().Set("foo", 3).Set("bar", 6), + }) + }, + s: ` + .map ( + { + total = add( foo, bar, 1 ) + } + ) + .map ( total )`, + outFn: func() *model.Value { + return model.NewValue([]any{ptr.To(int64(6)), ptr.To(int64(8)), ptr.To(int64(10))}) }, })) }) diff --git a/model/value_literal.go b/model/value_literal.go index 1879a0e3..9be3af85 100644 --- a/model/value_literal.go +++ b/model/value_literal.go @@ -3,6 +3,7 @@ package model import ( "fmt" "reflect" + "slices" ) func NewStringValue(x string) *Value { @@ -38,7 +39,7 @@ func (v *Value) IsInt() bool { } func (v *Value) isInt() bool { - return v.Value.Kind() == reflect.Int64 + return slices.Contains([]reflect.Kind{reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64}, v.Value.Kind()) } func (v *Value) IntValue() (int64, error) { @@ -60,7 +61,7 @@ func (v *Value) IsFloat() bool { } func (v *Value) isFloat() bool { - return v.Value.Kind() == reflect.Float64 + return slices.Contains([]reflect.Kind{reflect.Float32, reflect.Float64}, v.Value.Kind()) } func (v *Value) FloatValue() (float64, error) { From 1657e5d2cb31b201574c146cf1b7e0468fc2980b Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Tue, 1 Oct 2024 01:03:31 +0100 Subject: [PATCH 05/56] Get initial binary expressions working --- execution/execute.go | 48 +++++- execution/execute_test.go | 69 ++++++++ model/value_comparison.go | 153 +++++++++++++++++ model/value_math.go | 266 +++++++++++++++++++++++++++++ selector/ast/expression_complex.go | 13 ++ selector/lexer/token.go | 16 +- selector/lexer/tokenize.go | 30 +++- selector/lexer/tokenize_test.go | 54 +++++- selector/parser/denotations.go | 26 +++ selector/parser/parse_array.go | 28 ++- selector/parser/parse_func.go | 2 +- selector/parser/parse_map.go | 9 +- selector/parser/parse_object.go | 4 +- selector/parser/parse_variable.go | 31 ++++ selector/parser/parser.go | 47 +++-- selector/parser/parser_binary.go | 30 ++++ selector/parser/parser_test.go | 11 ++ 17 files changed, 805 insertions(+), 32 deletions(-) create mode 100644 model/value_comparison.go create mode 100644 model/value_math.go create mode 100644 selector/parser/denotations.go create mode 100644 selector/parser/parse_variable.go create mode 100644 selector/parser/parser_binary.go diff --git a/execution/execute.go b/execution/execute.go index 8681cb1a..96856609 100644 --- a/execution/execute.go +++ b/execution/execute.go @@ -59,6 +59,8 @@ func exprExecutor(expr ast.Expr) (expressionExecutor, error) { return indexExprExecutor(e) case ast.PropertyExpr: return propertyExprExecutor(e) + case ast.VariableExpr: + return variableExprExecutor(e) case ast.NumberIntExpr: return numberIntExprExecutor(e) case ast.NumberFloatExpr: @@ -78,7 +80,41 @@ func exprExecutor(expr ast.Expr) (expressionExecutor, error) { func binaryExprExecutor(e ast.BinaryExpr) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { - panic("not implemented") + left, err := ExecuteAST(e.Left, data) + if err != nil { + return nil, fmt.Errorf("error evaluating left expression: %w", err) + } + right, err := ExecuteAST(e.Right, data) + if err != nil { + return nil, fmt.Errorf("error evaluating right expression: %w", err) + } + + switch e.Operator.Kind { + case lexer.Plus: + return left.Add(right) + case lexer.Dash: + return left.Subtract(right) + case lexer.Star: + return left.Multiply(right) + case lexer.Slash: + return left.Divide(right) + case lexer.Percent: + return left.Modulo(right) + case lexer.GreaterThan: + return left.GreaterThan(right) + case lexer.GreaterThanOrEqual: + return left.GreaterThanOrEqual(right) + case lexer.LessThan: + return left.LessThan(right) + case lexer.LessThanOrEqual: + return left.LessThanOrEqual(right) + case lexer.Equal: + return left.Equal(right) + case lexer.NotEqual: + return left.NotEqual(right) + default: + return nil, fmt.Errorf("unhandled operator: %s", e.Operator.Value) + } }, nil } @@ -155,3 +191,13 @@ func propertyExprExecutor(e ast.PropertyExpr) (expressionExecutor, error) { return data.GetMapKey(keyStr) }, nil } + +func variableExprExecutor(e ast.VariableExpr) (expressionExecutor, error) { + return func(data *model.Value) (*model.Value, error) { + varName := e.Name + if varName == "this" { + return data, nil + } + return nil, fmt.Errorf("variable %s not found", varName) + }, nil +} diff --git a/execution/execute_test.go b/execution/execute_test.go index 75cc22f9..2e79c75e 100644 --- a/execution/execute_test.go +++ b/execution/execute_test.go @@ -54,6 +54,75 @@ func TestExecuteSelector_HappyPath(t *testing.T) { } } + t.Run("binary expressions", func(t *testing.T) { + t.Run("math", func(t *testing.T) { + t.Run("literals", func(t *testing.T) { + t.Run("addition", runTest(testCase{ + in: model.NewValue(nil), + s: `1 + 2`, + out: model.NewIntValue(3), + })) + t.Run("subtraction", runTest(testCase{ + in: model.NewValue(nil), + s: `5 - 2`, + out: model.NewIntValue(3), + })) + t.Run("multiplication", runTest(testCase{ + in: model.NewValue(nil), + s: `5 * 2`, + out: model.NewIntValue(10), + })) + t.Run("division", runTest(testCase{ + in: model.NewValue(nil), + s: `10 / 2`, + out: model.NewIntValue(5), + })) + t.Run("modulus", runTest(testCase{ + in: model.NewValue(nil), + s: `10 % 3`, + out: model.NewIntValue(1), + })) + t.Run("ordering", runTest(testCase{ + in: model.NewValue(nil), + s: `45.2 + 5 * 4 - 2 / 2`, // 45.2 + (5 * 4) - (2 / 2) = 45.2 + 20 - 1 + out: model.NewFloatValue(64.2), + })) + }) + }) + t.Run("comparison", func(t *testing.T) { + t.Run("equal", runTest(testCase{ + in: model.NewValue(nil), + s: `1 == 1`, + out: model.NewBoolValue(true), + })) + t.Run("not equal", runTest(testCase{ + in: model.NewValue(nil), + s: `1 != 1`, + out: model.NewBoolValue(false), + })) + t.Run("greater than", runTest(testCase{ + in: model.NewValue(nil), + s: `2 > 1`, + out: model.NewBoolValue(true), + })) + t.Run("greater than or equal", runTest(testCase{ + in: model.NewValue(nil), + s: `2 >= 2`, + out: model.NewBoolValue(true), + })) + t.Run("less than", runTest(testCase{ + in: model.NewValue(nil), + s: `1 < 2`, + out: model.NewBoolValue(true), + })) + t.Run("less than or equal", runTest(testCase{ + in: model.NewValue(nil), + s: `2 <= 2`, + out: model.NewBoolValue(true), + })) + }) + }) + t.Run("literal", func(t *testing.T) { t.Run("string", runTest(testCase{ in: model.NewValue(nil), diff --git a/model/value_comparison.go b/model/value_comparison.go new file mode 100644 index 00000000..c0cc5977 --- /dev/null +++ b/model/value_comparison.go @@ -0,0 +1,153 @@ +package model + +func (v *Value) Equal(other *Value) (*Value, error) { + if v.IsInt() && other.IsInt() { + a, err := v.IntValue() + if err != nil { + return nil, err + } + b, err := other.IntValue() + if err != nil { + return nil, err + } + return NewValue(a == b), nil + } + if v.IsFloat() && other.IsFloat() { + a, err := v.FloatValue() + if err != nil { + return nil, err + } + b, err := other.FloatValue() + if err != nil { + return nil, err + } + return NewValue(a == b), nil + } + if v.IsInt() && other.IsFloat() { + a, err := v.IntValue() + if err != nil { + return nil, err + } + b, err := other.FloatValue() + if err != nil { + return nil, err + } + return NewValue(float64(a) == b), nil + } + if v.IsFloat() && other.IsInt() { + a, err := v.FloatValue() + if err != nil { + return nil, err + } + b, err := other.IntValue() + if err != nil { + return nil, err + } + return NewValue(a == float64(b)), nil + } + return nil, &ErrIncompatibleTypes{A: v, B: other} +} + +func (v *Value) NotEqual(other *Value) (*Value, error) { + equals, err := v.Equal(other) + if err != nil { + return nil, err + } + boolValue, err := equals.BoolValue() + if err != nil { + return nil, err + } + return NewValue(!boolValue), nil +} + +func (v *Value) LessThan(other *Value) (*Value, error) { + if v.IsInt() && other.IsInt() { + a, err := v.IntValue() + if err != nil { + return nil, err + } + b, err := other.IntValue() + if err != nil { + return nil, err + } + return NewValue(a < b), nil + } + if v.IsFloat() && other.IsFloat() { + a, err := v.FloatValue() + if err != nil { + return nil, err + } + b, err := other.FloatValue() + if err != nil { + return nil, err + } + return NewValue(a < b), nil + } + if v.IsInt() && other.IsFloat() { + a, err := v.IntValue() + if err != nil { + return nil, err + } + b, err := other.FloatValue() + if err != nil { + return nil, err + } + return NewValue(float64(a) < b), nil + } + if v.IsFloat() && other.IsInt() { + a, err := v.FloatValue() + if err != nil { + return nil, err + } + b, err := other.IntValue() + if err != nil { + return nil, err + } + return NewValue(a < float64(b)), nil + } + return nil, &ErrIncompatibleTypes{A: v, B: other} +} + +func (v *Value) LessThanOrEqual(other *Value) (*Value, error) { + lessThan, err := v.LessThan(other) + if err != nil { + return nil, err + } + boolValue, err := lessThan.BoolValue() + if err != nil { + return nil, err + } + equals, err := v.Equal(other) + if err != nil { + return nil, err + } + boolEquals, err := equals.BoolValue() + if err != nil { + return nil, err + } + return NewValue(boolValue || boolEquals), nil +} + +func (v *Value) GreaterThan(other *Value) (*Value, error) { + lessThanOrEqual, err := v.LessThanOrEqual(other) + if err != nil { + return nil, err + } + boolValue, err := lessThanOrEqual.BoolValue() + if err != nil { + return nil, err + } + return NewValue(!boolValue), nil +} + +func (v *Value) GreaterThanOrEqual(other *Value) (*Value, error) { + lessThan, err := v.LessThan(other) + if err != nil { + return nil, err + } + boolValue, err := lessThan.BoolValue() + if err != nil { + return nil, err + } + return NewValue(!boolValue), nil +} diff --git a/model/value_math.go b/model/value_math.go new file mode 100644 index 00000000..94699dc7 --- /dev/null +++ b/model/value_math.go @@ -0,0 +1,266 @@ +package model + +import ( + "fmt" + "math" +) + +type ErrIncompatibleTypes struct { + A *Value + B *Value +} + +func (e *ErrIncompatibleTypes) Error() string { + return fmt.Sprintf("incompatible types: %s and %s", e.A.Type(), e.B.Type()) +} + +func (v *Value) Add(other *Value) (*Value, error) { + if v.IsInt() && other.IsInt() { + a, err := v.IntValue() + if err != nil { + return nil, err + } + b, err := other.IntValue() + if err != nil { + return nil, err + } + return NewValue(a + b), nil + } + if v.IsFloat() && other.IsFloat() { + a, err := v.FloatValue() + if err != nil { + return nil, err + } + b, err := other.FloatValue() + if err != nil { + return nil, err + } + return NewValue(a + b), nil + } + if v.IsInt() && other.IsFloat() { + a, err := v.IntValue() + if err != nil { + return nil, err + } + b, err := other.FloatValue() + if err != nil { + return nil, err + } + return NewValue(float64(a) + b), nil + } + if v.IsFloat() && other.IsInt() { + a, err := v.FloatValue() + if err != nil { + return nil, err + } + b, err := other.IntValue() + if err != nil { + return nil, err + } + return NewValue(a + float64(b)), nil + } + if v.IsString() && other.IsString() { + a, err := v.StringValue() + if err != nil { + return nil, err + } + b, err := other.StringValue() + if err != nil { + return nil, err + } + return NewValue(a + b), nil + } + return nil, &ErrIncompatibleTypes{A: v, B: other} +} + +func (v *Value) Subtract(other *Value) (*Value, error) { + if v.IsInt() && other.IsInt() { + a, err := v.IntValue() + if err != nil { + return nil, err + } + b, err := other.IntValue() + if err != nil { + return nil, err + } + return NewValue(a - b), nil + } + if v.IsFloat() && other.IsFloat() { + a, err := v.FloatValue() + if err != nil { + return nil, err + } + b, err := other.FloatValue() + if err != nil { + return nil, err + } + return NewValue(a - b), nil + } + if v.IsInt() && other.IsFloat() { + a, err := v.IntValue() + if err != nil { + return nil, err + } + b, err := other.FloatValue() + if err != nil { + return nil, err + } + return NewValue(float64(a) - b), nil + } + if v.IsFloat() && other.IsInt() { + a, err := v.FloatValue() + if err != nil { + return nil, err + } + b, err := other.IntValue() + if err != nil { + return nil, err + } + return NewValue(a - float64(b)), nil + } + return nil, &ErrIncompatibleTypes{A: v, B: other} +} + +func (v *Value) Multiply(other *Value) (*Value, error) { + if v.IsInt() && other.IsInt() { + a, err := v.IntValue() + if err != nil { + return nil, err + } + b, err := other.IntValue() + if err != nil { + return nil, err + } + return NewValue(a * b), nil + } + if v.IsFloat() && other.IsFloat() { + a, err := v.FloatValue() + if err != nil { + return nil, err + } + b, err := other.FloatValue() + if err != nil { + return nil, err + } + return NewValue(a * b), nil + } + if v.IsInt() && other.IsFloat() { + a, err := v.IntValue() + if err != nil { + return nil, err + } + b, err := other.FloatValue() + if err != nil { + return nil, err + } + return NewValue(float64(a) * b), nil + } + if v.IsFloat() && other.IsInt() { + a, err := v.FloatValue() + if err != nil { + return nil, err + } + b, err := other.IntValue() + if err != nil { + return nil, err + } + return NewValue(a * float64(b)), nil + } + return nil, &ErrIncompatibleTypes{A: v, B: other} +} + +func (v *Value) Divide(other *Value) (*Value, error) { + if v.IsInt() && other.IsInt() { + a, err := v.IntValue() + if err != nil { + return nil, err + } + b, err := other.IntValue() + if err != nil { + return nil, err + } + return NewValue(a / b), nil + } + if v.IsFloat() && other.IsFloat() { + a, err := v.FloatValue() + if err != nil { + return nil, err + } + b, err := other.FloatValue() + if err != nil { + return nil, err + } + return NewValue(a / b), nil + } + if v.IsInt() && other.IsFloat() { + a, err := v.IntValue() + if err != nil { + return nil, err + } + b, err := other.FloatValue() + if err != nil { + return nil, err + } + return NewValue(float64(a) / b), nil + } + if v.IsFloat() && other.IsInt() { + a, err := v.FloatValue() + if err != nil { + return nil, err + } + b, err := other.IntValue() + if err != nil { + return nil, err + } + return NewValue(a / float64(b)), nil + } + return nil, &ErrIncompatibleTypes{A: v, B: other} +} + +func (v *Value) Modulo(other *Value) (*Value, error) { + if v.IsInt() && other.IsInt() { + a, err := v.IntValue() + if err != nil { + return nil, err + } + b, err := other.IntValue() + if err != nil { + return nil, err + } + return NewValue(a % b), nil + } + if v.IsFloat() && other.IsFloat() { + a, err := v.FloatValue() + if err != nil { + return nil, err + } + b, err := other.FloatValue() + if err != nil { + return nil, err + } + return NewValue(math.Mod(a, b)), nil + } + if v.IsInt() && other.IsFloat() { + a, err := v.IntValue() + if err != nil { + return nil, err + } + b, err := other.FloatValue() + if err != nil { + return nil, err + } + return NewValue(math.Mod(float64(a), b)), nil + } + if v.IsFloat() && other.IsInt() { + a, err := v.FloatValue() + if err != nil { + return nil, err + } + b, err := other.IntValue() + if err != nil { + return nil, err + } + return NewValue(math.Mod(a, float64(b))), nil + } + return nil, &ErrIncompatibleTypes{A: v, B: other} +} diff --git a/selector/ast/expression_complex.go b/selector/ast/expression_complex.go index c616253a..6fd23040 100644 --- a/selector/ast/expression_complex.go +++ b/selector/ast/expression_complex.go @@ -10,6 +10,13 @@ type BinaryExpr struct { func (BinaryExpr) expr() {} +type UnaryExpr struct { + Operator lexer.Token + Right Expr +} + +func (UnaryExpr) expr() {} + type CallExpr struct { Function string Args Expressions @@ -76,3 +83,9 @@ type MapExpr struct { } func (MapExpr) expr() {} + +type VariableExpr struct { + Name string +} + +func (VariableExpr) expr() {} diff --git a/selector/lexer/token.go b/selector/lexer/token.go index 4d832984..2c9f3604 100644 --- a/selector/lexer/token.go +++ b/selector/lexer/token.go @@ -18,10 +18,9 @@ const ( CloseCurly OpenParen CloseParen - Equal - Equals - NotEqual - Not + Equal // == + Equals // = + NotEqual // != And Or Like @@ -29,7 +28,7 @@ const ( String Number Bool - Add + Plus Increment IncrementBy Dash @@ -40,6 +39,13 @@ const ( Percent Dot Spread + Dollar + Variable + GreaterThan + GreaterThanOrEqual + LessThan + LessThanOrEqual + Exclamation ) type Tokens []Token diff --git a/selector/lexer/tokenize.go b/selector/lexer/tokenize.go index bd2e73cc..23d200a7 100644 --- a/selector/lexer/tokenize.go +++ b/selector/lexer/tokenize.go @@ -41,6 +41,13 @@ func (p *Tokenizer) peekRuneEqual(i int, to rune) bool { return rune(p.src[i]) == to } +func (p *Tokenizer) peekRuneMatches(i int, fn func(rune) bool) bool { + if i >= p.srcLen { + return false + } + return fn(rune(p.src[i])) +} + func (p *Tokenizer) parseCurRune() (Token, error) { switch p.src[p.i] { case '.': @@ -70,6 +77,15 @@ func (p *Tokenizer) parseCurRune() (Token, error) { return NewToken(Slash, "/", p.i, 1), nil case '%': return NewToken(Percent, "%", p.i, 1), nil + case '$': + if p.peekRuneMatches(p.i+1, unicode.IsLetter) { + pos := p.i + 1 + for pos < p.srcLen && (unicode.IsLetter(rune(p.src[pos])) || unicode.IsDigit(rune(p.src[pos]))) { + pos++ + } + return NewToken(Variable, p.src[p.i+1:pos], p.i, pos-p.i), nil + } + return NewToken(Dollar, "$", p.i, 1), nil case '=': if p.peekRuneEqual(p.i+1, '=') { return NewToken(Equal, "==", p.i, 2), nil @@ -85,7 +101,7 @@ func (p *Tokenizer) parseCurRune() (Token, error) { if p.peekRuneEqual(p.i+1, '+') { return NewToken(Increment, "++", p.i, 2), nil } - return NewToken(Add, "+", p.i, 1), nil + return NewToken(Plus, "+", p.i, 1), nil case '-': if p.peekRuneEqual(p.i+1, '=') { return NewToken(DecrementBy, "-=", p.i, 2), nil @@ -94,6 +110,16 @@ func (p *Tokenizer) parseCurRune() (Token, error) { return NewToken(Decrement, "--", p.i, 2), nil } return NewToken(Dash, "-", p.i, 1), nil + case '>': + if p.peekRuneEqual(p.i+1, '=') { + return NewToken(GreaterThanOrEqual, ">=", p.i, 2), nil + } + return NewToken(GreaterThan, ">", p.i, 1), nil + case '<': + if p.peekRuneEqual(p.i+1, '=') { + return NewToken(LessThanOrEqual, "<>>=", p.i, 2), nil + } + return NewToken(LessThan, "<", p.i, 1), nil case '!': if p.peekRuneEqual(p.i+1, '=') { return NewToken(NotEqual, "!=", p.i, 2), nil @@ -101,7 +127,7 @@ func (p *Tokenizer) parseCurRune() (Token, error) { if p.peekRuneEqual(p.i+1, '~') { return NewToken(NotLike, "!~", p.i, 2), nil } - return NewToken(Not, "!", p.i, 1), nil + return NewToken(Exclamation, "!", p.i, 1), nil case '&': if p.peekRuneEqual(p.i+1, '&') { return NewToken(And, "&&", p.i, 2), nil diff --git a/selector/lexer/tokenize_test.go b/selector/lexer/tokenize_test.go index ab9685ff..3d0401d9 100644 --- a/selector/lexer/tokenize_test.go +++ b/selector/lexer/tokenize_test.go @@ -3,7 +3,56 @@ package lexer import "testing" func TestTokenizer_Parse(t *testing.T) { - tok := NewTokenizer("foo.bar.baz[1] != 42.123 || foo.bar.baz['hello'] == 42 && x == 'a\\'b' + false . .... asd...") + type testCase struct { + in string + out []TokenKind + } + + runTest := func(tc testCase) func(t *testing.T) { + return func(t *testing.T) { + tok := NewTokenizer(tc.in) + tokens, err := tok.Tokenize() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(tokens) != len(tc.out) { + t.Fatalf("unexpected number of tokens: %d", len(tokens)) + } + for i := range tokens { + if tokens[i].Kind != tc.out[i] { + t.Errorf("unexpected token kind at position %d: exp %v, got %v", i, tc.out[i], tokens[i].Kind) + return + } + } + } + } + + t.Run("variables", runTest(testCase{ + in: "$foo $bar123 $baz $", + out: []TokenKind{ + Variable, + Variable, + Variable, + Dollar, + }, + })) + + t.Run("everything", runTest(testCase{ + in: "foo.bar.baz[1] != 42.123 || foo.bar.baz['hello'] == 42 && x == 'a\\'b' + false . .... asd... $name", + out: []TokenKind{ + Symbol, Dot, Symbol, Dot, Symbol, OpenBracket, Number, CloseBracket, NotEqual, Number, + Or, + Symbol, Dot, Symbol, Dot, Symbol, OpenBracket, String, CloseBracket, Equal, Number, + And, + Symbol, Equal, String, + Plus, Bool, + Dot, Spread, Dot, + Symbol, Spread, + Variable, + }, + })) + + tok := NewTokenizer("foo.bar.baz[1] != 42.123 || foo.bar.baz['hello'] == 42 && x == 'a\\'b' + false . .... asd... $name") tokens, err := tok.Tokenize() if err != nil { t.Fatalf("unexpected error: %v", err) @@ -14,9 +63,10 @@ func TestTokenizer_Parse(t *testing.T) { Symbol, Dot, Symbol, Dot, Symbol, OpenBracket, String, CloseBracket, Equal, Number, And, Symbol, Equal, String, - Add, Bool, + Plus, Bool, Dot, Spread, Dot, Symbol, Spread, + Variable, } if len(tokens) != len(exp) { t.Fatalf("unexpected number of tokens: %d", len(tokens)) diff --git a/selector/parser/denotations.go b/selector/parser/denotations.go new file mode 100644 index 00000000..40589a0c --- /dev/null +++ b/selector/parser/denotations.go @@ -0,0 +1,26 @@ +package parser + +import "github.com/tomwright/dasel/v3/selector/lexer" + +// null denotation tokens are tokens that expect no token to the left of them. +var nullDenotationTokens = []lexer.TokenKind{} + +// left denotation tokens are tokens that expect a token to the left of them. +var leftDenotationTokens = []lexer.TokenKind{ + lexer.Plus, + lexer.Dash, + lexer.Slash, + lexer.Star, + lexer.Percent, + lexer.Equal, + lexer.NotEqual, + lexer.GreaterThan, + lexer.GreaterThanOrEqual, + lexer.LessThan, + lexer.LessThanOrEqual, +} + +// right denotation tokens are tokens that expect a token to the right of them. +var rightDenotationTokens = []lexer.TokenKind{ + lexer.Exclamation, // Not operator +} diff --git a/selector/parser/parse_array.go b/selector/parser/parse_array.go index c376861c..f6f3f33b 100644 --- a/selector/parser/parse_array.go +++ b/selector/parser/parse_array.go @@ -6,7 +6,7 @@ import ( ) func parseArray(p *Parser) (ast.Expr, error) { - if err := p.expect(lexer.Symbol); err != nil { + if err := p.expect(lexer.Symbol, lexer.Variable); err != nil { return nil, err } if err := p.expectN(1, lexer.OpenBracket); err != nil { @@ -19,10 +19,24 @@ func parseArray(p *Parser) (ast.Expr, error) { if err != nil { return nil, err } - return ast.ChainExprs( - ast.PropertyExpr{ + + var e ast.Expr + + switch { + case token.IsKind(lexer.Variable): + e = ast.VariableExpr{ + Name: token.Value, + } + case token.IsKind(lexer.Symbol): + e = ast.PropertyExpr{ Property: ast.StringExpr{Value: token.Value}, - }, + } + default: + panic("unexpected token kind") + } + + return ast.ChainExprs( + e, idx, ), nil } @@ -61,7 +75,7 @@ func parseSquareBrackets(p *Parser) (ast.Expr, error) { if p.current().IsKind(lexer.Colon) { p.advance() // We have no start index - end, err = p.parseExpression() + end, _, err = p.parseExpression(nil) if err != nil { return nil, err } @@ -71,7 +85,7 @@ func parseSquareBrackets(p *Parser) (ast.Expr, error) { }, nil } - start, err = p.parseExpression() + start, _, err = p.parseExpression(nil) if err != nil { return nil, err } @@ -98,7 +112,7 @@ func parseSquareBrackets(p *Parser) (ast.Expr, error) { }, nil } - end, err = p.parseExpression() + end, _, err = p.parseExpression(nil) if err != nil { return nil, err } diff --git a/selector/parser/parse_func.go b/selector/parser/parse_func.go index 8a8475ba..22bf2ddd 100644 --- a/selector/parser/parse_func.go +++ b/selector/parser/parse_func.go @@ -37,7 +37,7 @@ func parseArgs(p *Parser) ([]ast.Expr, error) { break } - arg, err := p.parseExpression() + arg, _, err := p.parseExpression(nil) if err != nil { return nil, err } diff --git a/selector/parser/parse_map.go b/selector/parser/parse_map.go index 7796a350..c73469dd 100644 --- a/selector/parser/parse_map.go +++ b/selector/parser/parse_map.go @@ -26,6 +26,9 @@ func parseMap(p *Parser) (ast.Expr, error) { expressions := make([]ast.Expr, 0) + var expr ast.Expr + var err error + var replaceLast bool for { if p.current().IsKind(lexer.CloseParen) { if len(expressions) == 0 { @@ -40,10 +43,14 @@ func parseMap(p *Parser) (ast.Expr, error) { continue } - expr, err := p.parseExpression() + expr, replaceLast, err = p.parseExpression(expr) if err != nil { return nil, err } + if replaceLast { + expressions[len(expressions)-1] = expr + continue + } expressions = append(expressions, expr) } diff --git a/selector/parser/parse_object.go b/selector/parser/parse_object.go index b8ecb204..1afd203b 100644 --- a/selector/parser/parse_object.go +++ b/selector/parser/parse_object.go @@ -51,7 +51,7 @@ func parseObject(p *Parser) (ast.Expr, error) { continue } - key, err := p.parseExpression() + key, _, err := p.parseExpression(nil) if err != nil { return nil, err } @@ -66,7 +66,7 @@ func parseObject(p *Parser) (ast.Expr, error) { } p.advance() - val, err := p.parseExpression() + val, _, err := p.parseExpression(nil) if err != nil { return nil, err } diff --git a/selector/parser/parse_variable.go b/selector/parser/parse_variable.go new file mode 100644 index 00000000..de70beea --- /dev/null +++ b/selector/parser/parse_variable.go @@ -0,0 +1,31 @@ +package parser + +import ( + "github.com/tomwright/dasel/v3/selector/ast" + "github.com/tomwright/dasel/v3/selector/lexer" +) + +func parseVariable(p *Parser) (ast.Expr, error) { + token := p.current() + + next := p.peek() + + if next.IsKind(lexer.OpenBracket) { + return parseArray(p) + } + + prop := ast.VariableExpr{ + Name: token.Value, + } + + if next.IsKind(lexer.Spread) { + p.advanceN(2) + return ast.ChainExprs( + prop, + ast.SpreadExpr{}, + ), nil + } + + p.advance() + return prop, nil +} diff --git a/selector/parser/parser.go b/selector/parser/parser.go index 9025c6cf..b3d4dc4c 100644 --- a/selector/parser/parser.go +++ b/selector/parser/parser.go @@ -2,6 +2,7 @@ package parser import ( "fmt" + "slices" "github.com/tomwright/dasel/v3/selector/ast" "github.com/tomwright/dasel/v3/selector/lexer" @@ -41,7 +42,7 @@ func (p *Parser) currentScope() scope { func (p *Parser) endOfExpressionTokens() []lexer.TokenKind { switch p.currentScope() { case scopeRoot: - return []lexer.TokenKind{lexer.EOF, lexer.Dot} + return append([]lexer.TokenKind{lexer.EOF, lexer.Dot}, leftDenotationTokens...) case scopeFuncArgs: return []lexer.TokenKind{lexer.Comma, lexer.CloseParen} case scopeMap: @@ -71,6 +72,9 @@ func NewParser(tokens lexer.Tokens) *Parser { func (p *Parser) Parse() (ast.Expr, error) { var expressions ast.Expressions + var expr ast.Expr + var err error + var replaceLast bool for p.hasToken() { if p.current().IsKind(lexer.EOF) { break @@ -79,10 +83,14 @@ func (p *Parser) Parse() (ast.Expr, error) { p.advance() continue } - expr, err := p.parseExpression() + expr, replaceLast, err = p.parseExpression(expr) if err != nil { return nil, err } + if replaceLast { + expressions[len(expressions)-1] = expr + continue + } expressions = append(expressions, expr) } switch len(expressions) { @@ -95,32 +103,41 @@ func (p *Parser) Parse() (ast.Expr, error) { } } -func (p *Parser) parseExpression() (res ast.Expr, err error) { +func (p *Parser) parseExpression(last ast.Expr) (res ast.Expr, replaceLast bool, err error) { defer func() { if err == nil { err = p.expectEndOfExpression() } }() + + if last != nil && slices.Contains(leftDenotationTokens, p.current().Kind) { + res, replaceLast, err = parseBinary(p, last) + return + } + switch p.current().Kind { case lexer.String: - return parseStringLiteral(p) + res, err = parseStringLiteral(p) case lexer.Number: - return parseNumberLiteral(p) + res, err = parseNumberLiteral(p) case lexer.Symbol: - return parseSymbol(p) + res, err = parseSymbol(p) case lexer.OpenBracket: - return parseSquareBrackets(p) + res, err = parseSquareBrackets(p) case lexer.OpenCurly: - return parseObject(p) + res, err = parseObject(p) case lexer.Bool: - return parseBoolLiteral(p) + res, err = parseBoolLiteral(p) case lexer.Spread: - return parseSpread(p) + res, err = parseSpread(p) + case lexer.Variable: + res, err = parseVariable(p) default: - return nil, &UnexpectedTokenError{ + return nil, false, &UnexpectedTokenError{ Token: p.current(), } } + return } func (p *Parser) hasToken() bool { @@ -138,6 +155,14 @@ func (p *Parser) current() lexer.Token { return lexer.Token{Kind: lexer.EOF} } +func (p *Parser) previous() lexer.Token { + i := p.i - 1 + if i > 0 && i < len(p.tokens) { + return p.tokens[i] + } + return lexer.Token{Kind: lexer.EOF} +} + func (p *Parser) advance() lexer.Token { p.i++ return p.current() diff --git a/selector/parser/parser_binary.go b/selector/parser/parser_binary.go new file mode 100644 index 00000000..dde40bf7 --- /dev/null +++ b/selector/parser/parser_binary.go @@ -0,0 +1,30 @@ +package parser + +import "github.com/tomwright/dasel/v3/selector/ast" + +func parseBinary(p *Parser, left ast.Expr) (ast.Expr, bool, error) { + if err := p.expect(leftDenotationTokens...); err != nil { + return nil, false, err + } + for { + if !p.current().IsKind(leftDenotationTokens...) { + break + } + + token := p.current() + p.advance() + + right, _, err := p.parseExpression(left) + if err != nil { + return nil, false, err + } + + left = ast.BinaryExpr{ + Left: left, + Operator: token, + Right: right, + } + } + + return left, true, nil +} diff --git a/selector/parser/parser_test.go b/selector/parser/parser_test.go index 494130e1..e456ea86 100644 --- a/selector/parser/parser_test.go +++ b/selector/parser/parser_test.go @@ -291,4 +291,15 @@ func TestParser_Parse_HappyPath(t *testing.T) { }}, })) }) + + t.Run("variables", func(t *testing.T) { + t.Run("single variable", run(t, testCase{ + input: `$foo`, + expected: ast.VariableExpr{Name: "foo"}, + })) + t.Run("variable passed to func", run(t, testCase{ + input: `len($foo)`, + expected: ast.CallExpr{Function: "len", Args: ast.Expressions{ast.VariableExpr{Name: "foo"}}}, + })) + }) } From 5058ae6a71f5ea8d2ab4f973695484267b750310 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Tue, 1 Oct 2024 01:42:55 +0100 Subject: [PATCH 06/56] Get binary binding powers working --- execution/execute_test.go | 8 ++++- selector/parser/denotations.go | 58 ++++++++++++++++++++++++++++++++ selector/parser/parse_array.go | 6 ++-- selector/parser/parse_func.go | 2 +- selector/parser/parse_map.go | 9 +---- selector/parser/parse_object.go | 4 +-- selector/parser/parser.go | 46 ++++++++++++------------- selector/parser/parser_binary.go | 34 +++++++------------ 8 files changed, 107 insertions(+), 60 deletions(-) diff --git a/execution/execute_test.go b/execution/execute_test.go index 2e79c75e..9547a050 100644 --- a/execution/execute_test.go +++ b/execution/execute_test.go @@ -84,9 +84,15 @@ func TestExecuteSelector_HappyPath(t *testing.T) { })) t.Run("ordering", runTest(testCase{ in: model.NewValue(nil), - s: `45.2 + 5 * 4 - 2 / 2`, // 45.2 + (5 * 4) - (2 / 2) = 45.2 + 20 - 1 + s: `45.2 + 5 * 4 - 2 / 2`, // 45.2 + (5 * 4) - (2 / 2) = 45.2 + 20 - 1 = 64.2 out: model.NewFloatValue(64.2), })) + // todo : implement grouping + //t.Run("ordering with groups", runTest(testCase{ + // in: model.NewValue(nil), + // s: `(45.2 + 5) * ((4 - 2) / 2)`, // (45.2 + 5) * ((4 - 2) / 2) = (50.2) * ((2) / 2) = (50.2) * (1) = 50.2 + // out: model.NewFloatValue(50.2), + //})) }) }) t.Run("comparison", func(t *testing.T) { diff --git a/selector/parser/denotations.go b/selector/parser/denotations.go index 40589a0c..c976cf61 100644 --- a/selector/parser/denotations.go +++ b/selector/parser/denotations.go @@ -24,3 +24,61 @@ var leftDenotationTokens = []lexer.TokenKind{ var rightDenotationTokens = []lexer.TokenKind{ lexer.Exclamation, // Not operator } + +type bindingPower int + +const ( + bpDefault bindingPower = iota + bpAssignment + bpLogical + bpRelational + bpAdditive + bpMultiplicative + bpUnary + bpCall + bpProperty + bpLiteral +) + +var tokenBindingPowers = map[lexer.TokenKind]bindingPower{ + lexer.String: bpLiteral, + lexer.Number: bpLiteral, + lexer.Bool: bpLiteral, + //lexer.Null: bpLiteral, + + lexer.Variable: bpProperty, + lexer.Dot: bpProperty, + lexer.OpenBracket: bpProperty, + + lexer.OpenParen: bpCall, + + lexer.Exclamation: bpUnary, + + lexer.Star: bpMultiplicative, + lexer.Slash: bpMultiplicative, + lexer.Percent: bpMultiplicative, + + lexer.Plus: bpAdditive, + lexer.Dash: bpAdditive, + + lexer.Equal: bpRelational, + lexer.NotEqual: bpRelational, + lexer.GreaterThan: bpRelational, + lexer.GreaterThanOrEqual: bpRelational, + lexer.LessThan: bpRelational, + lexer.LessThanOrEqual: bpRelational, + + lexer.And: bpLogical, + lexer.Or: bpLogical, + lexer.Like: bpLogical, + lexer.NotLike: bpLogical, + + lexer.Equals: bpAssignment, +} + +func getTokenBindingPower(t lexer.TokenKind) bindingPower { + if bp, ok := tokenBindingPowers[t]; ok { + return bp + } + return bpDefault +} diff --git a/selector/parser/parse_array.go b/selector/parser/parse_array.go index f6f3f33b..1403bd1e 100644 --- a/selector/parser/parse_array.go +++ b/selector/parser/parse_array.go @@ -75,7 +75,7 @@ func parseSquareBrackets(p *Parser) (ast.Expr, error) { if p.current().IsKind(lexer.Colon) { p.advance() // We have no start index - end, _, err = p.parseExpression(nil) + end, err = p.parseExpression(bpDefault) if err != nil { return nil, err } @@ -85,7 +85,7 @@ func parseSquareBrackets(p *Parser) (ast.Expr, error) { }, nil } - start, _, err = p.parseExpression(nil) + start, err = p.parseExpression(bpDefault) if err != nil { return nil, err } @@ -112,7 +112,7 @@ func parseSquareBrackets(p *Parser) (ast.Expr, error) { }, nil } - end, _, err = p.parseExpression(nil) + end, err = p.parseExpression(bpDefault) if err != nil { return nil, err } diff --git a/selector/parser/parse_func.go b/selector/parser/parse_func.go index 22bf2ddd..523c7df8 100644 --- a/selector/parser/parse_func.go +++ b/selector/parser/parse_func.go @@ -37,7 +37,7 @@ func parseArgs(p *Parser) ([]ast.Expr, error) { break } - arg, _, err := p.parseExpression(nil) + arg, err := p.parseExpression(bpCall) if err != nil { return nil, err } diff --git a/selector/parser/parse_map.go b/selector/parser/parse_map.go index c73469dd..668fe273 100644 --- a/selector/parser/parse_map.go +++ b/selector/parser/parse_map.go @@ -26,9 +26,6 @@ func parseMap(p *Parser) (ast.Expr, error) { expressions := make([]ast.Expr, 0) - var expr ast.Expr - var err error - var replaceLast bool for { if p.current().IsKind(lexer.CloseParen) { if len(expressions) == 0 { @@ -43,14 +40,10 @@ func parseMap(p *Parser) (ast.Expr, error) { continue } - expr, replaceLast, err = p.parseExpression(expr) + expr, err := p.parseExpression(bpDefault) if err != nil { return nil, err } - if replaceLast { - expressions[len(expressions)-1] = expr - continue - } expressions = append(expressions, expr) } diff --git a/selector/parser/parse_object.go b/selector/parser/parse_object.go index 1afd203b..9b47cae3 100644 --- a/selector/parser/parse_object.go +++ b/selector/parser/parse_object.go @@ -51,7 +51,7 @@ func parseObject(p *Parser) (ast.Expr, error) { continue } - key, _, err := p.parseExpression(nil) + key, err := p.parseExpression(bpDefault) if err != nil { return nil, err } @@ -66,7 +66,7 @@ func parseObject(p *Parser) (ast.Expr, error) { } p.advance() - val, _, err := p.parseExpression(nil) + val, err := p.parseExpression(bpDefault) if err != nil { return nil, err } diff --git a/selector/parser/parser.go b/selector/parser/parser.go index b3d4dc4c..35ff05c9 100644 --- a/selector/parser/parser.go +++ b/selector/parser/parser.go @@ -72,9 +72,6 @@ func NewParser(tokens lexer.Tokens) *Parser { func (p *Parser) Parse() (ast.Expr, error) { var expressions ast.Expressions - var expr ast.Expr - var err error - var replaceLast bool for p.hasToken() { if p.current().IsKind(lexer.EOF) { break @@ -83,14 +80,10 @@ func (p *Parser) Parse() (ast.Expr, error) { p.advance() continue } - expr, replaceLast, err = p.parseExpression(expr) + expr, err := p.parseExpression(bpDefault) if err != nil { return nil, err } - if replaceLast { - expressions[len(expressions)-1] = expr - continue - } expressions = append(expressions, expr) } switch len(expressions) { @@ -103,40 +96,47 @@ func (p *Parser) Parse() (ast.Expr, error) { } } -func (p *Parser) parseExpression(last ast.Expr) (res ast.Expr, replaceLast bool, err error) { +func (p *Parser) parseExpression(bp bindingPower) (left ast.Expr, err error) { defer func() { if err == nil { err = p.expectEndOfExpression() } }() - if last != nil && slices.Contains(leftDenotationTokens, p.current().Kind) { - res, replaceLast, err = parseBinary(p, last) - return - } - switch p.current().Kind { case lexer.String: - res, err = parseStringLiteral(p) + left, err = parseStringLiteral(p) case lexer.Number: - res, err = parseNumberLiteral(p) + left, err = parseNumberLiteral(p) case lexer.Symbol: - res, err = parseSymbol(p) + left, err = parseSymbol(p) case lexer.OpenBracket: - res, err = parseSquareBrackets(p) + left, err = parseSquareBrackets(p) case lexer.OpenCurly: - res, err = parseObject(p) + left, err = parseObject(p) case lexer.Bool: - res, err = parseBoolLiteral(p) + left, err = parseBoolLiteral(p) case lexer.Spread: - res, err = parseSpread(p) + left, err = parseSpread(p) case lexer.Variable: - res, err = parseVariable(p) + left, err = parseVariable(p) default: - return nil, false, &UnexpectedTokenError{ + return nil, &UnexpectedTokenError{ Token: p.current(), } } + + if err != nil { + return + } + + for p.hasToken() && slices.Contains(leftDenotationTokens, p.current().Kind) && getTokenBindingPower(p.current().Kind) > bp { + left, err = parseBinary(p, left) + if err != nil { + return + } + } + return } diff --git a/selector/parser/parser_binary.go b/selector/parser/parser_binary.go index dde40bf7..e976ea1b 100644 --- a/selector/parser/parser_binary.go +++ b/selector/parser/parser_binary.go @@ -2,29 +2,19 @@ package parser import "github.com/tomwright/dasel/v3/selector/ast" -func parseBinary(p *Parser, left ast.Expr) (ast.Expr, bool, error) { +func parseBinary(p *Parser, left ast.Expr) (ast.Expr, error) { if err := p.expect(leftDenotationTokens...); err != nil { - return nil, false, err + return nil, err } - for { - if !p.current().IsKind(leftDenotationTokens...) { - break - } - - token := p.current() - p.advance() - - right, _, err := p.parseExpression(left) - if err != nil { - return nil, false, err - } - - left = ast.BinaryExpr{ - Left: left, - Operator: token, - Right: right, - } + operator := p.current() + p.advance() + right, err := p.parseExpression(getTokenBindingPower(operator.Kind)) + if err != nil { + return nil, err } - - return left, true, nil + return ast.BinaryExpr{ + Left: left, + Operator: operator, + Right: right, + }, nil } From b792aafcb296aa2adfca95546536d955fe5dccb7 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Tue, 1 Oct 2024 01:46:08 +0100 Subject: [PATCH 07/56] Add a null token --- selector/lexer/token.go | 1 + selector/lexer/tokenize.go | 3 +++ selector/lexer/tokenize_test.go | 33 +++------------------------------ selector/parser/denotations.go | 2 +- 4 files changed, 8 insertions(+), 31 deletions(-) diff --git a/selector/lexer/token.go b/selector/lexer/token.go index 2c9f3604..f86167c5 100644 --- a/selector/lexer/token.go +++ b/selector/lexer/token.go @@ -46,6 +46,7 @@ const ( LessThan LessThanOrEqual Exclamation + Null ) type Tokens []Token diff --git a/selector/lexer/tokenize.go b/selector/lexer/tokenize.go index 23d200a7..82c5eb72 100644 --- a/selector/lexer/tokenize.go +++ b/selector/lexer/tokenize.go @@ -172,6 +172,9 @@ func (p *Tokenizer) parseCurRune() (Token, error) { default: pos := p.i + if pos+3 < p.srcLen && strings.EqualFold(p.src[pos:pos+4], "null") { + return NewToken(Null, p.src[pos:pos+4], p.i, 4), nil + } if pos+3 < p.srcLen && strings.EqualFold(p.src[pos:pos+4], "true") { return NewToken(Bool, p.src[pos:pos+4], p.i, 4), nil } diff --git a/selector/lexer/tokenize_test.go b/selector/lexer/tokenize_test.go index 3d0401d9..add571a1 100644 --- a/selector/lexer/tokenize_test.go +++ b/selector/lexer/tokenize_test.go @@ -38,44 +38,17 @@ func TestTokenizer_Parse(t *testing.T) { })) t.Run("everything", runTest(testCase{ - in: "foo.bar.baz[1] != 42.123 || foo.bar.baz['hello'] == 42 && x == 'a\\'b' + false . .... asd... $name", + in: "foo.bar.baz[1] != 42.123 || foo.bar.baz['hello'] == 42 && x == 'a\\'b' + false true . .... asd... $name null", out: []TokenKind{ Symbol, Dot, Symbol, Dot, Symbol, OpenBracket, Number, CloseBracket, NotEqual, Number, Or, Symbol, Dot, Symbol, Dot, Symbol, OpenBracket, String, CloseBracket, Equal, Number, And, Symbol, Equal, String, - Plus, Bool, + Plus, Bool, Bool, Dot, Spread, Dot, Symbol, Spread, - Variable, + Variable, Null, }, })) - - tok := NewTokenizer("foo.bar.baz[1] != 42.123 || foo.bar.baz['hello'] == 42 && x == 'a\\'b' + false . .... asd... $name") - tokens, err := tok.Tokenize() - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - exp := []TokenKind{ - Symbol, Dot, Symbol, Dot, Symbol, OpenBracket, Number, CloseBracket, NotEqual, Number, - Or, - Symbol, Dot, Symbol, Dot, Symbol, OpenBracket, String, CloseBracket, Equal, Number, - And, - Symbol, Equal, String, - Plus, Bool, - Dot, Spread, Dot, - Symbol, Spread, - Variable, - } - if len(tokens) != len(exp) { - t.Fatalf("unexpected number of tokens: %d", len(tokens)) - } - - for i := range tokens { - if tokens[i].Kind != exp[i] { - t.Errorf("unexpected token kind at position %d: exp %v, got %v", i, exp[i], tokens[i].Kind) - return - } - } } diff --git a/selector/parser/denotations.go b/selector/parser/denotations.go index c976cf61..49eed609 100644 --- a/selector/parser/denotations.go +++ b/selector/parser/denotations.go @@ -44,7 +44,7 @@ var tokenBindingPowers = map[lexer.TokenKind]bindingPower{ lexer.String: bpLiteral, lexer.Number: bpLiteral, lexer.Bool: bpLiteral, - //lexer.Null: bpLiteral, + lexer.Null: bpLiteral, lexer.Variable: bpProperty, lexer.Dot: bpProperty, From 28cd36a129f1931c876add82499e2b105c3cb1a3 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Tue, 1 Oct 2024 01:54:02 +0100 Subject: [PATCH 08/56] Implement grouping --- execution/execute_test.go | 11 ++++---- selector/ast/expression_complex.go | 6 +++++ selector/lexer/token.go | 4 +-- selector/parser/parse_group.go | 41 ++++++++++++++++++++++++++++++ selector/parser/parser.go | 5 ++++ 5 files changed, 59 insertions(+), 8 deletions(-) create mode 100644 selector/parser/parse_group.go diff --git a/execution/execute_test.go b/execution/execute_test.go index 9547a050..08bef007 100644 --- a/execution/execute_test.go +++ b/execution/execute_test.go @@ -87,12 +87,11 @@ func TestExecuteSelector_HappyPath(t *testing.T) { s: `45.2 + 5 * 4 - 2 / 2`, // 45.2 + (5 * 4) - (2 / 2) = 45.2 + 20 - 1 = 64.2 out: model.NewFloatValue(64.2), })) - // todo : implement grouping - //t.Run("ordering with groups", runTest(testCase{ - // in: model.NewValue(nil), - // s: `(45.2 + 5) * ((4 - 2) / 2)`, // (45.2 + 5) * ((4 - 2) / 2) = (50.2) * ((2) / 2) = (50.2) * (1) = 50.2 - // out: model.NewFloatValue(50.2), - //})) + t.Run("ordering with groups", runTest(testCase{ + in: model.NewValue(nil), + s: `(45.2 + 5) * ((4 - 2) / 2)`, // (45.2 + 5) * ((4 - 2) / 2) = (50.2) * ((2) / 2) = (50.2) * (1) = 50.2 + out: model.NewFloatValue(50.2), + })) }) }) t.Run("comparison", func(t *testing.T) { diff --git a/selector/ast/expression_complex.go b/selector/ast/expression_complex.go index 6fd23040..eeb3c05e 100644 --- a/selector/ast/expression_complex.go +++ b/selector/ast/expression_complex.go @@ -89,3 +89,9 @@ type VariableExpr struct { } func (VariableExpr) expr() {} + +type GroupExpr struct { + Expr Expr +} + +func (GroupExpr) expr() {} diff --git a/selector/lexer/token.go b/selector/lexer/token.go index f86167c5..b018d143 100644 --- a/selector/lexer/token.go +++ b/selector/lexer/token.go @@ -12,8 +12,8 @@ const ( Symbol Comma Colon - OpenBracket - CloseBracket + OpenBracket // [ + CloseBracket // ] OpenCurly CloseCurly OpenParen diff --git a/selector/parser/parse_group.go b/selector/parser/parse_group.go new file mode 100644 index 00000000..354f9590 --- /dev/null +++ b/selector/parser/parse_group.go @@ -0,0 +1,41 @@ +package parser + +import ( + "fmt" + + "github.com/tomwright/dasel/v3/selector/ast" + "github.com/tomwright/dasel/v3/selector/lexer" +) + +func parseGroup(p *Parser) (ast.Expr, error) { + p.pushScope(scopeGroup) + defer p.popScope() + if err := p.expect(lexer.OpenParen); err != nil { + return nil, err + } + p.advance() // skip the open paren + + expressions := ast.Expressions{} + for { + if p.current().Kind == lexer.CloseParen { + break + } + + expr, err := p.parseExpression(bpDefault) + if err != nil { + return nil, err + } + expressions = append(expressions, expr) + } + + if err := p.expect(lexer.CloseParen); err != nil { + return nil, err + } + p.advance() // skip the close paren + + if len(expressions) == 0 { + return nil, fmt.Errorf("group expression must contain at least one expression") + } + + return ast.ChainExprs(expressions...), nil +} diff --git a/selector/parser/parser.go b/selector/parser/parser.go index 35ff05c9..48706afc 100644 --- a/selector/parser/parser.go +++ b/selector/parser/parser.go @@ -16,6 +16,7 @@ const ( scopeArray scope = "array" scopeObject scope = "object" scopeMap scope = "map" + scopeGroup scope = "group" ) type Parser struct { @@ -51,6 +52,8 @@ func (p *Parser) endOfExpressionTokens() []lexer.TokenKind { return []lexer.TokenKind{lexer.CloseBracket, lexer.Colon, lexer.Number, lexer.Symbol} case scopeObject: return []lexer.TokenKind{lexer.CloseCurly, lexer.Equals, lexer.Number, lexer.Symbol, lexer.Comma} + case scopeGroup: + return []lexer.TokenKind{lexer.CloseParen} default: return nil } @@ -120,6 +123,8 @@ func (p *Parser) parseExpression(bp bindingPower) (left ast.Expr, err error) { left, err = parseSpread(p) case lexer.Variable: left, err = parseVariable(p) + case lexer.OpenParen: + left, err = parseGroup(p) default: return nil, &UnexpectedTokenError{ Token: p.current(), From ff73190df5cd750221bcc4c090d37957578c5409 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Tue, 1 Oct 2024 02:04:48 +0100 Subject: [PATCH 09/56] Add string concat test --- execution/execute_test.go | 5 +++++ selector/parser/parse_group.go | 5 +++++ selector/parser/parser.go | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/execution/execute_test.go b/execution/execute_test.go index 08bef007..efbeea62 100644 --- a/execution/execute_test.go +++ b/execution/execute_test.go @@ -196,6 +196,11 @@ func TestExecuteSelector_HappyPath(t *testing.T) { s: `name.first`, out: model.NewStringValue("Tom"), })) + t.Run("concat", runTest(testCase{ + in: inputMap(), + s: `title + " " + (name.first) + " " + (name.last)`, + out: model.NewStringValue("Mr Tom Wright"), + })) }) t.Run("object", func(t *testing.T) { diff --git a/selector/parser/parse_group.go b/selector/parser/parse_group.go index 354f9590..5136e17a 100644 --- a/selector/parser/parse_group.go +++ b/selector/parser/parse_group.go @@ -21,6 +21,11 @@ func parseGroup(p *Parser) (ast.Expr, error) { break } + if p.current().IsKind(lexer.Dot) { + p.advance() + continue + } + expr, err := p.parseExpression(bpDefault) if err != nil { return nil, err diff --git a/selector/parser/parser.go b/selector/parser/parser.go index 48706afc..a50784c0 100644 --- a/selector/parser/parser.go +++ b/selector/parser/parser.go @@ -53,7 +53,7 @@ func (p *Parser) endOfExpressionTokens() []lexer.TokenKind { case scopeObject: return []lexer.TokenKind{lexer.CloseCurly, lexer.Equals, lexer.Number, lexer.Symbol, lexer.Comma} case scopeGroup: - return []lexer.TokenKind{lexer.CloseParen} + return append([]lexer.TokenKind{lexer.CloseParen, lexer.Dot}, leftDenotationTokens...) default: return nil } From 174ec83f7ae6561fc116f7a92f071a4c98c63abe Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Tue, 1 Oct 2024 02:16:40 +0100 Subject: [PATCH 10/56] Add a test for setting evaluated fields --- dencoding/map.go | 24 +++++++++++++++++++++ execution/execute_test.go | 44 +++++++++++++++++++-------------------- 2 files changed, 46 insertions(+), 22 deletions(-) diff --git a/dencoding/map.go b/dencoding/map.go index 69aa8fd2..22fb1996 100644 --- a/dencoding/map.go +++ b/dencoding/map.go @@ -1,5 +1,7 @@ package dencoding +import "reflect" + // NewMap returns a new *Map that has its values initialised. func NewMap() *Map { keys := make([]string, 0) @@ -29,6 +31,28 @@ type Map struct { data map[string]any } +func (m *Map) Len() int { + return len(m.keys) +} + +func (m *Map) Equal(other *Map) bool { + if m.Len() != other.Len() { + return false + } + + for i, k := range m.keys { + if k != other.keys[i] { + return false + } + + if !reflect.DeepEqual(m.data[k], other.data[k]) { + return false + } + } + + return true +} + // Get returns the value found under the given key. func (m *Map) Get(key string) (any, bool) { v, ok := m.data[key] diff --git a/execution/execute_test.go b/execution/execute_test.go index efbeea62..f25348fd 100644 --- a/execution/execute_test.go +++ b/execution/execute_test.go @@ -27,6 +27,9 @@ func TestExecuteSelector_HappyPath(t *testing.T) { if tc.inFn != nil { in = tc.inFn() } + if in == nil { + in = model.NewValue(nil) + } exp := tc.out if tc.outFn != nil { exp = tc.outFn() @@ -48,7 +51,9 @@ func TestExecuteSelector_HappyPath(t *testing.T) { } expV, gotV := toInterface(exp), toInterface(res) - if !cmp.Equal(expV, gotV, cmpopts.IgnoreUnexported(dencoding.Map{})) { + if !cmp.Equal(expV, gotV, + cmpopts.IgnoreUnexported(dencoding.Map{}), + ) { t.Errorf("unexpected result: %v", cmp.Diff(expV, gotV)) } } @@ -58,37 +63,30 @@ func TestExecuteSelector_HappyPath(t *testing.T) { t.Run("math", func(t *testing.T) { t.Run("literals", func(t *testing.T) { t.Run("addition", runTest(testCase{ - in: model.NewValue(nil), s: `1 + 2`, out: model.NewIntValue(3), })) t.Run("subtraction", runTest(testCase{ - in: model.NewValue(nil), s: `5 - 2`, out: model.NewIntValue(3), })) t.Run("multiplication", runTest(testCase{ - in: model.NewValue(nil), s: `5 * 2`, out: model.NewIntValue(10), })) t.Run("division", runTest(testCase{ - in: model.NewValue(nil), s: `10 / 2`, out: model.NewIntValue(5), })) t.Run("modulus", runTest(testCase{ - in: model.NewValue(nil), s: `10 % 3`, out: model.NewIntValue(1), })) t.Run("ordering", runTest(testCase{ - in: model.NewValue(nil), s: `45.2 + 5 * 4 - 2 / 2`, // 45.2 + (5 * 4) - (2 / 2) = 45.2 + 20 - 1 = 64.2 out: model.NewFloatValue(64.2), })) t.Run("ordering with groups", runTest(testCase{ - in: model.NewValue(nil), s: `(45.2 + 5) * ((4 - 2) / 2)`, // (45.2 + 5) * ((4 - 2) / 2) = (50.2) * ((2) / 2) = (50.2) * (1) = 50.2 out: model.NewFloatValue(50.2), })) @@ -96,32 +94,26 @@ func TestExecuteSelector_HappyPath(t *testing.T) { }) t.Run("comparison", func(t *testing.T) { t.Run("equal", runTest(testCase{ - in: model.NewValue(nil), s: `1 == 1`, out: model.NewBoolValue(true), })) t.Run("not equal", runTest(testCase{ - in: model.NewValue(nil), s: `1 != 1`, out: model.NewBoolValue(false), })) t.Run("greater than", runTest(testCase{ - in: model.NewValue(nil), s: `2 > 1`, out: model.NewBoolValue(true), })) t.Run("greater than or equal", runTest(testCase{ - in: model.NewValue(nil), s: `2 >= 2`, out: model.NewBoolValue(true), })) t.Run("less than", runTest(testCase{ - in: model.NewValue(nil), s: `1 < 2`, out: model.NewBoolValue(true), })) t.Run("less than or equal", runTest(testCase{ - in: model.NewValue(nil), s: `2 <= 2`, out: model.NewBoolValue(true), })) @@ -130,27 +122,22 @@ func TestExecuteSelector_HappyPath(t *testing.T) { t.Run("literal", func(t *testing.T) { t.Run("string", runTest(testCase{ - in: model.NewValue(nil), s: `"hello"`, out: model.NewStringValue("hello"), })) t.Run("int", runTest(testCase{ - in: model.NewValue(nil), s: `123`, out: model.NewIntValue(123), })) t.Run("float", runTest(testCase{ - in: model.NewValue(nil), s: `123.4`, out: model.NewFloatValue(123.4), })) t.Run("true", runTest(testCase{ - in: model.NewValue(nil), s: `true`, out: model.NewBoolValue(true), })) t.Run("false", runTest(testCase{ - in: model.NewValue(nil), s: `false`, out: model.NewBoolValue(false), })) @@ -159,17 +146,14 @@ func TestExecuteSelector_HappyPath(t *testing.T) { t.Run("function", func(t *testing.T) { t.Run("add", func(t *testing.T) { t.Run("int", runTest(testCase{ - in: model.NewValue(nil), s: `add(1, 2, 3)`, out: model.NewIntValue(6), })) t.Run("float", runTest(testCase{ - in: model.NewValue(nil), s: `add(1f, 2.5, 3.5)`, out: model.NewFloatValue(7), })) t.Run("mixed", runTest(testCase{ - in: model.NewValue(nil), s: `add(1, 2f)`, out: model.NewFloatValue(3), })) @@ -181,6 +165,7 @@ func TestExecuteSelector_HappyPath(t *testing.T) { return model.NewValue( dencoding.NewMap(). Set("title", "Mr"). + Set("age", int64(31)). Set("name", dencoding.NewMap(). Set("first", "Tom"). Set("last", "Wright")), @@ -201,6 +186,21 @@ func TestExecuteSelector_HappyPath(t *testing.T) { s: `title + " " + (name.first) + " " + (name.last)`, out: model.NewStringValue("Mr Tom Wright"), })) + t.Run("add evaluated fields", runTest(testCase{ + in: inputMap(), + s: `{..., over30 = age > 30}`, + outFn: func() *model.Value { + return model.NewValue( + dencoding.NewMap(). + Set("title", "Mr"). + Set("age", int64(31)). + Set("name", dencoding.NewMap(). + Set("first", "Tom"). + Set("last", "Wright")). + Set("over30", true), + ) + }, + })) }) t.Run("object", func(t *testing.T) { From bf9f18fa4ce21db1e3b32e7e808853e749192123 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Tue, 1 Oct 2024 21:19:41 +0100 Subject: [PATCH 11/56] More progress --- execution/README.md | 3 + execution/execute.go | 40 +------ execution/execute_func.go | 8 +- execution/execute_spread.go | 62 ++++++++++ execution/execute_test.go | 186 +++++++++++++++++++++++++---- model/README.md | 5 + model/value.go | 3 +- model/value_metadata.go | 32 +++++ selector/README.md | 3 + selector/ast/expression_complex.go | 3 + selector/parser/parse_func.go | 26 ++-- selector/parser/parse_group.go | 36 +----- selector/parser/parse_map.go | 29 ++--- selector/parser/parser.go | 83 +++++++++++-- selector/parser/parser_test.go | 29 ++++- 15 files changed, 398 insertions(+), 150 deletions(-) create mode 100644 execution/README.md create mode 100644 execution/execute_spread.go create mode 100644 model/README.md create mode 100644 model/value_metadata.go create mode 100644 selector/README.md diff --git a/execution/README.md b/execution/README.md new file mode 100644 index 00000000..590ea6d4 --- /dev/null +++ b/execution/README.md @@ -0,0 +1,3 @@ +# Execution + +The execution package accepts a `model.Value`, parses a selector and executes the resulting AST on the value. diff --git a/execution/execute.go b/execution/execute.go index 96856609..187cdb5b 100644 --- a/execution/execute.go +++ b/execution/execute.go @@ -4,18 +4,13 @@ import ( "fmt" "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/selector" "github.com/tomwright/dasel/v3/selector/ast" "github.com/tomwright/dasel/v3/selector/lexer" - "github.com/tomwright/dasel/v3/selector/parser" ) -func ExecuteSelector(selector string, value *model.Value) (*model.Value, error) { - tokens, err := lexer.NewTokenizer(selector).Tokenize() - if err != nil { - return nil, fmt.Errorf("error tokenizing selector: %w", err) - } - - expr, err := parser.NewParser(tokens).Parse() +func ExecuteSelector(selectorStr string, value *model.Value) (*model.Value, error) { + expr, err := selector.Parse(selectorStr) if err != nil { return nil, fmt.Errorf("error parsing selector: %w", err) } @@ -131,35 +126,6 @@ func chainedExprExecutor(e ast.ChainedExpr) (expressionExecutor, error) { }, nil } -func spreadExprExecutor() (expressionExecutor, error) { - return func(data *model.Value) (*model.Value, error) { - s := model.NewSliceValue() - - switch { - case data.IsSlice(): - v, err := data.SliceValue() - if err != nil { - return nil, fmt.Errorf("error getting slice value: %w", err) - } - for _, sv := range v { - s.Append(model.NewValue(sv)) - } - case data.IsMap(): - v, err := data.MapValue() - if err != nil { - return nil, fmt.Errorf("error getting map value: %w", err) - } - for _, kv := range v.KeyValues() { - s.Append(model.NewValue(kv.Value)) - } - default: - return nil, fmt.Errorf("cannot spread on type %s", data.Type()) - } - - return s, nil - }, nil -} - func rangeExprExecutor(e ast.RangeExpr) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { panic("not implemented") diff --git a/execution/execute_func.go b/execution/execute_func.go index 83415bbd..d4e22a8e 100644 --- a/execution/execute_func.go +++ b/execution/execute_func.go @@ -14,7 +14,13 @@ func prepareArgs(data *model.Value, argsE ast.Expressions) (model.Values, error) if err != nil { return nil, fmt.Errorf("error evaluating argument %d: %w", i, err) } - args = append(args, res) + + argVals, err := prepareSpreadValues(res) + if err != nil { + return nil, fmt.Errorf("error handling spread values: %w", err) + } + + args = append(args, argVals...) } return args, nil } diff --git a/execution/execute_spread.go b/execution/execute_spread.go new file mode 100644 index 00000000..6225a2b8 --- /dev/null +++ b/execution/execute_spread.go @@ -0,0 +1,62 @@ +package execution + +import ( + "fmt" + + "github.com/tomwright/dasel/v3/model" +) + +func spreadExprExecutor() (expressionExecutor, error) { + return func(data *model.Value) (*model.Value, error) { + s := model.NewSliceValue() + + s.MarkAsSpread() + + switch { + case data.IsSlice(): + v, err := data.SliceValue() + if err != nil { + return nil, fmt.Errorf("error getting slice value: %w", err) + } + for _, sv := range v { + if err := s.Append(model.NewValue(sv)); err != nil { + return nil, fmt.Errorf("error appending value to slice: %w", err) + } + } + case data.IsMap(): + v, err := data.MapValue() + if err != nil { + return nil, fmt.Errorf("error getting map value: %w", err) + } + for _, kv := range v.KeyValues() { + if err := s.Append(model.NewValue(kv.Value)); err != nil { + return nil, fmt.Errorf("error appending value to slice: %w", err) + } + } + default: + return nil, fmt.Errorf("cannot spread on type %s", data.Type()) + } + + return s, nil + }, nil +} + +// prepareSpreadValues looks at the incoming value, and if we detect a spread value, we return the individual values. +func prepareSpreadValues(val *model.Value) (model.Values, error) { + if val.IsSlice() && val.IsSpread() { + sliceLen, err := val.SliceLen() + if err != nil { + return nil, fmt.Errorf("error getting slice length: %w", err) + } + values := make(model.Values, sliceLen) + for i := 0; i < sliceLen; i++ { + v, err := val.GetSliceIndex(i) + if err != nil { + return nil, fmt.Errorf("error getting slice index %d: %w", i, err) + } + values[i] = v + } + return values, nil + } + return model.Values{val}, nil +} diff --git a/execution/execute_test.go b/execution/execute_test.go index f25348fd..422d6803 100644 --- a/execution/execute_test.go +++ b/execution/execute_test.go @@ -91,32 +91,136 @@ func TestExecuteSelector_HappyPath(t *testing.T) { out: model.NewFloatValue(50.2), })) }) + t.Run("variables", func(t *testing.T) { + in := func() *model.Value { + return model.NewValue(dencoding.NewMap(). + Set("one", 1). + Set("two", 2). + Set("three", 3). + Set("four", 4). + Set("five", 5). + Set("six", 6). + Set("seven", 7). + Set("eight", 8). + Set("nine", 9). + Set("ten", 10). + Set("fortyfivepoint2", 45.2)) + } + t.Run("addition", runTest(testCase{ + inFn: in, + s: `one + two`, + out: model.NewIntValue(3), + })) + t.Run("subtraction", runTest(testCase{ + inFn: in, + s: `five - two`, + out: model.NewIntValue(3), + })) + t.Run("multiplication", runTest(testCase{ + inFn: in, + s: `five * two`, + out: model.NewIntValue(10), + })) + t.Run("division", runTest(testCase{ + inFn: in, + s: `ten / two`, + out: model.NewIntValue(5), + })) + t.Run("modulus", runTest(testCase{ + inFn: in, + s: `ten % three`, + out: model.NewIntValue(1), + })) + t.Run("ordering", runTest(testCase{ + inFn: in, + s: `fortyfivepoint2 + five * four - two / two`, // 45.2 + (5 * 4) - (2 / 2) = 45.2 + 20 - 1 = 64.2 + out: model.NewFloatValue(64.2), + })) + t.Run("ordering with groups", runTest(testCase{ + inFn: in, + s: `(fortyfivepoint2 + five) * ((four - two) / two)`, // (45.2 + 5) * ((4 - 2) / 2) = (50.2) * ((2) / 2) = (50.2) * (1) = 50.2 + out: model.NewFloatValue(50.2), + })) + }) }) t.Run("comparison", func(t *testing.T) { - t.Run("equal", runTest(testCase{ - s: `1 == 1`, - out: model.NewBoolValue(true), - })) - t.Run("not equal", runTest(testCase{ - s: `1 != 1`, - out: model.NewBoolValue(false), - })) - t.Run("greater than", runTest(testCase{ - s: `2 > 1`, - out: model.NewBoolValue(true), - })) - t.Run("greater than or equal", runTest(testCase{ - s: `2 >= 2`, - out: model.NewBoolValue(true), - })) - t.Run("less than", runTest(testCase{ - s: `1 < 2`, - out: model.NewBoolValue(true), - })) - t.Run("less than or equal", runTest(testCase{ - s: `2 <= 2`, - out: model.NewBoolValue(true), - })) + t.Run("literals", func(t *testing.T) { + t.Run("equal", runTest(testCase{ + s: `1 == 1`, + out: model.NewBoolValue(true), + })) + t.Run("not equal", runTest(testCase{ + s: `1 != 1`, + out: model.NewBoolValue(false), + })) + t.Run("greater than", runTest(testCase{ + s: `2 > 1`, + out: model.NewBoolValue(true), + })) + t.Run("greater than or equal", runTest(testCase{ + s: `2 >= 2`, + out: model.NewBoolValue(true), + })) + t.Run("less than", runTest(testCase{ + s: `1 < 2`, + out: model.NewBoolValue(true), + })) + t.Run("less than or equal", runTest(testCase{ + s: `2 <= 2`, + out: model.NewBoolValue(true), + })) + }) + + t.Run("variables", func(t *testing.T) { + in := func() *model.Value { + return model.NewValue(dencoding.NewMap(). + Set("one", 1). + Set("two", 2). + Set("nested", dencoding.NewMap(). + Set("three", 3). + Set("four", 4))) + } + t.Run("equal", runTest(testCase{ + inFn: in, + s: `one == one`, + out: model.NewBoolValue(true), + })) + t.Run("not equal", runTest(testCase{ + inFn: in, + s: `one != one`, + out: model.NewBoolValue(false), + })) + t.Run("greater than", runTest(testCase{ + inFn: in, + s: `two > one`, + out: model.NewBoolValue(true), + })) + t.Run("greater than or equal", runTest(testCase{ + inFn: in, + s: `two >= two`, + out: model.NewBoolValue(true), + })) + t.Run("less than", runTest(testCase{ + inFn: in, + s: `one < two`, + out: model.NewBoolValue(true), + })) + t.Run("less than or equal", runTest(testCase{ + inFn: in, + s: `two <= two`, + out: model.NewBoolValue(true), + })) + t.Run("nested with math more than", runTest(testCase{ + inFn: in, + s: `nested.three + nested.four * 0 > one * 1`, + out: model.NewBoolValue(true), + })) + t.Run("nested with grouped math more than", runTest(testCase{ + inFn: in, + s: `(nested.three + nested.four) * 0 > one * 1`, + out: model.NewBoolValue(false), + })) + }) }) }) @@ -157,6 +261,31 @@ func TestExecuteSelector_HappyPath(t *testing.T) { s: `add(1, 2f)`, out: model.NewFloatValue(3), })) + t.Run("properties", func(t *testing.T) { + in := func() *model.Value { + return model.NewValue(dencoding.NewMap(). + Set("numbers", dencoding.NewMap(). + Set("one", 1). + Set("two", 2). + Set("three", 3)). + Set("nums", []any{1, 2, 3})) + } + t.Run("nested props", runTest(testCase{ + inFn: in, + s: `numbers.one + add(numbers.two, numbers.three)`, + out: model.NewIntValue(6), + })) + t.Run("add on end of chain", runTest(testCase{ + inFn: in, + s: `numbers.one + numbers.add(two, three)`, + out: model.NewIntValue(6), + })) + t.Run("add with map and spread on slice with $this addition", runTest(testCase{ + inFn: in, + s: `add(nums.map(($this + 1))...)`, + out: model.NewIntValue(9), + })) + }) }) }) @@ -181,11 +310,16 @@ func TestExecuteSelector_HappyPath(t *testing.T) { s: `name.first`, out: model.NewStringValue("Tom"), })) - t.Run("concat", runTest(testCase{ + t.Run("concat with grouping", runTest(testCase{ in: inputMap(), s: `title + " " + (name.first) + " " + (name.last)`, out: model.NewStringValue("Mr Tom Wright"), })) + t.Run("concat", runTest(testCase{ + in: inputMap(), + s: `title + " " + name.first + " " + name.last`, + out: model.NewStringValue("Mr Tom Wright"), + })) t.Run("add evaluated fields", runTest(testCase{ in: inputMap(), s: `{..., over30 = age > 30}`, @@ -284,7 +418,7 @@ func TestExecuteSelector_HappyPath(t *testing.T) { }) }, s: ` - .map ( + map ( { total = add( foo, bar, 1 ) } diff --git a/model/README.md b/model/README.md new file mode 100644 index 00000000..5b7917a0 --- /dev/null +++ b/model/README.md @@ -0,0 +1,5 @@ +# Model + +The model package contains the Value struct and functionality for the application. + +`model.Value` is just a wrapper around `reflect.Value` but provides some additional logic for easier use. diff --git a/model/value.go b/model/value.go index 5891dbba..3dc432bb 100644 --- a/model/value.go +++ b/model/value.go @@ -22,7 +22,8 @@ const ( ) type Value struct { - Value reflect.Value + Value reflect.Value + Metadata map[string]any } func NewValue(v any) *Value { diff --git a/model/value_metadata.go b/model/value_metadata.go new file mode 100644 index 00000000..e1193fe0 --- /dev/null +++ b/model/value_metadata.go @@ -0,0 +1,32 @@ +package model + +func (v *Value) MetadataValue(key string) (any, bool) { + if v.Metadata == nil { + return nil, false + } + val, ok := v.Metadata[key] + return val, ok +} + +func (v *Value) SetMetadataValue(key string, val any) { + if v.Metadata == nil { + v.Metadata = map[string]any{} + } + v.Metadata[key] = val +} + +func (v *Value) IsSpread() bool { + val, ok := v.Metadata["spread"] + if !ok { + return false + } + spread, ok := val.(bool) + if !ok { + return false + } + return spread +} + +func (v *Value) MarkAsSpread() { + v.SetMetadataValue("spread", true) +} diff --git a/selector/README.md b/selector/README.md new file mode 100644 index 00000000..4821462e --- /dev/null +++ b/selector/README.md @@ -0,0 +1,3 @@ +# Selector + +The selector package contains everything needed to parse a selector string into an AST, which we can then execute. diff --git a/selector/ast/expression_complex.go b/selector/ast/expression_complex.go index eeb3c05e..8c20947f 100644 --- a/selector/ast/expression_complex.go +++ b/selector/ast/expression_complex.go @@ -29,6 +29,9 @@ type ChainedExpr struct { } func ChainExprs(exprs ...Expr) Expr { + if len(exprs) == 0 { + return nil + } if len(exprs) == 1 { return exprs[0] } diff --git a/selector/parser/parse_func.go b/selector/parser/parse_func.go index 523c7df8..59b48e9d 100644 --- a/selector/parser/parse_func.go +++ b/selector/parser/parse_func.go @@ -29,23 +29,11 @@ func parseFunc(p *Parser) (ast.Expr, error) { }, nil } -func parseArgs(p *Parser) ([]ast.Expr, error) { - args := make([]ast.Expr, 0) - for p.hasToken() { - if p.current().IsKind(lexer.CloseParen) { - p.advance() - break - } - - arg, err := p.parseExpression(bpCall) - if err != nil { - return nil, err - } - args = append(args, arg) - - if p.current().IsKind(lexer.Comma) { - p.advance() - } - } - return args, nil +func parseArgs(p *Parser) (ast.Expressions, error) { + return p.parseExpressionsAsSlice( + []lexer.TokenKind{lexer.CloseParen}, + []lexer.TokenKind{lexer.Comma}, + false, + bpCall, + ) } diff --git a/selector/parser/parse_group.go b/selector/parser/parse_group.go index 5136e17a..9181f3e2 100644 --- a/selector/parser/parse_group.go +++ b/selector/parser/parse_group.go @@ -1,8 +1,6 @@ package parser import ( - "fmt" - "github.com/tomwright/dasel/v3/selector/ast" "github.com/tomwright/dasel/v3/selector/lexer" ) @@ -15,32 +13,10 @@ func parseGroup(p *Parser) (ast.Expr, error) { } p.advance() // skip the open paren - expressions := ast.Expressions{} - for { - if p.current().Kind == lexer.CloseParen { - break - } - - if p.current().IsKind(lexer.Dot) { - p.advance() - continue - } - - expr, err := p.parseExpression(bpDefault) - if err != nil { - return nil, err - } - expressions = append(expressions, expr) - } - - if err := p.expect(lexer.CloseParen); err != nil { - return nil, err - } - p.advance() // skip the close paren - - if len(expressions) == 0 { - return nil, fmt.Errorf("group expression must contain at least one expression") - } - - return ast.ChainExprs(expressions...), nil + return p.parseExpressions( + []lexer.TokenKind{lexer.CloseParen}, + []lexer.TokenKind{}, + true, + bpDefault, + ) } diff --git a/selector/parser/parse_map.go b/selector/parser/parse_map.go index 668fe273..3e69e6fc 100644 --- a/selector/parser/parse_map.go +++ b/selector/parser/parse_map.go @@ -24,27 +24,14 @@ func parseMap(p *Parser) (ast.Expr, error) { } p.advance() - expressions := make([]ast.Expr, 0) - - for { - if p.current().IsKind(lexer.CloseParen) { - if len(expressions) == 0 { - return nil, fmt.Errorf("expected at least one expression in map") - } - p.advance() - break - } - - if p.current().IsKind(lexer.Dot) { - p.advance() - continue - } - - expr, err := p.parseExpression(bpDefault) - if err != nil { - return nil, err - } - expressions = append(expressions, expr) + expressions, err := p.parseExpressionsAsSlice( + []lexer.TokenKind{lexer.CloseParen}, + []lexer.TokenKind{}, + true, + bpCall, + ) + if err != nil { + return nil, err } return ast.MapExpr{ diff --git a/selector/parser/parser.go b/selector/parser/parser.go index a50784c0..49eff2d5 100644 --- a/selector/parser/parser.go +++ b/selector/parser/parser.go @@ -47,9 +47,9 @@ func (p *Parser) endOfExpressionTokens() []lexer.TokenKind { case scopeFuncArgs: return []lexer.TokenKind{lexer.Comma, lexer.CloseParen} case scopeMap: - return []lexer.TokenKind{lexer.Comma, lexer.CloseParen, lexer.Dot} + return []lexer.TokenKind{lexer.Comma, lexer.CloseParen, lexer.Dot, lexer.Spread} case scopeArray: - return []lexer.TokenKind{lexer.CloseBracket, lexer.Colon, lexer.Number, lexer.Symbol} + return []lexer.TokenKind{lexer.CloseBracket, lexer.Colon, lexer.Number, lexer.Symbol, lexer.Spread} case scopeObject: return []lexer.TokenKind{lexer.CloseCurly, lexer.Equals, lexer.Number, lexer.Symbol, lexer.Comma} case scopeGroup: @@ -73,32 +73,68 @@ func NewParser(tokens lexer.Tokens) *Parser { } } -func (p *Parser) Parse() (ast.Expr, error) { - var expressions ast.Expressions +func (p *Parser) parseExpressionsAsSlice( + breakOn []lexer.TokenKind, + splitOn []lexer.TokenKind, + requireExpressions bool, + bp bindingPower, +) (ast.Expressions, error) { + var finalExpr ast.Expressions + var current ast.Expressions for p.hasToken() { - if p.current().IsKind(lexer.EOF) { + if p.current().IsKind(breakOn...) { + p.advance() break } - if p.current().IsKind(lexer.Dot) { + if p.current().IsKind(splitOn...) { + if requireExpressions && len(current) == 0 { + return nil, &UnexpectedTokenError{Token: p.current()} + } + finalExpr = append(finalExpr, ast.ChainExprs(current...)) + current = nil p.advance() continue } - expr, err := p.parseExpression(bpDefault) + expr, err := p.parseExpression(bp) if err != nil { return nil, err } - expressions = append(expressions, expr) + current = append(current, expr) + } + + if len(current) > 0 { + finalExpr = append(finalExpr, ast.ChainExprs(current...)) + } + + if len(finalExpr) == 0 { + return nil, nil + } + + return finalExpr, nil +} + +func (p *Parser) parseExpressions( + breakOn []lexer.TokenKind, + splitOn []lexer.TokenKind, + requireExpressions bool, + bp bindingPower, +) (ast.Expr, error) { + expressions, err := p.parseExpressionsAsSlice(breakOn, splitOn, requireExpressions, bp) + if err != nil { + return nil, err } switch len(expressions) { case 0: return nil, nil - case 1: - return expressions[0], nil default: return ast.ChainExprs(expressions...), nil } } +func (p *Parser) Parse() (ast.Expr, error) { + return p.parseExpressions([]lexer.TokenKind{lexer.EOF}, nil, true, bpDefault) +} + func (p *Parser) parseExpression(bp bindingPower) (left ast.Expr, err error) { defer func() { if err == nil { @@ -135,6 +171,33 @@ func (p *Parser) parseExpression(bp bindingPower) (left ast.Expr, err error) { return } + toChain := ast.Expressions{left} + // Ensure dot separated chains are parsed as a sequence of expressions + if p.hasToken() && p.current().IsKind(lexer.Dot) { + for p.hasToken() && p.current().IsKind(lexer.Dot) { + p.advance() + expr, err := p.parseExpression(bpUnary) + if err != nil { + return nil, err + } + toChain = append(toChain, expr) + } + } + + // Handle spread + if p.hasToken() && p.current().IsKind(lexer.Spread) { + expr, err := p.parseExpression(bpLiteral) + if err != nil { + return nil, err + } + toChain = append(toChain, expr) + } + + if len(toChain) > 1 { + left = ast.ChainExprs(toChain...) + } + + // Handle binding powers for p.hasToken() && slices.Contains(leftDenotationTokens, p.current().Kind) && getTokenBindingPower(p.current().Kind) > bp { left, err = parseBinary(p, left) if err != nil { diff --git a/selector/parser/parser_test.go b/selector/parser/parser_test.go index e456ea86..a161eccc 100644 --- a/selector/parser/parser_test.go +++ b/selector/parser/parser_test.go @@ -123,7 +123,6 @@ func TestParser_Parse_HappyPath(t *testing.T) { }, End: ast.CallExpr{ Function: "calcEnd", - Args: ast.Expressions{}, }, }, })) @@ -173,7 +172,6 @@ func TestParser_Parse_HappyPath(t *testing.T) { }, End: ast.CallExpr{ Function: "calcEnd", - Args: ast.Expressions{}, }, }, ), @@ -237,8 +235,10 @@ func TestParser_Parse_HappyPath(t *testing.T) { ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.MapExpr{ Exprs: ast.Expressions{ - ast.PropertyExpr{Property: ast.StringExpr{Value: "x"}}, - ast.PropertyExpr{Property: ast.StringExpr{Value: "y"}}, + ast.ChainExprs( + ast.PropertyExpr{Property: ast.StringExpr{Value: "x"}}, + ast.PropertyExpr{Property: ast.StringExpr{Value: "y"}}, + ), }, }, ), @@ -286,7 +286,7 @@ func TestParser_Parse_HappyPath(t *testing.T) { {Key: ast.SpreadExpr{}, Value: ast.SpreadExpr{}}, {Key: ast.StringExpr{Value: "foo"}, Value: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}}, {Key: ast.StringExpr{Value: "bar"}, Value: ast.NumberIntExpr{Value: 2}}, - {Key: ast.StringExpr{Value: "baz"}, Value: ast.CallExpr{Function: "evalSomething", Args: ast.Expressions{}}}, + {Key: ast.StringExpr{Value: "baz"}, Value: ast.CallExpr{Function: "evalSomething"}}, {Key: ast.StringExpr{Value: "Name"}, Value: ast.StringExpr{Value: "Tom"}}, }}, })) @@ -302,4 +302,23 @@ func TestParser_Parse_HappyPath(t *testing.T) { expected: ast.CallExpr{Function: "len", Args: ast.Expressions{ast.VariableExpr{Name: "foo"}}}, })) }) + + t.Run("combinations and grouping", func(t *testing.T) { + t.Run("string concat with grouping", run(t, testCase{ + input: `(foo.a) + (foo.b)`, + expected: ast.BinaryExpr{ + Left: ast.ChainExprs(ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.PropertyExpr{Property: ast.StringExpr{Value: "a"}}), + Operator: lexer.Token{Kind: lexer.Plus, Value: "+", Pos: 8, Len: 1}, + Right: ast.ChainExprs(ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.PropertyExpr{Property: ast.StringExpr{Value: "b"}}), + }, + })) + t.Run("string concat with nested properties", run(t, testCase{ + input: `foo.a + foo.b`, + expected: ast.BinaryExpr{ + Left: ast.ChainExprs(ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.PropertyExpr{Property: ast.StringExpr{Value: "a"}}), + Operator: lexer.Token{Kind: lexer.Plus, Value: "+", Pos: 6, Len: 1}, + Right: ast.ChainExprs(ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.PropertyExpr{Property: ast.StringExpr{Value: "b"}}), + }, + })) + }) } From 482c6e042fd2c59fb0d14c62fb350e1f9efcacd5 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Wed, 2 Oct 2024 19:38:45 +0100 Subject: [PATCH 12/56] More setup --- execution/execute.go | 8 ++ execution/execute_object.go | 16 ++-- execution/execute_spread.go | 13 ++- execution/execute_test.go | 20 +---- internal/cli/generic_test.go | 106 ++++++++++++++++++++++ internal/cli/man.go | 30 +++++++ internal/cli/man_test.go | 43 +++++++++ internal/cli/root.go | 70 +++++++++++++++ internal/cli/root_test.go | 60 +++++++++++++ model/value.go | 41 +++++++++ model/value_comparison.go | 55 ++++++++++++ model/value_map.go | 169 ++++++++++++++++++++++++++++++----- model/value_map_test.go | 154 +++++++++++++++++++++++++++++++ model/value_slice.go | 36 ++++++++ parsing/format.go | 53 +++++++++++ parsing/json.go | 31 +++++++ parsing/toml.go | 23 +++++ parsing/yaml.go | 23 +++++ 18 files changed, 896 insertions(+), 55 deletions(-) create mode 100644 internal/cli/generic_test.go create mode 100644 internal/cli/man.go create mode 100644 internal/cli/man_test.go create mode 100644 internal/cli/root.go create mode 100644 internal/cli/root_test.go create mode 100644 model/value_map_test.go create mode 100644 parsing/format.go create mode 100644 parsing/json.go create mode 100644 parsing/toml.go create mode 100644 parsing/yaml.go diff --git a/execution/execute.go b/execution/execute.go index 187cdb5b..c53e2499 100644 --- a/execution/execute.go +++ b/execution/execute.go @@ -10,6 +10,10 @@ import ( ) func ExecuteSelector(selectorStr string, value *model.Value) (*model.Value, error) { + if selectorStr == "" { + return value, nil + } + expr, err := selector.Parse(selectorStr) if err != nil { return nil, fmt.Errorf("error parsing selector: %w", err) @@ -26,6 +30,10 @@ func ExecuteSelector(selectorStr string, value *model.Value) (*model.Value, erro type expressionExecutor func(data *model.Value) (*model.Value, error) func ExecuteAST(expr ast.Expr, value *model.Value) (*model.Value, error) { + if expr == nil { + return value, nil + } + executor, err := exprExecutor(expr) if err != nil { return nil, fmt.Errorf("error evaluating expression: %w", err) diff --git a/execution/execute_object.go b/execution/execute_object.go index ed350ae8..3bafc655 100644 --- a/execution/execute_object.go +++ b/execution/execute_object.go @@ -12,17 +12,13 @@ func objectExprExecutor(e ast.ObjectExpr) (expressionExecutor, error) { obj := model.NewMapValue() for _, p := range e.Pairs { if ast.IsSpreadExpr(p.Key) && ast.IsSpreadExpr(p.Value) { - if !data.IsMap() { - return nil, fmt.Errorf("cannot spread non-object into object") - } - m, err := data.MapValue() - if err != nil { - return nil, fmt.Errorf("error getting map value: %w", err) - } - for _, kv := range m.KeyValues() { - if err := obj.SetMapKey(kv.Key, model.NewValue(kv.Value)); err != nil { - return nil, fmt.Errorf("error setting map key: %w", err) + if err := data.RangeMap(func(key string, value *model.Value) error { + if err := obj.SetMapKey(key, value); err != nil { + return fmt.Errorf("error setting map key: %w", err) } + return nil + }); err != nil { + return nil, fmt.Errorf("error ranging map: %w", err) } continue } diff --git a/execution/execute_spread.go b/execution/execute_spread.go index 6225a2b8..dbe221ab 100644 --- a/execution/execute_spread.go +++ b/execution/execute_spread.go @@ -24,14 +24,13 @@ func spreadExprExecutor() (expressionExecutor, error) { } } case data.IsMap(): - v, err := data.MapValue() - if err != nil { - return nil, fmt.Errorf("error getting map value: %w", err) - } - for _, kv := range v.KeyValues() { - if err := s.Append(model.NewValue(kv.Value)); err != nil { - return nil, fmt.Errorf("error appending value to slice: %w", err) + if err := data.RangeMap(func(key string, value *model.Value) error { + if err := s.Append(value); err != nil { + return fmt.Errorf("error appending value to slice: %w", err) } + return nil + }); err != nil { + return nil, fmt.Errorf("error ranging map: %w", err) } default: return nil, fmt.Errorf("cannot spread on type %s", data.Type()) diff --git a/execution/execute_test.go b/execution/execute_test.go index 422d6803..c2691f8c 100644 --- a/execution/execute_test.go +++ b/execution/execute_test.go @@ -1,11 +1,9 @@ package execution_test import ( - "reflect" "testing" "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" "github.com/tomwright/dasel/v3/dencoding" "github.com/tomwright/dasel/v3/execution" "github.com/tomwright/dasel/v3/model" @@ -39,22 +37,8 @@ func TestExecuteSelector_HappyPath(t *testing.T) { t.Fatal(err) } - toInterface := func(v *model.Value) interface{} { - if v == nil { - return nil - } - if v.IsMap() { - m, _ := v.MapValue() - return m.KeyValues() - } - return v.UnpackKinds(reflect.Ptr).Interface() - } - expV, gotV := toInterface(exp), toInterface(res) - - if !cmp.Equal(expV, gotV, - cmpopts.IgnoreUnexported(dencoding.Map{}), - ) { - t.Errorf("unexpected result: %v", cmp.Diff(expV, gotV)) + if !res.EqualTypeValue(exp) { + t.Errorf("unexpected type: %v", cmp.Diff(exp, res)) } } } diff --git a/internal/cli/generic_test.go b/internal/cli/generic_test.go new file mode 100644 index 00000000..37a8cc53 --- /dev/null +++ b/internal/cli/generic_test.go @@ -0,0 +1,106 @@ +package cli_test + +import ( + "testing" + + "github.com/tomwright/dasel/v3/parsing" +) + +type inputProvider interface { + Format() parsing.Format + UserObject() []byte + ListOfNumbers() []byte + ListOfStrings() []byte + UserName() []byte +} + +type jsonInputProvider struct{} + +func (j jsonInputProvider) Format() parsing.Format { + return parsing.JSON +} + +func (j jsonInputProvider) UserObject() []byte { + return []byte(`{"name":"Tom"}`) +} + +func (j jsonInputProvider) ListOfNumbers() []byte { + return []byte(`[1,2,3]`) +} + +func (j jsonInputProvider) ListOfStrings() []byte { + return []byte(`["a","b","c"]`) +} + +func (j jsonInputProvider) UserName() []byte { + return []byte(`"Tom"`) +} + +type yamlInputProvider struct{} + +func (y yamlInputProvider) Format() parsing.Format { + return parsing.YAML +} + +func (y yamlInputProvider) UserObject() []byte { + return []byte(`name: Tom`) +} + +func (y yamlInputProvider) ListOfNumbers() []byte { + return []byte(`- 1 +- 2 +- 3`) +} + +func (y yamlInputProvider) ListOfStrings() []byte { + return []byte(`- a +- b +- c`) +} + +func (y yamlInputProvider) UserName() []byte { + return []byte(`Tom`) +} + +type tomlInputProvider struct{} + +func (t tomlInputProvider) Format() parsing.Format { + return parsing.TOML +} + +func (t tomlInputProvider) UserObject() []byte { + return []byte(`name = "Tom"`) +} + +func (t tomlInputProvider) ListOfNumbers() []byte { + return []byte(`[1, 2, 3]`) +} + +func (t tomlInputProvider) ListOfStrings() []byte { + return []byte(`["a", "b", "c"]`) +} + +func (t tomlInputProvider) UserName() []byte { + return []byte(`Tom`) +} + +func TestGeneric(t *testing.T) { + t.Run("json", runGenericTests(jsonInputProvider{})) + //t.Run("yaml", runGenericTests(yamlInputProvider{})) + //t.Run("toml", runGenericTests(tomlInputProvider{})) +} + +func runGenericTests(i inputProvider) func(*testing.T) { + return func(t *testing.T) { + t.Run("root", runTest(testCase{ + args: []string{"-i", i.Format().String(), ``}, + in: i.UserObject(), + stdout: i.UserObject(), + })) + t.Run("top level string", runTest(testCase{ + args: []string{"-i", i.Format().String(), `name`}, + in: i.UserObject(), + stdout: i.UserName(), + })) + } +} diff --git a/internal/cli/man.go b/internal/cli/man.go new file mode 100644 index 00000000..980aaeb1 --- /dev/null +++ b/internal/cli/man.go @@ -0,0 +1,30 @@ +package cli + +import ( + "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" +) + +func manCommand(root *cobra.Command) *cobra.Command { + // Do not include timestamp in generated man pages. + // See https://github.com/spf13/cobra/issues/142 + root.DisableAutoGenTag = true + + cmd := &cobra.Command{ + Use: "man -o ", + Short: "Generate manual pages for all dasel subcommands", + RunE: func(cmd *cobra.Command, args []string) error { + return manRunE(cmd, root) + }, + } + + cmd.Flags().StringP("output-directory", "o", ".", "The directory in which man pages will be created") + + return cmd +} + +func manRunE(cmd *cobra.Command, root *cobra.Command) error { + outputDirectory, _ := cmd.Flags().GetString("output-directory") + + return doc.GenManTree(root, nil, outputDirectory) +} diff --git a/internal/cli/man_test.go b/internal/cli/man_test.go new file mode 100644 index 00000000..1775a53b --- /dev/null +++ b/internal/cli/man_test.go @@ -0,0 +1,43 @@ +package cli_test + +import ( + "os" + "testing" +) + +func TestManCommand(t *testing.T) { + tempDir := t.TempDir() + + _, _, err := runDasel([]string{"man", "-o", tempDir}, nil) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + files, err := os.ReadDir(tempDir) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expectedFiles := []string{ + "dasel-completion-bash.1", + "dasel-completion-fish.1", + "dasel-completion-powershell.1", + "dasel-completion-zsh.1", + "dasel-completion.1", + //"dasel-delete.1", + "dasel-man.1", + //"dasel-put.1", + //"dasel-validate.1", + "dasel.1", + } + + if len(files) != len(expectedFiles) { + t.Fatalf("expected %d files, got %d", len(expectedFiles), len(files)) + } + + for i, f := range files { + if f.Name() != expectedFiles[i] { + t.Fatalf("expected %v, got %v", expectedFiles[i], f.Name()) + } + } +} diff --git a/internal/cli/root.go b/internal/cli/root.go new file mode 100644 index 00000000..147c9cfb --- /dev/null +++ b/internal/cli/root.go @@ -0,0 +1,70 @@ +package cli + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" + "github.com/tomwright/dasel/v3/execution" + "github.com/tomwright/dasel/v3/internal" + "github.com/tomwright/dasel/v3/parsing" +) + +func RootCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "dasel", + Short: "Query and modify data structures using selectors", + Long: `dasel is a command-line utility to query and modify data structures using selectors.`, + Version: internal.Version, + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + selectorStr := "" + if len(args) > 0 { + selectorStr = args[0] + } + + readerStr, _ := cmd.Flags().GetString("input") + writerStr, _ := cmd.Flags().GetString("output") + if writerStr == "" { + writerStr = readerStr + } + + reader, err := parsing.NewReader(parsing.Format(readerStr)) + writer, err := parsing.NewWriter(parsing.Format(writerStr)) + + inputBytes, err := io.ReadAll(cmd.InOrStdin()) + if err != nil { + return fmt.Errorf("error reading input: %w", err) + } + + inputData, err := reader.Read(inputBytes) + if err != nil { + return fmt.Errorf("error reading input: %w", err) + } + + outputData, err := execution.ExecuteSelector(selectorStr, inputData) + if err != nil { + return err + } + + outputBytes, err := writer.Write(outputData) + if err != nil { + return fmt.Errorf("error writing output: %w", err) + } + + _, err = cmd.OutOrStdout().Write(outputBytes) + if err != nil { + return fmt.Errorf("error writing output: %w", err) + } + + return nil + }, + } + + cmd.Flags().StringP("input", "i", "json", "The format of the input data. Can be one of: json, yaml, toml, xml, csv") + cmd.Flags().StringP("output", "o", "json", "The format of the output data. Can be one of: json, yaml, toml, xml, csv") + + cmd.AddCommand(manCommand(cmd)) + + return cmd +} diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go new file mode 100644 index 00000000..5f851111 --- /dev/null +++ b/internal/cli/root_test.go @@ -0,0 +1,60 @@ +package cli_test + +import ( + "bytes" + "errors" + "reflect" + "testing" + + "github.com/tomwright/dasel/v3/internal/cli" +) + +func runDasel(args []string, in []byte) ([]byte, []byte, error) { + stdOut := bytes.NewBuffer([]byte{}) + stdErr := bytes.NewBuffer([]byte{}) + + cmd := cli.RootCmd() + cmd.SetArgs(args) + cmd.SetOut(stdOut) + cmd.SetErr(stdErr) + + if in != nil { + cmd.SetIn(bytes.NewReader(in)) + } + + err := cmd.Execute() + return stdOut.Bytes(), stdErr.Bytes(), err +} + +type testCase struct { + args []string + in []byte + stdout []byte + stderr []byte + err error +} + +func runTest(tc testCase) func(t *testing.T) { + return func(t *testing.T) { + if tc.stdout == nil { + tc.stdout = []byte{} + } + if tc.stderr == nil { + tc.stderr = []byte{} + } + + gotStdOut, gotStdErr, gotErr := runDasel(tc.args, tc.in) + if !errors.Is(gotErr, tc.err) && !errors.Is(tc.err, gotErr) { + t.Errorf("expected error %v, got %v", tc.err, gotErr) + return + } + + if !reflect.DeepEqual(tc.stderr, gotStdErr) { + t.Errorf("expected stderr %s, got %s", string(tc.stderr), string(gotStdErr)) + } + + if !reflect.DeepEqual(tc.stdout, gotStdOut) { + t.Errorf("expected stdout %s, got %s", string(tc.stdout), string(gotStdOut)) + } + } +} diff --git a/model/value.go b/model/value.go index 3dc432bb..1a4ff5e0 100644 --- a/model/value.go +++ b/model/value.go @@ -1,6 +1,7 @@ package model import ( + "fmt" "reflect" "slices" ) @@ -21,12 +22,20 @@ const ( TypeUnknown Type = "unknown" ) +type KeyValue struct { + Key string + Value *Value +} + type Value struct { Value reflect.Value Metadata map[string]any } func NewValue(v any) *Value { + if v, ok := v.(*Value); ok { + return v + } if rv, ok := v.(reflect.Value); ok { return &Value{ Value: rv, @@ -41,6 +50,10 @@ func (v *Value) Interface() interface{} { return v.Value.Interface() } +func (v *Value) Kind() reflect.Kind { + return v.Value.Kind() +} + func (v *Value) UnpackKinds(kinds ...reflect.Kind) *Value { res := v.Value for { @@ -51,6 +64,34 @@ func (v *Value) UnpackKinds(kinds ...reflect.Kind) *Value { } } +func (v *Value) UnpackUntilType(t reflect.Type) (*Value, error) { + res := v.Value + for { + if res.Type() == t { + return NewValue(res), nil + } + if res.Kind() == reflect.Interface || res.Kind() == reflect.Ptr { + res = res.Elem() + continue + } + return nil, fmt.Errorf("could not unpack to type: %s", t) + } +} + +func (v *Value) UnpackUntilKind(k reflect.Kind) (*Value, error) { + res := v.Value + for { + if res.Kind() == k { + return NewValue(res), nil + } + if res.Kind() == reflect.Interface || res.Kind() == reflect.Ptr { + res = res.Elem() + continue + } + return nil, fmt.Errorf("could not unpack to kind: %s", k) + } +} + func (v *Value) Type() Type { switch { case v.IsString(): diff --git a/model/value_comparison.go b/model/value_comparison.go index c0cc5977..6b7b61c0 100644 --- a/model/value_comparison.go +++ b/model/value_comparison.go @@ -151,3 +151,58 @@ func (v *Value) GreaterThanOrEqual(other *Value) (*Value, error) { } return NewValue(!boolValue), nil } + +func (v *Value) EqualTypeValue(other *Value) bool { + if v.Type() != other.Type() { + return false + } + + switch v.Type() { + case TypeString: + a, _ := v.StringValue() + b, _ := other.StringValue() + return a == b + case TypeInt: + a, _ := v.IntValue() + b, _ := other.IntValue() + return a == b + case TypeFloat: + a, _ := v.FloatValue() + b, _ := other.FloatValue() + return a == b + case TypeBool: + a, _ := v.BoolValue() + b, _ := other.BoolValue() + return a == b + case TypeMap: + a, _ := v.MapKeys() + b, _ := other.MapKeys() + if len(a) != len(b) { + return false + } + for _, key := range a { + valA, _ := v.GetMapKey(key) + valB, _ := other.GetMapKey(key) + if !valA.EqualTypeValue(valB) { + return false + } + } + return true + case TypeSlice: + a, _ := v.SliceLen() + b, _ := other.SliceLen() + if a != b { + return false + } + for i := 0; i < a; i++ { + valA, _ := v.GetSliceIndex(i) + valB, _ := other.GetSliceIndex(i) + if !valA.EqualTypeValue(valB) { + return false + } + } + return true + default: + return false + } +} diff --git a/model/value_map.go b/model/value_map.go index 9ef38695..485a8d16 100644 --- a/model/value_map.go +++ b/model/value_map.go @@ -11,40 +11,169 @@ func NewMapValue() *Value { return NewValue(dencoding.NewMap()) } -func (v *Value) MapValue() (*dencoding.Map, error) { - if !v.IsMap() { +func (v *Value) IsMap() bool { + return v.isStandardMap() || v.isDencodingMap() +} + +func (v *Value) isStandardMap() bool { + return v.UnpackKinds(reflect.Interface, reflect.Ptr).Kind() == reflect.Map +} + +func (v *Value) isDencodingMap() bool { + return v.UnpackKinds(reflect.Interface, reflect.Ptr).Value.Type() == reflect.TypeFor[dencoding.Map]() +} + +func (v *Value) dencodingMapValue() (*dencoding.Map, error) { + if v.isDencodingMap() { + m, err := v.UnpackUntilType(reflect.TypeFor[*dencoding.Map]()) + if err != nil { + return nil, fmt.Errorf("error getting map: %w", err) + } + return m.Value.Interface().(*dencoding.Map), nil + } + return nil, fmt.Errorf("value is not a dencoding map") +} + +// SetMapKey sets the value at the specified key in the map. +func (v *Value) SetMapKey(key string, value *Value) error { + switch { + case v.isDencodingMap(): + m, err := v.dencodingMapValue() + if err != nil { + return fmt.Errorf("error getting map: %w", err) + } + m.Set(key, value.Value.Interface()) + return nil + case v.isStandardMap(): + unpacked, err := v.UnpackUntilKind(reflect.Map) + if err != nil { + return fmt.Errorf("error unpacking value: %w", err) + } + unpacked.Value.SetMapIndex(reflect.ValueOf(key), value.Value) + return nil + default: + return fmt.Errorf("value is not a map") + } +} + +// GetMapKey returns the value at the specified key in the map. +func (v *Value) GetMapKey(key string) (*Value, error) { + switch { + case v.isDencodingMap(): + m, err := v.dencodingMapValue() + if err != nil { + return nil, fmt.Errorf("error getting map: %w", err) + } + val, ok := m.Get(key) + if !ok { + return nil, &MapKeyNotFound{Key: key} + } + return &Value{ + Value: reflect.ValueOf(val), + }, nil + case v.isStandardMap(): + unpacked, err := v.UnpackUntilKind(reflect.Map) + if err != nil { + return nil, fmt.Errorf("error unpacking value: %w", err) + } + i := unpacked.Value.MapIndex(reflect.ValueOf(key)) + if !i.IsValid() { + return nil, &MapKeyNotFound{Key: key} + } + return &Value{ + Value: i, + }, nil + default: return nil, fmt.Errorf("value is not a map") } - return v.Value.Interface().(*dencoding.Map), nil } -func (v *Value) IsMap() bool { - return v.UnpackKinds(reflect.Interface).isMap() +// DeleteMapKey deletes the key from the map. +func (v *Value) DeleteMapKey(key string) error { + switch { + case v.isDencodingMap(): + m, err := v.dencodingMapValue() + if err != nil { + return fmt.Errorf("error getting map: %w", err) + } + m.Delete(key) + return nil + case v.isStandardMap(): + unpacked, err := v.UnpackUntilKind(reflect.Map) + if err != nil { + return fmt.Errorf("error unpacking value: %w", err) + } + unpacked.Value.SetMapIndex(reflect.ValueOf(key), reflect.Value{}) + return nil + default: + return fmt.Errorf("value is not a map") + } } -func (v *Value) isMap() bool { - return v.Value.Type() == reflect.TypeFor[*dencoding.Map]() +// MapKeys returns a list of keys in the map. +func (v *Value) MapKeys() ([]string, error) { + switch { + case v.isDencodingMap(): + m, err := v.dencodingMapValue() + if err != nil { + return nil, fmt.Errorf("error getting map: %w", err) + } + return m.Keys(), nil + case v.isStandardMap(): + unpacked, err := v.UnpackUntilKind(reflect.Map) + if err != nil { + return nil, fmt.Errorf("error unpacking value: %w", err) + } + keys := unpacked.Value.MapKeys() + strKeys := make([]string, len(keys)) + for i, k := range keys { + strKeys[i] = k.String() + } + return strKeys, nil + default: + return nil, fmt.Errorf("value is not a map") + } } -func (v *Value) SetMapKey(key string, value *Value) error { - m, err := v.MapValue() +// RangeMap iterates over each key in the map and calls the provided function with the key and value. +func (v *Value) RangeMap(f func(string, *Value) error) error { + keys, err := v.MapKeys() if err != nil { - return fmt.Errorf("error getting map: %w", err) + return fmt.Errorf("error getting map keys: %w", err) } - m.Set(key, value.Value.Interface()) + + for _, k := range keys { + va, err := v.GetMapKey(k) + if err != nil { + return fmt.Errorf("error getting map key: %w", err) + } + if err := f(k, va); err != nil { + return err + } + } + return nil } -func (v *Value) GetMapKey(key string) (*Value, error) { - m, err := v.MapValue() +// MapKeyValues returns a list of key value pairs in the map. +func (v *Value) MapKeyValues() ([]KeyValue, error) { + keys, err := v.MapKeys() if err != nil { - return nil, fmt.Errorf("error getting map: %w", err) + return nil, fmt.Errorf("error getting map keys: %w", err) } - val, ok := m.Get(key) - if !ok { - return nil, &MapKeyNotFound{Key: key} + + kvs := make([]KeyValue, len(keys)) + + for _, k := range keys { + va, err := v.GetMapKey(k) + if err != nil { + return nil, fmt.Errorf("error getting map key: %w", err) + } + kvs = append(kvs, KeyValue{ + Key: k, + Value: va, + }) } - return &Value{ - Value: reflect.ValueOf(val), - }, nil + + return kvs, nil } diff --git a/model/value_map_test.go b/model/value_map_test.go new file mode 100644 index 00000000..4fc43f00 --- /dev/null +++ b/model/value_map_test.go @@ -0,0 +1,154 @@ +package model_test + +import ( + "errors" + "testing" + + "github.com/tomwright/dasel/v3/dencoding" + "github.com/tomwright/dasel/v3/model" +) + +func TestMap(t *testing.T) { + standardMap := func() *model.Value { + return model.NewValue(map[string]interface{}{ + "foo": "foo1", + "bar": "bar1", + }) + } + + dencodingMap := func() *model.Value { + return model.NewValue(dencoding.NewMap(). + Set("foo", "foo1"). + Set("bar", "bar1")) + } + + modelMap := func() *model.Value { + res := model.NewMapValue() + if err := res.SetMapKey("foo", model.NewValue("foo1")); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := res.SetMapKey("bar", model.NewValue("bar1")); err != nil { + t.Fatalf("unexpected error: %s", err) + } + return res + } + + runTests := func(v func() *model.Value) func(t *testing.T) { + return func(t *testing.T) { + t.Run("IsMap", func(t *testing.T) { + v := v() + if !v.IsMap() { + t.Errorf("expected value to be a map") + } + }) + t.Run("GetMapKey", func(t *testing.T) { + v := v() + foo, err := v.GetMapKey("foo") + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + got, err := foo.StringValue() + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if got != "foo1" { + t.Errorf("expected foo1, got %s", got) + } + }) + t.Run("SetMapKey", func(t *testing.T) { + v := v() + if err := v.SetMapKey("baz", model.NewValue("baz1")); err != nil { + t.Errorf("unexpected error: %s", err) + return + } + baz, err := v.GetMapKey("baz") + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + got, err := baz.StringValue() + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if got != "baz1" { + t.Errorf("expected baz1, got %s", got) + } + }) + t.Run("MapKeys", func(t *testing.T) { + v := v() + keys, err := v.MapKeys() + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if len(keys) != 2 { + t.Errorf("expected 2 keys, got %d", len(keys)) + } + exp := []string{"foo", "bar"} + for _, k := range exp { + var found bool + for _, e := range keys { + if e == k { + found = true + break + } + } + if !found { + t.Errorf("expected key %s not found", k) + } + } + }) + t.Run("RangeMap", func(t *testing.T) { + v := v() + var keys []string + err := v.RangeMap(func(k string, v *model.Value) error { + keys = append(keys, k) + return nil + }) + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if len(keys) != 2 { + t.Errorf("expected 2 keys, got %d", len(keys)) + } + exp := []string{"foo", "bar"} + for _, k := range exp { + var found bool + for _, e := range keys { + if e == k { + found = true + break + } + } + if !found { + t.Errorf("expected key %s not found", k) + } + } + }) + t.Run("DeleteMapKey", func(t *testing.T) { + v := v() + if _, err := v.GetMapKey("foo"); err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if err := v.DeleteMapKey("foo"); err != nil { + t.Errorf("unexpected error: %s", err) + return + } + _, err := v.GetMapKey("foo") + notFoundErr := &model.MapKeyNotFound{} + if !errors.As(err, ¬FoundErr) { + t.Errorf("expected key not found error, got %s", err) + } + }) + } + } + + t.Run("standard map", runTests(standardMap)) + t.Run("dencoding map", runTests(dencodingMap)) + t.Run("model map", runTests(modelMap)) +} diff --git a/model/value_slice.go b/model/value_slice.go index 74268cb9..c515159b 100644 --- a/model/value_slice.go +++ b/model/value_slice.go @@ -32,6 +32,7 @@ func (v *Value) isSlice() bool { return v.Value.Kind() == reflect.Slice } +// Append appends a value to the slice. func (v *Value) Append(val *Value) error { unpacked := v.UnpackKinds(reflect.Interface, reflect.Ptr) if !unpacked.isSlice() { @@ -42,6 +43,7 @@ func (v *Value) Append(val *Value) error { return nil } +// SliceLen returns the length of the slice. func (v *Value) SliceLen() (int, error) { unpacked := v.UnpackKinds(reflect.Interface, reflect.Ptr) if !unpacked.isSlice() { @@ -50,6 +52,7 @@ func (v *Value) SliceLen() (int, error) { return unpacked.Value.Len(), nil } +// GetSliceIndex returns the value at the specified index in the slice. func (v *Value) GetSliceIndex(i int) (*Value, error) { unpacked := v.UnpackKinds(reflect.Interface, reflect.Ptr) if !unpacked.isSlice() { @@ -60,3 +63,36 @@ func (v *Value) GetSliceIndex(i int) (*Value, error) { } return NewValue(unpacked.Value.Index(i)), nil } + +// SetSliceIndex sets the value at the specified index in the slice. +func (v *Value) SetSliceIndex(i int, val *Value) error { + unpacked := v.UnpackKinds(reflect.Interface, reflect.Ptr) + if !unpacked.isSlice() { + return fmt.Errorf("expected slice, got %s", v.Type()) + } + if i < 0 || i >= unpacked.Value.Len() { + return fmt.Errorf("index out of range: %d", i) + } + unpacked.Value.Index(i).Set(val.Value) + return nil +} + +// RangeSlice iterates over each item in the slice and calls the provided function. +func (v *Value) RangeSlice(f func(int, *Value) error) error { + length, err := v.SliceLen() + if err != nil { + return fmt.Errorf("error getting slice length: %w", err) + } + + for i := 0; i < length; i++ { + va, err := v.GetSliceIndex(i) + if err != nil { + return fmt.Errorf("error getting slice index %d: %w", i, err) + } + if err := f(i, va); err != nil { + return err + } + } + + return nil +} diff --git a/parsing/format.go b/parsing/format.go new file mode 100644 index 00000000..0a7b8588 --- /dev/null +++ b/parsing/format.go @@ -0,0 +1,53 @@ +package parsing + +import ( + "fmt" + + "github.com/tomwright/dasel/v3/model" +) + +type Format string + +const ( + JSON Format = "json" + YAML Format = "yaml" + TOML Format = "toml" +) + +func (f Format) String() string { + return string(f) +} + +type Reader interface { + Read([]byte) (*model.Value, error) +} + +type Writer interface { + Write(*model.Value) ([]byte, error) +} + +func NewReader(format Format) (Reader, error) { + switch format { + case JSON: + return NewJSONReader() + case YAML: + return NewYAMLReader() + case TOML: + return NewTOMLReader() + default: + return nil, fmt.Errorf("unsupported file format: %s", format) + } +} + +func NewWriter(format Format) (Writer, error) { + switch format { + case JSON: + return NewJSONWriter() + case YAML: + return NewYAMLWriter() + case TOML: + return NewTOMLWriter() + default: + return nil, fmt.Errorf("unsupported file format: %s", format) + } +} diff --git a/parsing/json.go b/parsing/json.go new file mode 100644 index 00000000..6a10033c --- /dev/null +++ b/parsing/json.go @@ -0,0 +1,31 @@ +package parsing + +import ( + "encoding/json" + + "github.com/tomwright/dasel/v3/model" +) + +func NewJSONReader() (Reader, error) { + return &jsonReader{}, nil +} + +func NewJSONWriter() (Writer, error) { + return &jsonWriter{}, nil +} + +type jsonReader struct{} + +func (j *jsonReader) Read(data []byte) (*model.Value, error) { + var unmarshalled any + if err := json.Unmarshal(data, &unmarshalled); err != nil { + return nil, err + } + return model.NewValue(&unmarshalled), nil +} + +type jsonWriter struct{} + +func (j *jsonWriter) Write(value *model.Value) ([]byte, error) { + return json.Marshal(value.Interface()) +} diff --git a/parsing/toml.go b/parsing/toml.go new file mode 100644 index 00000000..4bbcbdf7 --- /dev/null +++ b/parsing/toml.go @@ -0,0 +1,23 @@ +package parsing + +import "github.com/tomwright/dasel/v3/model" + +func NewTOMLReader() (Reader, error) { + return &tomlReader{}, nil +} + +func NewTOMLWriter() (Writer, error) { + return &tomlWriter{}, nil +} + +type tomlReader struct{} + +func (j *tomlReader) Read(data []byte) (*model.Value, error) { + panic("not implemented") +} + +type tomlWriter struct{} + +func (j *tomlWriter) Write(value *model.Value) ([]byte, error) { + panic("not implemented") +} diff --git a/parsing/yaml.go b/parsing/yaml.go new file mode 100644 index 00000000..cdfe71a2 --- /dev/null +++ b/parsing/yaml.go @@ -0,0 +1,23 @@ +package parsing + +import "github.com/tomwright/dasel/v3/model" + +func NewYAMLReader() (Reader, error) { + return &yamlReader{}, nil +} + +func NewYAMLWriter() (Writer, error) { + return &yamlWriter{}, nil +} + +type yamlReader struct{} + +func (j *yamlReader) Read(data []byte) (*model.Value, error) { + panic("not implemented") +} + +type yamlWriter struct{} + +func (j *yamlWriter) Write(value *model.Value) ([]byte, error) { + panic("not implemented") +} From d92a503829728ebcaeb7df869d44fbcfcf780d64 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Thu, 3 Oct 2024 18:22:15 +0100 Subject: [PATCH 13/56] General fixes and improvements --- benchmark/README.md | 113 ----- benchmark/data.json | 25 - benchmark/data.yaml | 18 - benchmark/data/append_array_of_strings.json | 452 ------------------ benchmark/data/array_index.json | 452 ------------------ benchmark/data/delete_property.json | 452 ------------------ benchmark/data/list_array_keys.json | 452 ------------------ benchmark/data/nested_property.json | 452 ------------------ benchmark/data/overwrite_object.json | 452 ------------------ benchmark/data/root_object.json | 452 ------------------ benchmark/data/top_level_property.json | 452 ------------------ benchmark/data/update_string.json | 452 ------------------ .../diagrams/append_array_of_strings.jpg | Bin 19399 -> 0 bytes benchmark/diagrams/array_index.jpg | Bin 18054 -> 0 bytes benchmark/diagrams/delete_property.jpg | Bin 18475 -> 0 bytes benchmark/diagrams/list_array_keys.jpg | Bin 18864 -> 0 bytes benchmark/diagrams/nested_property.jpg | Bin 18577 -> 0 bytes benchmark/diagrams/overwrite_object.jpg | Bin 18847 -> 0 bytes benchmark/diagrams/root_object.jpg | Bin 18130 -> 0 bytes benchmark/diagrams/top_level_property.jpg | Bin 18624 -> 0 bytes benchmark/diagrams/update_string.jpg | Bin 18892 -> 0 bytes benchmark/partials/bottom.md | 0 benchmark/partials/top.md | 13 - benchmark/plot_barchart.py | 32 -- benchmark/run.sh | 65 --- benchmark/tests.txt | 72 --- cmd/dasel/main.go | 4 +- execution/execute.go | 32 -- execution/execute_array.go | 53 ++ execution/execute_func.go | 28 +- execution/execute_map.go | 20 +- execution/execute_object.go | 17 + execution/execute_spread.go | 13 +- execution/execute_test.go | 15 +- execution/func.go | 15 +- {ptr => internal/ptr}/to.go | 1 + model/error.go | 23 + model/value_comparison.go | 151 +++--- model/value_map.go | 2 + model/value_math.go | 10 - model/value_metadata.go | 6 + model/value_slice.go | 44 +- model/value_slice_test.go | 199 +++++++- parsing/format.go | 9 + parsing/json.go | 4 + parsing/toml.go | 4 + parsing/yaml.go | 4 + selector/parser/parse_map.go | 2 +- selector/parser/parser.go | 22 +- 49 files changed, 491 insertions(+), 4593 deletions(-) delete mode 100644 benchmark/README.md delete mode 100644 benchmark/data.json delete mode 100644 benchmark/data.yaml delete mode 100644 benchmark/data/append_array_of_strings.json delete mode 100644 benchmark/data/array_index.json delete mode 100644 benchmark/data/delete_property.json delete mode 100644 benchmark/data/list_array_keys.json delete mode 100644 benchmark/data/nested_property.json delete mode 100644 benchmark/data/overwrite_object.json delete mode 100644 benchmark/data/root_object.json delete mode 100644 benchmark/data/top_level_property.json delete mode 100644 benchmark/data/update_string.json delete mode 100644 benchmark/diagrams/append_array_of_strings.jpg delete mode 100644 benchmark/diagrams/array_index.jpg delete mode 100644 benchmark/diagrams/delete_property.jpg delete mode 100644 benchmark/diagrams/list_array_keys.jpg delete mode 100644 benchmark/diagrams/nested_property.jpg delete mode 100644 benchmark/diagrams/overwrite_object.jpg delete mode 100644 benchmark/diagrams/root_object.jpg delete mode 100644 benchmark/diagrams/top_level_property.jpg delete mode 100644 benchmark/diagrams/update_string.jpg delete mode 100644 benchmark/partials/bottom.md delete mode 100644 benchmark/partials/top.md delete mode 100644 benchmark/plot_barchart.py delete mode 100755 benchmark/run.sh delete mode 100644 benchmark/tests.txt create mode 100644 execution/execute_array.go rename {ptr => internal/ptr}/to.go (51%) diff --git a/benchmark/README.md b/benchmark/README.md deleted file mode 100644 index a4a86786..00000000 --- a/benchmark/README.md +++ /dev/null @@ -1,113 +0,0 @@ -# Benchmarks - -These benchmarks are auto generated using `./benchmark/run.sh`. - -``` -brew install hyperfine -pip install matplotlib -./benchmark/run.sh -``` - -I have put together what I believe to be equivalent commands in dasel/jq/yq. - -If you have any feedback or wish to add new benchmarks please submit a PR. -## Benchmarks - -### Root Object - -Root Object - -| Command | Mean [ms] | Min [ms] | Max [ms] | Relative | -|:---|---:|---:|---:|---:| -| `daselv2 -f benchmark/data.json` | 6.6 ± 0.2 | 6.1 | 7.2 | 1.00 | -| `dasel -f benchmark/data.json` | 8.7 ± 0.5 | 8.0 | 10.3 | 1.33 ± 0.09 | -| `jq '.' benchmark/data.json` | 28.1 ± 0.7 | 27.0 | 31.5 | 4.28 ± 0.19 | -| `yq --yaml-output '.' benchmark/data.yaml` | 127.9 ± 3.1 | 124.5 | 151.6 | 19.50 ± 0.84 | - -### Top level property - -Top level property - -| Command | Mean [ms] | Min [ms] | Max [ms] | Relative | -|:---|---:|---:|---:|---:| -| `daselv2 -f benchmark/data.json 'id'` | 6.6 ± 0.2 | 6.1 | 7.4 | 1.00 | -| `dasel -f benchmark/data.json '.id'` | 8.3 ± 0.3 | 7.8 | 9.7 | 1.27 ± 0.06 | -| `jq '.id' benchmark/data.json` | 28.2 ± 0.9 | 27.1 | 31.5 | 4.31 ± 0.21 | -| `yq --yaml-output '.id' benchmark/data.yaml` | 128.4 ± 10.1 | 124.4 | 211.7 | 19.59 ± 1.71 | - -### Nested property - -Nested property - -| Command | Mean [ms] | Min [ms] | Max [ms] | Relative | -|:---|---:|---:|---:|---:| -| `daselv2 -f benchmark/data.json 'user.name.first'` | 6.5 ± 0.2 | 6.1 | 7.3 | 1.00 | -| `dasel -f benchmark/data.json '.user.name.first'` | 8.3 ± 0.3 | 7.9 | 9.9 | 1.28 ± 0.07 | -| `jq '.user.name.first' benchmark/data.json` | 28.2 ± 0.9 | 27.0 | 32.9 | 4.34 ± 0.22 | -| `yq --yaml-output '.user.name.first' benchmark/data.yaml` | 126.7 ± 2.1 | 124.5 | 138.2 | 19.52 ± 0.81 | - -### Array index - -Array index - -| Command | Mean [ms] | Min [ms] | Max [ms] | Relative | -|:---|---:|---:|---:|---:| -| `daselv2 -f benchmark/data.json 'favouriteNumbers.[1]'` | 6.5 ± 0.2 | 6.0 | 7.5 | 1.00 | -| `dasel -f benchmark/data.json '.favouriteNumbers.[1]'` | 8.6 ± 0.7 | 7.9 | 11.3 | 1.33 ± 0.12 | -| `jq '.favouriteNumbers[1]' benchmark/data.json` | 28.4 ± 1.6 | 27.3 | 38.1 | 4.36 ± 0.29 | -| `yq --yaml-output '.favouriteNumbers[1]' benchmark/data.yaml` | 128.3 ± 9.2 | 124.2 | 213.8 | 19.69 ± 1.59 | - -### Append to array of strings - -Append to array of strings - -| Command | Mean [ms] | Min [ms] | Max [ms] | Relative | -|:---|---:|---:|---:|---:| -| `daselv2 put -f benchmark/data.json -t string -v 'blue' -o - 'favouriteColours.[]'` | 6.6 ± 0.3 | 6.1 | 8.3 | 1.00 | -| `dasel put string -f benchmark/data.json -o - '.favouriteColours.[]' blue` | 8.4 ± 0.3 | 7.8 | 9.2 | 1.28 ± 0.07 | -| `jq '.favouriteColours += ["blue"]' benchmark/data.json` | 28.3 ± 0.9 | 27.4 | 32.7 | 4.31 ± 0.25 | -| `yq --yaml-output '.favouriteColours += ["blue"]' benchmark/data.yaml` | 127.6 ± 2.4 | 124.1 | 140.3 | 19.45 ± 1.01 | - -### Update a string value - -Update a string value - -| Command | Mean [ms] | Min [ms] | Max [ms] | Relative | -|:---|---:|---:|---:|---:| -| `daselv2 put -f benchmark/data.json -t string -v 'blue' -o - 'favouriteColours.[0]'` | 6.6 ± 0.3 | 6.1 | 7.4 | 1.00 | -| `dasel put string -f benchmark/data.json -o - '.favouriteColours.[0]' blue` | 9.5 ± 1.7 | 8.0 | 12.9 | 1.45 ± 0.27 | -| `jq '.favouriteColours[0] = "blue"' benchmark/data.json` | 28.5 ± 1.3 | 27.3 | 33.1 | 4.33 ± 0.26 | -| `yq --yaml-output '.favouriteColours[0] = "blue"' benchmark/data.yaml` | 127.3 ± 2.7 | 125.0 | 149.4 | 19.36 ± 0.86 | - -### Overwrite an object - -Overwrite an object - -| Command | Mean [ms] | Min [ms] | Max [ms] | Relative | -|:---|---:|---:|---:|---:| -| `daselv2 put -f benchmark/data.json -o - -t json -v '{"first":"Frank","last":"Jones"}' 'user.name'` | 6.3 ± 0.3 | 6.0 | 7.2 | 1.00 | -| `dasel put document -f benchmark/data.json -o - -d json '.user.name' '{"first":"Frank","last":"Jones"}'` | 8.3 ± 0.3 | 7.8 | 9.6 | 1.31 ± 0.07 | -| `jq '.user.name = {"first":"Frank","last":"Jones"}' benchmark/data.json` | 28.2 ± 1.0 | 27.2 | 31.7 | 4.45 ± 0.23 | -| `yq --yaml-output '.user.name = {"first":"Frank","last":"Jones"}' benchmark/data.yaml` | 127.5 ± 2.5 | 124.6 | 143.8 | 20.10 ± 0.89 | - -### List keys of an array - -List keys of an array - -| Command | Mean [ms] | Min [ms] | Max [ms] | Relative | -|:---|---:|---:|---:|---:| -| `daselv2 -f benchmark/data.json 'all().key()'` | 6.4 ± 0.3 | 6.0 | 7.4 | 1.00 | -| `dasel -f benchmark/data.json -m '.-'` | 8.3 ± 0.3 | 7.8 | 9.6 | 1.30 ± 0.07 | -| `jq 'keys[]' benchmark/data.json` | 28.1 ± 1.0 | 27.1 | 32.1 | 4.41 ± 0.24 | -| `yq --yaml-output 'keys[]' benchmark/data.yaml` | 126.6 ± 2.1 | 123.7 | 138.3 | 19.82 ± 0.88 | - -### Delete property - -Delete property - -| Command | Mean [ms] | Min [ms] | Max [ms] | Relative | -|:---|---:|---:|---:|---:| -| `daselv2 delete -f benchmark/data.json -o - 'id'` | 6.5 ± 0.3 | 6.1 | 8.2 | 1.00 | -| `dasel delete -f benchmark/data.json -o - '.id'` | 8.4 ± 0.3 | 7.9 | 10.1 | 1.30 ± 0.08 | -| `jq 'del(.id)' benchmark/data.json` | 28.3 ± 0.9 | 27.4 | 32.0 | 4.38 ± 0.24 | -| `yq --yaml-output 'del(.id)' benchmark/data.yaml` | 127.5 ± 2.7 | 124.7 | 147.3 | 19.74 ± 0.99 | diff --git a/benchmark/data.json b/benchmark/data.json deleted file mode 100644 index 4db0e9a3..00000000 --- a/benchmark/data.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "id": "1234", - "user": { - "name": { - "first": "Tom", - "last": "Wright" - } - }, - "favouriteNumbers": [ - 1, 2, 3, 4 - ], - "favouriteColours": [ - "red", "green" - ], - "phones": [ - { - "make": "OnePlus", - "model": "8 Pro" - }, - { - "make": "Apple", - "model": "iPhone 12" - } - ] -} \ No newline at end of file diff --git a/benchmark/data.yaml b/benchmark/data.yaml deleted file mode 100644 index 8475f981..00000000 --- a/benchmark/data.yaml +++ /dev/null @@ -1,18 +0,0 @@ -id: "1234" -user: - name: - first: Tom - last: Wright -favouriteNumbers: - - 1 - - 2 - - 3 - - 4 -favouriteColours: - - red - - green -phones: - - make: OnePlus - model: 8 Pro - - make: Apple - model: iPhone 12 diff --git a/benchmark/data/append_array_of_strings.json b/benchmark/data/append_array_of_strings.json deleted file mode 100644 index 0d9de847..00000000 --- a/benchmark/data/append_array_of_strings.json +++ /dev/null @@ -1,452 +0,0 @@ -{ - "results": [ - { - "command": "daselv2 put -f benchmark/data.json -t string -v 'blue' -o - 'favouriteColours.[]'", - "mean": 0.006559234080000001, - "stddev": 0.00031854298910522985, - "median": 0.006491430500000001, - "user": 0.0034752449999999983, - "system": 0.0019132700000000006, - "min": 0.0061455780000000005, - "max": 0.008277945000000002, - "times": [ - 0.006303749000000001, - 0.006631118000000002, - 0.006537565, - 0.007438413000000001, - 0.0064620170000000005, - 0.006565493, - 0.006521805, - 0.006858463, - 0.006817912000000001, - 0.006348036000000001, - 0.006693780000000002, - 0.006865791000000001, - 0.006582719000000001, - 0.006369064000000001, - 0.006204007000000001, - 0.006465219000000001, - 0.006740448000000001, - 0.006536465000000002, - 0.006334705000000001, - 0.00631221, - 0.006295486000000001, - 0.006552023000000001, - 0.006945283000000002, - 0.007000094000000002, - 0.006553250000000002, - 0.0063897450000000005, - 0.006361923, - 0.006184466000000001, - 0.006665724000000001, - 0.006420157000000001, - 0.006370204000000001, - 0.006288761, - 0.006343052000000002, - 0.006363932000000001, - 0.00647316, - 0.006402695000000002, - 0.006668931000000001, - 0.006541594000000001, - 0.006582801000000001, - 0.006318445000000001, - 0.006391798000000001, - 0.006816351000000002, - 0.006677724000000001, - 0.006354416000000002, - 0.006566185, - 0.006488604, - 0.006282272, - 0.006343139000000001, - 0.006340616, - 0.0061589270000000015, - 0.006322890000000001, - 0.006777022000000001, - 0.006579106000000001, - 0.006367268000000001, - 0.0061455780000000005, - 0.0063252740000000005, - 0.007078150000000002, - 0.006927046000000001, - 0.006444273, - 0.007286268, - 0.008277945000000002, - 0.006734930000000002, - 0.006724939000000001, - 0.0063336170000000015, - 0.0064386220000000015, - 0.0066451320000000015, - 0.006584144000000002, - 0.006644218, - 0.0061654460000000015, - 0.006394843000000001, - 0.006808028000000001, - 0.006577802000000001, - 0.006859139, - 0.0067177190000000005, - 0.0066748760000000015, - 0.006459542, - 0.006456209000000001, - 0.006369647000000001, - 0.006324126000000001, - 0.006717296000000001, - 0.006654964000000001, - 0.006629765000000001, - 0.006462782, - 0.0061580160000000005, - 0.006389663, - 0.0064942570000000015, - 0.0076943210000000005, - 0.006775359000000002, - 0.006167174000000001, - 0.0062170340000000015, - 0.006594792, - 0.006984558000000002, - 0.006617657000000001, - 0.006359693000000001, - 0.006477142000000002, - 0.006428988000000002, - 0.006472503000000001, - 0.006237141000000002, - 0.006502066000000001, - 0.006713701000000001 - ] - }, - { - "command": "dasel put string -f benchmark/data.json -o - '.favouriteColours.[]' blue", - "mean": 0.008422656970000003, - "stddev": 0.00025259091696336404, - "median": 0.008380131000000002, - "user": 0.004592984999999998, - "system": 0.0025722500000000003, - "min": 0.007787787000000001, - "max": 0.009244352, - "times": [ - 0.008415565000000002, - 0.008523451000000001, - 0.008440252, - 0.009034088000000001, - 0.007981774, - 0.008095846, - 0.008162243000000001, - 0.008717780000000001, - 0.008437570000000002, - 0.008281179000000001, - 0.008536304000000002, - 0.008375172000000002, - 0.008247918000000002, - 0.008279651, - 0.008390803, - 0.008225355000000002, - 0.008426573000000001, - 0.008228413, - 0.008110071000000002, - 0.008865932000000002, - 0.008358800000000001, - 0.008252533000000001, - 0.008431301, - 0.008291585, - 0.008620712, - 0.008442852, - 0.008188211, - 0.008339394000000002, - 0.008153110000000002, - 0.008107502, - 0.008528822000000002, - 0.008870019000000002, - 0.008918309000000001, - 0.008473604000000001, - 0.008328539000000001, - 0.008514341000000002, - 0.008199196, - 0.008438659000000001, - 0.008860850000000002, - 0.008555207, - 0.008248031000000001, - 0.008231746000000002, - 0.008178108000000002, - 0.008615023000000001, - 0.008343317000000001, - 0.008848316000000002, - 0.008246622, - 0.008353140000000002, - 0.008884523000000002, - 0.008460423000000002, - 0.008209031, - 0.008008005, - 0.008596640000000001, - 0.008597531000000002, - 0.008138745000000001, - 0.008335233000000001, - 0.008312650000000001, - 0.008536947000000001, - 0.008326726000000001, - 0.008189442000000002, - 0.008295920000000002, - 0.008300092000000002, - 0.008616051000000001, - 0.008408462000000002, - 0.008564943000000002, - 0.008644960000000002, - 0.008815857000000002, - 0.008879512, - 0.009244352, - 0.008562036, - 0.008493366, - 0.008165944000000001, - 0.008532769, - 0.009191958000000002, - 0.008291675000000002, - 0.008156465000000002, - 0.008450780000000001, - 0.008731780000000001, - 0.008357380000000001, - 0.008227090000000001, - 0.008327979000000001, - 0.008419013000000001, - 0.008301521000000001, - 0.007787787000000001, - 0.008470077000000001, - 0.008395379000000001, - 0.008554001, - 0.008121759000000001, - 0.008370102, - 0.008148166000000002, - 0.008274022, - 0.008502364000000002, - 0.008295774, - 0.008303814000000001, - 0.008456303000000002, - 0.008316067000000002, - 0.008602751, - 0.008684052000000001, - 0.008385090000000001, - 0.008308599000000002 - ] - }, - { - "command": "jq '.favouriteColours += [\"blue\"]' benchmark/data.json", - "mean": 0.02827737350000002, - "stddev": 0.0009034554968123969, - "median": 0.027977958500000004, - "user": 0.023749294999999986, - "system": 0.0010989999999999995, - "min": 0.027433315000000003, - "max": 0.032711741, - "times": [ - 0.028249142, - 0.027607176000000004, - 0.028237523000000004, - 0.027965739000000007, - 0.027833120000000006, - 0.028149408000000004, - 0.027752193000000005, - 0.027708494000000004, - 0.028167615000000003, - 0.027505071000000002, - 0.027818529000000005, - 0.027737869000000002, - 0.027818596000000004, - 0.028173534000000004, - 0.027433315000000003, - 0.027962974000000005, - 0.027878396000000003, - 0.027924166000000004, - 0.027681920000000006, - 0.028897150000000007, - 0.028638061000000003, - 0.029799878000000005, - 0.028602848000000004, - 0.027682301000000003, - 0.027993433, - 0.030802984000000002, - 0.028793343000000002, - 0.027658053000000005, - 0.027993507000000004, - 0.028521237, - 0.027550071000000002, - 0.027552987000000004, - 0.027854634000000007, - 0.027478314000000004, - 0.028739950000000004, - 0.028959976000000002, - 0.032711741, - 0.028446239, - 0.031081846000000007, - 0.028377441000000007, - 0.027803511000000006, - 0.028191663000000002, - 0.028819486000000002, - 0.027961312000000006, - 0.028230457000000004, - 0.027755106000000005, - 0.027871419000000005, - 0.028027399000000005, - 0.027746458000000005, - 0.028239528000000003, - 0.027508068000000004, - 0.027703518000000007, - 0.028501507000000006, - 0.027968828000000005, - 0.027599649000000004, - 0.029671172000000006, - 0.028462668000000007, - 0.028453872, - 0.030067800000000002, - 0.028698855000000006, - 0.027985984000000002, - 0.027701038, - 0.027873509, - 0.027845307000000003, - 0.030754275, - 0.027844408000000005, - 0.027958159000000007, - 0.027847244000000004, - 0.028165775000000007, - 0.027454797000000003, - 0.027532953000000002, - 0.027947877000000006, - 0.027456149000000003, - 0.027607745000000003, - 0.029191528000000005, - 0.030514849000000007, - 0.028020067000000006, - 0.028056035000000003, - 0.028045762000000005, - 0.027502740000000005, - 0.027836392000000005, - 0.027846828000000007, - 0.028450904000000003, - 0.028528377000000004, - 0.027995119000000002, - 0.028167002000000007, - 0.027766284000000006, - 0.029786385000000002, - 0.027920931000000006, - 0.028838241000000004, - 0.027497927000000005, - 0.027969933000000006, - 0.027687134000000006, - 0.028433188, - 0.030652526000000003, - 0.028315720000000006, - 0.028198241000000002, - 0.027792101000000003, - 0.027434784000000004, - 0.028290082000000005 - ] - }, - { - "command": "yq --yaml-output '.favouriteColours += [\"blue\"]' benchmark/data.yaml", - "mean": 0.12757348592000006, - "stddev": 0.002423386351919514, - "median": 0.126939505, - "user": 0.100982175, - "system": 0.02253113999999999, - "min": 0.124055579, - "max": 0.140345268, - "times": [ - 0.128675823, - 0.126400477, - 0.125956854, - 0.13034874500000002, - 0.13147044200000002, - 0.126811315, - 0.127014623, - 0.127606391, - 0.126181221, - 0.125620472, - 0.12679447800000002, - 0.129316076, - 0.127785916, - 0.12570957400000002, - 0.12676715600000002, - 0.128046135, - 0.126416467, - 0.127508558, - 0.130695916, - 0.126317928, - 0.12795430300000002, - 0.126423871, - 0.13034294200000002, - 0.127369654, - 0.127041136, - 0.12736871, - 0.125452362, - 0.126104472, - 0.126491188, - 0.126336209, - 0.127610943, - 0.127407252, - 0.130519425, - 0.12742024500000002, - 0.12682843000000002, - 0.129290077, - 0.128543136, - 0.126169411, - 0.127122389, - 0.129844372, - 0.12644313000000001, - 0.126928857, - 0.125591383, - 0.140345268, - 0.139181639, - 0.128848355, - 0.13040216400000001, - 0.12674186, - 0.126526087, - 0.125780825, - 0.124903615, - 0.128609726, - 0.12974159400000002, - 0.126703288, - 0.12824459900000001, - 0.12574405800000002, - 0.126099089, - 0.12865711900000001, - 0.129010554, - 0.126941853, - 0.128240893, - 0.127679874, - 0.13064425600000001, - 0.131224538, - 0.128336573, - 0.12813685, - 0.125827146, - 0.12621572, - 0.12560569200000002, - 0.125860979, - 0.124055579, - 0.126881923, - 0.125962619, - 0.12518109900000002, - 0.127471768, - 0.127690343, - 0.128166174, - 0.127003408, - 0.12652575300000002, - 0.125387038, - 0.126035472, - 0.125921581, - 0.126444672, - 0.125781222, - 0.127364204, - 0.128575241, - 0.125477276, - 0.13448268700000002, - 0.12798704, - 0.125854668, - 0.126937157, - 0.126396237, - 0.12654605600000002, - 0.128356378, - 0.126673739, - 0.12586508700000001, - 0.12480339500000001, - 0.126783315, - 0.12731166300000002, - 0.12712312 - ] - } - ] -} diff --git a/benchmark/data/array_index.json b/benchmark/data/array_index.json deleted file mode 100644 index 9fe8aa05..00000000 --- a/benchmark/data/array_index.json +++ /dev/null @@ -1,452 +0,0 @@ -{ - "results": [ - { - "command": "daselv2 -f benchmark/data.json 'favouriteNumbers.[1]'", - "mean": 0.006514253420000005, - "stddev": 0.00023920540841924985, - "median": 0.006499365910000002, - "user": 0.0034387200000000006, - "system": 0.001897159999999999, - "min": 0.006029065410000001, - "max": 0.007518131410000002, - "times": [ - 0.006359525410000002, - 0.006347639410000001, - 0.006576967410000002, - 0.006356866410000002, - 0.006479986410000001, - 0.006564113410000003, - 0.0065071984100000015, - 0.006296185410000002, - 0.006579091410000001, - 0.006270604410000002, - 0.006377850410000002, - 0.006337435410000002, - 0.0064835464100000025, - 0.006029065410000001, - 0.006624118410000003, - 0.006805352410000002, - 0.006504854410000002, - 0.006361274410000001, - 0.006281048410000002, - 0.006218516410000001, - 0.006569730410000002, - 0.0067085914100000026, - 0.006597795410000003, - 0.006550108410000002, - 0.006199433410000002, - 0.006646654410000001, - 0.0066885634100000025, - 0.006426557410000002, - 0.006253597410000002, - 0.006507234410000002, - 0.006513284410000002, - 0.006509154410000002, - 0.006532394410000001, - 0.0062824794100000015, - 0.006370951410000002, - 0.007518131410000002, - 0.0066445524100000024, - 0.006324154410000001, - 0.006117128410000002, - 0.006303271410000002, - 0.006296227410000002, - 0.006374254410000002, - 0.006644465410000001, - 0.006604774410000002, - 0.006767576410000002, - 0.006675733410000002, - 0.0066030214100000015, - 0.006599147410000002, - 0.006396714410000002, - 0.0063837354100000025, - 0.006366449410000002, - 0.007385814410000002, - 0.006801945410000001, - 0.006620137410000003, - 0.006766972410000002, - 0.006599058410000002, - 0.006998681410000002, - 0.006589350410000002, - 0.006254862410000003, - 0.0064223514100000025, - 0.006246433410000002, - 0.006586239410000002, - 0.006303758410000002, - 0.006372647410000001, - 0.006681676410000002, - 0.006438418410000002, - 0.006268517410000002, - 0.006369256410000001, - 0.006462034410000001, - 0.006868734410000002, - 0.006789160410000002, - 0.006677967410000003, - 0.006568746410000001, - 0.0063789354100000015, - 0.006527026410000003, - 0.007141169410000002, - 0.006495515410000001, - 0.006374035410000001, - 0.006697130410000002, - 0.006644469410000003, - 0.006503216410000002, - 0.006392894410000002, - 0.006225889410000001, - 0.0064580144100000025, - 0.0068659544100000015, - 0.006844370410000002, - 0.006483242410000001, - 0.006274596410000001, - 0.006224130410000002, - 0.006540259410000002, - 0.006295319410000002, - 0.006269856410000002, - 0.0063334874100000015, - 0.006879290410000002, - 0.006494981410000001, - 0.006418471410000001, - 0.006356486410000002, - 0.0067143114100000015, - 0.006515453410000002, - 0.006666962410000001 - ] - }, - { - "command": "dasel -f benchmark/data.json '.favouriteNumbers.[1]'", - "mean": 0.008647657960000004, - "stddev": 0.0007277783433625674, - "median": 0.008415794910000003, - "user": 0.004660040000000002, - "system": 0.0026595400000000006, - "min": 0.007886576410000002, - "max": 0.011274111410000003, - "times": [ - 0.008091182410000002, - 0.008500325410000002, - 0.008437382410000001, - 0.008153446410000003, - 0.008247395410000002, - 0.008421636410000002, - 0.011274111410000003, - 0.010322209410000002, - 0.008463181410000003, - 0.009376744410000002, - 0.008555214410000001, - 0.008573467410000002, - 0.008128346410000003, - 0.008813606410000003, - 0.008438236410000003, - 0.009196735410000002, - 0.008564181410000002, - 0.010511814410000002, - 0.009468749410000001, - 0.008802863410000002, - 0.008605001410000002, - 0.010925524410000002, - 0.009946141410000001, - 0.008616927410000003, - 0.008637830410000002, - 0.010160582410000002, - 0.008824594410000003, - 0.008436981410000002, - 0.009372740410000002, - 0.009647739410000002, - 0.008806561410000002, - 0.008272658410000001, - 0.008816100410000001, - 0.008556087410000001, - 0.008144582410000002, - 0.008199081410000001, - 0.008271778410000001, - 0.008409896410000003, - 0.008233549410000002, - 0.008232787410000003, - 0.008105463410000001, - 0.008582689410000002, - 0.008192368410000001, - 0.008075073410000001, - 0.007968772410000002, - 0.008254628410000003, - 0.008527812410000002, - 0.008479314410000003, - 0.008354950410000003, - 0.008147517410000003, - 0.008525995410000002, - 0.008955531410000002, - 0.008065773410000003, - 0.008226882410000002, - 0.008409953410000002, - 0.008214646410000002, - 0.010703925410000002, - 0.011114687410000003, - 0.010800033410000002, - 0.009392145410000002, - 0.008435751410000003, - 0.009202693410000002, - 0.008168179410000002, - 0.008639129410000002, - 0.008029818410000002, - 0.008601068410000003, - 0.008394223410000002, - 0.008408747410000002, - 0.008187698410000001, - 0.008291510410000001, - 0.008568021410000001, - 0.008768702410000002, - 0.008409633410000002, - 0.007994059410000002, - 0.008288848410000002, - 0.008213092410000003, - 0.008336138410000003, - 0.008298858410000002, - 0.008338562410000002, - 0.008073126410000003, - 0.008204922410000003, - 0.007886576410000002, - 0.008812249410000002, - 0.008339298410000001, - 0.008733824410000002, - 0.007943106410000003, - 0.008180954410000002, - 0.008153251410000001, - 0.008367307410000002, - 0.008332021410000001, - 0.008155285410000003, - 0.008937120410000002, - 0.008431322410000002, - 0.008183839410000003, - 0.008171576410000003, - 0.008491446410000002, - 0.008337803410000002, - 0.008296235410000002, - 0.008074806410000001, - 0.008528844410000002 - ] - }, - { - "command": "jq '.favouriteNumbers[1]' benchmark/data.json", - "mean": 0.028413096960000005, - "stddev": 0.001565334465344961, - "median": 0.02790975291, - "user": 0.023769529999999994, - "system": 0.0011027, - "min": 0.027314145410000004, - "max": 0.03813759741, - "times": [ - 0.02866351441, - 0.02804803141, - 0.02905255641, - 0.028478343410000002, - 0.02778846241, - 0.027579653410000002, - 0.02777273541, - 0.028304104410000003, - 0.02771720641, - 0.02783232241, - 0.02751481041, - 0.02800383041, - 0.027813935410000004, - 0.02815265041, - 0.027663818410000006, - 0.02774647241, - 0.027657070410000005, - 0.027488081410000004, - 0.027503223410000006, - 0.02762275041, - 0.027677073410000003, - 0.02789480541, - 0.02742048641, - 0.02786538141, - 0.02784820841, - 0.02799416441, - 0.02937163541, - 0.02746712441, - 0.028044678410000003, - 0.02771839541, - 0.028703898410000005, - 0.02791840341, - 0.027554731410000005, - 0.030890312410000002, - 0.029575748409999998, - 0.02808533941, - 0.02839717841, - 0.027671851410000003, - 0.02791822241, - 0.028758057410000003, - 0.02826042341, - 0.02854033541, - 0.027612497410000006, - 0.02777538241, - 0.032834956410000005, - 0.02784191441, - 0.027901283409999998, - 0.028700409410000002, - 0.02805565441, - 0.02897347341, - 0.03218960441, - 0.027828689410000003, - 0.02745777841, - 0.02749297841, - 0.027491953410000006, - 0.027845749410000002, - 0.027456611410000005, - 0.027662951410000004, - 0.028730480410000002, - 0.02782251441, - 0.028070582410000003, - 0.027789150410000005, - 0.027636199410000006, - 0.027460479410000002, - 0.02789893141, - 0.029678910410000002, - 0.027764806410000002, - 0.02778805441, - 0.027667394410000004, - 0.028058332410000004, - 0.027620474410000005, - 0.027969598410000004, - 0.028256382410000003, - 0.02777342141, - 0.02780203941, - 0.028193748410000004, - 0.02823348941, - 0.02870256141, - 0.03603843141, - 0.03813759741, - 0.02844438941, - 0.03095494941, - 0.027886718410000004, - 0.02781991241, - 0.030275500410000003, - 0.028131206409999998, - 0.02882531041, - 0.027650028410000006, - 0.028074380410000002, - 0.028077266410000003, - 0.02993324941, - 0.027314145410000004, - 0.028671839410000004, - 0.028090556410000002, - 0.02780517941, - 0.028053429410000004, - 0.029660858410000004, - 0.027580942410000003, - 0.02875648341, - 0.02910785441 - ] - }, - { - "command": "yq --yaml-output '.favouriteNumbers[1]' benchmark/data.yaml", - "mean": 0.12829232476999997, - "stddev": 0.009213408897836083, - "median": 0.12641279091000002, - "user": 0.10105771, - "system": 0.02273175, - "min": 0.12417706341000001, - "max": 0.21379469041000002, - "times": [ - 0.12554962341, - 0.12477438441000002, - 0.12587045241000003, - 0.12486042041, - 0.12674162841, - 0.12793471041, - 0.12637979241000002, - 0.12532095641000002, - 0.12417706341000001, - 0.12476282241, - 0.12967359741, - 0.12590004441000002, - 0.12704103041, - 0.12584643541, - 0.12737940941, - 0.12785718641000002, - 0.12561894041000002, - 0.12914734541, - 0.12707724841, - 0.12584949741, - 0.12768988541, - 0.12790638941000002, - 0.12886176941000002, - 0.12559227941, - 0.13309706441000002, - 0.13219139341000002, - 0.12629921441000003, - 0.21379469041000002, - 0.14367038841000002, - 0.13730815441000002, - 0.12636776141, - 0.12595065941000003, - 0.12815899541, - 0.12532795641000002, - 0.14030630841000002, - 0.12699569041000003, - 0.12568449441, - 0.12593256241, - 0.12808099341, - 0.12975459141, - 0.12540442341000002, - 0.12478916041000002, - 0.12717988841000002, - 0.12717494841000002, - 0.12750201241, - 0.12793334341, - 0.12674744941000002, - 0.13053000241, - 0.12924352141, - 0.12764550341000003, - 0.12725821641, - 0.12614585541, - 0.12508599341, - 0.12599111241000002, - 0.12504626641000002, - 0.12777565041000002, - 0.12954196041000002, - 0.13542973941000003, - 0.12591780941000003, - 0.12617934741, - 0.12547421441, - 0.12693017841, - 0.13412827841000002, - 0.12755205341, - 0.13084645941, - 0.13301300941000002, - 0.12548762441000003, - 0.13012358141000002, - 0.12678677941000002, - 0.12610393741, - 0.12544432441, - 0.12504027641, - 0.12644109741, - 0.12456221241000001, - 0.12691115041, - 0.12645919641, - 0.12456890341000001, - 0.12492088641000001, - 0.12607901341000002, - 0.12595077741000002, - 0.12487406141000001, - 0.12877730841, - 0.12494141741, - 0.12586954341, - 0.12640240541, - 0.12451928741000001, - 0.12590133241, - 0.13326628341000002, - 0.12642317641, - 0.12915061241, - 0.12627308241000001, - 0.12645617441, - 0.12630826541, - 0.12598877341, - 0.12562453741000001, - 0.12810504541, - 0.12544396141000003, - 0.12650310441, - 0.12588862641, - 0.12443548841000002 - ] - } - ] -} diff --git a/benchmark/data/delete_property.json b/benchmark/data/delete_property.json deleted file mode 100644 index 76df8236..00000000 --- a/benchmark/data/delete_property.json +++ /dev/null @@ -1,452 +0,0 @@ -{ - "results": [ - { - "command": "daselv2 delete -f benchmark/data.json -o - 'id'", - "mean": 0.006462584990000004, - "stddev": 0.0002934537907552468, - "median": 0.006400216880000004, - "user": 0.0034548449999999994, - "system": 0.00191293, - "min": 0.006071011380000003, - "max": 0.008181844380000004, - "times": [ - 0.006523620380000003, - 0.006801800380000004, - 0.006656418380000003, - 0.006421608380000004, - 0.006071011380000003, - 0.006396515380000004, - 0.006438390380000004, - 0.006587852380000004, - 0.006433019380000004, - 0.006387138380000004, - 0.006194296380000004, - 0.006373346380000004, - 0.006380730380000003, - 0.006551954380000004, - 0.0063849413800000036, - 0.006292079380000003, - 0.006709308380000004, - 0.006726304380000004, - 0.006560664380000003, - 0.006151727380000003, - 0.006210096380000004, - 0.006484853380000004, - 0.006225769380000003, - 0.006519048380000003, - 0.006295020380000004, - 0.006076460380000004, - 0.006386713380000004, - 0.006260286380000004, - 0.006264486380000003, - 0.006516939380000003, - 0.006223403380000004, - 0.007283798380000003, - 0.006271291380000004, - 0.008181844380000004, - 0.006308980380000004, - 0.006491413380000003, - 0.006714409380000003, - 0.006287841380000004, - 0.006159291380000003, - 0.006347553380000004, - 0.006107234380000003, - 0.006788070380000004, - 0.0063410983800000036, - 0.006269166380000003, - 0.006452880380000003, - 0.006384252380000004, - 0.006734236380000003, - 0.0067479653800000036, - 0.006189380380000003, - 0.006183437380000004, - 0.006469931380000004, - 0.006469876380000003, - 0.006227551380000003, - 0.006518452380000003, - 0.006227649380000004, - 0.0064737283800000035, - 0.007018632380000004, - 0.006941159380000004, - 0.006595595380000003, - 0.006522178380000004, - 0.007185969380000004, - 0.006412871380000004, - 0.0065832913800000035, - 0.006759526380000004, - 0.0062513163800000035, - 0.007108463380000004, - 0.006668878380000003, - 0.006537349380000003, - 0.006417351380000004, - 0.006152635380000004, - 0.006510916380000004, - 0.006944444380000003, - 0.006535870380000003, - 0.006310626380000003, - 0.006079835380000004, - 0.006350962380000004, - 0.006375414380000003, - 0.006381707380000004, - 0.006377108380000003, - 0.006253683380000003, - 0.006254289380000003, - 0.006280740380000004, - 0.0064039183800000034, - 0.006243702380000004, - 0.0064915613800000035, - 0.006499035380000004, - 0.006279454380000004, - 0.006316164380000003, - 0.006262746380000003, - 0.006230493380000004, - 0.006297001380000004, - 0.006542056380000003, - 0.006491284380000004, - 0.0064794233800000035, - 0.006534393380000003, - 0.006631998380000004, - 0.006696304380000004, - 0.006312349380000004, - 0.006228607380000004, - 0.006370048380000003 - ] - }, - { - "command": "dasel delete -f benchmark/data.json -o - '.id'", - "mean": 0.008384274170000004, - "stddev": 0.00032488191902144457, - "median": 0.008326066880000003, - "user": 0.004562625, - "system": 0.0025348400000000004, - "min": 0.007896483380000003, - "max": 0.010092166380000003, - "times": [ - 0.008632449380000004, - 0.008126327380000003, - 0.008210599380000004, - 0.008103489380000004, - 0.008318152380000004, - 0.007896483380000003, - 0.008136694380000004, - 0.008516831380000004, - 0.008556746380000004, - 0.008144872380000003, - 0.008105856380000004, - 0.008532452380000003, - 0.008292100380000004, - 0.008302018380000003, - 0.008385852380000003, - 0.008261829380000004, - 0.008350843380000004, - 0.008127957380000004, - 0.008294285380000004, - 0.008577841380000003, - 0.008092074380000003, - 0.008164201380000003, - 0.008097422380000004, - 0.008057035380000004, - 0.008912470380000004, - 0.008766830380000003, - 0.008160295380000003, - 0.008217624380000003, - 0.009021105380000003, - 0.008296080380000003, - 0.008343097380000004, - 0.008136188380000003, - 0.008308167380000004, - 0.008168921380000003, - 0.008066609380000004, - 0.008142299380000004, - 0.008620015380000004, - 0.008641791380000003, - 0.008221956380000004, - 0.008093134380000004, - 0.008232084380000004, - 0.010092166380000003, - 0.008137383380000003, - 0.008484122380000004, - 0.008460836380000003, - 0.008317360380000003, - 0.008097974380000003, - 0.008052076380000004, - 0.008353207380000003, - 0.008392616380000004, - 0.008914519380000003, - 0.008136258380000004, - 0.008416963380000003, - 0.008509991380000003, - 0.008481845380000004, - 0.008208618380000003, - 0.008239198380000003, - 0.008104179380000003, - 0.008020998380000003, - 0.008081001380000003, - 0.008333981380000004, - 0.008540593380000003, - 0.008454602380000004, - 0.008514009380000003, - 0.008254774380000003, - 0.008359444380000004, - 0.008192411380000004, - 0.008629116380000003, - 0.008238469380000004, - 0.008626174380000004, - 0.008460451380000004, - 0.008168089380000004, - 0.008117273380000003, - 0.008868779380000004, - 0.008765519380000003, - 0.008447181380000003, - 0.008033323380000004, - 0.008643269380000004, - 0.008348699380000003, - 0.009242104380000003, - 0.008188816380000003, - 0.008533904380000004, - 0.008347605380000003, - 0.008421365380000004, - 0.008570044380000004, - 0.008259236380000003, - 0.008428365380000004, - 0.009599609380000004, - 0.008068953380000004, - 0.008167179380000004, - 0.008338064380000003, - 0.008477878380000003, - 0.008103978380000003, - 0.008309656380000004, - 0.008436991380000003, - 0.008420416380000004, - 0.008563287380000003, - 0.008451805380000003, - 0.008952137380000004, - 0.008415449380000004 - ] - }, - { - "command": "jq 'del(.id)' benchmark/data.json", - "mean": 0.028321697830000003, - "stddev": 0.0008928528791512938, - "median": 0.028021402380000004, - "user": 0.023717495000000005, - "system": 0.0011359500000000002, - "min": 0.027389924380000007, - "max": 0.031961913380000005, - "times": [ - 0.02790503238, - 0.027402959380000008, - 0.02759823738, - 0.027800803380000003, - 0.027781630380000005, - 0.027641249380000003, - 0.027840866380000003, - 0.027634188380000008, - 0.028114909380000004, - 0.02868352038, - 0.027724118380000003, - 0.02783807638, - 0.027701622380000006, - 0.028241956380000002, - 0.027590296380000004, - 0.028188352380000004, - 0.028291702380000006, - 0.028620646380000003, - 0.029156677380000003, - 0.028984475380000006, - 0.029153603380000004, - 0.029079290380000004, - 0.02857449738, - 0.028354916380000005, - 0.029073862380000003, - 0.030463022380000003, - 0.028571739380000002, - 0.029763361380000006, - 0.028266568380000003, - 0.02947337338, - 0.03126591838, - 0.027447031380000007, - 0.02770254138, - 0.028909887380000006, - 0.028268380380000006, - 0.02784867238, - 0.027861454380000004, - 0.027502637380000007, - 0.031047283380000006, - 0.027898154380000005, - 0.028423720380000003, - 0.028487762380000003, - 0.027954284380000007, - 0.02793334838, - 0.02820465038, - 0.02776063938, - 0.030876964380000003, - 0.031961913380000005, - 0.028083573380000004, - 0.02836499438, - 0.027792560380000005, - 0.02886763338, - 0.027746354380000005, - 0.02769368238, - 0.028670414380000002, - 0.027547167380000008, - 0.028480088380000004, - 0.02824932738, - 0.028197317380000003, - 0.027910828380000002, - 0.027856331380000002, - 0.027738983380000005, - 0.028038293380000003, - 0.027921961380000006, - 0.02776414738, - 0.028152741380000006, - 0.028163816380000005, - 0.027932751380000002, - 0.02784527338, - 0.027535820380000006, - 0.02865774938, - 0.027996039380000004, - 0.027936055380000005, - 0.02805494738, - 0.027739949380000004, - 0.028004511380000006, - 0.027850633380000003, - 0.031107026380000007, - 0.027909694380000002, - 0.029818966380000006, - 0.027797977380000005, - 0.028685633380000006, - 0.027888780380000006, - 0.029422888380000005, - 0.028476081380000004, - 0.02886139538, - 0.028052449380000004, - 0.027389924380000007, - 0.02885513738, - 0.027518568380000004, - 0.027580699380000004, - 0.027718841380000005, - 0.027460099380000005, - 0.027540914380000003, - 0.02804553538, - 0.028949621380000005, - 0.028102977380000005, - 0.02771834938, - 0.027953803380000003, - 0.027649672380000004 - ] - }, - { - "command": "yq --yaml-output 'del(.id)' benchmark/data.yaml", - "mean": 0.12754083835000002, - "stddev": 0.002708745253399845, - "median": 0.12667261338000002, - "user": 0.10090879500000001, - "system": 0.022498000000000004, - "min": 0.12466231938, - "max": 0.14727682938, - "times": [ - 0.12852639038000002, - 0.13086764538, - 0.13111911838, - 0.13053014038000002, - 0.12928528738, - 0.13032076238, - 0.13020268838000001, - 0.13070791138, - 0.12958945638000002, - 0.13139219638000002, - 0.12663775738000002, - 0.12605091038000002, - 0.12571975638000002, - 0.12647100038, - 0.12605892138000002, - 0.12908346738, - 0.12765379538000002, - 0.13048580338000001, - 0.12833174938, - 0.12587022838, - 0.12595621038000002, - 0.12487088838000002, - 0.12584067338000002, - 0.12589552638, - 0.12584753238000002, - 0.12726676938, - 0.12711129038000002, - 0.12831101138, - 0.12541951138000001, - 0.12526978638, - 0.12936814638000002, - 0.12482319038, - 0.12610445338, - 0.12973859038000002, - 0.12546581938, - 0.12697492738000002, - 0.12602576838000001, - 0.12658640938000001, - 0.12548401838, - 0.12604172338, - 0.12950636738000001, - 0.12829646638, - 0.12606076438, - 0.12582380838, - 0.12724688038, - 0.12848656438, - 0.12647799738, - 0.13026748938000002, - 0.12673980238000002, - 0.12557189838000002, - 0.12651612838, - 0.12952494138, - 0.12755315438, - 0.12627444038000002, - 0.12587682438, - 0.12703641838000002, - 0.12637814938, - 0.12521580038000002, - 0.12588377038, - 0.12681269738, - 0.12466231938, - 0.12653075138, - 0.12916903338000002, - 0.12681703538, - 0.12583430238, - 0.13038447438, - 0.14727682938, - 0.12884357038000002, - 0.12636121338, - 0.12878266438000002, - 0.12581123838, - 0.12679651838, - 0.12600387138000002, - 0.12651411538000001, - 0.12668466738, - 0.13090388938, - 0.12600993538000002, - 0.12753866938, - 0.12669067338, - 0.13121109438, - 0.12646465038000002, - 0.12570582738, - 0.13248716838000002, - 0.12666055938, - 0.12550285938, - 0.12618612338000001, - 0.12771003738, - 0.12786413938000002, - 0.12692584538, - 0.12611658238, - 0.12647015438, - 0.12514854238, - 0.12594847338, - 0.12714151738, - 0.12635010438000002, - 0.12861604838000001, - 0.12623786838, - 0.12724344138000002, - 0.12534301038, - 0.13027641838 - ] - } - ] -} diff --git a/benchmark/data/list_array_keys.json b/benchmark/data/list_array_keys.json deleted file mode 100644 index d5a6c6b0..00000000 --- a/benchmark/data/list_array_keys.json +++ /dev/null @@ -1,452 +0,0 @@ -{ - "results": [ - { - "command": "daselv2 -f benchmark/data.json 'all().key()'", - "mean": 0.006386365404999999, - "stddev": 0.00026077175804909504, - "median": 0.006328408524999999, - "user": 0.0034238599999999986, - "system": 0.0018489000000000003, - "min": 0.005956364024999998, - "max": 0.0073663480249999995, - "times": [ - 0.006823103024999998, - 0.0064611990249999985, - 0.006203120024999999, - 0.006298995024999999, - 0.006136317024999998, - 0.006265511024999999, - 0.006325998024999999, - 0.006201800024999998, - 0.006473850024999999, - 0.006980715025, - 0.006871870025, - 0.006265309025, - 0.006096389024999998, - 0.0062431850249999995, - 0.006527500025, - 0.0063891230249999995, - 0.006773702024999999, - 0.0059579650249999986, - 0.006081048025, - 0.006370505025, - 0.006391586024999999, - 0.006510955024999998, - 0.006139216024999999, - 0.0064211310249999995, - 0.006359475024999999, - 0.006423912025, - 0.006470235024999999, - 0.006169161024999999, - 0.0062588430249999995, - 0.006670626024999998, - 0.006243085024999998, - 0.006269853024999998, - 0.006219110024999999, - 0.006872447025, - 0.006281264024999999, - 0.006568001024999998, - 0.006368560024999998, - 0.006228630024999998, - 0.006340461025, - 0.006514619024999999, - 0.006886594024999999, - 0.006789752024999999, - 0.006273581025, - 0.006353929024999999, - 0.006761846025, - 0.006524992025, - 0.007333751024999999, - 0.006076344024999999, - 0.006295292024999999, - 0.006118911024999999, - 0.006885342024999999, - 0.006238494024999998, - 0.006240202025, - 0.006256806025, - 0.006330654024999999, - 0.0065517670249999995, - 0.006398470024999999, - 0.006333304024999999, - 0.006369339024999999, - 0.006324547024999999, - 0.006440147025, - 0.006227710024999999, - 0.005956364024999998, - 0.006653155024999999, - 0.006551595025, - 0.0061279170249999996, - 0.006482197025, - 0.006370144024999999, - 0.006060583024999999, - 0.006080086025, - 0.0067538890249999985, - 0.006691448024999999, - 0.006195852024999999, - 0.0063250080249999995, - 0.006491252024999999, - 0.006618424024999999, - 0.0062754600249999995, - 0.006097442024999999, - 0.006568754024999999, - 0.006400322024999999, - 0.006261797024999999, - 0.006326163024999999, - 0.0061050400249999985, - 0.006077227024999999, - 0.0073663480249999995, - 0.006468680024999999, - 0.006322582025, - 0.006370484024999999, - 0.006258679024999999, - 0.006159159024999999, - 0.0062668470249999985, - 0.006446402025, - 0.006340532024999999, - 0.0061810080249999995, - 0.006237171024999999, - 0.006541650024999999, - 0.006257626024999998, - 0.006026297024999999, - 0.0062337810249999985, - 0.006209025024999999 - ] - }, - { - "command": "dasel -f benchmark/data.json -m '.-'", - "mean": 0.008323275174999999, - "stddev": 0.00028002756793531094, - "median": 0.008276479024999998, - "user": 0.00452566, - "system": 0.00255623, - "min": 0.007805135024999999, - "max": 0.009648467025, - "times": [ - 0.008065891025, - 0.008671235024999998, - 0.008198722025, - 0.008494075024999999, - 0.008079633024999998, - 0.007861465025, - 0.008049851024999999, - 0.008756716025, - 0.008310123024999998, - 0.008153820024999998, - 0.008190754025, - 0.007980154024999998, - 0.008726181024999998, - 0.008454527024999998, - 0.008488313025, - 0.008157967025, - 0.007910841025, - 0.008276221024999999, - 0.008496079024999999, - 0.008384316024999998, - 0.008385384024999998, - 0.008041796024999999, - 0.008243508024999999, - 0.008078672025, - 0.008629602024999999, - 0.008481034025, - 0.008670758025, - 0.008558393025, - 0.008284490025, - 0.007908443025, - 0.008073299024999998, - 0.008405523025, - 0.008455882025, - 0.009648467025, - 0.008517730025, - 0.008529154025, - 0.008240335025, - 0.008569850024999999, - 0.008523820025, - 0.008712843025, - 0.008886034025, - 0.008155769024999999, - 0.008021048024999998, - 0.008613059024999998, - 0.008367404025, - 0.008262533024999998, - 0.008008223025, - 0.008115337024999999, - 0.008722667024999999, - 0.008367751024999998, - 0.007981787025, - 0.008272636024999998, - 0.008273013024999998, - 0.008211535024999998, - 0.007860345025, - 0.008329975024999998, - 0.008507433025, - 0.008627451025, - 0.008120705024999997, - 0.008023008025, - 0.008352531024999998, - 0.009055623025, - 0.008261318024999999, - 0.008040982025, - 0.008222700024999998, - 0.008530625024999999, - 0.008230760024999999, - 0.008276737024999998, - 0.008292712025, - 0.008402920025, - 0.008108297024999999, - 0.007805135024999999, - 0.008118455024999999, - 0.008473499024999998, - 0.008274075024999997, - 0.008128111024999999, - 0.008212798024999999, - 0.008205228025, - 0.008277310024999998, - 0.008403641024999998, - 0.008219625025, - 0.008486386025, - 0.008839279025, - 0.008407514025, - 0.008318761025, - 0.008447044025, - 0.008214079024999998, - 0.008495601025, - 0.008050656024999998, - 0.008150989025, - 0.008097720024999998, - 0.008593043025, - 0.008128884024999998, - 0.008025272024999999, - 0.008130464024999999, - 0.008202559025, - 0.008048412025, - 0.008377535025, - 0.008719098025, - 0.008307557024999999 - ] - }, - { - "command": "jq 'keys[]' benchmark/data.json", - "mean": 0.028143955954999984, - "stddev": 0.0009735027878388671, - "median": 0.027865165025000003, - "user": 0.023619860000000003, - "system": 0.0010893000000000005, - "min": 0.027056787025, - "max": 0.032070168025, - "times": [ - 0.027934758025, - 0.027698561025, - 0.031689497025, - 0.027392124025000002, - 0.027998845025000003, - 0.028202600025000003, - 0.027756287025, - 0.027576571025, - 0.027539574025000003, - 0.027666792025, - 0.029228857025, - 0.027923499025, - 0.027272014025000003, - 0.028314678024999998, - 0.027956029025000002, - 0.027698238025000002, - 0.027743653025000002, - 0.027538497025, - 0.027944904025, - 0.028101713025000002, - 0.027872767025000002, - 0.028127928025, - 0.027356851025, - 0.027316379025, - 0.028032076024999998, - 0.027687635025000004, - 0.028802335025, - 0.027694205025, - 0.027771865025000002, - 0.028042469025000002, - 0.027418824025, - 0.027744745025000003, - 0.030740821025000004, - 0.028118357025, - 0.027526325025000003, - 0.027694109025000004, - 0.027408485025000002, - 0.028917232025000003, - 0.028029996025000004, - 0.028490818025000003, - 0.027292986025, - 0.028173846025000004, - 0.027801290025, - 0.027591944025000002, - 0.027784581024999998, - 0.031238030025000002, - 0.029953265025000002, - 0.028131364025, - 0.027769269025, - 0.027648732025, - 0.027903154025, - 0.027598100025000002, - 0.027507720025, - 0.028115994025, - 0.028126665025000004, - 0.028893995025, - 0.028908099025, - 0.027903923025, - 0.027857563025000004, - 0.027493749025, - 0.030408693025000003, - 0.027661406025, - 0.027397789025, - 0.028019378025, - 0.027774265025, - 0.027783169025, - 0.027247329025000003, - 0.032070168025, - 0.027987995025000004, - 0.028234832025000003, - 0.027951489024999998, - 0.027788086025, - 0.027686869025, - 0.028503507025, - 0.027848491024999998, - 0.027823441025, - 0.028372427025, - 0.027979096025000004, - 0.028377635025, - 0.027674458025, - 0.027481699025, - 0.027637930025, - 0.027490243025, - 0.027816246024999998, - 0.027828545025, - 0.028536270025000005, - 0.028319113025000002, - 0.032059504025000005, - 0.028085674025, - 0.027608832025000002, - 0.030077762025000003, - 0.027468421025000002, - 0.028911626024999998, - 0.027953957025000004, - 0.028085417025000003, - 0.028674344025, - 0.028135312025000005, - 0.027554706025, - 0.027356500025, - 0.027056787025 - ] - }, - { - "command": "yq --yaml-output 'keys[]' benchmark/data.yaml", - "mean": 0.126571703835, - "stddev": 0.0021394719133965433, - "median": 0.125940086025, - "user": 0.10013251000000002, - "system": 0.022029060000000003, - "min": 0.123734583025, - "max": 0.13830425602500002, - "times": [ - 0.125756854025, - 0.128261602025, - 0.13582618302500002, - 0.13830425602500002, - 0.125245255025, - 0.125060733025, - 0.128660257025, - 0.129463940025, - 0.12602612902500002, - 0.12732227202500002, - 0.125472846025, - 0.125226568025, - 0.124530027025, - 0.124969231025, - 0.12590821702500002, - 0.128343272025, - 0.127953705025, - 0.12611021502500003, - 0.12532648602500002, - 0.12830964802500003, - 0.12583718902500002, - 0.126487929025, - 0.128857570025, - 0.126424799025, - 0.125313309025, - 0.12748003602500002, - 0.12535427902500001, - 0.128593588025, - 0.12649578802500003, - 0.125927754025, - 0.125435495025, - 0.128667324025, - 0.125223930025, - 0.126303508025, - 0.12655347902500003, - 0.12504773802500002, - 0.12554994402500003, - 0.12805640202500002, - 0.126659992025, - 0.12493408602499999, - 0.12463694802500001, - 0.128609102025, - 0.126847961025, - 0.125437499025, - 0.12554400502500002, - 0.12493894502500001, - 0.125184227025, - 0.12597276202500002, - 0.12550308402500002, - 0.12624721402500003, - 0.12681439902500002, - 0.123963375025, - 0.124511463025, - 0.12595756102500003, - 0.12818226402500002, - 0.12769913302500002, - 0.12683222202500002, - 0.125638937025, - 0.12580227802500002, - 0.12688296302500002, - 0.12567133202500003, - 0.12638211502500002, - 0.12843807602500001, - 0.12650712602500003, - 0.12541215602500003, - 0.12594728902500002, - 0.125041758025, - 0.12691484802500003, - 0.12774400602500002, - 0.125095811025, - 0.125020227025, - 0.124312893025, - 0.12719350002500002, - 0.12524880802500002, - 0.124531921025, - 0.129067605025, - 0.12719105802500003, - 0.131577940025, - 0.124861034025, - 0.12557717502500002, - 0.12896975402500002, - 0.12487534102499999, - 0.123734583025, - 0.12556551402500002, - 0.12789639302500003, - 0.129524043025, - 0.127940503025, - 0.12444353402500001, - 0.12552950702500001, - 0.129136937025, - 0.125623067025, - 0.12852164402500002, - 0.124284457025, - 0.12471930202499999, - 0.12654868402500002, - 0.12582711002500002, - 0.12481392502500001, - 0.12593288302500003, - 0.127950950025, - 0.125107395025 - ] - } - ] -} diff --git a/benchmark/data/nested_property.json b/benchmark/data/nested_property.json deleted file mode 100644 index f9b72d92..00000000 --- a/benchmark/data/nested_property.json +++ /dev/null @@ -1,452 +0,0 @@ -{ - "results": [ - { - "command": "daselv2 -f benchmark/data.json 'user.name.first'", - "mean": 0.006487652180000001, - "stddev": 0.00024423167525266897, - "median": 0.006445644290000002, - "user": 0.003428965, - "system": 0.0019433850000000006, - "min": 0.006095448290000001, - "max": 0.0073182502900000015, - "times": [ - 0.006831172290000001, - 0.006747688290000001, - 0.006431944290000002, - 0.006095448290000001, - 0.0068636282900000015, - 0.006326370290000003, - 0.006240141290000003, - 0.006234264290000001, - 0.0064460912900000025, - 0.006748853290000002, - 0.006574874290000001, - 0.006445601290000002, - 0.0062569812900000014, - 0.006912480290000002, - 0.006396435290000002, - 0.006361998290000002, - 0.006593969290000001, - 0.006442816290000002, - 0.006294693290000002, - 0.006737553290000002, - 0.006523573290000003, - 0.006657498290000003, - 0.006547728290000002, - 0.006445687290000002, - 0.006576812290000003, - 0.006292208290000002, - 0.006404524290000001, - 0.006124009290000001, - 0.006472860290000002, - 0.006285672290000002, - 0.006710900290000002, - 0.006383344290000002, - 0.006260324290000003, - 0.0061664112900000016, - 0.0064424132900000024, - 0.006538858290000001, - 0.006285781290000002, - 0.0063825252900000014, - 0.006209047290000002, - 0.0069047392900000015, - 0.006824828290000001, - 0.006356151290000002, - 0.006280992290000002, - 0.0065193832900000016, - 0.0065049282900000015, - 0.006443101290000001, - 0.006304665290000002, - 0.006197614290000002, - 0.006482574290000001, - 0.006546075290000002, - 0.006921652290000002, - 0.0064951202900000015, - 0.006550217290000002, - 0.006747768290000003, - 0.007300800290000003, - 0.006683423290000002, - 0.006549836290000001, - 0.006398740290000001, - 0.006501656290000001, - 0.006212342290000002, - 0.006276076290000002, - 0.0061985592900000025, - 0.006132589290000002, - 0.006110080290000002, - 0.006380609290000002, - 0.006570782290000002, - 0.006505264290000002, - 0.006190007290000002, - 0.006595402290000002, - 0.0063085012900000025, - 0.006312516290000002, - 0.006099382290000002, - 0.006273319290000003, - 0.006622400290000002, - 0.006402412290000001, - 0.006377549290000001, - 0.006488725290000002, - 0.006336644290000002, - 0.006903563290000001, - 0.0065346702900000025, - 0.006507768290000002, - 0.006706909290000002, - 0.0065434882900000015, - 0.0068588362900000015, - 0.0073182502900000015, - 0.006871688290000002, - 0.006584755290000003, - 0.006251557290000001, - 0.006458628290000002, - 0.006792576290000001, - 0.0064006102900000025, - 0.006258148290000001, - 0.0064059592900000024, - 0.006847130290000001, - 0.006784295290000002, - 0.006404388290000002, - 0.006322567290000003, - 0.006361548290000001, - 0.006244743290000002, - 0.006802520290000003 - ] - }, - { - "command": "dasel -f benchmark/data.json '.user.name.first'", - "mean": 0.008322260480000005, - "stddev": 0.00034664275213069337, - "median": 0.008256586790000003, - "user": 0.0045410749999999995, - "system": 0.0025487450000000007, - "min": 0.007858434290000002, - "max": 0.009907730290000001, - "times": [ - 0.008216180290000001, - 0.008447594290000002, - 0.008486039290000002, - 0.008090117290000002, - 0.008021255290000003, - 0.008191652290000002, - 0.008335455290000002, - 0.008229900290000002, - 0.008366511290000002, - 0.008381936290000003, - 0.008263803290000002, - 0.008103882290000001, - 0.008273973290000002, - 0.008775878290000002, - 0.008382195290000002, - 0.008433673290000002, - 0.008032323290000002, - 0.008254274290000002, - 0.008194083290000003, - 0.008314864290000001, - 0.008138005290000002, - 0.008066073290000001, - 0.008737797290000002, - 0.008658454290000003, - 0.008157911290000002, - 0.008668914290000001, - 0.008282954290000002, - 0.008087636290000002, - 0.008069797290000002, - 0.008026231290000001, - 0.008492900290000003, - 0.008364670290000003, - 0.008425305290000002, - 0.008051114290000001, - 0.008353079290000003, - 0.008137967290000003, - 0.007957766290000001, - 0.007858434290000002, - 0.008633432290000001, - 0.008387260290000002, - 0.008203868290000003, - 0.008083204290000002, - 0.008058489290000002, - 0.008374324290000002, - 0.008509135290000002, - 0.008003664290000002, - 0.007890454290000001, - 0.008354729290000001, - 0.008230575290000002, - 0.008513773290000002, - 0.008194116290000003, - 0.008182666290000001, - 0.007988283290000002, - 0.009339137290000002, - 0.009907730290000001, - 0.009511054290000003, - 0.008261410290000002, - 0.008424228290000002, - 0.008415410290000001, - 0.008552510290000002, - 0.008186474290000002, - 0.008291928290000002, - 0.008358543290000003, - 0.008182654290000002, - 0.008486117290000003, - 0.008310434290000002, - 0.008152130290000002, - 0.009259653290000003, - 0.008158226290000003, - 0.008030379290000002, - 0.007888863290000003, - 0.007978166290000002, - 0.008561305290000001, - 0.008652051290000002, - 0.008566420290000001, - 0.007902313290000003, - 0.008605550290000001, - 0.008329847290000003, - 0.008017567290000001, - 0.008258899290000002, - 0.008486068290000003, - 0.008411088290000002, - 0.008252188290000002, - 0.008019006290000002, - 0.009370380290000003, - 0.008481246290000003, - 0.008136593290000002, - 0.008089808290000002, - 0.008063672290000002, - 0.008190584290000002, - 0.008325156290000002, - 0.008216602290000001, - 0.008105132290000002, - 0.007952897290000002, - 0.008087382290000002, - 0.007942431290000001, - 0.008128853290000002, - 0.008295839290000002, - 0.008954371290000002, - 0.008189159290000003 - ] - }, - { - "command": "jq '.user.name.first' benchmark/data.json", - "mean": 0.028171564310000008, - "stddev": 0.0009405597556538961, - "median": 0.027870227290000004, - "user": 0.023672114999999997, - "system": 0.0010968650000000003, - "min": 0.027035319290000002, - "max": 0.03294296129, - "times": [ - 0.028135227290000002, - 0.027841496290000003, - 0.027634541290000002, - 0.028139402290000004, - 0.029014549290000004, - 0.029694246290000004, - 0.027494167290000003, - 0.027035319290000002, - 0.028340424290000006, - 0.027805572290000004, - 0.02790869429, - 0.027765366290000003, - 0.027200904290000005, - 0.028719174290000003, - 0.02766834329, - 0.027562324290000002, - 0.027631049290000004, - 0.02862848129, - 0.028577670290000003, - 0.029810451290000005, - 0.027981760290000005, - 0.02785606729, - 0.02783750729, - 0.028133694290000004, - 0.028495573290000006, - 0.027827889290000005, - 0.02796772729, - 0.027921327290000006, - 0.03144434729, - 0.027657399290000002, - 0.027790770290000003, - 0.027381074290000005, - 0.028586187290000002, - 0.027706438290000003, - 0.027842664290000006, - 0.028953905290000005, - 0.028802964290000004, - 0.02804459529, - 0.027851360290000002, - 0.030459600290000007, - 0.027607017290000004, - 0.028068594290000005, - 0.027892314290000005, - 0.028163632290000004, - 0.027624205290000002, - 0.027616289290000002, - 0.027523722290000004, - 0.027875247290000002, - 0.028187214290000005, - 0.027697603290000004, - 0.03004398229, - 0.028111386290000005, - 0.027902625290000004, - 0.02782274329, - 0.027908481290000002, - 0.02822099829, - 0.027865207290000006, - 0.027674405290000002, - 0.027543510290000003, - 0.028023704290000007, - 0.028422084290000003, - 0.028591124290000006, - 0.02789644729, - 0.02809500929, - 0.027802746290000006, - 0.027835874290000007, - 0.02813325029, - 0.028921283290000002, - 0.028330407290000005, - 0.02814936129, - 0.027675654290000004, - 0.027654205290000004, - 0.027860719290000002, - 0.03294296129, - 0.02781811529, - 0.027434146290000003, - 0.027599300290000003, - 0.027427154290000002, - 0.027447405290000004, - 0.02900386329, - 0.027739355290000003, - 0.027714250290000006, - 0.02760863729, - 0.02793279429, - 0.028196587290000005, - 0.02767645129, - 0.028033950290000005, - 0.027682183290000002, - 0.031910497290000006, - 0.027786269290000006, - 0.02794948329, - 0.027568970290000005, - 0.027483134290000003, - 0.02771581429, - 0.027842668290000004, - 0.02841784329, - 0.03082046729, - 0.02789747729, - 0.027788662290000002, - 0.027826680290000003 - ] - }, - { - "command": "yq --yaml-output '.user.name.first' benchmark/data.yaml", - "mean": 0.12665368810000005, - "stddev": 0.0021426641685805604, - "median": 0.12598725429000002, - "user": 0.100011725, - "system": 0.022276244999999997, - "min": 0.12445907229, - "max": 0.13821544329, - "times": [ - 0.12536145529, - 0.12544597029000001, - 0.12480518729000001, - 0.12563508729, - 0.12545197529, - 0.12732867729, - 0.12500454829, - 0.12613088929, - 0.12552378829, - 0.12462444029, - 0.12538138629, - 0.13174731829, - 0.12729835829, - 0.12591004529, - 0.12547844429, - 0.12481067929, - 0.12542326529, - 0.12745116929, - 0.12895119829, - 0.12666287729, - 0.12563772129, - 0.12583449529000001, - 0.12946466229, - 0.12533437429, - 0.12686138029, - 0.12709797529, - 0.12827146829, - 0.12785409529, - 0.12688063329, - 0.12530266729, - 0.12472259028999999, - 0.12516347629, - 0.12629098229, - 0.12590087029, - 0.12713439629, - 0.12503758229, - 0.12445907229, - 0.12771501629, - 0.12641518629, - 0.12457429629, - 0.12864072429, - 0.12457474329000001, - 0.12671371329, - 0.12464369729, - 0.12510077129, - 0.12494224629, - 0.12565757929000002, - 0.12621914329, - 0.12981714529, - 0.12531311929, - 0.12672209029, - 0.12655298829, - 0.12571011429, - 0.12570983129, - 0.12532608029, - 0.12583801529, - 0.12466616928999999, - 0.12895168929, - 0.13051969829, - 0.12770234229, - 0.12708315429, - 0.12768032729, - 0.12612925929, - 0.12758765529, - 0.13092517529, - 0.12879533229, - 0.12821580329, - 0.12497348229, - 0.12515832629, - 0.12481059728999999, - 0.12612197929, - 0.12967891629, - 0.12661266229, - 0.13029565129, - 0.12650265929, - 0.12910420729, - 0.13088482929, - 0.12452952529, - 0.12587831729, - 0.12466936129, - 0.12698203129, - 0.12514577529, - 0.12563987629, - 0.12710649329, - 0.12688229029, - 0.12800063429, - 0.12585519229, - 0.12688132429, - 0.12454633529, - 0.12546563929000001, - 0.12530741029, - 0.12501238529, - 0.12504354929, - 0.12462488729000001, - 0.12606446329, - 0.12464945729000002, - 0.12808851229, - 0.13319983729, - 0.13821544329, - 0.12731844629 - ] - } - ] -} diff --git a/benchmark/data/overwrite_object.json b/benchmark/data/overwrite_object.json deleted file mode 100644 index 34955558..00000000 --- a/benchmark/data/overwrite_object.json +++ /dev/null @@ -1,452 +0,0 @@ -{ - "results": [ - { - "command": "daselv2 put -f benchmark/data.json -o - -t json -v '{\"first\":\"Frank\",\"last\":\"Jones\"}' 'user.name'", - "mean": 0.006341373460000002, - "stddev": 0.00025171601114701303, - "median": 0.006286778339999999, - "user": 0.0034100350000000014, - "system": 0.0018213249999999997, - "min": 0.005981926339999999, - "max": 0.007235230339999999, - "times": [ - 0.006233814339999999, - 0.006371497339999999, - 0.00656039334, - 0.00625557834, - 0.00616375334, - 0.006115574339999999, - 0.0063470583399999995, - 0.0070183893399999985, - 0.00639418034, - 0.0063792083399999994, - 0.006037023339999999, - 0.006110430339999999, - 0.006430860339999999, - 0.006133216339999999, - 0.006313927339999999, - 0.006303803339999999, - 0.006427428339999999, - 0.0064329383399999995, - 0.006263417339999999, - 0.00625479034, - 0.005997828339999999, - 0.006180127339999999, - 0.00643621034, - 0.006277918339999999, - 0.006193547339999999, - 0.006083440339999999, - 0.00639727434, - 0.006354280339999999, - 0.00697133334, - 0.00639906134, - 0.00607994934, - 0.006134735339999999, - 0.00629563834, - 0.00621280934, - 0.00619897734, - 0.00612636934, - 0.006450544339999999, - 0.0069621083399999985, - 0.006539925339999999, - 0.00630446734, - 0.005981926339999999, - 0.006406678339999999, - 0.0066075293399999995, - 0.006880499339999999, - 0.00616346934, - 0.006171480339999999, - 0.006258659339999998, - 0.00608151434, - 0.006234356339999999, - 0.00635193534, - 0.006218463339999999, - 0.006272819339999999, - 0.006316442339999999, - 0.00658866434, - 0.006216271339999999, - 0.00606850334, - 0.00647316534, - 0.00624359834, - 0.0066381353399999985, - 0.00603157134, - 0.006296354339999999, - 0.006425741339999999, - 0.006197608339999999, - 0.006221534339999999, - 0.006118788339999999, - 0.006270434339999999, - 0.0066267123399999985, - 0.006375894339999999, - 0.007235230339999999, - 0.006158178339999999, - 0.00616178834, - 0.006694705339999999, - 0.006355839339999999, - 0.006536538339999998, - 0.0060859433399999985, - 0.006160449339999999, - 0.006437025339999999, - 0.006253797339999999, - 0.00630721934, - 0.00665190534, - 0.0059899193399999984, - 0.006478852339999999, - 0.006113119339999999, - 0.006436469339999999, - 0.005990861339999999, - 0.0061798103399999995, - 0.006978912339999999, - 0.0071938373399999984, - 0.00625667934, - 0.006313264339999999, - 0.006245034339999999, - 0.00645978834, - 0.006516215339999999, - 0.006189350339999998, - 0.006160162339999999, - 0.006315909339999998, - 0.006576675339999999, - 0.00641240834, - 0.00619732334, - 0.00621155434 - ] - }, - { - "command": "dasel put document -f benchmark/data.json -o - -d json '.user.name' '{\"first\":\"Frank\",\"last\":\"Jones\"}'", - "mean": 0.008305120169999996, - "stddev": 0.0003055619245520301, - "median": 0.00824025334, - "user": 0.004561955, - "system": 0.0024566849999999993, - "min": 0.007826440339999999, - "max": 0.00956123634, - "times": [ - 0.00815616334, - 0.00842398634, - 0.008201165339999998, - 0.00842376934, - 0.00851224634, - 0.00817089434, - 0.00797620734, - 0.00837247434, - 0.00814944234, - 0.008061429339999999, - 0.008073310339999999, - 0.00834033234, - 0.00855247134, - 0.00861009634, - 0.00827284534, - 0.00816883534, - 0.008226205339999999, - 0.00823579534, - 0.008420996339999999, - 0.00815685934, - 0.00848492834, - 0.008241168339999999, - 0.007959077339999999, - 0.00812036934, - 0.00838146134, - 0.00834674534, - 0.00854426834, - 0.008997975339999999, - 0.00888003834, - 0.00830601634, - 0.00815826634, - 0.00823933834, - 0.008825796339999999, - 0.008247047339999999, - 0.00817231934, - 0.007826440339999999, - 0.008487560339999999, - 0.008506216339999999, - 0.008029968339999999, - 0.008215666339999999, - 0.008447306339999999, - 0.008218668339999999, - 0.008063742339999999, - 0.007912756339999999, - 0.008356535339999999, - 0.00821390034, - 0.00837144234, - 0.00794838734, - 0.00798794234, - 0.00843467734, - 0.00844300634, - 0.007995561339999999, - 0.007987371339999999, - 0.00825739734, - 0.00851154834, - 0.008477175339999999, - 0.009025570339999999, - 0.00868199834, - 0.008182787339999999, - 0.00789868534, - 0.008099541339999999, - 0.00956123634, - 0.00813962134, - 0.00814225134, - 0.00803994334, - 0.00830310034, - 0.00863678934, - 0.00810758734, - 0.007928605339999999, - 0.00805092034, - 0.00812647834, - 0.00807571134, - 0.00796316534, - 0.00834672634, - 0.00838763134, - 0.00824460634, - 0.008008575339999999, - 0.00804327534, - 0.008130260339999999, - 0.00856786834, - 0.007986876339999999, - 0.00826565334, - 0.00852794434, - 0.00876601634, - 0.00858587934, - 0.009037517339999999, - 0.009427813339999999, - 0.008305326339999999, - 0.008079328339999999, - 0.008006308339999999, - 0.008327199339999999, - 0.00848087034, - 0.008293095339999999, - 0.00804069834, - 0.008201009339999999, - 0.00816989334, - 0.00834348834, - 0.00811271634, - 0.00813625134, - 0.008621583339999999 - ] - }, - { - "command": "jq '.user.name = {\"first\":\"Frank\",\"last\":\"Jones\"}' benchmark/data.json", - "mean": 0.028219372960000003, - "stddev": 0.0009514379323784507, - "median": 0.027847021340000003, - "user": 0.023802905, - "system": 0.0010287949999999999, - "min": 0.027172288340000003, - "max": 0.03170986134, - "times": [ - 0.027836747340000002, - 0.029546909340000004, - 0.030286126340000004, - 0.028043875340000003, - 0.029906048340000005, - 0.028626713340000005, - 0.031294193340000005, - 0.02897921834, - 0.02741279334, - 0.02761746734, - 0.027775631340000002, - 0.027353716340000004, - 0.027396287340000002, - 0.02801330734, - 0.027298554340000003, - 0.027683096340000005, - 0.02773906334, - 0.027172288340000003, - 0.02874248434, - 0.027897162340000003, - 0.028742514340000004, - 0.027563145340000005, - 0.02779021334, - 0.02754357734, - 0.029230977340000006, - 0.02752237634, - 0.027928665340000006, - 0.027670670340000002, - 0.027573084340000005, - 0.02723672434, - 0.02755409834, - 0.02805305634, - 0.027633621340000004, - 0.027534345340000002, - 0.02770430234, - 0.027501010340000003, - 0.02812557334, - 0.027991434340000003, - 0.02819818534, - 0.027619045340000002, - 0.027507159340000004, - 0.027520404340000002, - 0.02787176334, - 0.028829086340000004, - 0.02938263234, - 0.02725168534, - 0.02765125534, - 0.02757334234, - 0.02744664134, - 0.027457173340000002, - 0.027729292340000003, - 0.02734225934, - 0.030621315340000003, - 0.028711967340000005, - 0.02812930434, - 0.02766086734, - 0.02902451734, - 0.02788596434, - 0.027809459340000002, - 0.027857295340000005, - 0.02765912634, - 0.027516572340000003, - 0.02773792134, - 0.02868376434, - 0.028573131340000002, - 0.03001327434, - 0.03170986134, - 0.02945307734, - 0.028157527340000002, - 0.02918236734, - 0.027910540340000005, - 0.02783274434, - 0.028783700340000003, - 0.027521628340000003, - 0.028661903340000003, - 0.02786713934, - 0.02890315634, - 0.030401162340000003, - 0.027814426340000004, - 0.03107529834, - 0.02764136934, - 0.02737561834, - 0.02752955134, - 0.02786980134, - 0.02770941734, - 0.028435474340000003, - 0.027321903340000002, - 0.02773080134, - 0.027691142340000002, - 0.027764648340000003, - 0.028251598340000003, - 0.028448460340000002, - 0.027352670340000003, - 0.02733155434, - 0.029367740340000004, - 0.028063202340000003, - 0.028345224340000003, - 0.029124822340000002, - 0.029439972340000002, - 0.02908231034 - ] - }, - { - "command": "yq --yaml-output '.user.name = {\"first\":\"Frank\",\"last\":\"Jones\"}' benchmark/data.yaml", - "mean": 0.12746747059000002, - "stddev": 0.0025282221610687414, - "median": 0.12681947284, - "user": 0.10092174500000001, - "system": 0.022513125000000002, - "min": 0.12456903434000001, - "max": 0.14375962434, - "times": [ - 0.12654605234000002, - 0.12575955934000002, - 0.12708964234, - 0.12647232434000003, - 0.12470873034, - 0.12843976134, - 0.12656671734000002, - 0.12634188334000002, - 0.12881208834000002, - 0.12740605434000002, - 0.12891338934000002, - 0.12694720834, - 0.12576680034, - 0.12737960634, - 0.12615119534000002, - 0.12987429334, - 0.12530916434000003, - 0.12599174034000002, - 0.12836345734000001, - 0.12587978834000002, - 0.13604959434000002, - 0.14375962434, - 0.12991076934, - 0.13063876334000002, - 0.12568143134, - 0.12641532034000003, - 0.12511058934000002, - 0.12653845534000002, - 0.12651077634000002, - 0.12668432834, - 0.12760148634000001, - 0.12689729634000002, - 0.12553759234, - 0.12653033234000002, - 0.12632168734000002, - 0.12785821434000003, - 0.12731149434000003, - 0.12891333234000002, - 0.12743041234000002, - 0.12680749034000002, - 0.12473336634, - 0.12483021834000001, - 0.13007386834, - 0.12909832034000002, - 0.12901733434, - 0.12940269134000001, - 0.12715191034, - 0.12851855334, - 0.12762605134000002, - 0.13077005034000003, - 0.12568601534, - 0.12738594234, - 0.13162889034000003, - 0.12515017434, - 0.12653772634000002, - 0.12557007734, - 0.12547031334000003, - 0.12695055034000002, - 0.12790071834000002, - 0.12543737734000002, - 0.12555533434000002, - 0.12951044434, - 0.12769166334, - 0.12759169534, - 0.12788730234, - 0.12782468734000002, - 0.12683145534, - 0.12511537234, - 0.12804161634000003, - 0.12652413134, - 0.12640579034000002, - 0.12822792534000002, - 0.12963931134, - 0.12901060434000003, - 0.13053810434000002, - 0.12599571734, - 0.12660204534000002, - 0.12567934634000003, - 0.12635418034, - 0.12623169334, - 0.13373038934, - 0.12688536134, - 0.12535313234, - 0.12599578534000003, - 0.12655190034000002, - 0.12980068434000003, - 0.12456903434000001, - 0.12730251034, - 0.12560139734, - 0.12819560834000002, - 0.12594822634000002, - 0.12569246734, - 0.13085695134000003, - 0.12570623234, - 0.12652511234000002, - 0.12822100734000003, - 0.12797505034, - 0.12645839334, - 0.12647544334000002, - 0.12547535334 - ] - } - ] -} diff --git a/benchmark/data/root_object.json b/benchmark/data/root_object.json deleted file mode 100644 index 51792846..00000000 --- a/benchmark/data/root_object.json +++ /dev/null @@ -1,452 +0,0 @@ -{ - "results": [ - { - "command": "daselv2 -f benchmark/data.json", - "mean": 0.006560224705000003, - "stddev": 0.00023596029237584623, - "median": 0.006540232895000001, - "user": 0.0034437749999999987, - "system": 0.0019316349999999993, - "min": 0.006116507395000002, - "max": 0.007203835395000002, - "times": [ - 0.006567834395000001, - 0.006594772395000002, - 0.006734405395, - 0.006556529395000002, - 0.006576964395000001, - 0.006520937395000001, - 0.006330649395, - 0.006530635395000001, - 0.006985826395000002, - 0.006740282395000002, - 0.007102037395000002, - 0.006566879395000001, - 0.006835496395000001, - 0.0066586773950000015, - 0.0063945843950000015, - 0.006224949395000002, - 0.006378850395000002, - 0.006632221395, - 0.0064963383950000005, - 0.0062735653950000015, - 0.006130217395000002, - 0.006196721395000002, - 0.006628445395000002, - 0.006538455395000002, - 0.006433977395000002, - 0.006520898395, - 0.0065377473950000015, - 0.0063974073950000005, - 0.006752623395000001, - 0.006296350395000001, - 0.006631488395000001, - 0.006285373395000002, - 0.006788259395000001, - 0.007078114395000001, - 0.0064901753950000005, - 0.006467350395000002, - 0.006440913395000001, - 0.006580683395000002, - 0.006485750395000002, - 0.006470874395000001, - 0.006441796395000001, - 0.006265928395000002, - 0.007203835395000002, - 0.0069039733950000005, - 0.006491220395, - 0.006489775395000001, - 0.0063583893950000005, - 0.006617871395000001, - 0.006899467395000001, - 0.006812983395000001, - 0.006350959395000001, - 0.006616313395000002, - 0.006726820395000001, - 0.006414514395000001, - 0.006447211395000001, - 0.006290211395000002, - 0.006354181395000002, - 0.006289226395000002, - 0.007063264395000001, - 0.006520625395000002, - 0.006211097395000002, - 0.006624722395000001, - 0.006453677395000001, - 0.006784115395000002, - 0.006542010395000001, - 0.006609316395000002, - 0.006722085395000001, - 0.006538371395000001, - 0.006367511395000001, - 0.006116507395000002, - 0.006229110395000001, - 0.006638824395000001, - 0.006497498395000001, - 0.006404678395000002, - 0.006885262395000001, - 0.006571402395000002, - 0.0065670713950000004, - 0.007016068395000002, - 0.006807108395000001, - 0.006472902395000001, - 0.006336071395000002, - 0.006645642395000002, - 0.006705204395000001, - 0.0066509223950000015, - 0.006241939395000002, - 0.006874149395000001, - 0.0066784303950000005, - 0.006571269395000001, - 0.006628870395000002, - 0.006159877395000001, - 0.006377138395000001, - 0.006182463395000001, - 0.007065109395000001, - 0.006392855395000001, - 0.006639454395000001, - 0.006266390395000002, - 0.0066228413950000006, - 0.006899119395000002, - 0.006809273395000001, - 0.006803273395000002 - ] - }, - { - "command": "dasel -f benchmark/data.json", - "mean": 0.008721853545000002, - "stddev": 0.0005206001180551711, - "median": 0.008610990895, - "user": 0.004674445, - "system": 0.0027560949999999996, - "min": 0.008019583395000002, - "max": 0.010257668395, - "times": [ - 0.008834420395, - 0.008388022395000002, - 0.010141051395000001, - 0.008525065395000002, - 0.008616365395, - 0.008210266395, - 0.008278044395000001, - 0.008385545395000001, - 0.008949411395, - 0.008274936395000002, - 0.008245365395, - 0.008228149395000002, - 0.008847754395, - 0.008805193395, - 0.008403587395000002, - 0.010132482395000001, - 0.009021594395, - 0.008189383395000001, - 0.010038834395000001, - 0.009145862395, - 0.008870982395000001, - 0.008397042395000002, - 0.009557629395000001, - 0.009349802395, - 0.008978578395, - 0.008342795395000002, - 0.008244044395, - 0.008351229395000001, - 0.008872860395000001, - 0.008325318395000001, - 0.008767890395000002, - 0.008764965395000001, - 0.008931943395000001, - 0.008482727395000002, - 0.008161548395, - 0.008311093395000002, - 0.008711631395, - 0.008402891395, - 0.008166006395000001, - 0.008605616395000001, - 0.008412710395000002, - 0.008401700395000001, - 0.008070974395000001, - 0.008465424395, - 0.008588913395000002, - 0.008737300395000001, - 0.008284441395000001, - 0.009965046395000001, - 0.008556311395000002, - 0.009200428395000002, - 0.008620994395000001, - 0.008780138395000001, - 0.008413018395000001, - 0.008839649395000001, - 0.008325368395000001, - 0.009079632395000002, - 0.009514870395000001, - 0.008480242395, - 0.008019583395000002, - 0.008322638395000002, - 0.008702758395000001, - 0.008996964395000001, - 0.008519861395000002, - 0.008650518395, - 0.008706880395000002, - 0.008775020395000001, - 0.008179697395000001, - 0.008794986395000001, - 0.008321829395, - 0.009842835395000001, - 0.008152023395000001, - 0.008360102395000001, - 0.008749333395000001, - 0.009138742395000002, - 0.008219834395000002, - 0.009502691395000002, - 0.008683299395000002, - 0.010257668395, - 0.008203278395000001, - 0.009986183395, - 0.008509681395000001, - 0.009044321395, - 0.008361539395000001, - 0.008857812395000001, - 0.008535987395000002, - 0.009176784395000002, - 0.008320130395000002, - 0.010073778395000002, - 0.008711177395000002, - 0.008778289395000001, - 0.008394469395000001, - 0.008546210395000002, - 0.008297527395, - 0.008947252395, - 0.008129016395, - 0.008299666395, - 0.008681595395000002, - 0.009252908395000002, - 0.008271906395, - 0.008913471395 - ] - }, - { - "command": "jq '.' benchmark/data.json", - "mean": 0.028097767815000004, - "stddev": 0.0007492098365173988, - "median": 0.027899321895000002, - "user": 0.023576415000000003, - "system": 0.0010904649999999999, - "min": 0.026982224395000004, - "max": 0.031510457395, - "times": [ - 0.031510457395, - 0.028370955395000005, - 0.027849638395000004, - 0.027802898395, - 0.027629859395, - 0.028092254395000005, - 0.027679744395000004, - 0.027428224395000003, - 0.027940475395, - 0.027461962395, - 0.027572239395000003, - 0.028725923395000002, - 0.027846106395000004, - 0.027799792395000005, - 0.028047964395000003, - 0.027897221395, - 0.027865078395000003, - 0.027859685395000004, - 0.027876710395, - 0.028298880395, - 0.027951727395000004, - 0.028172074395, - 0.027616521395000003, - 0.027849657395, - 0.028517203395, - 0.027594388395000002, - 0.029102616395, - 0.027960897395, - 0.027853847395000002, - 0.027693930395, - 0.027448496395, - 0.027872028395, - 0.028529754395000006, - 0.027508139395000002, - 0.027901422395000004, - 0.027414456395, - 0.027819005395000002, - 0.027598881395000004, - 0.029974354395000003, - 0.027704042395, - 0.027495236395, - 0.027975416395000004, - 0.029172349395, - 0.028630498395000006, - 0.027506106395000004, - 0.027387203395000004, - 0.027950992395000002, - 0.028265581395, - 0.027910680395, - 0.028000458395, - 0.027881816395000005, - 0.027659263395, - 0.028334439395000004, - 0.028853144395000006, - 0.027948113395000005, - 0.028215505395000003, - 0.027185575395, - 0.027569745395, - 0.027707447395000005, - 0.027600329395, - 0.028135097395, - 0.028406717395, - 0.028145914395000003, - 0.028554875395000002, - 0.027382387395000003, - 0.028914260395000006, - 0.028067894395000004, - 0.027486960395000003, - 0.027607274395000003, - 0.028054896395000004, - 0.027780814395000002, - 0.028686025395000005, - 0.027827651395000003, - 0.027913641395000004, - 0.027530665395, - 0.027876622395000005, - 0.027968138395000005, - 0.027856587395000004, - 0.027644859395000002, - 0.027685054395000002, - 0.027595011395000004, - 0.028630052395000005, - 0.029345750395000005, - 0.029136327395000005, - 0.028887625395, - 0.031398376395, - 0.028032640395000006, - 0.027963829395000004, - 0.028004414395, - 0.027939705395000004, - 0.027589536395000003, - 0.028681414395000004, - 0.027627449395000003, - 0.026982224395000004, - 0.028161376395000003, - 0.027592776395, - 0.027453757395000004, - 0.030623943395000002, - 0.028563546395000004, - 0.029151266395000004 - ] - }, - { - "command": "yq --yaml-output '.' benchmark/data.yaml", - "mean": 0.127936466745, - "stddev": 0.0030504504074422385, - "median": 0.12732412939499999, - "user": 0.10109137500000007, - "system": 0.022681795000000008, - "min": 0.12454818439500003, - "max": 0.151617090395, - "times": [ - 0.126020138395, - 0.127092934395, - 0.125877971395, - 0.127085930395, - 0.125176883395, - 0.127202369395, - 0.131799086395, - 0.128546751395, - 0.128087112395, - 0.126591394395, - 0.127841590395, - 0.125481516395, - 0.129833084395, - 0.126245308395, - 0.125842218395, - 0.126434407395, - 0.130430079395, - 0.127171833395, - 0.126081132395, - 0.12815765839499998, - 0.126159084395, - 0.127363824395, - 0.129321385395, - 0.130149975395, - 0.126854810395, - 0.128767807395, - 0.126770359395, - 0.151617090395, - 0.127874442395, - 0.127236020395, - 0.129352732395, - 0.128764454395, - 0.125533678395, - 0.126200755395, - 0.12596059639499999, - 0.128172710395, - 0.125902279395, - 0.125206180395, - 0.128549377395, - 0.127280055395, - 0.128536370395, - 0.126439517395, - 0.126663426395, - 0.128162620395, - 0.12630688339499999, - 0.132538141395, - 0.130066040395, - 0.129044376395, - 0.127797294395, - 0.129187813395, - 0.129096153395, - 0.129613741395, - 0.129271941395, - 0.126403193395, - 0.13138635939499999, - 0.126705557395, - 0.127695397395, - 0.133381583395, - 0.126624526395, - 0.12454818439500003, - 0.127930087395, - 0.127235159395, - 0.127284434395, - 0.125313427395, - 0.130966019395, - 0.130876592395, - 0.126365450395, - 0.128947098395, - 0.130258513395, - 0.133456651395, - 0.125279712395, - 0.127611447395, - 0.126431386395, - 0.125592156395, - 0.129050438395, - 0.13162539139499999, - 0.125723364395, - 0.127783610395, - 0.127395588395, - 0.126037908395, - 0.125753619395, - 0.125909852395, - 0.130244560395, - 0.125210748395, - 0.126058809395, - 0.127099143395, - 0.128182861395, - 0.125297908395, - 0.128088664395, - 0.125362466395, - 0.12823981239499999, - 0.127251429395, - 0.126552255395, - 0.125944000395, - 0.127958247395, - 0.127579846395, - 0.129828011395, - 0.127573313395, - 0.127948911395, - 0.126891632395 - ] - } - ] -} diff --git a/benchmark/data/top_level_property.json b/benchmark/data/top_level_property.json deleted file mode 100644 index 940f3f2c..00000000 --- a/benchmark/data/top_level_property.json +++ /dev/null @@ -1,452 +0,0 @@ -{ - "results": [ - { - "command": "daselv2 -f benchmark/data.json 'id'", - "mean": 0.006550416384999996, - "stddev": 0.00024601099511433844, - "median": 0.006507407264999997, - "user": 0.0034442450000000003, - "system": 0.0019110250000000007, - "min": 0.006124006764999997, - "max": 0.007435303764999996, - "times": [ - 0.006409424764999998, - 0.0067714407649999975, - 0.0067454507649999974, - 0.0064456847649999975, - 0.006382688764999997, - 0.0066826097649999965, - 0.006436487764999997, - 0.006437265764999997, - 0.006535118764999997, - 0.0066288807649999976, - 0.0070239787649999975, - 0.006495777764999997, - 0.006400754764999997, - 0.006299325764999997, - 0.006271667764999997, - 0.006837474764999997, - 0.006608922764999997, - 0.006496156764999997, - 0.006171421764999997, - 0.006360695764999997, - 0.006200042764999997, - 0.0073152667649999965, - 0.006571944764999997, - 0.006483469764999998, - 0.006305482764999997, - 0.006493079764999998, - 0.006372934764999998, - 0.006313692764999997, - 0.006124006764999997, - 0.006342037764999997, - 0.006847285764999997, - 0.0067479717649999976, - 0.006698294764999998, - 0.0062721417649999976, - 0.006291056764999997, - 0.0064289147649999965, - 0.007022751764999996, - 0.006424592764999997, - 0.006377070764999998, - 0.0065125977649999976, - 0.006405452764999996, - 0.006540477764999997, - 0.006421203764999998, - 0.006546451764999997, - 0.006511866764999997, - 0.006506857764999997, - 0.0063986647649999975, - 0.006357210764999998, - 0.006140095764999997, - 0.006626021764999997, - 0.006485051764999998, - 0.007093897764999997, - 0.006641092764999997, - 0.006593332764999998, - 0.006553825764999997, - 0.006805259764999997, - 0.0067520077649999965, - 0.006749774764999997, - 0.0062603187649999965, - 0.006552321764999997, - 0.007125473764999998, - 0.006698240764999997, - 0.006433365764999997, - 0.006395765764999997, - 0.006966909764999998, - 0.0068169367649999965, - 0.0068186487649999974, - 0.006284971764999998, - 0.006410533764999997, - 0.006626721764999998, - 0.007015845764999997, - 0.0068382977649999975, - 0.006570978764999997, - 0.006443504764999997, - 0.006627231764999996, - 0.006457617764999998, - 0.0065755757649999975, - 0.006454909764999997, - 0.006558081764999998, - 0.006756486764999996, - 0.006507956764999998, - 0.006349025764999997, - 0.006149813764999998, - 0.006326886764999998, - 0.0065465437649999975, - 0.007435303764999996, - 0.0066341607649999974, - 0.006532906764999997, - 0.006325576764999997, - 0.006516942764999997, - 0.006425754764999998, - 0.006800023764999998, - 0.006333842764999997, - 0.006432710764999997, - 0.006469709764999998, - 0.006279320764999997, - 0.006579716764999997, - 0.006486502764999997, - 0.006560812764999997, - 0.006944972764999996 - ] - }, - { - "command": "dasel -f benchmark/data.json '.id'", - "mean": 0.008324318624999995, - "stddev": 0.0002552376514065979, - "median": 0.008294593764999997, - "user": 0.004518525, - "system": 0.002555445000000001, - "min": 0.007806666764999998, - "max": 0.009730712764999998, - "times": [ - 0.007952981764999997, - 0.008194517764999997, - 0.008203830764999998, - 0.008595876764999998, - 0.008142087764999997, - 0.008135427764999998, - 0.009730712764999998, - 0.008734463764999997, - 0.008223304764999996, - 0.008183735764999997, - 0.008370059764999997, - 0.008302982764999998, - 0.008535462764999998, - 0.008113701764999997, - 0.008482074764999997, - 0.008621676764999997, - 0.008354809764999997, - 0.007806666764999998, - 0.008177029764999998, - 0.008492614764999996, - 0.008201580764999997, - 0.007996902764999998, - 0.008311124764999997, - 0.008049495764999998, - 0.008380895764999997, - 0.008294717764999997, - 0.008102381764999998, - 0.008199158764999997, - 0.008638351764999998, - 0.008187583764999997, - 0.008136487764999997, - 0.008144617764999997, - 0.008307908764999997, - 0.008227715764999997, - 0.008155664764999997, - 0.008155552764999997, - 0.008356408764999997, - 0.008557816764999997, - 0.008225594764999997, - 0.008378913764999997, - 0.008202654764999998, - 0.008514292764999997, - 0.008401451764999996, - 0.008179008764999997, - 0.008636295764999998, - 0.008434292764999997, - 0.008035087764999997, - 0.008390621764999997, - 0.008299827764999997, - 0.008476040764999998, - 0.008441905764999997, - 0.008250349764999998, - 0.008448662764999997, - 0.008302803764999997, - 0.008268075764999997, - 0.008424627764999998, - 0.008634410764999998, - 0.008369244764999997, - 0.008153790764999997, - 0.008101582764999997, - 0.008536264764999997, - 0.008568117764999997, - 0.008279750764999998, - 0.008080794764999998, - 0.008208901764999997, - 0.008098129764999996, - 0.008082657764999997, - 0.008191190764999997, - 0.008276742764999998, - 0.008360571764999998, - 0.008299517764999997, - 0.008184424764999997, - 0.008119439764999997, - 0.008448582764999997, - 0.009151519764999998, - 0.008034101764999997, - 0.008172693764999997, - 0.008511641764999998, - 0.008400721764999997, - 0.008294469764999998, - 0.008362365764999997, - 0.008344852764999997, - 0.008283781764999998, - 0.008652207764999998, - 0.008094122764999998, - 0.008244314764999996, - 0.008147705764999997, - 0.008306131764999997, - 0.007917241764999998, - 0.008461015764999998, - 0.008488163764999997, - 0.009008179764999997, - 0.008530072764999997, - 0.008096654764999997, - 0.008147692764999997, - 0.008445677764999998, - 0.008381138764999997, - 0.008254616764999997, - 0.008514989764999998, - 0.008122774764999997 - ] - }, - { - "command": "jq '.id' benchmark/data.json", - "mean": 0.028229297375000004, - "stddev": 0.0008569398182191514, - "median": 0.027930878764999997, - "user": 0.023590864999999992, - "system": 0.0011729950000000013, - "min": 0.027056496765, - "max": 0.031523936764999996, - "times": [ - 0.028150794764999994, - 0.027760064764999995, - 0.027836681764999996, - 0.027894943765, - 0.027757178764999994, - 0.030488815764999996, - 0.027491311765, - 0.027809394764999995, - 0.027333003764999995, - 0.028818708765, - 0.029067745765, - 0.027773127764999996, - 0.027503405764999996, - 0.028013080764999997, - 0.027294267764999998, - 0.027647466764999996, - 0.028569303764999998, - 0.027847931764999993, - 0.028149714765, - 0.027864605764999995, - 0.027523819765, - 0.031461682765, - 0.027526306765, - 0.027599963765, - 0.027600968764999997, - 0.027762554764999996, - 0.028441223764999995, - 0.028300975765, - 0.027928310765, - 0.028279630764999994, - 0.027056496765, - 0.028080635764999994, - 0.027795632764999995, - 0.027421897765, - 0.027929237764999996, - 0.027930041764999997, - 0.027264766764999997, - 0.027892522764999997, - 0.027643479765, - 0.028433959764999996, - 0.027405744764999997, - 0.027458264764999997, - 0.027707037764999998, - 0.027917712764999998, - 0.027500607764999997, - 0.029067875764999994, - 0.027805670765, - 0.027691401765, - 0.030940199764999995, - 0.028235278764999996, - 0.027931715764999997, - 0.027655886765, - 0.027706172764999995, - 0.028233705765, - 0.027679379764999998, - 0.027852460764999996, - 0.028040724764999997, - 0.027759307764999998, - 0.028453048764999996, - 0.028588329764999997, - 0.027728493764999994, - 0.028764716764999997, - 0.027477060765, - 0.028018997764999998, - 0.031523936764999996, - 0.028107145764999994, - 0.029134737764999995, - 0.029880420765, - 0.029364351764999998, - 0.029320509764999995, - 0.029779757764999998, - 0.029933042764999994, - 0.027654918765, - 0.027763894764999998, - 0.027450659764999996, - 0.028449209764999997, - 0.027689131765, - 0.027878022764999996, - 0.028036176764999997, - 0.027965869764999998, - 0.027947193764999996, - 0.029738297764999998, - 0.029291946765, - 0.028775971764999998, - 0.029161355764999994, - 0.028420651765, - 0.028610678764999994, - 0.029122159764999996, - 0.028301313764999995, - 0.028411058764999997, - 0.029083622764999997, - 0.028200845764999996, - 0.028657506765, - 0.027994793764999998, - 0.028099175764999998, - 0.027863432764999994, - 0.028044536764999996, - 0.027571356765, - 0.027606647765, - 0.027529947765 - ] - }, - { - "command": "yq --yaml-output '.id' benchmark/data.yaml", - "mean": 0.12835159792500003, - "stddev": 0.010089130332329561, - "median": 0.126150159265, - "user": 0.10081843500000001, - "system": 0.022555625, - "min": 0.124434808765, - "max": 0.21169749476500002, - "times": [ - 0.12693241376500003, - 0.12598398276500003, - 0.21169749476500002, - 0.17417872276500002, - 0.127540026765, - 0.125897863765, - 0.12503282976500002, - 0.126176339765, - 0.126536660765, - 0.12465192176499999, - 0.124897110765, - 0.12531811876500001, - 0.12566856176500002, - 0.12634932576500002, - 0.124660133765, - 0.12709039876500003, - 0.125437084765, - 0.128998907765, - 0.12549196076500002, - 0.12595978476500003, - 0.12588422776500002, - 0.125576465765, - 0.12951706276500002, - 0.12528365876500003, - 0.125631733765, - 0.126737662765, - 0.128495776765, - 0.127303625765, - 0.12698701176500002, - 0.12559793576500003, - 0.12674259776500002, - 0.12666975976500003, - 0.12512005876500001, - 0.12537899576500003, - 0.126250524765, - 0.124855139765, - 0.125111263765, - 0.127306627765, - 0.12721274176500003, - 0.125394333765, - 0.12569692376500002, - 0.12614967676500002, - 0.12542189076500002, - 0.124653529765, - 0.12601579276500002, - 0.12685919576500002, - 0.125027263765, - 0.126288329765, - 0.12779064276500002, - 0.12523727776500002, - 0.12568267676500003, - 0.12788820176500001, - 0.127346185765, - 0.12615064176500002, - 0.12562583876500003, - 0.124434808765, - 0.12485411476500001, - 0.12869135276500002, - 0.126116951765, - 0.12596006476500002, - 0.124653065765, - 0.12730440676500002, - 0.125092436765, - 0.131659413765, - 0.12520082176500003, - 0.12558192376500002, - 0.127521421765, - 0.126022836765, - 0.126022785765, - 0.132629669765, - 0.12944566976500002, - 0.13018733076500003, - 0.12955609176500002, - 0.13097883076500003, - 0.129508235765, - 0.13060793676500002, - 0.130792193765, - 0.12969651676500002, - 0.129121444765, - 0.12936682276500003, - 0.12605445176500002, - 0.125716564765, - 0.12550641776500002, - 0.12513027776500002, - 0.127033002765, - 0.125618012765, - 0.128243197765, - 0.15014448976500003, - 0.13062317276500002, - 0.12600829276500003, - 0.12629204276500003, - 0.13053616076500002, - 0.12630749976500003, - 0.12558466976500002, - 0.128302790765, - 0.12897018076500003, - 0.125859204765, - 0.12597339276500003, - 0.12688513176500002, - 0.12597277076500002 - ] - } - ] -} diff --git a/benchmark/data/update_string.json b/benchmark/data/update_string.json deleted file mode 100644 index 57af0cba..00000000 --- a/benchmark/data/update_string.json +++ /dev/null @@ -1,452 +0,0 @@ -{ - "results": [ - { - "command": "daselv2 put -f benchmark/data.json -t string -v 'blue' -o - 'favouriteColours.[0]'", - "mean": 0.006574598319999999, - "stddev": 0.0002567829726151101, - "median": 0.006507481229999999, - "user": 0.003468090000000002, - "system": 0.001919109999999999, - "min": 0.0060936412299999985, - "max": 0.0073594002299999985, - "times": [ - 0.006508482229999999, - 0.00667546223, - 0.0070476082299999986, - 0.00658238823, - 0.006416818229999999, - 0.007076544229999999, - 0.00644592823, - 0.006498118229999999, - 0.006772679229999998, - 0.00653950423, - 0.006447516229999999, - 0.006428137229999999, - 0.0065064802299999985, - 0.00630173723, - 0.006444141229999999, - 0.00671026223, - 0.006686136229999999, - 0.006752340229999999, - 0.006709370229999999, - 0.006816884229999999, - 0.007300495229999999, - 0.006614820229999999, - 0.00671437823, - 0.006477968229999999, - 0.006465355229999999, - 0.00670829823, - 0.006676275229999999, - 0.006331348229999999, - 0.00623990923, - 0.006823571229999999, - 0.006398838229999999, - 0.006514375229999999, - 0.006355667229999999, - 0.006181164229999999, - 0.00663012323, - 0.0072106312299999985, - 0.00670738123, - 0.006669443229999999, - 0.006284765229999999, - 0.00632453023, - 0.006371354229999999, - 0.00629729823, - 0.0063768672299999996, - 0.006166249229999999, - 0.0064860632299999985, - 0.0066769792299999995, - 0.00648823623, - 0.006977415229999999, - 0.006338235229999999, - 0.00625367223, - 0.006498987229999999, - 0.00696681423, - 0.00654343223, - 0.006321695229999999, - 0.00647472323, - 0.006673124229999999, - 0.00668090423, - 0.00643350023, - 0.00654756223, - 0.00649328823, - 0.0063800132299999986, - 0.006323642229999999, - 0.006368507229999999, - 0.00639039923, - 0.006304803229999999, - 0.007064060229999999, - 0.0070125022299999985, - 0.006231804229999999, - 0.00631734623, - 0.00644078723, - 0.0062989022299999985, - 0.006499539229999999, - 0.006852715229999999, - 0.0064666942299999985, - 0.007033320229999999, - 0.0073594002299999985, - 0.006565490229999998, - 0.006794377229999999, - 0.00627824223, - 0.0066632262299999995, - 0.006373599229999999, - 0.00646909423, - 0.006271544229999999, - 0.0064087832299999994, - 0.00661281123, - 0.006790426229999999, - 0.006676818229999999, - 0.006312856229999999, - 0.006454350229999999, - 0.006629442229999999, - 0.006737081229999999, - 0.00708443923, - 0.006822252229999999, - 0.0064669282299999985, - 0.006879202229999999, - 0.006686017229999999, - 0.00664213923, - 0.0060936412299999985, - 0.006579762229999999, - 0.006760592229999999 - ] - }, - { - "command": "dasel put string -f benchmark/data.json -o - '.favouriteColours.[0]' blue", - "mean": 0.00951898981, - "stddev": 0.0017225102963251645, - "median": 0.00854833073, - "user": 0.004997020000000001, - "system": 0.003031049999999998, - "min": 0.008015123229999999, - "max": 0.01287717023, - "times": [ - 0.00821318523, - 0.00894063423, - 0.00876873223, - 0.00888657123, - 0.00911273723, - 0.01125576623, - 0.012431395230000001, - 0.012192480229999999, - 0.012503209230000001, - 0.012336281230000001, - 0.011628729229999999, - 0.012152274230000001, - 0.01231120923, - 0.012016926229999999, - 0.01242725123, - 0.012324456230000001, - 0.01206357623, - 0.012365973229999999, - 0.012784075229999999, - 0.01270946423, - 0.01260855023, - 0.012829796230000001, - 0.01251797723, - 0.01181978323, - 0.01267650223, - 0.012214772229999999, - 0.01200814723, - 0.01256976623, - 0.01255546323, - 0.01190315323, - 0.01287717023, - 0.01136526123, - 0.008957634229999999, - 0.00929474223, - 0.00950400023, - 0.008707574229999999, - 0.008323313229999999, - 0.008269586229999999, - 0.008528736229999999, - 0.008256986229999999, - 0.00811713523, - 0.008334235229999999, - 0.00874261623, - 0.00932214923, - 0.00846270823, - 0.008244466229999999, - 0.00838076623, - 0.008200653229999999, - 0.008305744229999999, - 0.00824571023, - 0.00907160523, - 0.008437339229999999, - 0.00818395123, - 0.008185578229999999, - 0.00841649823, - 0.00845421023, - 0.008515226229999999, - 0.00844964423, - 0.008417066229999999, - 0.008494160229999999, - 0.008153554229999999, - 0.00814737923, - 0.008511383229999999, - 0.008565272229999999, - 0.00967279823, - 0.008341950229999999, - 0.008015123229999999, - 0.00855768023, - 0.00864674023, - 0.00842890423, - 0.008480120229999999, - 0.008282252229999999, - 0.00830620423, - 0.008503487229999999, - 0.00868239123, - 0.008546387229999999, - 0.008408158229999999, - 0.00832261923, - 0.00806083323, - 0.00821423223, - 0.00854229323, - 0.00856298623, - 0.008247696229999999, - 0.00832277523, - 0.00861195123, - 0.008564946229999999, - 0.008315726229999999, - 0.008485519229999999, - 0.008498016229999999, - 0.00855027423, - 0.008265312229999999, - 0.00804678123, - 0.008601419229999999, - 0.008550365229999999, - 0.00833349823, - 0.00811315023, - 0.00854384823, - 0.00910500823, - 0.008357236229999999, - 0.008237400229999999 - ] - }, - { - "command": "jq '.favouriteColours[0] = \"blue\"' benchmark/data.json", - "mean": 0.02846197351, - "stddev": 0.0012764748490390692, - "median": 0.027919681230000003, - "user": 0.023889290000000004, - "system": 0.0011117999999999998, - "min": 0.027330357229999998, - "max": 0.03306570423, - "times": [ - 0.027894430230000003, - 0.027746254230000003, - 0.027955349230000003, - 0.028067233230000002, - 0.028926768230000004, - 0.02887793223, - 0.03134301123, - 0.02824778823, - 0.028242973230000003, - 0.027930654230000004, - 0.027969887230000004, - 0.02898230023, - 0.027487028230000002, - 0.02774380523, - 0.031752768230000006, - 0.02846507223, - 0.02755052223, - 0.027330357229999998, - 0.028078946230000006, - 0.027852178230000003, - 0.02898427723, - 0.028402340230000003, - 0.02997495923, - 0.02773111123, - 0.02789157723, - 0.02789533423, - 0.028675530230000003, - 0.027815301230000006, - 0.03027325023, - 0.028190308230000002, - 0.02808739123, - 0.028776548230000004, - 0.027678795229999997, - 0.031579968230000005, - 0.030911136230000004, - 0.027804677230000005, - 0.028202950230000003, - 0.02764073023, - 0.027847816230000003, - 0.028717526230000005, - 0.03152657423, - 0.02784010723, - 0.028108562230000002, - 0.027634619229999997, - 0.027614873230000003, - 0.027599764230000004, - 0.027848408230000003, - 0.02843871823, - 0.03167757123, - 0.02759215023, - 0.027891683230000006, - 0.02759530023, - 0.028425594230000004, - 0.027640023229999998, - 0.02843564323, - 0.027787908230000005, - 0.027836637230000003, - 0.028619985230000006, - 0.02794124723, - 0.03306570423, - 0.02849251623, - 0.02789543823, - 0.029186881230000003, - 0.027905235230000006, - 0.027742234230000003, - 0.027731174230000002, - 0.02761589223, - 0.027626630229999997, - 0.02758218323, - 0.027738300230000006, - 0.03240315723, - 0.03204120323, - 0.02842035423, - 0.02798019223, - 0.02840546823, - 0.030319187230000003, - 0.02788768723, - 0.02756192223, - 0.028613107230000004, - 0.02767977723, - 0.028140408230000004, - 0.027888797230000005, - 0.027695763230000003, - 0.027959094230000002, - 0.02781614023, - 0.027399465230000003, - 0.028712661230000006, - 0.027803387230000004, - 0.027609352230000002, - 0.03216698623, - 0.02948010223, - 0.027926486230000006, - 0.027514092230000002, - 0.02826437123, - 0.02783546923, - 0.02791287623, - 0.027692035230000002, - 0.027563090230000004, - 0.027458956229999998, - 0.02788141223 - ] - }, - { - "command": "yq --yaml-output '.favouriteColours[0] = \"blue\"' benchmark/data.yaml", - "mean": 0.12731649453000007, - "stddev": 0.0026900046135057403, - "median": 0.12662870373000001, - "user": 0.10077686, - "system": 0.02249842, - "min": 0.12496164923000001, - "max": 0.14938530723000001, - "times": [ - 0.12625821323000003, - 0.12616111523, - 0.12510531923, - 0.12701650523000002, - 0.12608100623000001, - 0.12591849123, - 0.12503278123000003, - 0.12717050623, - 0.12600768123, - 0.12665182323000002, - 0.12620209523, - 0.12649509923000002, - 0.12616235623000002, - 0.12496164923000001, - 0.12947022323000001, - 0.12656453523000002, - 0.12639286023000001, - 0.12619358323000002, - 0.12659386323000002, - 0.12712297923000002, - 0.12624938623, - 0.13030876723, - 0.13051090623000003, - 0.12534885723000003, - 0.12559348023000003, - 0.12661680123000002, - 0.12593376323000002, - 0.13015959623, - 0.12880130123000003, - 0.12962229223000002, - 0.12964488423, - 0.12600318323, - 0.12605403323000003, - 0.12840993523000002, - 0.12895786023000003, - 0.12785699423000002, - 0.12964867423, - 0.14938530723000001, - 0.12571257923, - 0.12864212123000002, - 0.12620285023000002, - 0.12752929623, - 0.12694289923000002, - 0.12698428723000002, - 0.12511889823000003, - 0.12618057123, - 0.12876998023000003, - 0.12952903623, - 0.13148400723, - 0.12707122923000003, - 0.12702110423000001, - 0.12565490323, - 0.12687027923000002, - 0.12593925023000002, - 0.12561652723000002, - 0.12744777623, - 0.12642652323, - 0.12666415923000002, - 0.12550447523000002, - 0.12541628423, - 0.12748333623000002, - 0.12669978223, - 0.12758566723, - 0.12929110823, - 0.12837133323000002, - 0.12598774223, - 0.12829949723, - 0.12757209623000001, - 0.12573498523, - 0.12750033723, - 0.12554053823000003, - 0.12603109123, - 0.12845883323, - 0.12638048823, - 0.12605912923, - 0.12524437823, - 0.12578552023, - 0.13052670723, - 0.12793777023000003, - 0.12600084823000002, - 0.12640200023, - 0.12654134723000002, - 0.12743096223000003, - 0.12660315223000002, - 0.12597433923, - 0.13026696923000003, - 0.12682443123, - 0.12956876923000002, - 0.12552745123, - 0.12642705823000003, - 0.12594537023000002, - 0.12513864523, - 0.12852205523000002, - 0.12745734423000002, - 0.12664060623, - 0.12664932123000003, - 0.12845450223000002, - 0.12999480023, - 0.12660916823, - 0.12678222223000002 - ] - } - ] -} diff --git a/benchmark/diagrams/append_array_of_strings.jpg b/benchmark/diagrams/append_array_of_strings.jpg deleted file mode 100644 index 3fe8ed0ce1a894fa17597ae1855920a151eb734b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19399 zcmeHv2UJv9w)Q1R5D<}!K%syjQ8JPh$x4t60wPEhkQ@X{K~a&61d%8?mPjaak%Q!% zl$>*pMb*2u)9*EOzv=0oe`d}5SK~TT)Gh8k`|NPOz4v!O52D9`^NO+xvH%7K24D*Q z1JEOYG=PVLgNt(p4;L2~A0O{5AsG=N0RbWP1yW)%IvNN)9Stol0}~eu!(|ReT3S}2 zs~p#O`1$!EEZ4<_dBwQ+_<6tI1Op!*pOAo%iin7c_Y&t^!E#Fvc;TVPRYb zFi9}5NHEZK00aOq&VaN1KH6&BX3Z2SU( zLc-TYq;JW{%E>Dzs%vO!Y3u0fnV3E_Gq@wnxmeaK*;KI8(f_=oq5YK*=9D1+wK3h!xzMSb;aBpzjW;9N zf{L^ZR@(9JZ3N?{C~$?OEf`YoyE;BJC#1wu{jAQ*aIQvvP$6Q`)pV}^ndpkTQBOe+ zb3t^#JCEmYl6{ADd&Qz~sqRe&ir#h8*NE3&j|<8CnrtaP6Qkag=S{}P$^<9~&-Is2 zN`kWIqZrUYjm)mfiX{B&+L7#-X!D>%N50sU?YH8!p6J!55NPxD^|50gPBgHslYlcX zA=+7ZtbC*lN960bJSpK!@>!8L9KY<#<#$eT6S@|*ptrsheWxM$&`Bia9lUGK>+?NE z#p0!KT*l2@(pm*FqxHC2v!aD;t}Lhibj&w51UHu1M8;!Kg&oj(aWrtYvBqd{%oNnO z&x)3j`DW2^(u(Ln$G4ylY46L#JU~5LnVn6EdX``s>V}!FV?8X9B^GQsTo^FjMc7PN zQ6t~Hb5{9k93$cmd#y-nA~S5bd$?OyZ>%6uBQ$Mr8^)%TA?Ghf;$U0O&sXH0lc=TmVrv-#%l#hFU+&^c z)`0i(AH+`bWi~qa-!{qehm@^#mvCcPYH{bfHL8X7QlES^mK1vqUf#OqvQoDXn9fi8gg0QI0aI1n6wAZekgW1Isf!WXFv`RFqGE-! zUM^vN5#ovsHe!n;h9ly^T8<8%GyK`+?7zj_zdzT5Q=Zyw`EfsKe0{HyHdOybiB^yd z{Oap+-}`j%*p`wT_nvo#j7Dl*(6jQ0uZ@CdT6_4r3eLEVm~$;~Ni73BO+0Ae&KbaC z`fj7xReoNk5c$NAuIF0nO$>n_Gg3>ETQZ8?(`T6OEyea{J@!=#(rAY&MlUc2-HGLR z$B{*Vr$Gn|9>~r4Z9w;fK`V;ulFYAcK?C^Q!<^-@#YfsBJ93Px6QN)_Aj)w@V(GH z46Di&x>X35r0~>2-O`WfuMRIF^u9I3__c40-qPhTH#UtaIyNAnzGCy<2*C~!;U*ir z>5Kf=YYOAjCgSOs29Av$^g$DE3g=-f+&Ly8myPnjCgHj;*x(8`VHb&G2nY<%Sc=0hlFu>A^SUc*>VGxh_4AQnVR2l#d8~}0er?r$8=UwI zOCb@{-Oo*8C3|g*S(&T1X{j1psdk0Q#Uz+IbGbOjU+MKEM^D>&^OxT2_74dMW=e3= zQXDs{$r1HAmsu~%CmB#I;rWpfkKOb*JHA{HAThA(>2uewogATiVaC>l_?kS|GomsW zV6spTJR#rBOIdJ`rccb!9TuA-;jCMt4nAaMGG9;JiRBU(FU0N5?r)|_x<}nBn|-dE zz?>SJWQV*~?)VKFAmFQyUk^Qg}ktP?2(Xt5TDEP6SI;vq6HVd_*zNg zC;`!n*EyZ_v))qe2#k^KN2rqb1}h8?HBFO513M!`inW?WN6N8JY}|2N4vIFj-^Sn0 zxSXTSJZ@m@EbOfB@~LMfmjfpDe!53TprUqJy+RqI9S@r}#NVuR7-0SA_fL-K1H3v~ zT2-v{Tu3dAEj)W)UA~V?>|u8DK!oa7`Mg^50=|R?IQLnoB?`090A4@%pLA{s8hH8$ z4eZwgPEOj_fBiOOJ-`aFEwU89H`{>*{8e$~WE|s9zNhx<-}?XHW~vw2)jhcb3M8#b zqHdvdwlGO!%Su<8+C|Jhp@9jSKIpvjfaF?RJQ|=%LXqTXqh1*}qL4#qV8ag>F8x$g z=9U^UOwGkWG_5#Ka7*WYxVSc>Phek%n`CuSG>%WQjF(B8Qnt>>YrRcjX(8&PTn#}< zw)j(bl05mK1U$xdPf7{)fgL)%?wZ3!Rizu)d=1QOUF_bvx{oFH8~DajoE(Pq+RPr! z6!|LMEh8kRV%6@kyZuB9f@IW#9iJR`=$k@2o5uezSB@|JERwyg8C25+qe;dKPYHcP6DktG~06kc3r!GT>$=>o|Z5 z5Gz)JJEFBF8b}wcD{%oA?MI@ z&l##lV*fyVI*+^3i3=2mVT;!+UDS*eV)w4vzGXMh)gg>;|NOg}n}1D+$8g?_q2C7n zieL1KR$-UZJoUq^clB0&x1t!`s*VPk%wa73;1ZtH%&$z~8$7?LfbXKUn=xP0!*>nO zJsTM?+X9oHT^C!wTC^+Z62bn^#u8i5FNFecih#;UD#>4q<@Z!1_>``Te=lIUz)NCi z(viuA=<_96h79B9)eONZiP<0MaPP)9^z6UV5tO=N+aK+zRUei8=;-~|Pmt>vvN%$6 zVbPJlV_xS1)33ZrmdPkeqmML=_Az(v9!YpN*S8Vh|NATVO-a-uceX5Wl&s{v=EL`@ zxWQa5zP8DPG3Kdv~(}pM=lkK&e-o8&!ZbDmi?tqbo2-H zYE7$r>}c8Yf;|EfOrm`UY7YhcZbo>>yezH06wT~AbY6DahNGC4hxWqtdNXi`K4IZ< zr&brE)P^&9gPUfQ(do5`t{>6j3vUe}ReWG-P=isvM>dMTD@~68qpEOI`^!?%?v*2eXe-#2`R_AfokMf%q4IyN~ytM%B%`2uUU5$@>MMMHwlwKZnmZ}$do@P9s!GiQn&a}JlK z&_)$QhU|YIcc42q+x&~xXdufjG2tAvN^(;fDS!suKoGdeyDLj!Mntz)(4rzOg^) zR-mNQ5d|hSp{^6KqcbDW`IKD0k}&`cG>fhe9DA=Iw(kzeNSz^5`;DO~uKZ-ln(<2P zgGl&wj0f&@(sa#_XierVFlWfyEm4Rpk<KHW3?<+}767OxNF5a7IL-E{_&m_Syn>{P^I(IHJ;m%!48 zW5qqi-tC=Fy*4I+CbW(RUCpGcXBUq&;?$mF6OxPx6fyzgN6vhlib!DXcgGrdkLLZ?Sd}V5Fb+%&Oby0 zWI@_&`!n;_>PE-9a}l>w3S+8O1if8ZP(2w?X)>tO8xWR{L{TA)hJur>@oIwd_JXyDkChNlWgB3gq8n zr(CI_QKnP4w7?nHcdEENi9?IpPg#4#Ze-{P#xNE5eig!f}JqpE|X_W6%~8$ofus*)Z) zsX=VAS|D~Na8Ymd4ty)))?r9L8t!Aa8=#*0D+m#jA&WCcv=u~FKVom%4_$kt75!;< z{hJH_SihbhLEss<@&4s2f5caQ&UCN-B8c*1be#6HIZrG!z_eCtv`YX&$1WuUa)3-t z=3g?tZ(Y=q({=@5$iN1l6{=l)+CcBQt&PV}N3Kg)Y?ZQpEH|0+J{|u~8DOO`EUla! z4P5qpiv|qOL+1-&(%k#W9wpk1+|qhK0*+|lR(=^ihNUSqT}xZQP$dX6Kr&VEITHWuzL zAliYmZ?=iYDOn`YLu)}L6h=yBN%y*a5;Ms@CM?h*vOy#^ATH&htBoaBiS-k^hs5C} z3{f5M76*Ke1Y1#Gm9N5LeRtSv3>gd8Q(rw4+STv~e9mEKw$Z`ZD2&%t+8ouJs@Wkm zD4gZ6FREUd-wU%{wYfaN1|^ zxZ7cCtEpJ{zu{2UeY(U)eknLGjloB|(2m3+`l?Q$!GoFx1+veaZ8fqw9?$B`(&wXP zQ%hHN%W^Ktloxxm>m(GoIQcPgjJby^U1(Ri@eIg0AR~M9%uQSdxbJocR7C&B1% zm>7B6SGTp=b>;O&XYr{97sMdPY^ergIw#F1Y!7pOjBYjEQdvZbbzal3&9Y-uJ5x}2 zC?Bd@kQ#cPUGYa-=S1`c{@K|3n=tCPpw(}@=LT(1Ik>nYxl}E=Z5qD%YpqDs$A#ja&lyf3-ugK-`YHRW~}BghtA zRrdpj+jf?Sa5|Pp9|^gtCKYtAUJ6kP&MI_?S4RwX;>5pPU4>?(SP{rQL^>GS1l;#peH&az*uFMT;1 zK5<784XE%Cq+j9+;K9)D-INlhW*}__v@brKQfd(vlXfZ%Xvi{8vxGiUPuG2``vSo; ze4dSz8t@$PSjV3O=p%pRK8*`|daEIm;`Fteeb9oWKARM?R(wbZzp*}k0b$}9--}H5 z9ouXd50B#lwM3(waV^gW+c+Jp0*6su&yW_Fk$-B>`$w(g?|tt7oaM|(o_B5uV9_fO zqA3`D=x^3F!8R$pO8bqiJ$G=LLr2s>*i39HNH&+fK02q4W?PVwo-uzN70V2JPgh^3 z1|MerDt*ONRJQBW6t!qY0AS^&bT>@fN27vNZ1;i@VvyPX+4{1wcAx7(l*BV_X`MLA zV*OZNIU|y4*mE9;)r(u0o>`ku*Zc!35klQX^H_X!H2hB)I#>CJuDFSH`+rWrPD;X6 z_o=RBovJY9_L)VLR2FZXOOAdKsYRS-11~xfVd8Q?+CI<;zTS=_Ox~v^McW=W@npnC zs~dzs{m_EAnr}JFRYm8QiQtZiR@aiVIBs-qv!!xB0VL%gp%j=yp2mcZm8Vkge;t?L zR3iG7y%xm_Z5p!#;T=92NMgzrb6r;^Y}tqKyMH57GvwK-K5c+TDsI{+86rwlIRl6? zt&Oe?oX%AfBawH-?`2IzCgz+d9Kkep+y6xAD3`I0LL_v*MqIB}p=*cAu$sXRKI;A` zS@hG;QBl=FiTM5kJZ`i+w|}ZOGOz7i{`g3MvOYms78Uc`n_Efpmy1J80;sjAGy(gu z>vQ}VV{Y!Nqczg*se^+GvoSIc?dL90L3nK5cBuUndyL)G_}_KU&`7-D3bC7`>)+J* zaJQq-VFZtQ3QJ;eMZe&ubn-;XZGrVC6Yifw_&@Xgv;tv|VZ{5*Xn@fx?bwY4mt16C{_=M2`8Nbotx(}EF<#^VfQn8`0(T4nv11!+cx9~*T zSfM`hXA;Lu%R}eN_jXJa)!qtXaH8V5xlq{<1W~KiH+Qh!w!NQQZa`KV|I(TGyd}a| zGvJbj#dzeC3od2p9JRK}!e@)yl>u=~zpC|sqIm1%FVS|d%sUhLLi>Aeg+$t9ZB0=7 z;#DohN5JQQDnOpVFMpK_{z!H2aU8e4$)t|TYsS9h&ZkXcArDwA>Kk^ynx6L@8p*eH zJNmX%1``6yd*_s3&B-;*)0JfveP*Raf_BVDStJW(kNM0ZfNn`)p5JOVnAF~TAc=EZy2{LtSi3+qzosabN-sz{H*${rFHjt+HI(?>@DXdEh$u0|~kc@T1<-_zo_D}4rIa`+s83TpmRNlYFc&|z^ z%op}d8KY5PI%2OsuVqqPuzyL-*el)gjk~LOcK_-EV?$UTN0RY_=`&M4RaFVZ4nH8q zQ~~=%1AIY@`D~bGDU+NDOGs)jezADIyU*VQ$W~PZ!bD<4oK3k^k{rH+V&r*NEd;(z` zqf`$+bNrD6j%|nD`a;+DNGjX};XSo5LC6S-He6 zLogR12&+YG&16Ey(m@tM>FHnZw?G5eyc}L_F?P> zp-~bg(gz{)PTFxGPhfowd0-X4w_;FG2k1!F9$K(6IP5A)vDmfZ*qoeix1 z`-P)su;f_(A60TZ(KGweZu)msQ^X>t?2uK6l_@k}IS@LK;|WHpD9Yn<=!(du-ugy- zLuAXQgdGI6!5ujbw$pM={7+}|dk`P!CaVEhTR%br^TkV@OC1tK-^=Z;XuxOvkv^0>mL2fF4uQdCkf2XF1i>N%-L3#`pu4?8 zzD0~=HUxQt3A-RXcpeH3fc$bM{NHaZeWV5X(vg-9i-JzDZBQQL7OEf#!;l~>IPsLD zo04B3!=F%EUg9#IwI?Siclsg2Bq&u>->c31g+=zpr=&}Ixk-~B((MXGN75sj2bs3= z(?vVYv3I6X&dZtuvYA=*rmn_InVoZr)%Vu?%hkbC41q!9XkZXEFnbTr#l6yyH`_4= zvWfypnsJ`r*-AHiP>3F|%-byEF5Kgw?|a4JL+?(RofA7|;Ki&EKzx&3eW7zv6C{hRo zbEd`ZFVP3ur&if69cU-0j`-%GJ@|o*|C6~Cz~FN~f19%I$4|Z4tJBXWHN2gJ8@#Bn z8mF|zzH=Mhk0W=DkW8a^sG1{*vE_xm<9Nu%%1r9F!4-rV-2)@@o8$NMO7* zeK)OvuGnAO{UnPb5d*|)elzd(Z=6%SmKP$}{K!n-J`a07&$c#L z4xY}qu|kXnutCGvyaO0)XoIZ0fnzriw|pE1Nl_oUx9unwMV$Nmhf#i)OI}VmMBhqy zdH+V^p`4-hOVE8F>EOAP9&p?6ZU7Ie^wqwtZO;ySGduOEkX=F8Nvy$ddu?CsFzA>_ zaBQE$4ka#!WIdKc`F^lNQV5{|`drEMK4Hmx>jQcd84uT!*OE|V-?J6zJ8%>MXedEy zJ2Z*MAB>u|XxJ>f|q>lsTI}o)@ z$WS5D5Aof!?Y^5fE@;{z1I60!-K-~4D<%wNm6jkE7B-cbu3TX0jtNNi8Mg2E!k&3V z=?@}g@kBH*9%qD7fGH6vYbG>fGk>8J?Ow8biRZ=;ne4|?Jn#K z)s3?CB;{3ZP=wQ}wO+v?x+3KY$UX6CY_(dt(f+}YzwSnF3TvZ_n>hKx*m}|qrx)$i0!Soj{4eUtUQwF3l9Xn&K!E_2qHvt# z)IWmAk8*eORd6+Fn>#k-X}q`l^U_wU8ODMKY9+zbXo;yKXy8l$8fXMf>Gae8u*w9M zjIa^2qG-St4IJGCceIoGoA=}%Cnj-IT7eFs7<-tGl-p2G->n3c<@qa1mRoyDP^5_X z-5s2-xu4mcoyHk?K>rpzK=)uH{usAF5=q(u(!}|_-ZntD={rICyb3o+c^pW6!a}kE zN8LBt-9Q5cOU>3JNVV_JfG@{`mAf9sBf;Z1*>_8^BaSic(9)mM_ft+Y``}t3n}$PR zW5C;L%`Dw^QOV!8(QHr=bEr1hw0pqc*RNc!)vF)c&E18(i1r_4tmu}UxLgD=EA*ag zL$%v|XtmU~6&vN5aqbYUDET+KO<>UY7}Adtx`RRkhg>4tBI8Jb@i9_2ZROC0#yUNX z5Cxv7_uKFBze>^N;zC$+;`7`(K!5~6;m?5X7QJIpT@(FQyj+Ye?_AeNYatbTNUfRDLf1j{u|o88 z`1zH+X0;s#s5kvFCH#hq^~I(lnR=Zs9vcpzF0g~3DnB=3#C6vZ!gsBaYuUXVn%!`H zA#VF`t{}|D-h((9g-&{RdLQxfFiI|}>koR^DMoBL;*|W_&c;b@2<4ufN5%u&>f{Ai z&r)_XGi5%*fp&{*7)0F`TTm#W+o$DJ zz*i6Ha;#I#VY7Xxk_hA#e;ZTl6+x{${ewi8wZTM3{}^-n!j#`Qm+?R3L9%QoFYenB zmXG*HL={-BhmSuu;e}{1IPUii|ML6;d(ghm^l^|ps;grcvg-#HJa&d=9(h%oN&2*I zZpDeawE<2P9u*az6dJ8kC;(ji-WP2Gx-h004S_i{4r0SVfU{==10%p@!rkvff;uXftDq+`l z{AOBvG|&%YFDs#pqwb*?ZBgoRqAJvVP#$tA)EfU;&~%v9yDOI)>0ErVu9I8!k5cis z@hy8)1SXoHt`u5?sk}^r!w=l$_e7D&$U$5nuR%l=H%E&$?po(WZ)G*-Nb=}opC|fX zE^pyisR-G&4NYAv9U$jW7E?q4gJWhN?$W?jBA0Ky*L@mqaW`t)s$>#EKJT$xIZE|G z_tWRk`NaXS(aDJm3e&QLpJbEg)-XyC?g0j#qWEGCVIeaRXAe9;IXnq%2 zCoNOa;E>ofY*UZN@<{92N4W*nX$1K9d$jMwYxUA!Y;(BE>~}5u1F7u{^Pr&YybN?4X<7iY@Kvi->7hHz1_W_XvTkDx|+LudW#1#kBRV)N{y3d7MB?Vf_6 z3+e&~!MDEP^bM8;t~&OlV!+sYZE+H9CQ4wO1#OVp6?0*EXLfg%eBHWdgwML3zdI9* zpke`N2DPE`y|f0bI1GcefO<3VV#x3NpjBL_P4*i_xEG_up_`RC3!`801poj zFa`er>;NDO93~yBd_qD3 z0`S`&;O77VB_Y)rK53%k>c+$@_o(?F2PYh6mC383xzVx0cIo#0Cr6H+prxZ{U_Z;j zdG5S`ppdYL=w;a}a`FmS6_svkXlg;Vb#zTk&F+|6SX$XTI6iQ4c5!|B%+t&J`3s+r z(6I1`m#TAF70Q_&=`diO%`b7!$>kt6}J^}I9e&HQ*0uOvj0>U$VL{!r1 z#K!lIv+zGYOf3_fkXLnt_0o+Mn%nm~j-Fr>7-e7m+O=PL_K$V!$xrp{Zyo#Ze)R*S z_;}#t;Zp(-U>n7m_~__g$N%jZOp5D`TcEk&X?iD5l<2g*c9=gqoG-TikOAeG|C6F~@+ zL_5E%c}P&u%c^5zUD%V?%>sW#Zyca$^d+I_X~1X}{CbDOK%C!YasIx60wI)OZO6w+ z|HtSF`hl(;f55SriM`#L^w6|km$C3s#Nmip+D|vzbJb37eY^#0)+i0=$Tg2aDH z)jIJzIwOb^moBv=KL2uOP9^K%ochi=GAsbZQ)nqSTk=d4C_mSoVuywt8!6#3CqP^$ zC#T5Ev>zHwd!EC6Vq8tnl6%t0bI>=!NaK)idwk=dhf*(b&E{p#qKvT@c*8kE1#hO) zivqXp0{14B;U8ulnRmH#DfXWrAR8hU_zxgd7B|&w33Pj7*pQ-Yta{1ZJV%HvTJ?ey z7^^Z6oBo%SEuh!CtuOSO6rH(uxlSL&bG2>MK+HPXatjN*T__2AVCTEaI_Mko^b-|# z$I+@1KY+NiBw^nl3y>((R<4A2_(F_}_9zC57WX83*B>m6pIk3pi6;-NVPf&nlP>8l zJW*7v&C6rVLtd-lqa~$y+l{laRWdxq7Gbd+aoljgs`t|84@V-#xSm;z1Exzxb{8dJ zwTr7*;AmUTuxUg;H#3In9QreFqUC9gb@nyC<{%}Tf#{%1e#m>H``j1MnjV?Muvo?r z;VuTTC>0?Jb{(3>R1Kv5L3n0uN!N)TP~mN_{u<8TM%;ehR zT!V}<^uT8vk&xwu&2bk&PZ9NVj>%h978E{l~Y}nwIZe z=1r(5>AE{z>dmCF_92H2zLZDL>}j$SB~w|t(y@XCyc4_b-d(y4vuw^#C%ACYJz29# zguCkWO)PM59sMvagZXcts=g`CFH(uvTD_o-NKF609Fzi6HZKiW%o&5d>Igo|Ns<{& zh3^QAn7SSP2ij}6$Et$)blG;xF|ChWx^NBJBq|oz=;t_{rmMWqe62_KY`4R_SlzRw z=YvxD)RI~6PCcXv#R7+Wv#@|{?t3io@GchE`N+KDZ&kT*c|LA?stpVHT_((E!xya{r>kz>1vB;LmYme<$!0__sZAQ)PX=atjV${^6)fUi2h zt0z`z(Yu!L?Kw3JPar~_V(~l^{N55Z)Ef&N{A@=*mIVvMar0vK!NLF7u{n2UKSj1} zNut^Z1S#B4aoZ(Xeg#sH?y_J1?vER-fK2z^3a5a)+dj$#V)MZs8GKCq(wIJ@>$uV6 z%iU67FCy(p7js{P~5?0#>VO+hTsQ{3K_@ypy6UnYltfUK#a!K+54 zw)@nD9Z<0m=3S153|QdJtzwLi?o2w6=)?ItM_R^kQH-h0iKeC1$AGNWb5@8r_G*CaG4?V8hG;t(7{j>vYE>nhDkGZ}hdE^BtEE-lJN-8OimIENrab zO^0P4_36Q)A1521^{gKi`DhE^Ab`XITEntBy1dVgdd*7rDga=sbl91II&*(M+~wpxF34l^SI z5>C7}-M%SnfBqbhl2h6`&2ft5klLiyOgzP1m%+hMo%ljc)_cqBZwGQj%nCFm#8Anq2bwWn@x^ z)TxMP<~?QR8L+XYrOt-FDl>}y5eow?Xz=Hvi=@C8pkX?)uV4FbOnq2o6YZ6}7{U}aX!3I#f}k__I*Gk^u;xE(b9mm{wKj^Lo)Xj2d%D!MRb z$FYFCqQU6FC3FY!wGLEVeH-xr0)mW%-hJ>>jCivRv##5quYrUASpQppD9Gh^6n(NzK7QH%IZd9fw_Sh%kFP}!XRTY`X@ZC zzTtCmjW4GvqplWsHJu~#gM5ClAhf&Q$fb}c#d`joj1+y1IZLV182V<$knETkOcLq5C*IwnYoI4NBPJL$8B*veV)M1rabJ@+Z`NsAR02lDHu=RUT z_GhmhBn(SGPBN3&4hNALnj%ts$%hj`w>$Ei&-v~BhZ%eY!(|oi&6smcxtgoZ?Y##) z6*AM!{3m=BlxB>KL&KKtY?j&RkTCRdw$@O^fCDSv8zk<)MSOks zx;^2^&aUac+C{ZTY z$>XvuLNMYeG7TWP({|Q0ymjBdFX`NLV-%`cOx;!_WgQegexzAs_z?wh<>`A;jK9V^ zNUl_v>t@CKG)oj(_o1Me3;UzbSs7kej_L{zsxF7WGY%|q&P|oaJIO{)aZ`$p>F3m(X2jpPxCv1%W*>avXBi&A)&^?dxDy+hXJQj?%Oqs4~NSL*>14DS#p ztcs7p+WYjK1KMg(4iOcmPPCNr!xk^^k-iREJSkt}XS%c{fR{1Kt)yZ=c4On(dY|G$ z5X{5t@N4o!4K;_Sj8J&D$Zk6ujM_LfGP)}eRS?vAWKteGvW95ftKh$kLJunq#FN6Q zXV&I1C(F7{$DD~UO3~>K6Vn#z#m_7f3Z7B1pRPjoe{9X*c@yd3V=!x->T=os(i6Jr zPj}4p!!eWr{4P_aKAiZWT>>huyV_i}&o;08~UuJ@t zXss;0$f0?u*|ETz?5~uhKalLH5PyN3>&7=owZM80NQ@fpQVSTOR-S@%HpjgN zCr&l+GF-j7c^?sA;8l1au~2?j6K8ZHwNkx6a)Ww-%WY3>1V!)iEM*%-aFew9pY|D( z3lK2VH`c(bcb^8mzQ^cvsoN2vm7;q}qajNCsK@k+@5f%6{7U2XaDtIfm5mBg}X{w#etpi^eOP&tY3YFeFy(afOCW-ds zz`Y>pb#t4R_v6b>xl3ik>b>rwJe6ld?~Rvc5q0IT_)p%_qN~lf4b~Q6-M8oyhL@Ec z1!l^~ioqe<=~y|r#$*ep zaxUB0oUj$!z89h%hF3rZtvvHSH}cU%cXLFsmpCgtD=|y)P-eW5l9!Xl4XKGo+Em0G zG%W)=nez&rh0Xr*^O~`kW>whCq9E5*qrv!7{4R#odVm+ZH+^o6K}QZ+HeqNMdSRBP z`f<7B+QrjZq}!x;Xf7rQakB^a#+NhQFV!RFstT3|2liuboDh8|ETkQ6A8mJ$5P)_q z6%Mi1x=MIuWrCSvh0Fn^mRD2|7K`el;}-NXB&0%DKTJn{=_!*-v`D$#r1Gdjdwc#U9Ar)Aj_y)_KM5YhnHe)I zKphA@zcV=l`<6kt{LNTlJrZw!!as7o?A?IP3Ys{jS}|}x4xv$e*pvDq`9A$~G4#jr zDRGlrRhQhvcZ&wdn*Ou^6|eH-+2g@!Y(?t^QaNmKWp)iIrMyj7u|R4Vo_&?8|NbVJ z1Wj@@bCd{QA8?7dzI%`6upi&L(8Fzsi}%G$HwxRF<<~l=km|)P{5^t0(on&~g%rNX-9MX$NN!tcRs?oNl>E?!yWjGd9v zx~NKP(a8O@N5OT2VSV^{aim$wWDrt7%;Qeo6-mu9k`k0CkS%FrGX#*t9L%P!nF4gEUI)({^ zgzLv>hh~QP`B(3EoJN(K-`k{hjJu?Sr#@IWrGLQo)<~Ej#MQN(FpgZDxu1#k35l4S z+0&c-?%@MdZ}mP*4it?U($md|O~lN#b`)@5s@cldvwV`{ENW0@&|M~|?wtq>{B8W>xS`947{#o3NEAeL=; z3wCa}!BT>abY4^4d3;|c^JPC@`(m2_-a^>7?1+BqbB<1Nm`8lh;^dI8wwxPn|FmT+ z2hYq0F{+QMJt7=W$hl0QJ-h;I8P*9_3_c}P20bI!W9vT!t4)vrH~tC-{+1&C<8a_t zw?gz<3{-b>Y zZc$l9Wo4BWC5Jr<*mv^1V7JFLMmb>hdj!O+8-Q`Ru(J z*QNFugg6%PY#bflUF(4?cf--Qm{;D5i(vst-L!pFAQte=K(#PWamixlE&Z z)X1?wTSNw^%}K%nV>7rv*GWQGZ8kA~3Y1MOTgQ3)SNXuoGJ4l4ZhH;_$_KJLFh;0J ziYbMJ4g;633V@o*^~p^5a5|_D_(hrEgmEua+uH1#vw@%h6DMyMC{$Ymc zESQNr@t>86{GN0k;E@0F49l9Kf93iiEKnS{pJt$JP`Fx*F*4i&HJt_Xjj)1$S z<>ciH!Yj8uBq??%H*op)4`PDsyx}Uo|EMtkSK<1(xrhhCDAO%eNRkd^iP4hggN&iZ zhVcM569mmPf#mB^L%MkkL$S5kRSyV;y+i&eu>*AY{tGnVrSH(7GW5}vCoidQ9(4_6 zkV2wjZU;mN1}9y>BR(1SL=?Xq+Epf;I%~-<#mgRGx;7E8gmMe#nk|AFUEYc(ztg7J zg%%w5CmN9TlO^2uZ0ql#3%(fGbdJllNZWr%b#8XkWwTGopF_24DT&j)uB6=gXq-`= zB%JSB2bwF%;!+vPF(E+9Pp*8*MM<7qhG$C*3aNg_)bq_%{I$mZ?^8+NZ;tr47Im1E z4_JI}g%B3_M1uJO*@|)Dc6M$XSDxHu76v_t^ULNnX)r|Qt2^K&K}DQfU{&QVH4he; z-^T(Q`i?V>({V&tVB&fySmE0SD}48m3CMCU45?aL6&5%o`S@TZA6IM=A+~c?^gpZi z_r1F2C-SBFXvGUb&U(;DdL~hm{MW(lqhZh3jw#3*UIxW>nq#~u3ahvsdX62Wpu19H zr^R^fIIDDW&yt6PoI9w4I|6-PIU|t?A2E#^S3t8HfNH$>Ezoa24uSg{?m} zNsdGf1y)n{Y1uDaiMA|yO2fgxALb;=`$rmFt%}aGt9GvNlS1Tf|F{v?YV2ZN7CTX;w#`pbW zbH_)A{R)|B;g%j1C$%xnlXur>yqs)X@7M1omXVjkMqZ-p;fupqz)EXJcavT7oCIWq zQTu|54${wzD+kWno1W;i+2-&ULBJ`N{vDlVwjrHI7Uo!v(}EZVxgQ4!eY3pPtIZ&oKjVkGNE}#~^NVV2eUEpca!0X@a^G{yqw;gt@}4AQ ze4tsSG;iSHC-VxA`JA4d&Cf%Sd`CvfUWsIpl6;p&$@uJyz=n20UfkU3!{B)5V)>4{ z4yEv87A=G^C31UHkwUkX_m(xB^^*kx6li!dgP}`% z7bpaOQilb`yW!P=-S8PpPzGLQ0$-hzOwxe7h3(S(LVSmvK*b3KD1&7M3y}OP$Ql*5 z>Jq<~y^`!*6A@Rhv8knqo>U|*82^e=TfFq(ecfci`-oSfLk%JrqBFWCgXc$Pn! zY#czx2`Xc*e^th8fPV8nR2t_O+F#gLY+saVrYV?sjB%eANqTjSl;PT=B_Lp%IPg_Z z@6#AzYRl(OGnCW=7FJxjx++H%<{%pa1Mrc&%AHN-15;9lNbeC>Tj<9qyy`HB`Elfa z90m{h^xvN}o&J~ADhjHVo!oG6ZbJ9uXLE|-<`>siNkf+EquNkqCIRmke;eW_iREG# z>aS6!tV%J4109|4R`L{apzWYq)y4}cvhvEqNA$EyC!kMXIk>(I0Qvu0pH;OAveTg zkJb+h%t$0)fst54j3VONmMZJGE&4;{;y749L53iUcap$|tlEBt6o@ONDr{NriTj^J ziWNkNN?f&LtIpg(mok&4`B*16`+u+TFf!wk#;P4y zRmItr+!%KMz~==SEFmU*muC5ozk4L6PcEsh2f4i(w@+LEK_71h%kF}69Zx+4Pq;Zd zUv@~Mw+RS1SmxQlLAXhG<&`djRjnk(&FvQsJCgyFhmTpR616KXi%w4o(na3SP>VS0 z+{B-y93d9VC+F&fgF-m#6&kh4kQV&s=HPcsEK5vfM=^a)7*&oyvuSl;0rOkjMav6Q zl6Q;)ZuPA@V*wpM`!0!@Uop&11Db||M395_t)9vJxey)5_vb7Or%dhwKiK))xf=P^ zhcRioeKm-gR4$7f)V#?`ikbi^7D#p+E4}#2r8D%t*oug5nCT!Y#_XF;0x5xmTvzY9 z-;rh>=x2s(5`Q%&*t-pHXPg~3Ei0G^2MD=kq~fbl>Uy} zO?-4tdj@6j`a)-)WzmBWReP?gLv^N-*;3Q1Sq0zAbfL_v#EpAQ!TU*Z3y?(`OxOI| zroKJ*PjKSblhfj^tC7IWg-tiNA-x-2Ns3qc-Q@G%;uRL>juuC6%m;DFl*ukeau*Ii zXL(cfCiKet?nM^G<(|map@*&YE`VSCJ8Siz_r|S!S^_mNr%`U3x@GM*;NC;W+C4Ev zTNa(mF6xbw)aGn5wRy}!q6BtQ@f2pxoth*pTX8OKUDKpq1hAMZHC* zd;JQNkL;GCkB5kW_>yA)b-K?hekql^JmO{Q44+m0M;8`Zw%+4-%#CMzIIqbCTwWw4 zx_`bWr|hC-q z6)TpF5^WWS$xR)96l*gS^|nsfG55T7v~xHbFR!9SC|S@Zj<@dxfBwE?dkWu7kA1Bq zSNrCoM;V{cTVpdb^Lr-^%Rg4Lg*N!n8~H1?eZjYfe^6i>!SAs6Ve{2HTM+K=fKiV^ z0=!xQBp=lhlY=gCU`kMZD=xHYUCd)x*v7f7f)hWZv(U}M*p^|4UER`&J!$t5X~`+7 zi)u`9O$Y&t1;3f>2B$NuVycH5Hb#PXkY1l}Sw2-gK~ur9^a zrnBu@ULJu+v&?5pg{_}{5k{7k=fpTTopuf10Gd8XCNofDZ=h!x32t<6wF{clT2s68 z_60zXmxRwra^~ShRX`At_<2=hfFN#HG*FFw=D2WHEy%S={#fyh44^qkQAJ&+`yNj9X&c%OoUIJ&$2{L zv~t;#jY_0(=IZq1YRwg1Fx2jTeMwx4$0fL8J^e==9@6VM7l)ffwA{kxo^$z z+J#xe&1nkEBTjs!|EaPbnU{DepCwClaDvoE1fK)>hz$oH@YA zd)qIt@V@n{J+tH+9v|M7DwiF9#q@;P&?S8%@8MYQ0vci1IolBeZk@|CD>*sZ-{WxC>1uFdARaNRK~SV*O0l;H72Sht~G~4?(P=gg%*`^mU>ivNXO2w*P1gTMSs@(ktZZy2xrKO7a-QX4 zW8;%Lb5`V>xVSik_q@EcsGN|PxahZs5KvH19HBbGOij%!dW!9o=pX*zYk_0r1QUcl zLhg93Q8*QhSH+|ApsE)Au$mN2{AEv zw-Z9Tnx{R7C)gF};3(=)Sk^9zeh z8=G6(JE*U_d+2X+5dcKr#rlV2hvYg2%0);_OhiofO)dgL7qAf>BPKa@j`a8?O|pA- zbey7($muVKrxeyvaEaYmXE3twrex$6pXAy2Cfd)E{XW5be@L=_2=;&F8Ul_G5rE1g zItD<2uV}v4K9qm8|FtnVRh6_-c5}pPbXKXWT)LCgPG3GY6r%tQ%)?xg@+&vDuN6`) z>C-Zg5fw`k3>Uq)LStFfkC>tM!UKkz@P*Uw#wlHs6hFPETZq*SNU>sV$WW{l<6JX3 z>Y3smBY*B9`$HL2v}7PnDQcIi*YxRqy{Gz|R@N^CgS{n^3U=cJG>DcGPj=^@T~o}< zjl8@xrSdY$j5vG7i`vaZ#xy(qK@!bFo0+#o{zetX*c^fg@-L9N5Caz>HZBqUL>iBQ zvXr#eu>YBBYFE3s44R#+P3Tgi2ned@^UIxP4yI~Is zTvGMUbQ|qc^sfeY7mj##Lpqb8BV9Lxo!=nQS6G#OjgBwr-p3a0z8;#4Yb6{W5>VsV ztvx$@>$tiY<`%&cY#4p6C*X~Q2kpv~3KQI1gZtI=+C2Luc8}XRYQqMX`-O7z?^Tq0 z-_S9k3EAfIGc=#(5u8q<5v=J9vC4?o3oEL@a99-#xd>gPY$ICjq=1gqw&vjhLUZ1q zs*v2cft~`AzKu2%THjk=Yx7})XVox?&kgUFYZuuiz7%Gdciz~)QCxbDSl3!t`AQ4*7OpzIe}}9Thc52X_j!&HUW=G}qkTJS00K-E_!3+qCjdUn8)j zHqVT)4BSvpN?m*}yner?KRvzdae=+0xW#j?WTn^a4H*R+W! z@@}T_40eN>e;zX(v3up_Q8?x!Z-Knivu1O@xTG)li%<$n40)!uBOY+O)PgY=a{9>G ziIb(u(`uq>wVNGow|&MnpF#gBZtE#x60)z{fCqM!yj!s;sFLQvvb`kJO})~Q}W$vLllOB3cZSo^_Ewt|E8@IEERJT==3|)WZ12kU+P->%QYP|^& z-$vdktn+@N(`Bv}gJ#|@GmjEqN(A29AZtM4xNEw#Zx*U1i{uJfPk(4vp%`ZFRAnP1 zvlmDx?-9($@w$2oDo*wd^)`?UxLj{P>w60(MkcR7x7$BTDkw8PmGq1=Mph^7+-HeO z(JZK4({(Sq9v|5&ecOBD4cR<9#X7{ICsO!dl+olNq-*QuC5+A=*BMY5HDD&yy7~Ol z!li;!)yiQvDt1lY)C;WMwoV1^QlH-vPxc^F4#N=(d>V7qx3^mfa8&i9mLbT1Eugu)_(Yc6B4@iF$oVa zC)0!m%|j2KOT`__)5ZlFIN&gYcwi$4SXjt+ICSpNSN?j;FUIRvWAn7zL`nI2OT&>| zQSxnxaI;Ba>cy{AcK0V&PY>Ncf9F(D2(uSC%bTS25DUb%%yQD#xh^~qtceH62XgR$ zLVg7v@O}t-&A9|k z8$ajhNWP1ci2wCNAO~M_owNoOnXwepZk1(@igjMHFVw$W#D^v%Pcz{9>6%1Ie{UXR zT>y2~qbpb3A2va+w&H=umbdXh3Q`{rASB@!qp)42QW%=uscV1B3=cH%5ve?273m3x z5|1ayBvdJ3Y@H=k*JurP;@-|)5pSpFA74Q`g_OQJ-xX_fyr}}sM_7pX4FfbyoxJzP zb%}d=FkL5M=?et)StY#>SMa1Ah(p`P4(oLo|X4wEb z<_RBVb|p)$->_cl_VyFkl=@d~^6pla!)xd_0+rByb-B>vuJ?p7RN1>8O1_=#tCFkq zeA%;&kJ0{Yw8@8^Y`A= z_A{!S$IN%k%9NeZ$3+FCeyf*t_a6u|uISbaX$kX~9t)Y0^Ls00PytM5GLQ^BONYTO zdAVfV88H}selJmnVJ6rwy!1&+-F#5nZ)%KCQ20_`Qj?T7*TA*sFZ8Gc`0~_5R9s?B zP48YXm31W|Vpq4!`YQ(fEoL58{r_%!OtYb0s#673jMSficb$OZzzmUx;n~FgaDrN$ zJ3DqJLoX+u{B+D^U}VK7@=@&!D(?-z_d4ibwlf8|y!QbK*Wf!=+u{UWyJR@k@c_gM zlNBC#9aG6a?@5FQxYrtBD5^3%(5+^`0MKhH{z=WyVHyuWmfkZU?-mx87e$;Aj4?7( z5{&4O6`7u8NVpoF$zjf-mB*?%7t@@oX$ZjsRFRFat=V)uFrE$Ft%Oe6yFSBtmLT?b zc<}%e7_kgr+v5TA>s$L_wy&$9I40gFi1~IkFp}MXZ#cs1XEvBHd*<-3OBy(#umVlm zRelb*-4}Z8Krn4PaBWvVkrNLj2@2z|;NXAg-dQ?@O;_mpB3~c04uE6#zWYik0OnB#B{KZ*daN~TLDy-!?U~@m5(nu^{v=U!!Kk^? z^e6t>58^+4xS2Z?czH(x5A>IR>dpRHx8*W4@LK4mItDy@x5@@fU)rrLI|13_^=8Eb zZw<_rXezpnTQZPX1do@5u$ z?G<;OOT>vkA8vK#(zzY=Dt1V#t^VxB)6&s~a z5YoN~m7!N24I^F^_73u$!rX7ee!2{v%dvVot zctAmNJZ^GfV=#pFK}ljyVeWNq1gZl+XemA|_7fi>`ngaB#pE(NMHCutH6jr15Om)GHwpW6bd|$%AGIgvaWuleHS9` zHcGE)cy6msk^VZtFB?q5RU4;B&4jBy2}Vv!mu9N9+>7e#vQ zV9)i#!B6|J@pVXSfAxpYTwR*J`j9?jufdF!T6P>a zm)vZ$PN;kqQDoUhK&IQo_>r+oB$=qDO5+eB(oG|y5UxU&k&Cyi#UkuJ9^Y-gSypTIg)EzO_sJ4oQ6J!k5Fft350r%+D(T{1mv;8|YD;Koped$QY1u0I75tV$AhImfc zU1Y61U65T?<&|Q2xw}AcB53K%>alR1r|rU{rbcO^$NFZDTA=c#1nCq77;|!UbXqxM z6f54Tw9{T^4(?~CQ|Ut*yhl2vJ}8eps%S~i7}^w*ZtfZ+5Ec}`1~ zm3)NuFWdTg;{IK>RZ$hjzUxgf#{4C-+O-q8Px1 zA9VHUW;pRg2jbC|azeKv!!$(haQ@A3(2uvMz2qlhd-UQk^tvB7pn2{r@DE)5ZST*! zv-W*YwbZ}Q4m%nTe`t!G!d#llSf9lVDW*wksyw`hYcIc7-j%4TD<4F_a=ZRG(0SrK z!;m)9RD${v`4`xpDPZ|tA_ ziuM}5BQaWiLFeUVZa-gD=5b$9rCUe^okS*E^urE`WdlpGLMh2s)+}krnax&~waBiI zdX7J5cn9Lu1EL>{R{str{oZSb(}V)sL+X9EFhns_64Da0i_4#r<$9?KQcqTD>!N{lj>S0p(jkUSXoVNRJMv5PGJf0tMc?| z`rKlz0gufX^U$4x)G>yaiH~aQ^rV1L$U~rr6Y?cBfLuP7&_~?!Lb>YQsV$|F9+NHg zlcle&=89Y}x-2YT7}^jwkekQVFd!q=Dt>y(Bm3eVq5OICbX!?#BU||ZvQmpr1d7J4 z<=3lws?TJ@7Uxbm5%Y9soa9F*s*n+J6S|$GoPJns?q){z#5z{X?nyq1 zI{CK2B$&o6ZxjYA?rv*#ph;fkJWidTBwq`;$tnT~0dD-*O!s^3(##Q6X%O?us>}>=i>n} zvQLx?mUQ)BsT?v?mZzj8)=zV-UVq}F5$&xpx)F{<&Y`tN0aI5$rLaa;_NI95F0wey zm|H-Ub6(QP~&D-{5xSG9cstP&et{*R=(jQ zdfDSC>|yn02(~oTg^K*FS6cIev7Y@=*%z0+(66U1b`ce@Ays}(jLiQQPA{_IfrDva zzRMxNeGnP^5+(dkj}Jmm4}Vv9lg8eORp@dZblW7lITS}x_YzJt7bg2UNTBJ0>tXv7 zSX`sT11%}9@c{BM1TAPe$9fQ)Zj0i9AmyBBPy20&@AZW5wIYe7#K9k_7BMus`ZnKy zF)Qcjn9E#9rTWZGjoR-Lj0$u<0d@Wik>W+LOKuG#qlZRZ?NR4b20TyLWw+mRaWW<@ zd|>EkPON?WtX(SqG-Wc4?7ZBZkL((){P1TL8}Z>26mq>!a`WY8xQONi3*DB^{TzCP zDNjel>8C1p^YjQ}rYpzP=q&D_^7ZZ!$=x*@X3uf{4j>PJY~V*B_TNFn|K#sM^?hNO z!~-nQ8JiLiG7^I*GD4d}c9)m*K87||rRTspg#>Ht44VLvB$FoZhNF>G+n{>h5UgXhnWpMG}dj?wHOJ zVf)RhzSVm*=afAhY>VB-tAZnflc|#-3pqU-r!tR2>+3BYv-q?q3adlqX|AO{KdrR(0tjZ#zXgaWi5Wh6 zV;&RN8S9Vy>EjH-$>`+yj=TI> zbzQtHawZ!WnNcDJt}Y*Rsb4Q4zjj;16hDiuerJ9uMndcOY417v+BcJKY<3151qFi( zv&I#H*PX*DNRb+e6SUbO%(P8!9^j6Z_6WbOw!~h>>A6yumJi?8X55Gn&Gu(^&c9PM zLSyOnb4~WIQp_JIT-u|@LcC7rT5o?;P<$(TIa_$Bq+2{x;T#$KMYU&huv2T7M4vW$ z&d{#Mt&P^V$dUw3d-0i=G0PT)w8*caEcE5~_f#RziiFFMTvj0pWl2zF zyMN75{4coik9CC~gd5fS&9SD-+GV{wZ+r3utvVBGqtq`HW8F3=|Tb4y@b`6CyY^2v5F&Kc`40ta8o@W{kkcHsaU#z`kN>3d~C zNOgpzn%%>Ay1Q2%0@5u)x27xOM)MtNSLa-fD2)8Bl@~|FmX`O%d)hYI`Ja_G>zq;{ ziVU@j>;@4CSi1eIAi(;g1%Vksr_vZyJ+@^3o|{LG{=%9x?3DtMe)N2SPI2f7K=(e{#=&BM1A9vTr|GnDrxUGrUBO`VdS6;+A%SxApo zvO`*1dT93xi*IIIrC6{ARUS9&pSYgb+!3xZL;V+w{KbOQ?=O-U^RAhCbV zULhxo2QKJlV$orE;8||E;VXl*9g)P-Q9SKu?j!~(y3J#RG-&WZS4=iYol3<6Qww;& zzjbnaZ?hk|)(6KJXbe2S1LvUnDVUaR8G){EJOLM+XQ{^i4#**3QvTJ~(H|jmwE5R@ zChqJE^GSy~kP6ZW0^#%Rw?G;J$lBU)^fC?GB17`QMdfLNwz@zv0Q)zpz)c)h`5RT> z8yTPhu{HY!KAr_q1u8ETEARO&M?qh5{GOge?xbSscG@5vVG6GctHq`)g&E9?U^*Y{ z*fcxtaXJwc0rF3$=NO9hl&)S~7!$pr-~IVF%SaXHz!GHfe^Uwamy2fK7oGl7wH2}m z49IkO!Gx`bugYxd?i7R|wg~Y+c^Ed+P|c7-Y`166TlwQO9vI`<#5~tjWazN-KZNf& zrq^w7ic|=+*6tzv=oZ)EwPDG>MIN{hR%}B5 zyYT(rmJ$gxZWR|IdU?zHqFGx)ddci_O4YxpE3-Kav%3~ZpW5!C+--)f_g9$@DJ+6+ zArvHXUH0|+{V0tIV$tc0%hq26x}8EenqfU=Jv)55ma;y?g}EUtcgdwk(vG#TFTGEg ztdUYvjUaG4>%be!?c&lqk`237R7HR+1so*Z)srR**d9m@u^BN7uLyY$WHmm0?5(M) z*I(3Vz2u^;k<*o5f6H9PImwAdVNRldx%SpR$HNad*r75U{<2_6%TcvF8XqPpM#*cwRSnJ!Q$klU34t8o6~SU3Xr zEVPEEW<_e<$m;^_?y>E|Yxlq6mgE+W*PZ*XTJrnl2BSY-EMOxx zuy<#Bhhd8r!(#~Y|B_K)g7GK<9s)V|mIumv3j11+wyw;qf*0^#_76-u4!mXmGPDgc z^cJV(C9mkZR_|qbyU+8c6E!N+!|C{-EODZ<6Pf#wiNxxp&YGagh5p;d9ic1oPP%N1 zIsyqBu|e?3W8G7TeaH}|{Si%s9b2Ly*NXjzb!%DY`-Q^DyJSa>4~u(D-%Oj5`t| zG>(o%Y=X92`Ym@U$EB}f67ayP(@crxPTTm$-IHh>*B*idr=0ii9{l~ewEw}!BK0Pc zPQdGpQER)g*szuNFihE&l=UdCrAZQ3y7j>wStq;~iU)=fxW2sg&vC_XQh8H){hGA0 zb^aL@fc5lSs4kvekUuCQyzhz!hMZ@u{NS5>2H+aYJ|0+jxBO{&tN~8S z+dBc86XfEd72?ALfOe4D!}ecFH2bg-IE0v_M0z(MR^fwNd$Jc$ctEf)%yT=Ls!X@( zWp=x`7Hf3E$aA)<&(({VyHWvHj0HHpAf>SYT7+u{-6OsZ@)>3}j7_+J2a2b0^3^57 zRqW*}O&;@Om7}`j1_{0UyqdkM#zP9va(G`wE>X6GRX z?hL0_n_lC*@j>%)aJa3gtvrdrK2tC_`10W5^+X~Jr*&e}tXf zh%;llIWJA%i*7^EPbi@vCP>KuWh4o~1I=K{?+*cged@IW>)TCuV7k9~hc%N7Wd-Nz zcu=7JtIK`x`mjFuf;q^xt}}*jEL}*w1ueGR<1kiqsT{ArrmXZfXc>20z`T4^Z`cag zdA~VcRI%!U`65DkE!a*MdZ344S^cytLpbH%*|R<`r7XB9a#OA;XnSo}6CH-7mK@(b zYlD+l#_(a4aRKo*7)Eeb<49eSP}rA5glmIl9`Y5w1DY{fROTz?mN2!aII>zGtJTYR z%HV>k$>aJf=$qjs&pp)7?VrHkx!jVzA-8x~Z!R*s`Ofd|4swH(q-P8&MpW+U!n!Hio7!{YeCp*tvl zpGDxm)L4H)wkLO=<1&^LoXrHxp(WOF!!9TXTH&LuLMv|7ps_beFDL$eaHaVLdJ&V# zOVDlcVfaKLVs{5}@Wnr>vl`2ZSWGi~bsP^^+=d?Lz5%U2hG0sV!mD5(+URbQWIl-K z>PaXpcRu&D`D=(xV0~Yv=2bm*=ARgJZ%54*fn=oa2l);whGO5QJdg|T>jvLD9aiYt z1x~CG9K|din2Fo&GMTy{i3d8UaAQzZf}5ZRJV*|F2_rTVEakbIz)i=CTY3^4s;nJe zc70U@Vf-(Br@BLlG5u<&=I4zAV^UY?z+e-Tb`z&7Jfx%Kt*ty@MV&0`(=0EE~-5Ag|!!NJl5TM=^ zn+*f?ApzAVrfet|dvreUKD!!m=Cl~yg~(H~Pl!ru!A%gwC}9>bns_LH;Xw|8i8Rb( zmcFTJRvze=dG%b(!ou<)nsULlLt!spyAeSxvBqF=ZXD(ss}ZU*dGrx!)Pxmr;#Q)! zvF6iE{-`}8v*1+zk_zLBX?pFn zf}oswhOKd(#&f33v!h{BVLLCH<;BMS9?(b>-CYiaq{H66!RC{Rohm~cjl3qjLX})zSb0bDx?7^5LvsO3GL3JSc6m*Isi2H_)V0hKO97D+dhTCNxOJ&l z5S9ouUKo5mY_RJqKxIiGu9l$Q!<~q|le>ysS#fzz-r*6@&q5W$HqBuClVrK~@qrcV zoo7@A7m)fI#U(MDJdv5vY>Apm_E*lT9osT~u1rh`ptM7Ht`~7u`ZANU%&__G9;Mdo zck=K$dFRCA==Vq$_B3xDGMc@*^IV}&bpNc23+GmYjM4C*xx2L=W6oLa2yW49_e73i zBCKvaZq%91|5R(nnWsD4K>9MnD@?nv@9cQvK;Cil^%7?=GZ8++F%OSQiU%ebrNJ%6 zJ8v2yn^@UoVn6d#ImUM$wXt-&Vt7xQVs3S?O3v6tcPjm9^HW&ui4!Zm77AL6o7g9j z0>Ztkls7p-wB;10YG@6S!wqznl8jnfF2_puNAx*W<(|~VkoUd9PDn{%i0#z@dB$JX zwthYq2-rZNY%tvJOc-u&7=~-ip!p>#YzJb#Cs!a=KDy;RAG|;1Ba9QVMP94C)B9KQ2}>5gevY0SR%niXM0&K6&pftHomR(vV++ zTfQQ9p%O!+k=00A6=}=JrTWB-{2__FuRhyS{X14NJS8=bN|!6^*A8IK1piW7Jr z&CjvDdrpm0Xkx;KDC@>0uHeMSp{0VZ2F)lb3|AZy!_9&lz_s}8L!4J}8iRBkO3=m3 zmsOB;A%V};@*pbjj0U+ewqP;{G8eP{#oo9xIjmf=oczTJofn&?eMG%n0dI!7-J#93 zdUU;|yQ3g?WnN(qimzo~`}AN~nXV%5h_-N+xeU!?iI2htMo7uZhnfrd`_;{akc57CZC=O4C=%-hE2%Uhf`KeQ0GH7=5|Ql~?Z3^3B1a^pKF6 zG{s=sD}`sQ)A(w;5DfDFa=#TptXGOc))GqUtCZB)m}_&2=Fvy@-reQqvYx6v*ZlEO zU6Wf5(z(^`zI?8~@3j+3AKZPEXrMB2K|oH%XT|hI=Qz(JtJJv^gksNi-ehUMGJZu_ YdLXL(%QyciJHQTauK0of3Gjpe2Zv3Y2><{9 diff --git a/benchmark/diagrams/list_array_keys.jpg b/benchmark/diagrams/list_array_keys.jpg deleted file mode 100644 index 2e1cef60069e1155c344d56d7d5bd5c3d281d0e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18864 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 K2mg~1hW`gBz3>45 diff --git a/benchmark/diagrams/overwrite_object.jpg b/benchmark/diagrams/overwrite_object.jpg deleted file mode 100644 index 6906e055ddb7a1d82ceb5e64ca5643ab1ed23387..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18847 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 R9`N-48r1&py(YvC{Xd}@-vj^v diff --git a/benchmark/diagrams/root_object.jpg b/benchmark/diagrams/root_object.jpg deleted file mode 100644 index 6118771df82b6d8f9210416124b0f9eda3ac5f18..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18130 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%0Ay8dbtIPtgGU3-b6CGJWOm+O8=bqL}|%M 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 R_jo#fZPfnnT@&Mb{vX6WbB6!` diff --git a/benchmark/diagrams/update_string.jpg b/benchmark/diagrams/update_string.jpg deleted file mode 100644 index 12468ac8eb30e71ada4e32618d766cbdffcd6282..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18892 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 "${outputFile}" - - name="" - key="" - daselV2Cmd="" - daselCmd="" - jqCmd="" - yqCmd="" - - while IFS= read -r line - do - if [ "$line" == "END" ] - then - jsonFile="benchmark/data/${key}.json" - imagePath="benchmark/diagrams/${key}.jpg" - readmeImagePath="diagrams/${key}.jpg" - - hyperfine --warmup 10 --runs 100 --export-json="${jsonFile}" --export-markdown="${mdOutputFile}" "${daselV2Cmd}" "${daselCmd}" "${jqCmd}" "${yqCmd}" - python benchmark/plot_barchart.py "${jsonFile}" --title "${name}" --out "${imagePath}" - - echo "\n### ${name}\n" >> "${outputFile}" - echo "\"${name}\"\n" >> "${outputFile}" - cat "${mdOutputFile}" >> "${outputFile}" - - rm "${mdOutputFile}" - - elif [ "$line" == "START" ] - then - counter=0 - else - counter=$(($counter+1)) - case $counter in - 1) name=$line - ;; - 2) key=$line - ;; - 3) daselV2Cmd=$line - ;; - 4) daselCmd=$line - ;; - 5) jqCmd=$line - ;; - 6) yqCmd=$line - ;; - esac - fi - done < $2 -} - -rm -rf benchmark/data -rm -rf benchmark/diagrams - -mkdir -p benchmark/data -mkdir -p benchmark/diagrams - -cat benchmark/partials/top.md > "${outputFile}" - -run_file "Benchmarks" "benchmark/tests.txt" - -cat benchmark/partials/bottom.md >> "${outputFile}" diff --git a/benchmark/tests.txt b/benchmark/tests.txt deleted file mode 100644 index 14e3a562..00000000 --- a/benchmark/tests.txt +++ /dev/null @@ -1,72 +0,0 @@ -START -Root Object -root_object -daselv2 -f benchmark/data.json -dasel -f benchmark/data.json -jq '.' benchmark/data.json -yq --yaml-output '.' benchmark/data.yaml -END -START -Top level property -top_level_property -daselv2 -f benchmark/data.json 'id' -dasel -f benchmark/data.json '.id' -jq '.id' benchmark/data.json -yq --yaml-output '.id' benchmark/data.yaml -END -START -Nested property -nested_property -daselv2 -f benchmark/data.json 'user.name.first' -dasel -f benchmark/data.json '.user.name.first' -jq '.user.name.first' benchmark/data.json -yq --yaml-output '.user.name.first' benchmark/data.yaml -END -START -Array index -array_index -daselv2 -f benchmark/data.json 'favouriteNumbers.[1]' -dasel -f benchmark/data.json '.favouriteNumbers.[1]' -jq '.favouriteNumbers[1]' benchmark/data.json -yq --yaml-output '.favouriteNumbers[1]' benchmark/data.yaml -END -START -Append to array of strings -append_array_of_strings -daselv2 put -f benchmark/data.json -t string -v 'blue' -o - 'favouriteColours.[]' -dasel put string -f benchmark/data.json -o - '.favouriteColours.[]' blue -jq '.favouriteColours += ["blue"]' benchmark/data.json -yq --yaml-output '.favouriteColours += ["blue"]' benchmark/data.yaml -END -START -Update a string value -update_string -daselv2 put -f benchmark/data.json -t string -v 'blue' -o - 'favouriteColours.[0]' -dasel put string -f benchmark/data.json -o - '.favouriteColours.[0]' blue -jq '.favouriteColours[0] = "blue"' benchmark/data.json -yq --yaml-output '.favouriteColours[0] = "blue"' benchmark/data.yaml -END -START -Overwrite an object -overwrite_object -daselv2 put -f benchmark/data.json -o - -t json -v '{"first":"Frank","last":"Jones"}' 'user.name' -dasel put document -f benchmark/data.json -o - -d json '.user.name' '{"first":"Frank","last":"Jones"}' -jq '.user.name = {"first":"Frank","last":"Jones"}' benchmark/data.json -yq --yaml-output '.user.name = {"first":"Frank","last":"Jones"}' benchmark/data.yaml -END -START -List keys of an array -list_array_keys -daselv2 -f benchmark/data.json 'all().key()' -dasel -f benchmark/data.json -m '.-' -jq 'keys[]' benchmark/data.json -yq --yaml-output 'keys[]' benchmark/data.yaml -END -START -Delete property -delete_property -daselv2 delete -f benchmark/data.json -o - 'id' -dasel delete -f benchmark/data.json -o - '.id' -jq 'del(.id)' benchmark/data.json -yq --yaml-output 'del(.id)' benchmark/data.yaml -END diff --git a/cmd/dasel/main.go b/cmd/dasel/main.go index 6921e1c5..7a564ae4 100644 --- a/cmd/dasel/main.go +++ b/cmd/dasel/main.go @@ -3,11 +3,11 @@ package main import ( "os" - "github.com/tomwright/dasel/v3/internal/command" + "github.com/tomwright/dasel/v3/internal/cli" ) func main() { - cmd := command.NewRootCMD() + cmd := cli.RootCmd() if err := cmd.Execute(); err != nil { cmd.PrintErrln("Error:", err.Error()) os.Exit(1) diff --git a/execution/execute.go b/execution/execute.go index c53e2499..0ac9c8d2 100644 --- a/execution/execute.go +++ b/execution/execute.go @@ -134,38 +134,6 @@ func chainedExprExecutor(e ast.ChainedExpr) (expressionExecutor, error) { }, nil } -func rangeExprExecutor(e ast.RangeExpr) (expressionExecutor, error) { - return func(data *model.Value) (*model.Value, error) { - panic("not implemented") - }, nil -} - -func indexExprExecutor(e ast.IndexExpr) (expressionExecutor, error) { - return func(data *model.Value) (*model.Value, error) { - panic("not implemented") - }, nil -} - -func propertyExprExecutor(e ast.PropertyExpr) (expressionExecutor, error) { - return func(data *model.Value) (*model.Value, error) { - if !data.IsMap() { - return nil, fmt.Errorf("expected map, got %s", data.Type()) - } - key, err := ExecuteAST(e.Property, data) - if err != nil { - return nil, fmt.Errorf("error evaluating property: %w", err) - } - if !key.IsString() { - return nil, fmt.Errorf("expected property to resolve to string, got %s", key.Type()) - } - keyStr, err := key.StringValue() - if err != nil { - return nil, fmt.Errorf("error getting string value: %w", err) - } - return data.GetMapKey(keyStr) - }, nil -} - func variableExprExecutor(e ast.VariableExpr) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { varName := e.Name diff --git a/execution/execute_array.go b/execution/execute_array.go new file mode 100644 index 00000000..97e4d92e --- /dev/null +++ b/execution/execute_array.go @@ -0,0 +1,53 @@ +package execution + +import ( + "fmt" + + "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/selector/ast" +) + +func rangeExprExecutor(e ast.RangeExpr) (expressionExecutor, error) { + return func(data *model.Value) (*model.Value, error) { + startE, err := ExecuteAST(e.Start, data) + if err != nil { + return nil, fmt.Errorf("error evaluating start expression: %w", err) + } + endE, err := ExecuteAST(e.End, data) + if err != nil { + return nil, fmt.Errorf("error evaluating end expression: %w", err) + } + + start, err := startE.IntValue() + if err != nil { + return nil, fmt.Errorf("error getting start int value: %w", err) + } + end, err := endE.IntValue() + if err != nil { + return nil, fmt.Errorf("error getting end int value: %w", err) + } + + res, err := data.SliceIndexRange(int(start), int(end)) + if err != nil { + return nil, fmt.Errorf("error getting slice index range: %w", err) + } + + return res, nil + }, nil +} + +func indexExprExecutor(e ast.IndexExpr) (expressionExecutor, error) { + return func(data *model.Value) (*model.Value, error) { + indexE, err := ExecuteAST(e.Index, data) + if err != nil { + return nil, fmt.Errorf("error evaluating index expression: %w", err) + } + + index, err := indexE.IntValue() + if err != nil { + return nil, fmt.Errorf("error getting index int value: %w", err) + } + + return data.GetSliceIndex(int(index)) + }, nil +} diff --git a/execution/execute_func.go b/execution/execute_func.go index d4e22a8e..e0319a50 100644 --- a/execution/execute_func.go +++ b/execution/execute_func.go @@ -25,7 +25,7 @@ func prepareArgs(data *model.Value, argsE ast.Expressions) (model.Values, error) return args, nil } -func callSingleExecutor(f singleResponseFunc, argsE ast.Expressions) (expressionExecutor, error) { +func callFnExecutor(f FuncFn, argsE ast.Expressions) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { args, err := prepareArgs(data, argsE) if err != nil { @@ -41,29 +41,13 @@ func callSingleExecutor(f singleResponseFunc, argsE ast.Expressions) (expression }, nil } -func callMultiExecutor(f multiResponseFunc, argsE ast.Expressions) (expressionExecutor, error) { - return func(data *model.Value) (*model.Value, error) { - panic("multi response functions are not supported") - //args, err := prepareArgs(data, argsE) - //if err != nil { - // return nil, fmt.Errorf("error preparing arguments: %w", err) - //} - - //res, err := f(data, args) - //if err != nil { - // return nil, fmt.Errorf("error executing function: %w", err) - //} - - //return res, nil - }, nil -} - func callExprExecutor(e ast.CallExpr) (expressionExecutor, error) { if f, ok := singleResponseFuncLookup[e.Function]; ok { - return callSingleExecutor(f, e.Args) - } - if f, ok := multiResponseFuncLookup[e.Function]; ok { - return callMultiExecutor(f, e.Args) + res, err := callFnExecutor(f, e.Args) + if err != nil { + return nil, fmt.Errorf("error executing function %q: %w", e.Function, err) + } + return res, nil } return nil, fmt.Errorf("unknown function: %q", e.Function) diff --git a/execution/execute_map.go b/execution/execute_map.go index 1bd0a441..ad42aade 100644 --- a/execution/execute_map.go +++ b/execution/execute_map.go @@ -12,24 +12,22 @@ func mapExprExecutor(e ast.MapExpr) (expressionExecutor, error) { if !data.IsSlice() { return nil, fmt.Errorf("cannot map over non-array") } - sliceLen, err := data.SliceLen() - if err != nil { - return nil, fmt.Errorf("error getting slice length: %w", err) - } res := model.NewSliceValue() - for i := 0; i < sliceLen; i++ { - item, err := data.GetSliceIndex(i) - if err != nil { - return nil, fmt.Errorf("error getting slice index: %w", err) - } + if err := data.RangeSlice(func(i int, item *model.Value) error { + var err error for _, expr := range e.Exprs { item, err = ExecuteAST(expr, item) if err != nil { - return nil, err + return err } } - res.Append(item) + if err := res.Append(item); err != nil { + return fmt.Errorf("error appending item to result: %w", err) + } + return nil + }); err != nil { + return nil, fmt.Errorf("error ranging over slice: %w", err) } return res, nil diff --git a/execution/execute_object.go b/execution/execute_object.go index 3bafc655..9e8091bf 100644 --- a/execution/execute_object.go +++ b/execution/execute_object.go @@ -46,3 +46,20 @@ func objectExprExecutor(e ast.ObjectExpr) (expressionExecutor, error) { return obj, nil }, nil } + +func propertyExprExecutor(e ast.PropertyExpr) (expressionExecutor, error) { + return func(data *model.Value) (*model.Value, error) { + key, err := ExecuteAST(e.Property, data) + if err != nil { + return nil, fmt.Errorf("error evaluating property: %w", err) + } + if !key.IsString() { + return nil, fmt.Errorf("expected property to resolve to string, got %s", key.Type()) + } + keyStr, err := key.StringValue() + if err != nil { + return nil, fmt.Errorf("error getting string value: %w", err) + } + return data.GetMapKey(keyStr) + }, nil +} diff --git a/execution/execute_spread.go b/execution/execute_spread.go index dbe221ab..152684e0 100644 --- a/execution/execute_spread.go +++ b/execution/execute_spread.go @@ -14,14 +14,13 @@ func spreadExprExecutor() (expressionExecutor, error) { switch { case data.IsSlice(): - v, err := data.SliceValue() - if err != nil { - return nil, fmt.Errorf("error getting slice value: %w", err) - } - for _, sv := range v { - if err := s.Append(model.NewValue(sv)); err != nil { - return nil, fmt.Errorf("error appending value to slice: %w", err) + if err := data.RangeSlice(func(key int, value *model.Value) error { + if err := s.Append(value); err != nil { + return fmt.Errorf("error appending value to slice: %w", err) } + return nil + }); err != nil { + return nil, fmt.Errorf("error ranging slice: %w", err) } case data.IsMap(): if err := data.RangeMap(func(key string, value *model.Value) error { diff --git a/execution/execute_test.go b/execution/execute_test.go index c2691f8c..f98a45aa 100644 --- a/execution/execute_test.go +++ b/execution/execute_test.go @@ -6,8 +6,8 @@ import ( "github.com/google/go-cmp/cmp" "github.com/tomwright/dasel/v3/dencoding" "github.com/tomwright/dasel/v3/execution" + "github.com/tomwright/dasel/v3/internal/ptr" "github.com/tomwright/dasel/v3/model" - "github.com/tomwright/dasel/v3/ptr" ) func TestExecuteSelector_HappyPath(t *testing.T) { @@ -37,7 +37,11 @@ func TestExecuteSelector_HappyPath(t *testing.T) { t.Fatal(err) } - if !res.EqualTypeValue(exp) { + equal, err := res.EqualTypeValue(exp) + if err != nil { + t.Fatal(err) + } + if !equal { t.Errorf("unexpected type: %v", cmp.Diff(exp, res)) } } @@ -264,11 +268,16 @@ func TestExecuteSelector_HappyPath(t *testing.T) { s: `numbers.one + numbers.add(two, three)`, out: model.NewIntValue(6), })) - t.Run("add with map and spread on slice with $this addition", runTest(testCase{ + t.Run("add with map and spread on slice with $this addition and grouping", runTest(testCase{ inFn: in, s: `add(nums.map(($this + 1))...)`, out: model.NewIntValue(9), })) + t.Run("add with map and spread on slice with $this addition", runTest(testCase{ + inFn: in, + s: `add(nums.map($this + 1 - 2)...)`, + out: model.NewIntValue(3), + })) }) }) }) diff --git a/execution/func.go b/execution/func.go index 3d4343be..fa54a36b 100644 --- a/execution/func.go +++ b/execution/func.go @@ -6,23 +6,16 @@ import ( "github.com/tomwright/dasel/v3/model" ) -type singleResponseFunc func(data *model.Value, args model.Values) (*model.Value, error) +type FuncFn func(data *model.Value, args model.Values) (*model.Value, error) -type multiResponseFunc func(data *model.Value, args model.Values) (model.Values, error) +var singleResponseFuncLookup = map[string]FuncFn{} -var singleResponseFuncLookup = map[string]singleResponseFunc{} -var multiResponseFuncLookup = map[string]multiResponseFunc{} - -func registerFunc(name string, fn singleResponseFunc) { +func RegisterFunc(name string, fn FuncFn) { singleResponseFuncLookup[name] = fn } -func registerMultiResponseFunc(name string, fn multiResponseFunc) { - multiResponseFuncLookup[name] = fn -} - func init() { - registerFunc("add", func(_ *model.Value, args model.Values) (*model.Value, error) { + RegisterFunc("add", func(_ *model.Value, args model.Values) (*model.Value, error) { var foundInts, foundFloats int var intRes int64 var floatRes float64 diff --git a/ptr/to.go b/internal/ptr/to.go similarity index 51% rename from ptr/to.go rename to internal/ptr/to.go index 6c3ee9bd..ce6ed25c 100644 --- a/ptr/to.go +++ b/internal/ptr/to.go @@ -1,5 +1,6 @@ package ptr +// To returns a pointer to the value passed in. func To[T any](v T) *T { return &v } diff --git a/model/error.go b/model/error.go index 3977c68c..b70fbb0a 100644 --- a/model/error.go +++ b/model/error.go @@ -2,10 +2,33 @@ package model import "fmt" +// MapKeyNotFound is returned when a key is not found in a map. type MapKeyNotFound struct { Key string } +// Error returns the error message. func (e *MapKeyNotFound) Error() string { return fmt.Sprintf("map key not found: %q", e.Key) } + +// SliceIndexOutOfRange is returned when an index is invalid. +type SliceIndexOutOfRange struct { + Index int +} + +// Error returns the error message. +func (e *SliceIndexOutOfRange) Error() string { + return fmt.Sprintf("slice index out of range: %d", e.Index) +} + +// ErrIncompatibleTypes is returned when two values are incompatible. +type ErrIncompatibleTypes struct { + A *Value + B *Value +} + +// Error returns the error message. +func (e *ErrIncompatibleTypes) Error() string { + return fmt.Sprintf("incompatible types: %s and %s", e.A.Type(), e.B.Type()) +} diff --git a/model/value_comparison.go b/model/value_comparison.go index 6b7b61c0..66049e16 100644 --- a/model/value_comparison.go +++ b/model/value_comparison.go @@ -1,28 +1,6 @@ package model func (v *Value) Equal(other *Value) (*Value, error) { - if v.IsInt() && other.IsInt() { - a, err := v.IntValue() - if err != nil { - return nil, err - } - b, err := other.IntValue() - if err != nil { - return nil, err - } - return NewValue(a == b), nil - } - if v.IsFloat() && other.IsFloat() { - a, err := v.FloatValue() - if err != nil { - return nil, err - } - b, err := other.FloatValue() - if err != nil { - return nil, err - } - return NewValue(a == b), nil - } if v.IsInt() && other.IsFloat() { a, err := v.IntValue() if err != nil { @@ -45,7 +23,16 @@ func (v *Value) Equal(other *Value) (*Value, error) { } return NewValue(a == float64(b)), nil } - return nil, &ErrIncompatibleTypes{A: v, B: other} + + if v.Type() != other.Type() { + return nil, &ErrIncompatibleTypes{A: v, B: other} + } + + isEqual, err := v.EqualTypeValue(other) + if err != nil { + return nil, err + } + return NewValue(isEqual), nil } func (v *Value) NotEqual(other *Value) (*Value, error) { @@ -152,57 +139,113 @@ func (v *Value) GreaterThanOrEqual(other *Value) (*Value, error) { return NewValue(!boolValue), nil } -func (v *Value) EqualTypeValue(other *Value) bool { +func (v *Value) EqualTypeValue(other *Value) (bool, error) { if v.Type() != other.Type() { - return false + return false, nil } switch v.Type() { case TypeString: - a, _ := v.StringValue() - b, _ := other.StringValue() - return a == b + a, err := v.StringValue() + if err != nil { + return false, err + } + b, err := other.StringValue() + if err != nil { + return false, err + } + return a == b, nil case TypeInt: - a, _ := v.IntValue() - b, _ := other.IntValue() - return a == b + a, err := v.IntValue() + if err != nil { + return false, err + } + b, err := other.IntValue() + if err != nil { + return false, err + } + return a == b, nil case TypeFloat: - a, _ := v.FloatValue() - b, _ := other.FloatValue() - return a == b + a, err := v.FloatValue() + if err != nil { + return false, err + } + b, err := other.FloatValue() + if err != nil { + return false, err + } + return a == b, nil case TypeBool: - a, _ := v.BoolValue() - b, _ := other.BoolValue() - return a == b + a, err := v.BoolValue() + if err != nil { + return false, err + } + b, err := other.BoolValue() + if err != nil { + return false, err + } + return a == b, nil case TypeMap: - a, _ := v.MapKeys() - b, _ := other.MapKeys() + a, err := v.MapKeys() + if err != nil { + return false, err + } + b, err := other.MapKeys() + if err != nil { + return false, err + } if len(a) != len(b) { - return false + return false, nil } for _, key := range a { - valA, _ := v.GetMapKey(key) - valB, _ := other.GetMapKey(key) - if !valA.EqualTypeValue(valB) { - return false + valA, err := v.GetMapKey(key) + if err != nil { + return false, err + } + valB, err := other.GetMapKey(key) + if err != nil { + return false, err + } + equal, err := valA.EqualTypeValue(valB) + if err != nil { + return false, err + } + if !equal { + return false, nil } } - return true + return true, nil case TypeSlice: - a, _ := v.SliceLen() - b, _ := other.SliceLen() + a, err := v.SliceLen() + if err != nil { + return false, err + } + b, err := other.SliceLen() + if err != nil { + return false, err + } if a != b { - return false + return false, nil } for i := 0; i < a; i++ { - valA, _ := v.GetSliceIndex(i) - valB, _ := other.GetSliceIndex(i) - if !valA.EqualTypeValue(valB) { - return false + valA, err := v.GetSliceIndex(i) + if err != nil { + return false, err + } + valB, err := other.GetSliceIndex(i) + if err != nil { + return false, err + } + equal, err := valA.EqualTypeValue(valB) + if err != nil { + return false, err + } + if !equal { + return false, nil } } - return true + return true, nil default: - return false + return false, nil } } diff --git a/model/value_map.go b/model/value_map.go index 485a8d16..7578eb4f 100644 --- a/model/value_map.go +++ b/model/value_map.go @@ -7,10 +7,12 @@ import ( "github.com/tomwright/dasel/v3/dencoding" ) +// NewMapValue creates a new map value. func NewMapValue() *Value { return NewValue(dencoding.NewMap()) } +// IsMap returns true if the value is a map. func (v *Value) IsMap() bool { return v.isStandardMap() || v.isDencodingMap() } diff --git a/model/value_math.go b/model/value_math.go index 94699dc7..eb492677 100644 --- a/model/value_math.go +++ b/model/value_math.go @@ -1,19 +1,9 @@ package model import ( - "fmt" "math" ) -type ErrIncompatibleTypes struct { - A *Value - B *Value -} - -func (e *ErrIncompatibleTypes) Error() string { - return fmt.Sprintf("incompatible types: %s and %s", e.A.Type(), e.B.Type()) -} - func (v *Value) Add(other *Value) (*Value, error) { if v.IsInt() && other.IsInt() { a, err := v.IntValue() diff --git a/model/value_metadata.go b/model/value_metadata.go index e1193fe0..a939c584 100644 --- a/model/value_metadata.go +++ b/model/value_metadata.go @@ -1,5 +1,6 @@ package model +// MetadataValue returns a metadata value. func (v *Value) MetadataValue(key string) (any, bool) { if v.Metadata == nil { return nil, false @@ -8,6 +9,7 @@ func (v *Value) MetadataValue(key string) (any, bool) { return val, ok } +// SetMetadataValue sets a metadata value. func (v *Value) SetMetadataValue(key string, val any) { if v.Metadata == nil { v.Metadata = map[string]any{} @@ -15,6 +17,8 @@ func (v *Value) SetMetadataValue(key string, val any) { v.Metadata[key] = val } +// IsSpread returns true if the value is a spread value. +// Spread values are used to represent the spread operator. func (v *Value) IsSpread() bool { val, ok := v.Metadata["spread"] if !ok { @@ -27,6 +31,8 @@ func (v *Value) IsSpread() bool { return spread } +// MarkAsSpread marks the value as a spread value. +// Spread values are used to represent the spread operator. func (v *Value) MarkAsSpread() { v.SetMetadataValue("spread", true) } diff --git a/model/value_slice.go b/model/value_slice.go index c515159b..c3efacea 100644 --- a/model/value_slice.go +++ b/model/value_slice.go @@ -5,6 +5,7 @@ import ( "reflect" ) +// NewSliceValue returns a new slice value. func NewSliceValue() *Value { s := reflect.MakeSlice(reflect.SliceOf(reflect.TypeFor[any]()), 0, 0) ptr := reflect.New(reflect.SliceOf(reflect.TypeFor[any]())) @@ -12,18 +13,7 @@ func NewSliceValue() *Value { return NewValue(ptr) } -func (v *Value) SliceValue() ([]any, error) { - unpacked := v.UnpackKinds(reflect.Interface, reflect.Ptr) - if !unpacked.IsSlice() { - return nil, fmt.Errorf("expected slice, got %s", v.Type()) - } - res, ok := unpacked.Interface().([]any) - if !ok { - return nil, fmt.Errorf("could not convert slice to []interface{}") - } - return res, nil -} - +// IsSlice returns true if the value is a slice. func (v *Value) IsSlice() bool { return v.UnpackKinds(reflect.Interface, reflect.Ptr).isSlice() } @@ -96,3 +86,33 @@ func (v *Value) RangeSlice(f func(int, *Value) error) error { return nil } + +// SliceIndexRange returns a new slice containing the values between the start and end indexes. +// Comparable to go's slice[start:end]. +// If start is -1, it will be treated as 0. e.g. slice[:end] becomes slice[-1:end]. +// If end is -1, it will be treated as the length of the slice. e.g. slice[start:] becomes slice[start:-1]. +func (v *Value) SliceIndexRange(start, end int) (*Value, error) { + var err error + if start == -1 { + start = 0 + } + if end == -1 { + end, err = v.SliceLen() + if err != nil { + return nil, fmt.Errorf("error getting slice length: %w", err) + } + } + + res := NewSliceValue() + for i := start; i < end; i++ { + item, err := v.GetSliceIndex(i) + if err != nil { + return nil, fmt.Errorf("error getting slice index: %w", err) + } + if err := res.Append(item); err != nil { + return nil, fmt.Errorf("error appending value to slice: %w", err) + } + } + + return res, nil +} diff --git a/model/value_slice_test.go b/model/value_slice_test.go index b6f00bb6..dc8bb572 100644 --- a/model/value_slice_test.go +++ b/model/value_slice_test.go @@ -3,26 +3,195 @@ package model_test import ( "testing" - "github.com/google/go-cmp/cmp" "github.com/tomwright/dasel/v3/model" - "github.com/tomwright/dasel/v3/ptr" ) -func TestNewSliceValue(t *testing.T) { - x := model.NewSliceValue() - if err := x.Append(model.NewStringValue("hello")); err != nil { - t.Errorf("unexpected error: %s", err) - } - if err := x.Append(model.NewStringValue("world")); err != nil { - t.Errorf("unexpected error: %s", err) +func TestSlice(t *testing.T) { + standardSlice := func() *model.Value { + return model.NewValue([]any{"foo", "bar"}) } - got, err := x.SliceValue() - if err != nil { - t.Errorf("unexpected error: %s", err) + modelSlice := func() *model.Value { + res := model.NewSliceValue() + if err := res.Append(model.NewValue("foo")); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := res.Append(model.NewValue("bar")); err != nil { + t.Fatalf("unexpected error: %s", err) + } + return res } - exp := []any{ptr.To("hello"), ptr.To("world")} - if !cmp.Equal(exp, got) { - t.Errorf("unexpected result: %s", cmp.Diff(exp, got)) + + runTests := func(v func() *model.Value) func(t *testing.T) { + return func(t *testing.T) { + t.Run("IsSlice", func(t *testing.T) { + v := v() + if !v.IsSlice() { + t.Errorf("expected value to be a slice") + } + }) + t.Run("GetSliceIndex", func(t *testing.T) { + v := v() + foo, err := v.GetSliceIndex(0) + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + got, err := foo.StringValue() + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if got != "foo" { + t.Errorf("expected foo, got %s", got) + } + }) + t.Run("SetSliceIndex", func(t *testing.T) { + v := v() + if err := v.SetSliceIndex(0, model.NewValue("baz")); err != nil { + t.Errorf("unexpected error: %s", err) + return + } + baz, err := v.GetSliceIndex(0) + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + got, err := baz.StringValue() + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if got != "baz" { + t.Errorf("expected baz, got %s", got) + } + }) + t.Run("Len", func(t *testing.T) { + v := v() + got, err := v.SliceLen() + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if got != 2 { + t.Errorf("expected len of 2, got %d", got) + } + }) + t.Run("RangeSlice", func(t *testing.T) { + v := v() + var keys []int + var vals []string + err := v.RangeSlice(func(k int, v *model.Value) error { + keys = append(keys, k) + s, err := v.StringValue() + if err != nil { + return err + } + vals = append(vals, s) + return nil + }) + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if len(keys) != 2 { + t.Errorf("expected 2 keys, got %d", len(keys)) + } + if len(vals) != 2 { + t.Errorf("expected 2 vals, got %d", len(keys)) + } + exp := []string{"foo", "bar"} + + for k, e := range exp { + if keys[k] != k { + t.Errorf("expected key %d, got %d", k, keys[k]) + } + if vals[k] != e { + t.Errorf("expected val %s, got %s", e, vals[k]) + } + } + }) + //t.Run("DeleteMapKey", func(t *testing.T) { + // v := v() + // if _, err := v.GetSliceIndex(1); err != nil { + // t.Errorf("unexpected error: %s", err) + // return + // } + // if err := v.DeleteSliceIndex(1); err != nil { + // t.Errorf("unexpected error: %s", err) + // return + // } + // _, err := v.GetSliceIndex(1) + // notFoundErr := &model.SliceIndexOutOfRange{} + // if !errors.As(err, ¬FoundErr) { + // t.Errorf("expected index not found error, got %s", err) + // } + //}) + t.Run("SliceIndexRange", func(t *testing.T) { + t.Run("end 1", func(t *testing.T) { + v := v() + s, err := v.SliceIndexRange(-1, 1) + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + length, err := s.SliceLen() + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if length != 1 { + t.Errorf("expected length of 1, got %d", length) + } + + val, err := s.GetSliceIndex(0) + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + got, err := val.StringValue() + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if got != "foo" { + t.Errorf("expected foo, got %s", got) + } + }) + t.Run("start 1", func(t *testing.T) { + v := v() + s, err := v.SliceIndexRange(1, -1) + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + length, err := s.SliceLen() + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if length != 1 { + t.Errorf("expected length of 1, got %d", length) + } + + val, err := s.GetSliceIndex(0) + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + got, err := val.StringValue() + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if got != "bar" { + t.Errorf("expected foo, got %s", got) + } + }) + }) + } } + + t.Run("standard slice", runTests(standardSlice)) + t.Run("model slice", runTests(modelSlice)) } diff --git a/parsing/format.go b/parsing/format.go index 0a7b8588..951b0eec 100644 --- a/parsing/format.go +++ b/parsing/format.go @@ -6,26 +6,34 @@ import ( "github.com/tomwright/dasel/v3/model" ) +// Format represents a file format. type Format string +// Supported file formats. const ( JSON Format = "json" YAML Format = "yaml" TOML Format = "toml" ) +// String returns the string representation of the format. func (f Format) String() string { return string(f) } +// Reader reads a value from a byte slice. type Reader interface { + // Read reads a value from a byte slice. Read([]byte) (*model.Value, error) } +// Writer writes a value to a byte slice. type Writer interface { + // Write writes a value to a byte slice. Write(*model.Value) ([]byte, error) } +// NewReader creates a new reader for the specified format. func NewReader(format Format) (Reader, error) { switch format { case JSON: @@ -39,6 +47,7 @@ func NewReader(format Format) (Reader, error) { } } +// NewWriter creates a new writer for the specified format. func NewWriter(format Format) (Writer, error) { switch format { case JSON: diff --git a/parsing/json.go b/parsing/json.go index 6a10033c..cfa4750c 100644 --- a/parsing/json.go +++ b/parsing/json.go @@ -6,16 +6,19 @@ import ( "github.com/tomwright/dasel/v3/model" ) +// NewJSONReader creates a new JSON reader. func NewJSONReader() (Reader, error) { return &jsonReader{}, nil } +// NewJSONWriter creates a new JSON writer. func NewJSONWriter() (Writer, error) { return &jsonWriter{}, nil } type jsonReader struct{} +// Read reads a value from a byte slice. func (j *jsonReader) Read(data []byte) (*model.Value, error) { var unmarshalled any if err := json.Unmarshal(data, &unmarshalled); err != nil { @@ -26,6 +29,7 @@ func (j *jsonReader) Read(data []byte) (*model.Value, error) { type jsonWriter struct{} +// Write writes a value to a byte slice. func (j *jsonWriter) Write(value *model.Value) ([]byte, error) { return json.Marshal(value.Interface()) } diff --git a/parsing/toml.go b/parsing/toml.go index 4bbcbdf7..77195d87 100644 --- a/parsing/toml.go +++ b/parsing/toml.go @@ -2,22 +2,26 @@ package parsing import "github.com/tomwright/dasel/v3/model" +// NewTOMLReader creates a new TOML reader. func NewTOMLReader() (Reader, error) { return &tomlReader{}, nil } +// NewTOMLWriter creates a new TOML writer. func NewTOMLWriter() (Writer, error) { return &tomlWriter{}, nil } type tomlReader struct{} +// Read reads a value from a byte slice. func (j *tomlReader) Read(data []byte) (*model.Value, error) { panic("not implemented") } type tomlWriter struct{} +// Write writes a value to a byte slice. func (j *tomlWriter) Write(value *model.Value) ([]byte, error) { panic("not implemented") } diff --git a/parsing/yaml.go b/parsing/yaml.go index cdfe71a2..1818a2cf 100644 --- a/parsing/yaml.go +++ b/parsing/yaml.go @@ -2,22 +2,26 @@ package parsing import "github.com/tomwright/dasel/v3/model" +// NewYAMLReader creates a new YAML reader. func NewYAMLReader() (Reader, error) { return &yamlReader{}, nil } +// NewYAMLWriter creates a new YAML writer. func NewYAMLWriter() (Writer, error) { return &yamlWriter{}, nil } type yamlReader struct{} +// Read reads a value from a byte slice. func (j *yamlReader) Read(data []byte) (*model.Value, error) { panic("not implemented") } type yamlWriter struct{} +// Write writes a value to a byte slice. func (j *yamlWriter) Write(value *model.Value) ([]byte, error) { panic("not implemented") } diff --git a/selector/parser/parse_map.go b/selector/parser/parse_map.go index 3e69e6fc..b8e5062f 100644 --- a/selector/parser/parse_map.go +++ b/selector/parser/parse_map.go @@ -28,7 +28,7 @@ func parseMap(p *Parser) (ast.Expr, error) { []lexer.TokenKind{lexer.CloseParen}, []lexer.TokenKind{}, true, - bpCall, + bpDefault, ) if err != nil { return nil, err diff --git a/selector/parser/parser.go b/selector/parser/parser.go index 49eff2d5..aefd9343 100644 --- a/selector/parser/parser.go +++ b/selector/parser/parser.go @@ -41,22 +41,30 @@ func (p *Parser) currentScope() scope { } func (p *Parser) endOfExpressionTokens() []lexer.TokenKind { + allowLeftDonation := true + var tokens []lexer.TokenKind switch p.currentScope() { case scopeRoot: - return append([]lexer.TokenKind{lexer.EOF, lexer.Dot}, leftDenotationTokens...) + tokens = append(tokens, lexer.EOF, lexer.Dot) case scopeFuncArgs: - return []lexer.TokenKind{lexer.Comma, lexer.CloseParen} + tokens = append(tokens, lexer.Comma, lexer.CloseParen) case scopeMap: - return []lexer.TokenKind{lexer.Comma, lexer.CloseParen, lexer.Dot, lexer.Spread} + tokens = append(tokens, lexer.Comma, lexer.CloseParen, lexer.Dot, lexer.Spread) case scopeArray: - return []lexer.TokenKind{lexer.CloseBracket, lexer.Colon, lexer.Number, lexer.Symbol, lexer.Spread} + tokens = append(tokens, lexer.CloseBracket, lexer.Colon, lexer.Number, lexer.Symbol, lexer.Spread) case scopeObject: - return []lexer.TokenKind{lexer.CloseCurly, lexer.Equals, lexer.Number, lexer.Symbol, lexer.Comma} + tokens = append(tokens, lexer.CloseCurly, lexer.Equals, lexer.Number, lexer.Symbol, lexer.Comma) case scopeGroup: - return append([]lexer.TokenKind{lexer.CloseParen, lexer.Dot}, leftDenotationTokens...) + tokens = append(tokens, lexer.CloseParen, lexer.Dot) default: - return nil + allowLeftDonation = false + } + + if allowLeftDonation { + tokens = append(tokens, leftDenotationTokens...) } + + return tokens } func (p *Parser) expectEndOfExpression() error { From 1f26dbd6b35e79d5172eb55e6d85b9bbcdbb8691 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Sat, 5 Oct 2024 23:23:50 +0100 Subject: [PATCH 14/56] Add if/elseif/else statements --- execution/execute.go | 2 + execution/execute_conditional.go | 32 +++++++++ execution/execute_test.go | 31 +++++++++ func_len_test.go | 4 +- internal/command/delete.go | 2 +- internal/command/put.go | 2 +- internal/command/select.go | 2 +- model/value.go | 6 +- model/value_literal.go | 12 ++++ selector/ast/expression_complex.go | 8 +++ selector/lexer/token.go | 3 + selector/lexer/tokenize.go | 35 ++++++++-- selector/lexer/tokenize_test.go | 9 +++ selector/parser/parse_if.go | 104 +++++++++++++++++++++++++++++ selector/parser/parser.go | 13 ++-- selector/parser/parser_test.go | 63 +++++++++++++++++ 16 files changed, 309 insertions(+), 19 deletions(-) create mode 100644 execution/execute_conditional.go create mode 100644 selector/parser/parse_if.go diff --git a/execution/execute.go b/execution/execute.go index 0ac9c8d2..8047c808 100644 --- a/execution/execute.go +++ b/execution/execute.go @@ -76,6 +76,8 @@ func exprExecutor(expr ast.Expr) (expressionExecutor, error) { return objectExprExecutor(e) case ast.MapExpr: return mapExprExecutor(e) + case ast.ConditionalExpr: + return conditionalExprExecutor(e) default: return nil, fmt.Errorf("unhandled expression type: %T", e) } diff --git a/execution/execute_conditional.go b/execution/execute_conditional.go new file mode 100644 index 00000000..53fe2e59 --- /dev/null +++ b/execution/execute_conditional.go @@ -0,0 +1,32 @@ +package execution + +import ( + "fmt" + + "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/selector/ast" +) + +func conditionalExprExecutor(e ast.ConditionalExpr) (expressionExecutor, error) { + return func(data *model.Value) (*model.Value, error) { + cond, err := ExecuteAST(e.Cond, data) + if err != nil { + return nil, fmt.Errorf("error evaluating condition: %w", err) + } + + condBool, err := cond.BoolValue() + if err != nil { + return nil, fmt.Errorf("error converting condition to boolean: %w", err) + } + + if condBool { + return ExecuteAST(e.Then, data) + } + + if e.Else != nil { + return ExecuteAST(e.Else, data) + } + + return model.NewNullValue(), nil + }, nil +} diff --git a/execution/execute_test.go b/execution/execute_test.go index f98a45aa..d5617439 100644 --- a/execution/execute_test.go +++ b/execution/execute_test.go @@ -422,4 +422,35 @@ func TestExecuteSelector_HappyPath(t *testing.T) { }, })) }) + + t.Run("conditional", func(t *testing.T) { + t.Run("true", runTest(testCase{ + s: `if (true) { "yes" } else { "no" }`, + out: model.NewStringValue("yes"), + })) + t.Run("false", runTest(testCase{ + s: `if (false) { "yes" } else { "no" }`, + out: model.NewStringValue("no"), + })) + t.Run("nested", runTest(testCase{ + s: `if (true) { if (true) { "yes" } else { "no" } } else { "no" }`, + out: model.NewStringValue("yes"), + })) + t.Run("nested false", runTest(testCase{ + s: `if (true) { if (false) { "yes" } else { "no" } } else { "no" }`, + out: model.NewStringValue("no"), + })) + t.Run("else if", runTest(testCase{ + s: `if (false) { "yes" } elseif (true) { "no" } else { "maybe" }`, + out: model.NewStringValue("no"), + })) + t.Run("else if else", runTest(testCase{ + s: `if (false) { "yes" } elseif (false) { "no" } else { "maybe" }`, + out: model.NewStringValue("maybe"), + })) + t.Run("if elseif elseif else", runTest(testCase{ + s: `if (false) { "yes" } elseif (false) { "no" } elseif (true) { "maybe" } else { "nope" }`, + out: model.NewStringValue("maybe"), + })) + }) } diff --git a/func_len_test.go b/func_len_test.go index da722afd..36876202 100644 --- a/func_len_test.go +++ b/func_len_test.go @@ -40,7 +40,7 @@ func TestLenFunc(t *testing.T) { ), ) t.Run( - "False Bool", + "Else Bool", selectTest( "falseBool.len()", data, @@ -48,7 +48,7 @@ func TestLenFunc(t *testing.T) { ), ) t.Run( - "True Bool", + "Then Bool", selectTest( "trueBool.len()", data, diff --git a/internal/command/delete.go b/internal/command/delete.go index e4443109..7a7df85b 100644 --- a/internal/command/delete.go +++ b/internal/command/delete.go @@ -31,7 +31,7 @@ func deleteFlags(cmd *cobra.Command) { cmd.Flags().String("csv-comma", ",", "Comma separator to use when working with csv files.") cmd.Flags().String("csv-write-comma", "", "Comma separator used when writing csv files. Overrides csv-comma when writing.") cmd.Flags().String("csv-comment", "", "Comma separator used when reading csv files.") - cmd.Flags().Bool("csv-crlf", false, "True to use CRLF when writing CSV files.") + cmd.Flags().Bool("csv-crlf", false, "Then to use CRLF when writing CSV files.") _ = cmd.MarkFlagFilename("file") } diff --git a/internal/command/put.go b/internal/command/put.go index 00e5bd7e..e9ad7fcb 100644 --- a/internal/command/put.go +++ b/internal/command/put.go @@ -37,7 +37,7 @@ func putFlags(cmd *cobra.Command) { cmd.Flags().String("csv-comma", ",", "Comma separator to use when working with csv files.") cmd.Flags().String("csv-write-comma", "", "Comma separator used when writing csv files. Overrides csv-comma when writing.") cmd.Flags().String("csv-comment", "", "Comma separator used when reading csv files.") - cmd.Flags().Bool("csv-crlf", false, "True to use CRLF when writing CSV files.") + cmd.Flags().Bool("csv-crlf", false, "Then to use CRLF when writing CSV files.") _ = cmd.MarkFlagFilename("file") } diff --git a/internal/command/select.go b/internal/command/select.go index 4367e727..181c09d8 100644 --- a/internal/command/select.go +++ b/internal/command/select.go @@ -30,7 +30,7 @@ func selectFlags(cmd *cobra.Command) { cmd.Flags().String("csv-comma", ",", "Comma separator to use when working with csv files.") cmd.Flags().String("csv-write-comma", "", "Comma separator used when writing csv files. Overrides csv-comma when writing.") cmd.Flags().String("csv-comment", "", "Comma separator used when reading csv files.") - cmd.Flags().Bool("csv-crlf", false, "True to use CRLF when writing CSV files.") + cmd.Flags().Bool("csv-crlf", false, "Then to use CRLF when writing CSV files.") _ = cmd.MarkFlagFilename("file") } diff --git a/model/value.go b/model/value.go index 1a4ff5e0..9d12fdc7 100644 --- a/model/value.go +++ b/model/value.go @@ -57,7 +57,7 @@ func (v *Value) Kind() reflect.Kind { func (v *Value) UnpackKinds(kinds ...reflect.Kind) *Value { res := v.Value for { - if !slices.Contains(kinds, res.Kind()) { + if !slices.Contains(kinds, res.Kind()) || res.IsNil() { return NewValue(res) } res = res.Elem() @@ -70,7 +70,7 @@ func (v *Value) UnpackUntilType(t reflect.Type) (*Value, error) { if res.Type() == t { return NewValue(res), nil } - if res.Kind() == reflect.Interface || res.Kind() == reflect.Ptr { + if res.Kind() == reflect.Interface || res.Kind() == reflect.Ptr && !res.IsNil() { res = res.Elem() continue } @@ -84,7 +84,7 @@ func (v *Value) UnpackUntilKind(k reflect.Kind) (*Value, error) { if res.Kind() == k { return NewValue(res), nil } - if res.Kind() == reflect.Interface || res.Kind() == reflect.Ptr { + if res.Kind() == reflect.Interface || res.Kind() == reflect.Ptr && !res.IsNil() { res = res.Elem() continue } diff --git a/model/value_literal.go b/model/value_literal.go index 9be3af85..bd15f0d7 100644 --- a/model/value_literal.go +++ b/model/value_literal.go @@ -6,6 +6,18 @@ import ( "slices" ) +func NewNullValue() *Value { + return NewValue(reflect.New(reflect.TypeFor[any]())) +} + +func (v *Value) IsNull() bool { + return v.UnpackKinds(reflect.Ptr, reflect.Interface).isNull() +} + +func (v *Value) isNull() bool { + return v.Value.IsNil() +} + func NewStringValue(x string) *Value { res := reflect.New(reflect.TypeFor[string]()) res.Elem().Set(reflect.ValueOf(x)) diff --git a/selector/ast/expression_complex.go b/selector/ast/expression_complex.go index 8c20947f..4c3440e0 100644 --- a/selector/ast/expression_complex.go +++ b/selector/ast/expression_complex.go @@ -98,3 +98,11 @@ type GroupExpr struct { } func (GroupExpr) expr() {} + +type ConditionalExpr struct { + Cond Expr + Then Expr + Else Expr +} + +func (ConditionalExpr) expr() {} diff --git a/selector/lexer/token.go b/selector/lexer/token.go index b018d143..4ce78e8e 100644 --- a/selector/lexer/token.go +++ b/selector/lexer/token.go @@ -47,6 +47,9 @@ const ( LessThanOrEqual Exclamation Null + If + Else + ElseIf ) type Tokens []Token diff --git a/selector/lexer/tokenize.go b/selector/lexer/tokenize.go index 82c5eb72..e4ef7909 100644 --- a/selector/lexer/tokenize.go +++ b/selector/lexer/tokenize.go @@ -3,6 +3,8 @@ package lexer import ( "strings" "unicode" + + "github.com/tomwright/dasel/v3/internal/ptr" ) type Tokenizer struct { @@ -172,14 +174,35 @@ func (p *Tokenizer) parseCurRune() (Token, error) { default: pos := p.i - if pos+3 < p.srcLen && strings.EqualFold(p.src[pos:pos+4], "null") { - return NewToken(Null, p.src[pos:pos+4], p.i, 4), nil + matchStr := func(pos int, m string, caseInsensitive bool, kind TokenKind) *Token { + l := len(m) + if pos+(l-1) >= p.srcLen { + return nil + } + other := p.src[pos : pos+l] + if m == other || caseInsensitive && strings.EqualFold(m, other) { + return ptr.To(NewToken(kind, other, pos, l)) + } + return nil + } + + if t := matchStr(pos, "null", true, Null); t != nil { + return *t, nil + } + if t := matchStr(pos, "true", true, Bool); t != nil { + return *t, nil + } + if t := matchStr(pos, "false", true, Bool); t != nil { + return *t, nil + } + if t := matchStr(pos, "elseif", false, ElseIf); t != nil { + return *t, nil } - if pos+3 < p.srcLen && strings.EqualFold(p.src[pos:pos+4], "true") { - return NewToken(Bool, p.src[pos:pos+4], p.i, 4), nil + if t := matchStr(pos, "if", false, If); t != nil { + return *t, nil } - if pos+4 < p.srcLen && strings.EqualFold(p.src[pos:pos+5], "false") { - return NewToken(Bool, p.src[pos:pos+5], p.i, 5), nil + if t := matchStr(pos, "else", false, Else); t != nil { + return *t, nil } if unicode.IsDigit(rune(p.src[pos])) { diff --git a/selector/lexer/tokenize_test.go b/selector/lexer/tokenize_test.go index add571a1..2bd21054 100644 --- a/selector/lexer/tokenize_test.go +++ b/selector/lexer/tokenize_test.go @@ -37,6 +37,15 @@ func TestTokenizer_Parse(t *testing.T) { }, })) + t.Run("if", runTest(testCase{ + in: `if elseif else`, + out: []TokenKind{ + If, + ElseIf, + Else, + }, + })) + t.Run("everything", runTest(testCase{ in: "foo.bar.baz[1] != 42.123 || foo.bar.baz['hello'] == 42 && x == 'a\\'b' + false true . .... asd... $name null", out: []TokenKind{ diff --git a/selector/parser/parse_if.go b/selector/parser/parse_if.go new file mode 100644 index 00000000..41b7a60b --- /dev/null +++ b/selector/parser/parse_if.go @@ -0,0 +1,104 @@ +package parser + +import ( + "github.com/tomwright/dasel/v3/selector/ast" + "github.com/tomwright/dasel/v3/selector/lexer" +) + +func parseIfBody(p *Parser) (ast.Expr, error) { + return p.parseExpressionsFromTo(lexer.OpenCurly, lexer.CloseCurly, []lexer.TokenKind{}, true, bpDefault) +} + +func parseIfCondition(p *Parser) (ast.Expr, error) { + return p.parseExpressionsFromTo(lexer.OpenParen, lexer.CloseParen, []lexer.TokenKind{}, true, bpDefault) +} + +func parseIf(p *Parser) (ast.Expr, error) { + p.pushScope(scopeIf) + defer p.popScope() + + if err := p.expect(lexer.If); err != nil { + return nil, err + } + p.advance() + + var exprs []*ast.ConditionalExpr + + process := func(parseCond bool) error { + var err error + var cond ast.Expr + if parseCond { + cond, err = parseIfCondition(p) + if err != nil { + return err + } + } + + body, err := parseIfBody(p) + if err != nil { + return err + } + + exprs = append(exprs, &ast.ConditionalExpr{ + Cond: cond, + Then: body, + }) + + return nil + } + + if err := process(true); err != nil { + return nil, err + } + + for p.current().IsKind(lexer.ElseIf) { + p.advance() + + if err := process(true); err != nil { + return nil, err + } + } + + if p.current().IsKind(lexer.Else) { + p.advance() + + body, err := parseIfBody(p) + if err != nil { + return nil, err + } + exprs[len(exprs)-1].Else = body + } + + for i := len(exprs) - 1; i >= 0; i-- { + if i > 0 { + exprs[i-1].Else = *exprs[i] + } + } + + return *exprs[0], nil +} + +func (p *Parser) parseExpressionsFromTo( + from lexer.TokenKind, + to lexer.TokenKind, + splitOn []lexer.TokenKind, + requireExpressions bool, + bp bindingPower, +) (ast.Expr, error) { + if err := p.expect(from); err != nil { + return nil, err + } + p.advance() + + t, err := p.parseExpressions( + []lexer.TokenKind{to}, + splitOn, + requireExpressions, + bp, + ) + if err != nil { + return nil, err + } + + return t, nil +} diff --git a/selector/parser/parser.go b/selector/parser/parser.go index aefd9343..7e6c3a27 100644 --- a/selector/parser/parser.go +++ b/selector/parser/parser.go @@ -17,6 +17,7 @@ const ( scopeObject scope = "object" scopeMap scope = "map" scopeGroup scope = "group" + scopeIf scope = "if" ) type Parser struct { @@ -144,11 +145,11 @@ 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() - } - }() + //defer func() { + // if err == nil { + // err = p.expectEndOfExpression() + // } + //}() switch p.current().Kind { case lexer.String: @@ -169,6 +170,8 @@ func (p *Parser) parseExpression(bp bindingPower) (left ast.Expr, err error) { left, err = parseVariable(p) case lexer.OpenParen: left, err = parseGroup(p) + case lexer.If: + left, err = parseIf(p) default: return nil, &UnexpectedTokenError{ Token: p.current(), diff --git a/selector/parser/parser_test.go b/selector/parser/parser_test.go index a161eccc..e9fef233 100644 --- a/selector/parser/parser_test.go +++ b/selector/parser/parser_test.go @@ -321,4 +321,67 @@ func TestParser_Parse_HappyPath(t *testing.T) { }, })) }) + + t.Run("conditional", func(t *testing.T) { + t.Run("if", run(t, testCase{ + input: `if (foo == 1) { "yes" } else { "no" }`, + expected: ast.ConditionalExpr{ + Cond: ast.BinaryExpr{ + Left: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, + Operator: lexer.Token{Kind: lexer.Equal, Value: "==", Pos: 8, Len: 2}, + Right: ast.NumberIntExpr{Value: 1}, + }, + Then: ast.StringExpr{Value: "yes"}, + Else: ast.StringExpr{Value: "no"}, + }, + })) + t.Run("if elseif else", run(t, testCase{ + input: `if (foo == 1) { "yes" } elseif (foo == 2) { "maybe" } else { "no" }`, + expected: ast.ConditionalExpr{ + Cond: ast.BinaryExpr{ + Left: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, + Operator: lexer.Token{Kind: lexer.Equal, Value: "==", Pos: 8, Len: 2}, + Right: ast.NumberIntExpr{Value: 1}, + }, + Then: ast.StringExpr{Value: "yes"}, + Else: ast.ConditionalExpr{ + Cond: ast.BinaryExpr{ + Left: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, + Operator: lexer.Token{Kind: lexer.Equal, Value: "==", Pos: 36, Len: 2}, + Right: ast.NumberIntExpr{Value: 2}, + }, + Then: ast.StringExpr{Value: "maybe"}, + Else: ast.StringExpr{Value: "no"}, + }, + }, + })) + t.Run("if elseif elseif else", run(t, testCase{ + input: `if (foo == 1) { "yes" } elseif (foo == 2) { "maybe" } elseif (foo == 3) { "probably" } else { "no" }`, + expected: ast.ConditionalExpr{ + Cond: ast.BinaryExpr{ + Left: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, + Operator: lexer.Token{Kind: lexer.Equal, Value: "==", Pos: 8, Len: 2}, + Right: ast.NumberIntExpr{Value: 1}, + }, + Then: ast.StringExpr{Value: "yes"}, + Else: ast.ConditionalExpr{ + Cond: ast.BinaryExpr{ + Left: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, + Operator: lexer.Token{Kind: lexer.Equal, Value: "==", Pos: 36, Len: 2}, + Right: ast.NumberIntExpr{Value: 2}, + }, + Then: ast.StringExpr{Value: "maybe"}, + Else: ast.ConditionalExpr{ + Cond: ast.BinaryExpr{ + Left: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, + Operator: lexer.Token{Kind: lexer.Equal, Value: "==", Pos: 66, Len: 2}, + Right: ast.NumberIntExpr{Value: 3}, + }, + Then: ast.StringExpr{Value: "probably"}, + Else: ast.StringExpr{Value: "no"}, + }, + }, + }, + })) + }) } From dbf367f7fbca082641fddc82d78bac41c970a297 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Sun, 6 Oct 2024 00:15:44 +0100 Subject: [PATCH 15/56] Small cleanup --- execution/execute_conditional.go | 12 ++++++++++-- execution/execute_test.go | 32 +++++++++++++++++++++++++------- model/value.go | 23 +++++++++++++++-------- model/value_literal.go | 16 ++++++++++------ 4 files changed, 60 insertions(+), 23 deletions(-) diff --git a/execution/execute_conditional.go b/execution/execute_conditional.go index 53fe2e59..e21abcd5 100644 --- a/execution/execute_conditional.go +++ b/execution/execute_conditional.go @@ -20,11 +20,19 @@ func conditionalExprExecutor(e ast.ConditionalExpr) (expressionExecutor, error) } if condBool { - return ExecuteAST(e.Then, data) + res, err := ExecuteAST(e.Then, data) + if err != nil { + return nil, fmt.Errorf("error executing then block: %w", err) + } + return res, nil } if e.Else != nil { - return ExecuteAST(e.Else, data) + res, err := ExecuteAST(e.Else, data) + if err != nil { + return nil, fmt.Errorf("error executing else block: %w", err) + } + return res, nil } return model.NewNullValue(), nil diff --git a/execution/execute_test.go b/execution/execute_test.go index d5617439..bcb57ffa 100644 --- a/execution/execute_test.go +++ b/execution/execute_test.go @@ -42,7 +42,7 @@ func TestExecuteSelector_HappyPath(t *testing.T) { t.Fatal(err) } if !equal { - t.Errorf("unexpected type: %v", cmp.Diff(exp, res)) + t.Errorf("unexpected output: %v", cmp.Diff(exp.Interface(), res.Interface())) } } } @@ -433,24 +433,42 @@ func TestExecuteSelector_HappyPath(t *testing.T) { out: model.NewStringValue("no"), })) t.Run("nested", runTest(testCase{ - s: `if (true) { if (true) { "yes" } else { "no" } } else { "no" }`, + s: ` + if (true) { + if (true) { "yes" } + else { "no" } + } else { "no" }`, out: model.NewStringValue("yes"), })) t.Run("nested false", runTest(testCase{ - s: `if (true) { if (false) { "yes" } else { "no" } } else { "no" }`, + s: ` + if (true) { + if (false) { "yes" } + else { "no" } + } else { "no" }`, out: model.NewStringValue("no"), })) t.Run("else if", runTest(testCase{ - s: `if (false) { "yes" } elseif (true) { "no" } else { "maybe" }`, + s: ` + if (false) { "yes" } + elseif (true) { "no" } + else { "maybe" }`, out: model.NewStringValue("no"), })) t.Run("else if else", runTest(testCase{ - s: `if (false) { "yes" } elseif (false) { "no" } else { "maybe" }`, + s: ` + if (false) { "yes" } + elseif (false) { "no" } + else { "maybe" }`, out: model.NewStringValue("maybe"), })) t.Run("if elseif elseif else", runTest(testCase{ - s: `if (false) { "yes" } elseif (false) { "no" } elseif (true) { "maybe" } else { "nope" }`, - out: model.NewStringValue("maybe"), + s: ` + if (false) { "yes" } + elseif (false) { "no" } + elseif (false) { "maybe" } + else { "nope" }`, + out: model.NewStringValue("nope"), })) }) } diff --git a/model/value.go b/model/value.go index 9d12fdc7..afc4a02e 100644 --- a/model/value.go +++ b/model/value.go @@ -33,16 +33,23 @@ type Value struct { } func NewValue(v any) *Value { - if v, ok := v.(*Value); ok { - return v - } - if rv, ok := v.(reflect.Value); ok { + switch val := v.(type) { + case *Value: + return val + case reflect.Value: return &Value{ - Value: rv, + Value: val, + } + case nil: + return NewNullValue() + default: + res := newPtr() + if v != nil { + res.Elem().Set(reflect.ValueOf(v)) + } + return &Value{ + Value: res, } - } - return &Value{ - Value: reflect.ValueOf(v), } } diff --git a/model/value_literal.go b/model/value_literal.go index bd15f0d7..38a45162 100644 --- a/model/value_literal.go +++ b/model/value_literal.go @@ -6,12 +6,16 @@ import ( "slices" ) +func newPtr() reflect.Value { + return reflect.New(reflect.TypeFor[any]()) +} + func NewNullValue() *Value { - return NewValue(reflect.New(reflect.TypeFor[any]())) + return NewValue(newPtr()) } func (v *Value) IsNull() bool { - return v.UnpackKinds(reflect.Ptr, reflect.Interface).isNull() + return v.isNull() } func (v *Value) isNull() bool { @@ -19,7 +23,7 @@ func (v *Value) isNull() bool { } func NewStringValue(x string) *Value { - res := reflect.New(reflect.TypeFor[string]()) + res := newPtr() res.Elem().Set(reflect.ValueOf(x)) return NewValue(res) } @@ -41,7 +45,7 @@ func (v *Value) StringValue() (string, error) { } func NewIntValue(x int64) *Value { - res := reflect.New(reflect.TypeFor[int64]()) + res := newPtr() res.Elem().Set(reflect.ValueOf(x)) return NewValue(res) } @@ -63,7 +67,7 @@ func (v *Value) IntValue() (int64, error) { } func NewFloatValue(x float64) *Value { - res := reflect.New(reflect.TypeFor[float64]()) + res := newPtr() res.Elem().Set(reflect.ValueOf(x)) return NewValue(res) } @@ -85,7 +89,7 @@ func (v *Value) FloatValue() (float64, error) { } func NewBoolValue(x bool) *Value { - res := reflect.New(reflect.TypeFor[bool]()) + res := newPtr() res.Elem().Set(reflect.ValueOf(x)) return NewValue(res) } From 2f36b1ffdfb8fc0046bf1e03d615fbf095244bcc Mon Sep 17 00:00:00 2001 From: Tom Date: Sun, 6 Oct 2024 21:13:52 +0100 Subject: [PATCH 16/56] More improvements --- .github/workflows/build-dev.yaml | 2 +- .github/workflows/build-test.yaml | 2 +- .github/workflows/build.yaml | 2 +- .github/workflows/test.yaml | 2 +- execution/execute_array.go | 35 ++++++---- execution/execute_test.go | 104 ++++++++++++++++++++++++++++++ execution/func.go | 15 +++++ go.mod | 2 +- model/value.go | 20 ++++++ model/value_map.go | 14 ++++ model/value_slice.go | 29 +++++++-- model/value_slice_test.go | 4 +- 12 files changed, 204 insertions(+), 27 deletions(-) diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index d6d7baa1..8f89faa5 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -65,7 +65,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v4 with: - go-version: '^1.21.0' # The Go version to download (if necessary) and use. + go-version: '^1.23.0' # The Go version to download (if necessary) and use. - name: Set env run: echo RELEASE_VERSION=development >> $GITHUB_ENV - name: Build diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 2c09d6db..ad99c602 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -77,7 +77,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v4 with: - go-version: '^1.21.0' # The Go version to download (if necessary) and use. + go-version: '^1.23.0' # The Go version to download (if necessary) and use. - name: Set env run: echo RELEASE_VERSION=development >> $GITHUB_ENV - name: Build diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 01a2fb5b..818ba8a2 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -73,7 +73,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-go@v4 with: - go-version: '^1.21.0' + go-version: '^1.23.0' - name: Set env run: echo RELEASE_VERSION=${GITHUB_REF:10} >> $GITHUB_ENV - name: Build diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 7bca04bf..f28efb46 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -7,7 +7,7 @@ jobs: - name: Install Go uses: actions/setup-go@v4 with: - go-version: '^1.21.0' + go-version: '^1.23.0' - name: Checkout code uses: actions/checkout@v3 - uses: actions/cache@v3 diff --git a/execution/execute_array.go b/execution/execute_array.go index 97e4d92e..f371b2f1 100644 --- a/execution/execute_array.go +++ b/execution/execute_array.go @@ -9,22 +9,29 @@ import ( func rangeExprExecutor(e ast.RangeExpr) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { - startE, err := ExecuteAST(e.Start, data) - if err != nil { - return nil, fmt.Errorf("error evaluating start expression: %w", err) - } - endE, err := ExecuteAST(e.End, data) - if err != nil { - return nil, fmt.Errorf("error evaluating end expression: %w", err) + var start, end int64 = -1, -1 + if e.Start != nil { + startE, err := ExecuteAST(e.Start, data) + if err != nil { + return nil, fmt.Errorf("error evaluating start expression: %w", err) + } + + start, err = startE.IntValue() + if err != nil { + return nil, fmt.Errorf("error getting start int value: %w", err) + } } - start, err := startE.IntValue() - if err != nil { - return nil, fmt.Errorf("error getting start int value: %w", err) - } - end, err := endE.IntValue() - if err != nil { - return nil, fmt.Errorf("error getting end int value: %w", err) + if e.End != nil { + endE, err := ExecuteAST(e.End, data) + if err != nil { + return nil, fmt.Errorf("error evaluating end expression: %w", err) + } + + end, err = endE.IntValue() + if err != nil { + return nil, fmt.Errorf("error getting end int value: %w", err) + } } res, err := data.SliceIndexRange(int(start), int(end)) diff --git a/execution/execute_test.go b/execution/execute_test.go index bcb57ffa..28a3c1f4 100644 --- a/execution/execute_test.go +++ b/execution/execute_test.go @@ -423,6 +423,110 @@ func TestExecuteSelector_HappyPath(t *testing.T) { })) }) + t.Run("array", func(t *testing.T) { + inSlice := func() *model.Value { + s := model.NewSliceValue() + if err := s.Append(model.NewIntValue(1)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := s.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := s.Append(model.NewIntValue(3)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + return s + } + inMap := func() *model.Value { + m := model.NewMapValue() + if err := m.SetMapKey("numbers", inSlice()); err != nil { + t.Fatalf("unexpected error: %s", err) + } + return m + } + + runArrayTests := func(in func() *model.Value, prefix string) func(t *testing.T) { + return func(t *testing.T) { + t.Run("1:2", runTest(testCase{ + s: prefix + `[1:2]`, + inFn: in, + outFn: func() *model.Value { + res := model.NewSliceValue() + if err := res.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := res.Append(model.NewIntValue(3)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + return res + }, + })) + t.Run("1:0", runTest(testCase{ + s: prefix + `[1:0]`, + inFn: in, + outFn: func() *model.Value { + res := model.NewSliceValue() + if err := res.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := res.Append(model.NewIntValue(1)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + return res + }, + })) + t.Run("1:", runTest(testCase{ + s: prefix + `[1:]`, + inFn: in, + outFn: func() *model.Value { + res := model.NewSliceValue() + if err := res.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := res.Append(model.NewIntValue(3)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + return res + }, + })) + t.Run(":1", runTest(testCase{ + s: prefix + `[:1]`, + inFn: in, + outFn: func() *model.Value { + res := model.NewSliceValue() + if err := res.Append(model.NewIntValue(1)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := res.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + return res + }, + })) + t.Run("reverse", runTest(testCase{ + s: prefix + `[len($this)-1:0]`, + inFn: in, + outFn: func() *model.Value { + res := model.NewSliceValue() + if err := res.Append(model.NewIntValue(3)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := res.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := res.Append(model.NewIntValue(1)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + return res + }, + })) + } + } + + t.Run("direct to slice", runArrayTests(inSlice, "")) + t.Run("property to slice", runArrayTests(inMap, "numbers")) + }) + t.Run("conditional", func(t *testing.T) { t.Run("true", runTest(testCase{ s: `if (true) { "yes" } else { "no" }`, diff --git a/execution/func.go b/execution/func.go index fa54a36b..193dda93 100644 --- a/execution/func.go +++ b/execution/func.go @@ -15,6 +15,21 @@ func RegisterFunc(name string, fn FuncFn) { } func init() { + RegisterFunc("len", func(data *model.Value, args model.Values) (*model.Value, error) { + if len(args) != 1 { + return nil, fmt.Errorf("len expects a single argument") + } + + arg := args[0] + + l, err := arg.Len() + if err != nil { + return nil, err + } + + return model.NewIntValue(int64(l)), nil + }) + RegisterFunc("add", func(_ *model.Value, args model.Values) (*model.Value, error) { var foundInts, foundFloats int var intRes int64 diff --git a/go.mod b/go.mod index b8c17069..9fe9ae68 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/tomwright/dasel/v3 -go 1.21 +go 1.23 require ( github.com/alecthomas/chroma/v2 v2.14.0 diff --git a/model/value.go b/model/value.go index afc4a02e..76edb7f2 100644 --- a/model/value.go +++ b/model/value.go @@ -117,3 +117,23 @@ func (v *Value) Type() Type { return TypeUnknown } } + +func (v *Value) Len() (int, error) { + var l int + var err error + + switch { + case v.IsSlice(): + l, err = v.SliceLen() + case v.IsMap(): + l, err = v.MapLen() + default: + err = fmt.Errorf("len expects slice or map") + } + + if err != nil { + return l, err + } + + return l, nil +} diff --git a/model/value_map.go b/model/value_map.go index 7578eb4f..12a0ffba 100644 --- a/model/value_map.go +++ b/model/value_map.go @@ -179,3 +179,17 @@ func (v *Value) MapKeyValues() ([]KeyValue, error) { return kvs, nil } + +// MapLen returns the length of the slice. +func (v *Value) MapLen() (int, error) { + if !v.IsMap() { + return 0, fmt.Errorf("expected map, got %s", v.Type()) + } + // There will be more efficient ways of doing this, but this + // accounts for maps, dencoding maps and structs. + keys, err := v.MapKeys() + if err != nil { + return 0, err + } + return len(keys), nil +} diff --git a/model/value_slice.go b/model/value_slice.go index c3efacea..0edec120 100644 --- a/model/value_slice.go +++ b/model/value_slice.go @@ -101,16 +101,33 @@ func (v *Value) SliceIndexRange(start, end int) (*Value, error) { if err != nil { return nil, fmt.Errorf("error getting slice length: %w", err) } + end = end - 1 + if end < 0 { + end = 0 + } } res := NewSliceValue() - for i := start; i < end; i++ { - item, err := v.GetSliceIndex(i) - if err != nil { - return nil, fmt.Errorf("error getting slice index: %w", err) + + if start > end { + for i := start; i >= end; i-- { + item, err := v.GetSliceIndex(i) + if err != nil { + return nil, fmt.Errorf("error getting slice index: %w", err) + } + if err := res.Append(item); err != nil { + return nil, fmt.Errorf("error appending value to slice: %w", err) + } } - if err := res.Append(item); err != nil { - return nil, fmt.Errorf("error appending value to slice: %w", err) + } else { + for i := start; i <= end; i++ { + item, err := v.GetSliceIndex(i) + if err != nil { + return nil, fmt.Errorf("error getting slice index: %w", err) + } + if err := res.Append(item); err != nil { + return nil, fmt.Errorf("error appending value to slice: %w", err) + } } } diff --git a/model/value_slice_test.go b/model/value_slice_test.go index dc8bb572..ef2a89d4 100644 --- a/model/value_slice_test.go +++ b/model/value_slice_test.go @@ -128,9 +128,9 @@ func TestSlice(t *testing.T) { // } //}) t.Run("SliceIndexRange", func(t *testing.T) { - t.Run("end 1", func(t *testing.T) { + t.Run("end 0", func(t *testing.T) { v := v() - s, err := v.SliceIndexRange(-1, 1) + s, err := v.SliceIndexRange(-1, 0) if err != nil { t.Errorf("unexpected error: %s", err) return From 25b260447580ef06b0ddbb385becbb6ce0fe7551 Mon Sep 17 00:00:00 2001 From: Tom Date: Sun, 6 Oct 2024 23:14:16 +0100 Subject: [PATCH 17/56] Update container.yaml for new selector format --- .github/workflows/container.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/container.yaml b/.github/workflows/container.yaml index 1d48e364..7c65d182 100644 --- a/.github/workflows/container.yaml +++ b/.github/workflows/container.yaml @@ -42,7 +42,7 @@ jobs: tags: dasel:test - name: Test run: | - echo '{"hello": "World"}' | docker run -i --rm dasel:test -r json 'hello' + echo '{"hello": "World"}' | docker run -i --rm dasel:test -i json 'hello' - name: Set version tag variables if: ${{ steps.version.outputs.is_valid == 'true' }} run: | From bb3b2b46fb515e3c926349ee14ba6f09db7d9068 Mon Sep 17 00:00:00 2001 From: Tom Date: Sun, 6 Oct 2024 23:16:25 +0100 Subject: [PATCH 18/56] Fix build-test.yaml --- .github/workflows/build-test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index ad99c602..fef6ff7a 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -88,4 +88,4 @@ jobs: - name: Test execution if: matrix.test_execution == true run: | - echo '{"hello": "World"}' | ./target/release/${{ matrix.artifact_name }} -r json 'hello' + echo '{"hello": "World"}' | ./target/release/${{ matrix.artifact_name }} -i json 'hello' From 5d3379fc721ca642d611c0a4448d938f284c8958 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 7 Oct 2024 00:35:10 +0100 Subject: [PATCH 19/56] Update tests --- internal/cli/generic_test.go | 323 ++++++++++++++++++++++++++--------- internal/cli/root.go | 4 +- parsing/json.go | 6 +- parsing/toml.go | 17 +- parsing/yaml.go | 20 ++- 5 files changed, 280 insertions(+), 90 deletions(-) diff --git a/internal/cli/generic_test.go b/internal/cli/generic_test.go index 37a8cc53..cf3fb4da 100644 --- a/internal/cli/generic_test.go +++ b/internal/cli/generic_test.go @@ -1,106 +1,267 @@ package cli_test import ( + "fmt" + "slices" "testing" "github.com/tomwright/dasel/v3/parsing" ) -type inputProvider interface { - Format() parsing.Format - UserObject() []byte - ListOfNumbers() []byte - ListOfStrings() []byte - UserName() []byte -} - -type jsonInputProvider struct{} - -func (j jsonInputProvider) Format() parsing.Format { - return parsing.JSON -} - -func (j jsonInputProvider) UserObject() []byte { - return []byte(`{"name":"Tom"}`) -} - -func (j jsonInputProvider) ListOfNumbers() []byte { - return []byte(`[1,2,3]`) +func newStringWithFormat(format parsing.Format, data string) bytesWithFormat { + return bytesWithFormat{ + format: format, + data: append([]byte(data), []byte("\n")...), + } } -func (j jsonInputProvider) ListOfStrings() []byte { - return []byte(`["a","b","c"]`) +type bytesWithFormat struct { + format parsing.Format + data []byte } -func (j jsonInputProvider) UserName() []byte { - return []byte(`"Tom"`) +type testCases struct { + selector string + in []bytesWithFormat + out []bytesWithFormat + args []string + skip []string } -type yamlInputProvider struct{} +func (tcs testCases) run(t *testing.T) { + for _, i := range tcs.in { + for _, o := range tcs.out { + tcName := fmt.Sprintf("%s to %s", i.format.String(), o.format.String()) -func (y yamlInputProvider) Format() parsing.Format { - return parsing.YAML -} + if slices.Contains(tcs.skip, tcName) { + // Run a test and skip for visibility. + t.Run(tcName, func(t *testing.T) { + t.Skip() + }) + continue + } -func (y yamlInputProvider) UserObject() []byte { - return []byte(`name: Tom`) + args := slices.Clone(tcs.args) + args = append(args, "--input", i.format.String(), "--output", o.format.String()) + if tcs.selector != "" { + args = append(args, tcs.selector) + } + tc := testCase{ + args: args, + in: i.data, + stdout: o.data, + } + t.Run(tcName, runTest(tc)) + } + } } -func (y yamlInputProvider) ListOfNumbers() []byte { - return []byte(`- 1 +func TestCrossFormatHappyPath(t *testing.T) { + jsonInputData := newStringWithFormat(parsing.JSON, `{ + "oneTwoThree": 123, + "oneTwoDotThree": 12.3, + "hello": "world", + "boolFalse": false, + "boolTrue": true, + "stringFalse": "false", + "stringTrue": "true", + "sliceOfNumbers": [1, 2, 3, 4, 5], + "map": { + "oneTwoThree": 123, + "oneTwoDotThree": 12.3, + "hello": "world", + "boolFalse": false, + "boolTrue": true, + "stringFalse": "false", + "stringTrue": "true", + "sliceOfNumbers": [1, 2, 3, 4, 5], + "map": { + "oneTwoThree": 123, + "oneTwoDotThree": 12.3, + "hello": "world", + "boolFalse": false, + "boolTrue": true, + "stringFalse": "false", + "stringTrue": "true", + "sliceOfNumbers": [1, 2, 3, 4, 5] + } + } +}`) + yamlInputData := newStringWithFormat(parsing.YAML, `oneTwoThree: 123 +oneTwoDotThree: 12.3 +hello: world +boolFalse: false +boolTrue: true +stringFalse: "false" +stringTrue: "true" +sliceOfNumbers: +- 1 - 2 -- 3`) -} - -func (y yamlInputProvider) ListOfStrings() []byte { - return []byte(`- a -- b -- c`) -} +- 3 +- 4 +- 5 +map: + oneTwoThree: 123 + oneTwoDotThree: 12.3 + hello: world + boolFalse: false + boolTrue: true + stringFalse: "false" + stringTrue: "true" + sliceOfNumbers: + - 1 + - 2 + - 3 + - 4 + - 5 + map: + oneTwoThree: 123 + oneTwoDotThree: 12.3 + hello: world + boolFalse: false + boolTrue: true + stringFalse: "false" + stringTrue: "true" + sliceOfNumbers: + - 1 + - 2 + - 3 + - 4 + - 5 +`) -func (y yamlInputProvider) UserName() []byte { - return []byte(`Tom`) -} + tomlInputData := newStringWithFormat(parsing.TOML, ` +oneTwoThree = 123 +oneTwoDotThree = 12.3 +hello = 'world' +boolFalse = false +boolTrue = true +stringFalse = 'false' +stringTrue = 'true' +sliceOfNumbers = [1, 2, 3, 4, 5] -type tomlInputProvider struct{} +[map] +oneTwoThree = 123 +oneTwoDotThree = 12.3 +hello = "world" +boolFalse = false +boolTrue = true +stringFalse = "false" +stringTrue = "true" +sliceOfNumbers = [1, 2, 3, 4, 5] -func (t tomlInputProvider) Format() parsing.Format { - return parsing.TOML -} +[map.map] +oneTwoThree = 123 +oneTwoDotThree = 12.3 +hello = "world" +boolFalse = false +boolTrue = true +stringFalse = "false" +stringTrue = "true" +sliceOfNumbers = [1, 2, 3, 4, 5] +`) -func (t tomlInputProvider) UserObject() []byte { - return []byte(`name = "Tom"`) -} + t.Run("select", func(t *testing.T) { + newTestsWithPrefix := func(prefix string) func(*testing.T) { + return func(t *testing.T) { + t.Run("string", testCases{ + selector: prefix + "hello", + in: []bytesWithFormat{ + jsonInputData, + yamlInputData, + tomlInputData, + }, + out: []bytesWithFormat{ + newStringWithFormat(parsing.JSON, `"world"`), + newStringWithFormat(parsing.YAML, `world`), + newStringWithFormat(parsing.TOML, `'world'`), + }, + }.run) + t.Run("int", testCases{ + selector: prefix + "oneTwoThree", + in: []bytesWithFormat{ + jsonInputData, + yamlInputData, + tomlInputData, + }, + out: []bytesWithFormat{ + newStringWithFormat(parsing.JSON, `123`), + newStringWithFormat(parsing.YAML, `123`), + newStringWithFormat(parsing.TOML, `123`), + }, + skip: []string{ + // Skipped because the parser outputs as a float. + "json to toml", + }, + }.run) + t.Run("float", testCases{ + selector: prefix + "oneTwoDotThree", + in: []bytesWithFormat{ + jsonInputData, + yamlInputData, + tomlInputData, + }, + out: []bytesWithFormat{ + newStringWithFormat(parsing.JSON, `12.3`), + newStringWithFormat(parsing.YAML, `12.3`), + newStringWithFormat(parsing.TOML, `12.3`), + }, + }.run) + t.Run("bool", func(t *testing.T) { + t.Run("true", testCases{ + selector: prefix + "boolTrue", + in: []bytesWithFormat{ + jsonInputData, + yamlInputData, + }, + out: []bytesWithFormat{ + newStringWithFormat(parsing.JSON, `true`), + newStringWithFormat(parsing.YAML, `true`), + newStringWithFormat(parsing.TOML, `true`), + }, + }.run) + t.Run("false", testCases{ + selector: prefix + "boolFalse", + in: []bytesWithFormat{ + jsonInputData, + yamlInputData, + }, + out: []bytesWithFormat{ + newStringWithFormat(parsing.JSON, `false`), + newStringWithFormat(parsing.YAML, `false`), + newStringWithFormat(parsing.TOML, `false`), + }, + }.run) + t.Run("true string", testCases{ + selector: prefix + "stringTrue", + in: []bytesWithFormat{ + jsonInputData, + yamlInputData, + }, + out: []bytesWithFormat{ + newStringWithFormat(parsing.JSON, `"true"`), + newStringWithFormat(parsing.YAML, `"true"`), + newStringWithFormat(parsing.TOML, `'true'`), + }, + }.run) + t.Run("false string", testCases{ + selector: prefix + "stringFalse", + in: []bytesWithFormat{ + jsonInputData, + yamlInputData, + }, + out: []bytesWithFormat{ + newStringWithFormat(parsing.JSON, `"false"`), + newStringWithFormat(parsing.YAML, `"false"`), + newStringWithFormat(parsing.TOML, `'false'`), + }, + }.run) + }) + } + } -func (t tomlInputProvider) ListOfNumbers() []byte { - return []byte(`[1, 2, 3]`) -} - -func (t tomlInputProvider) ListOfStrings() []byte { - return []byte(`["a", "b", "c"]`) -} - -func (t tomlInputProvider) UserName() []byte { - return []byte(`Tom`) -} - -func TestGeneric(t *testing.T) { - t.Run("json", runGenericTests(jsonInputProvider{})) - //t.Run("yaml", runGenericTests(yamlInputProvider{})) - //t.Run("toml", runGenericTests(tomlInputProvider{})) -} - -func runGenericTests(i inputProvider) func(*testing.T) { - return func(t *testing.T) { - t.Run("root", runTest(testCase{ - args: []string{"-i", i.Format().String(), ``}, - in: i.UserObject(), - stdout: i.UserObject(), - })) - t.Run("top level string", runTest(testCase{ - args: []string{"-i", i.Format().String(), `name`}, - in: i.UserObject(), - stdout: i.UserName(), - })) - } + t.Run("root", newTestsWithPrefix("")) + t.Run("nested once", newTestsWithPrefix("map.")) + t.Run("nested twice", newTestsWithPrefix("map.map.")) + }) } diff --git a/internal/cli/root.go b/internal/cli/root.go index 147c9cfb..86b14eb5 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -61,8 +61,8 @@ func RootCmd() *cobra.Command { }, } - cmd.Flags().StringP("input", "i", "json", "The format of the input data. Can be one of: json, yaml, toml, xml, csv") - cmd.Flags().StringP("output", "o", "json", "The format of the output data. Can be one of: json, yaml, toml, xml, csv") + cmd.Flags().StringP("input", "i", "", "The format of the input data. Can be one of: json, yaml, toml, xml, csv") + cmd.Flags().StringP("output", "o", "", "The format of the output data. Can be one of: json, yaml, toml, xml, csv") cmd.AddCommand(manCommand(cmd)) diff --git a/parsing/json.go b/parsing/json.go index cfa4750c..b662f44a 100644 --- a/parsing/json.go +++ b/parsing/json.go @@ -31,5 +31,9 @@ type jsonWriter struct{} // Write writes a value to a byte slice. func (j *jsonWriter) Write(value *model.Value) ([]byte, error) { - return json.Marshal(value.Interface()) + res, err := json.Marshal(value.Interface()) + if err != nil { + return nil, err + } + return append(res, []byte("\n")...), nil } diff --git a/parsing/toml.go b/parsing/toml.go index 77195d87..5de7afb4 100644 --- a/parsing/toml.go +++ b/parsing/toml.go @@ -1,6 +1,9 @@ package parsing -import "github.com/tomwright/dasel/v3/model" +import ( + "github.com/pelletier/go-toml/v2" + "github.com/tomwright/dasel/v3/model" +) // NewTOMLReader creates a new TOML reader. func NewTOMLReader() (Reader, error) { @@ -16,12 +19,20 @@ type tomlReader struct{} // Read reads a value from a byte slice. func (j *tomlReader) Read(data []byte) (*model.Value, error) { - panic("not implemented") + var unmarshalled any + if err := toml.Unmarshal(data, &unmarshalled); err != nil { + return nil, err + } + return model.NewValue(&unmarshalled), nil } type tomlWriter struct{} // Write writes a value to a byte slice. func (j *tomlWriter) Write(value *model.Value) ([]byte, error) { - panic("not implemented") + res, err := toml.Marshal(value.Interface()) + if err != nil { + return nil, err + } + return append(res, []byte("\n")...), nil } diff --git a/parsing/yaml.go b/parsing/yaml.go index 1818a2cf..a5488f53 100644 --- a/parsing/yaml.go +++ b/parsing/yaml.go @@ -1,6 +1,10 @@ package parsing -import "github.com/tomwright/dasel/v3/model" +import ( + "bytes" + "github.com/tomwright/dasel/v3/model" + "gopkg.in/yaml.v3" +) // NewYAMLReader creates a new YAML reader. func NewYAMLReader() (Reader, error) { @@ -16,12 +20,22 @@ type yamlReader struct{} // Read reads a value from a byte slice. func (j *yamlReader) Read(data []byte) (*model.Value, error) { - panic("not implemented") + d := yaml.NewDecoder(bytes.NewReader(data)) + var unmarshalled any + if err := d.Decode(&unmarshalled); err != nil { + return nil, err + } + return model.NewValue(&unmarshalled), nil } type yamlWriter struct{} // Write writes a value to a byte slice. func (j *yamlWriter) Write(value *model.Value) ([]byte, error) { - panic("not implemented") + buf := new(bytes.Buffer) + e := yaml.NewEncoder(buf) + if err := e.Encode(value.Interface()); err != nil { + return nil, err + } + return buf.Bytes(), nil } From 8f1ea537f556a43d8ef0e4e87eb9e007b5720ed5 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 7 Oct 2024 00:41:46 +0100 Subject: [PATCH 20/56] Delete the old dasel implementation --- api.go | 6 + context.go | 240 ------------ context_test.go | 119 ------ dencoding/yaml_decoder.go | 2 +- dencoding/yaml_encoder.go | 2 +- error.go | 107 ------ error_test.go | 188 --------- func.go | 208 ---------- func_all.go | 48 --- func_all_test.go | 41 -- func_and.go | 53 --- func_and_test.go | 41 -- func_append.go | 45 --- func_count.go | 12 - func_count_test.go | 41 -- func_equal.go | 79 ---- func_equal_test.go | 83 ---- func_filter.go | 54 --- func_filter_or.go | 54 --- func_filter_or_test.go | 72 ---- func_filter_test.go | 197 ---------- func_first.go | 34 -- func_first_test.go | 53 --- func_index.go | 92 ----- func_index_test.go | 101 ----- func_join.go | 61 --- func_join_test.go | 213 ----------- func_key.go | 24 -- func_keys.go | 118 ------ func_keys_test.go | 68 ---- func_last.go | 35 -- func_last_test.go | 53 --- func_len.go | 20 - func_len_test.go | 58 --- func_less_than.go | 89 ----- func_less_than_test.go | 41 -- func_map_of.go | 72 ---- func_map_of_test.go | 36 -- func_merge.go | 45 --- func_merge_test.go | 91 ----- func_metadata.go | 22 -- func_metadata_test.go | 16 - func_more_than.go | 89 ----- func_more_than_test.go | 41 -- func_not.go | 48 --- func_not_test.go | 53 --- func_null.go | 24 -- func_null_test.go | 29 -- func_or.go | 53 --- func_or_default.go | 72 ---- func_or_default_test.go | 86 ----- func_or_test.go | 41 -- func_parent.go | 54 --- func_parent_test.go | 101 ----- func_property.go | 94 ----- func_property_test.go | 105 ------ func_string.go | 22 -- func_string_test.go | 40 -- func_this.go | 11 - func_this_test.go | 46 --- func_type.go | 44 --- func_type_test.go | 101 ----- internal/command/delete.go | 115 ------ internal/command/delete_test.go | 74 ---- internal/command/man.go | 30 -- internal/command/man_test.go | 43 --- internal/command/options.go | 204 ---------- internal/command/put.go | 158 -------- internal/command/put_test.go | 227 ----------- internal/command/root.go | 25 -- internal/command/root_test.go | 58 --- internal/command/select.go | 109 ------ internal/command/select_test.go | 511 ------------------------- internal/command/validate.go | 145 ------- {util => internal/util}/to_string.go | 0 selector.go | 224 ----------- selector_test.go | 156 -------- step.go | 41 -- storage/colourise.go | 25 -- storage/csv.go | 271 ------------- storage/csv_test.go | 143 ------- storage/json.go | 133 ------- storage/json_test.go | 189 ---------- storage/option.go | 83 ---- storage/parser.go | 135 ------- storage/parser_test.go | 286 -------------- storage/plain.go | 42 --- storage/plain_test.go | 62 --- storage/toml.go | 99 ----- storage/toml_test.go | 156 -------- storage/xml.go | 133 ------- storage/xml_test.go | 245 ------------ storage/yaml.go | 129 ------- storage/yaml_test.go | 121 ------ tests/assets/broken.json | 9 - tests/assets/broken.xml | 1 - tests/assets/deployment.yaml | 30 -- tests/assets/example.json | 25 -- tests/assets/example.xml | 21 -- tests/assets/example.yaml | 14 - tests/assets/int-value.txt | 1 - tests/assets/json-value.json | 3 - tests/assets/string-value.txt | 1 - truthy.go | 61 --- truthy_test.go | 97 ----- value.go | 544 --------------------------- 106 files changed, 8 insertions(+), 9359 deletions(-) create mode 100644 api.go delete mode 100644 context.go delete mode 100644 context_test.go delete mode 100644 error.go delete mode 100644 error_test.go delete mode 100644 func.go delete mode 100644 func_all.go delete mode 100644 func_all_test.go delete mode 100644 func_and.go delete mode 100644 func_and_test.go delete mode 100644 func_append.go delete mode 100644 func_count.go delete mode 100644 func_count_test.go delete mode 100644 func_equal.go delete mode 100644 func_equal_test.go delete mode 100644 func_filter.go delete mode 100644 func_filter_or.go delete mode 100644 func_filter_or_test.go delete mode 100644 func_filter_test.go delete mode 100644 func_first.go delete mode 100644 func_first_test.go delete mode 100644 func_index.go delete mode 100644 func_index_test.go delete mode 100644 func_join.go delete mode 100644 func_join_test.go delete mode 100644 func_key.go delete mode 100644 func_keys.go delete mode 100644 func_keys_test.go delete mode 100644 func_last.go delete mode 100644 func_last_test.go delete mode 100644 func_len.go delete mode 100644 func_len_test.go delete mode 100644 func_less_than.go delete mode 100644 func_less_than_test.go delete mode 100644 func_map_of.go delete mode 100644 func_map_of_test.go delete mode 100644 func_merge.go delete mode 100644 func_merge_test.go delete mode 100644 func_metadata.go delete mode 100644 func_metadata_test.go delete mode 100644 func_more_than.go delete mode 100644 func_more_than_test.go delete mode 100644 func_not.go delete mode 100644 func_not_test.go delete mode 100644 func_null.go delete mode 100644 func_null_test.go delete mode 100644 func_or.go delete mode 100644 func_or_default.go delete mode 100644 func_or_default_test.go delete mode 100644 func_or_test.go delete mode 100644 func_parent.go delete mode 100644 func_parent_test.go delete mode 100644 func_property.go delete mode 100644 func_property_test.go delete mode 100644 func_string.go delete mode 100644 func_string_test.go delete mode 100644 func_this.go delete mode 100644 func_this_test.go delete mode 100644 func_type.go delete mode 100644 func_type_test.go delete mode 100644 internal/command/delete.go delete mode 100644 internal/command/delete_test.go delete mode 100644 internal/command/man.go delete mode 100644 internal/command/man_test.go delete mode 100644 internal/command/options.go delete mode 100644 internal/command/put.go delete mode 100644 internal/command/put_test.go delete mode 100644 internal/command/root.go delete mode 100644 internal/command/root_test.go delete mode 100644 internal/command/select.go delete mode 100644 internal/command/select_test.go delete mode 100644 internal/command/validate.go rename {util => internal/util}/to_string.go (100%) delete mode 100644 selector.go delete mode 100644 selector_test.go delete mode 100644 step.go delete mode 100644 storage/colourise.go delete mode 100644 storage/csv.go delete mode 100644 storage/csv_test.go delete mode 100644 storage/json.go delete mode 100644 storage/json_test.go delete mode 100644 storage/option.go delete mode 100644 storage/parser.go delete mode 100644 storage/parser_test.go delete mode 100644 storage/plain.go delete mode 100644 storage/plain_test.go delete mode 100644 storage/toml.go delete mode 100644 storage/toml_test.go delete mode 100644 storage/xml.go delete mode 100644 storage/xml_test.go delete mode 100644 storage/yaml.go delete mode 100644 storage/yaml_test.go delete mode 100644 tests/assets/broken.json delete mode 100644 tests/assets/broken.xml delete mode 100644 tests/assets/deployment.yaml delete mode 100644 tests/assets/example.json delete mode 100644 tests/assets/example.xml delete mode 100644 tests/assets/example.yaml delete mode 100644 tests/assets/int-value.txt delete mode 100644 tests/assets/json-value.json delete mode 100644 tests/assets/string-value.txt delete mode 100644 truthy.go delete mode 100644 truthy_test.go delete mode 100644 value.go diff --git a/api.go b/api.go new file mode 100644 index 00000000..78ae5cf2 --- /dev/null +++ b/api.go @@ -0,0 +1,6 @@ +// 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") +} diff --git a/context.go b/context.go deleted file mode 100644 index 80c38020..00000000 --- a/context.go +++ /dev/null @@ -1,240 +0,0 @@ -package dasel - -import ( - "fmt" - "reflect" -) - -// Context has scope over the entire query. -// Each individual function has its own step within the context. -// The context holds the entire data structure we're accessing/modifying. -type Context struct { - selector string - selectorResolver SelectorResolver - steps []*Step - data Value - functions *FunctionCollection - createWhenMissing bool - metadata map[string]interface{} -} - -func (c *Context) WithMetadata(key string, value interface{}) *Context { - if c.metadata == nil { - c.metadata = map[string]interface{}{} - } - c.metadata[key] = value - return c -} - -func (c *Context) Metadata(key string) interface{} { - if c.metadata == nil { - return nil - } - if val, ok := c.metadata[key]; ok { - return val - } - return nil -} - -func newContextWithFunctions(value interface{}, selector string, functions *FunctionCollection) *Context { - var v Value - if val, ok := value.(Value); ok { - v = val - } else { - var reflectVal reflect.Value - if val, ok := value.(reflect.Value); ok { - reflectVal = val - } else { - reflectVal = reflect.ValueOf(value) - } - - v = Value{ - Value: reflectVal, - } - } - - v.Value = makeAddressable(v.Value) - - // v.SetMapIndex(reflect.ValueOf("users"), v.MapIndex(ValueOf("users"))) - // v.MapIndex("users") - - v.setFn = func(value Value) { - v.Unpack().Set(value.Value) - } - - if v.Metadata("key") == nil { - v.WithMetadata("key", "root") - } - - return &Context{ - selector: selector, - data: v, - steps: []*Step{ - { - selector: Selector{ - funcName: "root", - funcArgs: []string{}, - }, - index: 0, - output: Values{v}, - }, - }, - functions: functions, - selectorResolver: NewSelectorResolver(selector, functions), - } -} - -func newSelectContext(value interface{}, selector string) *Context { - return newContextWithFunctions(value, selector, standardFunctions()) -} - -func newPutContext(value interface{}, selector string) *Context { - return newContextWithFunctions(value, selector, standardFunctions()). - WithCreateWhenMissing(true) -} - -func newDeleteContext(value interface{}, selector string) *Context { - return newContextWithFunctions(value, selector, standardFunctions()) -} - -func derefValue(v Value) Value { - res := ValueOf(deref(v.Value)) - res.metadata = v.metadata - return res -} - -func derefValues(values Values) Values { - results := make(Values, len(values)) - for k, v := range values { - results[k] = derefValue(v) - } - return results -} - -// Select resolves the given selector and returns the resulting values. -func Select(root interface{}, selector string) (Values, error) { - c := newSelectContext(root, selector) - values, err := c.Run() - if err != nil { - return nil, err - } - return derefValues(values), nil -} - -// Put resolves the given selector and writes the given value in their place. -// The root value may be changed in-place. If this is not desired you should copy the input -// value before passing it to Put. -func Put(root interface{}, selector string, value interface{}) (Value, error) { - toSet := ValueOf(value) - c := newPutContext(root, selector) - values, err := c.Run() - if err != nil { - return Value{}, err - } - for _, v := range values { - v.Set(toSet) - } - return c.Data(), nil -} - -// Delete resolves the given selector and deletes any found values. -// The root value may be changed in-place. If this is not desired you should copy the input -// value before passing it to Delete. -func Delete(root interface{}, selector string) (Value, error) { - c := newDeleteContext(root, selector) - values, err := c.Run() - if err != nil { - return Value{}, err - } - for _, v := range values { - v.Delete() - } - return c.Data(), nil -} - -func (c *Context) subSelectContext(value interface{}, selector string) *Context { - subC := newContextWithFunctions(value, selector, c.functions) - subC.metadata = c.metadata - return subC -} - -func (c *Context) subSelect(value interface{}, selector string) (Values, error) { - return c.subSelectContext(value, selector).Run() -} - -// WithSelector updates c with the given selector. -func (c *Context) WithSelector(s string) *Context { - c.selector = s - c.selectorResolver = NewSelectorResolver(s, c.functions) - return c -} - -// WithCreateWhenMissing updates c with the given create value. -// If this value is true, elements (such as properties) will be initialised instead -// of return not found errors. -func (c *Context) WithCreateWhenMissing(create bool) *Context { - c.createWhenMissing = create - return c -} - -// CreateWhenMissing returns true if the internal createWhenMissing value is true. -func (c *Context) CreateWhenMissing() bool { - return c.createWhenMissing -} - -// Data returns the root element of the context. -func (c *Context) Data() Value { - return derefValue(c.data) -} - -// Run calls Next repeatedly until no more steps are left. -// Returns the final Step. -func (c *Context) Run() (Values, error) { - var res *Step - for { - step, err := c.Next() - if err != nil { - return nil, err - } - if step == nil { - break - } - res = step - } - return res.output, nil -} - -// Next returns the next Step, or nil if we have reached the final Selector. -func (c *Context) Next() (*Step, error) { - nextSelector, err := c.selectorResolver.Next() - if err != nil { - return nil, fmt.Errorf("could not resolve selector: %w", err) - } - - if nextSelector == nil { - return nil, nil - } - - nextStep := &Step{ - context: c, - selector: *nextSelector, - index: len(c.steps), - output: nil, - } - - c.steps = append(c.steps, nextStep) - - if err := nextStep.execute(); err != nil { - return nextStep, err - } - - return nextStep, nil -} - -// Step returns the step at the given index. -func (c *Context) Step(i int) *Step { - if i < 0 || i > (len(c.steps)-1) { - return nil - } - return c.steps[i] -} diff --git a/context_test.go b/context_test.go deleted file mode 100644 index 215ee637..00000000 --- a/context_test.go +++ /dev/null @@ -1,119 +0,0 @@ -package dasel - -import ( - "errors" - "reflect" - "testing" -) - -func sameSlice(x, y []interface{}) bool { - if len(x) != len(y) { - return false - } - - if reflect.DeepEqual(x, y) { - return true - } - - // Test for equality ignoring ordering - diff := make([]interface{}, len(y)) - copy(diff, y) - for _, xv := range x { - for di, dv := range diff { - if reflect.DeepEqual(xv, dv) { - diff = append(diff[0:di], diff[di+1:]...) - break - } - } - } - - return len(diff) == 0 -} - -func selectTest(selector string, original interface{}, exp []interface{}) func(t *testing.T) { - return func(t *testing.T) { - c := newSelectContext(original, selector) - - values, err := c.Run() - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - - got := values.Interfaces() - if !sameSlice(exp, got) { - t.Errorf("expected %v, got %v", exp, got) - return - } - } -} - -func selectTestAssert(selector string, original interface{}, assertFn func(t *testing.T, got []any)) func(t *testing.T) { - return func(t *testing.T) { - c := newSelectContext(original, selector) - - values, err := c.Run() - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - - got := values.Interfaces() - assertFn(t, got) - } -} - -func selectTestErr(selector string, original interface{}, expErr error) func(t *testing.T) { - return func(t *testing.T) { - c := newSelectContext(original, selector) - - _, err := c.Run() - - if !errors.Is(err, expErr) { - t.Errorf("expected error: %v, got %v", expErr, err) - return - } - } -} - -func TestContext_Step(t *testing.T) { - step1 := &Step{index: 0} - step2 := &Step{index: 1} - c := &Context{ - steps: []*Step{ - step1, step2, - }, - } - expSteps := map[int]*Step{ - -1: nil, - 0: step1, - 1: step2, - 2: nil, - } - - for index, exp := range expSteps { - got := c.Step(index) - if exp != got { - t.Errorf("expected %v, got %v", exp, got) - } - } -} - -func TestContext_WithMetadata(t *testing.T) { - c := (&Context{}). - WithMetadata("x", 1). - WithMetadata("y", 2) - - expMetadata := map[string]interface{}{ - "x": 1, - "y": 2, - "z": nil, - } - - for index, exp := range expMetadata { - got := c.Metadata(index) - if exp != got { - t.Errorf("expected %v, got %v", exp, got) - } - } -} diff --git a/dencoding/yaml_decoder.go b/dencoding/yaml_decoder.go index a4be60d1..b9b5be2c 100644 --- a/dencoding/yaml_decoder.go +++ b/dencoding/yaml_decoder.go @@ -2,12 +2,12 @@ package dencoding import ( "fmt" + "github.com/tomwright/dasel/v3/internal/util" "io" "reflect" "strconv" "time" - "github.com/tomwright/dasel/v3/util" "gopkg.in/yaml.v3" ) diff --git a/dencoding/yaml_encoder.go b/dencoding/yaml_encoder.go index a25c2f74..d9c2c81c 100644 --- a/dencoding/yaml_encoder.go +++ b/dencoding/yaml_encoder.go @@ -1,10 +1,10 @@ package dencoding import ( + "github.com/tomwright/dasel/v3/internal/util" "io" "strconv" - "github.com/tomwright/dasel/v3/util" "gopkg.in/yaml.v3" ) diff --git a/error.go b/error.go deleted file mode 100644 index 84156822..00000000 --- a/error.go +++ /dev/null @@ -1,107 +0,0 @@ -package dasel - -import ( - "errors" - "fmt" - "reflect" -) - -// ErrMissingPreviousNode is returned when findValue doesn't have access to the previous node. -var ErrMissingPreviousNode = errors.New("missing previous node") - -// UnknownComparisonOperatorErr is returned when -type UnknownComparisonOperatorErr struct { - Operator string -} - -// Error returns the error message. -func (e UnknownComparisonOperatorErr) Error() string { - return fmt.Sprintf("unknown comparison operator: %s", e.Operator) -} - -// Is implements the errors interface, so the errors.Is() function can be used. -func (e UnknownComparisonOperatorErr) Is(err error) bool { - _, ok := err.(*UnknownComparisonOperatorErr) - return ok -} - -// InvalidIndexErr is returned when a selector targets an index that does not exist. -type InvalidIndexErr struct { - Index string -} - -// Error returns the error message. -func (e InvalidIndexErr) Error() string { - return fmt.Sprintf("invalid index: %s", e.Index) -} - -// Is implements the errors interface, so the errors.Is() function can be used. -func (e InvalidIndexErr) Is(err error) bool { - _, ok := err.(*InvalidIndexErr) - return ok -} - -// UnsupportedSelector is returned when a specific selector type is used in the wrong context. -type UnsupportedSelector struct { - Selector string -} - -// Error returns the error message. -func (e UnsupportedSelector) Error() string { - return fmt.Sprintf("selector is not supported here: %s", e.Selector) -} - -// Is implements the errors interface, so the errors.Is() function can be used. -func (e UnsupportedSelector) Is(err error) bool { - _, ok := err.(*UnsupportedSelector) - return ok -} - -// ValueNotFound is returned when a selector string cannot be fully resolved. -type ValueNotFound struct { - Selector string - PreviousValue reflect.Value -} - -// Error returns the error message. -func (e ValueNotFound) Error() string { - return fmt.Sprintf("no value found for selector: %s: %v", e.Selector, e.PreviousValue) -} - -// Is implements the errors interface, so the errors.Is() function can be used. -func (e ValueNotFound) Is(err error) bool { - _, ok := err.(*ValueNotFound) - return ok -} - -// UnexpectedPreviousNilValue is returned when the previous node contains a nil value. -type UnexpectedPreviousNilValue struct { - Selector string -} - -// Error returns the error message. -func (e UnexpectedPreviousNilValue) Error() string { - return fmt.Sprintf("previous value is nil: %s", e.Selector) -} - -// Is implements the errors interface, so the errors.Is() function can be used. -func (e UnexpectedPreviousNilValue) Is(err error) bool { - _, ok := err.(*UnexpectedPreviousNilValue) - return ok -} - -// UnhandledCheckType is returned when the a check doesn't know how to deal with the given type -type UnhandledCheckType struct { - Value interface{} -} - -// Error returns the error message. -func (e UnhandledCheckType) Error() string { - return fmt.Sprintf("unhandled check type: %T", e.Value) -} - -// Is implements the errors interface, so the errors.Is() function can be used. -func (e UnhandledCheckType) Is(err error) bool { - _, ok := err.(*UnhandledCheckType) - return ok -} diff --git a/error_test.go b/error_test.go deleted file mode 100644 index 8e999127..00000000 --- a/error_test.go +++ /dev/null @@ -1,188 +0,0 @@ -package dasel_test - -import ( - "errors" - "fmt" - "reflect" - "testing" - - "github.com/tomwright/dasel/v3" -) - -func TestErrorMessages(t *testing.T) { - tests := []struct { - In error - Out string - }{ - {In: dasel.ErrMissingPreviousNode, Out: "missing previous node"}, - {In: &dasel.UnknownComparisonOperatorErr{Operator: "<"}, Out: "unknown comparison operator: <"}, - {In: &dasel.InvalidIndexErr{Index: "1"}, Out: "invalid index: 1"}, - {In: &dasel.UnsupportedSelector{Selector: "..."}, Out: "selector is not supported here: ..."}, - {In: &dasel.ValueNotFound{ - Selector: ".name", - }, Out: "no value found for selector: .name: "}, - {In: &dasel.ValueNotFound{ - Selector: ".name", - PreviousValue: reflect.ValueOf(map[string]interface{}{}), - }, Out: "no value found for selector: .name: map[]"}, - {In: &dasel.UnexpectedPreviousNilValue{Selector: ".name"}, Out: "previous value is nil: .name"}, - {In: &dasel.UnhandledCheckType{Value: ""}, Out: "unhandled check type: string"}, - } - - for _, testCase := range tests { - tc := testCase - t.Run("ErrorMessage", func(t *testing.T) { - if exp, got := tc.Out, tc.In.Error(); exp != got { - t.Errorf("expected %s, got %s", exp, got) - } - }) - } -} - -func TestErrorsIs(t *testing.T) { - type args struct { - Err error - Target error - } - - tests := []struct { - In args - Out bool - }{ - { - In: args{ - Err: &dasel.UnknownComparisonOperatorErr{}, - Target: &dasel.UnknownComparisonOperatorErr{}, - }, - Out: true, - }, - { - In: args{ - Err: fmt.Errorf("some error: %w", &dasel.UnknownComparisonOperatorErr{}), - Target: &dasel.UnknownComparisonOperatorErr{}, - }, - Out: true, - }, - { - In: args{ - Err: errors.New("some error"), - Target: &dasel.UnknownComparisonOperatorErr{}, - }, - Out: false, - }, - { - In: args{ - Err: &dasel.InvalidIndexErr{}, - Target: &dasel.InvalidIndexErr{}, - }, - Out: true, - }, - { - In: args{ - Err: fmt.Errorf("some error: %w", &dasel.InvalidIndexErr{}), - Target: &dasel.InvalidIndexErr{}, - }, - Out: true, - }, - { - In: args{ - Err: errors.New("some error"), - Target: &dasel.InvalidIndexErr{}, - }, - Out: false, - }, - { - In: args{ - Err: &dasel.UnsupportedSelector{}, - Target: &dasel.UnsupportedSelector{}, - }, - Out: true, - }, - { - In: args{ - Err: fmt.Errorf("some error: %w", &dasel.UnsupportedSelector{}), - Target: &dasel.UnsupportedSelector{}, - }, - Out: true, - }, - { - In: args{ - Err: errors.New("some error"), - Target: &dasel.UnsupportedSelector{}, - }, - Out: false, - }, - { - In: args{ - Err: &dasel.ValueNotFound{}, - Target: &dasel.ValueNotFound{}, - }, - Out: true, - }, - { - In: args{ - Err: fmt.Errorf("some error: %w", &dasel.ValueNotFound{}), - Target: &dasel.ValueNotFound{}, - }, - Out: true, - }, - { - In: args{ - Err: errors.New("some error"), - Target: &dasel.ValueNotFound{}, - }, - Out: false, - }, - { - In: args{ - Err: &dasel.UnexpectedPreviousNilValue{}, - Target: &dasel.UnexpectedPreviousNilValue{}, - }, - Out: true, - }, - { - In: args{ - Err: fmt.Errorf("some error: %w", &dasel.UnexpectedPreviousNilValue{}), - Target: &dasel.UnexpectedPreviousNilValue{}, - }, - Out: true, - }, - { - In: args{ - Err: errors.New("some error"), - Target: &dasel.UnexpectedPreviousNilValue{}, - }, - Out: false, - }, - { - In: args{ - Err: &dasel.UnhandledCheckType{}, - Target: &dasel.UnhandledCheckType{}, - }, - Out: true, - }, - { - In: args{ - Err: fmt.Errorf("some error: %w", &dasel.UnhandledCheckType{}), - Target: &dasel.UnhandledCheckType{}, - }, - Out: true, - }, - { - In: args{ - Err: errors.New("some error"), - Target: &dasel.UnhandledCheckType{}, - }, - Out: false, - }, - } - - for _, testCase := range tests { - tc := testCase - t.Run("ErrorMessage", func(t *testing.T) { - if exp, got := tc.Out, errors.Is(tc.In.Err, tc.In.Target); exp != got { - t.Errorf("expected %v, got %v", exp, got) - } - }) - } -} diff --git a/func.go b/func.go deleted file mode 100644 index 7f01e2a8..00000000 --- a/func.go +++ /dev/null @@ -1,208 +0,0 @@ -package dasel - -import ( - "fmt" - "reflect" - "strings" -) - -type ErrUnknownFunction struct { - Function string -} - -func (e ErrUnknownFunction) Error() string { - return fmt.Sprintf("unknown function: %s", e.Function) -} - -func (e ErrUnknownFunction) Is(other error) bool { - _, ok := other.(*ErrUnknownFunction) - return ok -} - -type ErrUnexpectedFunctionArgs struct { - Function string - Args []string - Message string -} - -func (e ErrUnexpectedFunctionArgs) Error() string { - return fmt.Sprintf("unexpected function args: %s(%s): %s", e.Function, strings.Join(e.Args, ", "), e.Message) -} - -func (e ErrUnexpectedFunctionArgs) Is(other error) bool { - o, ok := other.(*ErrUnexpectedFunctionArgs) - if !ok { - return false - } - if o.Function != "" && o.Function != e.Function { - return false - } - if o.Message != "" && o.Message != e.Message { - return false - } - if o.Args != nil && !reflect.DeepEqual(o.Args, e.Args) { - return false - } - return true -} - -func standardFunctions() *FunctionCollection { - collection := &FunctionCollection{} - collection.Add( - // Generic - ThisFunc, - LenFunc, - KeyFunc, - KeysFunc, - MergeFunc, - CountFunc, - MapOfFunc, - TypeFunc, - JoinFunc, - StringFunc, - NullFunc, - - // Selectors - IndexFunc, - AllFunc, - FirstFunc, - LastFunc, - PropertyFunc, - AppendFunc, - OrDefaultFunc, - - // Filters - FilterFunc, - FilterOrFunc, - - // Comparisons - EqualFunc, - MoreThanFunc, - LessThanFunc, - AndFunc, - OrFunc, - NotFunc, - - // Metadata - MetadataFunc, - ParentFunc, - ) - return collection -} - -// SelectorFunc is a function that can be executed in a selector. -type SelectorFunc func(c *Context, step *Step, args []string) (Values, error) - -type FunctionCollection struct { - functions []Function -} - -func (fc *FunctionCollection) ParseSelector(part string) *Selector { - for _, f := range fc.functions { - if s := f.AlternativeSelector(part); s != nil { - return s - } - } - return nil -} - -func (fc *FunctionCollection) Add(fs ...Function) { - fc.functions = append(fc.functions, fs...) -} - -func (fc *FunctionCollection) GetAll() map[string]SelectorFunc { - res := make(map[string]SelectorFunc) - for _, f := range fc.functions { - res[f.Name()] = f.Run - } - return res -} - -func (fc *FunctionCollection) Get(name string) (SelectorFunc, error) { - if f, ok := fc.GetAll()[name]; ok { - return f, nil - } - return nil, &ErrUnknownFunction{Function: name} -} - -type Function interface { - Name() string - Run(c *Context, s *Step, args []string) (Values, error) - AlternativeSelector(part string) *Selector -} - -type BasicFunction struct { - name string - runFn func(c *Context, s *Step, args []string) (Values, error) - alternativeSelectorFn func(part string) *Selector -} - -func (bf BasicFunction) Name() string { - return bf.name -} - -func (bf BasicFunction) Run(c *Context, s *Step, args []string) (Values, error) { - return bf.runFn(c, s, args) -} - -func (bf BasicFunction) AlternativeSelector(part string) *Selector { - if bf.alternativeSelectorFn == nil { - return nil - } - return bf.alternativeSelectorFn(part) -} - -func requireNoArgs(name string, args []string) error { - if len(args) > 0 { - return &ErrUnexpectedFunctionArgs{ - Function: name, - Args: args, - Message: "0 arguments expected", - } - } - return nil -} - -func requireExactlyXArgs(name string, args []string, x int) error { - if len(args) != x { - return &ErrUnexpectedFunctionArgs{ - Function: name, - Args: args, - Message: fmt.Sprintf("exactly %d arguments expected", x), - } - } - return nil -} - -func requireXOrMoreArgs(name string, args []string, x int) error { - if len(args) < x { - return &ErrUnexpectedFunctionArgs{ - Function: name, - Args: args, - Message: fmt.Sprintf("expected %d or more arguments", x), - } - } - return nil -} - -func requireXOrLessArgs(name string, args []string, x int) error { - if len(args) > x { - return &ErrUnexpectedFunctionArgs{ - Function: name, - Args: args, - Message: fmt.Sprintf("expected %d or less arguments", x), - } - } - return nil -} - -func requireModulusXArgs(name string, args []string, x int) error { - if len(args)%x != 0 { - return &ErrUnexpectedFunctionArgs{ - Function: name, - Args: args, - Message: fmt.Sprintf("expected arguments in groups of %d", x), - } - } - return nil -} diff --git a/func_all.go b/func_all.go deleted file mode 100644 index b8a263e7..00000000 --- a/func_all.go +++ /dev/null @@ -1,48 +0,0 @@ -package dasel - -import ( - "fmt" - "reflect" - - "github.com/tomwright/dasel/v3/dencoding" -) - -var AllFunc = BasicFunction{ - name: "all", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - if err := requireNoArgs("all", args); err != nil { - return nil, err - } - - input := s.inputs() - - res := make(Values, 0) - - for _, val := range input { - switch val.Kind() { - case reflect.String: - for _, r := range val.String() { - res = append(res, ValueOf(string(r))) - } - case reflect.Slice, reflect.Array: - for i := 0; i < val.Len(); i++ { - res = append(res, val.Index(i)) - } - case reflect.Map: - for _, key := range val.MapKeys() { - res = append(res, val.MapIndex(key)) - } - default: - if val.IsDencodingMap() { - for _, k := range val.Interface().(*dencoding.Map).Keys() { - res = append(res, val.dencodingMapIndex(ValueOf(k))) - } - } else { - return nil, fmt.Errorf("cannot use all selector on non slice/array/map types") - } - } - } - - return res, nil - }, -} diff --git a/func_all_test.go b/func_all_test.go deleted file mode 100644 index aca27874..00000000 --- a/func_all_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package dasel - -import "testing" - -func TestAllFunc(t *testing.T) { - t.Run("Args", selectTestErr( - "all(x)", - map[string]interface{}{}, - &ErrUnexpectedFunctionArgs{ - Function: "all", - Args: []string{"x"}, - }), - ) - - t.Run( - "RootAllSlice", - selectTest( - "all()", - []interface{}{"red", "green", "blue"}, - []interface{}{"red", "green", "blue"}, - ), - ) - t.Run( - "NestedAllSlice", - selectTest( - "colours.all()", - map[string]interface{}{ - "colours": []interface{}{"red", "green", "blue"}, - }, - []interface{}{"red", "green", "blue"}, - ), - ) - t.Run( - "AllString", - selectTest( - "all()", - "asd", - []interface{}{"a", "s", "d"}, - ), - ) -} diff --git a/func_and.go b/func_and.go deleted file mode 100644 index 899ce011..00000000 --- a/func_and.go +++ /dev/null @@ -1,53 +0,0 @@ -package dasel - -import ( - "fmt" - "reflect" -) - -var AndFunc = BasicFunction{ - name: "and", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - if err := requireXOrMoreArgs("and", args, 1); err != nil { - return nil, err - } - - input := s.inputs() - - runComparison := func(value Value, selector string) (bool, error) { - gotValues, err := c.subSelect(value, selector) - if err != nil { - return false, err - } - - if len(gotValues) > 1 { - return false, fmt.Errorf("and expects selector to return a single value") - } - - if len(gotValues) == 0 { - return false, nil - } - - return IsTruthy(gotValues[0]), nil - } - - res := make(Values, 0) - - for _, val := range input { - valPassed := true - for _, cmp := range args { - pass, err := runComparison(val, cmp) - if err != nil { - return nil, err - } - if !pass { - valPassed = false - break - } - } - res = append(res, Value{Value: reflect.ValueOf(valPassed)}) - } - - return res, nil - }, -} diff --git a/func_and_test.go b/func_and_test.go deleted file mode 100644 index 8a03eb93..00000000 --- a/func_and_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package dasel - -import ( - "testing" -) - -func TestAndFunc(t *testing.T) { - t.Run("Args", selectTestErr( - "and()", - map[string]interface{}{}, - &ErrUnexpectedFunctionArgs{ - Function: "and", - Args: []string{}, - }), - ) - - t.Run( - "NoneEqualMoreThan", - selectTest( - "numbers.all().and(equal(.,2),moreThan(.,2))", - map[string]interface{}{ - "numbers": []interface{}{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, - }, - []interface{}{ - false, false, false, false, false, false, false, false, false, false, - }, - ), - ) - t.Run( - "SomeEqualMoreThan", - selectTest( - "numbers.all().and(equal(.,4),moreThan(.,2))", - map[string]interface{}{ - "numbers": []interface{}{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, - }, - []interface{}{ - false, false, false, false, true, false, false, false, false, false, - }, - ), - ) -} diff --git a/func_append.go b/func_append.go deleted file mode 100644 index 258d2728..00000000 --- a/func_append.go +++ /dev/null @@ -1,45 +0,0 @@ -package dasel - -import ( - "fmt" - "reflect" -) - -var AppendFunc = BasicFunction{ - name: "append", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - if err := requireNoArgs("append", args); err != nil { - return nil, err - } - - input := s.inputs() - - if c.CreateWhenMissing() { - input = input.initEmptySlices() - } - - res := make(Values, 0) - - for _, val := range input { - switch val.Kind() { - case reflect.Slice, reflect.Array: - val = val.Append() - value := val.Index(val.Len() - 1) - res = append(res, value) - default: - return nil, fmt.Errorf("cannot use append selector on non slice/array types") - } - } - - return res, nil - }, - alternativeSelectorFn: func(part string) *Selector { - if part == "[]" { - return &Selector{ - funcName: "append", - funcArgs: []string{}, - } - } - return nil - }, -} diff --git a/func_count.go b/func_count.go deleted file mode 100644 index 0da7d2b2..00000000 --- a/func_count.go +++ /dev/null @@ -1,12 +0,0 @@ -package dasel - -var CountFunc = BasicFunction{ - name: "count", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - input := s.inputs() - - return Values{ - ValueOf(len(input)), - }, nil - }, -} diff --git a/func_count_test.go b/func_count_test.go deleted file mode 100644 index d10af778..00000000 --- a/func_count_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package dasel - -import ( - "testing" -) - -func TestCountFunc(t *testing.T) { - data := map[string]interface{}{ - "string": "hello", - "slice": []interface{}{ - 1, 2, 3, - }, - "falseBool": false, - "trueBool": true, - } - - t.Run( - "RootObject", - selectTest( - "count()", - data, - []interface{}{1}, - ), - ) - t.Run( - "All", - selectTest( - "all().count()", - data, - []interface{}{4}, - ), - ) - t.Run( - "NestedAll", - selectTest( - "slice.all().count()", - data, - []interface{}{3}, - ), - ) -} diff --git a/func_equal.go b/func_equal.go deleted file mode 100644 index eac2340c..00000000 --- a/func_equal.go +++ /dev/null @@ -1,79 +0,0 @@ -package dasel - -import ( - "fmt" - "reflect" - - "github.com/tomwright/dasel/v3/util" -) - -var EqualFunc = BasicFunction{ - name: "equal", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - if err := requireXOrMoreArgs("equal", args, 2); err != nil { - return nil, err - } - if err := requireModulusXArgs("equal", args, 2); err != nil { - return nil, err - } - - input := s.inputs() - - type comparison struct { - selector string - value string - } - - comparisons := make([]comparison, 0) - - currentComparison := comparison{} - - for i, v := range args { - switch i % 2 { - case 0: - currentComparison.selector = v - case 1: - currentComparison.value = v - comparisons = append(comparisons, currentComparison) - currentComparison = comparison{} - } - } - - runComparison := func(value Value, cmp comparison) (bool, error) { - gotValues, err := c.subSelect(value, cmp.selector) - if err != nil { - return false, err - } - - if len(gotValues) > 1 { - return false, fmt.Errorf("equal expects selector to return a single value") - } - - if len(gotValues) == 0 { - return false, nil - } - - gotValue := util.ToString(gotValues[0].Interface()) - return gotValue == cmp.value, nil - } - - res := make(Values, 0) - - for _, val := range input { - valPassed := true - for _, cmp := range comparisons { - pass, err := runComparison(val, cmp) - if err != nil { - return nil, err - } - if !pass { - valPassed = false - break - } - } - res = append(res, Value{Value: reflect.ValueOf(valPassed)}) - } - - return res, nil - }, -} diff --git a/func_equal_test.go b/func_equal_test.go deleted file mode 100644 index 0e2a7220..00000000 --- a/func_equal_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package dasel - -import ( - "testing" -) - -func TestEqualFunc(t *testing.T) { - - t.Run("Args", selectTestErr( - "equal()", - map[string]interface{}{}, - &ErrUnexpectedFunctionArgs{ - Function: "equal", - Args: []string{}, - }), - ) - - t.Run( - "Single Equal", - selectTest( - "name.all().equal(key(),first)", - map[string]interface{}{ - "name": map[string]interface{}{ - "first": "Tom", - "last": "Wright", - }, - }, - []interface{}{ - true, - false, - }, - ), - ) - - t.Run( - "Multi Equal", - selectTest( - "name.all().equal(key(),first,key(),first)", - map[string]interface{}{ - "name": map[string]interface{}{ - "first": "Tom", - "last": "Wright", - }, - }, - []interface{}{ - true, - false, - }, - ), - ) - - t.Run( - "Single Equal Optional Field", - selectTest( - "all().equal(primary,true)", - []interface{}{ - map[string]interface{}{ - "name": "red", - "hex": "ff0000", - "primary": true, - }, - map[string]interface{}{ - "name": "green", - "hex": "00ff00", - "primary": true, - }, - map[string]interface{}{ - "name": "blue", - "hex": "0000ff", - "primary": true, - }, - map[string]interface{}{ - "name": "orange", - "hex": "ffa500", - "primary": false, - }, - }, - []interface{}{ - true, true, true, false, - }, - ), - ) -} diff --git a/func_filter.go b/func_filter.go deleted file mode 100644 index 4888d7df..00000000 --- a/func_filter.go +++ /dev/null @@ -1,54 +0,0 @@ -package dasel - -import ( - "fmt" -) - -var FilterFunc = BasicFunction{ - name: "filter", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - if err := requireXOrMoreArgs("filter", args, 1); err != nil { - return nil, err - } - - input := s.inputs() - - runComparison := func(value Value, selector string) (bool, error) { - gotValues, err := c.subSelect(value, selector) - if err != nil { - return false, err - } - - if len(gotValues) > 1 { - return false, fmt.Errorf("filter expects selector to return a single value") - } - - if len(gotValues) == 0 { - return false, nil - } - - return IsTruthy(gotValues[0]), nil - } - - res := make(Values, 0) - - for _, val := range input { - valPassed := true - for _, cmp := range args { - pass, err := runComparison(val, cmp) - if err != nil { - return nil, err - } - if !pass { - valPassed = false - break - } - } - if valPassed { - res = append(res, val) - } - } - - return res, nil - }, -} diff --git a/func_filter_or.go b/func_filter_or.go deleted file mode 100644 index 6549df21..00000000 --- a/func_filter_or.go +++ /dev/null @@ -1,54 +0,0 @@ -package dasel - -import ( - "fmt" -) - -var FilterOrFunc = BasicFunction{ - name: "filterOr", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - if err := requireXOrMoreArgs("filterOr", args, 1); err != nil { - return nil, err - } - - input := s.inputs() - - runComparison := func(value Value, selector string) (bool, error) { - gotValues, err := c.subSelect(value, selector) - if err != nil { - return false, err - } - - if len(gotValues) > 1 { - return false, fmt.Errorf("filter expects selector to return a single value") - } - - if len(gotValues) == 0 { - return false, nil - } - - return IsTruthy(gotValues[0]), nil - } - - res := make(Values, 0) - - for _, val := range input { - valPassed := false - for _, cmp := range args { - pass, err := runComparison(val, cmp) - if err != nil { - return nil, err - } - if pass { - valPassed = true - break - } - } - if valPassed { - res = append(res, val) - } - } - - return res, nil - }, -} diff --git a/func_filter_or_test.go b/func_filter_or_test.go deleted file mode 100644 index 5f4a74bd..00000000 --- a/func_filter_or_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package dasel - -import ( - "testing" -) - -func TestFilterOrFunc(t *testing.T) { - - t.Run("Args", selectTestErr( - "filterOr()", - map[string]interface{}{}, - &ErrUnexpectedFunctionArgs{ - Function: "filterOr", - Args: []string{}, - }), - ) - - t.Run( - "Filter Equal Key", - selectTest( - "name.all().filterOr(equal(key(),first))", - map[string]interface{}{ - "name": map[string]interface{}{ - "first": "Tom", - "last": "Wright", - }, - }, - []interface{}{ - "Tom", - }, - ), - ) - - t.Run( - "Multiple Filter Or Equal Key", - selectTest( - "name.all().filterOr(equal(key(),first),equal(key(),last))", - map[string]interface{}{ - "name": map[string]interface{}{ - "first": "Tom", - "last": "Wright", - }, - }, - []interface{}{ - "Tom", - "Wright", - }, - ), - ) - - t.Run( - "MoreThanEqual", - selectTest( - "nums.all().filterOr(moreThan(.,3),equal(.,3))", - map[string]interface{}{ - "nums": []interface{}{0, 1, 2, 3, 4, 5}, - }, - []interface{}{3, 4, 5}, - ), - ) - - t.Run( - "LessThanEqual", - selectTest( - "nums.all().filterOr(lessThan(.,3),equal(.,3))", - map[string]interface{}{ - "nums": []interface{}{0, 1, 2, 3, 4, 5}, - }, - []interface{}{0, 1, 2, 3}, - ), - ) -} diff --git a/func_filter_test.go b/func_filter_test.go deleted file mode 100644 index 2eb33d27..00000000 --- a/func_filter_test.go +++ /dev/null @@ -1,197 +0,0 @@ -package dasel - -import ( - "testing" -) - -func TestFilterFunc(t *testing.T) { - - t.Run("Args", selectTestErr( - "filter()", - map[string]interface{}{}, - &ErrUnexpectedFunctionArgs{ - Function: "filter", - Args: []string{}, - }), - ) - - t.Run( - "Filter Equal Key", - selectTest( - "name.all().filter(equal(key(),first))", - map[string]interface{}{ - "name": map[string]interface{}{ - "first": "Tom", - "last": "Wright", - }, - }, - []interface{}{ - "Tom", - }, - ), - ) - - t.Run( - "Multiple Filter Equal Key", - selectTest( - "name.all().filter(equal(key(),first),equal(key(),last))", - map[string]interface{}{ - "name": map[string]interface{}{ - "first": "Tom", - "last": "Wright", - }, - }, - []interface{}{}, - ), - ) - - t.Run( - "Filter Equal Prop", - selectTest( - "all().filter(equal(primary,true)).name", - []interface{}{ - map[string]interface{}{ - "name": "red", - "hex": "ff0000", - "primary": true, - }, - map[string]interface{}{ - "name": "green", - "hex": "00ff00", - "primary": true, - }, - map[string]interface{}{ - "name": "blue", - "hex": "0000ff", - "primary": true, - }, - map[string]interface{}{ - "name": "orange", - "hex": "ffa500", - "primary": false, - }, - }, - []interface{}{ - "red", "green", "blue", - }, - ), - ) - - t.Run( - "FilterNestedProp", - selectTest( - "all().filter(equal(flags.banned,false)).name", - []map[string]interface{}{ - { - "flags": map[string]interface{}{ - "banned": false, - }, - "name": "Tom", - }, - { - "flags": map[string]interface{}{ - "banned": true, - }, - "name": "Jim", - }, - }, - []interface{}{ - "Tom", - }, - ), - ) - - t.Run( - "Filter And", - selectTest( - "all().filter(and(equal(primary,true),equal(name,red))).name", - []interface{}{ - map[string]interface{}{ - "name": "red", - "hex": "ff0000", - "primary": true, - }, - map[string]interface{}{ - "name": "green", - "hex": "00ff00", - "primary": true, - }, - map[string]interface{}{ - "name": "blue", - "hex": "0000ff", - "primary": true, - }, - map[string]interface{}{ - "name": "orange", - "hex": "ffa500", - "primary": false, - }, - }, - []interface{}{ - "red", - }, - ), - ) - - t.Run( - "Filter And", - selectTest( - "all().filter(and(equal(primary,true),equal(name,orange))).name", - []interface{}{ - map[string]interface{}{ - "name": "red", - "hex": "ff0000", - "primary": true, - }, - map[string]interface{}{ - "name": "green", - "hex": "00ff00", - "primary": true, - }, - map[string]interface{}{ - "name": "blue", - "hex": "0000ff", - "primary": true, - }, - map[string]interface{}{ - "name": "orange", - "hex": "ffa500", - "primary": false, - }, - }, - []interface{}{}, - ), - ) - - t.Run( - "Filter Or", - selectTest( - "all().filter(or(equal(primary,true),equal(name,orange))).name", - []interface{}{ - map[string]interface{}{ - "name": "red", - "hex": "ff0000", - "primary": true, - }, - map[string]interface{}{ - "name": "green", - "hex": "00ff00", - "primary": true, - }, - map[string]interface{}{ - "name": "blue", - "hex": "0000ff", - "primary": true, - }, - map[string]interface{}{ - "name": "orange", - "hex": "ffa500", - "primary": false, - }, - }, - []interface{}{ - "red", "green", "blue", "orange", - }, - ), - ) -} diff --git a/func_first.go b/func_first.go deleted file mode 100644 index d2210001..00000000 --- a/func_first.go +++ /dev/null @@ -1,34 +0,0 @@ -package dasel - -import ( - "fmt" - "reflect" -) - -var FirstFunc = BasicFunction{ - name: "first", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - if err := requireNoArgs("first", args); err != nil { - return nil, err - } - - input := s.inputs() - - res := make(Values, 0) - - for _, val := range input { - switch val.Kind() { - case reflect.Slice, reflect.Array: - if val.Len() == 0 { - return nil, fmt.Errorf("index out of range: %w", &ErrIndexNotFound{Index: 0}) - } - value := val.Index(0) - res = append(res, value) - default: - return nil, fmt.Errorf("cannot use first selector on non slice/array types: %w", &ErrIndexNotFound{Index: 0}) - } - } - - return res, nil - }, -} diff --git a/func_first_test.go b/func_first_test.go deleted file mode 100644 index 4924f3de..00000000 --- a/func_first_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package dasel - -import ( - "testing" -) - -func TestFirstFunc(t *testing.T) { - t.Run("Args", selectTestErr( - "first(x)", - map[string]interface{}{}, - &ErrUnexpectedFunctionArgs{ - Function: "first", - Args: []string{"x"}, - }), - ) - - t.Run("NotFound", selectTestErr( - "first()", - []interface{}{}, - &ErrIndexNotFound{ - Index: 0, - }), - ) - - t.Run("NotFoundOnInvalidType", selectTestErr( - "x.first()", - map[string]interface{}{"x": "y"}, - &ErrIndexNotFound{ - Index: 0, - }), - ) - - original := map[string]interface{}{ - "name": map[string]interface{}{ - "first": "Tom", - "last": "Wright", - }, - "colours": []interface{}{ - "red", "green", "blue", - }, - } - - t.Run( - "First", - selectTest( - "colours.first()", - original, - []interface{}{ - "red", - }, - ), - ) -} diff --git a/func_index.go b/func_index.go deleted file mode 100644 index f1bc4a7e..00000000 --- a/func_index.go +++ /dev/null @@ -1,92 +0,0 @@ -package dasel - -import ( - "fmt" - "reflect" - "strconv" - "strings" -) - -type ErrIndexNotFound struct { - Index int -} - -func (e ErrIndexNotFound) Error() string { - return fmt.Sprintf("index not found: %d", e.Index) -} - -func (e ErrIndexNotFound) Is(other error) bool { - o, ok := other.(*ErrIndexNotFound) - if !ok { - return false - } - if o.Index >= 0 && o.Index != e.Index { - return false - } - return true -} - -var IndexFunc = BasicFunction{ - name: "index", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - if err := requireXOrMoreArgs("index", args, 1); err != nil { - return nil, err - } - - input := s.inputs() - - res := make(Values, 0) - - for _, val := range input { - for _, indexStr := range args { - isOptional := strings.HasSuffix(indexStr, "?") - if isOptional { - indexStr = strings.TrimSuffix(indexStr, "?") - } - - index, err := strconv.Atoi(indexStr) - if err != nil { - if isOptional { - continue - } - return nil, fmt.Errorf("invalid index: %w", err) - } - - switch val.Kind() { - case reflect.String: - runes := []rune(val.String()) - if index < 0 || index > len(runes)-1 { - if isOptional { - continue - } - return nil, fmt.Errorf("index out of range: %w", &ErrIndexNotFound{Index: index}) - } - res = append(res, ValueOf(string(runes[index]))) - case reflect.Slice, reflect.Array: - if index < 0 || index > val.Len()-1 { - if isOptional { - continue - } - return nil, fmt.Errorf("index out of range: %w", &ErrIndexNotFound{Index: index}) - } - value := val.Index(index) - res = append(res, value) - default: - return nil, fmt.Errorf("cannot use index selector on non slice/array types: %w", &ErrIndexNotFound{Index: index}) - } - } - } - - return res, nil - }, - alternativeSelectorFn: func(part string) *Selector { - if part != "[]" && strings.HasPrefix(part, "[") && strings.HasSuffix(part, "]") { - strings.Split(strings.TrimPrefix(strings.TrimSuffix(part, "]"), "["), ",") - return &Selector{ - funcName: "index", - funcArgs: strings.Split(strings.TrimPrefix(strings.TrimSuffix(part, "]"), "["), ","), - } - } - return nil - }, -} diff --git a/func_index_test.go b/func_index_test.go deleted file mode 100644 index 6f25054b..00000000 --- a/func_index_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package dasel - -import ( - "testing" -) - -func TestIndexFunc(t *testing.T) { - t.Run("Args", selectTestErr( - "index()", - map[string]interface{}{}, - &ErrUnexpectedFunctionArgs{ - Function: "index", - Args: []string{}, - }), - ) - - t.Run("NotFound", selectTestErr( - "[0]", - []interface{}{}, - &ErrIndexNotFound{ - Index: 0, - }), - ) - - t.Run("NotFoundOnInvalidType", selectTestErr( - "[0]", - map[string]interface{}{}, - &ErrIndexNotFound{ - Index: 0, - }), - ) - - original := map[string]interface{}{ - "name": map[string]interface{}{ - "first": "Tom", - "last": "Wright", - }, - "colours": []interface{}{ - "red", "green", "blue", - }, - } - - t.Run( - "Index", - selectTest( - "colours.index(1)", - original, - []interface{}{ - "green", - }, - ), - ) - - t.Run( - "IndexString", - selectTest( - "colours.index(1).index(1)", - original, - []interface{}{ - "r", - }, - ), - ) - - t.Run( - "IndexMulti", - selectTest( - "colours.index(0,1,2)", - original, - []interface{}{ - "red", - "green", - "blue", - }, - ), - ) - - t.Run( - "IndexShorthand", - selectTest( - "colours.[1]", - original, - []interface{}{ - "green", - }, - ), - ) - - t.Run( - "IndexShorthandMulti", - selectTest( - "colours.[0,1,2]", - original, - []interface{}{ - "red", - "green", - "blue", - }, - ), - ) -} diff --git a/func_join.go b/func_join.go deleted file mode 100644 index af9a8f52..00000000 --- a/func_join.go +++ /dev/null @@ -1,61 +0,0 @@ -package dasel - -import ( - "strings" - - "github.com/tomwright/dasel/v3/util" -) - -var JoinFunc = BasicFunction{ - name: "join", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - if err := requireXOrMoreArgs("join", args, 1); err != nil { - return nil, err - } - - input := s.inputs() - - getValues := func(value Value, selector string) ([]string, error) { - gotValues, err := c.subSelect(value, selector) - if err != nil { - return []string{}, err - } - - res := make([]string, len(gotValues)) - for k, v := range gotValues { - res[k] = util.ToString(v.Interface()) - } - return res, nil - } - - res := make(Values, 0) - - separator := args[0] - args = args[1:] - - // No args - join all input values - if len(args) == 0 { - values := make([]string, len(input)) - for k, v := range input { - values[k] = util.ToString(v.Interface()) - } - res = append(res, ValueOf(strings.Join(values, separator))) - return res, nil - } - - // There are args - use each as a selector and join any resulting values. - values := make([]string, 0) - for _, val := range input { - for _, cmp := range args { - vals, err := getValues(val, cmp) - if err != nil { - return nil, err - } - values = append(values, vals...) - } - } - res = append(res, ValueOf(strings.Join(values, separator))) - - return res, nil - }, -} diff --git a/func_join_test.go b/func_join_test.go deleted file mode 100644 index f9e5e362..00000000 --- a/func_join_test.go +++ /dev/null @@ -1,213 +0,0 @@ -package dasel - -import ( - "strings" - "testing" - - "github.com/tomwright/dasel/v3/dencoding" -) - -func TestJoinFunc(t *testing.T) { - t.Run("Args", selectTestErr( - "join()", - map[string]interface{}{}, - &ErrUnexpectedFunctionArgs{ - Function: "join", - Args: []string{}, - }), - ) - - original := dencoding.NewMap(). - Set("name", dencoding.NewMap(). - Set("first", "Tom"). - Set("last", "Wright")). - Set("colours", []interface{}{ - "red", "green", "blue", - }) - - t.Run( - "JoinCommaSeparator", - selectTestAssert( - "name.all().join(\\,)", - original, - func(t *testing.T, got []any) { - required := []string{"Tom", "Wright"} - if len(got) != 1 { - t.Errorf("expected 1 result, got %v", got) - return - } - str, ok := got[0].(string) - if !ok { - t.Errorf("expected 1st result to be a string, got %T", got[0]) - return - } - - gotStrs := strings.Split(str, ",") - for _, req := range required { - found := false - for _, got := range gotStrs { - if got == req { - found = true - continue - } - } - if !found { - t.Errorf("expected %v, got %v", required, got) - } - } - if len(got) != 1 { - t.Errorf("expected 1 result, got %v", got) - return - } - }, - ), - ) - - t.Run( - "JoinNewlineSeparator", - selectTestAssert( - "name.all().join(\\\n)", - original, - func(t *testing.T, got []any) { - if len(got) != 1 { - t.Errorf("expected 1 result, got %v", got) - return - } - str, ok := got[0].(string) - if !ok { - t.Errorf("expected 1st result to be a string, got %T", got[0]) - return - } - - exp := "Tom\nWright" - if exp != str { - t.Errorf("expected %v, got %v", exp, str) - return - } - - //gotStrs := strings.Split(str, ",") - //for _, req := range required { - // found := false - // for _, got := range gotStrs { - // if got == req { - // found = true - // continue - // } - // } - // if !found { - // t.Errorf("expected %v, got %v", required, got) - // } - //} - //if len(got) != 1 { - // t.Errorf("expected 1 result, got %v", got) - // return - //} - }, - ), - ) - - t.Run( - "JoinSpaceSeparator", - selectTestAssert( - "name.all().join( )", - original, - func(t *testing.T, got []any) { - required := []string{"Tom", "Wright"} - if len(got) != 1 { - t.Errorf("expected 1 result, got %v", got) - return - } - str, ok := got[0].(string) - if !ok { - t.Errorf("expected 1st result to be a string, got %T", got[0]) - return - } - - gotStrs := strings.Split(str, " ") - for _, req := range required { - found := false - for _, got := range gotStrs { - if got == req { - found = true - continue - } - } - if !found { - t.Errorf("expected %v, got %v", required, got) - } - } - if len(got) != 1 { - t.Errorf("expected 1 result, got %v", got) - return - } - }, - ), - ) - - t.Run( - "JoinWithSeparatorsAndSelectors", - selectTest( - "name.join( ,last,first)", - original, - []interface{}{ - "Wright Tom", - }, - ), - ) - - t.Run( - "JoinInMap", - selectTest( - "mapOf(first,name.first,last,name.last,full,name.join( ,string(Mr),first,last))", - original, - []interface{}{ - map[string]interface{}{ - "first": "Tom", - "full": "Mr Tom Wright", - "last": "Wright", - }, - }, - ), - ) - - t.Run( - "JoinManyLists", - selectTestAssert( - "all().join(\\,,all())", - dencoding.NewMap(). - Set("x", []interface{}{1, 2, 3}). - Set("y", []interface{}{4, 5, 6}). - Set("z", []interface{}{7, 8, 9}), - func(t *testing.T, got []any) { - required := []string{"1", "2", "3", "4", "5", "6", "7", "8", "9"} - if len(got) != 1 { - t.Errorf("expected 1 result, got %v", got) - return - } - str, ok := got[0].(string) - if !ok { - t.Errorf("expected 1st result to be a string, got %T", got[0]) - return - } - - gotStrs := strings.Split(str, ",") - for _, req := range required { - found := false - for _, got := range gotStrs { - if got == req { - found = true - continue - } - } - if !found { - t.Errorf("expected %v, got %v", required, got) - } - } - if len(got) != 1 { - t.Errorf("expected 1 result, got %v", got) - return - } - }, - ), - ) -} diff --git a/func_key.go b/func_key.go deleted file mode 100644 index 96cd5062..00000000 --- a/func_key.go +++ /dev/null @@ -1,24 +0,0 @@ -package dasel - -var KeyFunc = BasicFunction{ - name: "key", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - if err := requireNoArgs("key", args); err != nil { - return nil, err - } - - input := s.inputs() - - res := make(Values, 0) - - for _, i := range input { - p := i.Metadata("key") - if p == nil { - continue - } - res = append(res, ValueOf(p)) - } - - return res, nil - }, -} diff --git a/func_keys.go b/func_keys.go deleted file mode 100644 index 12ff8f6d..00000000 --- a/func_keys.go +++ /dev/null @@ -1,118 +0,0 @@ -package dasel - -import ( - "fmt" - "reflect" - "sort" - "strings" - - "github.com/tomwright/dasel/v3/dencoding" -) - -type ErrInvalidType struct { - ExpectedTypes []string - CurrentType string -} - -func (e *ErrInvalidType) Error() string { - return fmt.Sprintf("unexpected types: expect %s, get %s", strings.Join(e.ExpectedTypes, " "), e.CurrentType) -} - -func (e *ErrInvalidType) Is(other error) bool { - o, ok := other.(*ErrInvalidType) - if !ok { - return false - } - if len(e.ExpectedTypes) != len(o.ExpectedTypes) { - return false - } - if e.CurrentType != o.CurrentType { - return false - } - for i, t := range e.ExpectedTypes { - if t != o.ExpectedTypes[i] { - return false - } - } - return true -} - -var KeysFunc = BasicFunction{ - name: "keys", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - if err := requireNoArgs("keys", args); err != nil { - return nil, err - } - - input := s.inputs() - - res := make(Values, len(input)) - - for i, val := range input { - switch val.Kind() { - case reflect.Slice, reflect.Array: - list := make([]any, 0, val.Len()) - - for i := 0; i < val.Len(); i++ { - list = append(list, i) - } - - res[i] = ValueOf(list) - case reflect.Map: - keys := val.MapKeys() - - // we expect map keys to be string first so that we can sort them - list, ok := getStringList(keys) - if !ok { - list = getAnyList(keys) - } - - res[i] = ValueOf(list) - default: - if val.IsDencodingMap() { - dencodingMap := val.Interface().(*dencoding.Map) - mapKeys := dencodingMap.Keys() - list := make([]any, 0, len(mapKeys)) - for _, k := range mapKeys { - list = append(list, k) - } - res[i] = ValueOf(list) - } else { - return nil, &ErrInvalidType{ - ExpectedTypes: []string{"slice", "array", "map"}, - CurrentType: val.Kind().String(), - } - } - } - } - - return res, nil - }, -} - -func getStringList(values []Value) ([]any, bool) { - stringList := make([]string, len(values)) - for i, v := range values { - if v.Kind() != reflect.String { - return nil, false - } - stringList[i] = v.String() - } - - sort.Strings(stringList) - - anyList := make([]any, len(stringList)) - for i, v := range stringList { - anyList[i] = v - } - - return anyList, true -} - -func getAnyList(values []Value) []any { - anyList := make([]any, len(values)) - for i, v := range values { - anyList[i] = v.Interface() - } - return anyList -} diff --git a/func_keys_test.go b/func_keys_test.go deleted file mode 100644 index 9c55b637..00000000 --- a/func_keys_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package dasel - -import ( - "testing" - - "github.com/tomwright/dasel/v3/dencoding" -) - -func TestKeysFunc(t *testing.T) { - testdata := map[string]any{ - "object": map[string]any{ - "c": 3, "a": 1, "b": 2, - }, - "list": []any{111, 222, 333}, - "string": "something", - "dencodingMap": dencoding.NewMap(). - Set("a", 1). - Set("b", 2). - Set("c", 3), - } - - t.Run( - "root", - selectTest( - "keys()", - testdata, - []any{[]any{"dencodingMap", "list", "object", "string"}}, - ), - ) - - t.Run( - "List", - selectTest( - "list.keys()", - testdata, - []any{[]any{0, 1, 2}}, - ), - ) - - t.Run( - "Object", - selectTest( - "object.keys()", - testdata, - []any{[]any{"a", "b", "c"}}, // sorted - ), - ) - - t.Run( - "Dencoding Map", - selectTest( - "dencodingMap.keys()", - testdata, - []any{[]any{"a", "b", "c"}}, // sorted - ), - ) - - t.Run("InvalidType", - selectTestErr( - "string.keys()", - testdata, - &ErrInvalidType{ - ExpectedTypes: []string{"slice", "array", "map"}, - CurrentType: "string", - }, - ), - ) -} diff --git a/func_last.go b/func_last.go deleted file mode 100644 index cee41181..00000000 --- a/func_last.go +++ /dev/null @@ -1,35 +0,0 @@ -package dasel - -import ( - "fmt" - "reflect" -) - -var LastFunc = BasicFunction{ - name: "last", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - if err := requireNoArgs("last", args); err != nil { - return nil, err - } - - input := s.inputs() - - res := make(Values, 0) - - for _, val := range input { - switch val.Kind() { - case reflect.Slice, reflect.Array: - index := val.Len() - 1 - if val.Len() == 0 { - return nil, fmt.Errorf("index out of range: %w", &ErrIndexNotFound{Index: index}) - } - value := val.Index(index) - res = append(res, value) - default: - return nil, fmt.Errorf("cannot use last selector on non slice/array types: %w", &ErrIndexNotFound{Index: 0}) - } - } - - return res, nil - }, -} diff --git a/func_last_test.go b/func_last_test.go deleted file mode 100644 index bc529a85..00000000 --- a/func_last_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package dasel - -import ( - "testing" -) - -func TestLastFunc(t *testing.T) { - t.Run("Args", selectTestErr( - "last(x)", - map[string]interface{}{}, - &ErrUnexpectedFunctionArgs{ - Function: "last", - Args: []string{"x"}, - }), - ) - - t.Run("NotFound", selectTestErr( - "last()", - []interface{}{}, - &ErrIndexNotFound{ - Index: -1, - }), - ) - - t.Run("NotFoundOnInvalidType", selectTestErr( - "x.last()", - map[string]interface{}{"x": "y"}, - &ErrIndexNotFound{ - Index: 0, - }), - ) - - original := map[string]interface{}{ - "name": map[string]interface{}{ - "first": "Tom", - "last": "Wright", - }, - "colours": []interface{}{ - "red", "green", "blue", - }, - } - - t.Run( - "Last", - selectTest( - "colours.last()", - original, - []interface{}{ - "blue", - }, - ), - ) -} diff --git a/func_len.go b/func_len.go deleted file mode 100644 index c3b61390..00000000 --- a/func_len.go +++ /dev/null @@ -1,20 +0,0 @@ -package dasel - -var LenFunc = BasicFunction{ - name: "len", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - if err := requireNoArgs("len", args); err != nil { - return nil, err - } - - input := s.inputs() - - res := make(Values, 0) - - for _, val := range input { - res = append(res, ValueOf(val.Len())) - } - - return res, nil - }, -} diff --git a/func_len_test.go b/func_len_test.go deleted file mode 100644 index 36876202..00000000 --- a/func_len_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package dasel - -import ( - "testing" -) - -func TestLenFunc(t *testing.T) { - t.Run("Args", selectTestErr( - "len(x)", - map[string]interface{}{}, - &ErrUnexpectedFunctionArgs{ - Function: "len", - Args: []string{"x"}, - }), - ) - - data := map[string]interface{}{ - "string": "hello", - "slice": []interface{}{ - 1, 2, 3, - }, - "falseBool": false, - "trueBool": true, - } - - t.Run( - "String", - selectTest( - "string.len()", - data, - []interface{}{5}, - ), - ) - t.Run( - "Slice", - selectTest( - "slice.len()", - data, - []interface{}{3}, - ), - ) - t.Run( - "Else Bool", - selectTest( - "falseBool.len()", - data, - []interface{}{0}, - ), - ) - t.Run( - "Then Bool", - selectTest( - "trueBool.len()", - data, - []interface{}{1}, - ), - ) -} diff --git a/func_less_than.go b/func_less_than.go deleted file mode 100644 index e204dc1d..00000000 --- a/func_less_than.go +++ /dev/null @@ -1,89 +0,0 @@ -package dasel - -import ( - "fmt" - "reflect" - "sort" - - "github.com/tomwright/dasel/v3/util" -) - -var LessThanFunc = BasicFunction{ - name: "lessThan", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - if err := requireExactlyXArgs("lessThan", args, 2); err != nil { - return nil, err - } - - input := s.inputs() - - type comparison struct { - selector string - value string - } - - comparisons := make([]comparison, 0) - - currentComparison := comparison{} - - for i, v := range args { - switch i % 2 { - case 0: - currentComparison.selector = v - case 1: - currentComparison.value = v - comparisons = append(comparisons, currentComparison) - currentComparison = comparison{} - } - } - - runComparison := func(value Value, cmp comparison) (bool, error) { - gotValues, err := c.subSelect(value, cmp.selector) - if err != nil { - return false, err - } - - if len(gotValues) > 1 { - return false, fmt.Errorf("equal expects selector to return a single value") - } - - if len(gotValues) == 0 { - return false, nil - } - - gotValue := util.ToString(gotValues[0].Interface()) - - // The values are equal - if gotValue == cmp.value { - return false, nil - } - - sortedVals := []string{gotValue, cmp.value} - sort.Strings(sortedVals) - - if sortedVals[0] == gotValue { - return true, nil - } - return false, nil - } - - res := make(Values, 0) - - for _, val := range input { - valPassed := true - for _, cmp := range comparisons { - pass, err := runComparison(val, cmp) - if err != nil { - return nil, err - } - if !pass { - valPassed = false - break - } - } - res = append(res, Value{Value: reflect.ValueOf(valPassed)}) - } - - return res, nil - }, -} diff --git a/func_less_than_test.go b/func_less_than_test.go deleted file mode 100644 index bf8f99f2..00000000 --- a/func_less_than_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package dasel - -import ( - "testing" -) - -func TestLessThanFunc(t *testing.T) { - - t.Run("Args", selectTestErr( - "lessThan()", - map[string]interface{}{}, - &ErrUnexpectedFunctionArgs{ - Function: "lessThan", - Args: []string{}, - }), - ) - - t.Run( - "Less Than", - selectTest( - "nums.all().lessThan(.,5)", - map[string]interface{}{ - "nums": []any{ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, - }, - }, - []interface{}{ - true, - true, - true, - true, - true, - false, - false, - false, - false, - false, - }, - ), - ) -} diff --git a/func_map_of.go b/func_map_of.go deleted file mode 100644 index 6b096ed0..00000000 --- a/func_map_of.go +++ /dev/null @@ -1,72 +0,0 @@ -package dasel - -import ( - "fmt" - "reflect" -) - -var MapOfFunc = BasicFunction{ - name: "mapOf", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - if err := requireXOrMoreArgs("mapOf", args, 2); err != nil { - return nil, err - } - if err := requireModulusXArgs("mapOf", args, 2); err != nil { - return nil, err - } - - input := s.inputs() - - type pair struct { - key string - selector string - } - - pairs := make([]pair, 0) - - currentPair := pair{} - - for i, v := range args { - switch i % 2 { - case 0: - currentPair.key = v - case 1: - currentPair.selector = v - pairs = append(pairs, currentPair) - currentPair = pair{} - } - } - - getValue := func(value Value, p pair) (Value, error) { - gotValues, err := c.subSelect(value, p.selector) - if err != nil { - return Value{}, err - } - - if len(gotValues) != 1 { - return Value{}, fmt.Errorf("mapOf expects selector to return exactly 1 value") - } - - return gotValues[0], nil - } - - res := make(Values, 0) - - for _, val := range input { - result := reflect.MakeMap(mapStringInterfaceType) - - for _, p := range pairs { - gotValue, err := getValue(val, p) - if err != nil { - return nil, err - } - - result.SetMapIndex(reflect.ValueOf(p.key), gotValue.Value) - } - - res = append(res, ValueOf(result)) - } - - return res, nil - }, -} diff --git a/func_map_of_test.go b/func_map_of_test.go deleted file mode 100644 index 538a827b..00000000 --- a/func_map_of_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package dasel - -import ( - "testing" -) - -func TestMapOfFunc(t *testing.T) { - - t.Run("Args", selectTestErr( - "mapOf()", - map[string]interface{}{}, - &ErrUnexpectedFunctionArgs{ - Function: "mapOf", - Args: []string{}, - }), - ) - - t.Run( - "Single Equal", - selectTest( - "mapOf(firstName,name.first,lastName,name.last)", - map[string]interface{}{ - "name": map[string]interface{}{ - "first": "Tom", - "last": "Wright", - }, - }, - []interface{}{ - map[string]interface{}{ - "firstName": "Tom", - "lastName": "Wright", - }, - }, - ), - ) -} diff --git a/func_merge.go b/func_merge.go deleted file mode 100644 index 3224626d..00000000 --- a/func_merge.go +++ /dev/null @@ -1,45 +0,0 @@ -package dasel - -import "reflect" - -var MergeFunc = BasicFunction{ - name: "merge", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - input := s.inputs() - - res := make(Values, 0) - - if len(args) == 0 { - // Merge all inputs into a slice. - resSlice := reflect.MakeSlice(sliceInterfaceType, len(input), len(input)) - for i, val := range input { - resSlice.Index(i).Set(val.Value) - } - resPointer := reflect.New(resSlice.Type()) - resPointer.Elem().Set(resSlice) - - res = append(res, ValueOf(resPointer)) - return res, nil - } - - // Merge all inputs into a slice. - resSlice := reflect.MakeSlice(sliceInterfaceType, 0, 0) - for _, val := range input { - for _, a := range args { - gotValues, err := c.subSelect(val, a) - if err != nil { - return nil, err - } - - for _, gotVal := range gotValues { - resSlice = reflect.Append(resSlice, gotVal.Value) - } - } - } - resPointer := reflect.New(resSlice.Type()) - resPointer.Elem().Set(resSlice) - - res = append(res, ValueOf(resPointer)) - return res, nil - }, -} diff --git a/func_merge_test.go b/func_merge_test.go deleted file mode 100644 index c8896b0b..00000000 --- a/func_merge_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package dasel - -import ( - "testing" -) - -func TestMergeFunc(t *testing.T) { - - t.Run( - "MergeWithArgs", - selectTest( - "merge(name.first,firstNames.all())", - map[string]interface{}{ - "name": map[string]interface{}{ - "first": "Tom", - "last": "Wright", - }, - "firstNames": []interface{}{ - "Jim", - "Bob", - }, - }, - []interface{}{ - []interface{}{ - "Tom", - "Jim", - "Bob", - }, - }, - ), - ) - - t.Run( - "MergeWithArgsAll", - selectTest( - "merge(name.first,firstNames.all()).all()", - map[string]interface{}{ - "name": map[string]interface{}{ - "first": "Tom", - "last": "Wright", - }, - "firstNames": []interface{}{ - "Jim", - "Bob", - }, - }, - []interface{}{ - "Tom", - "Jim", - "Bob", - }, - ), - ) - - // Flaky test due to ordering. - // t.Run( - // "MergeNoArgs", - // selectTest( - // "name.all().merge()", - // map[string]interface{}{ - // "name": map[string]interface{}{ - // "first": "Tom", - // "last": "Wright", - // }, - // }, - // []interface{}{ - // []interface{}{ - // "Tom", - // "Wright", - // }, - // }, - // ), - // ) - - t.Run( - "MergeNoArgsAll", - selectTest( - "name.all().merge().all()", - map[string]interface{}{ - "name": map[string]interface{}{ - "first": "Tom", - "last": "Wright", - }, - }, - []interface{}{ - "Tom", - "Wright", - }, - ), - ) -} diff --git a/func_metadata.go b/func_metadata.go deleted file mode 100644 index 9c5d9d1d..00000000 --- a/func_metadata.go +++ /dev/null @@ -1,22 +0,0 @@ -package dasel - -var MetadataFunc = BasicFunction{ - name: "metadata", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - if err := requireXOrMoreArgs("metadata", args, 1); err != nil { - return nil, err - } - - input := s.inputs() - - res := make(Values, 0) - - for _, val := range input { - for _, a := range args { - res = append(res, ValueOf(val.Metadata(a))) - } - } - - return res, nil - }, -} diff --git a/func_metadata_test.go b/func_metadata_test.go deleted file mode 100644 index 4d66dedb..00000000 --- a/func_metadata_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package dasel - -import ( - "testing" -) - -func TestMetadataFunc(t *testing.T) { - t.Run("Args", selectTestErr( - "metadata()", - map[string]interface{}{}, - &ErrUnexpectedFunctionArgs{ - Function: "metadata", - Args: []string{}, - }), - ) -} diff --git a/func_more_than.go b/func_more_than.go deleted file mode 100644 index 9f2c6a89..00000000 --- a/func_more_than.go +++ /dev/null @@ -1,89 +0,0 @@ -package dasel - -import ( - "fmt" - "reflect" - "sort" - - "github.com/tomwright/dasel/v3/util" -) - -var MoreThanFunc = BasicFunction{ - name: "moreThan", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - if err := requireExactlyXArgs("moreThan", args, 2); err != nil { - return nil, err - } - - input := s.inputs() - - type comparison struct { - selector string - value string - } - - comparisons := make([]comparison, 0) - - currentComparison := comparison{} - - for i, v := range args { - switch i % 2 { - case 0: - currentComparison.selector = v - case 1: - currentComparison.value = v - comparisons = append(comparisons, currentComparison) - currentComparison = comparison{} - } - } - - runComparison := func(value Value, cmp comparison) (bool, error) { - gotValues, err := c.subSelect(value, cmp.selector) - if err != nil { - return false, err - } - - if len(gotValues) > 1 { - return false, fmt.Errorf("equal expects selector to return a single value") - } - - if len(gotValues) == 0 { - return false, nil - } - - gotValue := util.ToString(gotValues[0].Interface()) - - // The values are equal - if gotValue == cmp.value { - return false, nil - } - - sortedVals := []string{gotValue, cmp.value} - sort.Strings(sortedVals) - - if sortedVals[0] == cmp.value { - return true, nil - } - return false, nil - } - - res := make(Values, 0) - - for _, val := range input { - valPassed := true - for _, cmp := range comparisons { - pass, err := runComparison(val, cmp) - if err != nil { - return nil, err - } - if !pass { - valPassed = false - break - } - } - res = append(res, Value{Value: reflect.ValueOf(valPassed)}) - } - - return res, nil - }, -} diff --git a/func_more_than_test.go b/func_more_than_test.go deleted file mode 100644 index 51493b5e..00000000 --- a/func_more_than_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package dasel - -import ( - "testing" -) - -func TestMoreThanFunc(t *testing.T) { - - t.Run("Args", selectTestErr( - "moreThan()", - map[string]interface{}{}, - &ErrUnexpectedFunctionArgs{ - Function: "moreThan", - Args: []string{}, - }), - ) - - t.Run( - "More Than", - selectTest( - "nums.all().moreThan(.,5)", - map[string]interface{}{ - "nums": []any{ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, - }, - }, - []interface{}{ - false, - false, - false, - false, - false, - false, - true, - true, - true, - true, - }, - ), - ) -} diff --git a/func_not.go b/func_not.go deleted file mode 100644 index 1122596a..00000000 --- a/func_not.go +++ /dev/null @@ -1,48 +0,0 @@ -package dasel - -import ( - "fmt" - "reflect" -) - -var NotFunc = BasicFunction{ - name: "not", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - if err := requireXOrMoreArgs("not", args, 1); err != nil { - return nil, err - } - - input := s.inputs() - - runComparison := func(value Value, selector string) (bool, error) { - gotValues, err := c.subSelect(value, selector) - if err != nil { - return false, err - } - - if len(gotValues) > 1 { - return false, fmt.Errorf("not expects selector to return a single value") - } - - if len(gotValues) == 0 { - return false, nil - } - - return IsTruthy(gotValues[0].Interface()), nil - } - - res := make(Values, 0) - - for _, val := range input { - for _, selector := range args { - truthy, err := runComparison(val, selector) - if err != nil { - return nil, err - } - res = append(res, Value{Value: reflect.ValueOf(!truthy)}) - } - } - - return res, nil - }, -} diff --git a/func_not_test.go b/func_not_test.go deleted file mode 100644 index 4c2621ee..00000000 --- a/func_not_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package dasel - -import ( - "testing" -) - -func TestNotFunc(t *testing.T) { - t.Run("Args", selectTestErr( - "not()", - map[string]interface{}{}, - &ErrUnexpectedFunctionArgs{ - Function: "not", - Args: []string{}, - }), - ) - - t.Run( - "Single Equal", - selectTest( - "name.all().not(equal(key(),first))", - map[string]interface{}{ - "name": map[string]interface{}{ - "first": "Tom", - "last": "Wright", - }, - }, - []interface{}{ - false, - true, - }, - ), - ) - - t.Run( - "Not Banned", - selectTest( - "all().filter(not(equal(banned,true))).name", - []map[string]interface{}{ - { - "name": "Tom", - "banned": true, - }, - { - "name": "Jess", - "banned": false, - }, - }, - []interface{}{ - "Jess", - }, - ), - ) -} diff --git a/func_null.go b/func_null.go deleted file mode 100644 index aacec2ca..00000000 --- a/func_null.go +++ /dev/null @@ -1,24 +0,0 @@ -package dasel - -import ( - "reflect" -) - -var NullFunc = BasicFunction{ - name: "null", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - if err := requireNoArgs("null", args); err != nil { - return nil, err - } - - input := s.inputs() - - res := make(Values, len(input)) - - for k, _ := range args { - res[k] = ValueOf(reflect.ValueOf(new(any)).Elem()) - } - - return res, nil - }, -} diff --git a/func_null_test.go b/func_null_test.go deleted file mode 100644 index e68a65b4..00000000 --- a/func_null_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package dasel - -import ( - "testing" -) - -func TestNullFunc(t *testing.T) { - t.Run("Args", selectTestErr( - "null(1)", - map[string]interface{}{}, - &ErrUnexpectedFunctionArgs{ - Function: "null", - Args: []string{"1"}, - }), - ) - - original := map[string]interface{}{} - - t.Run( - "Null", - selectTest( - "null()", - original, - []interface{}{ - nil, - }, - ), - ) -} diff --git a/func_or.go b/func_or.go deleted file mode 100644 index 87bc152c..00000000 --- a/func_or.go +++ /dev/null @@ -1,53 +0,0 @@ -package dasel - -import ( - "fmt" - "reflect" -) - -var OrFunc = BasicFunction{ - name: "or", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - if err := requireXOrMoreArgs("or", args, 1); err != nil { - return nil, err - } - - input := s.inputs() - - runComparison := func(value Value, selector string) (bool, error) { - gotValues, err := c.subSelect(value, selector) - if err != nil { - return false, err - } - - if len(gotValues) > 1 { - return false, fmt.Errorf("or expects selector to return a single value") - } - - if len(gotValues) == 0 { - return false, nil - } - - return IsTruthy(gotValues[0]), nil - } - - res := make(Values, 0) - - for _, val := range input { - valPassed := false - for _, cmp := range args { - pass, err := runComparison(val, cmp) - if err != nil { - return nil, err - } - if pass { - valPassed = true - break - } - } - res = append(res, Value{Value: reflect.ValueOf(valPassed)}) - } - - return res, nil - }, -} diff --git a/func_or_default.go b/func_or_default.go deleted file mode 100644 index 0b26f07b..00000000 --- a/func_or_default.go +++ /dev/null @@ -1,72 +0,0 @@ -package dasel - -import ( - "errors" - "fmt" -) - -var OrDefaultFunc = BasicFunction{ - name: "orDefault", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - if err := requireExactlyXArgs("orDefault", args, 2); err != nil { - return nil, err - } - - input := s.inputs() - - if c.CreateWhenMissing() { - input = input.initEmptydencodingMaps() - } - - runSubselect := func(value Value, selector string, defaultSelector string) (Value, error) { - gotValues, err := c.subSelect(value, selector) - notFound := false - if err != nil { - if errors.Is(err, &ErrPropertyNotFound{}) { - notFound = true - } else if errors.Is(err, &ErrIndexNotFound{Index: -1}) { - notFound = true - } else { - return Value{}, err - } - } - - if !notFound { - // Check result of first query - if len(gotValues) != 1 { - return Value{}, fmt.Errorf("orDefault expects selector to return exactly 1 value") - } - - // Consider nil values as not found - if gotValues[0].IsNil() { - notFound = true - } - } - - if notFound { - gotValues, err = c.subSelect(value, defaultSelector) - if err != nil { - return Value{}, err - } - if len(gotValues) != 1 { - return Value{}, fmt.Errorf("orDefault expects selector to return exactly 1 value") - } - } - - return gotValues[0], nil - } - - res := make(Values, 0) - - for _, val := range input { - resolvedValue, err := runSubselect(val, args[0], args[1]) - if err != nil { - return nil, err - } - - res = append(res, resolvedValue) - } - - return res, nil - }, -} diff --git a/func_or_default_test.go b/func_or_default_test.go deleted file mode 100644 index 1f8e2af8..00000000 --- a/func_or_default_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package dasel - -import ( - "testing" -) - -func TestOrDefaultFunc(t *testing.T) { - t.Run("Args", selectTestErr( - "orDefault()", - map[string]interface{}{}, - &ErrUnexpectedFunctionArgs{ - Function: "orDefault", - Args: []string{}, - }), - ) - - t.Run("OriginalAndDefaultNotFoundProperty", selectTestErr( - "orDefault(a,b)", - map[string]interface{}{"x": "y"}, - &ErrPropertyNotFound{ - Property: "b", - }), - ) - - t.Run("OriginalAndDefaultNotFoundIndex", selectTestErr( - "orDefault(x.[1],x.[2])", - map[string]interface{}{"x": []int{1}}, - &ErrIndexNotFound{ - Index: 2, - }), - ) - - original := map[string]interface{}{ - "name": map[string]interface{}{ - "first": "Tom", - "last": "Wright", - }, - "colours": []interface{}{ - "red", "green", "blue", - }, - } - - t.Run( - "FirstNameOrLastName", - selectTest( - "orDefault(name.first,name.last)", - original, - []interface{}{ - "Tom", - }, - ), - ) - - t.Run( - "MiddleNameOrDefault", - selectTest( - "orDefault(name.middle,string(default))", - original, - []interface{}{ - "default", - }, - ), - ) - - t.Run( - "FirstColourOrSecondColour", - selectTest( - "orDefault(colours.[0],colours.[2])", - original, - []interface{}{ - "red", - }, - ), - ) - - t.Run( - "FourthColourOrDefault", - selectTest( - "orDefault(colours.[3],string(default))", - original, - []interface{}{ - "default", - }, - ), - ) -} diff --git a/func_or_test.go b/func_or_test.go deleted file mode 100644 index e87c5e18..00000000 --- a/func_or_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package dasel - -import ( - "testing" -) - -func TestOrFunc(t *testing.T) { - t.Run("Args", selectTestErr( - "or()", - map[string]interface{}{}, - &ErrUnexpectedFunctionArgs{ - Function: "or", - Args: []string{}, - }), - ) - - t.Run( - "NoneEqualMoreThan", - selectTest( - "numbers.all().or(equal(.,2),moreThan(.,2))", - map[string]interface{}{ - "numbers": []interface{}{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, - }, - []interface{}{ - false, false, true, true, true, true, true, true, true, true, - }, - ), - ) - t.Run( - "SomeEqualMoreThan", - selectTest( - "numbers.all().or(equal(.,0),moreThan(.,2))", - map[string]interface{}{ - "numbers": []interface{}{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, - }, - []interface{}{ - true, false, false, true, true, true, true, true, true, true, - }, - ), - ) -} diff --git a/func_parent.go b/func_parent.go deleted file mode 100644 index 9b553e39..00000000 --- a/func_parent.go +++ /dev/null @@ -1,54 +0,0 @@ -package dasel - -import ( - "strconv" -) - -var ParentFunc = BasicFunction{ - name: "parent", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - if err := requireXOrLessArgs("parent", args, 1); err != nil { - return nil, err - } - - levels := 1 - if len(args) > 0 { - arg, err := strconv.Atoi(args[0]) - if err != nil { - return nil, err - } - levels = arg - } - if levels < 1 { - levels = 1 - } - - input := s.inputs() - - res := make(Values, 0) - - getParent := func(v Value, levels int) (Value, bool) { - res := v - for i := 0; i < levels; i++ { - p := res.Metadata("parent") - if p == nil { - return res, false - } - if pv, ok := p.(Value); ok { - res = pv - } else { - return res, false - } - } - return res, true - } - - for _, i := range input { - if pv, ok := getParent(i, levels); ok { - res = append(res, pv) - } - } - - return res, nil - }, -} diff --git a/func_parent_test.go b/func_parent_test.go deleted file mode 100644 index d33f96ee..00000000 --- a/func_parent_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package dasel - -import ( - "testing" -) - -func TestParentFunc(t *testing.T) { - t.Run("Args", selectTestErr( - "parent(x,x)", - map[string]interface{}{}, - &ErrUnexpectedFunctionArgs{ - Function: "parent", - Args: []string{"x", "x"}, - }), - ) - - t.Run( - "SimpleParent", - selectTest( - "name.first.parent()", - map[string]interface{}{ - "name": map[string]interface{}{ - "first": "Tom", - "last": "Wright", - }, - }, - []interface{}{ - map[string]interface{}{ - "first": "Tom", - "last": "Wright", - }, - }, - ), - ) - - t.Run( - "SimpleParent2Levels", - selectTest( - "user.name.first.parent(2).deleted", - map[string]interface{}{ - "user": map[string]interface{}{ - "name": map[string]interface{}{ - "first": "Tom", - "last": "Wright", - }, - "deleted": false, - }, - }, - []interface{}{ - false, - }, - ), - ) - - t.Run( - "MultiParent", - selectTest( - "name.all().parent()", - map[string]interface{}{ - "name": map[string]interface{}{ - "first": "Tom", - "last": "Wright", - }, - }, - []interface{}{ - map[string]interface{}{ - "first": "Tom", - "last": "Wright", - }, - map[string]interface{}{ - "first": "Tom", - "last": "Wright", - }, - }, - ), - ) - - t.Run( - "FilteredParent", - selectTest( - "all().flags.filter(equal(banned,false)).parent().name", - []map[string]interface{}{ - { - "flags": map[string]interface{}{ - "banned": false, - }, - "name": "Tom", - }, - { - "flags": map[string]interface{}{ - "banned": true, - }, - "name": "Jim", - }, - }, - []interface{}{ - "Tom", - }, - ), - ) -} diff --git a/func_property.go b/func_property.go deleted file mode 100644 index 9d9be877..00000000 --- a/func_property.go +++ /dev/null @@ -1,94 +0,0 @@ -package dasel - -import ( - "fmt" - "reflect" - "strings" -) - -type ErrPropertyNotFound struct { - Property string -} - -func (e ErrPropertyNotFound) Error() string { - return fmt.Sprintf("property not found: %s", e.Property) -} - -func (e ErrPropertyNotFound) Is(other error) bool { - o, ok := other.(*ErrPropertyNotFound) - if !ok { - return false - } - if o.Property != "" && o.Property != e.Property { - return false - } - return true -} - -var PropertyFunc = BasicFunction{ - name: "property", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - if err := requireXOrMoreArgs("property", args, 1); err != nil { - return nil, err - } - - input := s.inputs() - - if c.CreateWhenMissing() { - input = input.initEmptydencodingMaps() - } - - res := make(Values, 0) - - for _, val := range input { - for _, property := range args { - isOptional := strings.HasSuffix(property, "?") - if isOptional { - property = strings.TrimSuffix(property, "?") - } - - switch val.Kind() { - case reflect.Map: - index := val.MapIndex(ValueOf(property)) - if index.IsEmpty() { - if isOptional { - continue - } - if !c.CreateWhenMissing() { - return nil, fmt.Errorf("could not access map index: %w", &ErrPropertyNotFound{Property: property}) - } - index = index.asUninitialised() - } - res = append(res, index) - case reflect.Struct: - value := val.FieldByName(property) - if value.IsEmpty() { - if isOptional { - continue - } - return nil, fmt.Errorf("could not access struct field: %w", &ErrPropertyNotFound{Property: property}) - } - res = append(res, value) - default: - if val.IsDencodingMap() { - index := val.dencodingMapIndex(ValueOf(property)) - if index.IsEmpty() { - if isOptional { - continue - } - if !c.CreateWhenMissing() { - return nil, fmt.Errorf("could not access map index: %w", &ErrPropertyNotFound{Property: property}) - } - index = index.asUninitialised() - } - res = append(res, index) - } else { - return nil, fmt.Errorf("cannot use property selector on non map/struct types: %s: %w", val.Kind().String(), &ErrPropertyNotFound{Property: property}) - } - } - } - } - - return res, nil - }, -} diff --git a/func_property_test.go b/func_property_test.go deleted file mode 100644 index 5fe6143f..00000000 --- a/func_property_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package dasel - -import ( - "testing" -) - -func TestPropertyFunc(t *testing.T) { - t.Run("Args", selectTestErr( - "property()", - map[string]interface{}{}, - &ErrUnexpectedFunctionArgs{ - Function: "property", - Args: []string{}, - }), - ) - - t.Run("NotFound", selectTestErr( - "asd", - map[string]interface{}{"x": "y"}, - &ErrPropertyNotFound{ - Property: "asd", - }), - ) - - t.Run("NotFoundOnString", selectTestErr( - "x.asd", - map[string]interface{}{"x": "y"}, - &ErrPropertyNotFound{ - Property: "asd", - }), - ) - - original := map[string]interface{}{ - "name": map[string]interface{}{ - "first": "Tom", - "last": "Wright", - }, - "colours": []interface{}{ - "red", "green", "blue", - }, - } - - t.Run( - "SingleLevelProperty", - selectTest( - "name", - original, - []interface{}{ - map[string]interface{}{ - "first": "Tom", - "last": "Wright", - }, - }, - ), - ) - - t.Run( - "SingleLevelPropertyFunc", - selectTest( - "property(name)", - original, - []interface{}{ - map[string]interface{}{ - "first": "Tom", - "last": "Wright", - }, - }, - ), - ) - - t.Run( - "NestedPropertyFunc", - selectTest( - "property(name).property(first)", - original, - []interface{}{ - "Tom", - }, - ), - ) - - t.Run( - "NestedMultiPropertyFunc", - selectTest( - "property(name).property(first,last)", - original, - []interface{}{ - "Tom", - "Wright", - }, - ), - ) - - t.Run( - "NestedMultiMissingPropertyFunc", - selectTest( - "property(name).property(first,last,middle?)", - original, - []interface{}{ - "Tom", - "Wright", - }, - ), - ) -} diff --git a/func_string.go b/func_string.go deleted file mode 100644 index f64f78f7..00000000 --- a/func_string.go +++ /dev/null @@ -1,22 +0,0 @@ -package dasel - -import "github.com/tomwright/dasel/v3/util" - -var StringFunc = BasicFunction{ - name: "string", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - if err := requireExactlyXArgs("string", args, 1); err != nil { - return nil, err - } - - input := s.inputs() - - res := make(Values, len(input)) - - for k, v := range args { - res[k] = ValueOf(util.ToString(v)) - } - - return res, nil - }, -} diff --git a/func_string_test.go b/func_string_test.go deleted file mode 100644 index f7bc43c5..00000000 --- a/func_string_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package dasel - -import ( - "testing" -) - -func TestStringFunc(t *testing.T) { - t.Run("Args", selectTestErr( - "string()", - map[string]interface{}{}, - &ErrUnexpectedFunctionArgs{ - Function: "string", - Args: []string{}, - }), - ) - - original := map[string]interface{}{} - - t.Run( - "String", - selectTest( - "string(x)", - original, - []interface{}{ - "x", - }, - ), - ) - - t.Run( - "Comma", - selectTest( - "string(\\,)", - original, - []interface{}{ - ",", - }, - ), - ) -} diff --git a/func_this.go b/func_this.go deleted file mode 100644 index 13a6d7f7..00000000 --- a/func_this.go +++ /dev/null @@ -1,11 +0,0 @@ -package dasel - -var ThisFunc = BasicFunction{ - name: "this", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - if err := requireNoArgs("this", args); err != nil { - return nil, err - } - return s.inputs(), nil - }, -} diff --git a/func_this_test.go b/func_this_test.go deleted file mode 100644 index d1aacb92..00000000 --- a/func_this_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package dasel - -import ( - "testing" -) - -func TestThisFunc(t *testing.T) { - t.Run("Args", selectTestErr( - "this(x)", - map[string]interface{}{}, - &ErrUnexpectedFunctionArgs{ - Function: "this", - Args: []string{"x"}, - }), - ) - t.Run( - "SimpleThis", - selectTest( - "name.this().first", - map[string]interface{}{ - "name": map[string]interface{}{ - "first": "Tom", - "last": "Wright", - }, - }, - []interface{}{ - "Tom", - }, - ), - ) - t.Run( - "BlankSelectorThis", - selectTest( - ".name.first", - map[string]interface{}{ - "name": map[string]interface{}{ - "first": "Tom", - "last": "Wright", - }, - }, - []interface{}{ - "Tom", - }, - ), - ) -} diff --git a/func_type.go b/func_type.go deleted file mode 100644 index 07c0f01f..00000000 --- a/func_type.go +++ /dev/null @@ -1,44 +0,0 @@ -package dasel - -import "reflect" - -var TypeFunc = BasicFunction{ - name: "type", - runFn: func(c *Context, s *Step, args []string) (Values, error) { - if err := requireNoArgs("type", args); err != nil { - return nil, err - } - - input := s.inputs() - - res := make(Values, 0) - - for _, val := range input { - resStr := "unknown" - - if val.IsNil() { - resStr = "null" - } else if val.IsDencodingMap() { - resStr = "object" - } else { - switch val.Kind() { - case reflect.Slice, reflect.Array: - resStr = "array" - case reflect.Map, reflect.Struct: - resStr = "object" - case reflect.String: - resStr = "string" - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, - reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, - reflect.Float32, reflect.Float64: - resStr = "number" - case reflect.Bool: - resStr = "bool" - } - } - res = append(res, ValueOf(resStr)) - } - - return res, nil - }, -} diff --git a/func_type_test.go b/func_type_test.go deleted file mode 100644 index a042d92b..00000000 --- a/func_type_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package dasel - -import ( - "testing" -) - -func TestTypeFunc(t *testing.T) { - t.Run("Args", selectTestErr( - "type(x)", - map[string]interface{}{}, - &ErrUnexpectedFunctionArgs{ - Function: "type", - Args: []string{"x"}, - }), - ) - - data := map[string]interface{}{ - "string": "hello", - "slice": []interface{}{ - 1, 2, 3, - }, - "map": map[string]interface{}{ - "x": 1, - }, - "int": int(1), - "float": float32(1), - "bool": true, - "null": nil, - } - - t.Run( - "String", - selectTest( - "string.type()", - data, - []interface{}{ - "string", - }, - ), - ) - t.Run( - "Slice", - selectTest( - "slice.type()", - data, - []interface{}{ - "array", - }, - ), - ) - t.Run( - "map", - selectTest( - "map.type()", - data, - []interface{}{ - "object", - }, - ), - ) - t.Run( - "int", - selectTest( - "int.type()", - data, - []interface{}{ - "number", - }, - ), - ) - t.Run( - "float", - selectTest( - "float.type()", - data, - []interface{}{ - "number", - }, - ), - ) - t.Run( - "bool", - selectTest( - "bool.type()", - data, - []interface{}{ - "bool", - }, - ), - ) - t.Run( - "null", - selectTest( - "null.type()", - data, - []interface{}{ - "null", - }, - ), - ) -} diff --git a/internal/command/delete.go b/internal/command/delete.go deleted file mode 100644 index 7a7df85b..00000000 --- a/internal/command/delete.go +++ /dev/null @@ -1,115 +0,0 @@ -package command - -import ( - "github.com/spf13/cobra" - "github.com/tomwright/dasel/v3" -) - -func deleteCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "delete -f -r ", - Short: "Delete properties from the given file.", - RunE: deleteRunE, - Args: cobra.MaximumNArgs(1), - } - - deleteFlags(cmd) - - return cmd -} - -func deleteFlags(cmd *cobra.Command) { - cmd.Flags().StringP("selector", "s", "", "The selector to use when querying the data structure.") - cmd.Flags().StringP("read", "r", "", "The parser to use when reading.") - cmd.Flags().StringP("file", "f", "", "The file to query.") - cmd.Flags().StringP("write", "w", "", "The parser to use when writing. Defaults to the read parser if not provided.") - cmd.Flags().StringP("out", "o", "", "The file to write output to.") - cmd.Flags().Bool("pretty", true, "Pretty print the output.") - cmd.Flags().Bool("colour", false, "Print colourised output.") - cmd.Flags().Bool("escape-html", false, "Escape HTML tags when writing output.") - cmd.Flags().Int("indent", 2, "The indention level when writing files.") - cmd.Flags().String("csv-comma", ",", "Comma separator to use when working with csv files.") - cmd.Flags().String("csv-write-comma", "", "Comma separator used when writing csv files. Overrides csv-comma when writing.") - cmd.Flags().String("csv-comment", "", "Comma separator used when reading csv files.") - cmd.Flags().Bool("csv-crlf", false, "Then to use CRLF when writing CSV files.") - - _ = cmd.MarkFlagFilename("file") -} - -func deleteRunE(cmd *cobra.Command, args []string) error { - selectorFlag, _ := cmd.Flags().GetString("selector") - readParserFlag, _ := cmd.Flags().GetString("read") - fileFlag, _ := cmd.Flags().GetString("file") - writeParserFlag, _ := cmd.Flags().GetString("write") - prettyPrintFlag, _ := cmd.Flags().GetBool("pretty") - colourFlag, _ := cmd.Flags().GetBool("colour") - escapeHTMLFlag, _ := cmd.Flags().GetBool("escape-html") - outFlag, _ := cmd.Flags().GetString("out") - indent, _ := cmd.Flags().GetInt("indent") - csvComma, _ := cmd.Flags().GetString("csv-comma") - csvWriteComma, _ := cmd.Flags().GetString("csv-write-comma") - csvComment, _ := cmd.Flags().GetString("csv-comment") - csvCRLF, _ := cmd.Flags().GetBool("csv-crlf") - - opts := &deleteOptions{ - Read: &readOptions{ - Reader: nil, - Parser: readParserFlag, - FilePath: fileFlag, - CsvComma: csvComma, - CsvComment: csvComment, - }, - Write: &writeOptions{ - Writer: nil, - Parser: writeParserFlag, - FilePath: outFlag, - PrettyPrint: prettyPrintFlag, - Colourise: colourFlag, - EscapeHTML: escapeHTMLFlag, - Indent: indent, - CsvComma: csvWriteComma, - CsvUseCRLF: csvCRLF, - }, - Selector: selectorFlag, - } - - if opts.Selector == "" && len(args) > 0 { - opts.Selector = args[0] - args = args[1:] - } - - if opts.Selector == "" { - opts.Selector = "." - } - - if opts.Write.FilePath == "" { - opts.Write.FilePath = opts.Read.FilePath - } - - return runDeleteCommand(opts, cmd) -} - -type deleteOptions struct { - Read *readOptions - Write *writeOptions - Selector string -} - -func runDeleteCommand(opts *deleteOptions, cmd *cobra.Command) error { - - rootValue, err := opts.Read.rootValue(cmd) - if err != nil { - return err - } - - value, err := dasel.Delete(rootValue, opts.Selector) - if err != nil { - return err - } - - if err := opts.Write.writeValue(cmd, opts.Read, value); err != nil { - return err - } - - return nil -} diff --git a/internal/command/delete_test.go b/internal/command/delete_test.go deleted file mode 100644 index 9399d008..00000000 --- a/internal/command/delete_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package command - -import ( - "testing" -) - -func TestDeleteCommand(t *testing.T) { - - t.Run("DeleteMapField", runTest( - []string{"delete", "-r", "json", "--pretty=false", "x"}, - []byte(`{"x":1,"y":2}`), - newline([]byte(`{"y":2}`)), - nil, - nil, - )) - - t.Run("DeleteNestedMapField", runTest( - []string{"delete", "-r", "json", "--pretty=false", "x.y"}, - []byte(`{"x":{"x":1,"y":2},"y":{"x":1,"y":2}}`), - newline([]byte(`{"x":{"x":1},"y":{"x":1,"y":2}}`)), - nil, - nil, - )) - - t.Run("DeleteSliceIndex", runTest( - []string{"delete", "-r", "json", "--pretty=false", "[1]"}, - []byte(`[0,1,2]`), - newline([]byte(`[0,2]`)), - nil, - nil, - )) - - t.Run("DeletedNestedSliceIndex", runTest( - []string{"delete", "-r", "json", "--pretty=false", "users.[1]"}, - []byte(`{"users":[0,1,2]}`), - newline([]byte(`{"users":[0,2]}`)), - nil, - nil, - )) - - t.Run("CheckIndentionForJSON", runTest( - []string{"delete", "-r", "json", "--indent", "6", "--pretty=true", "x.y"}, - []byte(`{"x":{"x":1,"y":2}}`), - newline([]byte("{\n \"x\": {\n \"x\": 1\n }\n}")), - nil, - nil, - )) - - t.Run("CheckIndentionForYAML", runTest( - []string{"delete", "-r", "json", "-w", "yaml", "--indent", "6", "--pretty=true", "x.y"}, - []byte(`{"x":{"x":1,"y":2}}`), - newline([]byte("x:\n x: 1")), - nil, - nil, - )) - - t.Run("CheckIndentionForTOML", runTest( - []string{"delete", "-r", "json", "-w", "toml", "--indent", "6", "--pretty=true", "x.y"}, - []byte(`{"x":{"x":1,"y":2}}`), - newline([]byte("[x]\n x = 1")), - nil, - nil, - )) - - t.Run("Issue346", func(t *testing.T) { - t.Run("DeleteNullValue", runTest( - []string{"delete", "-r", "json", "foo"}, - []byte(`{"foo":null}`), - newline([]byte("{}")), - nil, - nil, - )) - }) -} diff --git a/internal/command/man.go b/internal/command/man.go deleted file mode 100644 index 52720329..00000000 --- a/internal/command/man.go +++ /dev/null @@ -1,30 +0,0 @@ -package command - -import ( - "github.com/spf13/cobra" - "github.com/spf13/cobra/doc" -) - -func manCommand(root *cobra.Command) *cobra.Command { - // Do not include timestamp in generated man pages. - // See https://github.com/spf13/cobra/issues/142 - root.DisableAutoGenTag = true - - cmd := &cobra.Command{ - Use: "man -o ", - Short: "Generate manual pages for all dasel subcommands", - RunE: func(cmd *cobra.Command, args []string) error { - return manRunE(cmd, root) - }, - } - - cmd.Flags().StringP("output-directory", "o", ".", "The directory in which man pages will be created") - - return cmd -} - -func manRunE(cmd *cobra.Command, root *cobra.Command) error { - outputDirectory, _ := cmd.Flags().GetString("output-directory") - - return doc.GenManTree(root, nil, outputDirectory) -} diff --git a/internal/command/man_test.go b/internal/command/man_test.go deleted file mode 100644 index 9588bf13..00000000 --- a/internal/command/man_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package command - -import ( - "os" - "testing" -) - -func TestManCommand(t *testing.T) { - tempDir := t.TempDir() - - _, _, err := runDasel([]string{"man", "-o", tempDir}, nil) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - - files, err := os.ReadDir(tempDir) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - - expectedFiles := []string{ - "dasel-completion-bash.1", - "dasel-completion-fish.1", - "dasel-completion-powershell.1", - "dasel-completion-zsh.1", - "dasel-completion.1", - "dasel-delete.1", - "dasel-man.1", - "dasel-put.1", - "dasel-validate.1", - "dasel.1", - } - - if len(files) != len(expectedFiles) { - t.Fatalf("expected %d files, got %d", len(expectedFiles), len(files)) - } - - for i, f := range files { - if f.Name() != expectedFiles[i] { - t.Fatalf("expected %v, got %v", expectedFiles[i], f.Name()) - } - } -} diff --git a/internal/command/options.go b/internal/command/options.go deleted file mode 100644 index 93988236..00000000 --- a/internal/command/options.go +++ /dev/null @@ -1,204 +0,0 @@ -package command - -import ( - "fmt" - "io" - "os" - "strings" - - "github.com/spf13/cobra" - "github.com/tomwright/dasel/v3" - "github.com/tomwright/dasel/v3/dencoding" - "github.com/tomwright/dasel/v3/storage" -) - -type readOptions struct { - // Reader is an io.Reader that we should read from instead of FilePath. - Reader io.Reader - // Parser is the name of the parser we should use when reading. - Parser string - // FilePath is the path to the source file. - FilePath string - // CsvComma is the comma character used when reading CSV files. - CsvComma string - // CsvComment is the comment character used when reading CSV files. - CsvComment string -} - -func (o *readOptions) readFromStdin() bool { - return o.FilePath == "" || o.FilePath == "stdin" || o.FilePath == "-" -} - -func (o *readOptions) readParser() (storage.ReadParser, error) { - useStdin := o.readFromStdin() - - if useStdin && o.Parser == "" { - return nil, fmt.Errorf("read parser required when reading from stdin") - } - - if o.Parser == "" { - parser, err := storage.NewReadParserFromFilename(o.FilePath) - if err != nil { - return nil, fmt.Errorf("could not get read parser from filename: %w", err) - } - return parser, nil - } - parser, err := storage.NewReadParserFromString(o.Parser) - if err != nil { - return nil, fmt.Errorf("could not get read parser: %w", err) - } - return parser, nil -} - -func (o *readOptions) rootValue(cmd *cobra.Command) (dasel.Value, error) { - parser, err := o.readParser() - if err != nil { - return dasel.Value{}, fmt.Errorf("could not get read parser: %w", err) - } - - options := make([]storage.ReadWriteOption, 0) - if o.CsvComma != "" { - options = append(options, storage.CsvCommaOption([]rune(o.CsvComma)[0])) - } - if o.CsvComment != "" { - options = append(options, storage.CsvCommentOption([]rune(o.CsvComment)[0])) - } - - reader := o.Reader - if reader == nil { - if o.readFromStdin() { - reader = cmd.InOrStdin() - } else { - f, err := os.Open(o.FilePath) - if err != nil { - return dasel.Value{}, fmt.Errorf("could not open file for reading: %s: %w", o.FilePath, err) - } - defer f.Close() - reader = f - } - } - - rootNode, err := storage.Load(parser, reader, options...) - if err != nil { - return rootNode, err - } - - if !rootNode.Value.IsValid() { - var defaultValue any = dencoding.NewMap() - if _, ok := parser.(*storage.CSVParser); ok { - defaultValue = []any{} - } - rootNode = dasel.ValueOf(defaultValue) - } - - return rootNode, nil -} - -type writeOptions struct { - // Writer is an io.Writer that we should write to instead of FilePath. - Writer io.Writer - // Parser is the name of the parser we should use when reading. - Parser string - // FilePath is the path to the source file. - FilePath string - - PrettyPrint bool - Colourise bool - EscapeHTML bool - - // CsvComma is the comma character used when writing CSV files. - CsvComma string - // CsvUseCRLF determines whether CRLF is used when writing CSV files. - CsvUseCRLF bool - - Indent int -} - -func (o *writeOptions) writeToStdout() bool { - return o.FilePath == "" || o.FilePath == "stdout" || o.FilePath == "-" -} - -func (o *writeOptions) writeParser(readOptions *readOptions) (storage.WriteParser, error) { - if o.writeToStdout() && o.Parser == "" { - if readOptions != nil { - o.Parser = readOptions.Parser - } - } - - if o.writeToStdout() && o.Parser == "" && readOptions != nil && readOptions.FilePath != "" { - parser, err := storage.NewWriteParserFromFilename(readOptions.FilePath) - if err != nil { - return nil, fmt.Errorf("could not get write parser from read filename: %w", err) - } - return parser, nil - } - if o.Parser == "" { - parser, err := storage.NewWriteParserFromFilename(o.FilePath) - if err != nil { - return nil, fmt.Errorf("could not get write parser from filename: %w", err) - } - return parser, nil - } - parser, err := storage.NewWriteParserFromString(o.Parser) - if err != nil { - return nil, fmt.Errorf("could not get write parser: %w", err) - } - return parser, nil -} - -func (o *writeOptions) writeValues(cmd *cobra.Command, readOptions *readOptions, values dasel.Values) error { - parser, err := o.writeParser(readOptions) - if err != nil { - return err - } - - options := []storage.ReadWriteOption{ - storage.ColouriseOption(o.Colourise), - storage.EscapeHTMLOption(o.EscapeHTML), - storage.PrettyPrintOption(o.PrettyPrint), - storage.CsvUseCRLFOption(o.CsvUseCRLF), - } - - if o.CsvComma == "" && readOptions.CsvComma != "" { - o.CsvComma = readOptions.CsvComma - } - - if o.CsvComma != "" { - options = append(options, storage.CsvCommaOption([]rune(o.CsvComma)[0])) - } - - if o.Indent != 0 { - options = append(options, storage.IndentOption(strings.Repeat(" ", o.Indent))) - } - - writer := o.Writer - if writer == nil { - if o.writeToStdout() { - writer = cmd.OutOrStdout() - } else { - f, err := os.Create(o.FilePath) - if err != nil { - return fmt.Errorf("could not open file for writing: %s: %w", o.FilePath, err) - } - defer f.Close() - writer = f - } - } - - for _, value := range values { - valueBytes, err := parser.ToBytes(value, options...) - if err != nil { - return err - } - - if _, err := writer.Write(valueBytes); err != nil { - return err - } - } - - return nil -} - -func (o *writeOptions) writeValue(cmd *cobra.Command, readOptions *readOptions, value dasel.Value) error { - return o.writeValues(cmd, readOptions, dasel.Values{value}) -} diff --git a/internal/command/put.go b/internal/command/put.go deleted file mode 100644 index e9ad7fcb..00000000 --- a/internal/command/put.go +++ /dev/null @@ -1,158 +0,0 @@ -package command - -import ( - "fmt" - "strconv" - - "github.com/spf13/cobra" - "github.com/tomwright/dasel/v3" - "github.com/tomwright/dasel/v3/storage" -) - -func putCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "put -t -v -f -r ", - Short: "Write properties to the given file.", - RunE: putRunE, - Args: cobra.MaximumNArgs(1), - } - - putFlags(cmd) - - return cmd -} - -func putFlags(cmd *cobra.Command) { - cmd.Flags().StringP("selector", "s", "", "The selector to use when querying the data structure.") - cmd.Flags().StringP("read", "r", "", "The parser to use when reading.") - cmd.Flags().StringP("file", "f", "", "The file to query.") - cmd.Flags().StringP("write", "w", "", "The parser to use when writing. Defaults to the read parser if not provided.") - cmd.Flags().StringP("type", "t", "string", "The type of variable being written.") - cmd.Flags().StringP("value", "v", "", "The value to write.") - cmd.Flags().StringP("out", "o", "", "The file to write output to.") - cmd.Flags().Bool("pretty", true, "Pretty print the output.") - cmd.Flags().Bool("colour", false, "Print colourised output.") - cmd.Flags().Bool("escape-html", false, "Escape HTML tags when writing output.") - cmd.Flags().Int("indent", 2, "The indention level when writing files.") - cmd.Flags().String("csv-comma", ",", "Comma separator to use when working with csv files.") - cmd.Flags().String("csv-write-comma", "", "Comma separator used when writing csv files. Overrides csv-comma when writing.") - cmd.Flags().String("csv-comment", "", "Comma separator used when reading csv files.") - cmd.Flags().Bool("csv-crlf", false, "Then to use CRLF when writing CSV files.") - - _ = cmd.MarkFlagFilename("file") -} - -func putRunE(cmd *cobra.Command, args []string) error { - selectorFlag, _ := cmd.Flags().GetString("selector") - readParserFlag, _ := cmd.Flags().GetString("read") - fileFlag, _ := cmd.Flags().GetString("file") - writeParserFlag, _ := cmd.Flags().GetString("write") - typeFlag, _ := cmd.Flags().GetString("type") - valueFlag, _ := cmd.Flags().GetString("value") - prettyPrintFlag, _ := cmd.Flags().GetBool("pretty") - colourFlag, _ := cmd.Flags().GetBool("colour") - escapeHTMLFlag, _ := cmd.Flags().GetBool("escape-html") - outFlag, _ := cmd.Flags().GetString("out") - indent, _ := cmd.Flags().GetInt("indent") - csvComma, _ := cmd.Flags().GetString("csv-comma") - csvWriteComma, _ := cmd.Flags().GetString("csv-write-comma") - csvComment, _ := cmd.Flags().GetString("csv-comment") - csvCRLF, _ := cmd.Flags().GetBool("csv-crlf") - - opts := &putOptions{ - Read: &readOptions{ - Reader: nil, - Parser: readParserFlag, - FilePath: fileFlag, - CsvComma: csvComma, - CsvComment: csvComment, - }, - Write: &writeOptions{ - Writer: nil, - Parser: writeParserFlag, - FilePath: outFlag, - PrettyPrint: prettyPrintFlag, - Colourise: colourFlag, - EscapeHTML: escapeHTMLFlag, - Indent: indent, - CsvComma: csvWriteComma, - CsvUseCRLF: csvCRLF, - }, - Selector: selectorFlag, - ValueType: typeFlag, - Value: valueFlag, - } - - if opts.Selector == "" && len(args) > 0 { - opts.Selector = args[0] - args = args[1:] - } - - if opts.Selector == "" { - opts.Selector = "." - } - - if opts.Write.FilePath == "" { - opts.Write.FilePath = opts.Read.FilePath - } - - return runPutCommand(opts, cmd) -} - -type putOptions struct { - Read *readOptions - Write *writeOptions - Selector string - ValueType string - Value string -} - -func runPutCommand(opts *putOptions, cmd *cobra.Command) error { - - rootValue, err := opts.Read.rootValue(cmd) - if err != nil { - return err - } - - var toSet interface{} - - switch opts.ValueType { - case "string": - toSet = opts.Value - case "int": - intValue, err := strconv.ParseInt(opts.Value, 0, 64) - if err != nil { - return fmt.Errorf("invalid int value: %w", err) - } - toSet = intValue - case "float": - floatValue, err := strconv.ParseFloat(opts.Value, 64) - if err != nil { - return fmt.Errorf("invalid float value: %w", err) - } - toSet = floatValue - case "bool": - toSet = dasel.ValueOf(dasel.IsTruthy(opts.Value)) - default: - readParser, err := storage.NewReadParserFromString(opts.ValueType) - if err != nil { - return fmt.Errorf("unhandled value type") - } - docValue, err := readParser.FromBytes([]byte(opts.Value)) - if err != nil { - return fmt.Errorf("could not parse document: %w", err) - } - toSet = docValue - } - - value, err := dasel.Put(rootValue, opts.Selector, toSet) - if err != nil { - return err - } - - if err := opts.Write.writeValue(cmd, opts.Read, value); err != nil { - return err - } - - return nil -} diff --git a/internal/command/put_test.go b/internal/command/put_test.go deleted file mode 100644 index 13545381..00000000 --- a/internal/command/put_test.go +++ /dev/null @@ -1,227 +0,0 @@ -package command - -import ( - "fmt" - "testing" -) - -func TestPutCommand(t *testing.T) { - - t.Run("SetTypeOnExistingProperty", func(t *testing.T) { - tests := []struct { - name string - t string - value string - exp string - }{ - { - t: "string", - value: "some string", - exp: `"some string"`, - }, - { - t: "int", - value: "123", - exp: `123`, - }, - { - name: "float round number", - t: "float", - value: "123", - exp: `123`, - }, - { - name: "float 1 decimal place", - t: "float", - value: "123.4", - exp: `123.4`, - }, - { - name: "float 5 decimal place", - t: "float", - value: "123.45678", - exp: `123.45678`, - }, - { - name: "true bool", - t: "bool", - value: "true", - exp: `true`, - }, - { - name: "false bool", - t: "bool", - value: "false", - exp: `false`, - }, - { - t: "json", - value: `{"some":"json"}`, - exp: `{"some":"json"}`, - }, - } - - for _, test := range tests { - tc := test - if tc.name == "" { - tc.name = tc.t - } - t.Run(tc.name, runTest( - []string{"put", "-r", "json", "-t", tc.t, "--pretty=false", "-v", tc.value, "val"}, - []byte(`{"val":"oldVal"}`), - newline([]byte(fmt.Sprintf(`{"val":%s}`, tc.exp))), - nil, - nil, - )) - } - }) - - t.Run("SetStringOnExistingNestedProperty", runTest( - []string{"put", "-r", "json", "-t", "string", "--pretty=false", "-v", "Tom", "user.name"}, - []byte(`{"user":{"name":"oldName"}}`), - newline([]byte(`{"user":{"name":"Tom"}}`)), - nil, - nil, - )) - - t.Run("CreateStringProperty", runTest( - []string{"put", "-r", "json", "-t", "string", "--pretty=false", "-v", "Tom", "name"}, - []byte(`{}`), - newline([]byte(`{"name":"Tom"}`)), - nil, - nil, - )) - - t.Run("CreateNestedStringPropertyOnExistingParent", runTest( - []string{"put", "-r", "json", "-t", "string", "--pretty=false", "-v", "Tom", "user.name"}, - []byte(`{"user":{}}`), - newline([]byte(`{"user":{"name":"Tom"}}`)), - nil, - nil, - )) - - t.Run("CreateNestedStringPropertyOnMissingParent", runTest( - []string{"put", "-r", "json", "-t", "string", "--pretty=false", "-v", "Tom", "user.name"}, - []byte(`{}`), - newline([]byte(`{"user":{"name":"Tom"}}`)), - nil, - nil, - )) - - t.Run("SetStringOnExistingIndex", runTest( - []string{"put", "-r", "json", "-t", "string", "--pretty=false", "-v", "z", "[1]"}, - []byte(`["a","b","c"]`), - newline([]byte(`["a","z","c"]`)), - nil, - nil, - )) - - t.Run("SetStringOnExistingNestedIndex", runTest( - []string{"put", "-r", "json", "-t", "string", "--pretty=false", "-v", "z", "[0].[1]"}, - []byte(`[["a","b","c"],["d","e","f"]]`), - newline([]byte(`[["a","z","c"],["d","e","f"]]`)), - nil, - nil, - )) - - t.Run("AppendStringIndexToRoot", runTest( - []string{"put", "-r", "json", "-t", "string", "--pretty=false", "-v", "z", "[]"}, - []byte(`[]`), - newline([]byte(`["z"]`)), - nil, - nil, - )) - - t.Run("AppendStringIndexToNestedSlice", runTest( - []string{"put", "-r", "json", "-t", "string", "--pretty=false", "-v", "z", "[0].[]"}, - []byte(`[[]]`), - newline([]byte(`[["z"]]`)), - nil, - nil, - )) - - t.Run("AppendToChainOfMissingSlicesAndProperties", runTest( - []string{"put", "-r", "json", "-t", "string", "--pretty=false", "-v", "Tom", "users.[].name.first"}, - []byte(`{}`), - newline([]byte(`{"users":[{"name":{"first":"Tom"}}]}`)), - nil, - nil, - )) - - t.Run("AppendToEmptyExistingSlice", runTest( - []string{"put", "-r", "json", "-t", "string", "--pretty=false", "-v", "Tom", "users.[]"}, - []byte(`{"users":[]}`), - newline([]byte(`{"users":["Tom"]}`)), - nil, - nil, - )) - - t.Run("AppendToEmptyMissingSlice", runTest( - []string{"put", "-r", "json", "-t", "string", "--pretty=false", "-v", "Tom", "users.[]"}, - []byte(`{}`), - newline([]byte(`{"users":["Tom"]}`)), - nil, - nil, - )) - - // https://github.com/TomWright/dasel/issues/327 - t.Run("Yaml0xStringQuoted", runTest( - []string{"put", "-r", "yaml", "-t", "string", "--pretty=false", "-v", "0x12_11", "t"}, - []byte(`t:`), - newline([]byte(`t: "0x12_11"`)), - nil, - nil, - )) - - t.Run("YamlBoolLikeStringTrue", runTest( - []string{"put", "-r", "yaml", "-t", "string", "--pretty=false", "-v", "true", "t"}, - []byte(`t:`), - newline([]byte(`t: "true"`)), - nil, - nil, - )) - - t.Run("YamlBoolLikeStringFalse", runTest( - []string{"put", "-r", "yaml", "-t", "string", "--pretty=false", "-v", "false", "t"}, - []byte(`t:`), - newline([]byte(`t: "false"`)), - nil, - nil, - )) - - t.Run("CsvChangeSeparator", runTest( - []string{"put", "-r", "csv", "-t", "int", "-v", "5", "--csv-write-comma", ".", "[0].a"}, - []byte(`a,b -1,2 -3,4`), - newline([]byte(`a.b -5.2 -3.4`)), - nil, - nil, - )) - - t.Run("VerifyCorrectIndentionForJSON", runTest( - []string{"put", "-r", "json", "--indent", "6", "-t", "string", "--pretty=true", "-v", "Tom", "user"}, - []byte(`{}`), - newline([]byte("{\n \"user\": \"Tom\"\n}")), - nil, - nil, - )) - - t.Run("VerifyCorrectIndentionForYAML", runTest( - []string{"put", "-r", "yaml", "--indent", "6", "-t", "string", "-v", "Tom", "user.name"}, - []byte(``), - newline([]byte("user:\n name: Tom")), - nil, - nil, - )) - - t.Run("VerifyCorrectIndentionForTOML", runTest( - []string{"put", "-r", "toml", "--indent", "6", "-t", "string", "-v", "Tom", "user.name"}, - []byte(``), - newline([]byte("[user]\n name = 'Tom'")), - nil, - nil, - )) -} diff --git a/internal/command/root.go b/internal/command/root.go deleted file mode 100644 index b08cd791..00000000 --- a/internal/command/root.go +++ /dev/null @@ -1,25 +0,0 @@ -package command - -import ( - "github.com/spf13/cobra" - "github.com/tomwright/dasel/v3/internal" -) - -// NewRootCMD returns the root command for use with cobra. -func NewRootCMD() *cobra.Command { - selectCmd := selectCommand() - selectCmd.SilenceErrors = true - selectCmd.SilenceUsage = true - selectCmd.Version = internal.Version - - selectCmd.AddCommand( - putCommand(), - deleteCommand(), - validateCommand(), - ) - - manCmd := manCommand(selectCmd) - selectCmd.AddCommand(manCmd) - - return selectCmd -} diff --git a/internal/command/root_test.go b/internal/command/root_test.go deleted file mode 100644 index 21829a65..00000000 --- a/internal/command/root_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package command - -import ( - "bytes" - "errors" - "reflect" - "testing" -) - -// Runs the dasel root command. -// Returns stdout, stderr and error. -func runDasel(args []string, in []byte) ([]byte, []byte, error) { - stdOut := bytes.NewBuffer([]byte{}) - stdErr := bytes.NewBuffer([]byte{}) - - cmd := NewRootCMD() - cmd.SetArgs(args) - cmd.SetOut(stdOut) - cmd.SetErr(stdErr) - - if in != nil { - cmd.SetIn(bytes.NewReader(in)) - } - - err := cmd.Execute() - return stdOut.Bytes(), stdErr.Bytes(), err -} - -func runTest(args []string, in []byte, expStdOut []byte, expStdErr []byte, expErr error) func(t *testing.T) { - return func(t *testing.T) { - if expStdOut == nil { - expStdOut = []byte{} - } - if expStdErr == nil { - expStdErr = []byte{} - } - - gotStdOut, gotStdErr, gotErr := runDasel(args, in) - if expErr != gotErr && !errors.Is(expErr, gotErr) { - t.Errorf("expected error %v, got %v", expErr, gotErr) - return - } - - if !reflect.DeepEqual(expStdErr, gotStdErr) { - t.Errorf("expected stderr %s, got %s", string(expStdErr), string(gotStdErr)) - } - - if !reflect.DeepEqual(expStdOut, gotStdOut) { - t.Errorf("expected stdout %s, got %s", string(expStdOut), string(gotStdOut)) - } - } -} - -var newlineBytes = []byte("\n") - -func newline(input []byte) []byte { - return append(input, newlineBytes...) -} diff --git a/internal/command/select.go b/internal/command/select.go deleted file mode 100644 index 181c09d8..00000000 --- a/internal/command/select.go +++ /dev/null @@ -1,109 +0,0 @@ -package command - -import ( - "github.com/spf13/cobra" - "github.com/tomwright/dasel/v3" -) - -func selectCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "dasel -f -r ", - Short: "Select properties from the given file.", - RunE: selectRunE, - Args: cobra.MaximumNArgs(1), - } - - selectFlags(cmd) - - return cmd -} - -func selectFlags(cmd *cobra.Command) { - cmd.Flags().StringP("selector", "s", "", "The selector to use when querying the data structure.") - cmd.Flags().StringP("read", "r", "", "The parser to use when reading.") - cmd.Flags().StringP("file", "f", "", "The file to query.") - cmd.Flags().StringP("write", "w", "", "The parser to use when writing. Defaults to the read parser if not provided.") - cmd.Flags().Bool("pretty", true, "Pretty print the output.") - cmd.Flags().Bool("colour", false, "Print colourised output.") - cmd.Flags().Bool("escape-html", false, "Escape HTML tags when writing output.") - cmd.Flags().Int("indent", 2, "The indention level when writing files.") - cmd.Flags().String("csv-comma", ",", "Comma separator to use when working with csv files.") - cmd.Flags().String("csv-write-comma", "", "Comma separator used when writing csv files. Overrides csv-comma when writing.") - cmd.Flags().String("csv-comment", "", "Comma separator used when reading csv files.") - cmd.Flags().Bool("csv-crlf", false, "Then to use CRLF when writing CSV files.") - - _ = cmd.MarkFlagFilename("file") -} - -func selectRunE(cmd *cobra.Command, args []string) error { - selectorFlag, _ := cmd.Flags().GetString("selector") - readParserFlag, _ := cmd.Flags().GetString("read") - fileFlag, _ := cmd.Flags().GetString("file") - writeParserFlag, _ := cmd.Flags().GetString("write") - prettyPrintFlag, _ := cmd.Flags().GetBool("pretty") - colourFlag, _ := cmd.Flags().GetBool("colour") - escapeHTMLFlag, _ := cmd.Flags().GetBool("escape-html") - indent, _ := cmd.Flags().GetInt("indent") - csvComma, _ := cmd.Flags().GetString("csv-comma") - csvWriteComma, _ := cmd.Flags().GetString("csv-write-comma") - csvComment, _ := cmd.Flags().GetString("csv-comment") - csvCRLF, _ := cmd.Flags().GetBool("csv-crlf") - - opts := &selectOptions{ - Read: &readOptions{ - Reader: nil, - Parser: readParserFlag, - FilePath: fileFlag, - CsvComma: csvComma, - CsvComment: csvComment, - }, - Write: &writeOptions{ - Writer: nil, - Parser: writeParserFlag, - FilePath: "stdout", - PrettyPrint: prettyPrintFlag, - Colourise: colourFlag, - EscapeHTML: escapeHTMLFlag, - Indent: indent, - CsvComma: csvWriteComma, - CsvUseCRLF: csvCRLF, - }, - Selector: selectorFlag, - } - - if opts.Selector == "" && len(args) > 0 { - opts.Selector = args[0] - args = args[1:] - } - - if opts.Selector == "" { - opts.Selector = "." - } - - return runSelectCommand(opts, cmd) -} - -type selectOptions struct { - Read *readOptions - Write *writeOptions - Selector string -} - -func runSelectCommand(opts *selectOptions, cmd *cobra.Command) error { - - rootValue, err := opts.Read.rootValue(cmd) - if err != nil { - return err - } - - values, err := dasel.Select(rootValue, opts.Selector) - if err != nil { - return err - } - - if err := opts.Write.writeValues(cmd, opts.Read, values); err != nil { - return err - } - - return nil -} diff --git a/internal/command/select_test.go b/internal/command/select_test.go deleted file mode 100644 index 8841df71..00000000 --- a/internal/command/select_test.go +++ /dev/null @@ -1,511 +0,0 @@ -package command - -import ( - "testing" -) - -func standardJsonSelectTestData() []byte { - return []byte(`{ - "users": [ - { - "name": { - "first": "Tom", - "last": "Wright" - }, - "flags": { - "isBanned": false - } - }, - { - "name": { - "first": "Jim", - "last": "Wright" - }, - "flags": { - "isBanned": true - } - }, - { - "name": { - "first": "Joe", - "last": "Blogs" - }, - "flags": { - "isBanned": false - } - } - ] -}`) -} - -func TestSelectCommand(t *testing.T) { - - t.Run("TotalUsersLen", runTest( - []string{"-r", "json", "--pretty=false", "users.len()"}, - standardJsonSelectTestData(), - newline([]byte(`3`)), - nil, - nil, - )) - - t.Run("TotalUsersCount", runTest( - []string{"-r", "json", "--pretty=false", "users.all().count()"}, - standardJsonSelectTestData(), - newline([]byte(`3`)), - nil, - nil, - )) - - t.Run("TotalBannedUsers", runTest( - []string{"-r", "json", "--pretty=false", "users.all().filter(equal(flags.isBanned,true)).count()"}, - standardJsonSelectTestData(), - newline([]byte(`1`)), - nil, - nil, - )) - - t.Run("TotalNotBannedUsers", runTest( - []string{"-r", "json", "--pretty=false", "users.all().filter(equal(flags.isBanned,false)).count()"}, - standardJsonSelectTestData(), - newline([]byte(`2`)), - nil, - nil, - )) - - t.Run("NotBannedUsers", runTest( - []string{"-r", "json", "--pretty=false", "users.all().filter(equal(flags.isBanned,false)).name.first"}, - standardJsonSelectTestData(), - newline([]byte(`"Tom" -"Joe"`)), - nil, - nil, - )) - - t.Run("BannedUsers", runTest( - []string{"-r", "json", "--pretty=false", "users.all().filter(equal(flags.isBanned,true)).name.first"}, - standardJsonSelectTestData(), - newline([]byte(`"Jim"`)), - nil, - nil, - )) - - t.Run("VerifyCorrectIndentionForJSON", runTest( - []string{"-r", "json", "--indent", "6", "--pretty=true", "users.all().filter(equal(flags.isBanned,true)).name"}, - standardJsonSelectTestData(), - newline([]byte("{\n \"first\": \"Jim\",\n \"last\": \"Wright\"\n}")), - nil, - nil, - )) - - t.Run("VerifyCorrectIndentionForYAML", runTest( - []string{"-r", "json", "-w", "yaml", "--indent", "6", "--pretty=true", "users.all().filter(equal(flags.isBanned,true))"}, - standardJsonSelectTestData(), - newline([]byte("name:\n first: Jim\n last: Wright\nflags:\n isBanned: true")), - nil, - nil, - )) - - t.Run("VerifyCorrectIndentionForTOML", runTest( - []string{"-r", "json", "-w", "toml", "--indent", "6", "--pretty=true", "users.all().filter(equal(flags.isBanned,true))"}, - standardJsonSelectTestData(), - newline([]byte("[flags]\n isBanned = true\n\n[name]\n first = 'Jim'\n last = 'Wright'")), - nil, - nil, - )) - - t.Run("Issue258", runTest( - []string{"-r", "json", "--pretty=false", "-w", "csv", "phones.all().mapOf(make,make,model,model,first,parent().parent().user.name.first,last,parent().parent().user.name.last).merge()"}, - []byte(`{ - "id": "1234", - "user": { - "name": { - "first": "Tom", - "last": "Wright" - } - }, - "favouriteNumbers": [ - 1, 2, 3, 4 - ], - "favouriteColours": [ - "red", "green" - ], - "phones": [ - { - "make": "OnePlus", - "model": "8 Pro" - }, - { - "make": "Apple", - "model": "iPhone 12" - } - ] - }`), - newline([]byte(`first,last,make,model -Tom,Wright,OnePlus,8 Pro -Tom,Wright,Apple,iPhone 12`)), - nil, - nil, - )) - - t.Run("Issue181", runTest( - []string{"-r", "json", "--pretty=false", "all().filter(equal(this(),README.md))"}, - []byte(`[ - "README.md", - "tbump.toml" -]`), - newline([]byte(`"README.md"`)), - nil, - nil, - )) - - // Flaky test due to ordering - // t.Run("Discussion242", runTest( - // []string{"-r", "json", "--pretty=false", "-w", "plain", "all().filter(equal(type(),array)).key()"}, - // []byte(`{ - // "array1": [ - // { - // "a": "aaa", - // "b": "bbb", - // "c": "ccc" - // } - // ], - // "array2": [ - // { - // "a": "aaa", - // "b": "bbb", - // "c": "ccc" - // } - // ] - // }`), - // newline([]byte(`array1 - // array2`)), - // nil, - // nil, - // )) - - t.Run("YamlMultiDoc/Issue314", runTest( - []string{"-r", "yaml", ""}, - []byte(`a: x -b: foo ---- -a: y -c: bar -`), - newline([]byte(`a: x -b: foo ---- -a: y -c: bar`)), - nil, - nil, - )) - - t.Run("Issue316", runTest( - []string{"-r", "json"}, - []byte(`{ - "a": "alice", - "b": null, - "c": [ - { - "d": 9, - "e": null - }, - null - ] -}`), - newline([]byte(`{ - "a": "alice", - "b": null, - "c": [ - { - "d": 9, - "e": null - }, - null - ] -}`)), - nil, - nil, - )) - - // Hex, binary and octal values in YAML - t.Run("Issue326", runTest( - []string{"-r", "yaml"}, - []byte(`hex: 0x1234 -binary: 0b1001 -octal: 0o10 -`), - newline([]byte(`hex: 4660 -binary: 9 -octal: 8`)), - nil, - nil, - )) - - t.Run("Issue331 - YAML to JSON", runTest( - []string{"-r", "yaml", "-w", "json"}, - []byte(`createdAt: 2023-06-13T20:19:48.531620935Z`), - newline([]byte(`{ - "createdAt": "2023-06-13T20:19:48.531620935Z" -}`)), - nil, - nil, - )) - - t.Run("Issue285 - YAML alias on read", runTest( - []string{"-r", "yaml", "-w", "yaml"}, - []byte(`foo: &foofoo - bar: 1 - baz: &baz "baz" -spam: - ham: "eggs" - bar: 0 - <<: *foofoo - baz: "bazbaz" - -baz: *baz -`), - []byte(`foo: - bar: 1 - baz: baz -spam: - ham: eggs - bar: 1 - baz: bazbaz -baz: baz -`), - nil, - nil, - )) - - t.Run("OrDefaultString", runTest( - []string{"-r", "json", "all().orDefault(locale,string(nope))"}, - []byte(`{ - "-LCr5pXw_fN32IqNDr4E": { - "bookCategory": "poetry", - "locale": "en-us", - "mediaType": "book", - "publisher": "Pomelo Books", - "title": "Sound Waves", - "type": "poetry" - }, - "-LDDHjkdY0306fZdvhEQ": { - "ISBN13": "978-1534402966", - "bookCategory": "fiction", - "title": "What Can You Do with a Toolbox?", - "type": "picturebook" - } -}`), - newline([]byte(`"en-us" -"nope"`)), - nil, - nil, - )) - - t.Run("OrDefaultLookup", runTest( - []string{"-r", "json", "all().orDefault(locale,bookCategory)"}, - []byte(`{ - "-LCr5pXw_fN32IqNDr4E": { - "bookCategory": "poetry", - "locale": "en-us", - "mediaType": "book", - "publisher": "Pomelo Books", - "title": "Sound Waves", - "type": "poetry" - }, - "-LDDHjkdY0306fZdvhEQ": { - "ISBN13": "978-1534402966", - "bookCategory": "fiction", - "title": "What Can You Do with a Toolbox?", - "type": "picturebook" - } -}`), - newline([]byte(`"en-us" -"fiction"`)), - nil, - nil, - )) - - t.Run("Issue364 - CSV root element part 1", runTest( - []string{"-r", "csv", "-w", "csv", "all().merge()"}, - []byte(`A,B,C -a,b,c -d,e,f`), - newline([]byte(`A,B,C -a,b,c -d,e,f`)), - nil, - nil, - )) - - t.Run("Issue364 - CSV root element part 2", runTest( - []string{"-r", "csv", "-w", "csv"}, - []byte(`A,B,C -a,b,c -d,e,f`), - newline([]byte(`A,B,C -a,b,c -d,e,f`)), - nil, - nil, - )) - - t.Run("CSV custom separator", runTest( - []string{"-r", "csv", "-w", "csv", "--csv-comma", "."}, - []byte(`A.B.C -a.b.c -d.e.f`), - newline([]byte(`A.B.C -a.b.c -d.e.f`)), - nil, - nil, - )) - - t.Run("CSV change separator", runTest( - []string{"-r", "csv", "-w", "csv", "--csv-comma", ".", "--csv-write-comma", ","}, - []byte(`A.B.C -a.b.c -d.e.f`), - newline([]byte(`A,B,C -a,b,c -d,e,f`)), - nil, - nil, - )) - - t.Run("CSV change from default separator", runTest( - []string{"-r", "csv", "-w", "csv", "--csv-write-comma", "."}, - []byte(`A,B,C -a,b,c -d,e,f`), - newline([]byte(`A.B.C -a.b.c -d.e.f`)), - nil, - nil, - )) - - t.Run("Issue351 incorrectly escaped html, default false", runTest( - []string{"-r", "json"}, - []byte(`{ - "field1": "A", - "field2": "A > B && B > C" -}`), - newline([]byte(`{ - "field1": "A", - "field2": "A > B && B > C" -}`)), - nil, - nil, - )) - - t.Run("Issue351 incorrectly escaped html, specific false", runTest( - []string{"-r", "json", "--escape-html=false"}, - []byte(`{ - "field1": "A", - "field2": "A > B && B > C" -}`), - newline([]byte(`{ - "field1": "A", - "field2": "A > B && B > C" -}`)), - nil, - nil, - )) - - t.Run("Issue351 correctly escaped html", runTest( - []string{"-r", "json", "--escape-html=true"}, - []byte(`{ - "field1": "A", - "field2": "A > B && B > C" -}`), - newline([]byte(`{ - "field1": "A", - "field2": "A \u003e B \u0026\u0026 B \u003e C" -}`)), - nil, - nil, - )) - - t.Run("Issue 374 empty input", func(t *testing.T) { - tests := []struct { - format string - exp []byte - }{ - { - format: "json", - exp: []byte("{}\n"), - }, - { - format: "toml", - exp: []byte("\n"), - }, - { - format: "yaml", - exp: []byte("{}\n"), - }, - { - format: "xml", - exp: []byte("\n"), - }, - { - format: "csv", - exp: []byte(""), - }, - } - - for _, test := range tests { - tc := test - t.Run(tc.format, runTest( - []string{"-r", tc.format}, - []byte(``), - tc.exp, - nil, - nil, - )) - } - }) - - t.Run("Issue 392 panic", runTest( - []string{"-r", "csv", "--csv-comma", ";", "-w", "json", "equal([], )"}, - []byte(`Hello;There; -1;2;`), - []byte("false\n"), - nil, - nil, - )) - - t.Run("Issue346", func(t *testing.T) { - t.Run("Select null or default string", runTest( - []string{"-r", "json", "orDefault(foo,string(nope))"}, - []byte(`{ - "foo": null -}`), - newline([]byte(`"nope"`)), - nil, - nil, - )) - - t.Run("Select null or default null", runTest( - []string{"-r", "json", "orDefault(foo,null())"}, - []byte(`{ - "foo": null -}`), - newline([]byte(`null`)), - nil, - nil, - )) - - t.Run("Select null value", runTest( - []string{"-r", "json", "foo"}, - []byte(`{ - "foo": null -}`), - newline([]byte(`null`)), - nil, - nil, - )) - }) - -} diff --git a/internal/command/validate.go b/internal/command/validate.go deleted file mode 100644 index 472e0885..00000000 --- a/internal/command/validate.go +++ /dev/null @@ -1,145 +0,0 @@ -package command - -import ( - "fmt" - "github.com/spf13/cobra" - "io" - "path/filepath" - "sync" -) - -func validateCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "validate ", - Short: "Validate a list of files.", - RunE: validateRunE, - Args: cobra.ArbitraryArgs, - } - - validateFlags(cmd) - - return cmd -} - -func validateFlags(cmd *cobra.Command) { - cmd.Flags().Bool("include-error", true, "Show error/validation information") -} - -func validateRunE(cmd *cobra.Command, args []string) error { - includeErrorFlag, _ := cmd.Flags().GetBool("include-error") - - files := make([]validationFile, 0) - for _, a := range args { - matches, err := filepath.Glob(a) - if err != nil { - return err - } - - for _, m := range matches { - files = append(files, validationFile{ - File: m, - Parser: "", - }) - } - } - - return runValidateCommand(validateOptions{ - Files: files, - IncludeError: includeErrorFlag, - }, cmd) -} - -type validateOptions struct { - Files []validationFile - Reader io.Reader - Writer io.Writer - IncludeError bool -} - -func runValidateCommand(opts validateOptions, cmd *cobra.Command) error { - fileCount := len(opts.Files) - - wg := &sync.WaitGroup{} - wg.Add(fileCount) - - results := make([]validationFileResult, fileCount) - - for i, f := range opts.Files { - index := i - file := f - go func() { - defer wg.Done() - - pass, err := validateFile(cmd, file) - results[index] = validationFileResult{ - File: file, - Pass: pass, - Error: err, - } - }() - } - - wg.Wait() - - failureCount := 0 - for _, result := range results { - if !result.Pass { - failureCount++ - } - } - - // Set up our output writer if one wasn't provided. - if opts.Writer == nil { - if failureCount > 0 { - opts.Writer = cmd.OutOrStderr() - } else { - opts.Writer = cmd.OutOrStdout() - } - } - - for _, result := range results { - outputString := "" - - if result.Pass { - outputString += "pass" - } else { - outputString += "fail" - } - - outputString += " " + result.File.File - - if opts.IncludeError && result.Error != nil { - outputString += " " + result.Error.Error() - } - - if _, err := fmt.Fprintln(opts.Writer, outputString); err != nil { - return fmt.Errorf("could not write output: %w", err) - } - } - - if failureCount > 0 { - return fmt.Errorf("%d files failed validation", failureCount) - } - return nil -} - -type validationFile struct { - File string - Parser string -} - -type validationFileResult struct { - File validationFile - Pass bool - Error error -} - -func validateFile(cmd *cobra.Command, file validationFile) (bool, error) { - opts := readOptions{ - Parser: file.Parser, - FilePath: file.File, - } - _, err := opts.rootValue(cmd) - - return err == nil, err -} diff --git a/util/to_string.go b/internal/util/to_string.go similarity index 100% rename from util/to_string.go rename to internal/util/to_string.go diff --git a/selector.go b/selector.go deleted file mode 100644 index 95520c5e..00000000 --- a/selector.go +++ /dev/null @@ -1,224 +0,0 @@ -package dasel - -import ( - "fmt" - "io" - "strings" -) - -type ErrBadSelectorSyntax struct { - Part string - Message string -} - -func (e ErrBadSelectorSyntax) Error() string { - return fmt.Sprintf("bad syntax: %s, around %s", e.Message, e.Part) -} - -func (e ErrBadSelectorSyntax) Is(other error) bool { - o, ok := other.(*ErrBadSelectorSyntax) - if !ok { - return false - } - if o.Part != "" && o.Part != e.Part { - return false - } - if o.Message != "" && o.Message != e.Message { - return false - } - return true -} - -type Selector struct { - funcName string - funcArgs []string -} - -type SelectorResolver interface { - Original() string - Next() (*Selector, error) -} - -func NewSelectorResolver(selector string, functions *FunctionCollection) SelectorResolver { - return &standardSelectorResolver{ - functions: functions, - original: selector, - reader: strings.NewReader(selector), - separator: '.', - openFunc: '(', - closeFunc: ')', - argSeparator: ',', - escapeChar: '\\', - } -} - -type standardSelectorResolver struct { - functions *FunctionCollection - original string - reader *strings.Reader - separator rune - openFunc rune - closeFunc rune - argSeparator rune - escapeChar rune -} - -func (r *standardSelectorResolver) Original() string { - return r.original -} - -// nextPart returns the next part. -// It returns true if there are more parts to the selector, or false if we reached the end. -func (r *standardSelectorResolver) nextPart() (string, bool) { - b := &strings.Builder{} - bracketDepth := 0 - escaped := false - for { - readRune, _, err := r.reader.ReadRune() - if err == io.EOF { - return b.String(), false - } - if escaped { - b.WriteRune(readRune) - escaped = false - continue - } else if readRune == r.escapeChar { - b.WriteRune(readRune) - escaped = true - continue - } else if readRune == r.openFunc { - bracketDepth++ - } else if readRune == r.closeFunc { - bracketDepth-- - } - if readRune == r.separator && bracketDepth == 0 { - return b.String(), true - } - b.WriteRune(readRune) - } -} - -func (r *standardSelectorResolver) Next() (*Selector, error) { - nextPart, moreParts := r.nextPart() - if nextPart == "" && !moreParts { - return nil, nil - } - if nextPart == "" && moreParts { - return &Selector{ - funcName: "this", - funcArgs: []string{}, - }, nil - } - - if r.functions != nil { - if s := r.functions.ParseSelector(nextPart); s != nil { - return s, nil - } - } - - var hasOpenedFunc, hasClosedFunc = false, false - bracketDepth := 0 - - var funcNameBuilder = &strings.Builder{} - var argBuilder = &strings.Builder{} - - nextPartReader := strings.NewReader(nextPart) - - funcName := "" - args := make([]string, 0) - - escaped := false - for { - nextRune, _, err := nextPartReader.ReadRune() - if err == io.EOF { - if funcNameBuilder.Len() > 0 { - funcName = funcNameBuilder.String() - } - break - } - if err != nil { - return nil, fmt.Errorf("could not read selector: %w", err) - } - - switch { - case nextRune == r.escapeChar && !escaped: - escaped = true - continue - - case nextRune == r.openFunc && !escaped: - if !hasOpenedFunc { - hasOpenedFunc = true - funcName = funcNameBuilder.String() - if funcName == "" { - return nil, &ErrBadSelectorSyntax{ - Part: nextPart, - Message: "function name required before open bracket", - } - } - } else { - argBuilder.WriteRune(nextRune) - } - bracketDepth++ - - case nextRune == r.closeFunc && !escaped: - if bracketDepth > 1 { - argBuilder.WriteRune(nextRune) - } else if bracketDepth == 1 { - hasClosedFunc = true - arg := argBuilder.String() - if arg != "" { - args = append(args, argBuilder.String()) - } - } else if bracketDepth < 1 { - return nil, &ErrBadSelectorSyntax{ - Part: nextPart, - Message: "too many closing brackets", - } - } - bracketDepth-- - - case hasOpenedFunc && nextRune == r.argSeparator && !escaped: - if bracketDepth > 1 { - argBuilder.WriteRune(nextRune) - } else if bracketDepth == 1 { - arg := argBuilder.String() - argBuilder.Reset() - if arg != "" { - args = append(args, arg) - } - } - - case hasOpenedFunc: - if escaped { - escaped = false - } - argBuilder.WriteRune(nextRune) - - case hasClosedFunc: - // Do not allow anything after the closeFunc - return nil, &ErrBadSelectorSyntax{ - Part: nextPart, - Message: "selector function must end after closing bracket", - } - - default: - if escaped { - escaped = false - } - funcNameBuilder.WriteRune(nextRune) - } - } - - if !hasOpenedFunc { - return &Selector{ - funcName: "property", - funcArgs: []string{funcName}, - }, nil - } - - return &Selector{ - funcName: funcName, - funcArgs: args, - }, nil - -} diff --git a/selector_test.go b/selector_test.go deleted file mode 100644 index 4b0e2bca..00000000 --- a/selector_test.go +++ /dev/null @@ -1,156 +0,0 @@ -package dasel - -import ( - "errors" - "reflect" - "testing" -) - -func collectAll(r SelectorResolver) ([]Selector, error) { - res := make([]Selector, 0) - - for { - s, err := r.Next() - if err != nil { - return res, err - } - if s == nil { - break - } - res = append(res, *s) - } - - return res, nil -} - -func TestStandardSelectorResolver_Next(t *testing.T) { - r := NewSelectorResolver("index(1).property(user).name.property(first,last?)", nil) - - got, err := collectAll(r) - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - - exp := []Selector{ - { - funcName: "index", - funcArgs: []string{"1"}, - }, - { - funcName: "property", - funcArgs: []string{"user"}, - }, - { - funcName: "property", - funcArgs: []string{"name"}, - }, - { - funcName: "property", - funcArgs: []string{"first", "last?"}, - }, - } - - if !reflect.DeepEqual(exp, got) { - t.Errorf("exp: %v, got: %v", exp, got) - } -} - -func TestStandardSelectorResolver_Next_Nested(t *testing.T) { - r := NewSelectorResolver("nested(a().b(),c(),d()).nested(a().b(),c(),d())", nil) - - got, err := collectAll(r) - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - - exp := []Selector{ - { - funcName: "nested", - funcArgs: []string{"a().b()", "c()", "d()"}, - }, - { - funcName: "nested", - funcArgs: []string{"a().b()", "c()", "d()"}, - }, - } - - if !reflect.DeepEqual(exp, got) { - t.Errorf("exp: %v, got: %v", exp, got) - } -} - -func TestStandardSelectorResolver_Next_ExtraClosingBracket(t *testing.T) { - r := NewSelectorResolver("all().filter(not(equal(x,true))))", nil) - - expErr := &ErrBadSelectorSyntax{ - Part: "filter(not(equal(x,true))))", - Message: "too many closing brackets", - } - - _, err := collectAll(r) - - if !errors.Is(err, expErr) { - t.Errorf("expected error: %v, got %v", expErr, err) - return - } -} - -func TestStandardSelectorResolver_Next_EscapedDot(t *testing.T) { - r := NewSelectorResolver("plugins.io\\.containerd\\.grpc\\.v1\\.cri.registry", nil) - - got, err := collectAll(r) - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - - exp := []Selector{ - { - funcName: "property", - funcArgs: []string{"plugins"}, - }, - { - funcName: "property", - funcArgs: []string{"io.containerd.grpc.v1.cri"}, - }, - { - funcName: "property", - funcArgs: []string{"registry"}, - }, - } - - if !reflect.DeepEqual(exp, got) { - t.Errorf("exp: %v, got: %v", exp, got) - } -} - -func TestStandardSelectorResolver_Next_EscapedEverything(t *testing.T) { - r := NewSelectorResolver("a.b\\(\\.asdw\\\\\\].c(\\))", nil) - - got, err := collectAll(r) - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - - exp := []Selector{ - { - funcName: "property", - funcArgs: []string{"a"}, - }, - { - funcName: "property", - funcArgs: []string{"b(.asdw\\]"}, - }, - { - funcName: "c", - funcArgs: []string{")"}, - }, - } - - if !reflect.DeepEqual(exp, got) { - t.Errorf("exp: %v, got: %v", exp, got) - } -} diff --git a/step.go b/step.go deleted file mode 100644 index 8dcc667d..00000000 --- a/step.go +++ /dev/null @@ -1,41 +0,0 @@ -package dasel - -// Step is a single step in the query. -// Each function call has its own step. -// Each value in the output is simply a pointer to the actual data point in the context data. -type Step struct { - context *Context - selector Selector - index int - output Values -} - -func (s *Step) Selector() Selector { - return s.selector -} - -func (s *Step) Index() int { - return s.index -} - -func (s *Step) Output() Values { - return s.output -} - -func (s *Step) execute() error { - f, err := s.context.functions.Get(s.selector.funcName) - if err != nil { - return err - } - output, err := f(s.context, s, s.selector.funcArgs) - s.output = output - return err -} - -func (s *Step) inputs() Values { - prevStep := s.context.Step(s.index - 1) - if prevStep == nil { - return Values{} - } - return prevStep.output -} diff --git a/storage/colourise.go b/storage/colourise.go deleted file mode 100644 index 48ee712d..00000000 --- a/storage/colourise.go +++ /dev/null @@ -1,25 +0,0 @@ -package storage - -import ( - "bytes" - "github.com/alecthomas/chroma/v2/quick" -) - -// ColouriseStyle is the style used when colourising output. -const ColouriseStyle = "solarized-dark256" - -// ColouriseFormatter is the formatter used when colourising output. -const ColouriseFormatter = "terminal" - -// ColouriseBuffer colourises the given buffer in-place. -func ColouriseBuffer(content *bytes.Buffer, lexer string) error { - contentString := content.String() - content.Reset() - return quick.Highlight(content, contentString, lexer, ColouriseFormatter, ColouriseStyle) -} - -// Colourise colourises the given string. -func Colourise(content string, lexer string) (*bytes.Buffer, error) { - buf := new(bytes.Buffer) - return buf, quick.Highlight(buf, content, lexer, ColouriseFormatter, ColouriseStyle) -} diff --git a/storage/csv.go b/storage/csv.go deleted file mode 100644 index 4ffe46cf..00000000 --- a/storage/csv.go +++ /dev/null @@ -1,271 +0,0 @@ -package storage - -import ( - "bytes" - "encoding/csv" - "fmt" - "sort" - - "github.com/tomwright/dasel/v3" - "github.com/tomwright/dasel/v3/dencoding" - "github.com/tomwright/dasel/v3/util" -) - -func init() { - registerReadParser([]string{"csv"}, []string{".csv"}, &CSVParser{}) - registerWriteParser([]string{"csv"}, []string{".csv"}, &CSVParser{}) -} - -// CSVParser is a Parser implementation to handle csv files. -type CSVParser struct { -} - -// CSVDocument represents a CSV file. -// This is required to keep headers in the expected order. -type CSVDocument struct { - Value []map[string]interface{} - Headers []string -} - -// FromBytes returns some data that is represented by the given bytes. -func (p *CSVParser) FromBytes(byteData []byte, options ...ReadWriteOption) (dasel.Value, error) { - if byteData == nil { - return dasel.Value{}, fmt.Errorf("could not read csv file: no data") - } - - reader := csv.NewReader(bytes.NewBuffer(byteData)) - - for _, o := range options { - switch o.Key { - case OptionCSVComma: - if value, ok := o.Value.(rune); ok { - reader.Comma = value - } - case OptionCSVComment: - if value, ok := o.Value.(rune); ok { - reader.Comment = value - } - } - } - - res := make([]*dencoding.Map, 0) - records, err := reader.ReadAll() - if err != nil { - return dasel.Value{}, fmt.Errorf("could not read csv file: %w", err) - } - if len(records) == 0 { - return dasel.Value{}, nil - } - var headers []string - for i, row := range records { - if i == 0 { - headers = row - continue - } - rowRes := dencoding.NewMap() - allEmpty := true - for index, val := range row { - if val != "" { - allEmpty = false - } - rowRes.Set(headers[index], val) - } - if !allEmpty { - res = append(res, rowRes) - } - } - - return dasel.ValueOf(res). - WithMetadata("csvHeaders", headers), nil -} - -func interfaceToCSVDocument(val interface{}) (*CSVDocument, error) { - switch v := val.(type) { - case map[string]interface{}: - headers := make([]string, 0) - for k := range v { - headers = append(headers, k) - } - sort.Strings(headers) - return &CSVDocument{ - Value: []map[string]interface{}{v}, - Headers: headers, - }, nil - - case *dencoding.Map: - return &CSVDocument{ - Value: []map[string]any{v.UnorderedData()}, - Headers: v.Keys(), - }, nil - - case []interface{}: - mapVals := make([]map[string]interface{}, 0) - headers := make([]string, 0) - for _, val := range v { - switch x := val.(type) { - case map[string]any: - mapVals = append(mapVals, x) - for objectKey := range x { - found := false - for _, existingHeader := range headers { - if existingHeader == objectKey { - found = true - break - } - } - if !found { - headers = append(headers, objectKey) - } - } - case *dencoding.Map: - mapVals = append(mapVals, x.UnorderedData()) - for _, objectKey := range x.Keys() { - found := false - for _, existingHeader := range headers { - if existingHeader == objectKey { - found = true - break - } - } - if !found { - headers = append(headers, objectKey) - } - } - } - } - sort.Strings(headers) - return &CSVDocument{ - Value: mapVals, - Headers: headers, - }, nil - - case []*dencoding.Map: - mapVals := make([]map[string]interface{}, 0) - headers := make([]string, 0) - for _, val := range v { - mapVals = append(mapVals, val.UnorderedData()) - for _, objectKey := range val.Keys() { - found := false - for _, existingHeader := range headers { - if existingHeader == objectKey { - found = true - break - } - } - if !found { - headers = append(headers, objectKey) - } - } - } - sort.Strings(headers) - return &CSVDocument{ - Value: mapVals, - Headers: headers, - }, nil - - default: - return nil, fmt.Errorf("CSVParser.toBytes cannot handle type %T", val) - } -} - -// ToBytes returns a slice of bytes that represents the given value. -func (p *CSVParser) ToBytes(value dasel.Value, options ...ReadWriteOption) ([]byte, error) { - buffer := new(bytes.Buffer) - writer := csv.NewWriter(buffer) - - for _, o := range options { - switch o.Key { - case OptionCSVComma: - if value, ok := o.Value.(rune); ok { - writer.Comma = value - } - case OptionCSVComment: - if value, ok := o.Value.(bool); ok { - writer.UseCRLF = value - } - } - } - - // Allow for multi document output by just appending documents on the end of each other. - // This is really only supported so as we have nicer output when converting to CSV from - // other multi-document formats. - - docs := make([]*CSVDocument, 0) - - // headers, _ := value.Metadata("csvHeaders").([]string) - - switch { - case value.Metadata("isSingleDocument") == true: - doc, err := interfaceToCSVDocument(value.Interface()) - if err != nil { - return nil, err - } - docs = append(docs, doc) - case value.Metadata("isMultiDocument") == true: - for i := 0; i < value.Len(); i++ { - doc, err := interfaceToCSVDocument(value.Index(i).Interface()) - if err != nil { - return nil, err - } - docs = append(docs, doc) - } - default: - doc, err := interfaceToCSVDocument(value.Interface()) - if err != nil { - return nil, err - } - docs = append(docs, doc) - } - - for _, doc := range docs { - if err := p.toBytesHandleDoc(writer, doc); err != nil { - return nil, err - } - } - - return buffer.Bytes(), nil -} - -func (p *CSVParser) toBytesHandleDoc(writer *csv.Writer, doc *CSVDocument) error { - // Iterate through the rows and detect any new headers. - for _, r := range doc.Value { - for k := range r { - headerExists := false - for _, header := range doc.Headers { - if k == header { - headerExists = true - break - } - } - if !headerExists { - doc.Headers = append(doc.Headers, k) - } - } - } - - // Iterate through the rows and write the output. - for i, r := range doc.Value { - if i == 0 { - if err := writer.Write(doc.Headers); err != nil { - return fmt.Errorf("could not write headers: %w", err) - } - } - - values := make([]string, 0) - for _, header := range doc.Headers { - val, ok := r[header] - if !ok { - val = "" - } - values = append(values, util.ToString(val)) - } - - if err := writer.Write(values); err != nil { - return fmt.Errorf("could not write values: %w", err) - } - - writer.Flush() - } - - return nil -} diff --git a/storage/csv_test.go b/storage/csv_test.go deleted file mode 100644 index 919068f0..00000000 --- a/storage/csv_test.go +++ /dev/null @@ -1,143 +0,0 @@ -package storage_test - -import ( - "reflect" - "testing" - - "github.com/tomwright/dasel/v3" - "github.com/tomwright/dasel/v3/dencoding" - "github.com/tomwright/dasel/v3/storage" -) - -var csvBytes = []byte(`id,name -1,Tom -2,Jim -`) -var csvMap = []*dencoding.Map{ - dencoding.NewMap(). - Set("id", "1"). - Set("name", "Tom"), - dencoding.NewMap(). - Set("id", "2"). - Set("name", "Jim"), -} - -func TestCSVParser_FromBytes(t *testing.T) { - got, err := (&storage.CSVParser{}).FromBytes(csvBytes) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - exp := dasel.ValueOf(csvMap).WithMetadata("csvHeaders", []string{"id", "name"}) - if !reflect.DeepEqual(exp.Interface(), got.Interface()) { - t.Errorf("expected %v, got %v", exp, got) - } -} - -func TestCSVParser_FromBytes_Error(t *testing.T) { - _, err := (&storage.CSVParser{}).FromBytes(nil) - if err == nil { - t.Errorf("expected error but got none") - return - } - _, err = (&storage.CSVParser{}).FromBytes([]byte(`a,b -a,b,c`)) - if err == nil { - t.Errorf("expected error but got none") - return - } - _, err = (&storage.CSVParser{}).FromBytes([]byte(`a,b,c -a,b`)) - if err == nil { - t.Errorf("expected error but got none") - return - } -} - -func TestCSVParser_ToBytes(t *testing.T) { - t.Run("SingleDocument", func(t *testing.T) { - value := dasel.ValueOf(map[string]interface{}{ - "id": "1", - "name": "Tom", - }). - WithMetadata("isSingleDocument", true) - got, err := (&storage.CSVParser{}).ToBytes(value) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - deepEqualOneOf(t, got, []byte(`id,name -1,Tom -`), []byte(`name,id -Tom,1 -`)) - }) - t.Run("SingleDocumentSlice", func(t *testing.T) { - value := dasel.ValueOf([]interface{}{ - map[string]interface{}{ - "id": "1", - "name": "Tom", - }, - map[string]interface{}{ - "id": "2", - "name": "Tommy", - }, - }). - WithMetadata("isSingleDocument", true) - got, err := (&storage.CSVParser{}).ToBytes(value) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - deepEqualOneOf(t, got, []byte(`id,name -1,Tom -2,Tommy -`), []byte(`name,id -Tom,1 -`)) - }) - t.Run("MultiDocument", func(t *testing.T) { - value := dasel.ValueOf([]interface{}{ - map[string]interface{}{ - "id": "1", - "name": "Tom", - }, - map[string]interface{}{ - "id": "2", - "name": "Jim", - }, - }). - WithMetadata("isMultiDocument", true) - got, err := (&storage.CSVParser{}).ToBytes(value) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - deepEqualOneOf(t, got, []byte(`id,name -1,Tom -id,name -2,Jim -`), []byte(`name,id -Tom,1 -id,name -2,Jim -`), []byte(`id,name -1,Tom -name,id -Jim,2 -`), []byte(`name,id -Tom,1 -name,id -Jim,2 -`)) - }) -} - -func deepEqualOneOf(t *testing.T, got []byte, exps ...[]byte) { - for _, exp := range exps { - if reflect.DeepEqual(exp, got) { - return - } - } - t.Errorf("%s did not match any of the expected values", string(got)) -} diff --git a/storage/json.go b/storage/json.go deleted file mode 100644 index cb40264e..00000000 --- a/storage/json.go +++ /dev/null @@ -1,133 +0,0 @@ -package storage - -import ( - "bytes" - "fmt" - "io" - - "github.com/tomwright/dasel/v3" - "github.com/tomwright/dasel/v3/dencoding" -) - -func init() { - registerReadParser([]string{"json"}, []string{".json"}, &JSONParser{}) - registerWriteParser([]string{"json"}, []string{".json"}, &JSONParser{}) -} - -// JSONParser is a Parser implementation to handle json files. -type JSONParser struct { -} - -// FromBytes returns some data that is represented by the given bytes. -func (p *JSONParser) FromBytes(byteData []byte, options ...ReadWriteOption) (dasel.Value, error) { - res := make([]any, 0) - - decoder := dencoding.NewJSONDecoder(bytes.NewReader(byteData)) - -docLoop: - for { - var docData any - if err := decoder.Decode(&docData); err != nil { - if err == io.EOF { - break docLoop - } - return dasel.Value{}, fmt.Errorf("could not unmarshal data: %w", err) - } else { - res = append(res, docData) - } - } - - switch len(res) { - case 0: - return dasel.Value{}, nil - case 1: - return dasel.ValueOf(res[0]). - WithMetadata("isSingleDocument", true), nil - default: - return dasel.ValueOf(res). - WithMetadata("isMultiDocument", true), nil - } -} - -type toBytesOptions struct { - indent string - prefix string - prettyPrint bool - colourise bool - escapeHTML bool -} - -func getToBytesOptions(options ...ReadWriteOption) toBytesOptions { - res := toBytesOptions{ - indent: " ", - prefix: "", - prettyPrint: true, - colourise: false, - escapeHTML: false, - } - - for _, o := range options { - switch o.Key { - case OptionIndent: - if value, ok := o.Value.(string); ok { - res.indent = value - } - case OptionPrettyPrint: - if value, ok := o.Value.(bool); ok { - res.prettyPrint = value - } - case OptionColourise: - if value, ok := o.Value.(bool); ok { - res.colourise = value - } - case OptionEscapeHTML: - if value, ok := o.Value.(bool); ok { - res.escapeHTML = value - } - } - } - - if !res.prettyPrint { - res.indent = "" - } - - return res -} - -// ToBytes returns a slice of bytes that represents the given value. -func (p *JSONParser) ToBytes(value dasel.Value, options ...ReadWriteOption) ([]byte, error) { - encoderOptions := make([]dencoding.JSONEncoderOption, 0) - - baseOptions := getToBytesOptions(options...) - encoderOptions = append(encoderOptions, dencoding.JSONEscapeHTML(baseOptions.escapeHTML)) - encoderOptions = append(encoderOptions, dencoding.JSONEncodeIndent(baseOptions.prefix, baseOptions.indent)) - - buffer := new(bytes.Buffer) - encoder := dencoding.NewJSONEncoder(buffer, encoderOptions...) - defer encoder.Close() - - switch { - case value.Metadata("isSingleDocument") == true: - if err := encoder.Encode(value.Interface()); err != nil { - return nil, fmt.Errorf("could not encode single document: %w", err) - } - case value.Metadata("isMultiDocument") == true: - for i := 0; i < value.Len(); i++ { - if err := encoder.Encode(value.Index(i).Interface()); err != nil { - return nil, fmt.Errorf("could not encode multi document [%d]: %w", i, err) - } - } - default: - if err := encoder.Encode(value.Interface()); err != nil { - return nil, fmt.Errorf("could not encode default document type: %w", err) - } - } - - if baseOptions.colourise { - if err := ColouriseBuffer(buffer, "json"); err != nil { - return nil, fmt.Errorf("could not colourise output: %w", err) - } - } - - return buffer.Bytes(), nil -} diff --git a/storage/json_test.go b/storage/json_test.go deleted file mode 100644 index 2f869cc0..00000000 --- a/storage/json_test.go +++ /dev/null @@ -1,189 +0,0 @@ -package storage_test - -import ( - "reflect" - "testing" - - "github.com/tomwright/dasel/v3" - "github.com/tomwright/dasel/v3/dencoding" - "github.com/tomwright/dasel/v3/storage" -) - -var jsonBytes = []byte(`{ - "name": "Tom" -} -`) -var jsonMap = dencoding.NewMap().Set("name", "Tom") - -func TestJSONParser_FromBytes(t *testing.T) { - t.Run("Valid", func(t *testing.T) { - got, err := (&storage.JSONParser{}).FromBytes(jsonBytes) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - exp := jsonMap - if !reflect.DeepEqual(exp, got.Interface()) { - t.Errorf("expected %v, got %v", exp, got) - } - }) - t.Run("ValidMultiDocument", func(t *testing.T) { - got, err := (&storage.JSONParser{}).FromBytes(jsonBytesMulti) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - exp := jsonMapMulti - if !reflect.DeepEqual(exp, got.Interface()) { - t.Errorf("expected %v, got %v", jsonMap, got) - } - }) - t.Run("ValidMultiDocumentMixed", func(t *testing.T) { - got, err := (&storage.JSONParser{}).FromBytes(jsonBytesMultiMixed) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - exp := jsonMapMultiMixed - if !reflect.DeepEqual(exp, got.Interface()) { - t.Errorf("expected %v, got %v", jsonMap, got) - } - }) - t.Run("Empty", func(t *testing.T) { - got, err := (&storage.JSONParser{}).FromBytes([]byte(``)) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - if !reflect.DeepEqual(dasel.Value{}, got) { - t.Errorf("expected %v, got %v", nil, got) - } - }) -} - -func TestJSONParser_FromBytes_Error(t *testing.T) { - _, err := (&storage.JSONParser{}).FromBytes(yamlBytes) - if err == nil { - t.Errorf("expected error but got none") - return - } -} - -func TestJSONParser_ToBytes(t *testing.T) { - t.Run("Valid", func(t *testing.T) { - got, err := (&storage.JSONParser{}).ToBytes(dasel.ValueOf(jsonMap)) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - if string(jsonBytes) != string(got) { - t.Errorf("expected %v, got %v", string(jsonBytes), string(got)) - } - }) - - t.Run("ValidSingle", func(t *testing.T) { - got, err := (&storage.JSONParser{}).ToBytes(dasel.ValueOf(jsonMap).WithMetadata("isSingleDocument", true)) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - if string(jsonBytes) != string(got) { - t.Errorf("expected %v, got %v", string(jsonBytes), string(got)) - } - }) - - t.Run("ValidSingleNoPrettyPrint", func(t *testing.T) { - res, err := (&storage.JSONParser{}).ToBytes(dasel.ValueOf(jsonMap).WithMetadata("isSingleDocument", true), storage.PrettyPrintOption(false)) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - got := string(res) - exp := `{"name":"Tom"} -` - if exp != got { - t.Errorf("expected %v, got %v", exp, got) - } - }) - - t.Run("ValidSingleColourise", func(t *testing.T) { - got, err := (&storage.JSONParser{}).ToBytes(dasel.ValueOf(jsonMap).WithMetadata("isSingleDocument", true), storage.ColouriseOption(true)) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - expBuf, _ := storage.Colourise(`{ - "name": "Tom" -} -`, "json") - exp := expBuf.Bytes() - if !reflect.DeepEqual(exp, got) { - t.Errorf("expected %v, got %v", exp, got) - } - }) - - t.Run("ValidSingleCustomIndent", func(t *testing.T) { - res, err := (&storage.JSONParser{}).ToBytes(dasel.ValueOf(jsonMap).WithMetadata("isSingleDocument", true), storage.IndentOption(" ")) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - got := string(res) - exp := `{ - "name": "Tom" -} -` - if exp != got { - t.Errorf("expected %v, got %v", exp, got) - } - }) - - t.Run("ValidMulti", func(t *testing.T) { - got, err := (&storage.JSONParser{}).ToBytes(dasel.ValueOf(jsonMapMulti).WithMetadata("isMultiDocument", true)) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - if string(jsonBytesMulti) != string(got) { - t.Errorf("expected %v, got %v", string(jsonBytesMulti), string(got)) - } - }) - - t.Run("ValidMultiMixed", func(t *testing.T) { - got, err := (&storage.JSONParser{}).ToBytes(dasel.ValueOf(jsonMapMultiMixed).WithMetadata("isMultiDocument", true)) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - if string(jsonBytesMultiMixed) != string(got) { - t.Errorf("expected %v, got %v", string(jsonBytesMultiMixed), string(got)) - } - }) -} - -var jsonBytesMulti = []byte(`{ - "name": "Tom" -} -{ - "name": "Ellis" -} -`) - -var jsonMapMulti = []any{ - dencoding.NewMap().Set("name", "Tom"), - dencoding.NewMap().Set("name", "Ellis"), -} - -var jsonBytesMultiMixed = []byte(`{ - "name": "Tom", - "other": true -} -{ - "name": "Ellis" -} -`) - -var jsonMapMultiMixed = []interface{}{ - dencoding.NewMap().Set("name", "Tom").Set("other", true), - dencoding.NewMap().Set("name", "Ellis"), -} diff --git a/storage/option.go b/storage/option.go deleted file mode 100644 index 4a625ff6..00000000 --- a/storage/option.go +++ /dev/null @@ -1,83 +0,0 @@ -package storage - -// IndentOption returns a write option that sets the given indent. -func IndentOption(indent string) ReadWriteOption { - return ReadWriteOption{ - Key: OptionIndent, - Value: indent, - } -} - -// PrettyPrintOption returns an option that enables or disables pretty printing. -func PrettyPrintOption(enabled bool) ReadWriteOption { - return ReadWriteOption{ - Key: OptionPrettyPrint, - Value: enabled, - } -} - -// ColouriseOption returns an option that enables or disables colourised output. -func ColouriseOption(enabled bool) ReadWriteOption { - return ReadWriteOption{ - Key: OptionColourise, - Value: enabled, - } -} - -// EscapeHTMLOption returns an option that enables or disables HTML escaping. -func EscapeHTMLOption(enabled bool) ReadWriteOption { - return ReadWriteOption{ - Key: OptionEscapeHTML, - Value: enabled, - } -} - -// CsvCommaOption returns an option that modifies the separator character for CSV files. -func CsvCommaOption(comma rune) ReadWriteOption { - return ReadWriteOption{ - Key: OptionCSVComma, - Value: comma, - } -} - -// CsvCommentOption returns an option that modifies the comment character for CSV files. -func CsvCommentOption(comma rune) ReadWriteOption { - return ReadWriteOption{ - Key: OptionCSVComment, - Value: comma, - } -} - -// CsvUseCRLFOption returns an option that modifies the comment character for CSV files. -func CsvUseCRLFOption(enabled bool) ReadWriteOption { - return ReadWriteOption{ - Key: OptionCSVUseCRLF, - Value: enabled, - } -} - -// OptionKey is a defined type for keys within a ReadWriteOption. -type OptionKey string - -const ( - // OptionIndent is the key used with IndentOption. - OptionIndent OptionKey = "indent" - // OptionPrettyPrint is the key used with PrettyPrintOption. - OptionPrettyPrint OptionKey = "prettyPrint" - // OptionColourise is the key used with ColouriseOption. - OptionColourise OptionKey = "colourise" - // OptionEscapeHTML is the key used with EscapeHTMLOption. - OptionEscapeHTML OptionKey = "escapeHtml" - // OptionCSVComma is the key used with CsvCommaOption. - OptionCSVComma OptionKey = "csvComma" - // OptionCSVComment is the key used with CsvCommentOption. - OptionCSVComment OptionKey = "csvComment" - // OptionCSVUseCRLF is the key used with CsvUseCRLFOption. - OptionCSVUseCRLF OptionKey = "csvUseCRLF" -) - -// ReadWriteOption is an option to be used when writing. -type ReadWriteOption struct { - Key OptionKey - Value interface{} -} diff --git a/storage/parser.go b/storage/parser.go deleted file mode 100644 index 3b1b7f68..00000000 --- a/storage/parser.go +++ /dev/null @@ -1,135 +0,0 @@ -package storage - -import ( - "fmt" - "io" - "os" - "path/filepath" - "strings" - - "github.com/tomwright/dasel/v3" -) - -var readParsersByExtension = map[string]ReadParser{} -var writeParsersByExtension = map[string]WriteParser{} -var readParsersByName = map[string]ReadParser{} -var writeParsersByName = map[string]WriteParser{} - -func registerReadParser(names []string, extensions []string, parser ReadParser) { - for _, n := range names { - readParsersByName[n] = parser - } - for _, e := range extensions { - readParsersByExtension[e] = parser - } -} - -func registerWriteParser(names []string, extensions []string, parser WriteParser) { - for _, n := range names { - writeParsersByName[n] = parser - } - for _, e := range extensions { - writeParsersByExtension[e] = parser - } -} - -// UnknownParserErr is returned when an invalid parser name is given. -type UnknownParserErr struct { - Parser string -} - -func (e UnknownParserErr) Is(other error) bool { - _, ok := other.(*UnknownParserErr) - return ok -} - -// Error returns the error message. -func (e UnknownParserErr) Error() string { - return fmt.Sprintf("unknown parser: %s", e.Parser) -} - -// ReadParser can be used to convert bytes to data. -type ReadParser interface { - // FromBytes returns some data that is represented by the given bytes. - FromBytes(byteData []byte, options ...ReadWriteOption) (dasel.Value, error) -} - -// WriteParser can be used to convert data to bytes. -type WriteParser interface { - // ToBytes returns a slice of bytes that represents the given value. - ToBytes(value dasel.Value, options ...ReadWriteOption) ([]byte, error) -} - -// Parser can be used to load and save files from/to disk. -type Parser interface { - ReadParser - WriteParser -} - -// NewReadParserFromFilename returns a ReadParser from the given filename. -func NewReadParserFromFilename(filename string) (ReadParser, error) { - ext := strings.ToLower(filepath.Ext(filename)) - p, ok := readParsersByExtension[ext] - if !ok { - return nil, &UnknownParserErr{Parser: ext} - } - return p, nil -} - -// NewReadParserFromString returns a ReadParser from the given parser name. -func NewReadParserFromString(parser string) (ReadParser, error) { - p, ok := readParsersByName[parser] - if !ok { - return nil, &UnknownParserErr{Parser: parser} - } - return p, nil -} - -// NewWriteParserFromFilename returns a WriteParser from the given filename. -func NewWriteParserFromFilename(filename string) (WriteParser, error) { - ext := strings.ToLower(filepath.Ext(filename)) - p, ok := writeParsersByExtension[ext] - if !ok { - return nil, &UnknownParserErr{Parser: ext} - } - return p, nil -} - -// NewWriteParserFromString returns a WriteParser from the given parser name. -func NewWriteParserFromString(parser string) (WriteParser, error) { - p, ok := writeParsersByName[parser] - if !ok { - return nil, &UnknownParserErr{Parser: parser} - } - return p, nil -} - -// LoadFromFile loads data from the given file. -func LoadFromFile(filename string, p ReadParser, options ...ReadWriteOption) (dasel.Value, error) { - f, err := os.Open(filename) - if err != nil { - return dasel.Value{}, fmt.Errorf("could not open file for reading: %w", err) - } - return Load(p, f, options...) -} - -// Load loads data from the given io.Reader. -func Load(p ReadParser, reader io.Reader, options ...ReadWriteOption) (dasel.Value, error) { - byteData, err := io.ReadAll(reader) - if err != nil { - return dasel.Value{}, fmt.Errorf("could not read data: %w", err) - } - return p.FromBytes(byteData, options...) -} - -// Write writes the value to the given io.Writer. -func Write(p WriteParser, value dasel.Value, writer io.Writer, options ...ReadWriteOption) error { - byteData, err := p.ToBytes(value, options...) - if err != nil { - return fmt.Errorf("could not get byte data for file: %w", err) - } - if _, err := writer.Write(byteData); err != nil { - return fmt.Errorf("could not write data: %w", err) - } - return nil -} diff --git a/storage/parser_test.go b/storage/parser_test.go deleted file mode 100644 index 307ccb31..00000000 --- a/storage/parser_test.go +++ /dev/null @@ -1,286 +0,0 @@ -package storage_test - -import ( - "bytes" - "errors" - "reflect" - "strings" - "testing" - - "github.com/tomwright/dasel/v3" - "github.com/tomwright/dasel/v3/dencoding" - "github.com/tomwright/dasel/v3/storage" -) - -func TestUnknownParserErr_Error(t *testing.T) { - if exp, got := "unknown parser: x", (&storage.UnknownParserErr{Parser: "x"}).Error(); exp != got { - t.Errorf("expected error %s, got %s", exp, got) - } -} - -func TestNewReadParserFromString(t *testing.T) { - tests := []struct { - In string - Out storage.Parser - Err error - }{ - {In: "json", Out: &storage.JSONParser{}}, - {In: "yaml", Out: &storage.YAMLParser{}}, - {In: "yml", Out: &storage.YAMLParser{}}, - {In: "toml", Out: &storage.TOMLParser{}}, - {In: "xml", Out: &storage.XMLParser{}}, - {In: "csv", Out: &storage.CSVParser{}}, - {In: "bad", Out: nil, Err: &storage.UnknownParserErr{Parser: "bad"}}, - {In: "-", Out: nil, Err: &storage.UnknownParserErr{Parser: "-"}}, - } - - for _, testCase := range tests { - tc := testCase - t.Run(tc.In, func(t *testing.T) { - got, err := storage.NewReadParserFromString(tc.In) - if tc.Err == nil && err != nil { - t.Errorf("expected err %v, got %v", tc.Err, err) - return - } - if tc.Err != nil && err == nil { - t.Errorf("expected err %v, got %v", tc.Err, err) - return - } - if tc.Err != nil && err != nil && err.Error() != tc.Err.Error() { - t.Errorf("expected err %v, got %v", tc.Err, err) - return - } - if tc.Out != got { - t.Errorf("expected result %v, got %v", tc.Out, got) - } - }) - } -} - -func TestNewWriteParserFromString(t *testing.T) { - tests := []struct { - In string - Out storage.Parser - Err error - }{ - {In: "json", Out: &storage.JSONParser{}}, - {In: "yaml", Out: &storage.YAMLParser{}}, - {In: "yml", Out: &storage.YAMLParser{}}, - {In: "toml", Out: &storage.TOMLParser{}}, - {In: "xml", Out: &storage.XMLParser{}}, - {In: "csv", Out: &storage.CSVParser{}}, - {In: "-", Out: &storage.PlainParser{}}, - {In: "plain", Out: &storage.PlainParser{}}, - {In: "bad", Out: nil, Err: &storage.UnknownParserErr{Parser: "bad"}}, - } - - for _, testCase := range tests { - tc := testCase - t.Run(tc.In, func(t *testing.T) { - got, err := storage.NewWriteParserFromString(tc.In) - if tc.Err == nil && err != nil { - t.Errorf("expected err %v, got %v", tc.Err, err) - return - } - if tc.Err != nil && err == nil { - t.Errorf("expected err %v, got %v", tc.Err, err) - return - } - if tc.Err != nil && err != nil && err.Error() != tc.Err.Error() { - t.Errorf("expected err %v, got %v", tc.Err, err) - return - } - if tc.Out != got { - t.Errorf("expected result %v, got %v", tc.Out, got) - } - }) - } -} - -func TestNewReadParserFromFilename(t *testing.T) { - tests := []struct { - In string - Out storage.Parser - Err error - }{ - {In: "a.json", Out: &storage.JSONParser{}}, - {In: "a.yaml", Out: &storage.YAMLParser{}}, - {In: "a.yml", Out: &storage.YAMLParser{}}, - {In: "a.toml", Out: &storage.TOMLParser{}}, - {In: "a.xml", Out: &storage.XMLParser{}}, - {In: "a.csv", Out: &storage.CSVParser{}}, - {In: "a.txt", Out: nil, Err: &storage.UnknownParserErr{Parser: ".txt"}}, - } - - for _, testCase := range tests { - tc := testCase - t.Run(tc.In, func(t *testing.T) { - got, err := storage.NewReadParserFromFilename(tc.In) - if tc.Err == nil && err != nil { - t.Errorf("expected err %v, got %v", tc.Err, err) - return - } - if tc.Err != nil && err == nil { - t.Errorf("expected err %v, got %v", tc.Err, err) - return - } - if tc.Err != nil && err != nil && err.Error() != tc.Err.Error() { - t.Errorf("expected err %v, got %v", tc.Err, err) - return - } - if tc.Out != got { - t.Errorf("expected result %v, got %v", tc.Out, got) - } - }) - } -} - -func TestNewWriteParserFromFilename(t *testing.T) { - tests := []struct { - In string - Out storage.Parser - Err error - }{ - {In: "a.json", Out: &storage.JSONParser{}}, - {In: "a.yaml", Out: &storage.YAMLParser{}}, - {In: "a.yml", Out: &storage.YAMLParser{}}, - {In: "a.toml", Out: &storage.TOMLParser{}}, - {In: "a.xml", Out: &storage.XMLParser{}}, - {In: "a.csv", Out: &storage.CSVParser{}}, - {In: "a.txt", Out: nil, Err: &storage.UnknownParserErr{Parser: ".txt"}}, - } - - for _, testCase := range tests { - tc := testCase - t.Run(tc.In, func(t *testing.T) { - got, err := storage.NewWriteParserFromFilename(tc.In) - if tc.Err == nil && err != nil { - t.Errorf("expected err %v, got %v", tc.Err, err) - return - } - if tc.Err != nil && err == nil { - t.Errorf("expected err %v, got %v", tc.Err, err) - return - } - if tc.Err != nil && err != nil && err.Error() != tc.Err.Error() { - t.Errorf("expected err %v, got %v", tc.Err, err) - return - } - if tc.Out != got { - t.Errorf("expected result %v, got %v", tc.Out, got) - } - }) - } -} - -var jsonData = dencoding.NewMap(). - Set("name", "Tom"). - Set("preferences", dencoding.NewMap(). - Set("favouriteColour", "red"), - ). - Set("colours", []any{"red", "green", "blue"}). - Set("colourCodes", []any{ - dencoding.NewMap(). - Set("name", "red"). - Set("rgb", "ff0000"), - dencoding.NewMap(). - Set("name", "green"). - Set("rgb", "00ff00"), - dencoding.NewMap(). - Set("name", "blue"). - Set("rgb", "0000ff"), - }) - -func TestLoadFromFile(t *testing.T) { - t.Run("ValidJSON", func(t *testing.T) { - data, err := storage.LoadFromFile("../tests/assets/example.json", &storage.JSONParser{}) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - exp := jsonData.KeyValues() - got := data.Interface().(*dencoding.Map).KeyValues() - if !reflect.DeepEqual(exp, got) { - t.Errorf("data does not match: exp %v, got %v", exp, got) - } - }) - t.Run("BaseFilePath", func(t *testing.T) { - _, err := storage.LoadFromFile("x.json", &storage.JSONParser{}) - if err == nil || !strings.Contains(err.Error(), "could not open file for reading") { - t.Errorf("unexpected error: %v", err) - return - } - }) -} - -func TestLoad(t *testing.T) { - t.Run("ReaderErrHandled", func(t *testing.T) { - if _, err := storage.Load(&storage.JSONParser{}, &failingReader{}); !errors.Is(err, errFailingReaderErr) { - t.Errorf("unexpected error: %v", err) - return - } - }) -} - -var errFailingParserErr = errors.New("i am meant to fail at parsing") - -type failingParser struct { -} - -func (fp *failingParser) FromBytes(_ []byte) (dasel.Value, error) { - return dasel.Value{}, errFailingParserErr -} - -func (fp *failingParser) ToBytes(_ dasel.Value, options ...storage.ReadWriteOption) ([]byte, error) { - return nil, errFailingParserErr -} - -var errFailingWriterErr = errors.New("i am meant to fail at writing") - -type failingWriter struct { -} - -func (fp *failingWriter) Write(_ []byte) (int, error) { - return 0, errFailingWriterErr -} - -var errFailingReaderErr = errors.New("i am meant to fail at reading") - -type failingReader struct { -} - -func (fp *failingReader) Read(_ []byte) (n int, err error) { - return 0, errFailingReaderErr -} - -func TestWrite(t *testing.T) { - t.Run("Success", func(t *testing.T) { - var buf bytes.Buffer - if err := storage.Write(&storage.JSONParser{}, dasel.ValueOf(map[string]interface{}{"name": "Tom"}), &buf); err != nil { - t.Errorf("unexpected error: %s", err) - return - } - - if exp, got := `{ - "name": "Tom" -} -`, buf.String(); exp != got { - t.Errorf("unexpected output:\n%s\ngot:\n%s", exp, got) - } - }) - - t.Run("ParserErrHandled", func(t *testing.T) { - var buf bytes.Buffer - if err := storage.Write(&failingParser{}, dasel.ValueOf(map[string]interface{}{"name": "Tom"}), &buf); !errors.Is(err, errFailingParserErr) { - t.Errorf("unexpected error: %v", err) - return - } - }) - - t.Run("WriterErrHandled", func(t *testing.T) { - if err := storage.Write(&storage.JSONParser{}, dasel.ValueOf(map[string]interface{}{"name": "Tom"}), &failingWriter{}); !errors.Is(err, errFailingWriterErr) { - t.Errorf("unexpected error: %v", err) - return - } - }) -} diff --git a/storage/plain.go b/storage/plain.go deleted file mode 100644 index 491aa9e0..00000000 --- a/storage/plain.go +++ /dev/null @@ -1,42 +0,0 @@ -package storage - -import ( - "bytes" - "fmt" - - "github.com/tomwright/dasel/v3" -) - -func init() { - registerWriteParser([]string{"-", "plain"}, []string{}, &PlainParser{}) -} - -// PlainParser is a Parser implementation to handle plain files. -type PlainParser struct { -} - -// ErrPlainParserNotImplemented is returned when you try to use the PlainParser.FromBytes func. -var ErrPlainParserNotImplemented = fmt.Errorf("PlainParser.FromBytes not implemented") - -// FromBytes returns some data that is represented by the given bytes. -func (p *PlainParser) FromBytes(byteData []byte, options ...ReadWriteOption) (dasel.Value, error) { - return dasel.Value{}, ErrPlainParserNotImplemented -} - -// ToBytes returns a slice of bytes that represents the given value. -func (p *PlainParser) ToBytes(value dasel.Value, options ...ReadWriteOption) ([]byte, error) { - buf := new(bytes.Buffer) - - switch { - case value.Metadata("isSingleDocument") == true: - buf.Write([]byte(fmt.Sprintf("%v\n", value.Interface()))) - case value.Metadata("isMultiDocument") == true: - for i := 0; i < value.Len(); i++ { - buf.Write([]byte(fmt.Sprintf("%v\n", value.Index(i).Interface()))) - } - default: - buf.Write([]byte(fmt.Sprintf("%v\n", value.Interface()))) - } - - return buf.Bytes(), nil -} diff --git a/storage/plain_test.go b/storage/plain_test.go deleted file mode 100644 index a6804731..00000000 --- a/storage/plain_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package storage_test - -import ( - "errors" - "testing" - - "github.com/tomwright/dasel/v3" - "github.com/tomwright/dasel/v3/storage" -) - -func TestPlainParser_FromBytes(t *testing.T) { - _, err := (&storage.PlainParser{}).FromBytes(nil) - if !errors.Is(err, storage.ErrPlainParserNotImplemented) { - t.Errorf("unexpected error: %v", err) - } -} - -func TestPlainParser_ToBytes(t *testing.T) { - t.Run("Basic", func(t *testing.T) { - gotVal, err := (&storage.PlainParser{}).ToBytes(dasel.ValueOf("asd")) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - exp := `asd -` - got := string(gotVal) - if exp != got { - t.Errorf("expected:\n%s\ngot:\n%s", exp, got) - } - }) - t.Run("SingleDocument", func(t *testing.T) { - gotVal, err := (&storage.PlainParser{}).ToBytes(dasel.ValueOf("asd").WithMetadata("isSingleDocument", true)) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - exp := `asd -` - got := string(gotVal) - if exp != got { - t.Errorf("expected:\n%s\ngot:\n%s", exp, got) - } - }) - t.Run("MultiDocument", func(t *testing.T) { - val := dasel.ValueOf([]interface{}{"asd", "123"}) - daselVal := dasel.ValueOf(val).WithMetadata("isMultiDocument", true) - - gotVal, err := (&storage.PlainParser{}).ToBytes(daselVal) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - exp := `asd -123 -` - got := string(gotVal) - if exp != got { - t.Errorf("expected:\n%s\ngot:\n%s", exp, got) - } - }) -} diff --git a/storage/toml.go b/storage/toml.go deleted file mode 100644 index 68eb9646..00000000 --- a/storage/toml.go +++ /dev/null @@ -1,99 +0,0 @@ -package storage - -import ( - "bytes" - "fmt" - "io" - - "github.com/tomwright/dasel/v3" - "github.com/tomwright/dasel/v3/dencoding" -) - -func init() { - registerReadParser([]string{"toml"}, []string{".toml"}, &TOMLParser{}) - registerWriteParser([]string{"toml"}, []string{".toml"}, &TOMLParser{}) -} - -// TOMLParser is a Parser implementation to handle toml files. -type TOMLParser struct { -} - -// FromBytes returns some data that is represented by the given bytes. -func (p *TOMLParser) FromBytes(byteData []byte, options ...ReadWriteOption) (dasel.Value, error) { - res := make([]interface{}, 0) - - decoder := dencoding.NewTOMLDecoder(bytes.NewReader(byteData)) - -docLoop: - for { - var docData interface{} - if err := decoder.Decode(&docData); err != nil { - if err == io.EOF { - break docLoop - } - return dasel.Value{}, fmt.Errorf("could not unmarshal data: %w", err) - } - - formattedDocData := cleanupYamlMapValue(docData) - - res = append(res, formattedDocData) - } - switch len(res) { - case 0: - return dasel.Value{}, nil - case 1: - return dasel.ValueOf(res[0]).WithMetadata("isSingleDocument", true), nil - default: - return dasel.ValueOf(res).WithMetadata("isMultiDocument", true), nil - } -} - -// ToBytes returns a slice of bytes that represents the given value. -func (p *TOMLParser) ToBytes(value dasel.Value, options ...ReadWriteOption) ([]byte, error) { - buffer := new(bytes.Buffer) - - colourise := false - - encoderOptions := make([]dencoding.TOMLEncoderOption, 0) - - for _, o := range options { - switch o.Key { - case OptionColourise: - if value, ok := o.Value.(bool); ok { - colourise = value - } - case OptionIndent: - if value, ok := o.Value.(string); ok { - encoderOptions = append(encoderOptions, dencoding.TOMLIndentSymbol(value)) - } - } - } - - encoder := dencoding.NewTOMLEncoder(buffer, encoderOptions...) - defer encoder.Close() - - switch { - case value.Metadata("isSingleDocument") == true: - if err := encoder.Encode(value.Interface()); err != nil { - return nil, fmt.Errorf("could not encode single document: %w", err) - } - case value.Metadata("isMultiDocument") == true: - for i := 0; i < value.Len(); i++ { - if err := encoder.Encode(value.Index(i).Interface()); err != nil { - return nil, fmt.Errorf("could not encode multi document [%d]: %w", i, err) - } - } - default: - if err := encoder.Encode(value.Interface()); err != nil { - return nil, fmt.Errorf("could not encode default document type: %w", err) - } - } - - if colourise { - if err := ColouriseBuffer(buffer, "toml"); err != nil { - return nil, fmt.Errorf("could not colourise output: %w", err) - } - } - - return buffer.Bytes(), nil -} diff --git a/storage/toml_test.go b/storage/toml_test.go deleted file mode 100644 index 0788cac4..00000000 --- a/storage/toml_test.go +++ /dev/null @@ -1,156 +0,0 @@ -package storage_test - -import ( - "reflect" - "strings" - "testing" - - "github.com/tomwright/dasel/v3" - "github.com/tomwright/dasel/v3/storage" -) - -var tomlBytes = []byte(`names = ['John', 'Frank'] - -[person] -name = 'Tom' -`) -var tomlMap = map[string]interface{}{ - "person": map[string]interface{}{ - "name": "Tom", - }, - "names": []interface{}{"John", "Frank"}, -} - -func TestTOMLParser_FromBytes(t *testing.T) { - t.Run("Valid", func(t *testing.T) { - got, err := (&storage.TOMLParser{}).FromBytes(tomlBytes) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - if !reflect.DeepEqual(tomlMap, got.Interface()) { - t.Errorf("expected %v, got %v", tomlMap, got) - } - }) - t.Run("Invalid", func(t *testing.T) { - _, err := (&storage.TOMLParser{}).FromBytes([]byte(`x:x`)) - if err == nil || !strings.Contains(err.Error(), "could not unmarshal data") { - t.Errorf("unexpected error: %v", err) - return - } - }) -} - -func TestTOMLParser_ToBytes(t *testing.T) { - t.Run("Default", func(t *testing.T) { - got, err := (&storage.TOMLParser{}).ToBytes(dasel.ValueOf(tomlMap)) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - if string(tomlBytes) != string(got) { - t.Errorf("expected:\n---\n%s\n---\ngot:\n---\n%s\n---", tomlBytes, got) - } - }) - t.Run("SingleDocument", func(t *testing.T) { - got, err := (&storage.TOMLParser{}).ToBytes(dasel.ValueOf(tomlMap).WithMetadata("isSingleDocument", true)) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - if string(tomlBytes) != string(got) { - t.Errorf("expected:\n%s\ngot:\n%s", tomlBytes, got) - } - }) - t.Run("SingleDocumentColourise", func(t *testing.T) { - got, err := (&storage.TOMLParser{}).ToBytes(dasel.ValueOf(tomlMap).WithMetadata("isSingleDocument", true), storage.ColouriseOption(true)) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - - expBuf, _ := storage.Colourise(string(tomlBytes), "toml") - exp := expBuf.Bytes() - if !reflect.DeepEqual(exp, got) { - t.Errorf("expected %v, got %v", string(exp), string(got)) - } - }) - t.Run("SingleDocumentCustomIndent", func(t *testing.T) { - res, err := (&storage.TOMLParser{}).ToBytes(dasel.ValueOf(tomlMap).WithMetadata("isSingleDocument", true), storage.IndentOption(" ")) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - got := string(res) - exp := `names = ['John', 'Frank'] - -[person] - name = 'Tom' -` - if exp != got { - t.Errorf("expected:\n%s\ngot:\n%s", exp, got) - } - }) - t.Run("MultiDocument", func(t *testing.T) { - got, err := (&storage.TOMLParser{}).ToBytes(dasel.ValueOf([]interface{}{tomlMap, tomlMap}).WithMetadata("isMultiDocument", true)) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - exp := append([]byte{}, tomlBytes...) - exp = append(exp, tomlBytes...) - if string(exp) != string(got) { - t.Errorf("expected:\n%s\ngot:\n%s", exp, got) - } - }) - t.Run("SingleDocumentValue", func(t *testing.T) { - got, err := (&storage.TOMLParser{}).ToBytes(dasel.ValueOf("asd")) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - exp := `'asd' -` - if exp != string(got) { - t.Errorf("expected:\n---\n%s\n---\ngot:\n---\n%s\n---", exp, got) - } - }) - t.Run("DefaultValue", func(t *testing.T) { - got, err := (&storage.TOMLParser{}).ToBytes(dasel.ValueOf("asd")) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - exp := `'asd' -` - if exp != string(got) { - t.Errorf("expected:\n%s\ngot:\n%s", exp, got) - } - }) - t.Run("MultiDocumentValue", func(t *testing.T) { - got, err := (&storage.TOMLParser{}).ToBytes(dasel.ValueOf([]interface{}{"asd", 123}).WithMetadata("isMultiDocument", true)) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - exp := `'asd' -123 -` - if exp != string(got) { - t.Errorf("expected:\n%s\ngot:\n%s", exp, got) - } - }) - // t.Run("time.Time", func(t *testing.T) { - // v, _ := time.Tokenize(time.RFC3339, "2022-01-02T12:34:56Z") - // got, err := (&storage.TOMLParser{}).ToBytes(dasel.ValueOf(v)) - // if err != nil { - // t.Errorf("unexpected error: %s", err) - // return - // } - // exp := `2022-01-02T12:34:56Z - // ` - // if exp != string(got) { - // t.Errorf("expected:\n%s\ngot:\n%s", exp, got) - // } - // }) -} diff --git a/storage/xml.go b/storage/xml.go deleted file mode 100644 index 390cbac5..00000000 --- a/storage/xml.go +++ /dev/null @@ -1,133 +0,0 @@ -package storage - -import ( - "bytes" - "fmt" - "strings" - - "github.com/tomwright/dasel/v3" - "github.com/tomwright/dasel/v3/dencoding" - - "github.com/clbanning/mxj/v2" - "golang.org/x/net/html/charset" -) - -func init() { - // Required for https://github.com/TomWright/dasel/issues/61 - mxj.XMLEscapeCharsDecoder(true) - - // Required for https://github.com/TomWright/dasel/issues/164 - mxj.XmlCharsetReader = charset.NewReaderLabel - - registerReadParser([]string{"xml"}, []string{".xml"}, &XMLParser{}) - registerWriteParser([]string{"xml"}, []string{".xml"}, &XMLParser{}) -} - -// XMLParser is a Parser implementation to handle xml files. -type XMLParser struct { -} - -// FromBytes returns some data that is represented by the given bytes. -func (p *XMLParser) FromBytes(byteData []byte, options ...ReadWriteOption) (dasel.Value, error) { - if byteData == nil { - return dasel.Value{}, fmt.Errorf("cannot parse nil xml data") - } - if len(byteData) == 0 || strings.TrimSpace(string(byteData)) == "" { - return dasel.Value{}, nil - } - data, err := mxj.NewMapXml(byteData) - if err != nil { - return dasel.Value{}, fmt.Errorf("could not unmarshal data: %w", err) - } - return dasel.ValueOf(map[string]interface{}(data)).WithMetadata("isSingleDocument", true), nil -} - -// ToBytes returns a slice of bytes that represents the given value. -func (p *XMLParser) ToBytes(value dasel.Value, options ...ReadWriteOption) ([]byte, error) { - buf := new(bytes.Buffer) - - prettyPrint := true - colourise := false - indent := " " - - for _, o := range options { - switch o.Key { - case OptionIndent: - if value, ok := o.Value.(string); ok { - indent = value - } - case OptionPrettyPrint: - if value, ok := o.Value.(bool); ok { - prettyPrint = value - } - case OptionColourise: - if value, ok := o.Value.(bool); ok { - colourise = value - } - } - } - - writeMap := func(val interface{}) error { - var m map[string]interface{} - - switch v := val.(type) { - case *dencoding.Map: - m = v.UnorderedData() - case map[string]any: - m = v - default: - _, err := buf.Write([]byte(fmt.Sprintf("%v\n", val))) - return err - } - - mv := mxj.New() - for k, v := range m { - mv[k] = v - } - - var byteData []byte - var err error - if prettyPrint { - byteData, err = mv.XmlIndent("", indent) - } else { - byteData, err = mv.Xml() - } - - if err != nil { - return err - } - buf.Write(byteData) - buf.Write([]byte("\n")) - return nil - } - - switch { - case value.Metadata("isSingleDocument") == true: - if err := writeMap(value.Interface()); err != nil { - return nil, err - } - case value.Metadata("isMultiDocument") == true: - for i := 0; i < value.Len(); i++ { - if err := writeMap(value.Index(i).Interface()); err != nil { - return nil, err - } - } - case value.IsDencodingMap(): - dm := value.Interface().(*dencoding.Map) - if err := writeMap(dm.UnorderedData()); err != nil { - return nil, err - } - default: - if err := writeMap(value.Interface()); err != nil { - return nil, err - } - } - - if colourise { - if err := ColouriseBuffer(buf, "xml"); err != nil { - return nil, fmt.Errorf("could not colourise output: %w", err) - } - } - - return buf.Bytes(), nil -} diff --git a/storage/xml_test.go b/storage/xml_test.go deleted file mode 100644 index 6cfe3c61..00000000 --- a/storage/xml_test.go +++ /dev/null @@ -1,245 +0,0 @@ -package storage_test - -import ( - "bytes" - "fmt" - "io" - "reflect" - "testing" - - "github.com/tomwright/dasel/v3" - "github.com/tomwright/dasel/v3/storage" - - "golang.org/x/text/encoding/charmap" - "golang.org/x/text/encoding/unicode" -) - -var xmlBytes = []byte(` - Tom - -`) -var xmlMap = map[string]interface{}{ - "user": map[string]interface{}{ - "name": "Tom", - }, -} -var encodedXmlMap = map[string]interface{}{ - "user": map[string]interface{}{ - "name": "Tõm", - }, -} - -func TestXMLParser_FromBytes(t *testing.T) { - got, err := (&storage.XMLParser{}).FromBytes(xmlBytes) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - if !reflect.DeepEqual(xmlMap, got.Interface()) { - t.Errorf("expected %v, got %v", xmlMap, got) - } -} - -func TestXMLParser_FromBytes_Empty(t *testing.T) { - got, err := (&storage.XMLParser{}).FromBytes([]byte{}) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - if !got.IsEmpty() { - t.Errorf("expected %v, got %v", nil, got) - } -} - -func TestXMLParser_FromBytes_Error(t *testing.T) { - _, err := (&storage.XMLParser{}).FromBytes(nil) - if err == nil { - t.Errorf("expected error but got none") - return - } - _, err = (&storage.XMLParser{}).FromBytes(yamlBytes) - if err == nil { - t.Errorf("expected error but got none") - return - } -} - -func TestXMLParser_ToBytes_Default(t *testing.T) { - got, err := (&storage.XMLParser{}).ToBytes(dasel.ValueOf(xmlMap)) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - if !reflect.DeepEqual(xmlBytes, got) { - t.Errorf("expected %v, got %v", string(xmlBytes), string(got)) - } -} -func TestXMLParser_ToBytes_SingleDocument(t *testing.T) { - got, err := (&storage.XMLParser{}).ToBytes(dasel.ValueOf(xmlMap).WithMetadata("isSingleDocument", true)) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - if !reflect.DeepEqual(xmlBytes, got) { - t.Errorf("expected %v, got %v", string(xmlBytes), string(got)) - } -} -func TestXMLParser_ToBytes_SingleDocument_Colourise(t *testing.T) { - got, err := (&storage.XMLParser{}).ToBytes(dasel.ValueOf(xmlMap).WithMetadata("isSingleDocument", true), storage.ColouriseOption(true)) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - - expBuf, _ := storage.Colourise(string(xmlBytes), "xml") - exp := expBuf.Bytes() - if !reflect.DeepEqual(exp, got) { - t.Errorf("expected %v, got %v", exp, got) - } -} -func TestXMLParser_ToBytes_MultiDocument(t *testing.T) { - got, err := (&storage.XMLParser{}).ToBytes(dasel.ValueOf([]interface{}{xmlMap, xmlMap}).WithMetadata("isMultiDocument", true)) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - exp := append([]byte{}, xmlBytes...) - exp = append(exp, xmlBytes...) - if !reflect.DeepEqual(exp, got) { - t.Errorf("expected %v, got %v", string(exp), string(got)) - } -} -func TestXMLParser_ToBytes_DefaultValue(t *testing.T) { - got, err := (&storage.XMLParser{}).ToBytes(dasel.ValueOf("asd")) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - exp := []byte(`asd -`) - if !reflect.DeepEqual(exp, got) { - t.Errorf("expected %v, got %v", string(exp), string(got)) - } -} -func TestXMLParser_ToBytes_SingleDocumentValue(t *testing.T) { - got, err := (&storage.XMLParser{}).ToBytes(dasel.ValueOf("asd")) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - exp := []byte(`asd -`) - if !reflect.DeepEqual(exp, got) { - t.Errorf("expected %v, got %v", string(exp), string(got)) - } -} -func TestXMLParser_ToBytes_MultiDocumentValue(t *testing.T) { - got, err := (&storage.XMLParser{}).ToBytes(dasel.ValueOf([]interface{}{"asd", "123"}).WithMetadata("isMultiDocument", true)) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - exp := []byte(`asd -123 -`) - if !reflect.DeepEqual(exp, got) { - t.Errorf("expected %v, got %v", string(exp), string(got)) - } -} -func TestXMLParser_ToBytes_Entities(t *testing.T) { - bytes := []byte(` - - sudo /home/fozz/RetroPie-Setup/retropie_packages.sh retropiemenu launch %ROM% </dev/tty >/dev/tty - .rp .sh - RetroPie - retropie - /home/fozz/RetroPie/retropiemenu - - retropie - - -`) - - p := &storage.XMLParser{} - var doc interface{} - - t.Run("FromBytes", func(t *testing.T) { - res, err := p.FromBytes(bytes) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - doc = res.Interface() - got := doc.(map[string]interface{})["systemList"].(map[string]interface{})["system"].(map[string]interface{})["command"] - exp := "sudo /home/fozz/RetroPie-Setup/retropie_packages.sh retropiemenu launch %ROM% </dev/tty >/dev/tty" - if exp != got { - t.Errorf("expected %s, got %s", exp, got) - } - }) - - t.Run("ToBytes", func(t *testing.T) { - gotBytes, err := p.ToBytes(dasel.ValueOf(doc)) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - got := string(gotBytes) - exp := string(bytes) - if exp != got { - t.Errorf("expected %s, got %s", exp, got) - } - }) -} - -func TestXMLParser_DifferentEncodings(t *testing.T) { - newXmlBytes := func(newWriter func(io.Writer) io.Writer, encoding, text string) []byte { - const encodedXmlBytesFmt = `` - const xmlBody = `%s` - - var buf bytes.Buffer - - w := newWriter(&buf) - fmt.Fprintf(w, xmlBody, text) - - return []byte(fmt.Sprintf(encodedXmlBytesFmt, encoding) + buf.String()) - } - - testCases := []struct { - name string - xml []byte - }{ - { - name: "supports ISO-8859-1", - xml: newXmlBytes(charmap.ISO8859_1.NewEncoder().Writer, "ISO-8859-1", "Tõm"), - }, - { - name: "supports UTF-8", - xml: newXmlBytes(unicode.UTF8.NewEncoder().Writer, "UTF-8", "Tõm"), - }, - { - name: "supports latin1", - xml: newXmlBytes(charmap.Windows1252.NewEncoder().Writer, "latin1", "Tõm"), - }, - { - name: "supports UTF-16", - xml: newXmlBytes(unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewEncoder().Writer, "UTF-16", "Tõm"), - }, - { - name: "supports UTF-16 (big endian)", - xml: newXmlBytes(unicode.UTF16(unicode.BigEndian, unicode.UseBOM).NewEncoder().Writer, "UTF-16BE", "Tõm"), - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - got, err := (&storage.XMLParser{}).FromBytes(tc.xml) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - if !reflect.DeepEqual(encodedXmlMap, got.Interface()) { - t.Errorf("expected %v, got %v", encodedXmlMap, got) - } - }) - } -} diff --git a/storage/yaml.go b/storage/yaml.go deleted file mode 100644 index 38c06094..00000000 --- a/storage/yaml.go +++ /dev/null @@ -1,129 +0,0 @@ -package storage - -import ( - "bytes" - "fmt" - "io" - - "github.com/tomwright/dasel/v3" - "github.com/tomwright/dasel/v3/dencoding" - "github.com/tomwright/dasel/v3/util" -) - -func init() { - registerReadParser([]string{"yaml", "yml"}, []string{".yaml", ".yml"}, &YAMLParser{}) - registerWriteParser([]string{"yaml", "yml"}, []string{".yaml", ".yml"}, &YAMLParser{}) -} - -// YAMLParser is a Parser implementation to handle yaml files. -type YAMLParser struct { -} - -// FromBytes returns some data that is represented by the given bytes. -func (p *YAMLParser) FromBytes(byteData []byte, options ...ReadWriteOption) (dasel.Value, error) { - res := make([]interface{}, 0) - - decoder := dencoding.NewYAMLDecoder(bytes.NewReader(byteData)) - -docLoop: - for { - var docData interface{} - if err := decoder.Decode(&docData); err != nil { - if err == io.EOF { - break docLoop - } - return dasel.Value{}, fmt.Errorf("could not unmarshal data: %w", err) - } - - formattedDocData := cleanupYamlMapValue(docData) - - res = append(res, formattedDocData) - } - switch len(res) { - case 0: - return dasel.Value{}, nil - case 1: - return dasel.ValueOf(res[0]).WithMetadata("isSingleDocument", true), nil - default: - return dasel.ValueOf(res).WithMetadata("isMultiDocument", true), nil - } -} - -func cleanupYamlInterfaceArray(in []interface{}) []interface{} { - res := make([]interface{}, len(in)) - for i, v := range in { - res[i] = cleanupYamlMapValue(v) - } - return res -} - -func cleanupYamlInterfaceMap(in map[interface{}]interface{}) map[string]interface{} { - res := make(map[string]interface{}) - for k, v := range in { - res[util.ToString(k)] = cleanupYamlMapValue(v) - } - return res -} - -func cleanupYamlMapValue(v interface{}) interface{} { - switch v := v.(type) { - case []interface{}: - return cleanupYamlInterfaceArray(v) - case map[interface{}]interface{}: - return cleanupYamlInterfaceMap(v) - case string: - return v - default: - return v - } -} - -// ToBytes returns a slice of bytes that represents the given value. -func (p *YAMLParser) ToBytes(value dasel.Value, options ...ReadWriteOption) ([]byte, error) { - buffer := new(bytes.Buffer) - - colourise := false - - encoderOptions := make([]dencoding.YAMLEncoderOption, 0) - - for _, o := range options { - switch o.Key { - case OptionColourise: - if value, ok := o.Value.(bool); ok { - colourise = value - } - case OptionIndent: - if value, ok := o.Value.(string); ok { - encoderOptions = append(encoderOptions, dencoding.YAMLEncodeIndent(len(value))) - } - } - } - - encoder := dencoding.NewYAMLEncoder(buffer, encoderOptions...) - defer encoder.Close() - - switch { - case value.Metadata("isSingleDocument") == true: - if err := encoder.Encode(value.Interface()); err != nil { - return nil, fmt.Errorf("could not encode single document: %w", err) - } - case value.Metadata("isMultiDocument") == true: - for i := 0; i < value.Len(); i++ { - if err := encoder.Encode(value.Index(i).Interface()); err != nil { - return nil, fmt.Errorf("could not encode multi document [%d]: %w", i, err) - } - } - default: - if err := encoder.Encode(value.Interface()); err != nil { - return nil, fmt.Errorf("could not encode default document type: %w", err) - } - } - - if colourise { - if err := ColouriseBuffer(buffer, "yaml"); err != nil { - return nil, fmt.Errorf("could not colourise output: %w", err) - } - } - - return buffer.Bytes(), nil -} diff --git a/storage/yaml_test.go b/storage/yaml_test.go deleted file mode 100644 index c12652d4..00000000 --- a/storage/yaml_test.go +++ /dev/null @@ -1,121 +0,0 @@ -package storage_test - -import ( - "reflect" - "strings" - "testing" - - "github.com/tomwright/dasel/v3" - "github.com/tomwright/dasel/v3/dencoding" - "github.com/tomwright/dasel/v3/storage" -) - -var yamlBytes = []byte(`name: Tom -numbers: - - 1 - - 2 -`) -var yamlMap = dencoding.NewMap(). - Set("name", "Tom"). - Set("numbers", []interface{}{ - int64(1), - int64(2), - }) - -var yamlBytesMulti = []byte(`name: Tom ---- -name: Jim -`) -var yamlMapMulti = []interface{}{ - dencoding.NewMap().Set("name", "Tom"), - dencoding.NewMap().Set("name", "Jim"), -} - -func TestYAMLParser_FromBytes(t *testing.T) { - t.Run("Valid", func(t *testing.T) { - gotFromBytes, err := (&storage.YAMLParser{}).FromBytes(yamlBytes) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - exp := yamlMap.KeyValues() - got := gotFromBytes.Interface().(*dencoding.Map).KeyValues() - if !reflect.DeepEqual(exp, got) { - t.Errorf("expected %v, got %v", exp, got) - } - }) - t.Run("ValidMultiDocument", func(t *testing.T) { - got, err := (&storage.YAMLParser{}).FromBytes(yamlBytesMulti) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - exp := yamlMapMulti - - if !reflect.DeepEqual(exp, got.Interface()) { - t.Errorf("expected %v, got %v", exp, got) - } - }) - t.Run("Invalid", func(t *testing.T) { - _, err := (&storage.YAMLParser{}).FromBytes([]byte(`{1:asd`)) - if err == nil || !strings.Contains(err.Error(), "could not unmarshal data") { - t.Errorf("unexpected error: %v", err) - return - } - }) - t.Run("Empty", func(t *testing.T) { - got, err := (&storage.YAMLParser{}).FromBytes([]byte(``)) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - if !reflect.DeepEqual(dasel.Value{}, got) { - t.Errorf("expected %v, got %v", nil, got) - } - }) -} - -func TestYAMLParser_ToBytes(t *testing.T) { - t.Run("Valid", func(t *testing.T) { - got, err := (&storage.YAMLParser{}).ToBytes(dasel.ValueOf(yamlMap)) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - if string(yamlBytes) != string(got) { - t.Errorf("expected %s, got %s", yamlBytes, got) - } - }) - t.Run("ValidSingle", func(t *testing.T) { - got, err := (&storage.YAMLParser{}).ToBytes(dasel.ValueOf(yamlMap).WithMetadata("isSingleDocument", true)) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - if string(yamlBytes) != string(got) { - t.Errorf("expected %s, got %s", yamlBytes, got) - } - }) - t.Run("ValidSingleColourise", func(t *testing.T) { - got, err := (&storage.YAMLParser{}).ToBytes(dasel.ValueOf(yamlMap).WithMetadata("isSingleDocument", true), storage.ColouriseOption(true)) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - expBuf, _ := storage.Colourise(string(yamlBytes), "yaml") - exp := expBuf.Bytes() - if !reflect.DeepEqual(exp, got) { - t.Errorf("expected %v, got %v", exp, got) - } - }) - t.Run("ValidMulti", func(t *testing.T) { - got, err := (&storage.YAMLParser{}).ToBytes(dasel.ValueOf(yamlMapMulti).WithMetadata("isMultiDocument", true)) - if err != nil { - t.Errorf("unexpected error: %s", err) - return - } - if string(yamlBytesMulti) != string(got) { - t.Errorf("expected %s, got %s", yamlBytesMulti, got) - } - }) -} diff --git a/tests/assets/broken.json b/tests/assets/broken.json deleted file mode 100644 index 0c6ec1f5..00000000 --- a/tests/assets/broken.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "Tom", - "preferences": { - "favouriteColour": "red" - }, - "colours": [ - "red", - "green" -} \ No newline at end of file diff --git a/tests/assets/broken.xml b/tests/assets/broken.xml deleted file mode 100644 index a0b0dbab..00000000 --- a/tests/assets/broken.xml +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/tests/assets/deployment.yaml b/tests/assets/deployment.yaml deleted file mode 100644 index 8a102bc3..00000000 --- a/tests/assets/deployment.yaml +++ /dev/null @@ -1,30 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: auth-deployment -spec: - replicas: 3 - selector: - matchLabels: - component: auth - template: - metadata: - labels: - component: auth - spec: - containers: - - env: - - name: BUSINESS_SERVICE - value: business-cluster-ip:9000 - - name: PASSWORD - valueFrom: - secretKeyRef: - key: pgpassword - name: PGPASSWORD - - name: MY_NEW_ENV_VAR - value: NEW_VALUE - image: tomwright/auth:dev - name: auth - ports: - - containerPort: 9000 - - containerPort: 8000 diff --git a/tests/assets/example.json b/tests/assets/example.json deleted file mode 100644 index 845a5bbc..00000000 --- a/tests/assets/example.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "Tom", - "preferences": { - "favouriteColour": "red" - }, - "colours": [ - "red", - "green", - "blue" - ], - "colourCodes": [ - { - "name": "red", - "rgb": "ff0000" - }, - { - "name": "green", - "rgb": "00ff00" - }, - { - "name": "blue", - "rgb": "0000ff" - } - ] -} \ No newline at end of file diff --git a/tests/assets/example.xml b/tests/assets/example.xml deleted file mode 100644 index 5da9860b..00000000 --- a/tests/assets/example.xml +++ /dev/null @@ -1,21 +0,0 @@ - - Tom - - red - - red - green - blue - - red - ff0000 - - - green - 00ff00 - - - blue - 0000ff - - \ No newline at end of file diff --git a/tests/assets/example.yaml b/tests/assets/example.yaml deleted file mode 100644 index a4ceb131..00000000 --- a/tests/assets/example.yaml +++ /dev/null @@ -1,14 +0,0 @@ -name: Tom -preferences: - favouriteColour: red -colours: - - red - - green - - blue -colourCodes: - - name: red - rgb: ff0000 - - name: green - rgb: 00ff00 - - name: blue - rgb: 0000ff \ No newline at end of file diff --git a/tests/assets/int-value.txt b/tests/assets/int-value.txt deleted file mode 100644 index bd41cba7..00000000 --- a/tests/assets/int-value.txt +++ /dev/null @@ -1 +0,0 @@ -12345 \ No newline at end of file diff --git a/tests/assets/json-value.json b/tests/assets/json-value.json deleted file mode 100644 index 89634894..00000000 --- a/tests/assets/json-value.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "this": "is a value" -} \ No newline at end of file diff --git a/tests/assets/string-value.txt b/tests/assets/string-value.txt deleted file mode 100644 index 8b94090c..00000000 --- a/tests/assets/string-value.txt +++ /dev/null @@ -1 +0,0 @@ -This is a string value \ No newline at end of file diff --git a/truthy.go b/truthy.go deleted file mode 100644 index 043e657b..00000000 --- a/truthy.go +++ /dev/null @@ -1,61 +0,0 @@ -package dasel - -import ( - "reflect" - "strings" -) - -func IsTruthy(value interface{}) bool { - switch v := value.(type) { - case Value: - return IsTruthy(v.Unpack().Interface()) - - case reflect.Value: - return IsTruthy(unpackReflectValue(v).Interface()) - - case bool: - return v - - case string: - v = strings.ToLower(strings.TrimSpace(v)) - switch v { - case "false", "no", "0": - return false - default: - return v != "" - } - - case []byte: - return IsTruthy(string(v)) - - case int: - return v > 0 - case int8: - return v > 0 - case int16: - return v > 0 - case int32: - return v > 0 - case int64: - return v > 0 - - case uint: - return v > 0 - case uint8: - return v > 0 - case uint16: - return v > 0 - case uint32: - return v > 0 - case uint64: - return v > 0 - - case float32: - return v >= 1 - case float64: - return v >= 1 - - default: - return false - } -} diff --git a/truthy_test.go b/truthy_test.go deleted file mode 100644 index 8bca5227..00000000 --- a/truthy_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package dasel - -import ( - "fmt" - "reflect" - "testing" -) - -func TestIsTruthy(t *testing.T) { - - type testDef struct { - name string - in interface{} - out bool - } - - baseData := []testDef{ - {"bool:true", true, true}, - {"bool:false", false, false}, - {"string:lowercaseTrue", "true", true}, - {"string:lowercaseFalse", "false", false}, - {"string:uppercaseTrue", "TRUE", true}, - {"string:uppercaseFalse", "FALSE", false}, - {"string:lowercaseYes", "yes", true}, - {"string:lowercaseNo", "no", false}, - {"string:uppercaseYes", "YES", true}, - {"string:lowercaseNo", "NO", false}, - {"[]byte:lowercaseTrue", []byte("true"), true}, - {"[]byte:lowercaseFalse", []byte("false"), false}, - {"[]byte:uppercaseTrue", []byte("TRUE"), true}, - {"[]byte:uppercaseFalse", []byte("FALSE"), false}, - {"[]byte:lowercaseYes", []byte("yes"), true}, - {"[]byte:lowercaseNo", []byte("no"), false}, - {"[]byte:uppercaseYes", []byte("YES"), true}, - {"[]byte:lowercaseNo", []byte("NO"), false}, - {"int:0", int(0), false}, - {"int8:0", int8(0), false}, - {"int16:0", int16(0), false}, - {"int32:0", int32(0), false}, - {"int64:0", int64(0), false}, - {"int:-1", int(-1), false}, - {"int8:-1", int8(-1), false}, - {"int16:-1", int16(-1), false}, - {"int32:-1", int32(-1), false}, - {"int64:-1", int64(-1), false}, - {"uint:0", uint(0), false}, - {"uint8:0", uint8(0), false}, - {"uint16:0", uint16(0), false}, - {"uint32:0", uint32(0), false}, - {"uint64:0", uint64(0), false}, - {"int:1", int(1), true}, - {"int8:1", int8(1), true}, - {"int16:1", int16(1), true}, - {"int32:1", int32(1), true}, - {"int64:1", int64(1), true}, - {"uint:1", uint(1), true}, - {"uint8:1", uint8(1), true}, - {"uint16:1", uint16(1), true}, - {"uint32:1", uint32(1), true}, - {"uint64:1", uint64(1), true}, - {"float32:0", float32(0), false}, - {"float64:0", float64(0), false}, - {"float32:-1", float32(-1), false}, - {"float64:-1", float64(-1), false}, - {"float32:1", float32(1), true}, - {"float64:1", float64(1), true}, - {"unhandled:[]string", []string{}, false}, - } - - testData := make([]testDef, 0) - - for _, td := range baseData { - testData = append( - testData, - td, - testDef{ - name: fmt.Sprintf("reflect.Value:%s", td.name), - in: reflect.ValueOf(td.in), - out: td.out, - }, - testDef{ - name: fmt.Sprintf("dasel.Value:%s", td.name), - in: ValueOf(td.in), - out: td.out, - }, - ) - } - - for _, test := range testData { - tc := test - t.Run(tc.name, func(t *testing.T) { - if exp, got := tc.out, IsTruthy(tc.in); exp != got { - t.Errorf("expected %v, got %v", exp, got) - } - }) - } -} diff --git a/value.go b/value.go deleted file mode 100644 index 2dee6994..00000000 --- a/value.go +++ /dev/null @@ -1,544 +0,0 @@ -package dasel - -import ( - "reflect" - - "github.com/tomwright/dasel/v3/dencoding" - "github.com/tomwright/dasel/v3/util" -) - -// Value is a wrapper around reflect.Value that adds some handy helper funcs. -type Value struct { - reflect.Value - setFn func(value Value) - deleteFn func() - metadata map[string]interface{} -} - -// ValueOf wraps value in a Value. -func ValueOf(value interface{}) Value { - switch v := value.(type) { - case Value: - return v - case reflect.Value: - return Value{ - Value: v, - } - default: - return Value{ - Value: reflect.ValueOf(value), - } - } -} - -// Metadata returns the metadata with a key of key for v. -func (v Value) Metadata(key string) interface{} { - if v.metadata == nil { - return nil - } - if m, ok := v.metadata[key]; ok { - return m - } - return nil -} - -// WithMetadata sets the given value into the values metadata. -func (v Value) WithMetadata(key string, value interface{}) Value { - if v.metadata == nil { - v.metadata = map[string]interface{}{} - } - v.metadata[key] = value - return v -} - -// Interface returns the interface{} value of v. -func (v Value) Interface() interface{} { - return v.Unpack().Interface() -} - -// Len returns v's length. -func (v Value) Len() int { - if v.IsDencodingMap() { - return len(v.Interface().(*dencoding.Map).Keys()) - } - switch v.Kind() { - case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String: - return v.Unpack().Len() - case reflect.Bool: - if v.Interface() == true { - return 1 - } else { - return 0 - } - default: - return len(util.ToString(v.Interface())) - } -} - -// String returns the string v's underlying value, as a string. -func (v Value) String() string { - return v.Unpack().String() -} - -// IsEmpty returns true is v represents an empty reflect.Value. -func (v Value) IsEmpty() bool { - return isEmptyReflectValue(unpackReflectValue(v.Value)) -} - -func (v Value) IsNil() bool { - switch v.Kind() { - case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: - return v.Value.IsNil() - default: - return false - } -} - -func isEmptyReflectValue(v reflect.Value) bool { - if (v == reflect.Value{}) { - return true - } - return v.Kind() == reflect.String && v.Interface() == UninitialisedPlaceholder -} - -// Kind returns the underlying type of v. -func (v Value) Kind() reflect.Kind { - return v.Unpack().Kind() -} - -func containsKind(kinds []reflect.Kind, kind reflect.Kind) bool { - for _, v := range kinds { - if v == kind { - return true - } - } - return false -} - -var dencodingMapType = reflect.TypeOf(&dencoding.Map{}) - -func isDencodingMap(value reflect.Value) bool { - return value.Kind() == reflect.Ptr && value.Type() == dencodingMapType -} - -func unpackReflectValue(value reflect.Value, kinds ...reflect.Kind) reflect.Value { - if len(kinds) == 0 { - kinds = append(kinds, reflect.Ptr, reflect.Interface) - } - res := value - for { - if isDencodingMap(res) { - return res - } - if !containsKind(kinds, res.Kind()) { - return res - } - if res.IsNil() { - return res - } - res = res.Elem() - } -} - -func (v Value) FirstAddressable() reflect.Value { - res := v.Value - for !res.CanAddr() { - res = res.Elem() - } - return res -} - -// Unpack returns the underlying reflect.Value after resolving any pointers or interface types. -func (v Value) Unpack(kinds ...reflect.Kind) reflect.Value { - if !v.Value.IsValid() { - return reflect.ValueOf(new(any)).Elem() - } - return unpackReflectValue(v.Value, kinds...) -} - -func (v Value) Type() reflect.Type { - return v.Unpack().Type() -} - -// Set sets underlying value of v. -// Depends on setFn since the implementation can differ depending on how the Value was initialised. -func (v Value) Set(value Value) { - if v.setFn != nil { - v.setFn(value) - return - } - panic("unable to set value with missing setFn") -} - -// Delete deletes the current element. -// Depends on deleteFn since the implementation can differ depending on how the Value was initialised. -func (v Value) Delete() { - if v.deleteFn != nil { - v.deleteFn() - return - } - panic("unable to delete value with missing deleteFn") -} - -func (v Value) IsDencodingMap() bool { - if v.Kind() != reflect.Ptr { - return false - } - _, ok := v.Interface().(*dencoding.Map) - return ok -} - -func (v Value) dencodingMapIndex(key Value) Value { - getValueByKey := func() reflect.Value { - if !v.IsDencodingMap() { - return reflect.Value{} - } - om := v.Interface().(*dencoding.Map) - if v, ok := om.Get(key.Value.String()); !ok { - return reflect.Value{} - } else { - if v == nil { - return reflect.ValueOf(new(any)).Elem() - } - return reflect.ValueOf(v) - } - } - index := Value{ - Value: getValueByKey(), - setFn: func(value Value) { - // Note that we do not use Interface() here as it will dereference the received value. - // Instead, we only dereference the interface type to receive the pointer. - v.Interface().(*dencoding.Map).Set(key.Value.String(), value.Unpack(reflect.Interface).Interface()) - }, - deleteFn: func() { - v.Interface().(*dencoding.Map).Delete(key.Value.String()) - }, - } - return index. - WithMetadata("key", key.Interface()). - WithMetadata("parent", v) -} - -// MapIndex returns the value associated with key in the map v. -// It returns the zero Value if no field was found. -func (v Value) MapIndex(key Value) Value { - index := Value{ - Value: v.Unpack().MapIndex(key.Value), - setFn: func(value Value) { - v.Unpack().SetMapIndex(key.Value, value.Value) - }, - deleteFn: func() { - v.Unpack().SetMapIndex(key.Value, reflect.Value{}) - }, - } - return index. - WithMetadata("key", key.Interface()). - WithMetadata("parent", v) -} - -func (v Value) MapKeys() []Value { - res := make([]Value, 0) - for _, k := range v.Unpack().MapKeys() { - res = append(res, Value{Value: k}) - } - return res -} - -// FieldByName returns the struct field with the given name. -// It returns the zero Value if no field was found. -func (v Value) FieldByName(name string) Value { - return Value{ - Value: v.Unpack().FieldByName(name), - setFn: func(value Value) { - v.Unpack().FieldByName(name).Set(value.Value) - }, - deleteFn: func() { - field := v.Unpack().FieldByName(name) - field.Set(reflect.New(field.Type())) - }, - }. - WithMetadata("key", name). - WithMetadata("parent", v) -} - -// NumField returns the number of fields in the struct v. -func (v Value) NumField() int { - return v.Unpack().NumField() -} - -// Index returns v's i'th element. -// It panics if v's Kind is not Array, Slice, or String or i is out of range. -func (v Value) Index(i int) Value { - return Value{ - Value: v.Unpack().Index(i), - setFn: func(value Value) { - v.Unpack().Index(i).Set(value.Value) - }, - deleteFn: func() { - currentLen := v.Len() - updatedSlice := reflect.MakeSlice(sliceInterfaceType, currentLen-1, v.Len()-1) - // Rebuild the slice excluding the deleted element - for indexToRead := 0; indexToRead < currentLen; indexToRead++ { - indexToWrite := indexToRead - if indexToRead == i { - continue - } - if indexToRead > i { - indexToWrite-- - } - updatedSlice.Index(indexToWrite).Set( - v.Index(indexToRead).Value, - ) - } - - v.Unpack().Set(updatedSlice) - }, - }. - WithMetadata("key", i). - WithMetadata("parent", v) -} - -// Append appends an empty value to the end of the slice. -func (v Value) Append() Value { - currentLen := v.Len() - newLen := currentLen + 1 - - updatedSlice := reflect.MakeSlice(reflect.TypeOf(v.Interface()), newLen, newLen) - // copy all existing elements into updatedSlice. - // this leaves the last element empty. - for i := 0; i < currentLen; i++ { - updatedSlice.Index(i).Set( - v.Index(i).Value, - ) - } - - firstAddressable := v.FirstAddressable() - firstAddressable.Set(updatedSlice) - - // This code was causing a panic... - // It doesn't seem necessary. Leaving here for reference in-case it was needed. - // See https://github.com/TomWright/dasel/issues/392 - // Set the last element to uninitialised. - //updatedSlice.Index(currentLen).Set( - // v.Index(currentLen).asUninitialised().Value, - //) - - return v -} - -var sliceInterfaceType = reflect.TypeFor[[]any]() -var mapStringInterfaceType = reflect.TypeFor[map[string]interface{}]() - -var UninitialisedPlaceholder interface{} = "__dasel_not_found__" - -func (v Value) asUninitialised() Value { - v.Value = reflect.ValueOf(UninitialisedPlaceholder) - return v -} - -func (v Value) initEmptyMap() Value { - emptyMap := reflect.MakeMap(mapStringInterfaceType) - v.Set(Value{Value: emptyMap}) - v.Value = emptyMap - return v -} - -func (v Value) initEmptydencodingMap() Value { - om := dencoding.NewMap() - rom := reflect.ValueOf(om) - v.Set(Value{Value: rom}) - v.Value = rom - return v -} - -func (v Value) initEmptySlice() Value { - emptySlice := reflect.MakeSlice(sliceInterfaceType, 0, 0) - - addressableSlice := reflect.New(emptySlice.Type()) - addressableSlice.Elem().Set(emptySlice) - - v.Set(Value{Value: addressableSlice}) - v.Value = addressableSlice - return v -} - -func makeAddressableSlice(value reflect.Value) reflect.Value { - if !unpackReflectValue(value, reflect.Ptr).CanAddr() { - unpacked := unpackReflectValue(value) - - emptySlice := reflect.MakeSlice(unpacked.Type(), unpacked.Len(), unpacked.Len()) - - for i := 0; i < unpacked.Len(); i++ { - emptySlice.Index(i).Set(makeAddressable(unpacked.Index(i))) - } - - addressableSlice := reflect.New(emptySlice.Type()) - addressableSlice.Elem().Set(emptySlice) - - return addressableSlice - } else { - // Make contained values addressable - unpacked := unpackReflectValue(value) - for i := 0; i < unpacked.Len(); i++ { - unpacked.Index(i).Set(makeAddressable(unpacked.Index(i))) - } - - return value - } -} - -func makeAddressableMap(value reflect.Value) reflect.Value { - if !unpackReflectValue(value, reflect.Ptr).CanAddr() { - unpacked := unpackReflectValue(value) - - emptyMap := reflect.MakeMap(unpacked.Type()) - - for _, key := range unpacked.MapKeys() { - emptyMap.SetMapIndex(key, makeAddressable(unpacked.MapIndex(key))) - } - - addressableMap := reflect.New(emptyMap.Type()) - addressableMap.Elem().Set(emptyMap) - - return addressableMap - } else { - // Make contained values addressable - unpacked := unpackReflectValue(value) - - for _, key := range unpacked.MapKeys() { - unpacked.SetMapIndex(key, makeAddressable(unpacked.MapIndex(key))) - } - - return value - } -} - -func makeAddressable(value reflect.Value) reflect.Value { - unpacked := unpackReflectValue(value) - - if isDencodingMap(unpacked) { - om := value.Interface().(*dencoding.Map) - for _, kv := range om.KeyValues() { - var val any - if v := deref(reflect.ValueOf(kv.Value)); v.IsValid() { - val = makeAddressable(v).Interface() - } else { - val = nil - } - om.Set(kv.Key, val) - } - return value - } - - switch unpacked.Kind() { - case reflect.Slice: - return makeAddressableSlice(value) - case reflect.Map: - return makeAddressableMap(value) - default: - return value - } -} - -func derefSlice(value reflect.Value) reflect.Value { - unpacked := unpackReflectValue(value) - - res := reflect.MakeSlice(unpacked.Type(), unpacked.Len(), unpacked.Len()) - - for i := 0; i < unpacked.Len(); i++ { - if v := deref(unpacked.Index(i)); v.IsValid() { - res.Index(i).Set(v) - } - } - - return res -} - -func derefMap(value reflect.Value) reflect.Value { - unpacked := unpackReflectValue(value) - - res := reflect.MakeMap(unpacked.Type()) - - for _, key := range unpacked.MapKeys() { - if v := deref(unpacked.MapIndex(key)); v.IsValid() { - res.SetMapIndex(key, v) - } else { - res.SetMapIndex(key, reflect.ValueOf(new(any))) - } - } - - return res -} - -func deref(value reflect.Value) reflect.Value { - unpacked := unpackReflectValue(value) - - if isDencodingMap(unpacked) { - om := value.Interface().(*dencoding.Map) - for _, kv := range om.KeyValues() { - if v := deref(reflect.ValueOf(kv.Value)); v.IsValid() { - om.Set(kv.Key, v.Interface()) - } else { - om.Set(kv.Key, nil) - } - } - return value - } - - switch unpacked.Kind() { - case reflect.Slice: - return derefSlice(value) - case reflect.Map: - return derefMap(value) - default: - return unpackReflectValue(value) - } -} - -// Values represents a list of Value's. -type Values []Value - -// Interfaces returns the interface values for the underlying values stored in v. -func (v Values) Interfaces() []interface{} { - res := make([]interface{}, 0) - for _, val := range v { - res = append(res, val.Interface()) - } - return res -} - -//func (v Values) initEmptyMaps() Values { -// res := make(Values, len(v)) -// for k, value := range v { -// if value.IsEmpty() { -// res[k] = value.initEmptyMap() -// } else { -// res[k] = value -// } -// } -// return res -//} - -func (v Values) initEmptydencodingMaps() Values { - res := make(Values, len(v)) - for k, value := range v { - if value.IsEmpty() || value.IsNil() { - res[k] = value.initEmptydencodingMap() - } else { - res[k] = value - } - } - return res -} - -func (v Values) initEmptySlices() Values { - res := make(Values, len(v)) - for k, value := range v { - if value.IsEmpty() || value.IsNil() { - res[k] = value.initEmptySlice() - } else { - res[k] = value - } - } - return res -} From f8a5e272041d04dc1cf2076956f1cff16cbf83b1 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 7 Oct 2024 23:02:27 +0100 Subject: [PATCH 21/56] Add initial branch support --- api.go | 29 +++++++++++- api_test.go | 32 +++++++++++++ execution/execute.go | 25 ++++++++-- execution/execute_branch.go | 28 +++++++++++ execution/execute_test.go | 40 ++++++++++++++++ internal/cli/generic_test.go | 16 +++---- model/value.go | 2 +- model/value_metadata.go | 18 +++++++ model/value_set.go | 21 +++++++++ selector/ast/expression_complex.go | 12 +++++ selector/lexer/token.go | 5 +- selector/lexer/tokenize.go | 31 ++++++++---- selector/parser/error.go | 2 +- selector/parser/parse_array.go | 5 -- selector/parser/parse_func.go | 3 -- selector/parser/parse_group.go | 2 - selector/parser/parse_if.go | 3 -- selector/parser/parse_map.go | 36 ++++++++++---- selector/parser/parse_object.go | 3 -- selector/parser/parse_symbol.go | 4 -- selector/parser/parser.go | 76 +++--------------------------- selector/parser/parser_test.go | 23 +++++++++ 22 files changed, 293 insertions(+), 123 deletions(-) create mode 100644 api_test.go create mode 100644 execution/execute_branch.go create mode 100644 model/value_set.go diff --git a/api.go b/api.go index 78ae5cf2..e34cae15 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 00000000..f2c9550e --- /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 8047c808..a238d053 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 00000000..d64e61a1 --- /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 28a3c1f4..57b22657 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 cf3fb4da..cfc26227 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 76edb7f2..b86259da 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 a939c584..bed68b80 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 00000000..5cfe3d99 --- /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 4c3440e0..009c8d0d 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 4ce78e8e..66379ae7 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 e4ef7909..fd601500 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 89903945..e3611b35 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 1403bd1e..cd018e4f 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 59b48e9d..a8fa34b2 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 9181f3e2..923ee838 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 41b7a60b..cfc2c314 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 b8e5062f..76334bfa 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 9b47cae3..03f78f54 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 74ad4cbb..dc56cc11 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 7e6c3a27..7524dab2 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 e9fef233..f1ec6ccd 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"`, From 0a7aee3840d08ca9c16ba417fcb6957bb3836a59 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 8 Oct 2024 00:04:00 +0100 Subject: [PATCH 22/56] Add array support --- api_test.go | 13 +++--- execution/execute.go | 2 + execution/execute_array.go | 18 +++++++ execution/execute_test.go | 60 ++++++++++++++++++++++-- model/value.go | 14 ++++++ model/value_set.go | 9 +++- selector/ast/expression_complex.go | 6 +++ selector/lexer/token.go | 4 ++ selector/parser/parse_array.go | 27 +++++++++-- selector/parser/parse_object.go | 2 +- selector/parser/parse_symbol.go | 2 +- selector/parser/parse_variable.go | 2 +- selector/parser/parser.go | 2 +- selector/parser/parser_test.go | 75 +++++++++++++++++++----------- 14 files changed, 190 insertions(+), 46 deletions(-) diff --git a/api_test.go b/api_test.go index f2c9550e..a384f079 100644 --- a/api_test.go +++ b/api_test.go @@ -23,10 +23,11 @@ func (tc modifyTestCase) run(t *testing.T) { } 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) + // TODO : get this working + //t.Run("int over int", modifyTestCase{ + // selector: "$this[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 a238d053..3170df4e 100644 --- a/execution/execute.go +++ b/execution/execute.go @@ -97,6 +97,8 @@ func exprExecutor(expr ast.Expr) (expressionExecutor, error) { return conditionalExprExecutor(e) case ast.BranchExpr: return branchExprExecutor(e) + case ast.ArrayExpr: + return arrayExprExecutor(e) default: return nil, fmt.Errorf("unhandled expression type: %T", e) } diff --git a/execution/execute_array.go b/execution/execute_array.go index f371b2f1..b4b9b239 100644 --- a/execution/execute_array.go +++ b/execution/execute_array.go @@ -7,6 +7,24 @@ import ( "github.com/tomwright/dasel/v3/selector/ast" ) +func arrayExprExecutor(e ast.ArrayExpr) (expressionExecutor, error) { + return func(data *model.Value) (*model.Value, error) { + res := model.NewSliceValue() + + for _, expr := range e.Exprs { + el, err := ExecuteAST(expr, data) + if err != nil { + return nil, err + } + if err := res.Append(el); err != nil { + return nil, err + } + } + + return res, nil + }, nil +} + func rangeExprExecutor(e ast.RangeExpr) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { var start, end int64 = -1, -1 diff --git a/execution/execute_test.go b/execution/execute_test.go index 57b22657..d46e0475 100644 --- a/execution/execute_test.go +++ b/execution/execute_test.go @@ -239,6 +239,56 @@ func TestExecuteSelector_HappyPath(t *testing.T) { s: `false`, out: model.NewBoolValue(false), })) + t.Run("empty array", runTest(testCase{ + s: `[]`, + outFn: func() *model.Value { + r := model.NewSliceValue() + return r + }, + })) + t.Run("array with one element", runTest(testCase{ + s: `[1]`, + outFn: func() *model.Value { + r := model.NewSliceValue() + if err := r.Append(model.NewIntValue(1)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + return r + }, + })) + t.Run("array with many elements", runTest(testCase{ + s: `[1, 2.2, "foo", true, [1, 2, 3]]`, + outFn: func() *model.Value { + nested := model.NewSliceValue() + if err := nested.Append(model.NewIntValue(1)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := nested.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := nested.Append(model.NewIntValue(3)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + r := model.NewSliceValue() + if err := r.Append(model.NewIntValue(1)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := r.Append(model.NewFloatValue(2.2)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := r.Append(model.NewStringValue("foo")); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := r.Append(model.NewBoolValue(true)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := r.Append(nested); err != nil { + t.Fatalf("unexpected error: %v", err) + } + return r + }, + })) }) t.Run("function", func(t *testing.T) { @@ -321,7 +371,7 @@ func TestExecuteSelector_HappyPath(t *testing.T) { })) t.Run("add evaluated fields", runTest(testCase{ in: inputMap(), - s: `{..., over30 = age > 30}`, + s: `{..., "over30": age > 30}`, outFn: func() *model.Value { return model.NewValue( dencoding.NewMap(). @@ -376,7 +426,7 @@ func TestExecuteSelector_HappyPath(t *testing.T) { })) t.Run("set", runTest(testCase{ in: inputMap(), - s: `{title="Mrs"}`, + s: `{title:"Mrs"}`, outFn: func() *model.Value { res := model.NewMapValue() _ = res.SetMapKey("title", model.NewStringValue("Mrs")) @@ -385,7 +435,7 @@ func TestExecuteSelector_HappyPath(t *testing.T) { })) t.Run("set with spread", runTest(testCase{ in: inputMap(), - s: `{..., title="Mrs"}`, + s: `{..., title:"Mrs"}`, outFn: func() *model.Value { res := inputMap() _ = res.SetMapKey("title", model.NewStringValue("Mrs")) @@ -419,7 +469,7 @@ func TestExecuteSelector_HappyPath(t *testing.T) { s: ` map ( { - total = add( foo, bar, 1 ) + total: add( foo, bar, 1 ) } ) .map ( total )`, @@ -529,7 +579,7 @@ func TestExecuteSelector_HappyPath(t *testing.T) { } } - t.Run("direct to slice", runArrayTests(inSlice, "")) + t.Run("direct to slice", runArrayTests(inSlice, "$this")) t.Run("property to slice", runArrayTests(inMap, "numbers")) }) diff --git a/model/value.go b/model/value.go index b86259da..d0441b38 100644 --- a/model/value.go +++ b/model/value.go @@ -85,6 +85,20 @@ func (v *Value) UnpackUntilType(t reflect.Type) (*Value, error) { } } +func (v *Value) UnpackUntilAddressable() (*Value, error) { + res := v.Value + for { + if res.CanAddr() { + return NewValue(res), nil + } + if res.Kind() == reflect.Interface || res.Kind() == reflect.Ptr && !res.IsNil() { + res = res.Elem() + continue + } + return nil, fmt.Errorf("could not unpack addressable value") + } +} + func (v *Value) UnpackUntilKind(k reflect.Kind) (*Value, error) { res := v.Value for { diff --git a/model/value_set.go b/model/value_set.go index 5cfe3d99..0eefdf5b 100644 --- a/model/value_set.go +++ b/model/value_set.go @@ -13,9 +13,16 @@ func (v *Value) Set(newValue *Value) error { return nil } + // todo : figure this out x := newPtr() x.Elem().Set(b.Value) - v.Value.Set(x) + + target, err := v.UnpackUntilAddressable() + if err != nil { + return err + } + + target.Value.Set(x) return nil } diff --git a/selector/ast/expression_complex.go b/selector/ast/expression_complex.go index 009c8d0d..58b5d493 100644 --- a/selector/ast/expression_complex.go +++ b/selector/ast/expression_complex.go @@ -64,6 +64,12 @@ type IndexExpr struct { func (IndexExpr) expr() {} +type ArrayExpr struct { + Exprs Expressions +} + +func (ArrayExpr) expr() {} + type PropertyExpr struct { Property Expr } diff --git a/selector/lexer/token.go b/selector/lexer/token.go index 66379ae7..cfb96e3c 100644 --- a/selector/lexer/token.go +++ b/selector/lexer/token.go @@ -7,6 +7,10 @@ import ( type TokenKind int +func TokenKinds(tk ...TokenKind) []TokenKind { + return tk +} + const ( EOF TokenKind = iota Symbol diff --git a/selector/parser/parse_array.go b/selector/parser/parse_array.go index cd018e4f..7d16b050 100644 --- a/selector/parser/parse_array.go +++ b/selector/parser/parse_array.go @@ -6,6 +6,27 @@ import ( ) func parseArray(p *Parser) (ast.Expr, error) { + if err := p.expect(lexer.OpenBracket); err != nil { + return nil, err + } + p.advance() + + elements, err := p.parseExpressionsAsSlice( + lexer.TokenKinds(lexer.CloseBracket), + lexer.TokenKinds(lexer.Comma), + false, + bpLiteral, + ) + if err != nil { + return nil, err + } + + return ast.ArrayExpr{ + Exprs: elements, + }, nil +} + +func parseIndex(p *Parser) (ast.Expr, error) { if err := p.expect(lexer.Symbol, lexer.Variable); err != nil { return nil, err } @@ -15,7 +36,7 @@ func parseArray(p *Parser) (ast.Expr, error) { token := p.current() p.advance() - idx, err := parseSquareBrackets(p) + idx, err := parseIndexSquareBrackets(p) if err != nil { return nil, err } @@ -41,9 +62,9 @@ func parseArray(p *Parser) (ast.Expr, error) { ), nil } -// parseSquareBrackets parses square bracket array access. +// parseIndexSquareBrackets parses square bracket array access. // E.g. [0], [0:1], [0:], [:2] -func parseSquareBrackets(p *Parser) (ast.Expr, error) { +func parseIndexSquareBrackets(p *Parser) (ast.Expr, error) { // Handle index (from bracket) if err := p.expect(lexer.OpenBracket); err != nil { return nil, err diff --git a/selector/parser/parse_object.go b/selector/parser/parse_object.go index 03f78f54..ca571aec 100644 --- a/selector/parser/parse_object.go +++ b/selector/parser/parse_object.go @@ -58,7 +58,7 @@ func parseObject(p *Parser) (ast.Expr, error) { key = prop.Property } - if err := p.expect(lexer.Equals); err != nil { + if err := p.expect(lexer.Colon); err != nil { return nil, err } p.advance() diff --git a/selector/parser/parse_symbol.go b/selector/parser/parse_symbol.go index dc56cc11..dcdcf9f2 100644 --- a/selector/parser/parse_symbol.go +++ b/selector/parser/parse_symbol.go @@ -16,7 +16,7 @@ func parseSymbol(p *Parser) (ast.Expr, error) { } if next.IsKind(lexer.OpenBracket) { - return parseArray(p) + return parseIndex(p) } prop := ast.PropertyExpr{ diff --git a/selector/parser/parse_variable.go b/selector/parser/parse_variable.go index de70beea..207c0618 100644 --- a/selector/parser/parse_variable.go +++ b/selector/parser/parse_variable.go @@ -11,7 +11,7 @@ func parseVariable(p *Parser) (ast.Expr, error) { next := p.peek() if next.IsKind(lexer.OpenBracket) { - return parseArray(p) + return parseIndex(p) } prop := ast.VariableExpr{ diff --git a/selector/parser/parser.go b/selector/parser/parser.go index 7524dab2..7ddb853b 100644 --- a/selector/parser/parser.go +++ b/selector/parser/parser.go @@ -89,7 +89,7 @@ func (p *Parser) parseExpression(bp bindingPower) (left ast.Expr, err error) { case lexer.Symbol: left, err = parseSymbol(p) case lexer.OpenBracket: - left, err = parseSquareBrackets(p) + left, err = parseArray(p) case lexer.OpenCurly: left, err = parseObject(p) case lexer.Bool: diff --git a/selector/parser/parser_test.go b/selector/parser/parser_test.go index f1ec6ccd..4be29d2c 100644 --- a/selector/parser/parser_test.go +++ b/selector/parser/parser_test.go @@ -131,45 +131,66 @@ func TestParser_Parse_HappyPath(t *testing.T) { t.Run("array access", func(t *testing.T) { t.Run("root array", func(t *testing.T) { t.Run("index", run(t, testCase{ - input: "[1]", - expected: ast.IndexExpr{Index: ast.NumberIntExpr{Value: 1}}, + input: "$this[1]", + expected: ast.ChainExprs( + ast.VariableExpr{Name: "this"}, + ast.IndexExpr{Index: ast.NumberIntExpr{Value: 1}}, + ), })) t.Run("range", func(t *testing.T) { t.Run("start and end funcs", run(t, testCase{ - input: "[calcStart(1):calcEnd()]", - expected: ast.RangeExpr{ - Start: ast.CallExpr{ - Function: "calcStart", - Args: ast.Expressions{ - ast.NumberIntExpr{Value: 1}, + input: "$this[calcStart(1):calcEnd()]", + expected: ast.ChainExprs( + ast.VariableExpr{Name: "this"}, + ast.RangeExpr{ + Start: ast.CallExpr{ + Function: "calcStart", + Args: ast.Expressions{ + ast.NumberIntExpr{Value: 1}, + }, + }, + End: ast.CallExpr{ + Function: "calcEnd", }, }, - End: ast.CallExpr{ - Function: "calcEnd", - }, - }, + ), })) t.Run("start and end", run(t, testCase{ - input: "[5:10]", - expected: ast.RangeExpr{Start: ast.NumberIntExpr{Value: 5}, End: ast.NumberIntExpr{Value: 10}}, + input: "$this[5:10]", + expected: ast.ChainExprs( + ast.VariableExpr{Name: "this"}, + ast.RangeExpr{Start: ast.NumberIntExpr{Value: 5}, End: ast.NumberIntExpr{Value: 10}}, + ), })) t.Run("start", run(t, testCase{ - input: "[5:]", - expected: ast.RangeExpr{Start: ast.NumberIntExpr{Value: 5}}, + input: "$this[5:]", + expected: ast.ChainExprs( + ast.VariableExpr{Name: "this"}, + ast.RangeExpr{Start: ast.NumberIntExpr{Value: 5}}, + ), })) t.Run("end", run(t, testCase{ - input: "[:10]", - expected: ast.RangeExpr{End: ast.NumberIntExpr{Value: 10}}, + input: "$this[:10]", + expected: ast.ChainExprs( + ast.VariableExpr{Name: "this"}, + ast.RangeExpr{End: ast.NumberIntExpr{Value: 10}}, + ), })) }) t.Run("spread", func(t *testing.T) { t.Run("standard", run(t, testCase{ - input: "...", - expected: ast.SpreadExpr{}, + input: "$this...", + expected: ast.ChainExprs( + ast.VariableExpr{Name: "this"}, + ast.SpreadExpr{}, + ), })) t.Run("brackets", run(t, testCase{ - input: "[...]", - expected: ast.SpreadExpr{}, + input: "$this[...]", + expected: ast.ChainExprs( + ast.VariableExpr{Name: "this"}, + ast.SpreadExpr{}, + ), })) }) }) @@ -284,13 +305,13 @@ func TestParser_Parse_HappyPath(t *testing.T) { }}, })) t.Run("set single property", run(t, testCase{ - input: "{foo=1}", + input: `{"foo":1}`, expected: ast.ObjectExpr{Pairs: []ast.KeyValue{ {Key: ast.StringExpr{Value: "foo"}, Value: ast.NumberIntExpr{Value: 1}}, }}, })) t.Run("set multiple properties", run(t, testCase{ - input: "{foo=1, bar=2, baz=3}", + input: `{foo: 1, bar: 2, baz: 3}`, expected: ast.ObjectExpr{Pairs: []ast.KeyValue{ {Key: ast.StringExpr{Value: "foo"}, Value: ast.NumberIntExpr{Value: 1}}, {Key: ast.StringExpr{Value: "bar"}, Value: ast.NumberIntExpr{Value: 2}}, @@ -301,9 +322,9 @@ func TestParser_Parse_HappyPath(t *testing.T) { input: `{ ..., foo, - bar=2, - baz=evalSomething(), - "Name"="Tom", + bar: 2, + "baz": evalSomething(), + "Name": "Tom", }`, expected: ast.ObjectExpr{Pairs: []ast.KeyValue{ {Key: ast.SpreadExpr{}, Value: ast.SpreadExpr{}}, From 1a1e6c9000f26fd247d7244e1740537be830a7de Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Thu, 10 Oct 2024 18:14:17 +0100 Subject: [PATCH 23/56] More progress --- api.go | 50 ++++++-- api_test.go | 33 +++-- execution/execute.go | 41 ------ execution/execute_binary.go | 74 +++++++++++ execution/execute_branch.go | 12 +- execution/execute_test.go | 107 +++++++++++++++- model/value.go | 2 + model/value_map.go | 22 +++- model/value_set.go | 43 +++++-- model/value_set_test.go | 224 +++++++++++++++++++++++++++++++++ model/value_slice.go | 7 +- selector/ast/ast_test.go | 28 +++++ selector/parser/denotations.go | 5 + selector/parser/parse_array.go | 2 +- 14 files changed, 562 insertions(+), 88 deletions(-) create mode 100644 execution/execute_binary.go create mode 100644 model/value_set_test.go create mode 100644 selector/ast/ast_test.go diff --git a/api.go b/api.go index e34cae15..a90f0c1e 100644 --- a/api.go +++ b/api.go @@ -6,26 +6,50 @@ import ( "github.com/tomwright/dasel/v3/model" ) -func Select(data any, selector string) (any, error) { +// Query queries the data using the selector and returns the results. +func Query(data any, selector string) ([]*model.Value, int, error) { val := model.NewValue(data) - res, err := execution.ExecuteSelector(selector, val) + out, err := execution.ExecuteSelector(selector, val) if err != nil { - return nil, err + return nil, 0, err } - return res.Interface(), nil + + res := make([]*model.Value, 0) + + if out.IsBranch() { + if err := out.RangeSlice(func(i int, v *model.Value) error { + res = append(res, v) + return nil + }); err != nil { + return nil, 0, err + } + return res, len(res), nil + } + + return []*model.Value{out}, 1, nil } -func Modify(data any, selector string, newValue any) error { - val := model.NewValue(data) - newVal := model.NewValue(newValue) - res, err := execution.ExecuteSelector(selector, val) +func Select(data any, selector string) (any, int, error) { + res, count, err := Query(data, selector) if err != nil { - return err + return nil, 0, err } - - if err := res.Set(newVal); err != nil { - return err + out := make([]any, 0) + for _, v := range res { + out = append(out, v.Interface()) } + return out, count, err +} - return nil +func Modify(data any, selector string, newValue any) (int, error) { + res, count, err := Query(data, selector) + if err != nil { + return 0, err + } + for _, v := range res { + if err := v.Set(model.NewValue(newValue)); err != nil { + return 0, err + } + } + return count, nil } diff --git a/api_test.go b/api_test.go index a384f079..6d99e86f 100644 --- a/api_test.go +++ b/api_test.go @@ -1,9 +1,10 @@ package dasel_test import ( + "testing" + "github.com/google/go-cmp/cmp" "github.com/tomwright/dasel/v3" - "testing" ) type modifyTestCase struct { @@ -11,23 +12,37 @@ type modifyTestCase struct { in any value any exp any + count int } func (tc modifyTestCase) run(t *testing.T) { - if err := dasel.Modify(&tc.in, "[1]", 4); err != nil { + count, err := dasel.Modify(&tc.in, tc.selector, tc.value) + if err != nil { t.Fatalf("unexpected error: %v", err) } + if count != tc.count { + t.Errorf("unexpected count: %d", count) + } if !cmp.Equal(tc.exp, tc.in) { t.Errorf("unexpected result: %s", cmp.Diff(tc.exp, tc.in)) } } func TestModify(t *testing.T) { - // TODO : get this working - //t.Run("int over int", modifyTestCase{ - // selector: "$this[1]", - // in: []int{1, 2, 3}, - // value: 4, - // exp: []int{1, 4, 3}, - //}.run) + t.Run("index", func(t *testing.T) { + t.Run("int over int", modifyTestCase{ + selector: "$this[1]", + in: []int{1, 2, 3}, + value: 4, + exp: []int{1, 4, 3}, + count: 1, + }.run) + t.Run("string over int", modifyTestCase{ + selector: "$this[1]", + in: []any{1, 2, 3}, + value: "4", + exp: []any{1, "4", 3}, + count: 1, + }.run) + }) } diff --git a/execution/execute.go b/execution/execute.go index 3170df4e..6ede2a0f 100644 --- a/execution/execute.go +++ b/execution/execute.go @@ -6,7 +6,6 @@ import ( "github.com/tomwright/dasel/v3/model" "github.com/tomwright/dasel/v3/selector" "github.com/tomwright/dasel/v3/selector/ast" - "github.com/tomwright/dasel/v3/selector/lexer" ) func ExecuteSelector(selectorStr string, value *model.Value) (*model.Value, error) { @@ -104,46 +103,6 @@ func exprExecutor(expr ast.Expr) (expressionExecutor, error) { } } -func binaryExprExecutor(e ast.BinaryExpr) (expressionExecutor, error) { - return func(data *model.Value) (*model.Value, error) { - left, err := ExecuteAST(e.Left, data) - if err != nil { - return nil, fmt.Errorf("error evaluating left expression: %w", err) - } - right, err := ExecuteAST(e.Right, data) - if err != nil { - return nil, fmt.Errorf("error evaluating right expression: %w", err) - } - - switch e.Operator.Kind { - case lexer.Plus: - return left.Add(right) - case lexer.Dash: - return left.Subtract(right) - case lexer.Star: - return left.Multiply(right) - case lexer.Slash: - return left.Divide(right) - case lexer.Percent: - return left.Modulo(right) - case lexer.GreaterThan: - return left.GreaterThan(right) - case lexer.GreaterThanOrEqual: - return left.GreaterThanOrEqual(right) - case lexer.LessThan: - return left.LessThan(right) - case lexer.LessThanOrEqual: - return left.LessThanOrEqual(right) - case lexer.Equal: - return left.Equal(right) - case lexer.NotEqual: - return left.NotEqual(right) - default: - return nil, fmt.Errorf("unhandled operator: %s", e.Operator.Value) - } - }, nil -} - func chainedExprExecutor(e ast.ChainedExpr) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { for _, expr := range e.Exprs { diff --git a/execution/execute_binary.go b/execution/execute_binary.go new file mode 100644 index 00000000..266fa0c2 --- /dev/null +++ b/execution/execute_binary.go @@ -0,0 +1,74 @@ +package execution + +import ( + "fmt" + + "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/selector/ast" + "github.com/tomwright/dasel/v3/selector/lexer" +) + +func binaryExprExecutor(e ast.BinaryExpr) (expressionExecutor, error) { + return func(data *model.Value) (*model.Value, error) { + left, err := ExecuteAST(e.Left, data) + if err != nil { + return nil, fmt.Errorf("error evaluating left expression: %w", err) + } + right, err := ExecuteAST(e.Right, data) + if err != nil { + return nil, fmt.Errorf("error evaluating right expression: %w", err) + } + + switch e.Operator.Kind { + case lexer.Plus: + return left.Add(right) + case lexer.Dash: + return left.Subtract(right) + case lexer.Star: + return left.Multiply(right) + case lexer.Slash: + return left.Divide(right) + case lexer.Percent: + return left.Modulo(right) + case lexer.GreaterThan: + return left.GreaterThan(right) + case lexer.GreaterThanOrEqual: + return left.GreaterThanOrEqual(right) + case lexer.LessThan: + return left.LessThan(right) + case lexer.LessThanOrEqual: + return left.LessThanOrEqual(right) + case lexer.Equal: + return left.Equal(right) + case lexer.NotEqual: + return left.NotEqual(right) + case lexer.Equals: + err := left.Set(right) + return left, err + case lexer.And: + leftBool, err := left.BoolValue() + if err != nil { + return nil, fmt.Errorf("error getting left bool value: %w", err) + } + rightBool, err := right.BoolValue() + if err != nil { + return nil, fmt.Errorf("error getting right bool value: %w", err) + } + return model.NewBoolValue(leftBool && rightBool), nil + case lexer.Or: + leftBool, err := left.BoolValue() + if err != nil { + return nil, fmt.Errorf("error getting left bool value: %w", err) + } + rightBool, err := right.BoolValue() + if err != nil { + return nil, fmt.Errorf("error getting right bool value: %w", err) + } + return model.NewBoolValue(leftBool || rightBool), nil + //case lexer.Like: + //case lexer.NotLike: + default: + return nil, fmt.Errorf("unhandled operator: %s", e.Operator.Value) + } + }, nil +} diff --git a/execution/execute_branch.go b/execution/execute_branch.go index d64e61a1..6dd697cd 100644 --- a/execution/execute_branch.go +++ b/execution/execute_branch.go @@ -16,8 +16,16 @@ func branchExprExecutor(e ast.BranchExpr) (expressionExecutor, error) { 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) + + // This deals with the spread operator in the branch expression. + valsToAppend, err := prepareSpreadValues(r) + if err != nil { + return nil, fmt.Errorf("error handling spread values: %w", err) + } + for _, v := range valsToAppend { + if err := res.Append(v); err != nil { + return nil, fmt.Errorf("failed to append branch result: %w", err) + } } } diff --git a/execution/execute_test.go b/execution/execute_test.go index d46e0475..13ee3b6b 100644 --- a/execution/execute_test.go +++ b/execution/execute_test.go @@ -12,11 +12,12 @@ import ( func TestExecuteSelector_HappyPath(t *testing.T) { type testCase struct { - in *model.Value - inFn func() *model.Value - s string - out *model.Value - outFn func() *model.Value + in *model.Value + inFn func() *model.Value + s string + out *model.Value + outFn func() *model.Value + compareRoot bool } runTest := func(tc testCase) func(t *testing.T) { @@ -37,6 +38,10 @@ func TestExecuteSelector_HappyPath(t *testing.T) { t.Fatal(err) } + if tc.compareRoot { + res = in + } + equal, err := res.EqualTypeValue(exp) if err != nil { t.Fatal(err) @@ -289,6 +294,39 @@ func TestExecuteSelector_HappyPath(t *testing.T) { return r }, })) + t.Run("array with expressions", runTest(testCase{ + s: `[1 + 1, 2f - 2, "foo" + "bar", true || false, [1 + 1, 2 * 2, 3 / 3]]`, + outFn: func() *model.Value { + nested := model.NewSliceValue() + if err := nested.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := nested.Append(model.NewIntValue(4)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := nested.Append(model.NewIntValue(1)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + r := model.NewSliceValue() + if err := r.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := r.Append(model.NewFloatValue(0)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := r.Append(model.NewStringValue("foobar")); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := r.Append(model.NewBoolValue(true)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := r.Append(nested); err != nil { + t.Fatalf("unexpected error: %v", err) + } + return r + }, + })) }) t.Run("function", func(t *testing.T) { @@ -386,6 +424,48 @@ func TestExecuteSelector_HappyPath(t *testing.T) { })) }) + t.Run("set", func(t *testing.T) { + inputMap := func() *model.Value { + return model.NewValue( + dencoding.NewMap(). + Set("title", "Mr"). + Set("age", int64(31)). + Set("name", dencoding.NewMap(). + Set("first", "Tom"). + Set("last", "Wright")), + ) + } + inputSlice := func() *model.Value { + return model.NewValue([]any{1, 2, 3}) + } + + t.Run("set property", runTest(testCase{ + in: inputMap(), + s: `title = "Mrs"`, + outFn: func() *model.Value { + res := inputMap() + if err := res.SetMapKey("title", model.NewStringValue("Mrs")); err != nil { + t.Fatalf("unexpected error: %s", err) + } + return res + }, + compareRoot: true, + })) + + t.Run("set index", runTest(testCase{ + in: inputSlice(), + s: `$this[1] = 4`, + outFn: func() *model.Value { + res := inputSlice() + if err := res.SetSliceIndex(1, model.NewIntValue(4)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + return res + }, + compareRoot: true, + })) + }) + t.Run("object", func(t *testing.T) { inputMap := func() *model.Value { return model.NewValue(dencoding.NewMap(). @@ -664,5 +744,22 @@ func TestExecuteSelector_HappyPath(t *testing.T) { return r }, })) + t.Run("spread into many branches", runTest(testCase{ + s: "[1,2,3].branch(...)", + 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) + } + return r + }, + })) }) } diff --git a/model/value.go b/model/value.go index d0441b38..d785a791 100644 --- a/model/value.go +++ b/model/value.go @@ -30,6 +30,8 @@ type KeyValue struct { type Value struct { Value reflect.Value Metadata map[string]any + + setFn func(*Value) error } func NewValue(v any) *Value { diff --git a/model/value_map.go b/model/value_map.go index 12a0ffba..d35f9296 100644 --- a/model/value_map.go +++ b/model/value_map.go @@ -70,9 +70,12 @@ func (v *Value) GetMapKey(key string) (*Value, error) { if !ok { return nil, &MapKeyNotFound{Key: key} } - return &Value{ - Value: reflect.ValueOf(val), - }, nil + res := NewValue(val) + res.setFn = func(newValue *Value) error { + m.Set(key, newValue.Value.Interface()) + return nil + } + return res, nil case v.isStandardMap(): unpacked, err := v.UnpackUntilKind(reflect.Map) if err != nil { @@ -82,9 +85,16 @@ func (v *Value) GetMapKey(key string) (*Value, error) { if !i.IsValid() { return nil, &MapKeyNotFound{Key: key} } - return &Value{ - Value: i, - }, nil + res := NewValue(i) + res.setFn = func(newValue *Value) error { + mapRv, err := v.UnpackUntilKind(reflect.Map) + if err != nil { + return fmt.Errorf("error unpacking value: %w", err) + } + mapRv.Value.SetMapIndex(reflect.ValueOf(key), newValue.Value) + return nil + } + return res, nil default: return nil, fmt.Errorf("value is not a map") } diff --git a/model/value_set.go b/model/value_set.go index 0eefdf5b..2de052e6 100644 --- a/model/value_set.go +++ b/model/value_set.go @@ -1,28 +1,53 @@ package model import ( + "fmt" "reflect" ) func (v *Value) Set(newValue *Value) error { - a := v.UnpackKinds(reflect.Ptr, reflect.Interface) - b := newValue.UnpackKinds(reflect.Ptr, reflect.Interface) + if v.setFn != nil { + return v.setFn(newValue) + } + + a, err := v.UnpackUntilAddressable() + if err != nil { + return err + } + + if a.Kind() == newValue.Kind() { + a.Value.Set(newValue.Value) + return nil + } + + b := newValue.UnpackKinds(reflect.Ptr) + if a.Kind() == b.Kind() { + a.Value.Set(b.Value) + return nil + } + b = newValue.UnpackKinds(reflect.Interface) if a.Kind() == b.Kind() { a.Value.Set(b.Value) return nil } - // todo : figure this out - x := newPtr() - x.Elem().Set(b.Value) + b = newValue.UnpackKinds(reflect.Ptr, reflect.Interface) + if a.Kind() == b.Kind() { + a.Value.Set(b.Value) + return nil + } - target, err := v.UnpackUntilAddressable() + b, err = newValue.UnpackUntilAddressable() if err != nil { return err } + if a.Kind() == b.Kind() { + a.Value.Set(b.Value) + return nil + } - target.Value.Set(x) - - return nil + // This is a hard limitation at the moment. + // If the types are not the same, we cannot set the value. + return fmt.Errorf("could not set %s value on %s value", newValue.Type(), v.Type()) } diff --git a/model/value_set_test.go b/model/value_set_test.go new file mode 100644 index 00000000..d388aaff --- /dev/null +++ b/model/value_set_test.go @@ -0,0 +1,224 @@ +package model_test + +import ( + "testing" + + "github.com/tomwright/dasel/v3/model" +) + +type setTestCase struct { + valueFn func() *model.Value + value *model.Value + newValueFn func() *model.Value + newValue *model.Value +} + +func (tc setTestCase) run(t *testing.T) { + val := tc.value + if tc.valueFn != nil { + val = tc.valueFn() + } + newVal := tc.newValue + if tc.newValueFn != nil { + newVal = tc.newValueFn() + } + if err := val.Set(newVal); err != nil { + t.Errorf("unexpected error: %s", err) + return + } + + eq, err := val.EqualTypeValue(newVal) + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if !eq { + t.Errorf("expected values to be equal") + } +} + +func TestValue_Set(t *testing.T) { + testCases := []struct { + name string + stringValue func() *model.Value + intValue func() *model.Value + floatValue func() *model.Value + boolValue func() *model.Value + mapValue func() *model.Value + sliceValue func() *model.Value + }{ + { + name: "model constructor", + stringValue: func() *model.Value { + return model.NewStringValue("hello") + }, + intValue: func() *model.Value { + return model.NewIntValue(1) + }, + floatValue: func() *model.Value { + return model.NewFloatValue(1) + }, + boolValue: func() *model.Value { + return model.NewBoolValue(true) + }, + mapValue: func() *model.Value { + res := model.NewMapValue() + if err := res.SetMapKey("greeting", model.NewStringValue("hello")); err != nil { + t.Fatal(err) + } + return res + }, + sliceValue: func() *model.Value { + res := model.NewSliceValue() + if err := res.Append(model.NewStringValue("hello")); err != nil { + t.Fatal(err) + } + return res + }, + }, + { + name: "go types non ptr", + stringValue: func() *model.Value { + v := "hello" + return model.NewValue(v) + }, + intValue: func() *model.Value { + v := int64(1) + return model.NewValue(v) + }, + floatValue: func() *model.Value { + v := 1.0 + return model.NewValue(v) + }, + boolValue: func() *model.Value { + v := true + return model.NewValue(v) + }, + mapValue: func() *model.Value { + v := map[string]interface{}{ + "greeting": "hello", + } + return model.NewValue(v) + }, + sliceValue: func() *model.Value { + v := []interface{}{ + "hello", + } + return model.NewValue(v) + }, + }, + { + name: "go types ptr", + stringValue: func() *model.Value { + v := "hello" + return model.NewValue(&v) + }, + intValue: func() *model.Value { + v := int64(1) + return model.NewValue(&v) + }, + floatValue: func() *model.Value { + v := 1.0 + return model.NewValue(&v) + }, + boolValue: func() *model.Value { + v := true + return model.NewValue(&v) + }, + mapValue: func() *model.Value { + v := map[string]interface{}{ + "greeting": "hello", + } + return model.NewValue(&v) + }, + sliceValue: func() *model.Value { + v := []interface{}{ + "hello", + } + return model.NewValue(&v) + }, + }, + } + + for _, testCase := range testCases { + tc := testCase + t.Run(tc.name, func(t *testing.T) { + t.Run("string", setTestCase{ + valueFn: tc.stringValue, + newValue: model.NewStringValue("world"), + }.run) + t.Run("int", setTestCase{ + valueFn: tc.intValue, + newValue: model.NewIntValue(2), + }.run) + t.Run("float", setTestCase{ + valueFn: tc.floatValue, + newValue: model.NewFloatValue(2), + }.run) + t.Run("bool", setTestCase{ + valueFn: tc.boolValue, + newValue: model.NewBoolValue(false), + }.run) + t.Run("map", setTestCase{ + valueFn: tc.mapValue, + newValueFn: func() *model.Value { + res := model.NewMapValue() + if err := res.SetMapKey("greeting", model.NewStringValue("world")); err != nil { + t.Fatal(err) + } + return res + }, + }.run) + t.Run("slice", setTestCase{ + valueFn: tc.sliceValue, + newValueFn: func() *model.Value { + res := model.NewSliceValue() + if err := res.Append(model.NewStringValue("world")); err != nil { + t.Fatal(err) + } + return res + }, + }.run) + t.Run("string over int", setTestCase{ + valueFn: tc.intValue, + newValue: model.NewStringValue("world"), + }.run) + t.Run("int over float", setTestCase{ + valueFn: tc.floatValue, + newValue: model.NewIntValue(2), + }.run) + t.Run("float over bool", setTestCase{ + valueFn: tc.boolValue, + newValue: model.NewFloatValue(2), + }.run) + t.Run("bool over map", setTestCase{ + valueFn: tc.mapValue, + newValue: model.NewBoolValue(true), + }.run) + t.Run("map over slice", setTestCase{ + valueFn: tc.sliceValue, + newValueFn: func() *model.Value { + res := model.NewMapValue() + if err := res.SetMapKey("greeting", model.NewStringValue("world")); err != nil { + t.Fatal(err) + } + return res + }, + }.run) + t.Run("string over slice", setTestCase{ + valueFn: tc.sliceValue, + newValue: model.NewStringValue("world"), + }.run) + t.Run("slice over map", setTestCase{ + valueFn: tc.mapValue, + newValueFn: func() *model.Value { + res := model.NewSliceValue() + if err := res.Append(model.NewStringValue("world")); err != nil { + t.Fatal(err) + } + return res + }, + }.run) + }) + } +} diff --git a/model/value_slice.go b/model/value_slice.go index 0edec120..62ea893c 100644 --- a/model/value_slice.go +++ b/model/value_slice.go @@ -7,10 +7,12 @@ import ( // NewSliceValue returns a new slice value. func NewSliceValue() *Value { + res := newPtr() s := reflect.MakeSlice(reflect.SliceOf(reflect.TypeFor[any]()), 0, 0) ptr := reflect.New(reflect.SliceOf(reflect.TypeFor[any]())) ptr.Elem().Set(s) - return NewValue(ptr) + res.Elem().Set(ptr) + return NewValue(res) } // IsSlice returns true if the value is a slice. @@ -51,7 +53,8 @@ func (v *Value) GetSliceIndex(i int) (*Value, error) { if i < 0 || i >= unpacked.Value.Len() { return nil, fmt.Errorf("index out of range: %d", i) } - return NewValue(unpacked.Value.Index(i)), nil + res := NewValue(unpacked.Value.Index(i)) + return res, nil } // SetSliceIndex sets the value at the specified index in the slice. diff --git a/selector/ast/ast_test.go b/selector/ast/ast_test.go new file mode 100644 index 00000000..4744a2e9 --- /dev/null +++ b/selector/ast/ast_test.go @@ -0,0 +1,28 @@ +package ast + +import "testing" + +// TestExpr_expr tests the expr method of all the types in the ast package. +// Note that this doesn't actually do anything and is just forcing test coverage. +// The expr func only exists for type safety with the Expr interface. +func TestExpr_expr(t *testing.T) { + NumberFloatExpr{}.expr() + NumberIntExpr{}.expr() + StringExpr{}.expr() + BoolExpr{}.expr() + BinaryExpr{}.expr() + UnaryExpr{}.expr() + CallExpr{}.expr() + ChainedExpr{}.expr() + SpreadExpr{}.expr() + RangeExpr{}.expr() + IndexExpr{}.expr() + ArrayExpr{}.expr() + PropertyExpr{}.expr() + ObjectExpr{}.expr() + MapExpr{}.expr() + VariableExpr{}.expr() + GroupExpr{}.expr() + ConditionalExpr{}.expr() + BranchExpr{}.expr() +} diff --git a/selector/parser/denotations.go b/selector/parser/denotations.go index 49eed609..03ef35b0 100644 --- a/selector/parser/denotations.go +++ b/selector/parser/denotations.go @@ -18,6 +18,11 @@ var leftDenotationTokens = []lexer.TokenKind{ lexer.GreaterThanOrEqual, lexer.LessThan, lexer.LessThanOrEqual, + lexer.And, + lexer.Or, + lexer.Like, + lexer.NotLike, + lexer.Equals, } // right denotation tokens are tokens that expect a token to the right of them. diff --git a/selector/parser/parse_array.go b/selector/parser/parse_array.go index 7d16b050..fc9601d0 100644 --- a/selector/parser/parse_array.go +++ b/selector/parser/parse_array.go @@ -15,7 +15,7 @@ func parseArray(p *Parser) (ast.Expr, error) { lexer.TokenKinds(lexer.CloseBracket), lexer.TokenKinds(lexer.Comma), false, - bpLiteral, + bpDefault, ) if err != nil { return nil, err From 9010bd5cdacf2d2e5d0eccbc19b8dfdfac561c2a Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 10 Oct 2024 19:04:55 +0100 Subject: [PATCH 24/56] Add filter support --- execution/execute.go | 2 + execution/execute_filter.go | 41 +++++++++++++++++++ execution/execute_test.go | 66 ++++++++++++++++++++++++++++++ selector/ast/expression_complex.go | 6 +++ selector/parser/parse_map.go | 22 ++++++++++ selector/parser/parser.go | 4 +- 6 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 execution/execute_filter.go diff --git a/execution/execute.go b/execution/execute.go index 6ede2a0f..b18d636c 100644 --- a/execution/execute.go +++ b/execution/execute.go @@ -92,6 +92,8 @@ func exprExecutor(expr ast.Expr) (expressionExecutor, error) { return objectExprExecutor(e) case ast.MapExpr: return mapExprExecutor(e) + case ast.FilterExpr: + return filterExprExecutor(e) case ast.ConditionalExpr: return conditionalExprExecutor(e) case ast.BranchExpr: diff --git a/execution/execute_filter.go b/execution/execute_filter.go new file mode 100644 index 00000000..edf5d300 --- /dev/null +++ b/execution/execute_filter.go @@ -0,0 +1,41 @@ +package execution + +import ( + "fmt" + + "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/selector/ast" +) + +func filterExprExecutor(e ast.FilterExpr) (expressionExecutor, error) { + return func(data *model.Value) (*model.Value, error) { + if !data.IsSlice() { + return nil, fmt.Errorf("cannot filter over non-array") + } + res := model.NewSliceValue() + + if err := data.RangeSlice(func(i int, item *model.Value) error { + v, err := ExecuteAST(e.Expr, item) + if err != nil { + return err + } + + boolV, err := v.BoolValue() + if err != nil { + return err + } + + if !boolV { + return nil + } + if err := res.Append(item); err != nil { + return fmt.Errorf("error appending item to result: %w", err) + } + return nil + }); err != nil { + return nil, fmt.Errorf("error ranging over slice: %w", err) + } + + return res, nil + }, nil +} diff --git a/execution/execute_test.go b/execution/execute_test.go index 13ee3b6b..6d88ef24 100644 --- a/execution/execute_test.go +++ b/execution/execute_test.go @@ -559,6 +559,72 @@ func TestExecuteSelector_HappyPath(t *testing.T) { })) }) + t.Run("filter", func(t *testing.T) { + inSlice := func() *model.Value { + s := model.NewSliceValue() + if err := s.Append(model.NewIntValue(1)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := s.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := s.Append(model.NewIntValue(3)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + return s + } + t.Run("all true", runTest(testCase{ + inFn: inSlice, + s: "filter(true)", + outFn: func() *model.Value { + s := model.NewSliceValue() + if err := s.Append(model.NewIntValue(1)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := s.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := s.Append(model.NewIntValue(3)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + return s + }, + })) + t.Run("all false", runTest(testCase{ + inFn: inSlice, + s: "filter(false)", + outFn: func() *model.Value { + s := model.NewSliceValue() + return s + }, + })) + t.Run("equal 2", runTest(testCase{ + inFn: inSlice, + s: "filter($this == 2)", + outFn: func() *model.Value { + s := model.NewSliceValue() + if err := s.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + return s + }, + })) + t.Run("not equal 2", runTest(testCase{ + inFn: inSlice, + s: "filter($this != 2)", + outFn: func() *model.Value { + s := model.NewSliceValue() + if err := s.Append(model.NewIntValue(1)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := s.Append(model.NewIntValue(3)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + return s + }, + })) + }) + t.Run("array", func(t *testing.T) { inSlice := func() *model.Value { s := model.NewSliceValue() diff --git a/selector/ast/expression_complex.go b/selector/ast/expression_complex.go index 58b5d493..6fca73e1 100644 --- a/selector/ast/expression_complex.go +++ b/selector/ast/expression_complex.go @@ -93,6 +93,12 @@ type MapExpr struct { func (MapExpr) expr() {} +type FilterExpr struct { + Expr Expr +} + +func (FilterExpr) expr() {} + type VariableExpr struct { Name string } diff --git a/selector/parser/parse_map.go b/selector/parser/parse_map.go index 76334bfa..9a33223d 100644 --- a/selector/parser/parse_map.go +++ b/selector/parser/parse_map.go @@ -31,6 +31,28 @@ func parseMap(p *Parser) (ast.Expr, error) { }, nil } +func parseFilter(p *Parser) (ast.Expr, error) { + if err := p.expect(lexer.Filter); err != nil { + return nil, err + } + p.advance() + + expr, err := p.parseExpressionsFromTo( + lexer.OpenParen, + lexer.CloseParen, + []lexer.TokenKind{}, + true, + bpDefault, + ) + if err != nil { + return nil, err + } + + return ast.FilterExpr{ + Expr: expr, + }, nil +} + func parseBranch(p *Parser) (ast.Expr, error) { if err := p.expect(lexer.Branch); err != nil { return nil, err diff --git a/selector/parser/parser.go b/selector/parser/parser.go index 7ddb853b..c6eae065 100644 --- a/selector/parser/parser.go +++ b/selector/parser/parser.go @@ -106,8 +106,8 @@ func (p *Parser) parseExpression(bp bindingPower) (left ast.Expr, err error) { left, err = parseBranch(p) case lexer.Map: left, err = parseMap(p) - //case lexer.Filter: - // left, err = parseFilter(p) + case lexer.Filter: + left, err = parseFilter(p) default: return nil, &UnexpectedTokenError{ Token: p.current(), From 797f00106efacb1a1032886bb5f4e6aafd5d83e1 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 10 Oct 2024 19:13:19 +0100 Subject: [PATCH 25/56] Simplify map expr --- execution/execute.go | 2 ++ execution/execute_map.go | 9 +++------ selector/ast/expression_complex.go | 2 +- selector/parser/parse_map.go | 2 +- selector/parser/parser_test.go | 14 +++++--------- 5 files changed, 12 insertions(+), 17 deletions(-) diff --git a/execution/execute.go b/execution/execute.go index b18d636c..445d0a51 100644 --- a/execution/execute.go +++ b/execution/execute.go @@ -8,6 +8,7 @@ import ( "github.com/tomwright/dasel/v3/selector/ast" ) +// ExecuteSelector parses the selector and executes the resulting AST with the given input. func ExecuteSelector(selectorStr string, value *model.Value) (*model.Value, error) { if selectorStr == "" { return value, nil @@ -28,6 +29,7 @@ func ExecuteSelector(selectorStr string, value *model.Value) (*model.Value, erro type expressionExecutor func(data *model.Value) (*model.Value, error) +// ExecuteAST executes the given AST with the given input. func ExecuteAST(expr ast.Expr, value *model.Value) (*model.Value, error) { if expr == nil { return value, nil diff --git a/execution/execute_map.go b/execution/execute_map.go index ad42aade..41f175da 100644 --- a/execution/execute_map.go +++ b/execution/execute_map.go @@ -15,12 +15,9 @@ func mapExprExecutor(e ast.MapExpr) (expressionExecutor, error) { res := model.NewSliceValue() if err := data.RangeSlice(func(i int, item *model.Value) error { - var err error - for _, expr := range e.Exprs { - item, err = ExecuteAST(expr, item) - if err != nil { - return err - } + item, err := ExecuteAST(e.Expr, item) + if err != nil { + return err } if err := res.Append(item); err != nil { return fmt.Errorf("error appending item to result: %w", err) diff --git a/selector/ast/expression_complex.go b/selector/ast/expression_complex.go index 6fca73e1..0ea2b951 100644 --- a/selector/ast/expression_complex.go +++ b/selector/ast/expression_complex.go @@ -88,7 +88,7 @@ type ObjectExpr struct { func (ObjectExpr) expr() {} type MapExpr struct { - Exprs Expressions + Expr Expr } func (MapExpr) expr() {} diff --git a/selector/parser/parse_map.go b/selector/parser/parse_map.go index 9a33223d..6e0cfe35 100644 --- a/selector/parser/parse_map.go +++ b/selector/parser/parse_map.go @@ -27,7 +27,7 @@ func parseMap(p *Parser) (ast.Expr, error) { } return ast.MapExpr{ - Exprs: expressions, + Expr: ast.ChainExprs(expressions...), }, nil } diff --git a/selector/parser/parser_test.go b/selector/parser/parser_test.go index 4be29d2c..d2f0965b 100644 --- a/selector/parser/parser_test.go +++ b/selector/parser/parser_test.go @@ -267,9 +267,7 @@ func TestParser_Parse_HappyPath(t *testing.T) { expected: ast.ChainExprs( ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.MapExpr{ - Exprs: ast.Expressions{ - ast.PropertyExpr{Property: ast.StringExpr{Value: "x"}}, - }, + Expr: ast.PropertyExpr{Property: ast.StringExpr{Value: "x"}}, }, ), })) @@ -278,12 +276,10 @@ func TestParser_Parse_HappyPath(t *testing.T) { expected: ast.ChainExprs( ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.MapExpr{ - Exprs: ast.Expressions{ - ast.ChainExprs( - ast.PropertyExpr{Property: ast.StringExpr{Value: "x"}}, - ast.PropertyExpr{Property: ast.StringExpr{Value: "y"}}, - ), - }, + Expr: ast.ChainExprs( + ast.PropertyExpr{Property: ast.StringExpr{Value: "x"}}, + ast.PropertyExpr{Property: ast.StringExpr{Value: "y"}}, + ), }, ), })) From 605ec48ee0a3d717e53d4c3a20fe5b26e3fef6b4 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 10 Oct 2024 22:54:14 +0100 Subject: [PATCH 26/56] Add regex support --- execution/execute.go | 5 +++++ execution/execute_binary.go | 16 ++++++++++++++-- execution/execute_test.go | 8 ++++++++ execution/func.go | 27 +++++++++++++++++++++++++++ selector/ast/expression_literal.go | 8 ++++++++ selector/lexer/token.go | 1 + selector/lexer/tokenize.go | 17 +++++++++++++++++ selector/lexer/tokenize_test.go | 8 ++++++++ selector/parser/parse_literal.go | 21 +++++++++++++++++++++ selector/parser/parser.go | 2 ++ 10 files changed, 111 insertions(+), 2 deletions(-) diff --git a/execution/execute.go b/execution/execute.go index 445d0a51..8103052f 100644 --- a/execution/execute.go +++ b/execution/execute.go @@ -102,6 +102,11 @@ func exprExecutor(expr ast.Expr) (expressionExecutor, error) { return branchExprExecutor(e) case ast.ArrayExpr: return arrayExprExecutor(e) + case ast.RegexExpr: + // Noop + return func(data *model.Value) (*model.Value, error) { + return data, nil + }, nil default: return nil, fmt.Errorf("unhandled expression type: %T", e) } diff --git a/execution/execute_binary.go b/execution/execute_binary.go index 266fa0c2..41a9f38b 100644 --- a/execution/execute_binary.go +++ b/execution/execute_binary.go @@ -65,8 +65,20 @@ func binaryExprExecutor(e ast.BinaryExpr) (expressionExecutor, error) { return nil, fmt.Errorf("error getting right bool value: %w", err) } return model.NewBoolValue(leftBool || rightBool), nil - //case lexer.Like: - //case lexer.NotLike: + case lexer.Like, lexer.NotLike: + leftStr, err := left.StringValue() + if err != nil { + return nil, fmt.Errorf("like requires left side to be a string, got %s", left.Type().String()) + } + rightPatt, ok := e.Right.(ast.RegexExpr) + if !ok { + return nil, fmt.Errorf("like requires right side to be a regex pattern") + } + res := rightPatt.Regex.MatchString(leftStr) + if e.Operator.Kind == lexer.NotLike { + res = !res + } + return model.NewBoolValue(res), nil default: return nil, fmt.Errorf("unhandled operator: %s", e.Operator.Value) } diff --git a/execution/execute_test.go b/execution/execute_test.go index 6d88ef24..f9c699f6 100644 --- a/execution/execute_test.go +++ b/execution/execute_test.go @@ -168,6 +168,14 @@ func TestExecuteSelector_HappyPath(t *testing.T) { s: `2 <= 2`, out: model.NewBoolValue(true), })) + t.Run("like", runTest(testCase{ + s: `"hello world" =~ r/ello/`, + out: model.NewBoolValue(true), + })) + t.Run("not like", runTest(testCase{ + s: `"hello world" !~ r/helloworld/`, + out: model.NewBoolValue(true), + })) }) t.Run("variables", func(t *testing.T) { diff --git a/execution/func.go b/execution/func.go index 193dda93..e5485a71 100644 --- a/execution/func.go +++ b/execution/func.go @@ -60,4 +60,31 @@ func init() { } return model.NewIntValue(intRes), nil }) + + RegisterFunc("toString", func(data *model.Value, args model.Values) (*model.Value, error) { + switch data.Type() { + case model.TypeString: + return data, nil + case model.TypeInt: + i, err := data.IntValue() + if err != nil { + return nil, err + } + return model.NewStringValue(fmt.Sprintf("%d", i)), nil + case model.TypeFloat: + i, err := data.FloatValue() + if err != nil { + return nil, err + } + return model.NewStringValue(fmt.Sprintf("%f", i)), nil + case model.TypeBool: + i, err := data.BoolValue() + if err != nil { + return nil, err + } + return model.NewStringValue(fmt.Sprintf("%v", i)), nil + default: + return nil, fmt.Errorf("cannot convert %s to string", data.Type()) + } + }) } diff --git a/selector/ast/expression_literal.go b/selector/ast/expression_literal.go index b369b9fe..bb116efa 100644 --- a/selector/ast/expression_literal.go +++ b/selector/ast/expression_literal.go @@ -1,5 +1,7 @@ package ast +import "regexp" + type NumberFloatExpr struct { Value float64 } @@ -23,3 +25,9 @@ type BoolExpr struct { } func (BoolExpr) expr() {} + +type RegexExpr struct { + Regex *regexp.Regexp +} + +func (RegexExpr) expr() {} diff --git a/selector/lexer/token.go b/selector/lexer/token.go index cfb96e3c..f984c927 100644 --- a/selector/lexer/token.go +++ b/selector/lexer/token.go @@ -57,6 +57,7 @@ const ( Branch Map Filter + RegexPattern ) type Tokens []Token diff --git a/selector/lexer/tokenize.go b/selector/lexer/tokenize.go index fd601500..27213ead 100644 --- a/selector/lexer/tokenize.go +++ b/selector/lexer/tokenize.go @@ -197,6 +197,19 @@ func (p *Tokenizer) parseCurRune() (Token, error) { return ptr.To(NewToken(kind, other, pos, l)) } + matchRegexPattern := func(pos int) *Token { + if !(p.src[pos] == 'r' && p.peekRuneEqual(pos+1, '/')) { + return nil + } + start := pos + pos += 2 + for !p.peekRuneEqual(pos, '/') { + pos++ + } + pos++ + return ptr.To(NewToken(RegexPattern, p.src[start+2:pos-1], start, pos-start)) + } + if t := matchStr(pos, "null", true, Null); t != nil { return *t, nil } @@ -225,6 +238,10 @@ func (p *Tokenizer) parseCurRune() (Token, error) { return *t, nil } + if t := matchRegexPattern(pos); t != nil { + return *t, nil + } + if unicode.IsDigit(rune(p.src[pos])) { // Handle whole numbers for pos < p.srcLen && unicode.IsDigit(rune(p.src[pos])) { diff --git a/selector/lexer/tokenize_test.go b/selector/lexer/tokenize_test.go index 2bd21054..fed02c5b 100644 --- a/selector/lexer/tokenize_test.go +++ b/selector/lexer/tokenize_test.go @@ -46,6 +46,14 @@ func TestTokenizer_Parse(t *testing.T) { }, })) + t.Run("regex", runTest(testCase{ + in: `r/asd/ r/hello there/`, + out: []TokenKind{ + RegexPattern, + RegexPattern, + }, + })) + t.Run("everything", runTest(testCase{ in: "foo.bar.baz[1] != 42.123 || foo.bar.baz['hello'] == 42 && x == 'a\\'b' + false true . .... asd... $name null", out: []TokenKind{ diff --git a/selector/parser/parse_literal.go b/selector/parser/parse_literal.go index 870390e1..932934a3 100644 --- a/selector/parser/parse_literal.go +++ b/selector/parser/parse_literal.go @@ -1,6 +1,8 @@ package parser import ( + "fmt" + "regexp" "strconv" "strings" @@ -62,3 +64,22 @@ func parseNumberLiteral(p *Parser) (ast.Expr, error) { }, nil } } + +func parseRegexPattern(p *Parser) (ast.Expr, error) { + if err := p.expect(lexer.RegexPattern); err != nil { + return nil, err + } + + pattern := p.current() + + p.advance() + + comp, err := regexp.Compile(pattern.Value) + if err != nil { + return nil, fmt.Errorf("failed to compile regexp pattern: %w", err) + } + + return ast.RegexExpr{ + Regex: comp, + }, nil +} diff --git a/selector/parser/parser.go b/selector/parser/parser.go index c6eae065..7810e2cc 100644 --- a/selector/parser/parser.go +++ b/selector/parser/parser.go @@ -108,6 +108,8 @@ func (p *Parser) parseExpression(bp bindingPower) (left ast.Expr, err error) { left, err = parseMap(p) case lexer.Filter: left, err = parseFilter(p) + case lexer.RegexPattern: + left, err = parseRegexPattern(p) default: return nil, &UnexpectedTokenError{ Token: p.current(), From d857cb9e2dc2306210d2229f4b2aa42e28143a02 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 10 Oct 2024 23:02:19 +0100 Subject: [PATCH 27/56] Add missing error handling --- internal/cli/root.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/cli/root.go b/internal/cli/root.go index 86b14eb5..44522a16 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -30,7 +30,13 @@ func RootCmd() *cobra.Command { } reader, err := parsing.NewReader(parsing.Format(readerStr)) + if err != nil { + return fmt.Errorf("failed to get input reader: %w", err) + } writer, err := parsing.NewWriter(parsing.Format(writerStr)) + if err != nil { + return fmt.Errorf("failed to get output writer: %w", err) + } inputBytes, err := io.ReadAll(cmd.InOrStdin()) if err != nil { From 9999c1b82d22e70fbd71ad81000442a047c8cb11 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Sat, 19 Oct 2024 00:32:06 +0100 Subject: [PATCH 28/56] Improve spreading into object defs and rework functions --- api.go | 15 +- execution/execute.go | 14 +- execution/execute_func.go | 4 +- execution/execute_object.go | 37 +++- execution/execute_test.go | 85 ++++++++ execution/func.go | 300 ++++++++++++++++++++++------- execution/options.go | 30 +++ model/value.go | 4 +- model/value_literal.go | 8 + model/value_map.go | 6 +- selector/ast/ast.go | 29 +++ selector/ast/expression_complex.go | 5 - selector/parser/parse_object.go | 97 ++++++---- selector/parser/parser_test.go | 4 +- 14 files changed, 499 insertions(+), 139 deletions(-) create mode 100644 execution/options.go diff --git a/api.go b/api.go index a90f0c1e..02a677fe 100644 --- a/api.go +++ b/api.go @@ -7,16 +7,15 @@ import ( ) // Query queries the data using the selector and returns the results. -func Query(data any, selector string) ([]*model.Value, int, error) { +func Query(data any, selector string, opts ...execution.ExecuteOptionFn) ([]*model.Value, int, error) { val := model.NewValue(data) - out, err := execution.ExecuteSelector(selector, val) + out, err := execution.ExecuteSelector(selector, val, opts...) if err != nil { return nil, 0, err } - res := make([]*model.Value, 0) - if out.IsBranch() { + res := make([]*model.Value, 0) if err := out.RangeSlice(func(i int, v *model.Value) error { res = append(res, v) return nil @@ -29,8 +28,8 @@ func Query(data any, selector string) ([]*model.Value, int, error) { return []*model.Value{out}, 1, nil } -func Select(data any, selector string) (any, int, error) { - res, count, err := Query(data, selector) +func Select(data any, selector string, opts ...execution.ExecuteOptionFn) (any, int, error) { + res, count, err := Query(data, selector, opts...) if err != nil { return nil, 0, err } @@ -41,8 +40,8 @@ func Select(data any, selector string) (any, int, error) { return out, count, err } -func Modify(data any, selector string, newValue any) (int, error) { - res, count, err := Query(data, selector) +func Modify(data any, selector string, newValue any, opts ...execution.ExecuteOptionFn) (int, error) { + res, count, err := Query(data, selector, opts...) if err != nil { return 0, err } diff --git a/execution/execute.go b/execution/execute.go index 8103052f..03a97dcf 100644 --- a/execution/execute.go +++ b/execution/execute.go @@ -9,7 +9,7 @@ import ( ) // ExecuteSelector parses the selector and executes the resulting AST with the given input. -func ExecuteSelector(selectorStr string, value *model.Value) (*model.Value, error) { +func ExecuteSelector(selectorStr string, value *model.Value, opts ...ExecuteOptionFn) (*model.Value, error) { if selectorStr == "" { return value, nil } @@ -19,7 +19,7 @@ func ExecuteSelector(selectorStr string, value *model.Value) (*model.Value, erro return nil, fmt.Errorf("error parsing selector: %w", err) } - res, err := ExecuteAST(expr, value) + res, err := ExecuteAST(expr, value, opts...) if err != nil { return nil, fmt.Errorf("error executing selector: %w", err) } @@ -30,12 +30,14 @@ func ExecuteSelector(selectorStr string, value *model.Value) (*model.Value, erro type expressionExecutor func(data *model.Value) (*model.Value, error) // ExecuteAST executes the given AST with the given input. -func ExecuteAST(expr ast.Expr, value *model.Value) (*model.Value, error) { +func ExecuteAST(expr ast.Expr, value *model.Value, opts ...ExecuteOptionFn) (*model.Value, error) { + options := NewOptions(opts...) + if expr == nil { return value, nil } - executor, err := exprExecutor(expr) + executor, err := exprExecutor(options, expr) if err != nil { return nil, fmt.Errorf("error evaluating expression: %w", err) } @@ -64,12 +66,12 @@ func ExecuteAST(expr ast.Expr, value *model.Value) (*model.Value, error) { return res, nil } -func exprExecutor(expr ast.Expr) (expressionExecutor, error) { +func exprExecutor(opts *Options, expr ast.Expr) (expressionExecutor, error) { switch e := expr.(type) { case ast.BinaryExpr: return binaryExprExecutor(e) case ast.CallExpr: - return callExprExecutor(e) + return callExprExecutor(opts, e) case ast.ChainedExpr: return chainedExprExecutor(e) case ast.SpreadExpr: diff --git a/execution/execute_func.go b/execution/execute_func.go index e0319a50..64f0d873 100644 --- a/execution/execute_func.go +++ b/execution/execute_func.go @@ -41,8 +41,8 @@ func callFnExecutor(f FuncFn, argsE ast.Expressions) (expressionExecutor, error) }, nil } -func callExprExecutor(e ast.CallExpr) (expressionExecutor, error) { - if f, ok := singleResponseFuncLookup[e.Function]; ok { +func callExprExecutor(opts *Options, e ast.CallExpr) (expressionExecutor, error) { + if f, ok := opts.Funcs.Get(e.Function); ok { res, err := callFnExecutor(f, e.Args) if err != nil { return nil, fmt.Errorf("error executing function %q: %w", e.Function, err) diff --git a/execution/execute_object.go b/execution/execute_object.go index 9e8091bf..b1a9da6f 100644 --- a/execution/execute_object.go +++ b/execution/execute_object.go @@ -11,21 +11,46 @@ func objectExprExecutor(e ast.ObjectExpr) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { obj := model.NewMapValue() for _, p := range e.Pairs { - if ast.IsSpreadExpr(p.Key) && ast.IsSpreadExpr(p.Value) { - if err := data.RangeMap(func(key string, value *model.Value) error { + + if ast.IsType[ast.SpreadExpr](p.Key) { + var val *model.Value + var err error + if p.Value != nil { + // We need to spread the resulting value. + val, err = ExecuteAST(p.Value, data) + if err != nil { + return nil, fmt.Errorf("error evaluated spread values") + } + } else { + val = data + } + + if err := val.RangeMap(func(key string, value *model.Value) error { if err := obj.SetMapKey(key, value); err != nil { return fmt.Errorf("error setting map key: %w", err) } return nil }); err != nil { - return nil, fmt.Errorf("error ranging map: %w", err) + return nil, fmt.Errorf("error spreading into object: %w", err) } continue } - if ast.IsSpreadExpr(p.Key) { - return nil, fmt.Errorf("cannot spread object key name") - } + //if ast.IsType[ast.SpreadExpr](p.Key) && ast.IsType[ast.SpreadExpr](p.Value) { + // if err := data.RangeMap(func(key string, value *model.Value) error { + // if err := obj.SetMapKey(key, value); err != nil { + // return fmt.Errorf("error setting map key: %w", err) + // } + // return nil + // }); err != nil { + // return nil, fmt.Errorf("error ranging map: %w", err) + // } + // continue + //} + + //if ast.IsSpreadExpr(p.Key) { + // return nil, fmt.Errorf("cannot spread object key name") + //} key, err := ExecuteAST(p.Key, data) if err != nil { diff --git a/execution/execute_test.go b/execution/execute_test.go index f9c699f6..e104681c 100644 --- a/execution/execute_test.go +++ b/execution/execute_test.go @@ -382,6 +382,48 @@ func TestExecuteSelector_HappyPath(t *testing.T) { })) }) }) + t.Run("merge", func(t *testing.T) { + t.Run("shallow", runTest(testCase{ + inFn: func() *model.Value { + a := model.NewMapValue() + if err := a.SetMapKey("foo", model.NewStringValue("afoo")); err != nil { + t.Errorf("unexpected error: %v", err) + } + if err := a.SetMapKey("bar", model.NewStringValue("abar")); err != nil { + t.Errorf("unexpected error: %v", err) + } + b := model.NewMapValue() + if err := b.SetMapKey("bar", model.NewStringValue("bbar")); err != nil { + t.Errorf("unexpected error: %v", err) + } + if err := b.SetMapKey("baz", model.NewStringValue("bbaz")); err != nil { + t.Errorf("unexpected error: %v", err) + } + res := model.NewMapValue() + if err := res.SetMapKey("a", a); err != nil { + t.Errorf("unexpected error: %v", err) + } + if err := res.SetMapKey("b", b); err != nil { + t.Errorf("unexpected error: %v", err) + } + return res + }, + s: `merge(a, b)`, + outFn: func() *model.Value { + b := model.NewMapValue() + if err := b.SetMapKey("foo", model.NewStringValue("afoo")); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := b.SetMapKey("bar", model.NewStringValue("bbar")); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := b.SetMapKey("baz", model.NewStringValue("bbaz")); err != nil { + t.Fatalf("unexpected error: %v", err) + } + return b + }, + })) + }) }) t.Run("get", func(t *testing.T) { @@ -530,6 +572,49 @@ func TestExecuteSelector_HappyPath(t *testing.T) { return res }, })) + t.Run("merge with spread", runTest(testCase{ + inFn: func() *model.Value { + a := model.NewMapValue() + if err := a.SetMapKey("foo", model.NewStringValue("afoo")); err != nil { + t.Errorf("unexpected error: %v", err) + } + if err := a.SetMapKey("bar", model.NewStringValue("abar")); err != nil { + t.Errorf("unexpected error: %v", err) + } + b := model.NewMapValue() + if err := b.SetMapKey("bar", model.NewStringValue("bbar")); err != nil { + t.Errorf("unexpected error: %v", err) + } + if err := b.SetMapKey("baz", model.NewStringValue("bbaz")); err != nil { + t.Errorf("unexpected error: %v", err) + } + res := model.NewMapValue() + if err := res.SetMapKey("a", a); err != nil { + t.Errorf("unexpected error: %v", err) + } + if err := res.SetMapKey("b", b); err != nil { + t.Errorf("unexpected error: %v", err) + } + return res + }, + s: `{a..., b..., x: 1}`, + outFn: func() *model.Value { + b := model.NewMapValue() + if err := b.SetMapKey("foo", model.NewStringValue("afoo")); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := b.SetMapKey("bar", model.NewStringValue("bbar")); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := b.SetMapKey("baz", model.NewStringValue("bbaz")); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := b.SetMapKey("x", model.NewIntValue(1)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + return b + }, + })) }) t.Run("map", func(t *testing.T) { diff --git a/execution/func.go b/execution/func.go index e5485a71..ad41dce3 100644 --- a/execution/func.go +++ b/execution/func.go @@ -6,85 +6,255 @@ import ( "github.com/tomwright/dasel/v3/model" ) -type FuncFn func(data *model.Value, args model.Values) (*model.Value, error) +// ArgsValidator is a function that validates the arguments passed to a function. +type ArgsValidator func(name string, args model.Values) error -var singleResponseFuncLookup = map[string]FuncFn{} +// ValidateArgsExactly returns an ArgsValidator that validates that the number of arguments passed to a function is exactly the expected number. +func ValidateArgsExactly(expected int) ArgsValidator { + return func(name string, args model.Values) error { + if len(args) == expected { + return nil + } + return fmt.Errorf("func %q expects exactly %d arguments, got %d", name, expected, len(args)) + } +} -func RegisterFunc(name string, fn FuncFn) { - singleResponseFuncLookup[name] = fn +// ValidateArgsMin returns an ArgsValidator that validates that the number of arguments passed to a function is at least the expected number. +func ValidateArgsMin(expected int) ArgsValidator { + return func(name string, args model.Values) error { + if len(args) >= expected { + return nil + } + return fmt.Errorf("func %q expects at least %d arguments, got %d", name, expected, len(args)) + } } -func init() { - RegisterFunc("len", func(data *model.Value, args model.Values) (*model.Value, error) { - if len(args) != 1 { - return nil, fmt.Errorf("len expects a single argument") +// ValidateArgsMax returns an ArgsValidator that validates that the number of arguments passed to a function is at most the expected number. +func ValidateArgsMax(expected int) ArgsValidator { + return func(name string, args model.Values) error { + if len(args) <= expected { + return nil } + return fmt.Errorf("func %q expects no more than %d arguments, got %d", name, expected, len(args)) + } +} - arg := args[0] +// ValidateArgsMinMax returns an ArgsValidator that validates that the number of arguments passed to a function is between the min and max expected numbers. +func ValidateArgsMinMax(min int, max int) ArgsValidator { + return func(name string, args model.Values) error { + if len(args) >= min && len(args) <= max { + return nil + } + return fmt.Errorf("func %q expects between %d and %d arguments, got %d", name, min, max, len(args)) + } +} + +// Func represents a function that can be executed. +type Func struct { + name string + handler FuncFn + argsValidator ArgsValidator +} - l, err := arg.Len() - if err != nil { - return nil, err +// Handler returns a FuncFn that can be used to execute the function. +func (f *Func) Handler() FuncFn { + return func(data *model.Value, args model.Values) (*model.Value, error) { + if f.argsValidator != nil { + if err := f.argsValidator(f.name, args); err != nil { + return nil, err + } } + return f.handler(data, args) + } +} - return model.NewIntValue(int64(l)), nil - }) - - RegisterFunc("add", func(_ *model.Value, args model.Values) (*model.Value, error) { - var foundInts, foundFloats int - var intRes int64 - var floatRes float64 - for _, arg := range args { - if arg.IsFloat() { - foundFloats++ - v, err := arg.FloatValue() - if err != nil { - return nil, fmt.Errorf("error getting float value: %w", err) +// NewFunc creates a new Func. +func NewFunc(name string, handler FuncFn, argsValidator ArgsValidator) *Func { + return &Func{ + name: name, + handler: handler, + argsValidator: argsValidator, + } +} + +// FuncFn is a function that can be executed. +type FuncFn func(data *model.Value, args model.Values) (*model.Value, error) + +// FuncCollection is a collection of functions that can be executed. +type FuncCollection map[string]FuncFn + +// NewFuncCollection creates a new FuncCollection with the given functions. +func NewFuncCollection(funcs ...*Func) FuncCollection { + return FuncCollection{}.Register(funcs...) +} + +// Register registers the given functions with the FuncCollection. +func (fc FuncCollection) Register(funcs ...*Func) FuncCollection { + for _, f := range funcs { + fc[f.name] = f.Handler() + } + return fc +} + +// Get returns the function with the given name. +func (fc FuncCollection) Get(name string) (FuncFn, bool) { + fn, ok := fc[name] + return fn, ok +} + +// Delete deletes the functions with the given names. +func (fc FuncCollection) Delete(names ...string) FuncCollection { + for _, name := range names { + delete(fc, name) + } + return fc +} + +// Copy returns a copy of the FuncCollection. +func (fc FuncCollection) Copy() FuncCollection { + c := NewFuncCollection() + for k, v := range fc { + c[k] = v + } + return c +} + +var ( + // DefaultFuncCollection is the default collection of functions that can be executed. + DefaultFuncCollection = NewFuncCollection( + FuncLen, + FuncAdd, + FuncToString, + FuncMerge, + ) + + // FuncLen is a function that returns the length of the given value. + FuncLen = NewFunc( + "len", + func(data *model.Value, args model.Values) (*model.Value, error) { + arg := args[0] + + l, err := arg.Len() + if err != nil { + return nil, err + } + + return model.NewIntValue(int64(l)), nil + }, + ValidateArgsExactly(1), + ) + + // FuncAdd is a function that adds the given values together. + FuncAdd = NewFunc( + "add", + func(data *model.Value, args model.Values) (*model.Value, error) { + var foundInts, foundFloats int + var intRes int64 + var floatRes float64 + for _, arg := range args { + if arg.IsFloat() { + foundFloats++ + v, err := arg.FloatValue() + if err != nil { + return nil, fmt.Errorf("error getting float value: %w", err) + } + floatRes += v + continue + } + if arg.IsInt() { + foundInts++ + v, err := arg.IntValue() + if err != nil { + return nil, fmt.Errorf("error getting int value: %w", err) + } + intRes += v + continue } - floatRes += v - continue + return nil, fmt.Errorf("expected int or float, got %s", arg.Type()) + } + if foundFloats > 0 { + return model.NewFloatValue(floatRes + float64(intRes)), nil } - if arg.IsInt() { - foundInts++ - v, err := arg.IntValue() + return model.NewIntValue(intRes), nil + }, + ValidateArgsMin(1), + ) + + // FuncToString is a function that converts the given value to a string. + FuncToString = NewFunc( + "toString", + func(data *model.Value, args model.Values) (*model.Value, error) { + switch data.Type() { + case model.TypeString: + return data, nil + case model.TypeInt: + i, err := data.IntValue() if err != nil { - return nil, fmt.Errorf("error getting int value: %w", err) + return nil, err } - intRes += v - continue + return model.NewStringValue(fmt.Sprintf("%d", i)), nil + case model.TypeFloat: + i, err := data.FloatValue() + if err != nil { + return nil, err + } + return model.NewStringValue(fmt.Sprintf("%f", i)), nil + case model.TypeBool: + i, err := data.BoolValue() + if err != nil { + return nil, err + } + return model.NewStringValue(fmt.Sprintf("%v", i)), nil + default: + return nil, fmt.Errorf("cannot convert %s to string", data.Type()) } - return nil, fmt.Errorf("expected int or float, got %s", arg.Type()) - } - if foundFloats > 0 { - return model.NewFloatValue(floatRes + float64(intRes)), nil - } - return model.NewIntValue(intRes), nil - }) - - RegisterFunc("toString", func(data *model.Value, args model.Values) (*model.Value, error) { - switch data.Type() { - case model.TypeString: - return data, nil - case model.TypeInt: - i, err := data.IntValue() - if err != nil { - return nil, err + }, + ValidateArgsExactly(1), + ) + + // FuncMerge is a function that merges two or more items together. + FuncMerge = NewFunc( + "merge", + func(data *model.Value, args model.Values) (*model.Value, error) { + if len(args) == 1 { + return args[0], nil } - return model.NewStringValue(fmt.Sprintf("%d", i)), nil - case model.TypeFloat: - i, err := data.FloatValue() - if err != nil { - return nil, err + + expectedType := args[0].Type() + + switch expectedType { + case model.TypeMap: + break + default: + return nil, fmt.Errorf("merge exects a map, found %s", expectedType) } - return model.NewStringValue(fmt.Sprintf("%f", i)), nil - case model.TypeBool: - i, err := data.BoolValue() - if err != nil { - return nil, err + + // Validate types match + for _, a := range args { + if a.Type() != expectedType { + return nil, fmt.Errorf("merge expects all arguments to be of the same type. expected %s, got %s", expectedType.String(), a.Type().String()) + } } - return model.NewStringValue(fmt.Sprintf("%v", i)), nil - default: - return nil, fmt.Errorf("cannot convert %s to string", data.Type()) - } - }) -} + + base := model.NewMapValue() + + for i := 0; i < len(args); i++ { + next := args[i] + + nextKVs, err := next.MapKeyValues() + if err != nil { + return nil, fmt.Errorf("merge failed to extract key values for arg %d: %w", i, err) + } + + for _, kv := range nextKVs { + if err := base.SetMapKey(kv.Key, kv.Value); err != nil { + return nil, fmt.Errorf("merge failed to set map key %s: %w", kv.Key, err) + } + } + } + + return base, nil + }, + ValidateArgsMin(1), + ) +) diff --git a/execution/options.go b/execution/options.go new file mode 100644 index 00000000..99f9920f --- /dev/null +++ b/execution/options.go @@ -0,0 +1,30 @@ +package execution + +// ExecuteOptionFn is a function that can be used to set options on the execution of the selector. +type ExecuteOptionFn func(*Options) + +// Options contains the options for the execution of the selector. +type Options struct { + Funcs FuncCollection +} + +// NewOptions creates a new Options struct with the given options. +func NewOptions(opts ...ExecuteOptionFn) *Options { + o := &Options{ + Funcs: DefaultFuncCollection, + } + for _, opt := range opts { + if opt == nil { + continue + } + opt(o) + } + return o +} + +// WithFuncs sets the functions that can be used in the selector. +func WithFuncs(fc FuncCollection) ExecuteOptionFn { + return func(o *Options) { + o.Funcs = fc + } +} diff --git a/model/value.go b/model/value.go index d785a791..58a31690 100644 --- a/model/value.go +++ b/model/value.go @@ -143,8 +143,10 @@ func (v *Value) Len() (int, error) { l, err = v.SliceLen() case v.IsMap(): l, err = v.MapLen() + case v.IsString(): + l, err = v.StringLen() default: - err = fmt.Errorf("len expects slice or map") + err = fmt.Errorf("len expects string, slice or map") } if err != nil { diff --git a/model/value_literal.go b/model/value_literal.go index 38a45162..a26dd3a1 100644 --- a/model/value_literal.go +++ b/model/value_literal.go @@ -44,6 +44,14 @@ func (v *Value) StringValue() (string, error) { return unpacked.Value.String(), nil } +func (v *Value) StringLen() (int, error) { + val, err := v.StringValue() + if err != nil { + return 0, err + } + return len(val), nil +} + func NewIntValue(x int64) *Value { res := newPtr() res.Elem().Set(reflect.ValueOf(x)) diff --git a/model/value_map.go b/model/value_map.go index d35f9296..1c9bb037 100644 --- a/model/value_map.go +++ b/model/value_map.go @@ -176,15 +176,15 @@ func (v *Value) MapKeyValues() ([]KeyValue, error) { kvs := make([]KeyValue, len(keys)) - for _, k := range keys { + for i, k := range keys { va, err := v.GetMapKey(k) if err != nil { return nil, fmt.Errorf("error getting map key: %w", err) } - kvs = append(kvs, KeyValue{ + kvs[i] = KeyValue{ Key: k, Value: va, - }) + } } return kvs, nil diff --git a/selector/ast/ast.go b/selector/ast/ast.go index 923346e2..b5a97d48 100644 --- a/selector/ast/ast.go +++ b/selector/ast/ast.go @@ -5,3 +5,32 @@ type Expressions []Expr type Expr interface { expr() } + +func IsType[T Expr](e Expr) bool { + _, ok := AsType[T](e) + return ok +} + +func AsType[T Expr](e Expr) (T, bool) { + v, ok := e.(T) + return v, ok +} + +func LastAsType[T Expr](e Expr) (T, bool) { + return AsType[T](Last(e)) +} + +func Last(e Expr) Expr { + if v, ok := e.(ChainedExpr); ok { + return v.Exprs[len(v.Exprs)-1] + } + return e +} + +func RemoveLast(e Expr) Expr { + var res Expressions + if v, ok := e.(ChainedExpr); ok { + res = v.Exprs[0 : len(v.Exprs)-1] + } + return ChainExprs(res...) +} diff --git a/selector/ast/expression_complex.go b/selector/ast/expression_complex.go index 0ea2b951..f8a7509a 100644 --- a/selector/ast/expression_complex.go +++ b/selector/ast/expression_complex.go @@ -46,11 +46,6 @@ type SpreadExpr struct{} func (SpreadExpr) expr() {} -func IsSpreadExpr(e Expr) bool { - _, ok := e.(SpreadExpr) - return ok -} - type RangeExpr struct { Start Expr End Expr diff --git a/selector/parser/parse_object.go b/selector/parser/parse_object.go index ca571aec..40566c7d 100644 --- a/selector/parser/parse_object.go +++ b/selector/parser/parse_object.go @@ -1,11 +1,22 @@ package parser import ( + "fmt" + "github.com/tomwright/dasel/v3/selector/ast" "github.com/tomwright/dasel/v3/selector/lexer" ) func parseObject(p *Parser) (ast.Expr, error) { + + //p.parseExpressionsFromTo( + // lexer.OpenCurly, + // lexer.CloseCurly, + // lexer.TokenKinds(lexer.Comma), + // false, + // bpDefault, + //) + if err := p.expect(lexer.OpenCurly); err != nil { return nil, err } @@ -13,72 +24,74 @@ func parseObject(p *Parser) (ast.Expr, error) { pairs := make([]ast.KeyValue, 0) - for { - if p.current().IsKind(lexer.CloseCurly) { - break - } - - if p.current().IsKind(lexer.Comma) { - p.advance() - continue + parseKeyValue := func() (ast.KeyValue, error) { + var res ast.KeyValue + k, err := p.parseExpression(bpDefault) + if err != nil { + return res, err } - if p.current().IsKind(lexer.Spread) { - p.advance() - pairs = append(pairs, ast.KeyValue{ - Key: ast.SpreadExpr{}, - Value: ast.SpreadExpr{}, - }) + // Handle spread + kSpread, isSpread := ast.LastAsType[ast.SpreadExpr](k) + if isSpread { + res.Key = kSpread + res.Value = ast.RemoveLast(k) if err := p.expect(lexer.Comma, lexer.CloseCurly); err != nil { - return nil, err + return res, err } - continue + return res, nil } - if p.current().IsKind(lexer.Symbol) && p.peek().IsKind(lexer.Comma, lexer.CloseCurly) { - // if the next token is a comma or close curly, then it is a shorthand property - pairs = append(pairs, ast.KeyValue{ - Key: ast.StringExpr{Value: p.current().Value}, - Value: ast.PropertyExpr{Property: ast.StringExpr{Value: p.current().Value}}, - }) - p.advance() - if err := p.expect(lexer.Comma, lexer.CloseCurly); err != nil { - return nil, err + kProp, kIsProp := ast.AsType[ast.PropertyExpr](k) + if p.current().IsKind(lexer.Comma, lexer.CloseCurly) { + if !kIsProp { + return res, fmt.Errorf("invalid shorthand property") } - continue - } - - key, err := p.parseExpression(bpDefault) - if err != nil { - return nil, err + res.Key = kProp.Property + res.Value = kProp + return res, nil } - // Attempt to simplify the key to a string expression. - if prop, ok := key.(ast.PropertyExpr); ok { - key = prop.Property + // Handle unquoted keys + if kIsProp { + if kStr, ok := ast.AsType[ast.StringExpr](kProp.Property); ok { + k = kStr + } } if err := p.expect(lexer.Colon); err != nil { - return nil, err + return res, err } p.advance() - val, err := p.parseExpression(bpDefault) + v, err := p.parseExpression(bpDefault) + if err != nil { + return res, err + } + + res.Key = k + res.Value = v + return res, nil + } + + for !p.current().IsKind(lexer.CloseCurly) { + kv, err := parseKeyValue() if err != nil { return nil, err } - pairs = append(pairs, ast.KeyValue{ - Key: key, - Value: val, - }) + pairs = append(pairs, kv) + if err := p.expect(lexer.Comma, lexer.CloseCurly); err != nil { - return nil, err + return nil, fmt.Errorf("expected end of object element: %w", err) + } + if p.current().IsKind(lexer.Comma) { + p.advance() } } if err := p.expect(lexer.CloseCurly); err != nil { - return nil, err + return nil, fmt.Errorf("expected end of object: %w", err) } p.advance() diff --git a/selector/parser/parser_test.go b/selector/parser/parser_test.go index d2f0965b..c564c835 100644 --- a/selector/parser/parser_test.go +++ b/selector/parser/parser_test.go @@ -317,13 +317,15 @@ func TestParser_Parse_HappyPath(t *testing.T) { t.Run("combine get set", run(t, testCase{ input: `{ ..., + nestedSpread..., foo, bar: 2, "baz": evalSomething(), "Name": "Tom", }`, expected: ast.ObjectExpr{Pairs: []ast.KeyValue{ - {Key: ast.SpreadExpr{}, Value: ast.SpreadExpr{}}, + {Key: ast.SpreadExpr{}, Value: nil}, + {Key: ast.SpreadExpr{}, Value: ast.PropertyExpr{Property: ast.StringExpr{Value: "nestedSpread"}}}, {Key: ast.StringExpr{Value: "foo"}, Value: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}}, {Key: ast.StringExpr{Value: "bar"}, Value: ast.NumberIntExpr{Value: 2}}, {Key: ast.StringExpr{Value: "baz"}, Value: ast.CallExpr{Function: "evalSomething"}}, From 8bbae1ee01e32914f22e84822ea39eb8fc2ddbba Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Sat, 19 Oct 2024 01:52:04 +0100 Subject: [PATCH 29/56] Ensure variables are propagated and update root command --- api.go | 3 +- bar.json | 1 + execution/execute.go | 44 +++++++------ execution/execute_array.go | 14 ++-- execution/execute_binary.go | 6 +- execution/execute_branch.go | 4 +- execution/execute_conditional.go | 8 +-- execution/execute_filter.go | 4 +- execution/execute_func.go | 10 +-- execution/execute_map.go | 4 +- execution/execute_object.go | 14 ++-- execution/options.go | 11 ++++ internal/cli/root.go | 107 +++++++++++++++++++++++++++---- model/value_metadata.go | 6 ++ 14 files changed, 168 insertions(+), 68 deletions(-) create mode 100644 bar.json diff --git a/api.go b/api.go index 02a677fe..0a3dec3d 100644 --- a/api.go +++ b/api.go @@ -8,8 +8,9 @@ import ( // Query queries the data using the selector and returns the results. func Query(data any, selector string, opts ...execution.ExecuteOptionFn) ([]*model.Value, int, error) { + options := execution.NewOptions(opts...) val := model.NewValue(data) - out, err := execution.ExecuteSelector(selector, val, opts...) + out, err := execution.ExecuteSelector(selector, val, options) if err != nil { return nil, 0, err } diff --git a/bar.json b/bar.json new file mode 100644 index 00000000..c0c1bfaf --- /dev/null +++ b/bar.json @@ -0,0 +1 @@ +{"x":1} diff --git a/execution/execute.go b/execution/execute.go index 03a97dcf..7fa414b7 100644 --- a/execution/execute.go +++ b/execution/execute.go @@ -9,7 +9,7 @@ import ( ) // ExecuteSelector parses the selector and executes the resulting AST with the given input. -func ExecuteSelector(selectorStr string, value *model.Value, opts ...ExecuteOptionFn) (*model.Value, error) { +func ExecuteSelector(selectorStr string, value *model.Value, opts *Options) (*model.Value, error) { if selectorStr == "" { return value, nil } @@ -19,7 +19,7 @@ func ExecuteSelector(selectorStr string, value *model.Value, opts ...ExecuteOpti return nil, fmt.Errorf("error parsing selector: %w", err) } - res, err := ExecuteAST(expr, value, opts...) + res, err := ExecuteAST(expr, value, opts) if err != nil { return nil, fmt.Errorf("error executing selector: %w", err) } @@ -30,9 +30,7 @@ func ExecuteSelector(selectorStr string, value *model.Value, opts ...ExecuteOpti type expressionExecutor func(data *model.Value) (*model.Value, error) // ExecuteAST executes the given AST with the given input. -func ExecuteAST(expr ast.Expr, value *model.Value, opts ...ExecuteOptionFn) (*model.Value, error) { - options := NewOptions(opts...) - +func ExecuteAST(expr ast.Expr, value *model.Value, options *Options) (*model.Value, error) { if expr == nil { return value, nil } @@ -69,21 +67,21 @@ func ExecuteAST(expr ast.Expr, value *model.Value, opts ...ExecuteOptionFn) (*mo func exprExecutor(opts *Options, expr ast.Expr) (expressionExecutor, error) { switch e := expr.(type) { case ast.BinaryExpr: - return binaryExprExecutor(e) + return binaryExprExecutor(opts, e) case ast.CallExpr: return callExprExecutor(opts, e) case ast.ChainedExpr: - return chainedExprExecutor(e) + return chainedExprExecutor(opts, e) case ast.SpreadExpr: return spreadExprExecutor() case ast.RangeExpr: - return rangeExprExecutor(e) + return rangeExprExecutor(opts, e) case ast.IndexExpr: - return indexExprExecutor(e) + return indexExprExecutor(opts, e) case ast.PropertyExpr: - return propertyExprExecutor(e) + return propertyExprExecutor(opts, e) case ast.VariableExpr: - return variableExprExecutor(e) + return variableExprExecutor(opts, e) case ast.NumberIntExpr: return numberIntExprExecutor(e) case ast.NumberFloatExpr: @@ -93,17 +91,17 @@ func exprExecutor(opts *Options, expr ast.Expr) (expressionExecutor, error) { case ast.BoolExpr: return boolExprExecutor(e) case ast.ObjectExpr: - return objectExprExecutor(e) + return objectExprExecutor(opts, e) case ast.MapExpr: - return mapExprExecutor(e) + return mapExprExecutor(opts, e) case ast.FilterExpr: - return filterExprExecutor(e) + return filterExprExecutor(opts, e) case ast.ConditionalExpr: - return conditionalExprExecutor(e) + return conditionalExprExecutor(opts, e) case ast.BranchExpr: - return branchExprExecutor(e) + return branchExprExecutor(opts, e) case ast.ArrayExpr: - return arrayExprExecutor(e) + return arrayExprExecutor(opts, e) case ast.RegexExpr: // Noop return func(data *model.Value) (*model.Value, error) { @@ -114,10 +112,10 @@ func exprExecutor(opts *Options, expr ast.Expr) (expressionExecutor, error) { } } -func chainedExprExecutor(e ast.ChainedExpr) (expressionExecutor, error) { +func chainedExprExecutor(options *Options, e ast.ChainedExpr) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { for _, expr := range e.Exprs { - res, err := ExecuteAST(expr, data) + res, err := ExecuteAST(expr, data, options) if err != nil { return nil, fmt.Errorf("error executing expression: %w", err) } @@ -127,12 +125,16 @@ func chainedExprExecutor(e ast.ChainedExpr) (expressionExecutor, error) { }, nil } -func variableExprExecutor(e ast.VariableExpr) (expressionExecutor, error) { +func variableExprExecutor(opts *Options, e ast.VariableExpr) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { varName := e.Name if varName == "this" { return data, nil } - return nil, fmt.Errorf("variable %s not found", varName) + res, ok := opts.Vars[varName] + if !ok { + return nil, fmt.Errorf("variable %s not found", varName) + } + return res, nil }, nil } diff --git a/execution/execute_array.go b/execution/execute_array.go index b4b9b239..ab770982 100644 --- a/execution/execute_array.go +++ b/execution/execute_array.go @@ -7,12 +7,12 @@ import ( "github.com/tomwright/dasel/v3/selector/ast" ) -func arrayExprExecutor(e ast.ArrayExpr) (expressionExecutor, error) { +func arrayExprExecutor(opts *Options, e ast.ArrayExpr) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { res := model.NewSliceValue() for _, expr := range e.Exprs { - el, err := ExecuteAST(expr, data) + el, err := ExecuteAST(expr, data, opts) if err != nil { return nil, err } @@ -25,11 +25,11 @@ func arrayExprExecutor(e ast.ArrayExpr) (expressionExecutor, error) { }, nil } -func rangeExprExecutor(e ast.RangeExpr) (expressionExecutor, error) { +func rangeExprExecutor(opts *Options, e ast.RangeExpr) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { var start, end int64 = -1, -1 if e.Start != nil { - startE, err := ExecuteAST(e.Start, data) + startE, err := ExecuteAST(e.Start, data, opts) if err != nil { return nil, fmt.Errorf("error evaluating start expression: %w", err) } @@ -41,7 +41,7 @@ func rangeExprExecutor(e ast.RangeExpr) (expressionExecutor, error) { } if e.End != nil { - endE, err := ExecuteAST(e.End, data) + endE, err := ExecuteAST(e.End, data, opts) if err != nil { return nil, fmt.Errorf("error evaluating end expression: %w", err) } @@ -61,9 +61,9 @@ func rangeExprExecutor(e ast.RangeExpr) (expressionExecutor, error) { }, nil } -func indexExprExecutor(e ast.IndexExpr) (expressionExecutor, error) { +func indexExprExecutor(opts *Options, e ast.IndexExpr) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { - indexE, err := ExecuteAST(e.Index, data) + indexE, err := ExecuteAST(e.Index, data, opts) if err != nil { return nil, fmt.Errorf("error evaluating index expression: %w", err) } diff --git a/execution/execute_binary.go b/execution/execute_binary.go index 41a9f38b..3058857e 100644 --- a/execution/execute_binary.go +++ b/execution/execute_binary.go @@ -8,13 +8,13 @@ import ( "github.com/tomwright/dasel/v3/selector/lexer" ) -func binaryExprExecutor(e ast.BinaryExpr) (expressionExecutor, error) { +func binaryExprExecutor(opts *Options, e ast.BinaryExpr) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { - left, err := ExecuteAST(e.Left, data) + left, err := ExecuteAST(e.Left, data, opts) if err != nil { return nil, fmt.Errorf("error evaluating left expression: %w", err) } - right, err := ExecuteAST(e.Right, data) + right, err := ExecuteAST(e.Right, data, opts) if err != nil { return nil, fmt.Errorf("error evaluating right expression: %w", err) } diff --git a/execution/execute_branch.go b/execution/execute_branch.go index 6dd697cd..86699c14 100644 --- a/execution/execute_branch.go +++ b/execution/execute_branch.go @@ -7,12 +7,12 @@ import ( "github.com/tomwright/dasel/v3/selector/ast" ) -func branchExprExecutor(e ast.BranchExpr) (expressionExecutor, error) { +func branchExprExecutor(opts *Options, 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) + r, err := ExecuteAST(expr, data, opts) if err != nil { return nil, fmt.Errorf("failed to execute branch expr: %w", err) } diff --git a/execution/execute_conditional.go b/execution/execute_conditional.go index e21abcd5..bda61c60 100644 --- a/execution/execute_conditional.go +++ b/execution/execute_conditional.go @@ -7,9 +7,9 @@ import ( "github.com/tomwright/dasel/v3/selector/ast" ) -func conditionalExprExecutor(e ast.ConditionalExpr) (expressionExecutor, error) { +func conditionalExprExecutor(opts *Options, e ast.ConditionalExpr) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { - cond, err := ExecuteAST(e.Cond, data) + cond, err := ExecuteAST(e.Cond, data, opts) if err != nil { return nil, fmt.Errorf("error evaluating condition: %w", err) } @@ -20,7 +20,7 @@ func conditionalExprExecutor(e ast.ConditionalExpr) (expressionExecutor, error) } if condBool { - res, err := ExecuteAST(e.Then, data) + res, err := ExecuteAST(e.Then, data, opts) if err != nil { return nil, fmt.Errorf("error executing then block: %w", err) } @@ -28,7 +28,7 @@ func conditionalExprExecutor(e ast.ConditionalExpr) (expressionExecutor, error) } if e.Else != nil { - res, err := ExecuteAST(e.Else, data) + res, err := ExecuteAST(e.Else, data, opts) if err != nil { return nil, fmt.Errorf("error executing else block: %w", err) } diff --git a/execution/execute_filter.go b/execution/execute_filter.go index edf5d300..a86c8f1e 100644 --- a/execution/execute_filter.go +++ b/execution/execute_filter.go @@ -7,7 +7,7 @@ import ( "github.com/tomwright/dasel/v3/selector/ast" ) -func filterExprExecutor(e ast.FilterExpr) (expressionExecutor, error) { +func filterExprExecutor(opts *Options, e ast.FilterExpr) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { if !data.IsSlice() { return nil, fmt.Errorf("cannot filter over non-array") @@ -15,7 +15,7 @@ func filterExprExecutor(e ast.FilterExpr) (expressionExecutor, error) { res := model.NewSliceValue() if err := data.RangeSlice(func(i int, item *model.Value) error { - v, err := ExecuteAST(e.Expr, item) + v, err := ExecuteAST(e.Expr, item, opts) if err != nil { return err } diff --git a/execution/execute_func.go b/execution/execute_func.go index 64f0d873..4712ab90 100644 --- a/execution/execute_func.go +++ b/execution/execute_func.go @@ -7,10 +7,10 @@ import ( "github.com/tomwright/dasel/v3/selector/ast" ) -func prepareArgs(data *model.Value, argsE ast.Expressions) (model.Values, error) { +func prepareArgs(opts *Options, data *model.Value, argsE ast.Expressions) (model.Values, error) { args := make(model.Values, 0) for i, arg := range argsE { - res, err := ExecuteAST(arg, data) + res, err := ExecuteAST(arg, data, opts) if err != nil { return nil, fmt.Errorf("error evaluating argument %d: %w", i, err) } @@ -25,9 +25,9 @@ func prepareArgs(data *model.Value, argsE ast.Expressions) (model.Values, error) return args, nil } -func callFnExecutor(f FuncFn, argsE ast.Expressions) (expressionExecutor, error) { +func callFnExecutor(opts *Options, f FuncFn, argsE ast.Expressions) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { - args, err := prepareArgs(data, argsE) + args, err := prepareArgs(opts, data, argsE) if err != nil { return nil, fmt.Errorf("error preparing arguments: %w", err) } @@ -43,7 +43,7 @@ func callFnExecutor(f FuncFn, argsE ast.Expressions) (expressionExecutor, error) func callExprExecutor(opts *Options, e ast.CallExpr) (expressionExecutor, error) { if f, ok := opts.Funcs.Get(e.Function); ok { - res, err := callFnExecutor(f, e.Args) + res, err := callFnExecutor(opts, f, e.Args) if err != nil { return nil, fmt.Errorf("error executing function %q: %w", e.Function, err) } diff --git a/execution/execute_map.go b/execution/execute_map.go index 41f175da..d5efcd88 100644 --- a/execution/execute_map.go +++ b/execution/execute_map.go @@ -7,7 +7,7 @@ import ( "github.com/tomwright/dasel/v3/selector/ast" ) -func mapExprExecutor(e ast.MapExpr) (expressionExecutor, error) { +func mapExprExecutor(opts *Options, e ast.MapExpr) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { if !data.IsSlice() { return nil, fmt.Errorf("cannot map over non-array") @@ -15,7 +15,7 @@ func mapExprExecutor(e ast.MapExpr) (expressionExecutor, error) { res := model.NewSliceValue() if err := data.RangeSlice(func(i int, item *model.Value) error { - item, err := ExecuteAST(e.Expr, item) + item, err := ExecuteAST(e.Expr, item, opts) if err != nil { return err } diff --git a/execution/execute_object.go b/execution/execute_object.go index b1a9da6f..74d6db81 100644 --- a/execution/execute_object.go +++ b/execution/execute_object.go @@ -7,7 +7,7 @@ import ( "github.com/tomwright/dasel/v3/selector/ast" ) -func objectExprExecutor(e ast.ObjectExpr) (expressionExecutor, error) { +func objectExprExecutor(opts *Options, e ast.ObjectExpr) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { obj := model.NewMapValue() for _, p := range e.Pairs { @@ -17,9 +17,9 @@ func objectExprExecutor(e ast.ObjectExpr) (expressionExecutor, error) { var err error if p.Value != nil { // We need to spread the resulting value. - val, err = ExecuteAST(p.Value, data) + val, err = ExecuteAST(p.Value, data, opts) if err != nil { - return nil, fmt.Errorf("error evaluated spread values") + return nil, fmt.Errorf("error evaluating spread values: %w", err) } } else { val = data @@ -52,14 +52,14 @@ func objectExprExecutor(e ast.ObjectExpr) (expressionExecutor, error) { // return nil, fmt.Errorf("cannot spread object key name") //} - key, err := ExecuteAST(p.Key, data) + key, err := ExecuteAST(p.Key, data, opts) if err != nil { return nil, fmt.Errorf("error evaluating key: %w", err) } if !key.IsString() { return nil, fmt.Errorf("expected key to resolve to string, got %s", key.Type()) } - val, err := ExecuteAST(p.Value, data) + val, err := ExecuteAST(p.Value, data, opts) if err != nil { return nil, fmt.Errorf("error evaluating value: %w", err) } @@ -72,9 +72,9 @@ func objectExprExecutor(e ast.ObjectExpr) (expressionExecutor, error) { }, nil } -func propertyExprExecutor(e ast.PropertyExpr) (expressionExecutor, error) { +func propertyExprExecutor(opts *Options, e ast.PropertyExpr) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { - key, err := ExecuteAST(e.Property, data) + key, err := ExecuteAST(e.Property, data, opts) if err != nil { return nil, fmt.Errorf("error evaluating property: %w", err) } diff --git a/execution/options.go b/execution/options.go index 99f9920f..0f425315 100644 --- a/execution/options.go +++ b/execution/options.go @@ -1,17 +1,21 @@ package execution +import "github.com/tomwright/dasel/v3/model" + // ExecuteOptionFn is a function that can be used to set options on the execution of the selector. type ExecuteOptionFn func(*Options) // Options contains the options for the execution of the selector. type Options struct { Funcs FuncCollection + Vars map[string]*model.Value } // NewOptions creates a new Options struct with the given options. func NewOptions(opts ...ExecuteOptionFn) *Options { o := &Options{ Funcs: DefaultFuncCollection, + Vars: map[string]*model.Value{}, } for _, opt := range opts { if opt == nil { @@ -28,3 +32,10 @@ func WithFuncs(fc FuncCollection) ExecuteOptionFn { o.Funcs = fc } } + +// WithVariable sets a variable for use in the selector. +func WithVariable(key string, val *model.Value) ExecuteOptionFn { + return func(o *Options) { + o.Vars[key] = val + } +} diff --git a/internal/cli/root.go b/internal/cli/root.go index 44522a16..b29b71bb 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -1,54 +1,132 @@ package cli import ( + "errors" "fmt" "io" + "os" + "strings" "github.com/spf13/cobra" "github.com/tomwright/dasel/v3/execution" "github.com/tomwright/dasel/v3/internal" + "github.com/tomwright/dasel/v3/model" "github.com/tomwright/dasel/v3/parsing" ) +// ErrBadVarArg is returned when an invalid variable argument is provided. +var ErrBadVarArg = errors.New("invalid variable format, expect foo=bar, or foo=format:path") + +// varOptFromArg attempts to parse a variable declaration from a commandline argument. +func varOptFromArg(arg string, r parsing.Reader) (execution.ExecuteOptionFn, error) { + kv := strings.SplitN(arg, "=", 2) + if len(kv) != 2 { + return nil, ErrBadVarArg + } + + varName := kv[0] + + formatData := strings.SplitN(kv[1], ":", 2) + + reader := r + filepath := kv[1] + + if len(formatData) == 2 { + var err error + reader, err = parsing.NewReader(parsing.Format(formatData[0])) + if err != nil { + return nil, err + } + filepath = formatData[1] + } else if reader == nil { + return nil, fmt.Errorf("variable file format required") + } + + f, err := os.Open(filepath) + if err != nil { + return nil, err + } + defer f.Close() + + inputBytes, err := io.ReadAll(f) + if err != nil { + return nil, err + } + + value, err := reader.Read(inputBytes) + if err != nil { + return nil, fmt.Errorf("failed to read file contents for variable %q: %w", varName, err) + } + + return execution.WithVariable(varName, value), nil +} + +// RootCmd returns the root cli command. func RootCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "dasel", + Use: "dasel [flags] [variableName=fileFormat:filePath] selector", Short: "Query and modify data structures using selectors", Long: `dasel is a command-line utility to query and modify data structures using selectors.`, Version: internal.Version, - Args: cobra.MaximumNArgs(1), + Args: nil, + Example: `dasel -o json foo=json:bar.json '{"x": $foo.x, "y": $foo.x + 1}'`, RunE: func(cmd *cobra.Command, args []string) error { selectorStr := "" if len(args) > 0 { - selectorStr = args[0] + selectorStr = args[len(args)-1] + args = args[0 : len(args)-1] } + var opts []execution.ExecuteOptionFn + readerStr, _ := cmd.Flags().GetString("input") writerStr, _ := cmd.Flags().GetString("output") if writerStr == "" { writerStr = readerStr } - reader, err := parsing.NewReader(parsing.Format(readerStr)) - if err != nil { - return fmt.Errorf("failed to get input reader: %w", err) + var reader parsing.Reader + var err error + if len(readerStr) > 0 { + reader, err = parsing.NewReader(parsing.Format(readerStr)) + if err != nil { + return fmt.Errorf("failed to get input reader: %w", err) + } } + writer, err := parsing.NewWriter(parsing.Format(writerStr)) if err != nil { return fmt.Errorf("failed to get output writer: %w", err) } - inputBytes, err := io.ReadAll(cmd.InOrStdin()) - if err != nil { - return fmt.Errorf("error reading input: %w", err) + for _, a := range args { + o, err := varOptFromArg(a, reader) + if err != nil { + return fmt.Errorf("failed to process variable: %w", err) + } + opts = append(opts, o) } - inputData, err := reader.Read(inputBytes) - if err != nil { - return fmt.Errorf("error reading input: %w", err) + var inputBytes []byte + if reader != nil { + inputBytes, err = io.ReadAll(cmd.InOrStdin()) + if err != nil { + return fmt.Errorf("error reading input: %w", err) + } } - outputData, err := execution.ExecuteSelector(selectorStr, inputData) + var inputData *model.Value + if len(inputBytes) > 0 { + inputData, err = reader.Read(inputBytes) + if err != nil { + return fmt.Errorf("error reading input: %w", err) + } + opts = append(opts, execution.WithVariable("root", inputData)) + } + + options := execution.NewOptions(opts...) + + outputData, err := execution.ExecuteSelector(selectorStr, inputData, options) if err != nil { return err } @@ -70,7 +148,8 @@ func RootCmd() *cobra.Command { cmd.Flags().StringP("input", "i", "", "The format of the input data. Can be one of: json, yaml, toml, xml, csv") cmd.Flags().StringP("output", "o", "", "The format of the output data. Can be one of: json, yaml, toml, xml, csv") - cmd.AddCommand(manCommand(cmd)) + // TODO : apply fallback to root cmd + //cmd.AddCommand(manCommand(cmd)) return cmd } diff --git a/model/value_metadata.go b/model/value_metadata.go index bed68b80..ad36f2a4 100644 --- a/model/value_metadata.go +++ b/model/value_metadata.go @@ -20,6 +20,9 @@ func (v *Value) SetMetadataValue(key string, val any) { // IsSpread returns true if the value is a spread value. // Spread values are used to represent the spread operator. func (v *Value) IsSpread() bool { + if v == nil { + return false + } val, ok := v.Metadata["spread"] if !ok { return false @@ -39,6 +42,9 @@ func (v *Value) MarkAsSpread() { // IsBranch returns true if the value is a branched value. func (v *Value) IsBranch() bool { + if v == nil { + return false + } val, ok := v.Metadata["spread"] if !ok { return false From 7a9a9241f6d82be1699cdfdf867301c836908a18 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Sat, 19 Oct 2024 01:53:10 +0100 Subject: [PATCH 30/56] Fix tests --- execution/execute_test.go | 2 +- internal/cli/man_test.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/execution/execute_test.go b/execution/execute_test.go index e104681c..a776f254 100644 --- a/execution/execute_test.go +++ b/execution/execute_test.go @@ -33,7 +33,7 @@ func TestExecuteSelector_HappyPath(t *testing.T) { if tc.outFn != nil { exp = tc.outFn() } - res, err := execution.ExecuteSelector(tc.s, in) + res, err := execution.ExecuteSelector(tc.s, in, execution.NewOptions()) if err != nil { t.Fatal(err) } diff --git a/internal/cli/man_test.go b/internal/cli/man_test.go index 1775a53b..9a33cd3c 100644 --- a/internal/cli/man_test.go +++ b/internal/cli/man_test.go @@ -6,6 +6,7 @@ import ( ) func TestManCommand(t *testing.T) { + t.Skip("Temporarily disabled") tempDir := t.TempDir() _, _, err := runDasel([]string{"man", "-o", tempDir}, nil) From 15bffc61b71f23f616de2634e205bf6a1ae54e54 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Sat, 19 Oct 2024 01:56:51 +0100 Subject: [PATCH 31/56] Fix toString and remove test file --- bar.json | 1 - execution/func.go | 12 ++++++------ 2 files changed, 6 insertions(+), 7 deletions(-) delete mode 100644 bar.json diff --git a/bar.json b/bar.json deleted file mode 100644 index c0c1bfaf..00000000 --- a/bar.json +++ /dev/null @@ -1 +0,0 @@ -{"x":1} diff --git a/execution/func.go b/execution/func.go index ad41dce3..687f1fd3 100644 --- a/execution/func.go +++ b/execution/func.go @@ -184,29 +184,29 @@ var ( FuncToString = NewFunc( "toString", func(data *model.Value, args model.Values) (*model.Value, error) { - switch data.Type() { + switch args[0].Type() { case model.TypeString: - return data, nil + return args[0], nil case model.TypeInt: - i, err := data.IntValue() + i, err := args[0].IntValue() if err != nil { return nil, err } return model.NewStringValue(fmt.Sprintf("%d", i)), nil case model.TypeFloat: - i, err := data.FloatValue() + i, err := args[0].FloatValue() if err != nil { return nil, err } return model.NewStringValue(fmt.Sprintf("%f", i)), nil case model.TypeBool: - i, err := data.BoolValue() + i, err := args[0].BoolValue() if err != nil { return nil, err } return model.NewStringValue(fmt.Sprintf("%v", i)), nil default: - return nil, fmt.Errorf("cannot convert %s to string", data.Type()) + return nil, fmt.Errorf("cannot convert %s to string", args[0].Type()) } }, ValidateArgsExactly(1), From 5d6576e0944d647300a7ca7d78e24743f3c2678a Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Sat, 19 Oct 2024 19:14:03 +0100 Subject: [PATCH 32/56] Add reverse() --- execution/func.go | 42 +++++++++++++++++++++++++++++++++++- selector/parser/parse_map.go | 12 ++++------- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/execution/func.go b/execution/func.go index 687f1fd3..f687606a 100644 --- a/execution/func.go +++ b/execution/func.go @@ -64,7 +64,11 @@ func (f *Func) Handler() FuncFn { return nil, err } } - return f.handler(data, args) + res, err := f.handler(data, args) + if err != nil { + return nil, fmt.Errorf("error execution func %q: %w", f.name, err) + } + return res, nil } } @@ -126,6 +130,7 @@ var ( FuncAdd, FuncToString, FuncMerge, + FuncReverse, ) // FuncLen is a function that returns the length of the given value. @@ -257,4 +262,39 @@ var ( }, ValidateArgsMin(1), ) + + // FuncReverse is a function that reverses the input. + FuncReverse = NewFunc( + "reverse", + func(data *model.Value, args model.Values) (*model.Value, error) { + if len(args) == 1 { + return args[0], nil + } + + arg := args[0] + + switch arg.Type() { + case model.TypeString: + v, err := arg.StringValue() + if err != nil { + return nil, err + } + vBytes := []byte(v) + res := string(vBytes[len(vBytes)-1 : 0]) + return model.NewStringValue(res), nil + case model.TypeSlice: + l, err := arg.Len() + if err != nil { + return nil, err + } + if l <= 1 { + return arg, nil + } + return arg.SliceIndexRange(l-1, 0) + default: + return nil, fmt.Errorf("reverse expects a slice or string, got %s", arg.Type()) + } + }, + ValidateArgsExactly(1), + ) ) diff --git a/selector/parser/parse_map.go b/selector/parser/parse_map.go index 6e0cfe35..589329b9 100644 --- a/selector/parser/parse_map.go +++ b/selector/parser/parse_map.go @@ -9,15 +9,11 @@ func parseMap(p *Parser) (ast.Expr, error) { if err := p.expect(lexer.Map); 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}, + expr, err := p.parseExpressionsFromTo( + lexer.OpenParen, + lexer.CloseParen, []lexer.TokenKind{}, true, bpDefault, @@ -27,7 +23,7 @@ func parseMap(p *Parser) (ast.Expr, error) { } return ast.MapExpr{ - Expr: ast.ChainExprs(expressions...), + Expr: expr, }, nil } From 1b27839186df538e20b956a956735c0cf0e57d0a Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Sun, 20 Oct 2024 23:11:33 +0100 Subject: [PATCH 33/56] Update tests and fix range --- execution/execute_array.go | 17 +- execution/execute_array_test.go | 111 ++++ execution/execute_binary_test.go | 181 ++++++ execution/execute_branch_test.go | 58 ++ execution/execute_conditional_test.go | 56 ++ execution/execute_filter_test.go | 73 +++ execution/execute_func_test.go | 49 ++ execution/execute_literal_test.go | 113 ++++ execution/execute_map_test.go | 44 ++ execution/execute_object_test.go | 109 ++++ execution/execute_test.go | 886 ++------------------------ execution/func.go | 187 +----- execution/func_add.go | 43 ++ execution/func_add_test.go | 53 ++ execution/func_len.go | 19 + execution/func_merge.go | 53 ++ execution/func_merge_test.go | 50 ++ execution/func_reverse.go | 25 + execution/func_reverse_test.go | 31 + execution/func_to_string.go | 39 ++ model/value_literal.go | 50 ++ model/value_slice.go | 23 +- model/value_slice_test.go | 14 +- 23 files changed, 1253 insertions(+), 1031 deletions(-) create mode 100644 execution/execute_array_test.go create mode 100644 execution/execute_binary_test.go create mode 100644 execution/execute_branch_test.go create mode 100644 execution/execute_conditional_test.go create mode 100644 execution/execute_filter_test.go create mode 100644 execution/execute_func_test.go create mode 100644 execution/execute_literal_test.go create mode 100644 execution/execute_map_test.go create mode 100644 execution/execute_object_test.go create mode 100644 execution/func_add.go create mode 100644 execution/func_add_test.go create mode 100644 execution/func_len.go create mode 100644 execution/func_merge.go create mode 100644 execution/func_merge_test.go create mode 100644 execution/func_reverse.go create mode 100644 execution/func_reverse_test.go create mode 100644 execution/func_to_string.go diff --git a/execution/execute_array.go b/execution/execute_array.go index ab770982..99f89e27 100644 --- a/execution/execute_array.go +++ b/execution/execute_array.go @@ -27,7 +27,7 @@ func arrayExprExecutor(opts *Options, e ast.ArrayExpr) (expressionExecutor, erro func rangeExprExecutor(opts *Options, e ast.RangeExpr) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { - var start, end int64 = -1, -1 + var start, end int64 = 0, -1 if e.Start != nil { startE, err := ExecuteAST(e.Start, data, opts) if err != nil { @@ -52,9 +52,20 @@ func rangeExprExecutor(opts *Options, e ast.RangeExpr) (expressionExecutor, erro } } - res, err := data.SliceIndexRange(int(start), int(end)) + var res *model.Value + var err error + + switch data.Type() { + case model.TypeString: + res, err = data.StringIndexRange(int(start), int(end)) + case model.TypeSlice: + res, err = data.SliceIndexRange(int(start), int(end)) + default: + err = fmt.Errorf("range expects a slice or string, got %s", data.Type()) + } + if err != nil { - return nil, fmt.Errorf("error getting slice index range: %w", err) + return nil, err } return res, nil diff --git a/execution/execute_array_test.go b/execution/execute_array_test.go new file mode 100644 index 00000000..1b13bdda --- /dev/null +++ b/execution/execute_array_test.go @@ -0,0 +1,111 @@ +package execution_test + +import ( + "testing" + + "github.com/tomwright/dasel/v3/model" +) + +func TestArray(t *testing.T) { + inSlice := func() *model.Value { + s := model.NewSliceValue() + if err := s.Append(model.NewIntValue(1)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := s.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := s.Append(model.NewIntValue(3)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + return s + } + inMap := func() *model.Value { + m := model.NewMapValue() + if err := m.SetMapKey("numbers", inSlice()); err != nil { + t.Fatalf("unexpected error: %s", err) + } + return m + } + + runArrayTests := func(in func() *model.Value, prefix string) func(t *testing.T) { + return func(t *testing.T) { + t.Run("1:2", testCase{ + s: prefix + `[1:2]`, + inFn: in, + outFn: func() *model.Value { + res := model.NewSliceValue() + if err := res.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := res.Append(model.NewIntValue(3)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + return res + }, + }.run) + t.Run("1:0", testCase{ + s: prefix + `[1:0]`, + inFn: in, + outFn: func() *model.Value { + res := model.NewSliceValue() + if err := res.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := res.Append(model.NewIntValue(1)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + return res + }, + }.run) + t.Run("1:", testCase{ + s: prefix + `[1:]`, + inFn: in, + outFn: func() *model.Value { + res := model.NewSliceValue() + if err := res.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := res.Append(model.NewIntValue(3)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + return res + }, + }.run) + t.Run(":1", testCase{ + s: prefix + `[:1]`, + inFn: in, + outFn: func() *model.Value { + res := model.NewSliceValue() + if err := res.Append(model.NewIntValue(1)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := res.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + return res + }, + }.run) + t.Run("reverse", testCase{ + s: prefix + `[len($this)-1:0]`, + inFn: in, + outFn: func() *model.Value { + res := model.NewSliceValue() + if err := res.Append(model.NewIntValue(3)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := res.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := res.Append(model.NewIntValue(1)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + return res + }, + }.run) + } + } + + t.Run("direct to slice", runArrayTests(inSlice, "$this")) + t.Run("property to slice", runArrayTests(inMap, "numbers")) +} diff --git a/execution/execute_binary_test.go b/execution/execute_binary_test.go new file mode 100644 index 00000000..0d386a4f --- /dev/null +++ b/execution/execute_binary_test.go @@ -0,0 +1,181 @@ +package execution_test + +import ( + "testing" + + "github.com/tomwright/dasel/v3/dencoding" + "github.com/tomwright/dasel/v3/model" +) + +func TestBinary(t *testing.T) { + t.Run("math", func(t *testing.T) { + t.Run("literals", func(t *testing.T) { + t.Run("addition", testCase{ + s: `1 + 2`, + out: model.NewIntValue(3), + }.run) + t.Run("subtraction", testCase{ + s: `5 - 2`, + out: model.NewIntValue(3), + }.run) + t.Run("multiplication", testCase{ + s: `5 * 2`, + out: model.NewIntValue(10), + }.run) + t.Run("division", testCase{ + s: `10 / 2`, + out: model.NewIntValue(5), + }.run) + t.Run("modulus", testCase{ + s: `10 % 3`, + out: model.NewIntValue(1), + }.run) + t.Run("ordering", testCase{ + s: `45.2 + 5 * 4 - 2 / 2`, // 45.2 + (5 * 4) - (2 / 2) = 45.2 + 20 - 1 = 64.2 + out: model.NewFloatValue(64.2), + }.run) + t.Run("ordering with groups", testCase{ + s: `(45.2 + 5) * ((4 - 2) / 2)`, // (45.2 + 5) * ((4 - 2) / 2) = (50.2) * ((2) / 2) = (50.2) * (1) = 50.2 + out: model.NewFloatValue(50.2), + }.run) + }) + t.Run("variables", func(t *testing.T) { + in := func() *model.Value { + return model.NewValue(dencoding.NewMap(). + Set("one", 1). + Set("two", 2). + Set("three", 3). + Set("four", 4). + Set("five", 5). + Set("six", 6). + Set("seven", 7). + Set("eight", 8). + Set("nine", 9). + Set("ten", 10). + Set("fortyfivepoint2", 45.2)) + } + t.Run("addition", testCase{ + inFn: in, + s: `one + two`, + out: model.NewIntValue(3), + }.run) + t.Run("subtraction", testCase{ + inFn: in, + s: `five - two`, + out: model.NewIntValue(3), + }.run) + t.Run("multiplication", testCase{ + inFn: in, + s: `five * two`, + out: model.NewIntValue(10), + }.run) + t.Run("division", testCase{ + inFn: in, + s: `ten / two`, + out: model.NewIntValue(5), + }.run) + t.Run("modulus", testCase{ + inFn: in, + s: `ten % three`, + out: model.NewIntValue(1), + }.run) + t.Run("ordering", testCase{ + inFn: in, + s: `fortyfivepoint2 + five * four - two / two`, // 45.2 + (5 * 4) - (2 / 2) = 45.2 + 20 - 1 = 64.2 + out: model.NewFloatValue(64.2), + }.run) + t.Run("ordering with groups", testCase{ + inFn: in, + s: `(fortyfivepoint2 + five) * ((four - two) / two)`, // (45.2 + 5) * ((4 - 2) / 2) = (50.2) * ((2) / 2) = (50.2) * (1) = 50.2 + out: model.NewFloatValue(50.2), + }.run) + }) + }) + t.Run("comparison", func(t *testing.T) { + t.Run("literals", func(t *testing.T) { + t.Run("equal", testCase{ + s: `1 == 1`, + out: model.NewBoolValue(true), + }.run) + t.Run("not equal", testCase{ + s: `1 != 1`, + out: model.NewBoolValue(false), + }.run) + t.Run("greater than", testCase{ + s: `2 > 1`, + out: model.NewBoolValue(true), + }.run) + t.Run("greater than or equal", testCase{ + s: `2 >= 2`, + out: model.NewBoolValue(true), + }.run) + t.Run("less than", testCase{ + s: `1 < 2`, + out: model.NewBoolValue(true), + }.run) + t.Run("less than or equal", testCase{ + s: `2 <= 2`, + out: model.NewBoolValue(true), + }.run) + t.Run("like", testCase{ + s: `"hello world" =~ r/ello/`, + out: model.NewBoolValue(true), + }.run) + t.Run("not like", testCase{ + s: `"hello world" !~ r/helloworld/`, + out: model.NewBoolValue(true), + }.run) + }) + + t.Run("variables", func(t *testing.T) { + in := func() *model.Value { + return model.NewValue(dencoding.NewMap(). + Set("one", 1). + Set("two", 2). + Set("nested", dencoding.NewMap(). + Set("three", 3). + Set("four", 4))) + } + t.Run("equal", testCase{ + inFn: in, + s: `one == one`, + out: model.NewBoolValue(true), + }.run) + t.Run("not equal", testCase{ + inFn: in, + s: `one != one`, + out: model.NewBoolValue(false), + }.run) + t.Run("greater than", testCase{ + inFn: in, + s: `two > one`, + out: model.NewBoolValue(true), + }.run) + t.Run("greater than or equal", testCase{ + inFn: in, + s: `two >= two`, + out: model.NewBoolValue(true), + }.run) + t.Run("less than", testCase{ + inFn: in, + s: `one < two`, + out: model.NewBoolValue(true), + }.run) + t.Run("less than or equal", testCase{ + inFn: in, + s: `two <= two`, + out: model.NewBoolValue(true), + }.run) + t.Run("nested with math more than", testCase{ + inFn: in, + s: `nested.three + nested.four * 0 > one * 1`, + out: model.NewBoolValue(true), + }.run) + t.Run("nested with grouped math more than", testCase{ + inFn: in, + s: `(nested.three + nested.four) * 0 > one * 1`, + out: model.NewBoolValue(false), + }.run) + }) + }) +} diff --git a/execution/execute_branch_test.go b/execution/execute_branch_test.go new file mode 100644 index 00000000..6d33b8ad --- /dev/null +++ b/execution/execute_branch_test.go @@ -0,0 +1,58 @@ +package execution_test + +import ( + "testing" + + "github.com/tomwright/dasel/v3/model" +) + +func TestBranch(t *testing.T) { + t.Run("single branch", 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 + }, + }.run) + t.Run("many branches", 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 + }, + }.run) + t.Run("spread into many branches", testCase{ + s: "[1,2,3].branch(...)", + 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) + } + return r + }, + }.run) +} diff --git a/execution/execute_conditional_test.go b/execution/execute_conditional_test.go new file mode 100644 index 00000000..ff77ae66 --- /dev/null +++ b/execution/execute_conditional_test.go @@ -0,0 +1,56 @@ +package execution_test + +import ( + "testing" + + "github.com/tomwright/dasel/v3/model" +) + +func TestConditional(t *testing.T) { + t.Run("true", testCase{ + s: `if (true) { "yes" } else { "no" }`, + out: model.NewStringValue("yes"), + }.run) + t.Run("false", testCase{ + s: `if (false) { "yes" } else { "no" }`, + out: model.NewStringValue("no"), + }.run) + t.Run("nested", testCase{ + s: ` + if (true) { + if (true) { "yes" } + else { "no" } + } else { "no" }`, + out: model.NewStringValue("yes"), + }.run) + t.Run("nested false", testCase{ + s: ` + if (true) { + if (false) { "yes" } + else { "no" } + } else { "no" }`, + out: model.NewStringValue("no"), + }.run) + t.Run("else if", testCase{ + s: ` + if (false) { "yes" } + elseif (true) { "no" } + else { "maybe" }`, + out: model.NewStringValue("no"), + }.run) + t.Run("else if else", testCase{ + s: ` + if (false) { "yes" } + elseif (false) { "no" } + else { "maybe" }`, + out: model.NewStringValue("maybe"), + }.run) + t.Run("if elseif elseif else", testCase{ + s: ` + if (false) { "yes" } + elseif (false) { "no" } + elseif (false) { "maybe" } + else { "nope" }`, + out: model.NewStringValue("nope"), + }.run) +} diff --git a/execution/execute_filter_test.go b/execution/execute_filter_test.go new file mode 100644 index 00000000..fd82a44d --- /dev/null +++ b/execution/execute_filter_test.go @@ -0,0 +1,73 @@ +package execution_test + +import ( + "testing" + + "github.com/tomwright/dasel/v3/model" +) + +func TestFilter(t *testing.T) { + inSlice := func() *model.Value { + s := model.NewSliceValue() + if err := s.Append(model.NewIntValue(1)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := s.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := s.Append(model.NewIntValue(3)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + return s + } + t.Run("all true", testCase{ + inFn: inSlice, + s: "filter(true)", + outFn: func() *model.Value { + s := model.NewSliceValue() + if err := s.Append(model.NewIntValue(1)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := s.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := s.Append(model.NewIntValue(3)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + return s + }, + }.run) + t.Run("all false", testCase{ + inFn: inSlice, + s: "filter(false)", + outFn: func() *model.Value { + s := model.NewSliceValue() + return s + }, + }.run) + t.Run("equal 2", testCase{ + inFn: inSlice, + s: "filter($this == 2)", + outFn: func() *model.Value { + s := model.NewSliceValue() + if err := s.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + return s + }, + }.run) + t.Run("not equal 2", testCase{ + inFn: inSlice, + s: "filter($this != 2)", + outFn: func() *model.Value { + s := model.NewSliceValue() + if err := s.Append(model.NewIntValue(1)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := s.Append(model.NewIntValue(3)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + return s + }, + }.run) +} diff --git a/execution/execute_func_test.go b/execution/execute_func_test.go new file mode 100644 index 00000000..75339d52 --- /dev/null +++ b/execution/execute_func_test.go @@ -0,0 +1,49 @@ +package execution_test + +import ( + "testing" + + "github.com/tomwright/dasel/v3/execution" + "github.com/tomwright/dasel/v3/model" +) + +func TestFunc(t *testing.T) { + returnInputData := execution.NewFunc( + "returnInputData", + func(data *model.Value, args model.Values) (*model.Value, error) { + return data, nil + }, + execution.ValidateArgsExactly(0), + ) + + returnFirstArg := execution.NewFunc( + "returnFirstArg", + func(data *model.Value, args model.Values) (*model.Value, error) { + return args[0], nil + }, + execution.ValidateArgsExactly(1), + ) + + funcs := execution.NewFuncCollection( + returnInputData, + returnFirstArg, + ) + + opts := []execution.ExecuteOptionFn{ + func(options *execution.Options) { + options.Funcs = funcs + }, + } + + t.Run("returnInputData", testCase{ + s: `1.returnInputData()`, + out: model.NewIntValue(1), + opts: opts, + }.run) + + t.Run("returnFirstArg", testCase{ + s: `1.returnFirstArg(2)`, + out: model.NewIntValue(2), + opts: opts, + }.run) +} diff --git a/execution/execute_literal_test.go b/execution/execute_literal_test.go new file mode 100644 index 00000000..677bb7ff --- /dev/null +++ b/execution/execute_literal_test.go @@ -0,0 +1,113 @@ +package execution_test + +import ( + "testing" + + "github.com/tomwright/dasel/v3/model" +) + +func TestLiteral(t *testing.T) { + t.Run("string", testCase{ + s: `"hello"`, + out: model.NewStringValue("hello"), + }.run) + t.Run("int", testCase{ + s: `123`, + out: model.NewIntValue(123), + }.run) + t.Run("float", testCase{ + s: `123.4`, + out: model.NewFloatValue(123.4), + }.run) + t.Run("true", testCase{ + s: `true`, + out: model.NewBoolValue(true), + }.run) + t.Run("false", testCase{ + s: `false`, + out: model.NewBoolValue(false), + }.run) + t.Run("empty array", testCase{ + s: `[]`, + outFn: func() *model.Value { + r := model.NewSliceValue() + return r + }, + }.run) + t.Run("array with one element", testCase{ + s: `[1]`, + outFn: func() *model.Value { + r := model.NewSliceValue() + if err := r.Append(model.NewIntValue(1)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + return r + }, + }.run) + t.Run("array with many elements", testCase{ + s: `[1, 2.2, "foo", true, [1, 2, 3]]`, + outFn: func() *model.Value { + nested := model.NewSliceValue() + if err := nested.Append(model.NewIntValue(1)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := nested.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := nested.Append(model.NewIntValue(3)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + r := model.NewSliceValue() + if err := r.Append(model.NewIntValue(1)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := r.Append(model.NewFloatValue(2.2)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := r.Append(model.NewStringValue("foo")); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := r.Append(model.NewBoolValue(true)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := r.Append(nested); err != nil { + t.Fatalf("unexpected error: %v", err) + } + return r + }, + }.run) + t.Run("array with expressions", testCase{ + s: `[1 + 1, 2f - 2, "foo" + "bar", true || false, [1 + 1, 2 * 2, 3 / 3]]`, + outFn: func() *model.Value { + nested := model.NewSliceValue() + if err := nested.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := nested.Append(model.NewIntValue(4)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := nested.Append(model.NewIntValue(1)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + r := model.NewSliceValue() + if err := r.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := r.Append(model.NewFloatValue(0)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := r.Append(model.NewStringValue("foobar")); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := r.Append(model.NewBoolValue(true)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := r.Append(nested); err != nil { + t.Fatalf("unexpected error: %v", err) + } + return r + }, + }.run) +} diff --git a/execution/execute_map_test.go b/execution/execute_map_test.go new file mode 100644 index 00000000..2271abc8 --- /dev/null +++ b/execution/execute_map_test.go @@ -0,0 +1,44 @@ +package execution_test + +import ( + "testing" + + "github.com/tomwright/dasel/v3/dencoding" + "github.com/tomwright/dasel/v3/internal/ptr" + "github.com/tomwright/dasel/v3/model" +) + +func TestMap(t *testing.T) { + t.Run("property from slice of maps", testCase{ + inFn: func() *model.Value { + return model.NewValue([]any{ + dencoding.NewMap().Set("number", 1), + dencoding.NewMap().Set("number", 2), + dencoding.NewMap().Set("number", 3), + }) + }, + s: `map(number)`, + outFn: func() *model.Value { + return model.NewValue([]any{1, 2, 3}) + }, + }.run) + t.Run("with chain of selectors", testCase{ + inFn: func() *model.Value { + return model.NewValue([]any{ + dencoding.NewMap().Set("foo", 1).Set("bar", 4), + dencoding.NewMap().Set("foo", 2).Set("bar", 5), + dencoding.NewMap().Set("foo", 3).Set("bar", 6), + }) + }, + s: ` + map ( + { + total: add( foo, bar, 1 ) + } + ) + .map ( total )`, + outFn: func() *model.Value { + return model.NewValue([]any{ptr.To(int64(6)), ptr.To(int64(8)), ptr.To(int64(10))}) + }, + }.run) +} diff --git a/execution/execute_object_test.go b/execution/execute_object_test.go new file mode 100644 index 00000000..4189b86e --- /dev/null +++ b/execution/execute_object_test.go @@ -0,0 +1,109 @@ +package execution_test + +import ( + "testing" + + "github.com/tomwright/dasel/v3/dencoding" + "github.com/tomwright/dasel/v3/model" +) + +func TestObject(t *testing.T) { + inputMap := func() *model.Value { + return model.NewValue(dencoding.NewMap(). + Set("title", "Mr"). + Set("age", int64(30)). + Set("name", dencoding.NewMap(). + Set("first", "Tom"). + Set("last", "Wright"))) + } + t.Run("get", testCase{ + in: inputMap(), + s: `{title}`, + outFn: func() *model.Value { + return model.NewValue(dencoding.NewMap().Set("title", "Mr")) + //res := model.NewMapValue() + //_ = res.SetMapKey("title", model.NewStringValue("Mr")) + //return res + }, + }.run) + t.Run("get multiple", testCase{ + in: inputMap(), + s: `{title, age}`, + outFn: func() *model.Value { + return model.NewValue(dencoding.NewMap().Set("title", "Mr").Set("age", int64(30))) + //res := model.NewMapValue() + //_ = res.SetMapKey("title", model.NewStringValue("Mr")) + //_ = res.SetMapKey("age", model.NewIntValue(30)) + //return res + }, + }.run) + t.Run("get with spread", testCase{ + in: inputMap(), + s: `{...}`, + outFn: func() *model.Value { + res := inputMap() + return res + }, + }.run) + t.Run("set", testCase{ + in: inputMap(), + s: `{title:"Mrs"}`, + outFn: func() *model.Value { + res := model.NewMapValue() + _ = res.SetMapKey("title", model.NewStringValue("Mrs")) + return res + }, + }.run) + t.Run("set with spread", testCase{ + in: inputMap(), + s: `{..., title:"Mrs"}`, + outFn: func() *model.Value { + res := inputMap() + _ = res.SetMapKey("title", model.NewStringValue("Mrs")) + return res + }, + }.run) + t.Run("merge with spread", testCase{ + inFn: func() *model.Value { + a := model.NewMapValue() + if err := a.SetMapKey("foo", model.NewStringValue("afoo")); err != nil { + t.Errorf("unexpected error: %v", err) + } + if err := a.SetMapKey("bar", model.NewStringValue("abar")); err != nil { + t.Errorf("unexpected error: %v", err) + } + b := model.NewMapValue() + if err := b.SetMapKey("bar", model.NewStringValue("bbar")); err != nil { + t.Errorf("unexpected error: %v", err) + } + if err := b.SetMapKey("baz", model.NewStringValue("bbaz")); err != nil { + t.Errorf("unexpected error: %v", err) + } + res := model.NewMapValue() + if err := res.SetMapKey("a", a); err != nil { + t.Errorf("unexpected error: %v", err) + } + if err := res.SetMapKey("b", b); err != nil { + t.Errorf("unexpected error: %v", err) + } + return res + }, + s: `{a..., b..., x: 1}`, + outFn: func() *model.Value { + b := model.NewMapValue() + if err := b.SetMapKey("foo", model.NewStringValue("afoo")); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := b.SetMapKey("bar", model.NewStringValue("bbar")); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := b.SetMapKey("baz", model.NewStringValue("bbaz")); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := b.SetMapKey("x", model.NewIntValue(1)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + return b + }, + }.run) +} diff --git a/execution/execute_test.go b/execution/execute_test.go index a776f254..814e8a8c 100644 --- a/execution/execute_test.go +++ b/execution/execute_test.go @@ -6,426 +6,56 @@ import ( "github.com/google/go-cmp/cmp" "github.com/tomwright/dasel/v3/dencoding" "github.com/tomwright/dasel/v3/execution" - "github.com/tomwright/dasel/v3/internal/ptr" "github.com/tomwright/dasel/v3/model" ) -func TestExecuteSelector_HappyPath(t *testing.T) { - type testCase struct { - in *model.Value - inFn func() *model.Value - s string - out *model.Value - outFn func() *model.Value - compareRoot bool - } - - runTest := func(tc testCase) func(t *testing.T) { - return func(t *testing.T) { - in := tc.in - if tc.inFn != nil { - in = tc.inFn() - } - if in == nil { - in = model.NewValue(nil) - } - exp := tc.out - if tc.outFn != nil { - exp = tc.outFn() - } - res, err := execution.ExecuteSelector(tc.s, in, execution.NewOptions()) - if err != nil { - t.Fatal(err) - } - - if tc.compareRoot { - res = in - } - - equal, err := res.EqualTypeValue(exp) - if err != nil { - t.Fatal(err) - } - if !equal { - t.Errorf("unexpected output: %v", cmp.Diff(exp.Interface(), res.Interface())) - } +type testCase struct { + in *model.Value + inFn func() *model.Value + s string + out *model.Value + outFn func() *model.Value + compareRoot bool + opts []execution.ExecuteOptionFn +} - expMeta := exp.Metadata - gotMeta := res.Metadata - if !cmp.Equal(expMeta, gotMeta) { - t.Errorf("unexpected output metadata: %v", cmp.Diff(expMeta, gotMeta)) - } - } +func (tc testCase) run(t *testing.T) { + in := tc.in + if tc.inFn != nil { + in = tc.inFn() + } + if in == nil { + in = model.NewValue(nil) + } + exp := tc.out + if tc.outFn != nil { + exp = tc.outFn() + } + res, err := execution.ExecuteSelector(tc.s, in, execution.NewOptions(tc.opts...)) + if err != nil { + t.Fatal(err) } - t.Run("binary expressions", func(t *testing.T) { - t.Run("math", func(t *testing.T) { - t.Run("literals", func(t *testing.T) { - t.Run("addition", runTest(testCase{ - s: `1 + 2`, - out: model.NewIntValue(3), - })) - t.Run("subtraction", runTest(testCase{ - s: `5 - 2`, - out: model.NewIntValue(3), - })) - t.Run("multiplication", runTest(testCase{ - s: `5 * 2`, - out: model.NewIntValue(10), - })) - t.Run("division", runTest(testCase{ - s: `10 / 2`, - out: model.NewIntValue(5), - })) - t.Run("modulus", runTest(testCase{ - s: `10 % 3`, - out: model.NewIntValue(1), - })) - t.Run("ordering", runTest(testCase{ - s: `45.2 + 5 * 4 - 2 / 2`, // 45.2 + (5 * 4) - (2 / 2) = 45.2 + 20 - 1 = 64.2 - out: model.NewFloatValue(64.2), - })) - t.Run("ordering with groups", runTest(testCase{ - s: `(45.2 + 5) * ((4 - 2) / 2)`, // (45.2 + 5) * ((4 - 2) / 2) = (50.2) * ((2) / 2) = (50.2) * (1) = 50.2 - out: model.NewFloatValue(50.2), - })) - }) - t.Run("variables", func(t *testing.T) { - in := func() *model.Value { - return model.NewValue(dencoding.NewMap(). - Set("one", 1). - Set("two", 2). - Set("three", 3). - Set("four", 4). - Set("five", 5). - Set("six", 6). - Set("seven", 7). - Set("eight", 8). - Set("nine", 9). - Set("ten", 10). - Set("fortyfivepoint2", 45.2)) - } - t.Run("addition", runTest(testCase{ - inFn: in, - s: `one + two`, - out: model.NewIntValue(3), - })) - t.Run("subtraction", runTest(testCase{ - inFn: in, - s: `five - two`, - out: model.NewIntValue(3), - })) - t.Run("multiplication", runTest(testCase{ - inFn: in, - s: `five * two`, - out: model.NewIntValue(10), - })) - t.Run("division", runTest(testCase{ - inFn: in, - s: `ten / two`, - out: model.NewIntValue(5), - })) - t.Run("modulus", runTest(testCase{ - inFn: in, - s: `ten % three`, - out: model.NewIntValue(1), - })) - t.Run("ordering", runTest(testCase{ - inFn: in, - s: `fortyfivepoint2 + five * four - two / two`, // 45.2 + (5 * 4) - (2 / 2) = 45.2 + 20 - 1 = 64.2 - out: model.NewFloatValue(64.2), - })) - t.Run("ordering with groups", runTest(testCase{ - inFn: in, - s: `(fortyfivepoint2 + five) * ((four - two) / two)`, // (45.2 + 5) * ((4 - 2) / 2) = (50.2) * ((2) / 2) = (50.2) * (1) = 50.2 - out: model.NewFloatValue(50.2), - })) - }) - }) - t.Run("comparison", func(t *testing.T) { - t.Run("literals", func(t *testing.T) { - t.Run("equal", runTest(testCase{ - s: `1 == 1`, - out: model.NewBoolValue(true), - })) - t.Run("not equal", runTest(testCase{ - s: `1 != 1`, - out: model.NewBoolValue(false), - })) - t.Run("greater than", runTest(testCase{ - s: `2 > 1`, - out: model.NewBoolValue(true), - })) - t.Run("greater than or equal", runTest(testCase{ - s: `2 >= 2`, - out: model.NewBoolValue(true), - })) - t.Run("less than", runTest(testCase{ - s: `1 < 2`, - out: model.NewBoolValue(true), - })) - t.Run("less than or equal", runTest(testCase{ - s: `2 <= 2`, - out: model.NewBoolValue(true), - })) - t.Run("like", runTest(testCase{ - s: `"hello world" =~ r/ello/`, - out: model.NewBoolValue(true), - })) - t.Run("not like", runTest(testCase{ - s: `"hello world" !~ r/helloworld/`, - out: model.NewBoolValue(true), - })) - }) - - t.Run("variables", func(t *testing.T) { - in := func() *model.Value { - return model.NewValue(dencoding.NewMap(). - Set("one", 1). - Set("two", 2). - Set("nested", dencoding.NewMap(). - Set("three", 3). - Set("four", 4))) - } - t.Run("equal", runTest(testCase{ - inFn: in, - s: `one == one`, - out: model.NewBoolValue(true), - })) - t.Run("not equal", runTest(testCase{ - inFn: in, - s: `one != one`, - out: model.NewBoolValue(false), - })) - t.Run("greater than", runTest(testCase{ - inFn: in, - s: `two > one`, - out: model.NewBoolValue(true), - })) - t.Run("greater than or equal", runTest(testCase{ - inFn: in, - s: `two >= two`, - out: model.NewBoolValue(true), - })) - t.Run("less than", runTest(testCase{ - inFn: in, - s: `one < two`, - out: model.NewBoolValue(true), - })) - t.Run("less than or equal", runTest(testCase{ - inFn: in, - s: `two <= two`, - out: model.NewBoolValue(true), - })) - t.Run("nested with math more than", runTest(testCase{ - inFn: in, - s: `nested.three + nested.four * 0 > one * 1`, - out: model.NewBoolValue(true), - })) - t.Run("nested with grouped math more than", runTest(testCase{ - inFn: in, - s: `(nested.three + nested.four) * 0 > one * 1`, - out: model.NewBoolValue(false), - })) - }) - }) - }) - - t.Run("literal", func(t *testing.T) { - t.Run("string", runTest(testCase{ - s: `"hello"`, - out: model.NewStringValue("hello"), - })) - t.Run("int", runTest(testCase{ - s: `123`, - out: model.NewIntValue(123), - })) - t.Run("float", runTest(testCase{ - s: `123.4`, - out: model.NewFloatValue(123.4), - })) - t.Run("true", runTest(testCase{ - s: `true`, - out: model.NewBoolValue(true), - })) - t.Run("false", runTest(testCase{ - s: `false`, - out: model.NewBoolValue(false), - })) - t.Run("empty array", runTest(testCase{ - s: `[]`, - outFn: func() *model.Value { - r := model.NewSliceValue() - return r - }, - })) - t.Run("array with one element", runTest(testCase{ - s: `[1]`, - outFn: func() *model.Value { - r := model.NewSliceValue() - if err := r.Append(model.NewIntValue(1)); err != nil { - t.Fatalf("unexpected error: %v", err) - } - return r - }, - })) - t.Run("array with many elements", runTest(testCase{ - s: `[1, 2.2, "foo", true, [1, 2, 3]]`, - outFn: func() *model.Value { - nested := model.NewSliceValue() - if err := nested.Append(model.NewIntValue(1)); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if err := nested.Append(model.NewIntValue(2)); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if err := nested.Append(model.NewIntValue(3)); err != nil { - t.Fatalf("unexpected error: %v", err) - } - - r := model.NewSliceValue() - if err := r.Append(model.NewIntValue(1)); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if err := r.Append(model.NewFloatValue(2.2)); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if err := r.Append(model.NewStringValue("foo")); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if err := r.Append(model.NewBoolValue(true)); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if err := r.Append(nested); err != nil { - t.Fatalf("unexpected error: %v", err) - } - return r - }, - })) - t.Run("array with expressions", runTest(testCase{ - s: `[1 + 1, 2f - 2, "foo" + "bar", true || false, [1 + 1, 2 * 2, 3 / 3]]`, - outFn: func() *model.Value { - nested := model.NewSliceValue() - if err := nested.Append(model.NewIntValue(2)); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if err := nested.Append(model.NewIntValue(4)); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if err := nested.Append(model.NewIntValue(1)); err != nil { - t.Fatalf("unexpected error: %v", err) - } + if tc.compareRoot { + res = in + } - r := model.NewSliceValue() - if err := r.Append(model.NewIntValue(2)); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if err := r.Append(model.NewFloatValue(0)); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if err := r.Append(model.NewStringValue("foobar")); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if err := r.Append(model.NewBoolValue(true)); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if err := r.Append(nested); err != nil { - t.Fatalf("unexpected error: %v", err) - } - return r - }, - })) - }) + equal, err := res.EqualTypeValue(exp) + if err != nil { + t.Fatal(err) + } + if !equal { + t.Errorf("unexpected output: %v", cmp.Diff(exp.Interface(), res.Interface())) + } - t.Run("function", func(t *testing.T) { - t.Run("add", func(t *testing.T) { - t.Run("int", runTest(testCase{ - s: `add(1, 2, 3)`, - out: model.NewIntValue(6), - })) - t.Run("float", runTest(testCase{ - s: `add(1f, 2.5, 3.5)`, - out: model.NewFloatValue(7), - })) - t.Run("mixed", runTest(testCase{ - s: `add(1, 2f)`, - out: model.NewFloatValue(3), - })) - t.Run("properties", func(t *testing.T) { - in := func() *model.Value { - return model.NewValue(dencoding.NewMap(). - Set("numbers", dencoding.NewMap(). - Set("one", 1). - Set("two", 2). - Set("three", 3)). - Set("nums", []any{1, 2, 3})) - } - t.Run("nested props", runTest(testCase{ - inFn: in, - s: `numbers.one + add(numbers.two, numbers.three)`, - out: model.NewIntValue(6), - })) - t.Run("add on end of chain", runTest(testCase{ - inFn: in, - s: `numbers.one + numbers.add(two, three)`, - out: model.NewIntValue(6), - })) - t.Run("add with map and spread on slice with $this addition and grouping", runTest(testCase{ - inFn: in, - s: `add(nums.map(($this + 1))...)`, - out: model.NewIntValue(9), - })) - t.Run("add with map and spread on slice with $this addition", runTest(testCase{ - inFn: in, - s: `add(nums.map($this + 1 - 2)...)`, - out: model.NewIntValue(3), - })) - }) - }) - t.Run("merge", func(t *testing.T) { - t.Run("shallow", runTest(testCase{ - inFn: func() *model.Value { - a := model.NewMapValue() - if err := a.SetMapKey("foo", model.NewStringValue("afoo")); err != nil { - t.Errorf("unexpected error: %v", err) - } - if err := a.SetMapKey("bar", model.NewStringValue("abar")); err != nil { - t.Errorf("unexpected error: %v", err) - } - b := model.NewMapValue() - if err := b.SetMapKey("bar", model.NewStringValue("bbar")); err != nil { - t.Errorf("unexpected error: %v", err) - } - if err := b.SetMapKey("baz", model.NewStringValue("bbaz")); err != nil { - t.Errorf("unexpected error: %v", err) - } - res := model.NewMapValue() - if err := res.SetMapKey("a", a); err != nil { - t.Errorf("unexpected error: %v", err) - } - if err := res.SetMapKey("b", b); err != nil { - t.Errorf("unexpected error: %v", err) - } - return res - }, - s: `merge(a, b)`, - outFn: func() *model.Value { - b := model.NewMapValue() - if err := b.SetMapKey("foo", model.NewStringValue("afoo")); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if err := b.SetMapKey("bar", model.NewStringValue("bbar")); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if err := b.SetMapKey("baz", model.NewStringValue("bbaz")); err != nil { - t.Fatalf("unexpected error: %v", err) - } - return b - }, - })) - }) - }) + expMeta := exp.Metadata + gotMeta := res.Metadata + if !cmp.Equal(expMeta, gotMeta) { + t.Errorf("unexpected output metadata: %v", cmp.Diff(expMeta, gotMeta)) + } +} +func TestExecuteSelector_HappyPath(t *testing.T) { t.Run("get", func(t *testing.T) { inputMap := func() *model.Value { return model.NewValue( @@ -437,27 +67,27 @@ func TestExecuteSelector_HappyPath(t *testing.T) { Set("last", "Wright")), ) } - t.Run("property", runTest(testCase{ + t.Run("property", testCase{ in: inputMap(), s: `title`, out: model.NewStringValue("Mr"), - })) - t.Run("nested property", runTest(testCase{ + }.run) + t.Run("nested property", testCase{ in: inputMap(), s: `name.first`, out: model.NewStringValue("Tom"), - })) - t.Run("concat with grouping", runTest(testCase{ + }.run) + t.Run("concat with grouping", testCase{ in: inputMap(), s: `title + " " + (name.first) + " " + (name.last)`, out: model.NewStringValue("Mr Tom Wright"), - })) - t.Run("concat", runTest(testCase{ + }.run) + t.Run("concat", testCase{ in: inputMap(), s: `title + " " + name.first + " " + name.last`, out: model.NewStringValue("Mr Tom Wright"), - })) - t.Run("add evaluated fields", runTest(testCase{ + }.run) + t.Run("add evaluated fields", testCase{ in: inputMap(), s: `{..., "over30": age > 30}`, outFn: func() *model.Value { @@ -471,7 +101,7 @@ func TestExecuteSelector_HappyPath(t *testing.T) { Set("over30", true), ) }, - })) + }.run) }) t.Run("set", func(t *testing.T) { @@ -489,7 +119,7 @@ func TestExecuteSelector_HappyPath(t *testing.T) { return model.NewValue([]any{1, 2, 3}) } - t.Run("set property", runTest(testCase{ + t.Run("set property", testCase{ in: inputMap(), s: `title = "Mrs"`, outFn: func() *model.Value { @@ -500,9 +130,9 @@ func TestExecuteSelector_HappyPath(t *testing.T) { return res }, compareRoot: true, - })) + }.run) - t.Run("set index", runTest(testCase{ + t.Run("set index", testCase{ in: inputSlice(), s: `$this[1] = 4`, outFn: func() *model.Value { @@ -513,412 +143,6 @@ func TestExecuteSelector_HappyPath(t *testing.T) { return res }, compareRoot: true, - })) - }) - - t.Run("object", func(t *testing.T) { - inputMap := func() *model.Value { - return model.NewValue(dencoding.NewMap(). - Set("title", "Mr"). - Set("age", int64(30)). - Set("name", dencoding.NewMap(). - Set("first", "Tom"). - Set("last", "Wright"))) - } - t.Run("get", runTest(testCase{ - in: inputMap(), - s: `{title}`, - outFn: func() *model.Value { - return model.NewValue(dencoding.NewMap().Set("title", "Mr")) - //res := model.NewMapValue() - //_ = res.SetMapKey("title", model.NewStringValue("Mr")) - //return res - }, - })) - t.Run("get multiple", runTest(testCase{ - in: inputMap(), - s: `{title, age}`, - outFn: func() *model.Value { - return model.NewValue(dencoding.NewMap().Set("title", "Mr").Set("age", int64(30))) - //res := model.NewMapValue() - //_ = res.SetMapKey("title", model.NewStringValue("Mr")) - //_ = res.SetMapKey("age", model.NewIntValue(30)) - //return res - }, - })) - t.Run("get with spread", runTest(testCase{ - in: inputMap(), - s: `{...}`, - outFn: func() *model.Value { - res := inputMap() - return res - }, - })) - t.Run("set", runTest(testCase{ - in: inputMap(), - s: `{title:"Mrs"}`, - outFn: func() *model.Value { - res := model.NewMapValue() - _ = res.SetMapKey("title", model.NewStringValue("Mrs")) - return res - }, - })) - t.Run("set with spread", runTest(testCase{ - in: inputMap(), - s: `{..., title:"Mrs"}`, - outFn: func() *model.Value { - res := inputMap() - _ = res.SetMapKey("title", model.NewStringValue("Mrs")) - return res - }, - })) - t.Run("merge with spread", runTest(testCase{ - inFn: func() *model.Value { - a := model.NewMapValue() - if err := a.SetMapKey("foo", model.NewStringValue("afoo")); err != nil { - t.Errorf("unexpected error: %v", err) - } - if err := a.SetMapKey("bar", model.NewStringValue("abar")); err != nil { - t.Errorf("unexpected error: %v", err) - } - b := model.NewMapValue() - if err := b.SetMapKey("bar", model.NewStringValue("bbar")); err != nil { - t.Errorf("unexpected error: %v", err) - } - if err := b.SetMapKey("baz", model.NewStringValue("bbaz")); err != nil { - t.Errorf("unexpected error: %v", err) - } - res := model.NewMapValue() - if err := res.SetMapKey("a", a); err != nil { - t.Errorf("unexpected error: %v", err) - } - if err := res.SetMapKey("b", b); err != nil { - t.Errorf("unexpected error: %v", err) - } - return res - }, - s: `{a..., b..., x: 1}`, - outFn: func() *model.Value { - b := model.NewMapValue() - if err := b.SetMapKey("foo", model.NewStringValue("afoo")); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if err := b.SetMapKey("bar", model.NewStringValue("bbar")); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if err := b.SetMapKey("baz", model.NewStringValue("bbaz")); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if err := b.SetMapKey("x", model.NewIntValue(1)); err != nil { - t.Fatalf("unexpected error: %v", err) - } - return b - }, - })) - }) - - t.Run("map", func(t *testing.T) { - t.Run("property from slice of maps", runTest(testCase{ - inFn: func() *model.Value { - return model.NewValue([]any{ - dencoding.NewMap().Set("number", 1), - dencoding.NewMap().Set("number", 2), - dencoding.NewMap().Set("number", 3), - }) - }, - s: `map(number)`, - outFn: func() *model.Value { - return model.NewValue([]any{1, 2, 3}) - }, - })) - t.Run("with chain of selectors", runTest(testCase{ - inFn: func() *model.Value { - return model.NewValue([]any{ - dencoding.NewMap().Set("foo", 1).Set("bar", 4), - dencoding.NewMap().Set("foo", 2).Set("bar", 5), - dencoding.NewMap().Set("foo", 3).Set("bar", 6), - }) - }, - s: ` - map ( - { - total: add( foo, bar, 1 ) - } - ) - .map ( total )`, - outFn: func() *model.Value { - return model.NewValue([]any{ptr.To(int64(6)), ptr.To(int64(8)), ptr.To(int64(10))}) - }, - })) - }) - - t.Run("filter", func(t *testing.T) { - inSlice := func() *model.Value { - s := model.NewSliceValue() - if err := s.Append(model.NewIntValue(1)); err != nil { - t.Fatalf("unexpected error: %s", err) - } - if err := s.Append(model.NewIntValue(2)); err != nil { - t.Fatalf("unexpected error: %s", err) - } - if err := s.Append(model.NewIntValue(3)); err != nil { - t.Fatalf("unexpected error: %s", err) - } - return s - } - t.Run("all true", runTest(testCase{ - inFn: inSlice, - s: "filter(true)", - outFn: func() *model.Value { - s := model.NewSliceValue() - if err := s.Append(model.NewIntValue(1)); err != nil { - t.Fatalf("unexpected error: %s", err) - } - if err := s.Append(model.NewIntValue(2)); err != nil { - t.Fatalf("unexpected error: %s", err) - } - if err := s.Append(model.NewIntValue(3)); err != nil { - t.Fatalf("unexpected error: %s", err) - } - return s - }, - })) - t.Run("all false", runTest(testCase{ - inFn: inSlice, - s: "filter(false)", - outFn: func() *model.Value { - s := model.NewSliceValue() - return s - }, - })) - t.Run("equal 2", runTest(testCase{ - inFn: inSlice, - s: "filter($this == 2)", - outFn: func() *model.Value { - s := model.NewSliceValue() - if err := s.Append(model.NewIntValue(2)); err != nil { - t.Fatalf("unexpected error: %s", err) - } - return s - }, - })) - t.Run("not equal 2", runTest(testCase{ - inFn: inSlice, - s: "filter($this != 2)", - outFn: func() *model.Value { - s := model.NewSliceValue() - if err := s.Append(model.NewIntValue(1)); err != nil { - t.Fatalf("unexpected error: %s", err) - } - if err := s.Append(model.NewIntValue(3)); err != nil { - t.Fatalf("unexpected error: %s", err) - } - return s - }, - })) - }) - - t.Run("array", func(t *testing.T) { - inSlice := func() *model.Value { - s := model.NewSliceValue() - if err := s.Append(model.NewIntValue(1)); err != nil { - t.Fatalf("unexpected error: %s", err) - } - if err := s.Append(model.NewIntValue(2)); err != nil { - t.Fatalf("unexpected error: %s", err) - } - if err := s.Append(model.NewIntValue(3)); err != nil { - t.Fatalf("unexpected error: %s", err) - } - return s - } - inMap := func() *model.Value { - m := model.NewMapValue() - if err := m.SetMapKey("numbers", inSlice()); err != nil { - t.Fatalf("unexpected error: %s", err) - } - return m - } - - runArrayTests := func(in func() *model.Value, prefix string) func(t *testing.T) { - return func(t *testing.T) { - t.Run("1:2", runTest(testCase{ - s: prefix + `[1:2]`, - inFn: in, - outFn: func() *model.Value { - res := model.NewSliceValue() - if err := res.Append(model.NewIntValue(2)); err != nil { - t.Fatalf("unexpected error: %s", err) - } - if err := res.Append(model.NewIntValue(3)); err != nil { - t.Fatalf("unexpected error: %s", err) - } - return res - }, - })) - t.Run("1:0", runTest(testCase{ - s: prefix + `[1:0]`, - inFn: in, - outFn: func() *model.Value { - res := model.NewSliceValue() - if err := res.Append(model.NewIntValue(2)); err != nil { - t.Fatalf("unexpected error: %s", err) - } - if err := res.Append(model.NewIntValue(1)); err != nil { - t.Fatalf("unexpected error: %s", err) - } - return res - }, - })) - t.Run("1:", runTest(testCase{ - s: prefix + `[1:]`, - inFn: in, - outFn: func() *model.Value { - res := model.NewSliceValue() - if err := res.Append(model.NewIntValue(2)); err != nil { - t.Fatalf("unexpected error: %s", err) - } - if err := res.Append(model.NewIntValue(3)); err != nil { - t.Fatalf("unexpected error: %s", err) - } - return res - }, - })) - t.Run(":1", runTest(testCase{ - s: prefix + `[:1]`, - inFn: in, - outFn: func() *model.Value { - res := model.NewSliceValue() - if err := res.Append(model.NewIntValue(1)); err != nil { - t.Fatalf("unexpected error: %s", err) - } - if err := res.Append(model.NewIntValue(2)); err != nil { - t.Fatalf("unexpected error: %s", err) - } - return res - }, - })) - t.Run("reverse", runTest(testCase{ - s: prefix + `[len($this)-1:0]`, - inFn: in, - outFn: func() *model.Value { - res := model.NewSliceValue() - if err := res.Append(model.NewIntValue(3)); err != nil { - t.Fatalf("unexpected error: %s", err) - } - if err := res.Append(model.NewIntValue(2)); err != nil { - t.Fatalf("unexpected error: %s", err) - } - if err := res.Append(model.NewIntValue(1)); err != nil { - t.Fatalf("unexpected error: %s", err) - } - return res - }, - })) - } - } - - t.Run("direct to slice", runArrayTests(inSlice, "$this")) - t.Run("property to slice", runArrayTests(inMap, "numbers")) - }) - - t.Run("conditional", func(t *testing.T) { - t.Run("true", runTest(testCase{ - s: `if (true) { "yes" } else { "no" }`, - out: model.NewStringValue("yes"), - })) - t.Run("false", runTest(testCase{ - s: `if (false) { "yes" } else { "no" }`, - out: model.NewStringValue("no"), - })) - t.Run("nested", runTest(testCase{ - s: ` - if (true) { - if (true) { "yes" } - else { "no" } - } else { "no" }`, - out: model.NewStringValue("yes"), - })) - t.Run("nested false", runTest(testCase{ - s: ` - if (true) { - if (false) { "yes" } - else { "no" } - } else { "no" }`, - out: model.NewStringValue("no"), - })) - t.Run("else if", runTest(testCase{ - s: ` - if (false) { "yes" } - elseif (true) { "no" } - else { "maybe" }`, - out: model.NewStringValue("no"), - })) - t.Run("else if else", runTest(testCase{ - s: ` - if (false) { "yes" } - elseif (false) { "no" } - else { "maybe" }`, - out: model.NewStringValue("maybe"), - })) - t.Run("if elseif elseif else", runTest(testCase{ - s: ` - if (false) { "yes" } - elseif (false) { "no" } - elseif (false) { "maybe" } - else { "nope" }`, - 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 - }, - })) - t.Run("spread into many branches", runTest(testCase{ - s: "[1,2,3].branch(...)", - 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) - } - return r - }, - })) + }.run) }) } diff --git a/execution/func.go b/execution/func.go index f687606a..b1f69531 100644 --- a/execution/func.go +++ b/execution/func.go @@ -6,6 +6,17 @@ import ( "github.com/tomwright/dasel/v3/model" ) +var ( + // DefaultFuncCollection is the default collection of functions that can be executed. + DefaultFuncCollection = NewFuncCollection( + FuncLen, + FuncAdd, + FuncToString, + FuncMerge, + FuncReverse, + ) +) + // ArgsValidator is a function that validates the arguments passed to a function. type ArgsValidator func(name string, args model.Values) error @@ -122,179 +133,3 @@ func (fc FuncCollection) Copy() FuncCollection { } return c } - -var ( - // DefaultFuncCollection is the default collection of functions that can be executed. - DefaultFuncCollection = NewFuncCollection( - FuncLen, - FuncAdd, - FuncToString, - FuncMerge, - FuncReverse, - ) - - // FuncLen is a function that returns the length of the given value. - FuncLen = NewFunc( - "len", - func(data *model.Value, args model.Values) (*model.Value, error) { - arg := args[0] - - l, err := arg.Len() - if err != nil { - return nil, err - } - - return model.NewIntValue(int64(l)), nil - }, - ValidateArgsExactly(1), - ) - - // FuncAdd is a function that adds the given values together. - FuncAdd = NewFunc( - "add", - func(data *model.Value, args model.Values) (*model.Value, error) { - var foundInts, foundFloats int - var intRes int64 - var floatRes float64 - for _, arg := range args { - if arg.IsFloat() { - foundFloats++ - v, err := arg.FloatValue() - if err != nil { - return nil, fmt.Errorf("error getting float value: %w", err) - } - floatRes += v - continue - } - if arg.IsInt() { - foundInts++ - v, err := arg.IntValue() - if err != nil { - return nil, fmt.Errorf("error getting int value: %w", err) - } - intRes += v - continue - } - return nil, fmt.Errorf("expected int or float, got %s", arg.Type()) - } - if foundFloats > 0 { - return model.NewFloatValue(floatRes + float64(intRes)), nil - } - return model.NewIntValue(intRes), nil - }, - ValidateArgsMin(1), - ) - - // FuncToString is a function that converts the given value to a string. - FuncToString = NewFunc( - "toString", - func(data *model.Value, args model.Values) (*model.Value, error) { - switch args[0].Type() { - case model.TypeString: - return args[0], nil - case model.TypeInt: - i, err := args[0].IntValue() - if err != nil { - return nil, err - } - return model.NewStringValue(fmt.Sprintf("%d", i)), nil - case model.TypeFloat: - i, err := args[0].FloatValue() - if err != nil { - return nil, err - } - return model.NewStringValue(fmt.Sprintf("%f", i)), nil - case model.TypeBool: - i, err := args[0].BoolValue() - if err != nil { - return nil, err - } - return model.NewStringValue(fmt.Sprintf("%v", i)), nil - default: - return nil, fmt.Errorf("cannot convert %s to string", args[0].Type()) - } - }, - ValidateArgsExactly(1), - ) - - // FuncMerge is a function that merges two or more items together. - FuncMerge = NewFunc( - "merge", - func(data *model.Value, args model.Values) (*model.Value, error) { - if len(args) == 1 { - return args[0], nil - } - - expectedType := args[0].Type() - - switch expectedType { - case model.TypeMap: - break - default: - return nil, fmt.Errorf("merge exects a map, found %s", expectedType) - } - - // Validate types match - for _, a := range args { - if a.Type() != expectedType { - return nil, fmt.Errorf("merge expects all arguments to be of the same type. expected %s, got %s", expectedType.String(), a.Type().String()) - } - } - - base := model.NewMapValue() - - for i := 0; i < len(args); i++ { - next := args[i] - - nextKVs, err := next.MapKeyValues() - if err != nil { - return nil, fmt.Errorf("merge failed to extract key values for arg %d: %w", i, err) - } - - for _, kv := range nextKVs { - if err := base.SetMapKey(kv.Key, kv.Value); err != nil { - return nil, fmt.Errorf("merge failed to set map key %s: %w", kv.Key, err) - } - } - } - - return base, nil - }, - ValidateArgsMin(1), - ) - - // FuncReverse is a function that reverses the input. - FuncReverse = NewFunc( - "reverse", - func(data *model.Value, args model.Values) (*model.Value, error) { - if len(args) == 1 { - return args[0], nil - } - - arg := args[0] - - switch arg.Type() { - case model.TypeString: - v, err := arg.StringValue() - if err != nil { - return nil, err - } - vBytes := []byte(v) - res := string(vBytes[len(vBytes)-1 : 0]) - return model.NewStringValue(res), nil - case model.TypeSlice: - l, err := arg.Len() - if err != nil { - return nil, err - } - if l <= 1 { - return arg, nil - } - return arg.SliceIndexRange(l-1, 0) - default: - return nil, fmt.Errorf("reverse expects a slice or string, got %s", arg.Type()) - } - }, - ValidateArgsExactly(1), - ) -) diff --git a/execution/func_add.go b/execution/func_add.go new file mode 100644 index 00000000..fdca815e --- /dev/null +++ b/execution/func_add.go @@ -0,0 +1,43 @@ +package execution + +import ( + "fmt" + + "github.com/tomwright/dasel/v3/model" +) + +// FuncAdd is a function that adds the given values together. +var FuncAdd = NewFunc( + "add", + func(data *model.Value, args model.Values) (*model.Value, error) { + var foundInts, foundFloats int + var intRes int64 + var floatRes float64 + for _, arg := range args { + if arg.IsFloat() { + foundFloats++ + v, err := arg.FloatValue() + if err != nil { + return nil, fmt.Errorf("error getting float value: %w", err) + } + floatRes += v + continue + } + if arg.IsInt() { + foundInts++ + v, err := arg.IntValue() + if err != nil { + return nil, fmt.Errorf("error getting int value: %w", err) + } + intRes += v + continue + } + return nil, fmt.Errorf("expected int or float, got %s", arg.Type()) + } + if foundFloats > 0 { + return model.NewFloatValue(floatRes + float64(intRes)), nil + } + return model.NewIntValue(intRes), nil + }, + ValidateArgsMin(1), +) diff --git a/execution/func_add_test.go b/execution/func_add_test.go new file mode 100644 index 00000000..acb3613a --- /dev/null +++ b/execution/func_add_test.go @@ -0,0 +1,53 @@ +package execution_test + +import ( + "testing" + + "github.com/tomwright/dasel/v3/dencoding" + "github.com/tomwright/dasel/v3/model" +) + +func TestFuncAdd(t *testing.T) { + t.Run("int", testCase{ + s: `add(1, 2, 3)`, + out: model.NewIntValue(6), + }.run) + t.Run("float", testCase{ + s: `add(1f, 2.5, 3.5)`, + out: model.NewFloatValue(7), + }.run) + t.Run("mixed", testCase{ + s: `add(1, 2f)`, + out: model.NewFloatValue(3), + }.run) + t.Run("properties", func(t *testing.T) { + in := func() *model.Value { + return model.NewValue(dencoding.NewMap(). + Set("numbers", dencoding.NewMap(). + Set("one", 1). + Set("two", 2). + Set("three", 3)). + Set("nums", []any{1, 2, 3})) + } + t.Run("nested props", testCase{ + inFn: in, + s: `numbers.one + add(numbers.two, numbers.three)`, + out: model.NewIntValue(6), + }.run) + t.Run("add on end of chain", testCase{ + inFn: in, + s: `numbers.one + numbers.add(two, three)`, + out: model.NewIntValue(6), + }.run) + t.Run("add with map and spread on slice with $this addition and grouping", testCase{ + inFn: in, + s: `add(nums.map(($this + 1))...)`, + out: model.NewIntValue(9), + }.run) + t.Run("add with map and spread on slice with $this addition", testCase{ + inFn: in, + s: `add(nums.map($this + 1 - 2)...)`, + out: model.NewIntValue(3), + }.run) + }) +} diff --git a/execution/func_len.go b/execution/func_len.go new file mode 100644 index 00000000..e11b14f6 --- /dev/null +++ b/execution/func_len.go @@ -0,0 +1,19 @@ +package execution + +import "github.com/tomwright/dasel/v3/model" + +// FuncLen is a function that returns the length of the given value. +var FuncLen = NewFunc( + "len", + func(data *model.Value, args model.Values) (*model.Value, error) { + arg := args[0] + + l, err := arg.Len() + if err != nil { + return nil, err + } + + return model.NewIntValue(int64(l)), nil + }, + ValidateArgsExactly(1), +) diff --git a/execution/func_merge.go b/execution/func_merge.go new file mode 100644 index 00000000..b837d214 --- /dev/null +++ b/execution/func_merge.go @@ -0,0 +1,53 @@ +package execution + +import ( + "fmt" + + "github.com/tomwright/dasel/v3/model" +) + +// FuncMerge is a function that merges two or more items together. +var FuncMerge = NewFunc( + "merge", + func(data *model.Value, args model.Values) (*model.Value, error) { + if len(args) == 1 { + return args[0], nil + } + + expectedType := args[0].Type() + + switch expectedType { + case model.TypeMap: + break + default: + return nil, fmt.Errorf("merge exects a map, found %s", expectedType) + } + + // Validate types match + for _, a := range args { + if a.Type() != expectedType { + return nil, fmt.Errorf("merge expects all arguments to be of the same type. expected %s, got %s", expectedType.String(), a.Type().String()) + } + } + + base := model.NewMapValue() + + for i := 0; i < len(args); i++ { + next := args[i] + + nextKVs, err := next.MapKeyValues() + if err != nil { + return nil, fmt.Errorf("merge failed to extract key values for arg %d: %w", i, err) + } + + for _, kv := range nextKVs { + if err := base.SetMapKey(kv.Key, kv.Value); err != nil { + return nil, fmt.Errorf("merge failed to set map key %s: %w", kv.Key, err) + } + } + } + + return base, nil + }, + ValidateArgsMin(1), +) diff --git a/execution/func_merge_test.go b/execution/func_merge_test.go new file mode 100644 index 00000000..63262f2d --- /dev/null +++ b/execution/func_merge_test.go @@ -0,0 +1,50 @@ +package execution_test + +import ( + "testing" + + "github.com/tomwright/dasel/v3/model" +) + +func TestFuncMerge(t *testing.T) { + t.Run("shallow", testCase{ + inFn: func() *model.Value { + a := model.NewMapValue() + if err := a.SetMapKey("foo", model.NewStringValue("afoo")); err != nil { + t.Errorf("unexpected error: %v", err) + } + if err := a.SetMapKey("bar", model.NewStringValue("abar")); err != nil { + t.Errorf("unexpected error: %v", err) + } + b := model.NewMapValue() + if err := b.SetMapKey("bar", model.NewStringValue("bbar")); err != nil { + t.Errorf("unexpected error: %v", err) + } + if err := b.SetMapKey("baz", model.NewStringValue("bbaz")); err != nil { + t.Errorf("unexpected error: %v", err) + } + res := model.NewMapValue() + if err := res.SetMapKey("a", a); err != nil { + t.Errorf("unexpected error: %v", err) + } + if err := res.SetMapKey("b", b); err != nil { + t.Errorf("unexpected error: %v", err) + } + return res + }, + s: `merge(a, b)`, + outFn: func() *model.Value { + b := model.NewMapValue() + if err := b.SetMapKey("foo", model.NewStringValue("afoo")); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := b.SetMapKey("bar", model.NewStringValue("bbar")); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := b.SetMapKey("baz", model.NewStringValue("bbaz")); err != nil { + t.Fatalf("unexpected error: %v", err) + } + return b + }, + }.run) +} diff --git a/execution/func_reverse.go b/execution/func_reverse.go new file mode 100644 index 00000000..17c1184c --- /dev/null +++ b/execution/func_reverse.go @@ -0,0 +1,25 @@ +package execution + +import ( + "fmt" + + "github.com/tomwright/dasel/v3/model" +) + +// FuncReverse is a function that reverses the input. +var FuncReverse = NewFunc( + "reverse", + func(data *model.Value, args model.Values) (*model.Value, error) { + arg := args[0] + + switch arg.Type() { + case model.TypeString: + return arg.StringIndexRange(-1, 0) + case model.TypeSlice: + return arg.SliceIndexRange(-1, 0) + default: + return nil, fmt.Errorf("reverse expects a slice or string, got %s", arg.Type()) + } + }, + ValidateArgsExactly(1), +) diff --git a/execution/func_reverse_test.go b/execution/func_reverse_test.go new file mode 100644 index 00000000..e907b88c --- /dev/null +++ b/execution/func_reverse_test.go @@ -0,0 +1,31 @@ +package execution_test + +import ( + "testing" + + "github.com/tomwright/dasel/v3/model" +) + +func TestFuncReverse(t *testing.T) { + t.Run("array", testCase{ + s: `reverse([1, 2, 3])`, + outFn: func() *model.Value { + res := model.NewSliceValue() + if err := res.Append(model.NewIntValue(3)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := res.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := res.Append(model.NewIntValue(1)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + return res + }, + }.run) + + t.Run("string", testCase{ + s: `reverse("hello")`, + out: model.NewStringValue("olleh"), + }.run) +} diff --git a/execution/func_to_string.go b/execution/func_to_string.go new file mode 100644 index 00000000..ade65b83 --- /dev/null +++ b/execution/func_to_string.go @@ -0,0 +1,39 @@ +package execution + +import ( + "fmt" + + "github.com/tomwright/dasel/v3/model" +) + +// FuncToString is a function that converts the given value to a string. +var FuncToString = NewFunc( + "toString", + func(data *model.Value, args model.Values) (*model.Value, error) { + switch args[0].Type() { + case model.TypeString: + return args[0], nil + case model.TypeInt: + i, err := args[0].IntValue() + if err != nil { + return nil, err + } + return model.NewStringValue(fmt.Sprintf("%d", i)), nil + case model.TypeFloat: + i, err := args[0].FloatValue() + if err != nil { + return nil, err + } + return model.NewStringValue(fmt.Sprintf("%f", i)), nil + case model.TypeBool: + i, err := args[0].BoolValue() + if err != nil { + return nil, err + } + return model.NewStringValue(fmt.Sprintf("%v", i)), nil + default: + return nil, fmt.Errorf("cannot convert %s to string", args[0].Type()) + } + }, + ValidateArgsExactly(1), +) diff --git a/model/value_literal.go b/model/value_literal.go index a26dd3a1..51ac280a 100644 --- a/model/value_literal.go +++ b/model/value_literal.go @@ -10,10 +10,12 @@ func newPtr() reflect.Value { return reflect.New(reflect.TypeFor[any]()) } +// NewNullValue creates a new Value with a nil value. func NewNullValue() *Value { return NewValue(newPtr()) } +// IsNull returns true if the value is null. func (v *Value) IsNull() bool { return v.isNull() } @@ -22,12 +24,14 @@ func (v *Value) isNull() bool { return v.Value.IsNil() } +// NewStringValue creates a new Value with a string value. func NewStringValue(x string) *Value { res := newPtr() res.Elem().Set(reflect.ValueOf(x)) return NewValue(res) } +// IsString returns true if the value is a string. func (v *Value) IsString() bool { return v.UnpackKinds(reflect.Ptr, reflect.Interface).isString() } @@ -36,6 +40,7 @@ func (v *Value) isString() bool { return v.Value.Kind() == reflect.String } +// StringValue returns the string value of the Value. func (v *Value) StringValue() (string, error) { unpacked := v.UnpackKinds(reflect.Ptr, reflect.Interface) if !unpacked.isString() { @@ -44,6 +49,7 @@ func (v *Value) StringValue() (string, error) { return unpacked.Value.String(), nil } +// StringLen returns the length of the string. func (v *Value) StringLen() (int, error) { val, err := v.StringValue() if err != nil { @@ -52,12 +58,49 @@ func (v *Value) StringLen() (int, error) { return len(val), nil } +// StringIndexRange returns a new string containing the values between the start and end indexes. +// Comparable to go's string[start:end]. +func (v *Value) StringIndexRange(start, end int) (*Value, error) { + strVal, err := v.StringValue() + if err != nil { + return nil, err + } + + inBytes := []rune(strVal) + l := len(inBytes) + + if start < 0 { + start = l + start + } + if end < 0 { + end = l + end + } + + resBytes := make([]rune, 0) + + if start > end { + for i := start; i >= end; i-- { + resBytes = append(resBytes, inBytes[i]) + } + } else { + for i := start; i <= end; i++ { + resBytes = append(resBytes, inBytes[i]) + } + } + + res := string(resBytes) + + return NewStringValue(res), nil +} + +// NewIntValue creates a new Value with an int value. func NewIntValue(x int64) *Value { res := newPtr() res.Elem().Set(reflect.ValueOf(x)) return NewValue(res) } +// IsInt returns true if the value is an int. func (v *Value) IsInt() bool { return v.UnpackKinds(reflect.Ptr, reflect.Interface).isInt() } @@ -66,6 +109,7 @@ func (v *Value) isInt() bool { return slices.Contains([]reflect.Kind{reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64}, v.Value.Kind()) } +// IntValue returns the int value of the Value. func (v *Value) IntValue() (int64, error) { unpacked := v.UnpackKinds(reflect.Ptr, reflect.Interface) if !unpacked.isInt() { @@ -74,12 +118,14 @@ func (v *Value) IntValue() (int64, error) { return unpacked.Value.Int(), nil } +// NewFloatValue creates a new Value with a float value. func NewFloatValue(x float64) *Value { res := newPtr() res.Elem().Set(reflect.ValueOf(x)) return NewValue(res) } +// IsFloat returns true if the value is a float. func (v *Value) IsFloat() bool { return v.UnpackKinds(reflect.Ptr, reflect.Interface).isFloat() } @@ -88,6 +134,7 @@ func (v *Value) isFloat() bool { return slices.Contains([]reflect.Kind{reflect.Float32, reflect.Float64}, v.Value.Kind()) } +// FloatValue returns the float value of the Value. func (v *Value) FloatValue() (float64, error) { unpacked := v.UnpackKinds(reflect.Ptr, reflect.Interface) if !unpacked.IsFloat() { @@ -96,12 +143,14 @@ func (v *Value) FloatValue() (float64, error) { return unpacked.Value.Float(), nil } +// NewBoolValue creates a new Value with a bool value. func NewBoolValue(x bool) *Value { res := newPtr() res.Elem().Set(reflect.ValueOf(x)) return NewValue(res) } +// IsBool returns true if the value is a bool. func (v *Value) IsBool() bool { return v.UnpackKinds(reflect.Ptr, reflect.Interface).isBool() } @@ -110,6 +159,7 @@ func (v *Value) isBool() bool { return v.Value.Kind() == reflect.Bool } +// BoolValue returns the bool value of the Value. func (v *Value) BoolValue() (bool, error) { unpacked := v.UnpackKinds(reflect.Ptr, reflect.Interface) if !unpacked.IsBool() { diff --git a/model/value_slice.go b/model/value_slice.go index 62ea893c..e2d10c5a 100644 --- a/model/value_slice.go +++ b/model/value_slice.go @@ -92,22 +92,17 @@ func (v *Value) RangeSlice(f func(int, *Value) error) error { // SliceIndexRange returns a new slice containing the values between the start and end indexes. // Comparable to go's slice[start:end]. -// If start is -1, it will be treated as 0. e.g. slice[:end] becomes slice[-1:end]. -// If end is -1, it will be treated as the length of the slice. e.g. slice[start:] becomes slice[start:-1]. func (v *Value) SliceIndexRange(start, end int) (*Value, error) { - var err error - if start == -1 { - start = 0 + l, err := v.SliceLen() + if err != nil { + return nil, fmt.Errorf("error getting slice length: %w", err) } - if end == -1 { - end, err = v.SliceLen() - if err != nil { - return nil, fmt.Errorf("error getting slice length: %w", err) - } - end = end - 1 - if end < 0 { - end = 0 - } + + if start < 0 { + start = l + start + } + if end < 0 { + end = l + end } res := NewSliceValue() diff --git a/model/value_slice_test.go b/model/value_slice_test.go index ef2a89d4..91b9b9a1 100644 --- a/model/value_slice_test.go +++ b/model/value_slice_test.go @@ -128,9 +128,9 @@ func TestSlice(t *testing.T) { // } //}) t.Run("SliceIndexRange", func(t *testing.T) { - t.Run("end 0", func(t *testing.T) { + t.Run("last element", func(t *testing.T) { v := v() - s, err := v.SliceIndexRange(-1, 0) + s, err := v.SliceIndexRange(-1, -1) if err != nil { t.Errorf("unexpected error: %s", err) return @@ -154,13 +154,13 @@ func TestSlice(t *testing.T) { t.Errorf("unexpected error: %s", err) return } - if got != "foo" { - t.Errorf("expected foo, got %s", got) + if got != "bar" { + t.Errorf("expected bar, got %s", got) } }) - t.Run("start 1", func(t *testing.T) { + t.Run("first element", func(t *testing.T) { v := v() - s, err := v.SliceIndexRange(1, -1) + s, err := v.SliceIndexRange(0, 0) if err != nil { t.Errorf("unexpected error: %s", err) return @@ -184,7 +184,7 @@ func TestSlice(t *testing.T) { t.Errorf("unexpected error: %s", err) return } - if got != "bar" { + if got != "foo" { t.Errorf("expected foo, got %s", got) } }) From cfa37ca1253ad4c03e097bbbb5c39b29f723f10f Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Mon, 21 Oct 2024 00:20:36 +0100 Subject: [PATCH 34/56] Implement sortBy func --- execution/execute.go | 2 + execution/execute_sort_by.go | 154 ++++++++++++++++++++++++ execution/execute_sort_by_test.go | 181 +++++++++++++++++++++++++++++ model/value_comparison.go | 41 +++++++ selector/ast/expression_complex.go | 7 ++ selector/lexer/token.go | 3 + selector/lexer/tokenize.go | 9 ++ selector/lexer/tokenize_test.go | 12 ++ selector/parser/parse_array.go | 1 + selector/parser/parse_branch.go | 33 ++++++ selector/parser/parse_filter.go | 28 +++++ selector/parser/parse_func.go | 1 + selector/parser/parse_group.go | 1 + selector/parser/parse_if.go | 1 + selector/parser/parse_map.go | 48 -------- selector/parser/parse_sort_by.go | 60 ++++++++++ selector/parser/parser.go | 12 +- 17 files changed, 543 insertions(+), 51 deletions(-) create mode 100644 execution/execute_sort_by.go create mode 100644 execution/execute_sort_by_test.go create mode 100644 selector/parser/parse_branch.go create mode 100644 selector/parser/parse_filter.go create mode 100644 selector/parser/parse_sort_by.go diff --git a/execution/execute.go b/execution/execute.go index 7fa414b7..fcb14564 100644 --- a/execution/execute.go +++ b/execution/execute.go @@ -107,6 +107,8 @@ func exprExecutor(opts *Options, expr ast.Expr) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { return data, nil }, nil + case ast.SortByExpr: + return sortByExprExecutor(opts, e) default: return nil, fmt.Errorf("unhandled expression type: %T", e) } diff --git a/execution/execute_sort_by.go b/execution/execute_sort_by.go new file mode 100644 index 00000000..9424d067 --- /dev/null +++ b/execution/execute_sort_by.go @@ -0,0 +1,154 @@ +package execution + +import ( + "fmt" + "slices" + + "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/selector/ast" +) + +func sortByExprExecutor(opts *Options, e ast.SortByExpr) (expressionExecutor, error) { + return func(data *model.Value) (*model.Value, error) { + if !data.IsSlice() { + return nil, fmt.Errorf("cannot sort by on non-slice data") + } + + type sortableValue struct { + index int + value *model.Value + } + values := make([]sortableValue, 0) + + if err := data.RangeSlice(func(i int, item *model.Value) error { + item, err := ExecuteAST(e.Expr, item, opts) + if err != nil { + return err + } + values = append(values, sortableValue{ + index: i, + value: item, + }) + return nil + }); err != nil { + return nil, fmt.Errorf("error ranging over slice: %w", err) + } + + slices.SortFunc(values, func(i, j sortableValue) int { + res, err := i.value.Compare(j.value) + if err != nil { + return 0 + } + if e.Descending { + return -res + } + return res + }) + + res := model.NewSliceValue() + + for _, i := range values { + item, err := data.GetSliceIndex(i.index) + if err != nil { + return nil, fmt.Errorf("error getting slice index: %w", err) + } + if err := res.Append(item); err != nil { + return nil, fmt.Errorf("error appending item to result: %w", err) + } + } + + return res, nil + }, nil +} + +func sortByExprExecutor2(opts *Options, e ast.SortByExpr) (expressionExecutor, error) { + return func(data *model.Value) (*model.Value, error) { + if !data.IsSlice() { + return nil, fmt.Errorf("cannot sort by on non-slice data") + } + + sortedValues := model.NewSliceValue() + sortedIndexes := make([]int, 0) + + if err := data.RangeSlice(func(i int, item *model.Value) error { + item, err := ExecuteAST(e.Expr, item, opts) + if err != nil { + return err + } + if err := sortedValues.Append(item); err != nil { + return fmt.Errorf("error appending item to result: %w", err) + } + sortedIndexes = append(sortedIndexes, i) + return nil + }); err != nil { + return nil, fmt.Errorf("error ranging over slice: %w", err) + } + + l, err := sortedValues.Len() + if err != nil { + return nil, fmt.Errorf("error getting length of slice: %w", err) + } + + for i := 0; i < l-1; i++ { + cur, err := sortedValues.GetSliceIndex(i) + if err != nil { + return nil, fmt.Errorf("error getting slice index: %w", err) + } + curIndex := sortedIndexes[i] + next, err := sortedValues.GetSliceIndex(i + 1) + if err != nil { + return nil, fmt.Errorf("error getting slice index: %w", err) + } + nextIndex := sortedIndexes[i+1] + + cmp, err := cur.Compare(next) + if err != nil { + return nil, fmt.Errorf("error comparing values: %w", err) + } + + if cmp == 0 { + continue + } + + if !e.Descending { + if cmp > 0 { + if err := sortedValues.SetSliceIndex(i, next); err != nil { + return nil, fmt.Errorf("error setting slice index: %w", err) + } + sortedIndexes[i] = nextIndex + if err := sortedValues.SetSliceIndex(i+1, cur); err != nil { + return nil, fmt.Errorf("error setting slice index: %w", err) + } + sortedIndexes[i+1] = curIndex + i -= 1 + } + } else { + if cmp < 0 { + if err := sortedValues.SetSliceIndex(i, next); err != nil { + return nil, fmt.Errorf("error setting slice index: %w", err) + } + sortedIndexes[i] = nextIndex + if err := sortedValues.SetSliceIndex(i+1, cur); err != nil { + return nil, fmt.Errorf("error setting slice index: %w", err) + } + sortedIndexes[i+1] = curIndex + i -= 1 + } + } + } + + res := model.NewSliceValue() + + for _, i := range sortedIndexes { + item, err := data.GetSliceIndex(i) + if err != nil { + return nil, fmt.Errorf("error getting slice index: %w", err) + } + if err := res.Append(item); err != nil { + return nil, fmt.Errorf("error appending item to result: %w", err) + } + } + + return res, nil + }, nil +} diff --git a/execution/execute_sort_by_test.go b/execution/execute_sort_by_test.go new file mode 100644 index 00000000..07dc5436 --- /dev/null +++ b/execution/execute_sort_by_test.go @@ -0,0 +1,181 @@ +package execution_test + +import ( + "testing" + + "github.com/tomwright/dasel/v3/model" +) + +func TestFuncSortBy(t *testing.T) { + runSortTests := func(in func() *model.Value, outAsc func() *model.Value, outDesc func() *model.Value) func(*testing.T) { + return func(t *testing.T) { + t.Run("asc default", testCase{ + inFn: in, + s: `sortBy($this)`, + outFn: outAsc, + }.run) + t.Run("asc", testCase{ + inFn: in, + s: `sortBy($this, asc)`, + outFn: outAsc, + }.run) + t.Run("desc", testCase{ + inFn: in, + s: `sortBy($this, desc)`, + outFn: outDesc, + }.run) + } + } + + t.Run("int", runSortTests( + func() *model.Value { + res := model.NewSliceValue() + if err := res.Append(model.NewIntValue(2)); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewIntValue(1)); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewIntValue(4)); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewIntValue(3)); err != nil { + t.Fatal(err) + } + return res + }, + func() *model.Value { + res := model.NewSliceValue() + if err := res.Append(model.NewIntValue(1)); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewIntValue(2)); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewIntValue(3)); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewIntValue(4)); err != nil { + t.Fatal(err) + } + return res + }, + func() *model.Value { + res := model.NewSliceValue() + if err := res.Append(model.NewIntValue(4)); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewIntValue(3)); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewIntValue(2)); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewIntValue(1)); err != nil { + t.Fatal(err) + } + return res + }, + )) + + t.Run("float", runSortTests( + func() *model.Value { + res := model.NewSliceValue() + if err := res.Append(model.NewFloatValue(2.23)); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewFloatValue(2)); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewFloatValue(5.123)); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewFloatValue(4.2)); err != nil { + t.Fatal(err) + } + return res + }, + func() *model.Value { + res := model.NewSliceValue() + if err := res.Append(model.NewFloatValue(2)); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewFloatValue(2.23)); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewFloatValue(4.2)); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewFloatValue(5.123)); err != nil { + t.Fatal(err) + } + return res + }, + func() *model.Value { + res := model.NewSliceValue() + if err := res.Append(model.NewFloatValue(5.123)); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewFloatValue(4.2)); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewFloatValue(2.23)); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewFloatValue(2)); err != nil { + t.Fatal(err) + } + return res + }, + )) + t.Run("string", runSortTests( + func() *model.Value { + res := model.NewSliceValue() + if err := res.Append(model.NewStringValue("def")); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewStringValue("abc")); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewStringValue("cde")); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewStringValue("bcd")); err != nil { + t.Fatal(err) + } + return res + }, + func() *model.Value { + res := model.NewSliceValue() + if err := res.Append(model.NewStringValue("abc")); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewStringValue("bcd")); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewStringValue("cde")); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewStringValue("def")); err != nil { + t.Fatal(err) + } + return res + }, + func() *model.Value { + res := model.NewSliceValue() + if err := res.Append(model.NewStringValue("def")); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewStringValue("cde")); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewStringValue("bcd")); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewStringValue("abc")); err != nil { + t.Fatal(err) + } + return res + }, + )) +} diff --git a/model/value_comparison.go b/model/value_comparison.go index 66049e16..6633effb 100644 --- a/model/value_comparison.go +++ b/model/value_comparison.go @@ -1,5 +1,33 @@ package model +func (v *Value) Compare(other *Value) (int, error) { + eq, err := v.Equal(other) + if err != nil { + return 0, err + } + eqVal, err := eq.BoolValue() + if err != nil { + return 0, err + } + if eqVal { + return 0, nil + } + + lt, err := v.LessThan(other) + if err != nil { + return 0, err + } + ltVal, err := lt.BoolValue() + if err != nil { + return 0, err + } + if ltVal { + return -1, nil + } + + return 1, nil +} + func (v *Value) Equal(other *Value) (*Value, error) { if v.IsInt() && other.IsFloat() { a, err := v.IntValue() @@ -92,6 +120,19 @@ func (v *Value) LessThan(other *Value) (*Value, error) { } return NewValue(a < float64(b)), nil } + + if v.IsString() && other.IsString() { + a, err := v.StringValue() + if err != nil { + return nil, err + } + b, err := other.StringValue() + if err != nil { + return nil, err + } + return NewValue(a < b), nil + } + return nil, &ErrIncompatibleTypes{A: v, B: other} } diff --git a/selector/ast/expression_complex.go b/selector/ast/expression_complex.go index f8a7509a..0d0517dd 100644 --- a/selector/ast/expression_complex.go +++ b/selector/ast/expression_complex.go @@ -94,6 +94,13 @@ type FilterExpr struct { func (FilterExpr) expr() {} +type SortByExpr struct { + Expr Expr + Descending bool +} + +func (SortByExpr) expr() {} + type VariableExpr struct { Name string } diff --git a/selector/lexer/token.go b/selector/lexer/token.go index f984c927..514ca768 100644 --- a/selector/lexer/token.go +++ b/selector/lexer/token.go @@ -58,6 +58,9 @@ const ( Map Filter RegexPattern + SortBy + Asc + Desc ) type Tokens []Token diff --git a/selector/lexer/tokenize.go b/selector/lexer/tokenize.go index 27213ead..611af4b2 100644 --- a/selector/lexer/tokenize.go +++ b/selector/lexer/tokenize.go @@ -237,6 +237,15 @@ func (p *Tokenizer) parseCurRune() (Token, error) { if t := matchStr(pos, "filter", false, Filter); t != nil { return *t, nil } + if t := matchStr(pos, "sortBy", false, SortBy); t != nil { + return *t, nil + } + if t := matchStr(pos, "asc", false, Asc); t != nil { + return *t, nil + } + if t := matchStr(pos, "desc", false, Desc); t != nil { + return *t, nil + } if t := matchRegexPattern(pos); t != nil { return *t, nil diff --git a/selector/lexer/tokenize_test.go b/selector/lexer/tokenize_test.go index fed02c5b..19eb2a39 100644 --- a/selector/lexer/tokenize_test.go +++ b/selector/lexer/tokenize_test.go @@ -54,6 +54,18 @@ func TestTokenizer_Parse(t *testing.T) { }, })) + t.Run("sort by", runTest(testCase{ + in: `sortBy(foo, asc)`, + out: []TokenKind{ + SortBy, + OpenParen, + Symbol, + Comma, + Asc, + CloseParen, + }, + })) + t.Run("everything", runTest(testCase{ in: "foo.bar.baz[1] != 42.123 || foo.bar.baz['hello'] == 42 && x == 'a\\'b' + false true . .... asd... $name null", out: []TokenKind{ diff --git a/selector/parser/parse_array.go b/selector/parser/parse_array.go index fc9601d0..0e3e6454 100644 --- a/selector/parser/parse_array.go +++ b/selector/parser/parse_array.go @@ -16,6 +16,7 @@ func parseArray(p *Parser) (ast.Expr, error) { lexer.TokenKinds(lexer.Comma), false, bpDefault, + true, ) if err != nil { return nil, err diff --git a/selector/parser/parse_branch.go b/selector/parser/parse_branch.go new file mode 100644 index 00000000..0ba77757 --- /dev/null +++ b/selector/parser/parse_branch.go @@ -0,0 +1,33 @@ +package parser + +import ( + "github.com/tomwright/dasel/v3/selector/ast" + "github.com/tomwright/dasel/v3/selector/lexer" +) + +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, + true, + ) + if err != nil { + return nil, err + } + + return ast.BranchExpr{ + Exprs: expressions, + }, nil +} diff --git a/selector/parser/parse_filter.go b/selector/parser/parse_filter.go new file mode 100644 index 00000000..c9462a34 --- /dev/null +++ b/selector/parser/parse_filter.go @@ -0,0 +1,28 @@ +package parser + +import ( + "github.com/tomwright/dasel/v3/selector/ast" + "github.com/tomwright/dasel/v3/selector/lexer" +) + +func parseFilter(p *Parser) (ast.Expr, error) { + if err := p.expect(lexer.Filter); err != nil { + return nil, err + } + p.advance() + + expr, err := p.parseExpressionsFromTo( + lexer.OpenParen, + lexer.CloseParen, + []lexer.TokenKind{}, + true, + bpDefault, + ) + if err != nil { + return nil, err + } + + return ast.FilterExpr{ + Expr: expr, + }, nil +} diff --git a/selector/parser/parse_func.go b/selector/parser/parse_func.go index a8fa34b2..0935d3ec 100644 --- a/selector/parser/parse_func.go +++ b/selector/parser/parse_func.go @@ -32,5 +32,6 @@ func parseArgs(p *Parser) (ast.Expressions, error) { []lexer.TokenKind{lexer.Comma}, false, bpCall, + true, ) } diff --git a/selector/parser/parse_group.go b/selector/parser/parse_group.go index 923ee838..f6ff0175 100644 --- a/selector/parser/parse_group.go +++ b/selector/parser/parse_group.go @@ -16,5 +16,6 @@ func parseGroup(p *Parser) (ast.Expr, error) { []lexer.TokenKind{}, true, bpDefault, + true, ) } diff --git a/selector/parser/parse_if.go b/selector/parser/parse_if.go index cfc2c314..63d90f6c 100644 --- a/selector/parser/parse_if.go +++ b/selector/parser/parse_if.go @@ -92,6 +92,7 @@ func (p *Parser) parseExpressionsFromTo( splitOn, requireExpressions, bp, + true, ) if err != nil { return nil, err diff --git a/selector/parser/parse_map.go b/selector/parser/parse_map.go index 589329b9..5ad21fc0 100644 --- a/selector/parser/parse_map.go +++ b/selector/parser/parse_map.go @@ -26,51 +26,3 @@ func parseMap(p *Parser) (ast.Expr, error) { Expr: expr, }, nil } - -func parseFilter(p *Parser) (ast.Expr, error) { - if err := p.expect(lexer.Filter); err != nil { - return nil, err - } - p.advance() - - expr, err := p.parseExpressionsFromTo( - lexer.OpenParen, - lexer.CloseParen, - []lexer.TokenKind{}, - true, - bpDefault, - ) - if err != nil { - return nil, err - } - - return ast.FilterExpr{ - Expr: expr, - }, 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_sort_by.go b/selector/parser/parse_sort_by.go new file mode 100644 index 00000000..040e29e8 --- /dev/null +++ b/selector/parser/parse_sort_by.go @@ -0,0 +1,60 @@ +package parser + +import ( + "github.com/tomwright/dasel/v3/selector/ast" + "github.com/tomwright/dasel/v3/selector/lexer" +) + +func parseSortBy(p *Parser) (ast.Expr, error) { + if err := p.expect(lexer.SortBy); err != nil { + return nil, err + } + p.advance() + + if err := p.expect(lexer.OpenParen); err != nil { + return nil, err + } + p.advance() + + sortExpr, err := p.parseExpressions( + lexer.TokenKinds(lexer.CloseParen, lexer.Comma), + nil, + true, + bpDefault, + false, + ) + if err != nil { + return nil, err + } + + res := ast.SortByExpr{ + Expr: sortExpr, + Descending: false, + } + + if p.current().IsKind(lexer.CloseParen) { + p.advance() + return res, nil + } + + if err := p.expect(lexer.Comma); err != nil { + return nil, err + } + p.advance() + + if err := p.expect(lexer.Asc, lexer.Desc); err != nil { + return nil, err + } + + if p.current().IsKind(lexer.Desc) { + res.Descending = true + } + + p.advance() + if err := p.expect(lexer.CloseParen); err != nil { + return nil, err + } + p.advance() + + return res, nil +} diff --git a/selector/parser/parser.go b/selector/parser/parser.go index 7810e2cc..daea7c8b 100644 --- a/selector/parser/parser.go +++ b/selector/parser/parser.go @@ -23,12 +23,15 @@ func (p *Parser) parseExpressionsAsSlice( splitOn []lexer.TokenKind, requireExpressions bool, bp bindingPower, + advanceOnBreak bool, ) (ast.Expressions, error) { var finalExpr ast.Expressions var current ast.Expressions for p.hasToken() { if p.current().IsKind(breakOn...) { - p.advance() + if advanceOnBreak { + p.advance() + } break } if p.current().IsKind(splitOn...) { @@ -63,8 +66,9 @@ func (p *Parser) parseExpressions( splitOn []lexer.TokenKind, requireExpressions bool, bp bindingPower, + advanceOnBreak bool, ) (ast.Expr, error) { - expressions, err := p.parseExpressionsAsSlice(breakOn, splitOn, requireExpressions, bp) + expressions, err := p.parseExpressionsAsSlice(breakOn, splitOn, requireExpressions, bp, advanceOnBreak) if err != nil { return nil, err } @@ -77,7 +81,7 @@ func (p *Parser) parseExpressions( } func (p *Parser) Parse() (ast.Expr, error) { - return p.parseExpressions([]lexer.TokenKind{lexer.EOF}, nil, true, bpDefault) + return p.parseExpressions([]lexer.TokenKind{lexer.EOF}, nil, true, bpDefault, true) } func (p *Parser) parseExpression(bp bindingPower) (left ast.Expr, err error) { @@ -110,6 +114,8 @@ func (p *Parser) parseExpression(bp bindingPower) (left ast.Expr, err error) { left, err = parseFilter(p) case lexer.RegexPattern: left, err = parseRegexPattern(p) + case lexer.SortBy: + left, err = parseSortBy(p) default: return nil, &UnexpectedTokenError{ Token: p.current(), From bfb0bd8b6d8f77ef94cc807f96164aebcc67e09f Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Tue, 22 Oct 2024 19:20:41 +0100 Subject: [PATCH 35/56] Rework JSON parser --- dencoding/json.go | 20 -- dencoding/json_decoder.go | 174 ----------------- dencoding/json_decoder_test.go | 54 ----- dencoding/json_encoder.go | 92 --------- dencoding/json_encoder_test.go | 36 ---- dencoding/map.go | 4 +- model/value.go | 3 + model/value_literal.go | 2 +- model/value_literal_test.go | 14 ++ model/value_set_test.go | 15 ++ parsing/json.go | 348 ++++++++++++++++++++++++++++++++- parsing/json_test.go | 49 +++++ 12 files changed, 427 insertions(+), 384 deletions(-) delete mode 100644 dencoding/json.go delete mode 100644 dencoding/json_decoder.go delete mode 100644 dencoding/json_decoder_test.go delete mode 100644 dencoding/json_encoder.go delete mode 100644 dencoding/json_encoder_test.go create mode 100644 model/value_literal_test.go create mode 100644 parsing/json_test.go diff --git a/dencoding/json.go b/dencoding/json.go deleted file mode 100644 index 5a82c3d1..00000000 --- a/dencoding/json.go +++ /dev/null @@ -1,20 +0,0 @@ -package dencoding - -import "encoding/json" - -const ( - jsonOpenObject = json.Delim('{') - jsonCloseObject = json.Delim('}') - jsonOpenArray = json.Delim('[') - jsonCloseArray = json.Delim(']') -) - -// JSONEncoderOption is identifies an option that can be applied to a JSON encoder. -type JSONEncoderOption interface { - ApplyEncoder(encoder *JSONEncoder) -} - -// JSONDecoderOption is identifies an option that can be applied to a JSON decoder. -type JSONDecoderOption interface { - ApplyDecoder(decoder *JSONDecoder) -} diff --git a/dencoding/json_decoder.go b/dencoding/json_decoder.go deleted file mode 100644 index 26b5f788..00000000 --- a/dencoding/json_decoder.go +++ /dev/null @@ -1,174 +0,0 @@ -package dencoding - -import ( - "encoding/json" - "fmt" - "io" - "reflect" - "strings" -) - -// JSONDecoder wraps a standard json encoder to implement custom ordering logic. -type JSONDecoder struct { - decoder *json.Decoder -} - -// NewJSONDecoder returns a new dencoding JSONDecoder. -func NewJSONDecoder(r io.Reader, options ...JSONDecoderOption) *JSONDecoder { - jsonDecoder := json.NewDecoder(r) - jsonDecoder.UseNumber() - decoder := &JSONDecoder{ - decoder: jsonDecoder, - } - for _, o := range options { - o.ApplyDecoder(decoder) - } - return decoder -} - -// Decode decodes the next item found in the decoder and writes it to v. -func (decoder *JSONDecoder) Decode(v any) error { - rv := reflect.ValueOf(v) - if rv.Kind() != reflect.Pointer || rv.IsNil() { - return fmt.Errorf("invalid decode target: %s", reflect.TypeOf(v)) - } - - rve := rv.Elem() - - t, err := decoder.decoder.Token() - if err != nil { - return err - } - - switch t { - case jsonOpenObject: - object, err := decoder.decodeObject() - if err != nil { - return fmt.Errorf("could not decode object: %w", err) - } - rve.Set(reflect.ValueOf(object)) - case jsonOpenArray: - arr, err := decoder.decodeArray() - if err != nil { - return fmt.Errorf("could not decode array: %w", err) - } - rve.Set(reflect.ValueOf(arr)) - default: - value, err := decoder.decodeValue(t) - if err != nil { - return err - } - rve.Set(reflect.ValueOf(value)) - } - - return nil -} - -func (decoder *JSONDecoder) decodeObject() (*Map, error) { - res := NewMap() - - var key any = nil - - for { - t, err := decoder.decoder.Token() - if err != nil { - // We don't expect an EOF here since we're in the middle of processing an object. - return res, err - } - - switch t { - case jsonOpenArray: - if key == nil { - return res, fmt.Errorf("unexpected token: %v", t) - } - value, err := decoder.decodeArray() - if err != nil { - return res, err - } - res.Set(key.(string), value) - key = nil - case jsonCloseArray: - return res, fmt.Errorf("unexpected token: %v", t) - case jsonCloseObject: - return res, nil - case jsonOpenObject: - if key == nil { - return res, fmt.Errorf("unexpected token: %v", t) - } - value, err := decoder.decodeObject() - if err != nil { - return res, err - } - res.Set(key.(string), value) - key = nil - default: - if key == nil { - key = t - } else { - value, err := decoder.decodeValue(t) - if err != nil { - return nil, err - } - res.Set(key.(string), value) - key = nil - } - } - } -} - -func (decoder *JSONDecoder) decodeValue(t json.Token) (any, error) { - switch tv := t.(type) { - case json.Number: - strNum := tv.String() - if strings.Contains(strNum, ".") { - floatNum, err := tv.Float64() - if err == nil { - return floatNum, nil - } - return nil, err - } - intNum, err := tv.Int64() - if err == nil { - return intNum, nil - } - - return nil, err - } - return t, nil -} - -func (decoder *JSONDecoder) decodeArray() ([]any, error) { - res := make([]any, 0) - for { - t, err := decoder.decoder.Token() - if err != nil { - // We don't expect an EOF here since we're in the middle of processing an object. - return res, err - } - - switch t { - case jsonOpenArray: - value, err := decoder.decodeArray() - if err != nil { - return res, err - } - res = append(res, value) - case jsonCloseArray: - return res, nil - case jsonCloseObject: - return res, fmt.Errorf("unexpected token: %t", t) - case jsonOpenObject: - value, err := decoder.decodeObject() - if err != nil { - return res, err - } - res = append(res, value) - default: - value, err := decoder.decodeValue(t) - if err != nil { - return nil, err - } - res = append(res, value) - } - } -} diff --git a/dencoding/json_decoder_test.go b/dencoding/json_decoder_test.go deleted file mode 100644 index 2d95e146..00000000 --- a/dencoding/json_decoder_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package dencoding_test - -import ( - "bytes" - "io" - "reflect" - "testing" - - "github.com/tomwright/dasel/v3/dencoding" -) - -func TestJSONDecoder_Decode(t *testing.T) { - b := []byte(`{"x":1,"a":"hello"}{"x":2,"a":"there"}{"a":"Tom","x":3}`) - dec := dencoding.NewJSONDecoder(bytes.NewReader(b)) - - maps := make([]any, 0) - for { - var v any - if err := dec.Decode(&v); err != nil { - if err == io.EOF { - break - } - t.Errorf("unexpected error: %v", err) - return - } - maps = append(maps, v) - } - - exp := [][]dencoding.KeyValue{ - { - {Key: "x", Value: int64(1)}, - {Key: "a", Value: "hello"}, - }, - { - {Key: "x", Value: int64(2)}, - {Key: "a", Value: "there"}, - }, - { - {Key: "a", Value: "Tom"}, - {Key: "x", Value: int64(3)}, - }, - } - - got := make([][]dencoding.KeyValue, 0) - for _, v := range maps { - if m, ok := v.(*dencoding.Map); ok { - got = append(got, m.KeyValues()) - } - } - - if !reflect.DeepEqual(exp, got) { - t.Errorf("expected %v, got %v", exp, got) - } -} diff --git a/dencoding/json_encoder.go b/dencoding/json_encoder.go deleted file mode 100644 index da060767..00000000 --- a/dencoding/json_encoder.go +++ /dev/null @@ -1,92 +0,0 @@ -package dencoding - -import ( - "bytes" - "encoding/json" - "io" -) - -// lastOptions contains the options that the last JSONEncoder was created with. -// Find a better way of passing this information into nested MarshalJSON calls. -var lastOptions []JSONEncoderOption - -// JSONEncoder wraps a standard json encoder to implement custom ordering logic. -type JSONEncoder struct { - encoder *json.Encoder -} - -// NewJSONEncoder returns a new dencoding JSONEncoder. -func NewJSONEncoder(w io.Writer, options ...JSONEncoderOption) *JSONEncoder { - jsonEncoder := json.NewEncoder(w) - encoder := &JSONEncoder{ - encoder: jsonEncoder, - } - for _, o := range options { - o.ApplyEncoder(encoder) - } - lastOptions = options - return encoder -} - -// Encode encodes the given value and writes the encodes bytes to the stream. -func (encoder *JSONEncoder) Encode(v any) error { - // We rely on Map.MarshalJSON to ensure ordering. - return encoder.encoder.Encode(v) -} - -// Close cleans up the encoder. -func (encoder *JSONEncoder) Close() error { - return nil -} - -// JSONEscapeHTML enables or disables html escaping when encoding JSON. -func JSONEscapeHTML(escape bool) JSONEncoderOption { - return jsonEncodeHTMLOption{escapeHTML: escape} -} - -type jsonEncodeHTMLOption struct { - escapeHTML bool -} - -func (option jsonEncodeHTMLOption) ApplyEncoder(encoder *JSONEncoder) { - encoder.encoder.SetEscapeHTML(option.escapeHTML) -} - -// JSONEncodeIndent sets the indentation when encoding JSON. -func JSONEncodeIndent(prefix string, indent string) JSONEncoderOption { - return jsonEncodeIndent{prefix: prefix, indent: indent} -} - -type jsonEncodeIndent struct { - prefix string - indent string -} - -func (option jsonEncodeIndent) ApplyEncoder(encoder *JSONEncoder) { - encoder.encoder.SetIndent(option.prefix, option.indent) -} - -// MarshalJSON JSON encodes the map and returns the bytes. -// This maintains ordering. -func (m *Map) MarshalJSON() ([]byte, error) { - - buf := new(bytes.Buffer) - buf.Write([]byte(`{`)) - encoder := NewJSONEncoder(buf, lastOptions...) - for i, key := range m.keys { - last := i == len(m.keys)-1 - - if err := encoder.Encode(key); err != nil { - return nil, err - } - buf.Write([]byte(`:`)) - if err := encoder.Encode(m.data[key]); err != nil { - return nil, err - } - if !last { - buf.Write([]byte(`,`)) - } - } - buf.Write([]byte(`}`)) - return buf.Bytes(), nil -} diff --git a/dencoding/json_encoder_test.go b/dencoding/json_encoder_test.go deleted file mode 100644 index 7bb557f8..00000000 --- a/dencoding/json_encoder_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package dencoding_test - -import ( - "bytes" - "testing" - - "github.com/tomwright/dasel/v3/dencoding" -) - -func TestJSONEncoder_Encode(t *testing.T) { - orig := dencoding.NewMap(). - Set("c", "x"). - Set("b", "y"). - Set("a", "z") - - exp := `{ - "c": "x", - "b": "y", - "a": "z" -} -` - - gotBuffer := new(bytes.Buffer) - - encoder := dencoding.NewJSONEncoder(gotBuffer, dencoding.JSONEncodeIndent("", " ")) - if err := encoder.Encode(orig); err != nil { - t.Errorf("unexpected error: %v", err) - return - } - - got := gotBuffer.String() - - if exp != got { - t.Errorf("expected %s, got %s", exp, got) - } -} diff --git a/dencoding/map.go b/dencoding/map.go index 22fb1996..10ed13d1 100644 --- a/dencoding/map.go +++ b/dencoding/map.go @@ -1,6 +1,8 @@ package dencoding -import "reflect" +import ( + "reflect" +) // NewMap returns a new *Map that has its values initialised. func NewMap() *Map { diff --git a/model/value.go b/model/value.go index 58a31690..969bab4d 100644 --- a/model/value.go +++ b/model/value.go @@ -20,6 +20,7 @@ const ( TypeMap Type = "map" TypeSlice Type = "array" TypeUnknown Type = "unknown" + TypeNull Type = "null" ) type KeyValue struct { @@ -129,6 +130,8 @@ func (v *Value) Type() Type { return TypeMap case v.IsSlice(): return TypeSlice + case v.IsNull(): + return TypeNull default: return TypeUnknown } diff --git a/model/value_literal.go b/model/value_literal.go index 51ac280a..096a658b 100644 --- a/model/value_literal.go +++ b/model/value_literal.go @@ -17,7 +17,7 @@ func NewNullValue() *Value { // IsNull returns true if the value is null. func (v *Value) IsNull() bool { - return v.isNull() + return v.UnpackKinds(reflect.Ptr, reflect.Interface).isNull() } func (v *Value) isNull() bool { diff --git a/model/value_literal_test.go b/model/value_literal_test.go new file mode 100644 index 00000000..c34e8fff --- /dev/null +++ b/model/value_literal_test.go @@ -0,0 +1,14 @@ +package model_test + +import ( + "testing" + + "github.com/tomwright/dasel/v3/model" +) + +func TestValue_IsNull(t *testing.T) { + v := model.NewNullValue() + if !v.IsNull() { + t.Fatalf("expected value to be null") + } +} diff --git a/model/value_set_test.go b/model/value_set_test.go index d388aaff..11f0f03d 100644 --- a/model/value_set_test.go +++ b/model/value_set_test.go @@ -46,6 +46,7 @@ func TestValue_Set(t *testing.T) { boolValue func() *model.Value mapValue func() *model.Value sliceValue func() *model.Value + nullValue func() *model.Value }{ { name: "model constructor", @@ -75,6 +76,9 @@ func TestValue_Set(t *testing.T) { } return res }, + nullValue: func() *model.Value { + return model.NewNullValue() + }, }, { name: "go types non ptr", @@ -106,6 +110,9 @@ func TestValue_Set(t *testing.T) { } return model.NewValue(v) }, + nullValue: func() *model.Value { + return model.NewValue(nil) + }, }, { name: "go types ptr", @@ -137,6 +144,10 @@ func TestValue_Set(t *testing.T) { } return model.NewValue(&v) }, + nullValue: func() *model.Value { + var x any + return model.NewValue(&x) + }, }, } @@ -219,6 +230,10 @@ func TestValue_Set(t *testing.T) { return res }, }.run) + t.Run("string over null", setTestCase{ + valueFn: tc.nullValue, + newValue: model.NewStringValue("world"), + }.run) }) } } diff --git a/parsing/json.go b/parsing/json.go index b662f44a..589e9cde 100644 --- a/parsing/json.go +++ b/parsing/json.go @@ -1,11 +1,22 @@ package parsing import ( + "bytes" "encoding/json" + "fmt" + "io" + "strings" "github.com/tomwright/dasel/v3/model" ) +const ( + jsonOpenObject = json.Delim('{') + jsonCloseObject = json.Delim('}') + jsonOpenArray = json.Delim('[') + jsonCloseArray = json.Delim(']') +) + // NewJSONReader creates a new JSON reader. func NewJSONReader() (Reader, error) { return &jsonReader{}, nil @@ -20,20 +31,345 @@ type jsonReader struct{} // Read reads a value from a byte slice. func (j *jsonReader) Read(data []byte) (*model.Value, error) { - var unmarshalled any - if err := json.Unmarshal(data, &unmarshalled); err != nil { + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.UseNumber() + + t, err := decoder.Token() + if err != nil { + return nil, err + } + + var res *model.Value + + switch t { + case jsonOpenObject: + res, err = j.decodeObject(decoder) + if err != nil { + return nil, fmt.Errorf("could not decode object: %w", err) + } + case jsonOpenArray: + res, err = j.decodeArray(decoder) + if err != nil { + return nil, fmt.Errorf("could not decode array: %w", err) + } + default: + res, err = j.decodeToken(decoder, t) + if err != nil { + return nil, err + } + } + + return res, nil +} + +func (j *jsonReader) decodeObject(decoder *json.Decoder) (*model.Value, error) { + res := model.NewMapValue() + + var key any = nil + + for { + t, err := decoder.Token() + if err != nil { + // We don't expect an EOF here since we're in the middle of processing an object. + return res, err + } + + switch t { + case jsonOpenArray: + if key == nil { + return res, fmt.Errorf("unexpected token: %v", t) + } + value, err := j.decodeArray(decoder) + if err != nil { + return res, err + } + if err := res.SetMapKey(key.(string), value); err != nil { + return res, err + } + key = nil + case jsonCloseArray: + return res, fmt.Errorf("unexpected token: %v", t) + case jsonCloseObject: + return res, nil + case jsonOpenObject: + if key == nil { + return res, fmt.Errorf("unexpected token: %v", t) + } + value, err := j.decodeObject(decoder) + if err != nil { + return res, err + } + if err := res.SetMapKey(key.(string), value); err != nil { + return res, err + } + key = nil + default: + if key == nil { + if tStr, ok := t.(string); ok { + key = tStr + } else { + return nil, fmt.Errorf("unexpected token: %v", t) + } + } else { + value, err := j.decodeToken(decoder, t) + if err != nil { + return nil, err + } + if err := res.SetMapKey(key.(string), value); err != nil { + return res, err + } + key = nil + } + } + } +} + +func (j *jsonReader) decodeArray(decoder *json.Decoder) (*model.Value, error) { + res := model.NewSliceValue() + for { + t, err := decoder.Token() + if err != nil { + // We don't expect an EOF here since we're in the middle of processing an object. + return res, err + } + + switch t { + case jsonOpenArray: + value, err := j.decodeArray(decoder) + if err != nil { + return res, err + } + if err := res.Append(value); err != nil { + return res, err + } + case jsonCloseArray: + return res, nil + case jsonCloseObject: + return res, fmt.Errorf("unexpected token: %t", t) + case jsonOpenObject: + value, err := j.decodeObject(decoder) + if err != nil { + return res, err + } + if err := res.Append(value); err != nil { + return res, err + } + default: + value, err := j.decodeToken(decoder, t) + if err != nil { + return nil, err + } + if err := res.Append(value); err != nil { + return res, err + } + } + } +} + +func (j *jsonReader) decodeToken(decoder *json.Decoder, t json.Token) (*model.Value, error) { + switch tv := t.(type) { + case json.Number: + strNum := tv.String() + if strings.Contains(strNum, ".") { + floatNum, err := tv.Float64() + if err == nil { + return model.NewFloatValue(floatNum), nil + } + return nil, err + } + intNum, err := tv.Int64() + if err == nil { + return model.NewIntValue(intNum), nil + } + return nil, err + default: + return model.NewValue(tv), nil } - return model.NewValue(&unmarshalled), nil } type jsonWriter struct{} // Write writes a value to a byte slice. func (j *jsonWriter) Write(value *model.Value) ([]byte, error) { - res, err := json.Marshal(value.Interface()) - if err != nil { + buf := new(bytes.Buffer) + + es := encoderState{indentStr: " "} + + encoderFn := func(v any) error { + res, err := json.Marshal(v) + if err != nil { + return err + } + _, err = buf.Write(res) + return err + } + + if err := j.write(buf, encoderFn, es, value); err != nil { return nil, err } - return append(res, []byte("\n")...), nil + + if _, err := buf.Write([]byte("\n")); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +type encoderState struct { + indent int + indentStr string +} + +func (es encoderState) inc() encoderState { + es.indent++ + return es +} + +func (es encoderState) writeIndent(w io.Writer) error { + if es.indent == 0 || es.indentStr == "" { + return nil + } + i := strings.Repeat(es.indentStr, es.indent) + if _, err := w.Write([]byte(i)); err != nil { + return err + } + return nil +} + +type encoderFn func(v any) error + +func (j *jsonWriter) write(w io.Writer, encoder encoderFn, es encoderState, value *model.Value) error { + switch value.Type() { + case model.TypeMap: + return j.writeMap(w, encoder, es, value) + case model.TypeSlice: + return j.writeSlice(w, encoder, es, value) + case model.TypeString: + val, err := value.StringValue() + if err != nil { + return err + } + return encoder(val) + case model.TypeInt: + val, err := value.IntValue() + if err != nil { + return err + } + return encoder(val) + case model.TypeFloat: + val, err := value.FloatValue() + if err != nil { + return err + } + return encoder(val) + case model.TypeBool: + val, err := value.BoolValue() + if err != nil { + return err + } + return encoder(val) + case model.TypeNull: + return encoder(nil) + default: + return fmt.Errorf("unsupported type: %s", value.Type()) + } +} + +func (j *jsonWriter) writeMap(w io.Writer, encoder encoderFn, es encoderState, value *model.Value) error { + kvs, err := value.MapKeyValues() + if err != nil { + return err + } + + if _, err := w.Write([]byte(`{`)); err != nil { + return err + } + + if len(kvs) > 0 { + if _, err := w.Write([]byte("\n")); err != nil { + return err + } + + incEs := es.inc() + for i, kv := range kvs { + if err := incEs.writeIndent(w); err != nil { + return err + } + + if _, err := w.Write([]byte(fmt.Sprintf(`"%s": `, kv.Key))); err != nil { + return err + } + + if err := j.write(w, encoder, incEs, kv.Value); err != nil { + return err + } + + if i < len(kvs)-1 { + if _, err := w.Write([]byte(`,`)); err != nil { + return err + } + } + + if _, err := w.Write([]byte("\n")); err != nil { + return err + } + } + if err := es.writeIndent(w); err != nil { + return err + } + } + + if _, err := w.Write([]byte(`}`)); err != nil { + return err + } + + return nil +} + +func (j *jsonWriter) writeSlice(w io.Writer, encoder encoderFn, es encoderState, value *model.Value) error { + if _, err := w.Write([]byte(`[`)); err != nil { + return err + } + + length, err := value.SliceLen() + if err != nil { + return fmt.Errorf("error getting slice length: %w", err) + } + + if length > 0 { + if _, err := w.Write([]byte("\n")); err != nil { + return err + } + incEs := es.inc() + for i := 0; i < length; i++ { + if err := incEs.writeIndent(w); err != nil { + return err + } + va, err := value.GetSliceIndex(i) + if err != nil { + return fmt.Errorf("error getting slice index %d: %w", i, err) + } + if err := j.write(w, encoder, incEs, va); err != nil { + return err + } + if i < length-1 { + if _, err := w.Write([]byte(`,`)); err != nil { + return err + } + } + if _, err := w.Write([]byte("\n")); err != nil { + return err + } + } + if err := es.writeIndent(w); err != nil { + return err + } + } + + if _, err := w.Write([]byte(`]`)); err != nil { + return err + } + + return nil } diff --git a/parsing/json_test.go b/parsing/json_test.go new file mode 100644 index 00000000..0a55e1c4 --- /dev/null +++ b/parsing/json_test.go @@ -0,0 +1,49 @@ +package parsing_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/tomwright/dasel/v3/parsing" +) + +func TestJson(t *testing.T) { + doc := []byte(`{ + "string": "foo", + "int": 1, + "float": 1.1, + "bool": true, + "null": null, + "array": [ + 1, + 2, + 3 + ], + "object": { + "key": "value" + } +} +`) + reader, err := parsing.NewJSONReader() + if err != nil { + t.Fatal(err) + } + writer, err := parsing.NewJSONWriter() + if err != nil { + t.Fatal(err) + } + + value, err := reader.Read(doc) + if err != nil { + t.Fatal(err) + } + + newDoc, err := writer.Write(value) + if err != nil { + t.Fatal(err) + } + + if string(doc) != string(newDoc) { + t.Fatalf("expected %s, got %s...\n%s", string(doc), string(newDoc), cmp.Diff(string(doc), string(newDoc))) + } +} From e081e49de219b61258d6bea4ff05babbf801e871 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Thu, 24 Oct 2024 00:15:10 +0100 Subject: [PATCH 36/56] Add unary expr support and tests --- execution/execute.go | 6 + execution/execute_filter_test.go | 25 + execution/execute_unary.go | 29 ++ execution/execute_unary_test.go | 76 +++ execution/func.go | 1 + execution/func_type_of.go | 14 + execution/func_type_of_test.go | 38 ++ model/error.go | 24 +- model/generic.go | 1 - model/value.go | 19 +- model/value_comparison.go | 10 + model/value_comparison_test.go | 803 +++++++++++++++++++++++++++++ model/value_literal.go | 21 +- model/value_map.go | 20 +- model/value_math.go | 5 + model/value_math_test.go | 150 ++++++ model/value_metadata.go | 16 +- model/value_metadata_test.go | 29 ++ model/value_set.go | 1 + model/value_slice.go | 24 +- model/value_test.go | 53 ++ model/values.go | 3 - selector/ast/expression_literal.go | 4 + selector/parser/parser.go | 23 + 24 files changed, 1357 insertions(+), 38 deletions(-) create mode 100644 execution/execute_unary.go create mode 100644 execution/execute_unary_test.go create mode 100644 execution/func_type_of.go create mode 100644 execution/func_type_of_test.go delete mode 100644 model/generic.go create mode 100644 model/value_comparison_test.go create mode 100644 model/value_math_test.go create mode 100644 model/value_metadata_test.go create mode 100644 model/value_test.go delete mode 100644 model/values.go diff --git a/execution/execute.go b/execution/execute.go index fcb14564..854db706 100644 --- a/execution/execute.go +++ b/execution/execute.go @@ -68,6 +68,8 @@ func exprExecutor(opts *Options, expr ast.Expr) (expressionExecutor, error) { switch e := expr.(type) { case ast.BinaryExpr: return binaryExprExecutor(opts, e) + case ast.UnaryExpr: + return unaryExprExecutor(opts, e) case ast.CallExpr: return callExprExecutor(opts, e) case ast.ChainedExpr: @@ -109,6 +111,10 @@ func exprExecutor(opts *Options, expr ast.Expr) (expressionExecutor, error) { }, nil case ast.SortByExpr: return sortByExprExecutor(opts, e) + case ast.NullExpr: + return func(data *model.Value) (*model.Value, error) { + return model.NewNullValue(), nil + }, nil default: return nil, fmt.Errorf("unhandled expression type: %T", e) } diff --git a/execution/execute_filter_test.go b/execution/execute_filter_test.go index fd82a44d..c3cd78cf 100644 --- a/execution/execute_filter_test.go +++ b/execution/execute_filter_test.go @@ -37,6 +37,23 @@ func TestFilter(t *testing.T) { return s }, }.run) + t.Run("all !false", testCase{ + inFn: inSlice, + s: "filter(!false)", + outFn: func() *model.Value { + s := model.NewSliceValue() + if err := s.Append(model.NewIntValue(1)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := s.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if err := s.Append(model.NewIntValue(3)); err != nil { + t.Fatalf("unexpected error: %s", err) + } + return s + }, + }.run) t.Run("all false", testCase{ inFn: inSlice, s: "filter(false)", @@ -45,6 +62,14 @@ func TestFilter(t *testing.T) { return s }, }.run) + t.Run("all !true", testCase{ + inFn: inSlice, + s: "filter(!true)", + outFn: func() *model.Value { + s := model.NewSliceValue() + return s + }, + }.run) t.Run("equal 2", testCase{ inFn: inSlice, s: "filter($this == 2)", diff --git a/execution/execute_unary.go b/execution/execute_unary.go new file mode 100644 index 00000000..8ed02ae3 --- /dev/null +++ b/execution/execute_unary.go @@ -0,0 +1,29 @@ +package execution + +import ( + "fmt" + + "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/selector/ast" + "github.com/tomwright/dasel/v3/selector/lexer" +) + +func unaryExprExecutor(opts *Options, e ast.UnaryExpr) (expressionExecutor, error) { + return func(data *model.Value) (*model.Value, error) { + right, err := ExecuteAST(e.Right, data, opts) + if err != nil { + return nil, fmt.Errorf("error evaluating right expression: %w", err) + } + + switch e.Operator.Kind { + case lexer.Exclamation: + boolV, err := right.BoolValue() + if err != nil { + return nil, fmt.Errorf("error converting value to boolean: %w", err) + } + return model.NewBoolValue(!boolV), nil + default: + return nil, fmt.Errorf("unhandled unary operator: %s", e.Operator.Value) + } + }, nil +} diff --git a/execution/execute_unary_test.go b/execution/execute_unary_test.go new file mode 100644 index 00000000..ce8764d1 --- /dev/null +++ b/execution/execute_unary_test.go @@ -0,0 +1,76 @@ +package execution_test + +import ( + "testing" + + "github.com/tomwright/dasel/v3/dencoding" + "github.com/tomwright/dasel/v3/model" +) + +func TestUnary(t *testing.T) { + t.Run("not", func(t *testing.T) { + t.Run("literals", func(t *testing.T) { + t.Run("not true", testCase{ + s: `!true`, + out: model.NewBoolValue(false), + }.run) + t.Run("not not true", testCase{ + s: `!!true`, + out: model.NewBoolValue(true), + }.run) + t.Run("not not not true", testCase{ + s: `!!!true`, + out: model.NewBoolValue(false), + }.run) + t.Run("not false", testCase{ + s: `!false`, + out: model.NewBoolValue(true), + }.run) + t.Run("not not false", testCase{ + s: `!!false`, + out: model.NewBoolValue(false), + }.run) + t.Run("not not not false", testCase{ + s: `!!!false`, + out: model.NewBoolValue(true), + }.run) + }) + t.Run("variables", func(t *testing.T) { + in := func() *model.Value { + return model.NewValue(dencoding.NewMap(). + Set("t", true). + Set("f", false)) + } + t.Run("not true", testCase{ + s: `!t`, + inFn: in, + out: model.NewBoolValue(false), + }.run) + t.Run("not not true", testCase{ + s: `!!t`, + inFn: in, + out: model.NewBoolValue(true), + }.run) + t.Run("not not not true", testCase{ + s: `!!!t`, + inFn: in, + out: model.NewBoolValue(false), + }.run) + t.Run("not false", testCase{ + s: `!f`, + inFn: in, + out: model.NewBoolValue(true), + }.run) + t.Run("not not false", testCase{ + s: `!!f`, + inFn: in, + out: model.NewBoolValue(false), + }.run) + t.Run("not not not false", testCase{ + s: `!!!f`, + inFn: in, + out: model.NewBoolValue(true), + }.run) + }) + }) +} diff --git a/execution/func.go b/execution/func.go index b1f69531..5d8e7495 100644 --- a/execution/func.go +++ b/execution/func.go @@ -14,6 +14,7 @@ var ( FuncToString, FuncMerge, FuncReverse, + FuncTypeOf, ) ) diff --git a/execution/func_type_of.go b/execution/func_type_of.go new file mode 100644 index 00000000..54258054 --- /dev/null +++ b/execution/func_type_of.go @@ -0,0 +1,14 @@ +package execution + +import ( + "github.com/tomwright/dasel/v3/model" +) + +// FuncTypeOf is a function that returns the type of the first argument as a string. +var FuncTypeOf = NewFunc( + "typeOf", + func(data *model.Value, args model.Values) (*model.Value, error) { + return model.NewStringValue(args[0].Type().String()), nil + }, + ValidateArgsExactly(1), +) diff --git a/execution/func_type_of_test.go b/execution/func_type_of_test.go new file mode 100644 index 00000000..d2e8e876 --- /dev/null +++ b/execution/func_type_of_test.go @@ -0,0 +1,38 @@ +package execution_test + +import ( + "testing" + + "github.com/tomwright/dasel/v3/model" +) + +func TestFuncTypeOf(t *testing.T) { + t.Run("string", testCase{ + s: `typeOf("hello")`, + out: model.NewStringValue("string"), + }.run) + t.Run("int", testCase{ + s: `typeOf(123)`, + out: model.NewStringValue("int"), + }.run) + t.Run("float", testCase{ + s: `typeOf(12.3)`, + out: model.NewStringValue("float"), + }.run) + t.Run("bool", testCase{ + s: `typeOf(true)`, + out: model.NewStringValue("bool"), + }.run) + t.Run("array", testCase{ + s: `typeOf([])`, + out: model.NewStringValue("array"), + }.run) + t.Run("map", testCase{ + s: `typeOf({})`, + out: model.NewStringValue("map"), + }.run) + t.Run("null", testCase{ + s: `typeOf(null)`, + out: model.NewStringValue("null"), + }.run) +} diff --git a/model/error.go b/model/error.go index b70fbb0a..8cadf36a 100644 --- a/model/error.go +++ b/model/error.go @@ -8,7 +8,7 @@ type MapKeyNotFound struct { } // Error returns the error message. -func (e *MapKeyNotFound) Error() string { +func (e MapKeyNotFound) Error() string { return fmt.Sprintf("map key not found: %q", e.Key) } @@ -18,7 +18,7 @@ type SliceIndexOutOfRange struct { } // Error returns the error message. -func (e *SliceIndexOutOfRange) Error() string { +func (e SliceIndexOutOfRange) Error() string { return fmt.Sprintf("slice index out of range: %d", e.Index) } @@ -29,6 +29,24 @@ type ErrIncompatibleTypes struct { } // Error returns the error message. -func (e *ErrIncompatibleTypes) Error() string { +func (e ErrIncompatibleTypes) Error() string { return fmt.Sprintf("incompatible types: %s and %s", e.A.Type(), e.B.Type()) } + +type ErrUnexpectedType struct { + Expected Type + Actual Type +} + +func (e ErrUnexpectedType) Error() string { + return fmt.Sprintf("unexpected type: expected %s, got %s", e.Expected, e.Actual) +} + +type ErrUnexpectedTypes struct { + Expected []Type + Actual Type +} + +func (e ErrUnexpectedTypes) Error() string { + return fmt.Sprintf("unexpected type: expected %v, got %s", e.Expected, e.Actual) +} diff --git a/model/generic.go b/model/generic.go deleted file mode 100644 index 8b537907..00000000 --- a/model/generic.go +++ /dev/null @@ -1 +0,0 @@ -package model diff --git a/model/value.go b/model/value.go index 969bab4d..82b9205a 100644 --- a/model/value.go +++ b/model/value.go @@ -23,11 +23,16 @@ const ( TypeNull Type = "null" ) +// KeyValue represents a key value pair. type KeyValue struct { Key string Value *Value } +// Values represents a list of values. +type Values []*Value + +// Value represents a value. type Value struct { Value reflect.Value Metadata map[string]any @@ -35,6 +40,7 @@ type Value struct { setFn func(*Value) error } +// NewValue creates a new value. func NewValue(v any) *Value { switch val := v.(type) { case *Value: @@ -56,14 +62,17 @@ func NewValue(v any) *Value { } } +// Interface returns the value as an interface. func (v *Value) Interface() any { return v.Value.Interface() } +// Kind returns the reflect kind of the value. func (v *Value) Kind() reflect.Kind { return v.Value.Kind() } +// UnpackKinds unpacks the reflect value until it no longer matches the given kinds. func (v *Value) UnpackKinds(kinds ...reflect.Kind) *Value { res := v.Value for { @@ -74,6 +83,7 @@ func (v *Value) UnpackKinds(kinds ...reflect.Kind) *Value { } } +// UnpackUntilType unpacks the reflect value until it matches the given type. func (v *Value) UnpackUntilType(t reflect.Type) (*Value, error) { res := v.Value for { @@ -88,6 +98,7 @@ func (v *Value) UnpackUntilType(t reflect.Type) (*Value, error) { } } +// UnpackUntilAddressable unpacks the reflect value until it is addressable. func (v *Value) UnpackUntilAddressable() (*Value, error) { res := v.Value for { @@ -102,6 +113,7 @@ func (v *Value) UnpackUntilAddressable() (*Value, error) { } } +// UnpackUntilKind unpacks the reflect value until it matches the given kind. func (v *Value) UnpackUntilKind(k reflect.Kind) (*Value, error) { res := v.Value for { @@ -116,6 +128,7 @@ func (v *Value) UnpackUntilKind(k reflect.Kind) (*Value, error) { } } +// Type returns the type of the value. func (v *Value) Type() Type { switch { case v.IsString(): @@ -137,6 +150,7 @@ func (v *Value) Type() Type { } } +// Len returns the length of the value. func (v *Value) Len() (int, error) { var l int var err error @@ -149,7 +163,10 @@ func (v *Value) Len() (int, error) { case v.IsString(): l, err = v.StringLen() default: - err = fmt.Errorf("len expects string, slice or map") + err = ErrUnexpectedTypes{ + Expected: []Type{TypeSlice, TypeMap, TypeString}, + Actual: v.Type(), + } } if err != nil { diff --git a/model/value_comparison.go b/model/value_comparison.go index 6633effb..1603d5f5 100644 --- a/model/value_comparison.go +++ b/model/value_comparison.go @@ -1,5 +1,6 @@ package model +// Compare compares two values. func (v *Value) Compare(other *Value) (int, error) { eq, err := v.Equal(other) if err != nil { @@ -28,6 +29,7 @@ func (v *Value) Compare(other *Value) (int, error) { return 1, nil } +// Equal compares two values. func (v *Value) Equal(other *Value) (*Value, error) { if v.IsInt() && other.IsFloat() { a, err := v.IntValue() @@ -63,6 +65,7 @@ func (v *Value) Equal(other *Value) (*Value, error) { return NewValue(isEqual), nil } +// NotEqual compares two values. func (v *Value) NotEqual(other *Value) (*Value, error) { equals, err := v.Equal(other) if err != nil { @@ -75,6 +78,7 @@ func (v *Value) NotEqual(other *Value) (*Value, error) { return NewValue(!boolValue), nil } +// LessThan compares two values. func (v *Value) LessThan(other *Value) (*Value, error) { if v.IsInt() && other.IsInt() { a, err := v.IntValue() @@ -136,6 +140,7 @@ func (v *Value) LessThan(other *Value) (*Value, error) { return nil, &ErrIncompatibleTypes{A: v, B: other} } +// LessThanOrEqual compares two values. func (v *Value) LessThanOrEqual(other *Value) (*Value, error) { lessThan, err := v.LessThan(other) if err != nil { @@ -156,6 +161,7 @@ func (v *Value) LessThanOrEqual(other *Value) (*Value, error) { return NewValue(boolValue || boolEquals), nil } +// GreaterThan compares two values. func (v *Value) GreaterThan(other *Value) (*Value, error) { lessThanOrEqual, err := v.LessThanOrEqual(other) if err != nil { @@ -168,6 +174,7 @@ func (v *Value) GreaterThan(other *Value) (*Value, error) { return NewValue(!boolValue), nil } +// GreaterThanOrEqual compares two values. func (v *Value) GreaterThanOrEqual(other *Value) (*Value, error) { lessThan, err := v.LessThan(other) if err != nil { @@ -180,6 +187,7 @@ func (v *Value) GreaterThanOrEqual(other *Value) (*Value, error) { return NewValue(!boolValue), nil } +// EqualTypeValue compares two values of the same type. func (v *Value) EqualTypeValue(other *Value) (bool, error) { if v.Type() != other.Type() { return false, nil @@ -286,6 +294,8 @@ func (v *Value) EqualTypeValue(other *Value) (bool, error) { } } return true, nil + case TypeNull: + return other.Type() == TypeNull, nil default: return false, nil } diff --git a/model/value_comparison_test.go b/model/value_comparison_test.go new file mode 100644 index 00000000..646c2c88 --- /dev/null +++ b/model/value_comparison_test.go @@ -0,0 +1,803 @@ +package model_test + +import ( + "testing" + + "github.com/tomwright/dasel/v3/model" +) + +type compareTestCase struct { + a *model.Value + b *model.Value + exp bool +} + +func TestValue_Equal(t *testing.T) { + run := func(tc compareTestCase) func(t *testing.T) { + return func(t *testing.T) { + got, err := tc.a.Equal(tc.b) + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + gotBool, err := got.BoolValue() + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if gotBool != tc.exp { + t.Errorf("expected %v, got %v", tc.exp, got) + } + } + } + t.Run("string", func(t *testing.T) { + t.Run("equal", run(compareTestCase{ + a: model.NewStringValue("hello"), + b: model.NewStringValue("hello"), + exp: true, + })) + t.Run("not equal", run(compareTestCase{ + a: model.NewStringValue("hello"), + b: model.NewStringValue("world"), + exp: false, + })) + }) + t.Run("int", func(t *testing.T) { + t.Run("equal", run(compareTestCase{ + a: model.NewIntValue(1), + b: model.NewIntValue(1), + exp: true, + })) + t.Run("not equal", run(compareTestCase{ + a: model.NewIntValue(1), + b: model.NewIntValue(2), + exp: false, + })) + t.Run("equal float", run(compareTestCase{ + a: model.NewIntValue(1), + b: model.NewFloatValue(1), + exp: true, + })) + t.Run("not equal float", run(compareTestCase{ + a: model.NewIntValue(1), + b: model.NewFloatValue(2), + exp: false, + })) + }) + t.Run("float", func(t *testing.T) { + t.Run("equal", run(compareTestCase{ + a: model.NewFloatValue(1.1), + b: model.NewFloatValue(1.1), + exp: true, + })) + t.Run("not equal", run(compareTestCase{ + a: model.NewFloatValue(1.1), + b: model.NewFloatValue(1.2), + exp: false, + })) + t.Run("equal int", run(compareTestCase{ + a: model.NewFloatValue(1), + b: model.NewIntValue(1), + exp: true, + })) + t.Run("not equal int", run(compareTestCase{ + a: model.NewFloatValue(1.1), + b: model.NewIntValue(1), + exp: false, + })) + }) + t.Run("bool", func(t *testing.T) { + t.Run("equal", run(compareTestCase{ + a: model.NewBoolValue(true), + b: model.NewBoolValue(true), + exp: true, + })) + t.Run("not equal", run(compareTestCase{ + a: model.NewBoolValue(true), + b: model.NewBoolValue(false), + exp: false, + })) + }) + t.Run("map", func(t *testing.T) { + t.Run("equal", run(compareTestCase{ + a: model.NewValue(map[string]interface{}{ + "hello": "world", + }), + b: model.NewValue(map[string]interface{}{ + "hello": "world", + }), + exp: true, + })) + t.Run("not equal", run(compareTestCase{ + a: model.NewValue(map[string]interface{}{ + "hello": "world", + }), + b: model.NewValue(map[string]interface{}{ + "hello": "world2", + }), + exp: false, + })) + }) + t.Run("array", func(t *testing.T) { + t.Run("equal", run(compareTestCase{ + a: model.NewValue([]interface{}{ + "hello", "world", + }), + b: model.NewValue([]interface{}{ + "hello", "world", + }), + exp: true, + })) + t.Run("not equal", run(compareTestCase{ + a: model.NewValue([]interface{}{ + "hello", "world", + }), + b: model.NewValue([]interface{}{ + "hello", "world2", + }), + exp: false, + })) + }) + t.Run("null", func(t *testing.T) { + t.Run("equal", run(compareTestCase{ + a: model.NewValue(nil), + b: model.NewValue(nil), + exp: true, + })) + }) +} + +func TestValue_NotEqual(t *testing.T) { + run := func(tc compareTestCase) func(t *testing.T) { + return func(t *testing.T) { + got, err := tc.a.NotEqual(tc.b) + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + gotBool, err := got.BoolValue() + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if gotBool != tc.exp { + t.Errorf("expected %v, got %v", tc.exp, got) + } + } + } + t.Run("string", func(t *testing.T) { + t.Run("equal", run(compareTestCase{ + a: model.NewStringValue("hello"), + b: model.NewStringValue("hello"), + exp: false, + })) + t.Run("not equal", run(compareTestCase{ + a: model.NewStringValue("hello"), + b: model.NewStringValue("world"), + exp: true, + })) + }) + t.Run("int", func(t *testing.T) { + t.Run("equal", run(compareTestCase{ + a: model.NewIntValue(1), + b: model.NewIntValue(1), + exp: false, + })) + t.Run("not equal", run(compareTestCase{ + a: model.NewIntValue(1), + b: model.NewIntValue(2), + exp: true, + })) + t.Run("equal float", run(compareTestCase{ + a: model.NewIntValue(1), + b: model.NewFloatValue(1), + exp: false, + })) + t.Run("not equal float", run(compareTestCase{ + a: model.NewIntValue(1), + b: model.NewFloatValue(2), + exp: true, + })) + }) + t.Run("float", func(t *testing.T) { + t.Run("equal", run(compareTestCase{ + a: model.NewFloatValue(1.1), + b: model.NewFloatValue(1.1), + exp: false, + })) + t.Run("not equal", run(compareTestCase{ + a: model.NewFloatValue(1.1), + b: model.NewFloatValue(1.2), + exp: true, + })) + t.Run("equal int", run(compareTestCase{ + a: model.NewFloatValue(1), + b: model.NewIntValue(1), + exp: false, + })) + t.Run("not equal int", run(compareTestCase{ + a: model.NewFloatValue(1.1), + b: model.NewIntValue(1), + exp: true, + })) + }) + t.Run("bool", func(t *testing.T) { + t.Run("equal", run(compareTestCase{ + a: model.NewBoolValue(true), + b: model.NewBoolValue(true), + exp: false, + })) + t.Run("not equal", run(compareTestCase{ + a: model.NewBoolValue(true), + b: model.NewBoolValue(false), + exp: true, + })) + }) + t.Run("map", func(t *testing.T) { + t.Run("equal", run(compareTestCase{ + a: model.NewValue(map[string]interface{}{ + "hello": "world", + }), + b: model.NewValue(map[string]interface{}{ + "hello": "world", + }), + exp: false, + })) + t.Run("not equal", run(compareTestCase{ + a: model.NewValue(map[string]interface{}{ + "hello": "world", + }), + b: model.NewValue(map[string]interface{}{ + "hello": "world2", + }), + exp: true, + })) + }) + t.Run("array", func(t *testing.T) { + t.Run("equal", run(compareTestCase{ + a: model.NewValue([]interface{}{ + "hello", "world", + }), + b: model.NewValue([]interface{}{ + "hello", "world", + }), + exp: false, + })) + t.Run("not equal", run(compareTestCase{ + a: model.NewValue([]interface{}{ + "hello", "world", + }), + b: model.NewValue([]interface{}{ + "hello", "world2", + }), + exp: true, + })) + }) + t.Run("null", func(t *testing.T) { + t.Run("equal", run(compareTestCase{ + a: model.NewValue(nil), + b: model.NewValue(nil), + exp: false, + })) + }) +} + +func TestValue_LessThan(t *testing.T) { + run := func(tc compareTestCase) func(t *testing.T) { + return func(t *testing.T) { + got, err := tc.a.LessThan(tc.b) + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + gotBool, err := got.BoolValue() + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if gotBool != tc.exp { + t.Errorf("expected %v, got %v", tc.exp, got) + } + } + } + t.Run("int", func(t *testing.T) { + t.Run("less", run(compareTestCase{ + a: model.NewIntValue(1), + b: model.NewIntValue(2), + exp: true, + })) + t.Run("greater", run(compareTestCase{ + a: model.NewIntValue(2), + b: model.NewIntValue(1), + exp: false, + })) + t.Run("equal", run(compareTestCase{ + a: model.NewIntValue(1), + b: model.NewIntValue(1), + exp: false, + })) + }) + t.Run("float", func(t *testing.T) { + t.Run("less", run(compareTestCase{ + a: model.NewFloatValue(1.1), + b: model.NewFloatValue(1.2), + exp: true, + })) + t.Run("greater", run(compareTestCase{ + a: model.NewFloatValue(1.2), + b: model.NewFloatValue(1.1), + exp: false, + })) + t.Run("equal", run(compareTestCase{ + a: model.NewFloatValue(1.1), + b: model.NewFloatValue(1.1), + exp: false, + })) + }) + t.Run("int float", func(t *testing.T) { + t.Run("less", run(compareTestCase{ + a: model.NewIntValue(1), + b: model.NewFloatValue(2), + exp: true, + })) + t.Run("greater", run(compareTestCase{ + a: model.NewIntValue(2), + b: model.NewFloatValue(1), + exp: false, + })) + t.Run("equal", run(compareTestCase{ + a: model.NewIntValue(1), + b: model.NewFloatValue(1), + exp: false, + })) + }) + t.Run("float int", func(t *testing.T) { + t.Run("less", run(compareTestCase{ + a: model.NewFloatValue(1.1), + b: model.NewIntValue(2), + exp: true, + })) + t.Run("greater", run(compareTestCase{ + a: model.NewFloatValue(2), + b: model.NewIntValue(1), + exp: false, + })) + t.Run("equal", run(compareTestCase{ + a: model.NewFloatValue(1), + b: model.NewIntValue(1), + exp: false, + })) + }) + t.Run("string", func(t *testing.T) { + t.Run("less", run(compareTestCase{ + a: model.NewStringValue("a"), + b: model.NewStringValue("b"), + exp: true, + })) + t.Run("greater", run(compareTestCase{ + a: model.NewStringValue("b"), + b: model.NewStringValue("a"), + exp: false, + })) + t.Run("equal", run(compareTestCase{ + a: model.NewStringValue("a"), + b: model.NewStringValue("a"), + exp: false, + })) + }) +} + +func TestValue_LessThanOrEqual(t *testing.T) { + run := func(tc compareTestCase) func(t *testing.T) { + return func(t *testing.T) { + got, err := tc.a.LessThanOrEqual(tc.b) + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + gotBool, err := got.BoolValue() + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if gotBool != tc.exp { + t.Errorf("expected %v, got %v", tc.exp, got) + } + } + } + t.Run("int", func(t *testing.T) { + t.Run("less", run(compareTestCase{ + a: model.NewIntValue(1), + b: model.NewIntValue(2), + exp: true, + })) + t.Run("greater", run(compareTestCase{ + a: model.NewIntValue(2), + b: model.NewIntValue(1), + exp: false, + })) + t.Run("equal", run(compareTestCase{ + a: model.NewIntValue(1), + b: model.NewIntValue(1), + exp: true, + })) + }) + t.Run("float", func(t *testing.T) { + t.Run("less", run(compareTestCase{ + a: model.NewFloatValue(1.1), + b: model.NewFloatValue(1.2), + exp: true, + })) + t.Run("greater", run(compareTestCase{ + a: model.NewFloatValue(1.2), + b: model.NewFloatValue(1.1), + exp: false, + })) + t.Run("equal", run(compareTestCase{ + a: model.NewFloatValue(1.1), + b: model.NewFloatValue(1.1), + exp: true, + })) + }) + t.Run("int float", func(t *testing.T) { + t.Run("less", run(compareTestCase{ + a: model.NewIntValue(1), + b: model.NewFloatValue(2), + exp: true, + })) + t.Run("greater", run(compareTestCase{ + a: model.NewIntValue(2), + b: model.NewFloatValue(1), + exp: false, + })) + t.Run("equal", run(compareTestCase{ + a: model.NewIntValue(1), + b: model.NewFloatValue(1), + exp: true, + })) + }) + t.Run("float int", func(t *testing.T) { + t.Run("less", run(compareTestCase{ + a: model.NewFloatValue(1.1), + b: model.NewIntValue(2), + exp: true, + })) + t.Run("greater", run(compareTestCase{ + a: model.NewFloatValue(2), + b: model.NewIntValue(1), + exp: false, + })) + t.Run("equal", run(compareTestCase{ + a: model.NewFloatValue(1), + b: model.NewIntValue(1), + exp: true, + })) + }) + t.Run("string", func(t *testing.T) { + t.Run("less", run(compareTestCase{ + a: model.NewStringValue("a"), + b: model.NewStringValue("b"), + exp: true, + })) + t.Run("greater", run(compareTestCase{ + a: model.NewStringValue("b"), + b: model.NewStringValue("a"), + exp: false, + })) + t.Run("equal", run(compareTestCase{ + a: model.NewStringValue("a"), + b: model.NewStringValue("a"), + exp: true, + })) + }) +} + +func TestValue_GreaterThan(t *testing.T) { + run := func(tc compareTestCase) func(t *testing.T) { + return func(t *testing.T) { + got, err := tc.a.GreaterThan(tc.b) + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + gotBool, err := got.BoolValue() + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if gotBool != tc.exp { + t.Errorf("expected %v, got %v", tc.exp, got) + } + } + } + t.Run("int", func(t *testing.T) { + t.Run("less", run(compareTestCase{ + a: model.NewIntValue(1), + b: model.NewIntValue(2), + exp: false, + })) + t.Run("greater", run(compareTestCase{ + a: model.NewIntValue(2), + b: model.NewIntValue(1), + exp: true, + })) + t.Run("equal", run(compareTestCase{ + a: model.NewIntValue(1), + b: model.NewIntValue(1), + exp: false, + })) + }) + t.Run("float", func(t *testing.T) { + t.Run("less", run(compareTestCase{ + a: model.NewFloatValue(1.1), + b: model.NewFloatValue(1.2), + exp: false, + })) + t.Run("greater", run(compareTestCase{ + a: model.NewFloatValue(1.2), + b: model.NewFloatValue(1.1), + exp: true, + })) + t.Run("equal", run(compareTestCase{ + a: model.NewFloatValue(1.1), + b: model.NewFloatValue(1.1), + exp: false, + })) + }) + t.Run("int float", func(t *testing.T) { + t.Run("less", run(compareTestCase{ + a: model.NewIntValue(1), + b: model.NewFloatValue(2), + exp: false, + })) + t.Run("greater", run(compareTestCase{ + a: model.NewIntValue(2), + b: model.NewFloatValue(1), + exp: true, + })) + t.Run("equal", run(compareTestCase{ + a: model.NewIntValue(1), + b: model.NewFloatValue(1), + exp: false, + })) + }) + t.Run("float int", func(t *testing.T) { + t.Run("less", run(compareTestCase{ + a: model.NewFloatValue(1.1), + b: model.NewIntValue(2), + exp: false, + })) + t.Run("greater", run(compareTestCase{ + a: model.NewFloatValue(2), + b: model.NewIntValue(1), + exp: true, + })) + t.Run("equal", run(compareTestCase{ + a: model.NewFloatValue(1), + b: model.NewIntValue(1), + exp: false, + })) + }) + t.Run("string", func(t *testing.T) { + t.Run("less", run(compareTestCase{ + a: model.NewStringValue("a"), + b: model.NewStringValue("b"), + exp: false, + })) + t.Run("greater", run(compareTestCase{ + a: model.NewStringValue("b"), + b: model.NewStringValue("a"), + exp: true, + })) + t.Run("equal", run(compareTestCase{ + a: model.NewStringValue("a"), + b: model.NewStringValue("a"), + exp: false, + })) + }) +} + +func TestValue_GreaterThanOrEqual(t *testing.T) { + run := func(tc compareTestCase) func(t *testing.T) { + return func(t *testing.T) { + got, err := tc.a.GreaterThanOrEqual(tc.b) + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + gotBool, err := got.BoolValue() + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if gotBool != tc.exp { + t.Errorf("expected %v, got %v", tc.exp, got) + } + } + } + t.Run("int", func(t *testing.T) { + t.Run("less", run(compareTestCase{ + a: model.NewIntValue(1), + b: model.NewIntValue(2), + exp: false, + })) + t.Run("greater", run(compareTestCase{ + a: model.NewIntValue(2), + b: model.NewIntValue(1), + exp: true, + })) + t.Run("equal", run(compareTestCase{ + a: model.NewIntValue(1), + b: model.NewIntValue(1), + exp: true, + })) + }) + t.Run("float", func(t *testing.T) { + t.Run("less", run(compareTestCase{ + a: model.NewFloatValue(1.1), + b: model.NewFloatValue(1.2), + exp: false, + })) + t.Run("greater", run(compareTestCase{ + a: model.NewFloatValue(1.2), + b: model.NewFloatValue(1.1), + exp: true, + })) + t.Run("equal", run(compareTestCase{ + a: model.NewFloatValue(1.1), + b: model.NewFloatValue(1.1), + exp: true, + })) + }) + t.Run("int float", func(t *testing.T) { + t.Run("less", run(compareTestCase{ + a: model.NewIntValue(1), + b: model.NewFloatValue(2), + exp: false, + })) + t.Run("greater", run(compareTestCase{ + a: model.NewIntValue(2), + b: model.NewFloatValue(1), + exp: true, + })) + t.Run("equal", run(compareTestCase{ + a: model.NewIntValue(1), + b: model.NewFloatValue(1), + exp: true, + })) + }) + t.Run("float int", func(t *testing.T) { + t.Run("less", run(compareTestCase{ + a: model.NewFloatValue(1.1), + b: model.NewIntValue(2), + exp: false, + })) + t.Run("greater", run(compareTestCase{ + a: model.NewFloatValue(2), + b: model.NewIntValue(1), + exp: true, + })) + t.Run("equal", run(compareTestCase{ + a: model.NewFloatValue(1), + b: model.NewIntValue(1), + exp: true, + })) + }) + t.Run("string", func(t *testing.T) { + t.Run("less", run(compareTestCase{ + a: model.NewStringValue("a"), + b: model.NewStringValue("b"), + exp: false, + })) + t.Run("greater", run(compareTestCase{ + a: model.NewStringValue("b"), + b: model.NewStringValue("a"), + exp: true, + })) + t.Run("equal", run(compareTestCase{ + a: model.NewStringValue("a"), + b: model.NewStringValue("a"), + exp: true, + })) + }) +} + +func TestValue_Compare(t *testing.T) { + run := func(a *model.Value, b *model.Value, exp int) func(t *testing.T) { + return func(t *testing.T) { + got, err := a.Compare(b) + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if got != exp { + t.Errorf("expected %d, got %d", exp, got) + } + } + } + t.Run("int", func(t *testing.T) { + t.Run("less", run( + model.NewIntValue(1), + model.NewIntValue(2), + -1, + )) + t.Run("greater", run( + model.NewIntValue(2), + model.NewIntValue(1), + 1, + )) + t.Run("equal", run( + model.NewIntValue(1), + model.NewIntValue(1), + 0, + )) + }) + t.Run("float", func(t *testing.T) { + t.Run("less", run( + model.NewFloatValue(1.1), + model.NewFloatValue(1.2), + -1, + )) + t.Run("greater", run( + model.NewFloatValue(1.2), + model.NewFloatValue(1.1), + 1, + )) + t.Run("equal", run( + model.NewFloatValue(1.1), + model.NewFloatValue(1.1), + 0, + )) + }) + t.Run("int float", func(t *testing.T) { + t.Run("less", run( + model.NewIntValue(1), + model.NewFloatValue(2), + -1, + )) + t.Run("greater", run( + model.NewIntValue(2), + model.NewFloatValue(1), + 1, + )) + t.Run("equal", run( + model.NewIntValue(1), + model.NewFloatValue(1), + 0, + )) + }) + t.Run("float int", func(t *testing.T) { + t.Run("less", run( + model.NewFloatValue(1.1), + model.NewIntValue(2), + -1, + )) + t.Run("greater", run( + model.NewFloatValue(1.1), + model.NewIntValue(1), + 1, + )) + t.Run("equal", run( + model.NewFloatValue(1), + model.NewIntValue(1), + 0, + )) + }) + t.Run("string", func(t *testing.T) { + t.Run("less", run( + model.NewStringValue("a"), + model.NewStringValue("b"), + -1, + )) + t.Run("greater", run( + model.NewStringValue("b"), + model.NewStringValue("a"), + 1, + )) + t.Run("equal", run( + model.NewStringValue("a"), + model.NewStringValue("a"), + 0, + )) + }) +} diff --git a/model/value_literal.go b/model/value_literal.go index 096a658b..7b7d079e 100644 --- a/model/value_literal.go +++ b/model/value_literal.go @@ -1,7 +1,6 @@ package model import ( - "fmt" "reflect" "slices" ) @@ -44,7 +43,10 @@ func (v *Value) isString() bool { func (v *Value) StringValue() (string, error) { unpacked := v.UnpackKinds(reflect.Ptr, reflect.Interface) if !unpacked.isString() { - return "", fmt.Errorf("expected string, got %s", unpacked.Type()) + return "", ErrUnexpectedType{ + Expected: TypeString, + Actual: v.Type(), + } } return unpacked.Value.String(), nil } @@ -113,7 +115,10 @@ func (v *Value) isInt() bool { func (v *Value) IntValue() (int64, error) { unpacked := v.UnpackKinds(reflect.Ptr, reflect.Interface) if !unpacked.isInt() { - return 0, fmt.Errorf("expected int, got %s", unpacked.Type()) + return 0, ErrUnexpectedType{ + Expected: TypeInt, + Actual: v.Type(), + } } return unpacked.Value.Int(), nil } @@ -138,7 +143,10 @@ func (v *Value) isFloat() bool { func (v *Value) FloatValue() (float64, error) { unpacked := v.UnpackKinds(reflect.Ptr, reflect.Interface) if !unpacked.IsFloat() { - return 0, fmt.Errorf("expected float, got %s", unpacked.Type()) + return 0, ErrUnexpectedType{ + Expected: TypeFloat, + Actual: v.Type(), + } } return unpacked.Value.Float(), nil } @@ -163,7 +171,10 @@ func (v *Value) isBool() bool { func (v *Value) BoolValue() (bool, error) { unpacked := v.UnpackKinds(reflect.Ptr, reflect.Interface) if !unpacked.IsBool() { - return false, fmt.Errorf("expected bool, got %s", unpacked.Type()) + return false, ErrUnexpectedType{ + Expected: TypeBool, + Actual: v.Type(), + } } return unpacked.Value.Bool(), nil } diff --git a/model/value_map.go b/model/value_map.go index 1c9bb037..67976cb0 100644 --- a/model/value_map.go +++ b/model/value_map.go @@ -96,7 +96,10 @@ func (v *Value) GetMapKey(key string) (*Value, error) { } return res, nil default: - return nil, fmt.Errorf("value is not a map") + return nil, ErrUnexpectedType{ + Expected: TypeMap, + Actual: v.Type(), + } } } @@ -118,7 +121,10 @@ func (v *Value) DeleteMapKey(key string) error { unpacked.Value.SetMapIndex(reflect.ValueOf(key), reflect.Value{}) return nil default: - return fmt.Errorf("value is not a map") + return ErrUnexpectedType{ + Expected: TypeMap, + Actual: v.Type(), + } } } @@ -143,7 +149,10 @@ func (v *Value) MapKeys() ([]string, error) { } return strKeys, nil default: - return nil, fmt.Errorf("value is not a map") + return nil, ErrUnexpectedType{ + Expected: TypeMap, + Actual: v.Type(), + } } } @@ -192,11 +201,6 @@ func (v *Value) MapKeyValues() ([]KeyValue, error) { // MapLen returns the length of the slice. func (v *Value) MapLen() (int, error) { - if !v.IsMap() { - return 0, fmt.Errorf("expected map, got %s", v.Type()) - } - // There will be more efficient ways of doing this, but this - // accounts for maps, dencoding maps and structs. keys, err := v.MapKeys() if err != nil { return 0, err diff --git a/model/value_math.go b/model/value_math.go index eb492677..2f6adbb5 100644 --- a/model/value_math.go +++ b/model/value_math.go @@ -4,6 +4,7 @@ import ( "math" ) +// Add adds two values together. func (v *Value) Add(other *Value) (*Value, error) { if v.IsInt() && other.IsInt() { a, err := v.IntValue() @@ -63,6 +64,7 @@ func (v *Value) Add(other *Value) (*Value, error) { return nil, &ErrIncompatibleTypes{A: v, B: other} } +// Subtract returns the difference between two values. func (v *Value) Subtract(other *Value) (*Value, error) { if v.IsInt() && other.IsInt() { a, err := v.IntValue() @@ -111,6 +113,7 @@ func (v *Value) Subtract(other *Value) (*Value, error) { return nil, &ErrIncompatibleTypes{A: v, B: other} } +// Multiply returns the product of the two values. func (v *Value) Multiply(other *Value) (*Value, error) { if v.IsInt() && other.IsInt() { a, err := v.IntValue() @@ -159,6 +162,7 @@ func (v *Value) Multiply(other *Value) (*Value, error) { return nil, &ErrIncompatibleTypes{A: v, B: other} } +// Divide returns the result of dividing the value by another value. func (v *Value) Divide(other *Value) (*Value, error) { if v.IsInt() && other.IsInt() { a, err := v.IntValue() @@ -207,6 +211,7 @@ func (v *Value) Divide(other *Value) (*Value, error) { return nil, &ErrIncompatibleTypes{A: v, B: other} } +// Modulo returns the remainder of the division of two values. func (v *Value) Modulo(other *Value) (*Value, error) { if v.IsInt() && other.IsInt() { a, err := v.IntValue() diff --git a/model/value_math_test.go b/model/value_math_test.go new file mode 100644 index 00000000..f6b80e54 --- /dev/null +++ b/model/value_math_test.go @@ -0,0 +1,150 @@ +package model_test + +import ( + "testing" + + "github.com/tomwright/dasel/v3/model" +) + +func TestValue_Add(t *testing.T) { + run := func(a, b *model.Value, exp *model.Value) func(*testing.T) { + return func(t *testing.T) { + got, err := a.Add(b) + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + eq, err := got.EqualTypeValue(exp) + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if !eq { + t.Errorf("expected %v, got %v", exp, got) + } + } + } + t.Run("int", func(t *testing.T) { + t.Run("int", run(model.NewIntValue(1), model.NewIntValue(2), model.NewIntValue(3))) + t.Run("float", run(model.NewIntValue(1), model.NewFloatValue(2), model.NewFloatValue(3))) + }) + t.Run("float", func(t *testing.T) { + t.Run("int", run(model.NewFloatValue(1), model.NewIntValue(2), model.NewFloatValue(3))) + t.Run("float", run(model.NewFloatValue(1), model.NewFloatValue(2), model.NewFloatValue(3))) + }) + t.Run("string", func(t *testing.T) { + t.Run("string", run(model.NewStringValue("hello"), model.NewStringValue(" world"), model.NewStringValue("hello world"))) + }) +} + +func TestValue_Subtract(t *testing.T) { + run := func(a, b *model.Value, exp *model.Value) func(*testing.T) { + return func(t *testing.T) { + got, err := a.Subtract(b) + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + eq, err := got.EqualTypeValue(exp) + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if !eq { + t.Errorf("expected %v, got %v", exp, got) + } + } + } + t.Run("int", func(t *testing.T) { + t.Run("int", run(model.NewIntValue(3), model.NewIntValue(2), model.NewIntValue(1))) + t.Run("float", run(model.NewIntValue(3), model.NewFloatValue(2), model.NewFloatValue(1))) + }) + t.Run("float", func(t *testing.T) { + t.Run("int", run(model.NewFloatValue(3), model.NewIntValue(2), model.NewFloatValue(1))) + t.Run("float", run(model.NewFloatValue(3), model.NewFloatValue(2), model.NewFloatValue(1))) + }) +} + +func TestValue_Multiply(t *testing.T) { + run := func(a, b *model.Value, exp *model.Value) func(*testing.T) { + return func(t *testing.T) { + got, err := a.Multiply(b) + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + eq, err := got.EqualTypeValue(exp) + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if !eq { + t.Errorf("expected %v, got %v", exp, got) + } + } + } + t.Run("int", func(t *testing.T) { + t.Run("int", run(model.NewIntValue(3), model.NewIntValue(2), model.NewIntValue(6))) + t.Run("float", run(model.NewIntValue(3), model.NewFloatValue(2), model.NewFloatValue(6))) + }) + t.Run("float", func(t *testing.T) { + t.Run("int", run(model.NewFloatValue(3), model.NewIntValue(2), model.NewFloatValue(6))) + t.Run("float", run(model.NewFloatValue(3), model.NewFloatValue(2), model.NewFloatValue(6))) + }) +} + +func TestValue_Divide(t *testing.T) { + run := func(a, b *model.Value, exp *model.Value) func(*testing.T) { + return func(t *testing.T) { + got, err := a.Divide(b) + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + eq, err := got.EqualTypeValue(exp) + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if !eq { + t.Errorf("expected %v, got %v", exp, got) + } + } + } + t.Run("int", func(t *testing.T) { + t.Run("int", run(model.NewIntValue(6), model.NewIntValue(2), model.NewIntValue(3))) + t.Run("float", run(model.NewIntValue(6), model.NewFloatValue(2), model.NewFloatValue(3))) + }) + t.Run("float", func(t *testing.T) { + t.Run("int", run(model.NewFloatValue(6), model.NewIntValue(2), model.NewFloatValue(3))) + t.Run("float", run(model.NewFloatValue(6), model.NewFloatValue(2), model.NewFloatValue(3))) + }) +} + +func TestValue_Modulo(t *testing.T) { + run := func(a, b *model.Value, exp *model.Value) func(*testing.T) { + return func(t *testing.T) { + got, err := a.Modulo(b) + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + eq, err := got.EqualTypeValue(exp) + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if !eq { + t.Errorf("expected %v, got %v", exp, got) + } + } + } + t.Run("int", func(t *testing.T) { + t.Run("int", run(model.NewIntValue(10), model.NewIntValue(3), model.NewIntValue(1))) + t.Run("float", run(model.NewIntValue(10), model.NewFloatValue(3), model.NewFloatValue(1))) + }) + t.Run("float", func(t *testing.T) { + t.Run("int", run(model.NewFloatValue(10), model.NewIntValue(3), model.NewFloatValue(1))) + t.Run("float", run(model.NewFloatValue(10), model.NewFloatValue(3), model.NewFloatValue(1))) + }) +} diff --git a/model/value_metadata.go b/model/value_metadata.go index ad36f2a4..1347bd0a 100644 --- a/model/value_metadata.go +++ b/model/value_metadata.go @@ -23,15 +23,12 @@ func (v *Value) IsSpread() bool { if v == nil { return false } - val, ok := v.Metadata["spread"] + val, ok := v.MetadataValue("spread") if !ok { return false } spread, ok := val.(bool) - if !ok { - return false - } - return spread + return ok && spread } // MarkAsSpread marks the value as a spread value. @@ -45,15 +42,12 @@ func (v *Value) IsBranch() bool { if v == nil { return false } - val, ok := v.Metadata["spread"] - if !ok { - return false - } - spread, ok := val.(bool) + val, ok := v.MetadataValue("branch") if !ok { return false } - return spread + branch, ok := val.(bool) + return ok && branch } // MarkAsBranch marks the value as a branch value. diff --git a/model/value_metadata_test.go b/model/value_metadata_test.go new file mode 100644 index 00000000..64e0e610 --- /dev/null +++ b/model/value_metadata_test.go @@ -0,0 +1,29 @@ +package model_test + +import ( + "testing" + + "github.com/tomwright/dasel/v3/model" +) + +func TestValue_IsBranch(t *testing.T) { + val := model.NewNullValue() + if exp, got := false, val.IsBranch(); exp != got { + t.Errorf("expected %v, got %v", exp, got) + } + val.MarkAsBranch() + if exp, got := true, val.IsBranch(); exp != got { + t.Errorf("expected %v, got %v", exp, got) + } +} + +func TestValue_IsSpread(t *testing.T) { + val := model.NewNullValue() + if exp, got := false, val.IsSpread(); exp != got { + t.Errorf("expected %v, got %v", exp, got) + } + val.MarkAsSpread() + if exp, got := true, val.IsSpread(); exp != got { + t.Errorf("expected %v, got %v", exp, got) + } +} diff --git a/model/value_set.go b/model/value_set.go index 2de052e6..456fa44d 100644 --- a/model/value_set.go +++ b/model/value_set.go @@ -5,6 +5,7 @@ import ( "reflect" ) +// Set sets the value of the value. func (v *Value) Set(newValue *Value) error { if v.setFn != nil { return v.setFn(newValue) diff --git a/model/value_slice.go b/model/value_slice.go index e2d10c5a..59cfc5a6 100644 --- a/model/value_slice.go +++ b/model/value_slice.go @@ -28,7 +28,10 @@ func (v *Value) isSlice() bool { func (v *Value) Append(val *Value) error { unpacked := v.UnpackKinds(reflect.Interface, reflect.Ptr) if !unpacked.isSlice() { - return fmt.Errorf("expected slice, got %s", v.Type()) + return ErrUnexpectedType{ + Expected: TypeSlice, + Actual: v.Type(), + } } newVal := reflect.Append(unpacked.Value, val.Value) unpacked.Value.Set(newVal) @@ -39,7 +42,10 @@ func (v *Value) Append(val *Value) error { func (v *Value) SliceLen() (int, error) { unpacked := v.UnpackKinds(reflect.Interface, reflect.Ptr) if !unpacked.isSlice() { - return 0, fmt.Errorf("expected slice, got %s", v.Type()) + return 0, ErrUnexpectedType{ + Expected: TypeSlice, + Actual: v.Type(), + } } return unpacked.Value.Len(), nil } @@ -48,10 +54,13 @@ func (v *Value) SliceLen() (int, error) { func (v *Value) GetSliceIndex(i int) (*Value, error) { unpacked := v.UnpackKinds(reflect.Interface, reflect.Ptr) if !unpacked.isSlice() { - return nil, fmt.Errorf("expected slice, got %s", v.Type()) + return nil, ErrUnexpectedType{ + Expected: TypeSlice, + Actual: v.Type(), + } } if i < 0 || i >= unpacked.Value.Len() { - return nil, fmt.Errorf("index out of range: %d", i) + return nil, SliceIndexOutOfRange{Index: i} } res := NewValue(unpacked.Value.Index(i)) return res, nil @@ -61,10 +70,13 @@ func (v *Value) GetSliceIndex(i int) (*Value, error) { func (v *Value) SetSliceIndex(i int, val *Value) error { unpacked := v.UnpackKinds(reflect.Interface, reflect.Ptr) if !unpacked.isSlice() { - return fmt.Errorf("expected slice, got %s", v.Type()) + return ErrUnexpectedType{ + Expected: TypeSlice, + Actual: v.Type(), + } } if i < 0 || i >= unpacked.Value.Len() { - return fmt.Errorf("index out of range: %d", i) + return SliceIndexOutOfRange{Index: i} } unpacked.Value.Index(i).Set(val.Value) return nil diff --git a/model/value_test.go b/model/value_test.go new file mode 100644 index 00000000..322fce56 --- /dev/null +++ b/model/value_test.go @@ -0,0 +1,53 @@ +package model_test + +import ( + "testing" + + "github.com/tomwright/dasel/v3/model" +) + +func TestType_String(t *testing.T) { + run := func(ty model.Type, exp string) func(*testing.T) { + return func(t *testing.T) { + got := ty.String() + if got != exp { + t.Errorf("expected %s, got %s", exp, got) + } + } + } + t.Run("string", run(model.TypeString, "string")) + t.Run("int", run(model.TypeInt, "int")) + t.Run("float", run(model.TypeFloat, "float")) + t.Run("bool", run(model.TypeBool, "bool")) + t.Run("map", run(model.TypeMap, "map")) + t.Run("slice", run(model.TypeSlice, "array")) + t.Run("slice", run(model.TypeUnknown, "unknown")) + t.Run("slice", run(model.TypeNull, "null")) +} + +func TestValue_Len(t *testing.T) { + run := func(v *model.Value, exp int) func(*testing.T) { + return func(t *testing.T) { + got, err := v.Len() + if err != nil { + t.Errorf("unexpected error: %s", err) + return + } + if got != exp { + t.Errorf("expected %d, got %d", exp, got) + } + } + } + t.Run("string", func(t *testing.T) { + t.Run("empty", run(model.NewStringValue(""), 0)) + t.Run("non-empty", run(model.NewStringValue("hello"), 5)) + }) + t.Run("slice", func(t *testing.T) { + t.Run("empty", run(model.NewSliceValue(), 0)) + t.Run("non-empty", run(model.NewValue([]any{1, 2, 3}), 3)) + }) + t.Run("map", func(t *testing.T) { + t.Run("empty", run(model.NewMapValue(), 0)) + t.Run("non-empty", run(model.NewValue(map[string]any{"one": 1, "two": 2, "three": 3}), 3)) + }) +} diff --git a/model/values.go b/model/values.go deleted file mode 100644 index 2c485577..00000000 --- a/model/values.go +++ /dev/null @@ -1,3 +0,0 @@ -package model - -type Values []*Value diff --git a/selector/ast/expression_literal.go b/selector/ast/expression_literal.go index bb116efa..128d5d9a 100644 --- a/selector/ast/expression_literal.go +++ b/selector/ast/expression_literal.go @@ -31,3 +31,7 @@ type RegexExpr struct { } func (RegexExpr) expr() {} + +type NullExpr struct{} + +func (NullExpr) expr() {} diff --git a/selector/parser/parser.go b/selector/parser/parser.go index daea7c8b..a500d851 100644 --- a/selector/parser/parser.go +++ b/selector/parser/parser.go @@ -85,6 +85,25 @@ func (p *Parser) Parse() (ast.Expr, error) { } func (p *Parser) parseExpression(bp bindingPower) (left ast.Expr, err error) { + if p.hasToken() && slices.Contains(rightDenotationTokens, p.current().Kind) { + unary := ast.UnaryExpr{ + Operator: p.current(), + Right: nil, + } + p.advance() + expr, err := p.parseExpression(getTokenBindingPower(unary.Operator.Kind)) + if err != nil { + return nil, err + } + p.advance() + unary.Right = expr + left = unary + } + + if !p.hasToken() { + return + } + switch p.current().Kind { case lexer.String: left, err = parseStringLiteral(p) @@ -116,6 +135,10 @@ func (p *Parser) parseExpression(bp bindingPower) (left ast.Expr, err error) { left, err = parseRegexPattern(p) case lexer.SortBy: left, err = parseSortBy(p) + case lexer.Null: + left = ast.NullExpr{} + err = nil + p.advance() default: return nil, &UnexpectedTokenError{ Token: p.current(), From 9dbe990894ec1eedd8e306a1aebbfabce469ec8e Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Thu, 24 Oct 2024 19:32:15 +0100 Subject: [PATCH 37/56] Migrate to kong, make parsers extensible, add tests --- cmd/dasel/main.go | 10 +-- execution/execute_binary.go | 2 +- execution/func_to_string.go | 5 ++ go.mod | 16 +--- go.sum | 26 +----- internal/cli/command.go | 56 ++++++++++++ internal/cli/command_test.go | 111 +++++++++++++++++++++++ internal/cli/generic_test.go | 53 +++++------ internal/cli/man.go | 30 ------- internal/cli/man_test.go | 44 --------- internal/cli/query.go | 95 ++++++++++++++++++++ internal/cli/root.go | 155 -------------------------------- internal/cli/root_test.go | 60 ------------- internal/cli/variable.go | 80 +++++++++++++++++ model/value_set.go | 46 ++++++---- parsing/d/reader.go | 38 ++++++++ parsing/format.go | 71 ++++++--------- parsing/{ => json}/json.go | 27 ++++-- parsing/{ => json}/json_test.go | 7 +- parsing/reader.go | 19 ++++ parsing/{ => toml}/toml.go | 22 +++-- parsing/writer.go | 32 +++++++ parsing/{ => yaml}/yaml.go | 21 +++-- 23 files changed, 590 insertions(+), 436 deletions(-) create mode 100644 internal/cli/command.go create mode 100644 internal/cli/command_test.go delete mode 100644 internal/cli/man.go delete mode 100644 internal/cli/man_test.go create mode 100644 internal/cli/query.go delete mode 100644 internal/cli/root.go delete mode 100644 internal/cli/root_test.go create mode 100644 internal/cli/variable.go create mode 100644 parsing/d/reader.go rename parsing/{ => json}/json.go (93%) rename parsing/{ => json}/json_test.go (80%) create mode 100644 parsing/reader.go rename parsing/{ => toml}/toml.go (56%) create mode 100644 parsing/writer.go rename parsing/{ => yaml}/yaml.go (62%) diff --git a/cmd/dasel/main.go b/cmd/dasel/main.go index 7a564ae4..651331a0 100644 --- a/cmd/dasel/main.go +++ b/cmd/dasel/main.go @@ -4,12 +4,12 @@ import ( "os" "github.com/tomwright/dasel/v3/internal/cli" + _ "github.com/tomwright/dasel/v3/parsing/d" + _ "github.com/tomwright/dasel/v3/parsing/json" + _ "github.com/tomwright/dasel/v3/parsing/toml" + _ "github.com/tomwright/dasel/v3/parsing/yaml" ) func main() { - cmd := cli.RootCmd() - if err := cmd.Execute(); err != nil { - cmd.PrintErrln("Error:", err.Error()) - os.Exit(1) - } + cli.MustRun(os.Stdin, os.Stdout, os.Stderr) } diff --git a/execution/execute_binary.go b/execution/execute_binary.go index 3058857e..18c88ae7 100644 --- a/execution/execute_binary.go +++ b/execution/execute_binary.go @@ -44,7 +44,7 @@ func binaryExprExecutor(opts *Options, e ast.BinaryExpr) (expressionExecutor, er return left.NotEqual(right) case lexer.Equals: err := left.Set(right) - return left, err + return right, err case lexer.And: leftBool, err := left.BoolValue() if err != nil { diff --git a/execution/func_to_string.go b/execution/func_to_string.go index ade65b83..0f60f85b 100644 --- a/execution/func_to_string.go +++ b/execution/func_to_string.go @@ -12,6 +12,11 @@ var FuncToString = NewFunc( func(data *model.Value, args model.Values) (*model.Value, error) { switch args[0].Type() { case model.TypeString: + stringValue, err := args[0].StringValue() + if err != nil { + return nil, err + } + model.NewStringValue(stringValue) return args[0], nil case model.TypeInt: i, err := args[0].IntValue() diff --git a/go.mod b/go.mod index 9fe9ae68..9fe5a7ce 100644 --- a/go.mod +++ b/go.mod @@ -3,20 +3,8 @@ module github.com/tomwright/dasel/v3 go 1.23 require ( - github.com/alecthomas/chroma/v2 v2.14.0 - github.com/clbanning/mxj/v2 v2.7.0 + github.com/alecthomas/kong v1.2.1 + github.com/google/go-cmp v0.5.9 github.com/pelletier/go-toml/v2 v2.2.2 - github.com/spf13/cobra v1.8.1 - golang.org/x/net v0.29.0 - golang.org/x/text v0.18.0 gopkg.in/yaml.v3 v3.0.1 ) - -require ( - github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect - github.com/dlclark/regexp2 v1.11.0 // indirect - github.com/google/go-cmp v0.5.9 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect -) diff --git a/go.sum b/go.sum index 8e2fe7d6..fbd7fce0 100644 --- a/go.sum +++ b/go.sum @@ -1,34 +1,20 @@ -github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= -github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= -github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY= +github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/kong v1.2.1 h1:E8jH4Tsgv6wCRX2nGrdPyHDUCSG83WH2qE4XLACD33Q= +github.com/alecthomas/kong v1.2.1/go.mod h1:rKTSFhbdp3Ryefn8x5MOEprnRFQ7nlmMC01GKhehhBM= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= -github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= -github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= -github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= -github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -38,10 +24,6 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/cli/command.go b/internal/cli/command.go new file mode 100644 index 00000000..cdb977a0 --- /dev/null +++ b/internal/cli/command.go @@ -0,0 +1,56 @@ +package cli + +import ( + "io" + "reflect" + + "github.com/alecthomas/kong" + "github.com/tomwright/dasel/v3/internal" +) + +type Globals struct { + Stdin io.Reader `kong:"-"` + Stdout io.Writer `kong:"-"` + Stderr io.Writer `kong:"-"` +} + +type CLI struct { + Globals + + Query QueryCmd `cmd:"" help:"Execute a query"` +} + +func MustRun(stdin io.Reader, stdout, stderr io.Writer) { + ctx, err := Run(stdin, stdout, stderr) + ctx.FatalIfErrorf(err) +} + +func Run(stdin io.Reader, stdout, stderr io.Writer) (*kong.Context, error) { + cli := &CLI{ + Globals: Globals{ + Stdin: stdin, + Stdout: stdout, + Stderr: stderr, + }, + } + + ctx := kong.Parse( + cli, + kong.Name("dasel"), + kong.Description("Query and modify data structures from the command line."), + kong.UsageOnError(), + kong.ConfigureHelp(kong.HelpOptions{Compact: true}), + kong.Vars{ + "version": internal.Version, + }, + kong.Bind(&cli.Globals), + kong.TypeMapper(reflect.TypeFor[*[]variable](), &variableMapper{}), + kong.OptionFunc(func(k *kong.Kong) error { + k.Stdout = cli.Stdout + k.Stderr = cli.Stderr + return nil + }), + ) + err := ctx.Run() + return ctx, err +} diff --git a/internal/cli/command_test.go b/internal/cli/command_test.go new file mode 100644 index 00000000..9cc9c708 --- /dev/null +++ b/internal/cli/command_test.go @@ -0,0 +1,111 @@ +package cli_test + +import ( + "bytes" + "errors" + "os" + "reflect" + "testing" + + "github.com/tomwright/dasel/v3/internal/cli" +) + +func runDasel(args []string, in []byte) ([]byte, []byte, error) { + stdOut := bytes.NewBuffer([]byte{}) + stdErr := bytes.NewBuffer([]byte{}) + stdIn := bytes.NewReader(in) + + originalArgs := os.Args + defer func() { + os.Args = originalArgs + }() + + os.Args = append([]string{"dasel", "query"}, args...) + + _, err := cli.Run(stdIn, stdOut, stdErr) + + return stdOut.Bytes(), stdErr.Bytes(), err +} + +type testCase struct { + args []string + in []byte + stdout []byte + stderr []byte + err error +} + +func runTest(tc testCase) func(t *testing.T) { + return func(t *testing.T) { + if tc.stdout == nil { + tc.stdout = []byte{} + } + if tc.stderr == nil { + tc.stderr = []byte{} + } + + gotStdOut, gotStdErr, gotErr := runDasel(tc.args, tc.in) + if !errors.Is(gotErr, tc.err) && !errors.Is(tc.err, gotErr) { + t.Errorf("expected error %v, got %v", tc.err, gotErr) + return + } + + if !reflect.DeepEqual(tc.stderr, gotStdErr) { + t.Errorf("expected stderr %s, got %s", string(tc.stderr), string(gotStdErr)) + } + + if !reflect.DeepEqual(tc.stdout, gotStdOut) { + t.Errorf("expected stdout %s, got %s", string(tc.stdout), string(gotStdOut)) + } + } +} + +func TestRun(t *testing.T) { + t.Run("complex set", func(t *testing.T) { + t.Run("set nested with spread", runTest(testCase{ + args: []string{"-i", "json", "-o", "json", "--root", `user = {user..., name: {"first": $this.user.name, "last": "Doe"}}`}, + in: []byte(`{"user": {"name": "John"}}`), + stdout: []byte(`{ + "user": { + "name": { + "first": "John", + "last": "Doe" + } + } +} +`), + stderr: nil, + err: nil, + })) + t.Run("set nested", runTest(testCase{ + args: []string{"-i", "json", "-o", "json", "--root", `user.name = {"first": user.name, "last": "Doe"}`}, + in: []byte(`{"user": {"name": "John"}}`), + stdout: []byte(`{ + "user": { + "name": { + "first": "John", + "last": "Doe" + } + } +} +`), + stderr: nil, + err: nil, + })) + t.Run("set nested with localised group", runTest(testCase{ + args: []string{"-i", "json", "-o", "json", "--root", `user.(name = {"first": name, "last": "Doe"})`}, + in: []byte(`{"user": {"name": "John"}}`), + stdout: []byte(`{ + "user": { + "name": { + "first": "John", + "last": "Doe" + } + } +} +`), + stderr: nil, + err: nil, + })) + }) +} diff --git a/internal/cli/generic_test.go b/internal/cli/generic_test.go index cfc26227..9ab63fe4 100644 --- a/internal/cli/generic_test.go +++ b/internal/cli/generic_test.go @@ -6,6 +6,9 @@ import ( "testing" "github.com/tomwright/dasel/v3/parsing" + "github.com/tomwright/dasel/v3/parsing/json" + "github.com/tomwright/dasel/v3/parsing/toml" + "github.com/tomwright/dasel/v3/parsing/yaml" ) func newStringWithFormat(format parsing.Format, data string) bytesWithFormat { @@ -42,7 +45,7 @@ func (tcs testCases) run(t *testing.T) { } args := slices.Clone(tcs.args) - args = append(args, "--input", i.format.String(), "--output", o.format.String()) + args = append(args, "-i", i.format.String(), "-o", o.format.String()) if tcs.selector != "" { args = append(args, tcs.selector) } @@ -57,7 +60,7 @@ func (tcs testCases) run(t *testing.T) { } func TestCrossFormatHappyPath(t *testing.T) { - jsonInputData := newStringWithFormat(parsing.JSON, `{ + jsonInputData := newStringWithFormat(json.JSON, `{ "oneTwoThree": 123, "oneTwoDotThree": 12.3, "hello": "world", @@ -87,7 +90,7 @@ func TestCrossFormatHappyPath(t *testing.T) { } } }`) - yamlInputData := newStringWithFormat(parsing.YAML, `oneTwoThree: 123 + yamlInputData := newStringWithFormat(yaml.YAML, `oneTwoThree: 123 oneTwoDotThree: 12.3 hello: world boolFalse: false @@ -130,7 +133,7 @@ mapData: - 5 `) - tomlInputData := newStringWithFormat(parsing.TOML, ` + tomlInputData := newStringWithFormat(toml.TOML, ` oneTwoThree = 123 oneTwoDotThree = 12.3 hello = 'world' @@ -172,9 +175,9 @@ sliceOfNumbers = [1, 2, 3, 4, 5] tomlInputData, }, out: []bytesWithFormat{ - newStringWithFormat(parsing.JSON, `"world"`), - newStringWithFormat(parsing.YAML, `world`), - newStringWithFormat(parsing.TOML, `'world'`), + newStringWithFormat(json.JSON, `"world"`), + newStringWithFormat(yaml.YAML, `world`), + newStringWithFormat(toml.TOML, `'world'`), }, }.run) t.Run("int", testCases{ @@ -185,9 +188,9 @@ sliceOfNumbers = [1, 2, 3, 4, 5] tomlInputData, }, out: []bytesWithFormat{ - newStringWithFormat(parsing.JSON, `123`), - newStringWithFormat(parsing.YAML, `123`), - newStringWithFormat(parsing.TOML, `123`), + newStringWithFormat(json.JSON, `123`), + newStringWithFormat(yaml.YAML, `123`), + newStringWithFormat(toml.TOML, `123`), }, skip: []string{ // Skipped because the parser outputs as a float. @@ -202,9 +205,9 @@ sliceOfNumbers = [1, 2, 3, 4, 5] tomlInputData, }, out: []bytesWithFormat{ - newStringWithFormat(parsing.JSON, `12.3`), - newStringWithFormat(parsing.YAML, `12.3`), - newStringWithFormat(parsing.TOML, `12.3`), + newStringWithFormat(json.JSON, `12.3`), + newStringWithFormat(yaml.YAML, `12.3`), + newStringWithFormat(toml.TOML, `12.3`), }, }.run) t.Run("bool", func(t *testing.T) { @@ -215,9 +218,9 @@ sliceOfNumbers = [1, 2, 3, 4, 5] yamlInputData, }, out: []bytesWithFormat{ - newStringWithFormat(parsing.JSON, `true`), - newStringWithFormat(parsing.YAML, `true`), - newStringWithFormat(parsing.TOML, `true`), + newStringWithFormat(json.JSON, `true`), + newStringWithFormat(yaml.YAML, `true`), + newStringWithFormat(toml.TOML, `true`), }, }.run) t.Run("false", testCases{ @@ -227,9 +230,9 @@ sliceOfNumbers = [1, 2, 3, 4, 5] yamlInputData, }, out: []bytesWithFormat{ - newStringWithFormat(parsing.JSON, `false`), - newStringWithFormat(parsing.YAML, `false`), - newStringWithFormat(parsing.TOML, `false`), + newStringWithFormat(json.JSON, `false`), + newStringWithFormat(yaml.YAML, `false`), + newStringWithFormat(toml.TOML, `false`), }, }.run) t.Run("true string", testCases{ @@ -239,9 +242,9 @@ sliceOfNumbers = [1, 2, 3, 4, 5] yamlInputData, }, out: []bytesWithFormat{ - newStringWithFormat(parsing.JSON, `"true"`), - newStringWithFormat(parsing.YAML, `"true"`), - newStringWithFormat(parsing.TOML, `'true'`), + newStringWithFormat(json.JSON, `"true"`), + newStringWithFormat(yaml.YAML, `"true"`), + newStringWithFormat(toml.TOML, `'true'`), }, }.run) t.Run("false string", testCases{ @@ -251,9 +254,9 @@ sliceOfNumbers = [1, 2, 3, 4, 5] yamlInputData, }, out: []bytesWithFormat{ - newStringWithFormat(parsing.JSON, `"false"`), - newStringWithFormat(parsing.YAML, `"false"`), - newStringWithFormat(parsing.TOML, `'false'`), + newStringWithFormat(json.JSON, `"false"`), + newStringWithFormat(yaml.YAML, `"false"`), + newStringWithFormat(toml.TOML, `'false'`), }, }.run) }) diff --git a/internal/cli/man.go b/internal/cli/man.go deleted file mode 100644 index 980aaeb1..00000000 --- a/internal/cli/man.go +++ /dev/null @@ -1,30 +0,0 @@ -package cli - -import ( - "github.com/spf13/cobra" - "github.com/spf13/cobra/doc" -) - -func manCommand(root *cobra.Command) *cobra.Command { - // Do not include timestamp in generated man pages. - // See https://github.com/spf13/cobra/issues/142 - root.DisableAutoGenTag = true - - cmd := &cobra.Command{ - Use: "man -o ", - Short: "Generate manual pages for all dasel subcommands", - RunE: func(cmd *cobra.Command, args []string) error { - return manRunE(cmd, root) - }, - } - - cmd.Flags().StringP("output-directory", "o", ".", "The directory in which man pages will be created") - - return cmd -} - -func manRunE(cmd *cobra.Command, root *cobra.Command) error { - outputDirectory, _ := cmd.Flags().GetString("output-directory") - - return doc.GenManTree(root, nil, outputDirectory) -} diff --git a/internal/cli/man_test.go b/internal/cli/man_test.go deleted file mode 100644 index 9a33cd3c..00000000 --- a/internal/cli/man_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package cli_test - -import ( - "os" - "testing" -) - -func TestManCommand(t *testing.T) { - t.Skip("Temporarily disabled") - tempDir := t.TempDir() - - _, _, err := runDasel([]string{"man", "-o", tempDir}, nil) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - - files, err := os.ReadDir(tempDir) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - - expectedFiles := []string{ - "dasel-completion-bash.1", - "dasel-completion-fish.1", - "dasel-completion-powershell.1", - "dasel-completion-zsh.1", - "dasel-completion.1", - //"dasel-delete.1", - "dasel-man.1", - //"dasel-put.1", - //"dasel-validate.1", - "dasel.1", - } - - if len(files) != len(expectedFiles) { - t.Fatalf("expected %d files, got %d", len(expectedFiles), len(files)) - } - - for i, f := range files { - if f.Name() != expectedFiles[i] { - t.Fatalf("expected %v, got %v", expectedFiles[i], f.Name()) - } - } -} diff --git a/internal/cli/query.go b/internal/cli/query.go new file mode 100644 index 00000000..c70c7f62 --- /dev/null +++ b/internal/cli/query.go @@ -0,0 +1,95 @@ +package cli + +import ( + "fmt" + "io" + + "github.com/tomwright/dasel/v3/execution" + "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/parsing" +) + +type QueryCmd struct { + Vars *[]variable `flag:"" name:"var" help:"Variables to pass to the query. E.g. --var foo=\"bar\" --var baz=json:file:./some/file.json"` + InFormat string `flag:"" name:"in" short:"i" help:"The format of the input data."` + OutFormat string `flag:"" name:"out" short:"o" help:"The format of the output data."` + ReturnRoot bool `flag:"" name:"root" help:"Return the root value."` + + Query string `arg:"" help:"The query to execute." optional:"" default:""` +} + +func (c *QueryCmd) Run(ctx *Globals) error { + var opts []execution.ExecuteOptionFn + + if c.OutFormat == "" { + c.OutFormat = c.InFormat + } + + var reader parsing.Reader + var err error + if len(c.InFormat) > 0 { + reader, err = parsing.Format(c.InFormat).NewReader() + if err != nil { + return fmt.Errorf("failed to get input reader: %w", err) + } + } + + writerOptions := parsing.DefaultWriterOptions() + + writer, err := parsing.Format(c.OutFormat).NewWriter(writerOptions) + if err != nil { + return fmt.Errorf("failed to get output writer: %w", err) + } + + if c.Vars != nil { + for _, v := range *c.Vars { + opts = append(opts, execution.WithVariable(v.Name, v.Value)) + } + } + + // Default to null. If stdin is being read then this will be overwritten. + inputData := model.NewNullValue() + + var inputBytes []byte + if ctx.Stdin != nil { + inputBytes, err = io.ReadAll(ctx.Stdin) + if err != nil { + return fmt.Errorf("error reading stdin: %w", err) + } + } + + if len(inputBytes) > 0 { + if reader == nil { + return fmt.Errorf("input format is required when reading stdin") + } + inputData, err = reader.Read(inputBytes) + if err != nil { + return fmt.Errorf("error reading input: %w", err) + } + } + + opts = append(opts, execution.WithVariable("root", inputData)) + + options := execution.NewOptions(opts...) + + outputData, err := execution.ExecuteSelector(c.Query, inputData, options) + if err != nil { + return err + } + + if c.ReturnRoot { + outputData = inputData + } + + outputBytes, err := writer.Write(outputData) + if err != nil { + return fmt.Errorf("error writing output: %w", err) + } + + _, err = ctx.Stdout.Write(outputBytes) + if err != nil { + return fmt.Errorf("error writing output: %w", err) + } + + return nil +} diff --git a/internal/cli/root.go b/internal/cli/root.go deleted file mode 100644 index b29b71bb..00000000 --- a/internal/cli/root.go +++ /dev/null @@ -1,155 +0,0 @@ -package cli - -import ( - "errors" - "fmt" - "io" - "os" - "strings" - - "github.com/spf13/cobra" - "github.com/tomwright/dasel/v3/execution" - "github.com/tomwright/dasel/v3/internal" - "github.com/tomwright/dasel/v3/model" - "github.com/tomwright/dasel/v3/parsing" -) - -// ErrBadVarArg is returned when an invalid variable argument is provided. -var ErrBadVarArg = errors.New("invalid variable format, expect foo=bar, or foo=format:path") - -// varOptFromArg attempts to parse a variable declaration from a commandline argument. -func varOptFromArg(arg string, r parsing.Reader) (execution.ExecuteOptionFn, error) { - kv := strings.SplitN(arg, "=", 2) - if len(kv) != 2 { - return nil, ErrBadVarArg - } - - varName := kv[0] - - formatData := strings.SplitN(kv[1], ":", 2) - - reader := r - filepath := kv[1] - - if len(formatData) == 2 { - var err error - reader, err = parsing.NewReader(parsing.Format(formatData[0])) - if err != nil { - return nil, err - } - filepath = formatData[1] - } else if reader == nil { - return nil, fmt.Errorf("variable file format required") - } - - f, err := os.Open(filepath) - if err != nil { - return nil, err - } - defer f.Close() - - inputBytes, err := io.ReadAll(f) - if err != nil { - return nil, err - } - - value, err := reader.Read(inputBytes) - if err != nil { - return nil, fmt.Errorf("failed to read file contents for variable %q: %w", varName, err) - } - - return execution.WithVariable(varName, value), nil -} - -// RootCmd returns the root cli command. -func RootCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "dasel [flags] [variableName=fileFormat:filePath] selector", - Short: "Query and modify data structures using selectors", - Long: `dasel is a command-line utility to query and modify data structures using selectors.`, - Version: internal.Version, - Args: nil, - Example: `dasel -o json foo=json:bar.json '{"x": $foo.x, "y": $foo.x + 1}'`, - RunE: func(cmd *cobra.Command, args []string) error { - selectorStr := "" - if len(args) > 0 { - selectorStr = args[len(args)-1] - args = args[0 : len(args)-1] - } - - var opts []execution.ExecuteOptionFn - - readerStr, _ := cmd.Flags().GetString("input") - writerStr, _ := cmd.Flags().GetString("output") - if writerStr == "" { - writerStr = readerStr - } - - var reader parsing.Reader - var err error - if len(readerStr) > 0 { - reader, err = parsing.NewReader(parsing.Format(readerStr)) - if err != nil { - return fmt.Errorf("failed to get input reader: %w", err) - } - } - - writer, err := parsing.NewWriter(parsing.Format(writerStr)) - if err != nil { - return fmt.Errorf("failed to get output writer: %w", err) - } - - for _, a := range args { - o, err := varOptFromArg(a, reader) - if err != nil { - return fmt.Errorf("failed to process variable: %w", err) - } - opts = append(opts, o) - } - - var inputBytes []byte - if reader != nil { - inputBytes, err = io.ReadAll(cmd.InOrStdin()) - if err != nil { - return fmt.Errorf("error reading input: %w", err) - } - } - - var inputData *model.Value - if len(inputBytes) > 0 { - inputData, err = reader.Read(inputBytes) - if err != nil { - return fmt.Errorf("error reading input: %w", err) - } - opts = append(opts, execution.WithVariable("root", inputData)) - } - - options := execution.NewOptions(opts...) - - outputData, err := execution.ExecuteSelector(selectorStr, inputData, options) - if err != nil { - return err - } - - outputBytes, err := writer.Write(outputData) - if err != nil { - return fmt.Errorf("error writing output: %w", err) - } - - _, err = cmd.OutOrStdout().Write(outputBytes) - if err != nil { - return fmt.Errorf("error writing output: %w", err) - } - - return nil - }, - } - - cmd.Flags().StringP("input", "i", "", "The format of the input data. Can be one of: json, yaml, toml, xml, csv") - cmd.Flags().StringP("output", "o", "", "The format of the output data. Can be one of: json, yaml, toml, xml, csv") - - // TODO : apply fallback to root cmd - //cmd.AddCommand(manCommand(cmd)) - - return cmd -} diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go deleted file mode 100644 index 5f851111..00000000 --- a/internal/cli/root_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package cli_test - -import ( - "bytes" - "errors" - "reflect" - "testing" - - "github.com/tomwright/dasel/v3/internal/cli" -) - -func runDasel(args []string, in []byte) ([]byte, []byte, error) { - stdOut := bytes.NewBuffer([]byte{}) - stdErr := bytes.NewBuffer([]byte{}) - - cmd := cli.RootCmd() - cmd.SetArgs(args) - cmd.SetOut(stdOut) - cmd.SetErr(stdErr) - - if in != nil { - cmd.SetIn(bytes.NewReader(in)) - } - - err := cmd.Execute() - return stdOut.Bytes(), stdErr.Bytes(), err -} - -type testCase struct { - args []string - in []byte - stdout []byte - stderr []byte - err error -} - -func runTest(tc testCase) func(t *testing.T) { - return func(t *testing.T) { - if tc.stdout == nil { - tc.stdout = []byte{} - } - if tc.stderr == nil { - tc.stderr = []byte{} - } - - gotStdOut, gotStdErr, gotErr := runDasel(tc.args, tc.in) - if !errors.Is(gotErr, tc.err) && !errors.Is(tc.err, gotErr) { - t.Errorf("expected error %v, got %v", tc.err, gotErr) - return - } - - if !reflect.DeepEqual(tc.stderr, gotStdErr) { - t.Errorf("expected stderr %s, got %s", string(tc.stderr), string(gotStdErr)) - } - - if !reflect.DeepEqual(tc.stdout, gotStdOut) { - t.Errorf("expected stdout %s, got %s", string(tc.stdout), string(gotStdOut)) - } - } -} diff --git a/internal/cli/variable.go b/internal/cli/variable.go new file mode 100644 index 00000000..82536234 --- /dev/null +++ b/internal/cli/variable.go @@ -0,0 +1,80 @@ +package cli + +import ( + "fmt" + "io" + "os" + "reflect" + "strings" + + "github.com/alecthomas/kong" + "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/parsing" +) + +type variable struct { + Name string + Value *model.Value +} + +type variableMapper struct { +} + +// Decode decodes a variable from a flag. +// E.g. --var foo=bar +// E.g. --var foo=json:{"bar":"baz"} +// E.g. --var foo=json:file:/path/to/file.json +func (vm *variableMapper) Decode(ctx *kong.DecodeContext, target reflect.Value) error { + t := ctx.Scan.Pop() + + strVal, ok := t.Value.(string) + if !ok { + return fmt.Errorf("expected string value for variable") + } + + nameValueSplit := strings.SplitN(strVal, "=", 2) + if len(nameValueSplit) != 2 { + return fmt.Errorf("invalid variable format, expect foo=bar, or foo=format:file:path") + } + + res := variable{ + Name: nameValueSplit[0], + } + + format := "dasel" + valueRaw := nameValueSplit[1] + + firstSplit := strings.SplitN(valueRaw, ":", 2) + if len(firstSplit) == 2 { + format = firstSplit[0] + valueRaw = firstSplit[1] + } + if strings.HasPrefix(valueRaw, "file:") { + filePath := valueRaw[len("file:"):] + valueRaw = valueRaw[:len("file:")] + + f, err := os.Open(filePath) + if err != nil { + return fmt.Errorf("failed to open file: %w", err) + } + defer f.Close() + contents, err := io.ReadAll(f) + if err != nil { + return fmt.Errorf("failed to read file contents: %w", err) + } + valueRaw = string(contents) + } + + reader, err := parsing.Format(format).NewReader() + if err != nil { + return fmt.Errorf("failed to create reader: %w", err) + } + res.Value, err = reader.Read([]byte(valueRaw)) + if err != nil { + return fmt.Errorf("failed to read value: %w", err) + } + + target.Elem().Set(reflect.Append(target.Elem(), reflect.ValueOf(res))) + + return nil +} diff --git a/model/value_set.go b/model/value_set.go index 456fa44d..57c9b127 100644 --- a/model/value_set.go +++ b/model/value_set.go @@ -16,37 +16,45 @@ func (v *Value) Set(newValue *Value) error { return err } - if a.Kind() == newValue.Kind() { - a.Value.Set(newValue.Value) - return nil - } - b := newValue.UnpackKinds(reflect.Ptr) if a.Kind() == b.Kind() { a.Value.Set(b.Value) return nil } - b = newValue.UnpackKinds(reflect.Interface) - if a.Kind() == b.Kind() { - a.Value.Set(b.Value) - return nil - } - b = newValue.UnpackKinds(reflect.Ptr, reflect.Interface) if a.Kind() == b.Kind() { a.Value.Set(b.Value) return nil } - b, err = newValue.UnpackUntilAddressable() - if err != nil { - return err - } - if a.Kind() == b.Kind() { - a.Value.Set(b.Value) - return nil - } + // These are commented out because I don't think they are needed. + + //if a.Kind() == newValue.Kind() { + // a.Value.Set(newValue.Value) + // return nil + //} + + //b = newValue.UnpackKinds(reflect.Interface) + //if a.Kind() == b.Kind() { + // a.Value.Set(b.Value) + // return nil + //} + + //b = newValue.UnpackKinds(reflect.Ptr, reflect.Interface) + //if a.Kind() == b.Kind() { + // a.Value.Set(b.Value) + // return nil + //} + + //b, err = newValue.UnpackUntilAddressable() + //if err != nil { + // return err + //} + //if a.Kind() == b.Kind() { + // a.Value.Set(b.Value) + // return nil + //} // This is a hard limitation at the moment. // If the types are not the same, we cannot set the value. diff --git a/parsing/d/reader.go b/parsing/d/reader.go new file mode 100644 index 00000000..4e1c9204 --- /dev/null +++ b/parsing/d/reader.go @@ -0,0 +1,38 @@ +package json + +import ( + "fmt" + + "github.com/tomwright/dasel/v3/execution" + "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/parsing" +) + +const ( + // Dasel represents the dasel format. + Dasel parsing.Format = "dasel" +) + +var _ parsing.Reader = (*daselReader)(nil) + +func init() { + parsing.RegisterReader(Dasel, newDaselReader) +} + +func newDaselReader() (parsing.Reader, error) { + return &daselReader{}, nil +} + +type daselReader struct { +} + +func (dr *daselReader) Read(in []byte) (*model.Value, error) { + if len(in) == 0 { + return model.NewNullValue(), nil + } + out, err := execution.ExecuteSelector(string(in), model.NewNullValue(), execution.NewOptions()) + if err != nil { + return nil, fmt.Errorf("failed to read value: %w", err) + } + return out, nil +} diff --git a/parsing/format.go b/parsing/format.go index 951b0eec..73a6a0ae 100644 --- a/parsing/format.go +++ b/parsing/format.go @@ -2,61 +2,48 @@ package parsing import ( "fmt" - - "github.com/tomwright/dasel/v3/model" ) // Format represents a file format. type Format string -// Supported file formats. -const ( - JSON Format = "json" - YAML Format = "yaml" - TOML Format = "toml" -) - -// String returns the string representation of the format. -func (f Format) String() string { - return string(f) +// NewReader creates a new reader for the format. +func (f Format) NewReader() (Reader, error) { + fn, ok := readers[f] + if !ok { + return nil, fmt.Errorf("unsupported reader file format: %s", f) + } + return fn() } -// Reader reads a value from a byte slice. -type Reader interface { - // Read reads a value from a byte slice. - Read([]byte) (*model.Value, error) +// NewWriter creates a new writer for the format. +func (f Format) NewWriter(options WriterOptions) (Writer, error) { + fn, ok := writers[f] + if !ok { + return nil, fmt.Errorf("unsupported writer file format: %s", f) + } + return fn(options) } -// Writer writes a value to a byte slice. -type Writer interface { - // Write writes a value to a byte slice. - Write(*model.Value) ([]byte, error) +// String returns the string representation of the format. +func (f Format) String() string { + return string(f) } -// NewReader creates a new reader for the specified format. -func NewReader(format Format) (Reader, error) { - switch format { - case JSON: - return NewJSONReader() - case YAML: - return NewYAMLReader() - case TOML: - return NewTOMLReader() - default: - return nil, fmt.Errorf("unsupported file format: %s", format) +// RegisteredReaders returns a list of registered readers. +func RegisteredReaders() []Format { + var formats []Format + for format := range readers { + formats = append(formats, format) } + return formats } -// NewWriter creates a new writer for the specified format. -func NewWriter(format Format) (Writer, error) { - switch format { - case JSON: - return NewJSONWriter() - case YAML: - return NewYAMLWriter() - case TOML: - return NewTOMLWriter() - default: - return nil, fmt.Errorf("unsupported file format: %s", format) +// RegisteredWriters returns a list of registered writers. +func RegisteredWriters() []Format { + var formats []Format + for format := range writers { + formats = append(formats, format) } + return formats } diff --git a/parsing/json.go b/parsing/json/json.go similarity index 93% rename from parsing/json.go rename to parsing/json/json.go index 589e9cde..bcd33a16 100644 --- a/parsing/json.go +++ b/parsing/json/json.go @@ -1,4 +1,4 @@ -package parsing +package json import ( "bytes" @@ -8,23 +8,36 @@ import ( "strings" "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/parsing" ) const ( + // JSON represents the JSON file format. + JSON parsing.Format = "json" + jsonOpenObject = json.Delim('{') jsonCloseObject = json.Delim('}') jsonOpenArray = json.Delim('[') jsonCloseArray = json.Delim(']') ) -// NewJSONReader creates a new JSON reader. -func NewJSONReader() (Reader, error) { +var _ parsing.Reader = (*jsonReader)(nil) +var _ parsing.Writer = (*jsonWriter)(nil) + +func init() { + parsing.RegisterReader(JSON, newJSONReader) + parsing.RegisterWriter(JSON, newJSONWriter) +} + +func newJSONReader() (parsing.Reader, error) { return &jsonReader{}, nil } // NewJSONWriter creates a new JSON writer. -func NewJSONWriter() (Writer, error) { - return &jsonWriter{}, nil +func newJSONWriter(options parsing.WriterOptions) (parsing.Writer, error) { + return &jsonWriter{ + options: options, + }, nil } type jsonReader struct{} @@ -188,7 +201,9 @@ func (j *jsonReader) decodeToken(decoder *json.Decoder, t json.Token) (*model.Va } } -type jsonWriter struct{} +type jsonWriter struct { + options parsing.WriterOptions +} // Write writes a value to a byte slice. func (j *jsonWriter) Write(value *model.Value) ([]byte, error) { diff --git a/parsing/json_test.go b/parsing/json/json_test.go similarity index 80% rename from parsing/json_test.go rename to parsing/json/json_test.go index 0a55e1c4..02a2fab9 100644 --- a/parsing/json_test.go +++ b/parsing/json/json_test.go @@ -1,10 +1,11 @@ -package parsing_test +package json_test import ( "testing" "github.com/google/go-cmp/cmp" "github.com/tomwright/dasel/v3/parsing" + "github.com/tomwright/dasel/v3/parsing/json" ) func TestJson(t *testing.T) { @@ -24,11 +25,11 @@ func TestJson(t *testing.T) { } } `) - reader, err := parsing.NewJSONReader() + reader, err := json.JSON.NewReader() if err != nil { t.Fatal(err) } - writer, err := parsing.NewJSONWriter() + writer, err := json.JSON.NewWriter(parsing.DefaultWriterOptions()) if err != nil { t.Fatal(err) } diff --git a/parsing/reader.go b/parsing/reader.go new file mode 100644 index 00000000..1d66eafe --- /dev/null +++ b/parsing/reader.go @@ -0,0 +1,19 @@ +package parsing + +import "github.com/tomwright/dasel/v3/model" + +var readers = map[Format]NewReaderFn{} + +// Reader reads a value from a byte slice. +type Reader interface { + // Read reads a value from a byte slice. + Read([]byte) (*model.Value, error) +} + +// NewReaderFn is a function that creates a new reader. +type NewReaderFn func() (Reader, error) + +// RegisterReader registers a new reader for the format. +func RegisterReader(format Format, fn NewReaderFn) { + readers[format] = fn +} diff --git a/parsing/toml.go b/parsing/toml/toml.go similarity index 56% rename from parsing/toml.go rename to parsing/toml/toml.go index 5de7afb4..3695d5f8 100644 --- a/parsing/toml.go +++ b/parsing/toml/toml.go @@ -1,17 +1,29 @@ -package parsing +package toml import ( "github.com/pelletier/go-toml/v2" "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/parsing" ) -// NewTOMLReader creates a new TOML reader. -func NewTOMLReader() (Reader, error) { +// TODO : Implement using https://github.com/pelletier/go-toml/blob/v2/unstable/ast.go + +// TOML represents the TOML file format. +const TOML parsing.Format = "toml" + +var _ parsing.Reader = (*tomlReader)(nil) +var _ parsing.Writer = (*tomlWriter)(nil) + +func init() { + parsing.RegisterReader(TOML, newTOMLReader) + parsing.RegisterWriter(TOML, newTOMLWriter) +} + +func newTOMLReader() (parsing.Reader, error) { return &tomlReader{}, nil } -// NewTOMLWriter creates a new TOML writer. -func NewTOMLWriter() (Writer, error) { +func newTOMLWriter(options parsing.WriterOptions) (parsing.Writer, error) { return &tomlWriter{}, nil } diff --git a/parsing/writer.go b/parsing/writer.go new file mode 100644 index 00000000..97c4f236 --- /dev/null +++ b/parsing/writer.go @@ -0,0 +1,32 @@ +package parsing + +import "github.com/tomwright/dasel/v3/model" + +var writers = map[Format]NewWriterFn{} + +type WriterOptions struct { + Compact bool + Indent string +} + +// DefaultWriterOptions returns the default writer options. +func DefaultWriterOptions() WriterOptions { + return WriterOptions{ + Compact: false, + Indent: " ", + } +} + +// Writer writes a value to a byte slice. +type Writer interface { + // Write writes a value to a byte slice. + Write(*model.Value) ([]byte, error) +} + +// NewWriterFn is a function that creates a new writer. +type NewWriterFn func(options WriterOptions) (Writer, error) + +// RegisterWriter registers a new writer for the format. +func RegisterWriter(format Format, fn NewWriterFn) { + writers[format] = fn +} diff --git a/parsing/yaml.go b/parsing/yaml/yaml.go similarity index 62% rename from parsing/yaml.go rename to parsing/yaml/yaml.go index a5488f53..8f9e4337 100644 --- a/parsing/yaml.go +++ b/parsing/yaml/yaml.go @@ -1,18 +1,29 @@ -package parsing +package yaml import ( "bytes" + "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/parsing" "gopkg.in/yaml.v3" ) -// NewYAMLReader creates a new YAML reader. -func NewYAMLReader() (Reader, error) { +// YAML represents the YAML file format. +const YAML parsing.Format = "yaml" + +var _ parsing.Reader = (*yamlReader)(nil) +var _ parsing.Writer = (*yamlWriter)(nil) + +func init() { + parsing.RegisterReader(YAML, newYAMLReader) + parsing.RegisterWriter(YAML, newYAMLWriter) +} + +func newYAMLReader() (parsing.Reader, error) { return &yamlReader{}, nil } -// NewYAMLWriter creates a new YAML writer. -func NewYAMLWriter() (Writer, error) { +func newYAMLWriter(options parsing.WriterOptions) (parsing.Writer, error) { return &yamlWriter{}, nil } From 48ec95df50e97c3f2e57dab02a9c6b97101fce04 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Thu, 24 Oct 2024 19:39:33 +0100 Subject: [PATCH 38/56] Add version command --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .github/workflows/build-dev.yaml | 2 +- .github/workflows/build-test.yaml | 2 +- .github/workflows/build.yaml | 2 +- internal/cli/command.go | 3 ++- internal/cli/version.go | 11 +++++++++++ 6 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 internal/cli/version.go diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 35861d01..dd90ac9d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -31,7 +31,7 @@ If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] -- Version [e.g. 22] (`dasel --version`) +- Version [e.g. 22] (`dasel version`) **Additional context** Add any other context about the problem here. diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index 8f89faa5..5f6de346 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -72,4 +72,4 @@ jobs: run: GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} CGO_ENABLED=0 go build -o target/release/${{ matrix.artifact_name }} -ldflags="-X 'github.com/tomwright/dasel/v3/internal.Version=${{ env.RELEASE_VERSION }}'" ./cmd/dasel - name: Test version if: matrix.test_version == true - run: ./target/release/${{ matrix.artifact_name }} --version + run: ./target/release/${{ matrix.artifact_name }} version diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index fef6ff7a..124cc3fd 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -84,7 +84,7 @@ jobs: run: GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} CGO_ENABLED=0 go build -o target/release/${{ matrix.artifact_name }} -ldflags="-X 'github.com/tomwright/dasel/v3/internal.Version=${{ env.RELEASE_VERSION }}'" ./cmd/dasel - name: Test version if: matrix.test_version == true - run: ./target/release/${{ matrix.artifact_name }} --version + run: ./target/release/${{ matrix.artifact_name }} version - name: Test execution if: matrix.test_execution == true run: | diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 818ba8a2..b71531bc 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -80,7 +80,7 @@ jobs: run: GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} CGO_ENABLED=0 go build -o target/release/${{ matrix.artifact_name }} -ldflags="-X 'github.com/tomwright/dasel/v3/internal.Version=${{ env.RELEASE_VERSION }}'" ./cmd/dasel - name: Test version if: matrix.test_version == true - run: ./target/release/${{ matrix.artifact_name }} --version + run: ./target/release/${{ matrix.artifact_name }} version - name: Gzip binaries run: gzip -c ./target/release/${{ matrix.artifact_name }} > ./target/release/${{ matrix.artifact_name }}.gz - name: Upload binaries to release diff --git a/internal/cli/command.go b/internal/cli/command.go index cdb977a0..ee1dca8a 100644 --- a/internal/cli/command.go +++ b/internal/cli/command.go @@ -17,7 +17,8 @@ type Globals struct { type CLI struct { Globals - Query QueryCmd `cmd:"" help:"Execute a query"` + Query QueryCmd `cmd:"" help:"Execute a query"` + Version VersionCmd `cmd:"" help:"Print the version"` } func MustRun(stdin io.Reader, stdout, stderr io.Writer) { diff --git a/internal/cli/version.go b/internal/cli/version.go new file mode 100644 index 00000000..5c306084 --- /dev/null +++ b/internal/cli/version.go @@ -0,0 +1,11 @@ +package cli + +import "github.com/tomwright/dasel/v3/internal" + +type VersionCmd struct { +} + +func (c *VersionCmd) Run(ctx *Globals) error { + _, err := ctx.Stdout.Write([]byte(internal.Version + "\n")) + return err +} From 662076f75790c0703919820018313503df1c17c8 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Thu, 24 Oct 2024 19:54:43 +0100 Subject: [PATCH 39/56] Default to query command --- internal/cli/command.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/internal/cli/command.go b/internal/cli/command.go index ee1dca8a..b0621371 100644 --- a/internal/cli/command.go +++ b/internal/cli/command.go @@ -17,7 +17,7 @@ type Globals struct { type CLI struct { Globals - Query QueryCmd `cmd:"" help:"Execute a query"` + Query QueryCmd `cmd:"" help:"[default] Execute a query"` Version VersionCmd `cmd:"" help:"Print the version"` } @@ -51,6 +51,16 @@ func Run(stdin io.Reader, stdout, stderr io.Writer) (*kong.Context, error) { k.Stderr = cli.Stderr return nil }), + kong.PostBuild(func(k *kong.Kong) error { + defaultCommandName := "query" + for _, c := range k.Model.Children { + if c.Name == defaultCommandName { + k.Model.DefaultCmd = c + break + } + } + return nil + }), ) err := ctx.Run() return ctx, err From dd5953c2c46c1dd1c0554640d05a0e59bbebc78c Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Thu, 24 Oct 2024 20:01:02 +0100 Subject: [PATCH 40/56] Remove legacy dencoding package --- dencoding/README.md | 15 -- dencoding/keyvalue.go | 7 - dencoding/toml.go | 11 -- dencoding/toml_decoder.go | 36 ----- dencoding/toml_decoder_test.go | 80 ----------- dencoding/toml_encoder.go | 99 ------------- dencoding/toml_encoder_test.go | 34 ----- dencoding/yaml.go | 24 ---- dencoding/yaml_decoder.go | 191 ------------------------- dencoding/yaml_decoder_test.go | 154 -------------------- dencoding/yaml_encoder.go | 124 ---------------- dencoding/yaml_encoder_test.go | 37 ----- execution/execute_binary_test.go | 8 +- execution/execute_map_test.go | 14 +- execution/execute_object_test.go | 17 +-- execution/execute_test.go | 14 +- execution/execute_unary_test.go | 4 +- execution/func_add_test.go | 6 +- {dencoding => model/orderedmap}/map.go | 8 +- model/value_map.go | 12 +- model/value_map_test.go | 4 +- 21 files changed, 43 insertions(+), 856 deletions(-) delete mode 100644 dencoding/README.md delete mode 100644 dencoding/keyvalue.go delete mode 100644 dencoding/toml.go delete mode 100644 dencoding/toml_decoder.go delete mode 100644 dencoding/toml_decoder_test.go delete mode 100644 dencoding/toml_encoder.go delete mode 100644 dencoding/toml_encoder_test.go delete mode 100644 dencoding/yaml.go delete mode 100644 dencoding/yaml_decoder.go delete mode 100644 dencoding/yaml_decoder_test.go delete mode 100644 dencoding/yaml_encoder.go delete mode 100644 dencoding/yaml_encoder_test.go rename {dencoding => model/orderedmap}/map.go (94%) diff --git a/dencoding/README.md b/dencoding/README.md deleted file mode 100644 index 83ff2345..00000000 --- a/dencoding/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# dencoding - Dasel Encoding - -This package provides encoding implementations for all supported file formats. - -The main difference is that it aims to keep maps ordered, where the default encoders/decoders do not. - -## Support formats - -### Decoding - -Custom decoders are required to ensure that map/object values are decoded into the `Map` type rather than a standard `map[string]any`. - -### Encoding - -The `Map` type must have the appropriate Marshal func on it to ensure marshalling it in the desired format retains the ordering. diff --git a/dencoding/keyvalue.go b/dencoding/keyvalue.go deleted file mode 100644 index e3e4f281..00000000 --- a/dencoding/keyvalue.go +++ /dev/null @@ -1,7 +0,0 @@ -package dencoding - -// KeyValue is a single key value pair from a *Map. -type KeyValue struct { - Key string - Value any -} diff --git a/dencoding/toml.go b/dencoding/toml.go deleted file mode 100644 index 8cc64781..00000000 --- a/dencoding/toml.go +++ /dev/null @@ -1,11 +0,0 @@ -package dencoding - -// TOMLEncoderOption is identifies an option that can be applied to a TOML encoder. -type TOMLEncoderOption interface { - ApplyEncoder(encoder *TOMLEncoder) -} - -// TOMLDecoderOption is identifies an option that can be applied to a TOML decoder. -type TOMLDecoderOption interface { - ApplyDecoder(decoder *TOMLDecoder) -} diff --git a/dencoding/toml_decoder.go b/dencoding/toml_decoder.go deleted file mode 100644 index 8066088f..00000000 --- a/dencoding/toml_decoder.go +++ /dev/null @@ -1,36 +0,0 @@ -package dencoding - -import ( - "github.com/pelletier/go-toml/v2" - "github.com/pelletier/go-toml/v2/unstable" - "io" -) - -// TOMLDecoder wraps a standard toml encoder to implement custom ordering logic. -type TOMLDecoder struct { - reader io.Reader - p *unstable.Parser -} - -// NewTOMLDecoder returns a new dencoding TOMLDecoder. -func NewTOMLDecoder(r io.Reader, options ...TOMLDecoderOption) *TOMLDecoder { - decoder := &TOMLDecoder{ - reader: r, - } - for _, o := range options { - o.ApplyDecoder(decoder) - } - return decoder -} - -// Decode decodes the next item found in the decoder and writes it to v. -func (decoder *TOMLDecoder) Decode(v any) error { - data, err := io.ReadAll(decoder.reader) - if err != nil { - return err - } - if len(data) == 0 { - return io.EOF - } - return toml.Unmarshal(data, v) -} diff --git a/dencoding/toml_decoder_test.go b/dencoding/toml_decoder_test.go deleted file mode 100644 index 8718662e..00000000 --- a/dencoding/toml_decoder_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package dencoding_test - -import ( - "bytes" - "io" - "reflect" - "testing" - - "github.com/tomwright/dasel/v3/dencoding" -) - -func TestTOMLDecoder_Decode(t *testing.T) { - - t.Run("KeyValue", func(t *testing.T) { - b := []byte(`x = 1 -a = 'hello'`) - dec := dencoding.NewTOMLDecoder(bytes.NewReader(b)) - - maps := make([]any, 0) - for { - var v any - if err := dec.Decode(&v); err != nil { - if err == io.EOF { - break - } - t.Errorf("unexpected error: %v", err) - return - } - maps = append(maps, v) - } - - exp := []any{ - map[string]any{ - "x": int64(1), - "a": "hello", - }, - } - - got := maps - - if !reflect.DeepEqual(exp, got) { - t.Errorf("expected %v, got %v", exp, got) - } - }) - - t.Run("Table", func(t *testing.T) { - b := []byte(` -[user] -name = "Tom" -age = 29 -`) - dec := dencoding.NewTOMLDecoder(bytes.NewReader(b)) - - got := make([]any, 0) - for { - var v any - if err := dec.Decode(&v); err != nil { - if err == io.EOF { - break - } - t.Errorf("unexpected error: %v", err) - return - } - got = append(got, v) - } - - exp := []any{ - map[string]any{ - "user": map[string]any{ - "age": int64(29), - "name": "Tom", - }, - }, - } - - if !reflect.DeepEqual(exp, got) { - t.Errorf("expected %v, got %v", exp, got) - } - }) -} diff --git a/dencoding/toml_encoder.go b/dencoding/toml_encoder.go deleted file mode 100644 index 998d7b17..00000000 --- a/dencoding/toml_encoder.go +++ /dev/null @@ -1,99 +0,0 @@ -package dencoding - -import ( - "bytes" - "github.com/pelletier/go-toml/v2" - "io" -) - -// TOMLEncoder wraps a standard toml encoder to implement custom ordering logic. -type TOMLEncoder struct { - encoder *toml.Encoder - writer io.Writer - buffer *bytes.Buffer -} - -// NewTOMLEncoder returns a new dencoding TOMLEncoder. -func NewTOMLEncoder(w io.Writer, options ...TOMLEncoderOption) *TOMLEncoder { - buffer := new(bytes.Buffer) - tomlEncoder := toml.NewEncoder(buffer) - tomlEncoder.SetIndentTables(false) - encoder := &TOMLEncoder{ - writer: w, - encoder: tomlEncoder, - buffer: buffer, - } - for _, o := range options { - o.ApplyEncoder(encoder) - } - return encoder -} - -// Encode encodes the given value and writes the encodes bytes to the stream. -func (encoder *TOMLEncoder) Encode(v any) error { - // No ordering is done here. - adjusted := removeDencodingMap(v) - if err := encoder.encoder.Encode(adjusted); err != nil { - return err - } - data, err := io.ReadAll(encoder.buffer) - if err != nil { - return err - } - if _, err := encoder.writer.Write(data); err != nil { - return err - } - newline := []byte("\n") - if !bytes.HasSuffix(data, newline) { - if _, err := encoder.writer.Write(newline); err != nil { - return err - } - } - return nil -} - -// Close cleans up the encoder. -func (encoder *TOMLEncoder) Close() error { - return nil -} - -func removeDencodingMap(value any) any { - switch v := value.(type) { - case []any: - return removeDencodingMapFromArray(v) - case map[string]any: - return removeDencodingMapFromMap(v) - case *Map: - return removeDencodingMap(v.data) - default: - return v - } -} - -func removeDencodingMapFromArray(value []any) []any { - for k, v := range value { - value[k] = removeDencodingMap(v) - } - return value -} - -func removeDencodingMapFromMap(value map[string]any) map[string]any { - for k, v := range value { - value[k] = removeDencodingMap(v) - } - return value -} - -// TOMLIndentSymbol sets the indentation when encoding TOML. -func TOMLIndentSymbol(symbol string) TOMLEncoderOption { - return tomlEncodeSymbol{symbol: symbol} -} - -type tomlEncodeSymbol struct { - symbol string -} - -func (option tomlEncodeSymbol) ApplyEncoder(encoder *TOMLEncoder) { - encoder.encoder.SetIndentSymbol(option.symbol) - encoder.encoder.SetIndentTables(option.symbol != "") -} diff --git a/dencoding/toml_encoder_test.go b/dencoding/toml_encoder_test.go deleted file mode 100644 index 36ac7fb3..00000000 --- a/dencoding/toml_encoder_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package dencoding_test - -import ( - "bytes" - "testing" - - "github.com/tomwright/dasel/v3/dencoding" -) - -func TestTOMLEncoder_Encode(t *testing.T) { - orig := dencoding.NewMap(). - Set("c", "x"). - Set("b", "y"). - Set("a", []any{"a", "c", "b"}) - - exp := `a = ['a', 'c', 'b'] -b = 'y' -c = 'x' -` - - gotBuffer := new(bytes.Buffer) - - encoder := dencoding.NewTOMLEncoder(gotBuffer, dencoding.TOMLIndentSymbol(" ")) - if err := encoder.Encode(orig); err != nil { - t.Errorf("unexpected error: %v", err) - return - } - - got := gotBuffer.String() - - if exp != got { - t.Errorf("expected %s, got %s", exp, got) - } -} diff --git a/dencoding/yaml.go b/dencoding/yaml.go deleted file mode 100644 index cb0d46dc..00000000 --- a/dencoding/yaml.go +++ /dev/null @@ -1,24 +0,0 @@ -package dencoding - -const ( - yamlTagString = "!!str" - yamlTagMap = "!!map" - yamlTagArray = "!!seq" - yamlTagNull = "!!null" - yamlTagBinary = "!!binary" - yamlTagBool = "!!bool" - yamlTagInt = "!!int" - yamlTagFloat = "!!float" - yamlTagTimestamp = "!!timestamp" - yamlTagMerge = "!!merge" -) - -// YAMLEncoderOption is identifies an option that can be applied to a YAML encoder. -type YAMLEncoderOption interface { - ApplyEncoder(encoder *YAMLEncoder) -} - -// YAMLDecoderOption is identifies an option that can be applied to a YAML decoder. -type YAMLDecoderOption interface { - ApplyDecoder(decoder *YAMLDecoder) -} diff --git a/dencoding/yaml_decoder.go b/dencoding/yaml_decoder.go deleted file mode 100644 index b9b5be2c..00000000 --- a/dencoding/yaml_decoder.go +++ /dev/null @@ -1,191 +0,0 @@ -package dencoding - -import ( - "fmt" - "github.com/tomwright/dasel/v3/internal/util" - "io" - "reflect" - "strconv" - "time" - - "gopkg.in/yaml.v3" -) - -// YAMLDecoder wraps a standard yaml encoder to implement custom ordering logic. -type YAMLDecoder struct { - decoder *yaml.Decoder -} - -// NewYAMLDecoder returns a new dencoding YAMLDecoder. -func NewYAMLDecoder(r io.Reader, options ...YAMLDecoderOption) *YAMLDecoder { - yamlDecoder := yaml.NewDecoder(r) - decoder := &YAMLDecoder{ - decoder: yamlDecoder, - } - for _, o := range options { - o.ApplyDecoder(decoder) - } - return decoder -} - -// Decode decodes the next item found in the decoder and writes it to v. -func (decoder *YAMLDecoder) Decode(v any) error { - rv := reflect.ValueOf(v) - if rv.Kind() != reflect.Pointer || rv.IsNil() { - return fmt.Errorf("invalid decode target: %s", reflect.TypeOf(v)) - } - - rve := rv.Elem() - - node, err := decoder.nextNode() - if err != nil { - return err - } - - if node.Kind == yaml.DocumentNode && len(node.Content) == 1 && node.Content[0].ShortTag() == yamlTagNull { - return io.EOF - } - - val, err := decoder.getNodeValue(node) - if err != nil { - return err - } - - rve.Set(reflect.ValueOf(val)) - return nil -} - -func (decoder *YAMLDecoder) getNodeValue(node *yaml.Node) (any, error) { - switch node.Kind { - case yaml.DocumentNode: - return decoder.getNodeValue(node.Content[0]) - case yaml.MappingNode: - return decoder.getMappingNodeValue(node) - case yaml.SequenceNode: - return decoder.getSequenceNodeValue(node) - case yaml.ScalarNode: - return decoder.getScalarNodeValue(node) - case yaml.AliasNode: - return decoder.getNodeValue(node.Alias) - default: - return nil, fmt.Errorf("unhandled node kind: %v", node.Kind) - } -} - -func (decoder *YAMLDecoder) getMappingNodeValue(node *yaml.Node) (any, error) { - res := NewMap() - - content := make([]*yaml.Node, 0) - content = append(content, node.Content...) - - var keyNode *yaml.Node - var valueNode *yaml.Node - for { - if len(content) == 0 { - break - } - - keyNode, valueNode, content = content[0], content[1], content[2:] - - if keyNode.ShortTag() == yamlTagMerge { - content = append(valueNode.Alias.Content, content...) - continue - } - - keyValue, err := decoder.getNodeValue(keyNode) - if err != nil { - return nil, err - } - - value, err := decoder.getNodeValue(valueNode) - if err != nil { - return nil, err - } - - key := util.ToString(keyValue) - - res.Set(key, value) - } - - return res, nil -} - -func (decoder *YAMLDecoder) getSequenceNodeValue(node *yaml.Node) (any, error) { - res := make([]any, len(node.Content)) - for k, n := range node.Content { - val, err := decoder.getNodeValue(n) - if err != nil { - return nil, err - } - res[k] = val - } - return res, nil -} - -func (decoder *YAMLDecoder) getScalarNodeValue(node *yaml.Node) (any, error) { - switch node.ShortTag() { - case yamlTagNull: - return nil, nil - case yamlTagBool: - return node.Value == "true", nil - case yamlTagFloat: - return strconv.ParseFloat(node.Value, 64) - case yamlTagInt: - return strconv.ParseInt(node.Value, 0, 64) - case yamlTagString: - return node.Value, nil - case yamlTagTimestamp: - value, ok := parseTimestamp(node.Value) - if !ok { - return value, fmt.Errorf("could not parse timestamp: %v", node.Value) - } - return value, nil - default: - return nil, fmt.Errorf("unhandled scalar node tag: %v", node.ShortTag()) - } -} - -func (decoder *YAMLDecoder) nextNode() (*yaml.Node, error) { - var node yaml.Node - if err := decoder.decoder.Decode(&node); err != nil { - return nil, err - } - return &node, nil -} - -// This is a subset of the formats allowed by the regular expression -// defined at http://yaml.org/type/timestamp.html. -var allowedTimestampFormats = []string{ - "2006-1-2T15:4:5.999999999Z07:00", // RCF3339Nano with short date fields. - "2006-1-2t15:4:5.999999999Z07:00", // RFC3339Nano with short date fields and lower-case "t". - "2006-1-2 15:4:5.999999999", // space separated with no time zone - "2006-1-2", // date only - // Notable exception: time.Tokenize cannot handle: "2001-12-14 21:59:43.10 -5" - // from the set of examples. -} - -// parseTimestamp parses s as a timestamp string and -// returns the timestamp and reports whether it succeeded. -// Timestamp formats are defined at http://yaml.org/type/timestamp.html -// Copied from yaml.v3. -func parseTimestamp(s string) (time.Time, bool) { - // TODO write code to check all the formats supported by - // http://yaml.org/type/timestamp.html instead of using time.Tokenize. - - // Quick check: all date formats start with YYYY-. - i := 0 - for ; i < len(s); i++ { - if c := s[i]; c < '0' || c > '9' { - break - } - } - if i != 4 || i == len(s) || s[i] != '-' { - return time.Time{}, false - } - for _, format := range allowedTimestampFormats { - if t, err := time.Parse(format, s); err == nil { - return t, true - } - } - return time.Time{}, false -} diff --git a/dencoding/yaml_decoder_test.go b/dencoding/yaml_decoder_test.go deleted file mode 100644 index bb62e415..00000000 --- a/dencoding/yaml_decoder_test.go +++ /dev/null @@ -1,154 +0,0 @@ -package dencoding_test - -import ( - "bytes" - "io" - "reflect" - "testing" - - "github.com/tomwright/dasel/v3/dencoding" -) - -func TestYAMLDecoder_Decode(t *testing.T) { - - t.Run("Basic", func(t *testing.T) { - - b := []byte(` -x: 1 -a: hello ---- -x: 2 -a: there ---- -a: Tom -x: 3 ----`) - dec := dencoding.NewYAMLDecoder(bytes.NewReader(b)) - - maps := make([]any, 0) - for { - var v any - if err := dec.Decode(&v); err != nil { - if err == io.EOF { - break - } - t.Errorf("unexpected error: %v", err) - return - } - maps = append(maps, v) - } - - exp := [][]dencoding.KeyValue{ - { - {Key: "x", Value: int64(1)}, - {Key: "a", Value: "hello"}, - }, - { - {Key: "x", Value: int64(2)}, - {Key: "a", Value: "there"}, - }, - { - {Key: "a", Value: "Tom"}, - {Key: "x", Value: int64(3)}, - }, - } - - got := make([][]dencoding.KeyValue, 0) - for _, v := range maps { - if m, ok := v.(*dencoding.Map); ok { - got = append(got, m.KeyValues()) - } - } - - if !reflect.DeepEqual(exp, got) { - t.Errorf("expected %v, got %v", exp, got) - } - }) - - // https://github.com/TomWright/dasel/issues/278 - t.Run("Issue278", func(t *testing.T) { - b := []byte(` -key1: [value1,value2,value3,value4,value5] -key2: value6 -`) - dec := dencoding.NewYAMLDecoder(bytes.NewReader(b)) - - got := make([]any, 0) - for { - var v any - if err := dec.Decode(&v); err != nil { - if err == io.EOF { - break - } - t.Errorf("unexpected error: %v", err) - return - } - got = append(got, v) - } - - exp := []any{ - dencoding.NewMap(). - Set("key1", []any{"value1", "value2", "value3", "value4", "value5"}). - Set("key2", "value6"), - } - - if !reflect.DeepEqual(exp, got) { - t.Errorf("expected %v, got %v", exp, got) - } - }) - - t.Run("YamlAliases", func(t *testing.T) { - b := []byte(`foo: &foofoo - bar: 1 - baz: &baz "baz" -spam: - ham: "eggs" - bar: 0 - <<: *foofoo - baz: "bazbaz" - -baz: *baz -`) - - dec := dencoding.NewYAMLDecoder(bytes.NewReader(b)) - - got := make([]any, 0) - for { - var v any - if err := dec.Decode(&v); err != nil { - if err == io.EOF { - break - } - t.Errorf("unexpected error: %v", err) - return - } - got = append(got, v) - } - - exp := dencoding.NewMap(). - Set("foo", dencoding.NewMap(). - Set("bar", int64(1)). - Set("baz", "baz")). - Set("spam", dencoding.NewMap(). - Set("ham", "eggs"). - Set("bar", int64(1)). - Set("baz", "bazbaz")). - Set("baz", "baz") - - if len(got) != 1 { - t.Errorf("expected result len of %d, got %d", 1, len(got)) - return - } - - gotMap, ok := got[0].(*dencoding.Map) - if !ok { - t.Errorf("expected result to be of type %T, got %T", exp, got[0]) - return - } - - if !reflect.DeepEqual(exp, gotMap) { - t.Errorf("expected %v, got %v", exp, gotMap) - } - }) - -} diff --git a/dencoding/yaml_encoder.go b/dencoding/yaml_encoder.go deleted file mode 100644 index d9c2c81c..00000000 --- a/dencoding/yaml_encoder.go +++ /dev/null @@ -1,124 +0,0 @@ -package dencoding - -import ( - "github.com/tomwright/dasel/v3/internal/util" - "io" - "strconv" - - "gopkg.in/yaml.v3" -) - -// YAMLEncoder wraps a standard yaml encoder to implement custom ordering logic. -type YAMLEncoder struct { - encoder *yaml.Encoder -} - -// NewYAMLEncoder returns a new dencoding YAMLEncoder. -func NewYAMLEncoder(w io.Writer, options ...YAMLEncoderOption) *YAMLEncoder { - yamlEncoder := yaml.NewEncoder(w) - encoder := &YAMLEncoder{ - encoder: yamlEncoder, - } - for _, o := range options { - o.ApplyEncoder(encoder) - } - return encoder -} - -// Encode encodes the given value and writes the encodes bytes to the stream. -func (encoder *YAMLEncoder) Encode(v any) error { - // We rely on Map.MarshalYAML to ensure ordering. - return encoder.encoder.Encode(v) -} - -// Close cleans up the encoder. -func (encoder *YAMLEncoder) Close() error { - return encoder.encoder.Close() -} - -// MarshalYAML YAML encodes the map and returns the bytes. -// This maintains ordering. -func (m *Map) MarshalYAML() (any, error) { - return yamlOrderedMapToNode(m) -} - -// YAMLEncodeIndent sets the indentation when encoding YAML. -func YAMLEncodeIndent(spaces int) YAMLEncoderOption { - return yamlEncodeIndent{spaces: spaces} -} - -type yamlEncodeIndent struct { - spaces int -} - -func (option yamlEncodeIndent) ApplyEncoder(encoder *YAMLEncoder) { - encoder.encoder.SetIndent(option.spaces) -} - -func yamlValueToNode(value any) (*yaml.Node, error) { - switch v := value.(type) { - case *Map: - return yamlOrderedMapToNode(v) - case []any: - return yamlSliceToNode(v) - default: - return yamlScalarToNode(v) - } -} - -func yamlOrderedMapToNode(value *Map) (*yaml.Node, error) { - mapNode := &yaml.Node{ - Kind: yaml.MappingNode, - Style: yaml.TaggedStyle & yaml.DoubleQuotedStyle & yaml.SingleQuotedStyle & yaml.LiteralStyle & yaml.FoldedStyle & yaml.FlowStyle, - Content: make([]*yaml.Node, 0), - } - - for _, key := range value.keys { - keyNode, err := yamlValueToNode(key) - if err != nil { - return nil, err - } - valueNode, err := yamlValueToNode(value.data[key]) - if err != nil { - return nil, err - } - mapNode.Content = append(mapNode.Content, keyNode, valueNode) - } - - return mapNode, nil -} - -func yamlSliceToNode(value []any) (*yaml.Node, error) { - node := &yaml.Node{ - Kind: yaml.SequenceNode, - Content: make([]*yaml.Node, len(value)), - } - - for i, v := range value { - indexNode, err := yamlValueToNode(v) - if err != nil { - return nil, err - } - node.Content[i] = indexNode - } - - return node, nil -} - -func yamlScalarToNode(value any) (*yaml.Node, error) { - res := &yaml.Node{ - Kind: yaml.ScalarNode, - Value: util.ToString(value), - } - switch v := value.(type) { - case string: - if v == "true" || v == "false" { - // If the string can be evaluated as a bool, quote it. - res.Style = yaml.DoubleQuotedStyle - } else if _, err := strconv.ParseInt(v, 0, 64); err == nil { - // If the string can be evaluated as a number, quote it. - res.Style = yaml.DoubleQuotedStyle - } - } - return res, nil -} diff --git a/dencoding/yaml_encoder_test.go b/dencoding/yaml_encoder_test.go deleted file mode 100644 index a52ea0d4..00000000 --- a/dencoding/yaml_encoder_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package dencoding_test - -import ( - "bytes" - "testing" - - "github.com/tomwright/dasel/v3/dencoding" -) - -func TestYAMLEncoder_Encode(t *testing.T) { - orig := dencoding.NewMap(). - Set("c", "x"). - Set("b", "y"). - Set("a", []any{"a", "c", "b"}) - - exp := `c: x -b: y -a: - - a - - c - - b -` - - gotBuffer := new(bytes.Buffer) - - encoder := dencoding.NewYAMLEncoder(gotBuffer, dencoding.YAMLEncodeIndent(2)) - if err := encoder.Encode(orig); err != nil { - t.Errorf("unexpected error: %v", err) - return - } - - got := gotBuffer.String() - - if exp != got { - t.Errorf("expected %s, got %s", exp, got) - } -} diff --git a/execution/execute_binary_test.go b/execution/execute_binary_test.go index 0d386a4f..33d43b61 100644 --- a/execution/execute_binary_test.go +++ b/execution/execute_binary_test.go @@ -3,8 +3,8 @@ package execution_test import ( "testing" - "github.com/tomwright/dasel/v3/dencoding" "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/model/orderedmap" ) func TestBinary(t *testing.T) { @@ -41,7 +41,7 @@ func TestBinary(t *testing.T) { }) t.Run("variables", func(t *testing.T) { in := func() *model.Value { - return model.NewValue(dencoding.NewMap(). + return model.NewValue(orderedmap.NewMap(). Set("one", 1). Set("two", 2). Set("three", 3). @@ -129,10 +129,10 @@ func TestBinary(t *testing.T) { t.Run("variables", func(t *testing.T) { in := func() *model.Value { - return model.NewValue(dencoding.NewMap(). + return model.NewValue(orderedmap.NewMap(). Set("one", 1). Set("two", 2). - Set("nested", dencoding.NewMap(). + Set("nested", orderedmap.NewMap(). Set("three", 3). Set("four", 4))) } diff --git a/execution/execute_map_test.go b/execution/execute_map_test.go index 2271abc8..58fd318d 100644 --- a/execution/execute_map_test.go +++ b/execution/execute_map_test.go @@ -3,18 +3,18 @@ package execution_test import ( "testing" - "github.com/tomwright/dasel/v3/dencoding" "github.com/tomwright/dasel/v3/internal/ptr" "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/model/orderedmap" ) func TestMap(t *testing.T) { t.Run("property from slice of maps", testCase{ inFn: func() *model.Value { return model.NewValue([]any{ - dencoding.NewMap().Set("number", 1), - dencoding.NewMap().Set("number", 2), - dencoding.NewMap().Set("number", 3), + orderedmap.NewMap().Set("number", 1), + orderedmap.NewMap().Set("number", 2), + orderedmap.NewMap().Set("number", 3), }) }, s: `map(number)`, @@ -25,9 +25,9 @@ func TestMap(t *testing.T) { t.Run("with chain of selectors", testCase{ inFn: func() *model.Value { return model.NewValue([]any{ - dencoding.NewMap().Set("foo", 1).Set("bar", 4), - dencoding.NewMap().Set("foo", 2).Set("bar", 5), - dencoding.NewMap().Set("foo", 3).Set("bar", 6), + orderedmap.NewMap().Set("foo", 1).Set("bar", 4), + orderedmap.NewMap().Set("foo", 2).Set("bar", 5), + orderedmap.NewMap().Set("foo", 3).Set("bar", 6), }) }, s: ` diff --git a/execution/execute_object_test.go b/execution/execute_object_test.go index 4189b86e..37edb2a5 100644 --- a/execution/execute_object_test.go +++ b/execution/execute_object_test.go @@ -3,16 +3,16 @@ package execution_test import ( "testing" - "github.com/tomwright/dasel/v3/dencoding" "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/model/orderedmap" ) func TestObject(t *testing.T) { inputMap := func() *model.Value { - return model.NewValue(dencoding.NewMap(). + return model.NewValue(orderedmap.NewMap(). Set("title", "Mr"). Set("age", int64(30)). - Set("name", dencoding.NewMap(). + Set("name", orderedmap.NewMap(). Set("first", "Tom"). Set("last", "Wright"))) } @@ -20,21 +20,14 @@ func TestObject(t *testing.T) { in: inputMap(), s: `{title}`, outFn: func() *model.Value { - return model.NewValue(dencoding.NewMap().Set("title", "Mr")) - //res := model.NewMapValue() - //_ = res.SetMapKey("title", model.NewStringValue("Mr")) - //return res + return model.NewValue(orderedmap.NewMap().Set("title", "Mr")) }, }.run) t.Run("get multiple", testCase{ in: inputMap(), s: `{title, age}`, outFn: func() *model.Value { - return model.NewValue(dencoding.NewMap().Set("title", "Mr").Set("age", int64(30))) - //res := model.NewMapValue() - //_ = res.SetMapKey("title", model.NewStringValue("Mr")) - //_ = res.SetMapKey("age", model.NewIntValue(30)) - //return res + return model.NewValue(orderedmap.NewMap().Set("title", "Mr").Set("age", int64(30))) }, }.run) t.Run("get with spread", testCase{ diff --git a/execution/execute_test.go b/execution/execute_test.go index 814e8a8c..77116989 100644 --- a/execution/execute_test.go +++ b/execution/execute_test.go @@ -4,9 +4,9 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/tomwright/dasel/v3/dencoding" "github.com/tomwright/dasel/v3/execution" "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/model/orderedmap" ) type testCase struct { @@ -59,10 +59,10 @@ func TestExecuteSelector_HappyPath(t *testing.T) { t.Run("get", func(t *testing.T) { inputMap := func() *model.Value { return model.NewValue( - dencoding.NewMap(). + orderedmap.NewMap(). Set("title", "Mr"). Set("age", int64(31)). - Set("name", dencoding.NewMap(). + Set("name", orderedmap.NewMap(). Set("first", "Tom"). Set("last", "Wright")), ) @@ -92,10 +92,10 @@ func TestExecuteSelector_HappyPath(t *testing.T) { s: `{..., "over30": age > 30}`, outFn: func() *model.Value { return model.NewValue( - dencoding.NewMap(). + orderedmap.NewMap(). Set("title", "Mr"). Set("age", int64(31)). - Set("name", dencoding.NewMap(). + Set("name", orderedmap.NewMap(). Set("first", "Tom"). Set("last", "Wright")). Set("over30", true), @@ -107,10 +107,10 @@ func TestExecuteSelector_HappyPath(t *testing.T) { t.Run("set", func(t *testing.T) { inputMap := func() *model.Value { return model.NewValue( - dencoding.NewMap(). + orderedmap.NewMap(). Set("title", "Mr"). Set("age", int64(31)). - Set("name", dencoding.NewMap(). + Set("name", orderedmap.NewMap(). Set("first", "Tom"). Set("last", "Wright")), ) diff --git a/execution/execute_unary_test.go b/execution/execute_unary_test.go index ce8764d1..68a80866 100644 --- a/execution/execute_unary_test.go +++ b/execution/execute_unary_test.go @@ -3,8 +3,8 @@ package execution_test import ( "testing" - "github.com/tomwright/dasel/v3/dencoding" "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/model/orderedmap" ) func TestUnary(t *testing.T) { @@ -37,7 +37,7 @@ func TestUnary(t *testing.T) { }) t.Run("variables", func(t *testing.T) { in := func() *model.Value { - return model.NewValue(dencoding.NewMap(). + return model.NewValue(orderedmap.NewMap(). Set("t", true). Set("f", false)) } diff --git a/execution/func_add_test.go b/execution/func_add_test.go index acb3613a..530e9303 100644 --- a/execution/func_add_test.go +++ b/execution/func_add_test.go @@ -3,8 +3,8 @@ package execution_test import ( "testing" - "github.com/tomwright/dasel/v3/dencoding" "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/model/orderedmap" ) func TestFuncAdd(t *testing.T) { @@ -22,8 +22,8 @@ func TestFuncAdd(t *testing.T) { }.run) t.Run("properties", func(t *testing.T) { in := func() *model.Value { - return model.NewValue(dencoding.NewMap(). - Set("numbers", dencoding.NewMap(). + return model.NewValue(orderedmap.NewMap(). + Set("numbers", orderedmap.NewMap(). Set("one", 1). Set("two", 2). Set("three", 3)). diff --git a/dencoding/map.go b/model/orderedmap/map.go similarity index 94% rename from dencoding/map.go rename to model/orderedmap/map.go index 10ed13d1..9feef834 100644 --- a/dencoding/map.go +++ b/model/orderedmap/map.go @@ -1,9 +1,15 @@ -package dencoding +package orderedmap import ( "reflect" ) +// KeyValue is a single key value pair from a *Map. +type KeyValue struct { + Key string + Value any +} + // NewMap returns a new *Map that has its values initialised. func NewMap() *Map { keys := make([]string, 0) diff --git a/model/value_map.go b/model/value_map.go index 67976cb0..09fd9d20 100644 --- a/model/value_map.go +++ b/model/value_map.go @@ -4,12 +4,12 @@ import ( "fmt" "reflect" - "github.com/tomwright/dasel/v3/dencoding" + "github.com/tomwright/dasel/v3/model/orderedmap" ) // NewMapValue creates a new map value. func NewMapValue() *Value { - return NewValue(dencoding.NewMap()) + return NewValue(orderedmap.NewMap()) } // IsMap returns true if the value is a map. @@ -22,16 +22,16 @@ func (v *Value) isStandardMap() bool { } func (v *Value) isDencodingMap() bool { - return v.UnpackKinds(reflect.Interface, reflect.Ptr).Value.Type() == reflect.TypeFor[dencoding.Map]() + return v.UnpackKinds(reflect.Interface, reflect.Ptr).Value.Type() == reflect.TypeFor[orderedmap.Map]() } -func (v *Value) dencodingMapValue() (*dencoding.Map, error) { +func (v *Value) dencodingMapValue() (*orderedmap.Map, error) { if v.isDencodingMap() { - m, err := v.UnpackUntilType(reflect.TypeFor[*dencoding.Map]()) + m, err := v.UnpackUntilType(reflect.TypeFor[*orderedmap.Map]()) if err != nil { return nil, fmt.Errorf("error getting map: %w", err) } - return m.Value.Interface().(*dencoding.Map), nil + return m.Value.Interface().(*orderedmap.Map), nil } return nil, fmt.Errorf("value is not a dencoding map") } diff --git a/model/value_map_test.go b/model/value_map_test.go index 4fc43f00..cb3f29f7 100644 --- a/model/value_map_test.go +++ b/model/value_map_test.go @@ -4,8 +4,8 @@ import ( "errors" "testing" - "github.com/tomwright/dasel/v3/dencoding" "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/model/orderedmap" ) func TestMap(t *testing.T) { @@ -17,7 +17,7 @@ func TestMap(t *testing.T) { } dencodingMap := func() *model.Value { - return model.NewValue(dencoding.NewMap(). + return model.NewValue(orderedmap.NewMap(). Set("foo", "foo1"). Set("bar", "bar1")) } From 28d9744c097546014190075f130d006b4e43dd13 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Thu, 24 Oct 2024 20:07:53 +0100 Subject: [PATCH 41/56] Fix default command --- internal/cli/command.go | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/internal/cli/command.go b/internal/cli/command.go index b0621371..7ba2fb13 100644 --- a/internal/cli/command.go +++ b/internal/cli/command.go @@ -17,7 +17,7 @@ type Globals struct { type CLI struct { Globals - Query QueryCmd `cmd:"" help:"[default] Execute a query"` + Query QueryCmd `cmd:"" default:"withargs" help:"[default] Execute a query"` Version VersionCmd `cmd:"" help:"Print the version"` } @@ -51,16 +51,6 @@ func Run(stdin io.Reader, stdout, stderr io.Writer) (*kong.Context, error) { k.Stderr = cli.Stderr return nil }), - kong.PostBuild(func(k *kong.Kong) error { - defaultCommandName := "query" - for _, c := range k.Model.Children { - if c.Name == defaultCommandName { - k.Model.DefaultCmd = c - break - } - } - return nil - }), ) err := ctx.Run() return ctx, err From 31c8096e7f8bf4898708167489c98cd48774aeb4 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Thu, 24 Oct 2024 20:10:14 +0100 Subject: [PATCH 42/56] Remove unused code --- internal/util/to_string.go | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 internal/util/to_string.go diff --git a/internal/util/to_string.go b/internal/util/to_string.go deleted file mode 100644 index 04958ec8..00000000 --- a/internal/util/to_string.go +++ /dev/null @@ -1,26 +0,0 @@ -package util - -import ( - "fmt" -) - -// ToString converts the given value to a string. -func ToString(value any) string { - switch v := value.(type) { - case nil: - return "null" - case string: - return v - case []byte: - return string(v) - case int, int8, int16, int32, int64, - uint, uint8, uint16, uint32, uint64: - return fmt.Sprintf("%d", v) - case float32, float64: - return fmt.Sprintf("%f", v) - case bool: - return fmt.Sprint(v) - default: - return fmt.Sprint(v) - } -} From 99a9e8bf965e305bcc2ec39d899b8975af52b866 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 24 Oct 2024 23:05:45 +0100 Subject: [PATCH 43/56] Fix CLI output and improve branching capability --- execution/execute.go | 31 +++++-- execution/execute_binary.go | 147 ++++++++++++++++++++++--------- execution/execute_branch_test.go | 51 +++++++++++ internal/cli/query.go | 33 ++++--- 4 files changed, 204 insertions(+), 58 deletions(-) diff --git a/execution/execute.go b/execution/execute.go index 854db706..4c8105e6 100644 --- a/execution/execute.go +++ b/execution/execute.go @@ -51,8 +51,8 @@ func ExecuteAST(expr ast.Expr, value *model.Value, options *Options) (*model.Val res := model.NewSliceValue() res.MarkAsBranch() - if err := value.RangeSlice(func(i int, value *model.Value) error { - r, err := executor(value) + if err := value.RangeSlice(func(i int, v *model.Value) error { + r, err := executor(v) if err != nil { return err } @@ -123,9 +123,30 @@ func exprExecutor(opts *Options, expr ast.Expr) (expressionExecutor, error) { func chainedExprExecutor(options *Options, e ast.ChainedExpr) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { for _, expr := range e.Exprs { - res, err := ExecuteAST(expr, data, options) - if err != nil { - return nil, fmt.Errorf("error executing expression: %w", err) + + if !data.IsBranch() { + res, err := ExecuteAST(expr, data, options) + if err != nil { + return nil, fmt.Errorf("error executing expression: %w", err) + } + data = res + continue + } + + res := model.NewSliceValue() + res.MarkAsBranch() + if err := data.RangeSlice(func(i int, value *model.Value) error { + r, err := ExecuteAST(expr, value, options) + if err != nil { + return fmt.Errorf("error executing expression: %w", err) + } + + if err := res.Append(r); err != nil { + return err + } + return nil + }); err != nil { + return nil, err } data = res } diff --git a/execution/execute_binary.go b/execution/execute_binary.go index 18c88ae7..551b0340 100644 --- a/execution/execute_binary.go +++ b/execution/execute_binary.go @@ -14,73 +14,134 @@ func binaryExprExecutor(opts *Options, e ast.BinaryExpr) (expressionExecutor, er if err != nil { return nil, fmt.Errorf("error evaluating left expression: %w", err) } - right, err := ExecuteAST(e.Right, data, opts) - if err != nil { - return nil, fmt.Errorf("error evaluating right expression: %w", err) - } + + var doOperation func(a *model.Value, b *model.Value) (*model.Value, error) switch e.Operator.Kind { case lexer.Plus: - return left.Add(right) + doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { + return a.Add(b) + } case lexer.Dash: - return left.Subtract(right) + doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { + return a.Subtract(b) + } case lexer.Star: - return left.Multiply(right) + doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { + return a.Multiply(b) + } case lexer.Slash: - return left.Divide(right) + doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { + return a.Divide(b) + } case lexer.Percent: - return left.Modulo(right) + doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { + return a.Modulo(b) + } case lexer.GreaterThan: - return left.GreaterThan(right) + doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { + return a.GreaterThan(b) + } case lexer.GreaterThanOrEqual: - return left.GreaterThanOrEqual(right) + doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { + return a.GreaterThanOrEqual(b) + } case lexer.LessThan: - return left.LessThan(right) + doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { + return a.LessThan(b) + } case lexer.LessThanOrEqual: - return left.LessThanOrEqual(right) + doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { + return a.LessThanOrEqual(b) + } case lexer.Equal: - return left.Equal(right) + doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { + return a.Equal(b) + } case lexer.NotEqual: - return left.NotEqual(right) + doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { + return a.NotEqual(b) + } case lexer.Equals: - err := left.Set(right) - return right, err - case lexer.And: - leftBool, err := left.BoolValue() - if err != nil { - return nil, fmt.Errorf("error getting left bool value: %w", err) + doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { + err := a.Set(b) + return b, err } - rightBool, err := right.BoolValue() - if err != nil { - return nil, fmt.Errorf("error getting right bool value: %w", err) + case lexer.And: + doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { + leftBool, err := a.BoolValue() + if err != nil { + return nil, fmt.Errorf("error getting left bool value: %w", err) + } + rightBool, err := b.BoolValue() + if err != nil { + return nil, fmt.Errorf("error getting right bool value: %w", err) + } + return model.NewBoolValue(leftBool && rightBool), nil } - return model.NewBoolValue(leftBool && rightBool), nil case lexer.Or: - leftBool, err := left.BoolValue() - if err != nil { - return nil, fmt.Errorf("error getting left bool value: %w", err) + doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { + leftBool, err := a.BoolValue() + if err != nil { + return nil, fmt.Errorf("error getting left bool value: %w", err) + } + rightBool, err := b.BoolValue() + if err != nil { + return nil, fmt.Errorf("error getting right bool value: %w", err) + } + return model.NewBoolValue(leftBool || rightBool), nil } - rightBool, err := right.BoolValue() + case lexer.Like, lexer.NotLike: + doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { + leftStr, err := a.StringValue() + if err != nil { + return nil, fmt.Errorf("like requires left side to be a string, got %s", left.Type().String()) + } + rightPatt, ok := e.Right.(ast.RegexExpr) + if !ok { + return nil, fmt.Errorf("like requires right side to be a regex pattern") + } + res := rightPatt.Regex.MatchString(leftStr) + if e.Operator.Kind == lexer.NotLike { + res = !res + } + return model.NewBoolValue(res), nil + } + default: + return nil, fmt.Errorf("unhandled operator: %s", e.Operator.Value) + } + + if doOperation == nil { + return nil, fmt.Errorf("missing operation for operator %s", e.Operator.Value) + } + + if !left.IsBranch() { + right, err := ExecuteAST(e.Right, data, opts) if err != nil { - return nil, fmt.Errorf("error getting right bool value: %w", err) + return nil, fmt.Errorf("error evaluating right expression: %w", err) } - return model.NewBoolValue(leftBool || rightBool), nil - case lexer.Like, lexer.NotLike: - leftStr, err := left.StringValue() + return doOperation(left, right) + } + + res := model.NewSliceValue() + res.MarkAsBranch() + if err := left.RangeSlice(func(i int, v *model.Value) error { + right, err := ExecuteAST(e.Right, v, opts) if err != nil { - return nil, fmt.Errorf("like requires left side to be a string, got %s", left.Type().String()) + return fmt.Errorf("error evaluating right expression: %w", err) } - rightPatt, ok := e.Right.(ast.RegexExpr) - if !ok { - return nil, fmt.Errorf("like requires right side to be a regex pattern") + + r, err := doOperation(v, right) + if err != nil { + return err } - res := rightPatt.Regex.MatchString(leftStr) - if e.Operator.Kind == lexer.NotLike { - res = !res + if err := res.Append(r); err != nil { + return err } - return model.NewBoolValue(res), nil - default: - return nil, fmt.Errorf("unhandled operator: %s", e.Operator.Value) + return nil + }); err != nil { + return nil, err } + return res, nil }, nil } diff --git a/execution/execute_branch_test.go b/execution/execute_branch_test.go index 6d33b8ad..2077b2bc 100644 --- a/execution/execute_branch_test.go +++ b/execution/execute_branch_test.go @@ -55,4 +55,55 @@ func TestBranch(t *testing.T) { return r }, }.run) + t.Run("chained branch set", testCase{ + s: "branch(1, 2, 3).$this=5", + outFn: func() *model.Value { + r := model.NewSliceValue() + r.MarkAsBranch() + if err := r.Append(model.NewIntValue(5)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := r.Append(model.NewIntValue(5)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := r.Append(model.NewIntValue(5)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + return r + }, + }.run) + t.Run("chained branch math", testCase{ + s: "branch(1, 2, 3) * 2", + outFn: func() *model.Value { + r := model.NewSliceValue() + r.MarkAsBranch() + if err := r.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := r.Append(model.NewIntValue(4)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := r.Append(model.NewIntValue(6)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + return r + }, + }.run) + t.Run("chained branch math using branched value", testCase{ + s: `branch({"x":1}, {"x":2}, {"x":3}).x * $this`, + 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(4)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := r.Append(model.NewIntValue(9)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + return r + }, + }.run) } diff --git a/internal/cli/query.go b/internal/cli/query.go index c70c7f62..cea203b6 100644 --- a/internal/cli/query.go +++ b/internal/cli/query.go @@ -2,6 +2,7 @@ package cli import ( "fmt" + "github.com/tomwright/dasel/v3" "io" "github.com/tomwright/dasel/v3/execution" @@ -70,25 +71,37 @@ func (c *QueryCmd) Run(ctx *Globals) error { opts = append(opts, execution.WithVariable("root", inputData)) - options := execution.NewOptions(opts...) - - outputData, err := execution.ExecuteSelector(c.Query, inputData, options) + out, num, err := dasel.Query(inputData, c.Query, opts...) if err != nil { return err } if c.ReturnRoot { - outputData = inputData + outputBytes, err := writer.Write(inputData) + if err != nil { + return fmt.Errorf("error writing output: %w", err) + } + + _, err = ctx.Stdout.Write(outputBytes) + if err != nil { + return fmt.Errorf("error writing output: %w", err) + } } - outputBytes, err := writer.Write(outputData) - if err != nil { - return fmt.Errorf("error writing output: %w", err) + if num == 0 { + return nil } - _, err = ctx.Stdout.Write(outputBytes) - if err != nil { - return fmt.Errorf("error writing output: %w", err) + for _, o := range out { + outputBytes, err := writer.Write(o) + if err != nil { + return fmt.Errorf("error writing output: %w", err) + } + + _, err = ctx.Stdout.Write(outputBytes) + if err != nil { + return fmt.Errorf("error writing output: %w", err) + } } return nil From 0ffe7cc821fd169c9bc8432059f4598eb7e6e629 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 24 Oct 2024 23:10:02 +0100 Subject: [PATCH 44/56] Fix --root flag --- internal/cli/query.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/cli/query.go b/internal/cli/query.go index cea203b6..d556e009 100644 --- a/internal/cli/query.go +++ b/internal/cli/query.go @@ -86,6 +86,7 @@ func (c *QueryCmd) Run(ctx *Globals) error { if err != nil { return fmt.Errorf("error writing output: %w", err) } + return nil } if num == 0 { From ba0aa78a70276582f69ae42869088accb3542644 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 24 Oct 2024 23:45:38 +0100 Subject: [PATCH 45/56] Add mix, max and fix IsNull issue --- cmd/dasel/main.go | 8 +++++++- execution/func.go | 2 ++ execution/func_max.go | 32 ++++++++++++++++++++++++++++++++ execution/func_max_test.go | 22 ++++++++++++++++++++++ execution/func_min.go | 32 ++++++++++++++++++++++++++++++++ execution/func_min_test.go | 22 ++++++++++++++++++++++ model/value.go | 15 +++++++++++++++ model/value_literal.go | 7 ++++++- 8 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 execution/func_max.go create mode 100644 execution/func_max_test.go create mode 100644 execution/func_min.go create mode 100644 execution/func_min_test.go diff --git a/cmd/dasel/main.go b/cmd/dasel/main.go index 651331a0..0dc83463 100644 --- a/cmd/dasel/main.go +++ b/cmd/dasel/main.go @@ -1,6 +1,7 @@ package main import ( + "io" "os" "github.com/tomwright/dasel/v3/internal/cli" @@ -11,5 +12,10 @@ import ( ) func main() { - cli.MustRun(os.Stdin, os.Stdout, os.Stderr) + var stdin io.Reader = os.Stdin + fi, err := os.Stdin.Stat() + if err != nil || (fi.Mode()&os.ModeNamedPipe == 0) { + stdin = nil + } + cli.MustRun(stdin, os.Stdout, os.Stderr) } diff --git a/execution/func.go b/execution/func.go index 5d8e7495..504a8af8 100644 --- a/execution/func.go +++ b/execution/func.go @@ -15,6 +15,8 @@ var ( FuncMerge, FuncReverse, FuncTypeOf, + FuncMax, + FuncMin, ) ) diff --git a/execution/func_max.go b/execution/func_max.go new file mode 100644 index 00000000..9de230c4 --- /dev/null +++ b/execution/func_max.go @@ -0,0 +1,32 @@ +package execution + +import ( + "github.com/tomwright/dasel/v3/model" +) + +// FuncMax is a function that returns the highest number. +var FuncMax = NewFunc( + "max", + func(data *model.Value, args model.Values) (*model.Value, error) { + res := model.NewNullValue() + for _, arg := range args { + if res.IsNull() { + res = arg + continue + } + gt, err := arg.GreaterThan(res) + if err != nil { + return nil, err + } + gtBool, err := gt.BoolValue() + if err != nil { + return nil, err + } + if gtBool { + res = arg + } + } + return res, nil + }, + ValidateArgsMin(1), +) diff --git a/execution/func_max_test.go b/execution/func_max_test.go new file mode 100644 index 00000000..d006368c --- /dev/null +++ b/execution/func_max_test.go @@ -0,0 +1,22 @@ +package execution_test + +import ( + "testing" + + "github.com/tomwright/dasel/v3/model" +) + +func TestFuncMax(t *testing.T) { + t.Run("int", testCase{ + s: `max(1, 2, 3)`, + out: model.NewIntValue(3), + }.run) + t.Run("float", testCase{ + s: `max(1f, 2.5, 3.5)`, + out: model.NewFloatValue(3.5), + }.run) + t.Run("mixed", testCase{ + s: `max(1, 2f)`, + out: model.NewFloatValue(2), + }.run) +} diff --git a/execution/func_min.go b/execution/func_min.go new file mode 100644 index 00000000..e45af420 --- /dev/null +++ b/execution/func_min.go @@ -0,0 +1,32 @@ +package execution + +import ( + "github.com/tomwright/dasel/v3/model" +) + +// FuncMin is a function that returns the smalled number. +var FuncMin = NewFunc( + "min", + func(data *model.Value, args model.Values) (*model.Value, error) { + res := model.NewNullValue() + for _, arg := range args { + if res.IsNull() { + res = arg + continue + } + lt, err := arg.LessThan(res) + if err != nil { + return nil, err + } + ltBool, err := lt.BoolValue() + if err != nil { + return nil, err + } + if ltBool { + res = arg + } + } + return res, nil + }, + ValidateArgsMin(1), +) diff --git a/execution/func_min_test.go b/execution/func_min_test.go new file mode 100644 index 00000000..74f54bea --- /dev/null +++ b/execution/func_min_test.go @@ -0,0 +1,22 @@ +package execution_test + +import ( + "testing" + + "github.com/tomwright/dasel/v3/model" +) + +func TestFuncMin(t *testing.T) { + t.Run("int", testCase{ + s: `min(1, 2, 3)`, + out: model.NewIntValue(1), + }.run) + t.Run("float", testCase{ + s: `min(1f, 2.5, 3.5)`, + out: model.NewFloatValue(1), + }.run) + t.Run("mixed", testCase{ + s: `min(1, 2f)`, + out: model.NewIntValue(1), + }.run) +} diff --git a/model/value.go b/model/value.go index 82b9205a..623edc92 100644 --- a/model/value.go +++ b/model/value.go @@ -128,6 +128,21 @@ func (v *Value) UnpackUntilKind(k reflect.Kind) (*Value, error) { } } +// UnpackUntilKinds unpacks the reflect value until it matches the given kind. +func (v *Value) UnpackUntilKinds(kinds ...reflect.Kind) (*Value, error) { + res := v.Value + for { + if slices.Contains(kinds, res.Kind()) { + return NewValue(res), nil + } + if res.Kind() == reflect.Interface || res.Kind() == reflect.Ptr && !res.IsNil() { + res = res.Elem() + continue + } + return nil, fmt.Errorf("could not unpack to kinds: %v", kinds) + } +} + // Type returns the type of the value. func (v *Value) Type() Type { switch { diff --git a/model/value_literal.go b/model/value_literal.go index 7b7d079e..fd3a1bdb 100644 --- a/model/value_literal.go +++ b/model/value_literal.go @@ -20,7 +20,12 @@ func (v *Value) IsNull() bool { } func (v *Value) isNull() bool { - return v.Value.IsNil() + // This logic can be cleaned up. + unpacked, err := v.UnpackUntilKinds(reflect.Chan, reflect.Func, reflect.Map, reflect.Pointer, reflect.UnsafePointer, reflect.Interface, reflect.Slice) + if err != nil { + return false + } + return unpacked.Value.IsNil() } // NewStringValue creates a new Value with a string value. From 3ac9c8bc4174656831ec2e7c4e6fcfbf98ffa5e3 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Sun, 27 Oct 2024 16:47:22 +0000 Subject: [PATCH 46/56] Reimplement yaml parser --- model/value.go | 9 +- parsing/yaml/yaml.go | 242 ++++++++++++++++++++++++++++++++++++-- parsing/yaml/yaml_test.go | 186 +++++++++++++++++++++++++++++ 3 files changed, 427 insertions(+), 10 deletions(-) create mode 100644 parsing/yaml/yaml_test.go diff --git a/model/value.go b/model/value.go index 623edc92..40cb8731 100644 --- a/model/value.go +++ b/model/value.go @@ -47,7 +47,8 @@ func NewValue(v any) *Value { return val case reflect.Value: return &Value{ - Value: val, + Value: val, + Metadata: make(map[string]any), } case nil: return NewNullValue() @@ -57,13 +58,17 @@ func NewValue(v any) *Value { res.Elem().Set(reflect.ValueOf(v)) } return &Value{ - Value: res, + Value: res, + Metadata: make(map[string]any), } } } // Interface returns the value as an interface. func (v *Value) Interface() any { + if v.IsNull() { + return nil + } return v.Value.Interface() } diff --git a/parsing/yaml/yaml.go b/parsing/yaml/yaml.go index 8f9e4337..ce539d80 100644 --- a/parsing/yaml/yaml.go +++ b/parsing/yaml/yaml.go @@ -2,6 +2,9 @@ package yaml import ( "bytes" + "fmt" + "io" + "strconv" "github.com/tomwright/dasel/v3/model" "github.com/tomwright/dasel/v3/parsing" @@ -32,21 +35,244 @@ type yamlReader struct{} // Read reads a value from a byte slice. func (j *yamlReader) Read(data []byte) (*model.Value, error) { d := yaml.NewDecoder(bytes.NewReader(data)) - var unmarshalled any - if err := d.Decode(&unmarshalled); err != nil { - return nil, err + res := make([]*yamlValue, 0) + for { + unmarshalled := &yamlValue{} + if err := d.Decode(&unmarshalled); err != nil { + if err == io.EOF { + break + } + return nil, err + } + res = append(res, unmarshalled) + } + + switch len(res) { + case 0: + return model.NewNullValue(), nil + case 1: + return res[0].value, nil + default: + slice := model.NewSliceValue() + slice.MarkAsBranch() + for _, v := range res { + if err := slice.Append(v.value); err != nil { + return nil, err + } + } + return slice, nil } - return model.NewValue(&unmarshalled), nil } type yamlWriter struct{} // Write writes a value to a byte slice. func (j *yamlWriter) Write(value *model.Value) ([]byte, error) { - buf := new(bytes.Buffer) - e := yaml.NewEncoder(buf) - if err := e.Encode(value.Interface()); err != nil { + if value.IsBranch() { + res := make([]byte, 0) + sliceLen, err := value.SliceLen() + if err != nil { + return nil, err + } + if err := value.RangeSlice(func(i int, val *model.Value) error { + yv := &yamlValue{value: val} + marshalled, err := yaml.Marshal(yv) + if err != nil { + return err + } + res = append(res, marshalled...) + if i < sliceLen-1 { + res = append(res, []byte("---\n")...) + } + return nil + }); err != nil { + return nil, err + } + return res, nil + } + + yv := &yamlValue{value: value} + res, err := yv.ToNode() + if err != nil { + return nil, err + } + return yaml.Marshal(res) +} + +type yamlValue struct { + node *yaml.Node + value *model.Value +} + +func (yv *yamlValue) UnmarshalYAML(value *yaml.Node) error { + yv.node = value + switch value.Kind { + case yaml.ScalarNode: + switch value.Tag { + case "!!bool": + yv.value = model.NewBoolValue(value.Value == "true") + case "!!int": + i, err := strconv.Atoi(value.Value) + if err != nil { + return err + } + yv.value = model.NewIntValue(int64(i)) + case "!!float": + f, err := strconv.ParseFloat(value.Value, 64) + if err != nil { + return err + } + yv.value = model.NewFloatValue(f) + default: + yv.value = model.NewStringValue(value.Value) + } + case yaml.DocumentNode: + yv.value = model.NewNullValue() + case yaml.SequenceNode: + res := model.NewSliceValue() + for _, item := range value.Content { + newItem := &yamlValue{} + if err := newItem.UnmarshalYAML(item); err != nil { + return err + } + if err := res.Append(newItem.value); err != nil { + return err + } + } + yv.value = res + case yaml.MappingNode: + res := model.NewMapValue() + for i := 0; i < len(value.Content); i += 2 { + key := value.Content[i] + val := value.Content[i+1] + + newKey := &yamlValue{} + if err := newKey.UnmarshalYAML(key); err != nil { + return err + } + + newVal := &yamlValue{} + if err := newVal.UnmarshalYAML(val); err != nil { + return err + } + + keyStr, err := newKey.value.StringValue() + if err != nil { + return fmt.Errorf("keys are expected to be strings: %w", err) + } + + if err := res.SetMapKey(keyStr, newVal.value); err != nil { + return err + } + } + yv.value = res + case yaml.AliasNode: + newVal := &yamlValue{} + if err := newVal.UnmarshalYAML(value.Alias); err != nil { + return err + } + yv.value = newVal.value + yv.value.Metadata["yaml-alias"] = value.Value + } + return nil +} + +func (yv *yamlValue) ToNode() (*yaml.Node, error) { + res := &yaml.Node{} + + yamlAlias, ok := yv.value.Metadata["yaml-alias"].(string) + if ok { + //res.Kind = yaml.ScalarNode + res.Kind = yaml.AliasNode + res.Value = yamlAlias + //res.Alias = &yaml.Node{ + // Kind: yaml.ScalarNode, + // Value: yamlAlias, + //} + return res, nil + } + + switch yv.value.Type() { + case model.TypeString: + v, err := yv.value.StringValue() + if err != nil { + return nil, err + } + res.Kind = yaml.ScalarNode + res.Value = v + res.Tag = "!!str" + case model.TypeBool: + v, err := yv.value.BoolValue() + if err != nil { + return nil, err + } + res.Kind = yaml.ScalarNode + res.Value = fmt.Sprintf("%t", v) + res.Tag = "!!bool" + case model.TypeInt: + v, err := yv.value.IntValue() + if err != nil { + return nil, err + } + res.Kind = yaml.ScalarNode + res.Value = fmt.Sprintf("%d", v) + res.Tag = "!!int" + case model.TypeFloat: + v, err := yv.value.FloatValue() + if err != nil { + return nil, err + } + res.Kind = yaml.ScalarNode + res.Value = fmt.Sprintf("%g", v) + res.Tag = "!!float" + case model.TypeMap: + res.Kind = yaml.MappingNode + if err := yv.value.RangeMap(func(key string, val *model.Value) error { + keyNode := &yamlValue{value: model.NewStringValue(key)} + valNode := &yamlValue{value: val} + + marshalledKey, err := keyNode.ToNode() + if err != nil { + return err + } + marshalledVal, err := valNode.ToNode() + if err != nil { + return err + } + + res.Content = append(res.Content, marshalledKey) + res.Content = append(res.Content, marshalledVal) + + return nil + }); err != nil { + return nil, err + } + case model.TypeSlice: + res.Kind = yaml.SequenceNode + if err := yv.value.RangeSlice(func(i int, val *model.Value) error { + valNode := &yamlValue{value: val} + marshalledVal, err := valNode.ToNode() + if err != nil { + return err + } + res.Content = append(res.Content, marshalledVal) + return nil + }); err != nil { + return nil, err + } + case model.TypeNull: + res.Kind = yaml.DocumentNode + case model.TypeUnknown: + return nil, fmt.Errorf("unknown type: %s", yv.value.Type()) + } + + return res, nil +} + +func (yv *yamlValue) MarshalYAML() (any, error) { + res, err := yv.ToNode() + if err != nil { return nil, err } - return buf.Bytes(), nil + return res, nil } diff --git a/parsing/yaml/yaml_test.go b/parsing/yaml/yaml_test.go new file mode 100644 index 00000000..264e8abf --- /dev/null +++ b/parsing/yaml/yaml_test.go @@ -0,0 +1,186 @@ +package yaml_test + +import ( + "bytes" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/parsing" + "github.com/tomwright/dasel/v3/parsing/yaml" +) + +type testCase struct { + in string + assert func(t *testing.T, res *model.Value) +} + +func (tc testCase) run(t *testing.T) { + r, err := yaml.YAML.NewReader() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + res, err := r.Read([]byte(tc.in)) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + tc.assert(t, res) +} + +type rwTestCase struct { + in string + out string +} + +func (tc rwTestCase) run(t *testing.T) { + if tc.out == "" { + tc.out = tc.in + } + r, err := yaml.YAML.NewReader() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + w, err := yaml.YAML.NewWriter(parsing.WriterOptions{}) + res, err := r.Read([]byte(tc.in)) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + out, err := w.Write(res) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !bytes.Equal([]byte(tc.out), out) { + t.Errorf("unexpected output: %s", cmp.Diff(tc.out, string(out))) + } +} + +func TestYamlValue_UnmarshalYAML(t *testing.T) { + t.Run("simple key value", testCase{ + in: `name: Tom`, + assert: func(t *testing.T, res *model.Value) { + got, err := res.GetMapKey("name") + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + gotStr, err := got.StringValue() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if gotStr != "Tom" { + t.Errorf("unexpected value: %s", gotStr) + } + }, + }.run) + + t.Run("multi document", testCase{ + in: `name: Tom +--- +name: Jerry`, + assert: func(t *testing.T, res *model.Value) { + a, err := res.GetSliceIndex(0) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + got, err := a.GetMapKey("name") + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + gotStr, err := got.StringValue() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + if gotStr != "Tom" { + t.Errorf("unexpected value: %s", gotStr) + } + + b, err := res.GetSliceIndex(1) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + got, err = b.GetMapKey("name") + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + gotStr, err = got.StringValue() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + if gotStr != "Jerry" { + t.Errorf("unexpected value: %s", gotStr) + } + }, + }.run) + + t.Run("multi document", testCase{ + in: `name: Tom +--- +name: Jerry`, + assert: func(t *testing.T, res *model.Value) { + a, err := res.GetSliceIndex(0) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + got, err := a.GetMapKey("name") + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + gotStr, err := got.StringValue() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + if gotStr != "Tom" { + t.Errorf("unexpected value: %s", gotStr) + } + + b, err := res.GetSliceIndex(1) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + got, err = b.GetMapKey("name") + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + gotStr, err = got.StringValue() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + if gotStr != "Jerry" { + t.Errorf("unexpected value: %s", gotStr) + } + }, + }.run) + + t.Run("multi document", rwTestCase{ + in: `name: Tom +--- +name: Jerry +`, + }.run) + + t.Run("generic", rwTestCase{ + in: `str: foo +int: 1 +float: 1.1 +bool: true +map: + key: value +list: + - item1 + - item2 +`, + }.run) + + // This test is technically wrong because we're only supporting the alias on read and not write. + t.Run("alias", rwTestCase{ + in: `name: &name Tom +name2: *name +`, + out: `name: Tom +name2: Tom +`, + }.run) +} From 4ef342cc8f9df1eea5946b133f480d782c64f551 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Sun, 27 Oct 2024 22:20:47 +0000 Subject: [PATCH 47/56] Start implementing CSV parser --- cmd/dasel/main.go | 1 + execution/execute_binary.go | 12 ++- execution/execute_branch.go | 38 +++++---- internal/cli/query.go | 33 +++----- model/value.go | 10 +++ parsing/json/json.go | 26 ++++-- parsing/xml/xml.go | 156 ++++++++++++++++++++++++++++++++++++ parsing/xml/xml_test.go | 78 ++++++++++++++++++ 8 files changed, 310 insertions(+), 44 deletions(-) create mode 100644 parsing/xml/xml.go create mode 100644 parsing/xml/xml_test.go diff --git a/cmd/dasel/main.go b/cmd/dasel/main.go index 0dc83463..88fd0478 100644 --- a/cmd/dasel/main.go +++ b/cmd/dasel/main.go @@ -8,6 +8,7 @@ import ( _ "github.com/tomwright/dasel/v3/parsing/d" _ "github.com/tomwright/dasel/v3/parsing/json" _ "github.com/tomwright/dasel/v3/parsing/toml" + _ "github.com/tomwright/dasel/v3/parsing/xml" _ "github.com/tomwright/dasel/v3/parsing/yaml" ) diff --git a/execution/execute_binary.go b/execution/execute_binary.go index 551b0340..8e218d27 100644 --- a/execution/execute_binary.go +++ b/execution/execute_binary.go @@ -65,7 +65,17 @@ func binaryExprExecutor(opts *Options, e ast.BinaryExpr) (expressionExecutor, er case lexer.Equals: doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { err := a.Set(b) - return b, err + if err != nil { + return nil, fmt.Errorf("error setting value: %w", err) + } + switch a.Type() { + case model.TypeMap: + return a, nil + case model.TypeSlice: + return a, nil + default: + return b, nil + } } case lexer.And: doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { diff --git a/execution/execute_branch.go b/execution/execute_branch.go index 86699c14..95dad602 100644 --- a/execution/execute_branch.go +++ b/execution/execute_branch.go @@ -10,27 +10,37 @@ import ( func branchExprExecutor(opts *Options, e ast.BranchExpr) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { res := model.NewSliceValue() + res.MarkAsBranch() - for _, expr := range e.Exprs { - r, err := ExecuteAST(expr, data, opts) - if err != nil { - return nil, fmt.Errorf("failed to execute branch expr: %w", err) + if len(e.Exprs) == 0 { + if err := data.RangeSlice(func(_ int, value *model.Value) error { + if err := res.Append(value); err != nil { + return fmt.Errorf("failed to append branch result: %w", err) + } + return nil + }); err != nil { + return nil, fmt.Errorf("failed to range slice: %w", err) } + } else { + for _, expr := range e.Exprs { + r, err := ExecuteAST(expr, data, opts) + if err != nil { + return nil, fmt.Errorf("failed to execute branch expr: %w", err) + } - // This deals with the spread operator in the branch expression. - valsToAppend, err := prepareSpreadValues(r) - if err != nil { - return nil, fmt.Errorf("error handling spread values: %w", err) - } - for _, v := range valsToAppend { - if err := res.Append(v); err != nil { - return nil, fmt.Errorf("failed to append branch result: %w", err) + // This deals with the spread operator in the branch expression. + valsToAppend, err := prepareSpreadValues(r) + if err != nil { + return nil, fmt.Errorf("error handling spread values: %w", err) + } + for _, v := range valsToAppend { + if err := res.Append(v); err != nil { + return nil, fmt.Errorf("failed to append branch result: %w", err) + } } } } - res.MarkAsBranch() - return res, nil }, nil } diff --git a/internal/cli/query.go b/internal/cli/query.go index d556e009..979f141d 100644 --- a/internal/cli/query.go +++ b/internal/cli/query.go @@ -2,7 +2,6 @@ package cli import ( "fmt" - "github.com/tomwright/dasel/v3" "io" "github.com/tomwright/dasel/v3/execution" @@ -71,38 +70,24 @@ func (c *QueryCmd) Run(ctx *Globals) error { opts = append(opts, execution.WithVariable("root", inputData)) - out, num, err := dasel.Query(inputData, c.Query, opts...) + options := execution.NewOptions(opts...) + out, err := execution.ExecuteSelector(c.Query, inputData, options) if err != nil { return err } if c.ReturnRoot { - outputBytes, err := writer.Write(inputData) - if err != nil { - return fmt.Errorf("error writing output: %w", err) - } - - _, err = ctx.Stdout.Write(outputBytes) - if err != nil { - return fmt.Errorf("error writing output: %w", err) - } - return nil + out = inputData } - if num == 0 { - return nil + outputBytes, err := writer.Write(out) + if err != nil { + return fmt.Errorf("error writing output: %w", err) } - for _, o := range out { - outputBytes, err := writer.Write(o) - if err != nil { - return fmt.Errorf("error writing output: %w", err) - } - - _, err = ctx.Stdout.Write(outputBytes) - if err != nil { - return fmt.Errorf("error writing output: %w", err) - } + _, err = ctx.Stdout.Write(outputBytes) + if err != nil { + return fmt.Errorf("error writing output: %w", err) } return nil diff --git a/model/value.go b/model/value.go index 40cb8731..c01653f2 100644 --- a/model/value.go +++ b/model/value.go @@ -32,6 +32,16 @@ type KeyValue struct { // Values represents a list of values. type Values []*Value +func (v Values) ToSliceValue() (*Value, error) { + slice := NewSliceValue() + for _, val := range v { + if err := slice.Append(val); err != nil { + return nil, err + } + } + return slice, nil +} + // Value represents a value. type Value struct { Value reflect.Value diff --git a/parsing/json/json.go b/parsing/json/json.go index bcd33a16..43e3d8e9 100644 --- a/parsing/json/json.go +++ b/parsing/json/json.go @@ -220,12 +220,28 @@ func (j *jsonWriter) Write(value *model.Value) ([]byte, error) { return err } - if err := j.write(buf, encoderFn, es, value); err != nil { - return nil, err - } + if value.IsBranch() { + if err := value.RangeSlice(func(i int, v *model.Value) error { + if err := j.write(buf, encoderFn, es, v); err != nil { + return err + } - if _, err := buf.Write([]byte("\n")); err != nil { - return nil, err + if _, err := buf.Write([]byte("\n")); err != nil { + return err + } + + return nil + }); err != nil { + return nil, err + } + } else { + if err := j.write(buf, encoderFn, es, value); err != nil { + return nil, err + } + + if _, err := buf.Write([]byte("\n")); err != nil { + return nil, err + } } return buf.Bytes(), nil diff --git a/parsing/xml/xml.go b/parsing/xml/xml.go new file mode 100644 index 00000000..b7302b46 --- /dev/null +++ b/parsing/xml/xml.go @@ -0,0 +1,156 @@ +package xml + +import ( + "bytes" + "encoding/xml" + "errors" + "fmt" + "io" + "unicode" + + "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/parsing" +) + +const ( + // XML represents the XML file format. + XML parsing.Format = "xml" +) + +var _ parsing.Reader = (*xmlReader)(nil) +var _ parsing.Writer = (*xmlWriter)(nil) + +//var _ parsing.Writer = (*xmlWriter)(nil) + +func init() { + parsing.RegisterReader(XML, newXMLReader) + parsing.RegisterWriter(XML, newXMLWriter) +} + +func newXMLReader() (parsing.Reader, error) { + return &xmlReader{}, nil +} + +// NewXMLWriter creates a new XML writer. +func newXMLWriter(options parsing.WriterOptions) (parsing.Writer, error) { + return &xmlWriter{ + options: options, + }, nil +} + +type xmlReader struct{} + +// Read reads a value from a byte slice. +func (j *xmlReader) Read(data []byte) (*model.Value, error) { + decoder := xml.NewDecoder(bytes.NewReader(data)) + decoder.Strict = true + + el, err := j.parseElement(decoder, xml.StartElement{ + Name: xml.Name{ + Local: "root", + }, + }) + if err != nil { + return nil, err + } + + return el.toModel() +} + +type xmlAttr struct { + Name string + Value string +} + +type xmlElement struct { + Name string + Attrs []xmlAttr + Children []*xmlElement + Content string +} + +func (e *xmlElement) toModel() (*model.Value, error) { + attrs := model.NewMapValue() + for _, attr := range e.Attrs { + if err := attrs.SetMapKey(attr.Name, model.NewStringValue(attr.Value)); err != nil { + return nil, err + } + } + res := model.NewMapValue() + if err := res.SetMapKey("name", model.NewStringValue(e.Name)); err != nil { + return nil, err + } + if err := res.SetMapKey("attrs", attrs); err != nil { + return nil, err + } + + if err := res.SetMapKey("content", model.NewStringValue(e.Content)); err != nil { + return nil, err + } + children := model.NewSliceValue() + for _, child := range e.Children { + childModel, err := child.toModel() + if err != nil { + return nil, err + } + if err := children.Append(childModel); err != nil { + return nil, err + } + } + if err := res.SetMapKey("children", children); err != nil { + return nil, err + } + return res, nil +} + +func (j *xmlReader) parseElement(decoder *xml.Decoder, element xml.StartElement) (*xmlElement, error) { + el := &xmlElement{ + Name: element.Name.Local, + Attrs: make([]xmlAttr, 0), + Children: make([]*xmlElement, 0), + } + + for _, attr := range element.Attr { + el.Attrs = append(el.Attrs, xmlAttr{ + Name: attr.Name.Local, + Value: attr.Value, + }) + } + + for { + t, err := decoder.Token() + if errors.Is(err, io.EOF) { + if el.Name == "root" { + return el, nil + } + return nil, fmt.Errorf("unexpected EOF") + } + + switch t := t.(type) { + case xml.StartElement: + child, err := j.parseElement(decoder, t) + if err != nil { + return nil, err + } + el.Children = append(el.Children, child) + case xml.CharData: + if unicode.IsSpace([]rune(string(t))[0]) { + continue + } + el.Content += string(t) + case xml.EndElement: + return el, nil + default: + return nil, fmt.Errorf("unexpected token: %v", t) + } + } +} + +type xmlWriter struct { + options parsing.WriterOptions +} + +// Write writes a value to a byte slice. +func (j *xmlWriter) Write(value *model.Value) ([]byte, error) { + return nil, nil +} diff --git a/parsing/xml/xml_test.go b/parsing/xml/xml_test.go new file mode 100644 index 00000000..6d693871 --- /dev/null +++ b/parsing/xml/xml_test.go @@ -0,0 +1,78 @@ +package xml_test + +import ( + "bytes" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/parsing" + "github.com/tomwright/dasel/v3/parsing/xml" +) + +type testCase struct { + in string + assert func(t *testing.T, res *model.Value) +} + +func (tc testCase) run(t *testing.T) { + r, err := xml.XML.NewReader() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + res, err := r.Read([]byte(tc.in)) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + tc.assert(t, res) +} + +type rwTestCase struct { + in string + out string +} + +func (tc rwTestCase) run(t *testing.T) { + if tc.out == "" { + tc.out = tc.in + } + r, err := xml.XML.NewReader() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + w, err := xml.XML.NewWriter(parsing.WriterOptions{}) + res, err := r.Read([]byte(tc.in)) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + out, err := w.Write(res) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !bytes.Equal([]byte(tc.out), out) { + t.Errorf("unexpected output: %s", cmp.Diff(tc.out, string(out))) + } +} + +func TestYamlValue_UnmarshalXML(t *testing.T) { + //t.Run("generic", rwTestCase{ + // in: ` + // + // Test + // + // + //

Test

+ //

Test

+ //
+ //
+ //

Hello

+ //

World

+ //
+ // + //`, + // }.run) +} From bbc873f4ddc55032c3224dd009484ac7bf6d9847 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Mon, 28 Oct 2024 22:33:21 +0000 Subject: [PATCH 48/56] Add coalesce operator and unstable flag --- execution/execute.go | 12 ++ execution/execute_binary.go | 272 ++++++++++++++++------------- execution/execute_binary_test.go | 101 +++++++++++ execution/execute_branch_test.go | 21 ++- execution/execute_object.go | 23 ++- execution/func.go | 2 + execution/func_to_float.go | 49 ++++++ execution/func_to_int.go | 49 ++++++ execution/func_to_string.go | 2 +- execution/options.go | 19 +- internal/cli/query.go | 5 + model/value_comparison.go | 4 +- model/value_map.go | 4 +- model/value_map_test.go | 3 +- model/value_math.go | 10 +- selector/ast/expression_complex.go | 3 + selector/lexer/token.go | 2 + selector/lexer/tokenize.go | 5 + selector/parser/denotations.go | 4 + selector/parser/parse_symbol.go | 77 ++++++-- selector/parser/parse_variable.go | 21 +-- selector/parser/parser_binary.go | 43 ++++- selector/parser/parser_test.go | 45 ++++- 23 files changed, 606 insertions(+), 170 deletions(-) create mode 100644 execution/func_to_float.go create mode 100644 execution/func_to_int.go diff --git a/execution/execute.go b/execution/execute.go index 4c8105e6..5b763fbb 100644 --- a/execution/execute.go +++ b/execution/execute.go @@ -1,7 +1,10 @@ package execution import ( + "errors" "fmt" + "reflect" + "slices" "github.com/tomwright/dasel/v3/model" "github.com/tomwright/dasel/v3/selector" @@ -64,7 +67,16 @@ func ExecuteAST(expr ast.Expr, value *model.Value, options *Options) (*model.Val return res, nil } +var unstableAstTypes = []reflect.Type{ + reflect.TypeFor[ast.BranchExpr](), +} + func exprExecutor(opts *Options, expr ast.Expr) (expressionExecutor, error) { + if !opts.Unstable && (slices.Contains(unstableAstTypes, reflect.TypeOf(expr)) || + slices.Contains(unstableAstTypes, reflect.ValueOf(expr).Type())) { + return nil, errors.New("unstable ast types are not enabled. to enable them use --unstable") + } + switch e := expr.(type) { case ast.BinaryExpr: return binaryExprExecutor(opts, e) diff --git a/execution/execute_binary.go b/execution/execute_binary.go index 8e218d27..c8e466d6 100644 --- a/execution/execute_binary.go +++ b/execution/execute_binary.go @@ -1,6 +1,7 @@ package execution import ( + "errors" "fmt" "github.com/tomwright/dasel/v3/model" @@ -8,140 +9,31 @@ import ( "github.com/tomwright/dasel/v3/selector/lexer" ) -func binaryExprExecutor(opts *Options, e ast.BinaryExpr) (expressionExecutor, error) { - return func(data *model.Value) (*model.Value, error) { - left, err := ExecuteAST(e.Left, data, opts) +type binaryExpressionExecutorFn func(expr ast.BinaryExpr, value *model.Value, options *Options) (*model.Value, error) + +func basicBinaryExpressionExecutorFn(handler func(left *model.Value, right *model.Value, e ast.BinaryExpr) (*model.Value, error)) binaryExpressionExecutorFn { + return func(expr ast.BinaryExpr, value *model.Value, options *Options) (*model.Value, error) { + left, err := ExecuteAST(expr.Left, value, options) if err != nil { return nil, fmt.Errorf("error evaluating left expression: %w", err) } - var doOperation func(a *model.Value, b *model.Value) (*model.Value, error) - - switch e.Operator.Kind { - case lexer.Plus: - doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { - return a.Add(b) - } - case lexer.Dash: - doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { - return a.Subtract(b) - } - case lexer.Star: - doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { - return a.Multiply(b) - } - case lexer.Slash: - doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { - return a.Divide(b) - } - case lexer.Percent: - doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { - return a.Modulo(b) - } - case lexer.GreaterThan: - doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { - return a.GreaterThan(b) - } - case lexer.GreaterThanOrEqual: - doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { - return a.GreaterThanOrEqual(b) - } - case lexer.LessThan: - doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { - return a.LessThan(b) - } - case lexer.LessThanOrEqual: - doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { - return a.LessThanOrEqual(b) - } - case lexer.Equal: - doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { - return a.Equal(b) - } - case lexer.NotEqual: - doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { - return a.NotEqual(b) - } - case lexer.Equals: - doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { - err := a.Set(b) - if err != nil { - return nil, fmt.Errorf("error setting value: %w", err) - } - switch a.Type() { - case model.TypeMap: - return a, nil - case model.TypeSlice: - return a, nil - default: - return b, nil - } - } - case lexer.And: - doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { - leftBool, err := a.BoolValue() - if err != nil { - return nil, fmt.Errorf("error getting left bool value: %w", err) - } - rightBool, err := b.BoolValue() - if err != nil { - return nil, fmt.Errorf("error getting right bool value: %w", err) - } - return model.NewBoolValue(leftBool && rightBool), nil - } - case lexer.Or: - doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { - leftBool, err := a.BoolValue() - if err != nil { - return nil, fmt.Errorf("error getting left bool value: %w", err) - } - rightBool, err := b.BoolValue() - if err != nil { - return nil, fmt.Errorf("error getting right bool value: %w", err) - } - return model.NewBoolValue(leftBool || rightBool), nil - } - case lexer.Like, lexer.NotLike: - doOperation = func(a *model.Value, b *model.Value) (*model.Value, error) { - leftStr, err := a.StringValue() - if err != nil { - return nil, fmt.Errorf("like requires left side to be a string, got %s", left.Type().String()) - } - rightPatt, ok := e.Right.(ast.RegexExpr) - if !ok { - return nil, fmt.Errorf("like requires right side to be a regex pattern") - } - res := rightPatt.Regex.MatchString(leftStr) - if e.Operator.Kind == lexer.NotLike { - res = !res - } - return model.NewBoolValue(res), nil - } - default: - return nil, fmt.Errorf("unhandled operator: %s", e.Operator.Value) - } - - if doOperation == nil { - return nil, fmt.Errorf("missing operation for operator %s", e.Operator.Value) - } - if !left.IsBranch() { - right, err := ExecuteAST(e.Right, data, opts) + right, err := ExecuteAST(expr.Right, value, options) if err != nil { return nil, fmt.Errorf("error evaluating right expression: %w", err) } - return doOperation(left, right) + return handler(left, right, expr) } res := model.NewSliceValue() res.MarkAsBranch() if err := left.RangeSlice(func(i int, v *model.Value) error { - right, err := ExecuteAST(e.Right, v, opts) + right, err := ExecuteAST(expr.Right, v, options) if err != nil { return fmt.Errorf("error evaluating right expression: %w", err) } - - r, err := doOperation(v, right) + r, err := handler(v, right, expr) if err != nil { return err } @@ -153,5 +45,149 @@ func binaryExprExecutor(opts *Options, e ast.BinaryExpr) (expressionExecutor, er return nil, err } return res, nil + } +} + +var binaryExpressionExecutors = map[lexer.TokenKind]binaryExpressionExecutorFn{} + +func binaryExprExecutor(opts *Options, e ast.BinaryExpr) (expressionExecutor, error) { + return func(data *model.Value) (*model.Value, error) { + + exec, ok := binaryExpressionExecutors[e.Operator.Kind] + if !ok { + return nil, fmt.Errorf("unhandled operator: %s", e.Operator.Value) + } + + return exec(e, data, opts) }, nil } + +func init() { + binaryExpressionExecutors[lexer.Plus] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) { + return left.Add(right) + }) + binaryExpressionExecutors[lexer.Dash] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) { + return left.Subtract(right) + }) + binaryExpressionExecutors[lexer.Star] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) { + return left.Multiply(right) + }) + binaryExpressionExecutors[lexer.Slash] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) { + return left.Divide(right) + }) + binaryExpressionExecutors[lexer.Percent] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) { + return left.Modulo(right) + }) + binaryExpressionExecutors[lexer.GreaterThan] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) { + return left.GreaterThan(right) + }) + binaryExpressionExecutors[lexer.GreaterThanOrEqual] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) { + return left.GreaterThanOrEqual(right) + }) + binaryExpressionExecutors[lexer.LessThan] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) { + return left.LessThan(right) + }) + binaryExpressionExecutors[lexer.LessThanOrEqual] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) { + return left.LessThanOrEqual(right) + }) + binaryExpressionExecutors[lexer.Equal] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) { + return left.Equal(right) + }) + binaryExpressionExecutors[lexer.NotEqual] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) { + return left.NotEqual(right) + }) + binaryExpressionExecutors[lexer.Equals] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) { + err := left.Set(right) + if err != nil { + return nil, fmt.Errorf("error setting value: %w", err) + } + switch left.Type() { + case model.TypeMap: + return left, nil + case model.TypeSlice: + return left, nil + default: + return right, nil + } + }) + binaryExpressionExecutors[lexer.And] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) { + leftBool, err := left.BoolValue() + if err != nil { + return nil, fmt.Errorf("error getting left bool value: %w", err) + } + rightBool, err := right.BoolValue() + if err != nil { + return nil, fmt.Errorf("error getting right bool value: %w", err) + } + return model.NewBoolValue(leftBool && rightBool), nil + }) + binaryExpressionExecutors[lexer.Or] = basicBinaryExpressionExecutorFn(func(left *model.Value, right *model.Value, _ ast.BinaryExpr) (*model.Value, error) { + leftBool, err := left.BoolValue() + if err != nil { + return nil, fmt.Errorf("error getting left bool value: %w", err) + } + rightBool, err := right.BoolValue() + if err != nil { + return nil, fmt.Errorf("error getting right bool value: %w", err) + } + return model.NewBoolValue(leftBool || rightBool), nil + }) + binaryExpressionExecutors[lexer.Like] = basicBinaryExpressionExecutorFn(func(left *model.Value, _ *model.Value, e ast.BinaryExpr) (*model.Value, error) { + leftStr, err := left.StringValue() + if err != nil { + return nil, fmt.Errorf("like requires left side to be a string, got %s", left.Type().String()) + } + rightPatt, ok := e.Right.(ast.RegexExpr) + if !ok { + return nil, fmt.Errorf("like requires right side to be a regex pattern") + } + res := rightPatt.Regex.MatchString(leftStr) + return model.NewBoolValue(res), nil + }) + binaryExpressionExecutors[lexer.NotLike] = basicBinaryExpressionExecutorFn(func(left *model.Value, _ *model.Value, e ast.BinaryExpr) (*model.Value, error) { + leftStr, err := left.StringValue() + if err != nil { + return nil, fmt.Errorf("like requires left side to be a string, got %s", left.Type().String()) + } + rightPatt, ok := e.Right.(ast.RegexExpr) + if !ok { + return nil, fmt.Errorf("like requires right side to be a regex pattern") + } + res := rightPatt.Regex.MatchString(leftStr) + return model.NewBoolValue(!res), nil + }) + binaryExpressionExecutors[lexer.DoubleQuestionMark] = func(expr ast.BinaryExpr, value *model.Value, options *Options) (*model.Value, error) { + left, err := ExecuteAST(expr.Left, value, options) + + if err == nil && !left.IsNull() { + return left, nil + } + + if err != nil { + handleErrs := []any{ + model.ErrIncompatibleTypes{}, + model.ErrUnexpectedType{}, + model.ErrUnexpectedTypes{}, + model.SliceIndexOutOfRange{}, + model.MapKeyNotFound{}, + } + for _, e := range handleErrs { + if errors.As(err, &e) { + err = nil + break + } + } + + if err != nil { + return nil, fmt.Errorf("error evaluating left expression: %w", err) + } + } + + // Do we need to handle branches here? + right, err := ExecuteAST(expr.Right, value, options) + if err != nil { + return nil, fmt.Errorf("error evaluating right expression: %w", err) + } + return right, nil + } +} diff --git a/execution/execute_binary_test.go b/execution/execute_binary_test.go index 33d43b61..d1665337 100644 --- a/execution/execute_binary_test.go +++ b/execution/execute_binary_test.go @@ -38,6 +38,10 @@ func TestBinary(t *testing.T) { s: `(45.2 + 5) * ((4 - 2) / 2)`, // (45.2 + 5) * ((4 - 2) / 2) = (50.2) * ((2) / 2) = (50.2) * (1) = 50.2 out: model.NewFloatValue(50.2), }.run) + t.Run("ordering with groups", testCase{ + s: `1 + 1 - 1 + 1 * 2`, // 1 + 1 - 1 + (1 * 2) = 1 + 1 - 1 + 2 = 3 + out: model.NewIntValue(3), + }.run) }) t.Run("variables", func(t *testing.T) { in := func() *model.Value { @@ -177,5 +181,102 @@ func TestBinary(t *testing.T) { out: model.NewBoolValue(false), }.run) }) + + t.Run("coalesce", func(t *testing.T) { + t.Run("literals", func(t *testing.T) { + t.Run("coalesce", testCase{ + s: `null ?? 1`, + out: model.NewIntValue(1), + }.run) + t.Run("coalesce with null", testCase{ + s: `null ?? null`, + out: model.NewNullValue(), + }.run) + t.Run("coalesce with null and value", testCase{ + s: `null ?? 2`, + out: model.NewIntValue(2), + }.run) + t.Run("coalesce with value", testCase{ + s: `1 ?? 2`, + out: model.NewIntValue(1), + }.run) + }) + t.Run("variables", func(t *testing.T) { + in := func() *model.Value { + return model.NewValue(orderedmap.NewMap(). + Set("one", 1). + Set("two", 2). + Set("nested", orderedmap.NewMap(). + Set("one", 1). + Set("two", 2). + Set("three", 3). + Set("four", 4)). + Set("list", []any{1, 2, 3})) + } + t.Run("coalesce", testCase{ + inFn: in, + s: `nested.five ?? one`, + out: model.NewIntValue(1), + }.run) + t.Run("coalesce with null", testCase{ + inFn: in, + s: `nested.five ?? null`, + out: model.NewNullValue(), + }.run) + t.Run("coalesce with null and value", testCase{ + inFn: in, + s: `nested.five ?? 2`, + out: model.NewIntValue(2), + }.run) + t.Run("coalesce with value", testCase{ + inFn: in, + s: `nested.three ?? 2`, + out: model.NewIntValue(3), + }.run) + t.Run("coalesce with bad map key", testCase{ + inFn: in, + s: `nope ?? 2`, + out: model.NewIntValue(2), + }.run) + t.Run("coalesce with nested bad map key", testCase{ + inFn: in, + s: `nested.nope ?? 2`, + out: model.NewIntValue(2), + }.run) + t.Run("coalesce with list index", testCase{ + inFn: in, + s: `list[1] ?? 5`, + out: model.NewIntValue(2), + }.run) + t.Run("coalesce with list bad index", testCase{ + inFn: in, + s: `list[3] ?? 5`, + out: model.NewIntValue(5), + }.run) + t.Run("chained coalesce execute left to right", func(t *testing.T) { + // These tests ensure the coalesces run in order. + t.Run("no match", testCase{ + inFn: in, + s: `nested.five ?? nested.six ?? nested.seven ?? 10`, + out: model.NewIntValue(10), + }.run) + t.Run("first match when all exist", testCase{ + inFn: in, + s: `nested.one ?? nested.two ?? nested.three ?? 10`, + out: model.NewIntValue(1), + }.run) + t.Run("second match", testCase{ + inFn: in, + s: `nested.five ?? nested.two ?? nested.three ?? 10`, + out: model.NewIntValue(2), + }.run) + t.Run("third match", testCase{ + inFn: in, + s: `nested.five ?? nested.six ?? nested.three ?? 10`, + out: model.NewIntValue(3), + }.run) + }) + }) + }) }) } diff --git a/execution/execute_branch_test.go b/execution/execute_branch_test.go index 2077b2bc..5b09f06c 100644 --- a/execution/execute_branch_test.go +++ b/execution/execute_branch_test.go @@ -3,6 +3,7 @@ package execution_test import ( "testing" + "github.com/tomwright/dasel/v3/execution" "github.com/tomwright/dasel/v3/model" ) @@ -17,6 +18,9 @@ func TestBranch(t *testing.T) { } return r }, + opts: []execution.ExecuteOptionFn{ + execution.WithUnstable(), + }, }.run) t.Run("many branches", testCase{ s: "branch(1, 1+1, 3/1, 123)", @@ -37,6 +41,9 @@ func TestBranch(t *testing.T) { } return r }, + opts: []execution.ExecuteOptionFn{ + execution.WithUnstable(), + }, }.run) t.Run("spread into many branches", testCase{ s: "[1,2,3].branch(...)", @@ -54,6 +61,9 @@ func TestBranch(t *testing.T) { } return r }, + opts: []execution.ExecuteOptionFn{ + execution.WithUnstable(), + }, }.run) t.Run("chained branch set", testCase{ s: "branch(1, 2, 3).$this=5", @@ -71,9 +81,12 @@ func TestBranch(t *testing.T) { } return r }, + opts: []execution.ExecuteOptionFn{ + execution.WithUnstable(), + }, }.run) t.Run("chained branch math", testCase{ - s: "branch(1, 2, 3) * 2", + s: "(branch(1, 2, 3)) * 2", outFn: func() *model.Value { r := model.NewSliceValue() r.MarkAsBranch() @@ -88,6 +101,9 @@ func TestBranch(t *testing.T) { } return r }, + opts: []execution.ExecuteOptionFn{ + execution.WithUnstable(), + }, }.run) t.Run("chained branch math using branched value", testCase{ s: `branch({"x":1}, {"x":2}, {"x":3}).x * $this`, @@ -105,5 +121,8 @@ func TestBranch(t *testing.T) { } return r }, + opts: []execution.ExecuteOptionFn{ + execution.WithUnstable(), + }, }.run) } diff --git a/execution/execute_object.go b/execution/execute_object.go index 74d6db81..ad41de51 100644 --- a/execution/execute_object.go +++ b/execution/execute_object.go @@ -78,13 +78,22 @@ func propertyExprExecutor(opts *Options, e ast.PropertyExpr) (expressionExecutor if err != nil { return nil, fmt.Errorf("error evaluating property: %w", err) } - if !key.IsString() { - return nil, fmt.Errorf("expected property to resolve to string, got %s", key.Type()) - } - keyStr, err := key.StringValue() - if err != nil { - return nil, fmt.Errorf("error getting string value: %w", err) + switch { + case key.IsString(): + keyStr, err := key.StringValue() + if err != nil { + return nil, fmt.Errorf("error getting string value: %w", err) + } + + return data.GetMapKey(keyStr) + case key.IsInt(): + keyInt, err := key.IntValue() + if err != nil { + return nil, fmt.Errorf("error getting int value: %w", err) + } + return data.GetSliceIndex(int(keyInt)) + default: + return nil, fmt.Errorf("expected key to be a string or int, got %s", key.Type()) } - return data.GetMapKey(keyStr) }, nil } diff --git a/execution/func.go b/execution/func.go index 504a8af8..d0ea4b71 100644 --- a/execution/func.go +++ b/execution/func.go @@ -12,6 +12,8 @@ var ( FuncLen, FuncAdd, FuncToString, + FuncToInt, + FuncToFloat, FuncMerge, FuncReverse, FuncTypeOf, diff --git a/execution/func_to_float.go b/execution/func_to_float.go new file mode 100644 index 00000000..7a079908 --- /dev/null +++ b/execution/func_to_float.go @@ -0,0 +1,49 @@ +package execution + +import ( + "fmt" + "strconv" + + "github.com/tomwright/dasel/v3/model" +) + +// FuncToFloat is a function that converts the given value to a string. +var FuncToFloat = NewFunc( + "toFloat", + func(data *model.Value, args model.Values) (*model.Value, error) { + switch args[0].Type() { + case model.TypeString: + stringValue, err := args[0].StringValue() + if err != nil { + return nil, err + } + + i, err := strconv.ParseFloat(stringValue, 64) + if err != nil { + return nil, err + } + + return model.NewFloatValue(i), nil + case model.TypeInt: + i, err := args[0].IntValue() + if err != nil { + return nil, err + } + return model.NewFloatValue(float64(i)), nil + case model.TypeFloat: + return args[0], nil + case model.TypeBool: + i, err := args[0].BoolValue() + if err != nil { + return nil, err + } + if i { + return model.NewFloatValue(1), nil + } + return model.NewFloatValue(0), nil + default: + return nil, fmt.Errorf("cannot convert %s to float", args[0].Type()) + } + }, + ValidateArgsExactly(1), +) diff --git a/execution/func_to_int.go b/execution/func_to_int.go new file mode 100644 index 00000000..b8a90eb5 --- /dev/null +++ b/execution/func_to_int.go @@ -0,0 +1,49 @@ +package execution + +import ( + "fmt" + "strconv" + + "github.com/tomwright/dasel/v3/model" +) + +// FuncToInt is a function that converts the given value to a string. +var FuncToInt = NewFunc( + "toInt", + func(data *model.Value, args model.Values) (*model.Value, error) { + switch args[0].Type() { + case model.TypeString: + stringValue, err := args[0].StringValue() + if err != nil { + return nil, err + } + + i, err := strconv.ParseInt(stringValue, 10, 64) + if err != nil { + return nil, err + } + + return model.NewIntValue(i), nil + case model.TypeInt: + return args[0], nil + case model.TypeFloat: + i, err := args[0].FloatValue() + if err != nil { + return nil, err + } + return model.NewIntValue(int64(i)), nil + case model.TypeBool: + i, err := args[0].BoolValue() + if err != nil { + return nil, err + } + if i { + return model.NewIntValue(1), nil + } + return model.NewIntValue(0), nil + default: + return nil, fmt.Errorf("cannot convert %s to int", args[0].Type()) + } + }, + ValidateArgsExactly(1), +) diff --git a/execution/func_to_string.go b/execution/func_to_string.go index 0f60f85b..0409f37d 100644 --- a/execution/func_to_string.go +++ b/execution/func_to_string.go @@ -29,7 +29,7 @@ var FuncToString = NewFunc( if err != nil { return nil, err } - return model.NewStringValue(fmt.Sprintf("%f", i)), nil + return model.NewStringValue(fmt.Sprintf("%g", i)), nil case model.TypeBool: i, err := args[0].BoolValue() if err != nil { diff --git a/execution/options.go b/execution/options.go index 0f425315..9a286dfb 100644 --- a/execution/options.go +++ b/execution/options.go @@ -7,8 +7,9 @@ type ExecuteOptionFn func(*Options) // Options contains the options for the execution of the selector. type Options struct { - Funcs FuncCollection - Vars map[string]*model.Value + Funcs FuncCollection + Vars map[string]*model.Value + Unstable bool } // NewOptions creates a new Options struct with the given options. @@ -39,3 +40,17 @@ func WithVariable(key string, val *model.Value) ExecuteOptionFn { o.Vars[key] = val } } + +// WithUnstable allows access to potentially unstable features. +func WithUnstable() ExecuteOptionFn { + return func(o *Options) { + o.Unstable = true + } +} + +// WithoutUnstable disallows access to potentially unstable features. +func WithoutUnstable() ExecuteOptionFn { + return func(o *Options) { + o.Unstable = false + } +} diff --git a/internal/cli/query.go b/internal/cli/query.go index 979f141d..c43bd8da 100644 --- a/internal/cli/query.go +++ b/internal/cli/query.go @@ -14,6 +14,7 @@ type QueryCmd struct { InFormat string `flag:"" name:"in" short:"i" help:"The format of the input data."` OutFormat string `flag:"" name:"out" short:"o" help:"The format of the output data."` ReturnRoot bool `flag:"" name:"root" help:"Return the root value."` + Unstable bool `flag:"" name:"unstable" help:"Allow access to potentially unstable features."` Query string `arg:"" help:"The query to execute." optional:"" default:""` } @@ -70,6 +71,10 @@ func (c *QueryCmd) Run(ctx *Globals) error { opts = append(opts, execution.WithVariable("root", inputData)) + if c.Unstable { + opts = append(opts, execution.WithUnstable()) + } + options := execution.NewOptions(opts...) out, err := execution.ExecuteSelector(c.Query, inputData, options) if err != nil { diff --git a/model/value_comparison.go b/model/value_comparison.go index 1603d5f5..e369c118 100644 --- a/model/value_comparison.go +++ b/model/value_comparison.go @@ -55,7 +55,7 @@ func (v *Value) Equal(other *Value) (*Value, error) { } if v.Type() != other.Type() { - return nil, &ErrIncompatibleTypes{A: v, B: other} + return nil, ErrIncompatibleTypes{A: v, B: other} } isEqual, err := v.EqualTypeValue(other) @@ -137,7 +137,7 @@ func (v *Value) LessThan(other *Value) (*Value, error) { return NewValue(a < b), nil } - return nil, &ErrIncompatibleTypes{A: v, B: other} + return nil, ErrIncompatibleTypes{A: v, B: other} } // LessThanOrEqual compares two values. diff --git a/model/value_map.go b/model/value_map.go index 09fd9d20..527df885 100644 --- a/model/value_map.go +++ b/model/value_map.go @@ -68,7 +68,7 @@ func (v *Value) GetMapKey(key string) (*Value, error) { } val, ok := m.Get(key) if !ok { - return nil, &MapKeyNotFound{Key: key} + return nil, MapKeyNotFound{Key: key} } res := NewValue(val) res.setFn = func(newValue *Value) error { @@ -83,7 +83,7 @@ func (v *Value) GetMapKey(key string) (*Value, error) { } i := unpacked.Value.MapIndex(reflect.ValueOf(key)) if !i.IsValid() { - return nil, &MapKeyNotFound{Key: key} + return nil, MapKeyNotFound{Key: key} } res := NewValue(i) res.setFn = func(newValue *Value) error { diff --git a/model/value_map_test.go b/model/value_map_test.go index cb3f29f7..afb3bc89 100644 --- a/model/value_map_test.go +++ b/model/value_map_test.go @@ -140,8 +140,7 @@ func TestMap(t *testing.T) { return } _, err := v.GetMapKey("foo") - notFoundErr := &model.MapKeyNotFound{} - if !errors.As(err, ¬FoundErr) { + if !errors.As(err, &model.MapKeyNotFound{}) { t.Errorf("expected key not found error, got %s", err) } }) diff --git a/model/value_math.go b/model/value_math.go index 2f6adbb5..bc932f21 100644 --- a/model/value_math.go +++ b/model/value_math.go @@ -61,7 +61,7 @@ func (v *Value) Add(other *Value) (*Value, error) { } return NewValue(a + b), nil } - return nil, &ErrIncompatibleTypes{A: v, B: other} + return nil, ErrIncompatibleTypes{A: v, B: other} } // Subtract returns the difference between two values. @@ -110,7 +110,7 @@ func (v *Value) Subtract(other *Value) (*Value, error) { } return NewValue(a - float64(b)), nil } - return nil, &ErrIncompatibleTypes{A: v, B: other} + return nil, ErrIncompatibleTypes{A: v, B: other} } // Multiply returns the product of the two values. @@ -159,7 +159,7 @@ func (v *Value) Multiply(other *Value) (*Value, error) { } return NewValue(a * float64(b)), nil } - return nil, &ErrIncompatibleTypes{A: v, B: other} + return nil, ErrIncompatibleTypes{A: v, B: other} } // Divide returns the result of dividing the value by another value. @@ -208,7 +208,7 @@ func (v *Value) Divide(other *Value) (*Value, error) { } return NewValue(a / float64(b)), nil } - return nil, &ErrIncompatibleTypes{A: v, B: other} + return nil, ErrIncompatibleTypes{A: v, B: other} } // Modulo returns the remainder of the division of two values. @@ -257,5 +257,5 @@ func (v *Value) Modulo(other *Value) (*Value, error) { } return NewValue(math.Mod(a, float64(b))), nil } - return nil, &ErrIncompatibleTypes{A: v, B: other} + return nil, ErrIncompatibleTypes{A: v, B: other} } diff --git a/selector/ast/expression_complex.go b/selector/ast/expression_complex.go index 0d0517dd..3715633a 100644 --- a/selector/ast/expression_complex.go +++ b/selector/ast/expression_complex.go @@ -66,6 +66,9 @@ type ArrayExpr struct { func (ArrayExpr) expr() {} type PropertyExpr struct { + // Property can resolve to a string or number. + // If it resolves to a number, we expect to be reading from an array. + // If it resolves to a string, we expect to be reading from a map. Property Expr } diff --git a/selector/lexer/token.go b/selector/lexer/token.go index 514ca768..96e838af 100644 --- a/selector/lexer/token.go +++ b/selector/lexer/token.go @@ -61,6 +61,8 @@ const ( SortBy Asc Desc + QuestionMark + DoubleQuestionMark ) type Tokens []Token diff --git a/selector/lexer/tokenize.go b/selector/lexer/tokenize.go index 611af4b2..535f3664 100644 --- a/selector/lexer/tokenize.go +++ b/selector/lexer/tokenize.go @@ -151,6 +151,11 @@ func (p *Tokenizer) parseCurRune() (Token, error) { Pos: p.i, Token: rune(p.src[p.i]), } + case '?': + if p.peekRuneEqual(p.i+1, '?') { + return NewToken(DoubleQuestionMark, "??", p.i, 2), nil + } + return NewToken(QuestionMark, "?", p.i, 1), nil case '"', '\'': pos := p.i buf := make([]rune, 0) diff --git a/selector/parser/denotations.go b/selector/parser/denotations.go index 03ef35b0..8828cc8c 100644 --- a/selector/parser/denotations.go +++ b/selector/parser/denotations.go @@ -23,6 +23,7 @@ var leftDenotationTokens = []lexer.TokenKind{ lexer.Like, lexer.NotLike, lexer.Equals, + lexer.DoubleQuestionMark, } // right denotation tokens are tokens that expect a token to the right of them. @@ -36,6 +37,7 @@ const ( bpDefault bindingPower = iota bpAssignment bpLogical + bpEarlyLogical bpRelational bpAdditive bpMultiplicative @@ -78,6 +80,8 @@ var tokenBindingPowers = map[lexer.TokenKind]bindingPower{ lexer.Like: bpLogical, lexer.NotLike: bpLogical, + lexer.DoubleQuestionMark: bpEarlyLogical, + lexer.Equals: bpAssignment, } diff --git a/selector/parser/parse_symbol.go b/selector/parser/parse_symbol.go index dcdcf9f2..e38f29c6 100644 --- a/selector/parser/parse_symbol.go +++ b/selector/parser/parse_symbol.go @@ -5,6 +5,65 @@ import ( "github.com/tomwright/dasel/v3/selector/lexer" ) +// parseFollowingSymbols deals with the expressions following symbols/variables, e.g. +// $this[0][1]['name'] +// foo['bar']['baz'][1] +func parseFollowingSymbol(p *Parser, prev ast.Expr) (ast.Expr, error) { + res := ast.Expressions{prev} + + for p.hasToken() { + if p.current().IsKind(lexer.Spread) { + p.advanceN(1) + res = append(res, ast.SpreadExpr{}) + continue + } + + // String based indexes + if p.current().IsKind(lexer.OpenBracket) { + + if p.peekN(1).IsKind(lexer.Spread) && p.peekN(2).IsKind(lexer.CloseBracket) { + p.advanceN(3) + res = append(res, ast.SpreadExpr{}) + continue + } + + if p.peekN(1).IsKind(lexer.Star) && p.peekN(2).IsKind(lexer.CloseBracket) { + p.advanceN(3) + res = append(res, ast.SpreadExpr{}) + continue + } + + e, err := parseIndexSquareBrackets(p) + if err != nil { + return nil, err + } + switch ex := e.(type) { + case ast.RangeExpr: + res = append(res, ex) + case ast.IndexExpr: + // Convert this to a property expr. This property executor deals + // with maps + arrays. + res = append(res, ast.PropertyExpr{ + Property: ex.Index, + }) + } + + //e, err := p.parseExpressionsFromTo(lexer.OpenBracket, lexer.CloseBracket, nil, true, bpDefault) + //if err != nil { + // return nil, err + //} + //res = append(res, ast.PropertyExpr{ + // Property: e, + //}) + continue + } + + break + } + + return ast.ChainExprs(res...), nil +} + func parseSymbol(p *Parser) (ast.Expr, error) { token := p.current() @@ -15,22 +74,16 @@ func parseSymbol(p *Parser) (ast.Expr, error) { return parseFunc(p) } - if next.IsKind(lexer.OpenBracket) { - return parseIndex(p) - } - prop := ast.PropertyExpr{ Property: ast.StringExpr{Value: token.Value}, } - if next.IsKind(lexer.Spread) { - p.advanceN(2) - return ast.ChainExprs( - prop, - ast.SpreadExpr{}, - ), nil + p.advance() + + res, err := parseFollowingSymbol(p, prop) + if err != nil { + return nil, err } - p.advance() - return prop, nil + return res, nil } diff --git a/selector/parser/parse_variable.go b/selector/parser/parse_variable.go index 207c0618..26b38ea2 100644 --- a/selector/parser/parse_variable.go +++ b/selector/parser/parse_variable.go @@ -2,30 +2,21 @@ package parser import ( "github.com/tomwright/dasel/v3/selector/ast" - "github.com/tomwright/dasel/v3/selector/lexer" ) func parseVariable(p *Parser) (ast.Expr, error) { token := p.current() - next := p.peek() - - if next.IsKind(lexer.OpenBracket) { - return parseIndex(p) - } - prop := ast.VariableExpr{ Name: token.Value, } - if next.IsKind(lexer.Spread) { - p.advanceN(2) - return ast.ChainExprs( - prop, - ast.SpreadExpr{}, - ), nil + p.advance() + + res, err := parseFollowingSymbol(p, prop) + if err != nil { + return nil, err } - p.advance() - return prop, nil + return res, nil } diff --git a/selector/parser/parser_binary.go b/selector/parser/parser_binary.go index e976ea1b..64e4e7f4 100644 --- a/selector/parser/parser_binary.go +++ b/selector/parser/parser_binary.go @@ -1,6 +1,8 @@ package parser -import "github.com/tomwright/dasel/v3/selector/ast" +import ( + "github.com/tomwright/dasel/v3/selector/ast" +) func parseBinary(p *Parser, left ast.Expr) (ast.Expr, error) { if err := p.expect(leftDenotationTokens...); err != nil { @@ -12,6 +14,45 @@ func parseBinary(p *Parser, left ast.Expr) (ast.Expr, error) { if err != nil { return nil, err } + + //if l, ok := left.(ast.BinaryExpr); ok && l.Operator.Kind == lexer.DoubleQuestionMark { + // if r, ok := right.(ast.BinaryExpr); ok && r.Operator.Kind == lexer.DoubleQuestionMark { + // return ast.BinaryExpr{ + // Left: l.Left, + // Operator: l.Operator, + // Right: ast.BinaryExpr{ + // Left: l.Right, + // Operator: r.Operator, + // Right: r.Right, + // }, + // }, nil + // } + //} + // + //if r, ok := right.(ast.BinaryExpr); ok && r.Operator.Kind == lexer.DoubleQuestionMark { + // return ast.BinaryExpr{ + // Left: ast.BinaryExpr{ + // Left: left, + // Operator: operator, + // Right: r.Left, + // }, + // Operator: r.Operator, + // Right: r.Right, + // }, nil + //} + // + //if l, ok := left.(ast.BinaryExpr); ok && l.Operator.Kind == lexer.DoubleQuestionMark { + // return ast.BinaryExpr{ + // Left: l.Left, + // Operator: l.Operator, + // Right: ast.BinaryExpr{ + // Left: l.Right, + // Operator: operator, + // Right: right, + // }, + // }, nil + //} + return ast.BinaryExpr{ Left: left, Operator: operator, diff --git a/selector/parser/parser_test.go b/selector/parser/parser_test.go index c564c835..e04eec4e 100644 --- a/selector/parser/parser_test.go +++ b/selector/parser/parser_test.go @@ -134,7 +134,7 @@ func TestParser_Parse_HappyPath(t *testing.T) { input: "$this[1]", expected: ast.ChainExprs( ast.VariableExpr{Name: "this"}, - ast.IndexExpr{Index: ast.NumberIntExpr{Value: 1}}, + ast.PropertyExpr{Property: ast.NumberIntExpr{Value: 1}}, ), })) t.Run("range", func(t *testing.T) { @@ -199,7 +199,7 @@ func TestParser_Parse_HappyPath(t *testing.T) { input: "foo[1]", expected: ast.ChainExprs( ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, - ast.IndexExpr{Index: ast.NumberIntExpr{Value: 1}}, + ast.PropertyExpr{Property: ast.NumberIntExpr{Value: 1}}, ), })) t.Run("range", func(t *testing.T) { @@ -426,4 +426,45 @@ func TestParser_Parse_HappyPath(t *testing.T) { }, })) }) + + t.Run("coalesce", func(t *testing.T) { + t.Run("chained on left side", run(t, testCase{ + input: `foo ?? bar ?? baz`, + expected: ast.BinaryExpr{ + Left: ast.BinaryExpr{ + Left: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, + Operator: lexer.Token{Kind: lexer.DoubleQuestionMark, Value: "??", Pos: 4, Len: 2}, + Right: ast.PropertyExpr{Property: ast.StringExpr{Value: "bar"}}, + }, + Operator: lexer.Token{Kind: lexer.DoubleQuestionMark, Value: "??", Pos: 11, Len: 2}, + Right: ast.PropertyExpr{Property: ast.StringExpr{Value: "baz"}}, + }, + })) + + t.Run("chained nested on left side", run(t, testCase{ + input: `nested.one ?? nested.two ?? nested.three ?? 10`, + expected: ast.BinaryExpr{ + Left: ast.BinaryExpr{ + Left: ast.BinaryExpr{ + Left: ast.ChainExprs( + ast.PropertyExpr{Property: ast.StringExpr{Value: "nested"}}, + ast.PropertyExpr{Property: ast.StringExpr{Value: "one"}}, + ), + Operator: lexer.Token{Kind: lexer.DoubleQuestionMark, Value: "??", Pos: 11, Len: 2}, + Right: ast.ChainExprs( + ast.PropertyExpr{Property: ast.StringExpr{Value: "nested"}}, + ast.PropertyExpr{Property: ast.StringExpr{Value: "two"}}, + ), + }, + Operator: lexer.Token{Kind: lexer.DoubleQuestionMark, Value: "??", Pos: 25, Len: 2}, + Right: ast.ChainExprs( + ast.PropertyExpr{Property: ast.StringExpr{Value: "nested"}}, + ast.PropertyExpr{Property: ast.StringExpr{Value: "three"}}, + ), + }, + Operator: lexer.Token{Kind: lexer.DoubleQuestionMark, Value: "??", Pos: 41, Len: 2}, + Right: ast.NumberIntExpr{Value: 10}, + }, + })) + }) } From de29bd21c03565f43b4fdccb5ca6f9d1883e8be7 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Tue, 29 Oct 2024 18:22:01 +0000 Subject: [PATCH 49/56] Add interactive mode, fixes and improvements --- execution/execute.go | 27 +-- execution/execute_binary.go | 9 +- execution/execute_branch.go | 1 + execution/execute_branch_test.go | 20 ++ execution/execute_map.go | 1 - execution/execute_map_test.go | 13 +- execution/execute_test.go | 2 +- go.mod | 23 +++ go.sum | 47 +++++ internal/cli/command.go | 6 +- internal/cli/interactive.go | 316 +++++++++++++++++++++++++++++++ internal/cli/query.go | 99 +++------- internal/cli/read_write_flag.go | 59 ++++++ internal/cli/run.go | 99 ++++++++++ internal/cli/variable.go | 15 +- model/value.go | 61 ++++++ model/value_slice.go | 8 + parsing/d/reader.go | 2 +- parsing/format.go | 4 +- parsing/json/json.go | 2 +- parsing/json/json_test.go | 2 +- parsing/reader.go | 13 +- parsing/toml/toml.go | 2 +- parsing/writer.go | 2 + parsing/xml/xml.go | 84 +++++++- parsing/xml/xml_test.go | 4 +- parsing/yaml/yaml.go | 2 +- parsing/yaml/yaml_test.go | 4 +- selector/parser/parse_array.go | 11 +- selector/parser/parse_object.go | 11 +- 30 files changed, 817 insertions(+), 132 deletions(-) create mode 100644 internal/cli/interactive.go create mode 100644 internal/cli/read_write_flag.go create mode 100644 internal/cli/run.go diff --git a/execution/execute.go b/execution/execute.go index 5b763fbb..bf3ef770 100644 --- a/execution/execute.go +++ b/execution/execute.go @@ -135,30 +135,9 @@ func exprExecutor(opts *Options, expr ast.Expr) (expressionExecutor, error) { func chainedExprExecutor(options *Options, e ast.ChainedExpr) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { for _, expr := range e.Exprs { - - if !data.IsBranch() { - res, err := ExecuteAST(expr, data, options) - if err != nil { - return nil, fmt.Errorf("error executing expression: %w", err) - } - data = res - continue - } - - res := model.NewSliceValue() - res.MarkAsBranch() - if err := data.RangeSlice(func(i int, value *model.Value) error { - r, err := ExecuteAST(expr, value, options) - if err != nil { - return fmt.Errorf("error executing expression: %w", err) - } - - if err := res.Append(r); err != nil { - return err - } - return nil - }); err != nil { - return nil, err + res, err := ExecuteAST(expr, data, options) + if err != nil { + return nil, fmt.Errorf("error executing expression: %w", err) } data = res } diff --git a/execution/execute_binary.go b/execution/execute_binary.go index c8e466d6..1ea37c4d 100644 --- a/execution/execute_binary.go +++ b/execution/execute_binary.go @@ -23,7 +23,11 @@ func basicBinaryExpressionExecutorFn(handler func(left *model.Value, right *mode if err != nil { return nil, fmt.Errorf("error evaluating right expression: %w", err) } - return handler(left, right, expr) + res, err := handler(left, right, expr) + if err != nil { + return nil, err + } + return res, nil } res := model.NewSliceValue() @@ -52,6 +56,9 @@ var binaryExpressionExecutors = map[lexer.TokenKind]binaryExpressionExecutorFn{} func binaryExprExecutor(opts *Options, e ast.BinaryExpr) (expressionExecutor, error) { return func(data *model.Value) (*model.Value, error) { + if e.Left == nil || e.Right == nil { + return nil, fmt.Errorf("left and right expressions must be provided") + } exec, ok := binaryExpressionExecutors[e.Operator.Kind] if !ok { diff --git a/execution/execute_branch.go b/execution/execute_branch.go index 95dad602..95d5a060 100644 --- a/execution/execute_branch.go +++ b/execution/execute_branch.go @@ -13,6 +13,7 @@ func branchExprExecutor(opts *Options, e ast.BranchExpr) (expressionExecutor, er res.MarkAsBranch() if len(e.Exprs) == 0 { + // No expressions given. We'll branch on the input data. if err := data.RangeSlice(func(_ int, value *model.Value) error { if err := res.Append(value); err != nil { return fmt.Errorf("failed to append branch result: %w", err) diff --git a/execution/execute_branch_test.go b/execution/execute_branch_test.go index 5b09f06c..eb70160a 100644 --- a/execution/execute_branch_test.go +++ b/execution/execute_branch_test.go @@ -125,4 +125,24 @@ func TestBranch(t *testing.T) { execution.WithUnstable(), }, }.run) + t.Run("map on branch", testCase{ + s: `branch([1], [2], [3]).map($this * 2).branch()`, + outFn: func() *model.Value { + r := model.NewSliceValue() + r.MarkAsBranch() + if err := r.Append(model.NewIntValue(2)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := r.Append(model.NewIntValue(4)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := r.Append(model.NewIntValue(6)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + return r + }, + opts: []execution.ExecuteOptionFn{ + execution.WithUnstable(), + }, + }.run) } diff --git a/execution/execute_map.go b/execution/execute_map.go index d5efcd88..6059063a 100644 --- a/execution/execute_map.go +++ b/execution/execute_map.go @@ -26,7 +26,6 @@ func mapExprExecutor(opts *Options, e ast.MapExpr) (expressionExecutor, error) { }); err != nil { return nil, fmt.Errorf("error ranging over slice: %w", err) } - return res, nil }, nil } diff --git a/execution/execute_map_test.go b/execution/execute_map_test.go index 58fd318d..c03fc979 100644 --- a/execution/execute_map_test.go +++ b/execution/execute_map_test.go @@ -3,7 +3,6 @@ package execution_test import ( "testing" - "github.com/tomwright/dasel/v3/internal/ptr" "github.com/tomwright/dasel/v3/model" "github.com/tomwright/dasel/v3/model/orderedmap" ) @@ -38,7 +37,17 @@ func TestMap(t *testing.T) { ) .map ( total )`, outFn: func() *model.Value { - return model.NewValue([]any{ptr.To(int64(6)), ptr.To(int64(8)), ptr.To(int64(10))}) + res := model.NewSliceValue() + if err := res.Append(model.NewValue(6)); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewValue(8)); err != nil { + t.Fatal(err) + } + if err := res.Append(model.NewValue(10)); err != nil { + t.Fatal(err) + } + return res }, }.run) } diff --git a/execution/execute_test.go b/execution/execute_test.go index 77116989..62189ed4 100644 --- a/execution/execute_test.go +++ b/execution/execute_test.go @@ -45,7 +45,7 @@ func (tc testCase) run(t *testing.T) { t.Fatal(err) } if !equal { - t.Errorf("unexpected output: %v", cmp.Diff(exp.Interface(), res.Interface())) + t.Errorf("unexpected output: %v\nexp: %s\ngot: %s", cmp.Diff(exp.Interface(), res.Interface()), exp.String(), res.String()) } expMeta := exp.Metadata diff --git a/go.mod b/go.mod index 9fe5a7ce..437f883d 100644 --- a/go.mod +++ b/go.mod @@ -8,3 +8,26 @@ require ( github.com/pelletier/go-toml/v2 v2.2.2 gopkg.in/yaml.v3 v3.0.1 ) + +require ( + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/bubbles v0.20.0 // indirect + github.com/charmbracelet/bubbletea v1.1.2 // indirect + github.com/charmbracelet/lipgloss v0.13.1 // indirect + github.com/charmbracelet/x/ansi v0.4.0 // indirect + github.com/charmbracelet/x/term v0.2.0 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.3.8 // indirect +) diff --git a/go.sum b/go.sum index fbd7fce0..b4d49752 100644 --- a/go.sum +++ b/go.sum @@ -4,17 +4,56 @@ github.com/alecthomas/kong v1.2.1 h1:E8jH4Tsgv6wCRX2nGrdPyHDUCSG83WH2qE4XLACD33Q github.com/alecthomas/kong v1.2.1/go.mod h1:rKTSFhbdp3Ryefn8x5MOEprnRFQ7nlmMC01GKhehhBM= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v1.1.2 h1:naQXF2laRxyLyil/i7fxdpiz1/k06IKquhm4vBfHsIc= +github.com/charmbracelet/bubbletea v1.1.2/go.mod h1:9HIU/hBV24qKjlehyj8z1r/tR9TYTQEag+cWZnuXo8E= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/lipgloss v0.13.1 h1:Oik/oqDTMVA01GetT4JdEC033dNzWoQHdWnHnQmXE2A= +github.com/charmbracelet/lipgloss v0.13.1/go.mod h1:zaYVJ2xKSKEnTEEbX6uAHabh2d975RJ+0yfkFpRBz5U= +github.com/charmbracelet/x/ansi v0.4.0 h1:NqwHA4B23VwsDn4H3VcNX1W1tOmgnvY1NDx5tOXdnOU= +github.com/charmbracelet/x/ansi v0.4.0/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= +github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -24,6 +63,14 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/cli/command.go b/internal/cli/command.go index 7ba2fb13..2b74f12a 100644 --- a/internal/cli/command.go +++ b/internal/cli/command.go @@ -17,8 +17,9 @@ type Globals struct { type CLI struct { Globals - Query QueryCmd `cmd:"" default:"withargs" help:"[default] Execute a query"` - Version VersionCmd `cmd:"" help:"Print the version"` + Query QueryCmd `cmd:"" default:"withargs" help:"[default] Execute a query"` + Version VersionCmd `cmd:"" help:"Print the version"` + Interactive InteractiveCmd `cmd:"" help:"Start an interactive session"` } func MustRun(stdin io.Reader, stdout, stderr io.Writer) { @@ -46,6 +47,7 @@ func Run(stdin io.Reader, stdout, stderr io.Writer) (*kong.Context, error) { }, kong.Bind(&cli.Globals), kong.TypeMapper(reflect.TypeFor[*[]variable](), &variableMapper{}), + kong.TypeMapper(reflect.TypeFor[*[]extReadWriteFlag](), &extReadWriteFlagMapper{}), kong.OptionFunc(func(k *kong.Kong) error { k.Stdout = cli.Stdout k.Stderr = cli.Stderr diff --git a/internal/cli/interactive.go b/internal/cli/interactive.go new file mode 100644 index 00000000..a84ef9a4 --- /dev/null +++ b/internal/cli/interactive.go @@ -0,0 +1,316 @@ +package cli + +import ( + "bytes" + "fmt" + "io" + "strings" + + "github.com/charmbracelet/bubbles/textarea" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/tomwright/dasel/v3/internal" +) + +const ( + useHighPerformanceRenderer = false +) + +var ( + titleStyle = func() lipgloss.Style { + b := lipgloss.HiddenBorder() + return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1) + }() + + resultTitleStyle = func() lipgloss.Style { + b := lipgloss.HiddenBorder() + return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1) + }() + + viewportStyle = func() lipgloss.Style { + b := lipgloss.RoundedBorder() + return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1) + }() + + commandInputStyle = func() lipgloss.Style { + return lipgloss.NewStyle() + }() +) + +func NewInteractiveCmd(queryCmd *QueryCmd) *InteractiveCmd { + return &InteractiveCmd{ + Vars: queryCmd.Vars, + ExtReadFlags: queryCmd.ExtReadFlags, + ExtWriteFlags: queryCmd.ExtWriteFlags, + InFormat: queryCmd.InFormat, + OutFormat: queryCmd.OutFormat, + + Query: queryCmd.Query, + } +} + +type InteractiveCmd struct { + Vars *[]variable `flag:"" name:"var" help:"Variables to pass to the query. E.g. --var foo=\"bar\" --var baz=json:file:./some/file.json"` + ExtReadFlags *[]extReadWriteFlag `flag:"" name:"read-flag" help:"Reader flag to customise parsing. E.g. --read-flag xml-mode=structured"` + ExtWriteFlags *[]extReadWriteFlag `flag:"" name:"write-flag" help:"Writer flag to customise output"` + InFormat string `flag:"" name:"in" short:"i" help:"The format of the input data."` + OutFormat string `flag:"" name:"out" short:"o" help:"The format of the output data."` + + Query string `arg:"" help:"The query to execute." optional:"" default:""` +} + +func (c *InteractiveCmd) Run(ctx *Globals) error { + var err error + var stdInBytes []byte = nil + + if ctx.Stdin != nil { + stdInBytes, err = io.ReadAll(ctx.Stdin) + if err != nil { + return err + } + } + + m := initialModel(c, c.Query, stdInBytes) + + p := tea.NewProgram(m) + + _, err = p.Run() + return err +} + +func initialModel(it *InteractiveCmd, defaultCommand string, stdInBytes []byte) interactiveTeaModel { + ti := textarea.New() + ti.Placeholder = "Enter a query..." + ti.SetValue(defaultCommand) + ti.Focus() + ti.SetHeight(5) + ti.ShowLineNumbers = false + + return interactiveTeaModel{ + it: it, + commandInput: ti, + err: nil, + stdInBytes: stdInBytes, + output: "Loading...", + firstUpdate: true, + } +} + +type interactiveTeaModel struct { + it *InteractiveCmd + err error + originalErr error + commandInput textarea.Model + currentCommand string + output string + outputViewport viewport.Model + originalOutput string + originalOutputViewport viewport.Model + outputReady bool + previousCommand string + stdInBytes []byte + firstUpdate bool +} + +func (m interactiveTeaModel) Init() tea.Cmd { + return textarea.Blink +} + +func (m interactiveTeaModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + var cmd tea.Cmd + + m.previousCommand = m.currentCommand + + m.currentCommand = m.commandInput.Value() + if m.firstUpdate || m.currentCommand != m.previousCommand { + m.firstUpdate = false + // If the command has changed, we need to execute it + + { + out, err := m.execDasel(m.currentCommand, true) + if err != nil { + m.originalErr = err + } else { + m.originalErr = nil + m.originalOutput = out + m.originalOutputViewport.SetContent(m.originalOutput) + } + if m.originalErr != nil { + m.originalOutput = m.originalErr.Error() + m.originalOutputViewport.SetContent(m.originalOutput) + } + } + + out, err := m.execDasel(m.currentCommand, false) + if err != nil { + m.err = err + } else { + m.err = nil + m.output = out + m.outputViewport.SetContent(m.output) + } + if m.err != nil { + m.output = m.err.Error() + m.outputViewport.SetContent(m.output) + } + } + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.Type { + case tea.KeyEsc: + return m, tea.Quit + //if m.commandInput.Focused() { + // m.commandInput.Blur() + //} + case tea.KeyCtrlC: + return m, tea.Quit + default: + if !m.commandInput.Focused() { + cmd = m.commandInput.Focus() + cmds = append(cmds, cmd) + } + } + + case tea.WindowSizeMsg: + headerHeight := lipgloss.Height(m.headerView()) + commandInputHeight := lipgloss.Height(m.commandInputView()) + resultHeaderHeight := lipgloss.Height(m.resultHeaderView()) + verticalMarginHeight := headerHeight + 2 + commandInputHeight + resultHeaderHeight + + viewportHeight := msg.Height - verticalMarginHeight + viewportWidth := (msg.Width / 2) - 4 + + if !m.outputReady { + m.outputReady = true + + m.commandInput.SetWidth(msg.Width) + + m.outputViewport = viewport.New(viewportWidth, viewportHeight) + //m.outputViewport.YPosition = verticalMarginHeight + m.outputViewport.HighPerformanceRendering = useHighPerformanceRenderer + m.outputViewport.SetContent(m.output) + m.commandInput.SetWidth(msg.Width) + if useHighPerformanceRenderer { + m.outputViewport.YPosition = headerHeight + 1 + } + + m.originalOutputViewport = viewport.New(viewportWidth, viewportHeight) + m.originalOutputViewport.YPosition = verticalMarginHeight + m.originalOutputViewport.HighPerformanceRendering = useHighPerformanceRenderer + m.originalOutputViewport.SetContent(m.output) + + if useHighPerformanceRenderer { + m.originalOutputViewport.YPosition = headerHeight + 1 + } + } else { + m.commandInput.SetWidth(msg.Width) + + m.outputViewport.Width = viewportWidth + m.outputViewport.Height = viewportHeight + m.outputViewport.SetContent(m.output) + + m.originalOutputViewport.Width = viewportWidth + m.originalOutputViewport.Height = viewportHeight + m.outputViewport.SetContent(m.originalOutput) + } + + if useHighPerformanceRenderer { + // Render (or re-render) the whole viewport. Necessary both to + // initialize the viewport and when the window is resized. + // + // This is needed for high-performance rendering only. + cmds = append(cmds, viewport.Sync(m.outputViewport)) + cmds = append(cmds, viewport.Sync(m.originalOutputViewport)) + } + + // We handle errors just like any other message + case error: + m.err = msg + return m, nil + } + + m.commandInput, cmd = m.commandInput.Update(msg) + cmds = append(cmds, cmd) + + m.outputViewport, cmd = m.outputViewport.Update(msg) + cmds = append(cmds, cmd) + + m.originalOutputViewport, cmd = m.originalOutputViewport.Update(msg) + cmds = append(cmds, cmd) + + return m, tea.Batch(cmds...) +} + +func (m interactiveTeaModel) execDasel(selector string, root bool) (res string, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic: %v", r) + } + }() + var stdIn *bytes.Reader = nil + if m.stdInBytes != nil { + stdIn = bytes.NewReader(m.stdInBytes) + } else { + stdIn = bytes.NewReader([]byte{}) + } + + o := runOpts{ + Vars: m.it.Vars, + ExtReadFlags: m.it.ExtReadFlags, + ExtWriteFlags: m.it.ExtWriteFlags, + InFormat: m.it.InFormat, + OutFormat: m.it.OutFormat, + ReturnRoot: root, + Unstable: true, + Query: selector, + + Stdin: stdIn, + } + + outBytes, err := run(o) + return string(outBytes), err +} + +func (m interactiveTeaModel) headerView() string { + return titleStyle.Render("Dasel Interactive Mode - " + internal.Version + " - ctrl+c or esc to exit") +} + +func (m interactiveTeaModel) commandInputView() string { + return commandInputStyle.Render(m.commandInput.View()) +} + +func (m interactiveTeaModel) originalHeaderView() string { + return resultTitleStyle.Render("Root") +} + +func (m interactiveTeaModel) resultHeaderView() string { + return resultTitleStyle.Render("Result") +} + +func (m interactiveTeaModel) viewportView() string { + return viewportStyle.Render(m.outputViewport.View()) +} + +func (m interactiveTeaModel) originalViewportView() string { + return viewportStyle.Render(m.originalOutputViewport.View()) +} + +func (m interactiveTeaModel) View() string { + res := []string{ + m.headerView(), + m.commandInputView(), + } + + var left, right []string + + left = append(left, m.originalHeaderView(), m.originalViewportView()) + right = append(right, m.resultHeaderView(), m.viewportView()) + + viewports := lipgloss.JoinHorizontal(lipgloss.Bottom, strings.Join(left, "\n"), strings.Join(right, "\n")) + res = append(res, viewports) + + return strings.Join(res, "\n") +} diff --git a/internal/cli/query.go b/internal/cli/query.go index c43bd8da..f6e458aa 100644 --- a/internal/cli/query.go +++ b/internal/cli/query.go @@ -1,96 +1,43 @@ package cli -import ( - "fmt" - "io" - - "github.com/tomwright/dasel/v3/execution" - "github.com/tomwright/dasel/v3/model" - "github.com/tomwright/dasel/v3/parsing" -) +import "fmt" type QueryCmd struct { - Vars *[]variable `flag:"" name:"var" help:"Variables to pass to the query. E.g. --var foo=\"bar\" --var baz=json:file:./some/file.json"` - InFormat string `flag:"" name:"in" short:"i" help:"The format of the input data."` - OutFormat string `flag:"" name:"out" short:"o" help:"The format of the output data."` - ReturnRoot bool `flag:"" name:"root" help:"Return the root value."` - Unstable bool `flag:"" name:"unstable" help:"Allow access to potentially unstable features."` + Vars variables `flag:"" name:"var" help:"Variables to pass to the query. E.g. --var foo=\"bar\" --var baz=json:file:./some/file.json"` + ExtReadFlags extReadWriteFlags `flag:"" name:"read-flag" help:"Reader flag to customise parsing. E.g. --read-flag xml-mode=structured"` + ExtWriteFlags extReadWriteFlags `flag:"" name:"write-flag" help:"Writer flag to customise output"` + InFormat string `flag:"" name:"in" short:"i" help:"The format of the input data."` + OutFormat string `flag:"" name:"out" short:"o" help:"The format of the output data."` + ReturnRoot bool `flag:"" name:"root" help:"Return the root value."` + Unstable bool `flag:"" name:"unstable" help:"Allow access to potentially unstable features."` + Interactive bool `flag:"" name:"it" help:"Run in interactive mode."` Query string `arg:"" help:"The query to execute." optional:"" default:""` } func (c *QueryCmd) Run(ctx *Globals) error { - var opts []execution.ExecuteOptionFn - - if c.OutFormat == "" { - c.OutFormat = c.InFormat - } - - var reader parsing.Reader - var err error - if len(c.InFormat) > 0 { - reader, err = parsing.Format(c.InFormat).NewReader() - if err != nil { - return fmt.Errorf("failed to get input reader: %w", err) - } - } - - writerOptions := parsing.DefaultWriterOptions() - - writer, err := parsing.Format(c.OutFormat).NewWriter(writerOptions) - if err != nil { - return fmt.Errorf("failed to get output writer: %w", err) - } - - if c.Vars != nil { - for _, v := range *c.Vars { - opts = append(opts, execution.WithVariable(v.Name, v.Value)) - } - } - - // Default to null. If stdin is being read then this will be overwritten. - inputData := model.NewNullValue() - - var inputBytes []byte - if ctx.Stdin != nil { - inputBytes, err = io.ReadAll(ctx.Stdin) - if err != nil { - return fmt.Errorf("error reading stdin: %w", err) - } - } - - if len(inputBytes) > 0 { - if reader == nil { - return fmt.Errorf("input format is required when reading stdin") - } - inputData, err = reader.Read(inputBytes) - if err != nil { - return fmt.Errorf("error reading input: %w", err) - } + if c.Interactive { + return NewInteractiveCmd(c).Run(ctx) } - opts = append(opts, execution.WithVariable("root", inputData)) + o := runOpts{ + Vars: c.Vars, + ExtReadFlags: c.ExtReadFlags, + ExtWriteFlags: c.ExtWriteFlags, + InFormat: c.InFormat, + OutFormat: c.OutFormat, + ReturnRoot: c.ReturnRoot, + Unstable: c.Unstable, + Query: c.Query, - if c.Unstable { - opts = append(opts, execution.WithUnstable()) + Stdin: ctx.Stdin, } - - options := execution.NewOptions(opts...) - out, err := execution.ExecuteSelector(c.Query, inputData, options) + outBytes, err := run(o) if err != nil { return err } - if c.ReturnRoot { - out = inputData - } - - outputBytes, err := writer.Write(out) - if err != nil { - return fmt.Errorf("error writing output: %w", err) - } - - _, err = ctx.Stdout.Write(outputBytes) + _, err = ctx.Stdout.Write(outBytes) if err != nil { return fmt.Errorf("error writing output: %w", err) } diff --git a/internal/cli/read_write_flag.go b/internal/cli/read_write_flag.go new file mode 100644 index 00000000..d38e7c8e --- /dev/null +++ b/internal/cli/read_write_flag.go @@ -0,0 +1,59 @@ +package cli + +import ( + "fmt" + "reflect" + "strings" + + "github.com/alecthomas/kong" + "github.com/tomwright/dasel/v3/parsing" +) + +type extReadWriteFlag struct { + Name string + Value string +} + +type extReadWriteFlags *[]extReadWriteFlag + +func applyReaderFlags(readerOptions *parsing.ReaderOptions, f extReadWriteFlags) { + if f != nil { + for _, flag := range *f { + readerOptions.Ext[flag.Name] = flag.Value + } + } +} + +func applyWriterFlags(writerOptions *parsing.WriterOptions, f extReadWriteFlags) { + if f != nil { + for _, flag := range *f { + writerOptions.Ext[flag.Name] = flag.Value + } + } +} + +type extReadWriteFlagMapper struct { +} + +func (vm *extReadWriteFlagMapper) Decode(ctx *kong.DecodeContext, target reflect.Value) error { + t := ctx.Scan.Pop() + + strVal, ok := t.Value.(string) + if !ok { + return fmt.Errorf("expected string value for variable") + } + + nameValueSplit := strings.SplitN(strVal, "=", 2) + if len(nameValueSplit) != 2 { + return fmt.Errorf("invalid read/write flag format, expect foo=bar") + } + + res := extReadWriteFlag{ + Name: nameValueSplit[0], + Value: nameValueSplit[1], + } + + target.Elem().Set(reflect.Append(target.Elem(), reflect.ValueOf(res))) + + return nil +} diff --git a/internal/cli/run.go b/internal/cli/run.go new file mode 100644 index 00000000..baffccc0 --- /dev/null +++ b/internal/cli/run.go @@ -0,0 +1,99 @@ +package cli + +import ( + "fmt" + "io" + + "github.com/tomwright/dasel/v3/execution" + "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/parsing" +) + +type runOpts struct { + Vars variables + ExtReadFlags extReadWriteFlags + ExtWriteFlags extReadWriteFlags + InFormat string + OutFormat string + ReturnRoot bool + Unstable bool + Query string + + Stdin io.Reader +} + +func run(o runOpts) ([]byte, error) { + var opts []execution.ExecuteOptionFn + + if o.OutFormat == "" && o.InFormat != "" { + o.OutFormat = o.InFormat + } else if o.OutFormat != "" && o.InFormat == "" { + o.InFormat = o.OutFormat + } + + readerOptions := parsing.DefaultReaderOptions() + applyReaderFlags(&readerOptions, o.ExtReadFlags) + + var reader parsing.Reader + var err error + if len(o.InFormat) > 0 { + reader, err = parsing.Format(o.InFormat).NewReader(readerOptions) + if err != nil { + return nil, fmt.Errorf("failed to get input reader: %w", err) + } + } + + writerOptions := parsing.DefaultWriterOptions() + applyWriterFlags(&writerOptions, o.ExtWriteFlags) + + writer, err := parsing.Format(o.OutFormat).NewWriter(writerOptions) + if err != nil { + return nil, fmt.Errorf("failed to get output writer: %w", err) + } + + opts = append(opts, variableOptions(o.Vars)...) + + // Default to null. If stdin is being read then this will be overwritten. + inputData := model.NewNullValue() + + var inputBytes []byte + if o.Stdin != nil { + inputBytes, err = io.ReadAll(o.Stdin) + if err != nil { + return nil, fmt.Errorf("error reading stdin: %w", err) + } + } + + if len(inputBytes) > 0 { + if reader == nil { + return nil, fmt.Errorf("input format is required when reading stdin") + } + inputData, err = reader.Read(inputBytes) + if err != nil { + return nil, fmt.Errorf("error reading input: %w", err) + } + } + + opts = append(opts, execution.WithVariable("root", inputData)) + + if o.Unstable { + opts = append(opts, execution.WithUnstable()) + } + + options := execution.NewOptions(opts...) + out, err := execution.ExecuteSelector(o.Query, inputData, options) + if err != nil { + return nil, err + } + + if o.ReturnRoot { + out = inputData + } + + outputBytes, err := writer.Write(out) + if err != nil { + return nil, fmt.Errorf("error writing output: %w", err) + } + + return outputBytes, nil +} diff --git a/internal/cli/variable.go b/internal/cli/variable.go index 82536234..124ee7c3 100644 --- a/internal/cli/variable.go +++ b/internal/cli/variable.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/alecthomas/kong" + "github.com/tomwright/dasel/v3/execution" "github.com/tomwright/dasel/v3/model" "github.com/tomwright/dasel/v3/parsing" ) @@ -17,6 +18,18 @@ type variable struct { Value *model.Value } +type variables *[]variable + +func variableOptions(vars variables) []execution.ExecuteOptionFn { + var opts []execution.ExecuteOptionFn + if vars != nil { + for _, v := range *vars { + opts = append(opts, execution.WithVariable(v.Name, v.Value)) + } + } + return opts +} + type variableMapper struct { } @@ -65,7 +78,7 @@ func (vm *variableMapper) Decode(ctx *kong.DecodeContext, target reflect.Value) valueRaw = string(contents) } - reader, err := parsing.Format(format).NewReader() + reader, err := parsing.Format(format).NewReader(parsing.DefaultReaderOptions()) if err != nil { return fmt.Errorf("failed to create reader: %w", err) } diff --git a/model/value.go b/model/value.go index c01653f2..1a1ec255 100644 --- a/model/value.go +++ b/model/value.go @@ -4,6 +4,7 @@ import ( "fmt" "reflect" "slices" + "strings" ) type Type string @@ -50,6 +51,66 @@ type Value struct { setFn func(*Value) error } +func (v *Value) String() string { + switch v.Type() { + case TypeString: + val, err := v.StringValue() + if err != nil { + panic(err) + } + return fmt.Sprintf("string{%s}", val) + case TypeInt: + val, err := v.IntValue() + if err != nil { + panic(err) + } + return fmt.Sprintf("int{%d}", val) + case TypeFloat: + val, err := v.FloatValue() + if err != nil { + panic(err) + } + return fmt.Sprintf("float(%g)", val) + case TypeBool: + val, err := v.BoolValue() + if err != nil { + panic(err) + } + return fmt.Sprintf("bool{%t}", val) + case TypeMap: + res := fmt.Sprintf("map[%d]{", len(v.Metadata)) + if err := v.RangeMap(func(k string, v *Value) error { + res += fmt.Sprintf("%s: %s, ", k, v.String()) + return nil + }); err != nil { + panic(err) + } + res = strings.TrimSuffix(res, ", ") + return res + "}" + case TypeSlice: + md := "" + if v.IsSpread() { + md = "spread, " + } + if v.IsBranch() { + md += "branch, " + } + res := fmt.Sprintf("array[%s]{", strings.TrimSuffix(md, ", ")) + if err := v.RangeSlice(func(k int, v *Value) error { + res += fmt.Sprintf("%d: %s, ", k, v.String()) + return nil + }); err != nil { + panic(err) + } + res = strings.TrimSuffix(res, ", ") + return res + "}" + case TypeNull: + return "null" + default: + return fmt.Sprintf("unknown[%s]", v.Interface()) + } +} + // NewValue creates a new value. func NewValue(v any) *Value { switch val := v.(type) { diff --git a/model/value_slice.go b/model/value_slice.go index 59cfc5a6..52ce98a6 100644 --- a/model/value_slice.go +++ b/model/value_slice.go @@ -26,6 +26,14 @@ func (v *Value) isSlice() bool { // Append appends a value to the slice. func (v *Value) Append(val *Value) error { + // Branches behave differently when appending to a slice. + // We expect each item in a branch to be its own value. + if val.IsBranch() { + return val.RangeSlice(func(_ int, item *Value) error { + return v.Append(item) + }) + } + unpacked := v.UnpackKinds(reflect.Interface, reflect.Ptr) if !unpacked.isSlice() { return ErrUnexpectedType{ diff --git a/parsing/d/reader.go b/parsing/d/reader.go index 4e1c9204..0e7b544b 100644 --- a/parsing/d/reader.go +++ b/parsing/d/reader.go @@ -19,7 +19,7 @@ func init() { parsing.RegisterReader(Dasel, newDaselReader) } -func newDaselReader() (parsing.Reader, error) { +func newDaselReader(options parsing.ReaderOptions) (parsing.Reader, error) { return &daselReader{}, nil } diff --git a/parsing/format.go b/parsing/format.go index 73a6a0ae..096c5fc3 100644 --- a/parsing/format.go +++ b/parsing/format.go @@ -8,12 +8,12 @@ import ( type Format string // NewReader creates a new reader for the format. -func (f Format) NewReader() (Reader, error) { +func (f Format) NewReader(options ReaderOptions) (Reader, error) { fn, ok := readers[f] if !ok { return nil, fmt.Errorf("unsupported reader file format: %s", f) } - return fn() + return fn(options) } // NewWriter creates a new writer for the format. diff --git a/parsing/json/json.go b/parsing/json/json.go index 43e3d8e9..5486d192 100644 --- a/parsing/json/json.go +++ b/parsing/json/json.go @@ -29,7 +29,7 @@ func init() { parsing.RegisterWriter(JSON, newJSONWriter) } -func newJSONReader() (parsing.Reader, error) { +func newJSONReader(options parsing.ReaderOptions) (parsing.Reader, error) { return &jsonReader{}, nil } diff --git a/parsing/json/json_test.go b/parsing/json/json_test.go index 02a2fab9..8c104248 100644 --- a/parsing/json/json_test.go +++ b/parsing/json/json_test.go @@ -25,7 +25,7 @@ func TestJson(t *testing.T) { } } `) - reader, err := json.JSON.NewReader() + reader, err := json.JSON.NewReader(parsing.DefaultReaderOptions()) if err != nil { t.Fatal(err) } diff --git a/parsing/reader.go b/parsing/reader.go index 1d66eafe..c8e49027 100644 --- a/parsing/reader.go +++ b/parsing/reader.go @@ -4,6 +4,17 @@ import "github.com/tomwright/dasel/v3/model" var readers = map[Format]NewReaderFn{} +type ReaderOptions struct { + Ext map[string]string +} + +// DefaultReaderOptions returns the default reader options. +func DefaultReaderOptions() ReaderOptions { + return ReaderOptions{ + Ext: make(map[string]string), + } +} + // Reader reads a value from a byte slice. type Reader interface { // Read reads a value from a byte slice. @@ -11,7 +22,7 @@ type Reader interface { } // NewReaderFn is a function that creates a new reader. -type NewReaderFn func() (Reader, error) +type NewReaderFn func(options ReaderOptions) (Reader, error) // RegisterReader registers a new reader for the format. func RegisterReader(format Format, fn NewReaderFn) { diff --git a/parsing/toml/toml.go b/parsing/toml/toml.go index 3695d5f8..feef8457 100644 --- a/parsing/toml/toml.go +++ b/parsing/toml/toml.go @@ -19,7 +19,7 @@ func init() { parsing.RegisterWriter(TOML, newTOMLWriter) } -func newTOMLReader() (parsing.Reader, error) { +func newTOMLReader(options parsing.ReaderOptions) (parsing.Reader, error) { return &tomlReader{}, nil } diff --git a/parsing/writer.go b/parsing/writer.go index 97c4f236..27165b66 100644 --- a/parsing/writer.go +++ b/parsing/writer.go @@ -7,6 +7,7 @@ var writers = map[Format]NewWriterFn{} type WriterOptions struct { Compact bool Indent string + Ext map[string]string } // DefaultWriterOptions returns the default writer options. @@ -14,6 +15,7 @@ func DefaultWriterOptions() WriterOptions { return WriterOptions{ Compact: false, Indent: " ", + Ext: make(map[string]string), } } diff --git a/parsing/xml/xml.go b/parsing/xml/xml.go index b7302b46..c97d0337 100644 --- a/parsing/xml/xml.go +++ b/parsing/xml/xml.go @@ -20,15 +20,15 @@ const ( var _ parsing.Reader = (*xmlReader)(nil) var _ parsing.Writer = (*xmlWriter)(nil) -//var _ parsing.Writer = (*xmlWriter)(nil) - func init() { parsing.RegisterReader(XML, newXMLReader) parsing.RegisterWriter(XML, newXMLWriter) } -func newXMLReader() (parsing.Reader, error) { - return &xmlReader{}, nil +func newXMLReader(options parsing.ReaderOptions) (parsing.Reader, error) { + return &xmlReader{ + structured: options.Ext["xml-mode"] == "structured", + }, nil } // NewXMLWriter creates a new XML writer. @@ -38,7 +38,9 @@ func newXMLWriter(options parsing.WriterOptions) (parsing.Writer, error) { }, nil } -type xmlReader struct{} +type xmlReader struct { + structured bool +} // Read reads a value from a byte slice. func (j *xmlReader) Read(data []byte) (*model.Value, error) { @@ -54,7 +56,10 @@ func (j *xmlReader) Read(data []byte) (*model.Value, error) { return nil, err } - return el.toModel() + if j.structured { + return el.toStructuredModel() + } + return el.toFriendlyModel() } type xmlAttr struct { @@ -69,7 +74,7 @@ type xmlElement struct { Content string } -func (e *xmlElement) toModel() (*model.Value, error) { +func (e *xmlElement) toStructuredModel() (*model.Value, error) { attrs := model.NewMapValue() for _, attr := range e.Attrs { if err := attrs.SetMapKey(attr.Name, model.NewStringValue(attr.Value)); err != nil { @@ -89,7 +94,7 @@ func (e *xmlElement) toModel() (*model.Value, error) { } children := model.NewSliceValue() for _, child := range e.Children { - childModel, err := child.toModel() + childModel, err := child.toStructuredModel() if err != nil { return nil, err } @@ -103,6 +108,69 @@ func (e *xmlElement) toModel() (*model.Value, error) { return res, nil } +func (e *xmlElement) toFriendlyModel() (*model.Value, error) { + if len(e.Attrs) == 0 && len(e.Children) == 0 { + return model.NewStringValue(e.Content), nil + } + + res := model.NewMapValue() + for _, attr := range e.Attrs { + if err := res.SetMapKey("-"+attr.Name, model.NewStringValue(attr.Value)); err != nil { + return nil, err + } + } + + if len(e.Content) > 0 { + if err := res.SetMapKey("#text", model.NewStringValue(e.Content)); err != nil { + return nil, err + } + } + + if len(e.Children) > 0 { + childElementKeys := make([]string, 0) + childElements := make(map[string][]*xmlElement) + + for _, child := range e.Children { + if _, ok := childElements[child.Name]; !ok { + childElementKeys = append(childElementKeys, child.Name) + } + childElements[child.Name] = append(childElements[child.Name], child) + } + + for _, key := range childElementKeys { + cs := childElements[key] + switch len(cs) { + case 0: + continue + case 1: + childModel, err := cs[0].toFriendlyModel() + if err != nil { + return nil, err + } + if err := res.SetMapKey(key, childModel); err != nil { + return nil, err + } + default: + children := model.NewSliceValue() + for _, child := range cs { + childModel, err := child.toFriendlyModel() + if err != nil { + return nil, err + } + if err := children.Append(childModel); err != nil { + return nil, err + } + } + if err := res.SetMapKey(key, children); err != nil { + return nil, err + } + } + } + } + + return res, nil +} + func (j *xmlReader) parseElement(decoder *xml.Decoder, element xml.StartElement) (*xmlElement, error) { el := &xmlElement{ Name: element.Name.Local, diff --git a/parsing/xml/xml_test.go b/parsing/xml/xml_test.go index 6d693871..a72629dc 100644 --- a/parsing/xml/xml_test.go +++ b/parsing/xml/xml_test.go @@ -16,7 +16,7 @@ type testCase struct { } func (tc testCase) run(t *testing.T) { - r, err := xml.XML.NewReader() + r, err := xml.XML.NewReader(parsing.DefaultReaderOptions()) if err != nil { t.Fatalf("unexpected error: %s", err) } @@ -37,7 +37,7 @@ func (tc rwTestCase) run(t *testing.T) { if tc.out == "" { tc.out = tc.in } - r, err := xml.XML.NewReader() + r, err := xml.XML.NewReader(parsing.DefaultReaderOptions()) if err != nil { t.Fatalf("unexpected error: %s", err) } diff --git a/parsing/yaml/yaml.go b/parsing/yaml/yaml.go index ce539d80..cc9aae4b 100644 --- a/parsing/yaml/yaml.go +++ b/parsing/yaml/yaml.go @@ -22,7 +22,7 @@ func init() { parsing.RegisterWriter(YAML, newYAMLWriter) } -func newYAMLReader() (parsing.Reader, error) { +func newYAMLReader(options parsing.ReaderOptions) (parsing.Reader, error) { return &yamlReader{}, nil } diff --git a/parsing/yaml/yaml_test.go b/parsing/yaml/yaml_test.go index 264e8abf..12c4da45 100644 --- a/parsing/yaml/yaml_test.go +++ b/parsing/yaml/yaml_test.go @@ -16,7 +16,7 @@ type testCase struct { } func (tc testCase) run(t *testing.T) { - r, err := yaml.YAML.NewReader() + r, err := yaml.YAML.NewReader(parsing.DefaultReaderOptions()) if err != nil { t.Fatalf("unexpected error: %s", err) } @@ -37,7 +37,7 @@ func (tc rwTestCase) run(t *testing.T) { if tc.out == "" { tc.out = tc.in } - r, err := yaml.YAML.NewReader() + r, err := yaml.YAML.NewReader(parsing.DefaultReaderOptions()) if err != nil { t.Fatalf("unexpected error: %s", err) } diff --git a/selector/parser/parse_array.go b/selector/parser/parse_array.go index 0e3e6454..8f3cadb2 100644 --- a/selector/parser/parse_array.go +++ b/selector/parser/parse_array.go @@ -22,9 +22,16 @@ func parseArray(p *Parser) (ast.Expr, error) { return nil, err } - return ast.ArrayExpr{ + arr := ast.ArrayExpr{ Exprs: elements, - }, nil + } + + res, err := parseFollowingSymbol(p, arr) + if err != nil { + return nil, err + } + + return res, nil } func parseIndex(p *Parser) (ast.Expr, error) { diff --git a/selector/parser/parse_object.go b/selector/parser/parse_object.go index 40566c7d..d43e954f 100644 --- a/selector/parser/parse_object.go +++ b/selector/parser/parse_object.go @@ -95,7 +95,14 @@ func parseObject(p *Parser) (ast.Expr, error) { } p.advance() - return ast.ObjectExpr{ + obj := ast.ObjectExpr{ Pairs: pairs, - }, nil + } + + res, err := parseFollowingSymbol(p, obj) + if err != nil { + return nil, err + } + + return res, nil } From f6f2be30d158c95faea9642bf777855d72636ea3 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Tue, 29 Oct 2024 18:47:21 +0000 Subject: [PATCH 50/56] Pass stdout option --- internal/cli/interactive.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/internal/cli/interactive.go b/internal/cli/interactive.go index a84ef9a4..2d39794c 100644 --- a/internal/cli/interactive.go +++ b/internal/cli/interactive.go @@ -73,10 +73,14 @@ func (c *InteractiveCmd) Run(ctx *Globals) error { m := initialModel(c, c.Query, stdInBytes) - p := tea.NewProgram(m) + p := tea.NewProgram(m, tea.WithOutput(ctx.Stdout)) _, err = p.Run() - return err + if err != nil { + return err + } + + return nil } func initialModel(it *InteractiveCmd, defaultCommand string, stdInBytes []byte) interactiveTeaModel { @@ -124,6 +128,9 @@ func (m interactiveTeaModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.previousCommand = m.currentCommand m.currentCommand = m.commandInput.Value() + if m.firstUpdate { + cmds = append(cmds, tea.EnterAltScreen) + } if m.firstUpdate || m.currentCommand != m.previousCommand { m.firstUpdate = false // If the command has changed, we need to execute it @@ -160,12 +167,7 @@ func (m interactiveTeaModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { - case tea.KeyEsc: - return m, tea.Quit - //if m.commandInput.Focused() { - // m.commandInput.Blur() - //} - case tea.KeyCtrlC: + case tea.KeyEsc, tea.KeyCtrlC: return m, tea.Quit default: if !m.commandInput.Focused() { @@ -180,7 +182,7 @@ func (m interactiveTeaModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { resultHeaderHeight := lipgloss.Height(m.resultHeaderView()) verticalMarginHeight := headerHeight + 2 + commandInputHeight + resultHeaderHeight - viewportHeight := msg.Height - verticalMarginHeight + viewportHeight := msg.Height - verticalMarginHeight - 2 viewportWidth := (msg.Width / 2) - 4 if !m.outputReady { From d67cb15ebcc09b698dcd253d2b27ade01d364402 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Tue, 29 Oct 2024 23:38:42 +0000 Subject: [PATCH 51/56] Fix bug with parsing unfinished quoted strings --- selector/lexer/token.go | 17 ++- selector/lexer/tokenize.go | 28 ++-- selector/lexer/tokenize_test.go | 174 ++++++++++++++++--------- selector/parser/parser_test.go | 220 ++++++++++++++++---------------- 4 files changed, 249 insertions(+), 190 deletions(-) diff --git a/selector/lexer/token.go b/selector/lexer/token.go index 96e838af..5c7e116c 100644 --- a/selector/lexer/token.go +++ b/selector/lexer/token.go @@ -106,15 +106,6 @@ func (t Token) IsKind(kind ...TokenKind) bool { return slices.Contains(kind, t.Kind) } -type PositionalError struct { - Pos int - Err error -} - -func (e *PositionalError) Error() string { - return fmt.Sprintf("%v. Position %d.", e.Pos, e.Err) -} - type UnexpectedTokenError struct { Pos int Token rune @@ -123,3 +114,11 @@ type UnexpectedTokenError struct { func (e *UnexpectedTokenError) Error() string { return fmt.Sprintf("failed to tokenize: unexpected token: %s at position %d.", string(e.Token), e.Pos) } + +type UnexpectedEOFError struct { + Pos int +} + +func (e *UnexpectedEOFError) Error() string { + return fmt.Sprintf("failed to tokenize: unexpected EOF at position %d.", e.Pos) +} diff --git a/selector/lexer/tokenize.go b/selector/lexer/tokenize.go index 535f3664..4aebae15 100644 --- a/selector/lexer/tokenize.go +++ b/selector/lexer/tokenize.go @@ -160,25 +160,35 @@ func (p *Tokenizer) parseCurRune() (Token, error) { pos := p.i buf := make([]rune, 0) pos++ - var escaped bool + foundCloseRune := false for pos < p.srcLen { - if p.src[pos] == p.src[p.i] && !escaped { + if p.src[pos] == p.src[p.i] { + foundCloseRune = true break } - if escaped { - escaped = false - buf = append(buf, rune(p.src[pos])) - pos++ - continue - } if p.src[pos] == '\\' { pos++ - escaped = true + buf = append(buf, rune(p.src[pos])) + pos++ continue } buf = append(buf, rune(p.src[pos])) pos++ } + if !foundCloseRune { + // We didn't find a closing quote. + if pos < p.srcLen { + // This shouldn't be possible. + return Token{}, &UnexpectedTokenError{ + Pos: p.i, + Token: rune(p.src[pos]), + } + } + // This can happen if the selector ends before the closing quote. + return Token{}, &UnexpectedEOFError{ + Pos: pos, + } + } res := NewToken(String, string(buf), p.i, pos+1-p.i) return res, nil default: diff --git a/selector/lexer/tokenize_test.go b/selector/lexer/tokenize_test.go index 19eb2a39..3d89c8f9 100644 --- a/selector/lexer/tokenize_test.go +++ b/selector/lexer/tokenize_test.go @@ -1,83 +1,135 @@ -package lexer +package lexer_test -import "testing" +import ( + "errors" + "testing" -func TestTokenizer_Parse(t *testing.T) { - type testCase struct { - in string - out []TokenKind + "github.com/tomwright/dasel/v3/selector/lexer" +) + +type testCase struct { + in string + out []lexer.TokenKind +} + +func (tc testCase) run(t *testing.T) { + tok := lexer.NewTokenizer(tc.in) + tokens, err := tok.Tokenize() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(tokens) != len(tc.out) { + t.Fatalf("unexpected number of tokens: %d", len(tokens)) + } + for i := range tokens { + if tokens[i].Kind != tc.out[i] { + t.Errorf("unexpected token kind at position %d: exp %v, got %v", i, tc.out[i], tokens[i].Kind) + return + } } +} + +type errTestCase struct { + in string + match func(error) bool +} - runTest := func(tc testCase) func(t *testing.T) { - return func(t *testing.T) { - tok := NewTokenizer(tc.in) - tokens, err := tok.Tokenize() - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if len(tokens) != len(tc.out) { - t.Fatalf("unexpected number of tokens: %d", len(tokens)) - } - for i := range tokens { - if tokens[i].Kind != tc.out[i] { - t.Errorf("unexpected token kind at position %d: exp %v, got %v", i, tc.out[i], tokens[i].Kind) - return - } - } +func (tc errTestCase) run(t *testing.T) { + tok := lexer.NewTokenizer(tc.in) + tokens, err := tok.Tokenize() + if !tc.match(err) { + t.Errorf("unexpected error, got %v", err) + } + if tokens != nil { + t.Errorf("unexpected tokens: %v", tokens) + } +} + +func matchUnexpectedError(r rune, p int) func(error) bool { + return func(err error) bool { + var e *lexer.UnexpectedTokenError + if !errors.As(err, &e) { + return false } + + return e.Token == r && e.Pos == p } +} + +func matchUnexpectedEOFError(p int) func(error) bool { + return func(err error) bool { + var e *lexer.UnexpectedEOFError + if !errors.As(err, &e) { + return false + } - t.Run("variables", runTest(testCase{ + return e.Pos == p + } +} + +func TestTokenizer_Parse(t *testing.T) { + t.Run("variables", testCase{ in: "$foo $bar123 $baz $", - out: []TokenKind{ - Variable, - Variable, - Variable, - Dollar, + out: []lexer.TokenKind{ + lexer.Variable, + lexer.Variable, + lexer.Variable, + lexer.Dollar, }, - })) + }.run) - t.Run("if", runTest(testCase{ + t.Run("if", testCase{ in: `if elseif else`, - out: []TokenKind{ - If, - ElseIf, - Else, + out: []lexer.TokenKind{ + lexer.If, + lexer.ElseIf, + lexer.Else, }, - })) + }.run) - t.Run("regex", runTest(testCase{ + t.Run("regex", testCase{ in: `r/asd/ r/hello there/`, - out: []TokenKind{ - RegexPattern, - RegexPattern, + out: []lexer.TokenKind{ + lexer.RegexPattern, + lexer.RegexPattern, }, - })) + }.run) - t.Run("sort by", runTest(testCase{ + t.Run("sort by", testCase{ in: `sortBy(foo, asc)`, - out: []TokenKind{ - SortBy, - OpenParen, - Symbol, - Comma, - Asc, - CloseParen, + out: []lexer.TokenKind{ + lexer.SortBy, + lexer.OpenParen, + lexer.Symbol, + lexer.Comma, + lexer.Asc, + lexer.CloseParen, }, - })) + }.run) - t.Run("everything", runTest(testCase{ + t.Run("everything", testCase{ in: "foo.bar.baz[1] != 42.123 || foo.bar.baz['hello'] == 42 && x == 'a\\'b' + false true . .... asd... $name null", - out: []TokenKind{ - Symbol, Dot, Symbol, Dot, Symbol, OpenBracket, Number, CloseBracket, NotEqual, Number, - Or, - Symbol, Dot, Symbol, Dot, Symbol, OpenBracket, String, CloseBracket, Equal, Number, - And, - Symbol, Equal, String, - Plus, Bool, Bool, - Dot, Spread, Dot, - Symbol, Spread, - Variable, Null, + out: []lexer.TokenKind{ + lexer.Symbol, lexer.Dot, lexer.Symbol, lexer.Dot, lexer.Symbol, lexer.OpenBracket, lexer.Number, lexer.CloseBracket, lexer.NotEqual, lexer.Number, + lexer.Or, + lexer.Symbol, lexer.Dot, lexer.Symbol, lexer.Dot, lexer.Symbol, lexer.OpenBracket, lexer.String, lexer.CloseBracket, lexer.Equal, lexer.Number, + lexer.And, + lexer.Symbol, lexer.Equal, lexer.String, + lexer.Plus, lexer.Bool, lexer.Bool, + lexer.Dot, lexer.Spread, lexer.Dot, + lexer.Symbol, lexer.Spread, + lexer.Variable, lexer.Null, }, - })) + }.run) + + t.Run("unhappy", func(t *testing.T) { + t.Run("unfinished double quote", errTestCase{ + in: `"hello`, + match: matchUnexpectedEOFError(6), + }.run) + t.Run("unfinished single quote", errTestCase{ + in: `'hello`, + match: matchUnexpectedEOFError(6), + }.run) + }) } diff --git a/selector/parser/parser_test.go b/selector/parser/parser_test.go index e04eec4e..43debac1 100644 --- a/selector/parser/parser_test.go +++ b/selector/parser/parser_test.go @@ -9,30 +9,28 @@ import ( "github.com/tomwright/dasel/v3/selector/parser" ) -func TestParser_Parse_HappyPath(t *testing.T) { - type testCase struct { - input string - expected ast.Expr - } +type happyTestCase struct { + input string + expected ast.Expr +} - run := func(t *testing.T, tc testCase) func(*testing.T) { - return func(t *testing.T) { - tokens, err := lexer.NewTokenizer(tc.input).Tokenize() - if err != nil { - t.Fatal(err) - } - got, err := parser.NewParser(tokens).Parse() - if err != nil { - t.Fatal(err) - } - if !cmp.Equal(tc.expected, got) { - t.Errorf("unexpected result: %s", cmp.Diff(tc.expected, got)) - } - } +func (tc happyTestCase) run(t *testing.T) { + tokens, err := lexer.NewTokenizer(tc.input).Tokenize() + if err != nil { + t.Fatal(err) + } + got, err := parser.NewParser(tokens).Parse() + if err != nil { + t.Fatal(err) } + if !cmp.Equal(tc.expected, got) { + t.Errorf("unexpected result: %s", cmp.Diff(tc.expected, got)) + } +} +func TestParser_Parse_HappyPath(t *testing.T) { t.Run("branching", func(t *testing.T) { - t.Run("two branches", run(t, testCase{ + t.Run("two branches", happyTestCase{ input: `branch("hello", len("world"))`, expected: ast.BranchExprs( ast.StringExpr{Value: "hello"}, @@ -43,80 +41,80 @@ func TestParser_Parse_HappyPath(t *testing.T) { }, ), ), - })) - t.Run("three branches", run(t, testCase{ + }.run) + t.Run("three branches", happyTestCase{ input: `branch("foo", "bar", "baz")`, expected: ast.BranchExprs( ast.StringExpr{Value: "foo"}, ast.StringExpr{Value: "bar"}, ast.StringExpr{Value: "baz"}, ), - })) + }.run) }) t.Run("literal access", func(t *testing.T) { - t.Run("string", run(t, testCase{ + t.Run("string", happyTestCase{ input: `"hello world"`, expected: ast.StringExpr{Value: "hello world"}, - })) - t.Run("int", run(t, testCase{ + }.run) + t.Run("int", happyTestCase{ input: "42", expected: ast.NumberIntExpr{Value: 42}, - })) - t.Run("float", run(t, testCase{ + }.run) + t.Run("float", happyTestCase{ input: "42.1", expected: ast.NumberFloatExpr{Value: 42.1}, - })) - t.Run("whole number float", run(t, testCase{ + }.run) + t.Run("whole number float", happyTestCase{ input: "42f", expected: ast.NumberFloatExpr{Value: 42}, - })) - t.Run("bool true lowercase", run(t, testCase{ + }.run) + t.Run("bool true lowercase", happyTestCase{ input: "true", expected: ast.BoolExpr{Value: true}, - })) - t.Run("bool true uppercase", run(t, testCase{ + }.run) + t.Run("bool true uppercase", happyTestCase{ input: "TRUE", expected: ast.BoolExpr{Value: true}, - })) - t.Run("bool true mixed case", run(t, testCase{ + }.run) + t.Run("bool true mixed case", happyTestCase{ input: "TrUe", expected: ast.BoolExpr{Value: true}, - })) - t.Run("bool false lowercase", run(t, testCase{ + }.run) + t.Run("bool false lowercase", happyTestCase{ input: "false", expected: ast.BoolExpr{Value: false}, - })) - t.Run("bool false uppercase", run(t, testCase{ + }.run) + t.Run("bool false uppercase", happyTestCase{ input: "FALSE", expected: ast.BoolExpr{Value: false}, - })) - t.Run("bool false mixed case", run(t, testCase{ + }.run) + t.Run("bool false mixed case", happyTestCase{ input: "FaLsE", expected: ast.BoolExpr{Value: false}, - })) + }.run) }) t.Run("property access", func(t *testing.T) { - t.Run("single property access", run(t, testCase{ + t.Run("single property access", happyTestCase{ input: "foo", expected: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, - })) - t.Run("chained property access", run(t, testCase{ + }.run) + t.Run("chained property access", happyTestCase{ input: "foo.bar", expected: ast.ChainExprs( ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.PropertyExpr{Property: ast.StringExpr{Value: "bar"}}, ), - })) - t.Run("property access spread", run(t, testCase{ + }.run) + t.Run("property access spread", happyTestCase{ input: "foo...", expected: ast.ChainExprs( ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.SpreadExpr{}, ), - })) - t.Run("property access spread into property access", run(t, testCase{ + }.run) + t.Run("property access spread into property access", happyTestCase{ input: "foo....bar", expected: ast.ChainExprs( ast.ChainExprs( @@ -125,20 +123,20 @@ func TestParser_Parse_HappyPath(t *testing.T) { ), ast.PropertyExpr{Property: ast.StringExpr{Value: "bar"}}, ), - })) + }.run) }) t.Run("array access", func(t *testing.T) { t.Run("root array", func(t *testing.T) { - t.Run("index", run(t, testCase{ + t.Run("index", happyTestCase{ input: "$this[1]", expected: ast.ChainExprs( ast.VariableExpr{Name: "this"}, ast.PropertyExpr{Property: ast.NumberIntExpr{Value: 1}}, ), - })) + }.run) t.Run("range", func(t *testing.T) { - t.Run("start and end funcs", run(t, testCase{ + t.Run("start and end funcs", happyTestCase{ input: "$this[calcStart(1):calcEnd()]", expected: ast.ChainExprs( ast.VariableExpr{Name: "this"}, @@ -154,56 +152,56 @@ func TestParser_Parse_HappyPath(t *testing.T) { }, }, ), - })) - t.Run("start and end", run(t, testCase{ + }.run) + t.Run("start and end", happyTestCase{ input: "$this[5:10]", expected: ast.ChainExprs( ast.VariableExpr{Name: "this"}, ast.RangeExpr{Start: ast.NumberIntExpr{Value: 5}, End: ast.NumberIntExpr{Value: 10}}, ), - })) - t.Run("start", run(t, testCase{ + }.run) + t.Run("start", happyTestCase{ input: "$this[5:]", expected: ast.ChainExprs( ast.VariableExpr{Name: "this"}, ast.RangeExpr{Start: ast.NumberIntExpr{Value: 5}}, ), - })) - t.Run("end", run(t, testCase{ + }.run) + t.Run("end", happyTestCase{ input: "$this[:10]", expected: ast.ChainExprs( ast.VariableExpr{Name: "this"}, ast.RangeExpr{End: ast.NumberIntExpr{Value: 10}}, ), - })) + }.run) }) t.Run("spread", func(t *testing.T) { - t.Run("standard", run(t, testCase{ + t.Run("standard", happyTestCase{ input: "$this...", expected: ast.ChainExprs( ast.VariableExpr{Name: "this"}, ast.SpreadExpr{}, ), - })) - t.Run("brackets", run(t, testCase{ + }.run) + t.Run("brackets", happyTestCase{ input: "$this[...]", expected: ast.ChainExprs( ast.VariableExpr{Name: "this"}, ast.SpreadExpr{}, ), - })) + }.run) }) }) t.Run("property array", func(t *testing.T) { - t.Run("index", run(t, testCase{ + t.Run("index", happyTestCase{ input: "foo[1]", expected: ast.ChainExprs( ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.PropertyExpr{Property: ast.NumberIntExpr{Value: 1}}, ), - })) + }.run) t.Run("range", func(t *testing.T) { - t.Run("start and end funcs", run(t, testCase{ + t.Run("start and end funcs", happyTestCase{ input: "foo[calcStart(1):calcEnd()]", expected: ast.ChainExprs( ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, @@ -219,50 +217,50 @@ func TestParser_Parse_HappyPath(t *testing.T) { }, }, ), - })) - t.Run("start and end", run(t, testCase{ + }.run) + t.Run("start and end", happyTestCase{ input: "foo[5:10]", expected: ast.ChainExprs( ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.RangeExpr{Start: ast.NumberIntExpr{Value: 5}, End: ast.NumberIntExpr{Value: 10}}, ), - })) - t.Run("start", run(t, testCase{ + }.run) + t.Run("start", happyTestCase{ input: "foo[5:]", expected: ast.ChainExprs( ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.RangeExpr{Start: ast.NumberIntExpr{Value: 5}}, ), - })) - t.Run("end", run(t, testCase{ + }.run) + t.Run("end", happyTestCase{ input: "foo[:10]", expected: ast.ChainExprs( ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.RangeExpr{End: ast.NumberIntExpr{Value: 10}}, ), - })) + }.run) }) t.Run("spread", func(t *testing.T) { - t.Run("standard", run(t, testCase{ + t.Run("standard", happyTestCase{ input: "foo...", expected: ast.ChainExprs( ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.SpreadExpr{}, ), - })) - t.Run("brackets", run(t, testCase{ + }.run) + t.Run("brackets", happyTestCase{ input: "foo[...]", expected: ast.ChainExprs( ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.SpreadExpr{}, ), - })) + }.run) }) }) }) t.Run("map", func(t *testing.T) { - t.Run("single property", run(t, testCase{ + t.Run("single property", happyTestCase{ input: "foo.map(x)", expected: ast.ChainExprs( ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, @@ -270,8 +268,8 @@ func TestParser_Parse_HappyPath(t *testing.T) { Expr: ast.PropertyExpr{Property: ast.StringExpr{Value: "x"}}, }, ), - })) - t.Run("nested property", run(t, testCase{ + }.run) + t.Run("nested property", happyTestCase{ input: "foo.map(x.y)", expected: ast.ChainExprs( ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, @@ -282,39 +280,39 @@ func TestParser_Parse_HappyPath(t *testing.T) { ), }, ), - })) + }.run) }) t.Run("object", func(t *testing.T) { - t.Run("get single property", run(t, testCase{ + t.Run("get single property", happyTestCase{ input: "{foo}", expected: ast.ObjectExpr{Pairs: []ast.KeyValue{ {Key: ast.StringExpr{Value: "foo"}, Value: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}}, }}, - })) - t.Run("get multiple properties", run(t, testCase{ + }.run) + t.Run("get multiple properties", happyTestCase{ input: "{foo, bar, baz}", expected: ast.ObjectExpr{Pairs: []ast.KeyValue{ {Key: ast.StringExpr{Value: "foo"}, Value: ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}}, {Key: ast.StringExpr{Value: "bar"}, Value: ast.PropertyExpr{Property: ast.StringExpr{Value: "bar"}}}, {Key: ast.StringExpr{Value: "baz"}, Value: ast.PropertyExpr{Property: ast.StringExpr{Value: "baz"}}}, }}, - })) - t.Run("set single property", run(t, testCase{ + }.run) + t.Run("set single property", happyTestCase{ input: `{"foo":1}`, expected: ast.ObjectExpr{Pairs: []ast.KeyValue{ {Key: ast.StringExpr{Value: "foo"}, Value: ast.NumberIntExpr{Value: 1}}, }}, - })) - t.Run("set multiple properties", run(t, testCase{ + }.run) + t.Run("set multiple properties", happyTestCase{ input: `{foo: 1, bar: 2, baz: 3}`, expected: ast.ObjectExpr{Pairs: []ast.KeyValue{ {Key: ast.StringExpr{Value: "foo"}, Value: ast.NumberIntExpr{Value: 1}}, {Key: ast.StringExpr{Value: "bar"}, Value: ast.NumberIntExpr{Value: 2}}, {Key: ast.StringExpr{Value: "baz"}, Value: ast.NumberIntExpr{Value: 3}}, }}, - })) - t.Run("combine get set", run(t, testCase{ + }.run) + t.Run("combine get set", happyTestCase{ input: `{ ..., nestedSpread..., @@ -331,41 +329,41 @@ func TestParser_Parse_HappyPath(t *testing.T) { {Key: ast.StringExpr{Value: "baz"}, Value: ast.CallExpr{Function: "evalSomething"}}, {Key: ast.StringExpr{Value: "Name"}, Value: ast.StringExpr{Value: "Tom"}}, }}, - })) + }.run) }) t.Run("variables", func(t *testing.T) { - t.Run("single variable", run(t, testCase{ + t.Run("single variable", happyTestCase{ input: `$foo`, expected: ast.VariableExpr{Name: "foo"}, - })) - t.Run("variable passed to func", run(t, testCase{ + }.run) + t.Run("variable passed to func", happyTestCase{ input: `len($foo)`, expected: ast.CallExpr{Function: "len", Args: ast.Expressions{ast.VariableExpr{Name: "foo"}}}, - })) + }.run) }) t.Run("combinations and grouping", func(t *testing.T) { - t.Run("string concat with grouping", run(t, testCase{ + t.Run("string concat with grouping", happyTestCase{ input: `(foo.a) + (foo.b)`, expected: ast.BinaryExpr{ Left: ast.ChainExprs(ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.PropertyExpr{Property: ast.StringExpr{Value: "a"}}), Operator: lexer.Token{Kind: lexer.Plus, Value: "+", Pos: 8, Len: 1}, Right: ast.ChainExprs(ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.PropertyExpr{Property: ast.StringExpr{Value: "b"}}), }, - })) - t.Run("string concat with nested properties", run(t, testCase{ + }.run) + t.Run("string concat with nested properties", happyTestCase{ input: `foo.a + foo.b`, expected: ast.BinaryExpr{ Left: ast.ChainExprs(ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.PropertyExpr{Property: ast.StringExpr{Value: "a"}}), Operator: lexer.Token{Kind: lexer.Plus, Value: "+", Pos: 6, Len: 1}, Right: ast.ChainExprs(ast.PropertyExpr{Property: ast.StringExpr{Value: "foo"}}, ast.PropertyExpr{Property: ast.StringExpr{Value: "b"}}), }, - })) + }.run) }) t.Run("conditional", func(t *testing.T) { - t.Run("if", run(t, testCase{ + t.Run("if", happyTestCase{ input: `if (foo == 1) { "yes" } else { "no" }`, expected: ast.ConditionalExpr{ Cond: ast.BinaryExpr{ @@ -376,8 +374,8 @@ func TestParser_Parse_HappyPath(t *testing.T) { Then: ast.StringExpr{Value: "yes"}, Else: ast.StringExpr{Value: "no"}, }, - })) - t.Run("if elseif else", run(t, testCase{ + }.run) + t.Run("if elseif else", happyTestCase{ input: `if (foo == 1) { "yes" } elseif (foo == 2) { "maybe" } else { "no" }`, expected: ast.ConditionalExpr{ Cond: ast.BinaryExpr{ @@ -396,8 +394,8 @@ func TestParser_Parse_HappyPath(t *testing.T) { Else: ast.StringExpr{Value: "no"}, }, }, - })) - t.Run("if elseif elseif else", run(t, testCase{ + }.run) + t.Run("if elseif elseif else", happyTestCase{ input: `if (foo == 1) { "yes" } elseif (foo == 2) { "maybe" } elseif (foo == 3) { "probably" } else { "no" }`, expected: ast.ConditionalExpr{ Cond: ast.BinaryExpr{ @@ -424,11 +422,11 @@ func TestParser_Parse_HappyPath(t *testing.T) { }, }, }, - })) + }.run) }) t.Run("coalesce", func(t *testing.T) { - t.Run("chained on left side", run(t, testCase{ + t.Run("chained on left side", happyTestCase{ input: `foo ?? bar ?? baz`, expected: ast.BinaryExpr{ Left: ast.BinaryExpr{ @@ -439,9 +437,9 @@ func TestParser_Parse_HappyPath(t *testing.T) { Operator: lexer.Token{Kind: lexer.DoubleQuestionMark, Value: "??", Pos: 11, Len: 2}, Right: ast.PropertyExpr{Property: ast.StringExpr{Value: "baz"}}, }, - })) + }.run) - t.Run("chained nested on left side", run(t, testCase{ + t.Run("chained nested on left side", happyTestCase{ input: `nested.one ?? nested.two ?? nested.three ?? 10`, expected: ast.BinaryExpr{ Left: ast.BinaryExpr{ @@ -465,6 +463,6 @@ func TestParser_Parse_HappyPath(t *testing.T) { Operator: lexer.Token{Kind: lexer.DoubleQuestionMark, Value: "??", Pos: 41, Len: 2}, Right: ast.NumberIntExpr{Value: 10}, }, - })) + }.run) }) } From 11c723ea7c57be64b0d9d122fa15e6996343083c Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Wed, 30 Oct 2024 17:26:16 +0000 Subject: [PATCH 52/56] General fixes and improvements --- execution/execute.go | 3 + execution/execute_func.go | 9 + execution/func.go | 4 + execution/func_base64.go | 40 ++++ execution/func_ignore.go | 15 ++ execution/func_parse.go | 42 ++++ internal/cli/interactive.go | 304 ++++--------------------- internal/cli/interactive_tea.go | 209 +++++++++++++++++ internal/cli/interactive_tea_input.go | 50 ++++ internal/cli/interactive_tea_output.go | 106 +++++++++ model/value_metadata.go | 18 ++ 11 files changed, 539 insertions(+), 261 deletions(-) create mode 100644 execution/func_base64.go create mode 100644 execution/func_ignore.go create mode 100644 execution/func_parse.go create mode 100644 internal/cli/interactive_tea.go create mode 100644 internal/cli/interactive_tea_input.go create mode 100644 internal/cli/interactive_tea_output.go diff --git a/execution/execute.go b/execution/execute.go index bf3ef770..88d120aa 100644 --- a/execution/execute.go +++ b/execution/execute.go @@ -59,6 +59,9 @@ func ExecuteAST(expr ast.Expr, value *model.Value, options *Options) (*model.Val if err != nil { return err } + if r.IsIgnore() { + return nil + } return res.Append(r) }); err != nil { return nil, fmt.Errorf("branch execution error: %w", err) diff --git a/execution/execute_func.go b/execution/execute_func.go index 4712ab90..32466df0 100644 --- a/execution/execute_func.go +++ b/execution/execute_func.go @@ -1,7 +1,9 @@ package execution import ( + "errors" "fmt" + "slices" "github.com/tomwright/dasel/v3/model" "github.com/tomwright/dasel/v3/selector/ast" @@ -41,7 +43,14 @@ func callFnExecutor(opts *Options, f FuncFn, argsE ast.Expressions) (expressionE }, nil } +var unstableFuncs = []string{ + "ignore", +} + func callExprExecutor(opts *Options, e ast.CallExpr) (expressionExecutor, error) { + if !opts.Unstable && (slices.Contains(unstableFuncs, e.Function)) { + return nil, errors.New("unstable function are not enabled. to enable them use --unstable") + } if f, ok := opts.Funcs.Get(e.Function); ok { res, err := callFnExecutor(opts, f, e.Args) if err != nil { diff --git a/execution/func.go b/execution/func.go index d0ea4b71..452059d5 100644 --- a/execution/func.go +++ b/execution/func.go @@ -19,6 +19,10 @@ var ( FuncTypeOf, FuncMax, FuncMin, + FuncIgnore, + FuncBase64Encode, + FuncBase64Decode, + FuncParse, ) ) diff --git a/execution/func_base64.go b/execution/func_base64.go new file mode 100644 index 00000000..95b1466f --- /dev/null +++ b/execution/func_base64.go @@ -0,0 +1,40 @@ +package execution + +import ( + "encoding/base64" + + "github.com/tomwright/dasel/v3/model" +) + +// FuncBase64Encode base64 encodes the given value. +var FuncBase64Encode = NewFunc( + "base64e", + func(data *model.Value, args model.Values) (*model.Value, error) { + arg := args[0] + strVal, err := arg.StringValue() + if err != nil { + return nil, err + } + out := base64.StdEncoding.EncodeToString([]byte(strVal)) + return model.NewStringValue(out), nil + }, + ValidateArgsExactly(1), +) + +// FuncBase64Decode base64 decodes the given value. +var FuncBase64Decode = NewFunc( + "base64d", + func(data *model.Value, args model.Values) (*model.Value, error) { + arg := args[0] + strVal, err := arg.StringValue() + if err != nil { + return nil, err + } + out, err := base64.StdEncoding.DecodeString(strVal) + if err != nil { + return nil, err + } + return model.NewStringValue(string(out)), nil + }, + ValidateArgsExactly(1), +) diff --git a/execution/func_ignore.go b/execution/func_ignore.go new file mode 100644 index 00000000..351e9008 --- /dev/null +++ b/execution/func_ignore.go @@ -0,0 +1,15 @@ +package execution + +import ( + "github.com/tomwright/dasel/v3/model" +) + +// FuncIgnore is a function that ignores the value, causing it to be rejected from a branch. +var FuncIgnore = NewFunc( + "ignore", + func(data *model.Value, args model.Values) (*model.Value, error) { + data.MarkAsIgnore() + return data, nil + }, + ValidateArgsExactly(0), +) diff --git a/execution/func_parse.go b/execution/func_parse.go new file mode 100644 index 00000000..4214b2bd --- /dev/null +++ b/execution/func_parse.go @@ -0,0 +1,42 @@ +package execution + +import ( + "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/parsing" +) + +// FuncParse parses the given data at runtime. +var FuncParse = NewFunc( + "parse", + func(data *model.Value, args model.Values) (*model.Value, error) { + var format parsing.Format + var content []byte + { + strVal, err := args[0].StringValue() + if err != nil { + return nil, err + } + format = parsing.Format(strVal) + } + { + strVal, err := args[1].StringValue() + if err != nil { + return nil, err + } + content = []byte(strVal) + } + + reader, err := format.NewReader(parsing.DefaultReaderOptions()) + if err != nil { + return nil, err + } + + doc, err := reader.Read(content) + if err != nil { + return nil, err + } + + return doc, nil + }, + ValidateArgsExactly(2), +) diff --git a/internal/cli/interactive.go b/internal/cli/interactive.go index 2d39794c..6eee0ec1 100644 --- a/internal/cli/interactive.go +++ b/internal/cli/interactive.go @@ -4,38 +4,8 @@ import ( "bytes" "fmt" "io" - "strings" - "github.com/charmbracelet/bubbles/textarea" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/tomwright/dasel/v3/internal" -) - -const ( - useHighPerformanceRenderer = false -) - -var ( - titleStyle = func() lipgloss.Style { - b := lipgloss.HiddenBorder() - return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1) - }() - - resultTitleStyle = func() lipgloss.Style { - b := lipgloss.HiddenBorder() - return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1) - }() - - viewportStyle = func() lipgloss.Style { - b := lipgloss.RoundedBorder() - return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1) - }() - - commandInputStyle = func() lipgloss.Style { - return lipgloss.NewStyle() - }() + "github.com/tomwright/dasel/v3/parsing" ) func NewInteractiveCmd(queryCmd *QueryCmd) *InteractiveCmd { @@ -61,258 +31,70 @@ type InteractiveCmd struct { } func (c *InteractiveCmd) Run(ctx *Globals) error { - var err error var stdInBytes []byte = nil if ctx.Stdin != nil { + var err error stdInBytes, err = io.ReadAll(ctx.Stdin) if err != nil { return err } } - m := initialModel(c, c.Query, stdInBytes) - - p := tea.NewProgram(m, tea.WithOutput(ctx.Stdout)) - - _, err = p.Run() - if err != nil { - return err - } - - return nil -} - -func initialModel(it *InteractiveCmd, defaultCommand string, stdInBytes []byte) interactiveTeaModel { - ti := textarea.New() - ti.Placeholder = "Enter a query..." - ti.SetValue(defaultCommand) - ti.Focus() - ti.SetHeight(5) - ti.ShowLineNumbers = false - - return interactiveTeaModel{ - it: it, - commandInput: ti, - err: nil, - stdInBytes: stdInBytes, - output: "Loading...", - firstUpdate: true, + if c.InFormat == "" && c.OutFormat == "" { + c.InFormat = "json" + c.OutFormat = "json" + } else if c.InFormat == "" { + c.InFormat = c.OutFormat + } else if c.OutFormat == "" { + c.OutFormat = c.InFormat } -} - -type interactiveTeaModel struct { - it *InteractiveCmd - err error - originalErr error - commandInput textarea.Model - currentCommand string - output string - outputViewport viewport.Model - originalOutput string - originalOutputViewport viewport.Model - outputReady bool - previousCommand string - stdInBytes []byte - firstUpdate bool -} - -func (m interactiveTeaModel) Init() tea.Cmd { - return textarea.Blink -} - -func (m interactiveTeaModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmds []tea.Cmd - var cmd tea.Cmd - - m.previousCommand = m.currentCommand - - m.currentCommand = m.commandInput.Value() - if m.firstUpdate { - cmds = append(cmds, tea.EnterAltScreen) - } - if m.firstUpdate || m.currentCommand != m.previousCommand { - m.firstUpdate = false - // If the command has changed, we need to execute it - - { - out, err := m.execDasel(m.currentCommand, true) - if err != nil { - m.originalErr = err - } else { - m.originalErr = nil - m.originalOutput = out - m.originalOutputViewport.SetContent(m.originalOutput) - } - if m.originalErr != nil { - m.originalOutput = m.originalErr.Error() - m.originalOutputViewport.SetContent(m.originalOutput) - } - } - - out, err := m.execDasel(m.currentCommand, false) - if err != nil { - m.err = err - } else { - m.err = nil - m.output = out - m.outputViewport.SetContent(m.output) - } - if m.err != nil { - m.output = m.err.Error() - m.outputViewport.SetContent(m.output) - } - } - - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.Type { - case tea.KeyEsc, tea.KeyCtrlC: - return m, tea.Quit - default: - if !m.commandInput.Focused() { - cmd = m.commandInput.Focus() - cmds = append(cmds, cmd) - } - } - case tea.WindowSizeMsg: - headerHeight := lipgloss.Height(m.headerView()) - commandInputHeight := lipgloss.Height(m.commandInputView()) - resultHeaderHeight := lipgloss.Height(m.resultHeaderView()) - verticalMarginHeight := headerHeight + 2 + commandInputHeight + resultHeaderHeight - - viewportHeight := msg.Height - verticalMarginHeight - 2 - viewportWidth := (msg.Width / 2) - 4 - - if !m.outputReady { - m.outputReady = true - - m.commandInput.SetWidth(msg.Width) - - m.outputViewport = viewport.New(viewportWidth, viewportHeight) - //m.outputViewport.YPosition = verticalMarginHeight - m.outputViewport.HighPerformanceRendering = useHighPerformanceRenderer - m.outputViewport.SetContent(m.output) - m.commandInput.SetWidth(msg.Width) - if useHighPerformanceRenderer { - m.outputViewport.YPosition = headerHeight + 1 - } - - m.originalOutputViewport = viewport.New(viewportWidth, viewportHeight) - m.originalOutputViewport.YPosition = verticalMarginHeight - m.originalOutputViewport.HighPerformanceRendering = useHighPerformanceRenderer - m.originalOutputViewport.SetContent(m.output) - - if useHighPerformanceRenderer { - m.originalOutputViewport.YPosition = headerHeight + 1 + var runDasel interactiveDaselExecutor = func(selector string, root bool, formatIn parsing.Format, formatOut parsing.Format, in string) (res string, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic: %v", r) } + }() + var stdIn *bytes.Reader = nil + if in != "" { + stdIn = bytes.NewReader([]byte(in)) } else { - m.commandInput.SetWidth(msg.Width) - - m.outputViewport.Width = viewportWidth - m.outputViewport.Height = viewportHeight - m.outputViewport.SetContent(m.output) - - m.originalOutputViewport.Width = viewportWidth - m.originalOutputViewport.Height = viewportHeight - m.outputViewport.SetContent(m.originalOutput) + stdIn = bytes.NewReader([]byte{}) } - if useHighPerformanceRenderer { - // Render (or re-render) the whole viewport. Necessary both to - // initialize the viewport and when the window is resized. - // - // This is needed for high-performance rendering only. - cmds = append(cmds, viewport.Sync(m.outputViewport)) - cmds = append(cmds, viewport.Sync(m.originalOutputViewport)) + o := runOpts{ + Vars: c.Vars, + ExtReadFlags: c.ExtReadFlags, + ExtWriteFlags: c.ExtWriteFlags, + InFormat: formatIn.String(), + OutFormat: formatOut.String(), + ReturnRoot: root, + Unstable: true, + Query: selector, + + Stdin: stdIn, } - // We handle errors just like any other message - case error: - m.err = msg - return m, nil - } - - m.commandInput, cmd = m.commandInput.Update(msg) - cmds = append(cmds, cmd) - - m.outputViewport, cmd = m.outputViewport.Update(msg) - cmds = append(cmds, cmd) - - m.originalOutputViewport, cmd = m.originalOutputViewport.Update(msg) - cmds = append(cmds, cmd) - - return m, tea.Batch(cmds...) -} - -func (m interactiveTeaModel) execDasel(selector string, root bool) (res string, err error) { - defer func() { - if r := recover(); r != nil { - err = fmt.Errorf("panic: %v", r) - } - }() - var stdIn *bytes.Reader = nil - if m.stdInBytes != nil { - stdIn = bytes.NewReader(m.stdInBytes) - } else { - stdIn = bytes.NewReader([]byte{}) + outBytes, err := run(o) + return string(outBytes), err } - o := runOpts{ - Vars: m.it.Vars, - ExtReadFlags: m.it.ExtReadFlags, - ExtWriteFlags: m.it.ExtWriteFlags, - InFormat: m.it.InFormat, - OutFormat: m.it.OutFormat, - ReturnRoot: root, - Unstable: true, - Query: selector, + p, selectorFn := newInteractiveTeaProgram(string(stdInBytes), c.Query, parsing.Format(c.InFormat), parsing.Format(c.OutFormat), runDasel) - Stdin: stdIn, + _, err := p.Run() + if err != nil { + return err } - outBytes, err := run(o) - return string(outBytes), err -} - -func (m interactiveTeaModel) headerView() string { - return titleStyle.Render("Dasel Interactive Mode - " + internal.Version + " - ctrl+c or esc to exit") -} - -func (m interactiveTeaModel) commandInputView() string { - return commandInputStyle.Render(m.commandInput.View()) -} - -func (m interactiveTeaModel) originalHeaderView() string { - return resultTitleStyle.Render("Root") -} - -func (m interactiveTeaModel) resultHeaderView() string { - return resultTitleStyle.Render("Result") -} - -func (m interactiveTeaModel) viewportView() string { - return viewportStyle.Render(m.outputViewport.View()) -} - -func (m interactiveTeaModel) originalViewportView() string { - return viewportStyle.Render(m.originalOutputViewport.View()) -} - -func (m interactiveTeaModel) View() string { - res := []string{ - m.headerView(), - m.commandInputView(), + if selectorFn != nil { + s := selectorFn() + if s != "" { + if _, err := fmt.Fprintf(ctx.Stdout, "%s\n", s); err != nil { + return fmt.Errorf("error writing output: %w", err) + } + } } - var left, right []string - - left = append(left, m.originalHeaderView(), m.originalViewportView()) - right = append(right, m.resultHeaderView(), m.viewportView()) - - viewports := lipgloss.JoinHorizontal(lipgloss.Bottom, strings.Join(left, "\n"), strings.Join(right, "\n")) - res = append(res, viewports) - - return strings.Join(res, "\n") + return nil } diff --git a/internal/cli/interactive_tea.go b/internal/cli/interactive_tea.go new file mode 100644 index 00000000..95154f7f --- /dev/null +++ b/internal/cli/interactive_tea.go @@ -0,0 +1,209 @@ +package cli + +import ( + "fmt" + "slices" + "strings" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/tomwright/dasel/v3/internal" + "github.com/tomwright/dasel/v3/parsing" +) + +var ( + interactiveKeyQuit = tea.KeyCtrlC + interactiveKeyCycleRead = tea.KeyCtrlE + interactiveKeyCycleWrite = tea.KeyCtrlD + + headingStyle = func() lipgloss.Style { + return lipgloss.NewStyle().Padding(0, 1, 1, 1) + }() + shortcutStyle = func() lipgloss.Style { + return lipgloss.NewStyle().Padding(0, 1).Align(lipgloss.Left) + }() + headerStyle = func() lipgloss.Style { + return lipgloss.NewStyle().Padding(1).Align(lipgloss.Center) + }() + inputStyle = func() lipgloss.Style { + return lipgloss.NewStyle().Margin(0, 0, 1, 0) + }() + inputContentStyle = func() lipgloss.Style { + return lipgloss.NewStyle().Padding(0, 1).Border(lipgloss.RoundedBorder()) + }() + inputHeaderStyle = func() lipgloss.Style { + return lipgloss.NewStyle().Padding(0, 2).Margin(0, 0, 1, 0).Underline(true) + }() + outputContentStyle = func() lipgloss.Style { + return lipgloss.NewStyle().Padding(0, 1).Border(lipgloss.RoundedBorder()) + }() + outputHeaderStyle = func() lipgloss.Style { + return lipgloss.NewStyle().Padding(0, 2).Margin(0, 0, 1, 0).Underline(true) + }() +) + +type interactiveDaselExecutor func(selector string, root bool, formatIn parsing.Format, formatOut parsing.Format, in string) (res string, err error) + +func newInteractiveTeaProgram(initialInput string, initialSelector string, formatIn parsing.Format, formatOut parsing.Format, run interactiveDaselExecutor) (*tea.Program, func() string) { + m := newInteractiveRootModel(initialInput, initialSelector, formatIn, formatOut, run) + return tea.NewProgram(m, tea.WithAltScreen()), func() string { + return m.sharedData.selector + } +} + +type interactiveSharedData struct { + formatIn parsing.Format + formatOut parsing.Format + selector string + input string +} + +type interactiveRootModel struct { + sharedData *interactiveSharedData + inputModel *interactiveInputModel + outputModels []*interactiveOutputModel +} + +func newInteractiveRootModel(initialInput string, initialSelector string, formatIn parsing.Format, formatOut parsing.Format, run interactiveDaselExecutor) *interactiveRootModel { + res := &interactiveRootModel{ + sharedData: &interactiveSharedData{ + formatIn: formatIn, + formatOut: formatOut, + selector: initialSelector, + input: initialInput, + }, + outputModels: make([]*interactiveOutputModel, 0), + } + + res.inputModel = newInteractiveInputModel(res.sharedData) + + outputRootModel := newInteractiveOutputModel(res.sharedData, true, run) + outputResultModel := newInteractiveOutputModel(res.sharedData, false, run) + + res.outputModels = append(res.outputModels, outputRootModel, outputResultModel) + + return res +} + +func (m *interactiveRootModel) Init() tea.Cmd { + return nil +} + +func cycleFormats(all []parsing.Format, current parsing.Format) parsing.Format { + slices.SortFunc(all, func(i, j parsing.Format) int { + return strings.Compare(string(i), string(j)) + }) + cur := -1 + for i, format := range all { + if format == current { + cur = i + break + } + } + next := cur + 1 + if next > len(all)-1 { + next = 0 + } + return all[next] +} + +func (m *interactiveRootModel) cycleReader() { + m.sharedData.formatIn = cycleFormats(parsing.RegisteredReaders(), m.sharedData.formatIn) +} + +func (m *interactiveRootModel) cycleWriter() { + m.sharedData.formatOut = cycleFormats(parsing.RegisteredWriters(), m.sharedData.formatOut) +} + +func (m *interactiveRootModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + var cmd tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.Type { + case interactiveKeyQuit: + return m, tea.Quit + case interactiveKeyCycleRead: + m.cycleReader() + case interactiveKeyCycleWrite: + m.cycleWriter() + default: + } + + case tea.WindowSizeMsg: + headerStyle = headerStyle.Width(msg.Width).MaxWidth(msg.Width) + + var headerHeight int + { + headerHeight += lipgloss.Height(m.headerView()) + headerHeight += lipgloss.Height(m.inputView()) + } + verticalMarginHeight := headerHeight + + numCols := len(m.outputModels) + + viewportHeight := msg.Height - verticalMarginHeight - (2 * numCols) + viewportWidth := (msg.Width / numCols) - (2 * numCols) + + for _, outputModel := range m.outputModels { + outputModel.setSize(viewportWidth, viewportHeight) + outputModel.setVerticalPosition(verticalMarginHeight) + } + } + + { + var model tea.Model + model, cmd = m.inputModel.Update(msg) + m.inputModel = model.(*interactiveInputModel) + cmds = append(cmds, cmd) + } + + for i, outputModel := range m.outputModels { + var model tea.Model + model, cmd = outputModel.Update(msg) + m.outputModels[i] = model.(*interactiveOutputModel) + cmds = append(cmds, cmd) + } + + return m, tea.Batch(cmds...) +} + +func (m *interactiveRootModel) headerView() string { + header := headingStyle.Render("Dasel interactive mode - " + internal.Version) + + shortcuts := "\n" + shortcuts += fmt.Sprintf("%s: %s\n", interactiveKeyQuit, "Quit") + shortcuts += fmt.Sprintf("%s: %s\n", interactiveKeyCycleRead, "Cycle reader") + shortcuts += fmt.Sprintf("%s: %s\n", interactiveKeyCycleWrite, "Cycle writer") + + out := append([]string{header}, shortcutStyle.Render(shortcuts)) + + out = append(out, fmt.Sprintf("\nReader: %s | Writer: %s", m.sharedData.formatIn, m.sharedData.formatOut)) + + return headerStyle.Render(out...) +} + +func (m *interactiveRootModel) inputView() string { + return inputStyle.Render(m.inputModel.View()) +} + +func (m *interactiveRootModel) View() string { + rows := make([]string, 0) + + rows = append(rows, m.headerView()) + + rows = append(rows, m.inputView()) + + { + cols := make([]string, 0) + for _, outputModel := range m.outputModels { + cols = append(cols, outputModel.View()) + } + if len(cols) > 0 { + rows = append(rows, lipgloss.JoinHorizontal(lipgloss.Top, cols...)) + } + } + + return lipgloss.JoinVertical(lipgloss.Left, rows...) +} diff --git a/internal/cli/interactive_tea_input.go b/internal/cli/interactive_tea_input.go new file mode 100644 index 00000000..10865332 --- /dev/null +++ b/internal/cli/interactive_tea_input.go @@ -0,0 +1,50 @@ +package cli + +import ( + "github.com/charmbracelet/bubbles/textarea" + tea "github.com/charmbracelet/bubbletea" +) + +type interactiveInputModel struct { + sharedData *interactiveSharedData + inputModel textarea.Model +} + +func newInteractiveInputModel(sharedData *interactiveSharedData) *interactiveInputModel { + ti := textarea.New() + ti.Placeholder = "Enter a query..." + ti.SetValue(sharedData.selector) + ti.Focus() + ti.SetHeight(5) + ti.ShowLineNumbers = false + + return &interactiveInputModel{ + sharedData: sharedData, + inputModel: ti, + } +} + +func (m *interactiveInputModel) Init() tea.Cmd { + return textarea.Blink +} + +func (m *interactiveInputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + var cmd tea.Cmd + + m.sharedData.selector = m.inputModel.Value() + + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.inputModel.SetWidth(msg.Width) + } + + m.inputModel, cmd = m.inputModel.Update(msg) + cmds = append(cmds, cmd) + + return m, tea.Batch(cmds...) +} + +func (m *interactiveInputModel) View() string { + return m.inputModel.View() +} diff --git a/internal/cli/interactive_tea_output.go b/internal/cli/interactive_tea_output.go new file mode 100644 index 00000000..1ab0ddda --- /dev/null +++ b/internal/cli/interactive_tea_output.go @@ -0,0 +1,106 @@ +package cli + +import ( + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/tomwright/dasel/v3/parsing" +) + +type interactiveOutputModel struct { + sharedData *interactiveSharedData + hasUpdatedBefore bool + lastSeenSelector string + lastSeenFormatIn parsing.Format + lastSeenFormatOut parsing.Format + lastSeenInput string + root bool + run interactiveDaselExecutor + output string + outputViewport viewport.Model + outputViewportReady bool +} + +func newInteractiveOutputModel(sharedData *interactiveSharedData, root bool, run interactiveDaselExecutor) *interactiveOutputModel { + m := &interactiveOutputModel{ + sharedData: sharedData, + root: root, + run: run, + } + m.outputViewport = viewport.New(10, 10) + return m +} + +func (m *interactiveOutputModel) Init() tea.Cmd { + return nil +} + +func (m *interactiveOutputModel) setOutput(output string) { + m.output = output + if m.outputViewportReady { + m.outputViewport.SetContent(m.output) + } +} + +func (m *interactiveOutputModel) setSize(width int, height int) { + if !m.outputViewportReady { + m.outputViewportReady = true + } + + m.outputViewport.Width = width + m.outputViewport.Height = height + m.outputViewport.SetContent(m.output) +} + +func (m *interactiveOutputModel) setVerticalPosition(pos int) { + m.outputViewport.YPosition = pos +} + +func (m *interactiveOutputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + var cmd tea.Cmd + + defer func() { + m.lastSeenSelector = m.sharedData.selector + m.lastSeenFormatIn = m.sharedData.formatIn + m.lastSeenFormatOut = m.sharedData.formatOut + m.lastSeenInput = m.sharedData.input + }() + firstUpdate := !m.hasUpdatedBefore + m.hasUpdatedBefore = true + + queryChanged := m.lastSeenSelector != m.sharedData.selector || + m.lastSeenFormatIn != m.sharedData.formatIn || + m.lastSeenFormatOut != m.sharedData.formatOut || + m.lastSeenInput != m.sharedData.input + + // Take care of dasel execution + output. + if firstUpdate || queryChanged { + m.setOutput("Executing...") + out, err := m.run(m.sharedData.selector, m.root, m.sharedData.formatIn, m.sharedData.formatOut, m.sharedData.input) + if err != nil { + m.setOutput(err.Error()) + } else { + m.setOutput(out) + } + } + + m.outputViewport, cmd = m.outputViewport.Update(msg) + cmds = append(cmds, cmd) + + return m, tea.Batch(cmds...) +} + +func (m *interactiveOutputModel) View() string { + title := "Result" + if m.root { + title = "Root" + } + + content := "Initializing..." + if m.outputViewportReady { + content = m.outputViewport.View() + } + + return lipgloss.JoinVertical(lipgloss.Left, outputHeaderStyle.Render(title), outputContentStyle.Render(content)) +} diff --git a/model/value_metadata.go b/model/value_metadata.go index 1347bd0a..76f3c3e0 100644 --- a/model/value_metadata.go +++ b/model/value_metadata.go @@ -54,3 +54,21 @@ func (v *Value) IsBranch() bool { func (v *Value) MarkAsBranch() { v.SetMetadataValue("branch", true) } + +// IsIgnore returns true if value should be ignored. +func (v *Value) IsIgnore() bool { + if v == nil { + return false + } + val, ok := v.MetadataValue("ignore") + if !ok { + return false + } + ignore, ok := val.(bool) + return ok && ignore +} + +// MarkAsIgnore marks the value to be ignored. +func (v *Value) MarkAsIgnore() { + v.SetMetadataValue("ignore", true) +} From a6074e85df4f23239d57cd6f9bfe72c7f4dd2b18 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 31 Oct 2024 01:24:39 +0000 Subject: [PATCH 53/56] Start implementing HCL parser --- cmd/dasel/main.go | 1 + go.mod | 12 ++- go.sum | 20 ++++ model/value.go | 24 +++-- parsing/hcl/hcl.go | 19 ++++ parsing/hcl/reader.go | 181 +++++++++++++++++++++++++++++++++++++ parsing/hcl/reader_test.go | 65 +++++++++++++ parsing/hcl/writer.go | 19 ++++ 8 files changed, 330 insertions(+), 11 deletions(-) create mode 100644 parsing/hcl/hcl.go create mode 100644 parsing/hcl/reader.go create mode 100644 parsing/hcl/reader_test.go create mode 100644 parsing/hcl/writer.go diff --git a/cmd/dasel/main.go b/cmd/dasel/main.go index 88fd0478..b6151f41 100644 --- a/cmd/dasel/main.go +++ b/cmd/dasel/main.go @@ -6,6 +6,7 @@ import ( "github.com/tomwright/dasel/v3/internal/cli" _ "github.com/tomwright/dasel/v3/parsing/d" + _ "github.com/tomwright/dasel/v3/parsing/hcl" _ "github.com/tomwright/dasel/v3/parsing/json" _ "github.com/tomwright/dasel/v3/parsing/toml" _ "github.com/tomwright/dasel/v3/parsing/xml" diff --git a/go.mod b/go.mod index 437f883d..4cd760d2 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,15 @@ go 1.23 require ( github.com/alecthomas/kong v1.2.1 - github.com/google/go-cmp v0.5.9 + github.com/google/go-cmp v0.6.0 github.com/pelletier/go-toml/v2 v2.2.2 gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/agext/levenshtein v1.2.1 // indirect + github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/bubbles v0.20.0 // indirect @@ -19,15 +22,20 @@ require ( github.com/charmbracelet/x/term v0.2.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/hashicorp/hcl/v2 v2.22.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.15.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/zclconf/go-cty v1.13.0 // indirect + golang.org/x/mod v0.8.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.26.0 // indirect - golang.org/x/text v0.3.8 // indirect + golang.org/x/text v0.11.0 // indirect + golang.org/x/tools v0.6.0 // indirect ) diff --git a/go.sum b/go.sum index b4d49752..7c5fbb09 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,15 @@ +github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= +github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY= github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/kong v1.2.1 h1:E8jH4Tsgv6wCRX2nGrdPyHDUCSG83WH2qE4XLACD33Q= github.com/alecthomas/kong v1.2.1/go.mod h1:rKTSFhbdp3Ryefn8x5MOEprnRFQ7nlmMC01GKhehhBM= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= @@ -27,8 +33,12 @@ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M= +github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -41,6 +51,8 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -63,6 +75,10 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/zclconf/go-cty v1.13.0 h1:It5dfKTTZHe9aeppbNOda3mN7Ag7sg6QkBNm6TkyFa0= +github.com/zclconf/go-cty v1.13.0/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -71,6 +87,10 @@ golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/model/value.go b/model/value.go index 1a1ec255..19d0f8a7 100644 --- a/model/value.go +++ b/model/value.go @@ -52,6 +52,14 @@ type Value struct { } func (v *Value) String() string { + return v.string(0) +} + +func indentStr(indent int) string { + return strings.Repeat(" ", indent) +} + +func (v *Value) string(indent int) string { switch v.Type() { case TypeString: val, err := v.StringValue() @@ -78,15 +86,14 @@ func (v *Value) String() string { } return fmt.Sprintf("bool{%t}", val) case TypeMap: - res := fmt.Sprintf("map[%d]{", len(v.Metadata)) + res := fmt.Sprintf("{\n") if err := v.RangeMap(func(k string, v *Value) error { - res += fmt.Sprintf("%s: %s, ", k, v.String()) + res += fmt.Sprintf("%s%s: %s,\n", indentStr(indent+1), k, v.string(indent+1)) return nil }); err != nil { panic(err) } - res = strings.TrimSuffix(res, ", ") - return res + "}" + return res + indentStr(indent) + "}" case TypeSlice: md := "" if v.IsSpread() { @@ -95,17 +102,16 @@ func (v *Value) String() string { if v.IsBranch() { md += "branch, " } - res := fmt.Sprintf("array[%s]{", strings.TrimSuffix(md, ", ")) + res := fmt.Sprintf("array[%s]{\n", strings.TrimSuffix(md, ", ")) if err := v.RangeSlice(func(k int, v *Value) error { - res += fmt.Sprintf("%d: %s, ", k, v.String()) + res += fmt.Sprintf("%s%d: %s,\n", indentStr(indent+1), k, v.string(indent+1)) return nil }); err != nil { panic(err) } - res = strings.TrimSuffix(res, ", ") - return res + "}" + return res + indentStr(indent) + "}" case TypeNull: - return "null" + return indentStr(indent) + "null" default: return fmt.Sprintf("unknown[%s]", v.Interface()) } diff --git a/parsing/hcl/hcl.go b/parsing/hcl/hcl.go new file mode 100644 index 00000000..a1fddf37 --- /dev/null +++ b/parsing/hcl/hcl.go @@ -0,0 +1,19 @@ +package hcl + +import ( + "github.com/tomwright/dasel/v3/parsing" +) + +const ( + // HCL represents the hcl2 file format. + HCL parsing.Format = "hcl" +) + +var _ parsing.Reader = (*hclReader)(nil) + +//var _ parsing.Writer = (*hclWriter)(nil) + +func init() { + parsing.RegisterReader(HCL, newHCLReader) + parsing.RegisterWriter(HCL, newHCLWriter) +} diff --git a/parsing/hcl/reader.go b/parsing/hcl/reader.go new file mode 100644 index 00000000..fc72917b --- /dev/null +++ b/parsing/hcl/reader.go @@ -0,0 +1,181 @@ +package hcl + +import ( + "fmt" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/gohcl" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/parsing" + "github.com/zclconf/go-cty/cty" +) + +func newHCLReader(options parsing.ReaderOptions) (parsing.Reader, error) { + return &hclReader{}, nil +} + +type hclReader struct{} + +// Read reads a value from a byte slice. +func (j *hclReader) Read(data []byte) (*model.Value, error) { + f, _ := hclsyntax.ParseConfig(data, "input", hcl.InitialPos) + + body, ok := f.Body.(*hclsyntax.Body) + if !ok { + return nil, fmt.Errorf("failed to assert file body type") + } + + return decodeHCLBody(body) +} + +func decodeHCLBody(body *hclsyntax.Body) (*model.Value, error) { + res := model.NewMapValue() + + for _, attr := range body.Attributes { + val, err := decodeHCLExpr(attr.Expr) + if err != nil { + return nil, fmt.Errorf("failed to decode attr %q: %w", attr.Name, err) + } + + if err := res.SetMapKey(attr.Name, val); err != nil { + return nil, err + } + } + + blockTypeIndexes := make(map[string]int) + blockValues := make([][]*model.Value, 0) + for _, block := range body.Blocks { + if _, ok := blockTypeIndexes[block.Type]; !ok { + blockValues = append(blockValues, make([]*model.Value, 0)) + blockTypeIndexes[block.Type] = len(blockValues) - 1 + } + res, err := decodeHCLBlock(block) + if err != nil { + return nil, fmt.Errorf("failed to decode block %q: %w", block.Type, err) + } + blockValues[blockTypeIndexes[block.Type]] = append(blockValues[blockTypeIndexes[block.Type]], res) + } + + for t, index := range blockTypeIndexes { + blocks := blockValues[index] + switch len(blocks) { + case 0: + continue + case 1: + if err := res.SetMapKey(t, blocks[0]); err != nil { + return nil, err + } + default: + val := model.NewSliceValue() + for _, b := range blocks { + if err := val.Append(b); err != nil { + return nil, err + } + } + if err := res.SetMapKey(t, val); err != nil { + return nil, err + } + } + } + + return res, nil +} + +func decodeHCLBlock(block *hclsyntax.Block) (*model.Value, error) { + res, err := decodeHCLBody(block.Body) + if err != nil { + return nil, err + } + + labels := model.NewSliceValue() + for _, l := range block.Labels { + if err := labels.Append(model.NewStringValue(l)); err != nil { + return nil, err + } + } + + if err := res.SetMapKey("labels", labels); err != nil { + return nil, err + } + + if err := res.SetMapKey("type", model.NewStringValue(block.Type)); err != nil { + return nil, err + } + + return res, nil +} + +func decodeHCLExpr(expr hcl.Expression) (*model.Value, error) { + source := cty.Value{} + _ = gohcl.DecodeExpression(expr, nil, &source) + + return decodeCtyValue(source) +} + +func decodeCtyValue(source cty.Value) (res *model.Value, err error) { + defer func() { + r := recover() + if r != nil { + err = fmt.Errorf("failed to decode: %v", r) + return + } + }() + if source.IsNull() { + return model.NewNullValue(), nil + } + + sourceT := source.Type() + switch { + case sourceT.IsMapType(): + return nil, fmt.Errorf("map type not implemented") + case sourceT.IsListType(): + return nil, fmt.Errorf("list type not implemented") + case sourceT.IsCollectionType(): + return nil, fmt.Errorf("collection type not implemented") + case sourceT.IsCapsuleType(): + return nil, fmt.Errorf("capsule type not implemented") + case sourceT.IsTupleType(): + res = model.NewSliceValue() + it := source.ElementIterator() + for it.Next() { + k, v := it.Element() + // We don't need the index as they should be in order. + // Just validates the key is correct. + _, _ = k.AsBigFloat().Float64() + + val, err := decodeCtyValue(v) + if err != nil { + return nil, fmt.Errorf("failed to decode tuple value: %w", err) + } + + if err := res.Append(val); err != nil { + return nil, err + } + } + return res, nil + case sourceT.IsObjectType(): + return nil, fmt.Errorf("object type not implemented") + case sourceT.IsPrimitiveType(): + switch sourceT { + case cty.String: + v := source.AsString() + return model.NewStringValue(v), nil + case cty.Bool: + v := source.True() + return model.NewBoolValue(v), nil + case cty.Number: + v := source.AsBigFloat() + f64, _ := v.Float64() + if v.IsInt() { + return model.NewIntValue(int64(f64)), nil + } + return model.NewFloatValue(f64), nil + default: + return nil, fmt.Errorf("unhandled primitive type %q", source.Type()) + } + case sourceT.IsSetType(): + return nil, fmt.Errorf("set type not implemented") + default: + return nil, fmt.Errorf("unhandled type: %s", sourceT.FriendlyName()) + } +} diff --git a/parsing/hcl/reader_test.go b/parsing/hcl/reader_test.go new file mode 100644 index 00000000..e97e71fc --- /dev/null +++ b/parsing/hcl/reader_test.go @@ -0,0 +1,65 @@ +package hcl_test + +import ( + "fmt" + "github.com/tomwright/dasel/v3/parsing" + "github.com/tomwright/dasel/v3/parsing/hcl" + "testing" +) + +type readTestCase struct { + in string +} + +func (tc readTestCase) run(t *testing.T) { + r, err := hcl.HCL.NewReader(parsing.DefaultReaderOptions()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + in := []byte(tc.in) + + got, err := r.Read(in) + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + fmt.Println(got) +} + +func TestHclReader_Read(t *testing.T) { + t.Run("document a", readTestCase{ + in: `io_mode = "async" + +service "http" "web_proxy" { + listen_addr = "127.0.0.1:8080" + + process "main" { + command = ["/usr/local/bin/awesome-app", "server"] + } + + process "mgmt" { + command = ["/usr/local/bin/awesome-app", "mgmt"] + } +}`, + }.run) + t.Run("document b", readTestCase{ + in: `resource "aws_instance" "example" { + # (resource configuration omitted for brevity) + + provisioner "local-exec" { + command = "echo 'Hello World' >example.txt" + } + provisioner "file" { + source = "example.txt" + destination = "/tmp/example.txt" + } + provisioner "remote-exec" { + inline = [ + "sudo install-something -f /tmp/example.txt", + ] + } +}`, + }.run) +} diff --git a/parsing/hcl/writer.go b/parsing/hcl/writer.go new file mode 100644 index 00000000..307b4788 --- /dev/null +++ b/parsing/hcl/writer.go @@ -0,0 +1,19 @@ +package hcl + +import ( + "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/parsing" +) + +func newHCLWriter(options parsing.WriterOptions) (parsing.Writer, error) { + return &hclWriter{}, nil +} + +type hclWriter struct { + options parsing.WriterOptions +} + +// Write writes a value to a byte slice. +func (j *hclWriter) Write(value *model.Value) ([]byte, error) { + return nil, nil +} From b5aa01d76cfc5d12555113d932a7543900dd9524 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Thu, 31 Oct 2024 16:10:42 +0000 Subject: [PATCH 54/56] HCL improvements --- internal/cli/command.go | 4 +- internal/cli/interactive.go | 10 +- model/value.go | 9 ++ model/value_map.go | 23 +++++ parsing/hcl/hcl.go | 6 +- parsing/hcl/reader.go | 191 +++++++++++++++++++++++------------- parsing/hcl/reader_test.go | 24 ++++- 7 files changed, 188 insertions(+), 79 deletions(-) diff --git a/internal/cli/command.go b/internal/cli/command.go index 2b74f12a..b7389ebf 100644 --- a/internal/cli/command.go +++ b/internal/cli/command.go @@ -46,8 +46,8 @@ func Run(stdin io.Reader, stdout, stderr io.Writer) (*kong.Context, error) { "version": internal.Version, }, kong.Bind(&cli.Globals), - kong.TypeMapper(reflect.TypeFor[*[]variable](), &variableMapper{}), - kong.TypeMapper(reflect.TypeFor[*[]extReadWriteFlag](), &extReadWriteFlagMapper{}), + kong.TypeMapper(reflect.TypeFor[variables](), &variableMapper{}), + kong.TypeMapper(reflect.TypeFor[extReadWriteFlags](), &extReadWriteFlagMapper{}), kong.OptionFunc(func(k *kong.Kong) error { k.Stdout = cli.Stdout k.Stderr = cli.Stderr diff --git a/internal/cli/interactive.go b/internal/cli/interactive.go index 6eee0ec1..5543e449 100644 --- a/internal/cli/interactive.go +++ b/internal/cli/interactive.go @@ -21,11 +21,11 @@ func NewInteractiveCmd(queryCmd *QueryCmd) *InteractiveCmd { } type InteractiveCmd struct { - Vars *[]variable `flag:"" name:"var" help:"Variables to pass to the query. E.g. --var foo=\"bar\" --var baz=json:file:./some/file.json"` - ExtReadFlags *[]extReadWriteFlag `flag:"" name:"read-flag" help:"Reader flag to customise parsing. E.g. --read-flag xml-mode=structured"` - ExtWriteFlags *[]extReadWriteFlag `flag:"" name:"write-flag" help:"Writer flag to customise output"` - InFormat string `flag:"" name:"in" short:"i" help:"The format of the input data."` - OutFormat string `flag:"" name:"out" short:"o" help:"The format of the output data."` + Vars variables `flag:"" name:"var" help:"Variables to pass to the query. E.g. --var foo=\"bar\" --var baz=json:file:./some/file.json"` + ExtReadFlags extReadWriteFlags `flag:"" name:"read-flag" help:"Reader flag to customise parsing. E.g. --read-flag xml-mode=structured"` + ExtWriteFlags extReadWriteFlags `flag:"" name:"write-flag" help:"Writer flag to customise output"` + InFormat string `flag:"" name:"in" short:"i" help:"The format of the input data."` + OutFormat string `flag:"" name:"out" short:"o" help:"The format of the output data."` Query string `arg:"" help:"The query to execute." optional:"" default:""` } diff --git a/model/value.go b/model/value.go index 19d0f8a7..79402bc2 100644 --- a/model/value.go +++ b/model/value.go @@ -272,3 +272,12 @@ func (v *Value) Len() (int, error) { return l, nil } + +func (v *Value) Copy() (*Value, error) { + switch v.Type() { + case TypeMap: + return v.MapCopy() + default: + return nil, fmt.Errorf("copy not supported for type: %s", v.Type()) + } +} diff --git a/model/value_map.go b/model/value_map.go index 527df885..77b92ade 100644 --- a/model/value_map.go +++ b/model/value_map.go @@ -1,6 +1,7 @@ package model import ( + "errors" "fmt" "reflect" @@ -58,6 +59,28 @@ func (v *Value) SetMapKey(key string, value *Value) error { } } +func (v *Value) MapCopy() (*Value, error) { + res := NewMapValue() + kvs, err := v.MapKeyValues() + if err != nil { + return nil, fmt.Errorf("error getting map key values: %w", err) + } + for _, kv := range kvs { + if err := res.SetMapKey(kv.Key, kv.Value); err != nil { + return nil, fmt.Errorf("error setting map key: %w", err) + } + } + return res, nil +} + +func (v *Value) MapKeyExists(key string) (bool, error) { + _, err := v.GetMapKey(key) + if err != nil && !errors.As(err, &MapKeyNotFound{}) { + return false, err + } + return err == nil, nil +} + // GetMapKey returns the value at the specified key in the map. func (v *Value) GetMapKey(key string) (*Value, error) { switch { diff --git a/parsing/hcl/hcl.go b/parsing/hcl/hcl.go index a1fddf37..9b14104f 100644 --- a/parsing/hcl/hcl.go +++ b/parsing/hcl/hcl.go @@ -10,10 +10,10 @@ const ( ) var _ parsing.Reader = (*hclReader)(nil) - -//var _ parsing.Writer = (*hclWriter)(nil) +var _ parsing.Writer = (*hclWriter)(nil) func init() { parsing.RegisterReader(HCL, newHCLReader) - parsing.RegisterWriter(HCL, newHCLWriter) + // HCL writer is not implemented yet + //parsing.RegisterWriter(HCL, newHCLWriter) } diff --git a/parsing/hcl/reader.go b/parsing/hcl/reader.go index fc72917b..b6fa445a 100644 --- a/parsing/hcl/reader.go +++ b/parsing/hcl/reader.go @@ -2,6 +2,7 @@ package hcl import ( "fmt" + "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/hcl/v2/hclsyntax" @@ -11,13 +12,19 @@ import ( ) func newHCLReader(options parsing.ReaderOptions) (parsing.Reader, error) { - return &hclReader{}, nil + return &hclReader{ + alwaysReadLabelsToSlice: options.Ext["hcl-block-format"] == "array", + }, nil } -type hclReader struct{} +type hclReader struct { + alwaysReadLabelsToSlice bool +} // Read reads a value from a byte slice. -func (j *hclReader) Read(data []byte) (*model.Value, error) { +// Reads the HCL data into a model that follows the HCL JSON spec. +// See https://github.com/hashicorp/hcl/blob/main/json%2Fspec.md +func (r *hclReader) Read(data []byte) (*model.Value, error) { f, _ := hclsyntax.ParseConfig(data, "input", hcl.InitialPos) body, ok := f.Body.(*hclsyntax.Body) @@ -25,14 +32,15 @@ func (j *hclReader) Read(data []byte) (*model.Value, error) { return nil, fmt.Errorf("failed to assert file body type") } - return decodeHCLBody(body) + return r.decodeHCLBody(body) } -func decodeHCLBody(body *hclsyntax.Body) (*model.Value, error) { +func (r *hclReader) decodeHCLBody(body *hclsyntax.Body) (*model.Value, error) { res := model.NewMapValue() + var err error for _, attr := range body.Attributes { - val, err := decodeHCLExpr(attr.Expr) + val, err := r.decodeHCLExpr(attr.Expr) if err != nil { return nil, fmt.Errorf("failed to decode attr %q: %w", attr.Name, err) } @@ -42,77 +50,116 @@ func decodeHCLBody(body *hclsyntax.Body) (*model.Value, error) { } } - blockTypeIndexes := make(map[string]int) - blockValues := make([][]*model.Value, 0) + res, err = r.decodeHCLBodyBlocks(body, res) + if err != nil { + return nil, err + } + + return res, nil +} + +func (r *hclReader) decodeHCLBodyBlocks(body *hclsyntax.Body, res *model.Value) (*model.Value, error) { for _, block := range body.Blocks { - if _, ok := blockTypeIndexes[block.Type]; !ok { - blockValues = append(blockValues, make([]*model.Value, 0)) - blockTypeIndexes[block.Type] = len(blockValues) - 1 + if err := r.decodeHCLBlock(block, res); err != nil { + return nil, err } - res, err := decodeHCLBlock(block) + } + return res, nil +} + +func (r *hclReader) decodeHCLBlock(block *hclsyntax.Block, res *model.Value) error { + key := block.Type + v := res + for _, label := range block.Labels { + exists, err := v.MapKeyExists(key) if err != nil { - return nil, fmt.Errorf("failed to decode block %q: %w", block.Type, err) + return err } - blockValues[blockTypeIndexes[block.Type]] = append(blockValues[blockTypeIndexes[block.Type]], res) - } - for t, index := range blockTypeIndexes { - blocks := blockValues[index] - switch len(blocks) { - case 0: - continue - case 1: - if err := res.SetMapKey(t, blocks[0]); err != nil { - return nil, err + if exists { + keyV, err := v.GetMapKey(key) + if err != nil { + return err } - default: - val := model.NewSliceValue() - for _, b := range blocks { - if err := val.Append(b); err != nil { - return nil, err - } - } - if err := res.SetMapKey(t, val); err != nil { - return nil, err + v = keyV + } else { + keyV := model.NewMapValue() + if err := v.SetMapKey(key, keyV); err != nil { + return err } + v = keyV } - } - - return res, nil -} -func decodeHCLBlock(block *hclsyntax.Block) (*model.Value, error) { - res, err := decodeHCLBody(block.Body) - if err != nil { - return nil, err + key = label } - labels := model.NewSliceValue() - for _, l := range block.Labels { - if err := labels.Append(model.NewStringValue(l)); err != nil { - return nil, err - } + body, err := r.decodeHCLBody(block.Body) + if err != nil { + return err } - if err := res.SetMapKey("labels", labels); err != nil { - return nil, err + exists, err := v.MapKeyExists(key) + if err != nil { + return err } + if exists { + keyV, err := v.GetMapKey(key) + if err != nil { + return err + } - if err := res.SetMapKey("type", model.NewStringValue(block.Type)); err != nil { - return nil, err + switch keyV.Type() { + case model.TypeSlice: + if err := keyV.Append(body); err != nil { + return err + } + case model.TypeMap: + // Previous value was a map. + // Create a new slice containing the previous map and the new map. + newKeyV := model.NewSliceValue() + previousKeyV, err := keyV.Copy() + if err != nil { + return err + } + if err := newKeyV.Append(previousKeyV); err != nil { + return err + } + if err := newKeyV.Append(body); err != nil { + return err + } + if err := keyV.Set(newKeyV); err != nil { + return err + } + default: + return fmt.Errorf("unexpected type: %s", keyV.Type()) + } + } else { + if r.alwaysReadLabelsToSlice { + slice := model.NewSliceValue() + if err := slice.Append(body); err != nil { + return err + } + if err := v.SetMapKey(key, slice); err != nil { + return err + } + } else { + if err := v.SetMapKey(key, body); err != nil { + return err + } + } } - return res, nil + return nil } -func decodeHCLExpr(expr hcl.Expression) (*model.Value, error) { +func (r *hclReader) decodeHCLExpr(expr hcl.Expression) (*model.Value, error) { source := cty.Value{} _ = gohcl.DecodeExpression(expr, nil, &source) - return decodeCtyValue(source) + return r.decodeCtyValue(source) } -func decodeCtyValue(source cty.Value) (res *model.Value, err error) { +func (r *hclReader) decodeCtyValue(source cty.Value) (res *model.Value, err error) { defer func() { r := recover() if r != nil { @@ -126,15 +173,7 @@ func decodeCtyValue(source cty.Value) (res *model.Value, err error) { sourceT := source.Type() switch { - case sourceT.IsMapType(): - return nil, fmt.Errorf("map type not implemented") - case sourceT.IsListType(): - return nil, fmt.Errorf("list type not implemented") - case sourceT.IsCollectionType(): - return nil, fmt.Errorf("collection type not implemented") - case sourceT.IsCapsuleType(): - return nil, fmt.Errorf("capsule type not implemented") - case sourceT.IsTupleType(): + case sourceT.IsListType(), sourceT.IsTupleType(): res = model.NewSliceValue() it := source.ElementIterator() for it.Next() { @@ -143,7 +182,7 @@ func decodeCtyValue(source cty.Value) (res *model.Value, err error) { // Just validates the key is correct. _, _ = k.AsBigFloat().Float64() - val, err := decodeCtyValue(v) + val, err := r.decodeCtyValue(v) if err != nil { return nil, fmt.Errorf("failed to decode tuple value: %w", err) } @@ -153,8 +192,26 @@ func decodeCtyValue(source cty.Value) (res *model.Value, err error) { } } return res, nil - case sourceT.IsObjectType(): - return nil, fmt.Errorf("object type not implemented") + case sourceT.IsMapType(), sourceT.IsObjectType(), sourceT.IsSetType(): + v := model.NewMapValue() + it := source.ElementIterator() + for it.Next() { + k, el := it.Element() + if k.Type() != cty.String { + return nil, fmt.Errorf("object key must be a string") + } + kStr := k.AsString() + + elVal, err := r.decodeCtyValue(el) + if err != nil { + return nil, fmt.Errorf("failed to decode object value: %w", err) + } + + if err := v.SetMapKey(kStr, elVal); err != nil { + return nil, err + } + } + return v, nil case sourceT.IsPrimitiveType(): switch sourceT { case cty.String: @@ -173,9 +230,7 @@ func decodeCtyValue(source cty.Value) (res *model.Value, err error) { default: return nil, fmt.Errorf("unhandled primitive type %q", source.Type()) } - case sourceT.IsSetType(): - return nil, fmt.Errorf("set type not implemented") default: - return nil, fmt.Errorf("unhandled type: %s", sourceT.FriendlyName()) + return nil, fmt.Errorf("unsupported type: %s", sourceT.FriendlyName()) } } diff --git a/parsing/hcl/reader_test.go b/parsing/hcl/reader_test.go index e97e71fc..6b60a8f9 100644 --- a/parsing/hcl/reader_test.go +++ b/parsing/hcl/reader_test.go @@ -2,9 +2,10 @@ package hcl_test import ( "fmt" + "testing" + "github.com/tomwright/dasel/v3/parsing" "github.com/tomwright/dasel/v3/parsing/hcl" - "testing" ) type readTestCase struct { @@ -62,4 +63,25 @@ service "http" "web_proxy" { } }`, }.run) + t.Run("document c", readTestCase{ + in: `image_id = "ami-123" +cluster_min_nodes = 2 +cluster_decimal_nodes = 2.2 +cluster_max_nodes = true +availability_zone_names = [ +"us-east-1a", +"us-west-1c", +] +docker_ports = [{ +internal = 8300 +external = 8300 +protocol = "tcp" +}, +{ +internal = 8301 +external = 8301 +protocol = "tcp" +} +]`, + }.run) } From 38ec57f38ed65d923f2752443849d8b84c27fd08 Mon Sep 17 00:00:00 2001 From: Tom Wright Date: Thu, 31 Oct 2024 17:02:32 +0000 Subject: [PATCH 55/56] Deregister unimplemented XML writer --- parsing/xml/reader.go | 183 +++++++++++++++++++++++++++++++++++++++ parsing/xml/writer.go | 21 +++++ parsing/xml/xml.go | 197 +----------------------------------------- 3 files changed, 206 insertions(+), 195 deletions(-) create mode 100644 parsing/xml/reader.go create mode 100644 parsing/xml/writer.go diff --git a/parsing/xml/reader.go b/parsing/xml/reader.go new file mode 100644 index 00000000..61fa10ff --- /dev/null +++ b/parsing/xml/reader.go @@ -0,0 +1,183 @@ +package xml + +import ( + "bytes" + "encoding/xml" + "errors" + "fmt" + "io" + "unicode" + + "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/parsing" +) + +func newXMLReader(options parsing.ReaderOptions) (parsing.Reader, error) { + return &xmlReader{ + structured: options.Ext["xml-mode"] == "structured", + }, nil +} + +type xmlReader struct { + structured bool +} + +// Read reads a value from a byte slice. +func (j *xmlReader) Read(data []byte) (*model.Value, error) { + decoder := xml.NewDecoder(bytes.NewReader(data)) + decoder.Strict = true + + el, err := j.parseElement(decoder, xml.StartElement{ + Name: xml.Name{ + Local: "root", + }, + }) + if err != nil { + return nil, err + } + + if j.structured { + return el.toStructuredModel() + } + return el.toFriendlyModel() +} + +func (e *xmlElement) toStructuredModel() (*model.Value, error) { + attrs := model.NewMapValue() + for _, attr := range e.Attrs { + if err := attrs.SetMapKey(attr.Name, model.NewStringValue(attr.Value)); err != nil { + return nil, err + } + } + res := model.NewMapValue() + if err := res.SetMapKey("name", model.NewStringValue(e.Name)); err != nil { + return nil, err + } + if err := res.SetMapKey("attrs", attrs); err != nil { + return nil, err + } + + if err := res.SetMapKey("content", model.NewStringValue(e.Content)); err != nil { + return nil, err + } + children := model.NewSliceValue() + for _, child := range e.Children { + childModel, err := child.toStructuredModel() + if err != nil { + return nil, err + } + if err := children.Append(childModel); err != nil { + return nil, err + } + } + if err := res.SetMapKey("children", children); err != nil { + return nil, err + } + return res, nil +} + +func (e *xmlElement) toFriendlyModel() (*model.Value, error) { + if len(e.Attrs) == 0 && len(e.Children) == 0 { + return model.NewStringValue(e.Content), nil + } + + res := model.NewMapValue() + for _, attr := range e.Attrs { + if err := res.SetMapKey("-"+attr.Name, model.NewStringValue(attr.Value)); err != nil { + return nil, err + } + } + + if len(e.Content) > 0 { + if err := res.SetMapKey("#text", model.NewStringValue(e.Content)); err != nil { + return nil, err + } + } + + if len(e.Children) > 0 { + childElementKeys := make([]string, 0) + childElements := make(map[string][]*xmlElement) + + for _, child := range e.Children { + if _, ok := childElements[child.Name]; !ok { + childElementKeys = append(childElementKeys, child.Name) + } + childElements[child.Name] = append(childElements[child.Name], child) + } + + for _, key := range childElementKeys { + cs := childElements[key] + switch len(cs) { + case 0: + continue + case 1: + childModel, err := cs[0].toFriendlyModel() + if err != nil { + return nil, err + } + if err := res.SetMapKey(key, childModel); err != nil { + return nil, err + } + default: + children := model.NewSliceValue() + for _, child := range cs { + childModel, err := child.toFriendlyModel() + if err != nil { + return nil, err + } + if err := children.Append(childModel); err != nil { + return nil, err + } + } + if err := res.SetMapKey(key, children); err != nil { + return nil, err + } + } + } + } + + return res, nil +} + +func (j *xmlReader) parseElement(decoder *xml.Decoder, element xml.StartElement) (*xmlElement, error) { + el := &xmlElement{ + Name: element.Name.Local, + Attrs: make([]xmlAttr, 0), + Children: make([]*xmlElement, 0), + } + + for _, attr := range element.Attr { + el.Attrs = append(el.Attrs, xmlAttr{ + Name: attr.Name.Local, + Value: attr.Value, + }) + } + + for { + t, err := decoder.Token() + if errors.Is(err, io.EOF) { + if el.Name == "root" { + return el, nil + } + return nil, fmt.Errorf("unexpected EOF") + } + + switch t := t.(type) { + case xml.StartElement: + child, err := j.parseElement(decoder, t) + if err != nil { + return nil, err + } + el.Children = append(el.Children, child) + case xml.CharData: + if unicode.IsSpace([]rune(string(t))[0]) { + continue + } + el.Content += string(t) + case xml.EndElement: + return el, nil + default: + return nil, fmt.Errorf("unexpected token: %v", t) + } + } +} diff --git a/parsing/xml/writer.go b/parsing/xml/writer.go new file mode 100644 index 00000000..0a84e394 --- /dev/null +++ b/parsing/xml/writer.go @@ -0,0 +1,21 @@ +package xml + +import ( + "github.com/tomwright/dasel/v3/model" + "github.com/tomwright/dasel/v3/parsing" +) + +func newXMLWriter(options parsing.WriterOptions) (parsing.Writer, error) { + return &xmlWriter{ + options: options, + }, nil +} + +type xmlWriter struct { + options parsing.WriterOptions +} + +// Write writes a value to a byte slice. +func (j *xmlWriter) Write(value *model.Value) ([]byte, error) { + return nil, nil +} diff --git a/parsing/xml/xml.go b/parsing/xml/xml.go index c97d0337..897200dc 100644 --- a/parsing/xml/xml.go +++ b/parsing/xml/xml.go @@ -1,14 +1,6 @@ package xml import ( - "bytes" - "encoding/xml" - "errors" - "fmt" - "io" - "unicode" - - "github.com/tomwright/dasel/v3/model" "github.com/tomwright/dasel/v3/parsing" ) @@ -22,44 +14,8 @@ var _ parsing.Writer = (*xmlWriter)(nil) func init() { parsing.RegisterReader(XML, newXMLReader) - parsing.RegisterWriter(XML, newXMLWriter) -} - -func newXMLReader(options parsing.ReaderOptions) (parsing.Reader, error) { - return &xmlReader{ - structured: options.Ext["xml-mode"] == "structured", - }, nil -} - -// NewXMLWriter creates a new XML writer. -func newXMLWriter(options parsing.WriterOptions) (parsing.Writer, error) { - return &xmlWriter{ - options: options, - }, nil -} - -type xmlReader struct { - structured bool -} - -// Read reads a value from a byte slice. -func (j *xmlReader) Read(data []byte) (*model.Value, error) { - decoder := xml.NewDecoder(bytes.NewReader(data)) - decoder.Strict = true - - el, err := j.parseElement(decoder, xml.StartElement{ - Name: xml.Name{ - Local: "root", - }, - }) - if err != nil { - return nil, err - } - - if j.structured { - return el.toStructuredModel() - } - return el.toFriendlyModel() + // XML writer is not implemented yet + //parsing.RegisterWriter(XML, newXMLWriter) } type xmlAttr struct { @@ -73,152 +29,3 @@ type xmlElement struct { Children []*xmlElement Content string } - -func (e *xmlElement) toStructuredModel() (*model.Value, error) { - attrs := model.NewMapValue() - for _, attr := range e.Attrs { - if err := attrs.SetMapKey(attr.Name, model.NewStringValue(attr.Value)); err != nil { - return nil, err - } - } - res := model.NewMapValue() - if err := res.SetMapKey("name", model.NewStringValue(e.Name)); err != nil { - return nil, err - } - if err := res.SetMapKey("attrs", attrs); err != nil { - return nil, err - } - - if err := res.SetMapKey("content", model.NewStringValue(e.Content)); err != nil { - return nil, err - } - children := model.NewSliceValue() - for _, child := range e.Children { - childModel, err := child.toStructuredModel() - if err != nil { - return nil, err - } - if err := children.Append(childModel); err != nil { - return nil, err - } - } - if err := res.SetMapKey("children", children); err != nil { - return nil, err - } - return res, nil -} - -func (e *xmlElement) toFriendlyModel() (*model.Value, error) { - if len(e.Attrs) == 0 && len(e.Children) == 0 { - return model.NewStringValue(e.Content), nil - } - - res := model.NewMapValue() - for _, attr := range e.Attrs { - if err := res.SetMapKey("-"+attr.Name, model.NewStringValue(attr.Value)); err != nil { - return nil, err - } - } - - if len(e.Content) > 0 { - if err := res.SetMapKey("#text", model.NewStringValue(e.Content)); err != nil { - return nil, err - } - } - - if len(e.Children) > 0 { - childElementKeys := make([]string, 0) - childElements := make(map[string][]*xmlElement) - - for _, child := range e.Children { - if _, ok := childElements[child.Name]; !ok { - childElementKeys = append(childElementKeys, child.Name) - } - childElements[child.Name] = append(childElements[child.Name], child) - } - - for _, key := range childElementKeys { - cs := childElements[key] - switch len(cs) { - case 0: - continue - case 1: - childModel, err := cs[0].toFriendlyModel() - if err != nil { - return nil, err - } - if err := res.SetMapKey(key, childModel); err != nil { - return nil, err - } - default: - children := model.NewSliceValue() - for _, child := range cs { - childModel, err := child.toFriendlyModel() - if err != nil { - return nil, err - } - if err := children.Append(childModel); err != nil { - return nil, err - } - } - if err := res.SetMapKey(key, children); err != nil { - return nil, err - } - } - } - } - - return res, nil -} - -func (j *xmlReader) parseElement(decoder *xml.Decoder, element xml.StartElement) (*xmlElement, error) { - el := &xmlElement{ - Name: element.Name.Local, - Attrs: make([]xmlAttr, 0), - Children: make([]*xmlElement, 0), - } - - for _, attr := range element.Attr { - el.Attrs = append(el.Attrs, xmlAttr{ - Name: attr.Name.Local, - Value: attr.Value, - }) - } - - for { - t, err := decoder.Token() - if errors.Is(err, io.EOF) { - if el.Name == "root" { - return el, nil - } - return nil, fmt.Errorf("unexpected EOF") - } - - switch t := t.(type) { - case xml.StartElement: - child, err := j.parseElement(decoder, t) - if err != nil { - return nil, err - } - el.Children = append(el.Children, child) - case xml.CharData: - if unicode.IsSpace([]rune(string(t))[0]) { - continue - } - el.Content += string(t) - case xml.EndElement: - return el, nil - default: - return nil, fmt.Errorf("unexpected token: %v", t) - } - } -} - -type xmlWriter struct { - options parsing.WriterOptions -} - -// Write writes a value to a byte slice. -func (j *xmlWriter) Write(value *model.Value) ([]byte, error) { - return nil, nil -} From 7e48c95981d1457aa9b5eb61cca504bcdff40e8a Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 7 Nov 2024 22:08:17 +0000 Subject: [PATCH 56/56] Implement first draft of HCL writer --- parsing/hcl/hcl.go | 3 +- parsing/hcl/writer.go | 170 ++++++++++++++++++++++++++++++++++++- parsing/hcl/writer_test.go | 65 ++++++++++++++ 3 files changed, 235 insertions(+), 3 deletions(-) create mode 100644 parsing/hcl/writer_test.go diff --git a/parsing/hcl/hcl.go b/parsing/hcl/hcl.go index 9b14104f..17e9c3c8 100644 --- a/parsing/hcl/hcl.go +++ b/parsing/hcl/hcl.go @@ -14,6 +14,5 @@ var _ parsing.Writer = (*hclWriter)(nil) func init() { parsing.RegisterReader(HCL, newHCLReader) - // HCL writer is not implemented yet - //parsing.RegisterWriter(HCL, newHCLWriter) + parsing.RegisterWriter(HCL, newHCLWriter) } diff --git a/parsing/hcl/writer.go b/parsing/hcl/writer.go index 307b4788..7ebb510d 100644 --- a/parsing/hcl/writer.go +++ b/parsing/hcl/writer.go @@ -1,8 +1,12 @@ package hcl import ( + "bytes" + "fmt" + "github.com/hashicorp/hcl/v2/hclwrite" "github.com/tomwright/dasel/v3/model" "github.com/tomwright/dasel/v3/parsing" + "github.com/zclconf/go-cty/cty" ) func newHCLWriter(options parsing.WriterOptions) (parsing.Writer, error) { @@ -15,5 +19,169 @@ type hclWriter struct { // Write writes a value to a byte slice. func (j *hclWriter) Write(value *model.Value) ([]byte, error) { - return nil, nil + f, err := j.valueToFile(value) + if err != nil { + return nil, err + } + + buf := new(bytes.Buffer) + if _, err := f.WriteTo(buf); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func (j *hclWriter) valueToFile(v *model.Value) (*hclwrite.File, error) { + f := hclwrite.NewEmptyFile() + + body := f.Body() + + if err := j.addValueToBody(nil, v, body); err != nil { + return nil, err + } + + return f, nil +} + +func (j *hclWriter) addValueToBody(previousLabels []string, v *model.Value, body *hclwrite.Body) error { + if !v.IsMap() { + return fmt.Errorf("hcl body is expected to be a map, got %s", v.Type()) + } + + kvs, err := v.MapKeyValues() + if err != nil { + return err + } + + blocks := make([]*hclwrite.Block, 0) + for _, kv := range kvs { + switch kv.Value.Type() { + case model.TypeMap: + block, err := j.valueToBlock(kv.Key, previousLabels, kv.Value) + if err != nil { + return fmt.Errorf("failed to encode %q to hcl block: %w", kv.Key, err) + } + blocks = append(blocks, block) + case model.TypeSlice: + vals := make([]cty.Value, 0) + + allMaps := true + + if err := kv.Value.RangeSlice(func(_ int, value *model.Value) error { + ctyVal, err := j.valueToCty(value) + if err != nil { + return err + } + vals = append(vals, ctyVal) + + if !value.IsMap() { + allMaps = false + } + return nil + }); err != nil { + return err + } + + if allMaps { + if err := kv.Value.RangeSlice(func(_ int, value *model.Value) error { + block, err := j.valueToBlock(kv.Key, previousLabels, value) + if err != nil { + return fmt.Errorf("failed to encode %q to hcl block: %w", kv.Key, err) + } + blocks = append(blocks, block) + return nil + }); err != nil { + return err + } + } else { + body.SetAttributeValue(kv.Key, cty.TupleVal(vals)) + } + + default: + ctyVal, err := j.valueToCty(kv.Value) + if err != nil { + return fmt.Errorf("failed to encode attribute %q: %w", kv.Key, err) + } + body.SetAttributeValue(kv.Key, ctyVal) + } + } + + for _, block := range blocks { + body.AppendBlock(block) + } + + return nil +} + +func (j *hclWriter) valueToCty(v *model.Value) (cty.Value, error) { + switch v.Type() { + case model.TypeString: + val, err := v.StringValue() + if err != nil { + return cty.Value{}, err + } + return cty.StringVal(val), nil + case model.TypeBool: + val, err := v.BoolValue() + if err != nil { + return cty.Value{}, err + } + return cty.BoolVal(val), nil + case model.TypeInt: + val, err := v.IntValue() + if err != nil { + return cty.Value{}, err + } + return cty.NumberIntVal(val), nil + case model.TypeFloat: + val, err := v.FloatValue() + if err != nil { + return cty.Value{}, err + } + return cty.NumberFloatVal(val), nil + case model.TypeNull: + return cty.NullVal(cty.NilType), nil + case model.TypeSlice: + var vals []cty.Value + if err := v.RangeSlice(func(_ int, value *model.Value) error { + ctyVal, err := j.valueToCty(value) + if err != nil { + return err + } + vals = append(vals, ctyVal) + return nil + }); err != nil { + return cty.Value{}, err + } + return cty.TupleVal(vals), nil + case model.TypeMap: + mapV := map[string]cty.Value{} + if err := v.RangeMap(func(s string, value *model.Value) error { + ctyVal, err := j.valueToCty(value) + if err != nil { + return err + } + mapV[s] = ctyVal + return nil + }); err != nil { + return cty.Value{}, err + } + return cty.ObjectVal(mapV), nil + default: + return cty.Value{}, fmt.Errorf("unhandled type when converting to cty value %q", v.Type()) + } +} + +func (j *hclWriter) valueToBlock(key string, labels []string, v *model.Value) (*hclwrite.Block, error) { + if !v.IsMap() { + return nil, fmt.Errorf("must be map") + } + + b := hclwrite.NewBlock(key, labels) + + if err := j.addValueToBody(labels, v, b.Body()); err != nil { + return nil, err + } + + return b, nil } diff --git a/parsing/hcl/writer_test.go b/parsing/hcl/writer_test.go new file mode 100644 index 00000000..c7aa398e --- /dev/null +++ b/parsing/hcl/writer_test.go @@ -0,0 +1,65 @@ +package hcl_test + +import ( + "github.com/google/go-cmp/cmp" + "testing" + + "github.com/tomwright/dasel/v3/parsing" + "github.com/tomwright/dasel/v3/parsing/hcl" +) + +type readWriteTestCase struct { + in string +} + +func (tc readWriteTestCase) run(t *testing.T) { + r, err := hcl.HCL.NewReader(parsing.DefaultReaderOptions()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + w, err := hcl.HCL.NewWriter(parsing.DefaultWriterOptions()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + in := []byte(tc.in) + + data, err := r.Read(in) + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + got, err := w.Write(data) + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + gotStr := string(got) + + if !cmp.Equal(tc.in, gotStr) { + t.Errorf("unexpected output: %s", cmp.Diff(tc.in, gotStr)) + } +} + +func TestHclReader_ReadWrite(t *testing.T) { + t.Run("document a", readWriteTestCase{ + in: `io_mode = "async" + +service "http" "web_proxy" { + listen_addr = "127.0.0.1:8080" + + process "main" { + command = ["/usr/local/bin/awesome-app", "server"] + } + + process "mgmt" { + command = ["/usr/local/bin/awesome-app", "mgmt"] + } + + process "mgmt" { + command = ["/usr/local/bin/awesome-app", "mgmt2"] + } +}`, + }.run) +}