{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
module Headroom.Command.Run
( commandRun
, loadBuiltInTemplates
, loadTemplateFiles
, typeOfTemplate
, postProcessHeader'
, sanitizeHeader
)
where
import Data.Time.Calendar ( toGregorian )
import Data.Time.Clock ( getCurrentTime )
import Data.Time.Clock.POSIX ( getPOSIXTime )
import Data.Time.LocalTime ( getCurrentTimeZone
, localDay
, utcToLocalTime
)
import Headroom.Command.Types ( CommandRunOptions(..) )
import Headroom.Command.Utils ( bootstrap )
import Headroom.Configuration ( loadConfiguration
, makeConfiguration
, parseConfiguration
)
import Headroom.Configuration.Types ( Configuration(..)
, CtConfiguration
, CtHeaderFnConfigs
, HeaderConfig(..)
, HeaderSyntax(..)
, LicenseType(..)
, PtConfiguration
, RunMode(..)
, TemplateSource(..)
)
import Headroom.Data.EnumExtra ( EnumExtra(..) )
import Headroom.Data.Has ( Has(..) )
import Headroom.Data.Lens ( suffixLenses
, suffixLensesFor
)
import Headroom.Data.TextExtra ( mapLines )
import Headroom.Embedded ( defaultConfig
, licenseTemplate
)
import Headroom.Ext ( extractTemplateMeta )
import Headroom.FileSupport ( addHeader
, dropHeader
, extractFileInfo
, replaceHeader
)
import Headroom.FileSupport.Types ( FileInfo(..) )
import Headroom.FileSystem ( FileSystem(..)
, excludePaths
, fileExtension
, mkFileSystem
)
import Headroom.FileType ( configByFileType
, fileTypeByExt
)
import Headroom.FileType.Types ( FileType(..) )
import Headroom.HeaderFn ( mkConfiguredEnv
, postProcessHeader
)
import Headroom.Meta ( TemplateType
, productInfo
)
import Headroom.Template ( Template(..) )
import Headroom.Types ( CurrentYear(..)
, TemplateMeta(..)
)
import Headroom.UI ( Progress(..)
, zipWithProgress
)
import Headroom.Variables ( compileVariables
, dynamicVariables
, parseVariables
)
import Headroom.Variables.Types ( Variables(..) )
import RIO
import RIO.FilePath ( takeBaseName )
import qualified RIO.List as L
import qualified RIO.Map as M
import qualified RIO.Text as T
suffixLensesFor ["cHeaderFnConfigs"] ''Configuration
type TemplatesMap = Map FileType (Maybe TemplateMeta, TemplateType)
data RunAction = RunAction
{ raProcessed :: Bool
, raFunc :: Text -> Text
, raProcessedMsg :: Text
, raSkippedMsg :: Text
}
data StartupEnv = StartupEnv
{ envLogFunc :: LogFunc
, envRunOptions :: CommandRunOptions
}
suffixLenses ''StartupEnv
data Env = Env
{ envEnv :: StartupEnv
, envConfiguration :: CtConfiguration
, envCurrentYear :: CurrentYear
, envFileSystem :: FileSystem (RIO Env)
}
suffixLenses ''Env
instance Has CtConfiguration Env where
hasLens = envConfigurationL
instance Has CtHeaderFnConfigs Env where
hasLens = envConfigurationL . cHeaderFnConfigsL
instance Has StartupEnv StartupEnv where
hasLens = id
instance Has StartupEnv Env where
hasLens = envEnvL
instance HasLogFunc StartupEnv where
logFuncL = envLogFuncL
instance HasLogFunc Env where
logFuncL = hasLens @StartupEnv . logFuncL
instance Has CommandRunOptions StartupEnv where
hasLens = envRunOptionsL
instance Has CommandRunOptions Env where
hasLens = hasLens @StartupEnv . hasLens
instance Has CurrentYear Env where
hasLens = envCurrentYearL
instance Has (FileSystem (RIO Env)) Env where
hasLens = envFileSystemL
env' :: CommandRunOptions -> LogFunc -> IO Env
env' opts logFunc = do
let envEnv = StartupEnv { envLogFunc = logFunc, envRunOptions = opts }
envFileSystem = mkFileSystem
envConfiguration <- runRIO envEnv finalConfiguration
envCurrentYear <- currentYear
pure Env { .. }
commandRun :: CommandRunOptions
-> IO ()
commandRun opts = bootstrap (env' opts) (croDebug opts) $ do
CommandRunOptions {..} <- viewL
Configuration {..} <- viewL @CtConfiguration
let isCheck = cRunMode == Check
warnOnDryRun
startTS <- liftIO getPOSIXTime
templates <- withTemplateMeta <$> loadTemplates
sourceFiles <- findSourceFiles (M.keys templates)
(total, processed) <- processSourceFiles templates sourceFiles
endTS <- liftIO getPOSIXTime
logInfo "-----"
logInfo $ mconcat
[ "Done: "
, if isCheck then "outdated " else "modified "
, display processed
, if isCheck then ", up-to-date " else ", skipped "
, display (total - processed)
, " file(s) in "
, displayShow (endTS - startTS)
, " second(s)."
]
warnOnDryRun
when (not croDryRun && isCheck && processed > 0) (exitWith $ ExitFailure 1)
warnOnDryRun :: (HasLogFunc env, Has CommandRunOptions env) => RIO env ()
warnOnDryRun = do
CommandRunOptions {..} <- viewL
when croDryRun $ logWarn "[!] Running with '--dry-run', no files are changed!"
findSourceFiles :: ( Has CtConfiguration env
, Has (FileSystem (RIO env)) env
, HasLogFunc env
)
=> [FileType]
-> RIO env [FilePath]
findSourceFiles fileTypes = do
Configuration {..} <- viewL
FileSystem {..} <- viewL
logDebug $ "Using source paths: " <> displayShow cSourcePaths
files <-
mconcat <$> mapM (fsFindFilesByTypes cLicenseHeaders fileTypes) cSourcePaths
let files' = excludePaths cExcludedPaths files
logInfo $ mconcat
[ "Found "
, display $ L.length files'
, " source file(s) (excluded "
, display $ L.length files - L.length files'
, " file(s))"
]
pure files'
processSourceFiles :: ( Has CtConfiguration env
, Has CtHeaderFnConfigs env
, Has CommandRunOptions env
, Has CurrentYear env
, HasLogFunc env
)
=> TemplatesMap
-> [FilePath]
-> RIO env (Int, Int)
processSourceFiles templates paths = do
Configuration {..} <- viewL
year <- viewL
let dVars = dynamicVariables year
withFileType = mapMaybe (findFileType cLicenseHeaders) paths
withTemplate = mapMaybe (uncurry findTemplate) withFileType
cVars <- compileVariables (dVars <> cVariables)
processed <- mapM (process cVars dVars) (zipWithProgress withTemplate)
pure (L.length withTemplate, L.length . filter (== True) $ processed)
where
findFileType conf path =
fmap (, path) (fileExtension path >>= fileTypeByExt conf)
findTemplate ft p = (, ft, p) <$> M.lookup ft templates
process cVars dVars (pr, ((tm, tt), ft, p)) =
processSourceFile cVars dVars pr tm tt ft p
processSourceFile :: ( Has CommandRunOptions env
, Has CtConfiguration env
, Has CtHeaderFnConfigs env
, Has CurrentYear env
, HasLogFunc env
)
=> Variables
-> Variables
-> Progress
-> Maybe TemplateMeta
-> TemplateType
-> FileType
-> FilePath
-> RIO env Bool
processSourceFile cVars dVars progress meta template fileType path = do
Configuration {..} <- viewL @CtConfiguration
CommandRunOptions {..} <- viewL
fileContent <- readFileUtf8 path
let fileInfo@FileInfo {..} = extractFileInfo
fileType
(configByFileType cLicenseHeaders fileType)
meta
fileContent
variables = dVars <> cVars <> fiVariables
syntax = hcHeaderSyntax fiHeaderConfig
header' <- renderTemplate variables template
header <- postProcessHeader' syntax variables header'
RunAction {..} <- chooseAction fileInfo header
let result = raFunc fileContent
changed = raProcessed && (fileContent /= result)
message = if changed then raProcessedMsg else raSkippedMsg
isCheck = cRunMode == Check
logDebug $ "File info: " <> displayShow fileInfo
logInfo $ mconcat [display progress, " ", display message, fromString path]
when (not croDryRun && not isCheck && changed) (writeFileUtf8 path result)
pure changed
chooseAction :: (Has CtConfiguration env)
=> FileInfo
-> Text
-> RIO env RunAction
chooseAction info header = do
Configuration {..} <- viewL @CtConfiguration
let hasHeader = isJust $ fiHeaderPos info
pure $ go cRunMode hasHeader
where
go runMode hasHeader = case runMode of
Add -> aAction hasHeader
Check -> cAction hasHeader
Drop -> dAction hasHeader
Replace -> rAction hasHeader
aAction hasHeader = RunAction (not hasHeader)
(addHeader info header)
(justify "Adding header to:")
(justify "Header already exists in:")
cAction hasHeader = (rAction hasHeader)
{ raProcessedMsg = justify "Outdated header found in:"
, raSkippedMsg = justify "Header up-to-date in:"
}
dAction hasHeader = RunAction hasHeader
(dropHeader info)
(justify "Dropping header from:")
(justify "No header exists in:")
rAction hasHeader = if hasHeader then rAction' else go Add hasHeader
rAction' = RunAction True
(replaceHeader info header)
(justify "Replacing header in:")
(justify "Header up-to-date in:")
justify = T.justifyLeft 30 ' '
loadTemplateFiles :: (Has (FileSystem (RIO env)) env, HasLogFunc env)
=> [FilePath]
-> RIO env (Map FileType TemplateType)
loadTemplateFiles paths' = do
FileSystem {..} <- viewL
paths <- mconcat <$> mapM (`fsFindFilesByExts` extensions) paths'
logDebug $ "Using template paths: " <> displayShow paths
withTypes <- catMaybes <$> mapM (\p -> fmap (, p) <$> typeOfTemplate p) paths
parsed <- mapM
(\(t, p) ->
(t, ) <$> ((T.strip <$> fsLoadFile p) >>= parseTemplate (Just $ T.pack p))
)
withTypes
logInfo
$ mconcat ["Found ", display $ L.length parsed, " license template(s)"]
pure $ M.fromList parsed
where extensions = toList $ templateExtensions @TemplateType
loadBuiltInTemplates :: (HasLogFunc env)
=> LicenseType
-> RIO env (Map FileType TemplateType)
loadBuiltInTemplates licenseType = do
logInfo $ "Using built-in templates for license: " <> displayShow licenseType
parsed <- mapM (\(t, r) -> (t, ) <$> parseTemplate Nothing r) rawTemplates
pure $ M.fromList parsed
where
rawTemplates = fmap (\ft -> (ft, template ft)) (allValues @FileType)
template = licenseTemplate licenseType
loadTemplates :: ( Has CtConfiguration env
, Has (FileSystem (RIO env)) env
, HasLogFunc env
)
=> RIO env (Map FileType TemplateType)
loadTemplates = do
Configuration {..} <- viewL @CtConfiguration
case cTemplateSource of
TemplateFiles paths -> loadTemplateFiles paths
BuiltInTemplates licenseType -> loadBuiltInTemplates licenseType
withTemplateMeta :: Map FileType TemplateType -> TemplatesMap
withTemplateMeta = M.fromList . go . M.toList
where go = fmap (\(k, v) -> (k, (extractTemplateMeta k v, v)))
typeOfTemplate :: HasLogFunc env
=> FilePath
-> RIO env (Maybe FileType)
typeOfTemplate path = do
let fileType = textToEnum . T.pack . takeBaseName $ path
when (isNothing fileType)
(logWarn $ "Skipping unrecognized template type: " <> fromString path)
pure fileType
loadConfigurationSafe :: (HasLogFunc env)
=> FilePath
-> RIO env (Maybe PtConfiguration)
loadConfigurationSafe path = catch (Just <$> loadConfiguration path) onError
where
onError err = do
logDebug $ displayShow (err :: IOException)
logInfo $ mconcat
[ "Configuration file '"
, fromString path
, "' not found. You can either specify all required parameter by "
, "command line arguments, or generate one using "
, "'headroom gen -c >.headroom.yaml'. See official documentation "
, "for more details."
]
pure Nothing
finalConfiguration :: (HasLogFunc env, Has CommandRunOptions env)
=> RIO env CtConfiguration
finalConfiguration = do
logInfo $ display productInfo
defaultConfig' <- Just <$> parseConfiguration defaultConfig
cmdLineConfig <- Just <$> optionsToConfiguration
yamlConfig <- loadConfigurationSafe ".headroom.yaml"
let mergedConfig =
mconcat . catMaybes $ [defaultConfig', yamlConfig, cmdLineConfig]
config <- makeConfiguration mergedConfig
logDebug $ "Default config: " <> displayShow defaultConfig'
logDebug $ "YAML config: " <> displayShow yamlConfig
logDebug $ "CmdLine config: " <> displayShow cmdLineConfig
logDebug $ "Merged config: " <> displayShow mergedConfig
logDebug $ "Final config: " <> displayShow config
pure config
optionsToConfiguration :: (Has CommandRunOptions env) => RIO env PtConfiguration
optionsToConfiguration = do
CommandRunOptions {..} <- viewL
variables <- parseVariables croVariables
pure Configuration { cRunMode = maybe mempty pure croRunMode
, cSourcePaths = ifNot null croSourcePaths
, cExcludedPaths = ifNot null croExcludedPaths
, cTemplateSource = maybe mempty pure croTemplateSource
, cVariables = variables
, cLicenseHeaders = mempty
, cHeaderFnConfigs = mempty
}
where ifNot cond value = if cond value then mempty else pure value
currentYear :: (MonadIO m) => m CurrentYear
currentYear = do
now <- liftIO getCurrentTime
timezone <- liftIO getCurrentTimeZone
let zoneNow = utcToLocalTime timezone now
(year, _, _) = toGregorian $ localDay zoneNow
pure $ CurrentYear year
postProcessHeader' :: (Has CtHeaderFnConfigs env, Has CurrentYear env)
=> HeaderSyntax
-> Variables
-> Text
-> RIO env Text
postProcessHeader' syntax vars rawHeader = do
configs <- viewL @CtHeaderFnConfigs
year <- viewL
cEnv <- mkConfiguredEnv year vars configs
let processed = sanitizeHeader syntax . postProcessHeader cEnv $ rawHeader
pure processed
sanitizeHeader :: HeaderSyntax
-> Text
-> Text
sanitizeHeader (BlockComment _ _ ) text = text
sanitizeHeader (LineComment prefixedBy) text = mapLines process text
where
process line | T.isPrefixOf prefixedBy line = line
| otherwise = prefixedBy <> " " <> line