From b62303bd5661047bf9a1a6180e19550b33aa38a0 Mon Sep 17 00:00:00 2001 From: James Waters Date: Thu, 23 May 2024 10:43:19 +0000 Subject: [PATCH 1/3] Add deepobject tests for more complex types Specifically, types that there are special edge-cases for --- deepobject_test.go | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/deepobject_test.go b/deepobject_test.go index 237673ad..39dc207f 100644 --- a/deepobject_test.go +++ b/deepobject_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/oapi-codegen/runtime/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -17,6 +18,7 @@ type InnerObject struct { // These are all possible field types, mandatory and optional. type AllFields struct { + // Primitive types I int `json:"i"` Oi *int `json:"oi,omitempty"` F float32 `json:"f"` @@ -27,10 +29,16 @@ type AllFields struct { Oas *[]string `json:"oas,omitempty"` O InnerObject `json:"o"` Oo *InnerObject `json:"oo,omitempty"` - D MockBinder `json:"d"` - Od *MockBinder `json:"od,omitempty"` M map[string]int `json:"m"` Om *map[string]int `json:"om,omitempty"` + + // Complex types + Bi MockBinder `json:"bi"` + Obi *MockBinder `json:"obi,omitempty"` + Da types.Date `json:"da"` + Oda *types.Date `json:"oda,omitempty"` + Ti time.Time `json:"ti"` + Oti *time.Time `json:"oti,omitempty"` } func TestDeepObject(t *testing.T) { @@ -45,9 +53,13 @@ func TestDeepObject(t *testing.T) { om := map[string]int{ "additional": 1, } - d := MockBinder{Time: time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC)} + + bi := MockBinder{Time: time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC)} + da := types.Date{Time: time.Date(2020, 2, 2, 0, 0, 0, 0, time.UTC)} + ti := time.Now().UTC() srcObj := AllFields{ + // Primitive types I: 12, Oi: &oi, F: 4.2, @@ -61,10 +73,16 @@ func TestDeepObject(t *testing.T) { ID: 456, }, Oo: &oo, - D: d, - Od: &d, M: om, Om: &om, + + // Complex types + Bi: bi, + Obi: &bi, + Da: da, + Oda: &da, + Ti: ti, + Oti: &ti, } marshaled, err := MarshalDeepObject(srcObj, "p") From daeb3b943338a8371e906dfa269a5fb5772ffa65 Mon Sep 17 00:00:00 2001 From: James Waters Date: Thu, 23 May 2024 12:03:54 +0000 Subject: [PATCH 2/3] Add deepobject support for test unmarshalers --- deepobject.go | 13 +++++++++++++ deepobject_test.go | 6 ++++++ 2 files changed, 19 insertions(+) diff --git a/deepobject.go b/deepobject.go index 7ec2f027..19128196 100644 --- a/deepobject.go +++ b/deepobject.go @@ -1,6 +1,7 @@ package runtime import ( + "encoding" "encoding/json" "errors" "fmt" @@ -199,6 +200,18 @@ func assignPathValues(dst interface{}, pathValues fieldOrValue) error { iv := reflect.Indirect(v) it := iv.Type() + switch dst := v.Interface().(type) { + case Binder: + return dst.Bind(pathValues.value) + case encoding.TextUnmarshaler: + err := dst.UnmarshalText([]byte(pathValues.value)) + if err != nil { + return fmt.Errorf("error unmarshalling text '%s': %w", pathValues.value, err) + } + + return nil + } + switch it.Kind() { case reflect.Map: dstMap := reflect.MakeMap(iv.Type()) diff --git a/deepobject_test.go b/deepobject_test.go index 39dc207f..55bc9d32 100644 --- a/deepobject_test.go +++ b/deepobject_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/google/uuid" "github.com/oapi-codegen/runtime/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -39,6 +40,8 @@ type AllFields struct { Oda *types.Date `json:"oda,omitempty"` Ti time.Time `json:"ti"` Oti *time.Time `json:"oti,omitempty"` + U types.UUID `json:"u"` + Ou *types.UUID `json:"ou,omitempty"` } func TestDeepObject(t *testing.T) { @@ -57,6 +60,7 @@ func TestDeepObject(t *testing.T) { bi := MockBinder{Time: time.Date(2020, 2, 1, 0, 0, 0, 0, time.UTC)} da := types.Date{Time: time.Date(2020, 2, 2, 0, 0, 0, 0, time.UTC)} ti := time.Now().UTC() + u := uuid.New() srcObj := AllFields{ // Primitive types @@ -83,6 +87,8 @@ func TestDeepObject(t *testing.T) { Oda: &da, Ti: ti, Oti: &ti, + U: u, + Ou: &u, } marshaled, err := MarshalDeepObject(srcObj, "p") From e546c4f233328ff7dce5e960b5ecfcb6606237a1 Mon Sep 17 00:00:00 2001 From: James Waters Date: Thu, 23 May 2024 14:12:22 +0000 Subject: [PATCH 3/3] Make deepobject support nullable values --- deepobject.go | 47 +++++++++++++++++++++++++++++++++++++++++++++- deepobject_test.go | 17 +++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/deepobject.go b/deepobject.go index 19128196..de59e504 100644 --- a/deepobject.go +++ b/deepobject.go @@ -15,6 +15,11 @@ import ( "github.com/oapi-codegen/runtime/types" ) +type nullableLike interface { + SetNull() + UnmarshalJSON(data []byte) error +} + func marshalDeepObject(in interface{}, path []string) ([]string, error) { var result []string @@ -55,8 +60,16 @@ func marshalDeepObject(in interface{}, path []string) ([]string, error) { // into a deepObject style set of subscripts. [a, b, c] turns into // [a][b][c] prefix := "[" + strings.Join(path, "][") + "]" + + var value string + if t == nil { + value = "null" + } else { + value = fmt.Sprintf("%v", t) + } + result = []string{ - prefix + fmt.Sprintf("=%v", t), + prefix + fmt.Sprintf("=%s", value), } } return result, nil @@ -214,6 +227,38 @@ func assignPathValues(dst interface{}, pathValues fieldOrValue) error { switch it.Kind() { case reflect.Map: + // If the value looks like nullable.Nullable[T], we need to handle it properly. + if dst, ok := dst.(nullableLike); ok { + if pathValues.value == "null" { + dst.SetNull() + + return nil + } + + // We create a new empty value, who's type is the same as the + // 'T' in nullable.Nullable[T]. Because of how nullable.Nullable is + // implemented, we can do that by getting the type's element type. + data := reflect.New(it.Elem()).Interface() + + // We now try to assign the path values to the new type. + if err := assignPathValues(data, pathValues); err != nil { + return err + } + + // We'll marshal the data so that we can unmarshal it into + // the original nullable.Nullable value. + dataBytes, err := json.Marshal(data) + if err != nil { + return err + } + + if err := dst.UnmarshalJSON(dataBytes); err != nil { + return err + } + + return nil + } + dstMap := reflect.MakeMap(iv.Type()) for key, value := range pathValues.fields { dstKey := reflect.ValueOf(key) diff --git a/deepobject_test.go b/deepobject_test.go index 55bc9d32..f460dd1a 100644 --- a/deepobject_test.go +++ b/deepobject_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/google/uuid" + "github.com/oapi-codegen/nullable" "github.com/oapi-codegen/runtime/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -42,6 +43,13 @@ type AllFields struct { Oti *time.Time `json:"oti,omitempty"` U types.UUID `json:"u"` Ou *types.UUID `json:"ou,omitempty"` + + // Nullable + NiSet nullable.Nullable[int] `json:"ni_set,omitempty"` + NiNull nullable.Nullable[int] `json:"ni_null,omitempty"` + NiUnset nullable.Nullable[int] `json:"ni_unset,omitempty"` + No nullable.Nullable[InnerObject] `json:"no,omitempty"` + Nu nullable.Nullable[uuid.UUID] `json:"nu,omitempty"` } func TestDeepObject(t *testing.T) { @@ -89,6 +97,15 @@ func TestDeepObject(t *testing.T) { Oti: &ti, U: u, Ou: &u, + + // Nullable + NiSet: nullable.NewNullableWithValue(5), + NiNull: nullable.NewNullNullable[int](), + No: nullable.NewNullableWithValue(InnerObject{ + Name: "John Smith", + ID: 456, + }), + Nu: nullable.NewNullableWithValue(uuid.New()), } marshaled, err := MarshalDeepObject(srcObj, "p")