Skip to content

Commit

Permalink
Update documentation, add some examples and unexport some types
Browse files Browse the repository at this point in the history
  • Loading branch information
Olivier Poitrey committed Aug 7, 2015
1 parent 2c1abb4 commit c899247
Show file tree
Hide file tree
Showing 20 changed files with 358 additions and 105 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -709,11 +709,11 @@ A resource handler is easy to write though. Some handlers for popular databases
```go
type ResourceHandler interface {
Find(ctx context.Context, lookup *rest.Lookup, page, perPage int) (*rest.ItemList, *rest.Error)
Find(ctx context.Context, lookup rest.Lookup, page, perPage int) (*rest.ItemList, *rest.Error)
Insert(ctx context.Context, items []*Item) *Error
Update(ctx context.Context, item *Item, original *Item) *Error
Delete(ctx context.Context, item *rest.Item) *rest.Error
Clear(ctx context.Context, lookup *rest.Lookup) (int, *rest.Error)
Clear(ctx context.Context, lookup rest.Lookup) (int, *rest.Error)
}
```
Expand Down
2 changes: 1 addition & 1 deletion conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type Conf struct {
PaginationDefaultLimit int
}

// Mode defines CRUDL modes
// Mode defines CRUDL modes to be used with Conf.AllowedModes.
type Mode int

