@@ -9,7 +9,57 @@
9{-# LANGUAGE TypeFamilies #-}
9{-# LANGUAGE TypeFamilies #-}
10{-# LANGUAGE TypeOperators #-}
10{-# LANGUAGE TypeOperators #-}
11{-# LANGUAGE UndecidableInstances #-}
11{-# LANGUAGE UndecidableInstances #-}
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.
18+-- Content-Types are used in `ReqBody` and the method combinators:
20+-- >>> type MyEndpoint = ReqBody '[JSON, PlainText] Book :> Get '[JSON, PlainText] :> Book
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).
26+-- If you would like to support Content-Types beyond those provided here,
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`.
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
43+ -- * Provided Content-Types
49+ -- * Building your own Content-Type
58+ , FromFormUrlEncoded(..)
59+ , ToFormUrlEncoded(..)
61+ , eitherDecodeLenient
14import Control.Applicative ((<*))
64import Control.Applicative ((<*))
15import Control.Arrow (left)
65import Control.Arrow (left)
@@ -48,8 +98,12 @@ data OctetStream deriving Typeable
49-- Example:
99-- Example:
101+-- >>> import Network.HTTP.Media ((//), (/:))
51−-- > instance Accept HTML where
104+--instance Accept HTML where
52−-- > contentType _ = "text" // "html"
105+-- contentType _ = "text" // "html" /: ("charset", "utf-8")
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
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.
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
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")
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"
124−-- > type MyAPI = "path" :> ReqBody '[MyContentType] :> Get '[JSON] Int
186+-- >>> type MyAPI = "path" :> ReqBody '[MyContentType] Int :> Get '[JSON] Int
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
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
@@ -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
172− allMimeUnrender :: Proxy ls -> ByteString -> [(M.MediaType, Either String a)]
236+ -> [(M.MediaType, Either String a)]
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
185−type family IsNonEmpty (ls::[*]) :: Constraint where
249+type family IsNonEmpty (list :: [*]) :: Constraint where
186 IsNonEmpty (x ': xs) = ()
250 IsNonEmpty (x ': xs) = ()
@@ -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
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
@@ -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
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
210−-- | `id`
274+-- | @id@
211instance MimeRender OctetStream ByteString where
275instance MimeRender OctetStream ByteString where
212 toByteString _ = id
276 toByteString _ = id
@@ -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
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
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'
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
247−-- | `Right . id`
311+-- | @Right . id@
248instance MimeUnrender OctetStream ByteString where
312instance MimeUnrender OctetStream ByteString where
249 fromByteString _ = Right . id
313 fromByteString _ = Right . id
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
@@ -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
365+-- >>> import Servant.API
366+-- >>> import Data.Aeson
367+-- >>> import Data.Text
369+-- >>> instance ToJSON Book where { toJSON = undefined }