packages feed

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

{-# LANGUAGE OverloadedStrings #-}

module Servant.Hateoas.ContentType.HAL
(
  -- * Content-Type
  HAL

  -- * Resource-Type
, HALResource(..)
)
where

import Servant.Hateoas.Resource
import Servant.API.ContentTypes
import qualified Network.HTTP.Media as M
import qualified Data.Foldable as Foldable
import Data.Some.Constraint
import Data.Kind
import Data.Aeson
import Data.Aeson.KeyMap (singleton)
import GHC.Exts
import GHC.Generics

-- | Type representing Content-Types of Hypertext Application Language (HAL).
--
--   Type parameter @t@ is the mime type suffix in @application/hal+t@.
data HAL (t :: Type)

type instance MkResource (HAL t) = HALResource

-- | HAL-resource representation.
data HALResource a = HALResource
  { resource :: a                                       -- ^ Wrapped resource
  , rels     :: [(String, ResourceLink)]                -- ^ Pairs @(rel, link)@ for hypermedia relations
  , embedded :: [(String, SomeF HALResource ToJSON)]    -- ^ Pairs @(rel, resource)@ for embedded resources
  } deriving (Generic, Functor)

instance Resource HALResource where
  wrap x = HALResource x [] []
  addRel l (HALResource r ls es) = HALResource r (l:ls) es

instance Accept (HAL JSON) where
  contentType _ = "application" M.// "hal+json"

instance ToJSON (HALResource a) => MimeRender (HAL JSON) (HALResource a) where
  mimeRender _ = encode

instance {-# OVERLAPPABLE #-} ToJSON a => ToJSON (HALResource a) where
  toJSON (HALResource res ls es) = Object $ (singleton "_links" ls') <> (singleton "_embedded" es') <> (case toJSON res of Object kvm -> kvm ; _ -> mempty)
    where
      ls' = object [fromString rel .= object ["href" .= href] | (rel, href) <- ls]
      es' = object [fromString name .= toJSON e | (name, (Some1 e)) <- es]

instance {-# OVERLAPPING #-} ToJSON a => ToJSON (HALResource [a]) where
  toJSON (HALResource xs ls es) = object ["_links" .= ls', "_embedded" .= object (exs <> es')]
    where
      ls' = object [fromString rel .= object ["href" .= href] | (rel, href) <- ls]
      es' = fmap (\(eName, (Some1 e)) -> fromString eName .= toJSON e) es
      exs = [ "items"
              .= (Array $ Foldable.foldl' (\xs' x -> xs' <> pure (toJSON x)) mempty xs)
            ]

instance EmbeddingResource HALResource where
  embed e (HALResource r ls es) = HALResource r ls $ fmap Some1 e : es