life-sync-1.1.1.0: src/Life/Configuration.hs
{- |
Copyright: (c) 2017-2020 Kowainik
SPDX-License-Identifier: MPL-2.0
Maintainer: Kowainik <xrom.xkov@gmail.com>
Contains configuration data type — 'LifeConfiguration'.
-}
module Life.Configuration
( LifeConfiguration (..)
, singleDirConfig
, singleFileConfig
, defaultLifeConfig
, lifeConfigMinus
-- Config
, CorpseConfiguration (..)
, corpseConfiguationCodec
, resurrect
-- -- * Parsing exceptions
-- , ParseLifeException (..)
-- * Lenses for 'LifeConfiguration'
, filesL
, directoriesL
, branchL
-- * Parse 'LifeConfiguration' under @~/.life@
, parseHomeLife
, parseRepoLife
-- * Render 'LifeConfiguration' under @~/.life@
, renderLifeConfiguration
, writeGlobalLife
) where
import Control.Monad.Catch (MonadThrow (..))
import Path (Dir, File, Path, Rel, fromAbsFile, parseRelDir, parseRelFile, (</>))
import Relude.Extra.Lens (Lens', lens, (.~), (^.))
import Toml (TomlCodec, (.=))
import Life.Core (Branch (..), master)
import Life.Path (lifePath, relativeToHome, repoName)
import qualified Data.Set as Set
import qualified Data.Text as T
import qualified Toml
-- | The configurations data type.
data LifeConfiguration = LifeConfiguration
{ lifeConfigurationFiles :: !(Set (Path Rel File))
, lifeConfigurationDirectories :: !(Set (Path Rel Dir))
, lifeConfigurationBranch :: !(Last Branch)
} deriving stock (Show, Eq)
-- | Lens for 'lifeConfigurationFiles'.
filesL :: Lens' LifeConfiguration (Set (Path Rel File))
filesL = lens
lifeConfigurationFiles
(\config newFiles -> config {lifeConfigurationFiles = newFiles})
-- | Lens for 'lifeConfigurationDirectories'.
directoriesL :: Lens' LifeConfiguration (Set (Path Rel Dir))
directoriesL = lens
lifeConfigurationDirectories
(\config newDirs -> config {lifeConfigurationDirectories = newDirs})
-- | Lens for 'lifeConfigurationBranch'.
branchL :: Lens' LifeConfiguration (Last Branch)
branchL = lens
lifeConfigurationBranch
(\config newBr -> config {lifeConfigurationBranch = newBr})
----------------------------------------------------------------------------
-- Algebraic instances and utilities
----------------------------------------------------------------------------
instance Semigroup LifeConfiguration where
life1 <> life2 = LifeConfiguration
{ lifeConfigurationFiles = (life1 ^. filesL) <> (life2 ^. filesL)
, lifeConfigurationDirectories = (life1 ^. directoriesL) <> (life2 ^. directoriesL)
, lifeConfigurationBranch = (life1 ^. branchL) <> (life2 ^. branchL)
}
instance Monoid LifeConfiguration where
mempty = LifeConfiguration mempty mempty mempty
mappend = (<>)
-- | The defaulting 'LifeConfiguration', with the default @master@ branch.
defaultLifeConfig :: LifeConfiguration
defaultLifeConfig = LifeConfiguration
{ lifeConfigurationFiles = mempty
, lifeConfigurationDirectories = mempty
, lifeConfigurationBranch = Last $ Just master
}
-- | Creates a 'LifeConfiguration' with the given file.
singleFileConfig :: Path Rel File -> LifeConfiguration
singleFileConfig file = mempty & filesL .~ one file
-- | Creates a 'LifeConfiguration' with the given folder.
singleDirConfig :: Path Rel Dir -> LifeConfiguration
singleDirConfig dir = mempty & directoriesL .~ one dir
----------------------------------------------------------------------------
-- LifeConfiguration difference
----------------------------------------------------------------------------
lifeConfigMinus
:: LifeConfiguration -- ^ Repository @.life@ configuration
-> LifeConfiguration -- ^ Global configuration
-> LifeConfiguration -- ^ Configuration that is not in global
lifeConfigMinus dotfiles global = LifeConfiguration
(Set.difference (dotfiles ^. filesL) (global ^. filesL))
(Set.difference (dotfiles ^. directoriesL) (global ^. directoriesL))
(Last $ Just master)
----------------------------------------------------------------------------
-- Toml parser for life configuration
----------------------------------------------------------------------------
data CorpseConfiguration = CorpseConfiguration
{ corpseFiles :: [FilePath]
, corpseDirectories :: [FilePath]
}
corpseConfiguationCodec :: TomlCodec CorpseConfiguration
corpseConfiguationCodec = CorpseConfiguration
<$> Toml.arrayOf Toml._String "files" .= corpseFiles
<*> Toml.arrayOf Toml._String "directories" .= corpseDirectories
resurrect :: MonadThrow m => CorpseConfiguration -> m LifeConfiguration
resurrect CorpseConfiguration{..} = do
filePaths <- mapM parseRelFile corpseFiles
dirPaths <- mapM parseRelDir corpseDirectories
pure $ LifeConfiguration
{ lifeConfigurationFiles = Set.fromList filePaths
, lifeConfigurationDirectories = Set.fromList dirPaths
, lifeConfigurationBranch = Last (Just master)
}
-- TODO: should tomland one day support this?...
-- | Converts 'LifeConfiguration' into TOML file.
renderLifeConfiguration
:: Bool -- ^ True to see empty entries in output
-> LifeConfiguration
-> Text
renderLifeConfiguration printIfEmpty LifeConfiguration{..} = mconcat $
maybeToList (render "directories" lifeConfigurationDirectories)
++ [ "\n" ]
++ maybeToList (render "files" lifeConfigurationFiles)
where
render :: Text -> Set (Path b t) -> Maybe Text
render key paths = do
let prefix = key <> " = "
let array = renderStringArray $ map show $ toList paths
if not printIfEmpty && null paths
then Nothing
else Just $ prefix <> array
renderStringArray :: [Text] -> Text
renderStringArray = \case
[] -> "[]"
[x] -> "[" <> x <> "]"
l -> mconcat
[ "\n [ "
, T.intercalate "\n , " l
, "\n ]"
]
writeGlobalLife :: LifeConfiguration -> IO ()
writeGlobalLife config = do
lifeFilePath <- relativeToHome lifePath
writeFileText (fromAbsFile lifeFilePath) (renderLifeConfiguration True config)
----------------------------------------------------------------------------
-- Life configuration parsing
----------------------------------------------------------------------------
parseLife :: Path Rel File -> IO LifeConfiguration
parseLife path =
relativeToHome path
>>= Toml.decodeFile corpseConfiguationCodec . fromAbsFile
>>= resurrect
-- | Reads 'LifeConfiguration' from @~\/.life@ file.
parseHomeLife :: IO LifeConfiguration
parseHomeLife = parseLife lifePath
-- | Reads 'LifeConfiguration' from @~\/dotfiles\/.life@ file.
parseRepoLife :: IO LifeConfiguration
parseRepoLife = parseLife (repoName </> lifePath)