packages feed

document-build-0.1.0.0: src/Document/Build/Context.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TupleSections #-}
{-# OPTIONS_GHC -Wall #-}

{- | The context that rules run in, and the combinators that make rules and
choose the files they apply to.

A 'Rules' value is a function of the 'Context' of a build, to a 'Tracked'
build step. The build (see "Document.Build.Documents") runs it on every file
of its input directories, then on every file it makes, until it makes no
more; then on every directory of the output directory, and so on, until
nothing new is made; then once more, on the output directory, when the build
is complete. Rules combine with '<>', which runs both:

@
'Document.Build.Rules.pageCountRules' (\<> ".pages")
  <> 'onProject' "group/project" ('Document.Build.Rules.splitPdf' "report.pdf" [("1", "report_cover.pdf")])
@
-}
module Document.Build.Context (
  -- * Directories
  Directories (..),
  outputDirectory,
  inputDirectories,
  fontsDirectory,
  GetDirectories (..),
  HasDirectories (..),
  ReviewDirectories (..),
  AsDirectories (..),

  -- * Targets
  KnownFiles,
  Target (..),
  _TargetFile,
  _TargetDirectory,
  _TargetComplete,
  GetTarget (..),
  HasTarget (..),
  ReviewTarget (..),
  AsTarget (..),

  -- * Context
  Context (..),
  contextDirectories,
  contextProject,
  contextTarget,
  GetContext (..),
  HasContext (..),
  ReviewContext (..),
  AsContext (..),

  -- * Rules
  Rules,
  fromTracked,
  buildStep,
  buildStep',

  -- * Choosing files
  onProject,
  onProjects,
  withProject,
  onPaths,
  onFiles,
  onDirectories,
  onComplete,
  onSources,
  onOutputs,
  afterStep,
  unlessAfterStep,
) where

import Control.DeepSeq (NFData)
import Control.Lens (Field1, Field2, Field3, Getter, Lens', Prism', Review, lens, preview, prism', review, unto, view, (^.), _1, _2, _3, _Wrapped)
import Data.Bool (bool)
import Data.Data (Data)
import Data.Lens.Injection (Injection1 (..), Injection2 (..), Injection3 (..))
import Data.Map (Map)
import Document.Build.Build (Build', Command', io)
import GHC.Generics (Generic)
import System.Directory (doesFileExist)
import System.FilePather.Combinators (isWithin)
import System.FilePather.FilePather (FilePather (..))
import System.FilePather.Tracked (Tracked, step, step', whenPath, whenTrail)
import System.FilePather.Trail (Trail, lastStep, stepName)

{- $setup
>>> import Control.Lens ((^.), (&), (.~), (^?))
-}

{- | The directories of a build: the output directory, which the build removes
and writes to; the input directories, whose files the rules run on, in
order; and the fonts directory, whose fonts LibreOffice uses during the
build, if there is one.

>>> Directories "public" ["src"] Nothing ^. outputDirectory
"public"

>>> (Directories "public" ["src"] Nothing & fontsDirectory .~ Just "fonts") ^. fontsDirectory
Just "fonts"

@since 0.1.0.0
-}
data Directories
  = Directories FilePath [FilePath] (Maybe FilePath)
  deriving stock (Eq, Ord, Show, Generic, Data)
  deriving anyclass (NFData)

-- | @since 0.1.0.0
instance Field1 Directories Directories FilePath FilePath

-- | @since 0.1.0.0
instance Field2 Directories Directories [FilePath] [FilePath]

-- | @since 0.1.0.0
instance Field3 Directories Directories (Maybe FilePath) (Maybe FilePath)

{- | The directory that the build writes to. It is removed at the start of
the build, and every file the rules make must be in it.

@since 0.1.0.0
-}
outputDirectory :: Lens' Directories FilePath
outputDirectory =
  _1
{-# INLINE outputDirectory #-}

{- | The directories whose files the rules run on, in order: when two files
make the same file, the first is kept.

@since 0.1.0.0
-}
inputDirectories :: Lens' Directories [FilePath]
inputDirectories =
  _2
{-# INLINE inputDirectories #-}

{- | The directory of fonts that LibreOffice uses during the build, if there
is one (see 'Document.Build.Project.withFonts').

@since 0.1.0.0
-}
fontsDirectory :: Lens' Directories (Maybe FilePath)
fontsDirectory =
  _3
{-# INLINE fontsDirectory #-}

-- | @since 0.1.0.0
class GetDirectories s where
  getDirectories :: Getter s Directories

-- | @since 0.1.0.0
instance GetDirectories Directories where
  getDirectories =
    id
  {-# INLINE getDirectories #-}

-- | @since 0.1.0.0
class (GetDirectories s) => HasDirectories s where
  {-# MINIMAL setDirectories #-}
  setDirectories :: Directories -> s -> s

  directories :: Lens' s Directories
  directories =
    lens (view getDirectories) (flip setDirectories)
  {-# INLINE directories #-}

-- | @since 0.1.0.0
instance HasDirectories Directories where
  setDirectories =
    const
  {-# INLINE setDirectories #-}

-- | @since 0.1.0.0
class ReviewDirectories t where
  reviewDirectories :: Review t Directories

-- | @since 0.1.0.0
instance ReviewDirectories Directories where
  reviewDirectories =
    unto id
  {-# INLINE reviewDirectories #-}

-- | @since 0.1.0.0
class (ReviewDirectories t) => AsDirectories t where
  {-# MINIMAL matchDirectories #-}
  matchDirectories :: t -> Maybe Directories

  _Directories :: Prism' t Directories
  _Directories =
    prism' (review reviewDirectories) matchDirectories
  {-# INLINE _Directories #-}

-- | @since 0.1.0.0
instance AsDirectories Directories where
  matchDirectories =
    Just
  {-# INLINE matchDirectories #-}

{- | The files that a build knows: the files of its input directories, with
the empty trail, and the files that the rules made, with their trails, by
normalised path.

@since 0.1.0.0
-}
type KnownFiles = Map FilePath Trail

{- | What a build is running the rules on.

@since 0.1.0.0
-}
data Target
  = -- | A file: a file of an input directory, or a file the rules made.
    TargetFile
  | -- | A directory of the output directory.
    TargetDirectory
  | -- | The output directory, once, when the rules make no more files, with
    -- every file the build knows.
    TargetComplete KnownFiles
  deriving stock (Eq, Ord, Show, Generic, Data)
  deriving anyclass (NFData)

-- | @since 0.1.0.0
instance Injection1 Target Target () ()

-- | @since 0.1.0.0
instance Injection2 Target Target () ()

-- | @since 0.1.0.0
instance Injection3 Target Target KnownFiles KnownFiles

-- | @since 0.1.0.0
_TargetFile :: Prism' Target ()
_TargetFile =
  _I1
{-# INLINE _TargetFile #-}

-- | @since 0.1.0.0
_TargetDirectory :: Prism' Target ()
_TargetDirectory =
  _I2
{-# INLINE _TargetDirectory #-}

{- | >>> TargetComplete mempty ^? _TargetComplete
Just (fromList [])

@since 0.1.0.0
-}
_TargetComplete :: Prism' Target KnownFiles
_TargetComplete =
  _I3
{-# INLINE _TargetComplete #-}

-- | @since 0.1.0.0
class GetTarget s where
  getTarget :: Getter s Target

-- | @since 0.1.0.0
instance GetTarget Target where
  getTarget =
    id
  {-# INLINE getTarget #-}

-- | @since 0.1.0.0
class (GetTarget s) => HasTarget s where
  {-# MINIMAL setTarget #-}
  setTarget :: Target -> s -> s

  target :: Lens' s Target
  target =
    lens (view getTarget) (flip setTarget)
  {-# INLINE target #-}

-- | @since 0.1.0.0
instance HasTarget Target where
  setTarget =
    const
  {-# INLINE setTarget #-}

-- | @since 0.1.0.0
class ReviewTarget t where
  reviewTarget :: Review t Target

-- | @since 0.1.0.0
instance ReviewTarget Target where
  reviewTarget =
    unto id
  {-# INLINE reviewTarget #-}

-- | @since 0.1.0.0
class (ReviewTarget t) => AsTarget t where
  {-# MINIMAL matchTarget #-}
  matchTarget :: t -> Maybe Target

  _Target :: Prism' t Target
  _Target =
    prism' (review reviewTarget) matchTarget
  {-# INLINE _Target #-}

-- | @since 0.1.0.0
instance AsTarget Target where
  matchTarget =
    Just
  {-# INLINE matchTarget #-}

{- | The context that rules are run in: the directories of the build, the
path of the project (see 'Document.Build.Project.projectPath'), and what the
rules are running on.

@since 0.1.0.0
-}
data Context
  = Context Directories String Target
  deriving stock (Eq, Ord, Show, Generic, Data)
  deriving anyclass (NFData)

-- | @since 0.1.0.0
instance Field1 Context Context Directories Directories

-- | @since 0.1.0.0
instance Field2 Context Context String String

-- | @since 0.1.0.0
instance Field3 Context Context Target Target

-- | @since 0.1.0.0
contextDirectories :: Lens' Context Directories
contextDirectories =
  _1
{-# INLINE contextDirectories #-}

-- | @since 0.1.0.0
contextProject :: Lens' Context String
contextProject =
  _2
{-# INLINE contextProject #-}

-- | @since 0.1.0.0
contextTarget :: Lens' Context Target
contextTarget =
  _3
{-# INLINE contextTarget #-}

-- | @since 0.1.0.0
class GetContext s where
  getContext :: Getter s Context

-- | @since 0.1.0.0
instance GetContext Context where
  getContext =
    id
  {-# INLINE getContext #-}

-- | @since 0.1.0.0
class (GetContext s) => HasContext s where
  {-# MINIMAL setContext #-}
  setContext :: Context -> s -> s

  context :: Lens' s Context
  context =
    lens (view getContext) (flip setContext)
  {-# INLINE context #-}

-- | @since 0.1.0.0
instance HasContext Context where
  setContext =
    const
  {-# INLINE setContext #-}

-- | @since 0.1.0.0
instance GetDirectories Context where
  getDirectories =
    contextDirectories
  {-# INLINE getDirectories #-}

-- | @since 0.1.0.0
instance HasDirectories Context where
  setDirectories d (Context _ p t) =
    Context d p t
  {-# INLINE setDirectories #-}

-- | @since 0.1.0.0
instance GetTarget Context where
  getTarget =
    contextTarget
  {-# INLINE getTarget #-}

-- | @since 0.1.0.0
instance HasTarget Context where
  setTarget t (Context d p _) =
    Context d p t
  {-# INLINE setTarget #-}

-- | @since 0.1.0.0
class ReviewContext t where
  reviewContext :: Review t Context

-- | @since 0.1.0.0
instance ReviewContext Context where
  reviewContext =
    unto id
  {-# INLINE reviewContext #-}

-- | @since 0.1.0.0
class (ReviewContext t) => AsContext t where
  {-# MINIMAL matchContext #-}
  matchContext :: t -> Maybe Context

  _Context :: Prism' t Context
  _Context =
    prism' (review reviewContext) matchContext
  {-# INLINE _Context #-}

-- | @since 0.1.0.0
instance AsContext Context where
  matchContext =
    Just
  {-# INLINE matchContext #-}

{- | The rules of a build: a function of its context, to the build steps to
run on a file or directory. Rules combine with '<>', which runs both, and
'mempty' is no rules.

@since 0.1.0.0
-}
type Rules = Context -> Tracked Build' ()

{- | Rules that do not depend on the context.

@since 0.1.0.0
-}
fromTracked :: Tracked Build' () -> Rules
fromTracked =
  const
{-# INLINE fromTracked #-}

{- | A named step that runs a command, which makes the files given by a
function of its input file. See 'System.FilePather.Tracked.step'.

The command is not run if one of the files already exists, because the build
removes the output directory first, so another step made it in this build:
the first step to make a file keeps it.

@since 0.1.0.0
-}
buildStep :: (Monoid a) => String -> (FilePath -> [FilePath]) -> Command' a -> Tracked Build' a
buildStep n outs c =
  step n outs $
    FilePather $ \p ->
      io (or <$> traverse doesFileExist (outs p))
        >>= bool (view _Wrapped c p) (pure mempty)

{- | A named step that runs a command, which returns the files it made. See
'System.FilePather.Tracked.step''.

@since 0.1.0.0
-}
buildStep' :: String -> Command' [FilePath] -> Tracked Build' ()
buildStep' n c =
  step' n (fmap (,()) c)

{- | Rules for one project, by its path, such as @"group/project"@.

@since 0.1.0.0
-}
onProject :: String -> Rules -> Rules
onProject p =
  onProjects (== p)
{-# INLINE onProject #-}

{- | Rules for the projects whose paths satisfy a predicate.

@since 0.1.0.0
-}
onProjects :: (String -> Bool) -> Rules -> Rules
onProjects f r c =
  bool mempty (r c) (f (c ^. contextProject))
{-# INLINE onProjects #-}

{- | Rules that depend on the path of the project, such as rules with
different placeholders for some projects.

@since 0.1.0.0
-}
withProject :: (String -> Rules) -> Rules
withProject f c =
  f (c ^. contextProject) c
{-# INLINE withProject #-}

{- | Rules for the files and directories whose paths satisfy a predicate.

@since 0.1.0.0
-}
onPaths :: (FilePath -> Bool) -> Rules -> Rules
onPaths f r =
  whenPath f . r
{-# INLINE onPaths #-}

{- | Rules for files: the files of the input directories, and the files that
rules make.

@since 0.1.0.0
-}
onFiles :: Rules -> Rules
onFiles r c =
  maybe mempty (const (r c)) (preview (contextTarget . _TargetFile) c)
{-# INLINE onFiles #-}

{- | Rules for the directories of the output directory, which run after the
rules for files have made every file they can.

@since 0.1.0.0
-}
onDirectories :: Rules -> Rules
onDirectories r c =
  maybe mempty (const (r c)) (preview (contextTarget . _TargetDirectory) c)
{-# INLINE onDirectories #-}

{- | Rules that run once, on the output directory, when the rules make no
more files, given every file the build knows. The files they make are not
given to any rules.

@since 0.1.0.0
-}
onComplete :: (KnownFiles -> Rules) -> Rules
onComplete r c =
  maybe mempty (`r` c) (preview (contextTarget . _TargetComplete) c)
{-# INLINE onComplete #-}

{- | Rules for the files of the input directories only: files with the empty
trail.

@since 0.1.0.0
-}
onSources :: Rules -> Rules
onSources r =
  whenTrail (null . preview lastStep) . r
{-# INLINE onSources #-}

{- | Rules for the files and directories in the output directory. Every rule
that makes files from files the rules made, rather than from the files of the
input directories, should apply only to these, so that the input directories
are not changed.

@since 0.1.0.0
-}
onOutputs :: Rules -> Rules
onOutputs r c =
  whenPath (isWithin (c ^. contextDirectories . outputDirectory)) (r c)
{-# INLINE onOutputs #-}

{- | Rules for files made by a step whose name satisfies a predicate.

@since 0.1.0.0
-}
afterStep :: (String -> Bool) -> Rules -> Rules
afterStep f r =
  whenTrail (any f . preview (lastStep . stepName)) . r
{-# INLINE afterStep #-}

{- | Rules for the files of the input directories, and files made by a step
whose name does not satisfy a predicate.

@since 0.1.0.0
-}
unlessAfterStep :: (String -> Bool) -> Rules -> Rules
unlessAfterStep f r =
  whenTrail (not . any f . preview (lastStep . stepName)) . r
{-# INLINE unlessAfterStep #-}