diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go index 6feecf9b..c84469c1 100644 --- a/evaluator/evaluator_test.go +++ b/evaluator/evaluator_test.go @@ -1,6 +1,7 @@ package evaluator import ( + "flag" "fmt" "strings" "testing" @@ -14,8 +15,11 @@ func logErrorWithPosition(t *testing.T, msg string, expected interface{}) { errorStr := msg expected, _ = expected.(string) expectedStr := fmt.Sprintf("%s", expected) - if strings.Contains(errorStr, expectedStr) { - t.Log("expected error:", errorStr) + if strings.HasPrefix(errorStr, expectedStr) { + // Only log when we're running the verbose tests + if flag.Lookup("test.v").Value.String() == "true" { + t.Log("expected error:", errorStr) + } } else { expectedStr = fmt.Sprintf("ERROR: wrong error message. expected='%s',", expectedStr) t.Error(expectedStr, "\ngot=", errorStr) @@ -651,7 +655,7 @@ func TestBuiltinFunctions(t *testing.T) { {`find([1,2], f(x) {x == "some"})`, nil}, {`arg("o")`, "argument 0 to arg(...) is not supported (got: o, allowed: NUMBER)"}, {`arg(3)`, ""}, - {`pwd().split("").reverse().slice(0, 33).reverse().join("").replace("\\", "/", -1)`, "/evaluator"}, // Little trick to get travis to run this test, as the base path is not /go/src/ + {`pwd().split("").reverse().slice(0, 33).reverse().join("").replace("\\", "/", -1).suffix("/evaluator")`, true}, // Little trick to get travis to run this test, as the base path is not /go/src/ {`rand(1)`, 0}, {`int(10)`, 10}, {`int(10.5)`, 10}, @@ -684,8 +688,13 @@ func TestBuiltinFunctions(t *testing.T) { {`"{\"a\": null}".json().a`, nil}, {`type(null)`, "NULL"}, {`"{\"k\": \"v\"}".json()["k"]`, "v"}, - {`"hello".json()`, "argument to `json` must be a valid JSON object, got 'hello'"}, - {`"\"hello".json()`, "argument to `json` must be a valid JSON object, got '\"hello'"}, + {`"2".json()`, 2}, + {`'"2"'.json()`, "2"}, + {`'true'.json()`, true}, + {`'null'.json()`, nil}, + {`'"hello"'.json()`, "hello"}, + {`'[1, 2, 3]'.json()`, []int{1, 2, 3}}, + {`'"hello'.json()`, "argument to `json` must be a valid JSON object, got '\"hello'"}, {`split("a\"b\"c", "\"")`, []string{"a", "b", "c"}}, {`lines("a b @@ -774,7 +783,7 @@ c")`, []string{"a", "b", "c"}}, case string: s, ok := evaluated.(*object.String) if ok { - if !strings.Contains(s.Value, tt.expected.(string)) { + if s.Value != tt.expected.(string) { t.Errorf("result is not the right string for '%s'. got='%s', want='%s'", tt.input, s.Value, tt.expected) } continue @@ -863,7 +872,7 @@ func TestLogicalOperators(t *testing.T) { case string: s, ok := evaluated.(*object.String) if ok { - if !strings.Contains(s.Value, tt.expected.(string)) { + if s.Value != tt.expected.(string) { t.Errorf("result is not the right string for '%s'. got='%s', want='%s'", tt.input, s.Value, tt.expected) } @@ -916,7 +925,7 @@ func TestRangesOperators(t *testing.T) { case string: s, ok := evaluated.(*object.String) if ok { - if !strings.Contains(s.Value, tt.expected.(string)) { + if s.Value != tt.expected.(string) { t.Errorf("result is not the right string for '%s'. got='%s', want='%s'", tt.input, s.Value, tt.expected) } @@ -975,7 +984,7 @@ func TestBuiltinProperties(t *testing.T) { case string: s, ok := evaluated.(*object.String) if ok { - if !strings.Contains(s.Value, tt.expected.(string)) { + if s.Value != tt.expected.(string) { t.Errorf("result is not the right string for '%s'. got='%s', want='%s'", tt.input, s.Value, tt.expected) } @@ -1048,7 +1057,7 @@ func TestCommand(t *testing.T) { t.Errorf("object is not String. got=%T (%+v)", evaluated, evaluated) continue } - if !strings.Contains(stringObj.Value, expected) { + if stringObj.Value != expected { t.Errorf("result is not the right string for '%s'. got='%s', want='%s'", tt.input, stringObj.Value, expected) } } diff --git a/evaluator/functions.go b/evaluator/functions.go index cce11785..2f9b462c 100644 --- a/evaluator/functions.go +++ b/evaluator/functions.go @@ -254,9 +254,7 @@ func getFns() map[string]*object.Builtin { case *object.Number: return &object.Boolean{Token: tok, Value: true} case *object.String: - _, err := strconv.ParseFloat(arg.Value, 64) - - return &object.Boolean{Token: tok, Value: err == nil} + return &object.Boolean{Token: tok, Value: util.IsNumber(arg.Value)} default: // we will never reach here return newError(tok, "argument to `is_number` not supported, got %s", args[0].Type()) @@ -389,10 +387,6 @@ func getFns() map[string]*object.Builtin { // // Also, we're instantiating a new lexer & parser from // scratch, so this is a tad slow. - // - // This method is incomplete as it currently does not - // support most JSON types, but rather just objects, - // ie. "[1, 2, 3]".json() won't work. "json": &object.Builtin{ Types: []string{object.STRING_OBJ}, Fn: func(args ...object.Object) object.Object { @@ -402,13 +396,45 @@ func getFns() map[string]*object.Builtin { } s := args[0].(*object.String) + str := strings.TrimSpace(s.Value) env := object.NewEnvironment() - l := lexer.New(s.Value) + l := lexer.New(str) p := parser.New(l) - hl, ok := p.ParseHashLiteral().(*ast.HashLiteral) + var node ast.Node + ok := false + + // JSON types: + // - objects + // - arrays + // - number + // - string + // - null + // - bool + switch str[0] { + case '{': + node, ok = p.ParseHashLiteral().(*ast.HashLiteral) + case '[': + node, ok = p.ParseArrayLiteral().(*ast.ArrayLiteral) + } + + if str[0] == '"' && str[len(str)-1] == '"' { + node, ok = p.ParseStringLiteral().(*ast.StringLiteral) + } + + if util.IsNumber(str) { + node, ok = p.ParseNumberLiteral().(*ast.NumberLiteral) + } + + if str == "false" || str == "true" { + node, ok = p.ParseBoolean().(*ast.Boolean) + } + + if str == "null" { + return NULL + } if ok { - return evalHashLiteral(hl, env) + return Eval(node, env) } return newError(tok, "argument to `json` must be a valid JSON object, got '%s'", s.Value) diff --git a/parser/parser.go b/parser/parser.go index c8ff6580..8adb5fd6 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -84,14 +84,14 @@ func New(l *lexer.Lexer) *Parser { p.prefixParseFns = make(map[token.TokenType]prefixParseFn) p.registerPrefix(token.IDENT, p.parseIdentifier) - p.registerPrefix(token.NUMBER, p.parseNumberLiteral) + p.registerPrefix(token.NUMBER, p.ParseNumberLiteral) p.registerPrefix(token.STRING, p.ParseStringLiteral) p.registerPrefix(token.NULL, p.ParseNullLiteral) p.registerPrefix(token.BANG, p.parsePrefixExpression) p.registerPrefix(token.MINUS, p.parsePrefixExpression) p.registerPrefix(token.TILDE, p.parsePrefixExpression) - p.registerPrefix(token.TRUE, p.parseBoolean) - p.registerPrefix(token.FALSE, p.parseBoolean) + p.registerPrefix(token.TRUE, p.ParseBoolean) + p.registerPrefix(token.FALSE, p.ParseBoolean) p.registerPrefix(token.LPAREN, p.parseGroupedExpression) p.registerPrefix(token.IF, p.parseIfExpression) p.registerPrefix(token.WHILE, p.parseWhileExpression) @@ -368,7 +368,7 @@ func (p *Parser) parseIdentifier() ast.Expression { } // 1 or 1.1 -func (p *Parser) parseNumberLiteral() ast.Expression { +func (p *Parser) ParseNumberLiteral() ast.Expression { lit := &ast.NumberLiteral{Token: p.curToken} value, err := strconv.ParseFloat(p.curToken.Literal, 64) @@ -482,7 +482,7 @@ func (p *Parser) parseMethodExpression(object ast.Expression) ast.Expression { } // true -func (p *Parser) parseBoolean() ast.Expression { +func (p *Parser) ParseBoolean() ast.Expression { return &ast.Boolean{Token: p.curToken, Value: p.curTokenIs(token.TRUE)} } diff --git a/util/util.go b/util/util.go index 14e4a204..d3d870ee 100644 --- a/util/util.go +++ b/util/util.go @@ -1,5 +1,7 @@ package util +import "strconv" + // Checks whether the element e is in the // list of strings s func Contains(s []string, e string) bool { @@ -10,3 +12,9 @@ func Contains(s []string, e string) bool { } return false } + +func IsNumber(s string) bool { + _, err := strconv.ParseFloat(s, 64) + + return err == nil +}