on master help sign in
Merged in 0.3
330e9abDaniel Larsson
CHANGELOG.mdnew file+6
@@ -0,0 +1,6 @@
1+0.3
2+---
3+
4+* Add the ability to display multiple responses, with some accompanying `Text` to describe the context in which we get the corresponding JSON.
5+* Expose the `headers` lens
6+* Represent an endpoint's path as `[String]` (previously `String`), fixing a corner case where the leading `/` would be missing.
README.md+1−1
@@ -4,7 +4,7 @@
4
4
5![servant](https://raw.githubusercontent.com/haskell-servant/servant/master/servant.png)
5![servant](https://raw.githubusercontent.com/haskell-servant/servant/master/servant.png)
6
6
7−Generate API docs for your *servant* webservice.
7+Generate API docs for your *servant* webservice. Feel free to also take a look at [servant-pandoc](https://github.com/mpickering/servant-pandoc) which uses this package to target a broad range of output formats using the excellent **pandoc**.
8
8
9## Example
9## Example
10
10
example/greet.hs+5
@@ -39,6 +39,11 @@ instance ToParam (QueryParam "capital" Bool) where
39instance ToSample Greet where
39instance ToSample Greet where
40 toSample = Just $ Greet "Hello, haskeller!"
40 toSample = Just $ Greet "Hello, haskeller!"
41
41
42+ toSamples =
43+ [ ("If you use ?capital=true", Greet "HELLO, HASKELLER")
44+ , ("If you use ?capital=false", Greet "Hello, haskeller")
45+ ]
46+
42-- API specification
47-- API specification
43type TestApi =
48type TestApi =
44 -- GET /hello/:name?capital={true, false} returns a Greet as JSON
49 -- GET /hello/:name?capital={true, false} returns a Greet as JSON
example/greet.md+27−12
@@ -1,8 +1,7 @@
1−
2POST /greet
1POST /greet
3-----------
2-----------
4
3
5−**Request Body**:
4+**Request Body**:
6
5
7``` javascript
6``` javascript
8{
7{
@@ -10,50 +9,66 @@ POST /greet
10}
9}
11```
10```
12
11
13−**Response**:
12+**Response**:
14
13
15 - Status code 201
14 - Status code 201
16− - Response body as below.
15+ - If you use ?capital=true
17
16
18``` javascript
17``` javascript
19{
18{
19+ "msg": "HELLO, HASKELLER"
20+}
21+```
22+
23+ - If you use ?capital=false
24+
25+``` javascript
26+{
20− "msg": "Hello, haskeller!"
27+ "msg": "Hello, haskeller"
21}
28}
22```
29```
23
30
24GET /hello/:name
31GET /hello/:name
25----------------
32----------------
26
33
27−**Captures**:
34+**Captures**:
28
35
29- *name*: name of the person to greet
36- *name*: name of the person to greet
30
37
31−**GET Parameters**:
38+**GET Parameters**:
32
39
33 - capital
40 - capital
34 - **Values**: *true, false*
41 - **Values**: *true, false*
35 - **Description**: Get the greeting message in uppercase (true) or not (false). Default is false.
42 - **Description**: Get the greeting message in uppercase (true) or not (false). Default is false.
36
43
37
44
38−**Response**:
45+**Response**:
39
46
40 - Status code 200
47 - Status code 200
41− - Response body as below.
48+ - If you use ?capital=true
42
49
43``` javascript
50``` javascript
44{
51{
52+ "msg": "HELLO, HASKELLER"
53+}
54+```
55+
56+ - If you use ?capital=false
57+
58+``` javascript
59+{
45− "msg": "Hello, haskeller!"
60+ "msg": "Hello, haskeller"
46}
61}
47```
62```
48
63
49DELETE /greet/:greetid
64DELETE /greet/:greetid
50----------------------
65----------------------
51
66
52−**Captures**:
67+**Captures**:
53
68
54- *greetid*: identifier of the greet msg to remove
69- *greetid*: identifier of the greet msg to remove
55
70
56−**Response**:
71+**Response**:
57
72
58 - Status code 204
73 - Status code 204
59 - No response body
74 - No response body
servant-docs.cabal+5−2
@@ -1,5 +1,5 @@
1name: servant-docs
1name: servant-docs
2−version: 0.2.1
2+version: 0.3
3synopsis: generate API docs for your servant webservice
3synopsis: generate API docs for your servant webservice
4description:
4description:
5 Library for generating API docs from a servant API definition.
5 Library for generating API docs from a servant API definition.
@@ -9,13 +9,16 @@ license: BSD3
9license-file: LICENSE
9license-file: LICENSE
10author: Alp Mestanogullari, Sönke Hahn, Julian K. Arni
10author: Alp Mestanogullari, Sönke Hahn, Julian K. Arni
11maintainer: alpmestan@gmail.com
11maintainer: alpmestan@gmail.com
12−copyright: 2014 Zalora South East Asia Pte Ltd
12+copyright: 2014-2015 Zalora South East Asia Pte Ltd
13category: Web
13category: Web
14build-type: Simple
14build-type: Simple
15cabal-version: >=1.10
15cabal-version: >=1.10
16tested-with: GHC >= 7.8
16tested-with: GHC >= 7.8
17homepage: http://haskell-servant.github.io/
17homepage: http://haskell-servant.github.io/
18Bug-reports: http://github.com/haskell-servant/servant-docs/issues
18Bug-reports: http://github.com/haskell-servant/servant-docs/issues
19+extra-source-files:
20+ CHANGELOG.md
21+ README.md
19source-repository head
22source-repository head
20 type: git
23 type: git
21 location: http://github.com/haskell-servant/servant-docs.git
24 location: http://github.com/haskell-servant/servant-docs.git
src/Servant/Docs.hs+58−31
@@ -1,10 +1,12 @@
1{-# LANGUAGE TypeFamilies #-}
1{-# LANGUAGE TypeFamilies #-}
2{-# LANGUAGE DeriveGeneric #-}
2{-# LANGUAGE DeriveGeneric #-}
3+{-# LANGUAGE TupleSections #-}
3{-# LANGUAGE TypeOperators #-}
4{-# LANGUAGE TypeOperators #-}
5+{-# LANGUAGE TemplateHaskell #-}
4{-# LANGUAGE FlexibleContexts #-}
6{-# LANGUAGE FlexibleContexts #-}
5{-# LANGUAGE FlexibleInstances #-}
7{-# LANGUAGE FlexibleInstances #-}
8+{-# LANGUAGE OverloadedStrings #-}
6{-# LANGUAGE ScopedTypeVariables #-}
9{-# LANGUAGE ScopedTypeVariables #-}
7−{-# LANGUAGE TemplateHaskell #-}
8
10
9-------------------------------------------------------------------------------
11-------------------------------------------------------------------------------
10-- | This module lets you get API docs for free. It lets generate
12-- | This module lets you get API docs for free. It lets generate
@@ -83,12 +85,10 @@ module Servant.Docs
83 ( -- * 'HasDocs' class and key functions
85 ( -- * 'HasDocs' class and key functions
84 HasDocs(..), docs, markdown
86 HasDocs(..), docs, markdown
85
87
86− {- , -- * Serving the documentation
87− serveDocumentation -}
88−
89 , -- * Classes you need to implement for your types
88 , -- * Classes you need to implement for your types
90 ToSample(..)
89 ToSample(..)
91 , sampleByteString
90 , sampleByteString
91+ , sampleByteStrings
92 , ToParam(..)
92 , ToParam(..)
93 , ToCapture(..)
93 , ToCapture(..)
94
94
@@ -99,7 +99,7 @@ module Servant.Docs
99 , DocCapture(..), capSymbol, capDesc
99 , DocCapture(..), capSymbol, capDesc
100 , DocQueryParam(..), ParamKind(..), paramName, paramValues, paramDesc, paramKind
100 , DocQueryParam(..), ParamKind(..), paramName, paramValues, paramDesc, paramKind
101 , Response, respStatus, respBody, defResponse
101 , Response, respStatus, respBody, defResponse
102− , Action, captures, params, rqbody, response, defAction
102+ , Action, captures, headers, params, rqbody, response, defAction
103 , single
103 , single
104
104
105 , -- * Useful modules when defining your doc printers
105 , -- * Useful modules when defining your doc printers
@@ -114,6 +114,7 @@ import Data.ByteString.Lazy.Char8 (ByteString)
114import Data.Hashable
114import Data.Hashable
115import Data.HashMap.Strict (HashMap)
115import Data.HashMap.Strict (HashMap)
116import Data.List
116import Data.List
117+import Data.Maybe (listToMaybe)
117import Data.Monoid
118import Data.Monoid
118import Data.Proxy
119import Data.Proxy
119import Data.Text (Text, pack, unpack)
120import Data.Text (Text, pack, unpack)
@@ -123,6 +124,7 @@ import GHC.TypeLits
123import Servant.API
124import Servant.API
124
125
125import qualified Data.HashMap.Strict as HM
126import qualified Data.HashMap.Strict as HM
127+import qualified Data.Text as T
126
128
127-- | Supported HTTP request methods
129-- | Supported HTTP request methods
128data Method = DocDELETE -- ^ the DELETE method
130data Method = DocDELETE -- ^ the DELETE method
@@ -148,19 +150,26 @@ instance Hashable Method
148-- @
150-- @
149-- λ> 'defEndpoint'
151-- λ> 'defEndpoint'
150-- GET /
152-- GET /
151−-- λ> 'defEndpoint' & 'path' '<>~' "foo"
153+-- λ> 'defEndpoint' & 'path' '<>~' ["foo"]
152-- GET /foo
154-- GET /foo
153−-- λ> 'defEndpoint' & 'path' '<>~' "foo" & 'method' '.~' 'DocPOST'
155+-- λ> 'defEndpoint' & 'path' '<>~' ["foo"] & 'method' '.~' 'DocPOST'
154-- POST /foo
156-- POST /foo
155-- @
157-- @
156data Endpoint = Endpoint
158data Endpoint = Endpoint
157− { _path :: String -- type collected
159+ { _path :: [String] -- type collected
158− , _method :: Method -- type collected
160+ , _method :: Method -- type collected
159 } deriving (Eq, Generic)
161 } deriving (Eq, Generic)
160
162
161instance Show Endpoint where
163instance Show Endpoint where
162 show (Endpoint p m) =
164 show (Endpoint p m) =
163− show m ++ " " ++ p
165+ show m ++ " " ++ showPath p
166+
167+-- |
168+-- Render a path as a '/'-delimited string
169+--
170+showPath :: [String] -> String
171+showPath [] = "/"
172+showPath ps = concatMap ('/' :) ps
164
173
165-- | An 'Endpoint' whose path is `"/"` and whose method is 'DocGET'
174-- | An 'Endpoint' whose path is `"/"` and whose method is 'DocGET'
166--
175--
@@ -169,13 +178,13 @@ instance Show Endpoint where
169-- @
178-- @
170-- λ> 'defEndpoint'
179-- λ> 'defEndpoint'
171-- GET /
180-- GET /
172−-- λ> 'defEndpoint' & 'path' '<>~' "foo"
181+-- λ> 'defEndpoint' & 'path' '<>~' ["foo"]
173-- GET /foo
182-- GET /foo
174−-- λ> 'defEndpoint' & 'path' '<>~' "foo" & 'method' '.~' 'DocPOST'
183+-- λ> 'defEndpoint' & 'path' '<>~' ["foo"] & 'method' '.~' 'DocPOST'
175-- POST /foo
184-- POST /foo
176-- @
185-- @
177defEndpoint :: Endpoint
186defEndpoint :: Endpoint
178−defEndpoint = Endpoint "/" DocGET
187+defEndpoint = Endpoint [] DocGET
179
188
180instance Hashable Endpoint
189instance Hashable Endpoint
181
190
@@ -226,12 +235,12 @@ data ParamKind = Normal | List | Flag
226-- Can be tweaked with two lenses.
235-- Can be tweaked with two lenses.
227--
236--
228-- > λ> defResponse
237-- > λ> defResponse
229−-- > Response {_respStatus = 200, _respBody = Nothing}
238+-- > Response {_respStatus = 200, _respBody = []}
230−-- > λ> defResponse & respStatus .~ 204 & respBody .~ Just "[]"
239+-- > λ> defResponse & respStatus .~ 204 & respBody .~ [("If everything goes well", "{ \"status\": \"ok\" }")]
231−-- > Response {_respStatus = 204, _respBody = Just "[]"}
240+-- > Response {_respStatus = 204, _respBody = [("If everything goes well", "{ \"status\": \"ok\" }")]}
232data Response = Response
241data Response = Response
233 { _respStatus :: Int
242 { _respStatus :: Int
234− , _respBody :: Maybe ByteString
243+ , _respBody :: [(Text, ByteString)]
235 } deriving (Eq, Show)
244 } deriving (Eq, Show)
236
245
237-- | Default response: status code 200, no response body.
246-- | Default response: status code 200, no response body.
@@ -243,7 +252,7 @@ data Response = Response
243-- > λ> defResponse & respStatus .~ 204 & respBody .~ Just "[]"
252-- > λ> defResponse & respStatus .~ 204 & respBody .~ Just "[]"
244-- > Response {_respStatus = 204, _respBody = Just "[]"}
253-- > Response {_respStatus = 204, _respBody = Just "[]"}
245defResponse :: Response
254defResponse :: Response
246−defResponse = Response 200 Nothing
255+defResponse = Response 200 []
247
256
248-- | A datatype that represents everything that can happen
257-- | A datatype that represents everything that can happen
249-- at an endpoint, with its lenses:
258-- at an endpoint, with its lenses:
@@ -327,15 +336,29 @@ class HasDocs layout where
327-- > toSample = Just g
336-- > toSample = Just g
328-- >
337-- >
329-- > where g = Greet "Hello, haskeller!"
338-- > where g = Greet "Hello, haskeller!"
339+--
340+-- You can also instantiate this class using 'toSamples' instead of
341+-- 'toSample': it lets you specify different responses along with
342+-- some context (as 'Text') that explains when you're supposed to
343+-- get the corresponding response.
330class ToJSON a => ToSample a where
344class ToJSON a => ToSample a where
345+ {-# MINIMAL (toSample | toSamples) #-}
331 toSample :: Maybe a
346 toSample :: Maybe a
347+ toSample = fmap snd $ listToMaybe samples
348+ where samples = toSamples :: [(Text, a)]
332
349
333−instance ToSample () where
350+ toSamples :: [(Text, a)]
351+ toSamples = maybe [] (return . ("",)) s
334− toSample = Just ()
352+ where s = toSample :: Maybe a
335
353
336−sampleByteString :: forall a . ToSample a => Proxy a -> Maybe ByteString
354+sampleByteString :: forall a. ToSample a => Proxy a -> Maybe ByteString
337sampleByteString Proxy = fmap encodePretty (toSample :: Maybe a)
355sampleByteString Proxy = fmap encodePretty (toSample :: Maybe a)
338
356
357+sampleByteStrings :: forall a. ToSample a => Proxy a -> [(Text, ByteString)]
358+sampleByteStrings Proxy = samples & traverse._2 %~ encodePretty
359+
360+ where samples = toSamples :: [(Text, a)]
361+
339-- | The class that helps us automatically get documentation
362-- | The class that helps us automatically get documentation
340-- for GET parameters.
363-- for GET parameters.
341--
364--
@@ -376,7 +399,7 @@ markdown = unlines . concat . map (uncurry printEndpoint) . HM.toList
376 responseStr (action ^. response) ++
399 responseStr (action ^. response) ++
377 []
400 []
378
401
379− where str = show (endpoint^.method) ++ " " ++ endpoint^.path
402+ where str = show (endpoint^.method) ++ " " ++ showPath (endpoint^.path)
380 len = length str
403 len = length str
381
404
382 capturesStr :: [DocCapture] -> [String]
405 capturesStr :: [DocCapture] -> [String]
@@ -441,9 +464,13 @@ markdown = unlines . concat . map (uncurry printEndpoint) . HM.toList
441 "**Response**: " :
464 "**Response**: " :
442 "" :
465 "" :
443 (" - Status code " ++ show (resp ^. respStatus)) :
466 (" - Status code " ++ show (resp ^. respStatus)) :
467+ bodies
468+
444− (resp ^. respBody &
469+ where bodies = case resp ^. respBody of
445− maybe [" - No response body\n"]
470+ [] -> [" - No response body\n"]
446− (\b -> " - Response body as below." : jsonStr b))
471+ [("", r)] -> " - Response body as below." : jsonStr r
472+ xs ->
473+ concatMap (\(ctx, r) -> (" - " <> T.unpack ctx) : jsonStr r) xs
447
474
448-- * Instances
475-- * Instances
449
476
@@ -472,7 +499,7 @@ instance (KnownSymbol sym, ToCapture (Capture sym a), HasDocs sublayout)
472 captureP = Proxy :: Proxy (Capture sym a)
499 captureP = Proxy :: Proxy (Capture sym a)
473
500
474 action' = over captures (|> toCapture captureP) action
501 action' = over captures (|> toCapture captureP) action
475− endpoint' = over path (\p -> p++"/:"++symbolVal symP) endpoint
502+ endpoint' = over path (\p -> p ++ [":" ++ symbolVal symP]) endpoint
476 symP = Proxy :: Proxy sym
503 symP = Proxy :: Proxy sym
477
504
478
505
@@ -482,7 +509,7 @@ instance HasDocs Delete where
482
509
483 where endpoint' = endpoint & method .~ DocDELETE
510 where endpoint' = endpoint & method .~ DocDELETE
484
511
485− action' = action & response.respBody .~ Nothing
512+ action' = action & response.respBody .~ []
486 & response.respStatus .~ 204
513 & response.respStatus .~ 204
487
514
488instance ToSample a => HasDocs (Get a) where
515instance ToSample a => HasDocs (Get a) where
@@ -490,7 +517,7 @@ instance ToSample a => HasDocs (Get a) where
490 single endpoint' action'
517 single endpoint' action'
491
518
492 where endpoint' = endpoint & method .~ DocGET
519 where endpoint' = endpoint & method .~ DocGET
493− action' = action & response.respBody .~ sampleByteString p
520+ action' = action & response.respBody .~ sampleByteStrings p
494 p = Proxy :: Proxy a
521 p = Proxy :: Proxy a
495
522
496instance (KnownSymbol sym, HasDocs sublayout)
523instance (KnownSymbol sym, HasDocs sublayout)
@@ -508,7 +535,7 @@ instance ToSample a => HasDocs (Post a) where
508
535
509 where endpoint' = endpoint & method .~ DocPOST
536 where endpoint' = endpoint & method .~ DocPOST
510
537
511− action' = action & response.respBody .~ sampleByteString p
538+ action' = action & response.respBody .~ sampleByteStrings p
512 & response.respStatus .~ 201
539 & response.respStatus .~ 201
513
540
514 p = Proxy :: Proxy a
541 p = Proxy :: Proxy a
@@ -519,7 +546,7 @@ instance ToSample a => HasDocs (Put a) where
519
546
520 where endpoint' = endpoint & method .~ DocPUT
547 where endpoint' = endpoint & method .~ DocPUT
521
548
522− action' = action & response.respBody .~ sampleByteString p
549+ action' = action & response.respBody .~ sampleByteStrings p
523 & response.respStatus .~ 200
550 & response.respStatus .~ 200
524
551
525 p = Proxy :: Proxy a
552 p = Proxy :: Proxy a
@@ -614,7 +641,7 @@ instance (KnownSymbol path, HasDocs sublayout) => HasDocs (path :> sublayout) wh
614 docsFor sublayoutP (endpoint', action)
641 docsFor sublayoutP (endpoint', action)
615
642
616 where sublayoutP = Proxy :: Proxy sublayout
643 where sublayoutP = Proxy :: Proxy sublayout
617− endpoint' = endpoint & path <>~ symbolVal pa
644+ endpoint' = endpoint & path <>~ [symbolVal pa]
618 pa = Proxy :: Proxy path
645 pa = Proxy :: Proxy path
619
646
620{-
647{-