aboutsummaryrefslogtreecommitdiffstats
path: root/ship.go
diff options
context:
space:
mode:
Diffstat (limited to 'ship.go')
-rw-r--r--ship.go263
1 files changed, 148 insertions, 115 deletions
diff --git a/ship.go b/ship.go
index d76661a..0cfd733 100644
--- a/ship.go
+++ b/ship.go
@@ -3,14 +3,10 @@ package lulu
import (
"encoding/json"
"fmt"
- email "net/mail"
- "regexp"
)
//go:generate go run github.com/yawnak/string-enumer -t ShippingLevel -t Title --text -o ./ship_gen.go .
-var phoneExpr = regexp.MustCompile(`^\+?[\d\s\-.\/()]{8,20}$`)
-
// ShippingLevel is the quality/speed with which a package is shipped.
type ShippingLevel string
@@ -22,99 +18,174 @@ const (
Express ShippingLevel = "EXPRESS" // Overnight delivery. Fastest shipping available.
)
-type ShippingAddress struct {
- Country string // ISO 3166-2 country code
- State string // 2 or 3 letter state codes (officially called ISO-3166-2 subdivision codes). They are required for some countries (e.g. US, MX, CA, AU)
- City string
- Street1 string // First address line
- Street2 string // Second address line
- PostCode string // Required for most countries
- IsBusiness bool // Only relevant for US addresses. Some US carriers don't deliver to business-addresses on Saturday.
- Name string // Full name of the person, including first and last name.
- Title Title
- Organization string // Name of an organization. Required if no person name is given.
- Email *email.Address // Shipping carriers require an email address for notifications or handling delivery issues. If no email is given, the default email in the user profile will be used.
- Phone PhoneNumber // Shipping carriers require a phone number for handling delivery issues. If no phone number is given, the default in the API user profile will be used.
- TaxId string // The recipient’s tax identification number. Required for shipping addresses to Brazil, Chile, and Mexico.
-}
+type Title string
+
+const (
+ Mr Title = "MR"
+ Miss Title = "MISS"
+ Mrs Title = "MRS"
+ Ms Title = "MS"
+ Dr Title = "DR"
+)
-// They use "country" in some places, "country_code" in others, etc.
-// This is the union of all of them.
-type shippingAddress struct {
- Country string `json:"country"`
- CountryCode string `json:"country_code"`
- State string `json:"state"`
- StateCode string `json:"state_code"`
- City string `json:"city"`
- Street1 string `json:"street1"`
- Street2 string `json:"street2"`
- PostCode string `json:"postcode"`
- IsBusiness bool `json:"is_business"`
- Name string `json:"name"`
- FirstName string `json:"first_name"`
- LastName string `json:"last_name"`
- Title Title `json:"title"`
- Organization string `json:"organization"`
- Email string `json:"email"`
- Phone PhoneNumber `json:"phone_number"`
- TaxId string `json:"recipient_tax_id"`
+type ShippingAddress struct {
+ Country string // ISO 3166-2 country code
+
+ // 2 or 3 letter state codes (officially called ISO-3166-2
+ // subdivision codes). They are required for some countries (e.g.
+ // US, MX, CA, AU).
+ State string
+
+ City string
+ Street1 string // First address line
+ Street2 string // Second address line
+ PostCode string // Required for most countries
+
+ // Only relevant for US addresses. Some US carriers don't deliver
+ // to business-addresses on Saturday.
+ IsBusiness bool
+
+ // Full name of the person, including first and last name.
+ Name string
+ Title Title
+
+ // Name of an organization. Required if no person name is given.
+ Organization string
+
+ // Shipping carriers require an email address for notifications
+ // or handling delivery issues. If no email is given, the default
+ // email in the user profile will be used.
+ Email EmailAddress
+
+ // Shipping carriers require a phone number for handling delivery
+ // issues. If no phone number is given, the default in the API
+ // user profile will be used.
+ Phone PhoneNumber
+
+ // The recipient's tax identification number. Required for
+ // shipping addresses to Brazil, Chile, and Mexico.
+ TaxId string
}
func (a *ShippingAddress) UnmarshalJSON(data []byte) error {
- s := string(data)
- var alias shippingAddress
+ var alias extShippingAddress
if err := json.Unmarshal(data, &alias); err != nil {
return err
}
+ addr, err := alias.addr()
+ if err != nil {
+ return err
+ }
+ *a = addr
+ return nil
+}
+
+type ShippingAddressValidation struct {
+ Address ShippingAddress // The original address that was given to the server.
+ Suggested ShippingAddress // The address that the server recommends.
+ Warnings []ShippingAddressWarning
+}
- if country, both := either(alias.Country, alias.CountryCode); !both {
- a.Country = country
- } else {
- return fmt.Errorf(`address contains both "country" and "country_code": %q`, s)
+func (av *ShippingAddressValidation) UnmarshalJSON(data []byte) error {
+ var alias extShippingAddress
+ if err := json.Unmarshal(data, &alias); err != nil {
+ return err
}
+ addr, err := alias.addr()
+ if err != nil {
+ return err
+ }
+ av.Address = addr
+ av.Suggested = alias.Suggested
+ av.Warnings = alias.Warnings
+ return nil
+}
+
+type ShippingAddressWarning struct {
+ Type string `json:"type"` // eg "validation_warning"
+ Path string `json:"path"` // eg "external"
+ Code string `json:"code"` // eg "REPLACED"
+ Msg string `json:"message"` // eg "street1: Holstenstr. 40 -> Holstenstraße 40"
+}
+
+// extShippingAddress is the extended shipping address that is seen in
+// some responses. It includes "warnings" and "suggested_address". It
+// also unmarshals either "country" or "country_code" etc. to account for
+// the irregularities in the API.
+type extShippingAddress struct {
+ Country string `json:"country"`
+ CountryCode string `json:"country_code"`
+
+ State string `json:"state"`
+ StateCode string `json:"state_code"`
+
+ City string `json:"city"`
+ Street1 string `json:"street1"`
+ Street2 string `json:"street2"`
+ PostCode string `json:"postcode"`
+ IsBusiness bool `json:"is_business"`
+
+ Name string `json:"name"`
+ FirstName string `json:"first_name"`
+ LastName string `json:"last_name"`
+
+ Title Title `json:"title"`
+ Organization string `json:"organization"`
+ Email EmailAddress `json:"email"`
+ Phone PhoneNumber `json:"phone_number"`
+ TaxId string `json:"recipient_tax_id"`
+
+ Warnings []ShippingAddressWarning `json:"warnings"`
+ Suggested ShippingAddress `json:"suggested_address"`
+}
- if state, both := either(alias.State, alias.StateCode); !both {
- a.State = state
- } else {
- return fmt.Errorf(`address contains both "state" and "state_code": %q`, s)
+// addr takes the disjunctive union of the API's irregular fields
+// ("country"/"country_code", etc.) and returns only those fields of the
+// actual shipping address: excluding the suggested address and warnings.
+// It returns error if two or more of the "same field" are present, eg.
+// both "country" and "country_code".
+func (ext extShippingAddress) addr() (ShippingAddress, error) {
+ country, both := either(ext.Country, ext.CountryCode)
+ if both {
+ return ShippingAddress{}, fmt.Errorf(`address contains both "country" and "country_code"`)
}
- a.City = alias.City
- a.Street1 = alias.Street1
- a.Street2 = alias.Street2
- a.PostCode = alias.PostCode
- a.IsBusiness = alias.IsBusiness
+ state, both := either(ext.State, ext.StateCode)
+ if both {
+ return ShippingAddress{}, fmt.Errorf(`address contains both "state" and "state_code"`)
+ }
- hasName := alias.Name != ""
- hasFirst := alias.FirstName != ""
- hasLast := alias.LastName != ""
+ var name string
+ hasName := ext.Name != ""
+ hasFirst := ext.FirstName != ""
+ hasLast := ext.LastName != ""
if hasName && (hasFirst || hasLast) {
- return fmt.Errorf(`address contains both "name" and {"first_name", "last_name"}: %q`, s)
+ return ShippingAddress{}, fmt.Errorf(`address contains both "name" and {"first_name", "last_name"}`)
} else if hasName {
- a.Name = alias.Name
+ name = ext.Name
} else if hasFirst && hasLast {
- a.Name = fmt.Sprintf("%s %s", alias.FirstName, alias.LastName)
+ name = fmt.Sprintf("%s %s", ext.FirstName, ext.LastName)
} else if hasLast {
- a.Name = alias.LastName
+ name = ext.LastName
} else if hasFirst {
- a.Name = alias.FirstName
- }
-
- a.Title = alias.Title
- a.Organization = alias.Organization
-
- if alias.Email != "" {
- addr, err := email.ParseAddress(alias.Email)
- if err != nil {
- return err
- }
- a.Email = addr
+ name = ext.FirstName
}
- a.Phone = alias.Phone
- a.TaxId = alias.TaxId
-
- return nil
+ return ShippingAddress{
+ Country: country,
+ State: state,
+ City: ext.City,
+ Street1: ext.Street1,
+ Street2: ext.Street2,
+ PostCode: ext.PostCode,
+ IsBusiness: ext.IsBusiness,
+ Name: name,
+ Title: ext.Title,
+ Organization: ext.Organization,
+ Email: ext.Email,
+ Phone: ext.Phone,
+ TaxId: ext.TaxId,
+ }, nil
}
func either(a, b string) (string, bool) {
@@ -125,41 +196,3 @@ func either(a, b string) (string, bool) {
}
return b, false
}
-
-type Title string
-
-const (
- Mr Title = "MR"
- Miss Title = "MISS"
- Mrs Title = "MRS"
- Ms Title = "MS"
- Dr Title = "DR"
-)
-
-type PhoneNumber string
-
-func ParsePhoneNumber(s string) (PhoneNumber, error) {
- if phoneExpr.MatchString(s) {
- return PhoneNumber(s), nil
- }
- return "", fmt.Errorf("malformed phone number %q; must fit pattern `%s`", s, phoneExpr.String())
-}
-
-// MustParsePhoneNumber is like ParsePhoneNumber but panics if the phone
-// number cannot be parsed.
-func MustParsePhoneNumber(s string) PhoneNumber {
- n, err := ParsePhoneNumber(s)
- if err != nil {
- panic(fmt.Sprintf("lulu: ParsePhoneNumber(%q): %v", s, err))
- }
- return n
-}
-
-func (n *PhoneNumber) UnmarshalText(text []byte) error {
- pn, err := ParsePhoneNumber(string(text))
- if err != nil {
- return err
- }
- *n = pn
- return nil
-}