{-| Module : Mellon.Device Description : An interface for physical access devices Copyright : (c) 2018, Quixoftic, LLC License : BSD3 Maintainer : Drew Hess <dhess-src@quixoftic.com> Stability : experimental Portability : non-portable This module provides both a parameterized type for adapting a device-specific interface to the generic interface expected by a 'Mellon.Controller.Controller', and a "mock lock" device implementation, which is useful for testing. -} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE Safe #-} module Mellon.Device ( -- * The mellon-core device type Device(..) -- * A mock lock implementation -- -- | The mock lock type provided here logs lock / unlock -- events along with a timestamp. It is useful for testing -- but doesn't have any facility to control an actual -- physical access device. , MockLock , mockLock , MockLockEvent(..) , lockMockLock , unlockMockLock , events , mockLockDevice ) where import Protolude import Control.Concurrent (MVar, newMVar, putMVar, readMVar, takeMVar) import Control.Monad.IO.Class (MonadIO, liftIO) import Data.Data import Data.Time (UTCTime, getCurrentTime) -- | A parametric device type which provides two "methods," one to -- lock the device, and the other to unlock it. -- -- The parameter @d@ is the concrete device type and is used during -- construction to create the two methods by binding them to actions -- on the specific device. -- -- For example, the implementation of the 'mockLockDevice' function, -- which wraps a 'MockLock' in a 'Device' @d@, looks like this: -- -- > mockLockDevice :: MockLock -> Device MockLock -- > mockLockDevice l = -- > Device (liftIO $ lockMockLock l) -- > (liftIO $ unlockMockLock l) -- -- A program can construct such a device and use it like so: -- -- >>> ml <- mockLock -- >>> let mld = mockLockDevice ml -- >>> events ml -- [] -- >>> lockDevice mld -- >>> events ml -- [LockEvent ... UTC] -- >>> unlockDevice mld -- >>> events ml -- [LockEvent ... UTC,UnlockEvent ... UTC] data Device d = Device {lockDevice :: IO () ,unlockDevice :: IO ()} -- | Events logged by 'MockLock' are of this type. data MockLockEvent = LockEvent !UTCTime | UnlockEvent !UTCTime deriving (Eq,Show,Read,Generic,Data,Typeable) -- | A mock lock device that logs lock / unlock events. -- -- No constructor is exported. Use 'mockLock' to create a new -- instance and 'events' to extract the log. data MockLock = MockLock !(MVar [MockLockEvent]) deriving (Eq) -- | Construct a new mock lock with an empty event log. mockLock :: (MonadIO m) => m MockLock mockLock = liftIO $ MockLock <$> newMVar [] -- | Extract the current log of events from the mock lock. events :: (MonadIO m) => MockLock -> m [MockLockEvent] events (MockLock m) = liftIO $ readMVar m data MLE = MLL | MLU deriving (Eq) -- | Lock the mock lock. lockMockLock :: (MonadIO m) => MockLock -> m () lockMockLock = updateMockLock MLL -- | Unlock the mock lock. unlockMockLock :: (MonadIO m) => MockLock -> m () unlockMockLock = updateMockLock MLU -- | Wrap a 'MockLock' value with a 'Device' value, for use with a -- @mellon-core@ controller. mockLockDevice :: MockLock -> Device MockLock mockLockDevice l = Device (liftIO $ lockMockLock l) (liftIO $ unlockMockLock l) -- | Helpers updateMockLock :: (MonadIO m) => MLE -> MockLock -> m () updateMockLock mle (MockLock m) = liftIO $ do now <- getCurrentTime ev <- takeMVar m putMVar m (mappend ev [event now]) where event :: UTCTime -> MockLockEvent event = case mle of MLL -> LockEvent MLU -> UnlockEvent