Merge pull request #1228 from haskell-servant/pull-1219-no-content-verb-1028

Pull 1219: no content verb 1028
This commit is contained in:
Oleg Grenrus 2019-09-29 21:22:23 +03:00 committed by GitHub
commit 002ee3cd8f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 105 additions and 46 deletions

View file

@ -177,13 +177,12 @@ type UserAPI5 = "user" :> Capture "userid" Integer :> Get '[JSON] User
-- except that we explicitly say that "userid" -- except that we explicitly say that "userid"
-- must be an integer -- must be an integer
:<|> "user" :> Capture "userid" Integer :> DeleteNoContent '[JSON] NoContent :<|> "user" :> Capture "userid" Integer :> DeleteNoContent
-- equivalent to 'DELETE /user/:userid' -- equivalent to 'DELETE /user/:userid'
``` ```
In the second case, `DeleteNoContent` specifies a 204 response code, In the second case, `DeleteNoContent` specifies a 204 response code
`JSON` specifies the content types on which the handler will match, and that the response will always be empty.
and `NoContent` says that the response will always be empty.
### `QueryParam`, `QueryParams`, `QueryFlag` ### `QueryParam`, `QueryParams`, `QueryFlag`

View file

@ -830,7 +830,7 @@ type UserAPI3 = -- view the user with given userid, in JSON
Capture "userid" Int :> Get '[JSON] User Capture "userid" Int :> Get '[JSON] User
:<|> -- delete the user with given userid. empty response :<|> -- delete the user with given userid. empty response
Capture "userid" Int :> DeleteNoContent '[JSON] NoContent Capture "userid" Int :> DeleteNoContent
``` ```
We can instead factor out the `userid`: We can instead factor out the `userid`:
@ -838,7 +838,7 @@ We can instead factor out the `userid`:
``` haskell ``` haskell
type UserAPI4 = Capture "userid" Int :> type UserAPI4 = Capture "userid" Int :>
( Get '[JSON] User ( Get '[JSON] User
:<|> DeleteNoContent '[JSON] NoContent :<|> DeleteNoContent
) )
``` ```
@ -896,13 +896,13 @@ type API1 = "users" :>
-- we factor out the Request Body -- we factor out the Request Body
type API2 = ReqBody '[JSON] User :> type API2 = ReqBody '[JSON] User :>
( Get '[JSON] User -- just display the same user back, don't register it ( Get '[JSON] User -- just display the same user back, don't register it
:<|> PostNoContent '[JSON] NoContent -- register the user. empty response :<|> PostNoContent -- register the user. empty response
) )
-- we factor out a Header -- we factor out a Header
type API3 = Header "Authorization" Token :> type API3 = Header "Authorization" Token :>
( Get '[JSON] SecretData -- get some secret data, if authorized ( Get '[JSON] SecretData -- get some secret data, if authorized
:<|> ReqBody '[JSON] SecretData :> PostNoContent '[JSON] NoContent -- add some secret data, if authorized :<|> ReqBody '[JSON] SecretData :> PostNoContent -- add some secret data, if authorized
) )
newtype Token = Token ByteString newtype Token = Token ByteString
@ -915,11 +915,11 @@ API type only at the end.
``` haskell ``` haskell
type UsersAPI = type UsersAPI =
Get '[JSON] [User] -- list users Get '[JSON] [User] -- list users
:<|> ReqBody '[JSON] User :> PostNoContent '[JSON] NoContent -- add a user :<|> ReqBody '[JSON] User :> PostNoContent -- add a user
:<|> Capture "userid" Int :> :<|> Capture "userid" Int :>
( Get '[JSON] User -- view a user ( Get '[JSON] User -- view a user
:<|> ReqBody '[JSON] User :> PutNoContent '[JSON] NoContent -- update a user :<|> ReqBody '[JSON] User :> PutNoContent -- update a user
:<|> DeleteNoContent '[JSON] NoContent -- delete a user :<|> DeleteNoContent -- delete a user
) )
usersServer :: Server UsersAPI usersServer :: Server UsersAPI
@ -948,11 +948,11 @@ usersServer = getUsers :<|> newUser :<|> userOperations
``` haskell ``` haskell
type ProductsAPI = type ProductsAPI =
Get '[JSON] [Product] -- list products Get '[JSON] [Product] -- list products
:<|> ReqBody '[JSON] Product :> PostNoContent '[JSON] NoContent -- add a product :<|> ReqBody '[JSON] Product :> PostNoContent -- add a product
:<|> Capture "productid" Int :> :<|> Capture "productid" Int :>
( Get '[JSON] Product -- view a product ( Get '[JSON] Product -- view a product
:<|> ReqBody '[JSON] Product :> PutNoContent '[JSON] NoContent -- update a product :<|> ReqBody '[JSON] Product :> PutNoContent -- update a product
:<|> DeleteNoContent '[JSON] NoContent -- delete a product :<|> DeleteNoContent -- delete a product
) )
data Product = Product { productId :: Int } data Product = Product { productId :: Int }
@ -996,11 +996,11 @@ abstract that away:
-- indexed by values of type 'i' -- indexed by values of type 'i'
type APIFor a i = type APIFor a i =
Get '[JSON] [a] -- list 'a's Get '[JSON] [a] -- list 'a's
:<|> ReqBody '[JSON] a :> PostNoContent '[JSON] NoContent -- add an 'a' :<|> ReqBody '[JSON] a :> PostNoContent -- add an 'a'
:<|> Capture "id" i :> :<|> Capture "id" i :>
( Get '[JSON] a -- view an 'a' given its "identifier" of type 'i' ( Get '[JSON] a -- view an 'a' given its "identifier" of type 'i'
:<|> ReqBody '[JSON] a :> PutNoContent '[JSON] NoContent -- update an 'a' :<|> ReqBody '[JSON] a :> PutNoContent -- update an 'a'
:<|> DeleteNoContent '[JSON] NoContent -- delete an 'a' :<|> DeleteNoContent -- delete an 'a'
) )
-- Build the appropriate 'Server' -- Build the appropriate 'Server'

