{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE QuasiQuotes #-} module Utils.Vigilance.Client.Client ( getList , getInfo , pause , unPause , checkIn , test , displayList , displayWatch , displayWatchInfo , displayFailedNotifications -- for testing , renderList , renderWatch , renderWatchInfo , renderFailedNotifications , VError(..) ) where import ClassyPrelude import Control.Lens import Control.Monad.Trans.Reader (asks) import Data.Aeson ( FromJSON , json , Result(..) , fromJSON ) import Blaze.ByteString.Builder (Builder) import Data.Ix (inRange) import Network.Http.Client ( Method(GET, POST) , Response , emptyBody , withConnection , openConnection , http , setAccept , setHeader , buildRequest , sendRequest , receiveResponse , RequestBuilder , getStatusCode , StatusCode ) import System.IO.Streams.Attoparsec (parseFromStream) import qualified System.IO.Streams as S import Text.InterpolatedString.Perl6 (qc) import Utils.Vigilance.Client.Config import Utils.Vigilance.Types displayList :: [EWatch] -> IO () displayList = putStrLn . renderList renderList :: [EWatch] -> Text renderList = unlines' . map renderWatch displayWatch :: EWatch -> IO () displayWatch = putStrLn . renderWatch renderWatch :: EWatch -> Text renderWatch w = [qc|{name} ({i}) - {interval} - {state}|] where name = w ^. watchName . unWatchName i = w ^. watchId . unID interval = w ^. watchInterval state = w ^. watchWState . to renderState displayWatchInfo :: EWatch -> IO () displayWatchInfo = putStrLn . renderWatchInfo displayFailedNotifications :: [FailedNotification] -> IO () displayFailedNotifications = putStrLn . renderFailedNotifications renderFailedNotifications :: [FailedNotification] -> Text renderFailedNotifications [] = "All notifications sent successfully." renderFailedNotifications fns = unlines' . (header:) . map render $ fns where header = "The following errors were encountered when testing:" render fn = [qc|- {pref} for {wn} ({wid}): {err}|] where pref = fn ^. failedPref . to renderNotificationPref wn = fn ^. failedWatch . watchName . unWatchName wid = fn ^. failedWatch . watchId . unID err = fn ^. failedLastError . to renderNotificationError renderNotificationPref :: NotificationPreference -> Text renderNotificationPref (HTTPNotification u) = [qc|HTTP Notification ({u})|] renderNotificationPref (EmailNotification (EmailAddress a)) = [qc|Email Notification ({a})|] renderNotificationError :: NotificationError -> Text renderNotificationError (FailedByCode c) = [qc|Failed with status code {c}|] renderNotificationError (FailedByException e) = [qc|Failed with exception "{e}"|] renderWatchInfo :: EWatch -> Text renderWatchInfo w = [qc|{renderedWatch} Notifications: {renderedNotifications}|] where renderedWatch = renderWatch w notifications = w ^. watchNotifications renderedNotifications | null notifications = bullet "none" | otherwise = unlines' . map (bullet . renderNotification) $ notifications renderNotification :: NotificationPreference -> Text renderNotification (EmailNotification (EmailAddress a)) = [qc|Email: {a}|] renderNotification (HTTPNotification u) = [qc|HTTP: {u}|] renderState :: WatchState -> Text renderState (Active t) = [qc|Active {t}|] renderState x = show x bullet :: Text -> Text bullet x = [qc| - {x}|] getList :: ClientCtxT IO (VigilanceResponse [EWatch]) getList = makeRequest GET "/watches" emptyBody getInfo :: WatchName -> ClientCtxT IO (VigilanceResponse EWatch) getInfo n = makeRequest GET (watchRoute n) emptyBody pause :: WatchName -> ClientCtxT IO (VigilanceResponse ()) pause n = makeRequest_ POST (watchRoute n <> "/pause") emptyBody unPause :: WatchName -> ClientCtxT IO (VigilanceResponse ()) unPause n = makeRequest_ POST (watchRoute n <> "/unpause") emptyBody checkIn :: WatchName -> ClientCtxT IO (VigilanceResponse ()) checkIn n = makeRequest_ POST (watchRoute n <> "/checkin") emptyBody test :: WatchName -> ClientCtxT IO (VigilanceResponse [FailedNotification]) test n = makeRequest POST (watchRoute n <> "/test") emptyBody watchRoute :: WatchName -> ByteString watchRoute (WatchName n) = "/watches/" <> encodeUtf8 n makeRequest_ :: Method -> ByteString -> (S.OutputStream Builder -> IO b) -> ClientCtxT IO (VigilanceResponse ()) makeRequest_ = makeRequest' unitResponseHandler makeRequest :: FromJSON a => Method -> ByteString -> (S.OutputStream Builder -> IO b) -> ClientCtxT IO (VigilanceResponse a) makeRequest = makeRequest' jsonResponseHandler makeRequest':: (Response -> S.InputStream ByteString -> IO (VigilanceResponse a)) -> Method -> ByteString -> (S.OutputStream Builder -> IO b) -> ClientCtxT IO (VigilanceResponse a) makeRequest' handler m p body = do host <- asks serverHost port <- asks serverPort lift $ withConnection (openConnection host port) $ \c -> do req <- buildRequest $ do http m p setAccept "application/json" setUserAgent defaultUserAgent void $ sendRequest c req body receiveResponse c handler setUserAgent :: ByteString -> RequestBuilder () setUserAgent = setHeader "User-Agent" defaultUserAgent :: ByteString defaultUserAgent = "vigilance client" unitResponseHandler :: Response -> S.InputStream ByteString -> IO (VigilanceResponse ()) unitResponseHandler = responseHandler (const $ return $ Right ()) jsonResponseHandler :: FromJSON a => Response -> S.InputStream ByteString -> IO (VigilanceResponse a) jsonResponseHandler = responseHandler handleJSONBody responseHandler :: (S.InputStream ByteString -> IO (VigilanceResponse a)) -> Response -> S.InputStream ByteString -> IO (VigilanceResponse a) responseHandler successHandler resp stream | responseOk = successHandler stream | notFound = return . Left $ NotFound | otherwise = return . Left $ StatusError statusCode where statusCode = getStatusCode resp responseOk = inRange (200, 299) statusCode notFound = statusCode == 404 handleJSONBody :: FromJSON a => S.InputStream ByteString -> IO (VigilanceResponse a) handleJSONBody stream = coerceParsed <$> parseJSONBody stream data VError = NotFound | ParseError Text | StatusError StatusCode deriving (Show, Eq) parseJSONBody :: FromJSON a => S.InputStream ByteString -> IO (Result a) parseJSONBody = parseFromStream parser where parser = fmap fromJSON json type VigilanceResponse a = Either VError a coerceParsed :: Result a -> VigilanceResponse a coerceParsed (Success a) = Right a coerceParsed (Error e) = Left $ ParseError $ pack e unlines' :: [Text] -> Text unlines' = intercalate "\n"