4 changed files with 107 additions and 1 deletions
@ -0,0 +1,47 @@ |
|||||||
|
|
||||||
|
module FSM |
||||||
|
( |
||||||
|
FSMState(..) |
||||||
|
, FSMCallback(..) |
||||||
|
, FSM(..) |
||||||
|
, runFsm |
||||||
|
, makeFsm |
||||||
|
) where |
||||||
|
|
||||||
|
import Control.Concurrent.STM.TVar (TVar, newTVarIO, readTVarIO, |
||||||
|
writeTVar) |
||||||
|
import Control.Monad.Extra (whileM) |
||||||
|
import Control.Monad.IO.Class (MonadIO (..)) |
||||||
|
import Control.Monad.STM (atomically) |
||||||
|
import qualified Data.Map as M |
||||||
|
|
||||||
|
class FSMState a where |
||||||
|
isTerminalState :: a -> Bool |
||||||
|
|
||||||
|
newtype FSMCallback m a = FSMCallback (m (Maybe a)) |
||||||
|
|
||||||
|
data FSM a m = FSM |
||||||
|
{ fsmCurrentState :: TVar a |
||||||
|
, fsmHandlers :: M.Map a (FSMCallback m a) |
||||||
|
} |
||||||
|
|
||||||
|
runFsm :: (MonadIO m, FSMState a, Ord a) => FSM a m -> m () |
||||||
|
runFsm fsm = whileM $ do |
||||||
|
currentState <- liftIO . readTVarIO $ fsmCurrentState fsm |
||||||
|
case M.lookup currentState $ fsmHandlers fsm of |
||||||
|
Just (FSMCallback cb) -> do |
||||||
|
maybeNextState <- cb |
||||||
|
case maybeNextState of |
||||||
|
Just nextState -> do |
||||||
|
liftIO . atomically $ writeTVar (fsmCurrentState fsm) nextState |
||||||
|
pure True |
||||||
|
Nothing -> pure (not . isTerminalState $ currentState) |
||||||
|
Nothing -> pure False |
||||||
|
|
||||||
|
makeFsm :: (MonadIO m1, |
||||||
|
MonadIO m, |
||||||
|
FSMState a, |
||||||
|
Ord a) => a -> [(a, FSMCallback m a)] -> m1 (FSM a m) |
||||||
|
makeFsm initialState handlers = do |
||||||
|
currentState <- liftIO $ newTVarIO initialState |
||||||
|
pure $ FSM currentState (M.fromList handlers) |
||||||
@ -0,0 +1,47 @@ |
|||||||
|
|
||||||
|
module Test.FSM |
||||||
|
( |
||||||
|
unitTests |
||||||
|
) where |
||||||
|
|
||||||
|
import Control.Concurrent.STM.TVar (readTVarIO) |
||||||
|
import FSM |
||||||
|
import System.Timeout (timeout) |
||||||
|
import Test.Tasty |
||||||
|
import Test.Tasty.HUnit (testCase, (@?=)) |
||||||
|
|
||||||
|
unitTests :: TestTree |
||||||
|
unitTests = testGroup "FSM" |
||||||
|
[ testSimpleStateChange |
||||||
|
, testStateWithoutCallback ] |
||||||
|
|
||||||
|
data SimpleState = |
||||||
|
StateInitial |
||||||
|
| StateIntermediate |
||||||
|
| StateFinal |
||||||
|
deriving (Show, Eq, Ord) |
||||||
|
|
||||||
|
instance FSMState SimpleState where |
||||||
|
isTerminalState StateFinal = True |
||||||
|
isTerminalState _ = False |
||||||
|
|
||||||
|
testSimpleStateChange = testCase "Simple state change" $ do |
||||||
|
fsm <- makeFsm StateInitial [(StateInitial, callbackChangeState StateIntermediate), |
||||||
|
(StateIntermediate, callbackChangeState StateFinal), |
||||||
|
(StateFinal, doNothing)] |
||||||
|
timeout 100000 $ runFsm fsm |
||||||
|
endState <- readTVarIO (fsmCurrentState fsm) |
||||||
|
endState @?= StateFinal |
||||||
|
where |
||||||
|
callbackChangeState x = FSMCallback (pure $ Just x) |
||||||
|
doNothing = FSMCallback (pure Nothing) |
||||||
|
|
||||||
|
testStateWithoutCallback = testCase "Test without callback" $ do |
||||||
|
fsm <- makeFsm StateInitial [(StateInitial, callbackChangeState StateIntermediate), |
||||||
|
(StateFinal, doNothing)] |
||||||
|
timeout 100000 $ runFsm fsm |
||||||
|
endState <- readTVarIO (fsmCurrentState fsm) |
||||||
|
endState @?= StateIntermediate |
||||||
|
where |
||||||
|
callbackChangeState x = FSMCallback (pure $ Just x) |
||||||
|
doNothing = FSMCallback (pure Nothing) |
||||||
Loading…
Reference in new issue