In the previous parts, I’ve designed a parser, and implemented a function to get currency rates from the European Central Bank (ECB).

Now, to expose this via simple Microservice…

go-micro

I’ve chosen to implement this using @chuhnk’s micro but, given the underlying functions, there’s nothing specific to micro in the code I’ve written to extract the currency data.

gRPC and Protocol Buffers (protobufs)

gRPC uses Protobufs to declare the RPC call structure, and micro builds on top of this by making it easy to expose gRPC services in a standardised way.

syntax = "proto3";

message Rate {
	string currency = 1;
	string rate = 2;
	string referenceDate = 3;
}

service Rates {
	rpc Live(LiveRequest) returns (LiveResponse) {}
	rpc Historic(HistoricRequest) returns (HistoricResponse) {}
}

message LiveRequest {
	string currency = 1;
}

message LiveResponse {
	Rate rate = 1;
}

message HistoricRequest {
  string currency = 1;
  string refDate = 2;
}


message HistoricResponse {
	Rate rate = 1;
}

You’ll note that I’ve included a separate Historic call, the ECB provides a separate URL which provides data very similar to the Live data, but with 90 days of data.

Parsing this isn’t much different from parsing the Live data, and I’ve skipped the implementation here, but the code is available.

The protobuf file defines a Rates service with two RPC calls, Live and Historic.

The Live call takes a Live request, with just a currency, and returns a LiveResponse with a Rate element.

This makes sense, I ask for a single currency, and I get the data back.

Follow the micro installation instructions.

To compile the protobufs, I like to use a Makefile.

.PHONY: proto

proto:
	for f in proto/*.proto; do \
		protoc -I. --go_out=plugins=micro:. $$f; \
		echo compiled: $$f; \
	done \

This make target is reusable, so take note…

(master) $ make -s
compiled: proto/rates.proto
(master) $

Writing the service

Once I’ve compiled the protobuf, I get rates.pb.go and a peek inside finds various things, including a client definition.

The interface that I’m required to implement looks like this:

// Server API for Rates service

type RatesHandler interface {
	Live(context.Context, *LiveRequest, *LiveResponse) error
	Historic(context.Context, *HistoricRequest, *HistoricResponse) error
}

A simple implementation of this interface:

// RatesHandler is a micro service providing currency rate information.
type RatesHandler struct {
	liveProvider     parser.LiveProvider
	historicProvider parser.HistoricProvider
}

func (r *RatesHandler) Live(ctx context.Context, req *proto.LiveRequest, rsp *proto.LiveResponse) error {
	currency := req.GetCurrency()

	rates, err := r.liveProvider()
	if err != nil {
		return errors.InternalServerError("internal error", "error: %s", err)
	}

	rate := rates.GetRateForCurrency(currency)
	if rate == nil {
		return errors.NotFound("not found", "could not find currency %s", currency)
	}

	rsp.Rate = &proto.Rate{
		Currency:      rate.Currency,
		Rate:          rate.Rate,
		ReferenceDate: rate.ReferenceDate,
	}

	return nil
}

Walking through this code, it fetches the currency from the request, then calls the closure to fetch the data, and gets the currency rate for the requested currency from the result of the closure call.

If I don’t get a live rate for the currency, then a NotFound status error is returned, otherwise it populates the response struct.

Again, I’m opting not to convert the Rate to a floating point value, because returning it as a string allows the caller to retain the same accuracy as we got it from the ECB.

The errors package in use here is provided by the micro framework, and is not the usual Go package.

Testing gRPC handlers

Testing calls like this is a bit more involved, but still fairly easy…

func fakeLiveProvider() (parser.EuroRates, error) {
	rate := &parser.EuroRate{
		Currency:      "GBP",
		Rate:          "1.00",
		ReferenceDate: "2018-02-07",
	}

	rates := parser.EuroRates{"GBP": rate}
	return rates, nil
}

func fakeHistoricProvider() (map[string]parser.EuroRates, error) {
	return nil, nil
}

func TestLive(t *testing.T) {
	handler := NewRatesHandler(fakeLiveProvider, fakeHistoricProvider)
	req := &proto.LiveRequest{Currency: "GBP"}
	resp := &proto.LiveResponse{}

	err := handler.Live(context.TODO(), req, resp)
	if err != nil {
		t.Fatal(err)
	}

	rate := resp.Rate
	if rate.Currency != "GBP" {
		t.Errorf("incorrect currency: got %s, wanted %s", rate.Currency, "GBP")
	}

	if rate.Rate != "1.00" {
		t.Errorf("incorrect rate: got %s, wanted %s", rate.Rate, "1.00")
	}

	if rate.ReferenceDate != "2018-02-07" {
		t.Errorf("incorrect reference date: got %s, wanted %s", rate.ReferenceDate, "2018-02-07")
	}
}

First off, I create two fake providers, the liveProvider fake just returns a hard-coded response, the other returns an empty dataset, the test here doesn’t need it.

In the test, I instantiate the request and response objects, and make a call to handler.Live(context.TODO(), req, resp).

This passes in a Go context, and the request and response, the code shouldn’t get an error, but it makes sure we didn’t, and then asserts our response is as expected.

Fairly simple to setup and call…and for testing the handling of NotFound cases.

func TestLiveWithUnknownCurrency(t *testing.T) {
	handler := NewRatesHandler(fakeLiveProvider, fakeHistoricProvider)
	req := &proto.LiveRequest{Currency: "AUD"}
	resp := &proto.LiveResponse{}

	err := handler.Live(context.TODO(), req, resp)
	if err == nil {
		t.Fatal("expected an error")
	}

	e, ok := err.(*errors.Error)
	if !ok {
		t.Fatal("got an unknown error type")
	}

	if e.Code != http.StatusNotFound {
		t.Fatalf("incorrect error code: got %d, wanted %d", e.Code, http.StatusNotFound)
	}

The only major difference here is the type assertion to convert the response error into the micro-specific Error which contains the status code.