{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE UndecidableInstances #-}
module StripeAPI.Common
( Configuration (..),
doCallWithConfiguration,
doCallWithConfigurationM,
doBodyCallWithConfiguration,
doBodyCallWithConfigurationM,
runWithConfiguration,
MonadHTTP (..),
stringifyModel,
StringifyModel,
SecurityScheme (..),
AnonymousSecurityScheme (..),
textToByte,
JsonByteString (..),
JsonDateTime (..),
RequestBodyEncoding (..),
)
where
import qualified Control.Exception as Exception
import qualified Control.Monad.Reader as MR
import qualified Control.Monad.Trans.Class as MT
import qualified Data.Aeson as Aeson
import qualified Data.Bifunctor as BF
import qualified Data.ByteString.Char8 as B8
import qualified Data.HashMap.Strict as HMap
import qualified Data.Maybe as Maybe
import qualified Data.Scientific as Scientific
import qualified Data.Text as T
import Data.Text (Text)
import qualified Data.Time.LocalTime as Time
import qualified Data.Vector as Vector
import GHC.Generics
import qualified Network.HTTP.Client as HC
import qualified Network.HTTP.Simple as HS
class Monad m => MonadHTTP m where
httpBS :: HS.Request -> m (Either HS.HttpException (HS.Response B8.ByteString))
instance MonadHTTP IO where
httpBS request =
BF.first (\e -> e :: HS.HttpException)
<$> Exception.try (HS.httpBS request)
instance MonadHTTP m => MonadHTTP (MR.ReaderT r m) where
httpBS = MT.lift . httpBS
data Configuration s
= Configuration
{ configBaseURL :: Text,
configSecurityScheme :: s
}
deriving (Show, Ord, Eq, Generic)
data RequestBodyEncoding
=
RequestBodyEncodingJSON
|
RequestBodyEncodingFormData
class SecurityScheme s where
authenticateRequest :: s -> HS.Request -> HS.Request
data AnonymousSecurityScheme = AnonymousSecurityScheme
instance SecurityScheme AnonymousSecurityScheme where
authenticateRequest = const id
runWithConfiguration :: SecurityScheme s => Configuration s -> MR.ReaderT (Configuration s) m a -> m a
runWithConfiguration = flip MR.runReaderT
doCallWithConfiguration ::
(MonadHTTP m, SecurityScheme s) =>
Configuration s ->
Text ->
Text ->
[(Text, Maybe String)] ->
m (Either HS.HttpException (HS.Response B8.ByteString))
doCallWithConfiguration config method path queryParams =
httpBS $ createBaseRequest config method path queryParams
doCallWithConfigurationM ::
(MonadHTTP m, SecurityScheme s) =>
Text ->
Text ->
[(Text, Maybe String)] ->
MR.ReaderT (Configuration s) m (Either HS.HttpException (HS.Response B8.ByteString))
doCallWithConfigurationM method path queryParams = do
config <- MR.ask
MT.lift $ doCallWithConfiguration config method path queryParams
doBodyCallWithConfiguration ::
(MonadHTTP m, SecurityScheme s, Aeson.ToJSON body) =>
Configuration s ->
Text ->
Text ->
[(Text, Maybe String)] ->
body ->
RequestBodyEncoding ->
m (Either HS.HttpException (HS.Response B8.ByteString))
doBodyCallWithConfiguration config method path queryParams body RequestBodyEncodingJSON =
httpBS $ HS.setRequestMethod (textToByte method) $ HS.setRequestBodyJSON body baseRequest
where
baseRequest = createBaseRequest config method path queryParams
doBodyCallWithConfiguration config method path queryParams body RequestBodyEncodingFormData =
httpBS $ HS.setRequestMethod (textToByte method) $ HS.setRequestBodyURLEncoded byteStringData baseRequest
where
baseRequest = createBaseRequest config method path queryParams
byteStringData = createFormData body
doBodyCallWithConfigurationM ::
(MonadHTTP m, SecurityScheme s, Aeson.ToJSON body) =>
Text ->
Text ->
[(Text, Maybe String)] ->
body ->
RequestBodyEncoding ->
MR.ReaderT (Configuration s) m (Either HS.HttpException (HS.Response B8.ByteString))
doBodyCallWithConfigurationM method path queryParams body encoding = do
config <- MR.ask
MT.lift $ doBodyCallWithConfiguration config method path queryParams body encoding
createBaseRequest ::
SecurityScheme s =>
Configuration s ->
Text ->
Text ->
[(Text, Maybe String)] ->
HS.Request
createBaseRequest config method path queryParams =
authenticateRequest (configSecurityScheme config)
$ HS.setRequestMethod (textToByte method)
$ HS.setRequestQueryString query
$ HS.setRequestPath
(B8.pack (T.unpack $ byteToText basePathModifier <> path))
baseRequest
where
baseRequest = parseURL $ configBaseURL config
basePath = HC.path baseRequest
basePathModifier =
if basePath == B8.pack "/" && T.isPrefixOf "/" path
then ""
else basePath
query = [(textToByte a, Just $ B8.pack b) | (a, Just b) <- queryParams]
createFormData :: (Aeson.ToJSON a) => a -> [(B8.ByteString, B8.ByteString)]
createFormData body =
let formData = jsonToFormData $ Aeson.toJSON body
in fmap (BF.bimap textToByte textToByte) formData
byteToText :: B8.ByteString -> Text
byteToText = T.pack . B8.unpack
textToByte :: Text -> B8.ByteString
textToByte = B8.pack . T.unpack
parseURL :: Text -> HS.Request
parseURL url =
Maybe.fromMaybe HS.defaultRequest
$ HS.parseRequest
$ T.unpack url
jsonToFormData :: Aeson.Value -> [(Text, Text)]
jsonToFormData = jsonToFormDataPrefixed ""
jsonToFormDataPrefixed :: Text -> Aeson.Value -> [(Text, Text)]
jsonToFormDataPrefixed prefix (Aeson.Number a) = case Scientific.toBoundedInteger a :: Maybe Int of
Just myInt -> [(prefix, T.pack $ show myInt)]
Nothing -> [(prefix, T.pack $ show a)]
jsonToFormDataPrefixed prefix (Aeson.Bool True) = [(prefix, T.pack "true")]
jsonToFormDataPrefixed prefix (Aeson.Bool False) = [(prefix, T.pack "false")]
jsonToFormDataPrefixed _ Aeson.Null = []
jsonToFormDataPrefixed prefix (Aeson.String a) = [(prefix, a)]
jsonToFormDataPrefixed "" (Aeson.Object object) =
HMap.toList object >>= uncurry jsonToFormDataPrefixed
jsonToFormDataPrefixed prefix (Aeson.Object object) =
HMap.toList object >>= (\(x, y) -> jsonToFormDataPrefixed (prefix <> "[" <> x <> "]") y)
jsonToFormDataPrefixed prefix (Aeson.Array vector) =
Vector.toList vector >>= jsonToFormDataPrefixed (prefix <> "[]")
class Show a => StringifyModel a where
stringifyModel :: a -> String
instance StringifyModel String where
stringifyModel = id
instance {-# OVERLAPS #-} Show a => StringifyModel a where
stringifyModel = show
newtype JsonByteString = JsonByteString B8.ByteString
deriving (Show, Eq, Ord)
instance Aeson.ToJSON JsonByteString where
toJSON (JsonByteString s) = Aeson.toJSON $ B8.unpack s
instance Aeson.FromJSON JsonByteString where
parseJSON (Aeson.String s) = pure $ JsonByteString $ textToByte s
parseJSON _ = fail "Value cannot be converted to a 'JsonByteString'"
newtype JsonDateTime = JsonDateTime Time.ZonedTime
deriving (Show)
instance Eq JsonDateTime where
(JsonDateTime d1) == (JsonDateTime d2) = Time.zonedTimeToUTC d1 == Time.zonedTimeToUTC d2
instance Ord JsonDateTime where
(JsonDateTime d1) <= (JsonDateTime d2) = Time.zonedTimeToUTC d1 <= Time.zonedTimeToUTC d2
instance Aeson.ToJSON JsonDateTime where
toJSON (JsonDateTime d) = Aeson.toJSON d
instance Aeson.FromJSON JsonDateTime where
parseJSON o = JsonDateTime <$> Aeson.parseJSON o