1 {-# LANGUAGE AllowAmbiguousTypes #-}
2 {-# LANGUAGE ConstraintKinds #-}
4 {-# LANGUAGE KindSignatures #-}
5 {-# LANGUAGE OverloadedStrings #-}
9 import Control.Arrow (left)
11 import Data.ByteString qualified as BS
12 import Data.ByteString.Lazy qualified as BSL
13 import Data.ByteString.Lazy.Char8 qualified as BLC
14 import Data.Either (Either (..))
15 import Data.Foldable (toList)
16 import Data.Function (id, ($), (.))
17 import Data.Functor ((<$>))
19 import Data.Kind (Constraint, Type)
20 import Data.List.NonEmpty (NonEmpty (..))
21 import Data.Maybe (Maybe (..))
22 import Data.Proxy (Proxy (..))
23 import Data.Semigroup (Semigroup (..))
24 import Data.String (String)
25 import Data.Text qualified as T
26 import Data.Text.Encoding qualified as T
27 import Data.Text.Lazy qualified as TL
28 import Data.Text.Lazy.Encoding qualified as TL
29 import Data.Tuple (fst, snd)
30 import Data.Typeable (Typeable)
31 import Network.HTTP.Media qualified as Media
32 import Text.Read (readMaybe)
33 import Text.Show (Show (..))
35 --import qualified Web.FormUrlEncoded as Web
37 -- * Class 'MediaTypeFor'
38 class MediaTypeFor t where
39 mediaTypeFor :: Proxy t -> MediaType
40 mediaTypesFor :: Proxy t -> NonEmpty MediaType
41 mediaTypesFor t = mediaTypeFor t :| []
42 instance MediaTypeFor () where
43 mediaTypeFor _t = mimeAny
45 -- ** Type 'MediaType'
46 type MediaType = Media.MediaType
47 mediaType :: forall t. MediaTypeFor t => MediaType
48 mediaType = mediaTypeFor (Proxy @t)
49 {-# INLINE mediaType #-}
51 -- ** Type 'MediaTypes'
52 type MediaTypes = NonEmpty MediaType
53 mediaTypes :: forall ts c. MimeTypes ts c => MediaTypes
54 mediaTypes = fst <$> mimeTypesMap @ts @c
55 {-# INLINE mediaTypes #-}
57 charsetUTF8 :: MediaType -> MediaType
58 charsetUTF8 = (Media./: ("charset", "utf-8"))
64 data JSON deriving (Typeable)
65 instance MediaTypeFor JSON where
66 mediaTypeFor _t = charsetUTF8 $ "application" Media.// "json"
67 mediaTypesFor t = mediaTypeFor t :| ["application" Media.// "json"]
70 data HTML deriving (Typeable)
71 instance MediaTypeFor HTML where
72 mediaTypeFor _t = charsetUTF8 $ "text" Media.// "html"
73 mediaTypesFor t = mediaTypeFor t :| ["text" Media.// "html"]
75 -- ** Type 'FormUrlEncoded'
76 data FormUrlEncoded deriving (Typeable)
77 instance MediaTypeFor FormUrlEncoded where
78 mediaTypeFor _t = "application" Media.// "x-www-form-urlencoded"
80 -- ** Type 'OctetStream'
81 data OctetStream deriving (Typeable)
82 instance MediaTypeFor OctetStream where
83 mediaTypeFor _t = "application" Media.// "octet-stream"
85 -- ** Type 'PlainText'
86 data PlainText deriving (Typeable)
87 instance MediaTypeFor PlainText where
88 mediaTypeFor _t = charsetUTF8 $ "text" Media.// "plain"
92 -- | Existentially wraps a type-level type 't'
93 -- with a proof it respects 'Constraint' 'c'.
94 -- Usually 'c' is @'MimeEncodable' a@ or @'MimeDecodable' a@.
96 MimeType :: (c t, MediaTypeFor t) => Proxy t -> MimeType c
98 mimeType :: forall t c. MediaTypeFor t => c t => MimeType c
99 mimeType = MimeType (Proxy @t)
100 {-# INLINE mimeType #-}
101 mimeTypes :: forall ts c. MimeTypes ts c => NonEmpty (MimeType c)
102 mimeTypes = snd <$> mimeTypesMap @ts @c
103 {-# INLINE mimeTypes #-}
105 -- * Class 'MimeTypes'
107 -- | Implicitely generate 'MediaType's and 'MimeType's
108 -- from a type-level list of types.
109 class MimeTypes (ts :: [Type]) (c :: Type -> Constraint) where
110 mimeTypesMap :: NonEmpty (MediaType, MimeType c)
112 instance (MediaTypeFor t, c t) => MimeTypes '[t] c where
113 mimeTypesMap = (,MimeType @c @t Proxy) <$> mediaTypesFor (Proxy @t)
114 instance (MediaTypeFor t, MimeTypes (t1 ': ts) c, c t) => MimeTypes (t ': t1 ': ts) c where
116 ( (,MimeType @c @t Proxy)
117 <$> mediaTypesFor (Proxy @t)
119 <> mimeTypesMap @(t1 ': ts) @c
126 matchAccept = Media.mapAccept (toList $ mimeTypesMap @ts @c)
133 matchContent = Media.mapContent (toList $ mimeTypesMap @ts @c)
135 -- * Type 'MimeEncodable'
136 class MediaTypeFor t => MimeEncodable a t where
137 mimeEncode :: Proxy t -> MimeEncoder a
138 instance MimeEncodable () PlainText where
139 mimeEncode _ () = BLC.pack ""
141 -- | @BSL.fromStrict . T.encodeUtf8@
142 instance MimeEncodable String PlainText where
143 mimeEncode _ = BLC.pack
145 instance MimeEncodable T.Text PlainText where
146 mimeEncode _ = BSL.fromStrict . T.encodeUtf8
147 instance MimeEncodable TL.Text PlainText where
148 mimeEncode _ = TL.encodeUtf8
149 instance MimeEncodable BS.ByteString OctetStream where
150 mimeEncode _ = BSL.fromStrict
151 instance MimeEncodable BSL.ByteString OctetStream where
153 instance MimeEncodable Int PlainText where
154 mimeEncode _ = TL.encodeUtf8 . TL.pack . show
156 -- | @Web.urlEncodeAsForm@
157 -- Note that the @mimeDecode p (mimeEncode p x) == Right x@ law only
158 -- holds if every element of x is non-null (i.e., not @("", "")@)
159 --instance Web.ToForm a => MimeEncodable a FormUrlEncoded where
160 -- mimeEncode _ = Web.urlEncodeAsForm
162 -- ** Type 'MimeEncoder'
164 type MimeEncoder a = a -> BSL.ByteString
166 -- * Type 'MimeDecodable'
167 class MediaTypeFor mt => MimeDecodable a mt where
168 mimeDecode :: Proxy mt -> MimeDecoder a
170 -- mimeDecode p = mimeUnserializeWithType p (mimeType p)
172 -- ** Type 'MimeDecoder'
173 type MimeDecoder a = BSL.ByteString -> Either String a
175 instance MimeDecodable () PlainText where
177 | BLC.null bsl = Right ()
178 | otherwise = Left "not empty"
179 instance MimeDecodable String PlainText where
180 mimeDecode _ = Right . BLC.unpack
181 instance MimeDecodable T.Text PlainText where
182 mimeDecode _ = left show . T.decodeUtf8' . BSL.toStrict
183 instance MimeDecodable TL.Text PlainText where
184 mimeDecode _ = left show . TL.decodeUtf8'
185 instance MimeDecodable BS.ByteString OctetStream where
186 mimeDecode _ = Right . BSL.toStrict
187 instance MimeDecodable BSL.ByteString OctetStream where
189 instance MimeDecodable Int PlainText where
193 _ -> Left $ "cannot parse as Int: " <> s
195 s = TL.unpack (TL.decodeUtf8 bsl)
197 -- | @Web.urlDecodeAsForm@
198 -- Note that the @mimeDecode p (mimeEncode p x) == Right x@ law only
199 -- holds if every element of x is non-null (i.e., not @("", "")@)
200 --instance Web.FromForm a => MimeDecodable a FormUrlEncoded where
201 -- mimeDecode _ = left T.unpack . Web.urlDecodeAsForm