pagination 0.2.1 → 0.2.2
raw patch · 6 files changed
+239/−211 lines, 6 filesdep −semigroupsdep ~QuickCheckdep ~basedep ~deepseqPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies removed: semigroups
Dependency ranges changed: QuickCheck, base, deepseq, hspec
API changes (from Hackage documentation)
- Data.Pagination: instance GHC.Exception.Exception Data.Pagination.PaginationException
+ Data.Pagination: instance GHC.Exception.Type.Exception Data.Pagination.PaginationException
Files
- CHANGELOG.md +4/−0
- Data/Pagination.hs +97/−89
- LICENSE.md +1/−1
- README.md +9/−3
- pagination.cabal +52/−47
- tests/Main.hs +76/−71
CHANGELOG.md view
@@ -1,3 +1,7 @@+## Pagination 0.2.2++* Works with 9.0.1. Dropped support for GHC 8.6 and older.+ ## Pagination 0.2.1 * Fix test suite failure with `QuickCheck-2.10`.
Data/Pagination.hs view
@@ -1,6 +1,11 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE RecordWildCards #-}+ -- | -- Module : Data.Pagination--- Copyright : © 2016–2017 Mark Karpov+-- Copyright : © 2016–present Mark Karpov -- License : BSD 3 clause -- -- Maintainer : Mark Karpov <markkarpov92@gmail.com>@@ -8,72 +13,69 @@ -- Portability : portable -- -- Framework-agnostic pagination boilerplate.--{-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE DeriveFunctor #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE RecordWildCards #-}- module Data.Pagination ( -- * Pagination settings- Pagination- , mkPagination- , pageSize- , pageIndex+ Pagination,+ mkPagination,+ pageSize,+ pageIndex,+ -- * Paginated data- , Paginated- , paginate- , paginatedItems- , paginatedPagination- , paginatedPagesTotal- , paginatedItemsTotal- , hasOtherPages- , pageRange- , hasPrevPage- , hasNextPage- , backwardEllip- , forwardEllip+ Paginated,+ paginate,+ paginatedItems,+ paginatedPagination,+ paginatedPagesTotal,+ paginatedItemsTotal,+ hasOtherPages,+ pageRange,+ hasPrevPage,+ hasNextPage,+ backwardEllip,+ forwardEllip,+ -- * Exceptions- , PaginationException (..) )+ PaginationException (..),+ ) where import Control.DeepSeq import Control.Monad.Catch import Data.Data (Data) import Data.List.NonEmpty (NonEmpty (..))+import qualified Data.List.NonEmpty as NE import Data.Typeable (Typeable) import GHC.Generics import Numeric.Natural-import qualified Data.List.NonEmpty as NE ---------------------------------------------------------------------------- -- Pagination settings -- | Settings that are required to organize data in paginated form.- data Pagination = Pagination Natural Natural deriving (Eq, Show, Data, Typeable, Generic) instance NFData Pagination -- | Create a 'Pagination' value. May throw 'PaginationException'.--mkPagination :: MonadThrow m- => Natural -- ^ Page size- -> Natural -- ^ Page index- -> m Pagination -- ^ The pagination settings+mkPagination ::+ MonadThrow m =>+ -- | Page size+ Natural ->+ -- | Page index+ Natural ->+ -- | The pagination settings+ m Pagination mkPagination size index- | size == 0 = throwM ZeroPageSize+ | size == 0 = throwM ZeroPageSize | index == 0 = throwM ZeroPageIndex- | otherwise = return (Pagination size index)+ | otherwise = return (Pagination size index) -- | Get page size (maximum number of items on a page) from a 'Pagination'.- pageSize :: Pagination -> Natural pageSize (Pagination size _) = size -- | Get page index from a 'Pagination'.- pageIndex :: Pagination -> Natural pageIndex (Pagination _ index) = index @@ -81,13 +83,13 @@ -- Paginated data -- | Data in the paginated form.- data Paginated a = Paginated- { pgItems :: [a]- , pgPagination :: Pagination- , pgPagesTotal :: Natural- , pgItemsTotal :: Natural- } deriving (Eq, Show, Data, Typeable, Generic, Functor)+ { pgItems :: [a],+ pgPagination :: Pagination,+ pgPagesTotal :: Natural,+ pgItemsTotal :: Natural+ }+ deriving (Eq, Show, Data, Typeable, Generic, Functor) instance NFData a => NFData (Paginated a) @@ -96,111 +98,117 @@ instance Traversable Paginated where traverse f p =- let g p' xs = p' { pgItems = xs }- in g p <$> traverse f (pgItems p)+ let g p' xs = p' {pgItems = xs}+ in g p <$> traverse f (pgItems p) -- | Create paginated data.--paginate :: (Functor m, Integral n)- => Pagination -- ^ Pagination options- -> Natural -- ^ Total number of items- -> (n -> n -> m [a])- -- ^ The element producing callback. The function takes arguments:- -- offset and limit.- -> m (Paginated a) -- ^ The paginated data+paginate ::+ (Functor m, Integral n) =>+ -- | Pagination options+ Pagination ->+ -- | Total number of items+ Natural ->+ -- | The element producing callback. The function takes arguments:+ -- offset and limit.+ (n -> n -> m [a]) ->+ -- | The paginated data+ m (Paginated a) paginate (Pagination size index') totalItems f = r <$> f (fromIntegral offset) (fromIntegral size) where- r xs = Paginated- { pgItems = xs- , pgPagination = Pagination size index- , pgPagesTotal = totalPages- , pgItemsTotal = totalItems }+ r xs =+ Paginated+ { pgItems = xs,+ pgPagination = Pagination size index,+ pgPagesTotal = totalPages,+ pgItemsTotal = totalItems+ } (whole, rems) = totalItems `quotRem` size- totalPages = max 1 (whole + if rems == 0 then 0 else 1)- index = min index' totalPages- offset = (index - 1) * size+ totalPages = max 1 (whole + if rems == 0 then 0 else 1)+ index = min index' totalPages+ offset = (index - 1) * size -- | Get subset of items for current page.- paginatedItems :: Paginated a -> [a] paginatedItems = pgItems -- | Get 'Pagination' parameters that were used to create this paginated -- result.- paginatedPagination :: Paginated a -> Pagination paginatedPagination = pgPagination -- | Get the total number of pages in this collection.- paginatedPagesTotal :: Paginated a -> Natural paginatedPagesTotal = pgPagesTotal -- | Get the total number of items in this collection.- paginatedItemsTotal :: Paginated a -> Natural paginatedItemsTotal = pgItemsTotal -- | Test whether there are other pages.- hasOtherPages :: Paginated a -> Bool hasOtherPages Paginated {..} = pgPagesTotal > 1 -- | Is there previous page?- hasPrevPage :: Paginated a -> Bool hasPrevPage Paginated {..} = pageIndex pgPagination > 1 -- | Is there next page?- hasNextPage :: Paginated a -> Bool hasNextPage Paginated {..} = pageIndex pgPagination < pgPagesTotal -- | Get range of pages to show before and after the current page. This does -- not necessarily include the first and the last pages (they are supposed -- to be shown in all cases). Result of the function is always sorted.--pageRange- :: Paginated a -- ^ Paginated data- -> Natural -- ^ Number of pages to show before and after- -> NonEmpty Natural -- ^ Page range+pageRange ::+ -- | Paginated data+ Paginated a ->+ -- | Number of pages to show before and after+ Natural ->+ -- | Page range+ NonEmpty Natural pageRange Paginated {..} 0 = NE.fromList [pageIndex pgPagination] pageRange Paginated {..} n =- let len = min pgPagesTotal (n * 2 + 1)+ let len = min pgPagesTotal (n * 2 + 1) index = pageIndex pgPagination- shift | index <= n = 0- | index >= pgPagesTotal - n = pgPagesTotal - len- | otherwise = index - n - 1- in (+ shift) <$> NE.fromList [1..len]+ shift+ | index <= n = 0+ | index >= pgPagesTotal - n = pgPagesTotal - len+ | otherwise = index - n - 1+ in (+ shift) <$> NE.fromList [1 .. len] -- | Backward ellipsis appears when page range (pages around current page to -- jump to) has gap between its beginning and the first page.--backwardEllip- :: Paginated a -- ^ Paginated data- -> Natural -- ^ Number of pages to show before and after- -> Bool+backwardEllip ::+ -- | Paginated data+ Paginated a ->+ -- | Number of pages to show before and after+ Natural ->+ Bool backwardEllip p n = NE.head (pageRange p n) > 2 -- | Forward ellipsis appears when page range (pages around current page to -- jump to) has gap between its end and the last page.--forwardEllip- :: Paginated a -- ^ Paginated data- -> Natural -- ^ Number of pages to show before and after- -> Bool -- ^ Do we have forward ellipsis?+forwardEllip ::+ -- | Paginated data+ Paginated a ->+ -- | Number of pages to show before and after+ Natural ->+ -- | Do we have forward ellipsis?+ Bool forwardEllip p@Paginated {..} n = NE.last (pageRange p n) < pred pgPagesTotal ---------------------------------------------------------------------------- -- Exceptions -- | Exception indicating various problems when working with paginated data.- data PaginationException- = ZeroPageSize -- ^ Page size (number of items per page) was zero- | ZeroPageIndex -- ^ Page index was zero (they start from one)+ = -- | Page size (number of items per page) was zero+ ZeroPageSize+ | -- | Page index was zero (they start from one)+ ZeroPageIndex deriving (Eq, Show, Data, Typeable, Generic) instance NFData PaginationException+ instance Exception PaginationException
LICENSE.md view
@@ -1,4 +1,4 @@-Copyright © 2016–2017 Mark Karpov+Copyright © 2016–present Mark Karpov All rights reserved.
README.md view
@@ -4,13 +4,19 @@ [](https://hackage.haskell.org/package/pagination) [](http://stackage.org/nightly/package/pagination) [](http://stackage.org/lts/package/pagination)-[](https://travis-ci.org/mrkkrp/pagination)-[](https://coveralls.io/github/mrkkrp/pagination?branch=master)+ The package implements pagination boilerplate in a framework-agnostic way. +## Contribution++Issues, bugs, and questions may be reported in [the GitHub issue tracker for+this project](https://github.com/mrkkrp/pagination/issues).++Pull requests are also welcome.+ ## License -Copyright © 2016–2017 Mark Karpov+Copyright © 2016–present Mark Karpov Distributed under BSD 3 clause license.
pagination.cabal view
@@ -1,58 +1,63 @@-name: pagination-version: 0.2.1-cabal-version: >= 1.18-tested-with: GHC==7.10.3, GHC==8.0.2, GHC==8.2.1-license: BSD3-license-file: LICENSE.md-author: Mark Karpov <markkarpov92@gmail.com>-maintainer: Mark Karpov <markkarpov92@gmail.com>-homepage: https://github.com/mrkkrp/pagination-bug-reports: https://github.com/mrkkrp/pagination/issues-category: Data-synopsis: Framework-agnostic pagination boilerplate-build-type: Simple-description: Framework-agnostic pagination boilerplate.-extra-doc-files: CHANGELOG.md- , README.md+cabal-version: 1.18+name: pagination+version: 0.2.2+license: BSD3+license-file: LICENSE.md+maintainer: Mark Karpov <markkarpov92@gmail.com>+author: Mark Karpov <markkarpov92@gmail.com>+tested-with: ghc ==8.8.4 ghc ==8.10.4 ghc ==9.0.1+homepage: https://github.com/mrkkrp/pagination+bug-reports: https://github.com/mrkkrp/pagination/issues+synopsis: Framework-agnostic pagination boilerplate+description: Framework-agnostic pagination boilerplate.+category: Data+build-type: Simple+extra-doc-files:+ CHANGELOG.md+ README.md source-repository head- type: git- location: https://github.com/mrkkrp/pagination.git+ type: git+ location: https://github.com/mrkkrp/pagination.git flag dev- description: Turn on development settings.- manual: True- default: False+ description: Turn on development settings.+ default: False+ manual: True library- build-depends: base >= 4.8 && < 5.0- , deepseq >= 1.3 && < 1.5- , exceptions >= 0.6 && < 0.9+ exposed-modules: Data.Pagination+ default-language: Haskell2010+ build-depends:+ base >=4.13 && <5.0,+ deepseq >=1.3 && <1.5,+ exceptions >=0.6 && <0.11 - if !impl(ghc >= 8.0)- build-depends: semigroups == 0.18.*+ if flag(dev)+ ghc-options: -Wall -Werror - exposed-modules: Data.Pagination- if flag(dev)- ghc-options: -Wall -Werror- else- ghc-options: -O2 -Wall- default-language: Haskell2010+ else+ ghc-options: -O2 -Wall + if flag(dev)+ ghc-options:+ -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns+ -Wnoncanonical-monad-instances+ test-suite tests- main-is: Main.hs- hs-source-dirs: tests- type: exitcode-stdio-1.0- build-depends: base >= 4.8 && < 5.0- , QuickCheck >= 2.4 && < 3.0- , exceptions >= 0.6 && < 0.9- , hspec >= 2.0 && < 3.0- , pagination+ type: exitcode-stdio-1.0+ main-is: Main.hs+ hs-source-dirs: tests+ default-language: Haskell2010+ build-depends:+ base >=4.13 && <5.0,+ QuickCheck >=2.10 && <3.0,+ exceptions >=0.6 && <0.11,+ hspec >=2.0 && <3.0,+ pagination - if !impl(ghc >= 8.0)- build-depends: semigroups == 0.18.*- if flag(dev)- ghc-options: -Wall -Werror- else- ghc-options: -O2 -Wall- default-language: Haskell2010+ if flag(dev)+ ghc-options: -Wall -Werror++ else+ ghc-options: -O2 -Wall
tests/Main.hs view
@@ -1,18 +1,17 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE RankNTypes #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Main (main) where import Control.Monad-import Control.Monad.Catch (MonadThrow (..), fromException)+import Control.Monad.Catch (SomeException, fromException) import Data.List.NonEmpty (NonEmpty (..))+import qualified Data.List.NonEmpty as NE import Data.Maybe (fromJust) import Data.Pagination import Numeric.Natural import Test.Hspec import Test.QuickCheck hiding (total)-import qualified Data.List.NonEmpty as NE main :: IO () main = hspec spec@@ -33,20 +32,20 @@ property $ \size index -> (size > 0 && index > 0) ==> do p <- mkPagination size index- pageSize p `shouldBe` size+ pageSize p `shouldBe` size pageIndex p `shouldBe` index describe "Functor instance of Paginated" $ it "works" $ property $ \r -> let f :: Int -> Int f = (+ 1)- in paginatedItems (f <$> r) === (f <$> paginatedItems r)+ in paginatedItems (f <$> r) === (f <$> paginatedItems r) describe "Foldable instance of Paginated" $ it "foldr works like with lists" $ property $ \p n -> let f :: Foldable f => f Int -> Int f = foldr (+) n- in f p === f (paginatedItems p)+ in f p === f (paginatedItems p) describe "Traversable instance of Paginated" $ it "traverse works like with lists" $ property $ \p ->@@ -57,43 +56,44 @@ it "produces an empty pagination" $ property $ \p n -> do r <- paginate p 0 $ \offset limit -> do- offset `shouldBe` 0- limit `shouldBe` pageSize p- return []- paginatedItems r `shouldBe` ([] :: [Int])+ offset `shouldBe` 0+ limit `shouldBe` pageSize p+ return []+ paginatedItems r `shouldBe` ([] :: [Int]) (pageSize . paginatedPagination) r `shouldBe` pageSize p (pageIndex . paginatedPagination) r `shouldBe` 1 paginatedPagesTotal r `shouldBe` 1 paginatedItemsTotal r `shouldBe` 0- pageRange r n `shouldBe` 1 :| []- hasOtherPages r `shouldBe` False- hasPrevPage r `shouldBe` False- hasNextPage r `shouldBe` False- backwardEllip r n `shouldBe` False- forwardEllip r n `shouldBe` False+ pageRange r n `shouldBe` 1 :| []+ hasOtherPages r `shouldBe` False+ hasPrevPage r `shouldBe` False+ hasNextPage r `shouldBe` False+ backwardEllip r n `shouldBe` False+ forwardEllip r n `shouldBe` False describe "paginatedItems" $ it "number of actual items is less than or equal to page size" $ property $ \r -> let size = pageSize (paginatedPagination (r :: Paginated Int))- in (fromIntegral . length . paginatedItems) r `shouldSatisfy` (<= size)+ in (fromIntegral . length . paginatedItems) r `shouldSatisfy` (<= size) describe "paginatedPagination" $ it "returns original pagination correcting index if necessary" $ property $ \p n -> do r <- paginate p n $ \offset limit -> do- let totalPages = ptotal n (pageSize p)- offset `shouldBe`- min ((pageIndex p - 1) * pageSize p)- ((totalPages - 1) * pageSize p)- limit `shouldBe` pageSize p- return (replicate (plen n offset limit) (0 :: Int))- pageSize (paginatedPagination r) `shouldBe` pageSize p+ let totalPages = ptotal n (pageSize p)+ offset+ `shouldBe` min+ ((pageIndex p - 1) * pageSize p)+ ((totalPages - 1) * pageSize p)+ limit `shouldBe` pageSize p+ return (replicate (plen n offset limit) (0 :: Int))+ pageSize (paginatedPagination r) `shouldBe` pageSize p pageIndex (paginatedPagination r) `shouldSatisfy` (<= pageIndex p) describe "paginatedPagesTotal" $ it "returns correct number of total pages" $ property $ \r -> let itemsTotal = paginatedItemsTotal (r :: Paginated Int)- psize = pageSize (paginatedPagination r)- in paginatedPagesTotal r `shouldBe` ptotal itemsTotal psize+ psize = pageSize (paginatedPagination r)+ in paginatedPagesTotal r `shouldBe` ptotal itemsTotal psize describe "paginatedItemsTotal" $ it "returns the same number of items as it was specified for paginate" $ property $ \p n -> do@@ -106,60 +106,60 @@ describe "hasPrevPage" $ it "correctly detect whether paginated data has previous page" $ property $ \r ->- hasPrevPage (r :: Paginated Int) ===- (pageIndex (paginatedPagination r) /= 1)+ hasPrevPage (r :: Paginated Int)+ === (pageIndex (paginatedPagination r) /= 1) describe "hasNextPage" $ it "correctly detect whether paginated data has next page" $ property $ \r ->- hasNextPage (r :: Paginated Int) ===- (pageIndex (paginatedPagination r) /= paginatedPagesTotal r)+ hasNextPage (r :: Paginated Int)+ === (pageIndex (paginatedPagination r) /= paginatedPagesTotal r) describe "pageRange" $ it "correctly performs generation of page ranges" $- forM_ [1..10] $ \n -> do- p <- mkPagination 10 n- r <- paginate p 95 (\_ limit -> return [1..limit])- let x = NE.toList (pageRange (r :: Paginated Int) 2)- x `shouldBe` case n of- 1 -> [1..5]- 2 -> [1..5]- 3 -> [1..5]- 4 -> [2..6]- 5 -> [3..7]- 6 -> [4..8]- 7 -> [5..9]- 8 -> [6..10]- 9 -> [6..10]- _ -> [6..10]+ forM_ [1 .. 10] $+ \n -> do+ p <- mkPagination 10 n+ r <- paginate p 95 (\_ limit -> return [1 .. limit])+ let x = NE.toList (pageRange (r :: Paginated Int) 2)+ x `shouldBe` case n of+ 1 -> [1 .. 5]+ 2 -> [1 .. 5]+ 3 -> [1 .. 5]+ 4 -> [2 .. 6]+ 5 -> [3 .. 7]+ 6 -> [4 .. 8]+ 7 -> [5 .. 9]+ 8 -> [6 .. 10]+ 9 -> [6 .. 10]+ _ -> [6 .. 10] describe "backwardEllip" $ it "correctly detects when there is a backward ellipsis" $ property $ \r n ->- backwardEllip (r :: Paginated Int) n ===- (NE.head (pageRange r n) > 2)+ backwardEllip (r :: Paginated Int) n+ === (NE.head (pageRange r n) > 2) describe "forwardEllip" $ it "correctly detects when there is a forward ellipsis" $ property $ \r n ->- forwardEllip (r :: Paginated Int) n ===- (NE.last (pageRange r n) < paginatedPagesTotal r - 1)+ forwardEllip (r :: Paginated Int) n+ === (NE.last (pageRange r n) < paginatedPagesTotal r - 1) ---------------------------------------------------------------------------- -- Arbitrary instances -#if MIN_VERSION_QuickCheck(2,10,0) instance Arbitrary Natural where arbitrary = fromInteger . getNonNegative <$> arbitrary-#endif instance Arbitrary Pagination where arbitrary = do- size <- p+ size <- p index <- p (return . fromJust) (mkPagination size index)- where p = arbitrary `suchThat` (> 0)+ where+ p = arbitrary `suchThat` (> 0) instance Arbitrary a => Arbitrary (Paginated a) where arbitrary = do pagination <- arbitrary- total <- arbitrary+ total <- arbitrary let f offset limit = vector (plen total offset limit) paginate pagination total f @@ -167,27 +167,32 @@ -- Helpers -- | Run computation inside 'MonadThrow' and return result as an 'Either'.--asEither :: (forall m. MonadThrow m => m a) -> Either PaginationException a+asEither :: Either SomeException a -> Either PaginationException a asEither = either (Left . fromJust . fromException) Right -- | Calculate number of items in paginated selection given total number of -- items, offset, and limit.--plen :: Integral n- => Natural -- ^ Total items- -> Natural -- ^ Offset- -> Natural -- ^ Limit- -> n+plen ::+ Integral n =>+ -- | Total items+ Natural ->+ -- | Offset+ Natural ->+ -- | Limit+ Natural ->+ n plen total offset limit = fromIntegral (min (total - offset) limit) -- | Calculate total number of pages given total number of items, and page -- size.--ptotal :: Integral n- => Natural -- ^ Total items- -> Natural -- ^ Page size- -> n-ptotal total size = fromIntegral $- let (whole, rems) = total `quotRem` size- in max 1 (whole + if rems == 0 then 0 else 1)+ptotal ::+ Integral n =>+ -- | Total items+ Natural ->+ -- | Page size+ Natural ->+ n+ptotal total size =+ fromIntegral $+ let (whole, rems) = total `quotRem` size+ in max 1 (whole + if rems == 0 then 0 else 1)