packages feed

hinit-0.2.0: src/Hinit/Template.hs

{-# LANGUAGE QuasiQuotes #-}

module Hinit.Template where

import Control.Effect.Lift
import Control.Effect.Throw
import Control.Monad
import Control.Monad.IO.Class
import Data.Foldable
import qualified Data.Map.Strict as M
import Data.Maybe
import Data.Monoid
import Data.String.Interpolate
import Data.Text (Text, pack, unpack)
import qualified Data.Text.IO as T
import Data.Text.Prettyprint.Doc
import GHC.Generics
import Hinit.Errors
import Hinit.Template.Config
import Hinit.Types
import Hinit.Utils
import Path
import Path.IO
import Paths_hinit
import System.FilePath.Glob
import Text.Mustache hiding (Template)
import Text.Mustache.Types (Value)
import Toml (decode)
import Toml.Codec.Error (TomlDecodeError)

data Template
  = Local
      { path :: Path Abs Dir,
        name :: Text,
        templateConfig :: TemplateConfig
      }
  | Broken
      { path :: Path Abs Dir,
        name :: Text,
        errors :: [TomlDecodeError]
      }
  deriving (Show, Eq, Generic)

fromContext :: Context -> Value
fromContext = object . fmap (uncurry (~>)) . M.toList

underCurrentDir :: Path Rel a -> Bool
underCurrentDir = (== [reldir|./|]) . parent

-- | Copy a file from template to the corresponding location in the target directory and apply substitution.
--   Note that this function assumes that the parent directory of the target file exists.
copyFileFromTemplate ::
  ( Has (Throw MustacheError) sig m,
    Has (Throw IllformedPath) sig m,
    Has (Lift IO) sig m,
    ToMustache ctx
  ) =>
  -- | Mustache context
  ctx ->
  -- | Base dir of the template
  Path a Dir ->
  -- | Base dir of the target
  Path b Dir ->
  -- | Source file path relative to the template base
  Path Rel File ->
  m ()
copyFileFromTemplate ctx base tgtbase src = do
  let srcFilename = filename src
  let srcParentDir = parent src
  case compileTemplate "" (pack $ fromRelFile srcFilename) of
    Left e -> throwError $ TemplateParseError src True (pack $ show e)
    Right fileNameTmpl -> do
      let (errs, tgtFileNameRaw) = checkedSubstitute fileNameTmpl ctx
      unless (null errs) $
        throwError $ RenderingError src True errs
      let err = TemplateFile src tgtFileNameRaw
      case parseRelFile (unpack tgtFileNameRaw) of
        Nothing -> throwError err
        Just tgtFileName
          | not (underCurrentDir tgtFileName) -> throwError err
          | otherwise -> do
            let srcFilePath = base </> src
            let tgtFilePath = tgtbase </> srcParentDir </> tgtFileName
            srcFileContent <- sendIO $ T.readFile (toFilePath srcFilePath)
            case compileTemplate (toFilePath src) srcFileContent of
              Left e' -> throwError $ TemplateParseError src False (pack $ show e')
              Right fileTmpl -> do
                let (errs', tgtFileContent) = checkedSubstitute fileTmpl ctx
                unless (null errs') $
                  throwError $ RenderingError src False errs'
                sendIO $ T.writeFile (toFilePath tgtFilePath) tgtFileContent

-- | Copy a directory from template to the corresponding location in the target directory and apply substitution.
--   Note that this function assumes that the parent directory of the target directory exists.
copyDirFromTemplate ::
  ( Has (Throw MustacheError) sig m,
    Has (Throw IllformedPath) sig m,
    Has (Lift IO) sig m,
    ToMustache ctx
  ) =>
  -- | Mustache context
  ctx ->
  -- | Base dir of the template
  Path a Dir ->
  -- | Base dir of the target
  Path b Dir ->
  -- | Source dir path relative to the template base
  Path Rel Dir ->
  m ()
copyDirFromTemplate ctx _ tgtbase src = do
  let srcDirName = dirname src
  let srcParentDir = parent src
  case compileTemplate "" (pack $ fromRelDir srcDirName) of
    Left e -> throwError $ TemplateParseError src True (pack $ show e)
    Right fileNameTmpl -> do
      let (errs, tgtFileNameRaw) = checkedSubstitute fileNameTmpl ctx
      unless (null errs) $
        throwError $ RenderingError src True errs
      let err = TemplateFile src tgtFileNameRaw
      case parseRelDir (unpack tgtFileNameRaw) of
        Nothing -> throwError err
        Just tgtDirName
          | not (underCurrentDir tgtDirName) -> throwError err
          | otherwise -> do
            let tgtDirPath = tgtbase </> srcParentDir </> tgtDirName
            sendIO $ createDirIfMissing False tgtDirPath

-- | Initialize a project from a template
initFromTemplate ::
  ( Has (Throw MustacheError) sig m,
    Has (Throw IllformedPath) sig m,
    Has (Lift IO) sig m,
    MonadIO m,
    ToMustache ctx
  ) =>
  -- | Ignored files
  [Pattern] ->
  -- | Mustache context
  ctx ->
  -- | Base dir of the template
  Path a Dir ->
  -- | Base dir of the target
  Path b Dir ->
  m ()
initFromTemplate ignores ctx template target = do
  sendIO $ ensureDir target
  walkDirRel handler template
  where
    handler current subdirs files = do
      traverse_ (copyFileFromTemplate ctx template target) filesToCpy
      traverse_ (copyDirFromTemplate ctx template target) dirsToCpy
      pure $ WalkExclude excl
      where
        subdirs' = map (current </>) subdirs
        files' = map (current </>) files
        ignored :: Path Rel a -> Bool
        ignored a = ignores `matches` toFilePath a
        excl = filter ignored subdirs'
        dirsToCpy = filter (not . ignored) subdirs'
        filesToCpy = filter (not . ignored) files'

readTemplate :: Has (Lift IO) sig m => Path Abs Dir -> m (Maybe Template)
readTemplate templatePath = do
  let templateConfigFile = templatePath </> [relfile|template.toml|]
  let name = pack $ init $ fromRelDir $ dirname templatePath
  exists <- sendIO $ doesFileExist templateConfigFile
  if exists
    then do
      f <- sendIO $ T.readFile $ fromAbsFile templateConfigFile
      case decode templateConfigCodec f of
        Left errors -> pure $ Just Broken {path = templatePath, ..}
        Right templateConfig -> pure $ Just Local {path = templatePath, ..}
    else pure Nothing

getTemplateDirs :: Has (Lift IO) sig m => m [Path Abs Dir]
getTemplateDirs = do
  dataDirRaw <- sendIO getDataDir
  dataDir <- sendIO $ parseAbsDir dataDirRaw
  let bundledTemplateDir = dataDir </> [reldir|templates|]
  localTemplateDir <- sendIO $ getXdgDir XdgData (Just [reldir|hi|])
  let dirs = [bundledTemplateDir, localTemplateDir]
  traverse_ (sendIO . ensureDir) dirs
  pure dirs

getTemplate ::
  ( Has (Lift IO) sig m,
    Has (Throw IllformedPath) sig m
  ) =>
  Text ->
  m (Maybe Template)
getTemplate tmpl = do
  templateDirs <- getTemplateDirs
  case parseRelDir (unpack tmpl) of
    Nothing -> throwError $ TemplateName tmpl
    Just templateName -> do
      let templates = fmap (</> templateName) templateDirs
      mTemplates <- traverse readTemplate templates
      pure $ getFirst $ foldMap First mTemplates

getTemplates :: Has (Lift IO) sig m => m [Template]
getTemplates = do
  templateDirs <- getTemplateDirs
  contents <- traverse (sendIO . listDir) templateDirs
  let subdirs = concatMap fst contents
  mTemplates <- traverse readTemplate subdirs
  pure $ catMaybes mTemplates

prettyTemplate :: Bool -> Template -> Doc a
prettyTemplate verbose template
  | Broken {..} <- template =
    if verbose
      then
        vsep
          [ [i|template: #{name}|],
            [i|path: #{path}|]
          ]
      else pretty name
  | Local {..} <- template,
    TemplateConfig {..} <- templateConfig =
    if verbose
      then
        vsep $
          [i|template: #{name}|] :
          maybe [] (\d -> pure [i|desc: #{d}|]) desc
            ++ vsep
              [ "tags:",
                indent 2 $ mkBulletList $ fmap pretty tags
              ] :
          pure [i|path: #{path}|]
      else pretty name

prettyTemplates :: Bool -> [Template] -> Doc a
prettyTemplates verbose templates =
  vsep $
    [ "- local templates",
      indent 2 $ mkBulletList local
    ]
      ++ if null broken
        then []
        else
          [ "- broken templates",
            indent 2 $ mkBulletList broken
          ]
  where
    isBroken Broken {} = True
    isBroken _ = False
    isLocal Local {} = True
    isLocal _ = False
    prettyP = prettyTemplate verbose
    broken = map prettyP $ filter isBroken templates
    local = map prettyP $ filter isLocal templates