Skip to content

Commit

Permalink
Allow user (or config) to explicitely request the list total metadata (
Browse files Browse the repository at this point in the history
…#69)

Fix: #68
  • Loading branch information
rs committed Dec 24, 2016
1 parent 3505194 commit cbf74b0
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 4 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -655,6 +655,7 @@ The `resource.Conf` type has the following customizable properties:
| ------------------------ | -------------
| `AllowedModes` | A list of `resource.Mode` allowed for the resource.
| `PaginationDefaultLimit` | If set, pagination is enabled by default with a number of item per page defined here.
| `ForceTotal` | Control the behavior of the computation of `X-Total` header and the `total` query-string parameter. See `resource.ForceTotalMode` for available options.

### Modes

Expand Down
28 changes: 28 additions & 0 deletions resource/conf.go
Expand Up @@ -8,8 +8,36 @@ type Conf struct {
// no default page size is set resulting in no pagination if no `limit` parameter
// is provided.
PaginationDefaultLimit int
// ForceTotal controls how total number of items on list request is computed.
// By default (TotalOptIn), if the total cannot be computed by the storage
// handler for free, no total metadata is returned until the user explicitely
// request it using the total=1 query-string parameter. Note that if the
// storage cannot compute the total and does not implement the resource.Counter
// interface, a "not implemented" error is returned.
//
// The TotalAlways mode always force the computation of the total (make sure the
// storage either compute the total on Find or implement the resource.Counter
// interface.
//
// TotalDenied prevents the user from requesting the total.
ForceTotal ForceTotalMode
}

// ForceTotalMode defines Conf.ForceTotal modes
type ForceTotalMode int

const (
// TotalOptIn allows the end-user to opt-in to forcing the total count by
// adding the total=1 query-string parameter.
TotalOptIn ForceTotalMode = iota
// TotalAlways always force the total number of items on list requests
TotalAlways
// TotalDenied disallows forcing of the total count, and returns an error
// if total=1 is supplied, and the total count is not provided by the
// Storer's Find method.
TotalDenied
)

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

Expand Down
17 changes: 16 additions & 1 deletion resource/resource.go
Expand Up @@ -310,8 +310,20 @@ func (r *Resource) MultiGet(ctx context.Context, ids []interface{}) (items []*It
return
}

// Find implements Storer interface
// Find calls the Find method on the storage handler with the corresponding pre/post hooks.
func (r *Resource) Find(ctx context.Context, lookup *Lookup, offset, limit int) (list *ItemList, err error) {
return r.find(ctx, lookup, offset, limit, false)
}

// FindWithTotal calls the Find method on the storage handler with the corresponding pre/post hooks.
// If the storage is not able to compute the total, this method will call the Count method on the
// storage. If the storage Find does not compute the total and the Counter interface is not implemented,
// an ErrNotImpemented error is returned.
func (r *Resource) FindWithTotal(ctx context.Context, lookup *Lookup, offset, limit int) (list *ItemList, err error) {
return r.find(ctx, lookup, offset, limit, true)
}

func (r *Resource) find(ctx context.Context, lookup *Lookup, offset, limit int, forceTotal bool) (list *ItemList, err error) {
if LoggerLevel <= LogLevelDebug && Logger != nil {
defer func(t time.Time) {
found := -1
Expand All @@ -327,6 +339,9 @@ func (r *Resource) Find(ctx context.Context, lookup *Lookup, offset, limit int)
}
if err = r.hooks.onFind(ctx, lookup, offset, limit); err == nil {
list, err = r.storage.Find(ctx, lookup, offset, limit)
if err == nil && list.Total == -1 && forceTotal {
list.Total, err = r.storage.Count(ctx, lookup)
}
}
r.hooks.onFound(ctx, lookup, &list, &err)
return
Expand Down
29 changes: 27 additions & 2 deletions resource/storage.go
Expand Up @@ -12,8 +12,9 @@ type Storer interface {
// pagination argument must be respected. If no items are found, an empty list
// should be returned with no error.
//
// If the total number of item can't be easily computed, ItemList.Total should
// be set to -1. The requested page should be set to ItemList.Page.
// If the total number of item can't be computed for free, ItemList.Total must
// be set to -1. Your Storer may implement the Counter interface to let the user
// explicitely request the total.
//
// The whole lookup query must be treated. If a query operation is not implemented
// by the storage handler, a resource.ErrNotImplemented must be returned.
Expand Down Expand Up @@ -81,9 +82,20 @@ type MultiGetter interface {
MultiGet(ctx context.Context, ids []interface{}) ([]*Item, error)
}

// Counter is an optional interface a Storer can implement to provide a way to explicitely
// count the total number of elements a given lookup would return with no pagination
// provided. This method is called by REST Layer when the storage handler returned -1
// as ItemList.Total and the user (or configuration) explicitely request the total.
type Counter interface {
// Count returns the total number of item in the collection given the provided
// lookup filter.
Count(ctx context.Context, lookup *Lookup) (int, error)
}

type storageHandler interface {
Storer
MultiGetter
Counter
Get(ctx context.Context, id interface{}) (item *Item, err error)
}

Expand Down Expand Up @@ -228,3 +240,16 @@ func (s storageWrapper) Clear(ctx context.Context, lookup *Lookup) (deleted int,
}
return s.Storer.Clear(ctx, lookup)
}

func (s storageWrapper) Count(ctx context.Context, lookup *Lookup) (total int, err error) {
if s.Storer == nil {
return -1, ErrNoStorage
}
if ctx.Err() != nil {
return -1, ctx.Err()
}
if c, ok := s.Storer.(Counter); ok {
return c.Count(ctx, lookup)
}
return -1, ErrNotImplemented
}
21 changes: 20 additions & 1 deletion rest/method_get.go
Expand Up @@ -6,6 +6,8 @@ import (
"net/http"
"net/url"
"strconv"

"github.com/rs/rest-layer/resource"
)

// listGet handles GET resquests on a resource URL
Expand Down Expand Up @@ -45,11 +47,28 @@ func listGet(ctx context.Context, r *http.Request, route *RouteMatch) (status in
}
offset = (page-1)*limit + skip
}
forceTotal := false
switch rsrc.Conf().ForceTotal {
case resource.TotalOptIn:
forceTotal = route.Params.Get("total") == "1"
case resource.TotalAlways:
forceTotal = true
case resource.TotalDenied:
if route.Params.Get("total") == "1" {
return 422, nil, &Error{422, "Cannot use `total' parameter: denied by configuration", nil}
}
}
lookup, e := route.Lookup()
if e != nil {
return e.Code, nil, e
}
list, err := rsrc.Find(ctx, lookup, offset, limit)
var list *resource.ItemList
var err error
if forceTotal {
list, err = rsrc.FindWithTotal(ctx, lookup, offset, limit)
} else {
list, err = rsrc.Find(ctx, lookup, offset, limit)
}
if err != nil {
e = NewError(err)
return e.Code, nil, e
Expand Down

0 comments on commit cbf74b0

Please sign in to comment.