Once I have my in-memory feature storage implemented, it’s fairly easy to implement the other layers for this service.

package api

import (
	"github.com/bigkevmcd/go-features/store"
)

func New(fs store.FeatureStore, l Logger) *API {
	return &API{fs: fs, log: l}
}

type API struct {
	fs  store.FeatureStore
	log Logger
}

func (a *API) GetFeaturesForResource(resource, id string) ([]string, error) {
	a.log.Debugf("fetching all features for resource=%s id=%s", resource, id)
	return a.fs.GetFeaturesForResource(resource, id)
}

func (a *API) ResourceHasFeature(resource, id, feature string) (bool, error) {
	a.log.Debugf("checking for resource feature resource=%s id=%s feature=%s",
resource, id, feature)
	return a.fs.HasFeature(resource, id, feature)
}

func (a *API) AddFeatureToResource(resource, id, feature string) error {
	a.log.Debugf("adding feature to resource resource=%s id=%s feature=%s",
resource, id, feature)
	return a.fs.AddFeature(resource, id, feature)
}

This is basically delegating the calls to the underlying store, and logging the requests, it’d be fairly easy to time the calls, in order to improve visibility into issues fetchng from the store.

And to provide an HTTP implementation of this microservice…

package http

import (
	"encoding/json"
	"net/http"

	"github.com/bigkevmcd/go-features/api"
	"github.com/julienschmidt/httprouter"
)

func NewHTTPEndpoint(api *api.API) *HTTPEndpoint {
	endpoint := &HTTPEndpoint{Router: httprouter.New(), api: api}
	endpoint.GET("/features/:resource/:id", endpoint.GetFeaturesForResource)
	endpoint.GET("/features/:resource/:id/:feature", endpoint.ResourceHasFeature)
	endpoint.POST("/features/:resource/:id/:feature", endpoint.AddFeatureToResource)
	return endpoint
}

type HTTPEndpoint struct {
	api *api.API
	*httprouter.Router
}

func (e *HTTPEndpoint) GetFeaturesForResource(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
	resource := p.ByName("resource")
	resourceId := p.ByName("id")

	features, err := e.api.GetFeaturesForResource(resource, resourceId)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	w.WriteHeader(http.StatusOK)
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(features)
}

type hasFeatureResponse struct {
	Result bool `json:"result"`
}

func (e *HTTPEndpoint) ResourceHasFeature(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
	resource := p.ByName("resource")
	resourceId := p.ByName("id")
	feature := p.ByName("feature")

	has, err := e.api.ResourceHasFeature(resource, resourceId, feature)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	w.WriteHeader(http.StatusOK)
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(hasFeatureResponse{Result: has})
}

This is fairly simple to implement, and tests are fairly simple too…

package http

import (
	"bytes"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/bigkevmcd/go-features/api"
	store "github.com/bigkevmcd/go-features/store/memory"
)

func createRequest(method, url string, body []byte) *http.Request {
	req := httptest.NewRequest(method, url, bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	return req
}

func decodeJSON(t *testing.T, resp *http.Response) []byte {
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("got %v, wanted %v", resp.StatusCode, http.StatusOK)
	}

	body, _ := ioutil.ReadAll(resp.Body)
	defer resp.Body.Close()
	return body
}

func createAPI() *api.API {
	store := store.NewFeatureStore()
	return api.New(store)
}

func TestGetFeaturesForResource(t *testing.T) {
	endpoint := NewHTTPEndpoint(createAPI())
	req := createRequest("GET", "http://example.com/features/pipeline/76bd2782-b603-4590-ac71-aa43d278bc31", nil)
	w := httptest.NewRecorder()

	endpoint.ServeHTTP(w, req)

	resp := w.Result()
	body := decodeJSON(t, resp)
	if b := string(body); b != "[]\n" {
		t.Fatalf("got %s, wanted %s", b, "[]")
	}

}

func TestGetFeaturesForResourceWithFeatures(t *testing.T) {
	api := createAPI()
	api.AddFeatureToResource("pipeline", "76bd2782-b603-4590-ac71-aa43d278bc31", "feature-1")
	api.AddFeatureToResource("pipeline", "76bd2782-b603-4590-ac71-aa43d278bc31", "feature-2")
	endpoint := NewHTTPEndpoint(api)
	req := createRequest("GET", "http://example.com/features/pipeline/76bd2782-b603-4590-ac71-aa43d278bc31", nil)
	w := httptest.NewRecorder()

	endpoint.ServeHTTP(w, req)

	resp := w.Result()
	body := decodeJSON(t, resp)
	if b := string(body); b != "[\"feature-1\",\"feature-2\"]\n" {
		t.Fatalf("got %s, wanted %s", b, "[\"feature-1\",\"feature-2\"]\n")
	}

}

Which makes my cmd/api-service/main.go look like this…

package main

import (
	"net/http"

	"github.com/bigkevmcd/go-features/api"
	httpapi "github.com/bigkevmcd/go-features/http"
	"github.com/bigkevmcd/go-features/store/memory"
	log "github.com/sirupsen/logrus"
)

func main() {
	logger := log.WithFields(log.Fields{
		"srv": "api-service",
	})
	log.SetLevel(log.DebugLevel)

	store := memory.NewFeatureStore()
	api := api.New(store, logger)

	srv := httpapi.NewHTTPEndpoint(api)

	log.Println("listening on port :8080")
	log.Fatal(http.ListenAndServe(":8080", srv))
}

Exercising the service is fairly simple…

$ curl -X POST http://localhost:8080/features/users/1840298b-5ee4-4122-99a2-5798b252133d/feature1
$ curl http://localhost:8080/features/users/1840298b-5ee4-4122-99a2-5798b252133d/feature1
{"result":true}
$ curl http://localhost:8080/features/users/1840298b-5ee4-4122-99a2-5798b252133d
["feature1"]