module Data.Component.Mock ( (:~:)(..) , WithResult(..) , IsAction(..) , mockAction , InContextOf , Executes , runMock , withActions , module Data.Component.Mock.TH ) where import Relude import Data.Type.Equality ((:~:)(..)) import Data.Constraint ((:-), (\\)) import Data.Constraint.Forall (ForallF, instF) import Control.Monad.Trans.MultiState hiding (MultiState) import Data.HList.ContainsType import Data.Component.Mock.TH {- | Context that gets injected to the components to store the values that are being executed. These actions will be compared during the tests with the ones that you expect, failing in case of a mismatch. -} type InContextOf s = MultiStateT s IO {-| Constraint to specify that some actions contain the type of actions to be executed. -} type Executes action actions = ContainsType [WithResult action] actions {-| Runs the mock context and all the checks with it -} runMock :: InContextOf '[] a -> IO () runMock = runMultiStateTNil_ {-| Specify the expected actions for a given component to expect to be executed -} withActions :: IsAction action => [WithResult action] -> InContextOf ([WithResult action] : otherActions) a -> InContextOf otherActions (a, [WithResult action]) withActions actions execution = do (result, actionsRest) <- withMultiState actions execution case actionsRest of [] -> pure (result, actionsRest) remainingActions -> error $ "Execution ended, but those actions were expected to be run:\n" <> unlines (fmap (\(action :-> _) -> " • '" <> showAction action <> "'") remainingActions) {-| Operator to specify that an action returns some result -} data WithResult action where (:->) :: action result -> result -> WithResult action {-| Class that all actions must implement in order to work with the rest of the library. You only need to implement 'eqAction' -} class IsAction (action :: Type -> Type) where eqAction :: action a -> action b -> Maybe (a :~: b) showAction :: action a -> Text default showAction :: ForallF Show action => action a -> Text showAction = toText . showAction' where showAction' :: forall g a. ForallF Show g => g a -> String showAction' x = show x \\ (instF :: ForallF Show g :- Show (g a)) {-| Utility function to be used in the creation of the mock components, so the methods store action values instead of executing anything else -} mockAction :: ContainsType ([WithResult action]) actions => IsAction action => Text -> action result -> InContextOf actions result mockAction functionName action = do nextAction <- mGet case nextAction of [] -> error $ "Expected end of program, but called '" <> functionName <> "'\n" <> " given action: '" <> showAction action <> "'\n" (action' :-> result) : actions | Just Refl <- action `eqAction` action' -> do mSet actions pure result | otherwise -> error $ "Incorrect call to '" <> functionName <> "'\n" <> " called: '" <> showAction action <> "'\n" <> " expected a call to: '" <> showAction action' <> "'\n"