-- | This module is adapted from https://github.com/arybczak/effectful/blob/master/effectful/examples/FileSystem.hs,
-- originally BSD3 license, authors Andrzej Rybczak et al.
module Filesystem where

import           Cleff
import           Cleff.Error
import           Cleff.State
import           Control.Monad.Extra    (maybeM)
import           Data.Map.Strict        (Map)
import qualified Data.Map.Strict        as M
import qualified System.IO              as IO
import           UnliftIO.Exception

-- * Effect

-- | An effect for reading and writing files.
data Filesystem :: Effect where
  ReadFile :: FilePath -> Filesystem m String
  WriteFile :: FilePath -> String -> Filesystem m ()

-- * Operations

makeEffect ''Filesystem

-- * Interpretations

-- | File system error.
newtype FsError = FsError String
  deriving stock (Show)
  deriving anyclass (Exception)

-- | Run the 'Filesystem' effect with actual file IO.
runFilesystemIO :: '[IOE, Error FsError] :>> es => Eff (Filesystem ': es) a -> Eff es a
runFilesystemIO = interpret \case
  ReadFile path           -> adapt $ IO.readFile path
  WriteFile path contents -> adapt $ IO.writeFile path contents
  where
    adapt m = liftIO m `catch` \(e :: IOException) -> throwError $ FsError $ show e

-- | Run the 'Filesystem' effect with a faked filesystem.
runFilesystemPure :: Error FsError :> es => Map FilePath String -> Eff (Filesystem ': es) a -> Eff es a
runFilesystemPure fs = fmap fst . runState fs . reinterpret \case
  ReadFile path -> maybeM (throwError $ FsError $ "File not found: " ++ show path) pure $ gets (M.lookup path)
  WriteFile path contents -> modify $ M.insert path contents

f :: Either FsError (Either FsError String)
f = runPure $ runError @FsError $ runFilesystemPure M.empty $ runError @FsError $ Filesystem.readFile "nonexistent"

-- >>> f
-- Left (FsError "File not found: \"nonexistent\"")
