Skip to content

Commit abeda23

Browse files
committed
Make Verb and NoContentVerb HasServer instances delegate to MultiVerb
This is the first step toward unifying endpoint types around MultiVerb. Changes: - Verb's HasServer instances now delegate to MultiVerb - NoContentVerb delegates to MultiVerb with RespondAs '() 204 - Verb and NoContentVerb method parameter changed from polymorphic (k1) to strict (StdMethod) for consistency with MultiVerb. This is a breaking change, but arguably not a major one: I doubt that this extra polymorphism was ever used. - Added KnownStatus constraint to Verb instances. We previously only required `KnownNat`, but we need `KnownStatus` to be able to express `Verb` in terms of `MultiVerb. This is another potential breaking change: users using non-standard, custom statuses will have to implement `KnownStatus` instances. - Added ResponseRender instance for Respond with Headers to support the delegation Removed dead code: - methodRouter (was only used by Verb) - noContentRouter (was only used by NoContentVerb) - responseLBS import (no longer needed) Test changes: - Added KnownStatus instances for non-standard test status codes (210, 214, 280)
1 parent e5610be commit abeda23

File tree

5 files changed

+53
-65
lines changed

5 files changed

+53
-65
lines changed

servant-server/src/Servant/Server/Internal.hs

Lines changed: 12 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ module Servant.Server.Internal
1616
where
1717

1818
import Control.Applicative ((<|>))
19-
import Control.Monad (join, unless, when)
19+
import Control.Monad (join, unless, void, when)
2020
import Control.Monad.Trans (lift, liftIO)
2121
import Control.Monad.Trans.Resource (ReleaseKey, runResourceT)
2222
import Data.Acquire
@@ -60,7 +60,6 @@ import Network.Wai
6060
, requestHeaderHost
6161
, requestHeaders
6262
, requestMethod
63-
, responseLBS
6463
, responseStream
6564
, vault
6665
)
@@ -111,6 +110,7 @@ import Servant.API.ContentTypes
111110
, AllCTRender (..)
112111
, AllCTUnrender (..)
113112
, AllMime
113+
, AllMimeRender
114114
, MimeRender (..)
115115
, MimeUnrender (..)
116116
, NoContent
@@ -138,7 +138,7 @@ import Servant.API.ResponseHeaders
138138
, getHeaders
139139
, getResponse
140140
)
141-
import Servant.API.Status (statusFromNat)
141+
import Servant.API.Status (KnownStatus, statusFromNat)
142142
import Servant.API.TypeErrors
143143
import Servant.API.TypeLevel (AtMostOneFragment, FragmentUnique)
144144
import qualified Servant.Types.SourceT as S
@@ -369,82 +369,34 @@ acceptCheck proxy accH
369369
| canHandleAcceptH proxy accH = pure ()
370370
| otherwise = delayedFail err406
371371

372-
methodRouter
373-
:: AllCTRender ctypes a
374-
=> (b -> ([(HeaderName, B.ByteString)], a))
375-
-> Method
376-
-> Proxy ctypes
377-
-> Status
378-
-> Delayed env (Handler b)
379-
-> Router env
380-
methodRouter splitHeaders method proxy status action = leafRouter route'
381-
where
382-
route' env request respond =
383-
let accH = getAcceptHeader request
384-
in runAction
385-
( action
386-
`addMethodCheck` methodCheck method request
387-
`addAcceptCheck` acceptCheck proxy accH
388-
)
389-
env
390-
request
391-
respond
392-
$ \output -> do
393-
let (headers, b) = splitHeaders output
394-
case handleAcceptH proxy accH b of
395-
Nothing -> FailFatal err406 -- this should not happen (checked before), so we make it fatal if it does
396-
Just (contentT, body) ->
397-
let bdy = if allowedMethodHead method request then "" else body
398-
in Route $ responseLBS status ((hContentType, BSL.toStrict contentT) : headers) bdy
399-
400-
noContentRouter
401-
:: Method
402-
-> Status
403-
-> Delayed env (Handler b)
404-
-> Router env
405-
noContentRouter method status action = leafRouter route'
406-
where
407-
route' env request respond =
408-
runAction
409-
(action `addMethodCheck` methodCheck method request)
410-
env
411-
request
412-
respond
413-
$ \_output ->
414-
Route $ responseLBS status [] ""
415-
416372
instance
417373
{-# OVERLAPPABLE #-}
418374
( AllCTRender ctypes a
419-
, KnownNat status
375+
, AllMimeRender ctypes a
376+
, KnownStatus status
420377
, ReflectMethod method
421378
)
422379
=> HasServer (Verb method status ctypes a) context
423380
where
424381
type ServerT (Verb method status ctypes a) m = m a
425382
hoistServerWithContext _ _ nt = nt
426383

427-
route Proxy _ = methodRouter ([],) method (Proxy :: Proxy ctypes) status
428-
where
429-
method = reflectMethod (Proxy :: Proxy method)
430-
status = statusFromNat (Proxy :: Proxy status)
384+
route Proxy = route (Proxy @(MultiVerb method ctypes '[Respond status "" a] a))
431385

432386
instance
433387
{-# OVERLAPPING #-}
434388
( AllCTRender ctypes a
389+
, AllMimeRender ctypes a
435390
, GetHeaders (Headers h a)
436-
, KnownNat status
391+
, KnownStatus status
437392
, ReflectMethod method
438393
)
439394
=> HasServer (Verb method status ctypes (Headers h a)) context
440395
where
441396
type ServerT (Verb method status ctypes (Headers h a)) m = m (Headers h a)
442397
hoistServerWithContext _ _ nt = nt
443398

444-
route Proxy _ = methodRouter (\x -> (getHeaders x, getResponse x)) method (Proxy :: Proxy ctypes) status
445-
where
446-
method = reflectMethod (Proxy :: Proxy method)
447-
status = statusFromNat (Proxy :: Proxy status)
399+
route Proxy = route (Proxy @(MultiVerb method ctypes '[Respond status "" (Headers h a)] (Headers h a)))
448400

449401
instance
450402
ReflectMethod method
@@ -453,9 +405,9 @@ instance
453405
type ServerT (NoContentVerb method) m = m NoContent
454406
hoistServerWithContext _ _ nt = nt
455407

456-
route Proxy _ = noContentRouter method status204
457-
where
458-
method = reflectMethod (Proxy :: Proxy method)
408+
route Proxy ctx action =
409+
route (Proxy @(MultiVerb method '() '[RespondAs '() 204 "" ()] ())) ctx $
410+
fmap void action
459411

460412
instance
461413
{-# OVERLAPPABLE #-}

servant-server/src/Servant/Server/Internal/ResponseRender.hs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import Servant.API.ContentTypes
2525
, mimeRender
2626
)
2727
import Servant.API.MultiVerb
28+
import Servant.API.ResponseHeaders (GetHeaders (..), Headers, getResponse)
2829
import Servant.API.Status
2930
import Servant.API.Stream (SourceIO)
3031
import Servant.API.UVerb.Union
@@ -154,6 +155,7 @@ instance
154155
}
155156

156157
instance
158+
{-# OVERLAPPABLE #-}
157159
(AllMimeRender cs a, KnownStatus s)
158160
=> ResponseRender cs (Respond s desc a)
159161
where
@@ -176,6 +178,29 @@ instance
176178
, headers = mempty
177179
}
178180

181+
-- | Instance for Respond with Headers - extracts headers from the Headers wrapper
182+
-- This enables Verb with Headers to delegate to MultiVerb
183+
instance
184+
{-# OVERLAPPING #-}
185+
(AllMimeRender cs a, GetHeaders (Headers h a), KnownStatus s)
186+
=> ResponseRender cs (Respond s desc (Headers h a))
187+
where
188+
type ResponseStatus (Respond s desc (Headers h a)) = s
189+
type ResponseBody (Respond s desc (Headers h a)) = BSL.ByteString
190+
191+
responseRender (AcceptHeader acc) headersVal =
192+
M.mapAcceptMedia (map (uncurry mkRenderOutput) (allMimeRender (Proxy @cs) (getResponse headersVal))) acc
193+
where
194+
responseHeaders = Seq.fromList (getHeaders headersVal)
195+
mkRenderOutput :: M.MediaType -> BSL.ByteString -> (M.MediaType, InternalResponse BSL.ByteString)
196+
mkRenderOutput c body =
197+
(c,) . addContentType' c $
198+
InternalResponse
199+
{ statusCode = statusVal (Proxy @s)
200+
, responseBody = body
201+
, headers = responseHeaders
202+
}
203+
179204
addContentType :: forall ct a. Accept ct => InternalResponse a -> InternalResponse a
180205
addContentType = addContentType' (contentType (Proxy @ct))
181206

servant-server/test/Servant/ServerSpec.hs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ import Servant.API
115115
, (:>)
116116
)
117117
import Servant.API.QueryString (FromDeepQuery (..))
118+
import Servant.API.Status (KnownStatus (..), statusFromNat)
118119
import Servant.Test.ComprehensiveAPI
119120
import qualified Servant.Types.SourceT as S
120121
import Test.Hspec (Spec, context, describe, it, shouldBe, shouldContain)
@@ -156,6 +157,12 @@ import Servant.Server.Internal.BasicAuth
156157
)
157158
import Servant.Server.Internal.Context (NamedContext (..))
158159

160+
-- * KnownStatus instances for non-standard test status codes
161+
162+
instance KnownStatus 210 where statusVal _ = statusFromNat (Proxy @210)
163+
instance KnownStatus 214 where statusVal _ = statusFromNat (Proxy @214)
164+
instance KnownStatus 280 where statusVal _ = statusFromNat (Proxy @280)
165+
159166
-- * comprehensive api test
160167

161168
-- This declaration simply checks that all instances are in place.

servant/src/Servant/API/Status.hs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
{-# LANGUAGE DataKinds #-}
2-
-- Flexible instances is necessary on GHC 8.4 and earlier
3-
{-# LANGUAGE FlexibleInstances #-}
42
{-# LANGUAGE ScopedTypeVariables #-}
53

64
module Servant.API.Status where
@@ -12,7 +10,13 @@ import Network.HTTP.Types.Status
1210
statusFromNat :: forall a proxy. KnownNat a => proxy a -> Status
1311
statusFromNat = toEnum . fromInteger . natVal
1412

15-
-- | Witness that a type-level natural number corresponds to a HTTP status code
13+
-- | Witness that a type-level natural number corresponds to a HTTP status code.
14+
--
15+
-- Provides instances for all standard HTTP status codes. For non-standard codes,
16+
-- you can define your own instance:
17+
--
18+
-- > instance KnownStatus 299 where
19+
-- > statusVal _ = statusFromNat (Proxy @299)
1620
class KnownNat n => KnownStatus n where
1721
statusVal :: proxy n -> Status
1822

servant/src/Servant/API/Verbs.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,13 @@ import Network.HTTP.Types.Method
3333
-- provided, but you are free to define your own:
3434
--
3535
-- >>> type Post204 contentTypes a = Verb 'POST 204 contentTypes a
36-
data Verb (method :: k1) (statusCode :: Nat) (contentTypes :: [Type]) (a :: Type)
36+
data Verb (method :: StdMethod) (statusCode :: Nat) (contentTypes :: [Type]) (a :: Type)
3737
deriving (Generic, Typeable)
3838

3939
-- | @NoContentVerb@ is a specific type to represent 'NoContent' responses.
4040
-- It does not require either a list of content types (because there's
4141
-- no content) or a status code (because it should always be 204).
42-
data NoContentVerb (method :: k1)
42+
data NoContentVerb (method :: StdMethod)
4343
deriving (Generic, Typeable)
4444

4545
-- * 200 responses

0 commit comments

Comments
 (0)