servant-hateoas 0.3.3 → 0.3.4
raw patch · 14 files changed
+510/−217 lines, 14 files
Files
- CHANGELOG.md +11/−0
- README.md +53/−6
- servant-hateoas.cabal +2/−1
- src/Servant/Hateoas.hs +3/−1
- src/Servant/Hateoas/Combinator/Title.hs +36/−0
- src/Servant/Hateoas/ContentType/Collection.hs +4/−3
- src/Servant/Hateoas/ContentType/HAL.hs +8/−4
- src/Servant/Hateoas/Example.hs +5/−5
- src/Servant/Hateoas/Internal/Sym.hs +4/−1
- src/Servant/Hateoas/Layer/Build.hs +69/−94
- src/Servant/Hateoas/Layer/Type.hs +2/−3
- src/Servant/Hateoas/RelationLink.hs +294/−73
- src/Servant/Hateoas/Resource.hs +5/−19
- src/Servant/Hateoas/ResourceServer.hs +14/−7
CHANGELOG.md view
@@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [PVP versioning](https://pvp.haskell.org/). +## v0.3.4 _(2024-12-30)_++### Added+- Added class `HasTemplateLink` for fully templated links to endpoints+- Added combinator `data Title (sym :: Symbol)` for human-readable titles of resources++### Changed+- Class `HasRelationLink` now returns complete links instead of partially templated ones+- Replaced all usages of `Link` with `RelationLink`, allowing more flexibility when gathering information about the resource the link refers to+- Extended the rendering of `HALResource` by props `type` (Content-Type) and `title`+ ## v0.3.3 _(2024-12-28)_ ### Added
README.md view
@@ -61,18 +61,19 @@ -- add a link to the address-resource with the relation "address" for the user-resource instance Resource res => ToResource res User where- toResource _ usr = addRel ("address", CompleteLink $ mkAddrLink $ addressId usr) $ wrap usr+ toResource _ ct usr = addRel ("address", mkAddrLink $ addressId usr) $ wrap usr where- mkAddrLink = safeLink (Proxy @AddressGetOne) (Proxy @AddressGetOne)+ mkAddrLink = toRelationLink $ resourcifyProxy (Proxy @AddressGetOne) ct ``` Further we define our API as usual: ```haskell type Api = UserApi :<|> AddressApi -type UserApi = UserGetOne :<|> UserGetAll :<|> UserGetAllCool-type UserGetOne = "api" :> "user" :> Capture "id" Int :> Get '[JSON] User-type UserGetAll = "api" :> "user" :> Get '[JSON] [User]+type UserApi = UserGetOne :<|> UserGetAll :<|> UserGetQuery+type UserGetOne = "api" :> "user" :> Title "The user with the given id" :> Capture "id" Int :> Get '[JSON] User+type UserGetAll = "api" :> "user" :> Get '[JSON] [User]+type UserGetQuery = "api" :> "user" :> "query" :> QueryParam "addrId" Int :> QueryParam "income" Double :> Get '[JSON] User type AddressApi = AddressGetOne type AddressGetOne = "api" :> "address" :> Capture "id" Int :> Get '[JSON] Address@@ -89,9 +90,11 @@ This is nothing but the usual servant-server implementation, just that the implementation is not floating around in the source code and instead is bound to a class instance. ```haskell instance HasHandler UserGetOne where- getHandler _ _ = \uId -> return $ User uId 1 1000+ getHandler _ _ = \uId -> return $ User uId 0 0 instance HasHandler UserGetAll where getHandler _ _ = return [User 1 1 1000, User 2 2 2000, User 42 3 3000]+instance HasHandler UserGetQuery where+ getHandler _ _ = \mAddrId mIncome -> return $ User 42 (maybe 0 id mAddrId) (maybe 0 id mIncome) instance HasHandler AddressGetOne where getHandler _ _ = \aId -> return $ Address aId "Foo St" "BarBaz" ```@@ -103,6 +106,50 @@ ``` For now `apiServer` and `layerServer` exist in isolation, but the goal is to merge them into one.++When we now run the `layerServer` and request `GET http://host:port/api/user/query`, we get:+```json+{+ "_embedded": {},+ "_links": {+ "addrId": {+ "href": "/api/user/query{?addrId}",+ "templated": true,+ "type": "application/hal+json"+ },+ "income": {+ "href": "/api/user/query{?income}",+ "templated": true,+ "type": "application/hal+json"+ },+ "self": {+ "href": "/api/user/query",+ "type": "application/hal+json"+ }+ }+}+```++Similar for `userServer` and `GET http://host:port/api/user/42`:+```json+{+ "_embedded": {},+ "_links": {+ "address": {+ "href": "/api/address/0",+ "type": "application/hal+json"+ },+ "self": {+ "href": "/api/user/42",+ "title": "The user with the given id",+ "type": "application/hal+json"+ }+ },+ "addressId": 0,+ "income": 0,+ "usrId": 42+}+``` The complete example can be found [here](https://github.com/bruderj15/servant-hateoas/blob/main/src/Servant/Hateoas/Example.hs).
servant-hateoas.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: servant-hateoas-version: 0.3.3+version: 0.3.4 synopsis: HATEOAS extension for servant description: Create Resource-Representations for your types and make your API HATEOAS-compliant. Automatically derive a HATEOAS-API and server-implementation from your API or straight up define a HATEOAS-API yourself.@@ -41,6 +41,7 @@ , Servant.Hateoas.Layer.Build , Servant.Hateoas.Layer.Merge , Servant.Hateoas.ContentType.HAL+ , Servant.Hateoas.Combinator.Title , Servant.Hateoas.Internal.Sym , Servant.Hateoas.Internal.Polyvariadic
src/Servant/Hateoas.hs view
@@ -1,5 +1,6 @@ module Servant.Hateoas-( module Servant.Hateoas.ContentType.HAL+( module Servant.Hateoas.Combinator.Title+, module Servant.Hateoas.ContentType.HAL , module Servant.Hateoas.ResourceServer , module Servant.Hateoas.RelationLink , module Servant.Hateoas.HasHandler@@ -7,6 +8,7 @@ , module Servant.Hateoas.Layer ) where +import Servant.Hateoas.Combinator.Title import Servant.Hateoas.ContentType.HAL (HAL, HALResource) import Servant.Hateoas.ResourceServer import Servant.Hateoas.RelationLink
+ src/Servant/Hateoas/Combinator/Title.hs view
@@ -0,0 +1,36 @@+{-# LANGUAGE UndecidableInstances #-}++module Servant.Hateoas.Combinator.Title where++import Servant+import Servant.Hateoas.HasHandler+import Servant.Hateoas.RelationLink+import Servant.Hateoas.Internal.Polyvariadic+import Data.String (fromString)+import Control.Applicative ((<|>))+import GHC.TypeLits++-- | Combinator similar to 'Summary' and 'Description' but for the human readable title of the resource a 'RelationLink' refers to.+data Title (sym :: Symbol)++instance HasLink b => HasLink (Title sym :> b) where+ type MkLink (Title sym :> b) link = MkLink b link+ toLink f _ = toLink f (Proxy @b)++instance HasServer api ctx => HasServer (Title desc :> api) ctx where+ type ServerT (Title desc :> api) m = ServerT api m+ route _ = route (Proxy @api)+ hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy @api) pc nt s++instance HasHandler api => HasHandler (Title desc :> api) where+ getHandler m _ = getHandler m (Proxy @api)++instance (KnownSymbol title, HasTemplatedLink api) => HasTemplatedLink (Title title :> api) where+ toTemplatedLink _ = (\l -> l { _title = _title l <|> Just title }) $ toTemplatedLink (Proxy @api)+ where+ title = fromString $ symbolVal (Proxy @title)++instance (KnownSymbol title, RightLink api) => HasRelationLink (Title title :> api) where+ toRelationLink _ = (\l -> l { _title = _title l <|> Just title }) ... toRelationLink (Proxy @api)+ where+ title = fromString $ symbolVal (Proxy @title)
src/Servant/Hateoas/ContentType/Collection.hs view
@@ -8,6 +8,7 @@ where import Servant.Hateoas.Resource+import Servant.Hateoas.RelationLink import Servant.API.ContentTypes import qualified Network.HTTP.Media as M import Servant.Links@@ -28,7 +29,7 @@ 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+ , rels :: [(String, RelationLink)] -- ^ Pairs @(rel, link)@ for relations } deriving (Show, Generic, Functor) instance Semigroup (CollectionResource a) where@@ -40,7 +41,7 @@ -- | A single item inside a 'CollectionResource'. data CollectionItem a = CollectionItem { item :: a -- ^ Wrapped item- , itemLinks :: [(String, ResourceLink)] -- ^ Links for the wrapped item+ , itemLinks :: [(String, RelationLink)] -- ^ Links for the wrapped item } deriving (Show, Generic, Functor) instance Resource CollectionResource where@@ -57,7 +58,7 @@ instance ToJSON (CollectionResource a) => MimeRender (Collection JSON) (CollectionResource a) where mimeRender _ = encode -collectionLinks :: [(String, ResourceLink)] -> Value+collectionLinks :: [(String, RelationLink)] -> Value collectionLinks = Array . Foldable.foldl' (\xs (rel, l) -> pure (object ["name" .= rel, "value" .= l]) <> xs) mempty -- TODO: I dont like this at all
src/Servant/Hateoas/ContentType/HAL.hs view
@@ -19,6 +19,7 @@ import Data.Kind import Data.Aeson import Data.Aeson.KeyMap (singleton)+import qualified Data.Text as Text import GHC.Exts import GHC.Generics @@ -32,7 +33,7 @@ -- | HAL-resource representation. data HALResource a = HALResource { resource :: a -- ^ Wrapped resource- , rels :: [(String, ResourceLink)] -- ^ Pairs @(rel, link)@ for hypermedia relations+ , rels :: [(String, RelationLink)] -- ^ Pairs @(rel, link)@ for hypermedia relations , embedded :: [(String, SomeF HALResource ToJSON)] -- ^ Pairs @(rel, resource)@ for embedded resources } deriving (Generic, Functor) @@ -46,9 +47,12 @@ instance ToJSON (HALResource a) => MimeRender (HAL JSON) (HALResource a) where mimeRender _ = encode -renderHalLink :: ResourceLink -> Value-renderHalLink (CompleteLink l) = let uri = linkURI l in object ["href" .= uri { uriPath = "/" <> uriPath uri }]-renderHalLink (TemplateLink l) = object $ ["href" .= l {_path = "/" <> _path l } ] <> if _templated l then ["templated" .= True] else []+renderHalLink :: RelationLink -> Value+renderHalLink l = object $+ [ "href" .= getHref l+ , "type" .= Text.intercalate "|" (fromString . show <$> _contentTypes l)+ ] <> if _templated l then ["templated" .= True] else []+ <> maybe mempty (\t -> ["title" .= t]) (_title l) 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)
src/Servant/Hateoas/Example.hs view
@@ -17,16 +17,16 @@ deriving anyclass (ToJSON, ToResource res) instance Resource res => ToResource res User where- toResource _ usr = addRel ("address", CompleteLink $ mkAddrLink $ addressId usr) $ wrap usr+ toResource _ ct usr = addRel ("address", mkAddrLink $ addressId usr) $ wrap usr where- mkAddrLink = safeLink (Proxy @AddressGetOne) (Proxy @AddressGetOne)+ mkAddrLink = toRelationLink $ resourcifyProxy (Proxy @AddressGetOne) ct type Api = UserApi :<|> AddressApi type UserApi = UserGetOne :<|> UserGetAll :<|> UserGetQuery-type UserGetOne = "api" :> "user" :> Capture "id" Int :> Get '[JSON] User-type UserGetAll = "api" :> "user" :> Get '[JSON] [User]-type UserGetQuery = "api" :> "user" :> "querying" :> QueryParam "addrId" Int :> QueryParam "income" Double :> Get '[JSON] User+type UserGetOne = "api" :> "user" :> Title "The user with the given id" :> Capture "id" Int :> Get '[JSON] User+type UserGetAll = "api" :> "user" :> Get '[JSON] [User]+type UserGetQuery = "api" :> "user" :> "query" :>QueryParam "addrId" Int :> QueryParam "income" Double :> Get '[JSON] User type AddressApi = AddressGetOne type AddressGetOne = "api" :> "address" :> Capture "id" Int :> Get '[JSON] Address
src/Servant/Hateoas/Internal/Sym.hs view
@@ -25,7 +25,10 @@ type MkLink (Sym sym :> api) link = MkLink (sym :> api) link toLink f _ = toLink f (Proxy @(sym :> api)) -instance (HasRelationLink api, KnownSymbol sym) => HasRelationLink (Sym sym :> api) where+instance (HasTemplatedLink api, KnownSymbol sym) => HasTemplatedLink (Sym sym :> api) where+ toTemplatedLink _ = toTemplatedLink (Proxy @(sym :> api))++instance (KnownSymbol sym, HasRelationLink (sym :> api), HasLink api) => HasRelationLink (Sym sym :> api) where toRelationLink _ = toRelationLink (Proxy @(sym :> api)) instance (HasHandler api, KnownSymbol sym) => HasHandler (Sym sym :> api) where
src/Servant/Hateoas/Layer/Build.hs view
@@ -12,7 +12,6 @@ import Servant import Servant.API.ContentTypes-import Servant.Hateoas.Resource import Servant.Hateoas.RelationLink import Servant.Hateoas.Layer.Type import Servant.Hateoas.Internal.Sym@@ -27,179 +26,155 @@ ReplaceHandler (a -> b) replacement = a -> ReplaceHandler b replacement ReplaceHandler _ replacement = replacement --- | Create all 'ResourceLink's to a 'Layer's 'RelativeChildren'.+-- | Create all 'RelationLink's to a 'Layer's 'RelativeChildren'. type BuildLayerLinks :: Layer -> (Type -> Type) -> Constraint class BuildLayerLinks l m where- buildLayerLinks :: MonadIO m => Proxy l -> Proxy m -> ReplaceHandler (ServerT l m) [(String, ResourceLink)]+ buildLayerLinks :: MonadIO m => Proxy l -> Proxy m -> ReplaceHandler (ServerT l m) [(String, RelationLink)] instance ( api ~ MkPrefix apiCs verb- , HasLink api, IsElem api api- , mkSelf ~ MkLink api Link+ , HasRelationLink api+ , mkSelf ~ MkLink api RelationLink , PolyvariadicComp mkSelf (IsFun mkSelf)- , Return mkSelf (IsFun mkSelf) ~ Link- , Replace mkSelf [(String, ResourceLink)] (IsFun mkSelf) ~ ReplaceHandler (ServerT api m) [(String, ResourceLink)]+ , Return mkSelf (IsFun mkSelf) ~ RelationLink+ , Replace mkSelf [(String, RelationLink)] (IsFun mkSelf) ~ ReplaceHandler (ServerT api m) [(String, RelationLink)] ) => BuildLayerLinks ('Layer apiCs '[] verb) m where- buildLayerLinks _ _ = (pure @[] . ("self", ) . CompleteLink) ... mkSelf+ buildLayerLinks _ _ = (pure @[] . ("self", )) ... mkSelf where- mkSelf = safeLink (Proxy @api) (Proxy @api)+ mkSelf = toRelationLink (Proxy @api) instance ( c ~ MkPrefix (apiCs ++ '[Sym sym]) verb- , HasLink c, IsElem c c- , mkLink ~ MkLink c Link+ , HasRelationLink c+ , mkLink ~ MkLink c RelationLink , KnownSymbol sym , BuildLayerLinks ('Layer apiCs cs verb) m- , buildLinksFun ~ (ReplaceHandler (ServerT (MkPrefix apiCs verb) m) [(String, ResourceLink)])+ , buildLinksFun ~ (ReplaceHandler (ServerT (MkPrefix apiCs verb) m) [(String, RelationLink)]) , PolyvariadicComp2 mkLink buildLinksFun (IsFun mkLink)- , Return2 mkLink buildLinksFun (IsFun mkLink) ~ (Link, [(String, ResourceLink)])- , Replace2 mkLink buildLinksFun [(String, ResourceLink)] (IsFun mkLink) ~ buildLinksFun+ , Return2 mkLink buildLinksFun (IsFun mkLink) ~ (RelationLink, [(String, RelationLink)])+ , Replace2 mkLink buildLinksFun [(String, RelationLink)] (IsFun mkLink) ~ buildLinksFun ) => BuildLayerLinks ('Layer apiCs (Sym sym ': cs) verb) m where- buildLayerLinks _ m = pcomp2 (\(l, ls) -> (symbolVal (Proxy @sym), CompleteLink l) : ls) mkLink mkLinks+ buildLayerLinks _ m = pcomp2 (\(l, ls) -> (symbolVal (Proxy @sym), l) : ls) mkLink mkLinks where- mkLink = safeLink (Proxy @c) (Proxy @c)+ mkLink = toRelationLink (Proxy @c) mkLinks = buildLayerLinks (Proxy @('Layer apiCs cs verb)) m instance ( verb ~ Verb method status cts a , AllMime cts, ReflectMethod method , api ~ MkPrefix apiCs verb+ , HasRelationLink api , KnownSymbol sym- , HasRelationLink (MkPrefix '[Capture' mods sym x] verb)- , IsElem api api, HasLink api+ , HasTemplatedLink (MkPrefix '[Capture' mods sym x] verb) , BuildLayerLinks ('Layer apiCs cs verb) m- , buildLinksFun ~ (ReplaceHandler (ServerT api m) [(String, ResourceLink)])- , PolyvariadicComp2 (MkLink api Link) buildLinksFun (IsFun buildLinksFun)- , Return2 (MkLink api Link) buildLinksFun (IsFun buildLinksFun) ~ (Link, [(String, ResourceLink)])- , Replace2 (MkLink api Link) buildLinksFun [(String, ResourceLink)] (IsFun buildLinksFun) ~ buildLinksFun+ , buildLinksFun ~ (ReplaceHandler (ServerT api m) [(String, RelationLink)])+ , PolyvariadicComp2 (MkLink api RelationLink) buildLinksFun (IsFun buildLinksFun)+ , Return2 (MkLink api RelationLink) buildLinksFun (IsFun buildLinksFun) ~ (RelationLink, [(String, RelationLink)])+ , Replace2 (MkLink api RelationLink) buildLinksFun [(String, RelationLink)] (IsFun buildLinksFun) ~ buildLinksFun ) => BuildLayerLinks ('Layer apiCs (Capture' mods sym x ': cs) verb) m where- buildLayerLinks _ m = pcomp2 (\(self, ls) -> (relName, mkTemplatedNext self) : ls) mkSelf mkLinks+ buildLayerLinks _ m = pcomp2 (\(self, ls) -> (relName, self <<< child) : ls) mkSelf mkLinks where relName = symbolVal (Proxy @sym)- mkSelf = safeLink (Proxy @api) (Proxy @api)+ mkSelf = toRelationLink (Proxy @api) mkLinks = buildLayerLinks (Proxy @('Layer apiCs cs verb)) m- child = toRelationLink (Proxy @(MkPrefix '[Capture' mods sym x] verb))- mkTemplatedNext = TemplateLink- . (\rl -> rl { _path = _path rl `appendPath` _path child, _templated = True })- . fromURI (allMime $ Proxy @cts) (reflectStdMethod (Proxy @method))- . linkURI+ child = toTemplatedLink (Proxy @(MkPrefix '[Capture' mods sym x] verb)) instance ( verb ~ Verb method status cts a , AllMime cts, ReflectMethod method , api ~ MkPrefix apiCs verb+ , HasRelationLink api , KnownSymbol sym- , HasRelationLink (MkPrefix '[CaptureAll sym x] verb)- , IsElem api api, HasLink api+ , HasTemplatedLink (MkPrefix '[CaptureAll sym x] verb) , BuildLayerLinks ('Layer apiCs cs verb) m- , buildLinksFun ~ (ReplaceHandler (ServerT api m) [(String, ResourceLink)])- , PolyvariadicComp2 (MkLink api Link) buildLinksFun (IsFun buildLinksFun)- , Return2 (MkLink api Link) buildLinksFun (IsFun buildLinksFun) ~ (Link, [(String, ResourceLink)])- , Replace2 (MkLink api Link) buildLinksFun [(String, ResourceLink)] (IsFun buildLinksFun) ~ buildLinksFun+ , buildLinksFun ~ (ReplaceHandler (ServerT api m) [(String, RelationLink)])+ , PolyvariadicComp2 (MkLink api RelationLink) buildLinksFun (IsFun buildLinksFun)+ , Return2 (MkLink api RelationLink) buildLinksFun (IsFun buildLinksFun) ~ (RelationLink, [(String, RelationLink)])+ , Replace2 (MkLink api RelationLink) buildLinksFun [(String, RelationLink)] (IsFun buildLinksFun) ~ buildLinksFun ) => BuildLayerLinks ('Layer apiCs (CaptureAll sym x ': cs) verb) m where- buildLayerLinks _ m = pcomp2 (\(self, ls) -> (relName, mkTemplatedNext self) : ls) mkSelf mkLinks+ buildLayerLinks _ m = pcomp2 (\(self, ls) -> (relName, self <<< child) : ls) mkSelf mkLinks where relName = symbolVal (Proxy @sym)- mkSelf = safeLink (Proxy @api) (Proxy @api)+ mkSelf = toRelationLink (Proxy @api) mkLinks = buildLayerLinks (Proxy @('Layer apiCs cs verb)) m- child = toRelationLink (Proxy @(MkPrefix '[CaptureAll sym x] verb))- mkTemplatedNext = TemplateLink- . (\rl -> rl { _path = _path rl `appendPath` _path child, _templated = True })- . fromURI (allMime $ Proxy @cts) (reflectStdMethod (Proxy @method))- . linkURI+ child = toTemplatedLink (Proxy @(MkPrefix '[CaptureAll sym x] verb)) instance ( verb ~ Verb method status cts a , AllMime cts, ReflectMethod method , api ~ MkPrefix apiCs verb+ , HasRelationLink api , KnownSymbol sym- , HasRelationLink (MkPrefix '[QueryParam' mods sym x] verb)- , IsElem api api, HasLink api+ , HasTemplatedLink (MkPrefix '[QueryParam' mods sym x] verb) , BuildLayerLinks ('Layer apiCs cs verb) m- , buildLinksFun ~ (ReplaceHandler (ServerT api m) [(String, ResourceLink)])- , PolyvariadicComp2 (MkLink api Link) buildLinksFun (IsFun buildLinksFun)- , Return2 (MkLink api Link) buildLinksFun (IsFun buildLinksFun) ~ (Link, [(String, ResourceLink)])- , Replace2 (MkLink api Link) buildLinksFun [(String, ResourceLink)] (IsFun buildLinksFun) ~ buildLinksFun+ , buildLinksFun ~ (ReplaceHandler (ServerT api m) [(String, RelationLink)])+ , PolyvariadicComp2 (MkLink api RelationLink) buildLinksFun (IsFun buildLinksFun)+ , Return2 (MkLink api RelationLink) buildLinksFun (IsFun buildLinksFun) ~ (RelationLink, [(String, RelationLink)])+ , Replace2 (MkLink api RelationLink) buildLinksFun [(String, RelationLink)] (IsFun buildLinksFun) ~ buildLinksFun ) => BuildLayerLinks ('Layer apiCs (QueryParam' mods sym x ': cs) verb) m where- buildLayerLinks _ m = pcomp2 (\(self, ls) -> (relName, mkTemplatedNext self) : ls) mkSelf mkLinks+ buildLayerLinks _ m = pcomp2 (\(self, ls) -> (relName, self <<< child) : ls) mkSelf mkLinks where relName = symbolVal (Proxy @sym)- mkSelf = safeLink (Proxy @api) (Proxy @api)+ mkSelf = toRelationLink (Proxy @api) mkLinks = buildLayerLinks (Proxy @('Layer apiCs cs verb)) m- child = toRelationLink (Proxy @(MkPrefix '[QueryParam' mods sym x] verb))- mkTemplatedNext = TemplateLink- . (\rl -> rl { _params = _params rl ++ _params child, _templated = True })- . fromURI (allMime $ Proxy @cts) (reflectStdMethod (Proxy @method))- . linkURI+ child = toTemplatedLink (Proxy @(MkPrefix '[QueryParam' mods sym x] verb)) instance ( verb ~ Verb method status cts a , AllMime cts, ReflectMethod method , api ~ MkPrefix apiCs verb+ , HasRelationLink api , KnownSymbol sym- , HasRelationLink (MkPrefix '[QueryParams sym x] verb)- , IsElem api api, HasLink api+ , HasTemplatedLink (MkPrefix '[QueryParams sym x] verb) , BuildLayerLinks ('Layer apiCs cs verb) m- , buildLinksFun ~ (ReplaceHandler (ServerT api m) [(String, ResourceLink)])- , PolyvariadicComp2 (MkLink api Link) buildLinksFun (IsFun buildLinksFun)- , Return2 (MkLink api Link) buildLinksFun (IsFun buildLinksFun) ~ (Link, [(String, ResourceLink)])- , Replace2 (MkLink api Link) buildLinksFun [(String, ResourceLink)] (IsFun buildLinksFun) ~ buildLinksFun+ , buildLinksFun ~ (ReplaceHandler (ServerT api m) [(String, RelationLink)])+ , PolyvariadicComp2 (MkLink api RelationLink) buildLinksFun (IsFun buildLinksFun)+ , Return2 (MkLink api RelationLink) buildLinksFun (IsFun buildLinksFun) ~ (RelationLink, [(String, RelationLink)])+ , Replace2 (MkLink api RelationLink) buildLinksFun [(String, RelationLink)] (IsFun buildLinksFun) ~ buildLinksFun ) => BuildLayerLinks ('Layer apiCs (QueryParams sym x ': cs) verb) m where- buildLayerLinks _ m = pcomp2 (\(self, ls) -> (relName, mkTemplatedNext self) : ls) mkSelf mkLinks+ buildLayerLinks _ m = pcomp2 (\(self, ls) -> (relName, self <<< child) : ls) mkSelf mkLinks where relName = symbolVal (Proxy @sym)- mkSelf = safeLink (Proxy @api) (Proxy @api)+ mkSelf = toRelationLink (Proxy @api) mkLinks = buildLayerLinks (Proxy @('Layer apiCs cs verb)) m- child = toRelationLink (Proxy @(MkPrefix '[QueryParams sym x] verb))- mkTemplatedNext = TemplateLink- . (\rl -> rl { _params = _params rl ++ _params child, _templated = True })- . fromURI (allMime $ Proxy @cts) (reflectStdMethod (Proxy @method))- . linkURI+ child = toTemplatedLink (Proxy @(MkPrefix '[QueryParams sym x] verb)) instance ( verb ~ Verb method status cts a , AllMime cts, ReflectMethod method , api ~ MkPrefix apiCs verb+ , HasRelationLink api , KnownSymbol sym- , HasRelationLink (MkPrefix '[DeepQuery sym x] verb)- , IsElem api api, HasLink api+ , HasTemplatedLink (MkPrefix '[DeepQuery sym x] verb) , BuildLayerLinks ('Layer apiCs cs verb) m- , buildLinksFun ~ (ReplaceHandler (ServerT api m) [(String, ResourceLink)])- , PolyvariadicComp2 (MkLink api Link) buildLinksFun (IsFun buildLinksFun)- , Return2 (MkLink api Link) buildLinksFun (IsFun buildLinksFun) ~ (Link, [(String, ResourceLink)])- , Replace2 (MkLink api Link) buildLinksFun [(String, ResourceLink)] (IsFun buildLinksFun) ~ buildLinksFun+ , buildLinksFun ~ (ReplaceHandler (ServerT api m) [(String, RelationLink)])+ , PolyvariadicComp2 (MkLink api RelationLink) buildLinksFun (IsFun buildLinksFun)+ , Return2 (MkLink api RelationLink) buildLinksFun (IsFun buildLinksFun) ~ (RelationLink, [(String, RelationLink)])+ , Replace2 (MkLink api RelationLink) buildLinksFun [(String, RelationLink)] (IsFun buildLinksFun) ~ buildLinksFun ) => BuildLayerLinks ('Layer apiCs (DeepQuery sym x ': cs) verb) m where- buildLayerLinks _ m = pcomp2 (\(self, ls) -> (relName, mkTemplatedNext self) : ls) mkSelf mkLinks+ buildLayerLinks _ m = pcomp2 (\(self, ls) -> (relName, self <<< child) : ls) mkSelf mkLinks where relName = symbolVal (Proxy @sym)- mkSelf = safeLink (Proxy @api) (Proxy @api)+ mkSelf = toRelationLink (Proxy @api) mkLinks = buildLayerLinks (Proxy @('Layer apiCs cs verb)) m- child = toRelationLink (Proxy @(MkPrefix '[DeepQuery sym x] verb))- mkTemplatedNext = TemplateLink- . (\rl -> rl { _params = _params rl ++ _params child, _templated = True })- . fromURI (allMime $ Proxy @cts) (reflectStdMethod (Proxy @method))- . linkURI+ child = toTemplatedLink (Proxy @(MkPrefix '[DeepQuery sym x] verb)) instance ( verb ~ Verb method status cts a , AllMime cts, ReflectMethod method , api ~ MkPrefix apiCs verb+ , HasRelationLink api , KnownSymbol sym- , HasRelationLink (MkPrefix '[QueryFlag sym] verb)- , IsElem api api, HasLink api+ , HasTemplatedLink (MkPrefix '[QueryFlag sym] verb) , BuildLayerLinks ('Layer apiCs cs verb) m- , buildLinksFun ~ (ReplaceHandler (ServerT api m) [(String, ResourceLink)])- , PolyvariadicComp2 (MkLink api Link) buildLinksFun (IsFun buildLinksFun)- , Return2 (MkLink api Link) buildLinksFun (IsFun buildLinksFun) ~ (Link, [(String, ResourceLink)])- , Replace2 (MkLink api Link) buildLinksFun [(String, ResourceLink)] (IsFun buildLinksFun) ~ buildLinksFun+ , buildLinksFun ~ (ReplaceHandler (ServerT api m) [(String, RelationLink)])+ , PolyvariadicComp2 (MkLink api RelationLink) buildLinksFun (IsFun buildLinksFun)+ , Return2 (MkLink api RelationLink) buildLinksFun (IsFun buildLinksFun) ~ (RelationLink, [(String, RelationLink)])+ , Replace2 (MkLink api RelationLink) buildLinksFun [(String, RelationLink)] (IsFun buildLinksFun) ~ buildLinksFun ) => BuildLayerLinks ('Layer apiCs (QueryFlag sym ': cs) verb) m where- buildLayerLinks _ m = pcomp2 (\(self, ls) -> (relName, mkTemplatedNext self) : ls) mkSelf mkLinks+ buildLayerLinks _ m = pcomp2 (\(self, ls) -> (relName, self <<< child) : ls) mkSelf mkLinks where relName = symbolVal (Proxy @sym)- mkSelf = safeLink (Proxy @api) (Proxy @api)+ mkSelf = toRelationLink (Proxy @api) mkLinks = buildLayerLinks (Proxy @('Layer apiCs cs verb)) m- child = toRelationLink (Proxy @(MkPrefix '[QueryFlag sym] verb))- mkTemplatedNext = TemplateLink- . (\rl -> rl { _params = _params rl ++ _params child, _templated = True })- . fromURI (allMime $ Proxy @cts) (reflectStdMethod (Proxy @method))- . linkURI+ child = toTemplatedLink (Proxy @(MkPrefix '[QueryFlag sym] verb))
src/Servant/Hateoas/Layer/Type.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE DeriveAnyClass #-} module Servant.Hateoas.Layer.Type (@@ -105,8 +106,6 @@ -- | A response type for a 'Layer' that does not contain any data. newtype Intermediate = Intermediate () deriving newtype (Show, Eq, Ord, ToJSON)+ deriving anyclass (ToResource res) type GetIntermediate = Get '[] Intermediate--instance Resource res => ToResource res Intermediate where- toResource _ _ = wrap $ Intermediate ()
src/Servant/Hateoas/RelationLink.hs view
@@ -10,14 +10,24 @@ RelationParam(..), -- *** Creation+ fromLink, fromURI, -- *** Operations+ (<<<),+ getHref,+ getPath,+ getParams,+ prependSeg,+ prependSegs,+ addParam,+ addParams, mkPlaceHolder,- appendPath, -- ** Class+ HasTemplatedLink(..), HasRelationLink(..),+ RightLink, -- * Utility -- ** ReflectStdMethod@@ -25,59 +35,131 @@ ) where -import Prelude hiding (dropWhile, break)+import Prelude hiding (drop, dropWhile, break) import Servant import Servant.API.ContentTypes (AllMime(..)) import Servant.API.Modifiers (FoldRequired)-import Network.URI (unEscapeString)+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)+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- { _path :: Text+ { _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) --- | Create a placeholder for a URI template parameter.+-- | 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 <> "}" --- | Append a path to a URI.-appendPath :: Text -> Text -> Text-appendPath l "" = l-appendPath l r = l <> "/" <> r+-- | 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 _ _ path query _) = RelationLink- { _path = fromString path+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 -> RelationParam (fst $ break (== '=') kv) False)+ $ fmap (\kv -> let (k, drop 1 -> v) = break (== '=') kv in RelationParam k False $ if v == "" then Nothing else Just v) $ split (== '&') $ dropWhile (== '?') $ fromString@@ -92,148 +174,287 @@ unsafeMethodToStdMethod (parseMethod -> Left m) = error $ "Cannot convert " <> show m <> " to StdMethod" instance ToJSON RelationLink where- toJSON (RelationLink path params templated _ _ _ _) = String $- if not (null params) && templated- then path <> "{?" <> intercalate "," (_name <$> params) <> "}"- else path+ toJSON = String . getHref --- | Class for creating a 'RelationLink' to an API.-class HasRelationLink endpoint where- toRelationLink :: Proxy endpoint -> RelationLink+-- | Class for creating a templated 'RelationLink' to an endpoint.+class HasTemplatedLink endpoint where+ toTemplatedLink :: Proxy endpoint -> RelationLink -instance HasRelationLink b => HasRelationLink (EmptyAPI :> b) where- toRelationLink _ = toRelationLink (Proxy @b)+instance HasTemplatedLink b => HasTemplatedLink (EmptyAPI :> b) where+ toTemplatedLink _ = toTemplatedLink (Proxy @b) -instance (KnownSymbol sym, HasRelationLink b) => HasRelationLink ((sym :: Symbol) :> b) where- toRelationLink _ = let rl = toRelationLink (Proxy @b) in rl { _path = prefix `appendPath` _path rl }+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, HasRelationLink b) => HasRelationLink (Capture' mods sym a :> b) where- toRelationLink _ = let rl = toRelationLink (Proxy @b) in rl { _path = prefix `appendPath` _path rl, _templated = True }+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, HasRelationLink b) => HasRelationLink (CaptureAll sym a :> b) where- toRelationLink _ = let rl = toRelationLink (Proxy @b) in rl { _path = prefix `appendPath` _path rl, _templated = True }+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 HasRelationLink b => HasRelationLink (Header' mods sym a :> b) where- toRelationLink _ = toRelationLink (Proxy @b)+instance HasTemplatedLink b => HasTemplatedLink (Header' mods sym a :> b) where+ toTemplatedLink _ = toTemplatedLink (Proxy @b) -instance HasRelationLink b => HasRelationLink (HttpVersion :> b) where- toRelationLink _ = toRelationLink (Proxy @b)+instance HasTemplatedLink b => HasTemplatedLink (HttpVersion :> b) where+ toTemplatedLink _ = toTemplatedLink (Proxy @b) -instance (HasRelationLink b, KnownSymbol sym, SBoolI (FoldRequired mods)) => HasRelationLink (QueryParam' mods sym a :> b) where- toRelationLink _ = let rl = toRelationLink (Proxy @b) in rl { _params = param : _params rl, _templated = True }+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 (HasRelationLink b, KnownSymbol sym) => HasRelationLink (QueryParams sym a :> b) where- toRelationLink _ = let rl = toRelationLink (Proxy @b) in rl { _params = param : _params rl, _templated = True }+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 (HasRelationLink b, KnownSymbol sym) => HasRelationLink (QueryFlag sym :> b) where- toRelationLink _ = let rl = toRelationLink (Proxy @b) in rl { _params = param : _params rl, _templated = True }+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 HasRelationLink b => HasRelationLink (QueryString :> b) where- toRelationLink _ = toRelationLink (Proxy @b)+instance HasTemplatedLink b => HasTemplatedLink (QueryString :> b) where+ toTemplatedLink _ = toTemplatedLink (Proxy @b) -instance (HasRelationLink b, KnownSymbol sym) => HasRelationLink (DeepQuery sym a :> b) where- toRelationLink _ = let rl = toRelationLink (Proxy @b) in rl { _params = param : _params rl, _templated = True }+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 HasRelationLink b => HasRelationLink (Fragment a :> b) where- toRelationLink _ = toRelationLink (Proxy @b)+instance HasTemplatedLink b => HasTemplatedLink (Fragment a :> b) where+ toTemplatedLink _ = toTemplatedLink (Proxy @b) -instance HasRelationLink b => HasRelationLink (ReqBody' mods cts a :> b) where- toRelationLink _ = toRelationLink (Proxy @b)+instance HasTemplatedLink b => HasTemplatedLink (ReqBody' mods cts a :> b) where+ toTemplatedLink _ = toTemplatedLink (Proxy @b) -instance HasRelationLink b => HasRelationLink (RemoteHost :> b) where- toRelationLink _ = toRelationLink (Proxy @b)+instance HasTemplatedLink b => HasTemplatedLink (RemoteHost :> b) where+ toTemplatedLink _ = toTemplatedLink (Proxy @b) -instance HasRelationLink b => HasRelationLink (IsSecure :> b) where- toRelationLink _ = toRelationLink (Proxy @b)+instance HasTemplatedLink b => HasTemplatedLink (IsSecure :> b) where+ toTemplatedLink _ = toTemplatedLink (Proxy @b) -instance HasRelationLink b => HasRelationLink (Vault :> b) where- toRelationLink _ = toRelationLink (Proxy @b)+instance HasTemplatedLink b => HasTemplatedLink (Vault :> b) where+ toTemplatedLink _ = toTemplatedLink (Proxy @b) -instance HasRelationLink b => HasRelationLink (WithNamedContext name subs sub :> b) where- toRelationLink _ = toRelationLink (Proxy @b)+instance HasTemplatedLink b => HasTemplatedLink (WithNamedContext name subs sub :> b) where+ toTemplatedLink _ = toTemplatedLink (Proxy @b) -instance HasRelationLink b => HasRelationLink (WithResource res :> b) where- toRelationLink _ = toRelationLink (Proxy @b)+instance HasTemplatedLink b => HasTemplatedLink (WithResource res :> b) where+ toTemplatedLink _ = toTemplatedLink (Proxy @b) -instance (ReflectMethod m, AllMime cts) => HasRelationLink (Verb m s cts a) where- toRelationLink _ = RelationLink- { _path = mempty+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 => HasRelationLink (NoContentVerb m) where- toRelationLink _ = RelationLink- { _path = mempty+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- , _contentTypes = mempty+ , _title = Nothing } -instance (ReflectMethod m, AllMime cts) => HasRelationLink (UVerb m cts as) where+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- { _path = mempty+ { _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, Accept ct) => HasRelationLink (Stream m s f ct a) where+instance ReflectMethod m => HasRelationLink (NoContentVerb m) where toRelationLink _ = RelationLink- { _path = mempty+ { _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, HasRelationLink b) => HasRelationLink (Description sym :> b) where- toRelationLink _ = let rl = toRelationLink (Proxy @b) in rl { _description = Just descr }+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- descr = fromString $ symbolVal (Proxy @sym)+ 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, HasRelationLink b) => HasRelationLink (Summary sym :> b) where- toRelationLink _ = let rl = toRelationLink (Proxy @b) in rl { _summary = Just summary }+instance (KnownSymbol sym, RightLink b, ToHttpApiData a) => HasRelationLink (QueryParams sym a :> b) where+ toRelationLink _ xs = addParams params ... toRelationLink (Proxy @b) where- summary = fromString $ symbolVal (Proxy @sym)+ 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)
src/Servant/Hateoas/Resource.hs view
@@ -3,9 +3,6 @@ module Servant.Hateoas.Resource (- -- * ResourceLink- ResourceLink(..),- -- * Resource -- ** MkResource MkResource,@@ -30,27 +27,16 @@ -- | Type family computing the Resource-Type belonging to this Content-Type. type family MkResource ct :: (Type -> Type) --- | Type for Hypermedia-Links.----data ResourceLink =- CompleteLink Link -- ^ A complete 'Link' with all information.- | TemplateLink RelationLink -- ^ A 'RelationLink' that can be used as a template for URIs.- deriving (Show)--instance ToJSON ResourceLink where- toJSON (CompleteLink l) = let uri = linkURI l in toJSON $ uri { uriPath = "/" <> uriPath uri }- toJSON (TemplateLink l) = toJSON $ l { _path = "/" <> _path l }- -- | Class for resources that carry Hypermedia-Relations. class Resource res where -- | Wrap a value into a 'Resource'. wrap :: a -> res a -- | Add a hypermedia relation @(rel, link)@ to a 'Resource'.- addRel :: (String, ResourceLink) -> res a -> res a+ addRel :: (String, RelationLink) -> res a -> res a -- | Add the self-relation to a 'Resource'.-addSelfRel :: Resource res => ResourceLink -> res a -> res a+addSelfRel :: Resource res => RelationLink -> res a -> res a addSelfRel l = addRel ("self", l) -- | Class for 'Resource's that can embed other resources.@@ -69,8 +55,8 @@ -- Therefore you can derive an instance for this class. class ToResource res a where -- | Describes how a value @a@ can be converted to a 'Resource'.- toResource :: Proxy res -> a -> res a- default toResource :: Resource res => Proxy res -> a -> res a- toResource _ = wrap+ toResource :: (res ~ MkResource ct, Accept ct) => Proxy res -> Proxy ct -> a -> res a+ default toResource :: (res ~ MkResource ct, Resource res) => Proxy res -> Proxy ct -> a -> res a+ toResource _ _ = wrap instance Resource res => ToResource res [a]
src/Servant/Hateoas/ResourceServer.hs view
@@ -7,6 +7,7 @@ -- * Type-Families Resourcify,+ resourcifyProxy, ResourcifyServer ) where@@ -15,6 +16,7 @@ import Servant.Hateoas.Layer import Servant.Hateoas.Resource import Servant.Hateoas.HasHandler+import Servant.Hateoas.RelationLink import Servant.Hateoas.Internal.Polyvariadic import Data.Kind import Control.Monad.IO.Class@@ -30,6 +32,10 @@ Resourcify (x:xs) ct = Resourcify x ct : Resourcify xs ct Resourcify a _ = a +-- | A proxy function for 'Resourcify'.+resourcifyProxy :: forall api ct. Proxy api -> Proxy ct -> Proxy (Resourcify api ct)+resourcifyProxy _ _ = Proxy @(Resourcify api ct)+ -- | Turns a 'ServerT' into a resourceful 'ServerT' by replacing the result type @m a@ of the function @server@ with @m (res a)@ where -- @res := 'MkResource' ct@. --@@ -57,19 +63,20 @@ instance {-# OVERLAPPABLE #-} ( server ~ ServerT api m , ServerT (Resourcify api ct) m ~ ResourcifyServer server ct m- , mkLink ~ MkLink api Link+ , mkLink ~ MkLink (Resourcify api ct) RelationLink+ , Accept ct , res ~ MkResource ct , Resource res , ToResource res a , HasHandler api- , HasLink api, IsElem api api+ , HasRelationLink (Resourcify api ct) , PolyvariadicComp2 server mkLink (IsFun server)- , Return2 server mkLink (IsFun server) ~ (m a, Link)+ , Return2 server mkLink (IsFun server) ~ (m a, RelationLink) , Replace2 server mkLink (m (res a)) (IsFun mkLink) ~ ResourcifyServer server ct m ) => HasResourceServer (api :: Type) m ct where- getResourceServer m _ api = pcomp2 ((\(ma, self) -> (addSelfRel (CompleteLink self) . toResource (Proxy @res)) <$> ma)) (getHandler m api) mkSelf+ getResourceServer m _ api = pcomp2 ((\(ma, self) -> (addSelfRel self . toResource (Proxy @res) (Proxy @ct)) <$> ma)) (getHandler m api) mkSelf where- mkSelf = safeLink api api+ mkSelf = toRelationLink (Proxy @(Resourcify api ct)) instance ( api ~ LayerApi l@@ -77,11 +84,11 @@ , ServerT (Resourcify l ct) m ~ ResourcifyServer (ServerT l m) ct m , rServer ~ ResourcifyServer (ServerT l m) ct m , res ~ MkResource ct- , buildFun ~ ReplaceHandler rServer [(String, ResourceLink)]+ , buildFun ~ ReplaceHandler rServer [(String, RelationLink)] , Resource res , BuildLayerLinks (Resourcify l ct) m , PolyvariadicComp buildFun (IsFun buildFun)- , Return buildFun (IsFun buildFun) ~ [(String, ResourceLink)]+ , Return buildFun (IsFun buildFun) ~ [(String, RelationLink)] , Replace buildFun (m (res Intermediate)) (IsFun buildFun) ~ rServer ) => HasResourceServer l m ct where getResourceServer m _ _ = (return @m . foldr addRel (wrap @res $ Intermediate ())) ... buildLayerLinks (Proxy @(Resourcify l ct)) m