servant-hateoas 0.2.2 → 0.3.0
raw patch · 17 files changed
+992/−249 lines, 17 filesdep +singleton-booldep +textPVP ok
version bump matches the API change (PVP)
Dependencies added: singleton-bool, text
API changes (from Hackage documentation)
- Servant.Hateoas: data Collection (t :: Type)
- Servant.Hateoas: data CollectionResource a
- Servant.Hateoas.ContentType.Collection: CollectionItem :: a -> [(String, Link)] -> CollectionItem a
- Servant.Hateoas.ContentType.Collection: CollectionResource :: Maybe Link -> [CollectionItem a] -> [(String, Link)] -> CollectionResource a
- Servant.Hateoas.ContentType.Collection: [href] :: CollectionResource a -> Maybe Link
- Servant.Hateoas.ContentType.Collection: [itemLinks] :: CollectionItem a -> [(String, Link)]
- Servant.Hateoas.ContentType.Collection: [item] :: CollectionItem a -> a
- Servant.Hateoas.ContentType.Collection: [items] :: CollectionResource a -> [CollectionItem a]
- Servant.Hateoas.ContentType.Collection: [links] :: CollectionResource a -> [(String, Link)]
- Servant.Hateoas.ContentType.Collection: data Collection (t :: Type)
- Servant.Hateoas.ContentType.Collection: data CollectionItem a
- Servant.Hateoas.ContentType.Collection: data CollectionResource a
- Servant.Hateoas.ContentType.Collection: instance (Servant.Hateoas.Resource.Related a, GHC.Records.HasField (Servant.Hateoas.Resource.IdSelName a) a id, Servant.API.TypeLevel.IsElem (Servant.Hateoas.Resource.GetOneApi a) api, Servant.Links.HasLink (Servant.Hateoas.Resource.GetOneApi a), Servant.Links.MkLink (Servant.Hateoas.Resource.GetOneApi a) Servant.Links.Link GHC.Types.~ (id -> Servant.Links.Link), Servant.Hateoas.Resource.BuildRels api (Servant.Hateoas.Resource.Relations a) a, Servant.Hateoas.Resource.Resource Servant.Hateoas.ContentType.Collection.CollectionResource) => Servant.Hateoas.Resource.ToCollection api Servant.Hateoas.ContentType.Collection.CollectionResource a
- Servant.Hateoas.ContentType.Collection: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Servant.Hateoas.ContentType.Collection.CollectionItem a)
- Servant.Hateoas.ContentType.Collection: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Servant.Hateoas.ContentType.Collection.CollectionResource a)
- Servant.Hateoas.ContentType.Collection: instance Data.Aeson.Types.ToJSON.ToJSON a => Servant.API.ContentTypes.MimeRender (Servant.Hateoas.ContentType.Collection.Collection Servant.API.ContentTypes.JSON) (Servant.Hateoas.ContentType.Collection.CollectionResource a)
- Servant.Hateoas.ContentType.Collection: instance GHC.Generics.Generic (Servant.Hateoas.ContentType.Collection.CollectionItem a)
- Servant.Hateoas.ContentType.Collection: instance GHC.Generics.Generic (Servant.Hateoas.ContentType.Collection.CollectionResource a)
- Servant.Hateoas.ContentType.Collection: instance GHC.Show.Show a => GHC.Show.Show (Servant.Hateoas.ContentType.Collection.CollectionItem a)
- Servant.Hateoas.ContentType.Collection: instance GHC.Show.Show a => GHC.Show.Show (Servant.Hateoas.ContentType.Collection.CollectionResource a)
- Servant.Hateoas.ContentType.Collection: instance Servant.API.ContentTypes.Accept (Servant.Hateoas.ContentType.Collection.Collection Servant.API.ContentTypes.JSON)
- Servant.Hateoas.ContentType.Collection: instance Servant.Hateoas.Resource.CollectingResource Servant.Hateoas.ContentType.Collection.CollectionResource
- Servant.Hateoas.ContentType.Collection: instance Servant.Hateoas.Resource.Resource Servant.Hateoas.ContentType.Collection.CollectionItem
- Servant.Hateoas.ContentType.Collection: instance Servant.Hateoas.Resource.Resource Servant.Hateoas.ContentType.Collection.CollectionResource
- Servant.Hateoas.ContentType.HAL: [links] :: HALResource a -> [(String, Link)]
- Servant.Hateoas.ContentType.HAL: instance (Data.Aeson.Types.ToJSON.ToJSON a, Servant.Hateoas.Resource.Related a, GHC.TypeLits.KnownSymbol (Servant.Hateoas.Resource.CollectionName a)) => Data.Aeson.Types.ToJSON.ToJSON [Servant.Hateoas.ContentType.HAL.HALResource a]
- Servant.Hateoas.ContentType.HAL: instance (Servant.Hateoas.Resource.Related a, GHC.Records.HasField (Servant.Hateoas.Resource.IdSelName a) a id, Servant.API.TypeLevel.IsElem (Servant.Hateoas.Resource.GetOneApi a) api, Servant.Links.HasLink (Servant.Hateoas.Resource.GetOneApi a), Servant.Links.MkLink (Servant.Hateoas.Resource.GetOneApi a) Servant.Links.Link GHC.Types.~ (id -> Servant.Links.Link), Servant.Hateoas.Resource.BuildRels api (Servant.Hateoas.Resource.Relations a) a, Servant.Hateoas.Resource.Resource Servant.Hateoas.ContentType.HAL.HALResource) => Servant.Hateoas.Resource.ToResource api Servant.Hateoas.ContentType.HAL.HALResource a
- Servant.Hateoas.ContentType.HAL: instance Data.Aeson.Types.ToJSON.ToJSON a => Servant.API.ContentTypes.MimeRender (Servant.Hateoas.ContentType.HAL.HAL Servant.API.ContentTypes.JSON) (Servant.Hateoas.ContentType.HAL.HALResource a)
- Servant.Hateoas.Resource: -- | List of all relations <tt>a</tt> has
- Servant.Hateoas.Resource: HRel :: Symbol -> Symbol -> Type -> HRel
- Servant.Hateoas.Resource: [endpoint] :: HRel -> Type
- Servant.Hateoas.Resource: [relName] :: HRel -> Symbol
- Servant.Hateoas.Resource: [selName] :: HRel -> Symbol
- Servant.Hateoas.Resource: addLink :: Resource res => (String, Link) -> res a -> res a
- Servant.Hateoas.Resource: buildRels :: BuildRels api rs a => Proxy rs -> Proxy api -> a -> [(String, Link)]
- Servant.Hateoas.Resource: class BuildRels api rs a
- Servant.Hateoas.Resource: class Related a where {
- Servant.Hateoas.Resource: class ToCollection api res a
- Servant.Hateoas.Resource: data HRel
- Servant.Hateoas.Resource: defaultLinks :: forall api a id. (Related a, HasField (IdSelName a) a id, IsElem (GetOneApi a) api, HasLink (GetOneApi a), MkLink (GetOneApi a) Link ~ (id -> Link), BuildRels api (Relations a) a) => Proxy api -> a -> [(String, Link)]
- Servant.Hateoas.Resource: instance (GHC.TypeLits.KnownSymbol relName, GHC.Records.HasField selName a id, Servant.Links.HasLink endpoint, Servant.API.TypeLevel.IsElem endpoint api, Servant.Links.MkLink endpoint Servant.Links.Link GHC.Types.~ (id -> Servant.Links.Link), Servant.Hateoas.Resource.BuildRels api rs a) => Servant.Hateoas.Resource.BuildRels api ('Servant.Hateoas.Resource.HRel relName selName endpoint : rs) a
- Servant.Hateoas.Resource: instance Servant.Hateoas.Resource.BuildRels api '[] a
- Servant.Hateoas.Resource: relatedLinks :: forall api a. (Related a, BuildRels api (Relations a) a) => Proxy api -> a -> [(String, Link)]
- Servant.Hateoas.Resource: selfLink :: forall api a id. (Related a, HasField (IdSelName a) a id, IsElem (GetOneApi a) api, HasLink (GetOneApi a), MkLink (GetOneApi a) Link ~ (id -> Link)) => Proxy api -> a -> (String, Link)
- Servant.Hateoas.Resource: toCollection :: (ToCollection api res a, Foldable f) => f a -> res a
- Servant.Hateoas.Resource: toCollection' :: (ToCollection api res a, Foldable f) => Proxy api -> Proxy res -> f a -> res a
- Servant.Hateoas.Resource: toResource' :: ToResource api res a => Proxy api -> Proxy res -> a -> res a
- Servant.Hateoas.Resource: type CollectionName a = "items";
- Servant.Hateoas.Resource: type GetOneApi a :: Type;
- Servant.Hateoas.Resource: type IdSelName a :: Symbol;
- Servant.Hateoas.Resource: type Relations a :: [HRel];
- Servant.Hateoas.Resource: }
+ Servant.Hateoas.ContentType.HAL: [rels] :: HALResource a -> [(String, ResourceLink)]
+ Servant.Hateoas.ContentType.HAL: instance Data.Aeson.Types.ToJSON.ToJSON (Servant.Hateoas.ContentType.HAL.HALResource a) => Servant.API.ContentTypes.MimeRender (Servant.Hateoas.ContentType.HAL.HAL Servant.API.ContentTypes.JSON) (Servant.Hateoas.ContentType.HAL.HALResource a)
+ Servant.Hateoas.ContentType.HAL: instance Data.Aeson.Types.ToJSON.ToJSON a => Data.Aeson.Types.ToJSON.ToJSON (Servant.Hateoas.ContentType.HAL.HALResource [a])
+ Servant.Hateoas.ContentType.HAL: instance GHC.Base.Functor Servant.Hateoas.ContentType.HAL.HALResource
+ Servant.Hateoas.HasHandler: class HasHandler api
+ Servant.Hateoas.HasHandler: getHandler :: (HasHandler api, MonadIO m) => Proxy m -> Proxy api -> ServerT api m
+ Servant.Hateoas.HasHandler: instance (Servant.Hateoas.HasHandler.HasHandler a, Servant.Hateoas.HasHandler.HasHandler b) => Servant.Hateoas.HasHandler.HasHandler (a Servant.API.Alternative.:<|> b)
+ Servant.Hateoas.HasHandler: instance Servant.Hateoas.HasHandler.HasHandler Servant.API.Empty.EmptyAPI
+ Servant.Hateoas.HasHandler: instance Servant.Hateoas.HasHandler.HasHandler b => Servant.Hateoas.HasHandler.HasHandler (Servant.API.Description.Description sym Servant.API.Sub.:> b)
+ Servant.Hateoas.HasHandler: instance Servant.Hateoas.HasHandler.HasHandler b => Servant.Hateoas.HasHandler.HasHandler (Servant.API.Description.Summary sym Servant.API.Sub.:> b)
+ Servant.Hateoas.HasHandler: instance Servant.Hateoas.HasHandler.HasHandler b => Servant.Hateoas.HasHandler.HasHandler (Servant.API.Fragment.Fragment a Servant.API.Sub.:> b)
+ Servant.Hateoas.HasHandler: instance Servant.Hateoas.HasHandler.HasHandler b => Servant.Hateoas.HasHandler.HasHandler (a Servant.API.Sub.:> b)
+ Servant.Hateoas.Internal.Polyvariadic: (...) :: (PolyvariadicComp f b, IsFun f ~ b) => (Return f b -> r) -> f -> Replace f r b
+ Servant.Hateoas.Internal.Polyvariadic: class (b ~ IsFun f) => PolyvariadicComp f b where {
+ Servant.Hateoas.Internal.Polyvariadic: class (b ~ IsFun f, b ~ IsFun g) => PolyvariadicComp2 f g b where {
+ Servant.Hateoas.Internal.Polyvariadic: infixr 9 ...
+ Servant.Hateoas.Internal.Polyvariadic: instance ('GHC.Types.False GHC.Types.~ Servant.Hateoas.Internal.Polyvariadic.IsFun a) => Servant.Hateoas.Internal.Polyvariadic.PolyvariadicComp a 'GHC.Types.False
+ Servant.Hateoas.Internal.Polyvariadic: instance ('GHC.Types.False GHC.Types.~ Servant.Hateoas.Internal.Polyvariadic.IsFun a, Servant.Hateoas.Internal.Polyvariadic.IsFun b GHC.Types.~ 'GHC.Types.False) => Servant.Hateoas.Internal.Polyvariadic.PolyvariadicComp2 a b 'GHC.Types.False
+ Servant.Hateoas.Internal.Polyvariadic: instance (Servant.Hateoas.Internal.Polyvariadic.IsFun b GHC.Types.~ Servant.Hateoas.Internal.Polyvariadic.IsFun c, Servant.Hateoas.Internal.Polyvariadic.PolyvariadicComp2 b c (Servant.Hateoas.Internal.Polyvariadic.IsFun b)) => Servant.Hateoas.Internal.Polyvariadic.PolyvariadicComp2 (a -> b) (a -> c) 'GHC.Types.True
+ Servant.Hateoas.Internal.Polyvariadic: instance Servant.Hateoas.Internal.Polyvariadic.PolyvariadicComp b (Servant.Hateoas.Internal.Polyvariadic.IsFun b) => Servant.Hateoas.Internal.Polyvariadic.PolyvariadicComp (a -> b) 'GHC.Types.True
+ Servant.Hateoas.Internal.Polyvariadic: pcomp :: PolyvariadicComp f b => (Return f b -> r) -> f -> Replace f r b
+ Servant.Hateoas.Internal.Polyvariadic: pcomp2 :: PolyvariadicComp2 f g b => (Return2 f g b -> r) -> f -> g -> Replace2 f g r b
+ Servant.Hateoas.Internal.Polyvariadic: type Replace f r b :: Type;
+ Servant.Hateoas.Internal.Polyvariadic: type Replace2 f g r b :: Type;
+ Servant.Hateoas.Internal.Polyvariadic: type Return f b :: Type;
+ Servant.Hateoas.Internal.Polyvariadic: type Return2 f g b :: Type;
+ Servant.Hateoas.Internal.Polyvariadic: type family IsFun f
+ Servant.Hateoas.Internal.Polyvariadic: }
+ Servant.Hateoas.Internal.Sym: data Sym (sym :: Symbol)
+ Servant.Hateoas.Internal.Sym: instance (Servant.Hateoas.HasHandler.HasHandler api, GHC.TypeLits.KnownSymbol sym) => Servant.Hateoas.HasHandler.HasHandler (Servant.Hateoas.Internal.Sym.Sym sym Servant.API.Sub.:> api)
+ Servant.Hateoas.Internal.Sym: instance (Servant.Hateoas.RelationLink.HasRelationLink api, GHC.TypeLits.KnownSymbol sym) => Servant.Hateoas.RelationLink.HasRelationLink (Servant.Hateoas.Internal.Sym.Sym sym Servant.API.Sub.:> api)
+ Servant.Hateoas.Internal.Sym: instance (Servant.Links.HasLink api, GHC.TypeLits.KnownSymbol sym) => Servant.Links.HasLink (Servant.Hateoas.Internal.Sym.Sym sym Servant.API.Sub.:> api)
+ Servant.Hateoas.Internal.Sym: instance (Servant.Server.Internal.HasServer api context, GHC.TypeLits.KnownSymbol sym) => Servant.Server.Internal.HasServer (Servant.Hateoas.Internal.Sym.Sym sym Servant.API.Sub.:> api) context
+ Servant.Hateoas.Internal.Sym: type family Symify api
+ Servant.Hateoas.Layer: type family Normalize api
+ Servant.Hateoas.Layer.Build: buildLayerLinks :: (BuildLayerLinks l m, MonadIO m) => Proxy l -> Proxy m -> ReplaceHandler (ServerT l m) [(String, ResourceLink)]
+ Servant.Hateoas.Layer.Build: class BuildLayerLinks l m
+ Servant.Hateoas.Layer.Build: instance (Servant.Hateoas.Layer.Build.LayerLinkable apiCs cs verb m mkLink, c GHC.Types.~ Servant.Hateoas.Layer.Type.MkPrefix (apiCs Servant.Hateoas.Layer.Type.++ '[Servant.API.Capture.Capture' mods sym x]) verb, Servant.Hateoas.RelationLink.HasRelationLink c, (x -> mkLink) GHC.Types.~ Servant.Links.MkLink c Servant.Links.Link, GHC.TypeLits.KnownSymbol sym) => Servant.Hateoas.Layer.Build.BuildLayerLinks ('Servant.Hateoas.Layer.Type.Layer apiCs (Servant.API.Capture.Capture' mods sym x : cs) verb) m
+ Servant.Hateoas.Layer.Build: instance (Servant.Hateoas.Layer.Build.LayerLinkable apiCs cs verb m mkLink, c GHC.Types.~ Servant.Hateoas.Layer.Type.MkPrefix (apiCs Servant.Hateoas.Layer.Type.++ '[Servant.API.Capture.CaptureAll sym x]) verb, Servant.Hateoas.RelationLink.HasRelationLink c, (x -> mkLink) GHC.Types.~ Servant.Links.MkLink c Servant.Links.Link, GHC.TypeLits.KnownSymbol sym) => Servant.Hateoas.Layer.Build.BuildLayerLinks ('Servant.Hateoas.Layer.Type.Layer apiCs (Servant.API.Capture.CaptureAll sym x : cs) verb) m
+ Servant.Hateoas.Layer.Build: instance (Servant.Hateoas.Layer.Build.LayerLinkable apiCs cs verb m mkLink, c GHC.Types.~ Servant.Hateoas.Layer.Type.MkPrefix (apiCs Servant.Hateoas.Layer.Type.++ '[Servant.Hateoas.Internal.Sym.Sym sym]) verb, Servant.Links.HasLink c, Servant.API.TypeLevel.IsElem c c, mkLink GHC.Types.~ Servant.Links.MkLink c Servant.Links.Link, GHC.TypeLits.KnownSymbol sym) => Servant.Hateoas.Layer.Build.BuildLayerLinks ('Servant.Hateoas.Layer.Type.Layer apiCs (Servant.Hateoas.Internal.Sym.Sym sym : cs) verb) m
+ Servant.Hateoas.Layer.Build: instance (api GHC.Types.~ Servant.Hateoas.Layer.Type.MkPrefix apiCs verb, Servant.Links.HasLink api, Servant.API.TypeLevel.IsElem api api, mkSelf GHC.Types.~ Servant.Links.MkLink api Servant.Links.Link, Servant.Hateoas.Internal.Polyvariadic.PolyvariadicComp mkSelf (Servant.Hateoas.Internal.Polyvariadic.IsFun mkSelf), Servant.Hateoas.Internal.Polyvariadic.Return mkSelf (Servant.Hateoas.Internal.Polyvariadic.IsFun mkSelf) GHC.Types.~ Servant.Links.Link, Servant.Hateoas.Internal.Polyvariadic.Replace mkSelf [(GHC.Base.String, Servant.Hateoas.Resource.ResourceLink)] (Servant.Hateoas.Internal.Polyvariadic.IsFun mkSelf) GHC.Types.~ Servant.Hateoas.Layer.Build.ReplaceHandler (Servant.Server.Internal.ServerT api m) [(GHC.Base.String, Servant.Hateoas.Resource.ResourceLink)]) => Servant.Hateoas.Layer.Build.BuildLayerLinks ('Servant.Hateoas.Layer.Type.Layer apiCs '[] verb) m
+ Servant.Hateoas.Layer.Build: type LayerLinkable api cs verb m mkLink = (BuildLayerLinks ('Layer api cs verb) m, PolyvariadicComp mkLink (IsFun mkLink), ReplaceHandler (ServerT (MkPrefix api verb) m) [(String, ResourceLink)] ~ [(String, ResourceLink)], Replace mkLink [(String, ResourceLink)] (IsFun mkLink) ~ [(String, ResourceLink)], Return mkLink (IsFun mkLink) ~ Link)
+ Servant.Hateoas.Layer.Build: type family ReplaceHandler server replacement
+ Servant.Hateoas.Layer.Merge: type family WithAllChildrenOfLayerApi ls l
+ Servant.Hateoas.Layer.Type: Intermediate :: () -> Intermediate
+ Servant.Hateoas.Layer.Type: Layer :: [Type] -> [Type] -> Type -> Layer
+ Servant.Hateoas.Layer.Type: [api] :: Layer -> [Type]
+ Servant.Hateoas.Layer.Type: [relativeChildren] :: Layer -> [Type]
+ Servant.Hateoas.Layer.Type: [verb] :: Layer -> Type
+ Servant.Hateoas.Layer.Type: data Layer
+ Servant.Hateoas.Layer.Type: instance (Servant.Server.Internal.HasServer l context, Servant.Server.Internal.HasServer ls context) => Servant.Server.Internal.HasServer (l : ls) context
+ Servant.Hateoas.Layer.Type: instance Data.Aeson.Types.ToJSON.ToJSON Servant.Hateoas.Layer.Type.Intermediate
+ Servant.Hateoas.Layer.Type: instance GHC.Classes.Eq Servant.Hateoas.Layer.Type.Intermediate
+ Servant.Hateoas.Layer.Type: instance GHC.Classes.Ord Servant.Hateoas.Layer.Type.Intermediate
+ Servant.Hateoas.Layer.Type: instance GHC.Show.Show Servant.Hateoas.Layer.Type.Intermediate
+ Servant.Hateoas.Layer.Type: instance Servant.Hateoas.Resource.Resource res => Servant.Hateoas.Resource.ToResource res Servant.Hateoas.Layer.Type.Intermediate
+ Servant.Hateoas.Layer.Type: instance Servant.Server.Internal.HasServer '[] context
+ Servant.Hateoas.Layer.Type: instance Servant.Server.Internal.HasServer (Servant.Hateoas.Layer.Type.MkPrefix apiCs verb) context => Servant.Server.Internal.HasServer ('Servant.Hateoas.Layer.Type.Layer apiCs cs verb) context
+ Servant.Hateoas.Layer.Type: newtype Intermediate
+ Servant.Hateoas.Layer.Type: type (++) xs ys = AppendList xs ys
+ Servant.Hateoas.Layer.Type: type GetIntermediate = Get '[] Intermediate
+ Servant.Hateoas.Layer.Type: type family MkPrefix prefix api
+ Servant.Hateoas.RelationLink: RelationLink :: Text -> [RelationParam] -> Bool -> Text -> RelationLink
+ Servant.Hateoas.RelationLink: RelationParam :: Text -> Bool -> RelationParam
+ Servant.Hateoas.RelationLink: [_method] :: RelationLink -> Text
+ Servant.Hateoas.RelationLink: [_name] :: RelationParam -> Text
+ Servant.Hateoas.RelationLink: [_params] :: RelationLink -> [RelationParam]
+ Servant.Hateoas.RelationLink: [_path] :: RelationLink -> Text
+ Servant.Hateoas.RelationLink: [_required] :: RelationParam -> Bool
+ Servant.Hateoas.RelationLink: [_templated] :: RelationLink -> Bool
+ Servant.Hateoas.RelationLink: appendPath :: Text -> Text -> Text
+ Servant.Hateoas.RelationLink: class HasRelationLink endpoint
+ Servant.Hateoas.RelationLink: data RelationLink
+ Servant.Hateoas.RelationLink: data RelationParam
+ Servant.Hateoas.RelationLink: instance (GHC.TypeLits.KnownSymbol sym, Servant.Hateoas.RelationLink.HasRelationLink b) => Servant.Hateoas.RelationLink.HasRelationLink (Servant.API.Capture.Capture' mods sym a Servant.API.Sub.:> b)
+ Servant.Hateoas.RelationLink: instance (GHC.TypeLits.KnownSymbol sym, Servant.Hateoas.RelationLink.HasRelationLink b) => Servant.Hateoas.RelationLink.HasRelationLink (Servant.API.Capture.CaptureAll sym a Servant.API.Sub.:> b)
+ Servant.Hateoas.RelationLink: instance (GHC.TypeLits.KnownSymbol sym, Servant.Hateoas.RelationLink.HasRelationLink b) => Servant.Hateoas.RelationLink.HasRelationLink (sym Servant.API.Sub.:> b)
+ Servant.Hateoas.RelationLink: instance (Servant.Hateoas.RelationLink.HasRelationLink b, GHC.TypeLits.KnownSymbol sym) => Servant.Hateoas.RelationLink.HasRelationLink (Servant.API.QueryParam.QueryParams sym a Servant.API.Sub.:> b)
+ Servant.Hateoas.RelationLink: instance (Servant.Hateoas.RelationLink.HasRelationLink b, GHC.TypeLits.KnownSymbol sym) => Servant.Hateoas.RelationLink.HasRelationLink (Servant.API.QueryString.DeepQuery sym a Servant.API.Sub.:> b)
+ Servant.Hateoas.RelationLink: instance (Servant.Hateoas.RelationLink.HasRelationLink b, GHC.TypeLits.KnownSymbol sym, Data.Singletons.Bool.SBoolI (Servant.Hateoas.RelationLink.IsRequired mods)) => Servant.Hateoas.RelationLink.HasRelationLink (Servant.API.QueryParam.QueryParam' mods sym a Servant.API.Sub.:> b)
+ Servant.Hateoas.RelationLink: instance Data.Aeson.Types.ToJSON.ToJSON Servant.Hateoas.RelationLink.RelationLink
+ Servant.Hateoas.RelationLink: instance GHC.Classes.Eq Servant.Hateoas.RelationLink.RelationLink
+ Servant.Hateoas.RelationLink: instance GHC.Classes.Eq Servant.Hateoas.RelationLink.RelationParam
+ Servant.Hateoas.RelationLink: instance GHC.Show.Show Servant.Hateoas.RelationLink.RelationLink
+ Servant.Hateoas.RelationLink: instance GHC.Show.Show Servant.Hateoas.RelationLink.RelationParam
+ Servant.Hateoas.RelationLink: instance Servant.API.Verbs.ReflectMethod m => Servant.Hateoas.RelationLink.HasRelationLink (Servant.API.UVerb.UVerb m cts as)
+ Servant.Hateoas.RelationLink: instance Servant.Hateoas.RelationLink.HasRelationLink b => Servant.Hateoas.RelationLink.HasRelationLink (Data.Vault.Lazy.Vault Servant.API.Sub.:> b)
+ Servant.Hateoas.RelationLink: instance Servant.Hateoas.RelationLink.HasRelationLink b => Servant.Hateoas.RelationLink.HasRelationLink (Network.HTTP.Types.Version.HttpVersion Servant.API.Sub.:> b)
+ Servant.Hateoas.RelationLink: instance Servant.Hateoas.RelationLink.HasRelationLink b => Servant.Hateoas.RelationLink.HasRelationLink (Servant.API.BasicAuth.BasicAuth realm userData Servant.API.Sub.:> b)
+ Servant.Hateoas.RelationLink: instance Servant.Hateoas.RelationLink.HasRelationLink b => Servant.Hateoas.RelationLink.HasRelationLink (Servant.API.Description.Description sym Servant.API.Sub.:> b)
+ Servant.Hateoas.RelationLink: instance Servant.Hateoas.RelationLink.HasRelationLink b => Servant.Hateoas.RelationLink.HasRelationLink (Servant.API.Description.Summary sym Servant.API.Sub.:> b)
+ Servant.Hateoas.RelationLink: instance Servant.Hateoas.RelationLink.HasRelationLink b => Servant.Hateoas.RelationLink.HasRelationLink (Servant.API.Empty.EmptyAPI Servant.API.Sub.:> b)
+ Servant.Hateoas.RelationLink: instance Servant.Hateoas.RelationLink.HasRelationLink b => Servant.Hateoas.RelationLink.HasRelationLink (Servant.API.Fragment.Fragment a Servant.API.Sub.:> b)
+ Servant.Hateoas.RelationLink: instance Servant.Hateoas.RelationLink.HasRelationLink b => Servant.Hateoas.RelationLink.HasRelationLink (Servant.API.Header.Header' mods sym a Servant.API.Sub.:> b)
+ Servant.Hateoas.RelationLink: instance Servant.Hateoas.RelationLink.HasRelationLink b => Servant.Hateoas.RelationLink.HasRelationLink (Servant.API.IsSecure.IsSecure Servant.API.Sub.:> b)
+ Servant.Hateoas.RelationLink: instance Servant.Hateoas.RelationLink.HasRelationLink b => Servant.Hateoas.RelationLink.HasRelationLink (Servant.API.QueryString.QueryString Servant.API.Sub.:> b)
+ Servant.Hateoas.RelationLink: instance Servant.Hateoas.RelationLink.HasRelationLink b => Servant.Hateoas.RelationLink.HasRelationLink (Servant.API.RemoteHost.RemoteHost Servant.API.Sub.:> b)
+ Servant.Hateoas.RelationLink: instance Servant.Hateoas.RelationLink.HasRelationLink b => Servant.Hateoas.RelationLink.HasRelationLink (Servant.API.ReqBody.ReqBody' mods cts a Servant.API.Sub.:> b)
+ Servant.Hateoas.RelationLink: instance forall k b (name :: GHC.Types.Symbol) (subs :: [*]) (sub :: k). Servant.Hateoas.RelationLink.HasRelationLink b => Servant.Hateoas.RelationLink.HasRelationLink (Servant.API.WithNamedContext.WithNamedContext name subs sub Servant.API.Sub.:> b)
+ Servant.Hateoas.RelationLink: instance forall k b (res :: k). Servant.Hateoas.RelationLink.HasRelationLink b => Servant.Hateoas.RelationLink.HasRelationLink (Servant.API.WithResource.WithResource res Servant.API.Sub.:> b)
+ Servant.Hateoas.RelationLink: instance forall k1 (m :: k1) (s :: GHC.TypeNats.Nat) (cts :: [*]) a. Servant.API.Verbs.ReflectMethod m => Servant.Hateoas.RelationLink.HasRelationLink (Servant.API.Verbs.Verb m s cts a)
+ Servant.Hateoas.RelationLink: instance forall k1 (m :: k1) (s :: GHC.TypeNats.Nat) f ct a. Servant.API.Verbs.ReflectMethod m => Servant.Hateoas.RelationLink.HasRelationLink (Servant.API.Stream.Stream m s f ct a)
+ Servant.Hateoas.RelationLink: instance forall k1 (m :: k1). Servant.API.Verbs.ReflectMethod m => Servant.Hateoas.RelationLink.HasRelationLink (Servant.API.Verbs.NoContentVerb m)
+ Servant.Hateoas.RelationLink: mkPlaceHolder :: Text -> Text
+ Servant.Hateoas.RelationLink: toRelationLink :: HasRelationLink endpoint => Proxy endpoint -> RelationLink
+ Servant.Hateoas.Resource: CompleteLink :: Link -> ResourceLink
+ Servant.Hateoas.Resource: TemplateLink :: RelationLink -> ResourceLink
+ Servant.Hateoas.Resource: addRel :: Resource res => (String, ResourceLink) -> res a -> res a
+ Servant.Hateoas.Resource: addSelfRel :: Resource res => ResourceLink -> res a -> res a
+ Servant.Hateoas.Resource: data ResourceLink
+ Servant.Hateoas.Resource: instance Data.Aeson.Types.ToJSON.ToJSON Servant.Hateoas.Resource.ResourceLink
+ Servant.Hateoas.Resource: instance GHC.Show.Show Servant.Hateoas.Resource.ResourceLink
+ Servant.Hateoas.Resource: instance Servant.Hateoas.Resource.Resource res => Servant.Hateoas.Resource.ToResource res [a]
+ Servant.Hateoas.Resource: type family MkResource ct :: (Type -> Type)
+ Servant.Hateoas.Resource: wrap :: Resource res => a -> res a
+ Servant.Hateoas.ResourceServer: class HasResourceServer api m ct
+ Servant.Hateoas.ResourceServer: getResourceServer :: (HasResourceServer api m ct, MonadIO m) => Proxy m -> Proxy ct -> Proxy api -> ServerT (Resourcify api ct) m
+ Servant.Hateoas.ResourceServer: instance (Control.Monad.IO.Class.MonadIO m, Servant.Hateoas.ResourceServer.HasResourceServer ls m ct, Servant.Hateoas.ResourceServer.HasResourceServer l m ct, Servant.Hateoas.Layer.Build.BuildLayerLinks (Servant.Hateoas.ResourceServer.Resourcify l ct) m) => Servant.Hateoas.ResourceServer.HasResourceServer (l : ls) m ct
+ Servant.Hateoas.ResourceServer: instance (Servant.Hateoas.ResourceServer.HasResourceServer a m ct, Servant.Hateoas.ResourceServer.HasResourceServer b m ct) => Servant.Hateoas.ResourceServer.HasResourceServer (a Servant.API.Alternative.:<|> b) m ct
+ Servant.Hateoas.ResourceServer: instance (api GHC.Types.~ Servant.Hateoas.Layer.Type.LayerApi l, rApi GHC.Types.~ Servant.Hateoas.ResourceServer.Resourcify api ct, Servant.Server.Internal.ServerT (Servant.Hateoas.ResourceServer.Resourcify l ct) m GHC.Types.~ Servant.Hateoas.ResourceServer.ResourcifyServer (Servant.Server.Internal.ServerT l m) ct m, rServer GHC.Types.~ Servant.Hateoas.ResourceServer.ResourcifyServer (Servant.Server.Internal.ServerT l m) ct m, res GHC.Types.~ Servant.Hateoas.Resource.MkResource ct, buildFun GHC.Types.~ Servant.Hateoas.Layer.Build.ReplaceHandler rServer [(GHC.Base.String, Servant.Hateoas.Resource.ResourceLink)], Servant.Hateoas.Resource.Resource res, Servant.Hateoas.Layer.Build.BuildLayerLinks (Servant.Hateoas.ResourceServer.Resourcify l ct) m, Servant.Hateoas.Internal.Polyvariadic.PolyvariadicComp buildFun (Servant.Hateoas.Internal.Polyvariadic.IsFun buildFun), Servant.Hateoas.Internal.Polyvariadic.Return buildFun (Servant.Hateoas.Internal.Polyvariadic.IsFun buildFun) GHC.Types.~ [(GHC.Base.String, Servant.Hateoas.Resource.ResourceLink)], Servant.Hateoas.Internal.Polyvariadic.Replace buildFun (m (res Servant.Hateoas.Layer.Type.Intermediate)) (Servant.Hateoas.Internal.Polyvariadic.IsFun buildFun) GHC.Types.~ rServer) => Servant.Hateoas.ResourceServer.HasResourceServer l m ct
+ Servant.Hateoas.ResourceServer: instance (server GHC.Types.~ Servant.Server.Internal.ServerT api m, Servant.Server.Internal.ServerT (Servant.Hateoas.ResourceServer.Resourcify api ct) m GHC.Types.~ Servant.Hateoas.ResourceServer.ResourcifyServer server ct m, mkLink GHC.Types.~ Servant.Links.MkLink api Servant.Links.Link, res GHC.Types.~ Servant.Hateoas.Resource.MkResource ct, Servant.Hateoas.Resource.Resource res, Servant.Hateoas.Resource.ToResource res a, Servant.Hateoas.HasHandler.HasHandler api, Servant.Links.HasLink api, Servant.API.TypeLevel.IsElem api api, Servant.Hateoas.Internal.Polyvariadic.PolyvariadicComp2 server mkLink (Servant.Hateoas.Internal.Polyvariadic.IsFun server), Servant.Hateoas.Internal.Polyvariadic.Return2 server mkLink (Servant.Hateoas.Internal.Polyvariadic.IsFun server) GHC.Types.~ (m a, Servant.Links.Link), Servant.Hateoas.Internal.Polyvariadic.Replace2 server mkLink (m (res a)) (Servant.Hateoas.Internal.Polyvariadic.IsFun mkLink) GHC.Types.~ Servant.Hateoas.ResourceServer.ResourcifyServer server ct m) => Servant.Hateoas.ResourceServer.HasResourceServer api m ct
+ Servant.Hateoas.ResourceServer: instance Servant.Hateoas.ResourceServer.HasResourceServer '[] m ct
+ Servant.Hateoas.ResourceServer: type family ResourcifyServer server ct m
- Servant.Hateoas.ContentType.HAL: HALResource :: a -> [(String, Link)] -> [(String, SomeF HALResource ToJSON)] -> HALResource a
+ Servant.Hateoas.ContentType.HAL: HALResource :: a -> [(String, ResourceLink)] -> [(String, SomeF HALResource ToJSON)] -> HALResource a
- Servant.Hateoas.Resource: class ToResource api res a
+ Servant.Hateoas.Resource: class ToResource res a
- Servant.Hateoas.Resource: embed :: (EmbeddingResource res, ToJSON e) => (String, e) -> res a -> res a
+ Servant.Hateoas.Resource: embed :: (EmbeddingResource res, ToJSON e) => (String, res e) -> res a -> res a
- Servant.Hateoas.Resource: toResource :: ToResource api res a => a -> res a
+ Servant.Hateoas.Resource: toResource :: (ToResource res a, Resource res) => Proxy res -> a -> res a
Files
- CHANGELOG.md +11/−0
- README.md +78/−49
- servant-hateoas.cabal +25/−11
- src/Servant/Hateoas.hs +9/−3
- src/Servant/Hateoas/ContentType/Collection.hs +35/−31
- src/Servant/Hateoas/ContentType/HAL.hs +24/−32
- src/Servant/Hateoas/Example.hs +40/−15
- src/Servant/Hateoas/HasHandler.hs +32/−0
- src/Servant/Hateoas/Internal/Polyvariadic.hs +66/−0
- src/Servant/Hateoas/Internal/Sym.hs +39/−0
- src/Servant/Hateoas/Layer.hs +69/−0
- src/Servant/Hateoas/Layer/Build.hs +90/−0
- src/Servant/Hateoas/Layer/Merge.hs +38/−0
- src/Servant/Hateoas/Layer/Type.hs +112/−0
- src/Servant/Hateoas/RelationLink.hs +176/−0
- src/Servant/Hateoas/Resource.hs +50/−108
- src/Servant/Hateoas/ResourceServer.hs +98/−0
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.0 _(2024-12-24)_++### Added+- Added functionality to derive a HATEOAS Layer-Api and rewritten HATEOAS API from your API and their respective server-implementations.++### Changed+- Removed argument `api` from class `ToResource`++### Removed+- Temporarily removed support for content-type `application/vnd.collection+json`+ ## v0.2.2 _(2024-12-01)_ ### Changed
README.md view
@@ -5,77 +5,106 @@ # servant-hateoas HATEOAS support for [servant](https://hackage.haskell.org/package/servant). -## State-This is not affiliated with the official `servant` maintainers.+Infant state, highly experimental. -Currently in infant state.-Final goal is something similar to what has been proposed [here](https://www.servant.dev/extending.html#other-directions).+Find a motivating example further down this README. ## What can we do already?-Define an instance for class `ToResource api res a` where `api` is the type of your Servant-Api within which the resty representation-of your datatype `a` lives and `res` is the resource-representation to create.+- [x] Derive a layered HATEOAS-API and a server-implementation from an API, basically what has been touched on [here](https://www.servant.dev/extending.html#other-directions).+- [x] Derive a HATEOAS-API from an API by rewriting the API and its server-implementation+ - [x] Wrapping the response types of your API with Resource-Representations+ - [x] Automatically adding the self-link to every resource+ - [x] Adding custom links to resources via instances for type-class `ToResource`+- [x] Directly write a HATEOAS-API yourself -When providing some extra information with an instance for `Related a` there are stock instances which derive the links-based on the relations in your instance.+## What can we do better?+Deriving the layered HATEOAS-API from your API does not require your API to be structured in a certain way.++However, for rewriting your API we need you to specify your server-implementation as an instance of class `HasHandler` (bad name, should be `HasServer` - exists already).++This currently makes it tricky for APIs which have shared path segments, e.g. `"api" :> (UserApi :<|> AddressApi)`++Therefore we currently need an instance on each flattened endpoint of the API, e.g. for `"api :> UserApi"` and `"api :> AddressApi"`.++## What's on the horizon?+A lot. There are plenty of opportunities.+- [ ] Merging the derived HATEOAS Layer-API with the rewritten HATEOAS API.+- [ ] Automatically adding links for [servant-pagination](https://hackage.haskell.org/package/servant-pagination)+- [ ] Adding rich descriptions for Hypermedia-relations for content-types such as `application/prs.hal-forms+json`+- [ ] ...++## Media-Types+- [x] `application/hal+json`+- [ ] `application/vnd.collection+json`: Work in progrress+- [ ] `application/prs.hal-forms+json`: Soon+- [ ] Others: Maybe++Client usage with `MimeUnrender` is not yet supported.+ ## Example++Suppose we have users and addresses, where each user has an address: ```haskell data User = User { usrId :: Int, addressId :: Int, income :: Double } deriving stock (Generic, Show, Eq, Ord)- deriving anyclass ToJSON+ deriving anyclass (ToJSON) -data Address = Address { addrId :: Int, street :: String, number :: Int}+data Address = Address { addrId :: Int, street :: String, city :: String } deriving stock (Generic, Show, Eq, Ord)- deriving anyclass ToJSON+ deriving anyclass (ToJSON)+``` -type CompleteApi = AddressApi :<|> UserApi+We need to define how their resource-representation looks like:+```haskell+-- default just wrapps an address to a resource+instance ToResource res Address -type AddressApi = AddressGetOne-type AddressGetOne = "address" :> Capture "id" Int :> Get '[HAL JSON] (HALResource Address)+-- 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+ where+ mkAddrLink = safeLink (Proxy @AddressGetOne) (Proxy @AddressGetOne)+``` -type UserApi = UserGetOne :<|> UserGetAll-type UserGetOne = "user" :> Capture "id" Int :> Get '[HAL JSON] (HALResource User)-type UserGetAll = "user" :> Get '[Collection JSON] (CollectionResource User)+Further we define our API as usual:+```haskell+type Api = UserApi :<|> AddressApi -instance Related User where- type IdSelName User = "usrId"- type GetOneApi User = UserGetOne- type CollectionName User = "users"- type Relations User =- '[ 'HRel "address" "addressId" AddressGetOne- ]+type UserApi = UserGetOne :<|> UserGetAll :<|> UserGetAllCool+type UserGetOne = "api" :> "user" :> Capture "id" Int :> Get '[JSON] User+type UserGetAll = "api" :> "user" :> Get '[JSON] [User] +type AddressApi = AddressGetOne+type AddressGetOne = "api" :> "address" :> Capture "id" Int :> Get '[JSON] Address ```++Getting all the layers of the API in a HATEOAS way now is as simple as: ```haskell->>> mimeRender (Proxy @(HAL JSON)) $ toResource @CompleteApi @HALResource $ User 1 42 100000+layerServer :: Server (Resourcify (MkLayers Api) (HAL JSON))+layerServer = getResourceServer (Proxy @Handler) (Proxy @(HAL JSON)) (Proxy @(MkLayers Api)) ```-```json-{- "_links": {- "address": {- "href": "address/42"- },- "self": {- "href": "user/1"- }- },- "addressId": 42,- "income": 100000,- "usrId": 1-}++If we further want to rewrite our API to a HATEOAS-API, we need to define the server-implementation as an instance of `HasHandler`.++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+instance HasHandler UserGetAll where+ getHandler _ _ = return [User 1 1 1000, User 2 2 2000, User 42 3 3000]+instance HasHandler AddressGetOne where+ getHandler _ _ = \aId -> return $ Address aId "Foo St" "BarBaz" ``` -## Goals-- [x] Deriving simple links for self and relations-- [ ] Deriving more complex links...?-- [ ] Type-level rewriting of APIs like `CompleteAPI` to make API HATEOAS-compliant+Getting the rewritten HATEOAS-API and it's server-implementation is as simple as:+```haskell+apiServer :: Server (Resourcify Api (HAL JSON))+apiServer = getResourceServer (Proxy @Handler) (Proxy @(HAL JSON)) (Proxy @Api)+``` -## Media-Types-- [x] `application/hal+json`-- [x] `application/collection+json`-- [ ] `application/hal-forms+json` soon-- [ ] Others: Maybe+For now `apiServer` and `layerServer` exist in isolation, but the goal is to merge them into one. -Client usage with `MimeUnrender` is not yet supported.+The complete example can be found [here](https://github.com/bruderj15/servant-hateoas/blob/main/src/Servant/Hateoas/Example.hs). ## Contact information Contributions, critics and bug reports are welcome!
servant-hateoas.cabal view
@@ -1,11 +1,14 @@ cabal-version: 3.0 name: servant-hateoas-version: 0.2.2+version: 0.3.0 synopsis: HATEOAS extension for servant description: Create Resource-Representations for your types and make your API HATEOAS-compliant.- Automatically derive hypermedia-links where possible.- Currently HAL+JSON and Collection+JSON are the only supported Content-Types.- The ultimate goal is to generate an entirely HATEOAS-compliant API generically.+ Automatically derive a HATEOAS-API and server-implementation from your API or straight up define a HATEOAS-API yourself.+ Currently HAL+JSON is the only supported Content-Type. Work for further is on progress.+ For now only basic hypermedia-link derivations such as the self-link are automatically generated.+ Expect more sophisticated link-derivation e.g. for paging in the future.+ This library is highly experimental and subject to change.+ homepage: https://github.com/bruderj15/servant-hateoas bug-reports: https://github.com/bruderj15/servant-hateoas/issues license: BSD-3-Clause@@ -30,17 +33,28 @@ exposed-modules: Servant.Hateoas , Servant.Hateoas.Resource+ , Servant.Hateoas.HasHandler+ , Servant.Hateoas.RelationLink+ , Servant.Hateoas.ResourceServer+ , Servant.Hateoas.Layer+ , Servant.Hateoas.Layer.Type+ , Servant.Hateoas.Layer.Build+ , Servant.Hateoas.Layer.Merge , Servant.Hateoas.ContentType.HAL- , Servant.Hateoas.ContentType.Collection+ , Servant.Hateoas.Internal.Sym+ , Servant.Hateoas.Internal.Polyvariadic other-modules: Servant.Hateoas.Example+ , Servant.Hateoas.ContentType.Collection - build-depends: base >= 4.17.2 && < 5- , aeson >= 2.2.3 && < 2.3- , http-media >= 0.8.1 && < 0.9- , servant >= 0.20.2 && < 0.21- , servant-server >= 0.20.2 && < 0.21- , constrained-some >= 0.1.0 && < 0.2+ build-depends: base >= 4.17.2 && < 5+ , aeson >= 2.2.3 && < 2.3+ , text >= 1.2.3.0 && < 2.2+ , http-media >= 0.8.1 && < 0.9+ , servant >= 0.20.2 && < 0.21+ , servant-server >= 0.20.2 && < 0.21+ , singleton-bool >= 0.1.4 && < 0.2+ , constrained-some >= 0.1.0 && < 0.2 hs-source-dirs: src default-language: GHC2021 default-extensions: DataKinds, TypeFamilies
src/Servant/Hateoas.hs view
@@ -1,9 +1,15 @@ module Servant.Hateoas-( module Servant.Hateoas.ContentType.Collection-, module Servant.Hateoas.ContentType.HAL+( module Servant.Hateoas.ContentType.HAL+, module Servant.Hateoas.ResourceServer+, module Servant.Hateoas.RelationLink+, module Servant.Hateoas.HasHandler , module Servant.Hateoas.Resource+, module Servant.Hateoas.Layer ) where -import Servant.Hateoas.ContentType.Collection (Collection, CollectionResource) import Servant.Hateoas.ContentType.HAL (HAL, HALResource)+import Servant.Hateoas.ResourceServer+import Servant.Hateoas.RelationLink+import Servant.Hateoas.HasHandler import Servant.Hateoas.Resource+import Servant.Hateoas.Layer
src/Servant/Hateoas/ContentType/Collection.hs view
@@ -1,7 +1,4 @@ {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE UndecidableInstances #-}-{-# LANGUAGE DefaultSignatures #-}-{-# LANGUAGE ViewPatterns #-} module Servant.Hateoas.ContentType.Collection ( Collection@@ -17,9 +14,7 @@ import qualified Data.Foldable as Foldable import Data.Kind import Data.Aeson-import Data.Proxy import GHC.Exts-import GHC.Records import GHC.Generics -- | Data-Kind representing Content-Types of HATEOAS collections.@@ -27,56 +22,65 @@ -- 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 Link -- ^ Link to the collection- , items :: [CollectionItem a] -- ^ All items in the collection- , links :: [(String, Link)] -- ^ Pairs @(rel, link)@ for relations- } deriving (Show, Generic)+ { 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, Link)] -- ^ Links for the wrapped item- } deriving (Show, Generic)+ , itemLinks :: [(String, ResourceLink)] -- ^ Links for the wrapped item+ } deriving (Show, Generic, Functor) instance Resource CollectionResource where- addLink l (CollectionResource h r ls) = CollectionResource h r (l:ls)+ wrap x = CollectionResource Nothing [wrap x] []+ addRel l (CollectionResource h r ls) = CollectionResource h r (l:ls) instance Resource CollectionItem where- addLink l (CollectionItem i ls) = CollectionItem i (l:ls)+ 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 a => MimeRender (Collection JSON) (CollectionResource a) where+instance ToJSON (CollectionResource a) => MimeRender (Collection JSON) (CollectionResource a) where mimeRender _ = encode -collectionLinks :: [(String, Link)] -> Value-collectionLinks = Array . Foldable.foldl' (\xs (rel, l) -> pure (object ["name" .= rel, "value" .= linkURI l]) <> xs) mempty+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 (toJSON -> Object m) ls) = object ["data" .= itemData, "links" .= collectionLinks ls]+ 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 $ toList m- toJSON (CollectionItem (toJSON -> v) _) = v+ 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" .=) . linkURI) mHref+ 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--instance {-# OVERLAPPABLE #-}- ( Related a, HasField (IdSelName a) a id, IsElem (GetOneApi a) api- , HasLink (GetOneApi a), MkLink (GetOneApi a) Link ~ (id -> Link)- , BuildRels api (Relations a) a- , Resource CollectionResource- )- => ToCollection api CollectionResource a where- toCollection is = CollectionResource Nothing is' mempty- where- is' = Foldable.foldl' (\xs x -> CollectionItem x (defaultLinks (Proxy @api) x) : xs) mempty is
src/Servant/Hateoas/ContentType/HAL.hs view
@@ -1,9 +1,11 @@ {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE UndecidableInstances #-}-{-# LANGUAGE DefaultSignatures #-} module Servant.Hateoas.ContentType.HAL-( HAL+(+ -- * Content-Type+ HAL++ -- * Resource-Type , HALResource(..) ) where@@ -11,62 +13,52 @@ 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.Some.Constraint import Data.Kind-import Data.Proxy import Data.Aeson import Data.Aeson.KeyMap (singleton) import GHC.Exts-import GHC.TypeLits import GHC.Generics-import GHC.Records --- | Data-Kind representing Content-Types of Hypertext Application Language (HAL).+-- | 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) --- | Resource wrapper for HAL.+type instance MkResource (HAL t) = HALResource++-- | HAL-resource representation. data HALResource a = HALResource { resource :: a -- ^ Wrapped resource- , links :: [(String, Link)] -- ^ Pairs @(rel, link)@ for relations+ , rels :: [(String, ResourceLink)] -- ^ Pairs @(rel, link)@ for hypermedia relations , embedded :: [(String, SomeF HALResource ToJSON)] -- ^ Pairs @(rel, resource)@ for embedded resources- } deriving (Generic)+ } deriving (Generic, Functor) instance Resource HALResource where- addLink l (HALResource r ls es) = HALResource r (l:ls) es+ 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 a => MimeRender (HAL JSON) (HALResource a) where+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) = case toJSON res of- Object kvm -> Object $ (singleton "_links" ls') <> (singleton "_embedded" es') <> kvm- v -> v+ 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" .= linkURI href] | (rel, href) <- ls]+ ls' = object [fromString rel .= object ["href" .= href] | (rel, href) <- ls] es' = object [fromString name .= toJSON e | (name, (Some1 e)) <- es] -instance {-# OVERLAPPING #-} (ToJSON a, Related a, KnownSymbol (CollectionName a)) => ToJSON [HALResource a] where- toJSON xs = object ["_links" .= (mempty :: Object), "_embedded" .= es]+instance {-# OVERLAPPING #-} ToJSON a => ToJSON (HALResource [a]) where+ toJSON (HALResource xs ls es) = object ["_links" .= ls', "_embedded" .= object (exs <> es')] where- es = object $- [ fromString (symbolVal (Proxy @(CollectionName a)))- .= (Array $ Foldable.foldl' (\xs' x -> xs' <> pure (toJSON x)) mempty xs)- ]+ 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 (\res -> Some1 $ HALResource res [] []) e : es--instance {-# OVERLAPPABLE #-}- ( Related a, HasField (IdSelName a) a id, IsElem (GetOneApi a) api- , HasLink (GetOneApi a), MkLink (GetOneApi a) Link ~ (id -> Link)- , BuildRels api (Relations a) a- , Resource HALResource- ) => ToResource api HALResource a where- toResource x = HALResource x (defaultLinks (Proxy @api) x) mempty+ embed e (HALResource r ls es) = HALResource r ls $ fmap Some1 e : es
src/Servant/Hateoas/Example.hs view
@@ -10,25 +10,50 @@ data User = User { usrId :: Int, addressId :: Int, income :: Double } deriving stock (Generic, Show, Eq, Ord)- deriving anyclass ToJSON+ deriving anyclass (ToJSON) -data Address = Address { addrId :: Int, street :: String, number :: Int}+data Address = Address { addrId :: Int, street :: String, city :: String } deriving stock (Generic, Show, Eq, Ord)- deriving anyclass ToJSON+ deriving anyclass (ToJSON, ToResource res) -type CompleteApi = AddressApi :<|> UserApi+instance Resource res => ToResource res User where+ toResource _ usr = addRel ("address", CompleteLink $ mkAddrLink $ addressId usr) $ wrap usr+ where+ mkAddrLink = safeLink (Proxy @AddressGetOne) (Proxy @AddressGetOne) +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 UserGetAllCool = "api" :> "user" :> "cool-guys" :> Get '[JSON] [User]+ type AddressApi = AddressGetOne-type AddressGetOne = "address" :> Capture "id" Int :> Get '[HAL JSON] (HALResource Address)+type AddressGetOne = "api" :> "address" :> Capture "id" Int :> Get '[JSON] Address -type UserApi = UserGetOne :<|> UserGetAll-type UserGetOne = "user" :> Capture "id" Int :> Get '[HAL JSON] (HALResource User)-type UserGetAll = "user" :> Get '[Collection JSON] (CollectionResource User)+instance HasHandler UserGetOne where+ getHandler _ _ = \uId -> return $ User uId 1 1000 -instance Related User where- type IdSelName User = "usrId"- type GetOneApi User = UserGetOne- type CollectionName User = "users"- type Relations User =- '[ 'HRel "address" "addressId" AddressGetOne- ]+instance HasHandler UserGetAll where+ getHandler _ _ = return [User 1 1 1000, User 2 2 2000, User 42 3 3000]++instance HasHandler UserGetAllCool where+ getHandler _ _ = return [User 42 3 3000]++instance HasHandler AddressGetOne where+ getHandler _ _ = \aId -> return $ Address aId "Foo St" "BarBaz"++userApiServer :: Server UserApi+userApiServer = getHandler (Proxy @Handler) (Proxy @UserApi)++layerServer :: Server (Resourcify (MkLayers Api) (HAL JSON))+layerServer = getResourceServer (Proxy @Handler) (Proxy @(HAL JSON)) (Proxy @(MkLayers Api))++layerApp :: Application+layerApp = serve (Proxy @((Resourcify (MkLayers Api)) (HAL JSON))) layerServer++apiServer :: Server (Resourcify Api (HAL JSON))+apiServer = getResourceServer (Proxy @Handler) (Proxy @(HAL JSON)) (Proxy @Api)++apiApp :: Application+apiApp = serve (Proxy @((Resourcify Api) (HAL JSON))) apiServer
+ src/Servant/Hateoas/HasHandler.hs view
@@ -0,0 +1,32 @@+module Servant.Hateoas.HasHandler+(+ -- * Class+ HasHandler(..)+)+where++import Servant+import Control.Monad.IO.Class+import GHC.TypeLits++-- | Typeclass for extracting the handler of an API.+class HasHandler api where+ getHandler :: MonadIO m => Proxy m -> Proxy api -> ServerT api m++instance {-# OVERLAPPABLE #-} (HasHandler a, HasHandler b) => HasHandler (a :<|> b) where+ getHandler m _ = getHandler m (Proxy @a) :<|> getHandler m (Proxy @b)++instance {-# OVERLAPPABLE #-} HasHandler b => HasHandler ((a :: Symbol) :> b) where+ getHandler m _ = getHandler m (Proxy @b)++instance {-# OVERLAPPABLE #-} HasHandler b => HasHandler (Description sym :> b) where+ getHandler m _ = getHandler m (Proxy @b)++instance {-# OVERLAPPABLE #-} HasHandler b => HasHandler (Summary sym :> b) where+ getHandler m _ = getHandler m (Proxy @b)++instance {-# OVERLAPPABLE #-} HasHandler b => HasHandler (Fragment a :> b) where+ getHandler m _ = getHandler m (Proxy @b)++instance HasHandler EmptyAPI where+ getHandler _ _ = emptyServer
+ src/Servant/Hateoas/Internal/Polyvariadic.hs view
@@ -0,0 +1,66 @@+{-# LANGUAGE UndecidableInstances #-}++module Servant.Hateoas.Internal.Polyvariadic+(+ -- * IsFunction+ IsFun,++ -- * Simple Polyvariadic composition+ PolyvariadicComp(..),+ (...),++ -- * Polyvariadic composition with two functions+ PolyvariadicComp2(..),+)+where++import Data.Kind++-- | Type-level function to determine if a 'Type' is a function.+type family IsFun f where+ IsFun (_ -> _) = 'True+ IsFun _ = 'False++-- | Class for polyvariadic composition.+--+-- This is copied from the package @erisco/control-dotdotdot@.+class (b ~ IsFun f) => PolyvariadicComp f b where+ type Return f b :: Type+ type Replace f r b :: Type+ -- | @pcomp f g@ has @g@ consume all arguments and then has @f@ consume the result of @g@.+ pcomp :: (Return f b -> r) -> f -> Replace f r b++instance (False ~ IsFun a) => PolyvariadicComp a 'False where+ type Return a 'False = a+ type Replace a r 'False = r+ pcomp f a = f a++instance (PolyvariadicComp b (IsFun b)) => PolyvariadicComp (a -> b) 'True where+ type Return (a -> b) 'True = Return b (IsFun b)+ type Replace (a -> b) r 'True = a -> Replace b r (IsFun b)+ pcomp final f = \x -> final `pcomp` f x++-- | Infix for 'pcomp'.+(...) :: (PolyvariadicComp f b, IsFun f ~ b) => (Return f b -> r) -> f -> Replace f r b+(...) = pcomp+infixr 9 ...++-- | Like 'PolyvariadicComp' but allows to consume all arguments twice,+-- by two functions with the exact same arguments but potentially different return types.+class (b ~ IsFun f, b ~ IsFun g) => PolyvariadicComp2 f g b where+ type Return2 f g b :: Type+ type Replace2 f g r b :: Type+ -- | @pcomp2 f g h@ has each @g@ and @h@ consume all arguments and then has @f@ consume the result of @g@ and @h@.+ --+ -- This is highly similar to '(&&&)' from 'Control.Arrow' but for polyvariadic composition.+ pcomp2 :: (Return2 f g b -> r) -> f -> g -> Replace2 f g r b++instance (False ~ IsFun a, IsFun b ~ False) => PolyvariadicComp2 a b 'False where+ type Return2 a b 'False = (a, b)+ type Replace2 a b r 'False = r+ pcomp2 f a b = f (a, b)++instance (IsFun b ~ IsFun c, PolyvariadicComp2 b c (IsFun b)) => PolyvariadicComp2 (a -> b) (a -> c) 'True where+ type Return2 (a -> b) (a -> c) 'True = Return2 b c (IsFun b)+ type Replace2 (a -> b) (a -> c) r 'True = a -> Replace2 b c r (IsFun b)+ pcomp2 final f g = \x -> pcomp2 final (f x) (g x)
+ src/Servant/Hateoas/Internal/Sym.hs view
@@ -0,0 +1,39 @@+module Servant.Hateoas.Internal.Sym+(+ -- * Type+ Sym,++ -- * Type family+ Symify+)+where++import Servant+import Servant.Hateoas.HasHandler+import Servant.Hateoas.RelationLink+import GHC.TypeLits++-- | A wrapper for path segments of kind 'Symbol'.+data Sym (sym :: Symbol)++instance (HasServer api context, KnownSymbol sym) => HasServer (Sym sym :> api) context where+ type ServerT (Sym sym :> api) m = ServerT (sym :> api) m+ route _ = route (Proxy @(sym :> api))+ hoistServerWithContext _ = hoistServerWithContext (Proxy @(sym :> api))++instance (HasLink api, KnownSymbol sym) => HasLink (Sym sym :> api) where+ 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+ toRelationLink _ = toRelationLink (Proxy @(sym :> api))++instance (HasHandler api, KnownSymbol sym) => HasHandler (Sym sym :> api) where+ getHandler m _ = getHandler m (Proxy @(sym :> api))++-- | A type family that wraps all path segments of kind 'Symbol' in an API with 'Sym'.+type family Symify api where+ Symify (a :<|> b) = Symify a :<|> Symify b+ Symify ((sym :: Symbol) :> b) = Sym sym :> Symify b+ Symify (a :> b) = a :> Symify b+ Symify a = a
+ src/Servant/Hateoas/Layer.hs view
@@ -0,0 +1,69 @@+{-# LANGUAGE UndecidableInstances #-}++module Servant.Hateoas.Layer+(+ -- * Create+ MkLayers,++ -- * Type+ module Servant.Hateoas.Layer.Type,++ -- * Build+ module Servant.Hateoas.Layer.Build,++ -- * Merge+ module Servant.Hateoas.Layer.Merge,++ -- * Utilities+ GoLayers,+ Normalize,+)+where++import Servant+import Servant.Hateoas.Layer.Type+import Servant.Hateoas.Layer.Merge+import Servant.Hateoas.Layer.Build+import Servant.Hateoas.Internal.Sym+import Data.Kind++-- | Given an API, create a list of 'Layer's that represents the HATEOAS structure of the API.+--+-- This is done by:+--+-- 1. Rewriting every @(sym :: Symbol) :> b@ into @Sym sym :> b@ so all subsets of the API have kind 'Type', see 'Symify'.+--+-- 2. Normalizing the API to make every node-choice unambiguous, see 'Normalize'.+--+-- 3. Creating all intermediate layers of the API and their immediate next layers, see 'GoLayers'.+--+-- 4. Merging the immediate next layers of all layers that share the same API, see 'MergeLayers'.+--+type MkLayers :: p -> [Layer]+type family MkLayers api where+ MkLayers api = MergeLayers (GoLayers (Normalize (Symify api)) '[]) '[]++-- | Rewrite the API pulling out every shared prefix between all branches of the API.+--+-- This results in a tree-structure where every path from the root of the API to every node or leaf is unique.+type family Normalize api where+ Normalize ((prefix :> a) :<|> (prefix :> b)) = Normalize (prefix :> (Normalize a :<|> Normalize b))+ Normalize (a :<|> b) = Normalize a :<|> Normalize b -- | TODO: Here we also need to apply 'Normalize' again! How?+ Normalize ((prefix :> a) :> (prefix :> b)) = Normalize (prefix :> (Normalize a :> Normalize b))+ Normalize (a :> b) = a :> Normalize b+ Normalize a = a++-- | Creates all intermediate layers of the API and their immediate next layers.+--+-- This is done by traversing the API-Tree where in @GoLayers api stand@, @api@ is the API seen below from @stand@.+--+-- Argument @stand@ therefore also represents an API, but due to the right-associative nature of ':>' we model it as a list here,+-- which can be turned into an API by folding it with ':>', see 'MkPrefix'.+type GoLayers :: p -> [Type] -> [Layer]+type family GoLayers api stand where+ GoLayers (a :<|> b) prefix = GoLayers a prefix ++ GoLayers b prefix+ GoLayers (Sym a :> b) prefix = '[ 'Layer prefix '[Sym a] GetIntermediate ] ++ GoLayers b (prefix ++ '[Sym a])+ GoLayers (Capture' mods sym a :> b) prefix = '[ 'Layer prefix '[Capture' mods sym a] GetIntermediate ] ++ GoLayers b (prefix ++ '[Capture' mods sym a])+ GoLayers (CaptureAll sym a :> b) prefix = '[ 'Layer prefix '[CaptureAll sym a] GetIntermediate ] ++ GoLayers b (prefix ++ '[CaptureAll sym a])+ GoLayers (a :> b) prefix = GoLayers b (prefix ++ '[a])+ GoLayers _ _ = '[]
+ src/Servant/Hateoas/Layer/Build.hs view
@@ -0,0 +1,90 @@+{-# LANGUAGE UndecidableInstances #-}++module Servant.Hateoas.Layer.Build+(+ -- * Type family+ ReplaceHandler,++ -- * Class+ BuildLayerLinks(..),++ -- * Utility Constraint+ LayerLinkable+)+where++import Servant+import Servant.Hateoas.Resource+import Servant.Hateoas.RelationLink+import Servant.Hateoas.Layer.Type+import Servant.Hateoas.Internal.Sym+import Servant.Hateoas.Internal.Polyvariadic+import Data.Kind+import Control.Monad.IO.Class+import GHC.TypeLits++-- | Replace the result 'Type' of a function with a new 'Type'.+type family ReplaceHandler server replacement where+ ReplaceHandler (a :<|> b) replacement = ReplaceHandler a replacement :<|> ReplaceHandler b replacement+ ReplaceHandler (a -> b) replacement = a -> ReplaceHandler b replacement+ ReplaceHandler _ replacement = replacement++-- | Create all 'ResourceLink'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)]++instance+ ( api ~ MkPrefix apiCs verb+ , HasLink api, IsElem api api+ , mkSelf ~ MkLink api Link+ , PolyvariadicComp mkSelf (IsFun mkSelf)+ , Return mkSelf (IsFun mkSelf) ~ Link+ , Replace mkSelf [(String, ResourceLink)] (IsFun mkSelf) ~ ReplaceHandler (ServerT api m) [(String, ResourceLink)]+ ) => BuildLayerLinks ('Layer apiCs '[] verb) m where+ buildLayerLinks _ _ = (pure @[] . ("self", ) . CompleteLink) ... mkSelf+ where+ mkSelf = safeLink (Proxy @api) (Proxy @api)++-- | Convenience alias 'Constraint' for 'Layer's that can be linked.+type LayerLinkable api cs verb m mkLink =+ ( BuildLayerLinks ('Layer api cs verb) m+ , PolyvariadicComp mkLink (IsFun mkLink)+ , ReplaceHandler (ServerT (MkPrefix api verb) m) [(String, ResourceLink)] ~ [(String, ResourceLink)]+ , Replace mkLink [(String, ResourceLink)] (IsFun mkLink) ~ [(String, ResourceLink)]+ , Return mkLink (IsFun mkLink) ~ Link+ )++instance+ ( LayerLinkable apiCs cs verb m mkLink+ , c ~ MkPrefix (apiCs ++ '[Sym sym]) verb+ , HasLink c, IsElem c c+ , mkLink ~ MkLink c Link+ , KnownSymbol sym+ ) => BuildLayerLinks ('Layer apiCs (Sym sym ': cs) verb) m where+ buildLayerLinks _ m = ((: ls) . (symbolVal (Proxy @sym),) . CompleteLink) ... mkLink+ where+ mkLink = safeLink (Proxy @c) (Proxy @c)+ ls = buildLayerLinks (Proxy @('Layer apiCs cs verb)) m++instance+ ( LayerLinkable apiCs cs verb m mkLink+ , c ~ MkPrefix (apiCs ++ '[Capture' mods sym x]) verb+ , HasRelationLink c+ , (x -> mkLink) ~ MkLink c Link+ , KnownSymbol sym+ ) => BuildLayerLinks ('Layer apiCs (Capture' mods sym x ': cs) verb) m where+ buildLayerLinks _ m = ((: ls) . (symbolVal (Proxy @sym),) . TemplateLink) ... toRelationLink (Proxy @c)+ where+ ls = buildLayerLinks (Proxy @('Layer apiCs cs verb)) m++instance+ ( LayerLinkable apiCs cs verb m mkLink+ , c ~ MkPrefix (apiCs ++ '[CaptureAll sym x]) verb+ , HasRelationLink c+ , (x -> mkLink) ~ MkLink c Link+ , KnownSymbol sym+ ) => BuildLayerLinks ('Layer apiCs (CaptureAll sym x ': cs) verb) m where+ buildLayerLinks _ m = ((: ls) . (symbolVal (Proxy @sym),) . TemplateLink) ... toRelationLink (Proxy @c)+ where+ ls = buildLayerLinks (Proxy @('Layer apiCs cs verb)) m
+ src/Servant/Hateoas/Layer/Merge.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE UndecidableInstances #-}++module Servant.Hateoas.Layer.Merge+(+ -- * Merge+ MergeLayers,++ -- ** Utilities+ ContainsLayerApi,+ WithAllChildrenOfLayerApi+)+where++import Servant.Hateoas.Layer.Type+import Data.Type.Bool++-- | Given two lists of 'Layer's, merges them into one list by combining all 'RelativeChildren' of 'Layer's with the exact same API.+type MergeLayers :: [Layer] -> [Layer] -> [Layer]+type family MergeLayers ls acc where+ MergeLayers '[] acc = acc+ MergeLayers (l ': ls) acc = MergeLayers ls (+ If (ContainsLayerApi acc (LayerApiCs l))+ (WithAllChildrenOfLayerApi acc l)+ (l ': acc)+ )++-- | Returns 'True' iff the given list of 'Layer's contains a 'Layer' with the given API.+type family ContainsLayerApi ls api where+ ContainsLayerApi '[] _ = 'False+ ContainsLayerApi ('Layer api _ _ ': ls) api = 'True+ ContainsLayerApi (_ ': ls) api = ContainsLayerApi ls api++-- | If a list of 'Layer's contains a 'Layer' with the API of the given 'Layer',+-- merge the first matching 'Layer' with the given 'Layer' by combining their 'RelativeChildren'.+type family WithAllChildrenOfLayerApi ls l where+ WithAllChildrenOfLayerApi '[] _ = '[]+ WithAllChildrenOfLayerApi (('Layer api cs verb) ': ls) ('Layer api cs' verb) = 'Layer api (cs ++ cs') verb ': ls+ WithAllChildrenOfLayerApi (l ': ls) l' = l ': WithAllChildrenOfLayerApi ls l'
+ src/Servant/Hateoas/Layer/Type.hs view
@@ -0,0 +1,112 @@+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE UndecidableInstances #-}++module Servant.Hateoas.Layer.Type+(+ -- * Type+ Layer(..),++ -- ** Getter+ LayerApiCs, RelativeChildren, LayerVerb,++ -- * API-construction+ LayerApi, MkPrefix, type (++),++ -- * Intermediate+ Intermediate(..), GetIntermediate+)+where++import Servant+import Servant.Server.Internal.Router+import Servant.Hateoas.Internal.Sym+import Servant.Hateoas.Resource+import Data.Aeson+import Data.Kind++-- | Convenience alias for 'AppendList'.+type (++) xs ys = AppendList xs ys++-- | Data-kind for a layer in an API.+--+-- ==== __Example__+--+-- @+-- ''Layer' '['Sym' \"api\", 'Sym' \"user\"] '['Capture' \"id\" 'Int', 'Sym' \"vip\"] 'GetIntermediate'+-- @+--+-- Represents the API+--+-- @+-- \"api\" :> \"user\" :> 'GetIntermediate'+-- @ with children+--+-- @+-- \"api\" :> \"user\" :> 'Capture' \"id\" 'Int' :> 'GetIntermediate'+-- @ and+--+-- @+-- \"api\" :> \"user\" :> \"vip\" :> 'GetIntermediate'+-- @+data Layer = Layer+ { api :: [Type] -- ^ The API of this layer represented as list. Folding it with ':>' results in the actual API, see 'MkPrefix'.+ , relativeChildren :: [Type] -- ^ All immediate children of this layer.+ , verb :: Type -- ^ The 'Verb' for this layer.+ }++-- | Type-level getter for the API of a 'Layer'.+type family LayerApiCs (a :: Layer) where+ LayerApiCs ('Layer api _ _) = api++-- | Type-level getter for the children of a 'Layer'.+type family RelativeChildren (a :: Layer) where+ RelativeChildren ('Layer _ children _) = children++-- | Type-level getter for the verb of a 'Layer'.+type family LayerVerb (a :: Layer) where+ LayerVerb ('Layer _ _ verb) = verb++-- | Constructs the actual API of a 'Layer'.+type family LayerApi (a :: Layer) where+ LayerApi ('Layer api _ verb) = MkPrefix api verb++-- | Folds a list of path segments into an API by intercalating '(:>)'.+--+-- ==== __Example__+--+-- @+-- 'MkPrefix' '['Sym' \"api\", 'Sym' \"user\"] 'GetIntermediate'+-- @ resolves to+--+-- @+-- 'Sym' \"api\" :> 'Sym' \"user\" :> 'GetIntermediate'+-- @+type MkPrefix :: [Type] -> Type -> Type+type family MkPrefix prefix api where+ MkPrefix (Sym x ': xs) api = x :> MkPrefix xs api+ MkPrefix (x ': xs) api = x :> MkPrefix xs api+ MkPrefix '[] api = api++instance HasServer (MkPrefix apiCs verb) context => HasServer ('Layer apiCs cs verb) context where+ type ServerT ('Layer apiCs cs verb) m = ServerT (MkPrefix apiCs verb) m+ route _ = route (Proxy @(MkPrefix apiCs verb))+ hoistServerWithContext _ = hoistServerWithContext (Proxy @(MkPrefix apiCs verb))++instance HasServer ('[] :: [Layer]) context where+ type ServerT '[] m = ServerT EmptyAPI m+ route _ = route (Proxy @EmptyAPI)+ hoistServerWithContext _ = hoistServerWithContext (Proxy @EmptyAPI)++instance (HasServer l context, HasServer ls context) => HasServer (l ': ls :: [Layer]) context where+ type ServerT (l ': ls) m = ServerT l m :<|> ServerT ls m+ route _ ctx delayed = route (Proxy @l) ctx ((\(sl :<|> _) -> sl) <$> delayed) `choice` route (Proxy @ls) ctx ((\(_ :<|> sls) -> sls) <$> delayed)+ hoistServerWithContext _ ctx f (sl :<|> sls) = hoistServerWithContext (Proxy @l) ctx f sl :<|> hoistServerWithContext (Proxy @ls) ctx f sls++-- | A response type for a 'Layer' that does not contain any data.+newtype Intermediate = Intermediate ()+ deriving newtype (Show, Eq, Ord, ToJSON)++type GetIntermediate = Get '[] Intermediate++instance Resource res => ToResource res Intermediate where+ toResource _ _ = wrap $ Intermediate ()
+ src/Servant/Hateoas/RelationLink.hs view
@@ -0,0 +1,176 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE UndecidableInstances #-}++module Servant.Hateoas.RelationLink+(+ -- * Type+ RelationLink(..),+ RelationParam(..),+ mkPlaceHolder,+ appendPath,++ -- * Class+ HasRelationLink(..)+)+where++import Servant+import GHC.TypeLits+import Data.String (fromString)+import Data.Kind+import Data.Aeson+import Data.Text (Text, intercalate)+import Data.Text.Encoding+import Data.Singletons.Bool++-- | Link data-type for hypermedia-links in HATEOAS with potentially templated URIs.+data RelationLink = RelationLink+ { _path :: Text+ , _params :: [RelationParam]+ , _templated :: Bool+ , _method :: Text+ } deriving (Show, Eq)++-- | Parameter data-type for hypermedia-links in HATEOAS.+data RelationParam = RelationParam+ { _name :: Text+ , _required :: Bool+ } deriving (Show, Eq)++-- | Create a placeholder for a URI template parameter.+mkPlaceHolder :: Text -> Text+mkPlaceHolder s = "{" <> s <> "}"++-- | Append a path to a URI.+appendPath :: Text -> Text -> Text+appendPath l "" = l+appendPath l r = l <> "/" <> r++instance ToJSON RelationLink where+ toJSON (RelationLink path params templated _) = String $+ if templated+ then path <> "{?" <> intercalate "," (_name <$> params) <> "}"+ else path++-- | Class for creating a 'RelationLink' to an API.+class HasRelationLink endpoint where+ toRelationLink :: Proxy endpoint -> RelationLink++instance HasRelationLink b => HasRelationLink (EmptyAPI :> b) where+ toRelationLink _ = toRelationLink (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 }+ 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 }+ 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 }+ where+ prefix = mkPlaceHolder $ fromString $ symbolVal (Proxy @sym)++instance HasRelationLink b => HasRelationLink (Header' mods sym a :> b) where+ toRelationLink _ = toRelationLink (Proxy @b)++instance HasRelationLink b => HasRelationLink (HttpVersion :> b) where+ toRelationLink _ = toRelationLink (Proxy @b)++type family IsRequired (mods :: [Type]) :: Bool where+ IsRequired (Required ': mods) = 'True+ IsRequired (Optional ': mods) = 'False+ IsRequired (mod ': mods) = IsRequired mods+ IsRequired '[] = 'False++instance (HasRelationLink b, KnownSymbol sym, SBoolI (IsRequired mods)) => HasRelationLink (QueryParam' mods sym a :> b) where+ toRelationLink _ = let rl = toRelationLink (Proxy @b) in rl { _params = param : _params rl, _templated = True }+ where+ param = RelationParam+ { _name = fromString $ symbolVal (Proxy @sym)+ , _required = fromSBool $ sbool @(IsRequired mods)+ }++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 }+ where+ param = RelationParam+ { _name = fromString $ symbolVal (Proxy @sym)+ , _required = False+ }++instance HasRelationLink b => HasRelationLink (QueryString :> b) where+ toRelationLink _ = toRelationLink (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 }+ where+ param = RelationParam+ { _name = fromString $ symbolVal (Proxy @sym)+ , _required = False+ }++instance HasRelationLink b => HasRelationLink (Fragment a :> b) where+ toRelationLink _ = toRelationLink (Proxy @b)++instance HasRelationLink b => HasRelationLink (ReqBody' mods cts a :> b) where+ toRelationLink _ = toRelationLink (Proxy @b)++instance HasRelationLink b => HasRelationLink (RemoteHost :> b) where+ toRelationLink _ = toRelationLink (Proxy @b)++instance HasRelationLink b => HasRelationLink (IsSecure :> b) where+ toRelationLink _ = toRelationLink (Proxy @b)++instance HasRelationLink b => HasRelationLink (Vault :> b) where+ toRelationLink _ = toRelationLink (Proxy @b)++instance HasRelationLink b => HasRelationLink (WithNamedContext name subs sub :> b) where+ toRelationLink _ = toRelationLink (Proxy @b)++instance HasRelationLink b => HasRelationLink (WithResource res :> b) where+ toRelationLink _ = toRelationLink (Proxy @b)++instance ReflectMethod m => HasRelationLink (Verb m s cts a) where+ toRelationLink _ = RelationLink+ { _path = ""+ , _params = []+ , _templated = False+ , _method = decodeUtf8 $ reflectMethod (Proxy @m)+ }++instance ReflectMethod m => HasRelationLink (NoContentVerb m) where+ toRelationLink _ = RelationLink+ { _path = ""+ , _params = []+ , _templated = False+ , _method = decodeUtf8 $ reflectMethod (Proxy @m)+ }++instance ReflectMethod m => HasRelationLink (UVerb m cts as) where+ toRelationLink _ = RelationLink+ { _path = ""+ , _params = []+ , _templated = False+ , _method = decodeUtf8 $ reflectMethod (Proxy @m)+ }++instance ReflectMethod m => HasRelationLink (Stream m s f ct a) where+ toRelationLink _ = RelationLink+ { _path = ""+ , _params = []+ , _templated = False+ , _method = decodeUtf8 $ reflectMethod (Proxy @m)+ }++instance HasRelationLink b => HasRelationLink (BasicAuth realm userData :> b) where+ toRelationLink _ = toRelationLink (Proxy @b)++instance HasRelationLink b => HasRelationLink (Description sym :> b) where+ toRelationLink _ = toRelationLink (Proxy @b)++instance HasRelationLink b => HasRelationLink (Summary sym :> b) where+ toRelationLink _ = toRelationLink (Proxy @b)
src/Servant/Hateoas/Resource.hs view
@@ -1,44 +1,62 @@-{-# LANGUAGE UndecidableInstances #-}-{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE DefaultSignatures #-}+{-# LANGUAGE OverloadedStrings #-} module Servant.Hateoas.Resource (- -- * Resource- -- ** Construction- ToResource(..)-, ToCollection(..)+ -- * ResourceLink+ ResourceLink(..), - -- ** Modification-, Resource(..), EmbeddingResource(..), CollectingResource(..)+ -- * Resource+ -- ** MkResource+ MkResource, --- * Hypermedia-Relations--- ** Type-, HRel(..)+ -- ** Base Class+ Resource(..),+ addSelfRel, --- ** Class-, Related(..)+ -- ** Specialized classes+ EmbeddingResource(..),+ CollectingResource(..), --- ** Construction-, BuildRels(..)-, selfLink, relatedLinks, defaultLinks-)-where+ -- * Creation+ ToResource(..)+) where import Servant+import Servant.Hateoas.RelationLink import Data.Kind import Data.Aeson-import GHC.TypeLits-import GHC.Records +-- | 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- -- | Add a relation @(rel, link)@ to a resource.- addLink :: (String, Link) -> res a -> res a+ -- | 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++-- | Add the self-relation to a 'Resource'.+addSelfRel :: Resource res => ResourceLink -> res a -> res a+addSelfRel l = addRel ("self", l)+ -- | Class for 'Resource's that can embed other resources. class Resource res => EmbeddingResource res where -- | Embed a resource @b@ with its relation @rel@ as tuple @(rel, b)@.- embed :: ToJSON e => (String, e) -> res a -> res a+ embed :: ToJSON e => (String, res e) -> res a -> res a -- | Class for 'Resource's that can collect multiple resources. class Resource res => CollectingResource res where@@ -46,89 +64,13 @@ collect :: a -> res a -> res a -- | Class for converting values of @a@ to their respective Resource-Representation.-class ToResource api res a where- -- | Converts a value into it's Resource-Representation.- toResource :: a -> res a- toResource = toResource' (Proxy @api) (Proxy @res)-- -- | Like 'toResource' but takes proxies for ambiguity.- toResource' :: Proxy api -> Proxy res -> a -> res a- toResource' _ _ = toResource @api @res- {-# MINIMAL toResource | toResource' #-}---- | Class for converting multiple values of @a@ to their respective collection-like representation.-class ToCollection api res a where- -- | Converts many values into their Collection-Representation.- toCollection :: Foldable f => f a -> res a- toCollection = toCollection' (Proxy @api) (Proxy @res)-- -- | Like 'toCollection' but takes proxies for ambiguity.- toCollection' :: Foldable f => Proxy api -> Proxy res -> f a -> res a- toCollection' _ _ = toCollection @api @res- {-# MINIMAL toCollection | toCollection' #-}---- | Data-Kind for Hypermedia-Relations.-data HRel = HRel- { relName :: Symbol -- ^ Name of the relation- , selName :: Symbol -- ^ Record selectors field name- , endpoint :: Type -- ^ Servant-Endpoint to use for retrieving one value for relation @relName@- }---- | Types that have Hypermedia-Relations.-class Related a where- -- | Name of the record selector that holds the resources identifier- type IdSelName a :: Symbol- -- | Servant-Endpoint for retrieving one @a@ by its identifier- type GetOneApi a :: Type- -- | Name for collected values- type CollectionName a :: Symbol- type CollectionName a = "items"- -- | List of all relations @a@ has- type Relations a :: [HRel]---- | Class for deriving Hypermedia-Relations for types.-type BuildRels :: Type -> [HRel] -> Type -> Constraint-class BuildRels api rs a where- buildRels :: Proxy rs -> Proxy api -> a -> [(String, Link)]--instance BuildRels api '[] a where- buildRels _ _ _ = []--instance- ( KnownSymbol relName- , HasField selName a id- , HasLink endpoint- , IsElem endpoint api- , MkLink endpoint Link ~ (id -> Link)- , BuildRels api rs a- ) => BuildRels api (('HRel relName selName endpoint) ': rs) a where- buildRels _ api x = l : buildRels (Proxy @rs) api x- where- mkLink = safeLink api (Proxy @endpoint)- l = (symbolVal (Proxy @relName), mkLink $ getField @selName x)---- | Generates pairs @(rel, link)@ for all related resources as defined with 'Relations'.-relatedLinks :: forall api a. (Related a, BuildRels api (Relations a) a) => Proxy api -> a -> [(String, Link)]-relatedLinks = buildRels (Proxy @(Relations a))---- | Generates the pair @(\"self\", link)@ where @link@ is the 'Link' to @a@ itself.-selfLink :: forall api a id.- ( Related a, HasField (IdSelName a) a id- , IsElem (GetOneApi a) api, HasLink (GetOneApi a)- , MkLink (GetOneApi a) Link ~ (id -> Link)- ) => Proxy api -> a -> (String, Link)-selfLink api x = ("self", mkSelf $ getField @(IdSelName a) x)- where- mkSelf = safeLink api (Proxy @(GetOneApi a))---- | Generate Hypermedia-Links by default. ----- @--- defaultLinks api x = selfLink api x : relatedLinks api x--- @-defaultLinks :: forall api a id.- ( Related a, HasField (IdSelName a) a id, IsElem (GetOneApi a) api- , HasLink (GetOneApi a), MkLink (GetOneApi a) Link ~ (id -> Link)- , BuildRels api (Relations a) a- ) => Proxy api -> a -> [(String, Link)]-defaultLinks api x = selfLink api x : relatedLinks api x+-- The default implementation wraps the value into a 'Resource' without adding any further information.+-- 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++instance Resource res => ToResource res [a]
+ src/Servant/Hateoas/ResourceServer.hs view
@@ -0,0 +1,98 @@+{-# LANGUAGE UndecidableInstances #-}++module Servant.Hateoas.ResourceServer+(+ -- * Type-Class+ HasResourceServer(..),++ -- * Type-Families+ Resourcify,+ ResourcifyServer+)+where++import Servant+import Servant.Hateoas.Layer+import Servant.Hateoas.Resource+import Servant.Hateoas.HasHandler+import Servant.Hateoas.Internal.Polyvariadic+import Data.Kind+import Control.Monad.IO.Class++-- | Turns an API into a resourceful API by replacing the response type of each endpoint with a resource type.+type Resourcify :: k -> Type -> k+type family Resourcify api ct where+ Resourcify EmptyAPI ct = EmptyAPI+ Resourcify (a :<|> b) ct = Resourcify a ct :<|> Resourcify b ct+ Resourcify (a :> b) ct = a :> Resourcify b ct+ Resourcify (Verb m s _ a) ct = Verb m s '[ct] (MkResource ct a)+ Resourcify ('Layer api cs verb) ct = 'Layer (Resourcify api ct) (Resourcify cs ct) (Resourcify verb ct)+ Resourcify (x:xs) ct = Resourcify x ct : Resourcify xs ct+ Resourcify a _ = a++-- | 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@.+--+-- Together with 'Resourcify' the following 'Constraint' holds:+--+-- @+-- forall api ct m. ServerT (Resourcify api) ct m ~ ResourcifyServer (ServerT api m) ct m+-- @+type ResourcifyServer :: k -> Type -> (Type -> Type) -> Type+type family ResourcifyServer server ct m where+ ResourcifyServer EmptyServer ct m = EmptyServer+ ResourcifyServer (a :<|> b) ct m = ResourcifyServer a ct m :<|> ResourcifyServer b ct m+ ResourcifyServer (a -> b) ct m = a -> ResourcifyServer b ct m+ ResourcifyServer (m a) ct m = m (MkResource ct a)+ ResourcifyServer (f a) ct m = f (ResourcifyServer a ct m) -- needed for stepping into containers like [Foo]++-- | A typeclass providing a function to turn an API into a resourceful API.+class HasResourceServer api m ct where+ getResourceServer :: MonadIO m => Proxy m -> Proxy ct -> Proxy api -> ServerT (Resourcify api ct) m++instance {-# OVERLAPPING #-} (HasResourceServer a m ct, HasResourceServer b m ct) => HasResourceServer (a :<|> b) m ct where+ getResourceServer m ct _ = getResourceServer m ct (Proxy @a) :<|> getResourceServer m ct (Proxy @b)++-- | Adds a self-link to the resource.+instance {-# OVERLAPPABLE #-}+ ( server ~ ServerT api m+ , ServerT (Resourcify api ct) m ~ ResourcifyServer server ct m+ , mkLink ~ MkLink api Link+ , res ~ MkResource ct+ , Resource res+ , ToResource res a+ , HasHandler api+ , HasLink api, IsElem api api+ , PolyvariadicComp2 server mkLink (IsFun server)+ , Return2 server mkLink (IsFun server) ~ (m a, Link)+ , 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+ where+ mkSelf = safeLink api api++instance+ ( api ~ LayerApi l+ , rApi ~ Resourcify api ct+ , 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)]+ , Resource res+ , BuildLayerLinks (Resourcify l ct) m+ , PolyvariadicComp buildFun (IsFun buildFun)+ , Return buildFun (IsFun buildFun) ~ [(String, ResourceLink)]+ , 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++instance HasResourceServer ('[] :: [Layer]) m ct where+ getResourceServer _ _ _ = emptyServer++instance+ ( MonadIO m+ , HasResourceServer ls m ct+ , HasResourceServer l m ct+ , BuildLayerLinks (Resourcify l ct) m+ ) => HasResourceServer (l ': ls) m ct where+ getResourceServer m ct _ = getResourceServer m ct (Proxy @l) :<|> getResourceServer m ct (Proxy @ls)