|
@@ -0,0 +1,187 @@
|
|
|
+#![allow(unused)]
|
|
|
+
|
|
|
+use std::collections::HashMap;
|
|
|
+use std::rc::Rc;
|
|
|
+
|
|
|
+use ariadne::Cache;
|
|
|
+
|
|
|
+mod ledger;
|
|
|
+mod spec;
|
|
|
+
|
|
|
+pub struct LocationTag;
|
|
|
+impl stringstore::NamespaceTag for LocationTag {
|
|
|
+ const PREFIX: &'static str = "loc";
|
|
|
+}
|
|
|
+pub type SourceFile = stringstore::StoredOsString<LocationTag>;
|
|
|
+
|
|
|
+pub struct UnitTag;
|
|
|
+impl stringstore::NamespaceTag for UnitTag {
|
|
|
+ const PREFIX: &'static str = "unit";
|
|
|
+}
|
|
|
+pub type UnitName = stringstore::StoredString<UnitTag>;
|
|
|
+
|
|
|
+pub struct AccountTag;
|
|
|
+impl stringstore::NamespaceTag for AccountTag {
|
|
|
+ const PREFIX: &'static str = "acc";
|
|
|
+}
|
|
|
+pub type AccountName = stringstore::StoredString<AccountTag>;
|
|
|
+
|
|
|
+#[derive(Clone, Debug, PartialEq, Hash)]
|
|
|
+pub struct DataSource {
|
|
|
+ file: SourceFile,
|
|
|
+ range: std::ops::Range<usize>,
|
|
|
+}
|
|
|
+
|
|
|
+#[derive(Default)]
|
|
|
+pub struct FilesystemData {
|
|
|
+ file_data: HashMap<SourceFile, ariadne::Source<std::rc::Rc<str>>>,
|
|
|
+}
|
|
|
+
|
|
|
+impl std::fmt::Debug for FilesystemData {
|
|
|
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
+ f.debug_struct("FilesystemData").finish()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl ariadne::Cache<SourceFile> for FilesystemData {
|
|
|
+ type Storage = std::rc::Rc<str>;
|
|
|
+ fn fetch(
|
|
|
+ &mut self,
|
|
|
+ id: &SourceFile,
|
|
|
+ ) -> Result<&ariadne::Source<Self::Storage>, impl std::fmt::Debug> {
|
|
|
+ if !self.file_data.contains_key(id) {
|
|
|
+ match std::fs::read_to_string(id.as_str()) {
|
|
|
+ Ok(data) => {
|
|
|
+ let data: std::rc::Rc<_> = std::rc::Rc::from(data.into_boxed_str());
|
|
|
+ self.file_data.insert(*id, ariadne::Source::from(data));
|
|
|
+ }
|
|
|
+ Err(e) => return Err(e),
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Ok(self.file_data.get(id).unwrap())
|
|
|
+ }
|
|
|
+
|
|
|
+ fn display<'a>(&self, id: &'a SourceFile) -> Option<impl std::fmt::Display + 'a> {
|
|
|
+ Some(id.as_str().to_string_lossy())
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#[derive(Debug)]
|
|
|
+pub enum DataError {
|
|
|
+ IOError(std::io::Error),
|
|
|
+ Report(ariadne::Report<'static, (SourceFile, std::ops::Range<usize>)>),
|
|
|
+}
|
|
|
+
|
|
|
+impl std::fmt::Display for DataError {
|
|
|
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
+ <Self as std::fmt::Debug>::fmt(self, f)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl std::error::Error for DataError {}
|
|
|
+
|
|
|
+#[derive(Debug)]
|
|
|
+pub struct Root {
|
|
|
+ path: std::path::PathBuf,
|
|
|
+ root_spec: spec::RootSpec,
|
|
|
+
|
|
|
+ ledger_data: Vec<ledger::LedgerEntry>,
|
|
|
+
|
|
|
+ account_ledger_data: HashMap<AccountName, Vec<ledger::LedgerEntry>>,
|
|
|
+}
|
|
|
+
|
|
|
+impl Root {
|
|
|
+ pub fn load<'l, 'm>(
|
|
|
+ fsdata: &'l mut FilesystemData,
|
|
|
+ path: &'m std::path::Path,
|
|
|
+ ) -> Result<Self, DataError> {
|
|
|
+ let sf = SourceFile::new(path.as_os_str());
|
|
|
+ let root_data = fsdata.fetch(&sf).unwrap();
|
|
|
+
|
|
|
+ match toml::from_str::<spec::RootSpec>(root_data.text()) {
|
|
|
+ Ok(root_spec) => {
|
|
|
+ let mut r = Self {
|
|
|
+ path: path.into(),
|
|
|
+ root_spec,
|
|
|
+ ledger_data: vec![],
|
|
|
+ account_ledger_data: Default::default(),
|
|
|
+ };
|
|
|
+
|
|
|
+ r.load_ledgers(fsdata)?;
|
|
|
+ r.preprocess_ledger_data();
|
|
|
+
|
|
|
+ Ok(r)
|
|
|
+ }
|
|
|
+ Err(te) => {
|
|
|
+ let Some(range) = te.span() else {
|
|
|
+ panic!("TOML parse error with no range: {te}");
|
|
|
+ };
|
|
|
+
|
|
|
+ let report =
|
|
|
+ ariadne::Report::build(ariadne::ReportKind::Error, (sf, range.clone()))
|
|
|
+ .with_label(ariadne::Label::new((sf, range)).with_message(te.message()))
|
|
|
+ .with_message("Failed to parse root TOML")
|
|
|
+ .finish();
|
|
|
+
|
|
|
+ Err(DataError::Report(report))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ fn load_ledger<'s, 'd>(
|
|
|
+ &'s mut self,
|
|
|
+ fsdata: &'d mut FilesystemData,
|
|
|
+ path: &mut std::path::PathBuf,
|
|
|
+ ) -> Result<(), DataError> {
|
|
|
+ log::debug!("Loading ledger data from {}", path.display());
|
|
|
+
|
|
|
+ let md = std::fs::metadata(path.as_path()).map_err(DataError::IOError)?;
|
|
|
+
|
|
|
+ if md.is_dir() {
|
|
|
+ // recurse
|
|
|
+ for de in std::fs::read_dir(path.as_path()).map_err(DataError::IOError)? {
|
|
|
+ let de = de.map_err(DataError::IOError)?;
|
|
|
+ path.push(de.file_name());
|
|
|
+
|
|
|
+ self.load_ledger(fsdata, path)?;
|
|
|
+
|
|
|
+ path.pop();
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ let s = SourceFile::new(path.as_os_str());
|
|
|
+ let data = fsdata.fetch(&s).unwrap();
|
|
|
+ self.ledger_data
|
|
|
+ .extend(ledger::parse_ledger(s, data.text())?);
|
|
|
+ }
|
|
|
+
|
|
|
+ Ok(())
|
|
|
+ }
|
|
|
+
|
|
|
+ fn load_ledgers<'s, 'd>(&'s mut self, fsdata: &'d mut FilesystemData) -> Result<(), DataError> {
|
|
|
+ let mut ledger_path = self.path.to_owned();
|
|
|
+ ledger_path.pop();
|
|
|
+ ledger_path.push(&self.root_spec.ledger_path);
|
|
|
+
|
|
|
+ self.load_ledger(fsdata, &mut ledger_path)?;
|
|
|
+
|
|
|
+ self.ledger_data.sort();
|
|
|
+
|
|
|
+ Ok(())
|
|
|
+ }
|
|
|
+
|
|
|
+ fn preprocess_ledger_data(&mut self) {
|
|
|
+ for entry in &self.ledger_data {
|
|
|
+ for bal in &entry.balances {
|
|
|
+ self.account_ledger_data
|
|
|
+ .entry(bal.account)
|
|
|
+ .or_default()
|
|
|
+ .push(entry.clone());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ pub fn ledger_data_for(&self, aname: AccountName) -> Option<&[ledger::LedgerEntry]> {
|
|
|
+ self.account_ledger_data.get(&aname).map(Vec::as_slice)
|
|
|
+ }
|
|
|
+}
|