Skip to content

Commit 624caf1

Browse files
committed
moved stats to non-pointers; added some unit tests; organized a bit of code to new files
1 parent 7c1cd25 commit 624caf1

11 files changed

+288
-85
lines changed

Makefile

+1-3
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,8 @@ vet:
1515

1616
.PHONY: test
1717
test:
18-
go test ./...
18+
go test -cover ./...
1919

2020
.PHONY: run-example
2121
run-example:
2222
go run example/main.go
23-
24-
# TODO: deal with coverfiles

client.go

+1-72
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,9 @@
33
package listennotes
44

55
import (
6-
"encoding/json"
7-
"fmt"
86
"net/http"
97
)
108

11-
// Base urls for access the available api endpoints
12-
const (
13-
BaseURLProduction = "https://listen-api.listennotes.com/api/v2"
14-
BaseURLTest = "https://listen-api-test.listennotes.com/api/v2"
15-
)
16-
17-
// Request header keys
18-
const (
19-
RequestHeaderKeyAPI = "X-ListenAPI-Key"
20-
)
21-
22-
// Reponse header keys
23-
const (
24-
ResponseHeaderKeyFreeQuota = "X-ListenAPI-FreeQuota"
25-
ResponseHeaderKeyUsage = "X-ListenAPI-Usage"
26-
ResponseHeaderKeyLatencySeconds = "X-listenAPI-Latency-Seconds"
27-
ResponseHeaderKeyNextBillingDate = "X-Listenapi-NextBillingDate"
28-
)
29-
30-
// TimeFormat is the string format of all response times
31-
const TimeFormat = "2006-01-02T15:04:05.000000+07:00"
32-
33-
var defaultHTTPClient *http.Client = &http.Client{}
34-
35-
// Response is the standard response for all client functions
36-
type Response struct {
37-
Stats ResponseStatistics
38-
Data map[string]interface{}
39-
}
40-
419
// HTTPClient is the client interface
4210
type HTTPClient interface {
4311
Search(args map[string]string) (*Response, error)
@@ -74,44 +42,5 @@ func NewClient(apiKey string, opts ...ClientOption) HTTPClient {
7442
}
7543

7644
func (c *standardHTTPClient) Search(args map[string]string) (*Response, error) {
77-
78-
// TODO: move all this common stuff to a function
79-
url := fmt.Sprintf("%s/search", c.baseURL)
80-
81-
req, err := http.NewRequest("GET", url, nil)
82-
if err != nil {
83-
return nil, fmt.Errorf("failed to create request: %w", err)
84-
}
85-
req.Header.Add(RequestHeaderKeyAPI, c.apiKey)
86-
87-
q := req.URL.Query()
88-
for k, v := range args {
89-
q.Add(k, v)
90-
}
91-
req.URL.RawQuery = q.Encode()
92-
93-
resp, err := c.httpClient.Do(req)
94-
if err != nil {
95-
return nil, fmt.Errorf("failed to executing request: %w", err)
96-
}
97-
defer resp.Body.Close()
98-
99-
// map any generic status code errors
100-
if mappedError, ok := errMap[resp.StatusCode]; ok && mappedError != nil {
101-
return nil, mappedError
102-
}
103-
104-
// generic body parsing
105-
var genericJSON map[string]interface{}
106-
if err := json.NewDecoder(resp.Body).Decode(&genericJSON); err != nil {
107-
return nil, fmt.Errorf("failed parsing the response: %w", err)
108-
}
109-
110-
// gather the header statistics
111-
stats := parseStats(resp)
112-
113-
return &Response{
114-
Stats: stats,
115-
Data: genericJSON,
116-
}, nil
45+
return c.execute(args, "search")
11746
}

client_test.go

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package listennotes_test
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"net"
7+
"net/http"
8+
"testing"
9+
10+
listennotes "github.com/ListenNotes/podcast-api-go"
11+
)
12+
13+
func TestMockURL(t *testing.T) {
14+
noDial := fmt.Errorf("no-dial")
15+
16+
httpClient := &http.Client{
17+
Transport: &http.Transport{
18+
Dial: func(network, addr string) (net.Conn, error) {
19+
if addr != "listen-api-test.listennotes.com:443" {
20+
t.Errorf("custom baseURL was not used: %s", addr)
21+
}
22+
return nil, noDial
23+
},
24+
},
25+
}
26+
27+
client := listennotes.NewClient("", listennotes.WithHTTPClient(httpClient))
28+
_, err := client.Search(map[string]string{"q": "a"})
29+
30+
if !errors.Is(err, noDial) {
31+
t.Errorf("mock http client failure was not as expected: %s", err)
32+
}
33+
}
34+
35+
func TestProductionURL(t *testing.T) {
36+
noDial := fmt.Errorf("no-dial")
37+
38+
httpClient := &http.Client{
39+
Transport: &http.Transport{
40+
Dial: func(network, addr string) (net.Conn, error) {
41+
if addr != "listen-api.listennotes.com:443" {
42+
t.Errorf("custom baseURL was not used: %s", addr)
43+
}
44+
return nil, noDial
45+
},
46+
},
47+
}
48+
49+
client := listennotes.NewClient("anapikey", listennotes.WithHTTPClient(httpClient))
50+
_, err := client.Search(map[string]string{"q": "a"})
51+
52+
if !errors.Is(err, noDial) {
53+
t.Errorf("mock http client failure was not as expected: %s", err)
54+
}
55+
}

