Skip to content

Commit a0920af

Browse files
authored
Merge pull request #98 from creichert/http-client-backend
Implement http-client backend for stripe requests
2 parents 1a88579 + 56f4067 commit a0920af

File tree

7 files changed

+272
-1
lines changed

7 files changed

+272
-1
lines changed

.travis.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,19 @@ matrix:
1010
- env: GHCVER=ghc763
1111
- env: GHCVER=ghc783
1212
- env: GHCVER=ghc784
13+
- env: GHCVER=ghc801
14+
- env: GHCVER=ghc802
15+
- env: GHCVER=ghc822
1316
- env: GHCVER=ghcHEAD
1417
allow_failures:
1518
- env: GHCVER=ghc742
1619
- env: GHCVER=ghc763
1720
- env: GHCVER=ghc783
1821
- env: GHCVER=ghc784
1922
- env: GHCVER=ghc7103
23+
- env: GHCVER=ghc801
24+
- env: GHCVER=ghc802
25+
- env: GHCVER=ghc822
2026
- env: GHCVER=ghcHEAD
2127

2228
before_install:
@@ -25,4 +31,3 @@ before_install:
2531

2632
script:
2733
- nix-build -A stripe-haskell --argstr compiler $GHCVER
28-

stack.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ packages:
22
- stripe-core/
33
- stripe-haskell/
44
- stripe-http-streams/
5+
- stripe-http-client/
56
- stripe-tests/
67
resolver: lts-9.5

stripe-http-client/LICENSE

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2018 Christopher Reichert
4+
5+
Permission is hereby granted, free of charge, to any person obtaining
6+
a copy of this software and associated documentation files (the
7+
"Software"), to deal in the Software without restriction, including
8+
without limitation the rights to use, copy, modify, merge, publish,
9+
distribute, sublicense, and/or sell copies of the Software, and to
10+
permit persons to whom the Software is furnished to do so, subject to
11+
the following conditions:
12+
13+
The above copyright notice and this permission notice shall be
14+
included in all copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

