diff --git a/error.go b/error.go index 45f89f1..ffa7ebd 100644 --- a/error.go +++ b/error.go @@ -30,6 +30,9 @@ const ( // ErequiredType means the type mismatch against user required one. // For example M() requires map, A() requires array. ErequiredType + + // EinvalidValue means proxy can't treat the value. + EinvalidValue ) func (et ErrorType) String() string { @@ -48,6 +51,8 @@ func (et ErrorType) String() string { return "EinvalidQuery" case ErequiredType: return "ErequiredType" + case EinvalidValue: + return "EinvalidValue" default: return "Eunknown" } @@ -205,6 +210,9 @@ func (p *errorProxy) Error() string { case ErequiredType: return fmt.Sprintf("not required types: required=%s actual=%s: %s", p.expected.String(), p.actual.String(), p.FullAddress()) + case EinvalidValue: + // FIXME: better error message. + return fmt.Sprintf("invalid value: %s: %s", p.infoStr, p.FullAddress()) default: return fmt.Sprintf("unexpected: %s", p.FullAddress()) } diff --git a/go.mod b/go.mod index d34f7be..358c38d 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/koron/go-dproxy -go 1.13 +go 1.21 + +require github.com/google/go-cmp v0.7.0 diff --git a/go.sum b/go.sum index e69de29..40e761a 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= diff --git a/package_test.go b/package_test.go index 88ba1f7..59fa462 100644 --- a/package_test.go +++ b/package_test.go @@ -1,16 +1,15 @@ package dproxy import ( - "fmt" - "reflect" "testing" + + "github.com/google/go-cmp/cmp" ) -func assertEquals(t *testing.T, actual, expected interface{}, format string, a ...interface{}) { +func assertEquals(t *testing.T, want, got any) { t.Helper() - if !reflect.DeepEqual(actual, expected) { - msg := fmt.Sprintf(format, a...) - t.Errorf("not equal: %s\nexpect=%+v\nactual=%+v", msg, expected, actual) + if d := cmp.Diff(want, got); d != "" { + t.Errorf("not equal -want +got\n%s", d) } } diff --git a/pointer_test.go b/pointer_test.go index db0069e..87a0739 100644 --- a/pointer_test.go +++ b/pointer_test.go @@ -33,13 +33,14 @@ func TestPointerInvalidQuery(t *testing.T) { func TestPointer(t *testing.T) { f := func(q string, d, expected interface{}) { + t.Helper() p := Pointer(d, q) v, err := p.Value() if err != nil { t.Errorf("Pointer:%q for %+v failed: %s", q, d, err) return } - assertEquals(t, v, expected, "Pointer:%q for %+v", q, d) + assertEquals(t, expected, v) } v := parseJSON(`{ diff --git a/reflect.go b/reflect.go new file mode 100644 index 0000000..820f571 --- /dev/null +++ b/reflect.go @@ -0,0 +1,211 @@ +package dproxy + +import ( + "fmt" + "reflect" + "strconv" +) + +// reflectProxy is a proxy using reflect. +type reflectProxy struct { + rv reflect.Value + p frame // parent + l string // label +} + +var _ Proxy = (*reflectProxy)(nil) + +// NewReflect creates a new proxy with reflect. +func NewReflect(v interface{}) Proxy { + return newReflectProxy(v, nil, "") +} + +func newReflectProxy(v interface{}, parent frame, label string) Proxy { + rv := reflect.ValueOf(v) + for rv.Kind() == reflect.Ptr { + rv = rv.Elem() + if !rv.IsValid() { + return &errorProxy{ + errorType: EinvalidValue, + parent: parent, + label: label, + infoStr: fmt.Sprintf("%T", v), + } + } + } + return &reflectProxy{rv: rv, p: parent, l: label} +} + +func (rp *reflectProxy) parentFrame() frame { + return rp.p +} + +func (rp *reflectProxy) frameLabel() string { + return rp.l +} + +func (rp *reflectProxy) typeError(expected Type) *errorProxy { + return typeError(rp, expected, rp.rv.Interface()) +} + +func (rp *reflectProxy) Nil() bool { + return !rp.rv.IsValid() +} + +func (rp *reflectProxy) Value() (interface{}, error) { + return rp.rv.Interface(), nil +} + +func (rp *reflectProxy) Bool() (bool, error) { + switch rp.rv.Kind() { + case reflect.Bool: + return rp.rv.Bool(), nil + default: + return false, rp.typeError(Tbool) + } +} + +func (rp *reflectProxy) isInt() bool { + switch rp.rv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return true + default: + return false + } +} + +func (rp *reflectProxy) Int64() (int64, error) { + if !rp.isInt() { + return 0, rp.typeError(Tint64) + } + return rp.rv.Int(), nil +} + +func (rp *reflectProxy) isFloat() bool { + switch rp.rv.Kind() { + case reflect.Float32, reflect.Float64: + return true + default: + return false + } +} + +func (rp *reflectProxy) Float64() (float64, error) { + if !rp.isFloat() { + return 0, rp.typeError(Tfloat64) + } + return rp.rv.Float(), nil +} + +func (rp *reflectProxy) String() (string, error) { + if rp.rv.Kind() != reflect.String { + return "", rp.typeError(Tstring) + } + return rp.rv.String(), nil +} + +func (rp *reflectProxy) Array() ([]interface{}, error) { + if !rp.isArray() { + return nil, rp.typeError(Tarray) + } + if v, ok := rp.rv.Interface().([]interface{}); ok { + return v, nil + } + v := make([]interface{}, rp.rv.Len()) + for i := range v { + v[i] = rp.rv.Index(i).Interface() + } + return v, nil +} + +func (rp *reflectProxy) Map() (map[string]interface{}, error) { + if rp.rv.Kind() != reflect.Map { + return nil, rp.typeError(Tmap) + } + if v, ok := rp.rv.Interface().(map[string]interface{}); ok { + return v, nil + } + v := map[string]interface{}{} + for _, k := range rp.rv.MapKeys() { + v[k.String()] = rp.rv.MapIndex(k) + } + return v, nil +} + +func (rp *reflectProxy) isArray() bool { + switch rp.rv.Kind() { + case reflect.Array, reflect.Slice: + return true + default: + return false + } +} + +func (rp *reflectProxy) A(n int) Proxy { + if !rp.isArray() { + return rp.typeError(Tarray) + } + adrs := "[" + strconv.Itoa(n) + "]" + if n < 0 || n >= rp.rv.Len() { + return notfoundError(rp, adrs) + } + v := rp.rv.Index(n) + return newReflectProxy(v.Interface(), rp, adrs) +} + +func (rp *reflectProxy) M(k string) Proxy { + adrs := "." + k + switch rp.rv.Kind() { + case reflect.Map: + v := rp.rv.MapIndex(reflect.ValueOf(k)) + if !v.IsValid() { + return notfoundError(rp, adrs) + } + return newReflectProxy(v.Interface(), rp, adrs) + case reflect.Struct: + v := rp.rv.FieldByName(k) + if !v.IsValid() { + return notfoundError(rp, adrs) + } + return newReflectProxy(v.Interface(), rp, adrs) + default: + return rp.typeError(Tmap) + } +} + +func (rp *reflectProxy) P(q string) Proxy { + return pointer(rp, q) +} + +func (rp *reflectProxy) ProxySet() ProxySet { + // TODO: return proxy set for reflect vaue. + return nil +} + +func (rp *reflectProxy) Q(k string) ProxySet { + // TODO: return proxy set for queried value + return nil +} + +func (rp *reflectProxy) findJPT(t string) Proxy { + switch rp.rv.Kind() { + case reflect.Map, reflect.Struct: + return rp.M(t) + case reflect.Array, reflect.Slice: + n, err := strconv.ParseUint(t, 10, 0) + if err != nil { + return &errorProxy{ + errorType: EinvalidIndex, + parent: rp, + infoStr: err.Error(), + } + } + return rp.A(int(n)) + default: + return &errorProxy{ + errorType: EmapNorArray, + parent: rp, + actual: detectType(rp.rv.Interface()), + } + } +} diff --git a/reflect_test.go b/reflect_test.go new file mode 100644 index 0000000..d7b5830 --- /dev/null +++ b/reflect_test.go @@ -0,0 +1,90 @@ +package dproxy + +import "testing" + +func TestReflect_Readme(t *testing.T) { + v := parseJSON(`{ + "cities": [ "tokyo", 100, "osaka", 200, "hakata", 300 ], + "data": { + "custom": [ "male", 21, "female", 22 ] + } + }`) + + s, err := NewReflect(v).M("cities").A(0).String() + if s != "tokyo" { + t.Error("cities[0] must be \"tokyo\":", err) + } + + _, err = NewReflect(v).M("cities").A(0).Float64() + if err == nil { + t.Error("cities[0] (float64) must be failed:", err) + } + + n, err := NewReflect(v).M("cities").A(1).Float64() + if n != 100 { + t.Error("cities[1] must be 100:", err) + } + + s2, err := NewReflect(v).M("data").M("custom").A(2).String() + if s2 != "female" { + t.Error("data.custom[2] must be \"female\":", err) + } + + _, err = NewReflect(v).M("data").M("kustom").String() + if err == nil || err.Error() != "not found: data.kustom" { + t.Error("err is not \"not found: data.kustom\":", err) + } +} + +func TestReflect_MapBool(t *testing.T) { + v := parseJSON(`{ + "foo": true, + "bar": false + }`) + + // check "foo" + foo, err := NewReflect(v).M("foo").Bool() + if err != nil { + t.Error(err) + } else if foo != true { + t.Errorf("foo must be true") + } + + // check "bar" + bar, err := NewReflect(v).M("bar").Bool() + if err != nil { + t.Error(err) + } else if bar != false { + t.Errorf("bar must be false") + } +} + +func TestReflectMisc(t *testing.T) { + t.Run("nil", func(t *testing.T) { + if NewReflect(nil).Nil() == false { + t.Error("nil.Nil() should true but false") + } + if NewReflect("foo").Nil() == true { + t.Error("string.Nil() should false but true") + } + }) +} + +func TestReflect_Array(t *testing.T) { + got1, err := NewReflect(parseJSON(`{ + "cities": [ "tokyo", 100, "osaka", 200, "hakata", 300 ] + }`)).M("cities").Array() + if err != nil { + t.Fatalf("not array: %s", err) + } + assertEquals(t, []any{"tokyo", 100.0, "osaka", 200.0, "hakata", 300.0}, got1) + + got2, err := NewReflect([]string{"foo", "bar", "baz"}).Array() + if err != nil { + t.Fatalf("not array: %s", err) + } + assertEquals(t, []any{"foo", "bar", "baz"}, got2) + + _, err = NewReflect(0).Array() + assertError(t, err, "not matched types: expected=array actual=int64: (root)") +}