@@ -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 }