{- | Module : Control.Monad.Script.Http Description : A generic monad for expressing HTTP interactions. Copyright : 2018, Automattic, Inc. License : BSD3 Maintainer : Nathan Bloomfield (nbloomf@gmail.com) Stability : experimental Portability : POSIX A basic type and monad transformer transformer for describing HTTP interactions. -} {-# LANGUAGE CPP, GADTs, Rank2Types, RecordWildCards, OverloadedStrings, QuantifiedConstraints #-} module Control.Monad.Script.Http ( -- * HttpT HttpT() -- * HttpT , HttpTT() , execHttpTT , liftHttpTT -- * Error , throwError , throwJsonError , throwHttpException , throwIOException , catchError , catchJsonError , catchHttpException , catchIOException , catchAnyError , printError , E(..) -- * Reader , ask , local , reader , R(..) , basicEnv , trivialEnv , LogOptions(..) , basicLogOptions , trivialLogOptions -- * Writer , logEntries , LogSeverity(..) , setLogSeverity , W() , printHttpLogs , basicLogEntryPrinter -- * State , gets , modify , S(..) , basicState -- * Prompt , prompt , P(..) , evalIO , evalMockIO -- * API , comment , wait , logDebug , logInfo , logNotice , logWarning , logError , logCritical , logAlert , logEmergency -- ** IO , Control.Monad.Script.Http.hPutStrLn , hPutStrLnBlocking -- ** HTTP calls , httpGet , httpSilentGet , httpPost , httpSilentPost , httpDelete , httpSilentDelete -- ** JSON , parseJson , lookupKeyJson , constructFromJson -- * Types , Url , JsonError(..) , HttpResponse(..) -- * Testing , checkHttpTT ) where #if MIN_VERSION_base(4,9,0) import Prelude hiding (fail, lookup) #else import Prelude hiding (lookup) #endif import Control.Applicative ( Applicative(..), (<$>) ) import Control.Concurrent ( threadDelay ) import Control.Concurrent.MVar ( MVar, withMVar ) import Control.Exception ( IOException, Exception, try ) import Control.Monad ( Functor(..), Monad((>>=),return), ap ) import Control.Monad.Trans.Class ( MonadTrans(..) ) import Control.Monad.Trans.Identity ( IdentityT(..) ) import Control.Lens ( preview, (^.) ) import Data.Aeson ( Value(Object), Result(Success,Error), FromJSON, fromJSON, decode ) import Data.Aeson.Encode.Pretty ( encodePretty ) import Data.Aeson.Lens ( _Value ) import Data.ByteString.Lazy ( ByteString, fromStrict, readFile, writeFile, toStrict ) import Data.ByteString.Lazy.Char8 ( unpack, pack ) import Data.Functor.Identity ( Identity() ) import Data.IORef ( IORef, newIORef, readIORef, writeIORef ) import Data.List ( intercalate ) import Data.String ( fromString ) import Data.Text ( Text ) import qualified Data.Text as T import qualified Data.Text.IO as T import qualified Data.Text.Encoding as T import Data.Time ( UTCTime ) import Data.Time.Clock.System ( getSystemTime, systemToUTCTime ) import Data.Typeable ( Typeable ) import Data.Monoid ( Monoid(..) ) import Data.Semigroup ( Semigroup(..) ) import Network.HTTP.Client ( HttpException(..), CookieJar, HttpExceptionContent(StatusCodeException) , Response, responseCookieJar, responseBody , responseHeaders, responseVersion, responseStatus ) import Network.HTTP.Types ( HttpVersion, Status, ResponseHeaders ) import qualified Network.Wreq as Wreq ( Options, getWith, postWith, deleteWith, defaults, responseStatus, headers ) import qualified Network.Wreq.Session as S ( Session, newSession, getWith, postWith, deleteWith ) import System.IO ( Handle, hPutStrLn, hGetEcho, hSetEcho, hFlush , hFlush, hGetLine, hPutStr, hPutChar, stdout ) import System.IO.Error ( ioeGetFileName, ioeGetLocation, ioeGetErrorString ) import Test.QuickCheck ( Property, Arbitrary(..), Gen, Testable ) import qualified Control.Monad.Script as S import Network.HTTP.Client.Extras import Data.Aeson.Extras -- aeson 2.0.0.0 introduced KeyMap over HashMap #if MIN_VERSION_aeson(2,0,0) import Data.Aeson.Key (fromText) import Data.Aeson.KeyMap (lookup) #else import Data.HashMap.Strict (lookup) #endif -- Transitional MonadFail implementation #if MIN_VERSION_base(4,9,0) import Control.Monad.Fail #endif import Data.LogSeverity import Data.MockIO import Data.MockIO.FileSystem -- | An HTTP session returning an @a@, writing to a log of type @W e w@, reading from an environment of type @R e w r@, with state of type @S s@, throwing errors of type @E e@, performing effectful computations described by @P p a@, and with inner monad @t eff@. newtype HttpTT e r w s p t eff a = HttpTT { httpTT :: S.ScriptTT (E e) (R e w r) (W e w) (S s) (P p) t eff a } deriving Typeable -- | An HTTP session returning an @a@, writing to a log of type @W e w@, reading from an environment of type @R e w r@, with state of type @S s@, throwing errors of type @E e@, performing effectful computations described by @P p a@, with inner monad @eff@. `HttpTT` over `IdentityT`. type HttpT e r w s p = HttpTT e r w s p IdentityT instance (Monad eff, Monad (t eff), MonadTrans t) => Functor (HttpTT e r w s p t eff) where fmap f = HttpTT . fmap f . httpTT instance (Monad eff, Monad (t eff), MonadTrans t) => Applicative (HttpTT e r w s p t eff) where pure = return (<*>) = ap instance (Monad eff, Monad (t eff), MonadTrans t) => Monad (HttpTT e r w s p t eff) where return = HttpTT . return (HttpTT x) >>= f = HttpTT (x >>= (httpTT . f)) instance (Monad eff, Monad (t eff), MonadTrans t, MonadFail (t eff)) => MonadFail (HttpTT e r w s p t eff) where fail = HttpTT . fail instance (MonadTrans t, forall m. (Monad m) => Monad (t m)) => MonadTrans (HttpTT e r w s p t) where lift = HttpTT . lift -- | Lift a value from the inner transformer. liftHttpTT :: (Monad eff, Monad (t eff), MonadTrans t) => t eff a -> HttpTT e r w s p t eff a liftHttpTT = HttpTT . S.liftScriptTT -- | Execute an `HttpTT` session. execHttpTT :: (Monad eff, Monad (t eff), MonadTrans t) => S s -- ^ Initial state -> R e w r -- ^ Environment -> (forall u. P p u -> eff u) -- ^ Effect evaluator -> HttpTT e r w s p t eff a -> t eff (Either (E e) a, S s, W e w) execHttpTT s r p = S.execScriptTT s r p . httpTT -- | Turn an `HttpTT` into a property; for testing with QuickCheck. checkHttpTT :: forall eff t q e r w s p a prop . (Monad eff, Monad (t eff), MonadTrans t, Show q, Testable prop) => S s -- ^ Initial state -> R e w r -- ^ Environment -> (forall u. P p u -> eff u) -- ^ Effect evaluator -> (t eff (Either (E e) a, S s, W e w) -> IO q) -- ^ Condense to `IO` -> (q -> prop) -- ^ Result check -> HttpTT e r w s p t eff a -> Property checkHttpTT s r eval cond check = S.checkScriptTT s r eval cond check . httpTT -- | Retrieve the environment. ask :: (Monad eff, Monad (t eff), MonadTrans t) => HttpTT e r w s p t eff (R e w r) ask = HttpTT S.ask -- | Run an action with a locally adjusted environment of the same type. local :: (Monad eff, Monad (t eff), MonadTrans t) => (R e w r -> R e w r) -> HttpTT e r w s p t eff a -> HttpTT e r w s p t eff a local f = HttpTT . S.local f . httpTT -- | Run an action with a locally adjusted environment of a possibly different type. transport :: (Monad eff, Monad (t eff), MonadTrans t) => (R e w r2 -> R e w r1) -> HttpTT e r1 w s p t eff a -> HttpTT e r2 w s p t eff a transport f = HttpTT . S.transport f . httpTT -- | Retrieve the image of the environment under a given function. reader :: (Monad eff, Monad (t eff), MonadTrans t) => (R e w r -> a) -> HttpTT e r w s p t eff a reader f = HttpTT (S.reader f) -- | Retrieve the current state. get :: (Monad eff, Monad (t eff), MonadTrans t) => HttpTT e r w s p t eff (S s) get = HttpTT S.get -- | Replace the state. put :: (Monad eff, Monad (t eff), MonadTrans t) => S s -> HttpTT e r w s p t eff () put s = HttpTT (S.put s) -- | Modify the current state strictly. modify :: (Monad eff, Monad (t eff), MonadTrans t) => (S s -> S s) -> HttpTT e r w s p t eff () modify f = HttpTT (S.modify' f) -- | Retrieve the image of the current state under a given function. gets :: (Monad eff, Monad (t eff), MonadTrans t) => (S s -> a) -> HttpTT e r w s p t eff a gets f = HttpTT (S.gets f) -- | Do not export; we want to only allow writes to the log via functions that call @logNow@. tell :: (Monad eff, Monad (t eff), MonadTrans t) => W e w -> HttpTT e r w s p t eff () tell w = HttpTT (S.tell w) -- | Run an action that returns a value and a log-adjusting function, and apply the function to the local log. pass :: (Monad eff, Monad (t eff), MonadTrans t) => HttpTT e r w s p t eff (a, W e w -> W e w) -> HttpTT e r w s p t eff a pass = HttpTT . S.pass . httpTT -- | Run an action, applying a function to the local log. censor :: (Monad eff, Monad (t eff), MonadTrans t) => (W e w -> W e w) -> HttpTT e r w s p t eff a -> HttpTT e r w s p t eff a censor f = HttpTT . S.censor f . httpTT -- | Inject an 'Either' into a 'Script'. except :: (Monad eff, Monad (t eff), MonadTrans t) => Either (E e) a -> HttpTT e r w s p t eff a except e = HttpTT (S.except e) -- | Raise an error throw :: (Monad eff, Monad (t eff), MonadTrans t) => E e -> HttpTT e r w s p t eff a throw e = HttpTT (S.throw e) -- | Run an action, applying a handler in case of an error result. catch :: (Monad eff, Monad (t eff), MonadTrans t) => HttpTT e r w s p t eff a -- ^ Computation that may raise an error -> (E e -> HttpTT e r w s p t eff a) -- ^ Handler -> HttpTT e r w s p t eff a catch x f = HttpTT (S.catch (httpTT x) (httpTT . f)) -- | Inject an atomic effect. prompt :: (Monad eff, Monad (t eff), MonadTrans t) => P p a -> HttpTT e r w s p t eff a prompt p = HttpTT (S.prompt p) -- | Error type. data E e = E_Http HttpException | E_IO IOException | E_Json JsonError | E e -- ^ Client-supplied error type. deriving Show -- | Pretty printer for errors printError :: (e -> Text) -> E e -> Text printError p err = case err of E_Http e -> T.unlines [ "HTTP Exception:", T.pack $ show e ] E_IO e -> T.unlines [ "IO Exception:", T.pack $ show e ] E_Json e -> T.unlines [ "JSON Error:", T.pack $ show e ] E e -> T.unlines [ "Error:", p e ] -- | Also logs the exception. throwHttpException :: (Monad eff, Monad (t eff), MonadTrans t) => HttpException -> HttpTT e r w s p t eff a throwHttpException e = do logNow LogError $ errorMessage $ E_Http e throw $ E_Http e -- | Re-throws other error types. catchHttpException :: (Monad eff, Monad (t eff), MonadTrans t) => HttpTT e r w s p t eff a -> (HttpException -> HttpTT e r w s p t eff a) -- ^ Handler -> HttpTT e r w s p t eff a catchHttpException x handler = catch x $ \err -> case err of E_Http e -> handler e _ -> throw err -- | Also logs the exception. throwIOException :: (Monad eff, Monad (t eff), MonadTrans t) => IOException -> HttpTT e r w s p t eff a throwIOException e = do logNow LogError $ errorMessage $ E_IO e throw $ E_IO e -- | Re-throws other error types. catchIOException :: (Monad eff, Monad (t eff), MonadTrans t) => HttpTT e r w s p t eff a -> (IOException -> HttpTT e r w s p t eff a) -- ^ Handler -> HttpTT e r w s p t eff a catchIOException x handler = catch x $ \err -> case err of E_IO e -> handler e _ -> throw err -- | Also logs the exception. throwJsonError :: (Monad eff, Monad (t eff), MonadTrans t) => JsonError -> HttpTT e r w s p t eff a throwJsonError e = do logNow LogError $ errorMessage $ E_Json e throw $ E_Json e -- | Re-throws other error types. catchJsonError :: (Monad eff, Monad (t eff), MonadTrans t) => HttpTT e r w s p t eff a -> (JsonError -> HttpTT e r w s p t eff a) -- ^ Handler -> HttpTT e r w s p t eff a catchJsonError x handler = catch x $ \err -> case err of E_Json e -> handler e _ -> throw err -- | Also logs the exception. throwError :: (Monad eff, Monad (t eff), MonadTrans t) => e -> HttpTT e r w s p t eff a throwError e = do logNow LogError $ errorMessage $ E e throw $ E e -- | Re-throws other error types. catchError :: (Monad eff, Monad (t eff), MonadTrans t) => HttpTT e r w s p t eff a -> (e -> HttpTT e r w s p t eff a) -- ^ Handler -> HttpTT e r w s p t eff a catchError x handler = catch x $ \err -> case err of E e -> handler e _ -> throw err -- | Handle any thrown error. To handle only errors of a specific type, see @catchError@, @catchJsonError@, @catchIOException@, or @catchHttpException@. catchAnyError :: (Monad eff, Monad (t eff), MonadTrans t) => HttpTT e r w s p t eff a -> (e -> HttpTT e r w s p t eff a) -> (HttpException -> HttpTT e r w s p t eff a) -> (IOException -> HttpTT e r w s p t eff a) -> (JsonError -> HttpTT e r w s p t eff a) -> HttpTT e r w s p t eff a catchAnyError x hE hHttp hIO hJson = catch x $ \err -> case err of E e -> hE e E_Http e -> hHttp e E_IO e -> hIO e E_Json e -> hJson e -- | Generic session environment. data R e w r = R { _logOptions :: LogOptions e w -- | Printer for log entries. , _logEntryPrinter :: LogOptions e w -> LogEntry e w -> Maybe Text -- | Handle for printing logs , _logHandle :: Handle -- | Lock used to prevent race conditions when writing to the log. , _logLock :: Maybe (MVar ()) -- | Identifier string for the session; used to help match log entries emitted by the same session. , _uid :: Text -- | Function for elevating 'HttpException's to a client-supplied error type. , _httpErrorInject :: HttpException -> Maybe e -- | Client-supplied environment type. , _env :: r } -- | Environment constructor basicEnv :: (Show e, Show w) => r -- ^ Client-supplied environment value. -> R e w r basicEnv r = R { _httpErrorInject = const Nothing , _logOptions = basicLogOptions , _logEntryPrinter = basicLogEntryPrinter , _logHandle = stdout , _logLock = Nothing , _uid = "" , _env = r } -- | Environment constructor trivialEnv :: r -- ^ Client-supplied environment value. -> R e w r trivialEnv r = R { _httpErrorInject = const Nothing , _logOptions = trivialLogOptions , _logEntryPrinter = basicLogEntryPrinter , _logHandle = stdout , _logLock = Nothing , _uid = "" , _env = r } -- | Options for tweaking the logs. data LogOptions e w = LogOptions { -- | Toggle color _logColor :: Bool -- | Toggle JSON pretty printing , _logJson :: Bool -- | Toggle to silence the logs , _logSilent :: Bool -- | Suppress log output below this severity , _logMinSeverity :: LogSeverity -- | Toggle for printing HTTP headers , _logHeaders :: Bool -- | Printer for client-supplied error type. The boolean toggles JSON pretty printing. , _printUserError :: Bool -> e -> Text -- | Printer for client-supplied log type. the boolean toggles JSON pretty printing. , _printUserLog :: Bool -> w -> Text } -- | Noisy, in color, without parsing JSON responses, and using `Show` instances for user-supplied error and log types. basicLogOptions :: (Show e, Show w) => LogOptions e w basicLogOptions = LogOptions { _logColor = True , _logJson = False , _logSilent = False , _logMinSeverity = LogDebug , _logHeaders = True , _printUserError = \_ e -> T.pack $ show e , _printUserLog = \_ w -> T.pack $ show w } -- | Noisy, in color, without parsing JSON responses, and using trivial printers for user-supplied error and log types. For testing. trivialLogOptions :: LogOptions e w trivialLogOptions = LogOptions { _logColor = True , _logJson = False , _logSilent = False , _logMinSeverity = LogDebug , _logHeaders = True , _printUserError = \_ _ -> "ERROR" , _printUserLog = \_ _ -> "LOG" } -- | Simple default pretty printer for @LogEntry@s. basicLogEntryPrinter :: LogOptions e w -> LogEntry e w -> Maybe Text basicLogEntryPrinter opt@LogOptions{..} LogEntry{..} = if _logSilent || (_logEntrySeverity < _logMinSeverity) then Nothing else let colorize :: Text -> Text colorize msg = if _logColor then colorBySeverity _logEntrySeverity msg else msg timestamp :: Text timestamp = T.pack $ take 19 $ show _logEntryTimestamp in Just $ T.unwords $ filter (/= "") [ colorize timestamp , _logEntryUID , logEntryTitle _logEntry , logEntryBody opt _logEntry ] -- | Log type newtype W e w = W { unW :: [LogEntry e w] } deriving Show instance Semigroup (W e w) where (W a1) <> (W a2) = W (a1 ++ a2) instance Monoid (W e w) where mempty = W [] mappend = (<>) data LogEntry e w = LogEntry { _logEntryTimestamp :: UTCTime , _logEntryUID :: Text , _logEntrySeverity :: LogSeverity , _logEntry :: Log e w } deriving Show -- | Log entry type data Log e w = L_Comment Text | L_Request HttpVerb Url Wreq.Options (Maybe ByteString) | L_SilentRequest | L_Response HttpResponse | L_SilentResponse | L_Pause Int | L_HttpError HttpException | L_IOError IOException | L_JsonError JsonError -- | Client-supplied error type | L_Error e -- | Client-supplied log entry type | L_Log w deriving Show logEntryTitle :: Log e w -> LogEntryTitle logEntryTitle e = case e of L_Comment _ -> "Comment" L_Request _ _ _ _ -> "Request" L_SilentRequest -> "Silent Request" L_Response _ -> "Response" L_SilentResponse -> "Silent Response" L_Pause _ -> "Pause" L_HttpError _ -> "HTTP Exception" L_IOError _ -> "IO Exception" L_JsonError _ -> "JSON Error" L_Error _ -> "Error" L_Log _ -> "Log" -- | Used in the logs. data HttpVerb = DELETE | GET | POST deriving (Eq, Show) -- | All log statements should go through @logNow@. printHttpLogs :: Handle -> Maybe (MVar ()) -> LogOptions e w -> (LogOptions e w -> LogEntry e w -> Maybe Text) -> W e w -> IO () printHttpLogs handle lock opts printer (W ws) = do let printEntry w = case printer opts w of Nothing -> return () Just str -> do case lock of Just lock -> withMVar lock (\() -> T.hPutStrLn handle str) Nothing -> T.hPutStrLn handle str hFlush handle if _logSilent opts then return () else mapM_ printEntry ws -- | Convert errors to log entries errorMessage :: E e -> Log e w errorMessage e = case e of E_Http err -> L_HttpError err E_IO err -> L_IOError err E_Json err -> L_JsonError err E e -> L_Error e type LogEntryTitle = Text type LogEntryBody = Text logEntryBody :: LogOptions e w -> Log e w -> LogEntryBody logEntryBody LogOptions{..} entry = case entry of L_Comment msg -> msg L_Request verb url opt payload -> let head :: Text head = case (_logJson, _logHeaders) of (True, True) -> T.decodeUtf8 $ toStrict $ encodePretty $ jsonResponseHeaders $ opt ^. Wreq.headers (False, True) -> T.pack $ show $ opt ^. Wreq.headers (_, False) -> "" body :: Text body = case (_logJson, payload) of (True, Just p) -> case decode p of Nothing -> "JSON parse error:\n" <> T.decodeUtf8 (toStrict p) Just v -> T.decodeUtf8 $ toStrict $ encodePretty (v :: Value) (False, Just p) -> T.decodeUtf8 $ toStrict p (_, Nothing) -> "" in T.intercalate "\n" $ filter (/= "") [ T.unwords ["Request", T.pack $ show verb, url] , head , body ] L_SilentRequest -> "" L_Response response -> let head :: Text head = case (_logJson, _logHeaders) of (True, True) -> T.decodeUtf8 $ toStrict $ encodePretty $ jsonResponseHeaders $ _responseHeaders response (False, True) -> T.pack $ show $ _responseHeaders response (_, False) -> "" body :: Text body = case _logJson of True -> T.decodeUtf8 $ toStrict $ encodePretty $ preview _Value $ _responseBody response False -> T.pack $ show response in T.intercalate "\n" $ filter (/= "") ["Response", head, body] L_SilentResponse -> "" L_Pause k -> "Wait for " <> T.pack (show k) <> "μs" L_HttpError e -> if _logJson then let unpackHttpError :: HttpException -> Maybe (Text, Text) unpackHttpError err = case err of HttpExceptionRequest _ (StatusCodeException s r) -> do json <- decode $ fromStrict r let status = s ^. Wreq.responseStatus return (T.pack $ show status, T.decodeUtf8 $ toStrict $ encodePretty (json :: Value)) _ -> Nothing in case unpackHttpError e of Nothing -> T.pack $ show e Just (code, json) -> T.intercalate "\n" [ T.unwords [ "HTTP Error Response", code], json ] else T.pack $ show e L_IOError e -> T.unwords [ T.pack $ show $ ioeGetFileName e , T.pack $ ioeGetLocation e , T.pack $ ioeGetErrorString e ] L_JsonError e -> T.pack $ show e L_Error e -> T.unwords [ _printUserError _logJson e ] L_Log w -> T.unwords [ _printUserLog _logJson w ] -- | Extract the user-defined log entries. logEntries :: W e w -> [w] logEntries (W xs) = entries xs where entries [] = [] entries (w:ws) = case _logEntry w of L_Log u -> u : entries ws _ -> entries ws -- | State type data S s = S { _httpOptions :: Wreq.Options , _httpSession :: Maybe S.Session , _userState :: s } deriving Show -- | State constructor basicState :: s -> S s basicState s = S { _httpOptions = Wreq.defaults , _httpSession = Nothing , _userState = s } -- | Atomic effects data P p a where HPutStrLn :: Handle -> Text -> P p (Either IOException ()) HPutStrLnBlocking :: MVar () -> Handle -> Text -> P p (Either IOException ()) GetSystemTime :: P p UTCTime ThreadDelay :: Int -> P p () HttpGet :: Wreq.Options -> Maybe S.Session -> Url -> P p (Either HttpException HttpResponse) HttpPost :: Wreq.Options -> Maybe S.Session -> Url -> ByteString -> P p (Either HttpException HttpResponse) HttpDelete :: Wreq.Options -> Maybe S.Session -> Url -> P p (Either HttpException HttpResponse) P :: p a -> P p a -- | Basic evaluator for interpreting atomic 'Http' effects in 'IO'. evalIO :: (p a -> IO a) -- ^ Evaluator for user effects -> P p a -> IO a evalIO eval x = case x of HPutStrLn handle string -> try $ do T.hPutStrLn handle string hFlush handle HPutStrLnBlocking lock handle str -> try $ do withMVar lock (\() -> T.hPutStrLn handle str) hFlush handle GetSystemTime -> fmap systemToUTCTime getSystemTime ThreadDelay k -> threadDelay k HttpGet opts s url -> let url' = T.unpack url in case s of Nothing -> try $ readHttpResponse <$> Wreq.getWith opts url' Just sn -> try $ readHttpResponse <$> S.getWith opts sn url' HttpPost opts s url msg -> let url' = T.unpack url in case s of Nothing -> try $ readHttpResponse <$> Wreq.postWith opts url' msg Just sn -> try $ readHttpResponse <$> S.postWith opts sn url' msg HttpDelete opts s url -> let url' = T.unpack url in case s of Nothing -> try $ readHttpResponse <$> Wreq.deleteWith opts url' Just sn -> try $ readHttpResponse <$> S.deleteWith opts sn url' P act -> eval act -- | Basic evaluator for interpreting atomic 'Http' effects in 'MockIO'. evalMockIO :: (p a -> MockIO s a) -> P p a -> MockIO s a evalMockIO eval x = case x of HPutStrLn handle str -> do incrementTimer 1 fmap Right $ modifyMockWorld $ \w -> w { _files = appendLines (Right handle) (T.lines str) $ _files w } HPutStrLnBlocking _ handle str -> do incrementTimer 1 fmap Right $ modifyMockWorld $ \w -> w { _files = appendLines (Right handle) (T.lines str) $ _files w } GetSystemTime -> do incrementTimer 1 MockWorld{..} <- getMockWorld return _time ThreadDelay k -> incrementTimer k HttpGet _ _ url -> do incrementTimer 1 MockWorld{..} <- getMockWorld let (r,t) = unMockNetwork (_httpGet url) _serverState modifyMockWorld $ \w -> w { _serverState = t } return r HttpPost _ _ url payload -> do incrementTimer 1 MockWorld{..} <- getMockWorld let (r,t) = unMockNetwork (_httpPost url payload) _serverState modifyMockWorld $ \w -> w { _serverState = t } return r HttpDelete _ _ url -> do incrementTimer 1 MockWorld{..} <- getMockWorld let (r,t) = unMockNetwork (_httpDelete url) _serverState modifyMockWorld $ \w -> w { _serverState = t } return r P p -> do incrementTimer 1 eval p -- | All log statements should go through @logNow@. logNow :: (Monad eff, Monad (t eff), MonadTrans t) => LogSeverity -> Log e w -> HttpTT e r w s p t eff () logNow severity msg = do time <- prompt GetSystemTime printer <- reader _logEntryPrinter R{..} <- ask case printer _logOptions (LogEntry time _uid severity msg) of Nothing -> return () Just str -> case _logLock of Just lock -> hPutStrLnBlocking lock _logHandle str Nothing -> Control.Monad.Script.Http.hPutStrLn _logHandle str tell $ W [LogEntry time _uid severity msg] -- | Write a comment to the log comment :: (Monad eff, Monad (t eff), MonadTrans t) => Text -> HttpTT e r w s p t eff () comment msg = logNow LogInfo $ L_Comment msg -- | Pause the thread wait :: (Monad eff, Monad (t eff), MonadTrans t) => Int -- ^ milliseconds -> HttpTT e r w s p t eff () wait k = do logNow LogInfo $ L_Pause k prompt $ ThreadDelay k -- | Write an entry to the log logEntry :: (Monad eff, Monad (t eff), MonadTrans t) => LogSeverity -> w -> HttpTT e r w s p t eff () logEntry severity = logNow severity . L_Log -- | For debug level messages logDebug :: (Monad eff, Monad (t eff), MonadTrans t) => w -> HttpTT e r w s p t eff () logDebug = logEntry LogDebug -- | For informational messages logInfo :: (Monad eff, Monad (t eff), MonadTrans t) => w -> HttpTT e r w s p t eff () logInfo = logEntry LogInfo -- | For normal but significant conditions logNotice :: (Monad eff, Monad (t eff), MonadTrans t) => w -> HttpTT e r w s p t eff () logNotice = logEntry LogNotice -- | For warning conditions logWarning :: (Monad eff, Monad (t eff), MonadTrans t) => w -> HttpTT e r w s p t eff () logWarning = logEntry LogWarning -- | For error conditions logError :: (Monad eff, Monad (t eff), MonadTrans t) => w -> HttpTT e r w s p t eff () logError = logEntry LogError -- | For critical conditions logCritical :: (Monad eff, Monad (t eff), MonadTrans t) => w -> HttpTT e r w s p t eff () logCritical = logEntry LogCritical -- | Action must be taken immediately logAlert :: (Monad eff, Monad (t eff), MonadTrans t) => w -> HttpTT e r w s p t eff () logAlert = logEntry LogAlert -- | System is unusable logEmergency :: (Monad eff, Monad (t eff), MonadTrans t) => w -> HttpTT e r w s p t eff () logEmergency = logEntry LogEmergency -- | Set the severity level of all log actions in a session. setLogSeverity :: (Monad eff, Monad (t eff), MonadTrans t) => LogSeverity -> HttpTT e r w s p t eff a -> HttpTT e r w s p t eff a setLogSeverity severity = censor (W . map f . unW) where f :: LogEntry e w -> LogEntry e w f e = e { _logEntrySeverity = severity } -- | Write a line to a handle hPutStrLn :: (Monad eff, Monad (t eff), MonadTrans t) => Handle -> Text -> HttpTT e r w s p t eff () hPutStrLn h str = do result <- prompt $ HPutStrLn h str case result of Right () -> return () Left e -> throwIOException e -- | Write a line to a handle, using the given `MVar` as a lock hPutStrLnBlocking :: (Monad eff, Monad (t eff), MonadTrans t) => MVar () -> Handle -> Text -> HttpTT e r w s p t eff () hPutStrLnBlocking lock h str = do result <- prompt $ HPutStrLnBlocking lock h str case result of Right () -> return () Left e -> throwIOException e -- | Run a @GET@ request httpGet :: (Monad eff, Monad (t eff), MonadTrans t) => Url -> HttpTT e r w s p t eff HttpResponse httpGet url = do R{..} <- ask S{..} <- get logNow LogDebug $ L_Request GET url _httpOptions Nothing result <- prompt $ HttpGet _httpOptions _httpSession url case result of Right response -> do logNow LogDebug $ L_Response response return response Left err -> case _httpErrorInject err of Just z -> throwError z Nothing -> throwHttpException err -- | Run a @GET@ request, but do not write the request or response to the logs. httpSilentGet :: (Monad eff, Monad (t eff), MonadTrans t) => Url -> HttpTT e r w s p t eff HttpResponse httpSilentGet url = do R{..} <- ask S{..} <- get logNow LogDebug L_SilentRequest result <- prompt $ HttpGet _httpOptions _httpSession url case result of Right response -> do logNow LogDebug L_SilentResponse return response Left err -> case _httpErrorInject err of Just z -> throwError z Nothing -> throwHttpException err -- | Run a @POST@ request httpPost :: (Monad eff, Monad (t eff), MonadTrans t) => Url -> ByteString -- ^ Payload -> HttpTT e r w s p t eff HttpResponse httpPost url payload = do R{..} <- ask S{..} <- get logNow LogDebug $ L_Request POST url _httpOptions (Just payload) result <- prompt $ HttpPost _httpOptions _httpSession url payload case result of Right response -> do logNow LogDebug $ L_Response response return response Left err -> case _httpErrorInject err of Just z -> throwError z Nothing -> throwHttpException err -- | Run a @POST@ request, but do not write the request or response to the logs. httpSilentPost :: (Monad eff, Monad (t eff), MonadTrans t) => Url -> ByteString -- ^ Payload -> HttpTT e r w s p t eff HttpResponse httpSilentPost url payload = do R{..} <- ask S{..} <- get logNow LogDebug L_SilentRequest result <- prompt $ HttpPost _httpOptions _httpSession url payload case result of Right response -> do logNow LogDebug L_SilentResponse return response Left err -> case _httpErrorInject err of Just z -> throwError z Nothing -> throwHttpException err -- | Run a @DELETE@ request httpDelete :: (Monad eff, Monad (t eff), MonadTrans t) => Url -> HttpTT e r w s p t eff HttpResponse httpDelete url = do R{..} <- ask S{..} <- get logNow LogDebug $ L_Request DELETE url _httpOptions Nothing result <- prompt $ HttpDelete _httpOptions _httpSession url case result of Right response -> do logNow LogDebug$ L_Response response return response Left err -> case _httpErrorInject err of Just z -> throwError z Nothing -> throwHttpException err -- | Run a @DELETE@ request, but do not write the request or response to the logs. httpSilentDelete :: (Monad eff, Monad (t eff), MonadTrans t) => Url -> HttpTT e r w s p t eff HttpResponse httpSilentDelete url = do R{..} <- ask S{..} <- get logNow LogDebug L_SilentRequest result <- prompt $ HttpDelete _httpOptions _httpSession url case result of Right response -> do logNow LogDebug L_SilentResponse return response Left err -> case _httpErrorInject err of Just z -> throwError z Nothing -> throwHttpException err -- | Parse a `ByteString` to a JSON `Value`. parseJson :: (Monad eff, Monad (t eff), MonadTrans t) => ByteString -> HttpTT e r w s p t eff Value parseJson bytes = case preview _Value bytes of Just value -> return value Nothing -> throwJsonError $ JsonParseError bytes -- | Object member lookup. lookupKeyJson :: (Monad eff, Monad (t eff), MonadTrans t) => Text -- ^ Key name -> Value -- ^ JSON object -> HttpTT e r w s p t eff Value lookupKeyJson key v = case v of Object obj -> let #if MIN_VERSION_aeson(2,0,0) val = lookup (fromText key) obj #else val = lookup key obj #endif in case val of Nothing -> throwJsonError $ JsonKeyDoesNotExist key (Object obj) Just value -> return value _ -> throwJsonError $ JsonKeyLookupOffObject key v -- | Decode a `A.Value` to some other type. constructFromJson :: (Monad eff, Monad (t eff), MonadTrans t, FromJSON a) => Value -> HttpTT e r w s p t eff a constructFromJson value = case fromJSON value of Success x -> return x Error msg -> throwJsonError $ JsonConstructError msg