Fetching currency rates in Go - part 3 interfaces and functions
In the previous part of this series, I ended up with a fairly simple XML parsing function that returned a slice of elements parsed from an XML document.
Interfaces and Functions
At this point, many developers would be thinking, so…do we have a class that does the parsing?
In the final version, I would ideally parse the XML direct from an HTTP request, and indeed, the Body from an HTTP response provides the Read function that I want to be able to pass to the parser.
The simplest implementation is therefore just a function that takes a URL, and
gives us back our EuroRates
mapping between currencies and rates.
Go has function types, this means that you can declare a type which is a function, and then you can say that a function can take a function with the specified type.
// LiveProvider fetches the rates for the daily data.
type LiveProvider func() (EuroRates, error)
This declares a new function type LiveProvider
that takes no arguments, it
just returns one of EuroRates maps, and possibly an error.
We can say that another function can take a `LiveProvider, or it can be the type for a struct member.
This could also be declared as an interface something like this:
type LiveRatesGetter interface {
GetLiveRates() (EuroRates, error)
}
In this case, the function provides a slightly simpler declaration, for simple interfaces like this, a function can suffice.
Provider functions - closures
Now that I have a LiveProvider
type I can create functions that implement
the functionality, and for this, I’ve opted to create a closure.
// FileLiveProvider parses the named file and daily rates data.
func FileLiveProvider(fname string) LiveProvider {
return func() (EuroRates, error) {
f, err := os.Open(fname)
if err != nil {
return nil, err
}
defer f.Close()
rates, err := parse(f)
if err != nil {
return nil, err
}
return ratesToEuroRates(rates), nil
}
}
This is a simple function that takes a filename, and returns another function that can open the file, parse the XML, and then convert the list of currencies in the file into a EuroRates map.
If you’ve not seen closures before, then start here, but the simple version is that the returned function has access to the values available to it at the time it’s created, the fname string
value is available to the function.
The closure is used as the store for the filename, each time you call the function returned, it will read the data from the same file, not the most efficient implementation, but as I’ll demonstrate later, I can improve the performance.
The simplest possible call for this function is:
rates := FileLiveProvider("/var/tmp/download.xml")()
Note the ()
at the end of the call, this is to call the returned function.
Note that this could easily be…
rateGetter := FileLiveProvider("/var/tmp/download.xml")
rates := rateGetter()
And I can easily declare a struct that takes a LiveProvider:
type Service struct {
liveProvider LiveProvider
}
srv := Service{liveProvider: FileLiveProvider("/var/tmp/download.xml")}
HTTP Provider
While the FileLiveProvider is useful, especially for testing purposes, or maybe a static cache of data, it’s not really live.
To be live, I need to fetch the data from the ECB server.
Of course, I need to test this code…
func TestHTTPLiveProvider(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "testdata/eurofxref-daily.xml")
}))
defer ts.Close()
rates, err := HTTPLiveProvider(ts.URL)()
if err != nil {
t.Fatal(err)
}
wanted := &EuroRate{Currency: "CNY", Rate: "7.7457", ReferenceDate: "2017-05-22"}
if r := rates.GetRateForCurrency("CNY"); !reflect.DeepEqual(r, wanted) {
t.Errorf("incorrect CNY rate from daily fixture: got %#v, wanted %#v", r, wanted)
}
}
This test starts a simple test HTTP Server which responds by serving the usual fixture file.
We then create and call our HTTPLiveProvider with the URL for the test server, and finally verify that I got the expected result back from the fixture data.
// HTTPLiveProvider parses the specified URL and returns daily rates data.
// https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml
func HTTPLiveProvider(url string) LiveProvider {
return func() (EuroRates, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
rates, err := parse(resp.Body)
if err != nil {
return nil, err
}
return ratesToEuroRates(rates), nil
}
}
func ratesToEuroRates(rates []*EuroRate) EuroRates {
rateMap := make(EuroRates)
for _, e := range rates {
rateMap[e.Currency] = e
}
return rateMap
}
The ratesToEuroRates function just takes a slice of pointers to EuroRate structs, and converts it to a map.
The error handling in the provider is perhaps a bit sparse, but because I control the “server” that it’s talking to, I can respond appropriately, and see what happens.
Perhaps in this case, I’d expect to get an error, and that’s easily fixable.
func TestHTTPLiveProviderWith404(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
}))
defer ts.Close()
_, err := HTTPLiveProvider(ts.URL)()
if err == nil {
t.Fatal("expected to receive error, got nil")
}
if e := err.Error(); e != "failed to fetch daily data" {
t.Fatalf("incorrect error, got %s, wanted %s", e, "failed to fetch daily data")
}
}
Notice that instead of responding with the fixture file, the test server only sends back a 404 response.