packages feed

servant-hateoas-0.3.4: src/Servant/Hateoas/RelationLink.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE ViewPatterns #-}

module Servant.Hateoas.RelationLink
(
  -- * RelationLink
  -- ** Type
  RelationLink(..),
  RelationParam(..),

  -- *** Creation
  fromLink,
  fromURI,

  -- *** Operations
  (<<<),
  getHref,
  getPath,
  getParams,
  prependSeg,
  prependSegs,
  addParam,
  addParams,
  mkPlaceHolder,

  -- ** Class
  HasTemplatedLink(..),
  HasRelationLink(..),
  RightLink,

  -- * Utility
  -- ** ReflectStdMethod
  reflectStdMethod,
)
where

import Prelude hiding (drop, dropWhile, break)
import Servant
import Servant.API.ContentTypes (AllMime(..))
import Servant.API.Modifiers (FoldRequired)
import Servant.Hateoas.Internal.Polyvariadic
import Network.URI (unEscapeString, pathSegments)
import Network.HTTP.Media (MediaType)
import Network.HTTP.Types (parseMethod, Method)
import Data.Foldable (foldl')
import Data.Maybe
import Data.String (fromString)
import Data.Aeson
import Data.Text (Text, intercalate, dropWhile, split, break, drop, isPrefixOf, isSuffixOf)
import Data.Singletons.Bool
import Control.Applicative ((<|>))
import GHC.TypeLits

-- | Link data-type for hypermedia-links in HATEOAS with potentially templated URIs.
data RelationLink = RelationLink
  { _segs         :: [Text]
  , _params       :: [RelationParam]
  , _fragment     :: Maybe Text
  , _templated    :: Bool
  , _method       :: StdMethod
  , _contentTypes :: [MediaType]
  , _summary      :: Maybe Text
  , _description  :: Maybe Text
  , _title        :: Maybe Text
  } deriving (Show, Eq)

-- | Parameter data-type for hypermedia-links in HATEOAS.
data RelationParam = RelationParam
  { _name        :: Text
  , _required    :: Bool
  , _value       :: Maybe Text
  } deriving (Show, Eq)

-- | Shifting append-operator for 'RelationLink'.
--
-- This operator can be seen as a monoidal append for 'RelationLink' with a right-bias for meta information
-- e.g. '_method', '_contentTypes', '_summary' and '_description'.
(<<<) :: RelationLink -> RelationLink -> RelationLink
l1 <<< l2 =
  l1 { _segs         = _segs         l1  <> _segs        l2
     , _params       = _params       l1  <> _params      l2
     , _fragment     = _fragment     l1 <|> _fragment    l2
     , _templated    = _templated    l1  || _templated   l2
     , _method       = _method       l2
     , _contentTypes = _contentTypes l2
     , _summary      = _summary      l2 <|> _summary     l1
     , _description  = _description  l2 <|> _description l1
     }

-- | Get the hypermedia-reference of a 'RelationLink'.
getHref :: RelationLink -> Text
getHref l = getPath l <> getParams l <> maybe "" (\f -> "#" <> f) (_fragment l)

-- | Get the path of a 'RelationLink' as in 'getHref'.
getPath :: RelationLink -> Text
getPath = ("/" <>) .  intercalate "/" . _segs

-- | Get the parameters of a 'RelationLink' as in 'getHref'.
getParams :: RelationLink -> Text
getParams link =
     (if filledParams == [] then "" else "?" <> intercalate "&" (fmap (\(k,v) -> k <> "=" <> v) filledParams))
  <> (if templatedParams == [] then "" else "{?" <> intercalate "," templatedParams <> "}")
  where
    (filledParams, templatedParams) =
      foldl'
        (\(fs, ts) l -> case _value l of Nothing -> (fs, _name l : ts) ; Just v -> ((_name l, v) : fs, ts))
        ([], []) $
        _params link

-- | Prepend a path segment to a 'RelationLink'.
--
-- Takes care of potential templating.
prependSeg :: Text -> RelationLink -> RelationLink
prependSeg seg l
  | "{" `isPrefixOf` seg && "}" `isSuffixOf` seg = l { _segs = seg : _segs l, _templated = True }
  | otherwise = l { _segs = seg : _segs l }

-- | Prepend path segments to a 'RelationLink'.
--
-- Takes care of potential templating.
prependSegs :: [Text] -> RelationLink -> RelationLink
prependSegs segs l
  | any (\seg -> "{" `isPrefixOf` seg && "}" `isSuffixOf` seg) segs = l { _segs = segs <> _segs l, _templated = True }
  | otherwise                                                       = l { _segs = segs <> _segs l }

-- | Add a parameter to a 'RelationLink'.
--
-- Takes care of potential templating.
addParam :: RelationParam -> RelationLink -> RelationLink
addParam p l = l { _params = p : _params l, _templated = _templated l || isNothing (_value p) }

-- | Add parameters to a 'RelationLink'.
--
-- Takes care of potential templating.
addParams :: [RelationParam] -> RelationLink -> RelationLink
addParams ps l = l { _params = ps <> _params l, _templated = _templated l || any (isNothing . _value) ps }

-- | Create a placeholder for a template path segment.
mkPlaceHolder :: Text -> Text
mkPlaceHolder s = "{" <> s <> "}"

-- | Creates a 'RelationLink' from a 'Link'.
fromLink :: [MediaType] -> StdMethod -> Link -> RelationLink
fromLink cts m = fromURI cts m . linkURI

-- | Creates a 'RelationLink' from an 'URI'.
fromURI :: [MediaType] -> StdMethod -> URI -> RelationLink
fromURI cts m uri@(URI _ _ _ query frag) = RelationLink
  { _segs = fromString <$> pathSegments uri
  , _params = params
  , _fragment = if frag == "" then Nothing else Just (fromString frag)
  , _templated = False
  , _method = m
  , _contentTypes = cts
  , _summary = Nothing
  , _description = Nothing
  , _title = Nothing
  }
  where
    params = filter ((/= "") . _name)
      $ fmap (\kv -> let (k, drop 1 -> v) = break (== '=') kv in RelationParam k False $ if v == "" then Nothing else Just v)
      $ split (== '&')
      $ dropWhile (== '?')
      $ fromString
      $ unEscapeString query

-- | Like 'reflectMethod' but returns a 'StdMethod'.
reflectStdMethod :: ReflectMethod method => Proxy method -> StdMethod
reflectStdMethod = unsafeMethodToStdMethod . reflectMethod

unsafeMethodToStdMethod :: Method -> StdMethod
unsafeMethodToStdMethod (parseMethod -> Right m) = m
unsafeMethodToStdMethod (parseMethod -> Left  m) = error $ "Cannot convert " <> show m <> " to StdMethod"

instance ToJSON RelationLink where
  toJSON = String . getHref

-- | Class for creating a templated 'RelationLink' to an endpoint.
class HasTemplatedLink endpoint where
  toTemplatedLink :: Proxy endpoint -> RelationLink

instance HasTemplatedLink b => HasTemplatedLink (EmptyAPI :> b) where
  toTemplatedLink _ = toTemplatedLink (Proxy @b)

instance (KnownSymbol sym, HasTemplatedLink b) => HasTemplatedLink ((sym :: Symbol) :> b) where
  toTemplatedLink _ = prependSeg prefix $ toTemplatedLink (Proxy @b)
    where
      prefix = fromString $ symbolVal (Proxy @sym)

instance (KnownSymbol sym, HasTemplatedLink b) => HasTemplatedLink (Capture' mods sym a :> b) where
  toTemplatedLink _ = prependSeg prefix $ toTemplatedLink (Proxy @b)
    where
      prefix = mkPlaceHolder $ fromString $ symbolVal (Proxy @sym)

instance (KnownSymbol sym, HasTemplatedLink b) => HasTemplatedLink (CaptureAll sym a :> b) where
  toTemplatedLink _ = prependSeg prefix $ toTemplatedLink (Proxy @b)
    where
      prefix = mkPlaceHolder $ fromString $ symbolVal (Proxy @sym)

instance HasTemplatedLink b => HasTemplatedLink (Header' mods sym a :> b) where
  toTemplatedLink _ = toTemplatedLink (Proxy @b)

instance HasTemplatedLink b => HasTemplatedLink (HttpVersion :> b) where
  toTemplatedLink _ = toTemplatedLink (Proxy @b)

instance (HasTemplatedLink b, KnownSymbol sym, SBoolI (FoldRequired mods)) => HasTemplatedLink (QueryParam' mods sym a :> b) where
  toTemplatedLink _ = let rl = toTemplatedLink (Proxy @b) in rl { _params = param : _params rl, _templated = True }
    where
      param = RelationParam
        { _name = fromString $ symbolVal (Proxy @sym)
        , _required = fromSBool $ sbool @(FoldRequired mods)
        , _value = Nothing
        }

instance (HasTemplatedLink b, KnownSymbol sym) => HasTemplatedLink (QueryParams sym a :> b) where
  toTemplatedLink _ = let rl = toTemplatedLink (Proxy @b) in rl { _params = param : _params rl, _templated = True }
    where
      param = RelationParam
        { _name = fromString $ symbolVal (Proxy @sym)
        , _required = False
        , _value = Nothing
        }

instance (HasTemplatedLink b, KnownSymbol sym) => HasTemplatedLink (QueryFlag sym :> b) where
  toTemplatedLink _ = let rl = toTemplatedLink (Proxy @b) in rl { _params = param : _params rl, _templated = True }
    where
      param = RelationParam
        { _name = fromString $ symbolVal (Proxy @sym)
        , _required = False
        , _value = Nothing
        }

instance HasTemplatedLink b => HasTemplatedLink (QueryString :> b) where
  toTemplatedLink _ = toTemplatedLink (Proxy @b)

instance (HasTemplatedLink b, KnownSymbol sym) => HasTemplatedLink (DeepQuery sym a :> b) where
  toTemplatedLink _ = let rl = toTemplatedLink (Proxy @b) in rl { _params = param : _params rl, _templated = True }
    where
      param = RelationParam
        { _name = fromString $ symbolVal (Proxy @sym)
        , _required = False
        , _value = Nothing
        }

instance HasTemplatedLink b => HasTemplatedLink (Fragment a :> b) where
  toTemplatedLink _ = toTemplatedLink (Proxy @b)

instance HasTemplatedLink b => HasTemplatedLink (ReqBody' mods cts a :> b) where
  toTemplatedLink _ = toTemplatedLink (Proxy @b)

instance HasTemplatedLink b => HasTemplatedLink (RemoteHost :> b) where
  toTemplatedLink _ = toTemplatedLink (Proxy @b)

instance HasTemplatedLink b => HasTemplatedLink (IsSecure :> b) where
  toTemplatedLink _ = toTemplatedLink (Proxy @b)

instance HasTemplatedLink b => HasTemplatedLink (Vault :> b) where
  toTemplatedLink _ = toTemplatedLink (Proxy @b)

instance HasTemplatedLink b => HasTemplatedLink (WithNamedContext name subs sub :> b) where
  toTemplatedLink _ = toTemplatedLink (Proxy @b)

instance HasTemplatedLink b => HasTemplatedLink (WithResource res :> b) where
  toTemplatedLink _ = toTemplatedLink (Proxy @b)

instance (ReflectMethod m, AllMime cts) => HasTemplatedLink (Verb m s cts a) where
  toTemplatedLink _ = RelationLink
    { _segs = mempty
    , _params = []
    , _fragment = Nothing
    , _templated = False
    , _contentTypes = allMime (Proxy @cts)
    , _method = reflectStdMethod (Proxy @m)
    , _summary = Nothing
    , _description = Nothing
    , _title = Nothing
    }

instance ReflectMethod m => HasTemplatedLink (NoContentVerb m) where
  toTemplatedLink _ = RelationLink
    { _segs = mempty
    , _params = []
    , _fragment = Nothing
    , _templated = False
    , _contentTypes = mempty
    , _method = reflectStdMethod (Proxy @m)
    , _summary = Nothing
    , _description = Nothing
    , _title = Nothing
    }

instance (ReflectMethod m, AllMime cts) => HasTemplatedLink (UVerb m cts as) where
  toTemplatedLink _ = RelationLink
    { _segs = mempty
    , _params = []
    , _fragment = Nothing
    , _templated = False
    , _contentTypes = allMime (Proxy @cts)
    , _method = reflectStdMethod (Proxy @m)
    , _summary = Nothing
    , _description = Nothing
    , _title = Nothing
    }

instance (ReflectMethod m, Accept ct) => HasTemplatedLink (Stream m s f ct a) where
  toTemplatedLink _ = RelationLink
    { _segs = mempty
    , _params = []
    , _fragment = Nothing
    , _templated = False
    , _contentTypes = pure $ contentType (Proxy @ct)
    , _method = reflectStdMethod (Proxy @m)
    , _summary = Nothing
    , _description = Nothing
    , _title = Nothing
    }

instance HasTemplatedLink b => HasTemplatedLink (BasicAuth realm userData :> b) where
  toTemplatedLink _ = toTemplatedLink (Proxy @b)

instance (KnownSymbol sym, HasTemplatedLink b) => HasTemplatedLink (Description sym :> b) where
  toTemplatedLink _ = let rl = toTemplatedLink (Proxy @b) in rl { _description = _description rl <|> Just descr }
    where
      descr = fromString $ symbolVal (Proxy @sym)

instance (KnownSymbol sym, HasTemplatedLink b) => HasTemplatedLink (Summary sym :> b) where
  toTemplatedLink _ = let rl = toTemplatedLink (Proxy @b) in rl { _summary = _summary rl <|> Just summary }
    where
      summary = fromString $ symbolVal (Proxy @sym)

-- | Class for creating a 'RelationLink' to an endpoint.
--
-- This is highly similar to 'HasLink' but it also gathers HATEOAS meta-information for the resource a link refers to.
class HasLink endpoint => HasRelationLink endpoint where
  toRelationLink :: Proxy endpoint -> MkLink endpoint RelationLink

-- | Convenience alias-constraint for right-hand sides of @a ':>' b@ where b is some function producing a 'RelationLink'.
type RightLink b =
  ( HasRelationLink b
  , PolyvariadicComp (MkLink b RelationLink) (IsFun (MkLink b RelationLink))
  , Return (MkLink b RelationLink) (IsFun (MkLink b RelationLink)) ~ RelationLink
  , Replace (MkLink b RelationLink) RelationLink (IsFun (MkLink b RelationLink)) ~ MkLink b RelationLink
  )

instance (AllMime cts, ReflectMethod m) => HasRelationLink (Verb m s cts a) where
  toRelationLink _ = RelationLink
    { _segs = mempty
    , _params = []
    , _fragment = Nothing
    , _templated = False
    , _contentTypes = allMime (Proxy @cts)
    , _method = reflectStdMethod (Proxy @m)
    , _summary = Nothing
    , _description = Nothing
    , _title = Nothing
    }

instance (AllMime cts, ReflectMethod m) => HasRelationLink (UVerb m cts as) where
  toRelationLink _ = RelationLink
    { _segs = mempty
    , _params = []
    , _fragment = Nothing
    , _templated = False
    , _contentTypes = allMime (Proxy @cts)
    , _method = reflectStdMethod (Proxy @m)
    , _summary = Nothing
    , _description = Nothing
    , _title = Nothing
    }

instance ReflectMethod m => HasRelationLink (NoContentVerb m) where
  toRelationLink _ = RelationLink
    { _segs = mempty
    , _params = []
    , _fragment = Nothing
    , _templated = False
    , _contentTypes = mempty
    , _method = reflectStdMethod (Proxy @m)
    , _summary = Nothing
    , _description = Nothing
    , _title = Nothing
    }

instance (ReflectMethod m, Accept ct) => HasRelationLink (Stream m s f ct a) where
  toRelationLink _ = RelationLink
    { _segs = mempty
    , _params = []
    , _fragment = Nothing
    , _templated = False
    , _contentTypes = pure $ contentType (Proxy @ct)
    , _method = reflectStdMethod (Proxy @m)
    , _summary = Nothing
    , _description = Nothing
    , _title = Nothing
    }

instance (KnownSymbol sym, RightLink b) => HasRelationLink ((sym :: Symbol) :> b) where
  toRelationLink _ = prependSeg seg ... toRelationLink (Proxy @b)
    where
      seg = fromString $ symbolVal (Proxy @sym)

instance (KnownSymbol sym, RightLink b) => HasRelationLink (Summary sym :> b) where
  toRelationLink _ = (\rl -> rl { _summary = _summary rl <|> Just summary }) ... toRelationLink (Proxy @b)
    where
      summary = fromString $ symbolVal (Proxy @sym)

instance (KnownSymbol sym, RightLink b) => HasRelationLink (Description sym :> b) where
  toRelationLink _ = (\rl -> rl { _description = _description rl <|> Just descr }) ... toRelationLink (Proxy @b)
    where
      descr = fromString $ symbolVal (Proxy @sym)

instance HasRelationLink b => HasRelationLink (HttpVersion :> b) where
  toRelationLink _ = toRelationLink (Proxy @b)

instance HasRelationLink b => HasRelationLink (BasicAuth realm userData :> b) where
  toRelationLink _ = toRelationLink (Proxy @b)

instance (KnownSymbol sym, RightLink b, ToHttpApiData a) => HasRelationLink (Capture' mods sym a :> b) where
  toRelationLink _ x = prependSeg (toUrlPiece x) ... toRelationLink (Proxy @b)

instance (KnownSymbol sym, RightLink b, ToHttpApiData a) => HasRelationLink (CaptureAll sym a :> b) where
  toRelationLink _ xs = prependSegs (toUrlPiece <$> xs) ... toRelationLink (Proxy @b)

instance (RightLink b, ToHttpApiData a) => HasRelationLink (Fragment a :> b) where
  toRelationLink _ x = (\l -> l { _fragment = Just $ toQueryParam x }) ... toRelationLink (Proxy @b)

instance HasRelationLink b => HasRelationLink (Header' mods sym a :> b) where
  toRelationLink _ = toRelationLink (Proxy @b)

instance HasRelationLink b => HasRelationLink (IsSecure :> b) where
  toRelationLink _ = toRelationLink (Proxy @b)

instance (KnownSymbol sym, RightLink b) => HasRelationLink (QueryFlag sym :> b) where
  toRelationLink _ False = addParam (RelationParam (fromString $ symbolVal $ Proxy @sym) False (Just "false")) ... toRelationLink (Proxy @b)
  toRelationLink _ True  = addParam (RelationParam (fromString $ symbolVal $ Proxy @sym) False (Just "true")) ... toRelationLink (Proxy @b)

instance (KnownSymbol sym, ToHttpApiData a, RightLink b, SBoolI (FoldRequired mods)) => HasRelationLink (QueryParam' mods sym a :> b) where
  toRelationLink _ mv = addParam param ... toRelationLink (Proxy @b)
    where
      param = case sbool :: SBool (FoldRequired mods) of
        STrue  -> RelationParam (fromString $ symbolVal $ Proxy @sym) True    (Just $ toQueryParam mv)
        SFalse -> RelationParam (fromString $ symbolVal $ Proxy @sym) False $ toQueryParam <$> mv

instance (KnownSymbol sym, RightLink b, ToHttpApiData a) => HasRelationLink (QueryParams sym a :> b) where
  toRelationLink _ xs = addParams params ... toRelationLink (Proxy @b)
    where
      params = (\x -> RelationParam (fromString $ symbolVal $ Proxy @sym) False (Just $ toQueryParam x)) <$> xs

instance RightLink b => HasRelationLink (RemoteHost :> b) where
  toRelationLink _ = toRelationLink (Proxy @b)

instance RightLink b => HasRelationLink (ReqBody' mods cts a :> b) where
  toRelationLink _ = toRelationLink (Proxy @b)

instance RightLink b => HasRelationLink (WithResource res :> b) where
  toRelationLink _ = toRelationLink (Proxy @b)

instance RightLink b => HasRelationLink (Vault :> b) where
  toRelationLink _ = toRelationLink (Proxy @b)