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
32 mimeType :: MediaType -> MimeType mt
36 newtype AcceptHeader = AcceptHeader BS.ByteString
37 deriving (Eq, Show, Read, Typeable, Generic)
40 -- * Type 'MimeEncodable'
41 class MediaTypeable mt => MimeEncodable a mt where
42 mimeEncode :: Proxy mt -> Serializer a
43 instance MimeEncodable () PlainText where
44 mimeEncode _ () = BLC.pack ""
45 -- | @BSL.fromStrict . T.encodeUtf8@
46 instance MimeEncodable String PlainText where
47 mimeEncode _ = BLC.pack
48 instance MimeEncodable T.Text PlainText where
49 mimeEncode _ = BSL.fromStrict . T.encodeUtf8
50 instance MimeEncodable TL.Text PlainText where
51 mimeEncode _ = TL.encodeUtf8
52 instance MimeEncodable BS.ByteString OctetStream where
53 mimeEncode _ = BSL.fromStrict
54 instance MimeEncodable BSL.ByteString OctetStream where
56 -- | @Web.urlEncodeAsForm@
57 -- Note that the @mimeDecode p (mimeEncode p x) == Right x@ law only
58 -- holds if every element of x is non-null (i.e., not @("", "")@)
59 instance Web.ToForm a => MimeEncodable a FormUrlEncoded where
60 mimeEncode _ = Web.urlEncodeAsForm
63 instance {-# OVERLAPPABLE #-}
64 ToJSON a => MimeEncodable JSON a where
68 -- ** Type 'Serializer'
69 type Serializer a = a -> BSL.ByteString
72 class (AllMime list) => AllCTSerialize (list :: [*]) a where
73 -- If the Accept header can be matched, returns (Just) a tuple of the
74 -- Content-Type and response (serialization of @a@ into the appropriate
76 handleAcceptH :: Proxy list -> AcceptHeader -> a -> Maybe (ByteString, ByteString)
78 instance {-# OVERLAPPABLE #-}
79 (Accept ct, AllMime cts, AllMimeSerialize (ct ': cts) a) => AllCTSerialize (ct ': cts) a where
80 handleAcceptH _ (AcceptHeader accept) val = Media.mapAcceptMedia lkup accept
82 pctyps = Proxy :: Proxy (ct ': cts)
83 amrs = allMimeSerialize pctyps val
84 lkup = fmap (\(a,b) -> (a, (BSL.fromStrict $ Media.renderHeader a, b))) amrs
86 instance TL.TypeError ('TL.Text "No instance for (), use NoContent instead.") => AllCTSerialize '[] () where
87 handleAcceptH _ _ _ = error "unreachable"
90 -- * Type 'MimeDecodable'
91 class MediaTypeable mt => MimeDecodable a mt where
92 mimeDecode :: Proxy mt -> Unserializer a
93 -- mimeDecode p = mimeUnserializeWithType p (mimeType p)
95 -- ** Type 'Unserializer'
96 type Unserializer a = BSL.ByteString -> Either String a
99 -- | Variant which is given the actual 'Media.MediaType' provided by the other party.
101 -- In the most cases you don't want to branch based on the 'Media.MediaType'.
102 -- See <https://github.com/haskell-servant/servant/pull/552 pr552> for a motivating example.
103 mimeUnserializeWithType :: Proxy mt -> Media.MediaType -> Unserializer a
104 mimeUnserializeWithType p _ = mimeDecode p
105 {-# MINIMAL mimeDecode | mimeUnserializeWithType #-}
107 instance MimeDecodable () PlainText where
111 else Left "not empty"
112 instance MimeDecodable String PlainText where
113 mimeDecode _ = Right . BLC.unpack
114 instance MimeDecodable T.Text PlainText where
115 mimeDecode _ = left show . T.decodeUtf8' . BSL.toStrict
116 instance MimeDecodable TL.Text PlainText where
117 mimeDecode _ = left show . TL.decodeUtf8'
118 instance MimeDecodable BS.ByteString OctetStream where
119 mimeDecode _ = Right . BSL.toStrict
120 instance MimeDecodable BSL.ByteString OctetStream where
122 -- | @Web.urlDecodeAsForm@
123 -- Note that the @mimeDecode p (mimeEncode p x) == Right x@ law only
124 -- holds if every element of x is non-null (i.e., not @("", "")@)
125 instance Web.FromForm a => MimeDecodable a FormUrlEncoded where
126 mimeDecode _ = left T.unpack . Web.urlDecodeAsForm
128 -- | Like 'Data.Aeson.eitherDecode' but allows all JSON values instead of just
129 -- objects and arrays.
131 -- Will handle trailing whitespace, but not trailing junk. ie.
133 -- >>> eitherDecodeLenient "1 " :: Either String Int
136 -- >>> eitherDecodeLenient "1 junk" :: Either String Int
137 -- Left "trailing junk after valid JSON: endOfInput"
138 eitherDecodeLenient :: FromJSON a => ByteString -> Either String a
139 eitherDecodeLenient input =
140 parseOnly parser (cs input) >>= parseEither parseJSON
143 *> Data.Aeson.Parser.value
145 <* (endOfInput <?> "trailing junk after valid JSON")
148 instance FromJSON a => MimeDecodable JSON a where
149 mimeDecode _ = eitherDecodeLenient
165 -- | A type for responses without content-body.
166 data NoContent = NoContent
171 class AllCTUnserialize (list :: [*]) a where
174 -> ByteString -- Content-Type header
175 -> Maybe (ByteString -> Either String a)
177 handleCTypeH :: Proxy list
178 -> ByteString -- Content-Type header
179 -> ByteString -- Request body
180 -> Maybe (Either String a)
181 handleCTypeH p ctypeH body = ($ body) `fmap` canHandleCTypeH p ctypeH
183 instance ( AllMimeUnserialize ctyps a ) => AllCTUnserialize ctyps a where
184 canHandleCTypeH p ctypeH =
185 Media.mapContentMedia (allMimeUnserialize p) (cs ctypeH)
187 --------------------------------------------------------------------------
188 -- * Utils (Internal)
190 class AllMime (list :: [*]) where
191 allMime :: Proxy list -> [Media.MediaType]
193 instance AllMime '[] where
196 instance (MediaType ctyp, AllMime ctyps) => AllMime (ctyp ': ctyps) where
197 allMime _ = NE.toList (contentTypes pctyp) ++ allMime pctyps
199 pctyp = Proxy :: Proxy ctyp
200 pctyps = Proxy :: Proxy ctyps
202 canHandleAcceptH :: AllMime list => Proxy list -> AcceptHeader -> Bool
203 canHandleAcceptH p (AcceptHeader h ) = isJust $ Media.matchAccept (allMime p) h
205 --------------------------------------------------------------------------
206 -- Check that all elements of list are instances of MimeEncodable
207 --------------------------------------------------------------------------
208 class (AllMime list) => AllMimeSerialize (list :: [*]) a where
209 allMimeSerialize :: Proxy list
210 -> a -- value to serialize
211 -> [(Media.MediaType, ByteString)] -- content-types/response pairs
213 instance {-# OVERLAPPABLE #-} ( MimeEncodable ctyp a ) => AllMimeSerialize '[ctyp] a where
214 allMimeSerialize _ a = map (, bs) $ NE.toList $ contentTypes pctyp
216 bs = mimeEncode pctyp a
217 pctyp = Proxy :: Proxy ctyp
219 instance {-# OVERLAPPABLE #-}
220 ( MimeEncodable ctyp a
221 , AllMimeSerialize (ctyp' ': ctyps) a
222 ) => AllMimeSerialize (ctyp ': ctyp' ': ctyps) a where
223 allMimeSerialize _ a =
224 map (, bs) (NE.toList $ contentTypes pctyp)
225 ++ allMimeSerialize pctyps a
227 bs = mimeEncode pctyp a
228 pctyp = Proxy :: Proxy ctyp
229 pctyps = Proxy :: Proxy (ctyp' ': ctyps)
232 -- Ideally we would like to declare a 'MimeEncodable a NoContent' instance, and
233 -- then this would be taken care of. However there is no more specific instance
234 -- between that and 'MimeEncodable JSON a', so we do this instead
235 instance {-# OVERLAPPING #-} ( MediaType ctyp ) => AllMimeSerialize '[ctyp] NoContent where
236 allMimeSerialize _ _ = map (, "") $ NE.toList $ contentTypes pctyp
238 pctyp = Proxy :: Proxy ctyp
240 instance {-# OVERLAPPING #-}
241 ( AllMime (ctyp ': ctyp' ': ctyps)
242 ) => AllMimeSerialize (ctyp ': ctyp' ': ctyps) NoContent where
243 allMimeSerialize p _ = zip (allMime p) (repeat "")
245 --------------------------------------------------------------------------
246 -- Check that all elements of list are instances of MimeDecodable
247 --------------------------------------------------------------------------
248 class (AllMime list) => AllMimeUnserialize (list :: [*]) a where
249 allMimeUnserialize :: Proxy list
250 -> [(Media.MediaType, ByteString -> Either String a)]
251 instance AllMimeUnserialize '[] a where
252 allMimeUnserialize _ = []
253 instance ( MimeDecodable ctyp a
254 , AllMimeUnserialize ctyps a
255 ) => AllMimeUnserialize (ctyp ': ctyps) a where
256 allMimeUnserialize _ =
257 map mk (NE.toList $ contentTypes pctyp)
258 ++ allMimeUnserialize pctyps
260 mk ct = (ct, mimeUnserializeWithType pctyp ct)
261 pctyp = Proxy :: Proxy ctyp
262 pctyps = Proxy :: Proxy ctyps