View file

@ -50,8 +50,9 @@ import Servant.API
MimeUnrender (mimeUnrender), NoContent (NoContent), QueryFlag, MimeUnrender (mimeUnrender), NoContent (NoContent), QueryFlag,
QueryParam', QueryParams, Raw, ReflectMethod (..), RemoteHost, QueryParam', QueryParams, Raw, ReflectMethod (..), RemoteHost,
ReqBody', SBoolI, Stream, StreamBody', Summary, ToHttpApiData, ReqBody', SBoolI, Stream, StreamBody', Summary, ToHttpApiData,
ToSourceIO (..), Vault, Verb, WithNamedContext, contentType, ToSourceIO (..), Vault, Verb, NoContentVerb, WithNamedContext,
getHeadersHList, getResponse, toQueryParam, toUrlPiece) contentType, getHeadersHList, getResponse, toQueryParam,
toUrlPiece)
import Servant.API.ContentTypes import Servant.API.ContentTypes
(contentTypes) (contentTypes)
import Servant.API.Modifiers import Servant.API.Modifiers
@ -241,6 +242,17 @@ instance {-# OVERLAPPING #-}
hoistClientMonad _ _ f ma = f ma hoistClientMonad _ _ f ma = f ma
instance (RunClient m, ReflectMethod method) =>
HasClient m (NoContentVerb method) where
type Client m (NoContentVerb method)
= m NoContent
clientWithRoute _pm Proxy req = do
_response <- runRequest req { requestMethod = method }
return NoContent
where method = reflectMethod (Proxy :: Proxy method)
hoistClientMonad _ _ f ma = f ma
instance {-# OVERLAPPING #-} instance {-# OVERLAPPING #-}
-- Note [Non-Empty Content Types] -- Note [Non-Empty Content Types]
( RunClient m, MimeUnrender ct a, BuildHeadersTo ls ( RunClient m, MimeUnrender ct a, BuildHeadersTo ls

View file

@ -85,7 +85,7 @@ type TestHeaders = '[Header "X-Example1" Int, Header "X-Example2" String]
type Api = type Api =
Get '[JSON] Person Get '[JSON] Person
:<|> "get" :> Get '[JSON] Person :<|> "get" :> Get '[JSON] Person
:<|> "deleteEmpty" :> DeleteNoContent '[JSON] NoContent :<|> "deleteEmpty" :> DeleteNoContent
:<|> "capture" :> Capture "name" String :> Get '[JSON,FormUrlEncoded] Person :<|> "capture" :> Capture "name" String :> Get '[JSON,FormUrlEncoded] Person
:<|> "captureAll" :> CaptureAll "names" String :> Get '[JSON] [Person] :<|> "captureAll" :> CaptureAll "names" String :> Get '[JSON] [Person]
:<|> "body" :> ReqBody '[FormUrlEncoded,JSON] Person :> Post '[JSON] Person :<|> "body" :> ReqBody '[FormUrlEncoded,JSON] Person :> Post '[JSON] Person
@ -101,7 +101,7 @@ type Api =
ReqBody '[JSON] [(String, [Rational])] :> ReqBody '[JSON] [(String, [Rational])] :>
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
:<|> "redirectWithCookie" :> Raw :<|> "redirectWithCookie" :> Raw
:<|> "empty" :> EmptyAPI :<|> "empty" :> EmptyAPI

View file

@ -399,16 +399,7 @@
- Status code 204 - Status code 204
- Headers: [] - Headers: []
- Supported content types are: - No response body
- `application/json;charset=utf-8`
- `application/json`
- Example (`application/json;charset=utf-8`, `application/json`):
```javascript
```
## GET /raw ## GET /raw

View file

@ -862,6 +862,18 @@ instance {-# OVERLAPPABLE #-}
status = fromInteger $ natVal (Proxy :: Proxy status) status = fromInteger $ natVal (Proxy :: Proxy status)
p = Proxy :: Proxy a p = Proxy :: Proxy a
instance (ReflectMethod method) =>
HasDocs (NoContentVerb method) where
docsFor Proxy (endpoint, action) DocOptions{..} =
single endpoint' action'
where endpoint' = endpoint & method .~ method'
action' = action & response.respStatus .~ 204
& response.respTypes .~ []
& response.respBody .~ []
& response.respHeaders .~ []
method' = reflectMethod (Proxy :: Proxy method)
-- | TODO: mention the endpoint is streaming, its framing strategy -- | TODO: mention the endpoint is streaming, its framing strategy
-- --
-- Also there are no samples. -- Also there are no samples.

View file

@ -244,6 +244,19 @@ instance (Elem JSON list, HasForeignType lang ftype a, ReflectMethod method)
method = reflectMethod (Proxy :: Proxy method) method = reflectMethod (Proxy :: Proxy method)
methodLC = toLower $ decodeUtf8 method methodLC = toLower $ decodeUtf8 method
instance (HasForeignType lang ftype NoContent, ReflectMethod method)
=> HasForeign lang ftype (NoContentVerb method) where
type Foreign ftype (NoContentVerb method) = Req ftype
foreignFor lang Proxy Proxy req =
req & reqFuncName . _FunctionName %~ (methodLC :)
& reqMethod .~ method
& reqReturnType .~ Just retType
where
retType = typeFor lang (Proxy :: Proxy ftype) (Proxy :: Proxy NoContent)
method = reflectMethod (Proxy :: Proxy method)
methodLC = toLower $ decodeUtf8 method
-- | TODO: doesn't taking framing into account. -- | TODO: doesn't taking framing into account.
instance (ct ~ JSON, HasForeignType lang ftype a, ReflectMethod method) instance (ct ~ JSON, HasForeignType lang ftype a, ReflectMethod method)
=> HasForeign lang ftype (Stream method status framing ct a) where => HasForeign lang ftype (Stream method status framing ct a) where

View file

@ -115,7 +115,7 @@ type TestHeaders = '[Header "X-Example1" Int, Header "X-Example2" String]
type Api = type Api =
Get '[JSON] Person Get '[JSON] Person
:<|> "get" :> Get '[JSON] Person :<|> "get" :> Get '[JSON] Person
:<|> "deleteEmpty" :> DeleteNoContent '[JSON] NoContent :<|> "deleteEmpty" :> DeleteNoContent
:<|> "capture" :> Capture "name" String :> Get '[JSON,FormUrlEncoded] Person :<|> "capture" :> Capture "name" String :> Get '[JSON,FormUrlEncoded] Person
:<|> "captureAll" :> CaptureAll "names" String :> Get '[JSON] [Person] :<|> "captureAll" :> CaptureAll "names" String :> Get '[JSON] [Person]
:<|> "body" :> ReqBody '[FormUrlEncoded,JSON] Person :> Post '[JSON] Person :<|> "body" :> ReqBody '[FormUrlEncoded,JSON] Person :> Post '[JSON] Person
@ -131,7 +131,7 @@ type Api =
ReqBody '[JSON] [(String, [Rational])] :> ReqBody '[JSON] [(String, [Rational])] :>
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
:<|> "redirectWithCookie" :> Raw :<|> "redirectWithCookie" :> Raw
:<|> "empty" :> EmptyAPI :<|> "empty" :> EmptyAPI

View file

@ -75,10 +75,12 @@ import Servant.API
IsSecure (..), QueryFlag, QueryParam', QueryParams, Raw, IsSecure (..), QueryFlag, QueryParam', QueryParams, Raw,
ReflectMethod (reflectMethod), RemoteHost, ReqBody', ReflectMethod (reflectMethod), RemoteHost, ReqBody',
SBool (..), SBoolI (..), SourceIO, Stream, StreamBody', SBool (..), SBoolI (..), SourceIO, Stream, StreamBody',
Summary, ToSourceIO (..), Vault, Verb, WithNamedContext) Summary, ToSourceIO (..), Vault, Verb, NoContentVerb,
WithNamedContext)
import Servant.API.ContentTypes import Servant.API.ContentTypes
(AcceptHeader (..), AllCTRender (..), AllCTUnrender (..), (AcceptHeader (..), AllCTRender (..), AllCTUnrender (..),
AllMime, MimeRender (..), MimeUnrender (..), canHandleAcceptH) AllMime, MimeRender (..), MimeUnrender (..), canHandleAcceptH,
NoContent (NoContent))
import Servant.API.Modifiers import Servant.API.Modifiers
(FoldLenient, FoldRequired, RequestArgument, (FoldLenient, FoldRequired, RequestArgument,
unfoldRequestArgument) unfoldRequestArgument)
@ -262,6 +264,17 @@ methodRouter splitHeaders method proxy status action = leafRouter route'
let bdy = if allowedMethodHead method request then "" else body let bdy = if allowedMethodHead method request then "" else body
in Route $ responseLBS status ((hContentType, cs contentT) : headers) bdy in Route $ responseLBS status ((hContentType, cs contentT) : headers) bdy
noContentRouter :: Method
-> Status
-> Delayed env (Handler b)
-> Router env
noContentRouter method status action = leafRouter route'
where
route' env request respond =
runAction (action `addMethodCheck` methodCheck method request)
env request respond $ \ output ->
Route $ responseLBS status [] ""
instance {-# OVERLAPPABLE #-} instance {-# OVERLAPPABLE #-}
( AllCTRender ctypes a, ReflectMethod method, KnownNat status ( AllCTRender ctypes a, ReflectMethod method, KnownNat status
) => HasServer (Verb method status ctypes a) context where ) => HasServer (Verb method status ctypes a) context where
@ -285,6 +298,14 @@ instance {-# OVERLAPPING #-}
where method = reflectMethod (Proxy :: Proxy method) where method = reflectMethod (Proxy :: Proxy method)
status = toEnum . fromInteger $ natVal (Proxy :: Proxy status) status = toEnum . fromInteger $ natVal (Proxy :: Proxy status)
instance (ReflectMethod method) =>
HasServer (NoContentVerb method) context where
type ServerT (NoContentVerb method) m = m NoContent
hoistServerWithContext _ _ nt s = nt s
route Proxy _ = noContentRouter method status204
where method = reflectMethod (Proxy :: Proxy method)
instance {-# OVERLAPPABLE #-} instance {-# OVERLAPPABLE #-}
( MimeRender ctype chunk, ReflectMethod method, KnownNat status, ( MimeRender ctype chunk, ReflectMethod method, KnownNat status,

View file

@ -51,7 +51,7 @@ import Servant.API
JSON, NoContent (..), NoFraming, OctetStream, Patch, JSON, NoContent (..), NoFraming, OctetStream, Patch,
PlainText, Post, Put, QueryFlag, QueryParam, QueryParams, Raw, PlainText, Post, Put, QueryFlag, QueryParam, QueryParams, Raw,
RemoteHost, ReqBody, SourceIO, StdMethod (..), Stream, Verb, RemoteHost, ReqBody, SourceIO, StdMethod (..), Stream, Verb,
addHeader) NoContentVerb, addHeader)
import Servant.Server import Servant.Server
(Context ((:.), EmptyContext), Handler, Server, Tagged (..), (Context ((:.), EmptyContext), Handler, Server, Tagged (..),
emptyServer, err400, err401, err403, err404, serve, serveWithContext) emptyServer, err400, err401, err403, err404, serve, serveWithContext)
@ -103,7 +103,7 @@ spec = do
type VerbApi method status type VerbApi method status
= Verb method status '[JSON] Person = Verb method status '[JSON] Person
:<|> "noContent" :> Verb method status '[JSON] NoContent :<|> "noContent" :> NoContentVerb method
:<|> "header" :> Verb method status '[JSON] (Headers '[Header "H" Int] Person) :<|> "header" :> Verb method status '[JSON] (Headers '[Header "H" Int] Person)
:<|> "headerNC" :> Verb method status '[JSON] (Headers '[Header "H" Int] NoContent) :<|> "headerNC" :> Verb method status '[JSON] (Headers '[Header "H" Int] NoContent)
:<|> "accept" :> ( Verb method status '[JSON] Person :<|> "accept" :> ( Verb method status '[JSON] Person
@ -140,7 +140,7 @@ verbSpec = describe "Servant.API.Verb" $ do
it "returns no content on NoContent" $ do it "returns no content on NoContent" $ do
response <- THW.request method "/noContent" [] "" response <- THW.request method "/noContent" [] ""
liftIO $ statusCode (simpleStatus response) `shouldBe` status liftIO $ statusCode (simpleStatus response) `shouldBe` 204
liftIO $ simpleBody response `shouldBe` "" liftIO $ simpleBody response `shouldBe` ""
-- HEAD should not return body -- HEAD should not return body

View file

@ -130,7 +130,8 @@ import Servant.API.Verbs
Post, PostAccepted, PostCreated, PostNoContent, Post, PostAccepted, PostCreated, PostNoContent,
PostNonAuthoritative, PostResetContent, Put, PutAccepted, PostNonAuthoritative, PostResetContent, Put, PutAccepted,
PutCreated, PutNoContent, PutNonAuthoritative, PutCreated, PutNoContent, PutNonAuthoritative,
ReflectMethod (reflectMethod), StdMethod (..), Verb) ReflectMethod (reflectMethod), StdMethod (..),
Verb, NoContentVerb)
import Servant.API.WithNamedContext import Servant.API.WithNamedContext
(WithNamedContext) (WithNamedContext)
import Servant.Links import Servant.Links

View file

@ -29,6 +29,12 @@ import Network.HTTP.Types.Method
data Verb (method :: k1) (statusCode :: Nat) (contentTypes :: [*]) (a :: *) data Verb (method :: k1) (statusCode :: Nat) (contentTypes :: [*]) (a :: *)
deriving (Typeable, Generic) deriving (Typeable, Generic)
-- | @NoContentVerb@ is a specific type to represent 'NoContent' responses.
-- It does not require either a list of content types (because there's
-- no content) or a status code (because it should always be 204).
data NoContentVerb (method :: k1)
deriving (Typeable, Generic)
-- * 200 responses -- * 200 responses
-- --
-- The 200 response is the workhorse of web servers, but also fairly generic. -- The 200 response is the workhorse of web servers, but also fairly generic.
@ -113,15 +119,15 @@ type PutNonAuthoritative = Verb 'PUT 203
-- If the document view should be reset, use @205 Reset Content@. -- If the document view should be reset, use @205 Reset Content@.
-- | 'GET' with 204 status code. -- | 'GET' with 204 status code.
type GetNoContent = Verb 'GET 204 type GetNoContent = NoContentVerb 'GET
-- | 'POST' with 204 status code. -- | 'POST' with 204 status code.
type PostNoContent = Verb 'POST 204 type PostNoContent = NoContentVerb 'POST
-- | 'DELETE' with 204 status code. -- | 'DELETE' with 204 status code.
type DeleteNoContent = Verb 'DELETE 204 type DeleteNoContent = NoContentVerb 'DELETE
-- | 'PATCH' with 204 status code. -- | 'PATCH' with 204 status code.
type PatchNoContent = Verb 'PATCH 204 type PatchNoContent = NoContentVerb 'PATCH
-- | 'PUT' with 204 status code. -- | 'PUT' with 204 status code.
type PutNoContent = Verb 'PUT 204 type PutNoContent = NoContentVerb 'PUT
-- ** 205 Reset Content -- ** 205 Reset Content

View file

@ -177,7 +177,7 @@ import Servant.API.TypeLevel
import Servant.API.Vault import Servant.API.Vault
(Vault) (Vault)
import Servant.API.Verbs import Servant.API.Verbs
(Verb) (Verb, NoContentVerb)
import Servant.API.WithNamedContext import Servant.API.WithNamedContext
(WithNamedContext) (WithNamedContext)
import Web.HttpApiData import Web.HttpApiData
@ -546,6 +546,10 @@ instance HasLink (Verb m s ct a) where
type MkLink (Verb m s ct a) r = r type MkLink (Verb m s ct a) r = r
toLink toA _ = toA toLink toA _ = toA
instance HasLink (NoContentVerb m) where
type MkLink (NoContentVerb m) r = r
toLink toA _ = toA
instance HasLink Raw where instance HasLink Raw where
type MkLink Raw a = a type MkLink Raw a = a
toLink toA _ = toA toLink toA _ = toA

View file

@ -64,7 +64,7 @@ type ComprehensiveAPIWithoutStreamingOrRaw' endpoint =
:<|> "res-headers" :> Get '[JSON] (Headers '[Header "foo" Int] NoContent) :<|> "res-headers" :> Get '[JSON] (Headers '[Header "foo" Int] NoContent)
:<|> "foo" :> GET :<|> "foo" :> GET
:<|> "vault" :> Vault :> GET :<|> "vault" :> Vault :> GET
:<|> "post-no-content" :> Verb 'POST 204 '[JSON] NoContent :<|> "post-no-content" :> PostNoContent
:<|> "post-int" :> Verb 'POST 204 '[JSON] Int :<|> "post-int" :> Verb 'POST 204 '[JSON] Int
:<|> "named-context" :> WithNamedContext "foo" '[] GET :<|> "named-context" :> WithNamedContext "foo" '[] GET
:<|> "capture-all" :> CaptureAll "foo" Int :> GET :<|> "capture-all" :> CaptureAll "foo" Int :> GET