on master help sign in
Add Accept header handling.
380acb3Julian K. Arni
default.nixnew file+15
@@ -0,0 +1,15 @@
1+{ pkgs ? import <nixpkgs> { config.allowUnfree = true; }
2+, src ? builtins.filterSource (path: type:
3+ type != "unknown" &&
4+ baseNameOf path != ".git" &&
5+ baseNameOf path != "result" &&
6+ baseNameOf path != "dist") ./.
7+, servant ? import ../servant {}
8+}:
9+pkgs.haskellPackages.buildLocalCabalWithArgs {
10+ name = "servant-server";
11+ inherit src;
12+ args = {
13+ inherit servant;
14+ };
15+}
servant-server.cabal+2
@@ -31,6 +31,7 @@ library
31 exposed-modules:
31 exposed-modules:
32 Servant
32 Servant
33 Servant.Server
33 Servant.Server
34+ Servant.Server.ContentTypes
34 Servant.Server.Internal
35 Servant.Server.Internal
35 Servant.Utils.StaticFiles
36 Servant.Utils.StaticFiles
36 build-depends:
37 build-depends:
@@ -41,6 +42,7 @@ library
41 , either >= 4.3
42 , either >= 4.3
42 , http-types
43 , http-types
43 , network-uri >= 2.6
44 , network-uri >= 2.6
45+ , http-media == 0.4.*
44 , safe
46 , safe
45 , servant >= 0.2.2
47 , servant >= 0.2.2
46 , split
48 , split
src/Servant/Server/ContentTypes.hsnew file+114
@@ -0,0 +1,114 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE FlexibleInstances #-}
3+{-# LANGUAGE MultiParamTypeClasses #-}
4+{-# LANGUAGE OverloadedStrings #-}
5+{-# LANGUAGE PolyKinds #-}
6+{-# LANGUAGE ScopedTypeVariables #-}
7+{-# LANGUAGE TypeFamilies #-}
8+{-# LANGUAGE TypeOperators #-}
9+{-# LANGUAGE UndecidableInstances #-}
10+module Servant.Server.ContentTypes where
11+
12+import Data.Aeson (ToJSON(..), encode)
13+import Data.ByteString.Lazy (ByteString)
14+import qualified Data.ByteString as BS
15+import Data.Proxy (Proxy(..))
16+import Data.String.Conversions (cs)
17+import qualified Network.HTTP.Media as M
18+
19+
20+import Servant.API (XML, HTML, JSON, JavaScript, CSS, PlainText)
21+
22+-- | Instances of 'Accept' represent mimetypes. They are used for matching
23+-- against the @Accept@ HTTP header of the request, and for setting the
24+-- @Content-Type@ header of the response
25+--
26+-- Example:
27+--
28+-- instance Accept HTML where
29+-- contentType _ = "text" // "html"
30+--
31+class Accept ctype where
32+ contentType :: Proxy ctype -> M.MediaType
33+
34+instance Accept HTML where
35+ contentType _ = "text" M.// "html"
36+
37+instance Accept JSON where
38+ contentType _ = "application" M.// "json"
39+
40+instance Accept XML where
41+ contentType _ = "application" M.// "xml"
42+
43+instance Accept JavaScript where
44+ contentType _ = "application" M.// "javascript"
45+
46+instance Accept CSS where
47+ contentType _ = "text" M.// "css"
48+
49+instance Accept PlainText where
50+ contentType _ = "text" M.// "plain"
51+
52+newtype AcceptHeader = AcceptHeader BS.ByteString
53+ deriving (Eq, Show)
54+
55+-- | Instantiate this class to register a way of serializing a type based
56+-- on the @Accept@ header.
57+class Accept ctype => MimeRender ctype a where
58+ toByteString :: Proxy ctype -> a -> ByteString
59+
60+class AllCTRender list a where
61+ -- If the Accept header can be matched, returns (Just) a tuple of the
62+ -- Content-Type and response (serialization of @a@ into the appropriate
63+ -- mimetype).
64+ handleAcceptH :: Proxy list -> AcceptHeader -> a -> Maybe (ByteString, ByteString)
65+
66+instance ( AllMimeRender ctyps a, IsEmpty ctyps ~ 'False
67+ ) => AllCTRender ctyps a where
68+ handleAcceptH _ (AcceptHeader accept) val = M.mapAcceptMedia lkup accept
69+ where pctyps = Proxy :: Proxy ctyps
70+ amrs = amr pctyps val
71+ lkup = zip (map fst amrs) $ map (\(a,b) -> (cs $ show a, b)) amrs
72+
73+
74+--------------------------------------------------------------------------
75+-- Check that all elements of list are instances of MimeRender
76+--------------------------------------------------------------------------
77+class AllMimeRender ls a where
78+ amr :: Proxy ls -> a -> [(M.MediaType, ByteString)] -- list of content-types/response pairs
79+
80+instance ( MimeRender ctyp a ) => AllMimeRender '[ctyp] a where
81+ amr _ a = [(contentType pctyp, toByteString pctyp a)]
82+ where pctyp = Proxy :: Proxy ctyp
83+
84+instance ( MimeRender ctyp a
85+ , MimeRender ctyp' a
86+ , AllMimeRender ctyps a
87+ ) => AllMimeRender (ctyp ': ctyp' ': ctyps) a where
88+ amr _ a = (contentType pctyp, toByteString pctyp a)
89+ :(contentType pctyp', toByteString pctyp' a)
90+ :(amr pctyps a)
91+ where pctyp = Proxy :: Proxy ctyp
92+ pctyps = Proxy :: Proxy ctyps
93+ pctyp' = Proxy :: Proxy ctyp'
94+
95+
96+instance AllMimeRender '[] a where
97+ amr _ _ = []
98+
99+type family IsEmpty (ls::[*]) where
100+ IsEmpty '[] = 'True
101+ IsEmpty x = 'False
102+
103+--------------------------------------------------------------------------
104+-- MimeRender Instances
105+--------------------------------------------------------------------------
106+
107+instance ToJSON a => MimeRender JSON a where
108+ toByteString _ = encode
109+
110+instance Show a => MimeRender PlainText a where
111+ toByteString _ = encode . show
112+
113+instance MimeRender PlainText String where
114+ toByteString _ = encode
src/Servant/Server/Internal.hs+36−17
@@ -24,10 +24,14 @@ import qualified Data.Text as T
24import Data.Typeable
24import Data.Typeable
25import GHC.TypeLits (KnownSymbol, symbolVal)
25import GHC.TypeLits (KnownSymbol, symbolVal)
26import Network.HTTP.Types hiding (Header)
26import Network.HTTP.Types hiding (Header)
27−import Network.Wai (Response, Request, ResponseReceived, Application, pathInfo, requestBody,
27+import Network.Wai ( Response, Request, ResponseReceived, Application
28+ , pathInfo, requestBody, strictRequestBody
28− strictRequestBody, lazyRequestBody, requestHeaders, requestMethod,
29+ , lazyRequestBody, requestHeaders, requestMethod,
29 rawQueryString, responseLBS)
30 rawQueryString, responseLBS)
30−import Servant.API (QueryParams, QueryParam, QueryFlag, MatrixParams, MatrixParam, MatrixFlag, ReqBody, Header, Capture, Get, Delete, Put, Post, Patch, Raw, (:>), (:<|>)(..))
31+import Servant.API ( QueryParams, QueryParam, QueryFlag, ReqBody, Header
32+ , MatrixParams, MatrixParam, MatrixFlag,
33+ , Capture, Get, Delete, Put, Post, Patch, Raw, (:>), (:<|>)(..))
34+import Servant.Server.ContentTypes (AllCTRender(..), AcceptHeader(..))
31import Servant.Common.Text (FromText, fromText)
35import Servant.Common.Text (FromText, fromText)
32
36
33data ReqBodyState = Uncalled
37data ReqBodyState = Uncalled
@@ -225,7 +229,7 @@ instance (KnownSymbol capture, FromText a, HasServer sublayout)
225 _ -> respond $ failWith NotFound
229 _ -> respond $ failWith NotFound
226
230
227 where captureProxy = Proxy :: Proxy (Capture capture a)
231 where captureProxy = Proxy :: Proxy (Capture capture a)
228−
232+
229
233
230-- | If you have a 'Delete' endpoint in your API,
234-- | If you have a 'Delete' endpoint in your API,
231-- the handler for this endpoint is meant to delete
235-- the handler for this endpoint is meant to delete
@@ -264,14 +268,19 @@ instance HasServer Delete where
264-- If successfully returning a value, we just require that its type has
268-- If successfully returning a value, we just require that its type has
265-- a 'ToJSON' instance and servant takes care of encoding it for you,
269-- a 'ToJSON' instance and servant takes care of encoding it for you,
266-- yielding status code 200 along the way.
270-- yielding status code 200 along the way.
271+instance ( AllCTRender ctypes a, ToJSON a
267−instance ToJSON result => HasServer (Get result) where
272+ ) => HasServer (Get ctypes a) where
268− type Server (Get result) = EitherT (Int, String) IO result
273+ type Server (Get ctypes a) = EitherT (Int, String) IO a
269 route Proxy action request respond
274 route Proxy action request respond
270 | pathIsEmpty request && requestMethod request == methodGet = do
275 | pathIsEmpty request && requestMethod request == methodGet = do
271 e <- runEitherT action
276 e <- runEitherT action
272 respond . succeedWith $ case e of
277 respond . succeedWith $ case e of
273− Right output ->
278+ Right output -> do
279+ let accH = fromMaybe "*/*" $ lookup hAccept $ requestHeaders request
280+ case handleAcceptH (Proxy :: Proxy ctypes) (AcceptHeader accH) output of
281+ Nothing -> responseLBS (mkStatus 406 "") [] ""
274− responseLBS ok200 [("Content-Type", "application/json")] (encode output)
282+ Just (contentT, body) -> responseLBS ok200 [ ("Content-Type"
283+ , cs contentT)] body
275 Left (status, message) ->
284 Left (status, message) ->
276 responseLBS (mkStatus status (cs message)) [] (cs message)
285 responseLBS (mkStatus status (cs message)) [] (cs message)
277 | pathIsEmpty request && requestMethod request /= methodGet =
286 | pathIsEmpty request && requestMethod request /= methodGet =
@@ -321,15 +330,20 @@ instance (KnownSymbol sym, FromText a, HasServer sublayout)
321-- If successfully returning a value, we just require that its type has
330-- If successfully returning a value, we just require that its type has
322-- a 'ToJSON' instance and servant takes care of encoding it for you,
331-- a 'ToJSON' instance and servant takes care of encoding it for you,
323-- yielding status code 201 along the way.
332-- yielding status code 201 along the way.
333+instance ( AllCTRender ctypes a, ToJSON a
324−instance ToJSON a => HasServer (Post a) where
334+ )=> HasServer (Post ctypes a) where
325− type Server (Post a) = EitherT (Int, String) IO a
335+ type Server (Post ctypes a) = EitherT (Int, String) IO a
326
336
327 route Proxy action request respond
337 route Proxy action request respond
328 | pathIsEmpty request && requestMethod request == methodPost = do
338 | pathIsEmpty request && requestMethod request == methodPost = do
329 e <- runEitherT action
339 e <- runEitherT action
330 respond . succeedWith $ case e of
340 respond . succeedWith $ case e of
331− Right out ->
341+ Right output -> do
342+ let accH = fromMaybe "*/*" $ lookup hAccept $ requestHeaders request
343+ case handleAcceptH (Proxy :: Proxy ctypes) (AcceptHeader accH) output of
344+ Nothing -> responseLBS (mkStatus 406 "") [] ""
332− responseLBS status201 [("Content-Type", "application/json")] (encode out)
345+ Just (contentT, body) -> responseLBS status201 [ ("Content-Type"
346+ , cs contentT)] body
333 Left (status, message) ->
347 Left (status, message) ->
334 responseLBS (mkStatus status (cs message)) [] (cs message)
348 responseLBS (mkStatus status (cs message)) [] (cs message)
335 | pathIsEmpty request && requestMethod request /= methodPost =
349 | pathIsEmpty request && requestMethod request /= methodPost =
@@ -347,15 +361,20 @@ instance ToJSON a => HasServer (Post a) where
347-- If successfully returning a value, we just require that its type has
361-- If successfully returning a value, we just require that its type has
348-- a 'ToJSON' instance and servant takes care of encoding it for you,
362-- a 'ToJSON' instance and servant takes care of encoding it for you,
349-- yielding status code 200 along the way.
363-- yielding status code 200 along the way.
364+instance ( AllCTRender ctypes a, ToJSON a
350−instance ToJSON a => HasServer (Put a) where
365+ ) => HasServer (Put ctypes a) where
351− type Server (Put a) = EitherT (Int, String) IO a
366+ type Server (Put ctypes a) = EitherT (Int, String) IO a
352
367
353 route Proxy action request respond
368 route Proxy action request respond
354 | pathIsEmpty request && requestMethod request == methodPut = do
369 | pathIsEmpty request && requestMethod request == methodPut = do
355 e <- runEitherT action
370 e <- runEitherT action
356 respond . succeedWith $ case e of
371 respond . succeedWith $ case e of
357− Right out ->
372+ Right output -> do
358− responseLBS ok200 [("Content-Type", "application/json")] (encode out)
373+ let accH = fromMaybe "*/*" $ lookup hAccept $ requestHeaders request
374+ case handleAcceptH (Proxy :: Proxy ctypes) (AcceptHeader accH) output of
375+ Nothing -> responseLBS (mkStatus 406 "") [] ""
376+ Just (contentT, body) -> responseLBS status200 [ ("Content-Type"
377+ , cs contentT)] body
359 Left (status, message) ->
378 Left (status, message) ->
360 responseLBS (mkStatus status (cs message)) [] (cs message)
379 responseLBS (mkStatus status (cs message)) [] (cs message)
361 | pathIsEmpty request && requestMethod request /= methodPut =
380 | pathIsEmpty request && requestMethod request /= methodPut =
@@ -382,7 +401,7 @@ instance (Typeable a, ToJSON a) => HasServer (Patch a) where
382 e <- runEitherT action
401 e <- runEitherT action
383 respond . succeedWith $ case e of
402 respond . succeedWith $ case e of
384 Right out -> case cast out of
403 Right out -> case cast out of
385− Nothing -> responseLBS status200 [("Content-Type", "application/json")] (encode out)
404+ Nothing -> responseLBS status200 [("Content-Type", "application/json")] (encode out)
386 Just () -> responseLBS status204 [] ""
405 Just () -> responseLBS status204 [] ""
387 Left (status, message) ->
406 Left (status, message) ->
388 responseLBS (mkStatus status (cs message)) [] (cs message)
407 responseLBS (mkStatus status (cs message)) [] (cs message)
test/Servant/Server/ContentTypesSpec.hsnew file+129
@@ -0,0 +1,129 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE FlexibleInstances #-}
3+{-# LANGUAGE MultiParamTypeClasses #-}
4+{-# LANGUAGE OverloadedStrings #-}
5+{-# LANGUAGE TypeOperators #-}
6+{-# OPTIONS_GHC -fno-warn-orphans #-}
7+module Servant.Server.ContentTypesSpec where
8+
9+import Control.Applicative
10+import Data.Aeson (encode)
11+import Data.ByteString.Char8
12+import Data.Function (on)
13+import Data.Maybe (isJust, fromJust)
14+import Data.List (maximumBy)
15+import Data.Proxy (Proxy(..))
16+import Data.String (IsString(..))
17+import Data.String.Conversions (cs)
18+import Network.HTTP.Types (hAccept)
19+import Network.Wai (pathInfo, requestHeaders)
20+import Network.Wai.Test ( runSession, request, defaultRequest
21+ , assertContentType, assertStatus )
22+import Test.Hspec
23+import Test.QuickCheck
24+
25+import Servant.API
26+import Servant.Server
27+import Servant.Server.ContentTypes
28+
29+
30+spec :: Spec
31+spec = describe "Servant.Server.ContentTypes" $ do
32+ handleAcceptHSpec
33+ contentTypeSpec
34+
35+handleAcceptHSpec :: Spec
36+handleAcceptHSpec = describe "handleAcceptH" $ do
37+
38+ it "should return Just if the 'Accept' header matches" $ do
39+ handleAcceptH (Proxy :: Proxy '[JSON]) "*/*" (3 :: Int)
40+ `shouldSatisfy` isJust
41+ handleAcceptH (Proxy :: Proxy '[XML, JSON]) "application/json" (3 :: Int)
42+ `shouldSatisfy` isJust
43+ handleAcceptH (Proxy :: Proxy '[XML, JSON, HTML]) "text/html" (3 :: Int)
44+ `shouldSatisfy` isJust
45+
46+ it "should return the Content-Type as the first element of the tuple" $ do
47+ handleAcceptH (Proxy :: Proxy '[JSON]) "*/*" (3 :: Int)
48+ `shouldSatisfy` ((== "application/json") . fst . fromJust)
49+ handleAcceptH (Proxy :: Proxy '[XML, JSON]) "application/json" (3 :: Int)
50+ `shouldSatisfy` ((== "application/json") . fst . fromJust)
51+ handleAcceptH (Proxy :: Proxy '[XML, JSON, HTML]) "text/html" (3 :: Int)
52+ `shouldSatisfy` ((== "text/html") . fst . fromJust)
53+
54+ it "should return the appropriately serialized representation" $ do
55+ property $ \x -> handleAcceptH (Proxy :: Proxy '[JSON]) "*/*" (x :: Int)
56+ == Just ("application/json", encode x)
57+
58+ it "respects the Accept spec ordering" $
59+ property $ \a b c i -> fst (fromJust $ val a b c i) == (fst $ highest a b c)
60+ where
61+ highest a b c = maximumBy (compare `on` snd) [ ("text/html", a)
62+ , ("application/json", b)
63+ , ("application/xml", c)
64+ ]
65+ acceptH a b c = addToAccept (Proxy :: Proxy HTML) a $
66+ addToAccept (Proxy :: Proxy JSON) b $
67+ addToAccept (Proxy :: Proxy XML ) c ""
68+ val a b c i = handleAcceptH (Proxy :: Proxy '[HTML, JSON, XML])
69+ (acceptH a b c) (i :: Int)
70+
71+type ContentTypeApi = "foo" :> Get '[JSON] Int
72+ :<|> "bar" :> Get '[JSON, PlainText] Int
73+
74+contentTypeApi :: Proxy ContentTypeApi
75+contentTypeApi = Proxy
76+
77+contentTypeServer :: Server ContentTypeApi
78+contentTypeServer = return 5 :<|> return 3
79+
80+contentTypeSpec :: Spec
81+contentTypeSpec = do
82+ describe "Accept Headers" $ do
83+
84+ it "uses the highest quality possible in the header" $
85+ flip runSession (serve contentTypeApi contentTypeServer) $ do
86+ let acceptH = "text/plain; q=0.9, application/json; q=0.8"
87+ response <- Network.Wai.Test.request defaultRequest{
88+ requestHeaders = [(hAccept, acceptH)] ,
89+ pathInfo = ["bar"]
90+ }
91+ assertContentType "text/plain" response
92+
93+ it "returns the first content-type if the Accept header is missing" $
94+ flip runSession (serve contentTypeApi contentTypeServer) $ do
95+ response <- Network.Wai.Test.request defaultRequest{
96+ pathInfo = ["bar"]
97+ }
98+ assertContentType "application/json" response
99+
100+ it "returns 406 if it can't serve the requested content-type" $
101+ flip runSession (serve contentTypeApi contentTypeServer) $ do
102+ let acceptH = "text/css"
103+ response <- Network.Wai.Test.request defaultRequest{
104+ requestHeaders = [(hAccept, acceptH)] ,
105+ pathInfo = ["bar"]
106+ }
107+ assertStatus 406 response
108+
109+
110+instance Show a => MimeRender HTML a where
111+ toByteString _ = cs . show
112+
113+instance Show a => MimeRender XML a where
114+ toByteString _ = cs . show
115+
116+instance IsString AcceptHeader where
117+ fromString = AcceptHeader . fromString
118+
119+addToAccept :: Accept a => Proxy a -> ZeroToOne -> AcceptHeader -> AcceptHeader
120+addToAccept p (ZeroToOne f) (AcceptHeader h) = AcceptHeader (cont h)
121+ where new = cs (show $ contentType p) `append` "; q=" `append` pack (show f)
122+ cont "" = new
123+ cont old = old `append` ", " `append` new
124+
125+newtype ZeroToOne = ZeroToOne Float
126+ deriving (Eq, Show, Ord)
127+
128+instance Arbitrary ZeroToOne where
129+ arbitrary = ZeroToOne <$> elements [ x / 10 | x <- [1..10]]
test/Servant/ServerSpec.hs+10−9
@@ -22,6 +22,7 @@ import Network.Wai.Test (runSession, request, defaultRequest, simpleBody)
22import Test.Hspec (Spec, describe, it, shouldBe)
22import Test.Hspec (Spec, describe, it, shouldBe)
23import Test.Hspec.Wai (liftIO, with, get, post, shouldRespondWith, matchStatus)
23import Test.Hspec.Wai (liftIO, with, get, post, shouldRespondWith, matchStatus)
24
24
25+import Servant.API (JSON)
25import Servant.API.Capture (Capture)
26import Servant.API.Capture (Capture)
26import Servant.API.Get (Get)
27import Servant.API.Get (Get)
27import Servant.API.ReqBody (ReqBody)
28import Servant.API.ReqBody (ReqBody)
@@ -79,7 +80,7 @@ spec = do
79 errorsSpec
80 errorsSpec
80
81
81
82
82−type CaptureApi = Capture "legs" Integer :> Get Animal
83+type CaptureApi = Capture "legs" Integer :> Get '[JSON] Animal
83captureApi :: Proxy CaptureApi
84captureApi :: Proxy CaptureApi
84captureApi = Proxy
85captureApi = Proxy
85captureServer :: Integer -> EitherT (Int, String) IO Animal
86captureServer :: Integer -> EitherT (Int, String) IO Animal
@@ -105,7 +106,7 @@ captureSpec = do
105 get "/captured/foo" `shouldRespondWith` (fromString (show ["foo" :: String]))
106 get "/captured/foo" `shouldRespondWith` (fromString (show ["foo" :: String]))
106
107
107
108
108−type GetApi = Get Person
109+type GetApi = Get '[JSON] Person
109getApi :: Proxy GetApi
110getApi :: Proxy GetApi
110getApi = Proxy
111getApi = Proxy
111
112
@@ -123,9 +124,9 @@ getSpec = do
123 post "/" "" `shouldRespondWith` 405
124 post "/" "" `shouldRespondWith` 405
124
125
125
126
126−type QueryParamApi = QueryParam "name" String :> Get Person
127+type QueryParamApi = QueryParam "name" String :> Get '[JSON] Person
127− :<|> "a" :> QueryParams "names" String :> Get Person
128+ :<|> "a" :> QueryParams "names" String :> Get '[JSON] Person
128− :<|> "b" :> QueryFlag "capitalize" :> Get Person
129+ :<|> "b" :> QueryFlag "capitalize" :> Get '[JSON] Person
129
130
130queryParamApi :: Proxy QueryParamApi
131queryParamApi :: Proxy QueryParamApi
131queryParamApi = Proxy
132queryParamApi = Proxy
@@ -289,8 +290,8 @@ matrixParamSpec = do
289 }
290 }
290
291
291type PostApi =
292type PostApi =
292− ReqBody Person :> Post Integer
293+ ReqBody Person :> Post '[JSON] Integer
293− :<|> "bla" :> ReqBody Person :> Post Integer
294+ :<|> "bla" :> ReqBody Person :> Post '[JSON] Integer
294postApi :: Proxy PostApi
295postApi :: Proxy PostApi
295postApi = Proxy
296postApi = Proxy
296
297
@@ -344,8 +345,8 @@ rawSpec = do
344
345
345
346
346type AlternativeApi =
347type AlternativeApi =
347− "foo" :> Get Person
348+ "foo" :> Get '[JSON] Person
348− :<|> "bar" :> Get Animal
349+ :<|> "bar" :> Get '[JSON] Animal
349unionApi :: Proxy AlternativeApi
350unionApi :: Proxy AlternativeApi
350unionApi = Proxy
351unionApi = Proxy
351
352
test/Servant/Utils/StaticFilesSpec.hs+2−1
@@ -13,6 +13,7 @@ import System.IO.Temp (withSystemTempDirectory)
13import Test.Hspec (Spec, describe, it, around_)
13import Test.Hspec (Spec, describe, it, around_)
14import Test.Hspec.Wai (with, get, shouldRespondWith)
14import Test.Hspec.Wai (with, get, shouldRespondWith)
15
15
16+import Servant.API (JSON)
16import Servant.API.Alternative ((:<|>)((:<|>)))
17import Servant.API.Alternative ((:<|>)((:<|>)))
17import Servant.API.Capture (Capture)
18import Servant.API.Capture (Capture)
18import Servant.API.Get (Get)
19import Servant.API.Get (Get)
@@ -23,7 +24,7 @@ import Servant.ServerSpec (Person(Person))
23import Servant.Utils.StaticFiles (serveDirectory)
24import Servant.Utils.StaticFiles (serveDirectory)
24
25
25type Api =
26type Api =
26− "dummy_api" :> Capture "person_name" String :> Get Person
27+ "dummy_api" :> Capture "person_name" String :> Get '[JSON] Person
27 :<|> "static" :> Raw
28 :<|> "static" :> Raw
28
29
29
30