{-# LANGUAGE DeriveDataTypeable, OverloadedStrings, ConstraintKinds #-} {-# LANGUAGE ScopedTypeVariables, GeneralizedNewtypeDeriving, FlexibleInstances, MultiParamTypeClasses, UndecidableInstances, TypeFamilies, FlexibleContexts, RankNTypes,CPP,TemplateHaskell, StandaloneDeriving #-} -- | the utility monad and related functions, taking care of the HTTP, JSON, etc. module Web.MangoPay.Monad where import Web.MangoPay.Types import Control.Applicative import Control.Monad (MonadPlus, liftM, void, join) import Control.Monad.Base (MonadBase(..)) import Control.Monad.Fix (MonadFix) import Control.Monad.IO.Class (MonadIO) import Control.Monad.Trans.Class (MonadTrans(lift)) import Control.Monad.Trans.Control ( MonadTransControl(..), MonadBaseControl(..) , ComposeSt, defaultLiftBaseWith, defaultLiftWith , defaultRestoreM, defaultRestoreT ) import Control.Monad.Trans.Reader (ReaderT(..), ask, mapReaderT) import Data.Monoid ((<>)) import Data.Typeable (Typeable) import Data.Default import qualified Control.Monad.Trans.Resource as R import qualified Control.Exception.Lifted as L import qualified Data.Conduit as C import qualified Network.HTTP.Conduit as H import qualified Network.HTTP.Types as HT import Data.ByteString (ByteString) import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as BSC import Data.Aeson (json, fromJSON, toJSON, Result(..) ,FromJSON, ToJSON,encode, Object, Value(..)) import Data.Conduit.Attoparsec (sinkParser, ParseError) import qualified Data.Text.Encoding as TE import qualified Data.Text as T (Text) import Data.Maybe (fromMaybe) import Data.CaseInsensitive (CI) import Control.Monad.Logger #if DEBUG import Data.Conduit.Binary (sinkHandle) import System.IO (stdout) import Data.Conduit.Internal (zipSinks) import Control.Monad.IO.Class (liftIO) #endif -- | put our constraints in one synonym type MPUsableMonad m=(MonadBaseControl IO m, R.MonadResource m, MonadLogger m) -- | the mangopay monad transformer -- this encapsulates the data necessary to pass the app credentials, etc newtype MangoPayT m a = Mp { unIs :: ReaderT MpData m a } deriving ( Functor, Applicative, Alternative, Monad , MonadFix, MonadPlus, MonadIO, MonadTrans , R.MonadThrow ) deriving instance R.MonadResource m => R.MonadResource (MangoPayT m) instance MonadBase b m => MonadBase b (MangoPayT m) where liftBase = lift . liftBase instance MonadTransControl MangoPayT where type StT MangoPayT a = StT (ReaderT MpData) a liftWith = defaultLiftWith Mp unIs restoreT = defaultRestoreT Mp instance MonadBaseControl b m => MonadBaseControl b (MangoPayT m) where type StM (MangoPayT m) a = ComposeSt MangoPayT m a liftBaseWith = defaultLiftBaseWith restoreM = defaultRestoreM instance (MonadLogger m) => MonadLogger (MangoPayT m) where monadLoggerLog loc src lvl msg=lift $ monadLoggerLog loc src lvl msg -- | Run a computation in the 'MangoPayT' monad transformer with -- your credentials. runMangoPayT :: Credentials -- ^ Your app's credentials. -> H.Manager -- ^ Connection manager (see 'H.withManager'). -> AccessPoint -> MangoPayT m a -- ^ the action to run -> m a -- ^ the result runMangoPayT creds manager ap (Mp act) = runReaderT act (MpData creds manager ap) -- | Get the user's credentials. getCreds :: Monad m => MangoPayT m Credentials getCreds = mpCreds `liftM` Mp ask -- | Get the MangoPay host getHost :: Monad m => MangoPayT m ByteString getHost = (getAccessPointURL . mpAccessPoint) `liftM` Mp ask -- | Build a POST request to MangoPay. getPostRequest :: (Monad m, MonadIO m, HT.QueryLike q) => ByteString -- ^ The URL path. -> Maybe AccessToken -> q -- ^ The query parameters. -> MangoPayT m H.Request -- ^ The properly configured request. getPostRequest path mat query = do let b = HT.renderQuery False $ HT.toQuery query getBasicRequest HT.methodPost path $ \r -> r { H.requestHeaders = ("Content-Type", "application/x-www-form-urlencoded") : [("Authorization", at) | Just (AccessToken at) <- [mat]] , H.requestBody = H.RequestBodyBS b } -- | Build a GET request to MangoPay. getGetRequest :: (Monad m, MonadIO m, HT.QueryLike q) => ByteString -- ^ The URL path. -> Maybe AccessToken -> q -- ^ The query parameters. -> MangoPayT m H.Request -- ^ The properly configured request. getGetRequest path mat query=do let qs = HT.renderQuery True $ HT.toQuery query getBasicRequest HT.methodGet path $ \r -> r { H.queryString = qs , H.requestHeaders = getJSONHeaders mat } -- | (Internal) Build a basic request. getBasicRequest :: MonadIO m => HT.Method -> ByteString -> (H.Request -> H.Request) -> MangoPayT m H.Request getBasicRequest method path addRest = do host <- getHost let req1 = def { H.secure = True , H.host = host , H.port = 443 , H.path = path , H.method = method } req2 = addRest req1 #if DEBUG liftIO $ do print req2 putStrLn $ "^--> " ++ case H.requestBody req2 of H.RequestBodyLBS lbs -> "RequestBodyLBS " ++ show lbs H.RequestBodyBS bs -> "RequestBodyBS " ++ show bs H.RequestBodyBuilder s _ -> "RequestBodyBuilder " ++ show s ++ " " H.RequestBodyStream s _ -> "RequestBodyStream " ++ show s ++ " " H.RequestBodyStreamChunked _ -> "RequestBodyStreamChunked " #endif return req2 -- | build a delete request to MangoPay getDeleteRequest :: (Monad m,MonadIO m,HT.QueryLike q) => ByteString -- ^ the url path -> Maybe AccessToken -> q -- ^ the query parameters -> MangoPayT m H.Request -- ^ the properly configured request getDeleteRequest path mat query=do get<-getGetRequest path mat query return $ get {H.method=HT.methodDelete} -- | get the url to use for our clientId getClientURL :: (Monad m)=> ByteString -- ^ the url path -> MangoPayT m ByteString -- ^ the URL getClientURL path=do cid<- liftM clientIdBS getCreds return $ BS.concat ["/v2/",cid,path] -- | get the url to use for our clientId getClientURLMultiple :: (Monad m)=> [T.Text] -- ^ the url components -> MangoPayT m ByteString -- ^ the URL getClientURLMultiple path=do cid<- liftM clientIdBS getCreds return $ BS.concat $ ["/v2/",cid] ++ map TE.encodeUtf8 path -- | build a URL for a get operation with a single query getQueryURL :: (Monad m,HT.QueryLike q) => ByteString -- ^ the url path -> q -- ^ the query parameters -> MangoPayT m ByteString -- ^ the URL getQueryURL path query=do host<-getHost return $ BS.concat ["https://",host,path,HT.renderQuery True $ HT.toQuery query] -- | perform a HTTP request and deal with the JSON result -- The logic for errors is as follows: we have several cases: -- If the HTTP request return OK and we can parse the proper result, we return it. -- If we can't parse it into the data type we expect, we throw a MpJSONException: the server returned ok but we can't parse the result. -- If we get an HTTP error code, we try to parse the result and send the proper exception: we have encountered probably a normal error, when the user has filled in incorrect data, etc. -- If we can't even parse the result as JSON or if we can't understand the JSON error message, we throw an MpHttpException. mpReq :: forall b (m :: * -> *) wrappedErr c . (MPUsableMonad m,FromJSON b,FromJSON wrappedErr) => H.Request -> (wrappedErr -> MpError) -- ^ extract the error from the JSON -> (HT.ResponseHeaders -> b -> c) -> MangoPayT m c mpReq req extractError addHeaders=do -- we check the status ourselves let req' = req { H.checkStatus = \_ _ _ -> Nothing } mgr<-getManager res<-H.http req' mgr let status = H.responseStatus res headers = H.responseHeaders res cookies = H.responseCookieJar res ok=isOkay status err=H.StatusCodeException status headers cookies mpres<-L.catch (do #if DEBUG liftIO $ BSC.putStrLn "" liftIO $ print $ show req' (value,_)<-H.responseBody res C.$$+- zipSinks (sinkParser json) (sinkHandle stdout) liftIO $ BSC.putStrLn "" liftIO $ print headers #else value<-H.responseBody res C.$$+- sinkParser json #endif if ok then -- parse response as the expected value case fromJSON value of Success ot->return $ Right (value,addHeaders headers ot) Error jerr->return $ Left $ MpJSONException jerr -- got an ok response we couldn't parse else -- parse response as an error case fromJSON value of Success ise-> return $ Left $ MpAppException $ extractError ise _ -> return $ Left $ MpHttpException err $ Just value -- we can't even parse the error, throw the HTTP error inside our error type, but keep the JSON in case a human can make sens of it ) (\(_::ParseError)->return $ Left $ MpHttpException err Nothing) -- the error body wasn't even json, throw the HTTP error inside our error type let cr=CallRecord req' mpres -- log call $(logCall) cr recordResult cr -- | get a JSON response from a request to MangoPay -- MangoPay returns either a result, or an error getJSONResponse :: forall (m :: * -> *) v. (MPUsableMonad m,FromJSON v) => H.Request -> MangoPayT m v getJSONResponse req=mpReq req id (const id) -- | get a PagedList from the JSON items getJSONList:: forall (m :: * -> *) v. (MPUsableMonad m,FromJSON v) => H.Request -> MangoPayT m (PagedList v) getJSONList req=mpReq req id buildList -- | build a PagedList from the headers information buildList :: HT.ResponseHeaders -> [b] -> PagedList b buildList headers items=let cnt=fromMaybe (fromIntegral $ length items) $ getI "X-Number-Of-Items" -- yes, doc is wrong (they forgot the s) pgs=fromMaybe 1 $ getI "X-Number-Of-Pages" in PagedList items cnt pgs where getI :: CI ByteString -> Maybe Integer getI =join . fmap ((maybeRead :: String -> Maybe Integer). BSC.unpack) . findAssoc headers -- | get all items, hiding the pagination system getAll :: (MPUsableMonad m,FromJSON v) => (Maybe Pagination -> AccessToken -> MangoPayT m (PagedList v)) -> AccessToken -> MangoPayT m [v] getAll f at=readAll 1 [] where readAll p accum=do retL<-f (Just $ Pagination p 100) at let dts=accum ++ plData retL if plPageCount retL > p then readAll (p + 1) dts else return dts -- | get the headers necessary for a JSON call getJSONHeaders :: Maybe AccessToken -> HT.RequestHeaders getJSONHeaders mat= ("content-type", "application/json") : case mat of Just (AccessToken at) -> [("Authorization", at)] _ -> [] -- | send JSON via post, get JSON back postExchange :: forall (m :: * -> *) v p. (MPUsableMonad m,FromJSON v,ToJSON p) => ByteString -> Maybe AccessToken -> p -> MangoPayT m v postExchange=jsonExchange HT.methodPost -- | send JSON via post, get JSON back putExchange :: forall (m :: * -> *) v p. (MPUsableMonad m,FromJSON v,ToJSON p) => ByteString -> Maybe AccessToken -> p -> MangoPayT m v putExchange=jsonExchange HT.methodPut -- | send JSON, get JSON back jsonExchange :: forall (m :: * -> *) v p. (MPUsableMonad m,FromJSON v,ToJSON p) => HT.Method -> ByteString -> Maybe AccessToken -> p -> MangoPayT m v jsonExchange meth path mat p= getJSONRequest meth path mat p >>= getJSONResponse -- | Get JSON request. getJSONRequest :: (MPUsableMonad m, ToJSON p) => HT.Method -> ByteString -> Maybe AccessToken -> p -> MangoPayT m H.Request getJSONRequest method path mat p = do getBasicRequest method path $ \r -> r { H.requestHeaders = getJSONHeaders mat , H.requestBody = H.RequestBodyLBS $ encode p } -- | post JSON content and ignore the reply postNoReply :: forall (m :: * -> *) p. (MPUsableMonad m,ToJSON p) => ByteString -> Maybe AccessToken -> p -> MangoPayT m () postNoReply path mat p= do req<- getJSONRequest HT.methodPost path mat p mgr<-getManager void $ H.http req mgr -- | Get the 'H.Manager'. getManager :: Monad m => MangoPayT m H.Manager getManager = mpManager `liftM` Mp ask -- | Run a 'ResourceT' inside a 'MangoPayT'. runResourceInMp :: (MPUsableMonad m) => MangoPayT (R.ResourceT m) a -> MangoPayT m a runResourceInMp (Mp inner) = Mp $ ask >>= lift . R.runResourceT . runReaderT inner -- | Transform the computation inside a 'MangoPayT'. mapMangoPayT :: (m a -> n b) -> MangoPayT m a -> MangoPayT n b mapMangoPayT f = Mp . mapReaderT f . unIs -- | the data kept through the computations data MpData = MpData { mpCreds::Credentials -- ^ app credentials ,mpManager::H.Manager -- ^ HTTP connection manager ,mpAccessPoint:: AccessPoint -- ^ access point } deriving (Typeable) -- | @True@ if the the 'Status' is ok (i.e. @2XX@). isOkay :: HT.Status -> Bool isOkay status = let sc = HT.statusCode status in 200 <= sc && sc < 300 -- | helper function to create an entity using POST createGeneric :: (MPUsableMonad m, FromJSON a, ToJSON a) => ByteString -> a -> AccessToken -> MangoPayT m a createGeneric path x at = do url<-getClientURL path postExchange url (Just at) x -- | helper function to create an entity using PUT modifyGeneric :: (MPUsableMonad m, FromJSON a, ToJSON a) => T.Text -> a -> (a -> Maybe T.Text) -> AccessToken -> MangoPayT m a modifyGeneric = modifyGGeneric Nothing -- | even more generic version of 'modifyGeneric' that can put an -- arbitrary json value modifyGGeneric :: (MPUsableMonad m, FromJSON a, ToJSON a) => Maybe (Object -> Object) -> T.Text -> a -> (a -> Maybe T.Text) -> AccessToken -> MangoPayT m a modifyGGeneric mf path x fid at = case fid x of Nothing -> error $ show $ "Web.MangoPay.Users.modifyGGeneric : Nothing (" <> path <> ")" Just i -> do url<-getClientURLMultiple [path ,i] case mf of Nothing -> putExchange url (Just at) x Just f -> do let Object o = toJSON x putExchange url (Just at) $ f o -- | helper function to fetch an entity from its id fetchGeneric :: (MPUsableMonad m, FromJSON a) => T.Text -> T.Text -> AccessToken -> MangoPayT m a fetchGeneric path xid at = do url<-getClientURLMultiple [path ,xid] req<-getGetRequest url (Just at) ([]::HT.Query) getJSONResponse req -- | helper function to fetch paginated lists of an entity genericList :: (MPUsableMonad m, FromJSON a) => [T.Text] -> Maybe Pagination -> AccessToken -> MangoPayT m (PagedList a) genericList = genericListExtra [] -- | helper function to fetch paginated lists of an entity, with extra parameters genericListExtra :: (MPUsableMonad m, FromJSON a) => [(ByteString,Maybe ByteString)] -> [T.Text] -> Maybe Pagination -> AccessToken -> MangoPayT m (PagedList a) genericListExtra extra path mp at = do url <- getClientURLMultiple path req <- getGetRequest url (Just at) (extra ++ (paginationAttributes mp)) getJSONList req