packages feed

dirforest-0.1.0.0: src/Data/DirForest.hs

{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | A directory forest of files associated with values
--
-- This module is meant to be imported like this:
--
-- > import Data.DirForest (DirForest)
-- > import qualified Data.DirForest as DF
module Data.DirForest
  ( -- * Dirforest types
    DirTree (..),
    DirForest (..),
    InsertionError (..),
    FOD (..),

    -- * Comparisons
    eq1DirTree,
    ord1DirTree,
    eq1DirForest,
    ord1DirForest,

    -- * Query
    null,
    nullFiles,
    lookup,

    -- * Construction
    empty,
    singletonFile,
    singletonDir,
    insertFile,
    insertDir,

    -- * Traversal
    mapWithPath,
    traverseWithPath,
    traverseWithPath_,

    -- * Pruning
    pruneEmptyDirs,
    anyEmptyDir,

    -- * Conversion

    -- ** Map
    fromFileMap,
    toFileMap,
    fromMap,
    toMap,

    -- ** List
    fromFileList,
    toFileList,

    -- * IO

    -- ** Read
    read,
    readNonHidden,
    readFiltered,
    readNonHiddenFiltered,
    readOneLevel,
    readOneLevelNonHidden,
    readOneLevelFiltered,
    readOneLevelNonHiddenFiltered,
    hiddenRel,

    -- ** Write
    write,

    -- * Combinations

    -- ** Union
    InsertValidation (..),
    unpackInsertValidation,
    union,
    unionWith,
    unionWithKey,
    unions,

    -- ** Intersection
    intersection,
    intersectionWith,
    intersectionWithKey,
    intersections,

    -- ** Difference
    difference,
    differenceWith,
    differenceWithKey,

    -- * Filter
    filter,
    filterWithKey,
    filterHidden,
  )
where

import Autodocodec
import Control.Arrow (left)
import Control.DeepSeq
import Control.Monad
import Control.Monad.IO.Class
import Data.Aeson (FromJSON, ToJSON)
import Data.Functor.Classes
import Data.Functor.Identity
import Data.List (foldl')
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NE
import Data.Map (Map)
import qualified Data.Map as M
import Data.Maybe
import Data.Validity
import Data.Validity.Containers ()
import Data.Validity.Map
import Data.Validity.Path ()
import GHC.Generics (Generic)
import Path
import Path.IO
import Path.Internal
import qualified System.FilePath as FP
import Prelude hiding (filter, lookup, null, read)
import qualified Prelude

data DirTree a
  = NodeFile a
  | NodeDir (DirForest a)
  deriving stock (Show, Generic, Functor)
  deriving (FromJSON, ToJSON) via (Autodocodec (DirTree a))

instance (Validity a) => Validity (DirTree a)

instance Eq a => Eq (DirTree a) where
  (==) = eq1DirTree (==)

instance Ord a => Ord (DirTree a) where
  compare = ord1DirTree compare

instance Eq1 DirTree where
  liftEq = eq1DirTree

instance Ord1 DirTree where
  liftCompare = ord1DirTree

instance NFData a => NFData (DirTree a)

instance Foldable DirTree where
  foldMap func =
    \case
      NodeFile v -> func v
      NodeDir df -> foldMap func df

instance Traversable DirTree where
  traverse func =
    \case
      NodeFile v -> NodeFile <$> func v
      NodeDir df -> NodeDir <$> traverse func df

instance HasCodec a => HasCodec (DirTree a) where
  codec =
    named "DirTree" $
      dimapCodec f g $
        eitherCodec
          (codec :: JSONCodec a)
          (codec :: JSONCodec (DirForest a))
    where
      f = \case
        Left a -> NodeFile a
        Right b -> NodeDir b
      g = \case
        NodeFile a -> Left a
        NodeDir b -> Right b

eq1DirTree :: (a -> b -> Bool) -> DirTree a -> DirTree b -> Bool
eq1DirTree eq dt1 dt2 = case (dt1, dt2) of
  (NodeFile a1, NodeFile a2) -> eq a1 a2
  (NodeDir df1, NodeDir df2) -> eq1DirForest eq df1 df2
  _ -> False

ord1DirTree :: (a -> b -> Ordering) -> DirTree a -> DirTree b -> Ordering
ord1DirTree cmp dt1 dt2 = case (dt1, dt2) of
  (NodeFile a1, NodeFile a2) -> cmp a1 a2
  (NodeDir df1, NodeDir df2) -> ord1DirForest cmp df1 df2
  (NodeFile _, NodeDir _) -> LT
  (NodeDir _, NodeFile _) -> GT

newtype DirForest a = DirForest
  { unDirForest :: Map FilePath (DirTree a)
  }
  deriving stock (Show, Generic, Functor)
  deriving (FromJSON, ToJSON) via (Autodocodec (DirForest a))

instance (Validity a) => Validity (DirForest a) where
  validate df@(DirForest m) =
    mconcat
      [ genericValidate df,
        decorateMap m $ \p dt ->
          let isTopLevel p_ = parent p_ == [reldir|./|]
           in case dt of
                NodeFile _ ->
                  let rf = Path p :: Path Rel File
                   in mconcat
                        [ declare "There are no separators on this level" $ isTopLevel rf,
                          validate (Path p :: Path Rel File)
                        ]
                NodeDir (DirForest _) ->
                  let rd = Path (FP.addTrailingPathSeparator p) :: Path Rel Dir
                   in mconcat
                        [ declare "the path has no trailing path separator" $
                            not $
                              FP.hasTrailingPathSeparator p,
                          declare "There are no separators on this level" $ isTopLevel rd, -- We need this for equality with the files.
                          validate rd
                        ]
      ]

instance Eq a => Eq (DirForest a) where
  (==) = eq1DirForest (==)

instance Ord a => Ord (DirForest a) where
  compare = ord1DirForest compare

instance Eq1 DirForest where
  liftEq = eq1DirForest

instance Ord1 DirForest where
  liftCompare = ord1DirForest

instance NFData a => NFData (DirForest a)

instance Foldable DirForest where
  foldMap func (DirForest dtm) = foldMap (foldMap func) dtm

instance Traversable DirForest where
  traverse func (DirForest dtm) = DirForest <$> traverse (traverse func) dtm

instance HasCodec a => HasCodec (DirForest a) where
  codec =
    named "DirForest" $
      dimapCodec
        DirForest
        unDirForest
        (codec :: JSONCodec (Map FilePath (DirTree a)))

eq1DirForest :: (a -> b -> Bool) -> DirForest a -> DirForest b -> Bool
eq1DirForest eq (DirForest m1) (DirForest m2) =
  let l1 = M.toAscList m1
      l2 = M.toAscList m2
   in length l1 == length l2 && liftEq (\(p1, a1) (p2, a2) -> p1 == p2 && eq1DirTree eq a1 a2) l1 l2

ord1DirForest :: (a -> b -> Ordering) -> DirForest a -> DirForest b -> Ordering
ord1DirForest cmp (DirForest m1) (DirForest m2) =
  let l1 = M.toAscList m1
      l2 = M.toAscList m2
   in liftCompare (\(p1, a1) (p2, a2) -> compare p1 p2 <> ord1DirTree cmp a1 a2) l1 l2

-- | File or Dir
data FOD a
  = F a
  | D
  deriving (Show, Eq, Ord, Generic, Functor)

instance Validity a => Validity (FOD a)

-- | The empty forest
empty :: DirForest a
empty = DirForest M.empty

-- | True iff the forest is entirely empty
null :: DirForest a -> Bool
null (DirForest dtm) = M.null dtm

-- | True iff there are only empty directories in the directory forest
nullFiles :: DirForest a -> Bool
nullFiles (DirForest df) = all goTree df
  where
    goTree = \case
      NodeFile _ -> False
      NodeDir df' -> nullFiles df'

singletonFile :: Path Rel File -> a -> DirForest a
singletonFile rp a =
  case insertFile rp a empty of
    Right df -> df
    _ -> error "There can't have been anything in the way in an empty dir forest."

singletonDir :: Path Rel Dir -> DirForest a
singletonDir rp =
  case insertDir rp empty of
    Right df -> df
    _ -> error "There can't have been anything in the way in an empty dir forest."

mapWithPath :: (Path Rel File -> a -> b) -> DirForest a -> DirForest b
mapWithPath func df = runIdentity $ traverseWithPath (\a b -> Identity $ func a b) df

traverseWithPath :: forall a b f. Applicative f => (Path Rel File -> a -> f b) -> DirForest a -> f (DirForest b)
traverseWithPath func = goF [reldir|./|]
  where
    goF :: Path Rel Dir -> DirForest a -> f (DirForest b)
    goF cur (DirForest ts) = DirForest <$> M.traverseWithKey (goT cur) ts
    goT :: Path Rel Dir -> FilePath -> DirTree a -> f (DirTree b)
    goT cur fp = \case
      NodeFile v ->
        let rf = cur </> fromJust (parseRelFile fp)
         in NodeFile <$> func rf v
      NodeDir df ->
        let rd = cur </> fromJust (parseRelDir fp)
         in NodeDir <$> goF rd df

traverseWithPath_ :: forall a b f. Applicative f => (Path Rel File -> a -> f b) -> DirForest a -> f ()
traverseWithPath_ func = goF [reldir|./|]
  where
    goF :: Path Rel Dir -> DirForest a -> f ()
    goF cur (DirForest ts) = void $ M.traverseWithKey (goT cur) ts
    goT :: Path Rel Dir -> FilePath -> DirTree a -> f ()
    goT cur fp = \case
      NodeFile v ->
        let rf = cur </> fromJust (parseRelFile fp)
         in void $ func rf v
      NodeDir df ->
        let rd = cur </> fromJust (parseRelDir fp)
         in goF rd df

-- | Remove all empty directories from a 'DirForest'
--
-- This will return 'Nothing' if the root was also empty.
pruneEmptyDirs :: DirForest a -> Maybe (DirForest a)
pruneEmptyDirs (DirForest m) =
  let m' = M.mapMaybe goTree m
   in if M.null m' then Nothing else Just (DirForest m')
  where
    goTree :: DirTree a -> Maybe (DirTree a)
    goTree dt = case dt of
      NodeFile _ -> Just dt
      NodeDir df -> NodeDir <$> pruneEmptyDirs df

anyEmptyDir :: DirForest a -> Bool
anyEmptyDir (DirForest m) = M.null m || any goTree m
  where
    goTree :: DirTree a -> Bool
    goTree = \case
      NodeFile _ -> False
      NodeDir df -> anyEmptyDir df

lookup ::
  forall a.
  Path Rel File ->
  DirForest a ->
  Maybe a
lookup rp df = go df (FP.splitDirectories $ fromRelFile rp)
  where
    go :: DirForest a -> [FilePath] -> Maybe a
    go (DirForest ts) =
      \case
        [] -> Nothing
        [f] -> do
          dt <- M.lookup f ts
          case dt of
            NodeFile contents -> Just contents
            _ -> Nothing
        (d : ds) -> do
          dt <- M.lookup d ts
          case dt of
            NodeDir dt_ -> go dt_ ds
            _ -> Nothing

insertFOD ::
  forall a.
  FilePath ->
  FOD a ->
  DirForest a ->
  Either (InsertionError a) (DirForest a)
insertFOD fp fod dirForest = go [reldir|./|] dirForest (FP.splitDirectories fp)
  where
    node = case fod of
      F a -> NodeFile a
      D -> NodeDir empty
    go ::
      Path Rel Dir ->
      DirForest a ->
      [FilePath] ->
      Either (InsertionError a) (DirForest a)
    go cur df@(DirForest ts) =
      \case
        [] -> Right df -- Should not happen, but just insert nothing if it does.
        [f] ->
          -- The last piece
          case M.lookup f ts of
            Nothing ->
              pure $ DirForest $ M.insert f node ts
            Just dt ->
              case dt of
                NodeFile contents -> do
                  let rf = cur </> fromJust (parseRelFile f)
                  Left (FileInTheWay rf contents)
                NodeDir df' -> case fod of
                  F _ -> do
                    let rd = cur </> fromJust (parseRelDir f)
                    Left (DirInTheWay rd df')
                  D -> pure df -- If it's already there, nothing changes

        -- Not the last piece, must be a dir
        (d : ds) ->
          -- Check if this piece is already in the forest
          case M.lookup d ts of
            -- If it isn't, then we need to make it and try again
            Nothing -> do
              let df' = DirForest $ M.insert d (NodeDir empty) ts
              go cur df' (d : ds)
            -- If it is, then we can recurse down there.
            Just dt ->
              case dt of
                NodeFile contents -> do
                  let rf = cur </> fromJust (parseRelFile d)
                  Left (FileInTheWay rf contents)
                NodeDir df' -> do
                  let newCur = cur </> fromJust (parseRelDir d)
                  df'' <- go newCur df' ds
                  pure $ DirForest $ M.insert d (NodeDir df'') ts

insertFile ::
  forall a.
  Path Rel File ->
  a ->
  DirForest a ->
  Either (InsertionError a) (DirForest a)
insertFile rp a = insertFOD (fromRelFile rp) (F a)

insertDir ::
  forall a.
  Path Rel Dir ->
  DirForest a ->
  Either (InsertionError a) (DirForest a)
insertDir rp = insertFOD (FP.dropTrailingPathSeparator $ fromRelDir rp) D

fromFileList :: [(Path Rel File, a)] -> Either (InsertionError a) (DirForest a)
fromFileList = foldM (flip $ uncurry insertFile) empty

toFileList :: DirForest a -> [(Path Rel File, a)]
toFileList = M.toList . toFileMap

data InsertValidation e a = InsertionErrors (NonEmpty (InsertionError e)) | NoInsertionErrors a
  deriving (Show, Eq, Generic, Functor)

instance (Validity e, Validity a) => Validity (InsertValidation e a)

instance Applicative (InsertValidation e) where
  pure = NoInsertionErrors
  InsertionErrors es1 <*> InsertionErrors es2 = InsertionErrors $ es1 <> es2
  InsertionErrors es <*> NoInsertionErrors _ = InsertionErrors es
  NoInsertionErrors _ <*> InsertionErrors es = InsertionErrors es
  NoInsertionErrors f <*> NoInsertionErrors a = NoInsertionErrors $ f a

unpackInsertValidation :: InsertValidation e a -> Either (NonEmpty (InsertionError e)) a
unpackInsertValidation = \case
  InsertionErrors es -> Left es
  NoInsertionErrors r -> Right r

-- Left-biased
union :: DirForest a -> DirForest a -> InsertValidation a (DirForest a)
union = unionWith const

-- Left-biased
unionWith :: (a -> a -> a) -> DirForest a -> DirForest a -> InsertValidation a (DirForest a)
unionWith func = unionWithKey (\_ a b -> func a b)

-- Left-biased
unionWithKey :: forall a. (Path Rel File -> a -> a -> a) -> DirForest a -> DirForest a -> InsertValidation a (DirForest a)
unionWithKey func = goForest "" -- Because "" FP.</> "anything" = "anything"
  where
    goForest :: FilePath -> DirForest a -> DirForest a -> InsertValidation a (DirForest a)
    goForest base (DirForest dtm1) (DirForest dtm2) =
      DirForest
        <$> sequenceA
          ( M.unionWithKey
              ( \p e1 e2 -> case (e1, e2) of
                  (NoInsertionErrors m1, NoInsertionErrors m2) -> goTree (base FP.</> p) m1 m2
                  _ -> error "Should not happen because we just M.map-ed only NoInsertionErrors, but it did"
              )
              (M.map NoInsertionErrors dtm1)
              (M.map NoInsertionErrors dtm2)
          )
    goTree :: FilePath -> DirTree a -> DirTree a -> InsertValidation a (DirTree a)
    goTree base dt1 dt2 = case (dt1, dt2) of
      (NodeDir df1, NodeDir df2) -> NodeDir <$> goForest base df1 df2
      (NodeFile a1, NodeFile a2) -> NoInsertionErrors $ NodeFile $ func (fromJust $ parseRelFile base) a1 a2
      (NodeFile a, NodeDir _) -> InsertionErrors $ FileInTheWay (fromJust $ parseRelFile base) a :| []
      (NodeDir df, NodeFile _) -> InsertionErrors $ DirInTheWay (fromJust $ parseRelDir base) df :| []

unions :: [DirForest a] -> Either (InsertionError a) (DirForest a)
unions = foldM (\df1 df2 -> left NE.head $ unpackInsertValidation $ union df1 df2) empty

intersection :: DirForest a -> DirForest b -> DirForest a
intersection = intersectionWith const

intersectionWith :: (a -> b -> c) -> DirForest a -> DirForest b -> DirForest c
intersectionWith func = intersectionWithKey (\_ a b -> func a b)

intersectionWithKey :: forall a b c. (Path Rel File -> a -> b -> c) -> DirForest a -> DirForest b -> DirForest c
intersectionWithKey func = goForest "" -- Because "" FP.</> "anything" = "anything"
  where
    goForest :: FilePath -> DirForest a -> DirForest b -> DirForest c
    goForest base (DirForest dtm1) (DirForest dtm2) =
      DirForest $ M.mapMaybe id $ M.intersectionWithKey (\p m1 m2 -> goTree (base FP.</> p) m1 m2) dtm1 dtm2
    goTree :: FilePath -> DirTree a -> DirTree b -> Maybe (DirTree c)
    goTree base dt1 dt2 = case (dt1, dt2) of
      (NodeDir df1_, NodeDir df2_) -> Just $ NodeDir $ goForest base df1_ df2_
      (NodeFile f1, NodeFile f2) -> Just $ NodeFile $ func (fromJust $ parseRelFile base) f1 f2 -- TODO is this what we want?
      _ -> Nothing

intersections :: [DirForest a] -> DirForest a
intersections = foldl' intersection empty

filter :: (a -> Bool) -> DirForest a -> DirForest a
filter func = filterWithKey (const func)

filterWithKey :: forall a. (Path Rel File -> a -> Bool) -> DirForest a -> DirForest a
filterWithKey filePred = goForest "" -- Because "" FP.</> "anything" = "anything"
  where
    goForest :: FilePath -> DirForest a -> DirForest a
    goForest base (DirForest df) =
      DirForest $
        M.mapMaybeWithKey
          (\p dt -> goTree (base FP.</> p) dt)
          df
    goTree :: FilePath -> DirTree a -> Maybe (DirTree a) -- Nothing means it will be removed
    goTree base dt = case dt of
      NodeFile cts -> do
        rf <- parseRelFile base
        if filePred rf cts then Just dt else Nothing
      NodeDir df -> Just $ NodeDir $ goForest base df

filterHidden :: forall a. DirForest a -> DirForest a
filterHidden = goForest
  where
    goPair :: FilePath -> DirTree a -> Maybe (DirTree a)
    goPair fp dt = if hiddenHere fp then Nothing else Just $ goTree dt
    goForest :: DirForest a -> DirForest a
    goForest (DirForest m) =
      DirForest $ M.mapMaybeWithKey goPair m
    goTree :: DirTree a -> DirTree a
    goTree dt = case dt of
      NodeFile _ -> dt
      NodeDir df -> NodeDir $ goForest df

difference :: DirForest a -> DirForest b -> DirForest a
difference = differenceWith $ \_ _ -> Nothing

differenceWith :: (a -> b -> Maybe a) -> DirForest a -> DirForest b -> DirForest a
differenceWith func = differenceWithKey $ const func

differenceWithKey :: forall a b. (Path Rel File -> a -> b -> Maybe a) -> DirForest a -> DirForest b -> DirForest a
differenceWithKey func = goForest "" -- Because "" </> "anything" = "anything"
  where
    goForest :: FilePath -> DirForest a -> DirForest b -> DirForest a
    goForest base (DirForest df1_) (DirForest df2_) =
      DirForest $ M.differenceWithKey (\p dt1 dt2 -> goTree (base FP.</> p) dt1 dt2) df1_ df2_
    goTree :: FilePath -> DirTree a -> DirTree b -> Maybe (DirTree a)
    goTree base dt1 dt2 = case (dt1, dt2) of
      (NodeFile v1, NodeFile v2) -> NodeFile <$> func (fromJust $ parseRelFile base) v1 v2
      (NodeFile v, NodeDir _) -> Just $ NodeFile v -- TODO not sure what the semantics are here
      (NodeDir df, NodeFile _) -> Just $ NodeDir df -- TODO not sure what the semantics are here
      (NodeDir df1_, NodeDir df2_) -> Just $ NodeDir $ goForest base df1_ df2_

data InsertionError a
  = FileInTheWay (Path Rel File) a
  | DirInTheWay (Path Rel Dir) (DirForest a)
  deriving (Show, Eq, Ord, Generic)

instance (Validity a) => Validity (InsertionError a)

fromFileMap :: Map (Path Rel File) a -> Either (InsertionError a) (DirForest a)
fromFileMap = foldM (\df (rf, cts) -> insertFile rf cts df) empty . M.toList

toFileMap :: DirForest a -> Map (Path Rel File) a
toFileMap = M.foldlWithKey go M.empty . unDirForest
  where
    go :: Map (Path Rel File) a -> FilePath -> DirTree a -> Map (Path Rel File) a
    go m path =
      \case
        NodeFile contents ->
          let rf = fromJust (parseRelFile path) -- Cannot fail if the original dirforest is valid
           in M.insert rf contents m
        NodeDir df ->
          let rd = fromJust (parseRelDir path) -- Cannot fail if the original dirforest is valid
           in M.union m $ M.mapKeys (rd </>) (toFileMap df)

fromMap :: Map FilePath (FOD a) -> Either (InsertionError a) (DirForest a)
fromMap = foldM (\df (rf, fod) -> insertFOD rf fod df) empty . M.toList

toMap :: DirForest a -> Map FilePath (FOD a)
toMap = M.foldlWithKey go M.empty . unDirForest
  where
    go :: Map FilePath (FOD a) -> FilePath -> DirTree a -> Map FilePath (FOD a)
    go m path =
      \case
        NodeFile contents ->
          M.insert path (F contents) m
        NodeDir df ->
          M.insert path D $ M.union m $ M.mapKeys (path FP.</>) (toMap df)

read ::
  forall a b m.
  (MonadIO m) =>
  Path b Dir ->
  (Path b File -> m a) ->
  m (DirForest a)
read = readFiltered (const True) (const True)

readNonHidden ::
  forall a b m.
  (MonadIO m) =>
  Path b Dir ->
  (Path b File -> m a) ->
  m (DirForest a)
readNonHidden = readNonHiddenFiltered (const True) (const True)

readNonHiddenFiltered ::
  forall a b m.
  (MonadIO m) =>
  (Path b File -> Bool) ->
  (Path b Dir -> Bool) ->
  Path b Dir ->
  (Path b File -> m a) ->
  m (DirForest a)
readNonHiddenFiltered filePred dirPred root = readFiltered (\f -> go f && filePred f) (\d -> go d && dirPred d) root
  where
    go af = case stripProperPrefix root af of
      Nothing -> True -- Whatever
      Just rf -> not $ hiddenRel rf

readFiltered ::
  forall a b m.
  (MonadIO m) =>
  (Path b File -> Bool) ->
  (Path b Dir -> Bool) ->
  Path b Dir ->
  (Path b File -> m a) ->
  m (DirForest a)
readFiltered filePred dirPred root readFunc = do
  e <- doesDirExist root
  if e
    then do
      fs <- walkDirAccumRel (Just decendHandler) outputWriter root
      case unions fs of
        Left _ -> error "There can't have been any intra-dir collisions, but there were."
        Right r -> pure r
    else pure empty
  where
    decendHandler :: Path Rel Dir -> [Path Rel Dir] -> [Path Rel File] -> m (WalkAction Rel)
    decendHandler subdir dirs _ = do
      let toExclude = Prelude.filter (not . dirPred . ((root </> subdir) </>)) dirs
      pure $ WalkExclude toExclude
    outputWriter :: Path Rel Dir -> [Path Rel Dir] -> [Path Rel File] -> m [DirForest a]
    outputWriter subdir dirs files = do
      df1 <- foldM goDir empty dirs
      (: []) <$> foldM goFile df1 files
      where
        goDir :: DirForest a -> Path Rel Dir -> m (DirForest a)
        goDir df p =
          let path = root </> subdir </> p
           in if dirPred path
                then case insertDir (subdir </> p) df of
                  Left _ ->
                    error
                      "There can't have been anything in the way while reading a dirforest, but there was."
                  Right df' -> pure df'
                else pure df
        goFile :: DirForest a -> Path Rel File -> m (DirForest a)
        goFile df p =
          let path = root </> subdir </> p
           in if filePred path
                then do
                  contents <- readFunc path
                  case insertFile (subdir </> p) contents df of
                    Left _ ->
                      error
                        "There can't have been anything in the way while reading a dirforest, but there was."
                    Right df' -> pure df'
                else pure df

readOneLevel ::
  forall a b m.
  (MonadIO m) =>
  Path b Dir ->
  (Path b File -> m a) ->
  m (DirForest a)
readOneLevel = readOneLevelFiltered (const True) (const True)

readOneLevelNonHidden ::
  forall a b m.
  (MonadIO m) =>
  Path b Dir ->
  (Path b File -> m a) ->
  m (DirForest a)
readOneLevelNonHidden = readOneLevelNonHiddenFiltered (const True) (const True)

readOneLevelNonHiddenFiltered ::
  forall a b m.
  (MonadIO m) =>
  (Path b File -> Bool) ->
  (Path b Dir -> Bool) ->
  Path b Dir ->
  (Path b File -> m a) ->
  m (DirForest a)
readOneLevelNonHiddenFiltered filePred dirPred root = readOneLevelFiltered (\f -> go f && filePred f) (\d -> go d && dirPred d) root
  where
    go af = case stripProperPrefix root af of
      Nothing -> True -- Whatever
      Just rf -> not $ hiddenRel rf

readOneLevelFiltered ::
  forall a b m.
  (MonadIO m) =>
  (Path b File -> Bool) ->
  (Path b Dir -> Bool) ->
  Path b Dir ->
  (Path b File -> m a) ->
  m (DirForest a)
readOneLevelFiltered filePred dirPred root readFunc = do
  (dirs, files) <- fmap (fromMaybe ([], [])) $ liftIO $ forgivingAbsence $ listDirRel root
  df1 <- foldM goDir empty dirs
  foldM goFile df1 files
  where
    goDir :: DirForest a -> Path Rel Dir -> m (DirForest a)
    goDir df p =
      let path = root </> p
       in if dirPred path
            then case insertDir p df of
              Left _ ->
                error
                  "There can't have been anything in the way while reading a dirforest, but there was."
              Right df' -> pure df'
            else pure df
    goFile :: DirForest a -> Path Rel File -> m (DirForest a)
    goFile df p =
      let path = root </> p
       in if filePred path
            then do
              contents <- readFunc path
              case insertFile p contents df of
                Left _ ->
                  error
                    "There can't have been anything in the way while reading a dirforest, but there was."
                Right df' -> pure df'
            else pure df

write ::
  forall a b.
  (Show a, Ord a) =>
  Path b Dir ->
  DirForest a ->
  (Path b File -> a -> IO ()) ->
  IO ()
write root dirForest writeFunc = do
  ensureDir root
  forM_ (M.toList $ unDirForest dirForest) $ \(path, dt) ->
    case dt of
      NodeFile contents -> do
        f <- parseRelFile path
        let af = root </> f
        writeFunc af contents
      NodeDir df' -> do
        d <- parseRelDir path
        let ad = root </> d
        write ad df' writeFunc

hiddenRel :: Path Rel t -> Bool
hiddenRel = any hiddenHere . FP.splitDirectories . toFilePath

hiddenHere :: FilePath -> Bool
hiddenHere [] = False -- Technically not possible, but fine
hiddenHere ('.' : _) = True
hiddenHere _ = False