-
Notifications
You must be signed in to change notification settings - Fork 62
/
converter.go
147 lines (121 loc) · 3.51 KB
/
converter.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
package exchange
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"math"
"net/http"
"time"
"github.com/PacktPublishing/Hands-On-Dependency-Injection-in-Go/ch08/acme/internal/logging"
)
const (
// request URL for the exchange rate API
urlFormat = "%s/api/historical?access_key=%s&date=2018-06-20¤cies=%s"
// default price that is sent when an error occurs
defaultPrice = 0.0
)
// NewConverter creates and initializes the converter
func NewConverter(cfg Config) *Converter {
return &Converter{
cfg: cfg,
}
}
// Config is the config for Converter
type Config interface {
Logger() logging.Logger
ExchangeBaseURL() string
ExchangeAPIKey() string
}
// Converter will convert the base price to the currency supplied
// Note: we are expecting sane inputs and therefore skipping input validation
type Converter struct {
cfg Config
}
// Exchange will perform the conversion
func (c *Converter) Exchange(ctx context.Context, basePrice float64, currency string) (float64, error) {
// load rate from the external API
response, err := c.loadRateFromServer(ctx, currency)
if err != nil {
return defaultPrice, err
}
// extract rate from response
rate, err := c.extractRate(response, currency)
if err != nil {
return defaultPrice, err
}
// apply rate and round to 2 decimal places
return math.Floor((basePrice/rate)*100) / 100, nil
}
// load rate from the external API
func (c *Converter) loadRateFromServer(ctx context.Context, currency string) (*http.Response, error) {
// build the request
url := fmt.Sprintf(urlFormat,
c.cfg.ExchangeBaseURL(),
c.cfg.ExchangeAPIKey(),
currency)
// perform request
req, err := http.NewRequest("GET", url, nil)
if err != nil {
c.logger().Warn("[exchange] failed to create request. err: %s", err)
return nil, err
}
// set latency budget for the upstream call
subCtx, cancel := context.WithTimeout(ctx, 1*time.Second)
defer cancel()
// replace the default context with our custom one
req = req.WithContext(subCtx)
// perform the HTTP request
response, err := http.DefaultClient.Do(req)
if err != nil {
c.logger().Warn("[exchange] failed to load. err: %s", err)
return nil, err
}
if response.StatusCode != http.StatusOK {
err = fmt.Errorf("request failed with code %d", response.StatusCode)
c.logger().Warn("[exchange] %s", err)
return nil, err
}
return response, nil
}
func (c *Converter) extractRate(response *http.Response, currency string) (float64, error) {
defer func() {
_ = response.Body.Close()
}()
// extract data from response
data, err := c.extractResponse(response)
if err != nil {
return defaultPrice, err
}
// pull rate from response data
rate, found := data.Quotes["USD"+currency]
if !found {
err = fmt.Errorf("response did not include expected currency '%s'", currency)
c.logger().Error("[exchange] %s", err)
return defaultPrice, err
}
// happy path
return rate, nil
}
func (c *Converter) extractResponse(response *http.Response) (*apiResponseFormat, error) {
payload, err := ioutil.ReadAll(response.Body)
if err != nil {
c.logger().Error("[exchange] failed to ready response body. err: %s", err)
return nil, err
}
data := &apiResponseFormat{}
err = json.Unmarshal(payload, data)
if err != nil {
c.logger().Error("[exchange] error converting response. err: %s", err)
return nil, err
}
// happy path
return data, nil
}
func (c *Converter) logger() logging.Logger {
return c.cfg.Logger()
}
// the response format from the exchange rate API
type apiResponseFormat struct {
Quotes map[string]float64 `json:"quotes"`
}