# servant-pagination [![](https://img.shields.io/hackage/v/servant-pagination.svg)](https://hackage.haskell.org/package/servant-pagination) [![Build Status](https://travis-ci.org/chordify/haskell-servant-pagination.svg?branch=master)](https://travis-ci.org/chordify/haskell-servant-pagination) ## Overview This module offers opinionated helpers to declare a type-safe and a flexible pagination mechanism for Servant APIs. This design, inspired by [Heroku's API](https://devcenter.heroku.com/articles/platform-api-reference#ranges), provides a small framework to communicate about a possible pagination feature of an endpoint, enabling a client to consume the API in different fashions (pagination with offset / limit, endless scroll using last referenced resources, ascending and descending ordering, etc.) Therefore, client can provide a `Range` header with their request with the following format: - `Range: [][; offset ][; limit ][; order ]` For example: `Range: createdAt 2017-01-15T23%3A14%3A67.000Z; offset 5; order desc` indicates that the client is willing to retrieve the next batch of document in descending order that were created after the fifteenth of January, skipping the first 5. As a response, the server may return the list of corresponding document, and augment the response with 3 headers: - `Accept-Ranges`: A comma-separated list of fields upon which a range can be defined - `Content-Range`: Actual range corresponding to the content being returned - `Next-Range`: Indicate what should be the next `Range` header in order to retrieve the next range For example: - `Accept-Ranges: createdAt, modifiedAt` - `Content-Range: createdAt 2017-01-15T23%3A14%3A51.000Z..2017-02-18T06%3A10%3A23.000Z` - `Next-Range: createdAt 2017-02-19T12%3A56%3A28.000Z; offset 0; limit 100; order desc` ## Getting Started Code-wise the integration is quite seamless and unobtrusive. `servant-pagination` provides a `Ranges (fields :: [Symbol]) (resource :: *) -> *` data-type for declaring available ranges on a group of _fields_ and a target _resource_. To each combination (resource + field) is associated a given type `RangeType (resource :: *) (field :: Symbol) -> *` as described by the type-family in the `HasPagination` type-class. So, let's start with some imports and extensions to get this out of the way: ```hs {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} import Data.Aeson (ToJSON, genericToJSON) import Data.Maybe (fromMaybe) import Data.Proxy (Proxy (..)) import GHC.Generics (Generic) import Servant ((:>), GetPartialContent, Handler, Header, Headers, JSON, Server) import Servant.Pagination (HasPagination (..), PageHeaders, Range (..), Ranges, applyRange) import qualified Data.Aeson as Aeson import qualified Network.Wai.Handler.Warp as Warp import qualified Servant.Pagination as Pagination import qualified Servant ``` #### Declaring the Resource Servant APIs are rather resource-oriented, and so is `servant-pagination`. This guide shows a basic example working with `JSON` (as you could tell from the import list already). To make the world a better colored place, let's create an API to retrieve colors -- with pagination. ```hs data Color = Color { name :: String , rgb :: [Int] , hex :: String } deriving (Eq, Show, Generic) instance ToJSON Color where toJSON = genericToJSON Aeson.defaultOptions colors :: [Color] colors = [ Color "Black" [0, 0, 0] "#000000" , Color "Blue" [0, 0, 255] "#0000ff" , Color "Green" [0, 128, 0] "#008000" , Color "Grey" [128, 128, 128] "#808080" , Color "Purple" [128, 0, 128] "#800080" , Color "Red" [255, 0, 0] "#ff0000" , Color "Yellow" [255, 255, 0] "#ffff00" ] ``` #### Declaring the Ranges Now that we have defined our _resource_ (a.k.a `Color`), we are ready to declare a new `Range` that will operate on a "name" field (genuinely named after the `name` fields from the `Color` record). For that, we need to tell `servant-pagination` two things: - What is the type of the corresponding `Range` values - How do we get one of these values from our resource This is done via defining an instance of `HasPagination` as follows: ```hs instance HasPagination Color "name" where type RangeType Color "name" = String getFieldValue _ = name -- getRangeOptions :: Proxy "name" -> Proxy Color -> RangeOptions -- getDefaultRange :: Proxy Color -> Range "name" String defaultRange :: Range "name" String defaultRange = getDefaultRange (Proxy @Color) ``` Note that `getFieldValue :: Proxy "name" -> Color -> String` is the minimal complete definintion of the class. Yet, you can define `getRangeOptions` to provide different parsing options (see the last section of this guide). In the meantime, we've also defined a `defaultRange` as it will come in handy when defining our handler. #### API Good, we have a resource, we have a `Range` working on that resource, we can now declare our API using other Servant combinators we already know: ```hs type API = "colors" :> Header "Range" (Ranges '["name"] Color) :> GetPartialContent '[JSON] (Headers MyHeaders [Color]) type MyHeaders = Header "Total-Count" Int ': PageHeaders '["name"] Color ``` `PageHeaders` is a type alias provided by the library to declare the necessary response headers we mentionned in introduction. Expanding the alias boils down to the following: ```hs -- type MyHeaders -- = Header "Total-Count" Int -- :> Header "Accept-Ranges" (AcceptRanges '["name"]) -- :> Header "Content-Range" (ContentRange '["name"] Color) -- :> Header "Next-Range" (Ranges '["name"] Color) ``` As a result, we will need to provide all those headers with the response in our handler. Worry not, _servant-pagination_ provides an easy way to lift a collection of resources into such handler. #### Server Time to connect the last bits by defining the server implementation of our colorful API. The `Ranges` type we've defined above (tight to the `Range` HTTP header) indicates the server to parse any `Range` header, looking for the format defined in introduction with fields and target types we have just declared. If no such header is provided, we will end up receiving `Nothing`. Otherwise, it will be possible to _extract_ a `Range` from our `Ranges`. ```hs server :: Server API server = handler where handler :: Maybe (Ranges '["name"] Color) -> Handler (Headers MyHeaders [Color]) handler mrange = do let range = fromMaybe defaultRange (mrange >>= extractRange) addHeader (length colors) <$> returnRange range (applyRange range colors) main :: IO () main = Warp.run 1442 $ Servant.serve (Proxy @API) server ``` Let's try it out using different ranges to observe the server's behavior. As a reminder, here's the format we defined, where `` here can only be `name` and `` must parse to a `String`: - `Range: [][; offset ][; limit ][; order ]` Beside the target field, everything is pretty much optional in the `Range` HTTP header. Missing parts are deducted from the `RangeOptions` that are part of the `HasPagination` instance. Therefore, all following examples are valid requests to send to our server: - 1 - `curl http://localhost:1442/colors -vH 'Range: name'` - 2 - `curl http://localhost:1442/colors -vH 'Range: name; limit 2'` - 3 - `curl http://localhost:1442/colors -vH 'Range: name Green; order asc; offset 1'` Considering the following default options: - `defaultRangeLimit: 100` - `defaultRangeOffset: 0` - `defaultRangeOrder: RangeDesc` The previous ranges reads as follows: - 1 - The first 100 colors, ordered by descending names - 2 - The first 2 colors, ordered by descending names - 3 - The 100 colors after `Green` (not included), ordered by ascending names. > See `examples/Simple.hs` for a running version of this guide. ## Going Forward #### Multiple Ranges Note that in the simple above scenario, there's no ambiguity with `extractRange` and `returnRange` because there's only one possible `Range` defined on our resource. Yet, as you've most probably noticed, the `Ranges` combinator accepts a list of fields, each of which must declare a `HasPagination` instance. Doing so will make the other helper functions more ambiguous and type annotation are highly likely to be needed. ```hs instance HasPagination Color "hex" where type RangeType Color "hex" = String getFieldValue _ = hex -- to then define: Ranges '["name", "hex"] Color ``` > See `examples/Complex.hs` for more complex examples. #### Parsing Options By default, `servant-pagination` provides an implementation of `getRangeOptions` for each `HasPagination` type-class. However, this can be overwritten when defining a instance of that class to provide your own options. This options come into play when a `Range` header is received and isn't fully specified (`limit`, `offset`, `order` are all optional) to provide default fallback values for those. For instance, let's say we wanted to change the default limit to `5` in a new range on `"rgb"`, we could tweak the corresponding `HasPagination` instance as follows: ```hs instance HasPagination Color "rgb" where type RangeType Color "rgb" = String getFieldValue _ = sum . rgb getRangeOptions _ _ = defaultOptions { defaultRangeLimit = 5 } ``` ## Changelog [CHANGELOG.md](CHANGELOG.md) ## License [LGPL-3 © 2018 Chordify](LICENSE)