2015-04-20 19:52:29 +02:00
|
|
|
|
{-# LANGUAGE ConstraintKinds #-}
|
|
|
|
|
{-# LANGUAGE DataKinds #-}
|
|
|
|
|
{-# LANGUAGE FlexibleInstances #-}
|
2014-10-27 08:52:18 +01:00
|
|
|
|
{-# LANGUAGE FunctionalDependencies #-}
|
2015-04-20 19:52:29 +02:00
|
|
|
|
{-# LANGUAGE PolyKinds #-}
|
|
|
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
|
|
|
{-# LANGUAGE TypeFamilies #-}
|
|
|
|
|
{-# LANGUAGE TypeOperators #-}
|
|
|
|
|
{-# LANGUAGE UndecidableInstances #-}
|
2015-05-02 03:21:03 +02:00
|
|
|
|
{-# OPTIONS_HADDOCK not-home #-}
|
2015-01-23 07:44:06 +01:00
|
|
|
|
|
2015-01-29 00:07:01 +01:00
|
|
|
|
-- | Type safe generation of internal links.
|
2014-11-25 16:10:59 +01:00
|
|
|
|
--
|
2015-01-29 00:07:01 +01:00
|
|
|
|
-- Given an API with a few endpoints:
|
2014-11-25 16:35:56 +01:00
|
|
|
|
--
|
2015-01-29 00:07:01 +01:00
|
|
|
|
-- >>> :set -XDataKinds -XTypeFamilies -XTypeOperators
|
|
|
|
|
-- >>> import Servant.API
|
|
|
|
|
-- >>> import Servant.Utils.Links
|
|
|
|
|
-- >>> import Data.Proxy
|
|
|
|
|
-- >>>
|
|
|
|
|
-- >>>
|
|
|
|
|
-- >>>
|
2015-02-18 11:24:56 +01:00
|
|
|
|
-- >>> type Hello = "hello" :> Get '[JSON] Int
|
2015-05-06 21:21:35 +02:00
|
|
|
|
-- >>> type Bye = "bye" :> QueryParam "name" String :> Delete '[JSON] ()
|
2015-01-29 00:07:01 +01:00
|
|
|
|
-- >>> type API = Hello :<|> Bye
|
|
|
|
|
-- >>> let api = Proxy :: Proxy API
|
2015-01-23 07:44:06 +01:00
|
|
|
|
--
|
2015-01-29 00:07:01 +01:00
|
|
|
|
-- It is possible to generate links that are guaranteed to be within 'API' with
|
2015-01-30 01:03:48 +01:00
|
|
|
|
-- 'safeLink'. The first argument to 'safeLink' is a type representing the API
|
|
|
|
|
-- you would like to restrict links to. The second argument is the destination
|
|
|
|
|
-- endpoint you would like the link to point to, this will need to end with a
|
|
|
|
|
-- verb like GET or POST. Further arguments may be required depending on the
|
|
|
|
|
-- type of the endpoint. If everything lines up you will get a 'URI' out the
|
|
|
|
|
-- other end.
|
2015-01-23 07:44:06 +01:00
|
|
|
|
--
|
2015-01-29 00:07:01 +01:00
|
|
|
|
-- You may omit 'QueryParam's and the like should you not want to provide them,
|
2015-01-29 01:23:10 +01:00
|
|
|
|
-- but types which form part of the URL path like 'Capture' must be included.
|
2015-01-29 00:07:01 +01:00
|
|
|
|
-- The reason you may want to omit 'QueryParam's is that safeLink is a bit
|
|
|
|
|
-- magical: if parameters are included that could take input it will return a
|
2015-01-29 01:23:10 +01:00
|
|
|
|
-- function that accepts that input and generates a link. This is best shown
|
|
|
|
|
-- with an example. Here, a link is generated with no parameters:
|
2014-11-25 16:10:59 +01:00
|
|
|
|
--
|
2015-02-18 11:24:56 +01:00
|
|
|
|
-- >>> let hello = Proxy :: Proxy ("hello" :> Get '[JSON] Int)
|
2015-01-30 01:03:48 +01:00
|
|
|
|
-- >>> print (safeLink api hello :: URI)
|
2015-01-29 00:07:01 +01:00
|
|
|
|
-- hello
|
2014-11-25 16:10:59 +01:00
|
|
|
|
--
|
2015-01-29 00:07:01 +01:00
|
|
|
|
-- If the API has an endpoint with parameters then we can generate links with
|
|
|
|
|
-- or without those:
|
2014-11-25 16:10:59 +01:00
|
|
|
|
--
|
2015-05-06 21:21:35 +02:00
|
|
|
|
-- >>> let with = Proxy :: Proxy ("bye" :> QueryParam "name" String :> Delete '[JSON] ())
|
2015-06-19 10:29:06 +02:00
|
|
|
|
-- >>> print $ safeLink api with (Just "Hubert")
|
2015-01-29 00:07:01 +01:00
|
|
|
|
-- bye?name=Hubert
|
2014-11-25 16:10:59 +01:00
|
|
|
|
--
|
2015-05-06 21:21:35 +02:00
|
|
|
|
-- >>> let without = Proxy :: Proxy ("bye" :> Delete '[JSON] ())
|
2015-01-30 01:03:48 +01:00
|
|
|
|
-- >>> print $ safeLink api without
|
2015-01-29 00:07:01 +01:00
|
|
|
|
-- bye
|
2014-11-25 16:10:59 +01:00
|
|
|
|
--
|
2015-01-30 01:03:48 +01:00
|
|
|
|
-- If you would like create a helper for generating links only within that API,
|
|
|
|
|
-- you can partially apply safeLink if you specify a correct type signature
|
|
|
|
|
-- like so:
|
|
|
|
|
--
|
|
|
|
|
-- >>> :set -XConstraintKinds
|
|
|
|
|
-- >>> :{
|
|
|
|
|
-- >>> let apiLink :: (IsElem endpoint API, HasLink endpoint)
|
|
|
|
|
-- >>> => Proxy endpoint -> MkLink endpoint
|
|
|
|
|
-- >>> apiLink = safeLink api
|
|
|
|
|
-- >>> :}
|
|
|
|
|
--
|
2015-01-29 00:07:01 +01:00
|
|
|
|
-- Attempting to construct a link to an endpoint that does not exist in api
|
|
|
|
|
-- will result in a type error like this:
|
2014-11-25 16:10:59 +01:00
|
|
|
|
--
|
2015-05-06 21:21:35 +02:00
|
|
|
|
-- >>> let bad_link = Proxy :: Proxy ("hello" :> Delete '[JSON] ())
|
2015-01-30 01:03:48 +01:00
|
|
|
|
-- >>> safeLink api bad_link
|
2015-05-27 03:29:08 +02:00
|
|
|
|
-- ...
|
2015-01-29 00:07:01 +01:00
|
|
|
|
-- Could not deduce (Or
|
2016-01-08 17:43:10 +01:00
|
|
|
|
-- (IsElem' (Verb 'DELETE 200 '[JSON] ()) (Verb 'GET 200 '[JSON] Int))
|
2015-01-29 00:07:01 +01:00
|
|
|
|
-- (IsElem'
|
2015-05-06 21:21:35 +02:00
|
|
|
|
-- ("hello" :> Delete '[JSON] ())
|
|
|
|
|
-- ("bye" :> (QueryParam "name" String :> Delete '[JSON] ()))))
|
2015-01-29 00:07:01 +01:00
|
|
|
|
-- arising from a use of ‘safeLink’
|
2015-01-30 01:03:48 +01:00
|
|
|
|
-- In the expression: safeLink api bad_link
|
|
|
|
|
-- In an equation for ‘it’: it = safeLink api bad_link
|
2015-01-29 00:07:01 +01:00
|
|
|
|
--
|
|
|
|
|
-- This error is essentially saying that the type family couldn't find
|
|
|
|
|
-- bad_link under api after trying the open (but empty) type family
|
|
|
|
|
-- `IsElem'` as a last resort.
|
2014-12-10 10:34:49 +01:00
|
|
|
|
module Servant.Utils.Links (
|
2015-01-29 00:07:01 +01:00
|
|
|
|
-- * Building and using safe links
|
|
|
|
|
--
|
|
|
|
|
-- | Note that 'URI' is Network.URI.URI from the network-uri package.
|
|
|
|
|
safeLink
|
|
|
|
|
, URI(..)
|
|
|
|
|
-- * Adding custom types
|
|
|
|
|
, HasLink(..)
|
|
|
|
|
, linkURI
|
|
|
|
|
, Link
|
|
|
|
|
, IsElem'
|
|
|
|
|
-- * Illustrative exports
|
|
|
|
|
, IsElem
|
|
|
|
|
, Or
|
|
|
|
|
) where
|
2014-10-27 08:52:18 +01:00
|
|
|
|
|
2015-12-27 02:20:46 +01:00
|
|
|
|
import qualified Data.ByteString.Char8 as BSC
|
2016-03-03 10:45:08 +01:00
|
|
|
|
import Data.List
|
|
|
|
|
import Data.Monoid.Compat ( (<>) )
|
|
|
|
|
import Data.Proxy ( Proxy(..) )
|
|
|
|
|
import qualified Data.Text as Text
|
|
|
|
|
import GHC.Exts (Constraint)
|
|
|
|
|
import GHC.TypeLits ( KnownSymbol, symbolVal )
|
|
|
|
|
import Network.URI ( URI(..), escapeURIString, isUnreserved )
|
|
|
|
|
import Prelude ()
|
|
|
|
|
import Prelude.Compat
|
2014-10-27 08:52:18 +01:00
|
|
|
|
|
2015-10-07 23:38:47 +02:00
|
|
|
|
import Web.HttpApiData
|
2015-01-06 17:54:53 +01:00
|
|
|
|
import Servant.API.Capture ( Capture )
|
|
|
|
|
import Servant.API.ReqBody ( ReqBody )
|
2015-01-15 10:44:45 +01:00
|
|
|
|
import Servant.API.QueryParam ( QueryParam, QueryParams, QueryFlag )
|
2015-01-29 00:07:01 +01:00
|
|
|
|
import Servant.API.Header ( Header )
|
2015-11-27 02:05:34 +01:00
|
|
|
|
import Servant.API.Verbs ( Verb )
|
2015-01-06 17:54:53 +01:00
|
|
|
|
import Servant.API.Sub ( type (:>) )
|
2015-01-23 07:44:06 +01:00
|
|
|
|
import Servant.API.Raw ( Raw )
|
2015-01-06 17:54:53 +01:00
|
|
|
|
import Servant.API.Alternative ( type (:<|>) )
|
2014-10-27 08:52:18 +01:00
|
|
|
|
|
2015-01-08 16:24:19 +01:00
|
|
|
|
-- | A safe link datatype.
|
|
|
|
|
-- The only way of constructing a 'Link' is using 'safeLink', which means any
|
|
|
|
|
-- 'Link' is guaranteed to be part of the mentioned API.
|
|
|
|
|
data Link = Link
|
|
|
|
|
{ _segments :: [String] -- ^ Segments of "foo/bar" would be ["foo", "bar"]
|
|
|
|
|
, _queryParams :: [Param Query]
|
|
|
|
|
} deriving Show
|
|
|
|
|
|
2015-12-27 02:20:46 +01:00
|
|
|
|
instance ToHttpApiData Link where
|
|
|
|
|
toUrlPiece = Text.pack . show
|
|
|
|
|
toHeader = BSC.pack . show
|
|
|
|
|
|
2015-01-29 00:07:01 +01:00
|
|
|
|
-- | If either a or b produce an empty constraint, produce an empty constraint.
|
2015-01-27 06:06:21 +01:00
|
|
|
|
type family Or (a :: Constraint) (b :: Constraint) :: Constraint where
|
2015-05-15 02:37:18 +02:00
|
|
|
|
-- This works because of:
|
|
|
|
|
-- https://ghc.haskell.org/trac/ghc/wiki/NewAxioms/CoincidentOverlap
|
2015-01-27 06:06:21 +01:00
|
|
|
|
Or () b = ()
|
|
|
|
|
Or a () = ()
|
2014-10-27 08:52:18 +01:00
|
|
|
|
|
2015-02-18 10:40:55 +01:00
|
|
|
|
-- | If both a or b produce an empty constraint, produce an empty constraint.
|
|
|
|
|
type family And (a :: Constraint) (b :: Constraint) :: Constraint where
|
2015-05-13 16:07:57 +02:00
|
|
|
|
And () () = ()
|
2015-02-18 10:40:55 +01:00
|
|
|
|
|
2015-05-13 16:07:57 +02:00
|
|
|
|
-- | You may use this type family to tell the type checker that your custom
|
|
|
|
|
-- type may be skipped as part of a link. This is useful for things like
|
2015-01-29 00:39:02 +01:00
|
|
|
|
-- 'QueryParam' that are optional in a URI and do not affect them if they are
|
|
|
|
|
-- omitted.
|
2015-01-29 00:07:01 +01:00
|
|
|
|
--
|
|
|
|
|
-- >>> data CustomThing
|
|
|
|
|
-- >>> type instance IsElem' e (CustomThing :> s) = IsElem e s
|
|
|
|
|
--
|
2015-01-29 00:39:02 +01:00
|
|
|
|
-- Note that 'IsElem' is called, which will mutually recurse back to `IsElem'`
|
|
|
|
|
-- if it exhausts all other options again.
|
|
|
|
|
--
|
|
|
|
|
-- Once you have written a HasLink instance for CustomThing you are ready to
|
|
|
|
|
-- go.
|
2015-01-27 06:06:21 +01:00
|
|
|
|
type family IsElem' a s :: Constraint
|
2015-01-23 07:44:06 +01:00
|
|
|
|
|
2015-01-29 00:07:01 +01:00
|
|
|
|
-- | Closed type family, check if endpoint is within api
|
|
|
|
|
type family IsElem endpoint api :: Constraint where
|
2015-05-13 16:07:57 +02:00
|
|
|
|
IsElem e (sa :<|> sb) = Or (IsElem e sa) (IsElem e sb)
|
|
|
|
|
IsElem (e :> sa) (e :> sb) = IsElem sa sb
|
2015-07-21 15:22:56 +02:00
|
|
|
|
IsElem sa (Header sym x :> sb) = IsElem sa sb
|
2015-05-13 16:07:57 +02:00
|
|
|
|
IsElem sa (ReqBody y x :> sb) = IsElem sa sb
|
2015-05-27 03:29:08 +02:00
|
|
|
|
IsElem (Capture z y :> sa) (Capture x y :> sb)
|
|
|
|
|
= IsElem sa sb
|
2015-05-13 16:07:57 +02:00
|
|
|
|
IsElem sa (QueryParam x y :> sb) = IsElem sa sb
|
|
|
|
|
IsElem sa (QueryParams x y :> sb) = IsElem sa sb
|
|
|
|
|
IsElem sa (QueryFlag x :> sb) = IsElem sa sb
|
2015-11-27 02:05:34 +01:00
|
|
|
|
IsElem (Verb m s ct typ) (Verb m s ct' typ)
|
|
|
|
|
= IsSubList ct ct'
|
2015-05-13 16:07:57 +02:00
|
|
|
|
IsElem e e = ()
|
|
|
|
|
IsElem e a = IsElem' e a
|
2014-10-27 08:52:18 +01:00
|
|
|
|
|
2015-02-18 10:40:55 +01:00
|
|
|
|
type family IsSubList a b :: Constraint where
|
2015-05-13 16:07:57 +02:00
|
|
|
|
IsSubList '[] b = ()
|
2015-12-16 13:41:18 +01:00
|
|
|
|
IsSubList (x ': xs) y = Elem x y `And` IsSubList xs y
|
|
|
|
|
|
|
|
|
|
type family Elem e es :: Constraint where
|
|
|
|
|
Elem x (x ': xs) = ()
|
|
|
|
|
Elem y (x ': xs) = Elem y xs
|
2014-10-27 08:52:18 +01:00
|
|
|
|
|
2015-01-29 00:07:01 +01:00
|
|
|
|
-- Phantom types for Param
|
|
|
|
|
data Query
|
2014-10-27 08:52:18 +01:00
|
|
|
|
|
2015-10-08 22:40:46 +02:00
|
|
|
|
-- | Query param
|
2015-01-29 00:07:01 +01:00
|
|
|
|
data Param a
|
2015-12-27 02:20:46 +01:00
|
|
|
|
= SingleParam String Text.Text
|
|
|
|
|
| ArrayElemParam String Text.Text
|
2015-01-29 00:07:01 +01:00
|
|
|
|
| FlagParam String
|
|
|
|
|
deriving Show
|
2014-10-27 08:52:18 +01:00
|
|
|
|
|
2015-01-29 00:07:01 +01:00
|
|
|
|
addSegment :: String -> Link -> Link
|
2015-01-29 00:39:02 +01:00
|
|
|
|
addSegment seg l = l { _segments = _segments l <> [seg] }
|
2014-10-27 08:52:18 +01:00
|
|
|
|
|
2015-01-29 00:07:01 +01:00
|
|
|
|
addQueryParam :: Param Query -> Link -> Link
|
|
|
|
|
addQueryParam qp l =
|
2015-01-29 00:39:02 +01:00
|
|
|
|
l { _queryParams = _queryParams l <> [qp] }
|
2014-10-27 08:52:18 +01:00
|
|
|
|
|
2015-01-29 00:07:01 +01:00
|
|
|
|
linkURI :: Link -> URI
|
|
|
|
|
linkURI (Link segments q_params) =
|
|
|
|
|
URI mempty -- No scheme (relative)
|
|
|
|
|
Nothing -- Or authority (relative)
|
|
|
|
|
(intercalate "/" segments)
|
|
|
|
|
(makeQueries q_params) mempty
|
|
|
|
|
where
|
|
|
|
|
makeQueries :: [Param Query] -> String
|
|
|
|
|
makeQueries [] = ""
|
|
|
|
|
makeQueries xs =
|
|
|
|
|
"?" <> intercalate "&" (fmap makeQuery xs)
|
|
|
|
|
|
|
|
|
|
makeQuery :: Param Query -> String
|
2015-12-27 02:20:46 +01:00
|
|
|
|
makeQuery (ArrayElemParam k v) = escape k <> "[]=" <> escape (Text.unpack v)
|
|
|
|
|
makeQuery (SingleParam k v) = escape k <> "=" <> escape (Text.unpack v)
|
2015-01-29 00:07:01 +01:00
|
|
|
|
makeQuery (FlagParam k) = escape k
|
|
|
|
|
|
|
|
|
|
escape :: String -> String
|
|
|
|
|
escape = escapeURIString isUnreserved
|
|
|
|
|
|
|
|
|
|
-- | Create a valid (by construction) relative URI with query params.
|
|
|
|
|
--
|
|
|
|
|
-- This function will only typecheck if `endpoint` is part of the API `api`
|
|
|
|
|
safeLink
|
|
|
|
|
:: forall endpoint api. (IsElem endpoint api, HasLink endpoint)
|
2015-01-30 01:03:48 +01:00
|
|
|
|
=> Proxy api -- ^ The whole API that this endpoint is a part of
|
|
|
|
|
-> Proxy endpoint -- ^ The API endpoint you would like to point to
|
2015-01-29 00:07:01 +01:00
|
|
|
|
-> MkLink endpoint
|
2015-01-30 01:03:48 +01:00
|
|
|
|
safeLink _ endpoint = toLink endpoint (Link mempty mempty)
|
2015-01-29 00:07:01 +01:00
|
|
|
|
|
2015-01-30 01:03:48 +01:00
|
|
|
|
-- | Construct a toLink for an endpoint.
|
2015-01-29 00:07:01 +01:00
|
|
|
|
class HasLink endpoint where
|
|
|
|
|
type MkLink endpoint
|
2015-01-30 01:03:48 +01:00
|
|
|
|
toLink :: Proxy endpoint -- ^ The API endpoint you would like to point to
|
2015-01-29 00:07:01 +01:00
|
|
|
|
-> Link
|
|
|
|
|
-> MkLink endpoint
|
|
|
|
|
|
|
|
|
|
-- Naked symbol instance
|
|
|
|
|
instance (KnownSymbol sym, HasLink sub) => HasLink (sym :> sub) where
|
|
|
|
|
type MkLink (sym :> sub) = MkLink sub
|
2015-01-30 01:03:48 +01:00
|
|
|
|
toLink _ =
|
|
|
|
|
toLink (Proxy :: Proxy sub) . addSegment seg
|
2015-01-29 00:07:01 +01:00
|
|
|
|
where
|
|
|
|
|
seg = symbolVal (Proxy :: Proxy sym)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- QueryParam instances
|
2015-10-07 23:38:47 +02:00
|
|
|
|
instance (KnownSymbol sym, ToHttpApiData v, HasLink sub)
|
2015-01-29 00:07:01 +01:00
|
|
|
|
=> HasLink (QueryParam sym v :> sub) where
|
2015-06-18 18:03:48 +02:00
|
|
|
|
type MkLink (QueryParam sym v :> sub) = Maybe v -> MkLink sub
|
|
|
|
|
toLink _ l mv =
|
|
|
|
|
toLink (Proxy :: Proxy sub) $
|
2015-10-07 23:38:47 +02:00
|
|
|
|
maybe id (addQueryParam . SingleParam k . toQueryParam) mv l
|
2015-01-29 00:07:01 +01:00
|
|
|
|
where
|
|
|
|
|
k :: String
|
|
|
|
|
k = symbolVal (Proxy :: Proxy sym)
|
|
|
|
|
|
2015-10-07 23:38:47 +02:00
|
|
|
|
instance (KnownSymbol sym, ToHttpApiData v, HasLink sub)
|
2015-01-29 00:07:01 +01:00
|
|
|
|
=> HasLink (QueryParams sym v :> sub) where
|
|
|
|
|
type MkLink (QueryParams sym v :> sub) = [v] -> MkLink sub
|
2015-01-30 01:03:48 +01:00
|
|
|
|
toLink _ l =
|
|
|
|
|
toLink (Proxy :: Proxy sub) .
|
2015-10-07 23:38:47 +02:00
|
|
|
|
foldl' (\l' v -> addQueryParam (ArrayElemParam k (toQueryParam v)) l') l
|
2015-01-29 00:07:01 +01:00
|
|
|
|
where
|
|
|
|
|
k = symbolVal (Proxy :: Proxy sym)
|
|
|
|
|
|
|
|
|
|
instance (KnownSymbol sym, HasLink sub)
|
|
|
|
|
=> HasLink (QueryFlag sym :> sub) where
|
|
|
|
|
type MkLink (QueryFlag sym :> sub) = Bool -> MkLink sub
|
2015-01-30 01:03:48 +01:00
|
|
|
|
toLink _ l False =
|
|
|
|
|
toLink (Proxy :: Proxy sub) l
|
|
|
|
|
toLink _ l True =
|
|
|
|
|
toLink (Proxy :: Proxy sub) $ addQueryParam (FlagParam k) l
|
2015-01-29 00:07:01 +01:00
|
|
|
|
where
|
|
|
|
|
k = symbolVal (Proxy :: Proxy sym)
|
|
|
|
|
|
|
|
|
|
-- Misc instances
|
2015-03-17 02:10:24 +01:00
|
|
|
|
instance HasLink sub => HasLink (ReqBody ct a :> sub) where
|
|
|
|
|
type MkLink (ReqBody ct a :> sub) = MkLink sub
|
2015-01-30 01:03:48 +01:00
|
|
|
|
toLink _ = toLink (Proxy :: Proxy sub)
|
2015-01-29 00:07:01 +01:00
|
|
|
|
|
2015-10-07 23:38:47 +02:00
|
|
|
|
instance (ToHttpApiData v, HasLink sub)
|
2015-01-29 00:07:01 +01:00
|
|
|
|
=> HasLink (Capture sym v :> sub) where
|
|
|
|
|
type MkLink (Capture sym v :> sub) = v -> MkLink sub
|
2015-01-30 01:03:48 +01:00
|
|
|
|
toLink _ l v =
|
|
|
|
|
toLink (Proxy :: Proxy sub) $
|
2015-12-27 02:20:46 +01:00
|
|
|
|
addSegment (escape . Text.unpack $ toUrlPiece v) l
|
2015-01-29 00:07:01 +01:00
|
|
|
|
|
2015-06-18 12:40:00 +02:00
|
|
|
|
instance HasLink sub => HasLink (Header sym a :> sub) where
|
2015-06-18 13:09:18 +02:00
|
|
|
|
type MkLink (Header sym a :> sub) = MkLink sub
|
|
|
|
|
toLink _ = toLink (Proxy :: Proxy sub)
|
2015-06-18 12:40:00 +02:00
|
|
|
|
|
2015-01-29 00:07:01 +01:00
|
|
|
|
-- Verb (terminal) instances
|
2015-11-27 02:05:34 +01:00
|
|
|
|
instance HasLink (Verb m s ct a) where
|
|
|
|
|
type MkLink (Verb m s ct a) = URI
|
2015-01-30 01:03:48 +01:00
|
|
|
|
toLink _ = linkURI
|
2014-10-27 08:52:18 +01:00
|
|
|
|
|
2015-01-29 00:07:01 +01:00
|
|
|
|
instance HasLink Raw where
|
|
|
|
|
type MkLink Raw = URI
|
2015-01-30 01:03:48 +01:00
|
|
|
|
toLink _ = linkURI
|