package exchange import ( "context" "encoding/json" "fmt" "io/ioutil" "math" "net/http" "time" "github.com/PacktPublishing/Hands-On-Dependency-Injection-in-Go/ch12/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"` }