constants.go

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package listennotes
2+
3+
// Base urls for access the available api endpoints
4+
const (
5+
BaseURLProduction = "https://listen-api.listennotes.com/api/v2"
6+
BaseURLTest = "https://listen-api-test.listennotes.com/api/v2"
7+
)
8+
9+
// Request header keys
10+
const (
11+
RequestHeaderKeyAPI = "X-ListenAPI-Key"
12+
)
13+
14+
// Reponse header keys
15+
const (
16+
ResponseHeaderKeyFreeQuota = "X-ListenAPI-FreeQuota"
17+
ResponseHeaderKeyUsage = "X-ListenAPI-Usage"
18+
ResponseHeaderKeyLatencySeconds = "X-listenAPI-Latency-Seconds"
19+
ResponseHeaderKeyNextBillingDate = "X-Listenapi-NextBillingDate"
20+
)
21+
22+
// TimeFormat is the string format of all response times
23+
const TimeFormat = "2006-01-02T15:04:05.999999-07:00"

example/main.go

-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ func main() {
1414
// the test data will return the same page each time, but this is an example of getting the next_offset out fo the resulting payload
1515
nextOffset := fetchAndOutputPage(client, 0)
1616
fetchAndOutputPage(client, nextOffset)
17-
fetchAndOutputPage(client, nextOffset)
1817
}
1918

2019
func fetchAndOutputPage(client listennotes.HTTPClient, offset int) int {

httpclient.go

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package listennotes
2+
3+
import (
4+
"net"
5+
"net/http"
6+
"time"
7+
)
8+
9+
var defaultHTTPClient = &http.Client{
10+
Timeout: time.Second * 30,
11+
Transport: &http.Transport{
12+
Dial: (&net.Dialer{
13+
Timeout: 10 * time.Second,
14+
}).Dial,
15+
TLSHandshakeTimeout: 10 * time.Second,
16+
},
17+
}

options_test.go

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package listennotes_test
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"net"
7+
"net/http"
8+
"testing"
9+
10+
listennotes "github.com/ListenNotes/podcast-api-go"
11+
)
12+
13+
func TestWithHTTPOptions(t *testing.T) {
14+
15+
noDial := fmt.Errorf("no-dial")
16+
17+
httpClient := &http.Client{
18+
Transport: &http.Transport{
19+
Dial: func(network, addr string) (net.Conn, error) {
20+
return nil, noDial
21+
},
22+
},
23+
}
24+
25+
client := listennotes.NewClient("", listennotes.WithHTTPClient(httpClient))
26+
_, err := client.Search(map[string]string{"q": "a"})
27+
28+
if !errors.Is(err, noDial) {
29+
t.Errorf("mock http client failure was not as expected: %s", err)
30+
}
31+
32+
}
33+
34+
func TestWithBaseURL(t *testing.T) {
35+
noDial := fmt.Errorf("no-dial")
36+
37+
httpClient := &http.Client{
38+
Transport: &http.Transport{
39+
Dial: func(network, addr string) (net.Conn, error) {
40+
if addr != "localhost:80" {
41+
t.Errorf("custom baseURL was not used: %s", addr)
42+
}
43+
return nil, noDial
44+
},
45+
},
46+
}
47+
48+
client := listennotes.NewClient("", listennotes.WithHTTPClient(httpClient), listennotes.WithBaseURL("http://localhost/test-url"))
49+
_, err := client.Search(map[string]string{"q": "a"})
50+
51+
if !errors.Is(err, noDial) {
52+
t.Errorf("mock http client failure was not as expected: %s", err)
53+
}
54+
}

