Skip to content
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

Add ForceSendFields and a custom Marshaller #615

Merged
merged 26 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4b0ed4e
Create custom marshaller
hectorcast-db Sep 7, 2023
f6377d9
Delete file commited by mistake
hectorcast-db Sep 13, 2023
7594c7a
Remove slices package usage for compatiblity with older golang versions
hectorcast-db Sep 13, 2023
20732bd
Implement non omit empty fields
hectorcast-db Sep 13, 2023
4840554
Remove useless log
hectorcast-db Sep 13, 2023
3aa4e5b
Fix PR comments
hectorcast-db Sep 14, 2023
ea0abcc
Remove unnecesary retype
hectorcast-db Sep 14, 2023
a2d31f0
Update template
hectorcast-db Sep 14, 2023
421092b
Address PR comments
hectorcast-db Sep 15, 2023
d2cec31
Add ForceSendFields only to structs with fields which the omitempty j…
hectorcast-db Sep 15, 2023
e705c43
More PR comments
hectorcast-db Sep 22, 2023
eb90608
Merge remote-tracking branch 'origin/main' into custom-marshaller-test
hectorcast-db Sep 22, 2023
37407aa
Fix comments
hectorcast-db Sep 22, 2023
084272a
Cache reflect values
hectorcast-db Sep 22, 2023
611178a
Cache tag parsing
hectorcast-db Sep 22, 2023
e8d4dbd
Simplify tag caching
hectorcast-db Sep 25, 2023
49150a8
Use thread lock on reads
hectorcast-db Sep 25, 2023
22d5c4f
Unlock by defer to ensure unlocking
hectorcast-db Sep 25, 2023
e9a35c2
Merge remote-tracking branch 'origin/main' into custom-marshaller-test
hectorcast-db Sep 29, 2023
72e9718
Update openapi sha
hectorcast-db Sep 29, 2023
168f324
Missing file
hectorcast-db Sep 29, 2023
c4b5b3f
Merge remote-tracking branch 'origin/main' into custom-marshaller-test
hectorcast-db Oct 4, 2023
9480c80
Fmt
hectorcast-db Oct 4, 2023
d55c909
Improve tests
hectorcast-db Oct 11, 2023
30f09af
Merge branch 'main' into custom-marshaller-test
hectorcast-db Oct 11, 2023
baffd4d
Update comment in marshal/marshal.go
hectorcast-db Oct 11, 2023
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
13 changes: 13 additions & 0 deletions .codegen/model.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
{{range .ImportedPackages}}
"github.com/databricks/databricks-sdk-go/service/{{.}}"{{end}}
"io"
"github.com/databricks/databricks-sdk-go/marshal"
)

// all definitions in this file are in alphabetical order
Expand All @@ -15,7 +16,19 @@ type {{.PascalName}} struct {
{{- range .Fields}}
{{.Comment " // " 80}}
{{.PascalName}} {{if .IsOptionalObject}}*{{end}}{{template "type" .Entity}} `{{template "field-tag" . }}`{{end}}

{{if .ShouldIncludeForceSendFields}} ForceSendFields []string `json:"-"` {{end}}
}

{{if .ShouldIncludeForceSendFields}}
func (s *{{.PascalName}}) UnmarshalJSON(b []byte) error {
return marshal.Unmarshal(b, s)
}

func (s {{.PascalName}}) MarshalJSON() ([]byte, error) {
hectorcast-db marked this conversation as resolved.
Show resolved Hide resolved
return marshal.Marshal(s)
}
{{end}}
{{else if .MapValue}}{{.Comment "// " 80}}
type {{.PascalName}} {{template "type" .}}
{{else if .Enum}}{{.Comment "// " 80}}
Expand Down
171 changes: 171 additions & 0 deletions marshal/composite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package marshal

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"
)

type CompositeParent struct {
Field1 int `json:"field1,omitempty"`

ForceSendFields []string `json:"-"`
}

type CompositeSecondParent struct {
Field3 int `json:"field3,omitempty"`

ForceSendFields []string `json:"-"`
}

func (s *CompositeSecondParent) UnmarshalJSON(b []byte) error {
return Unmarshal(b, s)
}

func (s CompositeSecondParent) MarshalJSON() ([]byte, error) {
return Marshal(s)
}

func (s *CompositeParent) UnmarshalJSON(b []byte) error {
return Unmarshal(b, s)
}

func (s CompositeParent) MarshalJSON() ([]byte, error) {
return Marshal(s)
}

