packages feed

ema-extra-0.10.0.0: src/Ema/Route/Lib/Extra/PaginatedRoute.hs

{-# LANGUAGE DeriveAnyClass #-}

-- | Provides a `Page` type that enables any route to be paginated.
module Ema.Route.Lib.Extra.PaginatedRoute (
  -- * Page
  Page,

  -- * `Page` Functions
  pageNum,
  fromNum,
  pageRange,
  lookupPage,
  lookupPage',

  -- * Utility functions
  paginate,
) where

import Data.Default (Default (..))
import Data.Sequence qualified as Seq
import Data.Text qualified as T
import Ema.Route.Class (IsRoute (..))
import Ema.Route.Prism (toPrism_)
import Generics.SOP qualified as SOP
import Optics.Core (prism')
import Text.Show qualified as Show

{- | Represents a single page in a multi-page view/route.

  Use `pageNum` to retrieve the user-facing page number which are 1-index;
  `fromNum` to convert them back.
-}
newtype Page (t :: Type) = Page {unPage :: Word}
  deriving newtype (Eq, Ord, Num, Enum, Default)
  deriving stock (Generic)
  deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo)

instance Show (Page t) where
  show p = "Page " <> show (pageNum p)

-- | Get the user-facing page number.
pageNum :: forall a. Page a -> Int
pageNum (Page n) =
  fromInteger . toInteger $ n + 1

-- | Convert the user-facing page number.
fromNum :: forall a. Int -> Maybe (Page a)
fromNum n = do
  guard $ n > 0
  pure $ fromInteger . toInteger $ n - 1

-- | Enumerate list of all pages given the total number of pages.
pageRange :: forall a. HasCallStack => Int -> NonEmpty (Page a)
pageRange total =
  fromMaybe (error "pageRange: total must be positive and non-zero") $ do
    end <- fromNum @a total
    nonEmpty [def .. end]

-- | Retrieve the given page from the list.
lookupPage :: HasCallStack => Page a -> NonEmpty [a] -> [a]
lookupPage r xs =
  fromMaybe (error outOfBoundsError) $ lookupPage' r xs
  where
    outOfBoundsError =
      "lookupPage: Page "
        <> show r
        <> " is out of bounds of total available pages: "
        <> show (length xs)

lookupPage' :: Page a -> NonEmpty [a] -> Maybe [a]
lookupPage' p xs =
  toList xs !!? (fromInteger . toInteger $ unPage p)

instance IsRoute (Page a) where
  type RouteModel (Page a) = NonEmpty [a]
  routePrism m =
    -- TODO: Refactor this using lens composition.
    toPrism_ $
      prism'
        ( \page ->
            if page == def
              then "index.html"
              else "page/" <> show (pageNum page) <> ".html"
        )
        ( \fp -> do
            if fp == "index.html"
              then pure def
              else do
                page <- fmap toString $ T.stripSuffix ".html" =<< T.stripPrefix "page/" (toText fp)
                r <- fromNum <=< readMaybe $ page
                void $ lookupPage' r m -- Check if this page exists
                pure r
        )
  routeUniverse =
    toList . pageRange . length

-- | Break a list into pages given the page size.
paginate :: Int -> [a] -> NonEmpty [a]
paginate pageSize =
  fromMaybe (one mempty)
    . nonEmpty
    . fmap toList
    . toList
    . Seq.chunksOf pageSize
    . Seq.fromList