/
Session.hs
298 lines (260 loc) · 10.7 KB
/
Session.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
{-# LANGUAGE RankNTypes, RecordWildCards #-}
-- |
-- Module : Network.Wreq.Session
-- Copyright : (c) 2014 Bryan O'Sullivan
--
-- License : BSD-style
-- Maintainer : bos@serpentine.com
-- Stability : experimental
-- Portability : GHC
--
-- The functions in this module use a 'Session' to handle the
-- following common needs:
--
-- * TCP connection reuse. This is important for performance when
-- multiple requests go to a single server, particularly if TLS is
-- being used.
--
-- * Transparent cookie management. Any cookies set by the server
-- persist from one request to the next. (Bypass this overhead
-- using 'newAPISession'.)
--
--
-- This module is designed to be used alongside the "Network.Wreq"
-- module. Typical usage will look like this:
--
-- @
-- import "Network.Wreq"
-- import qualified "Network.Wreq.Session" as Sess
--
-- main = do
-- sess <- Sess.'newSession'
-- Sess.'get' sess \"http:\/\/httpbin.org\/get\"
-- @
--
-- We create a 'Session' using 'newSession', then pass the session to
-- subsequent functions. When talking to a REST-like service that does
-- not use cookies, it is more efficient to use 'newAPISession'.
--
-- Note the use of qualified import statements in the examples above,
-- so that we can refer unambiguously to the 'Session'-specific
-- implementation of HTTP GET.
--
-- One 'Network.HTTP.Client.Manager' (possibly set with 'newSessionControl') is used for all
-- session requests. The manager settings in the 'Options' parameter
-- for the 'getWith', 'postWith' and similar functions is ignored.
module Network.Wreq.Session
(
-- * Session creation
Session
, newSession
, newAPISession
, withSession
, withAPISession
-- ** More control-oriented session creation
, newSessionControl
, withSessionWith
, withSessionControl
-- ** Get information about session state
, getSessionCookieJar
-- * HTTP verbs
, get
, post
, head_
, options
, put
, delete
, customMethod
-- ** Configurable verbs
, getWith
, postWith
, headWith
, optionsWith
, putWith
, deleteWith
, customMethodWith
, customPayloadMethodWith
, customHistoriedMethodWith
, customHistoriedPayloadMethodWith
-- * Extending a session
, Lens.seshRun
) where
import Control.Lens ((&), (.~))
import Data.Foldable (forM_)
import Data.IORef (newIORef, readIORef, writeIORef)
import Network.Wreq (Options, Response, HistoriedResponse)
import Network.Wreq.Internal
import Network.Wreq.Internal.Types (Body(..), Req(..), Session(..), RunHistory)
import Network.Wreq.Types (Postable, Putable, Run)
import Prelude hiding (head)
import qualified Data.ByteString.Char8 as BC8
import qualified Data.ByteString.Lazy as L
import qualified Network.HTTP.Client as HTTP
import qualified Network.Wreq.Internal.Lens as Lens
import qualified Network.Wreq.Lens as Lens
import Data.Traversable as T
-- | Create a 'Session', passing it to the given function. The
-- 'Session' will no longer be valid after that function returns.
--
-- This session manages cookies and uses default session manager
-- configuration.
withSession :: (Session -> IO a) -> IO a
withSession act = newSession >>= act
{-# DEPRECATED withSession "Use newSession instead." #-}
-- | Create a 'Session'.
--
-- This session manages cookies and uses default session manager
-- configuration.
--
-- @since 0.5.2.0
newSession :: IO Session
newSession = newSessionControl (Just (HTTP.createCookieJar [])) defaultManagerSettings
-- | Create a session.
--
-- This uses the default session manager settings, but does not manage
-- cookies. It is intended for use with REST-like HTTP-based APIs,
-- which typically do not use cookies.
withAPISession :: (Session -> IO a) -> IO a
withAPISession act = newAPISession >>= act
{-# DEPRECATED withAPISession "Use newAPISession instead." #-}
-- | Create a session.
--
-- This uses the default session manager settings, but does not manage
-- cookies. It is intended for use with REST-like HTTP-based APIs,
-- which typically do not use cookies.
--
-- @since 0.5.2.0
newAPISession :: IO Session
newAPISession = newSessionControl Nothing defaultManagerSettings
-- | Create a session, using the given manager settings. This session
-- manages cookies.
withSessionWith :: HTTP.ManagerSettings -> (Session -> IO a) -> IO a
withSessionWith = withSessionControl (Just (HTTP.createCookieJar []))
{-# DEPRECATED withSessionWith "Use newSessionControl instead." #-}
-- | Create a session, using the given cookie jar and manager settings.
withSessionControl :: Maybe HTTP.CookieJar
-- ^ If 'Nothing' is specified, no cookie management
-- will be performed.
-> HTTP.ManagerSettings
-> (Session -> IO a) -> IO a
withSessionControl mj settings act = do
sess <- newSessionControl mj settings
act sess
{-# DEPRECATED withSessionControl "Use newSessionControl instead." #-}
-- | Create a session, using the given cookie jar and manager settings.
--
-- @since 0.5.2.0
newSessionControl :: Maybe HTTP.CookieJar
-- ^ If 'Nothing' is specified, no cookie management
-- will be performed.
-> HTTP.ManagerSettings
-> IO Session
newSessionControl mj settings = do
mref <- maybe (return Nothing) (fmap Just . newIORef) mj
mgr <- HTTP.newManager settings
return Session { seshCookies = mref
, seshManager = mgr
, seshRun = runWith
, seshRunHistory = runWithHistory
}
-- | Extract current 'Network.HTTP.Client.CookieJar' from a 'Session'
--
-- @since 0.5.2.0
getSessionCookieJar :: Session -> IO (Maybe HTTP.CookieJar)
getSessionCookieJar = T.traverse readIORef . seshCookies
-- | 'Session'-specific version of 'Network.Wreq.get'.
get :: Session -> String -> IO (Response L.ByteString)
get = getWith defaults
-- | 'Session'-specific version of 'Network.Wreq.post'.
post :: Postable a => Session -> String -> a -> IO (Response L.ByteString)
post = postWith defaults
-- | 'Session'-specific version of 'Network.Wreq.head_'.
head_ :: Session -> String -> IO (Response ())
head_ = headWith (defaults & Lens.redirects .~ 0)
-- | 'Session'-specific version of 'Network.Wreq.options'.
options :: Session -> String -> IO (Response ())
options = optionsWith defaults
-- | 'Session'-specific version of 'Network.Wreq.put'.
put :: Putable a => Session -> String -> a -> IO (Response L.ByteString)
put = putWith defaults
-- | 'Session'-specific version of 'Network.Wreq.delete'.
delete :: Session -> String -> IO (Response L.ByteString)
delete = deleteWith defaults
-- | 'Session'-specific version of 'Network.Wreq.customMethod'.
customMethod :: String -> Session -> String -> IO (Response L.ByteString)
customMethod = flip customMethodWith defaults
-- | 'Session'-specific version of 'Network.Wreq.getWith'.
getWith :: Options -> Session -> String -> IO (Response L.ByteString)
getWith opts sesh url = run string sesh =<< prepareGet opts url
-- | 'Session'-specific version of 'Network.Wreq.postWith'.
postWith :: Postable a => Options -> Session -> String -> a
-> IO (Response L.ByteString)
postWith opts sesh url payload =
run string sesh =<< preparePost opts url payload
-- | 'Session'-specific version of 'Network.Wreq.headWith'.
headWith :: Options -> Session -> String -> IO (Response ())
headWith opts sesh url = run ignore sesh =<< prepareHead opts url
-- | 'Session'-specific version of 'Network.Wreq.optionsWith'.
optionsWith :: Options -> Session -> String -> IO (Response ())
optionsWith opts sesh url = run ignore sesh =<< prepareOptions opts url
-- | 'Session'-specific version of 'Network.Wreq.putWith'.
putWith :: Putable a => Options -> Session -> String -> a
-> IO (Response L.ByteString)
putWith opts sesh url payload = run string sesh =<< preparePut opts url payload
-- | 'Session'-specific version of 'Network.Wreq.deleteWith'.
deleteWith :: Options -> Session -> String -> IO (Response L.ByteString)
deleteWith opts sesh url = run string sesh =<< prepareDelete opts url
-- | 'Session'-specific version of 'Network.Wreq.customMethodWith'.
customMethodWith :: String -> Options -> Session -> String -> IO (Response L.ByteString)
customMethodWith method opts sesh url = run string sesh =<< prepareMethod methodBS opts url
where
methodBS = BC8.pack method
-- | 'Session'-specific version of 'Network.Wreq.customHistoriedMethodWith'.
--
-- @since 0.5.2.0
customHistoriedMethodWith :: String -> Options -> Session -> String -> IO (HistoriedResponse L.ByteString)
customHistoriedMethodWith method opts sesh url =
runHistory stringHistory sesh =<< prepareMethod methodBS opts url
where
methodBS = BC8.pack method
-- | 'Session'-specific version of 'Network.Wreq.customPayloadMethodWith'.
customPayloadMethodWith :: Postable a => String -> Options -> Session -> String -> a
-> IO (Response L.ByteString)
customPayloadMethodWith method opts sesh url payload =
run string sesh =<< preparePayloadMethod methodBS opts url payload
where
methodBS = BC8.pack method
-- | 'Session'-specific version of 'Network.Wreq.customHistoriedPayloadMethodWith'.
--
-- @since 0.5.2.0
customHistoriedPayloadMethodWith :: Postable a => String -> Options -> Session -> String -> a
-> IO (HistoriedResponse L.ByteString)
customHistoriedPayloadMethodWith method opts sesh url payload =
runHistory stringHistory sesh =<< preparePayloadMethod methodBS opts url payload
where
methodBS = BC8.pack method
runWithGeneric :: (resp -> Response b) -> Session -> (Req -> IO resp) -> Req -> IO resp
runWithGeneric extract Session{..} act (Req _ req) = do
req' <- (\c -> req & Lens.cookieJar .~ c) `fmap` T.traverse readIORef seshCookies
resp <- act (Req (Right seshManager) req')
forM_ seshCookies $ \ref ->
writeIORef ref (HTTP.responseCookieJar (extract resp))
return resp
runWith :: Session -> Run Body -> Run Body
runWith = runWithGeneric id
runWithHistory :: Session -> RunHistory Body -> RunHistory Body
runWithHistory = runWithGeneric HTTP.hrFinalResponse
type Mapping a = (Body -> a, a -> Body, Run a)
type MappingHistory a = (Body -> a, a -> Body, RunHistory a)
run :: Mapping a -> Session -> Run a
run (to,from,act) sesh =
fmap (fmap to) . seshRun sesh sesh (fmap (fmap from) . act)
runHistory :: MappingHistory a -> Session -> RunHistory a
runHistory (to,from,act) sesh =
fmap (fmap to) . seshRunHistory sesh sesh (fmap (fmap from) . act)
string :: Mapping L.ByteString
string = (\(StringBody s) -> s, StringBody, runRead)
stringHistory :: MappingHistory L.ByteString
stringHistory = (\(StringBody s) -> s, StringBody, runReadHistory)
ignore :: Mapping ()
ignore = (const (), const NoBody, runIgnore)