git.iko.soy/typeable/servant
on master help sign in
Cleanup, including:

- Integrated doctests into cabal - Doctest more of the documentation - Remove the (:>) constructor - Give kind signatures to Symbols - Make all constructors typeable - Use stylish-haskell for consistency of styling - Cleanup documentation - Remove old TODO.md

commit635902e592632cbc8907006aa57c16cb8d2fd8ea authorJulian K. Arni <jkarni@gmail.com> dateTue 24 Feb 2015, 10:28 (12 years ago) parent8cac6c6
21 files changed+404−171
.travis.yml+1−3 TODO.md−1 servant.cabal+11 src/Servant/API.hs+40−30 src/Servant/API/Alternative.hs+21−4 src/Servant/API/Capture.hs+16−5 src/Servant/API/ContentTypes.hs+102−31 src/Servant/API/Delete.hs+11−3 src/Servant/API/Get.hs+13−6 src/Servant/API/Header.hs+20−8 src/Servant/API/MatrixParam.hs+26−11 src/Servant/API/Patch.hs+16−9 src/Servant/API/Post.hs+16−9 src/Servant/API/Put.hs+12−5 src/Servant/API/QueryParam.hs+25−11 src/Servant/API/ReqBody.hs+16−5 src/Servant/API/Sub.hs+18−8 src/Servant/Common/Text.hs+17−12 test-docs.sh−3 test/Doctests.hs+16 test/Servant/Common/TextSpec.hs+7−7
.travis.yml+1−3
@@ -7,14 +7,12 @@ script:
77 - cabal configure --enable-tests --enable-library-coverage && cabal build && cabal test
88 - cabal check
99 - cabal sdist
10− - cabal install doctest
11− - ./test-docs.sh
1210
1311after_script:
1412 - |
1513 if [ "$TRAVIS_PULL_REQUEST" -eq "$TRAVIS_PULL_REQUEST" ] 2>/dev/null || [ "$TRAVIS_BRANCH" == "master" ] ; then
1614 cabal install hpc-coveralls
17− hpc-coveralls --exclude-dir=test spec
15+ hpc-coveralls --exclude-dir=test spec doctests
1816 fi
1917
2018
TODO.mddeleted−1
@@ -1 +0,0 @@
1−- Try to find a way to abstract over the format(s) instead of the current focus on JSON, maybe
servant.cabal+11
@@ -97,3 +97,14 @@ test-suite spec
9797 , string-conversions
9898 , text
9999 , url
100+
101+test-suite doctests
102+ build-depends: base
103+ , servant
104+ , doctest
105+ , filemanip
106+ type: exitcode-stdio-1.0
107+ main-is: test/Doctests.hs
108+ buildable: True
109+ default-language: Haskell2010
110+ ghc-options: -threaded
src/Servant/API.hs+40−30
@@ -1,60 +1,70 @@
11module Servant.API (
22
33 -- * Combinators
4− -- | Type-level combinator for expressing subrouting: @':>'@
54 module Servant.API.Sub,
6− -- | Type-level combinator for alternative endpoints: @':<|>'@
5+ -- | Type-level combinator for expressing subrouting: @':>'@
76 module Servant.API.Alternative,
7+ -- | Type-level combinator for alternative endpoints: @':<|>'@
88
99 -- * Accessing information from the request
10− -- | Capturing parts of the url path as parsed values: @'Capture'@
1110 module Servant.API.Capture,
12− -- | Retrieving specific headers from the request
11+ -- | Capturing parts of the url path as parsed values: @'Capture'@
1312 module Servant.API.Header,
14− -- | Retrieving parameters from the query string of the 'URI': @'QueryParam'@
13+ -- | Retrieving specific headers from the request
1514 module Servant.API.QueryParam,
16− -- | Accessing the request body as a JSON-encoded type: @'ReqBody'@
15+ -- | Retrieving parameters from the query string of the 'URI': @'QueryParam'@
1716 module Servant.API.ReqBody,
18− -- | Retrieving matrix parameters from the 'URI' segment: @'MatrixParam'@
17+ -- | Accessing the request body as a JSON-encoded type: @'ReqBody'@
1918 module Servant.API.MatrixParam,
19+ -- | Retrieving matrix parameters from the 'URI' segment: @'MatrixParam'@
2020
2121 -- * Actual endpoints, distinguished by HTTP method
22− -- | GET requests
2322 module Servant.API.Get,
24− -- | POST requests
23+ -- | @GET@ requests
2524 module Servant.API.Post,
26− -- | DELETE requests
25+ -- | @POST@ requests
2726 module Servant.API.Delete,
28− -- | PUT requests
27+ -- | @DELETE@ requests
2928 module Servant.API.Put,
30− -- | PATCH requests
29+ -- | @PUT@ requests
3130 module Servant.API.Patch,
31+ -- | @PATCH@ requests
3232
3333 -- * Content Types
3434 module Servant.API.ContentTypes,
35+ -- | Serializing and deserializing types based on @Accept@ and
36+ -- @Content-Type@ headers.
3537
3638 -- * Untyped endpoints
37− -- | Plugging in a wai 'Network.Wai.Application', serving directories
3839 module Servant.API.Raw,
40+ -- | Plugging in a wai 'Network.Wai.Application', serving directories
41+
42+ -- * FromText and ToText
43+ module Servant.Common.Text,
44+ -- | Classes and instances for types that can be converted to and from @Text@
3945
4046 -- * Utilities
41− -- | Type-safe internal URIs
4247 module Servant.Utils.Links,
48+ -- | Type-safe internal URIs
4349 ) where
4450
45−import Servant.API.Alternative ( (:<|>)(..) )
46−import Servant.API.Capture ( Capture )
47−import Servant.API.ContentTypes ( JSON , PlainText, OctetStream
48− , MimeRender(..) , MimeUnrender(..))
49−import Servant.API.Delete ( Delete )
50−import Servant.API.Get ( Get )
51−import Servant.API.Header ( Header )
52−import Servant.API.Post ( Post )
53−import Servant.API.Put ( Put )
54−import Servant.API.Patch ( Patch )
55−import Servant.API.QueryParam ( QueryFlag, QueryParams, QueryParam )
56−import Servant.API.MatrixParam ( MatrixFlag, MatrixParams, MatrixParam )
57−import Servant.API.Raw ( Raw )
58−import Servant.API.ReqBody ( ReqBody )
59−import Servant.API.Sub ( (:>)(..) )
60−import Servant.Utils.Links ( safeLink, URI(..), IsElem, IsElem', HasLink(..) )
51+import Servant.Common.Text (FromText(..), ToText(..))
52+import Servant.API.Alternative ((:<|>) (..))
53+import Servant.API.Capture (Capture)
54+import Servant.API.ContentTypes (JSON, MimeRender (..),
55+ MimeUnrender (..), OctetStream,
56+ PlainText)
57+import Servant.API.Delete (Delete)
58+import Servant.API.Get (Get)
59+import Servant.API.Header (Header)
60+import Servant.API.MatrixParam (MatrixFlag, MatrixParam,
61+ MatrixParams)
62+import Servant.API.Patch (Patch)
63+import Servant.API.Post (Post)
64+import Servant.API.Put (Put)
65+import Servant.API.QueryParam (QueryFlag, QueryParam, QueryParams)
66+import Servant.API.Raw (Raw)
67+import Servant.API.ReqBody (ReqBody)
68+import Servant.API.Sub ((:>))
69+import Servant.Utils.Links (HasLink (..), IsElem, IsElem',
70+ URI (..), safeLink)
src/Servant/API/Alternative.hs+21−4
@@ -1,11 +1,28 @@
1−{-# LANGUAGE TypeOperators #-}
2−module Servant.API.Alternative where
1+{-# LANGUAGE DeriveDataTypeable #-}
2+{-# LANGUAGE TypeOperators #-}
3+module Servant.API.Alternative ((:<|>)(..)) where
34
5+import Data.Monoid (Monoid (..))
6+import Data.Typeable (Typeable)
47-- | Union of two APIs, first takes precedence in case of overlap.
58--
69-- Example:
710--
8−-- > type MyApi = "books" :> Get [Book] -- GET /books
9−-- > :<|> "books" :> ReqBody Book :> Post Book -- POST /books
11+-- >>> :{
12+--type MyApi = "books" :> Get '[JSON] [Book] -- GET /books
13+-- :<|> "books" :> ReqBody '[JSON] Book :> Post '[JSON] () -- POST /books
14+-- :}
1015data a :<|> b = a :<|> b
16+ deriving (Typeable, Eq, Show)
1117infixr 8 :<|>
18+
19+instance (Monoid a, Monoid b) => Monoid (a :<|> b) where
20+ mempty = mempty :<|> mempty
21+ (a :<|> b) `mappend` (a' :<|> b') = (a `mappend` a') :<|> (b `mappend` b')
22+
23+-- $setup
24+-- >>> import Servant.API
25+-- >>> import Data.Aeson
26+-- >>> import Data.Text
27+-- >>> data Book
28+-- >>> instance ToJSON Book where { toJSON = undefined }
src/Servant/API/Capture.hs+16−5
@@ -1,10 +1,21 @@
1−{-# LANGUAGE PolyKinds #-}
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE DeriveDataTypeable #-}
3+{-# LANGUAGE PolyKinds #-}
24module Servant.API.Capture (Capture) where
35
6+import Data.Typeable (Typeable)
7+import GHC.TypeLits (Symbol)
48-- | Capture a value from the request path under a certain type @a@.
59--
610-- Example:
7−--
8−-- > -- GET /books/:isbn
9−-- > type MyApi = "books" :> Capture "isbn" Text :> Get Book
10−data Capture sym a
11+-- >>> -- GET /books/:isbn
12+-- >>> type MyApi = "books" :> Capture "isbn" Text :> Get '[JSON] Book
13+data Capture (sym :: Symbol) a
14+ deriving (Typeable)
15+
16+-- $setup
17+-- >>> import Servant.API
18+-- >>> import Data.Aeson
19+-- >>> import Data.Text
20+-- >>> data Book
21+-- >>> instance ToJSON Book where { toJSON = undefined }
src/Servant/API/ContentTypes.hs+102−31
@@ -9,7 +9,57 @@
99{-# LANGUAGE TypeFamilies #-}
1010{-# LANGUAGE TypeOperators #-}
1111{-# LANGUAGE UndecidableInstances #-}
12−module Servant.API.ContentTypes where
12+
13+-- | A collection of basic Content-Types (also known as Internet Media
14+-- Types, or MIME types). Additionally, this module provides classes that
15+-- encapsulate how to serialize or deserialize values to or from
16+-- a particular Content-Type.
17+--
18+-- Content-Types are used in `ReqBody` and the method combinators:
19+--
20+-- >>> type MyEndpoint = ReqBody '[JSON, PlainText] Book :> Get '[JSON, PlainText] :> Book
21+--
22+-- Meaning the endpoint accepts requests of Content-Type @application/json@
23+-- or @text/plain;charset-utf8@, and returns data in either one of those
24+-- formats (depending on the @Accept@ header).
25+--
26+-- If you would like to support Content-Types beyond those provided here,
27+-- then:
28+--
29+-- (1) Declare a new data type with no constructors (e.g. @data HTML@).
30+-- (2) Make an instance of it for `Accept`.
31+-- (3) If you want to be able to serialize data *into* that
32+-- Content-Type, make an instance of it for `MimeRender`.
33+-- (4) If you want to be able to deserialize data *from* that
34+-- Content-Type, make an instance of it for `MimeUnrender`.
35+--
36+-- Note that roles are reversed in @servant-server@ and @servant-client@:
37+-- to be able to serve (or even typecheck) a @Get '[JSON, XML] MyData@,
38+-- you'll need to have the appropriate `MimeRender` instances in scope,
39+-- whereas to query that endpoint with @servant-client@, you'll need
40+-- a `MimeUnrender` instance in scope.
41+module Servant.API.ContentTypes
42+ (
43+ -- * Provided Content-Types
44+ JSON
45+ , PlainText
46+ , FormUrlEncoded
47+ , OctetStream
48+
49+ -- * Building your own Content-Type
50+ , Accept(..)
51+ , MimeRender(..)
52+ , MimeUnrender(..)
53+
54+ -- * Internal
55+ , AcceptHeader(..)
56+ , AllCTRender(..)
57+ , AllCTUnrender(..)
58+ , FromFormUrlEncoded(..)
59+ , ToFormUrlEncoded(..)
60+ , IsNonEmpty
61+ , eitherDecodeLenient
62+ ) where
1363
1464import Control.Applicative ((<*))
1565import Control.Arrow (left)
@@ -48,8 +98,12 @@ data OctetStream deriving Typeable
4898--
4999-- Example:
50100--
51−-- > instance Accept HTML where
52−-- > contentType _ = "text" // "html"
101+-- >>> import Network.HTTP.Media ((//), (/:))
102+-- >>> data HTML
103+-- >>> :{
104+--instance Accept HTML where
105+-- contentType _ = "text" // "html" /: ("charset", "utf-8")
106+-- :}
53107--
54108class Accept ctype where
55109 contentType :: Proxy ctype -> M.MediaType
@@ -93,7 +147,7 @@ newtype AcceptHeader = AcceptHeader BS.ByteString
93147class Accept ctype => MimeRender ctype a where
94148 toByteString :: Proxy ctype -> a -> ByteString
95149
96−class AllCTRender list a where
150+class AllCTRender (list :: [*]) a where
97151 -- If the Accept header can be matched, returns (Just) a tuple of the
98152 -- Content-Type and response (serialization of @a@ into the appropriate
99153 -- mimetype).
@@ -113,20 +167,28 @@ instance ( AllMimeRender ctyps a, IsNonEmpty ctyps
113167-- | Instantiate this class to register a way of deserializing a type based
114168-- on the request's @Content-Type@ header.
115169--
116−-- > data MyContentType = MyContentType String
117−-- >
118−-- > instance Accept MyContentType where
119−-- > contentType _ = "example" // "prs.me.mine" /: ("charset", "utf-8")
120−-- >
121−-- > instance Show a => MimeUnrender MyContentType where
122−-- > fromByteString _ bs = MyContentType $ unpack bs
123−-- >
124−-- > type MyAPI = "path" :> ReqBody '[MyContentType] :> Get '[JSON] Int
170+-- >>> import Network.HTTP.Media hiding (Accept)
171+-- >>> import qualified Data.ByteString.Lazy.Char8 as BSC
172+-- >>> data MyContentType = MyContentType String
173+--
174+-- >>> :{
175+--instance Accept MyContentType where
176+-- contentType _ = "example" // "prs.me.mine" /: ("charset", "utf-8")
177+-- :}
178+--
179+-- >>> :{
180+--instance Read a => MimeUnrender MyContentType a where
181+-- fromByteString _ bs = case BSC.take 12 bs of
182+-- "MyContentType" -> return . read . BSC.unpack $ BSC.drop 12 bs
183+-- _ -> Left "didn't start with the magic incantation"
184+-- :}
185+--
186+-- >>> type MyAPI = "path" :> ReqBody '[MyContentType] Int :> Get '[JSON] Int
125187--
126188class Accept ctype => MimeUnrender ctype a where
127189 fromByteString :: Proxy ctype -> ByteString -> Either String a
128190
129−class (IsNonEmpty list) => AllCTUnrender list a where
191+class (IsNonEmpty list) => AllCTUnrender (list :: [*]) a where
130192 handleCTypeH :: Proxy list
131193 -> ByteString -- Content-Type header
132194 -> ByteString -- Request body
@@ -144,8 +206,8 @@ instance ( AllMimeUnrender ctyps a, IsNonEmpty ctyps
144206--------------------------------------------------------------------------
145207-- Check that all elements of list are instances of MimeRender
146208--------------------------------------------------------------------------
147−class AllMimeRender ls a where
148− allMimeRender :: Proxy ls
209+class AllMimeRender (list :: [*]) a where
210+ allMimeRender :: Proxy list
149211 -> a -- value to serialize
150212 -> [(M.MediaType, ByteString)] -- content-types/response pairs
151213
@@ -168,8 +230,10 @@ instance AllMimeRender '[] a where
168230--------------------------------------------------------------------------
169231-- Check that all elements of list are instances of MimeUnrender
170232--------------------------------------------------------------------------
171−class AllMimeUnrender ls a where
172− allMimeUnrender :: Proxy ls -> ByteString -> [(M.MediaType, Either String a)]
233+class AllMimeUnrender (list :: [*]) a where
234+ allMimeUnrender :: Proxy list
235+ -> ByteString
236+ -> [(M.MediaType, Either String a)]
173237
174238instance AllMimeUnrender '[] a where
175239 allMimeUnrender _ _ = []
@@ -182,7 +246,7 @@ instance ( MimeUnrender ctyp a
182246 where pctyp = Proxy :: Proxy ctyp
183247 pctyps = Proxy :: Proxy ctyps
184248
185−type family IsNonEmpty (ls::[*]) :: Constraint where
249+type family IsNonEmpty (list :: [*]) :: Constraint where
186250 IsNonEmpty (x ': xs) = ()
187251
188252
@@ -193,9 +257,9 @@ type family IsNonEmpty (ls::[*]) :: Constraint where
193257instance ToJSON a => MimeRender JSON a where
194258 toByteString _ = encode
195259
196−-- | `encodeFormUrlEncoded . toFormUrlEncoded`
197−-- Note that the `fromByteString p (toByteString p x) == Right x` law only
198−-- holds if every element of x is non-null (i.e., not `("", "")`)
260+-- | @encodeFormUrlEncoded . toFormUrlEncoded@
261+-- Note that the @fromByteString p (toByteString p x) == Right x@ law only
262+-- holds if every element of x is non-null (i.e., not @("", "")@)
199263instance ToFormUrlEncoded a => MimeRender FormUrlEncoded a where
200264 toByteString _ = encodeFormUrlEncoded . toFormUrlEncoded
201265
@@ -203,11 +267,11 @@ instance ToFormUrlEncoded a => MimeRender FormUrlEncoded a where
203267instance MimeRender PlainText TextL.Text where
204268 toByteString _ = TextL.encodeUtf8
205269
206−-- | `fromStrict . TextS.encodeUtf8`
270+-- | @fromStrict . TextS.encodeUtf8@
207271instance MimeRender PlainText TextS.Text where
208272 toByteString _ = fromStrict . TextS.encodeUtf8
209273
210−-- | `id`
274+-- | @id@
211275instance MimeRender OctetStream ByteString where
212276 toByteString _ = id
213277
@@ -230,25 +294,25 @@ eitherDecodeLenient input = do
230294instance FromJSON a => MimeUnrender JSON a where
231295 fromByteString _ = eitherDecodeLenient
232296
233−-- | `decodeFormUrlEncoded >=> fromFormUrlEncoded`
234−-- Note that the `fromByteString p (toByteString p x) == Right x` law only
235−-- holds if every element of x is non-null (i.e., not `("", "")`)
297+-- | @decodeFormUrlEncoded >=> fromFormUrlEncoded@
298+-- Note that the @fromByteString p (toByteString p x) == Right x@ law only
299+-- holds if every element of x is non-null (i.e., not @("", "")@)
236300instance FromFormUrlEncoded a => MimeUnrender FormUrlEncoded a where
237301 fromByteString _ = decodeFormUrlEncoded >=> fromFormUrlEncoded
238302
239−-- | `left show . TextL.decodeUtf8'`
303+-- | @left show . TextL.decodeUtf8'@
240304instance MimeUnrender PlainText TextL.Text where
241305 fromByteString _ = left show . TextL.decodeUtf8'
242306
243−-- | `left show . TextS.decodeUtf8' . toStrict`
307+-- | @left show . TextS.decodeUtf8' . toStrict@
244308instance MimeUnrender PlainText TextS.Text where
245309 fromByteString _ = left show . TextS.decodeUtf8' . toStrict
246310
247−-- | `Right . id`
311+-- | @Right . id@
248312instance MimeUnrender OctetStream ByteString where
249313 fromByteString _ = Right . id
250314
251−-- | `Right . toStrict`
315+-- | @Right . toStrict@
252316instance MimeUnrender OctetStream BS.ByteString where
253317 fromByteString _ = Right . toStrict
254318
@@ -296,3 +360,10 @@ decodeFormUrlEncoded q = do
296360 unescape :: TextS.Text -> TextS.Text
297361 unescape = cs . unEscapeString . cs . TextS.intercalate "%20" . TextS.splitOn "+"
298362 mapM parsePair xs
363+
364+-- $setup
365+-- >>> import Servant.API
366+-- >>> import Data.Aeson
367+-- >>> import Data.Text
368+-- >>> data Book
369+-- >>> instance ToJSON Book where { toJSON = undefined }
src/Servant/API/Delete.hs+11−3
@@ -1,5 +1,5 @@
11{-# LANGUAGE DeriveDataTypeable #-}
2−module Servant.API.Delete where
2+module Servant.API.Delete (Delete) where
33
44import Data.Typeable ( Typeable )
55
@@ -7,7 +7,15 @@ import Data.Typeable ( Typeable )
77--
88-- Example:
99--
10−-- > -- DELETE /books/:isbn
11−-- > type MyApi = "books" :> Capture "isbn" Text :> Delete
10+-- >>> -- DELETE /books/:isbn
11+-- >>> type MyApi = "books" :> Capture "isbn" Text :> Delete
1212data Delete
1313 deriving Typeable
14+
15+
16+-- $setup
17+-- >>> import Servant.API
18+-- >>> import Data.Aeson
19+-- >>> import Data.Text
20+-- >>> data Book
21+-- >>> instance ToJSON Book where { toJSON = undefined }
src/Servant/API/Get.hs+13−6
@@ -1,14 +1,21 @@
1+{-# LANGUAGE DataKinds #-}
12{-# LANGUAGE DeriveDataTypeable #-}
2−{-# LANGUAGE DataKinds #-}
3−{-# LANGUAGE KindSignatures #-}
4−module Servant.API.Get where
3+{-# LANGUAGE KindSignatures #-}
4+module Servant.API.Get (Get) where
55
6−import Data.Typeable ( Typeable )
6+import Data.Typeable (Typeable)
77
88-- | Endpoint for simple GET requests. Serves the result as JSON.
99--
1010-- Example:
1111--
12−-- > type MyApi = "books" :> Get '[JSON] [Book]
13−data Get (contentTypes::[*]) a
12+-- >>> type MyApi = "books" :> Get '[JSON] [Book]
13+data Get (contentTypes :: [*]) a
1414 deriving Typeable
15+
16+-- $setup
17+-- >>> import Servant.API
18+-- >>> import Data.Aeson
19+-- >>> import Data.Text
20+-- >>> data Book
21+-- >>> instance ToJSON Book where { toJSON = undefined }
src/Servant/API/Header.hs+20−8
@@ -1,13 +1,25 @@
1−{-# LANGUAGE PolyKinds #-}
2−module Servant.API.Header where
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE DeriveDataTypeable #-}
3+{-# LANGUAGE PolyKinds #-}
4+module Servant.API.Header (Header) where
35
6+import Data.Typeable (Typeable)
7+import GHC.TypeLits (Symbol)
48-- | Extract the given header's value as a value of type @a@.
59--
610-- Example:
711--
8−-- > newtype Referer = Referer Text
9−-- > deriving (Eq, Show, FromText, ToText)
10−-- >
11−-- > -- GET /view-my-referer
12−-- > type MyApi = "view-my-referer" :> Header "from" Referer :> Get Referer
13−data Header sym a
12+-- >>> newtype Referer = Referer Text deriving (Eq, Show)
13+-- >>>
14+-- >>> -- GET /view-my-referer
15+-- >>> type MyApi = "view-my-referer" :> Header "from" Referer :> Get '[JSON] Referer
16+data Header (sym :: Symbol) a
17+ deriving Typeable
18+
19+-- $setup
20+-- >>> import Servant.API
21+-- >>> import Servant.Common.Text
22+-- >>> import Data.Aeson
23+-- >>> import Data.Text
24+-- >>> data Book
25+-- >>> instance ToJSON Book where { toJSON = undefined }
src/Servant/API/MatrixParam.hs+26−11
@@ -1,14 +1,19 @@
1−{-# LANGUAGE PolyKinds #-}
2−module Servant.API.MatrixParam where
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE DeriveDataTypeable #-}
3+{-# LANGUAGE PolyKinds #-}
4+module Servant.API.MatrixParam (MatrixFlag, MatrixParam, MatrixParams) where
35
6+import Data.Typeable (Typeable)
7+import GHC.TypeLits (Symbol)
48-- | Lookup the value associated to the @sym@ matrix string parameter
59-- and try to extract it as a value of type @a@.
610--
711-- Example:
812--
9−-- > -- /books;author=<author name>
10−-- > type MyApi = "books" :> MatrixParam "author" Text :> Get [Book]
11−data MatrixParam sym a
13+-- >>> -- /books;author=<author name>
14+-- >>> type MyApi = "books" :> MatrixParam "author" Text :> Get '[JSON] [Book]
15+data MatrixParam (sym :: Symbol) a
16+ deriving (Typeable)
1217
1318-- | Lookup the values associated to the @sym@ matrix string parameter
1419-- and try to extract it as a value of type @[a]@. This is typically
@@ -19,9 +24,10 @@ data MatrixParam sym a
1924--
2025-- Example:
2126--
22−-- > -- /books;authors[]=<author1>;authors[]=<author2>;...
23−-- > type MyApi = "books" :> MatrixParams "authors" Text :> Get [Book]
24−data MatrixParams sym a
27+-- >>> -- /books;authors[]=<author1>;authors[]=<author2>;...
28+-- >>> type MyApi = "books" :> MatrixParams "authors" Text :> Get '[JSON] [Book]
29+data MatrixParams (sym :: Symbol) a
30+ deriving (Typeable)
2531
2632-- | Lookup a potentially value-less matrix string parameter
2733-- with boolean semantics. If the param @sym@ is there without any value,
@@ -30,6 +36,15 @@ data MatrixParams sym a
3036--
3137-- Example:
3238--
33−-- > -- /books;published
34−-- > type MyApi = "books" :> MatrixFlag "published" :> Get [Book]
35−data MatrixFlag sym
39+-- >>> -- /books;published
40+-- >>> type MyApi = "books" :> MatrixFlag "published" :> Get '[JSON] [Book]
41+data MatrixFlag (sym :: Symbol)
42+ deriving (Typeable)
43+
44+
45+-- $setup
46+-- >>> import Servant.API
47+-- >>> import Data.Aeson
48+-- >>> import Data.Text
49+-- >>> data Book
50+-- >>> instance ToJSON Book where { toJSON = undefined }
src/Servant/API/Patch.hs+16−9
@@ -1,9 +1,9 @@
1+{-# LANGUAGE DataKinds #-}
12{-# LANGUAGE DeriveDataTypeable #-}
2−{-# LANGUAGE DataKinds #-}
3−{-# LANGUAGE KindSignatures #-}
4−module Servant.API.Patch where
3+{-# LANGUAGE KindSignatures #-}
4+module Servant.API.Patch (Patch) where
55
6−import Data.Typeable ( Typeable )
6+import Data.Typeable (Typeable)
77
88-- | Endpoint for PATCH requests. The type variable represents the type of the
99-- response body (not the request body, use 'Servant.API.ReqBody.ReqBody' for
@@ -13,9 +13,16 @@ import Data.Typeable ( Typeable )
1313--
1414-- Example:
1515--
16−-- > -- PATCH /books
17−-- > -- with a JSON encoded Book as the request body
18−-- > -- returning the just-created Book
19−-- > type MyApi = "books" :> ReqBody Book :> Patch '[JSON] Book
20−data Patch (contentTypes::[*]) a
16+-- >>> -- PATCH /books
17+-- >>> -- with a JSON encoded Book as the request body
18+-- >>> -- returning the just-created Book
19+-- >>> type MyApi = "books" :> ReqBody '[JSON] Book :> Patch '[JSON] Book
20+data Patch (contentTypes :: [*]) a
2121 deriving Typeable
22+
23+-- $setup
24+-- >>> import Servant.API
25+-- >>> import Data.Aeson
26+-- >>> import Data.Text
27+-- >>> data Book
28+-- >>> instance ToJSON Book where { toJSON = undefined }
src/Servant/API/Post.hs+16−9
@@ -1,9 +1,9 @@
1−{-# LANGUAGE DataKinds #-}
1+{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveDataTypeable #-}
3−{-# LANGUAGE KindSignatures #-}
4−module Servant.API.Post where
3+{-# LANGUAGE KindSignatures #-}
4+module Servant.API.Post (Post) where
55
6−import Data.Typeable ( Typeable )
6+import Data.Typeable (Typeable)
77
88-- | Endpoint for POST requests. The type variable represents the type of the
99-- response body (not the request body, use 'Servant.API.ReqBody.ReqBody' for
@@ -11,9 +11,16 @@ import Data.Typeable ( Typeable )
1111--
1212-- Example:
1313--
14−-- > -- POST /books
15−-- > -- with a JSON encoded Book as the request body
16−-- > -- returning the just-created Book
17−-- > type MyApi = "books" :> ReqBody Book :> Post '[JSON] Book
18−data Post (contentTypes::[*]) a
14+-- >>> -- POST /books
15+-- >>> -- with a JSON encoded Book as the request body
16+-- >>> -- returning the just-created Book
17+-- >>> type MyApi = "books" :> ReqBody '[JSON] Book :> Post '[JSON] Book
18+data Post (contentTypes :: [*]) a
1919 deriving Typeable
20+
21+-- $setup
22+-- >>> import Servant.API
23+-- >>> import Data.Aeson
24+-- >>> import Data.Text
25+-- >>> data Book
26+-- >>> instance ToJSON Book where { toJSON = undefined }
src/Servant/API/Put.hs+12−5
@@ -1,7 +1,7 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DeriveDataTypeable #-}
33{-# LANGUAGE KindSignatures #-}
4−module Servant.API.Put where
4+module Servant.API.Put (Put) where
55
66import Data.Typeable ( Typeable )
77
@@ -10,8 +10,15 @@ import Data.Typeable ( Typeable )
1010--
1111-- Example:
1212--
13−-- > -- PUT /books/:isbn
14−-- > -- with a Book as request body, returning the updated Book
15−-- > type MyApi = "books" :> Capture "isbn" Text :> ReqBody Book :> Put '[JSON] Book
16−data Put (contentTypes::[*]) a
13+-- >>> -- PUT /books/:isbn
14+-- >>> -- with a Book as request body, returning the updated Book
15+-- >>> type MyApi = "books" :> Capture "isbn" Text :> ReqBody '[JSON] Book :> Put '[JSON] Book
16+data Put (contentTypes :: [*]) a
1717 deriving Typeable
18+
19+-- $setup
20+-- >>> import Servant.API
21+-- >>> import Data.Aeson
22+-- >>> import Data.Text
23+-- >>> data Book
24+-- >>> instance ToJSON Book where { toJSON = undefined }
src/Servant/API/QueryParam.hs+25−11
@@ -1,14 +1,20 @@
1−{-# LANGUAGE PolyKinds #-}
2−module Servant.API.QueryParam where
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE DeriveDataTypeable #-}
3+{-# LANGUAGE TypeOperators #-}
4+{-# LANGUAGE PolyKinds #-}
5+module Servant.API.QueryParam (QueryFlag, QueryParam, QueryParams) where
36
7+import Data.Typeable (Typeable)
8+import GHC.TypeLits (Symbol)
49-- | Lookup the value associated to the @sym@ query string parameter
510-- and try to extract it as a value of type @a@.
611--
712-- Example:
813--
9−-- > -- /books?author=<author name>
10−-- > type MyApi = "books" :> QueryParam "author" Text :> Get [Book]
11−data QueryParam sym a
14+-- >>> -- /books?author=<author name>
15+-- >>> type MyApi = "books" :> QueryParam "author" Text :> Get '[JSON] [Book]
16+data QueryParam (sym :: Symbol) a
17+ deriving Typeable
1218
1319-- | Lookup the values associated to the @sym@ query string parameter
1420-- and try to extract it as a value of type @[a]@. This is typically
@@ -19,9 +25,10 @@ data QueryParam sym a
1925--
2026-- Example:
2127--
22−-- > -- /books?authors[]=<author1>&authors[]=<author2>&...
23−-- > type MyApi = "books" :> QueryParams "authors" Text :> Get [Book]
24−data QueryParams sym a
28+-- >>> -- /books?authors[]=<author1>&authors[]=<author2>&...
29+-- >>> type MyApi = "books" :> QueryParams "authors" Text :> Get '[JSON] [Book]
30+data QueryParams (sym :: Symbol) a
31+ deriving Typeable
2532
2633-- | Lookup a potentially value-less query string parameter
2734-- with boolean semantics. If the param @sym@ is there without any value,
@@ -30,6 +37,13 @@ data QueryParams sym a
3037--
3138-- Example:
3239--
33−-- > -- /books?published
34−-- > type MyApi = "books" :> QueryFlag "published" :> Get [Book]
35−data QueryFlag sym
40+-- >>> -- /books?published
41+-- >>> type MyApi = "books" :> QueryFlag "published" :> Get '[JSON] [Book]
42+data QueryFlag (sym :: Symbol)
43+
44+-- $setup
45+-- >>> import Servant.API
46+-- >>> import Data.Aeson
47+-- >>> import Data.Text
48+-- >>> data Book
49+-- >>> instance ToJSON Book where { toJSON = undefined }
src/Servant/API/ReqBody.hs+16−5
@@ -1,11 +1,22 @@
1−{-# LANGUAGE DataKinds #-}
2−{-# LANGUAGE PolyKinds #-}
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE DeriveDataTypeable #-}
3+{-# LANGUAGE PolyKinds #-}
34module Servant.API.ReqBody where
45
6+import Data.Typeable (Typeable)
57-- | Extract the request body as a value of type @a@.
68--
79-- Example:
810--
9−-- > -- POST /books
10−-- > type MyApi = "books" :> ReqBody '[JSON] Book :> Post Book
11−data ReqBody (contentTypes::[*]) a
11+-- >>> -- POST /books
12+-- >>> type MyApi = "books" :> ReqBody '[JSON] Book :> Post '[JSON] Book
13+data ReqBody (contentTypes :: [*]) a
14+ deriving (Typeable)
15+
16+-- $setup
17+-- >>> import Servant.API
18+-- >>> import Servant.Common.Text
19+-- >>> import Data.Aeson
20+-- >>> import Data.Text
21+-- >>> data Book
22+-- >>> instance ToJSON Book where { toJSON = undefined }
src/Servant/API/Sub.hs+18−8
@@ -1,16 +1,26 @@
1−{-# LANGUAGE PolyKinds #-}
2−{-# LANGUAGE TypeOperators #-}
3−module Servant.API.Sub where
1+{-# LANGUAGE DeriveDataTypeable #-}
2+{-# LANGUAGE PolyKinds #-}
3+{-# LANGUAGE TypeOperators #-}
4+module Servant.API.Sub ((:>)) where
45
5−import Data.Proxy ( Proxy )
6+import Data.Typeable (Typeable)
67
78-- | The contained API (second argument) can be found under @("/" ++ path)@
89-- (path being the first argument).
910--
1011-- Example:
1112--
12−-- > -- GET /hello/world
13−-- > -- returning a JSON encoded World value
14−-- > type MyApi = "hello" :> "world" :> Get World
15−data (path :: k) :> a = Proxy path :> a
13+-- >>> -- GET /hello/world
14+-- >>> -- returning a JSON encoded World value
15+-- >>> type MyApi = "hello" :> "world" :> Get '[JSON] World
16+data (path :: k) :> a
17+ deriving (Typeable)
1618infixr 9 :>
19+
20+-- $setup
21+-- >>> import Servant.API
22+-- >>> import Servant.Common.Text
23+-- >>> import Data.Aeson
24+-- >>> import Data.Text
25+-- >>> data World
26+-- >>> instance ToJSON World where { toJSON = undefined }
src/Servant/Common/Text.hs+17−12
@@ -1,16 +1,16 @@
1−{-# LANGUAGE FlexibleInstances #-}
2−{-# LANGUAGE OverloadedStrings #-}
1+{-# LANGUAGE FlexibleInstances #-}
2+{-# LANGUAGE OverloadedStrings #-}
33{-# LANGUAGE TypeSynonymInstances #-}
44module Servant.Common.Text
55 ( FromText(..)
66 , ToText(..)
77 ) where
88
9−import Data.String.Conversions ( cs )
10−import Data.Int ( Int8, Int16, Int32, Int64 )
11−import Data.Text ( Text )
12−import Data.Text.Read ( rational, signed, decimal, Reader )
13−import Data.Word ( Word, Word8, Word16, Word32, Word64 )
9+import Data.Int (Int16, Int32, Int64, Int8)
10+import Data.String.Conversions (cs)
11+import Data.Text (Text)
12+import Data.Text.Read (Reader, decimal, rational, signed)
13+import Data.Word (Word, Word16, Word32, Word64, Word8)
1414
1515-- | For getting values from url captures and query string parameters
1616-- Instances should obey:
@@ -37,17 +37,22 @@ instance ToText String where
3737 toText = cs
3838
3939-- |
40−-- > fromText "true" = Just True
41−-- > fromText "false" = Just False
42−-- > fromText _ = Nothing
40+-- >>> fromText ("true"::Text) :: Maybe Bool
41+-- Just True
42+-- >>> fromText ("false"::Text) :: Maybe Bool
43+-- Just False
44+-- >>> fromText ("anything else"::Text) :: Maybe Bool
45+-- Nothing
4346instance FromText Bool where
4447 fromText "true" = Just True
4548 fromText "false" = Just False
4649 fromText _ = Nothing
4750
4851-- |
49−-- > toText True = "true"
50−-- > toText False = "false"
52+-- >>> toText True
53+-- "true"
54+-- >>> toText False
55+-- "false"
5156instance ToText Bool where
5257 toText True = "true"
5358 toText False = "false"
test-docs.shdeleted−3
@@ -1,3 +0,0 @@
1−#!/bin/sh
2−
3−doctest -isrc -optP-include -optPdist/build/autogen/cabal_macros.h $(find src/ -name '*.hs')
test/Doctests.hsnew file+16
@@ -0,0 +1,16 @@
1+module Main where
2+
3+import System.FilePath.Find
4+import Test.DocTest
5+
6+main :: IO ()
7+main = do
8+ files <- find always (extension ==? ".hs") "src"
9+ doctest $ [ "-isrc"
10+ , "-optP-include"
11+ , "-optPdist/build/autogen/cabal_macros.h"
12+ , "-XOverloadedStrings"
13+ , "-XFlexibleInstances"
14+ , "-XMultiParamTypeClasses"
15+ ] ++ files
16+
test/Servant/Common/TextSpec.hs+7−7
@@ -1,12 +1,12 @@
11module Servant.Common.TextSpec where
22
3−import Servant.Common.Text
4−import Test.Hspec
5−import Test.QuickCheck
6−import Test.QuickCheck.Instances ()
7−import Data.Int ( Int8, Int16, Int32, Int64 )
8−import Data.Text ( Text )
9−import Data.Word ( Word, Word8, Word16, Word32, Word64 )
3+import Data.Int (Int16, Int32, Int64, Int8)
4+import Data.Text (Text)
5+import Data.Word (Word, Word16, Word32, Word64, Word8)
6+import Servant.Common.Text
7+import Test.Hspec
8+import Test.QuickCheck
9+import Test.QuickCheck.Instances ()
1010
1111spec :: Spec
1212spec = describe "Servant.Common.Text" $ do