module DzenDhall.Validation where
import DzenDhall.Config
import DzenDhall.Event
import DzenDhall.Extra
import Control.Monad
import Data.Maybe
import Data.Text (Text)
import Data.Void
import Lens.Micro
import System.Directory (findExecutable)
import System.Exit
import System.Process
import Text.Megaparsec hiding (Token, tokens)
import Text.Megaparsec.Char
import qualified Data.HashMap.Strict as H
import qualified Data.Text
type ParseErrors = Text.Megaparsec.ParseErrorBundle String Void
data Error
= InvalidAutomatonAddress ParseErrors Text
| BinaryNotInPath Text Text
| AssertionFailure Text Text
| InvalidColor ParseErrors Text
| InvalidHook
run :: [Token] -> IO ([Error], [Token])
run tokens = do
let errors = validate tokens
assertionErrors <- checkAssertions tokens
pure $ (errors <> assertionErrors, filterOutAssertions tokens)
filterOutAssertions :: [Token] -> [Token]
filterOutAssertions = filter \case
TokCheck _ -> False
_ -> True
validate :: [Token] -> [Error]
validate = reverse . go []
where
go acc [] = acc
go acc (TokOpen (OAutomaton address stt) : rest) =
let sttErrors =
(concat $ H.elems (unSTT stt) <&> (^. _2)) >>=
\(hook :: Hook) -> [ InvalidHook | null (hook ^. hookCommand) ]
in
go (proceed InvalidAutomatonAddress automatonAddressParser address
(sttErrors <> acc)) rest
go acc (TokOpen (OFG (Color color)) : rest) =
go (proceed InvalidColor colorParser color acc) rest
go acc (TokOpen (OBG (Color color)) : rest) =
go (proceed InvalidColor colorParser color acc) rest
go acc (_ : rest) =
go acc rest
proceed cont parser what acc =
case getError parser (Data.Text.unpack what) of
Nothing -> acc
Just err -> cont err what : acc
colorParser =
(try (hex 6) <|> try (hex 3) <|> colorName) <* eof
where
hex :: Int -> Parser Text
hex size = do
void $ char '#'
replicateM_ size hexDigitChar
pure ""
colorName = do
void letterChar
void $ many (alphaNumChar <|> spaceChar)
pure ""
checkAssertions :: [Token] -> IO [Error]
checkAssertions [] = pure []
checkAssertions (TokCheck check : xs) = do
newErrors <-
case check ^. chAssertion of
BinaryInPath binary -> do
mbPath <- findExecutable (Data.Text.unpack binary)
pure [ BinaryNotInPath binary (check ^. chMessage) | isNothing mbPath ]
SuccessfulExit code -> do
let process = shell (Data.Text.unpack code)
(exitCode, _, _) <- readCreateProcessWithExitCode process ""
pure [ AssertionFailure code (check ^. chMessage) | exitCode /= ExitSuccess ]
(newErrors ++) <$> checkAssertions xs
checkAssertions (_ : xs) = checkAssertions xs
getError :: Text.Megaparsec.Parsec Void String Text -> String -> Maybe (Text.Megaparsec.ParseErrorBundle String Void)
getError parser =
leftToJust . Text.Megaparsec.parse (parser *> Text.Megaparsec.eof) ""
report :: [Error] -> Text
report [] = "No errors."
report errors = header <> (foldMap ((<> "\n\n") . reportError) errors)
where
header = "Some errors encountered while trying to read the configuration:\n\n\n"
namingConventions = "More info: https://github.com/dzen-dhall/dzen-dhall#naming-conventions"
reportError :: Error -> Text
reportError = \case
InvalidAutomatonAddress err address -> fromLines
[ "Invalid automaton address: " <> address
, "Error: " <> Data.Text.pack (Text.Megaparsec.errorBundlePretty err)
, namingConventions
]
BinaryNotInPath binary message -> fromLines $
[ "One of required binaries was not found in $PATH: " <> binary
] <>
[ fromLines
[ ""
, "Message:"
, ""
, message
]
| not (Data.Text.null message)
]
AssertionFailure code message -> fromLines $
[ "One of assertions failed:"
, ""
, code
] <>
[ fromLines
[ ""
, "Message:"
, ""
, message
]
| not (Data.Text.null message)
]
InvalidColor err name -> fromLines
[ "Invalid color value encountered: " <> name
, "Error: " <> Data.Text.pack (Text.Megaparsec.errorBundlePretty err)
]
InvalidHook ->
"Detected a hook with empty command"