Skip to content

Commit

Permalink
Add Null(Int64|Float64|Bool) types to support empty XML elements
Browse files Browse the repository at this point in the history
These fields can be used with elements that either have a primitive
value (i.e. float64, int64, bool), but where they can be empty in the
response from Braintree.
  • Loading branch information
jszwedko committed Jan 24, 2015
1 parent 481af9e commit 4ee818e
Show file tree
Hide file tree
Showing 9 changed files with 332 additions and 55 deletions.
14 changes: 7 additions & 7 deletions disbursement_details.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import (
)

type DisbursementDetails struct {
XMLName xml.Name `xml:"disbursement-details"`
DisbursementDate string `xml:"disbursement-date"`
SettlementAmount string `xml:"settlement-amount"` // float64
SettlementCurrencyIsoCode string `xml:"settlement-currency-iso-code"`
SettlementCurrencyExchangeRate string `xml:"settlement-currency-exchange-rate"` // float64
FundsHeld string `xml:"funds-held"` // bool
Success string `xml:"success"` // bool
XMLName xml.Name `xml:"disbursement-details"`
DisbursementDate string `xml:"disbursement-date"`
SettlementAmount *NullFloat64 `xml:"settlement-amount"`
SettlementCurrencyIsoCode string `xml:"settlement-currency-iso-code"`
SettlementCurrencyExchangeRate *NullFloat64 `xml:"settlement-currency-exchange-rate"`
FundsHeld *NullBool `xml:"funds-held"`
Success *NullBool `xml:"success"`
}
2 changes: 1 addition & 1 deletion disbursement_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func TestDisbursementTransactions(t *testing.T) {
t.Fatal(err)
}

