git.iko.soy/typeable/servant
on master help sign in
Cleanup, including:
635902eJulian K. Arni
.travis.yml+1−3
@@ -7,14 +7,12 @@ script:
7 - cabal configure --enable-tests --enable-library-coverage && cabal build && cabal test
7 - cabal configure --enable-tests --enable-library-coverage && cabal build && cabal test
8 - cabal check
8 - cabal check
9 - cabal sdist
9 - cabal sdist
10− - cabal install doctest
11− - ./test-docs.sh
12
10
13after_script:
11after_script:
14 - |
12 - |
15 if [ "$TRAVIS_PULL_REQUEST" -eq "$TRAVIS_PULL_REQUEST" ] 2>/dev/null || [ "$TRAVIS_BRANCH" == "master" ] ; then
13 if [ "$TRAVIS_PULL_REQUEST" -eq "$TRAVIS_PULL_REQUEST" ] 2>/dev/null || [ "$TRAVIS_BRANCH" == "master" ] ; then
16 cabal install hpc-coveralls
14 cabal install hpc-coveralls
17− hpc-coveralls --exclude-dir=test spec
15+ hpc-coveralls --exclude-dir=test spec doctests
18 fi
16 fi
19
17
20
18
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
97 , string-conversions
97 , string-conversions
98 , text
98 , text
99 , url
99 , 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 @@
1module Servant.API (
1module Servant.API (
2
2
3 -- * Combinators
3 -- * Combinators
4− -- | Type-level combinator for expressing subrouting: @':>'@
5 module Servant.API.Sub,
4 module Servant.API.Sub,
6− -- | Type-level combinator for alternative endpoints: @':<|>'@
5+ -- | Type-level combinator for expressing subrouting: @':>'@
7 module Servant.API.Alternative,
6 module Servant.API.Alternative,
7+ -- | Type-level combinator for alternative endpoints: @':<|>'@
8
8
9 -- * Accessing information from the request
9 -- * Accessing information from the request
10− -- | Capturing parts of the url path as parsed values: @'Capture'@
11 module Servant.API.Capture,
10 module Servant.API.Capture,
12− -- | Retrieving specific headers from the request
11+ -- | Capturing parts of the url path as parsed values: @'Capture'@
13 module Servant.API.Header,
12 module Servant.API.Header,
14− -- | Retrieving parameters from the query string of the 'URI': @'QueryParam'@
13+ -- | Retrieving specific headers from the request
15 module Servant.API.QueryParam,
14 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'@
17 module Servant.API.ReqBody,
16 module Servant.API.ReqBody,
18− -- | Retrieving matrix parameters from the 'URI' segment: @'MatrixParam'@
17+ -- | Accessing the request body as a JSON-encoded type: @'ReqBody'@
19 module Servant.API.MatrixParam,
18 module Servant.API.MatrixParam,
19+ -- | Retrieving matrix parameters from the 'URI' segment: @'MatrixParam'@
20
20
21 -- * Actual endpoints, distinguished by HTTP method
21 -- * Actual endpoints, distinguished by HTTP method
22− -- | GET requests
23 module Servant.API.Get,
22 module Servant.API.Get,
24− -- | POST requests
23+ -- | @GET@ requests
25 module Servant.API.Post,
24 module Servant.API.Post,
26− -- | DELETE requests
25+ -- | @POST@ requests
27 module Servant.API.Delete,
26 module Servant.API.Delete,
28− -- | PUT requests
27+ -- | @DELETE@ requests
29 module Servant.API.Put,
28 module Servant.API.Put,
30− -- | PATCH requests
29+ -- | @PUT@ requests
31 module Servant.API.Patch,
30 module Servant.API.Patch,
31+ -- | @PATCH@ requests
32
32
33 -- * Content Types
33 -- * Content Types
34 module Servant.API.ContentTypes,
34 module Servant.API.ContentTypes,
35+ -- | Serializing and deserializing types based on @Accept@ and
36+ -- @Content-Type@ headers.
35
37
36 -- * Untyped endpoints
38 -- * Untyped endpoints
37− -- | Plugging in a wai 'Network.Wai.Application', serving directories
38 module Servant.API.Raw,
39 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@
39
45
40 -- * Utilities
46 -- * Utilities
41− -- | Type-safe internal URIs
42 module Servant.Utils.Links,
47 module Servant.Utils.Links,
48+ -- | Type-safe internal URIs
43 ) where
49 ) where
44
50
51+import Servant.Common.Text (FromText(..), ToText(..))
45−import Servant.API.Alternative ( (:<|>)(..) )
52+import Servant.API.Alternative ((:<|>) (..))
46−import Servant.API.Capture ( Capture )
53+import Servant.API.Capture (Capture)
47−import Servant.API.ContentTypes ( JSON , PlainText, OctetStream
54+import Servant.API.ContentTypes (JSON, MimeRender (..),
48− , MimeRender(..) , MimeUnrender(..))
55+ MimeUnrender (..), OctetStream,
56+ PlainText)
49−import Servant.API.Delete ( Delete )
57+import Servant.API.Delete (Delete)
50−import Servant.API.Get ( Get )
58+import Servant.API.Get (Get)
51−import Servant.API.Header ( Header )
59+import Servant.API.Header (Header)
60+import Servant.API.MatrixParam (MatrixFlag, MatrixParam,
61+ MatrixParams)
52−import Servant.API.Post ( Post )
62+import Servant.API.Patch (Patch)
53−import Servant.API.Put ( Put )
63+import Servant.API.Post (Post)
54−import Servant.API.Patch ( Patch )
64+import Servant.API.Put (Put)
55−import Servant.API.QueryParam ( QueryFlag, QueryParams, QueryParam )
65+import Servant.API.QueryParam (QueryFlag, QueryParam, QueryParams)
56−import Servant.API.MatrixParam ( MatrixFlag, MatrixParams, MatrixParam )
57−import Servant.API.Raw ( Raw )
66+import Servant.API.Raw (Raw)
58−import Servant.API.ReqBody ( ReqBody )
67+import Servant.API.ReqBody (ReqBody)
59−import Servant.API.Sub ( (:>)(..) )
68+import Servant.API.Sub ((:>))
60−import Servant.Utils.Links ( safeLink, URI(..), IsElem, IsElem', HasLink(..) )
69+import Servant.Utils.Links (HasLink (..), IsElem, IsElem',
70+ URI (..), safeLink)
src/Servant/API/Alternative.hs+21−4
@@ -1,11 +1,28 @@
1+{-# LANGUAGE DeriveDataTypeable #-}
1−{-# LANGUAGE TypeOperators #-}
2+{-# LANGUAGE TypeOperators #-}
2−module Servant.API.Alternative where
3+module Servant.API.Alternative ((:<|>)(..)) where
3
4
5+import Data.Monoid (Monoid (..))
6+import Data.Typeable (Typeable)
4-- | Union of two APIs, first takes precedence in case of overlap.
7-- | Union of two APIs, first takes precedence in case of overlap.
5--
8--
6-- Example:
9-- Example:
7--
10--
11+-- >>> :{
8−-- > type MyApi = "books" :> Get [Book] -- GET /books
12+--type MyApi = "books" :> Get '[JSON] [Book] -- GET /books
9−-- > :<|> "books" :> ReqBody Book :> Post Book -- POST /books
13+-- :<|> "books" :> ReqBody '[JSON] Book :> Post '[JSON] () -- POST /books
14+-- :}
10data a :<|> b = a :<|> b
15data a :<|> b = a :<|> b
16+ deriving (Typeable, Eq, Show)
11infixr 8 :<|>
17infixr 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 DataKinds #-}
2+{-# LANGUAGE DeriveDataTypeable #-}
1−{-# LANGUAGE PolyKinds #-}
3+{-# LANGUAGE PolyKinds #-}
2module Servant.API.Capture (Capture) where
4module Servant.API.Capture (Capture) where
3
5
6+import Data.Typeable (Typeable)
7+import GHC.TypeLits (Symbol)
4-- | Capture a value from the request path under a certain type @a@.
8-- | Capture a value from the request path under a certain type @a@.
5--
9--
6-- Example:
10-- Example:
7−--
8−-- > -- GET /books/:isbn
11+-- >>> -- GET /books/:isbn
9−-- > type MyApi = "books" :> Capture "isbn" Text :> Get Book
12+-- >>> type MyApi = "books" :> Capture "isbn" Text :> Get '[JSON] Book
10−data Capture sym a
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 @@
9{-# LANGUAGE TypeFamilies #-}
9{-# LANGUAGE TypeFamilies #-}
10{-# LANGUAGE TypeOperators #-}
10{-# LANGUAGE TypeOperators #-}
11{-# LANGUAGE UndecidableInstances #-}
11{-# LANGUAGE UndecidableInstances #-}
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.
12−module Servant.API.ContentTypes where
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
13
63
14import Control.Applicative ((<*))
64import Control.Applicative ((<*))
15import Control.Arrow (left)
65import Control.Arrow (left)
@@ -48,8 +98,12 @@ data OctetStream deriving Typeable
48--
98--
49-- Example:
99-- Example:
50--
100--
101+-- >>> import Network.HTTP.Media ((//), (/:))
102+-- >>> data HTML
103+-- >>> :{
51−-- > instance Accept HTML where
104+--instance Accept HTML where
52−-- > contentType _ = "text" // "html"
105+-- contentType _ = "text" // "html" /: ("charset", "utf-8")
106+-- :}
53--
107--
54class Accept ctype where
108class Accept ctype where
55 contentType :: Proxy ctype -> M.MediaType
109 contentType :: Proxy ctype -> M.MediaType
@@ -93,7 +147,7 @@ newtype AcceptHeader = AcceptHeader BS.ByteString
93class Accept ctype => MimeRender ctype a where
147class Accept ctype => MimeRender ctype a where
94 toByteString :: Proxy ctype -> a -> ByteString
148 toByteString :: Proxy ctype -> a -> ByteString
95
149
96−class AllCTRender list a where
150+class AllCTRender (list :: [*]) a where
97 -- If the Accept header can be matched, returns (Just) a tuple of the
151 -- If the Accept header can be matched, returns (Just) a tuple of the
98 -- Content-Type and response (serialization of @a@ into the appropriate
152 -- Content-Type and response (serialization of @a@ into the appropriate
99 -- mimetype).
153 -- mimetype).
@@ -113,20 +167,28 @@ instance ( AllMimeRender ctyps a, IsNonEmpty ctyps
113-- | Instantiate this class to register a way of deserializing a type based
167-- | Instantiate this class to register a way of deserializing a type based
114-- on the request's @Content-Type@ header.
168-- on the request's @Content-Type@ header.
115--
169--
170+-- >>> import Network.HTTP.Media hiding (Accept)
171+-- >>> import qualified Data.ByteString.Lazy.Char8 as BSC
116−-- > data MyContentType = MyContentType String
172+-- >>> data MyContentType = MyContentType String
117−-- >
173+--
174+-- >>> :{
118−-- > instance Accept MyContentType where
175+--instance Accept MyContentType where
119−-- > contentType _ = "example" // "prs.me.mine" /: ("charset", "utf-8")
176+-- contentType _ = "example" // "prs.me.mine" /: ("charset", "utf-8")
120−-- >
177+-- :}
178+--
179+-- >>> :{
121−-- > instance Show a => MimeUnrender MyContentType where
180+--instance Read a => MimeUnrender MyContentType a where
181+-- fromByteString _ bs = case BSC.take 12 bs of
122−-- > fromByteString _ bs = MyContentType $ unpack bs
182+-- "MyContentType" -> return . read . BSC.unpack $ BSC.drop 12 bs
123−-- >
183+-- _ -> Left "didn't start with the magic incantation"
184+-- :}
185+--
124−-- > type MyAPI = "path" :> ReqBody '[MyContentType] :> Get '[JSON] Int
186+-- >>> type MyAPI = "path" :> ReqBody '[MyContentType] Int :> Get '[JSON] Int
125--
187--
126class Accept ctype => MimeUnrender ctype a where
188class Accept ctype => MimeUnrender ctype a where
127 fromByteString :: Proxy ctype -> ByteString -> Either String a
189 fromByteString :: Proxy ctype -> ByteString -> Either String a
128
190
129−class (IsNonEmpty list) => AllCTUnrender list a where
191+class (IsNonEmpty list) => AllCTUnrender (list :: [*]) a where
130 handleCTypeH :: Proxy list
192 handleCTypeH :: Proxy list
131 -> ByteString -- Content-Type header
193 -> ByteString -- Content-Type header
132 -> ByteString -- Request body
194 -> ByteString -- Request body
@@ -144,8 +206,8 @@ instance ( AllMimeUnrender ctyps a, IsNonEmpty ctyps
144--------------------------------------------------------------------------
206--------------------------------------------------------------------------
145-- Check that all elements of list are instances of MimeRender
207-- Check that all elements of list are instances of MimeRender
146--------------------------------------------------------------------------
208--------------------------------------------------------------------------
147−class AllMimeRender ls a where
209+class AllMimeRender (list :: [*]) a where
148− allMimeRender :: Proxy ls
210+ allMimeRender :: Proxy list
149 -> a -- value to serialize
211 -> a -- value to serialize
150 -> [(M.MediaType, ByteString)] -- content-types/response pairs
212 -> [(M.MediaType, ByteString)] -- content-types/response pairs
151
213
@@ -168,8 +230,10 @@ instance AllMimeRender '[] a where
168--------------------------------------------------------------------------
230--------------------------------------------------------------------------
169-- Check that all elements of list are instances of MimeUnrender
231-- Check that all elements of list are instances of MimeUnrender
170--------------------------------------------------------------------------
232--------------------------------------------------------------------------
171−class AllMimeUnrender ls a where
233+class AllMimeUnrender (list :: [*]) a where
234+ allMimeUnrender :: Proxy list
235+ -> ByteString
172− allMimeUnrender :: Proxy ls -> ByteString -> [(M.MediaType, Either String a)]
236+ -> [(M.MediaType, Either String a)]
173
237
174instance AllMimeUnrender '[] a where
238instance AllMimeUnrender '[] a where
175 allMimeUnrender _ _ = []
239 allMimeUnrender _ _ = []
@@ -182,7 +246,7 @@ instance ( MimeUnrender ctyp a
182 where pctyp = Proxy :: Proxy ctyp
246 where pctyp = Proxy :: Proxy ctyp
183 pctyps = Proxy :: Proxy ctyps
247 pctyps = Proxy :: Proxy ctyps
184
248
185−type family IsNonEmpty (ls::[*]) :: Constraint where
249+type family IsNonEmpty (list :: [*]) :: Constraint where
186 IsNonEmpty (x ': xs) = ()
250 IsNonEmpty (x ': xs) = ()
187
251
188
252
@@ -193,9 +257,9 @@ type family IsNonEmpty (ls::[*]) :: Constraint where
193instance ToJSON a => MimeRender JSON a where
257instance ToJSON a => MimeRender JSON a where
194 toByteString _ = encode
258 toByteString _ = encode
195
259
196−-- | `encodeFormUrlEncoded . toFormUrlEncoded`
260+-- | @encodeFormUrlEncoded . toFormUrlEncoded@
197−-- Note that the `fromByteString p (toByteString p x) == Right x` law only
261+-- 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 `("", "")`)
262+-- holds if every element of x is non-null (i.e., not @("", "")@)
199instance ToFormUrlEncoded a => MimeRender FormUrlEncoded a where
263instance ToFormUrlEncoded a => MimeRender FormUrlEncoded a where
200 toByteString _ = encodeFormUrlEncoded . toFormUrlEncoded
264 toByteString _ = encodeFormUrlEncoded . toFormUrlEncoded
201
265
@@ -203,11 +267,11 @@ instance ToFormUrlEncoded a => MimeRender FormUrlEncoded a where
203instance MimeRender PlainText TextL.Text where
267instance MimeRender PlainText TextL.Text where
204 toByteString _ = TextL.encodeUtf8
268 toByteString _ = TextL.encodeUtf8
205
269
206−-- | `fromStrict . TextS.encodeUtf8`
270+-- | @fromStrict . TextS.encodeUtf8@
207instance MimeRender PlainText TextS.Text where
271instance MimeRender PlainText TextS.Text where
208 toByteString _ = fromStrict . TextS.encodeUtf8
272 toByteString _ = fromStrict . TextS.encodeUtf8
209
273
210−-- | `id`
274+-- | @id@
211instance MimeRender OctetStream ByteString where
275instance MimeRender OctetStream ByteString where
212 toByteString _ = id
276 toByteString _ = id
213
277
@@ -230,25 +294,25 @@ eitherDecodeLenient input = do
230instance FromJSON a => MimeUnrender JSON a where
294instance FromJSON a => MimeUnrender JSON a where
231 fromByteString _ = eitherDecodeLenient
295 fromByteString _ = eitherDecodeLenient
232
296
233−-- | `decodeFormUrlEncoded >=> fromFormUrlEncoded`
297+-- | @decodeFormUrlEncoded >=> fromFormUrlEncoded@
234−-- Note that the `fromByteString p (toByteString p x) == Right x` law only
298+-- 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 `("", "")`)
299+-- holds if every element of x is non-null (i.e., not @("", "")@)
236instance FromFormUrlEncoded a => MimeUnrender FormUrlEncoded a where
300instance FromFormUrlEncoded a => MimeUnrender FormUrlEncoded a where
237 fromByteString _ = decodeFormUrlEncoded >=> fromFormUrlEncoded
301 fromByteString _ = decodeFormUrlEncoded >=> fromFormUrlEncoded
238
302
239−-- | `left show . TextL.decodeUtf8'`
303+-- | @left show . TextL.decodeUtf8'@
240instance MimeUnrender PlainText TextL.Text where
304instance MimeUnrender PlainText TextL.Text where
241 fromByteString _ = left show . TextL.decodeUtf8'
305 fromByteString _ = left show . TextL.decodeUtf8'
242
306
243−-- | `left show . TextS.decodeUtf8' . toStrict`
307+-- | @left show . TextS.decodeUtf8' . toStrict@
244instance MimeUnrender PlainText TextS.Text where
308instance MimeUnrender PlainText TextS.Text where
245 fromByteString _ = left show . TextS.decodeUtf8' . toStrict
309 fromByteString _ = left show . TextS.decodeUtf8' . toStrict
246
310
247−-- | `Right . id`
311+-- | @Right . id@
248instance MimeUnrender OctetStream ByteString where
312instance MimeUnrender OctetStream ByteString where
249 fromByteString _ = Right . id
313 fromByteString _ = Right . id
250
314
251−-- | `Right . toStrict`
315+-- | @Right . toStrict@
252instance MimeUnrender OctetStream BS.ByteString where
316instance MimeUnrender OctetStream BS.ByteString where
253 fromByteString _ = Right . toStrict
317 fromByteString _ = Right . toStrict
254
318
@@ -296,3 +360,10 @@ decodeFormUrlEncoded q = do
296 unescape :: TextS.Text -> TextS.Text
360 unescape :: TextS.Text -> TextS.Text
297 unescape = cs . unEscapeString . cs . TextS.intercalate "%20" . TextS.splitOn "+"
361 unescape = cs . unEscapeString . cs . TextS.intercalate "%20" . TextS.splitOn "+"
298 mapM parsePair xs
362 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 @@
1{-# LANGUAGE DeriveDataTypeable #-}
1{-# LANGUAGE DeriveDataTypeable #-}
2−module Servant.API.Delete where
2+module Servant.API.Delete (Delete) where
3
3
4import Data.Typeable ( Typeable )
4import Data.Typeable ( Typeable )
5
5
@@ -7,7 +7,15 @@ import Data.Typeable ( Typeable )
7--
7--
8-- Example:
8-- Example:
9--
9--
10−-- > -- DELETE /books/:isbn
10+-- >>> -- DELETE /books/:isbn
11−-- > type MyApi = "books" :> Capture "isbn" Text :> Delete
11+-- >>> type MyApi = "books" :> Capture "isbn" Text :> Delete
12data Delete
12data Delete
13 deriving Typeable
13 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 #-}
1{-# LANGUAGE DeriveDataTypeable #-}
2{-# LANGUAGE DeriveDataTypeable #-}
2−{-# LANGUAGE DataKinds #-}
3−{-# LANGUAGE KindSignatures #-}
3+{-# LANGUAGE KindSignatures #-}
4−module Servant.API.Get where
4+module Servant.API.Get (Get) where
5
5
6−import Data.Typeable ( Typeable )
6+import Data.Typeable (Typeable)
7
7
8-- | Endpoint for simple GET requests. Serves the result as JSON.
8-- | Endpoint for simple GET requests. Serves the result as JSON.
9--
9--
10-- Example:
10-- Example:
11--
11--
12−-- > type MyApi = "books" :> Get '[JSON] [Book]
12+-- >>> type MyApi = "books" :> Get '[JSON] [Book]
13−data Get (contentTypes::[*]) a
13+data Get (contentTypes :: [*]) a
14 deriving Typeable
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/Header.hs+20−8
@@ -1,13 +1,25 @@
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE DeriveDataTypeable #-}
1−{-# LANGUAGE PolyKinds #-}
3+{-# LANGUAGE PolyKinds #-}
2−module Servant.API.Header where
4+module Servant.API.Header (Header) where
3
5
6+import Data.Typeable (Typeable)
7+import GHC.TypeLits (Symbol)
4-- | Extract the given header's value as a value of type @a@.
8-- | Extract the given header's value as a value of type @a@.
5--
9--
6-- Example:
10-- Example:
7--
11--
8−-- > newtype Referer = Referer Text
12+-- >>> newtype Referer = Referer Text deriving (Eq, Show)
9−-- > deriving (Eq, Show, FromText, ToText)
10−-- >
13+-- >>>
11−-- > -- GET /view-my-referer
14+-- >>> -- GET /view-my-referer
12−-- > type MyApi = "view-my-referer" :> Header "from" Referer :> Get Referer
15+-- >>> type MyApi = "view-my-referer" :> Header "from" Referer :> Get '[JSON] Referer
13−data Header sym a
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 DataKinds #-}
2+{-# LANGUAGE DeriveDataTypeable #-}
1−{-# LANGUAGE PolyKinds #-}
3+{-# LANGUAGE PolyKinds #-}
2−module Servant.API.MatrixParam where
4+module Servant.API.MatrixParam (MatrixFlag, MatrixParam, MatrixParams) where
3
5
6+import Data.Typeable (Typeable)
7+import GHC.TypeLits (Symbol)
4-- | Lookup the value associated to the @sym@ matrix string parameter
8-- | Lookup the value associated to the @sym@ matrix string parameter
5-- and try to extract it as a value of type @a@.
9-- and try to extract it as a value of type @a@.
6--
10--
7-- Example:
11-- Example:
8--
12--
9−-- > -- /books;author=<author name>
13+-- >>> -- /books;author=<author name>
10−-- > type MyApi = "books" :> MatrixParam "author" Text :> Get [Book]
14+-- >>> type MyApi = "books" :> MatrixParam "author" Text :> Get '[JSON] [Book]
11−data MatrixParam sym a
15+data MatrixParam (sym :: Symbol) a
16+ deriving (Typeable)
12
17
13-- | Lookup the values associated to the @sym@ matrix string parameter
18-- | Lookup the values associated to the @sym@ matrix string parameter
14-- and try to extract it as a value of type @[a]@. This is typically
19-- and try to extract it as a value of type @[a]@. This is typically
@@ -19,9 +24,10 @@ data MatrixParam sym a
19--
24--
20-- Example:
25-- Example:
21--
26--
22−-- > -- /books;authors[]=<author1>;authors[]=<author2>;...
27+-- >>> -- /books;authors[]=<author1>;authors[]=<author2>;...
23−-- > type MyApi = "books" :> MatrixParams "authors" Text :> Get [Book]
28+-- >>> type MyApi = "books" :> MatrixParams "authors" Text :> Get '[JSON] [Book]
24−data MatrixParams sym a
29+data MatrixParams (sym :: Symbol) a
30+ deriving (Typeable)
25
31
26-- | Lookup a potentially value-less matrix string parameter
32-- | Lookup a potentially value-less matrix string parameter
27-- with boolean semantics. If the param @sym@ is there without any value,
33-- with boolean semantics. If the param @sym@ is there without any value,
@@ -30,6 +36,15 @@ data MatrixParams sym a
30--
36--
31-- Example:
37-- Example:
32--
38--
33−-- > -- /books;published
39+-- >>> -- /books;published
34−-- > type MyApi = "books" :> MatrixFlag "published" :> Get [Book]
40+-- >>> type MyApi = "books" :> MatrixFlag "published" :> Get '[JSON] [Book]
35−data MatrixFlag sym
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 #-}
1{-# LANGUAGE DeriveDataTypeable #-}
2{-# LANGUAGE DeriveDataTypeable #-}
2−{-# LANGUAGE DataKinds #-}
3−{-# LANGUAGE KindSignatures #-}
3+{-# LANGUAGE KindSignatures #-}
4−module Servant.API.Patch where
4+module Servant.API.Patch (Patch) where
5
5
6−import Data.Typeable ( Typeable )
6+import Data.Typeable (Typeable)
7
7
8-- | Endpoint for PATCH requests. The type variable represents the type of the
8-- | Endpoint for PATCH requests. The type variable represents the type of the
9-- response body (not the request body, use 'Servant.API.ReqBody.ReqBody' for
9-- response body (not the request body, use 'Servant.API.ReqBody.ReqBody' for
@@ -13,9 +13,16 @@ import Data.Typeable ( Typeable )
13--
13--
14-- Example:
14-- Example:
15--
15--
16−-- > -- PATCH /books
16+-- >>> -- PATCH /books
17−-- > -- with a JSON encoded Book as the request body
17+-- >>> -- with a JSON encoded Book as the request body
18−-- > -- returning the just-created Book
18+-- >>> -- returning the just-created Book
19−-- > type MyApi = "books" :> ReqBody Book :> Patch '[JSON] Book
19+-- >>> type MyApi = "books" :> ReqBody '[JSON] Book :> Patch '[JSON] Book
20−data Patch (contentTypes::[*]) a
20+data Patch (contentTypes :: [*]) a
21 deriving Typeable
21 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 #-}
2{-# LANGUAGE DeriveDataTypeable #-}
2{-# LANGUAGE DeriveDataTypeable #-}
3−{-# LANGUAGE KindSignatures #-}
3+{-# LANGUAGE KindSignatures #-}
4−module Servant.API.Post where
4+module Servant.API.Post (Post) where
5
5
6−import Data.Typeable ( Typeable )
6+import Data.Typeable (Typeable)
7
7
8-- | Endpoint for POST requests. The type variable represents the type of the
8-- | Endpoint for POST requests. The type variable represents the type of the
9-- response body (not the request body, use 'Servant.API.ReqBody.ReqBody' for
9-- response body (not the request body, use 'Servant.API.ReqBody.ReqBody' for
@@ -11,9 +11,16 @@ import Data.Typeable ( Typeable )
11--
11--
12-- Example:
12-- Example:
13--
13--
14−-- > -- POST /books
14+-- >>> -- POST /books
15−-- > -- with a JSON encoded Book as the request body
15+-- >>> -- with a JSON encoded Book as the request body
16−-- > -- returning the just-created Book
16+-- >>> -- returning the just-created Book
17−-- > type MyApi = "books" :> ReqBody Book :> Post '[JSON] Book
17+-- >>> type MyApi = "books" :> ReqBody '[JSON] Book :> Post '[JSON] Book
18−data Post (contentTypes::[*]) a
18+data Post (contentTypes :: [*]) a
19 deriving Typeable
19 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 @@
1{-# LANGUAGE DataKinds #-}
1{-# LANGUAGE DataKinds #-}
2{-# LANGUAGE DeriveDataTypeable #-}
2{-# LANGUAGE DeriveDataTypeable #-}
3{-# LANGUAGE KindSignatures #-}
3{-# LANGUAGE KindSignatures #-}
4−module Servant.API.Put where
4+module Servant.API.Put (Put) where
5
5
6import Data.Typeable ( Typeable )
6import Data.Typeable ( Typeable )
7
7
@@ -10,8 +10,15 @@ import Data.Typeable ( Typeable )
10--
10--
11-- Example:
11-- Example:
12--
12--
13−-- > -- PUT /books/:isbn
13+-- >>> -- PUT /books/:isbn
14−-- > -- with a Book as request body, returning the updated Book
14+-- >>> -- with a Book as request body, returning the updated Book
15−-- > type MyApi = "books" :> Capture "isbn" Text :> ReqBody Book :> Put '[JSON] Book
15+-- >>> type MyApi = "books" :> Capture "isbn" Text :> ReqBody '[JSON] Book :> Put '[JSON] Book
16−data Put (contentTypes::[*]) a
16+data Put (contentTypes :: [*]) a
17 deriving Typeable
17 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 DataKinds #-}
2+{-# LANGUAGE DeriveDataTypeable #-}
3+{-# LANGUAGE TypeOperators #-}
1−{-# LANGUAGE PolyKinds #-}
4+{-# LANGUAGE PolyKinds #-}
2−module Servant.API.QueryParam where
5+module Servant.API.QueryParam (QueryFlag, QueryParam, QueryParams) where
3
6
7+import Data.Typeable (Typeable)
8+import GHC.TypeLits (Symbol)
4-- | Lookup the value associated to the @sym@ query string parameter
9-- | Lookup the value associated to the @sym@ query string parameter
5-- and try to extract it as a value of type @a@.
10-- and try to extract it as a value of type @a@.
6--
11--
7-- Example:
12-- Example:
8--
13--
9−-- > -- /books?author=<author name>
14+-- >>> -- /books?author=<author name>
10−-- > type MyApi = "books" :> QueryParam "author" Text :> Get [Book]
15+-- >>> type MyApi = "books" :> QueryParam "author" Text :> Get '[JSON] [Book]
11−data QueryParam sym a
16+data QueryParam (sym :: Symbol) a
17+ deriving Typeable
12
18
13-- | Lookup the values associated to the @sym@ query string parameter
19-- | Lookup the values associated to the @sym@ query string parameter
14-- and try to extract it as a value of type @[a]@. This is typically
20-- and try to extract it as a value of type @[a]@. This is typically
@@ -19,9 +25,10 @@ data QueryParam sym a
19--
25--
20-- Example:
26-- Example:
21--
27--
22−-- > -- /books?authors[]=<author1>&authors[]=<author2>&...
28+-- >>> -- /books?authors[]=<author1>&authors[]=<author2>&...
23−-- > type MyApi = "books" :> QueryParams "authors" Text :> Get [Book]
29+-- >>> type MyApi = "books" :> QueryParams "authors" Text :> Get '[JSON] [Book]
24−data QueryParams sym a
30+data QueryParams (sym :: Symbol) a
31+ deriving Typeable
25
32
26-- | Lookup a potentially value-less query string parameter
33-- | Lookup a potentially value-less query string parameter
27-- with boolean semantics. If the param @sym@ is there without any value,
34-- with boolean semantics. If the param @sym@ is there without any value,
@@ -30,6 +37,13 @@ data QueryParams sym a
30--
37--
31-- Example:
38-- Example:
32--
39--
33−-- > -- /books?published
40+-- >>> -- /books?published
34−-- > type MyApi = "books" :> QueryFlag "published" :> Get [Book]
41+-- >>> type MyApi = "books" :> QueryFlag "published" :> Get '[JSON] [Book]
35−data QueryFlag sym
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 #-}
1+{-# LANGUAGE DataKinds #-}
2+{-# LANGUAGE DeriveDataTypeable #-}
2−{-# LANGUAGE PolyKinds #-}
3+{-# LANGUAGE PolyKinds #-}
3module Servant.API.ReqBody where
4module Servant.API.ReqBody where
4
5
6+import Data.Typeable (Typeable)
5-- | Extract the request body as a value of type @a@.
7-- | Extract the request body as a value of type @a@.
6--
8--
7-- Example:
9-- Example:
8--
10--
9−-- > -- POST /books
11+-- >>> -- POST /books
10−-- > type MyApi = "books" :> ReqBody '[JSON] Book :> Post Book
12+-- >>> type MyApi = "books" :> ReqBody '[JSON] Book :> Post '[JSON] Book
11−data ReqBody (contentTypes::[*]) a
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 DeriveDataTypeable #-}
1−{-# LANGUAGE PolyKinds #-}
2+{-# LANGUAGE PolyKinds #-}
2−{-# LANGUAGE TypeOperators #-}
3+{-# LANGUAGE TypeOperators #-}
3−module Servant.API.Sub where
4+module Servant.API.Sub ((:>)) where
4
5
5−import Data.Proxy ( Proxy )
6+import Data.Typeable (Typeable)
6
7
7-- | The contained API (second argument) can be found under @("/" ++ path)@
8-- | The contained API (second argument) can be found under @("/" ++ path)@
8-- (path being the first argument).
9-- (path being the first argument).
9--
10--
10-- Example:
11-- Example:
11--
12--
12−-- > -- GET /hello/world
13+-- >>> -- GET /hello/world
13−-- > -- returning a JSON encoded World value
14+-- >>> -- returning a JSON encoded World value
14−-- > type MyApi = "hello" :> "world" :> Get World
15+-- >>> type MyApi = "hello" :> "world" :> Get '[JSON] World
15−data (path :: k) :> a = Proxy path :> a
16+data (path :: k) :> a
17+ deriving (Typeable)
16infixr 9 :>
18infixr 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 #-}
1+{-# LANGUAGE FlexibleInstances #-}
2−{-# LANGUAGE OverloadedStrings #-}
2+{-# LANGUAGE OverloadedStrings #-}
3{-# LANGUAGE TypeSynonymInstances #-}
3{-# LANGUAGE TypeSynonymInstances #-}
4module Servant.Common.Text
4module Servant.Common.Text
5 ( FromText(..)
5 ( FromText(..)
6 , ToText(..)
6 , ToText(..)
7 ) where
7 ) where
8
8
9−import Data.String.Conversions ( cs )
10−import Data.Int ( Int8, Int16, Int32, Int64 )
9+import Data.Int (Int16, Int32, Int64, Int8)
10+import Data.String.Conversions (cs)
11−import Data.Text ( Text )
11+import Data.Text (Text)
12−import Data.Text.Read ( rational, signed, decimal, Reader )
12+import Data.Text.Read (Reader, decimal, rational, signed)
13−import Data.Word ( Word, Word8, Word16, Word32, Word64 )
13+import Data.Word (Word, Word16, Word32, Word64, Word8)
14
14
15-- | For getting values from url captures and query string parameters
15-- | For getting values from url captures and query string parameters
16-- Instances should obey:
16-- Instances should obey:
@@ -37,17 +37,22 @@ instance ToText String where
37 toText = cs
37 toText = cs
38
38
39-- |
39-- |
40+-- >>> fromText ("true"::Text) :: Maybe Bool
40−-- > fromText "true" = Just True
41+-- Just True
42+-- >>> fromText ("false"::Text) :: Maybe Bool
41−-- > fromText "false" = Just False
43+-- Just False
44+-- >>> fromText ("anything else"::Text) :: Maybe Bool
42−-- > fromText _ = Nothing
45+-- Nothing
43instance FromText Bool where
46instance FromText Bool where
44 fromText "true" = Just True
47 fromText "true" = Just True
45 fromText "false" = Just False
48 fromText "false" = Just False
46 fromText _ = Nothing
49 fromText _ = Nothing
47
50
48-- |
51-- |
49−-- > toText True = "true"
52+-- >>> toText True
53+-- "true"
50−-- > toText False = "false"
54+-- >>> toText False
55+-- "false"
51instance ToText Bool where
56instance ToText Bool where
52 toText True = "true"
57 toText True = "true"
53 toText False = "false"
58 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 @@
1module Servant.Common.TextSpec where
1module Servant.Common.TextSpec where
2
2
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 )
3+import Data.Int (Int16, Int32, Int64, Int8)
8−import Data.Text ( Text )
4+import Data.Text (Text)
9−import Data.Word ( Word, Word8, Word16, Word32, Word64 )
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 ()
10
10
11spec :: Spec
11spec :: Spec
12spec = describe "Servant.Common.Text" $ do
12spec = describe "Servant.Common.Text" $ do