stripe-wreq (empty) → 1.0.0.0
raw patch · 4 files changed
+440/−0 lines, 4 filesdep +aesondep +basedep +bytestring
Dependencies added: aeson, base, bytestring, lens, stripe-concepts, text, unordered-containers, wreq
Files
- changelog.md +12/−0
- library/Stripe/Wreq.hs +362/−0
- license.txt +18/−0
- stripe-wreq.cabal +48/−0
+ changelog.md view
@@ -0,0 +1,12 @@+# Changelog++All notable changes to this package will be documented in this file.++The format is based on+[Keep a Changelog](https://keepachangelog.com/en/1.0.0/),+and this project adheres to the+[Haskell Package Versioning Policy](https://pvp.haskell.org/).++## 1.0.0.0 - 2018-12-20++- Initial release
+ library/Stripe/Wreq.hs view
@@ -0,0 +1,362 @@+{-# OPTIONS_GHC -Wall #-}++{- | Typical use of this module:++1. Run 'get', 'post', or 'delete' to get a 'WreqResponse'.+2. Use 'wreqResponse' to convert the Wreq response to a 'Response'.+3. Use 'responseValue' to obtain the response payload as an Aeson+ 'Data.Aeson.Value' (or an 'Error' if the request was not successful). -}++module Stripe.Wreq+ (+ -- * Request+ -- ** GET+ get, Get (..)+ -- ** POST+ , post, Post (..)+ -- ** DELETE+ , delete, Delete (..)++ -- * Response+ , WreqResponse, Response (..)+ , wreqResponse, responseValue, responseValueError++ -- * Error+ , Error (..), UserMessage (..), LogMessage (..), userError, logError++ -- * Status code+ , StatusCode (..)+ -- ** Predicates+ -- $predicates+ , isSuccess, isError, isClientError, isServerError+ -- ** Client error codes+ -- $constants+ , badRequest400, unauthorized401, requestFailed402+ , notFound404, conflict409, tooManyRequests429++ -- * Re-exports from Wreq+ , FormParam (..), Session, Network.Wreq.Session.newAPISession++ ) where++-- aeson+import qualified Data.Aeson++-- base+import qualified Control.Exception+import Control.Monad ((>=>))+import qualified Data.Bifunctor+import qualified Data.Semigroup+import Prelude hiding (userError)++-- bytestring+import qualified Data.ByteString+import qualified Data.ByteString.Lazy++-- lens+import Control.Lens ((&), (.~), (?~), (^.))++-- stripe-concepts+import Stripe.Concepts (ApiSecretKey (..))++-- text+import Data.Text (Text)+import qualified Data.Text++-- unordered-containers+import qualified Data.HashMap.Strict++-- wreq+import Network.Wreq (FormParam (..))+import qualified Network.Wreq+import Network.Wreq.Session (Session)+import qualified Network.Wreq.Session++------------------------------------------------------------++{- | An HTTP status code returned by Stripe.++"Stripe uses conventional HTTP response codes to indicate the success or failure+of an API request." - <https://stripe.com/docs/api#errors Stripe> -}++newtype StatusCode = StatusCode Int deriving Eq++{- $predicates++Some basic functions for interpreting status codes. -}++{- | "Codes in the 2xx range indicate success." -+<https://stripe.com/docs/api#errors Stripe> -}++isSuccess :: StatusCode -> Bool+isSuccess (StatusCode x) = x >= 200 && x < 300++{- | @isError x@ is equivalent to @'isClientError' x || 'isServerError' x@. -}++isError :: StatusCode -> Bool+isError (StatusCode x) = x >= 400 && x < 600++{- | "Codes in the 4xx range indicate an error that failed given the information+provided (e.g., a required parameter was omitted, a charge failed, etc.)." -+<https://stripe.com/docs/api#errors Stripe> -}++isClientError :: StatusCode -> Bool+isClientError (StatusCode x) = x >= 400 && x < 500++{- | "Codes in the 5xx range indicate an error with Stripe's servers." -+<https://stripe.com/docs/api#errors Stripe> -}++isServerError :: StatusCode -> Bool+isServerError (StatusCode x) = x >= 500 && x < 600++{- $constants++Constants for each of the error codes enumerated in the Stripe API+documentation, for your convenience. -}++{- | 400 - Bad Request++"The request was unacceptable, often due to missing a required parameter." -+<https://stripe.com/docs/api#errors Stripe> -}++badRequest400 :: StatusCode+badRequest400 = StatusCode 400++{- | 401 - Unauthorized++"No valid API key provided." - <https://stripe.com/docs/api#errors Stripe> -}++unauthorized401 :: StatusCode+unauthorized401 = StatusCode 401++{- | 402 - Request Failed++"The parameters were valid but the request failed." -+<https://stripe.com/docs/api#errors Stripe> -}++requestFailed402 :: StatusCode+requestFailed402 = StatusCode 402++{- | 404 - Not Found++"The requested resource doesn't exist." -+<https://stripe.com/docs/api#errors Stripe> -}++notFound404 :: StatusCode+notFound404 = StatusCode 404++{- | 409 - Conflict++"The request conflicts with another request (perhaps due to using the same+idempotent key)." - <https://stripe.com/docs/api#errors Stripe> -}++conflict409 :: StatusCode+conflict409 = StatusCode 409++{- | 429 - Too Many Requests++"Too many requests hit the API too quickly. We recommend an exponential backoff+of your requests." - <https://stripe.com/docs/api#errors Stripe> -}++tooManyRequests429 :: StatusCode+tooManyRequests429 = StatusCode 429++------------------------------------------------------------++data Get =+ Get+ { getPath :: [Text] -- ^ URL path components+ , getParams :: [(Text, Text)] -- ^ Query params+ }++data Post =+ Post+ { postPath :: [Text] -- ^ URL path components+ , postParams :: [FormParam] -- ^ Parameters to send in the request body+ }++data Delete =+ Delete+ { deletePath :: [Text] -- ^ URL path components+ , deleteParams :: [(Text, Text)] -- ^ Query params+ }++get :: Session -> ApiSecretKey -> Get -> IO WreqResponse+get session key x = Network.Wreq.Session.getWith opts session url+ where+ url = makeUrl (getPath x)+ opts = wreqDefaults & Network.Wreq.auth ?~ auth key+ & Network.Wreq.params .~ (getParams x)++post :: Session -> ApiSecretKey -> Post -> IO WreqResponse+post session key x = Network.Wreq.Session.postWith opts session url params+ where+ url = makeUrl (postPath x)+ params = postParams x+ opts = wreqDefaults & Network.Wreq.auth ?~ auth key++delete :: Session -> ApiSecretKey -> Delete -> IO WreqResponse+delete session key x = Network.Wreq.Session.deleteWith opts session url+ where+ url = makeUrl (deletePath x)+ opts = wreqDefaults & Network.Wreq.auth ?~ auth key+ & Network.Wreq.params .~ (deleteParams x)++urlBase :: Text+urlBase = Data.Text.pack "https://api.stripe.com/v1"++makeUrl :: [Text] -> String+makeUrl =+ Data.Text.unpack+ . Data.Text.intercalate (Data.Text.pack "/")+ . (urlBase :)++wreqDefaults :: Network.Wreq.Options+wreqDefaults = Network.Wreq.defaults & noCheckResponse++{- | Set a "response checker" that overrides Wreq's default one which causes+exceptions to be thrown for non-2xx HTTP status codes -}++noCheckResponse :: Network.Wreq.Options -> Network.Wreq.Options+noCheckResponse = Network.Wreq.checkResponse ?~ (\_ _ -> return ())++{- | Represent a Stripe API key as a Wreq 'Network.Wreq.Auth' value.++"Authentication to the API is performed via HTTP Basic Auth. Provide your API+key as the basic auth username value. You do not need to provide a password." -+<https://stripe.com/docs/api#authentication Stripe> -}++auth :: ApiSecretKey -> Network.Wreq.Auth+auth (ApiSecretKey key) = Network.Wreq.basicAuth key Data.ByteString.empty++------------------------------------------------------------++{- | An error message suitable for being shown to a user. -}++newtype UserMessage = UserMessage Text deriving (Eq, Show)++{- | An error message that should go into an error log, /not/ shown to a user.+-}++newtype LogMessage = LogMessage Text deriving (Eq, Show)++data Error =+ Error+ { userMessages :: [UserMessage]+ , logMessages :: [LogMessage]+ }+ deriving (Eq, Show)++instance Data.Semigroup.Semigroup Error+ where+ Error x y <> Error x' y' =+ Error+ ((Data.Semigroup.<>) x x')+ ((Data.Semigroup.<>) y y')++instance Monoid Error+ where+ mappend = (Data.Semigroup.<>)+ mempty = Error mempty mempty++instance Control.Exception.Exception Error++userError+ :: Text -- ^ An error message intended to be shown to a user.+ -> Error++userError x = Error { userMessages = [UserMessage x], logMessages = [] }++logError+ :: Text -- ^ An error message intended to go into a log file,+ -- /not/ to be shown to a user.+ -> Error++logError x = Error { userMessages = [], logMessages = [LogMessage x] }++------------------------------------------------------------++type WreqResponse = Network.Wreq.Response Data.ByteString.Lazy.ByteString++data Response =+ Response+ { responseBody :: Either Text Data.Aeson.Value+ -- ^ Every Stripe response should have a JSON body; but if not, this+ -- will be a 'Left' value with an error message from the JSON parser.+ , responseCode :: StatusCode+ -- ^ The status code of the HTTP response.+ }++{- | Convert a 'WreqResponse' into a 'Response' by parsing the JSON response+body (the Stripe API always returns JSON) and getting the HTTP status code. -}++wreqResponse :: WreqResponse -> Response+wreqResponse r =+ Response+ { responseBody =+ r ^. Network.Wreq.responseBody+ & Data.Aeson.eitherDecode+ & Data.Bifunctor.first Data.Text.pack+ , responseCode =+ r ^. Network.Wreq.responseStatus+ . Network.Wreq.statusCode+ & StatusCode+ }++{- | Interpret a response, returning 'Right' with the parsed JSON payload if+everything is okay, or 'Left' with an error if the response contains any+indication that something went wrong. -}++responseValue :: Response -> Either Error Data.Aeson.Value+responseValue r =+ case (responseBody r) of+ Left e -> Left (logError e)+ Right val ->+ case isSuccess (responseCode r) of+ True -> Right val+ False -> Left (responseValueError val)++{- | If the response object looks like this:++> {+> "error": {+> "type": "card_error",+> "message": "..."+> }+> }+> }++then we use the value of the @message@ field as a 'UserMessage'. Otherwise it is+a 'LogMessage'.++"@message@: A human-readable message providing more details about the error. For+card errors, these messages can be shown to your users. [...] Card errors are+the most common type of error you should expect to handle. They result when the+user enters a card that can't be charged for some reason." -+<https://stripe.com/docs/api#errors Stripe> -}++responseValueError :: Data.Aeson.Value -> Error+responseValueError val+ | isCardError = foldMap userError (msg val)+ | otherwise = foldMap logError (msg val)+ where++ isCardError = typ val == Just (Data.Text.pack "card_error")++ msg = aesonAttr "error" >=> aesonAttr "message" >=> aesonText+ typ = aesonAttr "error" >=> aesonAttr "type" >=> aesonText++------------------------------------------------------------++-- Internal Aeson decoding functions++aesonAttr :: String -> Data.Aeson.Value -> Maybe Data.Aeson.Value+aesonAttr x = aesonObject >=> Data.HashMap.Strict.lookup (Data.Text.pack x)++aesonObject :: Data.Aeson.Value -> Maybe Data.Aeson.Object+aesonObject (Data.Aeson.Object x) = Just x+aesonObject _ = Nothing++aesonText :: Data.Aeson.Value -> Maybe Text+aesonText (Data.Aeson.String x) = Just x+aesonText _ = Nothing
+ license.txt view
@@ -0,0 +1,18 @@+Copyright 2018 Typeclass Consulting, LLC++Permission is hereby granted, free of charge, to any person obtaining a copy of+this software and associated documentation files (the "Software"), to deal in+the Software without restriction, including without limitation the rights to+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of+the Software, and to permit persons to whom the Software is furnished to do so,+subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ stripe-wreq.cabal view
@@ -0,0 +1,48 @@+name: stripe-wreq+version: 1.0.0.0++synopsis: Use the Stripe API via Wreq+category: Web++description:+ This package helps you make requests to the+ <https://stripe.com/ Stripe> API in conjunction with+ the <https://hackage.haskell.org/package/wreq wreq> package.+ This is a minimal library that makes no attempt obscure+ either its underpinnings in Wreq or any of the details of the+ Stripe API. Responses are returned as+ <https://hackage.haskell.org/package/aeson aeson> values.++homepage: https://github.com/typeclasses/stripe+bug-reports: https://github.com/typeclasses/stripe/issues++author: Chris Martin+maintainer: Chris Martin, Julie Moronuki++copyright: 2018 Typeclass Consulting, LLC+license: MIT+license-file: license.txt++build-type: Simple+cabal-version: >=1.10+tested-with: GHC==8.2.2, GHC==8.4.3, GHC==8.6.1++extra-source-files:+ changelog.md++library+ hs-source-dirs: library+ default-language: Haskell2010++ exposed-modules:+ Stripe.Wreq++ build-depends:+ aeson+ , base >=4.10 && <5+ , bytestring+ , lens+ , stripe-concepts+ , text+ , unordered-containers+ , wreq