eve 0.1.0 → 0.1.1
raw patch · 17 files changed
+321/−215 lines, 17 files
Files
- README.md +1/−1
- eve.cabal +4/−3
- src/Eve.hs +58/−10
- src/Eve/Internal/Actions.hs +52/−35
- src/Eve/Internal/AppState.hs +27/−0
- src/Eve/Internal/Async.hs +20/−12
- src/Eve/Internal/Extensions.hs +0/−57
- src/Eve/Internal/Listeners.hs +26/−22
- src/Eve/Internal/Run.hs +15/−5
- src/Eve/Internal/States.hs +57/−0
- src/Eve/Internal/Testing.hs +0/−15
- src/Eve/Testing.hs +13/−0
- test/Eve/Internal/ActionsSpec.hs +15/−13
- test/Eve/Internal/AsyncSpec.hs +2/−3
- test/Eve/Internal/ListenersSpec.hs +11/−12
- test/Eve/Internal/RunSpec.hs +4/−8
- test/Fixtures.hs +16/−19
README.md view
@@ -17,7 +17,7 @@ \^ That guide will bring you through the process of making your first app! If you have any issues (and I'm sure there'll be a few; it's a new project!)-please report them [here](https://github.com/ChrisPenner/rasa/issues).+please report them [here](https://github.com/ChrisPenner/eve/issues). Core Principles ---------------
eve.cabal view
@@ -1,5 +1,5 @@ name: eve-version: 0.1.0+version: 0.1.1 synopsis: An extensible event framework description: An extensible event-driven application framework in haskell for building embarassingly modular software. homepage: https://github.com/ChrisPenner/eve#readme@@ -16,11 +16,12 @@ library hs-source-dirs: src exposed-modules: Eve- , Eve.Internal.Testing+ , Eve.Testing , Eve.Internal.Actions+ , Eve.Internal.AppState , Eve.Internal.Async , Eve.Internal.Events- , Eve.Internal.Extensions+ , Eve.Internal.States , Eve.Internal.Listeners , Eve.Internal.Run build-depends: base >= 4.7 && < 5
src/Eve.hs view
@@ -1,10 +1,15 @@ module Eve ( eve+ , eveT , App , Action+ , AppT+ , ActionT , liftAction+ , AppState + -- * Events , dispatchEvent , dispatchEvent_ , dispatchEventAsync@@ -14,6 +19,15 @@ , addListener_ , removeListener + -- * Working with Async Events/Actions+ , asyncActionProvider+ , asyncEventProvider+ , Dispatcher++ , Listener+ , ListenerId++ -- * Built-in Event Listeners , onInit , afterInit , beforeEvent@@ -22,16 +36,49 @@ , afterEvent_ , onExit - , asyncActionProvider- , asyncEventProvider-- , Listener- , ListenerId-- , HasExts(..)- , Exts+ -- * Working with State+ -- | All application-provided states are stored in the same+ -- Map; keyed by their 'Data.Typeable.TypeRep'. This means that if more than one state+ -- uses the same type then they'll conflict and overwrite each-other (this is less of a+ -- problem than you're probably thinking). This is easily solved by simply+ -- using a newtype around any types you haven't defined yourself.+ -- For example if your application stores a counter as an Int, wrap it in your own custom+ -- @Counter@ newtype when storing it. If you wish to store multiple copies of a given state+ -- simply store them in a list or map, then store that container as your state.+ --+ -- Because states are stored by their 'Data.Typeable.TypeRep', they must define an+ -- instance of 'Data.Typeable.Typeable', luckily GHC can derive this for you with+ -- @deriving Typeable@.+ --+ -- It is also required for all states to define an instance of+ -- 'Data.Default.Default', this is because accessing an extension which has not+ -- yet been stored will result in the default value.+ --+ -- If there's no default value that makes sense for your type, you can define+ -- a default of 'Data.Maybe.Nothing' and pattern-match on its value when you+ -- access it.+ --+ -- Stored states are accessed by using the `stateLens` lens, this lens is polymorphic+ -- and can return ANY type. GHC infers the needed type and the lens will retrieve the+ -- state that you want from the store of states. It seems a bit complicated, but it all+ -- works fine in practice.+ --+ -- To avoid confusion it's best to rename a version of `stateLens` with a more restrictive+ -- type for each different state type that you store. This helps prevent strange errors and+ -- makes your code much easier to read. For example:+ --+ -- > data MyState = MyState String+ -- > myState :: HasStates s => Lens' s MyState+ -- > myState = stateLens+ -- >+ -- > myAction = do+ -- > MyState str <- use stateLens+ --+ -- If GHC has trouble inferring the type, rename it and restrict the type as above.+ , HasStates(..)+ , States , HasEvents- , ext+ , stateLens , runAction , exit@@ -41,4 +88,5 @@ import Eve.Internal.Actions import Eve.Internal.Listeners import Eve.Internal.Async-import Eve.Internal.Extensions+import Eve.Internal.AppState+import Eve.Internal.States
src/Eve/Internal/Actions.hs view
@@ -2,74 +2,77 @@ {-# language DeriveFunctor #-} {-# language FlexibleInstances #-} {-# language MultiParamTypeClasses #-}-{-# language TemplateHaskell #-} {-# language RankNTypes #-} {-# language TypeFamilies #-} {-# language UndecidableInstances #-} {-# language ScopedTypeVariables #-}+{-# language TemplateHaskell #-} module Eve.Internal.Actions- ( Action(..)- , ActionF(..)- , App+ ( ActionF(..)+ , ActionT(..)+ , AppT+ , runApp+ , evalApp , execApp - , AppState(..)- , asyncQueue- , liftAction , runAction , exit , isExiting+ , asyncQueue , Exiting(..) ) where -import Eve.Internal.Extensions-import Data.Default+import Eve.Internal.States import Control.Monad.State import Control.Monad.Trans.Free import Control.Lens+import Data.Default+import Data.Typeable import Pipes.Concurrent -type App a = Action AppState a-data AppState = AppState- { _baseExts :: Exts- , _asyncQueue :: Output (App ())- }+type AppT s m a = ActionT s s m a -newtype ActionF next =- LiftAction (StateT AppState IO next)+newtype ActionF base m next =+ LiftAction (StateT base m next) deriving (Functor, Applicative) -newtype Action zoomed a = Action- { getAction :: FreeT ActionF (StateT zoomed IO) a- } deriving (Functor, Applicative, Monad, MonadIO, MonadState zoomed, MonadFree ActionF)-makeLenses ''AppState+newtype ActionT base zoomed m a = ActionT+ { getAction :: FreeT (ActionF base m) (StateT zoomed m) a+ } deriving (Functor, Applicative, Monad, MonadIO, MonadState zoomed) -instance HasExts AppState where- exts = baseExts+instance Monad n => MonadFree (ActionF base n) (ActionT base zoomed n) where+ wrap (LiftAction act) = join . ActionT . liftF . LiftAction $ act -instance HasEvents AppState where+instance MonadTrans (ActionT base zoomed) where+ lift = ActionT . lift . lift -unLift :: FreeT ActionF (StateT AppState IO) a -> StateT AppState IO a+unLift :: Monad m => FreeT (ActionF base m) (StateT base m) a -> StateT base m a unLift m = do step <- runFreeT m case step of Pure a -> return a Free (LiftAction next) -> next >>= unLift -liftAction :: Action AppState a -> Action zoomed a+liftAction :: Monad m => AppT base m a -> ActionT base zoomed m a liftAction = liftF . LiftAction . unLift . getAction -execApp :: AppState -> Action AppState a -> IO a-execApp appState = flip evalStateT appState . unLift . getAction+runApp :: Monad m => base -> AppT base m a -> m (a, base)+runApp baseState = flip runStateT baseState . unLift . getAction -type instance Zoomed (Action s) = Zoomed (FreeT ActionF (StateT s IO))-instance Zoom (Action s) (Action t) s t where- zoom l (Action action) = Action $ zoom l action+evalApp :: Monad m => base -> AppT base m a -> m a+evalApp baseState = fmap fst . runApp baseState +execApp :: Monad m => base -> AppT base m a -> m base+execApp baseState = fmap snd . runApp baseState++type instance Zoomed (ActionT base zoomed m) = Zoomed (FreeT (ActionF base m) (StateT zoomed m))+instance Monad m => Zoom (ActionT base s m) (ActionT base t m) s t where+ zoom l (ActionT action) = ActionT $ zoom l action+ runAction :: Zoom m n s t => LensLike' (Zoomed m c) t s -> m c -> n c runAction = zoom @@ -80,10 +83,24 @@ instance Default Exiting where def = Exiting False -exit :: App ()-exit = ext .= Exiting True+exit :: (Monad m, HasStates s) => ActionT s zoomed m ()+exit = liftAction $ stateLens .= Exiting True -isExiting :: App Bool-isExiting = do- Exiting b <- use ext+isExiting :: (Monad m, HasStates s) => ActionT s zoomed m Bool+isExiting = liftAction $ do+ Exiting b <- use stateLens return b++newtype AsyncQueue base m = AsyncQueue+ { _asyncQueue' :: Maybe (Output (AppT base m ()))+ } deriving Typeable+makeLenses ''AsyncQueue++instance Show (AsyncQueue base m) where+ show _ = "Async Queue"++instance Default (AsyncQueue base m) where+ def = AsyncQueue Nothing++asyncQueue :: (HasStates s, Typeable m, Typeable base) => Lens' s (Maybe (Output (AppT base m ())))+asyncQueue = stateLens.asyncQueue'
+ src/Eve/Internal/AppState.hs view
@@ -0,0 +1,27 @@+{-# language TemplateHaskell #-}+module Eve.Internal.AppState+ ( AppState(..)+ , App+ , Action+ ) where++import Eve.Internal.Actions+import Eve.Internal.States+import Control.Lens+import Data.Default++data AppState = AppState+ { _baseStates :: States+ }+makeLenses ''AppState++instance Default AppState where+ def = AppState mempty++instance HasStates AppState where+ states = baseStates++instance HasEvents AppState where++type App a = AppT AppState IO a+type Action s a = ActionT AppState s IO a
src/Eve/Internal/Async.hs view
@@ -9,25 +9,33 @@ ) where import Eve.Internal.Actions+import Eve.Internal.States import Control.Monad import Control.Monad.State import Control.Lens+import Data.Typeable import Pipes import Pipes.Concurrent dispatchActionAsync- :: IO (App ()) -> App ()-dispatchActionAsync asyncAction = do- queue <- use asyncQueue- let effect = (liftIO asyncAction >>= yield) >-> toOutput queue- liftIO . void . forkIO $ runEffect effect >> performGC+ :: (MonadIO m, HasStates base, Typeable m, Typeable base) => IO (AppT base m ()) -> ActionT base zoomed m ()+dispatchActionAsync asyncAction = liftAction $ do+ mQueue <- use asyncQueue+ case mQueue of+ Nothing -> return ()+ Just queue -> do+ let effect = (liftIO asyncAction >>= yield) >-> toOutput queue+ liftIO . void . forkIO $ runEffect effect >> performGC -asyncActionProvider :: ((App () -> IO ()) -> IO ()) -> App ()-asyncActionProvider provider = do- queue <- use asyncQueue- let dispatcher action =- let effect = yield action >-> toOutput queue- in void . forkIO $ runEffect effect >> performGC- liftIO . void . forkIO $ provider dispatcher+asyncActionProvider :: (MonadIO m, HasStates base, Typeable m, Typeable base) => ((AppT base m () -> IO ()) -> IO ()) -> ActionT base zoomed m ()+asyncActionProvider provider = liftAction $ do+ mQueue <- use asyncQueue+ case mQueue of+ Nothing -> return ()+ Just queue -> do+ let dispatcher action =+ let effect = yield action >-> toOutput queue+ in void . forkIO $ runEffect effect >> performGC+ liftIO . void . forkIO $ provider dispatcher
− src/Eve/Internal/Extensions.hs
@@ -1,57 +0,0 @@-{-# LANGUAGE ExistentialQuantification #-}-{-# LANGUAGE ScopedTypeVariables #-}--module Eve.Internal.Extensions- ( Exts- , HasExts(..)- , HasEvents- , ext- ) where--import Control.Lens-import Data.Map-import Unsafe.Coerce-import Data.Maybe-import Data.Typeable-import Data.Default---- | A wrapper to allow storing differing extensions in the same place.-data Ext =- forall ext. (Typeable ext, Show ext) =>- Ext ext--instance Show Ext where- show (Ext a) = show a---- | A map of extension types to their current value.-type Exts = Map TypeRep Ext---- | Represents a state which can be extended.--- 'exts' is a 'Lens'' which points to the state's 'Exts'-class HasExts s where- exts :: Lens' s Exts---- | A typeclass to ensure people don't dispatch events to states which shouldn't--- accept them.------ To allow dispatching events in an action over your state simply define the--- empty instance:------ > instance HasEvents MyState where--- -- Don't need anything here.-class (Typeable s, HasExts s) =>- HasEvents s---- | A polymorphic lens which accesses extensions in the extension state.--- It returns the default value ('def') if a state has not yet been set.-ext- :: forall a e.- (Show a, Typeable a, Default a, HasExts e)- => Lens' e a-ext = lens getter setter- where- getter s =- fromMaybe def $ s ^. exts . at (typeRep (Proxy :: Proxy a)) . mapping coerce- setter s new =- set (exts . at (typeRep (Proxy :: Proxy a)) . mapping coerce) (Just new) s- coerce = iso (\(Ext x) -> unsafeCoerce x) Ext
src/Eve/Internal/Listeners.hs view
@@ -24,9 +24,10 @@ , Listener , ListenerId+ , Dispatcher ) where -import Eve.Internal.Extensions+import Eve.Internal.States import Eve.Internal.Async import Eve.Internal.Actions import Eve.Internal.Events@@ -46,28 +47,28 @@ -- Though arbitrary actions may be performed in the configuration block; -- it's recommended to embed such actions in the onInit event listener -- so that all event listeners are registered before any dispatches occur.-onInit :: App result -> App ()-onInit action = void $ addListener (const (void action) :: Init -> App ())+onInit :: forall base zoomed m result. (Monad m, HasEvents zoomed, Typeable m, Typeable base) => ActionT base zoomed m result -> ActionT base zoomed m ()+onInit action = void $ addListener (const (void action) :: Init -> ActionT base zoomed m ()) -afterInit :: App a -> App ()-afterInit action = void $ addListener (const (void action) :: AfterInit -> App ())+afterInit :: forall base zoomed m a. (Monad m, HasEvents zoomed, Typeable m, Typeable base) => ActionT base zoomed m a -> ActionT base zoomed m ()+afterInit action = void $ addListener (const (void action) :: AfterInit -> ActionT base zoomed m ()) -- | Registers an action to be performed BEFORE each event phase.-beforeEvent :: App a -> App ListenerId-beforeEvent action = addListener (const (void action) :: BeforeEvent -> App ())+beforeEvent :: forall base zoomed m a. (Monad m, HasEvents zoomed, Typeable m, Typeable base) => ActionT base zoomed m a -> ActionT base zoomed m ListenerId+beforeEvent action = addListener (const (void action) :: BeforeEvent -> ActionT base zoomed m ()) -beforeEvent_ :: App a -> App ()+beforeEvent_ :: (Monad m, HasEvents zoomed, Typeable m, Typeable base) => ActionT base zoomed m a -> ActionT base zoomed m () beforeEvent_ = void . beforeEvent -- | Registers an action to be performed BEFORE each event phase.-afterEvent :: App a -> App ListenerId-afterEvent action = addListener (const (void action) :: AfterEvent -> App ())+afterEvent :: forall base zoomed m a. (Monad m, HasEvents zoomed, Typeable m, Typeable base) => ActionT base zoomed m a -> ActionT base zoomed m ListenerId+afterEvent action = addListener (const (void action) :: AfterEvent -> ActionT base zoomed m ()) -afterEvent_ :: App a -> App ()+afterEvent_ :: (Monad m, HasEvents zoomed, Typeable m, Typeable base) => ActionT base zoomed m a -> ActionT base zoomed m () afterEvent_ = void . afterEvent -onExit :: App a -> App ()-onExit action = void $ addListener (const $ void action :: Exit -> App ())+onExit :: forall base zoomed m a. (HasEvents zoomed, Typeable m, Typeable base, Monad m) => ActionT base zoomed m a -> ActionT base zoomed m ()+onExit action = void $ addListener (const $ void action :: Exit -> ActionT base zoomed m ()) dispatchEvent :: forall result eventType m s.@@ -147,15 +148,18 @@ -- registered for the resulting event will still be run syncronously, only the -- code which generates the event is asynchronous. dispatchEventAsync- :: (Typeable event)- => IO event -> App ()+ :: (Typeable event+ ,MonadIO m+ ,Typeable m+ ,HasEvents base+ ) => IO event -> ActionT base zoomed m () dispatchEventAsync ioEvent = dispatchActionAsync $ dispatchEvent <$> ioEvent -- | A wrapper around event listeners so they can be stored in 'Listeners'. data Listener where Listener :: (MonadState s m, Typeable m, Typeable eventType, Typeable result,- Monoid result, HasExts s) =>+ Monoid result, HasStates s) => TypeRep -> ListenerId -> (eventType -> m result) -> Listener instance Show Listener where@@ -175,7 +179,7 @@ -- | A map of event types to a list of listeners for that event type Listeners = M.Map TypeRep [Listener] --- | Store the listeners in extensions+-- | Store the listeners in the state-map data LocalListeners = LocalListeners Int Listeners@@ -185,9 +189,9 @@ def = LocalListeners 0 M.empty localListeners- :: HasExts s+ :: HasStates s => Lens' s LocalListeners-localListeners = ext+localListeners = stateLens -- | This extracts all event listeners from a map of listeners which match the type of the provided event. matchingListeners@@ -212,7 +216,7 @@ type Dispatcher = forall event. Typeable event => event -> IO () --- | This allows long-running IO processes to provide Events to the App asyncronously.+-- | This allows long-running IO processes to provide Events to the ActionT base zoomed m asyncronously. -- -- Don't let the type signature confuse you; it's much simpler than it seems. --@@ -238,9 +242,9 @@ -- > myAction :: Action s () -- > myAction = onInit $ asyncEventProvider myTimer asyncEventProvider- :: (Dispatcher -> IO ()) -> App ()+ :: (HasEvents base, MonadIO m, Typeable m) => (Dispatcher -> IO ()) -> ActionT base zoomed m () asyncEventProvider asyncEventProv = asyncActionProvider $ eventsToActions asyncEventProv where- eventsToActions :: (Dispatcher -> IO ()) -> (App () -> IO ()) -> IO ()+ eventsToActions :: (Monad m, HasEvents base, Typeable m) => (Dispatcher -> IO ()) -> (AppT base m () -> IO ()) -> IO () eventsToActions aEventProv dispatcher = aEventProv (dispatcher . dispatchEvent)
src/Eve/Internal/Run.hs view
@@ -1,22 +1,32 @@ module Eve.Internal.Run ( eve+ , eveT ) where import Eve.Internal.Actions+import Eve.Internal.AppState import Eve.Internal.Listeners import Eve.Internal.Events+import Eve.Internal.States() +import Control.Monad import Control.Monad.State+import Control.Lens+import Data.Default import Data.Maybe+import Data.Typeable import Pipes import Pipes.Concurrent import qualified Pipes.Parse as PP -eve :: App () -> IO AppState-eve initialize = do- (output, input) <- spawn unbounded- execApp (AppState mempty output) $ do+eve :: App () -> IO ()+eve = void . eveT def++eveT :: (MonadIO m, Typeable m, HasEvents base) => base -> AppT base m () -> m base+eveT startState initialize = do+ (output, input) <- liftIO $ spawn unbounded+ execApp (startState & asyncQueue .~ Just output) $ do initialize dispatchEvent_ Init dispatchEvent_ AfterInit@@ -27,7 +37,7 @@ -- | This is the main event loop, it runs recursively forever until something -- sets the exit status. It runs the pre-event listeners, then checks if any -- async events have finished, then runs the post event listeners and repeats.-eventLoop :: Producer (App ()) IO () -> App ()+eventLoop :: (MonadIO m, HasEvents base, Typeable m) => Producer (AppT base m ()) IO () -> AppT base m () eventLoop producer = do dispatchEvent_ BeforeEvent (mAction, nextProducer) <- liftIO $ PP.runStateT PP.draw producer
+ src/Eve/Internal/States.hs view
@@ -0,0 +1,57 @@+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Eve.Internal.States+ ( States+ , HasStates(..)+ , HasEvents+ , stateLens+ ) where++import Control.Lens+import Data.Map+import Unsafe.Coerce+import Data.Maybe+import Data.Typeable+import Data.Default++-- | A wrapper to allow storing types of states in the same place.+data StateWrapper =+ forall s. (Typeable s, Show s) =>+ StateWrapper s++instance Show StateWrapper where+ show (StateWrapper s) = show s++-- | A map of state types to their current value.+type States = Map TypeRep StateWrapper++-- | Represents a state which can itself store more states.+class HasStates s where+-- | This lens points to a given state's 'States' map.+ states :: Lens' s States++-- | A typeclass to ensure people don't dispatch events to states which shouldn't+-- accept them.+--+-- To allow dispatching events in an action over your state simply define the+-- empty instance:+--+-- > instance HasEvents MyState where+-- -- Don't need anything here.+class (Typeable s, HasStates s) =>+ HasEvents s++-- | A polymorphic lens which accesses stored states.+-- It returns the default value ('def') if a state has not yet been set.+stateLens+ :: forall a e.+ (Show a, Typeable a, Default a, HasStates e)+ => Lens' e a+stateLens = lens getter setter+ where+ getter s =+ fromMaybe def $ s ^. states . at (typeRep (Proxy :: Proxy a)) . mapping coerce+ setter s new =+ set (states . at (typeRep (Proxy :: Proxy a)) . mapping coerce) (Just new) s+ coerce = iso (\(StateWrapper x) -> unsafeCoerce x) StateWrapper
− src/Eve/Internal/Testing.hs
@@ -1,15 +0,0 @@-module Eve.Internal.Testing- ( AppState(..)- , Exiting(..)- , noEventTest- ) where--import Eve-import Eve.Internal.Actions-import Eve.Internal.Events-import Pipes.Concurrent--noEventTest :: App a -> IO a-noEventTest test = do- (output, input) <- spawn unbounded- execApp (AppState mempty output) $ test
+ src/Eve/Testing.hs view
@@ -0,0 +1,13 @@+module Eve.Testing+ ( noIOTest+ ) where++import Eve.Internal.Actions+import Eve.Internal.Run+import Eve.Internal.AppState++import Control.Monad.Identity+import Data.Default++noIOTest :: AppT AppState Identity a -> (a, AppState)+noIOTest = runIdentity . runApp def
test/Eve/Internal/ActionsSpec.hs view
@@ -4,36 +4,38 @@ import Fixtures import Eve-import Eve.Internal.Testing import Eve.Internal.Actions import Control.Lens import Control.Monad.State -appendEx :: Action String ()+appendEx :: Monad m => ActionT AppState String m () appendEx = modify (++ "!!") +liftActionTest :: Monad m => ActionT AppState String m String+liftActionTest = do+ put "new"+ liftAction $ runAction stateLens appendEx+ get+ spec :: Spec spec = do describe "Exiting" $ do- exiting <- runIO . noEventTest $ exit >> isExiting it "Exits" $- exiting `shouldBe` True+ let (didExit, _) = noIOTest (exit >> isExiting)+ in didExit `shouldBe` True describe "runAction " $ do- runActionResult <- runIO . noEventTest $ runAction ext (put "new" >> appendEx) >> runAction ext get it "runs lifted actions to zoomed monad" $- runActionResult `shouldBe` "new!!"+ let (runActionResult, _) = noIOTest $ runAction stateLens (put "new" >> appendEx) >> runAction stateLens get+ in runActionResult `shouldBe` "new!!" - traversalResult <- runIO . noEventTest $ runAction ext (put $ Just "new") >> runAction (ext._Just) (appendEx >> get) it "runs over traversals" $- traversalResult `shouldBe` "new!!"+ let (traversalResult, _) = noIOTest $ runAction stateLens (put $ Just "new") >> runAction (stateLens._Just) (appendEx >> get)+ in traversalResult `shouldBe` "new!!" describe "liftAction" $ do- liftActionResult <- runIO . noEventTest . runAction ext $ do- put "new" - liftAction $ runAction ext appendEx- get it "runs lifted actions to zoomed monad" $- liftActionResult `shouldBe` "new!!"+ let (liftActionResult, _) = noIOTest (runAction stateLens liftActionTest :: AppT AppState Identity String)+ in liftActionResult `shouldBe` "new!!"
test/Eve/Internal/AsyncSpec.hs view
@@ -15,11 +15,10 @@ spec :: Spec spec = do describe "asyncActionProvider" $ do- asyncState <- runIO $ eve (asyncActionProvider (\d -> d (store .= "new" >> exit)))+ asyncState <- ioTest (asyncActionProvider (\d -> d (store .= "new" >> exit))) it "Eventually Runs Provided Actions" $ (asyncState ^. store) `shouldBe` "new" describe "dispatchActionAsync" $ do- asyncState <- runIO $ eve (asyncActionProvider (\d -> d (store .= "new" >> exit)))- asyncActionsResult <- runIO $ eve asyncActionsTest+ asyncActionsResult <- ioTest asyncActionsTest it "Eventually Runs Provided Actions" $ (asyncActionsResult ^. store) `shouldBe` "new"
test/Eve/Internal/ListenersSpec.hs view
@@ -9,13 +9,11 @@ import Control.Monad.State import Control.Lens -testAction :: State ExtState () -> ExtState-testAction = flip execState emptyExts--basicAction :: State ExtState ()+basicAction :: AppT AppState Identity String basicAction = do- addListener (const (store .= "new") :: CustomEvent -> State ExtState ())+ addListener (const (store .= "new") :: CustomEvent -> AppT AppState Identity ()) dispatchEvent_ CustomEvent+ use store delayedExit :: App () delayedExit = do@@ -23,11 +21,12 @@ dispatchEventAsync (return CustomEvent) store .= "new" -removeListenersTest :: State ExtState ()+removeListenersTest :: AppT AppState Identity String removeListenersTest = do- listId <- addListener (const (store .= "new") :: CustomEvent -> State ExtState ())+ listId <- addListener (const (store .= "new") :: CustomEvent -> AppT AppState Identity ()) removeListener listId dispatchEvent_ CustomEvent+ use store asyncEventsTest :: App () asyncEventsTest = do@@ -45,14 +44,14 @@ spec :: Spec spec = do describe "dispatchEvent/addListener" $- it "Triggers Listeners" $ (testAction basicAction ^. store) `shouldBe` "new"+ it "Triggers Listeners" $ fst (noIOTest basicAction) `shouldBe` "new" describe "dispatchEventAsync" $ do- delayedExitState <- runIO $ eve delayedExit+ delayedExitState <- ioTest delayedExit it "Triggers Listeners Eventually" $ (delayedExitState ^. store) `shouldBe` "new" describe "removeListener" $- it "Removes Listeners" $ (testAction removeListenersTest ^. store) `shouldBe` "default"+ it "Removes Listeners" $ fst (noIOTest removeListenersTest) `shouldBe` "default" describe "asyncEventProvider" $ do- asyncEventsResult <- runIO $ eve asyncEventsTest+ asyncEventsResult <- ioTest asyncEventsTest it "Provides events eventually" $ (asyncEventsResult ^. store) `shouldBe` "new"- multiAsyncEventsResult <- runIO $ eve multiAsyncEventsTest+ multiAsyncEventsResult <- ioTest multiAsyncEventsTest it "Can provide different event types" $ (multiAsyncEventsResult ^. store) `shouldBe` "new"
test/Eve/Internal/RunSpec.hs view
@@ -4,22 +4,18 @@ import Test.Hspec import Fixtures import Eve-import Eve.Internal.Testing+import Eve.Internal.Actions import Control.Lens exiter :: (App () -> IO ()) -> IO () exiter dispatch = dispatch exit -runWithInit :: App () -> SpecM a AppState-runWithInit next = runIO $ eve (next >> asyncActionProvider exiter)- spec :: Spec spec = do describe "Running App" $ do- initializeState <- runWithInit (store .= "new") it "Initializes" $- (initializeState ^. store) `shouldBe` "new"+ fst (noIOTest (store .= "new" >> use store)) `shouldBe` "new" - asyncState <- runIO $ eve (asyncActionProvider (\d -> d (store .= "new" >> exit)))+ asyncResult <- ioTest (asyncActionProvider (\d -> d (store .= "new" >> exit))) it "Collects Async Actions" $- (asyncState ^. store) `shouldBe` "new"+ (asyncResult ^. store) `shouldBe` "new"
test/Fixtures.hs view
@@ -1,39 +1,36 @@ {-# language TemplateHaskell #-} module Fixtures- ( ExtState(..)- , store+ ( store , CustomEvent(..)- , emptyExts+ , noIOTest+ , ioTest ) where -import Eve.Internal.Extensions+import Eve.Testing+import Eve.Internal.States import Eve.Internal.Listeners+import Eve.Internal.Actions+import Eve.Internal.AppState+import Eve.Internal.Run import Control.Lens import Data.Default -data ExtState = ExtState- { _testExts :: Exts- }--makeLenses ''ExtState--instance HasExts ExtState where- exts = testExts--instance HasEvents ExtState--emptyExts = ExtState mempty+import Test.Hspec.Core.Spec (SpecM)+import Test.Hspec -data Store = Store +data Store = Store {_payload :: String } deriving (Show, Eq) makeLenses ''Store -store :: HasExts s => Lens' s String-store = ext.payload+store :: HasStates s => Lens' s String+store = stateLens.payload instance Default Store where def = Store "default" data CustomEvent = CustomEvent++ioTest :: App () -> SpecM m AppState+ioTest = runIO . eveT def