type compositeChild struct {
CompositeParent
*CompositeSecondParent
Field2 int `json:"field2,omitempty"`
}

func (s compositeChild) MarshalJSON() ([]byte, error) {
return Marshal(s)
}

func (s *compositeChild) UnmarshalJSON(b []byte) error {
return Unmarshal(b, s)
}

func TestComposite(t *testing.T) {
executeCompositeMarshalTest(
t, compositeTest{
st: compositeChild{
Field2: 2,
CompositeParent: CompositeParent{
Field1: 1,
ForceSendFields: []string{"Field1"},
},
CompositeSecondParent: &CompositeSecondParent{
Field3: 3,
ForceSendFields: []string{"Field3"},
},
},
jsonString: `{"field1":1, "field2":2, "field3":3}`,
matchClassic: true,
matchUnmarshal: true,
},
)

}

func TestCompositeNil(t *testing.T) {
element := compositeChild{
Field2: 2,
CompositeParent: CompositeParent{
Field1: 1,
ForceSendFields: []string{"Field1"},
},
}
result := executeCompositeMarshalTest(
t, compositeTest{
st: element,
jsonString: `{"field1":1, "field2":2}`,
matchClassic: true,
//CompositeSecondParent will be present due to limitations of the unmarshalling
matchUnmarshal: false,
},
)
element.CompositeSecondParent = &CompositeSecondParent{}
assert.Equal(t, element, result)

}

func TestCompositeDefault(t *testing.T) {
executeCompositeMarshalTest(
t, compositeTest{
st: compositeChild{
Field2: 0,
CompositeParent: CompositeParent{
Field1: 0,
ForceSendFields: []string{"Field1"},
},
CompositeSecondParent: &CompositeSecondParent{
Field3: 0,
ForceSendFields: []string{"Field3"},
},
},
jsonString: `{"field1":0, "field3":0}`,
matchClassic: true,
matchUnmarshal: true,
},
)
}

type compositeTest struct {
st compositeChild
jsonString string
// Compare marshal results with normal marshal
matchClassic bool
// Unmarshal may not match, since ForceSendFields will be populated during
// custom unmarshal process
matchUnmarshal bool
}

func executeCompositeMarshalTest(t *testing.T, tc compositeTest) compositeChild {
// Convert to JSON
res, err := json.Marshal(tc.st)
assert.NoError(t, err, "error while executing custom marshal")
compareJSON(t, tc.jsonString, string(res))

var reconstruct compositeChild
err = json.Unmarshal(res, &reconstruct)
assert.NoError(t, err, "error while unmarshaling")
if tc.matchUnmarshal {
assert.Equal(t, tc.st, reconstruct)
}

return reconstruct
}

type noSendFieldChild struct {
*CompositeSecondParent
Field2 int `json:"field2"`
}

func (s *noSendFieldChild) UnmarshalJSON(b []byte) error {
return Unmarshal(b, s)
}

func (s noSendFieldChild) MarshalJSON() ([]byte, error) {
return Marshal(s)
}

func TestNoSendFieldChild(t *testing.T) {
st := noSendFieldChild{
Field2: 2,
}

res, err := json.Marshal(st)
assert.NoError(t, err, "error while executing custom marshal")
compareJSON(t, `{"field2":2}`, string(res))

var reconstruct noSendFieldChild
err = json.Unmarshal(res, &reconstruct)
assert.NoError(t, err, "error while unmarshaling")
st.CompositeSecondParent = &CompositeSecondParent{}
assert.Equal(t, st, reconstruct)

}
hectorcast-db marked this conversation as resolved.
Show resolved Hide resolved
187 changes: 187 additions & 0 deletions marshal/marshal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package marshal

import (
"encoding/json"
"fmt"
"reflect"

"golang.org/x/exp/maps"
)

const forceSendFieldName = "ForceSendFields"

// Marshal returns a JSON encoding of the given object. Included fields:
hectorcast-db marked this conversation as resolved.
Show resolved Hide resolved
// - non-empty value
// - a basic type whose field's name is present in forceSendFields
// Embedded structs are still considered a separate struct. ForceSendFields
// in an embedded struct only impact the fields of the embedded struct.
// Conversely, an embedded struct is not impacted by the ForceSendFields
// of the struct containing it.
func Marshal(object any) ([]byte, error) {
hectorcast-db marked this conversation as resolved.
Show resolved Hide resolved
dataMap, err := structAsMap(object)
if err != nil {
return nil, err
}

return json.Marshal(dataMap)
}

