git.iko.soy/typeable/servant
on master help sign in
first shot at a server package
commite43532b71dd67a7c48f8ed69e66be031344e4ba4 authorAlp Mestanogullari <alpmestan@gmail.com> dateWed 10 Dec 2014, 16:10 (12 years ago)
14 files changed+1159
LICENSE+30 README.md+25 Setup.hs+2 example/README.md+2 example/greet.hs+72 example/greet.md+52 servant-server.cabal+95 src/Servant.hs+24 src/Servant/Server.hs+39 src/Servant/Server/Internal.hs+459 src/Servant/Utils/StaticFiles.hs+36 test/Servant/ServerSpec.hs+258 test/Servant/Utils/StaticFilesSpec.hs+64 test/Spec.hs+1
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.
README.mdnew file+25
@@ -0,0 +1,25 @@
1+# servant
2+
3+[![Build Status](https://secure.travis-ci.org/haskell-servant/servant.svg)](http://travis-ci.org/haskell-servant/servant)
4+
5+![servant](https://raw.githubusercontent.com/haskell-servant/servant/master/servant.png)
6+
7+These libraries provides a family of combinators to define webservices and automatically generate the documentation and client-side querying functions for each endpoint.
8+
9+In order to minimize the dependencies depending on your needs, we provide these features under different packages.
10+
11+- `servant`, which contains everything you need to *declare* a webservice and *implement* an HTTP server with handlers for each endpoint.
12+- `servant-client`, which lets you derive automatically Haskell functions that let you query each endpoint of a *servant* webservice.
13+- `servant-docs`, which lets you generate API docs for your webservice.
14+- `servant-jquery`, which lets you derive Javascript functions (based on jquery) to query your API's endpoints, in the same spirit as `servant-client`.
15+
16+## Getting started
17+
18+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.
19+
20+## Repositories and Haddocks
21+
22+- The core [servant](http://github.com/haskell-servant) package - [docs](http://haskell-servant.github.io/servant/)
23+- (Haskell) client-side function generation with [servant-client](http://github.com/haskell-servant/servant-client) - [docs](http://haskell-servant.github.io/servant-client/)
24+- (Javascript) client-side function generation with [servant-jquery](http://github.com/haskell-servant/servant-jquery) - [docs](http://haskell-servant.github.io/servant-jquery/)
25+- API docs generation with [servant-docs](http://github.com/haskell-servant/servant-docs) - [docs](http://haskell-servant.github.io/servant-docs/)
Setup.hsnew file+2
@@ -0,0 +1,2 @@
1+import Distribution.Simple
2+main = defaultMain
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.
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 Greet
31+
32+ -- POST /greet with a Greet as JSON in the request body,
33+ -- returns a Greet as JSON
34+ :<|> "greet" :> ReqBody Greet :> Post 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
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.cabalnew file+95
@@ -0,0 +1,95 @@
1+name: servant-server
2+version: 0.2.1
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+source-repository head
23+ type: git
24+ location: http://github.com/haskell-servant/servant-server.git
25+
26+library
27+ exposed-modules:
28+ Servant
29+ Servant.Server
30+ Servant.Utils.StaticFiles
31+ other-modules: Servant.Server.Internal
32+ build-depends:
33+ base >=4.7 && <5
34+ , aeson
35+ , attoparsec
36+ , bytestring
37+ , either
38+ , http-types
39+ , network-uri >= 2.6
40+ , safe
41+ , servant >= 0.2
42+ , split
43+ , string-conversions
44+ , system-filepath
45+ , text
46+ , transformers
47+ , wai
48+ , wai-app-static
49+ , warp
50+ hs-source-dirs: src
51+ default-language: Haskell2010
52+ ghc-options: -Wall
53+
54+executable greet
55+ main-is: greet.hs
56+ hs-source-dirs: example
57+ ghc-options: -Wall
58+ default-language: Haskell2010
59+ build-depends:
60+ base
61+ , servant
62+ , servant-server
63+ , aeson
64+ , warp
65+ , wai
66+ , text
67+
68+test-suite spec
69+ type: exitcode-stdio-1.0
70+ ghc-options:
71+ -Wall -fno-warn-name-shadowing -fno-warn-missing-signatures
72+ default-language: Haskell2010
73+ hs-source-dirs: test
74+ main-is: Spec.hs
75+ build-depends:
76+ base == 4.*
77+ , aeson
78+ , bytestring
79+ , directory
80+ , either
81+ , exceptions
82+ , hspec == 2.*
83+ , hspec-wai
84+ , http-types
85+ , network >= 2.6
86+ , QuickCheck
87+ , parsec
88+ , servant
89+ , string-conversions
90+ , temporary
91+ , text
92+ , transformers
93+ , wai
94+ , wai-extra
95+ , warp
src/Servant.hsnew file+24
@@ -0,0 +1,24 @@
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.QQ,
12+ module Servant.Utils.Links,
13+ module Servant.Utils.StaticFiles,
14+ -- | Useful re-exports
15+ Proxy(..),
16+ ) where
17+
18+import Data.Proxy
19+import Servant.API
20+import Servant.Common.Text
21+import Servant.Server
22+import Servant.QQ
23+import Servant.Utils.Links
24+import Servant.Utils.StaticFiles
src/Servant/Server.hsnew file+39
@@ -0,0 +1,39 @@
1+{-# LANGUAGE TypeFamilies #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+
4+-- | This module lets you implement 'Server's for defined APIs. You'll
5+-- most likely just need 'serve'.
6+module Servant.Server
7+ ( -- * Implementing an API
8+ serve
9+
10+ , -- * Handlers for all standard combinators
11+ HasServer(..)
12+ ) where
13+
14+import Data.Proxy
15+import Network.Wai
16+import Servant.Server.Internal
17+
18+-- * Implementing Servers
19+
20+-- | 'serve' allows you to implement an API and produce a wai 'Application'.
21+--
22+-- Example:
23+--
24+-- > type MyApi = "books" :> Get [Book] -- GET /books
25+-- > :<|> "books" :> ReqBody Book :> Post Book -- POST /books
26+-- >
27+-- > server :: Server MyApi
28+-- > server = listAllBooks :<|> postBook
29+-- > where listAllBooks = ...
30+-- > postBook book = ...
31+-- >
32+-- > app :: Application
33+-- > app = serve myApi server
34+-- >
35+-- > main :: IO ()
36+-- > main = Network.Wai.Handler.Warp.run 8080 app
37+serve :: HasServer layout => Proxy layout -> Server layout -> Application
38+serve p server = toApplication (route p server)
39+
src/Servant/Server/Internal.hsnew file+459
@@ -0,0 +1,459 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE PolyKinds #-}
3+{-# LANGUAGE TypeFamilies #-}
4+{-# LANGUAGE TypeOperators #-}
5+{-# LANGUAGE FlexibleInstances #-}
6+{-# LANGUAGE OverloadedStrings #-}
7+{-# LANGUAGE ScopedTypeVariables #-}
8+module Servant.Server.Internal where
9+
10+import Control.Applicative
11+import Control.Monad.Trans.Either
12+import Data.Aeson
13+import Data.Maybe (catMaybes)
14+import Data.Monoid
15+import Data.Proxy
16+import Data.String
17+import Data.String.Conversions
18+import Data.Text (Text)
19+import Data.Text.Encoding (decodeUtf8)
20+import GHC.TypeLits
21+import Network.HTTP.Types hiding (Header)
22+import Network.Wai
23+import Servant.API
24+import Servant.Common.Text
25+
26+toApplication :: RoutingApplication -> Application
27+toApplication ra request respond = do
28+ ra request (routingRespond . routeResult)
29+ where
30+ routingRespond :: Either RouteMismatch Response -> IO ResponseReceived
31+ routingRespond (Left NotFound) =
32+ respond $ responseLBS notFound404 [] "not found"
33+ routingRespond (Left WrongMethod) =
34+ respond $ responseLBS methodNotAllowed405 [] "method not allowed"
35+ routingRespond (Left InvalidBody) =
36+ respond $ responseLBS badRequest400 [] "Invalid JSON in request body"
37+ routingRespond (Right response) =
38+ respond response
39+
40+-- * Route mismatch
41+data RouteMismatch =
42+ NotFound -- ^ the usual "not found" error
43+ | WrongMethod -- ^ a more informative "you just got the HTTP method wrong" error
44+ | InvalidBody -- ^ an even more informative "your json request body wasn't valid" error
45+ deriving (Eq, Show)
46+
47+-- |
48+-- @
49+-- > mempty = NotFound
50+-- >
51+-- > NotFound `mappend` x = x
52+-- > WrongMethod `mappend` InvalidBody = InvalidBody
53+-- > WrongMethod `mappend` _ = WrongMethod
54+-- > InvalidBody `mappend` _ = InvalidBody
55+-- @
56+instance Monoid RouteMismatch where
57+ mempty = NotFound
58+
59+ NotFound `mappend` x = x
60+ WrongMethod `mappend` InvalidBody = InvalidBody
61+ WrongMethod `mappend` _ = WrongMethod
62+ InvalidBody `mappend` _ = InvalidBody
63+
64+-- | A wrapper around @'Either' 'RouteMismatch' a@.
65+newtype RouteResult a =
66+ RR { routeResult :: Either RouteMismatch a }
67+ deriving (Eq, Show)
68+
69+failWith :: RouteMismatch -> RouteResult a
70+failWith = RR . Left
71+
72+succeedWith :: a -> RouteResult a
73+succeedWith = RR . Right
74+
75+isMismatch :: RouteResult a -> Bool
76+isMismatch (RR (Left _)) = True
77+isMismatch _ = False
78+
79+-- | If we get a `Right`, it has precedence over everything else.
80+--
81+-- This in particular means that if we could get several 'Right's,
82+-- only the first we encounter would be taken into account.
83+instance Monoid (RouteResult a) where
84+ mempty = RR $ Left mempty
85+
86+ RR (Left x) `mappend` RR (Left y) = RR $ Left (x <> y)
87+ RR (Left _) `mappend` RR (Right y) = RR $ Right y
88+ r `mappend` _ = r
89+
90+type RoutingApplication =
91+ Request -- ^ the request, the field 'pathInfo' may be modified by url routing
92+ -> (RouteResult Response -> IO ResponseReceived) -> IO ResponseReceived
93+
94+class HasServer layout where
95+ type Server layout :: *
96+ route :: Proxy layout -> Server layout -> RoutingApplication
97+
98+-- * Instances
99+
100+-- | A server for @a ':<|>' b@ first tries to match the request again the route
101+-- represented by @a@ and if it fails tries @b@. You must provide a request
102+-- handler for each route.
103+--
104+-- > type MyApi = "books" :> Get [Book] -- GET /books
105+-- > :<|> "books" :> ReqBody Book :> Post Book -- POST /books
106+-- >
107+-- > server :: Server MyApi
108+-- > server = listAllBooks :<|> postBook
109+-- > where listAllBooks = ...
110+-- > postBook book = ...
111+instance (HasServer a, HasServer b) => HasServer (a :<|> b) where
112+ type Server (a :<|> b) = Server a :<|> Server b
113+ route Proxy (a :<|> b) request respond =
114+ route pa a request $ \ mResponse ->
115+ if isMismatch mResponse
116+ then route pb b request $ \mResponse' -> respond (mResponse <> mResponse')
117+ else respond mResponse
118+
119+ where pa = Proxy :: Proxy a
120+ pb = Proxy :: Proxy b
121+
122+captured :: FromText a => proxy (Capture sym a) -> Text -> Maybe a
123+captured _ = fromText
124+
125+-- | If you use 'Capture' in one of the endpoints for your API,
126+-- this automatically requires your server-side handler to be a function
127+-- that takes an argument of the type specified by the 'Capture'.
128+-- This lets servant worry about getting it from the URL and turning
129+-- it into a value of the type you specify.
130+--
131+-- You can control how it'll be converted from 'Text' to your type
132+-- by simply providing an instance of 'FromText' for your type.
133+--
134+-- Example:
135+--
136+-- > type MyApi = "books" :> Capture "isbn" Text :> Get Book
137+-- >
138+-- > server :: Server MyApi
139+-- > server = getBook
140+-- > where getBook :: Text -> EitherT (Int, String) IO Book
141+-- > getBook isbn = ...
142+instance (KnownSymbol capture, FromText a, HasServer sublayout)
143+ => HasServer (Capture capture a :> sublayout) where
144+
145+ type Server (Capture capture a :> sublayout) =
146+ a -> Server sublayout
147+
148+ route Proxy subserver request respond = case pathInfo request of
149+ (first : rest)
150+ -> case captured captureProxy first of
151+ Nothing -> respond $ failWith NotFound
152+ Just v -> route (Proxy :: Proxy sublayout) (subserver v) request{
153+ pathInfo = rest
154+ } respond
155+ _ -> respond $ failWith NotFound
156+
157+ where captureProxy = Proxy :: Proxy (Capture capture a)
158+
159+-- | If you have a 'Delete' endpoint in your API,
160+-- the handler for this endpoint is meant to delete
161+-- a resource.
162+--
163+-- The code of the handler will, just like
164+-- for 'Servant.API.Get.Get', 'Servant.API.Post.Post' and
165+-- 'Servant.API.Put.Put', run in @EitherT (Int, String) IO ()@.
166+-- The 'Int' represents the status code and the 'String' a message
167+-- to be returned. You can use 'Control.Monad.Trans.Either.left' to
168+-- painlessly error out if the conditions for a successful deletion
169+-- are not met.
170+instance HasServer Delete where
171+ type Server Delete = EitherT (Int, String) IO ()
172+
173+ route Proxy action request respond
174+ | null (pathInfo request) && requestMethod request == methodDelete = do
175+ e <- runEitherT action
176+ respond $ succeedWith $ case e of
177+ Right () ->
178+ responseLBS status204 [] ""
179+ Left (status, message) ->
180+ responseLBS (mkStatus status (cs message)) [] (cs message)
181+ | null (pathInfo request) && requestMethod request /= methodDelete =
182+ respond $ failWith WrongMethod
183+ | otherwise = respond $ failWith NotFound
184+
185+-- | When implementing the handler for a 'Get' endpoint,
186+-- just like for 'Servant.API.Delete.Delete', 'Servant.API.Post.Post'
187+-- and 'Servant.API.Put.Put', the handler code runs in the
188+-- @EitherT (Int, String) IO@ monad, where the 'Int' represents
189+-- the status code and the 'String' a message, returned in case of
190+-- failure. You can quite handily use 'Control.Monad.Trans.EitherT.left'
191+-- to quickly fail if some conditions are not met.
192+--
193+-- If successfully returning a value, we just require that its type has
194+-- a 'ToJSON' instance and servant takes care of encoding it for you,
195+-- yielding status code 200 along the way.
196+instance ToJSON result => HasServer (Get result) where
197+ type Server (Get result) = EitherT (Int, String) IO result
198+ route Proxy action request respond
199+ | null (pathInfo request) && requestMethod request == methodGet = do
200+ e <- runEitherT action
201+ respond . succeedWith $ case e of
202+ Right output ->
203+ responseLBS ok200 [("Content-Type", "application/json")] (encode output)
204+ Left (status, message) ->
205+ responseLBS (mkStatus status (cs message)) [] (cs message)
206+ | null (pathInfo request) && requestMethod request /= methodGet =
207+ respond $ failWith WrongMethod
208+ | otherwise = respond $ failWith NotFound
209+
210+-- | If you use 'Header' in one of the endpoints for your API,
211+-- this automatically requires your server-side handler to be a function
212+-- that takes an argument of the type specified by 'Header'.
213+-- This lets servant worry about extracting it from the request and turning
214+-- it into a value of the type you specify.
215+--
216+-- All it asks is for a 'FromText' instance.
217+--
218+-- Example:
219+--
220+-- > newtype Referer = Referer Text
221+-- > deriving (Eq, Show, FromText, ToText)
222+-- >
223+-- > -- GET /view-my-referer
224+-- > type MyApi = "view-my-referer" :> Header "Referer" Referer :> Get Referer
225+-- >
226+-- > server :: Server MyApi
227+-- > server = viewReferer
228+-- > where viewReferer :: Referer -> EitherT (Int, String) IO referer
229+-- > viewReferer referer = return referer
230+instance (KnownSymbol sym, FromText a, HasServer sublayout)
231+ => HasServer (Header sym a :> sublayout) where
232+
233+ type Server (Header sym a :> sublayout) =
234+ Maybe a -> Server sublayout
235+
236+ route Proxy subserver request respond = do
237+ let mheader = fromText . decodeUtf8 =<< lookup str (requestHeaders request)
238+ route (Proxy :: Proxy sublayout) (subserver mheader) request respond
239+
240+ where str = fromString $ symbolVal (Proxy :: Proxy sym)
241+
242+-- | When implementing the handler for a 'Post' endpoint,
243+-- just like for 'Servant.API.Delete.Delete', 'Servant.API.Get.Get'
244+-- and 'Servant.API.Put.Put', the handler code runs in the
245+-- @EitherT (Int, String) IO@ monad, where the 'Int' represents
246+-- the status code and the 'String' a message, returned in case of
247+-- failure. You can quite handily use 'Control.Monad.Trans.EitherT.left'
248+-- to quickly fail if some conditions are not met.
249+--
250+-- If successfully returning a value, we just require that its type has
251+-- a 'ToJSON' instance and servant takes care of encoding it for you,
252+-- yielding status code 201 along the way.
253+instance ToJSON a => HasServer (Post a) where
254+ type Server (Post a) = EitherT (Int, String) IO a
255+
256+ route Proxy action request respond
257+ | null (pathInfo request) && requestMethod request == methodPost = do
258+ e <- runEitherT action
259+ respond . succeedWith $ case e of
260+ Right out ->
261+ responseLBS status201 [("Content-Type", "application/json")] (encode out)
262+ Left (status, message) ->
263+ responseLBS (mkStatus status (cs message)) [] (cs message)
264+ | null (pathInfo request) && requestMethod request /= methodPost =
265+ respond $ failWith WrongMethod
266+ | otherwise = respond $ failWith NotFound
267+
268+-- | When implementing the handler for a 'Put' endpoint,
269+-- just like for 'Servant.API.Delete.Delete', 'Servant.API.Get.Get'
270+-- and 'Servant.API.Post.Post', the handler code runs in the
271+-- @EitherT (Int, String) IO@ monad, where the 'Int' represents
272+-- the status code and the 'String' a message, returned in case of
273+-- failure. You can quite handily use 'Control.Monad.Trans.EitherT.left'
274+-- to quickly fail if some conditions are not met.
275+--
276+-- If successfully returning a value, we just require that its type has
277+-- a 'ToJSON' instance and servant takes care of encoding it for you,
278+-- yielding status code 200 along the way.
279+instance ToJSON a => HasServer (Put a) where
280+ type Server (Put a) = EitherT (Int, String) IO a
281+
282+ route Proxy action request respond
283+ | null (pathInfo request) && requestMethod request == methodPut = do
284+ e <- runEitherT action
285+ respond . succeedWith $ case e of
286+ Right out ->
287+ responseLBS ok200 [("Content-Type", "application/json")] (encode out)
288+ Left (status, message) ->
289+ responseLBS (mkStatus status (cs message)) [] (cs message)
290+ | null (pathInfo request) && requestMethod request /= methodPut =
291+ respond $ failWith WrongMethod
292+
293+ | otherwise = respond $ failWith NotFound
294+
295+-- | If you use @'QueryParam' "author" Text@ in one of the endpoints for your API,
296+-- this automatically requires your server-side handler to be a function
297+-- that takes an argument of type @'Maybe' 'Text'@.
298+--
299+-- This lets servant worry about looking it up in the query string
300+-- and turning it into a value of the type you specify, enclosed
301+-- in 'Maybe', because it may not be there and servant would then
302+-- hand you 'Nothing'.
303+--
304+-- You can control how it'll be converted from 'Text' to your type
305+-- by simply providing an instance of 'FromText' for your type.
306+--
307+-- Example:
308+--
309+-- > type MyApi = "books" :> QueryParam "author" Text :> Get [Book]
310+-- >
311+-- > server :: Server MyApi
312+-- > server = getBooksBy
313+-- > where getBooksBy :: Maybe Text -> EitherT (Int, String) IO [Book]
314+-- > getBooksBy Nothing = ...return all books...
315+-- > getBooksBy (Just author) = ...return books by the given author...
316+instance (KnownSymbol sym, FromText a, HasServer sublayout)
317+ => HasServer (QueryParam sym a :> sublayout) where
318+
319+ type Server (QueryParam sym a :> sublayout) =
320+ Maybe a -> Server sublayout
321+
322+ route Proxy subserver request respond = do
323+ let querytext = parseQueryText $ rawQueryString request
324+ param =
325+ case lookup paramname querytext of
326+ Nothing -> Nothing -- param absent from the query string
327+ Just Nothing -> Nothing -- param present with no value -> Nothing
328+ Just (Just v) -> fromText v -- if present, we try to convert to
329+ -- the right type
330+
331+ route (Proxy :: Proxy sublayout) (subserver param) request respond
332+
333+ where paramname = cs $ symbolVal (Proxy :: Proxy sym)
334+
335+-- | If you use @'QueryParams' "authors" Text@ in one of the endpoints for your API,
336+-- this automatically requires your server-side handler to be a function
337+-- that takes an argument of type @['Text']@.
338+--
339+-- This lets servant worry about looking up 0 or more values in the query string
340+-- associated to @authors@ and turning each of them into a value of
341+-- the type you specify.
342+--
343+-- You can control how the individual values are converted from 'Text' to your type
344+-- by simply providing an instance of 'FromText' for your type.
345+--
346+-- Example:
347+--
348+-- > type MyApi = "books" :> QueryParams "authors" Text :> Get [Book]
349+-- >
350+-- > server :: Server MyApi
351+-- > server = getBooksBy
352+-- > where getBooksBy :: [Text] -> EitherT (Int, String) IO [Book]
353+-- > getBooksBy authors = ...return all books by these authors...
354+instance (KnownSymbol sym, FromText a, HasServer sublayout)
355+ => HasServer (QueryParams sym a :> sublayout) where
356+
357+ type Server (QueryParams sym a :> sublayout) =
358+ [a] -> Server sublayout
359+
360+ route Proxy subserver request respond = do
361+ let querytext = parseQueryText $ rawQueryString request
362+ -- if sym is "foo", we look for query string parameters
363+ -- named "foo" or "foo[]" and call fromText on the
364+ -- corresponding values
365+ parameters = filter looksLikeParam querytext
366+ values = catMaybes $ map (convert . snd) parameters
367+
368+ route (Proxy :: Proxy sublayout) (subserver values) request respond
369+
370+ where paramname = cs $ symbolVal (Proxy :: Proxy sym)
371+ looksLikeParam (name, _) = name == paramname || name == (paramname <> "[]")
372+ convert Nothing = Nothing
373+ convert (Just v) = fromText v
374+
375+-- | If you use @'QueryFlag' "published"@ in one of the endpoints for your API,
376+-- this automatically requires your server-side handler to be a function
377+-- that takes an argument of type 'Bool'.
378+--
379+-- Example:
380+--
381+-- > type MyApi = "books" :> QueryFlag "published" :> Get [Book]
382+-- >
383+-- > server :: Server MyApi
384+-- > server = getBooks
385+-- > where getBooks :: Bool -> EitherT (Int, String) IO [Book]
386+-- > getBooks onlyPublished = ...return all books, or only the ones that are already published, depending on the argument...
387+instance (KnownSymbol sym, HasServer sublayout)
388+ => HasServer (QueryFlag sym :> sublayout) where
389+
390+ type Server (QueryFlag sym :> sublayout) =
391+ Bool -> Server sublayout
392+
393+ route Proxy subserver request respond = do
394+ let querytext = parseQueryText $ rawQueryString request
395+ param = case lookup paramname querytext of
396+ Just Nothing -> True -- param is there, with no value
397+ Just (Just v) -> examine v -- param with a value
398+ Nothing -> False -- param not in the query string
399+
400+ route (Proxy :: Proxy sublayout) (subserver param) request respond
401+
402+ where paramname = cs $ symbolVal (Proxy :: Proxy sym)
403+ examine v | v == "true" || v == "1" || v == "" = True
404+ | otherwise = False
405+
406+-- | Just pass the request to the underlying application and serve its response.
407+--
408+-- Example:
409+--
410+-- > type MyApi = "images" :> Raw
411+-- >
412+-- > server :: Server MyApi
413+-- > server = serveDirectory "/var/www/images"
414+instance HasServer Raw where
415+ type Server Raw = Application
416+ route Proxy rawApplication request respond =
417+ rawApplication request (respond . succeedWith)
418+
419+-- | If you use 'ReqBody' in one of the endpoints for your API,
420+-- this automatically requires your server-side handler to be a function
421+-- that takes an argument of the type specified by 'ReqBody'.
422+-- This lets servant worry about extracting it from the request and turning
423+-- it into a value of the type you specify.
424+--
425+-- All it asks is for a 'FromJSON' instance.
426+--
427+-- Example:
428+--
429+-- > type MyApi = "books" :> ReqBody Book :> Post Book
430+-- >
431+-- > server :: Server MyApi
432+-- > server = postBook
433+-- > where postBook :: Book -> EitherT (Int, String) IO Book
434+-- > postBook book = ...insert into your db...
435+instance (FromJSON a, HasServer sublayout)
436+ => HasServer (ReqBody a :> sublayout) where
437+
438+ type Server (ReqBody a :> sublayout) =
439+ a -> Server sublayout
440+
441+ route Proxy subserver request respond = do
442+ mrqbody <- decode' <$> lazyRequestBody request
443+ case mrqbody of
444+ Nothing -> respond $ failWith InvalidBody
445+ Just v -> route (Proxy :: Proxy sublayout) (subserver v) request respond
446+
447+-- | Make sure the incoming request starts with @"/path"@, strip it and
448+-- pass the rest of the request path to @sublayout@.
449+instance (KnownSymbol path, HasServer sublayout) => HasServer (path :> sublayout) where
450+ type Server (path :> sublayout) = Server sublayout
451+ route Proxy subserver request respond = case pathInfo request of
452+ (first : rest)
453+ | first == cs (symbolVal proxyPath)
454+ -> route (Proxy :: Proxy sublayout) subserver request{
455+ pathInfo = rest
456+ } respond
457+ _ -> respond $ failWith NotFound
458+
459+ where proxyPath = Proxy :: Proxy path
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
11+import Servant.API.Raw
12+import Servant.Server.Internal
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 ++ "/")))
test/Servant/ServerSpec.hsnew file+258
@@ -0,0 +1,258 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE DeriveGeneric #-}
3+{-# LANGUAGE OverloadedStrings #-}
4+{-# LANGUAGE QuasiQuotes #-}
5+{-# LANGUAGE ScopedTypeVariables #-}
6+{-# LANGUAGE TypeOperators #-}
7+
8+module Servant.ServerSpec where
9+
10+
11+import Control.Monad.Trans.Either
12+import Data.Aeson
13+import Data.Char
14+import Data.Proxy
15+import Data.String
16+import Data.String.Conversions
17+import GHC.Generics
18+import Network.HTTP.Types
19+import Network.Wai
20+import Network.Wai.Test
21+import Test.Hspec
22+import Test.Hspec.Wai
23+
24+import Servant.API.Capture
25+import Servant.API.Get
26+import Servant.API.ReqBody
27+import Servant.API.Post
28+import Servant.API.QueryParam
29+import Servant.API.Raw
30+import Servant.API.Sub
31+import Servant.API.Alternative
32+import Servant.Server
33+
34+
35+-- * test data types
36+
37+data Person = Person {
38+ name :: String,
39+ age :: Integer
40+ }
41+ deriving (Eq, Show, Generic)
42+
43+instance ToJSON Person
44+instance FromJSON Person
45+
46+alice :: Person
47+alice = Person "Alice" 42
48+
49+data Animal = Animal {
50+ species :: String,
51+ numberOfLegs :: Integer
52+ }
53+ deriving (Eq, Show, Generic)
54+
55+instance ToJSON Animal
56+instance FromJSON Animal
57+
58+jerry :: Animal
59+jerry = Animal "Mouse" 4
60+
61+tweety :: Animal
62+tweety = Animal "Bird" 2
63+
64+
65+-- * specs
66+
67+spec :: Spec
68+spec = do
69+ captureSpec
70+ getSpec
71+ queryParamSpec
72+ postSpec
73+ rawSpec
74+ unionSpec
75+
76+
77+type CaptureApi = Capture "legs" Integer :> Get Animal
78+captureApi :: Proxy CaptureApi
79+captureApi = Proxy
80+captureServer :: Integer -> EitherT (Int, String) IO Animal
81+captureServer legs = case legs of
82+ 4 -> return jerry
83+ 2 -> return tweety
84+ _ -> left (404, "not found")
85+
86+captureSpec :: Spec
87+captureSpec = do
88+ describe "Servant.API.Capture" $ do
89+ with (return (serve captureApi captureServer)) $ do
90+ it "can capture parts of the 'pathInfo'" $ do
91+ response <- get "/2"
92+ liftIO $ do
93+ decode' (simpleBody response) `shouldBe` Just tweety
94+
95+ with (return (serve
96+ (Proxy :: Proxy (Capture "captured" String :> Raw))
97+ (\ "captured" request respond ->
98+ respond $ responseLBS ok200 [] (cs $ show $ pathInfo request)))) $ do
99+ it "strips the captured path snippet from pathInfo" $ do
100+ get "/captured/foo" `shouldRespondWith` (fromString (show ["foo" :: String]))
101+
102+
103+type GetApi = Get Person
104+getApi :: Proxy GetApi
105+getApi = Proxy
106+
107+getSpec :: Spec
108+getSpec = do
109+ describe "Servant.API.Get" $ do
110+ with (return (serve getApi (return alice))) $ do
111+ it "allows to GET a Person" $ do
112+ response <- get "/"
113+ return response `shouldRespondWith` 200
114+ liftIO $ do
115+ decode' (simpleBody response) `shouldBe` Just alice
116+
117+ it "throws 405 (wrong method) on POSTs" $ do
118+ post "/" "" `shouldRespondWith` 405
119+
120+
121+type QueryParamApi = QueryParam "name" String :> Get Person
122+ :<|> "a" :> QueryParams "names" String :> Get Person
123+ :<|> "b" :> QueryFlag "capitalize" :> Get Person
124+
125+queryParamApi :: Proxy QueryParamApi
126+queryParamApi = Proxy
127+
128+qpServer :: Server QueryParamApi
129+qpServer = queryParamServer :<|> qpNames :<|> qpCapitalize
130+
131+ where qpNames (_:name2:_) = return alice { name = name2 }
132+ qpNames _ = return alice
133+
134+ qpCapitalize False = return alice
135+ qpCapitalize True = return alice { name = map toUpper (name alice) }
136+
137+ queryParamServer (Just name) = return alice{name = name}
138+ queryParamServer Nothing = return alice
139+
140+queryParamSpec :: Spec
141+queryParamSpec = do
142+ describe "Servant.API.QueryParam" $ do
143+ it "allows to retrieve simple GET parameters" $
144+ (flip runSession) (serve queryParamApi qpServer) $ do
145+ let params1 = "?name=bob"
146+ response1 <- Network.Wai.Test.request defaultRequest{
147+ rawQueryString = params1,
148+ queryString = parseQuery params1
149+ }
150+ liftIO $ do
151+ decode' (simpleBody response1) `shouldBe` Just alice{
152+ name = "bob"
153+ }
154+
155+ it "allows to retrieve lists in GET parameters" $
156+ (flip runSession) (serve queryParamApi qpServer) $ do
157+ let params2 = "?names[]=bob&names[]=john"
158+ response2 <- Network.Wai.Test.request defaultRequest{
159+ rawQueryString = params2,
160+ queryString = parseQuery params2,
161+ pathInfo = ["a"]
162+ }
163+ liftIO $
164+ decode' (simpleBody response2) `shouldBe` Just alice{
165+ name = "john"
166+ }
167+
168+ it "allows to retrieve value-less GET parameters" $
169+ (flip runSession) (serve queryParamApi qpServer) $ do
170+ let params3 = "?capitalize"
171+ response3 <- Network.Wai.Test.request defaultRequest{
172+ rawQueryString = params3,
173+ queryString = parseQuery params3,
174+ pathInfo = ["b"]
175+ }
176+ liftIO $
177+ decode' (simpleBody response3) `shouldBe` Just alice{
178+ name = "ALICE"
179+ }
180+
181+ let params3' = "?capitalize="
182+ response3' <- Network.Wai.Test.request defaultRequest{
183+ rawQueryString = params3',
184+ queryString = parseQuery params3',
185+ pathInfo = ["b"]
186+ }
187+ liftIO $
188+ decode' (simpleBody response3') `shouldBe` Just alice{
189+ name = "ALICE"
190+ }
191+
192+type PostApi = ReqBody Person :> Post Integer
193+postApi :: Proxy PostApi
194+postApi = Proxy
195+
196+postSpec :: Spec
197+postSpec = do
198+ describe "Servant.API.Post and .ReqBody" $ do
199+ with (return (serve postApi (return . age))) $ do
200+ it "allows to POST a Person" $ do
201+ post "/" (encode alice) `shouldRespondWith` "42"{
202+ matchStatus = 201
203+ }
204+
205+ it "correctly rejects invalid request bodies with status 400" $ do
206+ post "/" "some invalid body" `shouldRespondWith` 400
207+
208+
209+type RawApi = "foo" :> Raw
210+rawApi :: Proxy RawApi
211+rawApi = Proxy
212+rawApplication :: Show a => (Request -> a) -> Application
213+rawApplication f request respond = respond $ responseLBS ok200 [] (cs $ show $ f request)
214+
215+rawSpec :: Spec
216+rawSpec = do
217+ describe "Servant.API.Raw" $ do
218+ it "runs applications" $ do
219+ (flip runSession) (serve rawApi (rawApplication (const (42 :: Integer)))) $ do
220+ response <- Network.Wai.Test.request defaultRequest{
221+ pathInfo = ["foo"]
222+ }
223+ liftIO $ do
224+ simpleBody response `shouldBe` "42"
225+
226+ it "gets the pathInfo modified" $ do
227+ (flip runSession) (serve rawApi (rawApplication pathInfo)) $ do
228+ response <- Network.Wai.Test.request defaultRequest{
229+ pathInfo = ["foo", "bar"]
230+ }
231+ liftIO $ do
232+ simpleBody response `shouldBe` cs (show ["bar" :: String])
233+
234+
235+type AlternativeApi =
236+ "foo" :> Get Person
237+ :<|> "bar" :> Get Animal
238+unionApi :: Proxy AlternativeApi
239+unionApi = Proxy
240+
241+unionServer :: Server AlternativeApi
242+unionServer =
243+ return alice
244+ :<|> return jerry
245+
246+unionSpec :: Spec
247+unionSpec = do
248+ describe "Servant.API.Alternative" $ do
249+ with (return $ serve unionApi unionServer) $ do
250+ it "unions endpoints" $ do
251+ response <- get "/foo"
252+ liftIO $ do
253+ decode' (simpleBody response) `shouldBe`
254+ Just alice
255+ response <- get "/bar"
256+ liftIO $ do
257+ decode' (simpleBody response) `shouldBe`
258+ Just jerry
test/Servant/Utils/StaticFilesSpec.hsnew file+64
@@ -0,0 +1,64 @@
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
9+import Data.Proxy
10+import Network.Wai
11+import System.Directory
12+import System.IO.Temp
13+import Test.Hspec hiding (pending)
14+import Test.Hspec.Wai
15+
16+import Servant.API.Alternative
17+import Servant.API.Capture
18+import Servant.API.Get
19+import Servant.API.Raw
20+import Servant.API.Sub
21+import Servant.Server
22+import Servant.ServerSpec
23+import Servant.Utils.StaticFiles
24+
25+type Api =
26+ "dummy_api" :> Capture "person_name" String :> Get Person
27+ :<|> "static" :> Raw
28+
29+
30+api :: Proxy Api
31+api = Proxy
32+
33+app :: Application
34+app = serve api server
35+
36+server :: Server Api
37+server =
38+ (\ name -> return (Person name 42))
39+ :<|> serveDirectory "static"
40+
41+withStaticFiles :: IO () -> IO ()
42+withStaticFiles action = withSystemTempDirectory "servant-test" $ \ tmpDir ->
43+ bracket (setup tmpDir) teardown (const action)
44+ where
45+ setup tmpDir = do
46+ outer <- getCurrentDirectory
47+ setCurrentDirectory tmpDir
48+ createDirectory "static"
49+ writeFile "static/foo.txt" "bar"
50+ writeFile "static/index.html" "index"
51+ return outer
52+
53+ teardown outer = do
54+ setCurrentDirectory outer
55+
56+spec :: Spec
57+spec = do
58+ around_ withStaticFiles $ with (return app) $ do
59+ describe "serveDirectory" $ do
60+ it "successfully serves files" $ do
61+ get "/static/foo.txt" `shouldRespondWith` "bar"
62+
63+ it "serves the contents of index.html when requesting the root of a directory" $ do
64+ get "/static" `shouldRespondWith` "index"
test/Spec.hsnew file+1
@@ -0,0 +1 @@
1+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}