Skip to content

Commit 958674f

Browse files
committed
Use the Path package from FP Complete for determining basedir and rundir
https://github.com/commercialhaskell/path
1 parent 53ed1a0 commit 958674f

19 files changed

+339
-207
lines changed

app/Main.hs

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
{-# LANGUAGE OverloadedStrings #-}
2+
{-# LANGUAGE QuasiQuotes #-}
23

34
module Main where
45

6+
import Path
57
import Turtle hiding (switch)
68
import Prelude hiding (FilePath, putStrLn)
79

810
import Options.Applicative
911

1012
import Hledger.Flow.Common
13+
import Hledger.Flow.BaseDir
1114
import qualified Hledger.Flow.RuntimeOptions as RT
1215
import Hledger.Flow.Reports
1316
import Hledger.Flow.CSVImport
@@ -36,7 +39,8 @@ main = do
3639

3740
toRuntimeOptionsImport :: MainParams -> ImportParams -> IO RT.RuntimeOptions
3841
toRuntimeOptionsImport mainParams' subParams' = do
39-
(bd, runDir) <- determineBaseDir $ maybeImportBaseDir subParams'
42+
let maybeBD = maybeImportBaseDir subParams' :: Maybe FilePath
43+
(bd, runDir) <- determineBaseDir maybeBD
4044
hli <- hledgerInfoFromPath $ hledgerPathOpt mainParams'
4145
return RT.RuntimeOptions { RT.baseDir = bd
4246
, RT.importRunDir = runDir
@@ -50,10 +54,11 @@ toRuntimeOptionsImport mainParams' subParams' = do
5054

5155
toRuntimeOptionsReport :: MainParams -> ReportParams -> IO RT.RuntimeOptions
5256
toRuntimeOptionsReport mainParams' subParams' = do
53-
(bd, _) <- determineBaseDir $ maybeReportBaseDir subParams'
57+
let maybeBD = maybeReportBaseDir subParams' :: Maybe FilePath
58+
(bd, _) <- determineBaseDir maybeBD
5459
hli <- hledgerInfoFromPath $ hledgerPathOpt mainParams'
5560
return RT.RuntimeOptions { RT.baseDir = bd
56-
, RT.importRunDir = "./"
61+
, RT.importRunDir = [reldir|.|]
5762
, RT.useRunDir = False
5863
, RT.hfVersion = versionInfo'
5964
, RT.hledgerInfo = hli

package.yaml

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: hledger-flow
2-
version: 0.13.2.0
2+
version: 0.14.0.0
33
synopsis: An hledger workflow focusing on automated statement import and classification.
44
category: Finance, Console
55
license: GPL-3
@@ -31,6 +31,9 @@ library:
3131
- -Wall
3232
dependencies:
3333
- turtle
34+
- path
35+
- path-io
36+
- exceptions
3437
- text
3538
- foldl
3639
- containers
@@ -52,6 +55,7 @@ executables:
5255

5356
dependencies:
5457
- hledger-flow
58+
- path
5559
- turtle
5660
- text
5761
- optparse-applicative
@@ -66,6 +70,8 @@ tests:
6670
- -with-rtsopts=-N
6771
dependencies:
6872
- hledger-flow
73+
- path
74+
- path-io
6975
- turtle
7076
- HUnit
7177
- containers

src/Hledger/Flow/BaseDir.hs

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
{-# LANGUAGE QuasiQuotes #-}
3+
4+
module Hledger.Flow.BaseDir where
5+
6+
import Path
7+
import Path.IO
8+
import Hledger.Flow.Types (HasBaseDir, HasRunDir, BaseDir, RunDir, baseDir, importRunDir)
9+
import Hledger.Flow.PathHelpers
10+
11+
import Data.Maybe
12+
13+
import Control.Monad.Catch (MonadThrow, throwM)
14+
import Control.Monad.IO.Class (MonadIO)
15+
16+
17+
import qualified Turtle as Turtle (stripPrefix)
18+
19+
determineBaseDir :: Maybe TurtlePath -> IO (BaseDir, RunDir)
20+
determineBaseDir suppliedDir = do
21+
pwd <- getCurrentDir
22+
determineBaseDir' pwd suppliedDir
23+
24+
determineBaseDir' :: AbsDir -> Maybe TurtlePath -> IO (BaseDir, RunDir)
25+
determineBaseDir' pwd (Just suppliedDir) = do
26+
absDir <- turtleToAbsDir pwd suppliedDir
27+
determineBaseDirFromStartDir absDir
28+
determineBaseDir' pwd Nothing = determineBaseDirFromStartDir pwd
29+
30+
determineBaseDirFromStartDir :: AbsDir -> IO (BaseDir, RunDir)
31+
determineBaseDirFromStartDir startDir = determineBaseDirFromStartDir' startDir startDir
32+
33+
determineBaseDirFromStartDir' :: (MonadIO m, MonadThrow m) => AbsDir -> AbsDir -> m (BaseDir, RunDir)
34+
determineBaseDirFromStartDir' startDir possibleBaseDir = do
35+
_ <- if (parent possibleBaseDir == possibleBaseDir) then throwM (MissingBaseDir startDir) else return ()
36+
foundBaseDir <- doesDirExist $ possibleBaseDir </> [reldir|import|]
37+
if foundBaseDir then
38+
do
39+
runDir <- makeRelative possibleBaseDir startDir
40+
return (possibleBaseDir, runDir)
41+
else determineBaseDirFromStartDir' startDir $ parent possibleBaseDir
42+
43+
relativeToBase :: HasBaseDir o => o -> TurtlePath -> TurtlePath
44+
relativeToBase opts = relativeToBase' $ pathToTurtle (baseDir opts)
45+
46+
relativeToBase' :: TurtlePath -> TurtlePath -> TurtlePath
47+
relativeToBase' bd p = if forceTrailingSlash bd == forceTrailingSlash p then "./" else
48+
fromMaybe p $ Turtle.stripPrefix (forceTrailingSlash bd) p
49+
50+
turtleBaseDir :: HasBaseDir o => o -> TurtlePath
51+
turtleBaseDir opts = pathToTurtle $ baseDir opts
52+
53+
turtleRunDir :: HasRunDir o => o -> TurtlePath
54+
turtleRunDir opts = pathToTurtle $ importRunDir opts

src/Hledger/Flow/CSVImport.hs

+5-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import qualified Data.Text as T
1010
import qualified Data.List.NonEmpty as NonEmpty
1111
import qualified Hledger.Flow.Types as FlowTypes
1212
import Hledger.Flow.Import.Types
13+
import Hledger.Flow.BaseDir (turtleBaseDir, turtleRunDir, relativeToBase)
14+
import Hledger.Flow.PathHelpers (forceTrailingSlash)
15+
import Hledger.Flow.DocHelpers (docURL)
1316
import Hledger.Flow.Common
1417
import Hledger.Flow.RuntimeOptions
1518
import Control.Concurrent.STM
@@ -35,8 +38,8 @@ inputFilePattern = contains (once (oneOf pathSeparators) <> asciiCI "1-in" <> on
3538

3639
importCSVs' :: RuntimeOptions -> TChan FlowTypes.LogMessage -> IO [FilePath]
3740
importCSVs' opts ch = do
38-
let baseImportDir = forceTrailingSlash $ (baseDir opts) </> "import"
39-
let runDir = forceTrailingSlash $ collapse $ (baseDir opts) </> (importRunDir opts)
41+
let baseImportDir = forceTrailingSlash $ (turtleBaseDir opts) </> "import"
42+
let runDir = forceTrailingSlash $ collapse $ (turtleBaseDir opts) </> (turtleRunDir opts)
4043
let effectiveDir = if useRunDir opts
4144
then if (forceTrailingSlash $ runDir </> "import") == baseImportDir then baseImportDir else runDir
4245
else baseImportDir

src/Hledger/Flow/Common.hs

+6-61
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@ import Data.Time.LocalTime
2020
import Data.Function (on)
2121
import qualified Data.List as List (nub, null, sort, sortBy, groupBy)
2222
import Data.Ord (comparing)
23+
2324
import Hledger.Flow.Types
2425
import qualified Hledger.Flow.Import.Types as IT
26+
import Hledger.Flow.BaseDir (turtleBaseDir, relativeToBase, relativeToBase')
27+
import Hledger.Flow.DocHelpers (docURL)
28+
2529
import Control.Concurrent.STM
2630

2731
import qualified Data.List.NonEmpty as NE
@@ -207,13 +211,6 @@ verboseTestFile opts ch p = do
207211
else logVerbose opts ch $ format ("Did not find a "%fp%" file at '"%fp%"'") (basename rel) rel
208212
return fileExists
209213

210-
relativeToBase :: HasBaseDir o => o -> FilePath -> FilePath
211-
relativeToBase opts = relativeToBase' (baseDir opts)
212-
213-
relativeToBase' :: FilePath -> FilePath -> FilePath
214-
relativeToBase' bd p = if forceTrailingSlash bd == forceTrailingSlash p then "./" else
215-
fromMaybe p $ stripPrefix (forceTrailingSlash bd) p
216-
217214
groupPairs' :: (Eq a, Ord a) => [(a, b)] -> [(a, [b])]
218215
groupPairs' = map (\ll -> (fst . head $ ll, map snd ll)) . List.groupBy ((==) `on` fst)
219216
. List.sortBy (comparing fst)
@@ -257,9 +254,6 @@ allYearIncludeFiles m = (m, yearsIncludeMap $ Map.keys m)
257254
yearsIncludeMap :: [FilePath] -> InputFileBundle
258255
yearsIncludeMap = groupValuesBy allYearsPath
259256

260-
docURL :: Line -> Text
261-
docURL = format ("https://github.com/apauley/hledger-flow#"%l)
262-
263257
lsDirs :: FilePath -> Shell FilePath
264258
lsDirs = onlyDirs . ls
265259

@@ -410,7 +404,7 @@ writeIncludesUpTo opts ch stopAt journalFiles = do
410404

411405
writeToplevelAllYearsInclude :: (HasBaseDir o, HasVerbosity o) => o -> IO [FilePath]
412406
writeToplevelAllYearsInclude opts = do
413-
let allTop = Map.singleton (baseDir opts </> allYearsFileName) ["import" </> allYearsFileName]
407+
let allTop = Map.singleton (turtleBaseDir opts </> allYearsFileName) ["import" </> allYearsFileName]
414408
writeFiles' $ (addPreamble . toIncludeFiles' Map.empty Map.empty) allTop
415409

416410
changeExtension :: Text -> FilePath -> FilePath
@@ -423,55 +417,6 @@ changeOutputPath :: FilePath -> FilePath -> FilePath
423417
changeOutputPath newOutputLocation srcFile = mconcat $ map changeSrcDir $ splitDirectories srcFile
424418
where changeSrcDir file = if (file == "1-in/" || file == "2-preprocessed/") then newOutputLocation else file
425419

426-
errorMessageBaseDir :: FilePath -> Text
427-
errorMessageBaseDir startDir = format ("\nUnable to find an hledger-flow import directory at '"%fp
428-
%"' (or in any of its parent directories).\n\n"
429-
%"Have a look at the documentation for more information:\n"%s%"\n")
430-
startDir (docURL "getting-started")
431-
432-
determineBaseDir :: Maybe FilePath -> IO (FilePath, FilePath)
433-
determineBaseDir (Just suppliedDir) = determineBaseDir' suppliedDir
434-
determineBaseDir Nothing = pwd >>= determineBaseDir'
435-
436-
determineBaseDir' :: FilePath -> IO (FilePath, FilePath)
437-
determineBaseDir' startDir = do
438-
currentDir <- pwd
439-
let absoluteStartDir = if relative startDir then collapse (currentDir </> startDir) else startDir
440-
ebd <- determineBaseDirFromAbsoluteStartDir absoluteStartDir
441-
case ebd of
442-
Right bd -> return bd
443-
Left t -> die t
444-
445-
determineBaseDirFromAbsoluteStartDir :: FilePath -> IO (Either Text (FilePath, FilePath))
446-
determineBaseDirFromAbsoluteStartDir absoluteStartDir = determineBaseDirFromAbsoluteStartDir' absoluteStartDir absoluteStartDir
447-
448-
determineBaseDirFromAbsoluteStartDir' :: FilePath -> FilePath -> IO (Either Text (FilePath, FilePath))
449-
determineBaseDirFromAbsoluteStartDir' startDir possibleBaseDir = do
450-
possibleBDExists <- testdir possibleBaseDir
451-
if not possibleBDExists then return $ Left $ format ("The provided directory does not exist: "%fp) possibleBaseDir
452-
else
453-
if relative startDir || relative possibleBaseDir then
454-
return $ Left $ format ("Internal error: found a relative path when expecting only absolute paths:\n"%fp%"\n"%fp%"\n") startDir possibleBaseDir
455-
else do
456-
foundBaseDir <- testdir $ possibleBaseDir </> "import"
457-
if foundBaseDir then
458-
do
459-
let baseD = forceTrailingSlash possibleBaseDir
460-
let runDir = forceTrailingSlash $ relativeToBase' baseD startDir
461-
return $ Right $ (baseD, runDir)
462-
else do
463-
let doneSearching = (possibleBaseDir `elem` ["/", "./"])
464-
if doneSearching
465-
then return $ Left $ errorMessageBaseDir startDir
466-
else determineBaseDirFromAbsoluteStartDir' startDir $ parent possibleBaseDir
467-
468-
469-
dirOrPwd :: Maybe FilePath -> IO FilePath
470-
dirOrPwd maybeBaseDir = fmap forceTrailingSlash (fromMaybe pwd $ fmap realpath maybeBaseDir)
471-
472-
forceTrailingSlash :: FilePath -> FilePath
473-
forceTrailingSlash p = directory (p </> "temp")
474-
475420
importDirBreakdown :: FilePath -> [FilePath]
476421
importDirBreakdown = importDirBreakdown' []
477422

@@ -493,7 +438,7 @@ extractImportDirs inputFile = do
493438
"Have a look at the documentation for a detailed explanation:\n"%s) inputFile (docURL "input-files")
494439

495440
listOwners :: HasBaseDir o => o -> Shell FilePath
496-
listOwners opts = fmap basename $ lsDirs $ (baseDir opts) </> "import"
441+
listOwners opts = fmap basename $ lsDirs $ (turtleBaseDir opts) </> "import"
497442

498443
intPath :: Integer -> FilePath
499444
intPath = fromText . (format d)

src/Hledger/Flow/DocHelpers.hs

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
3+
module Hledger.Flow.DocHelpers where
4+
5+
import Data.Text
6+
import Turtle
7+
8+
docURL :: Line -> Text
9+
docURL = format ("https://github.com/apauley/hledger-flow#"%l)

src/Hledger/Flow/PathHelpers.hs

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
3+
module Hledger.Flow.PathHelpers where
4+
5+
import Control.Monad.Catch (MonadThrow, Exception, throwM)
6+
import Control.Monad.IO.Class (MonadIO)
7+
8+
import qualified Data.Text as T
9+
import qualified Path as Path
10+
import qualified Path.IO as Path
11+
import qualified Turtle as Turtle
12+
13+
import Hledger.Flow.DocHelpers (docURL)
14+
15+
type TurtlePath = Turtle.FilePath
16+
17+
type AbsFile = Path.Path Path.Abs Path.File
18+
type RelFile = Path.Path Path.Rel Path.File
19+
type AbsDir = Path.Path Path.Abs Path.Dir
20+
type RelDir = Path.Path Path.Rel Path.Dir
21+
22+
data PathException = MissingBaseDir AbsDir | InvalidTurtleDir TurtlePath
23+
deriving (Eq)
24+
25+
instance Show PathException where
26+
show (MissingBaseDir d) = "Unable to find an import directory at " ++ show d ++
27+
" (or in any of its parent directories).\n\n" ++
28+
"Have a look at the documentation for more information:\n" ++
29+
T.unpack (docURL "getting-started")
30+
show (InvalidTurtleDir d) = "Expected a directory but got this instead: " ++ Turtle.encodeString d
31+
32+
instance Exception PathException
33+
34+
fromTurtleAbsFile :: MonadThrow m => TurtlePath -> m AbsFile
35+
fromTurtleAbsFile turtlePath = Path.parseAbsFile $ Turtle.encodeString turtlePath
36+
37+
fromTurtleRelFile :: MonadThrow m => TurtlePath -> m RelFile
38+
fromTurtleRelFile turtlePath = Path.parseRelFile $ Turtle.encodeString turtlePath
39+
40+
fromTurtleAbsDir :: MonadThrow m => TurtlePath -> m AbsDir
41+
fromTurtleAbsDir turtlePath = Path.parseAbsDir $ Turtle.encodeString turtlePath
42+
43+
fromTurtleRelDir :: MonadThrow m => TurtlePath -> m RelDir
44+
fromTurtleRelDir turtlePath = Path.parseRelDir $ Turtle.encodeString turtlePath
45+
46+
turtleToAbsDir :: (MonadIO m, MonadThrow m) => AbsDir -> TurtlePath -> m AbsDir
47+
turtleToAbsDir baseDir p = do
48+
isDir <- Turtle.testdir p
49+
if isDir
50+
then Path.resolveDir baseDir $ Turtle.encodeString p
51+
else throwM $ InvalidTurtleDir p
52+
53+
pathToTurtle :: Path.Path b t -> TurtlePath
54+
pathToTurtle = Turtle.decodeString . Path.toFilePath
55+
56+
forceTrailingSlash :: TurtlePath -> TurtlePath
57+
forceTrailingSlash p = Turtle.directory (p Turtle.</> "temp")

src/Hledger/Flow/Reports.hs

+6-3
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ module Hledger.Flow.Reports
66

77
import Turtle hiding (stdout, stderr, proc)
88
import Prelude hiding (FilePath, putStrLn, writeFile)
9+
910
import Hledger.Flow.RuntimeOptions
1011
import Hledger.Flow.Common
12+
import Hledger.Flow.BaseDir (turtleBaseDir, relativeToBase)
13+
1114
import Control.Concurrent.STM
1215
import Data.Either
1316
import Data.Maybe
@@ -46,7 +49,7 @@ generateReports' opts ch = do
4649
channelOutLn ch wipMsg
4750
owners <- single $ shellToList $ listOwners opts
4851
ledgerEnvValue <- need "LEDGER_FILE" :: IO (Maybe Text)
49-
let hledgerJournal = fromMaybe (baseDir opts </> allYearsFileName) $ fmap fromText ledgerEnvValue
52+
let hledgerJournal = fromMaybe (turtleBaseDir opts </> allYearsFileName) $ fmap fromText ledgerEnvValue
5053
hledgerJournalExists <- testfile hledgerJournal
5154
_ <- if not hledgerJournalExists then die $ format ("Unable to find journal file: "%fp%"\nIs your LEDGER_FILE environment variable set correctly?") hledgerJournal else return ()
5255
let journalWithYears = journalFile opts []
@@ -119,10 +122,10 @@ generateReport opts ch journal baseOutDir year fileName args successCheck = do
119122
return $ Left outputFile
120123

121124
journalFile :: RuntimeOptions -> [FilePath] -> FilePath
122-
journalFile opts dirs = (foldl (</>) (baseDir opts) ("import":dirs)) </> allYearsFileName
125+
journalFile opts dirs = (foldl (</>) (turtleBaseDir opts) ("import":dirs)) </> allYearsFileName
123126

124127
outputReportDir :: RuntimeOptions -> [FilePath] -> FilePath
125-
outputReportDir opts dirs = foldl (</>) (baseDir opts) ("reports":dirs)
128+
outputReportDir opts dirs = foldl (</>) (turtleBaseDir opts) ("reports":dirs)
126129

127130
ownerParameters :: RuntimeOptions -> TChan FlowTypes.LogMessage -> [FilePath] -> IO [ReportParams]
128131
ownerParameters opts ch owners = do

src/Hledger/Flow/RuntimeOptions.hs

+5-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import Turtle
55
import Prelude hiding (FilePath, putStrLn)
66
import Hledger.Flow.Types
77

8-
data RuntimeOptions = RuntimeOptions { baseDir :: FilePath
9-
, importRunDir :: FilePath
8+
data RuntimeOptions = RuntimeOptions { baseDir :: BaseDir
9+
, importRunDir :: RunDir
1010
, useRunDir :: Bool
1111
, hfVersion :: Text
1212
, hledgerInfo :: HledgerInfo
@@ -25,3 +25,6 @@ instance HasSequential RuntimeOptions where
2525

2626
instance HasBaseDir RuntimeOptions where
2727
baseDir (RuntimeOptions bd _ _ _ _ _ _ _ _) = bd
28+
29+
instance HasRunDir RuntimeOptions where
30+
importRunDir (RuntimeOptions _ rd _ _ _ _ _ _ _) = rd

0 commit comments

Comments
 (0)