// Converts the object to a map
func structAsMap(object any) (map[string]any, error) {
result := make(map[string]any)

// If the object is nil or a pointer to nil, don't do anything
if object == nil || (reflect.ValueOf(object).Kind() == reflect.Ptr && reflect.ValueOf(object).IsNil()) {
return result, nil
}

value := reflect.ValueOf(object)
value = reflect.Indirect(value)
objectType := value.Type()

includeFields, err := getForceSendFields(object, getTypeName(objectType))

if err != nil {
return nil, err
}

for _, field := range getTypeFields(objectType) {
if field.JsonTag.ignore {
continue
}
hectorcast-db marked this conversation as resolved.
Show resolved Hide resolved
fieldIndex := field.IndexInStruct
tag := field.JsonTag

fieldValue := value.Field(fieldIndex)
hectorcast-db marked this conversation as resolved.
Show resolved Hide resolved

// Anonymous fields should be marshalled using the same JSON, and then merged into the same map
if field.Anonymous && fieldValue.IsValid() {
anonymousFieldResult, err := structAsMap(fieldValue.Interface())
if err != nil {
return nil, err
}
result = mergeMaps(anonymousFieldResult, result)
continue
}

// Skip fields which should not be included
if !includeField(tag, fieldValue, includeFields[field.Name]) {
continue
}

if tag.asString {
result[tag.name] = formatAsString(fieldValue, field.Type.Kind())
} else {
result[tag.name] = fieldValue.Interface()
}
}
return result, nil
}

// returns the element as string
func formatAsString(v reflect.Value, kind reflect.Kind) string {
if kind == reflect.Ptr && !v.IsNil() {
v = v.Elem()
}

return fmt.Sprintf("%v", v.Interface())
}

type jsonTag struct {
name string
asString bool
ignore bool
omitempty bool
}

// Determines whether a field should be included or not
func includeField(tag jsonTag, value reflect.Value, forceSend bool) bool {
if tag.ignore {
return false
}
if !tag.omitempty {
return true
}
return (isBasicType(value.Type()) && forceSend) || !isEmptyValue(value)
}

// isEmptyValue returns whether v is the empty value for its type.
// This implementation is based on on the encoding/json package for consistency on the results
// https://github.com/golang/go/blob/a278550c40ef3f01a5fcbef43414dc49009201f8/src/encoding/json/encode.go#L306
func isEmptyValue(v reflect.Value) bool {
hectorcast-db marked this conversation as resolved.
Show resolved Hide resolved
switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Pointer:
return v.IsNil()
}
return false
}

// Merges two maps. Overrides duplicated elements, which is fine because json.Unmarshal also
// does it, and a JSON should not have duplicated entries.
func mergeMaps(m1 map[string]any, m2 map[string]any) map[string]any {
merged := make(map[string]any)
maps.Copy(merged, m1)
hectorcast-db marked this conversation as resolved.
Show resolved Hide resolved
maps.Copy(merged, m2)
return merged
}

func getForceSendFields(v any, structName string) (map[string]bool, error) {
// reflect.GetFieldByName panics if the field is inside a null anonymous field
field := getFieldByName(v, forceSendFieldName)
if !field.IsValid() {
return nil, nil
}
forceSendFields, ok := field.Interface().([]string)
if !ok {
return nil, fmt.Errorf("invalid type for %s field", forceSendFieldName)
}
existingFields := getFieldNames(v)
includeFields := make(map[string]bool)
for _, field := range forceSendFields {
if _, ok := existingFields[field]; ok {
includeFields[field] = true
} else {
return nil, fmt.Errorf("field %s cannot be found in struct %s", field, structName)
}
}

return includeFields, nil

}

func getFieldByName(v any, fieldName string) reflect.Value {
value := reflect.ValueOf(v)
value = reflect.Indirect(value)
objectType := value.Type()

for _, field := range getTypeFields(objectType) {
name := field.Name
if name == fieldName {
return value.Field(field.IndexInStruct)
}
hectorcast-db marked this conversation as resolved.
Show resolved Hide resolved
}
return reflect.Value{}
}

func getFieldNames(v any) map[string]bool {
result := map[string]bool{}
value := reflect.ValueOf(v)
value = reflect.Indirect(value)
objectType := value.Type()

for _, field := range getTypeFields(objectType) {
name := field.Name
result[name] = true
}
return result
}
Loading
Loading