const (
Expand Down
22 changes: 22 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
Package rest layer is a REST API framework heavily inspired by the excellent
Python Eve (http://python-eve.org).
It lets you automatically generate a comprehensive, customizable, and secure
REST API on top of any backend storage with no boiler plate code. You can focus
on your business logic now.
Implemented as a `net/http` middleware, it plays well with other middlewares like
CORS (http://github.com/rs/cors).
REST Layer is an opinionated framework. Unlike many web frameworks, you don't
directly control the routing. You just expose resources and sub-resources, the
framework automatically figures what routes to generate behind the scene.
You don't have to take care of the HTTP headers and response, JSON encoding, etc.
either. rest handles HTTP conditional requests, caching, integrity checking for
you. A powerful and extensible validation engine make sure that data comes
pre-validated to you resource handlers. Generic resource handlers for MongoDB and
other databases are also available so you have few to no code to write to make
the whole system work.
*/
package rest
9 changes: 6 additions & 3 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ var (
UnknownError = &Error{520, "Unknown Error", nil}
)

// Error defines a REST error
// Error defines a REST error with optional per fields error details
type Error struct {
// Code defines the error code to be used for the error and for the HTTP status
Code int
Expand All @@ -36,7 +36,10 @@ type Error struct {
}

// ContextError takes a context.Context error returned by ctx.Err() and return the
// appropriate rest.Error
// appropriate rest.Error.
//
// This method is to be used with `net/context` when the context's deadline is reached.
// Pass the output or `ctx.Err()` to this method to get the corresponding rest.Error.
func ContextError(err error) *Error {
switch err {
case context.Canceled:
Expand All @@ -46,7 +49,7 @@ func ContextError(err error) *Error {
case nil:
return nil
default:
return UnknownError
return &Error{520, err.Error(), nil}
}
}

Expand Down
2 changes: 1 addition & 1 deletion errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ func TestContextError(t *testing.T) {
assert.Equal(t, ClientClosedRequestError, ContextError(context.Canceled))
assert.Equal(t, GatewayTimeoutError, ContextError(context.DeadlineExceeded))
assert.Nil(t, ContextError(nil))
assert.Equal(t, UnknownError, ContextError(errors.New("test")))
assert.Equal(t, &Error{520, "test", nil}, ContextError(errors.New("test")))
}

func TestError(t *testing.T) {
Expand Down
141 changes: 141 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package rest_test

import (
"log"
"net/http"
"net/url"

"github.com/rs/cors"
"github.com/rs/rest-layer"
"github.com/rs/rest-layer-mem"
"github.com/rs/rest-layer/schema"
)

func Example() {
var (
// Define a user resource schema
user = schema.Schema{
"id": schema.Field{
Required: true,
// When a field is read-only, on default values or hooks can
// set their value. The client can't change it.
ReadOnly: true,
// This is a field hook called when a new user is created.
// The schema.NewID hook is a provided hook to generate a
// unique id when no value is provided.
OnInit: &schema.NewID,
// The Filterable and Sortable allows usage of filter and sort
// on this field in requests.
Filterable: true,
Sortable: true,
Validator: &schema.String{
Regexp: "^[0-9a-f]{32}$",
},
},
"created": schema.Field{
Required: true,
ReadOnly: true,
Filterable: true,
Sortable: true,
OnInit: &schema.Now,
Validator: &schema.Time{},
},
"updated": schema.Field{
Required: true,
ReadOnly: true,
Filterable: true,
Sortable: true,
OnInit: &schema.Now,
// The OnUpdate hook is called when the item is edited. Here we use
// provided Now hook which just return the current time.
OnUpdate: &schema.Now,
Validator: &schema.Time{},
},
// Define a name field as required with a string validator
"name": schema.Field{
Required: true,
Filterable: true,
Validator: &schema.String{
MaxLen: 150,
},
},
}

// Define a post resource schema
post = schema.Schema{
// schema.*Field are shortcuts for common fields (identical to users' same fields)
"id": schema.IDField,
"created": schema.CreatedField,
"updated": schema.UpdatedField,
// Define a user field which references the user owning the post.
// See bellow, the content of this field is enforced by the fact
// that posts is a sub-resource of users.
"user": schema.Field{
Required: true,
Filterable: true,
Validator: &schema.Reference{
Path: "users",
},
},
"public": schema.Field{
Filterable: true,
Validator: &schema.Bool{},
},
// Sub-documents are handled via a sub-schema
"meta": schema.Field{
Schema: &schema.Schema{
"title": schema.Field{
Required: true,
Validator: &schema.String{
MaxLen: 150,
},
},
"body": schema.Field{
Validator: &schema.String{
MaxLen: 100000,
},
},
},
},
}
)

// Create a REST API root resource
root := rest.New()

// Add a resource on /users[/:user_id]
users := root.Bind("users", rest.NewResource(user, mem.NewHandler(), rest.Conf{
// We allow all REST methods
// (rest.ReadWrite is a shortcut for []rest.Mode{Create, Read, Update, Delete, List})
AllowedModes: rest.ReadWrite,
}))

// Bind a sub resource on /users/:user_id/posts[/:post_id]
// and reference the user on each post using the "user" field of the posts resource.
posts := users.Bind("posts", "user", rest.NewResource(post, mem.NewHandler(), rest.Conf{
// Posts can only be read, created and deleted, not updated
AllowedModes: []rest.Mode{rest.Read, rest.List, rest.Create, rest.Delete},
}))

// Add a friendly alias to public posts
// (equivalent to /users/:user_id/posts?filter={"public":true})
posts.Alias("public", url.Values{"filter": []string{"{\"public\"=true}"}})

// Create API HTTP handler for the resource graph
api, err := rest.NewHandler(root)
if err != nil {
log.Fatalf("Invalid API configuration: %s", err)
}

// Add cors support
h := cors.New(cors.Options{OptionsPassthrough: true}).Handler(api)

// Bind the API under /api/ path
http.Handle("/api/", http.StripPrefix("/api/", h))

// Serve it
log.Print("Serving API on http://localhost:8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
2 changes: 1 addition & 1 deletion handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"golang.org/x/net/context"
)

// Handler is an HTTP handler used to serve the configured REST API
// Handler is a net/http compatible handler used to serve the configured REST API
type Handler struct {
// ResponseSender can be changed to extend the DefaultResponseSender
ResponseSender ResponseSender
Expand Down
21 changes: 17 additions & 4 deletions item.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,29 @@ import (

// Item represents an instance of an item
type Item struct {
ID interface{}
Etag string
// ID is used to uniquely identify the item in the resource collection.
ID interface{}
// ETag is an opaque identifier assigned by REST Layer to a specific version of the item.
//
// This ETag is used perform conditional requests and to ensure storage handler doesn't
// update an outdated version of the resource.
ETag string
// Updated stores the last time the item was updated. This field is used to populate the
// Last-Modified header and to handle conditional requests.
Updated time.Time
// Payload the actual data of the item
Payload map[string]interface{}
}

// ItemList represents a list of items
type ItemList struct {
// Total defines the total number of items in the collection matching the current
// context. If the storage handler cannot compute this value, -1 is set.
Total int
Page int
// Page is the current page represented by this ItemList.
Page int
// Items is the list of items contained in the current page given the current
// context.
Items []*Item
}

Expand All @@ -33,7 +46,7 @@ func NewItem(payload map[string]interface{}) (*Item, error) {
}
item := &Item{
ID: id,
Etag: etag,
ETag: etag,
Updated: time.Now(),
Payload: payload,
}
Expand Down
2 changes: 1 addition & 1 deletion item_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
func TestNewItem(t *testing.T) {
i, err := NewItem(map[string]interface{}{"id": 1})
assert.NoError(t, err)
assert.Equal(t, "d2ce28b9a7fd7e4407e2b0fd499b7fe4", i.Etag)
assert.Equal(t, "d2ce28b9a7fd7e4407e2b0fd499b7fe4", i.ETag)
}

func TestNewItemNoID(t *testing.T) {
Expand Down
54 changes: 34 additions & 20 deletions lookup.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,47 @@ import (
"github.com/rs/rest-layer/schema"
)

// Lookup holds key/value pairs used to select items on a resource
type Lookup struct {
// Lookup holds filter and sort used to select items in a resource collection
type Lookup interface {
// The client supplied filter. Filter is a MongoDB inspired query with a more limited
// set of capabilities. See [README](https://github.com/rs/rest-layer#filtering)
// for more info.
Filter schema.Query
Filter() schema.Query
// The client supplied soft. Sort is a list of resource fields or sub-fields separated
// by comas (,). To invert the sort, a minus (-) can be prefixed.
// See [README](https://github.com/rs/rest-layer#sorting) for more info.
Sort []string
Sort() []string
}

// NewLookup creates an empty lookup object
func NewLookup() *Lookup {
return &Lookup{
Filter: schema.Query{},
Sort: []string{},
type lookup struct {
// The client supplied filter. Filter is a MongoDB inspired query with a more limited
// set of capabilities. See [README](https://github.com/rs/rest-layer#filtering)
// for more info.
filter schema.Query
// The client supplied soft. Sort is a list of resource fields or sub-fields separated
// by comas (,). To invert the sort, a minus (-) can be prefixed.
// See [README](https://github.com/rs/rest-layer#sorting) for more info.
sort []string
}

// newLookup creates an empty lookup object
func newLookup() *lookup {
return &lookup{
filter: schema.Query{},
sort: []string{},
}
}

// SetSort parses and validate a sort parameter and set it as lookup's Sort
func (l *Lookup) SetSort(sort string, validator schema.Validator) error {
func (l *lookup) Sort() []string {
return l.sort
}

func (l *lookup) Filter() schema.Query {
return l.filter
}

// setSort parses and validate a sort parameter and set it as lookup's Sort
func (l *lookup) setSort(sort string, validator schema.Validator) error {
sorts := []string{}
for _, f := range strings.Split(sort, ",") {
f = strings.Trim(f, " ")
Expand All @@ -52,24 +71,19 @@ func (l *Lookup) SetSort(sort string, validator schema.Validator) error {
}
sorts = append(sorts, f)
}
l.Sort = sorts
l.sort = sorts
return nil
}

// SetFilter parses and validate a filter parameter and set it as lookup's Filter
// setFilter parses and validate a filter parameter and set it as lookup's Filter
//
// The filter query is validated against the provided validator to ensure all queried
// fields exists and are of the right type.
func (l *Lookup) SetFilter(filter string, validator schema.Validator) error {
func (l *lookup) setFilter(filter string, validator schema.Validator) error {
f, err := schema.ParseQuery(filter, validator)
if err != nil {
return err
}
l.Filter = f
l.filter = f
return nil
}

// Match evaluates lookup's filter on the provided payload and tells if it match
func (l *Lookup) Match(payload map[string]interface{}) bool {
return l.Filter.Match(payload)
}
Loading

0 comments on commit c899247

Please sign in to comment.