Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fancy generic templating stuff for nested data #620

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions lib/Hakyll/Core/Compiler.hs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ module Hakyll.Core.Compiler
, cached
, unsafeCompiler
, debugCompiler
, failBranch
, mapError
) where


--------------------------------------------------------------------------------
import Control.Monad (when, unless)
import Control.Monad (when, unless, (>=>))
import Data.Binary (Binary)
import Data.ByteString.Lazy (ByteString)
import Data.Typeable (Typeable)
Expand Down Expand Up @@ -62,6 +64,7 @@ getUnderlyingExtension = takeExtension . toFilePath <$> getUnderlying


--------------------------------------------------------------------------------
-- | Create an item from the underlying identifier and a given value.
makeItem :: a -> Compiler (Item a)
makeItem x = do
identifier <- getUnderlying
Expand Down Expand Up @@ -141,6 +144,10 @@ saveSnapshot snapshot item = do


--------------------------------------------------------------------------------
-- | Turn on caching for a compilation value to avoid recomputing it
-- on subsequent Hakyll runs.
-- The storage key consists of the underlying identifier of the compiled
-- ressource and the given name.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: resource

cached :: (Binary a, Typeable a)
=> String
-> Compiler a
Expand Down Expand Up @@ -177,12 +184,34 @@ cached name compiler = do


--------------------------------------------------------------------------------
-- | Run an IO computation without dependencies in a Compiler
unsafeCompiler :: IO a -> Compiler a
unsafeCompiler = compilerUnsafeIO


--------------------------------------------------------------------------------
-- | Compiler for debugging purposes
-- | Fail so that it is treated as non-defined in an @\$if()\$@ branching
-- "Hakyll.Web.Template" macro, and alternative
-- 'Hakyll.Web.Template.Context.Context's are tried
--
-- @since 4.12.0
failBranch :: String -> Compiler a
failBranch = compilerFailBranch . return


