vigilance-0.1.0.0: src/Utils/Vigilance/Types.hs
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
module Utils.Vigilance.Types where
import Prelude (FilePath)
import ClassyPrelude hiding (FilePath)
import Control.Concurrent.STM.TChan (TChan)
import Control.Monad (mzero)
import Control.Monad.Reader (ReaderT)
import Control.Lens hiding ((.=))
import Data.Aeson
import qualified Data.Attoparsec.Number as N
import Data.SafeCopy ( base
, SafeCopy
, deriveSafeCopy)
import Data.Store ( M
, O
, (:.)
, Store )
import qualified Data.Store as S
import Data.Store.Storable (Storable(..))
import Data.Time.Clock.POSIX (POSIXTime)
import qualified Data.Vector as V
import Network.Http.Client ( URL
, StatusCode )
import System.Log.FastLogger ( ToLogStr(..) )
import Text.InterpolatedString.Perl6 (qc)
import Yesod.Core.Dispatch (PathPiece)
newtype ID = ID { _unID :: Int } deriving ( Show
, Eq
, Enum
, Read
, PathPiece
, Ord
, Num
, SafeCopy
, FromJSON
, ToJSON
, Typeable)
makeClassy ''ID
instance Bounded ID where
minBound = ID 1
maxBound = ID maxBound
--TODO: somewhere have to handle the case of <= 0
data WatchInterval = Every Integer TimeUnit deriving (Show, Eq, Typeable, Ord)
instance ToJSON WatchInterval where
toJSON (Every n u) = Array $ V.fromList [toJSON n, toJSON u]
instance FromJSON WatchInterval where
parseJSON = withArray "WatchInterval" $ parseWatchInterval . V.toList
where parseWatchInterval [Number (N.I n), s@(String _)]
| n > 0 = Every <$> pure n <*> parseJSON s -- just get it out of the N.I and call pure?
| otherwise = fail "interval must be > 0"
parseWatchInterval _ = fail "expecting a pair of integer and string"
data TimeUnit = Seconds |
Minutes |
Hours |
Days |
Weeks |
Years deriving (Show, Eq, Ord)
instance ToJSON TimeUnit where
toJSON Seconds = String "seconds"
toJSON Minutes = String "minutes"
toJSON Hours = String "hours"
toJSON Days = String "days"
toJSON Weeks = String "weeks"
toJSON Years = String "years"
txtToTimeUnit :: Text -> Maybe TimeUnit
txtToTimeUnit "seconds" = Just Seconds
txtToTimeUnit "minutes" = Just Minutes
txtToTimeUnit "hours" = Just Hours
txtToTimeUnit "days" = Just Days
txtToTimeUnit "weeks" = Just Weeks
txtToTimeUnit "years" = Just Years
txtToTimeUnit _ = Nothing
instance FromJSON TimeUnit where
parseJSON = withText "TimeUnit" parseTimeUnit
where parseTimeUnit txt = maybe unknown return $ txtToTimeUnit txt
unknown = fail "unknown time unit"
newtype EmailAddress = EmailAddress { _unEmailAddress :: Text } deriving ( Show
, Eq
, Ord
, SafeCopy
, Typeable
, ToJSON
, FromJSON)
makeClassy ''EmailAddress
data NotificationPreference = EmailNotification EmailAddress |
HTTPNotification URL deriving (Show, Eq, Ord)
instance ToJSON NotificationPreference where
toJSON (EmailNotification a) = object [ "type" .= String "email"
, "address" .= String (a ^. unEmailAddress)]
toJSON (HTTPNotification u) = object [ "type" .= String "http"
, "url" .= String (decodeUtf8 u)]
instance FromJSON NotificationPreference where
parseJSON v = parseEmailNotification v <|>
parseHTTPNotification v
where parseEmailNotification = withObject "EmailNotification" parseEmail
parseEmail obj = do t <- obj .: "type"
case t of
String "email" -> EmailNotification <$> obj .: "address"
_ -> mzero
parseHTTPNotification = withObject "HTTPNotification" parseHttp
parseHttp obj = do t <- obj .: "type"
case t of
String "http" -> HTTPNotification <$> obj .: "url"
_ -> mzero
newtype POSIXWrapper = POSIXWrapper { unPOSIXWrapper :: POSIXTime }
instance FromJSON POSIXWrapper where
parseJSON = withNumber "POSIXTime" parsePOSIXTime
where parsePOSIXTime (N.I i) = pure . POSIXWrapper . fromIntegral $ i
parsePOSIXTime _ = fail "Expected integer"
instance ToJSON POSIXWrapper where
toJSON = Number . N.I . truncate . toRational . unPOSIXWrapper
data WatchState = Active { _lastCheckIn :: POSIXTime } |
Paused |
Notifying |
Triggered deriving (Show, Eq, Ord) -- ehhhhhh
makeClassy ''WatchState
instance Monoid WatchState where
mempty = Paused
mappend Paused Paused = Paused
mappend x Paused = x
mappend _ y = y
instance ToJSON WatchState where
toJSON (Active t) = object [ "name" .= String "active"
, "last_check_in" .= POSIXWrapper t ]
toJSON Paused = object [ "name" .= String "paused" ]
toJSON Notifying = object [ "name" .= String "notifying" ]
toJSON Triggered = object [ "name" .= String "triggered" ]
instance FromJSON WatchState where
parseJSON = withObject "WatchState" parseWatchState
where parseWatchState obj = withText "state name" (parseStateFromName obj) =<< (obj .: "name")
parseStateFromName _ "paused" = pure Paused
parseStateFromName _ "notifying" = pure Notifying
parseStateFromName _ "triggered" = pure Triggered
parseStateFromName obj "active" = Active <$> (unPOSIXWrapper <$> obj .: "last_check_in")
parseStateFromName _ _ = fail "Invalid value"
newtype WatchName = WatchName { _unWatchName :: Text } deriving ( Show
, Eq
, Ord
, FromJSON
, ToJSON
, IsString
, Read
, PathPiece ) -- not so sure about the isstring
makeLenses ''WatchName
data Watch i = Watch { _watchId :: i
, _watchName :: WatchName
, _watchInterval :: WatchInterval
, _watchWState :: WatchState
, _watchNotifications :: [NotificationPreference] } deriving (Show, Eq, Typeable, Ord)
makeLenses ''Watch
type NewWatch = Watch ()
type EWatch = Watch ID
instance ToJSON EWatch where
toJSON w = object [ "id" .= (w ^. watchId)
, "name" .= (w ^. watchName)
, "interval" .= (w ^. watchInterval)
, "state" .= (w ^. watchWState)
, "notifications" .= (w ^. watchNotifications)
, "name" .= (w ^. watchName) ]
instance FromJSON EWatch where
parseJSON = withObject "Watch" parseNewWatch
where parseNewWatch obj = Watch <$> obj .: "id"
<*> obj .: "name"
<*> obj .: "interval"
<*> obj .: "state"
<*> obj .: "notifications"
instance ToJSON NewWatch where
toJSON w = object [ "name" .= (w ^. watchName)
, "interval" .= (w ^. watchInterval)
, "state" .= (w ^. watchWState)
, "notifications" .= (w ^. watchNotifications)
, "name" .= (w ^. watchName) ]
instance FromJSON NewWatch where
parseJSON = withObject "Watch" parseNewWatch
where parseNewWatch obj = Watch <$> pure ()
<*> obj .: "name"
<*> obj .: "interval"
<*> obj .: "state"
<*> obj .: "notifications"
data WatchStoreTag = WatchStoreTag
-- tagspec
instance Storable NewWatch where
type StoreTS NewWatch = ID :. WatchName :. WatchInterval :. WatchState :. NotificationPreference
type StoreKRS NewWatch = O :. O :. O :. O :. M
type StoreIRS NewWatch = O :. O :. M :. M :. M
key (Watch _ wn wi ws wns) =
S.dimA S..:
S.dimO wn S..:
S.dimO wi S..:
S.dimO ws S..:.
S.dimM wns
type WatchTable = Store WatchStoreTag (StoreKRS NewWatch) (StoreIRS NewWatch) (StoreTS NewWatch) NewWatch
data NotificationError = FailedByCode StatusCode |
FailedByException Text deriving (Eq, Show, Typeable)
instance ToJSON NotificationError where
toJSON (FailedByCode c) = object [ "error" .= String "code_error"
, "status_code" .= c
, "message" .= String [qc|Failed with status code {c}|] ]
toJSON (FailedByException e) = object [ "error" .= String "exception"
, "exception" .= e
, "message" .= String [qc|Failed with exception {e}|] ]
instance FromJSON NotificationError where
parseJSON = withObject "NotificationError" parseNotificationError
where parseNotificationError obj = do
err <- obj .: "error"
case err of
String "code_error" -> FailedByCode <$> obj .: "status_code"
String "exception" -> FailedByException <$> obj .: "exception"
_ -> mzero
data FailedNotification = FailedNotification { _failedWatch :: EWatch
, _failedPref :: NotificationPreference
, _failedLastError :: NotificationError
, _retries :: Int } deriving (Typeable, Show, Eq)
instance ToJSON FailedNotification where
toJSON FailedNotification{..} = object [ "failed_watch" .= _failedWatch
, "failed_notification" .= _failedPref
, "last_error" .= _failedLastError
, "retries" .= _retries ]
instance FromJSON FailedNotification where
parseJSON = withObject "FailedNotification" parseFailedNotification
where parseFailedNotification obj = FailedNotification <$> obj .: "failed_watch"
<*> obj .: "failed_notification"
<*> obj .: "last_error"
<*> obj .: "retries"
makeClassy ''FailedNotification
data AppState = AppState { _wTable :: WatchTable
, _failed :: [FailedNotification] } deriving (Typeable)
makeLenses ''AppState
data LogCfg = LogCfg { _logCfgPath :: FilePath
, _logCfgVerbose :: Bool } deriving (Show, Eq)
makeClassy ''LogCfg
data Config = Config { _configAcidPath :: FilePath
, _configFromEmail :: Maybe EmailAddress
, _configPort :: Int
, _configLogCfg :: LogCfg
, _configWatches :: [NewWatch]
, _configMaxRetries :: Int } deriving (Show, Eq)
makeClassy ''Config
-- this is unsound
instance Monoid Config where
mempty = Config defaultAcidPath Nothing defaultPort defaultLogCfg mempty defaultMaxRetries
Config apa ea pa la wa ra `mappend` Config apb eb pb lb wb rb = Config (nonDefault defaultAcidPath apa apb)
(chooseJust ea eb)
(nonDefault defaultPort pa pb)
(nonDefault defaultLogCfg la lb)
(mappend wa wb)
(nonDefault defaultMaxRetries ra rb)
where chooseJust a@(Just _) _ = a
chooseJust _ b = b
nonDefault defValue a b
| a == defValue = b
| b == defValue = a
| otherwise = b
defaultMaxRetries :: Int
defaultMaxRetries = 3
defaultLogCfg :: LogCfg
defaultLogCfg = LogCfg defaultLogPath False
defaultLogPath :: FilePath
defaultLogPath = vigilanceDir <> "/vigilance.log"
defaultAcidPath :: FilePath
defaultAcidPath = vigilanceDir <> "/state/AppState"
defaultPort :: Int
defaultPort = 3000
vigilanceDir :: FilePath
vigilanceDir = "$(HOME)/.vigilance"
data LogMessage = LogMessage Text |
VerboseLogMessage Text deriving (Show, Eq)
instance ToLogStr LogMessage where
toLogStr (LogMessage x) = toLogStr x
toLogStr (VerboseLogMessage x) = toLogStr x
-- should i use chan, tmchan?
type LogChan = TChan [LogMessage]
-- maybe need a local ctx that can name the context and then nest a chan?
data LogCtx = LogCtx { _ctxName :: Text
, _ctxChan :: LogChan }
makeClassy ''LogCtx
type LogCtxT m a = ReaderT LogCtx m a
type Notifier = [EWatch] -> LogCtxT IO [FailedNotification]
newtype EmailNotifier = EmailNotifier { _emailNotifier :: Notifier }
makeFields ''EmailNotifier
newtype HTTPNotifier = HTTPNotifier { _httpNotifier :: Notifier }
makeFields ''HTTPNotifier
newtype LogNotifier = LogNotifier { _logNotifier :: Notifier }
makeFields ''LogNotifier
data NotifierGroup = NotifierGroup { _ngEmail :: Maybe EmailNotifier
, _ngHTTP :: HTTPNotifier
, _ngLog :: LogNotifier }
makeClassy ''NotifierGroup
deriveSafeCopy 0 'base ''WatchName
deriveSafeCopy 0 'base ''WatchState
deriveSafeCopy 0 'base ''TimeUnit
deriveSafeCopy 0 'base ''WatchInterval
deriveSafeCopy 0 'base ''Watch
deriveSafeCopy 0 'base ''NotificationPreference
deriveSafeCopy 0 'base ''NotificationError
deriveSafeCopy 0 'base ''FailedNotification
deriveSafeCopy 0 'base ''AppState