Skip to content

Commit

Permalink
Merge pull request blues#17 from xiatechs/improve-errors
Browse files Browse the repository at this point in the history
Improve errors
  • Loading branch information
golbanstefan authored May 18, 2023
2 parents 49373f6 + 1861ad7 commit d58a160
Show file tree
Hide file tree
Showing 15 changed files with 654 additions and 165 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*.so
*.dylib
.idea
.idea/*

# Test binary, built with `go test -c`
*.test
Expand Down
6 changes: 3 additions & 3 deletions callable.go
Original file line number Diff line number Diff line change
Expand Up @@ -689,7 +689,7 @@ func (f *transformationCallable) Call(argv []reflect.Value) (reflect.Value, erro

obj, err := f.clone(argv[0])
if err != nil {
return undefined, newEvalError(ErrClone, nil, nil)
return undefined, newEvalError(ErrClone, nil, nil, 0)
}

if obj == undefined {
Expand Down Expand Up @@ -746,7 +746,7 @@ func (f *transformationCallable) updateEntries(item reflect.Value) error {
}

if !jtypes.IsMap(updates) {
return newEvalError(ErrIllegalUpdate, f.updates, nil)
return newEvalError(ErrIllegalUpdate, f.updates, nil, 0)
}

for _, key := range updates.MapKeys() {
Expand All @@ -766,7 +766,7 @@ func (f *transformationCallable) deleteEntries(item reflect.Value) error {
deletes = arrayify(deletes)

if !jtypes.IsArrayOf(deletes, jtypes.IsString) {
return newEvalError(ErrIllegalDelete, f.deletes, nil)
return newEvalError(ErrIllegalDelete, f.deletes, nil, 0)
}

for i := 0; i < deletes.Len(); i++ {
Expand Down
5 changes: 3 additions & 2 deletions callable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package jsonata

import (
"errors"
"github.com/stretchr/testify/assert"
"math"
"reflect"
"regexp"
Expand Down Expand Up @@ -2329,8 +2330,8 @@ func testTransformationCallable(t *testing.T, tests []transformationCallableTest
}
}

if !reflect.DeepEqual(err, test.Error) {
t.Errorf("transform %d: expected error %v, got %v", i+1, test.Error, err)
if err != nil && test.Error != nil {
assert.EqualError(t, err, test.Error.Error())
}
}
}
Expand Down
65 changes: 37 additions & 28 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,39 +50,41 @@ const (
)

var errmsgs = map[ErrType]string{
ErrNonIntegerLHS: `left side of the "{{value}}" operator must evaluate to an integer`,
ErrNonIntegerRHS: `right side of the "{{value}}" operator must evaluate to an integer`,
ErrNonNumberLHS: `left side of the "{{value}}" operator must evaluate to a number`,
ErrNonNumberRHS: `right side of the "{{value}}" operator must evaluate to a number`,
ErrNonComparableLHS: `left side of the "{{value}}" operator must evaluate to a number or string`,
ErrNonComparableRHS: `right side of the "{{value}}" operator must evaluate to a number or string`,
ErrTypeMismatch: `both sides of the "{{value}}" operator must have the same type`,
ErrNonCallable: `cannot call non-function {{token}}`,
ErrNonCallableApply: `cannot use function application with non-function {{token}}`,
ErrNonCallablePartial: `cannot partially apply non-function {{token}}`,
ErrNumberInf: `result of the "{{value}}" operator is out of range`,
ErrNumberNaN: `result of the "{{value}}" operator is not a valid number`,
ErrMaxRangeItems: `range operator has too many items`,
ErrIllegalKey: `object key {{token}} does not evaluate to a string`,
ErrDuplicateKey: `multiple object keys evaluate to the value "{{value}}"`,
ErrClone: `object transformation: cannot make a copy of the object`,
ErrIllegalUpdate: `the insert/update clause of an object transformation must evaluate to an object`,
ErrIllegalDelete: `the delete clause of an object transformation must evaluate to an array of strings`,
ErrNonSortable: `expressions in a sort term must evaluate to strings or numbers`,
ErrSortMismatch: `expressions in a sort term must have the same type`,
ErrNonIntegerLHS: `left side of the "{{value}}" operator must evaluate to an integer, position:{{position}}, arguments: {{arguments}}`,
ErrNonIntegerRHS: `right side of the "{{value}}" operator must evaluate to an integer, position:{{position}}, arguments: {{arguments}}`,
ErrNonNumberLHS: `left side of the "{{value}}" operator must evaluate to a number, position:{{position}}, arguments: {{arguments}}`,
ErrNonNumberRHS: `right side of the "{{value}}" operator must evaluate to a number, position:{{position}}, arguments: {{arguments}}`,
ErrNonComparableLHS: `left side of the "{{value}}" operator must evaluate to a number or string, position:{{position}}, arguments: {{arguments}}`,
ErrNonComparableRHS: `right side of the "{{value}}" operator must evaluate to a number or string, position:{{position}}, arguments: {{arguments}}`,
ErrTypeMismatch: `both sides of the "{{value}}" operator must have the same type, position:{{position}}, arguments: {{arguments}}`,
ErrNonCallable: `cannot call non-function {{token}}, position:{{position}}, arguments: {{arguments}}`,
ErrNonCallableApply: `cannot use function application with non-function {{token}}, position:{{position}}, arguments: {{arguments}}`,
ErrNonCallablePartial: `cannot partially apply non-function {{token}}, position:{{position}}, arguments: {{arguments}}`,
ErrNumberInf: `result of the "{{value}}" operator is out of range, position:{{position}}, arguments: {{arguments}}`,
ErrNumberNaN: `result of the "{{value}}" operator is not a valid number, position:{{position}}, arguments: {{arguments}}`,
ErrMaxRangeItems: `range operator has too many items, position:{{position}}, arguments: {{arguments}}`,
ErrIllegalKey: `object key {{token}} does not evaluate to a string, position:{{position}}, arguments: {{arguments}}`,
ErrDuplicateKey: `multiple object keys evaluate to the value "{{value}}", position:{{position}}, arguments: {{arguments}}`,
ErrClone: `object transformation: cannot make a copy of the object, position:{{position}}, arguments: {{arguments}}`,
ErrIllegalUpdate: `the insert/update clause of an object transformation must evaluate to an object, position:{{position}}, arguments: {{arguments}}`,
ErrIllegalDelete: `the delete clause of an object transformation must evaluate to an array of strings, position:{{position}}, arguments: {{arguments}}`,
ErrNonSortable: `expressions in a sort term must evaluate to strings or numbers, position:{{position}}, arguments: {{arguments}}`,
ErrSortMismatch: `expressions in a sort term must have the same type, position:{{position}}, arguments: {{arguments}}`,
}

var reErrMsg = regexp.MustCompile("{{(token|value)}}")
var reErrMsg = regexp.MustCompile("{{(token|value|position|arguments)}}")

// An EvalError represents an error during evaluation of a
// JSONata expression.
type EvalError struct {
Type ErrType
Token string
Value string
Type ErrType
Token string
Value string
Pos int
Arguments string
}

func newEvalError(typ ErrType, token interface{}, value interface{}) *EvalError {
func newEvalError(typ ErrType, token interface{}, value interface{}, pos int) *EvalError {

stringify := func(v interface{}) string {
switch v := v.(type) {
Expand All @@ -99,6 +101,7 @@ func newEvalError(typ ErrType, token interface{}, value interface{}) *EvalError
Type: typ,
Token: stringify(token),
Value: stringify(value),
Pos: pos,
}
}

Expand All @@ -115,6 +118,10 @@ func (e EvalError) Error() string {
return e.Token
case "{{value}}":
return e.Value
case "{{arguments}}":
return e.Arguments
case "{{position}}":
return fmt.Sprintf("%v", e.Pos)
default:
return match
}
Expand Down Expand Up @@ -146,8 +153,10 @@ func (e ArgCountError) Error() string {
// expression contains a function call with the wrong argument
// type.
type ArgTypeError struct {
Func string
Which int
Func string
Which int
Pos int
Arguments string
}

func newArgTypeError(f jtypes.Callable, which int) *ArgTypeError {
Expand All @@ -158,5 +167,5 @@ func newArgTypeError(f jtypes.Callable, which int) *ArgTypeError {
}

func (e ArgTypeError) Error() string {
return fmt.Sprintf("argument %d of function %q does not match function signature", e.Which, e.Func)
return fmt.Sprintf("argument %d of function %q does not match function signature, position: %v, arguments: %v", e.Which, e.Func, e.Pos, e.Arguments)
}
190 changes: 190 additions & 0 deletions errrors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package jsonata

import (
"encoding/json"
"github.com/stretchr/testify/assert"
"testing"
)

func TestErrors(t *testing.T) {
// your JSON data
data1 := `{
"employees": [
{
"firstName": "John",
"lastName": "Doe",
"department": "Sales",
"salary": 50000,
"joining_date": "2020-05-12",
"details": {
"address": "123 Main St",
"city": "New York",
"state": "NY"
}
},
{
"firstName": "Anna",
"lastName": "Smith",
"department": "Marketing",
"salary": 60000,
"joining_date": "2019-07-01",
"details": {
"address": "456 Market St",
"city": "San Francisco",
"state": "CA"
}
},
{
"firstName": "Peter",
"lastName": "Jones",
"department": "Sales",
"salary": 70000,
"joining_date": "2021-01-20",
"details": {
"address": "789 Broad St",
"city": "Los Angeles",
"state": "CA"
}
}
]
}`
var data interface{}

// Decode JSON.
err := json.Unmarshal([]byte(data1), &data)
assert.NoError(t, err)
t.Run("wrong arithmetic errors", func(t *testing.T) {

// Create expression.
e := MustCompile("employees.firstName + 5")

// Evaluate.
_, err := e.Eval(data)
assert.Error(t, err, "left side of the \"value:+, position: 20\" operator must evaluate to a number")
})
t.Run("Cannot call non-function token:", func(t *testing.T) {

// Create expression.
e := MustCompile("employees.details.state.$address()")

// Evaluate.
_, err := e.Eval(data)
assert.EqualError(t, err, "cannot call non-function $address, position:25, arguments: ")

})
t.Run("Trying to get the maximum of a string field:", func(t *testing.T) {

// Create expression.
e := MustCompile("$max(employees.firstName)")

// Evaluate.
_, err := e.Eval(data)
assert.EqualError(t, err, "cannot call max on an array with non-number types, position: 1, arguments: number:0 value:[John Anna Peter] ")

})
t.Run("Invalid Function Call on a non-array field:", func(t *testing.T) {

// Create expression.
e := MustCompile("employees.department.$count()")

// Evaluate.
_, err := e.Eval(data)
assert.EqualError(t, err, "function \"count\" takes 1 argument(s), got 0, position: 22, arguments: ")

})
t.Run("Cannot use wildcard on non-object type:", func(t *testing.T) {

// Create expression.
e := MustCompile("employees.*.salary")

// Evaluate.
_, err := e.Eval(data)
assert.EqualError(t, err, "no results found")
})
t.Run("Indexing on non-array type:", func(t *testing.T) {

// Create expression.
e := MustCompile("employees.firstName[1]")

// Evaluate.
_, err := e.Eval(data)
assert.EqualError(t, err, "no results found")

})
t.Run("Use of an undefined variable:", func(t *testing.T) {

// Create expression.
e := MustCompile("$undefinedVariable")

// Evaluate.
_, err := e.Eval(data)
assert.EqualError(t, err, "no results found")

})
t.Run("Use of an undefined function:", func(t *testing.T) {

// Create expression.
e := MustCompile("$undefinedFunction()")

// Evaluate.
_, err := e.Eval(data)
assert.EqualError(t, err, "cannot call non-function $undefinedFunction, position:1, arguments: ")

})
t.Run("Comparison of incompatible types:", func(t *testing.T) {

// Create expression.
e := MustCompile("employees.firstName > employees.salary")

// Evaluate.
_, err := e.Eval(data)
assert.EqualError(t, err, "left side of the \">\" operator must evaluate to a number or string, position:0, arguments: ")

})
t.Run("Use of an invalid JSONata operator:", func(t *testing.T) {

// Create expression.
_, err := Compile("employees ! employees")
assert.EqualError(t, err, "syntax error: '', position: 10")
})
t.Run("Incorrect use of the reduce function:", func(t *testing.T) {

// Create expression.
e := MustCompile("$reduce(employees.firstName, function($acc, $val) { $acc + $val })")

// Evaluate.
_, err := e.Eval(data)
assert.ErrorContains(t, err, "left side of the \"+\" operator must evaluate to a number, position:57, arguments: ")

})
t.Run("Incorrect use of the map function:", func(t *testing.T) {

// Create expression.
e := MustCompile("$map(employees, function($employee) { $employee.firstName + 5 })")

// Evaluate.
_, err := e.Eval(data)
assert.ErrorContains(t, err, "left side of the \"+\" operator must evaluate to a number, position:58")

})
t.Run("Incorrect use of the filter function:", func(t *testing.T) {

// Create expression.
e := MustCompile("$filter(employees, function($employee) { $employee.salary.$uppercase() })")

// Evaluate.
_, err := e.Eval(data)
assert.ErrorContains(t, err, "argument 1 of function \"uppercase\" does not match function signature, position: 1, arguments: number")

})
t.Run("Incorrect use of the join function:", func(t *testing.T) {

// Create expression.
e := MustCompile("$join(employees.firstName, 5)")

// Evaluate.
_, err := e.Eval(data)
assert.ErrorContains(t, err, "argument 2 of function \"join\" does not match function signature, position: 1, arguments: number:0 value:[John Anna Peter] number:1 value:5 ")

})
}
Loading

0 comments on commit d58a160

Please sign in to comment.