packages feed

servant-hateoas-0.3.0: src/Servant/Hateoas/ContentType/Collection.hs

{-# LANGUAGE OverloadedStrings #-}

module Servant.Hateoas.ContentType.Collection
( Collection
, CollectionResource(..)
, CollectionItem(..)
)
where

import Servant.Hateoas.Resource
import Servant.API.ContentTypes
import qualified Network.HTTP.Media as M
import Servant.Links
import qualified Data.Foldable as Foldable
import Data.Kind
import Data.Aeson
import GHC.Exts
import GHC.Generics

-- | Data-Kind representing Content-Types of HATEOAS collections.
--
-- Type parameter @t@ is the Mime-Type suffix in @application/vnd.collection+t@.
data Collection (t :: Type)

type instance MkResource (Collection t) = CollectionResource

-- | Resource wrapper for 'Collection'.
data CollectionResource a = CollectionResource
  { href  :: Maybe URI                   -- ^ Link to the collection
  , items :: [CollectionItem a]          -- ^ All items in the collection
  , rels  :: [(String, ResourceLink)]    -- ^ Pairs @(rel, link)@ for relations
  } deriving (Show, Generic, Functor)

instance Semigroup (CollectionResource a) where
  (CollectionResource _ is ls) <> (CollectionResource _ is' ls') = CollectionResource Nothing (is <> is') (ls <> ls')

instance Monoid (CollectionResource a) where
  mempty = CollectionResource Nothing [] []

-- | A single item inside a 'CollectionResource'.
data CollectionItem a = CollectionItem
  { item :: a                             -- ^ Wrapped item
  , itemLinks :: [(String, ResourceLink)]         -- ^ Links for the wrapped item
  } deriving (Show, Generic, Functor)

instance Resource CollectionResource where
  wrap x = CollectionResource Nothing [wrap x] []
  addRel l (CollectionResource h r ls) = CollectionResource h r (l:ls)

instance Resource CollectionItem where
  wrap x = CollectionItem x []
  addRel l (CollectionItem i ls) = CollectionItem i (l:ls)

instance Accept (Collection JSON) where
  contentType _ = "application" M.// "vnd.collection+json"

instance ToJSON (CollectionResource a) => MimeRender (Collection JSON) (CollectionResource a) where
  mimeRender _ = encode

collectionLinks :: [(String, ResourceLink)] -> Value
collectionLinks = Array . Foldable.foldl' (\xs (rel, l) -> pure (object ["name" .= rel, "value" .= l]) <> xs) mempty

-- TODO: I dont like this at all
-- CollectionResource a represents [a]
-- So CollectionResource [a] represents [[a]]
-- This is bad, when rewriting [Foo] in for the ResourceServer we get CollectionResource [Foo] but we want CollectionResource Foo
-- Best would be to handle this when rewriting but does this result in bloat?
-- It actually may not because we can match specialized cases in type families Resourcify and ResourcifyServer
-- How does this affect other parts of the procedure?
-- Ideally we do not have to adjust any HasResourceServer/BuildLayerLink instances at all

instance ToJSON a => ToJSON (CollectionItem a) where
  toJSON (CollectionItem x ls) = object ["data" .= itemData, "links" .= collectionLinks ls]
    where
      itemData = Array
        $ Foldable.foldl' (\xs (k, v) -> pure (object ["name" .= k, "value" .= v]) <> xs) mempty
        $ case toJSON x of Object o -> toList o ; _ -> mempty

instance {-# OVERLAPPABLE #-} ToJSON a => ToJSON (CollectionResource a) where
  toJSON (CollectionResource mHref is ls) = object ["collection" .= collection]
    where
      collection = object $ ["version" .= ("1.0" :: String), "links" .= collectionLinks ls, "items" .= is'] <> maybe [] (pure . ("href" .=)) mHref
      is' = Array $ Foldable.foldl' (\xs i -> pure (toJSON i) <> xs) mempty is

instance CollectingResource CollectionResource where
  collect i (CollectionResource mHref is ls) = CollectionResource mHref (CollectionItem i mempty : is) ls