Safe Haskell | None |
---|---|
Language | Haskell2010 |
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 nextRange
header 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 instanceHasPagination
Color "name" where typeRangeType
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 ::
API) server)Server
API server mrange = do let range =fromMaybe
defaultRange (mrange >>=extractRange
)returnRange
range (applyRange
range colors) main ::IO
() main =run
1337 (serve
(Proxy
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 = (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 KnownSymbol field => HasPagination resource field where
- 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
- 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.
(KnownSymbol field, IsRangeType a) => Range | |
|
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
Instances
(ToHttpApiData (AcceptRanges (f ': fs)), KnownSymbol field) => ToHttpApiData (AcceptRanges (field ': (f ': fs))) Source # | |
Defined in Servant.Pagination toUrlPiece :: AcceptRanges (field ': (f ': fs)) -> Text # toEncodedUrlPiece :: AcceptRanges (field ': (f ': fs)) -> Builder # toHeader :: AcceptRanges (field ': (f ': fs)) -> ByteString # toQueryParam :: AcceptRanges (field ': (f ': fs)) -> Text # | |
KnownSymbol field => ToHttpApiData (AcceptRanges (field ': ([] :: [Symbol]))) Source # | |
Defined in Servant.Pagination toUrlPiece :: AcceptRanges (field ': []) -> Text # toEncodedUrlPiece :: AcceptRanges (field ': []) -> Builder # toHeader :: AcceptRanges (field ': []) -> ByteString # toQueryParam :: AcceptRanges (field ': []) -> Text # |
data ContentRange (fields :: [Symbol]) resource Source #
Actual range returned, in the `Content-Range` response's header
(KnownSymbol field, ToHttpApiData (RangeType resource field)) => ContentRange | |
|
Instances
ToHttpApiData (ContentRange fields res) Source # | |
Defined in Servant.Pagination toUrlPiece :: ContentRange fields res -> Text # toEncodedUrlPiece :: ContentRange fields res -> Builder # toHeader :: ContentRange fields res -> ByteString # toQueryParam :: ContentRange fields res -> Text # |
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
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.
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
RangeOptions | |
|
Instances
Eq RangeOptions Source # | |
Defined in Servant.Pagination (==) :: RangeOptions -> RangeOptions -> Bool # (/=) :: RangeOptions -> RangeOptions -> Bool # | |
Show RangeOptions Source # | |
Defined in Servant.Pagination showsPrec :: Int -> RangeOptions -> ShowS # show :: RangeOptions -> String # showList :: [RangeOptions] -> ShowS # |
defaultOptions :: RangeOptions Source #
Some default options of default values for a Range (limit 100; offset 0; order desc)
Use Ranges
:: (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"UTCTime
extractDateRange =extractRange
putRange :: (PutRange fields field, HasPagination resource field) => Range field (RangeType resource field) -> Ranges fields resource Source #
:: (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 |
Lift an API response in a Monad
, typically a Handler
. Ranges
headers can be quite cumbersome to
declare and can be deduced from the resources returned and the previous Range
. This is exactly what this function
does.
myHandler ::Maybe
(Ranges
'["created_at"] Resource) ->Handler
(Headers
(PageHeaders
'["created_at"] Resource) [Resource]) myHandler mrange = let range =fromMaybe
(getDefaultRange
(Proxy
@Resource)) (mrange >>=extractRange
)returnRange
range (applyRange
range resources)
:: 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.