Skip to content

Commit 5baa722

Browse files
committed
Provide client and DTOs for US Extract API.
1 parent 80b35ef commit 5baa722

File tree

6 files changed

+323
-0
lines changed

6 files changed

+323
-0
lines changed

us-extract-api/client.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package extract
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
7+
"github.com/smartystreets/smartystreets-go-sdk"
8+
)
9+
10+
// Client is responsible for sending requests to the us-extract-api.
11+
type Client struct {
12+
sender sdk.RequestSender
13+
}
14+
15+
// NewClient creates a client with the provided sender.
16+
func NewClient(sender sdk.RequestSender) *Client {
17+
return &Client{sender: sender}
18+
}
19+
20+
// SendBatch sends the batch of inputs, populating the output for each input if the batch was successful.
21+
func (c *Client) SendLookup(lookup *Lookup) error {
22+
if lookup == nil || len(lookup.Text) == 0 {
23+
return nil
24+
} else if response, err := c.sender.Send(buildRequest(lookup)); err != nil {
25+
return err
26+
} else {
27+
return deserializeResponse(response, lookup)
28+
}
29+
}
30+
31+
func deserializeResponse(response []byte, lookup *Lookup) error {
32+
var extraction Result
33+
err := json.Unmarshal(response, &extraction)
34+
if err != nil {
35+
return err
36+
}
37+
lookup.Result = &extraction
38+
return nil
39+
}
40+
41+
func buildRequest(lookup *Lookup) *http.Request {
42+
request, _ := http.NewRequest("POST", extractURL, nil) // We control the method and the URL. This is safe.
43+
lookup.populate(request)
44+
return request
45+
}
46+
47+
const extractURL = "/" // Remaining parts will be completed later by the sdk.BaseURLClient.

us-extract-api/client_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package extract
2+
3+
import (
4+
"errors"
5+
"net/http"
6+
"testing"
7+
8+
"github.com/smartystreets/assertions/should"
9+
"github.com/smartystreets/gunit"
10+
)
11+
12+
func TestClientFixture(t *testing.T) {
13+
gunit.Run(new(ClientFixture), t)
14+
}
15+
16+
type ClientFixture struct {
17+
*gunit.Fixture
18+
19+
sender *FakeSender
20+
client *Client
21+
22+
input *Lookup
23+
}
24+
25+
func (f *ClientFixture) Setup() {
26+
f.sender = &FakeSender{}
27+
f.client = NewClient(f.sender)
28+
f.input = new(Lookup)
29+
}
30+
31+
func (f *ClientFixture) TestLookupSerializedAndSent__ResponseSuggestionsIncorporatedIntoLookup() {
32+
f.sender.response = `{"meta": {"lines": 42}}`
33+
f.input.Text = "42"
34+
35+
err := f.client.SendLookup(f.input)
36+
37+
f.So(err, should.BeNil)
38+
f.So(f.sender.request, should.NotBeNil)
39+
f.So(f.sender.request.Method, should.Equal, "POST")
40+
f.So(f.sender.request.URL.Path, should.Equal, extractURL)
41+
f.So(readBody(f.sender.request), should.Equal, "42")
42+
43+
f.So(f.input.Result, should.Resemble, &Result{Metadata: Metadata{Lines: 42}})
44+
}
45+
46+
func (f *ClientFixture) TestNilLookupNOP() {
47+
err := f.client.SendLookup(nil)
48+
f.So(err, should.BeNil)
49+
f.So(f.sender.request, should.BeNil)
50+
}
51+
52+
func (f *ClientFixture) TestEmptyLookup_NOP() {
53+
err := f.client.SendLookup(new(Lookup))
54+
f.So(err, should.BeNil)
55+
f.So(f.sender.request, should.BeNil)
56+
}
57+
58+
func (f *ClientFixture) TestSenderErrorPreventsDeserialization() {
59+
f.sender.err = errors.New("GOPHERS!")
60+
f.sender.response = `{"meta": {"lines": 42}}` // would be deserialized if not for the err (above)
61+
f.input.Text = "HI"
62+
63+
err := f.client.SendLookup(f.input)
64+
65+
f.So(err, should.NotBeNil)
66+
f.So(f.input.Result, should.BeNil)
67+
}
68+
69+
func (f *ClientFixture) TestDeserializationErrorPreventsDeserialization() {
70+
f.sender.response = `I can't haz JSON`
71+
f.input.Text = "HI"
72+
73+
err := f.client.SendLookup(f.input)
74+
75+
f.So(err, should.NotBeNil)
76+
f.So(f.input.Result, should.BeNil)
77+
}
78+
79+
/*////////////////////////////////////////////////////////////////////////*/
80+
81+
type FakeSender struct {
82+
callCount int
83+
request *http.Request
84+
85+
response string
86+
err error
87+
}
88+
89+
func (f *FakeSender) Send(request *http.Request) ([]byte, error) {
90+
f.callCount++
91+
f.request = request
92+
return []byte(f.response), f.err
93+
}

