packages feed

librarian-0.1.0.0: src/Librarian.hs

module Librarian
  ( Rule (..),
    RuleName (..),

    -- * Collecting
    Matcher (..),
    Mover (..),
    CollectedFiles,
    fetchRulesOn,

    -- * Planning
    Move (..),
    planMoves,
    displayPlan,

    -- * Runner
    Action (..),
    ActionResult (..),
    RunResult,
    runPlan,
    displayResult,
  )
where

import Control.Exception (catch)
import Control.Monad
import Data.Function (on)
import Data.Functor (($>))
import Data.List (find, nubBy, sortOn)
import qualified Data.Map.Strict as Map
import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing)
import Data.String (IsString)
import Dhall (FromDhall)
import GHC.Generics (Generic)
import System.Directory (createDirectoryIfMissing, doesFileExist, renameFile)
import System.EasyFile (splitFileName)
import System.FilePath.Glob (compile, globDir)
import Text.RegexPR
import Text.Show.Pretty

data Rule = Rule
  { name :: RuleName,
    match :: Matcher,
    movers :: [Mover]
  }
  deriving stock (Eq, Show, Generic)

instance FromDhall Rule

newtype RuleName = RuleName {getRuleName :: String}
  deriving stock (Generic)
  deriving newtype (Eq, Ord, Show, IsString)
  deriving (FromDhall) via String

newtype Matcher = Matcher {matchPattern :: String}
  deriving stock (Generic)
  deriving newtype (Eq, Ord, Show, IsString)
  deriving (FromDhall) via String

data Mover = Mover
  { inputPattern :: String,
    newName :: String
  }
  deriving stock (Eq, Show, Generic)

instance FromDhall Mover

type CollectedFiles = Map.Map FilePath Rule

fetchRulesOn :: FilePath -> [Rule] -> IO CollectedFiles
fetchRulesOn root rules = do
  matches <- globDir (compile . matchPattern . match <$> rules) root
  let distributeRule :: [FilePath] -> Rule -> [(FilePath, Rule)]
      distributeRule fs r = map (\f -> (f, r)) fs
  files <- mapM (filterM doesFileExist) matches
  return $ Map.unions $ map Map.fromList $ zipWith distributeRule files rules

data Move = Move
  { original :: FilePath,
    new :: Maybe FilePath,
    rule :: Rule
  }
  deriving stock (Eq, Show, Generic)

planMoves :: CollectedFiles -> [Move]
planMoves = map (uncurry planMove) . Map.toList
  where
    planMove :: FilePath -> Rule -> Move
    planMove p rule =
      Move
        { original = p,
          new = newPath p $ movers rule,
          rule = rule
        }
    newPath :: FilePath -> [Mover] -> Maybe FilePath
    newPath p =
      fmap (\mover -> subRegexPR (inputPattern mover) (newName mover) p)
        . find (isJust . flip matchRegexPR p . inputPattern)

displayPlan :: [Move] -> IO ()
displayPlan xs = do
  forM_ xs $ \Move {..} ->
    putStrLn $ "[" <> getRuleName (name rule) <> "] '" <> original <> "' -> '" <> fromMaybe "UNABLE TO REPLACE" new <> "'"
  let unrenamed = nubBy ((==) `on` name) $ sortOn name $ map rule $ filter (isNothing . new) xs
  unless (null unrenamed) $ do
    putStrLn "Unable to generate a new name for:"
    forM_ unrenamed pPrint

data Action = Action
  { from :: FilePath,
    to :: FilePath
  }
  deriving stock (Eq, Show, Generic)

data ActionResult
  = Done
  | Existing
  | IOException IOError
  deriving stock (Eq, Show, Generic)

type RunResult = [(Action, ActionResult)]

runPlan :: [Move] -> IO RunResult
runPlan = fmap catMaybes . run
  where
    run =
      mapM $ \Move {..} ->
        case new of
          Nothing -> return Nothing
          Just newPath -> do
            let action = Action original newPath
            existing <- doesFileExist newPath
            if existing
              then return $ Just (action, Existing)
              else do
                prepareDirectory newPath
                Just . (,) action <$> ((renameFile original newPath $> Done) `catch` (return . IOException))
    prepareDirectory = createDirectoryIfMissing True . fst . splitFileName

displayResult :: RunResult -> IO ()
displayResult =
  mapM_ $ \(Action {..}, result) ->
    case result of
      Done -> return ()
      Existing -> putStrLn $ "'" <> from <> "' -> '" <> to <> "' ALREADY EXISTING"
      IOException e -> putStrLn $ "'" <> from <> "' -> '" <> to <> "' IOError (" <> show e <> ")"