stripe-http-client/Setup.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import Distribution.Simple
2+
main = defaultMain
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
{-# LANGUAGE FlexibleContexts #-}
2+
{-# LANGUAGE OverloadedStrings #-}
3+
4+
module Web.Stripe.Client.HttpClient
5+
(
6+
StripeRequest(..)
7+
, StripeError(..)
8+
, StripeConfig(..)
9+
10+
, stripe
11+
, stripeManager
12+
, stripeConn
13+
14+
-- * low-level
15+
, withConnection
16+
, withManager
17+
, callAPI
18+
19+
) where
20+
21+
import qualified Control.Arrow
22+
import qualified Data.ByteString.Lazy as BSL
23+
import qualified Data.Text.Encoding as TE
24+
import qualified Network.HTTP.Types as Http
25+
26+
import Data.Aeson as A
27+
import Data.ByteString (ByteString)
28+
import Data.Monoid ((<>))
29+
import Network.HTTP.Client as Http hiding (withManager)
30+
import Network.HTTP.Client.TLS as TLS
31+
32+
import qualified Web.Stripe.StripeRequest as S
33+
34+
import Web.Stripe.Client (APIVersion (..), StripeConfig (..),
35+
StripeError (..), StripeKey (..),
36+
StripeRequest, StripeReturn,
37+
attemptDecode, handleStream,
38+
parseFail, toBytestring, unknownCode)
39+
40+
41+
-- | Create a request to 'Stripe's API.
42+
--
43+
-- This function uses the global TLS manager from @http-client-tls@
44+
-- via 'getGlobalManager'.
45+
stripe :: FromJSON (StripeReturn a)
46+
=> StripeConfig
47+
-> StripeRequest a
48+
-> IO (Either StripeError (StripeReturn a))
49+
stripe config request = do
50+
man <- TLS.getGlobalManager
51+
callAPI man fromJSON config request
52+
53+
-- | Create a request to 'Stripe's API using a 'Manager'.
54+
stripeManager :: FromJSON (StripeReturn a)
55+
=> Manager
56+
-> StripeConfig
57+
-> StripeRequest a
58+
-> IO (Either StripeError (StripeReturn a))
59+
stripeManager manager config request = callAPI manager fromJSON config request
60+
61+
-- | Create a request to 'Stripe's API using a 'Manager'.
62+
--
63+
-- This function is used to maintain compatibility w/
64+
-- @stripe-http-streams@. However, the terminology in @http-streams@
65+
-- uses 'Connection' whereas @http-client@ uses connection 'Manager'.
66+
stripeConn :: FromJSON (StripeReturn a)
67+
=> Manager
68+
-> StripeConfig
69+
-> StripeRequest a
70+
-> IO (Either StripeError (StripeReturn a))
71+
stripeConn = stripeManager
72+
73+
withConnection :: (Manager -> IO (Either StripeError a))
74+
-> IO (Either StripeError a)
75+
withConnection = withManager
76+
77+
withManager :: (Manager -> IO (Either StripeError a))
78+
-> IO (Either StripeError a)
79+
withManager m = do
80+
81+
-- @http-client@ has a set of deprecated `withManager` functions
82+
-- that are not necessary to safely prevent a 'Manager' from
83+
-- leaking resources. "Manager's will be closed and shutdown
84+
-- automatically (and safely) via gargage collection.
85+
manager <- TLS.getGlobalManager
86+
m manager
87+
88+
-- | Create a request to 'Stripe's API using an existing 'Manager'
89+
--
90+
-- This is a low-level function. In most cases you probably want to
91+
-- use 'stripe' or 'stripeManager'.
92+
callAPI :: Manager
93+
-> (Value -> Result b)
94+
-> StripeConfig
95+
-> StripeRequest a
96+
-> IO (Either StripeError b)
97+
callAPI man fromJSON' config stripeRequest = do
98+
99+
res <- httpLbs mkStripeRequest man
100+
101+
let status = Http.statusCode (Http.responseStatus res)
102+
103+
if not (attemptDecode status) then
104+
return unknownCode
105+
106+
else do
107+
case A.eitherDecode (Http.responseBody res) of
108+
Left e -> pure $ parseFail e
109+
Right a -> pure $ handleStream fromJSON' status $ return a
110+
where
111+
mkStripeRequest =
112+
113+
let req = Http.applyBasicAuth (getStripeKey (secretKey config)) mempty $
114+
defaultRequest {
115+
Http.method = m2m (S.method stripeRequest)
116+
, Http.secure = True
117+
, Http.host = "api.stripe.com"
118+
, Http.port = 443
119+
, Http.path = "/v1/" <> TE.encodeUtf8 (S.endpoint stripeRequest)
120+
, Http.requestHeaders = [
121+
("Stripe-Version", toBytestring stripeVersion)
122+
, ("Connection", "Keep-Alive")
123+
]
124+
, Http.checkResponse = \_ _ -> return ()
125+
}
126+
127+
stripeQueryParams = fmap
128+
(Control.Arrow.second Just)
129+
(S.queryParams stripeRequest)
130+
131+
in if S.GET == S.method stripeRequest then
132+
Http.setQueryString stripeQueryParams req
133+
else
134+
urlEncodeBody (S.queryParams stripeRequest) req
135+
136+
m2m :: S.Method -> Http.Method
137+
m2m S.GET = Http.methodGet
138+
m2m S.POST = Http.methodPost
139+
m2m S.DELETE = Http.methodDelete
140+
141+
-- | This function is used instead of http-client's built-in 'urlEncodedBody' as
142+
-- the request method is set explicitly to POST in 'urlEncodeBody' but Stripe
143+
-- uses POST\/PUT\/DELETE. A PR should be submitted to http-client to fix
144+
-- eventually.
145+
urlEncodeBody :: [(ByteString, ByteString)] -> Request -> Request
146+
urlEncodeBody headers req = req {
147+
requestBody = RequestBodyLBS (BSL.fromChunks body)
148+
, requestHeaders =
149+
("Content-Type", "application/x-www-form-urlencoded")
150+
: filter (\(x, _) -> x /= "Content-Type") (requestHeaders req)
151+
}
152+
where
153+
body = pure (Http.renderSimpleQuery False headers)
154+
155+
stripeVersion :: APIVersion
156+
stripeVersion = V20141007
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: stripe-http-client
2+
version: 0.1
3+
license: MIT
4+
license-file: LICENSE
5+
author: Christopher Reichert
6+
synopsis: Stripe API for Haskell - http-client backend
7+
maintainer: creichert07@gmail.com
8+
copyright: Copyright (c) 2018 Christopher Reichert
9+
category: Web
10+
build-type: Simple
11+
cabal-version: >=1.10
12+
Description:
13+
.
14+
<<https://stripe.com/img/navigation/logo@2x.png>>
15+
.
16+
[Access Stripe API using http-client]
17+
This package provides access to the Stripe API using `stripe-core`
18+
and `http-client`. See also the `stripe` package.
19+
20+
library
21+
hs-source-dirs: src
22+
default-language: Haskell2010
23+
exposed-modules: Web.Stripe.Client.HttpClient
24+
ghc-options: -Wall
25+
other-extensions: OverloadedStrings
26+
RecordWildCards
27+
build-depends: base >= 4.7 && < 5
28+
, bytestring >= 0.10 && < 0.11
29+
, text >= 1.1 && < 1.3
30+
, aeson >= 0.8 && < 0.10 || >= 0.11 && < 1.3
31+
, http-client
32+
, http-client-tls
33+
, http-types
34+
, stripe-core
35+
36+
Test-Suite tests
37+
type: exitcode-stdio-1.0
38+
main-is: Main.hs
39+
hs-source-dirs: tests
40+
default-language: Haskell2010
41+
build-depends: base >= 4.7 && < 5
42+
, free >= 4.10 && < 4.13
43+
, hspec >= 2.1.0 && < 2.5
44+
, stripe-core
45+
, stripe-tests
46+
, http-client
47+
, stripe-http-client
48+
ghc-options: -Wall -threaded -rtsopts
49+
50+
source-repository head
51+
type: git
52+
subdir: stripe-http-client
53+
location: git://github.com/dmjio/stripe-haskell.git

stripe-http-client/tests/Main.hs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
module Main where
2+
3+
import Control.Monad.Trans.Free (FreeF(..), FreeT(..))
4+
import Network.HTTP.Client
5+
import Web.Stripe.Client (StripeConfig(..), StripeError(..))
6+
import Web.Stripe.Client.HttpClient (withConnection, callAPI)
7+
import Web.Stripe.Test.AllTests (allTests)
8+
import Web.Stripe.Test.Prelude (Stripe, StripeRequestF(..))
9+
10+
main :: IO ()
11+
main = allTests runStripe
12+
13+
runStripe :: StripeConfig
14+
-> Stripe a
15+
-> IO (Either StripeError a)
16+
runStripe config stripe =
17+
withConnection $ \conn ->
18+
runStripe' conn config stripe
19+
20+
runStripe' :: Manager
21+
-> StripeConfig
22+
-> Stripe a
23+
-> IO (Either StripeError a)
24+
runStripe' manager config (FreeT m) =
25+
do f <- m
26+
case f of
27+
(Pure a) -> return (Right a)
28+
(Free (StripeRequestF req decode')) ->
29+
do r <- callAPI manager decode' config req
30+
case r of
31+
(Left e) -> return (Left e)
32+
(Right next) -> runStripe' manager config next

0 commit comments

Comments
 (0)