1 module Symantic.HTTP.Mime
2 ( module Symantic.HTTP.Mime
3 , module Symantic.HTTP.Mime.Type
6 import Control.Arrow (left)
7 import Data.Either (Either(..))
8 import Data.Function ((.), id)
9 import Data.Proxy (Proxy(..))
10 import Data.String (String)
11 import Text.Show (Show(..))
12 import qualified Data.ByteString as BS
13 import qualified Data.ByteString.Lazy as BSL
14 import qualified Data.ByteString.Lazy.Char8 as BLC
15 import qualified Data.Text as T
16 import qualified Data.Text.Encoding as T
17 import qualified Data.Text.Lazy as TL
18 import qualified Data.Text.Lazy.Encoding as TL
19 import qualified Web.FormUrlEncoded as Web
21 import Symantic.HTTP.Mime.Type
25 data MimeType mt a where
29 MimeUnserialize mt a =>
32 mimeType :: MediaType -> MimeType mt
36 newtype AcceptHeader = AcceptHeader BS.ByteString
37 deriving (Eq, Show, Read, Typeable, Generic)
40 -- * Type 'MimeSerialize'
41 class MediaTypeable mt => MimeSerialize a mt where
42 mimeSerialize :: Proxy mt -> Serializer a
43 -- | @BSL.fromStrict . T.encodeUtf8@
44 instance MimeSerialize String PlainText where
45 mimeSerialize _ = BLC.pack
46 instance MimeSerialize T.Text PlainText where
47 mimeSerialize _ = BSL.fromStrict . T.encodeUtf8
48 instance MimeSerialize TL.Text PlainText where
49 mimeSerialize _ = TL.encodeUtf8
50 instance MimeSerialize BS.ByteString OctetStream where
51 mimeSerialize _ = BSL.fromStrict
52 instance MimeSerialize BSL.ByteString OctetStream where
54 -- | @Web.urlEncodeAsForm@
55 -- Note that the @mimeUnserialize p (mimeSerialize p x) == Right x@ law only
56 -- holds if every element of x is non-null (i.e., not @("", "")@)
57 instance Web.ToForm a => MimeSerialize a FormUrlEncoded where
58 mimeSerialize _ = Web.urlEncodeAsForm
61 instance {-# OVERLAPPABLE #-}
62 ToJSON a => MimeSerialize JSON a where
63 mimeSerialize _ = encode
66 -- ** Type 'Serializer'
67 type Serializer a = a -> BSL.ByteString
70 class (AllMime list) => AllCTSerialize (list :: [*]) a where
71 -- If the Accept header can be matched, returns (Just) a tuple of the
72 -- Content-Type and response (serialization of @a@ into the appropriate
74 handleAcceptH :: Proxy list -> AcceptHeader -> a -> Maybe (ByteString, ByteString)
76 instance {-# OVERLAPPABLE #-}
77 (Accept ct, AllMime cts, AllMimeSerialize (ct ': cts) a) => AllCTSerialize (ct ': cts) a where
78 handleAcceptH _ (AcceptHeader accept) val = Media.mapAcceptMedia lkup accept
80 pctyps = Proxy :: Proxy (ct ': cts)
81 amrs = allMimeSerialize pctyps val
82 lkup = fmap (\(a,b) -> (a, (BSL.fromStrict $ Media.renderHeader a, b))) amrs
84 instance TL.TypeError ('TL.Text "No instance for (), use NoContent instead.") => AllCTSerialize '[] () where
85 handleAcceptH _ _ _ = error "unreachable"
88 -- * Type 'MimeUnserialize'
89 class MediaTypeable mt => MimeUnserialize a mt where
90 mimeUnserialize :: Proxy mt -> Unserializer a
91 -- mimeUnserialize p = mimeUnserializeWithType p (mimeType p)
92 -- ** Type 'Unserializer'
93 type Unserializer a = BSL.ByteString -> Either String a
96 -- | Variant which is given the actual 'Media.MediaType' provided by the other party.
98 -- In the most cases you don't want to branch based on the 'Media.MediaType'.
99 -- See <https://github.com/haskell-servant/servant/pull/552 pr552> for a motivating example.
100 mimeUnserializeWithType :: Proxy mt -> Media.MediaType -> Unserializer a
101 mimeUnserializeWithType p _ = mimeUnserialize p
102 {-# MINIMAL mimeUnserialize | mimeUnserializeWithType #-}
104 instance MimeUnserialize String PlainText where
105 mimeUnserialize _ = Right . BLC.unpack
106 instance MimeUnserialize T.Text PlainText where
107 mimeUnserialize _ = left show . T.decodeUtf8' . BSL.toStrict
108 instance MimeUnserialize TL.Text PlainText where
109 mimeUnserialize _ = left show . TL.decodeUtf8'
110 instance MimeUnserialize BS.ByteString OctetStream where
111 mimeUnserialize _ = Right . BSL.toStrict
112 instance MimeUnserialize BSL.ByteString OctetStream where
113 mimeUnserialize _ = Right
114 -- | @Web.urlDecodeAsForm@
115 -- Note that the @mimeUnserialize p (mimeSerialize p x) == Right x@ law only
116 -- holds if every element of x is non-null (i.e., not @("", "")@)
117 instance Web.FromForm a => MimeUnserialize a FormUrlEncoded where
118 mimeUnserialize _ = left T.unpack . Web.urlDecodeAsForm
120 -- | Like 'Data.Aeson.eitherDecode' but allows all JSON values instead of just
121 -- objects and arrays.
123 -- Will handle trailing whitespace, but not trailing junk. ie.
125 -- >>> eitherDecodeLenient "1 " :: Either String Int
128 -- >>> eitherDecodeLenient "1 junk" :: Either String Int
129 -- Left "trailing junk after valid JSON: endOfInput"
130 eitherDecodeLenient :: FromJSON a => ByteString -> Either String a
131 eitherDecodeLenient input =
132 parseOnly parser (cs input) >>= parseEither parseJSON
135 *> Data.Aeson.Parser.value
137 <* (endOfInput <?> "trailing junk after valid JSON")
140 instance FromJSON a => MimeUnserialize JSON a where
141 mimeUnserialize _ = eitherDecodeLenient
157 -- | A type for responses without content-body.
158 data NoContent = NoContent
163 class AllCTUnserialize (list :: [*]) a where
166 -> ByteString -- Content-Type header
167 -> Maybe (ByteString -> Either String a)
169 handleCTypeH :: Proxy list
170 -> ByteString -- Content-Type header
171 -> ByteString -- Request body
172 -> Maybe (Either String a)
173 handleCTypeH p ctypeH body = ($ body) `fmap` canHandleCTypeH p ctypeH
175 instance ( AllMimeUnserialize ctyps a ) => AllCTUnserialize ctyps a where
176 canHandleCTypeH p ctypeH =
177 Media.mapContentMedia (allMimeUnserialize p) (cs ctypeH)
179 --------------------------------------------------------------------------
180 -- * Utils (Internal)
182 class AllMime (list :: [*]) where
183 allMime :: Proxy list -> [Media.MediaType]
185 instance AllMime '[] where
188 instance (MediaType ctyp, AllMime ctyps) => AllMime (ctyp ': ctyps) where
189 allMime _ = NE.toList (contentTypes pctyp) ++ allMime pctyps
191 pctyp = Proxy :: Proxy ctyp
192 pctyps = Proxy :: Proxy ctyps
194 canHandleAcceptH :: AllMime list => Proxy list -> AcceptHeader -> Bool
195 canHandleAcceptH p (AcceptHeader h ) = isJust $ Media.matchAccept (allMime p) h
197 --------------------------------------------------------------------------
198 -- Check that all elements of list are instances of MimeSerialize
199 --------------------------------------------------------------------------
200 class (AllMime list) => AllMimeSerialize (list :: [*]) a where
201 allMimeSerialize :: Proxy list
202 -> a -- value to serialize
203 -> [(Media.MediaType, ByteString)] -- content-types/response pairs
205 instance {-# OVERLAPPABLE #-} ( MimeSerialize ctyp a ) => AllMimeSerialize '[ctyp] a where
206 allMimeSerialize _ a = map (, bs) $ NE.toList $ contentTypes pctyp
208 bs = mimeSerialize pctyp a
209 pctyp = Proxy :: Proxy ctyp
211 instance {-# OVERLAPPABLE #-}
212 ( MimeSerialize ctyp a
213 , AllMimeSerialize (ctyp' ': ctyps) a
214 ) => AllMimeSerialize (ctyp ': ctyp' ': ctyps) a where
215 allMimeSerialize _ a =
216 map (, bs) (NE.toList $ contentTypes pctyp)
217 ++ allMimeSerialize pctyps a
219 bs = mimeSerialize pctyp a
220 pctyp = Proxy :: Proxy ctyp
221 pctyps = Proxy :: Proxy (ctyp' ': ctyps)
224 -- Ideally we would like to declare a 'MimeSerialize a NoContent' instance, and
225 -- then this would be taken care of. However there is no more specific instance
226 -- between that and 'MimeSerialize JSON a', so we do this instead
227 instance {-# OVERLAPPING #-} ( MediaType ctyp ) => AllMimeSerialize '[ctyp] NoContent where
228 allMimeSerialize _ _ = map (, "") $ NE.toList $ contentTypes pctyp
230 pctyp = Proxy :: Proxy ctyp
232 instance {-# OVERLAPPING #-}
233 ( AllMime (ctyp ': ctyp' ': ctyps)
234 ) => AllMimeSerialize (ctyp ': ctyp' ': ctyps) NoContent where
235 allMimeSerialize p _ = zip (allMime p) (repeat "")
237 --------------------------------------------------------------------------
238 -- Check that all elements of list are instances of MimeUnserialize
239 --------------------------------------------------------------------------
240 class (AllMime list) => AllMimeUnserialize (list :: [*]) a where
241 allMimeUnserialize :: Proxy list
242 -> [(Media.MediaType, ByteString -> Either String a)]
243 instance AllMimeUnserialize '[] a where
244 allMimeUnserialize _ = []
245 instance ( MimeUnserialize ctyp a
246 , AllMimeUnserialize ctyps a
247 ) => AllMimeUnserialize (ctyp ': ctyps) a where
248 allMimeUnserialize _ =
249 map mk (NE.toList $ contentTypes pctyp)
250 ++ allMimeUnserialize pctyps
252 mk ct = (ct, mimeUnserializeWithType pctyp ct)
253 pctyp = Proxy :: Proxy ctyp
254 pctyps = Proxy :: Proxy ctyps