packages feed

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)