{- | Module representing a JSON-API resource object. Specification: -} module Network.JSONApi.Resource ( Resource (..) , Relationships , ResourcefulEntity (..) , Relationship , indexLinks , mkRelationship , mkRelationships , showLink ) where import Control.Lens.TH import Data.Aeson (ToJSON, FromJSON, (.=), (.:), (.:?)) import qualified Data.Aeson as AE import Data.Aeson.Types (fieldLabelModifier) import Data.Map (Map) import qualified Data.Map as Map import Data.Text (Text, pack) import GHC.Generics hiding (Meta) import Network.JSONApi.Identifier (HasIdentifier (..), Identifier (..)) import Network.JSONApi.Link (Links, mkLinks) import Network.JSONApi.Meta (Meta) import Network.JSONApi.Pagination (Pagination (..), PageSize (..), PageNum (..), ResourceCount (..)) import Network.URI.Encode (encodeText) import Prelude hiding (id) {- | Type representing a JSON-API resource object. A Resource supplies standardized data and metadata about a resource. Specification: -} data Resource a = Resource { getIdentifier :: Identifier , getResource :: a , getLinks :: Maybe Links , getRelationships :: Maybe Relationships } deriving (Show, Eq, Generic) instance (ToJSON a) => ToJSON (Resource a) where toJSON (Resource (Identifier resId resType metaObj) resObj linksObj rels) = AE.object [ "id" .= resId , "type" .= resType , "attributes" .= resObj , "links" .= linksObj , "meta" .= metaObj , "relationships" .= rels ] instance (FromJSON a) => FromJSON (Resource a) where parseJSON = AE.withObject "resourceObject" $ \v -> do id <- v .: "id" typ <- v .: "type" attrs <- v .: "attributes" links <- v .:? "links" meta <- v .:? "meta" rels <- v .:? "relationships" return $ Resource (Identifier id typ meta) attrs links rels instance HasIdentifier (Resource a) where identifier = getIdentifier {- | A typeclass for decorating an entity with JSON API properties -} class (ToJSON a, FromJSON a) => ResourcefulEntity a where resourceIdentifier :: a -> Text resourceType :: a -> Text resourceLinks :: a -> Maybe Links resourceMetaData :: a -> Maybe Meta resourceRelationships :: a -> Maybe Relationships fromResource :: Resource a -> a fromResource = getResource toResource :: a -> Resource a toResource a = Resource (Identifier (resourceIdentifier a) (resourceType a) (resourceMetaData a)) a (resourceLinks a) (resourceRelationships a) {- | A type representing the Relationship between 2 entities A Relationship provides basic information for fetching further information about a related resource. Specification: -} data Relationship = Relationship { _data :: Maybe Identifier , _links :: Maybe Links } deriving (Show, Eq, Generic) instance ToJSON Relationship where toJSON = AE.genericToJSON AE.defaultOptions { fieldLabelModifier = drop 1 } instance FromJSON Relationship where parseJSON = AE.genericParseJSON AE.defaultOptions { fieldLabelModifier = drop 1 } newtype Relationships = Relationships (Map Text Relationship) deriving (Show, Eq, Generic) instance ToJSON Relationships instance FromJSON Relationships mkRelationships :: Relationship -> Relationships mkRelationships rel = Relationships $ Map.singleton (relationshipType rel) rel relationshipType :: Relationship -> Text relationshipType relationship = case _data relationship of Nothing -> "unidentified" (Just (Identifier _ typ _)) -> typ {- | Constructor function for creating a Relationship record A relationship must contain either an Identifier or a Links record -} mkRelationship :: Maybe Identifier -> Maybe Links -> Maybe Relationship mkRelationship Nothing Nothing = Nothing mkRelationship resId links = Just $ Relationship resId links makeLenses ''Resource {- | Helper function to build relative links for a single resource of type ResourceEntity -} showLink :: ResourcefulEntity e => e -> Links showLink resource = mkLinks [ ("self", buildLink) ] where buildLink = "/" <> resourceType resource <> "/" <> resourceIdentifier resource {- | Helper function to beuild relative links for a collection of resources of type ResourceEntity. This helper function assumes that the first page is always page 0. -} indexLinks :: Text -> Pagination -> Links indexLinks baseUrl (Pagination pageSize pageNum resourceCount) = mkLinks [ ("self", genLink pgNum) ,("first", genLink (0 :: Int)) ,("prev", genLink (if pgNum - 1 < 0 then 0 else pgNum - 1)) ,("next", genLink (pgNum + 1)) ,("last", genLink ((resCount `quot` pgSize) - 1))] where pgNum = if getPageNum pageNum < 0 then 0 else getPageNum pageNum pgSize = if getPageSize pageSize <= 0 then 1 else getPageSize pageSize resCount = if getResourceCount resourceCount < 0 then 0 else getResourceCount resourceCount genLink no = baseUrl <> "?" <> encodeText "page[number]" <> "=" <> (pack . show) no <> "&" <> encodeText "page[size]" <> "=" <> (pack . show) pgSize