Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package hyperdrive

import "os"

func (suite *HyperdriveTestSuite) TestNewConfig() {
suite.IsType(Config{}, NewConfig(), "expects an instance of *hyperdrive.Config")
}

func (suite *HyperdriveTestSuite) TestPortConfigFromDefault() {
c := NewConfig()
suite.Equal(5000, c.Port, "Port should be equal to default value")
}

func (suite *HyperdriveTestSuite) TestPortConfigFromEnv() {
os.Setenv("PORT", "5001")
c := NewConfig()
suite.Equal(5001, c.Port, "Port should be equal to PORT value set via ENV var")
}

func (suite *HyperdriveTestSuite) TestGetPort() {
c := NewConfig()
suite.Equal(":5000", c.GetPort(), "c.Port value should be return, prefixed with a colon, e.g. :5000")
}

func (suite *HyperdriveTestSuite) TestEnvConfigFromDefault() {
c := NewConfig()
suite.Equal("development", c.Env, "Env should be equal to default value")
}

func (suite *HyperdriveTestSuite) TestEnvConfigFromEnv() {
os.Setenv("HYPERDRIVE_ENVIRONMENT", "test")
c := NewConfig()
suite.Equal("test", c.Env, "Env should be equal to HYPERDRIVE_ENVIRONMENT value set via ENV var")
}
64 changes: 64 additions & 0 deletions discovery.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package hyperdrive

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

// Representation is a data structure representing the response output. The
// representation is used when automatically encoding responses based on the
// Content Type determined by content negotation.
type Representation map[string]interface{}

// RootResource contains information about the API and its Endpoints, and is
// the hypermedia respresentation returned by the Discovery URL endpoint for
// API clients to learn about the API.
type RootResource struct {
Name string
Endpoints []Endpointer
}

// NewRootResource creates an instance of RootResource, based on the given API.
func NewRootResource(api API) *RootResource {
return &RootResource{Name: api.Name}
}

// AddEndpointer adds Endpointers to the slice of Endpointers on an instance of RootResource.
func (root *RootResource) AddEndpointer(e Endpointer) {
root.Endpoints = append(root.Endpoints, e)
}

// Present returns an Representation of the RootResource to describe the API
// for the Discovery URL.
func (root *RootResource) Present() Representation {
return Representation{
"resource": "api",
"name": root.Name,
"endpoints": root.endpointRepresentations(),
}
}

func (root *RootResource) endpointRepresentations() []Representation {
var endpoints = []Representation{}
for _, e := range root.Endpoints {
endpoints = append(endpoints, PresentEndpoint(e))
}
return endpoints
}

// PresentEndpoint returns a Representation to describe an Endpoint for the Discovery URL.
func PresentEndpoint(e Endpointer) Representation {
return Representation{
"name": e.GetName(),
"desc": e.GetDesc(),
"path": e.GetPath(),
"methods": GetMethods(e),
}
}

// ServeHTTP satisfies the http.Handler interface and returns the hypermedia
// representation of the Discovery URL.
func (root *RootResource) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(root.Present())
}
33 changes: 33 additions & 0 deletions discovery_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package hyperdrive

import "net/http"

func (suite *HyperdriveTestSuite) TestNewRootResource() {
suite.IsType(&RootResource{}, suite.TestRoot, "expects an instance of RootResource")
}

func (suite *HyperdriveTestSuite) TestRootResourceEndpointsEmpty() {
suite.Equal(0, len(suite.TestRoot.Endpoints), "expects 0 Endpoints")
}

func (suite *HyperdriveTestSuite) TestAddEndpointer() {
suite.TestRoot.AddEndpointer(suite.TestEndpoint)
suite.Equal(1, len(suite.TestRoot.Endpoints), "expects 1 Endpoints")
}

func (suite *HyperdriveTestSuite) TestRootResourceServeHTTP() {
suite.Implements((*http.Handler)(nil), suite.TestRoot, "return an implementation of http.Handler")
}

func (suite *HyperdriveTestSuite) TestPresentRepresentation() {
suite.IsType(Representation{}, suite.TestRoot.Present(), "return a Representation")
}

func (suite *HyperdriveTestSuite) TestPresent() {
suite.TestRoot.AddEndpointer(suite.TestEndpoint)
suite.Equal(suite.TestRootRepresentation, suite.TestRoot.Present(), "return the correct Representation of RootResource")
}

func (suite *HyperdriveTestSuite) TestPresentEndpoint() {
suite.Equal(suite.TestEndpointRepresentation, PresentEndpoint(suite.TestEndpoint), "return the correct Representation of RootResource")
}
73 changes: 71 additions & 2 deletions endpoint.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package hyperdrive

import "net/http"
import (
"net/http"
"strings"

"github.com/gorilla/handlers"
)

// GetHandler interface is satisfied if the endpoint has implemented
// a http.Handler method called Get(). If this is not implemented,
Expand Down Expand Up @@ -52,7 +57,8 @@ type OptionsHandler interface {
}

