Skip to content

Commit

Permalink
Merge pull request #468 from natdm/parseboolptr
Browse files Browse the repository at this point in the history
Parse zero-val pointers
  • Loading branch information
brandur-stripe authored Oct 3, 2017
2 parents 1490ab1 + 516bdf1 commit f5fbde5
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 54 deletions.
112 changes: 61 additions & 51 deletions form/form.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@ type Appender interface {
AppendTo(values *Values, keyParts []string)
}

type encoderFunc func(values *Values, v reflect.Value, keyParts []string, options *formOptions)
// encoderFunc is used to encode any type from a request.
//
// A note about encodeZero:
// Since some types in the Stripe API are defaulted to non-zero values, and Go defaults types to their zero values,
// any type that has a Stripe API default of a non-zero value is defined as a go pointer, meaning nil defaults to
// the Stripe API non-zero value. To override this, a check is made to see if the value is the zero-value for that
// type. If it is, and encodeZero is true, it's encoded. This is ignored as a parameter when dealing with types
// like structs, where the decision can not be made preemptivelys.
type encoderFunc func(values *Values, v reflect.Value, keyParts []string, encodeZero bool, options *formOptions)

// field represents a single field found in a struct. It caches information
// about that field so that we can make encoding faster.
Expand Down Expand Up @@ -67,7 +75,7 @@ type structEncoder struct {
fieldEncs []encoderFunc
}

