Skip to content

Commit

Permalink
Merge ca0e087 into e6ecc94
Browse files Browse the repository at this point in the history
  • Loading branch information
angshumanHalder committed Apr 16, 2023
2 parents e6ecc94 + ca0e087 commit cbfbf03
Show file tree
Hide file tree
Showing 9 changed files with 485 additions and 178 deletions.
9 changes: 8 additions & 1 deletion array.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package httpexpect

import (
"encoding/json"
"errors"
"fmt"
"reflect"
Expand Down Expand Up @@ -1745,7 +1746,7 @@ func builtinComparator(opChain *chain, array []interface{}) func(x, y *Value) bo
var prev interface{}
for index, curr := range array {
switch curr.(type) {
case bool, float64, string, nil:
case bool, float64, string, nil, json.Number:
// ok, do nothing

default:
Expand Down Expand Up @@ -1817,6 +1818,12 @@ func builtinComparator(opChain *chain, array []interface{}) func(x, y *Value) bo
yVal := y.Raw().(string)
return xVal < yVal
}
case json.Number:
return func(x, y *Value) bool {
xVal := x.Raw().(json.Number)
yVal := y.Raw().(json.Number)
return xVal < yVal
}
case nil:
return func(x, y *Value) bool {
// `nil` is never less than `nil`
Expand Down
263 changes: 246 additions & 17 deletions canon.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package httpexpect

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"math/big"
"reflect"
"strconv"
)

func canonNumber(opChain *chain, in interface{}) (out float64, ok bool) {
func canonNumber(opChain *chain, in interface{}) (out *big.Float, ok bool) {
ok = true
defer func() {
if err := recover(); err != nil {
Expand All @@ -22,10 +26,79 @@ func canonNumber(opChain *chain, in interface{}) (out float64, ok bool) {
ok = false
}
}()
out = reflect.ValueOf(in).Convert(reflect.TypeOf(float64(0))).Float()

if in != in {
out, ok = nil, false
return
}

out, ok = canonNumberConvert(in)
if !ok {
opChain.fail(AssertionFailure{
Type: AssertValid,
Actual: &AssertionValue{in},
Errors: []error{
errors.New("expected: valid number"),
},
})
ok = false
}
return
}

func canonNumberConvert(in interface{}) (out *big.Float, ok bool) {
value := reflect.ValueOf(in)
switch in := in.(type) {
case big.Int:
val := in
return big.NewFloat(0).SetInt(&val), true
case big.Float:
return &in, true
case json.Number:
data := in.String()
num, ok := big.NewFloat(0).SetString(data)
return num, ok
default:
return canonConvertNumberNative(value, in)
}
}

func canonConvertNumberNative(
value reflect.Value,
in interface{},
) (out *big.Float, ok bool) {
t := reflect.TypeOf(in).Kind()
switch t {
case reflect.Float64, reflect.Float32:
float := value.Float()
return big.NewFloat(float), true
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
int := value.Int()
return big.NewFloat(0).SetInt64(int), true
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
int := value.Uint()
return big.NewFloat(0).SetUint64(int), true
case reflect.Invalid,
reflect.Bool,
reflect.Uintptr,
reflect.Complex64,
reflect.Complex128,
reflect.Array,
reflect.Chan,
reflect.Func,
reflect.Interface,
reflect.Map,
reflect.Slice,
reflect.String,
reflect.Struct,
reflect.Ptr,
reflect.UnsafePointer:
return big.NewFloat(0), false
default:
return big.NewFloat(0), false
}
}

func canonArray(opChain *chain, in interface{}) ([]interface{}, bool) {
var out []interface{}
data, ok := canonValue(opChain, in)
Expand Down Expand Up @@ -77,17 +150,9 @@ func canonValue(opChain *chain, in interface{}) (interface{}, bool) {
}

var out interface{}
if err := json.Unmarshal(b, &out); err != nil {
opChain.fail(AssertionFailure{
Type: AssertValid,
Actual: &AssertionValue{in},
Errors: []error{
errors.New("expected: unmarshalable value"),
err,
},
})
return nil, false
}

jsonDecode(opChain, b, &out)
out = convertJSONNumberToFloatOrBigFloat(out)

return out, true
}
Expand Down Expand Up @@ -115,14 +180,178 @@ func canonDecode(opChain *chain, value interface{}, target interface{}) {
return
}

if err := json.Unmarshal(b, target); err != nil {
jsonDecode(opChain, b, target)
switch t := target.(type) {
case *interface{}:
*t = convertJSONNumberToFloatOrBigFloat(*t)
case *[]interface{}:
for i, val := range *t {
(*t)[i] = convertJSONNumberToFloatOrBigFloat(val)
}
case *map[string]interface{}:
for key, val := range *t {
(*t)[key] = convertJSONNumberToFloatOrBigFloat(val)
}
default:
v := reflect.ValueOf(t)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() == reflect.Struct {
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
if field.CanInterface() {
val := field.Interface()
newVal := convertJSONNumberToFloatOrBigFloat(val)
field.Set(reflect.ValueOf(newVal))
}
}
}
}
}

func jsonDecode(opChain *chain, b []byte, target interface{}) {
reader := bytes.NewReader(b)
dec := json.NewDecoder(reader)
dec.UseNumber()

for {
if err := dec.Decode(target); err == io.EOF || target == nil {
break
} else if err != nil {
opChain.fail(AssertionFailure{
Type: AssertValid,
Actual: &AssertionValue{target},
Errors: []error{
errors.New("expected: value can be decoded into target argument"),
},
})
return
}
}
}

func canonNumberDecode(opChain *chain, value big.Float, target interface{}) {
if target == nil {
opChain.fail(AssertionFailure{
Type: AssertValid,
Actual: &AssertionValue{target},
Type: AssertUsage,
Errors: []error{
errors.New("expected: value can be unmarshaled into target argument"),
errors.New("unexpected nil target argument"),
},
})
return
}
t := reflect.Indirect(reflect.ValueOf(target)).Kind()
switch t {
case reflect.Float64, reflect.Interface:
f, _ := value.Float64()
canonDecode(opChain, f, target)
case reflect.Float32:
f, _ := value.Float32()
canonDecode(opChain, f, target)
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
i, _ := value.Int64()
canonDecode(opChain, i, target)
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
i, _ := value.Int64()
canonDecode(opChain, i, target)
case reflect.Invalid,
reflect.Bool,
reflect.Uintptr,
reflect.Complex64,
reflect.Complex128,
reflect.Array,
reflect.Chan,
reflect.Func,
reflect.Map,
reflect.Slice,
reflect.String,
reflect.Struct,
reflect.Ptr,
reflect.UnsafePointer:
canonDecode(opChain, value, target)
default:
canonDecode(opChain, value, target)
}
}

func convertJSONNumberToFloatOrBigFloat(data interface{}) interface{} {
v := reflect.ValueOf(data)
switch v.Kind() {
case reflect.Map:
for _, key := range v.MapKeys() {
val := v.MapIndex(key)
if val.IsNil() {
continue
}
newVal := convertJSONNumberToFloatOrBigFloat(val.Interface())
v.SetMapIndex(key, reflect.ValueOf(newVal))
}
return v.Interface()
case reflect.Slice, reflect.Array:
for i := 0; i < v.Len(); i++ {
if v.Index(i).IsNil() {
continue
}
newVal := convertJSONNumberToFloatOrBigFloat(v.Index(i).Interface())
v.Index(i).Set(reflect.ValueOf(newVal))
}
return v.Interface()
case reflect.Ptr:
if v.IsNil() {
return nil
}
newVal := convertJSONNumberToFloatOrBigFloat(v.Elem().Interface())
newV := reflect.New(v.Type().Elem())
newV.Elem().Set(reflect.ValueOf(newVal))
return newV.Interface()
case reflect.Interface:
newVal := convertJSONNumberToFloatOrBigFloat(v.Elem().Interface())
return reflect.ValueOf(newVal).Interface()
case reflect.String:
if jsonNum, ok := v.Interface().(json.Number); ok {
if hasPrecisionLoss(jsonNum) {
newVal := big.NewFloat(0)
newVal, _ = newVal.SetString(v.String())
return newVal
}
newVal, _ := strconv.ParseFloat(v.String(), 64)
return newVal
}
return data
case reflect.Struct:
newVal := reflect.New(v.Type()).Elem()
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
if !field.CanInterface() {
continue
}
newField := convertJSONNumberToFloatOrBigFloat(field.Interface())
newVal.Field(i).Set(reflect.ValueOf(newField))
}
return newVal.Interface()
case reflect.Invalid,
reflect.Bool,
reflect.Uintptr,
reflect.Complex64,
reflect.Complex128,
reflect.Chan,
reflect.Func,
reflect.Int,
reflect.Int8,
reflect.Int16,
reflect.Int32,
reflect.Int64,
reflect.Uint,
reflect.Uint8,
reflect.Uint16,
reflect.Uint32,
reflect.Uint64,
reflect.Float32,
reflect.Float64,
reflect.UnsafePointer:
return data
default:
return data
}
}
7 changes: 4 additions & 3 deletions canon_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package httpexpect

import (
"math/big"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -18,19 +19,19 @@ func TestCanon_Number(t *testing.T) {
{
name: "input is int",
in: 123,
out: 123.0,
out: big.NewFloat(123.0),
result: success,
},
{
name: "input is float",
in: 123.0,
out: 123.0,
out: big.NewFloat(123.0),
result: success,
},
{
name: "input is myInt",
in: myInt(123),
out: 123.0,
out: big.NewFloat(123.0),
result: success,
},
{
Expand Down
2 changes: 1 addition & 1 deletion expect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ func TestExpect_Traverse(t *testing.T) {

m.ContainsKey("aaa")
m.ContainsKey("bbb")
m.ContainsKey("aaa")
m.ContainsKey("ccc")

m.HasValue("aaa", data["aaa"])
m.HasValue("bbb", data["bbb"])
Expand Down
Loading

0 comments on commit cbfbf03

Please sign in to comment.