packages feed

filepather-0.7.0.0: src/System/FilePather/Trail.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# OPTIONS_GHC -Wall #-}

{- | How a file came to be built, and why a chain of build steps is stopped.

A 'Step' is one application of a named build step to a file. A 'Trail' is the
steps that built a file, from the file back to its source file. A 'Cycle' is
the reason a chain of steps is stopped: a step would make a file that is
already in the chain, or a step is already in the chain.
-}
module System.FilePather.Trail (
  -- * Steps
  Step (..),
  stepName,
  stepInput,
  GetStep (..),
  HasStep (..),
  ReviewStep (..),
  AsStep (..),

  -- * Trails
  Trail (..),
  trailFiles,
  lastStep,
  GetTrail (..),
  HasTrail (..),
  ReviewTrail (..),
  AsTrail (..),

  -- * Repetition
  Repetition (..),
  _Once,
  _Repeatable,
  GetRepetition (..),
  HasRepetition (..),
  ReviewRepetition (..),
  AsRepetition (..),

  -- * Cycles
  Cycle (..),
  _ReproducesInput,
  _RepeatsStep,
  cycleStep,
  cycleTrail,
  checkStep,
  displayCycle,
  GetCycle (..),
  HasCycle (..),
  ReviewCycle (..),
  AsCycle (..),
) where

import Control.DeepSeq (NFData)
import Control.Lens (AsEmpty (..), Cons (..), Each (..), Field1, Field2, Getter, Index, IxValue, Ixed (..), Lens', Prism', Reversing (..), Review, Rewrapped, Snoc (..), Traversal', Wrapped (..), iso, lens, nearly, prism, prism', review, toListOf, unsnoc, unto, view, _1, _2, _Wrapped, _head)
import Data.Data (Data)
import Data.Lens.Injection (Injection1 (..), Injection2 (..))
import Data.List (find, intercalate)
import GHC.Generics (Generic)
import System.FilePath (normalise)

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

{- | One application of a named build step to a file: the name of the step,
such as @"pdf page 1"@, and its input file.

>>> Step "pdf page 1" "public/a.pdf" ^. stepName
"pdf page 1"

@since 0.7.0.0
-}
data Step
  = Step String FilePath
  deriving stock (Eq, Ord, Show, Generic, Data)
  deriving anyclass (NFData)

-- | @since 0.7.0.0
instance Field1 Step Step String String

-- | @since 0.7.0.0
instance Field2 Step Step FilePath FilePath