// Endpointer interface provides flexibility in how endpoints are created
// allowing for expressiveness how developers make use of this package.
// allowing for expressiveness in how developers make use of the hyperdrive
// package.
type Endpointer interface {
GetName() string
GetDesc() string
Expand Down Expand Up @@ -92,3 +98,66 @@ func (e *Endpoint) GetPath() string {
func NewEndpoint(name string, desc string, path string) *Endpoint {
return &Endpoint{EndpointName: name, EndpointDesc: desc, EndpointPath: path}
}

// GetMethods returns a slice of the methods an Endpoint supports.
func GetMethods(e Endpointer) []string {
var methods = []string{"OPTIONS"}

if _, ok := interface{}(e).(GetHandler); ok {
methods = append(methods, "GET")
}

if _, ok := interface{}(e).(PostHandler); ok {
methods = append(methods, "POST")
}

if _, ok := interface{}(e).(PutHandler); ok {
methods = append(methods, "PUT")
}

if _, ok := interface{}(e).(PatchHandler); ok {
methods = append(methods, "PATCH")
}

if _, ok := interface{}(e).(DeleteHandler); ok {
methods = append(methods, "DELETE")
}

return methods
}

// GetMethodsList returns a list of the methods an Endpoint supports.
func GetMethodsList(e Endpointer) string {
return strings.Join(GetMethods(e), ", ")
}

// NewMethodHandler sets the correct http.Handler for each method, depending on
// the interfaces the Enpointer supports. It returns an http.HandlerFunc, ready
// to be served directly, wrapped in other middleware, etc.
func NewMethodHandler(e Endpointer) http.HandlerFunc {
handler := make(handlers.MethodHandler)
if h, ok := interface{}(e).(GetHandler); ok {
handler["GET"] = http.HandlerFunc(h.Get)
}

if h, ok := interface{}(e).(PostHandler); ok {
handler["POST"] = http.HandlerFunc(h.Post)
}

if h, ok := interface{}(e).(PutHandler); ok {
handler["PUT"] = http.HandlerFunc(h.Put)
}

if h, ok := interface{}(e).(PatchHandler); ok {
handler["PATCH"] = http.HandlerFunc(h.Patch)
}

if h, ok := interface{}(e).(DeleteHandler); ok {
handler["DELETE"] = http.HandlerFunc(h.Delete)
}

if h, ok := interface{}(e).(OptionsHandler); ok {
handler["OPTIONS"] = http.HandlerFunc(h.Options)
}
return http.HandlerFunc(handler.ServeHTTP)
}
29 changes: 29 additions & 0 deletions endpoint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package hyperdrive

func (suite *HyperdriveTestSuite) TestNewEndpoint() {
suite.IsType(&Endpoint{}, suite.TestEndpoint, "expects an instance of hyperdrive.Endpoint")
}

func (suite *HyperdriveTestSuite) TestGetName() {
suite.Equal("Test", suite.TestEndpoint.GetName(), "expects GetName() to return Name")
}

func (suite *HyperdriveTestSuite) TestGetDesc() {
suite.Equal("Test Endpoint", suite.TestEndpoint.GetDesc(), "expects GetDesc() to return Desc")
}

func (suite *HyperdriveTestSuite) TestGetPath() {
suite.Equal("/test", suite.TestEndpoint.GetPath(), "expects GetPath() to return Path")
}

func (suite *HyperdriveTestSuite) TestEndpointer() {
suite.Implements((*Endpointer)(nil), suite.TestEndpoint, "expects an implementation of hyperdrive.Endpointer interface")
}

func (suite *HyperdriveTestSuite) TestGetMethods() {
suite.Equal([]string{"OPTIONS"}, GetMethods(suite.TestEndpoint), "expects a slice of supported method strings")
}

func (suite *HyperdriveTestSuite) TestGetMethodsList() {
suite.Equal("OPTIONS", GetMethodsList(suite.TestEndpoint), "expects a list of supported method strings")
}
46 changes: 15 additions & 31 deletions hyperdrive.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,31 @@ import (
"net/http"
"time"

"github.com/gorilla/handlers"
"github.com/gorilla/mux"
)

// API is a logical collection of one or more endpoints, connecting requests
// to the response handlers using a gorlla mux Router.
type API struct {
Name string
Desc string
Router *mux.Router
Server *http.Server
Root *RootResource
conf Config
endpoints []Endpoint
}

// NewAPI creates an instance of an API with an initialized Router.
func NewAPI() API {
api := API{Router: mux.NewRouter(), conf: NewConfig()}
// NewAPI creates an instance of API, with an initialized Router, Config, Server, and RootResource.
func NewAPI(name string, desc string) API {
api := API{
Name: name,
Desc: desc,
Router: mux.NewRouter(),
conf: NewConfig(),
}
api.Root = NewRootResource(api)
api.Router.Handle("/", api.DefaultMiddlewareChain(api.Root)).Methods("GET")
api.Server = &http.Server{
Handler: api.Router,
Addr: api.conf.GetPort(),
Expand All @@ -34,33 +43,8 @@ func NewAPI() API {
// respond with a 405 error if the endpoint does not support a particular
// HTTP method.
func (api *API) AddEndpoint(e Endpointer) {
handler := make(handlers.MethodHandler)
if h, ok := interface{}(e).(GetHandler); ok {
handler["GET"] = http.HandlerFunc(h.Get)
}

if h, ok := interface{}(e).(PostHandler); ok {
handler["POST"] = http.HandlerFunc(h.Post)
}

if h, ok := interface{}(e).(PutHandler); ok {
handler["PUT"] = http.HandlerFunc(h.Put)
}

if h, ok := interface{}(e).(PatchHandler); ok {
handler["PATCH"] = http.HandlerFunc(h.Patch)
}

if h, ok := interface{}(e).(DeleteHandler); ok {
handler["DELETE"] = http.HandlerFunc(h.Delete)
}

if h, ok := interface{}(e).(OptionsHandler); ok {
handler["OPTIONS"] = http.HandlerFunc(h.Options)
}

middleware := api.LoggingMiddleware(api.RecoveryMiddleware(http.HandlerFunc(handler.ServeHTTP)))
api.Router.Handle(e.GetPath(), middleware)
api.Root.AddEndpointer(e)
api.Router.Handle(e.GetPath(), api.DefaultMiddlewareChain(NewMethodHandler(e)))
}

// Start starts the configured http server, listening on the configured Port
Expand Down
Loading