| Safe Haskell | Safe-Inferred |
|---|---|
| Language | Haskell2010 |
Servant.Pagination
Contents
Description
Opinionated Pagination Helpers for Servant APIs
Client can provide a Range header with their request with the following format
Range: <field> [<value>][; offset <o>][; limit <l>][; order <asc|desc>]
Available ranges are declared using type-level list of accepted fields, bound to a given
resource and type using the HasPagination type-class. The library provides unobtrusive
types and abstract away all the plumbing to hook that on an existing Servant API.
The IsRangeType constraints summarize all constraints that must apply to a possible field
and heavily rely on the FromHttpApiData and ToHttpApiData.
$ curl -v http://localhost:1337/colors -H 'Range: name; limit 10' > GET /colors HTTP/1.1 > Host: localhost:1337 > User-Agent: curl/7.47.0 > Accept: */* > < HTTP/1.1 206 Partial Content < Transfer-Encoding: chunked < Date: Tue, 30 Jan 2018 12:45:17 GMT < Server: Warp/3.2.13 < Content-Type: application/json;charset=utf-8 < Accept-Ranges: name < Content-Range: name Yellow..Purple < Next-Range: name Purple;limit 10;offset 1;order desc
The Range header is totally optional, but when provided, it indicates to the server what
parts of the collection is requested. As a reponse and in addition to the data, the server may
provide 3 headers to the client:
Accept-Ranges: A comma-separated list of field upon which a range can be definedContent-Range: Actual range corresponding to the content being returnedNext-Range: Indicate what should be the nextRangeheader in order to retrieve the next range
This allows the client to work in a very _dumb_ mode where it simply consumes data from the server using the value of the 'Next-Range' header to fetch each new batch of data. The 'Accept-Ranges' comes in handy to self-document the API telling the client about the available filtering and sorting options of a resource.
Here's a minimal example used to obtained the previous behavior; Most of the magic happens in the
returnRange function which lift a collection of resources into a Servant handler, computing the
corresponding ranges from the range used to retrieve the resources.
-- Resource Type
data Color = Color
{ name :: String
, rgb :: [Int]
, hex :: String
} deriving (Eq, Show, Generic)
colors :: [Color]
colors = [ {- ... -} ]
-- Ranges definitions
instance HasPagination Color "name" where
type RangeType Color "name" = String
getFieldValue _ = name
-- API
type API =
"colors"
:> Header "Range" (Ranges '["name"] Color)
:> GetPartialContent '[JSON] (Headers (PageHeaders '["name"] Color) [Color])
-- Application
defaultRange :: Range "name" String
defaultRange =
getDefaultRange (Proxy @Color)
server :: Server API
server mrange = do
let range =
fromMaybe defaultRange (mrange >>= extractRange)
returnRange range (applyRange range colors)
main :: IO ()
main =
run 1337 (serve (Proxy @API) server)
Synopsis
- data Ranges :: [Symbol] -> * -> *
- data Range (field :: Symbol) (a :: *) = (KnownSymbol field, IsRangeType a) => Range {
- rangeValue :: Maybe a
- rangeLimit :: Int
- rangeOffset :: Int
- rangeOrder :: RangeOrder
- rangeField :: Proxy field
- data RangeOrder
- data AcceptRanges (fields :: [Symbol]) = AcceptRanges
- data ContentRange (fields :: [Symbol]) resource = forall field.(KnownSymbol field, ToHttpApiData (RangeType resource field)) => ContentRange {
- contentRangeStart :: RangeType resource field
- contentRangeEnd :: RangeType resource field
- contentRangeField :: Proxy field
- type PageHeaders (fields :: [Symbol]) (resource :: *) = '[Header "Accept-Ranges" (AcceptRanges fields), Header "Content-Range" (ContentRange fields resource), Header "Next-Range" (Ranges fields resource)]
- type IsRangeType a = (Show a, Ord a, Eq a, FromHttpApiData a, ToHttpApiData a)
- class PutRange (fields :: [Symbol]) (field :: Symbol)
- class ExtractRange (fields :: [Symbol]) (field :: Symbol)
- class KnownSymbol field => HasPagination resource field where
- type RangeType resource field :: *
- getFieldValue :: Proxy field -> resource -> RangeType resource field
- getRangeOptions :: Proxy field -> Proxy resource -> RangeOptions
- getDefaultRange :: IsRangeType (RangeType resource field) => Proxy resource -> Range field (RangeType resource field)
- data RangeOptions = RangeOptions {}
- defaultOptions :: RangeOptions
- extractRange :: (ExtractRange fields field, HasPagination resource field) => Ranges fields resource -> Maybe (Range field (RangeType resource field))
- putRange :: (PutRange fields field, HasPagination resource field) => Range field (RangeType resource field) -> Ranges fields resource
- addPageHeaders :: (ToHttpApiData (AcceptRanges fields), KnownSymbol field, HasPagination resource field, IsRangeType (RangeType resource field), PutRange fields field) => Range field (RangeType resource field) -> [resource] -> Headers (PageHeaders fields resource) [resource]
- returnRange :: (Monad m, ToHttpApiData (AcceptRanges fields), KnownSymbol field, HasPagination resource field, IsRangeType (RangeType resource field), PutRange fields field) => Range field (RangeType resource field) -> [resource] -> m (Headers (PageHeaders fields resource) [resource])
- applyRange :: HasPagination resource field => Range field (RangeType resource field) -> [resource] -> [resource]
Types
data Ranges :: [Symbol] -> * -> * Source #
A type to specify accepted Ranges via the Range HTTP Header. For example:
type API = "resources" :>Header"Range" (Ranges'["created_at"] Resource) :>Get'[JSON] [Resource]
Instances
data Range (field :: Symbol) (a :: *) Source #
An actual Range instance obtained from parsing / to generate a Range HTTP Header.
Constructors
| (KnownSymbol field, IsRangeType a) => Range | |
Fields
| |
data RangeOrder Source #
Define the sorting order of the paginated resources (ascending or descending)
Instances
data AcceptRanges (fields :: [Symbol]) Source #
Accepted Ranges in the `Accept-Ranges` response's header
Constructors
| AcceptRanges |
Instances
| (ToHttpApiData (AcceptRanges (f ': fs)), KnownSymbol field) => ToHttpApiData (AcceptRanges (field ': (f ': fs))) Source # | |
Defined in Servant.Pagination Methods toUrlPiece :: AcceptRanges (field ': (f ': fs)) -> Text # toEncodedUrlPiece :: AcceptRanges (field ': (f ': fs)) -> Builder # toHeader :: AcceptRanges (field ': (f ': fs)) -> ByteString # toQueryParam :: AcceptRanges (field ': (f ': fs)) -> Text # toEncodedQueryParam :: AcceptRanges (field ': (f ': fs)) -> Builder # | |
| KnownSymbol field => ToHttpApiData (AcceptRanges '[field]) Source # | |
Defined in Servant.Pagination Methods toUrlPiece :: AcceptRanges '[field] -> Text # toEncodedUrlPiece :: AcceptRanges '[field] -> Builder # toHeader :: AcceptRanges '[field] -> ByteString # toQueryParam :: AcceptRanges '[field] -> Text # toEncodedQueryParam :: AcceptRanges '[field] -> Builder # | |
data ContentRange (fields :: [Symbol]) resource Source #
Actual range returned, in the `Content-Range` response's header
Constructors
| forall field.(KnownSymbol field, ToHttpApiData (RangeType resource field)) => ContentRange | |
Fields
| |
Instances
| ToHttpApiData (ContentRange fields res) Source # | |
Defined in Servant.Pagination Methods toUrlPiece :: ContentRange fields res -> Text # toEncodedUrlPiece :: ContentRange fields res -> Builder # toHeader :: ContentRange fields res -> ByteString # toQueryParam :: ContentRange fields res -> Text # toEncodedQueryParam :: ContentRange fields res -> Builder # | |
type PageHeaders (fields :: [Symbol]) (resource :: *) = '[Header "Accept-Ranges" (AcceptRanges fields), Header "Content-Range" (ContentRange fields resource), Header "Next-Range" (Ranges fields resource)] Source #
type IsRangeType a = (Show a, Ord a, Eq a, FromHttpApiData a, ToHttpApiData a) Source #
Set of constraints that must apply to every type target of a Range
class PutRange (fields :: [Symbol]) (field :: Symbol) Source #
Minimal complete definition
Instances
| PutRange (field ': fields) field Source # | |
Defined in Servant.Pagination | |
| PutRange fields field => PutRange (y ': fields) field Source # | |
Defined in Servant.Pagination | |
class ExtractRange (fields :: [Symbol]) (field :: Symbol) Source #
Minimal complete definition
Instances
| ExtractRange (field ': fields) field Source # | |
Defined in Servant.Pagination Methods extractRange :: HasPagination resource field => Ranges (field ': fields) resource -> Maybe (Range field (RangeType resource field)) Source # | |
| ExtractRange fields field => ExtractRange (y ': fields) field Source # | |
Defined in Servant.Pagination Methods extractRange :: HasPagination resource field => Ranges (y ': fields) resource -> Maybe (Range field (RangeType resource field)) Source # | |
Declare Ranges
class KnownSymbol field => HasPagination resource field where Source #
Available Range on a given resource must implements the HasPagination type-class.
This class defines how the library can interact with a given resource to access the value
to which a field refers.
Minimal complete definition
Methods
getFieldValue :: Proxy field -> resource -> RangeType resource field Source #
Get the corressponding value of a Resource
getRangeOptions :: Proxy field -> Proxy resource -> RangeOptions Source #
Get parsing options for the Range defined on this field
getDefaultRange :: IsRangeType (RangeType resource field) => Proxy resource -> Range field (RangeType resource field) Source #
Create a default Range from a value and default RangeOptions. Typical use-case
is for when no or an invalid Range header was provided.
data RangeOptions Source #
Default values to apply when parsing a Range
Constructors
| RangeOptions | |
Fields
| |
Instances
| Show RangeOptions Source # | |
Defined in Servant.Pagination Methods showsPrec :: Int -> RangeOptions -> ShowS # show :: RangeOptions -> String # showList :: [RangeOptions] -> ShowS # | |
| Eq RangeOptions Source # | |
Defined in Servant.Pagination | |
defaultOptions :: RangeOptions Source #
Some default options of default values for a Range (limit 100; offset 0; order desc)
Use Ranges
Arguments
| :: (ExtractRange fields field, HasPagination resource field) | |
| => Ranges fields resource | A list of accepted Ranges for the API |
| -> Maybe (Range field (RangeType resource field)) | A Range instance of the expected type, if it matches |
Extract a Range from a Ranges. Works like a safe read, trying to coerce a Range instance to
an expected type. Type annotation are most likely necessary to remove ambiguity. Note that a Range
can only be extracted to a type bound by the allowed fields on a given resource.
extractDateRange ::Ranges'["created_at", "name"] Resource ->Range"created_at"UTCTimeextractDateRange =extractRange
putRange :: (PutRange fields field, HasPagination resource field) => Range field (RangeType resource field) -> Ranges fields resource Source #
Arguments
| :: (ToHttpApiData (AcceptRanges fields), KnownSymbol field, HasPagination resource field, IsRangeType (RangeType resource field), PutRange fields field) | |
| => Range field (RangeType resource field) | Actual |
| -> [resource] | Resources to return, fetched from a db or a local store |
| -> Headers (PageHeaders fields resource) [resource] | The same resources, but with pagination headers |
Add headers representing a Range to a list of resources.
Ranges headers can be quite cumbersome to declare and can be deduced from a
collection of resources together with the Range used to retrieve it, so this function
is a shortcut for that.
myHandler ::Maybe(Ranges'["created_at"] Resource) ->Handler(Headers(PageHeaders'["created_at"] Resource) [Resource]) myHandler mrange = let range =fromMaybe(getDefaultRange(Proxy@Resource)) (mrange >>=extractRange)return(addPageHeadersrange (applyRangerange resources))
Arguments
| :: (Monad m, ToHttpApiData (AcceptRanges fields), KnownSymbol field, HasPagination resource field, IsRangeType (RangeType resource field), PutRange fields field) | |
| => Range field (RangeType resource field) | Actual |
| -> [resource] | Resources to return, fetched from a db or a local store |
| -> m (Headers (PageHeaders fields resource) [resource]) | Resources embedded in a given |
returnRangerange rs =return(addPageHeadersrange rs)
Arguments
| :: HasPagination resource field | |
| => Range field (RangeType resource field) | A |
| -> [resource] | A full-list of |
| -> [resource] | The sublist obtained by applying the |
Helper to apply a Range to a list of values. Most likely useless in practice
as results may come more realistically from a database, but useful for debugging or
testing.