us-extract-api/extraction.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package extract
2+
3+
import "github.com/smartystreets/smartystreets-go-sdk/us-street-api"
4+
5+
// Result, Metadata, and ExtractedAddress represent all output fields documented here:
6+
// https://smartystreets.com/docs/cloud/us-extract-api#http-response
7+
type Result struct {
8+
Metadata Metadata `json:"meta"`
9+
Addresses []ExtractedAddress `json:"addresses"`
10+
}
11+
12+
type Metadata struct {
13+
Lines int `json:"lines"`
14+
Characters int `json:"character_count"`
15+
Bytes int `json:"bytes"`
16+
Addresses int `json:"address_count"`
17+
VerifiedAddresses int `json:"verified_count"`
18+
ContainsNonASCIIUnicode bool `json:"unicode"`
19+
}
20+
21+
type ExtractedAddress struct {
22+
Text string `json:"text"`
23+
Verified bool `json:"verified"`
24+
Line int `json:"line"`
25+
Start int `json:"start"`
26+
End int `json:"end"`
27+
APIOutput []street.Candidate `json:"api_output"`
28+
}

us-extract-api/lookup.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package extract
2+
3+
import (
4+
"io/ioutil"
5+
"net/http"
6+
"strconv"
7+
"strings"
8+
)
9+
10+
// Lookup represents all input fields documented here:
11+
// https://smartystreets.com/docs/cloud/us-extract-api#http-request-input-fields
12+
type Lookup struct {
13+
Text string
14+
15+
HTML htmlPayload
16+
Aggressive bool
17+
AddressesWithLineBreaks bool
18+
AddressesPerLine int
19+
20+
Result *Result
21+
}
22+
23+
type htmlPayload string
24+
25+
const (
26+
HTMLUnspecified htmlPayload = "" // Indicates that the server may decide if Lookup.Text is HTML or not.
27+
HTMLYes htmlPayload = "true" // Indicates that the Lookup.Text is known to be HTML.
28+
HTMLNo htmlPayload = "false" // Indicates that the Lookup.Text is known to NOT be HTML.
29+
)
30+
31+
func (l *Lookup) populate(request *http.Request) {
32+
l.setQuery(request)
33+
l.setBody(request)
34+
l.setHeaders(request)
35+
}
36+
func (l *Lookup) setQuery(request *http.Request) {
37+
query := request.URL.Query()
38+
39+
if l.HTML != HTMLUnspecified {
40+
query.Set("html", string(l.HTML))
41+
}
42+
43+
if l.Aggressive {
44+
query.Set("aggressive", "true")
45+
}
46+
47+
if l.AddressesWithLineBreaks {
48+
query.Set("addr_line_breaks", "true")
49+
}
50+
51+
if l.AddressesPerLine > 0 {
52+
query.Set("addr_per_line", strconv.Itoa(l.AddressesPerLine))
53+
}
54+
55+
request.URL.RawQuery = query.Encode()
56+
}
57+
func (l *Lookup) setBody(request *http.Request) {
58+
if len(l.Text) == 0 {
59+
return
60+
}
61+
62+
body := strings.NewReader(l.Text)
63+
request.Body = ioutil.NopCloser(body)
64+
request.ContentLength = int64(body.Len())
65+
}
66+
func (l *Lookup) setHeaders(request *http.Request) {
67+
request.Header.Set("Content-Type", "text/plain")
68+
}

