lib: csv: refactor, allow writing different rules text
This commit is contained in:
parent
9cfb7bf6af
commit
ea1f19c71e
@ -20,6 +20,8 @@ module Hledger.Read.CsvReader (
|
|||||||
-- rules,
|
-- rules,
|
||||||
rulesFileFor,
|
rulesFileFor,
|
||||||
parseRulesFile,
|
parseRulesFile,
|
||||||
|
parseAndValidateCsvRules,
|
||||||
|
expandIncludes,
|
||||||
transactionFromCsvRecord,
|
transactionFromCsvRecord,
|
||||||
-- * Tests
|
-- * Tests
|
||||||
tests_Hledger_Read_CsvReader
|
tests_Hledger_Read_CsvReader
|
||||||
@ -100,11 +102,13 @@ readJournalFromCsv mrulesfile csvfile csvdata =
|
|||||||
-- parse rules
|
-- parse rules
|
||||||
let rulesfile = fromMaybe (rulesFileFor csvfile) mrulesfile
|
let rulesfile = fromMaybe (rulesFileFor csvfile) mrulesfile
|
||||||
rulesfileexists <- doesFileExist rulesfile
|
rulesfileexists <- doesFileExist rulesfile
|
||||||
when rulesfileexists $ hPrintf stderr "using conversion rules file %s\n" rulesfile
|
rulestext <-
|
||||||
rules <-
|
|
||||||
if rulesfileexists
|
if rulesfileexists
|
||||||
then liftIO (runExceptT $ parseRulesFile rulesfile) >>= either throwerr return
|
then do
|
||||||
else return defaultRules
|
hPrintf stderr "using conversion rules file %s\n" rulesfile
|
||||||
|
liftIO $ (readFile' rulesfile >>= expandIncludes (takeDirectory rulesfile))
|
||||||
|
else return $ defaultRulesText rulesfile
|
||||||
|
rules <- liftIO (runExceptT $ parseAndValidateCsvRules rulesfile rulestext) >>= either throwerr return
|
||||||
dbg2IO "rules" rules
|
dbg2IO "rules" rules
|
||||||
|
|
||||||
-- apply skip directive
|
-- apply skip directive
|
||||||
@ -114,7 +118,7 @@ readJournalFromCsv mrulesfile csvfile csvdata =
|
|||||||
oneorerror s = readDef (throwerr $ "could not parse skip value: " ++ show s) s
|
oneorerror s = readDef (throwerr $ "could not parse skip value: " ++ show s) s
|
||||||
|
|
||||||
-- parse csv
|
-- parse csv
|
||||||
-- parsec seems to fail if you pass it "-" here
|
-- parsec seems to fail if you pass it "-" here XXX try again with megaparsec
|
||||||
let parsecfilename = if csvfile == "-" then "(stdin)" else csvfile
|
let parsecfilename = if csvfile == "-" then "(stdin)" else csvfile
|
||||||
records <- (either throwerr id .
|
records <- (either throwerr id .
|
||||||
dbg2 "validateCsv" . validateCsv skip .
|
dbg2 "validateCsv" . validateCsv skip .
|
||||||
@ -144,7 +148,7 @@ readJournalFromCsv mrulesfile csvfile csvdata =
|
|||||||
|
|
||||||
when (not rulesfileexists) $ do
|
when (not rulesfileexists) $ do
|
||||||
hPrintf stderr "created default conversion rules file %s, edit this for better results\n" rulesfile
|
hPrintf stderr "created default conversion rules file %s, edit this for better results\n" rulesfile
|
||||||
writeFile rulesfile $ T.unpack $ defaultRulesText rulesfile
|
writeFile rulesfile $ T.unpack rulestext
|
||||||
|
|
||||||
return $ Right nulljournal{jtxns=sortBy (comparing tdate) txns'}
|
return $ Right nulljournal{jtxns=sortBy (comparing tdate) txns'}
|
||||||
|
|
||||||
@ -219,13 +223,6 @@ defaultRulesText csvfile = T.pack $ unlines
|
|||||||
," account2 assets:bank:savings\n"
|
," account2 assets:bank:savings\n"
|
||||||
]
|
]
|
||||||
|
|
||||||
defaultRules :: CsvRules
|
|
||||||
defaultRules =
|
|
||||||
either
|
|
||||||
(error' "Could not parse the default CSV rules, this should not happen")
|
|
||||||
id
|
|
||||||
$ parseCsvRules "" $ defaultRulesText ""
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
-- Conversion rules parsing
|
-- Conversion rules parsing
|
||||||
|
|
||||||
@ -351,23 +348,18 @@ getDirective directivename = lookup directivename . rdirectives
|
|||||||
instance ShowErrorComponent String where
|
instance ShowErrorComponent String where
|
||||||
showErrorComponent = id
|
showErrorComponent = id
|
||||||
|
|
||||||
|
-- | An error-throwing action that parses this file's content
|
||||||
|
-- as CSV conversion rules, interpolating any included files first,
|
||||||
|
-- and runs some extra validation checks.
|
||||||
parseRulesFile :: FilePath -> ExceptT String IO CsvRules
|
parseRulesFile :: FilePath -> ExceptT String IO CsvRules
|
||||||
parseRulesFile f = do
|
parseRulesFile f =
|
||||||
s <- liftIO $ (readFile' f >>= expandIncludes (takeDirectory f))
|
liftIO (readFile' f >>= expandIncludes (takeDirectory f)) >>= parseAndValidateCsvRules f
|
||||||
let rules = parseCsvRules f s
|
|
||||||
case rules of
|
|
||||||
Left e -> ExceptT $ return $ Left $ parseErrorPretty e
|
|
||||||
Right r -> do
|
|
||||||
r_ <- liftIO $ runExceptT $ validateRules r
|
|
||||||
ExceptT $ case r_ of
|
|
||||||
Left e -> return $ Left $ parseErrorPretty $ toParseError e
|
|
||||||
Right r -> return $ Right r
|
|
||||||
where
|
|
||||||
toParseError :: forall s. Ord s => s -> ParseError Char s
|
|
||||||
toParseError s = (mempty :: ParseError Char s) { errorCustom = S.singleton s}
|
|
||||||
|
|
||||||
-- | Pre-parse csv rules to interpolate included files, recursively.
|
-- | Look for hledger rules file-style include directives in this text,
|
||||||
-- This is a cheap hack to avoid rewriting the existing parser.
|
-- and interpolate the included files, recursively.
|
||||||
|
-- Included file paths may be relative to the directory of the
|
||||||
|
-- provided file path.
|
||||||
|
-- This is a cheap hack to avoid rewriting the CSV rules parser.
|
||||||
expandIncludes :: FilePath -> T.Text -> IO T.Text
|
expandIncludes :: FilePath -> T.Text -> IO T.Text
|
||||||
expandIncludes basedir content = do
|
expandIncludes basedir content = do
|
||||||
let (ls,rest) = break (T.isPrefixOf "include") $ T.lines content
|
let (ls,rest) = break (T.isPrefixOf "include") $ T.lines content
|
||||||
@ -380,6 +372,23 @@ expandIncludes basedir content = do
|
|||||||
return $ T.unlines [T.unlines ls, included, T.unlines ls']
|
return $ T.unlines [T.unlines ls, included, T.unlines ls']
|
||||||
ls' -> return $ T.unlines $ ls ++ ls' -- should never get here
|
ls' -> return $ T.unlines $ ls ++ ls' -- should never get here
|
||||||
|
|
||||||
|
-- | An error-throwing action that parses this text as CSV conversion rules
|
||||||
|
-- and runs some extra validation checks. The file path is for error messages.
|
||||||
|
parseAndValidateCsvRules :: FilePath -> T.Text -> ExceptT String IO CsvRules
|
||||||
|
parseAndValidateCsvRules rulesfile s = do
|
||||||
|
let rules = parseCsvRules rulesfile s
|
||||||
|
case rules of
|
||||||
|
Left e -> ExceptT $ return $ Left $ parseErrorPretty e
|
||||||
|
Right r -> do
|
||||||
|
r_ <- liftIO $ runExceptT $ validateRules r
|
||||||
|
ExceptT $ case r_ of
|
||||||
|
Left e -> return $ Left $ parseErrorPretty $ toParseError e
|
||||||
|
Right r -> return $ Right r
|
||||||
|
where
|
||||||
|
toParseError :: forall s. Ord s => s -> ParseError Char s
|
||||||
|
toParseError s = (mempty :: ParseError Char s) { errorCustom = S.singleton s}
|
||||||
|
|
||||||
|
-- | Parse this text as CSV conversion rules. The file path is for error messages.
|
||||||
parseCsvRules :: FilePath -> T.Text -> Either (ParseError Char Dec) CsvRules
|
parseCsvRules :: FilePath -> T.Text -> Either (ParseError Char Dec) CsvRules
|
||||||
-- parseCsvRules rulesfile s = runParser csvrulesfile nullrules{baseAccount=takeBaseName rulesfile} rulesfile s
|
-- parseCsvRules rulesfile s = runParser csvrulesfile nullrules{baseAccount=takeBaseName rulesfile} rulesfile s
|
||||||
parseCsvRules rulesfile s =
|
parseCsvRules rulesfile s =
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user