Skip to content

Improve deepobject unmarshalling to support nullable.Nullable and encode.TextUnmarshaler #45

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion deepobject.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package runtime

import (
"encoding"
"encoding/json"
"errors"
"fmt"
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
51 changes: 46 additions & 5 deletions deepobject_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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"`
Expand All @@ -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) {
Expand All @@ -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,
Expand All @@ -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")
Expand Down