dojang-0.1.0: src/Dojang/App.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeSynonymInstances #-}
-- | The application monad for Dojang.
module Dojang.App
( App
, AppEnv (..)
, Loc
, LogLevel
, LogSource
, LogStr
, currentEnvironment'
, doesManifestExist
, ensureContext
, ensureRepository
, loadManifest
, loadRepository
, lookupEnv'
, runAppWithLogging
, runAppWithStderrLogging
, runAppWithoutLogging
, saveManifest
) where
import Control.Monad (forM_)
import Control.Monad.IO.Class (MonadIO (liftIO))
import Data.List.NonEmpty (toList)
import Data.String (IsString (fromString))
import Dojang.Types.FilePathExpression (EnvironmentVariable)
import System.Environment (lookupEnv)
import System.Exit (exitWith)
import System.IO (stderr)
import System.IO.Error (isDoesNotExistError)
import Prelude hiding (readFile, writeFile)
import Control.Monad.Except (MonadError (..), tryError)
import Control.Monad.Logger
( Loc
, LogLevel
, LogSource
, LogStr
, LoggingT (runLoggingT)
, MonadLogger
, logDebug
, logDebugSH
, logError
, logInfo
, logWarn
, runStderrLoggingT
)
import Control.Monad.Reader (MonadReader, ReaderT, asks, runReaderT)
import Control.Monad.Trans (MonadTrans (lift))
import Data.Text (concat, pack, unlines, unpack)
import System.OsPath (normalise, (</>))
import System.OsPath.Types (OsPath)
import System.OsString (OsString)
import TextShow (FromStringShow (FromStringShow), TextShow (showt))
import Dojang.Commands
( Admonition (..)
, codeStyleFor
, die'
, dieWithErrors
, printStderr'
)
import Dojang.ExitCodes
( envFileReadError
, manifestReadError
, manifestUninitialized
, noEnvFile
)
import Dojang.MonadFileSystem (MonadFileSystem (..))
import Dojang.Syntax.Env (readEnvFile)
import Dojang.Syntax.Manifest.Parser (Error, formatErrors, readManifestFile)
import Dojang.Syntax.Manifest.Writer (writeManifestFile)
import Dojang.Types.Context (Context (..))
import Dojang.Types.Environment (Environment (..))
import Dojang.Types.Environment.Current
( MonadArchitecture (currentArchitecture)
, MonadEnvironment (currentEnvironment)
, MonadOperatingSystem (currentOperatingSystem)
)
import Dojang.Types.Manifest (Manifest)
import Dojang.Types.Repository (Repository (..))
-- | The environment for the application monad.
data AppEnv = AppEnv
{ sourceDirectory :: OsPath
-- ^ The source directory (i.e., repository).
, intermediateDirectory :: OsPath
-- ^ The intermediate directory which is managed by Dojang. It's normally
-- @.dojang@ in the source directory.
, manifestFile :: OsPath
-- ^ The manifest file.
, envFile :: OsPath
-- ^ The environment file.
, dryRun :: Bool
-- ^ Whether to actually perform actions or just print what would be done.
, debug :: Bool
-- ^ Whether debug logging is enabled.
}
deriving (Show)
-- | The application monad for Dojang.
newtype (MonadFileSystem i, MonadError IOError i, MonadIO i) => App i v = App
{ unApp :: ReaderT AppEnv (LoggingT i) v
}
deriving
( Functor
, Applicative
, Monad
, MonadIO
, MonadReader AppEnv
, MonadLogger
)
instance
(MonadFileSystem i, MonadError IOError i, MonadIO i)
=> MonadError IOError (App i)
where
throwError = App . throwError
catchError action handler = App $ catchError (unApp action) (unApp . handler)
instance (MonadFileSystem i, MonadIO i) => MonadFileSystem (App i) where
encodePath = App . lift . lift . encodePath
decodePath = App . lift . lift . decodePath
exists = App . lift . lift . exists
isFile = App . lift . lift . isFile
isDirectory = App . lift . lift . isDirectory
isSymlink = App . lift . lift . isSymlink
readFile = App . lift . lift . readFile
writeFile dst = App . lift . lift . writeFile dst
readSymlinkTarget = App . lift . lift . readSymlinkTarget
copyFile src = App . lift . lift . copyFile src
createDirectory = App . lift . lift . createDirectory
removeFile = App . lift . lift . removeFile
removeDirectory = App . lift . lift . removeDirectory
listDirectory = App . lift . lift . listDirectory
getFileSize = App . lift . lift . getFileSize
currentEnvironment' :: (MonadFileSystem i, MonadIO i) => App i Environment
currentEnvironment' = do
sourceDir <- asks (.sourceDirectory)
envFile' <- asks (.envFile)
let filePath = sourceDir </> envFile'
$(logDebug) $ "Environment file path: " <> showt (FromStringShow filePath)
result <-
readEnvFile filePath `catchError` \e -> do
if isDoesNotExistError e
then do
$(logWarn)
$ "Environment file not found: "
<> showt (FromStringShow e)
env <- liftIO currentEnvironment
return $ Right (env, [])
else die' noEnvFile $ showt e
case result of
Left errors -> do
let formattedErrors =
Data.Text.concat
["\n " <> pack e | e <- toList errors]
die' envFileReadError $ "Syntax errors in environment file:" <> formattedErrors
Right (env, warnings) -> do
$(logDebugSH) env
forM_ warnings $ \w -> $(logWarn) $ fromString w
return env
instance (MonadFileSystem i, MonadIO i) => MonadEnvironment (App i) where
currentEnvironment = currentEnvironment'
instance (MonadFileSystem i, MonadIO i) => MonadOperatingSystem (App i) where
currentOperatingSystem = (.operatingSystem) <$> currentEnvironment'
instance (MonadFileSystem i, MonadIO i) => MonadArchitecture (App i) where
currentArchitecture = (.architecture) <$> currentEnvironment'
runAppWithoutLogging
:: (MonadFileSystem i, MonadIO i) => AppEnv -> App i a -> i a
runAppWithoutLogging env app = runAppWithLogging env app (\_ _ _ _ -> pure ())
-- | Run the application monad with logging handled by the given function.
runAppWithLogging
:: (MonadFileSystem i, MonadIO i)
=> AppEnv
-> App i a
-> (Loc -> LogSource -> LogLevel -> LogStr -> IO ())
-> i a
runAppWithLogging env app = runLoggingT (runReaderT app.unApp env)
-- | Run the application monad with logging to stderr.
runAppWithStderrLogging
:: (MonadFileSystem i, MonadIO i) => AppEnv -> App i a -> i a
runAppWithStderrLogging env = runStderrLoggingT . (`runReaderT` env) . unApp
manifestPath :: (MonadIO i) => App i OsPath
manifestPath = do
sourceDir <- asks (.sourceDirectory)
manifestFile' <- asks (.manifestFile)
let path = sourceDir </> manifestFile'
$(logDebug) $ "Manifest path: " <> showt (FromStringShow path)
return path
loadManifest
:: (MonadFileSystem i, MonadIO i) => App i (Either Error (Maybe Manifest))
loadManifest = do
filename <- manifestPath
result <- tryError (readManifestFile filename)
case result of
Left e
| isDoesNotExistError e -> do
$(logInfo) "Manifest file not found"
return $ Right Nothing
Left e -> do
$(logError) $ "Error reading manifest file: " <> showt (FromStringShow e)
throwError e
Right (Left err) -> do
$(logError)
$ "Error parsing manifest file: "
<> Data.Text.unlines (formatErrors err)
return $ Left err
Right (Right (manifest, warnings)) -> do
forM_ warnings $ \w -> $(logWarn) w
$(logDebugSH) manifest
return $ Right $ Just manifest
doesManifestExist :: (MonadFileSystem i, MonadIO i) => App i Bool
doesManifestExist = do
filePath <- manifestPath
exists' <- exists filePath
$(logInfo)
$ "Manifest file "
<> showt (FromStringShow filePath)
<> " "
<> if exists' then "exists" else "does not exist"
return exists'
saveManifest :: (MonadFileSystem i, MonadIO i) => Manifest -> App i OsPath
saveManifest manifest = do
filename <- manifestPath
$(logDebugSH) manifest
writeManifestFile manifest filename `catchError` \e -> do
$(logError) $ "Error writing manifest file: " <> showt (FromStringShow e)
throwError e
$(logInfo)
$ "Manifest file "
<> showt (FromStringShow filename)
<> " written"
return filename
loadRepository
:: (MonadFileSystem i, MonadIO i) => App i (Either Error (Maybe Repository))
loadRepository = do
sourceDir <- asks (normalise . (.sourceDirectory))
intermediateDir <- asks (normalise . (.intermediateDirectory))
result <- loadManifest
case result of
Left err -> return $ Left err
Right Nothing -> return $ Right Nothing
Right (Just manifest) -> do
return
$ Right
$ Just
$ Repository sourceDir (sourceDir </> intermediateDir) manifest
ensureRepository :: (MonadFileSystem i, MonadIO i) => App i Repository
ensureRepository = do
result <- loadRepository
case result of
Left e ->
dieWithErrors manifestReadError $ formatErrors e
Right Nothing -> do
printStderr' Error "No manifest found."
codeStyle <- codeStyleFor stderr
printStderr'
Note
("Run `" <> codeStyle "dojang init" <> "' to create one.")
liftIO $ exitWith manifestUninitialized
Right (Just repo) -> return repo
ensureContext :: (MonadFileSystem i, MonadIO i) => App i (Context (App i))
ensureContext = do
repo <- ensureRepository
currentEnv <- currentEnvironment'
$(logDebugSH) currentEnv
return $ Context repo currentEnv lookupEnv'
lookupEnv'
:: (MonadFileSystem i, MonadIO i) => EnvironmentVariable -> i (Maybe OsString)
lookupEnv' env = do
value <- liftIO $ lookupEnv $ unpack env
case value of
Just v -> do
Just <$> encodePath v
Nothing -> return Nothing