Skip to content

Commit

Permalink
ToOne, ToMany, Mutate resource handler helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
Derek Dowling committed Dec 13, 2015
1 parent a69748e commit cbfed61
Show file tree
Hide file tree
Showing 9 changed files with 347 additions and 190 deletions.
4 changes: 2 additions & 2 deletions Godeps/Godeps.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 18 additions & 5 deletions api.go
Expand Up @@ -18,7 +18,16 @@ type API struct {
Logger *log.Logger
}

// New initializes a new top level API Resource Handler.
// New initializes a new top level API Resource Handler. The most basic implementation
// is:
//
// api := New("", nil)
//
// But also supports prefixing(/<api_prefix>/<routes>) and custom logging via
// log.Logger https://godoc.org/log#Logger:
//
// api := New("v1", log.New(os.Stdout, "apiV1: ", log.Ldate|log.Ltime|log.Lshortfile))
//
func New(prefix string, logger *log.Logger) *API {

// ensure that our top level prefix is "/" prefixed
Expand All @@ -38,16 +47,20 @@ func New(prefix string, logger *log.Logger) *API {
}
}

// AddResource adds a new resource of type "name" to the API's router
func (a *API) AddResource(resource *Resource) {
// Add implements mux support for a given resource which is effectively handled as:
// pat.New("/(prefix/)resource.Plu*)
func (a *API) Add(resource *Resource) {

// add prefix and logger
// ensure the resource is properly prefixed, and has access to the API logger
resource.prefix = a.prefix
resource.Logger = a.Logger

// track our associated resources, will enable auto-generation docs later
a.Resources[resource.Type] = resource

// Add subrouter to main API mux, use Matcher plus catch all
// Add resource wild card to the API mux. Use the resources Matcher() function
// after an API prefix is applied, as it does the dirty work of building the route
// automatically for us
a.Mux.HandleC(pat.New(resource.Matcher()+"*"), resource)
}

Expand Down
2 changes: 1 addition & 1 deletion api_test.go
Expand Up @@ -14,7 +14,7 @@ func TestAPI(t *testing.T) {

Convey("->AddResource()", func() {
resource := NewMockResource("", "test", 1, nil)
api.AddResource(resource)
api.Add(resource)

So(resource.prefix, ShouldEqual, "/foo")
So(api.Resources["test"], ShouldEqual, resource)
Expand Down
70 changes: 70 additions & 0 deletions mock_storage.go
@@ -0,0 +1,70 @@
package jshapi

import (
"log"
"strconv"

"github.com/derekdowling/go-json-spec-handler"
"golang.org/x/net/context"
)

// MockStorage allows you to mock out APIs really easily, and is also used internally
// for testing the API layer.
type MockStorage struct {
// ResourceType is the name of the resource you are mocking i.e. "user", "comment"
ResourceType string
// ResourceAttributes a sample set of attributes a resource object should have
// used by GET /resources and GET /resources/:id
ResourceAttributes interface{}
// ListCount is the number of sample objects to return in a GET /resources request
ListCount int
}

// Save assigns a URL of 1 to the object
func (m *MockStorage) Save(ctx context.Context, object *jsh.Object) (*jsh.Object, *jsh.Error) {
object.ID = "1"
return object, nil
}

// Get returns a resource with ID as specified by the request
func (m *MockStorage) Get(ctx context.Context, id string) (*jsh.Object, *jsh.Error) {
return m.SampleObject(id), nil
}

// List returns a sample list
func (m *MockStorage) List(ctx context.Context) (jsh.List, *jsh.Error) {
return m.SampleList(m.ListCount), nil
}

// Update does nothing
func (m *MockStorage) Update(ctx context.Context, object *jsh.Object) (*jsh.Object, *jsh.Error) {
return object, nil
}

// Delete does nothing
func (m *MockStorage) Delete(ctx context.Context, id string) *jsh.Error {
return nil
}

// SampleObject builds an object based on provided resource specifications
func (m *MockStorage) SampleObject(id string) *jsh.Object {
object, err := jsh.NewObject(id, m.ResourceType, m.ResourceAttributes)
if err != nil {
log.Fatal(err.Error())
}

return object
}

// SampleList generates a sample list of resources that can be used for/against the
// mock API
func (m *MockStorage) SampleList(length int) jsh.List {

list := jsh.List{}

for id := 1; id <= length; id++ {
list = append(list, m.SampleObject(strconv.Itoa(id)))
}

return list
}
11 changes: 11 additions & 0 deletions relationship.go
@@ -0,0 +1,11 @@
package jshapi

// Relationship helps define the relationship between two resources
type Relationship string

const (
// ToOne signifies a one to one relationship
ToOne Relationship = "One-To-One"
// ToMany signifies a one to many relationship
ToMany Relationship = "One-To-Many"
)
163 changes: 115 additions & 48 deletions resource.go
Expand Up @@ -26,7 +26,7 @@ const (
)

// Resource holds the necessary state for creating a REST API endpoint for a
// given resource type. Will be accessible via `/[prefix/]<type>s` where the
// given resource type. Will be accessible via `/(prefix/)types` where the
// proceeding `prefix/` is only precent if it is not empty.
//
// Using NewCRUDResource you can generate a generic CRUD handler for a
Expand Down Expand Up @@ -56,23 +56,23 @@ type Resource struct {
Type string
// An implementation of Go's standard logger
Logger *log.Logger
// Map of resource type to subresources
Subresources map[string]*Resource
// Prefix is set if the resource is not the top level of URI, "/prefix/resources
Routes []string
prefix string
// Map of relationships
Relationships map[string]Relationship
prefix string
}

// NewResource is a resource constructor that makes no assumptions about routes
// that you'd like to implement, but still provides some basic utilities for
// managing routes and handling API calls.
func NewResource(resourceType string) *Resource {
return &Resource{
Mux: goji.NewMux(),
Type: resourceType,
Subresources: map[string]*Resource{},
Routes: []string{},
prefix: "/",
Mux: goji.NewMux(),
Type: resourceType,
Relationships: map[string]Relationship{},
Routes: []string{},
prefix: "/",
}
}

Expand Down Expand Up @@ -160,15 +160,94 @@ func (res *Resource) Patch(storage store.Update) {
res.addRoute(patch, res.IDMatcher())
}

// ToOne handles the /resources/:id/(relationships/)<resourceType> route which
// represents a One-To-One relationship between the resource and the
// specified resourceType
func (res *Resource) ToOne(
resourceType string,
storage store.Get,
) {
resourceType = strings.TrimSuffix(resourceType, "s")

res.relationshipHandler(
resourceType,
func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
res.getHandler(ctx, w, r, storage)
},
)

res.Relationships[resourceType] = ToOne
}

// ToMany handles the /resources/:id/(relationships/)<resourceType>s route which
// represents a One-To-Many relationship between the resource and the
// specified resourceType
func (res *Resource) ToMany(
resourceType string,
storage store.ToMany,
) {
if !strings.HasSuffix(resourceType, "s") {
resourceType = fmt.Sprintf("%ss", resourceType)
}

res.relationshipHandler(
resourceType,
func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
res.toManyHandler(ctx, w, r, storage)
},
)

res.Relationships[resourceType] = ToMany
}

// relationshipHandler does the dirty work of setting up both routes for a single
// relationship
func (res *Resource) relationshipHandler(
resourceType string,
handler goji.HandlerFunc,
) {

// handle /.../:id/<resourceType>
matcher := fmt.Sprintf("%s/%s", res.IDMatcher(), resourceType)
res.HandleFuncC(
pat.Get(matcher),
handler,
)
res.addRoute(get, matcher)

// handle /.../:id/relationships/<resourceType>
relationshipMatcher := fmt.Sprintf("%s/relationships/%s", res.IDMatcher(), resourceType)
res.HandleFuncC(
pat.Get(relationshipMatcher),
handler,
)
res.addRoute(get, relationshipMatcher)
}

// Mutate allows you to add custom actions to your resource types, it uses the
// GET /(prefix/)resourceTypes/:id/<actionName> path format
func (res *Resource) Mutate(actionName string, storage store.Get) {
matcher := path.Join(res.IDMatcher(), actionName)

res.HandleFuncC(
pat.Get(matcher),
func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
res.mutateHandler(ctx, w, r, storage)
},
)

res.addRoute(patch, matcher)
}

// POST /resources
func (res *Resource) postHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, storage store.Save) {
object, err := jsh.ParseObject(r)
parsedObject, err := jsh.ParseObject(r)
if err != nil {
res.SendAndLog(ctx, w, r, err)
return
}

err = storage(ctx, object)
object, err := storage(ctx, parsedObject)
if err != nil {
res.SendAndLog(ctx, w, r, err)
return
Expand Down Expand Up @@ -216,13 +295,13 @@ func (res *Resource) deleteHandler(ctx context.Context, w http.ResponseWriter, r

// PATCH /resources/:id
func (res *Resource) patchHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, storage store.Update) {
object, err := jsh.ParseObject(r)
parsedObject, err := jsh.ParseObject(r)
if err != nil {
res.SendAndLog(ctx, w, r, err)
return
}

err = storage(ctx, object)
object, err := storage(ctx, parsedObject)
if err != nil {
res.SendAndLog(ctx, w, r, err)
return
Expand All @@ -231,20 +310,30 @@ func (res *Resource) patchHandler(ctx context.Context, w http.ResponseWriter, r
res.SendAndLog(ctx, w, r, object)
}

// NewAction allows you to add custom actions to your resource types, it uses the
// PATCH /(prefix/)resourceTypes/:id/<actionName> path format and expects a store.Update
// storage interface.
func (res *Resource) NewAction(actionName string, storage store.Update) {
matcher := path.Join(res.IDMatcher(), actionName)
// GET /resources/:id/(relationships/)<resourceType>s
func (res *Resource) toManyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, storage store.ToMany) {
id := pat.Param(ctx, "id")

res.HandleFuncC(
pat.Get(matcher),
func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
res.patchHandler(ctx, w, r, storage)
},
)
list, err := storage(ctx, id)
if err != nil {
res.SendAndLog(ctx, w, r, err)
return
}

res.addRoute(patch, matcher)
res.SendAndLog(ctx, w, r, list)
}

// All HTTP Methods for /resources/:id/<mutate>
func (res *Resource) mutateHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, storage store.Get) {
id := pat.Param(ctx, "id")

response, err := storage(ctx, id)
if err != nil {
res.SendAndLog(ctx, w, r, err)
return
}

res.SendAndLog(ctx, w, r, response)
}

// SendAndLog is a jsh wrapper function that handles logging 500 errors and
Expand Down Expand Up @@ -280,36 +369,14 @@ func (res *Resource) addRoute(method string, route string) {
res.Routes = append(res.Routes, fmt.Sprintf("%s - %s", method, route))
}

// NewNestedResource automatically builds a resource with the proper
// prefixes to ensure that it is accessible via /[prefix/]types/:id/subtypes
// and then returns it so you can register route. You can either manually add
// individual routes like normal, or make use of:
// subResource.CRUD(storage)
// to register the equivalent of what NewCRUDResource() gives you.
func (res *Resource) NewNestedResource(resourceType string) *Resource {
subResource := NewResource(resourceType)
subResource.prefix = res.IDMatcher()

res.Subresources[resourceType] = subResource

nestedMatcher := pat.New(fmt.Sprintf("%s/%s*", res.IDMatcher(), subResource.PluralType()))
log.Printf("nestedMatcher.String() = %+v\n", nestedMatcher.String())
res.HandleC(nestedMatcher, subResource)

return subResource
}

// RouteTree prints a recursive route tree based on what the resource, and
// all subresources have registered
func (res *Resource) RouteTree() string {
var routes string

for _, route := range res.Routes {
routes = strings.Join([]string{routes, route}, "\n")
}

for _, resource := range res.Subresources {
routes = strings.Join([]string{routes, resource.RouteTree()}, "\n")
}

return routes
}

0 comments on commit cbfed61

Please sign in to comment.