packages feed

hs-bindgen-1.0.0.0: src-internal/HsBindgen/ArtefactM.hs

module HsBindgen.ArtefactM (
    -- * Policies
    FilePolicy(..)
  , DirPolicy(..)
  , checkPolicy
    -- * File description
  , FileDescription(..)
  , FileLocation(..)
  , fileLocationToPath
  , RelativeToOutputDir(..)
  , FileContent(..)
    -- * ArtefactM monad
  , ArtefactM -- opaque
  , runArtefactM
  , askConfig
  , runCached
  , emitTrace
    -- ** Actions
  , delay
  , DelayedIO(..)
  , executeDelayedIOActions
    -- * Errors
  , DelayedIOError(..)
    -- * Traces
  , DelayedIOMsg(..)
  ) where

import Control.Monad.Except (ExceptT, MonadError (..))
import Control.Monad.Reader (MonadReader (..), ReaderT (..))
import Control.Monad.State (StateT (..), modify)
import Data.ByteString (ByteString)
import Data.ByteString qualified as BSS
import System.Directory qualified as Dir
import System.FilePath (takeDirectory, (</>))
import Text.SimplePrettyPrint ((<+>))
import Text.SimplePrettyPrint qualified as PP

import HsBindgen.Cache
import HsBindgen.Config.Internal
import HsBindgen.Imports
import HsBindgen.Util.Tracer

{-------------------------------------------------------------------------------
  Policies
-------------------------------------------------------------------------------}

data FilePolicy
  = AllowFileOverwrite
  | DoNotOverwriteFiles
  deriving (Show, Eq)

data DirPolicy
  = CreateOutputDirs
  | DoNotCreateOutputDirs
  deriving (Show, Eq)

checkPolicy :: DelayedIO -> ExceptT DelayedIOError IO ()
checkPolicy = \case
  WriteToStdOut{} -> pure ()
  WriteToFile fd -> case fd.location of
    UserSpecified path -> do
      let baseDir = takeDirectory path
      dirExists  <- liftIO $ Dir.doesDirectoryExist baseDir
      fileExists <- liftIO $ Dir.doesFileExist path
      unless (dirExists || fd.dirPolicy == CreateOutputDirs) $
        throwError $ DirectoryDoesNotExist baseDir
      when (fileExists && fd.filePolicy == DoNotOverwriteFiles) $
        throwError $ FileAlreadyExists path
    RelativeFileLocation relative -> do
      let path = relative.outputDir </> relative.localPath
      dirExists  <- liftIO $ Dir.doesDirectoryExist relative.outputDir
      fileExists <- liftIO $ Dir.doesFileExist path
      unless (dirExists || fd.dirPolicy == CreateOutputDirs) $
        throwError $ DirectoryDoesNotExist relative.outputDir
      when (fileExists && fd.filePolicy == DoNotOverwriteFiles) $
        throwError $ FileAlreadyExists path

{-------------------------------------------------------------------------------
  File description
-------------------------------------------------------------------------------}

data FileDescription = FileDescription {
      description :: String
    , location    :: FileLocation
    , filePolicy  :: FilePolicy
    , dirPolicy   :: DirPolicy
    , content     :: FileContent
    }

data FileLocation =
      UserSpecified FilePath
    | RelativeFileLocation RelativeToOutputDir
  deriving stock (Show, Generic)

data RelativeToOutputDir = RelativeToOutputDir {
      outputDir :: FilePath
    , localPath :: FilePath
    }
  deriving stock (Show, Generic)

fileLocationToPath :: FileLocation -> FilePath
fileLocationToPath = \case
    UserSpecified p -> p
    RelativeFileLocation relative -> relative.outputDir </> relative.localPath

-- | Content to be written to a file
--
data FileContent =
    StringContent     String
  | ByteStringContent ByteString
  deriving Show

{-------------------------------------------------------------------------------
  ArtefactM monad
-------------------------------------------------------------------------------}

newtype ArtefactM a =
  WrapArtefactM (StateT [DelayedIO] (ReaderT BindgenConfig IO) a)
  deriving newtype (
      Functor
    , Applicative
    , Monad
    , MonadReader BindgenConfig
    )

runArtefactM :: ArtefactM a -> BindgenConfig -> IO (a, [DelayedIO])
runArtefactM (WrapArtefactM ma) = runReaderT (runStateT ma [])

askConfig :: ArtefactM BindgenConfig
askConfig = ask

-- | Private (i.e., /not public/) API :-).
unsafeIO :: IO a -> ArtefactM a
unsafeIO = WrapArtefactM . liftIO

-- | Emit a trace while running artefacts.
emitTrace :: Tracer a -> a -> ArtefactM ()
emitTrace t = unsafeIO . traceWith t . withCallStack

runCached :: Cached a -> ArtefactM a
runCached = unsafeIO . getCached

{-------------------------------------------------------------------------------
  Actions
-------------------------------------------------------------------------------}

-- | Register a delayed IO action. The action will only be performed if the
--   artefacts are obtained without Error traces, and if the output policies
--   are met.
delay :: DelayedIO -> ArtefactM ()
delay a = WrapArtefactM $ modify (a :)

-- | Delayed IO action
data DelayedIO =
      WriteToStdOut  FileContent
    | WriteToFile    FileDescription

executeDelayedIOActions :: Tracer DelayedIOMsg -> [DelayedIO] -> IO ()
executeDelayedIOActions tracer as =
  forM_ as $ \case
    WriteToStdOut x -> case x of
      StringContent     s  -> putStrLn s
      ByteStringContent bs -> BSS.putStr bs
    WriteToFile  fd -> do
      let path = fileLocationToPath fd.location
      traceWith tracer $ withCallStack $ DelayedIOWriteToFile path fd.description
      -- Creating the directory is justified by checking the policy first.
      Dir.createDirectoryIfMissing True (takeDirectory path)
      case fd.content of
        StringContent     s  -> writeFile path s
        ByteStringContent bs -> BSS.writeFile path bs

{-------------------------------------------------------------------------------
  Errors
-------------------------------------------------------------------------------}

data DelayedIOError =
      DirectoryDoesNotExist FilePath
    | FileAlreadyExists     FilePath
  deriving Show

instance PrettyForTrace DelayedIOError where
  prettyForTrace = \case
    DirectoryDoesNotExist fp -> PP.vsep [
        "Output directory does not exist:" <+> PP.string fp
      , "Use --create-output-dirs to create it automatically, or create the directory manually."
      ]
    FileAlreadyExists fp -> PP.vsep [
        "Output file already exists:" <+> PP.string fp
      , "Use --overwrite-files to allow overwriting existing files, or delete the file manually."
      ]

{-------------------------------------------------------------------------------
  Traces
-------------------------------------------------------------------------------}

data DelayedIOMsg =
      DelayedIOWriteToFile FilePath String
  deriving stock (Show, Generic)

instance PrettyForTrace DelayedIOMsg where
  prettyForTrace = \case
    DelayedIOWriteToFile path what ->
      "Writing" <+> PP.show what <+> "to file" <+> PP.show path

instance IsTrace SafeLevel DelayedIOMsg where
  getDefaultLogLevel = \case
    DelayedIOWriteToFile{} -> SafeInfo
  getSource = const HsBindgen
  getTraceId = \case
    DelayedIOWriteToFile{} -> "delayedio-write-file"