diff --git a/Cargo.lock b/Cargo.lock index a9e4885..f81a1ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,17 @@ dependencies = [ "libc", ] +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atoi" version = "2.0.0" @@ -676,6 +687,7 @@ checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" name = "libdrcr" version = "0.1.0" dependencies = [ + "async-trait", "chrono", "downcast-rs", "dyn-clone", diff --git a/Cargo.toml b/Cargo.toml index d5bbea0..1546094 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +async-trait = "0.1.88" chrono = "0.4.41" downcast-rs = "2.0.1" dyn-clone = "1.0.19" diff --git a/src/db.rs b/src/db.rs index c977132..05d1595 100644 --- a/src/db.rs +++ b/src/db.rs @@ -17,39 +17,26 @@ */ use std::collections::HashMap; -use std::ops::DerefMut; -use std::{cell::RefCell, future::Future}; use chrono::NaiveDate; use sqlx::sqlite::SqliteRow; use sqlx::{Connection, Row, SqliteConnection}; -use tokio::runtime::Runtime; use crate::account_config::AccountConfiguration; use crate::{util::format_date, QuantityInt}; pub struct DbConnection { - sqlx_connection: RefCell, + url: String, metadata: DbMetadata, } -fn run_blocking(future: F) -> F::Output { - let rt = Runtime::new().unwrap(); - rt.block_on(future) -} - impl DbConnection { - /// Connect to the given Sqlite database - pub fn connect(url: &str) -> Self { - run_blocking(DbConnection::connect_async(url)) - } - - async fn connect_async(url: &str) -> Self { + pub async fn new(url: &str) -> Self { let mut connection = SqliteConnection::connect(url).await.expect("SQL error"); let metadata = DbMetadata::from_database(&mut connection).await; Self { - sqlx_connection: RefCell::new(connection), + url: url.to_string(), metadata, } } @@ -58,13 +45,15 @@ impl DbConnection { &self.metadata } - /// Get account balances from the database - pub fn get_balances(&self, date: NaiveDate) -> HashMap { - run_blocking(self.get_balances_async(date)) + pub async fn connect(&self) -> SqliteConnection { + SqliteConnection::connect(&self.url) + .await + .expect("SQL error") } - async fn get_balances_async(&self, date: NaiveDate) -> HashMap { - let mut connection = self.sqlx_connection.borrow_mut(); + /// Get account balances from the database + pub async fn get_balances(&self, date: NaiveDate) -> HashMap { + let mut connection = self.connect().await; let rows = sqlx::query( "-- Get last transaction for each account @@ -84,7 +73,7 @@ impl DbConnection { SELECT max_tid_by_account.account, running_balance AS quantity FROM max_tid_by_account JOIN transactions_with_running_balances ON max_tid = transactions_with_running_balances.transaction_id AND max_tid_by_account.account = transactions_with_running_balances.account" - ).bind(format_date(date)).fetch_all(connection.deref_mut()).await.expect("SQL error"); + ).bind(format_date(date)).fetch_all(&mut connection).await.expect("SQL error"); let mut balances = HashMap::new(); for row in rows { @@ -95,12 +84,8 @@ impl DbConnection { } /// Get account configurations from the database - pub fn get_account_configurations(&self) -> Vec { - run_blocking(self.get_account_configurations_async()) - } - - async fn get_account_configurations_async(&self) -> Vec { - let mut connection = self.sqlx_connection.borrow_mut(); + pub async fn get_account_configurations(&self) -> Vec { + let mut connection = self.connect().await; let mut account_configurations = sqlx::query("SELECT id, account, kind, data FROM account_configurations") @@ -110,7 +95,7 @@ impl DbConnection { kind: r.get("kind"), data: r.get("data"), }) - .fetch_all(connection.deref_mut()) + .fetch_all(&mut connection) .await .expect("SQL error"); diff --git a/src/main.rs b/src/main.rs index 27da957..2037235 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,11 +28,12 @@ use libdrcr::reporting::types::{ ReportingProductKind, VoidArgs, }; -fn main() { +#[tokio::main] +async fn main() { const YEAR: i32 = 2023; // Connect to database - let db_connection = DbConnection::connect("sqlite:drcr_testing.db"); + let db_connection = DbConnection::new("sqlite:drcr_testing.db").await; // Initialise ReportingContext let mut context = ReportingContext::new( @@ -85,7 +86,7 @@ fn main() { }, ]; - let products = generate_report(targets, &context).unwrap(); + let products = generate_report(targets, &context).await.unwrap(); let result = products .get_or_err(&ReportingProductId { name: "AllTransactionsExceptEarningsToEquity", @@ -119,7 +120,7 @@ fn main() { }, ]; - let products = generate_report(targets, &context).unwrap(); + let products = generate_report(targets, &context).await.unwrap(); let result = products .get_or_err(&ReportingProductId { name: "BalanceSheet", diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index d6a5e8b..1f1e860 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -23,6 +23,9 @@ use std::collections::HashMap; use std::fmt::Display; +use async_trait::async_trait; +use tokio::sync::RwLock; + use crate::transaction::update_balances_from_transactions; use super::calculator::{has_step_or_can_build, HasStepOrCanBuild, ReportingGraphDependencies}; @@ -124,6 +127,7 @@ impl Display for BalancesAtToBalancesBetween { } } +#[async_trait] impl ReportingStep for BalancesAtToBalancesBetween { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -153,13 +157,15 @@ impl ReportingStep for BalancesAtToBalancesBetween { ] } - fn execute( + async fn execute( &self, _context: &ReportingContext, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + products: &RwLock, + ) -> Result { + let products = products.read().await; + // Get balances at dates let balances_start = &products .get_or_err(&ReportingProductId { @@ -196,7 +202,8 @@ impl ReportingStep for BalancesAtToBalancesBetween { } // Store result - products.insert( + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: self.id().name, kind: ReportingProductKind::BalancesBetween, @@ -204,8 +211,7 @@ impl ReportingStep for BalancesAtToBalancesBetween { }, Box::new(balances), ); - - Ok(()) + Ok(result) } } @@ -285,6 +291,7 @@ impl Display for GenerateBalances { } } +#[async_trait] impl ReportingStep for GenerateBalances { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -303,13 +310,15 @@ impl ReportingStep for GenerateBalances { }] } - fn execute( + async fn execute( &self, _context: &ReportingContext, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + products: &RwLock, + ) -> Result { + let products = products.read().await; + // Get the transactions let transactions = &products .get_or_err(&ReportingProductId { @@ -328,7 +337,8 @@ impl ReportingStep for GenerateBalances { update_balances_from_transactions(&mut balances.balances, transactions.iter()); // Store result - products.insert( + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: self.step_name, kind: ReportingProductKind::BalancesAt, @@ -336,8 +346,7 @@ impl ReportingStep for GenerateBalances { }, Box::new(balances), ); - - Ok(()) + Ok(result) } } @@ -441,6 +450,7 @@ impl Display for UpdateBalancesAt { } } +#[async_trait] impl ReportingStep for UpdateBalancesAt { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -499,13 +509,15 @@ impl ReportingStep for UpdateBalancesAt { } } - fn execute( + async fn execute( &self, _context: &ReportingContext, steps: &Vec>, dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + products: &RwLock, + ) -> Result { + let products = products.read().await; + // Look up the parent step, so we can extract the appropriate args let parent_step = steps .iter() @@ -566,7 +578,8 @@ impl ReportingStep for UpdateBalancesAt { ); // Store result - products.insert( + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: self.step_name, kind: ReportingProductKind::BalancesAt, @@ -574,8 +587,7 @@ impl ReportingStep for UpdateBalancesAt { }, Box::new(balances), ); - - Ok(()) + Ok(result) } } @@ -646,6 +658,7 @@ impl Display for UpdateBalancesBetween { } } +#[async_trait] impl ReportingStep for UpdateBalancesBetween { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -683,13 +696,15 @@ impl ReportingStep for UpdateBalancesBetween { ); } - fn execute( + async fn execute( &self, _context: &ReportingContext, steps: &Vec>, dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + products: &RwLock, + ) -> Result { + let products = products.read().await; + // Look up the parent step, so we can extract the appropriate args let parent_step = steps .iter() @@ -736,7 +751,8 @@ impl ReportingStep for UpdateBalancesBetween { ); // Store result - products.insert( + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: self.step_name, kind: ReportingProductKind::BalancesBetween, @@ -744,7 +760,6 @@ impl ReportingStep for UpdateBalancesBetween { }, Box::new(balances), ); - - Ok(()) + Ok(result) } } diff --git a/src/reporting/dynamic_report.rs b/src/reporting/dynamic_report.rs index d1b25a6..82ef0ca 100644 --- a/src/reporting/dynamic_report.rs +++ b/src/reporting/dynamic_report.rs @@ -16,6 +16,8 @@ along with this program. If not, see . */ +// FIXME: Tidy up this file + use std::cell::RefCell; use std::collections::HashMap; @@ -25,17 +27,21 @@ use crate::QuantityInt; use super::types::{GenericReportingProduct, ReportingProduct}; -/// Represents a dynamically generated report composed of [DynamicReportEntry] -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct DynamicReport { +/// Represents a dynamically generated report composed of [CalculatableDynamicReportEntry] +#[derive(Clone, Debug)] +pub struct CalculatableDynamicReport { pub title: String, pub columns: Vec, // This must use RefCell as, during calculation, we iterate while mutating the report - pub entries: Vec>, + pub entries: Vec>, } -impl DynamicReport { - pub fn new(title: String, columns: Vec, entries: Vec) -> Self { +impl CalculatableDynamicReport { + pub fn new( + title: String, + columns: Vec, + entries: Vec, + ) -> Self { Self { title, columns, @@ -43,67 +49,60 @@ impl DynamicReport { } } - /// Remove all entries from the report where auto_hide is enabled and quantity is zero - pub fn auto_hide(&mut self) { - self.entries.retain(|e| match &mut *e.borrow_mut() { - DynamicReportEntry::Section(section) => { - section.auto_hide_children(); - if section.can_auto_hide_self() { - false - } else { - true - } - } - DynamicReportEntry::LiteralRow(row) => { - if row.can_auto_hide() { - false - } else { - true - } - } - DynamicReportEntry::CalculatedRow(_) => true, - DynamicReportEntry::Spacer => true, - }); - } - /// Recursively calculate all [CalculatedRow] entries - pub fn calculate(&mut self) { + pub fn calculate(self) -> DynamicReport { + let mut calculated_entries = Vec::new(); + for (entry_idx, entry) in self.entries.iter().enumerate() { let entry_ref = entry.borrow(); match &*entry_ref { - DynamicReportEntry::Section(section) => { + CalculatableDynamicReportEntry::CalculatableSection(section) => { // Clone first, in case calculation needs to take reference to the section - let mut updated_section = section.clone(); - updated_section.calculate(&self); + let updated_section = section.clone().calculate(&self); drop(entry_ref); // Drop entry_ref so we can borrow mutably let mut entry_mut = self.entries[entry_idx].borrow_mut(); - *entry_mut = DynamicReportEntry::Section(updated_section); + *entry_mut = CalculatableDynamicReportEntry::Section(updated_section.clone()); + + calculated_entries.push(DynamicReportEntry::Section(updated_section)); } - DynamicReportEntry::LiteralRow(_) => (), - DynamicReportEntry::CalculatedRow(row) => { + CalculatableDynamicReportEntry::Section(section) => { + calculated_entries.push(DynamicReportEntry::Section(section.clone())); + } + CalculatableDynamicReportEntry::LiteralRow(row) => { + calculated_entries.push(DynamicReportEntry::LiteralRow(row.clone())); + } + CalculatableDynamicReportEntry::CalculatedRow(row) => { let updated_row = row.calculate(&self); drop(entry_ref); // Drop entry_ref so we can borrow mutably let mut entry_mut = self.entries[entry_idx].borrow_mut(); - *entry_mut = DynamicReportEntry::LiteralRow(updated_row); + *entry_mut = CalculatableDynamicReportEntry::LiteralRow(updated_row.clone()); + + calculated_entries.push(DynamicReportEntry::LiteralRow(updated_row)); } - DynamicReportEntry::Spacer => (), + CalculatableDynamicReportEntry::Spacer => (), } } + + DynamicReport { + title: self.title, + columns: self.columns, + entries: calculated_entries, + } } - /// Look up [DynamicReportEntry] by id + /// Look up [CalculatableDynamicReportEntry] by id /// - /// Returns a cloned copy of the [DynamicReportEntry]. This is necessary because the entry may be within a [Section], and [RefCell] semantics cannot express this type of nested borrow. - pub fn by_id(&self, id: &str) -> Option { + /// Returns a cloned copy of the [CalculatableDynamicReportEntry]. This is necessary because the entry may be within a [Section], and [RefCell] semantics cannot express this type of nested borrow. + pub fn by_id(&self, id: &str) -> Option { // Manually iterate over self.entries rather than self.entries() // To catch the situation where entry is already mutably borrowed for entry in self.entries.iter() { match entry.try_borrow() { Ok(entry) => match &*entry { - DynamicReportEntry::Section(section) => { + CalculatableDynamicReportEntry::CalculatableSection(section) => { if let Some(i) = §ion.id { if i == id { return Some(entry.clone()); @@ -113,15 +112,35 @@ impl DynamicReport { return Some(e); } } - DynamicReportEntry::LiteralRow(row) => { + CalculatableDynamicReportEntry::Section(section) => { + if let Some(i) = §ion.id { + if i == id { + return Some(entry.clone()); + } + } + if let Some(e) = section.by_id(id) { + return Some(match e { + DynamicReportEntry::Section(section) => { + CalculatableDynamicReportEntry::Section(section.clone()) + } + DynamicReportEntry::LiteralRow(row) => { + CalculatableDynamicReportEntry::LiteralRow(row.clone()) + } + DynamicReportEntry::Spacer => { + CalculatableDynamicReportEntry::Spacer + } + }); + } + } + CalculatableDynamicReportEntry::LiteralRow(row) => { if let Some(i) = &row.id { if i == id { return Some(entry.clone()); } } } - DynamicReportEntry::CalculatedRow(_) => (), - DynamicReportEntry::Spacer => (), + CalculatableDynamicReportEntry::CalculatedRow(_) => (), + CalculatableDynamicReportEntry::Spacer => (), }, Err(err) => panic!( "Attempt to call by_id on DynamicReportEntry which is mutably borrowed: {}", @@ -136,7 +155,7 @@ impl DynamicReport { /// Calculate the subtotals for the [Section] with the given id pub fn subtotal_for_id(&self, id: &str) -> Vec { let entry = self.by_id(id).expect("Invalid id"); - if let DynamicReportEntry::Section(section) = entry { + if let CalculatableDynamicReportEntry::CalculatableSection(section) = entry { section.subtotal(&self) } else { panic!("Called subtotal_for_id on non-Section"); @@ -146,59 +165,26 @@ impl DynamicReport { // Return the quantities for the [LiteralRow] with the given id pub fn quantity_for_id(&self, id: &str) -> Vec { let entry = self.by_id(id).expect("Invalid id"); - if let DynamicReportEntry::LiteralRow(row) = entry { + if let CalculatableDynamicReportEntry::LiteralRow(row) = entry { row.quantity } else { panic!("Called quantity_for_id on non-LiteralRow"); } } - - /// Serialise the report (as JSON) using serde - pub fn to_json(&self) -> String { - serde_json::to_string(self).unwrap() - } } -impl GenericReportingProduct for DynamicReport {} -impl ReportingProduct for DynamicReport {} - +/// Represents a dynamically generated report composed of [DynamicReportEntry], with no [CalculatedRow]s #[derive(Clone, Debug, Deserialize, Serialize)] -pub enum DynamicReportEntry { - Section(Section), - LiteralRow(LiteralRow), - #[serde(skip)] - CalculatedRow(CalculatedRow), - Spacer, +pub struct DynamicReport { + pub title: String, + pub columns: Vec, + pub entries: Vec, } -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Section { - pub text: String, - pub id: Option, - pub visible: bool, - pub auto_hide: bool, - pub entries: Vec>, -} - -impl Section { - pub fn new( - text: String, - id: Option, - visible: bool, - auto_hide: bool, - entries: Vec, - ) -> Self { - Self { - text, - id, - visible, - auto_hide, - entries: entries.into_iter().map(|e| RefCell::new(e)).collect(), - } - } - - fn auto_hide_children(&mut self) { - self.entries.retain(|e| match &mut *e.borrow_mut() { +impl DynamicReport { + /// Remove all entries from the report where auto_hide is enabled and quantity is zero + pub fn auto_hide(&mut self) { + self.entries.retain_mut(|e| match e { DynamicReportEntry::Section(section) => { section.auto_hide_children(); if section.can_auto_hide_self() { @@ -214,59 +200,116 @@ impl Section { true } } - DynamicReportEntry::CalculatedRow(_) => true, DynamicReportEntry::Spacer => true, }); } - fn can_auto_hide_self(&self) -> bool { - self.auto_hide - && self.entries.iter().all(|e| match &*e.borrow() { - DynamicReportEntry::Section(section) => section.can_auto_hide_self(), - DynamicReportEntry::LiteralRow(row) => row.can_auto_hide(), - DynamicReportEntry::CalculatedRow(_) => false, - DynamicReportEntry::Spacer => true, - }) + /// Serialise the report (as JSON) using serde + pub fn to_json(&self) -> String { + serde_json::to_string(self).unwrap() + } +} + +impl GenericReportingProduct for DynamicReport {} +impl ReportingProduct for DynamicReport {} + +#[derive(Clone, Debug)] +pub enum CalculatableDynamicReportEntry { + CalculatableSection(CalculatableSection), + Section(Section), + LiteralRow(LiteralRow), + CalculatedRow(CalculatedRow), + Spacer, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum DynamicReportEntry { + Section(Section), + LiteralRow(LiteralRow), + Spacer, +} + +#[derive(Clone, Debug)] +pub struct CalculatableSection { + pub text: String, + pub id: Option, + pub visible: bool, + pub auto_hide: bool, + pub entries: Vec>, +} + +impl CalculatableSection { + pub fn new( + text: String, + id: Option, + visible: bool, + auto_hide: bool, + entries: Vec, + ) -> Self { + Self { + text, + id, + visible, + auto_hide, + entries: entries.into_iter().map(|e| RefCell::new(e)).collect(), + } } /// Recursively calculate all [CalculatedRow] entries - pub fn calculate(&mut self, report: &DynamicReport) { + pub fn calculate(&mut self, report: &CalculatableDynamicReport) -> Section { + let mut calculated_entries = Vec::new(); + for (entry_idx, entry) in self.entries.iter().enumerate() { let entry_ref = entry.borrow(); match &*entry_ref { - DynamicReportEntry::Section(section) => { - // Clone first, in case calculation needs to take reference to the section - let mut updated_section = section.clone(); - updated_section.calculate(&report); + CalculatableDynamicReportEntry::CalculatableSection(section) => { + let updated_section = section.clone().calculate(&report); drop(entry_ref); // Drop entry_ref so we can borrow mutably let mut entry_mut = self.entries[entry_idx].borrow_mut(); - *entry_mut = DynamicReportEntry::Section(updated_section); + *entry_mut = CalculatableDynamicReportEntry::Section(updated_section.clone()); + + calculated_entries.push(DynamicReportEntry::Section(updated_section)); } - DynamicReportEntry::LiteralRow(_) => (), - DynamicReportEntry::CalculatedRow(row) => { + CalculatableDynamicReportEntry::Section(section) => { + calculated_entries.push(DynamicReportEntry::Section(section.clone())); + } + CalculatableDynamicReportEntry::LiteralRow(row) => { + calculated_entries.push(DynamicReportEntry::LiteralRow(row.clone())); + } + CalculatableDynamicReportEntry::CalculatedRow(row) => { let updated_row = row.calculate(&report); drop(entry_ref); // Drop entry_ref so we can borrow mutably let mut entry_mut = self.entries[entry_idx].borrow_mut(); - *entry_mut = DynamicReportEntry::LiteralRow(updated_row); + *entry_mut = CalculatableDynamicReportEntry::LiteralRow(updated_row.clone()); + + calculated_entries.push(DynamicReportEntry::LiteralRow(updated_row)); } - DynamicReportEntry::Spacer => (), + CalculatableDynamicReportEntry::Spacer => (), } } + + Section { + text: self.text.clone(), + id: self.id.clone(), + visible: self.visible, + auto_hide: self.auto_hide, + entries: calculated_entries, + } } - /// Look up [DynamicReportEntry] by id + /// Look up [CalculatableDynamicReportEntry] by id /// - /// Returns a cloned copy of the [DynamicReportEntry]. - pub fn by_id(&self, id: &str) -> Option { + /// Returns a cloned copy of the [CalculatableDynamicReportEntry]. + pub fn by_id(&self, id: &str) -> Option { // Manually iterate over self.entries rather than self.entries() // To catch the situation where entry is already mutably borrowed for entry in self.entries.iter() { match entry.try_borrow() { Ok(entry) => match &*entry { - DynamicReportEntry::Section(section) => { + CalculatableDynamicReportEntry::CalculatableSection(section) => { if let Some(i) = §ion.id { if i == id { return Some(entry.clone()); @@ -276,15 +319,16 @@ impl Section { return Some(e); } } - DynamicReportEntry::LiteralRow(row) => { + CalculatableDynamicReportEntry::Section(_) => todo!(), + CalculatableDynamicReportEntry::LiteralRow(row) => { if let Some(i) = &row.id { if i == id { return Some(entry.clone()); } } } - DynamicReportEntry::CalculatedRow(_) => (), - DynamicReportEntry::Spacer => (), + CalculatableDynamicReportEntry::CalculatedRow(_) => (), + CalculatableDynamicReportEntry::Spacer => (), }, Err(err) => panic!( "Attempt to call by_id on DynamicReportEntry which is mutably borrowed: {}", @@ -296,11 +340,111 @@ impl Section { None } - /// Calculate the subtotals for this [Section] - pub fn subtotal(&self, report: &DynamicReport) -> Vec { + /// Calculate the subtotals for this [CalculatableSection] + pub fn subtotal(&self, report: &CalculatableDynamicReport) -> Vec { let mut subtotals = vec![0; report.columns.len()]; for entry in self.entries.iter() { match &*entry.borrow() { + CalculatableDynamicReportEntry::CalculatableSection(section) => { + for (col_idx, subtotal) in section.subtotal(report).into_iter().enumerate() { + subtotals[col_idx] += subtotal; + } + } + CalculatableDynamicReportEntry::Section(section) => { + for (col_idx, subtotal) in section.subtotal(report).into_iter().enumerate() { + subtotals[col_idx] += subtotal; + } + } + CalculatableDynamicReportEntry::LiteralRow(row) => { + for (col_idx, subtotal) in row.quantity.iter().enumerate() { + subtotals[col_idx] += subtotal; + } + } + CalculatableDynamicReportEntry::CalculatedRow(_) => (), + CalculatableDynamicReportEntry::Spacer => (), + } + } + subtotals + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Section { + pub text: String, + pub id: Option, + pub visible: bool, + pub auto_hide: bool, + pub entries: Vec, +} + +impl Section { + fn auto_hide_children(&mut self) { + self.entries.retain_mut(|e| match e { + DynamicReportEntry::Section(section) => { + section.auto_hide_children(); + if section.can_auto_hide_self() { + false + } else { + true + } + } + DynamicReportEntry::LiteralRow(row) => { + if row.can_auto_hide() { + false + } else { + true + } + } + DynamicReportEntry::Spacer => true, + }); + } + + fn can_auto_hide_self(&self) -> bool { + self.auto_hide + && self.entries.iter().all(|e| match e { + DynamicReportEntry::Section(section) => section.can_auto_hide_self(), + DynamicReportEntry::LiteralRow(row) => row.can_auto_hide(), + DynamicReportEntry::Spacer => true, + }) + } + + /// Look up [DynamicReportEntry] by id + /// + /// Returns a cloned copy of the [DynamicReportEntry]. + pub fn by_id(&self, id: &str) -> Option { + // Manually iterate over self.entries rather than self.entries() + // To catch the situation where entry is already mutably borrowed + for entry in self.entries.iter() { + match entry { + DynamicReportEntry::Section(section) => { + if let Some(i) = §ion.id { + if i == id { + return Some(entry.clone()); + } + } + if let Some(e) = section.by_id(id) { + return Some(e); + } + } + DynamicReportEntry::LiteralRow(row) => { + if let Some(i) = &row.id { + if i == id { + return Some(entry.clone()); + } + } + } + DynamicReportEntry::Spacer => (), + } + } + + None + } + + /// Calculate the subtotals for this [Section] + pub fn subtotal(&self, report: &CalculatableDynamicReport) -> Vec { + let mut subtotals = vec![0; report.columns.len()]; + for entry in self.entries.iter() { + match entry { DynamicReportEntry::Section(section) => { for (col_idx, subtotal) in section.subtotal(report).into_iter().enumerate() { subtotals[col_idx] += subtotal; @@ -311,7 +455,6 @@ impl Section { subtotals[col_idx] += subtotal; } } - DynamicReportEntry::CalculatedRow(_) => (), DynamicReportEntry::Spacer => (), } } @@ -341,7 +484,7 @@ impl LiteralRow { #[derive(Clone, Debug)] pub struct CalculatedRow { //pub text: String, - pub calculate_fn: fn(report: &DynamicReport) -> LiteralRow, + pub calculate_fn: fn(report: &CalculatableDynamicReport) -> LiteralRow, //pub id: Option, //pub visible: bool, //pub auto_hide: bool, @@ -351,7 +494,7 @@ pub struct CalculatedRow { } impl CalculatedRow { - fn calculate(&self, report: &DynamicReport) -> LiteralRow { + fn calculate(&self, report: &CalculatableDynamicReport) -> LiteralRow { (self.calculate_fn)(report) } } @@ -361,7 +504,7 @@ pub fn entries_for_kind( invert: bool, balances: &Vec<&HashMap>, kinds_for_account: &HashMap>, -) -> Vec { +) -> Vec { // Get accounts of specified kind let mut accounts = kinds_for_account .iter() @@ -393,7 +536,7 @@ pub fn entries_for_kind( heading: false, bordered: false, }; - entries.push(DynamicReportEntry::LiteralRow(entry)); + entries.push(CalculatableDynamicReportEntry::LiteralRow(entry)); } entries diff --git a/src/reporting/executor.rs b/src/reporting/executor.rs index 5c9dd4d..decc709 100644 --- a/src/reporting/executor.rs +++ b/src/reporting/executor.rs @@ -16,23 +16,35 @@ along with this program. If not, see . */ -use super::{calculator::ReportingGraphDependencies, types::{ReportingContext, ReportingProducts, ReportingStep}}; +use tokio::sync::RwLock; + +use super::{ + calculator::ReportingGraphDependencies, + types::{ReportingContext, ReportingProducts, ReportingStep}, +}; #[derive(Debug)] pub enum ReportingExecutionError { - DependencyNotAvailable { message: String } + DependencyNotAvailable { message: String }, } -pub fn execute_steps( +pub async fn execute_steps( steps: Vec>, dependencies: ReportingGraphDependencies, context: &ReportingContext, ) -> Result { - let mut products = ReportingProducts::new(); + let products = RwLock::new(ReportingProducts::new()); for step in steps.iter() { - step.execute(context, &steps, &dependencies, &mut products)?; + // Execute the step + // TODO: Do this in parallel + let mut new_products = step + .execute(context, &steps, &dependencies, &products) + .await?; + + // Insert the new products + products.write().await.append(&mut new_products); } - Ok(products) + Ok(products.into_inner()) } diff --git a/src/reporting/mod.rs b/src/reporting/mod.rs index 1ea2e13..9214bd3 100644 --- a/src/reporting/mod.rs +++ b/src/reporting/mod.rs @@ -48,7 +48,7 @@ impl From for ReportingError { /// Calculate the steps required to generate the requested [ReportingProductId]s and then execute them /// /// Helper function to call [steps_for_targets] followed by [execute_steps]. -pub fn generate_report( +pub async fn generate_report( targets: Vec, context: &ReportingContext, ) -> Result { @@ -56,7 +56,7 @@ pub fn generate_report( let (sorted_steps, dependencies) = steps_for_targets(targets, context)?; // Execute steps - let products = execute_steps(sorted_steps, dependencies, context)?; + let products = execute_steps(sorted_steps, dependencies, context).await?; Ok(products) } diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 7febc34..d836b9c 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -21,7 +21,9 @@ use std::collections::HashMap; use std::fmt::Display; +use async_trait::async_trait; use chrono::Datelike; +use tokio::sync::RwLock; use crate::account_config::kinds_for_account; use crate::reporting::types::{BalancesAt, DateStartDateEndArgs, ReportingProductId, Transactions}; @@ -33,7 +35,8 @@ use crate::QuantityInt; use super::calculator::ReportingGraphDependencies; use super::dynamic_report::{ - entries_for_kind, CalculatedRow, DynamicReport, DynamicReportEntry, LiteralRow, Section, + entries_for_kind, CalculatableDynamicReport, CalculatableDynamicReportEntry, + CalculatableSection, CalculatedRow, LiteralRow, }; use super::executor::ReportingExecutionError; use super::types::{ @@ -105,6 +108,7 @@ impl Display for AllTransactionsExceptEarningsToEquity { } } +#[async_trait] impl ReportingStep for AllTransactionsExceptEarningsToEquity { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -123,13 +127,15 @@ impl ReportingStep for AllTransactionsExceptEarningsToEquity { }] } - fn execute( + async fn execute( &self, _context: &ReportingContext, _steps: &Vec>, dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + products: &RwLock, + ) -> Result { + let products = products.read().await; + // Get all dependencies let step_dependencies = dependencies.dependencies_for_step(&self.id()); @@ -139,7 +145,8 @@ impl ReportingStep for AllTransactionsExceptEarningsToEquity { for (product_id, product) in products.map().iter().rev() { if step_dependencies.iter().any(|d| d.product == *product_id) { // Store the result - products.insert( + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: self.id().name, kind: product_kind, @@ -147,8 +154,7 @@ impl ReportingStep for AllTransactionsExceptEarningsToEquity { }, product.clone(), ); - - return Ok(()); + return Ok(result); } } @@ -197,6 +203,7 @@ impl Display for AllTransactionsIncludingEarningsToEquity { } } +#[async_trait] impl ReportingStep for AllTransactionsIncludingEarningsToEquity { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -229,13 +236,15 @@ impl ReportingStep for AllTransactionsIncludingEarningsToEquity { ] } - fn execute( + async fn execute( &self, _context: &ReportingContext, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + products: &RwLock, + ) -> Result { + let products = products.read().await; + // Get opening balances from AllTransactionsExceptEarningsToEquity let opening_balances = products .get_or_err(&ReportingProductId { @@ -280,7 +289,8 @@ impl ReportingStep for AllTransactionsIncludingEarningsToEquity { ); // Store result - products.insert( + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: self.id().name, kind: ReportingProductKind::BalancesAt, @@ -288,8 +298,7 @@ impl ReportingStep for AllTransactionsIncludingEarningsToEquity { }, Box::new(balances), ); - - Ok(()) + Ok(result) } } @@ -326,6 +335,7 @@ impl Display for BalanceSheet { } } +#[async_trait] impl ReportingStep for BalanceSheet { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -350,13 +360,15 @@ impl ReportingStep for BalanceSheet { result } - fn execute( + async fn execute( &self, context: &ReportingContext, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + products: &RwLock, + ) -> Result { + let products = products.read().await; + // Get balances for each period let mut balances: Vec<&HashMap> = Vec::new(); for date_args in self.args.dates.iter() { @@ -371,14 +383,14 @@ impl ReportingStep for BalanceSheet { // Get names of all balance sheet accounts let kinds_for_account = - kinds_for_account(context.db_connection.get_account_configurations()); + kinds_for_account(context.db_connection.get_account_configurations().await); // Init report - let mut report = DynamicReport::new( + let report = CalculatableDynamicReport::new( "Balance sheet".to_string(), self.args.dates.iter().map(|d| d.date.to_string()).collect(), vec![ - DynamicReportEntry::Section(Section::new( + CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new( "Assets".to_string(), Some("assets".to_string()), true, @@ -386,23 +398,25 @@ impl ReportingStep for BalanceSheet { { let mut entries = entries_for_kind("drcr.asset", false, &balances, &kinds_for_account); - entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow { - calculate_fn: |report| LiteralRow { - text: "Total assets".to_string(), - quantity: report.subtotal_for_id("assets"), - id: Some("total_assets".to_string()), - visible: true, - auto_hide: false, - link: None, - heading: true, - bordered: true, + entries.push(CalculatableDynamicReportEntry::CalculatedRow( + CalculatedRow { + calculate_fn: |report| LiteralRow { + text: "Total assets".to_string(), + quantity: report.subtotal_for_id("assets"), + id: Some("total_assets".to_string()), + visible: true, + auto_hide: false, + link: None, + heading: true, + bordered: true, + }, }, - })); + )); entries }, )), - DynamicReportEntry::Spacer, - DynamicReportEntry::Section(Section::new( + CalculatableDynamicReportEntry::Spacer, + CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new( "Liabilities".to_string(), Some("liabilities".to_string()), true, @@ -410,23 +424,25 @@ impl ReportingStep for BalanceSheet { { let mut entries = entries_for_kind("drcr.liability", true, &balances, &kinds_for_account); - entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow { - calculate_fn: |report| LiteralRow { - text: "Total liabilities".to_string(), - quantity: report.subtotal_for_id("liabilities"), - id: Some("total_liabilities".to_string()), - visible: true, - auto_hide: false, - link: None, - heading: true, - bordered: true, + entries.push(CalculatableDynamicReportEntry::CalculatedRow( + CalculatedRow { + calculate_fn: |report| LiteralRow { + text: "Total liabilities".to_string(), + quantity: report.subtotal_for_id("liabilities"), + id: Some("total_liabilities".to_string()), + visible: true, + auto_hide: false, + link: None, + heading: true, + bordered: true, + }, }, - })); + )); entries }, )), - DynamicReportEntry::Spacer, - DynamicReportEntry::Section(Section::new( + CalculatableDynamicReportEntry::Spacer, + CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new( "Equity".to_string(), Some("equity".to_string()), true, @@ -434,29 +450,32 @@ impl ReportingStep for BalanceSheet { { let mut entries = entries_for_kind("drcr.equity", true, &balances, &kinds_for_account); - entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow { - calculate_fn: |report| LiteralRow { - text: "Total equity".to_string(), - quantity: report.subtotal_for_id("equity"), - id: Some("total_equity".to_string()), - visible: true, - auto_hide: false, - link: None, - heading: true, - bordered: true, + entries.push(CalculatableDynamicReportEntry::CalculatedRow( + CalculatedRow { + calculate_fn: |report| LiteralRow { + text: "Total equity".to_string(), + quantity: report.subtotal_for_id("equity"), + id: Some("total_equity".to_string()), + visible: true, + auto_hide: false, + link: None, + heading: true, + bordered: true, + }, }, - })); + )); entries }, )), ], ); - report.calculate(); + let mut report = report.calculate(); report.auto_hide(); // Store the result - products.insert( + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: "BalanceSheet", kind: ReportingProductKind::Generic, @@ -464,8 +483,7 @@ impl ReportingStep for BalanceSheet { }, Box::new(report), ); - - Ok(()) + Ok(result) } } @@ -498,6 +516,7 @@ impl Display for CalculateIncomeTax { } } +#[async_trait] impl ReportingStep for CalculateIncomeTax { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -540,20 +559,21 @@ impl ReportingStep for CalculateIncomeTax { } } - fn execute( + async fn execute( &self, _context: &ReportingContext, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + _products: &RwLock, + ) -> Result { eprintln!("Stub: CalculateIncomeTax.execute"); let transactions = Transactions { transactions: Vec::new(), }; - products.insert( + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: self.id().name, kind: ReportingProductKind::Transactions, @@ -561,8 +581,7 @@ impl ReportingStep for CalculateIncomeTax { }, Box::new(transactions), ); - - Ok(()) + Ok(result) } } @@ -601,6 +620,7 @@ impl Display for CombineOrdinaryTransactions { } } +#[async_trait] impl ReportingStep for CombineOrdinaryTransactions { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -627,13 +647,15 @@ impl ReportingStep for CombineOrdinaryTransactions { ] } - fn execute( + async fn execute( &self, _context: &ReportingContext, _steps: &Vec>, dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + products: &RwLock, + ) -> Result { + let products = products.read().await; + // Sum balances of all dependencies let mut balances = BalancesAt { @@ -653,7 +675,8 @@ impl ReportingStep for CombineOrdinaryTransactions { } // Store result - products.insert( + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: self.id().name, kind: ReportingProductKind::BalancesAt, @@ -661,8 +684,7 @@ impl ReportingStep for CombineOrdinaryTransactions { }, Box::new(balances), ); - - Ok(()) + Ok(result) } } @@ -699,6 +721,7 @@ impl Display for CurrentYearEarningsToEquity { } } +#[async_trait] impl ReportingStep for CurrentYearEarningsToEquity { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -722,13 +745,14 @@ impl ReportingStep for CurrentYearEarningsToEquity { }] } - fn execute( + async fn execute( &self, context: &ReportingContext, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + products: &RwLock, + ) -> Result { + let products = products.read().await; let eofy_date = get_eofy(&self.args.date, &context.eofy_date); // Get balances for this financial year @@ -746,7 +770,7 @@ impl ReportingStep for CurrentYearEarningsToEquity { // Get income and expense accounts let kinds_for_account = - kinds_for_account(context.db_connection.get_account_configurations()); + kinds_for_account(context.db_connection.get_account_configurations().await); // Transfer income and expense balances to current year earnings let mut transactions = Transactions { @@ -789,7 +813,8 @@ impl ReportingStep for CurrentYearEarningsToEquity { } // Store product - products.insert( + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: self.id().name, kind: ReportingProductKind::Transactions, @@ -797,8 +822,7 @@ impl ReportingStep for CurrentYearEarningsToEquity { }, Box::new(transactions), ); - - Ok(()) + Ok(result) } } @@ -835,6 +859,7 @@ impl Display for DBBalances { } } +#[async_trait] impl ReportingStep for DBBalances { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -844,19 +869,21 @@ impl ReportingStep for DBBalances { } } - fn execute( + async fn execute( &self, context: &ReportingContext, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + _products: &RwLock, + ) -> Result { // Get balances from DB let balances = BalancesAt { - balances: context.db_connection.get_balances(self.args.date), + balances: context.db_connection.get_balances(self.args.date).await, }; - products.insert( + // Store result + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: self.id().name, kind: ReportingProductKind::BalancesAt, @@ -864,8 +891,7 @@ impl ReportingStep for DBBalances { }, Box::new(balances), ); - - Ok(()) + Ok(result) } } @@ -902,6 +928,7 @@ impl Display for IncomeStatement { } } +#[async_trait] impl ReportingStep for IncomeStatement { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -926,13 +953,15 @@ impl ReportingStep for IncomeStatement { result } - fn execute( + async fn execute( &self, context: &ReportingContext, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + products: &RwLock, + ) -> Result { + let products = products.read().await; + // Get balances for each period let mut balances: Vec<&HashMap> = Vec::new(); for date_args in self.args.dates.iter() { @@ -947,10 +976,10 @@ impl ReportingStep for IncomeStatement { // Get names of all income statement accounts let kinds_for_account = - kinds_for_account(context.db_connection.get_account_configurations()); + kinds_for_account(context.db_connection.get_account_configurations().await); // Init report - let mut report = DynamicReport::new( + let report = CalculatableDynamicReport::new( "Income statement".to_string(), self.args .dates @@ -958,7 +987,7 @@ impl ReportingStep for IncomeStatement { .map(|d| d.date_end.to_string()) .collect(), vec![ - DynamicReportEntry::Section(Section::new( + CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new( "Income".to_string(), Some("income".to_string()), true, @@ -966,23 +995,25 @@ impl ReportingStep for IncomeStatement { { let mut entries = entries_for_kind("drcr.income", true, &balances, &kinds_for_account); - entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow { - calculate_fn: |report| LiteralRow { - text: "Total income".to_string(), - quantity: report.subtotal_for_id("income"), - id: Some("total_income".to_string()), - visible: true, - auto_hide: false, - link: None, - heading: true, - bordered: true, + entries.push(CalculatableDynamicReportEntry::CalculatedRow( + CalculatedRow { + calculate_fn: |report| LiteralRow { + text: "Total income".to_string(), + quantity: report.subtotal_for_id("income"), + id: Some("total_income".to_string()), + visible: true, + auto_hide: false, + link: None, + heading: true, + bordered: true, + }, }, - })); + )); entries }, )), - DynamicReportEntry::Spacer, - DynamicReportEntry::Section(Section::new( + CalculatableDynamicReportEntry::Spacer, + CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new( "Expenses".to_string(), Some("expenses".to_string()), true, @@ -990,23 +1021,25 @@ impl ReportingStep for IncomeStatement { { let mut entries = entries_for_kind("drcr.expense", false, &balances, &kinds_for_account); - entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow { - calculate_fn: |report| LiteralRow { - text: "Total expenses".to_string(), - quantity: report.subtotal_for_id("expenses"), - id: Some("total_expenses".to_string()), - visible: true, - auto_hide: false, - link: None, - heading: true, - bordered: true, + entries.push(CalculatableDynamicReportEntry::CalculatedRow( + CalculatedRow { + calculate_fn: |report| LiteralRow { + text: "Total expenses".to_string(), + quantity: report.subtotal_for_id("expenses"), + id: Some("total_expenses".to_string()), + visible: true, + auto_hide: false, + link: None, + heading: true, + bordered: true, + }, }, - })); + )); entries }, )), - DynamicReportEntry::Spacer, - DynamicReportEntry::CalculatedRow(CalculatedRow { + CalculatableDynamicReportEntry::Spacer, + CalculatableDynamicReportEntry::CalculatedRow(CalculatedRow { calculate_fn: |report| LiteralRow { text: "Net surplus (deficit)".to_string(), quantity: report @@ -1026,11 +1059,12 @@ impl ReportingStep for IncomeStatement { ], ); - report.calculate(); + let mut report = report.calculate(); report.auto_hide(); // Store the result - products.insert( + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: "IncomeStatement", kind: ReportingProductKind::Generic, @@ -1038,8 +1072,7 @@ impl ReportingStep for IncomeStatement { }, Box::new(report), ); - - Ok(()) + Ok(result) } } @@ -1076,6 +1109,7 @@ impl Display for PostUnreconciledStatementLines { } } +#[async_trait] impl ReportingStep for PostUnreconciledStatementLines { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -1085,20 +1119,22 @@ impl ReportingStep for PostUnreconciledStatementLines { } } - fn execute( + async fn execute( &self, _context: &ReportingContext, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + _products: &RwLock, + ) -> Result { eprintln!("Stub: PostUnreconciledStatementLines.execute"); let transactions = Transactions { transactions: Vec::new(), }; - products.insert( + // Store result + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: self.id().name, kind: ReportingProductKind::Transactions, @@ -1106,8 +1142,7 @@ impl ReportingStep for PostUnreconciledStatementLines { }, Box::new(transactions), ); - - Ok(()) + Ok(result) } } @@ -1144,6 +1179,7 @@ impl Display for RetainedEarningsToEquity { } } +#[async_trait] impl ReportingStep for RetainedEarningsToEquity { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -1167,13 +1203,14 @@ impl ReportingStep for RetainedEarningsToEquity { }] } - fn execute( + async fn execute( &self, context: &ReportingContext, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + products: &RwLock, + ) -> Result { + let products = products.read().await; let eofy_date = get_eofy(&self.args.date, &context.eofy_date); let last_eofy_date = eofy_date.with_year(eofy_date.year() - 1).unwrap(); @@ -1191,7 +1228,7 @@ impl ReportingStep for RetainedEarningsToEquity { // Get income and expense accounts let kinds_for_account = - kinds_for_account(context.db_connection.get_account_configurations()); + kinds_for_account(context.db_connection.get_account_configurations().await); // Transfer income and expense balances to retained earnings let mut transactions = Transactions { @@ -1234,7 +1271,8 @@ impl ReportingStep for RetainedEarningsToEquity { } // Store product - products.insert( + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: self.id().name, kind: ReportingProductKind::Transactions, @@ -1242,7 +1280,6 @@ impl ReportingStep for RetainedEarningsToEquity { }, Box::new(transactions), ); - - Ok(()) + Ok(result) } } diff --git a/src/reporting/types.rs b/src/reporting/types.rs index 1cbfb95..d1e6428 100644 --- a/src/reporting/types.rs +++ b/src/reporting/types.rs @@ -20,12 +20,14 @@ use std::collections::HashMap; use std::fmt::{Debug, Display}; use std::hash::Hash; +use async_trait::async_trait; use chrono::NaiveDate; use downcast_rs::Downcast; use dyn_clone::DynClone; use dyn_eq::DynEq; use dyn_hash::DynHash; use indexmap::IndexMap; +use tokio::sync::RwLock; use crate::db::DbConnection; use crate::transaction::TransactionWithPostings; @@ -159,7 +161,7 @@ pub enum ReportingProductKind { } /// Represents the result of a [ReportingStep] -pub trait ReportingProduct: Debug + Downcast + DynClone {} +pub trait ReportingProduct: Debug + Downcast + DynClone + Send + Sync {} downcast_rs::impl_downcast!(ReportingProduct); dyn_clone::clone_trait_object!(ReportingProduct); @@ -205,14 +207,25 @@ impl ReportingProducts { } } + /// Returns a reference to the underlying [IndexMap] pub fn map(&self) -> &IndexMap> { &self.map } + /// Insert a key-value pair in the map + /// + /// See [IndexMap::insert]. pub fn insert(&mut self, key: ReportingProductId, value: Box) { self.map.insert(key, value); } + /// Moves all key-value pairs from `other` into `self`, leaving `other` empty + /// + /// See [IndexMap::append]. + pub fn append(&mut self, other: &mut ReportingProducts) { + self.map.append(&mut other.map); + } + pub fn get_or_err( &self, key: &ReportingProductId, @@ -260,7 +273,8 @@ impl Display for ReportingStepId { } /// Represents a step in a reporting job -pub trait ReportingStep: Debug + Display + Downcast { +#[async_trait] +pub trait ReportingStep: Debug + Display + Downcast + Send + Sync { /// Get the [ReportingStepId] for this [ReportingStep] fn id(&self) -> ReportingStepId; @@ -293,14 +307,16 @@ pub trait ReportingStep: Debug + Display + Downcast { } /// Called to generate the [ReportingProduct] for this [ReportingStep] + /// + /// Returns a [ReportingProducts] containing (only) the new [ReportingProduct]s. #[allow(unused_variables)] - fn execute( + async fn execute( &self, context: &ReportingContext, steps: &Vec>, dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + products: &RwLock, + ) -> Result { todo!("{}", self); } } @@ -311,7 +327,10 @@ downcast_rs::impl_downcast!(ReportingStep); // REPORTING STEP ARGUMENTS /// Represents arguments to a [ReportingStep] -pub trait ReportingStepArgs: Debug + Display + Downcast + DynClone + DynEq + DynHash {} +pub trait ReportingStepArgs: + Debug + Display + Downcast + DynClone + DynEq + DynHash + Send + Sync +{ +} downcast_rs::impl_downcast!(ReportingStepArgs); dyn_clone::clone_trait_object!(ReportingStepArgs);