diff --git a/deepobject.go b/deepobject.go index 7ec2f02..de59e50 100644 --- a/deepobject.go +++ b/deepobject.go @@ -1,6 +1,7 @@ package runtime import ( + "encoding" "encoding/json" "errors" "fmt" @@ -14,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 @@ -54,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 @@ -199,8 +213,52 @@ 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: + // 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 237673a..f460dd1 100644 --- a/deepobject_test.go +++ b/deepobject_test.go @@ -6,6 +6,9 @@ import ( "testing" "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" ) @@ -17,6 +20,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 +31,25 @@ 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"` + 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) { @@ -45,9 +64,14 @@ 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() + u := uuid.New() srcObj := AllFields{ + // Primitive types I: 12, Oi: &oi, F: 4.2, @@ -61,10 +85,27 @@ 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, + 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")