module Servant.Pagination
(
Range(..)
, RangeOrder(..)
, AcceptRanges (..)
, ContentRange (..)
, NextRange (..)
, PageHeaders
, TotalCount
, FromRange(..)
, FromHttpApiData(..)
, FromRangeOptions(..)
, defaultOptions
, defaultRange
, HasPagination(..)
, applyRange
, (:|:)(..)
) where
import Data.List (filter, find)
import Data.Maybe (fromMaybe, listToMaybe)
import Data.Proxy (Proxy (..))
import Data.Semigroup ((<>))
import Data.Text (Text)
import GHC.Generics (Generic)
import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)
import Numeric.Natural (Natural)
import Servant
import qualified Data.List as List
import qualified Data.Text as Text
import qualified Safe
import Servant.Pagination.Internal
data Range (field :: Symbol) typ = Range
{ rangeValue :: Maybe typ
, rangeLimit :: Int
, rangeOffset :: Int
, rangeOrder :: RangeOrder
} deriving (Eq, Show, Generic)
instance Functor (Range field) where
fmap f r =
r { rangeValue = f <$> rangeValue r }
instance (ToHttpApiData typ, KnownSymbol field) => ToHttpApiData (Range field typ) where
toUrlPiece Range{..} =
Text.pack (symbolVal (Proxy :: Proxy field))
<> maybe "" (\v -> " " <> toUrlPiece v) rangeValue
<> ";limit " <> toUrlPiece rangeLimit
<> ";offset " <> toUrlPiece rangeOffset
<> ";order " <> toUrlPiece rangeOrder
instance (KnownSymbol field) => ToAcceptRanges (Range field typ) where
toAcceptRanges _ =
Text.pack (symbolVal (Proxy :: Proxy field))
data RangeOrder
= RangeAsc
| RangeDesc
deriving (Eq, Show, Ord, Generic)
instance ToHttpApiData RangeOrder where
toUrlPiece order =
case order of
RangeAsc -> "asc"
RangeDesc -> "desc"
instance FromHttpApiData RangeOrder where
parseUrlPiece txt =
case txt of
"asc" -> pure RangeAsc
"desc" -> pure RangeDesc
_ -> Left "Invalid Range Order"
data AcceptRanges range = AcceptRanges
instance (ToAcceptRanges a) => ToHttpApiData (AcceptRanges a) where
toUrlPiece _ =
toAcceptRanges (Proxy :: Proxy a)
data ContentRange range = ContentRange
{ contentRangeStart :: range
, contentRangeEnd :: range
}
instance (ToHttpApiData typ, KnownSymbol field) => ToHttpApiData (ContentRange (Range field typ)) where
toUrlPiece (ContentRange start end) =
Text.pack (symbolVal (Proxy :: Proxy field))
<> " " <> (fromMaybe "" (toUrlPiece <$> rangeValue start))
<> ".." <> (fromMaybe "" (toUrlPiece <$> rangeValue end))
instance (ToHttpApiData (ContentRange a), ToHttpApiData (ContentRange b)) => ToHttpApiData (ContentRange (a :|: b)) where
toUrlPiece (ContentRange (InL sa) (InL ea)) =
toUrlPiece (ContentRange sa ea)
toUrlPiece (ContentRange (InR sb) (InR eb)) =
toUrlPiece (ContentRange sb eb)
toUrlPiece _ =
error "impossible"
data NextRange range = NextRange range
instance (ToHttpApiData typ, KnownSymbol field) => ToHttpApiData (NextRange (Range field typ)) where
toUrlPiece (NextRange r) =
toUrlPiece r
instance (ToHttpApiData (NextRange a), ToHttpApiData (NextRange b)) => ToHttpApiData (NextRange (a :|: b)) where
toUrlPiece (NextRange (InL a)) =
toUrlPiece (NextRange a)
toUrlPiece (NextRange (InR b)) =
toUrlPiece (NextRange b)
type PageHeaders range =
'[ Header "Accept-Ranges" (AcceptRanges range)
, Header "Content-Range" (ContentRange range)
, Header "Next-Range" (NextRange range)
, Header "Total-Count" Natural
]
data FromRangeOptions = FromRangeOptions
{ defaultRangeLimit :: Int
, defaultRangeOffset :: Int
, defaultRangeOrder :: RangeOrder
} deriving (Eq, Show)
defaultOptions :: FromRangeOptions
defaultOptions =
FromRangeOptions 100 0 RangeDesc
defaultRange :: Maybe a -> FromRangeOptions -> Range field a
defaultRange val opts =
let
(FromRangeOptions lim off ord) =
opts
in
Range val lim off ord
class FromRange a where
parseRange :: FromRangeOptions -> Text -> Either Text a
instance (FromHttpApiData typ, KnownSymbol field) => FromRange (Range field typ) where
parseRange FromRangeOptions{..} txt =
let
toTuples =
filter (/= "") . Text.splitOn (Text.singleton ' ')
args =
map toTuples $ Text.splitOn (Text.singleton ';') txt
field =
Text.pack $ symbolVal (Proxy :: Proxy field)
in
case args of
(field' : value) : rest | field == field' -> do
opts <-
traverse parseOpt rest
Range
<$> sequence (fmap parseQueryParam (listToMaybe value))
<*> ifOpt "limit" defaultRangeLimit opts
<*> ifOpt "offset" defaultRangeOffset opts
<*> ifOpt "order" defaultRangeOrder opts
_ ->
Left "Invalid Range"
where
parseOpt :: [Text] -> Either Text (Text, Text)
parseOpt piece =
case piece of
[opt, arg] ->
pure (opt, arg)
_ ->
Left "Invalid Range Options"
ifOpt :: FromHttpApiData o => Text -> o -> [(Text, Text)] -> Either Text o
ifOpt opt def =
maybe (pure def) (parseQueryParam . snd) . find ((== opt) . fst)
instance (FromHttpApiData typ, KnownSymbol field) => FromHttpApiData (Range field typ) where
parseUrlPiece =
parseRange defaultOptions
type TotalCount =
Maybe Natural
class KnownSymbol field => HasPagination resource field where
type RangeType resource field :: *
getRangeField :: Proxy field -> resource -> RangeType resource field
returnPage_ :: forall m ranges.
( Monad m
, (Range field (RangeType resource field)) :<: ranges
, ToAcceptRanges ranges
, ToHttpApiData (ContentRange ranges)
, ToHttpApiData (NextRange ranges)
, Ord (RangeType resource field)
) => (Range field (RangeType resource field)) -> [resource] -> m (Headers (PageHeaders ranges) [resource])
returnPage_ =
returnPage Nothing
returnPage :: forall m ranges.
( Monad m
, (Range field (RangeType resource field)) :<: ranges
, ToAcceptRanges ranges
, ToHttpApiData (ContentRange ranges)
, ToHttpApiData (NextRange ranges)
, Ord (RangeType resource field)
) => TotalCount -> (Range field (RangeType resource field)) -> [resource] -> m (Headers (PageHeaders ranges) [resource])
returnPage count range rs = do
let field =
Proxy :: Proxy field
let boundaries = (,)
<$> fmap (getRangeField field) (Safe.headMay rs)
<*> fmap (getRangeField field) (Safe.lastMay rs)
let acceptRanges =
addHeader (AcceptRanges :: AcceptRanges ranges)
let totalCount =
maybe noHeader addHeader count
case boundaries of
Nothing ->
return $
acceptRanges $ noHeader $ noHeader $ totalCount rs
Just (start, end) -> do
let rangeStart =
liftRange $ (range { rangeValue = Just start } :: Range field (RangeType resource field))
let rangeEnd =
liftRange $ (range { rangeValue = Just end } :: Range field (RangeType resource field))
let nextOffset | rangeValue range `compare` Just end == EQ = rangeOffset range + length rs
| otherwise = length $ takeWhile (\r -> getRangeField field r `compare` end == EQ) $ reverse rs
let rangeNext =
liftRange $ (range { rangeValue = Just end, rangeOffset = nextOffset } :: Range field (RangeType resource field))
let contentRange =
addHeader $ ContentRange
{ contentRangeStart = rangeStart
, contentRangeEnd = rangeEnd
}
let nextRange =
addHeader $ NextRange $ rangeNext
return
$ acceptRanges $ contentRange $ nextRange $ totalCount rs
applyRange :: forall b field. (HasPagination b field, Ord (RangeType b field)) => Range field (RangeType b field) -> [b] -> [b]
applyRange Range{..} =
let
field =
Proxy :: Proxy (field :: Symbol)
sortRel =
case rangeOrder of
RangeDesc ->
\a b -> compare (getRangeField field b) (getRangeField field a)
RangeAsc ->
\a b -> compare (getRangeField field a) (getRangeField field b)
dropRel =
case (rangeValue, rangeOrder) of
(Nothing, _) ->
const False
(Just a, RangeDesc) ->
(> a) . (getRangeField field)
(Just a, RangeAsc) ->
(< a) . (getRangeField field)
in
List.take rangeLimit
. List.drop rangeOffset
. List.dropWhile dropRel
. List.sortBy sortRel