func (se *structEncoder) encode(values *Values, v reflect.Value, keyParts []string, _ *formOptions) {
func (se *structEncoder) encode(values *Values, v reflect.Value, keyParts []string, _ bool, _ *formOptions) {
for i, f := range se.fields {
var fieldKeyParts []string
fieldV := v.Field(f.index)
Expand All @@ -83,7 +91,7 @@ func (se *structEncoder) encode(values *Values, v reflect.Value, keyParts []stri
fieldKeyParts = append(keyParts, f.formName)
}

se.fieldEncs[i](values, fieldV, fieldKeyParts, f.options)
se.fieldEncs[i](values, fieldV, fieldKeyParts, f.isPtr, f.options)
if f.isAppender && (!f.isPtr || !fieldV.IsNil()) {
fieldV.Interface().(Appender).AppendTo(values, fieldKeyParts)
}
Expand All @@ -109,7 +117,7 @@ var structCache struct {
// AppendTo uses reflection to form encode into the given values collection
// based off the form tags that it defines.
func AppendTo(values *Values, i interface{}) {
reflectValue(values, reflect.ValueOf(i), nil)
reflectValue(values, reflect.ValueOf(i), false, nil)
}

// AppendToPrefixed is the same as AppendTo, but it allows a slice of key parts
Expand All @@ -119,7 +127,7 @@ func AppendTo(values *Values, i interface{}) {
// for recipients. Recipients is going away, and when it does, we can probably
// remove it again.
func AppendToPrefixed(values *Values, i interface{}, keyParts []string) {
reflectValue(values, reflect.ValueOf(i), keyParts)
reflectValue(values, reflect.ValueOf(i), false, keyParts)
}

// FormatKey takes a series of key parts that may be parameter keyParts, map keys,
Expand All @@ -139,32 +147,31 @@ func FormatKey(parts []string) string {

// ---

func boolEncoder(values *Values, v reflect.Value, keyParts []string, options *formOptions) {
if !v.Bool() {
func boolEncoder(values *Values, v reflect.Value, keyParts []string, encodeZero bool, options *formOptions) {
val := v.Bool()
if !val && !encodeZero {
return
}

if options != nil {
if v.Bool() {
switch {
case options.Empty:
values.Add(FormatKey(keyParts), "")
case options.Invert:
values.Add(FormatKey(keyParts), strconv.FormatBool(false))
case options.Zero:
values.Add(FormatKey(keyParts), "0")
}
switch {
case options.Empty:
values.Add(FormatKey(keyParts), "")
case options.Invert:
values.Add(FormatKey(keyParts), strconv.FormatBool(false))
case options.Zero:
values.Add(FormatKey(keyParts), "0")
}
} else {
values.Add(FormatKey(keyParts), strconv.FormatBool(v.Bool()))
values.Add(FormatKey(keyParts), strconv.FormatBool(val))
}
}

func buildArrayOrSliceEncoder(t reflect.Type) encoderFunc {
// Gets an encoder for the type that the array or slice will hold
elemF := getCachedOrBuildTypeEncoder(t.Elem())

return func(values *Values, v reflect.Value, keyParts []string, options *formOptions) {
return func(values *Values, v reflect.Value, keyParts []string, _ bool, options *formOptions) {
// FormatKey automatically adds square brackets, so just pass an empty
// string into the breadcrumb trail
arrNames := append(keyParts, "")
Expand All @@ -178,7 +185,7 @@ func buildArrayOrSliceEncoder(t reflect.Type) encoderFunc {
}

indexV := v.Index(i)
elemF(values, indexV, arrNames, nil)
elemF(values, indexV, arrNames, indexV.Kind() == reflect.Ptr, nil)

if isAppender(indexV.Type()) && !indexV.IsNil() {
v.Interface().(Appender).AppendTo(values, arrNames)
Expand All @@ -191,11 +198,11 @@ func buildPtrEncoder(t reflect.Type) encoderFunc {
// Gets an encoder for the type that the pointer wraps
elemF := getCachedOrBuildTypeEncoder(t.Elem())

return func(values *Values, v reflect.Value, keyParts []string, options *formOptions) {
return func(values *Values, v reflect.Value, keyParts []string, _ bool, options *formOptions) {
if v.IsNil() {
return
}
elemF(values, v.Elem(), keyParts, options)
elemF(values, v.Elem(), keyParts, true, options)
}
}

Expand All @@ -204,18 +211,20 @@ func buildStructEncoder(t reflect.Type) encoderFunc {
return se.encode
}

func float32Encoder(values *Values, v reflect.Value, keyParts []string, _ *formOptions) {
if v.Float() == 0.0 {
func float32Encoder(values *Values, v reflect.Value, keyParts []string, encodeZero bool, options *formOptions) {
val := v.Float()
if val == 0.0 && !encodeZero {
return
}
values.Add(FormatKey(keyParts), strconv.FormatFloat(v.Float(), 'f', 4, 32))
values.Add(FormatKey(keyParts), strconv.FormatFloat(val, 'f', 4, 32))
}

func float64Encoder(values *Values, v reflect.Value, keyParts []string, _ *formOptions) {
if v.Float() == 0.0 {
func float64Encoder(values *Values, v reflect.Value, keyParts []string, encodeZero bool, options *formOptions) {
val := v.Float()
if val == 0.0 && !encodeZero {
return
}
values.Add(FormatKey(keyParts), strconv.FormatFloat(v.Float(), 'f', 4, 64))
values.Add(FormatKey(keyParts), strconv.FormatFloat(val, 'f', 4, 64))
}

func getCachedOrBuildStructEncoder(t reflect.Type) *structEncoder {
Expand Down Expand Up @@ -279,54 +288,57 @@ func getCachedOrBuildTypeEncoder(t reflect.Type) encoderFunc {
return f
}

func intEncoder(values *Values, v reflect.Value, keyParts []string, _ *formOptions) {
if v.Int() == 0 {
func intEncoder(values *Values, v reflect.Value, keyParts []string, encodeZero bool, options *formOptions) {
val := v.Int()
if val == 0 && !encodeZero {
return
}
values.Add(FormatKey(keyParts), strconv.FormatInt(v.Int(), 10))
values.Add(FormatKey(keyParts), strconv.FormatInt(val, 10))
}

func interfaceEncoder(values *Values, v reflect.Value, keyParts []string, _ *formOptions) {
func interfaceEncoder(values *Values, v reflect.Value, keyParts []string, _ bool, _ *formOptions) {
if v.IsNil() {
return
}
reflectValue(values, v.Elem(), keyParts)
reflectValue(values, v.Elem(), false, keyParts)
}

func isAppender(t reflect.Type) bool {
return t.Implements(reflect.TypeOf((*Appender)(nil)).Elem())
}

func mapEncoder(values *Values, v reflect.Value, keyParts []string, _ *formOptions) {
func mapEncoder(values *Values, v reflect.Value, keyParts []string, _ bool, _ *formOptions) {
for _, keyVal := range v.MapKeys() {
if Strict && keyVal.Kind() != reflect.String {
panic("Don't support serializing maps with non-string keys")
}

reflectValue(values, v.MapIndex(keyVal), append(keyParts, keyVal.String()))
reflectValue(values, v.MapIndex(keyVal), false, append(keyParts, keyVal.String()))
}
}

func stringEncoder(values *Values, v reflect.Value, keyParts []string, _ *formOptions) {
if v.String() == "" {
func stringEncoder(values *Values, v reflect.Value, keyParts []string, encodeZero bool, options *formOptions) {
val := v.String()
if val == "" && !encodeZero {
return
}
values.Add(FormatKey(keyParts), v.String())
values.Add(FormatKey(keyParts), val)
}

func uintEncoder(values *Values, v reflect.Value, keyParts []string, _ *formOptions) {
if v.Uint() == 0 {
func uintEncoder(values *Values, v reflect.Value, keyParts []string, encodeZero bool, options *formOptions) {
val := v.Uint()
if val == 0 && !encodeZero {
return
}
values.Add(FormatKey(keyParts), strconv.FormatUint(v.Uint(), 10))
values.Add(FormatKey(keyParts), strconv.FormatUint(val, 10))
}

func reflectValue(values *Values, v reflect.Value, keyParts []string) {
func reflectValue(values *Values, v reflect.Value, _ bool, keyParts []string) {
t := v.Type()

f := getCachedOrBuildTypeEncoder(t)
if f != nil {
f(values, v, keyParts, nil)
f(values, v, keyParts, v.Kind() == reflect.Ptr, nil)
}

if isAppender(t) {
Expand Down Expand Up @@ -358,30 +370,28 @@ func makeStructEncoder(t reflect.Type) *structEncoder {
continue
}

fldTyp := reflectField.Type
fldKind := fldTyp.Kind()

if Strict && options != nil &&
(options.Empty || options.Invert || options.Zero) &&
reflectField.Type.Kind() != reflect.Bool {
fldKind != reflect.Bool {

panic(fmt.Sprintf(
"Cannot specify `empty`, `invert`, or `zero` for non-boolean field; on: %s/%s",
t.Name(), reflectField.Name,
))
}

var isPtr bool
if reflectField.Type.Kind() == reflect.Ptr {
isPtr = true
}

se.fields = append(se.fields, &field{
formName: formName,
index: i,
isAppender: isAppender(reflectField.Type),
isPtr: isPtr,
isAppender: isAppender(fldTyp),
isPtr: fldKind == reflect.Ptr,
options: options,
})
se.fieldEncs = append(se.fieldEncs,
getCachedOrBuildTypeEncoder(reflectField.Type))
getCachedOrBuildTypeEncoder(fldTyp))
}

return se
Expand Down
48 changes: 45 additions & 3 deletions form/form_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,21 +129,30 @@ func BenchmarkAppendTo(b *testing.B) {
func TestAppendTo(t *testing.T) {
var arrayVal = [3]string{"1", "2", "3"}

var boolVal = true
var boolValT = true
var boolValF = false

var float32Val float32 = 1.2345
var float32Val0 float32

var float64Val = 1.2345
var float64Val0 = 0.0

var intVal = 123
var intVal0 = 0
var int8Val int8 = 123
var int8Val0 int8
var int16Val int16 = 123
var int16Val0 int16
var int32Val int32 = 123
var int32Val0 int32
var int64Val int64 = 123
var int64Val0 int64

var sliceVal = []string{"1", "2", "3"}

var stringVal = "123"
var stringVal0 = ""

var subStructVal = testSubStruct{
SubSubStruct: testSubSubStruct{
Expand All @@ -152,10 +161,15 @@ func TestAppendTo(t *testing.T) {
}

var uintVal uint = 123
var uintVal0 uint
var uint8Val uint8 = 123
var uint8Val0 uint8
var uint16Val uint16 = 123
var uint16Val0 uint16
var uint32Val uint32 = 123
var uint32Val0 uint32
var uint64Val uint64 = 123
var uint64Val0 uint64

testCases := []struct {
field string
Expand All @@ -166,27 +180,43 @@ func TestAppendTo(t *testing.T) {

{"array_indexed[2]", &testStruct{ArrayIndexed: arrayVal}, "3"},

{"bool", &testStruct{Bool: boolVal}, "true"},
{"bool_ptr", &testStruct{BoolPtr: &boolVal}, "true"},
{"bool", &testStruct{Bool: boolValT}, "true"},
{"bool_ptr", &testStruct{}, ""},
{"bool_ptr", &testStruct{BoolPtr: &boolValT}, "true"},
{"bool_ptr", &testStruct{BoolPtr: &boolValF}, "false"},

{"emptied", &testStruct{Emptied: true}, ""},

{"float32", &testStruct{Float32: float32Val}, "1.2345"},
{"float32_ptr", &testStruct{Float32Ptr: &float32Val}, "1.2345"},
{"float32_ptr", &testStruct{Float32Ptr: &float32Val0}, "0.0000"},
{"float32_ptr", &testStruct{}, ""},

{"float64", &testStruct{Float64: float64Val}, "1.2345"},
{"float64_ptr", &testStruct{Float64Ptr: &float64Val}, "1.2345"},
{"float64_ptr", &testStruct{Float64Ptr: &float64Val0}, "0.0000"},
{"float64_ptr", &testStruct{}, ""},

{"int", &testStruct{Int: intVal}, "123"},
{"int_ptr", &testStruct{IntPtr: &intVal}, "123"},
{"int_ptr", &testStruct{IntPtr: &intVal0}, "0"},
{"int_ptr", &testStruct{}, ""},
{"int8", &testStruct{Int8: int8Val}, "123"},
{"int8_ptr", &testStruct{Int8Ptr: &int8Val}, "123"},
{"int8_ptr", &testStruct{Int8Ptr: &int8Val0}, "0"},
{"int8_ptr", &testStruct{}, ""},
{"int16", &testStruct{Int16: int16Val}, "123"},
{"int16_ptr", &testStruct{Int16Ptr: &int16Val}, "123"},
{"int16_ptr", &testStruct{Int16Ptr: &int16Val0}, "0"},
{"int16_ptr", &testStruct{}, ""},
{"int32", &testStruct{Int32: int32Val}, "123"},
{"int32_ptr", &testStruct{Int32Ptr: &int32Val}, "123"},
{"int32_ptr", &testStruct{Int32Ptr: &int32Val0}, "0"},
{"int32_ptr", &testStruct{}, ""},
{"int64", &testStruct{Int64: int64Val}, "123"},
{"int64_ptr", &testStruct{Int64Ptr: &int64Val}, "123"},
{"int64_ptr", &testStruct{Int64Ptr: &int64Val0}, "0"},
{"int64_ptr", &testStruct{}, ""},

{"inverted", &testStruct{Inverted: true}, "false"},

Expand All @@ -212,6 +242,8 @@ func TestAppendTo(t *testing.T) {

{"string", &testStruct{String: stringVal}, stringVal},
{"string_ptr", &testStruct{StringPtr: &stringVal}, stringVal},
{"string_ptr", &testStruct{StringPtr: &stringVal0}, stringVal0},
{"string_ptr", &testStruct{}, stringVal0},

{"substruct[subsubstruct][string]", &testStruct{SubStruct: subStructVal}, "123"},
{"substruct_ptr[subsubstruct][string]", &testStruct{SubStructPtr: &subStructVal}, "123"},
Expand All @@ -221,14 +253,24 @@ func TestAppendTo(t *testing.T) {

{"uint", &testStruct{Uuint: uintVal}, "123"},
{"uint_ptr", &testStruct{UuintPtr: &uintVal}, "123"},
{"uint_ptr", &testStruct{UuintPtr: &uintVal0}, "0"},
{"uint_ptr", &testStruct{}, ""},
{"uint8", &testStruct{Uuint8: uint8Val}, "123"},
{"uint8_ptr", &testStruct{Uuint8Ptr: &uint8Val}, "123"},
{"uint8_ptr", &testStruct{Uuint8Ptr: &uint8Val0}, "0"},
{"uint8_ptr", &testStruct{}, ""},
{"uint16", &testStruct{Uuint16: uint16Val}, "123"},
{"uint16_ptr", &testStruct{Uuint16Ptr: &uint16Val}, "123"},
{"uint16_ptr", &testStruct{Uuint16Ptr: &uint16Val0}, "0"},
{"uint16_ptr", &testStruct{}, ""},
{"uint32", &testStruct{Uuint32: uint32Val}, "123"},
{"uint32_ptr", &testStruct{Uuint32Ptr: &uint32Val}, "123"},
{"uint32_ptr", &testStruct{Uuint32Ptr: &uint32Val0}, "0"},
{"uint32_ptr", &testStruct{}, ""},
{"uint64", &testStruct{Uuint64: uint64Val}, "123"},
{"uint64_ptr", &testStruct{Uuint64Ptr: &uint64Val}, "123"},
{"uint64_ptr", &testStruct{Uuint64Ptr: &uint64Val0}, "0"},
{"uint64_ptr", &testStruct{}, ""},

{"zeroed", &testStruct{Zeroed: true}, "0"},
}
Expand Down

0 comments on commit f5fbde5

Please sign in to comment.