hledger-flow-0.20.0: src/Hledger/Flow/Import/ImportHelpers.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Hledger.Flow.Import.ImportHelpers (withStagedOutput, extractImportDirs, groupIncludesUpTo, includeFileName, rulesFileCandidates, defaultRulesCandidates, importDirLines) where
import qualified Data.Map.Strict as Map
import Data.Maybe (fromMaybe)
import qualified Data.Text as T
import Hledger.Flow.Common (groupValuesBy)
import Hledger.Flow.DocHelpers (DocLink (..), docURL)
import Hledger.Flow.Import.Types (ImportDirs (..), InputFileBundle)
import Hledger.Flow.PathHelpers (AbsFile, RelDir, RelFile, pathSize, withStagedOutput)
import Path
import Control.Monad.Catch (MonadThrow)
import System.FilePath (dropTrailingPathSeparator)
import qualified System.FilePath as FilePath
extractImportDirs :: Path b Dir -> Path b File -> Either T.Text (ImportDirs b)
extractImportDirs root inputFile =
-- Split the short relative part once: each Path.parent renormalises the whole path.
case FilePath.splitDirectories . toFilePath <$> stripProperPrefix root inputFile of
Just [owner, bank, account, state, year, _] -> maybe malformed Right $ do
ownerD <- below root owner
bankD <- below ownerD bank
accountD <- below bankD account
stateD <- below accountD state
yearD <- below stateD year
return (ImportDirs root ownerD bankD accountD stateD yearD)
_ -> malformed
where
below dir name = (dir </>) <$> parseRelDir name
malformed = Left $
"I couldn't find the right number of directories between \"import\" and the input file:\n"
<> T.pack (toFilePath inputFile)
<> "\n\nhledger-flow expects to find input files in this structure:\n"
<> "import/owner/bank/account/filestate/year/trxfile\n\n"
<> "Have a look at the documentation for a detailed explanation:\n"
<> docURL InputFiles
groupIncludesUpTo :: RelDir -> [RelFile] -> InputFileBundle
groupIncludesUpTo stopAt = collect Map.empty
where
collect acc files =
let belowStop f = isProperPrefixOf stopAt f && parent f /= stopAt
groups = Map.filterWithKey (\f _ -> isProperPrefixOf stopAt f) $
groupValuesBy includePath (filter belowStop files)
in if Map.null groups
then acc
else collect (Map.unionWith (++) acc groups) (Map.keys groups)
includePath file
| pathSize (parent file) == 6 = initialIncludeFilePath file
| otherwise = parentIncludeFilePath file
initialIncludeFilePath :: RelFile -> RelFile
initialIncludeFilePath p = (parent . parent . parent) p </> includeFileName p
parentIncludeFilePath :: RelFile -> RelFile
parentIncludeFilePath p = (parent . parent) p </> filename p
includeFileName :: RelFile -> RelFile
includeFileName f = do
let includeFile = (dropTrailingPathSeparator . toFilePath . dirname . parent) f ++ "-include.journal"
fromMaybe [relfile|unknown-include.journal|] $ parseRelFile includeFile
-- | Rules files for a statement in lookup order: statement-specific overrides,
-- then the account's default rules files.
rulesFileCandidates :: AbsFile -> ImportDirs Abs -> IO [AbsFile]
rulesFileCandidates csv dirs = do
overrides <- if "rfo" `T.isPrefixOf` suffix && not (T.any (== '_') suffix)
then do
name <- rulesName [suffix]
return [dir </> name | dir <- [accountDir dirs, bankDir dirs, importDir dirs]]
else return []
(overrides ++) <$> defaultRulesCandidates dirs
where
statementName = T.pack (toFilePath (filename csv))
-- The statement prefix may contain dots, and the override may have several extensions.
suffix = T.takeWhile (/= '.') $ case T.breakOnEnd "_rfo" statementName of
("", _) -> statementName
(_, rest) -> "rfo" <> rest
-- | The account-level and bank-level rules files, in lookup order.
defaultRulesCandidates :: ImportDirs Abs -> IO [AbsFile]
defaultRulesCandidates dirs = do
let bank = importDirLines (bankDir dirs)
account = importDirLines (accountDir dirs)
accountRule <- rulesName (bank ++ account)
bankRule <- rulesName bank
return [accountDir dirs </> accountRule, importDir dirs </> bankRule]
rulesName :: MonadThrow m => [T.Text] -> m RelFile
rulesName parts = parseRelFile (T.unpack (T.intercalate "-" parts) FilePath.<.> "rules")
importDirLines :: Path b Dir -> [T.Text]
importDirLines = T.splitOn "\n" . T.pack . dropTrailingPathSeparator . toFilePath . dirname