Skip to content

Commit

Permalink
Merge branch 'bryan/enrichment-lookup'
Browse files Browse the repository at this point in the history
  • Loading branch information
Bryan Amundson committed Feb 1, 2024
2 parents 591644a + 323d1db commit 6248853
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 47 deletions.
16 changes: 15 additions & 1 deletion examples/us-enrichment-api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package main
import (
"fmt"
"log"
"net/http"
"os"

us_enrichment "github.com/smartystreets/smartystreets-go-sdk/us-enrichment-api"
"github.com/smartystreets/smartystreets-go-sdk/wireup"
)

Expand All @@ -26,9 +28,21 @@ func main() {

smartyKey := "1682393594"

err, results := client.SendPropertyPrincipalLookup(smartyKey)
lookup := us_enrichment.Lookup{
SmartyKey: smartyKey,
Include: "group_structural,sale_date", // optional: only include these attributes in the returned data
Exclude: "", // optional: exclude attributes from the returned data
ETag: "", // optional: check if the record has been updated
}

err, results := client.SendPropertyPrincipal(&lookup)

if err != nil {
// If ETag was supplied in the lookup, this status will be returned if the ETag value for the record is current
if client.IsHTTPErrorCode(err, http.StatusNotModified) {
log.Printf("Record has not been modified since the last request")
return
}
log.Fatal("Error sending lookup:", err)
}

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/smartystreets/smartystreets-go-sdk

go 1.13
go 1.21

require (
github.com/smarty/assertions v1.15.1
Expand Down
10 changes: 10 additions & 0 deletions internal/sdk/http_sender.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package sdk

import (
"io"
"maps"
"net/http"

"github.com/smartystreets/smartystreets-go-sdk"
Expand Down Expand Up @@ -30,6 +31,7 @@ func (s *HTTPSender) Send(request *http.Request) ([]byte, error) {
} else if content, err := readResponseBody(response); err != nil {
return content, err
} else {
request.Response = response // make headers available in the request
return interpret(response, content)
}
}
Expand All @@ -52,3 +54,11 @@ func interpret(response *http.Response, content []byte) ([]byte, error) {
}
return nil, sdk.NewHTTPStatusError(response.StatusCode, content)
}

func interpretAndReturnHeaders(response *http.Response, content []byte, headers http.Header) ([]byte, http.Header, error) {
if response.StatusCode == http.StatusOK {
maps.Copy(headers, response.Header)
return content, headers, nil
}
return nil, nil, sdk.NewHTTPStatusError(response.StatusCode, content)
}
3 changes: 3 additions & 0 deletions internal/sdk/retry_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ func (r *RetryClient) handleHttpStatusCode(response *http.Response, attempt *int
if response.StatusCode >= http.StatusBadRequest && response.StatusCode <= http.StatusUnprocessableEntity {
return false
}
if response.StatusCode == http.StatusNotModified {
return false
}
if response.StatusCode == http.StatusTooManyRequests {
r.sleeper(time.Second * time.Duration(r.random(backOffRateLimit)))
// Setting attempt to 1 will make 429s retry indefinitely; this is intended behavior.
Expand Down
53 changes: 35 additions & 18 deletions us-enrichment-api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,33 +16,35 @@ func NewClient(sender sdk.RequestSender) *Client {
return &Client{sender: sender}
}

// Deprecated: SendPropertyFinancialLookup is deprecated. Use SendPropertyFinancial
func (c *Client) SendPropertyFinancialLookup(smartyKey string) (error, []*FinancialResponse) {
l := &financialLookup{
SmartyKey: smartyKey,
}
err := c.sendLookup(l)

return err, l.Response
return c.SendPropertyFinancial(&Lookup{SmartyKey: smartyKey})
}
func (c *Client) SendPropertyFinancial(lookup *Lookup) (error, []*FinancialResponse) {
propertyLookup := &financialLookup{Lookup: lookup}
err := c.sendLookup(propertyLookup)
return err, propertyLookup.Response
}

// Deprecated: SendPropertyFinancialLookup is deprecated. Use SendPropertyPrincipal
func (c *Client) SendPropertyPrincipalLookup(smartyKey string) (error, []*PrincipalResponse) {
l := &principalLookup{
SmartyKey: smartyKey,
}
err := c.sendLookup(l)

return err, l.Response
return c.SendPropertyPrincipal(&Lookup{SmartyKey: smartyKey})
}
func (c *Client) SendPropertyPrincipal(lookup *Lookup) (error, []*PrincipalResponse) {
propertyLookup := &principalLookup{Lookup: lookup}
err := c.sendLookup(propertyLookup)
return err, propertyLookup.Response
}

func (c *Client) sendLookup(lookup enrichmentLookup) error {
return c.sendLookupWithContext(context.Background(), lookup)
}

func (c *Client) sendLookupWithContext(ctx context.Context, lookup enrichmentLookup) error {
if lookup == nil {
if lookup == nil || lookup.getLookup() == nil {
return nil
}
if len(lookup.GetSmartyKey()) == 0 {
if len(lookup.getSmartyKey()) == 0 {
return nil
}

Expand All @@ -54,25 +56,40 @@ func (c *Client) sendLookupWithContext(ctx context.Context, lookup enrichmentLoo
return err
}

return lookup.UnmarshalResponse(response)
var headers http.Header
if request.Response != nil {
headers = request.Response.Header
}

return lookup.unmarshalResponse(response, headers)
}

func (c *Client) IsHTTPErrorCode(err error, code int) bool {
if serr, ok := err.(*sdk.HTTPStatusError); ok && serr.StatusCode() == code {
return true
}
return false
}

func buildRequest(lookup enrichmentLookup) *http.Request {
request, _ := http.NewRequest("GET", buildLookupURL(lookup), nil) // We control the method and the URL. This is safe.
query := request.URL.Query()
lookup.populate(query)
request.Header.Add(lookupETagHeader, lookup.getLookup().ETag)
request.URL.RawQuery = query.Encode()
return request
}

func buildLookupURL(lookup enrichmentLookup) string {
newLookupURL := strings.Replace(lookupURL, lookupURLSmartyKey, lookup.GetSmartyKey(), 1)
newLookupURL = strings.Replace(newLookupURL, lookupURLDataSet, lookup.GetDataSet(), 1)
return strings.Replace(newLookupURL, lookupURLDataSubSet, lookup.GetDataSubset(), 1)
newLookupURL := strings.Replace(lookupURL, lookupURLSmartyKey, lookup.getSmartyKey(), 1)
newLookupURL = strings.Replace(newLookupURL, lookupURLDataSet, lookup.getDataSet(), 1)
return strings.Replace(newLookupURL, lookupURLDataSubSet, lookup.getDataSubset(), 1)
}

const (
lookupURLSmartyKey = ":smartykey"
lookupURLDataSet = ":dataset"
lookupURLDataSubSet = ":datasubset"
lookupURL = "/lookup/" + lookupURLSmartyKey + "/" + lookupURLDataSet + "/" + lookupURLDataSubSet // Remaining parts will be completed later by the sdk.BaseURLClient.
lookupETagHeader = "Etag"
)
12 changes: 6 additions & 6 deletions us-enrichment-api/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,15 @@ func (f *ClientFixture) Setup() {
func (f *ClientFixture) TestLookupSerializedAndSentWithContext__ResponseSuggestionsIncorporatedIntoLookup() {
smartyKey := "123"
f.sender.response = validFinancialResponse
f.input = &financialLookup{
SmartyKey: smartyKey,
}
f.input = &financialLookup{Lookup: &Lookup{SmartyKey: smartyKey}}

ctx := context.WithValue(context.Background(), "key", "value")
err := f.client.sendLookupWithContext(ctx, f.input)

f.So(err, should.BeNil)
f.So(f.sender.request, should.NotBeNil)
f.So(f.sender.request.Method, should.Equal, "GET")
f.So(f.sender.request.URL.Path, should.Equal, "/lookup/"+smartyKey+"/"+f.input.GetDataSet()+"/"+f.input.GetDataSubset())
f.So(f.sender.request.URL.Path, should.Equal, "/lookup/"+smartyKey+"/"+f.input.getDataSet()+"/"+f.input.getDataSubset())
f.So(f.sender.request.Context(), should.Resemble, ctx)

response := f.input.(*financialLookup).Response
Expand All @@ -57,6 +55,7 @@ func (f *ClientFixture) TestLookupSerializedAndSentWithContext__ResponseSuggesti
VeteranTaxExemption: "Veteran_Tax_Exemption",
WidowTaxExemption: "Widow_Tax_Exemption",
},
Etag: "ABCDEFG",
},
})
}
Expand All @@ -76,7 +75,7 @@ func (f *ClientFixture) TestEmptyLookup_NOP() {
func (f *ClientFixture) TestSenderErrorPreventsDeserialization() {
f.sender.err = errors.New("gophers")
f.sender.response = validPrincipalResponse // would be deserialized if not for the err (above)
f.input = &principalLookup{SmartyKey: "12345"}
f.input = &principalLookup{Lookup: &Lookup{SmartyKey: "12345"}}

err := f.client.sendLookup(f.input)

Expand All @@ -86,7 +85,7 @@ func (f *ClientFixture) TestSenderErrorPreventsDeserialization() {

func (f *ClientFixture) TestDeserializationErrorPreventsDeserialization() {
f.sender.response = `I can't haz JSON`
f.input = &principalLookup{SmartyKey: "12345"}
f.input = &principalLookup{Lookup: &Lookup{SmartyKey: "12345"}}

err := f.client.sendLookup(f.input)

Expand All @@ -110,5 +109,6 @@ type FakeSender struct {
func (f *FakeSender) Send(request *http.Request) ([]byte, error) {
f.callCount++
f.request = request
f.request.Response = &http.Response{Header: http.Header{"Etag": []string{"ABCDEFG"}}}
return []byte(f.response), f.err
}
115 changes: 94 additions & 21 deletions us-enrichment-api/lookup.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,121 @@ package us_enrichment

import (
"encoding/json"
"net/http"
"net/url"
)

type enrichmentLookup interface {
GetSmartyKey() string
GetDataSet() string
GetDataSubset() string
type Lookup struct {
SmartyKey string
Include string
Exclude string
ETag string
}

UnmarshalResponse([]byte) error
type enrichmentLookup interface {
getSmartyKey() string
getDataSet() string
getDataSubset() string
getLookup() *Lookup
getResponse() interface{}
unmarshalResponse([]byte, http.Header) error
populate(query url.Values)
}

////////////////////////////////////////////////////////////////////////////////////////

type financialLookup struct {
SmartyKey string
Response []*FinancialResponse
Lookup *Lookup
Response []*FinancialResponse
}

func (f *financialLookup) GetSmartyKey() string {
return f.SmartyKey
func (f *financialLookup) getSmartyKey() string {
return f.Lookup.SmartyKey
}

func (f *financialLookup) GetDataSet() string {
func (f *financialLookup) getDataSet() string {
return propertyDataSet
}

func (f *financialLookup) GetDataSubset() string {
func (f *financialLookup) getDataSubset() string {
return financialDataSubset
}

func (f *financialLookup) UnmarshalResponse(bytes []byte) error {
return json.Unmarshal(bytes, &f.Response)
func (f *financialLookup) getLookup() *Lookup {
return f.Lookup
}

func (f *financialLookup) getResponse() interface{} {
return f.Response
}

func (f *financialLookup) unmarshalResponse(bytes []byte, headers http.Header) error {
if err := json.Unmarshal(bytes, &f.Response); err != nil {
return err
}

if headers != nil {
if etag, found := headers[lookupETagHeader]; found {
if len(etag) > 0 && len(f.Response) > 0 {
f.Response[0].Etag = etag[0]
}
}
}

return nil
}

func (e *financialLookup) populate(query url.Values) {
e.Lookup.populateInclude(query)
e.Lookup.populateExclude(query)
}

////////////////////////////////////////////////////////////////////////////////////////

type principalLookup struct {
SmartyKey string
Response []*PrincipalResponse
Lookup *Lookup
Response []*PrincipalResponse
}

func (p *principalLookup) GetSmartyKey() string {
return p.SmartyKey
func (p *principalLookup) getSmartyKey() string {
return p.Lookup.SmartyKey
}

func (p *principalLookup) GetDataSet() string {
func (p *principalLookup) getDataSet() string {
return propertyDataSet
}

func (p *principalLookup) GetDataSubset() string {
func (p *principalLookup) getDataSubset() string {
return principalDataSubset
}

func (p *principalLookup) UnmarshalResponse(bytes []byte) error {
return json.Unmarshal(bytes, &p.Response)
func (p *principalLookup) getLookup() *Lookup {
return p.Lookup
}

func (f *principalLookup) getResponse() interface{} {
return f.Response
}

func (p *principalLookup) unmarshalResponse(bytes []byte, headers http.Header) error {
if err := json.Unmarshal(bytes, &p.Response); err != nil {
return err
}

if headers != nil {
if etag, found := headers[lookupETagHeader]; found {
if len(etag) > 0 && len(p.Response) > 0 {
p.Response[0].Etag = etag[0]
}
}
}

return nil
}

func (e *principalLookup) populate(query url.Values) {
e.Lookup.populateInclude(query)
e.Lookup.populateExclude(query)
}

////////////////////////////////////////////////////////////////////////////////////////
Expand All @@ -65,3 +126,15 @@ const (
principalDataSubset = "principal"
propertyDataSet = "property"
)

func (l Lookup) populateInclude(query url.Values) {
if len(l.Include) > 0 {
query.Set("include", l.Include)
}
}

func (l Lookup) populateExclude(query url.Values) {
if len(l.Include) > 0 {
query.Set("exclude", l.Exclude)
}
}
Loading

0 comments on commit 6248853

Please sign in to comment.