use core::mem;
use beancount_pretty_printer::PrettyPrinter;
use beancount_tree_writer::Config as TreeWriterConfig;
use beancount_tree_writer::TreeWriter;
use beancount_types::common_keys;
use beancount_types::Directive;
use camino::Utf8Path;
use camino::Utf8PathBuf;
use miette::Context as _;
use miette::IntoDiagnostic as _;
use miette::Report as ErrorReport;
use miette::Result;
use relative_path::RelativePathBuf;
use crate::ImporterProtocol;
use crate::ImporterRegistry;
mod archive;
#[derive(Debug)]
pub struct Builder {
archive_directory: Utf8PathBuf,
dry_run: bool,
importers: ImporterRegistry<ErrorReport>,
overwrite: bool,
}
impl Builder {
pub fn archive_directory(&mut self, directory: Utf8PathBuf) -> &mut Self {
self.archive_directory = directory;
self
}
pub fn build(&mut self) -> Runner {
let Self {
archive_directory,
dry_run,
importers,
overwrite,
} = mem::take(self);
let archive_directory = archive_directory.canonicalize_utf8().unwrap();
Runner {
archive_directory,
dry_run,
importers,
overwrite,
}
}
pub fn dry_run(&mut self) -> &mut Self {
self.dry_run = true;
self
}
pub fn overwrite(&mut self) -> &mut Self {
self.overwrite = true;
self
}
pub fn register_importer(
&mut self,
importer: impl ImporterProtocol<Error = ErrorReport> + 'static,
) -> &mut Self {
self.importers.register_importer(importer);
self
}
pub fn with_dry_run(&mut self, dry_run: bool) -> &mut Self {
self.dry_run = dry_run;
self
}
pub fn with_overwrite(&mut self, overwrite: bool) -> &mut Self {
self.overwrite = overwrite;
self
}
}
impl Default for Builder {
fn default() -> Self {
let (dry_run, importers, overwrite) = Default::default();
Self {
archive_directory: Utf8PathBuf::from("archive"),
dry_run,
importers,
overwrite,
}
}
}
#[derive(Debug)]
pub struct Runner {
archive_directory: Utf8PathBuf,
dry_run: bool,
importers: ImporterRegistry<ErrorReport>,
overwrite: bool,
}
impl Runner {
pub fn builder() -> Builder {
Builder::default()
}
}
impl Runner {
#[tracing::instrument]
pub fn run(&self, paths: &[Utf8PathBuf], tree_writer_config: TreeWriterConfig) -> Result<()> {
let directives = self.process_paths(paths)?;
if directives.is_empty() {
tracing::warn!("extracted no directives");
return Ok(());
}
tracing::info!("extracted {} directives", directives.len());
if self.dry_run {
let config = beancount_pretty_printer::Config::derive_from_directives(&directives);
let mut printer = PrettyPrinter::unbuffered(config, std::io::stdout().lock());
printer.print_directives(&directives).into_diagnostic()?;
} else {
TreeWriter::new(tree_writer_config).write_directives(directives)?;
tracing::info!("successfully wrote tree of accounts");
}
Ok(())
}
}
impl Runner {
#[tracing::instrument(fields(importer = importer.name()), skip(self))]
fn archive_file<'a>(
&self,
importer: &(dyn ImporterProtocol<Error = ErrorReport>),
file: &'a Utf8Path,
destination: RelativePathBuf,
) -> Result<()> {
if self.dry_run {
return Ok(());
}
let destination = destination.to_logical_path(&self.archive_directory);
let destination = Utf8Path::from_path(&destination).expect("should be UTF-8");
if file != destination {
tracing::info!(?file, ?destination, "archiving file");
if destination.exists() {
miette::ensure!(
self.overwrite,
"destination {destination:?} already exists",
destination = destination,
);
}
archive::move_file(file, destination).wrap_err("while moving file into archive")?;
}
Ok(())
}
#[tracing::instrument(skip(self, buffer))]
fn identify_file(&self, buffer: &[u8]) -> Option<&dyn ImporterProtocol<Error = ErrorReport>> {
self.importers
.iter()
.find(|importer| importer.identify(buffer).unwrap_or_default())
}
#[tracing::instrument(skip(self, directives))]
fn process_dir(&self, directives: &mut Vec<Directive>, path: &Utf8Path) -> Result<()> {
let entries = path
.read_dir_utf8()
.into_diagnostic()
.wrap_err("could not read directory")?;
for entry in entries {
let entry = entry.into_diagnostic()?;
let path = entry.path();
self.process_path(directives, path)
}
Ok(())
}
#[tracing::instrument(skip(self, directives))]
fn process_file(&self, directives: &mut Vec<Directive>, file: &Utf8Path) -> Result<()> {
let buffer = std::fs::read(file).into_diagnostic()?;
let Some(importer) = self.identify_file(&buffer) else {
tracing::warn!(%file, "ignoring file since no importer could identify it");
return Ok(());
};
let destination = archive::file_name(importer, file, &buffer)?;
let mut extracted_directives = importer
.extract(&buffer, &[]) .wrap_err_with(|| format!("error in importer {:?}", importer.name()))?;
for directive in &mut extracted_directives {
directive.add_meta(common_keys::IMPORTED_FROM, destination.as_str());
}
self.archive_file(importer, file, destination)
.wrap_err("while archiving file")?;
directives.append(&mut extracted_directives);
Ok(())
}
#[tracing::instrument(skip(self, directives))]
fn process_path(&self, directives: &mut Vec<Directive>, path: &Utf8Path) {
let (kind, result) = if path.is_dir() {
("directory", self.process_dir(directives, path))
} else {
("file", self.process_file(directives, path))
};
if let Err(error) = result {
tracing::error!(%path, ?error, "error while importing {kind}");
}
}
#[tracing::instrument(skip(self))]
fn process_paths(&self, paths: &[Utf8PathBuf]) -> Result<Vec<Directive>> {
paths.iter().try_fold(Vec::new(), |mut directives, path| {
let path = path.canonicalize_utf8().into_diagnostic()?;
self.process_path(&mut directives, &path);
Ok(directives)
})
}
}