packages feed

json-api 0.1.1.2 → 0.1.2.0

raw patch · 7 files changed

+311/−14 lines, 7 files

Files

README.md view
@@ -10,10 +10,10 @@  > If you’ve ever argued with your team about the way your JSON responses should > be formatted, JSON API can be your anti-bikeshedding tool.-> +> > By following shared conventions, you can increase productivity, take advantage > of generalized tooling, and focus on what matters: your application.-> +> > Clients built around JSON API are able to take advantage of its features around > efficiently caching responses, sometimes eliminating network requests entirely. @@ -111,13 +111,86 @@ for more robust representations of resourceful payloads. Here, you'll start to see the more comprehensive benefits of a discoverable API. +##### Pagination Example +Let's use the same example User record: -#### Example Project+```Haskell+data User = User+  { userId        :: Int+  , userFirstName :: String+  , userLastName  :: String+  } deriving (Eq, Show) -There is an [example project](https://github.com/toddmohney/json-api/tree/master/example) illustrating how the library can be used in the context of a web server.+$(deriveJSON defaultOptions ''User)+``` +Suppose we now have a list of 2 users; +```Haskell+let usrs =+  [ User 1 "Isaac" "Newton"+  , User 2 "Albert" "Einstein"+  ]+```++From this, we can use the `json-api` package to produce a payload for a collection with pagination links conformant+to the [JSON-API pagination specification](https://jsonapi.org/format/#fetching-pagination) like so:++```Haskell+let paginate = Pagination (PageIndex 1) (PageSize 1) (ResourceCount $ toEnum (length usrs))+let resourceLink = (fromJust . importURL) "/users"+let paginationLinks = mkPaginationLinks PageStrategy resourceLink paginate+let doc = mkDocument [head usrs] (Just paginationLinks) Nothing+```++When delivered as a response from a web server, for example, we get a payload+that looks like this:++```JSON+{+  "data": [+    {+      "attributes": {+        "userFirstName": "Isaac",+        "userLastName": "Newton",+        "userId": 1+      },+      "relationships": null,+      "id": "1",+      "meta": null,+      "type": "users",+      "links": null+    }+  ],+  "meta": null,+  "included": [+  ],+  "links": {+    "next": "/users?page%5bsize%5d=1&page%5bnumber%5d=2",+    "first": "/users?page%5bsize%5d=1&page%5bnumber%5d=1",+    "last": "/users?page%5bsize%5d=1&page%5bnumber%5d=2"+  }+}+```++The key function in the code example is `mkPaginationLinks` which has the following signature;++```Haskell+mkPaginationLinks :: Strategy -> URL -> Pagination -> Links+```++`Strategy` is a sum type that represents the different paging strategies as laid out in the [JSON-API pagination specification](https://jsonapi.org/format/#fetching-pagination). At the time of writing this README, the library only supports 2 paging strategies Offset and Page. Offset is a 0 index based approach unlike Page, i.e. `page[offset]` 0 is the same as `page[number]` 1.++The `URL` type is used to build the links that appear in the JSON payload. The `Pagination` type contains the requisite information for the `mkPaginationLinks` function to generate the paging links.++So let's break this example down. To get started we need to create a `Pagination` record. The first attribute of the record is `PageIndex`. This attribute informs the caller that the page we are looking at is the first in the entire collection (`PageIndex` is either a 0 based index or 1 based index depending on the `Strategy`). So in our example as we are using `PageStrategy`, `PageIndex 1` implies we are after the first page. The second attribute of the record is `PageSize`. This atrribute tells the caller how many items can appear in the list at most. So in our example seeing there are only 2 users, a `PageSize` of 1 would mean that in total we have 2 pages. The third attribute is `ResourceCount`. This attribute is required by the function `mkPaginationLinks` to figure out which links to generate.++The links object in the JSON payload can have 4 attributes `next`, `prev`, `first` and `last`. This library only generates valid links. For example if the request is for the first page of a list, then the `prev` link is not present.++#### Example Project++There is an [example project](https://github.com/toddmohney/json-api/tree/master/example) illustrating how the library can be used in the context of a web server.  #### Hackage 
json-api.cabal view
@@ -1,16 +1,15 @@ name:                json-api-version:             0.1.1.2+version:             0.1.2.0 homepage:            https://github.com/toddmohney/json-api bug-reports:         https://github.com/toddmohney/json-api/issues license:             MIT license-file:        LICENSE author:              Todd Mohney-maintainer:          toddmohney@gmail.com+maintainer:          Todd Mohney <toddmohney@gmail.com> copyright:           2016 Todd Mohney category:            Network build-type:          Simple cabal-version:       >=1.10-maintainer:          Todd Mohney <toddmohney@gmail.com> stability:           experimental tested-with:         GHC == 7.10.3 synopsis:            Utilities for generating JSON-API payloads@@ -61,6 +60,7 @@     Network.JSONApi.Meta     Network.JSONApi.Link     Network.JSONApi.Resource+    Network.JSONApi.Pagination    other-modules: @@ -109,6 +109,7 @@     Network.JSONApi.DocumentSpec     Network.JSONApi.IdentifierSpec     Network.JSONApi.MetaSpec+    Network.JSONApi.PaginationSpec     Network.JSONApi.ResourceSpec     TestHelpers 
src/Network/JSONApi.hs view
@@ -13,10 +13,16 @@ , R.ResourcefulEntity (..) , I.HasIdentifier (..) , I.Identifier (..)-, L.Links+, L.Links (..) , M.Meta , M.MetaObject (..) , L.mkLinks+, P.Pagination (..)+, P.PageIndex (..)+, P.PageSize (..)+, P.ResourceCount (..)+, P.Strategy (..)+, P.mkPaginationLinks , R.mkRelationship , R.mkRelationships , D.mkDocument@@ -34,5 +40,6 @@ import qualified Network.JSONApi.Identifier as I import qualified Network.JSONApi.Link as L import qualified Network.JSONApi.Meta as M+import qualified Network.JSONApi.Pagination as P import qualified Network.JSONApi.Resource as R 
src/Network/JSONApi/Link.hs view
@@ -4,7 +4,7 @@ Specification: <http://jsonapi.org/format/#document-links> -} module Network.JSONApi.Link-( Links+( Links (..) , Rel , Href , mkLinks
+ src/Network/JSONApi/Pagination.hs view
@@ -0,0 +1,123 @@+{-# LANGUAGE OverloadedStrings #-}++module Network.JSONApi.Pagination (+    Pagination (..)+  , PageIndex (..)+  , PageSize (..)+  , ResourceCount (..)+  , Strategy (..)+  , mkPaginationLinks+) where++import Network.JSONApi.Link (Links, Rel, mkLinks)+import Network.URL (URL, add_param)++{- |+Wrapper type for the various components of pagination being page size, page index+and the number of resources in total.+-}+data Pagination = Pagination {+                      getPaginationPageIndex :: PageIndex+                    , getPaginationPageSize :: PageSize+                    , getPaginationResourceCount :: ResourceCount+                  }++{- |+We can specify limits on the number of rows we would like back from the database+-}+newtype PageSize = PageSize {+  getPageSize :: Word+} deriving Show++newtype PageIndex = PageIndex {+  getPageIndex :: Word+} deriving Show++newtype ResourceCount = ResourceCount {+  getResourceCount :: Word+} deriving Show++{- |+Pagination strategies are commonly implemented by the server of which Page and Offset+are commonly used.+-}+data Strategy = PageStrategy | OffsetStrategy++{- |+Helper function to build relative links for a collection of resources of type ResourceEntity.++This helper function assumes that the first page is always page 0.+-}+mkPaginationLinks :: Strategy -> URL -> Pagination -> Links+mkPaginationLinks strategy baseUrl page =+  mkLinks (baseLinks ++ nextLinks ++ prevLinks)+    where+      pgIndex    = getPageIndex $ getPaginationPageIndex page+      pgSize     = getPageSize $ getPaginationPageSize page+      baseLinks  = [mkPaginationLink strategy "first" baseUrl (firstPageIndex strategy) pgSize+                  , mkPaginationLink strategy "last" baseUrl (lastPageIndex strategy page) pgSize]+      nextLinks  = [mkPaginationLink strategy "next" baseUrl (pgIndex + 1) pgSize | shouldGenNextLink strategy page]+      prevLinks  = [mkPaginationLink strategy "prev" baseUrl (pgIndex - 1) pgSize | shouldGenPrevLink strategy page]++{- |+If we are at the last page then we do not generate a next link. This function tells us whether to+generate a next link based on the page strategy.+-}+shouldGenNextLink :: Strategy -> Pagination -> Bool+shouldGenNextLink PageStrategy pagination =+  (getPageIndex . getPaginationPageIndex) pagination < numberOfPagesInPageList pagination+shouldGenNextLink OffsetStrategy pagination =+  (getPageIndex . getPaginationPageIndex) pagination < numberOfPagesInPageList pagination - 1++{- |+If we on the first page then we do not generate a prev link. This function tells us whether we can generate+a prev link.+-}+shouldGenPrevLink :: Strategy -> Pagination -> Bool+shouldGenPrevLink strategy pagination =+  (getPageIndex . getPaginationPageIndex) pagination > firstPageIndex strategy++{- |+This function calculates the number of pages in the list.+-}+numberOfPagesInPageList :: Pagination -> Word+numberOfPagesInPageList (Pagination _ pageSize resourceCount) =+  if resCount `mod` pgSize == 0+  then resCount `quot` pgSize+  else (resCount `quot` pgSize) + 1+    where+      pgSize     = getPageSize pageSize+      resCount   = getResourceCount resourceCount++{- |+Helper function used to generate a single pagination link.+-}+mkPaginationLink :: Strategy -> Rel -> URL -> Word -> Word -> (Rel, URL)+mkPaginationLink strategy key baseUrl pageNo pageSize =+  (key, link)+    where+      pageNoUrl = add_param baseUrl (strategyToQueryStringNumberKey strategy, show pageNo)+      link      = add_param pageNoUrl (strategyToQueryStringSizeKey strategy, show pageSize)++{- |+In the page strategy page numbering starts at 1, where as in the case of offset the numbering+starts at 0.+-}+firstPageIndex :: Strategy -> Word+firstPageIndex PageStrategy = 1+firstPageIndex OffsetStrategy = 0++lastPageIndex :: Strategy -> Pagination -> Word+lastPageIndex PageStrategy page = numberOfPagesInPageList page+lastPageIndex OffsetStrategy page = numberOfPagesInPageList page - 1++{- |+Simple pattern matcher than translates a Strategy to a query string element name.+-}+strategyToQueryStringNumberKey :: Strategy -> String+strategyToQueryStringNumberKey PageStrategy = "page[number]"+strategyToQueryStringNumberKey OffsetStrategy = "page[offset]"++strategyToQueryStringSizeKey :: Strategy -> String+strategyToQueryStringSizeKey PageStrategy = "page[size]"+strategyToQueryStringSizeKey OffsetStrategy = "page[limit]"
+ test/Network/JSONApi/PaginationSpec.hs view
@@ -0,0 +1,93 @@+{-# LANGUAGE OverloadedStrings #-}++module Network.JSONApi.PaginationSpec where++import Data.Map (toList)+import Data.Maybe (fromJust)+import Network.JSONApi+import Network.URL (importURL)+import Test.Hspec++main :: IO ()+main = hspec spec++spec :: Spec+spec =+  describe "Pagination" $ do+    it "should return mandatory keys" $ do+      let p = Pagination (PageIndex 2) (PageSize 10) (ResourceCount 30)+      let results = mkPaginationLinks PageStrategy (fromJust $ importURL "/users") p+      case results of+        Links lm -> do+          let links = toList lm+          map fst links `shouldBe` ["first", "last", "next", "prev"]++    it "should return proper hrefs for paging strategy" $ do+      let p = Pagination (PageIndex 2) (PageSize 10) (ResourceCount 30)+      let results = mkPaginationLinks PageStrategy (fromJust $ importURL "/users") p+      case results of+        Links lm -> do+          let links = toList lm+          links `shouldBe` [("first", "/users?page%5bsize%5d=10&page%5bnumber%5d=1"),+                            ("last", "/users?page%5bsize%5d=10&page%5bnumber%5d=3"),+                            ("next", "/users?page%5bsize%5d=10&page%5bnumber%5d=3"),+                            ("prev", "/users?page%5bsize%5d=10&page%5bnumber%5d=1")]++    it "should return proper hrefs for offset strategy" $ do+      let p = Pagination (PageIndex 1) (PageSize 10) (ResourceCount 30)+      let results = mkPaginationLinks OffsetStrategy (fromJust $ importURL "/users") p+      case results of+        Links lm -> do+          let links = toList lm+          links `shouldBe` [("first", "/users?page%5blimit%5d=10&page%5boffset%5d=0"),+                            ("last", "/users?page%5blimit%5d=10&page%5boffset%5d=2"),+                            ("next", "/users?page%5blimit%5d=10&page%5boffset%5d=2"),+                            ("prev", "/users?page%5blimit%5d=10&page%5boffset%5d=0")]++    it "should support the page strategy" $ do+      let p = Pagination (PageIndex 0) (PageSize 10) (ResourceCount 20)+      let results = mkPaginationLinks PageStrategy (fromJust $ importURL "/users") p+      case results of+        Links lm -> do+          let links = toList lm+          (snd . head) links `shouldBe` "/users?page%5bsize%5d=10&page%5bnumber%5d=1"++    it "should support the offset strategy" $ do+      let p = Pagination (PageIndex 0) (PageSize 10) (ResourceCount 20)+      let results = mkPaginationLinks OffsetStrategy (fromJust $ importURL "/users") p+      case results of+        Links lm -> do+          let links = toList lm+          (snd . head) links `shouldBe` "/users?page%5blimit%5d=10&page%5boffset%5d=0"++    it "should omit prev when we are on the first page of a PageStrategy" $ do+      let p = Pagination (PageIndex 1) (PageSize 10) (ResourceCount 20)+      let results = mkPaginationLinks PageStrategy (fromJust $ importURL "/users") p+      case results of+        Links lm -> do+          let links = toList lm+          map fst links `shouldBe` ["first", "last", "next"]++    it "should omit next when we are on the last page of a PageStrategy" $ do+      let p = Pagination (PageIndex 2) (PageSize 10) (ResourceCount 20)+      let results = mkPaginationLinks PageStrategy (fromJust $ importURL "/users") p+      case results of+        Links lm -> do+          let links = toList lm+          map fst links `shouldBe` ["first", "last", "prev"]++    it "should omit prev when we are on the first page of a OffsetStrategy" $ do+      let p = Pagination (PageIndex 0) (PageSize 10) (ResourceCount 20)+      let results = mkPaginationLinks OffsetStrategy (fromJust $ importURL "/users") p+      case results of+        Links lm -> do+          let links = toList lm+          map fst links `shouldBe` ["first", "last", "next"]++    it "should omit next when we are on the last page of a OffsetStrategy" $ do+      let p = Pagination (PageIndex 1) (PageSize 10) (ResourceCount 20)+      let results = mkPaginationLinks OffsetStrategy (fromJust $ importURL "/users") p+      case results of+        Links lm -> do+          let links = toList lm+          map fst links `shouldBe` ["first", "last", "prev"]
test/Network/JSONApi/ResourceSpec.hs view
@@ -41,14 +41,14 @@   resourceMetaData _ = Just myResourceMetaData   resourceRelationships _ = Just myRelationshipss -data Pagination = Pagination+data PaginationMetaObject = PaginationMetaObject   { currentPage :: Int   , totalPages :: Int   } deriving (Show, Generic) -instance AE.ToJSON Pagination-instance AE.FromJSON Pagination-instance MetaObject Pagination where+instance AE.ToJSON PaginationMetaObject+instance AE.FromJSON PaginationMetaObject+instance MetaObject PaginationMetaObject where   typeName _ = "pagination"  myRelationshipss :: Relationships@@ -74,7 +74,7 @@           ]  myResourceMetaData :: Meta-myResourceMetaData = mkMeta (Pagination 1 14)+myResourceMetaData = mkMeta (PaginationMetaObject 1 14)  toURL :: String -> URL toURL = fromJust . importURL