diff options
| author | Sam Anthony <sam@samanthony.xyz> | 2026-05-12 15:35:06 -0400 |
|---|---|---|
| committer | Sam Anthony <sam@samanthony.xyz> | 2026-05-12 15:37:29 -0400 |
| commit | 9010ebe8a581fb9db7bc6e97d40ff062fb18495f (patch) | |
| tree | 4301a455762a59d4951507c8a8781e99c6f91c6d /ship.go | |
| parent | 329257be8d9fb05d3dcea49823acea0f878ed52c (diff) | |
| download | lulu-9010ebe8a581fb9db7bc6e97d40ff062fb18495f.zip | |
unmarshal GET /print-jobs response
Diffstat (limited to 'ship.go')
| -rw-r--r-- | ship.go | 263 |
1 files changed, 148 insertions, 115 deletions
@@ -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 -} |