request.go

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package listennotes
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
)
8+
9+
// Response is the standard response for all client functions
10+
type Response struct {
11+
Stats ResponseStatistics
12+
Data map[string]interface{}
13+
}
14+
15+
func (c *standardHTTPClient) execute(args map[string]string, path string) (*Response, error) {
16+
url := fmt.Sprintf("%s/%s", c.baseURL, path)
17+
18+
req, err := http.NewRequest("GET", url, nil)
19+
if err != nil {
20+
return nil, fmt.Errorf("failed to create request to %s: %w", path, err)
21+
}
22+
req.Header.Add(RequestHeaderKeyAPI, c.apiKey)
23+
24+
q := req.URL.Query()
25+
for k, v := range args {
26+
q.Add(k, v)
27+
}
28+
req.URL.RawQuery = q.Encode()
29+
30+
resp, err := c.httpClient.Do(req)
31+
if err != nil {
32+
return nil, fmt.Errorf("failed to executing request to %s: %w", path, err)
33+
}
34+
defer resp.Body.Close()
35+
36+
// map any generic status code errors
37+
if mappedError, ok := errMap[resp.StatusCode]; ok && mappedError != nil {
38+
return nil, mappedError
39+
}
40+
41+
// generic body parsing
42+
var genericJSON map[string]interface{}
43+
if err := json.NewDecoder(resp.Body).Decode(&genericJSON); err != nil {
44+
return nil, fmt.Errorf("failed parsing the response from %s: %w", path, err)
45+
}
46+
47+
// gather the header statistics
48+
stats := parseStats(resp)
49+
50+
return &Response{
51+
Stats: stats,
52+
Data: genericJSON,
53+
}, nil
54+
}

request_test.go

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package listennotes
2+
3+
import "testing"
4+
5+
func TestStandardClientExecute(t *testing.T) {
6+
7+
}

stats.go

+10-9
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import (
88

99
// ResponseStatistics will contain the stats information provided by API response headers
1010
type ResponseStatistics struct {
11-
FreeQuota *int
12-
Usage *int
13-
LatencySeconds *float64
14-
NextBillingDate *time.Time
11+
FreeQuota int
12+
Usage int
13+
LatencySeconds float64
14+
NextBillingDate time.Time
1515
}
1616

1717
func parseStats(resp *http.Response) ResponseStatistics {
@@ -21,16 +21,17 @@ func parseStats(resp *http.Response) ResponseStatistics {
2121
stats := ResponseStatistics{}
2222

2323
if freeQuota, err := strconv.Atoi(resp.Header.Get(ResponseHeaderKeyFreeQuota)); err == nil {
24-
stats.FreeQuota = &freeQuota
24+
stats.FreeQuota = freeQuota
2525
}
2626
if usage, err := strconv.Atoi(resp.Header.Get(ResponseHeaderKeyUsage)); err == nil {
27-
stats.Usage = &usage
27+
stats.Usage = usage
2828
}
2929
if latency, err := strconv.ParseFloat(resp.Header.Get(ResponseHeaderKeyLatencySeconds), 64); err == nil {
30-
stats.LatencySeconds = &latency
30+
stats.LatencySeconds = latency
3131
}
32-
if nextBill, err := time.Parse(resp.Header.Get(ResponseHeaderKeyNextBillingDate), ""); err == nil {
33-
stats.NextBillingDate = &nextBill
32+
33+
if nextBill, err := time.Parse(TimeFormat, resp.Header.Get(ResponseHeaderKeyNextBillingDate)); err == nil {
34+
stats.NextBillingDate = nextBill
3435
}
3536

3637
return stats

0 commit comments

Comments
 (0)