us-extract-api/lookup_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package extract
2+
3+
import (
4+
"io/ioutil"
5+
"net/http"
6+
"net/url"
7+
"testing"
8+
9+
"github.com/smartystreets/assertions/should"
10+
"github.com/smartystreets/gunit"
11+
)
12+
13+
func TestLookupFixture(t *testing.T) {
14+
gunit.Run(new(LookupFixture), t)
15+
}
16+
17+
type LookupFixture struct {
18+
*gunit.Fixture
19+
20+
lookup *Lookup
21+
request *http.Request
22+
}
23+
24+
func (f *LookupFixture) Setup() {
25+
f.lookup = new(Lookup)
26+
f.request, _ = http.NewRequest("POST", "/?hello=world", nil)
27+
}
28+
29+
func (f *LookupFixture) query() url.Values {
30+
return f.request.URL.Query()
31+
}
32+
33+
func readBody(request *http.Request) string {
34+
bytes, _ := ioutil.ReadAll(request.Body)
35+
return string(bytes)
36+
}
37+
38+
func (f *LookupFixture) TestPopulate_TextCopiedToBody() {
39+
body := "Hello, World!"
40+
f.lookup.Text = body
41+
42+
f.lookup.populate(f.request)
43+
44+
f.So(readBody(f.request), should.Equal, body)
45+
f.So(f.request.ContentLength, should.Equal, len(body))
46+
f.So(f.request.Header.Get("Content-Type"), should.Equal, "text/plain")
47+
}
48+
49+
func (f *LookupFixture) TestPopulate_NothingSet_NothingAddedToQueryStringOrBody() {
50+
f.lookup.populate(f.request)
51+
f.So(f.query().Encode(), should.Equal, "hello=world")
52+
f.So(f.request.Body, should.BeNil)
53+
f.So(f.request.ContentLength, should.Equal, 0)
54+
}
55+
56+
func (f *LookupFixture) TestPopulate_HTMLYes() {
57+
f.lookup.HTML = HTMLYes
58+
f.lookup.populate(f.request)
59+
f.So(f.query().Get("html"), should.Equal, "true")
60+
}
61+
62+
func (f *LookupFixture) TestPopulate_HTMLNo() {
63+
f.lookup.HTML = HTMLNo
64+
f.lookup.populate(f.request)
65+
f.So(f.query().Get("html"), should.Equal, "false")
66+
}
67+
68+
func (f *LookupFixture) TestPopulate_SimpleParameters_Set() {
69+
f.lookup.AddressesPerLine = 42
70+
f.lookup.AddressesWithLineBreaks = true
71+
f.lookup.Aggressive = true
72+
73+
f.lookup.populate(f.request)
74+
75+
f.So(f.query().Get("aggressive"), should.Equal, "true")
76+
f.So(f.query().Get("addr_line_breaks"), should.Equal, "true")
77+
f.So(f.query().Get("addr_per_line"), should.Equal, "42")
78+
}

wireup/builder.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
sdk "github.com/smartystreets/smartystreets-go-sdk"
1010
internal "github.com/smartystreets/smartystreets-go-sdk/internal/sdk"
1111
"github.com/smartystreets/smartystreets-go-sdk/us-autocomplete-api"
12+
"github.com/smartystreets/smartystreets-go-sdk/us-extract-api"
1213
"github.com/smartystreets/smartystreets-go-sdk/us-street-api"
1314
"github.com/smartystreets/smartystreets-go-sdk/us-zipcode-api"
1415
)
@@ -128,6 +129,13 @@ func (b *ClientBuilder) BuildUSAutocompleteAPIClient() *autocomplete.Client {
128129
return autocomplete.NewClient(b.buildHTTPSender())
129130
}
130131

132+
// BuildUSExtractAPIClient builds the us-extract-api client using the provided
133+
// configuration details provided by other methods on the ClientBuilder.
134+
func (b *ClientBuilder) BuildUSExtractAPIClient() *extract.Client {
135+
b.ensureBaseURLNotNil(defaultBaseURL_USExtractAPI)
136+
return extract.NewClient(b.buildHTTPSender())
137+
}
138+
131139
func (b *ClientBuilder) ensureBaseURLNotNil(u *url.URL) {
132140
if b.baseURL == nil {
133141
b.baseURL = u
@@ -158,4 +166,5 @@ var (
158166
defaultBaseURL_USStreetAPI, _ = url.Parse("https://us-street.api.smartystreets.com")
159167
defaultBaseURL_USZIPCodeAPI, _ = url.Parse("https://us-zipcode.api.smartystreets.com")
160168
defaultBaseURL_USAutocompleteAPI, _ = url.Parse("https://us-autocomplete.api.smartystreets.com")
169+
defaultBaseURL_USExtractAPI, _ = url.Parse("https://us-extract.api.smartystreets.com")
161170
)

0 commit comments

Comments
 (0)