--------------------------------------------------------------------------------
-- | Map over the error list from a failed compilation.
-- Unlike @\``Control.Monad.Except.catchError`\` ('Control.Monad.Except.throwError' . f)@,
-- it keeps the distinction between 'fail' and 'failBranch'.
--
-- @since 4.12.0
mapError :: ([String] -> [String]) -> Compiler a -> Compiler a
mapError f = compilerTry >=> either (compilerResult . CompilerError . fmap f) return


--------------------------------------------------------------------------------
-- | Compiler for debugging purposes.
-- Passes a message to the debug logger that is printed in verbose mode.
debugCompiler :: String -> Compiler ()
debugCompiler msg = do
logger <- compilerLogger <$> compilerAsk
Expand Down
157 changes: 116 additions & 41 deletions lib/Hakyll/Core/Compiler/Internal.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,33 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE MultiParamTypeClasses #-}
module Hakyll.Core.Compiler.Internal
( -- * Types
Snapshot
, CompilerRead (..)
, CompilerWrite (..)
, Reason (..)
, CompilerResult (..)
, Compiler (..)
, runCompiler

-- * Core operations
, compilerResult
, compilerTell
, compilerAsk
, compilerUnsafeIO

-- * Error operations
, compilerThrow
, compilerFailBranch
, compilerCatch
, compilerResult
, compilerUnsafeIO
, compilerTry
, getReason

-- * Utilities
, compilerDebugEntries
, compilerTellDependencies
, compilerTellCacheHits
) where
Expand All @@ -32,7 +40,7 @@ module Hakyll.Core.Compiler.Internal
import Control.Applicative (Alternative (..))
import Control.Exception (SomeException, handle)
import Control.Monad (forM_)
import Control.Monad.Except (MonadError (..))
import Control.Monad.Except (MonadError (..))
#if MIN_VERSION_base(4,9,0)
import Data.Semigroup (Semigroup (..))
#endif
Expand All @@ -45,7 +53,6 @@ import Hakyll.Core.Configuration
import Hakyll.Core.Dependencies
import Hakyll.Core.Identifier
import Hakyll.Core.Identifier.Pattern
import Hakyll.Core.Logger (Logger)
import qualified Hakyll.Core.Logger as Logger
import Hakyll.Core.Metadata
import Hakyll.Core.Provider
Expand Down Expand Up @@ -75,7 +82,7 @@ data CompilerRead = CompilerRead
, -- | Compiler store
compilerStore :: Store
, -- | Logger
compilerLogger :: Logger
compilerLogger :: Logger.Logger
}


Expand Down Expand Up @@ -104,11 +111,28 @@ instance Monoid CompilerWrite where


--------------------------------------------------------------------------------
data CompilerResult a where
CompilerDone :: a -> CompilerWrite -> CompilerResult a
CompilerSnapshot :: Snapshot -> Compiler a -> CompilerResult a
CompilerError :: [String] -> CompilerResult a
CompilerRequire :: (Identifier, Snapshot) -> Compiler a -> CompilerResult a
-- | Distinguishes reasons in a 'CompilerError'
data Reason a
-- | An exception occured during compilation
= CompilationFailure a
-- | Absence of any result, most notably in template contexts
| NoCompilationResult a
deriving Functor


-- | Unwrap a `Reason`
getReason :: Reason a -> a
getReason (CompilationFailure x) = x
getReason (NoCompilationResult x) = x


--------------------------------------------------------------------------------
-- | An intermediate result of a compilation step
data CompilerResult a
= CompilerDone a CompilerWrite
| CompilerSnapshot Snapshot (Compiler a)
| CompilerRequire (Identifier, Snapshot) (Compiler a)
| CompilerError (Reason [String])


--------------------------------------------------------------------------------
Expand All @@ -126,14 +150,14 @@ instance Functor Compiler where
return $ case res of
CompilerDone x w -> CompilerDone (f x) w
CompilerSnapshot s c' -> CompilerSnapshot s (fmap f c')
CompilerError e -> CompilerError e
CompilerRequire i c' -> CompilerRequire i (fmap f c')
CompilerError e -> CompilerError e
{-# INLINE fmap #-}


--------------------------------------------------------------------------------
instance Monad Compiler where
return x = Compiler $ \_ -> return $ CompilerDone x mempty
return x = compilerResult $ CompilerDone x mempty
{-# INLINE return #-}

Compiler c >>= f = Compiler $ \r -> do
Expand All @@ -146,14 +170,14 @@ instance Monad Compiler where
CompilerSnapshot s c' -> CompilerSnapshot s $ do
compilerTell w -- Save dependencies!
c'
CompilerError e -> CompilerError e
CompilerRequire i c' -> CompilerRequire i $ do
compilerTell w -- Save dependencies!
c'
CompilerError e -> CompilerError e

CompilerSnapshot s c' -> return $ CompilerSnapshot s (c' >>= f)
CompilerError e -> return $ CompilerError e
CompilerRequire i c' -> return $ CompilerRequire i (c' >>= f)
CompilerError e -> return $ CompilerError e
{-# INLINE (>>=) #-}

fail = compilerThrow . return
Expand All @@ -170,87 +194,138 @@ instance Applicative Compiler where


--------------------------------------------------------------------------------
-- | Access provided metadata from anywhere
instance MonadMetadata Compiler where
getMetadata = compilerGetMetadata
getMatches = compilerGetMatches


--------------------------------------------------------------------------------
-- | Compilation may fail with multiple error messages.
-- 'catchError' handles errors from 'throwError', 'fail' and 'Hakyll.Core.Compiler.failBranch'
instance MonadError [String] Compiler where
throwError = compilerThrow
catchError = compilerCatch
throwError = compilerThrow
catchError c = compilerCatch c . (. getReason)


--------------------------------------------------------------------------------
-- | Like 'unCompiler' but treating IO exceptions as 'CompilerError's
runCompiler :: Compiler a -> CompilerRead -> IO (CompilerResult a)
runCompiler compiler read' = handle handler $ unCompiler compiler read'
where
handler :: SomeException -> IO (CompilerResult a)
handler e = return $ CompilerError [show e]
handler e = return $ CompilerError $ CompilationFailure [show e]


--------------------------------------------------------------------------------
-- | Trying alternative compilers if the first fails, regardless whether through
-- 'fail', 'throwError' or 'Hakyll.Core.Compiler.failBranch'.
-- Aggregates error messages if all fail.
instance Alternative Compiler where
empty = compilerThrow []
x <|> y = compilerCatch x $ \es -> do
logger <- compilerLogger <$> compilerAsk
forM_ es $ \e -> compilerUnsafeIO $ Logger.debug logger $
"Hakyll.Core.Compiler.Internal: Alternative failed: " ++ e
y
empty = compilerFailBranch []
x <|> y = x `compilerCatch` (\rx -> y `compilerCatch` (\ry ->
case (rx, ry) of
(CompilationFailure xs, CompilationFailure ys) -> compilerThrow $ xs ++ ys
(CompilationFailure xs, NoCompilationResult ys) -> debug ys >> compilerThrow xs
(NoCompilationResult xs, CompilationFailure ys) -> debug xs >> compilerThrow ys
(NoCompilationResult xs, NoCompilationResult ys) -> compilerFailBranch $ xs ++ ys
))
where
debug = compilerDebugEntries "Hakyll.Core.Compiler.Internal: Alternative fail suppressed"
{-# INLINE (<|>) #-}


--------------------------------------------------------------------------------
-- | Put the result back in a compiler
compilerResult :: CompilerResult a -> Compiler a
compilerResult x = Compiler $ \_ -> return x
{-# INLINE compilerResult #-}


--------------------------------------------------------------------------------
-- | Get the current environment
compilerAsk :: Compiler CompilerRead
compilerAsk = Compiler $ \r -> return $ CompilerDone r mempty
{-# INLINE compilerAsk #-}


--------------------------------------------------------------------------------
-- | Put a 'CompilerWrite'
compilerTell :: CompilerWrite -> Compiler ()
compilerTell deps = Compiler $ \_ -> return $ CompilerDone () deps
compilerTell = compilerResult . CompilerDone ()
{-# INLINE compilerTell #-}


--------------------------------------------------------------------------------
-- | Run an IO computation without dependencies in a Compiler
compilerUnsafeIO :: IO a -> Compiler a
compilerUnsafeIO io = Compiler $ \_ -> do
x <- io
return $ CompilerDone x mempty
{-# INLINE compilerUnsafeIO #-}


--------------------------------------------------------------------------------
-- | Put a 'CompilerError' with multiple error messages as 'CompilationFailure'
compilerThrow :: [String] -> Compiler a
compilerThrow es = Compiler $ \_ -> return $ CompilerError es
{-# INLINE compilerThrow #-}
compilerThrow = compilerResult . CompilerError . CompilationFailure


-- | Put a 'CompilerError' with multiple messages as 'NoCompilationResult'
compilerFailBranch :: [String] -> Compiler a
compilerFailBranch = compilerResult . CompilerError . NoCompilationResult


--------------------------------------------------------------------------------
compilerCatch :: Compiler a -> ([String] -> Compiler a) -> Compiler a
-- | Allows to distinguish 'CompilerError's and branch on them with 'Either'
--
-- prop> compilerTry = (`compilerCatch` return . Left) . fmap Right
compilerTry :: Compiler a -> Compiler (Either (Reason [String]) a)
compilerTry (Compiler x) = Compiler $ \r -> do
res <- x r
case res of
CompilerDone res' w -> return (CompilerDone (Right res') w)
CompilerSnapshot s c -> return (CompilerSnapshot s (compilerTry c))
CompilerRequire i c -> return (CompilerRequire i (compilerTry c))
CompilerError e -> return (CompilerDone (Left e) mempty)
{-# INLINE compilerTry #-}


--------------------------------------------------------------------------------
-- | Allows you to recover from 'CompilerError's.
-- Uses the same parameter order as 'catchError' so that it can be used infix.
--
-- prop> c `compilerCatch` f = compilerTry c >>= either f return
compilerCatch :: Compiler a -> (Reason [String] -> Compiler a) -> Compiler a
compilerCatch (Compiler x) f = Compiler $ \r -> do
res <- x r
case res of
CompilerDone res' w -> return (CompilerDone res' w)
CompilerSnapshot s c -> return (CompilerSnapshot s (compilerCatch c f))
CompilerError e -> unCompiler (f e) r
CompilerRequire i c -> return (CompilerRequire i (compilerCatch c f))
CompilerError e -> unCompiler (f e) r
{-# INLINE compilerCatch #-}


--------------------------------------------------------------------------------
-- | Put the result back in a compiler
compilerResult :: CompilerResult a -> Compiler a
compilerResult x = Compiler $ \_ -> return x
{-# INLINE compilerResult #-}

compilerDebugLog :: [String] -> Compiler ()
compilerDebugLog ms = do
logger <- compilerLogger <$> compilerAsk
compilerUnsafeIO $ forM_ ms $ Logger.debug logger

--------------------------------------------------------------------------------
compilerUnsafeIO :: IO a -> Compiler a
compilerUnsafeIO io = Compiler $ \_ -> do
x <- io
return $ CompilerDone x mempty
{-# INLINE compilerUnsafeIO #-}
-- | Pass a list of messages with a heading to the debug logger
compilerDebugEntries :: String -> [String] -> Compiler ()
compilerDebugEntries msg = compilerDebugLog . (msg:) . map indent
where
indent = unlines . map (" "++) . lines


--------------------------------------------------------------------------------
compilerTellDependencies :: [Dependency] -> Compiler ()
compilerTellDependencies ds = do
logger <- compilerLogger <$> compilerAsk
forM_ ds $ \d -> compilerUnsafeIO $ Logger.debug logger $
"Hakyll.Core.Compiler.Internal: Adding dependency: " ++ show d
compilerDebugLog $ map (\d ->
"Hakyll.Core.Compiler.Internal: Adding dependency: " ++ show d) ds
compilerTell mempty {compilerDependencies = ds}
{-# INLINE compilerTellDependencies #-}

Expand Down
2 changes: 2 additions & 0 deletions lib/Hakyll/Core/Compiler/Require.hs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ loadBody id' = loadSnapshotBody id' final


--------------------------------------------------------------------------------
-- | A shortcut for only requiring the body for a specific snapshot of an item
loadSnapshotBody :: (Binary a, Typeable a)
=> Identifier -> Snapshot -> Compiler a
loadSnapshotBody id' snapshot = fmap itemBody $ loadSnapshot id' snapshot
Expand All @@ -103,6 +104,7 @@ loadAll pattern = loadAllSnapshots pattern final


--------------------------------------------------------------------------------
-- | Load a specific snapshot for each of dynamic list of items
loadAllSnapshots :: (Binary a, Typeable a)
=> Pattern -> Snapshot -> Compiler [Item a]
loadAllSnapshots pattern snapshot = do
Expand Down
Loading