git.iko.soy/typeable/servant
on master help sign in
first shot at splitting servant into servant, servant-client and servant-docs
commit67abcff47f20866e86541a5b146900974af17c6c authorAlp Mestanogullari <alpmestan@gmail.com> dateThu 27 Nov 2014, 18:28 (12 years ago) parent9923d17
21 files changed+12−1459
example/greet.hs+2−65 servant.cabal+8−21 src/Servant.hs−6 src/Servant/API/Alternative.hs+1−34 src/Servant/API/Capture.hs−49 src/Servant/API/Delete.hs−22 src/Servant/API/Get.hs−20 src/Servant/API/Post.hs−24 src/Servant/API/Put.hs−24 src/Servant/API/QueryParam.hs−150 src/Servant/API/Raw.hs−20 src/Servant/API/ReqBody.hs−42 src/Servant/API/Sub.hs−22 src/Servant/Client.hs−39 src/Servant/Common/BaseUrl.hs−55 src/Servant/Common/Req.hs−132 src/Servant/Docs.hs−426 src/Servant/Utils/StaticFiles.hs+1−33 test/Servant/ClientSpec.hs−203 test/Servant/Common/BaseUrlSpec.hs−69 test/Servant/ServerSpec.hs−3
example/greet.hs+2−65
@@ -3,13 +3,8 @@
33{-# LANGUAGE TypeFamilies #-}
44{-# LANGUAGE DeriveGeneric #-}
55{-# LANGUAGE TypeOperators #-}
6−{-# LANGUAGE FlexibleInstances #-}
76{-# LANGUAGE OverloadedStrings #-}
8−{-# LANGUAGE QuasiQuotes #-}
9−{-# OPTIONS_GHC -fno-warn-orphans #-}
107
11−import Control.Concurrent (forkIO, killThread)
12−import Control.Monad.Trans.Either
138import Data.Aeson
149import Data.Monoid
1510import Data.Proxy
@@ -18,10 +13,7 @@ import GHC.Generics
1813import Network.Wai
1914import Network.Wai.Handler.Warp
2015
21−import Servant.API
22−import Servant.Client
23−import Servant.Docs
24−import Servant.Server
16+import Servant
2517
2618-- * Example
2719
@@ -32,25 +24,6 @@ newtype Greet = Greet { msg :: Text }
3224instance FromJSON Greet
3325instance ToJSON Greet
3426
35−-- We add some useful annotations to our captures,
36−-- query parameters and request body to make the docs
37−-- really helpful.
38−instance ToCapture (Capture "name" Text) where
39− toCapture _ = DocCapture "name" "name of the person to greet"
40−
41−instance ToCapture (Capture "greetid" Text) where
42− toCapture _ = DocCapture "greetid" "identifier of the greet msg to remove"
43−
44−instance ToParam (QueryParam "capital" Bool) where
45− toParam _ =
46− DocQueryParam "capital"
47− ["true", "false"]
48− "Get the greeting message in uppercase (true) or not (false). Default is false."
49− Normal
50−
51−instance ToSample Greet where
52− toSample = Just $ Greet "Hello, haskeller!"
53−
5427-- API specification
5528type TestApi =
5629 -- GET /hello/:name?capital={true, false} returns a Greet as JSON
@@ -83,30 +56,11 @@ server = helloH :<|> postGreetH :<|> deleteGreetH
8356
8457 deleteGreetH _ = return ()
8558
86−-- Client-side querying functions
87−--
88−-- They're all derived automatically from the type, and glued together
89−-- with :<|> just like in the type and for the server handlers, except
90−-- that we don't have to implement them!
91−clientApi :: Client TestApi
92−clientApi = client testApi
93−
94−getGreet :: Text -> Maybe Bool -> BaseUrl -> EitherT String IO Greet
95−postGreet :: Greet -> BaseUrl -> EitherT String IO Greet
96−deleteGreet :: Text -> BaseUrl -> EitherT String IO ()
97−getGreet :<|> postGreet :<|> deleteGreet = clientApi
98−
9959-- Turn the server into a WAI app. 'serve' is provided by servant,
10060-- more precisely by the Servant.Server module.
10161test :: Application
10262test = serve testApi server
10363
104−-- Generate the data that lets us have API docs. This
105−-- is derived from the type as well as from
106−-- the 'ToCapture', 'ToParam' and 'ToSample' instances from above.
107−docsGreet :: API
108−docsGreet = docs testApi
109−
11064-- Run the server.
11165--
11266-- 'run' comes from Network.Wai.Handler.Warp
@@ -115,21 +69,4 @@ runTestServer port = run port test
11569
11670-- Put this all to work!
11771main :: IO ()
118−main = do
119− -- we start the server, binding it to port 8001
120− tid <- forkIO $ runTestServer 8001
121−
122− -- we tell the client where to find it
123− let uri = BaseUrl Http "localhost" 8001
124−
125− -- we run a couple of requests against the server
126− print =<< runEitherT (getGreet "alp" (Just True) uri)
127− print =<< runEitherT (getGreet "alp" (Just False) uri)
128− let g = Greet "yo"
129− print =<< runEitherT (postGreet g uri)
130− print =<< runEitherT (deleteGreet "blah" uri)
131− killThread tid
132− putStrLn "\n---------\n"
133−
134− -- we print the markdown docs
135− putStrLn $ markdown docsGreet
72+main = runTestServer 8001
servant.cabal+8−21
@@ -26,40 +26,30 @@ library
2626 Servant.API.Raw
2727 Servant.API.ReqBody
2828 Servant.API.Sub
29− Servant.Client
30− Servant.Common.BaseUrl
31− Servant.Common.Req
3229 Servant.Common.Text
33− Servant.Docs
3430 Servant.QQ
3531 Servant.Server
3632 Servant.Utils.Links
3733 Servant.Utils.StaticFiles
3834 build-depends:
3935 base >=4.7 && <5
40− , either
4136 , aeson
4237 , attoparsec
4338 , bytestring
44− , exceptions
45− , string-conversions
46− , split
47− , http-client
39+ , either
4840 , http-types
4941 , network-uri >= 2.6
50− , wai
51− , wai-app-static
52− , warp
5342 , parsec
5443 , safe
55− , transformers
44+ , split
45+ , string-conversions
46+ , system-filepath
5647 , template-haskell
5748 , text
58− , system-filepath
59− , lens
60− , unordered-containers
61− , hashable
62− , system-filepath
49+ , transformers
50+ , wai
51+ , wai-app-static
52+ , warp
6353 hs-source-dirs: src
6454 default-language: Haskell2010
6555 ghc-options: -O0 -Wall
@@ -75,7 +65,6 @@ executable greet
7565 , aeson
7666 , warp
7767 , wai
78− , either
7968 , text
8069
8170test-suite spec
@@ -89,13 +78,11 @@ test-suite spec
8978 base == 4.*
9079 , aeson
9180 , bytestring
92− , deepseq
9381 , directory
9482 , either
9583 , exceptions
9684 , hspec == 2.*
9785 , hspec-wai
98− , http-client
9986 , http-types
10087 , network >= 2.6
10188 , QuickCheck
src/Servant.hs−6
@@ -5,10 +5,6 @@ module Servant (
55 module Servant.API,
66 -- | For implementing servers for servant APIs.
77 module Servant.Server,
8− -- | For accessing servant APIs as API clients.
9− module Servant.Client,
10− -- | For generating documentation for servant APIs.
11− module Servant.Docs,
128 -- | Using your types in request paths and query string parameters
139 module Servant.Common.Text,
1410 -- | Utilities on top of the servant core
@@ -21,9 +17,7 @@ module Servant (
2117
2218import Data.Proxy
2319import Servant.API
24−import Servant.Client
2520import Servant.Common.Text
26−import Servant.Docs
2721import Servant.Server
2822import Servant.QQ
2923import Servant.Utils.Links
src/Servant/API/Alternative.hs+1−34
@@ -3,9 +3,8 @@
33{-# LANGUAGE ScopedTypeVariables #-}
44module Servant.API.Alternative where
55
6+import Data.Monoid
67import Data.Proxy
7−import Servant.Client
8−import Servant.Docs
98import Servant.Server
109
1110-- | Union of two APIs, first takes precedence in case of overlap.
@@ -38,35 +37,3 @@ instance (HasServer a, HasServer b) => HasServer (a :<|> b) where
3837
3938 where pa = Proxy :: Proxy a
4039 pb = Proxy :: Proxy b
41−
42−-- | A client querying function for @a ':<|>' b@ will actually hand you
43−-- one function for querying @a@ and another one for querying @b@,
44−-- stitching them together with ':<|>', which really is just like a pair.
45−--
46−-- > type MyApi = "books" :> Get [Book] -- GET /books
47−-- > :<|> "books" :> ReqBody Book :> Post Book -- POST /books
48−-- >
49−-- > myApi :: Proxy MyApi
50−-- > myApi = Proxy
51−-- >
52−-- > getAllBooks :: BaseUrl -> EitherT String IO [Book]
53−-- > postNewBook :: Book -> BaseUrl -> EitherT String IO Book
54−-- > (getAllBooks :<|> postNewBook) = client myApi
55−instance (HasClient a, HasClient b) => HasClient (a :<|> b) where
56− type Client (a :<|> b) = Client a :<|> Client b
57− clientWithRoute Proxy req =
58− clientWithRoute (Proxy :: Proxy a) req :<|>
59− clientWithRoute (Proxy :: Proxy b) req
60−
61−-- | The generated docs for @a ':<|>' b@ just appends the docs
62−-- for @a@ with the docs for @b@.
63−instance (HasDocs layout1, HasDocs layout2)
64− => HasDocs (layout1 :<|> layout2) where
65−
66− docsFor Proxy (ep, action) = docsFor p1 (ep, action) <> docsFor p2 (ep, action)
67−
68− where p1 :: Proxy layout1
69− p1 = Proxy
70−
71− p2 :: Proxy layout2
72− p2 = Proxy
src/Servant/API/Capture.hs−49
@@ -11,10 +11,7 @@ import Data.Text
1111import GHC.TypeLits
1212import Network.Wai
1313import Servant.API.Sub
14−import Servant.Client
15−import Servant.Common.Req
1614import Servant.Common.Text
17−import Servant.Docs
1815import Servant.Server
1916
2017-- | Capture a value from the request path under a certain type @a@.
@@ -61,49 +58,3 @@ instance (KnownSymbol capture, FromText a, HasServer sublayout)
6158 _ -> respond $ failWith NotFound
6259
6360 where captureProxy = Proxy :: Proxy (Capture capture a)
64−
65−-- | If you use a 'Capture' in one of your endpoints in your API,
66−-- the corresponding querying function will automatically take
67−-- an additional argument of the type specified by your 'Capture'.
68−-- That function will take care of inserting a textual representation
69−-- of this value at the right place in the request path.
70−--
71−-- You can control how values for this type are turned into
72−-- text by specifying a 'ToText' instance for your type.
73−--
74−-- Example:
75−--
76−-- > type MyApi = "books" :> Capture "isbn" Text :> Get Book
77−-- >
78−-- > myApi :: Proxy MyApi
79−-- > myApi = Proxy
80−-- >
81−-- > getBook :: Text -> BaseUrl -> EitherT String IO Book
82−-- > getBook = client myApi
83−-- > -- then you can just use "getBook" to query that endpoint
84−instance (KnownSymbol capture, ToText a, HasClient sublayout)
85− => HasClient (Capture capture a :> sublayout) where
86−
87− type Client (Capture capture a :> sublayout) =
88− a -> Client sublayout
89−
90− clientWithRoute Proxy req val =
91− clientWithRoute (Proxy :: Proxy sublayout) $
92− appendToPath p req
93−
94− where p = unpack (toText val)
95−
96−-- | @"books" :> 'Capture' "isbn" Text@ will appear as
97−-- @/books/:isbn@ in the docs.
98−instance (KnownSymbol sym, ToCapture (Capture sym a), HasDocs sublayout)
99− => HasDocs (Capture sym a :> sublayout) where
100−
101− docsFor Proxy (endpoint, action) =
102− docsFor sublayoutP (endpoint', action')
103−
104− where sublayoutP = Proxy :: Proxy sublayout
105− captureP = Proxy :: Proxy (Capture sym a)
106−
107− action' = over captures (|> toCapture captureP) action
108− endpoint' = over path (\p -> p++"/:"++symbolVal symP) endpoint
109− symP = Proxy :: Proxy sym
src/Servant/API/Delete.hs−22
@@ -10,9 +10,6 @@ import Data.String.Conversions
1010import Data.Typeable
1111import Network.HTTP.Types
1212import Network.Wai
13−import Servant.Client
14−import Servant.Common.Req
15−import Servant.Docs
1613import Servant.Server
1714
1815-- | Combinator for DELETE requests.
@@ -49,22 +46,3 @@ instance HasServer Delete where
4946 | null (pathInfo request) && requestMethod request /= methodDelete =
5047 respond $ failWith WrongMethod
5148 | otherwise = respond $ failWith NotFound
52−
53−-- | If you have a 'Delete' endpoint in your API, the client
54−-- side querying function that is created when calling 'client'
55−-- will just require an argument that specifies the scheme, host
56−-- and port to send the request to.
57−instance HasClient Delete where
58− type Client Delete = BaseUrl -> EitherT String IO ()
59−
60− clientWithRoute Proxy req host =
61− performRequestJSON methodDelete req 204 host
62−
63−instance HasDocs Delete where
64− docsFor Proxy (endpoint, action) =
65− single endpoint' action'
66−
67− where endpoint' = endpoint & method .~ DocDELETE
68−
69− action' = action & response.respBody .~ Nothing
70− & response.respStatus .~ 204
src/Servant/API/Get.hs−20
@@ -11,9 +11,6 @@ import Data.String.Conversions
1111import Data.Typeable
1212import Network.HTTP.Types
1313import Network.Wai
14−import Servant.Client
15−import Servant.Common.Req
16−import Servant.Docs
1714import Servant.Server
1815
1916-- | Endpoint for simple GET requests. Serves the result as JSON.
@@ -48,20 +45,3 @@ instance ToJSON result => HasServer (Get result) where
4845 | null (pathInfo request) && requestMethod request /= methodGet =
4946 respond $ failWith WrongMethod
5047 | otherwise = respond $ failWith NotFound
51−
52−-- | If you have a 'Get' endpoint in your API, the client
53−-- side querying function that is created when calling 'client'
54−-- will just require an argument that specifies the scheme, host
55−-- and port to send the request to.
56−instance FromJSON result => HasClient (Get result) where
57− type Client (Get result) = BaseUrl -> EitherT String IO result
58− clientWithRoute Proxy req host =
59− performRequestJSON methodGet req 200 host
60−
61−instance ToSample a => HasDocs (Get a) where
62− docsFor Proxy (endpoint, action) =
63− single endpoint' action'
64−
65− where endpoint' = endpoint & method .~ DocGET
66− action' = action & response.respBody .~ sampleByteString p
67− p = Proxy :: Proxy a
src/Servant/API/Post.hs−24
@@ -11,9 +11,6 @@ import Data.String.Conversions
1111import Data.Typeable
1212import Network.HTTP.Types
1313import Network.Wai
14−import Servant.Client
15−import Servant.Common.Req
16−import Servant.Docs
1714import Servant.Server
1815
1916-- | Endpoint for POST requests. The type variable represents the type of the
@@ -54,24 +51,3 @@ instance ToJSON a => HasServer (Post a) where
5451 | null (pathInfo request) && requestMethod request /= methodPost =
5552 respond $ failWith WrongMethod
5653 | otherwise = respond $ failWith NotFound
57−
58−-- | If you have a 'Post' endpoint in your API, the client
59−-- side querying function that is created when calling 'client'
60−-- will just require an argument that specifies the scheme, host
61−-- and port to send the request to.
62−instance FromJSON a => HasClient (Post a) where
63− type Client (Post a) = BaseUrl -> EitherT String IO a
64−
65− clientWithRoute Proxy req uri =
66− performRequestJSON methodPost req 201 uri
67−
68−instance ToSample a => HasDocs (Post a) where
69− docsFor Proxy (endpoint, action) =
70− single endpoint' action'
71−
72− where endpoint' = endpoint & method .~ DocPOST
73−
74− action' = action & response.respBody .~ sampleByteString p
75− & response.respStatus .~ 201
76−
77− p = Proxy :: Proxy a
src/Servant/API/Put.hs−24
@@ -11,9 +11,6 @@ import Data.String.Conversions
1111import Data.Typeable
1212import Network.HTTP.Types
1313import Network.Wai
14−import Servant.Client
15−import Servant.Common.Req
16−import Servant.Docs
1714import Servant.Server
1815
1916-- | Endpoint for PUT requests, usually used to update a ressource.
@@ -53,24 +50,3 @@ instance ToJSON a => HasServer (Put a) where
5350 respond $ failWith WrongMethod
5451
5552 | otherwise = respond $ failWith NotFound
56−
57−-- | If you have a 'Put' endpoint in your API, the client
58−-- side querying function that is created when calling 'client'
59−-- will just require an argument that specifies the scheme, host
60−-- and port to send the request to.
61−instance FromJSON a => HasClient (Put a) where
62− type Client (Put a) = BaseUrl -> EitherT String IO a
63−
64− clientWithRoute Proxy req host =
65− performRequestJSON methodPut req 200 host
66−
67−instance ToSample a => HasDocs (Put a) where
68− docsFor Proxy (endpoint, action) =
69− single endpoint' action'
70−
71− where endpoint' = endpoint & method .~ DocPUT
72−
73− action' = action & response.respBody .~ sampleByteString p
74− & response.respStatus .~ 200
75−
76− p = Proxy :: Proxy a
src/Servant/API/QueryParam.hs−150
@@ -7,7 +7,6 @@
77{-# LANGUAGE ScopedTypeVariables #-}
88module Servant.API.QueryParam where
99
10−import Data.List
1110import Data.Maybe
1211import Data.Proxy
1312import Data.String.Conversions
@@ -15,10 +14,7 @@ import GHC.TypeLits
1514import Network.HTTP.Types
1615import Network.Wai
1716import Servant.API.Sub
18−import Servant.Client
19−import Servant.Common.Req
2017import Servant.Common.Text
21−import Servant.Docs
2218import Servant.Server
2319
2420-- | Lookup the value associated to the @sym@ query string parameter
@@ -70,56 +66,6 @@ instance (KnownSymbol sym, FromText a, HasServer sublayout)
7066
7167 where paramname = cs $ symbolVal (Proxy :: Proxy sym)
7268
73−-- | If you use a 'QueryParam' in one of your endpoints in your API,
74−-- the corresponding querying function will automatically take
75−-- an additional argument of the type specified by your 'QueryParam',
76−-- enclosed in Maybe.
77−--
78−-- If you give Nothing, nothing will be added to the query string.
79−--
80−-- If you give a non-'Nothing' value, this function will take care
81−-- of inserting a textual representation of this value in the query string.
82−--
83−-- You can control how values for your type are turned into
84−-- text by specifying a 'ToText' instance for your type.
85−--
86−-- Example:
87−--
88−-- > type MyApi = "books" :> QueryParam "author" Text :> Get [Book]
89−-- >
90−-- > myApi :: Proxy MyApi
91−-- > myApi = Proxy
92−-- >
93−-- > getBooksBy :: Maybe Text -> BaseUrl -> EitherT String IO [Book]
94−-- > getBooksBy = client myApi
95−-- > -- then you can just use "getBooksBy" to query that endpoint.
96−-- > -- 'getBooksBy Nothing' for all books
97−-- > -- 'getBooksBy (Just "Isaac Asimov")' to get all books by Isaac Asimov
98−instance (KnownSymbol sym, ToText a, HasClient sublayout)
99− => HasClient (QueryParam sym a :> sublayout) where
100−
101− type Client (QueryParam sym a :> sublayout) =
102− Maybe a -> Client sublayout
103−
104− -- if mparam = Nothing, we don't add it to the query string
105− clientWithRoute Proxy req mparam =
106− clientWithRoute (Proxy :: Proxy sublayout) $
107− appendToQueryString pname mparamText req
108−
109− where pname = cs pname'
110− pname' = symbolVal (Proxy :: Proxy sym)
111− mparamText = fmap toText mparam
112−
113−instance (KnownSymbol sym, ToParam (QueryParam sym a), HasDocs sublayout)
114− => HasDocs (QueryParam sym a :> sublayout) where
115−
116− docsFor Proxy (endpoint, action) =
117− docsFor sublayoutP (endpoint, action')
118−
119− where sublayoutP = Proxy :: Proxy sublayout
120− paramP = Proxy :: Proxy (QueryParam sym a)
121− action' = over params (|> toParam paramP) action
122−
12369-- | Lookup the values associated to the @sym@ query string parameter
12470-- and try to extract it as a value of type @[a]@. This is typically
12571-- meant to support query string parameters of the form
@@ -173,57 +119,6 @@ instance (KnownSymbol sym, FromText a, HasServer sublayout)
173119 convert Nothing = Nothing
174120 convert (Just v) = fromText v
175121
176−-- | If you use a 'QueryParams' in one of your endpoints in your API,
177−-- the corresponding querying function will automatically take
178−-- an additional argument, a list of values of the type specified
179−-- by your 'QueryParams'.
180−--
181−-- If you give an empty list, nothing will be added to the query string.
182−--
183−-- Otherwise, this function will take care
184−-- of inserting a textual representation of your values in the query string,
185−-- under the same query string parameter name.
186−--
187−-- You can control how values for your type are turned into
188−-- text by specifying a 'ToText' instance for your type.
189−--
190−-- Example:
191−--
192−-- > type MyApi = "books" :> QueryParams "authors" Text :> Get [Book]
193−-- >
194−-- > myApi :: Proxy MyApi
195−-- > myApi = Proxy
196−-- >
197−-- > getBooksBy :: [Text] -> BaseUrl -> EitherT String IO [Book]
198−-- > getBooksBy = client myApi
199−-- > -- then you can just use "getBooksBy" to query that endpoint.
200−-- > -- 'getBooksBy []' for all books
201−-- > -- 'getBooksBy ["Isaac Asimov", "Robert A. Heinlein"]'
202−-- > -- to get all books by Asimov and Heinlein
203−instance (KnownSymbol sym, ToText a, HasClient sublayout)
204− => HasClient (QueryParams sym a :> sublayout) where
205−
206− type Client (QueryParams sym a :> sublayout) =
207− [a] -> Client sublayout
208−
209− clientWithRoute Proxy req paramlist =
210− clientWithRoute (Proxy :: Proxy sublayout) $
211− foldl' (\ value req' -> appendToQueryString pname req' value) req paramlist'
212−
213− where pname = cs pname'
214− pname' = symbolVal (Proxy :: Proxy sym)
215− paramlist' = map (Just . toText) paramlist
216−
217−instance (KnownSymbol sym, ToParam (QueryParams sym a), HasDocs sublayout)
218− => HasDocs (QueryParams sym a :> sublayout) where
219−
220− docsFor Proxy (endpoint, action) =
221− docsFor sublayoutP (endpoint, action')
222−
223− where sublayoutP = Proxy :: Proxy sublayout
224− paramP = Proxy :: Proxy (QueryParams sym a)
225− action' = over params (|> toParam paramP) action
226−
227122-- | Lookup a potentially value-less query string parameter
228123-- with boolean semantics. If the param @sym@ is there without any value,
229124-- or if it's there with value "true" or "1", it's interpreted as 'True'.
@@ -265,48 +160,3 @@ instance (KnownSymbol sym, HasServer sublayout)
265160 where paramname = cs $ symbolVal (Proxy :: Proxy sym)
266161 examine v | v == "true" || v == "1" || v == "" = True
267162 | otherwise = False
268−
269−-- | If you use a 'QueryFlag' in one of your endpoints in your API,
270−-- the corresponding querying function will automatically take
271−-- an additional 'Bool' argument.
272−--
273−-- If you give 'False', nothing will be added to the query string.
274−--
275−-- Otherwise, this function will insert a value-less query string
276−-- parameter under the name associated to your 'QueryFlag'.
277−--
278−-- Example:
279−--
280−-- > type MyApi = "books" :> QueryFlag "published" :> Get [Book]
281−-- >
282−-- > myApi :: Proxy MyApi
283−-- > myApi = Proxy
284−-- >
285−-- > getBooks :: Bool -> BaseUrl -> EitherT String IO [Book]
286−-- > getBooks = client myApi
287−-- > -- then you can just use "getBooks" to query that endpoint.
288−-- > -- 'getBooksBy False' for all books
289−-- > -- 'getBooksBy True' to only get _already published_ books
290−instance (KnownSymbol sym, HasClient sublayout)
291− => HasClient (QueryFlag sym :> sublayout) where
292−
293− type Client (QueryFlag sym :> sublayout) =
294− Bool -> Client sublayout
295−
296− clientWithRoute Proxy req flag =
297− clientWithRoute (Proxy :: Proxy sublayout) $
298− if flag
299− then appendToQueryString paramname Nothing req
300− else req
301−
302− where paramname = cs $ symbolVal (Proxy :: Proxy sym)
303−
304−instance (KnownSymbol sym, ToParam (QueryFlag sym), HasDocs sublayout)
305− => HasDocs (QueryFlag sym :> sublayout) where
306−
307− docsFor Proxy (endpoint, action) =
308− docsFor sublayoutP (endpoint, action')
309−
310− where sublayoutP = Proxy :: Proxy sublayout
311− paramP = Proxy :: Proxy (QueryFlag sym)
312− action' = over params (|> toParam paramP) action
src/Servant/API/Raw.hs−20
@@ -3,14 +3,8 @@
33{-# LANGUAGE TypeFamilies #-}
44module Servant.API.Raw where
55
6−import Control.Monad.Trans.Either
7−import Data.ByteString.Lazy
86import Data.Proxy
9−import Network.HTTP.Types
107import Network.Wai
11−import Servant.Client
12−import Servant.Common.Req
13−import Servant.Docs hiding (Method)
148import Servant.Server
159
1610-- | Endpoint for plugging in your own Wai 'Application's.
@@ -36,17 +30,3 @@ instance HasServer Raw where
3630 type Server Raw = Application
3731 route Proxy rawApplication request respond =
3832 rawApplication request (respond . succeedWith)
39−
40−-- | Pick a 'Method' and specify where the server you want to query is. You get
41−-- back the status code and the response body as a 'ByteString'.
42−instance HasClient Raw where
43− type Client Raw = Method -> BaseUrl -> EitherT String IO (Int, ByteString)
44−
45− clientWithRoute :: Proxy Raw -> Req -> Client Raw
46− clientWithRoute Proxy req httpMethod host =
47− performRequest httpMethod req (const True) host
48−
49−
50−instance HasDocs Raw where
51− docsFor _proxy (endpoint, action) =
52− single endpoint action
src/Servant/API/ReqBody.hs−42
@@ -10,9 +10,6 @@ import Data.Aeson
1010import Data.Proxy
1111import Network.Wai
1212import Servant.API.Sub
13−import Servant.Client
14−import Servant.Common.Req
15−import Servant.Docs
1613import Servant.Server
1714
1815-- | Extract the request body as a value of type @a@.
@@ -50,42 +47,3 @@ instance (FromJSON a, HasServer sublayout)
5047 case mrqbody of
5148 Nothing -> respond $ failWith InvalidBody
5249 Just v -> route (Proxy :: Proxy sublayout) (subserver v) request respond
53−
54−-- | If you use a 'ReqBody' in one of your endpoints in your API,
55−-- the corresponding querying function will automatically take
56−-- an additional argument of the type specified by your 'ReqBody'.
57−-- That function will take care of encoding this argument as JSON and
58−-- of using it as the request body.
59−--
60−-- All you need is for your type to have a 'ToJSON' instance.
61−--
62−-- Example:
63−--
64−-- > type MyApi = "books" :> ReqBody Book :> Post Book
65−-- >
66−-- > myApi :: Proxy MyApi
67−-- > myApi = Proxy
68−-- >
69−-- > addBook :: Book -> BaseUrl -> EitherT String IO Book
70−-- > addBook = client myApi
71−-- > -- then you can just use "addBook" to query that endpoint
72−instance (ToJSON a, HasClient sublayout)
73− => HasClient (ReqBody a :> sublayout) where
74−
75− type Client (ReqBody a :> sublayout) =
76− a -> Client sublayout
77−
78− clientWithRoute Proxy req body =
79− clientWithRoute (Proxy :: Proxy sublayout) $
80− setRQBody (encode body) req
81−
82−instance (ToSample a, HasDocs sublayout)
83− => HasDocs (ReqBody a :> sublayout) where
84−
85− docsFor Proxy (endpoint, action) =
86− docsFor sublayoutP (endpoint, action')
87−
88− where sublayoutP = Proxy :: Proxy sublayout
89−
90− action' = action & rqbody .~ sampleByteString p
91− p = Proxy :: Proxy a
src/Servant/API/Sub.hs−22
@@ -8,9 +8,6 @@ import Data.Proxy
88import Data.String.Conversions
99import GHC.TypeLits
1010import Network.Wai
11−import Servant.Client
12−import Servant.Common.Req
13−import Servant.Docs
1411import Servant.Server
1512
1613-- | The contained API (second argument) can be found under @("/" ++ path)@
@@ -37,22 +34,3 @@ instance (KnownSymbol path, HasServer sublayout) => HasServer (path :> sublayout
3734 _ -> respond $ failWith NotFound
3835
3936 where proxyPath = Proxy :: Proxy path
40−
41−-- | Make the querying function append @path@ to the request path.
42−instance (KnownSymbol path, HasClient sublayout) => HasClient (path :> sublayout) where
43− type Client (path :> sublayout) = Client sublayout
44−
45− clientWithRoute Proxy req =
46− clientWithRoute (Proxy :: Proxy sublayout) $
47− appendToPath p req
48−
49− where p = symbolVal (Proxy :: Proxy path)
50−
51−instance (KnownSymbol path, HasDocs sublayout) => HasDocs (path :> sublayout) where
52−
53− docsFor Proxy (endpoint, action) =
54− docsFor sublayoutP (endpoint', action)
55−
56− where sublayoutP = Proxy :: Proxy sublayout
57− endpoint' = endpoint & path <>~ symbolVal pa
58− pa = Proxy :: Proxy path
src/Servant/Client.hsdeleted−39
@@ -1,39 +0,0 @@
1−{-# LANGUAGE TypeFamilies #-}
2−{-# LANGUAGE OverloadedStrings #-}
3−-- | This module provides 'client' which can automatically generate
4−-- querying functions for each endpoint just from the type representing your
5−-- API.
6−module Servant.Client
7− ( client
8− , HasClient(..)
9− , module Servant.Common.BaseUrl
10− ) where
11−
12−import Data.Proxy
13−
14−import Servant.Common.BaseUrl
15−import Servant.Common.Req
16−
17−-- * Accessing APIs as a Client
18−
19−-- | 'client' allows you to produce operations to query an API from a client.
20−--
21−-- > type MyApi = "books" :> Get [Book] -- GET /books
22−-- > :<|> "books" :> ReqBody Book :> Post Book -- POST /books
23−-- >
24−-- > myApi :: Proxy MyApi
25−-- > myApi = Proxy
26−-- >
27−-- > getAllBooks :: BaseUrl -> EitherT String IO [Book]
28−-- > postNewBook :: Book -> BaseUrl -> EitherT String IO Book
29−-- > (getAllBooks :<|> postNewBook) = client myApi
30−client :: HasClient layout => Proxy layout -> Client layout
31−client p = clientWithRoute p defReq
32−
33−-- | This class lets us define how each API combinator
34−-- influences the creation of an HTTP request. Use 'client'
35−-- directly, this class implements the client-side
36−-- behavior of each combinator but you don't have to worry about it.
37−class HasClient layout where
38− type Client layout :: *
39− clientWithRoute :: Proxy layout -> Req -> Client layout
src/Servant/Common/BaseUrl.hsdeleted−55
@@ -1,55 +0,0 @@
1−{-# LANGUAGE DeriveGeneric #-}
2−{-# LANGUAGE ViewPatterns #-}
3−module Servant.Common.BaseUrl where
4−
5−import Data.List
6−import GHC.Generics
7−import Network.URI
8−import Safe
9−import Text.Read
10−
11−-- | URI scheme to use
12−data Scheme =
13− Http -- ^ http://
14− | Https -- ^ https://
15− deriving (Show, Eq, Ord, Generic)
16−
17−-- | Simple data type to represent the target of HTTP requests
18−-- for servant's automatically-generated clients.
19−data BaseUrl = BaseUrl
20− { baseUrlScheme :: Scheme -- ^ URI scheme to use
21− , baseUrlHost :: String -- ^ host (eg "haskell.org")
22− , baseUrlPort :: Int -- ^ port (eg 80)
23− } deriving (Show, Eq, Ord, Generic)
24−
25−showBaseUrl :: BaseUrl -> String
26−showBaseUrl (BaseUrl urlscheme host port) =
27− schemeString ++ "//" ++ host ++ portString
28− where
29− schemeString = case urlscheme of
30− Http -> "http:"
31− Https -> "https:"
32− portString = case (urlscheme, port) of
33− (Http, 80) -> ""
34− (Https, 443) -> ""
35− _ -> ":" ++ show port
36−
37−parseBaseUrl :: String -> Either String BaseUrl
38−parseBaseUrl s = case parseURI (removeTrailingSlash s) of
39− -- This is a rather hacky implementation and should be replaced with something
40− -- implemented in attoparsec (which is already a dependency anyhow (via aeson)).
41− Just (URI "http:" (Just (URIAuth "" host (':' : (readMaybe -> Just port)))) "" "" "") ->
42− Right (BaseUrl Http host port)
43− Just (URI "http:" (Just (URIAuth "" host "")) "" "" "") ->
44− Right (BaseUrl Http host 80)
45− Just (URI "https:" (Just (URIAuth "" host (':' : (readMaybe -> Just port)))) "" "" "") ->
46− Right (BaseUrl Https host port)
47− Just (URI "https:" (Just (URIAuth "" host "")) "" "" "") ->
48− Right (BaseUrl Https host 443)
49− _ -> if "://" `isInfixOf` s
50− then Left ("invalid base url: " ++ s)
51− else parseBaseUrl ("http://" ++ s)
52− where
53− removeTrailingSlash str = case lastMay str of
54− Just '/' -> init str
55− _ -> str
src/Servant/Common/Req.hsdeleted−132
@@ -1,132 +0,0 @@
1−{-# LANGUAGE OverloadedStrings #-}
2−{-# LANGUAGE ScopedTypeVariables #-}
3−module Servant.Common.Req where
4−
5−import Control.Applicative
6−import Control.Concurrent
7−import Control.Exception
8−import Control.Monad
9−import Control.Monad.Catch (MonadThrow)
10−import Control.Monad.IO.Class
11−import Control.Monad.Trans.Either
12−import Data.Aeson
13−import Data.Aeson.Parser
14−import Data.Aeson.Types
15−import Data.Attoparsec.ByteString
16−import Data.ByteString.Lazy
17−import Data.String.Conversions
18−import Data.Text
19−import Network.HTTP.Client
20−import Network.HTTP.Types
21−import Network.URI
22−import Servant.Common.BaseUrl
23−import System.IO.Unsafe
24−
25−import qualified Network.HTTP.Client as Client
26−
27−data Req = Req
28− { reqPath :: String
29− , qs :: QueryText
30− , reqBody :: ByteString
31− }
32−
33−defReq :: Req
34−defReq = Req "" [] ""
35−
36−appendToPath :: String -> Req -> Req
37−appendToPath p req =
38− req { reqPath = reqPath req ++ "/" ++ p }
39−
40−appendToQueryString :: Text -- ^ param name
41− -> Maybe Text -- ^ param value
42− -> Req
43− -> Req
44−appendToQueryString pname pvalue req =
45− req { qs = qs req ++ [(pname, pvalue)]
46− }
47−
48−setRQBody :: ByteString -> Req -> Req
49−setRQBody b req = req { reqBody = b }
50−
51−reqToRequest :: (Functor m, MonadThrow m) => Req -> BaseUrl -> m Request
52−reqToRequest req (BaseUrl reqScheme reqHost reqPort) = fmap (setrqb . setQS ) $ parseUrl url
53−
54− where url = show $ nullURI { uriScheme = case reqScheme of
55− Http -> "http:"
56− Https -> "https:"
57− , uriAuthority = Just $
58− URIAuth { uriUserInfo = ""
59− , uriRegName = reqHost
60− , uriPort = ":" ++ show reqPort
61− }
62− , uriPath = reqPath req
63− }
64−
65− setrqb r = r { requestBody = RequestBodyLBS (reqBody req) }
66− setQS = setQueryString $ queryTextToQuery (qs req)
67−
68−
69−-- * performing requests
70−
71−{-# NOINLINE __manager #-}
72−__manager :: MVar Manager
73−__manager = unsafePerformIO (newManager defaultManagerSettings >>= newMVar)
74−
75−__withGlobalManager :: (Manager -> IO a) -> IO a
76−__withGlobalManager action = modifyMVar __manager $ \ manager -> do
77− result <- action manager
78− return (manager, result)
79−
80−
81−displayHttpRequest :: Method -> String
82−displayHttpRequest httpmethod = "HTTP " ++ cs httpmethod ++ " request"
83−
84−
85−performRequest :: Method -> Req -> (Int -> Bool) -> BaseUrl -> EitherT String IO (Int, ByteString)
86−performRequest reqMethod req isWantedStatus reqHost = do
87− partialRequest <- liftIO $ reqToRequest req reqHost
88−
89− let request = partialRequest { Client.method = reqMethod
90− , checkStatus = \ _status _headers _cookies -> Nothing
91− }
92−
93− eResponse <- liftIO $ __withGlobalManager $ \ manager ->
94− catchStatusCodeException $
95− Client.httpLbs request manager
96− case eResponse of
97− Left status ->
98− left (displayHttpRequest reqMethod ++ " failed with status: " ++ showStatus status)
99−
100− Right response -> do
101− let status = Client.responseStatus response
102− unless (isWantedStatus (statusCode status)) $
103− left (displayHttpRequest reqMethod ++ " failed with status: " ++ showStatus status)
104− return $ (statusCode status, Client.responseBody response)
105− where
106− showStatus (Status code message) =
107− show code ++ " - " ++ cs message
108−
109−
110−performRequestJSON :: FromJSON result =>
111− Method -> Req -> Int -> BaseUrl -> EitherT String IO result
112−performRequestJSON reqMethod req wantedStatus reqHost = do
113− (_status, respBody) <- performRequest reqMethod req (== wantedStatus) reqHost
114− either
115− (\ message -> left (displayHttpRequest reqMethod ++ " returned invalid json: " ++ message))
116− return
117− (decodeLenient respBody)
118−
119−
120−catchStatusCodeException :: IO a -> IO (Either Status a)
121−catchStatusCodeException action =
122− catch (Right <$> action) $ \e ->
123− case e of
124− Client.StatusCodeException status _ _ -> return $ Left status
125− exc -> throwIO exc
126−
127−-- | Like 'Data.Aeson.decode' but allows all JSON values instead of just
128−-- objects and arrays.
129−decodeLenient :: FromJSON a => ByteString -> Either String a
130−decodeLenient input = do
131− v :: Value <- parseOnly (Data.Aeson.Parser.value <* endOfInput) (cs input)
132− parseEither parseJSON v
src/Servant/Docs.hsdeleted−426
@@ -1,426 +0,0 @@
1−{-# LANGUAGE TypeFamilies #-}
2−{-# LANGUAGE DeriveGeneric #-}
3−{-# LANGUAGE ScopedTypeVariables #-}
4−{-# LANGUAGE TemplateHaskell #-}
5−
6−-------------------------------------------------------------------------------
7−-- | This module lets you get API docs for free. It lets generate
8−-- an 'API' from the type that represents your API using 'docs':
9−--
10−-- @docs :: 'HasDocs' api => 'Proxy' api -> 'API'@
11−--
12−-- You can then call 'markdown' on it:
13−--
14−-- @printMarkdown :: 'API' -> String@
15−--
16−-- or define a custom pretty printer:
17−--
18−-- @yourPrettyDocs :: 'API' -> String -- or blaze-html's HTML, or ...@
19−--
20−-- The only thing you'll need to do will be to implement some classes
21−-- for your captures, get parameters and request or response bodies.
22−--
23−-- Here's a little (but complete) example that you can run to see the
24−-- markdown pretty printer in action:
25−--
26−-- > {-# LANGUAGE DataKinds #-}
27−-- > {-# LANGUAGE PolyKinds #-}
28−-- > {-# LANGUAGE TypeFamilies #-}
29−-- > {-# LANGUAGE DeriveGeneric #-}
30−-- > {-# LANGUAGE TypeOperators #-}
31−-- > {-# LANGUAGE FlexibleInstances #-}
32−-- > {-# LANGUAGE OverloadedStrings #-}
33−-- >
34−-- > import Data.Proxy
35−-- > import Data.Text
36−-- > import Servant
37−-- >
38−-- > -- our type for a Greeting message
39−-- > data Greet = Greet { _msg :: Text }
40−-- > deriving (Generic, Show)
41−-- >
42−-- > -- we get our JSON serialization for free
43−-- > instance FromJSON Greet
44−-- > instance ToJSON Greet
45−-- >
46−-- > -- we provide a sample value for the 'Greet' type
47−-- > instance ToSample Greet where
48−-- > toSample = Just g
49−-- >
50−-- > where g = Greet "Hello, haskeller!"
51−-- >
52−-- > instance ToParam (QueryParam "capital" Bool) where
53−-- > toParam _ =
54−-- > DocQueryParam "capital"
55−-- > ["true", "false"]
56−-- > "Get the greeting message in uppercase (true) or not (false). Default is false."
57−-- >
58−-- > instance ToCapture (Capture "name" Text) where
59−-- > toCapture _ = DocCapture "name" "name of the person to greet"
60−-- >
61−-- > instance ToCapture (Capture "greetid" Text) where
62−-- > toCapture _ = DocCapture "greetid" "identifier of the greet msg to remove"
63−-- >
64−-- > -- API specification
65−-- > type TestApi =
66−-- > "hello" :> Capture "name" Text :> QueryParam "capital" Bool :> Get Greet
67−-- > :<|> "greet" :> RQBody Greet :> Post Greet
68−-- > :<|> "delete" :> Capture "greetid" Text :> Delete
69−-- >
70−-- > testApi :: Proxy TestApi
71−-- > testApi = Proxy
72−-- >
73−-- > -- Generate the Documentation's ADT
74−-- > greetDocs :: API
75−-- > greetDocs = docs testApi
76−-- >
77−-- > main :: IO ()
78−-- > main = putStrLn $ markdown greetDocs
79−module Servant.Docs
80− ( -- * 'HasDocs' class and key functions
81− HasDocs(..), docs, markdown
82−
83− , -- * Classes you need to implement for your types
84− ToSample(..)
85− , sampleByteString
86− , ToParam(..)
87− , ToCapture(..)
88−
89− , -- * ADTs to represent an 'API'
90− Method(..)
91− , Endpoint, path, method, defEndpoint
92− , API, emptyAPI
93− , DocCapture(..), capSymbol, capDesc
94− , DocQueryParam(..), ParamKind(..), paramName, paramValues, paramDesc, paramKind
95− , Response, respStatus, respBody, defResponse
96− , Action, captures, params, rqbody, response, defAction
97− , single
98−
99− , -- * Useful modules when defining your doc printers
100− module Control.Lens
101− , module Data.Monoid
102− ) where
103−
104−import Control.Lens hiding (Action)
105−import Data.Aeson
106−import Data.ByteString.Lazy.Char8 (ByteString)
107−import Data.Hashable
108−import Data.HashMap.Strict (HashMap)
109−import Data.List
110−import Data.Monoid
111−import Data.Proxy
112−import Data.String.Conversions
113−import GHC.Generics
114−
115−import qualified Data.HashMap.Strict as HM
116−
117−-- | Supported HTTP request methods
118−data Method = DocDELETE -- ^ the DELETE method
119− | DocGET -- ^ the GET method
120− | DocPOST -- ^ the POST method
121− | DocPUT -- ^ the PUT method
122− deriving (Eq, Generic)
123−
124−instance Show Method where
125− show DocGET = "GET"
126− show DocPOST = "POST"
127− show DocDELETE = "DELETE"
128− show DocPUT = "PUT"
129−
130−instance Hashable Method
131−
132−-- | An 'Endpoint' type that holds the 'path' and the 'method'.
133−--
134−-- Gets used as the key in the 'API' hashmap. Modify 'defEndpoint'
135−-- or any 'Endpoint' value you want using the 'path' and 'method'
136−-- lenses to tweak.
137−--
138−-- @
139−-- λ> 'defEndpoint'
140−-- GET /
141−-- λ> 'defEndpoint' & 'path' '<>~' "foo"
142−-- GET /foo
143−-- λ> 'defEndpoint' & 'path' '<>~' "foo" & 'method' '.~' 'DocPOST'
144−-- POST /foo
145−-- @
146−data Endpoint = Endpoint
147− { _path :: String -- type collected
148− , _method :: Method -- type collected
149− } deriving (Eq, Generic)
150−
151−instance Show Endpoint where
152− show (Endpoint p m) =
153− show m ++ " " ++ p
154−
155−-- | An 'Endpoint' whose path is `"/"` and whose method is 'DocGET'
156−--
157−-- Here's how you can modify it:
158−--
159−-- @
160−-- λ> 'defEndpoint'
161−-- GET /
162−-- λ> 'defEndpoint' & 'path' '<>~' "foo"
163−-- GET /foo
164−-- λ> 'defEndpoint' & 'path' '<>~' "foo" & 'method' '.~' 'DocPOST'
165−-- POST /foo
166−-- @
167−defEndpoint :: Endpoint
168−defEndpoint = Endpoint "/" DocGET
169−
170−instance Hashable Endpoint
171−
172−-- | Our API type, a good old hashmap from 'Endpoint' to 'Action'
173−type API = HashMap Endpoint Action
174−
175−-- | An empty 'API'
176−emptyAPI :: API
177−emptyAPI = HM.empty
178−
179−-- | A type to represent captures. Holds the name of the capture
180−-- and a description.
181−--
182−-- Write a 'ToCapture' instance for your captured types.
183−data DocCapture = DocCapture
184− { _capSymbol :: String -- type supplied
185− , _capDesc :: String -- user supplied
186− } deriving (Eq, Show)
187−
188−-- | A type to represent a /GET/ parameter from the Query String. Holds its name,
189−-- the possible values (leave empty if there isn't a finite number of them),
190−-- and a description of how it influences the output or behavior.
191−--
192−-- Write a 'ToParam' instance for your GET parameter types
193−data DocQueryParam = DocQueryParam
194− { _paramName :: String -- type supplied
195− , _paramValues :: [String] -- user supplied
196− , _paramDesc :: String -- user supplied
197− , _paramKind :: ParamKind
198− } deriving (Eq, Show)
199−
200−-- | Type of GET parameter:
201−--
202−-- - Normal corresponds to @QueryParam@, i.e your usual GET parameter
203−-- - List corresponds to @QueryParams@, i.e GET parameters with multiple values
204−-- - Flag corresponds to @QueryFlag@, i.e a value-less GET parameter
205−data ParamKind = Normal | List | Flag
206− deriving (Eq, Show)
207−
208−-- | A type to represent an HTTP response. Has an 'Int' status and
209−-- a 'Maybe ByteString' response body. Tweak 'defResponse' using
210−-- the 'respStatus' and 'respBody' lenses if you want.
211−--
212−-- If you want to respond with a non-empty response body, you'll most likely
213−-- want to write a 'ToSample' instance for the type that'll be represented
214−-- as some JSON in the response.
215−--
216−-- Can be tweaked with two lenses.
217−--
218−-- > λ> defResponse
219−-- > Response {_respStatus = 200, _respBody = Nothing}
220−-- > λ> defResponse & respStatus .~ 204 & respBody .~ Just "[]"
221−-- > Response {_respStatus = 204, _respBody = Just "[]"}
222−data Response = Response
223− { _respStatus :: Int
224− , _respBody :: Maybe ByteString
225− } deriving (Eq, Show)
226−
227−-- | Default response: status code 200, no response body.
228−--
229−-- Can be tweaked with two lenses.
230−--
231−-- > λ> defResponse
232−-- > Response {_respStatus = 200, _respBody = Nothing}
233−-- > λ> defResponse & respStatus .~ 204 & respBody .~ Just "[]"
234−-- > Response {_respStatus = 204, _respBody = Just "[]"}
235−defResponse :: Response
236−defResponse = Response 200 Nothing
237−
238−-- | A datatype that represents everything that can happen
239−-- at an endpoint, with its lenses:
240−--
241−-- - List of captures ('captures')
242−-- - List of GET parameters ('params')
243−-- - What the request body should look like, if any is requested ('rqbody')
244−-- - What the response should be if everything goes well ('response')
245−--
246−-- You can tweak an 'Action' (like the default 'defAction') with these lenses
247−-- to transform an action and add some information to it.
248−data Action = Action
249− { _captures :: [DocCapture] -- type collected + user supplied info
250− , _params :: [DocQueryParam] -- type collected + user supplied info
251− , _rqbody :: Maybe ByteString -- user supplied
252− , _response :: Response -- user supplied
253− } deriving (Eq, Show)
254−
255−-- Default 'Action'. Has no 'captures', no GET 'params', expects
256−-- no request body ('rqbody') and the typical response is 'defResponse'.
257−--
258−-- Tweakable with lenses.
259−--
260−-- > λ> defAction
261−-- > Action {_captures = [], _params = [], _rqbody = Nothing, _response = Response {_respStatus = 200, _respBody = Nothing}}
262−-- > λ> defAction & response.respStatus .~ 201
263−-- > Action {_captures = [], _params = [], _rqbody = Nothing, _response = Response {_respStatus = 201, _respBody = Nothing}}
264−defAction :: Action
265−defAction =
266− Action []
267− []
268− Nothing
269− defResponse
270−
271−-- | Create an API that's comprised of a single endpoint.
272−-- 'API' is a 'Monoid', so combine multiple endpoints with
273−-- 'mappend' or '<>'.
274−single :: Endpoint -> Action -> API
275−single = HM.singleton
276−
277−-- gimme some lenses
278−makeLenses ''Endpoint
279−makeLenses ''DocCapture
280−makeLenses ''DocQueryParam
281−makeLenses ''Response
282−makeLenses ''Action
283−
284−-- | Generate the docs for a given API that implements 'HasDocs'.
285−docs :: HasDocs layout => Proxy layout -> API
286−docs p = docsFor p (defEndpoint, defAction)
287−
288−-- | The class that abstracts away the impact of API combinators
289−-- on documentation generation.
290−class HasDocs layout where
291− docsFor :: Proxy layout -> (Endpoint, Action) -> API
292−
293−-- | The class that lets us display a sample JSON input or output
294−-- when generating documentation for endpoints that either:
295−--
296−-- - expect a request body, or
297−-- - return a non empty response body
298−--
299−-- Example of an instance:
300−--
301−-- > {-# LANGUAGE DeriveGeneric #-}
302−-- > {-# LANGUAGE OverloadedStrings #-}
303−-- >
304−-- > import Data.Aeson
305−-- > import Data.Text
306−-- > import GHC.Generics
307−-- >
308−-- > data Greet = Greet { _msg :: Text }
309−-- > deriving (Generic, Show)
310−-- >
311−-- > instance FromJSON Greet
312−-- > instance ToJSON Greet
313−-- >
314−-- > instance ToSample Greet where
315−-- > toSample = Just g
316−-- >
317−-- > where g = Greet "Hello, haskeller!"
318−class ToJSON a => ToSample a where
319− toSample :: Maybe a
320−
321−instance ToSample () where
322− toSample = Just ()
323−
324−sampleByteString :: forall a . ToSample a => Proxy a -> Maybe ByteString
325−sampleByteString Proxy = fmap encode (toSample :: Maybe a)
326−
327−-- | The class that helps us automatically get documentation
328−-- for GET parameters.
329−--
330−-- Example of an instance:
331−--
332−-- > instance ToParam (QueryParam "capital" Bool) where
333−-- > toParam _ =
334−-- > DocQueryParam "capital"
335−-- > ["true", "false"]
336−-- > "Get the greeting message in uppercase (true) or not (false). Default is false."
337−class ToParam t where
338− toParam :: Proxy t -> DocQueryParam
339−
340−-- | The class that helps us automatically get documentation
341−-- for URL captures.
342−--
343−-- Example of an instance:
344−--
345−-- > instance ToCapture (Capture "name" Text) where
346−-- > toCapture _ = DocCapture "name" "name of the person to greet"
347−class ToCapture c where
348− toCapture :: Proxy c -> DocCapture
349−
350−-- | Generate documentation in Markdown format for
351−-- the given 'API'.
352−markdown :: API -> String
353−markdown = unlines . concat . map (uncurry printEndpoint) . HM.toList
354−
355− where printEndpoint :: Endpoint -> Action -> [String]
356− printEndpoint endpoint action =
357− str :
358− replicate len '-' :
359− "" :
360− capturesStr (action ^. captures) ++
361− paramsStr (action ^. params) ++
362− rqbodyStr (action ^. rqbody) ++
363− responseStr (action ^. response) ++
364− []
365−
366− where str = show (endpoint^.method) ++ " " ++ endpoint^.path
367− len = length str
368−
369− capturesStr :: [DocCapture] -> [String]
370− capturesStr [] = []
371− capturesStr l =
372− "**Captures**: " :
373− "" :
374− map captureStr l ++
375− "" :
376− []
377− captureStr cap =
378− "- *" ++ (cap ^. capSymbol) ++ "*: " ++ (cap ^. capDesc)
379−
380− paramsStr :: [DocQueryParam] -> [String]
381− paramsStr [] = []
382− paramsStr l =
383− "**GET Parameters**: " :
384− "" :
385− map paramStr l ++
386− "" :
387− []
388− paramStr param = unlines $
389− (" - " ++ param ^. paramName) :
390− (if (not (null values) || param ^. paramKind /= Flag)
391− then [" - **Values**: *" ++ intercalate ", " values ++ "*"]
392− else []) ++
393− (" - **Description**: " ++ param ^. paramDesc) :
394− (if (param ^. paramKind == List)
395− then [" - This parameter is a **list**. All GET parameters with the name "
396− ++ param ^. paramName ++ "[] will forward their values in a list to the handler."]
397− else []) ++
398− (if (param ^. paramKind == Flag)
399− then [" - This parameter is a **flag**. This means no value is expected to be associated to this parameter."]
400− else []) ++
401− []
402−
403− where values = param ^. paramValues
404−
405− rqbodyStr :: Maybe ByteString -> [String]
406− rqbodyStr Nothing = []
407− rqbodyStr (Just b) =
408− "**Request Body**: " :
409− jsonStr b
410−
411− jsonStr b =
412− "" :
413− "``` javascript" :
414− cs b :
415− "```" :
416− "" :
417− []
418−
419− responseStr :: Response -> [String]
420− responseStr resp =
421− "**Response**: " :
422− "" :
423− (" - Status code " ++ show (resp ^. respStatus)) :
424− (resp ^. respBody &
425− maybe [" - No response body\n"]
426− (\b -> " - Response body as below." : jsonStr b))
src/Servant/Utils/StaticFiles.hs+1−33
@@ -1,24 +1,17 @@
1−-- | This module defines sever-side handlers that let you serve static files
2−-- and your API's docs.
1+-- | This module defines a sever-side handler that lets you serve static files.
32--
43-- - 'serveDirectory' lets you serve anything that lives under a particular
54-- directory on your filesystem.
6−-- - 'serveDocumentation' lets you serve the markdown-version of the docs for
7−-- your API.
85module Servant.Utils.StaticFiles (
96 serveDirectory,
10− serveDocumentation,
117 ) where
128
13−import Data.Proxy
149import Data.String.Conversions
1510import Filesystem.Path.CurrentOS (decodeString)
16−import Network.HTTP.Types
1711import Network.Wai
1812import Network.Wai.Application.Static
1913
2014import Servant.API.Raw
21−import Servant.Docs
2215import Servant.Server
2316
2417-- | Serve anything under the specified directory as a 'Raw' endpoint.
@@ -44,28 +37,3 @@ import Servant.Server
4437serveDirectory :: FilePath -> Server Raw
4538serveDirectory documentRoot =
4639 staticApp (defaultFileServerSettings (decodeString (documentRoot ++ "/")))
47−
48−-- | Serve your API's docs as markdown embedded in an html \<pre> tag.
49−--
50−-- > type MyApi = "users" :> Get [User]
51−-- > :<|> "docs :> Raw
52−-- >
53−-- > apiProxy :: Proxy MyApi
54−-- > apiProxy = Proxy
55−-- >
56−-- > server :: Server MyApi
57−-- > server = listUsers
58−-- > :<|> serveDocumentation apiProxy
59−serveDocumentation :: HasDocs api => Proxy api -> Server Raw
60−serveDocumentation proxy _request respond =
61− respond $ responseLBS ok200 [] $ cs $ toHtml $ markdown $ docs proxy
62−
63−toHtml :: String -> String
64−toHtml md =
65− "<html>" ++
66− "<body>" ++
67− "<pre>" ++
68− md ++
69− "</pre>" ++
70− "</body>" ++
71− "</html>"
test/Servant/ClientSpec.hsdeleted−203
@@ -1,203 +0,0 @@
1−{-# LANGUAGE DataKinds #-}
2−{-# LANGUAGE GADTs #-}
3−{-# LANGUAGE OverloadedStrings #-}
4−{-# LANGUAGE TypeOperators #-}
5−{-# LANGUAGE ViewPatterns #-}
6−
7−module Servant.ClientSpec where
8−
9−import Control.Concurrent
10−import Control.Exception
11−import Control.Monad.Trans.Either
12−import Data.ByteString.Lazy (ByteString)
13−import Data.Char
14−import Data.Foldable (forM_)
15−import Data.Proxy
16−import Data.Typeable
17−import Network.HTTP.Types
18−import Network.Socket
19−import Network.Wai
20−import Network.Wai.Handler.Warp
21−import Test.Hspec
22−import Test.Hspec.QuickCheck
23−import Test.QuickCheck
24−
25−import Servant.API
26−import Servant.Client
27−import Servant.Server
28−
29−import Servant.ServerSpec
30−
31−type Api =
32− "get" :> Get Person
33− :<|> "capture" :> Capture "name" String :> Get Person
34− :<|> "body" :> ReqBody Person :> Post Person
35− :<|> "param" :> QueryParam "name" String :> Get Person
36− :<|> "params" :> QueryParams "names" String :> Get [Person]
37− :<|> "flag" :> QueryFlag "flag" :> Get Bool
38− :<|> "rawSuccess" :> Raw
39− :<|> "rawFailure" :> Raw
40− :<|> "multiple" :>
41− Capture "first" String :>
42− QueryParam "second" Int :>
43− QueryFlag "third" :>
44− ReqBody [(String, [Rational])] :>
45− Get (String, Maybe Int, Bool, [(String, [Rational])])
46−api :: Proxy Api
47−api = Proxy
48−
49−server :: Application
50−server = serve api (
51− return alice
52− :<|> (\ name -> return $ Person name 0)
53− :<|> return
54− :<|> (\ name -> case name of
55− Just "alice" -> return alice
56− Just name -> left (400, name ++ " not found")
57− Nothing -> left (400, "missing parameter"))
58− :<|> (\ names -> return (zipWith Person names [0..]))
59− :<|> return
60− :<|> (\ _request respond -> respond $ responseLBS ok200 [] "rawSuccess")
61− :<|> (\ _request respond -> respond $ responseLBS badRequest400 [] "rawFailure")
62− :<|> \ a b c d -> return (a, b, c, d)
63− )
64−
65−withServer :: (BaseUrl -> IO a) -> IO a
66−withServer action = withWaiDaemon (return server) action
67−
68−getGet :: BaseUrl -> EitherT String IO Person
69−getCapture :: String -> BaseUrl -> EitherT String IO Person
70−getBody :: Person -> BaseUrl -> EitherT String IO Person
71−getQueryParam :: Maybe String -> BaseUrl -> EitherT String IO Person
72−getQueryParams :: [String] -> BaseUrl -> EitherT String IO [Person]
73−getQueryFlag :: Bool -> BaseUrl -> EitherT String IO Bool
74−getRawSuccess :: Method -> BaseUrl -> EitherT String IO (Int, ByteString)
75−getRawFailure :: Method -> BaseUrl -> EitherT String IO (Int, ByteString)
76−getMultiple :: String -> Maybe Int -> Bool -> [(String, [Rational])]
77− -> BaseUrl
78− -> EitherT String IO (String, Maybe Int, Bool, [(String, [Rational])])
79−( getGet
80− :<|> getCapture
81− :<|> getBody
82− :<|> getQueryParam
83− :<|> getQueryParams
84− :<|> getQueryFlag
85− :<|> getRawSuccess
86− :<|> getRawFailure
87− :<|> getMultiple)
88− = client api
89−
90−spec :: Spec
91−spec = do
92− it "Servant.API.Get" $ withServer $ \ host -> do
93− runEitherT (getGet host) `shouldReturn` Right alice
94−
95− it "Servant.API.Capture" $ withServer $ \ host -> do
96− runEitherT (getCapture "Paula" host) `shouldReturn` Right (Person "Paula" 0)
97−
98− it "Servant.API.ReqBody" $ withServer $ \ host -> do
99− let p = Person "Clara" 42
100− runEitherT (getBody p host) `shouldReturn` Right p
101−
102− it "Servant.API.QueryParam" $ withServer $ \ host -> do
103− runEitherT (getQueryParam (Just "alice") host) `shouldReturn` Right alice
104− Left result <- runEitherT (getQueryParam (Just "bob") host)
105− result `shouldContain` "bob not found"
106−
107− it "Servant.API.QueryParam.QueryParams" $ withServer $ \ host -> do
108− runEitherT (getQueryParams [] host) `shouldReturn` Right []
109− runEitherT (getQueryParams ["alice", "bob"] host)
110− `shouldReturn` Right [Person "alice" 0, Person "bob" 1]
111−
112− context "Servant.API.QueryParam.QueryFlag" $
113− forM_ [False, True] $ \ flag ->
114− it (show flag) $ withServer $ \ host -> do
115− runEitherT (getQueryFlag flag host) `shouldReturn` Right flag
116−
117− it "Servant.API.Raw on success" $ withServer $ \ host -> do
118− runEitherT (getRawSuccess methodGet host) `shouldReturn` Right (200, "rawSuccess")
119−
120− it "Servant.API.Raw on failure" $ withServer $ \ host -> do
121− runEitherT (getRawFailure methodGet host) `shouldReturn` Right (400, "rawFailure")
122−
123− modifyMaxSuccess (const 20) $ do
124− it "works for a combination of Capture, QueryParam, QueryFlag and ReqBody" $
125− property $ forAllShrink pathGen shrink $ \ a -> \ b c d ->
126− ioProperty $ do
127− withServer $ \ host -> do
128− result <- runEitherT (getMultiple a b c d host)
129− return $
130− result === Right (a, b, c, d)
131−
132−
133− context "client correctly handles error status codes" $ do
134− let test :: WrappedApi -> Spec
135− test (WrappedApi api) =
136− it (show (typeOf api)) $
137− withWaiDaemon (return (serve api (left (500, "error message")))) $
138− \ host -> do
139− let getResponse :: BaseUrl -> EitherT String IO ()
140− getResponse = client api
141− Left result <- runEitherT (getResponse host)
142− result `shouldContain` "error message"
143− mapM_ test $
144− (WrappedApi (Proxy :: Proxy Delete)) :
145− (WrappedApi (Proxy :: Proxy (Get ()))) :
146− (WrappedApi (Proxy :: Proxy (Post ()))) :
147− (WrappedApi (Proxy :: Proxy (Put ()))) :
148− []
149−
150−data WrappedApi where
151− WrappedApi :: (HasServer api, Server api ~ EitherT (Int, String) IO a,
152− HasClient api, Client api ~ (BaseUrl -> EitherT String IO ()),
153− Typeable api) =>
154− Proxy api -> WrappedApi
155−
156−
157−-- * utils
158−
159−withWaiDaemon :: IO Application -> (BaseUrl -> IO a) -> IO a
160−withWaiDaemon mkApplication action = do
161− application <- mkApplication
162− bracket (acquire application) free (\ (_, _, baseUrl) -> action baseUrl)
163− where
164− acquire application = do
165− (notifyStart, waitForStart) <- lvar
166− (notifyKilled, waitForKilled) <- lvar
167− thread <- forkIO $ (do
168− (krakenPort, socket) <- openTestSocket
169− let settings =
170− setPort krakenPort $ -- set here just for consistency, shouldn't be
171− -- used (it's set in the socket)
172− setBeforeMainLoop (notifyStart krakenPort)
173− defaultSettings
174− runSettingsSocket settings socket application)
175− `finally` notifyKilled ()
176− krakenPort <- waitForStart
177− let baseUrl = (BaseUrl Http "localhost" 80){baseUrlPort = krakenPort}
178− return (thread, waitForKilled, baseUrl)
179− free (thread, waitForKilled, _) = do
180− killThread thread
181− waitForKilled
182−
183− lvar :: IO (a -> IO (), IO a)
184− lvar = do
185− mvar <- newEmptyMVar
186− let put = putMVar mvar
187− wait = readMVar mvar
188− return (put, wait)
189−
190−openTestSocket :: IO (Port, Socket)
191−openTestSocket = do
192− s <- socket AF_INET Stream defaultProtocol
193− localhost <- inet_addr "127.0.0.1"
194− bind s (SockAddrInet aNY_PORT localhost)
195− listen s 1
196− port <- socketPort s
197− return (fromIntegral port, s)
198−
199−pathGen :: Gen String
200−pathGen = listOf $ elements $
201− filter (not . (`elem` "?%[]/#")) $
202− filter isPrint $
203− map chr [0..127]
test/Servant/Common/BaseUrlSpec.hsdeleted−69
@@ -1,69 +0,0 @@
1−{-# OPTIONS_GHC -fno-warn-orphans #-}
2−module Servant.Common.BaseUrlSpec where
3−
4−import Control.Applicative
5−import Control.DeepSeq
6−import Test.Hspec
7−import Test.QuickCheck
8−
9−import Servant.Common.BaseUrl
10−
11−spec :: Spec
12−spec = do
13− describe "showBaseUrl" $ do
14− it "shows a BaseUrl" $ do
15− showBaseUrl (BaseUrl Http "foo.com" 80) `shouldBe` "http://foo.com"
16−
17− it "shows a https BaseUrl" $ do
18− showBaseUrl (BaseUrl Https "foo.com" 443) `shouldBe` "https://foo.com"
19−
20− describe "httpBaseUrl" $ do
21− it "allows to construct default http BaseUrls" $ do
22− BaseUrl Http "bar" 80 `shouldBe` BaseUrl Http "bar" 80
23−
24− describe "parseBaseUrl" $ do
25− it "is total" $ do
26− property $ \ string ->
27− deepseq (fmap show (parseBaseUrl string)) True
28−
29− it "is the inverse of showBaseUrl" $ do
30− property $ \ baseUrl ->
31− counterexample (showBaseUrl baseUrl) $
32− parseBaseUrl (showBaseUrl baseUrl) ===
33− Right baseUrl
34−
35− it "allows trailing slashes" $ do
36− parseBaseUrl "foo.com/" `shouldBe` Right (BaseUrl Http "foo.com" 80)
37−
38− context "urls without scheme" $ do
39− it "assumes http" $ do
40− parseBaseUrl "foo.com" `shouldBe` Right (BaseUrl Http "foo.com" 80)
41−
42− it "allows port numbers" $ do
43− parseBaseUrl "foo.com:8080" `shouldBe` Right (BaseUrl Http "foo.com" 8080)
44−
45− it "rejects ftp urls" $ do
46− parseBaseUrl "ftp://foo.com" `shouldSatisfy` isLeft
47−
48−instance Arbitrary BaseUrl where
49− arbitrary = BaseUrl <$>
50− elements [Http, Https] <*>
51− hostNameGen <*>
52− portGen
53− where
54− -- this does not perfectly mirror the url standard, but I hope it's good
55− -- enough.
56− hostNameGen = do
57− let letters = ['a' .. 'z'] ++ ['A' .. 'Z']
58− first <- elements letters
59− middle <- listOf1 $ elements (letters ++ ['0' .. '9'] ++ ['.', '-'])
60− last <- elements letters
61− return (first : middle ++ [last])
62− portGen = frequency $
63− (1, return 80) :
64− (1, return 443) :
65− (1, choose (1, 20000)) :
66− []
67−
68−isLeft :: Either a b -> Bool
69−isLeft = either (const True) (const False)
test/Servant/ServerSpec.hs−3
@@ -29,7 +29,6 @@ import Servant.API.QueryParam
2929import Servant.API.Raw
3030import Servant.API.Sub
3131import Servant.API.Alternative
32−import Servant.Docs
3332import Servant.Server
3433
3534
@@ -43,8 +42,6 @@ data Person = Person {
4342
4443instance ToJSON Person
4544instance FromJSON Person
46−instance ToSample Person where
47− toSample = Just alice
4845
4946alice :: Person
5047alice = Person "Alice" 42