wreq 0.3.0.0 → 0.3.0.1
raw patch · 14 files changed
+266/−36 lines, 14 filesdep ~http-client
Dependency ranges changed: http-client
Files
- Network/Wreq.hs +18/−0
- Network/Wreq/Internal/Lens.hs +10/−0
- Network/Wreq/Internal/Types.hs +8/−3
- Network/Wreq/Lens.hs +5/−1
- Network/Wreq/Session.hs +52/−0
- Network/Wreq/Types.hs +1/−1
- changelog +0/−19
- changelog.md +23/−0
- examples/SimpleSession.hs +24/−0
- examples/wreq-examples.cabal +11/−0
- tests/UnitTests.hs +43/−6
- wreq.cabal +3/−3
- www/index.md +14/−3
- www/tutorial.md +54/−0
Network/Wreq.hs view
@@ -37,6 +37,10 @@ module Network.Wreq ( -- * HTTP verbs++ -- ** Sessions+ -- $session+ -- ** GET get , getWith@@ -525,6 +529,20 @@ -> String -- ^ The body for this 'Form.Part'. -> Form.Part partString name value = Form.partBS name (encodeUtf8 (T.pack value))++-- $session+--+-- The basic HTTP functions ('get', 'post', and so on) in this module+-- have a few key drawbacks:+--+-- * If several requests go to the same server, there is no reuse of+-- TCP connections.+--+-- * There is no management of cookies across multiple requests.+--+-- This makes these functions inefficient and verbose for many common+-- uses. For greater efficiency, use the "Network.Wreq.Session"+-- module. -- $cookielenses --
Network/Wreq/Internal/Lens.hs view
@@ -30,6 +30,7 @@ , assoc , assoc2 , setHeader+ , maybeSetHeader , deleteKey ) where @@ -55,8 +56,17 @@ assoc2 k f = fmap (uncurry ((++) . fmap ((,) k))) . _1 (f . fmap snd) . partition ((==k) . fst) +-- | Set a header to the given value, replacing any prior value. setHeader :: HeaderName -> S.ByteString -> Request -> Request setHeader name value = requestHeaders %~ ((name,value) :) . deleteKey name++-- | Set a header to the given value, but only if the header was not+-- already set.+maybeSetHeader :: HeaderName -> S.ByteString -> Request -> Request+maybeSetHeader name value = requestHeaders %~+ \hdrs -> case lookup name hdrs of+ Just _ -> hdrs+ Nothing -> (name,value) : hdrs deleteKey :: (Eq a) => a -> [(a,b)] -> [(a,b)] deleteKey key = filter ((/= key) . fst)
Network/Wreq/Internal/Types.hs view
@@ -149,13 +149,13 @@ -- etc.), this field will be used only for the /first/ HTTP request -- to be issued during a 'Network.Wreq.Session.Session'. Any changes -- changes made for subsequent requests will be ignored.- , checkStatus :: + , checkStatus :: Maybe (Status -> ResponseHeaders -> CookieJar -> Maybe SomeException) -- ^ Function that checks the status code and potentially returns an exception. -- -- This defaults to 'Nothing', which will just use the default of -- 'Network.HTTP.Client.Request' which throws a 'StatusException' if the status- -- is not 2XX. + -- is not 2XX. } deriving (Typeable) -- | Supported authentication types.@@ -255,6 +255,10 @@ -- | A request that is ready to be submitted. data Req = Req Mgr Request +-- | Return the URL associated with the given 'Req'.+--+-- This includes the port number if not standard, and the query string+-- if one exists. reqURL :: Req -> S.ByteString reqURL (Req _ req) = mconcat [ if https then "https" else "http"@@ -275,7 +279,8 @@ -- response. type Run body = Req -> IO (Response body) --- | A session that spans multiple requests.+-- | A session that spans multiple requests. This is responsible for+-- cookie management and TCP connection reuse. data Session = Session { seshCookies :: MVar CookieJar , seshManager :: Manager
Network/Wreq/Lens.hs view
@@ -167,7 +167,7 @@ -- Example (note the use of TLS): -- -- @---let opts = 'Network.Wreq.defaults' 'Control.Lens.&' 'Lens.auth' 'Control.Lens..~' 'Network.Wreq.basicAuth' \"user\" \"pass\"+--let opts = 'Network.Wreq.defaults' 'Control.Lens.&' 'Lens.auth' 'Control.Lens.?~' 'Network.Wreq.basicAuth' \"user\" \"pass\" --'Network.Wreq.getWith' opts \"https:\/\/httpbin.org\/basic-auth\/user\/pass\" -- @ auth :: Lens' Options (Maybe Auth)@@ -232,6 +232,10 @@ checkStatus = TH.checkStatus -- | A traversal onto the cookie with the given name, if one exists.+--+-- N.B. This is an \"illegal\" 'Traversal'': we can change the+-- 'cookieName' of the associated 'Cookie' so that it differs from the+-- name provided to this function. cookie :: ByteString -> Traversal' Options Cookie cookie = TH.cookie
Network/Wreq/Session.hs view
@@ -1,5 +1,42 @@ {-# LANGUAGE RankNTypes, RecordWildCards #-} +-- |+-- Module : Network.Wreq+-- 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.+--+--+-- 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 = Sess.'withSession' $ \\sess ->+-- Sess.'get' sess \"http:\/\/httpbin.org\/get\"+-- @+--+-- We create a 'Session' using 'withSession', then pass the session to+-- subsequent functions.+--+-- Note the use of a qualified import statement, so that we can refer+-- unambiguously to the 'Session'-specific implementation of HTTP GET.+ module Network.Wreq.Session ( Session@@ -35,9 +72,12 @@ import qualified Network.Wreq as Wreq import qualified Network.Wreq.Internal.Lens as Lens +-- | Create a 'Session', passing it to the given function. The+-- 'Session' will no longer be valid after that function returns. withSession :: (Session -> IO a) -> IO a withSession = withSessionWith defaultManagerSettings +-- | Create a session, using the given manager settings. withSessionWith :: HTTP.ManagerSettings -> (Session -> IO a) -> IO a withSessionWith settings act = do mv <- newMVar $ HTTP.createCookieJar []@@ -47,42 +87,54 @@ , seshRun = runWith } +-- | '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 +-- | '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.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
Network/Wreq/Types.hs view
@@ -142,5 +142,5 @@ payload :: S.ByteString -> HTTP.RequestBody -> Request -> IO Request payload ct body req = AWS.addTmpPayloadHashHeader $ req- & Lens.setHeader "Content-Type" ct+ & Lens.maybeSetHeader "Content-Type" ct & Lens.requestBody .~ body
− changelog
@@ -1,19 +0,0 @@--*- markdown -*---2014-12-02 0.3.0.0--* Support for Amazon Web Services request signing--* New customMethod, customMethodWith functions allow use of arbitrary- HTTP verbs--* httpProxy, basicAuth, oauth2Bearer, oauth2Token: removed Maybe from- result types, changed documentation to suggest use of (?~)--2014-08-25 0.2.0.0--* Support for lens 4.4--2014-04-22 0.1.0.0--* Initial release.
+ changelog.md view
@@ -0,0 +1,23 @@+-*- markdown -*-++2014-12-11 0.3.0.1++* Bump lower bound on http-client to 0.3.0.1++2014-12-02 0.3.0.0++* Support for Amazon Web Services request signing++* New customMethod, customMethodWith functions allow use of arbitrary+ HTTP verbs++* httpProxy, basicAuth, oauth2Bearer, oauth2Token: removed Maybe from+ result types, changed documentation to suggest use of (?~)++2014-08-25 0.2.0.0++* Support for lens 4.4++2014-04-22 0.1.0.0++* Initial release.
+ examples/SimpleSession.hs view
@@ -0,0 +1,24 @@+{-# LANGUAGE OverloadedStrings #-}++import Control.Lens+import Network.Wreq+import qualified Network.Wreq.Session as S++main :: IO ()+main = S.withSession $ \sess -> do+ -- Our first request causes the httpbin.org server to set a cookie+ -- in its response.++ S.get sess "http://httpbin.org/cookies/set?name=hi"++ -- The session value manages both cookies and HTTP connection reuse+ -- for us. When we issue the second request, it should+ -- transparently reuse the same connection, and also send the+ -- cookies that we set during the first request.++ r2 <- S.post sess "http://httpbin.org/post" ["a" := (3 :: Int)]++ -- And here's where we verify that the cookie is still set on the+ -- second request.++ print $ r2 ^. responseCookie "name" . cookieValue
examples/wreq-examples.cabal view
@@ -26,6 +26,17 @@ text, wreq ++executable wreq-example-simple-session+ main-is: SimpleSession.hs+ ghc-options: -Wall -fwarn-tabs -threaded+ default-language: Haskell98++ build-depends:+ base >= 4.5 && < 5,+ lens,+ wreq+ executable upload-paste main-is: UploadPaste.hs ghc-options: -Wall -fwarn-tabs -threaded
tests/UnitTests.hs view
@@ -11,7 +11,7 @@ import Control.Exception (Exception, toException) import Control.Lens ((^.), (^?), (.~), (?~), (&)) import Control.Monad (unless, void)-import Data.Aeson (Value(..), object)+import Data.Aeson import Data.Aeson.Lens (key) import Data.ByteString (ByteString) import Data.Char (toUpper)@@ -113,6 +113,41 @@ r <- put (site "/put") ("wibble" :: ByteString) assertEqual "PUT succeeds" status200 (r ^. responseStatus) +data SolrAdd = SolrAdd+ { doc :: String+ , boost :: Float+ , overwrite :: Bool+ , commitWithin :: Integer+ }++instance ToJSON SolrAdd where+ toJSON (SolrAdd doc boost overwrite commitWithin) =+ object+ [+ "add" .= object+ [ "doc" .= toJSON doc+ , "boost" .= boost+ , "overwrite" .= overwrite+ , "commitWithin" .= commitWithin+ ]+ ]++solrAdd :: SolrAdd+solrAdd = SolrAdd "wibble" 1.0 True 10000++jsonPut Verb{..} site = do+ r <- put (site "/put") $ toJSON solrAdd+ assertEqual "toJSON PUT request has correct Content-Type header"+ (Just "application/json")+ (r ^. responseBody ^? key "headers" . key "Content-Type")++byteStringPut Verb{..} site = do+ let opts = defaults & header "Content-Type" .~ ["application/json"]+ r <- putWith opts (site "/put") $ encode solrAdd+ assertEqual "ByteString PUT request has correct Content-Type header"+ (Just "application/json")+ (r ^. responseBody ^? key "headers" . key "Content-Type")+ basicDelete Verb{..} site = do r <- delete (site "/delete") assertEqual "DELETE succeeds" status200 (r ^. responseStatus)@@ -181,16 +216,16 @@ r <- getWith opts (site "/status/404") assertThrows "Non 404 throws error" inspect $ getWith opts (site "/get")- assertEqual "Status 404" + assertEqual "Status 404" 404 (r ^. responseStatus . statusCode)- where - customCs (Status 404 _) _ _ = Nothing + where+ customCs (Status 404 _) _ _ = Nothing customCs s h cj = Just . toException . StatusCodeException s h $ cj inspect e = case e of- (StatusCodeException (Status sc _) _ _) -> - assertEqual "200 Status Error" sc 200 + (StatusCodeException (Status sc _) _ _) ->+ assertEqual "200 Status Error" sc 200 getGzip Verb{..} site = do r <- get (site "/gzip")@@ -266,6 +301,8 @@ , testCase "params" $ getParams verb site , testCase "headers" $ getHeaders verb site , testCase "gzip" $ getGzip verb site+ , testCase "json put" $ jsonPut verb site+ , testCase "bytestring put" $ byteStringPut verb site , testCase "cookiesSet" $ cookiesSet verb site , testCase "getWithManager" $ getWithManager site , testCase "cookieSession" $ cookieSession site
wreq.cabal view
@@ -1,5 +1,5 @@ name: wreq-version: 0.3.0.0+version: 0.3.0.1 synopsis: An easy-to-use HTTP client library. description: .@@ -40,7 +40,7 @@ extra-source-files: README.md TODO.md- changelog+ changelog.md examples/*.cabal examples/*.hs www/*.css@@ -100,7 +100,7 @@ exceptions >= 0.5, ghc-prim, hashable,- http-client >= 0.4.3,+ http-client >= 0.4.6, http-client-tls >= 0.2, http-types >= 0.8, lens >= 4.5,
www/index.md view
@@ -34,13 +34,24 @@ # Whirlwind tour +All of the examples that follow assume that you are using the+[`OverloadedStrings`](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/type-class-extensions.html#overloaded-strings)+language extension, which you can enable in `ghci` as follows:+ ~~~~ {.haskell}+ghci> :set -XOverloadedStrings+~~~~++And now let's get started.++~~~~ {.haskell} ghci> import Network.Wreq ghci> r <- get "http://httpbin.org/get" ~~~~ -Its `lens`-based API is easy to learn (the tutorial walks you through-the [basics of lenses](tutorial.html#a-quick-lens-backgrounder) and+The `wreq` library's `lens`-based API is easy to learn (the tutorial+walks you through the+[basics of lenses](tutorial.html#a-quick-lens-backgrounder)) and powerful to work with. ~~~~ {.haskell}@@ -55,7 +66,7 @@ ~~~~ {.haskell} ghci> let opts = defaults & param "q" .~ ["tetris"] & param "language" .~ ["haskell"]-ghci> r <- getWith opts "https://api.github.com/search/code"+ghci> r <- getWith opts "https://api.github.com/search/repositories" ~~~~ Haskell-to-JSON interoperation is seamless.
www/tutorial.md view
@@ -414,7 +414,10 @@ "bar" ~~~~ +To make cookies even easier to deal with, we'll want to+[use the `Session` API](#session), but we'll come back to that later. + # Authentication The `wreq` library supports both basic authentication and OAuth2@@ -540,3 +543,54 @@ authentication during a session, to avoid the need for an unauthenticated failure followed by an authenticated success if we visit the same endpoint repeatedly.)+++# Handling multiple HTTP requests++<a id="session">For non-trivial applications</a>, we'll always want to+use a+[`Session`](http://hackage.haskell.org/package/wreq/docs/Network-Wreq-Session.html#t:Session)+to efficiently and correctly handle multiple requests.++The `Session` API provides two important features:++* When we issue multiple HTTP requests to the same server, a `Session`+ will reuse TCP and TLS connections for us. (The simpler API we've+ discussed so far does not do this.) This greatly improves+ efficiency.++* A `Session` transparently manages HTTP cookies. (We can manage them+ by hand, but it's awkward and verbose, so we won't cover it in this+ tutorial.)++Here's a complete example.++~~~~ {.haskell}+{-# LANGUAGE OverloadedStrings #-}++import Control.Lens+import Network.Wreq+import qualified Network.Wreq.Session as S++main :: IO ()+main = S.withSession $ \sess -> do+ -- First request: tell the server to set a cookie+ S.get sess "http://httpbin.org/cookies/set?name=hi"++ -- Second request: the cookie should still be set afterwards.+ r <- S.post sess "http://httpbin.org/post" ["a" := (3 :: Int)]+ print $ r ^. responseCookie "name" . cookieValue+~~~~++The key differences from the basic API are as follows.++* We import the+ [`Network.Wreq.Session`](http://hackage.haskell.org/package/wreq/docs/Network-Wreq-Session.html)+ module qualified, and we'll identify its functions by prefixing them+ with "`S.`".++* To create a `Session`, we use `S.withSession`. It calls our code+ with `sess`, the `Session` value we'll use.++* Instead of `get` and `post`, we call the `Session`-specific+ versions, [`S.get`](http://hackage.haskell.org/package/wreq/docs/Network-Wreq-Session.html#v:get) and [`S.post`](http://hackage.haskell.org/package/wreq/docs/Network-Wreq-Session.html#v:post), and pass `sess` to each of them.