on master help sign in
Merge remote-tracking branch 'servant-server/prepare-merge' into merge
commit29a54fe2901563f98fc30c4a5829959598bbcfc7 authorJulian K. Arni <jkarni@gmail.com> dateMon 20 Apr 2015, 11:23 (11 years ago) parent90ad824ed502f5
16 files changed+1978
servant-server/CHANGELOG.md+26 servant-server/LICENSE+30 servant-server/README.md+20 servant-server/Setup.hs+2 servant-server/default.nix+15 servant-server/example/README.md+2 servant-server/example/greet.hs+72 servant-server/example/greet.md+52 servant-server/servant-server.cabal+101 servant-server/src/Servant.hs+22 servant-server/src/Servant/Server.hs+49 servant-server/src/Servant/Server/Internal.hs+866 servant-server/src/Servant/Utils/StaticFiles.hs+36 servant-server/test/Servant/ServerSpec.hs+619 servant-server/test/Servant/Utils/StaticFilesSpec.hs+65 servant-server/test/Spec.hs+1
servant-server/CHANGELOG.mdnew file+26
@@ -0,0 +1,26 @@
1+0.3
2+---
3+* Add a `RouteMismatch` constructor for arbitrary HTTP response codes (https://github.com/haskell-servant/servant-server/pull/22)
4+* Add support for the `Patch` combinator
5+* Support for `Accept`/`Content-type` headers and for the content-type aware combinators in *servant-0.3*
6+* Export `toApplication` from `Servant.Server` (https://github.com/haskell-servant/servant-server/pull/29)
7+* Support other Monads than just `EitherT (Int, String) IO` (https://github.com/haskell-servant/servant-server/pull/21)
8+* Canonicalize API types before generating the handler types with `Server`
9+* Make methods return status code 204 if they return () (https://github.com/haskell-servant/servant-server/issues/28)
10+* Add server support for response headers
11+
12+0.2.4
13+-----
14+* Added support for matrix parameters, see e.g. http://www.w3.org/DesignIssues/MatrixURIs.html
15+* Add support for serializing based on Accept header
16+ (https://github.com/haskell-servant/servant-server/issues/9)
17+* Ignore trailing slashes
18+ (https://github.com/haskell-servant/servant-server/issues/5)
19+
20+
21+0.2.3
22+-----
23+
24+* Fix consuming request body issue
25+ (https://github.com/haskell-servant/servant/issues/3)
26+* Make code sample in Servant.Server complete
servant-server/LICENSEnew file+30
@@ -0,0 +1,30 @@
1+Copyright (c) 2014, Zalora South East Asia Pte Ltd
2+
3+All rights reserved.
4+
5+Redistribution and use in source and binary forms, with or without
6+modification, are permitted provided that the following conditions are met:
7+
8+ * Redistributions of source code must retain the above copyright
9+ notice, this list of conditions and the following disclaimer.
10+
11+ * Redistributions in binary form must reproduce the above
12+ copyright notice, this list of conditions and the following
13+ disclaimer in the documentation and/or other materials provided
14+ with the distribution.
15+
16+ * Neither the name of Zalora South East Asia Pte Ltd nor the names of other
17+ contributors may be used to endorse or promote products derived
18+ from this software without specific prior written permission.
19+
20+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
servant-server/README.mdnew file+20
@@ -0,0 +1,20 @@
1+# servant-server
2+
3+[![Build Status](https://secure.travis-ci.org/haskell-servant/servant-server.svg)](http://travis-ci.org/haskell-servant/servant-server)
4+[![Coverage Status](https://coveralls.io/repos/haskell-servant/servant-server/badge.svg)](https://coveralls.io/r/haskell-servant/servant-server)
5+
6+![servant](https://raw.githubusercontent.com/haskell-servant/servant/master/servant.png)
7+
8+This library lets you *implement* an HTTP server with handlers for each endpoint of a servant API, handling most of the boilerplate for you.
9+
10+## Getting started
11+
12+We've written a [Getting Started](http://haskell-servant.github.io/getting-started/) guide that introduces the core types and features of servant. After this article, you should be able to write your first servant webservices, learning the rest from the haddocks' examples.
13+
14+## Repositories and Haddocks
15+
16+- The core [servant](http://github.com/haskell-servant) package - [docs](http://hackage.haskell.org/package/servant)
17+- Implementing an HTTP server for a webservice API with [servant-server](http://github.com/haskell-servant/servant-server) - [docs](http://hackage.haskell.org/package/servant-server)
18+- (Haskell) client-side function generation with [servant-client](http://github.com/haskell-servant/servant-client) - [docs](http://hackage.haskell.org/package/servant-client)
19+- (Javascript) client-side function generation with [servant-jquery](http://github.com/haskell-servant/servant-jquery) - [docs](http://hackage.haskell.org/package/servant-jquery)
20+- API docs generation with [servant-docs](http://github.com/haskell-servant/servant-docs) - [docs](http://hackage.haskell.org/package/servant-docs)
servant-server/Setup.hsnew file+2
@@ -0,0 +1,2 @@
1+import Distribution.Simple
2+main = defaultMain
servant-server/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/example/README.mdnew file+2
@@ -0,0 +1,2 @@
1+- `greet.hs` shows how to write a simple webservice, run it, query it with automatically-derived haskell functions and print the (generated) markdown documentation for the API.
2+- `greet.md` contains the aforementionned generated documentation.
servant-server/example/greet.hsnew file+72
@@ -0,0 +1,72 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE PolyKinds #-}
3+{-# LANGUAGE TypeFamilies #-}
4+{-# LANGUAGE DeriveGeneric #-}
5+{-# LANGUAGE TypeOperators #-}
6+{-# LANGUAGE OverloadedStrings #-}
7+
8+import Data.Aeson
9+import Data.Monoid
10+import Data.Proxy
11+import Data.Text
12+import GHC.Generics
13+import Network.Wai
14+import Network.Wai.Handler.Warp
15+
16+import Servant
17+
18+-- * Example
19+
20+-- | A greet message data type
21+newtype Greet = Greet { _msg :: Text }
22+ deriving (Generic, Show)
23+
24+instance FromJSON Greet
25+instance ToJSON Greet
26+
27+-- API specification
28+type TestApi =
29+ -- GET /hello/:name?capital={true, false} returns a Greet as JSON
30+ "hello" :> Capture "name" Text :> QueryParam "capital" Bool :> Get '[JSON] Greet
31+
32+ -- POST /greet with a Greet as JSON in the request body,
33+ -- returns a Greet as JSON
34+ :<|> "greet" :> ReqBody '[JSON] Greet :> Post '[JSON] Greet
35+
36+ -- DELETE /greet/:greetid
37+ :<|> "greet" :> Capture "greetid" Text :> Delete
38+
39+testApi :: Proxy TestApi
40+testApi = Proxy
41+
42+-- Server-side handlers.
43+--
44+-- There's one handler per endpoint, which, just like in the type
45+-- that represents the API, are glued together using :<|>.
46+--
47+-- Each handler runs in the 'EitherT (Int, String) IO' monad.
48+server :: Server TestApi
49+server = helloH :<|> postGreetH :<|> deleteGreetH
50+
51+ where helloH name Nothing = helloH name (Just False)
52+ helloH name (Just False) = return . Greet $ "Hello, " <> name
53+ helloH name (Just True) = return . Greet . toUpper $ "Hello, " <> name
54+
55+ postGreetH greet = return greet
56+
57+ deleteGreetH _ = return ()
58+
59+-- Turn the server into a WAI app. 'serve' is provided by servant,
60+-- more precisely by the Servant.Server module.
61+test :: Application
62+test = serve testApi server
63+
64+-- Run the server.
65+--
66+-- 'run' comes from Network.Wai.Handler.Warp
67+runTestServer :: Port -> IO ()
68+runTestServer port = run port test
69+
70+-- Put this all to work!
71+main :: IO ()
72+main = runTestServer 8001
servant-server/example/greet.mdnew file+52
@@ -0,0 +1,52 @@
1+POST /greet
2+-----------
3+
4+**Request Body**:
5+
6+``` javascript
7+{"msg":"Hello, haskeller!"}
8+```
9+
10+**Response**:
11+
12+ - Status code 201
13+ - Response body as below.
14+
15+``` javascript
16+{"msg":"Hello, haskeller!"}
17+```
18+
19+GET /hello/:name
20+----------------
21+
22+**Captures**:
23+
24+- *name*: name of the person to greet
25+
26+**GET Parameters**:
27+
28+ - capital
29+ - **Values**: *true, false*
30+ - **Description**: Get the greeting message in uppercase (true) or not (false). Default is false.
31+
32+
33+**Response**:
34+
35+ - Status code 200
36+ - Response body as below.
37+
38+``` javascript
39+{"msg":"Hello, haskeller!"}
40+```
41+
42+DELETE /greet/:greetid
43+----------------------
44+
45+**Captures**:
46+
47+- *greetid*: identifier of the greet msg to remove
48+
49+**Response**:
50+
51+ - Status code 204
52+ - No response body
servant-server/servant-server.cabalnew file+101
@@ -0,0 +1,101 @@
1+name: servant-server
2+version: 0.2.4
3+synopsis: A family of combinators for defining webservices APIs and serving them
4+description:
5+ A family of combinators for defining webservices APIs and serving them
6+ .
7+ You can learn about the basics in <http://haskell-servant.github.io/getting-started/ the getting started> guide.
8+ .
9+ <https://github.com/haskell-servant/servant-server/blob/master/example/greet.hs Here>'s a runnable example, with comments, that defines a dummy API and
10+ implements a webserver that serves this API, using this package.
11+homepage: http://haskell-servant.github.io/
12+Bug-reports: http://github.com/haskell-servant/servant-server/issues
13+license: BSD3
14+license-file: LICENSE
15+author: Alp Mestanogullari, Sönke Hahn, Julian K. Arni
16+maintainer: alpmestan@gmail.com
17+copyright: 2014 Zalora South East Asia Pte Ltd
18+category: Web
19+build-type: Simple
20+cabal-version: >=1.10
21+tested-with: GHC >= 7.8
22+extra-source-files:
23+ CHANGELOG.md
24+ README.md
25+source-repository head
26+ type: git
27+ location: http://github.com/haskell-servant/servant-server.git
28+
29+
30+library
31+ exposed-modules:
32+ Servant
33+ Servant.Server
34+ Servant.Server.Internal
35+ Servant.Utils.StaticFiles
36+ build-depends:
37+ base >= 4.7 && < 5
38+ , aeson >= 0.7 && < 0.9
39+ , attoparsec >= 0.12 && < 0.13
40+ , bytestring >= 0.10 && < 0.11
41+ , either >= 4.3 && < 4.4
42+ , http-types >= 0.8 && < 0.9
43+ , network-uri >= 2.6 && < 2.7
44+ , safe >= 0.3 && < 0.4
45+ , servant >= 0.2 && < 0.4
46+ , split >= 0.2 && < 0.3
47+ , string-conversions >= 0.3 && < 0.4
48+ , system-filepath >= 0.4 && < 0.5
49+ , text >= 1.2 && < 1.3
50+ , transformers >= 0.3 && < 0.5
51+ , wai >= 3.0 && < 3.1
52+ , wai-app-static >= 3.0 && < 3.1
53+ , warp >= 3.0 && < 3.1
54+ hs-source-dirs: src
55+ default-language: Haskell2010
56+ ghc-options: -Wall
57+
58+executable greet
59+ main-is: greet.hs
60+ hs-source-dirs: example
61+ ghc-options: -Wall
62+ default-language: Haskell2010
63+ build-depends:
64+ base
65+ , servant
66+ , servant-server
67+ , aeson
68+ , warp
69+ , wai
70+ , text
71+
72+test-suite spec
73+ type: exitcode-stdio-1.0
74+ ghc-options:
75+ -Wall -fno-warn-name-shadowing -fno-warn-missing-signatures
76+ default-language: Haskell2010
77+ hs-source-dirs: test
78+ main-is: Spec.hs
79+ build-depends:
80+ base == 4.*
81+ , aeson
82+ , bytestring
83+ , bytestring-conversion
84+ , directory
85+ , either
86+ , exceptions
87+ , hspec == 2.*
88+ , hspec-wai
89+ , http-types
90+ , network >= 2.6
91+ , QuickCheck
92+ , parsec
93+ , servant
94+ , servant-server
95+ , string-conversions
96+ , temporary
97+ , text
98+ , transformers
99+ , wai
100+ , wai-extra
101+ , warp
servant-server/src/Servant.hsnew file+22
@@ -0,0 +1,22 @@
1+module Servant (
2+ -- | This module and its submodules can be used to define servant APIs. Note
3+ -- that these API definitions don't directly implement a server (or anything
4+ -- else).
5+ module Servant.API,
6+ -- | For implementing servers for servant APIs.
7+ module Servant.Server,
8+ -- | Using your types in request paths and query string parameters
9+ module Servant.Common.Text,
10+ -- | Utilities on top of the servant core
11+ module Servant.Utils.Links,
12+ module Servant.Utils.StaticFiles,
13+ -- | Useful re-exports
14+ Proxy(..),
15+ ) where
16+
17+import Data.Proxy
18+import Servant.API
19+import Servant.Common.Text
20+import Servant.Server
21+import Servant.Utils.Links
22+import Servant.Utils.StaticFiles
servant-server/src/Servant/Server.hsnew file+49
@@ -0,0 +1,49 @@
1+{-# LANGUAGE TypeFamilies #-}
2+{-# LANGUAGE FlexibleContexts #-}
3+{-# LANGUAGE OverloadedStrings #-}
4+
5+-- | This module lets you implement 'Server's for defined APIs. You'll
6+-- most likely just need 'serve'.
7+module Servant.Server
8+ ( -- * Run a wai application from an API
9+ serve
10+
11+ , -- * Construct a wai Application from an API
12+ toApplication
13+
14+ , -- * Handlers for all standard combinators
15+ HasServer(..)
16+ , Server
17+ , ServerT
18+ ) where
19+
20+import Data.Proxy (Proxy)
21+import Network.Wai (Application)
22+import Servant.API (Canonicalize, canonicalize)
23+import Servant.Server.Internal
24+
25+
26+-- * Implementing Servers
27+
28+-- | 'serve' allows you to implement an API and produce a wai 'Application'.
29+--
30+-- Example:
31+--
32+-- > type MyApi = "books" :> Get '[JSON] [Book] -- GET /books
33+-- > :<|> "books" :> ReqBody Book :> Post '[JSON] Book -- POST /books
34+-- >
35+-- > server :: Server MyApi
36+-- > server = listAllBooks :<|> postBook
37+-- > where listAllBooks = ...
38+-- > postBook book = ...
39+-- >
40+-- > myApi :: Proxy MyApi
41+-- > myApi = Proxy
42+-- >
43+-- > app :: Application
44+-- > app = serve myApi server
45+-- >
46+-- > main :: IO ()
47+-- > main = Network.Wai.Handler.Warp.run 8080 app
48+serve :: HasServer (Canonicalize layout) => Proxy layout -> Server layout -> Application
49+serve p server = toApplication (route (canonicalize p) server)
servant-server/src/Servant/Server/Internal.hsnew file+866
@@ -0,0 +1,866 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE FlexibleContexts #-}
3+{-# LANGUAGE FlexibleInstances #-}
4+{-# LANGUAGE OverlappingInstances #-}
5+{-# LANGUAGE OverloadedStrings #-}
6+{-# LANGUAGE PolyKinds #-}
7+{-# LANGUAGE ScopedTypeVariables #-}
8+{-# LANGUAGE TypeFamilies #-}
9+{-# LANGUAGE TypeOperators #-}
10+module Servant.Server.Internal where
11+
12+import Control.Applicative ((<$>))
13+import Control.Monad.Trans.Either (EitherT, runEitherT)
14+import qualified Data.ByteString as B
15+import qualified Data.ByteString.Lazy as BL
16+import Data.IORef (newIORef, readIORef, writeIORef)
17+import Data.List (unfoldr)
18+import Data.Maybe (catMaybes, fromMaybe)
19+import Data.Monoid (Monoid, mappend, mempty)
20+import Data.String (fromString)
21+import Data.String.Conversions (cs, (<>))
22+import Data.Text (Text)
23+import qualified Data.Text as T
24+import Data.Text.Encoding (decodeUtf8, encodeUtf8)
25+import Data.Typeable
26+import GHC.TypeLits (KnownSymbol, symbolVal)
27+import Network.HTTP.Types hiding (Header, ResponseHeaders)
28+import Network.Wai (Application, Request, Response,
29+ ResponseReceived, lazyRequestBody,
30+ pathInfo, rawQueryString,
31+ requestBody, requestHeaders,
32+ requestMethod, responseLBS,
33+ strictRequestBody)
34+import Servant.API ((:<|>) (..), (:>), Capture,
35+ Canonicalize, Delete, Get, Header,
36+ MatrixFlag, MatrixParam, MatrixParams,
37+ Patch, Post, Put, QueryFlag,
38+ QueryParam, QueryParams, Raw,
39+ ReqBody)
40+import Servant.API.ContentTypes (AcceptHeader (..),
41+ AllCTRender (..),
42+ AllCTUnrender (..))
43+import Servant.API.ResponseHeaders (Headers, getResponse, getHeaders)
44+import Servant.Common.Text (FromText, fromText)
45+
46+data ReqBodyState = Uncalled
47+ | Called !B.ByteString
48+ | Done !B.ByteString
49+
50+
51+toApplication :: RoutingApplication -> Application
52+toApplication ra request respond = do
53+ reqBodyRef <- newIORef Uncalled
54+ -- We may need to consume the requestBody more than once. In order to
55+ -- maintain the illusion that 'requestBody' works as expected,
56+ -- 'ReqBodyState' is introduced, and the complete body is memoized and
57+ -- returned as many times as requested with empty "Done" marker chunks in
58+ -- between.
59+ -- See https://github.com/haskell-servant/servant/issues/3
60+ let memoReqBody = do
61+ ior <- readIORef reqBodyRef
62+ case ior of
63+ Uncalled -> do
64+ r <- BL.toStrict <$> strictRequestBody request
65+ writeIORef reqBodyRef $ Done r
66+ return r
67+ Called bs -> do
68+ writeIORef reqBodyRef $ Done bs
69+ return bs
70+ Done bs -> do
71+ writeIORef reqBodyRef $ Called bs
72+ return B.empty
73+
74+ ra request{ requestBody = memoReqBody } (routingRespond . routeResult)
75+ where
76+ routingRespond :: Either RouteMismatch Response -> IO ResponseReceived
77+ routingRespond (Left NotFound) =
78+ respond $ responseLBS notFound404 [] "not found"
79+ routingRespond (Left WrongMethod) =
80+ respond $ responseLBS methodNotAllowed405 [] "method not allowed"
81+ routingRespond (Left (InvalidBody err)) =
82+ respond $ responseLBS badRequest400 [] $ fromString $ "invalid request body: " ++ err
83+ routingRespond (Left UnsupportedMediaType) =
84+ respond $ responseLBS unsupportedMediaType415 [] "unsupported media type"
85+ routingRespond (Left (HttpError status body)) =
86+ respond $ responseLBS status [] $ fromMaybe (BL.fromStrict $ statusMessage status) body
87+ routingRespond (Right response) =
88+ respond response
89+
90+-- Note that the ordering of the constructors has great significance! It
91+-- determines the Ord instance and, consequently, the monoid instance.
92+-- * Route mismatch
93+data RouteMismatch =
94+ NotFound -- ^ the usual "not found" error
95+ | WrongMethod -- ^ a more informative "you just got the HTTP method wrong" error
96+ | UnsupportedMediaType -- ^ request body has unsupported media type
97+ | InvalidBody String -- ^ an even more informative "your json request body wasn't valid" error
98+ | HttpError Status (Maybe BL.ByteString) -- ^ an even even more informative arbitrary HTTP response code error.
99+ deriving (Eq, Ord, Show)
100+
101+instance Monoid RouteMismatch where
102+ mempty = NotFound
103+ -- The following isn't great, since it picks @InvalidBody@ based on
104+ -- alphabetical ordering, but any choice would be arbitrary.
105+ --
106+ -- "As one judge said to the other, 'Be just and if you can't be just, be
107+ -- arbitrary'" -- William Burroughs
108+ mappend = max
109+
110+
111+-- | A wrapper around @'Either' 'RouteMismatch' a@.
112+newtype RouteResult a =
113+ RR { routeResult :: Either RouteMismatch a }
114+ deriving (Eq, Show)
115+
116+failWith :: RouteMismatch -> RouteResult a
117+failWith = RR . Left
118+
119+succeedWith :: a -> RouteResult a
120+succeedWith = RR . Right
121+
122+isMismatch :: RouteResult a -> Bool
123+isMismatch (RR (Left _)) = True
124+isMismatch _ = False
125+
126+-- | Like `null . pathInfo`, but works with redundant trailing slashes.
127+pathIsEmpty :: Request -> Bool
128+pathIsEmpty = f . processedPathInfo
129+ where
130+ f [] = True
131+ f [""] = True
132+ f _ = False
133+
134+-- | If we get a `Right`, it has precedence over everything else.
135+--
136+-- This in particular means that if we could get several 'Right's,
137+-- only the first we encounter would be taken into account.
138+instance Monoid (RouteResult a) where
139+ mempty = RR $ Left mempty
140+
141+ RR (Left x) `mappend` RR (Left y) = RR $ Left (x <> y)
142+ RR (Left _) `mappend` RR (Right y) = RR $ Right y
143+ r `mappend` _ = r
144+
145+type RoutingApplication =
146+ Request -- ^ the request, the field 'pathInfo' may be modified by url routing
147+ -> (RouteResult Response -> IO ResponseReceived) -> IO ResponseReceived
148+
149+splitMatrixParameters :: Text -> (Text, Text)
150+splitMatrixParameters = T.break (== ';')
151+
152+parsePathInfo :: Request -> [Text]
153+parsePathInfo = filter (/= "") . mergePairs . map splitMatrixParameters . pathInfo
154+ where mergePairs = concat . unfoldr pairToList
155+ pairToList [] = Nothing
156+ pairToList ((a, b):xs) = Just ([a, b], xs)
157+
158+-- | Returns a processed pathInfo from the request.
159+--
160+-- In order to handle matrix parameters in the request correctly, the raw pathInfo needs to be
161+-- processed, so routing works as intended. Therefor this function should be used to access
162+-- the pathInfo for routing purposes.
163+processedPathInfo :: Request -> [Text]
164+processedPathInfo r =
165+ case pinfo of
166+ (x:xs) | T.head x == ';' -> xs
167+ _ -> pinfo
168+ where pinfo = parsePathInfo r
169+
170+class HasServer layout where
171+ type ServerT' layout (m :: * -> *) :: *
172+
173+ route :: Proxy layout -> Server' layout -> RoutingApplication
174+
175+type Server layout = Server' (Canonicalize layout)
176+type Server' layout = ServerT' layout (EitherT (Int, String) IO)
177+type ServerT layout m = ServerT' (Canonicalize layout) m
178+
179+-- * Instances
180+
181+-- | A server for @a ':<|>' b@ first tries to match the request against the route
182+-- represented by @a@ and if it fails tries @b@. You must provide a request
183+-- handler for each route.
184+--
185+-- > type MyApi = "books" :> Get '[JSON] [Book] -- GET /books
186+-- > :<|> "books" :> ReqBody Book :> Post '[JSON] Book -- POST /books
187+-- >
188+-- > server :: Server MyApi
189+-- > server = listAllBooks :<|> postBook
190+-- > where listAllBooks = ...
191+-- > postBook book = ...
192+instance (HasServer a, HasServer b) => HasServer (a :<|> b) where
193+
194+ type ServerT' (a :<|> b) m = ServerT' a m :<|> ServerT' b m
195+
196+ route Proxy (a :<|> b) request respond =
197+ route pa a request $ \mResponse ->
198+ if isMismatch mResponse
199+ then route pb b request $ \mResponse' -> respond (mResponse <> mResponse')
200+ else respond mResponse
201+
202+ where pa = Proxy :: Proxy a
203+ pb = Proxy :: Proxy b
204+
205+captured :: FromText a => proxy (Capture sym a) -> Text -> Maybe a
206+captured _ = fromText
207+
208+-- | If you use 'Capture' in one of the endpoints for your API,
209+-- this automatically requires your server-side handler to be a function
210+-- that takes an argument of the type specified by the 'Capture'.
211+-- This lets servant worry about getting it from the URL and turning
212+-- it into a value of the type you specify.
213+--
214+-- You can control how it'll be converted from 'Text' to your type
215+-- by simply providing an instance of 'FromText' for your type.
216+--
217+-- Example:
218+--
219+-- > type MyApi = "books" :> Capture "isbn" Text :> Get '[JSON] Book
220+-- >
221+-- > server :: Server MyApi
222+-- > server = getBook
223+-- > where getBook :: Text -> EitherT (Int, String) IO Book
224+-- > getBook isbn = ...
225+instance (KnownSymbol capture, FromText a, HasServer sublayout)
226+ => HasServer (Capture capture a :> sublayout) where
227+
228+ type ServerT' (Capture capture a :> sublayout) m =
229+ a -> ServerT' sublayout m
230+
231+ route Proxy subserver request respond = case processedPathInfo request of
232+ (first : rest)
233+ -> case captured captureProxy first of
234+ Nothing -> respond $ failWith NotFound
235+ Just v -> route (Proxy :: Proxy sublayout) (subserver v) request{
236+ pathInfo = rest
237+ } respond
238+ _ -> respond $ failWith NotFound
239+
240+ where captureProxy = Proxy :: Proxy (Capture capture a)
241+
242+
243+-- | If you have a 'Delete' endpoint in your API,
244+-- the handler for this endpoint is meant to delete
245+-- a resource.
246+--
247+-- The code of the handler will, just like
248+-- for 'Servant.API.Get.Get', 'Servant.API.Post.Post' and
249+-- 'Servant.API.Put.Put', run in @EitherT (Int, String) IO ()@.
250+-- The 'Int' represents the status code and the 'String' a message
251+-- to be returned. You can use 'Control.Monad.Trans.Either.left' to
252+-- painlessly error out if the conditions for a successful deletion
253+-- are not met.
254+instance HasServer Delete where
255+
256+ type ServerT' Delete m = m ()
257+
258+ route Proxy action request respond
259+ | pathIsEmpty request && requestMethod request == methodDelete = do
260+ e <- runEitherT action
261+ respond $ succeedWith $ case e of
262+ Right () ->
263+ responseLBS status204 [] ""
264+ Left (status, message) ->
265+ responseLBS (mkStatus status (cs message)) [] (cs message)
266+ | pathIsEmpty request && requestMethod request /= methodDelete =
267+ respond $ failWith WrongMethod
268+ | otherwise = respond $ failWith NotFound
269+
270+-- | When implementing the handler for a 'Get' endpoint,
271+-- just like for 'Servant.API.Delete.Delete', 'Servant.API.Post.Post'
272+-- and 'Servant.API.Put.Put', the handler code runs in the
273+-- @EitherT (Int, String) IO@ monad, where the 'Int' represents
274+-- the status code and the 'String' a message, returned in case of
275+-- failure. You can quite handily use 'Control.Monad.Trans.EitherT.left'
276+-- to quickly fail if some conditions are not met.
277+--
278+-- If successfully returning a value, we use the type-level list, combined
279+-- with the request's @Accept@ header, to encode the value for you
280+-- (returning a status code of 200). If there was no @Accept@ header or it
281+-- was @*/*@, we return encode using the first @Content-Type@ type on the
282+-- list.
283+instance ( AllCTRender ctypes a
284+ ) => HasServer (Get ctypes a) where
285+
286+ type ServerT' (Get ctypes a) m = m a
287+
288+ route Proxy action request respond
289+ | pathIsEmpty request && requestMethod request == methodGet = do
290+ e <- runEitherT action
291+ respond $ case e of
292+ Right output -> do
293+ let accH = fromMaybe "*/*" $ lookup hAccept $ requestHeaders request
294+ case handleAcceptH (Proxy :: Proxy ctypes) (AcceptHeader accH) output of
295+ Nothing -> failWith UnsupportedMediaType
296+ Just (contentT, body) -> succeedWith $
297+ responseLBS ok200 [ ("Content-Type" , cs contentT)] body
298+ Left (status, message) -> succeedWith $
299+ responseLBS (mkStatus status (cs message)) [] (cs message)
300+ | pathIsEmpty request && requestMethod request /= methodGet =
301+ respond $ failWith WrongMethod
302+ | otherwise = respond $ failWith NotFound
303+
304+-- '()' ==> 204 No Content
305+instance HasServer (Get ctypes ()) where
306+ type ServerT' (Get ctypes ()) m = m ()
307+ route Proxy action request respond
308+ | pathIsEmpty request && requestMethod request == methodGet = do
309+ e <- runEitherT action
310+ respond . succeedWith $ case e of
311+ Right () -> responseLBS noContent204 [] ""
312+ Left (status, message) ->
313+ responseLBS (mkStatus status (cs message)) [] (cs message)
314+ | pathIsEmpty request && requestMethod request /= methodGet =
315+ respond $ failWith WrongMethod
316+ | otherwise = respond $ failWith NotFound
317+
318+-- Add response headers
319+instance ( AllCTRender ctypes v ) => HasServer (Get ctypes (Headers h v)) where
320+ type ServerT' (Get ctypes (Headers h v)) m = m (Headers h v)
321+ route Proxy action request respond
322+ | pathIsEmpty request && requestMethod request == methodGet = do
323+ e <- runEitherT action
324+ respond $ case e of
325+ Right output -> do
326+ let accH = fromMaybe "*/*" $ lookup hAccept $ requestHeaders request
327+ headers = getHeaders output
328+ case handleAcceptH (Proxy :: Proxy ctypes) (AcceptHeader accH) (getResponse output) of
329+ Nothing -> failWith UnsupportedMediaType
330+ Just (contentT, body) -> succeedWith $
331+ responseLBS ok200 ( ("Content-Type" , cs contentT) : headers) body
332+ Left (status, message) -> succeedWith $
333+ responseLBS (mkStatus status (cs message)) [] (cs message)
334+ | pathIsEmpty request && requestMethod request /= methodGet =
335+ respond $ failWith WrongMethod
336+ | otherwise = respond $ failWith NotFound
337+
338+-- | If you use 'Header' in one of the endpoints for your API,
339+-- this automatically requires your server-side handler to be a function
340+-- that takes an argument of the type specified by 'Header'.
341+-- This lets servant worry about extracting it from the request and turning
342+-- it into a value of the type you specify.
343+--
344+-- All it asks is for a 'FromText' instance.
345+--
346+-- Example:
347+--
348+-- > newtype Referer = Referer Text
349+-- > deriving (Eq, Show, FromText, ToText)
350+-- >
351+-- > -- GET /view-my-referer
352+-- > type MyApi = "view-my-referer" :> Header "Referer" Referer :> Get '[JSON] Referer
353+-- >
354+-- > server :: Server MyApi
355+-- > server = viewReferer
356+-- > where viewReferer :: Referer -> EitherT (Int, String) IO referer
357+-- > viewReferer referer = return referer
358+instance (KnownSymbol sym, FromText a, HasServer sublayout)
359+ => HasServer (Header sym a :> sublayout) where
360+
361+ type ServerT' (Header sym a :> sublayout) m =
362+ Maybe a -> ServerT' sublayout m
363+
364+ route Proxy subserver request respond = do
365+ let mheader = fromText . decodeUtf8 =<< lookup str (requestHeaders request)
366+ route (Proxy :: Proxy sublayout) (subserver mheader) request respond
367+
368+ where str = fromString $ symbolVal (Proxy :: Proxy sym)
369+
370+-- | When implementing the handler for a 'Post' endpoint,
371+-- just like for 'Servant.API.Delete.Delete', 'Servant.API.Get.Get'
372+-- and 'Servant.API.Put.Put', the handler code runs in the
373+-- @EitherT (Int, String) IO@ monad, where the 'Int' represents
374+-- the status code and the 'String' a message, returned in case of
375+-- failure. You can quite handily use 'Control.Monad.Trans.EitherT.left'
376+-- to quickly fail if some conditions are not met.
377+--
378+-- If successfully returning a value, we use the type-level list, combined
379+-- with the request's @Accept@ header, to encode the value for you
380+-- (returning a status code of 201). If there was no @Accept@ header or it
381+-- was @*/*@, we return encode using the first @Content-Type@ type on the
382+-- list.
383+instance ( AllCTRender ctypes a
384+ ) => HasServer (Post ctypes a) where
385+
386+ type ServerT' (Post ctypes a) m = m a
387+
388+ route Proxy action request respond
389+ | pathIsEmpty request && requestMethod request == methodPost = do
390+ e <- runEitherT action
391+ respond $ case e of
392+ Right output -> do
393+ let accH = fromMaybe "*/*" $ lookup hAccept $ requestHeaders request
394+ case handleAcceptH (Proxy :: Proxy ctypes) (AcceptHeader accH) output of
395+ Nothing -> failWith UnsupportedMediaType
396+ Just (contentT, body) -> succeedWith $
397+ responseLBS status201 [ ("Content-Type" , cs contentT)] body
398+ Left (status, message) -> succeedWith $
399+ responseLBS (mkStatus status (cs message)) [] (cs message)
400+ | pathIsEmpty request && requestMethod request /= methodPost =
401+ respond $ failWith WrongMethod
402+ | otherwise = respond $ failWith NotFound
403+
404+instance HasServer (Post ctypes ()) where
405+ type ServerT' (Post ctypes ()) m = m ()
406+ route Proxy action request respond
407+ | pathIsEmpty request && requestMethod request == methodPost = do
408+ e <- runEitherT action
409+ respond . succeedWith $ case e of
410+ Right () -> responseLBS noContent204 [] ""
411+ Left (status, message) ->
412+ responseLBS (mkStatus status (cs message)) [] (cs message)
413+ | pathIsEmpty request && requestMethod request /= methodPost =
414+ respond $ failWith WrongMethod
415+ | otherwise = respond $ failWith NotFound
416+
417+-- Add response headers
418+instance ( AllCTRender ctypes v ) => HasServer (Post ctypes (Headers h v)) where
419+ type ServerT' (Post ctypes (Headers h v)) m = m (Headers h v)
420+ route Proxy action request respond
421+ | pathIsEmpty request && requestMethod request == methodPost = do
422+ e <- runEitherT action
423+ respond $ case e of
424+ Right output -> do
425+ let accH = fromMaybe "*/*" $ lookup hAccept $ requestHeaders request
426+ headers = getHeaders output
427+ case handleAcceptH (Proxy :: Proxy ctypes) (AcceptHeader accH) (getResponse output) of
428+ Nothing -> failWith UnsupportedMediaType
429+ Just (contentT, body) -> succeedWith $
430+ responseLBS status201 ( ("Content-Type" , cs contentT) : headers) body
431+ Left (status, message) -> succeedWith $
432+ responseLBS (mkStatus status (cs message)) [] (cs message)
433+ | pathIsEmpty request && requestMethod request /= methodPost =
434+ respond $ failWith WrongMethod
435+ | otherwise = respond $ failWith NotFound
436+
437+-- | When implementing the handler for a 'Put' endpoint,
438+-- just like for 'Servant.API.Delete.Delete', 'Servant.API.Get.Get'
439+-- and 'Servant.API.Post.Post', the handler code runs in the
440+-- @EitherT (Int, String) IO@ monad, where the 'Int' represents
441+-- the status code and the 'String' a message, returned in case of
442+-- failure. You can quite handily use 'Control.Monad.Trans.EitherT.left'
443+-- to quickly fail if some conditions are not met.
444+--
445+-- If successfully returning a value, we use the type-level list, combined
446+-- with the request's @Accept@ header, to encode the value for you
447+-- (returning a status code of 200). If there was no @Accept@ header or it
448+-- was @*/*@, we return encode using the first @Content-Type@ type on the
449+-- list.
450+instance ( AllCTRender ctypes a
451+ ) => HasServer (Put ctypes a) where
452+
453+ type ServerT' (Put ctypes a) m = m a
454+
455+ route Proxy action request respond
456+ | pathIsEmpty request && requestMethod request == methodPut = do
457+ e <- runEitherT action
458+ respond $ case e of
459+ Right output -> do
460+ let accH = fromMaybe "*/*" $ lookup hAccept $ requestHeaders request
461+ case handleAcceptH (Proxy :: Proxy ctypes) (AcceptHeader accH) output of
462+ Nothing -> failWith UnsupportedMediaType
463+ Just (contentT, body) -> succeedWith $
464+ responseLBS status200 [ ("Content-Type" , cs contentT)] body
465+ Left (status, message) -> succeedWith $
466+ responseLBS (mkStatus status (cs message)) [] (cs message)
467+ | pathIsEmpty request && requestMethod request /= methodPut =
468+ respond $ failWith WrongMethod
469+ | otherwise = respond $ failWith NotFound
470+
471+instance HasServer (Put ctypes ()) where
472+ type ServerT' (Put ctypes ()) m = m ()
473+ route Proxy action request respond
474+ | pathIsEmpty request && requestMethod request == methodPut = do
475+ e <- runEitherT action
476+ respond . succeedWith $ case e of
477+ Right () -> responseLBS noContent204 [] ""
478+ Left (status, message) ->
479+ responseLBS (mkStatus status (cs message)) [] (cs message)
480+ | pathIsEmpty request && requestMethod request /= methodPut =
481+ respond $ failWith WrongMethod
482+ | otherwise = respond $ failWith NotFound
483+
484+-- Add response headers
485+instance ( AllCTRender ctypes v ) => HasServer (Put ctypes (Headers h v)) where
486+ type ServerT' (Put ctypes (Headers h v)) m = m (Headers h v)
487+ route Proxy action request respond
488+ | pathIsEmpty request && requestMethod request == methodPut = do
489+ e <- runEitherT action
490+ respond $ case e of
491+ Right output -> do
492+ let accH = fromMaybe "*/*" $ lookup hAccept $ requestHeaders request
493+ headers = getHeaders output
494+ case handleAcceptH (Proxy :: Proxy ctypes) (AcceptHeader accH) (getResponse output) of
495+ Nothing -> failWith UnsupportedMediaType
496+ Just (contentT, body) -> succeedWith $
497+ responseLBS status200 ( ("Content-Type" , cs contentT) : headers) body
498+ Left (status, message) -> succeedWith $
499+ responseLBS (mkStatus status (cs message)) [] (cs message)
500+ | pathIsEmpty request && requestMethod request /= methodPut =
501+ respond $ failWith WrongMethod
502+ | otherwise = respond $ failWith NotFound
503+
504+-- | When implementing the handler for a 'Patch' endpoint,
505+-- just like for 'Servant.API.Delete.Delete', 'Servant.API.Get.Get'
506+-- and 'Servant.API.Put.Put', the handler code runs in the
507+-- @EitherT (Int, String) IO@ monad, where the 'Int' represents
508+-- the status code and the 'String' a message, returned in case of
509+-- failure. You can quite handily use 'Control.Monad.Trans.EitherT.left'
510+-- to quickly fail if some conditions are not met.
511+--
512+-- If successfully returning a value, we just require that its type has
513+-- a 'ToJSON' instance and servant takes care of encoding it for you,
514+-- yielding status code 200 along the way.
515+instance ( AllCTRender ctypes a
516+ ) => HasServer (Patch ctypes a) where
517+ type ServerT' (Patch ctypes a) m = m a
518+
519+ route Proxy action request respond
520+ | pathIsEmpty request && requestMethod request == methodPatch = do
521+ e <- runEitherT action
522+ respond $ case e of
523+ Right output -> do
524+ let accH = fromMaybe "*/*" $ lookup hAccept $ requestHeaders request
525+ case handleAcceptH (Proxy :: Proxy ctypes) (AcceptHeader accH) output of
526+ Nothing -> failWith UnsupportedMediaType
527+ Just (contentT, body) -> succeedWith $
528+ responseLBS status200 [ ("Content-Type" , cs contentT)] body
529+ Left (status, message) -> succeedWith $
530+ responseLBS (mkStatus status (cs message)) [] (cs message)
531+ | pathIsEmpty request && requestMethod request /= methodPatch =
532+ respond $ failWith WrongMethod
533+ | otherwise = respond $ failWith NotFound
534+
535+instance HasServer (Patch ctypes ()) where
536+ type ServerT' (Patch ctypes ()) m = m ()
537+ route Proxy action request respond
538+ | pathIsEmpty request && requestMethod request == methodPatch = do
539+ e <- runEitherT action
540+ respond . succeedWith $ case e of
541+ Right () -> responseLBS noContent204 [] ""
542+ Left (status, message) ->
543+ responseLBS (mkStatus status (cs message)) [] (cs message)
544+ | pathIsEmpty request && requestMethod request /= methodPatch =
545+ respond $ failWith WrongMethod
546+ | otherwise = respond $ failWith NotFound
547+
548+-- Add response headers
549+instance ( AllCTRender ctypes v ) => HasServer (Patch ctypes (Headers h v)) where
550+ type ServerT' (Patch ctypes (Headers h v)) m = m (Headers h v)
551+ route Proxy action request respond
552+ | pathIsEmpty request && requestMethod request == methodPatch = do
553+ e <- runEitherT action
554+ respond $ case e of
555+ Right outpatch -> do
556+ let accH = fromMaybe "*/*" $ lookup hAccept $ requestHeaders request
557+ headers = getHeaders outpatch
558+ case handleAcceptH (Proxy :: Proxy ctypes) (AcceptHeader accH) (getResponse outpatch) of
559+ Nothing -> failWith UnsupportedMediaType
560+ Just (contentT, body) -> succeedWith $
561+ responseLBS status200 ( ("Content-Type" , cs contentT) : headers) body
562+ Left (status, message) -> succeedWith $
563+ responseLBS (mkStatus status (cs message)) [] (cs message)
564+ | pathIsEmpty request && requestMethod request /= methodPatch =
565+ respond $ failWith WrongMethod
566+ | otherwise = respond $ failWith NotFound
567+
568+-- | If you use @'QueryParam' "author" Text@ in one of the endpoints for your API,
569+-- this automatically requires your server-side handler to be a function
570+-- that takes an argument of type @'Maybe' 'Text'@.
571+--
572+-- This lets servant worry about looking it up in the query string
573+-- and turning it into a value of the type you specify, enclosed
574+-- in 'Maybe', because it may not be there and servant would then
575+-- hand you 'Nothing'.
576+--
577+-- You can control how it'll be converted from 'Text' to your type
578+-- by simply providing an instance of 'FromText' for your type.
579+--
580+-- Example:
581+--
582+-- > type MyApi = "books" :> QueryParam "author" Text :> Get '[JSON] [Book]
583+-- >
584+-- > server :: Server MyApi
585+-- > server = getBooksBy
586+-- > where getBooksBy :: Maybe Text -> EitherT (Int, String) IO [Book]
587+-- > getBooksBy Nothing = ...return all books...
588+-- > getBooksBy (Just author) = ...return books by the given author...
589+instance (KnownSymbol sym, FromText a, HasServer sublayout)
590+ => HasServer (QueryParam sym a :> sublayout) where
591+
592+ type ServerT' (QueryParam sym a :> sublayout) m =
593+ Maybe a -> ServerT' sublayout m
594+
595+ route Proxy subserver request respond = do
596+ let querytext = parseQueryText $ rawQueryString request
597+ param =
598+ case lookup paramname querytext of
599+ Nothing -> Nothing -- param absent from the query string
600+ Just Nothing -> Nothing -- param present with no value -> Nothing
601+ Just (Just v) -> fromText v -- if present, we try to convert to
602+ -- the right type
603+
604+ route (Proxy :: Proxy sublayout) (subserver param) request respond
605+
606+ where paramname = cs $ symbolVal (Proxy :: Proxy sym)
607+
608+-- | If you use @'QueryParams' "authors" Text@ in one of the endpoints for your API,
609+-- this automatically requires your server-side handler to be a function
610+-- that takes an argument of type @['Text']@.
611+--
612+-- This lets servant worry about looking up 0 or more values in the query string
613+-- associated to @authors@ and turning each of them into a value of
614+-- the type you specify.
615+--
616+-- You can control how the individual values are converted from 'Text' to your type
617+-- by simply providing an instance of 'FromText' for your type.
618+--
619+-- Example:
620+--
621+-- > type MyApi = "books" :> QueryParams "authors" Text :> Get '[JSON] [Book]
622+-- >
623+-- > server :: Server MyApi
624+-- > server = getBooksBy
625+-- > where getBooksBy :: [Text] -> EitherT (Int, String) IO [Book]
626+-- > getBooksBy authors = ...return all books by these authors...
627+instance (KnownSymbol sym, FromText a, HasServer sublayout)
628+ => HasServer (QueryParams sym a :> sublayout) where
629+
630+ type ServerT' (QueryParams sym a :> sublayout) m =
631+ [a] -> ServerT' sublayout m
632+
633+ route Proxy subserver request respond = do
634+ let querytext = parseQueryText $ rawQueryString request
635+ -- if sym is "foo", we look for query string parameters
636+ -- named "foo" or "foo[]" and call fromText on the
637+ -- corresponding values
638+ parameters = filter looksLikeParam querytext
639+ values = catMaybes $ map (convert . snd) parameters
640+
641+ route (Proxy :: Proxy sublayout) (subserver values) request respond
642+
643+ where paramname = cs $ symbolVal (Proxy :: Proxy sym)
644+ looksLikeParam (name, _) = name == paramname || name == (paramname <> "[]")
645+ convert Nothing = Nothing
646+ convert (Just v) = fromText v
647+
648+-- | If you use @'QueryFlag' "published"@ in one of the endpoints for your API,
649+-- this automatically requires your server-side handler to be a function
650+-- that takes an argument of type 'Bool'.
651+--
652+-- Example:
653+--
654+-- > type MyApi = "books" :> QueryFlag "published" :> Get '[JSON] [Book]
655+-- >
656+-- > server :: Server MyApi
657+-- > server = getBooks
658+-- > where getBooks :: Bool -> EitherT (Int, String) IO [Book]
659+-- > getBooks onlyPublished = ...return all books, or only the ones that are already published, depending on the argument...
660+instance (KnownSymbol sym, HasServer sublayout)
661+ => HasServer (QueryFlag sym :> sublayout) where
662+
663+ type ServerT' (QueryFlag sym :> sublayout) m =
664+ Bool -> ServerT' sublayout m
665+
666+ route Proxy subserver request respond = do
667+ let querytext = parseQueryText $ rawQueryString request
668+ param = case lookup paramname querytext of
669+ Just Nothing -> True -- param is there, with no value
670+ Just (Just v) -> examine v -- param with a value
671+ Nothing -> False -- param not in the query string
672+
673+ route (Proxy :: Proxy sublayout) (subserver param) request respond
674+
675+ where paramname = cs $ symbolVal (Proxy :: Proxy sym)
676+ examine v | v == "true" || v == "1" || v == "" = True
677+ | otherwise = False
678+
679+parseMatrixText :: B.ByteString -> QueryText
680+parseMatrixText = parseQueryText
681+
682+-- | If you use @'MatrixParam' "author" Text@ in one of the endpoints for your API,
683+-- this automatically requires your server-side handler to be a function
684+-- that takes an argument of type @'Maybe' 'Text'@.
685+--
686+-- This lets servant worry about looking it up in the query string
687+-- and turning it into a value of the type you specify, enclosed
688+-- in 'Maybe', because it may not be there and servant would then
689+-- hand you 'Nothing'.
690+--
691+-- You can control how it'll be converted from 'Text' to your type
692+-- by simply providing an instance of 'FromText' for your type.
693+--
694+-- Example:
695+--
696+-- > type MyApi = "books" :> MatrixParam "author" Text :> Get [Book]
697+-- >
698+-- > server :: Server MyApi
699+-- > server = getBooksBy
700+-- > where getBooksBy :: Maybe Text -> EitherT (Int, String) IO [Book]
701+-- > getBooksBy Nothing = ...return all books...
702+-- > getBooksBy (Just author) = ...return books by the given author...
703+instance (KnownSymbol sym, FromText a, HasServer sublayout)
704+ => HasServer (MatrixParam sym a :> sublayout) where
705+
706+ type ServerT' (MatrixParam sym a :> sublayout) m =
707+ Maybe a -> ServerT' sublayout m
708+
709+ route Proxy subserver request respond = case parsePathInfo request of
710+ (first : _)
711+ -> do let querytext = parseMatrixText . encodeUtf8 $ T.tail first
712+ param = case lookup paramname querytext of
713+ Nothing -> Nothing -- param absent from the query string
714+ Just Nothing -> Nothing -- param present with no value -> Nothing
715+ Just (Just v) -> fromText v -- if present, we try to convert to
716+ -- the right type
717+ route (Proxy :: Proxy sublayout) (subserver param) request respond
718+ _ -> route (Proxy :: Proxy sublayout) (subserver Nothing) request respond
719+
720+ where paramname = cs $ symbolVal (Proxy :: Proxy sym)
721+
722+-- | If you use @'MatrixParams' "authors" Text@ in one of the endpoints for your API,
723+-- this automatically requires your server-side handler to be a function
724+-- that takes an argument of type @['Text']@.
725+--
726+-- This lets servant worry about looking up 0 or more values in the query string
727+-- associated to @authors@ and turning each of them into a value of
728+-- the type you specify.
729+--
730+-- You can control how the individual values are converted from 'Text' to your type
731+-- by simply providing an instance of 'FromText' for your type.
732+--
733+-- Example:
734+--
735+-- > type MyApi = "books" :> MatrixParams "authors" Text :> Get [Book]
736+-- >
737+-- > server :: Server MyApi
738+-- > server = getBooksBy
739+-- > where getBooksBy :: [Text] -> EitherT (Int, String) IO [Book]
740+-- > getBooksBy authors = ...return all books by these authors...
741+instance (KnownSymbol sym, FromText a, HasServer sublayout)
742+ => HasServer (MatrixParams sym a :> sublayout) where
743+
744+ type ServerT' (MatrixParams sym a :> sublayout) m =
745+ [a] -> ServerT' sublayout m
746+
747+ route Proxy subserver request respond = case parsePathInfo request of
748+ (first : _)
749+ -> do let matrixtext = parseMatrixText . encodeUtf8 $ T.tail first
750+ -- if sym is "foo", we look for matrix parameters
751+ -- named "foo" or "foo[]" and call fromText on the
752+ -- corresponding values
753+ parameters = filter looksLikeParam matrixtext
754+ values = catMaybes $ map (convert . snd) parameters
755+ route (Proxy :: Proxy sublayout) (subserver values) request respond
756+ _ -> route (Proxy :: Proxy sublayout) (subserver []) request respond
757+
758+ where paramname = cs $ symbolVal (Proxy :: Proxy sym)
759+ looksLikeParam (name, _) = name == paramname || name == (paramname <> "[]")
760+ convert Nothing = Nothing
761+ convert (Just v) = fromText v
762+
763+-- | If you use @'MatrixFlag' "published"@ in one of the endpoints for your API,
764+-- this automatically requires your server-side handler to be a function
765+-- that takes an argument of type 'Bool'.
766+--
767+-- Example:
768+--
769+-- > type MyApi = "books" :> MatrixFlag "published" :> Get [Book]
770+-- >
771+-- > server :: Server MyApi
772+-- > server = getBooks
773+-- > where getBooks :: Bool -> EitherT (Int, String) IO [Book]
774+-- > getBooks onlyPublished = ...return all books, or only the ones that are already published, depending on the argument...
775+instance (KnownSymbol sym, HasServer sublayout)
776+ => HasServer (MatrixFlag sym :> sublayout) where
777+
778+ type ServerT' (MatrixFlag sym :> sublayout) m =
779+ Bool -> ServerT' sublayout m
780+
781+ route Proxy subserver request respond = case parsePathInfo request of
782+ (first : _)
783+ -> do let matrixtext = parseMatrixText . encodeUtf8 $ T.tail first
784+ param = case lookup paramname matrixtext of
785+ Just Nothing -> True -- param is there, with no value
786+ Just (Just v) -> examine v -- param with a value
787+ Nothing -> False -- param not in the query string
788+
789+ route (Proxy :: Proxy sublayout) (subserver param) request respond
790+
791+ _ -> route (Proxy :: Proxy sublayout) (subserver False) request respond
792+
793+ where paramname = cs $ symbolVal (Proxy :: Proxy sym)
794+ examine v | v == "true" || v == "1" || v == "" = True
795+ | otherwise = False
796+
797+-- | Just pass the request to the underlying application and serve its response.
798+--
799+-- Example:
800+--
801+-- > type MyApi = "images" :> Raw
802+-- >
803+-- > server :: Server MyApi
804+-- > server = serveDirectory "/var/www/images"
805+instance HasServer Raw where
806+
807+ type ServerT' Raw m = Application
808+
809+ route Proxy rawApplication request respond =
810+ rawApplication request (respond . succeedWith)
811+
812+-- | If you use 'ReqBody' in one of the endpoints for your API,
813+-- this automatically requires your server-side handler to be a function
814+-- that takes an argument of the type specified by 'ReqBody'.
815+-- The @Content-Type@ header is inspected, and the list provided is used to
816+-- attempt deserialization. If the request does not have a @Content-Type@
817+-- header, it is treated as @application/octet-stream@.
818+-- This lets servant worry about extracting it from the request and turning
819+-- it into a value of the type you specify.
820+--
821+--
822+-- All it asks is for a 'FromJSON' instance.
823+--
824+-- Example:
825+--
826+-- > type MyApi = "books" :> ReqBody '[JSON] Book :> Post '[JSON] Book
827+-- >
828+-- > server :: Server MyApi
829+-- > server = postBook
830+-- > where postBook :: Book -> EitherT (Int, String) IO Book
831+-- > postBook book = ...insert into your db...
832+instance ( AllCTUnrender list a, HasServer sublayout
833+ ) => HasServer (ReqBody list a :> sublayout) where
834+
835+ type ServerT' (ReqBody list a :> sublayout) m =
836+ a -> ServerT' sublayout m
837+
838+ route Proxy subserver request respond = do
839+ -- See HTTP RFC 2616, section 7.2.1
840+ -- http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
841+ -- See also "W3C Internet Media Type registration, consistency of use"
842+ -- http://www.w3.org/2001/tag/2002/0129-mime
843+ let contentTypeH = fromMaybe "application/octet-stream"
844+ $ lookup hContentType $ requestHeaders request
845+ mrqbody <- handleCTypeH (Proxy :: Proxy list) (cs contentTypeH)
846+ <$> lazyRequestBody request
847+ case mrqbody of
848+ Nothing -> respond . failWith $ UnsupportedMediaType
849+ Just (Left e) -> respond . failWith $ InvalidBody e
850+ Just (Right v) -> route (Proxy :: Proxy sublayout) (subserver v) request respond
851+
852+-- | Make sure the incoming request starts with @"/path"@, strip it and
853+-- pass the rest of the request path to @sublayout@.
854+instance (KnownSymbol path, HasServer sublayout) => HasServer (path :> sublayout) where
855+
856+ type ServerT' (path :> sublayout) m = ServerT' sublayout m
857+
858+ route Proxy subserver request respond = case processedPathInfo request of
859+ (first : rest)
860+ | first == cs (symbolVal proxyPath)
861+ -> route (Proxy :: Proxy sublayout) subserver request{
862+ pathInfo = rest
863+ } respond
864+ _ -> respond $ failWith NotFound
865+
866+ where proxyPath = Proxy :: Proxy path
servant-server/src/Servant/Utils/StaticFiles.hsnew file+36
@@ -0,0 +1,36 @@
1+-- | This module defines a sever-side handler that lets you serve static files.
2+--
3+-- - 'serveDirectory' lets you serve anything that lives under a particular
4+-- directory on your filesystem.
5+module Servant.Utils.StaticFiles (
6+ serveDirectory,
7+ ) where
8+
9+import Filesystem.Path.CurrentOS (decodeString)
10+import Network.Wai.Application.Static (staticApp, defaultFileServerSettings)
11+import Servant.API.Raw (Raw)
12+import Servant.Server (Server)
13+
14+-- | Serve anything under the specified directory as a 'Raw' endpoint.
15+--
16+-- @
17+-- type MyApi = "static" :> Raw
18+--
19+-- server :: Server MyApi
20+-- server = serveDirectory "\/var\/www"
21+-- @
22+--
23+-- would capture any request to @\/static\/\<something>@ and look for
24+-- @\<something>@ under @\/var\/www@.
25+--
26+-- It will do its best to guess the MIME type for that file, based on the extension,
27+-- and send an appropriate /Content-Type/ header if possible.
28+--
29+-- If your goal is to serve HTML, CSS and Javascript files that use the rest of the API
30+-- as a webapp backend, you will most likely not want the static files to be hidden
31+-- behind a /\/static\// prefix. In that case, remember to put the 'serveDirectory'
32+-- handler in the last position, because /servant/ will try to match the handlers
33+-- in order.
34+serveDirectory :: FilePath -> Server Raw
35+serveDirectory documentRoot =
36+ staticApp (defaultFileServerSettings (decodeString (documentRoot ++ "/")))
servant-server/test/Servant/ServerSpec.hsnew file+619
@@ -0,0 +1,619 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE DeriveGeneric #-}
3+{-# LANGUAGE OverloadedStrings #-}
4+{-# LANGUAGE ScopedTypeVariables #-}
5+{-# LANGUAGE TypeOperators #-}
6+{-# LANGUAGE TypeSynonymInstances #-}
7+{-# LANGUAGE FlexibleInstances #-}
8+
9+module Servant.ServerSpec where
10+
11+
12+import Control.Monad (forM_, when)
13+import Control.Monad.Trans.Either (EitherT, left)
14+import Data.Aeson (FromJSON, ToJSON, decode', encode)
15+import Data.ByteString.Conversion ()
16+import Data.Char (toUpper)
17+import Data.Monoid ((<>))
18+import Data.Proxy (Proxy (Proxy))
19+import Data.String (fromString)
20+import Data.String.Conversions (cs)
21+import qualified Data.Text as T
22+import GHC.Generics (Generic)
23+import Network.HTTP.Types (hAccept, hContentType,
24+ methodDelete, methodGet,
25+ methodPatch, methodPost, methodPut,
26+ ok200, parseQuery, status409)
27+import Network.Wai (Application, Request, pathInfo,
28+ queryString, rawQueryString,
29+ responseLBS)
30+import Network.Wai.Test (defaultRequest, request,
31+ runSession, simpleBody)
32+import Test.Hspec (Spec, describe, it, shouldBe)
33+import Test.Hspec.Wai (get, liftIO, matchHeaders,
34+ matchStatus, post, request,
35+ shouldRespondWith, with, (<:>))
36+
37+import Servant.API ((:<|>) (..), (:>),
38+ AddHeader (addHeader), Capture,
39+ Delete, Get, Header (..), Headers,
40+ JSON, MatrixFlag, MatrixParam,
41+ MatrixParams, Patch, PlainText,
42+ Post, Put, QueryFlag, QueryParam,
43+ QueryParams, Raw, ReqBody)
44+import Servant.Server (Server, serve)
45+import Servant.Server.Internal (RouteMismatch (..))
46+
47+
48+-- * test data types
49+
50+data Person = Person {
51+ name :: String,
52+ age :: Integer
53+ }
54+ deriving (Eq, Show, Generic)
55+
56+instance ToJSON Person
57+instance FromJSON Person
58+
59+alice :: Person
60+alice = Person "Alice" 42
61+
62+data Animal = Animal {
63+ species :: String,
64+ numberOfLegs :: Integer
65+ }
66+ deriving (Eq, Show, Generic)
67+
68+instance ToJSON Animal
69+instance FromJSON Animal
70+
71+jerry :: Animal
72+jerry = Animal "Mouse" 4
73+
74+tweety :: Animal
75+tweety = Animal "Bird" 2
76+
77+
78+-- * specs
79+
80+spec :: Spec
81+spec = do
82+ captureSpec
83+ getSpec
84+ postSpec
85+ putSpec
86+ patchSpec
87+ queryParamSpec
88+ matrixParamSpec
89+ headerSpec
90+ rawSpec
91+ unionSpec
92+ errorsSpec
93+ responseHeadersSpec
94+
95+
96+type CaptureApi = Capture "legs" Integer :> Get '[JSON] Animal
97+captureApi :: Proxy CaptureApi
98+captureApi = Proxy
99+captureServer :: Integer -> EitherT (Int, String) IO Animal
100+captureServer legs = case legs of
101+ 4 -> return jerry
102+ 2 -> return tweety
103+ _ -> left (404, "not found")
104+
105+captureSpec :: Spec
106+captureSpec = do
107+ describe "Servant.API.Capture" $ do
108+ with (return (serve captureApi captureServer)) $ do
109+
110+ it "can capture parts of the 'pathInfo'" $ do
111+ response <- get "/2"
112+ liftIO $ decode' (simpleBody response) `shouldBe` Just tweety
113+
114+ it "returns 404 if the decoding fails" $ do
115+ get "/notAnInt" `shouldRespondWith` 404
116+
117+ with (return (serve
118+ (Proxy :: Proxy (Capture "captured" String :> Raw))
119+ (\ "captured" request_ respond ->
120+ respond $ responseLBS ok200 [] (cs $ show $ pathInfo request_)))) $ do
121+ it "strips the captured path snippet from pathInfo" $ do
122+ get "/captured/foo" `shouldRespondWith` (fromString (show ["foo" :: String]))
123+
124+
125+type GetApi = Get '[JSON] Person
126+ :<|> "empty" :> Get '[] ()
127+getApi :: Proxy GetApi
128+getApi = Proxy
129+
130+getSpec :: Spec
131+getSpec = do
132+ describe "Servant.API.Get" $ do
133+ let server = return alice :<|> return ()
134+ with (return $ serve getApi server) $ do
135+
136+ it "allows to GET a Person" $ do
137+ response <- get "/"
138+ return response `shouldRespondWith` 200
139+ liftIO $ decode' (simpleBody response) `shouldBe` Just alice
140+
141+ it "throws 405 (wrong method) on POSTs" $ do
142+ post "/" "" `shouldRespondWith` 405
143+ post "/empty" "" `shouldRespondWith` 405
144+
145+ it "returns 204 if the type is '()'" $ do
146+ get "empty" `shouldRespondWith` ""{ matchStatus = 204 }
147+
148+ it "returns 415 if the Accept header is not supported" $ do
149+ Test.Hspec.Wai.request methodGet "" [(hAccept, "crazy/mime")] ""
150+ `shouldRespondWith` 415
151+
152+
153+
154+type QueryParamApi = QueryParam "name" String :> Get '[JSON] Person
155+ :<|> "a" :> QueryParams "names" String :> Get '[JSON] Person
156+ :<|> "b" :> QueryFlag "capitalize" :> Get '[JSON] Person
157+
158+queryParamApi :: Proxy QueryParamApi
159+queryParamApi = Proxy
160+
161+qpServer :: Server QueryParamApi
162+qpServer = queryParamServer :<|> qpNames :<|> qpCapitalize
163+
164+ where qpNames (_:name2:_) = return alice { name = name2 }
165+ qpNames _ = return alice
166+
167+ qpCapitalize False = return alice
168+ qpCapitalize True = return alice { name = map toUpper (name alice) }
169+
170+ queryParamServer (Just name_) = return alice{name = name_}
171+ queryParamServer Nothing = return alice
172+
173+queryParamSpec :: Spec
174+queryParamSpec = do
175+ describe "Servant.API.QueryParam" $ do
176+ it "allows to retrieve simple GET parameters" $
177+ (flip runSession) (serve queryParamApi qpServer) $ do
178+ let params1 = "?name=bob"
179+ response1 <- Network.Wai.Test.request defaultRequest{
180+ rawQueryString = params1,
181+ queryString = parseQuery params1
182+ }
183+ liftIO $ do
184+ decode' (simpleBody response1) `shouldBe` Just alice{
185+ name = "bob"
186+ }
187+
188+ it "allows to retrieve lists in GET parameters" $
189+ (flip runSession) (serve queryParamApi qpServer) $ do
190+ let params2 = "?names[]=bob&names[]=john"
191+ response2 <- Network.Wai.Test.request defaultRequest{
192+ rawQueryString = params2,
193+ queryString = parseQuery params2,
194+ pathInfo = ["a"]
195+ }
196+ liftIO $
197+ decode' (simpleBody response2) `shouldBe` Just alice{
198+ name = "john"
199+ }
200+
201+
202+ it "allows to retrieve value-less GET parameters" $
203+ (flip runSession) (serve queryParamApi qpServer) $ do
204+ let params3 = "?capitalize"
205+ response3 <- Network.Wai.Test.request defaultRequest{
206+ rawQueryString = params3,
207+ queryString = parseQuery params3,
208+ pathInfo = ["b"]
209+ }
210+ liftIO $
211+ decode' (simpleBody response3) `shouldBe` Just alice{
212+ name = "ALICE"
213+ }
214+
215+ let params3' = "?capitalize="
216+ response3' <- Network.Wai.Test.request defaultRequest{
217+ rawQueryString = params3',
218+ queryString = parseQuery params3',
219+ pathInfo = ["b"]
220+ }
221+ liftIO $
222+ decode' (simpleBody response3') `shouldBe` Just alice{
223+ name = "ALICE"
224+ }
225+
226+ let params3'' = "?unknown="
227+ response3' <- Network.Wai.Test.request defaultRequest{
228+ rawQueryString = params3'',
229+ queryString = parseQuery params3'',
230+ pathInfo = ["b"]
231+ }
232+ liftIO $
233+ decode' (simpleBody response3') `shouldBe` Just alice{
234+ name = "Alice"
235+ }
236+
237+type MatrixParamApi = "a" :> MatrixParam "name" String :> Get '[JSON] Person
238+ :<|> "b" :> MatrixParams "names" String :> "bsub" :> MatrixParams "names" String :> Get '[JSON] Person
239+ :<|> "c" :> MatrixFlag "capitalize" :> Get '[JSON] Person
240+ :<|> "d" :> Capture "foo" Integer :> MatrixParam "name" String :> MatrixFlag "capitalize" :> "dsub" :> Get '[JSON] Person
241+
242+matrixParamApi :: Proxy MatrixParamApi
243+matrixParamApi = Proxy
244+
245+mpServer :: Server MatrixParamApi
246+mpServer = matrixParamServer :<|> mpNames :<|> mpCapitalize alice :<|> mpComplex
247+ where mpNames (_:name2:_) _ = return alice { name = name2 }
248+ mpNames _ _ = return alice
249+
250+ mpCapitalize p False = return p
251+ mpCapitalize p True = return p { name = map toUpper (name p) }
252+
253+ matrixParamServer (Just name) = return alice{name = name}
254+ matrixParamServer Nothing = return alice
255+
256+ mpAge age p = return p { age = age }
257+ mpComplex capture name cap = matrixParamServer name >>= flip mpCapitalize cap >>= mpAge capture
258+
259+matrixParamSpec :: Spec
260+matrixParamSpec = do
261+ describe "Servant.API.MatrixParam" $ do
262+ it "allows to retrieve simple matrix parameters" $
263+ (flip runSession) (serve matrixParamApi mpServer) $ do
264+ response1 <- Network.Wai.Test.request defaultRequest{
265+ pathInfo = ["a;name=bob"]
266+ }
267+ liftIO $ do
268+ decode' (simpleBody response1) `shouldBe` Just alice{
269+ name = "bob"
270+ }
271+
272+ it "allows to retrieve lists in matrix parameters" $
273+ (flip runSession) (serve matrixParamApi mpServer) $ do
274+ response2 <- Network.Wai.Test.request defaultRequest{
275+ pathInfo = ["b;names=bob;names=john", "bsub;names=anna;names=sarah"]
276+ }
277+ liftIO $
278+ decode' (simpleBody response2) `shouldBe` Just alice{
279+ name = "john"
280+ }
281+
282+ it "allows to retrieve value-less matrix parameters" $
283+ (flip runSession) (serve matrixParamApi mpServer) $ do
284+ response3 <- Network.Wai.Test.request defaultRequest{
285+ pathInfo = ["c;capitalize"]
286+ }
287+ liftIO $
288+ decode' (simpleBody response3) `shouldBe` Just alice{
289+ name = "ALICE"
290+ }
291+
292+ response3' <- Network.Wai.Test.request defaultRequest{
293+ pathInfo = ["c;capitalize="]
294+ }
295+ liftIO $
296+ decode' (simpleBody response3') `shouldBe` Just alice{
297+ name = "ALICE"
298+ }
299+
300+ it "allows to retrieve matrix parameters on captured segments" $
301+ (flip runSession) (serve matrixParamApi mpServer) $ do
302+ response4 <- Network.Wai.Test.request defaultRequest{
303+ pathInfo = ["d", "12;name=stephen;capitalize", "dsub"]
304+ }
305+ liftIO $
306+ decode' (simpleBody response4) `shouldBe` Just alice{
307+ name = "STEPHEN",
308+ age = 12
309+ }
310+
311+ response4' <- Network.Wai.Test.request defaultRequest{
312+ pathInfo = ["d;ignored=1", "5", "dsub"]
313+ }
314+ liftIO $
315+ decode' (simpleBody response4') `shouldBe` Just alice{
316+ name = "Alice",
317+ age = 5
318+ }
319+
320+type PostApi =
321+ ReqBody '[JSON] Person :> Post '[JSON] Integer
322+ :<|> "bla" :> ReqBody '[JSON] Person :> Post '[JSON] Integer
323+ :<|> "empty" :> Post '[] ()
324+
325+postApi :: Proxy PostApi
326+postApi = Proxy
327+
328+postSpec :: Spec
329+postSpec = do
330+ describe "Servant.API.Post and .ReqBody" $ do
331+ let server = return . age :<|> return . age :<|> return ()
332+ with (return $ serve postApi server) $ do
333+ let post' x = Test.Hspec.Wai.request methodPost x [(hContentType
334+ , "application/json;charset=utf-8")]
335+
336+ it "allows to POST a Person" $ do
337+ post' "/" (encode alice) `shouldRespondWith` "42"{
338+ matchStatus = 201
339+ }
340+
341+ it "allows alternative routes if all have request bodies" $ do
342+ post' "/bla" (encode alice) `shouldRespondWith` "42"{
343+ matchStatus = 201
344+ }
345+
346+ it "handles trailing '/' gracefully" $ do
347+ post' "/bla/" (encode alice) `shouldRespondWith` "42"{
348+ matchStatus = 201
349+ }
350+
351+ it "correctly rejects invalid request bodies with status 400" $ do
352+ post' "/" "some invalid body" `shouldRespondWith` 400
353+
354+ it "returns 204 if the type is '()'" $ do
355+ post' "empty" "" `shouldRespondWith` ""{ matchStatus = 204 }
356+
357+ it "responds with 415 if the requested media type is unsupported" $ do
358+ let post'' x = Test.Hspec.Wai.request methodPost x [(hContentType
359+ , "application/nonsense")]
360+ post'' "/" "anything at all" `shouldRespondWith` 415
361+
362+type PutApi =
363+ ReqBody '[JSON] Person :> Put '[JSON] Integer
364+ :<|> "bla" :> ReqBody '[JSON] Person :> Put '[JSON] Integer
365+ :<|> "empty" :> Put '[] ()
366+
367+putApi :: Proxy PutApi
368+putApi = Proxy
369+
370+putSpec :: Spec
371+putSpec = do
372+ describe "Servant.API.Put and .ReqBody" $ do
373+ let server = return . age :<|> return . age :<|> return ()
374+ with (return $ serve putApi server) $ do
375+ let put' x = Test.Hspec.Wai.request methodPut x [(hContentType
376+ , "application/json;charset=utf-8")]
377+
378+ it "allows to put a Person" $ do
379+ put' "/" (encode alice) `shouldRespondWith` "42"{
380+ matchStatus = 200
381+ }
382+
383+ it "allows alternative routes if all have request bodies" $ do
384+ put' "/bla" (encode alice) `shouldRespondWith` "42"{
385+ matchStatus = 200
386+ }
387+
388+ it "handles trailing '/' gracefully" $ do
389+ put' "/bla/" (encode alice) `shouldRespondWith` "42"{
390+ matchStatus = 200
391+ }
392+
393+ it "correctly rejects invalid request bodies with status 400" $ do
394+ put' "/" "some invalid body" `shouldRespondWith` 400
395+
396+ it "returns 204 if the type is '()'" $ do
397+ put' "empty" "" `shouldRespondWith` ""{ matchStatus = 204 }
398+
399+ it "responds with 415 if the requested media type is unsupported" $ do
400+ let put'' x = Test.Hspec.Wai.request methodPut x [(hContentType
401+ , "application/nonsense")]
402+ put'' "/" "anything at all" `shouldRespondWith` 415
403+
404+type PatchApi =
405+ ReqBody '[JSON] Person :> Patch '[JSON] Integer
406+ :<|> "bla" :> ReqBody '[JSON] Person :> Patch '[JSON] Integer
407+ :<|> "empty" :> Patch '[] ()
408+
409+patchApi :: Proxy PatchApi
410+patchApi = Proxy
411+
412+patchSpec :: Spec
413+patchSpec = do
414+ describe "Servant.API.Patch and .ReqBody" $ do
415+ let server = return . age :<|> return . age :<|> return ()
416+ with (return $ serve patchApi server) $ do
417+ let patch' x = Test.Hspec.Wai.request methodPatch x [(hContentType
418+ , "application/json;charset=utf-8")]
419+
420+ it "allows to patch a Person" $ do
421+ patch' "/" (encode alice) `shouldRespondWith` "42"{
422+ matchStatus = 200
423+ }
424+
425+ it "allows alternative routes if all have request bodies" $ do
426+ patch' "/bla" (encode alice) `shouldRespondWith` "42"{
427+ matchStatus = 200
428+ }
429+
430+ it "handles trailing '/' gracefully" $ do
431+ patch' "/bla/" (encode alice) `shouldRespondWith` "42"{
432+ matchStatus = 200
433+ }
434+
435+ it "correctly rejects invalid request bodies with status 400" $ do
436+ patch' "/" "some invalid body" `shouldRespondWith` 400
437+
438+ it "returns 204 if the type is '()'" $ do
439+ patch' "empty" "" `shouldRespondWith` ""{ matchStatus = 204 }
440+
441+ it "responds with 415 if the requested media type is unsupported" $ do
442+ let patch'' x = Test.Hspec.Wai.request methodPatch x [(hContentType
443+ , "application/nonsense")]
444+ patch'' "/" "anything at all" `shouldRespondWith` 415
445+
446+type HeaderApi a = Header "MyHeader" a :> Delete
447+headerApi :: Proxy (HeaderApi a)
448+headerApi = Proxy
449+
450+headerSpec :: Spec
451+headerSpec = describe "Servant.API.Header" $ do
452+
453+ let expectsInt :: Maybe Int -> EitherT (Int,String) IO ()
454+ expectsInt (Just x) = when (x /= 5) $ error "Expected 5"
455+ expectsInt Nothing = error "Expected an int"
456+
457+ let expectsString :: Maybe String -> EitherT (Int,String) IO ()
458+ expectsString (Just x) = when (x /= "more from you") $ error "Expected more from you"
459+ expectsString Nothing = error "Expected a string"
460+
461+ with (return (serve headerApi expectsInt)) $ do
462+ let delete' x = Test.Hspec.Wai.request methodDelete x [("MyHeader" ,"5")]
463+
464+ it "passes the header to the handler (Int)" $
465+ delete' "/" "" `shouldRespondWith` 204
466+
467+ with (return (serve headerApi expectsString)) $ do
468+ let delete' x = Test.Hspec.Wai.request methodDelete x [("MyHeader" ,"more from you")]
469+
470+ it "passes the header to the handler (String)" $
471+ delete' "/" "" `shouldRespondWith` 204
472+
473+
474+type RawApi = "foo" :> Raw
475+rawApi :: Proxy RawApi
476+rawApi = Proxy
477+rawApplication :: Show a => (Request -> a) -> Application
478+rawApplication f request_ respond = respond $ responseLBS ok200 [] (cs $ show $ f request_)
479+
480+rawSpec :: Spec
481+rawSpec = do
482+ describe "Servant.API.Raw" $ do
483+ it "runs applications" $ do
484+ (flip runSession) (serve rawApi (rawApplication (const (42 :: Integer)))) $ do
485+ response <- Network.Wai.Test.request defaultRequest{
486+ pathInfo = ["foo"]
487+ }
488+ liftIO $ do
489+ simpleBody response `shouldBe` "42"
490+
491+ it "gets the pathInfo modified" $ do
492+ (flip runSession) (serve rawApi (rawApplication pathInfo)) $ do
493+ response <- Network.Wai.Test.request defaultRequest{
494+ pathInfo = ["foo", "bar"]
495+ }
496+ liftIO $ do
497+ simpleBody response `shouldBe` cs (show ["bar" :: String])
498+
499+
500+type AlternativeApi =
501+ "foo" :> Get '[JSON] Person
502+ :<|> "bar" :> Get '[JSON] Animal
503+ :<|> "foo" :> Get '[PlainText] T.Text
504+ :<|> "bar" :> Post '[JSON] Animal
505+ :<|> "bar" :> Put '[JSON] Animal
506+ :<|> "bar" :> Delete
507+unionApi :: Proxy AlternativeApi
508+unionApi = Proxy
509+
510+unionServer :: Server AlternativeApi
511+unionServer =
512+ return alice
513+ :<|> return jerry
514+ :<|> return "a string"
515+ :<|> return jerry
516+ :<|> return jerry
517+ :<|> return ()
518+
519+unionSpec :: Spec
520+unionSpec = do
521+ describe "Servant.API.Alternative" $ do
522+ with (return $ serve unionApi unionServer) $ do
523+
524+ it "unions endpoints" $ do
525+ response <- get "/foo"
526+ liftIO $ do
527+ decode' (simpleBody response) `shouldBe`
528+ Just alice
529+ response_ <- get "/bar"
530+ liftIO $ do
531+ decode' (simpleBody response_) `shouldBe`
532+ Just jerry
533+
534+ it "checks all endpoints before returning 415" $ do
535+ get "/foo" `shouldRespondWith` 200
536+
537+ it "returns 404 if the path does not exist" $ do
538+ get "/nonexistent" `shouldRespondWith` 404
539+
540+type ResponseHeadersApi =
541+ Get '[JSON] (Headers '[Header "H1" Int, Header "H2" String] String)
542+ :<|> Post '[JSON] (Headers '[Header "H1" Int, Header "H2" String] String)
543+ :<|> Put '[JSON] (Headers '[Header "H1" Int, Header "H2" String] String)
544+ :<|> Patch '[JSON] (Headers '[Header "H1" Int, Header "H2" String] String)
545+
546+
547+responseHeadersServer :: Server ResponseHeadersApi
548+responseHeadersServer = let h = return $ addHeader 5 $ addHeader "kilroy" "hi"
549+ in h :<|> h :<|> h :<|> h
550+
551+
552+responseHeadersSpec :: Spec
553+responseHeadersSpec = describe "ResponseHeaders" $ do
554+ with (return $ serve (Proxy :: Proxy ResponseHeadersApi) responseHeadersServer) $ do
555+
556+ let methods = [(methodGet, 200), (methodPost, 201), (methodPut, 200), (methodPatch, 200)]
557+
558+ it "includes the headers in the response" $
559+ forM_ methods $ \(method, expected) ->
560+ Test.Hspec.Wai.request method "/" [] ""
561+ `shouldRespondWith` "\"hi\""{ matchHeaders = ["H1" <:> "5", "H2" <:> "kilroy"]
562+ , matchStatus = expected
563+ }
564+
565+ it "responds with not found for non-existent endpoints" $
566+ forM_ methods $ \(method,_) ->
567+ Test.Hspec.Wai.request method "blahblah" [] ""
568+ `shouldRespondWith` 404
569+
570+ it "returns 415 if the Accept header is not supported" $
571+ forM_ methods $ \(method,_) ->
572+ Test.Hspec.Wai.request method "" [(hAccept, "crazy/mime")] ""
573+ `shouldRespondWith` 415
574+
575+
576+-- | Test server error functionality.
577+errorsSpec :: Spec
578+errorsSpec = do
579+ let he = HttpError status409 (Just "A custom error")
580+ let ib = InvalidBody "The body is invalid"
581+ let wm = WrongMethod
582+ let nf = NotFound
583+
584+ describe "Servant.Server.Internal.RouteMismatch" $ do
585+ it "HttpError > *" $ do
586+ ib <> he `shouldBe` he
587+ wm <> he `shouldBe` he
588+ nf <> he `shouldBe` he
589+
590+ he <> ib `shouldBe` he
591+ he <> wm `shouldBe` he
592+ he <> nf `shouldBe` he
593+
594+ it "HE > InvalidBody > (WM,NF)" $ do
595+ he <> ib `shouldBe` he
596+ wm <> ib `shouldBe` ib
597+ nf <> ib `shouldBe` ib
598+
599+ ib <> he `shouldBe` he
600+ ib <> wm `shouldBe` ib
601+ ib <> nf `shouldBe` ib
602+
603+ it "HE > IB > WrongMethod > NF" $ do
604+ he <> wm `shouldBe` he
605+ ib <> wm `shouldBe` ib
606+ nf <> wm `shouldBe` wm
607+
608+ wm <> he `shouldBe` he
609+ wm <> ib `shouldBe` ib
610+ wm <> nf `shouldBe` wm
611+
612+ it "* > NotFound" $ do
613+ he <> nf `shouldBe` he
614+ ib <> nf `shouldBe` ib
615+ wm <> nf `shouldBe` wm
616+
617+ nf <> he `shouldBe` he
618+ nf <> ib `shouldBe` ib
619+ nf <> wm `shouldBe` wm
servant-server/test/Servant/Utils/StaticFilesSpec.hsnew file+65
@@ -0,0 +1,65 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE FlexibleInstances #-}
3+{-# LANGUAGE OverloadedStrings #-}
4+{-# LANGUAGE TypeOperators #-}
5+{-# OPTIONS_GHC -fno-warn-orphans #-}
6+module Servant.Utils.StaticFilesSpec where
7+
8+import Control.Exception (bracket)
9+import Data.Proxy (Proxy(Proxy))
10+import Network.Wai (Application)
11+import System.Directory (getCurrentDirectory, setCurrentDirectory, createDirectory)
12+import System.IO.Temp (withSystemTempDirectory)
13+import Test.Hspec (Spec, describe, it, around_)
14+import Test.Hspec.Wai (with, get, shouldRespondWith)
15+
16+import Servant.API (JSON)
17+import Servant.API.Alternative ((:<|>)((:<|>)))
18+import Servant.API.Capture (Capture)
19+import Servant.API.Get (Get)
20+import Servant.API.Raw (Raw)
21+import Servant.API.Sub ((:>))
22+import Servant.Server (Server, serve)
23+import Servant.ServerSpec (Person(Person))
24+import Servant.Utils.StaticFiles (serveDirectory)
25+
26+type Api =
27+ "dummy_api" :> Capture "person_name" String :> Get '[JSON] Person
28+ :<|> "static" :> Raw
29+
30+
31+api :: Proxy Api
32+api = Proxy
33+
34+app :: Application
35+app = serve api server
36+
37+server :: Server Api
38+server =
39+ (\ name_ -> return (Person name_ 42))
40+ :<|> serveDirectory "static"
41+
42+withStaticFiles :: IO () -> IO ()
43+withStaticFiles action = withSystemTempDirectory "servant-test" $ \ tmpDir ->
44+ bracket (setup tmpDir) teardown (const action)
45+ where
46+ setup tmpDir = do
47+ outer <- getCurrentDirectory
48+ setCurrentDirectory tmpDir
49+ createDirectory "static"
50+ writeFile "static/foo.txt" "bar"
51+ writeFile "static/index.html" "index"
52+ return outer
53+
54+ teardown outer = do
55+ setCurrentDirectory outer
56+
57+spec :: Spec
58+spec = do
59+ around_ withStaticFiles $ with (return app) $ do
60+ describe "serveDirectory" $ do
61+ it "successfully serves files" $ do
62+ get "/static/foo.txt" `shouldRespondWith` "bar"
63+
64+ it "serves the contents of index.html when requesting the root of a directory" $ do
65+ get "/static/" `shouldRespondWith` "index"
servant-server/test/Spec.hsnew file+1
@@ -0,0 +1 @@
1+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}