1 module Language.Symantic.HTTP.Mime where
3 -- import qualified Data.List.NonEmpty as NE
4 import Control.Arrow (left)
5 import Data.Either (Either(..))
6 import Data.Function ((.), id)
7 import Data.Proxy (Proxy(..))
8 import Data.String (String)
10 import Text.Show (Show(..))
11 import Web.FormUrlEncoded (FromForm, urlDecodeAsForm{-, urlEncodeAsForm, ToForm-})
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 Network.HTTP.Media as M
21 import Language.Symantic.HTTP.Media
24 newtype AcceptHeader = AcceptHeader BS.ByteString
25 deriving (Eq, Show, Read, Typeable, Generic)
28 -- * Type 'MimeRender'
29 class MediaTypeable mt => MimeRender mt a where
30 mimeRender :: Proxy mt -> a -> BSL.ByteString
31 -- | @BSL.fromStrict . T.encodeUtf8@
32 instance MimeRender PlainText String where
33 mimeRender _ = BLC.pack
34 instance MimeRender PlainText T.Text where
35 mimeRender _ = BSL.fromStrict . T.encodeUtf8
36 instance MimeRender PlainText TL.Text where
37 mimeRender _ = TL.encodeUtf8
38 instance MimeRender OctetStream BS.ByteString where
39 mimeRender _ = BSL.fromStrict
40 instance MimeRender OctetStream BSL.ByteString where
44 instance {-# OVERLAPPABLE #-}
45 ToJSON a => MimeRender JSON a where
48 -- | @urlEncodeAsForm@
49 -- Note that the @mimeUnrender p (mimeRender p x) == Right x@ law only
50 -- holds if every element of x is non-null (i.e., not @("", "")@)
51 instance {-# OVERLAPPABLE #-}
52 ToForm a => MimeRender FormUrlEncoded a where
53 mimeRender _ = urlEncodeAsForm
57 class (AllMime list) => AllCTRender (list :: [*]) a where
58 -- If the Accept header can be matched, returns (Just) a tuple of the
59 -- Content-Type and response (serialization of @a@ into the appropriate
61 handleAcceptH :: Proxy list -> AcceptHeader -> a -> Maybe (ByteString, ByteString)
63 instance {-# OVERLAPPABLE #-}
64 (Accept ct, AllMime cts, AllMimeRender (ct ': cts) a) => AllCTRender (ct ': cts) a where
65 handleAcceptH _ (AcceptHeader accept) val = M.mapAcceptMedia lkup accept
67 pctyps = Proxy :: Proxy (ct ': cts)
68 amrs = allMimeRender pctyps val
69 lkup = fmap (\(a,b) -> (a, (BSL.fromStrict $ M.renderHeader a, b))) amrs
71 instance TL.TypeError ('TL.Text "No instance for (), use NoContent instead.") => AllCTRender '[] () where
72 handleAcceptH _ _ _ = error "unreachable"
75 -- * Type 'MimeUnrender'
76 class MediaTypeable mt => MimeUnrender mt a where
77 mimeUnrender :: Proxy mt -> BSL.ByteString -> Either String a
78 mimeUnrender p = mimeUnrenderWithType p (mediaType p)
80 -- | Variant which is given the actual 'M.MediaType' provided by the other party.
82 -- In the most cases you don't want to branch based on the 'M.MediaType'.
83 -- See <https://github.com/haskell-servant/servant/pull/552 pr552> for a motivating example.
84 mimeUnrenderWithType :: Proxy mt -> M.MediaType -> BSL.ByteString -> Either String a
85 mimeUnrenderWithType p _ = mimeUnrender p
86 {-# MINIMAL mimeUnrender | mimeUnrenderWithType #-}
87 instance MimeUnrender PlainText String where
88 mimeUnrender _ = Right . BLC.unpack
89 instance MimeUnrender PlainText T.Text where
90 mimeUnrender _ = left show . T.decodeUtf8' . BSL.toStrict
91 instance MimeUnrender PlainText TL.Text where
92 mimeUnrender _ = left show . TL.decodeUtf8'
93 instance MimeUnrender OctetStream BS.ByteString where
94 mimeUnrender _ = Right . BSL.toStrict
95 instance MimeUnrender OctetStream BSL.ByteString where
96 mimeUnrender _ = Right
97 -- | @urlDecodeAsForm@
98 -- Note that the @mimeUnrender p (mimeRender p x) == Right x@ law only
99 -- holds if every element of x is non-null (i.e., not @("", "")@)
100 instance FromForm a => MimeUnrender FormUrlEncoded a where
101 mimeUnrender _ = left T.unpack . urlDecodeAsForm
103 -- | Like 'Data.Aeson.eitherDecode' but allows all JSON values instead of just
104 -- objects and arrays.
106 -- Will handle trailing whitespace, but not trailing junk. ie.
108 -- >>> eitherDecodeLenient "1 " :: Either String Int
111 -- >>> eitherDecodeLenient "1 junk" :: Either String Int
112 -- Left "trailing junk after valid JSON: endOfInput"
113 eitherDecodeLenient :: FromJSON a => ByteString -> Either String a
114 eitherDecodeLenient input =
115 parseOnly parser (cs input) >>= parseEither parseJSON
118 *> Data.Aeson.Parser.value
120 <* (endOfInput <?> "trailing junk after valid JSON")
123 instance FromJSON a => MimeUnrender JSON a where
124 mimeUnrender _ = eitherDecodeLenient
140 -- | A type for responses without content-body.
141 data NoContent = NoContent
146 class AllCTUnrender (list :: [*]) a where
149 -> ByteString -- Content-Type header
150 -> Maybe (ByteString -> Either String a)
152 handleCTypeH :: Proxy list
153 -> ByteString -- Content-Type header
154 -> ByteString -- Request body
155 -> Maybe (Either String a)
156 handleCTypeH p ctypeH body = ($ body) `fmap` canHandleCTypeH p ctypeH
158 instance ( AllMimeUnrender ctyps a ) => AllCTUnrender ctyps a where
159 canHandleCTypeH p ctypeH =
160 M.mapContentMedia (allMimeUnrender p) (cs ctypeH)
162 --------------------------------------------------------------------------
163 -- * Utils (Internal)
165 class AllMime (list :: [*]) where
166 allMime :: Proxy list -> [M.MediaType]
168 instance AllMime '[] where
171 instance (MediaType ctyp, AllMime ctyps) => AllMime (ctyp ': ctyps) where
172 allMime _ = NE.toList (contentTypes pctyp) ++ allMime pctyps
174 pctyp = Proxy :: Proxy ctyp
175 pctyps = Proxy :: Proxy ctyps
177 canHandleAcceptH :: AllMime list => Proxy list -> AcceptHeader -> Bool
178 canHandleAcceptH p (AcceptHeader h ) = isJust $ M.matchAccept (allMime p) h
180 --------------------------------------------------------------------------
181 -- Check that all elements of list are instances of MimeRender
182 --------------------------------------------------------------------------
183 class (AllMime list) => AllMimeRender (list :: [*]) a where
184 allMimeRender :: Proxy list
185 -> a -- value to serialize
186 -> [(M.MediaType, ByteString)] -- content-types/response pairs
188 instance {-# OVERLAPPABLE #-} ( MimeRender ctyp a ) => AllMimeRender '[ctyp] a where
189 allMimeRender _ a = map (, bs) $ NE.toList $ contentTypes pctyp
191 bs = mimeRender pctyp a
192 pctyp = Proxy :: Proxy ctyp
194 instance {-# OVERLAPPABLE #-}
196 , AllMimeRender (ctyp' ': ctyps) a
197 ) => AllMimeRender (ctyp ': ctyp' ': ctyps) a where
199 map (, bs) (NE.toList $ contentTypes pctyp)
200 ++ allMimeRender pctyps a
202 bs = mimeRender pctyp a
203 pctyp = Proxy :: Proxy ctyp
204 pctyps = Proxy :: Proxy (ctyp' ': ctyps)
207 -- Ideally we would like to declare a 'MimeRender a NoContent' instance, and
208 -- then this would be taken care of. However there is no more specific instance
209 -- between that and 'MimeRender JSON a', so we do this instead
210 instance {-# OVERLAPPING #-} ( MediaType ctyp ) => AllMimeRender '[ctyp] NoContent where
211 allMimeRender _ _ = map (, "") $ NE.toList $ contentTypes pctyp
213 pctyp = Proxy :: Proxy ctyp
215 instance {-# OVERLAPPING #-}
216 ( AllMime (ctyp ': ctyp' ': ctyps)
217 ) => AllMimeRender (ctyp ': ctyp' ': ctyps) NoContent where
218 allMimeRender p _ = zip (allMime p) (repeat "")
220 --------------------------------------------------------------------------
221 -- Check that all elements of list are instances of MimeUnrender
222 --------------------------------------------------------------------------
223 class (AllMime list) => AllMimeUnrender (list :: [*]) a where
224 allMimeUnrender :: Proxy list
225 -> [(M.MediaType, ByteString -> Either String a)]
226 instance AllMimeUnrender '[] a where
227 allMimeUnrender _ = []
228 instance ( MimeUnrender ctyp a
229 , AllMimeUnrender ctyps a
230 ) => AllMimeUnrender (ctyp ': ctyps) a where
232 map mk (NE.toList $ contentTypes pctyp)
233 ++ allMimeUnrender pctyps
235 mk ct = (ct, mimeUnrenderWithType pctyp ct)
236 pctyp = Proxy :: Proxy ctyp
237 pctyps = Proxy :: Proxy ctyps