{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE FlexibleContexts #-} module Laborantin.DSL ( scenario , describe , parameter , require , requireTExpr , dependency , check , resolve , values , str , num , range , arr , setup , teardown , run , self , backend , param , ancestors , ancestorsMatching , ancestorsMatchingTExpr , getVar , setVar , recover , analyze , result , writeResult , appendResult , logger , dbg , err ) where import qualified Data.Map as M import Data.List (partition, nubBy) import Laborantin import Laborantin.Types import Laborantin.Query import Laborantin.Query.Parse import Laborantin.Query.Interpret import Control.Monad.State import Control.Monad.Reader import Control.Monad.Error import Control.Applicative import Data.Dynamic import Data.Text (Text, pack, unpack) import qualified Data.Text as T class Describable a where changeDescription :: Text -> a -> a instance Describable (ScenarioDescription a) where changeDescription d sc = sc { sDesc = d } instance Describable ParameterDescription where changeDescription d pa = pa { pDesc = d } instance Describable (Dependency a) where changeDescription d dep = dep { dDesc = d } -- | DSL entry point to build a 'ScenarioDescription'. scenario :: (Monad m) => Text -> State (ScenarioDescription m) () -> ScenarioDescription m scenario name f = execState f sc0 where sc0 = emptyScenario { sName = name, sHooks = defaultNoHooks } where defaultNoHooks = M.fromList [ ("setup", noop) , ("teardown", noop) , ("run", noop) , ("analyze", noop) ] noop = Action (return ()) -- | Attach a description to the 'Parameter' / 'Scnario' describe :: Describable a => Text -> State a () describe desc = modify (changeDescription desc) -- | DSL entry point to build a 'ParameterDescription' within a scenario. parameter :: Text -> State ParameterDescription () -> State (ScenarioDescription m) () parameter name f = modify (addParam name param) where addParam k v sc0 = sc0 { sParams = M.insert k v (sParams sc0) } param = execState f param0 where param0 = PDesc name "" [] -- | DSL entry point to build a 'Dependency a' within a scenario. dependency :: (Monad m) => Text -> State (Dependency m) () -> State (ScenarioDescription m) () dependency name f = modify (addDep dep) where addDep v sc0 = sc0 { sDeps = v:(sDeps sc0) } dep = execState f dep0 where dep0 = Dep name "" checkF solveF checkF = const (return True) solveF = return . fst -- | Set verification action for the dependency check :: (Execution m -> m Bool) -> State (Dependency m) () check f = do dep0 <- get put $ dep0 { dCheck = f } -- | Set resolution action for the dependency resolve :: ((Execution m, Backend m) -> m (Execution m)) -> State (Dependency m) () resolve f = do dep0 <- get put $ dep0 { dSolve = f } -- | Set default values for the paramater values :: [ParameterValue] -> State ParameterDescription () values xs = do param0 <- get put $ param0 { pValues = xs } -- | Encapsulate a Text as a 'ParameterValue' str :: Text -> ParameterValue str = StringParam -- | Encapsulate an integer value as a 'ParameterValue' num :: Integer -> ParameterValue num = NumberParam . fromInteger -- | Encapsulate a range as a 'ParameterValue' range :: Rational -> Rational -> Rational -> ParameterValue range = Range -- | Encapsulate an array of 'str' or 'num' values as a 'ParameterValue' arr :: [ParameterValue] -> ParameterValue arr = Array -- | Define the setup hook for this scenario setup :: Step m () -> State (ScenarioDescription m) () setup = appendHook "setup" -- | Define the main run hook for this scenario run :: Step m () -> State (ScenarioDescription m) () run = appendHook "run" -- | Define the teardown hook for this scenario teardown :: Step m () -> State (ScenarioDescription m) () teardown = appendHook "teardown" -- | Define the recovery hook for this scenario recover :: (ExecutionError -> Step m ()) -> State (ScenarioDescription m) () recover f = modify (setRecoveryAction action) where action err = Action (f err) setRecoveryAction act sc = sc { sRecoveryAction = Just act } -- | Define the offline analysis hook for this scenario analyze :: Step m () -> State (ScenarioDescription m) () analyze = appendHook "analyze" appendHook :: Text -> Step m () -> State (ScenarioDescription m) () appendHook name f = modify (addHook name $ Action f) where addHook k v sc0 = sc0 { sHooks = M.insert k v (sHooks sc0) } -- | Defines the TExpr Bool to load ancestor requireTExpr :: (MonadIO m, Monad m) => ScenarioDescription m -> TExpr Bool -> State (ScenarioDescription m) () requireTExpr sc query = do let depName = T.concat [(sName sc), " ~> ", (pack $ show query)] modify (\sc0 -> sc0 {sQuery = query}) dependency depName $ do describe "auto-generated dependency for `require` statement" check $ \exec -> do let ancestors = filter ((sName sc ==) . sName . eScenario) (eAncestors exec) let existing = map eParamSet ancestors let missing = missingParameterSets sc query existing return (null $ missing) resolve $ \(exec,backend) -> do storedAncestors <- load backend [sc] query let (execAncestors, otherAncestors) = partition ((sName sc ==) . sName . eScenario) (eAncestors exec) let allAncestors = nubBy (samePath) (storedAncestors ++ execAncestors) newAncestors <- sequence $ prepare backend query allAncestors sc return (exec { eAncestors = newAncestors ++ allAncestors }) where samePath e1 e2 = ePath e1 == ePath e2 -- | Defines the TExpr Bool to load ancestor require :: (MonadIO m, Monad m) => ScenarioDescription m -> Text -> State (ScenarioDescription m) () require sc txt = requireTExpr sc query where query = either (const deflt) (toTExpr deflt) (parseUExpr defaultParsePrefs (unpack txt)) deflt = (B True) -- | Returns current execution self :: Monad m => Step m (Execution m) self = liftM snd ask -- | Returns current backend backend :: Monad m => Step m (Backend m) backend = liftM fst ask -- | Returns a 'Result' object for the given name. -- -- Implementations will return their specific results. result :: Monad m => FilePath -> Step m (Result m) result name = do (b,r) <- ask bResult b r name -- | Write (overwrite) the result in its entirety. -- -- Implementations will return their specific results. writeResult :: Monad m => FilePath -- ^ result name -> Text -- ^ result content -> Step m () writeResult name dat = result name >>= flip pWrite dat -- | Appends a chunk of data to the result. -- -- Implementations will return their specific results. appendResult :: Monad m => FilePath -- ^ result name -> Text -- ^ content to add -> Step m () appendResult name dat = result name >>= flip pAppend dat -- | Return a 'LogHandler' object for this scenario. logger :: Monad m => Step m (LogHandler m) logger = ask >>= uncurry bLogger -- | Sends a line of data to the logger (debug mode) dbg :: Monad m => Text -> Step m () dbg msg = logger >>= flip lLog msg -- | Interrupts the scenario by throwing an error err :: Monad m => String -> Step m () err = throwError . ExecutionError -- | Get the parameter with given name. -- Throw an error if the parameter is missing. param :: Monad m => Text -- ^ the parameter name -> Step m ParameterValue param key = do ret <- liftM (M.lookup key . eParamSet . snd) ask maybe (throwError $ ExecutionError $ "missing param: " ++ unpack key) return ret getVar' :: (Functor m, MonadState DynEnv m) => Text -> m (Maybe Dynamic) getVar' k = M.lookup k <$> get setVar' :: (MonadState DynEnv m) => Text -> Dynamic -> m () setVar' k v = modify (M.insert k v) -- | Set an execution variable. setVar :: (Typeable v, MonadState DynEnv m) => Text -- ^ name of the variable -> v -- ^ value of the variable -> m () setVar k v = setVar' k (toDyn v) -- | Get an execution variable and tries to cast it from it's Dynamic -- representation. -- -- Returns 'Nothing' if the variable is missing or if it could not -- be cast to the wanted type. getVar :: (Typeable v, Functor m, MonadState DynEnv m) => Text -- ^ name of the variable -> m (Maybe v) getVar k = maybe Nothing fromDynamic <$> getVar' k -- | Get all ancestors for a given scenario name and matching a TExpr Bool query. ancestorsMatchingTExpr :: (Monad m) => Text -> TExpr Bool -> Step m [Execution m] ancestorsMatchingTExpr name query = liftM (matching . eAncestors . snd) ask where matching = filter (matchTExpr query) -- | Get all ancestors for a given scenario name and matching a query expressed as a string. -- Current implementation silences errors. ancestorsMatching :: (Monad m) => Text -> Text -> Step m [Execution m] ancestorsMatching name txt = ancestorsMatchingTExpr name query where query = either (const deflt) (toTExpr deflt) (parseUExpr defaultParsePrefs (unpack txt)) deflt = (B False) -- | Get all ancestors for a given scenario name. ancestors :: (Monad m) => Text -> Step m [Execution m] ancestors = flip ancestorsMatchingTExpr (B True)