diff --git a/.gitignore b/.gitignore
index 49ce3c1..61ead86 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1 @@
-/vendor
\ No newline at end of file
+/vendor
diff --git a/add_on_usage.go b/add_on_usage.go
new file mode 100644
index 0000000..6e6ff21
--- /dev/null
+++ b/add_on_usage.go
@@ -0,0 +1,121 @@
+package recurly
+
+import (
+ "context"
+ "encoding/xml"
+ "fmt"
+ "net/http"
+)
+
+// AddOnUsageService manages the interactions for add-on usages.
+type AddOnUsageService interface {
+ // List returns a pager to paginate usages for add-on in subscription. PagerOptions are used to
+ // optionally filter the results.
+ //
+ // https://dev.recurly.com/docs/list-add-ons-usage
+ List(uuid, addOnCode string, opts *PagerOptions) Pager
+
+ // Create creates a new usage for add-on in subscription.
+ //
+ // https://dev.recurly.com/docs/log-usage
+ Create(ctx context.Context, uuid, addOnCode string, usage AddOnUsage) (*AddOnUsage, error)
+
+ // Get retrieves an usage. If the usage does not exist,
+ // a nil usage and nil error are returned.
+ //
+ // https://dev.recurly.com/docs/lookup-usage-record
+ Get(ctx context.Context, uuid, addOnCode, usageID string) (*AddOnUsage, error)
+
+ // Update updates the usage information. Once usage is billed, only MerchantTag can be updated.
+ //
+ // https://dev.recurly.com/docs/update-usage
+ Update(ctx context.Context, uuid, addOnCode, usageID string, usage AddOnUsage) (*AddOnUsage, error)
+
+ // Delete removes an usage from subscription add-on. If usage is billed, it can't be removed
+ //
+ // https://dev.recurly.com/docs/delete-a-usage-record
+ Delete(ctx context.Context, uuid, addOnCode, usageID string) error
+}
+
+// Usage is a billable event or group of events recorded on a purchased usage-based add-on and billed in arrears each billing cycle.
+//
+// https://dev.recurly.com/docs/usage-record-object
+type AddOnUsage struct {
+ XMLName xml.Name `xml:"usage"`
+ ID int `xml:"id,omitempty"`
+ Amount int `xml:"amount,omitempty"`
+ MerchantTag string `xml:"merchant_tag,omitempty"`
+ RecordingTimestamp NullTime `xml:"recording_timestamp,omitempty"`
+ UsageTimestamp NullTime `xml:"usage_timestamp,omitempty"`
+ CreatedAt NullTime `xml:"created_at,omitempty"`
+ UpdatedAt NullTime `xml:"updated_at,omitempty"`
+ BilledAt NullTime `xml:"billed_at,omitempty"`
+ UsageType string `xml:"usage_type,omitempty"`
+ UnitAmountInCents int `xml:"unit_amount_in_cents,omitempty"`
+ UsagePercentage NullFloat `xml:"usage_percentage,omitempty"`
+}
+
+var _ AddOnUsageService = &addOnUsageServiceImpl{}
+
+type addOnUsageServiceImpl serviceImpl
+
+func (s *addOnUsageServiceImpl) List(uuid, addOnCode string, opts *PagerOptions) Pager {
+ path := fmt.Sprintf("/subscriptions/%s/add_ons/%s/usage", uuid, addOnCode)
+ return s.client.newPager("GET", path, opts)
+}
+
+func (s *addOnUsageServiceImpl) Get(ctx context.Context, uuid, addOnCode, usageID string) (*AddOnUsage, error) {
+ path := fmt.Sprintf("/subscriptions/%s/add_ons/%s/usage/%s", uuid, addOnCode, usageID)
+ req, err := s.client.newRequest("GET", path, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ var dst AddOnUsage
+ if _, err := s.client.do(ctx, req, &dst); err != nil {
+ if e, ok := err.(*ClientError); ok && e.Response.StatusCode == http.StatusNotFound {
+ return nil, nil
+ }
+ return nil, err
+ }
+ return &dst, nil
+}
+
+func (s *addOnUsageServiceImpl) Create(ctx context.Context, uuid, addOnCode string, usage AddOnUsage) (*AddOnUsage, error) {
+ path := fmt.Sprintf("/subscriptions/%s/add_ons/%s/usage", uuid, addOnCode)
+ req, err := s.client.newRequest("POST", path, usage)
+ if err != nil {
+ return nil, err
+ }
+
+ var dst AddOnUsage
+ if _, err := s.client.do(ctx, req, &dst); err != nil {
+ return nil, err
+ }
+ return &dst, nil
+}
+
+func (s *addOnUsageServiceImpl) Update(ctx context.Context, uuid, addOnCode, usageID string, usage AddOnUsage) (*AddOnUsage, error) {
+ path := fmt.Sprintf("/subscriptions/%s/add_ons/%s/usage/%s", uuid, addOnCode, usageID)
+ req, err := s.client.newRequest("PUT", path, usage)
+ if err != nil {
+ return nil, err
+ }
+
+ var dst AddOnUsage
+ if _, err := s.client.do(ctx, req, &dst); err != nil {
+ return nil, err
+ }
+ return &dst, nil
+}
+
+func (s *addOnUsageServiceImpl) Delete(ctx context.Context, uuid, addOnCode, usageID string) error {
+ path := fmt.Sprintf("/subscriptions/%s/add_ons/%s/usage/%s", uuid, addOnCode, usageID)
+ req, err := s.client.newRequest("DELETE", path, nil)
+ if err != nil {
+ return err
+ }
+
+ _, err = s.client.do(ctx, req, nil)
+ return err
+}
diff --git a/add_on_usage_test.go b/add_on_usage_test.go
new file mode 100644
index 0000000..b8f1c1c
--- /dev/null
+++ b/add_on_usage_test.go
@@ -0,0 +1,264 @@
+package recurly_test
+
+import (
+ "bytes"
+ "context"
+ "encoding/xml"
+ "net/http"
+ "strconv"
+ "testing"
+ "time"
+
+ "github.com/blacklightcms/recurly"
+ "github.com/google/go-cmp/cmp"
+)
+
+// Ensure structs are encoded to XML properly.
+func TestAddOnUsage_Encoding(t *testing.T) {
+ now := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
+ tests := []struct {
+ v recurly.AddOnUsage
+ expected string
+ }{
+ {
+ expected: MustCompactString(`
+
+
+ `),
+ },
+ {
+ v: recurly.AddOnUsage{ID: 123456},
+ expected: MustCompactString(`
+
+ 123456
+
+ `),
+ },
+ {
+ v: recurly.AddOnUsage{Amount: 100},
+ expected: MustCompactString(`
+
+ 100
+
+ `),
+ },
+ {
+ v: recurly.AddOnUsage{MerchantTag: "some_merchant"},
+ expected: MustCompactString(`
+
+ some_merchant
+
+ `),
+ },
+ {
+ v: recurly.AddOnUsage{RecordingTimestamp: recurly.NewTime(now)},
+ expected: MustCompactString(`
+
+ 2000-01-01T00:00:00Z
+
+ `),
+ },
+ {
+ v: recurly.AddOnUsage{UsageTimestamp: recurly.NewTime(now)},
+ expected: MustCompactString(`
+
+ 2000-01-01T00:00:00Z
+
+ `),
+ },
+ {
+ v: recurly.AddOnUsage{CreatedAt: recurly.NewTime(now)},
+ expected: MustCompactString(`
+
+ 2000-01-01T00:00:00Z
+
+ `),
+ },
+ {
+ v: recurly.AddOnUsage{UpdatedAt: recurly.NewTime(now)},
+ expected: MustCompactString(`
+
+ 2000-01-01T00:00:00Z
+
+ `),
+ },
+ {
+ v: recurly.AddOnUsage{BilledAt: recurly.NewTime(now)},
+ expected: MustCompactString(`
+
+ 2000-01-01T00:00:00Z
+
+ `),
+ },
+ {
+ v: recurly.AddOnUsage{UsageType: "price"},
+ expected: MustCompactString(`
+
+ price
+
+ `),
+ },
+
+ {
+ v: recurly.AddOnUsage{UnitAmountInCents: 313},
+ expected: MustCompactString(`
+
+ 313
+
+ `),
+ },
+ {
+ v: recurly.AddOnUsage{UsagePercentage: recurly.NewFloat(0.50)},
+ expected: MustCompactString(`
+
+ 0.5
+
+ `),
+ },
+ }
+
+ for i, tt := range tests {
+ t.Run(strconv.Itoa(i), func(t *testing.T) {
+ buf := new(bytes.Buffer)
+ if err := xml.NewEncoder(buf).Encode(tt.v); err != nil {
+ t.Fatal(err)
+ } else if buf.String() != tt.expected {
+ t.Fatal(buf.String())
+ }
+ })
+ }
+}
+
+func TestAddOnUsage_List(t *testing.T) {
+ client, s := recurly.NewTestServer()
+ defer s.Close()
+
+ var invocations int
+ s.HandleFunc("GET", "/v2/subscriptions/1122334455/add_ons/addOnCode/usage", func(w http.ResponseWriter, r *http.Request) {
+ invocations++
+ w.WriteHeader(http.StatusOK)
+ w.Write(MustOpenFile("add_on_usages.xml"))
+ }, t)
+
+ pager := client.AddOnUsages.List("1122334455", "addOnCode", nil)
+ for pager.Next() {
+ var a []recurly.AddOnUsage
+ if err := pager.Fetch(context.Background(), &a); err != nil {
+ t.Fatal(err)
+ } else if !s.Invoked {
+ t.Fatal("expected s to be invoked")
+ } else if diff := cmp.Diff(a, []recurly.AddOnUsage{*NewTestAddOnUsage()}); diff != "" {
+ t.Fatal(diff)
+ }
+ }
+ if invocations != 1 {
+ t.Fatalf("unexpected number of invocations: %d", invocations)
+ }
+}
+
+func TestAddOnUsage_Get(t *testing.T) {
+ t.Run("OK", func(t *testing.T) {
+ client, s := recurly.NewTestServer()
+ defer s.Close()
+
+ s.HandleFunc("GET", "/v2/subscriptions/1122334455/add_ons/addOnCode/usage/1234", func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ w.Write(MustOpenFile("add_on_usage.xml"))
+ }, t)
+
+ if a, err := client.AddOnUsages.Get(context.Background(), "1122334455", "addOnCode", "1234"); err != nil {
+ t.Fatal(err)
+ } else if diff := cmp.Diff(a, NewTestAddOnUsage()); diff != "" {
+ t.Fatal(diff)
+ } else if !s.Invoked {
+ t.Fatal("expected fn invocation")
+ }
+ })
+
+ // Ensure a 404 returns nil values.
+ t.Run("ErrNotFound", func(t *testing.T) {
+ client, s := recurly.NewTestServer()
+ defer s.Close()
+
+ s.HandleFunc("GET", "/v2/subscriptions/1122334455/add_ons/addOnCode/usage/8888", func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusNotFound)
+ }, t)
+
+ if a, err := client.AddOnUsages.Get(context.Background(), "1122334455", "addOnCode", "8888"); !s.Invoked {
+ t.Fatal("expected fn invocation")
+ } else if err != nil {
+ t.Fatal(err)
+ } else if a != nil {
+ t.Fatalf("expected nil: %#v", a)
+ }
+ })
+}
+
+func TestAddOnUsage_Create(t *testing.T) {
+ client, s := recurly.NewTestServer()
+ defer s.Close()
+
+ s.HandleFunc("POST", "/v2/subscriptions/1122334455/add_ons/addOnCode/usage", func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusCreated)
+ w.Write(MustOpenFile("add_on_usage.xml"))
+ }, t)
+
+ if a, err := client.AddOnUsages.Create(context.Background(), "1122334455", "addOnCode", recurly.AddOnUsage{}); !s.Invoked {
+ t.Fatal("expected fn invocation")
+ } else if err != nil {
+ t.Fatal(err)
+ } else if diff := cmp.Diff(a, NewTestAddOnUsage()); diff != "" {
+ t.Fatal(diff)
+ }
+}
+
+func TestAddOnUsage_Update(t *testing.T) {
+ client, s := recurly.NewTestServer()
+ defer s.Close()
+
+ s.HandleFunc("PUT", "/v2/subscriptions/1122334455/add_ons/addOnCode/usage/1234", func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ w.Write(MustOpenFile("add_on_usage.xml"))
+ }, t)
+
+ if a, err := client.AddOnUsages.Update(context.Background(), "1122334455", "addOnCode", "1234", recurly.AddOnUsage{}); !s.Invoked {
+ t.Fatal("expected fn invocation")
+ } else if err != nil {
+ t.Fatal(err)
+ } else if diff := cmp.Diff(a, NewTestAddOnUsage()); diff != "" {
+ t.Fatal(diff)
+ }
+}
+
+func TestAddOnUsage_Delete(t *testing.T) {
+ client, s := recurly.NewTestServer()
+ defer s.Close()
+
+ s.HandleFunc("DELETE", "/v2/subscriptions/1122334455/add_ons/addOnCode/usage/1234", func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusNoContent)
+ }, t)
+
+ if err := client.AddOnUsages.Delete(context.Background(), "1122334455", "addOnCode", "1234"); !s.Invoked {
+ t.Fatal("expected fn invocation")
+ } else if err != nil {
+ t.Fatal(err)
+ }
+}
+
+// Returns add on corresponding to testdata/add_on.xml
+func NewTestAddOnUsage() *recurly.AddOnUsage {
+ now := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
+ return &recurly.AddOnUsage{
+ XMLName: xml.Name{Local: "usage"},
+ Amount: 1,
+ MerchantTag: "Order ID: 4939853977878713",
+ RecordingTimestamp: recurly.NewTime(now),
+ UsageTimestamp: recurly.NewTime(now),
+ CreatedAt: recurly.NewTime(now),
+ UpdatedAt: recurly.NullTime{},
+ BilledAt: recurly.NullTime{},
+ UsageType: "price",
+ UnitAmountInCents: 45,
+ UsagePercentage: recurly.NewFloat(12.34),
+ }
+}
diff --git a/mock/add_on_usage.go b/mock/add_on_usage.go
new file mode 100644
index 0000000..7f6f3ff
--- /dev/null
+++ b/mock/add_on_usage.go
@@ -0,0 +1,50 @@
+package mock
+
+import (
+ "context"
+ "github.com/blacklightcms/recurly"
+)
+
+var _ recurly.AddOnUsageService = &AddOnUsageService{}
+
+type AddOnUsageService struct {
+ OnList func(uuid, addOnCode string, opts *recurly.PagerOptions) recurly.Pager
+ ListInvoked bool
+
+ OnGet func(ctx context.Context, uuid, addOnCode, usageId string) (*recurly.AddOnUsage, error)
+ GetInvoked bool
+
+ OnCreate func(ctx context.Context, uuid, addOnCode string, usage recurly.AddOnUsage) (*recurly.AddOnUsage, error)
+ CreateInvoked bool
+
+ OnUpdate func(ctx context.Context, uuid, addOnCode string, usage recurly.AddOnUsage) (*recurly.AddOnUsage, error)
+ UpdateInvoked bool
+
+ OnDelete func(ctx context.Context, uuid, addOnCode, usageId string) error
+ DeleteInvoked bool
+}
+
+func (m *AddOnUsageService) List(uuid, addOnCode string, opts *recurly.PagerOptions) recurly.Pager {
+ m.ListInvoked = true
+ return m.OnList(uuid, addOnCode, opts)
+}
+
+func (m *AddOnUsageService) Get(ctx context.Context, uuid, addOnCode, usageId string) (*recurly.AddOnUsage, error) {
+ m.GetInvoked = true
+ return m.OnGet(ctx, uuid, addOnCode, usageId)
+}
+
+func (m *AddOnUsageService) Create(ctx context.Context, uuid, addOnCode string, usage recurly.AddOnUsage) (*recurly.AddOnUsage, error) {
+ m.CreateInvoked = true
+ return m.OnCreate(ctx, uuid, addOnCode, usage)
+}
+
+func (m *AddOnUsageService) Update(ctx context.Context, uuid, addOnCode, usageId string, usage recurly.AddOnUsage) (*recurly.AddOnUsage, error) {
+ m.UpdateInvoked = true
+ return m.OnUpdate(ctx, uuid, addOnCode, usage)
+}
+
+func (m *AddOnUsageService) Delete(ctx context.Context, uuid, addOnCode, usageId string) error {
+ m.DeleteInvoked = true
+ return m.OnDelete(ctx, uuid, addOnCode, usageId)
+}
diff --git a/mock/client.go b/mock/client.go
index bfe570a..efe28b8 100644
--- a/mock/client.go
+++ b/mock/client.go
@@ -11,6 +11,7 @@ type Client struct {
Accounts AccountsService
AddOns AddOnsService
+ AddOnUsages AddOnUsageService
Adjustments AdjustmentsService
Billing BillingService
Coupons CouponsService
@@ -33,6 +34,7 @@ func NewClient(subdomain, apiKey string) *Client {
// Attach mock implementations.
c.Client.Accounts = &c.Accounts
c.Client.AddOns = &c.AddOns
+ c.Client.AddOnUsages = &c.AddOnUsages
c.Client.Adjustments = &c.Adjustments
c.Client.Billing = &c.Billing
c.Client.Coupons = &c.Coupons
diff --git a/pager.go b/pager.go
index 25c860e..2a81255 100644
--- a/pager.go
+++ b/pager.go
@@ -116,6 +116,7 @@ func (p *pager) Fetch(ctx context.Context, dst interface{}) error {
Account []Account `xml:"account"`
Adjustment []Adjustment `xml:"adjustment"`
AddOn []AddOn `xml:"add_on"`
+ AddOnUsage []AddOnUsage `xml:"usage"`
Coupon []Coupon `xml:"coupon"`
CreditPayment []CreditPayment `xml:"credit_payment"`
ExportDate []ExportDate `xml:"export_date"`
@@ -146,6 +147,8 @@ func (p *pager) Fetch(ctx context.Context, dst interface{}) error {
*v = unmarshaler.Adjustment
case *[]AddOn:
*v = unmarshaler.AddOn
+ case *[]AddOnUsage:
+ *v = unmarshaler.AddOnUsage
case *[]Coupon:
*v = unmarshaler.Coupon
case *[]CreditPayment:
@@ -213,6 +216,16 @@ func (p *pager) FetchAll(ctx context.Context, dst interface{}) error {
all = append(all, dst...)
}
*v = all
+ case *[]AddOnUsage:
+ var all []AddOnUsage
+ for p.Next() {
+ var dst []AddOnUsage
+ if err := p.Fetch(ctx, &dst); err != nil {
+ return err
+ }
+ all = append(all, dst...)
+ }
+ *v = all
case *[]Coupon:
var all []Coupon
for p.Next() {
@@ -345,7 +358,7 @@ type PagerOptions struct {
// query is for any one-off URL params used by a specific endpoint.
// Values sent as time.Time or recurly.NullTime will be automatically
// converted to a valid datetime format for Recurly.
- query query
+ Query query
// Cursor is set internally by the library. If you are paginating
// records non-consecutively and obtained the next cursor, you can set it
@@ -390,19 +403,19 @@ func (q query) append(u *url.URL) {
// append appends params to a URL.
func (p PagerOptions) append(u *url.URL) {
- if p.query == nil {
- p.query = map[string]interface{}{}
+ if p.Query == nil {
+ p.Query = map[string]interface{}{}
}
if p.PerPage > 0 {
- p.query["per_page"] = p.PerPage
+ p.Query["per_page"] = p.PerPage
}
- p.query["begin_time"] = p.BeginTime.String()
- p.query["end_time"] = p.EndTime.String()
- p.query["sort"] = p.Sort
- p.query["order"] = p.Order
- p.query["state"] = p.State
- p.query["type"] = p.Type
- p.query["cursor"] = p.Cursor
- p.query.append(u)
+ p.Query["begin_time"] = p.BeginTime.String()
+ p.Query["end_time"] = p.EndTime.String()
+ p.Query["sort"] = p.Sort
+ p.Query["order"] = p.Order
+ p.Query["state"] = p.State
+ p.Query["type"] = p.Type
+ p.Query["cursor"] = p.Cursor
+ p.Query.append(u)
}
diff --git a/recurly.go b/recurly.go
index b8de622..b94bdf3 100644
--- a/recurly.go
+++ b/recurly.go
@@ -54,6 +54,7 @@ type Client struct {
Accounts AccountsService
Adjustments AdjustmentsService
AddOns AddOnsService
+ AddOnUsages AddOnUsageService
AutomatedExports AutomatedExportsService
Billing BillingService
Coupons CouponsService
@@ -96,6 +97,7 @@ func NewClient(subdomain, apiKey string) *Client {
client.Accounts = &accountsImpl{client: client}
client.Adjustments = &adjustmentsImpl{client: client}
client.AddOns = &addOnsImpl{client: client}
+ client.AddOnUsages = &addOnUsageServiceImpl{client: client}
client.AutomatedExports = &automatedExportsImpl{client: client}
client.Billing = &billingImpl{client: client}
client.Coupons = &couponsImpl{client: client}
diff --git a/testdata/add_on_usage.xml b/testdata/add_on_usage.xml
new file mode 100644
index 0000000..950fb4c
--- /dev/null
+++ b/testdata/add_on_usage.xml
@@ -0,0 +1,14 @@
+
+
+
+ 1
+ Order ID: 4939853977878713
+ 2000-01-01T00:00:00Z
+ 2000-01-01T00:00:00Z
+ 2000-01-01T00:00:00Z
+
+
+ price
+ 45
+ 12.34
+
\ No newline at end of file
diff --git a/testdata/add_on_usages.xml b/testdata/add_on_usages.xml
new file mode 100644
index 0000000..671d242
--- /dev/null
+++ b/testdata/add_on_usages.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ 1
+ Order ID: 4939853977878713
+ 2000-01-01T00:00:00Z
+ 2000-01-01T00:00:00Z
+ 2000-01-01T00:00:00Z
+
+
+ price
+ 45
+ 12.34
+
+
\ No newline at end of file
diff --git a/xml.go b/xml.go
index d528272..d049ae1 100644
--- a/xml.go
+++ b/xml.go
@@ -164,6 +164,82 @@ func (n NullInt) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return nil
}
+// NullFloat is used for properly handling float types that could be null. (float64 is returned)
+type NullFloat struct {
+ value float64
+ valid bool
+}
+
+// NullFloat returns NullFloat with a valid value of f.
+func NewFloat(f float64) NullFloat {
+ return NullFloat{value: f, valid: true}
+}
+
+// NewFloatPtr returns a new NullFloat from a pointer to float64.
+func NewFloatPtr(f *float64) NullFloat {
+ if f == nil {
+ return NullFloat{}
+ }
+ return NewFloat(*f)
+}
+
+// Float64 returns the float64 value, regardless of validity. Use Value() if
+// you need to know whether the value is valid.
+func (n NullFloat) Float64() float64 {
+ return n.value
+}
+
+// Float64Ptr returns a pointer to the float64 value, or nil if the value is not valid.
+func (n NullFloat) Float64Ptr() *float64 {
+ if n.valid {
+ return &n.value
+ }
+ return nil
+}
+
+// Value returns the value of NullFloat. The value should only be considered
+// valid if ok returns true.
+func (n NullFloat) Value() (value float64, ok bool) {
+ return n.value, n.valid
+}
+
+// Equal compares the equality of two NullFloat.
+func (n NullFloat) Equal(v NullFloat) bool {
+ return n.value == v.value && n.valid == v.valid
+}
+
+// MarshalJSON marshals an float64 based on whether valid is true.
+func (n NullFloat) MarshalJSON() ([]byte, error) {
+ if n.valid {
+ return json.Marshal(n.value)
+ }
+ return []byte("null"), nil
+}
+
+// UnmarshalXML unmarshals an float64 properly, as well as marshaling an empty string to nil.
+func (n *NullFloat) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
+ var v struct {
+ Float float64 `xml:",chardata"`
+ Nil string `xml:"nil,attr"`
+ }
+ if err := d.DecodeElement(&v, &start); err != nil {
+ return err
+ } else if strings.EqualFold(v.Nil, "nil") || strings.EqualFold(v.Nil, "true") {
+ return nil
+ }
+ *n = NewFloat(v.Float)
+ return nil
+}
+
+// MarshalXML marshals NullFloat greater than zero to XML. Otherwise nothing is
+// marshaled.
+func (n NullFloat) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
+ if n.valid {
+ return e.EncodeElement(n.value, start)
+ }
+ return nil
+}
+
// DateTimeFormat is the format Recurly uses to represent datetimes.
const DateTimeFormat = "2006-01-02T15:04:05Z07:00"
diff --git a/xml_test.go b/xml_test.go
index 8fa28ba..5f87d06 100644
--- a/xml_test.go
+++ b/xml_test.go
@@ -233,6 +233,116 @@ func TestXML_NullIntPtr(t *testing.T) {
}
}
+func TestXML_NullFloat(t *testing.T) {
+ t.Run("ZeroValue", func(t *testing.T) {
+ var f recurly.NullFloat
+ if value, ok := f.Value(); ok {
+ t.Fatal("expected ok to be false")
+ } else if value != 0 {
+ t.Fatalf("unexpected value: %f", value)
+ }
+
+ if f.Float64() != 0 {
+ t.Fatalf("unexpected value: %f", f.Float64())
+ } else if ptr := f.Float64Ptr(); ptr != nil {
+ t.Fatalf("expected nil: %#v", ptr)
+ }
+ })
+
+ f := recurly.NewFloat(12.34)
+ if value, ok := f.Value(); !ok {
+ t.Fatal("expected ok to be true")
+ } else if value != 12.34 {
+ t.Fatalf("unexpected value: %f", value)
+ }
+
+ if f.Float64() != 12.34 {
+ t.Fatalf("unexpected value: %f", f.Float64())
+ } else if ptr := f.Float64Ptr(); ptr == nil {
+ t.Fatal("expected non-nil value")
+ } else if *ptr != 12.34 {
+ t.Fatalf("unexpected value: %#v", ptr)
+ }
+
+ f = recurly.NewFloat(0.00)
+ if value, ok := f.Value(); !ok {
+ t.Fatal("expected ok to be true")
+ } else if value != 0 {
+ t.Fatalf("unexpected value: %f", value)
+ }
+
+ if f.Float64() != 0 {
+ t.Fatalf("unexpected value: %f", f.Float64())
+ } else if ptr := f.Float64Ptr(); ptr == nil {
+ t.Fatal("expected non-nil value")
+ } else if *ptr != 0 {
+ t.Fatalf("unexpected value: %#v", ptr)
+ }
+
+ type testStruct struct {
+ XMLName xml.Name `xml:"test"`
+ Value recurly.NullFloat `xml:"f"`
+ }
+
+ t.Run("Encode", func(t *testing.T) {
+ for i, tt := range []struct {
+ value recurly.NullFloat
+ expect string
+ }{
+ {value: recurly.NewFloat(12.34), expect: `12.34`},
+ {value: recurly.NewFloat(0.0000), expect: `0`},
+ {value: recurly.NewFloat(-12.34), expect: `-12.34`},
+ {value: recurly.NewFloat(-0.01), expect: `-0.01`},
+ {value: recurly.NewFloat(0.009), expect: `0.009`},
+ {expect: ``}, // zero value
+ } {
+ if aXml, err := xml.Marshal(testStruct{Value: tt.value}); err != nil {
+ t.Fatalf("%d %#v", i, err)
+ } else if string(aXml) != tt.expect {
+ t.Fatalf("%d %s", i, string(aXml))
+ }
+ }
+ })
+
+ t.Run("Decode", func(t *testing.T) {
+ for i, tt := range []struct {
+ expect recurly.NullFloat
+ input string
+ }{
+ {expect: recurly.NewFloat(12.34), input: `12.34`},
+ {expect: recurly.NewFloat(0), input: `0`},
+ {expect: recurly.NewFloat(-12.34), input: `-12.34`},
+ {expect: recurly.NewFloat(-0.01), input: `-0.01`},
+ {expect: recurly.NewFloat(0.009), input: `0.009`},
+ {input: ``}, // zero value
+ } {
+ var dst testStruct
+ if err := xml.Unmarshal([]byte(tt.input), &dst); err != nil {
+ t.Fatalf("%d %#v", i, err)
+ } else if diff := cmp.Diff(testStruct{XMLName: xml.Name{Local: "test"}, Value: tt.expect}, dst); diff != "" {
+ t.Fatalf("%d %s", i, diff)
+ }
+ }
+ })
+}
+
+func TestXML_NullFloatPtr(t *testing.T) {
+ f := recurly.NewFloatPtr(nil)
+ if value, ok := f.Value(); ok {
+ t.Fatal("expected ok to be false")
+ } else if value != 0 {
+ t.Fatalf("unexpected value: %f", value)
+ }
+
+ floatVal := 0.07
+ f = recurly.NewFloatPtr(&floatVal)
+ if value, ok := f.Value(); !ok {
+ t.Fatal("expected ok to be true")
+ } else if value != 0.07 {
+ t.Fatalf("unexpected value: %f", value)
+ }
+}
+
func TestXML_NullTime(t *testing.T) {
t.Run("ZeroValue", func(t *testing.T) {
var rt recurly.NullTime