axel-0.0.13: test/Axel/Test/Eff/FileSystemMock.hs
{-# LANGUAGE TemplateHaskell #-}
module Axel.Test.Eff.FileSystemMock where
import Axel.Prelude
import Axel.Eff.FileSystem as Effs
import Axel.Utils.FilePath
import Control.Lens hiding (children)
import Data.List.Split
import Data.Maybe
import qualified Data.Text as T
import Effectful ((:>))
import qualified Effectful as Eff
import qualified Effectful.Dispatch.Dynamic as Eff
import qualified Effectful.Error.Static as Eff
import qualified Effectful.State.Static.Local as Eff
import TestUtils
data FSNode
= Directory FilePath [FSNode]
| File FilePath Text
deriving (Eq, Show)
fsPath :: Lens' FSNode FilePath
fsPath =
lens
(\case
Directory path _ -> path
File path _ -> path)
(\node newPath ->
case node of
Directory _ children -> Directory newPath children
File _ contents -> File newPath contents)
data FileSystemState =
FileSystemState
{ _fsCurrentDirectory :: FilePath
, _fsRoot :: FSNode
, _fsTempCounter :: Int
}
deriving (Eq, Show)
makeFieldsNoPrefix ''FileSystemState
mkFileSystemState :: [FSNode] -> FileSystemState
mkFileSystemState rootContents =
FileSystemState
{ _fsCurrentDirectory = FilePath "/"
, _fsRoot = Directory (FilePath "/") rootContents
, _fsTempCounter = 0
}
lookupNode :: [FilePath] -> FSNode -> Maybe FSNode
lookupNode _ (File _ _) = Nothing
lookupNode [] _ = Nothing
lookupNode [FilePath "."] rootNode = pure rootNode
lookupNode [segment] (Directory _ rootChildren) =
let matchingChildren =
filter (\child -> child ^. fsPath == segment) rootChildren
in case matchingChildren of
[child] -> pure child
_ -> Nothing
lookupNode (segment:xs) rootNode@(Directory _ _) = do
child@(Directory _ _) <- lookupNode [segment] rootNode
lookupNode xs child
deleteNode :: [FilePath] -> FSNode -> Maybe FSNode
deleteNode _ (File _ _) = Nothing
deleteNode [] _ = Nothing
deleteNode [segment] rootNode@(Directory rootPath rootChildren) = do
child <- lookupNode [segment] rootNode
let rootChildren' = filter (/= child) rootChildren
pure $ Directory rootPath rootChildren'
deleteNode (segment:xs) rootNode@(Directory rootPath rootChildren) = do
needle <- lookupNode [segment] rootNode
rootChildren' <-
mapM
(\child ->
case child of
File _ _ -> pure child
_ ->
if child == needle
then deleteNode xs child
else pure child)
rootChildren
pure $ Directory rootPath rootChildren'
insertNode :: [FilePath] -> FSNode -> FSNode -> Maybe FSNode
insertNode _ _ (File _ _) = Nothing
insertNode [] _ _ = Nothing
insertNode [segment] newNode (Directory rootPath rootChildren) =
if newNode ^. fsPath == segment
then let rootChildren' =
filter
(\child -> child ^. fsPath /= newNode ^. fsPath)
rootChildren
in pure $ Directory rootPath (newNode : rootChildren')
else Nothing
insertNode (segment:xs) newNode rootNode@(Directory rootPath rootChildren) = do
needle <- lookupNode [segment] rootNode
rootChildren' <-
mapM
(\child ->
case child of
File _ _ -> pure child
_ ->
if child == needle
then insertNode xs newNode child
else pure child)
rootChildren
pure $ Directory rootPath rootChildren'
type instance Index FSNode = FilePath
type instance IxValue FSNode = FSNode
instance Ixed FSNode where
ix :: FilePath -> Traversal' FSNode FSNode
ix path f root =
case lookupNode pathSegments root of
Just node ->
f node <&> \newNode -> fromJust $ insertNode pathSegments newNode root
Nothing -> pure root
where
pathSegments :: [FilePath]
pathSegments = splitDirectories path
instance At FSNode where
at :: FilePath -> Lens' FSNode (Maybe FSNode)
at path =
let setter :: FSNode -> Maybe FSNode -> FSNode
setter root =
fromMaybe root . \case
Just newNode -> insertNode pathSegments newNode root
Nothing -> deleteNode pathSegments root
in lens (lookupNode pathSegments) setter
where
pathSegments :: [FilePath]
pathSegments = splitDirectories path
absifyPath :: FilePath -> FilePath -> FilePath
absifyPath relativePath currentDirectory =
let absolutePath =
case T.unpack $ op FilePath relativePath of
'/':_ -> relativePath
_ -> FilePath "/" </> currentDirectory </> relativePath
normalizedSegments =
concatMap
(\case
[_, FilePath ".."] -> []
[FilePath "."] -> []
xs -> xs) $
chunksOf 2 $
dropLeadingSlash $
splitDirectories absolutePath
in case joinPath normalizedSegments of
FilePath "" -> FilePath "/"
path -> path
where
dropLeadingSlash (FilePath "/":xs) = xs
dropLeadingSlash x = x
absify ::
(Eff.State FileSystemState :> effs) => FilePath -> Eff.Eff effs FilePath
absify relativePath =
absifyPath relativePath <$> Eff.gets (^. fsCurrentDirectory)
runFileSystem ::
forall effs a. (Eff.Error Text :> effs)
=> FileSystemState
-> Eff.Eff (Effs.FileSystem ': effs) a
-> Eff.Eff effs (a, FileSystemState)
runFileSystem origState = Eff.reinterpret (Eff.runState origState) (const go)
where
go :: FileSystem m a' -> Eff.Eff (Eff.State FileSystemState ': effs) a'
go (AppendFile relativePath addedContents) = do
oldContents <- go $ ReadFile relativePath
let newContents = oldContents <> addedContents
go $ WriteFile relativePath newContents
go (CopyFile src dest) = do
contents <- go $ ReadFile src
go $ WriteFile dest contents
go (CreateDirectoryIfMissing createParents relativePath) = do
path <- absify relativePath
parentNode <- Eff.gets (^. fsRoot . at (takeDirectory path))
case parentNode of
Just _ ->
Eff.modify $ fsRoot . at path ?~ Directory (takeFileName path) []
Nothing ->
if createParents
then Eff.modify $ fsRoot %~ \origRoot ->
fst
(foldl
(\(root, segments) pathSegment ->
let newSegments = segments <> [pathSegment]
newRoot =
case root ^. at (joinPath newSegments) of
Just _ -> root
Nothing ->
root & at (joinPath newSegments) ?~
Directory pathSegment []
in (newRoot, newSegments))
(origRoot, [])
(splitDirectories path))
else throwInterpretError
@FileSystemState
"createDirectoryIfMissing"
("Missing parents for directory: " <> op FilePath path)
go (DoesDirectoryExist relativePath) = do
path <- absify relativePath
directoryNode <- Eff.gets (^. fsRoot . at path)
pure $
case directoryNode of
Just (Directory _ _) -> True
_ -> False
go GetCurrentDirectory = Eff.gets (^. fsCurrentDirectory)
go (GetDirectoryContents relativePath) = do
path <- absify relativePath
directoryNode <- Eff.gets (^. fsRoot . at path)
case directoryNode of
Just (Directory _ children) -> pure $ map (^. fsPath) children
_ ->
throwInterpretError
@FileSystemState
"getDirectoryContents"
("No such directory: " <> op FilePath path)
go GetTemporaryDirectory = do
tempCounter <- Eff.gets (^. fsTempCounter)
Eff.modify $ fsTempCounter %~ (+ 1)
let tempDirName = FilePath "/tmp" </> FilePath (showText tempCounter)
go $ CreateDirectoryIfMissing True tempDirName
pure tempDirName
go (ReadFile relativePath) = do
path <- absify relativePath
fileNode <- Eff.gets (^. fsRoot . at path)
case fileNode of
Just (File _ contents) -> pure contents
_ ->
throwInterpretError
@FileSystemState
"readFile"
("No such file: " <> op FilePath path)
go (RemoveFile relativePath) = do
path <- absify relativePath
Eff.gets (deleteNode (splitDirectories path) . (^. fsRoot)) >>= \case
Just newRoot -> Eff.modify $ fsRoot .~ newRoot
Nothing ->
throwInterpretError
@FileSystemState
"removeFile"
("No such file: " <> op FilePath path)
go (SetCurrentDirectory relativePath) = do
path <- absify relativePath
Eff.modify $ fsCurrentDirectory .~ path
go (WriteFile relativePath contents) = do
path <- absify relativePath
Eff.modify $ fsRoot . at path ?~ File (takeFileName path) contents