conduit-vfs-0.1.0.0: src/Data/Conduit/VFS/Pure.hs
{-|
Description: VFS persisted purely through 'StateT'
This is intended primarily to be a drop-in testing mock for VFS implementations, but you may also find it useful within your program if you want to
work with some hierarchically-organized data.
Individual read and write operations are atomic.
The 'FilePath' values used in this VFS are split using 'splitPath' and joined using '</>', but are otherwise used directly: there is no concept of
paths being "relative" or "absolute" for this VFS. It is also possible for a file and a directory to have the same name, since directories names
are appended with @/@, as per 'splitPath'. (This implementation detail is up for debate and may be changed in a future major release: please file
an issue if you want to have a discussion around it.)
-}
module Data.Conduit.VFS.Pure
( PureVFS
, runPureVFS
, runPureVFS'
, runPureVFS_
) where
import Control.Monad.Trans.State.Lazy
import ClassyPrelude hiding (ByteString, handle, hash)
import Data.Conduit.VFS.Import
import qualified Data.HashMap.Lazy as HashMap
import Data.HashMap.Lazy (HashMap)
import Control.Monad.Extra (whenJust, maybeM)
import Control.Monad.Fail (MonadFail)
import System.FilePath (splitPath, (</>))
import qualified Data.Text as Text
-- | The possible kinds of nodes
data PNode
= PNodeDir (HashMap Text PNode) -- ^ A directory
| PNodeFile ByteString -- ^ A file
-- | The basic implementation of the VFS.
newtype PureVFS m a = PureVFS
{ unPVFS :: StateT PNode m a }
deriving (Applicative, Functor, MonadFail, Monad, MonadTrans)
-- | Given a 'PureVFS', run it in the local monad and return both the monadic return value and the root node of the VFS.
runPureVFS :: PureVFS m a -> m (a, PNode)
runPureVFS pvfs = runStateT (unPVFS pvfs) (PNodeDir mempty)
{-# INLINE runPureVFS #-}
-- | Given a 'PureVFS', run it in the local monad and return the root node of the VFS.
runPureVFS' :: (Monad m) => PureVFS m a -> m PNode
runPureVFS' pvfs = execStateT (unPVFS pvfs) (PNodeDir mempty)
{-# INLINE runPureVFS' #-}
-- | Given a 'PureVFS', run it in the local monad and disregard any results.
runPureVFS_ :: (Monad m) => PureVFS m a -> m ()
runPureVFS_ = void . runPureVFS
{-# INLINE runPureVFS_ #-}
-- | Retrieves the root of the 'PureVFS'.
getRoot :: (Monad m) => PureVFS m PNode
getRoot = PureVFS get
{-# INLINE getRoot #-}
-- | Sets the root of the 'PureVFS' and returns the new root
setRoot :: (Monad m) => PNode -> PureVFS m PNode
setRoot newRoot = setRoot_ newRoot >> return newRoot
{-# INLINE setRoot #-}
-- | Sets the root of the 'PureVFS' without returning it
setRoot_ :: (Monad m) => PNode -> PureVFS m ()
setRoot_ = PureVFS . put
{-# INLINE setRoot_ #-}
-- | Changes the root without providing any return value
modifyRoot_ :: (Monad m) => (PNode -> PureVFS m PNode) -> PureVFS m ()
modifyRoot_ f = getRoot >>= f >>= PureVFS . put -- Oh, right: StateT doesn't have concurrency problems. Sweet!
{-# INLINE modifyRoot_ #-}
-- | Changes the root and provides the updated value.
modifyRoot :: (Monad m) => (PNode -> PureVFS m PNode) -> PureVFS m PNode
modifyRoot f = modifyRoot_ f >> getRoot
{-# INLINE modifyRoot #-}
getNodeC :: (Monad m) => FilePath -> ConduitT i o (PureVFS m) (Maybe PNode)
getNodeC = lift . getNode
{-# INLINE getNodeC #-}
getNode :: (Monad m) => FilePath -> PureVFS m (Maybe PNode)
getNode filepath = loop (splitPath filepath) <$> getRoot
where
loop [] _ = Nothing
loop _ (PNodeFile _) = Nothing
loop [filename] (PNodeDir hash) = HashMap.lookup (Text.pack filename) hash
loop (dirname:rest) (PNodeDir hash) = HashMap.lookup (Text.pack dirname) hash >>= loop rest
{-# INLINE getNode #-}
modifyNodeC_ :: (Monad m) => FilePath -> (Maybe PNode -> PureVFS m (Maybe PNode)) -> ConduitT i o (PureVFS m) ()
modifyNodeC_ filepath f = lift $ modifyNode_ filepath f
{-# INLINE modifyNodeC_ #-}
modifyNode_ :: (Monad m) => FilePath -> (Maybe PNode -> PureVFS m (Maybe PNode)) -> PureVFS m ()
modifyNode_ filepath f = modifyRoot_ $ loop (splitPath filepath)
where
loop [] node = return node
loop _ file@(PNodeFile _) = return file
loop (nodename:rest) dir@(PNodeDir hash) = case HashMap.lookup (Text.pack nodename) hash of
Nothing -> case rest of
[] ->
maybeM
(return dir)
(\result -> return . PNodeDir $ HashMap.insert (Text.pack nodename) result hash)
(f (Just dir))
(restHead:restTail) ->
loop restTail (PNodeDir mempty) >>= \result ->
return . PNodeDir $ HashMap.insert (Text.pack nodename) (PNodeDir $ HashMap.singleton (Text.pack restHead) result) hash
(Just node) -> loop rest node >>= \result -> return $ PNodeDir $ HashMap.insert (Text.pack nodename) result hash
{-# INLINEABLE modifyNode_ #-}
modifyNodeC :: (Monad m) => FilePath -> (Maybe PNode -> PureVFS m (Maybe PNode)) -> ConduitT i o (PureVFS m) (Maybe PNode)
modifyNodeC filepath f = lift $ modifyNode filepath f
{-# INLINE modifyNodeC #-}
-- | Updates the node at the given filepath and then returns the updated node.
modifyNode :: (Monad m) => FilePath -> (Maybe PNode -> PureVFS m (Maybe PNode)) -> PureVFS m (Maybe PNode)
modifyNode filepath f = modifyNode_ filepath f >> getNode filepath
{-# INLINE modifyNode #-}
instance (Monad m) => ReadVFSC (PureVFS m) where
vfsTypeC = awaitForever $ \filepath -> do
maybeNode <- getNodeC filepath
yield (filepath, toType <$> maybeNode)
where
toType (PNodeDir _) = VDirectory
toType (PNodeFile _) = VFile
{-# INLINE vfsTypeC #-}
vfsContentsC = awaitForever $ \filepath -> do
maybeResult <- getNodeC filepath
whenJust maybeResult $ \case
(PNodeFile bytes) -> yield (filepath, bytes)
(PNodeDir _) -> return ()
{-# INLINE vfsContentsC #-}
vfsChildrenC = awaitForever $ \filepath -> do
maybeNode <- getNodeC filepath
case maybeNode of
Nothing -> yield filepath
(Just (PNodeFile _)) -> yield filepath
(Just (PNodeDir hash)) -> yieldMany $ (filepath </>) . Text.unpack <$> HashMap.keys hash
{-# INLINE vfsChildrenC #-}
-- | A class denoting that the type is usable as VFS conduits for writing.
instance (Monad m) => WriteVFSC (PureVFS m) where
vfsWriteSink = awaitForever $ \(filepath, bs) -> modifyNodeC_ filepath (const . return . Just $ PNodeFile bs)
{-# INLINE vfsWriteSink #-}
vfsRemoveSink = awaitForever $ flip modifyNodeC_ (const $ return Nothing)
{-# INLINE vfsRemoveSink #-}
-- | A class denoting that the type is usable as VFS conduits for reading and writing.
instance (Monad m) => VFSC (PureVFS m)