if result.TotalItems != "1" {
if !result.TotalItems.Valid || result.TotalItems.Int64 != 1 {
t.Fatal(result)
}

Expand Down
135 changes: 135 additions & 0 deletions null_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package braintree

import (
"database/sql"
"strconv"
)

// NullInt64 wraps sql.NullInt64 to allow it to be serializable to/from XML
// via TextMarshaler and TextUnmarshaler
type NullInt64 struct {
sql.NullInt64
}

// NewNullInt64 creats a new NullInt64
func NewNullInt64(n int64, valid bool) NullInt64 {
return NullInt64{
sql.NullInt64{
Valid: valid,
Int64: n,
},
}
}

// UnmarshalText initializes an invalid NullInt64 if text is empty
// otherwise it tries to parse it as an integer in base 10
func (n *NullInt64) UnmarshalText(text []byte) (err error) {
if len(text) == 0 {
n.Valid = false
return nil
}

n.Int64, err = strconv.ParseInt(string(text), 10, 64)
if err != nil {
return err
}

n.Valid = true
return nil
}

// UnmarshalText initializes an invalid NullInt64 if text is empty
// otherwise it tries to parse it as an integer in base 10
// MarshalText returns "" for invalid NullInt64s, otherwise the integer value
func (n NullInt64) MarshalText() ([]byte, error) {
if !n.Valid {
return []byte{}, nil
}
return []byte(strconv.FormatInt(n.Int64, 10)), nil
}

// NullFloat64 wraps sql.NullFloat64 to allow it to be serializable to/from XML
// via TextMarshaler and TextUnmarshaler
type NullFloat64 struct {
sql.NullFloat64
}

// NewNullFloat64 creats a new NullFloat64
func NewNullFloat64(n float64, valid bool) NullFloat64 {
return NullFloat64{
sql.NullFloat64{
Valid: valid,
Float64: n,
},
}
}

// UnmarshalText initializes an invalid NullFloat64 if text is empty
// otherwise it tries to parse it as an integer in base 10
func (n *NullFloat64) UnmarshalText(text []byte) (err error) {
if len(text) == 0 {
n.Valid = false
return nil
}

n.Float64, err = strconv.ParseFloat(string(text), 64)
if err != nil {
return err
}

n.Valid = true
return nil
}

// UnmarshalText initializes an invalid NullFloat64 if text is empty
// otherwise it tries to parse it as an integer in base 10
// MarshalText returns "" for invalid NullFloat64s, otherwise the float string
func (n NullFloat64) MarshalText() ([]byte, error) {
if !n.Valid {
return []byte{}, nil
}
return []byte(strconv.FormatFloat(n.Float64, 'f', -1, 64)), nil
}

// NullBool wraps sql.NullBool to allow it to be serializable to/from XML
// via TextMarshaler and TextUnmarshaler
type NullBool struct {
sql.NullBool
}

// NewNullBool creats a new NullBool
func NewNullBool(b bool, valid bool) NullBool {
return NullBool{
sql.NullBool{
Valid: valid,
Bool: b,
},
}
}

// UnmarshalText initializes an invalid NullBool if text is empty
// otherwise it tries to parse it as a boolean
func (n *NullBool) UnmarshalText(text []byte) (err error) {
if len(text) == 0 {
n.Valid = false
return nil
}

n.Bool, err = strconv.ParseBool(string(text))
if err != nil {
return err
}

n.Valid = true
return nil
}

// UnmarshalText initializes an invalid NullBool if text is empty
// otherwise it tries to parse it as an integer in base 10
// MarshalText returns "" for invalid NullBools, otherwise the boolean value
func (n NullBool) MarshalText() ([]byte, error) {
if !n.Valid {
return []byte{}, nil
}
return []byte(strconv.FormatBool(n.Bool)), nil
}
153 changes: 153 additions & 0 deletions null_types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package braintree

import (
"bytes"
"testing"
)

func TestNullInt64UnmarshalText(t *testing.T) {
tests := []struct {
in []byte
out NullInt64
sholudError bool
}{
{[]byte(""), NewNullInt64(0, false), false},
{[]byte("10"), NewNullInt64(10, true), false},
{[]byte("abcd"), NewNullInt64(0, false), true},
}

for _, tt := range tests {
n := NullInt64{}
err := n.UnmarshalText(tt.in)

if tt.sholudError {
if err == nil {
t.Errorf("expected UnmarshalText(%q) => to error, but it did not", tt.in)
}
} else {
if err != nil {
t.Errorf("expected UnmarshalText(%q) => to not error, but it did with %s", tt.in, err)
}
}

if n != tt.out {
t.Errorf("UnmarshalText(%q) => %q, want %q", tt.in, n, tt.out)
}
}
}

func TestNullInt64MarshalText(t *testing.T) {
tests := []struct {
in NullInt64
out []byte
}{
{NewNullInt64(0, false), []byte("")},
{NewNullInt64(10, true), []byte("10")},
}

for _, tt := range tests {
b, err := tt.in.MarshalText()

if !bytes.Equal(b, tt.out) || err != nil {
t.Errorf("%q.MarshalText() => (%s, %s), want (%s, %s)", tt.in, b, err, tt.out, nil)
}
}
}

func TestNullFloat64UnmarshalText(t *testing.T) {
tests := []struct {
in []byte
out NullFloat64
sholudError bool
}{
{[]byte(""), NewNullFloat64(0, false), false},
{[]byte("10"), NewNullFloat64(10, true), false},
{[]byte("abcd"), NewNullFloat64(0, false), true},
}

for _, tt := range tests {
n := NullFloat64{}
err := n.UnmarshalText(tt.in)

if tt.sholudError {
if err == nil {
t.Errorf("expected UnmarshalText(%q) => to error, but it did not", tt.in)
}
} else {
if err != nil {
t.Errorf("expected UnmarshalText(%q) => to not error, but it did with %s", tt.in, err)
}
}

if n != tt.out {
t.Errorf("UnmarshalText(%q) => %q, want %q", tt.in, n, tt.out)
}
}
}

func TestNullFloat64MarshalText(t *testing.T) {
tests := []struct {
in NullFloat64
out []byte
}{
{NewNullFloat64(0, false), []byte("")},
{NewNullFloat64(10, true), []byte("10")},
}

for _, tt := range tests {
b, err := tt.in.MarshalText()

if !bytes.Equal(b, tt.out) || err != nil {
t.Errorf("%q.MarshalText() => (%s, %s), want (%s, %s)", tt.in, b, err, tt.out, nil)
}
}
}

func TestNullBoolUnmarshalText(t *testing.T) {
tests := []struct {
in []byte
out NullBool
sholudError bool
}{
{[]byte(""), NewNullBool(false, false), false},
{[]byte("true"), NewNullBool(true, true), false},
{[]byte("abcd"), NewNullBool(false, false), true},
}

for _, tt := range tests {
n := NullBool{}
err := n.UnmarshalText(tt.in)

if tt.sholudError {
if err == nil {
t.Errorf("expected UnmarshalText(%q) => to error, but it did not", tt.in)
}
} else {
if err != nil {
t.Errorf("expected UnmarshalText(%q) => to not error, but it did with %s", tt.in, err)
}
}

if n != tt.out {
t.Errorf("UnmarshalText(%q) => %q, want %q", tt.in, n, tt.out)
}
}
}

func TestNullBoolMarshalText(t *testing.T) {
tests := []struct {
in NullBool
out []byte
}{
{NewNullBool(false, false), []byte("")},
{NewNullBool(true, true), []byte("true")},
}

for _, tt := range tests {
b, err := tt.in.MarshalText()

if !bytes.Equal(b, tt.out) || err != nil {
t.Errorf("%q.MarshalText() => (%s, %s), want (%s, %s)", tt.in, b, err, tt.out, nil)
}
}
}
36 changes: 15 additions & 21 deletions plan.go
Original file line number Diff line number Diff line change
@@ -1,31 +1,25 @@
package braintree

type Plan struct {
XMLName string `xml:"plan"`
Id string `xml:"id"`
MerchantId string `xml:"merchant-id"`
BillingDayOfMonth string `xml:"billing-day-of-month"` // int
BillingFrequency string `xml:"billing-frequency"` // int
CurrencyISOCode string `xml:"currency-iso-code"`
Description string `xml:"description"`
Name string `xml:"name"`
NumberOfBillingCycles string `xml:"number-of-billing-cycles"` // int
Price float64 `xml:"price"`
TrialDuration string `xml:"trial-duration"` // int
TrialDurationUnit string `xml:"trial-duration-unit"`
TrialPeriod string `xml:"trial-period"` // bool
CreatedAt string `xml:"created-at"`
UpdatedAt string `xml:"updated-at"`
XMLName string `xml:"plan"`
Id string `xml:"id"`
MerchantId string `xml:"merchant-id"`
BillingDayOfMonth *NullInt64 `xml:"billing-day-of-month"`
BillingFrequency *NullInt64 `xml:"billing-frequency"`
CurrencyISOCode string `xml:"currency-iso-code"`
Description string `xml:"description"`
Name string `xml:"name"`
NumberOfBillingCycles *NullInt64 `xml:"number-of-billing-cycles"`
Price float64 `xml:"price"`
TrialDuration *NullInt64 `xml:"trial-duration"`
TrialDurationUnit string `xml:"trial-duration-unit"`
TrialPeriod *NullBool `xml:"trial-period"`
CreatedAt string `xml:"created-at"`
UpdatedAt string `xml:"updated-at"`
// AddOns []interface{} `xml:"add-ons"`
// Discounts []interface{} `xml:"discounts"`
}

// TODO(eaigner): it is suboptimal that we use string instead of int/bool types here,
// but I see no way around this atm to avoid integer parse errors if the field is empty.
//
// If there is a better method, and it can be unmarshalled directly to the correct type
// without errors, this needs to be changed.

type Plans struct {
XMLName string `xml:"plans"`
Plan []*Plan `xml:"plan"`
Expand Down
Loading

0 comments on commit 4ee818e

Please sign in to comment.