{-# LANGUAGE FlexibleContexts #-} {-| Utility functions based on 'Data.Text.Template' to offer @ $var @ variable expansion in string throughout a B9 artifact. -} module B9.Artifact.Content.StringTemplate ( subst , substE , substEB , substFile , substPath , readTemplateFile , SourceFile(..) , SourceFileConversion(..) ) where #if !MIN_VERSION_base(4,8,0) import Control.Applicative #endif import B9.Environment import B9.QCUtil import Control.Exception (SomeException) import Control.Monad.IO.Class (MonadIO(liftIO)) import Control.Monad.Reader (runReaderT) import Control.Monad.Trans.Identity () import Control.Parallel.Strategies import Data.Bifunctor import Data.Binary import qualified Data.ByteString as Strict import qualified Data.ByteString.Lazy as Lazy import Data.Data import Data.Hashable import qualified Data.Text.Lazy as LazyT import qualified Data.Text as StrictT import qualified Data.Text.Encoding as StrictE import qualified Data.Text.Lazy.Encoding as LazyE import Data.Text.Template (render, renderA, templateSafe) import GHC.Generics (Generic) import System.IO.B9Extras import Test.QuickCheck import Text.Printf -- | A wrapper around a file path and a flag indicating if template variable -- expansion should be performed when reading the file contents. data SourceFile = Source SourceFileConversion FilePath deriving (Read, Show, Typeable, Data, Eq, Generic) instance Hashable SourceFile instance Binary SourceFile instance NFData SourceFile data SourceFileConversion = NoConversion | ExpandVariables deriving (Read, Show, Typeable, Data, Eq, Generic) instance Hashable SourceFileConversion instance Binary SourceFileConversion instance NFData SourceFileConversion readTemplateFile :: (MonadIO m, MonadEnvironment m) => SourceFile -> m Lazy.ByteString readTemplateFile (Source conv f') = do env <- askEnvironment case substE env f' of Left e -> error (printf "Failed to substitute templates in source \ \file name '%s'/\nError: %s\n" f' e) Right f -> do c <- liftIO (Lazy.readFile f) convert f c where convert f c = case conv of NoConversion -> return c ExpandVariables -> do env <- askEnvironment case substEB env c of Left e -> error (printf "readTemplateFile '%s' failed: \n%s\n" f e) Right c' -> return c' -- String template substitution via dollar subst :: Environment -> String -> String subst env templateStr = case substE env templateStr of Left e -> error e Right r -> r -- String template substitution via dollar substE :: Environment -> String -> Either String String substE env templateStr = second (LazyT.unpack . LazyE.decodeUtf8) (substEB env (LazyE.encodeUtf8 (LazyT.pack templateStr))) -- String template substitution via dollar substEB :: Environment -> Lazy.ByteString -> Either String Lazy.ByteString substEB env templateStr = do t <- template' res <- renderA t env' return (LazyE.encodeUtf8 res) where env' t = case lookupEither (LazyT.fromStrict t) env of Right v -> Right (LazyT.toStrict v) Left e -> Left (show e ++ "\nIn template: \"" ++ show templateStr ++ "\"\n") template' = case templateSafe (LazyT.toStrict (LazyE.decodeUtf8 templateStr)) of Left (row, col) -> Left ("Invalid template, error at row: " ++ show row ++ ", col: " ++ show col ++ " in: \"" ++ show templateStr) Right t -> Right t substFile :: MonadIO m => Environment -> FilePath -> FilePath -> m () substFile env src dest = do templateBs <- liftIO (Strict.readFile src) let t = templateSafe (StrictE.decodeUtf8 templateBs) case t of Left (r, c) -> let badLine = unlines (take r (lines (StrictT.unpack (StrictE.decodeUtf8 templateBs)))) colMarker = replicate (c - 1) '-' ++ "^" in error (printf "Template error in file '%s' line %i:\n\n%s\n%s\n" src r badLine colMarker) Right template' -> do let out = LazyE.encodeUtf8 (render template' envLookup) liftIO (Lazy.writeFile dest out) return () where envLookup :: StrictT.Text -> StrictT.Text envLookup x = either err LazyT.toStrict (runReaderT (lookupOrThrow (LazyT.fromStrict x)) env) where err :: SomeException -> a err e = error (show e ++ "\nIn file: \'" ++ src ++ "\'\n") substPath :: Environment -> SystemPath -> SystemPath substPath env src = case src of Path p -> Path (subst env p) InHomeDir p -> InHomeDir (subst env p) InB9UserDir p -> InB9UserDir (subst env p) InTempDir p -> InTempDir (subst env p) instance Arbitrary SourceFile where arbitrary = Source <$> elements [NoConversion, ExpandVariables] <*> smaller arbitraryFilePath