{-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_HADDOCK hide #-} -- | Jenkins REST API interface internals module Jenkins.Rest.Internal where import Control.Applicative #if ! MIN_VERSION_free(5,0,0) import Control.Applicative.Backwards (Backwards(..)) #endif import Control.Concurrent.Async.Lifted (concurrently) import Control.Exception (Exception(..)) import Control.Exception.Lifted (bracket, catch, throwIO) import Control.Monad import Control.Monad.Free.Church (liftF) import Control.Monad.Error (MonadError(..)) import Control.Monad.IO.Class (MonadIO(..)) import Control.Monad.Reader (MonadReader(..)) import Control.Monad.State (MonadState(..)) import Control.Monad.Trans.Free.Church (FT, iterTM) import Control.Monad.Trans.Class (MonadTrans(..)) import Control.Monad.Trans.Control (MonadTransControl(..), MonadBaseControl(..)) import Control.Monad.Trans.Reader (ReaderT) import qualified Control.Monad.Trans.Reader as Reader import Control.Monad.Trans.Maybe (MaybeT(..), mapMaybeT) import Control.Monad.Writer (MonadWriter(..)) import Data.ByteString.Lazy (ByteString) import Data.Text (Text) import qualified Data.Text.Encoding as Text import Data.Typeable (Typeable) import Network.HTTP.Client (Request, HttpException) import qualified Network.HTTP.Client as Http import qualified Network.HTTP.Client.TLS as Http import Network.HTTP.Types (Status(..)) import Jenkins.Rest.Method.Internal (Method, Type(..), render, slash) {-# ANN module ("HLint: ignore Use join" :: String) #-} -- | The value of this type describes Jenkins REST API requests sequence newtype JenkinsT m a = JenkinsT { unJenkinsT :: FT (JF m) m a } deriving (Functor) instance MonadIO m => MonadIO (JenkinsT m) where liftIO = JenkinsT . liftIO instance MonadTrans JenkinsT where lift = JenkinsT . lift instance Applicative (JenkinsT m) where pure = JenkinsT . pure #if MIN_VERSION_free(5,0,0) JenkinsT f <*> JenkinsT x = JenkinsT (f <*> x) #else -- https://github.com/ekmett/free/pull/80 JenkinsT f <*> JenkinsT x = JenkinsT (forwards (Backwards f <*> Backwards x)) #endif instance Monad (JenkinsT m) where return = JenkinsT . return JenkinsT m >>= k = JenkinsT (m >>= unJenkinsT . k) instance MonadReader r m => MonadReader r (JenkinsT m) where ask = JenkinsT ask local f = JenkinsT . local f . unJenkinsT instance MonadWriter w m => MonadWriter w (JenkinsT m) where tell = JenkinsT . tell listen = JenkinsT . listen . unJenkinsT pass = JenkinsT . pass . unJenkinsT writer = JenkinsT . writer instance MonadState s m => MonadState s (JenkinsT m) where get = JenkinsT get put = JenkinsT . put state = JenkinsT . state instance MonadError e m => MonadError e (JenkinsT m) where throwError = JenkinsT . throwError m `catchError` f = JenkinsT (unJenkinsT m `catchError` (unJenkinsT . f)) data JF m a where Get :: Method Complete f -> (ByteString -> a) -> JF n a Post :: (forall f. Method Complete f) -> ByteString -> a -> JF m a Conc :: JenkinsT m a -> JenkinsT m b -> (a -> b -> c) -> JF m c Or :: JenkinsT m a -> (JenkinsException -> JenkinsT m a) -> JF m a With :: (Request -> Request) -> JenkinsT m b -> (b -> a) -> JF m a Dcon :: JF m a instance Functor (JF m) where fmap f (Get m g) = Get m (f . g) fmap f (Post m body a) = Post m body (f a) fmap f (Conc m n g) = Conc m n (\a b -> f (g a b)) fmap f (Or a b) = Or (fmap f a) (fmap f . b) fmap f (With h j g) = With h j (f . g) fmap _ Dcon = Dcon -- | Lift 'JF' to 'JenkinsT' liftJ :: JF m a -> JenkinsT m a liftJ = JenkinsT . liftF -- | The kind of exceptions that can be thrown by performing requests -- to the Jenkins REST API newtype JenkinsException = JenkinsHttpException HttpException deriving (Show, Typeable) instance Exception JenkinsException runInternal :: (MonadIO m, MonadBaseControl IO m) => String -> Text -> Text -> JenkinsT m a -> m (Maybe a) runInternal h user token jenk = do url <- wrapException (liftIO (Http.parseUrl h)) bracket (newManager Http.tlsManagerSettings) closeManager $ \m -> Reader.runReaderT (runMaybeT (runInterpT (iterInterpT m jenk))) . Http.applyBasicAuth (Text.encodeUtf8 user) (Text.encodeUtf8 token) $ url newManager :: MonadIO m => Http.ManagerSettings -> m Http.Manager newManager = liftIO . Http.newManager closeManager :: MonadIO m => Http.Manager -> m () closeManager = liftIO . Http.closeManager newtype InterpT m a = InterpT { runInterpT :: MaybeT (ReaderT Request m) a } deriving (Functor) instance (Functor m, Monad m) => Applicative (InterpT m) where pure = return (<*>) = ap instance Monad m => Monad (InterpT m) where return = InterpT . return InterpT m >>= k = InterpT (m >>= runInterpT . k) instance (Functor m, Monad m) => Alternative (InterpT m) where empty = mzero (<|>) = mplus instance Monad m => MonadPlus (InterpT m) where mzero = InterpT mzero InterpT x `mplus` InterpT y = InterpT (x `mplus` y) instance MonadTrans InterpT where lift = InterpT . lift . lift -- | Interpret the 'JF' AST in 'InterpT' iterInterpT :: (MonadIO m, MonadBaseControl IO m) => Http.Manager -> JenkinsT m a -> InterpT m a iterInterpT manager = iter (interpreter manager) -- | Tear down the 'JF' AST with a 'JF'-algebra iter :: (Monad m, Monad (t m), MonadTrans t) => (JF m (t m a) -> t m a) -> JenkinsT m a -> t m a iter go = iterTM go . unJenkinsT -- | 'JF' AST interpreter interpreter :: forall m a. (MonadIO m, MonadBaseControl IO m) => Http.Manager -> JF m (InterpT m a) -> InterpT m a interpreter man = go where go :: JF m (InterpT m a) -> InterpT m a go (Get m next) = InterpT $ do req <- lift Reader.ask res <- liftIO $ wrapException (liftM Http.responseBody (Http.httpLbs (prepareGet m req) man)) runInterpT (next res) go (Post m body next) = InterpT $ do req <- lift Reader.ask _ <- liftIO $ wrapException (Http.httpNoBody (preparePost m body req) man) runInterpT next go (Conc ja jb next) = do (a, b) <- intoM man $ \run -> concurrently (run ja) (run jb) c <- outoM (return a) d <- outoM (return b) next c d go (Or ja jb) = do res <- intoM man $ \run -> run ja `catch` (run . jb) next <- outoM (return res) next go (With f jenk next) = InterpT $ do res <- mapMaybeT (Reader.local f) (runInterpT (iterInterpT man jenk)) runInterpT (next res) go Dcon = mzero intoM :: forall m a. (MonadIO m, MonadBaseControl IO m) => Http.Manager -> ((forall b. JenkinsT m b -> m (StT (ReaderT Request) (StT MaybeT b))) -> m a) -> InterpT m a intoM m f = InterpT $ liftWith $ \run' -> liftWith $ \run''' -> let run :: JenkinsT m t -> m (StT (ReaderT Request) (StT MaybeT t)) run = run''' . run' . runInterpT . iterInterpT m in f run outoM :: Monad m => m (StT (ReaderT Request) (StT MaybeT b)) -> InterpT m b outoM = InterpT . restoreT . restoreT prepareGet :: Method Complete f -> Request -> Request prepareGet m r = r { Http.method = "GET" , Http.path = Http.path r `slash` render m } preparePost :: Method Complete f -> ByteString -> Request -> Request preparePost m body r = r { Http.checkStatus = statusCheck , Http.redirectCount = 0 , Http.requestBody = Http.RequestBodyLBS body , Http.method = "POST" , Http.path = Http.path r `slash` render m } where statusCheck s@(Status st _) hs cookie_jar = if 200 <= st && st < 400 then Nothing else Just . toException $ Http.StatusCodeException s hs cookie_jar wrapException :: MonadBaseControl IO m => m a -> m a wrapException m = m `catch` (throwIO . JenkinsHttpException)