@@ -7,94 +7,393 @@
7-- Stability : experimental
7-- Stability : experimental
8-- Portability : portable
8-- Portability : portable
10−-- Library for safe construction of HTTP requests.
10+-- This is an easy-to-use, type-safe, expandable, high-level HTTP library
11+-- that just works without any fooling around.
13+-- The documentation below is structured in such a way that most important
14+-- information goes first: you learn how to do HTTP requests, then how to
15+-- embed them any monad you have, then it goes on giving you details about
16+-- less-common things you may want to know about. The documentation is
17+-- written with sufficient coverage of details and with examples, it's
18+-- designed to be a complete tutorial on its own.
20+-- The library uses the following well-known and mature packages under the
21+-- hood to guarantee you best experience without bugs or other funny
24+-- * <https://hackage.haskell.org/package/http-client> — low level HTTP
25+-- client used everywhere in Haskell.
26+-- * <https://hackage.haskell.org/package/http-client-tls> — TLS (HTTPS)
28+-- * <https://hackage.haskell.org/package/http-conduit> — conduit
29+-- interface to @http-client@.
31+-- You generally won't need low-level interface of @http-client@ at all
32+-- because this package covers /everything/, so don't import it or import
33+-- qualified because it has naming conflicts with @req@.
12{-# LANGUAGE DataKinds #-}
35{-# LANGUAGE DataKinds #-}
13{-# LANGUAGE FlexibleInstances #-}
36{-# LANGUAGE FlexibleInstances #-}
14−{-# LANGUAGE FunctionalDependencies #-}
15{-# LANGUAGE GADTs #-}
37{-# LANGUAGE GADTs #-}
16{-# LANGUAGE GeneralizedNewtypeDeriving #-}
38{-# LANGUAGE GeneralizedNewtypeDeriving #-}
17{-# LANGUAGE KindSignatures #-}
39{-# LANGUAGE KindSignatures #-}
18{-# LANGUAGE MultiParamTypeClasses #-}
40{-# LANGUAGE MultiParamTypeClasses #-}
19{-# LANGUAGE OverloadedStrings #-}
41{-# LANGUAGE OverloadedStrings #-}
42+{-# LANGUAGE ScopedTypeVariables #-}
43+{-# LANGUAGE TypeFamilies #-}
44+{-# LANGUAGE UndecidableInstances #-}
21module Network.HTTP.Req
46module Network.HTTP.Req
22− ( HasBody (..)
47+ ( -- * Making a request
49+ -- * Embedding requests into your monad
23 , HttpMethod (..)
66 , HttpMethod (..)
73+ , HttpBody (..) -- TODO more stuff here
74+ -- ** Optional parameters
76+ -- *** Query parameters
24− , Scheme (..)
90+ , Scheme (..) )
30import Control.Monad.IO.Class
93import Control.Monad.IO.Class
31import Control.Monad.Reader
94import Control.Monad.Reader
32import Data.Aeson
95import Data.Aeson
33−import Data.Binary.Builder (Builder)
96+import Data.ByteString
34import Data.Semigroup hiding (Option)
98import Data.Semigroup hiding (Option)
35import Data.Text (Text)
99import Data.Text (Text)
36−import Network.HTTP.Client
100+import GHC.TypeLits
101+import Numeric.Natural
37−import qualified Data.ByteString as B
102+import qualified Data.ByteString as B
38import qualified Data.ByteString.Lazy as BL
103import qualified Data.ByteString.Lazy as BL
104+import qualified Network.HTTP.Client as L
105+import qualified Network.HTTP.Types as Y
40−data HasBody = HasBody | NoBody
107+----------------------------------------------------------------------------
42−data HttpMethod (b :: HasBody) where
110+-- | Perform an HTTP request.
43− GET :: HttpMethod 'NoBody
44− HEAD :: HttpMethod 'NoBody
45− OPTIONS :: HttpMethod 'NoBody
46− DELETE :: HttpMethod 'NoBody
47− POST :: HttpMethod 'HasBody
48− PUT :: HttpMethod 'HasBody
49− PATCH :: HttpMethod 'HasBody
51−data Scheme = Http | Https
112+req
116+ , HttpResponse response
117+ , AllowsBody method ~ ProvidesBody body
118+ , ProvidesConnection m ~ scheme )
53−data Endpoint (s :: Scheme) where
126+-- TODO We need to catch exceptions here, only relevant ones and only
54− Http' :: Text -> Endpoint 'Http
127+-- synchronous to give user a chance to work with pure, explicit exceptions
55− Https' :: Text -> Endpoint 'Https
128+-- if he wants to do so.
56− (:/) :: Endpoint s -> Text -> Endpoint s
58−http = Http' -- don't export the constructors
130+-- TODO How to specify return type here?
131+-- TODO It needs a way to signal errors.
60−https = Https'
133+----------------------------------------------------------------------------
134+-- Embedding requests into your monad
62−infixl 5 :/
136+class MonadIO m => MonadHttp m where
137+ type ProvidesConnection m :: Scheme
138+ getHttpConfig :: m (HttpConfig (ProvidesConnection m))
64−data ReqBody (b :: HasBody) where
140+-- | 'HttpConfig' contains general and default settings to be used when
65− NoReqBody :: ReqBody 'NoBody
141+-- making HTTP requests.
66− ReqBodyLBS :: BL.ByteString -> ReqBody 'HasBody
67− ReqBodyBS :: B.ByteString -> ReqBody 'HasBody
68− ReqBodyBuilder :: Builder -> ReqBody 'HasBody
69− ReqBodyUrlEncoded :: [(Text,Text)] -> ReqBody 'HasBody
70− -- ↑ use something nicer than tuples? (:=) pairs perhaps?
71− ReqBodyJSON :: Value -> ReqBody 'HasBody
74−newtype Options = Options [Option] deriving (Semigroup, Monoid)
143+data HttpConfig (scheme :: Scheme) = HttpConfig
144+ { httpConfigManager :: L.Manager
76−data Option
147+httpConfig :: MonadIO m => m (HttpConfig 'Http)
77− = QP Text Text -- ^ Query param
148+httpConfig = undefined -- FIXME not final, need to pass params in
78− -- TODO add much more, headers, cookies, all the optional stuff
80−data ReqConfig (s :: Scheme) = ReqConfig Manager
150+httpsConfig :: MonadIO m => m (HttpConfig 'Https)
151+httpsConfig = undefined -- FIXME
82−class HasReqConfig a s | a -> s where
153+----------------------------------------------------------------------------
83− reqConfig :: a -> ReqConfig s
154+-- Request — Methods
85−instance HasReqConfig (ReqConfig s) s where
156+-- $request-methods
158+-- The package provides all methods as defined by RFC 2616, and 'PATCH'
159+-- which is defined by RFC 5789 — that should be enough to talk to RESTful
160+-- APIs. In some cases however, you may want to add more methods (e.g. you
161+-- work with WebDAV <https://en.wikipedia.org/wiki/WebDAV>); no need to
162+-- compromise on type safety and hack, it only takes a couple of seconds to
163+-- define a new method that will works seamlessly, see 'HttpMethod'.
88−req
165+-- | 'GET' method.
89− :: (MonadIO m, MonadReader r m, HasReqConfig r s)
166+
90− => HttpMethod b
167+data GET = GET
92− -> ReqBody b
169+instance HttpMethod GET where
93− -> Options
170+ type AllowsBody GET = 'NoBody
94− -> m ()
171+ httpMethodName Proxy = Y.methodGet
177+instance HttpMethod POST where
178+ type AllowsBody POST = 'CanHaveBody
179+ httpMethodName Proxy = Y.methodPost
185+instance HttpMethod HEAD where
186+ type AllowsBody HEAD = 'NoBody
187+ httpMethodName Proxy = Y.methodHead
193+instance HttpMethod PUT where
194+ type AllowsBody PUT = 'CanHaveBody
195+ httpMethodName Proxy = Y.methodPut
197+-- | 'DELETE' method.
201+instance HttpMethod DELETE where
202+ type AllowsBody DELETE = 'NoBody
203+ httpMethodName Proxy = Y.methodDelete
209+instance HttpMethod TRACE where
210+ type AllowsBody TRACE = 'CanHaveBody
211+ httpMethodName Proxy = Y.methodTrace
213+-- | 'CONNECT' method.
215+data CONNECT = CONNECT
217+instance HttpMethod CONNECT where
218+ type AllowsBody CONNECT = 'CanHaveBody
219+ httpMethodName Proxy = Y.methodConnect
221+-- | 'OPTIONS' method.
223+data OPTIONS = OPTIONS
225+instance HttpMethod OPTIONS where
226+ type AllowsBody OPTIONS = 'NoBody
227+ httpMethodName Proxy = Y.methodOptions
233+instance HttpMethod PATCH where
234+ type AllowsBody PATCH = 'CanHaveBody
235+ httpMethodName Proxy = Y.methodPatch
237+-- | A type class for types that can be used as an HTTP method. To define a
238+-- non-standard method, follow this example that defines COPY:
240+-- > data COPY = COPY
242+-- > instance HttpMethod COPY where
243+-- > type AllowsMody COPY = 'CanHaveBody
244+-- > httpMethodName Proxy = "COPY"
246+class HttpMethod a where
248+ -- | Type function 'AllowsBody' returns type of kind 'CanHaveBody' which
249+ -- tells the rest of the library whether the method can have a body or
250+ -- not. We use the special type 'CanHaveBody' “lifted” into kind instead
251+ -- of 'Bool' to get more user-friendly compiler messages.
253+ type AllowsBody a :: CanHaveBody
255+ -- | Return name of the method as a 'ByteString'.
257+ httpMethodName :: Proxy a -> Y.Method
259+-- NOTE Now we state how to get an endomorphism on 'Request's that changes
260+-- given 'Request' so it has the specified method.
262+instance HttpMethod method => RequestComponent (Womb "method" method) where
263+ getRequestMod _ = Endo $ \x ->
264+ x { L.method = httpMethodName (Proxy :: Proxy method) }
97−-- | Example of request.
266+----------------------------------------------------------------------------
269+newtype Endpoint (scheme :: Scheme) = Endpoint { unEndpoint :: Text }
271+-- data Endpoint (s :: Scheme) where
272+-- Http' :: Text -> Endpoint 'Http
273+-- Https' :: Text -> Endpoint 'Https
274+-- (:/) :: Endpoint s -> Text -> Endpoint s
276+http :: Text -> Endpoint 'Http
277+http = Endpoint . percentEncode
279+https :: Text -> Endpoint 'Https
280+https = Endpoint . percentEncode
284+(/:) :: Endpoint scheme -> Text -> Endpoint scheme
285+(/:) = undefined -- TODO
287+-- TODO Escape things in endpoint and query parameters, only :/ should work
290+parseUrlHttp :: Text -> Endpoint 'Http
291+parseUrlHttp = undefined
293+parseUrlHttps :: Text -> Endpoint 'Https
294+parseUrlHttps = undefined
296+----------------------------------------------------------------------------
299+class HttpBody b where
300+ type ProvidesBody b :: CanHaveBody
301+ getReqestBody :: b -> ByteString -- FIXME should use a conduit here
303+data UrlEncodedParam = UrlEncodedParam Text (Maybe Text)
305+instance QueryParam UrlEncodedParam where
306+ queryParam = UrlEncodedParam
308+instance HttpBody b => RequestComponent (Womb "body" b) where
309+ getRequestMod = undefined -- FIXME
311+-- data ReqBody (b :: HasBody) where
312+-- NoReqBody :: ReqBody 'NoBody
313+-- ReqBodyLBS :: BL.ByteString -> ReqBody 'HasBody
314+-- ReqBodyBS :: B.ByteString -> ReqBody 'HasBody
315+-- ReqBodyBuilder :: Builder -> ReqBody 'HasBody
316+-- ReqBodyUrlEncoded :: [(Text,Text)] -> ReqBody 'HasBody
317+-- -- ↑ use something nicer than tuples? (:=) pairs perhaps? The pairs
318+-- -- should represent query parameters and body of url encoded stuff at the
320+-- ReqBodyJSON :: Value -> ReqBody 'HasBody
321+-- -- TODO add more? add conduit support and depend on http-conduit
323+----------------------------------------------------------------------------
324+-- Request — Optional parameters
326+-- | Opaque 'Option' type is a 'Monoid' you can use to pack collection of
327+-- optional parameters like query parameters and headers. We also provide
328+-- authorization helpers out-of-the-box which are also of this type.
330+newtype Option = Option { unOption :: Endo L.Request }
331+ deriving (Semigroup, Monoid)
333+instance RequestComponent Option where
334+ getRequestMod = unOption
336+----------------------------------------------------------------------------
337+-- Request — Optional parameters — Query Parameters
339+-- | Synonym of '(=:)' in form of function, not operator.
342+(=:) :: QueryParam a => Text -> Text -> a
343+name =: value = queryParam name (pure value)
347+queryFlag :: QueryParam a => Text -> a
348+queryFlag name = queryParam name Nothing
350+class QueryParam a where
351+ queryParam :: Text -> Maybe Text -> a
353+instance QueryParam Option where
354+ queryParam = undefined -- TODO
356+----------------------------------------------------------------------------
357+-- Request — Optional parameters — Headers
359+header :: Text -> Text -> Option
362+----------------------------------------------------------------------------
363+-- Request — Optional parameters — Authorization
365+-- Hmm, need to take a look at wreq first…
367+----------------------------------------------------------------------------
368+-- Request — Optional parameters — Other
370+port :: Natural -> Option
371+port = undefined -- TODO
373+redirectCount :: Natural -> Option
374+redirectCount = undefined -- TODO
378+----------------------------------------------------------------------------
381+-- Here we need to provide various options how to consume responses.
383+class HttpResponse response where
384+ interpretResponse :: ByteString -> response -- FIXME use conduit
386+----------------------------------------------------------------------------
389+newtype Womb (tag :: Symbol) a = Womb { unWomb :: a }
391+class RequestComponent a where
392+ getRequestMod :: a -> Endo L.Request
394+data CanHaveBody = CanHaveBody | NoBody
396+data Scheme = Http | Https
99−myReq :: ReaderT (ReqConfig 'Http) IO ()
398+percentEncode :: Text -> Text
100−myReq = req GET (http "example.org" :/ "v1" :/ "items") NoReqBody mempty
399+percentEncode = undefined