{- | The name of a step.

@since 0.7.0.0
-}
stepName :: Lens' Step String
stepName =
  _1
{-# INLINE stepName #-}

{- | The input file of a step.

>>> Step "pdf page 1" "public/a.pdf" ^. stepInput
"public/a.pdf"

@since 0.7.0.0
-}
stepInput :: Lens' Step FilePath
stepInput =
  _2
{-# INLINE stepInput #-}

-- | @since 0.7.0.0
class GetStep s where
  getStep :: Getter s Step

-- | @since 0.7.0.0
instance GetStep Step where
  getStep =
    id
  {-# INLINE getStep #-}

-- | @since 0.7.0.0
class (GetStep s) => HasStep s where
  {-# MINIMAL setStep #-}
  setStep :: Step -> s -> s

  stepLens :: Lens' s Step
  stepLens =
    lens (view getStep) (flip setStep)
  {-# INLINE stepLens #-}

-- | @since 0.7.0.0
instance HasStep Step where
  setStep =
    const
  {-# INLINE setStep #-}

-- | @since 0.7.0.0
class ReviewStep t where
  reviewStep :: Review t Step

-- | @since 0.7.0.0
instance ReviewStep Step where
  reviewStep =
    unto id
  {-# INLINE reviewStep #-}

-- | @since 0.7.0.0
class (ReviewStep t) => AsStep t where
  {-# MINIMAL matchStep #-}
  matchStep :: t -> Maybe Step

  _Step :: Prism' t Step
  _Step =
    prism' (review reviewStep) matchStep
  {-# INLINE _Step #-}

-- | @since 0.7.0.0
instance AsStep Step where
  matchStep =
    Just
  {-# INLINE matchStep #-}

{- | The steps that built a file, nearest first: the step that made the file,
then the step that made its input, and so on, back to the step whose input
is a source file. A source file has the empty trail.

@since 0.7.0.0
-}
newtype Trail
  = Trail [Step]
  deriving stock (Eq, Ord, Show, Generic, Data)
  deriving newtype (NFData, Semigroup, Monoid)

-- | @since 0.7.0.0
instance Wrapped Trail where
  type Unwrapped Trail = [Step]
  _Wrapped' =
    iso (\(Trail x) -> x) Trail
  {-# INLINE _Wrapped' #-}

-- | @since 0.7.0.0
instance (t ~ Trail) => Rewrapped Trail t

-- | @since 0.7.0.0
instance Cons Trail Trail Step Step where
  _Cons =
    prism
      (\(s, Trail ss) -> Trail (s : ss))
      ( \(Trail x) ->
          case x of
            [] -> Left (Trail [])
            s : ss -> Right (s, Trail ss)
      )
  {-# INLINE _Cons #-}

-- | @since 0.7.0.0
instance Snoc Trail Trail Step Step where
  _Snoc =
    prism
      (\(Trail ss, s) -> Trail (ss <> [s]))
      (\t@(Trail x) -> maybe (Left t) (\(ss, s) -> Right (Trail ss, s)) (unsnoc x))
  {-# INLINE _Snoc #-}

-- | @since 0.7.0.0
instance AsEmpty Trail where
  _Empty =
    nearly (Trail []) (\(Trail x) -> null x)
  {-# INLINE _Empty #-}

-- | @since 0.7.0.0
instance Each Trail Trail Step Step where
  each =
    _Wrapped . traverse
  {-# INLINE each #-}

-- | @since 0.7.0.0
type instance Index Trail = Int

-- | @since 0.7.0.0
type instance IxValue Trail = Step

-- | @since 0.7.0.0
instance Ixed Trail where
  ix i =
    _Wrapped . ix i
  {-# INLINE ix #-}

-- | @since 0.7.0.0
instance Reversing Trail where
  reversing (Trail x) =
    Trail (reverse x)
  {-# INLINE reversing #-}

{- | The input files of the steps of a trail, nearest first.

>>> Trail [Step "pdf page 1" "public/a.pdf", Step "markdown to pdf" "src/a.md"] ^.. trailFiles
["public/a.pdf","src/a.md"]

@since 0.7.0.0
-}
trailFiles :: Traversal' Trail FilePath
trailFiles =
  each . stepInput
{-# INLINE trailFiles #-}

{- | The last step of a trail: the step that made the file. A source file has
no last step.

>>> Trail [Step "pdf page 1" "public/a.pdf", Step "markdown to pdf" "src/a.md"] ^? lastStep . stepName
Just "pdf page 1"

>>> Trail [] ^? lastStep
Nothing

@since 0.7.0.0
-}
lastStep :: Traversal' Trail Step
lastStep =
  _Wrapped . _head
{-# INLINE lastStep #-}

-- | @since 0.7.0.0
class GetTrail s where
  getTrail :: Getter s Trail

-- | @since 0.7.0.0
instance GetTrail Trail where
  getTrail =
    id
  {-# INLINE getTrail #-}

-- | @since 0.7.0.0
class (GetTrail s) => HasTrail s where
  {-# MINIMAL setTrail #-}
  setTrail :: Trail -> s -> s

  trail :: Lens' s Trail
  trail =
    lens (view getTrail) (flip setTrail)
  {-# INLINE trail #-}

-- | @since 0.7.0.0
instance HasTrail Trail where
  setTrail =
    const
  {-# INLINE setTrail #-}

-- | @since 0.7.0.0
class ReviewTrail t where
  reviewTrail :: Review t Trail

-- | @since 0.7.0.0
instance ReviewTrail Trail where
  reviewTrail =
    unto id
  {-# INLINE reviewTrail #-}

-- | @since 0.7.0.0
class (ReviewTrail t) => AsTrail t where
  {-# MINIMAL matchTrail #-}
  matchTrail :: t -> Maybe Trail

  _Trail :: Prism' t Trail
  _Trail =
    prism' (review reviewTrail) matchTrail
  {-# INLINE _Trail #-}

-- | @since 0.7.0.0
instance AsTrail Trail where
  matchTrail =
    Just
  {-# INLINE matchTrail #-}

{- | Whether a step may appear more than once in a trail. A step that may
not is stopped with 'RepeatsStep' when it is already in the trail.

@since 0.7.0.0
-}
data Repetition
  = -- | The step may appear once in a trail.
    Once
  | -- | The step may appear any number of times in a trail.
    Repeatable
  deriving stock (Eq, Ord, Show, Generic, Data)
  deriving anyclass (NFData)

-- | @since 0.7.0.0
instance Injection1 Repetition Repetition () ()

-- | @since 0.7.0.0
instance Injection2 Repetition Repetition () ()

-- | @since 0.7.0.0
_Once :: Prism' Repetition ()
_Once =
  _I1
{-# INLINE _Once #-}

-- | @since 0.7.0.0
_Repeatable :: Prism' Repetition ()
_Repeatable =
  _I2
{-# INLINE _Repeatable #-}

-- | @since 0.7.0.0
class GetRepetition s where
  getRepetition :: Getter s Repetition

-- | @since 0.7.0.0
instance GetRepetition Repetition where
  getRepetition =
    id
  {-# INLINE getRepetition #-}

-- | @since 0.7.0.0
class (GetRepetition s) => HasRepetition s where
  {-# MINIMAL setRepetition #-}
  setRepetition :: Repetition -> s -> s

  repetition :: Lens' s Repetition
  repetition =
    lens (view getRepetition) (flip setRepetition)
  {-# INLINE repetition #-}

-- | @since 0.7.0.0
instance HasRepetition Repetition where
  setRepetition =
    const
  {-# INLINE setRepetition #-}

-- | @since 0.7.0.0
class ReviewRepetition t where
  reviewRepetition :: Review t Repetition

-- | @since 0.7.0.0
instance ReviewRepetition Repetition where
  reviewRepetition =
    unto id
  {-# INLINE reviewRepetition #-}

-- | @since 0.7.0.0
class (ReviewRepetition t) => AsRepetition t where
  {-# MINIMAL matchRepetition #-}
  matchRepetition :: t -> Maybe Repetition

  _Repetition :: Prism' t Repetition
  _Repetition =
    prism' (review reviewRepetition) matchRepetition
  {-# INLINE _Repetition #-}

-- | @since 0.7.0.0
instance AsRepetition Repetition where
  matchRepetition =
    Just
  {-# INLINE matchRepetition #-}

{- | Why a chain of build steps is stopped, with the step that is stopped and
the trail of its input file.

@since 0.7.0.0
-}
data Cycle
  = -- | The step would make a file that is already in the chain: its input
    -- file, or an input file of a step in the trail.
    ReproducesInput FilePath Step Trail
  | -- | The step is already in the trail.
    RepeatsStep Step Trail
  deriving stock (Eq, Ord, Show, Generic, Data)
  deriving anyclass (NFData)

-- | @since 0.7.0.0
instance Injection1 Cycle Cycle (FilePath, Step, Trail) (FilePath, Step, Trail)

-- | @since 0.7.0.0
instance Injection2 Cycle Cycle (Step, Trail) (Step, Trail)

{- | >>> _RepeatsStep # (Step "pdf page 1" "a_page1.pdf", Trail [])
RepeatsStep (Step "pdf page 1" "a_page1.pdf") (Trail [])

@since 0.7.0.0
-}
_ReproducesInput :: Prism' Cycle (FilePath, Step, Trail)
_ReproducesInput =
  _I1
{-# INLINE _ReproducesInput #-}

-- | @since 0.7.0.0
_RepeatsStep :: Prism' Cycle (Step, Trail)
_RepeatsStep =
  _I2
{-# INLINE _RepeatsStep #-}

{- | The step that is stopped.

@since 0.7.0.0
-}
cycleStep :: Lens' Cycle Step
cycleStep =
  lens
    ( \case
        ReproducesInput _ s _ -> s
        RepeatsStep s _ -> s
    )
    ( \c s ->
        case c of
          ReproducesInput o _ t -> ReproducesInput o s t
          RepeatsStep _ t -> RepeatsStep s t
    )
{-# INLINE cycleStep #-}

{- | The trail of the input file of the step that is stopped.

@since 0.7.0.0
-}
cycleTrail :: Lens' Cycle Trail
cycleTrail =
  lens
    ( \case
        ReproducesInput _ _ t -> t
        RepeatsStep _ t -> t
    )
    ( \c t ->
        case c of
          ReproducesInput o s _ -> ReproducesInput o s t
          RepeatsStep s _ -> RepeatsStep s t
    )
{-# INLINE cycleTrail #-}

{- | Check a step, with the files it makes, against the trail of its input
file. A step that is already in the trail, and may appear 'Once', is a
'RepeatsStep'. Otherwise, a step that makes its input file, or the input file
of a step in the trail, is a 'ReproducesInput'. File paths are compared after
normalisation.

>>> let t = Trail [Step "markdown to pdf" "src/a.md"]
>>> checkStep Once (Step "pdf page 1" "public/a.pdf") ["public/a_page1.pdf"] t
Right ()

>>> checkStep Once (Step "markdown to pdf" "public/a.pdf") ["public/b.pdf"] t
Left (RepeatsStep (Step "markdown to pdf" "public/a.pdf") (Trail [Step "markdown to pdf" "src/a.md"]))

>>> checkStep Repeatable (Step "markdown to pdf" "public/a.pdf") ["public/b.pdf"] t
Right ()

>>> checkStep Once (Step "pdf to markdown" "public/a.pdf") ["./src/a.md"] t
Left (ReproducesInput "./src/a.md" (Step "pdf to markdown" "public/a.pdf") (Trail [Step "markdown to pdf" "src/a.md"]))

@since 0.7.0.0
-}
checkStep :: Repetition -> Step -> [FilePath] -> Trail -> Either Cycle ()
checkStep r s@(Step n i) os t =
  let repeated =
        case r of
          Once -> n `elem` toListOf (each . stepName) t
          Repeatable -> False
      files = fmap normalise (i : toListOf trailFiles t)
   in if repeated
        then Left (RepeatsStep s t)
        else maybe (Right ()) (\o -> Left (ReproducesInput o s t)) (find ((`elem` files) . normalise) os)

{- | A description of a cycle, for people to read: the chain of files and
steps from the source file.

>>> putStrLn (displayCycle (RepeatsStep (Step "pdf page 1" "public/a_page1.pdf") (Trail [Step "pdf page 1" "public/a.pdf", Step "markdown to pdf" "src/a.md"])))
src/a.md → markdown to pdf → public/a.pdf → pdf page 1 → public/a_page1.pdf → pdf page 1 again

>>> putStrLn (displayCycle (ReproducesInput "src/a.md" (Step "pdf to markdown" "public/a.pdf") (Trail [Step "markdown to pdf" "src/a.md"])))
src/a.md → markdown to pdf → public/a.pdf → pdf to markdown → src/a.md, which is already in the chain

@since 0.7.0.0
-}
displayCycle :: Cycle -> String
displayCycle c =
  let chain (Trail ss) = concatMap (\(Step n i) -> [i, n]) (reverse ss)
      arrows = intercalate " → "
   in case c of
        RepeatsStep (Step n i) t ->
          arrows (chain t <> [i, n <> " again"])
        ReproducesInput o (Step n i) t ->
          arrows (chain t <> [i, n, o]) <> ", which is already in the chain"

-- | @since 0.7.0.0
class GetCycle s where
  getCycle :: Getter s Cycle

-- | @since 0.7.0.0
instance GetCycle Cycle where
  getCycle =
    id
  {-# INLINE getCycle #-}

-- | @since 0.7.0.0
class (GetCycle s) => HasCycle s where
  {-# MINIMAL setCycle #-}
  setCycle :: Cycle -> s -> s

  cycleLens :: Lens' s Cycle
  cycleLens =
    lens (view getCycle) (flip setCycle)
  {-# INLINE cycleLens #-}

-- | @since 0.7.0.0
instance HasCycle Cycle where
  setCycle =
    const
  {-# INLINE setCycle #-}

-- | @since 0.7.0.0
class ReviewCycle t where
  reviewCycle :: Review t Cycle

-- | @since 0.7.0.0
instance ReviewCycle Cycle where
  reviewCycle =
    unto id
  {-# INLINE reviewCycle #-}

-- | @since 0.7.0.0
class (ReviewCycle t) => AsCycle t where
  {-# MINIMAL matchCycle #-}
  matchCycle :: t -> Maybe Cycle

  _Cycle :: Prism' t Cycle
  _Cycle =
    prism' (review reviewCycle) matchCycle
  {-# INLINE _Cycle #-}

-- | @since 0.7.0.0
instance AsCycle Cycle where
  matchCycle =
    Just
  {-# INLINE matchCycle #-}