Repository: stripe/stripe-go
Pull request #468 (2017-09-28 16:00:23)

Parse zero-val pointers
If a boolean value is meant to be `true` by default, `stripe-go` uses a pointer to a bool, and uses `nil` to render no url parameter, which ends up being `true` to the stripe-api.

Unfortunately, there was no ability to pass a pointer to a boolean of `true`. This attempts to fix it in a non-breaking, non-major change or refactor way.

I made some smaller changes as well to assist in the readability (since I had to do a lot of that -- reading).


Patch:
diff --git a/form/form.go b/form/form.go
index d8ca3885..471d9fb6 100644
--- a/form/form.go
+++ b/form/form.go
@@ -27,7 +27,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.
@@ -68,7 +76,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)
@@ -84,7 +92,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)
 		}
@@ -110,7 +118,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
@@ -120,7 +128,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,
@@ -140,24 +148,23 @@ 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))
 	}
 }
 
@@ -165,7 +172,7 @@ 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, "")
@@ -179,7 +186,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)
@@ -192,11 +199,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)
 	}
 }
 
@@ -205,18 +212,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 {
@@ -278,54 +287,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) {
@@ -357,9 +369,12 @@ 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",
@@ -367,20 +382,15 @@ func makeStructEncoder(t reflect.Type) *structEncoder {
 			))
 		}
 
-		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


Test patch:
diff --git a/form/form_test.go b/form/form_test.go
index e22e3044..48832efe 100644
--- a/form/form_test.go
+++ b/form/form_test.go
@@ -128,21 +128,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{
@@ -151,10 +160,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
@@ -165,27 +179,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"},
 
@@ -211,6 +241,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"},
@@ -220,14 +252,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"},
 	}