packages feed

headroom-0.1.0.0: src/Headroom/AppConfig.hs

{-|
Module      : Headroom.AppConfig
Description : Application configuration
Copyright   : (c) 2019-2020 Vaclav Svejcar
License     : BSD-3
Maintainer  : vaclav.svejcar@gmail.com
Stability   : experimental
Portability : POSIX

This module adds support for loading and parsing application configuration.
Such configuration can be loaded either from /YAML/ config file, or from command
line arguments. Provided 'Semigroup' and 'Monoid' instances allows to merge
multiple loaded configurations into one.
-}
{-# LANGUAGE DeriveGeneric     #-}
{-# LANGUAGE NoImplicitPrelude #-}
module Headroom.AppConfig
  ( AppConfig(..)
  , loadAppConfig
  , makePathsRelativeTo
  , parseAppConfig
  , parseVariables
  , validateAppConfig
  )
where

import           Control.Lens
import           Data.Aeson                     ( FromJSON(parseJSON)
                                                , genericParseJSON
                                                )
import           Data.Validation
import qualified Data.Yaml                     as Y
import           Headroom.Types                 ( AppConfigError(..)
                                                , HeadroomError(..)
                                                , RunMode(..)
                                                )
import           Headroom.Types.Utils           ( customOptions )
import           RIO
import qualified RIO.ByteString                as B
import           RIO.FilePath                   ( takeDirectory
                                                , (</>)
                                                )
import qualified RIO.HashMap                   as HM
import qualified RIO.Text                      as T


-- | Application configuration, loaded either from configuration file or command
-- line options.
data AppConfig = AppConfig
  { acRunMode       :: RunMode           -- ^ selected mode of /Run/ command
  , acSourcePaths   :: [FilePath]        -- ^ paths to source code files
  , acTemplatePaths :: [FilePath]        -- ^ paths to template files
  , acVariables     :: HashMap Text Text -- ^ variables to replace
  }
  deriving (Eq, Generic, Show)

-- | Support for reading configuration from /YAML/.
instance FromJSON AppConfig where
  parseJSON = genericParseJSON customOptions

instance Semigroup AppConfig where
  x <> y = AppConfig (acRunMode x)
                     (acSourcePaths x <> acSourcePaths y)
                     (acTemplatePaths x <> acTemplatePaths y)
                     (acVariables x <> acVariables y)

instance Monoid AppConfig where
  mempty = AppConfig Add [] [] HM.empty

-- | Loads and parses application configuration from given file.
loadAppConfig :: MonadIO m
              => FilePath    -- ^ path to configuration file
              -> m AppConfig -- ^ parsed configuration
loadAppConfig path = do
  appConfig <- liftIO $ B.readFile path >>= parseAppConfig
  return $ makePathsRelativeTo (takeDirectory path) appConfig

-- | Rewrites all file paths in 'AppConfig' to be relative to given file path.
makePathsRelativeTo :: FilePath  -- ^ file path to use
                    -> AppConfig -- ^ input application configuration
                    -> AppConfig -- ^ result with relativized file paths
makePathsRelativeTo root appConfig = appConfig
  { acSourcePaths   = processPaths . acSourcePaths $ appConfig
  , acTemplatePaths = processPaths . acTemplatePaths $ appConfig
  }
  where processPaths = fmap (root </>)

-- | Parses application configuration from given raw input.
parseAppConfig :: MonadThrow m
               => B.ByteString -- ^ raw input to parse
               -> m AppConfig  -- ^ parsed application configuration
parseAppConfig = Y.decodeThrow

-- | Parses variables from raw input in @key=value@ format.
--
-- >>> parseVariables ["key1=value1"]
-- fromList [("key1","value1")]
parseVariables :: MonadThrow m
               => [Text]                -- ^ list of raw variables
               -> m (HashMap Text Text) -- ^ parsed variables
parseVariables variables = fmap HM.fromList (mapM parse variables)
 where
  parse input = case T.split (== '=') input of
    [key, value] -> return (key, value)
    _            -> throwM $ InvalidVariable input

-- | Validates whether given 'AppConfig' contains valid data.
validateAppConfig :: MonadThrow m
                  => AppConfig   -- ^ application config to validate
                  -> m AppConfig -- ^ validated application config (or errors)
validateAppConfig appConfig = case checked of
  Success ac'    -> return ac'
  Failure errors -> throwM $ InvalidAppConfig errors
 where
  checked          = appConfig <$ checkSourcePaths <* checkTemplatePaths
  checkSourcePaths = if null (acSourcePaths appConfig)
    then _Failure # [EmptySourcePaths]
    else _Success # appConfig
  checkTemplatePaths = if null (acTemplatePaths appConfig)
    then _Failure # [EmptyTemplatePaths]
    else _Success # appConfig