Merge pull request #753 from DaveCTurner/issue-753
Add a type representing an empty API
This commit is contained in:
commit
1ccb0ef812
19 changed files with 154 additions and 12 deletions
|
@ -321,11 +321,33 @@ data BasicAuth (realm :: Symbol) (userData :: *)
|
||||||
Which is used like so:
|
Which is used like so:
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
type ProtectedAPI12
|
type ProtectedAPI11
|
||||||
= UserAPI -- this is public
|
= UserAPI -- this is public
|
||||||
:<|> BasicAuth "my-realm" User :> UserAPI2 -- this is protected by auth
|
:<|> BasicAuth "my-realm" User :> UserAPI2 -- this is protected by auth
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Empty APIs
|
||||||
|
|
||||||
|
Sometimes it is useful to be able to generalise an API over the type of some
|
||||||
|
part of it:
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
type UserAPI12 innerAPI
|
||||||
|
= UserAPI -- this is the fixed bit of the API
|
||||||
|
:<|> "inner" :> innerAPI -- this lets us put various other APIs under /inner
|
||||||
|
```
|
||||||
|
|
||||||
|
If there is a case where you do not have anything extra to serve, you can use
|
||||||
|
the `EmptyAPI` combinator to indicate this:
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
type UserAPI12Alone = UserAPI12 EmptyAPI
|
||||||
|
```
|
||||||
|
|
||||||
|
This also works well as a placeholder for unfinished parts of an API while it
|
||||||
|
is under development, for when you know that there should be _something_ there
|
||||||
|
but you don't yet know what. Think of it as similar to the unit type `()`.
|
||||||
|
|
||||||
### Interoperability with `wai`: `Raw`
|
### Interoperability with `wai`: `Raw`
|
||||||
|
|
||||||
Finally, we also include a combinator named `Raw` that provides an escape hatch
|
Finally, we also include a combinator named `Raw` that provides an escape hatch
|
||||||
|
@ -334,7 +356,7 @@ you want to plug a [wai `Application`](http://hackage.haskell.org/package/wai)
|
||||||
into your webservice:
|
into your webservice:
|
||||||
|
|
||||||
``` haskell
|
``` haskell
|
||||||
type UserAPI11 = "users" :> Get '[JSON] [User]
|
type UserAPI13 = "users" :> Get '[JSON] [User]
|
||||||
-- a /users endpoint
|
-- a /users endpoint
|
||||||
|
|
||||||
:<|> Raw
|
:<|> Raw
|
||||||
|
|
|
@ -93,6 +93,19 @@ position :<|> hello :<|> marketing = client api
|
||||||
|
|
||||||
`client api` returns client functions for our _entire_ API, combined with `:<|>`, which we can pattern match on as above. You could say `client` "calculates" the correct type and number of client functions for the API type it is given (via a `Proxy`), as well as their implementations.
|
`client api` returns client functions for our _entire_ API, combined with `:<|>`, which we can pattern match on as above. You could say `client` "calculates" the correct type and number of client functions for the API type it is given (via a `Proxy`), as well as their implementations.
|
||||||
|
|
||||||
|
If you have an `EmptyAPI` in your API, servant-client will hand you a value of
|
||||||
|
type `EmptyClient` in the corresponding slot, where `data EmptyClient =
|
||||||
|
EmptyClient`, as a way to indicate that you can't do anything useful with it.
|
||||||
|
|
||||||
|
``` haskell ignore
|
||||||
|
type API' = API :<|> EmptyAPI
|
||||||
|
|
||||||
|
api' :: Proxy API'
|
||||||
|
api' = Proxy
|
||||||
|
|
||||||
|
(position' :<|> hello' :<|> marketing') :<|> EmptyClient = client api'
|
||||||
|
```
|
||||||
|
|
||||||
``` haskell ignore
|
``` haskell ignore
|
||||||
-- | URI scheme to use
|
-- | URI scheme to use
|
||||||
data Scheme =
|
data Scheme =
|
||||||
|
|
|
@ -89,6 +89,8 @@ instance ToSample Email where
|
||||||
```
|
```
|
||||||
|
|
||||||
Types that are used as request or response bodies have to instantiate the `ToSample` typeclass which lets you specify one or more examples of values. `Capture`s and `QueryParam`s have to instantiate their respective `ToCapture` and `ToParam` classes and provide a name and some information about the concrete meaning of that argument, as illustrated in the code above.
|
Types that are used as request or response bodies have to instantiate the `ToSample` typeclass which lets you specify one or more examples of values. `Capture`s and `QueryParam`s have to instantiate their respective `ToCapture` and `ToParam` classes and provide a name and some information about the concrete meaning of that argument, as illustrated in the code above.
|
||||||
|
The `EmptyAPI` combinator needs no special treatment as it generates no
|
||||||
|
documentation: an empty API has no endpoints to document.
|
||||||
|
|
||||||
With all of this, we can derive docs for our API.
|
With all of this, we can derive docs for our API.
|
||||||
|
|
||||||
|
|
|
@ -149,6 +149,9 @@ Why two different API types, proxies and servers though? Simply because we
|
||||||
don't want to generate javascript functions for the `Raw` part of our API type,
|
don't want to generate javascript functions for the `Raw` part of our API type,
|
||||||
so we need a `Proxy` for our API type `API'` without its `Raw` endpoint.
|
so we need a `Proxy` for our API type `API'` without its `Raw` endpoint.
|
||||||
|
|
||||||
|
The `EmptyAPI` combinator needs no special treatment as it generates no
|
||||||
|
Javascript functions: an empty API has no endpoints to access.
|
||||||
|
|
||||||
Very similarly to how one can derive haskell functions, we can derive the
|
Very similarly to how one can derive haskell functions, we can derive the
|
||||||
javascript with just a simple function call to `jsForAPI` from
|
javascript with just a simple function call to `jsForAPI` from
|
||||||
`Servant.JS`.
|
`Servant.JS`.
|
||||||
|
|
|
@ -1020,6 +1020,17 @@ serverFor = error "..."
|
||||||
-- or the mailing list if you get stuck!
|
-- or the mailing list if you get stuck!
|
||||||
```
|
```
|
||||||
|
|
||||||
|
When your API contains the `EmptyAPI` combinator, you'll want to use
|
||||||
|
`emptyServer` in the corresponding slot for your server, which will simply fail
|
||||||
|
with 404 whenever a request reaches it:
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
type CombinedAPI2 = API :<|> "empty" :> EmptyAPI
|
||||||
|
|
||||||
|
server11 :: Server CombinedAPI2
|
||||||
|
server11 = server3 :<|> emptyServer
|
||||||
|
```
|
||||||
|
|
||||||
## Using another monad for your handlers
|
## Using another monad for your handlers
|
||||||
|
|
||||||
Remember how `Server` turns combinators for HTTP methods into `Handler`? Well, actually, there's more to that. `Server` is actually a
|
Remember how `Server` turns combinators for HTTP methods into `Handler`? Well, actually, there's more to that. `Server` is actually a
|
||||||
|
|
|
@ -24,6 +24,7 @@ module Servant.Client
|
||||||
, ClientEnv (ClientEnv)
|
, ClientEnv (ClientEnv)
|
||||||
, mkAuthenticateReq
|
, mkAuthenticateReq
|
||||||
, ServantError(..)
|
, ServantError(..)
|
||||||
|
, EmptyClient(..)
|
||||||
, module Servant.Common.BaseUrl
|
, module Servant.Common.BaseUrl
|
||||||
) where
|
) where
|
||||||
|
|
||||||
|
@ -88,6 +89,23 @@ instance (HasClient a, HasClient b) => HasClient (a :<|> b) where
|
||||||
clientWithRoute (Proxy :: Proxy a) req :<|>
|
clientWithRoute (Proxy :: Proxy a) req :<|>
|
||||||
clientWithRoute (Proxy :: Proxy b) req
|
clientWithRoute (Proxy :: Proxy b) req
|
||||||
|
|
||||||
|
-- | Singleton type representing a client for an empty API.
|
||||||
|
data EmptyClient = EmptyClient deriving (Eq, Show, Bounded, Enum)
|
||||||
|
|
||||||
|
-- | The client for 'EmptyAPI' is simply 'EmptyClient'.
|
||||||
|
--
|
||||||
|
-- > type MyAPI = "books" :> Get '[JSON] [Book] -- GET /books
|
||||||
|
-- > :<|> "nothing" :> EmptyAPI
|
||||||
|
-- >
|
||||||
|
-- > myApi :: Proxy MyApi
|
||||||
|
-- > myApi = Proxy
|
||||||
|
-- >
|
||||||
|
-- > getAllBooks :: ClientM [Book]
|
||||||
|
-- > (getAllBooks :<|> EmptyClient) = client myApi
|
||||||
|
instance HasClient EmptyAPI where
|
||||||
|
type Client EmptyAPI = EmptyClient
|
||||||
|
clientWithRoute Proxy _ = EmptyClient
|
||||||
|
|
||||||
-- | If you use a 'Capture' in one of your endpoints in your API,
|
-- | If you use a 'Capture' in one of your endpoints in your API,
|
||||||
-- the corresponding querying function will automatically take
|
-- the corresponding querying function will automatically take
|
||||||
-- an additional argument of the type specified by your 'Capture'.
|
-- an additional argument of the type specified by your 'Capture'.
|
||||||
|
|
|
@ -111,6 +111,8 @@ type Api =
|
||||||
Get '[JSON] (String, Maybe Int, Bool, [(String, [Rational])])
|
Get '[JSON] (String, Maybe Int, Bool, [(String, [Rational])])
|
||||||
:<|> "headers" :> Get '[JSON] (Headers TestHeaders Bool)
|
:<|> "headers" :> Get '[JSON] (Headers TestHeaders Bool)
|
||||||
:<|> "deleteContentType" :> DeleteNoContent '[JSON] NoContent
|
:<|> "deleteContentType" :> DeleteNoContent '[JSON] NoContent
|
||||||
|
:<|> "empty" :> EmptyAPI
|
||||||
|
|
||||||
api :: Proxy Api
|
api :: Proxy Api
|
||||||
api = Proxy
|
api = Proxy
|
||||||
|
|
||||||
|
@ -130,6 +132,7 @@ getMultiple :: String -> Maybe Int -> Bool -> [(String, [Rational])]
|
||||||
-> SCR.ClientM (String, Maybe Int, Bool, [(String, [Rational])])
|
-> SCR.ClientM (String, Maybe Int, Bool, [(String, [Rational])])
|
||||||
getRespHeaders :: SCR.ClientM (Headers TestHeaders Bool)
|
getRespHeaders :: SCR.ClientM (Headers TestHeaders Bool)
|
||||||
getDeleteContentType :: SCR.ClientM NoContent
|
getDeleteContentType :: SCR.ClientM NoContent
|
||||||
|
|
||||||
getGet
|
getGet
|
||||||
:<|> getDeleteEmpty
|
:<|> getDeleteEmpty
|
||||||
:<|> getCapture
|
:<|> getCapture
|
||||||
|
@ -142,7 +145,8 @@ getGet
|
||||||
:<|> getRawFailure
|
:<|> getRawFailure
|
||||||
:<|> getMultiple
|
:<|> getMultiple
|
||||||
:<|> getRespHeaders
|
:<|> getRespHeaders
|
||||||
:<|> getDeleteContentType = client api
|
:<|> getDeleteContentType
|
||||||
|
:<|> EmptyClient = client api
|
||||||
|
|
||||||
server :: Application
|
server :: Application
|
||||||
server = serve api (
|
server = serve api (
|
||||||
|
@ -162,7 +166,7 @@ server = serve api (
|
||||||
:<|> (\ a b c d -> return (a, b, c, d))
|
:<|> (\ a b c d -> return (a, b, c, d))
|
||||||
:<|> (return $ addHeader 1729 $ addHeader "eg2" True)
|
:<|> (return $ addHeader 1729 $ addHeader "eg2" True)
|
||||||
:<|> return NoContent
|
:<|> return NoContent
|
||||||
)
|
:<|> emptyServer)
|
||||||
|
|
||||||
|
|
||||||
type FailApi =
|
type FailApi =
|
||||||
|
|
|
@ -683,6 +683,10 @@ instance OVERLAPPABLE_
|
||||||
p2 :: Proxy b
|
p2 :: Proxy b
|
||||||
p2 = Proxy
|
p2 = Proxy
|
||||||
|
|
||||||
|
-- | The generated docs for @'EmptyAPI'@ are empty.
|
||||||
|
instance HasDocs EmptyAPI where
|
||||||
|
docsFor Proxy _ _ = emptyAPI
|
||||||
|
|
||||||
-- | @"books" :> 'Capture' "isbn" Text@ will appear as
|
-- | @"books" :> 'Capture' "isbn" Text@ will appear as
|
||||||
-- @/books/:isbn@ in the docs.
|
-- @/books/:isbn@ in the docs.
|
||||||
instance (KnownSymbol sym, ToCapture (Capture sym a), HasDocs api)
|
instance (KnownSymbol sym, ToCapture (Capture sym a), HasDocs api)
|
||||||
|
|
|
@ -104,6 +104,9 @@ spec = describe "Servant.Docs" $ do
|
||||||
it "contains request body samples" $
|
it "contains request body samples" $
|
||||||
md `shouldContain` "17"
|
md `shouldContain` "17"
|
||||||
|
|
||||||
|
it "does not generate any docs mentioning the 'empty-api' path" $
|
||||||
|
md `shouldNotContain` "empty-api"
|
||||||
|
|
||||||
|
|
||||||
-- * APIs
|
-- * APIs
|
||||||
|
|
||||||
|
@ -128,6 +131,7 @@ instance MimeRender PlainText Int where
|
||||||
type TestApi1 = Get '[JSON, PlainText] (Headers '[Header "Location" String] Int)
|
type TestApi1 = Get '[JSON, PlainText] (Headers '[Header "Location" String] Int)
|
||||||
:<|> ReqBody '[JSON] String :> Post '[JSON] Datatype1
|
:<|> ReqBody '[JSON] String :> Post '[JSON] Datatype1
|
||||||
:<|> Header "X-Test" Int :> Put '[JSON] Int
|
:<|> Header "X-Test" Int :> Put '[JSON] Int
|
||||||
|
:<|> "empty-api" :> EmptyAPI
|
||||||
|
|
||||||
data TT = TT1 | TT2 deriving (Show, Eq)
|
data TT = TT1 | TT2 deriving (Show, Eq)
|
||||||
data UT = UT1 | UT2 deriving (Show, Eq)
|
data UT = UT1 | UT2 deriving (Show, Eq)
|
||||||
|
|
|
@ -187,6 +187,13 @@ instance (HasForeign lang ftype a, HasForeign lang ftype b)
|
||||||
foreignFor lang ftype (Proxy :: Proxy a) req
|
foreignFor lang ftype (Proxy :: Proxy a) req
|
||||||
:<|> foreignFor lang ftype (Proxy :: Proxy b) req
|
:<|> foreignFor lang ftype (Proxy :: Proxy b) req
|
||||||
|
|
||||||
|
data EmptyForeignAPI = EmptyForeignAPI
|
||||||
|
|
||||||
|
instance HasForeign lang ftype EmptyAPI where
|
||||||
|
type Foreign ftype EmptyAPI = EmptyForeignAPI
|
||||||
|
|
||||||
|
foreignFor Proxy Proxy Proxy _ = EmptyForeignAPI
|
||||||
|
|
||||||
instance (KnownSymbol sym, HasForeignType lang ftype t, HasForeign lang ftype api)
|
instance (KnownSymbol sym, HasForeignType lang ftype t, HasForeign lang ftype api)
|
||||||
=> HasForeign lang ftype (Capture sym t :> api) where
|
=> HasForeign lang ftype (Capture sym t :> api) where
|
||||||
type Foreign ftype (Capture sym t :> api) = Foreign ftype api
|
type Foreign ftype (Capture sym t :> api) = Foreign ftype api
|
||||||
|
@ -349,6 +356,9 @@ instance HasForeign lang ftype api
|
||||||
class GenerateList ftype reqs where
|
class GenerateList ftype reqs where
|
||||||
generateList :: reqs -> [Req ftype]
|
generateList :: reqs -> [Req ftype]
|
||||||
|
|
||||||
|
instance GenerateList ftype EmptyForeignAPI where
|
||||||
|
generateList _ = []
|
||||||
|
|
||||||
instance GenerateList ftype (Req ftype) where
|
instance GenerateList ftype (Req ftype) where
|
||||||
generateList r = [r]
|
generateList r = [r]
|
||||||
|
|
||||||
|
|
|
@ -57,13 +57,14 @@ type TestApi
|
||||||
:<|> "test" :> QueryParams "params" Int :> ReqBody '[JSON] String :> Put '[JSON] NoContent
|
:<|> "test" :> QueryParams "params" Int :> ReqBody '[JSON] String :> Put '[JSON] NoContent
|
||||||
:<|> "test" :> Capture "id" Int :> Delete '[JSON] NoContent
|
:<|> "test" :> Capture "id" Int :> Delete '[JSON] NoContent
|
||||||
:<|> "test" :> CaptureAll "ids" Int :> Get '[JSON] [Int]
|
:<|> "test" :> CaptureAll "ids" Int :> Get '[JSON] [Int]
|
||||||
|
:<|> "test" :> EmptyAPI
|
||||||
|
|
||||||
testApi :: [Req String]
|
testApi :: [Req String]
|
||||||
testApi = listFromAPI (Proxy :: Proxy LangX) (Proxy :: Proxy String) (Proxy :: Proxy TestApi)
|
testApi = listFromAPI (Proxy :: Proxy LangX) (Proxy :: Proxy String) (Proxy :: Proxy TestApi)
|
||||||
|
|
||||||
listFromAPISpec :: Spec
|
listFromAPISpec :: Spec
|
||||||
listFromAPISpec = describe "listFromAPI" $ do
|
listFromAPISpec = describe "listFromAPI" $ do
|
||||||
it "generates 4 endpoints for TestApi" $ do
|
it "generates 5 endpoints for TestApi" $ do
|
||||||
length testApi `shouldBe` 5
|
length testApi `shouldBe` 5
|
||||||
|
|
||||||
let [getReq, postReq, putReq, deleteReq, captureAllReq] = testApi
|
let [getReq, postReq, putReq, deleteReq, captureAllReq] = testApi
|
||||||
|
|
|
@ -17,6 +17,8 @@ module Servant.Server
|
||||||
, -- * Handlers for all standard combinators
|
, -- * Handlers for all standard combinators
|
||||||
HasServer(..)
|
HasServer(..)
|
||||||
, Server
|
, Server
|
||||||
|
, EmptyServer
|
||||||
|
, emptyServer
|
||||||
, Handler (..)
|
, Handler (..)
|
||||||
, runHandler
|
, runHandler
|
||||||
|
|
||||||
|
@ -219,8 +221,8 @@ layoutWithContext p context =
|
||||||
--
|
--
|
||||||
-- >>> import Control.Monad.Reader
|
-- >>> import Control.Monad.Reader
|
||||||
-- >>> import qualified Control.Category as C
|
-- >>> import qualified Control.Category as C
|
||||||
-- >>> type ReaderAPI = "ep1" :> Get '[JSON] Int :<|> "ep2" :> Get '[JSON] String :<|> Raw
|
-- >>> type ReaderAPI = "ep1" :> Get '[JSON] Int :<|> "ep2" :> Get '[JSON] String :<|> Raw :<|> EmptyAPI
|
||||||
-- >>> let readerServer = return 1797 :<|> ask :<|> Tagged (error "raw server") :: ServerT ReaderAPI (Reader String)
|
-- >>> let readerServer = return 1797 :<|> ask :<|> Tagged (error "raw server") :<|> emptyServer :: ServerT ReaderAPI (Reader String)
|
||||||
-- >>> let nt = generalizeNat C.. (runReaderTNat "hi") :: Reader String :~> Handler
|
-- >>> let nt = generalizeNat C.. (runReaderTNat "hi") :: Reader String :~> Handler
|
||||||
-- >>> let mainServer = enter nt readerServer :: Server ReaderAPI
|
-- >>> let mainServer = enter nt readerServer :: Server ReaderAPI
|
||||||
--
|
--
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{-# LANGUAGE CPP #-}
|
{-# LANGUAGE CPP #-}
|
||||||
{-# LANGUAGE ConstraintKinds #-}
|
{-# LANGUAGE ConstraintKinds #-}
|
||||||
{-# LANGUAGE DataKinds #-}
|
{-# LANGUAGE DataKinds #-}
|
||||||
|
{-# LANGUAGE DeriveDataTypeable #-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
{-# LANGUAGE FlexibleInstances #-}
|
{-# LANGUAGE FlexibleInstances #-}
|
||||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||||
|
@ -32,7 +33,7 @@ import Data.Maybe (fromMaybe, mapMaybe)
|
||||||
import Data.Either (partitionEithers)
|
import Data.Either (partitionEithers)
|
||||||
import Data.String (fromString)
|
import Data.String (fromString)
|
||||||
import Data.String.Conversions (cs, (<>))
|
import Data.String.Conversions (cs, (<>))
|
||||||
import Data.Tagged (Tagged, untag)
|
import Data.Tagged (Tagged(..), untag)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Data.Typeable
|
import Data.Typeable
|
||||||
import GHC.TypeLits (KnownNat, KnownSymbol, natVal,
|
import GHC.TypeLits (KnownNat, KnownSymbol, natVal,
|
||||||
|
@ -52,7 +53,7 @@ import Web.HttpApiData (FromHttpApiData, parseHeader,
|
||||||
parseUrlPieceMaybe,
|
parseUrlPieceMaybe,
|
||||||
parseUrlPieces)
|
parseUrlPieces)
|
||||||
import Servant.API ((:<|>) (..), (:>), BasicAuth, Capture,
|
import Servant.API ((:<|>) (..), (:>), BasicAuth, Capture,
|
||||||
CaptureAll, Verb,
|
CaptureAll, Verb, EmptyAPI,
|
||||||
ReflectMethod(reflectMethod),
|
ReflectMethod(reflectMethod),
|
||||||
IsSecure(..), Header, QueryFlag,
|
IsSecure(..), Header, QueryFlag,
|
||||||
QueryParam, QueryParams, Raw,
|
QueryParam, QueryParams, Raw,
|
||||||
|
@ -532,6 +533,24 @@ instance HasServer api context => HasServer (HttpVersion :> api) context where
|
||||||
route Proxy context subserver =
|
route Proxy context subserver =
|
||||||
route (Proxy :: Proxy api) context (passToServer subserver httpVersion)
|
route (Proxy :: Proxy api) context (passToServer subserver httpVersion)
|
||||||
|
|
||||||
|
-- | Singleton type representing a server that serves an empty API.
|
||||||
|
data EmptyServer = EmptyServer deriving (Typeable, Eq, Show, Bounded, Enum)
|
||||||
|
|
||||||
|
-- | Server for `EmptyAPI`
|
||||||
|
emptyServer :: ServerT EmptyAPI m
|
||||||
|
emptyServer = Tagged EmptyServer
|
||||||
|
|
||||||
|
-- | The server for an `EmptyAPI` is `emptyAPIServer`.
|
||||||
|
--
|
||||||
|
-- > type MyApi = "nothing" :> EmptyApi
|
||||||
|
-- >
|
||||||
|
-- > server :: Server MyApi
|
||||||
|
-- > server = emptyAPIServer
|
||||||
|
instance HasServer EmptyAPI context where
|
||||||
|
type ServerT EmptyAPI m = Tagged m EmptyServer
|
||||||
|
|
||||||
|
route Proxy _ _ = StaticRouter mempty mempty
|
||||||
|
|
||||||
-- | Basic Authentication
|
-- | Basic Authentication
|
||||||
instance ( KnownSymbol realm
|
instance ( KnownSymbol realm
|
||||||
, HasServer api context
|
, HasServer api context
|
||||||
|
|
|
@ -42,14 +42,14 @@ import Servant.API ((:<|>) (..), (:>), AuthProtect,
|
||||||
Headers, HttpVersion,
|
Headers, HttpVersion,
|
||||||
IsSecure (..), JSON,
|
IsSecure (..), JSON,
|
||||||
NoContent (..), Patch, PlainText,
|
NoContent (..), Patch, PlainText,
|
||||||
Post, Put,
|
Post, Put, EmptyAPI,
|
||||||
QueryFlag, QueryParam, QueryParams,
|
QueryFlag, QueryParam, QueryParams,
|
||||||
Raw, RemoteHost, ReqBody,
|
Raw, RemoteHost, ReqBody,
|
||||||
StdMethod (..), Verb, addHeader)
|
StdMethod (..), Verb, addHeader)
|
||||||
import Servant.API.Internal.Test.ComprehensiveAPI
|
import Servant.API.Internal.Test.ComprehensiveAPI
|
||||||
import Servant.Server (Server, Handler, Tagged (..), err401, err403,
|
import Servant.Server (Server, Handler, Tagged (..), err401, err403,
|
||||||
err404, serve, serveWithContext,
|
err404, serve, serveWithContext,
|
||||||
Context((:.), EmptyContext))
|
Context((:.), EmptyContext), emptyServer)
|
||||||
import Test.Hspec (Spec, context, describe, it,
|
import Test.Hspec (Spec, context, describe, it,
|
||||||
shouldBe, shouldContain)
|
shouldBe, shouldContain)
|
||||||
import qualified Test.Hspec.Wai as THW
|
import qualified Test.Hspec.Wai as THW
|
||||||
|
@ -609,6 +609,7 @@ type MiscCombinatorsAPI
|
||||||
= "version" :> HttpVersion :> Get '[JSON] String
|
= "version" :> HttpVersion :> Get '[JSON] String
|
||||||
:<|> "secure" :> IsSecure :> Get '[JSON] String
|
:<|> "secure" :> IsSecure :> Get '[JSON] String
|
||||||
:<|> "host" :> RemoteHost :> Get '[JSON] String
|
:<|> "host" :> RemoteHost :> Get '[JSON] String
|
||||||
|
:<|> "empty" :> EmptyAPI
|
||||||
|
|
||||||
miscApi :: Proxy MiscCombinatorsAPI
|
miscApi :: Proxy MiscCombinatorsAPI
|
||||||
miscApi = Proxy
|
miscApi = Proxy
|
||||||
|
@ -617,6 +618,7 @@ miscServ :: Server MiscCombinatorsAPI
|
||||||
miscServ = versionHandler
|
miscServ = versionHandler
|
||||||
:<|> secureHandler
|
:<|> secureHandler
|
||||||
:<|> hostHandler
|
:<|> hostHandler
|
||||||
|
:<|> emptyServer
|
||||||
|
|
||||||
where versionHandler = return . show
|
where versionHandler = return . show
|
||||||
secureHandler Secure = return "secure"
|
secureHandler Secure = return "secure"
|
||||||
|
@ -635,6 +637,9 @@ miscCombinatorSpec = with (return $ serve miscApi miscServ) $
|
||||||
it "Checks that hspec-wai issues request from 0.0.0.0" $
|
it "Checks that hspec-wai issues request from 0.0.0.0" $
|
||||||
go "/host" "\"0.0.0.0:0\""
|
go "/host" "\"0.0.0.0:0\""
|
||||||
|
|
||||||
|
it "Doesn't serve anything from the empty API" $
|
||||||
|
Test.Hspec.Wai.get "empty" `shouldRespondWith` 404
|
||||||
|
|
||||||
where go path res = Test.Hspec.Wai.get path `shouldRespondWith` res
|
where go path res = Test.Hspec.Wai.get path `shouldRespondWith` res
|
||||||
|
|
||||||
-- }}}
|
-- }}}
|
||||||
|
|
|
@ -36,6 +36,7 @@ library
|
||||||
Servant.API.BasicAuth
|
Servant.API.BasicAuth
|
||||||
Servant.API.Capture
|
Servant.API.Capture
|
||||||
Servant.API.ContentTypes
|
Servant.API.ContentTypes
|
||||||
|
Servant.API.Empty
|
||||||
Servant.API.Experimental.Auth
|
Servant.API.Experimental.Auth
|
||||||
Servant.API.Header
|
Servant.API.Header
|
||||||
Servant.API.HttpVersion
|
Servant.API.HttpVersion
|
||||||
|
|
|
@ -5,6 +5,8 @@ module Servant.API (
|
||||||
-- | Type-level combinator for expressing subrouting: @':>'@
|
-- | Type-level combinator for expressing subrouting: @':>'@
|
||||||
module Servant.API.Alternative,
|
module Servant.API.Alternative,
|
||||||
-- | Type-level combinator for alternative endpoints: @':<|>'@
|
-- | Type-level combinator for alternative endpoints: @':<|>'@
|
||||||
|
module Servant.API.Empty,
|
||||||
|
-- | Type-level combinator for an empty API: @'EmptyAPI'@
|
||||||
|
|
||||||
-- * Accessing information from the request
|
-- * Accessing information from the request
|
||||||
module Servant.API.Capture,
|
module Servant.API.Capture,
|
||||||
|
@ -66,6 +68,7 @@ import Servant.API.ContentTypes (Accept (..), FormUrlEncoded,
|
||||||
MimeRender (..), NoContent (NoContent),
|
MimeRender (..), NoContent (NoContent),
|
||||||
MimeUnrender (..), OctetStream,
|
MimeUnrender (..), OctetStream,
|
||||||
PlainText)
|
PlainText)
|
||||||
|
import Servant.API.Empty (EmptyAPI (..))
|
||||||
import Servant.API.Experimental.Auth (AuthProtect)
|
import Servant.API.Experimental.Auth (AuthProtect)
|
||||||
import Servant.API.Header (Header (..))
|
import Servant.API.Header (Header (..))
|
||||||
import Servant.API.HttpVersion (HttpVersion (..))
|
import Servant.API.HttpVersion (HttpVersion (..))
|
||||||
|
|
12
servant/src/Servant/API/Empty.hs
Normal file
12
servant/src/Servant/API/Empty.hs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{-# LANGUAGE DeriveDataTypeable #-}
|
||||||
|
{-# OPTIONS_HADDOCK not-home #-}
|
||||||
|
module Servant.API.Empty(EmptyAPI(..)) where
|
||||||
|
|
||||||
|
import Data.Typeable (Typeable)
|
||||||
|
import Prelude ()
|
||||||
|
import Prelude.Compat
|
||||||
|
|
||||||
|
-- | An empty API: one which serves nothing. Morally speaking, this should be
|
||||||
|
-- the unit of ':<|>'. Implementors of interpretations of API types should
|
||||||
|
-- treat 'EmptyAPI' as close to the unit as possible.
|
||||||
|
data EmptyAPI = EmptyAPI deriving (Typeable, Eq, Show, Bounded, Enum)
|
|
@ -37,7 +37,8 @@ type ComprehensiveAPIWithoutRaw =
|
||||||
Verb 'POST 204 '[JSON] NoContent :<|>
|
Verb 'POST 204 '[JSON] NoContent :<|>
|
||||||
Verb 'POST 204 '[JSON] Int :<|>
|
Verb 'POST 204 '[JSON] Int :<|>
|
||||||
WithNamedContext "foo" '[] GET :<|>
|
WithNamedContext "foo" '[] GET :<|>
|
||||||
CaptureAll "foo" Int :> GET
|
CaptureAll "foo" Int :> GET :<|>
|
||||||
|
EmptyAPI
|
||||||
|
|
||||||
comprehensiveAPIWithoutRaw :: Proxy ComprehensiveAPIWithoutRaw
|
comprehensiveAPIWithoutRaw :: Proxy ComprehensiveAPIWithoutRaw
|
||||||
comprehensiveAPIWithoutRaw = Proxy
|
comprehensiveAPIWithoutRaw = Proxy
|
||||||
|
|
|
@ -25,6 +25,7 @@ type TestApi =
|
||||||
:<|> "post" :> ReqBody '[JSON] 'True :> Post '[JSON] NoContent
|
:<|> "post" :> ReqBody '[JSON] 'True :> Post '[JSON] NoContent
|
||||||
:<|> "delete" :> Header "ponies" String :> Delete '[JSON] NoContent
|
:<|> "delete" :> Header "ponies" String :> Delete '[JSON] NoContent
|
||||||
:<|> "raw" :> Raw
|
:<|> "raw" :> Raw
|
||||||
|
:<|> NoEndpoint
|
||||||
|
|
||||||
|
|
||||||
apiLink :: (IsElem endpoint TestApi, HasLink endpoint)
|
apiLink :: (IsElem endpoint TestApi, HasLink endpoint)
|
||||||
|
@ -98,6 +99,11 @@ spec = describe "Servant.Utils.Links" $ do
|
||||||
-- ...Could not deduce...
|
-- ...Could not deduce...
|
||||||
-- ...
|
-- ...
|
||||||
--
|
--
|
||||||
|
-- >>> apiLink (Proxy :: Proxy NoEndpoint)
|
||||||
|
-- ...
|
||||||
|
-- ...No instance for...
|
||||||
|
-- ...
|
||||||
|
--
|
||||||
-- sanity check
|
-- sanity check
|
||||||
-- >>> toUrlPiece $ apiLink (Proxy :: Proxy AllGood)
|
-- >>> toUrlPiece $ apiLink (Proxy :: Proxy AllGood)
|
||||||
-- "get"
|
-- "get"
|
||||||
|
@ -107,3 +113,4 @@ type WrongContentType = "get" :> Get '[OctetStream] NoContent
|
||||||
type WrongMethod = "get" :> Post '[JSON] NoContent
|
type WrongMethod = "get" :> Post '[JSON] NoContent
|
||||||
type NotALink = "hello" :> ReqBody '[JSON] 'True :> Get '[JSON] Bool
|
type NotALink = "hello" :> ReqBody '[JSON] 'True :> Get '[JSON] Bool
|
||||||
type AllGood = "get" :> Get '[JSON] NoContent
|
type AllGood = "get" :> Get '[JSON] NoContent
|
||||||
|
type NoEndpoint = "empty" :> EmptyAPI
|
||||||
|
|
Loading…
Reference in a new issue