git.iko.soy/typeable/servant
on master help sign in
Merge pull request #873 from phadej/modifiers

Servant.API.Modifiers

commitbf289ccd59d3800fd1079360861275c9f507c7f3 authorOleg Grenrus <oleg.grenrus@iki.fi> dateThu 25 Jan 2018, 11:12 (9 years ago) parent0a50e75bc3f61d
18 files changed+421−148
servant-client-core/src/Servant/Client/Core/Internal/HasClient.hs+31−27 servant-docs/src/Servant/Docs/Internal.hs+6−6 servant-docs/test/Servant/DocsSpec.hs+3−1 servant-foreign/servant-foreign.cabal+5−4 servant-foreign/src/Servant/Foreign/Internal.hs+14−15 servant-foreign/test/Servant/ForeignSpec.hs+4−2 servant-server/src/Servant/Server/Internal.hs+69−46 servant-server/test/Servant/ServerSpec.hs+6−6 servant/servant.cabal+3−1 servant/src/Servant/API.hs+14−4 servant/src/Servant/API/Header.hs+9−7 servant/src/Servant/API/Internal/Test/ComprehensiveAPI.hs+3 servant/src/Servant/API/Modifiers.hs+150 servant/src/Servant/API/QueryParam.hs+7−2 servant/src/Servant/API/ReqBody.hs+12−3 servant/src/Servant/API/ResponseHeaders.hs+14−5 servant/src/Servant/Utils/Links.hs+52−12 servant/test/Servant/Utils/LinksSpec.hs+19−7
servant-client-core/src/Servant/Client/Core/Internal/HasClient.hs+31−27
@@ -25,7 +25,7 @@ import Data.Monoid ((<>))
2525import Data.Proxy (Proxy (Proxy))
2626import Data.Sequence (fromList)
2727import Data.String (fromString)
28−import Data.Text (pack)
28+import Data.Text (Text, pack)
2929import GHC.TypeLits (KnownSymbol, symbolVal)
3030import qualified Network.HTTP.Types as H
3131import Servant.API ((:<|>) ((:<|>)), (:>),
@@ -37,16 +37,17 @@ import Servant.API ((:<|>) ((:<|>)), (:>),
3737 Capture, CaptureAll,
3838 Description, EmptyAPI,
3939 FramingUnrender (..),
40− Header, Headers (..),
40+ Header', Headers (..),
4141 HttpVersion, IsSecure,
4242 MimeRender (mimeRender),
4343 MimeUnrender (mimeUnrender),
4444 NoContent (NoContent),
45− QueryFlag, QueryParam,
45+ QueryFlag, QueryParam',
4646 QueryParams, Raw,
4747 ReflectMethod (..),
48− RemoteHost, ReqBody,
48+ RemoteHost, ReqBody',
4949 ResultStream(..),
50+ SBoolI,
5051 Stream,
5152 Summary, ToHttpApiData,
5253 Vault, Verb,
@@ -57,6 +58,9 @@ import Servant.API ((:<|>) ((:<|>)), (:>),
5758 toQueryParam,
5859 toUrlPiece)
5960import Servant.API.ContentTypes (contentTypes)
61+import Servant.API.Modifiers (FoldRequired,
62+ RequiredArgument,
63+ foldRequiredArgument)
6064
6165import Servant.Client.Core.Internal.Auth
6266import Servant.Client.Core.Internal.BasicAuth
@@ -325,20 +329,20 @@ instance OVERLAPPABLE_
325329-- > viewReferer = client myApi
326330-- > -- then you can just use "viewRefer" to query that endpoint
327331-- > -- specifying Nothing or e.g Just "http://haskell.org/" as arguments
328−instance (KnownSymbol sym, ToHttpApiData a, HasClient m api)
329− => HasClient m (Header sym a :> api) where
332+instance (KnownSymbol sym, ToHttpApiData a, HasClient m api, SBoolI (FoldRequired mods))
333+ => HasClient m (Header' mods sym a :> api) where
330334
331− type Client m (Header sym a :> api) =
332− Maybe a -> Client m api
335+ type Client m (Header' mods sym a :> api) =
336+ RequiredArgument mods a -> Client m api
333337
334338 clientWithRoute pm Proxy req mval =
335− clientWithRoute pm (Proxy :: Proxy api)
336− (maybe req
337− (\value -> addHeader hname value req)
338− mval
339− )
339+ clientWithRoute pm (Proxy :: Proxy api) $ foldRequiredArgument
340+ (Proxy :: Proxy mods) add (maybe req add) mval
341+ where
342+ hname = fromString $ symbolVal (Proxy :: Proxy sym)
340343
341− where hname = fromString $ symbolVal (Proxy :: Proxy sym)
344+ add :: a -> Request
345+ add value = addHeader hname value req
342346
343347-- | Using a 'HttpVersion' combinator in your API doesn't affect the client
344348-- functions.
@@ -388,22 +392,22 @@ instance HasClient m api => HasClient m (Description desc :> api) where
388392-- > -- then you can just use "getBooksBy" to query that endpoint.
389393-- > -- 'getBooksBy Nothing' for all books
390394-- > -- 'getBooksBy (Just "Isaac Asimov")' to get all books by Isaac Asimov
391−instance (KnownSymbol sym, ToHttpApiData a, HasClient m api)
392− => HasClient m (QueryParam sym a :> api) where
395+instance (KnownSymbol sym, ToHttpApiData a, HasClient m api, SBoolI (FoldRequired mods))
396+ => HasClient m (QueryParam' mods sym a :> api) where
393397
394− type Client m (QueryParam sym a :> api) =
395− Maybe a -> Client m api
398+ type Client m (QueryParam' mods sym a :> api) =
399+ RequiredArgument mods a -> Client m api
396400
397401 -- if mparam = Nothing, we don't add it to the query string
398402 clientWithRoute pm Proxy req mparam =
399− clientWithRoute pm (Proxy :: Proxy api)
400− (maybe req
401− (flip (appendToQueryString pname) req . Just)
402− mparamText
403− )
403+ clientWithRoute pm (Proxy :: Proxy api) $ foldRequiredArgument
404+ (Proxy :: Proxy mods) add (maybe req add) mparam
405+ where
406+ add :: a -> Request
407+ add param = appendToQueryString pname (Just $ toQueryParam param) req
404408
405− where pname = pack $ symbolVal (Proxy :: Proxy sym)
406− mparamText = fmap toQueryParam mparam
409+ pname :: Text
410+ pname = pack $ symbolVal (Proxy :: Proxy sym)
407411
408412-- | If you use a 'QueryParams' in one of your endpoints in your API,
409413-- the corresponding querying function will automatically take
@@ -514,9 +518,9 @@ instance RunClient m => HasClient m Raw where
514518-- > addBook = client myApi
515519-- > -- then you can just use "addBook" to query that endpoint
516520instance (MimeRender ct a, HasClient m api)
517− => HasClient m (ReqBody (ct ': cts) a :> api) where
521+ => HasClient m (ReqBody' mods (ct ': cts) a :> api) where
518522
519− type Client m (ReqBody (ct ': cts) a :> api) =
523+ type Client m (ReqBody' mods (ct ': cts) a :> api) =
520524 a -> Client m api
521525
522526 clientWithRoute pm Proxy req body =
servant-docs/src/Servant/Docs/Internal.hs+6−6
@@ -534,7 +534,7 @@ sampleByteStrings ctypes@Proxy Proxy =
534534--
535535-- Example of an instance:
536536--
537−-- > instance ToParam (QueryParam "capital" Bool) where
537+-- > instance ToParam (QueryParam' mods "capital" Bool) where
538538-- > toParam _ =
539539-- > DocQueryParam "capital"
540540-- > ["true", "false"]
@@ -859,7 +859,7 @@ instance OVERLAPPING_
859859 p = Proxy :: Proxy a
860860
861861instance (KnownSymbol sym, HasDocs api)
862− => HasDocs (Header sym a :> api) where
862+ => HasDocs (Header' mods sym a :> api) where
863863 docsFor Proxy (endpoint, action) =
864864 docsFor subApiP (endpoint, action')
865865
@@ -867,14 +867,14 @@ instance (KnownSymbol sym, HasDocs api)
867867 action' = over headers (|> headername) action
868868 headername = T.pack $ symbolVal (Proxy :: Proxy sym)
869869
870−instance (KnownSymbol sym, ToParam (QueryParam sym a), HasDocs api)
871− => HasDocs (QueryParam sym a :> api) where
870+instance (KnownSymbol sym, ToParam (QueryParam' mods sym a), HasDocs api)
871+ => HasDocs (QueryParam' mods sym a :> api) where
872872
873873 docsFor Proxy (endpoint, action) =
874874 docsFor subApiP (endpoint, action')
875875
876876 where subApiP = Proxy :: Proxy api
877− paramP = Proxy :: Proxy (QueryParam sym a)
877+ paramP = Proxy :: Proxy (QueryParam' mods sym a)
878878 action' = over params (|> toParam paramP) action
879879
880880instance (KnownSymbol sym, ToParam (QueryParams sym a), HasDocs api)
@@ -929,7 +929,7 @@ instance (KnownSymbol desc, HasDocs api)
929929-- 'AllMimeUnrender' and 'AllMimeRender' actually agree (or to suppose that
930930-- both are even defined) for any particular type.
931931instance (ToSample a, AllMimeRender (ct ': cts) a, HasDocs api)
932− => HasDocs (ReqBody (ct ': cts) a :> api) where
932+ => HasDocs (ReqBody' mods (ct ': cts) a :> api) where
933933
934934 docsFor Proxy (endpoint, action) opts@DocOptions{..} =
935935 docsFor subApiP (endpoint, action') opts
servant-docs/test/Servant/DocsSpec.hs+3−1
@@ -33,7 +33,9 @@ import Servant.Docs.Internal
3333-- This declaration simply checks that all instances are in place.
3434_ = docs comprehensiveAPI
3535
36−instance ToParam (QueryParam "foo" Int) where
36+instance ToParam (QueryParam' mods "foo" Int) where
37+ toParam = error "unused"
38+instance ToParam (QueryParam' mods "bar" Int) where
3739 toParam = error "unused"
3840instance ToParam (QueryParams "foo" Int) where
3941 toParam = error "unused"
servant-foreign/servant-foreign.cabal+5−4
@@ -36,10 +36,11 @@ library
3636 exposed-modules: Servant.Foreign
3737 , Servant.Foreign.Internal
3838 , Servant.Foreign.Inflections
39− build-depends: base == 4.*
40− , lens == 4.*
41− , servant == 0.12.*
42− , text >= 1.2 && < 1.3
39+ build-depends: base >= 4.7 && <4.11
40+ , base-compat >= 0.9.3 && <0.10
41+ , lens == 4.*
42+ , servant == 0.12.*
43+ , text >= 1.2 && < 1.3
4344 , http-types
4445 hs-source-dirs: src
4546 default-language: Haskell2010
servant-foreign/src/Servant/Foreign/Internal.hs+14−15
@@ -20,12 +20,12 @@
2020-- arbitrary programming languages.
2121module Servant.Foreign.Internal where
2222
23+import Prelude ()
24+import Prelude.Compat
25+
2326import Control.Lens (makePrisms, makeLenses, Getter, (&), (<>~), (%~),
2427 (.~))
2528import Data.Data (Data)
26−#if !MIN_VERSION_base(4,8,0)
27−import Data.Monoid
28−#endif
2929import Data.Proxy
3030import Data.String
3131import Data.Text
@@ -33,10 +33,9 @@ import Data.Typeable (Typeable)
3333import Data.Text.Encoding (decodeUtf8)
3434import GHC.TypeLits
3535import qualified Network.HTTP.Types as HTTP
36−import Prelude hiding (concat)
3736import Servant.API
3837import Servant.API.TypeLevel
39−
38+import Servant.API.Modifiers (RequiredArgument)
4039
4140newtype FunctionName = FunctionName { unFunctionName :: [Text] }
4241 deriving (Data, Show, Eq, Monoid, Typeable)
@@ -238,9 +237,9 @@ instance (Elem JSON list, HasForeignType lang ftype a, ReflectMethod method)
238237 method = reflectMethod (Proxy :: Proxy method)
239238 methodLC = toLower $ decodeUtf8 method
240239
241−instance (KnownSymbol sym, HasForeignType lang ftype (Maybe a), HasForeign lang ftype api)
242− => HasForeign lang ftype (Header sym a :> api) where
243− type Foreign ftype (Header sym a :> api) = Foreign ftype api
240+instance (KnownSymbol sym, HasForeignType lang ftype (RequiredArgument mods a), HasForeign lang ftype api)
241+ => HasForeign lang ftype (Header' mods sym a :> api) where
242+ type Foreign ftype (Header' mods sym a :> api) = Foreign ftype api
244243
245244 foreignFor lang Proxy Proxy req =
246245 foreignFor lang Proxy subP $ req & reqHeaders <>~ [HeaderArg arg]
@@ -248,12 +247,12 @@ instance (KnownSymbol sym, HasForeignType lang ftype (Maybe a), HasForeign lang
248247 hname = pack . symbolVal $ (Proxy :: Proxy sym)
249248 arg = Arg
250249 { _argName = PathSegment hname
251− , _argType = typeFor lang (Proxy :: Proxy ftype) (Proxy :: Proxy (Maybe a)) }
250+ , _argType = typeFor lang (Proxy :: Proxy ftype) (Proxy :: Proxy (RequiredArgument mods a)) }
252251 subP = Proxy :: Proxy api
253252
254−instance (KnownSymbol sym, HasForeignType lang ftype a, HasForeign lang ftype api)
255− => HasForeign lang ftype (QueryParam sym a :> api) where
256− type Foreign ftype (QueryParam sym a :> api) = Foreign ftype api
253+instance (KnownSymbol sym, HasForeignType lang ftype (RequiredArgument mods a), HasForeign lang ftype api)
254+ => HasForeign lang ftype (QueryParam' mods sym a :> api) where
255+ type Foreign ftype (QueryParam' mods sym a :> api) = Foreign ftype api
257256
258257 foreignFor lang Proxy Proxy req =
259258 foreignFor lang (Proxy :: Proxy ftype) (Proxy :: Proxy api) $
@@ -262,7 +261,7 @@ instance (KnownSymbol sym, HasForeignType lang ftype a, HasForeign lang ftype ap
262261 str = pack . symbolVal $ (Proxy :: Proxy sym)
263262 arg = Arg
264263 { _argName = PathSegment str
265− , _argType = typeFor lang (Proxy :: Proxy ftype) (Proxy :: Proxy a) }
264+ , _argType = typeFor lang (Proxy :: Proxy ftype) (Proxy :: Proxy (RequiredArgument mods a)) }
266265
267266instance
268267 (KnownSymbol sym, HasForeignType lang ftype [a], HasForeign lang ftype api)
@@ -299,8 +298,8 @@ instance HasForeign lang ftype Raw where
299298 & reqMethod .~ method
300299
301300instance (Elem JSON list, HasForeignType lang ftype a, HasForeign lang ftype api)
302− => HasForeign lang ftype (ReqBody list a :> api) where
303− type Foreign ftype (ReqBody list a :> api) = Foreign ftype api
301+ => HasForeign lang ftype (ReqBody' mods list a :> api) where
302+ type Foreign ftype (ReqBody' mods list a :> api) = Foreign ftype api
304303
305304 foreignFor lang ftype Proxy req =
306305 foreignFor lang ftype (Proxy :: Proxy api) $
servant-foreign/test/Servant/ForeignSpec.hs+4−2
@@ -7,9 +7,11 @@
77{-# LANGUAGE OverloadedStrings #-}
88{-# LANGUAGE PolyKinds #-}
99{-# LANGUAGE ScopedTypeVariables #-}
10−{-# LANGUAGE TemplateHaskell #-}
1110{-# LANGUAGE TypeFamilies #-}
1211{-# LANGUAGE TypeOperators #-}
12+#if __GLASGOW__HASKELL < 709
13+{-# OPTIONS_GHC -fcontext-stack=41 #-}
14+#endif
1315#include "overlapping-compat.h"
1416
1517module Servant.ForeignSpec where
@@ -99,7 +101,7 @@ listFromAPISpec = describe "listFromAPI" $ do
99101 shouldBe postReq $ defReq
100102 { _reqUrl = Url
101103 [ Segment $ Static "test" ]
102− [ QueryArg (Arg "param" "intX") Normal ]
104+ [ QueryArg (Arg "param" "maybe intX") Normal ]
103105 , _reqMethod = "POST"
104106 , _reqHeaders = []
105107 , _reqBody = Just "listX of stringX"
servant-server/src/Servant/Server/Internal.hs+69−46
@@ -12,6 +12,7 @@
1212{-# LANGUAGE TupleSections #-}
1313{-# LANGUAGE TypeFamilies #-}
1414{-# LANGUAGE TypeOperators #-}
15+{-# LANGUAGE UndecidableInstances #-}
1516
1617#if MIN_VERSION_base(4,9,0) && __GLASGOW_HASKELL__ >= 802
1718#define HAS_TYPE_ERROR
@@ -33,7 +34,7 @@ module Servant.Server.Internal
3334 , module Servant.Server.Internal.ServantErr
3435 ) where
3536
36−import Control.Monad (when)
37+import Control.Monad (join, when)
3738import Control.Monad.Trans (liftIO)
3839import Control.Monad.Trans.Resource (runResourceT)
3940import qualified Data.ByteString as B
@@ -43,7 +44,7 @@ import qualified Data.ByteString.Lazy as BL
4344import Data.Maybe (fromMaybe, mapMaybe,
4445 isNothing, maybeToList)
4546import Data.Either (partitionEithers)
46−import Data.String (fromString)
47+import Data.String (IsString (..))
4748import Data.String.Conversions (cs, (<>))
4849import Data.Tagged (Tagged(..), retag, untag)
4950import qualified Data.Text as T
@@ -69,15 +70,17 @@ import Web.HttpApiData (FromHttpApiData, parseHeader,
6970import Servant.API ((:<|>) (..), (:>), BasicAuth, Capture,
7071 CaptureAll, Verb, EmptyAPI,
7172 ReflectMethod(reflectMethod),
72− IsSecure(..), Header, QueryFlag,
73− QueryParam, QueryParams, Raw,
74− RemoteHost, ReqBody, Vault,
73+ IsSecure(..), Header', QueryFlag,
74+ QueryParam', QueryParams, Raw,
75+ RemoteHost, ReqBody', Vault,
7576 WithNamedContext,
7677 Description, Summary,
7778 Accept(..),
7879 FramingRender(..), Stream,
7980 StreamGenerator(..), ToStreamGenerator(..),
80− BoundaryStrategy(..))
81+ BoundaryStrategy(..),
82+ If, SBool (..), SBoolI (..))
83+import Servant.API.Modifiers (unfoldRequestArgument, RequestArgument, FoldRequired, FoldLenient)
8184import Servant.API.ContentTypes (AcceptHeader (..),
8285 AllCTRender (..),
8386 AllCTUnrender (..),
@@ -361,29 +364,39 @@ streamRouter splitHeaders method framingproxy ctypeproxy action = leafRouter $ \
361364-- > server = viewReferer
362365-- > where viewReferer :: Referer -> Handler referer
363366-- > viewReferer referer = return referer
364−instance (KnownSymbol sym, FromHttpApiData a, HasServer api context)
365− => HasServer (Header sym a :> api) context where
366−
367− type ServerT (Header sym a :> api) m =
368− Maybe a -> ServerT api m
367+instance
368+ (KnownSymbol sym, FromHttpApiData a, HasServer api context
369+ , SBoolI (FoldRequired mods), SBoolI (FoldLenient mods)
370+ )
371+ => HasServer (Header' mods sym a :> api) context where
372+------
373+ type ServerT (Header' mods sym a :> api) m =
374+ RequestArgument mods a -> ServerT api m
369375
370376 hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s
371377
372378 route Proxy context subserver = route (Proxy :: Proxy api) context $
373379 subserver `addHeaderCheck` withRequest headerCheck
374380 where
375− headerName = symbolVal (Proxy :: Proxy sym)
381+ headerName :: IsString n => n
382+ headerName = fromString $ symbolVal (Proxy :: Proxy sym)
383+
384+ headerCheck :: Request -> DelayedIO (RequestArgument mods a)
376385 headerCheck req =
377− case lookup (fromString headerName) (requestHeaders req) of
378− Nothing -> return Nothing
379− Just txt ->
380− case parseHeader txt of
381− Left e -> delayedFailFatal err400
382− { errBody = cs $ "Error parsing header "
383− <> fromString headerName
384− <> " failed: " <> e
385− }
386− Right hdr -> return $ Just hdr
386+ unfoldRequestArgument (Proxy :: Proxy mods) errReq errSt mev
387+ where
388+ mev :: Maybe (Either T.Text a)
389+ mev = fmap parseHeader $ lookup headerName (requestHeaders req)
390+
391+ errReq = delayedFailFatal err400
392+ { errBody = "Header " <> headerName <> " is required"
393+ }
394+
395+ errSt e = delayedFailFatal err400
396+ { errBody = cs $ "Error parsing header "
397+ <> headerName
398+ <> " failed: " <> e
399+ }
387400
388401-- | If you use @'QueryParam' "author" Text@ in one of the endpoints for your API,
389402-- this automatically requires your server-side handler to be a function
@@ -406,33 +419,41 @@ instance (KnownSymbol sym, FromHttpApiData a, HasServer api context)
406419-- > where getBooksBy :: Maybe Text -> Handler [Book]
407420-- > getBooksBy Nothing = ...return all books...
408421-- > getBooksBy (Just author) = ...return books by the given author...
409−instance (KnownSymbol sym, FromHttpApiData a, HasServer api context)
410− => HasServer (QueryParam sym a :> api) context where
411−
412− type ServerT (QueryParam sym a :> api) m =
413− Maybe a -> ServerT api m
422+instance
423+ ( KnownSymbol sym, FromHttpApiData a, HasServer api context
424+ , SBoolI (FoldRequired mods), SBoolI (FoldLenient mods)
425+ )
426+ => HasServer (QueryParam' mods sym a :> api) context where
427+------
428+ type ServerT (QueryParam' mods sym a :> api) m =
429+ RequestArgument mods a -> ServerT api m
414430
415431 hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s
416432
417433 route Proxy context subserver =
418434 let querytext req = parseQueryText $ rawQueryString req
435+ paramname = cs $ symbolVal (Proxy :: Proxy sym)
436+
437+ parseParam :: Request -> DelayedIO (RequestArgument mods a)
419438 parseParam req =
420− case lookup paramname (querytext req) of
421− Nothing -> return Nothing -- param absent from the query string
422− Just Nothing -> return Nothing -- param present with no value -> Nothing
423− Just (Just v) ->
424− case parseQueryParam v of
425− Left e -> delayedFailFatal err400
426− { errBody = cs $ "Error parsing query parameter "
427− <> paramname <> " failed: " <> e
428− }
429−
430− Right param -> return $ Just param
439+ unfoldRequestArgument (Proxy :: Proxy mods) errReq errSt mev
440+ where
441+ mev :: Maybe (Either T.Text a)
442+ mev = fmap parseQueryParam $ join $ lookup paramname $ querytext req
443+
444+ errReq = delayedFailFatal err400
445+ { errBody = cs $ "Query parameter " <> paramname <> " is required"
446+ }
447+
448+ errSt e = delayedFailFatal err400
449+ { errBody = cs $ "Error parsing query parameter "
450+ <> paramname <> " failed: " <> e
451+ }
452+
431453 delayed = addParameterCheck subserver . withRequest $ \req ->
432454 parseParam req
433455
434456 in route (Proxy :: Proxy api) context delayed
435− where paramname = cs $ symbolVal (Proxy :: Proxy sym)
436457
437458-- | If you use @'QueryParams' "authors" Text@ in one of the endpoints for your API,
438459-- this automatically requires your server-side handler to be a function
@@ -561,11 +582,11 @@ instance HasServer Raw context where
561582-- > server = postBook
562583-- > where postBook :: Book -> Handler Book
563584-- > postBook book = ...insert into your db...
564−instance ( AllCTUnrender list a, HasServer api context
565− ) => HasServer (ReqBody list a :> api) context where
585+instance ( AllCTUnrender list a, HasServer api context, SBoolI (FoldLenient mods)
586+ ) => HasServer (ReqBody' mods list a :> api) context where
566587
567− type ServerT (ReqBody list a :> api) m =
568− a -> ServerT api m
588+ type ServerT (ReqBody' mods list a :> api) m =
589+ If (FoldLenient mods) (Either String a) a -> ServerT api m
569590
570591 hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s
571592
@@ -588,9 +609,11 @@ instance ( AllCTUnrender list a, HasServer api context
588609 -- Body check, we get a body parsing functions as the first argument.
589610 bodyCheck f = withRequest $ \ request -> do
590611 mrqbody <- f <$> liftIO (lazyRequestBody request)
591− case mrqbody of
592− Left e -> delayedFailFatal err400 { errBody = cs e }
593− Right v -> return v
612+ case sbool :: SBool (FoldLenient mods) of
613+ STrue -> return mrqbody
614+ SFalse -> case mrqbody of
615+ Left e -> delayedFailFatal err400 { errBody = cs e }
616+ Right v -> return v
594617
595618-- | Make sure the incoming request starts with @"/path"@, strip it and
596619-- pass the rest of the request path to @api@.
servant-server/test/Servant/ServerSpec.hs+6−6
@@ -47,7 +47,7 @@ import Network.Wai.Test (defaultRequest, request,
4747 simpleHeaders, simpleStatus)
4848import Servant.API ((:<|>) (..), (:>), AuthProtect,
4949 BasicAuth, BasicAuthData(BasicAuthData),
50− Capture, CaptureAll, Delete, Get, Header (..),
50+ Capture, CaptureAll, Delete, Get, Header,
5151 Headers, HttpVersion,
5252 IsSecure (..), JSON,
5353 NoContent (..), Patch, PlainText,
@@ -461,8 +461,8 @@ reqBodySpec = describe "Servant.API.ReqBody" $ do
461461------------------------------------------------------------------------------
462462
463463type HeaderApi a = Header "MyHeader" a :> Delete '[JSON] NoContent
464−headerApi :: Proxy (HeaderApi a)
465−headerApi = Proxy
464+headerApi :: Proxy a -> Proxy (HeaderApi a)
465+headerApi _ = Proxy
466466
467467headerSpec :: Spec
468468headerSpec = describe "Servant.API.Header" $ do
@@ -479,19 +479,19 @@ headerSpec = describe "Servant.API.Header" $ do
479479 return NoContent
480480 expectsString Nothing = error "Expected a string"
481481
482− with (return (serve headerApi expectsInt)) $ do
482+ with (return (serve (headerApi (Proxy :: Proxy Int)) expectsInt)) $ do
483483 let delete' x = THW.request methodDelete x [("MyHeader", "5")]
484484
485485 it "passes the header to the handler (Int)" $
486486 delete' "/" "" `shouldRespondWith` 200
487487
488− with (return (serve headerApi expectsString)) $ do
488+ with (return (serve (headerApi (Proxy :: Proxy String)) expectsString)) $ do
489489 let delete' x = THW.request methodDelete x [("MyHeader", "more from you")]
490490
491491 it "passes the header to the handler (String)" $
492492 delete' "/" "" `shouldRespondWith` 200
493493
494− with (return (serve headerApi expectsInt)) $ do
494+ with (return (serve (headerApi (Proxy :: Proxy Int)) expectsInt)) $ do
495495 let delete' x = THW.request methodDelete x [("MyHeader", "not a number")]
496496
497497 it "checks for parse errors" $
servant/servant.cabal+3−1
@@ -49,12 +49,13 @@ library
4949 Servant.API.HttpVersion
5050 Servant.API.Internal.Test.ComprehensiveAPI
5151 Servant.API.IsSecure
52+ Servant.API.Modifiers
5253 Servant.API.QueryParam
5354 Servant.API.Raw
54− Servant.API.Stream
5555 Servant.API.RemoteHost
5656 Servant.API.ReqBody
5757 Servant.API.ResponseHeaders
58+ Servant.API.Stream
5859 Servant.API.Sub
5960 Servant.API.TypeLevel
6061 Servant.API.Vault
@@ -77,6 +78,7 @@ library
7778 , mmorph >= 1 && < 1.2
7879 , tagged >= 0.7.3 && < 0.9
7980 , text >= 1 && < 1.3
81+ , singleton-bool >= 0.1.2.0 && <0.2
8082 , string-conversions >= 0.3 && < 0.5
8183 , network-uri >= 2.6 && < 2.7
8284 , vault >= 0.3 && < 0.4
servant/src/Servant/API.hs+14−4
@@ -7,6 +7,8 @@ module Servant.API (
77 -- | Type-level combinator for alternative endpoints: @':<|>'@
88 module Servant.API.Empty,
99 -- | Type-level combinator for an empty API: @'EmptyAPI'@
10+ module Servant.API.Modifiers,
11+ -- | Type-level modifiers for 'QueryParam', 'Header' and 'ReqBody'.
1012
1113 -- * Accessing information from the request
1214 module Servant.API.Capture,
@@ -64,6 +66,10 @@ module Servant.API (
6466 -- * Utilities
6567 module Servant.Utils.Links,
6668 -- | Type-safe internal URIs
69+
70+ -- * Re-exports
71+ If,
72+ SBool (..), SBoolI (..)
6773 ) where
6874
6975import Servant.API.Alternative ((:<|>) (..))
@@ -77,10 +83,11 @@ import Servant.API.ContentTypes (Accept (..), FormUrlEncoded,
7783import Servant.API.Description (Description, Summary)
7884import Servant.API.Empty (EmptyAPI (..))
7985import Servant.API.Experimental.Auth (AuthProtect)
80−import Servant.API.Header (Header (..))
86+import Servant.API.Header (Header, Header')
8187import Servant.API.HttpVersion (HttpVersion (..))
8288import Servant.API.IsSecure (IsSecure (..))
83−import Servant.API.QueryParam (QueryFlag, QueryParam,
89+import Servant.API.Modifiers (Required, Optional, Lenient, Strict)
90+import Servant.API.QueryParam (QueryFlag, QueryParam, QueryParam',
8491 QueryParams)
8592import Servant.API.Raw (Raw)
8693import Servant.API.Stream (Stream, StreamGet, StreamPost,
@@ -93,12 +100,12 @@ import Servant.API.Stream (Stream, StreamGet, StreamPost,
93100 NewlineFraming,
94101 NetstringFraming)
95102import Servant.API.RemoteHost (RemoteHost)
96−import Servant.API.ReqBody (ReqBody)
103+import Servant.API.ReqBody (ReqBody, ReqBody')
97104import Servant.API.ResponseHeaders (AddHeader, addHeader, noHeader,
98105 BuildHeadersTo (buildHeadersTo),
99106 GetHeaders (getHeaders),
100107 HList (..), Headers (..),
101− getHeadersHList, getResponse)
108+ getHeadersHList, getResponse, ResponseHeader (..))
102109import Servant.API.Sub ((:>))
103110import Servant.API.Vault (Vault)
104111import Servant.API.Verbs (PostCreated, Delete, DeleteAccepted,
@@ -124,3 +131,6 @@ import Servant.Utils.Links (HasLink (..), Link, IsElem, IsElem
124131 URI (..), safeLink)
125132import Web.HttpApiData (FromHttpApiData (..),
126133 ToHttpApiData (..))
134+
135+import Data.Type.Bool (If)
136+import Data.Singletons.Bool (SBool (..), SBoolI (..))
servant/src/Servant/API/Header.hs+9−7
@@ -4,13 +4,15 @@
44{-# LANGUAGE PolyKinds #-}
55{-# OPTIONS_HADDOCK not-home #-}
66module Servant.API.Header (
7− Header(..),
8−) where
7+ Header, Header',
8+ ) where
99
10−import Data.ByteString (ByteString)
1110import Data.Typeable (Typeable)
1211import GHC.TypeLits (Symbol)
12+import Servant.API.Modifiers
13+
1314-- | Extract the given header's value as a value of type @a@.
15+-- I.e. header sent by client, parsed by server.
1416--
1517-- Example:
1618--
@@ -18,10 +20,10 @@ import GHC.TypeLits (Symbol)
1820-- >>>
1921-- >>> -- GET /view-my-referer
2022-- >>> type MyApi = "view-my-referer" :> Header "from" Referer :> Get '[JSON] Referer
21−data Header (sym :: Symbol) a = Header a
22− | MissingHeader
23− | UndecodableHeader ByteString
24− deriving (Typeable, Eq, Show, Functor)
23+type Header = Header' '[Optional, Strict]
24+
25+data Header' (mods :: [*]) (sym :: Symbol) a
26+ deriving Typeable
2527
2628-- $setup
2729-- >>> import Servant.API
servant/src/Servant/API/Internal/Test/ComprehensiveAPI.hs+3
@@ -24,13 +24,16 @@ type ComprehensiveAPIWithoutRaw =
2424 Get '[JSON] Int :<|>
2525 Capture "foo" Int :> GET :<|>
2626 Header "foo" Int :> GET :<|>
27+ Header' '[Required, Lenient] "bar" Int :> GET :<|>
2728 HttpVersion :> GET :<|>
2829 IsSecure :> GET :<|>
2930 QueryParam "foo" Int :> GET :<|>
31+ QueryParam' '[Required, Lenient] "bar" Int :> GET :<|>
3032 QueryParams "foo" Int :> GET :<|>
3133 QueryFlag "foo" :> GET :<|>
3234 RemoteHost :> GET :<|>
3335 ReqBody '[JSON] Int :> GET :<|>
36+ ReqBody' '[Lenient] '[JSON] Int :> GET :<|>
3437 Get '[JSON] (Headers '[Header "foo" Int] NoContent) :<|>
3538 "foo" :> GET :<|>
3639 Vault :> GET :<|>
servant/src/Servant/API/Modifiers.hsnew file+150
@@ -0,0 +1,150 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE KindSignatures #-}
3+{-# LANGUAGE FlexibleContexts #-}
4+{-# LANGUAGE ScopedTypeVariables #-}
5+{-# LANGUAGE TypeFamilies #-}
6+{-# LANGUAGE TypeOperators #-}
7+module Servant.API.Modifiers (
8+ -- * Required / optional argument
9+ Required, Optional,
10+ FoldRequired, FoldRequired',
11+ -- * Lenient / strict parsing
12+ Lenient, Strict,
13+ FoldLenient, FoldLenient',
14+ -- * Utilities
15+ RequiredArgument,
16+ foldRequiredArgument,
17+ unfoldRequiredArgument,
18+ RequestArgument,
19+ unfoldRequestArgument,
20+ ) where
21+
22+import Data.Proxy (Proxy (..))
23+import Data.Singletons.Bool (SBool (..), SBoolI (..))
24+import Data.Text (Text)
25+import Data.Type.Bool (If)
26+
27+-- | Required argument. Not wrapped.
28+data Required
29+
30+-- | Optional argument. Wrapped in 'Maybe'.
31+data Optional
32+
33+-- | Fold modifier list to decide whether argument is required.
34+--
35+-- >>> :kind! FoldRequired '[Required, Description "something"]
36+-- FoldRequired '[Required, Description "something"] :: Bool
37+-- = 'True
38+--
39+-- >>> :kind! FoldRequired '[Required, Optional]
40+-- FoldRequired '[Required, Optional] :: Bool
41+-- = 'False
42+--
43+-- >>> :kind! FoldRequired '[]
44+-- FoldRequired '[] :: Bool
45+-- = 'False
46+--
47+type FoldRequired mods = FoldRequired' 'False mods
48+
49+-- | Implementation of 'FoldRequired'.
50+type family FoldRequired' (acc :: Bool) (mods :: [*]) :: Bool where
51+ FoldRequired' acc '[] = acc
52+ FoldRequired' acc (Required ': mods) = FoldRequired' 'True mods
53+ FoldRequired' acc (Optional ': mods) = FoldRequired' 'False mods
54+ FoldRequired' acc (mod ': mods) = FoldRequired' acc mods
55+
56+-- | Leniently parsed argument, i.e. parsing never fail. Wrapped in @'Either' 'Text'@.
57+data Lenient
58+
59+-- | Strictly parsed argument. Not wrapped.
60+data Strict
61+
62+-- | Fold modifier list to decide whether argument should be parsed strictly or leniently.
63+--
64+-- >>> :kind! FoldLenient '[]
65+-- FoldLenient '[] :: Bool
66+-- = 'False
67+--
68+type FoldLenient mods = FoldLenient' 'False mods
69+
70+-- | Implementation of 'FoldLenient'.
71+type family FoldLenient' (acc :: Bool) (mods :: [*]) :: Bool where
72+ FoldLenient' acc '[] = acc
73+ FoldLenient' acc (Lenient ': mods) = FoldLenient' 'True mods
74+ FoldLenient' acc (Strict ': mods) = FoldLenient' 'False mods
75+ FoldLenient' acc (mod ': mods) = FoldLenient' acc mods
76+
77+-- | Helper type alias.
78+--
79+-- * 'Required' ↦ @a@
80+--
81+-- * 'Optional' ↦ @'Maybe' a@
82+--
83+type RequiredArgument mods a = If (FoldRequired mods) a (Maybe a)
84+
85+-- | Fold a 'RequiredAgument' into a value
86+foldRequiredArgument
87+ :: forall mods a r. (SBoolI (FoldRequired mods))
88+ => Proxy mods
89+ -> (a -> r) -- ^ 'Required'
90+ -> (Maybe a -> r) -- ^ 'Optional'
91+ -> RequiredArgument mods a
92+ -> r
93+foldRequiredArgument _ f g mx =
94+ case (sbool :: SBool (FoldRequired mods), mx) of
95+ (STrue, x) -> f x
96+ (SFalse, x) -> g x
97+
98+-- | Unfold a value into a 'RequiredArgument'.
99+unfoldRequiredArgument
100+ :: forall mods m a. (Monad m, SBoolI (FoldRequired mods), SBoolI (FoldLenient mods))
101+ => Proxy mods
102+ -> m (RequiredArgument mods a) -- ^ error when argument is required
103+ -> (Text -> m (RequiredArgument mods a)) -- ^ error when argument is strictly parsed
104+ -> Maybe (Either Text a) -- ^ value
105+ -> m (RequiredArgument mods a)
106+unfoldRequiredArgument _ errReq errSt mex =
107+ case (sbool :: SBool (FoldRequired mods), mex) of
108+ (STrue, Nothing) -> errReq
109+ (SFalse, Nothing) -> return Nothing
110+ (STrue, Just ex) -> either errSt return ex
111+ (SFalse, Just ex) -> either errSt (return . Just) ex
112+
113+-- | Helper type alias.
114+--
115+-- By default argument is 'Optional' and 'Strict'.
116+--
117+-- * 'Required', 'Strict' ↦ @a@
118+--
119+-- * 'Required', 'Lenient' ↦ @'Either' 'Text' a@
120+--
121+-- * 'Optional', 'Strict' ↦ @'Maybe' a@
122+--
123+-- * 'Optional', 'Lenient' ↦ @'Maybe' ('Either' 'Text' a)@
124+--
125+type RequestArgument mods a =
126+ If (FoldRequired mods)
127+ (If (FoldLenient mods) (Either Text a) a)
128+ (Maybe (If (FoldLenient mods) (Either Text a) a))
129+
130+
131+
132+-- | Unfold a value into a 'RequestArgument'.
133+unfoldRequestArgument
134+ :: forall mods m a. (Monad m, SBoolI (FoldRequired mods), SBoolI (FoldLenient mods))
135+ => Proxy mods
136+ -> m (RequestArgument mods a) -- ^ error when argument is required
137+ -> (Text -> m (RequestArgument mods a)) -- ^ error when argument is strictly parsed
138+ -> Maybe (Either Text a) -- ^ value
139+ -> m (RequestArgument mods a)
140+unfoldRequestArgument _ errReq errSt mex =
141+ case (sbool :: SBool (FoldRequired mods), mex, sbool :: SBool (FoldLenient mods)) of
142+ (STrue, Nothing, _) -> errReq
143+ (SFalse, Nothing, _) -> return Nothing
144+ (STrue, Just ex, STrue) -> return ex
145+ (STrue, Just ex, SFalse) -> either errSt return ex
146+ (SFalse, Just ex, STrue) -> return (Just ex)
147+ (SFalse, Just ex, SFalse) -> either errSt (return . Just) ex
148+
149+-- $setup
150+-- >>> import Servant.API
servant/src/Servant/API/QueryParam.hs+7−2
@@ -3,10 +3,12 @@
33{-# LANGUAGE TypeOperators #-}
44{-# LANGUAGE PolyKinds #-}
55{-# OPTIONS_HADDOCK not-home #-}
6−module Servant.API.QueryParam (QueryFlag, QueryParam, QueryParams) where
6+module Servant.API.QueryParam (QueryFlag, QueryParam, QueryParam', QueryParams) where
77
88import Data.Typeable (Typeable)
99import GHC.TypeLits (Symbol)
10+import Servant.API.Modifiers
11+
1012-- | Lookup the value associated to the @sym@ query string parameter
1113-- and try to extract it as a value of type @a@.
1214--
@@ -14,7 +16,10 @@ import GHC.TypeLits (Symbol)
1416--
1517-- >>> -- /books?author=<author name>
1618-- >>> type MyApi = "books" :> QueryParam "author" Text :> Get '[JSON] [Book]
17−data QueryParam (sym :: Symbol) (a :: *)
19+type QueryParam = QueryParam' '[Optional, Strict]
20+
21+-- | 'QueryParam' which can be 'Required', 'Lenient', or modified otherwise.
22+data QueryParam' (mods :: [*]) (sym :: Symbol) (a :: *)
1823 deriving Typeable
1924
2025-- | Lookup the values associated to the @sym@ query string parameter
servant/src/Servant/API/ReqBody.hs+12−3
@@ -2,16 +2,25 @@
22{-# LANGUAGE DeriveDataTypeable #-}
33{-# LANGUAGE PolyKinds #-}
44{-# OPTIONS_HADDOCK not-home #-}
5−module Servant.API.ReqBody where
5+module Servant.API.ReqBody (
6+ ReqBody, ReqBody',
7+ ) where
8+
9+import Data.Typeable (Typeable)
10+import Servant.API.Modifiers
611
7−import Data.Typeable (Typeable)
812-- | Extract the request body as a value of type @a@.
913--
1014-- Example:
1115--
1216-- >>> -- POST /books
1317-- >>> type MyApi = "books" :> ReqBody '[JSON] Book :> Post '[JSON] Book
14−data ReqBody (contentTypes :: [*]) (a :: *)
18+type ReqBody = ReqBody' '[Required, Strict]
19+
20+-- |
21+--
22+-- /Note:/ 'ReqBody'' is always 'Required'.
23+data ReqBody' (mods :: [*]) (contentTypes :: [*]) (a :: *)
1524 deriving (Typeable)
1625
1726-- $setup
servant/src/Servant/API/ResponseHeaders.hs+14−5
@@ -1,5 +1,6 @@
11{-# LANGUAGE CPP #-}
22{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE DeriveDataTypeable #-}
34{-# LANGUAGE DeriveFunctor #-}
45{-# LANGUAGE FlexibleContexts #-}
56{-# LANGUAGE FlexibleInstances #-}
@@ -23,6 +24,7 @@
2324-- example above).
2425module Servant.API.ResponseHeaders
2526 ( Headers(..)
27+ , ResponseHeader (..)
2628 , AddHeader
2729 , addHeader
2830 , noHeader
@@ -32,15 +34,16 @@ module Servant.API.ResponseHeaders
3234 , HList(..)
3335 ) where
3436
35−import Data.ByteString.Char8 as BS (pack, unlines, init)
37+import Data.ByteString.Char8 as BS (ByteString, pack, unlines, init)
38+import Data.Typeable (Typeable)
3639import Web.HttpApiData (ToHttpApiData, toHeader,
3740 FromHttpApiData, parseHeader)
3841import qualified Data.CaseInsensitive as CI
3942import Data.Proxy
40−import GHC.TypeLits (KnownSymbol, symbolVal)
43+import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)
4144import qualified Network.HTTP.Types.Header as HTTP
4245
43−import Servant.API.Header (Header (..))
46+import Servant.API.Header (Header)
4447import Prelude ()
4548import Prelude.Compat
4649
@@ -52,9 +55,15 @@ data Headers ls a = Headers { getResponse :: a
5255 -- ^ HList of headers.
5356 } deriving (Functor)
5457
58+data ResponseHeader (sym :: Symbol) a
59+ = Header a
60+ | MissingHeader
61+ | UndecodableHeader ByteString
62+ deriving (Typeable, Eq, Show, Functor)
63+
5564data HList a where
5665 HNil :: HList '[]
57− HCons :: Header h x -> HList xs -> HList (Header h x ': xs)
66+ HCons :: ResponseHeader h x -> HList xs -> HList (Header h x ': xs)
5867
5968type family HeaderValMap (f :: * -> *) (xs :: [*]) where
6069 HeaderValMap f '[] = '[]
@@ -110,7 +119,7 @@ instance OVERLAPPABLE_ ( KnownSymbol h, GetHeaders (HList rest), ToHttpApiData v
110119-- We need all these fundeps to save type inference
111120class AddHeader h v orig new
112121 | h v orig -> new, new -> h, new -> v, new -> orig where
113− addOptionalHeader :: Header h v -> orig -> new -- ^ N.B.: The same header can't be added multiple times
122+ addOptionalHeader :: ResponseHeader h v -> orig -> new -- ^ N.B.: The same header can't be added multiple times
114123
115124
116125instance OVERLAPPING_ ( KnownSymbol h, ToHttpApiData v )
servant/src/Servant/Utils/Links.hs+52−12
@@ -1,11 +1,13 @@
11{-# LANGUAGE ConstraintKinds #-}
22{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE FlexibleContexts #-}
34{-# LANGUAGE FlexibleInstances #-}
45{-# LANGUAGE FunctionalDependencies #-}
56{-# LANGUAGE PolyKinds #-}
67{-# LANGUAGE ScopedTypeVariables #-}
78{-# LANGUAGE TypeFamilies #-}
89{-# LANGUAGE TypeOperators #-}
10+{-# LANGUAGE UndecidableInstances #-}
911{-# OPTIONS_HADDOCK not-home #-}
1012
1113-- | Type safe generation of internal links.
@@ -101,8 +103,10 @@ module Servant.Utils.Links (
101103import Data.List
102104import Data.Monoid.Compat ( (<>) )
103105import Data.Proxy ( Proxy(..) )
106+import Data.Singletons.Bool ( SBool (..), SBoolI (..) )
104107import qualified Data.Text as Text
105108import qualified Data.Text.Encoding as TE
109+import Data.Type.Bool (If)
106110import GHC.TypeLits ( KnownSymbol, symbolVal )
107111import Network.URI ( URI(..), escapeURIString, isUnreserved )
108112import Prelude ()
@@ -112,15 +116,22 @@ import Web.HttpApiData
112116import Servant.API.Alternative ( (:<|>)((:<|>)) )
113117import Servant.API.BasicAuth ( BasicAuth )
114118import Servant.API.Capture ( Capture, CaptureAll )
115−import Servant.API.ReqBody ( ReqBody )
116−import Servant.API.QueryParam ( QueryParam, QueryParams, QueryFlag )
117−import Servant.API.Header ( Header )
119+import Servant.API.ReqBody ( ReqBody' )
120+import Servant.API.QueryParam ( QueryParam', QueryParams, QueryFlag )
121+import Servant.API.Header ( Header' )
122+import Servant.API.HttpVersion (HttpVersion)
118123import Servant.API.RemoteHost ( RemoteHost )
124+import Servant.API.IsSecure (IsSecure)
125+import Servant.API.Empty (EmptyAPI (..))
119126import Servant.API.Verbs ( Verb )
120127import Servant.API.Sub ( type (:>) )
121128import Servant.API.Raw ( Raw )
122129import Servant.API.Stream ( Stream )
123130import Servant.API.TypeLevel
131+import Servant.API.Modifiers (FoldRequired)
132+import Servant.API.Description (Description, Summary)
133+import Servant.API.Vault (Vault)
134+import Servant.API.WithNamedContext (WithNamedContext)
124135import Servant.API.Experimental.Auth ( AuthProtect )
125136
126137-- | A safe link datatype.
@@ -282,14 +293,15 @@ instance (KnownSymbol sym, HasLink sub) => HasLink (sym :> sub) where
282293 where
283294 seg = symbolVal (Proxy :: Proxy sym)
284295
285−
286296-- QueryParam instances
287−instance (KnownSymbol sym, ToHttpApiData v, HasLink sub)
288− => HasLink (QueryParam sym v :> sub) where
289− type MkLink (QueryParam sym v :> sub) = Maybe v -> MkLink sub
297+instance (KnownSymbol sym, ToHttpApiData v, HasLink sub, SBoolI (FoldRequired mods))
298+ => HasLink (QueryParam' mods sym v :> sub) where
299+ type MkLink (QueryParam' mods sym v :> sub) = If (FoldRequired mods) v (Maybe v) -> MkLink sub
290300 toLink _ l mv =
291301 toLink (Proxy :: Proxy sub) $
292− maybe id (addQueryParam . SingleParam k . toQueryParam) mv l
302+ case sbool :: SBool (FoldRequired mods) of
303+ STrue -> (addQueryParam . SingleParam k . toQueryParam) mv l
304+ SFalse -> maybe id (addQueryParam . SingleParam k . toQueryParam) mv l
293305 where
294306 k :: String
295307 k = symbolVal (Proxy :: Proxy sym)
@@ -319,8 +331,8 @@ instance (HasLink a, HasLink b) => HasLink (a :<|> b) where
319331 toLink _ l = toLink (Proxy :: Proxy a) l :<|> toLink (Proxy :: Proxy b) l
320332
321333-- Misc instances
322−instance HasLink sub => HasLink (ReqBody ct a :> sub) where
323− type MkLink (ReqBody ct a :> sub) = MkLink sub
334+instance HasLink sub => HasLink (ReqBody' mods ct a :> sub) where
335+ type MkLink (ReqBody' mods ct a :> sub) = MkLink sub
324336 toLink _ = toLink (Proxy :: Proxy sub)
325337
326338instance (ToHttpApiData v, HasLink sub)
@@ -337,8 +349,32 @@ instance (ToHttpApiData v, HasLink sub)
337349 toLink (Proxy :: Proxy sub) $
338350 foldl' (flip $ addSegment . escaped . Text.unpack . toUrlPiece) l vs
339351
340−instance HasLink sub => HasLink (Header sym a :> sub) where
341− type MkLink (Header sym a :> sub) = MkLink sub
352+instance HasLink sub => HasLink (Header' mods sym a :> sub) where
353+ type MkLink (Header' mods sym a :> sub) = MkLink sub
354+ toLink _ = toLink (Proxy :: Proxy sub)
355+
356+instance HasLink sub => HasLink (Vault :> sub) where
357+ type MkLink (Vault :> sub) = MkLink sub
358+ toLink _ = toLink (Proxy :: Proxy sub)
359+
360+instance HasLink sub => HasLink (Description s :> sub) where
361+ type MkLink (Description s :> sub) = MkLink sub
362+ toLink _ = toLink (Proxy :: Proxy sub)
363+
364+instance HasLink sub => HasLink (Summary s :> sub) where
365+ type MkLink (Summary s :> sub) = MkLink sub
366+ toLink _ = toLink (Proxy :: Proxy sub)
367+
368+instance HasLink sub => HasLink (HttpVersion :> sub) where
369+ type MkLink (HttpVersion:> sub) = MkLink sub
370+ toLink _ = toLink (Proxy :: Proxy sub)
371+
372+instance HasLink sub => HasLink (IsSecure :> sub) where
373+ type MkLink (IsSecure :> sub) = MkLink sub
374+ toLink _ = toLink (Proxy :: Proxy sub)
375+
376+instance HasLink sub => HasLink (WithNamedContext name context sub) where
377+ type MkLink (WithNamedContext name context sub) = MkLink sub
342378 toLink _ = toLink (Proxy :: Proxy sub)
343379
344380instance HasLink sub => HasLink (RemoteHost :> sub) where
@@ -349,6 +385,10 @@ instance HasLink sub => HasLink (BasicAuth realm a :> sub) where
349385 type MkLink (BasicAuth realm a :> sub) = MkLink sub
350386 toLink _ = toLink (Proxy :: Proxy sub)
351387
388+instance HasLink EmptyAPI where
389+ type MkLink EmptyAPI = EmptyAPI
390+ toLink _ _ = EmptyAPI
391+
352392-- Verb (terminal) instances
353393instance HasLink (Verb m s ct a) where
354394 type MkLink (Verb m s ct a) = Link
servant/test/Servant/Utils/LinksSpec.hs+19−7
@@ -1,8 +1,12 @@
1+{-# LANGUAGE CPP #-}
12{-# LANGUAGE ConstraintKinds #-}
23{-# LANGUAGE DataKinds #-}
34{-# LANGUAGE PolyKinds #-}
45{-# LANGUAGE TypeOperators #-}
56{-# LANGUAGE ScopedTypeVariables #-}
7+#if __GLASGOW__HASKELL < 709
8+{-# OPTIONS_GHC -fcontext-stack=41 #-}
9+#endif
610module Servant.Utils.LinksSpec where
711
812import Data.Proxy (Proxy (..))
@@ -11,11 +15,13 @@ import Test.Hspec (Expectation, Spec, describe, it,
1115import Data.String (fromString)
1216
1317import Servant.API
14−import Servant.Utils.Links (allLinks)
18+import Servant.Utils.Links (allLinks, linkURI)
19+import Servant.API.Internal.Test.ComprehensiveAPI (comprehensiveAPIWithoutRaw)
1520
1621type TestApi =
1722 -- Capture and query params
1823 "hello" :> Capture "name" String :> QueryParam "capital" Bool :> Delete '[JSON] NoContent
24+ :<|> "hi" :> Capture "name" String :> QueryParam' '[Required] "capital" Bool :> Delete '[JSON] NoContent
1925 :<|> "all" :> CaptureAll "names" String :> Get '[JSON] NoContent
2026
2127 -- Flags
@@ -55,6 +61,11 @@ spec = describe "Servant.Utils.Links" $ do
5561 :> Delete '[JSON] NoContent)
5662 apiLink l2 "bye" (Just True) `shouldBeLink` "hello/bye?capital=true"
5763
64+ let l4 = Proxy :: Proxy ("hi" :> Capture "name" String
65+ :> QueryParam' '[Required] "capital" Bool
66+ :> Delete '[JSON] NoContent)
67+ apiLink l4 "privet" False `shouldBeLink` "hi/privet?capital=false"
68+
5869 it "generates correct links for CaptureAll" $ do
5970 apiLink (Proxy :: Proxy ("all" :> CaptureAll "names" String :> Get '[JSON] NoContent))
6071 ["roads", "lead", "to", "rome"]
@@ -75,11 +86,12 @@ spec = describe "Servant.Utils.Links" $ do
7586
7687 it "can generate all links for an API that has only linkable endpoints" $ do
7788 let (allNames :<|> simple) = allLinks (Proxy :: Proxy LinkableApi)
78− simple
79− `shouldBeLink` "get"
80− allNames ["Seneca", "Aurelius"]
81− `shouldBeLink` "all/Seneca/Aurelius"
89+ simple `shouldBeLink` "get"
90+ allNames ["Seneca", "Aurelius"] `shouldBeLink` "all/Seneca/Aurelius"
8291
92+ it "can generate all links for ComprehensiveAPIWithoutRaw" $ do
93+ let (firstLink :<|> _) = allLinks comprehensiveAPIWithoutRaw
94+ firstLink `shouldBeLink` ""
8395
8496-- |
8597-- Before https://github.com/CRogers/should-not-typecheck/issues/5 is fixed,
@@ -112,9 +124,9 @@ spec = describe "Servant.Utils.Links" $ do
112124-- ...Could not deduce...
113125-- ...
114126--
115−-- >>> apiLink (Proxy :: Proxy NoEndpoint)
127+-- >>> linkURI $ apiLink (Proxy :: Proxy NoEndpoint)
116128-- ...
117−-- ...No instance for...
129+-- <interactive>...
118130-- ...
119131--
120132-- sanity check