Skip to content

Commit

Permalink
Allow .json(...) to be called on all types, closes #54
Browse files Browse the repository at this point in the history
```
⧐  type('{}'.json())
HASH
⧐  type('[]'.json())
ARRAY
⧐  type('"hello"'.json())
STRING
⧐  type('1'.json())
NUMBER
```

In this PR I had to revert some of the changes we made in the tests
earlier on. Using `strings.Contains` in the tests is very dangerous,
and I reverted back to equality for most tests -- with the exception
of the tests that check for error messages that use `strings.HasPrefix`
which should be a bit more robust. The problem with `strings.Contains`
is that is a tests expects a string (`"hello"`) and instead an error
is thrown (`"Unable to call function: hello"`) the test will silently
pass since it finds our expected string inside the error message. With
`strings.HasPrefix` we limit these cases considerably.

The end goal would be to make sure
we can actually differentiate when ABS returns a string and an error string,
but for now this is something we can work with.
  • Loading branch information
odino committed Jan 22, 2019
1 parent 7c23ce6 commit 4e277f7
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 25 deletions.
29 changes: 19 additions & 10 deletions evaluator/evaluator_test.go
@@ -1,6 +1,7 @@
package evaluator

import (
"flag"
"fmt"
"strings"
"testing"
Expand All @@ -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)
Expand Down Expand Up @@ -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},
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
}
}
Expand Down
46 changes: 36 additions & 10 deletions evaluator/functions.go
Expand Up @@ -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())
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand Down
10 changes: 5 additions & 5 deletions parser/parser.go
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)}
}

Expand Down
8 changes: 8 additions & 0 deletions 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 {
Expand All @@ -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
}

0 comments on commit 4e277f7

Please sign in to comment.