Implement RateProvider adapter
This commit is contained in:
parent
f941eb7a03
commit
9a9a6b338b
14
go.mod
14
go.mod
|
|
@ -2,4 +2,16 @@ module converter
|
|||
|
||||
go 1.24.3
|
||||
|
||||
require github.com/shopspring/decimal v1.4.0
|
||||
require (
|
||||
github.com/go-resty/resty/v2 v2.16.5
|
||||
github.com/shopspring/decimal v1.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
17
go.sum
17
go.sum
|
|
@ -1,2 +1,19 @@
|
|||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
|
||||
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
138
infrastructure/coinmarketcap/client.go
Normal file
138
infrastructure/coinmarketcap/client.go
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
package coinmarketcap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
// CoinMarketCap specific errors
|
||||
var (
|
||||
ErrUnknownSymbol = errors.New("unknown symbol")
|
||||
ErrInvalidConvert = errors.New("invalid convert currency")
|
||||
ErrAPIError = errors.New("coinmarketcap api error")
|
||||
ErrInvalidResponse = errors.New("invalid response format")
|
||||
)
|
||||
|
||||
const (
|
||||
baseURL = "https://pro-api.coinmarketcap.com"
|
||||
quotesPath = "/v1/cryptocurrency/quotes/latest"
|
||||
)
|
||||
|
||||
// Client implements the RateProvider interface for CoinMarketCap API
|
||||
type Client struct {
|
||||
httpClient *resty.Client
|
||||
apiKey string
|
||||
}
|
||||
|
||||
// NewClient creates a new CoinMarketCap API client
|
||||
func NewClient(apiKey string) *Client {
|
||||
client := resty.New().
|
||||
SetBaseURL(baseURL).
|
||||
SetHeader("Accept", "application/json").
|
||||
SetHeader("X-CMC_PRO_API_KEY", apiKey)
|
||||
|
||||
return &Client{
|
||||
httpClient: client,
|
||||
apiKey: apiKey,
|
||||
}
|
||||
}
|
||||
|
||||
// GetRate fetches exchange rate from CoinMarketCap API
|
||||
func (c *Client) GetRate(ctx context.Context, fromCode, toCode string) (RateDTO, error) {
|
||||
// Try direct conversion first
|
||||
rateDTO, err := c.tryGetRate(ctx, fromCode, toCode)
|
||||
if err == nil {
|
||||
return rateDTO, nil
|
||||
}
|
||||
|
||||
// If empty result but no other error, try reverse
|
||||
if err == ErrUnknownSymbol {
|
||||
reverseDTO, reverseErr := c.tryGetRate(ctx, toCode, fromCode)
|
||||
if reverseErr == nil {
|
||||
return reverseDTO, nil
|
||||
}
|
||||
|
||||
// If reverse request failed with invalid convert, that confirms the original
|
||||
// "from" currency is indeed unknown (it's now the "convert" parameter)
|
||||
if reverseErr == ErrInvalidConvert {
|
||||
return RateDTO{}, err // Return original unknown symbol error
|
||||
}
|
||||
|
||||
// Any other error from reverse request (network, auth, etc) should be surfaced
|
||||
return RateDTO{}, reverseErr
|
||||
}
|
||||
|
||||
// Return original error (either non-unknown-symbol error or first currency not in crypto database)
|
||||
return RateDTO{}, err
|
||||
}
|
||||
|
||||
// tryGetRate attempts to get rate in specified direction
|
||||
func (c *Client) tryGetRate(ctx context.Context, fromCode, toCode string) (RateDTO, error) {
|
||||
var response APIResponse
|
||||
|
||||
// Make API request
|
||||
resp, err := c.httpClient.R().
|
||||
SetContext(ctx).
|
||||
SetQueryParam("symbol", fromCode).
|
||||
SetQueryParam("convert", toCode).
|
||||
SetResult(&response).
|
||||
Get(quotesPath)
|
||||
|
||||
if err != nil {
|
||||
return RateDTO{}, fmt.Errorf("%w: %v", ErrAPIError, err)
|
||||
}
|
||||
|
||||
// Check HTTP status first
|
||||
if resp.StatusCode() != 200 {
|
||||
switch resp.StatusCode() {
|
||||
case 400:
|
||||
return RateDTO{}, fmt.Errorf("%w: %s", ErrInvalidConvert, toCode)
|
||||
case 401:
|
||||
return RateDTO{}, fmt.Errorf("%w: unauthorized - bad or missing API key", ErrAPIError)
|
||||
default:
|
||||
return RateDTO{}, fmt.Errorf("%w: HTTP %d", ErrAPIError, resp.StatusCode())
|
||||
}
|
||||
}
|
||||
|
||||
// Check if data is empty - invalid currency
|
||||
if len(response.Data) == 0 {
|
||||
return RateDTO{}, fmt.Errorf("%w: %s", ErrUnknownSymbol, fromCode)
|
||||
}
|
||||
|
||||
// Extract rate from response
|
||||
rateDTO, err := c.extractRateDTO(response, fromCode, toCode)
|
||||
if err != nil {
|
||||
return RateDTO{}, err
|
||||
}
|
||||
|
||||
return rateDTO, nil
|
||||
}
|
||||
|
||||
|
||||
// extractRateDTO converts API response to RateDTO
|
||||
func (c *Client) extractRateDTO(response APIResponse, fromCode, toCode string) (RateDTO, error) {
|
||||
// Get crypto data for the from currency
|
||||
cryptoData, exists := response.Data[fromCode]
|
||||
if !exists {
|
||||
return RateDTO{}, ErrInvalidResponse
|
||||
}
|
||||
|
||||
// Get quote for the to currency
|
||||
quote, exists := cryptoData.Quote[toCode]
|
||||
if !exists {
|
||||
return RateDTO{}, ErrInvalidResponse
|
||||
}
|
||||
|
||||
// Return the rate exactly as API provided it
|
||||
return RateDTO{
|
||||
FromCode: fromCode,
|
||||
ToCode: toCode,
|
||||
FromName: cryptoData.Name,
|
||||
ToName: toCode,
|
||||
Price: quote.Price,
|
||||
Source: "coinmarketcap",
|
||||
}, nil
|
||||
}
|
||||
34
infrastructure/coinmarketcap/models.go
Normal file
34
infrastructure/coinmarketcap/models.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
package coinmarketcap
|
||||
|
||||
// APIResponse represents the top-level CoinMarketCap API response
|
||||
type APIResponse struct {
|
||||
Status Status `json:"status"`
|
||||
Data map[string]CryptoQuote `json:"data"`
|
||||
}
|
||||
|
||||
// Status represents the API response status
|
||||
type Status struct {
|
||||
ErrorCode int `json:"error_code"`
|
||||
ErrorMessage *string `json:"error_message"`
|
||||
}
|
||||
|
||||
// CryptoQuote represents cryptocurrency data with quotes
|
||||
type CryptoQuote struct {
|
||||
Name string `json:"name"`
|
||||
Quote map[string]CurrencyQuote `json:"quote"`
|
||||
}
|
||||
|
||||
// CurrencyQuote represents price quote in a specific currency
|
||||
type CurrencyQuote struct {
|
||||
Price float64 `json:"price"`
|
||||
}
|
||||
|
||||
// RateDTO represents exchange rate data from infrastructure layer
|
||||
type RateDTO struct {
|
||||
FromCode string
|
||||
ToCode string
|
||||
FromName string
|
||||
ToName string
|
||||
Price float64
|
||||
Source string
|
||||
}
|
||||
Loading…
Reference in a new issue