diff --git a/servant.cabal b/servant.cabal index 52d039a7..c0048a12 100644 --- a/servant.cabal +++ b/servant.cabal @@ -31,8 +31,8 @@ library Servant.Common.Req Servant.Common.Text Servant.Docs + Servant.QQ Servant.Server - Servant.Utils.ApiQuasiQuoting Servant.Utils.Links Servant.Utils.StaticFiles build-depends: diff --git a/src/Servant.hs b/src/Servant.hs index 69fb50a2..a6263ab7 100644 --- a/src/Servant.hs +++ b/src/Servant.hs @@ -12,7 +12,7 @@ module Servant ( -- | Using your types in request paths and query string parameters module Servant.Common.Text, -- | Utilities on top of the servant core - module Servant.Utils.ApiQuasiQuoting, + module Servant.QQ, module Servant.Utils.Links, module Servant.Utils.StaticFiles, -- | Useful re-exports @@ -25,6 +25,6 @@ import Servant.Client import Servant.Common.Text import Servant.Docs import Servant.Server -import Servant.Utils.ApiQuasiQuoting +import Servant.QQ import Servant.Utils.Links import Servant.Utils.StaticFiles diff --git a/src/Servant/API.hs b/src/Servant/API.hs index 76b4fbcc..c5a7a288 100644 --- a/src/Servant/API.hs +++ b/src/Servant/API.hs @@ -31,7 +31,7 @@ module Servant.API ( -- * Utilities -- | QuasiQuotes for endpoints - module Servant.Utils.ApiQuasiQuoting, + module Servant.QQ, -- | Type-safe internal URLs module Servant.Utils.Links, ) where @@ -46,6 +46,6 @@ import Servant.API.QueryParam import Servant.API.Raw import Servant.API.ReqBody import Servant.API.Sub -import Servant.Utils.ApiQuasiQuoting (sitemap) +import Servant.QQ (sitemap) import Servant.Utils.Links (mkLink) import Servant.Utils.StaticFiles diff --git a/src/Servant/Utils/ApiQuasiQuoting.hs b/src/Servant/QQ.hs similarity index 78% rename from src/Servant/Utils/ApiQuasiQuoting.hs rename to src/Servant/QQ.hs index e768c73e..a886c90f 100644 --- a/src/Servant/Utils/ApiQuasiQuoting.hs +++ b/src/Servant/QQ.hs @@ -5,7 +5,28 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} {-# OPTIONS_GHC -fno-warn-unused-do-bind #-} -module Servant.Utils.ApiQuasiQuoting where +-- | QuasiQuoting utilities for API types. +-- +-- 'sitemap' allows you to write your type in a very natural way: +-- +-- @ +-- [sitemap| +-- PUT hello String -> () +-- POST hello/p:Int String -> () +-- GET hello/?name:String Int +-- |] +-- @ +-- +-- Will generate: +-- +-- @ +-- "hello" :> ReqBody String :> Put () +-- :\<|> "hello" :> Capture "p" Int :> ReqBody String :> Post () +-- :\<|> "hello" :> QueryParam "name" String :> Get Int +-- @ +-- +-- Note the @/@ before a @QueryParam@! +module Servant.QQ where import Control.Monad (void) import Control.Applicative hiding (many, (<|>), optional) @@ -23,6 +44,11 @@ import Servant.API.ReqBody import Servant.API.Sub import Servant.API.Alternative +-- | Finally-tagless encoding for our DSL. +-- Keeping 'repr'' and 'repr' distinct when writing functions with an +-- @ExpSYM@ context ensures certain invariants (for instance, that there is +-- only one of 'get', 'post', 'put', and 'delete' in a value), but +-- sometimes requires a little more work. class ExpSYM repr' repr | repr -> repr', repr' -> repr where lit :: String -> repr' -> repr capture :: String -> String -> repr -> repr @@ -148,6 +174,19 @@ parseAll = do where union :: Type -> Type -> Type union a = AppT (AppT (ConT ''(:<|>)) a) +-- | The sitemap QuasiQuoter. +-- +-- * @.../:/...@ becomes a capture +-- * @.../?:@ becomes a query parameter +-- * @ ... @ becomes a method returning @@ +-- * @ ... -> @ becomes a method with request +-- body of @@ and returning @@ +-- +-- Comments are allowed, and have the standard Haskell format +-- +-- * @--@ for inline +-- * @{- ... -}@ for block +-- sitemap :: QuasiQuoter sitemap = QuasiQuoter { quoteExp = undefined , quotePat = undefined diff --git a/src/Servant/Utils/Links.hs b/src/Servant/Utils/Links.hs index 97f65f21..547a3555 100644 --- a/src/Servant/Utils/Links.hs +++ b/src/Servant/Utils/Links.hs @@ -6,6 +6,33 @@ {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE UndecidableInstances #-} +-- | Type safe internal links. +-- +-- Provides the function 'mkLink': +-- +-- @ +-- type API = Proxy ("hello" :> Get Int +-- :<|> "bye" :> QueryParam "name" String :> Post Bool) +-- +-- api :: API +-- api = proxy +-- +-- link1 :: Proxy ("hello" :> Get Int) +-- link1 = proxy +-- +-- link2 :: Proxy ("hello" :> Delete) +-- link2 = proxy +-- +-- mkLink link1 API -- typechecks, returns 'Link "/hello"' +-- +-- mkLink link2 API -- doesn't typecheck +-- @ +-- +-- That is, 'mkLink' takes two arguments, a link proxy and a sitemap, and +-- returns a 'Link', but only typechecks if the link proxy is a valid link, +-- and part of the sitemap. +-- +-- __N.B.:__ 'mkLink' assumes a capture matches any string (without slashes). module Servant.Utils.Links where import Data.Proxy diff --git a/test/Servant/Utils/ApiQuasiQuotingSpec.hs b/test/Servant/QQSpec.hs similarity index 99% rename from test/Servant/Utils/ApiQuasiQuotingSpec.hs rename to test/Servant/QQSpec.hs index 7dd3bfc1..adf59611 100644 --- a/test/Servant/Utils/ApiQuasiQuotingSpec.hs +++ b/test/Servant/QQSpec.hs @@ -7,7 +7,7 @@ {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} -module Servant.Utils.ApiQuasiQuotingSpec where +module Servant.QQSpec where import Test.Hspec diff --git a/test/Servant/Utils/LinksSpec.hs b/test/Servant/Utils/LinksSpec.hs index b896d7f5..3f16d71b 100644 --- a/test/Servant/Utils/LinksSpec.hs +++ b/test/Servant/Utils/LinksSpec.hs @@ -6,7 +6,7 @@ module Servant.Utils.LinksSpec where import Test.Hspec import Servant.API -import Servant.Utils.ApiQuasiQuotingSpec ( (~>) ) +import Servant.QQSpec ( (~>) ) import Servant.Utils.Links (IsElem, IsLink) type TestApi =